From aa2b19ef00ecdfc1e5f4c164176211507164a9d8 Mon Sep 17 00:00:00 2001
From: BZ-CO <30245815+BZ-CO@users.noreply.github.com>
Date: Thu, 2 Mar 2023 23:46:47 +0200
Subject: [PATCH] Migration to the new Poloniex API [Private endpoints]
---
.../Exchanges/Poloniex/ExchangePoloniexAPI.cs | 1698 ++++++++---------
.../ExchangePoloniexAPITests.cs | 4 +-
2 files changed, 849 insertions(+), 853 deletions(-)
diff --git a/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs b/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs
index dc060b77..22dc2fb4 100644
--- a/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs
+++ b/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs
@@ -11,50 +11,48 @@ The above copyright notice and this permission notice shall be included in all c
*/
using System.Diagnostics;
-using System.Web;
namespace ExchangeSharp
{
- using Newtonsoft.Json.Linq;
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Threading.Tasks;
-
- public sealed partial class ExchangePoloniexAPI : ExchangeAPI
- {
- public override string BaseUrl { get; set; } = "https://api.poloniex.com";
- public override string BaseUrlWebSocket { get; set; } = "wss://api2.poloniex.com";
+ using Newtonsoft.Json.Linq;
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Threading.Tasks;
+
+ public sealed partial class ExchangePoloniexAPI : ExchangeAPI
+ {
+ public override string BaseUrl { get; set; } = "https://api.poloniex.com";
+ public override string BaseUrlWebSocket { get; set; } = "wss://api2.poloniex.com";
private ExchangePoloniexAPI()
- {
- RequestContentType = "application/x-www-form-urlencoded";
- MarketSymbolSeparator = "_";
- MarketSymbolIsReversed = true;
- WebSocketOrderBookType = WebSocketOrderBookType.DeltasOnly;
- }
-
- ///
- /// Number of fields Poloniex provides for withdrawals since specifying
- /// extra content in the API request won't be rejected and may cause withdraweal to get stuck.
- ///
- public static IReadOnlyDictionary WithdrawalFieldCount { get; set; }
-
- private async Task MakePrivateAPIRequestAsync(string command, IReadOnlyList parameters = null)
- {
- Dictionary payload = await GetNoncePayloadAsync();
- payload["command"] = command;
- if (parameters != null && parameters.Count % 2 == 0)
- {
- for (int i = 0; i < parameters.Count;)
- {
- payload[parameters[i++].ToStringInvariant()] = parameters[i++];
- }
- }
-
- return await MakeJsonRequestAsync("/tradingApi", null, payload);
- }
+ {
+ RequestContentType = "application/json";
+ MarketSymbolSeparator = "_";
+ WebSocketOrderBookType = WebSocketOrderBookType.DeltasOnly;
+ }
+
+ ///
+ /// Number of fields Poloniex provides for withdrawals since specifying
+ /// extra content in the API request won't be rejected and may cause withdraweal to get stuck.
+ ///
+ public static IReadOnlyDictionary WithdrawalFieldCount { get; set; }
+
+ private async Task MakePrivateAPIRequestAsync(string command, IReadOnlyList parameters = null)
+ {
+ Dictionary payload = await GetNoncePayloadAsync();
+ payload["command"] = command;
+ if (parameters != null && parameters.Count % 2 == 0)
+ {
+ for (int i = 0; i < parameters.Count;)
+ {
+ payload[parameters[i++].ToStringInvariant()] = parameters[i++];
+ }
+ }
+
+ return await MakeJsonRequestAsync("/tradingApi", null, payload);
+ }
///
protected override Task OnInitializeAsync()
@@ -62,7 +60,9 @@ protected override Task OnInitializeAsync()
// load withdrawal field counts
var fieldCount = new Dictionary(StringComparer.OrdinalIgnoreCase);
- using var resourceStream = typeof(ExchangePoloniexAPI).Assembly.GetManifestResourceStream("ExchangeSharp.Properties.Resources.PoloniexWithdrawalFields.csv");
+ using var resourceStream =
+ typeof(ExchangePoloniexAPI).Assembly.GetManifestResourceStream(
+ "ExchangeSharp.Properties.Resources.PoloniexWithdrawalFields.csv");
Debug.Assert(resourceStream != null, nameof(resourceStream) + " != null");
using var sr = new StreamReader(resourceStream);
@@ -109,33 +109,28 @@ public ExchangeOrderResult ParsePlacedOrder(JToken result, ExchangeOrderRequest
return order;
}
- ///
- /// Parses an order which has not been filled.
- ///
- /// The JToken to parse.
- /// Market symbol or null if it's in the result
- /// ExchangeOrderResult with the open order and how much is remaining to fill
- public ExchangeOrderResult ParseOpenOrder(JToken result, string marketSymbol = null)
- {
- ExchangeOrderResult order = new ExchangeOrderResult
- {
- Amount = result["startingAmount"].ConvertInvariant(),
- IsBuy = result["type"].ToStringLowerInvariant() != "sell",
- OrderDate = result["date"].ToDateTimeInvariant(),
- OrderId = result["orderNumber"].ToStringInvariant(),
- Price = result["rate"].ConvertInvariant(),
- Result = ExchangeAPIOrderResult.Open,
- MarketSymbol = (marketSymbol ?? result.Parent.Path)
- };
-
- decimal amount = result["amount"].ConvertInvariant();
- order.AmountFilled = amount - order.Amount;
-
- // fee is a percentage taken from the traded amount rounded to 8 decimals
- order.Fees = CalculateFees(amount, order.Price.Value, order.IsBuy, result["fee"].ConvertInvariant());
-
- return order;
- }
+ public static ExchangeOrderResult ParseOrder(JToken result, string marketSymbol = null)
+ {
+ var order = new ExchangeOrderResult
+ {
+ Amount = result["quantity"].ConvertInvariant(),
+ AmountFilled = result["filledQuantity"].ConvertInvariant(),
+ IsBuy = result["side"].ToStringLowerInvariant() != "sell",
+ OrderDate = result["createTime"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(),
+ OrderId = result["id"].ToStringInvariant(),
+ Price = result["price"].ConvertInvariant(),
+ AveragePrice = result["avgPrice"].ConvertInvariant(),
+ Result = ParseOrderStatus(result["state"].ToStringInvariant()),
+ MarketSymbol = result["symbol"].ToStringInvariant(),
+ ClientOrderId = result["clientOrderId"].ToStringInvariant()
+ };
+
+ // fee is a percentage taken from the traded amount rounded to 8 decimals
+ order.Fees = CalculateFees(order.Amount, order.Price.Value, order.IsBuy,
+ result["fee"].ConvertInvariant());
+
+ return order;
+ }
public void ParseOrderTrades(IEnumerable trades, ExchangeOrderResult order)
{
@@ -150,14 +145,17 @@ public void ParseOrderTrades(IEnumerable trades, ExchangeOrderResult ord
{
parsedSymbol = trade.Parent.Path;
}
+
if (order.MarketSymbol == "all" || !string.IsNullOrWhiteSpace(parsedSymbol))
{
order.MarketSymbol = parsedSymbol;
}
+
if (!string.IsNullOrWhiteSpace(order.MarketSymbol))
{
order.FeesCurrency = ParseFeesCurrency(order.IsBuy, order.MarketSymbol);
}
+
orderMetadataSet = true;
}
@@ -194,160 +192,162 @@ public void ParseOrderTrades(IEnumerable trades, ExchangeOrderResult ord
{
order.Result = ExchangeAPIOrderResult.FilledPartially;
}
+
// Poloniex does not provide a way to get the original price
order.AveragePrice = order.AveragePrice?.Normalize();
order.Price = order.AveragePrice;
}
- public void ParseClosePositionTrades(IEnumerable trades, ExchangeCloseMarginPositionResult closePosition)
- {
- bool closePositionMetadataSet = false;
- var tradeIds = new List();
- foreach (JToken trade in trades)
- {
- if (!closePositionMetadataSet)
- {
- closePosition.IsBuy = trade["type"].ToStringLowerInvariant() != "sell";
-
- if (!string.IsNullOrWhiteSpace(closePosition.MarketSymbol))
- {
- closePosition.FeesCurrency = ParseFeesCurrency(closePosition.IsBuy, closePosition.MarketSymbol);
- }
-
- closePositionMetadataSet = true;
- }
-
- decimal tradeAmt = trade["amount"].ConvertInvariant();
- decimal tradeRate = trade["rate"].ConvertInvariant();
-
- closePosition.AveragePrice = (closePosition.AveragePrice * closePosition.AmountFilled + tradeAmt * tradeRate) / (closePosition.AmountFilled + tradeAmt);
- closePosition.AmountFilled += tradeAmt;
-
- tradeIds.Add(trade["tradeID"].ToStringInvariant());
-
- if (closePosition.CloseDate == DateTime.MinValue)
- {
- closePosition.CloseDate = trade["date"].ToDateTimeInvariant();
- }
-
- // fee is a percentage taken from the traded amount rounded to 8 decimals
- closePosition.Fees += CalculateFees(tradeAmt, tradeRate, closePosition.IsBuy, trade["fee"].ConvertInvariant());
- }
-
- closePosition.TradeIds = tradeIds.ToArray();
- }
-
- private static decimal CalculateFees(decimal tradeAmt, decimal tradeRate, bool isBuy, decimal fee)
- {
- decimal amount = isBuy ? tradeAmt * fee : tradeAmt * tradeRate * fee;
- return Math.Round(amount, 8, MidpointRounding.AwayFromZero);
- }
-
- private void ParseCompletedOrderDetails(List orders, JToken trades, string marketSymbol)
- {
- IEnumerable orderNumsInTrades = trades.Select(x => x["orderNumber"].ToStringInvariant()).Distinct();
- foreach (string orderNum in orderNumsInTrades)
- {
- IEnumerable tradesForOrder = trades.Where(x => x["orderNumber"].ToStringInvariant() == orderNum);
- ExchangeOrderResult order = new ExchangeOrderResult { OrderId = orderNum, MarketSymbol = marketSymbol };
- ParseOrderTrades(tradesForOrder, order);
- //order.Price = order.AveragePrice;
- order.Result = ExchangeAPIOrderResult.Filled;
- orders.Add(order);
- }
- }
-
- private async Task ParseTickerWebSocketAsync(string symbol, JToken token)
- {
- /*
- last: args[1],
- lowestAsk: args[2],
- highestBid: args[3],
- percentChange: args[4],
- baseVolume: args[5],
- quoteVolume: args[6],
- isFrozen: args[7],
- high24hr: args[8],
- low24hr: args[9]
- */
- return await this.ParseTickerAsync(token, symbol, 2, 3, 1, 5, 6);
- }
-
- public override string PeriodSecondsToString(int seconds)
- {
- var allowedPeriods = new[]
- {
- "MINUTE_1", "MINUTE_5", "MINUTE_10", "MINUTE_15",
- "MINUTE_30", "HOUR_1", "HOUR_2", "HOUR_4", "HOUR_6",
- "HOUR_12", "DAY_1", "DAY_3", "WEEK_1", "MONTH_1"
- };
- var period = CryptoUtility.SecondsToPeriodStringLongReverse(seconds);
- var periodIsvalid = allowedPeriods.Any(x => x == period);
- if (!periodIsvalid) throw new ArgumentOutOfRangeException(nameof(period), $"{period} is not valid period on Poloniex");
-
- return period;
- }
-
- protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload)
- {
- if (CanMakeAuthenticatedRequest(payload))
- {
- payload["signTimestamp"] = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds();
- var form = payload.GetFormForPayload();
- var sig = $"{request.Method}\n" +
- $"{request.RequestUri.PathAndQuery}\n" +
- $"{HttpUtility.UrlEncode(form)}";
- request.AddHeader("key", PublicApiKey.ToUnsecureString());
- request.AddHeader("signature", CryptoUtility.SHA256Sign(sig, PrivateApiKey.ToUnsecureString()));
- request.AddHeader("signTimestamp", payload["signTimestamp"].ToStringInvariant());
- await request.WriteToRequestAsync(form);
- }
- }
-
- protected override async Task> OnGetCurrenciesAsync()
- {
- /*
- * {"1CR":{"id":1,"name":"1CRedit","txFee":"0.01000000","minConf":3,"depositAddress":null,"disabled":0,"delisted":1,"frozen":0},
- * "XC":{"id":230,"name":"XCurrency","txFee":"0.01000000","minConf":12,"depositAddress":null,"disabled":1,"delisted":1,"frozen":0},
- * ... }
- */
- var currencies = new Dictionary();
- Dictionary currencyMap = await MakeJsonRequestAsync>("/public?command=returnCurrencies");
- foreach (var kvp in currencyMap)
- {
- var currency = new ExchangeCurrency
- {
- BaseAddress = kvp.Value["depositAddress"].ToStringInvariant(),
- FullName = kvp.Value["name"].ToStringInvariant(),
- DepositEnabled = true,
- WithdrawalEnabled = true,
- MinConfirmations = kvp.Value["minConf"].ConvertInvariant(),
- Name = kvp.Key,
- TxFee = kvp.Value["txFee"].ConvertInvariant(),
- };
-
- string disabled = kvp.Value["disabled"].ToStringInvariant();
- string delisted = kvp.Value["delisted"].ToStringInvariant();
- string frozen = kvp.Value["frozen"].ToStringInvariant();
- if (string.Equals(disabled, "1") || string.Equals(delisted, "1") || string.Equals(frozen, "1"))
- {
- currency.DepositEnabled = false;
- currency.WithdrawalEnabled = false;
- }
-
- currencies[currency.Name] = currency;
- }
-
- return currencies;
- }
-
- protected override async Task> OnGetMarketSymbolsAsync()
- {
- return (await GetMarketSymbolsMetadataAsync()).Where(x => x.IsActive.Value).Select(x => x.MarketSymbol);
- }
-
- protected internal override async Task> OnGetMarketSymbolsMetadataAsync()
- {
+ private static decimal CalculateFees(decimal tradeAmt, decimal tradeRate, bool isBuy, decimal fee)
+ {
+ decimal amount = isBuy ? tradeAmt * fee : tradeAmt * tradeRate * fee;
+ return Math.Round(amount, 8, MidpointRounding.AwayFromZero);
+ }
+
+ private static IEnumerable ParseCompletedOrderDetails(JToken tradeHistory)
+ => tradeHistory
+ .Select(o => new ExchangeOrderResult
+ {
+ OrderId = o["orderId"].ToStringInvariant(),
+ MarketSymbol = o["symbol"].ToStringInvariant(),
+ Amount = o["quantity"].ConvertInvariant(),
+ IsBuy = o["side"].ToStringLowerInvariant() != "sell",
+ OrderDate = o["createTime"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(),
+ Price = o["price"].ConvertInvariant(),
+ Result = ExchangeAPIOrderResult.Filled,
+ ClientOrderId = o["clientOrderId"].ToStringInvariant(),
+ Fees = o["feeAmount"].ConvertInvariant(),
+ FeesCurrency = o["feeCurrency"].ToStringInvariant()
+ });
+
+ private async Task ParseTickerWebSocketAsync(string symbol, JToken token)
+ {
+ /*
+ last: args[1],
+ lowestAsk: args[2],
+ highestBid: args[3],
+ percentChange: args[4],
+ baseVolume: args[5],
+ quoteVolume: args[6],
+ isFrozen: args[7],
+ high24hr: args[8],
+ low24hr: args[9]
+ */
+ return await this.ParseTickerAsync(token, symbol, 2, 3, 1, 5, 6);
+ }
+
+ public override string PeriodSecondsToString(int seconds)
+ {
+ var allowedPeriods = new[]
+ {
+ "MINUTE_1", "MINUTE_5", "MINUTE_10", "MINUTE_15",
+ "MINUTE_30", "HOUR_1", "HOUR_2", "HOUR_4", "HOUR_6",
+ "HOUR_12", "DAY_1", "DAY_3", "WEEK_1", "MONTH_1"
+ };
+ var period = CryptoUtility.SecondsToPeriodStringLongReverse(seconds);
+ var periodIsvalid = allowedPeriods.Any(x => x == period);
+ if (!periodIsvalid)
+ throw new ArgumentOutOfRangeException(nameof(period), $"{period} is not valid period on Poloniex");
+
+ return period;
+ }
+
+ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload)
+ {
+ if (CanMakeAuthenticatedRequest(payload))
+ {
+ payload.Remove("nonce");
+ var timestamp = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds();
+ var sig = string.Empty;
+ switch (request.Method)
+ {
+ case "GET":
+ case "DELETE":
+ {
+ payload["signTimestamp"] = timestamp;
+ var form = payload.GetFormForPayload();
+ sig = $"{request.Method}\n" +
+ $"{request.RequestUri.PathAndQuery}\n" +
+ $"{form}";
+
+ await request.WriteToRequestAsync(form);
+ break;
+ }
+ case "POST":
+ {
+ var pl = $"requestBody={payload.GetJsonForPayload()}&signTimestamp={timestamp}";
+ sig = $"{request.Method}\n" +
+ $"{request.RequestUri.PathAndQuery}\n" +
+ $"{pl}";
+ await request.WritePayloadJsonToRequestAsync(payload);
+ break;
+ }
+ }
+
+ request.AddHeader("key", PublicApiKey.ToUnsecureString());
+ request.AddHeader("signature",
+ CryptoUtility.SHA256SignBase64(sig, PrivateApiKey.ToUnsecureBytesUTF8()));
+ request.AddHeader("signTimestamp", timestamp.ToStringInvariant());
+ }
+ }
+
+ protected override async Task> OnGetCurrenciesAsync()
+ {
+ //https://api.poloniex.com/v2/currencies
+ // [
+ // {
+ // "id": 1,
+ // "coin": "1CR",
+ // "delisted": true,
+ // "tradeEnable": false,
+ // "name": "1CRedit",
+ // "networkList": [
+ // {
+ // "id": 1,
+ // "coin": "1CR",
+ // "name": "1CRedit",
+ // "currencyType": "address",
+ // "blockchain": "1CR",
+ // "withdrawalEnable": false,
+ // "depositEnable": false,
+ // "depositAddress": null,
+ // "withdrawMin": null,
+ // "decimals": 8,
+ // "withdrawFee": "0.01000000",
+ // "minConfirm": 10000
+ // }
+ // ]
+ // }
+ // ]
+
+ var currencies = new Dictionary();
+ var result = await MakeJsonRequestAsync("/v2/currencies");
+ foreach (var c in result)
+ {
+ var currency = new ExchangeCurrency
+ {
+ Name = c["coin"].ToStringInvariant(),
+ FullName = c["name"].ToStringInvariant(),
+ TxFee = c["networkList"][0]["withdrawFee"].ConvertInvariant(),
+ DepositEnabled = c["networkList"][0]["depositEnable"].ConvertInvariant(),
+ WithdrawalEnabled = c["networkList"][0]["withdrawalEnable"].ConvertInvariant(),
+ MinWithdrawalSize = (c["networkList"][0]["withdrawMin"] ?? 0).ConvertInvariant(),
+ BaseAddress = (c["networkList"][0]["depositAddress"] ?? string.Empty).ToStringInvariant(),
+ MinConfirmations = (c["networkList"][0]["minConfirm"] ?? 0).ConvertInvariant()
+ };
+ currencies[currency.Name] = currency;
+ }
+
+ return currencies;
+ }
+
+ protected override async Task> OnGetMarketSymbolsAsync()
+ {
+ return (await GetMarketSymbolsMetadataAsync()).Where(x => x.IsActive.Value).Select(x => x.MarketSymbol);
+ }
+
+ protected internal override async Task> OnGetMarketSymbolsMetadataAsync()
+ {
//https://api.poloniex.com/markets
// [
// {
@@ -374,109 +374,116 @@ protected internal override async Task> OnGetMarketS
// }
// ]
- var markets = new List();
- var symbols = await MakeJsonRequestAsync("/markets");
-
- foreach (var symbol in symbols)
- {
- var market = new ExchangeMarket
- {
- MarketSymbol = symbol["symbol"].ToStringInvariant(),
- IsActive = ParsePairState(symbol["state"].ToStringInvariant()),
- BaseCurrency = symbol["baseCurrencyName"].ToStringInvariant(),
- QuoteCurrency = symbol["quoteCurrencyName"].ToStringInvariant(),
- MinTradeSize = symbol["symbolTradeLimit"]["minQuantity"].Value(),
- MinTradeSizeInQuoteCurrency = symbol["symbolTradeLimit"]["minAmount"].Value(),
- PriceStepSize = CryptoUtility.PrecisionToStepSize(symbol["symbolTradeLimit"]["priceScale"].Value()),
- QuantityStepSize = CryptoUtility.PrecisionToStepSize(symbol["symbolTradeLimit"]["quantityScale"].Value()),
- MarginEnabled = symbol["crossMargin"]["supportCrossMargin"].Value()
- };
- markets.Add(market);
- }
-
- return markets;
- }
-
- protected override async Task OnGetTickerAsync(string marketSymbol)
- {
- IEnumerable> tickers = await GetTickersAsync();
- foreach (var kv in tickers)
- {
- if (kv.Key == marketSymbol)
- {
- return kv.Value;
- }
- }
- return null;
- }
-
- protected override async Task>> OnGetTickersAsync()
- {
- //https://api.poloniex.com/markets/ticker24h
- // [ {
- // "symbol" : "BTS_BTC",
- // "open" : "0.0000005026",
- // "low" : "0.0000004851",
- // "high" : "0.0000005799",
- // "close" : "0.0000004851",
- // "quantity" : "34444",
- // "amount" : "0.0179936481",
- // "tradeCount" : 48,
- // "startTime" : 1676918100000,
- // "closeTime" : 1677004501011,
- // "displayName" : "BTS/BTC",
- // "dailyChange" : "-0.0348",
- // "bid" : "0.0000004852",
- // "bidQuantity" : "725",
- // "ask" : "0.0000004962",
- // "askQuantity" : "238",
- // "ts" : 1677004503839,
- // "markPrice" : "0.000000501"
- // }]
- var tickers = new List>();
- var tickerResponse = await MakeJsonRequestAsync("/markets/ticker24h");
- foreach (var instrument in tickerResponse)
- {
- var symbol = instrument["symbol"].ToStringInvariant();
- var ticker = await this.ParseTickerAsync(
- instrument, symbol, askKey: "ask", bidKey: "bid", baseVolumeKey: "quantity", lastKey: "close",
- quoteVolumeKey: "amount", timestampKey: "ts", timestampType: TimestampType.UnixMilliseconds);
- tickers.Add(new KeyValuePair(symbol, ticker));
- }
-
- return tickers;
- }
-
- protected override async Task OnGetTickersWebSocketAsync(Action>> callback, params string[] symbols)
- {
- Dictionary idsToSymbols = new Dictionary();
- return await ConnectPublicWebSocketAsync(string.Empty, async (_socket, msg) =>
- {
- JToken token = JToken.Parse(msg.ToStringFromUTF8());
- if (token[0].ConvertInvariant() == 1002)
- {
- if (token is JArray outerArray && outerArray.Count > 2 && outerArray[2] is JArray array && array.Count > 9 &&
- idsToSymbols.TryGetValue(array[0].ToStringInvariant(), out string symbol))
- {
- callback.Invoke(new List>
- {
- new KeyValuePair(symbol, await ParseTickerWebSocketAsync(symbol, array))
- });
- }
- }
- }, async (_socket) =>
- {
- var tickers = await GetTickersAsync();
- foreach (var ticker in tickers)
- {
- idsToSymbols[ticker.Value.Id] = ticker.Key;
- }
- // subscribe to ticker channel (1002)
- await _socket.SendMessageAsync(new { command = "subscribe", channel = 1002 });
- });
- }
-
- protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols)
+ var symbols = await MakeJsonRequestAsync("/markets");
+
+ return symbols.Select(symbol => new ExchangeMarket
+ {
+ MarketSymbol = symbol["symbol"].ToStringInvariant(),
+ IsActive = ParsePairState(symbol["state"].ToStringInvariant()),
+ BaseCurrency = symbol["baseCurrencyName"].ToStringInvariant(),
+ QuoteCurrency = symbol["quoteCurrencyName"].ToStringInvariant(),
+ MinTradeSize = symbol["symbolTradeLimit"]["minQuantity"].Value(),
+ MaxTradeSize = decimal.MaxValue,
+ MinTradeSizeInQuoteCurrency = symbol["symbolTradeLimit"]["minAmount"].Value(),
+ MinPrice = CryptoUtility.PrecisionToStepSize(symbol["symbolTradeLimit"]["priceScale"]
+ .Value()),
+ MaxPrice = decimal.MaxValue,
+ PriceStepSize =
+ CryptoUtility.PrecisionToStepSize(symbol["symbolTradeLimit"]["priceScale"].Value()),
+ QuantityStepSize =
+ CryptoUtility.PrecisionToStepSize(symbol["symbolTradeLimit"]["quantityScale"].Value()),
+ MarginEnabled = symbol["crossMargin"]["supportCrossMargin"].Value()
+ });
+ }
+
+ protected override async Task OnGetTickerAsync(string marketSymbol)
+ {
+ var tickers = await GetTickersAsync();
+ foreach (var kv in tickers)
+ {
+ if (kv.Key == marketSymbol)
+ {
+ return kv.Value;
+ }
+ }
+
+ return null;
+ }
+
+ protected override async Task>> OnGetTickersAsync()
+ {
+ //https://api.poloniex.com/markets/ticker24h
+ // [ {
+ // "symbol" : "BTS_BTC",
+ // "open" : "0.0000005026",
+ // "low" : "0.0000004851",
+ // "high" : "0.0000005799",
+ // "close" : "0.0000004851",
+ // "quantity" : "34444",
+ // "amount" : "0.0179936481",
+ // "tradeCount" : 48,
+ // "startTime" : 1676918100000,
+ // "closeTime" : 1677004501011,
+ // "displayName" : "BTS/BTC",
+ // "dailyChange" : "-0.0348",
+ // "bid" : "0.0000004852",
+ // "bidQuantity" : "725",
+ // "ask" : "0.0000004962",
+ // "askQuantity" : "238",
+ // "ts" : 1677004503839,
+ // "markPrice" : "0.000000501"
+ // }]
+ var tickers = new List>();
+ var tickerResponse = await MakeJsonRequestAsync("/markets/ticker24h");
+ foreach (var instrument in tickerResponse)
+ {
+ var symbol = instrument["symbol"].ToStringInvariant();
+ var ticker = await this.ParseTickerAsync(
+ instrument, symbol, askKey: "ask", bidKey: "bid", baseVolumeKey: "quantity", lastKey: "close",
+ quoteVolumeKey: "amount", timestampKey: "ts", timestampType: TimestampType.UnixMilliseconds);
+ tickers.Add(new KeyValuePair(symbol, ticker));
+ }
+
+ return tickers;
+ }
+
+ protected override async Task OnGetTickersWebSocketAsync(
+ Action>> callback,
+ params string[] symbols)
+ {
+ Dictionary idsToSymbols = new Dictionary();
+ return await ConnectPublicWebSocketAsync(string.Empty, async (_socket, msg) =>
+ {
+ JToken token = JToken.Parse(msg.ToStringFromUTF8());
+ if (token[0].ConvertInvariant() == 1002)
+ {
+ if (token is JArray outerArray && outerArray.Count > 2 && outerArray[2] is JArray array &&
+ array.Count > 9 &&
+ idsToSymbols.TryGetValue(array[0].ToStringInvariant(), out string symbol))
+ {
+ callback.Invoke(new List>
+ {
+ new KeyValuePair(symbol,
+ await ParseTickerWebSocketAsync(symbol, array))
+ });
+ }
+ }
+ }, async (_socket) =>
+ {
+ var tickers = await GetTickersAsync();
+ foreach (var ticker in tickers)
+ {
+ idsToSymbols[ticker.Value.Id] = ticker.Key;
+ }
+
+ // subscribe to ticker channel (1002)
+ await _socket.SendMessageAsync(new { command = "subscribe", channel = 1002 });
+ });
+ }
+
+ protected override async Task OnGetTradesWebSocketAsync(
+ Func, Task> callback,
+ params string[] marketSymbols)
{
Dictionary messageIdToSymbol = new Dictionary();
Dictionary symbolToMessageId = new Dictionary();
@@ -486,6 +493,7 @@ protected override async Task OnGetTradesWebSocketAsync(Func
{
JToken token = JToken.Parse(msg.ToStringFromUTF8());
@@ -495,7 +503,7 @@ protected override async Task OnGetTradesWebSocketAsync(Func OnGetTradesWebSocketAsync(Func", <1 for buy 0 for sell>, "", "", , ""]
- ExchangeTrade trade = data.ParseTrade(amountKey: 4, priceKey: 3, typeKey: 2, timestampKey: 6,
+ ExchangeTrade trade = data.ParseTrade(amountKey: 4, priceKey: 3, typeKey: 2,
+ timestampKey: 6,
timestampType: TimestampType.UnixMilliseconds, idKey: 1, typeKeyIsBuyValue: "1");
await callback(new KeyValuePair(symbol, trade));
}
@@ -539,6 +549,7 @@ protected override async Task OnGetTradesWebSocketAsync(Func symbolToMessageId[s]);
}
+
// subscribe to order book and trades channel for each symbol
foreach (var id in marketIDs)
{
@@ -547,108 +558,120 @@ protected override async Task OnGetTradesWebSocketAsync(Func OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 20, params string[] marketSymbols)
- {
- Dictionary> messageIdToSymbol = new Dictionary>();
- return await ConnectPublicWebSocketAsync(string.Empty, (_socket, msg) =>
- {
- JToken token = JToken.Parse(msg.ToStringFromUTF8());
- int msgId = token[0].ConvertInvariant();
-
- //return if this is a heartbeat message
- if (msgId == 1010)
- {
- return Task.CompletedTask;
- }
-
- var seq = token[1].ConvertInvariant();
- var dataArray = token[2];
- ExchangeOrderBook book = new ExchangeOrderBook();
- foreach (var data in dataArray)
- {
- var dataType = data[0].ToStringInvariant();
- if (dataType == "i")
- {
- var marketInfo = data[1];
- var market = marketInfo["currencyPair"].ToStringInvariant();
- messageIdToSymbol[msgId] = new Tuple(market, 0);
-
- // we are only returning the deltas, this would create a full order book which we don't want, but keeping it
- // here for historical reference
- /*
- foreach (JProperty jprop in marketInfo["orderBook"][0].Cast())
- {
- var depth = new ExchangeOrderPrice
- {
- Price = jprop.Name.ConvertInvariant(),
- Amount = jprop.Value.ConvertInvariant()
- };
- book.Asks[depth.Price] = depth;
- }
- foreach (JProperty jprop in marketInfo["orderBook"][1].Cast())
- {
- var depth = new ExchangeOrderPrice
- {
- Price = jprop.Name.ConvertInvariant(),
- Amount = jprop.Value.ConvertInvariant()
- };
- book.Bids[depth.Price] = depth;
- }
- */
- }
- else if (dataType == "o")
- {
- //removes or modifies an existing item on the order books
- if (messageIdToSymbol.TryGetValue(msgId, out Tuple symbol))
- {
- int type = data[1].ConvertInvariant();
- var depth = new ExchangeOrderPrice { Price = data[2].ConvertInvariant(), Amount = data[3].ConvertInvariant() };
- var list = (type == 1 ? book.Bids : book.Asks);
- list[depth.Price] = depth;
- book.MarketSymbol = symbol.Item1;
- book.SequenceId = symbol.Item2 + 1;
- messageIdToSymbol[msgId] = new Tuple(book.MarketSymbol, book.SequenceId);
- }
- }
- else
- {
- continue;
- }
- }
- if (book != null && (book.Asks.Count != 0 || book.Bids.Count != 0))
- {
- callback(book);
- }
- return Task.CompletedTask;
- }, async (_socket) =>
- {
- if (marketSymbols == null || marketSymbols.Length == 0)
- {
- marketSymbols = (await GetMarketSymbolsAsync()).ToArray();
- }
- // subscribe to order book and trades channel for each symbol
- foreach (var sym in marketSymbols)
- {
- await _socket.SendMessageAsync(new { command = "subscribe", channel = NormalizeMarketSymbol(sym) });
- }
- });
- }
-
- protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100)
- {
- //https://api.poloniex.com/markets/{symbol}/orderBook?scale={scale}&limit={limit}
- // {
- // "time" : 1677005825632,
- // "scale" : "0.01",
- // "asks" : [ "24702.89", "0.046082", "24702.90", "0.001681", "24703.09", "0.002037", "24710.10", "0.143572", "24712.18", "0.00118", "24713.68", "0.606951", "24724.80", "0.133", "24728.93", "0.7", "24728.94", "0.4", "24737.10", "0.135203" ],
- // "bids" : [ "24700.03", "1.006472", "24700.02", "0.001208", "24698.71", "0.607319", "24697.99", "0.001973", "24688.50", "0.133", "24679.41", "0.4", "24679.40", "0.135", "24678.55", "0.3", "24667.00", "0.262", "24661.39", "0.14" ],
- // "ts" : 1677005825637
- // }
- var response = await MakeJsonRequestAsync($"/markets/{marketSymbol}/orderBook?limit={maxCount}");
- return response.ParseOrderBookFromJTokenArray();
- }
-
- protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null)
+ protected override async Task OnGetDeltaOrderBookWebSocketAsync(
+ Action callback,
+ int maxCount = 20,
+ params string[] marketSymbols)
+ {
+ Dictionary> messageIdToSymbol = new Dictionary>();
+ return await ConnectPublicWebSocketAsync(string.Empty, (_socket, msg) =>
+ {
+ JToken token = JToken.Parse(msg.ToStringFromUTF8());
+ int msgId = token[0].ConvertInvariant();
+
+ //return if this is a heartbeat message
+ if (msgId == 1010)
+ {
+ return Task.CompletedTask;
+ }
+
+ var seq = token[1].ConvertInvariant();
+ var dataArray = token[2];
+ ExchangeOrderBook book = new ExchangeOrderBook();
+ foreach (var data in dataArray)
+ {
+ var dataType = data[0].ToStringInvariant();
+ if (dataType == "i")
+ {
+ var marketInfo = data[1];
+ var market = marketInfo["currencyPair"].ToStringInvariant();
+ messageIdToSymbol[msgId] = new Tuple(market, 0);
+
+ // we are only returning the deltas, this would create a full order book which we don't want, but keeping it
+ // here for historical reference
+ /*
+ foreach (JProperty jprop in marketInfo["orderBook"][0].Cast())
+ {
+ var depth = new ExchangeOrderPrice
+ {
+ Price = jprop.Name.ConvertInvariant(),
+ Amount = jprop.Value.ConvertInvariant()
+ };
+ book.Asks[depth.Price] = depth;
+ }
+ foreach (JProperty jprop in marketInfo["orderBook"][1].Cast())
+ {
+ var depth = new ExchangeOrderPrice
+ {
+ Price = jprop.Name.ConvertInvariant(),
+ Amount = jprop.Value.ConvertInvariant()
+ };
+ book.Bids[depth.Price] = depth;
+ }
+ */
+ }
+ else if (dataType == "o")
+ {
+ //removes or modifies an existing item on the order books
+ if (messageIdToSymbol.TryGetValue(msgId, out Tuple symbol))
+ {
+ int type = data[1].ConvertInvariant();
+ var depth = new ExchangeOrderPrice
+ {
+ Price = data[2].ConvertInvariant(),
+ Amount = data[3].ConvertInvariant()
+ };
+ var list = (type == 1 ? book.Bids : book.Asks);
+ list[depth.Price] = depth;
+ book.MarketSymbol = symbol.Item1;
+ book.SequenceId = symbol.Item2 + 1;
+ messageIdToSymbol[msgId] = new Tuple(book.MarketSymbol, book.SequenceId);
+ }
+ }
+ else
+ {
+ continue;
+ }
+ }
+
+ if (book != null && (book.Asks.Count != 0 || book.Bids.Count != 0))
+ {
+ callback(book);
+ }
+
+ return Task.CompletedTask;
+ }, async (_socket) =>
+ {
+ if (marketSymbols == null || marketSymbols.Length == 0)
+ {
+ marketSymbols = (await GetMarketSymbolsAsync()).ToArray();
+ }
+
+ // subscribe to order book and trades channel for each symbol
+ foreach (var sym in marketSymbols)
+ {
+ await _socket.SendMessageAsync(new { command = "subscribe", channel = NormalizeMarketSymbol(sym) });
+ }
+ });
+ }
+
+ protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100)
+ {
+ //https://api.poloniex.com/markets/{symbol}/orderBook?scale={scale}&limit={limit}
+ // {
+ // "time" : 1677005825632,
+ // "scale" : "0.01",
+ // "asks" : [ "24702.89", "0.046082", "24702.90", "0.001681", "24703.09", "0.002037", "24710.10", "0.143572", "24712.18", "0.00118", "24713.68", "0.606951", "24724.80", "0.133", "24728.93", "0.7", "24728.94", "0.4", "24737.10", "0.135203" ],
+ // "bids" : [ "24700.03", "1.006472", "24700.02", "0.001208", "24698.71", "0.607319", "24697.99", "0.001973", "24688.50", "0.133", "24679.41", "0.4", "24679.40", "0.135", "24678.55", "0.3", "24667.00", "0.262", "24661.39", "0.14" ],
+ // "ts" : 1677005825637
+ // }
+ var response = await MakeJsonRequestAsync($"/markets/{marketSymbol}/orderBook?limit={maxCount}");
+ return response.ParseOrderBookFromJTokenArray();
+ }
+
+ protected override async Task> OnGetRecentTradesAsync(
+ string marketSymbol,
+ int? limit = null)
{
//https://api.poloniex.com/markets/{symbol}/trades?limit={limit}
// Returns a list of recent trades, request param limit is optional, its default value is 500, and max value is 1000.
@@ -678,284 +701,230 @@ protected override async Task> OnGetRecentTradesAsync
return trades;
}
- protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null)
- {
- //https://api.poloniex.com/markets/{symbol}/candles?interval={interval}&limit={limit}&startTime={startTime}&endTime={endTime}
- // [
- // [
- // "45218",
- // "47590.82",
- // "47009.11",
- // "45516.6",
- // "13337805.8",
- // "286.639111",
- // "0",
- // "0",
- // 0,
- // 0,
- // "46531.7",
- // "DAY_1",
- // 1648684800000,
- // 1648771199999
- // ]
- // ]
- limit = (limit == null || limit < 1 || limit > 500) ? 500 : limit;
- var period = PeriodSecondsToString(periodSeconds);
- var url = $"/markets/{marketSymbol}/candles?interval={period}&limit={limit}";
- if (startDate != null)
- {
- url = $"{url}&startTime={new DateTimeOffset(startDate.Value).ToUnixTimeMilliseconds()}";
- }
- if (endDate != null)
- {
- url = $"{url}&endTime={new DateTimeOffset(endDate.Value).ToUnixTimeMilliseconds()}";
- }
-
- var candleResponse = await MakeJsonRequestAsync(url);
- return candleResponse
- .Select(cr => this.ParseCandle(
- cr, marketSymbol, periodSeconds, 2, 1, 0, 3, 12, TimestampType.UnixMilliseconds,
- 5, 4, 10))
- .ToList();
- }
-
- protected override async Task> OnGetAmountsAsync()
- {
- // Dictionary payload = await GetNoncePayloadAsync();
- Dictionary payload = new Dictionary();
- var response = await MakeJsonRequestAsync("/accounts/balances", payload: payload);
- return null;
- }
-
- protected override async Task> OnGetAmountsAvailableToTradeAsync()
- {
- Dictionary amounts = new Dictionary(StringComparer.OrdinalIgnoreCase);
- JToken result = await MakePrivateAPIRequestAsync("returnBalances");
- foreach (JProperty child in result.Children())
- {
- decimal amount = child.Value.ConvertInvariant();
- if (amount > 0m)
- {
- amounts[child.Name] = amount;
- }
- }
- return amounts;
- }
-
- protected override async Task> OnGetMarginAmountsAvailableToTradeAsync(bool includeZeroBalances)
- {
- Dictionary amounts = new Dictionary(StringComparer.OrdinalIgnoreCase);
- var accountArgumentName = "account";
- var accountArgumentValue = "margin";
- JToken result = await MakePrivateAPIRequestAsync("returnAvailableAccountBalances", new object[] { accountArgumentName, accountArgumentValue });
- foreach (JProperty child in result[accountArgumentValue].Children())
- {
- decimal amount = child.Value.ConvertInvariant();
- if (amount > 0m || includeZeroBalances)
- {
- amounts[child.Name] = amount;
- }
- }
- return amounts;
- }
-
- protected override async Task OnGetOpenPositionAsync(string marketSymbol)
- {
- List orderParams = new List
- {
- "currencyPair", marketSymbol
- };
-
- JToken result = await MakePrivateAPIRequestAsync("getMarginPosition", orderParams);
- ExchangeMarginPositionResult marginPositionResult = new ExchangeMarginPositionResult()
- {
- Amount = result["amount"].ConvertInvariant(),
- Total = result["total"].ConvertInvariant(),
- BasePrice = result["basePrice"].ConvertInvariant(),
- LiquidationPrice = result["liquidationPrice"].ConvertInvariant(),
- ProfitLoss = result["pl"].ConvertInvariant(),
- LendingFees = result["lendingFees"].ConvertInvariant(),
- Type = result["type"].ToStringInvariant(),
- MarketSymbol = marketSymbol
- };
- return marginPositionResult;
- }
-
- protected override async Task OnCloseMarginPositionAsync(string marketSymbol)
- {
- List orderParams = new List
- {
- "currencyPair", marketSymbol
- };
-
- JToken result = await MakePrivateAPIRequestAsync("closeMarginPosition", orderParams);
-
- ExchangeCloseMarginPositionResult closePositionResult = new ExchangeCloseMarginPositionResult()
- {
- Success = result["success"].ConvertInvariant(),
- Message = result["message"].ToStringInvariant(),
- MarketSymbol = marketSymbol
- };
-
- JToken symbolTrades = result["resultingTrades"];
- if (symbolTrades == null || !symbolTrades.Any())
- return closePositionResult;
-
- JToken trades = symbolTrades[marketSymbol];
- if (trades != null && trades.Children().Count() != 0)
- {
- ParseClosePositionTrades(trades, closePositionResult);
- }
-
- return closePositionResult;
- }
-
- protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order)
- {
- if (order.OrderType == OrderType.Market)
- {
- throw new NotSupportedException("Order type " + order.OrderType + " not supported");
- }
-
- decimal orderAmount = await ClampOrderQuantity(order.MarketSymbol, order.Amount);
- if (order.Price == null) throw new ArgumentNullException(nameof(order.Price));
- decimal orderPrice = await ClampOrderPrice(order.MarketSymbol, order.Price.Value);
-
- List orderParams = new List
- {
- "currencyPair", order.MarketSymbol,
- "rate", orderPrice.ToStringInvariant(),
- "amount", orderAmount.ToStringInvariant()
- };
- if (order.IsPostOnly != null) { orderParams.Add("postOnly"); orderParams.Add(order.IsPostOnly.Value ? "1" : "0"); } // (optional) Set to "1" if you want this sell order to only be placed if no portion of it fills immediately.
- foreach (KeyValuePair kv in order.ExtraParameters)
- {
- orderParams.Add(kv.Key);
- orderParams.Add(kv.Value);
- }
-
- JToken result = null;
- try
+ protected override async Task> OnGetCandlesAsync(
+ string marketSymbol,
+ int periodSeconds,
+ DateTime? startDate = null,
+ DateTime? endDate = null,
+ int? limit = null)
+ {
+ //https://api.poloniex.com/markets/{symbol}/candles?interval={interval}&limit={limit}&startTime={startTime}&endTime={endTime}
+ // [
+ // [
+ // "45218",
+ // "47590.82",
+ // "47009.11",
+ // "45516.6",
+ // "13337805.8",
+ // "286.639111",
+ // "0",
+ // "0",
+ // 0,
+ // 0,
+ // "46531.7",
+ // "DAY_1",
+ // 1648684800000,
+ // 1648771199999
+ // ]
+ // ]
+ limit = (limit == null || limit < 1 || limit > 500) ? 500 : limit;
+ var period = PeriodSecondsToString(periodSeconds);
+ var url = $"/markets/{marketSymbol}/candles?interval={period}&limit={limit}";
+ if (startDate != null)
{
- result = await MakePrivateAPIRequestAsync(order.IsBuy ? (order.IsMargin ? "marginBuy" : "buy") : (order.IsMargin ? "marginSell" : "sell"), orderParams);
+ url = $"{url}&startTime={new DateTimeOffset(startDate.Value).ToUnixTimeMilliseconds()}";
}
- catch (Exception e)
+
+ if (endDate != null)
{
- if (!e.Message.Contains("Unable to fill order completely"))
+ url = $"{url}&endTime={new DateTimeOffset(endDate.Value).ToUnixTimeMilliseconds()}";
+ }
+
+ var candleResponse = await MakeJsonRequestAsync(url);
+ return candleResponse
+ .Select(cr => this.ParseCandle(
+ cr, marketSymbol, periodSeconds, 2, 1, 0, 3, 12, TimestampType.UnixMilliseconds,
+ 5, 4, 10));
+ }
+
+ protected override async Task> OnGetAmountsAsync()
+ {
+ var response =
+ await MakeJsonRequestAsync("/accounts/balances", payload: await GetNoncePayloadAsync());
+ return (response[0]?["balances"] ?? throw new InvalidOperationException())
+ .Select(x => new
{
- throw;
- }
- result = JToken.FromObject(new { orderNumber = "0", currencyPair = order.MarketSymbol });
+ Currency = x["currency"].Value(),
+ TotalBalance = x["available"].Value() + x["hold"].Value()
+ })
+ .ToDictionary(k => k.Currency, v => v.TotalBalance);
+ }
+
+ protected override async Task> OnGetAmountsAvailableToTradeAsync()
+ {
+ var response =
+ await MakeJsonRequestAsync("/accounts/balances", payload: await GetNoncePayloadAsync());
+ return (response[0]?["balances"] ?? throw new InvalidOperationException())
+ .Select(x => new
+ {
+ Currency = x["currency"].Value(),
+ AvailableBalance = x["available"].Value()
+ })
+ .ToDictionary(k => k.Currency, v => v.AvailableBalance);
+ }
+
+ protected override async Task> OnGetMarginAmountsAvailableToTradeAsync(
+ bool includeZeroBalances)
+ {
+ var response =
+ await MakeJsonRequestAsync("/margin/borrowStatus", payload: await GetNoncePayloadAsync());
+ var balances = response.Select(x => new
+ {
+ Currency = x["currency"].Value(),
+ AvailableBalance = x["available"].Value()
+ });
+ if (includeZeroBalances)
+ {
+ balances = balances.Where(x => x.AvailableBalance > 0);
+ }
+
+ return balances.ToDictionary(k => k.Currency, v => v.AvailableBalance);
+ }
+
+ protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order)
+ {
+ if (order.Price == null && order.OrderType != OrderType.Market)
+ throw new ArgumentNullException(nameof(order.Price));
+ var orderAmount = await ClampOrderQuantity(order.MarketSymbol, order.Amount);
+ var orderPrice = order.Price.GetValueOrDefault();
+ if (order.OrderType != OrderType.Market)
+ {
+ orderPrice = await ClampOrderPrice(order.MarketSymbol, order.Price!.Value);
+ }
+
+ var payload = await GetNoncePayloadAsync();
+
+ payload["symbol"] = order.MarketSymbol;
+ payload["side"] = order.IsBuy ? "BUY" : "SELL";
+ payload["quantity"] = orderAmount;
+
+ if (!string.IsNullOrEmpty(order.ClientOrderId))
+ {
+ payload["clientOrderId"] = order.ClientOrderId;
+ }
+
+ if (order.IsPostOnly.GetValueOrDefault())
+ {
+ payload["type"] = "LIMIT_MAKER";
+ }
+
+ switch (order.OrderType)
+ {
+ case OrderType.Limit when !order.IsPostOnly.GetValueOrDefault():
+ payload["type"] = "LIMIT";
+ payload["price"] = orderPrice;
+ break;
+ case OrderType.Limit when order.IsPostOnly.GetValueOrDefault():
+ payload["type"] = "LIMIT_MAKER";
+ break;
+ case OrderType.Market:
+ payload["type"] = "MARKET";
+ break;
+ case OrderType.Stop:
+ default: throw new ArgumentOutOfRangeException(nameof(order.OrderType));
+ }
+
+ foreach (var kvp in order.ExtraParameters)
+ {
+ payload[kvp.Key] = kvp.Value;
}
- ExchangeOrderResult exchangeOrderResult = ParsePlacedOrder(result, order);
- exchangeOrderResult.MarketSymbol = order.MarketSymbol;
- exchangeOrderResult.FeesCurrency = ParseFeesCurrency(order.IsBuy, order.MarketSymbol);
- return exchangeOrderResult;
- }
-
- protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null)
- {
- if (marketSymbol.Length == 0)
- {
- marketSymbol = "all";
- }
-
- List orders = new List();
- JToken result = await MakePrivateAPIRequestAsync("returnOpenOrders", new object[] { "currencyPair", marketSymbol });
- if (marketSymbol == "all")
- {
- foreach (JProperty prop in result)
- {
- if (prop.Value is JArray array)
- {
- foreach (JToken token in array)
- {
- orders.Add(ParseOpenOrder(token, null));
- }
- }
- }
- }
- else if (result is JArray array)
- {
- foreach (JToken token in array)
- {
- orders.Add(ParseOpenOrder(token, marketSymbol));
- }
- }
-
- return orders;
- }
-
- protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
+
+ var response = await MakeJsonRequestAsync("/orders", payload: payload, requestMethod: "POST");
+ var orderInfo = await GetOrderDetailsAsync(response["id"].ToStringInvariant());
+
+ return orderInfo;
+ }
+
+ protected override async Task> OnGetOpenOrderDetailsAsync(
+ string marketSymbol = null)
{
- if (isClientOrderId) throw new NotSupportedException("Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature");
- JToken resultArray = await MakePrivateAPIRequestAsync("returnOrderTrades", new object[] { "orderNumber", orderId });
- string tickerSymbol = resultArray[0]["currencyPair"].ToStringInvariant();
- List orders = new List();
- ParseCompletedOrderDetails(orders, resultArray, tickerSymbol);
- if (orders.Count != 1)
- {
- throw new APIException($"ReturnOrderTrades for a single orderNumber returned {orders.Count} orders. Expected 1.");
- }
-
- orders[0].OrderId = orderId;
- orders[0].Price = orders[0].AveragePrice;
- return orders[0];
- }
-
- protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null)
- {
- marketSymbol = string.IsNullOrWhiteSpace(marketSymbol) ? "all" : NormalizeMarketSymbol(marketSymbol);
-
- List orders = new List();
- afterDate = afterDate ?? CryptoUtility.UtcNow.Subtract(TimeSpan.FromDays(365.0));
- long afterTimestamp = (long)afterDate.Value.UnixTimestampFromDateTimeSeconds();
- JToken result = await MakePrivateAPIRequestAsync("returnTradeHistory", new object[] { "currencyPair", marketSymbol, "limit", 10000, "start", afterTimestamp });
- if (marketSymbol != "all")
- {
- ParseCompletedOrderDetails(orders, result as JArray, marketSymbol);
- }
- else
- {
- foreach (JProperty prop in result)
- {
- ParseCompletedOrderDetails(orders, prop.Value as JArray, prop.Name);
- }
- }
- return orders;
- }
-
- protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
+ var query = !string.IsNullOrEmpty(marketSymbol) ? $"?symbol={marketSymbol}" : "";
+ var result = await MakeJsonRequestAsync($"/orders{query}", payload: await GetNoncePayloadAsync());
+
+ return result
+ .Select(o => ParseOrder(o, marketSymbol)).ToList();
+ }
+
+ protected override async Task OnGetOrderDetailsAsync(
+ string orderId,
+ string marketSymbol = null,
+ bool isClientOrderId = false)
{
- if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature");
- await MakePrivateAPIRequestAsync("cancelOrder", new object[] { "orderNumber", orderId.ConvertInvariant() });
- }
-
- protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest)
- {
- // If we have an address tag, verify that Polo lets you specify it as part of the withdrawal
- if (!string.IsNullOrWhiteSpace(withdrawalRequest.AddressTag))
- {
- if (!WithdrawalFieldCount.TryGetValue(withdrawalRequest.Currency, out int fieldCount) || fieldCount == 0)
- {
- throw new APIException($"Coin {withdrawalRequest.Currency} has unknown withdrawal field count. Please manually verify the number of fields allowed during a withdrawal (Address + Tag = 2) and add it to PoloniexWithdrawalFields.csv before calling Withdraw");
- }
- else if (fieldCount == 1)
- {
- throw new APIException($"Coin {withdrawalRequest.Currency} only allows an address to be specified and address tag {withdrawalRequest.AddressTag} was provided.");
- }
- else if (fieldCount > 2)
- {
- throw new APIException("More than two fields on a withdrawal is unsupported.");
- }
- }
-
- var paramsList = new List { "currency", NormalizeMarketSymbol(withdrawalRequest.Currency), "amount", withdrawalRequest.Amount, "address", withdrawalRequest.Address };
- if (!string.IsNullOrWhiteSpace(withdrawalRequest.AddressTag))
- {
- paramsList.Add("paymentId");
- paramsList.Add(withdrawalRequest.AddressTag);
- }
+ var result =
+ await MakeJsonRequestAsync($"/orders/{orderId}", payload: await GetNoncePayloadAsync());
+ return ParseOrder(result);
+ }
+
+ protected override async Task> OnGetCompletedOrderDetailsAsync(
+ string marketSymbol = null,
+ DateTime? afterDate = null)
+ {
+ var query = !string.IsNullOrEmpty(marketSymbol) ? $"?symbol={marketSymbol}" : "";
+ if (afterDate != null)
+ {
+ var startDateParam = $"startDate={new DateTimeOffset(afterDate.Value).ToUnixTimeMilliseconds()}";
+ query = query.Length > 0 ? $"?{startDateParam}" : $"&{startDateParam}";
+ }
+
+ var result = await MakeJsonRequestAsync($"/trades{query}", payload: await GetNoncePayloadAsync());
+
+ return ParseCompletedOrderDetails(result);
+ }
+
+ protected override async Task OnCancelOrderAsync(
+ string orderId,
+ string marketSymbol = null,
+ bool isClientOrderId = false)
+ {
+ await MakeJsonRequestAsync(
+ $"/orders/{orderId}",
+ payload: await GetNoncePayloadAsync(),
+ requestMethod: "DELETE");
+ }
+
+ protected override async Task OnWithdrawAsync(
+ ExchangeWithdrawalRequest withdrawalRequest)
+ {
+ // If we have an address tag, verify that Polo lets you specify it as part of the withdrawal
+ if (!string.IsNullOrWhiteSpace(withdrawalRequest.AddressTag))
+ {
+ if (!WithdrawalFieldCount.TryGetValue(withdrawalRequest.Currency, out int fieldCount) ||
+ fieldCount == 0)
+ {
+ throw new APIException(
+ $"Coin {withdrawalRequest.Currency} has unknown withdrawal field count. Please manually verify the number of fields allowed during a withdrawal (Address + Tag = 2) and add it to PoloniexWithdrawalFields.csv before calling Withdraw");
+ }
+ else if (fieldCount == 1)
+ {
+ throw new APIException(
+ $"Coin {withdrawalRequest.Currency} only allows an address to be specified and address tag {withdrawalRequest.AddressTag} was provided.");
+ }
+ else if (fieldCount > 2)
+ {
+ throw new APIException("More than two fields on a withdrawal is unsupported.");
+ }
+ }
+
+ var paramsList = new List
+ {
+ "currency", NormalizeMarketSymbol(withdrawalRequest.Currency), "amount", withdrawalRequest.Amount,
+ "address", withdrawalRequest.Address
+ };
+ if (!string.IsNullOrWhiteSpace(withdrawalRequest.AddressTag))
+ {
+ paramsList.Add("paymentId");
+ paramsList.Add(withdrawalRequest.AddressTag);
+ }
var token = await MakePrivateAPIRequestAsync("withdraw", paramsList.ToArray());
@@ -966,160 +935,187 @@ protected override async Task OnWithdrawAsync(Exchan
};
}
- protected override async Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false)
- {
- // Never reuse IOTA addresses
- if (currency.Equals("MIOTA", StringComparison.OrdinalIgnoreCase))
- {
- forceRegenerate = true;
- }
-
- IReadOnlyDictionary currencies = await GetCurrenciesAsync();
- var depositAddresses = new Dictionary(StringComparer.OrdinalIgnoreCase);
- if (!forceRegenerate && !(await TryFetchExistingAddresses(currency, currencies, depositAddresses)))
- {
- return null;
- }
-
- if (!depositAddresses.TryGetValue(currency, out var depositDetails))
- {
- depositDetails = await CreateDepositAddress(currency, currencies);
- }
-
- return depositDetails;
- }
-
- /// Gets the deposit history for a symbol
- /// (ignored) The currency to check.
- /// Collection of ExchangeCoinTransfers
- protected override async Task> OnGetDepositHistoryAsync(string currency)
- {
- JToken result = await MakePrivateAPIRequestAsync("returnDepositsWithdrawals",
- new object[]
- {
- "start", DateTime.MinValue.ToUniversalTime().UnixTimestampFromDateTimeSeconds(),
- "end", CryptoUtility.UtcNow.UnixTimestampFromDateTimeSeconds()
- });
-
- var transactions = new List();
-
- foreach (JToken token in result["deposits"])
- {
- var deposit = new ExchangeTransaction
- {
- Currency = token["currency"].ToStringUpperInvariant(),
- Address = token["address"].ToStringInvariant(),
- Amount = token["amount"].ConvertInvariant(),
- BlockchainTxId = token["txid"].ToStringInvariant(),
- Timestamp = token["timestamp"].ConvertInvariant().UnixTimeStampToDateTimeSeconds()
- };
-
- string status = token["status"].ToStringUpperInvariant();
- switch (status)
- {
- case "COMPLETE":
- deposit.Status = TransactionStatus.Complete;
- break;
- case "PENDING":
- deposit.Status = TransactionStatus.Processing;
- break;
- default:
- // TODO: API Docs don't specify what other options there will be for transaction status
- deposit.Status = TransactionStatus.Unknown;
- deposit.Notes = "Transaction status: " + status;
- break;
- }
-
- transactions.Add(deposit);
- }
-
- return transactions;
- }
-
- private static string ParseFeesCurrency(bool isBuy, string symbol)
- {
- string feesCurrency = null;
- string[] currencies = symbol.Split('_');
- if (currencies.Length == 2)
- {
- // fees are in the "To" currency
- feesCurrency = isBuy ? currencies[1] : currencies[0];
- }
-
- return feesCurrency;
- }
-
- private async Task TryFetchExistingAddresses(string currency, IReadOnlyDictionary currencies, Dictionary depositAddresses)
- {
- JToken result = await MakePrivateAPIRequestAsync("returnDepositAddresses");
- foreach (JToken jToken in result)
- {
- var token = (JProperty)jToken;
- var details = new ExchangeDepositDetails { Currency = token.Name };
-
- if (!TryPopulateAddressAndTag(currency, currencies, details, token.Value.ToStringInvariant()))
- {
- return false;
- }
-
- depositAddresses[details.Currency] = details;
- }
-
- return true;
- }
-
- private static bool TryPopulateAddressAndTag(string currency, IReadOnlyDictionary currencies, ExchangeDepositDetails details, string address)
- {
- if (currencies.TryGetValue(currency, out ExchangeCurrency coin))
- {
- if (!string.IsNullOrWhiteSpace(coin.BaseAddress))
- {
- details.Address = coin.BaseAddress;
- details.AddressTag = address;
- }
- else
- {
- details.Address = address;
- }
-
- return true;
- }
-
- // Cannot find currency in master list.
- // Stay safe and don't return a possibly half-baked deposit address missing a tag
- return false;
-
- }
-
- private static bool ParsePairState(string state)
- {
- if (string.IsNullOrWhiteSpace(state)) return false;
-
- return state == "NORMAL";
- }
-
- ///
- /// Create a deposit address
- ///
- /// Currency to create an address for
- /// Lookup of existing currencies
- /// ExchangeDepositDetails with an address or a BaseAddress/AddressTag pair.
- private async Task CreateDepositAddress(string currency, IReadOnlyDictionary currencies)
- {
- JToken result = await MakePrivateAPIRequestAsync("generateNewAddress", new object[] { "currency", currency });
- var details = new ExchangeDepositDetails
- {
- Currency = currency,
- };
-
- if (!TryPopulateAddressAndTag(currency, currencies, details, result["response"].ToStringInvariant()))
- {
- return null;
- }
-
- return details;
- }
- }
-
- public partial class ExchangeName { public const string Poloniex = "Poloniex"; }
+ protected override async Task OnGetDepositAddressAsync(
+ string currency,
+ bool forceRegenerate = false)
+ {
+ // Never reuse IOTA addresses
+ if (currency.Equals("MIOTA", StringComparison.OrdinalIgnoreCase))
+ {
+ forceRegenerate = true;
+ }
+
+ IReadOnlyDictionary currencies = await GetCurrenciesAsync();
+ var depositAddresses = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ if (!forceRegenerate && !(await TryFetchExistingAddresses(currency, currencies, depositAddresses)))
+ {
+ return null;
+ }
+
+ if (!depositAddresses.TryGetValue(currency, out var depositDetails))
+ {
+ depositDetails = await CreateDepositAddress(currency, currencies);
+ }
+
+ return depositDetails;
+ }
+
+ /// Gets the deposit history for a symbol
+ /// (ignored) The currency to check.
+ /// Collection of ExchangeCoinTransfers
+ protected override async Task> OnGetDepositHistoryAsync(string currency)
+ {
+ JToken result = await MakePrivateAPIRequestAsync("returnDepositsWithdrawals",
+ new object[]
+ {
+ "start", DateTime.MinValue.ToUniversalTime().UnixTimestampFromDateTimeSeconds(),
+ "end", CryptoUtility.UtcNow.UnixTimestampFromDateTimeSeconds()
+ });
+
+ var transactions = new List();
+
+ foreach (JToken token in result["deposits"])
+ {
+ var deposit = new ExchangeTransaction
+ {
+ Currency = token["currency"].ToStringUpperInvariant(),
+ Address = token["address"].ToStringInvariant(),
+ Amount = token["amount"].ConvertInvariant(),
+ BlockchainTxId = token["txid"].ToStringInvariant(),
+ Timestamp = token["timestamp"].ConvertInvariant().UnixTimeStampToDateTimeSeconds()
+ };
+
+ string status = token["status"].ToStringUpperInvariant();
+ switch (status)
+ {
+ case "COMPLETE":
+ deposit.Status = TransactionStatus.Complete;
+ break;
+ case "PENDING":
+ deposit.Status = TransactionStatus.Processing;
+ break;
+ default:
+ // TODO: API Docs don't specify what other options there will be for transaction status
+ deposit.Status = TransactionStatus.Unknown;
+ deposit.Notes = "Transaction status: " + status;
+ break;
+ }
+
+ transactions.Add(deposit);
+ }
+
+ return transactions;
+ }
+
+ private static string ParseFeesCurrency(bool isBuy, string symbol)
+ {
+ string feesCurrency = null;
+ string[] currencies = symbol.Split('_');
+ if (currencies.Length == 2)
+ {
+ // fees are in the "To" currency
+ feesCurrency = isBuy ? currencies[1] : currencies[0];
+ }
+
+ return feesCurrency;
+ }
+
+ private async Task