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

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