/// <summary>Stop the subscription</summary> public async Task Stop() { // Stop if in the running state if (State != EState.Running && State != EState.Starting) { return; } try { // Only disconnect from the same web socket if (WebSocket == null || WebSocket != Api.WebSocket) { return; } // Disconnect if open if (WebSocket.State == WebSocketState.Open && ChannelId != null) { // Send the unsubscribe request State = EState.Stopping; var json_request = UnsubscribeRequest(); using (Task_.NoSyncContext()) await Api.WebSocket.SendAsync(json_request, Api.Shutdown); } } catch { State = EState.Initial; throw; } }
/// <summary>Subscribe to the channel. Must be called when an open web socket connection is available</summary> public async Task Start() { if (State != EState.Initial) { throw new Exception($"Cannot start a subscription from state: {State}"); } // Prevent duplicate subscriptions await Stop(); try { // Record the web socket used to subscribe with. WebSocket = Api.WebSocket; if (WebSocket?.State == WebSocketState.Open) { // Send the subscription request State = EState.Starting; var json_request = SubscribeRequest(); using (Task_.NoSyncContext()) await WebSocket.SendAsync(json_request, Api.Shutdown); } } catch { State = EState.Initial; throw; } }
/// <summary>Helper for POSTs</summary> private async Task <JToken> PostData(string method, string command, CancellationToken?cancel, Params parameters = null) { // If called from the UI thread, disable the SynchronisationContext // to prevent deadlocks when waiting for Async results. using (Task_.NoSyncContext()) { // Poloniex requires the 'nonce' values to be strictly increasing. // That means all POSTs must be serialised to avoid a race condition // when POSTing two messages in quick succession. var cancel_token = CancellationTokenSource.CreateLinkedTokenSource(Shutdown, cancel ?? CancellationToken.None).Token; using (RequestThrottle.Lock(cancel_token)) // Limit requests to the required rate { await RequestThrottle.Wait(cancel_token); // Add the command parameter parameters = parameters ?? new Params(); parameters["command"] = command; parameters["nonce"] = Misc.Nonce; // Create the content to POST var post_data_string = Http_.UrlEncode(parameters).TrimStart('?'); var content = new StringContent(post_data_string, Encoding.UTF8, "application/x-www-form-urlencoded"); var msg_hash = Hasher.ComputeHash(Encoding.UTF8.GetBytes(post_data_string)); var signature = Misc.ToStringHex(msg_hash); content.Headers.Add("Sign", signature); var url = $"{Client.BaseAddress}{method}{Http_.UrlEncode(parameters)}"; // Submit the request // Result Codes: // - 422 Un-processable Entity: // Status code is directly reported by Poloniex server. It means the server understands the content type of the request entity, // and the syntax of the request entity is correct, but was unable to process the contained instructions. var response = await Client.PostAsync(url, content, cancel_token); if (!response.IsSuccessStatusCode) { throw new HttpException((int)response.StatusCode, response.ReasonPhrase); } // Interpret the reply var reply = await response.Content.ReadAsStringAsync(); return(JToken.Parse(reply)); } } }
/// <summary>Helper for GETs</summary> private async Task <JToken> GetData(string method, string command, CancellationToken?cancel, Params parameters = null) { // If called from the UI thread, disable the SynchronisationContext // to prevent deadlocks when waiting for Async results. using (Task_.NoSyncContext()) { var cancel_token = CancellationTokenSource.CreateLinkedTokenSource(Shutdown, cancel ?? CancellationToken.None).Token; using (RequestThrottle.Lock(cancel_token)) // Limit requests to the required rate { await RequestThrottle.Wait(cancel_token); // Add the API key for non-public methods parameters = parameters ?? new Params(); if (method != Method.Public) { parameters["apikey"] = Key; parameters["nonce"] = Misc.Nonce; } // Create the URL for the command + parameters var url = $"{UrlRestAddress}api/v1.1/{method}/{command}{Http_.UrlEncode(parameters)}"; // Construct the GET request var req = new HttpRequestMessage(HttpMethod.Get, url); if (method != Method.Public) { var hash = Hasher.ComputeHash(Encoding.UTF8.GetBytes(url)); req.Headers.Add("apisign", Misc.ToStringHex(hash)); } // Submit the request var response = await Client.SendAsync(req, cancel_token); if (!response.IsSuccessStatusCode) { throw new HttpException((int)response.StatusCode, response.ReasonPhrase); } // Interpret the reply var reply = await response.Content.ReadAsStringAsync(); return(JToken.Parse(reply)); } } }
/// <summary>Helper for GETs</summary> private async Task <JToken> GetData(string method, string command, CancellationToken?cancel, Params parameters = null) { // If called from the UI thread, disable the SynchronisationContext // to prevent deadlocks when waiting for Async results. using (Task_.NoSyncContext()) { // Poloniex requires the 'nonce' values to be strictly increasing. // That means all POSTs must be serialised to avoid a race condition // when POSTing two messages in quick succession. var cancel_token = CancellationTokenSource.CreateLinkedTokenSource(Shutdown, cancel ?? CancellationToken.None).Token; using (RequestThrottle.Lock(cancel_token)) // Limit requests to the required rate { await RequestThrottle.Wait(cancel_token); // Add the command to the parameters parameters = parameters ?? new Params(); parameters["command"] = command; // Create the URL for the command + parameters var url = $"{UrlRestAddress}{method}{Http_.UrlEncode(parameters)}"; // Submit the request var response = await Client.GetAsync(url, cancel_token); if (!response.IsSuccessStatusCode) { throw new HttpException((int)response.StatusCode, response.ReasonPhrase); } // Interpret the reply var reply = await response.Content.ReadAsStringAsync(); return(JToken.Parse(reply)); } } }
/// <summary>Helper for GETs</summary> private async Task <JToken> GetData(HttpMethod method, ESecurityType security, string command, CancellationToken?cancel, Params parameters = null, bool timestamp = false, bool log_trace = false) { // If called from the UI thread, disable the SynchronisationContext // to prevent deadlocks when waiting for Async results. using (Task_.NoSyncContext()) { // Poloniex requires the 'nonce' values to be strictly increasing. // That means all POSTs must be serialised to avoid a race condition // when POSTing two messages in quick succession. var cancel_token = CancellationTokenSource.CreateLinkedTokenSource(Shutdown, cancel ?? CancellationToken.None).Token; using (RequestThrottle.Lock(cancel_token)) // Limit requests to the required rate { await RequestThrottle.Wait(cancel_token); // Add fields to the request based on 'security' parameters = parameters ?? new Params(); if (timestamp) { // Needs to be added after waiting on the throttle parameters["timestamp"] = RequestTimestamp; } if (security == ESecurityType.TRADE || security == ESecurityType.USER_DATA) { var query_string = Http_.UrlEncode(parameters).TrimStart('?'); var hash = Hasher.ComputeHash(Encoding.UTF8.GetBytes(query_string)); parameters["signature"] = Misc.ToStringHex(hash); } // Create the request var url = $"{UrlRestAddress}{command}{Http_.UrlEncode(parameters)}"; var req = new HttpRequestMessage(method, url); if (security == ESecurityType.TRADE || security == ESecurityType.USER_DATA || security == ESecurityType.USER_STREAM || security == ESecurityType.MARKET_DATA) { req.Headers.Add("X-MBX-APIKEY", Key); } if (log_trace) { Log.Write(ELogLevel.Debug, req.ToString()); } // Submit the request var sw = new Stopwatch().StartNow(); var response = await Client.SendAsync(req, cancel_token); var reply = await response.Content.ReadAsStringAsync(); Log.Write(ELogLevel.Debug, $"Req time: {sw.Elapsed.ToPrettyString(min_unit: TimeSpan_.ETimeUnits.Milliseconds)} - {url}"); if (log_trace) { Log.Write(ELogLevel.Debug, reply.ToString()); } // Check the API usage weight if (response.Headers.TryGetValues("X-MBX-USED-WEIGHT", out var weights)) { RequestThrottle.UsedWeight = long.Parse(weights.First()); if (RequestThrottle.UsedWeight > 0.5 * RequestThrottle.WeightLimit) { Debug.Assert(false); } } // Interpret the reply if (!response.IsSuccessStatusCode) { // Check for an error var jobj = reply != null?JObject.Parse(reply) : null; if (jobj != null && jobj["code"]?.Value <int>() is int code && jobj["msg"]?.Value <string>() is string msg) { throw new BinanceException((EErrorCode)code, msg); } else { throw new HttpException((int)response.StatusCode, response.ReasonPhrase); } } return(JToken.Parse(reply)); } }
/// <summary> /// Determine if executing trades in 'loop' should result in a profit. /// Returns true if profitable and a copy of this loop in 'loop'</summary> public bool IsProfitable(bool forward, Fund fund, out Loop loop) { // How to think about this: // - We want to see what happens if we convert some currency to each of the coins // in the loop, ending up back at the initial currency. If the result is more than // we started with, then it's a profitable loop. // - We can go in either direction around the loop. // - We want to execute each trade around a profitable loop at the same time, so we're // limited to the smallest balance for the coins in the loop. // - The rate by volume does not depend on our account balance. We calculate the effective // rate at each of the offered volumes then determine if any of those volumes are profitable // and whether we have enough balance for the given volumes. // - The 'Bid' table contains the amounts of base currency people want to buy, ordered by price. // - The 'Ask' table contains the amounts of base currency people want to sell, ordered by price. loop = null; // Construct an "order book" of volumes and complete-loop prices (e.g. BTC to BTC price for each volume) var dir = forward ? +1 : -1; var coin = forward ? Beg : End; var tt = forward ? ETradeType.B2Q : ETradeType.Q2B; var obk = new OrderBook(coin, coin, tt) { new Offer(1m, decimal.MaxValue._(coin.Symbol)) }; foreach (var pair in EnumPairs(dir)) { // Limit the volume calculated, there's no point in calculating large volumes if we can't trade them Unit <decimal> bal = 0m; OrderBook b2q = null, q2b = null; using (Task_.NoSyncContext()) { Misc.RunOnMainThread(() => { bal = coin.Balances[fund].Available; b2q = new OrderBook(pair.MarketDepth.B2Q); q2b = new OrderBook(pair.MarketDepth.Q2B); }).Wait(); } // Note: the trade prices are in quote currency if (pair.Base == coin) { obk = MergeRates(obk, b2q, bal, invert: false); } else if (pair.Quote == coin) { obk = MergeRates(obk, q2b, bal, invert: true); } else { throw new Exception($"Pair {pair} does not include Coin {coin}. Loop is invalid."); } // Get the next coin in the loop coin = pair.OtherCoin(coin); } if (obk.Count == 0) { return(false); } // Save the best profit ratio for this loop (as an indication) if (forward) { ProfitRatioFwd = obk[0].PriceQ2B; } else { ProfitRatioBck = obk[0].PriceQ2B; } // Look for any volumes that have a nett gain var amount_gain = obk.Where(x => x.PriceQ2B > 1).Sum(x => x.PriceQ2B * x.AmountBase); if (amount_gain == 0) { return(false); } // Create a copy of the loop for editing (with the direction set) loop = new Loop(this, obk, dir); // Find the maximum profitable volume to trade var amount = 0m._(loop.Beg); foreach (var ordr in loop.Rate.Where(x => x.PriceQ2B > 1)) { amount += ordr.AmountBase; } // Calculate the effective fee in initial coin currency. // Do all trades assuming no fee, but accumulate the fee separately var fee = 0m._(loop.Beg); var initial_volume = amount; // Trade each pair in the loop (in the given direction) to check // that the trade is still profitable after fees. Record each trade // so that we can determine the trade scale coin = loop.Beg; var trades = new List <Trade>(); foreach (var pair in loop.EnumPairs(loop.Direction)) { // If we trade 'volume' using 'pair' that will result in a new volume // in the new currency. There will also be a fee charged (in quote currency). // If we're trading to quote currency, the new volume is reduced by the fee. // If we're trading to base currency, the cost is increased by the fee. // Calculate the result of the trade var new_coin = pair.OtherCoin(coin); var trade = pair.Base == coin ? pair.BaseToQuote(fund, amount) : pair.QuoteToBase(fund, amount); // Record the trade amount. trades.Add(trade); // Convert the fee so far to the new coin using the effective rate, // and add on the fee for this trade. var rate = trade.AmountOut / trade.AmountIn; fee = fee * rate + trade.AmountOut * pair.Fee; // Advance to the next pair coin = new_coin; amount = trade.AmountOut; } // Record the volume to trade, the scale, and the expected profit. // If the new volume is greater than the initial volume, WIN! // Update the profitability of the loop now we've accounted for fees. loop.TradeScale = 1m; loop.Tradeability = string.Empty; loop.TradeVolume = initial_volume; loop.Profit = (amount - fee) - initial_volume; if (forward) { loop.ProfitRatioFwd = ProfitRatioFwd = (amount - fee) / initial_volume; } else { loop.ProfitRatioBck = ProfitRatioBck = (amount - fee) / initial_volume; } if (loop.ProfitRatio <= 1m) { return(false); } // Determine the trade scale based on the available balances foreach (var trade in trades) { var pair = trade.Pair; // Get the balance available for this trade and determine a trade scaling factor. // Increase the required volume to allow for the fee // Reduce the available balance slightly to deal with rounding errors var bal = trade.CoinIn.Balances[fund].Available * 0.999m; var req = trade.AmountIn * (1 + pair.Fee); var scale = Math_.Clamp((decimal)(bal / req), 0m, 1m); if (scale < loop.TradeScale) { loop.TradeScale = Math_.Clamp(scale, 0, loop.TradeScale); loop.LimitingCoin = trade.CoinIn; } } // Check that all traded volumes are within the limits var all_trades_valid = EValidation.Valid; foreach (var trade in trades) { // Check the unscaled amount, if that's too small we'll ignore this loop var validation0 = trade.Validate(); all_trades_valid |= validation0; // Record why the base trade isn't valid if (validation0 != EValidation.Valid) { loop.Tradeability += $"{trade.Description} - {validation0}\n"; } // If the volume to trade, multiplied by the trade scale, is outside the // allowed range of trading volume, set the scale to zero. This is to prevent // loops being traded where part of the loop would be rejected. var validation1 = new Trade(trade, loop.TradeScale).Validate(); if (validation1.HasFlag(EValidation.AmountInOutOfRange)) { loop.Tradeability += $"Not enough {trade.CoinIn} to trade\n"; } if (validation1.HasFlag(EValidation.AmountOutOutOfRange)) { loop.Tradeability += $"Trade result volume of {trade.CoinOut} is too small\n"; } if (validation1 != EValidation.Valid) { loop.TradeScale = 0m; } } // Return the profitable loop (even if scaled to 0) return(all_trades_valid == EValidation.Valid); }