From a2541a4e4ce499ce2b0b58f3fc56e51c4232a115 Mon Sep 17 00:00:00 2001 From: JacobJT Date: Sat, 28 Nov 2020 11:37:47 -0600 Subject: [PATCH 1/7] Initial Bybit support --- .../API/Exchanges/Bybit/ExchangeBybitAPI.cs | 947 ++++++++++++++++++ .../Model/ExchangeOrderResult.cs | 6 + tests/ExchangeSharpTests/ExchangeTests.cs | 2 +- 3 files changed, 954 insertions(+), 1 deletion(-) create mode 100644 src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs new file mode 100644 index 00000000..cfd9c9cd --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs @@ -0,0 +1,947 @@ +/* +MIT LICENSE + +Copyright 2020 Digital Ruby, LLC - http://www.digitalruby.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace ExchangeSharp +{ + public sealed partial class ExchangeBybitAPI : ExchangeAPI + { + private int _recvWindow = 30000; + + public override string BaseUrl { get; set; } = "https://api.bybit.com"; + public override string BaseUrlWebSocket { get; set; } = "wss://stream.bybit.com/realtime"; + // public override string BaseUrl { get; set; } = "https://api-testnet.bybit.com/"; + // public override string BaseUrlWebSocket { get; set; } = "wss://stream-testnet.bybit.com/realtime"; + + public ExchangeBybitAPI() + { + NonceStyle = NonceStyle.UnixMilliseconds; + NonceOffset = TimeSpan.FromSeconds(1.0); + + MarketSymbolSeparator = string.Empty; + RequestContentType = "application/json"; + WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; + + RateLimit = new RateGate(100, TimeSpan.FromMinutes(1)); + } + + public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) + { + throw new NotImplementedException(); + } + + public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) + { + throw new NotImplementedException(); + } + + // Was initially struggling with 10002 timestamp errors, so tried calcing clock drift on every request. + // Settled on positive NonceOffset so our clock is not likely ahead of theirs on arrival (assuming accurate client/server side clocks) + // And larger recv_window so our packets have plenty of time to arrive + // protected override async Task OnGetNonceOffset() + // { + // string stringResult = await MakeRequestAsync("/v2/public/time"); + // var token = JsonConvert.DeserializeObject(stringResult); + // DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["time_now"].ConvertInvariant()); + // var now = CryptoUtility.UtcNow; + // NonceOffset = now - serverDate + TimeSpan.FromSeconds(1); // how much time to substract from Nonce when making a request + // } + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if ((payload != null) && payload.ContainsKey("sign") && request.Method == "POST") + { + await CryptoUtility.WritePayloadJsonToRequestAsync(request, payload); + } + } + +#nullable enable + //Not using MakeJsonRequest... so we can perform our own check on the ret_code + private async Task DoMakeJsonRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null) + { + await new SynchronizationContextRemover(); + + string stringResult = await MakeRequestAsync(url, baseUrl, payload, requestMethod); + return JsonConvert.DeserializeObject(stringResult); + } +#nullable disable + + private JToken CheckRetCode(JToken response, string[] allowedRetCodes) + { + var result = GetResult(response, out var retCode, out var retMessage); + if (!allowedRetCodes.Contains(retCode)) + { + throw new Exception($"Invalid ret_code {retCode}, ret_msg {retMessage}"); + } + return result; + } + + private JToken CheckRetCode(JToken response) + { + return CheckRetCode(response, new string[] {"0"}); + } + + private JToken GetResult(JToken response, out string retCode, out string retMessage) + { + retCode = response["ret_code"].ToStringInvariant(); + retMessage = response["ret_msg"].ToStringInvariant(); + return response["result"]; + } + + private async Task SendWebsocketAuth(IWebSocket socket) { + var payload = await GetNoncePayloadAsync(); + var nonce = (payload["nonce"].ConvertInvariant() + 5000).ToStringInvariant(); + var signature = CryptoUtility.SHA256Sign($"GET/realtime{nonce}", CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + await socket.SendMessageAsync(new { op = "auth", args = new [] {PublicApiKey.ToUnsecureString(), nonce, signature} }); + } + + private async Task> GetAuthenticatedPayload(Dictionary requestPayload = null) + { + var payload = await GetNoncePayloadAsync(); + var nonce = payload["nonce"].ConvertInvariant(); + payload.Remove("nonce"); + payload["api_key"] = PublicApiKey.ToUnsecureString(); + payload["timestamp"] = nonce.ToStringInvariant(); + payload["recv_window"] = _recvWindow; + if (requestPayload != null) + { + payload = payload.Concat(requestPayload).ToDictionary(p => p.Key, p => p.Value); + } + + string form = CryptoUtility.GetFormForPayload(payload, false, true); + form = form.Replace("=False", "=false"); + form = form.Replace("=True", "=true"); + payload["sign"] = CryptoUtility.SHA256Sign(form, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + return payload; + } + + private async Task GetAuthenticatedQueryString(Dictionary requestPayload = null) + { + var payload = await GetAuthenticatedPayload(requestPayload); + var sign = payload["sign"].ToStringInvariant(); + payload.Remove("sign"); + string form = CryptoUtility.GetFormForPayload(payload, false, true); + form += "&sign=" + sign; + return form; + } + + private Task DoConnectWebSocketAsync(Func connected, Func callback, int symbolArrayIndex = 3) + { + Timer pingTimer = null; + return ConnectWebSocketAsync(url: string.Empty, messageCallback: async (_socket, msg) => + { + var msgString = msg.ToStringFromUTF8(); + JToken token = JToken.Parse(msgString); + + if (token["ret_msg"]?.ToStringInvariant() == "pong") + { // received reply to our ping + return; + } + + if (token["topic"] != null) + { + var data = token["data"]; + await callback(_socket, data); + } + else + { + /* + subscription response: + { + "success": true, // Whether subscription is successful + "ret_msg": "", // Successful subscription: "", otherwise it shows error message + "conn_id":"e0e10eee-4eff-4d21-881e-a0c55c25e2da",// current connection id + "request": { // Request to your subscription + "op": "subscribe", + "args": [ + "kline.BTCUSD.1m" + ] + } + } + */ + JToken response = token["request"]; + var op = response["op"]?.ToStringInvariant(); + if ((response != null) && ((op == "subscribe") || (op == "auth"))) + { + var responseMessage = token["ret_msg"]?.ToStringInvariant(); + if (responseMessage != "") + { + Logger.Info("Websocket unable to connect: " + msgString); + return; + } + else if (pingTimer == null) + { + /* + ping response: + { + "success": true, // Whether ping is successful + "ret_msg": "pong", + "conn_id": "036e5d21-804c-4447-a92d-b65a44d00700",// current connection id + "request": { + "op": "ping", + "args": null + } + } + */ + pingTimer = new Timer(callback: async s => await _socket.SendMessageAsync(new { op = "ping" }), + state: null, dueTime: 0, period: 15000); // send a ping every 15 seconds + return; + } + } + } + }, + connectCallback: async (_socket) => + { + await connected(_socket); + _socket.ConnectInterval = TimeSpan.FromHours(0); + }, + disconnectCallback: s => + { + pingTimer.Dispose(); + pingTimer = null; + return Task.CompletedTask; + }); + } + + private async Task AddMarketSymbolsToChannel(IWebSocket socket, string argsPrefix, string[] marketSymbols) + { + string fullArgs = argsPrefix; + if (marketSymbols == null || marketSymbols.Length == 0) + { + fullArgs += "*"; + } + else + { + foreach (var symbol in marketSymbols) + { + fullArgs += symbol + "|"; + } + fullArgs = fullArgs.TrimEnd('|'); + } + + await socket.SendMessageAsync(new { op = "subscribe", args = new [] {fullArgs} }); + } + + protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + { + /* + request: + {"op":"subscribe","args":["trade.BTCUSD|XRPUSD"]} + */ + /* + response: + { + "topic": "trade.BTCUSD", + "data": [ + { + "timestamp": "2020-01-12T16:59:59.000Z", + "trade_time_ms": 1582793344685, // trade time in millisecond + "symbol": "BTCUSD", + "side": "Sell", + "size": 328, + "price": 8098, + "tick_direction": "MinusTick", + "trade_id": "00c706e1-ba52-5bb0-98d0-bf694bdc69f7", + "cross_seq": 1052816407 + } + ] + } + */ + return await DoConnectWebSocketAsync(async (_socket) => + { + await AddMarketSymbolsToChannel(_socket, "trade.", marketSymbols); + }, async (_socket, token) => + { + foreach (var dataRow in token) + { + ExchangeTrade trade = dataRow.ParseTrade( + amountKey: "size", + priceKey: "price", + typeKey: "side", + timestampKey: "timestamp", + timestampType: TimestampType.Iso8601, + idKey: "trade_id"); + await callback(new KeyValuePair(dataRow["symbol"].ToStringInvariant(), trade)); + } + }); + } + + public async Task GetPositionWebSocketAsync(Action callback) + { + /* + request: + {"op": "subscribe", "args": ["position"]} + */ + /* + response: + { + "topic": "position", + "action": "update", + "data": [ + { + "user_id": 1, // user ID + "symbol": "BTCUSD", // the contract for this position + "size": 11, // the current position amount + "side": "Sell", // side + "position_value": "0.00159252", // positional value + "entry_price": "6907.291588174717", // entry price + "liq_price": "7100.234", // liquidation price + "bust_price": "7088.1234", // bankruptcy price + "leverage": "1", // leverage + "order_margin": "1", // order margin + "position_margin": "1", // position margin + "available_balance": "2", // available balance + "take_profit": "0", // take profit price + "tp_trigger_by": "LastPrice", // take profit trigger price, eg: LastPrice, IndexPrice. Conditional order only + "stop_loss": "0", // stop loss price + "sl_trigger_by": "", // stop loss trigger price, eg: LastPrice, IndexPrice. Conditional order only + "realised_pnl": "0.10", // realised PNL + "trailing_stop": "0", // trailing stop points + "trailing_active": "0", // trailing stop trigger price + "wallet_balance": "4.12", // wallet balance + "risk_id": 1, + "occ_closing_fee": "0.1", // position closing + "occ_funding_fee": "0.1", // funding fee + "auto_add_margin": 0, // auto margin replenishment switch + "cum_realised_pnl": "0.12", // Total realized profit and loss + "position_status": "Normal", // status of position (Normal: normal Liq: in the process of liquidation Adl: in the process of Auto-Deleveraging) + // Auto margin replenishment enabled (0: no 1: yes) + "position_seq": 14 // position version number + } + ] + } + */ + return await DoConnectWebSocketAsync(async (_socket) => + { + await SendWebsocketAuth(_socket); + await _socket.SendMessageAsync(new { op = "subscribe", args = new [] {"position"} }); + }, async (_socket, token) => + { + foreach (var dataRow in token) + { + callback(ParsePosition(dataRow)); + } + await Task.CompletedTask; + }); + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + var m = await GetMarketSymbolsMetadataAsync(); + return m.Select(x => x.MarketSymbol); + } + + protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": [ + { + "name": "BTCUSD", + "base_currency": "BTC", + "quote_currency": "USD", + "price_scale": 2, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 100, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.5", + "max_price": "999999.5", + "tick_size": "0.5" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "ETHUSD", + "base_currency": "ETH", + "quote_currency": "USD", + "price_scale": 2, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.05", + "max_price": "99999.95", + "tick_size": "0.05" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "EOSUSD", + "base_currency": "EOS", + "quote_currency": "USD", + "price_scale": 3, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.001", + "max_price": "1999.999", + "tick_size": "0.001" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "XRPUSD", + "base_currency": "XRP", + "quote_currency": "USD", + "price_scale": 4, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.0001", + "max_price": "199.9999", + "tick_size": "0.0001" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + } + ], + "time_now": "1581411225.414179" + }} + */ + + List markets = new List(); + JToken allSymbols = CheckRetCode(await DoMakeJsonRequestAsync("/v2/public/symbols")); + foreach (JToken marketSymbolToken in allSymbols) + { + var market = new ExchangeMarket + { + MarketSymbol = marketSymbolToken["name"].ToStringUpperInvariant(), + IsActive = true, + QuoteCurrency = marketSymbolToken["quote_currency"].ToStringUpperInvariant(), + BaseCurrency = marketSymbolToken["base_currency"].ToStringUpperInvariant(), + }; + + try + { + JToken priceFilter = marketSymbolToken["price_filter"]; + market.MinPrice = priceFilter["min_price"].ConvertInvariant(); + market.MaxPrice = priceFilter["max_price"].ConvertInvariant(); + market.PriceStepSize = priceFilter["tick_size"].ConvertInvariant(); + + JToken lotSizeFilter = marketSymbolToken["lot_size_filter"]; + market.MinTradeSize = lotSizeFilter["min_trading_qty"].ConvertInvariant(); + market.MaxTradeSize = lotSizeFilter["max_trading_qty"].ConvertInvariant(); + market.QuantityStepSize = lotSizeFilter["qty_step"].ConvertInvariant(); + } + catch + { + + } + markets.Add(market); + } + return markets; + } + + + private async Task> DoGetAmountsAsync(string field) + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "BTC": { + "equity": 1002, //equity = wallet_balance + unrealised_pnl + "available_balance": 999.99987471, //available_balance + //In Isolated Margin Mode: + // available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + //In Cross Margin Mode: + //if unrealised_pnl > 0: + //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin); + //if unrealised_pnl < 0: + //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + unrealised_pnl + "used_margin": 0.00012529, //used_margin = wallet_balance - available_balance + "order_margin": 0.00012529, //Used margin by order + "position_margin": 0, //position margin + "occ_closing_fee": 0, //position closing fee + "occ_funding_fee": 0, //funding fee + "wallet_balance": 1000, //wallet balance. When in Cross Margin mod, the number minus your unclosed loss is your real wallet balance. + "realised_pnl": 0, //daily realized profit and loss + "unrealised_pnl": 2, //unrealised profit and loss + //when side is sell: + // unrealised_pnl = size * (1.0 / mark_price - 1.0 / entry_price) + //when side is buy: + // unrealised_pnl = size * (1.0 / entry_price - 1.0 / mark_price) + "cum_realised_pnl": 0, //total relised profit and loss + "given_cash": 0, //given_cash + "service_cash": 0 //service_cash + } + }, + "time_now": "1578284274.816029", + "rate_limit_status": 98, + "rate_limit_reset_ms": 1580885703683, + "rate_limit": 100 + } + */ + Dictionary amounts = new Dictionary(); + var queryString = await GetAuthenticatedQueryString(); + JToken currencies = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/wallet/balance?" + queryString, BaseUrl, null, "GET")); + foreach (JProperty currency in currencies.Children()) + { + var balance = currency.Value[field].ConvertInvariant(); + if (amounts.ContainsKey(currency.Name)) + { + amounts[currency.Name] += balance; + } + else + { + amounts[currency.Name] = balance; + } + } + return amounts; + } + + protected override async Task> OnGetAmountsAsync() + { + return await DoGetAmountsAsync("equity"); + } + + protected override async Task> OnGetAmountsAvailableToTradeAsync() + { + return await DoGetAmountsAsync("available_balance"); + } + + public async Task> GetCurrentPositionsAsync() + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "id": 27913, + "user_id": 1, + "risk_id": 1, + "symbol": "BTCUSD", + "side": "Buy", + "size": 5, + "position_value": "0.0006947", + "entry_price": "7197.35137469", + "is_isolated":true, + "auto_add_margin": 0, + "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level + "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: + effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) + "position_margin": "0.0006947", + "liq_price": "3608", + "bust_price": "3599", + "occ_closing_fee": "0.00000105", + "occ_funding_fee": "0", + "take_profit": "0", + "stop_loss": "0", + "trailing_stop": "0", + "position_status": "Normal", + "deleverage_indicator": 4, + "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", + "order_margin": "0.00029477", + "wallet_balance": "0.03000227", + "realised_pnl": "-0.00000126", + "unrealised_pnl": 0, + "cum_realised_pnl": "-0.00001306", + "cross_seq": 444081383, + "position_seq": 287141589, + "created_at": "2019-10-19T17:04:55Z", + "updated_at": "2019-12-27T20:25:45.158767Z" + }, + "time_now": "1577480599.097287", + "rate_limit_status": 119, + "rate_limit_reset_ms": 1580885703683, + "rate_limit": 120 + } + */ + var queryString = await GetAuthenticatedQueryString(); + JToken token = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/position/list?" + queryString, BaseUrl, null, "GET")); + List positions = new List(); + foreach (var item in token) + { + positions.Add(ParsePosition(item["data"])); + } + return positions; + } + + protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_status"] = "Created,New,PartiallyFilled"; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + var queryString = await GetAuthenticatedQueryString(extraParams); + JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order/list?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); + + List orders = new List(); + foreach (JToken order in token["data"]) + { + orders.Add(ParseOrder(order, retCode, retMessage)); + } + + return orders; + } + + protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_id"] = orderId; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + + var queryString = await GetAuthenticatedQueryString(extraParams); + JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); + + List orders = new List(); + foreach (JToken order in token) + { + orders.Add(ParseOrder(order, retCode, retMessage)); + } + + return orders[0]; + } + + protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_id"] = orderId; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + + var payload = await GetAuthenticatedPayload(extraParams); + CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancel", BaseUrl, payload, "POST")); + // new string[] {"0", "30032"}); + //30032: order has been finished or canceled + } + + public async Task CancelAllOrdersAsync(string marketSymbol) + { + var extraParams = new Dictionary(); + extraParams["symbol"] = marketSymbol; + var payload = await GetAuthenticatedPayload(extraParams); + CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancelAll", BaseUrl, payload, "POST")); + } + + protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + var payload = new Dictionary(); + await AddOrderToPayload(order, payload); + payload = await GetAuthenticatedPayload(payload); + JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/create", BaseUrl, payload, "POST"), out var retCode, out var retMessage); + return ParseOrder(token, retCode, retMessage); + } + + public async Task OnAmendOrderAsync(ExchangeOrderRequest order) + { + var payload = new Dictionary(); + payload["symbol"] = order.MarketSymbol; + if(order.OrderId != null) + payload["order_id"] = order.OrderId; + else if(order.ClientOrderId != null) + payload["order_link_id"] = order.ClientOrderId; + else + throw new Exception("Need either OrderId or ClientOrderId"); + + payload["p_r_qty"] = (long) await ClampOrderQuantity(order.MarketSymbol, order.Amount); + if(order.OrderType!=OrderType.Market) + payload["p_r_price"] = order.Price; + + payload = await GetAuthenticatedPayload(payload); + JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/replace", BaseUrl, payload, "POST"), out var retCode, out var retMessage); + + var result = new ExchangeOrderResult(); + result.ResultCode = retCode; + result.Message = retMessage; + if (retCode == "0") + result.OrderId = token["order_id"].ToStringInvariant(); + return result; + } + + private async Task AddOrderToPayload(ExchangeOrderRequest order, Dictionary payload) + { + /* + side true string Side + symbol true string Symbol + order_type true string Active order type + qty true integer Order quantity in USD + price false number Order price + time_in_force true string Time in force + take_profit false number Take profit price, only take effect upon opening the position + stop_loss false number Stop loss price, only take effect upon opening the position + reduce_only false bool What is a reduce-only order? True means your position can only reduce in size if this order is triggered + close_on_trigger false bool What is a close on trigger order? For a closing order. It can only reduce your position, not increase it. If the account has insufficient available balance when the closing order is triggered, then other active orders of similar contracts will be cancelled or reduced. It can be used to ensure your stop loss reduces your position regardless of current available margin. + order_link_id false string Customised order ID, maximum length at 36 characters, and order ID under the same agency has to be unique. + */ + + payload["side"] = order.IsBuy ? "Buy" : "Sell"; + payload["symbol"] = order.MarketSymbol; + payload["order_type"] = order.OrderType.ToStringInvariant(); + payload["qty"] = await ClampOrderQuantity(order.MarketSymbol, order.Amount); + + if(order.OrderType!=OrderType.Market) + payload["price"] = order.Price; + + if(order.ClientOrderId != null) + payload["order_link_id"] = order.ClientOrderId; + + if (order.ExtraParameters.TryGetValue("reduce_only", out var reduceOnly)) + { + payload["reduce_only"] = reduceOnly; + } + + if (order.ExtraParameters.TryGetValue("time_in_force", out var timeInForce)) + { + payload["time_in_force"] = timeInForce; + } + else + { + payload["time_in_force"] = "GoodTillCancel"; + } + } + + private ExchangePosition ParsePosition(JToken token) + { + /* + "id": 27913, + "user_id": 1, + "risk_id": 1, + "symbol": "BTCUSD", + "side": "Buy", + "size": 5, + "position_value": "0.0006947", + "entry_price": "7197.35137469", + "is_isolated":true, + "auto_add_margin": 0, + "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level + "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: + effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) + "position_margin": "0.0006947", + "liq_price": "3608", + "bust_price": "3599", + "occ_closing_fee": "0.00000105", + "occ_funding_fee": "0", + "take_profit": "0", + "stop_loss": "0", + "trailing_stop": "0", + "position_status": "Normal", + "deleverage_indicator": 4, + "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", + "order_margin": "0.00029477", + "wallet_balance": "0.03000227", + "realised_pnl": "-0.00000126", + "unrealised_pnl": 0, + "cum_realised_pnl": "-0.00001306", + "cross_seq": 444081383, + "position_seq": 287141589, + "created_at": "2019-10-19T17:04:55Z", + "updated_at": "2019-12-27T20:25:45.158767Z + */ + ExchangePosition result = new ExchangePosition + { + MarketSymbol = token["symbol"].ToStringUpperInvariant(), + Amount = token["size"].ConvertInvariant(), + AveragePrice = token["entry_price"].ConvertInvariant(), + LiquidationPrice = token["liq_price"].ConvertInvariant(), + Leverage = token["effective_leverage"].ConvertInvariant(), + TimeStamp = CryptoUtility.ParseTimestamp(token["updated_at"], TimestampType.Iso8601) + }; + if (token["side"].ToStringInvariant() == "Sell") + result.Amount *= -1; + return result; + } + + private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string resultMessage) + { + /* + Active Order: + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "user_id": 106958, + "symbol": "BTCUSD", + "side": "Buy", + "order_type": "Limit", + "price": "11756.5", + "qty": 1, + "time_in_force": "PostOnly", + "order_status": "Filled", + "ext_fields": { + "o_req_num": -68948112492, + "xreq_type": "x_create" + }, + "last_exec_time": "1596304897.847944", + "last_exec_price": "11756.5", + "leaves_qty": 0, + "leaves_value": "0", + "cum_exec_qty": 1, + "cum_exec_value": "0.00008505", + "cum_exec_fee": "-0.00000002", + "reject_reason": "", + "cancel_type": "", + "order_link_id": "", + "created_at": "2020-08-01T18:00:26Z", + "updated_at": "2020-08-01T18:01:37Z", + "order_id": "e66b101a-ef3f-4647-83b5-28e0f38dcae0" + }, + "time_now": "1597171013.867068", + "rate_limit_status": 599, + "rate_limit_reset_ms": 1597171013861, + "rate_limit": 600 + } + + Active Order List: + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "data": [ + { + "user_id": 160861, + "order_status": "Cancelled", + "symbol": "BTCUSD", + "side": "Buy", + "order_type": "Market", + "price": "9800", + "qty": "16737", + "time_in_force": "ImmediateOrCancel", + "order_link_id": "", + "order_id": "fead08d7-47c0-4d6a-b9e7-5c71d5df8ba1", + "created_at": "2020-07-24T08:22:30Z", + "updated_at": "2020-07-24T08:22:30Z", + "leaves_qty": "0", + "leaves_value": "0", + "cum_exec_qty": "0", + "cum_exec_value": "0", + "cum_exec_fee": "0", + "reject_reason": "EC_NoImmediateQtyToFill" + } + ], + "cursor": "w01XFyyZc8lhtCLl6NgAaYBRfsN9Qtpp1f2AUy3AS4+fFDzNSlVKa0od8DKCqgAn" + }, + "time_now": "1604653633.173848", + "rate_limit_status": 599, + "rate_limit_reset_ms": 1604653633171, + "rate_limit": 600 + } + */ + ExchangeOrderResult result = new ExchangeOrderResult(); + if (token.Count() > 0) + { + result.Amount = token["qty"].ConvertInvariant(); + result.AmountFilled = token["cum_exec_qty"].ConvertInvariant(); + result.Price = token["price"].ConvertInvariant(); + result.IsBuy = token["side"].ToStringInvariant().EqualsWithOption("Buy"); + result.OrderDate = token["created_at"].ConvertInvariant(); + result.OrderId = token["order_id"].ToStringInvariant(); + result.ClientOrderId = token["order_link_id"].ToStringInvariant(); + result.MarketSymbol = token["symbol"].ToStringInvariant(); + + switch (token["order_status"].ToStringInvariant()) + { + case "Created": + case "New": + result.Result = ExchangeAPIOrderResult.Pending; + break; + case "PartiallyFilled": + result.Result = ExchangeAPIOrderResult.FilledPartially; + break; + case "Filled": + result.Result = ExchangeAPIOrderResult.Filled; + break; + case "Cancelled": + result.Result = ExchangeAPIOrderResult.Canceled; + break; + + default: + result.Result = ExchangeAPIOrderResult.Error; + break; + } + } + result.ResultCode = resultCode; + result.Message = resultMessage; + + return result; + } + } + + public partial class ExchangeName { public const string Bybit = "Bybit"; } +} diff --git a/src/ExchangeSharp/Model/ExchangeOrderResult.cs b/src/ExchangeSharp/Model/ExchangeOrderResult.cs index cca52ee7..2824a1d0 100644 --- a/src/ExchangeSharp/Model/ExchangeOrderResult.cs +++ b/src/ExchangeSharp/Model/ExchangeOrderResult.cs @@ -30,6 +30,12 @@ public sealed class ExchangeOrderResult /// Result of the order public ExchangeAPIOrderResult Result { get; set; } + /// + /// Result/Error code from exchange + /// Not all exchanges support this + /// + public string ResultCode { get; set; } + /// Message if any public string Message { get; set; } diff --git a/tests/ExchangeSharpTests/ExchangeTests.cs b/tests/ExchangeSharpTests/ExchangeTests.cs index 1ad96a0e..c7316f26 100644 --- a/tests/ExchangeSharpTests/ExchangeTests.cs +++ b/tests/ExchangeSharpTests/ExchangeTests.cs @@ -80,7 +80,7 @@ public async Task GlobalSymbolTest() if (api is ExchangeUfoDexAPI || api is ExchangeOKExAPI || api is ExchangeHitBTCAPI || api is ExchangeKuCoinAPI || api is ExchangeOKCoinAPI || api is ExchangeDigifinexAPI || api is ExchangeNDAXAPI || api is ExchangeBL3PAPI || api is ExchangeBinanceUSAPI || api is ExchangeBinanceJerseyAPI || api is ExchangeBinanceDEXAPI || - api is ExchangeBitMEXAPI || api is ExchangeBTSEAPI) + api is ExchangeBitMEXAPI || api is ExchangeBTSEAPI || api is ExchangeBybitAPI) { // WIP continue; From d62376255a940209cfce69ab6332782d304ab3a2 Mon Sep 17 00:00:00 2001 From: JacobJT Date: Sat, 28 Nov 2020 12:11:24 -0600 Subject: [PATCH 2/7] whitespace --- .../API/Exchanges/Bybit/ExchangeBybitAPI.cs | 1738 ++++++++--------- 1 file changed, 865 insertions(+), 873 deletions(-) diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs index cfd9c9cd..126c17bc 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs @@ -24,134 +24,134 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - public sealed partial class ExchangeBybitAPI : ExchangeAPI - { - private int _recvWindow = 30000; + public sealed partial class ExchangeBybitAPI : ExchangeAPI + { + private int _recvWindow = 30000; - public override string BaseUrl { get; set; } = "https://api.bybit.com"; - public override string BaseUrlWebSocket { get; set; } = "wss://stream.bybit.com/realtime"; - // public override string BaseUrl { get; set; } = "https://api-testnet.bybit.com/"; - // public override string BaseUrlWebSocket { get; set; } = "wss://stream-testnet.bybit.com/realtime"; + public override string BaseUrl { get; set; } = "https://api.bybit.com"; + public override string BaseUrlWebSocket { get; set; } = "wss://stream.bybit.com/realtime"; + // public override string BaseUrl { get; set; } = "https://api-testnet.bybit.com/"; + // public override string BaseUrlWebSocket { get; set; } = "wss://stream-testnet.bybit.com/realtime"; - public ExchangeBybitAPI() - { + public ExchangeBybitAPI() + { NonceStyle = NonceStyle.UnixMilliseconds; - NonceOffset = TimeSpan.FromSeconds(1.0); - - MarketSymbolSeparator = string.Empty; - RequestContentType = "application/json"; - WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; - - RateLimit = new RateGate(100, TimeSpan.FromMinutes(1)); - } - - public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) - { - throw new NotImplementedException(); - } - - public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) - { - throw new NotImplementedException(); - } - - // Was initially struggling with 10002 timestamp errors, so tried calcing clock drift on every request. - // Settled on positive NonceOffset so our clock is not likely ahead of theirs on arrival (assuming accurate client/server side clocks) - // And larger recv_window so our packets have plenty of time to arrive - // protected override async Task OnGetNonceOffset() - // { - // string stringResult = await MakeRequestAsync("/v2/public/time"); - // var token = JsonConvert.DeserializeObject(stringResult); - // DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["time_now"].ConvertInvariant()); - // var now = CryptoUtility.UtcNow; - // NonceOffset = now - serverDate + TimeSpan.FromSeconds(1); // how much time to substract from Nonce when making a request - // } - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if ((payload != null) && payload.ContainsKey("sign") && request.Method == "POST") - { - await CryptoUtility.WritePayloadJsonToRequestAsync(request, payload); - } - } + NonceOffset = TimeSpan.FromSeconds(1.0); + + MarketSymbolSeparator = string.Empty; + RequestContentType = "application/json"; + WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; + + RateLimit = new RateGate(100, TimeSpan.FromMinutes(1)); + } + + public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) + { + throw new NotImplementedException(); + } + + public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) + { + throw new NotImplementedException(); + } + + // Was initially struggling with 10002 timestamp errors, so tried calcing clock drift on every request. + // Settled on positive NonceOffset so our clock is not likely ahead of theirs on arrival (assuming accurate client/server side clocks) + // And larger recv_window so our packets have plenty of time to arrive + // protected override async Task OnGetNonceOffset() + // { + // string stringResult = await MakeRequestAsync("/v2/public/time"); + // var token = JsonConvert.DeserializeObject(stringResult); + // DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["time_now"].ConvertInvariant()); + // var now = CryptoUtility.UtcNow; + // NonceOffset = now - serverDate + TimeSpan.FromSeconds(1); // how much time to substract from Nonce when making a request + // } + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if ((payload != null) && payload.ContainsKey("sign") && request.Method == "POST") + { + await CryptoUtility.WritePayloadJsonToRequestAsync(request, payload); + } + } #nullable enable - //Not using MakeJsonRequest... so we can perform our own check on the ret_code - private async Task DoMakeJsonRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null) - { - await new SynchronizationContextRemover(); - - string stringResult = await MakeRequestAsync(url, baseUrl, payload, requestMethod); - return JsonConvert.DeserializeObject(stringResult); - } + //Not using MakeJsonRequest... so we can perform our own check on the ret_code + private async Task DoMakeJsonRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null) + { + await new SynchronizationContextRemover(); + + string stringResult = await MakeRequestAsync(url, baseUrl, payload, requestMethod); + return JsonConvert.DeserializeObject(stringResult); + } #nullable disable - private JToken CheckRetCode(JToken response, string[] allowedRetCodes) - { - var result = GetResult(response, out var retCode, out var retMessage); - if (!allowedRetCodes.Contains(retCode)) - { - throw new Exception($"Invalid ret_code {retCode}, ret_msg {retMessage}"); - } - return result; - } - - private JToken CheckRetCode(JToken response) - { - return CheckRetCode(response, new string[] {"0"}); - } - - private JToken GetResult(JToken response, out string retCode, out string retMessage) - { - retCode = response["ret_code"].ToStringInvariant(); - retMessage = response["ret_msg"].ToStringInvariant(); - return response["result"]; - } - - private async Task SendWebsocketAuth(IWebSocket socket) { - var payload = await GetNoncePayloadAsync(); - var nonce = (payload["nonce"].ConvertInvariant() + 5000).ToStringInvariant(); - var signature = CryptoUtility.SHA256Sign($"GET/realtime{nonce}", CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + private JToken CheckRetCode(JToken response, string[] allowedRetCodes) + { + var result = GetResult(response, out var retCode, out var retMessage); + if (!allowedRetCodes.Contains(retCode)) + { + throw new Exception($"Invalid ret_code {retCode}, ret_msg {retMessage}"); + } + return result; + } + + private JToken CheckRetCode(JToken response) + { + return CheckRetCode(response, new string[] {"0"}); + } + + private JToken GetResult(JToken response, out string retCode, out string retMessage) + { + retCode = response["ret_code"].ToStringInvariant(); + retMessage = response["ret_msg"].ToStringInvariant(); + return response["result"]; + } + + private async Task SendWebsocketAuth(IWebSocket socket) { + var payload = await GetNoncePayloadAsync(); + var nonce = (payload["nonce"].ConvertInvariant() + 5000).ToStringInvariant(); + var signature = CryptoUtility.SHA256Sign($"GET/realtime{nonce}", CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); await socket.SendMessageAsync(new { op = "auth", args = new [] {PublicApiKey.ToUnsecureString(), nonce, signature} }); - } - - private async Task> GetAuthenticatedPayload(Dictionary requestPayload = null) - { - var payload = await GetNoncePayloadAsync(); - var nonce = payload["nonce"].ConvertInvariant(); - payload.Remove("nonce"); - payload["api_key"] = PublicApiKey.ToUnsecureString(); - payload["timestamp"] = nonce.ToStringInvariant(); - payload["recv_window"] = _recvWindow; - if (requestPayload != null) - { - payload = payload.Concat(requestPayload).ToDictionary(p => p.Key, p => p.Value); - } - - string form = CryptoUtility.GetFormForPayload(payload, false, true); - form = form.Replace("=False", "=false"); - form = form.Replace("=True", "=true"); - payload["sign"] = CryptoUtility.SHA256Sign(form, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); - return payload; - } - - private async Task GetAuthenticatedQueryString(Dictionary requestPayload = null) - { - var payload = await GetAuthenticatedPayload(requestPayload); - var sign = payload["sign"].ToStringInvariant(); - payload.Remove("sign"); - string form = CryptoUtility.GetFormForPayload(payload, false, true); - form += "&sign=" + sign; - return form; - } - - private Task DoConnectWebSocketAsync(Func connected, Func callback, int symbolArrayIndex = 3) - { + } + + private async Task> GetAuthenticatedPayload(Dictionary requestPayload = null) + { + var payload = await GetNoncePayloadAsync(); + var nonce = payload["nonce"].ConvertInvariant(); + payload.Remove("nonce"); + payload["api_key"] = PublicApiKey.ToUnsecureString(); + payload["timestamp"] = nonce.ToStringInvariant(); + payload["recv_window"] = _recvWindow; + if (requestPayload != null) + { + payload = payload.Concat(requestPayload).ToDictionary(p => p.Key, p => p.Value); + } + + string form = CryptoUtility.GetFormForPayload(payload, false, true); + form = form.Replace("=False", "=false"); + form = form.Replace("=True", "=true"); + payload["sign"] = CryptoUtility.SHA256Sign(form, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + return payload; + } + + private async Task GetAuthenticatedQueryString(Dictionary requestPayload = null) + { + var payload = await GetAuthenticatedPayload(requestPayload); + var sign = payload["sign"].ToStringInvariant(); + payload.Remove("sign"); + string form = CryptoUtility.GetFormForPayload(payload, false, true); + form += "&sign=" + sign; + return form; + } + + private Task DoConnectWebSocketAsync(Func connected, Func callback, int symbolArrayIndex = 3) + { Timer pingTimer = null; - return ConnectWebSocketAsync(url: string.Empty, messageCallback: async (_socket, msg) => - { + return ConnectWebSocketAsync(url: string.Empty, messageCallback: async (_socket, msg) => + { var msgString = msg.ToStringFromUTF8(); - JToken token = JToken.Parse(msgString); + JToken token = JToken.Parse(msgString); if (token["ret_msg"]?.ToStringInvariant() == "pong") { // received reply to our ping @@ -159,789 +159,781 @@ private Task DoConnectWebSocketAsync(Func connecte } if (token["topic"] != null) - { - var data = token["data"]; - await callback(_socket, data); - } - else - { - /* - subscription response: - { - "success": true, // Whether subscription is successful - "ret_msg": "", // Successful subscription: "", otherwise it shows error message - "conn_id":"e0e10eee-4eff-4d21-881e-a0c55c25e2da",// current connection id - "request": { // Request to your subscription - "op": "subscribe", - "args": [ - "kline.BTCUSD.1m" - ] - } - } - */ - JToken response = token["request"]; - var op = response["op"]?.ToStringInvariant(); - if ((response != null) && ((op == "subscribe") || (op == "auth"))) - { - var responseMessage = token["ret_msg"]?.ToStringInvariant(); - if (responseMessage != "") - { - Logger.Info("Websocket unable to connect: " + msgString); - return; - } - else if (pingTimer == null) - { - /* - ping response: - { - "success": true, // Whether ping is successful - "ret_msg": "pong", - "conn_id": "036e5d21-804c-4447-a92d-b65a44d00700",// current connection id - "request": { - "op": "ping", - "args": null - } - } - */ - pingTimer = new Timer(callback: async s => await _socket.SendMessageAsync(new { op = "ping" }), - state: null, dueTime: 0, period: 15000); // send a ping every 15 seconds - return; - } - } + { + var data = token["data"]; + await callback(_socket, data); + } + else + { + /* + subscription response: + { + "success": true, // Whether subscription is successful + "ret_msg": "", // Successful subscription: "", otherwise it shows error message + "conn_id":"e0e10eee-4eff-4d21-881e-a0c55c25e2da",// current connection id + "request": { // Request to your subscription + "op": "subscribe", + "args": [ + "kline.BTCUSD.1m" + ] + } + } + */ + JToken response = token["request"]; + var op = response["op"]?.ToStringInvariant(); + if ((response != null) && ((op == "subscribe") || (op == "auth"))) + { + var responseMessage = token["ret_msg"]?.ToStringInvariant(); + if (responseMessage != "") + { + Logger.Info("Websocket unable to connect: " + msgString); + return; + } + else if (pingTimer == null) + { + /* + ping response: + { + "success": true, // Whether ping is successful + "ret_msg": "pong", + "conn_id": "036e5d21-804c-4447-a92d-b65a44d00700",// current connection id + "request": { + "op": "ping", + "args": null + } + } + */ + pingTimer = new Timer(callback: async s => await _socket.SendMessageAsync(new { op = "ping" }), + state: null, dueTime: 0, period: 15000); // send a ping every 15 seconds + return; + } + } } - }, - connectCallback: async (_socket) => - { - await connected(_socket); - _socket.ConnectInterval = TimeSpan.FromHours(0); - }, - disconnectCallback: s => + }, + connectCallback: async (_socket) => + { + await connected(_socket); + _socket.ConnectInterval = TimeSpan.FromHours(0); + }, + disconnectCallback: s => { pingTimer.Dispose(); pingTimer = null; return Task.CompletedTask; }); - } + } - private async Task AddMarketSymbolsToChannel(IWebSocket socket, string argsPrefix, string[] marketSymbols) - { - string fullArgs = argsPrefix; + private async Task AddMarketSymbolsToChannel(IWebSocket socket, string argsPrefix, string[] marketSymbols) + { + string fullArgs = argsPrefix; if (marketSymbols == null || marketSymbols.Length == 0) { fullArgs += "*"; } - else - { - foreach (var symbol in marketSymbols) - { - fullArgs += symbol + "|"; - } - fullArgs = fullArgs.TrimEnd('|'); - } - - await socket.SendMessageAsync(new { op = "subscribe", args = new [] {fullArgs} }); - } - - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) - { + else + { + foreach (var symbol in marketSymbols) + { + fullArgs += symbol + "|"; + } + fullArgs = fullArgs.TrimEnd('|'); + } + + await socket.SendMessageAsync(new { op = "subscribe", args = new [] {fullArgs} }); + } + + protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + { /* - request: - {"op":"subscribe","args":["trade.BTCUSD|XRPUSD"]} + request: + {"op":"subscribe","args":["trade.BTCUSD|XRPUSD"]} */ /* - response: - { - "topic": "trade.BTCUSD", - "data": [ - { - "timestamp": "2020-01-12T16:59:59.000Z", - "trade_time_ms": 1582793344685, // trade time in millisecond - "symbol": "BTCUSD", - "side": "Sell", - "size": 328, - "price": 8098, - "tick_direction": "MinusTick", - "trade_id": "00c706e1-ba52-5bb0-98d0-bf694bdc69f7", - "cross_seq": 1052816407 - } - ] - } - */ + response: + { + "topic": "trade.BTCUSD", + "data": [ + { + "timestamp": "2020-01-12T16:59:59.000Z", + "trade_time_ms": 1582793344685, // trade time in millisecond + "symbol": "BTCUSD", + "side": "Sell", + "size": 328, + "price": 8098, + "tick_direction": "MinusTick", + "trade_id": "00c706e1-ba52-5bb0-98d0-bf694bdc69f7", + "cross_seq": 1052816407 + } + ] + } + */ return await DoConnectWebSocketAsync(async (_socket) => { await AddMarketSymbolsToChannel(_socket, "trade.", marketSymbols); }, async (_socket, token) => { - foreach (var dataRow in token) - { - ExchangeTrade trade = dataRow.ParseTrade( - amountKey: "size", - priceKey: "price", - typeKey: "side", - timestampKey: "timestamp", - timestampType: TimestampType.Iso8601, - idKey: "trade_id"); - await callback(new KeyValuePair(dataRow["symbol"].ToStringInvariant(), trade)); - } + foreach (var dataRow in token) + { + ExchangeTrade trade = dataRow.ParseTrade( + amountKey: "size", + priceKey: "price", + typeKey: "side", + timestampKey: "timestamp", + timestampType: TimestampType.Iso8601, + idKey: "trade_id"); + await callback(new KeyValuePair(dataRow["symbol"].ToStringInvariant(), trade)); + } }); - } + } - public async Task GetPositionWebSocketAsync(Action callback) - { + public async Task GetPositionWebSocketAsync(Action callback) + { /* - request: - {"op": "subscribe", "args": ["position"]} + request: + {"op": "subscribe", "args": ["position"]} */ /* - response: - { - "topic": "position", - "action": "update", - "data": [ - { - "user_id": 1, // user ID - "symbol": "BTCUSD", // the contract for this position - "size": 11, // the current position amount - "side": "Sell", // side - "position_value": "0.00159252", // positional value - "entry_price": "6907.291588174717", // entry price - "liq_price": "7100.234", // liquidation price - "bust_price": "7088.1234", // bankruptcy price - "leverage": "1", // leverage - "order_margin": "1", // order margin - "position_margin": "1", // position margin - "available_balance": "2", // available balance - "take_profit": "0", // take profit price - "tp_trigger_by": "LastPrice", // take profit trigger price, eg: LastPrice, IndexPrice. Conditional order only - "stop_loss": "0", // stop loss price - "sl_trigger_by": "", // stop loss trigger price, eg: LastPrice, IndexPrice. Conditional order only - "realised_pnl": "0.10", // realised PNL - "trailing_stop": "0", // trailing stop points - "trailing_active": "0", // trailing stop trigger price - "wallet_balance": "4.12", // wallet balance - "risk_id": 1, - "occ_closing_fee": "0.1", // position closing - "occ_funding_fee": "0.1", // funding fee - "auto_add_margin": 0, // auto margin replenishment switch - "cum_realised_pnl": "0.12", // Total realized profit and loss - "position_status": "Normal", // status of position (Normal: normal Liq: in the process of liquidation Adl: in the process of Auto-Deleveraging) - // Auto margin replenishment enabled (0: no 1: yes) - "position_seq": 14 // position version number - } - ] - } - */ + response: + { + "topic": "position", + "action": "update", + "data": [ + { + "user_id": 1, // user ID + "symbol": "BTCUSD", // the contract for this position + "size": 11, // the current position amount + "side": "Sell", // side + "position_value": "0.00159252", // positional value + "entry_price": "6907.291588174717", // entry price + "liq_price": "7100.234", // liquidation price + "bust_price": "7088.1234", // bankruptcy price + "leverage": "1", // leverage + "order_margin": "1", // order margin + "position_margin": "1", // position margin + "available_balance": "2", // available balance + "take_profit": "0", // take profit price + "tp_trigger_by": "LastPrice", // take profit trigger price, eg: LastPrice, IndexPrice. Conditional order only + "stop_loss": "0", // stop loss price + "sl_trigger_by": "", // stop loss trigger price, eg: LastPrice, IndexPrice. Conditional order only + "realised_pnl": "0.10", // realised PNL + "trailing_stop": "0", // trailing stop points + "trailing_active": "0", // trailing stop trigger price + "wallet_balance": "4.12", // wallet balance + "risk_id": 1, + "occ_closing_fee": "0.1", // position closing + "occ_funding_fee": "0.1", // funding fee + "auto_add_margin": 0, // auto margin replenishment switch + "cum_realised_pnl": "0.12", // Total realized profit and loss + "position_status": "Normal", // status of position (Normal: normal Liq: in the process of liquidation Adl: in the process of Auto-Deleveraging) + // Auto margin replenishment enabled (0: no 1: yes) + "position_seq": 14 // position version number + } + ] + } + */ return await DoConnectWebSocketAsync(async (_socket) => { - await SendWebsocketAuth(_socket); - await _socket.SendMessageAsync(new { op = "subscribe", args = new [] {"position"} }); + await SendWebsocketAuth(_socket); + await _socket.SendMessageAsync(new { op = "subscribe", args = new [] {"position"} }); }, async (_socket, token) => { - foreach (var dataRow in token) - { - callback(ParsePosition(dataRow)); - } - await Task.CompletedTask; + foreach (var dataRow in token) + { + callback(ParsePosition(dataRow)); + } + await Task.CompletedTask; }); - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - var m = await GetMarketSymbolsMetadataAsync(); - return m.Select(x => x.MarketSymbol); - } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() - { - /* - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": [ - { - "name": "BTCUSD", - "base_currency": "BTC", - "quote_currency": "USD", - "price_scale": 2, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 100, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.5", - "max_price": "999999.5", - "tick_size": "0.5" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "ETHUSD", - "base_currency": "ETH", - "quote_currency": "USD", - "price_scale": 2, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.05", - "max_price": "99999.95", - "tick_size": "0.05" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "EOSUSD", - "base_currency": "EOS", - "quote_currency": "USD", - "price_scale": 3, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.001", - "max_price": "1999.999", - "tick_size": "0.001" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "XRPUSD", - "base_currency": "XRP", - "quote_currency": "USD", - "price_scale": 4, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.0001", - "max_price": "199.9999", - "tick_size": "0.0001" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - } - ], - "time_now": "1581411225.414179" - }} - */ - - List markets = new List(); - JToken allSymbols = CheckRetCode(await DoMakeJsonRequestAsync("/v2/public/symbols")); + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + var m = await GetMarketSymbolsMetadataAsync(); + return m.Select(x => x.MarketSymbol); + } + + protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": [ + { + "name": "BTCUSD", + "base_currency": "BTC", + "quote_currency": "USD", + "price_scale": 2, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 100, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.5", + "max_price": "999999.5", + "tick_size": "0.5" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "ETHUSD", + "base_currency": "ETH", + "quote_currency": "USD", + "price_scale": 2, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.05", + "max_price": "99999.95", + "tick_size": "0.05" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "EOSUSD", + "base_currency": "EOS", + "quote_currency": "USD", + "price_scale": 3, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.001", + "max_price": "1999.999", + "tick_size": "0.001" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "XRPUSD", + "base_currency": "XRP", + "quote_currency": "USD", + "price_scale": 4, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.0001", + "max_price": "199.9999", + "tick_size": "0.0001" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + } + ], + "time_now": "1581411225.414179" + }} + */ + + List markets = new List(); + JToken allSymbols = CheckRetCode(await DoMakeJsonRequestAsync("/v2/public/symbols")); foreach (JToken marketSymbolToken in allSymbols) - { - var market = new ExchangeMarket - { - MarketSymbol = marketSymbolToken["name"].ToStringUpperInvariant(), - IsActive = true, - QuoteCurrency = marketSymbolToken["quote_currency"].ToStringUpperInvariant(), - BaseCurrency = marketSymbolToken["base_currency"].ToStringUpperInvariant(), - }; - - try - { - JToken priceFilter = marketSymbolToken["price_filter"]; - market.MinPrice = priceFilter["min_price"].ConvertInvariant(); - market.MaxPrice = priceFilter["max_price"].ConvertInvariant(); - market.PriceStepSize = priceFilter["tick_size"].ConvertInvariant(); - - JToken lotSizeFilter = marketSymbolToken["lot_size_filter"]; - market.MinTradeSize = lotSizeFilter["min_trading_qty"].ConvertInvariant(); - market.MaxTradeSize = lotSizeFilter["max_trading_qty"].ConvertInvariant(); - market.QuantityStepSize = lotSizeFilter["qty_step"].ConvertInvariant(); - } - catch - { - - } - markets.Add(market); - } - return markets; - } - - - private async Task> DoGetAmountsAsync(string field) - { - /* - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "BTC": { - "equity": 1002, //equity = wallet_balance + unrealised_pnl - "available_balance": 999.99987471, //available_balance - //In Isolated Margin Mode: - // available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) - //In Cross Margin Mode: - //if unrealised_pnl > 0: - //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin); - //if unrealised_pnl < 0: - //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + unrealised_pnl - "used_margin": 0.00012529, //used_margin = wallet_balance - available_balance - "order_margin": 0.00012529, //Used margin by order - "position_margin": 0, //position margin - "occ_closing_fee": 0, //position closing fee - "occ_funding_fee": 0, //funding fee - "wallet_balance": 1000, //wallet balance. When in Cross Margin mod, the number minus your unclosed loss is your real wallet balance. - "realised_pnl": 0, //daily realized profit and loss - "unrealised_pnl": 2, //unrealised profit and loss - //when side is sell: - // unrealised_pnl = size * (1.0 / mark_price - 1.0 / entry_price) - //when side is buy: - // unrealised_pnl = size * (1.0 / entry_price - 1.0 / mark_price) - "cum_realised_pnl": 0, //total relised profit and loss - "given_cash": 0, //given_cash - "service_cash": 0 //service_cash - } - }, - "time_now": "1578284274.816029", - "rate_limit_status": 98, - "rate_limit_reset_ms": 1580885703683, - "rate_limit": 100 - } - */ - Dictionary amounts = new Dictionary(); - var queryString = await GetAuthenticatedQueryString(); - JToken currencies = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/wallet/balance?" + queryString, BaseUrl, null, "GET")); - foreach (JProperty currency in currencies.Children()) - { - var balance = currency.Value[field].ConvertInvariant(); - if (amounts.ContainsKey(currency.Name)) - { - amounts[currency.Name] += balance; - } - else - { - amounts[currency.Name] = balance; - } - } - return amounts; - } - - protected override async Task> OnGetAmountsAsync() - { - return await DoGetAmountsAsync("equity"); - } - - protected override async Task> OnGetAmountsAvailableToTradeAsync() - { - return await DoGetAmountsAsync("available_balance"); - } - - public async Task> GetCurrentPositionsAsync() - { - /* - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "id": 27913, - "user_id": 1, - "risk_id": 1, - "symbol": "BTCUSD", - "side": "Buy", - "size": 5, - "position_value": "0.0006947", - "entry_price": "7197.35137469", - "is_isolated":true, - "auto_add_margin": 0, - "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level - "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: - effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) - "position_margin": "0.0006947", - "liq_price": "3608", - "bust_price": "3599", - "occ_closing_fee": "0.00000105", - "occ_funding_fee": "0", - "take_profit": "0", - "stop_loss": "0", - "trailing_stop": "0", - "position_status": "Normal", - "deleverage_indicator": 4, - "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", - "order_margin": "0.00029477", - "wallet_balance": "0.03000227", - "realised_pnl": "-0.00000126", - "unrealised_pnl": 0, - "cum_realised_pnl": "-0.00001306", - "cross_seq": 444081383, - "position_seq": 287141589, - "created_at": "2019-10-19T17:04:55Z", - "updated_at": "2019-12-27T20:25:45.158767Z" - }, - "time_now": "1577480599.097287", - "rate_limit_status": 119, - "rate_limit_reset_ms": 1580885703683, - "rate_limit": 120 - } - */ - var queryString = await GetAuthenticatedQueryString(); - JToken token = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/position/list?" + queryString, BaseUrl, null, "GET")); - List positions = new List(); - foreach (var item in token) - { - positions.Add(ParsePosition(item["data"])); - } - return positions; - } - - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - var extraParams = new Dictionary(); - extraParams["order_status"] = "Created,New,PartiallyFilled"; - if (!string.IsNullOrWhiteSpace(marketSymbol)) - { - extraParams["symbol"] = marketSymbol; - } - else - { - throw new Exception("marketSymbol is required"); - } - var queryString = await GetAuthenticatedQueryString(extraParams); - JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order/list?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); - - List orders = new List(); - foreach (JToken order in token["data"]) - { - orders.Add(ParseOrder(order, retCode, retMessage)); - } - - return orders; - } - - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) - { - var extraParams = new Dictionary(); - extraParams["order_id"] = orderId; - if (!string.IsNullOrWhiteSpace(marketSymbol)) - { - extraParams["symbol"] = marketSymbol; - } - else - { - throw new Exception("marketSymbol is required"); - } - - var queryString = await GetAuthenticatedQueryString(extraParams); - JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); - - List orders = new List(); - foreach (JToken order in token) - { - orders.Add(ParseOrder(order, retCode, retMessage)); - } - - return orders[0]; - } - - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) - { - var extraParams = new Dictionary(); - extraParams["order_id"] = orderId; - if (!string.IsNullOrWhiteSpace(marketSymbol)) - { - extraParams["symbol"] = marketSymbol; - } - else - { - throw new Exception("marketSymbol is required"); - } - - var payload = await GetAuthenticatedPayload(extraParams); - CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancel", BaseUrl, payload, "POST")); - // new string[] {"0", "30032"}); - //30032: order has been finished or canceled - } - - public async Task CancelAllOrdersAsync(string marketSymbol) - { - var extraParams = new Dictionary(); - extraParams["symbol"] = marketSymbol; - var payload = await GetAuthenticatedPayload(extraParams); - CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancelAll", BaseUrl, payload, "POST")); - } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - var payload = new Dictionary(); - await AddOrderToPayload(order, payload); - payload = await GetAuthenticatedPayload(payload); - JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/create", BaseUrl, payload, "POST"), out var retCode, out var retMessage); - return ParseOrder(token, retCode, retMessage); - } - - public async Task OnAmendOrderAsync(ExchangeOrderRequest order) - { - var payload = new Dictionary(); - payload["symbol"] = order.MarketSymbol; - if(order.OrderId != null) - payload["order_id"] = order.OrderId; - else if(order.ClientOrderId != null) - payload["order_link_id"] = order.ClientOrderId; - else - throw new Exception("Need either OrderId or ClientOrderId"); - - payload["p_r_qty"] = (long) await ClampOrderQuantity(order.MarketSymbol, order.Amount); - if(order.OrderType!=OrderType.Market) - payload["p_r_price"] = order.Price; - - payload = await GetAuthenticatedPayload(payload); - JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/replace", BaseUrl, payload, "POST"), out var retCode, out var retMessage); - - var result = new ExchangeOrderResult(); - result.ResultCode = retCode; - result.Message = retMessage; - if (retCode == "0") - result.OrderId = token["order_id"].ToStringInvariant(); - return result; - } - - private async Task AddOrderToPayload(ExchangeOrderRequest order, Dictionary payload) - { - /* - side true string Side - symbol true string Symbol - order_type true string Active order type - qty true integer Order quantity in USD - price false number Order price - time_in_force true string Time in force - take_profit false number Take profit price, only take effect upon opening the position - stop_loss false number Stop loss price, only take effect upon opening the position - reduce_only false bool What is a reduce-only order? True means your position can only reduce in size if this order is triggered - close_on_trigger false bool What is a close on trigger order? For a closing order. It can only reduce your position, not increase it. If the account has insufficient available balance when the closing order is triggered, then other active orders of similar contracts will be cancelled or reduced. It can be used to ensure your stop loss reduces your position regardless of current available margin. - order_link_id false string Customised order ID, maximum length at 36 characters, and order ID under the same agency has to be unique. - */ - - payload["side"] = order.IsBuy ? "Buy" : "Sell"; - payload["symbol"] = order.MarketSymbol; - payload["order_type"] = order.OrderType.ToStringInvariant(); - payload["qty"] = await ClampOrderQuantity(order.MarketSymbol, order.Amount); - - if(order.OrderType!=OrderType.Market) - payload["price"] = order.Price; - - if(order.ClientOrderId != null) - payload["order_link_id"] = order.ClientOrderId; - - if (order.ExtraParameters.TryGetValue("reduce_only", out var reduceOnly)) - { - payload["reduce_only"] = reduceOnly; - } - - if (order.ExtraParameters.TryGetValue("time_in_force", out var timeInForce)) - { - payload["time_in_force"] = timeInForce; - } - else - { - payload["time_in_force"] = "GoodTillCancel"; - } - } - - private ExchangePosition ParsePosition(JToken token) - { - /* - "id": 27913, - "user_id": 1, - "risk_id": 1, - "symbol": "BTCUSD", - "side": "Buy", - "size": 5, - "position_value": "0.0006947", - "entry_price": "7197.35137469", - "is_isolated":true, - "auto_add_margin": 0, - "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level - "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: - effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) - "position_margin": "0.0006947", - "liq_price": "3608", - "bust_price": "3599", - "occ_closing_fee": "0.00000105", - "occ_funding_fee": "0", - "take_profit": "0", - "stop_loss": "0", - "trailing_stop": "0", - "position_status": "Normal", - "deleverage_indicator": 4, - "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", - "order_margin": "0.00029477", - "wallet_balance": "0.03000227", - "realised_pnl": "-0.00000126", - "unrealised_pnl": 0, - "cum_realised_pnl": "-0.00001306", - "cross_seq": 444081383, - "position_seq": 287141589, - "created_at": "2019-10-19T17:04:55Z", - "updated_at": "2019-12-27T20:25:45.158767Z - */ - ExchangePosition result = new ExchangePosition - { - MarketSymbol = token["symbol"].ToStringUpperInvariant(), - Amount = token["size"].ConvertInvariant(), - AveragePrice = token["entry_price"].ConvertInvariant(), - LiquidationPrice = token["liq_price"].ConvertInvariant(), - Leverage = token["effective_leverage"].ConvertInvariant(), - TimeStamp = CryptoUtility.ParseTimestamp(token["updated_at"], TimestampType.Iso8601) - }; - if (token["side"].ToStringInvariant() == "Sell") - result.Amount *= -1; - return result; - } - - private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string resultMessage) - { - /* - Active Order: - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "user_id": 106958, - "symbol": "BTCUSD", - "side": "Buy", - "order_type": "Limit", - "price": "11756.5", - "qty": 1, - "time_in_force": "PostOnly", - "order_status": "Filled", - "ext_fields": { - "o_req_num": -68948112492, - "xreq_type": "x_create" - }, - "last_exec_time": "1596304897.847944", - "last_exec_price": "11756.5", - "leaves_qty": 0, - "leaves_value": "0", - "cum_exec_qty": 1, - "cum_exec_value": "0.00008505", - "cum_exec_fee": "-0.00000002", - "reject_reason": "", - "cancel_type": "", - "order_link_id": "", - "created_at": "2020-08-01T18:00:26Z", - "updated_at": "2020-08-01T18:01:37Z", - "order_id": "e66b101a-ef3f-4647-83b5-28e0f38dcae0" - }, - "time_now": "1597171013.867068", - "rate_limit_status": 599, - "rate_limit_reset_ms": 1597171013861, - "rate_limit": 600 - } - - Active Order List: - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "data": [ - { - "user_id": 160861, - "order_status": "Cancelled", - "symbol": "BTCUSD", - "side": "Buy", - "order_type": "Market", - "price": "9800", - "qty": "16737", - "time_in_force": "ImmediateOrCancel", - "order_link_id": "", - "order_id": "fead08d7-47c0-4d6a-b9e7-5c71d5df8ba1", - "created_at": "2020-07-24T08:22:30Z", - "updated_at": "2020-07-24T08:22:30Z", - "leaves_qty": "0", - "leaves_value": "0", - "cum_exec_qty": "0", - "cum_exec_value": "0", - "cum_exec_fee": "0", - "reject_reason": "EC_NoImmediateQtyToFill" - } - ], - "cursor": "w01XFyyZc8lhtCLl6NgAaYBRfsN9Qtpp1f2AUy3AS4+fFDzNSlVKa0od8DKCqgAn" - }, - "time_now": "1604653633.173848", - "rate_limit_status": 599, - "rate_limit_reset_ms": 1604653633171, - "rate_limit": 600 - } - */ - ExchangeOrderResult result = new ExchangeOrderResult(); - if (token.Count() > 0) - { - result.Amount = token["qty"].ConvertInvariant(); - result.AmountFilled = token["cum_exec_qty"].ConvertInvariant(); - result.Price = token["price"].ConvertInvariant(); - result.IsBuy = token["side"].ToStringInvariant().EqualsWithOption("Buy"); - result.OrderDate = token["created_at"].ConvertInvariant(); - result.OrderId = token["order_id"].ToStringInvariant(); - result.ClientOrderId = token["order_link_id"].ToStringInvariant(); - result.MarketSymbol = token["symbol"].ToStringInvariant(); - - switch (token["order_status"].ToStringInvariant()) - { - case "Created": - case "New": - result.Result = ExchangeAPIOrderResult.Pending; - break; - case "PartiallyFilled": - result.Result = ExchangeAPIOrderResult.FilledPartially; - break; - case "Filled": - result.Result = ExchangeAPIOrderResult.Filled; - break; - case "Cancelled": - result.Result = ExchangeAPIOrderResult.Canceled; - break; - - default: - result.Result = ExchangeAPIOrderResult.Error; - break; - } - } - result.ResultCode = resultCode; - result.Message = resultMessage; - - return result; - } - } - - public partial class ExchangeName { public const string Bybit = "Bybit"; } + { + var market = new ExchangeMarket(); + market.MarketSymbol = marketSymbolToken["name"].ToStringUpperInvariant(); + market.IsActive = true; + market.QuoteCurrency = marketSymbolToken["quote_currency"].ToStringUpperInvariant(); + market.BaseCurrency = marketSymbolToken["base_currency"].ToStringUpperInvariant(); + + JToken priceFilter = marketSymbolToken["price_filter"]; + market.MinPrice = priceFilter["min_price"].ConvertInvariant(); + market.MaxPrice = priceFilter["max_price"].ConvertInvariant(); + market.PriceStepSize = priceFilter["tick_size"].ConvertInvariant(); + + JToken lotSizeFilter = marketSymbolToken["lot_size_filter"]; + market.MinTradeSize = lotSizeFilter["min_trading_qty"].ConvertInvariant(); + market.MaxTradeSize = lotSizeFilter["max_trading_qty"].ConvertInvariant(); + market.QuantityStepSize = lotSizeFilter["qty_step"].ConvertInvariant(); + + markets.Add(market); + } + return markets; + } + + + private async Task> DoGetAmountsAsync(string field) + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "BTC": { + "equity": 1002, //equity = wallet_balance + unrealised_pnl + "available_balance": 999.99987471, //available_balance + //In Isolated Margin Mode: + // available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + //In Cross Margin Mode: + //if unrealised_pnl > 0: + //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin); + //if unrealised_pnl < 0: + //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + unrealised_pnl + "used_margin": 0.00012529, //used_margin = wallet_balance - available_balance + "order_margin": 0.00012529, //Used margin by order + "position_margin": 0, //position margin + "occ_closing_fee": 0, //position closing fee + "occ_funding_fee": 0, //funding fee + "wallet_balance": 1000, //wallet balance. When in Cross Margin mod, the number minus your unclosed loss is your real wallet balance. + "realised_pnl": 0, //daily realized profit and loss + "unrealised_pnl": 2, //unrealised profit and loss + //when side is sell: + // unrealised_pnl = size * (1.0 / mark_price - 1.0 / entry_price) + //when side is buy: + // unrealised_pnl = size * (1.0 / entry_price - 1.0 / mark_price) + "cum_realised_pnl": 0, //total relised profit and loss + "given_cash": 0, //given_cash + "service_cash": 0 //service_cash + } + }, + "time_now": "1578284274.816029", + "rate_limit_status": 98, + "rate_limit_reset_ms": 1580885703683, + "rate_limit": 100 + } + */ + Dictionary amounts = new Dictionary(); + var queryString = await GetAuthenticatedQueryString(); + JToken currencies = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/wallet/balance?" + queryString, BaseUrl, null, "GET")); + foreach (JProperty currency in currencies.Children()) + { + var balance = currency.Value[field].ConvertInvariant(); + if (amounts.ContainsKey(currency.Name)) + { + amounts[currency.Name] += balance; + } + else + { + amounts[currency.Name] = balance; + } + } + return amounts; + } + + protected override async Task> OnGetAmountsAsync() + { + return await DoGetAmountsAsync("equity"); + } + + protected override async Task> OnGetAmountsAvailableToTradeAsync() + { + return await DoGetAmountsAsync("available_balance"); + } + + public async Task> GetCurrentPositionsAsync() + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "id": 27913, + "user_id": 1, + "risk_id": 1, + "symbol": "BTCUSD", + "side": "Buy", + "size": 5, + "position_value": "0.0006947", + "entry_price": "7197.35137469", + "is_isolated":true, + "auto_add_margin": 0, + "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level + "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: + effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) + "position_margin": "0.0006947", + "liq_price": "3608", + "bust_price": "3599", + "occ_closing_fee": "0.00000105", + "occ_funding_fee": "0", + "take_profit": "0", + "stop_loss": "0", + "trailing_stop": "0", + "position_status": "Normal", + "deleverage_indicator": 4, + "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", + "order_margin": "0.00029477", + "wallet_balance": "0.03000227", + "realised_pnl": "-0.00000126", + "unrealised_pnl": 0, + "cum_realised_pnl": "-0.00001306", + "cross_seq": 444081383, + "position_seq": 287141589, + "created_at": "2019-10-19T17:04:55Z", + "updated_at": "2019-12-27T20:25:45.158767Z" + }, + "time_now": "1577480599.097287", + "rate_limit_status": 119, + "rate_limit_reset_ms": 1580885703683, + "rate_limit": 120 + } + */ + var queryString = await GetAuthenticatedQueryString(); + JToken token = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/position/list?" + queryString, BaseUrl, null, "GET")); + List positions = new List(); + foreach (var item in token) + { + positions.Add(ParsePosition(item["data"])); + } + return positions; + } + + protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_status"] = "Created,New,PartiallyFilled"; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + var queryString = await GetAuthenticatedQueryString(extraParams); + JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order/list?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); + + List orders = new List(); + foreach (JToken order in token["data"]) + { + orders.Add(ParseOrder(order, retCode, retMessage)); + } + + return orders; + } + + protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_id"] = orderId; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + + var queryString = await GetAuthenticatedQueryString(extraParams); + JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); + + List orders = new List(); + foreach (JToken order in token) + { + orders.Add(ParseOrder(order, retCode, retMessage)); + } + + return orders[0]; + } + + protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_id"] = orderId; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + + var payload = await GetAuthenticatedPayload(extraParams); + CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancel", BaseUrl, payload, "POST")); + // new string[] {"0", "30032"}); + //30032: order has been finished or canceled + } + + public async Task CancelAllOrdersAsync(string marketSymbol) + { + var extraParams = new Dictionary(); + extraParams["symbol"] = marketSymbol; + var payload = await GetAuthenticatedPayload(extraParams); + CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancelAll", BaseUrl, payload, "POST")); + } + + protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + var payload = new Dictionary(); + await AddOrderToPayload(order, payload); + payload = await GetAuthenticatedPayload(payload); + JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/create", BaseUrl, payload, "POST"), out var retCode, out var retMessage); + return ParseOrder(token, retCode, retMessage); + } + + public async Task OnAmendOrderAsync(ExchangeOrderRequest order) + { + var payload = new Dictionary(); + payload["symbol"] = order.MarketSymbol; + if(order.OrderId != null) + payload["order_id"] = order.OrderId; + else if(order.ClientOrderId != null) + payload["order_link_id"] = order.ClientOrderId; + else + throw new Exception("Need either OrderId or ClientOrderId"); + + payload["p_r_qty"] = (long) await ClampOrderQuantity(order.MarketSymbol, order.Amount); + if(order.OrderType!=OrderType.Market) + payload["p_r_price"] = order.Price; + + payload = await GetAuthenticatedPayload(payload); + JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/replace", BaseUrl, payload, "POST"), out var retCode, out var retMessage); + + var result = new ExchangeOrderResult(); + result.ResultCode = retCode; + result.Message = retMessage; + if (retCode == "0") + result.OrderId = token["order_id"].ToStringInvariant(); + return result; + } + + private async Task AddOrderToPayload(ExchangeOrderRequest order, Dictionary payload) + { + /* + side true string Side + symbol true string Symbol + order_type true string Active order type + qty true integer Order quantity in USD + price false number Order price + time_in_force true string Time in force + take_profit false number Take profit price, only take effect upon opening the position + stop_loss false number Stop loss price, only take effect upon opening the position + reduce_only false bool What is a reduce-only order? True means your position can only reduce in size if this order is triggered + close_on_trigger false bool What is a close on trigger order? For a closing order. It can only reduce your position, not increase it. If the account has insufficient available balance when the closing order is triggered, then other active orders of similar contracts will be cancelled or reduced. It can be used to ensure your stop loss reduces your position regardless of current available margin. + order_link_id false string Customised order ID, maximum length at 36 characters, and order ID under the same agency has to be unique. + */ + + payload["side"] = order.IsBuy ? "Buy" : "Sell"; + payload["symbol"] = order.MarketSymbol; + payload["order_type"] = order.OrderType.ToStringInvariant(); + payload["qty"] = await ClampOrderQuantity(order.MarketSymbol, order.Amount); + + if(order.OrderType!=OrderType.Market) + payload["price"] = order.Price; + + if(order.ClientOrderId != null) + payload["order_link_id"] = order.ClientOrderId; + + if (order.ExtraParameters.TryGetValue("reduce_only", out var reduceOnly)) + { + payload["reduce_only"] = reduceOnly; + } + + if (order.ExtraParameters.TryGetValue("time_in_force", out var timeInForce)) + { + payload["time_in_force"] = timeInForce; + } + else + { + payload["time_in_force"] = "GoodTillCancel"; + } + } + + private ExchangePosition ParsePosition(JToken token) + { + /* + "id": 27913, + "user_id": 1, + "risk_id": 1, + "symbol": "BTCUSD", + "side": "Buy", + "size": 5, + "position_value": "0.0006947", + "entry_price": "7197.35137469", + "is_isolated":true, + "auto_add_margin": 0, + "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level + "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: + effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) + "position_margin": "0.0006947", + "liq_price": "3608", + "bust_price": "3599", + "occ_closing_fee": "0.00000105", + "occ_funding_fee": "0", + "take_profit": "0", + "stop_loss": "0", + "trailing_stop": "0", + "position_status": "Normal", + "deleverage_indicator": 4, + "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", + "order_margin": "0.00029477", + "wallet_balance": "0.03000227", + "realised_pnl": "-0.00000126", + "unrealised_pnl": 0, + "cum_realised_pnl": "-0.00001306", + "cross_seq": 444081383, + "position_seq": 287141589, + "created_at": "2019-10-19T17:04:55Z", + "updated_at": "2019-12-27T20:25:45.158767Z + */ + ExchangePosition result = new ExchangePosition + { + MarketSymbol = token["symbol"].ToStringUpperInvariant(), + Amount = token["size"].ConvertInvariant(), + AveragePrice = token["entry_price"].ConvertInvariant(), + LiquidationPrice = token["liq_price"].ConvertInvariant(), + Leverage = token["effective_leverage"].ConvertInvariant(), + TimeStamp = CryptoUtility.ParseTimestamp(token["updated_at"], TimestampType.Iso8601) + }; + if (token["side"].ToStringInvariant() == "Sell") + result.Amount *= -1; + return result; + } + + private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string resultMessage) + { + /* + Active Order: + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "user_id": 106958, + "symbol": "BTCUSD", + "side": "Buy", + "order_type": "Limit", + "price": "11756.5", + "qty": 1, + "time_in_force": "PostOnly", + "order_status": "Filled", + "ext_fields": { + "o_req_num": -68948112492, + "xreq_type": "x_create" + }, + "last_exec_time": "1596304897.847944", + "last_exec_price": "11756.5", + "leaves_qty": 0, + "leaves_value": "0", + "cum_exec_qty": 1, + "cum_exec_value": "0.00008505", + "cum_exec_fee": "-0.00000002", + "reject_reason": "", + "cancel_type": "", + "order_link_id": "", + "created_at": "2020-08-01T18:00:26Z", + "updated_at": "2020-08-01T18:01:37Z", + "order_id": "e66b101a-ef3f-4647-83b5-28e0f38dcae0" + }, + "time_now": "1597171013.867068", + "rate_limit_status": 599, + "rate_limit_reset_ms": 1597171013861, + "rate_limit": 600 + } + + Active Order List: + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "data": [ + { + "user_id": 160861, + "order_status": "Cancelled", + "symbol": "BTCUSD", + "side": "Buy", + "order_type": "Market", + "price": "9800", + "qty": "16737", + "time_in_force": "ImmediateOrCancel", + "order_link_id": "", + "order_id": "fead08d7-47c0-4d6a-b9e7-5c71d5df8ba1", + "created_at": "2020-07-24T08:22:30Z", + "updated_at": "2020-07-24T08:22:30Z", + "leaves_qty": "0", + "leaves_value": "0", + "cum_exec_qty": "0", + "cum_exec_value": "0", + "cum_exec_fee": "0", + "reject_reason": "EC_NoImmediateQtyToFill" + } + ], + "cursor": "w01XFyyZc8lhtCLl6NgAaYBRfsN9Qtpp1f2AUy3AS4+fFDzNSlVKa0od8DKCqgAn" + }, + "time_now": "1604653633.173848", + "rate_limit_status": 599, + "rate_limit_reset_ms": 1604653633171, + "rate_limit": 600 + } + */ + ExchangeOrderResult result = new ExchangeOrderResult(); + if (token.Count() > 0) + { + result.Amount = token["qty"].ConvertInvariant(); + result.AmountFilled = token["cum_exec_qty"].ConvertInvariant(); + result.Price = token["price"].ConvertInvariant(); + result.IsBuy = token["side"].ToStringInvariant().EqualsWithOption("Buy"); + result.OrderDate = token["created_at"].ConvertInvariant(); + result.OrderId = token["order_id"].ToStringInvariant(); + result.ClientOrderId = token["order_link_id"].ToStringInvariant(); + result.MarketSymbol = token["symbol"].ToStringInvariant(); + + switch (token["order_status"].ToStringInvariant()) + { + case "Created": + case "New": + result.Result = ExchangeAPIOrderResult.Pending; + break; + case "PartiallyFilled": + result.Result = ExchangeAPIOrderResult.FilledPartially; + break; + case "Filled": + result.Result = ExchangeAPIOrderResult.Filled; + break; + case "Cancelled": + result.Result = ExchangeAPIOrderResult.Canceled; + break; + + default: + result.Result = ExchangeAPIOrderResult.Error; + break; + } + } + result.ResultCode = resultCode; + result.Message = resultMessage; + + return result; + } + } + + public partial class ExchangeName { public const string Bybit = "Bybit"; } } From 0b2dd3922089d6fbdb405816907058b7f26f54da Mon Sep 17 00:00:00 2001 From: JacobJT Date: Sat, 28 Nov 2020 12:11:33 -0600 Subject: [PATCH 3/7] aa --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b02e5462..49c6f8c2 100644 --- a/.gitignore +++ b/.gitignore @@ -456,4 +456,5 @@ launchSettings.json **/keys.bin dist/ data/** -!data/.gitkeep \ No newline at end of file +!data/.gitkeep +src/ExchangeSharp/API/Exchanges/Bybit/.ExchangeBybitAPI.cs.swp From c6f3166cf65d254be335f0e792f2cb60e6baf03b Mon Sep 17 00:00:00 2001 From: jacobjthompson <46873491+jacobjthompson@users.noreply.github.com> Date: Sat, 28 Nov 2020 12:16:28 -0600 Subject: [PATCH 4/7] Delete .gitignore --- .gitignore | 460 ----------------------------------------------------- 1 file changed, 460 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 49c6f8c2..00000000 --- a/.gitignore +++ /dev/null @@ -1,460 +0,0 @@ - -# Created by https://www.gitignore.io/api/csharp -# Edit at https://www.gitignore.io/?templates=csharp - -### Csharp ### -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# End of https://www.gitignore.io/api/csharp - -# Created by https://www.gitignore.io/api/jetbrains+all -# Edit at https://www.gitignore.io/?templates=jetbrains+all - -### JetBrains+all ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### JetBrains+all Patch ### -# Ignores the whole .idea folder and all .iml files -# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 - -.idea/ - -# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 - -*.iml -modules.xml -.idea/misc.xml -*.ipr - -# Sonarlint plugin -.idea/sonarlint - -# End of https://www.gitignore.io/api/jetbrains+all - -# Trader binary and log files -*.bin -*.log* -launchSettings.json -**/PublishProfiles/* - -## Project specific -**/keys.bin -dist/ -data/** -!data/.gitkeep -src/ExchangeSharp/API/Exchanges/Bybit/.ExchangeBybitAPI.cs.swp From 941931746959d7372b195098af656cd64085c3f6 Mon Sep 17 00:00:00 2001 From: JacobJT Date: Sat, 28 Nov 2020 12:22:50 -0600 Subject: [PATCH 5/7] whitespace --- .../API/Exchanges/Bybit/ExchangeBybitAPI.cs | 1738 ++++++++--------- 1 file changed, 865 insertions(+), 873 deletions(-) diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs index cfd9c9cd..126c17bc 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs @@ -24,134 +24,134 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - public sealed partial class ExchangeBybitAPI : ExchangeAPI - { - private int _recvWindow = 30000; + public sealed partial class ExchangeBybitAPI : ExchangeAPI + { + private int _recvWindow = 30000; - public override string BaseUrl { get; set; } = "https://api.bybit.com"; - public override string BaseUrlWebSocket { get; set; } = "wss://stream.bybit.com/realtime"; - // public override string BaseUrl { get; set; } = "https://api-testnet.bybit.com/"; - // public override string BaseUrlWebSocket { get; set; } = "wss://stream-testnet.bybit.com/realtime"; + public override string BaseUrl { get; set; } = "https://api.bybit.com"; + public override string BaseUrlWebSocket { get; set; } = "wss://stream.bybit.com/realtime"; + // public override string BaseUrl { get; set; } = "https://api-testnet.bybit.com/"; + // public override string BaseUrlWebSocket { get; set; } = "wss://stream-testnet.bybit.com/realtime"; - public ExchangeBybitAPI() - { + public ExchangeBybitAPI() + { NonceStyle = NonceStyle.UnixMilliseconds; - NonceOffset = TimeSpan.FromSeconds(1.0); - - MarketSymbolSeparator = string.Empty; - RequestContentType = "application/json"; - WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; - - RateLimit = new RateGate(100, TimeSpan.FromMinutes(1)); - } - - public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) - { - throw new NotImplementedException(); - } - - public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) - { - throw new NotImplementedException(); - } - - // Was initially struggling with 10002 timestamp errors, so tried calcing clock drift on every request. - // Settled on positive NonceOffset so our clock is not likely ahead of theirs on arrival (assuming accurate client/server side clocks) - // And larger recv_window so our packets have plenty of time to arrive - // protected override async Task OnGetNonceOffset() - // { - // string stringResult = await MakeRequestAsync("/v2/public/time"); - // var token = JsonConvert.DeserializeObject(stringResult); - // DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["time_now"].ConvertInvariant()); - // var now = CryptoUtility.UtcNow; - // NonceOffset = now - serverDate + TimeSpan.FromSeconds(1); // how much time to substract from Nonce when making a request - // } - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if ((payload != null) && payload.ContainsKey("sign") && request.Method == "POST") - { - await CryptoUtility.WritePayloadJsonToRequestAsync(request, payload); - } - } + NonceOffset = TimeSpan.FromSeconds(1.0); + + MarketSymbolSeparator = string.Empty; + RequestContentType = "application/json"; + WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; + + RateLimit = new RateGate(100, TimeSpan.FromMinutes(1)); + } + + public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) + { + throw new NotImplementedException(); + } + + public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) + { + throw new NotImplementedException(); + } + + // Was initially struggling with 10002 timestamp errors, so tried calcing clock drift on every request. + // Settled on positive NonceOffset so our clock is not likely ahead of theirs on arrival (assuming accurate client/server side clocks) + // And larger recv_window so our packets have plenty of time to arrive + // protected override async Task OnGetNonceOffset() + // { + // string stringResult = await MakeRequestAsync("/v2/public/time"); + // var token = JsonConvert.DeserializeObject(stringResult); + // DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["time_now"].ConvertInvariant()); + // var now = CryptoUtility.UtcNow; + // NonceOffset = now - serverDate + TimeSpan.FromSeconds(1); // how much time to substract from Nonce when making a request + // } + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if ((payload != null) && payload.ContainsKey("sign") && request.Method == "POST") + { + await CryptoUtility.WritePayloadJsonToRequestAsync(request, payload); + } + } #nullable enable - //Not using MakeJsonRequest... so we can perform our own check on the ret_code - private async Task DoMakeJsonRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null) - { - await new SynchronizationContextRemover(); - - string stringResult = await MakeRequestAsync(url, baseUrl, payload, requestMethod); - return JsonConvert.DeserializeObject(stringResult); - } + //Not using MakeJsonRequest... so we can perform our own check on the ret_code + private async Task DoMakeJsonRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null) + { + await new SynchronizationContextRemover(); + + string stringResult = await MakeRequestAsync(url, baseUrl, payload, requestMethod); + return JsonConvert.DeserializeObject(stringResult); + } #nullable disable - private JToken CheckRetCode(JToken response, string[] allowedRetCodes) - { - var result = GetResult(response, out var retCode, out var retMessage); - if (!allowedRetCodes.Contains(retCode)) - { - throw new Exception($"Invalid ret_code {retCode}, ret_msg {retMessage}"); - } - return result; - } - - private JToken CheckRetCode(JToken response) - { - return CheckRetCode(response, new string[] {"0"}); - } - - private JToken GetResult(JToken response, out string retCode, out string retMessage) - { - retCode = response["ret_code"].ToStringInvariant(); - retMessage = response["ret_msg"].ToStringInvariant(); - return response["result"]; - } - - private async Task SendWebsocketAuth(IWebSocket socket) { - var payload = await GetNoncePayloadAsync(); - var nonce = (payload["nonce"].ConvertInvariant() + 5000).ToStringInvariant(); - var signature = CryptoUtility.SHA256Sign($"GET/realtime{nonce}", CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + private JToken CheckRetCode(JToken response, string[] allowedRetCodes) + { + var result = GetResult(response, out var retCode, out var retMessage); + if (!allowedRetCodes.Contains(retCode)) + { + throw new Exception($"Invalid ret_code {retCode}, ret_msg {retMessage}"); + } + return result; + } + + private JToken CheckRetCode(JToken response) + { + return CheckRetCode(response, new string[] {"0"}); + } + + private JToken GetResult(JToken response, out string retCode, out string retMessage) + { + retCode = response["ret_code"].ToStringInvariant(); + retMessage = response["ret_msg"].ToStringInvariant(); + return response["result"]; + } + + private async Task SendWebsocketAuth(IWebSocket socket) { + var payload = await GetNoncePayloadAsync(); + var nonce = (payload["nonce"].ConvertInvariant() + 5000).ToStringInvariant(); + var signature = CryptoUtility.SHA256Sign($"GET/realtime{nonce}", CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); await socket.SendMessageAsync(new { op = "auth", args = new [] {PublicApiKey.ToUnsecureString(), nonce, signature} }); - } - - private async Task> GetAuthenticatedPayload(Dictionary requestPayload = null) - { - var payload = await GetNoncePayloadAsync(); - var nonce = payload["nonce"].ConvertInvariant(); - payload.Remove("nonce"); - payload["api_key"] = PublicApiKey.ToUnsecureString(); - payload["timestamp"] = nonce.ToStringInvariant(); - payload["recv_window"] = _recvWindow; - if (requestPayload != null) - { - payload = payload.Concat(requestPayload).ToDictionary(p => p.Key, p => p.Value); - } - - string form = CryptoUtility.GetFormForPayload(payload, false, true); - form = form.Replace("=False", "=false"); - form = form.Replace("=True", "=true"); - payload["sign"] = CryptoUtility.SHA256Sign(form, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); - return payload; - } - - private async Task GetAuthenticatedQueryString(Dictionary requestPayload = null) - { - var payload = await GetAuthenticatedPayload(requestPayload); - var sign = payload["sign"].ToStringInvariant(); - payload.Remove("sign"); - string form = CryptoUtility.GetFormForPayload(payload, false, true); - form += "&sign=" + sign; - return form; - } - - private Task DoConnectWebSocketAsync(Func connected, Func callback, int symbolArrayIndex = 3) - { + } + + private async Task> GetAuthenticatedPayload(Dictionary requestPayload = null) + { + var payload = await GetNoncePayloadAsync(); + var nonce = payload["nonce"].ConvertInvariant(); + payload.Remove("nonce"); + payload["api_key"] = PublicApiKey.ToUnsecureString(); + payload["timestamp"] = nonce.ToStringInvariant(); + payload["recv_window"] = _recvWindow; + if (requestPayload != null) + { + payload = payload.Concat(requestPayload).ToDictionary(p => p.Key, p => p.Value); + } + + string form = CryptoUtility.GetFormForPayload(payload, false, true); + form = form.Replace("=False", "=false"); + form = form.Replace("=True", "=true"); + payload["sign"] = CryptoUtility.SHA256Sign(form, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + return payload; + } + + private async Task GetAuthenticatedQueryString(Dictionary requestPayload = null) + { + var payload = await GetAuthenticatedPayload(requestPayload); + var sign = payload["sign"].ToStringInvariant(); + payload.Remove("sign"); + string form = CryptoUtility.GetFormForPayload(payload, false, true); + form += "&sign=" + sign; + return form; + } + + private Task DoConnectWebSocketAsync(Func connected, Func callback, int symbolArrayIndex = 3) + { Timer pingTimer = null; - return ConnectWebSocketAsync(url: string.Empty, messageCallback: async (_socket, msg) => - { + return ConnectWebSocketAsync(url: string.Empty, messageCallback: async (_socket, msg) => + { var msgString = msg.ToStringFromUTF8(); - JToken token = JToken.Parse(msgString); + JToken token = JToken.Parse(msgString); if (token["ret_msg"]?.ToStringInvariant() == "pong") { // received reply to our ping @@ -159,789 +159,781 @@ private Task DoConnectWebSocketAsync(Func connecte } if (token["topic"] != null) - { - var data = token["data"]; - await callback(_socket, data); - } - else - { - /* - subscription response: - { - "success": true, // Whether subscription is successful - "ret_msg": "", // Successful subscription: "", otherwise it shows error message - "conn_id":"e0e10eee-4eff-4d21-881e-a0c55c25e2da",// current connection id - "request": { // Request to your subscription - "op": "subscribe", - "args": [ - "kline.BTCUSD.1m" - ] - } - } - */ - JToken response = token["request"]; - var op = response["op"]?.ToStringInvariant(); - if ((response != null) && ((op == "subscribe") || (op == "auth"))) - { - var responseMessage = token["ret_msg"]?.ToStringInvariant(); - if (responseMessage != "") - { - Logger.Info("Websocket unable to connect: " + msgString); - return; - } - else if (pingTimer == null) - { - /* - ping response: - { - "success": true, // Whether ping is successful - "ret_msg": "pong", - "conn_id": "036e5d21-804c-4447-a92d-b65a44d00700",// current connection id - "request": { - "op": "ping", - "args": null - } - } - */ - pingTimer = new Timer(callback: async s => await _socket.SendMessageAsync(new { op = "ping" }), - state: null, dueTime: 0, period: 15000); // send a ping every 15 seconds - return; - } - } + { + var data = token["data"]; + await callback(_socket, data); + } + else + { + /* + subscription response: + { + "success": true, // Whether subscription is successful + "ret_msg": "", // Successful subscription: "", otherwise it shows error message + "conn_id":"e0e10eee-4eff-4d21-881e-a0c55c25e2da",// current connection id + "request": { // Request to your subscription + "op": "subscribe", + "args": [ + "kline.BTCUSD.1m" + ] + } + } + */ + JToken response = token["request"]; + var op = response["op"]?.ToStringInvariant(); + if ((response != null) && ((op == "subscribe") || (op == "auth"))) + { + var responseMessage = token["ret_msg"]?.ToStringInvariant(); + if (responseMessage != "") + { + Logger.Info("Websocket unable to connect: " + msgString); + return; + } + else if (pingTimer == null) + { + /* + ping response: + { + "success": true, // Whether ping is successful + "ret_msg": "pong", + "conn_id": "036e5d21-804c-4447-a92d-b65a44d00700",// current connection id + "request": { + "op": "ping", + "args": null + } + } + */ + pingTimer = new Timer(callback: async s => await _socket.SendMessageAsync(new { op = "ping" }), + state: null, dueTime: 0, period: 15000); // send a ping every 15 seconds + return; + } + } } - }, - connectCallback: async (_socket) => - { - await connected(_socket); - _socket.ConnectInterval = TimeSpan.FromHours(0); - }, - disconnectCallback: s => + }, + connectCallback: async (_socket) => + { + await connected(_socket); + _socket.ConnectInterval = TimeSpan.FromHours(0); + }, + disconnectCallback: s => { pingTimer.Dispose(); pingTimer = null; return Task.CompletedTask; }); - } + } - private async Task AddMarketSymbolsToChannel(IWebSocket socket, string argsPrefix, string[] marketSymbols) - { - string fullArgs = argsPrefix; + private async Task AddMarketSymbolsToChannel(IWebSocket socket, string argsPrefix, string[] marketSymbols) + { + string fullArgs = argsPrefix; if (marketSymbols == null || marketSymbols.Length == 0) { fullArgs += "*"; } - else - { - foreach (var symbol in marketSymbols) - { - fullArgs += symbol + "|"; - } - fullArgs = fullArgs.TrimEnd('|'); - } - - await socket.SendMessageAsync(new { op = "subscribe", args = new [] {fullArgs} }); - } - - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) - { + else + { + foreach (var symbol in marketSymbols) + { + fullArgs += symbol + "|"; + } + fullArgs = fullArgs.TrimEnd('|'); + } + + await socket.SendMessageAsync(new { op = "subscribe", args = new [] {fullArgs} }); + } + + protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + { /* - request: - {"op":"subscribe","args":["trade.BTCUSD|XRPUSD"]} + request: + {"op":"subscribe","args":["trade.BTCUSD|XRPUSD"]} */ /* - response: - { - "topic": "trade.BTCUSD", - "data": [ - { - "timestamp": "2020-01-12T16:59:59.000Z", - "trade_time_ms": 1582793344685, // trade time in millisecond - "symbol": "BTCUSD", - "side": "Sell", - "size": 328, - "price": 8098, - "tick_direction": "MinusTick", - "trade_id": "00c706e1-ba52-5bb0-98d0-bf694bdc69f7", - "cross_seq": 1052816407 - } - ] - } - */ + response: + { + "topic": "trade.BTCUSD", + "data": [ + { + "timestamp": "2020-01-12T16:59:59.000Z", + "trade_time_ms": 1582793344685, // trade time in millisecond + "symbol": "BTCUSD", + "side": "Sell", + "size": 328, + "price": 8098, + "tick_direction": "MinusTick", + "trade_id": "00c706e1-ba52-5bb0-98d0-bf694bdc69f7", + "cross_seq": 1052816407 + } + ] + } + */ return await DoConnectWebSocketAsync(async (_socket) => { await AddMarketSymbolsToChannel(_socket, "trade.", marketSymbols); }, async (_socket, token) => { - foreach (var dataRow in token) - { - ExchangeTrade trade = dataRow.ParseTrade( - amountKey: "size", - priceKey: "price", - typeKey: "side", - timestampKey: "timestamp", - timestampType: TimestampType.Iso8601, - idKey: "trade_id"); - await callback(new KeyValuePair(dataRow["symbol"].ToStringInvariant(), trade)); - } + foreach (var dataRow in token) + { + ExchangeTrade trade = dataRow.ParseTrade( + amountKey: "size", + priceKey: "price", + typeKey: "side", + timestampKey: "timestamp", + timestampType: TimestampType.Iso8601, + idKey: "trade_id"); + await callback(new KeyValuePair(dataRow["symbol"].ToStringInvariant(), trade)); + } }); - } + } - public async Task GetPositionWebSocketAsync(Action callback) - { + public async Task GetPositionWebSocketAsync(Action callback) + { /* - request: - {"op": "subscribe", "args": ["position"]} + request: + {"op": "subscribe", "args": ["position"]} */ /* - response: - { - "topic": "position", - "action": "update", - "data": [ - { - "user_id": 1, // user ID - "symbol": "BTCUSD", // the contract for this position - "size": 11, // the current position amount - "side": "Sell", // side - "position_value": "0.00159252", // positional value - "entry_price": "6907.291588174717", // entry price - "liq_price": "7100.234", // liquidation price - "bust_price": "7088.1234", // bankruptcy price - "leverage": "1", // leverage - "order_margin": "1", // order margin - "position_margin": "1", // position margin - "available_balance": "2", // available balance - "take_profit": "0", // take profit price - "tp_trigger_by": "LastPrice", // take profit trigger price, eg: LastPrice, IndexPrice. Conditional order only - "stop_loss": "0", // stop loss price - "sl_trigger_by": "", // stop loss trigger price, eg: LastPrice, IndexPrice. Conditional order only - "realised_pnl": "0.10", // realised PNL - "trailing_stop": "0", // trailing stop points - "trailing_active": "0", // trailing stop trigger price - "wallet_balance": "4.12", // wallet balance - "risk_id": 1, - "occ_closing_fee": "0.1", // position closing - "occ_funding_fee": "0.1", // funding fee - "auto_add_margin": 0, // auto margin replenishment switch - "cum_realised_pnl": "0.12", // Total realized profit and loss - "position_status": "Normal", // status of position (Normal: normal Liq: in the process of liquidation Adl: in the process of Auto-Deleveraging) - // Auto margin replenishment enabled (0: no 1: yes) - "position_seq": 14 // position version number - } - ] - } - */ + response: + { + "topic": "position", + "action": "update", + "data": [ + { + "user_id": 1, // user ID + "symbol": "BTCUSD", // the contract for this position + "size": 11, // the current position amount + "side": "Sell", // side + "position_value": "0.00159252", // positional value + "entry_price": "6907.291588174717", // entry price + "liq_price": "7100.234", // liquidation price + "bust_price": "7088.1234", // bankruptcy price + "leverage": "1", // leverage + "order_margin": "1", // order margin + "position_margin": "1", // position margin + "available_balance": "2", // available balance + "take_profit": "0", // take profit price + "tp_trigger_by": "LastPrice", // take profit trigger price, eg: LastPrice, IndexPrice. Conditional order only + "stop_loss": "0", // stop loss price + "sl_trigger_by": "", // stop loss trigger price, eg: LastPrice, IndexPrice. Conditional order only + "realised_pnl": "0.10", // realised PNL + "trailing_stop": "0", // trailing stop points + "trailing_active": "0", // trailing stop trigger price + "wallet_balance": "4.12", // wallet balance + "risk_id": 1, + "occ_closing_fee": "0.1", // position closing + "occ_funding_fee": "0.1", // funding fee + "auto_add_margin": 0, // auto margin replenishment switch + "cum_realised_pnl": "0.12", // Total realized profit and loss + "position_status": "Normal", // status of position (Normal: normal Liq: in the process of liquidation Adl: in the process of Auto-Deleveraging) + // Auto margin replenishment enabled (0: no 1: yes) + "position_seq": 14 // position version number + } + ] + } + */ return await DoConnectWebSocketAsync(async (_socket) => { - await SendWebsocketAuth(_socket); - await _socket.SendMessageAsync(new { op = "subscribe", args = new [] {"position"} }); + await SendWebsocketAuth(_socket); + await _socket.SendMessageAsync(new { op = "subscribe", args = new [] {"position"} }); }, async (_socket, token) => { - foreach (var dataRow in token) - { - callback(ParsePosition(dataRow)); - } - await Task.CompletedTask; + foreach (var dataRow in token) + { + callback(ParsePosition(dataRow)); + } + await Task.CompletedTask; }); - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - var m = await GetMarketSymbolsMetadataAsync(); - return m.Select(x => x.MarketSymbol); - } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() - { - /* - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": [ - { - "name": "BTCUSD", - "base_currency": "BTC", - "quote_currency": "USD", - "price_scale": 2, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 100, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.5", - "max_price": "999999.5", - "tick_size": "0.5" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "ETHUSD", - "base_currency": "ETH", - "quote_currency": "USD", - "price_scale": 2, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.05", - "max_price": "99999.95", - "tick_size": "0.05" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "EOSUSD", - "base_currency": "EOS", - "quote_currency": "USD", - "price_scale": 3, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.001", - "max_price": "1999.999", - "tick_size": "0.001" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "XRPUSD", - "base_currency": "XRP", - "quote_currency": "USD", - "price_scale": 4, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.0001", - "max_price": "199.9999", - "tick_size": "0.0001" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - } - ], - "time_now": "1581411225.414179" - }} - */ - - List markets = new List(); - JToken allSymbols = CheckRetCode(await DoMakeJsonRequestAsync("/v2/public/symbols")); + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + var m = await GetMarketSymbolsMetadataAsync(); + return m.Select(x => x.MarketSymbol); + } + + protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": [ + { + "name": "BTCUSD", + "base_currency": "BTC", + "quote_currency": "USD", + "price_scale": 2, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 100, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.5", + "max_price": "999999.5", + "tick_size": "0.5" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "ETHUSD", + "base_currency": "ETH", + "quote_currency": "USD", + "price_scale": 2, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.05", + "max_price": "99999.95", + "tick_size": "0.05" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "EOSUSD", + "base_currency": "EOS", + "quote_currency": "USD", + "price_scale": 3, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.001", + "max_price": "1999.999", + "tick_size": "0.001" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "XRPUSD", + "base_currency": "XRP", + "quote_currency": "USD", + "price_scale": 4, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.0001", + "max_price": "199.9999", + "tick_size": "0.0001" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + } + ], + "time_now": "1581411225.414179" + }} + */ + + List markets = new List(); + JToken allSymbols = CheckRetCode(await DoMakeJsonRequestAsync("/v2/public/symbols")); foreach (JToken marketSymbolToken in allSymbols) - { - var market = new ExchangeMarket - { - MarketSymbol = marketSymbolToken["name"].ToStringUpperInvariant(), - IsActive = true, - QuoteCurrency = marketSymbolToken["quote_currency"].ToStringUpperInvariant(), - BaseCurrency = marketSymbolToken["base_currency"].ToStringUpperInvariant(), - }; - - try - { - JToken priceFilter = marketSymbolToken["price_filter"]; - market.MinPrice = priceFilter["min_price"].ConvertInvariant(); - market.MaxPrice = priceFilter["max_price"].ConvertInvariant(); - market.PriceStepSize = priceFilter["tick_size"].ConvertInvariant(); - - JToken lotSizeFilter = marketSymbolToken["lot_size_filter"]; - market.MinTradeSize = lotSizeFilter["min_trading_qty"].ConvertInvariant(); - market.MaxTradeSize = lotSizeFilter["max_trading_qty"].ConvertInvariant(); - market.QuantityStepSize = lotSizeFilter["qty_step"].ConvertInvariant(); - } - catch - { - - } - markets.Add(market); - } - return markets; - } - - - private async Task> DoGetAmountsAsync(string field) - { - /* - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "BTC": { - "equity": 1002, //equity = wallet_balance + unrealised_pnl - "available_balance": 999.99987471, //available_balance - //In Isolated Margin Mode: - // available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) - //In Cross Margin Mode: - //if unrealised_pnl > 0: - //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin); - //if unrealised_pnl < 0: - //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + unrealised_pnl - "used_margin": 0.00012529, //used_margin = wallet_balance - available_balance - "order_margin": 0.00012529, //Used margin by order - "position_margin": 0, //position margin - "occ_closing_fee": 0, //position closing fee - "occ_funding_fee": 0, //funding fee - "wallet_balance": 1000, //wallet balance. When in Cross Margin mod, the number minus your unclosed loss is your real wallet balance. - "realised_pnl": 0, //daily realized profit and loss - "unrealised_pnl": 2, //unrealised profit and loss - //when side is sell: - // unrealised_pnl = size * (1.0 / mark_price - 1.0 / entry_price) - //when side is buy: - // unrealised_pnl = size * (1.0 / entry_price - 1.0 / mark_price) - "cum_realised_pnl": 0, //total relised profit and loss - "given_cash": 0, //given_cash - "service_cash": 0 //service_cash - } - }, - "time_now": "1578284274.816029", - "rate_limit_status": 98, - "rate_limit_reset_ms": 1580885703683, - "rate_limit": 100 - } - */ - Dictionary amounts = new Dictionary(); - var queryString = await GetAuthenticatedQueryString(); - JToken currencies = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/wallet/balance?" + queryString, BaseUrl, null, "GET")); - foreach (JProperty currency in currencies.Children()) - { - var balance = currency.Value[field].ConvertInvariant(); - if (amounts.ContainsKey(currency.Name)) - { - amounts[currency.Name] += balance; - } - else - { - amounts[currency.Name] = balance; - } - } - return amounts; - } - - protected override async Task> OnGetAmountsAsync() - { - return await DoGetAmountsAsync("equity"); - } - - protected override async Task> OnGetAmountsAvailableToTradeAsync() - { - return await DoGetAmountsAsync("available_balance"); - } - - public async Task> GetCurrentPositionsAsync() - { - /* - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "id": 27913, - "user_id": 1, - "risk_id": 1, - "symbol": "BTCUSD", - "side": "Buy", - "size": 5, - "position_value": "0.0006947", - "entry_price": "7197.35137469", - "is_isolated":true, - "auto_add_margin": 0, - "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level - "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: - effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) - "position_margin": "0.0006947", - "liq_price": "3608", - "bust_price": "3599", - "occ_closing_fee": "0.00000105", - "occ_funding_fee": "0", - "take_profit": "0", - "stop_loss": "0", - "trailing_stop": "0", - "position_status": "Normal", - "deleverage_indicator": 4, - "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", - "order_margin": "0.00029477", - "wallet_balance": "0.03000227", - "realised_pnl": "-0.00000126", - "unrealised_pnl": 0, - "cum_realised_pnl": "-0.00001306", - "cross_seq": 444081383, - "position_seq": 287141589, - "created_at": "2019-10-19T17:04:55Z", - "updated_at": "2019-12-27T20:25:45.158767Z" - }, - "time_now": "1577480599.097287", - "rate_limit_status": 119, - "rate_limit_reset_ms": 1580885703683, - "rate_limit": 120 - } - */ - var queryString = await GetAuthenticatedQueryString(); - JToken token = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/position/list?" + queryString, BaseUrl, null, "GET")); - List positions = new List(); - foreach (var item in token) - { - positions.Add(ParsePosition(item["data"])); - } - return positions; - } - - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - var extraParams = new Dictionary(); - extraParams["order_status"] = "Created,New,PartiallyFilled"; - if (!string.IsNullOrWhiteSpace(marketSymbol)) - { - extraParams["symbol"] = marketSymbol; - } - else - { - throw new Exception("marketSymbol is required"); - } - var queryString = await GetAuthenticatedQueryString(extraParams); - JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order/list?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); - - List orders = new List(); - foreach (JToken order in token["data"]) - { - orders.Add(ParseOrder(order, retCode, retMessage)); - } - - return orders; - } - - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) - { - var extraParams = new Dictionary(); - extraParams["order_id"] = orderId; - if (!string.IsNullOrWhiteSpace(marketSymbol)) - { - extraParams["symbol"] = marketSymbol; - } - else - { - throw new Exception("marketSymbol is required"); - } - - var queryString = await GetAuthenticatedQueryString(extraParams); - JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); - - List orders = new List(); - foreach (JToken order in token) - { - orders.Add(ParseOrder(order, retCode, retMessage)); - } - - return orders[0]; - } - - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) - { - var extraParams = new Dictionary(); - extraParams["order_id"] = orderId; - if (!string.IsNullOrWhiteSpace(marketSymbol)) - { - extraParams["symbol"] = marketSymbol; - } - else - { - throw new Exception("marketSymbol is required"); - } - - var payload = await GetAuthenticatedPayload(extraParams); - CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancel", BaseUrl, payload, "POST")); - // new string[] {"0", "30032"}); - //30032: order has been finished or canceled - } - - public async Task CancelAllOrdersAsync(string marketSymbol) - { - var extraParams = new Dictionary(); - extraParams["symbol"] = marketSymbol; - var payload = await GetAuthenticatedPayload(extraParams); - CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancelAll", BaseUrl, payload, "POST")); - } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - var payload = new Dictionary(); - await AddOrderToPayload(order, payload); - payload = await GetAuthenticatedPayload(payload); - JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/create", BaseUrl, payload, "POST"), out var retCode, out var retMessage); - return ParseOrder(token, retCode, retMessage); - } - - public async Task OnAmendOrderAsync(ExchangeOrderRequest order) - { - var payload = new Dictionary(); - payload["symbol"] = order.MarketSymbol; - if(order.OrderId != null) - payload["order_id"] = order.OrderId; - else if(order.ClientOrderId != null) - payload["order_link_id"] = order.ClientOrderId; - else - throw new Exception("Need either OrderId or ClientOrderId"); - - payload["p_r_qty"] = (long) await ClampOrderQuantity(order.MarketSymbol, order.Amount); - if(order.OrderType!=OrderType.Market) - payload["p_r_price"] = order.Price; - - payload = await GetAuthenticatedPayload(payload); - JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/replace", BaseUrl, payload, "POST"), out var retCode, out var retMessage); - - var result = new ExchangeOrderResult(); - result.ResultCode = retCode; - result.Message = retMessage; - if (retCode == "0") - result.OrderId = token["order_id"].ToStringInvariant(); - return result; - } - - private async Task AddOrderToPayload(ExchangeOrderRequest order, Dictionary payload) - { - /* - side true string Side - symbol true string Symbol - order_type true string Active order type - qty true integer Order quantity in USD - price false number Order price - time_in_force true string Time in force - take_profit false number Take profit price, only take effect upon opening the position - stop_loss false number Stop loss price, only take effect upon opening the position - reduce_only false bool What is a reduce-only order? True means your position can only reduce in size if this order is triggered - close_on_trigger false bool What is a close on trigger order? For a closing order. It can only reduce your position, not increase it. If the account has insufficient available balance when the closing order is triggered, then other active orders of similar contracts will be cancelled or reduced. It can be used to ensure your stop loss reduces your position regardless of current available margin. - order_link_id false string Customised order ID, maximum length at 36 characters, and order ID under the same agency has to be unique. - */ - - payload["side"] = order.IsBuy ? "Buy" : "Sell"; - payload["symbol"] = order.MarketSymbol; - payload["order_type"] = order.OrderType.ToStringInvariant(); - payload["qty"] = await ClampOrderQuantity(order.MarketSymbol, order.Amount); - - if(order.OrderType!=OrderType.Market) - payload["price"] = order.Price; - - if(order.ClientOrderId != null) - payload["order_link_id"] = order.ClientOrderId; - - if (order.ExtraParameters.TryGetValue("reduce_only", out var reduceOnly)) - { - payload["reduce_only"] = reduceOnly; - } - - if (order.ExtraParameters.TryGetValue("time_in_force", out var timeInForce)) - { - payload["time_in_force"] = timeInForce; - } - else - { - payload["time_in_force"] = "GoodTillCancel"; - } - } - - private ExchangePosition ParsePosition(JToken token) - { - /* - "id": 27913, - "user_id": 1, - "risk_id": 1, - "symbol": "BTCUSD", - "side": "Buy", - "size": 5, - "position_value": "0.0006947", - "entry_price": "7197.35137469", - "is_isolated":true, - "auto_add_margin": 0, - "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level - "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: - effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) - "position_margin": "0.0006947", - "liq_price": "3608", - "bust_price": "3599", - "occ_closing_fee": "0.00000105", - "occ_funding_fee": "0", - "take_profit": "0", - "stop_loss": "0", - "trailing_stop": "0", - "position_status": "Normal", - "deleverage_indicator": 4, - "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", - "order_margin": "0.00029477", - "wallet_balance": "0.03000227", - "realised_pnl": "-0.00000126", - "unrealised_pnl": 0, - "cum_realised_pnl": "-0.00001306", - "cross_seq": 444081383, - "position_seq": 287141589, - "created_at": "2019-10-19T17:04:55Z", - "updated_at": "2019-12-27T20:25:45.158767Z - */ - ExchangePosition result = new ExchangePosition - { - MarketSymbol = token["symbol"].ToStringUpperInvariant(), - Amount = token["size"].ConvertInvariant(), - AveragePrice = token["entry_price"].ConvertInvariant(), - LiquidationPrice = token["liq_price"].ConvertInvariant(), - Leverage = token["effective_leverage"].ConvertInvariant(), - TimeStamp = CryptoUtility.ParseTimestamp(token["updated_at"], TimestampType.Iso8601) - }; - if (token["side"].ToStringInvariant() == "Sell") - result.Amount *= -1; - return result; - } - - private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string resultMessage) - { - /* - Active Order: - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "user_id": 106958, - "symbol": "BTCUSD", - "side": "Buy", - "order_type": "Limit", - "price": "11756.5", - "qty": 1, - "time_in_force": "PostOnly", - "order_status": "Filled", - "ext_fields": { - "o_req_num": -68948112492, - "xreq_type": "x_create" - }, - "last_exec_time": "1596304897.847944", - "last_exec_price": "11756.5", - "leaves_qty": 0, - "leaves_value": "0", - "cum_exec_qty": 1, - "cum_exec_value": "0.00008505", - "cum_exec_fee": "-0.00000002", - "reject_reason": "", - "cancel_type": "", - "order_link_id": "", - "created_at": "2020-08-01T18:00:26Z", - "updated_at": "2020-08-01T18:01:37Z", - "order_id": "e66b101a-ef3f-4647-83b5-28e0f38dcae0" - }, - "time_now": "1597171013.867068", - "rate_limit_status": 599, - "rate_limit_reset_ms": 1597171013861, - "rate_limit": 600 - } - - Active Order List: - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "data": [ - { - "user_id": 160861, - "order_status": "Cancelled", - "symbol": "BTCUSD", - "side": "Buy", - "order_type": "Market", - "price": "9800", - "qty": "16737", - "time_in_force": "ImmediateOrCancel", - "order_link_id": "", - "order_id": "fead08d7-47c0-4d6a-b9e7-5c71d5df8ba1", - "created_at": "2020-07-24T08:22:30Z", - "updated_at": "2020-07-24T08:22:30Z", - "leaves_qty": "0", - "leaves_value": "0", - "cum_exec_qty": "0", - "cum_exec_value": "0", - "cum_exec_fee": "0", - "reject_reason": "EC_NoImmediateQtyToFill" - } - ], - "cursor": "w01XFyyZc8lhtCLl6NgAaYBRfsN9Qtpp1f2AUy3AS4+fFDzNSlVKa0od8DKCqgAn" - }, - "time_now": "1604653633.173848", - "rate_limit_status": 599, - "rate_limit_reset_ms": 1604653633171, - "rate_limit": 600 - } - */ - ExchangeOrderResult result = new ExchangeOrderResult(); - if (token.Count() > 0) - { - result.Amount = token["qty"].ConvertInvariant(); - result.AmountFilled = token["cum_exec_qty"].ConvertInvariant(); - result.Price = token["price"].ConvertInvariant(); - result.IsBuy = token["side"].ToStringInvariant().EqualsWithOption("Buy"); - result.OrderDate = token["created_at"].ConvertInvariant(); - result.OrderId = token["order_id"].ToStringInvariant(); - result.ClientOrderId = token["order_link_id"].ToStringInvariant(); - result.MarketSymbol = token["symbol"].ToStringInvariant(); - - switch (token["order_status"].ToStringInvariant()) - { - case "Created": - case "New": - result.Result = ExchangeAPIOrderResult.Pending; - break; - case "PartiallyFilled": - result.Result = ExchangeAPIOrderResult.FilledPartially; - break; - case "Filled": - result.Result = ExchangeAPIOrderResult.Filled; - break; - case "Cancelled": - result.Result = ExchangeAPIOrderResult.Canceled; - break; - - default: - result.Result = ExchangeAPIOrderResult.Error; - break; - } - } - result.ResultCode = resultCode; - result.Message = resultMessage; - - return result; - } - } - - public partial class ExchangeName { public const string Bybit = "Bybit"; } + { + var market = new ExchangeMarket(); + market.MarketSymbol = marketSymbolToken["name"].ToStringUpperInvariant(); + market.IsActive = true; + market.QuoteCurrency = marketSymbolToken["quote_currency"].ToStringUpperInvariant(); + market.BaseCurrency = marketSymbolToken["base_currency"].ToStringUpperInvariant(); + + JToken priceFilter = marketSymbolToken["price_filter"]; + market.MinPrice = priceFilter["min_price"].ConvertInvariant(); + market.MaxPrice = priceFilter["max_price"].ConvertInvariant(); + market.PriceStepSize = priceFilter["tick_size"].ConvertInvariant(); + + JToken lotSizeFilter = marketSymbolToken["lot_size_filter"]; + market.MinTradeSize = lotSizeFilter["min_trading_qty"].ConvertInvariant(); + market.MaxTradeSize = lotSizeFilter["max_trading_qty"].ConvertInvariant(); + market.QuantityStepSize = lotSizeFilter["qty_step"].ConvertInvariant(); + + markets.Add(market); + } + return markets; + } + + + private async Task> DoGetAmountsAsync(string field) + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "BTC": { + "equity": 1002, //equity = wallet_balance + unrealised_pnl + "available_balance": 999.99987471, //available_balance + //In Isolated Margin Mode: + // available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + //In Cross Margin Mode: + //if unrealised_pnl > 0: + //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin); + //if unrealised_pnl < 0: + //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + unrealised_pnl + "used_margin": 0.00012529, //used_margin = wallet_balance - available_balance + "order_margin": 0.00012529, //Used margin by order + "position_margin": 0, //position margin + "occ_closing_fee": 0, //position closing fee + "occ_funding_fee": 0, //funding fee + "wallet_balance": 1000, //wallet balance. When in Cross Margin mod, the number minus your unclosed loss is your real wallet balance. + "realised_pnl": 0, //daily realized profit and loss + "unrealised_pnl": 2, //unrealised profit and loss + //when side is sell: + // unrealised_pnl = size * (1.0 / mark_price - 1.0 / entry_price) + //when side is buy: + // unrealised_pnl = size * (1.0 / entry_price - 1.0 / mark_price) + "cum_realised_pnl": 0, //total relised profit and loss + "given_cash": 0, //given_cash + "service_cash": 0 //service_cash + } + }, + "time_now": "1578284274.816029", + "rate_limit_status": 98, + "rate_limit_reset_ms": 1580885703683, + "rate_limit": 100 + } + */ + Dictionary amounts = new Dictionary(); + var queryString = await GetAuthenticatedQueryString(); + JToken currencies = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/wallet/balance?" + queryString, BaseUrl, null, "GET")); + foreach (JProperty currency in currencies.Children()) + { + var balance = currency.Value[field].ConvertInvariant(); + if (amounts.ContainsKey(currency.Name)) + { + amounts[currency.Name] += balance; + } + else + { + amounts[currency.Name] = balance; + } + } + return amounts; + } + + protected override async Task> OnGetAmountsAsync() + { + return await DoGetAmountsAsync("equity"); + } + + protected override async Task> OnGetAmountsAvailableToTradeAsync() + { + return await DoGetAmountsAsync("available_balance"); + } + + public async Task> GetCurrentPositionsAsync() + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "id": 27913, + "user_id": 1, + "risk_id": 1, + "symbol": "BTCUSD", + "side": "Buy", + "size": 5, + "position_value": "0.0006947", + "entry_price": "7197.35137469", + "is_isolated":true, + "auto_add_margin": 0, + "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level + "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: + effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) + "position_margin": "0.0006947", + "liq_price": "3608", + "bust_price": "3599", + "occ_closing_fee": "0.00000105", + "occ_funding_fee": "0", + "take_profit": "0", + "stop_loss": "0", + "trailing_stop": "0", + "position_status": "Normal", + "deleverage_indicator": 4, + "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", + "order_margin": "0.00029477", + "wallet_balance": "0.03000227", + "realised_pnl": "-0.00000126", + "unrealised_pnl": 0, + "cum_realised_pnl": "-0.00001306", + "cross_seq": 444081383, + "position_seq": 287141589, + "created_at": "2019-10-19T17:04:55Z", + "updated_at": "2019-12-27T20:25:45.158767Z" + }, + "time_now": "1577480599.097287", + "rate_limit_status": 119, + "rate_limit_reset_ms": 1580885703683, + "rate_limit": 120 + } + */ + var queryString = await GetAuthenticatedQueryString(); + JToken token = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/position/list?" + queryString, BaseUrl, null, "GET")); + List positions = new List(); + foreach (var item in token) + { + positions.Add(ParsePosition(item["data"])); + } + return positions; + } + + protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_status"] = "Created,New,PartiallyFilled"; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + var queryString = await GetAuthenticatedQueryString(extraParams); + JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order/list?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); + + List orders = new List(); + foreach (JToken order in token["data"]) + { + orders.Add(ParseOrder(order, retCode, retMessage)); + } + + return orders; + } + + protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_id"] = orderId; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + + var queryString = await GetAuthenticatedQueryString(extraParams); + JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); + + List orders = new List(); + foreach (JToken order in token) + { + orders.Add(ParseOrder(order, retCode, retMessage)); + } + + return orders[0]; + } + + protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_id"] = orderId; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + + var payload = await GetAuthenticatedPayload(extraParams); + CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancel", BaseUrl, payload, "POST")); + // new string[] {"0", "30032"}); + //30032: order has been finished or canceled + } + + public async Task CancelAllOrdersAsync(string marketSymbol) + { + var extraParams = new Dictionary(); + extraParams["symbol"] = marketSymbol; + var payload = await GetAuthenticatedPayload(extraParams); + CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancelAll", BaseUrl, payload, "POST")); + } + + protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + var payload = new Dictionary(); + await AddOrderToPayload(order, payload); + payload = await GetAuthenticatedPayload(payload); + JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/create", BaseUrl, payload, "POST"), out var retCode, out var retMessage); + return ParseOrder(token, retCode, retMessage); + } + + public async Task OnAmendOrderAsync(ExchangeOrderRequest order) + { + var payload = new Dictionary(); + payload["symbol"] = order.MarketSymbol; + if(order.OrderId != null) + payload["order_id"] = order.OrderId; + else if(order.ClientOrderId != null) + payload["order_link_id"] = order.ClientOrderId; + else + throw new Exception("Need either OrderId or ClientOrderId"); + + payload["p_r_qty"] = (long) await ClampOrderQuantity(order.MarketSymbol, order.Amount); + if(order.OrderType!=OrderType.Market) + payload["p_r_price"] = order.Price; + + payload = await GetAuthenticatedPayload(payload); + JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/replace", BaseUrl, payload, "POST"), out var retCode, out var retMessage); + + var result = new ExchangeOrderResult(); + result.ResultCode = retCode; + result.Message = retMessage; + if (retCode == "0") + result.OrderId = token["order_id"].ToStringInvariant(); + return result; + } + + private async Task AddOrderToPayload(ExchangeOrderRequest order, Dictionary payload) + { + /* + side true string Side + symbol true string Symbol + order_type true string Active order type + qty true integer Order quantity in USD + price false number Order price + time_in_force true string Time in force + take_profit false number Take profit price, only take effect upon opening the position + stop_loss false number Stop loss price, only take effect upon opening the position + reduce_only false bool What is a reduce-only order? True means your position can only reduce in size if this order is triggered + close_on_trigger false bool What is a close on trigger order? For a closing order. It can only reduce your position, not increase it. If the account has insufficient available balance when the closing order is triggered, then other active orders of similar contracts will be cancelled or reduced. It can be used to ensure your stop loss reduces your position regardless of current available margin. + order_link_id false string Customised order ID, maximum length at 36 characters, and order ID under the same agency has to be unique. + */ + + payload["side"] = order.IsBuy ? "Buy" : "Sell"; + payload["symbol"] = order.MarketSymbol; + payload["order_type"] = order.OrderType.ToStringInvariant(); + payload["qty"] = await ClampOrderQuantity(order.MarketSymbol, order.Amount); + + if(order.OrderType!=OrderType.Market) + payload["price"] = order.Price; + + if(order.ClientOrderId != null) + payload["order_link_id"] = order.ClientOrderId; + + if (order.ExtraParameters.TryGetValue("reduce_only", out var reduceOnly)) + { + payload["reduce_only"] = reduceOnly; + } + + if (order.ExtraParameters.TryGetValue("time_in_force", out var timeInForce)) + { + payload["time_in_force"] = timeInForce; + } + else + { + payload["time_in_force"] = "GoodTillCancel"; + } + } + + private ExchangePosition ParsePosition(JToken token) + { + /* + "id": 27913, + "user_id": 1, + "risk_id": 1, + "symbol": "BTCUSD", + "side": "Buy", + "size": 5, + "position_value": "0.0006947", + "entry_price": "7197.35137469", + "is_isolated":true, + "auto_add_margin": 0, + "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level + "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: + effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) + "position_margin": "0.0006947", + "liq_price": "3608", + "bust_price": "3599", + "occ_closing_fee": "0.00000105", + "occ_funding_fee": "0", + "take_profit": "0", + "stop_loss": "0", + "trailing_stop": "0", + "position_status": "Normal", + "deleverage_indicator": 4, + "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", + "order_margin": "0.00029477", + "wallet_balance": "0.03000227", + "realised_pnl": "-0.00000126", + "unrealised_pnl": 0, + "cum_realised_pnl": "-0.00001306", + "cross_seq": 444081383, + "position_seq": 287141589, + "created_at": "2019-10-19T17:04:55Z", + "updated_at": "2019-12-27T20:25:45.158767Z + */ + ExchangePosition result = new ExchangePosition + { + MarketSymbol = token["symbol"].ToStringUpperInvariant(), + Amount = token["size"].ConvertInvariant(), + AveragePrice = token["entry_price"].ConvertInvariant(), + LiquidationPrice = token["liq_price"].ConvertInvariant(), + Leverage = token["effective_leverage"].ConvertInvariant(), + TimeStamp = CryptoUtility.ParseTimestamp(token["updated_at"], TimestampType.Iso8601) + }; + if (token["side"].ToStringInvariant() == "Sell") + result.Amount *= -1; + return result; + } + + private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string resultMessage) + { + /* + Active Order: + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "user_id": 106958, + "symbol": "BTCUSD", + "side": "Buy", + "order_type": "Limit", + "price": "11756.5", + "qty": 1, + "time_in_force": "PostOnly", + "order_status": "Filled", + "ext_fields": { + "o_req_num": -68948112492, + "xreq_type": "x_create" + }, + "last_exec_time": "1596304897.847944", + "last_exec_price": "11756.5", + "leaves_qty": 0, + "leaves_value": "0", + "cum_exec_qty": 1, + "cum_exec_value": "0.00008505", + "cum_exec_fee": "-0.00000002", + "reject_reason": "", + "cancel_type": "", + "order_link_id": "", + "created_at": "2020-08-01T18:00:26Z", + "updated_at": "2020-08-01T18:01:37Z", + "order_id": "e66b101a-ef3f-4647-83b5-28e0f38dcae0" + }, + "time_now": "1597171013.867068", + "rate_limit_status": 599, + "rate_limit_reset_ms": 1597171013861, + "rate_limit": 600 + } + + Active Order List: + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "data": [ + { + "user_id": 160861, + "order_status": "Cancelled", + "symbol": "BTCUSD", + "side": "Buy", + "order_type": "Market", + "price": "9800", + "qty": "16737", + "time_in_force": "ImmediateOrCancel", + "order_link_id": "", + "order_id": "fead08d7-47c0-4d6a-b9e7-5c71d5df8ba1", + "created_at": "2020-07-24T08:22:30Z", + "updated_at": "2020-07-24T08:22:30Z", + "leaves_qty": "0", + "leaves_value": "0", + "cum_exec_qty": "0", + "cum_exec_value": "0", + "cum_exec_fee": "0", + "reject_reason": "EC_NoImmediateQtyToFill" + } + ], + "cursor": "w01XFyyZc8lhtCLl6NgAaYBRfsN9Qtpp1f2AUy3AS4+fFDzNSlVKa0od8DKCqgAn" + }, + "time_now": "1604653633.173848", + "rate_limit_status": 599, + "rate_limit_reset_ms": 1604653633171, + "rate_limit": 600 + } + */ + ExchangeOrderResult result = new ExchangeOrderResult(); + if (token.Count() > 0) + { + result.Amount = token["qty"].ConvertInvariant(); + result.AmountFilled = token["cum_exec_qty"].ConvertInvariant(); + result.Price = token["price"].ConvertInvariant(); + result.IsBuy = token["side"].ToStringInvariant().EqualsWithOption("Buy"); + result.OrderDate = token["created_at"].ConvertInvariant(); + result.OrderId = token["order_id"].ToStringInvariant(); + result.ClientOrderId = token["order_link_id"].ToStringInvariant(); + result.MarketSymbol = token["symbol"].ToStringInvariant(); + + switch (token["order_status"].ToStringInvariant()) + { + case "Created": + case "New": + result.Result = ExchangeAPIOrderResult.Pending; + break; + case "PartiallyFilled": + result.Result = ExchangeAPIOrderResult.FilledPartially; + break; + case "Filled": + result.Result = ExchangeAPIOrderResult.Filled; + break; + case "Cancelled": + result.Result = ExchangeAPIOrderResult.Canceled; + break; + + default: + result.Result = ExchangeAPIOrderResult.Error; + break; + } + } + result.ResultCode = resultCode; + result.Message = resultMessage; + + return result; + } + } + + public partial class ExchangeName { public const string Bybit = "Bybit"; } } From 56079d7f7ce5df9fee5a7942f976ec5d53665255 Mon Sep 17 00:00:00 2001 From: JacobJT Date: Sat, 28 Nov 2020 12:24:30 -0600 Subject: [PATCH 6/7] Revert "Merge branch 'Bybit' of https://github.com/jacobjthompson/ExchangeSharp into Bybit" This reverts commit ae3da5f3f326c3a553620d369a3bffbaa14de7c8, reversing changes made to 941931746959d7372b195098af656cd64085c3f6. --- .gitignore | 459 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b02e5462 --- /dev/null +++ b/.gitignore @@ -0,0 +1,459 @@ + +# Created by https://www.gitignore.io/api/csharp +# Edit at https://www.gitignore.io/?templates=csharp + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# End of https://www.gitignore.io/api/csharp + +# Created by https://www.gitignore.io/api/jetbrains+all +# Edit at https://www.gitignore.io/?templates=jetbrains+all + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +# End of https://www.gitignore.io/api/jetbrains+all + +# Trader binary and log files +*.bin +*.log* +launchSettings.json +**/PublishProfiles/* + +## Project specific +**/keys.bin +dist/ +data/** +!data/.gitkeep \ No newline at end of file From 9f9ed880248a5d0b0e12db209ef41a0cd5f5804e Mon Sep 17 00:00:00 2001 From: JacobJT Date: Sat, 28 Nov 2020 12:24:39 -0600 Subject: [PATCH 7/7] Revert "whitespace" This reverts commit 941931746959d7372b195098af656cd64085c3f6. --- .../API/Exchanges/Bybit/ExchangeBybitAPI.cs | 1738 +++++++++-------- 1 file changed, 873 insertions(+), 865 deletions(-) diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs index 126c17bc..cfd9c9cd 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs @@ -24,134 +24,134 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - public sealed partial class ExchangeBybitAPI : ExchangeAPI - { - private int _recvWindow = 30000; + public sealed partial class ExchangeBybitAPI : ExchangeAPI + { + private int _recvWindow = 30000; - public override string BaseUrl { get; set; } = "https://api.bybit.com"; - public override string BaseUrlWebSocket { get; set; } = "wss://stream.bybit.com/realtime"; - // public override string BaseUrl { get; set; } = "https://api-testnet.bybit.com/"; - // public override string BaseUrlWebSocket { get; set; } = "wss://stream-testnet.bybit.com/realtime"; + public override string BaseUrl { get; set; } = "https://api.bybit.com"; + public override string BaseUrlWebSocket { get; set; } = "wss://stream.bybit.com/realtime"; + // public override string BaseUrl { get; set; } = "https://api-testnet.bybit.com/"; + // public override string BaseUrlWebSocket { get; set; } = "wss://stream-testnet.bybit.com/realtime"; - public ExchangeBybitAPI() - { + public ExchangeBybitAPI() + { NonceStyle = NonceStyle.UnixMilliseconds; - NonceOffset = TimeSpan.FromSeconds(1.0); - - MarketSymbolSeparator = string.Empty; - RequestContentType = "application/json"; - WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; - - RateLimit = new RateGate(100, TimeSpan.FromMinutes(1)); - } - - public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) - { - throw new NotImplementedException(); - } - - public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) - { - throw new NotImplementedException(); - } - - // Was initially struggling with 10002 timestamp errors, so tried calcing clock drift on every request. - // Settled on positive NonceOffset so our clock is not likely ahead of theirs on arrival (assuming accurate client/server side clocks) - // And larger recv_window so our packets have plenty of time to arrive - // protected override async Task OnGetNonceOffset() - // { - // string stringResult = await MakeRequestAsync("/v2/public/time"); - // var token = JsonConvert.DeserializeObject(stringResult); - // DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["time_now"].ConvertInvariant()); - // var now = CryptoUtility.UtcNow; - // NonceOffset = now - serverDate + TimeSpan.FromSeconds(1); // how much time to substract from Nonce when making a request - // } - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if ((payload != null) && payload.ContainsKey("sign") && request.Method == "POST") - { - await CryptoUtility.WritePayloadJsonToRequestAsync(request, payload); - } - } + NonceOffset = TimeSpan.FromSeconds(1.0); + + MarketSymbolSeparator = string.Empty; + RequestContentType = "application/json"; + WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; + + RateLimit = new RateGate(100, TimeSpan.FromMinutes(1)); + } + + public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) + { + throw new NotImplementedException(); + } + + public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) + { + throw new NotImplementedException(); + } + + // Was initially struggling with 10002 timestamp errors, so tried calcing clock drift on every request. + // Settled on positive NonceOffset so our clock is not likely ahead of theirs on arrival (assuming accurate client/server side clocks) + // And larger recv_window so our packets have plenty of time to arrive + // protected override async Task OnGetNonceOffset() + // { + // string stringResult = await MakeRequestAsync("/v2/public/time"); + // var token = JsonConvert.DeserializeObject(stringResult); + // DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["time_now"].ConvertInvariant()); + // var now = CryptoUtility.UtcNow; + // NonceOffset = now - serverDate + TimeSpan.FromSeconds(1); // how much time to substract from Nonce when making a request + // } + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if ((payload != null) && payload.ContainsKey("sign") && request.Method == "POST") + { + await CryptoUtility.WritePayloadJsonToRequestAsync(request, payload); + } + } #nullable enable - //Not using MakeJsonRequest... so we can perform our own check on the ret_code - private async Task DoMakeJsonRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null) - { - await new SynchronizationContextRemover(); - - string stringResult = await MakeRequestAsync(url, baseUrl, payload, requestMethod); - return JsonConvert.DeserializeObject(stringResult); - } + //Not using MakeJsonRequest... so we can perform our own check on the ret_code + private async Task DoMakeJsonRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null) + { + await new SynchronizationContextRemover(); + + string stringResult = await MakeRequestAsync(url, baseUrl, payload, requestMethod); + return JsonConvert.DeserializeObject(stringResult); + } #nullable disable - private JToken CheckRetCode(JToken response, string[] allowedRetCodes) - { - var result = GetResult(response, out var retCode, out var retMessage); - if (!allowedRetCodes.Contains(retCode)) - { - throw new Exception($"Invalid ret_code {retCode}, ret_msg {retMessage}"); - } - return result; - } - - private JToken CheckRetCode(JToken response) - { - return CheckRetCode(response, new string[] {"0"}); - } - - private JToken GetResult(JToken response, out string retCode, out string retMessage) - { - retCode = response["ret_code"].ToStringInvariant(); - retMessage = response["ret_msg"].ToStringInvariant(); - return response["result"]; - } - - private async Task SendWebsocketAuth(IWebSocket socket) { - var payload = await GetNoncePayloadAsync(); - var nonce = (payload["nonce"].ConvertInvariant() + 5000).ToStringInvariant(); - var signature = CryptoUtility.SHA256Sign($"GET/realtime{nonce}", CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + private JToken CheckRetCode(JToken response, string[] allowedRetCodes) + { + var result = GetResult(response, out var retCode, out var retMessage); + if (!allowedRetCodes.Contains(retCode)) + { + throw new Exception($"Invalid ret_code {retCode}, ret_msg {retMessage}"); + } + return result; + } + + private JToken CheckRetCode(JToken response) + { + return CheckRetCode(response, new string[] {"0"}); + } + + private JToken GetResult(JToken response, out string retCode, out string retMessage) + { + retCode = response["ret_code"].ToStringInvariant(); + retMessage = response["ret_msg"].ToStringInvariant(); + return response["result"]; + } + + private async Task SendWebsocketAuth(IWebSocket socket) { + var payload = await GetNoncePayloadAsync(); + var nonce = (payload["nonce"].ConvertInvariant() + 5000).ToStringInvariant(); + var signature = CryptoUtility.SHA256Sign($"GET/realtime{nonce}", CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); await socket.SendMessageAsync(new { op = "auth", args = new [] {PublicApiKey.ToUnsecureString(), nonce, signature} }); - } - - private async Task> GetAuthenticatedPayload(Dictionary requestPayload = null) - { - var payload = await GetNoncePayloadAsync(); - var nonce = payload["nonce"].ConvertInvariant(); - payload.Remove("nonce"); - payload["api_key"] = PublicApiKey.ToUnsecureString(); - payload["timestamp"] = nonce.ToStringInvariant(); - payload["recv_window"] = _recvWindow; - if (requestPayload != null) - { - payload = payload.Concat(requestPayload).ToDictionary(p => p.Key, p => p.Value); - } - - string form = CryptoUtility.GetFormForPayload(payload, false, true); - form = form.Replace("=False", "=false"); - form = form.Replace("=True", "=true"); - payload["sign"] = CryptoUtility.SHA256Sign(form, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); - return payload; - } - - private async Task GetAuthenticatedQueryString(Dictionary requestPayload = null) - { - var payload = await GetAuthenticatedPayload(requestPayload); - var sign = payload["sign"].ToStringInvariant(); - payload.Remove("sign"); - string form = CryptoUtility.GetFormForPayload(payload, false, true); - form += "&sign=" + sign; - return form; - } - - private Task DoConnectWebSocketAsync(Func connected, Func callback, int symbolArrayIndex = 3) - { + } + + private async Task> GetAuthenticatedPayload(Dictionary requestPayload = null) + { + var payload = await GetNoncePayloadAsync(); + var nonce = payload["nonce"].ConvertInvariant(); + payload.Remove("nonce"); + payload["api_key"] = PublicApiKey.ToUnsecureString(); + payload["timestamp"] = nonce.ToStringInvariant(); + payload["recv_window"] = _recvWindow; + if (requestPayload != null) + { + payload = payload.Concat(requestPayload).ToDictionary(p => p.Key, p => p.Value); + } + + string form = CryptoUtility.GetFormForPayload(payload, false, true); + form = form.Replace("=False", "=false"); + form = form.Replace("=True", "=true"); + payload["sign"] = CryptoUtility.SHA256Sign(form, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + return payload; + } + + private async Task GetAuthenticatedQueryString(Dictionary requestPayload = null) + { + var payload = await GetAuthenticatedPayload(requestPayload); + var sign = payload["sign"].ToStringInvariant(); + payload.Remove("sign"); + string form = CryptoUtility.GetFormForPayload(payload, false, true); + form += "&sign=" + sign; + return form; + } + + private Task DoConnectWebSocketAsync(Func connected, Func callback, int symbolArrayIndex = 3) + { Timer pingTimer = null; - return ConnectWebSocketAsync(url: string.Empty, messageCallback: async (_socket, msg) => - { + return ConnectWebSocketAsync(url: string.Empty, messageCallback: async (_socket, msg) => + { var msgString = msg.ToStringFromUTF8(); - JToken token = JToken.Parse(msgString); + JToken token = JToken.Parse(msgString); if (token["ret_msg"]?.ToStringInvariant() == "pong") { // received reply to our ping @@ -159,781 +159,789 @@ private Task DoConnectWebSocketAsync(Func connecte } if (token["topic"] != null) - { - var data = token["data"]; - await callback(_socket, data); - } - else - { - /* - subscription response: - { - "success": true, // Whether subscription is successful - "ret_msg": "", // Successful subscription: "", otherwise it shows error message - "conn_id":"e0e10eee-4eff-4d21-881e-a0c55c25e2da",// current connection id - "request": { // Request to your subscription - "op": "subscribe", - "args": [ - "kline.BTCUSD.1m" - ] - } - } - */ - JToken response = token["request"]; - var op = response["op"]?.ToStringInvariant(); - if ((response != null) && ((op == "subscribe") || (op == "auth"))) - { - var responseMessage = token["ret_msg"]?.ToStringInvariant(); - if (responseMessage != "") - { - Logger.Info("Websocket unable to connect: " + msgString); - return; - } - else if (pingTimer == null) - { - /* - ping response: - { - "success": true, // Whether ping is successful - "ret_msg": "pong", - "conn_id": "036e5d21-804c-4447-a92d-b65a44d00700",// current connection id - "request": { - "op": "ping", - "args": null - } - } - */ - pingTimer = new Timer(callback: async s => await _socket.SendMessageAsync(new { op = "ping" }), - state: null, dueTime: 0, period: 15000); // send a ping every 15 seconds - return; - } - } + { + var data = token["data"]; + await callback(_socket, data); + } + else + { + /* + subscription response: + { + "success": true, // Whether subscription is successful + "ret_msg": "", // Successful subscription: "", otherwise it shows error message + "conn_id":"e0e10eee-4eff-4d21-881e-a0c55c25e2da",// current connection id + "request": { // Request to your subscription + "op": "subscribe", + "args": [ + "kline.BTCUSD.1m" + ] + } + } + */ + JToken response = token["request"]; + var op = response["op"]?.ToStringInvariant(); + if ((response != null) && ((op == "subscribe") || (op == "auth"))) + { + var responseMessage = token["ret_msg"]?.ToStringInvariant(); + if (responseMessage != "") + { + Logger.Info("Websocket unable to connect: " + msgString); + return; + } + else if (pingTimer == null) + { + /* + ping response: + { + "success": true, // Whether ping is successful + "ret_msg": "pong", + "conn_id": "036e5d21-804c-4447-a92d-b65a44d00700",// current connection id + "request": { + "op": "ping", + "args": null + } + } + */ + pingTimer = new Timer(callback: async s => await _socket.SendMessageAsync(new { op = "ping" }), + state: null, dueTime: 0, period: 15000); // send a ping every 15 seconds + return; + } + } } - }, - connectCallback: async (_socket) => - { - await connected(_socket); - _socket.ConnectInterval = TimeSpan.FromHours(0); - }, - disconnectCallback: s => + }, + connectCallback: async (_socket) => + { + await connected(_socket); + _socket.ConnectInterval = TimeSpan.FromHours(0); + }, + disconnectCallback: s => { pingTimer.Dispose(); pingTimer = null; return Task.CompletedTask; }); - } + } - private async Task AddMarketSymbolsToChannel(IWebSocket socket, string argsPrefix, string[] marketSymbols) - { - string fullArgs = argsPrefix; + private async Task AddMarketSymbolsToChannel(IWebSocket socket, string argsPrefix, string[] marketSymbols) + { + string fullArgs = argsPrefix; if (marketSymbols == null || marketSymbols.Length == 0) { fullArgs += "*"; } - else - { - foreach (var symbol in marketSymbols) - { - fullArgs += symbol + "|"; - } - fullArgs = fullArgs.TrimEnd('|'); - } - - await socket.SendMessageAsync(new { op = "subscribe", args = new [] {fullArgs} }); - } - - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) - { + else + { + foreach (var symbol in marketSymbols) + { + fullArgs += symbol + "|"; + } + fullArgs = fullArgs.TrimEnd('|'); + } + + await socket.SendMessageAsync(new { op = "subscribe", args = new [] {fullArgs} }); + } + + protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + { /* - request: - {"op":"subscribe","args":["trade.BTCUSD|XRPUSD"]} + request: + {"op":"subscribe","args":["trade.BTCUSD|XRPUSD"]} */ /* - response: - { - "topic": "trade.BTCUSD", - "data": [ - { - "timestamp": "2020-01-12T16:59:59.000Z", - "trade_time_ms": 1582793344685, // trade time in millisecond - "symbol": "BTCUSD", - "side": "Sell", - "size": 328, - "price": 8098, - "tick_direction": "MinusTick", - "trade_id": "00c706e1-ba52-5bb0-98d0-bf694bdc69f7", - "cross_seq": 1052816407 - } - ] - } - */ + response: + { + "topic": "trade.BTCUSD", + "data": [ + { + "timestamp": "2020-01-12T16:59:59.000Z", + "trade_time_ms": 1582793344685, // trade time in millisecond + "symbol": "BTCUSD", + "side": "Sell", + "size": 328, + "price": 8098, + "tick_direction": "MinusTick", + "trade_id": "00c706e1-ba52-5bb0-98d0-bf694bdc69f7", + "cross_seq": 1052816407 + } + ] + } + */ return await DoConnectWebSocketAsync(async (_socket) => { await AddMarketSymbolsToChannel(_socket, "trade.", marketSymbols); }, async (_socket, token) => { - foreach (var dataRow in token) - { - ExchangeTrade trade = dataRow.ParseTrade( - amountKey: "size", - priceKey: "price", - typeKey: "side", - timestampKey: "timestamp", - timestampType: TimestampType.Iso8601, - idKey: "trade_id"); - await callback(new KeyValuePair(dataRow["symbol"].ToStringInvariant(), trade)); - } + foreach (var dataRow in token) + { + ExchangeTrade trade = dataRow.ParseTrade( + amountKey: "size", + priceKey: "price", + typeKey: "side", + timestampKey: "timestamp", + timestampType: TimestampType.Iso8601, + idKey: "trade_id"); + await callback(new KeyValuePair(dataRow["symbol"].ToStringInvariant(), trade)); + } }); - } + } - public async Task GetPositionWebSocketAsync(Action callback) - { + public async Task GetPositionWebSocketAsync(Action callback) + { /* - request: - {"op": "subscribe", "args": ["position"]} + request: + {"op": "subscribe", "args": ["position"]} */ /* - response: - { - "topic": "position", - "action": "update", - "data": [ - { - "user_id": 1, // user ID - "symbol": "BTCUSD", // the contract for this position - "size": 11, // the current position amount - "side": "Sell", // side - "position_value": "0.00159252", // positional value - "entry_price": "6907.291588174717", // entry price - "liq_price": "7100.234", // liquidation price - "bust_price": "7088.1234", // bankruptcy price - "leverage": "1", // leverage - "order_margin": "1", // order margin - "position_margin": "1", // position margin - "available_balance": "2", // available balance - "take_profit": "0", // take profit price - "tp_trigger_by": "LastPrice", // take profit trigger price, eg: LastPrice, IndexPrice. Conditional order only - "stop_loss": "0", // stop loss price - "sl_trigger_by": "", // stop loss trigger price, eg: LastPrice, IndexPrice. Conditional order only - "realised_pnl": "0.10", // realised PNL - "trailing_stop": "0", // trailing stop points - "trailing_active": "0", // trailing stop trigger price - "wallet_balance": "4.12", // wallet balance - "risk_id": 1, - "occ_closing_fee": "0.1", // position closing - "occ_funding_fee": "0.1", // funding fee - "auto_add_margin": 0, // auto margin replenishment switch - "cum_realised_pnl": "0.12", // Total realized profit and loss - "position_status": "Normal", // status of position (Normal: normal Liq: in the process of liquidation Adl: in the process of Auto-Deleveraging) - // Auto margin replenishment enabled (0: no 1: yes) - "position_seq": 14 // position version number - } - ] - } - */ + response: + { + "topic": "position", + "action": "update", + "data": [ + { + "user_id": 1, // user ID + "symbol": "BTCUSD", // the contract for this position + "size": 11, // the current position amount + "side": "Sell", // side + "position_value": "0.00159252", // positional value + "entry_price": "6907.291588174717", // entry price + "liq_price": "7100.234", // liquidation price + "bust_price": "7088.1234", // bankruptcy price + "leverage": "1", // leverage + "order_margin": "1", // order margin + "position_margin": "1", // position margin + "available_balance": "2", // available balance + "take_profit": "0", // take profit price + "tp_trigger_by": "LastPrice", // take profit trigger price, eg: LastPrice, IndexPrice. Conditional order only + "stop_loss": "0", // stop loss price + "sl_trigger_by": "", // stop loss trigger price, eg: LastPrice, IndexPrice. Conditional order only + "realised_pnl": "0.10", // realised PNL + "trailing_stop": "0", // trailing stop points + "trailing_active": "0", // trailing stop trigger price + "wallet_balance": "4.12", // wallet balance + "risk_id": 1, + "occ_closing_fee": "0.1", // position closing + "occ_funding_fee": "0.1", // funding fee + "auto_add_margin": 0, // auto margin replenishment switch + "cum_realised_pnl": "0.12", // Total realized profit and loss + "position_status": "Normal", // status of position (Normal: normal Liq: in the process of liquidation Adl: in the process of Auto-Deleveraging) + // Auto margin replenishment enabled (0: no 1: yes) + "position_seq": 14 // position version number + } + ] + } + */ return await DoConnectWebSocketAsync(async (_socket) => { - await SendWebsocketAuth(_socket); - await _socket.SendMessageAsync(new { op = "subscribe", args = new [] {"position"} }); + await SendWebsocketAuth(_socket); + await _socket.SendMessageAsync(new { op = "subscribe", args = new [] {"position"} }); }, async (_socket, token) => { - foreach (var dataRow in token) - { - callback(ParsePosition(dataRow)); - } - await Task.CompletedTask; + foreach (var dataRow in token) + { + callback(ParsePosition(dataRow)); + } + await Task.CompletedTask; }); - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - var m = await GetMarketSymbolsMetadataAsync(); - return m.Select(x => x.MarketSymbol); - } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() - { - /* - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": [ - { - "name": "BTCUSD", - "base_currency": "BTC", - "quote_currency": "USD", - "price_scale": 2, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 100, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.5", - "max_price": "999999.5", - "tick_size": "0.5" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "ETHUSD", - "base_currency": "ETH", - "quote_currency": "USD", - "price_scale": 2, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.05", - "max_price": "99999.95", - "tick_size": "0.05" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "EOSUSD", - "base_currency": "EOS", - "quote_currency": "USD", - "price_scale": 3, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.001", - "max_price": "1999.999", - "tick_size": "0.001" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "XRPUSD", - "base_currency": "XRP", - "quote_currency": "USD", - "price_scale": 4, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.0001", - "max_price": "199.9999", - "tick_size": "0.0001" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - } - ], - "time_now": "1581411225.414179" - }} - */ - - List markets = new List(); - JToken allSymbols = CheckRetCode(await DoMakeJsonRequestAsync("/v2/public/symbols")); + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + var m = await GetMarketSymbolsMetadataAsync(); + return m.Select(x => x.MarketSymbol); + } + + protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": [ + { + "name": "BTCUSD", + "base_currency": "BTC", + "quote_currency": "USD", + "price_scale": 2, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 100, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.5", + "max_price": "999999.5", + "tick_size": "0.5" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "ETHUSD", + "base_currency": "ETH", + "quote_currency": "USD", + "price_scale": 2, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.05", + "max_price": "99999.95", + "tick_size": "0.05" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "EOSUSD", + "base_currency": "EOS", + "quote_currency": "USD", + "price_scale": 3, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.001", + "max_price": "1999.999", + "tick_size": "0.001" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "XRPUSD", + "base_currency": "XRP", + "quote_currency": "USD", + "price_scale": 4, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.0001", + "max_price": "199.9999", + "tick_size": "0.0001" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + } + ], + "time_now": "1581411225.414179" + }} + */ + + List markets = new List(); + JToken allSymbols = CheckRetCode(await DoMakeJsonRequestAsync("/v2/public/symbols")); foreach (JToken marketSymbolToken in allSymbols) - { - var market = new ExchangeMarket(); - market.MarketSymbol = marketSymbolToken["name"].ToStringUpperInvariant(); - market.IsActive = true; - market.QuoteCurrency = marketSymbolToken["quote_currency"].ToStringUpperInvariant(); - market.BaseCurrency = marketSymbolToken["base_currency"].ToStringUpperInvariant(); - - JToken priceFilter = marketSymbolToken["price_filter"]; - market.MinPrice = priceFilter["min_price"].ConvertInvariant(); - market.MaxPrice = priceFilter["max_price"].ConvertInvariant(); - market.PriceStepSize = priceFilter["tick_size"].ConvertInvariant(); - - JToken lotSizeFilter = marketSymbolToken["lot_size_filter"]; - market.MinTradeSize = lotSizeFilter["min_trading_qty"].ConvertInvariant(); - market.MaxTradeSize = lotSizeFilter["max_trading_qty"].ConvertInvariant(); - market.QuantityStepSize = lotSizeFilter["qty_step"].ConvertInvariant(); - - markets.Add(market); - } - return markets; - } - - - private async Task> DoGetAmountsAsync(string field) - { - /* - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "BTC": { - "equity": 1002, //equity = wallet_balance + unrealised_pnl - "available_balance": 999.99987471, //available_balance - //In Isolated Margin Mode: - // available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) - //In Cross Margin Mode: - //if unrealised_pnl > 0: - //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin); - //if unrealised_pnl < 0: - //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + unrealised_pnl - "used_margin": 0.00012529, //used_margin = wallet_balance - available_balance - "order_margin": 0.00012529, //Used margin by order - "position_margin": 0, //position margin - "occ_closing_fee": 0, //position closing fee - "occ_funding_fee": 0, //funding fee - "wallet_balance": 1000, //wallet balance. When in Cross Margin mod, the number minus your unclosed loss is your real wallet balance. - "realised_pnl": 0, //daily realized profit and loss - "unrealised_pnl": 2, //unrealised profit and loss - //when side is sell: - // unrealised_pnl = size * (1.0 / mark_price - 1.0 / entry_price) - //when side is buy: - // unrealised_pnl = size * (1.0 / entry_price - 1.0 / mark_price) - "cum_realised_pnl": 0, //total relised profit and loss - "given_cash": 0, //given_cash - "service_cash": 0 //service_cash - } - }, - "time_now": "1578284274.816029", - "rate_limit_status": 98, - "rate_limit_reset_ms": 1580885703683, - "rate_limit": 100 - } - */ - Dictionary amounts = new Dictionary(); - var queryString = await GetAuthenticatedQueryString(); - JToken currencies = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/wallet/balance?" + queryString, BaseUrl, null, "GET")); - foreach (JProperty currency in currencies.Children()) - { - var balance = currency.Value[field].ConvertInvariant(); - if (amounts.ContainsKey(currency.Name)) - { - amounts[currency.Name] += balance; - } - else - { - amounts[currency.Name] = balance; - } - } - return amounts; - } - - protected override async Task> OnGetAmountsAsync() - { - return await DoGetAmountsAsync("equity"); - } - - protected override async Task> OnGetAmountsAvailableToTradeAsync() - { - return await DoGetAmountsAsync("available_balance"); - } - - public async Task> GetCurrentPositionsAsync() - { - /* - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "id": 27913, - "user_id": 1, - "risk_id": 1, - "symbol": "BTCUSD", - "side": "Buy", - "size": 5, - "position_value": "0.0006947", - "entry_price": "7197.35137469", - "is_isolated":true, - "auto_add_margin": 0, - "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level - "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: - effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) - "position_margin": "0.0006947", - "liq_price": "3608", - "bust_price": "3599", - "occ_closing_fee": "0.00000105", - "occ_funding_fee": "0", - "take_profit": "0", - "stop_loss": "0", - "trailing_stop": "0", - "position_status": "Normal", - "deleverage_indicator": 4, - "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", - "order_margin": "0.00029477", - "wallet_balance": "0.03000227", - "realised_pnl": "-0.00000126", - "unrealised_pnl": 0, - "cum_realised_pnl": "-0.00001306", - "cross_seq": 444081383, - "position_seq": 287141589, - "created_at": "2019-10-19T17:04:55Z", - "updated_at": "2019-12-27T20:25:45.158767Z" - }, - "time_now": "1577480599.097287", - "rate_limit_status": 119, - "rate_limit_reset_ms": 1580885703683, - "rate_limit": 120 - } - */ - var queryString = await GetAuthenticatedQueryString(); - JToken token = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/position/list?" + queryString, BaseUrl, null, "GET")); - List positions = new List(); - foreach (var item in token) - { - positions.Add(ParsePosition(item["data"])); - } - return positions; - } - - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - var extraParams = new Dictionary(); - extraParams["order_status"] = "Created,New,PartiallyFilled"; - if (!string.IsNullOrWhiteSpace(marketSymbol)) - { - extraParams["symbol"] = marketSymbol; - } - else - { - throw new Exception("marketSymbol is required"); - } - var queryString = await GetAuthenticatedQueryString(extraParams); - JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order/list?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); - - List orders = new List(); - foreach (JToken order in token["data"]) - { - orders.Add(ParseOrder(order, retCode, retMessage)); - } - - return orders; - } - - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) - { - var extraParams = new Dictionary(); - extraParams["order_id"] = orderId; - if (!string.IsNullOrWhiteSpace(marketSymbol)) - { - extraParams["symbol"] = marketSymbol; - } - else - { - throw new Exception("marketSymbol is required"); - } - - var queryString = await GetAuthenticatedQueryString(extraParams); - JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); - - List orders = new List(); - foreach (JToken order in token) - { - orders.Add(ParseOrder(order, retCode, retMessage)); - } - - return orders[0]; - } - - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) - { - var extraParams = new Dictionary(); - extraParams["order_id"] = orderId; - if (!string.IsNullOrWhiteSpace(marketSymbol)) - { - extraParams["symbol"] = marketSymbol; - } - else - { - throw new Exception("marketSymbol is required"); - } - - var payload = await GetAuthenticatedPayload(extraParams); - CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancel", BaseUrl, payload, "POST")); - // new string[] {"0", "30032"}); - //30032: order has been finished or canceled - } - - public async Task CancelAllOrdersAsync(string marketSymbol) - { - var extraParams = new Dictionary(); - extraParams["symbol"] = marketSymbol; - var payload = await GetAuthenticatedPayload(extraParams); - CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancelAll", BaseUrl, payload, "POST")); - } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - var payload = new Dictionary(); - await AddOrderToPayload(order, payload); - payload = await GetAuthenticatedPayload(payload); - JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/create", BaseUrl, payload, "POST"), out var retCode, out var retMessage); - return ParseOrder(token, retCode, retMessage); - } - - public async Task OnAmendOrderAsync(ExchangeOrderRequest order) - { - var payload = new Dictionary(); - payload["symbol"] = order.MarketSymbol; - if(order.OrderId != null) - payload["order_id"] = order.OrderId; - else if(order.ClientOrderId != null) - payload["order_link_id"] = order.ClientOrderId; - else - throw new Exception("Need either OrderId or ClientOrderId"); - - payload["p_r_qty"] = (long) await ClampOrderQuantity(order.MarketSymbol, order.Amount); - if(order.OrderType!=OrderType.Market) - payload["p_r_price"] = order.Price; - - payload = await GetAuthenticatedPayload(payload); - JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/replace", BaseUrl, payload, "POST"), out var retCode, out var retMessage); - - var result = new ExchangeOrderResult(); - result.ResultCode = retCode; - result.Message = retMessage; - if (retCode == "0") - result.OrderId = token["order_id"].ToStringInvariant(); - return result; - } - - private async Task AddOrderToPayload(ExchangeOrderRequest order, Dictionary payload) - { - /* - side true string Side - symbol true string Symbol - order_type true string Active order type - qty true integer Order quantity in USD - price false number Order price - time_in_force true string Time in force - take_profit false number Take profit price, only take effect upon opening the position - stop_loss false number Stop loss price, only take effect upon opening the position - reduce_only false bool What is a reduce-only order? True means your position can only reduce in size if this order is triggered - close_on_trigger false bool What is a close on trigger order? For a closing order. It can only reduce your position, not increase it. If the account has insufficient available balance when the closing order is triggered, then other active orders of similar contracts will be cancelled or reduced. It can be used to ensure your stop loss reduces your position regardless of current available margin. - order_link_id false string Customised order ID, maximum length at 36 characters, and order ID under the same agency has to be unique. - */ - - payload["side"] = order.IsBuy ? "Buy" : "Sell"; - payload["symbol"] = order.MarketSymbol; - payload["order_type"] = order.OrderType.ToStringInvariant(); - payload["qty"] = await ClampOrderQuantity(order.MarketSymbol, order.Amount); - - if(order.OrderType!=OrderType.Market) - payload["price"] = order.Price; - - if(order.ClientOrderId != null) - payload["order_link_id"] = order.ClientOrderId; - - if (order.ExtraParameters.TryGetValue("reduce_only", out var reduceOnly)) - { - payload["reduce_only"] = reduceOnly; - } - - if (order.ExtraParameters.TryGetValue("time_in_force", out var timeInForce)) - { - payload["time_in_force"] = timeInForce; - } - else - { - payload["time_in_force"] = "GoodTillCancel"; - } - } - - private ExchangePosition ParsePosition(JToken token) - { - /* - "id": 27913, - "user_id": 1, - "risk_id": 1, - "symbol": "BTCUSD", - "side": "Buy", - "size": 5, - "position_value": "0.0006947", - "entry_price": "7197.35137469", - "is_isolated":true, - "auto_add_margin": 0, - "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level - "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: - effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) - "position_margin": "0.0006947", - "liq_price": "3608", - "bust_price": "3599", - "occ_closing_fee": "0.00000105", - "occ_funding_fee": "0", - "take_profit": "0", - "stop_loss": "0", - "trailing_stop": "0", - "position_status": "Normal", - "deleverage_indicator": 4, - "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", - "order_margin": "0.00029477", - "wallet_balance": "0.03000227", - "realised_pnl": "-0.00000126", - "unrealised_pnl": 0, - "cum_realised_pnl": "-0.00001306", - "cross_seq": 444081383, - "position_seq": 287141589, - "created_at": "2019-10-19T17:04:55Z", - "updated_at": "2019-12-27T20:25:45.158767Z - */ - ExchangePosition result = new ExchangePosition - { - MarketSymbol = token["symbol"].ToStringUpperInvariant(), - Amount = token["size"].ConvertInvariant(), - AveragePrice = token["entry_price"].ConvertInvariant(), - LiquidationPrice = token["liq_price"].ConvertInvariant(), - Leverage = token["effective_leverage"].ConvertInvariant(), - TimeStamp = CryptoUtility.ParseTimestamp(token["updated_at"], TimestampType.Iso8601) - }; - if (token["side"].ToStringInvariant() == "Sell") - result.Amount *= -1; - return result; - } - - private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string resultMessage) - { - /* - Active Order: - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "user_id": 106958, - "symbol": "BTCUSD", - "side": "Buy", - "order_type": "Limit", - "price": "11756.5", - "qty": 1, - "time_in_force": "PostOnly", - "order_status": "Filled", - "ext_fields": { - "o_req_num": -68948112492, - "xreq_type": "x_create" - }, - "last_exec_time": "1596304897.847944", - "last_exec_price": "11756.5", - "leaves_qty": 0, - "leaves_value": "0", - "cum_exec_qty": 1, - "cum_exec_value": "0.00008505", - "cum_exec_fee": "-0.00000002", - "reject_reason": "", - "cancel_type": "", - "order_link_id": "", - "created_at": "2020-08-01T18:00:26Z", - "updated_at": "2020-08-01T18:01:37Z", - "order_id": "e66b101a-ef3f-4647-83b5-28e0f38dcae0" - }, - "time_now": "1597171013.867068", - "rate_limit_status": 599, - "rate_limit_reset_ms": 1597171013861, - "rate_limit": 600 - } - - Active Order List: - { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "data": [ - { - "user_id": 160861, - "order_status": "Cancelled", - "symbol": "BTCUSD", - "side": "Buy", - "order_type": "Market", - "price": "9800", - "qty": "16737", - "time_in_force": "ImmediateOrCancel", - "order_link_id": "", - "order_id": "fead08d7-47c0-4d6a-b9e7-5c71d5df8ba1", - "created_at": "2020-07-24T08:22:30Z", - "updated_at": "2020-07-24T08:22:30Z", - "leaves_qty": "0", - "leaves_value": "0", - "cum_exec_qty": "0", - "cum_exec_value": "0", - "cum_exec_fee": "0", - "reject_reason": "EC_NoImmediateQtyToFill" - } - ], - "cursor": "w01XFyyZc8lhtCLl6NgAaYBRfsN9Qtpp1f2AUy3AS4+fFDzNSlVKa0od8DKCqgAn" - }, - "time_now": "1604653633.173848", - "rate_limit_status": 599, - "rate_limit_reset_ms": 1604653633171, - "rate_limit": 600 - } - */ - ExchangeOrderResult result = new ExchangeOrderResult(); - if (token.Count() > 0) - { - result.Amount = token["qty"].ConvertInvariant(); - result.AmountFilled = token["cum_exec_qty"].ConvertInvariant(); - result.Price = token["price"].ConvertInvariant(); - result.IsBuy = token["side"].ToStringInvariant().EqualsWithOption("Buy"); - result.OrderDate = token["created_at"].ConvertInvariant(); - result.OrderId = token["order_id"].ToStringInvariant(); - result.ClientOrderId = token["order_link_id"].ToStringInvariant(); - result.MarketSymbol = token["symbol"].ToStringInvariant(); - - switch (token["order_status"].ToStringInvariant()) - { - case "Created": - case "New": - result.Result = ExchangeAPIOrderResult.Pending; - break; - case "PartiallyFilled": - result.Result = ExchangeAPIOrderResult.FilledPartially; - break; - case "Filled": - result.Result = ExchangeAPIOrderResult.Filled; - break; - case "Cancelled": - result.Result = ExchangeAPIOrderResult.Canceled; - break; - - default: - result.Result = ExchangeAPIOrderResult.Error; - break; - } - } - result.ResultCode = resultCode; - result.Message = resultMessage; - - return result; - } - } - - public partial class ExchangeName { public const string Bybit = "Bybit"; } + { + var market = new ExchangeMarket + { + MarketSymbol = marketSymbolToken["name"].ToStringUpperInvariant(), + IsActive = true, + QuoteCurrency = marketSymbolToken["quote_currency"].ToStringUpperInvariant(), + BaseCurrency = marketSymbolToken["base_currency"].ToStringUpperInvariant(), + }; + + try + { + JToken priceFilter = marketSymbolToken["price_filter"]; + market.MinPrice = priceFilter["min_price"].ConvertInvariant(); + market.MaxPrice = priceFilter["max_price"].ConvertInvariant(); + market.PriceStepSize = priceFilter["tick_size"].ConvertInvariant(); + + JToken lotSizeFilter = marketSymbolToken["lot_size_filter"]; + market.MinTradeSize = lotSizeFilter["min_trading_qty"].ConvertInvariant(); + market.MaxTradeSize = lotSizeFilter["max_trading_qty"].ConvertInvariant(); + market.QuantityStepSize = lotSizeFilter["qty_step"].ConvertInvariant(); + } + catch + { + + } + markets.Add(market); + } + return markets; + } + + + private async Task> DoGetAmountsAsync(string field) + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "BTC": { + "equity": 1002, //equity = wallet_balance + unrealised_pnl + "available_balance": 999.99987471, //available_balance + //In Isolated Margin Mode: + // available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + //In Cross Margin Mode: + //if unrealised_pnl > 0: + //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin); + //if unrealised_pnl < 0: + //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + unrealised_pnl + "used_margin": 0.00012529, //used_margin = wallet_balance - available_balance + "order_margin": 0.00012529, //Used margin by order + "position_margin": 0, //position margin + "occ_closing_fee": 0, //position closing fee + "occ_funding_fee": 0, //funding fee + "wallet_balance": 1000, //wallet balance. When in Cross Margin mod, the number minus your unclosed loss is your real wallet balance. + "realised_pnl": 0, //daily realized profit and loss + "unrealised_pnl": 2, //unrealised profit and loss + //when side is sell: + // unrealised_pnl = size * (1.0 / mark_price - 1.0 / entry_price) + //when side is buy: + // unrealised_pnl = size * (1.0 / entry_price - 1.0 / mark_price) + "cum_realised_pnl": 0, //total relised profit and loss + "given_cash": 0, //given_cash + "service_cash": 0 //service_cash + } + }, + "time_now": "1578284274.816029", + "rate_limit_status": 98, + "rate_limit_reset_ms": 1580885703683, + "rate_limit": 100 + } + */ + Dictionary amounts = new Dictionary(); + var queryString = await GetAuthenticatedQueryString(); + JToken currencies = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/wallet/balance?" + queryString, BaseUrl, null, "GET")); + foreach (JProperty currency in currencies.Children()) + { + var balance = currency.Value[field].ConvertInvariant(); + if (amounts.ContainsKey(currency.Name)) + { + amounts[currency.Name] += balance; + } + else + { + amounts[currency.Name] = balance; + } + } + return amounts; + } + + protected override async Task> OnGetAmountsAsync() + { + return await DoGetAmountsAsync("equity"); + } + + protected override async Task> OnGetAmountsAvailableToTradeAsync() + { + return await DoGetAmountsAsync("available_balance"); + } + + public async Task> GetCurrentPositionsAsync() + { + /* + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "id": 27913, + "user_id": 1, + "risk_id": 1, + "symbol": "BTCUSD", + "side": "Buy", + "size": 5, + "position_value": "0.0006947", + "entry_price": "7197.35137469", + "is_isolated":true, + "auto_add_margin": 0, + "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level + "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: + effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) + "position_margin": "0.0006947", + "liq_price": "3608", + "bust_price": "3599", + "occ_closing_fee": "0.00000105", + "occ_funding_fee": "0", + "take_profit": "0", + "stop_loss": "0", + "trailing_stop": "0", + "position_status": "Normal", + "deleverage_indicator": 4, + "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", + "order_margin": "0.00029477", + "wallet_balance": "0.03000227", + "realised_pnl": "-0.00000126", + "unrealised_pnl": 0, + "cum_realised_pnl": "-0.00001306", + "cross_seq": 444081383, + "position_seq": 287141589, + "created_at": "2019-10-19T17:04:55Z", + "updated_at": "2019-12-27T20:25:45.158767Z" + }, + "time_now": "1577480599.097287", + "rate_limit_status": 119, + "rate_limit_reset_ms": 1580885703683, + "rate_limit": 120 + } + */ + var queryString = await GetAuthenticatedQueryString(); + JToken token = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/position/list?" + queryString, BaseUrl, null, "GET")); + List positions = new List(); + foreach (var item in token) + { + positions.Add(ParsePosition(item["data"])); + } + return positions; + } + + protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_status"] = "Created,New,PartiallyFilled"; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + var queryString = await GetAuthenticatedQueryString(extraParams); + JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order/list?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); + + List orders = new List(); + foreach (JToken order in token["data"]) + { + orders.Add(ParseOrder(order, retCode, retMessage)); + } + + return orders; + } + + protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_id"] = orderId; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + + var queryString = await GetAuthenticatedQueryString(extraParams); + JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); + + List orders = new List(); + foreach (JToken order in token) + { + orders.Add(ParseOrder(order, retCode, retMessage)); + } + + return orders[0]; + } + + protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + var extraParams = new Dictionary(); + extraParams["order_id"] = orderId; + if (!string.IsNullOrWhiteSpace(marketSymbol)) + { + extraParams["symbol"] = marketSymbol; + } + else + { + throw new Exception("marketSymbol is required"); + } + + var payload = await GetAuthenticatedPayload(extraParams); + CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancel", BaseUrl, payload, "POST")); + // new string[] {"0", "30032"}); + //30032: order has been finished or canceled + } + + public async Task CancelAllOrdersAsync(string marketSymbol) + { + var extraParams = new Dictionary(); + extraParams["symbol"] = marketSymbol; + var payload = await GetAuthenticatedPayload(extraParams); + CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancelAll", BaseUrl, payload, "POST")); + } + + protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + var payload = new Dictionary(); + await AddOrderToPayload(order, payload); + payload = await GetAuthenticatedPayload(payload); + JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/create", BaseUrl, payload, "POST"), out var retCode, out var retMessage); + return ParseOrder(token, retCode, retMessage); + } + + public async Task OnAmendOrderAsync(ExchangeOrderRequest order) + { + var payload = new Dictionary(); + payload["symbol"] = order.MarketSymbol; + if(order.OrderId != null) + payload["order_id"] = order.OrderId; + else if(order.ClientOrderId != null) + payload["order_link_id"] = order.ClientOrderId; + else + throw new Exception("Need either OrderId or ClientOrderId"); + + payload["p_r_qty"] = (long) await ClampOrderQuantity(order.MarketSymbol, order.Amount); + if(order.OrderType!=OrderType.Market) + payload["p_r_price"] = order.Price; + + payload = await GetAuthenticatedPayload(payload); + JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/replace", BaseUrl, payload, "POST"), out var retCode, out var retMessage); + + var result = new ExchangeOrderResult(); + result.ResultCode = retCode; + result.Message = retMessage; + if (retCode == "0") + result.OrderId = token["order_id"].ToStringInvariant(); + return result; + } + + private async Task AddOrderToPayload(ExchangeOrderRequest order, Dictionary payload) + { + /* + side true string Side + symbol true string Symbol + order_type true string Active order type + qty true integer Order quantity in USD + price false number Order price + time_in_force true string Time in force + take_profit false number Take profit price, only take effect upon opening the position + stop_loss false number Stop loss price, only take effect upon opening the position + reduce_only false bool What is a reduce-only order? True means your position can only reduce in size if this order is triggered + close_on_trigger false bool What is a close on trigger order? For a closing order. It can only reduce your position, not increase it. If the account has insufficient available balance when the closing order is triggered, then other active orders of similar contracts will be cancelled or reduced. It can be used to ensure your stop loss reduces your position regardless of current available margin. + order_link_id false string Customised order ID, maximum length at 36 characters, and order ID under the same agency has to be unique. + */ + + payload["side"] = order.IsBuy ? "Buy" : "Sell"; + payload["symbol"] = order.MarketSymbol; + payload["order_type"] = order.OrderType.ToStringInvariant(); + payload["qty"] = await ClampOrderQuantity(order.MarketSymbol, order.Amount); + + if(order.OrderType!=OrderType.Market) + payload["price"] = order.Price; + + if(order.ClientOrderId != null) + payload["order_link_id"] = order.ClientOrderId; + + if (order.ExtraParameters.TryGetValue("reduce_only", out var reduceOnly)) + { + payload["reduce_only"] = reduceOnly; + } + + if (order.ExtraParameters.TryGetValue("time_in_force", out var timeInForce)) + { + payload["time_in_force"] = timeInForce; + } + else + { + payload["time_in_force"] = "GoodTillCancel"; + } + } + + private ExchangePosition ParsePosition(JToken token) + { + /* + "id": 27913, + "user_id": 1, + "risk_id": 1, + "symbol": "BTCUSD", + "side": "Buy", + "size": 5, + "position_value": "0.0006947", + "entry_price": "7197.35137469", + "is_isolated":true, + "auto_add_margin": 0, + "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level + "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: + effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) + "position_margin": "0.0006947", + "liq_price": "3608", + "bust_price": "3599", + "occ_closing_fee": "0.00000105", + "occ_funding_fee": "0", + "take_profit": "0", + "stop_loss": "0", + "trailing_stop": "0", + "position_status": "Normal", + "deleverage_indicator": 4, + "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", + "order_margin": "0.00029477", + "wallet_balance": "0.03000227", + "realised_pnl": "-0.00000126", + "unrealised_pnl": 0, + "cum_realised_pnl": "-0.00001306", + "cross_seq": 444081383, + "position_seq": 287141589, + "created_at": "2019-10-19T17:04:55Z", + "updated_at": "2019-12-27T20:25:45.158767Z + */ + ExchangePosition result = new ExchangePosition + { + MarketSymbol = token["symbol"].ToStringUpperInvariant(), + Amount = token["size"].ConvertInvariant(), + AveragePrice = token["entry_price"].ConvertInvariant(), + LiquidationPrice = token["liq_price"].ConvertInvariant(), + Leverage = token["effective_leverage"].ConvertInvariant(), + TimeStamp = CryptoUtility.ParseTimestamp(token["updated_at"], TimestampType.Iso8601) + }; + if (token["side"].ToStringInvariant() == "Sell") + result.Amount *= -1; + return result; + } + + private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string resultMessage) + { + /* + Active Order: + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "user_id": 106958, + "symbol": "BTCUSD", + "side": "Buy", + "order_type": "Limit", + "price": "11756.5", + "qty": 1, + "time_in_force": "PostOnly", + "order_status": "Filled", + "ext_fields": { + "o_req_num": -68948112492, + "xreq_type": "x_create" + }, + "last_exec_time": "1596304897.847944", + "last_exec_price": "11756.5", + "leaves_qty": 0, + "leaves_value": "0", + "cum_exec_qty": 1, + "cum_exec_value": "0.00008505", + "cum_exec_fee": "-0.00000002", + "reject_reason": "", + "cancel_type": "", + "order_link_id": "", + "created_at": "2020-08-01T18:00:26Z", + "updated_at": "2020-08-01T18:01:37Z", + "order_id": "e66b101a-ef3f-4647-83b5-28e0f38dcae0" + }, + "time_now": "1597171013.867068", + "rate_limit_status": 599, + "rate_limit_reset_ms": 1597171013861, + "rate_limit": 600 + } + + Active Order List: + { + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "data": [ + { + "user_id": 160861, + "order_status": "Cancelled", + "symbol": "BTCUSD", + "side": "Buy", + "order_type": "Market", + "price": "9800", + "qty": "16737", + "time_in_force": "ImmediateOrCancel", + "order_link_id": "", + "order_id": "fead08d7-47c0-4d6a-b9e7-5c71d5df8ba1", + "created_at": "2020-07-24T08:22:30Z", + "updated_at": "2020-07-24T08:22:30Z", + "leaves_qty": "0", + "leaves_value": "0", + "cum_exec_qty": "0", + "cum_exec_value": "0", + "cum_exec_fee": "0", + "reject_reason": "EC_NoImmediateQtyToFill" + } + ], + "cursor": "w01XFyyZc8lhtCLl6NgAaYBRfsN9Qtpp1f2AUy3AS4+fFDzNSlVKa0od8DKCqgAn" + }, + "time_now": "1604653633.173848", + "rate_limit_status": 599, + "rate_limit_reset_ms": 1604653633171, + "rate_limit": 600 + } + */ + ExchangeOrderResult result = new ExchangeOrderResult(); + if (token.Count() > 0) + { + result.Amount = token["qty"].ConvertInvariant(); + result.AmountFilled = token["cum_exec_qty"].ConvertInvariant(); + result.Price = token["price"].ConvertInvariant(); + result.IsBuy = token["side"].ToStringInvariant().EqualsWithOption("Buy"); + result.OrderDate = token["created_at"].ConvertInvariant(); + result.OrderId = token["order_id"].ToStringInvariant(); + result.ClientOrderId = token["order_link_id"].ToStringInvariant(); + result.MarketSymbol = token["symbol"].ToStringInvariant(); + + switch (token["order_status"].ToStringInvariant()) + { + case "Created": + case "New": + result.Result = ExchangeAPIOrderResult.Pending; + break; + case "PartiallyFilled": + result.Result = ExchangeAPIOrderResult.FilledPartially; + break; + case "Filled": + result.Result = ExchangeAPIOrderResult.Filled; + break; + case "Cancelled": + result.Result = ExchangeAPIOrderResult.Canceled; + break; + + default: + result.Result = ExchangeAPIOrderResult.Error; + break; + } + } + result.ResultCode = resultCode; + result.Message = resultMessage; + + return result; + } + } + + public partial class ExchangeName { public const string Bybit = "Bybit"; } }