diff --git a/.editorconfig b/.editorconfig index 43784d5..7bb5195 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,3 +20,6 @@ dotnet_diagnostic.CS8600.severity = none # CS8601: 引用类型赋值可能为 null。 dotnet_diagnostic.CS8601.severity = none + +# CS8629: 可为 null 的值类型可为 null。 +dotnet_diagnostic.CS8629.severity = none diff --git a/Binance.TradeRobot.API/Controllers/OrderController.cs b/Binance.TradeRobot.API/Controllers/OrderController.cs index 8f52fd5..7962408 100644 --- a/Binance.TradeRobot.API/Controllers/OrderController.cs +++ b/Binance.TradeRobot.API/Controllers/OrderController.cs @@ -17,7 +17,7 @@ namespace Binance.TradeRobot.API.Controllers } /// - /// 获取现货/逐仓杠杆订单记录 + /// 获取现货/逐仓杠杆订单记录(最近20条) /// /// /// @@ -28,7 +28,7 @@ namespace Binance.TradeRobot.API.Controllers } /// - /// 获取执行日志记录 + /// 获取执行日志记录(最近50条) /// /// /// diff --git a/Binance.TradeRobot.Business/Binance.TradeRobot.Business.csproj b/Binance.TradeRobot.Business/Binance.TradeRobot.Business.csproj index 2f34003..366c9da 100644 --- a/Binance.TradeRobot.Business/Binance.TradeRobot.Business.csproj +++ b/Binance.TradeRobot.Business/Binance.TradeRobot.Business.csproj @@ -24,4 +24,8 @@ + + + + diff --git a/Binance.TradeRobot.Business/Business/OrderPublishBusiness/Spot/BaseSpotOrderPublishTradeBusiness.cs b/Binance.TradeRobot.Business/Business/OrderPublishBusiness/Spot/BaseSpotOrderPublishTradeBusiness.cs new file mode 100644 index 0000000..61aa287 --- /dev/null +++ b/Binance.TradeRobot.Business/Business/OrderPublishBusiness/Spot/BaseSpotOrderPublishTradeBusiness.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Caching.Memory; +using SDKAdapter.Model; +using System; +using Yitter.IdGenerator; + +namespace Binance.TradeRobot.Business +{ + public class BaseSpotOrderPublishTradeBusiness : BaseBusiness + { + public BaseSpotOrderPublishTradeBusiness(IFreeSql fsql, + NLogManager logManager, + IIdGenerator idGenerator, + IMemoryCache memoryCache) : base(fsql, logManager, idGenerator, memoryCache) { } + + public virtual void OnSpotOrderPublish(SpotOrderTradePublishInfo spotOrderTradePublishInfo) + { + throw new NotImplementedException(); + } + } +} diff --git a/Binance.TradeRobot.Business/Business/OrderPublishBusiness/Spot/D21OrderPublishTradeBusiness.cs b/Binance.TradeRobot.Business/Business/OrderPublishBusiness/Spot/D21OrderPublishTradeBusiness.cs new file mode 100644 index 0000000..0d08849 --- /dev/null +++ b/Binance.TradeRobot.Business/Business/OrderPublishBusiness/Spot/D21OrderPublishTradeBusiness.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Binance.TradeRobot.Business.Business.OrderPublishBusiness.Spot +{ + internal class D21OrderPublishTradeBusiness + { + } +} diff --git a/Binance.TradeRobot.Business/Business/TradeBusiness/BaseTradeBusiness.cs b/Binance.TradeRobot.Business/Business/TradeBusiness/BaseTradeBusiness.cs index a3a5a4a..1942324 100644 --- a/Binance.TradeRobot.Business/Business/TradeBusiness/BaseTradeBusiness.cs +++ b/Binance.TradeRobot.Business/Business/TradeBusiness/BaseTradeBusiness.cs @@ -41,11 +41,17 @@ namespace Binance.TradeRobot.Business dingBusiness.Send($"{errorMsg} {ex.Message}"); } - protected string CreateClientOrderId(long robotId) + /// + /// 创建客户端订单号 + /// + /// + /// + /// + protected string CreateClientOrderId(long robotId, Enums.TradePolicy tradePolicy) { var guid = Guid.NewGuid(); var random = new Random(guid.GetHashCode()); - return $"{Convert.ToChar(random.Next(97, 123))}{guid.ToString().Substring(0, 4)}_{robotId}"; + return $"{Convert.ToChar(random.Next(97, 123))}{guid.ToString().Substring(0, 4)}_{robotId}_{(int)tradePolicy}"; } } } diff --git a/Binance.TradeRobot.Business/Business/TradeBusiness/D21TradeBusiness.cs b/Binance.TradeRobot.Business/Business/TradeBusiness/Spot/D21TradeBusiness.cs similarity index 99% rename from Binance.TradeRobot.Business/Business/TradeBusiness/D21TradeBusiness.cs rename to Binance.TradeRobot.Business/Business/TradeBusiness/Spot/D21TradeBusiness.cs index 4fc8feb..1d193dc 100644 --- a/Binance.TradeRobot.Business/Business/TradeBusiness/D21TradeBusiness.cs +++ b/Binance.TradeRobot.Business/Business/TradeBusiness/Spot/D21TradeBusiness.cs @@ -209,7 +209,7 @@ namespace Binance.TradeRobot.Business #region 下单 step = "下单"; - var clientOrderId = CreateClientOrderId(robot.Id); + var clientOrderId = CreateClientOrderId(robot.Id, robot.TradePolicy); var orderId = apiClient.IsolatedMarginPlaceOrder(robot.Symbol, Enums.TradeDirection.Buy, Enums.OrderType.MARKET, @@ -313,7 +313,7 @@ namespace Binance.TradeRobot.Business var newestPrice = globalContext.GetSpotNewestPrice(robot.KLineKey) ?? singalRequest.ClosePrice; var apiClient = GetBaseAPIClient(robot.ExchangeId, robot.ExchangeAPIKey.AccountId, robot.ExchangeAPIKey.APIKey, robot.ExchangeAPIKey.SecretKey); - var clientOrderId = CreateClientOrderId(robot.Id); + var clientOrderId = CreateClientOrderId(robot.Id, robot.TradePolicy); var orderId = apiClient.IsolatedMarginPlaceOrder(robot.Symbol, Enums.TradeDirection.Sell, Enums.OrderType.MARKET, diff --git a/Binance.TradeRobot.Business/GlobalContext.cs b/Binance.TradeRobot.Business/GlobalContext.cs index 712611a..962823a 100644 --- a/Binance.TradeRobot.Business/GlobalContext.cs +++ b/Binance.TradeRobot.Business/GlobalContext.cs @@ -1,8 +1,9 @@ using Binance.TradeRobot.Common.DI; using Binance.TradeRobot.Model.Dto; using Microsoft.Extensions.DependencyInjection; +using SDKAdapter.Model; using SDKAdapter.WebSockets.Market; -using SDKAdapter.WebSockets.Order.SpotOrder; +using SDKAdapter.WebSockets.Order.Spot; using System.Collections.Generic; namespace Binance.TradeRobot.Business @@ -52,17 +53,17 @@ namespace Binance.TradeRobot.Business /// public void SubscribeOrderPublish(RobotResponse robot) { - if (!spotOrderWebSocketClientDictionary.TryGetValue(robot.OrderPublishListenKey, out SpotOrderWebSocketClient spotOrderWebSocketClient)) + if (!spotOrderWebSocketClientDictionary.TryGetValue(robot.OrderPublishKey, out SpotOrderWebSocketClient spotOrderWebSocketClient)) { spotOrderWebSocketClient = SpotOrderWebSocketClient.Create(robot.BusinessType, robot.ExchangeId, robot.ExchangeAPIKey.AccountId, robot.ExchangeAPIKey.APIKey, robot.ExchangeAPIKey.SecretKey, - logManager.GetLogger(robot.OrderPublishListenKey), - (e) => { }); + logManager.GetLogger(robot.OrderPublishKey), + OnSpotOrderPublish); } - spotOrderWebSocketClient.Start(); + spotOrderWebSocketClient.Start(robot.Symbol); } @@ -73,8 +74,8 @@ namespace Binance.TradeRobot.Business /// public void UnSubscribeOrderPublish(RobotResponse robot) { - if (spotOrderWebSocketClientDictionary.TryGetValue(robot.OrderPublishListenKey, out SpotOrderWebSocketClient spotOrderWebSocketClient)) - spotOrderWebSocketClient.Stop(); + if (spotOrderWebSocketClientDictionary.TryGetValue(robot.OrderPublishKey, out SpotOrderWebSocketClient spotOrderWebSocketClient)) + spotOrderWebSocketClient.Stop(robot.Symbol); } /// @@ -88,5 +89,10 @@ namespace Binance.TradeRobot.Business return spotMarketWebSocketClient.NewestPrice; return null; } + + public void OnSpotOrderPublish(SpotOrderTradePublishInfo spotOrderTradePublishInfo) + { + + } } } diff --git a/Binance.TradeRobot.Model/Dto/Response/Robot/RobotResponse.cs b/Binance.TradeRobot.Model/Dto/Response/Robot/RobotResponse.cs index 43d4b63..a989bd3 100644 --- a/Binance.TradeRobot.Model/Dto/Response/Robot/RobotResponse.cs +++ b/Binance.TradeRobot.Model/Dto/Response/Robot/RobotResponse.cs @@ -30,27 +30,27 @@ namespace Binance.TradeRobot.Model.Dto /// 订单推送监听实例Key /// 币安逐仓杠杆需要单独的运行实例 /// - public virtual string OrderPublishListenKey + public virtual string OrderPublishKey { get { string key = $"{BusinessType}-{ExchangeAPIKey.AccountId}"; //币安现货,币安合约,同一个账户内不区分websocket实例 if (ExchangeId == Enums.Exchange.Binance && BusinessType == Enums.BusinessType.IsolateMargin) key = $"{BusinessType}-{ExchangeAPIKey.AccountId}-{Symbol}"; //币安逐仓杠杆,同一个账户内的每个交易对需要区分websocket实例 - return $"OrderPublish(Origin)-{ExchangeId}-{key}"; + return $"OrderPublish-{ExchangeId}-{key}"; } } /// /// 订单推送日志Key /// - public virtual string OrderPublishLogKey - { - get - { - return $"OrderPublish-{ExchangeId}-{BusinessType}-{Symbol}"; - } - } + //public virtual string OrderPublishLogKey + //{ + // get + // { + // return $"OrderPublish-{ExchangeId}-{BusinessType}-{Symbol}"; + // } + //} public SymbolInfoResponse SymbolInfo { get; set; } diff --git a/SDKAdapter/Model/SpotOrderTradePublishInfo.cs b/SDKAdapter/Model/SpotOrderTradePublishInfo.cs index 5eced01..782e97b 100644 --- a/SDKAdapter/Model/SpotOrderTradePublishInfo.cs +++ b/SDKAdapter/Model/SpotOrderTradePublishInfo.cs @@ -7,12 +7,16 @@ namespace SDKAdapter.Model { public class SpotOrderTradePublishInfo { + public long OrderId { get; set; } + + public long RobotId { get; set; } + + public Enums.TradePolicy TradePolicy { get; set; } + public Enums.Exchange Exchange { get; set; } public long AccountId { get; set; } - public long OrderId { get; set; } - public string ClientOrderId { get; set; } public string Symbol { get; set; } diff --git a/SDKAdapter/SDKAdapter.csproj b/SDKAdapter/SDKAdapter.csproj index 80ec790..0d2ae08 100644 --- a/SDKAdapter/SDKAdapter.csproj +++ b/SDKAdapter/SDKAdapter.csproj @@ -6,7 +6,7 @@ - + diff --git a/SDKAdapter/WebSockets/Order/Spot/BinanceSpotOrderWebSocketClient.cs b/SDKAdapter/WebSockets/Order/Spot/BinanceSpotOrderWebSocketClient.cs new file mode 100644 index 0000000..4e54df0 --- /dev/null +++ b/SDKAdapter/WebSockets/Order/Spot/BinanceSpotOrderWebSocketClient.cs @@ -0,0 +1,183 @@ +using Binance.Net.Clients; +using Binance.Net.Objects; +using Binance.TradeRobot.Model.Base; +using CryptoExchange.Net.Authentication; +using Newtonsoft.Json; +using SDKAdapter.Model; +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; + +namespace SDKAdapter.WebSockets.Order.Spot +{ + public class BinanceSpotOrderWebSocketClient : SpotOrderWebSocketClient + { + private BinanceSocketClient binanceSocketClient; + private BinanceClient binanceClient; + private CancellationTokenSource cancellationTokenSource; + private string listenKey; + private IList ignoreOrderStateList; + + public BinanceSpotOrderWebSocketClient(Enums.BusinessType businessType, + long accountId, + string apiKey, + string secret, + NLog.ILogger logger, + Action onOrderUpdated) : base(businessType, + accountId, + apiKey, + secret, + logger, + onOrderUpdated) + { + var spotClientOption = new BinanceApiClientOptions() + { + BaseAddress = "https://api.binance.com", + ApiCredentials = new ApiCredentials(apiKey, secret) + }; + //var usdFuturesClientOption = new BinanceApiClientOptions() + //{ + // BaseAddress = "https://fapi.binance.com", + // ApiCredentials = new ApiCredentials(apiKey, secret) + //}; + binanceClient = new BinanceClient(new BinanceClientOptions() + { + //UsdFuturesApiOptions = usdFuturesClientOption, + SpotApiOptions = spotClientOption + }); + binanceSocketClient = new BinanceSocketClient(); + listenKey = string.Empty; + ignoreOrderStateList = new List() + { + Binance.Net.Enums.OrderStatus.New, + Binance.Net.Enums.OrderStatus.PendingCancel, + Binance.Net.Enums.OrderStatus.Insurance, + Binance.Net.Enums.OrderStatus.Adl + }; + } + + public override void Start(string symbol = "") + { + if (IsConnected) + return; + IsConnected = true; + cancellationTokenSource = new CancellationTokenSource(); + var getListenKeyResponse = binanceClient.SpotApi.Account.StartIsolatedMarginUserStreamAsync(symbol).Result; + if (!getListenKeyResponse.Success) + throw new Exception(getListenKeyResponse.Error?.Message ?? ""); + listenKey = getListenKeyResponse.Data; + _ = binanceSocketClient.SpotStreams.SubscribeToUserDataUpdatesAsync(listenKey, + (e) => + { + try + { + var originData = new StringBuilder(); + var isStop = false; + if (ignoreOrderStateList.Contains(e.Data.Status)) + { + originData.Append($"不支持的订单状态{e.Data.Status}"); + isStop = true; + } + + else if (string.IsNullOrEmpty(e.Data.ClientOrderId)) + { + originData.Append($"缺少ClientOrderId"); + isStop = true; + } + + long? robotId = null; + Enums.TradePolicy? tradePolicy = null; + + if (!isStop) + { + var match = Regex.Match(e.Data.ClientOrderId, @"^([a-z0-9]{5})_(\d{15,})_(\d{1,2})$"); + if (!match.Success) + { + originData.Append($"非机器人交易的ClientOrderId"); + isStop = true; + } + else + { + robotId = long.Parse(match.Groups[2].Value); + tradePolicy = (Enums.TradePolicy)int.Parse(match.Groups[3].Value); + } + } + originData.Append($" {JsonConvert.SerializeObject(e.Data)}"); + logger.Info(originData); + if (isStop) + return; + + var orderState = Enums.SpotOrderState.Unknow; + switch (e.Data.Status) + { + case Binance.Net.Enums.OrderStatus.PartiallyFilled: + case Binance.Net.Enums.OrderStatus.Filled: + case Binance.Net.Enums.OrderStatus.Canceled: + orderState = (Enums.SpotOrderState)(int)e.Data.Status; + break; + case Binance.Net.Enums.OrderStatus.Rejected: + orderState = Enums.SpotOrderState.Rejected; + break; + case Binance.Net.Enums.OrderStatus.Expired: + orderState = Enums.SpotOrderState.Expired; + break; + } + + OnOrderUpdated?.Invoke(new SpotOrderTradePublishInfo() + { + OrderId = e.Data.Id, + RobotId = robotId.Value, + TradePolicy = tradePolicy.Value, + Symbol = e.Data.Symbol, + AccountId = this.AccountId, + OrderType = (Enums.OrderType)(int)e.Data.Type, + SpotOrderState = orderState, + TradeDirection = (Enums.TradeDirection)(int)e.Data.Side, + ClientOrderId = e.Data.ClientOrderId, + CummulativeTradeAmount = e.Data.QuoteQuantityFilled, + CummulativeTradeQuantity = e.Data.QuantityFilled, + Exchange = Enums.Exchange.Binance, + Fee = e.Data.Fee, + FeeUnit = e.Data.FeeAsset, + LastTradeAmount = e.Data.LastQuoteQuantity, + LastTradePrice = e.Data.LastPriceFilled, + LastTradeQuantity = e.Data.LastQuantityFilled, + LastTradeTime = e.Data.UpdateTime, + CreateTime = e.Data.CreateTime + }); + } + catch (Exception ex) + { + logger.Error(ex); + } + }, + (e) => + { + + }, + (e) => + { + + }, + (e) => + { + + }, + cancellationTokenSource.Token); + } + + public override void Stop(string symbol = "") + { + if (!IsConnected) + return; + IsConnected = false; + cancellationTokenSource.Cancel(); + binanceSocketClient.SpotStreams.Dispose(); + cancellationTokenSource = null; + _ = binanceClient.SpotApi.Account.CloseIsolatedMarginUserStreamAsync(symbol, listenKey).Result; + listenKey = string.Empty; + } + } +} diff --git a/SDKAdapter/WebSockets/Order/SpotOrder/SpotOrderWebSocketClient.cs b/SDKAdapter/WebSockets/Order/Spot/SpotOrderWebSocketClient.cs similarity index 87% rename from SDKAdapter/WebSockets/Order/SpotOrder/SpotOrderWebSocketClient.cs rename to SDKAdapter/WebSockets/Order/Spot/SpotOrderWebSocketClient.cs index bc918c7..2cd05bd 100644 --- a/SDKAdapter/WebSockets/Order/SpotOrder/SpotOrderWebSocketClient.cs +++ b/SDKAdapter/WebSockets/Order/Spot/SpotOrderWebSocketClient.cs @@ -2,7 +2,7 @@ using SDKAdapter.Model; using System; -namespace SDKAdapter.WebSockets.Order.SpotOrder +namespace SDKAdapter.WebSockets.Order.Spot { public class SpotOrderWebSocketClient { @@ -19,17 +19,18 @@ namespace SDKAdapter.WebSockets.Order.SpotOrder public static SpotOrderWebSocketClient Create(Enums.BusinessType businessType, Enums.Exchange exchange, long accountId, string apiKey, string secret, NLog.ILogger logger, Action onOrderUpdated) { if (exchange == Enums.Exchange.Binance) - return new BinanceSpotOrderWebSocketClient(businessType, accountId, apiKey, secret, logger); + return new BinanceSpotOrderWebSocketClient(businessType, accountId, apiKey, secret, logger, onOrderUpdated); return null; } - public SpotOrderWebSocketClient(Enums.BusinessType businessType, long accountId, string apiKey, string secret, NLog.ILogger logger) + public SpotOrderWebSocketClient(Enums.BusinessType businessType, long accountId, string apiKey, string secret, NLog.ILogger logger, Action onOrderUpdated) { this.BusinessType = businessType; this.AccountId = accountId; this.ApiKey = apiKey; this.Secret = secret; this.logger = logger; + this.OnOrderUpdated = onOrderUpdated; } public virtual void Start(string symbol = "") diff --git a/SDKAdapter/WebSockets/Order/SpotOrder/BinanceSpotOrderWebSocketClient.cs b/SDKAdapter/WebSockets/Order/SpotOrder/BinanceSpotOrderWebSocketClient.cs deleted file mode 100644 index a11f6ad..0000000 --- a/SDKAdapter/WebSockets/Order/SpotOrder/BinanceSpotOrderWebSocketClient.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Binance.Net.Clients; -using Binance.Net.Objects; -using Binance.TradeRobot.Model.Base; -using CryptoExchange.Net.Authentication; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Threading; - -namespace SDKAdapter.WebSockets.Order.SpotOrder -{ - public class BinanceSpotOrderWebSocketClient : SpotOrderWebSocketClient - { - private BinanceSocketClient binanceSocketClient; - private BinanceClient binanceClient; - private CancellationTokenSource cancellationTokenSource; - private string listenKey; - private IList unSupportStateList; - - public BinanceSpotOrderWebSocketClient(Enums.BusinessType businessType, long accountId, string apiKey, string secret, NLog.ILogger logger) - : base(businessType, accountId, apiKey, secret, logger) - { - var spotClientOption = new BinanceApiClientOptions() - { - BaseAddress = "https://api.binance.com", - ApiCredentials = new ApiCredentials(apiKey, secret) - }; - //var usdFuturesClientOption = new BinanceApiClientOptions() - //{ - // BaseAddress = "https://fapi.binance.com", - // ApiCredentials = new ApiCredentials(apiKey, secret) - //}; - binanceClient = new BinanceClient(new BinanceClientOptions() - { - //UsdFuturesApiOptions = usdFuturesClientOption, - SpotApiOptions = spotClientOption - }); - binanceSocketClient = new BinanceSocketClient(); - listenKey = string.Empty; - unSupportStateList = new List() - { - Binance.Net.Enums.OrderStatus.PendingCancel, - Binance.Net.Enums.OrderStatus.Insurance, - Binance.Net.Enums.OrderStatus.Adl - }; - } - - public override void Start(string symbol = "") - { - if (IsConnected) - return; - IsConnected = true; - cancellationTokenSource = new CancellationTokenSource(); - var getListenKeyResponse = binanceClient.SpotApi.Account.StartIsolatedMarginUserStreamAsync(symbol).Result; - if (!getListenKeyResponse.Success) - throw new Exception(getListenKeyResponse.Error?.Message ?? ""); - listenKey = getListenKeyResponse.Data; - binanceSocketClient.SpotStreams.SubscribeToUserDataUpdatesAsync(listenKey, - (e) => - { - logger.Info(JsonConvert.SerializeObject(e.Data)); - if (unSupportStateList.Contains(e.Data.Status)) - return; - OnOrderUpdated?.Invoke(new Model.SpotOrderTradePublishInfo() - { - OrderId = e.Data.Id, - Symbol = e.Data.Symbol, - AccountId = this.AccountId, - OrderType = (Enums.OrderType)(int)e.Data.Type, - SpotOrderState = (Enums.SpotOrderState)(int)e.Data.Status, - TradeDirection = (Enums.TradeDirection)(int)e.Data.Side, - ClientOrderId = e.Data.ClientOrderId, - CummulativeTradeAmount = e.Data.QuoteQuantityFilled, - CummulativeTradeQuantity = e.Data.QuantityFilled, - Exchange = Enums.Exchange.Binance, - Fee = e.Data.Fee, - FeeUnit = e.Data.FeeAsset, - LastTradeAmount = e.Data.LastQuoteQuantity, - LastTradePrice = e.Data.LastPriceFilled, - LastTradeQuantity = e.Data.LastQuantityFilled, - LastTradeTime = e.Data.UpdateTime, - CreateTime = e.Data.CreateTime - }); - }, - (e) => - { - - }, - (e) => - { - - }, - (e) => - { - - }, - cancellationTokenSource.Token); - } - - public override void Stop(string symbol = "") - { - if (!IsConnected) - return; - IsConnected = false; - cancellationTokenSource.Cancel(); - binanceSocketClient.SpotStreams.Dispose(); - cancellationTokenSource = null; - _ = binanceClient.SpotApi.Account.CloseIsolatedMarginUserStreamAsync(symbol, listenKey).Result; - listenKey = string.Empty; - } - } -}