Skip to content

Commit 8a51761

Browse files
authored
Add ExchangeKrakenAPI.OnGetCandlesWebSocketAsync implementation (#600)
The kraken API (https://docs.kraken.com/websockets/#message-ohlc) actually only sends one candle at a time, not an array of candles, so I changed the interface accordingly The API also requires the candle interval parameter which is part of the channel name, so this was also added to the interface Provided an implementation for Kraken exchange. There is one quirk* mentioned below Added ws-candles to ExchangeSharpConsole. Also added a IOptionWithPeriod so the candle interval can be specified on commandline - and applied this to the REST-based CandlesOption with the same default value of 1800 I'm not sure if we have formal tests but I tested from the console app and all looked good except for: *Note that Kraken sends updates to the current candle when there are new trades, and when it does the open-time is the current time, i.e. the open time changes over the life-time of the candle but the close-time does not. Current implementation passes open-time to ParseCandle extension method as MarketCandle.TimeStamp is specified as the open time of the OHLC candle. So you'll see candle updates with new candle timestamp come through. I do not know if this would be consider correct/desired functionality by the community, or if the open-time should be set as close-time - period so it is constant? It would be a trivial change to var candle = this.ParseCandle(token[1], marketSymbol, interval * 60, 2, 3, 4, 5, 0, TimestampType.UnixSeconds, 7, null, 6); you are welcome to make. * Update .gitignore Navigating to a folder using the "Finder" on Mac generates a .DS_Store file holding metadata about the folder (e.g. thumbnails etc.). These files can pollute your git commits and are annoying. * Update MovingAverageCalculator.cs Pull methods and expose accessors on base class for consistency. * Implemented ws-candles for Kraken * Update ExchangeKrakenAPI.cs fix candle open time as close time -interval
1 parent 5bafa6e commit 8a51761

File tree

7 files changed

+127
-24
lines changed

7 files changed

+127
-24
lines changed

src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -843,30 +843,77 @@ protected override async Task OnCancelOrderAsync(string orderId, string marketSy
843843
await MakeJsonRequestAsync<JToken>("/0/private/CancelOrder", null, payload);
844844
}
845845

846+
protected override async Task<IWebSocket> OnGetCandlesWebSocketAsync(Func<MarketCandle, Task> callbackAsync, int periodSeconds, params string[] marketSymbols)
847+
{
848+
if (marketSymbols == null || marketSymbols.Length == 0)
849+
{
850+
marketSymbols = (await GetMarketSymbolsAsync(true)).ToArray();
851+
}
852+
//kraken has multiple OHLC channels named ohlc-1|5|15|30|60|240|1440|10080|21600 with interval specified in minutes
853+
int interval = periodSeconds / 60;
854+
855+
return await ConnectWebSocketAsync(null, messageCallback: async (_socket, msg) =>
856+
{
857+
/*
858+
https://docs.kraken.com/websockets/#message-ohlc
859+
[0]channelID integer Channel ID of subscription -deprecated, use channelName and pair
860+
[1]Array array
861+
-time decimal Begin time of interval, in seconds since epoch
862+
-etime decimal End time of interval, in seconds since epoch
863+
-open decimal Open price of interval
864+
-high decimal High price within interval
865+
-low decimal Low price within interval
866+
-close decimal Close price of interval
867+
-vwap decimal Volume weighted average price within interval
868+
-volume decimal Accumulated volume within interval
869+
-count integer Number of trades within interval
870+
[2]channelName string Channel Name of subscription
871+
[3]pair string Asset pair
872+
*/
873+
if (JToken.Parse(msg.ToStringFromUTF8()) is JArray token && token[2].ToStringInvariant() == ($"ohlc-{interval}"))
874+
{
875+
string marketSymbol = token[3].ToStringInvariant();
876+
//Kraken updates the candle open time to the current time, but we want it as open-time i.e. close-time - interval
877+
token[1][0] = token[1][1].ConvertInvariant<long>() - interval * 60;
878+
var candle = this.ParseCandle(token[1], marketSymbol, interval * 60, 2, 3, 4, 5, 0, TimestampType.UnixSeconds, 7, null, 6);
879+
await callbackAsync(candle);
880+
}
881+
}, connectCallback: async (_socket) =>
882+
{
883+
List<string> marketSymbolList = await GetMarketSymbolList(marketSymbols);
884+
await _socket.SendMessageAsync(new
885+
{
886+
@event = "subscribe",
887+
pair = marketSymbolList,
888+
subscription = new { name = "ohlc", interval = interval }
889+
});
890+
});
891+
}
892+
846893
protected override async Task<IWebSocket> OnGetTickersWebSocketAsync(Action<IReadOnlyCollection<KeyValuePair<string, ExchangeTicker>>> tickers, params string[] marketSymbols)
847894
{
848895
if (marketSymbols == null || marketSymbols.Length == 0)
849896
{
850897
marketSymbols = (await GetMarketSymbolsAsync(true)).ToArray();
851898
}
852899
return await ConnectWebSocketAsync(null, messageCallback: async (_socket, msg) =>
853-
{
854-
if (JToken.Parse(msg.ToStringFromUTF8()) is JArray token)
855-
{
856-
var exchangeTicker = await ConvertToExchangeTickerAsync(token[3].ToString(), token[1]);
857-
var kv = new KeyValuePair<string, ExchangeTicker>(exchangeTicker.MarketSymbol, exchangeTicker);
858-
tickers(new List<KeyValuePair<string, ExchangeTicker>> { kv });
859-
}
860-
}, connectCallback: async (_socket) =>
861-
{
862-
List<string> marketSymbolList = await GetMarketSymbolList(marketSymbols);
863-
await _socket.SendMessageAsync(new
864-
{
865-
@event = "subscribe",
866-
pair = marketSymbolList,
867-
subscription = new { name = "ticker" }
868-
});
869-
});
900+
{
901+
if (JToken.Parse(msg.ToStringFromUTF8()) is JArray token)
902+
{
903+
var exchangeTicker = await ConvertToExchangeTickerAsync(token[3].ToString(), token[1]);
904+
var kv = new KeyValuePair<string, ExchangeTicker>(exchangeTicker.MarketSymbol, exchangeTicker);
905+
tickers(new List<KeyValuePair<string, ExchangeTicker>> { kv });
906+
}
907+
}, connectCallback: async (_socket) =>
908+
{
909+
List<string> marketSymbolList = await GetMarketSymbolList(marketSymbols);
910+
await _socket.SendMessageAsync(new
911+
{
912+
@event = "subscribe",
913+
pair = marketSymbolList,
914+
subscription = new { name = "ticker" }
915+
});
916+
});
870917
}
871918

872919
protected override async Task<IWebSocket> OnGetTradesWebSocketAsync(Func<KeyValuePair<string, ExchangeTrade>, Task> callback, params string[] marketSymbols)

src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ protected virtual Task<ExchangeMarginPositionResult> OnGetOpenPositionAsync(stri
178178
throw new NotImplementedException();
179179
protected virtual Task<ExchangeCloseMarginPositionResult> OnCloseMarginPositionAsync(string marketSymbol) =>
180180
throw new NotImplementedException();
181-
protected virtual Task<IWebSocket> OnGetCandlesWebSocketAsync(Func<IReadOnlyCollection<MarketCandle>, Task> callbackAsync, params string[] marketSymbols) =>
181+
protected virtual Task<IWebSocket> OnGetCandlesWebSocketAsync(Func<MarketCandle, Task> callbackAsync, int periodSeconds, params string[] marketSymbols) =>
182182
throw new NotImplementedException();
183183
protected virtual Task<IWebSocket> OnGetTickersWebSocketAsync(Action<IReadOnlyCollection<KeyValuePair<string, ExchangeTicker>>> tickers, params string[] marketSymbols) =>
184184
throw new NotImplementedException();
@@ -1005,10 +1005,10 @@ public virtual async Task<ExchangeCloseMarginPositionResult> CloseMarginPosition
10051005
/// <param name="callbackAsync">Callback</param>
10061006
/// <param name="marketSymbols">Market Symbols</param>
10071007
/// <returns>Web socket, call Dispose to close</returns>
1008-
public virtual Task<IWebSocket> GetCandlesWebSocketAsync(Func<IReadOnlyCollection<MarketCandle>, Task> callbackAsync, params string[] marketSymbols)
1008+
public virtual Task<IWebSocket> GetCandlesWebSocketAsync(Func<MarketCandle, Task> callbackAsync, int periodSeconds, params string[] marketSymbols)
10091009
{
10101010
callbackAsync.ThrowIfNull(nameof(callbackAsync), "Callback must not be null");
1011-
return OnGetCandlesWebSocketAsync(callbackAsync, marketSymbols);
1011+
return OnGetCandlesWebSocketAsync(callbackAsync, periodSeconds, marketSymbols);
10121012
}
10131013

10141014
/// <summary>

src/ExchangeSharp/API/Exchanges/_Base/IExchangeAPI.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,10 @@ public interface IExchangeAPI : IDisposable, IBaseAPI, IOrderBookProvider
241241
/// Gets Candles (OHLC) websocket
242242
/// </summary>
243243
/// <param name="callbackAsync">Callback</param>
244+
/// <param name="periodSeconds">Candle interval in seconds</param>
244245
/// <param name="marketSymbols">Market Symbols</param>
245246
/// <returns>Web socket, call Dispose to close</returns>
246-
Task<IWebSocket> GetCandlesWebSocketAsync(Func<IReadOnlyCollection<MarketCandle>, Task> callbackAsync, params string[] marketSymbols);
247+
Task<IWebSocket> GetCandlesWebSocketAsync(Func<MarketCandle, Task> callbackAsync, int periodSeconds, params string[] marketSymbols);
247248

248249
/// <summary>
249250
/// Get all tickers via web socket

src/ExchangeSharpConsole/Options/CandlesOption.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
namespace ExchangeSharpConsole.Options
88
{
99
[Verb("candles", HelpText = "Prints all candle data from a 12 days period for the given exchange.")]
10-
public class CandlesOption : BaseOption, IOptionPerExchange, IOptionPerMarketSymbol
10+
public class CandlesOption : BaseOption, IOptionPerExchange, IOptionPerMarketSymbol, IOptionWithPeriod
1111
{
1212
public override async Task RunCommand()
1313
{
1414
using var api = GetExchangeInstance(ExchangeName);
1515

1616
var candles = await api.GetCandlesAsync(
1717
MarketSymbol,
18-
1800,
18+
Period,
1919
//TODO: Add interfaces for start and end date
2020
CryptoUtility.UtcNow.AddDays(-12),
2121
CryptoUtility.UtcNow
@@ -32,5 +32,7 @@ public override async Task RunCommand()
3232
public string ExchangeName { get; set; }
3333

3434
public string MarketSymbol { get; set; }
35+
36+
public int Period { get; set; }
3537
}
3638
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using CommandLine;
2+
3+
namespace ExchangeSharpConsole.Options.Interfaces
4+
{
5+
public interface IOptionWithPeriod
6+
{
7+
[Option('p', "period", Default = 1800,
8+
HelpText = "Period in seconds.")]
9+
int Period { get; set; }
10+
}
11+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using CommandLine;
6+
using ExchangeSharp;
7+
using ExchangeSharpConsole.Options.Interfaces;
8+
9+
namespace ExchangeSharpConsole.Options
10+
{
11+
[Verb("ws-candles", HelpText =
12+
"Connects to the given exchange websocket and keeps printing the candles from that exchange." +
13+
"If market symbol is not set then uses all.")]
14+
public class WebSocketsCandlesOption : BaseOption, IOptionPerExchange, IOptionWithMultipleMarketSymbol, IOptionWithPeriod
15+
{
16+
public override async Task RunCommand()
17+
{
18+
async Task<IWebSocket> GetWebSocket(IExchangeAPI api)
19+
{
20+
var symbols = await ValidateMarketSymbolsAsync(api, MarketSymbols.ToArray(), true);
21+
22+
return await api.GetCandlesWebSocketAsync(candle =>
23+
{
24+
Console.WriteLine($"Market {candle.Name,8}: {candle}");
25+
return Task.CompletedTask;
26+
},
27+
Period,
28+
symbols
29+
);
30+
}
31+
32+
await RunWebSocket(ExchangeName, GetWebSocket);
33+
}
34+
35+
public string ExchangeName { get; set; }
36+
37+
public IEnumerable<string> MarketSymbols { get; set; }
38+
39+
public int Period { get; set; }
40+
}
41+
}

src/ExchangeSharpConsole/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public partial class Program
3535
typeof(TradeHistoryOption),
3636
typeof(WebSocketsOrderbookOption),
3737
typeof(WebSocketsTickersOption),
38-
typeof(WebSocketsTradesOption)
38+
typeof(WebSocketsTradesOption),
39+
typeof(WebSocketsCandlesOption)
3940
};
4041

4142
public Program()

0 commit comments

Comments
 (0)