// Notes: // - This is the simplest example of a websocket stream. The design is // A cache of ticker data that contains an instance of a stream object // that handles the interaction with the WebSocket. Clients access data // in the cache which lazily creates the stream object. If a socket error // occurs, the stream object disposes itself. The next client access will // create a new stream object. // - The general flow of a stream object is to take a snapshot of the data, // connect the web socket, and handle messages to keep the data up to date. // - There shouldn't be any need for a "watchdog" mechanism, each stream object // should be able to tell when its connection is in a bad state and dispose // itself. Reconnection isn't necessary, the client triggers that with the // next access. public TickerDataCache(BinanceApi api) { Api = api; Streams = new Dictionary <int, TickerStream>(); }
// Notes: // - See TickerDataCache for the simplest example // - Using one socket per currency pair because there is no mechanism // for adding/removing subscriptions to streams. public MarketDataCache(BinanceApi api) { Api = api; Streams = new Dictionary <CurrencyPair, MarketStream>(); }
/// <summary>Round parameters to match the server rules</summary> public OrderParams Canonicalise(CurrencyPair pair, BinanceApi api) { // Canonicalise doesn't throw, it just does it's best. // Use Validate to get error messages. // Find the rules for 'cp'. Valid if no rules found var rules = api.SymbolRules[pair]; if (rules == null) { return(this); } var ticker = api.TickerData[pair]; if (Type == EOrderType.MARKET) { PriceQ2B = ticker.PriceQ2B; } if (!Type.IsAlgo()) { StopPriceQ2B = null; } else if (StopPriceQ2B == null) { StopPriceQ2B = PriceQ2B; } // Truncate to the expected precision. Can't round because we might round to a value greater than the balance AmountBase = Math_.Truncate(AmountBase, rules.BaseAssetPrecision); if (PriceQ2B != null) { PriceQ2B = Math_.Truncate(PriceQ2B.Value, rules.PricePrecision); } if (StopPriceQ2B != null) { StopPriceQ2B = Math_.Truncate(StopPriceQ2B.Value, rules.PricePrecision); } if (IcebergAmountBase != null) { IcebergAmountBase = Math_.Truncate(IcebergAmountBase.Value, rules.BaseAssetPrecision); } // Round to the tick size foreach (var filter in rules.Filters.OfType <ServerRulesData.FilterPrice>().Where(x => x.FilterType == EFilterType.PRICE_FILTER)) { if (PriceQ2B != null) { PriceQ2B = filter.Round(PriceQ2B.Value); } if (StopPriceQ2B != null) { StopPriceQ2B = filter.Round(StopPriceQ2B.Value); } } // Round to the lot size var filter_type = Type != EOrderType.MARKET ? EFilterType.LOT_SIZE : EFilterType.MARKET_LOT_SIZE; foreach (var filter in rules.Filters.OfType <ServerRulesData.FilterLotSize>().Where(x => x.FilterType == filter_type)) { AmountBase = filter.Round(AmountBase); if (IcebergAmountBase != null) { IcebergAmountBase = filter.Round(IcebergAmountBase.Value); } } // Test against min notional foreach (var filter in rules.Filters.OfType <ServerRulesData.FilterMinNotional>().Where(x => x.FilterType == EFilterType.MIN_NOTIONAL)) { if (Type == EOrderType.MARKET && !filter.ApplyToMarketOrders) { continue; } AmountBase = filter.Round(AmountBase, PriceQ2B.Value); } return(this); }
/// <summary>Validate these parameters against the server rules</summary> public Exception Validate(CurrencyPair pair, BinanceApi api) { // If there are no rules for the pair, just hope... var rules = api.SymbolRules[pair]; if (rules == null) { return(null); } // Check the symbol is tradable if (rules.Status != ESymbolStatus.TRADING) { return(new Exception($"{pair.Id} is not available for trading")); } if (!rules.IsSpotTradingAllowed) { return(new Exception($"Spot trading {pair.Id} is not allowed")); } if (!rules.OrderTypes.Contains(Type)) { return(new Exception($"Order type {Type} is not support for {pair.Id}")); } if (PriceQ2B == null && Type != EOrderType.MARKET) { return(new Exception($"Order type {Type} requires a price parameter")); } if (StopPriceQ2B == null && Type.IsAlgo()) { return(new Exception($"Order type {Type} requires a spot price parameter")); } // Validate against the filters foreach (var filter in rules.Filters) { switch (filter.FilterType) { default: throw new Exception($"Unknown filter type: {filter.FilterType}"); case EFilterType.PRICE_FILTER: { var f = (ServerRulesData.FilterPrice)filter; if (PriceQ2B != null && PriceQ2B.Value > f.MaxPrice) { return(new Exception($"Trade price ({PriceQ2B.Value}) above maximum ({f.MaxPrice})")); } if (StopPriceQ2B != null && StopPriceQ2B.Value > f.MaxPrice) { return(new Exception($"Stop price ({StopPriceQ2B.Value}) above maximum ({f.MaxPrice})")); } if (PriceQ2B != null && PriceQ2B.Value < f.MinPrice) { return(new Exception($"Trade price ({PriceQ2B.Value}) below minimum ({f.MinPrice})")); } if (StopPriceQ2B != null && StopPriceQ2B.Value < f.MinPrice) { return(new Exception($"Stop price ({StopPriceQ2B.Value}) below minimum ({f.MinPrice})")); } if (PriceQ2B != null && (PriceQ2B.Value - f.MinPrice) % f.TickSize != 0) { return(new Exception($"Trade price ({PriceQ2B.Value}) must be a mulitple of the minimum tick size ({f.TickSize})")); } if (StopPriceQ2B != null && (StopPriceQ2B.Value - f.MinPrice) % f.TickSize != 0) { return(new Exception($"Stop price ({StopPriceQ2B.Value}) must be a mulitple of the minimum tick size ({f.TickSize})")); } break; } case EFilterType.PERCENT_PRICE: { var f = (ServerRulesData.FilterPercentPrice)filter; var ticker = api.TickerData[pair]; var lo = ticker.WeightedAvgPrice * f.MultiplierDown; var hi = ticker.WeightedAvgPrice * f.MultiplierUp; if (PriceQ2B != null && PriceQ2B.Value < lo) { return(new Exception($"Trade price ({PriceQ2B.Value}) is below the minimum average price band ({lo})")); } if (StopPriceQ2B != null && StopPriceQ2B.Value < lo) { return(new Exception($"Stop price ({StopPriceQ2B.Value}) is below the minimum average price band ({lo})")); } if (PriceQ2B != null && PriceQ2B.Value > hi) { return(new Exception($"Trade price ({PriceQ2B.Value}) is above the maximum average price band ({hi})")); } if (StopPriceQ2B != null && StopPriceQ2B.Value > hi) { return(new Exception($"Stop price ({StopPriceQ2B.Value}) is above the maximum average price band ({hi})")); } break; } case EFilterType.LOT_SIZE: case EFilterType.MARKET_LOT_SIZE: { var f = (ServerRulesData.FilterLotSize)filter; if ((Type == EOrderType.MARKET) == (filter.FilterType == EFilterType.MARKET_LOT_SIZE)) { if (AmountBase > f.MaxQuantity) { return(new Exception($"Trade amount ({AmountBase}) is above the maximum amount ({f.MaxQuantity})")); } if (AmountBase < f.MinQuantity) { return(new Exception($"Trade amount ({AmountBase}) is below the minimum amount ({f.MinQuantity})")); } if ((AmountBase - f.MinQuantity) % f.StepSize != 0) { return(new Exception($"Trade amount ({AmountBase}) must be a multiple of the step size ({f.StepSize})")); } } break; } case EFilterType.MIN_NOTIONAL: { var f = (ServerRulesData.FilterMinNotional)filter; if (Type != EOrderType.MARKET || f.ApplyToMarketOrders) { var ticker = api.TickerData[pair]; var price_q2b = Type != EOrderType.MARKET ? PriceQ2B.Value : (decimal)ticker.PriceQ2B; var value = price_q2b * AmountBase; if (value < f.MinNotional) { return(new Exception($"Trade notional value ({value}) is below the minimum ({f.MinNotional})")); } } break; } case EFilterType.ICEBERG_PARTS: { var f = (ServerRulesData.FilterLimit)filter; if (IcebergAmountBase is decimal iceberg_amount) { var parts = Math.Ceiling(AmountBase / iceberg_amount); if (parts > f.Limit) { return(new Exception($"Number of iceberg parts ({parts}) is above the limit ({f.Limit})")); } } break; } case EFilterType.MAX_NUM_ORDERS: { var f = (ServerRulesData.FilterLimit)filter; var num = api.UserData.Orders[pair].Count; if (num > f.Limit) { return(new Exception($"Creating this trade would exceed the number of allowed orders. (Limit = ({f.Limit})")); } break; } case EFilterType.MAX_NUM_ALGO_ORDERS: { var f = (ServerRulesData.FilterLimit)filter; if (Type.IsAlgo()) { var num = api.UserData.Orders[pair].Count(x => x.OrderType.IsAlgo()); if (num > f.Limit) { return(new Exception($"Creating this trade would exceed the number of allowed algorithm orders. (Limit = ({f.Limit})")); } } break; } case EFilterType.MAX_NUM_ICEBERG_ORDERS: { var f = (ServerRulesData.FilterLimit)filter; if (IcebergAmountBase > 0) { var num = api.UserData.Orders[pair].Count(x => x.IcebergAmount > 0); if (num > f.Limit) { return(new Exception($"Creating this trade would exceed the number of allowed iceberg orders. (Limit = ({f.Limit})")); } } break; } case EFilterType.EXCHANGE_MAX_NUM_ORDERS: { // Todo, need the total number of orders on the exchange break; } case EFilterType.EXCHANGE_MAX_NUM_ALGO_ORDERS: { // Todo, need the total number of "Algo" orders on the exchange break; } } } // Sweet as bro return(null); }
// Notes: // - See TickerDataCache for the simplest example public CandleDataCache(BinanceApi api) { Api = api; Streams = new Dictionary <PairAndTF, CandleStream>(); }
// Notes: // - See TickerDataCache for the simplest example public UserDataCache(BinanceApi api) { Api = api; Streams = new Dictionary <int, UserDataStream>(); }