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;
}
}
}