Orlean (#32)
* 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:
490
src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs
Normal file
490
src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user