Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 134 additions & 3 deletions Common/Brokerages/InteractiveBrokersBrokerageModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,42 @@ public class InteractiveBrokersBrokerageModel : DefaultBrokerageModel
{SecurityType.Future, Market.CME},
{SecurityType.FutureOption, Market.CME},
{SecurityType.Forex, Market.Oanda},
{SecurityType.Cfd, Market.InteractiveBrokers}
{SecurityType.Cfd, Market.InteractiveBrokers},
// where the backtest data lives, IB's listing is checked by ticker
{SecurityType.Crypto, Market.Coinbase}
}.ToReadOnlyDictionary();

/// <summary>
/// The only order types IB accepts for cryptocurrencies
/// </summary>
private static readonly IReadOnlySet<OrderType> _supportedCryptoOrderTypes = new HashSet<OrderType>
{
OrderType.Market,
OrderType.Limit
};

/// <summary>
/// How far from the best ask IB lets a cryptocurrency buy limit order sit, the greater of these two
/// </summary>
private const decimal _cryptoLimitPriceBand = 10m;
private const decimal _cryptoLimitPriceBandPercent = 0.0025m;

/// <summary>
/// IB routes API cryptocurrency orders from Sunday 03:00 to Friday 16:00 New York time only
/// </summary>
private static readonly Lazy<SecurityExchangeHours> _cryptoVenueHours = new(() =>
MarketHoursDatabase.FromDataFolder().GetExchangeHours(Market.InteractiveBrokers, null, SecurityType.Crypto));

/// <summary>
/// The cryptocurrency pairs IB lists, the <see cref="Market.InteractiveBrokers"/> entries of the symbol
/// properties database, keyed by ticker: the traded symbol stays on the market holding the backtest data
/// </summary>
private static readonly Lazy<HashSet<string>> _supportedCryptoPairs = new(() =>
SymbolPropertiesDatabase.FromDataFolder()
.GetSymbolPropertiesList(Market.InteractiveBrokers, SecurityType.Crypto)
.Select(entry => entry.Key.Symbol)
.ToHashSet(StringComparer.InvariantCultureIgnoreCase));

/// <summary>
/// Supported time in force
/// </summary>
Expand Down Expand Up @@ -137,7 +170,13 @@ public override decimal GetLeverage(Security security)
return 1m;
}

return security.Type == SecurityType.Cfd ? 10m : base.GetLeverage(security);
return security.Type switch
{
SecurityType.Cfd => 10m,
// IB does not lend against cryptocurrencies
SecurityType.Crypto => 1m,
_ => base.GetLeverage(security)
};
}

/// <summary>
Expand Down Expand Up @@ -189,14 +228,72 @@ public override bool CanSubmitOrder(Security security, Order order, out Brokerag
security.Type != SecurityType.FutureOption &&
security.Type != SecurityType.Index &&
security.Type != SecurityType.IndexOption &&
security.Type != SecurityType.Cfd)
security.Type != SecurityType.Cfd &&
security.Type != SecurityType.Crypto)
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.DefaultBrokerageModel.UnsupportedSecurityType(this, security));

return false;
}

if (security.Type == SecurityType.Crypto)
{
// from what is permanently wrong to what depends on the market: the pair, then the
// order, then the holdings, then the price, which is the only one a retry can fix
if (!_supportedCryptoPairs.Value.Contains(security.Symbol.Value))
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.InteractiveBrokersBrokerageModel.UnsupportedCryptoPair(this, security));

return false;
}

if (!_supportedCryptoOrderTypes.Contains(order.Type))
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.InteractiveBrokersBrokerageModel.UnsupportedCryptoOrderType(this, order, _supportedCryptoOrderTypes));

return false;
}

if (!IsValidOrderSize(security, order.Quantity, out message))
{
return false;
}

if (order.Quantity < 0 && security.Holdings.Quantity < order.AbsoluteQuantity)
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.InteractiveBrokersBrokerageModel.UnsupportedCryptoShortSale(this, security));

return false;
}

if (!IsWithinCryptoLimitPriceBand(security, order, out message))
{
return false;
}

if (order.Type == OrderType.Market && order.Direction == OrderDirection.Buy && security.Price <= 0)
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.InteractiveBrokersBrokerageModel.CryptoBuyMarketOrderWithoutPrice(security));

return false;
}

// the crypto market never closes, IB's venue does
var venueTime = security.LocalTime.ConvertTo(security.Exchange.TimeZone, _cryptoVenueHours.Value.TimeZone);
if (!_cryptoVenueHours.Value.IsOpen(venueTime, false))
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.InteractiveBrokersBrokerageModel.CryptoVenueClosed(security, _cryptoVenueHours.Value.GetNextMarketOpen(venueTime, false)));

return false;
}
}

