Lean  $LEAN_TAG$
InteractiveBrokersFeeModel.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
19 using System.Collections.Generic;
20 
22 {
23  /// <summary>
24  /// Provides the default implementation of <see cref="IFeeModel"/>
25  /// </summary>
27  {
28  private readonly decimal _forexCommissionRate;
29  private readonly decimal _forexMinimumOrderFee;
30 
31  // option commission function takes number of contracts and the size of the option premium and returns total commission
32  private readonly Dictionary<string, Func<decimal, decimal, CashAmount>> _optionFee =
33  new Dictionary<string, Func<decimal, decimal, CashAmount>>();
34 
35  #pragma warning disable CS1570
36  /// <summary>
37  /// Reference at https://www.interactivebrokers.com/en/index.php?f=commission&p=futures1
38  /// </summary>
39  #pragma warning restore CS1570
40  private readonly Dictionary<string, Func<Security, CashAmount>> _futureFee =
41  // IB fee + exchange fee
42  new()
43  {
44  { Market.USA, UnitedStatesFutureFees },
45  { Market.HKFE, HongKongFutureFees },
46  { Market.EUREX, EUREXFutureFees }
47  };
48 
49  /// <summary>
50  /// Initializes a new instance of the <see cref="ImmediateFillModel"/>
51  /// </summary>
52  /// <param name="monthlyForexTradeAmountInUSDollars">Monthly FX dollar volume traded</param>
53  /// <param name="monthlyOptionsTradeAmountInContracts">Monthly options contracts traded</param>
54  public InteractiveBrokersFeeModel(decimal monthlyForexTradeAmountInUSDollars = 0, decimal monthlyOptionsTradeAmountInContracts = 0)
55  {
56  ProcessForexRateSchedule(monthlyForexTradeAmountInUSDollars, out _forexCommissionRate, out _forexMinimumOrderFee);
57  Func<decimal, decimal, CashAmount> optionsCommissionFunc;
58  ProcessOptionsRateSchedule(monthlyOptionsTradeAmountInContracts, out optionsCommissionFunc);
59  // only USA for now
60  _optionFee.Add(Market.USA, optionsCommissionFunc);
61  }
62 
63  /// <summary>
64  /// Gets the order fee associated with the specified order. This returns the cost
65  /// of the transaction in the account currency
66  /// </summary>
67  /// <param name="parameters">A <see cref="OrderFeeParameters"/> object
68  /// containing the security and order</param>
69  /// <returns>The cost of the order in units of the account currency</returns>
70  public override OrderFee GetOrderFee(OrderFeeParameters parameters)
71  {
72  var order = parameters.Order;
73  var security = parameters.Security;
74 
75  // Option exercise for equity options is free of charge
76  if (order.Type == OrderType.OptionExercise)
77  {
78  var optionOrder = (OptionExerciseOrder)order;
79 
80  // For Futures Options, contracts are charged the standard commission at expiration of the contract.
81  // Read more here: https://www1.interactivebrokers.com/en/index.php?f=14718#trading-related-fees
82  if (optionOrder.Symbol.ID.SecurityType == SecurityType.Option)
83  {
84  return OrderFee.Zero;
85  }
86  }
87 
88  var quantity = order.AbsoluteQuantity;
89  decimal feeResult;
90  string feeCurrency;
91  var market = security.Symbol.ID.Market;
92  switch (security.Type)
93  {
94  case SecurityType.Forex:
95  // get the total order value in the account currency
96  var totalOrderValue = order.GetValue(security);
97  var fee = Math.Abs(_forexCommissionRate*totalOrderValue);
98  feeResult = Math.Max(_forexMinimumOrderFee, fee);
99  // IB Forex fees are all in USD
100  feeCurrency = Currencies.USD;
101  break;
102 
103  case SecurityType.Option:
104  case SecurityType.IndexOption:
105  Func<decimal, decimal, CashAmount> optionsCommissionFunc;
106  if (!_optionFee.TryGetValue(market, out optionsCommissionFunc))
107  {
108  throw new KeyNotFoundException(Messages.InteractiveBrokersFeeModel.UnexpectedOptionMarket(market));
109  }
110  // applying commission function to the order
111  var optionFee = optionsCommissionFunc(quantity, GetPotentialOrderPrice(order, security));
112  feeResult = optionFee.Amount;
113  feeCurrency = optionFee.Currency;
114  break;
115 
116  case SecurityType.Future:
117  case SecurityType.FutureOption:
118  // The futures options fee model is exactly the same as futures' fees on IB.
119  if (market == Market.Globex || market == Market.NYMEX
120  || market == Market.CBOT || market == Market.ICE
121  || market == Market.CFE || market == Market.COMEX
122  || market == Market.CME || market == Market.NYSELIFFE)
123  {
124  // just in case...
125  market = Market.USA;
126  }
127 
128  if (!_futureFee.TryGetValue(market, out var feeRatePerContractFunc))
129  {
130  throw new KeyNotFoundException(Messages.InteractiveBrokersFeeModel.UnexpectedFutureMarket(market));
131  }
132 
133  var feeRatePerContract = feeRatePerContractFunc(security);
134  feeResult = quantity * feeRatePerContract.Amount;
135  feeCurrency = feeRatePerContract.Currency;
136  break;
137 
138  case SecurityType.Equity:
139  EquityFee equityFee;
140  switch (market)
141  {
142  case Market.USA:
143  equityFee = new EquityFee(Currencies.USD, feePerShare: 0.005m, minimumFee: 1, maximumFeeRate: 0.005m);
144  break;
145  case Market.India:
146  equityFee = new EquityFee(Currencies.INR, feePerShare: 0.01m, minimumFee: 6, maximumFeeRate: 20);
147  break;
148  default:
149  throw new KeyNotFoundException(Messages.InteractiveBrokersFeeModel.UnexpectedEquityMarket(market));
150  }
151  var tradeValue = Math.Abs(order.GetValue(security));
152 
153  //Per share fees
154  var tradeFee = equityFee.FeePerShare * quantity;
155 
156  //Maximum Per Order: equityFee.MaximumFeeRate
157  //Minimum per order. $equityFee.MinimumFee
158  var maximumPerOrder = equityFee.MaximumFeeRate * tradeValue;
159  if (tradeFee < equityFee.MinimumFee)
160  {
161  tradeFee = equityFee.MinimumFee;
162  }
163  else if (tradeFee > maximumPerOrder)
164  {
165  tradeFee = maximumPerOrder;
166  }
167 
168  feeCurrency = equityFee.Currency;
169  //Always return a positive fee.
170  feeResult = Math.Abs(tradeFee);
171  break;
172 
173  case SecurityType.Cfd:
174  var value = Math.Abs(order.GetValue(security));
175  feeResult = 0.00002m * value; // 0.002%
176  feeCurrency = security.QuoteCurrency.Symbol;
177 
178  var minimumFee = security.QuoteCurrency.Symbol switch
179  {
180  "JPY" => 40.0m,
181  "HKD" => 10.0m,
182  _ => 1.0m
183  };
184  feeResult = Math.Max(feeResult, minimumFee);
185  break;
186 
187  default:
188  // unsupported security type
189  throw new ArgumentException(Messages.FeeModel.UnsupportedSecurityType(security));
190  }
191 
192  return new OrderFee(new CashAmount(
193  feeResult,
194  feeCurrency));
195  }
196 
197  /// <summary>
198  /// Approximates the order's price based on the order type
199  /// </summary>
200  protected static decimal GetPotentialOrderPrice(Order order, Security security)
201  {
202  decimal price = 0;
203  switch (order.Type)
204  {
205  case OrderType.TrailingStop:
206  price = (order as TrailingStopOrder).StopPrice;
207  break;
208  case OrderType.StopMarket:
209  price = (order as StopMarketOrder).StopPrice;
210  break;
211  case OrderType.ComboMarket:
212  case OrderType.MarketOnOpen:
213  case OrderType.MarketOnClose:
214  case OrderType.Market:
215  decimal securityPrice;
216  if (order.Direction == OrderDirection.Buy)
217  {
218  price = security.BidPrice;
219  }
220  else
221  {
222  price = security.AskPrice;
223  }
224  break;
225  case OrderType.ComboLimit:
226  price = (order as ComboLimitOrder).GroupOrderManager.LimitPrice;
227  break;
228  case OrderType.ComboLegLimit:
229  price = (order as ComboLegLimitOrder).LimitPrice;
230  break;
231  case OrderType.StopLimit:
232  price = (order as StopLimitOrder).LimitPrice;
233  break;
234  case OrderType.LimitIfTouched:
235  price = (order as LimitIfTouchedOrder).LimitPrice;
236  break;
237  case OrderType.Limit:
238  price = (order as LimitOrder).LimitPrice;
239  break;
240  }
241 
242  return price;
243  }
244 
245  /// <summary>
246  /// Determines which tier an account falls into based on the monthly trading volume
247  /// </summary>
248  private static void ProcessForexRateSchedule(decimal monthlyForexTradeAmountInUSDollars, out decimal commissionRate, out decimal minimumOrderFee)
249  {
250  const decimal bp = 0.0001m;
251  if (monthlyForexTradeAmountInUSDollars <= 1000000000) // 1 billion
252  {
253  commissionRate = 0.20m * bp;
254  minimumOrderFee = 2.00m;
255  }
256  else if (monthlyForexTradeAmountInUSDollars <= 2000000000) // 2 billion
257  {
258  commissionRate = 0.15m * bp;
259  minimumOrderFee = 1.50m;
260  }
261  else if (monthlyForexTradeAmountInUSDollars <= 5000000000) // 5 billion
262  {
263  commissionRate = 0.10m * bp;
264  minimumOrderFee = 1.25m;
265  }
266  else
267  {
268  commissionRate = 0.08m * bp;
269  minimumOrderFee = 1.00m;
270  }
271  }
272 
273  /// <summary>
274  /// Determines which tier an account falls into based on the monthly trading volume
275  /// </summary>
276  private static void ProcessOptionsRateSchedule(decimal monthlyOptionsTradeAmountInContracts, out Func<decimal, decimal, CashAmount> optionsCommissionFunc)
277  {
278  if (monthlyOptionsTradeAmountInContracts <= 10000)
279  {
280  optionsCommissionFunc = (orderSize, premium) =>
281  {
282  var commissionRate = premium >= 0.1m ?
283  0.65m :
284  (0.05m <= premium && premium < 0.1m ? 0.5m : 0.25m);
285  return new CashAmount(Math.Max(orderSize * commissionRate, 1.0m), Currencies.USD);
286  };
287  }
288  else if (monthlyOptionsTradeAmountInContracts <= 50000)
289  {
290  optionsCommissionFunc = (orderSize, premium) =>
291  {
292  var commissionRate = premium >= 0.05m ? 0.5m : 0.25m;
293  return new CashAmount(Math.Max(orderSize * commissionRate, 1.0m), Currencies.USD);
294  };
295  }
296  else if (monthlyOptionsTradeAmountInContracts <= 100000)
297  {
298  optionsCommissionFunc = (orderSize, premium) =>
299  {
300  var commissionRate = 0.25m;
301  return new CashAmount(Math.Max(orderSize * commissionRate, 1.0m), Currencies.USD);
302  };
303  }
304  else
305  {
306  optionsCommissionFunc = (orderSize, premium) =>
307  {
308  var commissionRate = 0.15m;
309  return new CashAmount(Math.Max(orderSize * commissionRate, 1.0m), Currencies.USD);
310  };
311  }
312  }
313 
314  private static CashAmount UnitedStatesFutureFees(Security security)
315  {
316  IDictionary<string, decimal> fees, exchangeFees;
317  decimal ibFeePerContract, exchangeFeePerContract;
318  string symbol;
319 
320  switch (security.Symbol.SecurityType)
321  {
322  case SecurityType.Future:
323  fees = _usaFuturesFees;
324  exchangeFees = _usaFuturesExchangeFees;
325  symbol = security.Symbol.ID.Symbol;
326  break;
327  case SecurityType.FutureOption:
328  fees = _usaFutureOptionsFees;
329  exchangeFees = _usaFutureOptionsExchangeFees;
330  symbol = security.Symbol.Underlying.ID.Symbol;
331  break;
332  default:
333  throw new ArgumentException(Messages.InteractiveBrokersFeeModel.UnitedStatesFutureFeesUnsupportedSecurityType(security));
334  }
335 
336  if (!fees.TryGetValue(symbol, out ibFeePerContract))
337  {
338  ibFeePerContract = 0.85m;
339  }
340 
341  if (!exchangeFees.TryGetValue(symbol, out exchangeFeePerContract))
342  {
343  exchangeFeePerContract = 1.60m;
344  }
345 
346  // Add exchange fees + IBKR regulatory fee (0.02)
347  return new CashAmount(ibFeePerContract + exchangeFeePerContract + 0.02m, Currencies.USD);
348  }
349 
350  /// <summary>
351  /// See https://www.hkex.com.hk/Services/Rules-and-Forms-and-Fees/Fees/Listed-Derivatives/Trading/Transaction?sc_lang=en
352  /// </summary>
353  private static CashAmount HongKongFutureFees(Security security)
354  {
355  if (security.Symbol.ID.Symbol.Equals("HSI", StringComparison.InvariantCultureIgnoreCase))
356  {
357  // IB fee + exchange fee
358  return new CashAmount(30 + 10, Currencies.HKD);
359  }
360 
361  decimal ibFeePerContract;
362  switch (security.QuoteCurrency.Symbol)
363  {
364  case Currencies.CNH:
365  ibFeePerContract = 13;
366  break;
367  case Currencies.HKD:
368  ibFeePerContract = 20;
369  break;
370  case Currencies.USD:
371  ibFeePerContract = 2.40m;
372  break;
373  default:
374  throw new ArgumentException(Messages.InteractiveBrokersFeeModel.HongKongFutureFeesUnexpectedQuoteCurrency(security));
375  }
376 
377  // let's add a 50% extra charge for exchange fees
378  return new CashAmount(ibFeePerContract * 1.5m, security.QuoteCurrency.Symbol);
379  }
380 
381  private static CashAmount EUREXFutureFees(Security security)
382  {
383  IDictionary<string, decimal> fees, exchangeFees;
384  decimal ibFeePerContract, exchangeFeePerContract;
385  string symbol;
386 
387  switch (security.Symbol.SecurityType)
388  {
389  case SecurityType.Future:
390  fees = _eurexFuturesFees;
391  exchangeFees = _eurexFuturesExchangeFees;
392  symbol = security.Symbol.ID.Symbol;
393  break;
394  default:
395  throw new ArgumentException(Messages.InteractiveBrokersFeeModel.EUREXFutureFeesUnsupportedSecurityType(security));
396  }
397 
398  if (!fees.TryGetValue(symbol, out ibFeePerContract))
399  {
400  ibFeePerContract = 1.00m;
401  }
402 
403  if (!exchangeFees.TryGetValue(symbol, out exchangeFeePerContract))
404  {
405  exchangeFeePerContract = 0.00m;
406  }
407 
408  // Add exchange fees + IBKR regulatory fee (0.02)
409  return new CashAmount(ibFeePerContract + exchangeFeePerContract + 0.02m, Currencies.EUR);
410  }
411 
412  /// <summary>
413  /// Reference at https://www.interactivebrokers.com/en/pricing/commissions-futures.php?re=amer
414  /// </summary>
415  private static readonly Dictionary<string, decimal> _usaFuturesFees = new()
416  {
417  // Micro E-mini Futures
418  { "MYM", 0.25m }, { "M2K", 0.25m }, { "MES", 0.25m }, { "MNQ", 0.25m }, { "2YY", 0.25m }, { "5YY", 0.25m }, { "10Y", 0.25m },
419  { "30Y", 0.25m }, { "MCL", 0.25m }, { "MGC", 0.25m }, { "SIL", 0.25m },
420  // Cryptocurrency Futures
421  { "BTC", 5m }, { "MBT", 2.25m }, { "ETH", 3m }, { "MET", 0.20m },
422  // E-mini FX (currencies) Futures
423  { "E7", 0.50m }, { "J7", 0.50m },
424  // Micro E-mini FX (currencies) Futures
425  { "M6E", 0.15m }, { "M6A", 0.15m }, { "M6B", 0.15m }, { "MCD", 0.15m }, { "MJY", 0.15m }, { "MSF", 0.15m }, { "M6J", 0.15m },
426  { "MIR", 0.15m }, { "M6C", 0.15m }, { "M6S", 0.15m }, { "MNH", 0.15m },
427  };
428 
429  /// <summary>
430  /// Reference at https://www.interactivebrokers.com/en/pricing/commissions-futures-europe.php?re=europe
431  /// </summary>
432  private static readonly Dictionary<string, decimal> _eurexFuturesFees = new()
433  {
434  // Futures
435  { "FESX", 1.00m },
436  };
437 
438  private static readonly Dictionary<string, decimal> _usaFutureOptionsFees = new()
439  {
440  // Micro E-mini Future Options
441  { "MYM", 0.25m }, { "M2K", 0.25m }, { "MES", 0.25m }, { "MNQ", 0.25m }, { "2YY", 0.25m }, { "5YY", 0.25m }, { "10Y", 0.25m },
442  { "30Y", 0.25m }, { "MCL", 0.25m }, { "MGC", 0.25m }, { "SIL", 0.25m },
443  // Cryptocurrency Future Options
444  { "BTC", 5m }, { "MBT", 1.25m }, { "ETH", 3m }, { "MET", 0.10m },
445  };
446 
447  private static readonly Dictionary<string, decimal> _usaFuturesExchangeFees = new()
448  {
449  // E-mini Futures
450  { "ES", 1.28m }, { "NQ", 1.28m }, { "YM", 1.28m }, { "RTY", 1.28m }, { "EMD", 1.28m },
451  // Micro E-mini Futures
452  { "MYM", 0.30m }, { "M2K", 0.30m }, { "MES", 0.30m }, { "MNQ", 0.30m }, { "2YY", 0.30m }, { "5YY", 0.30m }, { "10Y", 0.30m },
453  { "30Y", 0.30m }, { "MCL", 0.30m }, { "MGC", 0.30m }, { "SIL", 0.30m },
454  // Cryptocurrency Futures
455  { "BTC", 6m }, { "MBT", 2.5m }, { "ETH", 4m }, { "MET", 0.20m },
456  // E-mini FX (currencies) Futures
457  { "E7", 0.85m }, { "J7", 0.85m },
458  // Micro E-mini FX (currencies) Futures
459  { "M6E", 0.24m }, { "M6A", 0.24m }, { "M6B", 0.24m }, { "MCD", 0.24m }, { "MJY", 0.24m }, { "MSF", 0.24m }, { "M6J", 0.24m },
460  { "MIR", 0.24m }, { "M6C", 0.24m }, { "M6S", 0.24m }, { "MNH", 0.24m },
461  };
462 
463  private static readonly Dictionary<string, decimal> _eurexFuturesExchangeFees = new()
464  {
465  // Futures
466  { "FESX", 0.00m },
467  };
468 
469  private static readonly Dictionary<string, decimal> _usaFutureOptionsExchangeFees = new()
470  {
471  // E-mini Future Options
472  { "ES", 0.55m }, { "NQ", 0.55m }, { "YM", 0.55m }, { "RTY", 0.55m }, { "EMD", 0.55m },
473  // Micro E-mini Future Options
474  { "MYM", 0.20m }, { "M2K", 0.20m }, { "MES", 0.20m }, { "MNQ", 0.20m }, { "2YY", 0.20m }, { "5YY", 0.20m }, { "10Y", 0.20m },
475  { "30Y", 0.20m }, { "MCL", 0.20m }, { "MGC", 0.20m }, { "SIL", 0.20m },
476  // Cryptocurrency Future Options
477  { "BTC", 5m }, { "MBT", 2.5m }, { "ETH", 4m }, { "MET", 0.20m },
478  };
479 
480  /// <summary>
481  /// Helper class to handle IB Equity fees
482  /// </summary>
483  private class EquityFee
484  {
485  public string Currency { get; }
486  public decimal FeePerShare { get; }
487  public decimal MinimumFee { get; }
488  public decimal MaximumFeeRate { get; }
489 
490  public EquityFee(string currency,
491  decimal feePerShare,
492  decimal minimumFee,
493  decimal maximumFeeRate)
494  {
495  Currency = currency;
496  FeePerShare = feePerShare;
497  MinimumFee = minimumFee;
498  MaximumFeeRate = maximumFeeRate;
499  }
500  }
501  }
502 }