Trading bot grain (#33)

* Trading bot Grain

* Fix a bit more of the trading bot

* Advance on the tradingbot grain

* Fix build

* Fix db script

* Fix user login

* Fix a bit backtest

* Fix cooldown and backtest

* start fixing bot start

* Fix startup

* Setup local db

* Fix build and update candles and scenario

* Add bot registry

* Add reminder

* Updateing the grains

* fix bootstraping

* Save stats on tick

* Save bot data every tick

* Fix serialization

* fix save bot stats

* Fix get candles

* use dict instead of list for position

* Switch hashset to dict

* Fix a bit

* Fix bot launch and bot view

* add migrations

* Remove the tolist

* Add agent grain

* Save agent summary

* clean

* Add save bot

* Update get bots

* Add get bots

* Fix stop/restart

* fix Update config

* Update scanner table on new backtest saved

* Fix backtestRowDetails.tsx

* Fix agentIndex

* Update agentIndex

* Fix more things

* Update user cache

* Fix

* Fix account load/start/restart/run
This commit is contained in:
Oda
2025-08-04 23:07:06 +02:00
committed by GitHub
parent cd378587aa
commit 082ae8714b
215 changed files with 9562 additions and 14028 deletions

View File

@@ -0,0 +1,27 @@
namespace Managing.Application.Abstractions.Grains
{
public interface IAgentGrain : IGrainWithIntegerKey
{
/// <summary>
/// Initializes the agent grain with user-specific data.
/// </summary>
/// <param name="userId">The ID of the user (used as grain key).</param>
/// <param name="agentName">The display name of the agent.</param>
Task InitializeAsync(int userId, string agentName);
/// <summary>
/// Generates a summary of the agent's stats for the AgentRegistryGrain.
/// </summary>
Task UpdateSummary();
/// <summary>
/// Registers a new bot with this agent.
/// </summary>
Task RegisterBotAsync(Guid botId);
/// <summary>
/// Unregisters a bot from this agent.
/// </summary>
Task UnregisterBotAsync(Guid botId);
}
}

View File

@@ -0,0 +1,22 @@
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Indicators;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// Orleans grain interface for scenario execution and signal generation.
/// This stateless grain handles candle management and signal generation for live trading.
/// </summary>
public interface IScenarioRunnerGrain : IGrainWithGuidKey
{
/// <summary>
/// Generates signals based on the current candles and scenario
/// </summary>
/// <param name="config">The trading bot configuration</param>
/// <param name="previousSignals">Previous signals to consider</param>
/// <param name="startDate">Start date</param>
/// <returns>The generated signal or null if no signal</returns>
Task<LightSignal> GetSignals(TradingBotConfig config, Dictionary<string, LightSignal> previousSignals, DateTime startDate,
Candle candle);
}

View File

@@ -1,21 +0,0 @@
using Managing.Domain.Bots;
namespace Managing.Application.Abstractions
{
public interface IBotFactory
{
/// <summary>
/// Creates a trading bot using the unified TradingBot class
/// </summary>
/// <param name="config">The trading bot configuration</param>
/// <returns>ITradingBot instance</returns>
Task<ITradingBot> CreateTradingBot(TradingBotConfig config);
/// <summary>
/// Creates a trading bot for backtesting using the unified TradingBot class
/// </summary>
/// <param name="config">The trading bot configuration</param>
/// <returns>ITradingBot instance configured for backtesting</returns>
Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config);
}
}

View File

@@ -1,38 +1,47 @@
using Managing.Domain.Bots;
using Managing.Domain.Users;
using Managing.Domain.Workflows;
using Managing.Domain.Trades;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions;
public interface IBotService
{
Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data);
void AddSimpleBotToCache(IBot bot);
void AddTradingBotToCache(ITradingBot bot);
List<ITradingBot> GetActiveBots();
Task<IEnumerable<BotBackup>> GetSavedBotsAsync();
Task StartBotFromBackup(BotBackup backupBot);
Task<BotBackup> GetBotBackup(string identifier);
Task<IEnumerable<Bot>> GetBotsAsync();
Task<IEnumerable<Bot>> GetBotsByStatusAsync(BotStatus status);
Task<BotStatus> StopBot(Guid identifier);
Task<BotStatus> RestartBot(Guid identifier);
Task<bool> DeleteBot(Guid identifier);
Task<bool> UpdateBotConfiguration(Guid identifier, TradingBotConfig newConfig);
Task<IEnumerable<string>> GetActiveBotsNamesAsync();
Task<IEnumerable<Bot>> GetBotsByUser(int id);
Task<IEnumerable<Bot>> GetBotsByIdsAsync(IEnumerable<Guid> botIds);
Task<Bot> GetBotByName(string name);
Task<Bot> GetBotByIdentifier(Guid identifier);
Task<Position> OpenPositionManuallyAsync(Guid identifier, TradeDirection direction);
Task<Position> ClosePositionAsync(Guid identifier, Guid positionId);
Task<TradingBotConfig> GetBotConfig(Guid identifier);
Task<bool> UpdateBotStatisticsAsync(Guid identifier);
Task<bool> SaveBotStatisticsAsync(Bot bot);
/// <summary>
/// Creates a trading bot using the unified TradingBot class
/// Gets paginated bots with filtering and sorting
/// </summary>
/// <param name="config">The trading bot configuration</param>
/// <returns>ITradingBot instance</returns>
Task<ITradingBot> CreateTradingBot(TradingBotConfig config);
/// <summary>
/// Creates a trading bot for backtesting using the unified TradingBot class
/// </summary>
/// <param name="config">The trading bot configuration</param>
/// <returns>ITradingBot instance configured for backtesting</returns>
Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config);
IBot CreateSimpleBot(string botName, Workflow workflow);
Task<string> StopBot(string botName);
Task<bool> DeleteBot(string botName);
Task<string> RestartBot(string botName);
Task ToggleIsForWatchingOnly(string botName);
Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig);
/// <param name="pageNumber">Page number (1-based)</param>
/// <param name="pageSize">Number of items per page</param>
/// <param name="status">Filter by status (optional)</param>
/// <param name="name">Filter by name (partial match, case-insensitive)</param>
/// <param name="ticker">Filter by ticker (partial match, case-insensitive)</param>
/// <param name="agentName">Filter by agent name (partial match, case-insensitive)</param>
/// <param name="sortBy">Sort field</param>
/// <param name="sortDirection">Sort direction ("Asc" or "Desc")</param>
/// <returns>Tuple containing the bots for the current page and total count</returns>
Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
int pageNumber,
int pageSize,
BotStatus? status = null,
string? name = null,
string? ticker = null,
string? agentName = null,
string sortBy = "CreateDate",
string sortDirection = "Desc");
}

View File

