diff --git a/Cargo.toml b/Cargo.toml index fe36cb8e..75b369d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,17 @@ license = "BSD-2-Clause" repository = "https://github.com/nash-io/openlimits" keywords = ["cryptocurrency", "exchange", "openlimits", "api"] +[[example]] +name = "orderbook" +path = "examples/rust/orderbook.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] +bindings = ["ligen", "ligen-macro", "ligen-csharp"] default = ["rust_gmp"] rust_gmp = ["nash-protocol/rust_gmp", "nash-native-client/rust_gmp"] num_bigint = ["nash-protocol/num_bigint", "nash-native-client/num_bigint"] -python = ["pyo3"] [dependencies] async-trait = "0.1" @@ -41,8 +44,17 @@ tungstenite = "0.12" sha2 = "0.9.1" url = "2.1.1" derive_more = "0.99" -nash-protocol = { version = "0.1", default-features = false, features = ["rustcrypto"] } -nash-native-client = { version = "0.1", default-features = false, features = ["rustcrypto"] } +#nash-protocol = { version = "0.1", default-features = false, features = ["rustcrypto"] } +#nash-native-client = { version = "0.1", default-features = false, features = ["rustcrypto"] } +nash-protocol = { path = "../nash-rust/nash-protocol", default-features = false, features = ["rustcrypto"] } +nash-native-client = { path = "../nash-rust/nash-native-client", default-features = false, features = ["rustcrypto"] } pyo3 = { version = "0.12.3", optional = true } -openlimits-binance = { version = "0.1" } -openlimits-exchange = { version = "0.1" } +openlimits-binance = { path = "crates/openlimits-binance" } +openlimits-exchange = { path = "crates/openlimits-exchange" } +ligen-macro = { path = "../../sensorial/systems/ligen/ligen/macro", optional = true } +ligen = { path = "../../sensorial/systems/ligen/ligen", optional = true } +lazy_static = "1.4.0" + +[build-dependencies] +ligen = { path = "../../sensorial/systems/ligen/ligen", optional = true } +ligen-csharp = { path = "../../sensorial/systems/ligen/generators/ligen-csharp", optional = true } \ No newline at end of file diff --git a/bindings/REMOVE_ME b/bindings/REMOVE_ME new file mode 100644 index 00000000..e69de29b diff --git a/bindings/csharp/.gitignore b/bindings/csharp/.gitignore new file mode 100644 index 00000000..7de5508b --- /dev/null +++ b/bindings/csharp/.gitignore @@ -0,0 +1,2 @@ +obj +bin diff --git a/bindings/csharp/AskBid.cs b/bindings/csharp/AskBid.cs new file mode 100644 index 00000000..f5be6609 --- /dev/null +++ b/bindings/csharp/AskBid.cs @@ -0,0 +1,47 @@ +namespace OpenLimits +{ + using System; + using System.Runtime.InteropServices; + using System.Globalization; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct FFIAskBid + { + public readonly IntPtr price; + public readonly IntPtr qty; + + public FFIAskBid(IntPtr price, IntPtr qty) + { + this.price = price; + this.qty = qty; + } + + public void Dispose() { + ExchangeClient.FreeString(price); + ExchangeClient.FreeString(qty); + } + + public AskBid ToAskBid() { + return new AskBid( + CString.ToString(this.price), + CString.ToString(this.qty) + ); + } + } + + public struct AskBid + { + public readonly decimal price; + public readonly decimal qty; + + public AskBid(string price, string qty) + { + this.price = Decimal.Parse(price, System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + this.qty = Decimal.Parse(qty, System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + } + + public override string ToString() + { + return "AskBid { price=" + price + ", qty=" + qty + "}"; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/Balance.cs b/bindings/csharp/Balance.cs new file mode 100644 index 00000000..fe6ccd81 --- /dev/null +++ b/bindings/csharp/Balance.cs @@ -0,0 +1,41 @@ +namespace OpenLimits +{ + using System.Runtime.InteropServices; + using System; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct FFIBalance + { + public readonly IntPtr asset; + public readonly IntPtr total; + public readonly IntPtr free; + + public void Dispose() { + ExchangeClient.FreeString(asset); + ExchangeClient.FreeString(total); + ExchangeClient.FreeString(free); + } + + public Balance ToBalance() { + return new Balance( + CString.ToString(this.asset), + CString.ToString(this.total), + CString.ToString(this.free) + ); + } + } + + public struct Balance + { + public readonly string asset; + public readonly string total; + public readonly string free; + + public Balance(string asset, string total, string free) + { + this.asset = asset; + this.total = total; + this.free = free; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/BinanceClientConfig.cs b/bindings/csharp/BinanceClientConfig.cs new file mode 100644 index 00000000..e0c1a120 --- /dev/null +++ b/bindings/csharp/BinanceClientConfig.cs @@ -0,0 +1,27 @@ + +namespace OpenLimits +{ + using System.Runtime.InteropServices; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct BinanceClientConfig + { + public readonly string apikey; + public readonly string secret; + public readonly bool sandbox; + + private BinanceClientConfig(string apikey, string secret, bool sandbox) + { + this.apikey = apikey; + this.secret = secret; + this.sandbox = sandbox; + } + + public static BinanceClientConfig Authenticated(string apikey, string secret, bool sandbox) { + return new BinanceClientConfig(apikey, secret, sandbox); + } + + public static BinanceClientConfig Unauthenticated(bool sandbox) { + return new BinanceClientConfig(null, null, sandbox); + } + } +} \ No newline at end of file diff --git a/bindings/csharp/CString.cs b/bindings/csharp/CString.cs new file mode 100644 index 00000000..062b59ce --- /dev/null +++ b/bindings/csharp/CString.cs @@ -0,0 +1,19 @@ +namespace OpenLimits +{ + using System.Runtime.InteropServices; + using System; + using System.Text; + + public class CString { + public static string ToString(IntPtr handle) { + if (handle.ToInt64() == 0) { + return null; + } + int len = 0; + while (Marshal.ReadByte(handle,len) != 0) { ++len; } + byte[] buffer = new byte[len]; + Marshal.Copy(handle, buffer, 0, buffer.Length); + return Encoding.UTF8.GetString(buffer); + } + } +} \ No newline at end of file diff --git a/bindings/csharp/Candle.cs b/bindings/csharp/Candle.cs new file mode 100644 index 00000000..0995712d --- /dev/null +++ b/bindings/csharp/Candle.cs @@ -0,0 +1,24 @@ +namespace OpenLimits +{ + using System.Runtime.InteropServices; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct Candle + { + public readonly ulong time; + public readonly double low; + public readonly double high; + public readonly double open; + public readonly double close; + public readonly double volume; + + public Candle(ulong time, double low, double high, double open, double close, double volume) + { + this.time = time; + this.low = low; + this.high = high; + this.open = open; + this.close = close; + this.volume = volume; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/CoinbaseClientConfig.cs b/bindings/csharp/CoinbaseClientConfig.cs new file mode 100644 index 00000000..d503bb18 --- /dev/null +++ b/bindings/csharp/CoinbaseClientConfig.cs @@ -0,0 +1,29 @@ + +namespace OpenLimits +{ + using System.Runtime.InteropServices; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct CoinbaseClientConfig + { + public readonly string apikey; + public readonly string secret; + public readonly string passphrase; + public readonly bool sandbox; + + private CoinbaseClientConfig(string apikey, string secret, string passphrase, bool sandbox) + { + this.apikey = apikey; + this.secret = secret; + this.passphrase = passphrase; + this.sandbox = sandbox; + } + + public static CoinbaseClientConfig Authenticated(string apikey, string secret, string passphrase, bool sandbox) { + return new CoinbaseClientConfig(apikey, secret, passphrase, sandbox); + } + + public static CoinbaseClientConfig Unauthenticated(bool sandbox) { + return new CoinbaseClientConfig(null, null, null, sandbox); + } + } +} \ No newline at end of file diff --git a/bindings/csharp/ExchangeClient.cs b/bindings/csharp/ExchangeClient.cs new file mode 100644 index 00000000..8948f1d2 --- /dev/null +++ b/bindings/csharp/ExchangeClient.cs @@ -0,0 +1,712 @@ + +namespace OpenLimits +{ + + using System; + using System.Threading; + using System.Collections.Generic; + using System.Runtime.InteropServices; + public class ExchangeClient + { + static HashSet _clients = new HashSet(); + private void handleResult(FFIResult result) { + string message = "Unknown error"; + if (result.message.ToInt64() != 0) { + message = CString.ToString(result.message); + FreeString(result.message); + } + switch(result.tag) { + case ResultTag.Ok: return; + case ResultTag.InvalidArgument: + throw new ArgumentException(message); + case ResultTag.BinanceError: + throw new BinanceError(message); + case ResultTag.CoinbaseError: + throw new CoinbaseError(message); + case ResultTag.NashProtocolError: + throw new NashProtocolError(message); + case ResultTag.MissingImplementation: + throw new MissingImplementation(message); + case ResultTag.AssetNotFound: + throw new AssetNotFound(message); + case ResultTag.NoApiKeySet: + throw new NoApiKeySet(message); + case ResultTag.InternalServerError: + throw new InternalServerError(message); + case ResultTag.ServiceUnavailable: + throw new ServiceUnavailable(message); + case ResultTag.Unauthorized: + throw new Unauthorized(message); + case ResultTag.SymbolNotFound: + throw new SymbolNotFound(message); + case ResultTag.SocketError: + throw new SocketError(message); + case ResultTag.GetTimestampFailed: + throw new GetTimestampFailed(message); + case ResultTag.ReqError: + throw new ReqError(message); + case ResultTag.InvalidHeaderError: + throw new InvalidHeaderError(message); + case ResultTag.InvalidPayloadSignature: + throw new InvalidPayloadSignature(message); + case ResultTag.IoError: + throw new IoError(message); + case ResultTag.PoisonError: + throw new PoisonError(message); + case ResultTag.JsonError: + throw new JsonError(message); + case ResultTag.ParseFloatError: + throw new ParseFloatError(message); + case ResultTag.UrlParserError: + throw new UrlParserError(message); + case ResultTag.Tungstenite: + throw new Tungstenite(message); + case ResultTag.TimestampError: + throw new TimestampError(message); + case ResultTag.UnkownResponse: + throw new UnkownResponse(message); + case ResultTag.NotParsableResponse: + throw new NotParsableResponse(message); + case ResultTag.MissingParameter: + throw new MissingParameter(message); + case ResultTag.WebSocketMessageNotSupported: + throw new WebSocketMessageNotSupported(message); } + } + /// Used by rust to write data directly to C# thus avoiding changing ownership + private FFITrade[] subTradesBuff = new FFITrade[1024]; + private FFIAskBid[] subAsksBuff = new FFIAskBid[1024]; + private FFIAskBid[] subBidsBuff = new FFIAskBid[1024]; + + // Callbacks from rust into C#. Some callbacks come in a "private" and public version. + // Some objects, especially those containing strings or array of objects will be serialized into a + // C# version after arriving. Strings exchanged from rust to C# must be freed manually. So it is important not to expose + // The internals + public delegate void OnError(); + public delegate void OnPing(); + public delegate void OnDisconnect(); + public delegate void OnOrderbook(OrderbookResponse orderbook); + unsafe private delegate void OnOrderbookFFI(ulong bidActualValueLen, ulong askActualValueLen, IntPtr market, ulong lastUpdateId, ulong updateId); + public delegate void OnTrades(TradesResponse trades); + private delegate void OnTradesFFI(ulong bidActualValueLen, IntPtr market); + private OnError onErrorCb; + private List onErrorCbs = new List(); + + private OnPing onPingCb; + private List onPingCbs = new List(); + private OnOrderbookFFI onOrderbookCb; + private OnTradesFFI onTradesCb; + + private Dictionary> onOrderbookCbs = new Dictionary>(); + private Dictionary> onTradesCbs = new Dictionary>(); + + private OnDisconnect onDisconnectCb; + private List onDisconnectCbs = new List(); + + + + const string NativeLib = "libopenlimits_sharp"; + + unsafe private IntPtr _client_handle; + unsafe private IntPtr _sub_handle; + + [DllImport(NativeLib, EntryPoint = "free_string", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern void FreeStringInternal(IntPtr handle); + static public void FreeString(IntPtr handle) { + if (handle.ToInt64() == 0) { + return; + } + FreeStringInternal(handle); + } + + + [DllImport(NativeLib, EntryPoint = "disconnect", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe internal static extern void Disconnect(IntPtr subhandle); + + [DllImport(NativeLib, EntryPoint = "init_binance", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult InitBinance(BinanceClientConfig config, out IntPtr client); + + [DllImport(NativeLib, EntryPoint = "init_nash", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult InitNash(string apikey, string secret, ulong clientid, NashEnvironment environment, ulong timeout, string affiliateCode, out IntPtr client); + + + [DllImport(NativeLib, EntryPoint = "init_coinbase", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult InitCoinbase(string apikey, string secret, string passphrase, bool sandbox, out IntPtr client); + + + [DllImport(NativeLib, EntryPoint = "init_subscriptions", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult InitCbs(IntPtr client, + OnError onError, OnPing onPing, OnOrderbookFFI onOrderbook, OnTradesFFI onTrades, OnDisconnect onDisconnect, + IntPtr bidBuffPtr, UIntPtr bidBufLen, + IntPtr askBuffPtr, UIntPtr askBufLen, + IntPtr taskBuffPtr, UIntPtr tradeBufLen, + out IntPtr subhandle + ); + + + [DllImport(NativeLib, EntryPoint = "order_book", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult Orderbook(IntPtr client, string market, + IntPtr bidBuffPtr, ulong bidBufLen, out ulong bidActualValueLen, + IntPtr askBuffPtr, ulong AskBufLen, out ulong askActualValueLen, + out ulong lastUpdateId, + out ulong updateId + ); + + [DllImport(NativeLib, EntryPoint = "get_price_ticker", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult GetPriceTicker(IntPtr client, string market, out double price); + + [DllImport(NativeLib, EntryPoint = "get_historic_rates", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult GetHistoricRates(IntPtr client, string market, Interval interval, Paginator paginator, + IntPtr buffPtr, UIntPtr valueBufLen, out UIntPtr actualValueLen + ); + + [DllImport(NativeLib, EntryPoint = "get_historic_trades", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult GetHistoricTrades(IntPtr client, string market, Paginator paginator, + IntPtr buffPtr, UIntPtr valueBufLen, out UIntPtr actualValueLen + ); + + [DllImport(NativeLib, EntryPoint = "place_order", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult PlaceOrder(IntPtr client, string market, + string qty, + bool limit, + string price, + Side side, + TimeInForce tif, + ulong tifDuration, + bool postOnly, + out FFIOrder order + ); + + [DllImport(NativeLib, EntryPoint = "get_all_open_orders", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult GetAllOpenOrders(IntPtr client, + IntPtr buffPtr, UIntPtr valueBufLen, out UIntPtr actualValueLen + ); + + [DllImport(NativeLib, EntryPoint = "subscribe_orderbook", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult SubscribeToOrderbook(IntPtr client, IntPtr subhandle, string market); + + [DllImport(NativeLib, EntryPoint = "subscribe_trades", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult SubscribeToTrades(IntPtr client, IntPtr subhandle, string market); + + [DllImport(NativeLib, EntryPoint = "get_order_history", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult GetOrderHistory(IntPtr client, + string market, Paginator paginator, + IntPtr buffPtr, UIntPtr valueBufLen, out UIntPtr actualValueLen + ); + + [DllImport(NativeLib, EntryPoint = "get_trade_history", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult GetTradeHistory(IntPtr client, + string market, string orderId, Paginator paginator, + IntPtr buffPtr, UIntPtr valueBufLen, out UIntPtr actualValueLen + ); + + [DllImport(NativeLib, EntryPoint = "get_account_balances", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult GetAccountBalances(IntPtr client, + Paginator paginator, + IntPtr buffPtr, UIntPtr valueBufLen, out UIntPtr actualValueLen + ); + + + [DllImport(NativeLib, EntryPoint = "cancel_all_orders", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult CancelAllOrders(IntPtr client, string market, IntPtr buffPtr, UIntPtr valueBufLen, out UIntPtr actualValueLen); + + [DllImport(NativeLib, EntryPoint = "cancel_order", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult CancelOrder(IntPtr client, string orderId, string market); + + + [DllImport(NativeLib, EntryPoint = "get_order", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult GetOrder(IntPtr client, string orderId, string market, out FFIOrder result); + + [DllImport(NativeLib, EntryPoint = "receive_pairs", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + unsafe private static extern FFIResult ReceivePairs(IntPtr client, IntPtr buffPtr, UIntPtr valueBufLen, out UIntPtr actualValueLen); + + private void handleFFIResult(FFIResult result) { + } + private void onPingHandler() { + foreach(var callback in this.onPingCbs) { + callback(); + } + } + private void onErrorHandler() { + foreach(var callback in this.onErrorCbs) { + callback(); + } + } + unsafe private void onTradesHandler(ulong tradeBuffLen, IntPtr marketStr) { + var market = CString.ToString(marketStr); + FreeString(marketStr); + var tradesList = new List(); + + for (int i = 0 ; i < (int)tradeBuffLen ; i ++) { + tradesList.Add(subTradesBuff[i].ToTrade()); + subTradesBuff[i].Dispose(); + } + + if (!onTradesCbs.ContainsKey(market)) { + return; + } + var trades = new TradesResponse(market, tradesList); + this.onTradesCbs.TryGetValue(market, out var callbacks); + foreach(var callback in callbacks) { + callback(trades); + } + } + unsafe private void onOrderbookHandler(ulong bidActualValueLen, ulong askActualValueLen, IntPtr marketStr, ulong lastUpdateId, ulong updateId) { + var market = CString.ToString(marketStr); + FreeString(marketStr); + + var bidsList = new List(); + var asksList = new List(); + + + for (int i = 0 ; i < (int)bidActualValueLen ; i ++) { + bidsList.Add(subBidsBuff[i].ToAskBid()); + subBidsBuff[i].Dispose(); + } + + for (int i = 0 ; i < (int)askActualValueLen ; i ++) { + asksList.Add(subAsksBuff[i].ToAskBid()); + subAsksBuff[i].Dispose(); + } + + if (!onOrderbookCbs.ContainsKey(market)) { + return; + } + var latestOrderbook = new OrderbookResponse( + market, + asksList, + bidsList, + lastUpdateId, + updateId + ); + + this.onOrderbookCbs.TryGetValue(market, out var callbacks); + foreach(var callback in callbacks) { + callback(latestOrderbook); + } + } + EventWaitHandle ewh = new EventWaitHandle(false, EventResetMode.ManualReset); + Thread ewhThreadHandle = null; + private void onDisconnect() { + ewh.Set(); + _clients.Remove(this); + + foreach(var callback in this.onDisconnectCbs) { + callback(); + } + } + + unsafe private IntPtr InitCbs() { + _clients.Add(this); + fixed (FFIAskBid* bidBuff = subBidsBuff.AsSpan()) { + fixed (FFIAskBid* askBuff = subAsksBuff.AsSpan()) { + fixed (FFITrade* tradeBuff = subTradesBuff.AsSpan()) { + this.onOrderbookCb = this.onOrderbookHandler; + this.onTradesCb = this.onTradesHandler; + this.onPingCb = this.onPingHandler; + this.onErrorCb = this.onErrorHandler; + this.onTradesCb = this.onTradesHandler; + this.onDisconnectCb = this.onDisconnect; + InitCbs( + _client_handle, + this.onErrorCb, + this.onPingCb, + this.onOrderbookCb, + this.onTradesCb, + this.onDisconnectCb, + + (IntPtr)bidBuff, (UIntPtr)subBidsBuff.Length, + (IntPtr)askBuff, (UIntPtr)subAsksBuff.Length, + (IntPtr)tradeBuff, (UIntPtr)subTradesBuff.Length, + out var handle + ); + return handle; + } + } + } + } + + unsafe public ExchangeClient(BinanceClientConfig config) { + handleResult( + ExchangeClient.InitBinance(config, out var client_handle) + ); + + _client_handle = client_handle; + _sub_handle = InitCbs(); + } + + unsafe public ExchangeClient(NashClientConfig config) { + handleResult( + ExchangeClient.InitNash(config.apikey, config.secret, config.clientId, config.environment, config.timeout, config.affiliateCode, out var client_handle) + ); + _client_handle = client_handle; + _sub_handle = InitCbs(); + } + unsafe public ExchangeClient(CoinbaseClientConfig config) { + handleResult( + ExchangeClient.InitCoinbase(config.apikey, config.secret, config.passphrase, config.sandbox, out var client_handle) + ); + _client_handle = client_handle; + _sub_handle = InitCbs(); + } + + unsafe public double GetPriceTicker(string market) { + var result = ExchangeClient.GetPriceTicker(_client_handle, market, out double price); + return price; + } + unsafe public OrderbookResponse Orderbook(string market) { + var bids = new FFIAskBid[512]; + var asks = new FFIAskBid[512]; + var bidsLen = bids.Length; + var asksLen = asks.Length; + var bidsList = new List(); + var asksList = new List(); + ulong lastUpdateId = 0; + ulong updateId = 0; + + fixed (FFIAskBid* bidBuff = bids.AsSpan()) { + fixed (FFIAskBid* askBuff = asks.AsSpan()) { + handleResult( + ExchangeClient.Orderbook( + _client_handle, + market, + (IntPtr)bidBuff, (ulong) bidsLen, out var actualBidsLen, + (IntPtr)askBuff, (ulong) asksLen, out var actualAsksLen, + out lastUpdateId, + out updateId + ) + ); + for (int i = 0 ; i < Math.Min(bidsLen, (int)actualBidsLen) ; i ++) { + bidsList.Add(bids[i].ToAskBid()); + bids[i].Dispose(); + } + for (int i = 0 ; i < Math.Min(asksLen, (int)actualAsksLen) ; i ++) { + asksList.Add(asks[i].ToAskBid()); + asks[i].Dispose(); + } + } + } + + return new OrderbookResponse( + market, + asksList, + bidsList, + lastUpdateId, + updateId + ); + } + + unsafe public IEnumerable GetHistoricRates(GetHistoricRatesRequest req) { + var limit = req.paginator == null ? 0 : req.paginator.limit; + var candles = new Candle[Math.Max(limit, 256)]; + var candlesLen = candles.Length; + var candlesList = new List(); + + + fixed (Candle* candleBuff = candles.AsSpan()) { + handleResult(ExchangeClient.GetHistoricRates( + _client_handle, + req.market, req.interval, req.paginator, + (IntPtr)candleBuff, (UIntPtr)candlesLen, out var actualCandleLen + )); + for (int i = 0 ; i < (int)actualCandleLen ; i ++) { + candlesList.Add(candles[i]); + } + } + + return candlesList; + } + unsafe public IEnumerable GetHistoricTrades(GetHistoricTradesRequest req) { + var limit = req.paginator == null ? 0 : req.paginator.limit; + var trades = new FFITrade[Math.Max(limit, 256)]; + var tradesLen = trades.Length; + var tradesList = new List(); + + + fixed (FFITrade* tradeBuff = trades.AsSpan()) { + handleResult(ExchangeClient.GetHistoricTrades( + _client_handle, + req.market, + req.paginator, + (IntPtr)tradeBuff, (UIntPtr)tradesLen, out var actualTradeLen + )); + for (int i = 0 ; i < (int)actualTradeLen ; i ++) { + tradesList.Add(trades[i].ToTrade()); + trades[i].Dispose(); + } + } + + return tradesList; + } + + + unsafe public Order LimitBuy(LimitOrderRequest request) { + handleResult(ExchangeClient.PlaceOrder( + _client_handle, + request.market, + request.size, + true, + request.price, + Side.Buy, + request.timeInForce, + request.timeInForceDurationMs, + request.postOnly, + out FFIOrder ffiOrder + )); + var order = ffiOrder.ToOrder(); + ffiOrder.Dispose(); + return order; + } + unsafe public Order LimitSell(LimitOrderRequest request) { + handleResult(ExchangeClient.PlaceOrder( + _client_handle, + request.market, + request.size, + true, + request.price, + Side.Sell, + request.timeInForce, + request.timeInForceDurationMs, + request.postOnly, + out FFIOrder ffiOrder + )); + var order = ffiOrder.ToOrder(); + ffiOrder.Dispose(); + return order; + } + + unsafe public Order MarketBuy(MarketOrderRequest request) { + handleResult(ExchangeClient.PlaceOrder( + _client_handle, + request.market, + request.size, + false, + null, + Side.Buy, + TimeInForce.GTC, + 0, + false, + out FFIOrder ffiOrder + )); + var order = ffiOrder.ToOrder(); + ffiOrder.Dispose(); + return order; + } + + unsafe public void CancelOrder(string orderId, string market) { + handleResult(ExchangeClient.CancelOrder( + _client_handle, + orderId, + market + )); + } + + unsafe public Order GetOrder(string orderId, string market) { + handleResult(ExchangeClient.GetOrder( + _client_handle, + orderId, + market, + out var result + )); + + var order = result.ToOrder(); + result.Dispose(); + return order; + } + + unsafe public void CancelOrder(string orderId) { + CancelOrder(orderId, null); + } + + unsafe public Order MarketSell(MarketOrderRequest request) { + handleResult(ExchangeClient.PlaceOrder( + _client_handle, + request.market, + request.size, + false, + null, + Side.Sell, + TimeInForce.GTC, + 0, + false, + out FFIOrder ffiOrder + )); + var order = ffiOrder.ToOrder(); + ffiOrder.Dispose(); + return order; + } + unsafe public IEnumerable GetAllOpenOrders() { + var orders = new FFIOrder[256]; + var ordersLen = orders.Length; + var ordersList = new List(); + + + fixed (FFIOrder* orderBuff = orders.AsSpan()) { + handleResult(ExchangeClient.GetAllOpenOrders( + _client_handle, + (IntPtr)orderBuff, (UIntPtr)ordersLen, out var actualCandleLen + )); + for (int i = 0 ; i < (int)actualCandleLen ; i ++) { + ordersList.Add(orderBuff[i].ToOrder()); + orderBuff[i].Dispose(); + } + } + + return ordersList; + } + + unsafe public IEnumerable GetOrderHistory(GetOrderHistoryRequest req) { + var limit = req.paginator == null ? 0 : req.paginator.limit; + var orders = new FFIOrder[Math.Max(limit, 256)]; + var ordersLen = orders.Length; + var ordersList = new List(); + + + fixed (FFIOrder* orderBuff = orders.AsSpan()) { + handleResult(ExchangeClient.GetOrderHistory( + _client_handle, + req.market, req.paginator, + (IntPtr)orderBuff, (UIntPtr)ordersLen, out var actualCandleLen + )); + for (int i = 0 ; i < (int)actualCandleLen ; i ++) { + ordersList.Add(orderBuff[i].ToOrder()); + orderBuff[i].Dispose(); + } + } + + return ordersList; + } + + unsafe public IEnumerable GetTradeHistory(GetTradeHistoryRequest req) { + var limit = req.paginator == null ? 0 : req.paginator.limit; + var trades = new FFITrade[Math.Max(limit, 256)]; + var tradesLen = trades.Length; + var tradesList = new List(); + + + fixed (FFITrade* tradeBuff = trades.AsSpan()) { + handleResult(ExchangeClient.GetTradeHistory( + _client_handle, + req.market, req.orderId, req.paginator, + (IntPtr)tradeBuff, (UIntPtr)tradesLen, out var actualCandleLen + )); + for (int i = 0 ; i < (int)actualCandleLen ; i ++) { + tradesList.Add(tradeBuff[i].ToTrade()); + tradeBuff[i].Dispose(); + } + } + + return tradesList; + } + + unsafe public IEnumerable GetAccountBalances(Paginator paginator) { + var limit = paginator == null ? 0 : paginator.limit; + var balances = new FFIBalance[Math.Max(limit, 256)]; + var balancesLen = balances.Length; + var balancesList = new List(); + + + fixed (FFIBalance* balanceBuff = balances.AsSpan()) { + handleResult(ExchangeClient.GetAccountBalances( + _client_handle, + paginator, + (IntPtr)balanceBuff, (UIntPtr)balancesLen, out var actualCandleLen + )); + for (int i = 0 ; i < (int)actualCandleLen ; i ++) { + balancesList.Add(balanceBuff[i].ToBalance()); + balanceBuff[i].Dispose(); + } + } + + return balancesList; + } + public IEnumerable GetAccountBalances() { + return this.GetAccountBalances(null); + } + + unsafe public IEnumerable CancelAllOrders(string market) { + var orders = new IntPtr[1024]; + var ordersLen = orders.Length; + var cancelledOrdersList = new List(); + fixed (IntPtr* orderBuff = orders.AsSpan()) { + handleResult(ExchangeClient.CancelAllOrders( + _client_handle, + market, + (IntPtr)orderBuff, (UIntPtr)ordersLen, out var actualLen + )); + for (int i = 0 ; i < (int)actualLen ; i ++) { + cancelledOrdersList.Add(CString.ToString(orders[i])); + ExchangeClient.FreeString(orders[i]); + } + } + return cancelledOrdersList; + } + + unsafe public IEnumerable ReceivePairs() { + var marketPairs = new FFIMarketPair[1024]; + var marketPairsLen = marketPairs.Length; + var pairs = new List(); + fixed (FFIMarketPair* buff = marketPairs.AsSpan()) { + handleResult(ExchangeClient.ReceivePairs( + _client_handle, + (IntPtr)buff, (UIntPtr)marketPairsLen, out var actualLen + )); + for (int i = 0 ; i < (int)actualLen ; i ++) { + pairs.Add(marketPairs[i].ToMarketPair()); + marketPairs[i].Dispose(); + } + } + return pairs; + } + + private void WaitForEwh() { + ewh.WaitOne(); + } + + private void SetupEWH() { + if (ewhThreadHandle != null) { + return; + } + + ewhThreadHandle = new Thread(this.WaitForEwh); + ewhThreadHandle.Start(); + } + + public void Listen( + OnError onError, + OnPing onPing + ) { + this.onErrorCbs.Add(onError); + this.onPingCbs.Add(onPing); + + this.SetupEWH(); + + } + + unsafe public void SubscribeToOrderbook(string market, OnOrderbook onOrderbook) { + if (!this.onOrderbookCbs.ContainsKey(market)) { + this.onOrderbookCbs.Add(market, new List()); + } + this.onOrderbookCbs.TryGetValue(market, out var callbacks); + callbacks.Add(onOrderbook); + handleFFIResult(SubscribeToOrderbook(this._client_handle, this._sub_handle, market)); + this.SetupEWH(); + } + unsafe public void SubscribeToTrades(string market, OnTrades onTrades) { + if (!this.onTradesCbs.ContainsKey(market)) { + this.onTradesCbs.Add(market, new List()); + } + this.onTradesCbs.TryGetValue(market, out var callbacks); + callbacks.Add(onTrades); + handleFFIResult(SubscribeToTrades(this._client_handle, this._sub_handle, market)); + this.SetupEWH(); + } + + public void SubscribeToDisconnect(OnDisconnect cb) { + this.onDisconnectCbs.Add(cb); + } + + unsafe public void Disconnect() { + Disconnect(_sub_handle); + } + } +} diff --git a/bindings/csharp/FFIResult.cs b/bindings/csharp/FFIResult.cs new file mode 100644 index 00000000..4cc4737d --- /dev/null +++ b/bindings/csharp/FFIResult.cs @@ -0,0 +1,143 @@ +namespace OpenLimits +{ + + using System; + using System.Runtime.InteropServices; + + // Used to store a result from rust to C# + [StructLayout(LayoutKind.Sequential)] + internal struct FFIResult { + public ResultTag tag; + public IntPtr message; + } + + // Contains the different error types + internal enum ResultTag { + Ok, + InvalidArgument, + BinanceError, + CoinbaseError, + NashProtocolError, + MissingImplementation, + AssetNotFound, + NoApiKeySet, + InternalServerError, + ServiceUnavailable, + Unauthorized, + SymbolNotFound, + SocketError, + GetTimestampFailed, + ReqError, + InvalidHeaderError, + InvalidPayloadSignature, + IoError, + PoisonError, + JsonError, + ParseFloatError, + UrlParserError, + Tungstenite, + TimestampError, + UnkownResponse, + NotParsableResponse, + MissingParameter, + WebSocketMessageNotSupported, + + InitializeException, + SubscribeException, + NoMarketPair + } + + public class OpenLimitsError: Exception { + public OpenLimitsError(string message): base(message) { } + + } + + public class BinanceError : OpenLimitsError { + public BinanceError(string message): base(message) { } + }; + public class CoinbaseError : OpenLimitsError { + public CoinbaseError(string message): base(message) { } + }; + public class NashProtocolError : OpenLimitsError { + public NashProtocolError(string message): base(message) { } + }; + public class MissingImplementation : OpenLimitsError { + public MissingImplementation(string message): base(message) { } + }; + public class AssetNotFound : OpenLimitsError { + public AssetNotFound(string message): base(message) { } + }; + public class NoApiKeySet : OpenLimitsError { + public NoApiKeySet(string message): base(message) { } + }; + public class InternalServerError : OpenLimitsError { + public InternalServerError(string message): base(message) { } + }; + public class ServiceUnavailable : OpenLimitsError { + public ServiceUnavailable(string message): base(message) { } + }; + public class Unauthorized : OpenLimitsError { + public Unauthorized(string message): base(message) { } + }; + public class SymbolNotFound : OpenLimitsError { + public SymbolNotFound(string message): base(message) { } + }; + public class SocketError : OpenLimitsError { + public SocketError(string message): base(message) { } + }; + public class GetTimestampFailed : OpenLimitsError { + public GetTimestampFailed(string message): base(message) { } + }; + public class ReqError : OpenLimitsError { + public ReqError(string message): base(message) { } + }; + public class InvalidHeaderError : OpenLimitsError { + public InvalidHeaderError(string message): base(message) { } + }; + public class InvalidPayloadSignature : OpenLimitsError { + public InvalidPayloadSignature(string message): base(message) { } + }; + public class IoError : OpenLimitsError { + public IoError(string message): base(message) { } + }; + public class PoisonError : OpenLimitsError { + public PoisonError(string message): base(message) { } + }; + public class JsonError : OpenLimitsError { + public JsonError(string message): base(message) { } + }; + public class ParseFloatError : OpenLimitsError { + public ParseFloatError(string message): base(message) { } + }; + public class UrlParserError : OpenLimitsError { + public UrlParserError(string message): base(message) { } + }; + public class Tungstenite : OpenLimitsError { + public Tungstenite(string message): base(message) { } + }; + public class TimestampError : OpenLimitsError { + public TimestampError(string message): base(message) { } + }; + public class UnkownResponse : OpenLimitsError { + public UnkownResponse(string message): base(message) { } + }; + public class NotParsableResponse : OpenLimitsError { + public NotParsableResponse(string message): base(message) { } + }; + public class MissingParameter : OpenLimitsError { + public MissingParameter(string message): base(message) { } + }; + public class WebSocketMessageNotSupported : OpenLimitsError { + public WebSocketMessageNotSupported(string message): base(message) { } + }; + + public class InitializeException : OpenLimitsError { + public InitializeException(string message): base(message) { } + }; + public class SubscribeException : OpenLimitsError { + public SubscribeException(string message): base(message) { } + }; + public class NoMarketPair : OpenLimitsError { + public NoMarketPair(string message): base(message) { } + }; +} \ No newline at end of file diff --git a/bindings/csharp/GetHistoricRatesRequest.cs b/bindings/csharp/GetHistoricRatesRequest.cs new file mode 100644 index 00000000..2a32d24a --- /dev/null +++ b/bindings/csharp/GetHistoricRatesRequest.cs @@ -0,0 +1,44 @@ +namespace OpenLimits +{ + using System.Runtime.InteropServices; + + public enum Interval { + OneMinute, + ThreeMinutes, + FiveMinutes, + FifteenMinutes, + ThirtyMinutes, + OneHour, + TwoHours, + FourHours, + SixHours, + EightHours, + TwelveHours, + OneDay, + ThreeDays, + OneWeek, + OneMonth, + } + + [StructLayout(LayoutKind.Sequential)] + public struct GetHistoricRatesRequest + { + public readonly string market; + public readonly Interval interval; + public readonly Paginator paginator; + + public GetHistoricRatesRequest(string market, Interval interval, Paginator paginator) + { + this.market = market; + this.interval = interval; + this.paginator = paginator; + } + + public GetHistoricRatesRequest(string market, Interval interval) + { + this.market = market; + this.interval = interval; + this.paginator = null; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/GetHistoricTradesRequest.cs b/bindings/csharp/GetHistoricTradesRequest.cs new file mode 100644 index 00000000..93fc688f --- /dev/null +++ b/bindings/csharp/GetHistoricTradesRequest.cs @@ -0,0 +1,22 @@ +namespace OpenLimits +{ + using System.Runtime.InteropServices; + [StructLayout(LayoutKind.Sequential, Pack=1)] + public struct GetHistoricTradesRequest + { + public readonly string market; + public readonly Paginator paginator; + + public GetHistoricTradesRequest(string market, Paginator paginator) + { + this.market = market; + this.paginator = paginator; + } + + public GetHistoricTradesRequest(string market) + { + this.market = market; + this.paginator = null; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/GetOrderHistoryRequest.cs b/bindings/csharp/GetOrderHistoryRequest.cs new file mode 100644 index 00000000..487738a1 --- /dev/null +++ b/bindings/csharp/GetOrderHistoryRequest.cs @@ -0,0 +1,20 @@ +namespace OpenLimits +{ + public struct GetOrderHistoryRequest + { + public readonly string market; + public readonly Paginator paginator; + + public GetOrderHistoryRequest(string market, Paginator paginator) + { + this.market = market; + this.paginator = paginator; + } + + public GetOrderHistoryRequest(string market) + { + this.market = market; + this.paginator = null; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/GetTradeHistoryRequest.cs b/bindings/csharp/GetTradeHistoryRequest.cs new file mode 100644 index 00000000..6b04a187 --- /dev/null +++ b/bindings/csharp/GetTradeHistoryRequest.cs @@ -0,0 +1,23 @@ +namespace OpenLimits +{ + public struct GetTradeHistoryRequest + { + public readonly string market; + public readonly string orderId; + public readonly Paginator paginator; + + public GetTradeHistoryRequest(string market, string orderId, Paginator paginator) + { + this.market = market; + this.orderId = orderId; + this.paginator = paginator; + } + + public GetTradeHistoryRequest(string market, string orderId) + { + this.market = market; + this.orderId = orderId; + this.paginator = null; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/LICENSE.txt b/bindings/csharp/LICENSE.txt new file mode 100644 index 00000000..dcdf74a7 --- /dev/null +++ b/bindings/csharp/LICENSE.txt @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2020, openlimits +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/bindings/csharp/LimitOrderRequest.cs b/bindings/csharp/LimitOrderRequest.cs new file mode 100644 index 00000000..41c7a785 --- /dev/null +++ b/bindings/csharp/LimitOrderRequest.cs @@ -0,0 +1,48 @@ +namespace OpenLimits +{ + public struct LimitOrderRequest + { + public readonly string price; + public readonly string size; + public readonly string market; + public readonly TimeInForce timeInForce; + public readonly ulong timeInForceDurationMs; + public readonly bool postOnly; + + public LimitOrderRequest(string price, string size, string market, TimeInForce timeInForce, ulong timeInForceDurationMs, bool postOnly) + { + this.price = price; + this.size = size; + this.market = market; + this.timeInForce = timeInForce; + this.timeInForceDurationMs = timeInForceDurationMs; + this.postOnly = postOnly; + } + + public static LimitOrderRequest immediateOrCancel(string price, string size, string market) { + return new LimitOrderRequest(price, size, market, TimeInForce.IOC, 0, false); + } + public static LimitOrderRequest goodTillCancelled(string price, string size, string market) { + return new LimitOrderRequest(price, size, market, TimeInForce.GTC, 0, false); + } + public static LimitOrderRequest fillOrKill(string price, string size, string market) { + return new LimitOrderRequest(price, size, market, TimeInForce.FOK, 0, false); + } + public static LimitOrderRequest goodTillTIme(string price, string size, string market, ulong timeInForceDurationMs) { + return new LimitOrderRequest(price, size, market, TimeInForce.GTT, timeInForceDurationMs, false); + } + + public static LimitOrderRequest immediateOrCancelPostOnly(string price, string size, string market) { + return new LimitOrderRequest(price, size, market, TimeInForce.IOC, 0, true); + } + public static LimitOrderRequest goodTillCancelledPostOnly(string price, string size, string market) { + return new LimitOrderRequest(price, size, market, TimeInForce.GTC, 0, true); + } + public static LimitOrderRequest fillOrKillPostOnly(string price, string size, string market) { + return new LimitOrderRequest(price, size, market, TimeInForce.FOK, 0, true); + } + public static LimitOrderRequest goodTillTImePostOnly(string price, string size, string market, ulong timeInForceDurationMs) { + return new LimitOrderRequest(price, size, market, TimeInForce.GTT, timeInForceDurationMs, true); + } + } +} \ No newline at end of file diff --git a/bindings/csharp/Liquidity.cs b/bindings/csharp/Liquidity.cs new file mode 100644 index 00000000..82bbd2dc --- /dev/null +++ b/bindings/csharp/Liquidity.cs @@ -0,0 +1,9 @@ +namespace OpenLimits +{ + public enum Liquidity + { + Unknown, + Maker, + Taker + } +} \ No newline at end of file diff --git a/bindings/csharp/MarketOrderRequest.cs b/bindings/csharp/MarketOrderRequest.cs new file mode 100644 index 00000000..ffa6bdc8 --- /dev/null +++ b/bindings/csharp/MarketOrderRequest.cs @@ -0,0 +1,14 @@ +namespace OpenLimits +{ + public struct MarketOrderRequest + { + public readonly string size; + public readonly string market; + + public MarketOrderRequest(string size, string market) + { + this.size = size; + this.market = market; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/MarketPair.cs b/bindings/csharp/MarketPair.cs new file mode 100644 index 00000000..89823836 --- /dev/null +++ b/bindings/csharp/MarketPair.cs @@ -0,0 +1,61 @@ +namespace OpenLimits +{ + using System.Runtime.InteropServices; + using System; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct FFIMarketPair + { + public readonly IntPtr baseSymbol; + public readonly IntPtr quote; + public readonly IntPtr symbol; + public readonly IntPtr baseIncrement; + public readonly IntPtr quoteIncrement; + public readonly IntPtr baseMinPrice; + public readonly IntPtr quoteMinPrice; + + public void Dispose() { + ExchangeClient.FreeString(baseSymbol); + ExchangeClient.FreeString(quote); + ExchangeClient.FreeString(symbol); + ExchangeClient.FreeString(baseIncrement); + ExchangeClient.FreeString(quoteIncrement); + ExchangeClient.FreeString(baseMinPrice); + ExchangeClient.FreeString(quoteMinPrice); + } + + public MarketPair ToMarketPair() { + return new MarketPair( + CString.ToString(this.baseSymbol), + CString.ToString(this.quote), + CString.ToString(this.symbol), + CString.ToString(this.baseIncrement), + CString.ToString(this.quoteIncrement), + CString.ToString(this.baseMinPrice), + CString.ToString(this.quoteMinPrice) + ); + } + } + + public struct MarketPair + { + public readonly string baseSymbol; + public readonly string quote; + public readonly string symbol; + public readonly string baseIncrement; + public readonly string quoteIncrement; + public readonly string baseMinPrice; + public readonly string quoteMinPrice; + + public MarketPair(string baseSymbol, string quote, string symbol, string baseIncrement, string quoteIncrement, string baseMinPrice, string quoteMinPrice) + { + this.baseSymbol = baseSymbol; + this.quote = quote; + this.symbol = symbol; + this.baseIncrement = baseIncrement; + this.quoteIncrement = quoteIncrement; + this.baseMinPrice = baseMinPrice; + this.quoteMinPrice = quoteMinPrice; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/NashClientConfig.cs b/bindings/csharp/NashClientConfig.cs new file mode 100644 index 00000000..857e57b9 --- /dev/null +++ b/bindings/csharp/NashClientConfig.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +namespace OpenLimits +{ + public enum NashEnvironment { + Sandbox, + Production + } + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct NashClientConfig + { + public readonly string apikey; + public readonly string secret; + public readonly string affiliateCode; + + public readonly ulong clientId; + public readonly NashEnvironment environment; + public readonly ulong timeout; + + private NashClientConfig(string apikey, string secret, ulong clientId, NashEnvironment environment, ulong timeout, string affiliateCode) + { + this.apikey = apikey; + this.secret = secret; + this.clientId = clientId; + this.environment = environment; + this.timeout = timeout; + this.affiliateCode = affiliateCode; + } + + static public NashClientConfig Authenticated(string apikey, string secret, ulong clientId, NashEnvironment environment, ulong timeout) { + return new NashClientConfig(apikey, secret, clientId, environment, timeout, null); + } + + static public NashClientConfig Authenticated(string apikey, string secret, string affiliateCode, ulong clientId, NashEnvironment environment, ulong timeout) { + return new NashClientConfig(apikey, secret, clientId, environment, timeout, affiliateCode); + } + + static public NashClientConfig Unauthenticated(ulong clientId, NashEnvironment environment, ulong timeout) { + return new NashClientConfig(null, null, clientId, environment, timeout, null); + } + } +} \ No newline at end of file diff --git a/bindings/csharp/OpenLimits.csproj b/bindings/csharp/OpenLimits.csproj new file mode 100644 index 00000000..27f0948c --- /dev/null +++ b/bindings/csharp/OpenLimits.csproj @@ -0,0 +1,38 @@ + + + Openlimits + 0.1.15 + jankjr + nash + Nash;Openlimits;Trading;Cryptocurrency + + This is the offical openlimits c# wrapper. + + + + LICENSE.txt + + + netcoreapp5.0 + + + true + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/bindings/csharp/Order.cs b/bindings/csharp/Order.cs new file mode 100644 index 00000000..9de76df6 --- /dev/null +++ b/bindings/csharp/Order.cs @@ -0,0 +1,97 @@ +namespace OpenLimits +{ + using System; + using System.Globalization; + + internal struct FFIOrder + { + public readonly IntPtr id; + public readonly IntPtr marketPair; + public readonly IntPtr clientOrderId; + public readonly ulong createdAt; + public readonly OrderType orderType; + public readonly Side side; + public readonly OrderStatus status; + public readonly IntPtr size; + public readonly IntPtr price; + public readonly IntPtr remaining; + + public void Dispose() { + ExchangeClient.FreeString(id); + ExchangeClient.FreeString(marketPair); + ExchangeClient.FreeString(size); + ExchangeClient.FreeString(price); + ExchangeClient.FreeString(remaining); + } + + public Order ToOrder() { + return new Order( + CString.ToString(this.id), + CString.ToString(this.marketPair), + CString.ToString(this.clientOrderId), + this.createdAt, + this.orderType, + this.side, + this.status, + CString.ToString(this.size), + CString.ToString(this.price), + CString.ToString(this.remaining) + ); + } + } + + public struct Order + { + public readonly string id; + public readonly string marketPair; + public readonly string clientOrderId; + public readonly ulong createdAt; + public readonly OrderType orderType; + public readonly Side side; + public readonly OrderStatus status; + public readonly decimal size; + public readonly decimal? price; + public readonly decimal? remaining; + + public Order(string id, string marketPair, string clientOrderId, ulong createdAt, OrderType orderType, Side side, OrderStatus status, string size, string price, string remaining) + { + this.id = id; + this.marketPair = marketPair; + this.clientOrderId = clientOrderId; + this.createdAt = createdAt; + this.orderType = orderType; + this.side = side; + this.status = status; + + this.size = decimal.Parse(size, System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + this.price = price == null ? default(decimal?) : decimal.Parse(price, System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + this.remaining = remaining == null ? default(decimal?) : decimal.Parse(remaining, System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + } + + public override bool Equals(object obj) + { + return base.Equals(obj); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override string ToString() + { + return "Order{" + + "id='" + id + '\'' + + ", market='" + marketPair + '\'' + + ", clientOrderId='" + clientOrderId + '\'' + + ", createdAt=" + createdAt + + ", orderType='" + orderType + '\'' + + ", side='" + side + '\'' + + ", status='" + status + '\'' + + ", size='" + size + '\'' + + ", price='" + price + '\'' + + ", remaining='" + remaining + '\'' + + '}'; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/OrderStatus.cs b/bindings/csharp/OrderStatus.cs new file mode 100644 index 00000000..0e092d6e --- /dev/null +++ b/bindings/csharp/OrderStatus.cs @@ -0,0 +1,16 @@ +namespace OpenLimits +{ + public enum OrderStatus + { + New, + PartiallyFilled, + Filled, + Canceled, + PendingCancel, + Rejected, + Expired, + Open, + Pending, + Active, + } +} \ No newline at end of file diff --git a/bindings/csharp/OrderType.cs b/bindings/csharp/OrderType.cs new file mode 100644 index 00000000..76e95a61 --- /dev/null +++ b/bindings/csharp/OrderType.cs @@ -0,0 +1,11 @@ +namespace OpenLimits +{ + public enum OrderType + { + Limit, + Market, + StopLimit, + StopMarket, + Unknown, + } +} \ No newline at end of file diff --git a/bindings/csharp/Orderbook.cs b/bindings/csharp/Orderbook.cs new file mode 100644 index 00000000..8aed160d --- /dev/null +++ b/bindings/csharp/Orderbook.cs @@ -0,0 +1,29 @@ +namespace OpenLimits { + using System.Collections.Generic; + public class Orderbook { + private readonly Dictionary _bids = new Dictionary(); + private readonly Dictionary _asks = new Dictionary(); + + public void Update(OrderbookResponse changes) { + foreach(var ask in changes.asks) { + if (ask.qty == 0){ + if (_asks.ContainsKey(ask.price)) { + _asks.Remove(ask.price); + } + } else { + _asks.Add(ask.price, ask.qty); + } + } + + foreach(var bid in changes.bids) { + if (bid.qty == 0){ + if (_bids.ContainsKey(bid.price)) { + _bids.Remove(bid.price); + } + } else { + _bids.Add(bid.price, bid.qty); + } + } + } + } +} \ No newline at end of file diff --git a/bindings/csharp/OrderbookResponse.cs b/bindings/csharp/OrderbookResponse.cs new file mode 100644 index 00000000..b81775cf --- /dev/null +++ b/bindings/csharp/OrderbookResponse.cs @@ -0,0 +1,23 @@ +namespace OpenLimits +{ + using System.Collections.Generic; + + public class OrderbookResponse + { + readonly public string market; + readonly public IEnumerable asks; + readonly public IEnumerable bids; + + readonly public ulong lastUpdateId; + readonly public ulong updateId; + + public OrderbookResponse(string market, IEnumerable asks, IEnumerable bids, ulong lastUpdateId, ulong updateId) + { + this.market = market; + this.asks = asks; + this.bids = bids; + this.lastUpdateId = lastUpdateId; + this.updateId = updateId; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/Paginator.cs b/bindings/csharp/Paginator.cs new file mode 100644 index 00000000..beba137c --- /dev/null +++ b/bindings/csharp/Paginator.cs @@ -0,0 +1,22 @@ +namespace OpenLimits +{ + using System.Runtime.InteropServices; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public class Paginator + { + public readonly ulong startTime; + public readonly ulong endTime; + public readonly ulong limit; + public readonly string before; + public readonly string after; + + public Paginator(ulong startTime, ulong endTime, ulong limit, string before, string after) + { + this.startTime = startTime; + this.endTime = endTime; + this.limit = limit; + this.before = before; + this.after = after; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/Side.cs b/bindings/csharp/Side.cs new file mode 100644 index 00000000..fded14ce --- /dev/null +++ b/bindings/csharp/Side.cs @@ -0,0 +1,8 @@ +namespace OpenLimits +{ + public enum Side + { + Buy, + Sell + } +} \ No newline at end of file diff --git a/bindings/csharp/TimeInForce.cs b/bindings/csharp/TimeInForce.cs new file mode 100644 index 00000000..fc39877e --- /dev/null +++ b/bindings/csharp/TimeInForce.cs @@ -0,0 +1,10 @@ +namespace OpenLimits +{ + public enum TimeInForce + { + GTC, + FOK, + IOC, + GTT + } +} \ No newline at end of file diff --git a/bindings/csharp/Trade.cs b/bindings/csharp/Trade.cs new file mode 100644 index 00000000..fefed07f --- /dev/null +++ b/bindings/csharp/Trade.cs @@ -0,0 +1,91 @@ +namespace OpenLimits +{ + using System.Runtime.InteropServices; + using System; + using System.Globalization; + + + [StructLayout(LayoutKind.Sequential)] + internal struct FFITrade { + public readonly IntPtr id; + public readonly IntPtr buyerOrderId; + public readonly IntPtr sellerOrderId; + public readonly IntPtr marketPair; + + public readonly IntPtr price; + public readonly IntPtr qty; + public readonly IntPtr fees; + public readonly Side side; + public readonly Liquidity liquidity; + public readonly ulong createdAt; + + public void Dispose() { + ExchangeClient.FreeString(id); + ExchangeClient.FreeString(buyerOrderId); + ExchangeClient.FreeString(sellerOrderId); + ExchangeClient.FreeString(marketPair); + ExchangeClient.FreeString(price); + ExchangeClient.FreeString(qty); + ExchangeClient.FreeString(fees); + } + + public Trade ToTrade() { + return new Trade( + CString.ToString(this.id), + CString.ToString(this.buyerOrderId), + CString.ToString(this.sellerOrderId), + CString.ToString(this.marketPair), + CString.ToString(this.price), + CString.ToString(this.qty), + CString.ToString(this.fees), + this.side, + this.liquidity, + this.createdAt + ); + } + } + + public struct Trade { + public readonly string id; + public readonly string buyerOrderId; + public readonly string sellerOrderId; + public readonly string marketPair; + public readonly decimal price; + public readonly decimal qty; + public readonly decimal? fees; + + public readonly Side side; + public readonly Liquidity liquidity; + public readonly ulong createdAt; + + public Trade(string id, string buyerOrderId, string sellerOrderId, string marketPair, string price, string qty, string fees, Side side, Liquidity liquidity, ulong createdAt) + { + this.id = id; + this.buyerOrderId = buyerOrderId; + this.sellerOrderId = sellerOrderId; + this.marketPair = marketPair; + this.price = decimal.Parse(price, System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + this.qty = decimal.Parse(qty, System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + this.fees = fees == null ? default(decimal?): decimal.Parse(fees, System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + this.side = side; + this.liquidity = liquidity; + this.createdAt = createdAt; + } + + public override string ToString() + { + return "Trade{" + + "id='" + id + '\'' + + ", buyer_order_id='" + buyerOrderId + '\'' + + ", seller_order_id='" + sellerOrderId + '\'' + + ", market_pair='" + marketPair + '\'' + + ", price=" + price + + ", qty=" + qty + + ", fees=" + fees + + ", side='" + side + '\'' + + ", liquidity='" + liquidity + '\'' + + ", createdAt=" + createdAt + + '}'; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/TradesResponse.cs b/bindings/csharp/TradesResponse.cs new file mode 100644 index 00000000..69601802 --- /dev/null +++ b/bindings/csharp/TradesResponse.cs @@ -0,0 +1,16 @@ +namespace OpenLimits +{ + using System.Collections.Generic; + + public class TradesResponse + { + readonly public string market; + readonly public IEnumerable trades; + + public TradesResponse(string market, IEnumerable trades) + { + this.market = market; + this.trades = trades; + } + } +} \ No newline at end of file diff --git a/bindings/csharp/lib/Cargo.toml b/bindings/csharp/lib/Cargo.toml new file mode 100644 index 00000000..6b1d6c19 --- /dev/null +++ b/bindings/csharp/lib/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "openlimitssharp" +version = "0.1.0" +authors = ["kjr "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "openlimits_sharp" +crate-type = ["cdylib"] + +[dependencies] +tokio = { version = "0.2.22", features = ["full"] } +chrono = { version = "0.4.11" } +futures-util = "0.3" +thiserror = "1.0.22" +rust_decimal = "1.7.0" +openlimits = { path = "../../../" } diff --git a/bindings/csharp/lib/src/lib.rs b/bindings/csharp/lib/src/lib.rs new file mode 100644 index 00000000..4cf754c5 --- /dev/null +++ b/bindings/csharp/lib/src/lib.rs @@ -0,0 +1,1594 @@ + +#[no_mangle] +pub extern "cdecl" fn say_something() { + println!("Saying something"); +} + +// use std::{convert::{TryInto}}; +// use rust_decimal::prelude::*; +// use rust_decimal::Decimal; +// use chrono::Duration; +// use openlimits::{ +// OpenLimits, +// exchange::{ +// traits::{ +// ExchangeAccount, +// ExchangeMarketData, +// stream::OpenLimitsWs, +// info::{MarketPair, ExchangeInfoRetrieval} +// }, +// // nash::{ +// // NashCredentials, +// // NashParameters, +// // Environment +// // }, +// binance::{ +// BinanceCredentials, +// BinanceParameters, +// }, +// coinbase::{ +// CoinbaseCredentials, +// CoinbaseParameters, +// }, +// model::{ +// OrderBookRequest, +// GetOrderRequest, +// Liquidity, +// Side, +// CancelAllOrdersRequest, +// CancelOrderRequest, +// OrderType, +// AskBid, +// TimeInForce, +// OpenLimitOrderRequest, +// OrderStatus, +// OpenMarketOrderRequest, +// GetOrderHistoryRequest, +// TradeHistoryRequest, +// GetHistoricTradesRequest, +// GetHistoricRatesRequest, +// GetPriceTickerRequest, +// Paginator, +// Balance, +// Order, +// Trade, +// Interval, +// Candle, +// websocket::{Subscription, OpenLimitsWebSocketMessage, WebSocketResponse} +// } +// }, +// errors::OpenLimitsError, +// // any_exchange::{AnyExchange, InitAnyExchange, AnyWsExchange}, +// }; +// use tokio::stream::StreamExt; +// use std::{ffi::CStr, ffi::CString, os::raw::c_char}; +// use thiserror::Error; +// +// #[repr(u32)] +// #[derive(Debug, Copy, Clone)] +// pub enum FFIInterval { +// OneMinute, +// ThreeMinutes, +// FiveMinutes, +// FifteenMinutes, +// ThirtyMinutes, +// OneHour, +// TwoHours, +// FourHours, +// SixHours, +// EightHours, +// TwelveHours, +// OneDay, +// ThreeDays, +// OneWeek, +// OneMonth +// } +// +// #[repr(C)] +// #[derive(Debug, Copy, Clone)] +// pub struct FFIMarketPair { +// base: *mut c_char, +// quote: *mut c_char, +// symbol: *mut c_char, +// base_increment: *mut c_char, +// quote_increment: *mut c_char, +// base_min_price: *mut c_char, +// quote_min_price: *mut c_char, +// } +// +// fn interval_from_ffi_interval( +// interval: FFIInterval +// ) -> Result { +// #[allow(unreachable_patterns)] +// match interval { +// FFIInterval::OneMinute => Ok(Interval::OneMinute), +// FFIInterval::ThreeMinutes => Ok(Interval::ThreeMinutes), +// FFIInterval::FiveMinutes => Ok(Interval::FiveMinutes), +// FFIInterval::FifteenMinutes => Ok(Interval::FifteenMinutes), +// FFIInterval::ThirtyMinutes => Ok(Interval::ThirtyMinutes), +// FFIInterval::OneHour => Ok(Interval::OneHour), +// FFIInterval::TwoHours => Ok(Interval::TwoHours), +// FFIInterval::FourHours => Ok(Interval::FourHours), +// FFIInterval::SixHours => Ok(Interval::SixHours), +// FFIInterval::EightHours => Ok(Interval::EightHours), +// FFIInterval::TwelveHours => Ok(Interval::TwelveHours), +// FFIInterval::OneDay => Ok(Interval::OneDay), +// FFIInterval::ThreeDays => Ok(Interval::ThreeDays), +// FFIInterval::OneWeek => Ok(Interval::OneWeek), +// FFIInterval::OneMonth => Ok(Interval::OneMonth), +// _ => Err(format!("Invalid interval value {:?}", interval)) +// } +// } +// +// +// #[repr(C)] +// #[derive(Debug, Copy, Clone)] +// pub struct FFICandle { +// time: u64, +// low: f64, +// high: f64, +// open: f64, +// close: f64, +// volume: f64, +// } +// +// #[repr(C)] +// #[derive(Debug, Copy, Clone)] +// pub struct FFIPaginator { +// start_time: u64, +// end_time: u64, +// limit: u64, +// before: *mut c_char, +// after: *mut c_char, +// } +// +// +// fn string_to_c_str(s: String) -> *mut c_char { +// let cex = CString::new(s).expect("Failed to create CString!"); +// let raw = cex.into_raw(); +// // println!("Handling ownership of {:?} to c#", raw); +// +// raw +// } +// +// +// #[repr(C)] +// #[derive(Debug, Copy, Clone)] +// pub struct FFIBalance { +// asset: *mut c_char, +// total: *mut c_char, +// free: *mut c_char, +// } +// +// fn to_ffi_balance(b: Balance) -> FFIBalance { +// FFIBalance { +// asset: string_to_c_str(b.asset), +// total: string_to_c_str(b.total.to_string()), +// free: string_to_c_str(b.free.to_string()) +// } +// } +// +// fn market_pair_to_ffi(pair: MarketPair) -> FFIMarketPair { +// let base_min_price = pair.min_base_trade_size.map(|f|string_to_c_str(f.to_string())).unwrap_or(std::ptr::null_mut()); +// let quote_min_price = pair.min_quote_trade_size.map(|f|string_to_c_str(f.to_string())).unwrap_or(std::ptr::null_mut()); +// +// FFIMarketPair { +// base: string_to_c_str(pair.base), +// quote: string_to_c_str(pair.quote), +// symbol: string_to_c_str(pair.symbol), +// base_increment: string_to_c_str(pair.base_increment.to_string()), +// quote_increment: string_to_c_str(pair.quote_increment.to_string()), +// base_min_price, +// quote_min_price, +// } +// } +// +// fn c_str_to_string(s: *mut c_char) -> Result { +// let str = unsafe { CStr::from_ptr(s) }; +// str.to_str().map(String::from) +// } +// fn nullable_cstr(s: *mut c_char) -> Result, std::str::Utf8Error> { +// if s.is_null() { +// Ok(None) +// } else { +// c_str_to_string(s).map(Some) +// } +// } +// +// +// impl TryInto for FFIPaginator { +// type Error = std::str::Utf8Error; +// fn try_into(self) -> Result { +// Ok( +// Paginator { +// start_time: match self.start_time { 0 => None, v => Some(v) }, +// end_time: match self.end_time { 0 => None, v => Some(v) }, +// limit: match self.limit { 0 => None, v => Some(v) }, +// before: nullable_cstr(self.before)?, +// after: nullable_cstr(self.after)?, +// } +// ) +// } +// } +// +// +// #[derive(Error, Debug)] +// pub enum OpenlimitsSharpError { +// #[error("Invalid argument {0}")] +// InvalidArgument(String), +// #[error("Failed to initialize: {0}")] +// InitializeException(String), +// #[error("Failed to subscribe: {0}")] +// SubscribeException(String), +// #[error("{0}")] +// OpenLimitsError(#[from] OpenLimitsError) +// } +// +// #[repr(u32)] +// #[derive(Debug, Copy, Clone)] +// pub enum OpenLimitsResultTag { +// Ok, +// InvalidArgument, +// BinanceError, +// CoinbaseError, +// NashProtocolError, +// MissingImplementation, +// AssetNotFound, +// NoApiKeySet, +// InternalServerError, +// ServiceUnavailable, +// Unauthorized, +// SymbolNotFound, +// SocketError, +// GetTimestampFailed, +// ReqError, +// InvalidHeaderError, +// InvalidPayloadSignature, +// IoError, +// PoisonError, +// JsonError, +// ParseFloatError, +// UrlParserError, +// Tungstenite, +// TimestampError, +// UnkownResponse, +// NotParsableResponse, +// MissingParameter, +// +// WebSocketMessageNotSupported, +// +// InitializeException, +// SubscribeException, +// NoMarketPair +// } +// +// #[repr(C)] +// #[derive(Debug, Copy, Clone)] +// pub struct OpenLimitsResult { +// tag: OpenLimitsResultTag, +// message: *mut c_char +// } +// +// fn result_to_ffi(r: Result<(), OpenlimitsSharpError>) -> OpenLimitsResult { +// match r { +// Ok(_) => OpenLimitsResult { tag: OpenLimitsResultTag::Ok, message: std::ptr::null_mut() }, +// Err(e) => { +// match e { +// OpenlimitsSharpError::InvalidArgument(msg) => OpenLimitsResult { tag: OpenLimitsResultTag::InvalidArgument, message: string_to_c_str(msg) }, +// OpenlimitsSharpError::InitializeException(msg) => OpenLimitsResult { tag: OpenLimitsResultTag::InitializeException, message: string_to_c_str(msg) }, +// OpenlimitsSharpError::SubscribeException(msg) => OpenLimitsResult { tag: OpenLimitsResultTag::SubscribeException, message: string_to_c_str(msg) }, +// OpenlimitsSharpError::OpenLimitsError(e) => { +// let message = match &e { +// OpenLimitsError::BinanceError(e) => e.msg.clone(), +// OpenLimitsError::CoinbaseError(e) => e.message.clone(), +// OpenLimitsError::NashProtocolError(e) => e.0.to_string(), +// OpenLimitsError::MissingImplementation(e) => e.message.clone(), +// OpenLimitsError::AssetNotFound() => String::from("Asset not found"), +// OpenLimitsError::NoApiKeySet() => String::from("No api key set"), +// OpenLimitsError::InternalServerError() => String::from("Internal server error"), +// OpenLimitsError::ServiceUnavailable() => String::from("Service unavailable"), +// OpenLimitsError::Unauthorized() => String::from("Unauthorized"), +// OpenLimitsError::SymbolNotFound() => String::from("Symbol not found"), +// OpenLimitsError::SocketError() => String::from("Socket error"), +// OpenLimitsError::GetTimestampFailed() => String::from("Get timestamp failed"), +// OpenLimitsError::ReqError(e) => e.to_string(), +// OpenLimitsError::InvalidHeaderError(e) => e.to_string(), +// OpenLimitsError::InvalidPayloadSignature(e) => e.to_string(), +// OpenLimitsError::IoError(e) => e.to_string(), +// OpenLimitsError::PoisonError() => String::from("Poison error"), +// OpenLimitsError::JsonError(e) => e.to_string(), +// OpenLimitsError::ParseFloatError(e) => e.to_string(), +// OpenLimitsError::UrlParserError(e) => e.to_string(), +// OpenLimitsError::Tungstenite(e) => e.to_string(), +// OpenLimitsError::TimestampError(e) => e.to_string(), +// OpenLimitsError::UnkownResponse(e) => e.clone(), +// OpenLimitsError::NotParsableResponse(e) => e.clone(), +// OpenLimitsError::MissingParameter(e) => e.clone(), +// OpenLimitsError::WebSocketMessageNotSupported() => String::from("WebSocket message not supported"), +// OpenLimitsError::NoMarketPair => String::from("No market pair") +// }; +// let tag = match &e { +// OpenLimitsError::BinanceError(_) => OpenLimitsResultTag::BinanceError, +// OpenLimitsError::CoinbaseError(_) => OpenLimitsResultTag::CoinbaseError, +// OpenLimitsError::NashProtocolError(_) => OpenLimitsResultTag::NashProtocolError, +// OpenLimitsError::MissingImplementation(_) => OpenLimitsResultTag::MissingImplementation, +// OpenLimitsError::AssetNotFound() => OpenLimitsResultTag::AssetNotFound, +// OpenLimitsError::NoApiKeySet() => OpenLimitsResultTag::NoApiKeySet, +// OpenLimitsError::InternalServerError() => OpenLimitsResultTag::InternalServerError, +// OpenLimitsError::ServiceUnavailable() => OpenLimitsResultTag::ServiceUnavailable, +// OpenLimitsError::Unauthorized() => OpenLimitsResultTag::Unauthorized, +// OpenLimitsError::SymbolNotFound() => OpenLimitsResultTag::SymbolNotFound, +// OpenLimitsError::SocketError() => OpenLimitsResultTag::SocketError, +// OpenLimitsError::GetTimestampFailed() => OpenLimitsResultTag::GetTimestampFailed, +// OpenLimitsError::ReqError(_) => OpenLimitsResultTag::ReqError, +// OpenLimitsError::InvalidHeaderError(_) => OpenLimitsResultTag::InvalidHeaderError, +// OpenLimitsError::InvalidPayloadSignature(_) => OpenLimitsResultTag::InvalidPayloadSignature, +// OpenLimitsError::IoError(_) => OpenLimitsResultTag::IoError, +// OpenLimitsError::PoisonError() => OpenLimitsResultTag::PoisonError, +// OpenLimitsError::JsonError(_) => OpenLimitsResultTag::JsonError, +// OpenLimitsError::ParseFloatError(_) => OpenLimitsResultTag::ParseFloatError, +// OpenLimitsError::UrlParserError(_) => OpenLimitsResultTag::UrlParserError, +// OpenLimitsError::Tungstenite(_) => OpenLimitsResultTag::Tungstenite, +// OpenLimitsError::TimestampError(_) => OpenLimitsResultTag::TimestampError, +// OpenLimitsError::UnkownResponse(_) => OpenLimitsResultTag::UnkownResponse, +// OpenLimitsError::NotParsableResponse(_) => OpenLimitsResultTag::NotParsableResponse, +// OpenLimitsError::MissingParameter(_) => OpenLimitsResultTag::MissingParameter, +// OpenLimitsError::WebSocketMessageNotSupported() => OpenLimitsResultTag::WebSocketMessageNotSupported, +// OpenLimitsError::NoMarketPair => OpenLimitsResultTag::NoMarketPair, +// }; +// OpenLimitsResult { tag, message: string_to_c_str(message) } +// }, +// } +// } +// } +// } +// +// #[repr(C)] +// #[derive(Debug, Copy, Clone)] +// pub struct FFIAskBid { +// pub price: *mut c_char, +// pub qty: *mut c_char, +// } +// +// #[repr(u32)] +// #[derive(Debug, Copy, Clone)] +// pub enum FFILiquidity { +// Unknown, +// Maker, +// Taker, +// } +// #[repr(u32)] +// #[derive(Debug, Copy, Clone)] +// pub enum FFISide { +// Buy, +// Sell, +// } +// +// #[repr(u32)] +// #[derive(Debug, Copy, Clone)] +// pub enum FFITIF { +// GTC, +// FOK, +// IOC, +// GTT +// } +// +// fn ffitif_to_tif(tif: FFITIF, ms: u64) -> TimeInForce { +// match tif { +// FFITIF::GTC => TimeInForce::GoodTillCancelled, +// FFITIF::IOC => TimeInForce::ImmediateOrCancelled, +// FFITIF::FOK => TimeInForce::FillOrKill, +// FFITIF::GTT => TimeInForce::GoodTillTime( +// Duration::milliseconds(ms as i64) +// ), +// } +// } +// +// #[repr(u32)] +// #[derive(Debug, Copy, Clone)] +// pub enum FFIOrderType { +// Limit, +// Market, +// StopLimit, +// StopMarket, +// Unknown, +// } +// +// fn order_type_to_ffi(t: OrderType) -> FFIOrderType { +// match t { +// OrderType::Limit => FFIOrderType::Limit, +// OrderType::Market => FFIOrderType::Market, +// OrderType::StopLimit => FFIOrderType::StopLimit, +// OrderType::StopMarket => FFIOrderType::StopMarket, +// OrderType::Unknown => FFIOrderType::Unknown, +// } +// } +// +// #[repr(u32)] +// #[derive(Debug, Copy, Clone)] +// pub enum FFIOrderStatus { +// New, +// PartiallyFilled, +// Filled, +// Canceled, +// PendingCancel, +// Rejected, +// Expired, +// Open, +// Pending, +// Active, +// } +// +// +// fn order_status_to_ffi(t: OrderStatus) -> FFIOrderStatus { +// match t { +// OrderStatus::New => FFIOrderStatus::New, +// OrderStatus::PartiallyFilled => FFIOrderStatus::PartiallyFilled, +// OrderStatus::Filled => FFIOrderStatus::Filled, +// OrderStatus::Canceled => FFIOrderStatus::Canceled, +// OrderStatus::PendingCancel => FFIOrderStatus::PendingCancel, +// OrderStatus::Rejected => FFIOrderStatus::Rejected, +// OrderStatus::Expired => FFIOrderStatus::Expired, +// OrderStatus::Open => FFIOrderStatus::Open, +// OrderStatus::Pending => FFIOrderStatus::Pending, +// OrderStatus::Active => FFIOrderStatus::Active, +// } +// } +// +// #[repr(C)] +// #[derive(Debug, Copy, Clone)] +// pub struct FFITrade { +// id: *mut c_char, +// buyer_order_id: *mut c_char, +// seller_order_id: *mut c_char, +// market_pair: *mut c_char, +// price: *mut c_char, +// qty: *mut c_char, +// fees: *mut c_char, +// side: FFISide, +// liquidity: FFILiquidity, +// created_at: u64, +// } +// +// #[repr(C)] +// #[derive(Debug, Copy, Clone)] +// pub struct FFIOrder { +// pub id: *mut c_char, +// pub market_pair: *mut c_char, +// pub client_order_id: *mut c_char, +// pub created_at: u64, +// pub order_type: FFIOrderType, +// pub side: FFISide, +// pub status: FFIOrderStatus, +// pub size: *mut c_char, +// pub price: *mut c_char, +// pub remaining: *mut c_char, +// } +// +// fn order_to_ffi(t: Order) -> FFIOrder { +// FFIOrder { +// id: string_to_c_str(t.id), +// market_pair: string_to_c_str(t.market_pair), +// client_order_id: match t.client_order_id { +// None => std::ptr::null_mut(), +// Some(client_order_id) => string_to_c_str(client_order_id) +// }, +// created_at: match t.created_at { +// None => 0, +// Some(created_at) => created_at +// }, +// order_type: order_type_to_ffi(t.order_type), +// side: match t.side { +// Side::Buy => FFISide::Buy, +// Side::Sell => FFISide::Sell, +// }, +// status: order_status_to_ffi(t.status), +// size: string_to_c_str(t.size.to_string()), +// price: match t.price { +// Some(price) => string_to_c_str(price.to_string()), +// None => std::ptr::null_mut() +// }, +// remaining: match t.remaining { +// Some(rem) => string_to_c_str(rem.to_string()), +// None => std::ptr::null_mut() +// } +// } +// } +// +// #[repr(C)] +// #[derive(Debug, Copy, Clone)] +// pub struct FFIGetHistoricTradesRequest { +// market: *mut c_char, +// paginator: *mut FFIPaginator +// } +// +// +// fn to_ffi_ask_bid(f: &AskBid) -> FFIAskBid { +// FFIAskBid { +// price: string_to_c_str(f.price.to_string()), +// qty: string_to_c_str(f.qty.to_string()) +// } +// } +// +// fn to_ffi_candle(f: &Candle) -> FFICandle { +// FFICandle { +// time: f.time, +// low: f.low.to_f64().unwrap(), +// high: f.high.to_f64().unwrap(), +// open: f.open.to_f64().unwrap(), +// close: f.close.to_f64().unwrap(), +// volume: f.volume.to_f64().unwrap(), +// } +// } +// +// fn option_string_to_c_str(s: Option) -> *mut c_char { +// match s { +// None => std::ptr::null_mut(), +// Some(s ) => string_to_c_str(s) +// } +// } +// +// fn to_ffi_trade(f: &Trade) -> FFITrade { +// FFITrade { +// id: string_to_c_str(f.id.clone()), +// buyer_order_id: option_string_to_c_str(f.buyer_order_id.clone()), +// seller_order_id: option_string_to_c_str(f.seller_order_id.clone()), +// market_pair: string_to_c_str(f.market_pair.clone()), +// price: string_to_c_str(f.price.to_string()), +// qty: string_to_c_str(f.qty.to_string()), +// fees: match f.fees { +// Some(f) => string_to_c_str(f.to_string()), +// None => std::ptr::null_mut(), +// }, +// side: match f.side { +// Side::Buy => FFISide::Buy, +// Side::Sell => FFISide::Sell, +// }, +// liquidity: match f.liquidity { +// Some(Liquidity::Maker) => FFILiquidity::Maker, +// Some(Liquidity::Taker) => FFILiquidity::Taker, +// None => FFILiquidity::Unknown, +// }, +// created_at: f.created_at, +// } +// } +// +// #[repr(C)] +// #[derive(Debug)] +// pub struct FFIBinanceConfig { +// apikey: *mut c_char, +// secret: *mut c_char, +// sandbox: bool +// } +// +// type Out = *mut T; +// +// +// fn binance_credentials_from_ptrs(apikey: *mut c_char, secret: *mut c_char) -> Result, std::str::Utf8Error> { +// if apikey.is_null() { +// return Ok(None) +// } +// if secret.is_null() { +// return Ok(None) +// } +// +// Ok( +// Some( +// BinanceCredentials { +// api_key: c_str_to_string(apikey)?, +// api_secret: c_str_to_string(secret)? +// } +// ) +// ) +// } +// +// impl TryInto for FFIBinanceConfig { +// type Error = (); +// fn try_into(self) -> Result { +// Ok( +// BinanceParameters { +// credentials: binance_credentials_from_ptrs(self.apikey, self.secret).map_err(|_|())?, +// sandbox: self.sandbox, +// } +// ) +// } +// } +// +// #[repr(u32)] +// #[derive(Debug)] +// pub enum FFINashEnv { +// Sandbox, +// Production +// } +// +// #[repr(C)] +// pub struct ExchangeClient { +// client: AnyExchange, +// init_params: InitAnyExchange, +// channel: Option>, +// runtime: tokio::runtime::Runtime +// } +// +// #[repr(C)] +// #[derive(Debug)] +// pub struct InitResult { +// client: *mut ExchangeClient, +// } +// type SubResult = std::result::Result; +// type SubChannel = tokio::sync::oneshot::Sender; +// pub enum SubthreadCmd { +// Sub(Subscription, SubChannel), +// Disconnect +// } +// +// #[no_mangle] +// pub extern "cdecl" fn init_binance( +// config: FFIBinanceConfig, +// out_client: Out<*mut ExchangeClient> +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError>{ +// let init_params: InitAnyExchange = config.try_into().map(InitAnyExchange::Binance).map_err(|_| OpenlimitsSharpError::InitializeException(String::from("Failed to parse config")))?; +// let mut runtime = tokio::runtime::Builder::new().basic_scheduler().enable_all().build().map_err(|_| OpenlimitsSharpError::InitializeException(String::from("Failed to start tokio runtime")))?; +// +// let client_future = OpenLimits::instantiate(init_params.clone()); +// let client: AnyExchange = runtime.block_on(client_future)?; +// +// +// let b = Box::new(ExchangeClient{ +// client, +// init_params, +// channel: None, +// runtime +// }); +// unsafe { +// *out_client = Box::into_raw(b); +// Ok(()) +// } +// }; +// +// result_to_ffi(call()) +// } +// +// #[no_mangle] +// pub extern "cdecl" fn init_coinbase( +// apikey: *mut c_char, +// api_secret: *mut c_char, +// passphrase: *mut c_char, +// sandbox: bool, +// out_client: Out<*mut ExchangeClient> +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError>{ +// let api_key = nullable_cstr(apikey).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse apikey string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// let api_secret = nullable_cstr(api_secret).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse api_secret string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// let passphrase = nullable_cstr(passphrase).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse passphrase string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// let init_params: InitAnyExchange = InitAnyExchange::Coinbase( +// CoinbaseParameters { +// sandbox, +// credentials: match (api_key, api_secret, passphrase) { +// (Some(api_key), Some(api_secret), Some(passphrase)) => Ok( +// Some( +// CoinbaseCredentials { +// api_key, +// api_secret, +// passphrase +// } +// ) +// ), +// (None, None, None) => Ok(None), +// _ => Err(OpenlimitsSharpError::InvalidArgument(format!("Invalid credentials"))) +// }? +// } +// ); +// +// let mut runtime = tokio::runtime::Builder::new().basic_scheduler().enable_all().build().map_err(|_| OpenlimitsSharpError::InitializeException(String::from("Failed to start tokio runtime")))?; +// +// let client_future = OpenLimits::instantiate(init_params.clone()); +// let client: AnyExchange = runtime.block_on(client_future)?; +// +// +// let b = Box::new(ExchangeClient{ +// client, +// init_params, +// channel: None, +// runtime +// }); +// unsafe { +// *out_client = Box::into_raw(b); +// Ok(()) +// } +// }; +// +// result_to_ffi(call()) +// } +// +// +// +// #[no_mangle] +// pub extern "cdecl" fn init_nash( +// apikey: *mut c_char, +// secret: *mut c_char, +// client_id: u64, +// environment: FFINashEnv, +// timeout: u64, +// affiliate_code: *mut c_char, +// out_client: Out<*mut ExchangeClient> +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError>{ +// let mut credentials: Option = None; +// if !apikey.is_null() && !secret.is_null() { +// credentials = Some( +// NashCredentials { +// secret: c_str_to_string(secret).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?, +// session: c_str_to_string(apikey).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse session string. Invalid character on pos {}", e.valid_up_to())) +// )? +// } +// ) +// } +// +// let environment = match environment { +// FFINashEnv::Production => Environment::Production, +// FFINashEnv::Sandbox => Environment::Sandbox, +// }; +// +// let affiliate_code = nullable_cstr(affiliate_code).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse affiliate_code string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// let nash_params = NashParameters { +// affiliate_code, +// credentials, +// client_id, +// timeout: std::time::Duration::from_millis(timeout), +// environment +// }; +// +// let init_params = InitAnyExchange::Nash( +// nash_params +// ); +// +// let mut runtime = tokio::runtime::Builder::new().basic_scheduler().enable_all().build().map_err(|_| OpenlimitsSharpError::InitializeException(String::from("Failed to start tokio runtime")))?; +// +// let client_future = OpenLimits::instantiate(init_params.clone()); +// let client: AnyExchange = runtime.block_on(client_future)?; +// +// let b = Box::new(ExchangeClient{ +// client, +// init_params, +// channel: None, +// runtime +// }); +// unsafe { +// *out_client = Box::into_raw(b); +// Ok(()) +// } +// }; +// +// result_to_ffi(call()) +// } +// +// #[no_mangle] +// pub extern "cdecl" fn order_book( +// client: *mut ExchangeClient, +// market: *mut c_char, +// bids_buff: *mut FFIAskBid, bids_buff_len: u64, actual_bids_buff_len: Out, +// asks_buff: *mut FFIAskBid, asks_buff_len: u64, actual_asks_buff_len: Out, +// last_update_id: Out, +// update_id: Out, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError>{ +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// +// if market.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("market is null"))); +// } +// let market_pair = c_str_to_string(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// let req = OrderBookRequest { +// market_pair +// }; +// unsafe { +// let resp = (*client).runtime.block_on( +// (*client).client.order_book(&req) +// )?; +// +// let bids = std::slice::from_raw_parts_mut::(bids_buff, bids_buff_len as usize); +// let ffi_bids: Vec = resp.bids.iter().map(to_ffi_ask_bid).collect(); +// let l = std::cmp::min(bids_buff_len as usize, ffi_bids.len() as usize); +// bids[0..l].copy_from_slice(&ffi_bids[0..l]); +// (*actual_bids_buff_len) = l as u64; +// +// let asks = std::slice::from_raw_parts_mut::(asks_buff, asks_buff_len as usize); +// let ffi_asks: Vec = resp.asks.iter().map(to_ffi_ask_bid).collect(); +// let l = std::cmp::min(asks_buff_len as usize, ffi_asks.len() as usize); +// asks[0..l].copy_from_slice(&ffi_asks[0..l]); +// (*actual_asks_buff_len) = l as u64; +// (*last_update_id) = resp.last_update_id.unwrap_or_default(); +// (*update_id) = resp.update_id.unwrap_or_default(); +// }; +// Ok(()) +// }; +// +// result_to_ffi(call()) +// } +// +// +// +// #[no_mangle] +// pub extern "cdecl" fn get_price_ticker( +// client: *mut ExchangeClient, +// market: *mut c_char, +// price: Out +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// +// if market.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("market is null"))); +// } +// let market_pair = c_str_to_string(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// let req = GetPriceTickerRequest { +// market_pair +// }; +// unsafe { +// let resp = (*client).runtime.block_on( +// (*client).client.get_price_ticker(&req) +// )?; +// let price_opt = resp.price; +// let price_opt = price_opt.map(|f| f.to_f64()).flatten(); +// (*price) = price_opt.unwrap_or(std::f64::NAN); +// Ok(()) +// } +// }; +// +// +// result_to_ffi(call()) +// } +// +// +// #[no_mangle] +// pub extern "cdecl" fn get_historic_rates( +// client: *mut ExchangeClient, +// market: *mut c_char, +// interval: FFIInterval, +// paginator: *mut FFIPaginator, +// candles_buff: *mut FFICandle, candles_buff_len: usize, actual_candles_buff_len: Out, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// let mut paginator_res: Option> = None; +// if !paginator.is_null() { +// unsafe { +// let pag: Result = (*paginator).try_into(); +// paginator_res = Some(pag); +// } +// } +// let paginator = paginator_res.transpose().map_err(|_| OpenlimitsSharpError::InvalidArgument(String::from("Invalid paginator")))?; +// let market_pair = c_str_to_string(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// let interval = interval_from_ffi_interval(interval).map_err(|_| OpenlimitsSharpError::InvalidArgument(String::from("Invalid interval")))?; +// +// let req = GetHistoricRatesRequest { +// paginator, +// market_pair, +// interval +// }; +// unsafe { +// let resp = (*client).runtime.block_on( +// (*client).client.get_historic_rates(&req) +// )?; +// +// +// let canles = std::slice::from_raw_parts_mut::(candles_buff, candles_buff_len); +// let ffi_candles: Vec = resp.iter().map(to_ffi_candle).collect(); +// let l = std::cmp::min(candles_buff_len, ffi_candles.len()); +// canles[0..l].copy_from_slice(&ffi_candles[0..l]); +// (*actual_candles_buff_len) = l; +// Ok(()) +// } +// }; +// result_to_ffi(call()) +// } +// +// #[no_mangle] +// pub extern "cdecl" fn get_historic_trades( +// client: *mut ExchangeClient, +// market: *mut c_char, +// paginator: *mut FFIPaginator, +// buff: *mut FFITrade, buff_len: usize, actual_buff_len: Out, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// let market_pair = c_str_to_string(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// +// let mut paginator_res: Option> = None; +// if !paginator.is_null() { +// unsafe { +// let pag: Result = (*paginator).try_into(); +// paginator_res = Some(pag); +// } +// } +// let paginator = paginator_res.transpose().map_err(|_| OpenlimitsSharpError::InvalidArgument(String::from("Invalid paginator")))?; +// +// let req = GetHistoricTradesRequest { +// paginator, +// market_pair, +// }; +// unsafe { +// let resp = (*client).runtime.block_on( +// (*client).client.get_historic_trades(&req) +// )?; +// +// let trades = std::slice::from_raw_parts_mut::(buff, buff_len); +// let ffi_trades: Vec = resp.iter().map(to_ffi_trade).collect(); +// let l = std::cmp::min(buff_len, ffi_trades.len()); +// trades[0..l].copy_from_slice(&ffi_trades[0..l]); +// (*actual_buff_len) = l; +// Ok(()) +// } +// }; +// result_to_ffi(call()) +// } +// #[no_mangle] +// +// pub extern "cdecl" fn place_order( +// client: *mut ExchangeClient, +// market: *mut c_char, +// qty: *mut c_char, +// limit: bool, +// price: *mut c_char, +// side: FFISide, +// tif: FFITIF, +// tif_duration: u64, +// _post_only: bool, +// +// result: Out +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// let market_pair = c_str_to_string(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// let size = c_str_to_string(qty).map(|q| Decimal::from_str(q.as_str())); +// let size = size.map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse size string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// let size = size.map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse size string: {}", e)) +// )?; +// +// +// if limit == false { +// let req = OpenMarketOrderRequest { +// market_pair, +// size +// }; +// +// unsafe { +// #[allow(unreachable_patterns)] +// match side { +// FFISide::Buy => { +// let order = (*client).runtime.block_on( +// (*client).client.market_buy(&req) +// )?; +// (*result) = order_to_ffi(order); +// return Ok(()); +// }, +// FFISide::Sell => { +// let order = (*client).runtime.block_on( +// (*client).client.market_sell(&req) +// )?; +// (*result) = order_to_ffi(order); +// return Ok(()); +// }, +// e => return Err(OpenlimitsSharpError::InvalidArgument(format!("Invalid side size string: {:?}", e))) +// } +// } +// } +// let price = c_str_to_string(price).map(|q| Decimal::from_str(q.as_str())).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse price string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// let price = price.map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse price string: {}", e)) +// )?; +// +// let time_in_force = ffitif_to_tif(tif, tif_duration); +// let req = OpenLimitOrderRequest { +// market_pair, +// price, +// time_in_force, +// size, +// post_only: _post_only +// }; +// unsafe { +// #[allow(unreachable_patterns)] +// match side { +// FFISide::Buy => { +// let order = (*client).runtime.block_on( +// (*client).client.limit_buy(&req) +// )?; +// (*result) = order_to_ffi(order); +// return Ok(()); +// }, +// FFISide::Sell => { +// let order = (*client).runtime.block_on( +// (*client).client.limit_sell(&req) +// )?; +// (*result) = order_to_ffi(order); +// return Ok(()); +// }, +// e => return Err(OpenlimitsSharpError::InvalidArgument(format!("Invalid side size string: {:?}", e))) +// } +// } +// }; +// +// result_to_ffi(call()) +// } +// +// #[no_mangle] +// pub extern "cdecl" fn get_all_open_orders( +// client: *mut ExchangeClient, +// buff: *mut FFIOrder, buff_len: usize, actual_buff_len: Out, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// +// unsafe { +// let resp = (*client).runtime.block_on( +// (*client).client.get_all_open_orders() +// )?; +// +// let orders = std::slice::from_raw_parts_mut::(buff, buff_len); +// let ffi_orders: Vec = resp.into_iter().map(order_to_ffi).collect(); +// let l = std::cmp::min(buff_len, ffi_orders.len()); +// orders[0..ffi_orders.len()].copy_from_slice(&ffi_orders[0..l]); +// (*actual_buff_len) = l; +// }; +// Ok(()) +// }; +// +// result_to_ffi(call()) +// } +// +// #[no_mangle] +// pub extern "cdecl" fn get_order_history( +// client: *mut ExchangeClient, +// market: *mut c_char, +// paginator: *mut FFIPaginator, +// buff: *mut FFIOrder, buff_len: usize, actual_buff_len: Out, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// let market_pair = nullable_cstr(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// +// let mut paginator_res: Option> = None; +// if !paginator.is_null() { +// unsafe { +// let pag: Result = (*paginator).try_into(); +// paginator_res = Some(pag); +// } +// } +// let paginator = paginator_res.transpose().map_err(|_| OpenlimitsSharpError::InvalidArgument(String::from("Invalid paginator")))?; +// +// let req = GetOrderHistoryRequest { +// paginator, +// market_pair, +// }; +// unsafe { +// let resp = (*client).runtime.block_on( +// (*client).client.get_order_history(&req) +// )?; +// +// let orders = std::slice::from_raw_parts_mut::(buff, buff_len); +// let ffi_orders: Vec = resp.into_iter().map(order_to_ffi).collect(); +// let l = std::cmp::min(buff_len, ffi_orders.len()); +// +// orders[0..l].copy_from_slice(&ffi_orders[0..l]); +// (*actual_buff_len) = l; +// } +// Ok(()) +// }; +// +// result_to_ffi(call()) +// } +// +// +// +// #[no_mangle] +// pub extern "cdecl" fn get_trade_history( +// client: *mut ExchangeClient, +// market: *mut c_char, +// order_id: *mut c_char, +// paginator: *mut FFIPaginator, +// buff: *mut FFITrade, buff_len: usize, actual_buff_len: Out, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// let market_pair = nullable_cstr(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// let order_id = nullable_cstr(order_id).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse order_id string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// +// let mut paginator_res: Option> = None; +// if !paginator.is_null() { +// unsafe { +// let pag: Result = (*paginator).try_into(); +// paginator_res = Some(pag); +// } +// } +// let paginator = paginator_res.transpose().map_err(|_| OpenlimitsSharpError::InvalidArgument(String::from("Invalid paginator")))?; +// +// let req = TradeHistoryRequest { +// paginator, +// order_id, +// market_pair, +// }; +// unsafe { +// let resp = (*client).runtime.block_on( +// (*client).client.get_trade_history(&req) +// )?; +// +// let trades = std::slice::from_raw_parts_mut::(buff, buff_len); +// let ffi_trades: Vec = resp.iter().map(to_ffi_trade).collect(); +// let l = std::cmp::min(buff_len, ffi_trades.len()); +// +// trades[0..ffi_trades.len()].copy_from_slice(&ffi_trades[0..l]); +// (*actual_buff_len) = l; +// } +// Ok(()) +// }; +// +// result_to_ffi(call()) +// +// } +// +// +// #[no_mangle] +// pub extern "cdecl" fn get_account_balances( +// client: *mut ExchangeClient, +// paginator: *mut FFIPaginator, +// buff: *mut FFIBalance, buff_len: usize, actual_buff_len: Out, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// +// let mut paginator_res: Option> = None; +// if !paginator.is_null() { +// unsafe { +// let pag: Result = (*paginator).try_into(); +// paginator_res = Some(pag); +// } +// } +// let paginator = paginator_res.transpose().map_err(|_| OpenlimitsSharpError::InvalidArgument(String::from("Invalid paginator")))?; +// +// +// unsafe { +// let resp = (*client).runtime.block_on( +// (*client).client.get_account_balances(paginator) +// )?; +// +// let balances = std::slice::from_raw_parts_mut::(buff, buff_len); +// let ffi_balances: Vec = resp.into_iter().map(to_ffi_balance).collect(); +// let l = std::cmp::min(buff_len, ffi_balances.len()); +// +// balances[0..l].copy_from_slice(&ffi_balances[0..l]); +// (*actual_buff_len) = l; +// } +// Ok(()) +// }; +// +// result_to_ffi(call()) +// } +// +// +// #[no_mangle] +// pub extern "cdecl" fn cancel_all_orders( +// client: *mut ExchangeClient, +// market: *mut c_char, +// buff: *mut *mut c_char, buff_len: usize, actual_buff_len: Out, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// let market_pair = nullable_cstr(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// +// unsafe { +// let resp = (*client).runtime.block_on( +// (*client).client.cancel_all_orders(&CancelAllOrdersRequest { +// market_pair +// }) +// )?; +// +// let ids = std::slice::from_raw_parts_mut::<*mut c_char>(buff, buff_len); +// let ffi_ids: Vec<*mut c_char> = resp.into_iter().map(|c|string_to_c_str(c.id)).collect(); +// let l = std::cmp::min(buff_len, ffi_ids.len()); +// +// ids[0..l].copy_from_slice(&ffi_ids[0..l]); +// (*actual_buff_len) = l; +// } +// Ok(()) +// }; +// result_to_ffi(call()) +// } +// +// #[no_mangle] +// pub extern "cdecl" fn get_order( +// client: *mut ExchangeClient, +// order_id: *mut c_char, +// market: *mut c_char, +// result: Out, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// +// let id = c_str_to_string(order_id).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// let market_pair = nullable_cstr(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// unsafe { +// let order = (*client).runtime.block_on( +// (*client).client.get_order( &GetOrderRequest { +// id, +// market_pair +// }) +// )?; +// (*result) = order_to_ffi(order); +// } +// +// Ok(()) +// }; +// +// result_to_ffi(call()) +// } +// +// #[no_mangle] +// pub extern "cdecl" fn cancel_order( +// client: *mut ExchangeClient, +// order_id: *mut c_char, +// market: *mut c_char, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// let id = c_str_to_string(order_id).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// let market_pair = nullable_cstr(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// unsafe { +// (*client).runtime.block_on( +// (*client).client.cancel_order(&CancelOrderRequest { +// id, +// market_pair +// }) +// )?; +// } +// Ok(()) +// }; +// result_to_ffi(call()) +// } +// +// +// #[no_mangle] +// pub extern "cdecl" fn receive_pairs( +// client: *mut ExchangeClient, +// buff: *mut FFIMarketPair, buff_len: usize, actual_buff_len: Out, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if client.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("client is null"))); +// } +// unsafe { +// let pairs = (*client).runtime.block_on( +// (*client).client.retrieve_pairs() +// )?; +// +// let pairs_buff = std::slice::from_raw_parts_mut::(buff, buff_len); +// let pairs_ffi: Vec = pairs.into_iter().map(market_pair_to_ffi).collect(); +// let l = std::cmp::min(buff_len, pairs_ffi.len()); +// +// pairs_buff[0..l].copy_from_slice(&pairs_ffi[0..l]); +// (*actual_buff_len) = l; +// } +// Ok(()) +// }; +// result_to_ffi(call()) +// } +// +// #[repr(C)] +// #[derive(Copy, Clone)] +// pub struct FFITradeBox(*mut FFITrade); +// unsafe impl Send for FFITradeBox {} +// unsafe impl Sync for FFITradeBox {} +// #[repr(C)] +// #[derive(Copy, Clone)] +// pub struct FFIAskBidBox(*mut FFIAskBid); +// unsafe impl Send for FFIAskBidBox {} +// unsafe impl Sync for FFIAskBidBox {} +// +// #[no_mangle] +// #[allow(unsafe_code)] +// pub extern "cdecl" fn init_subscriptions( +// client: *mut ExchangeClient, +// on_error: extern fn(), +// on_ping: extern fn(), +// on_orderbook: extern fn(bids_len: u64, asks_len: u64, market: *mut c_char, last_update_id: u64, update_id: u64), +// on_trades: extern fn(buff_len: u64, market: *mut c_char), +// on_disconnet: extern fn(), +// bids_buff: FFIAskBidBox, bids_buff_len: usize, +// asks_buff: FFIAskBidBox, asks_buff_len: usize, +// trades_buff: FFITradeBox, trades_buff_len: usize, +// sub_handle: Out<*mut tokio::sync::mpsc::UnboundedSender> +// ) -> OpenLimitsResult { +// let (sub_request_tx, mut sub_rx) = tokio::sync::mpsc::unbounded_channel::(); +// +// let init_params = unsafe { +// (*client).init_params.clone() +// }; +// let (finish_tx, finish_rx) = tokio::sync::oneshot::channel::>(); +// +// std::thread::spawn(move || { +// let call = move|| -> Result<(tokio::runtime::Runtime, OpenLimitsWs), OpenlimitsSharpError> { +// let mut rt = tokio::runtime::Builder::new() +// .basic_scheduler() +// .enable_all() +// .build() +// .map_err(|_| OpenlimitsSharpError::InitializeException(String::from("Failed to start tokio runtime")))?; +// let client: OpenLimitsWs = rt.block_on(OpenLimitsWs::instantiate(init_params))?; +// +// Ok((rt, client)) +// }; +// +// let (mut rt, client) = match call() { +// Ok(e) => e, +// Err(e) => { +// finish_tx.send(Err(e)).expect("Failed to communicate result back to main thread"); +// return; +// } +// }; +// finish_tx.send(Ok(())).expect("Failed to communicate result back to main thread"); +// +// loop { +// let subcmd = sub_rx.next(); +// let thread_cmd = rt.block_on(subcmd); +// match thread_cmd { +// Some(SubthreadCmd::Disconnect) => { +// break; +// }, +// Some(SubthreadCmd::Sub(sub, writer)) => { +// let result = rt.block_on(client.subscribe(sub.clone(), move |resp| { +// let out_asks = unsafe { std::slice::from_raw_parts_mut::(asks_buff.0, asks_buff_len) }; +// let out_bids = unsafe { std::slice::from_raw_parts_mut::(bids_buff.0, bids_buff_len) }; +// let resp = match resp { +// Ok(e) => e, +// Err(_) => { +// on_error(); +// return +// } +// }; +// let resp = match resp { +// WebSocketResponse::Generic(msg) => msg, +// _ => { +// return; +// } +// }; +// +// match resp { +// OpenLimitsWebSocketMessage::Ping => { +// on_ping(); +// }, +// OpenLimitsWebSocketMessage::Trades(trades) => { +// let out_trades = unsafe { std::slice::from_raw_parts_mut::(trades_buff.0, trades_buff_len) }; +// let market = match sub.clone() { +// Subscription::Trades(market) => market, +// _ => panic!("Unreachable") +// }; +// for (i, trade) in trades.iter().enumerate() { +// out_trades[i] = to_ffi_trade(trade); +// } +// on_trades(trades.len() as u64, string_to_c_str(market)); +// }, +// OpenLimitsWebSocketMessage::OrderBook(resp) => { +// let market = match sub.clone() { +// Subscription::OrderBookUpdates(market) => market, +// _ => panic!("Unreachable") +// }; +// for (i, bid) in resp.bids.iter().enumerate() { +// out_bids[i] = to_ffi_ask_bid(bid); +// } +// for (i, ask) in resp.asks.iter().enumerate() { +// out_asks[i] = to_ffi_ask_bid(ask); +// } +// on_orderbook( +// resp.bids.len() as u64, +// resp.asks.len() as u64, +// string_to_c_str(market.clone()), +// resp.last_update_id.unwrap_or_default(), +// resp.update_id.unwrap_or_default() +// ); +// }, +// OpenLimitsWebSocketMessage::OrderBookDiff(resp) => { +// let market = match sub.clone() { +// Subscription::OrderBookUpdates(market) => market, +// _ => panic!("Unreachable") +// }; +// for (i, bid) in resp.bids.iter().enumerate() { +// out_bids[i] = to_ffi_ask_bid(bid); +// } +// for (i, ask) in resp.asks.iter().enumerate() { +// out_asks[i] = to_ffi_ask_bid(ask); +// } +// on_orderbook( +// resp.bids.len() as u64, +// resp.asks.len() as u64, +// string_to_c_str(market.clone()), +// resp.last_update_id.unwrap_or_default(), +// resp.update_id.unwrap_or_default() +// ); +// } +// }; +// })); +// writer.send(result).expect("Failed to send result back to subcribe call"); +// }, +// None => {} +// } +// } +// on_disconnet(); +// }); +// +// unsafe { +// let r = match (*client).runtime.block_on( +// finish_rx +// ) { +// Err(error) => Err(OpenlimitsSharpError::InitializeException(format!("Failed while waiting for subscription thread to intialize: {}", error.to_string()))), +// Ok(e) => e +// }; +// +// *sub_handle = Box::into_raw(Box::new(sub_request_tx)); +// +// result_to_ffi(r) +// } +// } +// +// +// #[no_mangle] +// pub extern fn free_string(s: *mut c_char) { +// unsafe { +// if s.is_null() { return } +// CString::from_raw(s) +// }; +// } +// +// #[no_mangle] +// pub extern "cdecl" fn subscribe_orderbook( +// client: *mut ExchangeClient, +// channel: *mut tokio::sync::mpsc::UnboundedSender, +// market: *mut c_char, +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if channel.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("channel is null"))); +// } +// let market_pair = c_str_to_string(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// +// let (finish_tx, finish_rx) = tokio::sync::oneshot::channel::(); +// unsafe { +// (*channel).send( +// SubthreadCmd::Sub(Subscription::OrderBookUpdates( +// market_pair, +// ), finish_tx) +// ).map_err(|_| OpenlimitsSharpError::SubscribeException(String::from("failed to send subscription to handler")))?; +// +// let result = (*client).runtime.block_on(finish_rx).map_err(|_| OpenlimitsSharpError::SubscribeException(String::from("failed to get subscription result from handler")))?; +// +// match result { +// Ok(_) => Ok(()), +// Err(e) => Err(OpenlimitsSharpError::OpenLimitsError(e)) +// } +// } +// }; +// result_to_ffi(call()) +// } +// +// #[no_mangle] +// pub extern "cdecl" fn subscribe_trades( +// client: *mut ExchangeClient, +// channel: *mut tokio::sync::mpsc::UnboundedSender, +// market: *mut c_char +// ) -> OpenLimitsResult { +// let call = move|| -> Result<(), OpenlimitsSharpError> { +// if channel.is_null() { +// return Err(OpenlimitsSharpError::InvalidArgument(String::from("channel is null"))); +// } +// let market_pair = c_str_to_string(market).map_err(|e| +// OpenlimitsSharpError::InvalidArgument(format!("Failed to parse market string. Invalid character on pos {}", e.valid_up_to())) +// )?; +// let (finish_tx, finish_rx) = tokio::sync::oneshot::channel::(); +// +// unsafe { +// (*channel).send( +// SubthreadCmd::Sub(Subscription::Trades( +// market_pair, +// ), finish_tx) +// ).map_err(|_| OpenlimitsSharpError::SubscribeException(String::from("failed to send subscription to handler")))?; +// +// let result = (*client).runtime.block_on(finish_rx).map_err(|_| OpenlimitsSharpError::SubscribeException(String::from("failed to get subscription result from handler")))?; +// +// match result { +// Ok(_) => Ok(()), +// Err(e) => Err(OpenlimitsSharpError::OpenLimitsError(e)) +// } +// } +// }; +// result_to_ffi(call()) +// } +// +// #[no_mangle] +// pub extern "cdecl" fn disconnect( +// channel: *mut tokio::sync::mpsc::UnboundedSender, +// ) { +// unsafe { +// let res = (*channel).send( +// SubthreadCmd::Disconnect +// ); +// res.map_err(|_| "Send error").expect("Failed to disconnect"); +// } +// } \ No newline at end of file diff --git a/build.rs b/build.rs index 5ea0c46b..86c289f6 100644 --- a/build.rs +++ b/build.rs @@ -1,42 +1,17 @@ -#[cfg(target_os = "windows")] -fn main() { - use std::process::Command; - use std::path::Path; - - let rustup_output = Command::new("rustup") - .arg("which") - .arg("rustc") - .output() - .expect("Couldn't get rustup output."); - let rustc_path = String::from_utf8(rustup_output.stdout).expect("Couldn't get toolchain path"); - let toolchain_path = Path::new(&rustc_path) - .parent().unwrap() - .parent().unwrap(); - let toolchain_triple = toolchain_path - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .map(|name| name.replace("stable-", "")) - .expect("Couldn't get toolchain triple."); - let architecture = if let Some(_) = toolchain_triple.find("x86_64") { - "x86_64" - } else { - "x86" - }; +fn main() { + #[cfg(feature = "bindings")] + { + use ligen::prelude::*; + use ligen_csharp::CSharpGenerator; - let source_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("redist").join(architecture); - let dll_path = source_path.join("gmp.dll"); - let lib_path = source_path.join("gmp.lib"); - let target_path = toolchain_path - .join("lib") - .join("rustlib") - .join(toolchain_triple) - .join("lib"); - let from_dll = target_path.join("gmp.dll"); - let from_lib = target_path.join("gmp.lib"); - std::fs::copy(dll_path, from_dll).expect(&format!("Couldn't copy dll from {}", from_dll.to_string())); - std::fs::copy(lib_path, from_lib).expect(&format!("Couldn't copy lib from {}", from_lib.to_string())); + match Project::current() { + Ok(project) => { + let csharp_generator = CSharpGenerator::default(); + csharp_generator.generate(&project).expect("Failed to generate C# bindings."); + }, + Err(e) => println!("e: {:#?}", e) + } + } } -#[cfg(not(target_os = "windows"))] -fn main() {} \ No newline at end of file diff --git a/crates/openlimits-binance/Cargo.toml b/crates/openlimits-binance/Cargo.toml index bc2ee210..d9c2c60c 100644 --- a/crates/openlimits-binance/Cargo.toml +++ b/crates/openlimits-binance/Cargo.toml @@ -1,13 +1,18 @@ [package] name = "openlimits-binance" -version = "0.1.0" -authors = ["Danilo Guanabara "] +version = "0.1.1-alpha.0" +authors = ["Danilo Guanabara "] edition = "2018" +description = "Binance implementation for OpenLimits." +license = "BSD-2-Clause" +repository = "https://github.com/nash-io/openlimits" +keywords = ["cryptocurrency", "exchange", "openlimits", "api"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -exchange = { path = "../openlimits-exchange" } +openlimits-exchange = { path = "../openlimits-exchange" } async-trait = "0.1" serde = { version = "1.0.126", features = ["derive"] } serde_json = "1.0.64" @@ -23,4 +28,4 @@ sha2 = "0.9.1" url = "2.1.1" futures = "0.3" tokio = "1.6.0" -tokio-tungstenite = { version = "0.13", features = ["tls"] } +tokio-tungstenite = { version = "0.13", features = ["tls"] } \ No newline at end of file diff --git a/crates/openlimits-binance/src/binance_parameters.rs b/crates/openlimits-binance/src/binance_parameters.rs index 6ec01603..2e46eb3b 100644 --- a/crates/openlimits-binance/src/binance_parameters.rs +++ b/crates/openlimits-binance/src/binance_parameters.rs @@ -1,9 +1,10 @@ use super::BinanceCredentials; +use openlimits_exchange::exchange::Environment; /// This struct represents the type of environment that will be used and receives a boolean and the credentials as parameters. #[derive(Default, Clone, Debug)] pub struct BinanceParameters { - pub sandbox: bool, + pub environment: Environment, pub credentials: Option, } @@ -11,15 +12,15 @@ impl BinanceParameters { /// Sandbox environment pub fn sandbox() -> Self { Self { - sandbox: true, + environment: Environment::Sandbox, ..Default::default() } } /// Production environment - pub fn prod() -> Self { + pub fn production() -> Self { Self { - sandbox: false, + environment: Environment::Production, ..Default::default() } } diff --git a/crates/openlimits-binance/src/client/account.rs b/crates/openlimits-binance/src/client/account.rs index 7b390d3e..934f0f53 100644 --- a/crates/openlimits-binance/src/client/account.rs +++ b/crates/openlimits-binance/src/client/account.rs @@ -1,13 +1,9 @@ use std::collections::HashMap; use rust_decimal::prelude::*; use serde_json::json; -use exchange::errors::OpenLimitsError; -use crate::model::{ - AccountInformation, AllOrderReq, Balance, Order, ORDER_SIDE_BUY, ORDER_SIDE_SELL, ORDER_TYPE_LIMIT, - ORDER_TYPE_LIMIT_MAKER, ORDER_TYPE_MARKET, OrderCanceled, OrderRequest, TimeInForce, - TradeHistory, TradeHistoryReq, -}; -use exchange::traits::info::MarketPair; +use crate::model::{AccountInformation, AllOrderReq, Balance, Order, ORDER_SIDE_BUY, ORDER_SIDE_SELL, ORDER_TYPE_LIMIT, ORDER_TYPE_LIMIT_MAKER, ORDER_TYPE_MARKET, OrderCanceled, OrderRequest, TimeInForce, TradeHistory, TradeHistoryReq, MarketPair}; +use openlimits_exchange::errors::OpenLimitsError; +use openlimits_exchange::traits::info::MarketPairInfo; use super::BaseClient; use super::shared::Result; @@ -79,17 +75,17 @@ impl BaseClient { // Place a LIMIT order - BUY pub async fn limit_buy( &self, - pair: MarketPair, + pair: MarketPairInfo, qty: Decimal, price: Decimal, tif: TimeInForce, post_only: bool, ) -> Result { - let order_type = match post_only { - true => ORDER_TYPE_LIMIT_MAKER, - false => ORDER_TYPE_LIMIT, - } - .to_string(); + let (order_type, time_in_force) = match post_only { + true => (ORDER_TYPE_LIMIT_MAKER.to_string(), None), + false => (ORDER_TYPE_LIMIT.to_string(), Some(tif)), + }; + let buy: OrderRequest = OrderRequest { symbol: pair.symbol, quantity: qty.round_dp(pair.base_increment.normalize().scale()), @@ -99,7 +95,7 @@ impl BaseClient { )), order_side: ORDER_SIDE_BUY.to_string(), order_type, - time_in_force: Some(tif), + time_in_force, }; let transaction = self @@ -114,17 +110,17 @@ impl BaseClient { pub async fn limit_sell( &self, - pair: MarketPair, + pair: MarketPairInfo, qty: Decimal, price: Decimal, tif: TimeInForce, post_only: bool, ) -> Result { - let order_type = match post_only { - true => ORDER_TYPE_LIMIT_MAKER, - false => ORDER_TYPE_LIMIT, - } - .to_string(); + let (order_type, time_in_force) = match post_only { + true => (ORDER_TYPE_LIMIT_MAKER.to_string(), None), + false => (ORDER_TYPE_LIMIT.to_string(), Some(tif)), + }; + let sell: OrderRequest = OrderRequest { symbol: pair.symbol, quantity: qty.round_dp(pair.base_increment.normalize().scale()), @@ -134,7 +130,7 @@ impl BaseClient { )), order_side: ORDER_SIDE_SELL.to_string(), order_type, - time_in_force: Some(tif), + time_in_force, }; let transaction = self @@ -146,7 +142,7 @@ impl BaseClient { } // Place a MARKET order - BUY - pub async fn market_buy(&self, pair: MarketPair, qty: Decimal) -> Result { + pub async fn market_buy(&self, pair: MarketPairInfo, qty: Decimal) -> Result { let buy: OrderRequest = OrderRequest { symbol: pair.symbol, quantity: qty.round_dp(pair.base_increment.normalize().scale()), @@ -165,7 +161,7 @@ impl BaseClient { } // Place a MARKET order - SELL - pub async fn market_sell(&self, pair: MarketPair, qty: Decimal) -> Result { + pub async fn market_sell(&self, pair: MarketPairInfo, qty: Decimal) -> Result { let sell: OrderRequest = OrderRequest { symbol: pair.symbol, quantity: qty.round_dp(pair.base_increment.normalize().scale()), @@ -192,7 +188,8 @@ impl BaseClient { Ok(order_canceled) } - pub async fn cancel_all_orders(&self, symbol: &str) -> Result> { + pub async fn cancel_all_orders>(&self, symbol: P) -> Result> { + let symbol = symbol.into().0; let params = json! {{"symbol":symbol}}; let orders_canceled = self .transport diff --git a/crates/openlimits-binance/src/client/market.rs b/crates/openlimits-binance/src/client/market.rs index a236fa3e..a6d4ce70 100644 --- a/crates/openlimits-binance/src/client/market.rs +++ b/crates/openlimits-binance/src/client/market.rs @@ -1,21 +1,20 @@ use serde_json::json; use serde_json::Value; use super::BaseClient; -use crate::model::{ - BookTickers, KlineParams, KlineSummaries, KlineSummary, OrderBook, PriceStats, Prices, - SymbolPrice, Ticker, -}; -pub use exchange::OpenLimitsError; +use crate::model::{BookTickers, KlineParams, KlineSummaries, KlineSummary, OrderBook, PriceStats, Prices, SymbolPrice, Ticker, MarketPair}; +pub use openlimits_exchange::OpenLimitsError; use rust_decimal::prelude::Decimal; use super::shared::Result; // Market Data endpoints impl BaseClient { // Order book (Default 100; max 100) - pub async fn get_depth(&self, symbol: &str, limit: I) -> Result + pub async fn get_depth(&self, symbol: S, limit: I) -> Result where I: Into>, + S: Into { + let symbol = format!("{}", symbol.into().0); let limit = limit.into().unwrap_or(100); let params = json! {{"symbol": symbol, "limit": limit}}; @@ -31,7 +30,8 @@ impl BaseClient { } // Latest price for ONE symbol. - pub async fn get_price(&self, symbol: &str) -> Result { + pub async fn get_price>(&self, symbol: S) -> Result { + let symbol = format!("{}", symbol.into().0); let params = json! {{"symbol": symbol}}; let price = self diff --git a/crates/openlimits-binance/src/client/websocket.rs b/crates/openlimits-binance/src/client/websocket.rs index 0632e532..30026c71 100644 --- a/crates/openlimits-binance/src/client/websocket.rs +++ b/crates/openlimits-binance/src/client/websocket.rs @@ -6,21 +6,22 @@ use serde::{de, Deserialize, Serialize}; use serde_json::Value; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; -use exchange::errors::OpenLimitsError; -pub use crate::{ +use openlimits_exchange::errors::OpenLimitsError; +use crate::{ BinanceParameters, model::websocket::{BinanceSubscription, BinanceWebsocketMessage}, }; -pub use exchange::{ +use openlimits_exchange::{ model::websocket::OpenLimitsWebSocketMessage, model::websocket::Subscription, model::websocket::WebSocketResponse, }; -use exchange::traits::stream::{ExchangeWs, Subscriptions}; +use openlimits_exchange::traits::stream::{ExchangeWs, Subscriptions}; use super::shared::Result; +use openlimits_exchange::exchange::Environment; -const WS_URL_PROD: &str = "wss://stream.openlimits-binance.com:9443/stream"; -const WS_URL_SANDBOX: &str = "wss://testnet.openlimits-binance.vision/stream"; +const WS_URL_PROD: &str = "wss://stream.binance.com:9443/stream"; +const WS_URL_SANDBOX: &str = "wss://testnet.binance.vision/stream"; #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] @@ -67,11 +68,11 @@ impl ExchangeWs for BinanceWebsocket { .collect::>() .join("/"); - let ws_url = match self.parameters.sandbox { - true => WS_URL_SANDBOX, - false => WS_URL_PROD, + let ws_url = match self.parameters.environment { + Environment::Sandbox => WS_URL_SANDBOX, + Environment::Production => WS_URL_PROD, }; - let endpoint = url::Url::parse(&format!("{}?streams={}", ws_url, streams)) + let endpoint = url::Url::parse(&format!("{}?streams={}", ws_url, streams.to_lowercase())) .map_err(OpenLimitsError::UrlParserError)?; let (ws_stream, _) = connect_async(endpoint).await?; @@ -179,9 +180,8 @@ impl Display for BinanceSubscription { impl From for BinanceSubscription { fn from(subscription: Subscription) -> Self { match subscription { - Subscription::OrderBookUpdates(symbol) => BinanceSubscription::Depth(symbol, None), - Subscription::Trades(symbol) => BinanceSubscription::Trade(symbol), - _ => unimplemented!(), + Subscription::OrderBookUpdates(symbol) => BinanceSubscription::Depth(crate::model::MarketPair::from(symbol).0, None), + Subscription::Trades(symbol) => BinanceSubscription::Trade(crate::model::MarketPair::from(symbol).0) } } } diff --git a/crates/openlimits-binance/src/lib.rs b/crates/openlimits-binance/src/lib.rs index 4e4a2475..2c518039 100644 --- a/crates/openlimits-binance/src/lib.rs +++ b/crates/openlimits-binance/src/lib.rs @@ -2,7 +2,7 @@ pub mod model; -pub use exchange::shared; +pub use openlimits_exchange::shared; use async_trait::async_trait; use model::KlineSummaries; @@ -10,7 +10,7 @@ use transport::Transport; use client::BaseClient; use std::convert::TryFrom; use model::{websocket::TradeMessage, SymbolFilter, ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET}; -use exchange::{ +use openlimits_exchange::{ errors::OpenLimitsError, model::{ AskBid, Balance, CancelAllOrdersRequest, CancelOrderRequest, Candle, @@ -21,7 +21,7 @@ use exchange::{ } }; -use exchange::Result; +use openlimits_exchange::Result; mod binance_content_error; mod binance_credentials; @@ -36,8 +36,10 @@ pub use transport::*; pub mod client; pub use client::websocket::BinanceWebsocket; -use exchange::traits::info::{ExchangeInfo, ExchangeInfoRetrieval, MarketPair, MarketPairHandle}; -use exchange::traits::{Exchange, ExchangeMarketData, ExchangeAccount}; +use openlimits_exchange::traits::info::{ExchangeInfo, ExchangeInfoRetrieval, MarketPairInfo, MarketPairHandle}; +use openlimits_exchange::traits::{Exchange, ExchangeMarketData, ExchangeAccount}; +use openlimits_exchange::exchange::Environment; +use openlimits_exchange::model::market_pair::MarketPair; /// The main struct of the openlimits-binance module #[derive(Clone)] @@ -59,14 +61,14 @@ impl Exchange for Binance { transport: Transport::with_credential( &credentials.api_key, &credentials.api_secret, - parameters.sandbox, + parameters.environment == Environment::Sandbox, )?, }, }, None => Binance { exchange_info: ExchangeInfo::new(), client: BaseClient { - transport: Transport::new(parameters.sandbox)?, + transport: Transport::new(parameters.environment == Environment::Sandbox)?, }, }, }; @@ -82,7 +84,7 @@ impl Exchange for Binance { #[async_trait] impl ExchangeInfoRetrieval for Binance { - async fn retrieve_pairs(&self) -> Result> { + async fn retrieve_pairs(&self) -> Result> { self.client.get_exchange_info().await.map(|v| { v.symbols .into_iter() @@ -113,7 +115,7 @@ impl ExchangeInfoRetrieval for Binance { }) .expect("Couldn't find tick size."); - MarketPair { + MarketPairInfo { base: symbol.base_asset, quote: symbol.quote_asset, symbol: symbol.symbol, @@ -133,8 +135,9 @@ impl ExchangeInfoRetrieval for Binance { .await } - async fn get_pair(&self, name: &str) -> Result { - self.exchange_info.get_pair(name) + async fn get_pair(&self, market_pair: &MarketPair) -> Result { + let name = crate::model::MarketPair::from(market_pair.clone()).0; + self.exchange_info.get_pair(&name) } } @@ -142,14 +145,14 @@ impl ExchangeInfoRetrieval for Binance { impl ExchangeMarketData for Binance { async fn order_book(&self, req: &OrderBookRequest) -> Result { self.client - .get_depth(req.market_pair.as_str(), None) + .get_depth(req.market_pair.clone(), None) .await .map(Into::into) } async fn get_price_ticker(&self, req: &GetPriceTickerRequest) -> Result { self.client - .get_price(&req.market_pair) + .get_price(req.market_pair.clone()) .await .map(Into::into) } @@ -171,7 +174,7 @@ impl ExchangeMarketData for Binance { #[async_trait] impl ExchangeAccount for Binance { async fn limit_buy(&self, req: &OpenLimitOrderRequest) -> Result { - let pair = self.exchange_info.get_pair(&req.market_pair)?.read()?; + let pair = self.get_pair(&req.market_pair).await?.read()?; self.client .limit_buy( pair, @@ -184,7 +187,7 @@ impl ExchangeAccount for Binance { .map(Into::into) } async fn limit_sell(&self, req: &OpenLimitOrderRequest) -> Result { - let pair = self.exchange_info.get_pair(&req.market_pair)?.read()?; + let pair = self.get_pair(&req.market_pair).await?.read()?; self.client .limit_sell( pair, @@ -198,11 +201,11 @@ impl ExchangeAccount for Binance { } async fn market_buy(&self, req: &OpenMarketOrderRequest) -> Result { - let pair = self.exchange_info.get_pair(&req.market_pair)?.read()?; + let pair = self.get_pair(&req.market_pair).await?.read()?; self.client.market_buy(pair, req.size).await.map(Into::into) } async fn market_sell(&self, req: &OpenMarketOrderRequest) -> Result { - let pair = self.exchange_info.get_pair(&req.market_pair)?.read()?; + let pair = self.get_pair(&req.market_pair).await?.read()?; self.client .market_sell(pair, req.size) .await @@ -227,7 +230,7 @@ impl ExchangeAccount for Binance { async fn cancel_all_orders(&self, req: &CancelAllOrdersRequest) -> Result> { if let Some(pair) = req.market_pair.as_ref() { self.client - .cancel_all_orders(pair) + .cancel_all_orders(pair.clone()) .await .map(|v| v.into_iter().map(Into::into).collect()) } else { @@ -315,7 +318,7 @@ impl From for Vec { false => Side::Sell, }, liquidity: None, - created_at: trade_message.event_time, + created_at: trade_message.event_time.to_string(), }] } } @@ -336,7 +339,7 @@ impl From for Trade { false => Side::Buy, }, liquidity: None, - created_at: trade.trade_order_time, + created_at: trade.trade_order_time.to_string(), } } } @@ -425,7 +428,7 @@ impl From for Trade { true => Some(Liquidity::Maker), false => Some(Liquidity::Taker), }, - created_at: trade_history.time, + created_at: trade_history.time.to_string(), } } } @@ -444,9 +447,10 @@ impl TryFrom<&GetOrderHistoryRequest> for model::AllOrderReq { fn try_from(req: &GetOrderHistoryRequest) -> Result { Ok(Self { paginator: req.paginator.clone().map(|p| p.into()), - symbol: req.market_pair.clone().ok_or_else(|| { - OpenLimitsError::MissingParameter("market_pair parameter is required.".to_string()) - })?, + symbol: req.market_pair + .clone() + .map(|market| crate::model::MarketPair::from(market).0) + .ok_or_else(|| OpenLimitsError::MissingParameter("market_pair parameter is required.".to_string()))?, }) } } @@ -456,9 +460,10 @@ impl TryFrom<&TradeHistoryRequest> for model::TradeHistoryReq { fn try_from(trade_history: &TradeHistoryRequest) -> Result { Ok(Self { paginator: trade_history.paginator.clone().map(|p| p.into()), - symbol: trade_history.market_pair.clone().ok_or_else(|| { - OpenLimitsError::MissingParameter("market_pair parameter is required.".to_string()) - })?, + symbol: trade_history.market_pair + .clone() + .map(|market| crate::model::MarketPair::from(market).0) + .ok_or_else(|| OpenLimitsError::MissingParameter("market_pair parameter is required.".to_string()))?, }) } } @@ -466,11 +471,11 @@ impl TryFrom<&TradeHistoryRequest> for model::TradeHistoryReq { impl From<&GetHistoricRatesRequest> for model::KlineParams { fn from(req: &GetHistoricRatesRequest) -> Self { let interval: &str = req.interval.into(); - + let symbol = crate::model::MarketPair::from(req.market_pair.clone()).0; Self { interval: String::from(interval), paginator: req.paginator.clone().map(|d| d.into()), - symbol: req.market_pair.clone(), + symbol, } } } diff --git a/crates/openlimits-binance/src/model/market_pair.rs b/crates/openlimits-binance/src/model/market_pair.rs new file mode 100644 index 00000000..531830f0 --- /dev/null +++ b/crates/openlimits-binance/src/model/market_pair.rs @@ -0,0 +1,11 @@ +use openlimits_exchange::model::market_pair::MarketPair as OMarketPair; +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MarketPair(pub String); + +impl From for MarketPair { + fn from(from: OMarketPair) -> MarketPair { + MarketPair(format!("{}{}", from.0, from.1).to_uppercase()) + } +} \ No newline at end of file diff --git a/crates/openlimits-binance/src/model/mod.rs b/crates/openlimits-binance/src/model/mod.rs index 640d4c10..56dadbf3 100644 --- a/crates/openlimits-binance/src/model/mod.rs +++ b/crates/openlimits-binance/src/model/mod.rs @@ -7,6 +7,7 @@ pub const ORDER_SIDE_BUY: &str = "BUY"; pub const ORDER_SIDE_SELL: &str = "SELL"; pub const TIME_IN_FORCE_GTC: &str = "GTC"; +mod market_pair; mod account_information; mod all_order_req; mod ask_bid; @@ -46,6 +47,7 @@ mod transaction; mod user_data_stream; pub mod websocket; +pub use market_pair::MarketPair; pub use account_information::AccountInformation; pub use all_order_req::AllOrderReq; pub use ask_bid::AskBid; diff --git a/crates/openlimits-binance/src/model/order.rs b/crates/openlimits-binance/src/model/order.rs index 9e6b1449..802ef9cf 100644 --- a/crates/openlimits-binance/src/model/order.rs +++ b/crates/openlimits-binance/src/model/order.rs @@ -12,19 +12,21 @@ pub struct Order { pub symbol: String, pub order_id: u64, pub client_order_id: String, - #[serde(with = "string_to_decimal")] + #[serde(default, with = "string_to_decimal")] pub price: Decimal, - #[serde(with = "string_to_decimal")] + #[serde(default, with = "string_to_decimal")] pub orig_qty: Decimal, - #[serde(with = "string_to_decimal")] + #[serde(default, with = "string_to_decimal")] pub executed_qty: Decimal, + #[serde(default)] pub status: OrderStatus, - pub time_in_force: String, - #[serde(rename = "type")] + #[serde(default)] + pub time_in_force: Option, + #[serde(default, rename = "type")] pub type_name: String, - pub side: String, - #[serde(with = "string_to_opt_decimal")] #[serde(default)] + pub side: String, + #[serde(default, with = "string_to_opt_decimal")] pub stop_price: Option, #[serde(default)] pub iceberg_qty: Option, diff --git a/crates/openlimits-binance/src/model/order_status.rs b/crates/openlimits-binance/src/model/order_status.rs index 726aacb1..6570b45b 100644 --- a/crates/openlimits-binance/src/model/order_status.rs +++ b/crates/openlimits-binance/src/model/order_status.rs @@ -12,4 +12,10 @@ pub enum OrderStatus { PendingCancel, Rejected, Expired, +} + +impl Default for OrderStatus { + fn default() -> Self { + Self::New + } } \ No newline at end of file diff --git a/crates/openlimits-binance/src/transport.rs b/crates/openlimits-binance/src/transport.rs index a3ebb072..80844802 100644 --- a/crates/openlimits-binance/src/transport.rs +++ b/crates/openlimits-binance/src/transport.rs @@ -9,7 +9,7 @@ use serde::Serialize; use sha2::Sha256; use url::Url; use crate::BinanceContentError; -use exchange::OpenLimitsError; +use openlimits_exchange::OpenLimitsError; use super::shared::Result; type HmacSha256 = Hmac; @@ -55,9 +55,9 @@ impl Transport { fn get_base_url(sandbox: bool) -> String { if sandbox { - String::from("https://testnet.openlimits-binance.vision") + String::from("https://testnet.binance.vision") } else { - String::from("https://api.openlimits-binance.com") + String::from("https://api.binance.com") } } diff --git a/crates/openlimits-exchange/.gitignore b/crates/openlimits-exchange/.gitignore new file mode 100644 index 00000000..fa8d85ac --- /dev/null +++ b/crates/openlimits-exchange/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target diff --git a/crates/openlimits-exchange/Cargo.toml b/crates/openlimits-exchange/Cargo.toml index bbf9dfad..483dce4d 100644 --- a/crates/openlimits-exchange/Cargo.toml +++ b/crates/openlimits-exchange/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openlimits-exchange" -version = "0.1.1-alpha.0" +version = "0.2.1-alpha.0" authors = ["Danilo Guanabara "] edition = "2018" description = "The exchange traits of OpenLimits." diff --git a/crates/openlimits-exchange/src/exchange.rs b/crates/openlimits-exchange/src/exchange.rs index 8663cbc1..72d0b3f2 100644 --- a/crates/openlimits-exchange/src/exchange.rs +++ b/crates/openlimits-exchange/src/exchange.rs @@ -1,15 +1,22 @@ -use messaging::Subscriber; -use async_trait::async_trait; - -#[async_trait] -pub trait Exchange: Subscriber { - type InitializationParameters; - - fn endpoint_url(environment: Environment) -> &'static str; - async fn new(parameters: Self::InitializationParameters) -> Result where Self: Sized; -} +// use messaging::Subscriber; +// use async_trait::async_trait; +// +// #[async_trait] +// pub trait Exchange: Subscriber { +// type InitializationParameters; +// +// fn endpoint_url(environment: Environment) -> &'static str; +// async fn new(parameters: Self::InitializationParameters) -> Result where Self: Sized; +// } +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Environment { Production, Sandbox } + +impl Default for Environment { + fn default() -> Self { + Self::Production + } +} diff --git a/crates/openlimits-exchange/src/lib.rs b/crates/openlimits-exchange/src/lib.rs index 8dcf1379..e62b74c6 100644 --- a/crates/openlimits-exchange/src/lib.rs +++ b/crates/openlimits-exchange/src/lib.rs @@ -1,11 +1,9 @@ -// mod openlimits-exchange; -// pub use crate::openlimits-exchange::*; -pub mod market; pub mod message; pub mod errors; pub mod prelude; pub mod model; pub mod shared; pub mod traits; +pub mod exchange; pub use errors::*; \ No newline at end of file diff --git a/crates/openlimits-exchange/src/market/mod.rs b/crates/openlimits-exchange/src/market/mod.rs deleted file mode 100644 index 06d1f16d..00000000 --- a/crates/openlimits-exchange/src/market/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod symbol; - -use symbol::Symbol; - -#[derive(Debug, Clone)] -pub struct MarketPair(pub Symbol, pub Symbol); diff --git a/crates/openlimits-exchange/src/market/symbol.rs b/crates/openlimits-exchange/src/market/symbol.rs deleted file mode 100644 index cbe617c0..00000000 --- a/crates/openlimits-exchange/src/market/symbol.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[derive(Debug, Clone)] -pub enum Symbol { - ETH, - BTC, - NEO, - USDC -} diff --git a/crates/openlimits-exchange/src/message/subscription.rs b/crates/openlimits-exchange/src/message/subscription.rs index 358df9b0..76eacbdf 100644 --- a/crates/openlimits-exchange/src/message/subscription.rs +++ b/crates/openlimits-exchange/src/message/subscription.rs @@ -1,4 +1,4 @@ -use crate::market::MarketPair; +use crate::model::market_pair::MarketPair; #[derive(Debug, Clone)] pub enum Subscription { diff --git a/crates/openlimits-exchange/src/model/currency.rs b/crates/openlimits-exchange/src/model/currency.rs new file mode 100644 index 00000000..26117800 --- /dev/null +++ b/crates/openlimits-exchange/src/model/currency.rs @@ -0,0 +1,19 @@ +use serde::{Serialize, Deserialize}; +use std::fmt; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[allow(missing_docs)] +pub enum Currency { + BTC, + ETH, + Other(String) +} + +impl fmt::Display for Currency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Other(s) => write!(f, "{}", s), + _ => write!(f, "{:?}", self) + } + } +} diff --git a/crates/openlimits-exchange/src/model/market_pair.rs b/crates/openlimits-exchange/src/model/market_pair.rs new file mode 100644 index 00000000..9b143377 --- /dev/null +++ b/crates/openlimits-exchange/src/model/market_pair.rs @@ -0,0 +1,12 @@ +use serde::{Serialize, Deserialize}; +pub use crate::model::currency::Currency; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[allow(missing_docs)] +pub struct MarketPair(pub Currency, pub Currency); + +impl MarketPair { + pub fn inverse(&self) -> MarketPair { + MarketPair(self.1.clone(), self.0.clone()) + } +} \ No newline at end of file diff --git a/crates/openlimits-exchange/src/model/mod.rs b/crates/openlimits-exchange/src/model/mod.rs index 6a9bf508..26f87d1c 100644 --- a/crates/openlimits-exchange/src/model/mod.rs +++ b/crates/openlimits-exchange/src/model/mod.rs @@ -1,23 +1,25 @@ //! This module provides models that are used in the openlimits-exchange module -mod request; -mod ask_bid; -mod balance; -mod candle; -mod interval; -mod liquidity; -mod order_canceled; -mod order_filter; -mod order_status; -mod order_type; -mod order; -mod paginator; -mod side; -mod ticker; -mod time_in_force_visitor; -mod time_in_force; -mod trade; -mod transaction; +pub mod request; +pub mod ask_bid; +pub mod balance; +pub mod candle; +pub mod interval; +pub mod liquidity; +pub mod order_canceled; +pub mod order_filter; +pub mod order_status; +pub mod order_type; +pub mod order; +pub mod paginator; +pub mod side; +pub mod ticker; +pub mod time_in_force_visitor; +pub mod time_in_force; +pub mod trade; +pub mod transaction; +pub mod currency; +pub mod market_pair; pub use request::*; pub use ask_bid::AskBid; diff --git a/crates/openlimits-exchange/src/model/request/cancel_all_order_request.rs b/crates/openlimits-exchange/src/model/request/cancel_all_order_request.rs index 8ea34b7a..c5dcfe0e 100644 --- a/crates/openlimits-exchange/src/model/request/cancel_all_order_request.rs +++ b/crates/openlimits-exchange/src/model/request/cancel_all_order_request.rs @@ -1,9 +1,10 @@ use derive_more::Constructor; use serde::Deserialize; use serde::Serialize; +use crate::model::market_pair::MarketPair; /// This struct represents the cancellation of all orders #[derive(Serialize, Deserialize, Clone, Constructor, Debug, PartialEq)] pub struct CancelAllOrdersRequest { - pub market_pair: Option, + pub market_pair: Option, } \ No newline at end of file diff --git a/crates/openlimits-exchange/src/model/request/get_historic_rates_request.rs b/crates/openlimits-exchange/src/model/request/get_historic_rates_request.rs index d2db566d..b2611bb0 100644 --- a/crates/openlimits-exchange/src/model/request/get_historic_rates_request.rs +++ b/crates/openlimits-exchange/src/model/request/get_historic_rates_request.rs @@ -2,11 +2,12 @@ use derive_more::Constructor; use serde::Deserialize; use serde::Serialize; use crate::model::{Paginator, Interval}; +use crate::model::market_pair::MarketPair; /// This struct represents the historic of the rates #[derive(Serialize, Deserialize, Clone, Constructor, Debug)] pub struct GetHistoricRatesRequest { - pub market_pair: String, + pub market_pair: MarketPair, pub paginator: Option, pub interval: Interval, } \ No newline at end of file diff --git a/crates/openlimits-exchange/src/model/request/get_order_history_request.rs b/crates/openlimits-exchange/src/model/request/get_order_history_request.rs index bdd5ea55..57e9a83f 100644 --- a/crates/openlimits-exchange/src/model/request/get_order_history_request.rs +++ b/crates/openlimits-exchange/src/model/request/get_order_history_request.rs @@ -2,11 +2,12 @@ use derive_more::Constructor; use serde::Deserialize; use serde::Serialize; use crate::model::{OrderStatus, Paginator}; +use crate::model::market_pair::MarketPair; /// This struct represents the historic of the orders #[derive(Serialize, Deserialize, Clone, Constructor, Debug)] pub struct GetOrderHistoryRequest { - pub market_pair: Option, + pub market_pair: Option, pub order_status: Option>, pub paginator: Option, } \ No newline at end of file diff --git a/crates/openlimits-exchange/src/model/request/get_price_ticker_request.rs b/crates/openlimits-exchange/src/model/request/get_price_ticker_request.rs index 330b8865..f44c6f77 100644 --- a/crates/openlimits-exchange/src/model/request/get_price_ticker_request.rs +++ b/crates/openlimits-exchange/src/model/request/get_price_ticker_request.rs @@ -1,9 +1,10 @@ use derive_more::Constructor; use serde::Deserialize; use serde::Serialize; +use crate::model::market_pair::MarketPair; /// This struct represents the ticker price. -#[derive(Serialize, Deserialize, Clone, Constructor, Debug, Default, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Constructor, Debug, PartialEq)] pub struct GetPriceTickerRequest { - pub market_pair: String, + pub market_pair: MarketPair, } \ No newline at end of file diff --git a/crates/openlimits-exchange/src/model/request/open_limit_order_request.rs b/crates/openlimits-exchange/src/model/request/open_limit_order_request.rs index a7c7220f..681ded2f 100644 --- a/crates/openlimits-exchange/src/model/request/open_limit_order_request.rs +++ b/crates/openlimits-exchange/src/model/request/open_limit_order_request.rs @@ -3,12 +3,13 @@ use rust_decimal::prelude::Decimal; use serde::Deserialize; use serde::Serialize; use crate::model::TimeInForce; +use crate::model::market_pair::MarketPair; /// This struct represents an open limit order -#[derive(Serialize, Deserialize, Clone, Constructor, Debug, Default, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Constructor, Debug, PartialEq)] pub struct OpenLimitOrderRequest { pub client_order_id: Option, - pub market_pair: String, + pub market_pair: MarketPair, pub size: Decimal, pub price: Decimal, pub time_in_force: TimeInForce, diff --git a/crates/openlimits-exchange/src/model/request/open_market_order_request.rs b/crates/openlimits-exchange/src/model/request/open_market_order_request.rs index 2195b302..6cdfa169 100644 --- a/crates/openlimits-exchange/src/model/request/open_market_order_request.rs +++ b/crates/openlimits-exchange/src/model/request/open_market_order_request.rs @@ -2,11 +2,12 @@ use derive_more::Constructor; use rust_decimal::prelude::Decimal; use serde::Deserialize; use serde::Serialize; +use crate::model::market_pair::MarketPair; /// This struct represents an open market order -#[derive(Serialize, Deserialize, Clone, Constructor, Debug, Default, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Constructor, Debug, PartialEq)] pub struct OpenMarketOrderRequest { pub client_order_id: Option, - pub market_pair: String, + pub market_pair: MarketPair, pub size: Decimal, } \ No newline at end of file diff --git a/crates/openlimits-exchange/src/model/request/order_book_request.rs b/crates/openlimits-exchange/src/model/request/order_book_request.rs index cf45293f..7f7446f2 100644 --- a/crates/openlimits-exchange/src/model/request/order_book_request.rs +++ b/crates/openlimits-exchange/src/model/request/order_book_request.rs @@ -1,9 +1,10 @@ use derive_more::Constructor; use serde::Deserialize; use serde::Serialize; +use crate::model::market_pair::MarketPair; /// This struct represents an order book request -#[derive(Serialize, Deserialize, Clone, Constructor, Debug, Default, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Constructor, Debug, PartialEq)] pub struct OrderBookRequest { - pub market_pair: String, + pub market_pair: MarketPair, } \ No newline at end of file diff --git a/crates/openlimits-exchange/src/model/request/trade_history_request.rs b/crates/openlimits-exchange/src/model/request/trade_history_request.rs index af2f8397..40110f0a 100644 --- a/crates/openlimits-exchange/src/model/request/trade_history_request.rs +++ b/crates/openlimits-exchange/src/model/request/trade_history_request.rs @@ -1,11 +1,12 @@ use serde::Deserialize; use serde::Serialize; use crate::model::Paginator; +use crate::model::market_pair::MarketPair; /// This struct represents the trade history #[derive(Serialize, Deserialize, Default)] pub struct TradeHistoryRequest { - pub market_pair: Option, + pub market_pair: Option, pub order_id: Option, pub paginator: Option, } \ No newline at end of file diff --git a/crates/openlimits-exchange/src/model/trade.rs b/crates/openlimits-exchange/src/model/trade.rs index fac27965..9ccf9ed2 100644 --- a/crates/openlimits-exchange/src/model/trade.rs +++ b/crates/openlimits-exchange/src/model/trade.rs @@ -17,5 +17,5 @@ pub struct Trade { pub fees: Option, pub side: Side, pub liquidity: Option, - pub created_at: u64, + pub created_at: String, } \ No newline at end of file diff --git a/crates/openlimits-exchange/src/model/transaction.rs b/crates/openlimits-exchange/src/model/transaction.rs index e48cb08e..43cc091c 100644 --- a/crates/openlimits-exchange/src/model/transaction.rs +++ b/crates/openlimits-exchange/src/model/transaction.rs @@ -9,4 +9,4 @@ pub struct Transaction { pub market_pair: String, pub client_order_id: Option, pub created_at: u64, -} \ No newline at end of file +} diff --git a/crates/openlimits-exchange/src/model/websocket.rs b/crates/openlimits-exchange/src/model/websocket.rs index 7bee5494..aa25ea76 100644 --- a/crates/openlimits-exchange/src/model/websocket.rs +++ b/crates/openlimits-exchange/src/model/websocket.rs @@ -2,11 +2,12 @@ use super::{OrderBookResponse, Trade}; use crate::model::{OrderStatus, OrderType, Side}; use serde::{Deserialize, Serialize}; use std::ops::Range; +use crate::model::market_pair::MarketPair; /// This struct represents the account order #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] pub struct AccountOrders { - pub market: Option, + pub market: Option, pub order_type: Option>, pub buy_or_sell: Option, pub range: Option>, @@ -16,12 +17,12 @@ pub struct AccountOrders { /// This enum represents a subscription #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] pub enum Subscription { - Ticker(String), // symbol - OrderBookUpdates(String), // symbol - Trades(String), // symbol - AccountTrades(String), // symbol - AccountBalance(String), // symbol - AccountOrders(AccountOrders), + // Ticker(MarketPair), // symbol + OrderBookUpdates(MarketPair), // symbol + Trades(MarketPair), // symbol + // AccountTrades(MarketPair), // symbol + // AccountBalance(MarketPair), // symbol + // AccountOrders(AccountOrders), } /// This enum represents a websocket response diff --git a/crates/openlimits-exchange/src/traits/info/exchange_info.rs b/crates/openlimits-exchange/src/traits/info/exchange_info.rs index 25ec88b7..a01332cb 100644 --- a/crates/openlimits-exchange/src/traits/info/exchange_info.rs +++ b/crates/openlimits-exchange/src/traits/info/exchange_info.rs @@ -5,12 +5,12 @@ use crate::errors::OpenLimitsError; use super::shared::Result; use super::ExchangeInfoRetrieval; use super::MarketPairHandle; -use super::MarketPair; +use super::MarketPairInfo; /// This struct represents informations about the openlimits-exchange #[derive(Clone)] pub struct ExchangeInfo { - pairs: Arc>>>>, + pairs: Arc>>>>, } impl ExchangeInfo { diff --git a/crates/openlimits-exchange/src/traits/info/exchange_info_retrieval.rs b/crates/openlimits-exchange/src/traits/info/exchange_info_retrieval.rs index 558addba..1fe69f5f 100644 --- a/crates/openlimits-exchange/src/traits/info/exchange_info_retrieval.rs +++ b/crates/openlimits-exchange/src/traits/info/exchange_info_retrieval.rs @@ -1,12 +1,13 @@ use async_trait::async_trait; use super::shared::Result; use super::MarketPairHandle; -use super::MarketPair; +use super::MarketPairInfo; +use crate::model::market_pair::MarketPair; /// This struct represents the information retrieval #[async_trait] pub trait ExchangeInfoRetrieval: Sync { - async fn get_pair(&self, name: &str) -> Result; - async fn retrieve_pairs(&self) -> Result>; + async fn get_pair(&self, market_pair: &MarketPair) -> Result; + async fn retrieve_pairs(&self) -> Result>; async fn refresh_market_info(&self) -> Result>; } \ No newline at end of file diff --git a/crates/openlimits-exchange/src/traits/info/market_pair.rs b/crates/openlimits-exchange/src/traits/info/market_pair.rs index f6e6ed28..93017192 100644 --- a/crates/openlimits-exchange/src/traits/info/market_pair.rs +++ b/crates/openlimits-exchange/src/traits/info/market_pair.rs @@ -1,7 +1,9 @@ use rust_decimal::Decimal; +// TODO: Use MarketPair inside MarketPairInfo. + #[derive(Debug, Clone)] -pub struct MarketPair { +pub struct MarketPairInfo { pub base: String, pub quote: String, pub symbol: String, diff --git a/crates/openlimits-exchange/src/traits/info/market_pair_handle.rs b/crates/openlimits-exchange/src/traits/info/market_pair_handle.rs index e68c7bb0..d91df17f 100644 --- a/crates/openlimits-exchange/src/traits/info/market_pair_handle.rs +++ b/crates/openlimits-exchange/src/traits/info/market_pair_handle.rs @@ -2,20 +2,20 @@ use std::sync::Arc; use std::sync::RwLock; use crate::errors::OpenLimitsError; use super::shared::Result; -use super::MarketPair; +use super::MarketPairInfo; #[derive(Debug)] pub struct MarketPairHandle { - pub inner: Arc>, + pub inner: Arc>, } impl<'a> MarketPairHandle { - pub fn new(inner: Arc>) -> Self { + pub fn new(inner: Arc>) -> Self { Self { inner } } - pub fn read(&'a self) -> Result { + pub fn read(&'a self) -> Result { self.inner .read() .map(|guard| guard.clone()) diff --git a/crates/openlimits-exchange/src/traits/info/mod.rs b/crates/openlimits-exchange/src/traits/info/mod.rs index 31916951..a92a4584 100644 --- a/crates/openlimits-exchange/src/traits/info/mod.rs +++ b/crates/openlimits-exchange/src/traits/info/mod.rs @@ -8,7 +8,7 @@ mod utils; pub use exchange_info_retrieval::ExchangeInfoRetrieval; pub use exchange_info::ExchangeInfo; pub use market_pair_handle::MarketPairHandle; -pub use market_pair::MarketPair; +pub use market_pair::MarketPairInfo; pub use utils::*; pub use super::shared; diff --git a/examples/binance.rs b/examples/binance.rs deleted file mode 100644 index d1a550dc..00000000 --- a/examples/binance.rs +++ /dev/null @@ -1,16 +0,0 @@ -use openlimits::exchange::binance::Binance; -use openlimits::exchange::binance::BinanceParameters; -use openlimits::prelude::*; - -#[tokio::main] -async fn main() { - let binance = Binance::new(BinanceParameters::prod()) - .await - .expect("Couldn't create openlimits-binance client"); - - let order_book = binance.order_book(&OrderBookRequest {market_pair: "BTCEUR".to_string()}) - .await - .expect("Couldn't get order book"); - - println!("{:?}", order_book); -} \ No newline at end of file diff --git a/examples/binance_websocket.rs b/examples/binance_websocket.rs deleted file mode 100644 index ec88fff5..00000000 --- a/examples/binance_websocket.rs +++ /dev/null @@ -1,30 +0,0 @@ -use openlimits::exchange::traits::stream::OpenLimitsWs; -use openlimits::exchange::binance::BinanceWebsocket; -use openlimits::exchange::binance::BinanceParameters; -use openlimits::prelude::*; -use openlimits::model::websocket::OpenLimitsWebSocketMessage::OrderBook; -use openlimits::model::websocket::Subscription::OrderBookUpdates; -use openlimits::model::websocket::WebSocketResponse::Generic; - -#[tokio::main] -async fn main() { - let binance_websocket = OpenLimitsWs { - websocket: BinanceWebsocket::new(BinanceParameters::prod()) - .await - .expect("Failed to create Client") - }; - - binance_websocket.subscribe(OrderBookUpdates("btceur".to_string()), move |m| { - let r = m.as_ref(); - - if let Ok(Generic(OrderBook(order_book))) = r { - println!("{:?}", order_book) - } else if let Err(err) = r { - println!("{:#?}", err); - } - }) - .await - .expect("Failed to subscribe to orderbook on Binance"); - - std::thread::sleep(std::time::Duration::from_millis(5000)); -} \ No newline at end of file diff --git a/examples/coinbase.rs b/examples/coinbase.rs deleted file mode 100644 index cda6395e..00000000 --- a/examples/coinbase.rs +++ /dev/null @@ -1,16 +0,0 @@ -use openlimits::exchange::coinbase::Coinbase; -use openlimits::exchange::coinbase::CoinbaseParameters; -use openlimits::prelude::*; - -#[tokio::main] -async fn main() { - let coinbase = Coinbase::new(CoinbaseParameters::prod()) - .await - .expect("Couldn't create coinbase client"); - - let order_book = coinbase.order_book(&OrderBookRequest {market_pair: "BTC-EUR".to_string()}) - .await - .expect("Couldn't get order book"); - - println!("{:?}", order_book); -} \ No newline at end of file diff --git a/examples/csharp/.gitignore b/examples/csharp/.gitignore new file mode 100644 index 00000000..7de5508b --- /dev/null +++ b/examples/csharp/.gitignore @@ -0,0 +1,2 @@ +obj +bin diff --git a/examples/csharp/Example.csproj b/examples/csharp/Example.csproj new file mode 100644 index 00000000..6ed5d8c3 --- /dev/null +++ b/examples/csharp/Example.csproj @@ -0,0 +1,12 @@ + + + + + + + + Exe + netcoreapp5.0 + + + diff --git a/examples/csharp/Program.cs b/examples/csharp/Program.cs new file mode 100644 index 00000000..7b1a5445 --- /dev/null +++ b/examples/csharp/Program.cs @@ -0,0 +1,55 @@ +using System; + +namespace Example +{ + using Openlimits; + using System.Threading; + using System.Collections.Generic; + using System.Runtime.InteropServices; + class Program + { + static public void Main(string[] args) + { + var ffi_string = Client.FFIGetName(); + Console.WriteLine(ffi_string); +// var ptr = FFIString.FFIGetPointer(ffi_string); +// Console.WriteLine(ptr); + return; + CoinbaseParameters parameters = new CoinbaseParameters(Environment.Production, "a", "b", "c"); + string key = parameters.apiKey; + Client client = Client.Coinbase(parameters); +// Console.WriteLine(Decimal.Parse(askBid.qty)); +// Console.WriteLine(Decimal.Parse(askBid.qty)); +// Console.WriteLine(askBid.price); + var list = new List(); + list.Add(1); + list.Add(2); + list.Add(3); + Console.WriteLine(client.Sum(list)); + var result = client.Mul(list, 2); + foreach (var value in result) { + Console.WriteLine(value); + } + +// Test.Display(person); +// NashClientConfig config = NashClientConfig.Unauthenticated(0, NashEnvironment.Production, 1000); +// Console.WriteLine(config.environment); +// var client = new ExchangeClient(config); +// +// client.SubscribeToDisconnect(() => { +// Console.WriteLine("Disconnected"); +// }); +// foreach(var market in client.ReceivePairs()) { +// client.SubscribeToOrderbook(market.symbol, PrintBook); +// } +// +// GC.Collect(); +// GC.WaitForPendingFinalizers(); + + // Noia markets only available in NashEnvironment.Production + // Console.WriteLine("Listening to the noia markets"); + // client.SubscribeToOrderbook("noia_usdc", PrintBook); + // client.SubscribeToOrderbook("noia_btc", PrintBook); + } + } +} \ No newline at end of file diff --git a/examples/csharp/run.sh b/examples/csharp/run.sh new file mode 100755 index 00000000..6f622285 --- /dev/null +++ b/examples/csharp/run.sh @@ -0,0 +1,5 @@ +# cargo build --manifest-path ../../Cargo.toml +cp ../../target/ligen/openlimits/lib/openlimits.dll ./bin/Debug/netcoreapp5.0/openlimits_csharp.dll +# cp ../../target/ligen/openlimits/lib/libopenlimits.so ./bin/Debug/netcoreapp5.0/libopenlimits_csharp.so +dotnet run + diff --git a/examples/coinbase_websocket.rs b/examples/rust/orderbook.rs similarity index 59% rename from examples/coinbase_websocket.rs rename to examples/rust/orderbook.rs index 6fca2ccb..7884f8af 100644 --- a/examples/coinbase_websocket.rs +++ b/examples/rust/orderbook.rs @@ -1,18 +1,18 @@ -use openlimits::exchange::traits::stream::OpenLimitsWs; +use openlimits::prelude::*; use openlimits::exchange::coinbase::client::websocket::CoinbaseWebsocket; use openlimits::exchange::coinbase::CoinbaseParameters; use openlimits::model::websocket::OpenLimitsWebSocketMessage::OrderBook; use openlimits::model::websocket::Subscription::OrderBookUpdates; use openlimits::model::websocket::WebSocketResponse::Generic; - +use openlimits_exchange::model::market_pair::MarketPair; +use openlimits_exchange::model::currency::Currency; #[tokio::main] async fn main() { - let coinbase_websocket = OpenLimitsWs { - websocket: CoinbaseWebsocket::new(CoinbaseParameters::prod()) - }; + let coinbase_websocket = CoinbaseWebsocket::new(CoinbaseParameters::production()).await.unwrap(); + let market = MarketPair(Currency::ETH, Currency::BTC); - coinbase_websocket.subscribe(OrderBookUpdates("BTC-EUR".to_string()), move |m| { + coinbase_websocket.subscribe(OrderBookUpdates(market), move |m| { let r = m.as_ref(); if let Ok(Generic(OrderBook(order_book))) = r { @@ -21,8 +21,8 @@ async fn main() { println!("{:#?}", err); } }) - .await - .expect("Failed to subscribe to orderbook on Coinbase"); + .await + .expect("Failed to subscribe to orderbook on Coinbase"); std::thread::sleep(std::time::Duration::from_millis(5000)); } \ No newline at end of file diff --git a/redist/x86/gmp.dll b/redist/x86/gmp.dll deleted file mode 100644 index 74b9d5d6..00000000 Binary files a/redist/x86/gmp.dll and /dev/null differ diff --git a/redist/x86/gmp.lib b/redist/x86/gmp.lib deleted file mode 100644 index afecfaca..00000000 Binary files a/redist/x86/gmp.lib and /dev/null differ diff --git a/redist/x86_64/gmp.dll b/redist/x86_64/gmp.dll deleted file mode 100644 index c7253c97..00000000 Binary files a/redist/x86_64/gmp.dll and /dev/null differ diff --git a/redist/x86_64/gmp.lib b/redist/x86_64/gmp.lib deleted file mode 100644 index f21f469c..00000000 Binary files a/redist/x86_64/gmp.lib and /dev/null differ diff --git a/src/bindings/ask_bid.rs b/src/bindings/ask_bid.rs new file mode 100644 index 00000000..e0391973 --- /dev/null +++ b/src/bindings/ask_bid.rs @@ -0,0 +1,34 @@ +use ligen::marshalling::{MarshalFrom, MarshalInto}; +use crate::model::AskBid; +use rust_decimal::Decimal; +use std::str::FromStr; +use crate::bindings::string::FFIString; + +use ligen_macro::inner_ligen; + +inner_ligen! { + ffi(AskBid(name = "FFIAskBid")), + csharp(ffi(FFIAskBid(name = "AskBid"))) +} + +#[repr(C)] +pub struct FFIAskBid { + price: FFIString, + qty: FFIString +} + +impl MarshalFrom for AskBid { + fn marshal_from(from: FFIAskBid) -> Self { + let qty = Decimal::from_str(&String::marshal_from(from.qty)).expect("Invalid number format."); + let price = Decimal::from_str(&String::marshal_from(from.price)).expect("Invalid number format."); + Self { qty, price } + } +} + +impl MarshalFrom for FFIAskBid { + fn marshal_from(from: AskBid) -> Self { + let qty = from.qty.marshal_into(); + let price = from.price.marshal_into(); + Self { price, qty } + } +} \ No newline at end of file diff --git a/src/bindings/client.rs b/src/bindings/client.rs new file mode 100644 index 00000000..0311be36 --- /dev/null +++ b/src/bindings/client.rs @@ -0,0 +1,54 @@ +use runtime::RUNTIME; + +use crate::exchange::coinbase::{Coinbase, CoinbaseParameters}; +// use crate::prelude::*; + +pub mod coinbase; + +mod runtime { + use ligen_macro::inner_ligen; + + pub use runtime::RUNTIME; + + inner_ligen!(ignore); + mod runtime { + lazy_static::lazy_static! { + pub static ref RUNTIME: tokio::runtime::Runtime = tokio::runtime::Runtime::new().unwrap(); + } + } +} + +#[repr(C)] +pub struct Client { + client: *mut Coinbase +} + +impl Client { + pub fn coinbase(parameters: CoinbaseParameters) -> Self { + let client = crate::OpenLimits::instantiate(parameters); + let client = RUNTIME.block_on(client); + let client = client.unwrap(); + let client = Box::into_raw(Box::new(client)); + Self { client } + } + + pub fn sum(self, a: Vec) -> u64 { + a.iter().sum() + } + + pub fn mul(self, a: Vec, n: u64) -> Vec { + a.iter().map(|x| x * n).collect() + } + + pub fn get_name() -> String { + "Abc".into() + } + + // pub fn order_book(self, market_pair: String) -> OrderBookResponse { + // unsafe { + // let client = self.client.as_ref().unwrap(); + // let response = RUNTIME.block_on(client.order_book(&OrderBookRequest { market_pair })); + // response.unwrap() + // } + // } +} diff --git a/src/bindings/client/coinbase.rs b/src/bindings/client/coinbase.rs new file mode 100644 index 00000000..77dfd571 --- /dev/null +++ b/src/bindings/client/coinbase.rs @@ -0,0 +1,47 @@ +use crate::bindings::string::FFIString; +use crate::bindings::environment::Environment; +use crate::exchange::coinbase::{CoinbaseParameters, CoinbaseCredentials}; +use ligen::marshalling::MarshalFrom; +use ligen_macro::inner_ligen; + +inner_ligen! { + ffi(CoinbaseParameters(name = "FFICoinbaseParameters")), + marshal( + FFICoinbaseParameters( + name = "CoinbaseParameters" + ) + ), + csharp( + ffi( + FFICoinbaseParameters( + name = "CoinbaseParameters" + ) + ), + ) +} + +#[repr(C, packed(1))] +pub struct FFICoinbaseParameters { + environment: Environment, + apiKey: FFIString, + apiSecret: FFIString, + passphrase: FFIString +} + +impl MarshalFrom for CoinbaseParameters { + fn marshal_from(from: FFICoinbaseParameters) -> Self { + let sandbox = match from.environment { + Environment::Sandbox => true, + _ => false + }; + let api_key = String::marshal_from(from.apiKey); + let api_secret = String::marshal_from(from.apiSecret); + let passphrase = String::marshal_from(from.passphrase); + let credentials = if !api_key.is_empty() && !api_secret.is_empty() && !passphrase.is_empty() { + Some(CoinbaseCredentials { api_key, api_secret, passphrase }) + } else { + None + }; + Self { sandbox, credentials } + } +} \ No newline at end of file diff --git a/src/bindings/environment.rs b/src/bindings/environment.rs new file mode 100644 index 00000000..452cb3f2 --- /dev/null +++ b/src/bindings/environment.rs @@ -0,0 +1,5 @@ +#[repr(u32)] +pub enum Environment { + Sandbox, + Production +} diff --git a/src/bindings/mod.rs b/src/bindings/mod.rs new file mode 100644 index 00000000..a57ddb39 --- /dev/null +++ b/src/bindings/mod.rs @@ -0,0 +1,45 @@ +#![allow(non_snake_case)] + +pub mod string; +pub mod environment; +pub mod client; +pub mod ask_bid; +pub mod orderbook; +pub mod vector; +// use ligen::marshalling::{MarshalFrom, MarshalInto}; + +// #[repr(C)] +// pub struct Test { +// pub value: i32, +// pub environment: Environment, +// } +// +// // pub type Callback = Box; +// +// pub struct Person { +// first_name: String, +// last_name: String +// } +// +// +// impl Test { +// pub fn hello() { +// println!("Hello from Test"); +// } +// +// pub fn create(value: i32) -> Self { +// Self { value, environment: Environment::Production } +// } +// +// pub fn print(value: String) { +// println!("{}", value); +// } +// +// pub fn display(person: Person) { +// println!("{} {}", person.first_name, person.last_name); +// } +// +// // pub fn set_callback(callback: Callback) { +// // callback(Environment::Production); +// // } +// } diff --git a/src/bindings/option.rs b/src/bindings/option.rs new file mode 100644 index 00000000..7f995d7d --- /dev/null +++ b/src/bindings/option.rs @@ -0,0 +1,4 @@ +// pub enum FFIOption { +// Some(T), +// None +// } diff --git a/src/bindings/orderbook.rs b/src/bindings/orderbook.rs new file mode 100644 index 00000000..ff3e2486 --- /dev/null +++ b/src/bindings/orderbook.rs @@ -0,0 +1,32 @@ +use crate::model::OrderBookResponse; +use crate::bindings::vector::FFIVector; +use ligen_macro::inner_ligen; +use std::ptr::null_mut; +use ligen::marshalling::MarshalFrom; +use crate::bindings::ask_bid::FFIAskBid; + +// # Replicate generics logic to struct fields. +// # We should have a FFI and a normal structure. + +inner_ligen! { + ffi(OrderBookResponse(name = "FFIOrderBookResponse")), + csharp() +} + +#[repr(C)] +pub struct FFIOrderBookResponse { + update_id: *mut u64, + last_update_id: *mut u64, + bids: FFIVector, + asks: FFIVector +} + +impl MarshalFrom for FFIOrderBookResponse { + fn marshal_from(_value: OrderBookResponse) -> Self { + let update_id = null_mut(); + let last_update_id = null_mut(); + let bids = Default::default(); + let asks = Default::default(); + Self { update_id, last_update_id, bids, asks } + } +} diff --git a/src/bindings/string/mod.rs b/src/bindings/string/mod.rs new file mode 100644 index 00000000..5673d34e --- /dev/null +++ b/src/bindings/string/mod.rs @@ -0,0 +1,89 @@ +use ligen::marshalling::{MarshalFrom, MarshalInto}; +use ligen_macro::inner_ligen; +use rust_decimal::Decimal; +use std::ffi::{CString, CStr}; + +inner_ligen! { + ffi( + String( + opaque = true, + name = "FFIString" + ), + FFIString(opaque = true) + ), + + csharp( + ffi( + String( + name = "FFIString" + ), + ), + marshal( + FFIString( + methods = "src/bindings/string/string.methods.cs", + ), + String( + name = "string", + methods = "src/bindings/string/string.methods.cs", + ) + ), + ) + + // csharp( + // marshal( + // FFIString( + // name = "string", + // MarshalAs = "UnmanagedType.LPStr" + // ), + // String( + // name = "string", + // MarshalAs = "UnmanagedType.LPStr" + // ) + // ), + // ) +} + +pub struct FFIString { + string: CString +} + +impl Drop for FFIString { + fn drop(&mut self) { + println!("Why is {} being dropped?", self.get_pointer() as u64); + } +} + +impl FFIString { + pub fn new(pointer: *mut i8) -> Self { + let string = unsafe { + CStr::from_ptr(pointer).to_owned() + }; + Self { string } + } + + pub fn get_pointer(&self) -> *const i8 { + let ptr = self.string.as_ptr(); + println!("ptr: {} of {}", ptr as u64, self.string.to_string_lossy()); + ptr + } +} + +impl MarshalFrom for String { + fn marshal_from(value: FFIString) -> Self { + value.string.to_string_lossy().to_string() + } +} + +impl MarshalFrom for FFIString { + fn marshal_from(value: String) -> Self { + let error = format!("Failed to create CString from String({}).", value); + let string = CString::new(value).expect(&error); + Self { string } + } +} + +impl MarshalFrom for FFIString { + fn marshal_from(value: Decimal) -> Self { + value.to_string().marshal_into() + } +} diff --git a/src/bindings/string/string.methods.cs b/src/bindings/string/string.methods.cs new file mode 100644 index 00000000..925d1229 --- /dev/null +++ b/src/bindings/string/string.methods.cs @@ -0,0 +1,12 @@ + public static implicit operator string(FFIString from) { + unsafe { + return Marshal.PtrToStringUTF8(from.GetPointer()); + } + } + + public static implicit operator FFIString(string from) { + unsafe { + return new FFIString(Marshal.StringToHGlobalAnsi(from)); + // FIXME: Memory leak https://docs.microsoft.com/pt-br/dotnet/api/system.runtime.interopservices.marshal.freehglobal?view=net-5.0 + } + } \ No newline at end of file diff --git a/src/bindings/vector/mod.rs b/src/bindings/vector/mod.rs new file mode 100644 index 00000000..5450cdc7 --- /dev/null +++ b/src/bindings/vector/mod.rs @@ -0,0 +1,54 @@ +use ligen_macro::inner_ligen; +use ligen::marshalling::MarshalFrom; +use std::ptr::null_mut; + +inner_ligen! { + ffi(Vec(name = "FFIVector")), + csharp( + ffi( + Vec(name = "FFIVector"), + FFIVector(name = "FFIVector") + ), + marshal( + Vec(name = "List"), + FFIVector( + name = "List", + methods = "src/bindings/vector/vector.methods.cs", + generics = " where T: unmanaged" + ) + ), + ) +} + +#[repr(C)] +pub struct FFIVector { + pub pointer: *mut T, + pub length: u64 +} + +impl Default for FFIVector { + fn default() -> Self { + let pointer = null_mut(); + let length = 0; + Self { pointer, length } + } +} + +impl MarshalFrom> for Vec { + fn marshal_from(from: FFIVector) -> Self { + let length = from.length as usize; + unsafe { + std::slice::from_raw_parts(from.pointer, length).to_vec() + } + } +} + +impl MarshalFrom> for FFIVector { + fn marshal_from(mut from: Vec) -> Self { + let pointer = from.as_mut_ptr(); + let length = from.len() as u64; + // FIXME: Memory leak. + std::mem::forget(from); + Self { pointer, length } + } +} diff --git a/src/bindings/vector/vector.methods.cs b/src/bindings/vector/vector.methods.cs new file mode 100644 index 00000000..93c824d5 --- /dev/null +++ b/src/bindings/vector/vector.methods.cs @@ -0,0 +1,24 @@ + public static implicit operator FFIVector(List from) { + var array = from.ToArray(); + ulong length = (ulong) from.Count; + GCHandle handle = GCHandle.Alloc(array, GCHandleType.Pinned); + try { + IntPtr pointer = handle.AddrOfPinnedObject(); + return new FFIVector(pointer, length); + } finally { + // FIXME: Memory leak? It seems to be garbage collected. + // if (handle.IsAllocated) + // handle.Free(); + } + } + + public static implicit operator List(FFIVector from) { + unsafe { + T[] array = new T[from.length]; + fixed (T* apointer = array) { + long length = (long) from.length * (long) sizeof(T); + Buffer.MemoryCopy((void*) from.pointer, (void*) apointer, length, length); + return new List(array); + } + } + } \ No newline at end of file diff --git a/src/exchange/coinbase/client/account.rs b/src/exchange/coinbase/client/account.rs index 02619276..88a14154 100644 --- a/src/exchange/coinbase/client/account.rs +++ b/src/exchange/coinbase/client/account.rs @@ -6,9 +6,10 @@ use crate::exchange::{ Paginator, }, }; -use crate::exchange::traits::info::MarketPair; +use crate::exchange::traits::info::MarketPairInfo; use super::BaseClient; use super::shared::Result; +use crate::exchange::coinbase::model::MarketPair; impl BaseClient { pub async fn get_account(&self, paginator: Option<&Paginator>) -> Result> { @@ -26,7 +27,7 @@ impl BaseClient { } // TODO: refactor buy and sell in order creation in commun function - pub async fn market_buy(&self, pair: MarketPair, size: Decimal) -> Result { + pub async fn market_buy(&self, pair: MarketPairInfo, size: Decimal) -> Result { let data = OrderRequest { product_id: pair.symbol, client_oid: None, @@ -47,7 +48,7 @@ impl BaseClient { Ok(transaction) } - pub async fn market_sell(&self, pair: MarketPair, size: Decimal) -> Result { + pub async fn market_sell(&self, pair: MarketPairInfo, size: Decimal) -> Result { let data = OrderRequest { product_id: pair.symbol, client_oid: None, @@ -70,7 +71,7 @@ impl BaseClient { pub async fn limit_buy( &self, - pair: MarketPair, + pair: MarketPairInfo, size: Decimal, price: Decimal, time_in_force: OrderTimeInForce, @@ -102,7 +103,7 @@ impl BaseClient { pub async fn limit_sell( &self, - pair: MarketPair, + pair: MarketPairInfo, size: Decimal, price: Decimal, time_in_force: OrderTimeInForce, @@ -150,10 +151,10 @@ impl BaseClient { Ok(resp) } - pub async fn cancel_all_orders(&self, product_id: Option<&str>) -> Result> { + pub async fn cancel_all_orders>(&self, product_id: Option

) -> Result> { let params = if let Some(product_id) = product_id { CancelAllOrders { - product_id: Some(String::from(product_id)), + product_id: Some(product_id.into().0), } } else { CancelAllOrders { product_id: None } diff --git a/src/exchange/coinbase/client/base_client.rs b/src/exchange/coinbase/client/base_client.rs index 4fab813b..71138bd7 100644 --- a/src/exchange/coinbase/client/base_client.rs +++ b/src/exchange/coinbase/client/base_client.rs @@ -1,7 +1,7 @@ use super::Transport; /// The coinbase client -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct BaseClient { pub transport: Transport, } \ No newline at end of file diff --git a/src/exchange/coinbase/client/market.rs b/src/exchange/coinbase/client/market.rs index 78831b48..937966a3 100644 --- a/src/exchange/coinbase/client/market.rs +++ b/src/exchange/coinbase/client/market.rs @@ -7,6 +7,7 @@ use crate::{ }; use super::shared::Result; use super::BaseClient; +use crate::exchange::coinbase::model::MarketPair; impl BaseClient { pub async fn products(&self) -> Result> { @@ -18,31 +19,32 @@ impl BaseClient { self.transport.get::<_, ()>(&endpoint, None).await } - pub async fn book(&self, pair: &str) -> Result> + pub async fn book(&self, pair: P) -> Result> where T: BookLevel + Debug + 'static, T: for<'de> Deserialize<'de>, + P: Into { - let endpoint = format!("/products/{}/book?level={}", pair, T::level()); + let endpoint = format!("/products/{}/book?level={}", pair.into().0, T::level()); self.transport.get::<_, ()>(&endpoint, None).await } - pub async fn trades(&self, pair: &str, paginator: Option<&Paginator>) -> Result> { - let endpoint = format!("/products/{}/trades", pair); + pub async fn trades>(&self, pair: P, paginator: Option<&Paginator>) -> Result> { + let endpoint = format!("/products/{}/trades", pair.into().0); self.transport.get(&endpoint, paginator).await } - pub async fn ticker(&self, pair: &str) -> Result { - let endpoint = format!("/products/{}/ticker", pair); + pub async fn ticker>(&self, pair: P) -> Result { + let endpoint = format!("/products/{}/ticker", pair.into().0); self.transport.get::<_, ()>(&endpoint, None).await } - pub async fn candles( + pub async fn candles>( &self, - pair: &str, + pair: P, params: Option<&CandleRequestParams>, ) -> Result> { - let endpoint = format!("/products/{}/candles", pair); + let endpoint = format!("/products/{}/candles", pair.into().0); self.transport.get(&endpoint, params).await } } diff --git a/src/exchange/coinbase/client/websocket.rs b/src/exchange/coinbase/client/websocket.rs index 7936ec44..0cdadb77 100644 --- a/src/exchange/coinbase/client/websocket.rs +++ b/src/exchange/coinbase/client/websocket.rs @@ -21,9 +21,10 @@ use futures::stream::BoxStream; use std::sync::Mutex; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use super::shared::Result; +use openlimits_exchange::exchange::Environment; -const WS_URL_PROD: &str = "wss://ws-feed.pro.coinbase.com"; -const WS_URL_SANDBOX: &str = "wss://ws-feed-public.sandbox.pro.coinbase.com"; +const WS_URL_PROD: &str = "wss://ws-feed.exchange.coinbase.com"; +const WS_URL_SANDBOX: &str = "wss://ws-feed-public.sandbox.exchange.coinbase.com"; #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] @@ -42,14 +43,6 @@ pub struct CoinbaseWebsocket { } impl CoinbaseWebsocket { - pub fn new(parameters: CoinbaseParameters) -> Self { - Self { - subscriptions: Default::default(), - parameters, - disconnection_senders: Default::default(), - } - } - pub async fn subscribe_(&mut self, subscription: CoinbaseSubscription) -> Result<()> { let (channels, product_ids) = match &subscription { CoinbaseSubscription::Level2(product_id) => ( @@ -60,7 +53,10 @@ impl CoinbaseWebsocket { vec![Channel::Name(ChannelType::Heartbeat)], vec![product_id.clone()], ), - _ => unimplemented!(), + CoinbaseSubscription::Matches(product_id) => ( + vec![Channel::Name(ChannelType::Matches)], + vec![product_id.clone()] + ) }; let subscribe = Subscribe { _type: SubscribeCmd::Subscribe, @@ -75,7 +71,7 @@ impl CoinbaseWebsocket { } pub async fn connect(&self, subscribe: Subscribe) -> Result> { - let ws_url = if self.parameters.sandbox { + let ws_url = if self.parameters.environment == Environment::Sandbox { WS_URL_SANDBOX } else { WS_URL_PROD @@ -123,7 +119,11 @@ impl ExchangeWs for CoinbaseWebsocket { type Response = CoinbaseWebsocketMessage; async fn new(parameters: Self::InitParams) -> Result { - Ok(CoinbaseWebsocket::new(parameters)) + Ok(Self { + subscriptions: Default::default(), + parameters, + disconnection_senders: Default::default(), + }) } async fn disconnect(&self) { @@ -139,7 +139,7 @@ impl ExchangeWs for CoinbaseWebsocket { &self, subscription: Subscriptions, ) -> Result>> { - let ws_url = if self.parameters.sandbox { + let ws_url = if self.parameters.environment == Environment::Sandbox { WS_URL_SANDBOX } else { WS_URL_PROD @@ -156,7 +156,10 @@ impl ExchangeWs for CoinbaseWebsocket { vec![Channel::Name(ChannelType::Heartbeat)], vec![product_id.clone()], ), - _ => unimplemented!(), + CoinbaseSubscription::Matches(product_id) => ( + vec![Channel::Name(ChannelType::Matches)], + vec![product_id.clone()] + ) }; let subscribe = Subscribe { _type: SubscribeCmd::Subscribe, diff --git a/src/exchange/coinbase/coinbase_credentials.rs b/src/exchange/coinbase/coinbase_credentials.rs index 824b7825..af125906 100644 --- a/src/exchange/coinbase/coinbase_credentials.rs +++ b/src/exchange/coinbase/coinbase_credentials.rs @@ -1,5 +1,5 @@ /// This struct represents the coinbase credentials -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct CoinbaseCredentials { pub api_key: String, pub api_secret: String, diff --git a/src/exchange/coinbase/coinbase_parameters.rs b/src/exchange/coinbase/coinbase_parameters.rs index a85557b0..bd33ffb2 100644 --- a/src/exchange/coinbase/coinbase_parameters.rs +++ b/src/exchange/coinbase/coinbase_parameters.rs @@ -1,23 +1,24 @@ use super::CoinbaseCredentials; +use openlimits_exchange::exchange::Environment; /// This struct represents the coinbase parameters -#[derive(Default, Clone)] +#[derive(Default, Clone, Debug)] pub struct CoinbaseParameters { - pub sandbox: bool, + pub environment: Environment, pub credentials: Option, } impl CoinbaseParameters { pub fn sandbox() -> Self { Self { - sandbox: true, + environment: Environment::Sandbox, ..Default::default() } } - pub fn prod() -> Self { + pub fn production() -> Self { Self { - sandbox: false, + environment: Environment::Production, ..Default::default() } } diff --git a/src/exchange/coinbase/mod.rs b/src/exchange/coinbase/mod.rs index 1ae4c1c4..df889739 100644 --- a/src/exchange/coinbase/mod.rs +++ b/src/exchange/coinbase/mod.rs @@ -1,17 +1,19 @@ //! This module provides functionality for communicating with the coinbase API. //! # Example -//! ``` +//! ```no_run //! use openlimits::exchange::coinbase::Coinbase; //! use openlimits::exchange::coinbase::CoinbaseParameters; //! use openlimits::prelude::*; +//! use openlimits::model::market_pair::*; //! //! #[tokio::main] //! async fn main() { -//! let coinbase = Coinbase::new(CoinbaseParameters::prod()) +//! let coinbase = Coinbase::new(CoinbaseParameters::production()) //! .await //! .expect("Couldn't create coinbase client"); - -//! let order_book = coinbase.order_book(&OrderBookRequest {market_pair: "BTC-EUR".to_string()}) +//! +//! let market_pair = MarketPair(Currency::BTC, Currency::ETH); +//! let order_book = coinbase.order_book(&OrderBookRequest { market_pair }) //! .await //! .expect("Couldn't get order book"); @@ -19,6 +21,11 @@ //! } //! ``` +#[cfg(feature = "bindings")] +use ligen_macro::inner_ligen; + +#[cfg(feature = "bindings")] +inner_ligen!(ignore); use std::convert::TryFrom; use async_trait::async_trait; @@ -35,7 +42,7 @@ use crate::{ Paginator, Side, Ticker, TimeInForce, Trade, TradeHistoryRequest, }, }; -use crate::exchange::traits::info::{ExchangeInfoRetrieval, MarketPair, MarketPairHandle}; +use crate::exchange::traits::info::{ExchangeInfoRetrieval, MarketPairInfo, MarketPairHandle}; use crate::exchange::traits::Exchange; use crate::prelude::*; use super::shared::Result; @@ -52,6 +59,9 @@ pub use coinbase_content_error::CoinbaseContentError; pub use coinbase_credentials::CoinbaseCredentials; pub use coinbase_parameters::CoinbaseParameters; pub use super::shared; +use openlimits_exchange::exchange::Environment; +pub use crate::exchange::coinbase::client::websocket::CoinbaseWebsocket; +use openlimits_exchange::model::market_pair::MarketPair; #[derive(Clone)] pub struct Coinbase { @@ -73,14 +83,14 @@ impl Exchange for Coinbase { &credentials.api_key, &credentials.api_secret, &credentials.passphrase, - parameters.sandbox, + parameters.environment == Environment::Sandbox, )?, }, }, None => Coinbase { exchange_info: ExchangeInfo::new(), client: BaseClient { - transport: Transport::new(parameters.sandbox)?, + transport: Transport::new(parameters.environment == Environment::Sandbox)?, }, }, }; @@ -96,10 +106,10 @@ impl Exchange for Coinbase { #[async_trait] impl ExchangeInfoRetrieval for Coinbase { - async fn retrieve_pairs(&self) -> Result> { + async fn retrieve_pairs(&self) -> Result> { self.client.products().await.map(|v| { v.into_iter() - .map(|product| MarketPair { + .map(|product| MarketPairInfo { symbol: product.id, base: product.base_currency, quote: product.quote_currency, @@ -118,8 +128,9 @@ impl ExchangeInfoRetrieval for Coinbase { .await } - async fn get_pair(&self, name: &str) -> Result { - self.exchange_info.get_pair(name) + async fn get_pair(&self, name: &MarketPair) -> Result { + let name = crate::exchange::coinbase::model::MarketPair::from(name.clone()).0; + self.exchange_info.get_pair(&name) } } @@ -127,19 +138,19 @@ impl ExchangeInfoRetrieval for Coinbase { impl ExchangeMarketData for Coinbase { async fn order_book(&self, req: &OrderBookRequest) -> Result { self.client - .book::(&req.market_pair) + .book::(req.market_pair.clone()) .await .map(Into::into) } async fn get_price_ticker(&self, req: &GetPriceTickerRequest) -> Result { - self.client.ticker(&req.market_pair).await.map(Into::into) + self.client.ticker(req.market_pair.clone()).await.map(Into::into) } async fn get_historic_rates(&self, req: &GetHistoricRatesRequest) -> Result> { let params = model::CandleRequestParams::try_from(req)?; self.client - .candles(&req.market_pair, Some(¶ms)) + .candles(req.market_pair.clone(), Some(¶ms)) .await .map(|v| v.into_iter().map(Into::into).collect()) } @@ -199,7 +210,7 @@ impl From for Order { #[async_trait] impl ExchangeAccount for Coinbase { async fn limit_buy(&self, req: &OpenLimitOrderRequest) -> Result { - let pair = self.exchange_info.get_pair(&req.market_pair)?.read()?; + let pair = self.get_pair(&req.market_pair).await?.read()?; self.client .limit_buy( pair, @@ -213,7 +224,7 @@ impl ExchangeAccount for Coinbase { } async fn limit_sell(&self, req: &OpenLimitOrderRequest) -> Result { - let pair = self.exchange_info.get_pair(&req.market_pair)?.read()?; + let pair = self.get_pair(&req.market_pair).await?.read()?; self.client .limit_sell( pair, @@ -227,12 +238,12 @@ impl ExchangeAccount for Coinbase { } async fn market_buy(&self, req: &OpenMarketOrderRequest) -> Result { - let pair = self.exchange_info.get_pair(&req.market_pair)?.read()?; + let pair = self.get_pair(&req.market_pair).await?.read()?; self.client.market_buy(pair, req.size).await.map(Into::into) } async fn market_sell(&self, req: &OpenMarketOrderRequest) -> Result { - let pair = self.exchange_info.get_pair(&req.market_pair)?.read()?; + let pair = self.get_pair(&req.market_pair).await?.read()?; self.client .market_sell(pair, req.size) .await @@ -248,7 +259,7 @@ impl ExchangeAccount for Coinbase { async fn cancel_all_orders(&self, req: &CancelAllOrdersRequest) -> Result> { self.client - .cancel_all_orders(req.market_pair.as_deref()) + .cancel_all_orders(req.market_pair.clone()) .await .map(|v| v.into_iter().map(Into::into).collect()) } @@ -334,7 +345,7 @@ impl From for Trade { "T" => Some(Liquidity::Taker), _ => None, }, - created_at: (fill.created_at.timestamp_millis()) as u64, + created_at: fill.created_at.to_string(), } } } @@ -375,7 +386,7 @@ impl TryFrom<&GetHistoricRatesRequest> for model::CandleRequestParams { impl From<&GetOrderHistoryRequest> for model::GetOrderRequest { fn from(req: &GetOrderHistoryRequest) -> Self { Self { - product_id: req.market_pair.clone(), + product_id: req.market_pair.clone().map(|market| crate::exchange::coinbase::model::MarketPair::from(market).0), paginator: req.paginator.clone().map(|p| p.into()), status: None, } @@ -466,7 +477,7 @@ impl From<&TradeHistoryRequest> for model::GetFillsReq { Self { order_id: req.order_id.clone(), paginator: req.paginator.clone().map(|p| p.into()), - product_id: req.market_pair.clone(), + product_id: req.market_pair.clone().map(|market| crate::exchange::coinbase::model::MarketPair::from(market).0), } } } diff --git a/src/exchange/coinbase/model/market_pair.rs b/src/exchange/coinbase/model/market_pair.rs new file mode 100644 index 00000000..0fbc4da7 --- /dev/null +++ b/src/exchange/coinbase/model/market_pair.rs @@ -0,0 +1,11 @@ +use openlimits_exchange::model::market_pair::MarketPair as OMarketPair; +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MarketPair(pub String); + +impl From for MarketPair { + fn from(from: OMarketPair) -> MarketPair { + MarketPair(format!("{}-{}", from.0, from.1).to_uppercase()) + } +} \ No newline at end of file diff --git a/src/exchange/coinbase/model/mod.rs b/src/exchange/coinbase/model/mod.rs index e146c0c3..77641b6b 100644 --- a/src/exchange/coinbase/model/mod.rs +++ b/src/exchange/coinbase/model/mod.rs @@ -1,4 +1,6 @@ //! This module provides models that are used in the coinbase module + +mod market_pair; mod account; mod book_level; mod book_record_l1; @@ -31,6 +33,7 @@ mod ticker; mod trade; pub mod websocket; +pub use market_pair::MarketPair; pub use account::Account; pub use book_level::BookLevel; pub use book_record_l1::BookRecordL1; diff --git a/src/exchange/coinbase/model/websocket/coinbase_subscription.rs b/src/exchange/coinbase/model/websocket/coinbase_subscription.rs index c6b44e1f..91fb19e3 100644 --- a/src/exchange/coinbase/model/websocket/coinbase_subscription.rs +++ b/src/exchange/coinbase/model/websocket/coinbase_subscription.rs @@ -4,19 +4,20 @@ use crate::model::websocket::Subscription; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CoinbaseSubscription { Heartbeat(String), - Status, + // Status, // Ticker(String), Level2(String), // User, - // Matches, + Matches(String), // FullChannel } impl From for CoinbaseSubscription { fn from(subscription: Subscription) -> Self { match subscription { - Subscription::OrderBookUpdates(symbol) => CoinbaseSubscription::Level2(symbol), - _ => unimplemented!(), + Subscription::OrderBookUpdates(symbol) => CoinbaseSubscription::Level2(crate::exchange::coinbase::model::MarketPair::from(symbol).0), + Subscription::Trades(symbol) => CoinbaseSubscription::Matches(crate::exchange::coinbase::model::MarketPair::from(symbol).0), + // Subscription::Ticker(ticket) => } } } \ No newline at end of file diff --git a/src/exchange/coinbase/model/websocket/mod.rs b/src/exchange/coinbase/model/websocket/mod.rs index 08f1e71c..079e11aa 100644 --- a/src/exchange/coinbase/model/websocket/mod.rs +++ b/src/exchange/coinbase/model/websocket/mod.rs @@ -52,6 +52,7 @@ pub use subscribe_cmd::SubscribeCmd; pub use subscribe::Subscribe; pub use ticker::Ticker; pub use super::shared; +use openlimits_exchange::model::Trade; impl TryFrom for WebSocketResponse { type Error = OpenLimitsError; @@ -60,12 +61,33 @@ impl TryFrom for WebSocketResponse { Ok(WebSocketResponse::Generic(level2.try_into()?)) - } - _ => Ok(WebSocketResponse::Raw(value)), + }, + CoinbaseWebsocketMessage::Match(match_) => { + Ok(WebSocketResponse::Generic(match_.into())) + }, + _ => Ok(WebSocketResponse::Raw(value)) } } } +impl From for OpenLimitsWebSocketMessage { + fn from(match_: Match) -> Self { + let market_pair = match_.product_id; + let price = match_.price; + let qty = match_.size; + let id = format!("{}", match_.trade_id); + let buyer_order_id = Some(match_.taker_order_id); + let seller_order_id = Some(match_.maker_order_id); + let created_at = match_.time; + let fees = None; + let liquidity = None; + let side = match_.side.into(); + let trade = Trade { market_pair, price, qty, id, buyer_order_id, created_at, fees, liquidity, seller_order_id, side }; + let trades = vec![trade]; + Self::Trades(trades) + } +} + impl TryFrom for OpenLimitsWebSocketMessage { type Error = OpenLimitsError; diff --git a/src/exchange/coinbase/transport.rs b/src/exchange/coinbase/transport.rs index efd6fb14..8b3f8b6e 100644 --- a/src/exchange/coinbase/transport.rs +++ b/src/exchange/coinbase/transport.rs @@ -65,9 +65,9 @@ impl Transport { fn get_base_url(sandbox: bool) -> String { if sandbox { - String::from("https://api-public.sandbox.pro.coinbase.com") + String::from("https://api-public.sandbox.exchange.coinbase.com") } else { - String::from("https://api.pro.coinbase.com") + String::from("https://api.exchange.coinbase.com") } } @@ -235,7 +235,6 @@ impl Transport { }; let sign_message = format!("{}{}{}", prefix, path, body); - println!("sign message: {}", sign_message); mac.update(sign_message.as_bytes()); let signature = base64::encode(mac.finalize().into_bytes()); @@ -250,7 +249,6 @@ impl Transport { StatusCode::OK => { let text = response.text().await?; serde_json::from_str::(&text).map_err(move |err| { - println!("{}", &text); OpenLimitsError::NotParsableResponse(format!("Error:{} Payload: {}", err, text)) }) } diff --git a/src/exchange/mod.rs b/src/exchange/mod.rs index ad054e1f..1e9eda25 100644 --- a/src/exchange/mod.rs +++ b/src/exchange/mod.rs @@ -1,9 +1,9 @@ //! This module contains all the implemented exchanges. -pub use binance; +pub use openlimits_binance as binance; pub mod coinbase; pub mod nash; -pub use exchange::traits; -pub use exchange::shared; -pub use exchange::model; -pub use exchange::errors; \ No newline at end of file +pub use openlimits_exchange::traits; +pub use openlimits_exchange::shared; +pub use openlimits_exchange::model; +pub use openlimits_exchange::errors; diff --git a/src/exchange/nash/mod.rs b/src/exchange/nash/mod.rs index cb780146..8f08f613 100644 --- a/src/exchange/nash/mod.rs +++ b/src/exchange/nash/mod.rs @@ -4,20 +4,17 @@ mod nash_credentials; mod nash_parameters; mod nash_websocket; -mod subscription_response_wrapper; mod utils; pub use nash_credentials::NashCredentials; pub use nash_parameters::NashParameters; pub use nash_websocket::NashWebsocket; -pub use subscription_response_wrapper::SubscriptionResponseWrapper; pub use utils::client_from_params_failable; pub use super::shared; -use std::convert::{TryFrom, TryInto}; +use std::convert::TryInto; use async_trait::async_trait; use nash_native_client::Client; -use nash_protocol::protocol::subscriptions::SubscriptionResponse; use rust_decimal::prelude::*; use crate::{ errors::OpenLimitsError, @@ -26,18 +23,19 @@ use crate::{ GetHistoricTradesRequest, GetOrderHistoryRequest, GetOrderRequest, GetPriceTickerRequest, OpenLimitOrderRequest, OpenMarketOrderRequest, Order, OrderBookRequest, OrderBookResponse, OrderCanceled, Paginator, - Ticker, Trade, TradeHistoryRequest, websocket::WebSocketResponse, + Ticker, Trade, TradeHistoryRequest, }, - model::websocket::OpenLimitsWebSocketMessage, }; -use crate::exchange::traits::info::ExchangeInfo; -use crate::exchange::traits::info::ExchangeInfoRetrieval; -use crate::exchange::traits::Exchange; -use crate::exchange::traits::ExchangeMarketData; -use crate::exchange::traits::ExchangeAccount; -use crate::exchange::traits::info::MarketPair; -use crate::exchange::traits::info::MarketPairHandle; use super::shared::Result; +use openlimits_exchange::traits::info::ExchangeInfo; +use openlimits_exchange::traits::info::ExchangeInfoRetrieval; +use openlimits_exchange::traits::Exchange; +use openlimits_exchange::traits::ExchangeMarketData; +use openlimits_exchange::traits::ExchangeAccount; +use openlimits_exchange::traits::info::MarketPairInfo; +use openlimits_exchange::traits::info::MarketPairHandle; +use crate::model::market_pair::MarketPair; +use openlimits_exchange::MissingImplementationContent; /// This struct is the main struct of this module and it is used for communications with the nash openlimits-exchange pub struct Nash { @@ -51,10 +49,12 @@ impl Exchange for Nash { type InnerClient = Client; async fn new(params: Self::InitParams) -> Result { - Ok(Self { + let nash = Self { exchange_info: ExchangeInfo::new(), transport: client_from_params_failable(params).await?, - }) + }; + nash.refresh_market_info().await.ok(); + Ok(nash) } fn inner_client(&self) -> Option<&Self::InnerClient> { @@ -245,7 +245,7 @@ impl ExchangeAccount for Nash { async fn market_sell(&self, req: &OpenMarketOrderRequest) -> Result { let req: nash_protocol::protocol::place_order::MarketOrderRequest = Nash::convert_market_request(req); - + println!("{:#?}", req); let resp = self.transport.run_http(req).await; Ok( Nash::unwrap_response::( @@ -255,8 +255,9 @@ impl ExchangeAccount for Nash { ) } - async fn market_buy(&self, _: &OpenMarketOrderRequest) -> Result { - unimplemented!("Market buys are not supported by nash. A market buy can be simulated by placing a market sell in the inverse market. Market buy in btc_usdc should be translated to a market sell in usdc_btc.") + async fn market_buy(&self, _req: &OpenMarketOrderRequest) -> Result { + let message = "Nash client doesn't implement market_buy".into(); + Err(OpenLimitsError::MissingImplementation(MissingImplementationContent { message })) } async fn get_order(&self, req: &GetOrderRequest) -> Result { @@ -288,13 +289,13 @@ impl Nash { req: &OpenLimitOrderRequest, buy_or_sell: nash_protocol::types::BuyOrSell, ) -> nash_protocol::protocol::place_order::LimitOrderRequest { + let market = req.market_pair.clone(); + let market = nash_protocol::types::market_pair::MarketPair::from(market).0; nash_protocol::protocol::place_order::LimitOrderRequest { client_order_id: req.client_order_id.clone(), - cancellation_policy: nash_protocol::types::OrderCancellationPolicy::from( - req.time_in_force, - ), + cancellation_policy: req.time_in_force.into(), allow_taker: !req.post_only, - market: req.market_pair.clone(), + market, buy_or_sell, amount: format!("{}", req.size), price: format!("{}", req.price), @@ -304,9 +305,11 @@ impl Nash { pub fn convert_market_request( req: &OpenMarketOrderRequest, ) -> nash_protocol::protocol::place_order::MarketOrderRequest { + let market = req.market_pair.clone(); + let market = nash_protocol::types::market_pair::MarketPair::from(market).0; nash_protocol::protocol::place_order::MarketOrderRequest { client_order_id: req.client_order_id.clone(), - market: req.market_pair.clone(), + market, amount: format!("{}", req.size), } } @@ -330,13 +333,13 @@ impl Nash { #[async_trait] impl ExchangeInfoRetrieval for Nash { - async fn retrieve_pairs(&self) -> Result> { + async fn retrieve_pairs(&self) -> Result> { Ok(self .list_markets() .await? .markets .iter() - .map(|(symbol, v)| MarketPair { + .map(|(symbol, v)| MarketPairInfo { symbol: symbol.to_string(), base: v.asset_a.asset.name().to_string(), quote: v.asset_b.asset.name().to_string(), @@ -360,492 +363,8 @@ impl ExchangeInfoRetrieval for Nash { .await } - async fn get_pair(&self, name: &str) -> Result { - self.exchange_info.get_pair(name) - } -} - -impl From<&OrderBookRequest> for nash_protocol::protocol::orderbook::OrderbookRequest { - fn from(req: &OrderBookRequest) -> Self { - let market = req.market_pair.clone(); - Self { market } - } -} - -impl From for OrderBookResponse { - fn from(book: nash_protocol::protocol::orderbook::OrderbookResponse) -> Self { - Self { - update_id: Some(book.update_id as u64), - last_update_id: Some(book.last_update_id as u64), - bids: book.bids.into_iter().map(Into::into).collect(), - asks: book.asks.into_iter().map(Into::into).collect(), - } - } -} - -impl From for AskBid { - fn from(resp: nash_protocol::types::OrderbookOrder) -> Self { - let price = Decimal::from_str(&resp.price).expect("Couldn't parse Decimal from string."); - let qty = Decimal::from_str(&resp.amount.to_string()) - .expect("Couldn't parse Decimal from string."); - Self { price, qty } - } -} - -impl From<&CancelOrderRequest> for nash_protocol::protocol::cancel_order::CancelOrderRequest { - fn from(req: &CancelOrderRequest) -> Self { - // TODO: why this param? - let market = req.market_pair.clone().expect("Couldn't get market_pair."); - - Self { - market, - order_id: req.id.clone(), - } - } -} - -impl From for OrderCanceled { - fn from(resp: nash_protocol::protocol::cancel_order::CancelOrderResponse) -> Self { - Self { id: resp.order_id } - } -} - -impl From<&CancelAllOrdersRequest> for nash_protocol::protocol::cancel_all_orders::CancelAllOrders { - fn from(req: &CancelAllOrdersRequest) -> Self { - // TODO: why is this required param for Nash? - let market = req - .market_pair - .clone() - .expect("Market pair is a required param for Nash"); - Self { market } - } -} - -impl From for OrderType { - fn from(order_type: nash_protocol::types::OrderType) -> Self { - match order_type { - nash_protocol::types::OrderType::Limit => OrderType::Limit, - nash_protocol::types::OrderType::Market => OrderType::Market, - nash_protocol::types::OrderType::StopLimit => OrderType::StopLimit, - nash_protocol::types::OrderType::StopMarket => OrderType::StopMarket, - } - } -} - -impl From for Order { - fn from(resp: nash_protocol::protocol::place_order::PlaceOrderResponse) -> Self { - Self { - id: resp.order_id, - market_pair: resp.market.name, - client_order_id: None, - created_at: Some(resp.placed_at.timestamp_millis() as u64), - order_type: resp.order_type.into(), - side: resp.buy_or_sell.into(), - status: resp.status.into(), - size: Decimal::from(0), - price: None, - remaining: None, - trades: Vec::new(), - } - } -} - -impl TryFrom<&TradeHistoryRequest> - for nash_protocol::protocol::list_account_trades::ListAccountTradesRequest -{ - type Error = OpenLimitsError; - fn try_from(req: &TradeHistoryRequest) -> super::shared::Result { - let (before, limit, range) = try_split_paginator(req.paginator.clone())?; - - Ok(Self { - market: req.market_pair.clone(), - before, - limit, - range, - }) - } -} - -impl From for Trade { - fn from(resp: nash_protocol::types::Trade) -> Self { - let qty = Decimal::from_str(&resp.amount.to_string()) - .expect("Couldn't parse Decimal from string."); - let price = Decimal::from_str(&resp.limit_price.to_string()) - .expect("Couldn't parse Decimal from string."); - - let fees = match resp.account_side { - nash_protocol::types::AccountTradeSide::Taker => { - Decimal::from_str(&resp.taker_fee.to_string()) - .expect("Couldn't parse Decimal from string.") - } - _ => Decimal::from(0), - }; - - let (buyer_order_id, seller_order_id) = match resp.direction { - nash_protocol::types::BuyOrSell::Buy => (resp.taker_order_id, resp.maker_order_id), - nash_protocol::types::BuyOrSell::Sell => (resp.maker_order_id, resp.taker_order_id), - }; - - Self { - id: resp.id, - created_at: resp.executed_at.timestamp_millis() as u64, - fees: Some(fees), - liquidity: Some(resp.account_side.into()), - market_pair: resp.market.clone(), - buyer_order_id: Some(buyer_order_id), - seller_order_id: Some(seller_order_id), - price, - qty, - side: resp.direction.into(), - } - } -} - -impl From for Side { - fn from(side: nash_protocol::types::BuyOrSell) -> Self { - match side { - nash_protocol::types::BuyOrSell::Buy => Side::Buy, - nash_protocol::types::BuyOrSell::Sell => Side::Sell, - } - } -} - -impl From for Liquidity { - fn from(side: nash_protocol::types::AccountTradeSide) -> Self { - match side { - nash_protocol::types::AccountTradeSide::Taker => Liquidity::Taker, - _ => Liquidity::Maker, - } - } -} - -impl TryFrom<&GetHistoricRatesRequest> - for nash_protocol::protocol::list_candles::ListCandlesRequest -{ - type Error = OpenLimitsError; - fn try_from(req: &GetHistoricRatesRequest) -> super::shared::Result { - let (before, limit, range) = try_split_paginator(req.paginator.clone())?; - - Ok(Self { - market: req.market_pair.clone(), - chronological: None, - before, - interval: Some( - req.interval - .try_into() - .expect("Couldn't convert Interval to CandleInterval."), - ), - limit, - range, - }) - } -} - -impl TryFrom<&GetHistoricTradesRequest> - for nash_protocol::protocol::list_trades::ListTradesRequest -{ - type Error = OpenLimitsError; - fn try_from(req: &GetHistoricTradesRequest) -> super::shared::Result { - let market = req.market_pair.clone(); - let (before, limit, _) = try_split_paginator(req.paginator.clone())?; - //FIXME: Some issues with the graphql protocol for the market to be non nil - Ok(Self { - market, - before, - limit, - }) - } -} - -impl TryFrom for nash_protocol::types::CandleInterval { - type Error = OpenLimitsError; - fn try_from(interval: Interval) -> super::shared::Result { - match interval { - Interval::OneMinute => Ok(nash_protocol::types::CandleInterval::OneMinute), - Interval::FiveMinutes => Ok(nash_protocol::types::CandleInterval::FiveMinute), - Interval::FifteenMinutes => Ok(nash_protocol::types::CandleInterval::FifteenMinute), - Interval::ThirtyMinutes => Ok(nash_protocol::types::CandleInterval::ThirtyMinute), - Interval::OneHour => Ok(nash_protocol::types::CandleInterval::OneHour), - Interval::SixHours => Ok(nash_protocol::types::CandleInterval::SixHour), - Interval::TwelveHours => Ok(nash_protocol::types::CandleInterval::TwelveHour), - Interval::OneDay => Ok(nash_protocol::types::CandleInterval::OneDay), - _ => { - let err = MissingImplementationContent { - message: String::from("Not supported interval"), - }; - Err(OpenLimitsError::MissingImplementation(err)) - } - } - } -} - -impl From for Candle { - fn from(candle: nash_protocol::types::Candle) -> Self { - let close = Decimal::from_str(&candle.close_price.to_string()) - .expect("Couldn't parse Decimal from string."); - let high = Decimal::from_str(&candle.high_price.to_string()) - .expect("Couldn't parse Decimal from string."); - let low = Decimal::from_str(&candle.low_price.to_string()) - .expect("Couldn't parse Decimal from string."); - let open = Decimal::from_str(&candle.open_price.to_string()) - .expect("Couldn't parse Decimal from string."); - let volume = Decimal::from_str(&candle.a_volume.to_string()) - .expect("Couldn't parse Decimal from string."); - - Self { - close, - high, - low, - open, - time: candle.interval_start.timestamp_millis() as u64, - volume, - } - } -} - -impl TryFrom<&GetOrderHistoryRequest> - for nash_protocol::protocol::list_account_orders::ListAccountOrdersRequest -{ - type Error = OpenLimitsError; - fn try_from(req: &GetOrderHistoryRequest) -> super::shared::Result { - let (before, limit, range) = try_split_paginator(req.paginator.clone())?; - - Ok(Self { - market: req.market_pair.clone(), - before, - limit, - range, - buy_or_sell: None, - order_type: None, - status: match req.order_status.clone() { - Some(v) => Some( - v.into_iter() - .map(TryInto::try_into) - .collect::>>()?, - ), - None => None, - }, - }) - } -} - -impl From for Order { - fn from(order: nash_protocol::types::Order) -> Self { - let size = Decimal::from_str(&order.amount_placed.to_string()) - .expect("Couldn't parse Decimal from string."); - let price = order - .limit_price - .map(|p| Decimal::from_str(&p.to_string()).unwrap()); - let remaining = Some( - Decimal::from_str(&order.amount_remaining.to_string()) - .expect("Couldn't parse Decimal from string."), - ); - - Self { - id: order.id, - market_pair: order.market.clone(), - client_order_id: None, - created_at: Some(order.placed_at.timestamp_millis() as u64), - order_type: order.order_type.into(), - side: order.buy_or_sell.into(), - status: order.status.into(), - size, - price, - remaining, - trades: order.trades.into_iter().map(Into::into).collect(), - } - } -} - -impl From for OrderStatus { - fn from(status: nash_protocol::types::OrderStatus) -> Self { - match status { - nash_protocol::types::OrderStatus::Filled => OrderStatus::Filled, - nash_protocol::types::OrderStatus::Open => OrderStatus::Open, - nash_protocol::types::OrderStatus::Canceled => OrderStatus::Canceled, - nash_protocol::types::OrderStatus::Pending => OrderStatus::Pending, - } - } -} - -impl TryFrom for nash_protocol::types::OrderStatus { - type Error = OpenLimitsError; - fn try_from(status: OrderStatus) -> super::shared::Result { - Ok(match status { - OrderStatus::Filled => nash_protocol::types::OrderStatus::Filled, - OrderStatus::Open => nash_protocol::types::OrderStatus::Open, - OrderStatus::Canceled => nash_protocol::types::OrderStatus::Canceled, - OrderStatus::Pending => nash_protocol::types::OrderStatus::Pending, - _ => { - return Err(OpenLimitsError::InvalidParameter( - "Had invalid order status for Nash".to_string(), - )) - } - }) - } -} - -impl From<&GetPriceTickerRequest> for nash_protocol::protocol::get_ticker::TickerRequest { - fn from(req: &GetPriceTickerRequest) -> Self { - let market = req.market_pair.clone(); - - Self { market } - } -} - -impl From for Ticker { - fn from(resp: nash_protocol::protocol::get_ticker::TickerResponse) -> Self { - let mut price = None; - if resp.best_ask_price.is_some() && resp.best_bid_price.is_some() { - let ask = Decimal::from_str(&resp.best_ask_price.unwrap().to_string()) - .expect("Couldn't parse Decimal from string."); - let bid = Decimal::from_str(&resp.best_bid_price.unwrap().to_string()) - .expect("Couldn't parse Decimal from string."); - price = Some((ask + bid) / Decimal::from(2)); - } - let mut price_24h = None; - if resp.high_price_24h.is_some() && resp.low_price_24h.is_some() { - let day_high = Decimal::from_str( - &resp - .high_price_24h - .expect("Couldn't get high price 24h.") - .to_string(), - ) - .expect("Couldn't parse Decimal from string."); - let day_low = Decimal::from_str( - &resp - .low_price_24h - .expect("Couldn't get low price 24h.") - .to_string(), - ) - .expect("Couldn't parse Decimal from string."); - price_24h = Some((day_high + day_low) / Decimal::from(2)); - } - Self { price, price_24h } - } -} - -impl From<&GetOrderRequest> for nash_protocol::protocol::get_account_order::GetAccountOrderRequest { - fn from(req: &GetOrderRequest) -> Self { - Self { - order_id: req.id.clone(), - } - } -} - -impl From for BuyOrSell { - fn from(side: Side) -> Self { - match side { - Side::Buy => BuyOrSell::Buy, - Side::Sell => BuyOrSell::Sell, - } - } -} - -impl TryFrom for nash_protocol::types::OrderType { - type Error = OpenLimitsError; - fn try_from(order_type: OrderType) -> Result { - match order_type { - OrderType::Limit => Ok(Self::Limit), - OrderType::Market => Ok(Self::Market), - OrderType::StopLimit => Ok(Self::StopLimit), - OrderType::StopMarket => Ok(Self::StopMarket), - OrderType::Unknown => Err(OpenLimitsError::InvalidParameter( - "Had invalid order type for Nash".to_string(), - )), - } - } -} - -impl From for SubscribeAccountOrders { - fn from(account_orders: AccountOrders) -> Self { - Self { - market: account_orders.market.clone(), - order_type: account_orders.order_type.map(|x| { - x.iter() - .cloned() - .map(|x| x.try_into().ok()) - .filter(|x| x.is_some()) - .map(|x| x.unwrap()) - .collect() - }), - range: account_orders.range.map(|range| DateTimeRange { - start: timestamp_to_utc_datetime(range.start), - stop: timestamp_to_utc_datetime(range.end), - }), - buy_or_sell: account_orders.buy_or_sell.map(|x| x.into()), - status: account_orders.status.map(|x| { - x.iter() - .cloned() - .map(|x| x.try_into().ok()) - .filter(|x| x.is_some()) - .map(|x| x.unwrap()) - .collect() - }), - } - } -} - -impl From for nash_protocol::protocol::subscriptions::SubscriptionRequest { - fn from(sub: Subscription) -> Self { - match sub { - Subscription::OrderBookUpdates(market) => Self::Orderbook( - nash_protocol::protocol::subscriptions::updated_orderbook::SubscribeOrderbook { - market, - }, - ), - Subscription::Trades(market) => Self::Trades( - nash_protocol::protocol::subscriptions::trades::SubscribeTrades { market }, - ), - Subscription::AccountOrders(account_orders) => Self::AccountOrders( - account_orders.into() - ), - Subscription::AccountTrades(market_name) => Self::AccountTrades( - nash_protocol::protocol::subscriptions::new_account_trades::SubscribeAccountTrades { - market_name - } - ), - Subscription::AccountBalance(symbol) => Self::AccountBalances( - nash_protocol::protocol::subscriptions::updated_account_balances::SubscribeAccountBalances { - symbol - } - ), - _ => panic!("Not supported Subscription"), - } - } -} - -impl TryFrom for WebSocketResponse { - type Error = OpenLimitsError; - - fn try_from(value: SubscriptionResponseWrapper) -> Result { - match value.0 { - SubscriptionResponse::Orderbook(resp) => Ok(WebSocketResponse::Generic( - OpenLimitsWebSocketMessage::OrderBook(OrderBookResponse { - update_id: Some(resp.update_id as u64), - last_update_id: Some(resp.last_update_id as u64), - asks: resp.asks.into_iter().map(Into::into).collect(), - bids: resp.bids.into_iter().map(Into::into).collect(), - }), - )), - SubscriptionResponse::Trades(resp) => { - let trades = resp.trades.into_iter().map(|x| x.into()).collect(); - Ok(WebSocketResponse::Generic( - OpenLimitsWebSocketMessage::Trades(trades), - )) - } - SubscriptionResponse::Ticker(resp) => Ok(WebSocketResponse::Raw( - SubscriptionResponseWrapper(SubscriptionResponse::Ticker(resp)), - )), - SubscriptionResponse::AccountTrades(resp) => Ok(WebSocketResponse::Raw( - SubscriptionResponseWrapper(SubscriptionResponse::AccountTrades(resp)), - )), - SubscriptionResponse::AccountOrders(resp) => Ok(WebSocketResponse::Raw( - SubscriptionResponseWrapper(SubscriptionResponse::AccountOrders(resp)), - )), - SubscriptionResponse::AccountBalances(resp) => Ok(WebSocketResponse::Raw( - SubscriptionResponseWrapper(SubscriptionResponse::AccountBalances(resp)), - )), - } + async fn get_pair(&self, name: &MarketPair) -> Result { + let name = nash_protocol::types::market_pair::MarketPair::from(name.clone()).0; + self.exchange_info.get_pair(&name) } } diff --git a/src/exchange/nash/nash_credentials.rs b/src/exchange/nash/nash_credentials.rs index f42653a7..39e0f678 100644 --- a/src/exchange/nash/nash_credentials.rs +++ b/src/exchange/nash/nash_credentials.rs @@ -1,6 +1,6 @@ /// This structure represents the Nash account credentials #[derive(Clone)] pub struct NashCredentials { - pub secret: String, - pub session: String, + pub api_secret: String, + pub api_key: String, } \ No newline at end of file diff --git a/src/exchange/nash/nash_parameters.rs b/src/exchange/nash/nash_parameters.rs index 3ac62efb..2d3b6e50 100644 --- a/src/exchange/nash/nash_parameters.rs +++ b/src/exchange/nash/nash_parameters.rs @@ -1,6 +1,7 @@ use tokio::time::Duration; -pub use nash_native_client::{Client, Environment}; +pub use nash_native_client::Client; use super::NashCredentials; +use openlimits_exchange::exchange::Environment; /// This struct represents the parameters #[derive(Clone)] @@ -12,3 +13,27 @@ pub struct NashParameters { pub timeout: Duration, pub sign_states_loop_interval: Option, } + +impl NashParameters { + pub fn production() -> Self { + Self { + affiliate_code: None, + credentials: None, + client_id: 1, + environment: Environment::Production, + timeout: Duration::new(10, 0), + sign_states_loop_interval: None + } + } + + pub fn sandbox() -> Self { + Self { + affiliate_code: None, + credentials: None, + client_id: 1, + environment: Environment::Sandbox, + timeout: Duration::new(10, 0), + sign_states_loop_interval: None + } + } +} diff --git a/src/exchange/nash/nash_websocket.rs b/src/exchange/nash/nash_websocket.rs index d8791194..d51dcce1 100644 --- a/src/exchange/nash/nash_websocket.rs +++ b/src/exchange/nash/nash_websocket.rs @@ -2,14 +2,13 @@ use std::{pin::Pin, task::Context, task::Poll}; use async_trait::async_trait; use futures::stream::{BoxStream, SelectAll, Stream, StreamExt}; pub use nash_native_client::{Client, Environment}; -use nash_protocol::protocol::subscriptions::SubscriptionRequest; use nash_protocol::protocol::ResponseOrError; use crate::errors::OpenLimitsError; use crate::exchange::traits::stream::{ExchangeWs, Subscriptions}; use super::NashParameters; -use super::SubscriptionResponseWrapper; use super::utils::*; use super::shared::Result; +use nash_protocol::protocol::subscriptions::{SubscriptionRequest, SubscriptionResponse}; /// This struct represents a websocket connection pub struct NashWebsocket { @@ -30,9 +29,8 @@ impl Stream for NashWebsocket { #[async_trait] impl ExchangeWs for NashWebsocket { type InitParams = NashParameters; - type Subscription = SubscriptionRequest; - type Response = SubscriptionResponseWrapper; + type Response = SubscriptionResponse; async fn new(params: Self::InitParams) -> Result { Ok(Self { @@ -57,7 +55,7 @@ impl ExchangeWs for NashWebsocket { let s = streams.map(|message| match message { Ok(msg) => match msg { - ResponseOrError::Response(resp) => Ok(SubscriptionResponseWrapper(resp.data)), + ResponseOrError::Response(resp) => Ok(resp.data), ResponseOrError::Error(resp) => { let f = resp .errors diff --git a/src/exchange/nash/subscription_response_wrapper.rs b/src/exchange/nash/subscription_response_wrapper.rs deleted file mode 100644 index 25e0e83c..00000000 --- a/src/exchange/nash/subscription_response_wrapper.rs +++ /dev/null @@ -1,30 +0,0 @@ -pub use nash_native_client::{Client, Environment}; -use nash_protocol::protocol::subscriptions::SubscriptionResponse; - -#[derive(Debug)] -pub struct SubscriptionResponseWrapper(pub SubscriptionResponse); - -impl Clone for SubscriptionResponseWrapper { - fn clone(&self) -> Self { - match &self.0 { - SubscriptionResponse::Orderbook(o) => { - SubscriptionResponseWrapper(SubscriptionResponse::Orderbook(o.clone())) - } - SubscriptionResponse::Trades(t) => { - SubscriptionResponseWrapper(SubscriptionResponse::Trades(t.clone())) - } - SubscriptionResponse::Ticker(ticker) => { - SubscriptionResponseWrapper(SubscriptionResponse::Ticker(ticker.clone())) - } - SubscriptionResponse::AccountBalances(balances) => { - SubscriptionResponseWrapper(SubscriptionResponse::AccountBalances(balances.clone())) - } - SubscriptionResponse::AccountOrders(orders) => { - SubscriptionResponseWrapper(SubscriptionResponse::AccountOrders(orders.clone())) - } - SubscriptionResponse::AccountTrades(trades) => { - SubscriptionResponseWrapper(SubscriptionResponse::AccountTrades(trades.clone())) - } - } - } -} \ No newline at end of file diff --git a/src/exchange/nash/utils.rs b/src/exchange/nash/utils.rs index 788d0402..17712ed3 100644 --- a/src/exchange/nash/utils.rs +++ b/src/exchange/nash/utils.rs @@ -6,12 +6,12 @@ pub async fn client_from_params_failable(params: NashParameters) -> Result { Client::from_keys( - &credentials.secret, - &credentials.session, + &credentials.api_secret, + &credentials.api_key, params.affiliate_code, false, params.client_id, - params.environment, + params.environment.into(), params.timeout, ) .await? @@ -22,7 +22,7 @@ pub async fn client_from_params_failable(params: NashParameters) -> Result(parameters: E::InitParams) -> Result { - Ok(E::new(parameters).await?) - } -} +mod openlimits; +pub use crate::openlimits::*; \ No newline at end of file diff --git a/src/model/order_filter.rs b/src/model/order_filter.rs deleted file mode 100644 index f210cd7e..00000000 --- a/src/model/order_filter.rs +++ /dev/null @@ -1,83 +0,0 @@ -use rust_decimal::prelude::Decimal; -use serde::Deserialize; -use serde::Serialize; -use super::OrderStatus; -use super::OrderType; -use super::Side; -use std::ops::Range; - -/// This struct represents an order -#[derive(Default, Serialize, Deserialize, Clone, Debug)] -#[allow(missing_docs)] -pub struct OrderFilter { - pub market_pair: Option, - pub client_order_id: Option, - pub order_type: Option, - pub side: Option, - pub status: Option, - pub created_at: Option>, - pub size: Option>, - pub price: Option>, - pub remaining: Option>, -} - -impl OrderFilter { - /// Creates an OrderFilter without any filtering rule. - pub fn new() -> Self { - Default::default() - } - - /// Set market pair. - pub fn with_market_pair(mut self, market_pair: Option) -> Self { - self.market_pair = market_pair; - self - } - - /// Set client order ID. - pub fn with_client_order_id(mut self, client_order_id: Option) -> Self { - self.client_order_id = client_order_id; - self - } - - /// Set OrderType. - pub fn with_order_type(mut self, order_type: Option) -> Self { - self.order_type = order_type; - self - } - - /// Set Side. - pub fn with_side(mut self, side: Option) -> Self { - self.side = side; - self - } - - /// Set OrderStatus. - pub fn with_status(mut self, status: Option) -> Self { - self.status = status; - self - } - - /// Set creation time. - pub fn with_created_at(mut self, created_at: Option>) -> Self { - self.created_at = created_at; - self - } - - /// Set size. - pub fn with_size(mut self, size: Option>) -> Self { - self.size = size; - self - } - - /// Set price. - pub fn with_price(mut self, price: Option>) -> Self { - self.price = price; - self - } - - /// Set remaining. - pub fn with_remaining(mut self, remaining: Option>) -> Self { - self.remaining = remaining; - self - } -} \ No newline at end of file diff --git a/src/openlimits.rs b/src/openlimits.rs new file mode 100644 index 00000000..b218bbda --- /dev/null +++ b/src/openlimits.rs @@ -0,0 +1,16 @@ +#[cfg(feature = "bindings")] +use ligen_macro::inner_ligen; +#[cfg(feature = "bindings")] +inner_ligen!(ignore); + +use crate::prelude::*; +use crate::exchange::shared::Result; + +/// Can be used to initiate exchanges +pub struct OpenLimits {} + +impl OpenLimits { + pub async fn instantiate(parameters: E::InitParams) -> Result { + Ok(E::new(parameters).await?) + } +} diff --git a/src/prelude/mod.rs b/src/prelude/mod.rs index edd40969..e6b4eb75 100644 --- a/src/prelude/mod.rs +++ b/src/prelude/mod.rs @@ -2,8 +2,6 @@ pub use crate::model::{OrderBookRequest, OrderBookResponse}; pub use crate::exchange::traits::stream::ExchangeWs; -pub use crate::exchange::traits::info::{ExchangeInfo, ExchangeInfoRetrieval, MarketPair, MarketPairHandle}; +pub use crate::exchange::traits::info::{ExchangeInfo, ExchangeInfoRetrieval, MarketPairInfo, MarketPairHandle}; pub use crate::exchange::traits::{Exchange, ExchangeAccount, ExchangeMarketData}; -pub use nash_native_client::Environment; -pub use tokio::time::Duration; diff --git a/tests/binance/account.rs b/tests/binance/account.rs index e47e4184..e986c740 100644 --- a/tests/binance/account.rs +++ b/tests/binance/account.rs @@ -1,258 +1,57 @@ -use dotenv::dotenv; -use std::env; - -use openlimits::{ - OpenLimits, - exchange::binance::Binance, - exchange::binance::BinanceCredentials, - exchange::binance::BinanceParameters, - prelude::*, - model::{ - CancelAllOrdersRequest, CancelOrderRequest, GetOrderHistoryRequest, OpenLimitOrderRequest, - OpenMarketOrderRequest, TimeInForce, TradeHistoryRequest, - }, -}; -use rust_decimal::prelude::Decimal; -use openlimits::model::GetPriceTickerRequest; +use crate::template::account; +use super::client::init_signed as init; #[tokio::test] -#[ignore] async fn limit_buy() { - let pair_text = "BNBBUSD"; - let exchange = init().await; - let price = get_price(&exchange, pair_text).await; - let req = OpenLimitOrderRequest { - client_order_id: None, - price, - size: Decimal::new(1, 1), - market_pair: String::from(pair_text), - post_only: false, - time_in_force: TimeInForce::GoodTillCancelled, - }; - let resp = exchange.limit_buy(&req).await.expect("Couldn't limit buy."); - println!("{:?}", resp); + account::limit_buy(&init().await).await; } #[tokio::test] async fn limit_sell() { - let exchange = init().await; - let price = get_price(&exchange, "BNBBTC").await; - let req = OpenLimitOrderRequest { - client_order_id: None, - price, - post_only: false, - size: Decimal::new(1, 1), - market_pair: String::from("BNBBTC"), - time_in_force: TimeInForce::GoodTillCancelled, - }; - let resp = exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - println!("{:?}", resp); + account::limit_sell(&init().await).await; } #[tokio::test] -#[ignore] async fn post_only() { - let exchange = init().await; - /*let req = CancelAllOrdersRequest { - market_pair: Some("BNBBTC".to_string()), - }; - openlimits-exchange.cancel_all_orders(&req).await.expect("Couldn't cancel all orders.");*/ - - let req = OpenLimitOrderRequest { - client_order_id: None, - price: Decimal::new(1, 3), - size: Decimal::new(1, 1), - market_pair: String::from("BNBBTC"), - post_only: true, - time_in_force: TimeInForce::GoodTillCancelled, - }; - let resp = exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - println!("{:?}", resp); + account::post_only(&init().await).await; } #[tokio::test] -#[ignore] async fn market_buy() { - let exchange = init().await; - let req = OpenMarketOrderRequest { - client_order_id: None, - size: Decimal::new(1, 2), - market_pair: String::from("BNBBUSD"), - }; - let resp = exchange - .market_buy(&req) - .await - .expect("Couldn't market buy."); - println!("{:?}", resp); + account::market_buy(&init().await).await; } #[tokio::test] async fn market_sell() { - let exchange = init().await; - let req = OpenMarketOrderRequest { - client_order_id: None, - size: Decimal::new(1, 1), - market_pair: String::from("BNBBTC"), - }; - let resp = exchange - .market_sell(&req) - .await - .expect("Couldn't market sell."); - println!("{:?}", resp); + account::market_sell(&init().await).await; } #[tokio::test] -#[ignore = "TODO fix"] async fn cancel_order() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - price: Decimal::new(1, 3), - size: Decimal::new(1, 1), - market_pair: String::from("BNBBTC"), - post_only: false, - time_in_force: TimeInForce::GoodTillCancelled, - }; - let order = exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - - let req = CancelOrderRequest { - id: order.id, - market_pair: Some(order.market_pair), - }; - - let resp = exchange - .cancel_order(&req) - .await - .expect("Couldn't cancel order."); - println!("{:?}", resp); + account::cancel_order(&init().await).await; } #[tokio::test] -#[ignore = "TODO fix"] async fn cancel_all_orders() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - price: Decimal::new(1, 3), - size: Decimal::new(1, 1), - market_pair: String::from("BNBBTC"), - post_only: false, - time_in_force: TimeInForce::GoodTillCancelled, - }; - exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - - exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - - let req = CancelAllOrdersRequest { - market_pair: Some("BNBBTC".to_string()), - }; - - let resp = exchange - .cancel_all_orders(&req) - .await - .expect("Couldn't cancel all orders."); - println!("{:?}", resp); + account::cancel_all_orders(&init().await).await; } #[tokio::test] async fn get_order_history() { - let exchange = init().await; - let req = GetOrderHistoryRequest { - market_pair: Some(String::from("BNBBTC")), - order_status: None, - paginator: None, - }; - - let resp = exchange - .get_order_history(&req) - .await - .expect("Couldn't get order history."); - println!("{:?}", resp); -} - -async fn get_price(exchange: &Binance, pair: &str) -> Decimal { - let get_price_ticker_request = GetPriceTickerRequest { market_pair: pair.to_string() }; - let ticker = exchange.get_price_ticker(&get_price_ticker_request).await.expect("Couldn't get ticker."); - ticker.price.expect("Couldn't get price.") + account::get_order_history(&init().await).await; } #[tokio::test] async fn get_all_open_orders() { - let exchange = init().await; - let price = get_price(&exchange, "BNBBTC").await; - let req = OpenLimitOrderRequest { - client_order_id: None, - price, - size: Decimal::new(1, 1), - market_pair: String::from("BNBBTC"), - post_only: false, - time_in_force: TimeInForce::GoodTillCancelled, - }; - exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - - let resp = exchange - .get_all_open_orders() - .await - .expect("Couldn't get all open orders."); - println!("{:?}", resp); + account::get_all_open_orders(&init().await).await; } #[tokio::test] async fn get_account_balances() { - let exchange = init().await; - - let resp = exchange - .get_account_balances(None) - .await - .expect("Couldn't get acount balances."); - println!("{:?}", resp); + account::get_account_balances(&init().await).await; } #[tokio::test] async fn get_trade_history() { - let exchange = init().await; - let req = TradeHistoryRequest { - market_pair: Some("BNBBTC".to_string()), - ..Default::default() - }; - - let resp = exchange - .get_trade_history(&req) - .await - .expect("Couldn't get trade history."); - println!("{:?}", resp); -} - -async fn init() -> Binance { - dotenv().ok(); - - let parameters = BinanceParameters { - credentials: Some(BinanceCredentials { - api_key: env::var("BINANCE_API_KEY").expect("Couldn't get environment variable."), - api_secret: env::var("BINANCE_API_SECRET").expect("Couldn't get environment variable."), - }), - sandbox: true, - }; - - OpenLimits::instantiate(parameters) - .await - .expect("Failed to create Client") + account::get_trade_history(&init().await).await; } diff --git a/tests/binance/callbacks.rs b/tests/binance/callbacks.rs new file mode 100644 index 00000000..f96142b8 --- /dev/null +++ b/tests/binance/callbacks.rs @@ -0,0 +1,12 @@ +use crate::template::callbacks; +use super::client::init_ws as init; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn orderbook() { + callbacks::orderbook(&init().await).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn trades() { + callbacks::trades(&init().await).await; +} \ No newline at end of file diff --git a/tests/binance/client.rs b/tests/binance/client.rs new file mode 100644 index 00000000..11b0cd8e --- /dev/null +++ b/tests/binance/client.rs @@ -0,0 +1,38 @@ +use dotenv::dotenv; +use openlimits::{ + exchange::binance::{Binance, BinanceParameters, BinanceWebsocket, BinanceCredentials}, + OpenLimits, + exchange::traits::{ + Exchange, + stream::ExchangeWs + } +}; +use openlimits_exchange::exchange::Environment; + +pub async fn init_ws() -> BinanceWebsocket { + BinanceWebsocket::new(BinanceParameters::production()) + .await + .expect("Failed to create Binance stream.") +} + +pub async fn init() -> Binance { + Binance::new(BinanceParameters::sandbox()) + .await + .expect("Failed to create Client") +} + +pub async fn init_signed() -> Binance { + dotenv().ok(); + + let parameters = BinanceParameters { + credentials: Some(BinanceCredentials { + api_key: std::env::var("BINANCE_API_KEY").expect("Couldn't get environment variable."), + api_secret: std::env::var("BINANCE_API_SECRET").expect("Couldn't get environment variable."), + }), + environment: Environment::Sandbox, + }; + + OpenLimits::instantiate(parameters) + .await + .expect("Failed to create Client") +} \ No newline at end of file diff --git a/tests/binance/market.rs b/tests/binance/market.rs index e7e4b91a..561dfacd 100644 --- a/tests/binance/market.rs +++ b/tests/binance/market.rs @@ -1,67 +1,22 @@ -use openlimits::{ - OpenLimits, - exchange::binance::Binance, - exchange::binance::BinanceParameters, - prelude::*, - model::{GetHistoricRatesRequest, GetPriceTickerRequest, Interval, OrderBookRequest}, -}; +use crate::template::market; +use super::client::init; #[tokio::test] async fn order_book() { - let exchange = init().await; - let req = OrderBookRequest { - market_pair: "BNBBTC".to_string(), - }; - let _response = exchange - .order_book(&req) - .await - .expect("Couldn't get order book."); + market::order_book(&init().await).await; } #[tokio::test] async fn get_price_ticker() { - let exchange = init().await; - let req = GetPriceTickerRequest { - market_pair: "BNBBTC".to_string(), - }; - let _response = exchange - .get_price_ticker(&req) - .await - .expect("Couldn't get price ticker."); + market::get_price_ticker(&init().await).await; } #[tokio::test] async fn get_historic_rates() { - let exchange = init().await; - let req = GetHistoricRatesRequest { - market_pair: "BNBBTC".to_string(), - interval: Interval::OneHour, - paginator: None, - }; - let _response = exchange - .get_historic_rates(&req) - .await - .expect("Couldn't get historic rates."); + market::get_historic_rates(&init().await).await; } #[tokio::test] async fn pair() { - let exchange = Binance::new(BinanceParameters::sandbox()) - .await - .expect("Failed to create Client"); - let _response = exchange - .get_pair("BTCUSDT") - .await - .expect("Couldn't get pair."); -} - -async fn init() -> Binance { - let parameters = BinanceParameters { - credentials: None, - sandbox: true, - }; - - OpenLimits::instantiate(parameters) - .await - .expect("Failed to create Client") + market::pair(&init().await).await; } diff --git a/tests/binance/mod.rs b/tests/binance/mod.rs index 94d82bd7..7a509945 100644 --- a/tests/binance/mod.rs +++ b/tests/binance/mod.rs @@ -1,4 +1,5 @@ mod account; mod market; -mod ws_callbacks; -mod ws_streams; +mod callbacks; +mod streams; +pub mod client; \ No newline at end of file diff --git a/tests/binance/streams.rs b/tests/binance/streams.rs new file mode 100644 index 00000000..88c96950 --- /dev/null +++ b/tests/binance/streams.rs @@ -0,0 +1,12 @@ +use crate::template::streams; +use super::client::init_ws as init; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn orderbook() { + streams::orderbook(&init().await).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn trades() { + streams::trades(&init().await).await; +} diff --git a/tests/binance/ws_callbacks.rs b/tests/binance/ws_callbacks.rs deleted file mode 100644 index 7fbd0332..00000000 --- a/tests/binance/ws_callbacks.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::sync::mpsc::sync_channel; - -use openlimits::{ - exchange::binance::{BinanceParameters, client::websocket::BinanceWebsocket}, - model::websocket::Subscription, -}; -use openlimits::exchange::traits::stream::{ExchangeWs, OpenLimitsWs}; - -async fn test_subscription_callback(websocket: OpenLimitsWs, sub: Subscription) { - let (tx, rx) = sync_channel(0); - - websocket - .subscribe(sub, move |m| { - m.as_ref().expect("Couldn't get response."); - tx.send(()).expect("Couldn't send sync message."); - }) - .await - .expect("Couldn't subscribe."); - - rx.recv().expect("Couldn't receive sync message."); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn orderbook() { - let ws = init().await; - let sub = Subscription::OrderBookUpdates("bnbbtc".to_string()); - test_subscription_callback(ws, sub).await; -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn trades() { - let ws = init().await; - let sub = Subscription::Trades("btcusdt".to_string()); - test_subscription_callback(ws, sub).await; -} - -async fn init() -> OpenLimitsWs { - OpenLimitsWs { - websocket: BinanceWebsocket::new(BinanceParameters::prod()) - .await - .expect("Failed to create Client"), - } -} diff --git a/tests/binance/ws_streams.rs b/tests/binance/ws_streams.rs deleted file mode 100644 index 10298387..00000000 --- a/tests/binance/ws_streams.rs +++ /dev/null @@ -1,39 +0,0 @@ -use futures::stream::StreamExt; - -use openlimits::{ - exchange::binance::{BinanceParameters, BinanceWebsocket}, - model::websocket::Subscription, -}; -use openlimits::exchange::traits::stream::{ExchangeWs, OpenLimitsWs}; - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn orderbook() { - let ws = init().await; - let s = ws - .create_stream(&[Subscription::OrderBookUpdates("bnbbtc".to_string())]) - .await; - - let ob = s.expect("Couldn't create stream.").next().await; - - print!("{:?}", ob); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn trades() { - let ws = init().await; - let s = ws - .create_stream(&[Subscription::Trades("bnbbtc".to_string())]) - .await; - - let trades = s.expect("Couldn't create stream.").next().await; - - print!("{:?}", trades); -} - -async fn init() -> OpenLimitsWs { - OpenLimitsWs { - websocket: BinanceWebsocket::new(BinanceParameters::prod()) - .await - .expect("Failed to create Client"), - } -} diff --git a/tests/coinbase/account.rs b/tests/coinbase/account.rs index bf0c0803..e986c740 100644 --- a/tests/coinbase/account.rs +++ b/tests/coinbase/account.rs @@ -1,245 +1,57 @@ -use dotenv::dotenv; -use std::env; - -use openlimits::{ - OpenLimits, - exchange::coinbase::Coinbase, - exchange::coinbase::CoinbaseCredentials, - exchange::coinbase::CoinbaseParameters, - prelude::*, - model::{ - CancelAllOrdersRequest, CancelOrderRequest, GetOrderHistoryRequest, OpenLimitOrderRequest, - OpenMarketOrderRequest, TimeInForce, TradeHistoryRequest, - }, -}; -use rust_decimal::prelude::Decimal; +use crate::template::account; +use super::client::init_signed as init; #[tokio::test] -#[ignore] async fn limit_buy() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillCancelled, - post_only: false, - price: Decimal::new(1, 3), - size: Decimal::new(1, 1), - market_pair: String::from("ETH-BTC"), - }; - let resp = ExchangeAccount::limit_buy(&exchange, &req) - .await - .expect("Couldn't limit buy."); - println!("{:?}", resp); + account::limit_buy(&init().await).await; } #[tokio::test] -#[ignore] async fn limit_sell() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillCancelled, - post_only: false, - price: Decimal::new(1, 1), - size: Decimal::new(1, 1), - market_pair: String::from("ETH-BTC"), - }; - let resp = exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - println!("{:?}", resp); + account::limit_sell(&init().await).await; } #[tokio::test] async fn post_only() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillCancelled, - price: Decimal::new(1, 1), - size: Decimal::new(1, 1), - post_only: true, - market_pair: String::from("ETH-BTC"), - }; - let resp = exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - println!("{:?}", resp); + account::post_only(&init().await).await; } #[tokio::test] -#[ignore] async fn market_buy() { - let exchange = init().await; - let req = OpenMarketOrderRequest { - client_order_id: None, - size: Decimal::new(1, 1), - market_pair: String::from("ETH-BTC"), - }; - let resp = exchange - .market_buy(&req) - .await - .expect("Couldn't limit buy."); - println!("{:?}", resp); + account::market_buy(&init().await).await; } #[tokio::test] -#[ignore] async fn market_sell() { - let exchange = init().await; - let req = OpenMarketOrderRequest { - client_order_id: None, - size: Decimal::new(1, 1), - market_pair: String::from("ETH-BTC"), - }; - let resp = exchange - .market_sell(&req) - .await - .expect("Couldn't market sell."); - println!("{:?}", resp); + account::market_sell(&init().await).await; } #[tokio::test] async fn cancel_order() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillCancelled, - post_only: false, - price: Decimal::new(1, 1), - size: Decimal::new(1, 1), - market_pair: String::from("ETH-BTC"), - }; - let order = exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - - let req = CancelOrderRequest { - id: order.id, - market_pair: Some(order.market_pair), - }; - let resp = exchange - .cancel_order(&req) - .await - .expect("Couldn't cancel order."); - println!("{:?}", resp); + account::cancel_order(&init().await).await; } #[tokio::test] -#[ignore] async fn cancel_all_orders() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillCancelled, - post_only: false, - price: Decimal::new(1, 1), - size: Decimal::new(1, 1), - market_pair: String::from("ETH-BTC"), - }; - exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - - exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - - let req = CancelAllOrdersRequest { - market_pair: Some("ETH-BTC".to_string()), - }; - - let resp = exchange - .cancel_all_orders(&req) - .await - .expect("Couldn't cancel all orders."); - println!("{:?}", resp); + account::cancel_all_orders(&init().await).await; } #[tokio::test] async fn get_order_history() { - let exchange = init().await; - let req = GetOrderHistoryRequest { - market_pair: Some(String::from("ETH-BTC")), - order_status: None, - paginator: None, - }; - - let resp = exchange - .get_order_history(&req) - .await - .expect("Couldn't get order history."); - println!("{:?}", resp); + account::get_order_history(&init().await).await; } #[tokio::test] async fn get_all_open_orders() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillCancelled, - post_only: false, - price: Decimal::new(1, 1), - size: Decimal::new(1, 1), - market_pair: String::from("ETH-BTC"), - }; - exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - - let resp = exchange - .get_all_open_orders() - .await - .expect("Couldn't get all open orders."); - println!("{:?}", resp); + account::get_all_open_orders(&init().await).await; } #[tokio::test] async fn get_account_balances() { - let exchange = init().await; - let resp = exchange - .get_account_balances(None) - .await - .expect("Couldn't get account balances."); - println!("{:?}", resp); + account::get_account_balances(&init().await).await; } #[tokio::test] async fn get_trade_history() { - let exchange = init().await; - let req = TradeHistoryRequest { - market_pair: Some("ETH-BTC".to_string()), - ..Default::default() - }; - - let resp = exchange - .get_trade_history(&req) - .await - .expect("Couldn't get trade history."); - println!("{:?}", resp); -} - -async fn init() -> Coinbase { - dotenv().ok(); - - let parameters = CoinbaseParameters { - credentials: Some(CoinbaseCredentials { - api_key: env::var("COINBASE_API_KEY").expect("Couldn't get environment variable."), - api_secret: env::var("COINBASE_API_SECRET") - .expect("Couldn't get environment variable."), - passphrase: env::var("COINBASE_PASSPHRASE") - .expect("Couldn't get environment variable."), - }), - sandbox: true, - }; - - OpenLimits::instantiate(parameters) - .await - .expect("Failed to create Client") + account::get_trade_history(&init().await).await; } diff --git a/tests/coinbase/callbacks.rs b/tests/coinbase/callbacks.rs new file mode 100644 index 00000000..f96142b8 --- /dev/null +++ b/tests/coinbase/callbacks.rs @@ -0,0 +1,12 @@ +use crate::template::callbacks; +use super::client::init_ws as init; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn orderbook() { + callbacks::orderbook(&init().await).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn trades() { + callbacks::trades(&init().await).await; +} \ No newline at end of file diff --git a/tests/coinbase/client.rs b/tests/coinbase/client.rs new file mode 100644 index 00000000..9501d0ed --- /dev/null +++ b/tests/coinbase/client.rs @@ -0,0 +1,36 @@ +use dotenv::dotenv; +use openlimits::{ + exchange::coinbase::{Coinbase, CoinbaseParameters, CoinbaseWebsocket, CoinbaseCredentials}, + OpenLimits, +}; +use openlimits::prelude::*; +use openlimits_exchange::exchange::Environment; + +pub async fn init_ws() -> CoinbaseWebsocket { + CoinbaseWebsocket::new(CoinbaseParameters::production()) + .await + .expect("Failed to create Binance stream.") +} + +pub async fn init() -> Coinbase { + Coinbase::new(CoinbaseParameters::sandbox()) + .await + .expect("Failed to create Client") +} + +pub async fn init_signed() -> Coinbase { + dotenv().ok(); + + let parameters = CoinbaseParameters { + credentials: Some(CoinbaseCredentials { + api_key: std::env::var("COINBASE_API_KEY").expect("Couldn't get environment variable."), + api_secret: std::env::var("COINBASE_API_SECRET").expect("Couldn't get environment variable."), + passphrase: std::env::var("COINBASE_PASSPHRASE").expect("Couldn't get environment variable.") + }), + environment: Environment::Sandbox, + }; + + OpenLimits::instantiate(parameters) + .await + .expect("Failed to create Client") +} \ No newline at end of file diff --git a/tests/coinbase/exchange.rs b/tests/coinbase/exchange.rs deleted file mode 100644 index 921af01c..00000000 --- a/tests/coinbase/exchange.rs +++ /dev/null @@ -1,21 +0,0 @@ -use openlimits::exchange::coinbase::{Coinbase, CoinbaseParameters}; -use openlimits::OpenLimits; -use openlimits::prelude::*; - -#[tokio::test] -async fn retrieve_pairs() { - let exchange = init().await; - let result = exchange.retrieve_pairs().await.expect("Couldn't retrieve pairs"); - println!("{:#?}", result); -} - -async fn init() -> Coinbase { - let parameters = CoinbaseParameters { - credentials: None, - sandbox: true, - }; - - OpenLimits::instantiate(parameters) - .await - .expect("Failed to create Client") -} diff --git a/tests/coinbase/market.rs b/tests/coinbase/market.rs index d121f5f0..561dfacd 100644 --- a/tests/coinbase/market.rs +++ b/tests/coinbase/market.rs @@ -1,81 +1,22 @@ -use openlimits::{ - OpenLimits, - exchange::coinbase::Coinbase, - exchange::coinbase::CoinbaseParameters, - prelude::*, - model::{GetHistoricRatesRequest, GetPriceTickerRequest, Interval, OrderBookRequest}, -}; +use crate::template::market; +use super::client::init; #[tokio::test] async fn order_book() { - let exchange = init().await; - let req = OrderBookRequest { - market_pair: "ETH-BTC".to_string(), - }; - let _response = exchange - .order_book(&req) - .await - .expect("Couldn't get order book."); + market::order_book(&init().await).await; } #[tokio::test] async fn get_price_ticker() { - let exchange = init().await; - let req = GetPriceTickerRequest { - market_pair: "ETH-BTC".to_string(), - }; - let _response = exchange - .get_price_ticker(&req) - .await - .expect("Couldn't get price ticker."); + market::get_price_ticker(&init().await).await; } #[tokio::test] async fn get_historic_rates() { - let exchange = init().await; - let req = GetHistoricRatesRequest { - market_pair: "ETH-BTC".to_string(), - interval: Interval::OneHour, - paginator: None, - }; - let _response = exchange - .get_historic_rates(&req) - .await - .expect("Couldn't get historic rates."); -} - -#[tokio::test] -async fn get_historic_rates_invalid_interval() { - let exchange = init().await; - let req = GetHistoricRatesRequest { - market_pair: "ETH-BTC".to_string(), - interval: Interval::TwoHours, - paginator: None, - }; - let _response = exchange - .get_historic_rates(&req) - .await - .expect_err("Invalid rate isn't invalid."); + market::get_historic_rates(&init().await).await; } #[tokio::test] async fn pair() { - let exchange = Coinbase::new(CoinbaseParameters::sandbox()) - .await - .expect("Failed to create Client"); - let _response = exchange - .get_pair("BTC-USD") - .await - .expect("Couldn't get pair."); -} - -async fn init() -> Coinbase { - let parameters = CoinbaseParameters { - credentials: None, - sandbox: true, - }; - - OpenLimits::instantiate(parameters) - .await - .expect("Failed to create Client") + market::pair(&init().await).await; } diff --git a/tests/coinbase/mod.rs b/tests/coinbase/mod.rs index b1498295..7a509945 100644 --- a/tests/coinbase/mod.rs +++ b/tests/coinbase/mod.rs @@ -1,3 +1,5 @@ mod account; mod market; -mod exchange; \ No newline at end of file +mod callbacks; +mod streams; +pub mod client; \ No newline at end of file diff --git a/tests/coinbase/streams.rs b/tests/coinbase/streams.rs new file mode 100644 index 00000000..88c96950 --- /dev/null +++ b/tests/coinbase/streams.rs @@ -0,0 +1,12 @@ +use crate::template::streams; +use super::client::init_ws as init; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn orderbook() { + streams::orderbook(&init().await).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn trades() { + streams::trades(&init().await).await; +} diff --git a/tests/exchange/websocket.rs b/tests/exchange/websocket.rs index a185127e..a1ddd8f2 100644 --- a/tests/exchange/websocket.rs +++ b/tests/exchange/websocket.rs @@ -3,15 +3,15 @@ // supported exchanges. use dotenv::dotenv; -use nash_native_client::Environment; +use openlimits::prelude::*; use openlimits::exchange::coinbase::client::websocket::CoinbaseWebsocket; use openlimits::exchange::coinbase::{Coinbase, CoinbaseCredentials, CoinbaseParameters}; use openlimits::OpenLimits; -use openlimits::exchange::nash::{Nash, NashCredentials, NashParameters}; use openlimits::exchange::shared::Result; use std::env; -use tokio::time::Duration; -use openlimits::prelude::*; +// use nash_native_client::Environment; +// use openlimits::exchange::old_nash_tests::{Nash, NashCredentials, NashParameters}; +// use tokio::time::Duration; #[tokio::test] async fn account_test() { @@ -23,20 +23,20 @@ async fn ws_test() { let _websocket = init_ws().await; } -async fn _nash() -> Result { - let parameters = NashParameters { - affiliate_code: None, - credentials: Some(NashCredentials { - secret: env::var("NASH_API_SECRET").expect("Couldn't get environment variable."), - session: env::var("NASH_API_KEY").expect("Couldn't get environment variable."), - }), - environment: Environment::Sandbox, - client_id: 1, - timeout: Duration::from_secs_f32(10.0), - sign_states_loop_interval: None, - }; - OpenLimits::instantiate(parameters).await -} +// async fn _nash() -> Result { +// let parameters = NashParameters { +// affiliate_code: None, +// credentials: Some(NashCredentials { +// secret: env::var("NASH_API_SECRET").expect("Couldn't get environment variable."), +// session: env::var("NASH_API_KEY").expect("Couldn't get environment variable."), +// }), +// environment: Environment::Sandbox, +// client_id: 1, +// timeout: Duration::from_secs_f32(10.0), +// sign_states_loop_interval: None, +// }; +// OpenLimits::instantiate(parameters).await +// } async fn coinbase() -> Result { let parameters = CoinbaseParameters { diff --git a/tests/mod.rs b/tests/mod.rs index e0c2354c..6721ddd8 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -1,7 +1,9 @@ extern crate openlimits; -mod exchange; -mod apis; -// mod openlimits-binance; +mod template; +// mod exchange; +// mod apis; +mod binance; mod coinbase; mod nash; +// mod old_nash_tests; diff --git a/tests/nash/account.rs b/tests/nash/account.rs index 2b1deba2..887d5d8b 100644 --- a/tests/nash/account.rs +++ b/tests/nash/account.rs @@ -1,279 +1,58 @@ -use chrono::Duration; -use dotenv::dotenv; -use nash_native_client::Environment; -use openlimits::{ - OpenLimits, - prelude::*, - model::OpenMarketOrderRequest, - model::{ - CancelAllOrdersRequest, CancelOrderRequest, GetOrderHistoryRequest, OpenLimitOrderRequest, - TimeInForce, TradeHistoryRequest, - }, - exchange::nash::Nash, - exchange::nash::NashCredentials, - exchange::nash::NashParameters, -}; -use rust_decimal::prelude::{Decimal, FromStr}; -use std::env; -use std::time::Duration as NativeDuration; +use crate::template::account; +use super::client::init_signed as init; -// FIXME: https://github.com/nash-io/openlimits/issues/157 -// #[tokio::test] -async fn _limit_buy() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillCancelled, - price: Decimal::from_str("100.46").expect("Couldn't parse string."), - size: Decimal::from_str("0.10000").expect("Couldn't parse string."), - market_pair: String::from("eth_usdc"), - post_only: false, - }; - let resp = exchange - .limit_buy(&req) - .await - .expect("Couldn't parse string."); - println!("{:?}", resp); -} - -// FIXME: https://github.com/nash-io/openlimits/issues/157 -// #[tokio::test] -async fn _limit_buy_ioc() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::ImmediateOrCancelled, - price: Decimal::from_str("414.46").expect("Couldn't parse string."), - size: Decimal::from_str("0.10000").expect("Couldn't parse string."), - market_pair: String::from("eth_usdc"), - post_only: false, - }; - let resp = exchange - .limit_buy(&req) - .await - .expect("Couldn't request limit buy."); - println!("{:?}", resp); -} - -// FIXME: https://github.com/nash-io/openlimits/issues/157 -// #[tokio::test] -async fn _limit_buy_fok() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::FillOrKill, - price: Decimal::from_str("414.46").expect("Couldn't parse string."), - size: Decimal::from_str("0.10000").expect("Couldn't parse string."), - market_pair: String::from("eth_usdc"), - post_only: false, - }; - let resp = exchange - .limit_buy(&req) - .await - .expect("Couldn't request limit buy."); - println!("{:?}", resp); +#[tokio::test] +async fn limit_buy() { + account::limit_buy(&init().await).await; } #[tokio::test] -#[ignore] -async fn limit_buy_ggt() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillTime(Duration::hours(2)), - price: Decimal::from_str("414.46").expect("Couldn't parse string."), - size: Decimal::from_str("0.02000").expect("Couldn't parse string."), - market_pair: String::from("eth_usdc"), - post_only: false, - }; - let resp = exchange - .limit_buy(&req) - .await - .expect("Couldn't request limit buy."); - println!("{:?}", resp); +async fn limit_sell() { + account::limit_sell(&init().await).await; } -// FIXME: https://github.com/nash-io/openlimits/issues/157 -// #[tokio::test] -async fn _market_buy() { - let exchange = init().await; - let req = OpenMarketOrderRequest { - client_order_id: None, - size: Decimal::from_str("10.0").expect("Couldn't parse string."), - market_pair: String::from("usdc_eth"), - }; - let resp = exchange - .market_sell(&req) - .await - .expect("Couldn't request market buy."); - println!("{:?}", resp); +#[tokio::test] +async fn post_only() { + account::post_only(&init().await).await; } -// FIXME: https://github.com/nash-io/openlimits/issues/157 -// #[tokio::test] -async fn _market_sell() { - let exchange = init().await; - let req = OpenMarketOrderRequest { - client_order_id: None, - size: Decimal::from_str("0.02").expect("Couldn't parse string."), - market_pair: String::from("eth_usdc"), - }; - let resp = exchange - .market_sell(&req) - .await - .expect("Couldn't request market buy."); - println!("{:?}", resp); +#[tokio::test] +#[should_panic] +async fn market_buy() { + account::market_buy(&init().await).await; } -// FIXME: https://github.com/nash-io/openlimits/issues/157 -// #[tokio::test] -async fn _limit_sell() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillTime(Duration::hours(2)), - price: Decimal::from_str("800.46").expect("Couldn't parse string."), - size: Decimal::from_str("0.02").expect("Couldn't parse string."), - market_pair: String::from("eth_usdc"), - post_only: false, - }; - let resp = exchange - .limit_sell(&req) - .await - .expect("Couldn't request limit sell."); - println!("{:?}", resp); +#[tokio::test] +async fn market_sell() { + account::market_sell(&init().await).await; } -// FIXME: https://github.com/nash-io/openlimits/issues/157 -// #[tokio::test] -async fn _cancel_order() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillCancelled, - price: Decimal::from_str("200.46").expect("Couldn't parse string."), - size: Decimal::from_str("0.0300").expect("Couldn't parse string."), - market_pair: String::from("eth_usdc"), - post_only: false, - }; - let order = exchange - .limit_buy(&req) - .await - .expect("Couldn't request limit buy."); - - let req = CancelOrderRequest { - id: order.id, - market_pair: Some(order.market_pair), - }; - let resp = exchange - .cancel_order(&req) - .await - .expect("Couldn't cancel order."); - println!("{:?}", resp); +#[tokio::test] +async fn cancel_order() { + account::cancel_order(&init().await).await; } -// FIXME: https://github.com/nash-io/openlimits/issues/157 -// #[tokio::test] -async fn _cancel_all_orders() { - let exchange = init().await; - let req = OpenLimitOrderRequest { - client_order_id: None, - time_in_force: TimeInForce::GoodTillCancelled, - price: Decimal::from_str("200.46").expect("Couldn't parse string."), - size: Decimal::from_str("0.10000").expect("Couldn't parse string."), - market_pair: String::from("eth_usdc"), - post_only: false, - }; - - exchange - .limit_sell(&req) - .await - .expect("Couldn't limit sell."); - - let req = CancelAllOrdersRequest { - market_pair: Some("eth_btc".to_string()), - }; - - let resp = exchange - .cancel_all_orders(&req) - .await - .expect("Couldn't cancel all orders."); - println!("{:?}", resp); +#[tokio::test] +async fn cancel_all_orders() { + account::cancel_all_orders(&init().await).await; } #[tokio::test] async fn get_order_history() { - let exchange = init().await; - let req = GetOrderHistoryRequest { - market_pair: Some(String::from("eth_btc")), - order_status: None, - paginator: None, - }; - - let resp = exchange - .get_order_history(&req) - .await - .expect("Couldn't get order history."); - println!("{:?}", resp); + account::get_order_history(&init().await).await; } -// #[tokio::test] -// async fn get_all_open_orders() { -// let mut openlimits-exchange = init().await; -// let req = OpenLimitOrderRequest { -// client_order_id: None, -// time_in_force: TimeInForce::GoodTillCancelled, -// price: Decimal::new(1, 1), -// size: Decimal::new(2, 2), -// market_pair: String::from("eth_btc"), -// }; -// openlimits-exchange.limit_sell(&req).await.expect("Couldn't limit sell."); - -// let resp = openlimits-exchange.get_all_open_orders().await.expect("Couldn't get all open orders."); -// println!("{:?}", resp); -// } +#[tokio::test] +async fn get_all_open_orders() { + account::get_all_open_orders(&init().await).await; +} #[tokio::test] async fn get_account_balances() { - let exchange = init().await; - let resp = exchange - .get_account_balances(None) - .await - .expect("Couldn't get account balances."); - println!("{:?}", resp); + account::get_account_balances(&init().await).await; } #[tokio::test] async fn get_trade_history() { - let exchange = init().await; - let req = TradeHistoryRequest { - market_pair: Some("eth_btc".to_string()), - ..Default::default() - }; - - let resp = exchange - .get_trade_history(&req) - .await - .expect("Couldn't get trade history."); - println!("{:?}", resp); -} - -async fn init() -> Nash { - dotenv().ok(); - - let parameters = NashParameters { - affiliate_code: None, - credentials: Some(NashCredentials { - secret: env::var("NASH_API_SECRET").expect("Couldn't get environment variable."), - session: env::var("NASH_API_KEY").expect("Couldn't get environment variable."), - }), - environment: Environment::Sandbox, - client_id: 1, - timeout: NativeDuration::new(10, 0), - sign_states_loop_interval: None, - }; - - OpenLimits::instantiate(parameters) - .await - .expect("Failed to create Client") + account::get_trade_history(&init().await).await; } diff --git a/tests/nash/callbacks.rs b/tests/nash/callbacks.rs new file mode 100644 index 00000000..f96142b8 --- /dev/null +++ b/tests/nash/callbacks.rs @@ -0,0 +1,12 @@ +use crate::template::callbacks; +use super::client::init_ws as init; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn orderbook() { + callbacks::orderbook(&init().await).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn trades() { + callbacks::trades(&init().await).await; +} \ No newline at end of file diff --git a/tests/nash/client.rs b/tests/nash/client.rs new file mode 100644 index 00000000..bc3486ec --- /dev/null +++ b/tests/nash/client.rs @@ -0,0 +1,40 @@ +use dotenv::dotenv; +use openlimits::{ + exchange::nash::{Nash, NashParameters, NashWebsocket, NashCredentials}, + OpenLimits, +}; +use openlimits::prelude::*; +use openlimits_exchange::exchange::Environment; +use std::time::Duration; + +pub async fn init_ws() -> NashWebsocket { + NashWebsocket::new(NashParameters::production()) + .await + .expect("Failed to create Binance stream.") +} + +pub async fn init() -> Nash { + Nash::new(NashParameters::sandbox()) + .await + .expect("Failed to create Client") +} + +pub async fn init_signed() -> Nash { + dotenv().ok(); + + let parameters = NashParameters { + credentials: Some(NashCredentials { + api_key: std::env::var("NASH_API_KEY").expect("Couldn't get environment variable."), + api_secret: std::env::var("NASH_API_SECRET").expect("Couldn't get environment variable."), + }), + environment: Environment::Sandbox, + affiliate_code: None, + client_id: 1, + sign_states_loop_interval: None, + timeout: Duration::new(10, 0) + }; + + OpenLimits::instantiate(parameters) + .await + .expect("Failed to create Client") +} \ No newline at end of file diff --git a/tests/nash/market.rs b/tests/nash/market.rs index 15484203..561dfacd 100644 --- a/tests/nash/market.rs +++ b/tests/nash/market.rs @@ -1,110 +1,22 @@ -use nash_native_client::Environment; -use openlimits::{ - OpenLimits, - prelude::*, - model::GetHistoricTradesRequest, - model::Paginator, - model::{GetHistoricRatesRequest, GetPriceTickerRequest, Interval, OrderBookRequest}, - exchange::nash::Nash, - exchange::nash::NashCredentials, - exchange::nash::NashParameters, -}; - -use dotenv::dotenv; -use std::env; -use tokio::time::Duration; +use crate::template::market; +use super::client::init; #[tokio::test] async fn order_book() { - let exchange = init().await; - let req = OrderBookRequest { - market_pair: "eth_btc".to_string(), - }; - let _response = exchange - .order_book(&req) - .await - .expect("Couldn't get order book."); + market::order_book(&init().await).await; } #[tokio::test] async fn get_price_ticker() { - let exchange = init().await; - let req = GetPriceTickerRequest { - market_pair: "eth_btc".to_string(), - }; - let _response = exchange - .get_price_ticker(&req) - .await - .expect("Couldn't get price ticker."); + market::get_price_ticker(&init().await).await; } #[tokio::test] async fn get_historic_rates() { - let exchange = init().await; - let req = GetHistoricRatesRequest { - market_pair: "eth_btc".to_string(), - interval: Interval::OneHour, - paginator: None, - }; - let _response = exchange - .get_historic_rates(&req) - .await - .expect("Couldn't get historic rates."); + market::get_historic_rates(&init().await).await; } #[tokio::test] -async fn get_historic_trades() { - let exchange = init().await; - let req = GetHistoricTradesRequest { - market_pair: "eth_btc".to_string(), - paginator: Some(Paginator { - limit: Some(100), - ..Default::default() - }), - }; - let _response = exchange - .get_historic_trades(&req) - .await - .expect("Couldn't get historic trades."); -} - -#[tokio::test] -async fn retrieve_pairs() { - let exchange = init().await; - let _response = exchange - .refresh_market_info() - .await - .expect("Couldn't get pairs."); -} - -// #[tokio::test] -// async fn get_historic_rates_invalid_interval() { -// let mut openlimits-exchange = init().await; -// let req = GetHistoricRatesRequest { -// market_pair: "eth_btc".to_string(), -// interval: Interval::TwoHours, -// paginator: None, -// }; -// let resp = openlimits-exchange.get_historic_rates(&req).await; -// assert!(resp.is_err()); -// } - -async fn init() -> Nash { - dotenv().ok(); - - let parameters = NashParameters { - affiliate_code: None, - credentials: Some(NashCredentials { - secret: env::var("NASH_API_SECRET").expect("Couldn't get environment variable."), - session: env::var("NASH_API_KEY").expect("Couldn't get environment variable."), - }), - environment: Environment::Sandbox, - client_id: 1, - timeout: Duration::new(10, 0), - sign_states_loop_interval: None, - }; - - OpenLimits::instantiate(parameters) - .await - .expect("Failed to create Client") +async fn pair() { + market::pair(&init().await).await; } diff --git a/tests/nash/mod.rs b/tests/nash/mod.rs index b66643a7..7a509945 100644 --- a/tests/nash/mod.rs +++ b/tests/nash/mod.rs @@ -1,3 +1,5 @@ mod account; mod market; -mod websocket; +mod callbacks; +mod streams; +pub mod client; \ No newline at end of file diff --git a/tests/nash/streams.rs b/tests/nash/streams.rs new file mode 100644 index 00000000..88c96950 --- /dev/null +++ b/tests/nash/streams.rs @@ -0,0 +1,12 @@ +use crate::template::streams; +use super::client::init_ws as init; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn orderbook() { + streams::orderbook(&init().await).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn trades() { + streams::trades(&init().await).await; +} diff --git a/tests/old_nash_tests/account.rs b/tests/old_nash_tests/account.rs new file mode 100644 index 00000000..2b1deba2 --- /dev/null +++ b/tests/old_nash_tests/account.rs @@ -0,0 +1,279 @@ +use chrono::Duration; +use dotenv::dotenv; +use nash_native_client::Environment; +use openlimits::{ + OpenLimits, + prelude::*, + model::OpenMarketOrderRequest, + model::{ + CancelAllOrdersRequest, CancelOrderRequest, GetOrderHistoryRequest, OpenLimitOrderRequest, + TimeInForce, TradeHistoryRequest, + }, + exchange::nash::Nash, + exchange::nash::NashCredentials, + exchange::nash::NashParameters, +}; +use rust_decimal::prelude::{Decimal, FromStr}; +use std::env; +use std::time::Duration as NativeDuration; + +// FIXME: https://github.com/nash-io/openlimits/issues/157 +// #[tokio::test] +async fn _limit_buy() { + let exchange = init().await; + let req = OpenLimitOrderRequest { + client_order_id: None, + time_in_force: TimeInForce::GoodTillCancelled, + price: Decimal::from_str("100.46").expect("Couldn't parse string."), + size: Decimal::from_str("0.10000").expect("Couldn't parse string."), + market_pair: String::from("eth_usdc"), + post_only: false, + }; + let resp = exchange + .limit_buy(&req) + .await + .expect("Couldn't parse string."); + println!("{:?}", resp); +} + +// FIXME: https://github.com/nash-io/openlimits/issues/157 +// #[tokio::test] +async fn _limit_buy_ioc() { + let exchange = init().await; + let req = OpenLimitOrderRequest { + client_order_id: None, + time_in_force: TimeInForce::ImmediateOrCancelled, + price: Decimal::from_str("414.46").expect("Couldn't parse string."), + size: Decimal::from_str("0.10000").expect("Couldn't parse string."), + market_pair: String::from("eth_usdc"), + post_only: false, + }; + let resp = exchange + .limit_buy(&req) + .await + .expect("Couldn't request limit buy."); + println!("{:?}", resp); +} + +// FIXME: https://github.com/nash-io/openlimits/issues/157 +// #[tokio::test] +async fn _limit_buy_fok() { + let exchange = init().await; + let req = OpenLimitOrderRequest { + client_order_id: None, + time_in_force: TimeInForce::FillOrKill, + price: Decimal::from_str("414.46").expect("Couldn't parse string."), + size: Decimal::from_str("0.10000").expect("Couldn't parse string."), + market_pair: String::from("eth_usdc"), + post_only: false, + }; + let resp = exchange + .limit_buy(&req) + .await + .expect("Couldn't request limit buy."); + println!("{:?}", resp); +} + +#[tokio::test] +#[ignore] +async fn limit_buy_ggt() { + let exchange = init().await; + let req = OpenLimitOrderRequest { + client_order_id: None, + time_in_force: TimeInForce::GoodTillTime(Duration::hours(2)), + price: Decimal::from_str("414.46").expect("Couldn't parse string."), + size: Decimal::from_str("0.02000").expect("Couldn't parse string."), + market_pair: String::from("eth_usdc"), + post_only: false, + }; + let resp = exchange + .limit_buy(&req) + .await + .expect("Couldn't request limit buy."); + println!("{:?}", resp); +} + +// FIXME: https://github.com/nash-io/openlimits/issues/157 +// #[tokio::test] +async fn _market_buy() { + let exchange = init().await; + let req = OpenMarketOrderRequest { + client_order_id: None, + size: Decimal::from_str("10.0").expect("Couldn't parse string."), + market_pair: String::from("usdc_eth"), + }; + let resp = exchange + .market_sell(&req) + .await + .expect("Couldn't request market buy."); + println!("{:?}", resp); +} + +// FIXME: https://github.com/nash-io/openlimits/issues/157 +// #[tokio::test] +async fn _market_sell() { + let exchange = init().await; + let req = OpenMarketOrderRequest { + client_order_id: None, + size: Decimal::from_str("0.02").expect("Couldn't parse string."), + market_pair: String::from("eth_usdc"), + }; + let resp = exchange + .market_sell(&req) + .await + .expect("Couldn't request market buy."); + println!("{:?}", resp); +} + +// FIXME: https://github.com/nash-io/openlimits/issues/157 +// #[tokio::test] +async fn _limit_sell() { + let exchange = init().await; + let req = OpenLimitOrderRequest { + client_order_id: None, + time_in_force: TimeInForce::GoodTillTime(Duration::hours(2)), + price: Decimal::from_str("800.46").expect("Couldn't parse string."), + size: Decimal::from_str("0.02").expect("Couldn't parse string."), + market_pair: String::from("eth_usdc"), + post_only: false, + }; + let resp = exchange + .limit_sell(&req) + .await + .expect("Couldn't request limit sell."); + println!("{:?}", resp); +} + +// FIXME: https://github.com/nash-io/openlimits/issues/157 +// #[tokio::test] +async fn _cancel_order() { + let exchange = init().await; + let req = OpenLimitOrderRequest { + client_order_id: None, + time_in_force: TimeInForce::GoodTillCancelled, + price: Decimal::from_str("200.46").expect("Couldn't parse string."), + size: Decimal::from_str("0.0300").expect("Couldn't parse string."), + market_pair: String::from("eth_usdc"), + post_only: false, + }; + let order = exchange + .limit_buy(&req) + .await + .expect("Couldn't request limit buy."); + + let req = CancelOrderRequest { + id: order.id, + market_pair: Some(order.market_pair), + }; + let resp = exchange + .cancel_order(&req) + .await + .expect("Couldn't cancel order."); + println!("{:?}", resp); +} + +// FIXME: https://github.com/nash-io/openlimits/issues/157 +// #[tokio::test] +async fn _cancel_all_orders() { + let exchange = init().await; + let req = OpenLimitOrderRequest { + client_order_id: None, + time_in_force: TimeInForce::GoodTillCancelled, + price: Decimal::from_str("200.46").expect("Couldn't parse string."), + size: Decimal::from_str("0.10000").expect("Couldn't parse string."), + market_pair: String::from("eth_usdc"), + post_only: false, + }; + + exchange + .limit_sell(&req) + .await + .expect("Couldn't limit sell."); + + let req = CancelAllOrdersRequest { + market_pair: Some("eth_btc".to_string()), + }; + + let resp = exchange + .cancel_all_orders(&req) + .await + .expect("Couldn't cancel all orders."); + println!("{:?}", resp); +} + +#[tokio::test] +async fn get_order_history() { + let exchange = init().await; + let req = GetOrderHistoryRequest { + market_pair: Some(String::from("eth_btc")), + order_status: None, + paginator: None, + }; + + let resp = exchange + .get_order_history(&req) + .await + .expect("Couldn't get order history."); + println!("{:?}", resp); +} + +// #[tokio::test] +// async fn get_all_open_orders() { +// let mut openlimits-exchange = init().await; +// let req = OpenLimitOrderRequest { +// client_order_id: None, +// time_in_force: TimeInForce::GoodTillCancelled, +// price: Decimal::new(1, 1), +// size: Decimal::new(2, 2), +// market_pair: String::from("eth_btc"), +// }; +// openlimits-exchange.limit_sell(&req).await.expect("Couldn't limit sell."); + +// let resp = openlimits-exchange.get_all_open_orders().await.expect("Couldn't get all open orders."); +// println!("{:?}", resp); +// } + +#[tokio::test] +async fn get_account_balances() { + let exchange = init().await; + let resp = exchange + .get_account_balances(None) + .await + .expect("Couldn't get account balances."); + println!("{:?}", resp); +} + +#[tokio::test] +async fn get_trade_history() { + let exchange = init().await; + let req = TradeHistoryRequest { + market_pair: Some("eth_btc".to_string()), + ..Default::default() + }; + + let resp = exchange + .get_trade_history(&req) + .await + .expect("Couldn't get trade history."); + println!("{:?}", resp); +} + +async fn init() -> Nash { + dotenv().ok(); + + let parameters = NashParameters { + affiliate_code: None, + credentials: Some(NashCredentials { + secret: env::var("NASH_API_SECRET").expect("Couldn't get environment variable."), + session: env::var("NASH_API_KEY").expect("Couldn't get environment variable."), + }), + environment: Environment::Sandbox, + client_id: 1, + timeout: NativeDuration::new(10, 0), + sign_states_loop_interval: None, + }; + + OpenLimits::instantiate(parameters) + .await + .expect("Failed to create Client") +} diff --git a/tests/old_nash_tests/market.rs b/tests/old_nash_tests/market.rs new file mode 100644 index 00000000..15484203 --- /dev/null +++ b/tests/old_nash_tests/market.rs @@ -0,0 +1,110 @@ +use nash_native_client::Environment; +use openlimits::{ + OpenLimits, + prelude::*, + model::GetHistoricTradesRequest, + model::Paginator, + model::{GetHistoricRatesRequest, GetPriceTickerRequest, Interval, OrderBookRequest}, + exchange::nash::Nash, + exchange::nash::NashCredentials, + exchange::nash::NashParameters, +}; + +use dotenv::dotenv; +use std::env; +use tokio::time::Duration; + +#[tokio::test] +async fn order_book() { + let exchange = init().await; + let req = OrderBookRequest { + market_pair: "eth_btc".to_string(), + }; + let _response = exchange + .order_book(&req) + .await + .expect("Couldn't get order book."); +} + +#[tokio::test] +async fn get_price_ticker() { + let exchange = init().await; + let req = GetPriceTickerRequest { + market_pair: "eth_btc".to_string(), + }; + let _response = exchange + .get_price_ticker(&req) + .await + .expect("Couldn't get price ticker."); +} + +#[tokio::test] +async fn get_historic_rates() { + let exchange = init().await; + let req = GetHistoricRatesRequest { + market_pair: "eth_btc".to_string(), + interval: Interval::OneHour, + paginator: None, + }; + let _response = exchange + .get_historic_rates(&req) + .await + .expect("Couldn't get historic rates."); +} + +#[tokio::test] +async fn get_historic_trades() { + let exchange = init().await; + let req = GetHistoricTradesRequest { + market_pair: "eth_btc".to_string(), + paginator: Some(Paginator { + limit: Some(100), + ..Default::default() + }), + }; + let _response = exchange + .get_historic_trades(&req) + .await + .expect("Couldn't get historic trades."); +} + +#[tokio::test] +async fn retrieve_pairs() { + let exchange = init().await; + let _response = exchange + .refresh_market_info() + .await + .expect("Couldn't get pairs."); +} + +// #[tokio::test] +// async fn get_historic_rates_invalid_interval() { +// let mut openlimits-exchange = init().await; +// let req = GetHistoricRatesRequest { +// market_pair: "eth_btc".to_string(), +// interval: Interval::TwoHours, +// paginator: None, +// }; +// let resp = openlimits-exchange.get_historic_rates(&req).await; +// assert!(resp.is_err()); +// } + +async fn init() -> Nash { + dotenv().ok(); + + let parameters = NashParameters { + affiliate_code: None, + credentials: Some(NashCredentials { + secret: env::var("NASH_API_SECRET").expect("Couldn't get environment variable."), + session: env::var("NASH_API_KEY").expect("Couldn't get environment variable."), + }), + environment: Environment::Sandbox, + client_id: 1, + timeout: Duration::new(10, 0), + sign_states_loop_interval: None, + }; + + OpenLimits::instantiate(parameters) + .await + .expect("Failed to create Client") +} diff --git a/tests/old_nash_tests/mod.rs b/tests/old_nash_tests/mod.rs new file mode 100644 index 00000000..b66643a7 --- /dev/null +++ b/tests/old_nash_tests/mod.rs @@ -0,0 +1,3 @@ +mod account; +mod market; +mod websocket; diff --git a/tests/nash/websocket.rs b/tests/old_nash_tests/websocket.rs similarity index 100% rename from tests/nash/websocket.rs rename to tests/old_nash_tests/websocket.rs diff --git a/tests/template/account.rs b/tests/template/account.rs new file mode 100644 index 00000000..d7b007a3 --- /dev/null +++ b/tests/template/account.rs @@ -0,0 +1,243 @@ +use std::str::FromStr; + +use openlimits::{ + prelude::*, + model::{ + CancelAllOrdersRequest, CancelOrderRequest, GetOrderHistoryRequest, OpenLimitOrderRequest, + OpenMarketOrderRequest, TimeInForce, TradeHistoryRequest, GetPriceTickerRequest + }, +}; +use rust_decimal::prelude::*; +use openlimits::exchange::model::market_pair::MarketPair; +use openlimits::model::currency::Currency; + + +async fn get_current_price(exchange: &impl Exchange, market_pair: &MarketPair, multiplier: f32) -> Decimal { + let market_pair = market_pair.clone(); + let ticker = exchange + .get_price_ticker(&GetPriceTickerRequest { market_pair }) + .await + .expect("Failed to get price ticker."); + let price = ticker.price.unwrap_or(Decimal::from_f32(1.0).unwrap()); + price * Decimal::from_f32(multiplier).unwrap() +} + +pub async fn limit_buy(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let price = get_price(exchange, &market_pair).await; + let req = OpenLimitOrderRequest { + client_order_id: None, + price, + size: Decimal::from_f32(0.1).unwrap(), + market_pair, + post_only: false, + time_in_force: TimeInForce::GoodTillCancelled, + }; + let resp = exchange.limit_buy(&req).await.expect("Couldn't limit buy."); + println!("{:?}", resp); +} + +pub async fn limit_sell(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let price = get_price(exchange, &market_pair).await; + let req = OpenLimitOrderRequest { + client_order_id: None, + price, + post_only: false, + size: Decimal::new(1, 1), + market_pair, + time_in_force: TimeInForce::GoodTillCancelled, + }; + let resp = exchange + .limit_sell(&req) + .await + .expect("Couldn't limit sell."); + println!("{:?}", resp); +} + +pub async fn post_only(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let price = get_current_price(exchange, &market_pair, 1.5).await; + let req = OpenLimitOrderRequest { + client_order_id: None, + price, + size: Decimal::new(1, 1), + market_pair: market_pair.clone(), + post_only: true, + time_in_force: TimeInForce::GoodTillCancelled, + }; + let resp = exchange + .limit_sell(&req) + .await + .expect("Couldn't limit sell."); + println!("{:?}", resp); + + let price = get_current_price(exchange, &market_pair, 0.8).await; + let req = OpenLimitOrderRequest { + client_order_id: None, + price, + size: Decimal::new(1, 1), + market_pair, + post_only: true, + time_in_force: TimeInForce::GoodTillCancelled, + }; + let resp = exchange + .limit_buy(&req) + .await + .expect("Couldn't limit buy."); + + println!("{:?}", resp); +} + +pub async fn market_buy(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let req = OpenMarketOrderRequest { + client_order_id: None, + size: Decimal::from_str("0.1").unwrap(), + market_pair, + }; + let resp = exchange + .market_buy(&req) + .await + .expect("Couldn't market buy."); + println!("{:?}", resp); +} + +pub async fn market_sell(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let req = OpenMarketOrderRequest { + client_order_id: None, + size: Decimal::new(1, 1), + market_pair, + }; + let resp = exchange + .market_sell(&req) + .await + .expect("Couldn't market sell."); + println!("{:?}", resp); +} + +pub async fn cancel_order(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let price = get_current_price(exchange, &market_pair, 1.5).await; + let req = OpenLimitOrderRequest { + client_order_id: None, + price, + size: Decimal::from_str("1.0").unwrap(), + market_pair, + post_only: false, + time_in_force: TimeInForce::GoodTillCancelled, + }; + let order = exchange + .limit_sell(&req) + .await + .expect("Couldn't limit sell."); + + let req = CancelOrderRequest { + id: order.id, + market_pair: Some(order.market_pair), + }; + + let resp = exchange + .cancel_order(&req) + .await + .expect("Couldn't cancel order."); + println!("{:?}", resp); +} + +pub async fn cancel_all_orders(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let price = get_current_price(exchange, &market_pair, 1.5).await; + let req = OpenLimitOrderRequest { + client_order_id: None, + price, + size: Decimal::from_str("1.0").unwrap(), + market_pair, + post_only: false, + time_in_force: TimeInForce::GoodTillCancelled, + }; + exchange + .limit_sell(&req) + .await + .expect("Couldn't limit sell."); + + exchange + .limit_sell(&req) + .await + .expect("Couldn't limit sell."); + + let market_pair = Some(MarketPair(Currency::ETH, Currency::BTC)); + let req = CancelAllOrdersRequest { market_pair }; + + let resp = exchange + .cancel_all_orders(&req) + .await + .expect("Couldn't cancel all orders."); + println!("{:?}", resp); +} + +pub async fn get_order_history(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let market_pair = Some(market_pair); + let req = GetOrderHistoryRequest { + market_pair, + order_status: None, + paginator: None, + }; + + let resp = exchange + .get_order_history(&req) + .await + .expect("Couldn't get order history."); + println!("{:?}", resp); +} + +async fn get_price(exchange: &impl Exchange, market_pair: &MarketPair) -> Decimal { + let market_pair = market_pair.clone(); + let get_price_ticker_request = GetPriceTickerRequest { market_pair }; + let ticker = exchange.get_price_ticker(&get_price_ticker_request).await.expect("Couldn't get ticker."); + ticker.price.unwrap_or(Decimal::from_f32(1.0).unwrap()) +} + +pub async fn get_all_open_orders(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let price = get_price(exchange, &market_pair).await; + let req = OpenLimitOrderRequest { + client_order_id: None, + price, + size: Decimal::new(1, 1), + market_pair, + post_only: false, + time_in_force: TimeInForce::GoodTillCancelled, + }; + exchange + .limit_sell(&req) + .await + .expect("Couldn't limit sell."); + + let resp = exchange + .get_all_open_orders() + .await + .expect("Couldn't get all open orders."); + println!("{:?}", resp); +} + +pub async fn get_account_balances(exchange: &impl Exchange) { + let resp = exchange + .get_account_balances(None) + .await + .expect("Couldn't get acount balances."); + println!("{:?}", resp); +} + +pub async fn get_trade_history(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let market_pair = Some(market_pair); + let req = TradeHistoryRequest { market_pair, ..Default::default() }; + + let resp = exchange + .get_trade_history(&req) + .await + .expect("Couldn't get trade history."); + println!("{:?}", resp); +} \ No newline at end of file diff --git a/tests/template/callbacks.rs b/tests/template/callbacks.rs new file mode 100644 index 00000000..7fc8adde --- /dev/null +++ b/tests/template/callbacks.rs @@ -0,0 +1,33 @@ +use std::sync::mpsc::sync_channel; + +use openlimits::model::websocket::Subscription; +use openlimits::exchange::traits::stream::ExchangeWs; +use openlimits_exchange::model::market_pair::MarketPair; +use openlimits::model::currency::Currency; +use std::time::Duration; + +async fn test_subscription_callback(websocket: &impl ExchangeWs, sub: Subscription) { + let (tx, rx) = sync_channel(0); + + websocket + .subscribe(sub, move |m| { + m.as_ref().expect("Failed to get response."); + tx.send(()).expect("Failed to send sync message."); + }) + .await + .expect("Failed to subscribe."); + + rx.recv_timeout(Duration::new(3, 0)).ok(); +} + +pub async fn orderbook(ws: &impl ExchangeWs) { + let market = MarketPair(Currency::ETH, Currency::BTC); + let sub = Subscription::OrderBookUpdates(market); + test_subscription_callback(ws, sub).await; +} + +pub async fn trades(ws: &impl ExchangeWs) { + let market = MarketPair(Currency::ETH, Currency::BTC); + let sub = Subscription::Trades(market); + test_subscription_callback(ws, sub).await; +} diff --git a/tests/template/market.rs b/tests/template/market.rs new file mode 100644 index 00000000..7a2f1d3c --- /dev/null +++ b/tests/template/market.rs @@ -0,0 +1,46 @@ +use openlimits::{ + prelude::*, + model::{GetHistoricRatesRequest, GetPriceTickerRequest, Interval, OrderBookRequest}, +}; +use openlimits_exchange::model::market_pair::MarketPair; +use openlimits_exchange::model::currency::Currency; + +pub async fn order_book(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let req = OrderBookRequest { market_pair }; + let _response = exchange + .order_book(&req) + .await + .expect("Couldn't get order book."); +} + +pub async fn get_price_ticker(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let req = GetPriceTickerRequest { market_pair }; + let _response = exchange + .get_price_ticker(&req) + .await + .expect("Couldn't get price ticker."); +} + +pub async fn get_historic_rates(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let req = GetHistoricRatesRequest { + market_pair, + interval: Interval::OneHour, + paginator: None, + }; + let _response = exchange + .get_historic_rates(&req) + .await + .expect("Couldn't get historic rates."); +} + +pub async fn pair(exchange: &impl Exchange) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let response = exchange + .get_pair(&market_pair) + .await + .expect("Couldn't get pair."); + println!("{:#?}", response); +} \ No newline at end of file diff --git a/tests/template/mod.rs b/tests/template/mod.rs new file mode 100644 index 00000000..111c103f --- /dev/null +++ b/tests/template/mod.rs @@ -0,0 +1,4 @@ +pub mod account; +pub mod market; +pub mod streams; +pub mod callbacks; \ No newline at end of file diff --git a/tests/template/streams.rs b/tests/template/streams.rs new file mode 100644 index 00000000..3b29fb1a --- /dev/null +++ b/tests/template/streams.rs @@ -0,0 +1,32 @@ +use futures::stream::StreamExt; + +use openlimits::model::websocket::Subscription; +use openlimits::exchange::traits::stream::ExchangeWs; +use openlimits_exchange::model::market_pair::MarketPair; +use openlimits::exchange::model::currency::Currency; +use tokio::time::timeout; +use std::time::Duration; + +pub async fn orderbook(ws: &impl ExchangeWs) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let s = ws + .create_stream(&[Subscription::OrderBookUpdates(market_pair)]) + .await; + + let ob = s.expect("Couldn't create stream.").next().await; + + print!("{:?}", ob); +} + +pub async fn trades(ws: &impl ExchangeWs) { + let market_pair = MarketPair(Currency::ETH, Currency::BTC); + let mut s = ws + .create_stream(&[Subscription::Trades(market_pair)]) + .await + .expect("Couldn't create stream."); + + let trades = timeout(Duration::new(2, 0), s.next()).await; + print!("{:?}", trades); + let trades = timeout(Duration::new(2, 0), s.next()).await; + print!("{:?}", trades); +}