From cc25a405c69edcbfb6ec105b37e0873729ac2a9c Mon Sep 17 00:00:00 2001 From: Jkorf Date: Wed, 3 Jun 2026 10:13:34 +0200 Subject: [PATCH] Updated examples --- CryptoExchange.Net.sln | 6 - Examples/BlazorClient/BlazorClient.csproj | 52 +- Examples/BlazorClient/Pages/SpotClient.razor | 17 +- Examples/ConsoleClient/ConsoleClient.csproj | 22 +- .../Exchanges/BinanceExchange.cs | 78 --- .../ConsoleClient/Exchanges/BybitExchange.cs | 76 --- Examples/ConsoleClient/Exchanges/IExchange.cs | 22 - Examples/ConsoleClient/Models/OpenOrder.cs | 20 - Examples/ConsoleClient/Program.cs | 445 ++++++++++++------ Examples/README.md | 8 + Examples/SharedClients/Program.cs | 22 +- Examples/SharedClients/SharedClients.csproj | 6 +- 12 files changed, 366 insertions(+), 408 deletions(-) delete mode 100644 Examples/ConsoleClient/Exchanges/BinanceExchange.cs delete mode 100644 Examples/ConsoleClient/Exchanges/BybitExchange.cs delete mode 100644 Examples/ConsoleClient/Exchanges/IExchange.cs delete mode 100644 Examples/ConsoleClient/Models/OpenOrder.cs create mode 100644 Examples/README.md diff --git a/CryptoExchange.Net.sln b/CryptoExchange.Net.sln index 09864183..39abdc12 100644 --- a/CryptoExchange.Net.sln +++ b/CryptoExchange.Net.sln @@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,10 +41,6 @@ Global {988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU {988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU - {CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Examples/BlazorClient/BlazorClient.csproj b/Examples/BlazorClient/BlazorClient.csproj index b3b3a378..b889ef75 100644 --- a/Examples/BlazorClient/BlazorClient.csproj +++ b/Examples/BlazorClient/BlazorClient.csproj @@ -5,33 +5,33 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + diff --git a/Examples/BlazorClient/Pages/SpotClient.razor b/Examples/BlazorClient/Pages/SpotClient.razor index f3935d9f..f2e0c541 100644 --- a/Examples/BlazorClient/Pages/SpotClient.razor +++ b/Examples/BlazorClient/Pages/SpotClient.razor @@ -16,13 +16,26 @@ { var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "BTC"); var tasks = restClients.Select(x => x.GetSpotTickerAsync(new GetTickerRequest(symbol))); - await Task.WhenAll(tasks); - foreach (var ticker in tasks.Select(x => x.Result)) + await foreach (var ticker in ParallelEnumerateAsync(tasks)) { if (ticker.Success) _prices.Add(ticker.Exchange, ticker.Data.LastPrice); else Debug.WriteLine($"{ticker.Exchange} failed: {ticker.Error}"); + + await InvokeAsync(StateHasChanged); + } + } + + private async IAsyncEnumerable ParallelEnumerateAsync(IEnumerable> tasks) + { + var remaining = new List>(tasks); + + while (remaining.Count != 0) + { + var task = await Task.WhenAny(remaining).ConfigureAwait(false); + remaining.Remove(task); + yield return task.Result; } } diff --git a/Examples/ConsoleClient/ConsoleClient.csproj b/Examples/ConsoleClient/ConsoleClient.csproj index f36fe6d0..89e225b4 100644 --- a/Examples/ConsoleClient/ConsoleClient.csproj +++ b/Examples/ConsoleClient/ConsoleClient.csproj @@ -3,23 +3,13 @@ Exe net8.0 + enable + enable - - - - - - - - - - - - - - - - + + + + diff --git a/Examples/ConsoleClient/Exchanges/BinanceExchange.cs b/Examples/ConsoleClient/Exchanges/BinanceExchange.cs deleted file mode 100644 index 5f906481..00000000 --- a/Examples/ConsoleClient/Exchanges/BinanceExchange.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Binance.Net.Clients; -using Binance.Net.Interfaces.Clients; -using ConsoleClient.Models; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets; - -namespace ConsoleClient.Exchanges -{ - internal class BinanceExchange : IExchange - { - private IBinanceSocketClient _socketClient = new BinanceSocketClient(); - - public async Task CancelOrder(string symbol, string id) - { - using var client = new BinanceRestClient(); - var result = await client.SpotApi.Trading.CancelOrderAsync(symbol, long.Parse(id)); - return result.AsDataless(); - } - - public async Task> GetBalances() - { - using var client = new BinanceRestClient(); - var result = await client.SpotApi.Account.GetAccountInfoAsync(); - return result.Data.Balances.ToDictionary(b => b.Asset, b => b.Total); - } - - public async Task> GetOpenOrders() - { - using var client = new BinanceRestClient(); - var result = await client.SpotApi.Trading.GetOpenOrdersAsync(); - // Should check result success status here - return result.Data.Select(o => new OpenOrder - { - Symbol = o.Symbol, - OrderSide = o.Side.ToString(), - OrderStatus = o.Status.ToString(), - OrderTime = o.CreateTime, - OrderType = o.Type.ToString(), - Price = o.Price, - Quantity = o.Quantity, - QuantityFilled = o.QuantityFilled - }); - } - - public async Task GetPrice(string symbol) - { - using var client = new BinanceRestClient(); - var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol); - // Should check result success status here - return result.Data.Price; - } - - public async Task> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price) - { - using var client = new BinanceRestClient(); - var result = await client.SpotApi.Trading.PlaceOrderAsync( - symbol, - side.ToLower() == "buy" ? Binance.Net.Enums.OrderSide.Buy: Binance.Net.Enums.OrderSide.Sell, - type == "market" ? Binance.Net.Enums.SpotOrderType.Market : Binance.Net.Enums.SpotOrderType.Limit, - quantity, - price: price, - timeInForce: type == "market" ? null: Binance.Net.Enums.TimeInForce.GoodTillCanceled); - return result.As(result.Data?.Id.ToString()); - } - - public async Task SubscribePrice(string symbol, Action handler) - { - var sub = await _socketClient.SpotApi.ExchangeData.SubscribeToMiniTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice)); - return sub.Data; - } - } -} diff --git a/Examples/ConsoleClient/Exchanges/BybitExchange.cs b/Examples/ConsoleClient/Exchanges/BybitExchange.cs deleted file mode 100644 index 3a734d24..00000000 --- a/Examples/ConsoleClient/Exchanges/BybitExchange.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Bybit.Net.Clients; -using Bybit.Net.Interfaces.Clients; -using ConsoleClient.Models; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace ConsoleClient.Exchanges -{ - internal class BybitExchange : IExchange - { - private IBybitSocketClient _socketClient = new BybitSocketClient(); - - public async Task CancelOrder(string symbol, string id) - { - using var client = new BybitRestClient(); - var result = await client.V5Api.Trading.CancelOrderAsync(Bybit.Net.Enums.Category.Spot, symbol, id); - return result.AsDataless(); - } - - public async Task> GetBalances() - { - using var client = new BybitRestClient(); - var result = await client.V5Api.Account.GetBalancesAsync(Bybit.Net.Enums.AccountType.Spot); - return result.Data.List.First().Assets.ToDictionary(d => d.Asset, d => d.WalletBalance ?? 0); - } - - public async Task> GetOpenOrders() - { - using var client = new BybitRestClient(); - var order = await client.V5Api.Trading.GetOrdersAsync(Bybit.Net.Enums.Category.Spot); - return order.Data.List.Select(o => new OpenOrder - { - Symbol = o.Symbol, - OrderSide = o.Side.ToString(), - OrderStatus = o.Status.ToString(), - OrderTime = o.CreateTime, - OrderType = o.OrderType.ToString(), - Price = o.Price ?? 0, - Quantity = o.Quantity, - QuantityFilled = o.QuantityFilled ?? 0 - }); - } - - public async Task GetPrice(string symbol) - { - using var client = new BybitRestClient(); - var result = await client.V5Api.ExchangeData.GetSpotTickersAsync(symbol); - return result.Data.List.First().LastPrice; - } - - public async Task> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price) - { - using var client = new BybitRestClient(); - var result = await client.V5Api.Trading.PlaceOrderAsync( - Bybit.Net.Enums.Category.Spot, - symbol, - side.ToLower() == "buy" ? Bybit.Net.Enums.OrderSide.Buy : Bybit.Net.Enums.OrderSide.Sell, - type == "market" ? Bybit.Net.Enums.NewOrderType.Market : Bybit.Net.Enums.NewOrderType.Limit, - quantity, - price: price); - return result.As(result.Data?.OrderId.ToString()); - } - - public async Task SubscribePrice(string symbol, Action handler) - { - var sub = await _socketClient.V5SpotApi.SubscribeToTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice)); - return sub.Data; - } - } -} diff --git a/Examples/ConsoleClient/Exchanges/IExchange.cs b/Examples/ConsoleClient/Exchanges/IExchange.cs deleted file mode 100644 index d0b7ea1e..00000000 --- a/Examples/ConsoleClient/Exchanges/IExchange.cs +++ /dev/null @@ -1,22 +0,0 @@ -using ConsoleClient.Models; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace ConsoleClient.Exchanges -{ - public interface IExchange - { - Task GetPrice(string symbol); - Task> GetOpenOrders(); - Task> GetBalances(); - Task CancelOrder(string symbol, string id); - Task> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price ); - Task SubscribePrice(string symbol, Action handler); - } -} diff --git a/Examples/ConsoleClient/Models/OpenOrder.cs b/Examples/ConsoleClient/Models/OpenOrder.cs deleted file mode 100644 index f264e994..00000000 --- a/Examples/ConsoleClient/Models/OpenOrder.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace ConsoleClient.Models -{ - public class OpenOrder - { - public string Symbol { get; set; } - public decimal Price { get; set; } - public decimal Quantity { get; set; } - public decimal QuantityFilled { get; set; } - public string OrderType { get; set; } - public string OrderStatus { get; set; } - public string OrderSide { get; set; } - public DateTime OrderTime { get; set; } - } -} diff --git a/Examples/ConsoleClient/Program.cs b/Examples/ConsoleClient/Program.cs index 2c88cb00..83356aff 100644 --- a/Examples/ConsoleClient/Program.cs +++ b/Examples/ConsoleClient/Program.cs @@ -1,164 +1,303 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; using Binance.Net; using Binance.Net.Clients; using Bybit.Net; using Bybit.Net.Clients; -using ConsoleClient.Exchanges; -using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.SharedApis; -namespace ConsoleClient +var binanceRest = new BinanceRestClient(options => { - internal class Program + options.ApiCredentials = new BinanceCredentials("KEY", "SECRET"); +}); +var bybitRest = new BybitRestClient(options => +{ + options.ApiCredentials = new BybitCredentials("KEY", "SECRET"); +}); +var binanceSocket = new BinanceSocketClient(options => +{ + options.ApiCredentials = new BinanceCredentials("KEY", "SECRET"); +}); +var bybitSocket = new BybitSocketClient(options => +{ + options.ApiCredentials = new BybitCredentials("KEY", "SECRET"); +}); + +var exchanges = new Dictionary(StringComparer.OrdinalIgnoreCase) +{ + ["binance"] = new ExchangeClients( + "Binance", + binanceRest.SpotApi.SharedClient, + binanceRest.SpotApi.SharedClient, + binanceRest.SpotApi.SharedClient, + binanceRest.SpotApi.SharedClient, + binanceSocket.SpotApi.SharedClient), + ["bybit"] = new ExchangeClients( + "Bybit", + bybitRest.V5Api.SharedClient, + bybitRest.V5Api.SharedClient, + bybitRest.V5Api.SharedClient, + bybitRest.V5Api.SharedClient, + bybitSocket.V5SpotApi.SharedClient) +}; + +PrintHelp(); + +while (true) +{ + Console.Write("> "); + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + continue; + + var commandArgs = input.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var command = commandArgs[0].ToLowerInvariant(); + + try { - static Dictionary _exchanges = new Dictionary + switch (command) { - { "Binance", new Exchanges.BinanceExchange() }, - { "Bybit", new Exchanges.BybitExchange() } - }; - - static async Task Main(string[] args) - { - BinanceRestClient.SetDefaultOptions(options => - { - options.ApiCredentials = new BinanceCredentials("APIKEY", "APISECRET"); - }); - BybitRestClient.SetDefaultOptions(options => - { - options.ApiCredentials = new BybitCredentials("APIKEY", "APISECRET"); - }); - - while (true) - { - Console.WriteLine("> Available commands: PlaceOrder, GetBalances, GetOpenOrders, CancelOrder, GetPrice, SubscribePrice"); - var input = Console.ReadLine(); - - switch (input) - { - case "PlaceOrder": - await ProcessPlaceOrder(); - break; - case "GetBalances": - await ProcessGetBalances(); - break; - case "GetOpenOrders": - await ProcessGetOpenOrders(); - break; - case "CancelOrder": - await ProcessCancelOrder(); - break; - case "GetPrice": - await ProcessGetPrice(); - break; - case "SubscribePrice": - var sub = await ProcessSubscribePrice(); - Console.ReadLine(); - await sub.CloseAsync(); - break; - default: - Console.WriteLine("> Unknown command"); - break; - } - } - } - - static async Task ProcessPlaceOrder() - { - var exchange = GetInput("Exchange?"); - var symbol = GetInput("Symbol?"); - var side = GetInput("Buy/Sell?"); - var type = GetInput("Limit/Market?"); - var quantity = GetDecimalInput("Quantity?"); - decimal? price = null; - if (type != "Market") - price = GetDecimalInput("Price?"); - var result = await _exchanges[exchange].PlaceOrder(symbol, side, type, quantity, price); - if (result.Success) - Console.WriteLine("Order placed, ID: " + result.Data); - else - Console.WriteLine("Failed to place order: " + result.Error); - } - - static async Task ProcessGetBalances() - { - var exchange = GetInput("Exchange?"); - Console.WriteLine("Requesting balances..."); - var balances = await _exchanges[exchange].GetBalances(); - foreach (var balance in balances.Where(b => b.Value != 0)) - Console.WriteLine($"> {balance.Key}: {balance.Value}"); - } - - static async Task ProcessGetOpenOrders() - { - var exchange = GetInput("Exchange?"); - Console.WriteLine("Requesting open orders..."); - var orders = await _exchanges[exchange].GetOpenOrders(); - if (!orders.Any()) - Console.WriteLine("No open orders found"); - else - { - foreach(var openOrder in orders) - Console.WriteLine($"> {openOrder.OrderTime} - {openOrder.Symbol} {openOrder.OrderType} {openOrder.OrderSide} {openOrder.QuantityFilled}/{openOrder.Quantity} @ {openOrder.Price}"); - } - } - - static async Task ProcessCancelOrder() - { - var exchange = GetInput("Exchange?"); - var symbol = GetInput("Symbol?"); - var id = GetInput("Order id?"); - var result = await _exchanges[exchange].CancelOrder(symbol, id); - if (result) - Console.WriteLine("Order canceled"); - else - Console.WriteLine("Order cancel failed: " + result.Error); - } - - static async Task ProcessGetPrice() - { - var exchange = GetInput("Exchange?"); - var symbol = GetInput("Symbol?"); - Console.WriteLine("Requesting price..."); - var price = await _exchanges[exchange].GetPrice(symbol); - Console.WriteLine($"> {exchange} price for {symbol}: {price}"); - } - - static async Task ProcessSubscribePrice() - { - var exchange = GetInput("Exchange?"); - var symbol = GetInput("Symbol?"); - Console.WriteLine("Subscribing price..."); - return await _exchanges[exchange].SubscribePrice(symbol, price => HandlePriceUpdate(exchange,symbol, price)); - } - - static void HandlePriceUpdate(string exchange, string symbol, decimal price) - { - Console.Clear(); - Console.WriteLine($"> {exchange} price for {symbol}: {price}"); - Console.WriteLine("Press enter to stop live price updates"); - } - - static decimal GetDecimalInput(string question) - { - while (true) - { - Console.WriteLine("> " + question); - var response = Console.ReadLine(); - if (decimal.TryParse(response, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var answer)) - return answer; - else - Console.WriteLine("Invalid decimal value"); - } - } - - static string GetInput(string question) - { - Console.WriteLine("> " + question); - return Console.ReadLine(); + case "help": + PrintHelp(); + break; + case "exit": + case "quit": + return; + case "prices": + await PrintPricesAsync(commandArgs); + break; + case "stream": + await StreamTickerAsync(commandArgs); + break; + case "orderbook": + await PrintOrderBookAsync(commandArgs); + break; + case "balances": + await PrintBalancesAsync(commandArgs); + break; + case "open-orders": + await PrintOpenOrdersAsync(commandArgs); + break; + case "place-limit": + await PlaceLimitOrderAsync(commandArgs); + break; + case "cancel-order": + await CancelOrderAsync(commandArgs); + break; + default: + Console.WriteLine("Unknown command. Type `help` for examples."); + break; } } + catch (Exception ex) + { + Console.WriteLine($"Command failed: {ex.Message}"); + } } + +async Task PrintPricesAsync(string[] args) +{ + var symbol = GetSymbol(args, 1); + var tasks = exchanges.Values.Select(async exchange => + { + var result = await exchange.TickerRest.GetSpotTickerAsync(new GetTickerRequest(symbol)); + if (!result.Success) + { + Console.WriteLine($"{exchange.Name,-8} error: {result.Error}"); + return; + } + + Console.WriteLine($"{exchange.Name,-8} {result.Data.Symbol,-12} last={result.Data.LastPrice}"); + }); + + await Task.WhenAll(tasks); +} + +async Task StreamTickerAsync(string[] args) +{ + var exchange = GetExchange(args, 1); + var symbol = GetSymbol(args, 2); + + using var cts = new CancellationTokenSource(); + var result = await exchange.TickerSocket.SubscribeToTickerUpdatesAsync( + new SubscribeTickerRequest(symbol), + update => Console.WriteLine($"{exchange.Name,-8} {update.Data.Symbol,-12} last={update.Data.LastPrice}"), + cts.Token); + + if (!result.Success) + { + Console.WriteLine($"Subscription failed: {result.Error}"); + return; + } + + Console.WriteLine("Streaming ticker updates. Press Enter to stop."); + Console.ReadLine(); + cts.Cancel(); + await result.Data.CloseAsync(); +} + +async Task PrintOrderBookAsync(string[] args) +{ + var exchange = GetExchange(args, 1); + var symbol = GetSymbol(args, 2); + + var result = await exchange.OrderBookRest.GetOrderBookAsync(new GetOrderBookRequest(symbol, limit: 5)); + if (!result.Success) + { + Console.WriteLine($"Order book request failed: {result.Error}"); + return; + } + + Console.WriteLine($"{exchange.Name} {symbol.BaseAsset}/{symbol.QuoteAsset} top of book"); + for (var i = 0; i < Math.Min(5, Math.Min(result.Data.Bids.Length, result.Data.Asks.Length)); i++) + Console.WriteLine($"bid {result.Data.Bids[i].Price,15} | ask {result.Data.Asks[i].Price,15}"); +} + +async Task PrintBalancesAsync(string[] args) +{ + var exchange = GetExchange(args, 1); + var result = await exchange.BalanceRest.GetBalancesAsync(new GetBalancesRequest(TradingMode.Spot)); + if (!result.Success) + { + Console.WriteLine($"Balances request failed: {result.Error}"); + Console.WriteLine("Set API credentials in environment variables before calling private endpoints."); + return; + } + + foreach (var balance in result.Data.Where(x => x.Total != 0).OrderBy(x => x.Asset)) + Console.WriteLine($"{balance.Asset,-8} available={balance.Available} total={balance.Total}"); +} + +async Task PrintOpenOrdersAsync(string[] args) +{ + var exchange = GetExchange(args, 1); + var symbol = GetSymbol(args, 2); + + var result = await exchange.OrderRest.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol)); + if (!result.Success) + { + Console.WriteLine($"Open orders request failed: {result.Error}"); + return; + } + + if (result.Data.Length == 0) + { + Console.WriteLine("No open orders."); + return; + } + + foreach (var order in result.Data) + Console.WriteLine($"{order.OrderId,-24} {order.Side,-4} {order.OrderType,-6} {order.Status,-12} {order.OrderQuantity?.QuantityInBaseAsset} @ {order.OrderPrice}"); +} + +async Task PlaceLimitOrderAsync(string[] args) +{ + var exchange = GetExchange(args, 1); + var symbol = GetSymbol(args, 2); + var side = ParseSide(args.ElementAtOrDefault(4)); + var quantity = ParseDecimal(args.ElementAtOrDefault(5), "quantity"); + var price = ParseDecimal(args.ElementAtOrDefault(6), "price"); + + var request = new PlaceSpotOrderRequest( + symbol, + side, + SharedOrderType.Limit, + SharedQuantity.Base(quantity), + price, + SharedTimeInForce.GoodTillCanceled, + exchange.OrderRest.GenerateClientOrderId()); + + var validationError = exchange.OrderRest.PlaceSpotOrderOptions.ValidateRequest( + exchange.Name, + request, + TradingMode.Spot, + exchange.OrderRest.SupportedTradingModes, + exchange.OrderRest.SpotSupportedOrderTypes, + exchange.OrderRest.SpotSupportedTimeInForce, + exchange.OrderRest.SpotSupportedOrderQuantity); + if (validationError != null) + { + Console.WriteLine($"Order request is not valid for {exchange.Name}: {validationError}"); + return; + } + + var result = await exchange.OrderRest.PlaceSpotOrderAsync(request); + Console.WriteLine(result.Success + ? $"Order placed. Id: {result.Data.Id}" + : $"Order failed: {result.Error}"); +} + +async Task CancelOrderAsync(string[] args) +{ + var exchange = GetExchange(args, 1); + var symbol = GetSymbol(args, 2); + var orderId = args.ElementAtOrDefault(4); + if (string.IsNullOrWhiteSpace(orderId)) + throw new ArgumentException("Missing order id. Example: cancel-order binance BTC USDT 123456"); + + var result = await exchange.OrderRest.CancelSpotOrderAsync(new CancelOrderRequest(symbol, orderId)); + Console.WriteLine(result.Success + ? $"Order canceled. Id: {result.Data.Id}" + : $"Cancel failed: {result.Error}"); +} + +ExchangeClients GetExchange(string[] args, int index) +{ + var name = args.ElementAtOrDefault(index); + if (name == null || !exchanges.TryGetValue(name, out var exchange)) + throw new ArgumentException($"Unknown exchange `{name}`. Use one of: {string.Join(", ", exchanges.Keys)}"); + + return exchange; +} + +SharedSymbol GetSymbol(string[] args, int index) +{ + var baseAsset = args.ElementAtOrDefault(index) ?? "BTC"; + var quoteAsset = args.ElementAtOrDefault(index + 1) ?? "USDT"; + return new SharedSymbol(TradingMode.Spot, baseAsset.ToUpperInvariant(), quoteAsset.ToUpperInvariant()); +} + +static decimal ParseDecimal(string? value, string name) +{ + if (!decimal.TryParse(value, out var result)) + throw new ArgumentException($"Invalid {name} value `{value}`."); + + return result; +} + +static SharedOrderSide ParseSide(string? value) +{ + return value?.ToLowerInvariant() switch + { + "buy" => SharedOrderSide.Buy, + "sell" => SharedOrderSide.Sell, + _ => throw new ArgumentException("Side should be `buy` or `sell`.") + }; +} + +static void PrintHelp() +{ + Console.WriteLine("CryptoExchange.Net SharedApis console example"); + Console.WriteLine(); + Console.WriteLine("Public market data:"); + Console.WriteLine(" prices [base] [quote] e.g. prices BTC USDT"); + Console.WriteLine(" stream [exchange] [base] [quote] e.g. stream binance ETH USDT"); + Console.WriteLine(" orderbook [exchange] [base] [quote] e.g. orderbook bybit BTC USDT"); + Console.WriteLine(); + Console.WriteLine("Private endpoints, credentials required:"); + Console.WriteLine(" balances [exchange]"); + Console.WriteLine(" open-orders [exchange] [base] [quote]"); + Console.WriteLine(" place-limit [exchange] [base] [quote] [buy|sell] [quantity] [price] --live"); + Console.WriteLine(" cancel-order [exchange] [base] [quote] [order-id]"); + Console.WriteLine(); + Console.WriteLine("Other:"); + Console.WriteLine(" help"); + Console.WriteLine(" exit"); + Console.WriteLine(); +} + +internal record ExchangeClients( + string Name, + ISpotTickerRestClient TickerRest, + IOrderBookRestClient OrderBookRest, + IBalanceRestClient BalanceRest, + ISpotOrderRestClient OrderRest, + ITickerSocketClient TickerSocket); diff --git a/Examples/README.md b/Examples/README.md new file mode 100644 index 00000000..8f9d228e --- /dev/null +++ b/Examples/README.md @@ -0,0 +1,8 @@ +# CryptoExchange.Net Examples + +| Path | Topic | +| --- | --- | --- | +| `SharedClients` | Small runnable exchange agnostic quickstart for REST tickers and websocket ticker updates | +| `ConsoleClient` | Runnable CLI for some common operations | +| `ai-friendly` | Compact snippets that are easy to paste into small apps or coding-assistant prompts | +| `BlazorClient` | Server-side Blazor demo showing DI, live data, order books, and trackers | diff --git a/Examples/SharedClients/Program.cs b/Examples/SharedClients/Program.cs index d48ee502..053361b8 100644 --- a/Examples/SharedClients/Program.cs +++ b/Examples/SharedClients/Program.cs @@ -1,5 +1,6 @@ -using Binance.Net.Clients; +using Binance.Net.Clients; using BitMart.Net.Clients; +using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.SharedApis; using OKX.Net.Clients; @@ -20,11 +21,15 @@ Console.WriteLine(); Console.WriteLine("Press enter to start websocket"); Console.ReadLine(); -await SubscribeTickerUpdatesAsync(binanceSpotSocketClient, symbol); -await SubscribeTickerUpdatesAsync(okxSpotSocketClient, symbol); -await SubscribeTickerUpdatesAsync(bitmartSpotSocketClient, symbol); +var subscriptions = new List(); +await SubscribeTickerUpdatesAsync(binanceSpotSocketClient, symbol, subscriptions); +await SubscribeTickerUpdatesAsync(okxSpotSocketClient, symbol, subscriptions); +await SubscribeTickerUpdatesAsync(bitmartSpotSocketClient, symbol, subscriptions); +Console.WriteLine("Press enter to stop websocket updates"); Console.ReadLine(); +foreach (var subscription in subscriptions) + await subscription.CloseAsync(); async Task GetLastTradePriceAsync(ISpotTickerRestClient client, SharedSymbol symbol) { @@ -38,7 +43,7 @@ async Task GetLastTradePriceAsync(ISpotTickerRestClient client, SharedSymbol sym Console.WriteLine($"{client.Exchange} {result.Data.Symbol}: {result.Data.LastPrice}"); } -async Task SubscribeTickerUpdatesAsync(ITickerSocketClient client, SharedSymbol symbol) +async Task SubscribeTickerUpdatesAsync(ITickerSocketClient client, SharedSymbol symbol, ICollection subscriptions) { var result = await client.SubscribeToTickerUpdatesAsync(new SubscribeTickerRequest(symbol), update => { @@ -46,5 +51,10 @@ async Task SubscribeTickerUpdatesAsync(ITickerSocketClient client, SharedSymbol }); if (!result.Success) + { Console.WriteLine($"Failed to subscribe ticker: {result.Error}"); -} \ No newline at end of file + return; + } + + subscriptions.Add(result.Data); +} diff --git a/Examples/SharedClients/SharedClients.csproj b/Examples/SharedClients/SharedClients.csproj index c88abe1b..9151c061 100644 --- a/Examples/SharedClients/SharedClients.csproj +++ b/Examples/SharedClients/SharedClients.csproj @@ -8,9 +8,9 @@ - - - + + +