@@ -8,19 +8,7 @@ namespace Managing.Application.Abstractions
public interface IScenarioService
{
Task<Scenario> CreateScenario(string name, List<string> strategies, int? loopbackPeriod = 1);
Task<IEnumerable<Indicator>> GetIndicatorsAsync();
Task<Indicator> CreateStrategy(IndicatorType type,
string name,
int? period = null,
int? fastPeriods = null,
int? slowPeriods = null,
int? signalPeriods = null,
double? multiplier = null,
int? stochPeriods = null,
int? smoothPeriods = null,
int? cyclePeriods = null);
Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync();
Task<bool> UpdateScenario(string name, List<string> strategies, int? loopbackPeriod);
Task<bool> UpdateStrategy(IndicatorType indicatorType, string name, int? period, int? fastPeriods,
@@ -29,12 +17,12 @@ namespace Managing.Application.Abstractions
Task<IEnumerable<Scenario>> GetScenariosByUserAsync(User user);
Task<Scenario> CreateScenarioForUser(User user, string name, List<string> strategies, int? loopbackPeriod = 1);
Task<IEnumerable<Indicator>> GetIndicatorsByUserAsync(User user);
Task<IEnumerable<IndicatorBase>> GetIndicatorsByUserAsync(User user);
Task<bool> DeleteIndicatorByUser(User user, string name);
Task<bool> DeleteScenarioByUser(User user, string name);
Task<Scenario> GetScenarioByUser(User user, string name);
Task<Indicator> CreateIndicatorForUser(User user,
Task<IndicatorBase> CreateIndicatorForUser(User user,
IndicatorType type,
string name,
int? period = null,

View File

@@ -1,37 +1,28 @@
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies.Base;
using Managing.Domain.Indicators;
using Managing.Domain.Trades;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions
{
public interface ITradingBot : IBot
public interface ITradingBot
{
TradingBotConfig Config { get; set; }
Account Account { get; set; }
FixedSizeQueue<Candle> OptimizedCandles { get; set; }
HashSet<Candle> Candles { get; set; }
HashSet<LightSignal> Signals { get; set; }
List<Position> Positions { get; set; }
Dictionary<string, LightSignal> Signals { get; set; }
Dictionary<Guid, Position> Positions { get; set; }
Dictionary<DateTime, decimal> WalletBalances { get; set; }
Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; }
DateTime StartupTime { get; }
DateTime CreateDate { get; }
DateTime PreloadSince { get; set; }
int PreloadedCandlesCount { get; set; }
long ExecutionCount { get; set; }
Candle LastCandle { get; set; }
Task Run();
Task ToggleIsForWatchOnly();
int GetWinRate();
decimal GetProfitAndLoss();
decimal GetTotalFees();
void LoadScenario(Scenario scenario);
void UpdateIndicatorsValues();
Task LoadAccount();
Task<Position> OpenPositionManually(TradeDirection direction);

View File

@@ -21,7 +21,6 @@ namespace Managing.Application.Backtesting
private readonly IBacktestRepository _backtestRepository;
private readonly ILogger<Backtester> _logger;
private readonly IExchangeService _exchangeService;
private readonly IBotFactory _botFactory;
private readonly IScenarioService _scenarioService;
private readonly IAccountService _accountService;
private readonly IMessengerService _messengerService;
@@ -31,7 +30,6 @@ namespace Managing.Application.Backtesting
public Backtester(
IExchangeService exchangeService,
IBotFactory botFactory,
IBacktestRepository backtestRepository,
ILogger<Backtester> logger,
IScenarioService scenarioService,
@@ -42,7 +40,6 @@ namespace Managing.Application.Backtesting
IGrainFactory grainFactory)
{
_exchangeService = exchangeService;
_botFactory = botFactory;
_backtestRepository = backtestRepository;
_logger = logger;
_scenarioService = scenarioService;
@@ -99,7 +96,6 @@ namespace Managing.Application.Backtesting
try
{
var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate);
throw new Exception();
return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata);
}
catch (Exception ex)
@@ -145,7 +141,7 @@ namespace Managing.Application.Backtesting
/// <returns>The lightweight backtest results</returns>
public async Task<LightBacktestResponse> RunTradingBotBacktest(
TradingBotConfig config,
List<Candle> candles,
HashSet<Candle> candles,
User user = null,
bool withCandles = false,
string requestId = null,
@@ -159,7 +155,7 @@ namespace Managing.Application.Backtesting
/// </summary>
private async Task<LightBacktestResponse> RunBacktestWithCandles(
TradingBotConfig config,
List<Candle> candles,
HashSet<Candle> candles,
User user = null,
bool save = false,
bool withCandles = false,
@@ -201,7 +197,7 @@ namespace Managing.Application.Backtesting
return await _accountService.GetAccountByAccountName(config.AccountName, false, false);
}
private List<Candle> GetCandles(Ticker ticker, Timeframe timeframe,
private HashSet<Candle> GetCandles(Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate)
{
var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker,

View File

@@ -1,49 +0,0 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot;
using Managing.Domain.Bots;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Bots.Base
{
public class BotFactory : IBotFactory
{
private readonly IExchangeService _exchangeService;
private readonly IMessengerService _messengerService;
private readonly IAccountService _accountService;
private readonly ILogger<TradingBotBase> _tradingBotLogger;
private readonly ITradingService _tradingService;
private readonly IBotService _botService;
private readonly IBackupBotService _backupBotService;
public BotFactory(
IExchangeService exchangeService,
ILogger<TradingBotBase> tradingBotLogger,
IMessengerService messengerService,
IAccountService accountService,
ITradingService tradingService,
IBotService botService,
IBackupBotService backupBotService)
{
_tradingBotLogger = tradingBotLogger;
_exchangeService = exchangeService;
_messengerService = messengerService;
_accountService = accountService;
_tradingService = tradingService;
_botService = botService;
_backupBotService = backupBotService;
}
public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config)
{
// Delegate to BotService which handles scenario loading properly
return await _botService.CreateTradingBot(config);
}
public async Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config)
{
// Delegate to BotService which handles scenario loading properly
return await _botService.CreateBacktestTradingBot(config);
}
}
}

View File

@@ -0,0 +1,163 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots.Models;
using Managing.Domain.Statistics;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Bots.Grains;
public class AgentGrain : Grain, IAgentGrain, IRemindable
{
private readonly IPersistentState<AgentGrainState> _state;
private readonly ILogger<AgentGrain> _logger;
private readonly IBotService _botService;
private readonly IStatisticService _statisticService;
private const string _updateSummaryReminderName = "UpdateAgentSummary";
public AgentGrain(
[PersistentState("agent-state", "agent-store")]
IPersistentState<AgentGrainState> state,
ILogger<AgentGrain> logger,
IBotService botService,
IStatisticService statisticService)
{
_state = state;
_logger = logger;
_botService = botService;
_statisticService = statisticService;
}
public override Task OnActivateAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("AgentGrain activated for user {UserId}", this.GetPrimaryKeyLong());
return base.OnActivateAsync(cancellationToken);
}
public async Task InitializeAsync(int userId, string agentName)
{
_state.State.AgentName = agentName;
await _state.WriteStateAsync();
_logger.LogInformation("Agent {UserId} initialized with name {AgentName}", userId, agentName);
await RegisterReminderAsync();
}
private async Task RegisterReminderAsync()
{
try
{
// Register a reminder that fires every 5 minutes
await this.RegisterOrUpdateReminder(_updateSummaryReminderName, TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(1));
_logger.LogInformation("Reminder registered for agent {UserId} to update summary every 5 minutes",
this.GetPrimaryKeyLong());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register reminder for agent {UserId}", this.GetPrimaryKeyLong());
}
}
public async Task ReceiveReminder(string reminderName, TickStatus status)
{
if (reminderName == _updateSummaryReminderName)
{
try
{
_logger.LogInformation("Reminder triggered for agent {UserId} to update summary",
this.GetPrimaryKeyLong());
await UpdateSummary();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating agent summary from reminder for user {UserId}",
this.GetPrimaryKeyLong());
}
}
}
public async Task UpdateSummary()
{
try
{
// Get all bots for this agent
var bots = await _botService.GetBotsByIdsAsync(_state.State.BotIds);
// Calculate aggregated statistics from bot data
var totalPnL = bots.Sum(b => b.Pnl);
var totalWins = bots.Sum(b => b.TradeWins);
var totalLosses = bots.Sum(b => b.TradeLosses);
// Calculate ROI based on total volume traded with proper division by zero handling
var totalVolume = bots.Sum(b => b.Volume);
decimal totalROI;
if (totalVolume > 0)
{
totalROI = (totalPnL / totalVolume) * 100;
}
else if (totalVolume == 0 && totalPnL == 0)
{
// No trading activity yet
totalROI = 0;
}
else if (totalVolume == 0 && totalPnL != 0)
{
// Edge case: PnL exists but no volume (shouldn't happen in normal cases)
_logger.LogWarning("Agent {UserId} has PnL {PnL} but zero volume", this.GetPrimaryKeyLong(), totalPnL);
totalROI = 0;
}
else
{
// Fallback for any other edge cases
totalROI = 0;
}
// Calculate Runtime based on the farthest date from bot startup times
DateTime? runtime = null;
if (bots.Any())
{
runtime = bots.Max(b => b.StartupTime);
}
var summary = new AgentSummary
{
UserId = (int)this.GetPrimaryKeyLong(),
AgentName = _state.State.AgentName,
TotalPnL = totalPnL,
Wins = totalWins,
Losses = totalLosses,
TotalROI = totalROI,
Runtime = runtime,
ActiveStrategiesCount = bots.Count(b => b.Status == BotStatus.Up),
TotalVolume = totalVolume,
};
// Save summary to database
await _statisticService.SaveOrUpdateAgentSummary(summary);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calculating agent summary for user {UserId}", this.GetPrimaryKeyLong());
}
}
public async Task RegisterBotAsync(Guid botId)
{
if (_state.State.BotIds.Add(botId))
{
await _state.WriteStateAsync();
_logger.LogInformation("Bot {BotId} registered to Agent {UserId}", botId, this.GetPrimaryKeyLong());
}
}
public async Task UnregisterBotAsync(Guid botId)
{
if (_state.State.BotIds.Remove(botId))
{
await _state.WriteStateAsync();
_logger.LogInformation("Bot {BotId} unregistered from Agent {UserId}", botId, this.GetPrimaryKeyLong());
}
}
}

View File

@@ -1,7 +1,5 @@
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Models;
using Managing.Application.Abstractions.Repositories;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
@@ -9,7 +7,6 @@ using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -52,7 +49,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// <returns>The complete backtest result</returns>
public async Task<LightBacktest> RunBacktestAsync(
TradingBotConfig config,
List<Candle> candles,
HashSet<Candle> candles,
User user = null,
bool save = false,
bool withCandles = false,
@@ -66,7 +63,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Create a fresh TradingBotBase instance for this backtest
var tradingBot = await CreateTradingBotInstance(config);
tradingBot.Start();
tradingBot.Account = user.Accounts.First(a => a.Name == config.AccountName);
var totalCandles = candles.Count;
var currentCandle = 0;
@@ -79,11 +76,15 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
tradingBot.WalletBalances.Clear();
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
var fixedCandles = new HashSet<Candle>();
// Process all candles following the exact pattern from GetBacktestingResult
foreach (var candle in candles)
{
tradingBot.OptimizedCandles.Enqueue(candle);
tradingBot.Candles.Add(candle);
fixedCandles.Add(candle);
tradingBot.LastCandle = candle;
// Update signals manually only for backtesting
await tradingBot.UpdateSignals(fixedCandles);
await tradingBot.Run();
currentCandle++;
@@ -97,43 +98,16 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
currentWalletBalance, currentCandle, totalCandles, candle.Date.ToString("yyyy-MM-dd HH:mm"));
break;
}
// 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;
}
}
_logger.LogInformation("Backtest processing completed. Calculating final results...");
// Set all candles for final calculations
tradingBot.Candles = new HashSet<Candle>(candles);
// Only calculate indicators values if withCandles is true
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues = null;
if (withCandles)
{
// Convert LightScenario back to full Scenario for indicator calculations
var fullScenario = config.Scenario.ToScenario();
indicatorsValues = GetIndicatorsValues(fullScenario.Indicators, candles);
}
// Calculate final results following the exact pattern from GetBacktestingResult
var finalPnl = tradingBot.GetProfitAndLoss();
var winRate = tradingBot.GetWinRate();
var stats = TradingHelpers.GetStatistics(tradingBot.WalletBalances);
var growthPercentage =
TradingHelpers.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, finalPnl);
var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last());
var hodlPercentage = TradingHelpers.GetHodlPercentage(candles.First(), candles.Last());
var fees = tradingBot.GetTotalFees();
var scoringParams = new BacktestScoringParams(
@@ -148,7 +122,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
maxDrawdown: stats.MaxDrawdown,
initialBalance: tradingBot.WalletBalances.FirstOrDefault().Value,
tradingBalance: config.BotTradingBalance,
startDate: candles[0].Date,
startDate: candles.First().Date,
endDate: candles.Last().Date,
timeframe: config.Timeframe,
moneyManagement: config.MoneyManagement
@@ -160,8 +134,8 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
var finalRequestId = requestId ?? Guid.NewGuid().ToString();
// Create backtest result with conditional candles and indicators values
var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals.ToList(),
withCandles ? candles : new List<Candle>())
var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals,
withCandles ? candles : new HashSet<Candle>())
{
FinalPnl = finalPnl,
WinRate = winRate,
@@ -170,9 +144,6 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
Fees = fees,
WalletBalances = tradingBot.WalletBalances.ToList(),
Statistics = stats,
IndicatorsValues = withCandles
? AggregateValues(indicatorsValues, tradingBot.IndicatorsValues)
: new Dictionary<IndicatorType, IndicatorsResultBase>(),
Score = scoringResult.Score,
ScoreMessage = scoringResult.GenerateSummaryMessage(),
Id = Guid.NewGuid().ToString(),
@@ -190,9 +161,6 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Send notification if backtest meets criteria
await SendBacktestNotificationIfCriteriaMet(result);
// Clean up the trading bot instance
tradingBot.Stop();
// Convert Backtest to LightBacktest for safe Orleans serialization
return ConvertToLightBacktest(result);
}
@@ -241,13 +209,6 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Create the trading bot instance
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
// Set the user if available
if (user != null)
{
tradingBot.User = user;
}
return tradingBot;
}
@@ -276,8 +237,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// Aggregates indicator values (following Backtester.cs pattern)
/// </summary>
private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues(
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues,
Dictionary<IndicatorType, IndicatorsResultBase> botStrategiesValues)
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues)
{
var result = new Dictionary<IndicatorType, IndicatorsResultBase>();
foreach (var indicator in indicatorsValues)
@@ -291,23 +251,17 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// <summary>
/// Gets indicators values (following Backtester.cs pattern)
/// </summary>
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
List<Candle> candles)
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<LightIndicator> indicators,
HashSet<Candle> candles)
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var fixedCandles = new FixedSizeQueue<Candle>(10000);
foreach (var candle in candles)
{
fixedCandles.Enqueue(candle);
}
foreach (var indicator in indicators)
{
try
{
var s = ScenarioHelpers.BuildIndicator(indicator, 10000);
s.Candles = fixedCandles;
indicatorsValues[indicator.Type] = s.GetIndicatorValues();
var builtIndicator = ScenarioHelpers.BuildIndicator(indicator);
indicatorsValues[indicator.Type] = builtIndicator.GetIndicatorValues(candles);
}
catch (Exception e)
{
@@ -325,79 +279,4 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
_isDisposed = true;
}
}
public Task<BacktestProgress> GetBacktestProgressAsync()
{
throw new NotImplementedException();
}
public Task StartAsync()
{
throw new NotImplementedException();
}
public Task StopAsync()
{
throw new NotImplementedException();
}
public Task<BotStatus> GetStatusAsync()
{
throw new NotImplementedException();
}
public Task<TradingBotConfig> GetConfigurationAsync()
{
throw new NotImplementedException();
}
public Task<Position> OpenPositionManuallyAsync(TradeDirection direction)
{
throw new NotImplementedException();
}
public Task ToggleIsForWatchOnlyAsync()
{
throw new NotImplementedException();
}
public Task<TradingBotResponse> GetBotDataAsync()
{
throw new NotImplementedException();
}
public Task LoadBackupAsync(BotBackup backup)
{
throw new NotImplementedException();
}
public Task SaveBackupAsync()
{
throw new NotImplementedException();
}
public Task<decimal> GetProfitAndLossAsync()
{
throw new NotImplementedException();
}
public Task<int> GetWinRateAsync()
{
throw new NotImplementedException();
}
public Task<long> GetExecutionCountAsync()
{
throw new NotImplementedException();
}
public Task<DateTime> GetStartupTimeAsync()
{
throw new NotImplementedException();
}
public Task<DateTime> GetCreateDateAsync()
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,179 @@
using Managing.Application.Abstractions.Grains;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Bots.Grains;
/// <summary>
/// Orleans grain for LiveBotRegistry operations.
/// This grain acts as a central, durable directory for all LiveTradingBot grains.
/// It maintains a persistent, up-to-date list of all known bot IDs and their status.
/// </summary>
public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain
{
private readonly IPersistentState<BotRegistryState> _state;
private readonly ILogger<LiveBotRegistryGrain> _logger;
public LiveBotRegistryGrain(
[PersistentState("bot-registry", "registry-store")]
IPersistentState<BotRegistryState> state,
ILogger<LiveBotRegistryGrain> logger)
{
_state = state;
_logger = logger;
}
public override async Task OnActivateAsync(CancellationToken cancellationToken)
{
await base.OnActivateAsync(cancellationToken);
_logger.LogInformation("LiveBotRegistryGrain activated with {TotalBots} bots registered",
_state.State.TotalBotsCount);
}
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
{
_logger.LogInformation("LiveBotRegistryGrain deactivating. Reason: {Reason}. Total bots: {TotalBots}",
reason.Description, _state.State.TotalBotsCount);
await base.OnDeactivateAsync(reason, cancellationToken);
}
public async Task RegisterBot(Guid identifier, int userId)
{
try
{
if (_state.State.Bots.ContainsKey(identifier))
{
_logger.LogWarning("Bot {Identifier} is already registered in the registry", identifier);
return;
}
var entry = new BotRegistryEntry(identifier, userId);
_state.State.Bots[identifier] = entry;
// O(1) FIX: Increment the counters
_state.State.TotalBotsCount++;
_state.State.ActiveBotsCount++;
_state.State.LastUpdated = DateTime.UtcNow;
await _state.WriteStateAsync();
_logger.LogInformation(
"Bot {Identifier} registered successfully for user {UserId}. Total bots: {TotalBots}, Active bots: {ActiveBots}",
identifier, userId, _state.State.TotalBotsCount, _state.State.ActiveBotsCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register bot {Identifier} for user {UserId}", identifier, userId);
throw;
}
}
public async Task UnregisterBot(Guid identifier)
{
try
{
if (!_state.State.Bots.TryGetValue(identifier, out var entryToRemove))
{
_logger.LogWarning("Bot {Identifier} is not registered in the registry", identifier);
return;
}
_state.State.Bots.Remove(identifier);
// O(1) FIX: Decrement the counters based on the removed entry's status
_state.State.TotalBotsCount--;
if (entryToRemove.Status == BotStatus.Up)
{
_state.State.ActiveBotsCount--;
}
_state.State.LastUpdated = DateTime.UtcNow;
await _state.WriteStateAsync();
_logger.LogInformation(
"Bot {Identifier} unregistered successfully from user {UserId}. Total bots: {TotalBots}",
identifier, entryToRemove.UserId, _state.State.TotalBotsCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to unregister bot {Identifier}", identifier);
throw;
}
}
public Task<List<BotRegistryEntry>> GetAllBots()
{
var bots = _state.State.Bots.Values.ToList();
_logger.LogDebug("Retrieved {Count} bots from registry", bots.Count);
return Task.FromResult(bots);
}
public Task<List<BotRegistryEntry>> GetBotsForUser(int userId)
{
var userBots = _state.State.Bots.Values
.Where(b => b.UserId == userId)
.ToList();
_logger.LogDebug("Retrieved {Count} bots for user {UserId}", userBots.Count, userId);
return Task.FromResult(userBots);
}
public async Task UpdateBotStatus(Guid identifier, BotStatus newStatus)
{
try
{
if (!_state.State.Bots.TryGetValue(identifier, out var entry))
{
_logger.LogWarning("Bot {Identifier} is not registered in the registry, cannot update status",
identifier);
return;
}
var previousStatus = entry.Status;
if (previousStatus == newStatus)
{
_logger.LogDebug("Bot {Identifier} status unchanged ({Status}), skipping state write", identifier,
newStatus);
return;
}
// O(1) FIX: Conditionally adjust the counter
if (newStatus == BotStatus.Up && previousStatus != BotStatus.Up)
{
_state.State.ActiveBotsCount++;
}
else if (newStatus != BotStatus.Up && previousStatus == BotStatus.Up)
{
_state.State.ActiveBotsCount--;
}
entry.Status = newStatus;
entry.LastStatusUpdate = DateTime.UtcNow;
_state.State.LastUpdated = DateTime.UtcNow;
await _state.WriteStateAsync();
_logger.LogInformation(
"Bot {Identifier} status updated from {PreviousStatus} to {NewStatus}. Active bots: {ActiveBots}",
identifier, previousStatus, newStatus, _state.State.ActiveBotsCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update status for bot {Identifier} to {Status}", identifier, newStatus);
throw;
}
}
public Task<BotStatus> GetBotStatus(Guid identifier)
{
if (!_state.State.Bots.TryGetValue(identifier, out var entry))
{
_logger.LogWarning("Bot {Identifier} is not registered in the registry, returning None", identifier);
return Task.FromResult(BotStatus.None);
}
return Task.FromResult(entry.Status);
}
}

View File

@@ -1,7 +1,12 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Models;
using Managing.Core;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
@@ -13,125 +18,200 @@ namespace Managing.Application.Bots.Grains;
/// Uses composition with TradingBotBase to maintain separation of concerns.
/// This grain handles live trading scenarios with real-time market data and execution.
/// </summary>
public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
{
private readonly IPersistentState<TradingBotGrainState> _state;
private readonly ILogger<LiveTradingBotGrain> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private TradingBotBase? _tradingBot;
private IDisposable? _timer;
private bool _isDisposed = false;
private string _reminderName = "RebootReminder";
public LiveTradingBotGrain(
[PersistentState("live-trading-bot", "bot-store")]
IPersistentState<TradingBotGrainState> state,
ILogger<LiveTradingBotGrain> logger,
IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
_state = state;
}
public override async Task OnActivateAsync(CancellationToken cancellationToken)
{
await base.OnActivateAsync(cancellationToken);
_logger.LogInformation("LiveTradingBotGrain {GrainId} activated", this.GetPrimaryKey());
// Initialize the grain state if not already done
if (!State.IsInitialized)
{
State.Identifier = this.GetPrimaryKey().ToString();
State.CreateDate = DateTime.UtcNow;
State.Status = BotStatus.Down;
State.IsInitialized = true;
await WriteStateAsync();
}
await base.OnActivateAsync(cancellationToken);
await ResumeBotIfRequiredAsync();
}
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
{
_logger.LogInformation("LiveTradingBotGrain {GrainId} deactivating. Reason: {Reason}",
_logger.LogInformation("LiveTradingBotGrain {GrainId} deactivating. Reason: {Reason}",
this.GetPrimaryKey(), reason.Description);
// Stop the timer and trading bot
await StopAsync();
StopAndDisposeTimer();
await base.OnDeactivateAsync(reason, cancellationToken);
}
public async Task CreateAsync(TradingBotConfig config, User user)
{
if (config == null || string.IsNullOrEmpty(config.Name))
{
throw new InvalidOperationException("Bot configuration is not properly initialized");
}
if (config.IsForBacktest)
{
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
}
// This is a new bot, so we can assume it's not registered or active.
_state.State.Config = config;
_state.State.User = user;
_state.State.CreateDate = DateTime.UtcNow;
_state.State.Identifier = this.GetPrimaryKey();
await _state.WriteStateAsync();
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
await botRegistry.RegisterBot(_state.State.Identifier, user.Id);
// Register the bot with the user's agent
var agentGrain = GrainFactory.GetGrain<IAgentGrain>(user.Id);
await agentGrain.RegisterBotAsync(_state.State.Identifier);
await SaveBotAsync(BotStatus.None);
_logger.LogInformation("LiveTradingBotGrain {GrainId} created successfully", this.GetPrimaryKey());
}
private async Task ResumeBotIfRequiredAsync()
{
// Make the network call to the registry to get the source of truth
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
var botId = this.GetPrimaryKey();
var botStatus = await botRegistry.GetBotStatus(botId);
_logger.LogInformation("LiveTradingBotGrain {GrainId} activated. Registry status: {Status}",
botId, botStatus);
if (botStatus == BotStatus.Up && _tradingBot == null)
{
// Now, we can proceed with resuming the bot.
await ResumeBotInternalAsync();
}
}
private async Task ResumeBotInternalAsync()
{
// The core of this method remains idempotent thanks to the _tradingBot null check
if (_tradingBot != null)
{
return;
}
try
{
// Load state from persisted grain state
_tradingBot = CreateTradingBotInstance(_state.State.Config);
LoadStateIntoBase();
await _tradingBot.Start();
// Start the in-memory timer and persistent reminder
RegisterAndStartTimer();
await RegisterReminder();
await SaveBotAsync(BotStatus.Up);
_logger.LogInformation("LiveTradingBotGrain {GrainId} resumed successfully", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to resume bot {GrainId}", this.GetPrimaryKey());
// If resume fails, update the status to Down via the registry and stop
await UpdateBotRegistryStatus(BotStatus.Down);
throw;
}
}
public async Task StartAsync()
{
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
var botId = this.GetPrimaryKey();
var status = await botRegistry.GetBotStatus(botId);
// This is the new idempotency check, using the registry as the source of truth
if (status == BotStatus.Up && _tradingBot != null)
{
await RegisterReminder();
_logger.LogInformation("LiveTradingBotGrain {GrainId} is already running", this.GetPrimaryKey());
return;
}
try
{
if (State.Status == BotStatus.Up)
{
_logger.LogWarning("Bot {GrainId} is already running", this.GetPrimaryKey());
return;
}
if (State.Config == null || string.IsNullOrEmpty(State.Config.Name))
{
throw new InvalidOperationException("Bot configuration is not properly initialized");
}
// Ensure this is not a backtest configuration
if (State.Config.IsForBacktest)
{
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
}
// Create the TradingBotBase instance using composition
_tradingBot = await CreateTradingBotInstance();
// Load backup if available
if (State.User != null)
{
await LoadBackupFromState();
}
// Start the trading bot
_tradingBot.Start();
// Update state
State.Status = BotStatus.Up;
State.StartupTime = DateTime.UtcNow;
await WriteStateAsync();
// Start Orleans timer for periodic execution
StartTimer();
// Resume the bot using the internal logic
await ResumeBotInternalAsync();
// Update registry status (if it was previously 'Down')
await UpdateBotRegistryStatus(BotStatus.Up);
_logger.LogInformation("LiveTradingBotGrain {GrainId} started successfully", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
State.Status = BotStatus.Down;
await WriteStateAsync();
// Ensure registry status is correct even on failure
await UpdateBotRegistryStatus(BotStatus.Down);
throw;
}
}
private async Task RegisterReminder()
{
var reminderPeriod = TimeSpan.FromMinutes(2);
await this.RegisterOrUpdateReminder(_reminderName, reminderPeriod, reminderPeriod);
}
/// <summary>
/// Starts the Orleans timer for periodic bot execution
/// </summary>
private void RegisterAndStartTimer()
{
if (_tradingBot == null) return;
if (_timer != null) return;
_timer = this.RegisterGrainTimer(
async _ => await ExecuteBotCycle(),
new GrainTimerCreationOptions
{
Period = TimeSpan.FromMinutes(1),
DueTime = TimeSpan.FromMinutes(1),
KeepAlive = true
});
}
public async Task StopAsync()
{
// The check is now against the registry status
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
var botStatus = await botRegistry.GetBotStatus(this.GetPrimaryKey());
if (botStatus == BotStatus.Down)
{
_logger.LogInformation("Bot {GrainId} is already stopped", this.GetPrimaryKey());
return;
}
try
{
// Stop the timer
_timer?.Dispose();
_timer = null;
StopAndDisposeTimer();
await UnregisterReminder();
// Stop the trading bot
if (_tradingBot != null)
{
_tradingBot.Stop();
// Save backup before stopping
await SaveBackupToState();
_tradingBot = null;
}
// Update state
State.Status = BotStatus.Down;
await WriteStateAsync();
// Sync state from the volatile TradingBotBase before destroying it
SyncStateFromBase();
await _state.WriteStateAsync();
await SaveBotAsync(BotStatus.Down);
_tradingBot = null;
await UpdateBotRegistryStatus(BotStatus.Down);
_logger.LogInformation("LiveTradingBotGrain {GrainId} stopped successfully", this.GetPrimaryKey());
}
catch (Exception ex)
@@ -141,50 +221,88 @@ public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
}
}
public Task<BotStatus> GetStatusAsync()
private void StopAndDisposeTimer()
{
return Task.FromResult(State.Status);
if (_timer != null)
{
// Stop the timer
_timer?.Dispose();
_timer = null;
}
}
public Task<TradingBotConfig> GetConfigurationAsync()
private async Task UnregisterReminder()
{
return Task.FromResult(State.Config);
var reminder = await this.GetReminder(_reminderName);
if (reminder != null)
{
await this.UnregisterReminder(reminder);
}
}
public async Task<bool> UpdateConfigurationAsync(TradingBotConfig newConfig)
/// <summary>
/// Creates a TradingBotBase instance using composition
/// </summary>
private TradingBotBase CreateTradingBotInstance(TradingBotConfig config)
{
if (string.IsNullOrEmpty(config.AccountName))
{
throw new InvalidOperationException("Account name is required for live trading");
}
// Create the trading bot instance
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
// Restore state from grain state
tradingBot.Signals = _state.State.Signals;
tradingBot.Positions = _state.State.Positions;
tradingBot.WalletBalances = _state.State.WalletBalances;
tradingBot.PreloadedCandlesCount = _state.State.PreloadedCandlesCount;
tradingBot.ExecutionCount = _state.State.ExecutionCount;
tradingBot.Identifier = _state.State.Identifier;
tradingBot.LastPositionClosingTime = _state.State.LastPositionClosingTime;
return tradingBot;
}
/// <summary>
/// Executes one cycle of the trading bot
/// </summary>
private async Task ExecuteBotCycle()
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
return;
}
// Ensure this is not a backtest configuration
if (newConfig.IsForBacktest)
{
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
}
// Execute the bot's Run method
await _tradingBot.Run();
SyncStateFromBase();
await _state.WriteStateAsync();
// Update the configuration in the trading bot
var success = await _tradingBot.UpdateConfiguration(newConfig);
if (success)
{
// Update the state
State.Config = newConfig;
await WriteStateAsync();
}
return success;
// Save bot statistics to database
await SaveBotAsync(BotStatus.Up);
}
catch (ObjectDisposedException)
{
// Gracefully handle disposed service provider during shutdown
_logger.LogInformation("Service provider disposed during shutdown for LiveTradingBotGrain {GrainId}",
this.GetPrimaryKey());
return;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update configuration for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
return false;
// TODO : Turn off the bot if an error occurs
_logger.LogError(ex, "Error during bot execution cycle for LiveTradingBotGrain {GrainId}",
this.GetPrimaryKey());
}
}
public async Task<Position> OpenPositionManuallyAsync(TradeDirection direction)
{
try
@@ -198,12 +316,14 @@ public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to open manual position for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
_logger.LogError(ex, "Failed to open manual position for LiveTradingBotGrain {GrainId}",
this.GetPrimaryKey());
throw;
}
}
public async Task ToggleIsForWatchOnlyAsync()
public Task<TradingBotResponse> GetBotDataAsync()
{
try
{
@@ -212,39 +332,20 @@ public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
throw new InvalidOperationException("Bot is not running");
}
await _tradingBot.ToggleIsForWatchOnly();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to toggle watch-only mode for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task<TradingBotResponse> GetBotDataAsync()
{
try
{
if (_tradingBot == null)
return Task.FromResult(new TradingBotResponse
{
throw new InvalidOperationException("Bot is not running");
}
return new TradingBotResponse
{
Identifier = State.Identifier,
Name = State.Name,
Status = State.Status,
Config = State.Config,
Identifier = _state.State.Identifier,
Name = _state.State.Name,
Config = _state.State.Config,
Positions = _tradingBot.Positions,
Signals = _tradingBot.Signals.ToList(),
Signals = _tradingBot.Signals,
WalletBalances = _tradingBot.WalletBalances,
ProfitAndLoss = _tradingBot.GetProfitAndLoss(),
WinRate = _tradingBot.GetWinRate(),
ExecutionCount = _tradingBot.ExecutionCount,
StartupTime = State.StartupTime,
CreateDate = State.CreateDate
};
ExecutionCount = _state.State.ExecutionCount,
StartupTime = _state.State.StartupTime,
CreateDate = _state.State.CreateDate
});
}
catch (Exception ex)
{
@@ -253,244 +354,236 @@ public class LiveTradingBotGrain : Grain<TradingBotGrainState>, ITradingBotGrain
}
}
public async Task LoadBackupAsync(BotBackup backup)
private void LoadStateIntoBase()
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
if (_tradingBot == null)
_tradingBot = CreateTradingBotInstance(_state.State.Config);
_tradingBot.LoadBackup(backup);
// Update state from backup
State.User = backup.User;
State.Identifier = backup.Identifier;
State.Status = backup.LastStatus;
State.CreateDate = backup.Data.CreateDate;
State.StartupTime = backup.Data.StartupTime;
await WriteStateAsync();
if (_tradingBot == null) throw new InvalidOperationException("TradingBotBase instance could not be created");
_logger.LogInformation("Backup loaded successfully for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load backup for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
_tradingBot.Signals = _state.State.Signals;
_tradingBot.Positions = _state.State.Positions;
_tradingBot.WalletBalances = _state.State.WalletBalances;
_tradingBot.PreloadedCandlesCount = _state.State.PreloadedCandlesCount;
_tradingBot.ExecutionCount = _state.State.ExecutionCount;
_tradingBot.Identifier = _state.State.Identifier;
_tradingBot.LastPositionClosingTime = _state.State.LastPositionClosingTime;
}
public async Task SaveBackupAsync()
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
await _tradingBot.SaveBackup();
await SaveBackupToState();
_logger.LogInformation("Backup saved successfully for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save backup for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task<decimal> GetProfitAndLossAsync()
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
return _tradingBot.GetProfitAndLoss();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get P&L for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task<int> GetWinRateAsync()
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
return _tradingBot.GetWinRate();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get win rate for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public Task<long> GetExecutionCountAsync()
{
return Task.FromResult(State.ExecutionCount);
}
public Task<DateTime> GetStartupTimeAsync()
{
return Task.FromResult(State.StartupTime);
}
public Task<DateTime> GetCreateDateAsync()
{
return Task.FromResult(State.CreateDate);
}
/// <summary>
/// Creates a TradingBotBase instance using composition
/// </summary>
private async Task<TradingBotBase> CreateTradingBotInstance()
{
// Validate configuration for live trading
if (State.Config == null)
{
throw new InvalidOperationException("Bot configuration is not initialized");
}
if (State.Config.IsForBacktest)
{
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
}
if (string.IsNullOrEmpty(State.Config.AccountName))
{
throw new InvalidOperationException("Account name is required for live trading");
}
// Create the trading bot instance
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, State.Config);
// Set the user if available
if (State.User != null)
{
tradingBot.User = State.User;
}
return tradingBot;
}
/// <summary>
/// Starts the Orleans timer for periodic bot execution
/// </summary>
private void StartTimer()
private void SyncStateFromBase()
{
if (_tradingBot == null) return;
var interval = _tradingBot.Interval;
_timer = RegisterTimer(
async _ => await ExecuteBotCycle(),
null,
TimeSpan.FromMilliseconds(interval),
TimeSpan.FromMilliseconds(interval));
_state.State.Signals = _tradingBot.Signals;
_state.State.Positions = _tradingBot.Positions;
_state.State.WalletBalances = _tradingBot.WalletBalances;
_state.State.PreloadedCandlesCount = _tradingBot.PreloadedCandlesCount;
_state.State.ExecutionCount = _tradingBot.ExecutionCount;
_state.State.Identifier = _tradingBot.Identifier;
_state.State.LastPositionClosingTime = _tradingBot.LastPositionClosingTime;
_state.State.Config = _tradingBot.Config;
}
/// <summary>
/// Executes one cycle of the trading bot
/// </summary>
private async Task ExecuteBotCycle()
public async Task<bool> UpdateConfiguration(TradingBotConfig newConfig)
{
if (_tradingBot == null)
LoadStateIntoBase();
var result = await _tradingBot!.UpdateConfiguration(newConfig);
if (result)
{
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
var botId = this.GetPrimaryKey();
var status = await botRegistry.GetBotStatus(botId);
_state.State.Config = newConfig;
await _state.WriteStateAsync();
await SaveBotAsync(status);
}
return result;
}
public Task<Account> GetAccount()
{
return Task.FromResult(_tradingBot.Account);
}
public Task<TradingBotConfig> GetConfiguration()
{
return Task.FromResult(_state.State.Config);
}
public async Task<Position> ClosePositionAsync(Guid positionId)
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
if (!_tradingBot.Positions.TryGetValue(positionId, out var position))
{
throw new InvalidOperationException($"Position with ID {positionId} not found");
}
var signal = _tradingBot.Signals.TryGetValue(position.SignalIdentifier, out var foundSignal)
? foundSignal
: null;
if (signal == null)
{
throw new InvalidOperationException($"Signal with ID {position.SignalIdentifier} not found");
}
await _tradingBot.CloseTrade(signal, position, position.Open, _tradingBot.LastCandle.Close, true);
return position;
}
public async Task RestartAsync()
{
await StopAsync();
await StartAsync();
}
public async Task DeleteAsync()
{
try
{
if (_tradingBot == null || State.Status != BotStatus.Up || _isDisposed)
// Stop the bot first if it's running
await StopAsync();
// Unregister from the bot registry
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
await botRegistry.UnregisterBot(_state.State.Identifier);
// Unregister from the user's agent
if (_state.State.User != null)
{
return;
var agentGrain = GrainFactory.GetGrain<IAgentGrain>(_state.State.User.Id);
await agentGrain.UnregisterBotAsync(_state.State.Identifier);
}
// Execute the bot's Run method
await _tradingBot.Run();
// Update execution count
State.ExecutionCount++;
await SaveBackupToState();
}
catch (ObjectDisposedException)
{
// Gracefully handle disposed service provider during shutdown
_logger.LogInformation("Service provider disposed during shutdown for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
return;
// Clear the state
_tradingBot = null;
await _state.ClearStateAsync();
_logger.LogInformation("LiveTradingBotGrain {GrainId} deleted successfully", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during bot execution cycle for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
_logger.LogError(ex, "Failed to delete LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
/// <summary>
/// Saves the current bot state to Orleans state storage
/// Updates the bot status in the central BotRegistry
/// </summary>
private async Task SaveBackupToState()
private async Task UpdateBotRegistryStatus(BotStatus status)
{
if (_tradingBot == null) return;
try
{
// Sync state from TradingBotBase
State.Config = _tradingBot.Config;
State.Signals = _tradingBot.Signals;
State.Positions = _tradingBot.Positions;
State.WalletBalances = _tradingBot.WalletBalances;
State.PreloadSince = _tradingBot.PreloadSince;
State.PreloadedCandlesCount = _tradingBot.PreloadedCandlesCount;
State.Interval = _tradingBot.Interval;
State.MaxSignals = _tradingBot._maxSignals;
State.LastBackupTime = DateTime.UtcNow;
await WriteStateAsync();
var botRegistry = GrainFactory.GetGrain<ILiveBotRegistryGrain>(0);
var botId = this.GetPrimaryKey();
await botRegistry.UpdateBotStatus(botId, status);
_logger.LogDebug("Bot {BotId} status updated to {Status} in BotRegistry", botId, status);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
_logger.LogError(ex, "Failed to update bot {BotId} status to {Status} in BotRegistry", this.GetPrimaryKey(),
status);
}
}
public async Task ReceiveReminder(string reminderName, TickStatus status)
{
_logger.LogInformation("Reminder '{ReminderName}' received for grain {GrainId}.", reminderName,
this.GetPrimaryKey());
if (reminderName == _reminderName)
{
// Now a single, clean call to the method that handles all the logic
await ResumeBotIfRequiredAsync();
}
}
/// <summary>
/// Loads bot state from Orleans state storage
/// Saves the current bot statistics to the database using BotService
/// </summary>
private async Task LoadBackupFromState()
private async Task SaveBotAsync(BotStatus status)
{
if (_tradingBot == null) return;
try
{
// Sync state to TradingBotBase
_tradingBot.Signals = State.Signals;
_tradingBot.Positions = State.Positions;
_tradingBot.WalletBalances = State.WalletBalances;
_tradingBot.PreloadSince = State.PreloadSince;
_tradingBot.PreloadedCandlesCount = State.PreloadedCandlesCount;
_tradingBot.Config = State.Config;
Bot bot = null;
if (_tradingBot == null || _state.State.User == null)
{
// Save bot statistics for saved bots
bot = new Bot
{
Identifier = _state.State.Identifier,
Name = _state.State.Config.Name,
Ticker = _state.State.Config.Ticker,
User = _state.State.User,
Status = status,
CreateDate = _state.State.CreateDate,
StartupTime = _state.State.StartupTime,
TradeWins = 0,
TradeLosses = 0,
Pnl = 0,
Roi = 0,
Volume = 0,
Fees = 0
};
}
else
{
// Calculate statistics using TradingBox helpers
var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(_tradingBot.Positions);
var pnl = _tradingBot.GetProfitAndLoss();
var fees = _tradingBot.GetTotalFees();
var volume = TradingBox.GetTotalVolumeTraded(_tradingBot.Positions);
// Calculate ROI based on total investment
var totalInvestment = _tradingBot.Positions.Values
.Sum(p => p.Open.Quantity * p.Open.Price);
var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0;
// Create complete Bot object with all statistics
bot = new Bot
{
Identifier = _state.State.Identifier,
Name = _state.State.Config.Name,
Ticker = _state.State.Config.Ticker,
User = _state.State.User,
Status = status,
StartupTime = _state.State.StartupTime,
CreateDate = _state.State.CreateDate,
TradeWins = tradeWins,
TradeLosses = tradeLosses,
Pnl = pnl,
Roi = roi,
Volume = volume,
Fees = fees
};
}
// Pass the complete Bot object to BotService for saving
var success = await ServiceScopeHelpers.WithScopedService<IBotService, bool>(_scopeFactory,
async (botService) => { return await botService.SaveBotStatisticsAsync(bot); });
if (success)
{
_logger.LogDebug(
"Successfully saved bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
_state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
}
else
{
_logger.LogWarning("Failed to save bot statistics for bot {BotId}", _state.State.Identifier);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
_logger.LogError(ex, "Failed to save bot statistics for bot {BotId}", _state.State.Identifier);
}
}
public void Dispose()
{
if (!_isDisposed)
{
_timer?.Dispose();
_isDisposed = true;
}
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Managing.Application.Bots.Models
{
public class AgentGrainState
{
public string AgentName { get; set; }
public HashSet<Guid> BotIds { get; set; } = new HashSet<Guid>();
}
}

View File

@@ -1,58 +0,0 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot;
using Managing.Domain.Bots;
using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Managing.Application.Bots
{
public class SimpleBot : Bot
{
public readonly ILogger<TradingBotBase> Logger;
private readonly IBotService _botService;
private readonly IBackupBotService _backupBotService;
private Workflow _workflow;
public SimpleBot(string name, ILogger<TradingBotBase> logger, Workflow workflow, IBotService botService,
IBackupBotService backupBotService) :
base(name)
{
Logger = logger;
_botService = botService;
_backupBotService = backupBotService;
_workflow = workflow;
Interval = 100;
}
public override void Start()
{
Task.Run(() => InitWorker(Run));
base.Start();
}
public async Task Run()
{
await Task.Run(
async () =>
{
Logger.LogInformation(Identifier);
Logger.LogInformation(DateTime.Now.ToString());
await _workflow.Execute();
await SaveBackup();
Logger.LogInformation("__________________________________________________");
});
}
public override async Task SaveBackup()
{
var data = JsonConvert.SerializeObject(_workflow);
await _backupBotService.SaveOrUpdateBotBackup(User, Identifier, Status, new TradingBotBackup());
}
public override void LoadBackup(BotBackup backup)
{
_workflow = new Workflow();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
using Managing.Domain.Bots;
using Managing.Domain.Indicators;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using static Managing.Common.Enums;
namespace Managing.Application.Bots;
@@ -23,13 +23,13 @@ public class TradingBotGrainState
/// Collection of trading signals generated by the bot
/// </summary>
[Id(1)]
public HashSet<LightSignal> Signals { get; set; } = new();
public Dictionary<string, LightSignal> Signals { get; set; } = new();
/// <summary>
/// List of trading positions opened by the bot
/// Dictionary of trading positions opened by the bot, keyed by position identifier
/// </summary>
[Id(2)]
public List<Position> Positions { get; set; } = new();
public Dictionary<Guid, Position> Positions { get; set; } = new();
/// <summary>
/// Historical wallet balances tracked over time
@@ -37,12 +37,6 @@ public class TradingBotGrainState
[Id(3)]
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new();
/// <summary>
/// Current status of the bot (Running, Stopped, etc.)
/// </summary>
[Id(4)]
public BotStatus Status { get; set; } = BotStatus.Down;
/// <summary>
/// When the bot was started
/// </summary>
@@ -71,7 +65,7 @@ public class TradingBotGrainState
/// Bot identifier/name
/// </summary>
[Id(9)]
public string Identifier { get; set; } = string.Empty;
public Guid Identifier { get; set; } = Guid.Empty;
/// <summary>
/// Bot display name
@@ -114,4 +108,10 @@ public class TradingBotGrainState
/// </summary>
[Id(16)]
public DateTime LastBackupTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// Last time a position was closed (for cooldown period tracking)
/// </summary>
[Id(17)]
public DateTime? LastPositionClosingTime { get; set; }
}

View File

@@ -8,6 +8,7 @@ using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Risk;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -624,9 +625,9 @@ public class TradingBotChromosome : ChromosomeBase
return clone;
}
public List<GeneticIndicator> GetSelectedIndicators()
public List<LightIndicator> GetSelectedIndicators()
{
var selected = new List<GeneticIndicator>();
var selected = new List<LightIndicator>();
var genes = GetGenes();
// Check all indicator selection slots (genes 5 to 5+N-1 where N is number of eligible indicators)
@@ -634,7 +635,7 @@ public class TradingBotChromosome : ChromosomeBase
{
if (genes[5 + i].Value.ToString() == "1")
{
var indicator = new GeneticIndicator
var indicator = new LightIndicator(_eligibleIndicators[i].ToString(), _eligibleIndicators[i])
{
Type = _eligibleIndicators[i]
};
@@ -713,39 +714,24 @@ public class TradingBotChromosome : ChromosomeBase
// Enforce proper risk-reward constraints
var minStopLoss = 0.2; // Minimum 0.2% to cover fees
var maxStopLoss = takeProfit / 1.1; // Ensure risk-reward ratio is at least 1.1:1
// Generate a random stop loss between min and max
var randomStopLoss = GetRandomInRange((minStopLoss, maxStopLoss));
// Use the random value instead of clamping the original
stopLoss = randomStopLoss;
// Log the generated values (for debugging)
Console.WriteLine($"Generated: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit/stopLoss:F2}:1)");
Console.WriteLine($"Generated: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit / stopLoss:F2}:1)");
// Get loopback period from gene 4
var loopbackPeriod = Convert.ToInt32(genes[4].Value);
// Build scenario using selected indicators
var scenario = new Scenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod);
foreach (var geneticIndicator in selectedIndicators)
var scenario = new LightScenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod)
{
var indicator = ScenarioHelpers.BuildIndicator(
type: geneticIndicator.Type,
name: $"Genetic_{geneticIndicator.Type}_{Guid.NewGuid():N}",
period: geneticIndicator.Period,
fastPeriods: geneticIndicator.FastPeriods,
slowPeriods: geneticIndicator.SlowPeriods,
signalPeriods: geneticIndicator.SignalPeriods,
multiplier: geneticIndicator.Multiplier,
stochPeriods: geneticIndicator.StochPeriods,
smoothPeriods: geneticIndicator.SmoothPeriods,
cyclePeriods: geneticIndicator.CyclePeriods
);
scenario.AddIndicator(indicator);
}
Indicators = selectedIndicators
};
var mm = new MoneyManagement
{
@@ -776,7 +762,7 @@ public class TradingBotChromosome : ChromosomeBase
UseForPositionSizing = false,
UseForSignalFiltering = false,
UseForDynamicStopLoss = false,
Scenario = LightScenario.FromScenario(scenario),
Scenario = scenario,
MoneyManagement = mm,
RiskManagement = new RiskManagement
{
@@ -853,7 +839,7 @@ public class TradingBotChromosome : ChromosomeBase
ReplaceGene(1, new Gene(stopLoss));
// Log the initial values (for debugging)
Console.WriteLine($"Initialized: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit/stopLoss:F2}:1)");
Console.WriteLine($"Initialized: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit / stopLoss:F2}:1)");
// Initialize remaining genes normally
for (int i = 2; i < Length; i++)
@@ -863,22 +849,6 @@ public class TradingBotChromosome : ChromosomeBase
}
}
/// <summary>
/// Genetic indicator with parameters
/// </summary>
public class GeneticIndicator
{
public IndicatorType Type { get; set; }
public int? Period { get; set; }
public int? FastPeriods { get; set; }
public int? SlowPeriods { get; set; }
public int? SignalPeriods { get; set; }
public double? Multiplier { get; set; }
public int? StochPeriods { get; set; }
public int? SmoothPeriods { get; set; }
public int? CyclePeriods { get; set; }
}
/// <summary>
/// Multi-objective fitness function for trading bot optimization
/// </summary>
@@ -889,7 +859,8 @@ public class TradingBotFitness : IFitness
private GeneticAlgorithm _geneticAlgorithm;
private readonly ILogger<GeneticService> _logger;
public TradingBotFitness(IServiceScopeFactory serviceScopeFactory, GeneticRequest request, ILogger<GeneticService> logger)
public TradingBotFitness(IServiceScopeFactory serviceScopeFactory, GeneticRequest request,
ILogger<GeneticService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_request = request;

View File

@@ -1,52 +0,0 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Bots;
using Managing.Domain.Users;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
{
public interface IBackupBotService
{
Task<BotBackup> GetBotBackup(string identifier);
Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data);
}
public class BackupBotService : IBackupBotService
{
private readonly IBotRepository _botRepository;
public BackupBotService(IBotRepository botRepository)
{
_botRepository = botRepository;
}
public async Task<BotBackup> GetBotBackup(string identifier)
{
return await _botRepository.GetBotByIdentifierAsync(identifier);
}
public async Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data)
{
var backup = await GetBotBackup(identifier);
if (backup != null)
{
backup.LastStatus = status;
backup.Data = data;
await _botRepository.UpdateBackupBot(backup);
}
else
{
var botBackup = new BotBackup
{
LastStatus = status,
User = user,
Identifier = identifier,
Data = data
};
await _botRepository.InsertBotAsync(botBackup);
}
}
}
}

View File

@@ -1,12 +1,13 @@
using System.Collections.Concurrent;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots;
using Managing.Core;
using Managing.Domain.Bots;
using Managing.Domain.Scenarios;
using Managing.Domain.Users;
using Managing.Domain.Workflows;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
@@ -16,301 +17,136 @@ namespace Managing.Application.ManageBot
public class BotService : IBotService
{
private readonly IBotRepository _botRepository;
private readonly IExchangeService _exchangeService;
private readonly IMessengerService _messengerService;
private readonly IAccountService _accountService;
private readonly ILogger<TradingBotBase> _tradingBotLogger;
private readonly ITradingService _tradingService;
private readonly IMoneyManagementService _moneyManagementService;
private readonly IUserService _userService;
private readonly IBackupBotService _backupBotService;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IGrainFactory _grainFactory;
private readonly IServiceScopeFactory _scopeFactory;
private ConcurrentDictionary<string, BotTaskWrapper> _botTasks =
new ConcurrentDictionary<string, BotTaskWrapper>();
public BotService(IBotRepository botRepository, IExchangeService exchangeService,
IMessengerService messengerService, IAccountService accountService, ILogger<TradingBotBase> tradingBotLogger,
ITradingService tradingService, IMoneyManagementService moneyManagementService, IUserService userService,
IBackupBotService backupBotService, IServiceScopeFactory scopeFactory, IGrainFactory grainFactory)
public BotService(IBotRepository botRepository,
IMessengerService messengerService, ILogger<TradingBotBase> tradingBotLogger,
ITradingService tradingService, IGrainFactory grainFactory, IServiceScopeFactory scopeFactory)
{
_botRepository = botRepository;
_exchangeService = exchangeService;
_messengerService = messengerService;
_accountService = accountService;
_tradingBotLogger = tradingBotLogger;
_tradingService = tradingService;
_moneyManagementService = moneyManagementService;
_userService = userService;
_backupBotService = backupBotService;
_scopeFactory = scopeFactory;
_grainFactory = grainFactory;
_scopeFactory = scopeFactory;
}
public class BotTaskWrapper
{
public Task Task { get; private set; }
public Type BotType { get; private set; }
public object BotInstance { get; private set; }
public BotTaskWrapper(Task task, Type botType, object botInstance)
{
Task = task;
BotType = botType;
BotInstance = botInstance;
}
}
public void AddSimpleBotToCache(IBot bot)
{
var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot);
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
}
public void AddTradingBotToCache(ITradingBot bot)
{
var botTask = new BotTaskWrapper(Task.Run(() => bot.Start()), bot.GetType(), bot);
_botTasks.AddOrUpdate(bot.Identifier, botTask, (key, existingVal) => botTask);
}
private async Task InitBot(ITradingBot bot, BotBackup backupBot)
{
try
{
var user = await _userService.GetUser(backupBot.User.Name);
bot.User = user;
// Load backup data into the bot
bot.LoadBackup(backupBot);
// Only start the bot if the backup status is Up
if (backupBot.LastStatus == BotStatus.Up)
{
// Start the bot asynchronously without waiting for completion
_ = Task.Run(() => bot.Start());
}
else
{
// Keep the bot in Down status if it was originally Down
bot.Stop();
}
}
catch (Exception ex)
{
_tradingBotLogger.LogError(ex, "Error initializing bot {Identifier} from backup", backupBot.Identifier);
// Ensure the bot is stopped if initialization fails
bot.Stop();
throw;
}
}
public List<ITradingBot> GetActiveBots()
{
var bots = _botTasks.Values
.Where(wrapper => typeof(ITradingBot).IsAssignableFrom(wrapper.BotType))
.Select(wrapper => wrapper.BotInstance as ITradingBot)
.Where(bot => bot != null)
.ToList();
return bots;
}
public async Task<IEnumerable<BotBackup>> GetSavedBotsAsync()
public async Task<IEnumerable<Bot>> GetBotsAsync()
{
return await _botRepository.GetBotsAsync();
}
public async Task StartBotFromBackup(BotBackup backupBot)
public async Task<IEnumerable<Bot>> GetBotsByStatusAsync(BotStatus status)
{
object bot = null;
Task botTask = null;
return await _botRepository.GetBotsByStatusAsync(status);
}
var scalpingBotData = 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))
public async Task<BotStatus> StopBot(Guid identifier)
{
try
{
var moneyManagement = _moneyManagementService
.GetMoneyMangement(scalpingConfig.MoneyManagement.Name).Result;
if (moneyManagement != null)
{
scalpingConfig.MoneyManagement = moneyManagement;
}
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
await grain.StopAsync();
return BotStatus.Down;
}
// Ensure the scenario is properly loaded from database if needed
if (scalpingConfig.Scenario == null && !string.IsNullOrEmpty(scalpingConfig.ScenarioName))
catch (Exception e)
{
var scenario = await _tradingService.GetScenarioByNameAsync(scalpingConfig.ScenarioName);
if (scenario != null)
_tradingBotLogger.LogError(e, "Error stopping bot {Identifier}", identifier);
return BotStatus.Down;
}
}
public async Task<bool> DeleteBot(Guid identifier)
{
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
try
{
var config = await grain.GetConfiguration();
var account = await grain.GetAccount();
await grain.StopAsync();
await _botRepository.DeleteBot(identifier);
await grain.DeleteAsync();
var deleteMessage = $"🗑️ **Bot Deleted**\n\n" +
$"🎯 **Agent:** {account.User.AgentName}\n" +
$"🤖 **Bot Name:** {config.Name}\n" +
$"⏰ **Deleted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"⚠️ **Bot has been permanently deleted and all data removed.**";
await _messengerService.SendTradeMessage(deleteMessage, false, account.User);
return true;
}
catch (Exception e)
{
_tradingBotLogger.LogError(e, "Error deleting bot {Identifier}", identifier);
return false;
}
}
public async Task<BotStatus> RestartBot(Guid identifier)
{
try
{
var registryGrain = _grainFactory.GetGrain<ILiveBotRegistryGrain>(0);
var previousStatus = await registryGrain.GetBotStatus(identifier);
// If bot is already up, return the status directly
if (previousStatus == BotStatus.Up)
{
scalpingConfig.Scenario = LightScenario.FromScenario(scenario);
return BotStatus.Up;
}
var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
if (previousStatus == BotStatus.None)
{
// First time startup
await botGrain.StartAsync();
var grainState = await botGrain.GetBotDataAsync();
var account = await botGrain.GetAccount();
var startupMessage = $"🚀 **Bot Started**\n\n" +
$"🎯 **Agent:** {account.User.AgentName}\n" +
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
$"⏰ **Started At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n" +
$"🕐 **Startup Time:** {grainState.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"✅ **Bot has been successfully started and is now active.**";
await _messengerService.SendTradeMessage(startupMessage, false, account.User);
}
else
{
throw new ArgumentException(
$"Scenario '{scalpingConfig.ScenarioName}' not found in database when loading backup");
}
}
if (scalpingConfig.Scenario == null)
{
throw new ArgumentException(
"Scenario object must be provided or ScenarioName must be valid when loading backup");
}
// Ensure critical properties are set correctly for restored bots
scalpingConfig.IsForBacktest = false;
// IMPORTANT: Save the backup to database BEFORE creating the Orleans grain
// This ensures the backup exists when the grain tries to serialize it
await SaveOrUpdateBotBackup(backupBot.User, backupBot.Identifier, backupBot.LastStatus, backupBot.Data);
bot = await CreateTradingBot(scalpingConfig);
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
if (bot != null && botTask != null)
{
var botWrapper = new BotTaskWrapper(botTask, bot.GetType(), bot);
_botTasks.AddOrUpdate(backupBot.Identifier, botWrapper, (key, existingVal) => botWrapper);
}
}
public async Task<BotBackup> GetBotBackup(string identifier)
{
return await _botRepository.GetBotByIdentifierAsync(identifier);
}
public async Task SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, TradingBotBackup data)
{
var backup = await GetBotBackup(identifier);
if (backup != null)
{
backup.LastStatus = status;
backup.Data = data;
await _botRepository.UpdateBackupBot(backup);
}
else
{
var botBackup = new BotBackup
{
LastStatus = status,
User = user,
Identifier = identifier,
Data = data
};
await _botRepository.InsertBotAsync(botBackup);
}
}
public IBot CreateSimpleBot(string botName, Workflow workflow)
{
return new SimpleBot(botName, _tradingBotLogger, workflow, this, _backupBotService);
}
public async Task<string> StopBot(string identifier)
{
if (_botTasks.TryGetValue(identifier, out var botWrapper))
{
if (botWrapper.BotInstance is IBot bot)
{
await Task.Run(() =>
bot.Stop());
var stopMessage = $"🛑 **Bot Stopped**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" +
$"🤖 **Bot Name:** {bot.Name}\n" +
$"⏰ **Stopped At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"✅ **Bot has been safely stopped and is no longer active.**";
await _messengerService.SendTradeMessage(stopMessage, false, bot.User);
return bot.GetStatus();
}
}
return BotStatus.Down.ToString();
}
public async Task<bool> DeleteBot(string identifier)
{
if (_botTasks.TryRemove(identifier, out var botWrapper))
{
try
{
if (botWrapper.BotInstance is IBot bot)
{
await Task.Run(() =>
bot.Stop());
var deleteMessage = $"🗑️ **Bot Deleted**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" +
$"🤖 **Bot Name:** {bot.Name}\n" +
$"⏰ **Deleted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"⚠️ **Bot has been permanently deleted and all data removed.**";
await _messengerService.SendTradeMessage(deleteMessage, false, bot.User);
}
await _botRepository.DeleteBotBackup(identifier);
return true;
}
catch (Exception e)
{
Console.WriteLine(e);
return false;
}
}
return false;
}
public async Task<string> RestartBot(string identifier)
{
if (_botTasks.TryGetValue(identifier, out var botWrapper))
{
if (botWrapper.BotInstance is IBot bot)
{
// Stop the bot first to ensure clean state
bot.Stop();
// Small delay to ensure stop is complete
await Task.Delay(100);
// Restart the bot (this will update StartupTime)
bot.Restart();
// Start the bot asynchronously without waiting for completion
_ = Task.Run(() => bot.Start());
// Restart (bot was previously down)
await botGrain.RestartAsync();
var grainState = await botGrain.GetBotDataAsync();
var account = await botGrain.GetAccount();
var restartMessage = $"🔄 **Bot Restarted**\n\n" +
$"🎯 **Agent:** {bot.User.AgentName}\n" +
$"🤖 **Bot Name:** {bot.Name}\n" +
$"🎯 **Agent:** {account.User.AgentName}\n" +
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
$"⏰ **Restarted At:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n" +
$"🕐 **New Startup Time:** {bot.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"🕐 **New Startup Time:** {grainState.StartupTime:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"🚀 **Bot has been successfully restarted and is now active.**";
await _messengerService.SendTradeMessage(restartMessage, false, bot.User);
return bot.GetStatus();
await _messengerService.SendTradeMessage(restartMessage, false, account.User);
}
}
return BotStatus.Down.ToString();
return BotStatus.Up;
}
catch (Exception e)
{
_tradingBotLogger.LogError(e, "Error restarting bot {Identifier}", identifier);
return BotStatus.Down;
}
}
public async Task ToggleIsForWatchingOnly(string identifier)
private async Task<Bot> GetBot(Guid identifier)
{
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) &&
botTaskWrapper.BotInstance is ITradingBot tradingBot)
{
await tradingBot.ToggleIsForWatchOnly();
}
var bot = await _botRepository.GetBotByIdentifierAsync(identifier);
return bot;
}
/// <summary>
@@ -319,128 +155,198 @@ namespace Managing.Application.ManageBot
/// <param name="identifier">The bot identifier</param>
/// <param name="newConfig">The new configuration to apply</param>
/// <returns>True if the configuration was successfully updated, false otherwise</returns>
public async Task<bool> UpdateBotConfiguration(string identifier, TradingBotConfig newConfig)
public async Task<bool> UpdateBotConfiguration(Guid identifier, TradingBotConfig newConfig)
{
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) &&
botTaskWrapper.BotInstance is TradingBotBase tradingBot)
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
// Ensure the scenario is properly loaded from database if needed
if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName))
{
// Ensure the scenario is properly loaded from database if needed
if (newConfig.Scenario == null && !string.IsNullOrEmpty(newConfig.ScenarioName))
var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName);
if (scenario != null)
{
var scenario = await _tradingService.GetScenarioByNameAsync(newConfig.ScenarioName);
if (scenario != null)
{
newConfig.Scenario = LightScenario.FromScenario(scenario);
}
else
{
throw new ArgumentException(
$"Scenario '{newConfig.ScenarioName}' not found in database when updating configuration");
}
newConfig.Scenario = LightScenario.FromScenario(scenario);
}
if (newConfig.Scenario == null)
else
{
throw new ArgumentException(
"Scenario object must be provided or ScenarioName must be valid when updating configuration");
}
// Check if the bot name is changing
if (newConfig.Name != identifier && !string.IsNullOrEmpty(newConfig.Name))
{
// Check if new name already exists
if (_botTasks.ContainsKey(newConfig.Name))
{
return false; // New name already in use
}
// 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)
{
// Delete old backup
await _botRepository.DeleteBotBackup(identifier);
// Save new backup will be handled by the bot's SaveBackup method
}
}
}
return updateResult;
}
else
{
// No name change, just update configuration
return await tradingBot.UpdateConfiguration(newConfig);
$"Scenario '{newConfig.ScenarioName}' not found in database when updating configuration");
}
}
return false;
if (newConfig.Scenario == null)
{
throw new ArgumentException(
"Scenario object must be provided or ScenarioName must be valid when updating configuration");
}
return await grain.UpdateConfiguration(newConfig);
}
public async Task<ITradingBot> CreateTradingBot(TradingBotConfig config)
public async Task<TradingBotConfig> GetBotConfig(Guid identifier)
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
{
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null)
{
config.Scenario = LightScenario.FromScenario(scenario);
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
}
}
if (config.Scenario == null)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
}
// For now, use TradingBot for both live trading and backtesting
// TODO: Implement Orleans grain for live trading when ready
if (!config.IsForBacktest)
{
// Ensure critical properties are set correctly for live trading
config.IsForBacktest = false;
}
return new TradingBotBase(_tradingBotLogger, _scopeFactory, config);
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
return await grain.GetConfiguration();
}
public async Task<ITradingBot> CreateBacktestTradingBot(TradingBotConfig config)
public async Task<IEnumerable<string>> GetActiveBotsNamesAsync()
{
// Ensure the scenario is properly loaded from database if needed
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
var bots = await _botRepository.GetBotsByStatusAsync(BotStatus.Up);
return bots.Select(b => b.Name);
}
public async Task<IEnumerable<Bot>> GetBotsByUser(int id)
{
return await _botRepository.GetBotsByUserIdAsync(id);
}
public async Task<IEnumerable<Bot>> GetBotsByIdsAsync(IEnumerable<Guid> botIds)
{
return await _botRepository.GetBotsByIdsAsync(botIds);
}
public async Task<Bot> GetBotByName(string name)
{
return await _botRepository.GetBotByNameAsync(name);
}
public async Task<Bot> GetBotByIdentifier(Guid identifier)
{
return await _botRepository.GetBotByIdentifierAsync(identifier);
}
public async Task<Position> OpenPositionManuallyAsync(Guid identifier, TradeDirection direction)
{
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
return await grain.OpenPositionManuallyAsync(direction);
}
public async Task<Position> ClosePositionAsync(Guid identifier, Guid positionId)
{
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
return await grain.ClosePositionAsync(positionId);
}
public async Task<bool> UpdateBotStatisticsAsync(Guid identifier)
{
try
{
var scenario = await _tradingService.GetScenarioByNameAsync(config.ScenarioName);
if (scenario != null)
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
var botData = await grain.GetBotDataAsync();
// Get the current bot from database
var existingBot = await _botRepository.GetBotByIdentifierAsync(identifier);
if (existingBot == null)
{
config.Scenario = LightScenario.FromScenario(scenario);
_tradingBotLogger.LogWarning("Bot {Identifier} not found in database for statistics update",
identifier);
return false;
}
// Calculate statistics using TradingBox helpers
var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(botData.Positions);
var pnl = botData.ProfitAndLoss;
var fees = botData.Positions.Values.Sum(p =>
{
if (p.Open.Price > 0 && p.Open.Quantity > 0)
{
var positionSizeUsd = (p.Open.Price * p.Open.Quantity) * p.Open.Leverage;
var uiFeeRate = 0.001m; // 0.1%
var uiFeeOpen = positionSizeUsd * uiFeeRate;
var networkFeeForOpening = 0.50m;
return uiFeeOpen + networkFeeForOpening;
}
return 0;
});
var volume = TradingBox.GetTotalVolumeTraded(botData.Positions);
// Calculate ROI based on total investment
var totalInvestment = botData.Positions.Values
.Where(p => p.IsFinished())
.Sum(p => p.Open.Quantity * p.Open.Price);
var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0;
// Update bot statistics
existingBot.TradeWins = tradeWins;
existingBot.TradeLosses = tradeLosses;
existingBot.Pnl = pnl;
existingBot.Roi = roi;
existingBot.Volume = volume;
existingBot.Fees = fees;
// Use the new SaveBotStatisticsAsync method
return await SaveBotStatisticsAsync(existingBot);
}
catch (Exception e)
{
_tradingBotLogger.LogError(e, "Error updating bot statistics for {Identifier}", identifier);
return false;
}
}
public async Task<bool> SaveBotStatisticsAsync(Bot bot)
{
try
{
if (bot == null)
{
_tradingBotLogger.LogWarning("Cannot save bot statistics: bot object is null");
return false;
}
// Check if bot already exists in database
var existingBot = await _botRepository.GetBotByIdentifierAsync(bot.Identifier);
if (existingBot != null)
{
// Update existing bot
await _botRepository.UpdateBot(bot);
_tradingBotLogger.LogDebug(
"Updated bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
bot.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
}
else
{
throw new ArgumentException($"Scenario '{config.ScenarioName}' not found in database");
// Insert new bot
await _botRepository.InsertBotAsync(bot);
_tradingBotLogger.LogInformation(
"Created new bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
bot.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
}
}
if (config.Scenario == null)
return true;
}
catch (Exception e)
{
throw new ArgumentException("Scenario object must be provided or ScenarioName must be valid");
_tradingBotLogger.LogError(e, "Error saving bot statistics for bot {BotId}", bot?.Identifier);
return false;
}
}
config.IsForBacktest = true;
return new TradingBotBase(_tradingBotLogger, _scopeFactory, config);
public async Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
int pageNumber,
int pageSize,
BotStatus? status = null,
string? name = null,
string? ticker = null,
string? agentName = null,
string sortBy = "CreateDate",
string sortDirection = "Desc")
{
return await ServiceScopeHelpers.WithScopedService<IBotRepository, (IEnumerable<Bot> Bots, int TotalCount)>(
_scopeFactory,
async repo =>
{
return await repo.GetBotsPaginatedAsync(
pageNumber,
pageSize,
status,
name,
ticker,
agentName,
sortBy,
sortDirection);
});
}
}
}

