public override IWebSocket GetDepthWebSocket(Action <ExchangeDepth> callback, int maxCount = 20, params Symbol[] symbols) { /* * {[ * { * "binary": 0, * "channel": "addChannel", * "data": { * "result": true, * "channel": "ok_sub_spot_bch_btc_depth_5" * } * } * ]} * * {[ * { * "data": { * "asks": [ * [ * "8364.1163", * "0.005" * ], * * ], * "bids": [ * [ * "8335.99", * "0.01837999" * ], * [ * "8335.9899", * "0.06" * ], * ], * "timestamp": 1526734386064 * }, * "binary": 0, * "channel": "ok_sub_spot_btc_usdt_depth_20" * } * ]} * */ return(ConnectWebSocketOkex(async(_socket) => { //todo symbols = await AddMarketSymbolsToChannel(_socket, $"ok_sub_spot_{{0}}_depth_{maxCount}", symbols); }, (_socket, symbol, sArray, token) => { ExchangeDepth book = token.ParseDepthFromJTokenArrays(null, sequence: "timestamp", maxCount: maxCount); //todo:改为symbol callback(book); return Task.CompletedTask; })); }
/// <summary> /// Common order book parsing method, most exchanges use "asks" and "bids" with /// arrays of length 2 for price and amount (or amount and price) /// </summary> /// <param name="token">Token</param> /// <param name="symbol"></param> /// <param name="asks">Asks key</param> /// <param name="bids">Bids key</param> /// <param name="timestampType"></param> /// <param name="maxCount">Max count</param> /// <param name="sequence"></param> /// <returns>Order book</returns> internal static ExchangeDepth ParseDepthFromJTokenArrays ( this JToken token, Symbol symbol, string asks = "asks", string bids = "bids", string sequence = "ts", TimestampType timestampType = TimestampType.None, int maxCount = 100 ) { var book = new ExchangeDepth { SequenceId = token[sequence].ConvertInvariant <long>(), LastUpdatedUtc = CryptoUtility.ParseTimestamp(token[sequence], timestampType), Symbol = symbol }; foreach (JArray array in token[asks]) { var depth = new ExchangeOrderPrice { Price = array[0].ConvertInvariant <decimal>(), Amount = array[1].ConvertInvariant <decimal>() }; book.Asks[depth.Price] = depth; if (book.Asks.Count == maxCount) { break; } } foreach (JArray array in token[bids]) { var depth = new ExchangeOrderPrice { Price = array[0].ConvertInvariant <decimal>(), Amount = array[1].ConvertInvariant <decimal>() }; book.Bids[depth.Price] = depth; if (book.Bids.Count == maxCount) { break; } } return(book); }
/// <summary> /// Common order book parsing method, checks for "amount" or "quantity" and "price" /// elements /// </summary> /// <param name="token">Token</param> /// <param name="asks">Asks key</param> /// <param name="bids">Bids key</param> /// <param name="price">Price key</param> /// <param name="amount">Quantity key</param> /// <param name="sequence">Sequence key</param> /// <param name="maxCount">Max count</param> /// <returns>Order book</returns> internal static ExchangeDepth ParseDepthFromJTokenDictionaries ( this JToken token, string asks = "asks", string bids = "bids", string price = "price", string amount = "amount", string sequence = "ts", int maxCount = 100 ) { var book = new ExchangeDepth { SequenceId = token[sequence].ConvertInvariant <long>() }; foreach (var ask in token[asks]) { var depth = new ExchangeOrderPrice { Price = ask[price].ConvertInvariant <decimal>(), Amount = ask[amount].ConvertInvariant <decimal>() }; book.Asks[depth.Price] = depth; if (book.Asks.Count == maxCount) { break; } } foreach (var bid in token[bids]) { var depth = new ExchangeOrderPrice { Price = bid[price].ConvertInvariant <decimal>(), Amount = bid[amount].ConvertInvariant <decimal>() }; book.Bids[depth.Price] = depth; if (book.Bids.Count == maxCount) { break; } } return(book); }
/// <summary> /// Place a limit order by first querying the order book and then placing the order for a threshold below the bid or /// above the ask that would fully fulfill the amount. /// The order book is scanned until an amount of bids or asks that will fulfill the order is found and then the order /// is placed at the lowest bid or highest ask price multiplied /// by priceThreshold. /// </summary> /// <param name="api"></param> /// <param name="symbol">Symbol to sell</param> /// <param name="amount">Amount to sell</param> /// <param name="isBuy">True for buy, false for sell</param> /// <param name="orderBookCount">Amount of bids/asks to request in the order book</param> /// <param name="priceThreshold"> /// Threshold below the lowest bid or above the highest ask to set the limit order price at. For buys, this is /// converted to 1 / priceThreshold. /// This can be set to 0 if you want to set the price like a market order. /// </param> /// <param name="thresholdToAbort"> /// If the lowest bid/highest ask price divided by the highest bid/lowest ask price is below this threshold, throw an /// exception. /// This ensures that your order does not buy or sell at an extreme margin. /// </param> /// <param name="abortIfOrderBookTooSmall"> /// Whether to abort if the order book does not have enough bids or ask amounts to /// fulfill the order. /// </param> /// <returns>Order result</returns> public static async Task <List <ExchangeOrderResult> > PlaceSafeMarketOrderAsync( this ExchangeAPI api, string symbol, decimal amount, bool isBuy, int orderBookCount = 100, decimal priceThreshold = 0.9m, decimal thresholdToAbort = 0.75m, bool abortIfOrderBookTooSmall = false) { if (priceThreshold > 0.9m) { throw new APIException( "You cannot specify a price threshold above 0.9m, otherwise there is a chance your order will never be fulfilled. For buys, this is " + "converted to 1.0m / priceThreshold, so always specify the value below 0.9m"); } if (priceThreshold <= 0m) { priceThreshold = 1m; } else if (isBuy && priceThreshold > 0m) { priceThreshold = 1.0m / priceThreshold; } ExchangeDepth book = null; //todo await api.GetDepthAsync(symbol, orderBookCount); if (book == null || isBuy && book.Asks.Count == 0 || !isBuy && book.Bids.Count == 0) { throw new APIException($"Error getting order book for {symbol}"); } var counter = 0m; var highPrice = decimal.MinValue; var lowPrice = decimal.MaxValue; if (isBuy) { foreach (var ask in book.Asks.Values) { counter += ask.Amount; highPrice = Math.Max(highPrice, ask.Price); lowPrice = Math.Min(lowPrice, ask.Price); if (counter >= amount) { break; } } } else { foreach (var bid in book.Bids.Values) { counter += bid.Amount; highPrice = Math.Max(highPrice, bid.Price); lowPrice = Math.Min(lowPrice, bid.Price); if (counter >= amount) { break; } } } if (abortIfOrderBookTooSmall && counter < amount) { throw new APIException( $"{(isBuy ? "Buy" : "Sell")} order for {symbol} and amount {amount} cannot be fulfilled because the order book is too thin."); } if (lowPrice / highPrice < thresholdToAbort) { throw new APIException( $"{(isBuy ? "Buy" : "Sell")} order for {symbol} and amount {amount} would place for a price below threshold of {thresholdToAbort}, aborting."); } var request = new ExchangeOrderRequest { Amount = amount, OrderType = OrderType.Limit, Price = CryptoUtility.RoundAmount((isBuy ? highPrice : lowPrice) * priceThreshold), ShouldRoundAmount = true, Symbol = null //todo symbol }; var result = await api.PlaceOrdersAsync(request); // wait about 10 seconds until the order is fulfilled var i = 0; const int maxTries = 20; // 500 ms for each try for (; i < maxTries; i++) { await Task.Delay(500); } //TODO //result = await api.GetCanceledOrdersAsync(result.OrderId, symbol); //switch (result.Result) //{ // case ExchangeAPIOrderResult.Filled: // case ExchangeAPIOrderResult.Canceled: // case ExchangeAPIOrderResult.Error: // break; //} if (i == maxTries) { throw new APIException( $"{(isBuy ? "Buy" : "Sell")} order for {symbol} and amount {amount} timed out and may not have been fulfilled"); } return(result); }