// 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>();
        }
Beispiel #3
0
        /// <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);
        }
Beispiel #4
0
        /// <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);
        }
Beispiel #5
0
        // Notes:
        //  - See TickerDataCache for the simplest example

        public CandleDataCache(BinanceApi api)
        {
            Api     = api;
            Streams = new Dictionary <PairAndTF, CandleStream>();
        }
Beispiel #6
0
        // Notes:
        //  - See TickerDataCache for the simplest example

        public UserDataCache(BinanceApi api)
        {
            Api     = api;
            Streams = new Dictionary <int, UserDataStream>();
        }