View File

@@ -4,10 +4,10 @@ namespace Managing.Application.ManageBot.Commands;
public class DeleteBotCommand : IRequest<bool>
{
public string Name { get; }
public Guid Identifier { get; }
public DeleteBotCommand(string name)
public DeleteBotCommand(Guid identifier)
{
Name = name;
Identifier = identifier;
}
}

View File

@@ -1,12 +1,16 @@
using Managing.Application.Abstractions;
using Managing.Domain.Bots;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
public class GetActiveBotsCommand : IRequest<List<ITradingBot>>
public class GetBotsByStatusCommand : IRequest<IEnumerable<Bot>>
{
public GetActiveBotsCommand()
public BotStatus Status { get; }
public GetBotsByStatusCommand(BotStatus status)
{
Status = status;
}
}
}

View File

@@ -0,0 +1,21 @@
using Managing.Domain.Statistics;
using MediatR;
namespace Managing.Application.ManageBot.Commands
{
/// <summary>
/// Command to retrieve all agent summaries with complete data
/// </summary>
public class GetAllAgentSummariesCommand : IRequest<IEnumerable<AgentSummary>>
{
/// <summary>
/// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
/// </summary>
public string TimeFilter { get; }
public GetAllAgentSummariesCommand(string timeFilter = "Total")
{
TimeFilter = timeFilter;
}
}
}

