Skip to content

Commit

Permalink
Feature: support StopMarket OrderType (#39)
Browse files Browse the repository at this point in the history
* feat: support StopMarket
test:fix: transaction process
test:feat: Future StopMarket

* fix: GetAccountHoldings and GetCashBalance in CoinFuture

* test:fix: type of variable name

* test:feat: StopMarket in BinanceCoinFutures
  • Loading branch information
Romazes authored Dec 4, 2024
1 parent 1ff901f commit 57e9b15
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 30 deletions.
27 changes: 27 additions & 0 deletions QuantConnect.BinanceBrokerage.Tests/BinanceBrokerageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
using System.Threading;
using QuantConnect.Lean.Engine.DataFeeds;
using QuantConnect.Tests.Brokerages;
using QuantConnect.Data.Market;
using QuantConnect.Data;

namespace QuantConnect.Brokerages.Binance.Tests
{
Expand Down Expand Up @@ -205,5 +207,30 @@ protected override void ModifyOrderUntilFilled(Order order, OrderTestParameters
{
Assert.Pass("Order update not supported. Please cancel and re-create.");
}

public static Security CreateSecurity(Symbol symbol)
{
var timezone = TimeZones.NewYork;

var config = new SubscriptionDataConfig(
typeof(TradeBar),
symbol,
Resolution.Hour,
timezone,
timezone,
true,
false,
false);

return new Security(
SecurityExchangeHours.AlwaysOpen(timezone),
config,
new Cash(Currencies.USD, 0, 1),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
using System;
using System.Linq;
using NUnit.Framework;
using QuantConnect.Orders;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using System.Collections.Generic;
using QuantConnect.Configuration;
using QuantConnect.Tests.Brokerages;
using QuantConnect.Lean.Engine.DataFeeds;
Expand All @@ -41,16 +43,17 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec
{
var securities = new SecurityManager(new TimeKeeper(DateTime.UtcNow, TimeZones.NewYork))
{
{ Symbol, CreateSecurity(Symbol) }
{ Symbol, BinanceBrokerageTests.CreateSecurity(Symbol) }
};
var algorithmSettings = new AlgorithmSettings();
var transactions = new SecurityTransactionManager(null, securities);
transactions.SetOrderProcessor(new FakeOrderProcessor());
var orderProcessor = new FakeOrderProcessor();
transactions.SetOrderProcessor(orderProcessor);

var algorithm = new Mock<IAlgorithm>();
algorithm.Setup(a => a.Transactions).Returns(transactions);
algorithm.Setup(a => a.Securities).Returns(securities);
algorithm.Setup(a => a.BrokerageModel).Returns(new BinanceBrokerageModel());
algorithm.Setup(a => a.BrokerageModel).Returns(new BinanceCoinFuturesBrokerageModel(AccountType.Margin));
algorithm.Setup(a => a.Portfolio).Returns(new SecurityPortfolioManager(securities, transactions, algorithmSettings));

var apiKey = Config.Get("binance-api-key");
Expand All @@ -65,7 +68,7 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec
apiSecret,
apiUrl);

return new BinanceCoinFuturesBrokerage(
var brokerage = new BinanceCoinFuturesBrokerage(
apiKey,
apiSecret,
apiUrl,
Expand All @@ -74,6 +77,26 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec
new AggregationManager(),
null
);

brokerage.OrdersStatusChanged += (sender, orderEvents) =>
{
var orderEvent = orderEvents[0];

switch (orderEvent.Status)
{
case OrderStatus.Submitted:
var externalOrder = OrderProvider.GetOrderById(orderEvent.OrderId);
orderProcessor.AddOrder(externalOrder);
break;
case OrderStatus.Canceled:
case OrderStatus.Filled:
var order = orderProcessor.GetOrderById(orderEvent.OrderId);
order.Status = orderEvent.Status;
break;
};
};

return brokerage;
}

/// <summary>
Expand All @@ -85,28 +108,32 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec
/// Gets the symbol to be traded, must be shortable
/// </summary>
protected override Symbol Symbol => StaticSymbol;
private static Symbol StaticSymbol => Symbol.Create("XRPUSD", SecurityType.CryptoFuture, Market.Binance);
private static Symbol StaticSymbol => Symbol.Create("BTCUSD", SecurityType.CryptoFuture, Market.Binance);

/// <summary>
/// Gets the security type associated with the <see cref="BrokerageTests.Symbol" />
/// </summary>
protected override SecurityType SecurityType => Symbol.SecurityType;

public static TestCaseData[] OrderParametersFutures => new[]
public static IEnumerable<TestCaseData> OrderParametersFutures
{
new TestCaseData(new MarketOrderTestParameters(StaticSymbol)).SetName("MarketOrder"),
new TestCaseData(new LimitOrderTestParameters(StaticSymbol, HighPrice, LowPrice)).SetName("LimitOrder"),
};
get
{
yield return new TestCaseData(new MarketOrderTestParameters(StaticSymbol));
yield return new TestCaseData(new LimitOrderTestParameters(StaticSymbol, HighPrice, LowPrice));
yield return new TestCaseData(new StopMarketOrderTestParameters(StaticSymbol, HighPrice, LowPrice));
}
}

/// <summary>
/// Gets a high price for the specified symbol so a limit sell won't fill
/// </summary>
private const decimal HighPrice = 0.5m;
private const decimal HighPrice = 100_000m;

/// <summary>
/// Gets a low price for the specified symbol so a limit buy won't fill
/// </summary>
private const decimal LowPrice = 0.2m;
private const decimal LowPrice = 90_000m;

/// <summary>
/// Gets the current market price of the specified security
Expand All @@ -130,7 +157,7 @@ protected override decimal GetAskPrice(Symbol symbol)
/// <summary>
/// Gets the default order quantity. Min order 5USD.
/// </summary>
protected override decimal GetDefaultQuantity() => 30m;
protected override decimal GetDefaultQuantity() => 1m;

[Explicit("This test requires a configured and testable Binance practice account")]
[Test, TestCaseSource(nameof(OrderParametersFutures))]
Expand Down
48 changes: 38 additions & 10 deletions QuantConnect.BinanceBrokerage.Tests/BinanceFuturesBrokerageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
using System;
using System.Linq;
using NUnit.Framework;
using QuantConnect.Orders;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using QuantConnect.Configuration;
using System.Collections.Generic;
using QuantConnect.Tests.Brokerages;
using QuantConnect.Lean.Engine.DataFeeds;
using QuantConnect.Tests.Common.Securities;
Expand All @@ -41,16 +43,17 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec
{
var securities = new SecurityManager(new TimeKeeper(DateTime.UtcNow, TimeZones.NewYork))
{
{ Symbol, CreateSecurity(Symbol) }
{ Symbol, BinanceBrokerageTests.CreateSecurity(Symbol) }
};
var algorithmSettings = new AlgorithmSettings();
var transactions = new SecurityTransactionManager(null, securities);
transactions.SetOrderProcessor(new FakeOrderProcessor());
var orderProcessor = new FakeOrderProcessor();
transactions.SetOrderProcessor(orderProcessor);

var algorithm = new Mock<IAlgorithm>();
algorithm.Setup(a => a.Transactions).Returns(transactions);
algorithm.Setup(a => a.Securities).Returns(securities);
algorithm.Setup(a => a.BrokerageModel).Returns(new BinanceBrokerageModel());
algorithm.Setup(a => a.BrokerageModel).Returns(new BinanceFuturesBrokerageModel(AccountType.Margin));
algorithm.Setup(a => a.Portfolio).Returns(new SecurityPortfolioManager(securities, transactions, algorithmSettings));

var apiKey = Config.Get("binance-api-key");
Expand All @@ -65,7 +68,7 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec
apiSecret,
apiUrl);

return new BinanceFuturesBrokerage(
var brokerage = new BinanceFuturesBrokerage(
apiKey,
apiSecret,
apiUrl,
Expand All @@ -74,6 +77,26 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec
new AggregationManager(),
null
);

brokerage.OrdersStatusChanged += (sender, orderEvents) =>
{
var orderEvent = orderEvents[0];

switch (orderEvent.Status)
{
case OrderStatus.Submitted:
var externalOrder = OrderProvider.GetOrderById(orderEvent.OrderId);
orderProcessor.AddOrder(externalOrder);
break;
case OrderStatus.Canceled:
case OrderStatus.Filled:
var order = orderProcessor.GetOrderById(orderEvent.OrderId);
order.Status = orderEvent.Status;
break;
};
};

return brokerage;
}

/// <summary>
Expand All @@ -92,21 +115,26 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec
/// </summary>
protected override SecurityType SecurityType => Symbol.SecurityType;

public static TestCaseData[] OrderParametersFutures => new[]
public static IEnumerable<TestCaseData> OrderParametersFutures
{
new TestCaseData(new MarketOrderTestParameters(StaticSymbol)).SetName("MarketOrder"),
new TestCaseData(new LimitOrderTestParameters(StaticSymbol, HighPrice, LowPrice)).SetName("LimitOrder"),
};
get
{
yield return new TestCaseData(new MarketOrderTestParameters(StaticSymbol));
yield return new TestCaseData(new LimitOrderTestParameters(StaticSymbol, HighPrice, LowPrice));
yield return new TestCaseData(new StopMarketOrderTestParameters(StaticSymbol, HighPrice, LowPrice));
}

}

/// <summary>
/// Gets a high price for the specified symbol so a limit sell won't fill
/// </summary>
private const decimal HighPrice = 0.5m;
private const decimal HighPrice = 3m;

/// <summary>
/// Gets a low price for the specified symbol so a limit buy won't fill
/// </summary>
private const decimal LowPrice = 0.2m;
private const decimal LowPrice = 2m;

/// <summary>
/// Gets the current market price of the specified security
Expand Down
13 changes: 11 additions & 2 deletions QuantConnect.BinanceBrokerage/BinanceBaseRestApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ protected virtual IDictionary<string, object> CreateOrderBody(Order order)
case StopLimitOrder stopLimitOrder:
if (order.SecurityType == SecurityType.CryptoFuture)
{
throw new NotSupportedException($"BinanceBrokerage.ConvertOrderType: Unsupported order type: {order.Type} for {SecurityType.CryptoFuture}");
throw new NotSupportedException($"{nameof(BinanceBaseRestApiClient)}.{nameof(CreateOrderBody)}: Unsupported order type: {order.Type} for {SecurityType.CryptoFuture}");
}
var ticker = GetTickerPrice(order);
var stopPrice = stopLimitOrder.StopPrice;
Expand All @@ -298,9 +298,18 @@ protected virtual IDictionary<string, object> CreateOrderBody(Order order)
body["timeInForce"] = "GTC";
body["stopPrice"] = stopPrice.ToStringInvariant();
body["price"] = stopLimitOrder.LimitPrice.ToStringInvariant();
break;
case StopMarketOrder stopMarketOrder:
if (order.SecurityType == SecurityType.Crypto)
{
throw new NotSupportedException($"{nameof(BinanceBaseRestApiClient)}.{nameof(CreateOrderBody)}: Unsupported order type: {order.Type} for {SecurityType.Crypto}");
}
body["type"] = "STOP_MARKET";
body["stopPrice"] = stopMarketOrder.StopPrice.ToStringInvariant();

break;
default:
throw new NotSupportedException($"BinanceBrokerage.ConvertOrderType: Unsupported order type: {order.Type}");
throw new NotSupportedException($"{nameof(BinanceBaseRestApiClient)}.{nameof(CreateOrderBody)}: Unsupported order type: {order.Type}");
}

return body;
Expand Down
4 changes: 4 additions & 0 deletions QuantConnect.BinanceBrokerage/BinanceBrokerage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ public override List<Order> GetOpenOrders()
order = new StopLimitOrder(orderLeanSymbol, orderQuantity, item.StopPrice, item.Price, orderTime);
break;

case "STOP_MARKET":
order = new StopMarketOrder(orderLeanSymbol, orderQuantity, item.StopPrice, orderTime);
break;

default:
OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, -1,
"BinanceBrokerage.GetOpenOrders: Unsupported order type returned from brokerage: " + item.Type));
Expand Down
20 changes: 20 additions & 0 deletions QuantConnect.BinanceBrokerage/BinanceCoinFuturesRestApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
*/

