Add synthApi (#27)

* Add synthApi

* Put confidence for Synth proba

* Update the code

* Update readme

* Fix bootstraping

* fix github build

* Update the endpoints for scenario

* Add scenario and update backtest modal

* Update bot modal

* Update interfaces for synth

* add synth to backtest

* Add Kelly criterion and better signal

* Update signal confidence

* update doc

* save leaderboard and prediction

* Update nswag to generate ApiClient in the correct path

* Unify the trading modal

* Save miner and prediction

* Update messaging and block new signal until position not close when flipping off

* Rename strategies to indicators

* Update doc

* Update chart + add signal name

* Fix signal direction

* Update docker webui

* remove crypto npm

* Clean
This commit is contained in:
Oda
2025-07-03 00:13:42 +07:00
committed by GitHub
parent 453806356d
commit a547c4a040
103 changed files with 9916 additions and 810 deletions

View File

@@ -0,0 +1,324 @@
using System.Text.Json;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Synth.Models;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Synth;
/// <summary>
/// Client for communicating with the Synth API
/// </summary>
public class SynthApiClient : ISynthApiClient, IDisposable
{
private readonly HttpClient _httpClient;
private readonly ILogger<SynthApiClient> _logger;
private readonly JsonSerializerOptions _jsonOptions;
// Private configuration - should come from app settings or environment variables
private readonly string _apiKey;
private readonly string _baseUrl;
public SynthApiClient(HttpClient httpClient, ILogger<SynthApiClient> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// TODO: These should come from IConfiguration or environment variables
_apiKey = Environment.GetEnvironmentVariable("SYNTH_API_KEY") ??
"bfd2a078b412452af2e01ca74b2a7045d4ae411a85943342";
_baseUrl = Environment.GetEnvironmentVariable("SYNTH_BASE_URL") ?? "https://api.synthdata.co";
// Configure HttpClient once
ConfigureHttpClient();
// Configure JSON options
_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
/// <summary>
/// Configures the HTTP client with API settings
/// </summary>
private void ConfigureHttpClient()
{
// Validate API configuration
if (string.IsNullOrEmpty(_apiKey) || string.IsNullOrEmpty(_baseUrl))
{
throw new InvalidOperationException(
"Synth API configuration is missing. Please set SYNTH_API_KEY and SYNTH_BASE_URL environment variables.");
}
// Set base address and authorization
_httpClient.BaseAddress = new Uri(_baseUrl);
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Apikey {_apiKey}");
_httpClient.Timeout = TimeSpan.FromSeconds(30);
}
/// <summary>
/// Fetches the current leaderboard from Synth API
/// </summary>
public async Task<List<MinerInfo>> GetLeaderboardAsync(SynthConfiguration config)
{
try
{
_logger.LogInformation("🔍 **Synth API** - Fetching leaderboard");
var response = await _httpClient.GetAsync("/leaderboard/latest");
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
$"Synth API leaderboard request failed: {response.StatusCode} - {response.ReasonPhrase}");
return new List<MinerInfo>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var miners = JsonSerializer.Deserialize<List<MinerInfo>>(jsonContent, _jsonOptions);
_logger.LogInformation($"📊 **Synth API** - Retrieved {miners?.Count ?? 0} miners from leaderboard");
return miners ?? new List<MinerInfo>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error while fetching Synth leaderboard");
return new List<MinerInfo>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, "Timeout while fetching Synth leaderboard");
return new List<MinerInfo>();
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON deserialization error while parsing Synth leaderboard");
return new List<MinerInfo>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while fetching Synth leaderboard");
return new List<MinerInfo>();
}
}
/// <summary>
/// Fetches historical leaderboard data from Synth API for a specific time range
/// </summary>
public async Task<List<MinerInfo>> GetHistoricalLeaderboardAsync(DateTime startTime, DateTime endTime, SynthConfiguration config)
{
try
{
// Format dates to ISO 8601 format as required by the API
var startTimeStr = Uri.EscapeDataString(startTime.ToString("yyyy-MM-ddTHH:mm:ssZ"));
var endTimeStr = Uri.EscapeDataString(endTime.ToString("yyyy-MM-ddTHH:mm:ssZ"));
var url = $"/leaderboard/historical?start_time={startTimeStr}&end_time={endTimeStr}";
_logger.LogInformation($"🔍 **Synth API** - Fetching historical leaderboard from {startTime:yyyy-MM-dd HH:mm} to {endTime:yyyy-MM-dd HH:mm}");
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
$"Synth API historical leaderboard request failed: {response.StatusCode} - {response.ReasonPhrase}");
return new List<MinerInfo>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var miners = JsonSerializer.Deserialize<List<MinerInfo>>(jsonContent, _jsonOptions);
_logger.LogInformation($"📊 **Synth API** - Retrieved {miners?.Count ?? 0} miners from historical leaderboard");
return miners ?? new List<MinerInfo>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error while fetching Synth historical leaderboard");
return new List<MinerInfo>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, "Timeout while fetching Synth historical leaderboard");
return new List<MinerInfo>();
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON deserialization error while parsing Synth historical leaderboard");
return new List<MinerInfo>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while fetching Synth historical leaderboard");
return new List<MinerInfo>();
}
}
/// <summary>
/// Fetches latest predictions from specified miners
/// </summary>
public async Task<List<MinerPrediction>> GetMinerPredictionsAsync(
List<int> minerUids,
string asset,
int timeIncrement,
int timeLength,
SynthConfiguration config)
{
if (minerUids == null || !minerUids.Any())
{
_logger.LogWarning("No miner UIDs provided for prediction request");
return new List<MinerPrediction>();
}
try
{
// Build URL with proper array formatting for miner parameter
var queryParams = new List<string>
{
$"asset={Uri.EscapeDataString(asset)}",
$"time_increment={timeIncrement}",
$"time_length={timeLength}"
};
// Add each miner UID as a separate parameter (standard array query parameter format)
foreach (var minerUid in minerUids)
{
queryParams.Add($"miner={minerUid}");
}
var url = $"/prediction/latest?{string.Join("&", queryParams)}";
_logger.LogInformation(
$"🔮 **Synth API** - Fetching predictions for {minerUids.Count} miners, asset: {asset}, time: {timeLength}s");
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
$"Synth API predictions request failed: {response.StatusCode} - {response.ReasonPhrase}");
return new List<MinerPrediction>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var predictions = JsonSerializer.Deserialize<List<MinerPrediction>>(jsonContent, _jsonOptions);
var totalPaths = predictions?.Sum(p => p.NumSimulations) ?? 0;
_logger.LogInformation(
$"📈 **Synth API** - Retrieved {predictions?.Count ?? 0} predictions with {totalPaths} total simulation paths");
return predictions ?? new List<MinerPrediction>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, $"HTTP error while fetching Synth predictions for {asset}");
return new List<MinerPrediction>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, $"Timeout while fetching Synth predictions for {asset}");
return new List<MinerPrediction>();
}
catch (JsonException ex)
{
_logger.LogError(ex, $"JSON deserialization error while parsing Synth predictions for {asset}");
return new List<MinerPrediction>();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error while fetching Synth predictions for {asset}");
return new List<MinerPrediction>();
}
}
/// <summary>
/// Fetches historical predictions from specified miners for a specific time point
/// </summary>
public async Task<List<MinerPrediction>> GetHistoricalMinerPredictionsAsync(
List<int> minerUids,
string asset,
DateTime startTime,
int timeIncrement,
int timeLength,
SynthConfiguration config)
{
if (minerUids == null || !minerUids.Any())
{
_logger.LogWarning("No miner UIDs provided for historical prediction request");
return new List<MinerPrediction>();
}
try
{
// Format start time to ISO 8601 format as required by the API
var startTimeStr = Uri.EscapeDataString(startTime.ToString("yyyy-MM-ddTHH:mm:ssZ"));
// Build URL with proper array formatting for miner parameter
var queryParams = new List<string>
{
$"asset={Uri.EscapeDataString(asset)}",
$"start_time={startTimeStr}",
$"time_increment={timeIncrement}",
$"time_length={timeLength}"
};
// Add each miner UID as a separate parameter (standard array query parameter format)
foreach (var minerUid in minerUids)
{
queryParams.Add($"miner={minerUid}");
}
var url = $"/prediction/historical?{string.Join("&", queryParams)}";
_logger.LogInformation(
$"🔮 **Synth API** - Fetching historical predictions for {minerUids.Count} miners, asset: {asset}, time: {startTime:yyyy-MM-dd HH:mm}, duration: {timeLength}s");
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
$"Synth API historical predictions request failed: {response.StatusCode} - {response.ReasonPhrase}");
return new List<MinerPrediction>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var predictions = JsonSerializer.Deserialize<List<MinerPrediction>>(jsonContent, _jsonOptions);
var totalPaths = predictions?.Sum(p => p.NumSimulations) ?? 0;
_logger.LogInformation(
$"📈 **Synth API** - Retrieved {predictions?.Count ?? 0} historical predictions with {totalPaths} total simulation paths");
return predictions ?? new List<MinerPrediction>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, $"HTTP error while fetching Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, $"Timeout while fetching Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
catch (JsonException ex)
{
_logger.LogError(ex, $"JSON deserialization error while parsing Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error while fetching Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
}
public void Dispose()
{
_httpClient?.Dispose();
}
}

View File

@@ -0,0 +1,169 @@
using Managing.Domain.Synth.Models;
namespace Managing.Application.Synth;
/// <summary>
/// Helper class for creating and configuring Synth API integration
/// </summary>
public static class SynthConfigurationHelper
{
/// <summary>
/// Creates a default Synth configuration for live trading
/// </summary>
/// <returns>A configured SynthConfiguration instance</returns>
public static SynthConfiguration CreateLiveTradingConfig()
{
return new SynthConfiguration
{
IsEnabled = true,
TopMinersCount = 10,
TimeIncrement = 300, // 5 minutes
DefaultTimeLength = 86400, // 24 hours
MaxLiquidationProbability = 0.10m, // 10% max risk
PredictionCacheDurationMinutes = 5,
UseForPositionSizing = true,
UseForSignalFiltering = true,
UseForDynamicStopLoss = true
};
}
/// <summary>
/// Creates a conservative Synth configuration with lower risk tolerances
/// </summary>
/// <returns>A conservative SynthConfiguration instance</returns>
public static SynthConfiguration CreateConservativeConfig()
{
return new SynthConfiguration
{
IsEnabled = true,
TopMinersCount = 10,
TimeIncrement = 300, // 5 minutes
DefaultTimeLength = 86400, // 24 hours
MaxLiquidationProbability = 0.05m, // 5% max risk (more conservative)
PredictionCacheDurationMinutes = 3, // More frequent updates
UseForPositionSizing = true,
UseForSignalFiltering = true,
UseForDynamicStopLoss = true
};
}
/// <summary>
/// Creates an aggressive Synth configuration with higher risk tolerances
/// </summary>
/// <returns>An aggressive SynthConfiguration instance</returns>
public static SynthConfiguration CreateAggressiveConfig()
{
return new SynthConfiguration
{
IsEnabled = true,
TopMinersCount = 15, // More miners for broader consensus
TimeIncrement = 300, // 5 minutes
DefaultTimeLength = 86400, // 24 hours
MaxLiquidationProbability = 0.15m, // 15% max risk (more aggressive)
PredictionCacheDurationMinutes = 7, // Less frequent updates to reduce API calls
UseForPositionSizing = true,
UseForSignalFiltering = false, // Don't filter signals in aggressive mode
UseForDynamicStopLoss = true
};
}
/// <summary>
/// Creates a disabled Synth configuration (bot will operate without Synth predictions)
/// </summary>
/// <returns>A disabled SynthConfiguration instance</returns>
public static SynthConfiguration CreateDisabledConfig()
{
return new SynthConfiguration
{
IsEnabled = false,
TopMinersCount = 10,
TimeIncrement = 300,
DefaultTimeLength = 86400,
MaxLiquidationProbability = 0.10m,
PredictionCacheDurationMinutes = 5,
UseForPositionSizing = false,
UseForSignalFiltering = false,
UseForDynamicStopLoss = false
};
}
/// <summary>
/// Creates a Synth configuration optimized for backtesting (disabled)
/// </summary>
/// <returns>A backtesting-optimized SynthConfiguration instance</returns>
public static SynthConfiguration CreateBacktestConfig()
{
// Synth predictions are not available for historical data, so always disabled for backtests
return CreateDisabledConfig();
}
/// <summary>
/// Validates and provides suggestions for improving a Synth configuration
/// </summary>
/// <param name="config">The configuration to validate</param>
/// <returns>List of validation messages and suggestions</returns>
public static List<string> ValidateConfiguration(SynthConfiguration config)
{
var messages = new List<string>();
if (config == null)
{
messages.Add("❌ Configuration is null");
return messages;
}
if (!config.IsEnabled)
{
messages.Add(" Synth API is disabled - bot will operate without predictions");
return messages;
}
if (config.TopMinersCount <= 0)
{
messages.Add("❌ TopMinersCount must be greater than 0");
}
else if (config.TopMinersCount > 20)
{
messages.Add("⚠️ TopMinersCount > 20 may result in slower performance and higher API usage");
}
if (config.TimeIncrement <= 0)
{
messages.Add("❌ TimeIncrement must be greater than 0");
}
if (config.DefaultTimeLength <= 0)
{
messages.Add("❌ DefaultTimeLength must be greater than 0");
}
if (config.MaxLiquidationProbability < 0 || config.MaxLiquidationProbability > 1)
{
messages.Add("❌ MaxLiquidationProbability must be between 0 and 1");
}
else if (config.MaxLiquidationProbability < 0.02m)
{
messages.Add("⚠️ MaxLiquidationProbability < 2% is very conservative and may block many trades");
}
else if (config.MaxLiquidationProbability > 0.20m)
{
messages.Add("⚠️ MaxLiquidationProbability > 20% is very aggressive and may increase risk");
}
if (config.PredictionCacheDurationMinutes <= 0)
{
messages.Add("❌ PredictionCacheDurationMinutes must be greater than 0");
}
else if (config.PredictionCacheDurationMinutes < 1)
{
messages.Add("⚠️ Cache duration < 1 minute may result in excessive API calls");
}
if (messages.Count == 0)
{
messages.Add("✅ Configuration appears valid");
}
return messages;
}
}

File diff suppressed because it is too large Load Diff