View File

@@ -1,4 +1,4 @@
using Managing.Application.Abstractions;
using Managing.Domain.Bots;
using Managing.Domain.Users;
using MediatR;
@@ -7,7 +7,7 @@ namespace Managing.Application.ManageBot.Commands
/// <summary>
/// Command to retrieve all active agents and their strategies
/// </summary>
public class GetAllAgentsCommand : IRequest<Dictionary<User, List<ITradingBot>>>
public class GetAllAgentsCommand : IRequest<Dictionary<User, List<Bot>>>
{
/// <summary>
/// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)

View File

@@ -0,0 +1,18 @@
using Managing.Domain.Bots;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
public class GetBotsByUserAndStatusCommand : IRequest<IEnumerable<Bot>>
{
public int UserId { get; }
public BotStatus Status { get; }
public GetBotsByUserAndStatusCommand(int userId, BotStatus status)
{
UserId = userId;
Status = status;
}
}
}

View File

@@ -0,0 +1,51 @@
using Managing.Domain.Statistics;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
/// <summary>
/// Command to retrieve paginated agent summaries with sorting and filtering
/// </summary>
public class GetPaginatedAgentSummariesCommand : IRequest<(IEnumerable<AgentSummary> Results, int TotalCount)>
{
/// <summary>
/// Page number (1-based)
/// </summary>
public int Page { get; }
/// <summary>
/// Number of items per page
/// </summary>
public int PageSize { get; }
/// <summary>
/// Field to sort by
/// </summary>
public SortableFields SortBy { get; }
/// <summary>
/// Sort order (asc or desc)
/// </summary>
public string SortOrder { get; }
/// <summary>
/// Optional list of agent names to filter by
/// </summary>
public IEnumerable<string>? AgentNames { get; }
public GetPaginatedAgentSummariesCommand(
int page = 1,
int pageSize = 10,
SortableFields sortBy = SortableFields.TotalPnL,
string sortOrder = "desc",
IEnumerable<string>? agentNames = null)
{
Page = page;
PageSize = pageSize;
SortBy = sortBy;
SortOrder = sortOrder;
AgentNames = agentNames;
}
}
}

