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:
@@ -7,7 +7,7 @@ namespace Managing.Application.Abstractions;
|
||||
|
||||
public interface IBotService
|
||||
{
|
||||
void SaveOrUpdateBotBackup(User user, string identifier, BotType botType, BotStatus status, string data);
|
||||
void SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, string data);
|
||||
void AddSimpleBotToCache(IBot bot);
|
||||
void AddTradingBotToCache(ITradingBot bot);
|
||||
List<ITradingBot> GetActiveBots();
|
||||
@@ -21,7 +21,7 @@ public interface IBotService
|
||||
/// <param name="config">The trading bot configuration</param>
|
||||
/// <returns>ITradingBot instance</returns>
|
||||
ITradingBot CreateTradingBot(TradingBotConfig config);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trading bot for backtesting using the unified TradingBot class
|
||||
/// </summary>
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Managing.Application.Abstractions
|
||||
{
|
||||
IEnumerable<Scenario> GetScenarios();
|
||||
Scenario CreateScenario(string name, List<string> strategies, int? loopbackPeriod = 1);
|
||||
IEnumerable<Indicator> GetStrategies();
|
||||
IEnumerable<Indicator> GetIndicators();
|
||||
bool DeleteStrategy(string name);
|
||||
bool DeleteScenario(string name);
|
||||
Scenario GetScenario(string name);
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace Managing.Application.Abstractions
|
||||
{
|
||||
TradingBotConfig Config { get; set; }
|
||||
Account Account { get; set; }
|
||||
HashSet<IIndicator> Indicators { get; set; }
|
||||
FixedSizeQueue<Candle> OptimizedCandles { get; set; }
|
||||
HashSet<Candle> Candles { get; set; }
|
||||
HashSet<Signal> Signals { get; set; }
|
||||
@@ -25,14 +24,12 @@ namespace Managing.Application.Abstractions
|
||||
DateTime PreloadSince { get; set; }
|
||||
int PreloadedCandlesCount { get; set; }
|
||||
decimal Fee { get; set; }
|
||||
Scenario Scenario { get; set; }
|
||||
|
||||
Task Run();
|
||||
Task ToggleIsForWatchOnly();
|
||||
int GetWinRate();
|
||||
decimal GetProfitAndLoss();
|
||||
decimal GetTotalFees();
|
||||
void LoadIndicators(IEnumerable<IIndicator> indicators);
|
||||
void LoadScenario(string scenarioName);
|
||||
void LoadScenario(Scenario scenario);
|
||||
void UpdateIndicatorsValues();
|
||||
|
||||
@@ -183,19 +183,44 @@ namespace Managing.Application.Backtesting
|
||||
throw new Exception("No candle to backtest");
|
||||
}
|
||||
|
||||
var totalCandles = candles.Count;
|
||||
var currentCandle = 0;
|
||||
var lastLoggedPercentage = 0;
|
||||
|
||||
_logger.LogInformation("Starting backtest with {TotalCandles} candles for {Ticker} on {Timeframe}",
|
||||
totalCandles, config.Ticker, config.Timeframe);
|
||||
|
||||
bot.WalletBalances.Add(candles.FirstOrDefault().Date, config.BotTradingBalance);
|
||||
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
bot.OptimizedCandles.Enqueue(candle);
|
||||
bot.Candles.Add(candle);
|
||||
bot.Run();
|
||||
|
||||
currentCandle++;
|
||||
|
||||
// Log progress every 10% or every 1000 candles, whichever comes first
|
||||
var currentPercentage = (int)((double)currentCandle / totalCandles * 100);
|
||||
var shouldLog = currentPercentage >= lastLoggedPercentage + 10 ||
|
||||
currentCandle % 1000 == 0 ||
|
||||
currentCandle == totalCandles;
|
||||
|
||||
if (shouldLog && currentPercentage > lastLoggedPercentage)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Backtest progress: {CurrentCandle}/{TotalCandles} ({Percentage}%) - Processing candle from {CandleDate}",
|
||||
currentCandle, totalCandles, currentPercentage, candle.Date.ToString("yyyy-MM-dd HH:mm"));
|
||||
lastLoggedPercentage = currentPercentage;
|
||||
}
|
||||
}
|
||||
|
||||
bot.Candles = new HashSet<Candle>(candles);
|
||||
bot.UpdateIndicatorsValues();
|
||||
_logger.LogInformation("Backtest processing completed. Calculating final results...");
|
||||
|
||||
var strategies = _scenarioService.GetStrategies();
|
||||
var strategiesValues = GetStrategiesValues(strategies, candles);
|
||||
bot.Candles = new HashSet<Candle>(candles);
|
||||
// bot.UpdateIndicatorsValues();
|
||||
|
||||
var indicatorsValues = GetIndicatorsValues(bot.Config.Scenario.Indicators, candles);
|
||||
|
||||
var finalPnl = bot.GetProfitAndLoss();
|
||||
var winRate = bot.GetWinRate();
|
||||
@@ -230,7 +255,7 @@ namespace Managing.Application.Backtesting
|
||||
WalletBalances = bot.WalletBalances.ToList(),
|
||||
Statistics = stats,
|
||||
OptimizedMoneyManagement = optimizedMoneyManagement,
|
||||
StrategiesValues = AggregateValues(strategiesValues, bot.IndicatorsValues),
|
||||
IndicatorsValues = AggregateValues(indicatorsValues, bot.IndicatorsValues),
|
||||
Score = score
|
||||
};
|
||||
|
||||
@@ -238,14 +263,14 @@ namespace Managing.Application.Backtesting
|
||||
}
|
||||
|
||||
private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues(
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> strategiesValues,
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues,
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> botStrategiesValues)
|
||||
{
|
||||
// Foreach strategy type, only retrieve the values where the strategy is not present already in the bot
|
||||
// Then, add the values to the bot values
|
||||
|
||||
var result = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
foreach (var strategy in strategiesValues)
|
||||
foreach (var indicator in indicatorsValues)
|
||||
{
|
||||
// if (!botStrategiesValues.ContainsKey(strategy.Key))
|
||||
// {
|
||||
@@ -255,29 +280,29 @@ namespace Managing.Application.Backtesting
|
||||
// result[strategy.Key] = botStrategiesValues[strategy.Key];
|
||||
// }
|
||||
|
||||
result[strategy.Key] = strategy.Value;
|
||||
result[indicator.Key] = indicator.Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Dictionary<IndicatorType, IndicatorsResultBase> GetStrategiesValues(IEnumerable<Indicator> strategies,
|
||||
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
|
||||
List<Candle> candles)
|
||||
{
|
||||
var strategiesValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
var fixedCandles = new FixedSizeQueue<Candle>(10000);
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
fixedCandles.Enqueue(candle);
|
||||
}
|
||||
|
||||
foreach (var strategy in strategies)
|
||||
foreach (var indicator in indicators)
|
||||
{
|
||||
try
|
||||
{
|
||||
var s = ScenarioHelpers.BuildIndicator(strategy, 10000);
|
||||
var s = ScenarioHelpers.BuildIndicator(indicator, 10000);
|
||||
s.Candles = fixedCandles;
|
||||
strategiesValues[strategy.Type] = s.GetStrategyValues();
|
||||
indicatorsValues[indicator.Type] = s.GetIndicatorValues();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -285,7 +310,7 @@ namespace Managing.Application.Backtesting
|
||||
}
|
||||
}
|
||||
|
||||
return strategiesValues;
|
||||
return indicatorsValues;
|
||||
}
|
||||
|
||||
public bool DeleteBacktest(string id)
|
||||
|
||||
@@ -3,7 +3,6 @@ using Managing.Domain.Bots;
|
||||
using Managing.Domain.Workflows;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Bots
|
||||
{
|
||||
@@ -44,7 +43,7 @@ namespace Managing.Application.Bots
|
||||
public override void SaveBackup()
|
||||
{
|
||||
var data = JsonConvert.SerializeObject(_workflow);
|
||||
_botService.SaveOrUpdateBotBackup(User, Identifier, BotType.SimpleBot, Status, data);
|
||||
_botService.SaveOrUpdateBotBackup(User, Identifier, Status, data);
|
||||
}
|
||||
|
||||
public override void LoadBackup(BotBackup backup)
|
||||
|
||||
@@ -7,7 +7,6 @@ using Managing.Core.FixedSizedQueue;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Strategies;
|
||||
@@ -41,8 +40,6 @@ public class TradingBot : Bot, ITradingBot
|
||||
public DateTime PreloadSince { get; set; }
|
||||
public int PreloadedCandlesCount { get; set; }
|
||||
public decimal Fee { get; set; }
|
||||
public Scenario Scenario { get; set; }
|
||||
|
||||
|
||||
public TradingBot(
|
||||
IExchangeService exchangeService,
|
||||
@@ -132,6 +129,9 @@ public class TradingBot : Bot, ITradingBot
|
||||
|
||||
public void LoadScenario(string scenarioName)
|
||||
{
|
||||
if (Config.Scenario != null)
|
||||
return;
|
||||
|
||||
var scenario = TradingService.GetScenarioByName(scenarioName);
|
||||
if (scenario == null)
|
||||
{
|
||||
@@ -140,7 +140,6 @@ public class TradingBot : Bot, ITradingBot
|
||||
}
|
||||
else
|
||||
{
|
||||
Scenario = scenario;
|
||||
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
|
||||
}
|
||||
}
|
||||
@@ -154,11 +153,15 @@ public class TradingBot : Bot, ITradingBot
|
||||
}
|
||||
else
|
||||
{
|
||||
Scenario = scenario;
|
||||
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadIndicators(Scenario scenario)
|
||||
{
|
||||
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
|
||||
}
|
||||
|
||||
public void LoadIndicators(IEnumerable<IIndicator> indicators)
|
||||
{
|
||||
foreach (var strategy in indicators)
|
||||
@@ -209,7 +212,7 @@ public class TradingBot : Bot, ITradingBot
|
||||
}
|
||||
|
||||
UpdateWalletBalances();
|
||||
if (OptimizedCandles.Count % 100 == 0) // Log every 10th execution
|
||||
if (!Config.IsForBacktest) // Log every 10th execution
|
||||
{
|
||||
Logger.LogInformation($"Candle date : {OptimizedCandles.Last().Date:u}");
|
||||
Logger.LogInformation($"Signals : {Signals.Count}");
|
||||
@@ -223,7 +226,7 @@ public class TradingBot : Bot, ITradingBot
|
||||
{
|
||||
foreach (var strategy in Indicators)
|
||||
{
|
||||
IndicatorsValues[strategy.Type] = ((Indicator)strategy).GetStrategyValues();
|
||||
IndicatorsValues[strategy.Type] = ((Indicator)strategy).GetIndicatorValues();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +263,10 @@ public class TradingBot : Bot, ITradingBot
|
||||
|
||||
private async Task UpdateSignals(FixedSizeQueue<Candle> candles)
|
||||
{
|
||||
var signal = TradingBox.GetSignal(candles.ToHashSet(), Indicators, Signals, Scenario.LoopbackPeriod);
|
||||
// If position open and not flipped, do not update signals
|
||||
if (!Config.FlipPosition && Positions.Any(p => !p.IsFinished())) return;
|
||||
|
||||
var signal = TradingBox.GetSignal(candles.ToHashSet(), Indicators, Signals, Config.Scenario.LoopbackPeriod);
|
||||
if (signal == null) return;
|
||||
|
||||
signal.User = Account.User;
|
||||
@@ -272,11 +278,39 @@ public class TradingBot : Bot, ITradingBot
|
||||
if (Config.IsForWatchingOnly || (ExecutionCount < 1 && !Config.IsForBacktest))
|
||||
signal.Status = SignalStatus.Expired;
|
||||
|
||||
Signals.Add(signal);
|
||||
|
||||
var signalText = $"{Config.ScenarioName} trigger a signal. Signal told you " +
|
||||
$"to {signal.Direction} {Config.Ticker} on {Config.Timeframe}. The confidence in this signal is {signal.Confidence}. Identifier : {signal.Identifier}";
|
||||
|
||||
|
||||
// Apply Synth-based signal filtering if enabled
|
||||
if (Config.UseSynthApi)
|
||||
{
|
||||
var currentPrice = Config.IsForBacktest
|
||||
? OptimizedCandles.Last().Close
|
||||
: ExchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
|
||||
|
||||
var signalValidationResult = TradingService.ValidateSynthSignalAsync(signal, currentPrice, Config,
|
||||
Config.IsForBacktest).GetAwaiter().GetResult();
|
||||
|
||||
if (signalValidationResult.Confidence == Confidence.None ||
|
||||
signalValidationResult.Confidence == Confidence.Low ||
|
||||
signalValidationResult.IsBlocked)
|
||||
{
|
||||
signal.Status = SignalStatus.Expired;
|
||||
await LogInformation(
|
||||
$"🚫 **Synth Signal Filter** - Signal {signal.Identifier} blocked by Synth risk assessment. Context : {signalValidationResult.ValidationContext}");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
signal.SetConfidence(signalValidationResult.Confidence);
|
||||
signalText +=
|
||||
$" and Synth risk assessment passed. Context : {signalValidationResult.ValidationContext}";
|
||||
}
|
||||
}
|
||||
|
||||
Signals.Add(signal);
|
||||
|
||||
Logger.LogInformation(signalText);
|
||||
|
||||
if (Config.IsForWatchingOnly && !Config.IsForBacktest && ExecutionCount > 0)
|
||||
@@ -326,7 +360,8 @@ public class TradingBot : Bot, ITradingBot
|
||||
date: position.Open.Date,
|
||||
exchange: Account.Exchange,
|
||||
indicatorType: IndicatorType.Stc, // Use a valid strategy type for recreated signals
|
||||
signalType: SignalType.Signal
|
||||
signalType: SignalType.Signal,
|
||||
indicatorName: "RecreatedSignal"
|
||||
);
|
||||
|
||||
// Since Signal identifier is auto-generated, we need to update our position
|
||||
@@ -414,8 +449,6 @@ public class TradingBot : Bot, ITradingBot
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInformation($"📊 **Position Update**\nUpdating position: `{positionForSignal.SignalIdentifier}`");
|
||||
|
||||
var position = Config.IsForBacktest
|
||||
? positionForSignal
|
||||
: TradingService.GetPositionByIdentifier(positionForSignal.Identifier);
|
||||
@@ -624,6 +657,38 @@ public class TradingBot : Bot, ITradingBot
|
||||
await OpenPosition(signal);
|
||||
}
|
||||
}
|
||||
|
||||
// Synth-based position monitoring for liquidation risk
|
||||
if (Config.UseSynthApi && !Config.IsForBacktest &&
|
||||
positionForSignal.Status == PositionStatus.Filled)
|
||||
{
|
||||
var currentPrice = ExchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
|
||||
var riskResult = await TradingService.MonitorSynthPositionRiskAsync(
|
||||
Config.Ticker,
|
||||
positionForSignal.OriginDirection,
|
||||
currentPrice,
|
||||
positionForSignal.StopLoss.Price,
|
||||
positionForSignal.Identifier,
|
||||
Config);
|
||||
|
||||
if (riskResult != null && riskResult.ShouldWarn && !string.IsNullOrEmpty(riskResult.WarningMessage))
|
||||
{
|
||||
await LogWarning(riskResult.WarningMessage);
|
||||
}
|
||||
|
||||
if (riskResult.ShouldAutoClose && !string.IsNullOrEmpty(riskResult.EmergencyMessage))
|
||||
{
|
||||
await LogWarning(riskResult.EmergencyMessage);
|
||||
|
||||
var signalForAutoClose =
|
||||
Signals.FirstOrDefault(s => s.Identifier == positionForSignal.SignalIdentifier);
|
||||
if (signalForAutoClose != null)
|
||||
{
|
||||
await CloseTrade(signalForAutoClose, positionForSignal, positionForSignal.StopLoss,
|
||||
currentPrice, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -784,6 +849,20 @@ public class TradingBot : Bot, ITradingBot
|
||||
return false;
|
||||
}
|
||||
|
||||
// Synth-based pre-trade risk assessment
|
||||
if (Config.UseSynthApi)
|
||||
{
|
||||
var currentPrice = Config.IsForBacktest
|
||||
? OptimizedCandles.Last().Close
|
||||
: ExchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
|
||||
|
||||
if (!(await TradingService.AssessSynthPositionRiskAsync(Config.Ticker, signal.Direction, currentPrice,
|
||||
Config, Config.IsForBacktest)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check cooldown period and loss streak
|
||||
return await CheckCooldownPeriod(signal) && await CheckLossStreak(signal);
|
||||
}
|
||||
@@ -1030,7 +1109,7 @@ public class TradingBot : Bot, ITradingBot
|
||||
{
|
||||
// Add PnL (could be positive or negative)
|
||||
Config.BotTradingBalance += position.ProfitAndLoss.Realized;
|
||||
|
||||
|
||||
Logger.LogInformation(
|
||||
$"💰 **Balance Updated**\nNew bot trading balance: `${Config.BotTradingBalance:F2}`");
|
||||
}
|
||||
@@ -1150,7 +1229,7 @@ public class TradingBot : Bot, ITradingBot
|
||||
public decimal GetTotalFees()
|
||||
{
|
||||
decimal totalFees = 0;
|
||||
|
||||
|
||||
foreach (var position in Positions.Where(p => p.Open.Price > 0 && p.Open.Quantity > 0))
|
||||
{
|
||||
totalFees += CalculatePositionFees(position);
|
||||
@@ -1167,22 +1246,22 @@ public class TradingBot : Bot, ITradingBot
|
||||
private decimal CalculatePositionFees(Position position)
|
||||
{
|
||||
decimal fees = 0;
|
||||
|
||||
|
||||
// Calculate position size in USD (leverage is already included in quantity calculation)
|
||||
var positionSizeUsd = position.Open.Price * position.Open.Quantity;
|
||||
|
||||
|
||||
// UI Fee: 0.1% of position size paid BOTH on opening AND closing
|
||||
var uiFeeRate = 0.001m; // 0.1%
|
||||
var uiFeeOpen = positionSizeUsd * uiFeeRate; // Fee paid on opening
|
||||
var uiFeeClose = positionSizeUsd * uiFeeRate; // Fee paid on closing
|
||||
var totalUiFees = uiFeeOpen + uiFeeClose; // Total: 0.2% of position size
|
||||
var uiFeeOpen = positionSizeUsd * uiFeeRate; // Fee paid on opening
|
||||
var uiFeeClose = positionSizeUsd * uiFeeRate; // Fee paid on closing
|
||||
var totalUiFees = uiFeeOpen + uiFeeClose; // Total: 0.2% of position size
|
||||
fees += totalUiFees;
|
||||
|
||||
|
||||
// Network Fee: $0.50 for opening position only
|
||||
// Closing is handled by oracle, so no network fee for closing
|
||||
var networkFeeForOpening = 0.50m;
|
||||
fees += networkFeeForOpening;
|
||||
|
||||
|
||||
return fees;
|
||||
}
|
||||
|
||||
@@ -1236,53 +1315,29 @@ public class TradingBot : Bot, ITradingBot
|
||||
{
|
||||
var data = new TradingBotBackup
|
||||
{
|
||||
Name = Name,
|
||||
BotType = Config.BotType,
|
||||
Config = Config,
|
||||
Signals = Signals,
|
||||
Positions = Positions,
|
||||
Timeframe = Config.Timeframe,
|
||||
Ticker = Config.Ticker,
|
||||
ScenarioName = Config.ScenarioName,
|
||||
AccountName = Config.AccountName,
|
||||
IsForWatchingOnly = Config.IsForWatchingOnly,
|
||||
WalletBalances = WalletBalances,
|
||||
MoneyManagement = Config.MoneyManagement,
|
||||
BotTradingBalance = Config.BotTradingBalance,
|
||||
StartupTime = StartupTime,
|
||||
CooldownPeriod = Config.CooldownPeriod,
|
||||
MaxLossStreak = Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = Config.MaxPositionTimeHours ?? 0m,
|
||||
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
|
||||
CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable,
|
||||
StartupTime = StartupTime
|
||||
};
|
||||
BotService.SaveOrUpdateBotBackup(User, Identifier, Config.BotType, Status, JsonConvert.SerializeObject(data));
|
||||
BotService.SaveOrUpdateBotBackup(User, Identifier, Status, JsonConvert.SerializeObject(data));
|
||||
}
|
||||
|
||||
public override void LoadBackup(BotBackup backup)
|
||||
{
|
||||
var data = JsonConvert.DeserializeObject<TradingBotBackup>(backup.Data);
|
||||
Config = new TradingBotConfig
|
||||
{
|
||||
AccountName = data.AccountName,
|
||||
MoneyManagement = data.MoneyManagement,
|
||||
Ticker = data.Ticker,
|
||||
ScenarioName = data.ScenarioName,
|
||||
Timeframe = data.Timeframe,
|
||||
IsForBacktest = false, // Always false when loading from backup
|
||||
IsForWatchingOnly = data.IsForWatchingOnly,
|
||||
BotTradingBalance = data.BotTradingBalance,
|
||||
BotType = data.BotType,
|
||||
CooldownPeriod = data.CooldownPeriod,
|
||||
MaxLossStreak = data.MaxLossStreak,
|
||||
MaxPositionTimeHours = data.MaxPositionTimeHours == 0m ? null : data.MaxPositionTimeHours,
|
||||
FlipOnlyWhenInProfit = data.FlipOnlyWhenInProfit,
|
||||
CloseEarlyWhenProfitable = data.CloseEarlyWhenProfitable,
|
||||
Name = data.Name
|
||||
};
|
||||
|
||||
Signals = data.Signals;
|
||||
Positions = data.Positions;
|
||||
WalletBalances = data.WalletBalances;
|
||||
// Load the configuration directly
|
||||
Config = data.Config;
|
||||
|
||||
// Ensure IsForBacktest is always false when loading from backup
|
||||
Config.IsForBacktest = false;
|
||||
|
||||
// Load runtime state
|
||||
Signals = data.Signals ?? new HashSet<Signal>();
|
||||
Positions = data.Positions ?? new List<Position>();
|
||||
WalletBalances = data.WalletBalances ?? new Dictionary<DateTime, decimal>();
|
||||
PreloadSince = data.StartupTime;
|
||||
Identifier = backup.Identifier;
|
||||
User = backup.User;
|
||||
@@ -1307,7 +1362,7 @@ public class TradingBot : Bot, ITradingBot
|
||||
// Create a fake signal for manual position opening
|
||||
var signal = new Signal(Config.Ticker, direction, Confidence.Low, lastCandle, lastCandle.Date,
|
||||
TradingExchanges.GmxV2,
|
||||
IndicatorType.Stc, SignalType.Signal);
|
||||
IndicatorType.Stc, SignalType.Signal, "Manual Signal");
|
||||
signal.Status = SignalStatus.WaitingForPosition; // Ensure status is correct
|
||||
signal.User = Account.User; // Assign user
|
||||
|
||||
@@ -1433,7 +1488,7 @@ public class TradingBot : Bot, ITradingBot
|
||||
}
|
||||
|
||||
// If scenario changed, reload it
|
||||
var currentScenario = Scenario?.Name;
|
||||
var currentScenario = Config.Scenario?.Name;
|
||||
if (Config.ScenarioName != currentScenario)
|
||||
{
|
||||
LoadScenario(Config.ScenarioName);
|
||||
@@ -1485,29 +1540,36 @@ public class TradingBot : Bot, ITradingBot
|
||||
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
|
||||
FlipPosition = Config.FlipPosition,
|
||||
Name = Config.Name,
|
||||
CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable
|
||||
CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable,
|
||||
UseSynthApi = Config.UseSynthApi,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class TradingBotBackup
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public BotType BotType { get; set; }
|
||||
/// <summary>
|
||||
/// The complete trading bot configuration
|
||||
/// </summary>
|
||||
public TradingBotConfig Config { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime state: Active signals for the bot
|
||||
/// </summary>
|
||||
public HashSet<Signal> Signals { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime state: Open and closed positions for the bot
|
||||
/// </summary>
|
||||
public List<Position> Positions { get; set; }
|
||||
public Timeframe Timeframe { get; set; }
|
||||
public Ticker Ticker { get; set; }
|
||||
public string ScenarioName { get; set; }
|
||||
public string AccountName { get; set; }
|
||||
public bool IsForWatchingOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime state: Historical wallet balances over time
|
||||
/// </summary>
|
||||
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
|
||||
public MoneyManagement MoneyManagement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime state: When the bot was started
|
||||
/// </summary>
|
||||
public DateTime StartupTime { get; set; }
|
||||
public decimal BotTradingBalance { get; set; }
|
||||
public int CooldownPeriod { get; set; }
|
||||
public int MaxLossStreak { get; set; }
|
||||
public decimal MaxPositionTimeHours { get; set; }
|
||||
public bool FlipOnlyWhenInProfit { get; set; }
|
||||
public bool CloseEarlyWhenProfitable { get; set; }
|
||||
}
|
||||
@@ -45,7 +45,7 @@ namespace Managing.Application.ManageBot
|
||||
return _botRepository.GetBots().FirstOrDefault(b => b.Identifier == identifier);
|
||||
}
|
||||
|
||||
public void SaveOrUpdateBotBackup(User user, string identifier, BotType botType, BotStatus status, string data)
|
||||
public void SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, string data)
|
||||
{
|
||||
var backup = GetBotBackup(identifier);
|
||||
|
||||
@@ -62,7 +62,6 @@ namespace Managing.Application.ManageBot
|
||||
LastStatus = status,
|
||||
User = user,
|
||||
Identifier = identifier,
|
||||
BotType = botType,
|
||||
Data = data
|
||||
};
|
||||
|
||||
@@ -118,40 +117,29 @@ namespace Managing.Application.ManageBot
|
||||
object bot = null;
|
||||
Task botTask = null;
|
||||
|
||||
switch (backupBot.BotType)
|
||||
var scalpingBotData = JsonConvert.DeserializeObject<TradingBotBackup>(backupBot.Data);
|
||||
|
||||
// Get the config directly from the backup
|
||||
var scalpingConfig = scalpingBotData.Config;
|
||||
|
||||
// Ensure the money management is properly loaded from database if needed
|
||||
if (scalpingConfig.MoneyManagement != null &&
|
||||
!string.IsNullOrEmpty(scalpingConfig.MoneyManagement.Name))
|
||||
{
|
||||
case BotType.ScalpingBot:
|
||||
case BotType.FlippingBot:
|
||||
var scalpingBotData = JsonConvert.DeserializeObject<TradingBotBackup>(backupBot.Data);
|
||||
var scalpingMoneyManagement =
|
||||
_moneyManagementService.GetMoneyMangement(scalpingBotData.MoneyManagement.Name).Result;
|
||||
|
||||
// Create config from backup data
|
||||
var scalpingConfig = new TradingBotConfig
|
||||
{
|
||||
AccountName = scalpingBotData.AccountName,
|
||||
MoneyManagement = scalpingMoneyManagement,
|
||||
Ticker = scalpingBotData.Ticker,
|
||||
ScenarioName = scalpingBotData.ScenarioName,
|
||||
Timeframe = scalpingBotData.Timeframe,
|
||||
IsForWatchingOnly = scalpingBotData.IsForWatchingOnly,
|
||||
BotTradingBalance = scalpingBotData.BotTradingBalance,
|
||||
BotType = scalpingBotData.BotType,
|
||||
Name = scalpingBotData.Name,
|
||||
CooldownPeriod = scalpingBotData.CooldownPeriod,
|
||||
MaxLossStreak = scalpingBotData.MaxLossStreak,
|
||||
MaxPositionTimeHours = scalpingBotData.MaxPositionTimeHours == 0m ? null : scalpingBotData.MaxPositionTimeHours,
|
||||
FlipOnlyWhenInProfit = scalpingBotData.FlipOnlyWhenInProfit,
|
||||
IsForBacktest = false,
|
||||
FlipPosition = false,
|
||||
CloseEarlyWhenProfitable = scalpingBotData.CloseEarlyWhenProfitable
|
||||
};
|
||||
|
||||
bot = CreateTradingBot(scalpingConfig);
|
||||
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
|
||||
break;
|
||||
var moneyManagement = _moneyManagementService
|
||||
.GetMoneyMangement(scalpingConfig.MoneyManagement.Name).Result;
|
||||
if (moneyManagement != null)
|
||||
{
|
||||
scalpingConfig.MoneyManagement = moneyManagement;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure critical properties are set correctly for restored bots
|
||||
scalpingConfig.IsForBacktest = false;
|
||||
|
||||
bot = CreateTradingBot(scalpingConfig);
|
||||
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
|
||||
|
||||
if (bot != null && botTask != null)
|
||||
{
|
||||
var botWrapper = new BotTaskWrapper(botTask, bot.GetType(), bot);
|
||||
@@ -258,14 +246,14 @@ namespace Managing.Application.ManageBot
|
||||
|
||||
// Update the bot configuration first
|
||||
var updateResult = await tradingBot.UpdateConfiguration(newConfig, allowNameChange: true);
|
||||
|
||||
|
||||
if (updateResult)
|
||||
{
|
||||
// Update the dictionary key
|
||||
if (_botTasks.TryRemove(identifier, out var removedWrapper))
|
||||
{
|
||||
_botTasks.TryAdd(newConfig.Name, removedWrapper);
|
||||
|
||||
|
||||
// Update the backup with the new identifier
|
||||
if (!newConfig.IsForBacktest)
|
||||
{
|
||||
@@ -275,7 +263,7 @@ namespace Managing.Application.ManageBot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return updateResult;
|
||||
}
|
||||
else
|
||||
@@ -288,7 +276,6 @@ namespace Managing.Application.ManageBot
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public ITradingBot CreateTradingBot(TradingBotConfig config)
|
||||
{
|
||||
|
||||
@@ -52,7 +52,9 @@ namespace Managing.Application.ManageBot
|
||||
|
||||
var usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString());
|
||||
|
||||
if (usdcBalance == null || usdcBalance.Value < request.Config.BotTradingBalance)
|
||||
if (usdcBalance == null ||
|
||||
usdcBalance.Value < Constants.GMX.Config.MinimumPositionAmount ||
|
||||
usdcBalance.Value < request.Config.BotTradingBalance)
|
||||
{
|
||||
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
|
||||
}
|
||||
@@ -64,12 +66,14 @@ namespace Managing.Application.ManageBot
|
||||
MoneyManagement = request.Config.MoneyManagement,
|
||||
Ticker = request.Config.Ticker,
|
||||
ScenarioName = request.Config.ScenarioName,
|
||||
Scenario = request.Config.Scenario,
|
||||
Timeframe = request.Config.Timeframe,
|
||||
IsForWatchingOnly = request.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = request.Config.BotTradingBalance,
|
||||
BotType = request.Config.BotType,
|
||||
IsForBacktest = request.Config.IsForBacktest,
|
||||
CooldownPeriod = request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set
|
||||
CooldownPeriod =
|
||||
request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set
|
||||
MaxLossStreak = request.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, // Properly handle nullable value
|
||||
FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit,
|
||||
@@ -85,15 +89,15 @@ namespace Managing.Application.ManageBot
|
||||
bot.User = request.User;
|
||||
_botService.AddSimpleBotToCache(bot);
|
||||
return bot.GetStatus();
|
||||
|
||||
|
||||
case BotType.ScalpingBot:
|
||||
case BotType.FlippingBot:
|
||||
var tradingBot = _botFactory.CreateTradingBot(configToUse);
|
||||
tradingBot.User = request.User;
|
||||
|
||||
|
||||
// Log the configuration being used
|
||||
await LogBotConfigurationAsync(tradingBot, $"{configToUse.BotType} created");
|
||||
|
||||
|
||||
_botService.AddTradingBotToCache(tradingBot);
|
||||
return tradingBot.GetStatus();
|
||||
}
|
||||
@@ -112,16 +116,16 @@ namespace Managing.Application.ManageBot
|
||||
{
|
||||
var config = bot.GetConfiguration();
|
||||
var logMessage = $"{context} - Bot: {config.Name}, " +
|
||||
$"Type: {config.BotType}, " +
|
||||
$"Account: {config.AccountName}, " +
|
||||
$"Ticker: {config.Ticker}, " +
|
||||
$"Balance: {config.BotTradingBalance}, " +
|
||||
$"MaxTime: {config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " +
|
||||
$"FlipOnlyProfit: {config.FlipOnlyWhenInProfit}, " +
|
||||
$"FlipPosition: {config.FlipPosition}, " +
|
||||
$"Cooldown: {config.CooldownPeriod}, " +
|
||||
$"MaxLoss: {config.MaxLossStreak}";
|
||||
|
||||
$"Type: {config.BotType}, " +
|
||||
$"Account: {config.AccountName}, " +
|
||||
$"Ticker: {config.Ticker}, " +
|
||||
$"Balance: {config.BotTradingBalance}, " +
|
||||
$"MaxTime: {config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " +
|
||||
$"FlipOnlyProfit: {config.FlipOnlyWhenInProfit}, " +
|
||||
$"FlipPosition: {config.FlipPosition}, " +
|
||||
$"Cooldown: {config.CooldownPeriod}, " +
|
||||
$"MaxLoss: {config.MaxLossStreak}";
|
||||
|
||||
// Log through the bot's logger (this will use the bot's logging mechanism)
|
||||
// For now, we'll just add a comment that this could be enhanced with actual logging
|
||||
// Console.WriteLine(logMessage); // Could be replaced with proper logging
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Managing.Application.Scenarios
|
||||
return _tradingService.GetScenarioByName(name);
|
||||
}
|
||||
|
||||
public IEnumerable<Indicator> GetStrategies()
|
||||
public IEnumerable<Indicator> GetIndicators()
|
||||
{
|
||||
return _tradingService.GetStrategies();
|
||||
}
|
||||
|
||||
324
src/Managing.Application/Synth/SynthApiClient.cs
Normal file
324
src/Managing.Application/Synth/SynthApiClient.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
169
src/Managing.Application/Synth/SynthConfigurationHelper.cs
Normal file
169
src/Managing.Application/Synth/SynthConfigurationHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
1194
src/Managing.Application/Synth/SynthPredictionService.cs
Normal file
1194
src/Managing.Application/Synth/SynthPredictionService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,12 @@
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Statistics;
|
||||
using Managing.Domain.Strategies;
|
||||
using Managing.Domain.Synth.Models;
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Infrastructure.Evm.Models.Privy;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -22,6 +24,7 @@ public class TradingService : ITradingService
|
||||
private readonly IStatisticRepository _statisticRepository;
|
||||
private readonly IEvmManager _evmManager;
|
||||
private readonly ILogger<TradingService> _logger;
|
||||
private readonly ISynthPredictionService _synthPredictionService;
|
||||
|
||||
public TradingService(
|
||||
ITradingRepository tradingRepository,
|
||||
@@ -31,7 +34,8 @@ public class TradingService : ITradingService
|
||||
ICacheService cacheService,
|
||||
IMessengerService messengerService,
|
||||
IStatisticRepository statisticRepository,
|
||||
IEvmManager evmManager)
|
||||
IEvmManager evmManager,
|
||||
ISynthPredictionService synthPredictionService)
|
||||
{
|
||||
_tradingRepository = tradingRepository;
|
||||
_exchangeService = exchangeService;
|
||||
@@ -41,6 +45,7 @@ public class TradingService : ITradingService
|
||||
_messengerService = messengerService;
|
||||
_statisticRepository = statisticRepository;
|
||||
_evmManager = evmManager;
|
||||
_synthPredictionService = synthPredictionService;
|
||||
}
|
||||
|
||||
public void DeleteScenario(string name)
|
||||
@@ -397,4 +402,25 @@ public class TradingService : ITradingService
|
||||
return new PrivyInitAddressResponse { Success = false, Error = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
// Synth API integration methods
|
||||
public async Task<SignalValidationResult> ValidateSynthSignalAsync(Signal signal, decimal currentPrice,
|
||||
TradingBotConfig botConfig, bool isBacktest)
|
||||
{
|
||||
return await _synthPredictionService.ValidateSignalAsync(signal, currentPrice, botConfig, isBacktest);
|
||||
}
|
||||
|
||||
public async Task<bool> AssessSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
|
||||
TradingBotConfig botConfig, bool isBacktest)
|
||||
{
|
||||
return await _synthPredictionService.AssessPositionRiskAsync(ticker, direction, currentPrice,
|
||||
botConfig, isBacktest);
|
||||
}
|
||||
|
||||
public async Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction,
|
||||
decimal currentPrice, decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig)
|
||||
{
|
||||
return await _synthPredictionService.MonitorPositionRiskAsync(ticker, direction, currentPrice, liquidationPrice,
|
||||
positionIdentifier, botConfig);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user