Skip to content

Commit

Permalink
Add Bybit integration (QuantConnect#5898)
Browse files Browse the repository at this point in the history
  • Loading branch information
arodus committed Sep 16, 2023
1 parent d276d63 commit cc0d406
Show file tree
Hide file tree
Showing 17 changed files with 2,066 additions and 1 deletion.
6 changes: 6 additions & 0 deletions Common/Brokerages/BrokerageName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,11 @@ public enum BrokerageName
/// Transaction and submit/execution rules will use RBI models
/// </summary>
RBI,

/// <summary>
/// Transaction and submit/execution rules will use Bybit models
/// </summary>
Bybit,

}
}
149 changes: 149 additions & 0 deletions Common/Brokerages/BybitBrokerageModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using QuantConnect.Benchmarks;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Securities;
using QuantConnect.Util;

namespace QuantConnect.Brokerages;

/// <summary>
///
/// </summary>
public class BybitBrokerageModel : DefaultBrokerageModel
{
/// <summary>
/// Market name
/// </summary>
protected virtual string MarketName => Market.Bybit;

/// <summary>
/// Gets a map of the default markets to be used for each security type
/// </summary>
public override IReadOnlyDictionary<SecurityType, string> DefaultMarkets { get; } = GetDefaultMarkets(Market.Bybit);


public override IFeeModel GetFeeModel(Security security) => new BybitFeeModel();


public BybitBrokerageModel(AccountType accountType = AccountType.Cash) : base(accountType)
{
}

/// <summary>
/// Bybit global leverage rule
/// </summary>
/// <param name="security"></param>
/// <returns></returns>
public override decimal GetLeverage(Security security)
{
if (AccountType == AccountType.Cash || security.IsInternalFeed() || security.Type == SecurityType.Base)
{
return 1m;
}

return security.Symbol.SecurityType == SecurityType.CryptoFuture ? 10 : 10;
}

/// <summary>
/// Get the benchmark for this model
/// </summary>
/// <param name="securities">SecurityService to create the security with if needed</param>
/// <returns>The benchmark for this brokerage</returns>
public override IBenchmark GetBenchmark(SecurityManager securities)
{
var symbol = Symbol.Create("BTCUSDC", SecurityType.Crypto, MarketName);
return SecurityBenchmark.CreateInstance(securities, symbol);
}


public override bool CanUpdateOrder(Security security, Order order, UpdateOrderRequest request,
out BrokerageMessageEvent message)
{
security = security ?? throw new ArgumentNullException(nameof(security));
order = order ?? throw new ArgumentNullException(nameof(order));
request = request ?? throw new ArgumentNullException(nameof(request));

//can only update linear, inverse, and options
if (security.Type != SecurityType.CryptoFuture)
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.DefaultBrokerageModel.OrderUpdateNotSupported);
}

if (order.Status is not (OrderStatus.New or OrderStatus.PartiallyFilled))
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.DefaultBrokerageModel.OrderUpdateNotSupported);
return false;
}

if (request.Quantity.HasValue && !IsOrderSizeLargeEnough(security, Math.Abs(request.Quantity.Value)))
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.DefaultBrokerageModel.InvalidOrderQuantity(security, request.Quantity.Value));
return false;
}

message = null;
return true;
}

protected static IReadOnlyDictionary<SecurityType, string> GetDefaultMarkets(string marketName)
{
var map = DefaultMarketMap.ToDictionary();
map[SecurityType.Crypto] = marketName;
return map.ToReadOnlyDictionary();
}


public override bool CanSubmitOrder(Security security, Order order, out BrokerageMessageEvent message)
{
security = security ?? throw new ArgumentNullException(nameof(security));
order = order ?? throw new ArgumentNullException(nameof(order));

if (security.Type != SecurityType.Crypto && security.Type != SecurityType.CryptoFuture)
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.DefaultBrokerageModel.UnsupportedSecurityType(this, security));

return false;
}

message = null;
var quantityIsValid = true;

switch (order)
{
case StopLimitOrder:
case StopMarketOrder:
case LimitOrder:
case MarketOrder:
quantityIsValid = IsOrderSizeLargeEnough(security, Math.Abs(order.Quantity));
break;
default:
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.DefaultBrokerageModel.UnsupportedOrderType(this, order,
new[] { OrderType.StopMarket, OrderType.StopLimit, OrderType.Market, OrderType.Limit }));
return false;
}