View File

@@ -1,4 +1,4 @@
using Managing.Application.Abstractions;
using Managing.Domain.Bots;
using MediatR;
namespace Managing.Application.ManageBot.Commands
@@ -6,13 +6,13 @@ namespace Managing.Application.ManageBot.Commands
/// <summary>
/// Command to retrieve all strategies owned by a specific user
/// </summary>
public class GetUserStrategiesCommand : IRequest<List<ITradingBot>>
public class GetUserStrategiesCommand : IRequest<List<Bot>>
{
public string UserName { get; }
public string AgentName { get; }
public GetUserStrategiesCommand(string userName)
public GetUserStrategiesCommand(string agentName)
{
UserName = userName;
AgentName = agentName;
}
}
}

View File

@@ -1,4 +1,4 @@
using Managing.Application.Abstractions;
using Managing.Domain.Bots;
using MediatR;
namespace Managing.Application.ManageBot.Commands
@@ -6,7 +6,7 @@ namespace Managing.Application.ManageBot.Commands
/// <summary>
/// Command to retrieve a specific strategy owned by a user
/// </summary>
public class GetUserStrategyCommand : IRequest<ITradingBot>
public class GetUserStrategyCommand : IRequest<Bot>
{
/// <summary>
/// The username of the agent/user that owns the strategy

View File

@@ -0,0 +1,16 @@
using Managing.Domain.Trades;
using MediatR;
namespace Managing.Application.ManageBot.Commands;
public class ManualPositionCommand : IRequest<Position>
{
public Guid PositionId { get; set; }
public Guid Identifier { get; set; }
public ManualPositionCommand(Guid identifier, Guid positionId)
{
Identifier = identifier;
PositionId = positionId;
}
}

View File

@@ -3,15 +3,13 @@ using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
public class RestartBotCommand : IRequest<string>
public class RestartBotCommand : IRequest<BotStatus>
{
public string Name { get; }
public BotType BotType { get; }
public Guid Identifier { get; }
public RestartBotCommand(BotType botType, string name)
public RestartBotCommand(Guid identifier)
{
BotType = botType;
Name = name;
Identifier = identifier;
}
}
}

View File

@@ -1,20 +1,21 @@
using Managing.Domain.Bots;
using Managing.Domain.Users;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
public class StartBotCommand : IRequest<string>
public class StartBotCommand : IRequest<BotStatus>
{
public string Name { get; }
public TradingBotConfig Config { get; }
public User User { get; }
public User User { get; internal set; }
public bool CreateOnly { get; }
public StartBotCommand(TradingBotConfig config, string name, User user)
public StartBotCommand(TradingBotConfig config, User user, bool createOnly = false)
{
Config = config;
Name = name;
User = user;
CreateOnly = createOnly;
}
}
}

View File

