Skip to content

Commit

Permalink
Fix bug ImmediateExecutionModel when using Crypto (#8355)
Browse files Browse the repository at this point in the history
* First draft of the solution

* Add missing algorithm

* Improve regression tests

* Nit change

* Address requests

* Nit change

* Address minor requests

* Nit changes

* Nit suggestion
  • Loading branch information
Marinovsky authored Oct 7, 2024
1 parent 16c893d commit 0e61415
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 6 deletions.
119 changes: 119 additions & 0 deletions Algorithm.CSharp/ImmediateExecutionModelWorksWithBinanceFeeModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Interfaces;
using QuantConnect.Orders;
using System;
using System.Collections.Generic;

namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Regression algorithm to test ImmediateExecutionModel places orders with the
/// correct quantity (taking into account the fee's) so that the fill quantity
/// is the expected one.
/// </summary>
public class ImmediateExecutionModelWorksWithBinanceFeeModel: QCAlgorithm, IRegressionAlgorithmDefinition
{
public override void Initialize()
{
SetStartDate(2022, 12, 13);
SetEndDate(2022, 12, 14);
SetAccountCurrency("BUSD");
SetCash("BUSD", 100000, 1);

UniverseSettings.Resolution = Resolution.Minute;

var symbols = new List<Symbol>() { QuantConnect.Symbol.Create("BTCBUSD", SecurityType.Crypto, Market.Binance) };
SetUniverseSelection(new ManualUniverseSelectionModel(symbols));
SetAlpha(new ConstantAlphaModel(InsightType.Price, InsightDirection.Up, TimeSpan.FromMinutes(20), 0.025, null));

SetPortfolioConstruction(new EqualWeightingPortfolioConstructionModel(Resolution.Minute));
SetExecution(new ImmediateExecutionModel());
SetBrokerageModel(Brokerages.BrokerageName.Binance, AccountType.Margin);
}

public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status == OrderStatus.Filled)
{
if (Math.Abs(orderEvent.Quantity - 5.8m) > 0.01m)
{
throw new RegressionTestException($"The expected quantity was {5.8m} but the quantity from the order was {orderEvent.Quantity}");
}
}
}

public bool CanRunLocally => true;

/// <summary>
/// This is used by the regression test system to indicate which languages this algorithm is written in.
/// </summary>
public List<Language> Languages { get; } = new() { Language.CSharp, Language.Python };

/// <summary>
/// Data Points count of all timeslices of algorithm
/// </summary>
public long DataPoints => 2882;

/// <summary>
/// Data Points count of the algorithm history
/// </summary>
public int AlgorithmHistoryDataPoints => 60;

/// <summary>
/// Final status of the algorithm
/// </summary>
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;

/// <summary>
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "1"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "0%"},
{"Drawdown", "0%"},
{"Expectancy", "0"},
{"Start Equity", "100000.00"},
{"End Equity", "103411.39"},
{"Net Profit", "0%"},
{"Sharpe Ratio", "0"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0"},
{"Beta", "0"},
{"Annual Standard Deviation", "0"},
{"Annual Variance", "0"},
{"Information Ratio", "0"},
{"Tracking Error", "0"},
{"Treynor Ratio", "0"},
{"Total Fees", "BUSD99.75"},
{"Estimated Strategy Capacity", "BUSD600000.00"},
{"Lowest Capacity Asset", "BTCBUSD 18N"},
{"Portfolio Turnover", "48.18%"},
{"OrderListHash", "2ad07f12d7c80fd4a904269d62794e9e"}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# region imports
from AlgorithmImports import *
from Execution.ImmediateExecutionModel import ImmediateExecutionModel
from QuantConnect.Orders import OrderEvent
# endregion

### <summary>
### Regression algorithm to test ImmediateExecutionModel places orders with the
### correct quantity (taking into account the fee's) so that the fill quantity
### is the expected one.
### </summary>
class ImmediateExecutionModelWorksWithBinanceFeeModel(QCAlgorithm):

def Initialize(self):
# *** initial configurations and backtest ***
self.SetStartDate(2022, 12, 13) # Set Start Date
self.SetEndDate(2022, 12, 14) # Set End Date
self.SetAccountCurrency("BUSD") # Set Account Currency
self.SetCash("BUSD", 100000, 1) # Set Strategy Cash

self.universe_settings.resolution = Resolution.MINUTE

symbols = [ Symbol.create("BTCBUSD", SecurityType.CRYPTO, Market.BINANCE) ]

# set algorithm framework models
self.set_universe_selection(ManualUniverseSelectionModel(symbols))
self.set_alpha(ConstantAlphaModel(InsightType.PRICE, InsightDirection.UP, timedelta(minutes = 20), 0.025, None))

self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel(Resolution.MINUTE))
self.set_execution(ImmediateExecutionModel())


self.SetBrokerageModel(BrokerageName.Binance, AccountType.Margin)

def on_order_event(self, order_event: OrderEvent) -> None:
if order_event.status == OrderStatus.FILLED:
if abs(order_event.quantity - 5.8) > 0.01:
raise Exception(f"The expected quantity was 5.8 but the quantity from the order was {order_event.quantity}")
3 changes: 2 additions & 1 deletion Algorithm/Execution/ImmediateExecutionModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public override void Execute(QCAlgorithm algorithm, IPortfolioTarget[] targets)
var security = algorithm.Securities[target.Symbol];