if (!quantityIsValid)
{
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
Messages.DefaultBrokerageModel.InvalidOrderQuantity(security, order.Quantity));

return false;
}


return base.CanSubmitOrder(security, order, out message);
}

protected virtual bool IsOrderSizeLargeEnough(Security security, decimal orderQuantity)
{
return !security.SymbolProperties.MinimumOrderSize.HasValue ||
orderQuantity > security.SymbolProperties.MinimumOrderSize;
}
}
54 changes: 54 additions & 0 deletions Common/Brokerages/BybitFuturesBrokerageModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using QuantConnect.Benchmarks;
using QuantConnect.Orders.Fees;
using QuantConnect.Securities;
using QuantConnect.Securities.CryptoFuture;

namespace QuantConnect.Brokerages;

public class BybitFuturesBrokerageModel : BybitBrokerageModel
{
public BybitFuturesBrokerageModel(AccountType accountType = AccountType.Margin) : base(accountType)
{
if (accountType == AccountType.Cash)
{
throw new InvalidOperationException($"{SecurityType.CryptoFuture} can only be traded using a {AccountType.Margin} account type");
}
}

/// <summary>
/// Get the benchmark for this model
/// </summary>
/// <param name="securities">SecurityService to create the security with if needed</param>
/// <returns>The benchmark for this brokerage</returns>
public override IBenchmark GetBenchmark(SecurityManager securities)
{
var symbol = Symbol.Create("BTCUSDT", SecurityType.CryptoFuture, MarketName);
return SecurityBenchmark.CreateInstance(securities, symbol);
}

/// <summary>
/// Provides Binance Futures fee model
/// </summary>
/// <param name="security">The security to get a fee model for</param>
/// <returns>The new fee model for this brokerage</returns>
public override IFeeModel GetFeeModel(Security security)
{
return new BybitFuturesFeeModel();
}
/// <summary>
/// Gets a new margin interest rate model for the security
/// </summary>
/// <param name="security">The security to get a margin interest rate model for</param>
/// <returns>The margin interest rate model for this brokerage</returns>
public override IMarginInterestRateModel GetMarginInterestRateModel(Security security)
{
//todo is there something else needed to support this?
// only applies for perpetual futures
if (security.Symbol.SecurityType == SecurityType.CryptoFuture && security.Symbol.ID.Date == SecurityIdentifier.DefaultDate)
{
return new BybitFutureMarginInterestRateModel();
}
return base.GetMarginInterestRateModel(security);
}
}
6 changes: 6 additions & 0 deletions Common/Brokerages/IBrokerageModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ public static IBrokerageModel Create(IOrderProvider orderProvider, BrokerageName

case BrokerageName.RBI:
return new RBIBrokerageModel(accountType);

case BrokerageName.Bybit:
return new BybitBrokerageModel(accountType);

default:
throw new ArgumentOutOfRangeException(nameof(brokerage), brokerage, null);
Expand Down Expand Up @@ -345,6 +348,9 @@ public static BrokerageName GetBrokerageName(IBrokerageModel brokerageModel)
case RBIBrokerageModel _:
return BrokerageName.RBI;

case BybitBrokerageModel _:
return BrokerageName.Bybit;

case DefaultBrokerageModel _:
return BrokerageName.Default;

Expand Down
3 changes: 3 additions & 0 deletions Common/Exchange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ public class Exchange
public static Exchange NYSELIFFE { get; }
= new("NYSELIFFE", "NYSELIFFE", "London International Financial Futures and Options Exchange", QuantConnect.Market.NYSELIFFE, SecurityType.Future, SecurityType.FutureOption);

public static Exchange Bybit { get; }
= new("Bybit", "BYBIT", "Bybit", QuantConnect.Market.Bybit, SecurityType.CryptoFuture, SecurityType.Crypto); //todo is this correct, the other markets don't seem to have this

/// <summary>
/// Exchange description
/// </summary>
Expand Down
8 changes: 7 additions & 1 deletion Common/Market.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public static class Market
Tuple.Create(CFE, 33),
Tuple.Create(FTX, 34),
Tuple.Create(FTXUS, 35),
Tuple.Create(BinanceUS, 36)
Tuple.Create(BinanceUS, 36),
Tuple.Create(Bybit, 37)
};

