* Start building with orlean

* Add missing file

* Serialize grain state

* Remove grain and proxies

* update and add plan

* Update a bit

* Fix backtest grain

* Fix backtest grain

* Clean a bit
This commit is contained in:
Oda
2025-07-30 11:03:30 +02:00
committed by GitHub
parent d281d7cd02
commit 3de8b5e00e
59 changed files with 2626 additions and 677 deletions

View File

@@ -0,0 +1,490 @@
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Models;
using Managing.Domain.Bots;
using Managing.Domain.Trades;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Bots.Grains;
/// <summary>
/// Orleans grain for live trading bot operations.
/// 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
{
private readonly ILogger<LiveTradingBotGrain> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private TradingBotBase? _tradingBot;
private IDisposable? _timer;
private bool _isDisposed = false;
public LiveTradingBotGrain(
ILogger<LiveTradingBotGrain> logger,
IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
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();
}
}
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
{
_logger.LogInformation("LiveTradingBotGrain {GrainId} deactivating. Reason: {Reason}",
this.GetPrimaryKey(), reason.Description);
// Stop the timer and trading bot
await StopAsync();
await base.OnDeactivateAsync(reason, cancellationToken);
}
public async Task StartAsync()
{
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();
_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();
throw;
}
}
public async Task StopAsync()
{
try
{
// Stop the timer
_timer?.Dispose();
_timer = null;
// 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();
_logger.LogInformation("LiveTradingBotGrain {GrainId} stopped successfully", this.GetPrimaryKey());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to stop LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public Task<BotStatus> GetStatusAsync()
{
return Task.FromResult(State.Status);
}
public Task<TradingBotConfig> GetConfigurationAsync()
{
return Task.FromResult(State.Config);
}
public async Task<bool> UpdateConfigurationAsync(TradingBotConfig newConfig)
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
// Ensure this is not a backtest configuration
if (newConfig.IsForBacktest)
{
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
}
// 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;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update configuration for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
return false;
}
}
public async Task<Position> OpenPositionManuallyAsync(TradeDirection direction)
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
return await _tradingBot.OpenPositionManually(direction);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to open manual position for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task ToggleIsForWatchOnlyAsync()
{
try
{
if (_tradingBot == null)
{
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)
{
throw new InvalidOperationException("Bot is not running");
}
return new TradingBotResponse
{
Identifier = State.Identifier,
Name = State.Name,
Status = State.Status,
Config = State.Config,
Positions = _tradingBot.Positions,
Signals = _tradingBot.Signals.ToList(),
WalletBalances = _tradingBot.WalletBalances,
ProfitAndLoss = _tradingBot.GetProfitAndLoss(),
WinRate = _tradingBot.GetWinRate(),
ExecutionCount = _tradingBot.ExecutionCount,
StartupTime = State.StartupTime,
CreateDate = State.CreateDate
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get bot data for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
throw;
}
}
public async Task LoadBackupAsync(BotBackup backup)
{
try
{
if (_tradingBot == null)
{
throw new InvalidOperationException("Bot is not running");
}
_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();
_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;
}
}
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()
{
if (_tradingBot == null) return;
var interval = _tradingBot.Interval;
_timer = RegisterTimer(
async _ => await ExecuteBotCycle(),
null,
TimeSpan.FromMilliseconds(interval),
TimeSpan.FromMilliseconds(interval));
}
/// <summary>
/// Executes one cycle of the trading bot
/// </summary>
private async Task ExecuteBotCycle()
{
try
{
if (_tradingBot == null || State.Status != BotStatus.Up)
{
return;
}
// Execute the bot's Run method
await _tradingBot.Run();
// Update execution count
State.ExecutionCount++;
await SaveBackupToState();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during bot execution cycle for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
}
}
/// <summary>
/// Saves the current bot state to Orleans state storage
/// </summary>
private async Task SaveBackupToState()
{
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();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
}
}
/// <summary>
/// Loads bot state from Orleans state storage
/// </summary>
private async Task LoadBackupFromState()
{
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;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load state for LiveTradingBotGrain {GrainId}", this.GetPrimaryKey());
}
}
public void Dispose()
{
if (!_isDisposed)
{
_timer?.Dispose();
_isDisposed = true;
}
}
}