diff --git a/src/ExchangeSharp/API/Exchanges/Bitflyer/ExchangeBitflyerApi.cs b/src/ExchangeSharp/API/Exchanges/Bitflyer/ExchangeBitflyerApi.cs new file mode 100644 index 00000000..22990665 --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/Bitflyer/ExchangeBitflyerApi.cs @@ -0,0 +1,167 @@ +using Newtonsoft.Json.Linq; +using SocketIOClient; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ExchangeSharp +{ + public sealed partial class ExchangeBitflyerApi : ExchangeAPI + { + public override string BaseUrl { get; set; } = "https://api.bitflyer.com"; + public override string BaseUrlWebSocket { get; set; } = "https://io.lightstream.bitflyer.com"; + + public ExchangeBitflyerApi() + { + //NonceStyle = new guid + //NonceOffset not needed + // WebSocketOrderBookType = not implemented + MarketSymbolSeparator = "_"; + MarketSymbolIsUppercase = true; + // ExchangeGlobalCurrencyReplacements[] not implemented + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + /* + [ + { + "product_code": "BTC_JPY", + "market_type": "Spot" + }, + { + "product_code": "XRP_JPY", + "market_type": "Spot" + }, + { + "product_code": "ETH_JPY", + "market_type": "Spot" + }, + { + "product_code": "XLM_JPY", + "market_type": "Spot" + }, + { + "product_code": "MONA_JPY", + "market_type": "Spot" + }, + { + "product_code": "ETH_BTC", + "market_type": "Spot" + }, + { + "product_code": "BCH_BTC", + "market_type": "Spot" + }, + { + "product_code": "FX_BTC_JPY", + "market_type": "FX" + }, + { + "product_code": "BTCJPY12MAR2021", + "alias": "BTCJPY_MAT1WK", + "market_type": "Futures" + }, + { + "product_code": "BTCJPY19MAR2021", + "alias": "BTCJPY_MAT2WK", + "market_type": "Futures" + }, + { + "product_code": "BTCJPY26MAR2021", + "alias": "BTCJPY_MAT3M", + "market_type": "Futures" + } + ] + */ + JToken instruments = await MakeJsonRequestAsync("v1/getmarkets"); + var markets = new List(); + foreach (JToken instrument in instruments) + { + markets.Add(new ExchangeMarket + { + MarketSymbol = instrument["product_code"].ToStringUpperInvariant(), + AltMarketSymbol = instrument["alias"].ToStringInvariant(), + AltMarketSymbol2 = instrument["market_type"].ToStringInvariant(), + }); + } + return markets.Select(m => m.MarketSymbol); + } + protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + { + if (marketSymbols == null || marketSymbols.Length == 0) + { + marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); + } + var client = new SocketIOWrapper(BaseUrlWebSocket); + + foreach (var marketSymbol in marketSymbols) + { // {product_code} can be obtained from the market list. It cannot be an alias. + // BTC/JPY (Spot): lightning_executions_BTC_JPY + client.socketIO.On($"lightning_executions_{marketSymbol}", response => + { /* [[ { + "id": 39361, + "side": "SELL", + "price": 35100, + "size": 0.01, + "exec_date": "2015-07-07T10:44:33.547Z", + "buy_child_order_acceptance_id": "JRF20150707-014356-184990", + "sell_child_order_acceptance_id": "JRF20150707-104433-186048" + } ]] */ + var token = JToken.Parse(response.ToStringInvariant()); + foreach (var tradeToken in token[0]) + { + var trade = tradeToken.ParseTradeBitflyer("size", "price", "side", "exec_date", TimestampType.Iso8601UTC, "id"); + + // If it is executed during an Itayose, it will be an empty string. + if (string.IsNullOrWhiteSpace(tradeToken["side"].ToStringInvariant())) + trade.Flags |= ExchangeTradeFlags.HasNoSide; + + callback(new KeyValuePair(marketSymbol, trade)).Wait(); + } + }); + } + + client.socketIO.OnConnected += async (sender, e) => + { + foreach (var marketSymbol in marketSymbols) + { // {product_code} can be obtained from the market list. It cannot be an alias. + // BTC/JPY (Spot): lightning_executions_BTC_JPY + await client.socketIO.EmitAsync("subscribe", $"lightning_executions_{marketSymbol}"); + } + }; + await client.socketIO.ConnectAsync(); + return client; + } + } + + class SocketIOWrapper : IWebSocket + { + public SocketIO socketIO; + public SocketIOWrapper(string url) + { + socketIO = new SocketIO(url); + socketIO.Options.Transport = SocketIOClient.Transport.TransportProtocol.WebSocket; + socketIO.OnConnected += (s, e) => Connected?.Invoke(this); + socketIO.OnDisconnected += (s, e) => Disconnected?.Invoke(this); + } + + public TimeSpan ConnectInterval + { get => socketIO.Options.ConnectionTimeout; set => socketIO.Options.ConnectionTimeout = value; } + + public TimeSpan KeepAlive + { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public event WebSocketConnectionDelegate Connected; + public event WebSocketConnectionDelegate Disconnected; + + public Task SendMessageAsync(object message) => throw new NotImplementedException(); + + public void Dispose() => socketIO.Dispose(); + } + + public partial class ExchangeName { public const string Bitflyer = "Bitflyer"; } +} diff --git a/src/ExchangeSharp/API/Exchanges/Bitflyer/Models/BitflyerTrade.cs b/src/ExchangeSharp/API/Exchanges/Bitflyer/Models/BitflyerTrade.cs new file mode 100644 index 00000000..a3da4142 --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/Bitflyer/Models/BitflyerTrade.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ExchangeSharp.Bitflyer +{ + public class BitflyerTrade : ExchangeTrade + { + public string BuyChildOrderAcceptanceId { get; set; } + public string SellChildOrderAcceptanceId { get; set; } + + public override string ToString() + { + return string.Format("{0},{1}, {2}", base.ToString(), BuyChildOrderAcceptanceId, SellChildOrderAcceptanceId); + } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs index 1af0d194..ba9297d1 100644 --- a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs +++ b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs @@ -25,6 +25,7 @@ The above copyright notice and this permission notice shall be included in all c using ExchangeSharp.NDAX; using ExchangeSharp.API.Exchanges.FTX.Models; using ExchangeSharp.Bybit; +using ExchangeSharp.Bitflyer; namespace ExchangeSharp { @@ -541,6 +542,16 @@ internal static ExchangeTrade ParseTradeBinance(this JToken token, object amount return trade; } + internal static ExchangeTrade ParseTradeBitflyer(this JToken token, object amountKey, object priceKey, object typeKey, + object timestampKey, TimestampType timestampType, object idKey, string typeKeyIsBuyValue = "buy") + { + var trade = ParseTradeComponents(token, amountKey, priceKey, typeKey, + timestampKey, timestampType, idKey, typeKeyIsBuyValue); + trade.BuyChildOrderAcceptanceId = token["buy_child_order_acceptance_id"].ConvertInvariant(); + trade.SellChildOrderAcceptanceId = token["sell_child_order_acceptance_id"].ConvertInvariant(); + return trade; + } + internal static ExchangeTrade ParseTradeBybit(this JToken token, object amountKey, object priceKey, object typeKey, object timestampKey, TimestampType timestampType, object idKey, string typeKeyIsBuyValue = "buy") { diff --git a/src/ExchangeSharp/ExchangeSharp.csproj b/src/ExchangeSharp/ExchangeSharp.csproj index bb2ec1cf..c292f3a1 100644 --- a/src/ExchangeSharp/ExchangeSharp.csproj +++ b/src/ExchangeSharp/ExchangeSharp.csproj @@ -35,6 +35,7 @@ + diff --git a/src/ExchangeSharp/Model/ExchangeTrade.cs b/src/ExchangeSharp/Model/ExchangeTrade.cs index a4edbaee..878879c1 100644 --- a/src/ExchangeSharp/Model/ExchangeTrade.cs +++ b/src/ExchangeSharp/Model/ExchangeTrade.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -120,6 +120,11 @@ public enum ExchangeTradeFlags /// /// Whether the trade is the last trade from a snapshot /// - IsLastFromSnapshot = 4 + IsLastFromSnapshot = 4, + + /// + /// Is neither buy nor sell + /// + HasNoSide = 8, } }