static Market()
Expand Down Expand Up @@ -230,6 +231,11 @@ static Market()
/// </summary>
public const string BinanceUS = "binanceus";

/// <summary>
/// Bybit
/// </summary>
public const string Bybit = "bybit";

/// <summary>
/// Adds the specified market to the map of available markets with the specified identifier.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions Common/Orders/BybitOrderProperties.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace QuantConnect.Orders;

public class BybitOrderProperties : OrderProperties
{

/// <summary>
/// This flag will ensure the order executes only as a maker (no fee) order.
/// If part of the order results in taking liquidity rather than providing,
/// it will be rejected and no part of the order will execute.
/// Note: this flag is only applied to Limit orders.
/// </summary>
public bool PostOnly { get; set; }
}
95 changes: 95 additions & 0 deletions Common/Orders/Fees/BybitFeeModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using QuantConnect.Securities;
using QuantConnect.Util;

namespace QuantConnect.Orders.Fees;

public class BybitFeeModel : FeeModel
{
/// <summary>
/// Tier 1 maker fees
/// https://learn.bybit.com/bybit-guide/bybit-trading-fees/
/// </summary>
public const decimal MakerNonVIPFee = 0.001m;

/// <summary>
/// Tier 1 taker fees
/// https://learn.bybit.com/bybit-guide/bybit-trading-fees/
/// </summary>
public const decimal TakerNonVIPFee = 0.001m;

private readonly decimal _makerFee;
private readonly decimal _takerFee;

/// <summary>
/// Creates Binance fee model setting fees values
/// </summary>
/// <param name="mFee">Maker fee value</param>
/// <param name="tFee">Taker fee value</param>
public BybitFeeModel(decimal mFee = MakerNonVIPFee, decimal tFee = TakerNonVIPFee)
{
_makerFee = mFee;
_takerFee = tFee;
}

public override OrderFee GetOrderFee(OrderFeeParameters parameters)
{
var security = parameters.Security;
var order = parameters.Order;

var fee = GetFee(order);

if(security.Symbol.ID.SecurityType == SecurityType.CryptoFuture)
{
var positionValue = security.Holdings.GetQuantityValue(order.AbsoluteQuantity, security.Price);
return new OrderFee(new CashAmount(positionValue.Amount * fee, positionValue.Cash.Symbol));
}

if (order.Direction == OrderDirection.Buy)
{
// fees taken in the received currency
CurrencyPairUtil.DecomposeCurrencyPair(order.Symbol, out var baseCurrency, out _);
return new OrderFee(new CashAmount(order.AbsoluteQuantity * fee, baseCurrency));
}

// get order value in quote currency
var unitPrice = order.Direction == OrderDirection.Buy ? security.AskPrice : security.BidPrice;
if (order.Type == OrderType.Limit)
{
// limit order posted to the order book
unitPrice = ((LimitOrder)order).LimitPrice;
}

unitPrice *= security.SymbolProperties.ContractMultiplier;

return new OrderFee(new CashAmount(
unitPrice * order.AbsoluteQuantity * fee,
security.QuoteCurrency.Symbol));
}

/// <summary>
/// Gets the fee factor for the given order
/// </summary>
/// <param name="order">The order to get the fee factor for</param>
/// <returns>The fee factor for the given order</returns>
protected virtual decimal GetFee(Order order)
{
return GetFee(order, _makerFee, _takerFee);
}

protected static decimal GetFee(Order order, decimal makerFee, decimal takerFee)
{
// apply fee factor, currently we do not model 30-day volume, so we use the first tier
var fee = takerFee;
var props = order.Properties as BybitOrderProperties;

if (order.Type == OrderType.Limit && ((props != null && props.PostOnly) || !order.IsMarketable))
{
// limit order posted to the order book
fee = makerFee;
}

return fee;
}
}
Loading

0 comments on commit cc0d406

Please sign in to comment.