Skip to content

Commit

Permalink
Fix bug in repeated count and other misc
Browse files Browse the repository at this point in the history
  • Loading branch information
LouisSzeto committed Jan 14, 2025
1 parent 8c7c354 commit ae864e1
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 67 deletions.
45 changes: 23 additions & 22 deletions Algorithm.CSharp/InteractiveBrokersTieredFeeModelAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public override void OnOrderEvent(OrderEvent orderEvent)
{
// Assert if the monthly traded volume is correct in the fee model.
_monthlyTradedVolume += orderEvent.AbsoluteFillQuantity;
var modelTradedVolume = _feeModel.MonthTradedVolume[SecurityType.Equity];
// Month volume will update only at the next scan, so we add this fill quantity.
var modelTradedVolume = _feeModel.MonthTradedVolume[SecurityType.Equity] + orderEvent.AbsoluteFillQuantity;
if (_monthlyTradedVolume != modelTradedVolume)
{
throw new Exception($"Monthly traded volume is incorrect - Actual: {_monthlyTradedVolume} - Model: {modelTradedVolume}");
Expand Down Expand Up @@ -116,32 +117,32 @@ public override void OnOrderEvent(OrderEvent orderEvent)
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "36"},
{"Average Win", "0.00%"},
{"Average Loss", "0.00%"},
{"Compounding Annual Return", "-2.237%"},
{"Drawdown", "0.000%"},
{"Expectancy", "-0.486"},
{"Average Win", "0.02%"},
{"Average Loss", "-0.05%"},
{"Compounding Annual Return", "-26.786%"},
{"Drawdown", "0.600%"},
{"Expectancy", "-0.658"},
{"Start Equity", "1000000000"},
{"End Equity", "999762433.94"},
{"Net Profit", "-0.024%"},
{"Sharpe Ratio", "-8.397"},
{"Sortino Ratio", "-11.384"},
{"End Equity", "996730872.35"},
{"Net Profit", "-0.327%"},
{"Sharpe Ratio", "-7.559"},
{"Sortino Ratio", "-9.624"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "75%"},
{"Win Rate", "25%"},
{"Profit-Loss Ratio", "1.06"},
{"Alpha", "-0.035"},
{"Beta", "0.009"},
{"Annual Standard Deviation", "0.003"},
{"Annual Variance", "0"},
{"Information Ratio", "-5.78"},
{"Tracking Error", "0.269"},
{"Treynor Ratio", "-2.319"},
{"Total Fees", "$185772.29"},
{"Estimated Strategy Capacity", "$11000000.00"},
{"Profit-Loss Ratio", "0.37"},
{"Alpha", "-0.358"},
{"Beta", "0.098"},
{"Annual Standard Deviation", "0.027"},
{"Annual Variance", "0.001"},
{"Information Ratio", "-7.11"},
{"Tracking Error", "0.245"},
{"Treynor Ratio", "-2.105"},
{"Total Fees", "$4126697.73"},
{"Estimated Strategy Capacity", "$180000000.00"},
{"Lowest Capacity Asset", "AIG R735QTJ8XC9X"},
{"Portfolio Turnover", "2.37%"},
{"OrderListHash", "d35a4e91c145a100d4bffb7c0fc0ff35"}
{"Portfolio Turnover", "40.56%"},
{"OrderListHash", "bd43a7c7f61e734a7dbc06180af8fc36"}
};
}
}
7 changes: 4 additions & 3 deletions Algorithm.Python/InteractiveBrokersTieredFeeModelAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ def on_data(self, slice: Slice) -> None:
self.market_on_close_order(self.bac, -60000)