// calculate remaining quantity to be ordered
var quantity = OrderSizing.GetUnorderedQuantity(algorithm, target, security);
var quantity = OrderSizing.GetUnorderedQuantity(algorithm, target, security, true);

if (quantity != 0)
{
if (security.BuyingPowerModel.AboveMinimumOrderMarginPortfolioPercentage(security, quantity,
Expand Down
3 changes: 2 additions & 1 deletion Algorithm/Execution/ImmediateExecutionModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def execute(self, algorithm, targets):
for target in self.targets_collection.order_by_margin_impact(algorithm):
security = algorithm.securities[target.symbol]
# calculate remaining quantity to be ordered
quantity = OrderSizing.get_unordered_quantity(algorithm, target, security)
quantity = OrderSizing.get_unordered_quantity(algorithm, target, security, True)

if quantity != 0:
above_minimum_portfolio = BuyingPowerModelExtensions.above_minimum_order_margin_portfolio_percentage(
security.buying_power_model,
Expand Down
14 changes: 14 additions & 0 deletions Common/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
using QuantConnect.Securities.Option;
using QuantConnect.Statistics;
using Newtonsoft.Json.Linq;
using QuantConnect.Orders.Fees;

namespace QuantConnect
{
Expand Down Expand Up @@ -4378,6 +4379,19 @@ public static bool IsCustomDataType(Symbol symbol, Type type)
return type.Namespace != typeof(Bar).Namespace || Extensions.GetCustomDataTypeFromSymbols(new Symbol[] { symbol }) != null;
}

/// <summary>
/// Returns the amount of fee's charged by executing a market order with the given arguments
/// </summary>
/// <param name="security">Security for which we would like to make a market order</param>
/// <param name="quantity">Quantity of the security we are seeking to trade</param>
/// <param name="time">Time the order was placed</param>
/// <param name="marketOrder">This out parameter will contain the market order constructed</param>
public static CashAmount GetMarketOrderFees(Security security, decimal quantity, DateTime time, out MarketOrder marketOrder)
{
marketOrder = new MarketOrder(security.Symbol, quantity, time);
return security.FeeModel.GetOrderFee(new OrderFeeParameters(security, marketOrder)).Value;
}

private static Symbol ConvertToSymbol(PyObject item, bool dispose)
{
if (PyString.IsStringType(item))
Expand Down
16 changes: 15 additions & 1 deletion Common/Orders/OrderSizing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using QuantConnect.Securities.Crypto;

namespace QuantConnect.Orders
{
Expand Down Expand Up @@ -84,14 +85,27 @@ public static decimal GetUnorderedQuantity(IAlgorithm algorithm, IPortfolioTarge
/// <param name="algorithm">The algorithm instance</param>
/// <param name="target">The portfolio target</param>
/// <param name="security">The target security</param>
/// <param name="accountForFees">True for taking into account the fee's in the order quantity.
/// False, otherwise.</param>
/// <returns>The signed remaining quantity to be ordered</returns>
public static decimal GetUnorderedQuantity(IAlgorithm algorithm, IPortfolioTarget target, Security security)
public static decimal GetUnorderedQuantity(IAlgorithm algorithm, IPortfolioTarget target, Security security, bool accountForFees = false)
{
var holdings = security.Holdings.Quantity;
var openOrderQuantity = algorithm.Transactions.GetOpenOrderTickets(target.Symbol)
.Aggregate(0m, (d, t) => d + t.Quantity - t.QuantityFilled);
var quantity = target.Quantity - holdings - openOrderQuantity;

// Adjust the order quantity taking into account the fee's
if (accountForFees && security.Symbol.SecurityType == SecurityType.Crypto && quantity > 0)
{
var orderFee = Extensions.GetMarketOrderFees(security, quantity, algorithm.UtcTime, out _);
var baseCurrency = ((Crypto)security).BaseCurrency.Symbol;
if (baseCurrency == orderFee.Currency)
{
quantity += orderFee.Amount;
}
}

return AdjustByLotSize(security, quantity);
}

Expand Down
4 changes: 1 addition & 3 deletions Common/Securities/SecurityHolding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -485,13 +485,11 @@ public virtual decimal TotalCloseProfit(bool includeFees = true, decimal? exitPr
}

// this is in the account currency
var marketOrder = new MarketOrder(_security.Symbol, -quantityToUse, _security.LocalTime.ConvertToUtc(_security.Exchange.TimeZone));
var orderFee = Extensions.GetMarketOrderFees(_security, -quantityToUse, _security.LocalTime.ConvertToUtc(_security.Exchange.TimeZone), out var marketOrder);

var feesInAccountCurrency = 0m;
if (includeFees)
{
var orderFee = _security.FeeModel.GetOrderFee(
new OrderFeeParameters(_security, marketOrder)).Value;
feesInAccountCurrency = _currencyConverter.ConvertToAccountCurrency(orderFee).Amount;
}

Expand Down

0 comments on commit 0e61415

Please sign in to comment.