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

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

View File

@@ -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);

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -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)
{

View File

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

View File

@@ -79,7 +79,7 @@ namespace Managing.Application.Scenarios
return _tradingService.GetScenarioByName(name);
}
public IEnumerable<Indicator> GetStrategies()
public IEnumerable<Indicator> GetIndicators()
{
return _tradingService.GetStrategies();
}

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

View File

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