Skip to content

Commit ae864e1

Browse files
committed
Fix bug in repeated count and other misc
1 parent 8c7c354 commit ae864e1

8 files changed

+118
-67
lines changed

Algorithm.CSharp/InteractiveBrokersTieredFeeModelAlgorithm.cs

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ public override void OnOrderEvent(OrderEvent orderEvent)
7777
{
7878
// Assert if the monthly traded volume is correct in the fee model.
7979
_monthlyTradedVolume += orderEvent.AbsoluteFillQuantity;
80-
var modelTradedVolume = _feeModel.MonthTradedVolume[SecurityType.Equity];
80+
// Month volume will update only at the next scan, so we add this fill quantity.
81+
var modelTradedVolume = _feeModel.MonthTradedVolume[SecurityType.Equity] + orderEvent.AbsoluteFillQuantity;
8182
if (_monthlyTradedVolume != modelTradedVolume)
8283
{
8384
throw new Exception($"Monthly traded volume is incorrect - Actual: {_monthlyTradedVolume} - Model: {modelTradedVolume}");
@@ -116,32 +117,32 @@ public override void OnOrderEvent(OrderEvent orderEvent)
116117
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
117118
{
118119
{"Total Orders", "36"},
119-
{"Average Win", "0.00%"},
120-
{"Average Loss", "0.00%"},
121-
{"Compounding Annual Return", "-2.237%"},
122-
{"Drawdown", "0.000%"},
123-
{"Expectancy", "-0.486"},
120+
{"Average Win", "0.02%"},
121+
{"Average Loss", "-0.05%"},
122+
{"Compounding Annual Return", "-26.786%"},
123+
{"Drawdown", "0.600%"},
124+
{"Expectancy", "-0.658"},
124125
{"Start Equity", "1000000000"},
125-
{"End Equity", "999762433.94"},
126-
{"Net Profit", "-0.024%"},
127-
{"Sharpe Ratio", "-8.397"},
128-
{"Sortino Ratio", "-11.384"},
126+
{"End Equity", "996730872.35"},
127+
{"Net Profit", "-0.327%"},
128+
{"Sharpe Ratio", "-7.559"},
129+
{"Sortino Ratio", "-9.624"},
129130
{"Probabilistic Sharpe Ratio", "0%"},
130131
{"Loss Rate", "75%"},
131132
{"Win Rate", "25%"},
132-
{"Profit-Loss Ratio", "1.06"},
133-
{"Alpha", "-0.035"},
134-
{"Beta", "0.009"},
135-
{"Annual Standard Deviation", "0.003"},
136-
{"Annual Variance", "0"},
137-
{"Information Ratio", "-5.78"},
138-
{"Tracking Error", "0.269"},
139-
{"Treynor Ratio", "-2.319"},
140-
{"Total Fees", "$185772.29"},
141-
{"Estimated Strategy Capacity", "$11000000.00"},
133+
{"Profit-Loss Ratio", "0.37"},
134+
{"Alpha", "-0.358"},
135+
{"Beta", "0.098"},
136+
{"Annual Standard Deviation", "0.027"},
137+
{"Annual Variance", "0.001"},
138+
{"Information Ratio", "-7.11"},
139+
{"Tracking Error", "0.245"},
140+
{"Treynor Ratio", "-2.105"},
141+
{"Total Fees", "$4126697.73"},
142+
{"Estimated Strategy Capacity", "$180000000.00"},
142143
{"Lowest Capacity Asset", "AIG R735QTJ8XC9X"},
143-
{"Portfolio Turnover", "2.37%"},
144-
{"OrderListHash", "d35a4e91c145a100d4bffb7c0fc0ff35"}
144+
{"Portfolio Turnover", "40.56%"},
145+
{"OrderListHash", "bd43a7c7f61e734a7dbc06180af8fc36"}
145146
};
146147
}
147148
}

Algorithm.Python/InteractiveBrokersTieredFeeModelAlgorithm.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ def on_data(self, slice: Slice) -> None:
5050
self.market_on_close_order(self.bac, -60000)
5151

5252
def on_order_event(self, order_event: OrderEvent) -> None:
53-
if order_event.status != OrderStatus.FILLED:
53+
if order_event.status == OrderStatus.FILLED:
5454
# Assert if the monthly traded volume is correct in the fee model.
5555
self.monthly_traded_volume += order_event.absolute_fill_quantity
56-
model_traded_volume = self.fee_model.monthly_trade_volume[SecurityType.EQUITY]
56+
# Month volume will update only at the next scan, so we add this fill quantity.
57+
model_traded_volume = self.fee_model.monthly_trade_volume[SecurityType.EQUITY] + order_event.absolute_fill_quantity
5758
if self.monthly_traded_volume != model_traded_volume:
58-
raise Exception(f"Monthly traded volume is incorrect - Actual: {self.monthly_traded_volume} - Model: {model_traded_volume}")
59+
raise Exception(f"Monthly traded volume is incorrect - Actual: {self.monthly_traded_volume} - Model: {model_traded_volume}")