// validate order quantity
//https://www.interactivebrokers.com/en/?f=%2Fen%2Ftrading%2FforexOrderSize.php
if (security.Type == SecurityType.Forex &&
Expand Down Expand Up @@ -263,6 +360,40 @@ public override bool CanExecuteOrder(Security security, Order order)
return order.SecurityType != SecurityType.Base;
}

/// <summary>
/// Returns true if the given cryptocurrency limit order is priced where IB accepts it. A buy has
/// to be within 10 dollars or 0.25% of the best ask, whichever is greater, so it cannot rest below
/// the market. Sells are not restricted. The order is let through when there is no price to
/// compare against.
/// </summary>
private bool IsWithinCryptoLimitPriceBand(Security security, Order order, out BrokerageMessageEvent message)
{
message = null;

if (order is not LimitOrder limitOrder || order.Direction != OrderDirection.Buy)
{
return true;
}

// the ask is not always there, the last price is a good enough reference for the check
var reference = security.AskPrice > 0 ? security.AskPrice : security.Price;
if (reference <= 0)
{
return true;
}

var tolerance = Math.Max(_cryptoLimitPriceBand, reference * _cryptoLimitPriceBandPercent);
if (Math.Abs(limitOrder.LimitPrice - reference) <= tolerance)
{
return true;
}

message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.InteractiveBrokersBrokerageModel.InvalidCryptoLimitPrice(limitOrder, reference, tolerance));

return false;
}

/// <summary>
/// Returns true if the specified order is within IB's order size limits
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions Common/Brokerages/InteractiveBrokersFixModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ public InteractiveBrokersFixModel(AccountType accountType = AccountType.Margin)
/// <returns>True if the brokerage could process the order, false otherwise</returns>
public override bool CanSubmitOrder(Security security, Order order, out BrokerageMessageEvent message)
{
// IB does not route cryptocurrencies over FIX: the session has no CRYPTO security type,
// no PAXOS/ZEROHASH destination and no immediate-or-cancel time in force
if (security.Type == SecurityType.Crypto)
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.InteractiveBrokersFixModel.UnsupportedCryptoSecurityType(this, security));

return false;
}