def on_order_event(self, order_event: OrderEvent) -> None:
if order_event.status != OrderStatus.FILLED:
if order_event.status == OrderStatus.FILLED:
# Assert if the monthly traded volume is correct in the fee model.
self.monthly_traded_volume += order_event.absolute_fill_quantity
model_traded_volume = self.fee_model.monthly_trade_volume[SecurityType.EQUITY]
# Month volume will update only at the next scan, so we add this fill quantity.
model_traded_volume = self.fee_model.monthly_trade_volume[SecurityType.EQUITY] + order_event.absolute_fill_quantity
if self.monthly_traded_volume != model_traded_volume:
raise Exception(f"Monthly traded volume is incorrect - Actual: {self.monthly_traded_volume} - Model: {model_traded_volume}")
raise Exception(f"Monthly traded volume is incorrect - Actual: {self.monthly_traded_volume} - Model: {model_traded_volume}")
9 changes: 5 additions & 4 deletions Common/Orders/Fees/InteractiveBrokersFeeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ internal static void ProcessCryptoRateSchedule(decimal monthlyCryptoTradeAmountI
internal static decimal CalculateForexFee(Security security, Order order, decimal forexCommissionRate,
decimal forexMinimumOrderFee, out decimal fee, out string currency)
{
// get the total order value in the account currency
// get the total order value in USD.
var totalOrderValue = Math.Abs(order.GetValue(security));
var baseFee = forexCommissionRate*totalOrderValue;

Expand Down Expand Up @@ -270,8 +270,9 @@ internal static void CalculateEquityFee(decimal quantity, decimal tradeValue, st
/// </summary>
internal static void CalculateCfdFee(Security security, Order order, out decimal fee, out string currency)
{
var value = order.AbsoluteQuantity * order.Price;
fee = 0.00002m * value; // 0.002%
var securityPrice = order.Quantity > 0 ? security.AskPrice : security.BidPrice;
var value = order.AbsoluteQuantity * (securityPrice == 0m ? security.Price : securityPrice);
fee = 0.0001m * value; // 0.01%
currency = security.QuoteCurrency.Symbol;

var minimumFee = security.QuoteCurrency.Symbol switch
Expand All @@ -294,7 +295,7 @@ internal static decimal CalculateCryptoFee(Security security, Order order, decim
var totalTradeValue = Math.Abs(order.GetValue(security));
var cryptoFee = cryptoCommissionRate*totalTradeValue;
// 1% maximum fee
fee = Math.Max(Math.Min(totalTradeValue * 0.01m, cryptoFee), cryptoMinimumOrderFee);
fee = Math.Max(Math.Min(totalTradeValue * 0.01m, cryptoMinimumOrderFee), cryptoFee);
// IB Crypto fees are all in USD
currency = Currencies.USD;

Expand Down
30 changes: 25 additions & 5 deletions Common/Orders/Fees/InteractiveBrokersTieredFeeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class InteractiveBrokersTieredFeeModel : FeeModel
// List of Option exchanges susceptible to pay ORF regulatory fee.
private static readonly List<string> _optionExchangesOrfFee = new() { Market.CBOE, Market.USA };
private Dictionary<SecurityType, decimal> _monthlyTradeVolume;
private Dictionary<Order, decimal> _volumeByOrder = new();

/// <summary>
/// The traded volume by security type for selecting the corresponding tier on the current month.
Expand Down Expand Up @@ -85,6 +86,20 @@ public InteractiveBrokersTieredFeeModel(decimal monthlyEquityTradeVolume = 0, de
};
}

private void UpdateMonthVolume()
{
foreach (var (order, volume) in new Dictionary<Order, decimal>(_volumeByOrder))
{
// Tier only changed by the filled volume.
if (order.Status == OrderStatus.Filled)
{
_monthlyTradeVolume[order.SecurityType] += volume;
// Remove the processed order.
_volumeByOrder.Remove(order);
}
}
}

/// <summary>
/// Reprocess the rate schedule based on the current traded volume in various assets.
/// </summary>
Expand Down Expand Up @@ -117,10 +132,13 @@ public override OrderFee GetOrderFee(OrderFeeParameters parameters)
var order = parameters.Order;
var security = parameters.Security;

// Update the monthly volume with filled quantity.
UpdateMonthVolume();
// Reset monthly trade value tracker when month rollover.
if (_lastOrderTime.Month != order.Time.Month && _lastOrderTime != DateTime.MinValue)
{
_monthlyTradeVolume = _monthlyTradeVolume.ToDictionary(kvp => kvp.Key, _ => 0m);
_volumeByOrder.Clear();
}
// Reprocess the rate schedule based on the current traded volume in various assets.
ReprocessRateSchedule(_monthlyTradeVolume[SecurityType.Equity], _monthlyTradeVolume[SecurityType.Future], _monthlyTradeVolume[SecurityType.Forex],
Expand All @@ -147,7 +165,8 @@ public override OrderFee GetOrderFee(OrderFeeParameters parameters)
{
case SecurityType.Forex:
// Update the monthly value traded
_monthlyTradeVolume[SecurityType.Forex] += InteractiveBrokersFeeHelper.CalculateForexFee(security, order, _forexCommissionRate, _forexMinimumOrderFee, out feeResult, out feeCurrency);
var forexTradedValue = InteractiveBrokersFeeHelper.CalculateForexFee(security, order, _forexCommissionRate, _forexMinimumOrderFee, out feeResult, out feeCurrency);
_volumeByOrder[order] = forexTradedValue;
break;

case SecurityType.Option:
Expand All @@ -167,14 +186,14 @@ public override OrderFee GetOrderFee(OrderFeeParameters parameters)
feeResult += regulatory + transaction + clearing;

// Update the monthly value traded
_monthlyTradeVolume[SecurityType.Option] += quantity * orderPrice;
_volumeByOrder[order] = quantity * orderPrice;
break;

case SecurityType.Future:
case SecurityType.FutureOption:
InteractiveBrokersFeeHelper.CalculateFutureFopFee(security, quantity, market, _futureFee, out feeResult, out feeCurrency);
// Update the monthly contracts traded
_monthlyTradeVolume[SecurityType.Future] += quantity;
_volumeByOrder[order] = quantity;
break;

case SecurityType.Equity:
Expand All @@ -198,16 +217,17 @@ public override OrderFee GetOrderFee(OrderFeeParameters parameters)
feeResult += regulatoryFee + clearingFee + exchangeFee + passThroughFee;

// Update the monthly volume shares traded
_monthlyTradeVolume[SecurityType.Equity] += quantity;
_volumeByOrder[order] = quantity;
break;

case SecurityType.Cfd:
InteractiveBrokersFeeHelper.CalculateCfdFee(security, order, out feeResult, out feeCurrency);
break;

case SecurityType.Crypto:
var cryptoTradedValue = InteractiveBrokersFeeHelper.CalculateCryptoFee(security, order, _cryptoCommissionRate, CryptoMinimumOrderFee, out feeResult, out feeCurrency);
// Update the monthly value traded
_monthlyTradeVolume[SecurityType.Crypto] += InteractiveBrokersFeeHelper.CalculateCryptoFee(security, order, _cryptoCommissionRate, CryptoMinimumOrderFee, out feeResult, out feeCurrency);
_volumeByOrder[order] = cryptoTradedValue;
break;

default:
Expand Down
22 changes: 17 additions & 5 deletions Common/Orders/Fees/InteractiveBrokersTieredFeeModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class InteractiveBrokersTieredFeeModel(FeeModel):
last_order_time = datetime.min
# List of Option exchanges susceptible to pay ORF regulatory fee.
option_exchanges_orf_fee = [Market.CBOE, Market.USA]
volume_by_order = {}

def __init__(self, monthly_equity_trade_volume: float = 0, monthly_future_trade_volume: float = 0, monthly_forex_trade_amount_in_us_dollars: float = 0,
monthly_options_trade_amount_in_contracts: float = 0, monthly_crypto_trade_amount_in_us_dollars: float = 0) -> None:
Expand Down Expand Up @@ -60,6 +61,14 @@ def __init__(self, monthly_equity_trade_volume: float = 0, monthly_future_trade_
SecurityType.OPTION: monthly_options_trade_amount_in_contracts,
SecurityType.CRYPTO: monthly_crypto_trade_amount_in_us_dollars
}

def update_month_volume(self) -> None:
for order, volume in self.volume_by_order.copy().items():
# Tier only changed by the filled volume.
if order.status == OrderStatus.FILLED:
self.monthly_trade_volume[order.security_type] += volume
# Remove the processed order.
self.volume_by_order.pop(order)

def reprocess_rate_schedule(self, monthly_equity_trade_volume: float, monthly_future_trade_volume: float, monthly_forex_trade_amount_in_us_dollars: float,
monthly_options_trade_amount_in_contracts: float, monthly_crypto_trade_amount_in_us_dollars: float) -> None:
Expand Down Expand Up @@ -166,9 +175,12 @@ def get_order_fee(self, parameters: OrderFeeParameters) -> OrderFee:
order = parameters.order
security = parameters.security

# Update the monthly volume with filled quantity.
self.update_month_volume()
# Reset monthly trade value tracker when month rollover.
if self.last_order_time.month != order.time.month and self.last_order_time != datetime.min:
self.monthly_trade_volume = {key: 0 for key in self.monthly_trade_volume.keys()}
self.volume_by_order = {}
# Reprocess the rate schedule based on the current traded volume in various assets.
self.reprocess_rate_schedule(self.monthly_trade_volume[SecurityType.EQUITY],
self.monthly_trade_volume[SecurityType.FUTURE],
Expand All @@ -190,7 +202,7 @@ def get_order_fee(self, parameters: OrderFeeParameters) -> OrderFee:
fee_result, fee_currency, trade_value = self.calculate_forex_fee(security, order, self.forex_commission_rate, self.forex_minimum_order_fee)

# Update the monthly value traded
self.monthly_trade_volume[SecurityType.FOREX] += trade_value
self.volume_by_order[order] = trade_value

elif security.Type == SecurityType.OPTION or security.Type == SecurityType.INDEX_OPTION:
fee_result, fee_currency, order_price = self.calculate_option_fee(security, order, quantity, market, self.option_fee)
Expand All @@ -204,13 +216,13 @@ def get_order_fee(self, parameters: OrderFeeParameters) -> OrderFee:
fee_result += regulatory + transaction + clearing

# Update the monthly value traded
self.monthly_trade_volume[SecurityType.OPTION] += quantity * order_price
self.volume_by_order[order] = quantity * order_price

elif security.Type == SecurityType.FUTURE or security.Type == SecurityType.FUTURE_OPTION:
fee_result, fee_currency = self.calculate_future_fop_fee(security, quantity, market, self.future_fee)

# Update the monthly value traded
self.monthly_trade_volume[SecurityType.FUTURE] += quantity
self.volume_by_order[order] = quantity

elif security.Type == SecurityType.EQUITY:
trade_value = abs(order.get_value(security))
Expand All @@ -233,7 +245,7 @@ def get_order_fee(self, parameters: OrderFeeParameters) -> OrderFee:
fee_result += regulatory + exchange + clearing + pass_through

# Update the monthly value traded
self.monthly_trade_volume[SecurityType.EQUITY] += quantity
self.volume_by_order[order] = quantity

elif security.Type == SecurityType.CFD:
fee_result, fee_currency = self.calculate_cfd_fee(security, order)
Expand All @@ -242,7 +254,7 @@ def get_order_fee(self, parameters: OrderFeeParameters) -> OrderFee:
fee_result, fee_currency, trade_value = self.calculate_crypto_fee(security, order, self.crypto_commission_rate, self.crypto_minimum_order_fee)

# Update the monthly value traded
self.monthly_trade_volume[SecurityType.CRYPTO] += trade_value
self.volume_by_order[order] = trade_value

else:
# unsupported security type
Expand Down
2 changes: 1 addition & 1 deletion Launcher/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"environment": "backtesting", // "live-paper", "backtesting", "live-interactive", "live-interactive-iqfeed"

// algorithm class selector
"algorithm-type-name": "BasicTemplateFrameworkAlgorithm",
"algorithm-type-name": "InteractiveBrokersTieredFeeModelAlgorithm",

// Algorithm language selector - options CSharp, Python
"algorithm-language": "CSharp",
Expand Down
25 changes: 14 additions & 11 deletions Tests/Common/Orders/Fees/InteractiveBrokersFeeModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,18 @@ public void USAFutureFee(Symbol symbol, decimal expectedFee)
Assert.AreEqual(1000 * expectedFee, fee.Value.Amount);
}

[TestCase("USD", 70000, 0.00002 * 70000)]
[TestCase("USD", 100000, 0.00002 * 100000)]
[TestCase("USD", 10000, 1)] // The calculated fee will be under 1, but the minimum fee is 1 USD
[TestCase("JPY", 3000000, 0.00002 * 3000000)]
[TestCase("JPY", 1000000, 40)]// The calculated fee will be under 40, but the minimum fee is 40 JPY
[TestCase("HKD", 600000, 0.00002 * 600000)]
[TestCase("HKD", 200000, 10)]// The calculated fee will be under 10, but the minimum fee is 10 HKD
public void CalculatesCFDFee(string quoteCurrency, decimal price, decimal expectedFee)
[TestCase("USD", 70000, 1, 0.0001 * 70001)]
[TestCase("USD", 100000, 1, 0.0001 * 100001)]
[TestCase("USD", 70000, -1, 0.0001 * 69999)]
[TestCase("USD", 100000, -1, 0.0001 * 99999)]
[TestCase("USD", 100, 1, 1)] // The calculated fee will be under 1, but the minimum fee is 1 USD
[TestCase("JPY", 3000000, 1, 0.0001 * 3000001)]
[TestCase("JPY", 3000000, -1, 0.0001 * 2999999)]
[TestCase("JPY", 10000, 1, 40)]// The calculated fee will be under 40, but the minimum fee is 40 JPY
[TestCase("HKD", 600000, 1, 0.0001 * 600001)]
[TestCase("HKD", 600000, -1, 0.0001 * 599999)]
[TestCase("HKD", 2000, 1, 10)]// The calculated fee will be under 10, but the minimum fee is 10 HKD
public void CalculatesCFDFee(string quoteCurrency, decimal price, decimal quantity, decimal expectedFee)
{
var security = new Cfd(Symbols.DE10YBEUR,
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
Expand All @@ -119,10 +123,9 @@ public void CalculatesCFDFee(string quoteCurrency, decimal price, decimal expect
new SecurityCache());
security.QuoteCurrency.ConversionRate = 1;

security.SetMarketPrice(new Tick(DateTime.UtcNow, security.Symbol, price-1m, price+1m));

security.SetMarketPrice(new Tick(DateTime.UtcNow, security.Symbol, price, price));

var order = new MarketOrder(security.Symbol, 1, DateTime.UtcNow);
var order = new MarketOrder(security.Symbol, quantity, DateTime.UtcNow);
var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order));

Assert.AreEqual(quoteCurrency, fee.Value.Currency);
Expand Down
Loading

0 comments on commit ae864e1

Please sign in to comment.