using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.SharedApis; using System; using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net { /// /// General helpers functions /// public static class ExchangeHelpers { private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789"; private const string _allowedRandomHexChars = "0123456789ABCDEF"; private static readonly Dictionary _monthSymbols = new Dictionary() { { 1, "F" }, { 2, "G" }, { 3, "H" }, { 4, "J" }, { 5, "K" }, { 6, "M" }, { 7, "N" }, { 8, "Q" }, { 9, "U" }, { 10, "V" }, { 11, "X" }, { 12, "Z" }, }; /// /// The last used id, use NextId() to get the next id and up this /// private static int _lastId; /// /// Clamp a value between a min and max /// /// /// /// /// public static decimal ClampValue(decimal min, decimal max, decimal value) { value = Math.Min(max, value); value = Math.Max(min, value); return value; } /// /// Adjust a value to be between the min and max parameters and rounded to the closest step. /// /// The min value /// The max value /// The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54 /// How to round /// The input value /// public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value) { if(step == 0) throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step)); value = Math.Min(max, value); value = Math.Max(min, value); if (step == null) return value; var offset = value % step.Value; if(roundingType == RoundingType.Down) { value -= offset; } else if(roundingType == RoundingType.Up) { if (offset != 0) value += (step.Value - offset); } else { if (offset < step / 2) value -= offset; else value += (step.Value - offset); } return value.Normalize(); } /// /// Adjust a value to be between the min and max parameters and rounded to the closest precision. /// /// The min value /// The max value /// The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542 /// How to round /// The input value /// public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value) { value = Math.Min(max, value); value = Math.Max(min, value); if (precision == null) return value; return RoundToSignificantDigits(value, precision.Value, roundingType); } /// /// Apply the provided rules to the value /// /// Value to be adjusted /// Max decimal places /// The value step for increase/decrease value /// public static decimal ApplyRules( decimal value, int? decimals = null, decimal? valueStep = null) { if (valueStep.HasValue) { var offset = value % valueStep.Value; if (offset != 0) { if (offset < valueStep.Value / 2) value -= offset; else value += (valueStep.Value - offset); } } if (decimals.HasValue) value = Math.Round(value, decimals.Value); return value; } /// /// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12 /// /// The value to round /// The total amount of digits (NOT decimal places) to round to /// How to round /// public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType) { var val = (double)value; if (value == 0) return 0; double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1); if(roundingType == RoundingType.Closest) return (decimal)(scale * Math.Round(val / scale, digits)); else return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits)); } /// /// Rounds a value down /// public static decimal RoundDown(decimal i, double decimalPlaces) { var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces)); return Math.Floor(i * power) / power; } /// /// Rounds a value up /// public static decimal RoundUp(decimal i, double decimalPlaces) { var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces)); return Math.Ceiling(i * power) / power; } /// /// Strips any trailing zero's of a decimal value, useful when converting the value to string. /// /// /// public static decimal Normalize(this decimal value) { return value / 1.000000000000000000000000000000000m; } /// /// Generate a new unique id. The id is statically stored so it is guaranteed to be unique /// /// public static int NextId() => Interlocked.Increment(ref _lastId); /// /// Return the last unique id that was generated /// /// public static int LastId() => _lastId; /// /// Generate a random string of specified length /// /// Length of the random string /// public static string RandomString(int length) { var randomChars = new char[length]; #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER for (int i = 0; i < length; i++) randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)]; #else var random = new Random(); for (int i = 0; i < length; i++) randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)]; #endif return new string(randomChars); } /// /// Generate a random string of specified length /// /// Length of the random string /// public static string RandomHexString(int length) { #if NET9_0_OR_GREATER return "0x" + RandomNumberGenerator.GetHexString(length * 2); #else var randomChars = new char[length * 2]; var random = new Random(); for (int i = 0; i < length * 2; i++) randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)]; return "0x" + new string(randomChars); #endif } /// /// Generate a long value /// /// Max character length /// public static long RandomLong(int maxLength) { #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER var value = RandomNumberGenerator.GetInt32(0, int.MaxValue); #else var random = new Random(); var value = random.Next(0, int.MaxValue); #endif var val = value.ToString(); if (val.Length > maxLength) return int.Parse(val.Substring(0, maxLength)); else return value; } /// /// Generate a random string of specified length /// /// The initial string /// Total length of the resulting string /// public static string AppendRandomString(string source, int totalLength) { if (totalLength < source.Length) throw new ArgumentException("Total length smaller than source string length", nameof(totalLength)); if (totalLength == source.Length) return source; return source + RandomString(totalLength - source.Length); } /// /// Get the month representation for futures symbol based on the delivery month /// /// Delivery time /// public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month]; /// /// Execute multiple requests to retrieve multiple pages of the result set /// /// Type of the client /// Type of the request /// The func to execute with each request /// The request parameters /// Cancellation token /// public static async IAsyncEnumerable> ExecutePages(Func>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default) { var result = new List(); ExchangeWebResult batch; INextPageToken? nextPageToken = null; while (true) { batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false); yield return batch; if (!batch || ct.IsCancellationRequested) break; result.AddRange(batch.Data); nextPageToken = batch.NextPageToken; if (nextPageToken == null) break; } } /// /// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price /// /// The symbol as retrieved from the exchange /// Quantity to trade /// Price to trade at /// Quantity adjusted to match all trading rules /// Price adjusted to match all trading rules public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice) { adjustedPrice = price; adjustedQuantity = quantity; var minNotionalAdjust = false; if (price != null) { adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value); adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice; adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice; if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue) { adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value; minNotionalAdjust = true; } } adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity); adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity; } /// /// Queue updates received from a websocket subscriptions and process them async /// /// The queued update type /// The subscribe call /// The async update handler /// The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with fullMode /// What should happen if the queue contains maxQueuedItems pending updates. If no max is set this setting is ignored public static async Task> ProcessQueuedAsync( Func>, Task>> subscribeCall, Func, Task> asyncHandler, int? maxQueuedItems = null, QueueFullBehavior? fullBehavior = null) { var processor = new ProcessQueue>(asyncHandler, maxQueuedItems, fullBehavior); await processor.StartAsync().ConfigureAwait(false); var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false); if (!result) { await processor.StopAsync().ConfigureAwait(false); return result; } processor.Exception += result.Data._subscription.InvokeExceptionHandler; result.Data.SubscriptionStatusChanged += (upd) => { if (upd == CryptoExchange.Net.Objects.SubscriptionStatus.Closed) _ = processor.StopAsync(true); }; return result; } /// /// Queue updates and process them async /// /// The queued update type /// The subscribe call /// The async update handler /// The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with fullMode /// What should happen if the queue contains maxQueuedItems pending updates. If no max is set this setting is ignored /// Cancellation token to stop the processing public static async Task ProcessQueuedAsync( Func, Task> subscribeCall, Func asyncHandler, CancellationToken ct, int? maxQueuedItems = null, QueueFullBehavior? fullBehavior = null) { var processor = new ProcessQueue(asyncHandler, maxQueuedItems, fullBehavior); await processor.StartAsync().ConfigureAwait(false); ct.Register(async () => { await processor.StopAsync().ConfigureAwait(false); }); await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false); } /// /// Queue updates received from a websocket subscriptions and process them async /// /// The type of the queued item /// The type of the item to pass to the processor /// The subscribe call /// The mapper function to go from TEventType to TOutputType /// The async update handler /// The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with fullMode /// What should happen if the queue contains maxQueuedItems pending updates. If no max is set this setting is ignored public static async Task> ProcessQueuedAsync( Func>, Task>> subscribeCall, Func, DataEvent> mapper, Func, Task> asyncHandler, int? maxQueuedItems = null, QueueFullBehavior? fullBehavior = null ) { var processor = new ProcessQueue>((update) => { return asyncHandler.Invoke(mapper.Invoke(update)); }, maxQueuedItems, fullBehavior); await processor.StartAsync().ConfigureAwait(false); var result = await subscribeCall(processor).ConfigureAwait(false); if (!result) { await processor.StopAsync().ConfigureAwait(false); return result; } processor.Exception += result.Data._subscription.InvokeExceptionHandler; result.Data.SubscriptionStatusChanged += (upd) => { if (upd == SubscriptionStatus.Closed) _ = processor.StopAsync(true); }; return result; } /// /// Parse a decimal value from a string /// public static decimal? ParseDecimal(string? value) { // Value is null or empty is the most common case to return null so check before trying to parse if (string.IsNullOrEmpty(value)) return null; // Try parse, only fails for these reasons: // 1. string is null or empty // 2. value is larger or smaller than decimal max/min // 3. unparsable format if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue)) return decValue; // Check for values which should be parsed to null if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase) || string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase)) { return null; } // Infinity value should be parsed to min/max value if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase)) return decimal.MaxValue; else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase)) return decimal.MinValue; if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue)) { // Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly. // assume overflow if (overflowValue < 0) return decimal.MinValue; else return decimal.MaxValue; } // Unknown decimal format, return null return null; } } }