// only check supported combo order types
if (order is ComboOrder && order.GroupOrderManager != null && SupportedOrderTypes.Contains(order.Type))
{
Expand Down
77 changes: 77 additions & 0 deletions Common/Messages/Messages.Brokerages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,18 @@ public static string UnsupportedFopFutureComboOrders(Brokerages.InteractiveBroke
{
return Invariant($@"The {brokerageModel.GetType().Name} does not support {order.Type} combining future options and futures legs.");
}

/// <summary>
/// Returns a string message saying the given brokerage model does not support cryptocurrencies,
/// which Interactive Brokers does not route over FIX
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string UnsupportedCryptoSecurityType(Brokerages.InteractiveBrokersFixModel brokerageModel,
Securities.Security security)
{
return Invariant($@"The {brokerageModel.GetType().Name} does not support {SecurityType.Crypto
}, Interactive Brokers does not route {security.Symbol.Value} over FIX. Use the Interactive Brokers brokerage instead.");
}
}

/// <summary>
Expand Down Expand Up @@ -487,6 +499,71 @@ public static string UnsupportedFourLegComboLegLimitOrders(Brokerages.Interactiv
return Invariant($"The {brokerageModel.GetType().Name} does not support four-leg ComboLegLimit orders. Use ComboLimit orders for four-leg combinations or more.");
}

/// <summary>
/// Returns a string message saying the given brokerage model does not support the given order type for cryptocurrencies
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string UnsupportedCryptoOrderType(Brokerages.InteractiveBrokersBrokerageModel brokerageModel,
Orders.Order order, IEnumerable<OrderType> supportedOrderTypes)
{
return Invariant($@"The {brokerageModel.GetType().Name} does not support {order.Type
} orders for {SecurityType.Crypto}. Only {string.Join(", ", supportedOrderTypes)} orders are supported.");
}

/// <summary>
/// Returns a string message saying the given brokerage model does not support the given cryptocurrency pair
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string UnsupportedCryptoPair(Brokerages.InteractiveBrokersBrokerageModel brokerageModel,
Securities.Security security)
{
return Invariant($@"The {brokerageModel.GetType().Name} does not support {security.Symbol.Value
}, Interactive Brokers does not list it. The pairs it lists are the {SecurityType.Crypto
} entries of the {QuantConnect.Market.InteractiveBrokers} market in the symbol properties database.");
}

/// <summary>
/// Returns a string message saying the given brokerage model does not support short selling cryptocurrencies
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string UnsupportedCryptoShortSale(Brokerages.InteractiveBrokersBrokerageModel brokerageModel,
Securities.Security security)
{
return Invariant($@"The {brokerageModel.GetType().Name} does not support short sales of {
SecurityType.Crypto}, {security.Symbol.Value} holdings are {security.Holdings.Quantity}.");
}

/// <summary>
/// Returns a string message saying the given cryptocurrency limit order is priced too far from the market
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string InvalidCryptoLimitPrice(Orders.LimitOrder order, decimal reference, decimal tolerance)
{
return Invariant($@"Interactive Brokers cancels {SecurityType.Crypto} buy limit orders priced further than {
tolerance} from the best ask: the limit price of {order.LimitPrice} for {order.Symbol.Value
} is away from {reference}.");
}

/// <summary>
/// Returns a string message saying the given cryptocurrency buy market order cannot be sized without a price
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string CryptoBuyMarketOrderWithoutPrice(Securities.Security security)
{
return Invariant($@"Interactive Brokers sizes {SecurityType.Crypto} buy market orders by the cash amount to spend, so {
security.Symbol.Value} needs a known price to convert the quantity. Use a limit order or wait for data.");
}

/// <summary>
/// Returns a string message saying Interactive Brokers is not routing cryptocurrency orders at this time
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string CryptoVenueClosed(Securities.Security security, DateTime nextOpen)
{
return Invariant($@"Interactive Brokers routes {SecurityType.Crypto} orders from Sunday 03:00 to Friday 16:00 New York time only, a {
security.Symbol.Value} order placed now would be held until it reopens on {nextOpen:yyyy-MM-dd HH:mm} New York time.");
}

/// <summary>
/// Returns a string message containing the minimum and maximum limits for the allowable order size as well as the currency
/// </summary>
Expand Down
22 changes: 19 additions & 3 deletions Common/Orders/Fees/InteractiveBrokersFeeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ public class InteractiveBrokersFeeModel : FeeModel
/// </summary>
private const decimal _koreaFutureFeeRate = 0.00004m;

/// <summary>
/// Cryptocurrency commissions go from 0.12% to 0.18% of the trade value depending on the
/// monthly volume, we assume the highest rate.
/// Reference at https://www.interactivebrokers.com/en/pricing/commissions-cryptocurrencies.php
/// </summary>
private const decimal _cryptoCommissionRate = 0.0018m;

/// <summary>
/// Minimum cryptocurrency commission charged per order, USD 1.75 or its equivalent in the quote currency
/// </summary>
private const decimal _cryptoMinimumOrderFee = 1.75m;

/// <summary>
/// Initializes a new instance of the <see cref="ImmediateFillModel"/>
/// </summary>
Expand Down Expand Up @@ -94,7 +106,8 @@ public override OrderFee GetOrderFee(OrderFeeParameters parameters)

var quantity = order.AbsoluteQuantity;
decimal feeResult;
string feeCurrency;
// IB Forex and Crypto fees are all in USD
var feeCurrency = Currencies.USD;
var market = security.Symbol.ID.Market;
switch (security.Type)
{
Expand All @@ -103,8 +116,6 @@ public override OrderFee GetOrderFee(OrderFeeParameters parameters)
var totalOrderValue = order.GetValue(security);
var fee = Math.Abs(_forexCommissionRate*totalOrderValue);
feeResult = Math.Max(_forexMinimumOrderFee, fee);
// IB Forex fees are all in USD
feeCurrency = Currencies.USD;
break;

case SecurityType.Option:
Expand Down Expand Up @@ -191,6 +202,11 @@ public override OrderFee GetOrderFee(OrderFeeParameters parameters)
feeResult = Math.Max(feeResult, minimumFee);
break;

case SecurityType.Crypto:
var cryptoValue = Math.Abs(order.GetValue(security));
feeResult = Math.Max(_cryptoMinimumOrderFee, _cryptoCommissionRate * cryptoValue);
break;

default:
// unsupported security type
throw new ArgumentException(Messages.FeeModel.UnsupportedSecurityType(security));
Expand Down
48 changes: 48 additions & 0 deletions Data/market-hours/market-hours-database.json
Original file line number Diff line number Diff line change
Expand Up @@ -91630,6 +91630,54 @@
"holidays": [],
"earlyCloses": {}
},
"Crypto-interactivebrokers-[*]": {
"dataTimeZone": "UTC",
"exchangeTimeZone": "America/New_York",
"sunday": [
{
"start": "03:00:00",
"end": "1.00:00:00",
"state": "market"
}
],
"monday": [
{
"start": "00:00:00",
"end": "1.00:00:00",
"state": "market"
}
],
"tuesday": [
{
"start": "00:00:00",
"end": "1.00:00:00",
"state": "market"
}
],
"wednesday": [
{
"start": "00:00:00",
"end": "1.00:00:00",
"state": "market"
}
],
"thursday": [
{
"start": "00:00:00",
"end": "1.00:00:00",
"state": "market"
}
],
"friday": [
{
"start": "00:00:00",
"end": "16:00:00",
"state": "market"
}
],
"saturday": [],
"holidays": []
},
"Crypto-coinbase-[*]": {
"dataTimeZone": "UTC",
"exchangeTimeZone": "UTC",
Expand Down
Loading
Loading