Common/Orders/Fees/InteractiveBrokersFeeHelper.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ internal static void ProcessCryptoRateSchedule(decimal monthlyCryptoTradeAmountI
166166
internal static decimal CalculateForexFee(Security security, Order order, decimal forexCommissionRate,
167167
decimal forexMinimumOrderFee, out decimal fee, out string currency)
168168
{
169-
// get the total order value in the account currency
169+
// get the total order value in USD.
170170
var totalOrderValue = Math.Abs(order.GetValue(security));
171171
var baseFee = forexCommissionRate*totalOrderValue;
172172

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

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

Common/Orders/Fees/InteractiveBrokersTieredFeeModel.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class InteractiveBrokersTieredFeeModel : FeeModel
4646
// List of Option exchanges susceptible to pay ORF regulatory fee.
4747
private static readonly List<string> _optionExchangesOrfFee = new() { Market.CBOE, Market.USA };
4848
private Dictionary<SecurityType, decimal> _monthlyTradeVolume;
49+
private Dictionary<Order, decimal> _volumeByOrder = new();
4950

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

89+
private void UpdateMonthVolume()
90+
{
91+
foreach (var (order, volume) in new Dictionary<Order, decimal>(_volumeByOrder))
92+
{
93+
// Tier only changed by the filled volume.
94+
if (order.Status == OrderStatus.Filled)
95+
{
96+
_monthlyTradeVolume[order.SecurityType] += volume;
97+
// Remove the processed order.
98+
_volumeByOrder.Remove(order);
99+
}
100+
}
101+
}
102+
88103
/// <summary>
89104
/// Reprocess the rate schedule based on the current traded volume in various assets.
90105
/// </summary>
@@ -117,10 +132,13 @@ public override OrderFee GetOrderFee(OrderFeeParameters parameters)
117132
var order = parameters.Order;
118133
var security = parameters.Security;
119134

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

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

169188
// Update the monthly value traded
170-
_monthlyTradeVolume[SecurityType.Option] += quantity * orderPrice;
189+
_volumeByOrder[order] = quantity * orderPrice;
171190
break;
172191

173192
case SecurityType.Future:
174193
case SecurityType.FutureOption:
175194
InteractiveBrokersFeeHelper.CalculateFutureFopFee(security, quantity, market, _futureFee, out feeResult, out feeCurrency);
176195
// Update the monthly contracts traded
177-
_monthlyTradeVolume[SecurityType.Future] += quantity;
196+
_volumeByOrder[order] = quantity;
178197
break;
179198

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

200219
// Update the monthly volume shares traded
201-
_monthlyTradeVolume[SecurityType.Equity] += quantity;
220+
_volumeByOrder[order] = quantity;
202221
break;
203222

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

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

213233
default:

Common/Orders/Fees/InteractiveBrokersTieredFeeModel.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class InteractiveBrokersTieredFeeModel(FeeModel):
2929
last_order_time = datetime.min
3030
# List of Option exchanges susceptible to pay ORF regulatory fee.
3131
option_exchanges_orf_fee = [Market.CBOE, Market.USA]
32+
volume_by_order = {}
3233

3334
def __init__(self, monthly_equity_trade_volume: float = 0, monthly_future_trade_volume: float = 0, monthly_forex_trade_amount_in_us_dollars: float = 0,
3435
monthly_options_trade_amount_in_contracts: float = 0, monthly_crypto_trade_amount_in_us_dollars: float = 0) -> None:
@@ -60,6 +61,14 @@ def __init__(self, monthly_equity_trade_volume: float = 0, monthly_future_trade_
6061
SecurityType.OPTION: monthly_options_trade_amount_in_contracts,
6162
SecurityType.CRYPTO: monthly_crypto_trade_amount_in_us_dollars
6263
}
64+
65+
def update_month_volume(self) -> None:
66+
for order, volume in self.volume_by_order.copy().items():
67+
# Tier only changed by the filled volume.
68+
if order.status == OrderStatus.FILLED:
69+
self.monthly_trade_volume[order.security_type] += volume
70+
# Remove the processed order.
71+
self.volume_by_order.pop(order)
6372

6473
def reprocess_rate_schedule(self, monthly_equity_trade_volume: float, monthly_future_trade_volume: float, monthly_forex_trade_amount_in_us_dollars: float,
6574
monthly_options_trade_amount_in_contracts: float, monthly_crypto_trade_amount_in_us_dollars: float) -> None:
@@ -166,9 +175,12 @@ def get_order_fee(self, parameters: OrderFeeParameters) -> OrderFee:
166175
order = parameters.order
167176
security = parameters.security
168177

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

192204
# Update the monthly value traded
193-
self.monthly_trade_volume[SecurityType.FOREX] += trade_value
205+
self.volume_by_order[order] = trade_value
194206

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

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

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

212224
# Update the monthly value traded
213-
self.monthly_trade_volume[SecurityType.FUTURE] += quantity
225+
self.volume_by_order[order] = quantity
214226

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

235247
# Update the monthly value traded
236-
self.monthly_trade_volume[SecurityType.EQUITY] += quantity
248+
self.volume_by_order[order] = quantity
237249

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

244256
# Update the monthly value traded
245-
self.monthly_trade_volume[SecurityType.CRYPTO] += trade_value
257+
self.volume_by_order[order] = trade_value
246258

247259
else:
248260
# unsupported security type

Launcher/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"environment": "backtesting", // "live-paper", "backtesting", "live-interactive", "live-interactive-iqfeed"
1010

1111
// algorithm class selector
12-
"algorithm-type-name": "BasicTemplateFrameworkAlgorithm",
12+
"algorithm-type-name": "InteractiveBrokersTieredFeeModelAlgorithm",
1313

1414
// Algorithm language selector - options CSharp, Python
1515
"algorithm-language": "CSharp",

Tests/Common/Orders/Fees/InteractiveBrokersFeeModelTests.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,18 @@ public void USAFutureFee(Symbol symbol, decimal expectedFee)
101101
Assert.AreEqual(1000 * expectedFee, fee.Value.Amount);
102102
}
103103

104-
[TestCase("USD", 70000, 0.00002 * 70000)]
105-
[TestCase("USD", 100000, 0.00002 * 100000)]
106-
[TestCase("USD", 10000, 1)] // The calculated fee will be under 1, but the minimum fee is 1 USD
107-
[TestCase("JPY", 3000000, 0.00002 * 3000000)]
108-
[TestCase("JPY", 1000000, 40)]// The calculated fee will be under 40, but the minimum fee is 40 JPY
109-
[TestCase("HKD", 600000, 0.00002 * 600000)]
110-
[TestCase("HKD", 200000, 10)]// The calculated fee will be under 10, but the minimum fee is 10 HKD
111-
public void CalculatesCFDFee(string quoteCurrency, decimal price, decimal expectedFee)
104+
[TestCase("USD", 70000, 1, 0.0001 * 70001)]
105+
[TestCase("USD", 100000, 1, 0.0001 * 100001)]
106+
[TestCase("USD", 70000, -1, 0.0001 * 69999)]
107+
[TestCase("USD", 100000, -1, 0.0001 * 99999)]
108+
[TestCase("USD", 100, 1, 1)] // The calculated fee will be under 1, but the minimum fee is 1 USD
109+
[TestCase("JPY", 3000000, 1, 0.0001 * 3000001)]
110+
[TestCase("JPY", 3000000, -1, 0.0001 * 2999999)]
111+
[TestCase("JPY", 10000, 1, 40)]// The calculated fee will be under 40, but the minimum fee is 40 JPY
112+
[TestCase("HKD", 600000, 1, 0.0001 * 600001)]
113+
[TestCase("HKD", 600000, -1, 0.0001 * 599999)]
114+
[TestCase("HKD", 2000, 1, 10)]// The calculated fee will be under 10, but the minimum fee is 10 HKD
115+
public void CalculatesCFDFee(string quoteCurrency, decimal price, decimal quantity, decimal expectedFee)
112116
{
113117
var security = new Cfd(Symbols.DE10YBEUR,
114118
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
@@ -119,10 +123,9 @@ public void CalculatesCFDFee(string quoteCurrency, decimal price, decimal expect
119123
new SecurityCache());
120124
security.QuoteCurrency.ConversionRate = 1;
121125

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

123-
security.SetMarketPrice(new Tick(DateTime.UtcNow, security.Symbol, price, price));
124-
125-
var order = new MarketOrder(security.Symbol, 1, DateTime.UtcNow);
128+
var order = new MarketOrder(security.Symbol, quantity, DateTime.UtcNow);
126129
var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order));
127130

128131
Assert.AreEqual(quoteCurrency, fee.Value.Currency);

0 commit comments

Comments
 (0)