Skip to content

Commit 65f3843

Browse files
authored
BTSE: added some private REST endpoints (#529)
* add BTSE public api * BTSE: Private REST
1 parent 2127a7a commit 65f3843

File tree

2 files changed

+224
-12
lines changed

2 files changed

+224
-12
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ The following cryptocurrency exchanges are supported:
3333
| Bittrex | x | x | T R |
3434
| BL3P | x | x | R B | Trades stream does not send trade's ids.
3535
| Bleutrade | x | x | |
36-
| BTSE | x | | |
36+
| BTSE | x | x | |
3737
| Coinbase | x | x | T R |
3838
| Digifinex | x | x | R B |
3939
| Gemini | x | x | R |
@@ -157,4 +157,4 @@ [email protected]
157157
http://www.digitalruby.com
158158

159159
[nuget]: https://www.nuget.org/packages/DigitalRuby.ExchangeSharp/
160-
[websocket4net]: https://github.com/kerryjiang/WebSocket4Net
160+
[websocket4net]: https://github.com/kerryjiang/WebSocket4Net

src/ExchangeSharp/API/Exchanges/BTSE/ExchangeBTSEAPI.cs

Lines changed: 222 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
namespace ExchangeSharp {
1+
using Newtonsoft.Json;
2+
3+
namespace ExchangeSharp
4+
{
25
using Newtonsoft.Json.Linq;
36
using System;
47
using System.Collections.Generic;
58
using System.Linq;
69
using System.Threading.Tasks;
710

8-
public sealed partial class ExchangeBTSEAPI :ExchangeAPI {
11+
public sealed partial class ExchangeBTSEAPI : ExchangeAPI
12+
{
913
public override string BaseUrl { get; set; } = "https://api.btse.com";
1014

1115
protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
@@ -16,7 +20,8 @@ protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
1620
protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>> OnGetTickersAsync()
1721
{
1822
JToken allPairs = await MakeJsonRequestAsync<JArray>("/spot/api/v3/market_summary");
19-
var tasks = allPairs.Select(async token => await this.ParseTickerAsync(token, token["symbol"].Value<string>(), "lowestAsk", "highestBid", "last", "volume", null,
23+
var tasks = allPairs.Select(async token => await this.ParseTickerAsync(token,
24+
token["symbol"].Value<string>(), "lowestAsk", "highestBid", "last", "volume", null,
2025
null, TimestampType.UnixMilliseconds, "base", "quote", "symbol"));
2126

2227
return (await Task.WhenAll(tasks)).Select(ticker =>
@@ -25,15 +30,17 @@ protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>>
2530

2631
protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymbol)
2732
{
28-
JToken ticker = await MakeJsonRequestAsync<JObject>("/spot/api/v3/market_summary", null, new Dictionary<string, object>()
29-
{
30-
{"symbol", marketSymbol}
31-
});
33+
JToken ticker = await MakeJsonRequestAsync<JObject>("/spot/api/v3/market_summary", null,
34+
new Dictionary<string, object>()
35+
{
36+
{"symbol", marketSymbol}
37+
});
3238
return await this.ParseTickerAsync(ticker, marketSymbol, "lowestAsk", "highestBid", "last", "volume", null,
3339
null, TimestampType.UnixMilliseconds, "base", "quote", "symbol");
3440
}
3541

36-
protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null,
42+
protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(string marketSymbol,
43+
int periodSeconds, DateTime? startDate = null, DateTime? endDate = null,
3744
int? limit = null)
3845
{
3946
var payload = new Dictionary<string, object>()
@@ -46,6 +53,7 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
4653
{
4754
payload.Add("start", startDate.Value.UnixTimestampFromDateTimeMilliseconds());
4855
}
56+
4957
if (endDate != null)
5058
{
5159
payload.Add("end", startDate.Value.UnixTimestampFromDateTimeMilliseconds());
@@ -56,15 +64,219 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
5664
this.ParseCandle(token, marketSymbol, periodSeconds, 1, 2, 3, 4, 0, TimestampType.UnixMilliseconds, 5));
5765
}
5866

67+
protected override async Task OnCancelOrderAsync(string orderId, string? marketSymbol = null)
68+
{
69+
var payload = await GetNoncePayloadAsync();
70+
71+
payload["order_id"] = orderId.ConvertInvariant<long>();
72+
var url = new UriBuilder(BaseUrl) {Path = "/spot/api/v3/order"};
73+
url.AppendPayloadToQuery(new Dictionary<string, object>()
74+
{
75+
{"symbol", marketSymbol},
76+
{"orderID", orderId}
77+
});
78+
79+
await MakeJsonRequestAsync<JToken>(url.ToStringInvariant().Replace(BaseUrl, ""),
80+
requestMethod: "DELETE", payload: payload);
81+
}
82+
83+
protected override async Task<Dictionary<string, decimal>> OnGetAmountsAsync()
84+
{
85+
var payload = await GetNoncePayloadAsync();
86+
87+
var result = await MakeJsonRequestAsync<JToken>("/spot/api/v3/user/wallet",
88+
requestMethod: "GET", payload: payload);
89+
return Extract(result, token => (token["currency"].Value<string>(), token["total"].Value<decimal>()));
90+
}
91+
92+
protected override async Task<Dictionary<string, decimal>> OnGetAmountsAvailableToTradeAsync()
93+
{
94+
var payload = await GetNoncePayloadAsync();
95+
96+
var result = await MakeJsonRequestAsync<JToken>("/spot/api/v3/user/wallet",
97+
requestMethod: "GET", payload: payload);
98+
return Extract(result, token => (token["currency"].Value<string>(), token["available"].Value<decimal>()));
99+
}
100+
101+
protected override async Task<Dictionary<string, decimal>> OnGetFeesAsync()
102+
{
103+
var payload = await GetNoncePayloadAsync();
104+
105+
var result = await MakeJsonRequestAsync<JToken>("/spot/api/v3/user/fees",
106+
requestMethod: "GET", payload: payload);
107+
108+
//taker or maker fees in BTSE.. i chose take for here
109+
return Extract(result, token => (token["symbol"].Value<string>(), token["taker"].Value<decimal>()));
110+
}
111+
112+
protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetOpenOrderDetailsAsync(
113+
string? marketSymbol = null)
114+
{
115+
if (marketSymbol == null) throw new ArgumentNullException(nameof(marketSymbol));
116+
var payload = await GetNoncePayloadAsync();
117+
var url = new UriBuilder(BaseUrl) {Path = "/spot/api/v3/open_orders"};
118+
url.AppendPayloadToQuery(new Dictionary<string, object>()
119+
{
120+
{"symbol", marketSymbol}
121+
});
122+
var result = await MakeJsonRequestAsync<JToken>(url.ToStringInvariant().Replace(BaseUrl, ""),
123+
requestMethod: "GET", payload: payload);
124+
125+
//taker or maker fees in BTSE.. i chose take for here
126+
return Extract2(result, token => new ExchangeOrderResult()
127+
{
128+
Amount = token["size"].Value<decimal>(),
129+
AmountFilled = token["filledSize"].Value<decimal>(),
130+
OrderId = token["orderID"].Value<string>(),
131+
IsBuy = token["side"].Value<string>() == "BUY",
132+
Price = token["price"].Value<decimal>(),
133+
MarketSymbol = token["symbol"].Value<string>(),
134+
OrderDate = token["timestamp"].ConvertInvariant<long>().UnixTimeStampToDateTimeMilliseconds()
135+
});
136+
}
137+
138+
public override async Task<ExchangeOrderResult[]> PlaceOrdersAsync(params ExchangeOrderRequest[] orders)
139+
{
140+
var payload = await GetNoncePayloadAsync();
141+
payload.Add("body", orders.Select(request => new
142+
{
143+
size = request.Amount,
144+
side = request.IsBuy ? "BUY" : "SELL",
145+
price = request.Price,
146+
stopPrice = request.StopPrice,
147+
symbol = request.MarketSymbol,
148+
txType = request.OrderType == OrderType.Limit ? "LIMIT" :
149+
request.OrderType == OrderType.Stop ? "STOP" : null,
150+
type = request.OrderType == OrderType.Limit ? "LIMIT" :
151+
request.OrderType == OrderType.Market ? "MARKET" : null
152+
}));
153+
var result = await MakeJsonRequestAsync<JToken>("/spot/api/v3/order",
154+
requestMethod: "POST", payload: payload);
155+
return Extract2(result, token =>
156+
{
157+
var status = ExchangeAPIOrderResult.Unknown;
158+
switch (token["status"].Value<int>())
159+
{
160+
case 2:
161+
status = ExchangeAPIOrderResult.Pending;
162+
break;
163+
case 4:
164+
status = ExchangeAPIOrderResult.Filled;
165+
break;
166+
case 5:
167+
status = ExchangeAPIOrderResult.FilledPartially;
168+
break;
169+
case 6:
170+
status = ExchangeAPIOrderResult.Canceled;
171+
break;
172+
case 9: //trigger inserted
173+
case 10: //trigger activated
174+
status = ExchangeAPIOrderResult.Pending;
175+
break;
176+
case 15: //rejected
177+
status = ExchangeAPIOrderResult.Error;
178+
break;
179+
case 16: //not found
180+
status = ExchangeAPIOrderResult.Unknown;
181+
break;
182+
}
183+
184+
return new ExchangeOrderResult()
185+
{
186+
Message = token["message"].Value<string>(),
187+
OrderId = token["orderID"].Value<string>(),
188+
IsBuy = token["orderType"].Value<string>().ToLowerInvariant() == "buy",
189+
Price = token["price"].Value<decimal>(),
190+
MarketSymbol = token["symbol"].Value<string>(),
191+
Result = status,
192+
Amount = token["size"].Value<decimal>(),
193+
OrderDate = token["timestamp"].ConvertInvariant<long>().UnixTimeStampToDateTimeMilliseconds(),
194+
};
195+
}).ToArray();
196+
}
197+
198+
private Dictionary<TKey, TValue> Extract<TKey, TValue>(JToken token, Func<JToken, (TKey, TValue)> processor)
199+
{
200+
if (token is JArray resultArr)
201+
{
202+
return resultArr.Select(processor.Invoke)
203+
.ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2);
204+
}
205+
206+
var resItem = processor.Invoke(token);
207+
return new Dictionary<TKey, TValue>()
208+
{
209+
{resItem.Item1, resItem.Item2}
210+
};
211+
}
212+
213+
private IEnumerable<TValue> Extract2<TValue>(JToken token, Func<JToken, TValue> processor)
214+
{
215+
if (token is JArray resultArr)
216+
{
217+
return resultArr.Select(processor.Invoke);
218+
}
219+
220+
return new List<TValue>()
221+
{
222+
processor.Invoke(token)
223+
};
224+
}
225+
59226
protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary<string, object> payload, string method)
60227
{
61-
if ( method == "GET" && (payload?.Count??0) != 0)
228+
if (method == "GET" && (payload?.Count ?? 0) != 0 && !payload.ContainsKey("nonce"))
62229
{
63230
url.AppendPayloadToQuery(payload);
64231
}
232+
65233
return base.ProcessRequestUrl(url, payload, method);
66234
}
235+
236+
protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary<string, object> payload)
237+
{
238+
if (CanMakeAuthenticatedRequest(payload))
239+
{
240+
if (payload.TryGetValue("body", out var body))
241+
{
242+
payload.Remove("body");
243+
}
244+
245+
var nonce = payload["nonce"].ToString();
246+
payload.Remove("nonce");
247+
248+
249+
var json = JsonConvert.SerializeObject(body ?? payload);
250+
if (json == "{}")
251+
{
252+
json = "";
253+
}
254+
255+
var hexSha384 = CryptoUtility.SHA384Sign(
256+
$"{request.RequestUri.PathAndQuery.Replace("/spot", string.Empty)}{nonce}{json}",
257+
PrivateApiKey.ToUnsecureString());
258+
request.AddHeader("btse-sign", hexSha384);
259+
request.AddHeader("btse-api", PublicApiKey.ToUnsecureString());
260+
await request.WriteToRequestAsync(json);
261+
}
262+
263+
await base.ProcessRequestAsync(request, payload);
264+
}
265+
266+
protected override async Task<Dictionary<string, object>> GetNoncePayloadAsync()
267+
{
268+
var result = await base.GetNoncePayloadAsync();
269+
if (result.ContainsKey("recvWindow"))
270+
{
271+
result.Remove("recvWindow");
272+
}
273+
274+
return result;
275+
}
67276
}
68277

69-
public partial class ExchangeName { public const string BTSE = "BTSE"; }
278+
public partial class ExchangeName
279+
{
280+
public const string BTSE = "BTSE";
281+
}
70282
}

0 commit comments

Comments
 (0)