@@ -1,12 +1,13 @@
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
public class StopBotCommand : IRequest<string>
public class StopBotCommand : IRequest<BotStatus>
{
public string Identifier { get; }
public Guid Identifier { get; }
public StopBotCommand(string identifier)
public StopBotCommand(Guid identifier)
{
Identifier = identifier;
}

View File

@@ -1,14 +0,0 @@
using MediatR;
namespace Managing.Application.ManageBot.Commands
{
public class ToggleIsForWatchingCommand : IRequest<string>
{
public string Name { get; }
public ToggleIsForWatchingCommand(string name)
{
Name = name;
}
}
}

View File

@@ -6,12 +6,12 @@ namespace Managing.Application.ManageBot.Commands
/// <summary>
/// Command to update the configuration of a running trading bot
/// </summary>
public class UpdateBotConfigCommand : IRequest<string>
public class UpdateBotConfigCommand : IRequest<bool>
{
public string Identifier { get; }
public Guid Identifier { get; }
public TradingBotConfig NewConfig { get; }
public UpdateBotConfigCommand(string identifier, TradingBotConfig newConfig)
public UpdateBotConfigCommand(Guid identifier, TradingBotConfig newConfig)
{
Identifier = identifier;
NewConfig = newConfig;

View File

@@ -18,6 +18,6 @@ public class DeleteBotCommandHandler : IRequestHandler<DeleteBotCommand, bool>
public Task<bool> Handle(DeleteBotCommand request, CancellationToken cancellationToken)
{
return _botService.DeleteBot(request.Name);
return _botService.DeleteBot(request.Identifier);
}
}

View File

@@ -1,15 +1,16 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Bots;
using MediatR;
namespace Managing.Application.ManageBot
{
public class GetActiveBotsCommandHandler(IBotService botService)
: IRequestHandler<GetActiveBotsCommand, List<ITradingBot>>
public class GetBotsByStatusCommandHandler(IBotService botService)
: IRequestHandler<GetBotsByStatusCommand, IEnumerable<Bot>>
{
public Task<List<ITradingBot>> Handle(GetActiveBotsCommand request, CancellationToken cancellationToken)
public async Task<IEnumerable<Bot>> Handle(GetBotsByStatusCommand request, CancellationToken cancellationToken)
{
return Task.FromResult(botService.GetActiveBots());
return await botService.GetBotsByStatusAsync(request.Status);
}
}
}

View File

@@ -20,11 +20,11 @@ namespace Managing.Application.ManageBot
_accountService = accountService;
}
public Task<List<AgentStatusResponse>> Handle(GetAgentStatusesCommand request,
public async Task<List<AgentStatusResponse>> Handle(GetAgentStatusesCommand request,
CancellationToken cancellationToken)
{
var result = new List<AgentStatusResponse>();
var allActiveBots = _botService.GetActiveBots();
var allActiveBots = await _botService.GetBotsByStatusAsync(BotStatus.Up);
// Group bots by user and determine status
var agentGroups = allActiveBots
@@ -38,7 +38,9 @@ namespace Managing.Application.ManageBot
var bots = agentGroup.ToList();
// Determine agent status: Online if at least one strategy is running, Offline otherwise
var agentStatus = bots.Any(bot => bot.GetStatus() == BotStatus.Up.ToString()) ? AgentStatus.Online : AgentStatus.Offline;
var agentStatus = bots.Any(bot => bot.Status == BotStatus.Up)
? AgentStatus.Online
: AgentStatus.Offline;
result.Add(new AgentStatusResponse
{
@@ -47,7 +49,7 @@ namespace Managing.Application.ManageBot
});
}
return Task.FromResult(result);
return result;
}
}
}
}

View File

@@ -0,0 +1,54 @@
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Statistics;
using MediatR;
namespace Managing.Application.ManageBot
{
/// <summary>
/// Handler for retrieving all agent summaries with complete data
/// </summary>
public class GetAllAgentSummariesCommandHandler : IRequestHandler<GetAllAgentSummariesCommand,
IEnumerable<AgentSummary>>
{
private readonly IStatisticService _statisticService;
public GetAllAgentSummariesCommandHandler(IStatisticService statisticService)
{
_statisticService = statisticService;
}
public async Task<IEnumerable<AgentSummary>> Handle(GetAllAgentSummariesCommand request,
CancellationToken cancellationToken)
{
// Get all agent summaries from the database
var allAgentSummaries = await _statisticService.GetAllAgentSummaries();
if (request.TimeFilter != "Total")
{
var cutoffDate = GetCutoffDate(request.TimeFilter);
allAgentSummaries = allAgentSummaries.Where(a =>
a.UpdatedAt >= cutoffDate ||
(a.Runtime.HasValue && a.Runtime.Value >= cutoffDate));
}
return allAgentSummaries;
}
/// <summary>
/// Gets the cutoff date based on the time filter
/// </summary>
private DateTime GetCutoffDate(string timeFilter)
{
return timeFilter switch
{
"24H" => DateTime.UtcNow.AddHours(-24),
"3D" => DateTime.UtcNow.AddDays(-3),
"1W" => DateTime.UtcNow.AddDays(-7),
"1M" => DateTime.UtcNow.AddMonths(-1),
"1Y" => DateTime.UtcNow.AddYears(-1),
_ => DateTime.MinValue // Default to include all data
};
}
}
}

View File

@@ -1,101 +0,0 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using Managing.Common;
using Managing.Domain.Users;
using MediatR;
namespace Managing.Application.ManageBot
{
/// <summary>
/// Handler for retrieving all agents and their strategies
/// </summary>
public class GetAllAgentsCommandHandler : IRequestHandler<GetAllAgentsCommand, Dictionary<User, List<ITradingBot>>>
{
private readonly IBotService _botService;
private readonly IAccountService _accountService;
public GetAllAgentsCommandHandler(IBotService botService, IAccountService accountService)
{
_botService = botService;
_accountService = accountService;
}
public Task<Dictionary<User, List<ITradingBot>>> Handle(GetAllAgentsCommand request,
CancellationToken cancellationToken)
{
var result = new Dictionary<User, List<ITradingBot>>();
var allActiveBots = _botService.GetActiveBots();
// Group bots by user
foreach (var bot in allActiveBots)
{
if (bot.User == null)
{
// Skip bots without a user (this shouldn't happen, but just to be safe)
continue;
}
// Apply time filtering if needed (except for "Total")
if (request.TimeFilter != "Total")
{
// Check if this bot had activity within the specified time range
if (!BotHasActivityInTimeRange(bot, request.TimeFilter))
{
continue; // Skip this bot if it doesn't have activity in the time range
}
}
// Add the bot to the user's list
if (!result.ContainsKey(bot.User))
{
result[bot.User] = new List<ITradingBot>();
}
result[bot.User].Add(bot);
}
return Task.FromResult(result);
}
/// <summary>
/// Checks if a bot has had trading activity within the specified time range
/// </summary>
private bool BotHasActivityInTimeRange(ITradingBot bot, string timeFilter)
{
// Convert time filter to a DateTime
DateTime cutoffDate = DateTime.UtcNow;
switch (timeFilter)
{
case "24H":
cutoffDate = DateTime.UtcNow.AddHours(-24);
break;
case "3D":
cutoffDate = DateTime.UtcNow.AddDays(-3);
break;
case "1W":
cutoffDate = DateTime.UtcNow.AddDays(-7);
break;
case "1M":
cutoffDate = DateTime.UtcNow.AddMonths(-1);
break;
case "1Y":
cutoffDate = DateTime.UtcNow.AddYears(-1);
break;
default:
// Default to "Total" (no filtering)
return true;
}
// Check if there are any positions with activity after the cutoff date
return bot.Positions.Any(p =>
p.Date >= cutoffDate ||
(p.Open.Date >= cutoffDate) ||
(p.StopLoss.Status == Enums.TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) ||
(p.TakeProfit1.Status == Enums.TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) ||
(p.TakeProfit2 != null && p.TakeProfit2.Status == Enums.TradeStatus.Filled &&
p.TakeProfit2.Date >= cutoffDate));
}
}
}

View File

@@ -0,0 +1,20 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Bots;
using MediatR;
namespace Managing.Application.ManageBot
{
public class GetBotsByUserAndStatusCommandHandler(IBotService botService)
: IRequestHandler<GetBotsByUserAndStatusCommand, IEnumerable<Bot>>
{
public async Task<IEnumerable<Bot>> Handle(GetBotsByUserAndStatusCommand request, CancellationToken cancellationToken)
{
// Get all bots for the user
var userBots = await botService.GetBotsByUser(request.UserId);
// Filter by status
return userBots.Where(bot => bot.Status == request.Status);
}
}
}

View File

@@ -1,52 +1,25 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
{
/// <summary>
/// Handler for retrieving only online agent names
/// </summary>
public class GetOnlineAgentNamesCommandHandler : IRequestHandler<GetOnlineAgentNamesCommand, List<string>>
public class GetOnlineAgentNamesCommandHandler : IRequestHandler<GetOnlineAgentNamesCommand, IEnumerable<string>>
{
private readonly IBotService _botService;
private readonly IAccountService _accountService;
public GetOnlineAgentNamesCommandHandler(IBotService botService, IAccountService accountService)
public GetOnlineAgentNamesCommandHandler(IBotService botService)
{
_botService = botService;
_accountService = accountService;
}
public Task<List<string>> Handle(GetOnlineAgentNamesCommand request,
public async Task<IEnumerable<string>> Handle(GetOnlineAgentNamesCommand request,
CancellationToken cancellationToken)
{
var onlineAgentNames = new List<string>();
var allActiveBots = _botService.GetActiveBots();
// Group bots by user and determine status
var agentGroups = allActiveBots
.Where(bot => bot.User != null)
.GroupBy(bot => bot.User)
.ToList();
foreach (var agentGroup in agentGroups)
{
var user = agentGroup.Key;
var bots = agentGroup.ToList();
// Only include agents that have at least one strategy running (Online status)
var isOnline = bots.Any(bot => bot.GetStatus() == BotStatus.Up.ToString());
if (isOnline)
{
onlineAgentNames.Add(user.AgentName);
}
}
return Task.FromResult(onlineAgentNames);
return await _botService.GetActiveBotsNamesAsync();
}
}
}

View File

@@ -0,0 +1,33 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Statistics;
using MediatR;
namespace Managing.Application.ManageBot
{
/// <summary>
/// Handler for retrieving paginated agent summaries with sorting and filtering
/// </summary>
public class GetPaginatedAgentSummariesCommandHandler : IRequestHandler<GetPaginatedAgentSummariesCommand,
(IEnumerable<AgentSummary> Results, int TotalCount)>
{
private readonly IAgentSummaryRepository _agentSummaryRepository;
public GetPaginatedAgentSummariesCommandHandler(IAgentSummaryRepository agentSummaryRepository)
{
_agentSummaryRepository = agentSummaryRepository;
}
public async Task<(IEnumerable<AgentSummary> Results, int TotalCount)> Handle(
GetPaginatedAgentSummariesCommand request,
CancellationToken cancellationToken)
{
return await _agentSummaryRepository.GetPaginatedAsync(
request.Page,
request.PageSize,
request.SortBy,
request.SortOrder,
request.AgentNames);
}
}
}

View File

@@ -1,26 +1,26 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Bots;
using MediatR;
namespace Managing.Application.ManageBot
{
public class GetUserStrategiesCommandHandler : IRequestHandler<GetUserStrategiesCommand, List<ITradingBot>>
public class GetUserStrategiesCommandHandler : IRequestHandler<GetUserStrategiesCommand, IEnumerable<Bot>>
{
private readonly IBotService _botService;
private readonly IUserService _userService;
public GetUserStrategiesCommandHandler(IBotService botService)
public GetUserStrategiesCommandHandler(IBotService botService, IUserService userService)
{
_botService = botService;
_userService = userService;
}
public Task<List<ITradingBot>> Handle(GetUserStrategiesCommand request, CancellationToken cancellationToken)
public async Task<IEnumerable<Bot>> Handle(GetUserStrategiesCommand request, CancellationToken cancellationToken)
{
var allActiveBots = _botService.GetActiveBots();
var userBots = allActiveBots
.Where(bot => bot.User != null && bot.User.AgentName == request.UserName)
.ToList();
return Task.FromResult(userBots);
var user = await _userService.GetUserByAgentName(request.AgentName);
return await _botService.GetBotsByUser(user.Id);
}
}
}

View File

@@ -1,5 +1,6 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Bots;
using MediatR;
namespace Managing.Application.ManageBot
@@ -7,7 +8,7 @@ namespace Managing.Application.ManageBot
/// <summary>
/// Handler for retrieving a specific strategy owned by a user
/// </summary>
public class GetUserStrategyCommandHandler : IRequestHandler<GetUserStrategyCommand, ITradingBot>
public class GetUserStrategyCommandHandler : IRequestHandler<GetUserStrategyCommand, Bot>
{
private readonly IBotService _botService;
@@ -16,17 +17,14 @@ namespace Managing.Application.ManageBot
_botService = botService;
}
public Task<ITradingBot> Handle(GetUserStrategyCommand request, CancellationToken cancellationToken)
public async Task<Bot> Handle(GetUserStrategyCommand request, CancellationToken cancellationToken)
{
var allActiveBots = _botService.GetActiveBots();
// Find the specific strategy that matches both user and strategy name
var strategy = allActiveBots
.FirstOrDefault(bot =>
bot.User.AgentName == request.AgentName &&
bot.Identifier == request.StrategyName);
return Task.FromResult(strategy);
var strategy = await _botService.GetBotByName(request.StrategyName);
if (strategy == null)
{
throw new Exception($"Strategy with name {request.StrategyName} not found");
}
return strategy;
}
}
}

View File

@@ -1,125 +0,0 @@
using Managing.Application.Abstractions;
using Managing.Core;
using MediatR;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot;
public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand, string>
{
private readonly IBotService _botService;
private readonly ILogger<LoadBackupBotCommandHandler> _logger;
public LoadBackupBotCommandHandler(
ILogger<LoadBackupBotCommandHandler> logger, IBotService botService)
{
_logger = logger;
_botService = botService;
}
public async Task<string> Handle(LoadBackupBotCommand request, CancellationToken cancellationToken)
{
var backupBots = (await _botService.GetSavedBotsAsync()).ToList();
_logger.LogInformation("Loading {Count} backup bots.", backupBots.Count);
var result = new Dictionary<string, BotStatus>();
bool anyBackupStarted = false;
bool anyBotActive = false;
foreach (var backupBot in backupBots)
{
try
{
var activeBot = _botService.GetActiveBots().FirstOrDefault(b => b.Identifier == backupBot.Identifier);
if (activeBot == null)
{
_logger.LogInformation("No active instance found for bot {Identifier}. Starting backup...",
backupBot.Identifier);
// Start the bot from backup
_botService.StartBotFromBackup(backupBot);
// Wait a short time to allow the bot to initialize
await Task.Delay(1000, cancellationToken);
// Try to get the active bot multiple times to ensure it's properly started
int attempts = 0;
const int maxAttempts = 2;
while (attempts < maxAttempts)
{
activeBot = _botService.GetActiveBots()
.FirstOrDefault(b => b.Identifier == backupBot.Identifier);
if (activeBot != null)
{
// Check if the bot was originally Down
if (backupBot.LastStatus == BotStatus.Down)
{
result[activeBot.Identifier] = BotStatus.Down;
_logger.LogInformation(
"Backup bot {Identifier} loaded but kept in Down status as it was originally Down.",
backupBot.Identifier);
}
else
{
result[activeBot.Identifier] = BotStatus.Up;
anyBackupStarted = true;
_logger.LogInformation("Backup bot {Identifier} started successfully.",
backupBot.Identifier);
}
break;
}
attempts++;
if (attempts < maxAttempts)
{
await Task.Delay(1000, cancellationToken); // Wait another second before next attempt
}
}
if (activeBot == null)
{
result[backupBot.Identifier] = BotStatus.Down;
_logger.LogWarning("Backup bot {Identifier} failed to start after {MaxAttempts} attempts.",
backupBot.Identifier, maxAttempts);
}
}
else
{
var status = MiscExtensions.ParseEnum<BotStatus>(activeBot.GetStatus());
result[activeBot.Identifier] = status;
anyBotActive = true;
_logger.LogInformation("Bot {Identifier} is already active with status {Status}.",
activeBot.Identifier,
status);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading bot {Identifier}. Deleting its backup.", backupBot.Identifier);
result[backupBot.Identifier] = BotStatus.Down;
}
}
var summary = string.Join(", ", result.Select(b => $"{b.Key}: {b.Value}"));
_logger.LogInformation("Bot loading completed. Summary: {Summary}", summary);
// Determine final status
BotStatus finalStatus = anyBackupStarted
? BotStatus.Backup
: anyBotActive
? BotStatus.Up
: BotStatus.Down;
_logger.LogInformation("Final aggregate bot status: {FinalStatus}", finalStatus);
return finalStatus.ToString();
}
}
public class LoadBackupBotCommand : IRequest<string>
{
}

View File

@@ -0,0 +1,27 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Trades;
using MediatR;
namespace Managing.Application.ManageBot;
public class ManualPositionCommandHandler : IRequestHandler<ManualPositionCommand, Position>
{
private readonly IBotService _botService;
public ManualPositionCommandHandler(IBotService botService)
{
_botService = botService;
}
public async Task<Position> Handle(ManualPositionCommand request, CancellationToken cancellationToken)
{
var bot = await _botService.GetBotByIdentifier(request.Identifier);
if (bot == null)
{
throw new Exception($"Bot with identifier {request.Identifier} not found");
}
return await _botService.ClosePositionAsync(request.Identifier, request.PositionId);
}
}

View File

@@ -1,10 +1,11 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
{
public class RestartBotCommandHandler : IRequestHandler<RestartBotCommand, string>
public class RestartBotCommandHandler : IRequestHandler<RestartBotCommand, BotStatus>
{
private readonly IBotService _botService;
@@ -13,9 +14,9 @@ namespace Managing.Application.ManageBot
_botService = botService;
}
public Task<string> Handle(RestartBotCommand request, CancellationToken cancellationToken)
public async Task<BotStatus> Handle(RestartBotCommand request, CancellationToken cancellationToken)
{
return _botService.RestartBot(request.Name);
return await _botService.RestartBot(request.Identifier);
}
}
}

