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 @@
-
-
-
+
+
+