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