View File

@@ -1,42 +1,38 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using Managing.Common;
using Managing.Domain.Bots;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
{
public class StartBotCommandHandler : IRequestHandler<StartBotCommand, string>
public class StartBotCommandHandler : IRequestHandler<StartBotCommand, BotStatus>
{
private readonly IBotFactory _botFactory;
private readonly IBotService _botService;
private readonly IMoneyManagementService _moneyManagementService;
private readonly IExchangeService _exchangeService;
private readonly IAccountService _accountService;
private readonly IGrainFactory _grainFactory;
public StartBotCommandHandler(IBotFactory botFactory, IBotService botService,
IMoneyManagementService moneyManagementService, IExchangeService exchangeService,
IAccountService accountService)
public StartBotCommandHandler(
IAccountService accountService, IGrainFactory grainFactory)
{
_botFactory = botFactory;
_botService = botService;
_moneyManagementService = moneyManagementService;
_exchangeService = exchangeService;
_accountService = accountService;
_grainFactory = grainFactory;
}
public async Task<string> Handle(StartBotCommand request, CancellationToken cancellationToken)
public async Task<BotStatus> Handle(StartBotCommand request, CancellationToken cancellationToken)
{
BotStatus botStatus = BotStatus.Down;
// Validate the configuration
if (request.Config == null)
{
throw new ArgumentException("Bot configuration is required");
}
if (request.Config.Scenario == null || !request.Config.Scenario.Indicators.Any())
{
throw new InvalidOperationException(
"Scenario or indicators not loaded properly in constructor. This indicates a configuration error.");
}
if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
{
throw new ArgumentException(
@@ -59,68 +55,23 @@ namespace Managing.Application.ManageBot
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
}
// Ensure essential configuration values are properly set
var configToUse = new TradingBotConfig
{
AccountName = request.Config.AccountName,
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,
IsForBacktest = request.Config.IsForBacktest,
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,
FlipPosition = request.Config.FlipPosition, // Set FlipPosition
Name = request.Config.Name ?? request.Name,
CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable
};
var tradingBot = await _botFactory.CreateTradingBot(configToUse);
tradingBot.User = request.User;
// Log the configuration being used
LogBotConfigurationAsync(tradingBot, $"{configToUse.Name} created");
_botService.AddTradingBotToCache(tradingBot);
return tradingBot.GetStatus();
return botStatus.ToString();
}
/// <summary>
/// Logs the bot configuration for debugging and audit purposes
/// </summary>
/// <param name="bot">The trading bot instance</param>
/// <param name="context">Context information for the log</param>
private void LogBotConfigurationAsync(ITradingBot bot, string context)
{
try
{
var config = bot.GetConfiguration();
var logMessage = $"{context} - Bot: {config.Name}, " +
$"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
var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(Guid.NewGuid());
await botGrain.CreateAsync(request.Config, request.User);
// Only start the bot if createOnly is false
if (!request.CreateOnly)
{
await botGrain.StartAsync();
}
}
catch (Exception)
catch (Exception ex)
{
// Ignore logging errors to not affect bot creation
throw new Exception($"Failed to start bot: {ex.Message}, {ex.StackTrace}");
}
return request.CreateOnly ? BotStatus.None : BotStatus.Up;
}
}
}

View File

@@ -1,10 +1,11 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
{
public class StopBotCommandHandler : IRequestHandler<StopBotCommand, string>
public class StopBotCommandHandler : IRequestHandler<StopBotCommand, BotStatus>
{
private readonly IBotService _botService;
@@ -13,9 +14,9 @@ namespace Managing.Application.ManageBot
_botService = botService;
}
public Task<string> Handle(StopBotCommand request, CancellationToken cancellationToken)
public async Task<BotStatus> Handle(StopBotCommand request, CancellationToken cancellationToken)
{
return _botService.StopBot(request.Identifier);
return await _botService.StopBot(request.Identifier);
}
}
}

View File

@@ -1,23 +0,0 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using MediatR;
namespace Managing.Application.ManageBot
{
public class ToggleIsForWatchingCommandHandler : IRequestHandler<ToggleIsForWatchingCommand, string>
{
private readonly IBotService _botService;
public ToggleIsForWatchingCommandHandler(IBotService botService)
{
_botService = botService;
}
public Task<string> Handle(ToggleIsForWatchingCommand request, CancellationToken cancellationToken)
{
_botService.ToggleIsForWatchingOnly(request.Name);
var bot = _botService.GetActiveBots().FirstOrDefault(b => b.Name == request.Name);
return Task.FromResult(bot?.Config.IsForWatchingOnly.ToString());
}
}
}

View File

@@ -7,7 +7,7 @@ namespace Managing.Application.ManageBot
/// <summary>
/// Handler for updating trading bot configurations
/// </summary>
public class UpdateBotConfigCommandHandler : IRequestHandler<UpdateBotConfigCommand, string>
public class UpdateBotConfigCommandHandler : IRequestHandler<UpdateBotConfigCommand, bool>
{
private readonly IBotService _botService;
@@ -16,44 +16,27 @@ namespace Managing.Application.ManageBot
_botService = botService;
}
public async Task<string> Handle(UpdateBotConfigCommand request, CancellationToken cancellationToken)
public async Task<bool> Handle(UpdateBotConfigCommand request, CancellationToken cancellationToken)
{
try
{
if (string.IsNullOrEmpty(request.Identifier))
{
throw new ArgumentException("Bot identifier is required");
}
if (request.NewConfig == null)
{
throw new ArgumentException("New configuration is required");
}
// Get the bot from active bots
var activeBots = _botService.GetActiveBots();
var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier);
var bot = await _botService.GetBotByIdentifier(request.Identifier);
if (bot == null)
{
return $"Bot with identifier {request.Identifier} not found or is not running";
throw new Exception($"Bot with identifier {request.Identifier} not found");
}
// Update the bot configuration
var updateResult = await bot.UpdateConfiguration(request.NewConfig);
if (updateResult)
{
return $"Bot configuration updated successfully for {request.Identifier}";
}
else
{
return $"Failed to update bot configuration for {request.Identifier}";
}
return await _botService.UpdateBotConfiguration(request.Identifier, request.NewConfig);
}
catch (Exception ex)
{
return $"Error updating bot configuration: {ex.Message}";
throw new Exception($"Error updating bot configuration: {ex.Message}");
}
}
}

View File