using QuantConnect.Securities;
using System.Collections.Generic;
using QuantConnect.Brokerages.Binance.Messages;

namespace QuantConnect.Brokerages.Binance
{
Expand Down Expand Up @@ -46,5 +48,23 @@ string restApiUrl
: base(symbolMapper, securityProvider, apiKey, apiSecret, restApiUrl)
{
}

/// <summary>
/// Gets all open positions
/// </summary>
/// <returns>The list of all account holdings</returns>
public override List<Holding> GetAccountHoldings()
{
return GetAccountHoldings(_prefix);
}

/// <summary>
/// Gets the total account cash balance for specified account type
/// </summary>
/// <returns></returns>
public override BalanceEntry[] GetCashBalance()
{
return GetCashBalance(_prefix);
}
}
}
34 changes: 28 additions & 6 deletions QuantConnect.BinanceBrokerage/BinanceFuturesRestApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@ namespace QuantConnect.Brokerages.Binance
/// </summary>
public class BinanceFuturesRestApiClient : BinanceBaseRestApiClient
{
/// <summary>
/// The API endpoint prefix for Binance Futures API version 1.
/// </summary>
private const string _prefix = "/fapi/v1";

/// <summary>
/// The API endpoint prefix for Binance Futures API version 2.
/// </summary>
private const string _prefixV2 = "/fapi/v2";

protected override JsonConverter CreateAccountConverter() => new FuturesAccountConverter();

/// <summary>
Expand Down Expand Up @@ -72,9 +80,28 @@ string restApiUrl
/// </summary>
/// <returns>The list of all account holdings</returns>
public override List<Holding> GetAccountHoldings()
{
return GetAccountHoldings(_prefixV2);
}

public override BalanceEntry[] GetCashBalance()
{
return GetCashBalance(_prefixV2);
}

/// <summary>
/// Retrieves the current account holdings for a specified API prefix from the Binance brokerage.
/// </summary>
/// <param name="apiPrefix">
/// The API endpoint prefix to be used for the request, typically including the base URL and version.
/// </param>
/// <returns>
/// A list of <see cref="Holding"/> objects representing the current positions with non-zero amounts.
/// </returns>
protected List<Holding> GetAccountHoldings(string apiPrefix)
{
var queryString = $"timestamp={GetNonce()}";
var endpoint = $"/fapi/v2/account?{queryString}&signature={AuthenticationToken(queryString)}";
var endpoint = $"{apiPrefix}/account?{queryString}&signature={AuthenticationToken(queryString)}";
var request = new RestRequest(endpoint, Method.GET);
request.AddHeader(KeyHeader, ApiKey);

Expand All @@ -96,10 +123,5 @@ public override List<Holding> GetAccountHoldings()
})
.ToList();
}

public override BalanceEntry[] GetCashBalance()
{
return GetCashBalance("/fapi/v2");
}
}
}

0 comments on commit 57e9b15

Please sign in to comment.