@@ -7,31 +7,33 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="MoneyManagements\Abstractions\**" />
<EmbeddedResource Remove="MoneyManagements\Abstractions\**" />
<None Remove="MoneyManagements\Abstractions\**" />
<Compile Remove="MoneyManagements\Abstractions\**"/>
<EmbeddedResource Remove="MoneyManagements\Abstractions\**"/>
<None Remove="MoneyManagements\Abstractions\**"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.1" />
<PackageReference Include="GeneticSharp" Version="3.1.4" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="Microsoft.Orleans.Client" Version="9.2.1" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1" />
<PackageReference Include="Polly" Version="8.4.0" />
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0" />
<PackageReference Include="FluentValidation" Version="11.9.1"/>
<PackageReference Include="GeneticSharp" Version="3.1.4"/>
<PackageReference Include="MediatR" Version="12.2.0"/>
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0"/>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1"/>
<PackageReference Include="Microsoft.Orleans.Client" Version="9.2.1"/>
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1"/>
<PackageReference Include="Microsoft.Orleans.Reminders" Version="9.2.1"/>
<PackageReference Include="Microsoft.Orleans.Runtime" Version="9.2.1"/>
<PackageReference Include="Polly" Version="8.4.0"/>
<PackageReference Include="Skender.Stock.Indicators" Version="2.5.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj" />
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj" />
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj" />
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj"/>
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj"/>
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,99 @@
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services;
using Managing.Core;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.Shared.Helpers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Orleans.Concurrency;
using static Managing.Common.Enums;
namespace Managing.Application.Scenarios;
/// <summary>
/// Orleans grain for scenario execution and signal generation.
/// This stateless grain handles candle management and signal generation for live trading.
/// </summary>
[StatelessWorker]
public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
{
private readonly ILogger<ScenarioRunnerGrain> _logger;
private readonly IServiceScopeFactory _scopeFactory;
public ScenarioRunnerGrain(
ILogger<ScenarioRunnerGrain> logger,
IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
private async Task<HashSet<Candle>> GetCandlesAsync(TradingBotConfig config, DateTime startDate)
{
try
{
var newCandles = await ServiceScopeHelpers.WithScopedService<IExchangeService, HashSet<Candle>>(
_scopeFactory, async exchangeService =>
{
return await exchangeService.GetCandlesInflux(
TradingExchanges.Evm,
config.Ticker,
startDate,
config.Timeframe,
500);
});
_logger.LogInformation($"Updated {newCandles.Count} candles for {config.Ticker}");
return newCandles;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update candles for {Ticker}", config.Ticker);
throw;
}
}
public async Task<LightSignal> GetSignals(TradingBotConfig config, Dictionary<string, LightSignal> previousSignals,
DateTime startDate, Candle candle)
{
try
{
// return new LightSignal(config.Ticker, TradeDirection.Long, Confidence.High,
// candle, candle.Date, TradingExchanges.Evm, IndicatorType.Composite,
// SignalType.Signal, "Generated Signal");
var candlesHashSet = await GetCandlesAsync(config, startDate);
if (candlesHashSet.LastOrDefault()!.Date <= candle.Date)
{
return null; // No new candles, no need to generate a signal
}
var signal = TradingBox.GetSignal(
candlesHashSet,
config.Scenario,
previousSignals,
config.Scenario?.LoopbackPeriod ?? 1);
if (signal != null && signal.Date >= candle.Date)
{
_logger.LogInformation(
$"Generated signal for {config.Ticker}: {signal.Direction} with confidence {signal.Confidence}");
return new LightSignal(signal.Ticker, signal.Direction, Confidence.High,
candle, candle.Date, signal.Exchange, signal.IndicatorType,
signal.SignalType, signal.IndicatorName);
}
else
{
return null; // No signal generated
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update signals for {Ticker}", config.Ticker);
throw;
}
}
}

View File

@@ -1,4 +1,5 @@
using Managing.Application.Abstractions;
using System.Data;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
@@ -25,7 +26,7 @@ namespace Managing.Application.Scenarios
foreach (var strategy in strategies)
{
scenario.AddIndicator(await _tradingService.GetStrategyByNameAsync(strategy));
scenario.AddIndicator(await _tradingService.GetIndicatorByNameAsync(strategy));
}
try
@@ -41,41 +42,14 @@ namespace Managing.Application.Scenarios
return scenario;
}
public async Task<Indicator> CreateStrategy(
IndicatorType type,
string name,
int? period = null,
int? fastPeriods = null,
int? slowPeriods = null,
int? signalPeriods = null,
double? multiplier = null,
int? stochPeriods = null,
int? smoothPeriods = null,
int? cyclePeriods = null)
{
var strategy = ScenarioHelpers.BuildIndicator(
type,
name,
period,
fastPeriods,
slowPeriods,
signalPeriods,
multiplier,
stochPeriods,
smoothPeriods,
cyclePeriods);
await _tradingService.InsertStrategyAsync(strategy);
return strategy;
}
public async Task<IEnumerable<Scenario>> GetScenariosAsync()
{
return await _tradingService.GetScenariosAsync();
}
public async Task<IEnumerable<Indicator>> GetIndicatorsAsync()
public async Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync()
{
return await _tradingService.GetStrategiesAsync();
return await _tradingService.GetIndicatorsAsync();
}
public async Task<bool> DeleteScenarioAsync(string name)
@@ -100,7 +74,7 @@ namespace Managing.Application.Scenarios
scenario.Indicators.Clear();
foreach (var strategy in strategies)
{
scenario.AddIndicator(await _tradingService.GetStrategyByNameAsync(strategy));
scenario.AddIndicator(await _tradingService.GetIndicatorByNameAsync(strategy));
}
scenario.LoopbackPeriod = loopbackPeriod ?? 1;
@@ -120,7 +94,7 @@ namespace Managing.Application.Scenarios
{
try
{
var strategy = await _tradingService.GetStrategyByNameAsync(name);
var strategy = await _tradingService.GetIndicatorByNameAsync(name);
strategy.Type = indicatorType;
strategy.Period = period;
strategy.FastPeriods = fastPeriods;
@@ -130,7 +104,7 @@ namespace Managing.Application.Scenarios
strategy.StochPeriods = stochPeriods;
strategy.SmoothPeriods = smoothPeriods;
strategy.CyclePeriods = cyclePeriods;
await _tradingService.UpdateStrategyAsync(strategy);
await _tradingService.UpdateIndicatorAsync(strategy);
return true;
}
catch (Exception e)
@@ -156,7 +130,7 @@ namespace Managing.Application.Scenarios
foreach (var strategyName in strategies)
{
var strategy = await _tradingService.GetStrategyByNameAsync(strategyName);
var strategy = await _tradingService.GetIndicatorByNameAsync(strategyName);
if (strategy != null && strategy.User?.Name == user.Name)
{
scenario.AddIndicator(strategy);
@@ -167,7 +141,7 @@ namespace Managing.Application.Scenarios
return scenario;
}
public async Task<IEnumerable<Indicator>> GetIndicatorsByUserAsync(User user)
public async Task<IEnumerable<IndicatorBase>> GetIndicatorsByUserAsync(User user)
{
var indicators = await GetIndicatorsAsync();
return indicators.Where(s => s.User?.Name == user.Name);
@@ -175,10 +149,10 @@ namespace Managing.Application.Scenarios
public async Task<bool> DeleteIndicatorByUser(User user, string name)
{
var strategy = await _tradingService.GetStrategyByNameAsync(name);
var strategy = await _tradingService.GetIndicatorByNameAsync(name);
if (strategy != null && strategy.User?.Name == user.Name)
{
await _tradingService.DeleteStrategyAsync(strategy.Name);
await _tradingService.DeleteIndicatorAsync(strategy.Name);
return true;
}
@@ -229,23 +203,35 @@ namespace Managing.Application.Scenarios
return scenario != null && scenario.User?.Name == user.Name ? scenario : null;
}
public async Task<Indicator> CreateIndicatorForUser(User user, IndicatorType type, string name,
public async Task<IndicatorBase> CreateIndicatorForUser(User user, IndicatorType type, string name,
int? period = null,
int? fastPeriods = null, int? slowPeriods = null, int? signalPeriods = null,
double? multiplier = null, int? stochPeriods = null, int? smoothPeriods = null,
int? cyclePeriods = null)
{
// Create a new strategy using the existing implementation
var strategy = await CreateStrategy(type, name, period, fastPeriods, slowPeriods, signalPeriods,
multiplier, stochPeriods, smoothPeriods, cyclePeriods);
var existingIndicator = await _tradingService.GetIndicatorByNameUserAsync(name, user);
// Set the user
strategy.User = user;
// Update the strategy to save the user property
await _tradingService.UpdateStrategyAsync(strategy);
return strategy;
if (existingIndicator != null)
{
throw new DuplicateNameException("An indicator with this name already exists for the user.");
}
else
{
var indicator = new IndicatorBase(name, type)
{
Period = period,
FastPeriods = fastPeriods,
SlowPeriods = slowPeriods,
SignalPeriods = signalPeriods,
Multiplier = multiplier,
StochPeriods = stochPeriods,
SmoothPeriods = smoothPeriods,
CyclePeriods = cyclePeriods,
User = user
};
await _tradingService.InsertIndicatorAsync(indicator);
return indicator;
}
}
public async Task<bool> DeleteStrategiesByUser(User user)
@@ -255,7 +241,7 @@ namespace Managing.Application.Scenarios
var strategies = await GetIndicatorsByUserAsync(user);
foreach (var strategy in strategies)
{
await _tradingService.DeleteStrategyAsync(strategy.Name);
await _tradingService.DeleteIndicatorAsync(strategy.Name);
}
return true;
@@ -281,7 +267,7 @@ namespace Managing.Application.Scenarios
foreach (var strategyName in strategies)
{
var strategy = await _tradingService.GetStrategyByNameAsync(strategyName);
var strategy = await _tradingService.GetIndicatorByNameAsync(strategyName);
if (strategy != null && strategy.User?.Name == user.Name)
{
scenario.AddIndicator(strategy);
@@ -296,7 +282,7 @@ namespace Managing.Application.Scenarios
int? fastPeriods, int? slowPeriods, int? signalPeriods, double? multiplier,
int? stochPeriods, int? smoothPeriods, int? cyclePeriods)
{
var strategy = await _tradingService.GetStrategyByNameAsync(name);
var strategy = await _tradingService.GetIndicatorByNameAsync(name);
if (strategy == null || strategy.User?.Name != user.Name)
{
return false;
@@ -311,7 +297,7 @@ namespace Managing.Application.Scenarios
public async Task<Scenario> GetScenarioByNameAndUserAsync(string scenarioName, User user)
{
var scenario = await _tradingService.GetScenarioByNameAsync(scenarioName);
var scenario = await _tradingService.GetScenarioByNameUserAsync(scenarioName, user);
if (scenario == null)
{
throw new InvalidOperationException($"Scenario {scenarioName} not found for user {user.Name}");

View File

@@ -90,7 +90,7 @@ public class SettingsService : ISettingsService
private async Task SetupStochSTCTrend()
{
var name = "STCTrend";
var strategy = await _scenarioService.CreateStrategy(IndicatorType.Stc,
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.Stc,
name,
fastPeriods: 23,
slowPeriods: 50,
@@ -101,7 +101,7 @@ public class SettingsService : ISettingsService
private async Task SetupMacd()
{
var name = "MacdCross";
var strategy = await _scenarioService.CreateStrategy(IndicatorType.MacdCross,
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.MacdCross,
name,
fastPeriods: 12,
slowPeriods: 26,
@@ -112,7 +112,7 @@ public class SettingsService : ISettingsService
private async Task SetupRsiDiv()
{
var name = "RsiDiv6";
var strategy = await _scenarioService.CreateStrategy(IndicatorType.RsiDivergence,
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.RsiDivergence,
name,
period: 6);
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
@@ -121,7 +121,7 @@ public class SettingsService : ISettingsService
private async Task SetupRsiDivConfirm()
{
var name = "RsiDivConfirm6";
var strategy = await _scenarioService.CreateStrategy(IndicatorType.RsiDivergenceConfirm,
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.RsiDivergenceConfirm,
name,
period: 6);
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
@@ -130,7 +130,7 @@ public class SettingsService : ISettingsService
private async Task SetupSuperTrend()
{
var name = "SuperTrend";
var strategy = await _scenarioService.CreateStrategy(IndicatorType.SuperTrend,
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.SuperTrend,
name,
period: 10,
multiplier: 3);
@@ -140,7 +140,7 @@ public class SettingsService : ISettingsService
private async Task SetupChandelierExit()
{
var name = "ChandelierExit";
var strategy = await _scenarioService.CreateStrategy(IndicatorType.ChandelierExit,
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.ChandelierExit,
name,
period: 22,
multiplier: 3);
@@ -150,7 +150,7 @@ public class SettingsService : ISettingsService
private async Task SetupStochRsiTrend()
{
var name = "StochRsiTrend";
var strategy = await _scenarioService.CreateStrategy(IndicatorType.StochRsiTrend,
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.StochRsiTrend,
name,
period: 14,
stochPeriods: 14,
@@ -162,7 +162,7 @@ public class SettingsService : ISettingsService
private async Task SetupEmaTrend()
{
var name = "Ema200Trend";
var strategy = await _scenarioService.CreateStrategy(IndicatorType.EmaTrend,
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.EmaTrend,
name,
period: 200);
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });
@@ -171,7 +171,7 @@ public class SettingsService : ISettingsService
private async Task SetupEmaCross()
{
var name = "Ema200Cross";
var strategy = await _scenarioService.CreateStrategy(IndicatorType.EmaCross,
var strategy = await _scenarioService.CreateIndicatorForUser(null, IndicatorType.EmaCross,
name,
period: 200);
await _scenarioService.CreateScenario(name, new List<string> { strategy.Name });

View File

@@ -1,6 +1,7 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Bots;
using Managing.Domain.Indicators;
using Managing.Domain.Risk;
using Managing.Domain.Synth.Models;
using Microsoft.Extensions.Logging;
@@ -867,7 +868,7 @@ public class SynthPredictionService : ISynthPredictionService
/// Monitors liquidation risk for an open position
/// </summary>
public async Task<SynthRiskResult> MonitorPositionRiskAsync(Ticker ticker, TradeDirection direction,
decimal currentPrice, decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig)
decimal currentPrice, decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig)
{
var result = new SynthRiskResult();
var config = BuildConfigurationForTimeframe(botConfig.Timeframe, botConfig);

View File

@@ -27,7 +27,7 @@ namespace Managing.Application.Trading
}
var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator;
var position = new Position(Guid.NewGuid().ToString(), request.AccountName, request.Direction,
var position = new Position(Guid.NewGuid(), request.AccountName, request.Direction,
request.Ticker,
request.MoneyManagement,
initiator, request.Date, request.User);

View File

@@ -1,9 +1,9 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Statistics;
@@ -11,6 +11,7 @@ using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
using Managing.Domain.Synth.Models;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Managing.Infrastructure.Evm.Models.Privy;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
@@ -56,12 +57,12 @@ public class TradingService : ITradingService
await _tradingRepository.DeleteScenarioAsync(name);
}
public async Task DeleteStrategyAsync(string name)
public async Task DeleteIndicatorAsync(string name)
{
await _tradingRepository.DeleteIndicatorAsync(name);
}
public async Task<Position> GetPositionByIdentifierAsync(string identifier)
public async Task<Position> GetPositionByIdentifierAsync(Guid identifier)
{
return await _tradingRepository.GetPositionByIdentifierAsync(identifier);
}
@@ -87,12 +88,12 @@ public class TradingService : ITradingService
return await _tradingRepository.GetScenariosAsync();
}
public async Task<IEnumerable<Indicator>> GetStrategiesAsync()
public async Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync()
{
return await _tradingRepository.GetStrategiesAsync();
}
public async Task<Indicator> GetStrategyByNameAsync(string strategy)
public async Task<IndicatorBase> GetIndicatorByNameAsync(string strategy)
{
return await _tradingRepository.GetStrategyByNameAsync(strategy);
}
@@ -107,9 +108,9 @@ public class TradingService : ITradingService
await _tradingRepository.InsertScenarioAsync(scenario);
}
public async Task InsertStrategyAsync(Indicator indicator)
public async Task InsertIndicatorAsync(IndicatorBase indicatorBase)
{
await _tradingRepository.InsertStrategyAsync(indicator);
await _tradingRepository.InsertIndicatorAsync(indicatorBase);
}
public async Task<Position> ManagePosition(Account account, Position position)
@@ -170,7 +171,6 @@ public class TradingService : ITradingService
}
public async Task UpdatePositionAsync(Position position)
{
await _tradingRepository.UpdatePositionAsync(position);
@@ -235,9 +235,9 @@ public class TradingService : ITradingService
await _tradingRepository.UpdateScenarioAsync(scenario);
}
public async Task UpdateStrategyAsync(Indicator indicator)
public async Task UpdateIndicatorAsync(IndicatorBase indicatorBase)
{
await _tradingRepository.UpdateStrategyAsync(indicator);
await _tradingRepository.UpdateStrategyAsync(indicatorBase);
}
public async Task<IEnumerable<Position>> GetBrokerPositions(Account account)
@@ -372,7 +372,7 @@ public class TradingService : ITradingService
}
public async Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction,
decimal currentPrice, decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig)
decimal currentPrice, decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig)
{
return await _synthPredictionService.MonitorPositionRiskAsync(ticker, direction, currentPrice, liquidationPrice,
positionIdentifier, botConfig);
@@ -386,7 +386,7 @@ public class TradingService : ITradingService
/// <returns>A dictionary of indicator types to their calculated values.</returns>
public Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValuesAsync(
Scenario scenario,
List<Candle> candles)
HashSet<Candle> candles)
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
@@ -395,27 +395,15 @@ public class TradingService : ITradingService
return indicatorsValues;
}
// Convert candles to FixedSizeQueue for indicators
var fixedCandles = new FixedSizeQueue<Candle>(10000);
foreach (var candle in candles)
{
fixedCandles.Enqueue(candle);
}
// Build indicators from scenario
foreach (var indicator in scenario.Indicators)
{
try
{
// Build the indicator using ScenarioHelpers
var builtIndicator = ScenarioHelpers.BuildIndicator(indicator, 10000);
builtIndicator.Candles = fixedCandles;
indicatorsValues[indicator.Type] = builtIndicator.GetIndicatorValues();
indicatorsValues[indicator.Type] = indicator.GetIndicatorValues(candles);
}
catch (Exception ex)
{
// Log the error but continue with other indicators
_logger.LogError(ex, "Error calculating indicator {IndicatorName}: {ErrorMessage}",
indicator.Name, ex.Message);
}
@@ -423,4 +411,14 @@ public class TradingService : ITradingService
return indicatorsValues;
}
public async Task<IndicatorBase?> GetIndicatorByNameUserAsync(string name, User user)
{
return await _tradingRepository.GetStrategyByNameUserAsync(name, user);
}
public async Task<Scenario?> GetScenarioByNameUserAsync(string scenarioName, User user)
{
return await _tradingRepository.GetScenarioByNameUserAsync(scenarioName, user);
}
}

View File

@@ -1,8 +1,10 @@
using System.Text.RegularExpressions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Common;
using Managing.Domain.Accounts;
using Managing.Domain.Statistics;
using Managing.Domain.Users;
using Microsoft.Extensions.Logging;
@@ -15,6 +17,8 @@ public class UserService : IUserService
private readonly IAccountService _accountService;
private readonly ILogger<UserService> _logger;
private readonly ICacheService _cacheService;
private readonly IGrainFactory _grainFactory;
private readonly IAgentSummaryRepository _agentSummaryRepository;
private string[] authorizedAddresses =
[
@@ -33,15 +37,19 @@ public class UserService : IUserService
public UserService(
IEvmManager evmManager,
IUserRepository userRepository,
IAccountService accountService,
IAccountService accountService,
ILogger<UserService> logger,
ICacheService cacheService)
ICacheService cacheService,
IGrainFactory grainFactory,
IAgentSummaryRepository agentSummaryRepository)
{
_evmManager = evmManager;
_userRepository = userRepository;
_accountService = accountService;
_logger = logger;
_cacheService = cacheService;
_grainFactory = grainFactory;
_agentSummaryRepository = agentSummaryRepository;
}
public async Task<User> Authenticate(string name, string address, string message, string signature)
@@ -120,26 +128,32 @@ public class UserService : IUserService
return user;
}
public async Task<User> GetUserByAddressAsync(string address)
public async Task<User> GetUserByAddressAsync(string address, bool useCache = true)
{
var cacheKey = $"user-by-address-{address}";
// Check cache first
var cachedUser = _cacheService.GetValue<User>(cacheKey);
if (cachedUser != null)
// Check cache first if caching is enabled
if (useCache)
{
return cachedUser;
var cachedUser = _cacheService.GetValue<User>(cacheKey);
if (cachedUser != null)
{
return cachedUser;
}
}
// Cache miss - fetch from database
// Fetch from database (either cache miss or cache disabled)
var account = await _accountService.GetAccountByKey(address, true, false);
var user = await _userRepository.GetUserByNameAsync(account.User.Name);
// Use proper async version to avoid DbContext concurrency issues
user.Accounts = (await _accountService.GetAccountsByUserAsync(user)).ToList();
// Save to cache for 10 minutes (JWT middleware calls this on every request)
_cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(10));
// Save to cache for 10 minutes if caching is enabled (JWT middleware calls this on every request)
if (useCache)
{
_cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(10));
}
return user;
}
@@ -154,8 +168,25 @@ public class UserService : IUserService
}
else
{
user = await GetUserByName(user.Name);
user.AgentName = agentName;
await _userRepository.UpdateUser(user);
// Initialize the AgentGrain for this user
try
{
var agentGrain = _grainFactory.GetGrain<IAgentGrain>(user.Id);
await agentGrain.InitializeAsync(user.Id, agentName);
_logger.LogInformation("AgentGrain initialized for user {UserId} with agent name {AgentName}", user.Id,
agentName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize AgentGrain for user {UserId} with agent name {AgentName}",
user.Id, agentName);
// Don't throw here to avoid breaking the user update process
}
return user;
}
}
@@ -176,6 +207,7 @@ public class UserService : IUserService
throw new Exception("URL must point to a JPEG or PNG image");
}
user = await GetUserByName(user.Name);
user.AvatarUrl = avatarUrl;
await _userRepository.UpdateUser(user);
return user;
@@ -193,13 +225,41 @@ public class UserService : IUserService
}
}
user = await GetUserByName(user.Name);
user.TelegramChannel = telegramChannel;
await _userRepository.UpdateUser(user);
return user;
}
public async Task<User> GetUser(string name)
public async Task<User> GetUserByName(string name)
{
return await _userRepository.GetUserByNameAsync(name);
}
public async Task<User> GetUserByAgentName(string agentName)
{
var user = await _userRepository.GetUserByAgentNameAsync(agentName);
if (user == null)
{
throw new Exception($"User with agent name {agentName} not found");
}
return user;
}
public async Task SaveOrUpdateAgentSummary(AgentSummary agentSummary)
{
try
{
await _agentSummaryRepository.SaveOrUpdateAsync(agentSummary);
_logger.LogInformation("AgentSummary saved/updated for user {UserId} with agent name {AgentName}",
agentSummary.UserId, agentSummary.AgentName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save/update AgentSummary for user {UserId} with agent name {AgentName}",
agentSummary.UserId, agentSummary.AgentName);
throw;
}
}
}

View File

@@ -1,7 +1,7 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using Managing.Domain.Bots;
using Managing.Domain.Statistics;
using MediatR;
using Microsoft.Extensions.Logging;
@@ -46,20 +46,21 @@ public class BalanceTrackingWorker : BaseWorker<BalanceTrackingWorker>
_logger.LogInformation("Starting balance tracking...");
// Get all active bots
var bots = await _mediator.Send(new GetActiveBotsCommand());
var bots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Up));
if (bots.Count == 0)
var botCount = bots.Count();
if (botCount == 0)
{
_logger.LogWarning("No active bots found. Skipping balance tracking.");
return;
}
_logger.LogInformation($"Found {bots.Count} active bots. Proceeding with balance tracking.");
_logger.LogInformation($"Found {botCount} active bots. Proceeding with balance tracking.");
await TrackBalances(bots);
_logger.LogInformation("Completed balance tracking");
}
private async Task TrackBalances(List<ITradingBot> bots)
private async Task TrackBalances(IEnumerable<Bot> bots)
{
// Group bots by agent/user
var botsByAgent = bots
@@ -98,9 +99,9 @@ public class BalanceTrackingWorker : BaseWorker<BalanceTrackingWorker>
// Calculate total allocated balance for all bots
foreach (var bot in agentBots)
{
totalBotAllocatedBalance += bot.Config.BotTradingBalance;
totalBotAllocatedBalance += bot.Volume;
_logger.LogInformation(
$"Bot {bot.Name} allocated balance: {bot.Config.BotTradingBalance} USD");
$"Bot {bot.Name} allocated balance: {bot.Volume} USD");
}
// Get account balances for this agent (only once per agent)
@@ -140,20 +141,7 @@ public class BalanceTrackingWorker : BaseWorker<BalanceTrackingWorker>
// Process all bots in a single iteration
foreach (var bot in agentBots)
{
// Get wallet balance
var latestBotBalance = bot.WalletBalances
.OrderByDescending(x => x.Key)
.FirstOrDefault();
if (latestBotBalance.Key != default)
{
botsAllocationUsdValue += latestBotBalance.Value;
_logger.LogInformation(
$"Bot {bot.Name} wallet balance: {latestBotBalance.Value} USD at {latestBotBalance.Key}");
}
// Calculate PnL
totalPnL += bot.GetProfitAndLoss();
totalPnL += bot.Pnl;
}
totalAgentValue = totalAccountUsdValue + botsAllocationUsdValue;

View File

@@ -1,22 +0,0 @@
using Managing.Application.ManageBot;
using MediatR;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Workers;
public class BotManagerWorker(
ILogger<BotManagerWorker> logger,
IServiceProvider serviceProvider,
IMediator mediadior)
: BaseWorker<BotManagerWorker>(WorkerType.BotManager,
logger,
TimeSpan.FromMinutes(5),
serviceProvider)
{
protected override async Task Run(CancellationToken cancellationToken)
{
var loadBackupBotCommand = new LoadBackupBotCommand();
await mediadior.Send(loadBackupBotCommand, cancellationToken);
}
}