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:
@@ -12,14 +12,14 @@ namespace Managing.Application.Bots.Base
|
||||
private readonly IExchangeService _exchangeService;
|
||||
private readonly IMessengerService _messengerService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ILogger<TradingBot> _tradingBotLogger;
|
||||
private readonly ILogger<TradingBotBase> _tradingBotLogger;
|
||||
private readonly ITradingService _tradingService;
|
||||
private readonly IBotService _botService;
|
||||
private readonly IBackupBotService _backupBotService;
|
||||
|
||||
public BotFactory(
|
||||
IExchangeService exchangeService,
|
||||
ILogger<TradingBot> tradingBotLogger,
|
||||
ILogger<TradingBotBase> tradingBotLogger,
|
||||
IMessengerService messengerService,
|
||||
IAccountService accountService,
|
||||
ITradingService tradingService,
|
||||
|
||||
403
src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs
Normal file
403
src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs
Normal file
@@ -0,0 +1,403 @@
|
||||
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;
|
||||
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;
|
||||
using Orleans.Concurrency;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Bots.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// Orleans grain for backtest trading bot operations.
|
||||
/// Uses composition with TradingBotBase to maintain separation of concerns.
|
||||
/// This grain is stateless and follows the exact pattern of GetBacktestingResult from Backtester.cs.
|
||||
/// </summary>
|
||||
[StatelessWorker]
|
||||
public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
{
|
||||
private readonly ILogger<BacktestTradingBotGrain> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IBacktestRepository _backtestRepository;
|
||||
private bool _isDisposed = false;
|
||||
|
||||
public BacktestTradingBotGrain(
|
||||
ILogger<BacktestTradingBotGrain> logger,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IBacktestRepository backtestRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeFactory = scopeFactory;
|
||||
_backtestRepository = backtestRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a complete backtest following the exact pattern of GetBacktestingResult from Backtester.cs
|
||||
/// </summary>
|
||||
/// <param name="config">The trading bot configuration for this backtest</param>
|
||||
/// <param name="candles">The candles to use for backtesting</param>
|
||||
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
|
||||
/// <param name="requestId">The request ID to associate with this backtest</param>
|
||||
/// <param name="metadata">Additional metadata to associate with this backtest</param>
|
||||
/// <returns>The complete backtest result</returns>
|
||||
public async Task<LightBacktest> RunBacktestAsync(
|
||||
TradingBotConfig config,
|
||||
List<Candle> candles,
|
||||
User user = null,
|
||||
bool save = false,
|
||||
bool withCandles = false,
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
{
|
||||
if (candles == null || candles.Count == 0)
|
||||
{
|
||||
throw new Exception("No candle to backtest");
|
||||
}
|
||||
|
||||
// Create a fresh TradingBotBase instance for this backtest
|
||||
var tradingBot = await CreateTradingBotInstance(config);
|
||||
tradingBot.Start();
|
||||
|
||||
var totalCandles = candles.Count;
|
||||
var currentCandle = 0;
|
||||
var lastLoggedPercentage = 0;
|
||||
|
||||
_logger.LogInformation("Starting backtest with {TotalCandles} candles for {Ticker} on {Timeframe}",
|
||||
totalCandles, config.Ticker, config.Timeframe);
|
||||
|
||||
// Initialize wallet balance with first candle
|
||||
tradingBot.WalletBalances.Clear();
|
||||
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
|
||||
|
||||
// Process all candles following the exact pattern from GetBacktestingResult
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
tradingBot.OptimizedCandles.Enqueue(candle);
|
||||
tradingBot.Candles.Add(candle);
|
||||
await tradingBot.Run();
|
||||
|
||||
currentCandle++;
|
||||
|
||||
// Check if wallet balance fell below 10 USDC and break if so
|
||||
var currentWalletBalance = tradingBot.WalletBalances.Values.LastOrDefault();
|
||||
if (currentWalletBalance < 10m)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Backtest stopped early: Wallet balance fell below 10 USDC (Current: {CurrentBalance:F2} USDC) at candle {CurrentCandle}/{TotalCandles} from {CandleDate}",
|
||||
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 fees = tradingBot.GetTotalFees();
|
||||
var scoringParams = new BacktestScoringParams(
|
||||
sharpeRatio: (double)stats.SharpeRatio,
|
||||
growthPercentage: (double)growthPercentage,
|
||||
hodlPercentage: (double)hodlPercentage,
|
||||
winRate: winRate,
|
||||
totalPnL: (double)finalPnl,
|
||||
fees: (double)fees,
|
||||
tradeCount: tradingBot.Positions.Count,
|
||||
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
|
||||
maxDrawdown: stats.MaxDrawdown,
|
||||
initialBalance: tradingBot.WalletBalances.FirstOrDefault().Value,
|
||||
tradingBalance: config.BotTradingBalance,
|
||||
startDate: candles[0].Date,
|
||||
endDate: candles.Last().Date,
|
||||
timeframe: config.Timeframe,
|
||||
moneyManagement: config.MoneyManagement
|
||||
);
|
||||
|
||||
var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams);
|
||||
|
||||
// Generate requestId if not provided
|
||||
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>())
|
||||
{
|
||||
FinalPnl = finalPnl,
|
||||
WinRate = winRate,
|
||||
GrowthPercentage = growthPercentage,
|
||||
HodlPercentage = hodlPercentage,
|
||||
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(),
|
||||
RequestId = finalRequestId,
|
||||
Metadata = metadata,
|
||||
StartDate = candles.FirstOrDefault()!.OpenTime,
|
||||
EndDate = candles.LastOrDefault()!.OpenTime,
|
||||
};
|
||||
|
||||
if (save && user != null)
|
||||
{
|
||||
_backtestRepository.InsertBacktestForUser(user, result);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Backtest to LightBacktest for safe Orleans serialization
|
||||
/// </summary>
|
||||
/// <param name="backtest">The full backtest to convert</param>
|
||||
/// <returns>A lightweight backtest suitable for Orleans serialization</returns>
|
||||
private LightBacktest ConvertToLightBacktest(Backtest backtest)
|
||||
{
|
||||
return new LightBacktest
|
||||
{
|
||||
Id = backtest.Id,
|
||||
Config = backtest.Config,
|
||||
FinalPnl = backtest.FinalPnl,
|
||||
WinRate = backtest.WinRate,
|
||||
GrowthPercentage = backtest.GrowthPercentage,
|
||||
HodlPercentage = backtest.HodlPercentage,
|
||||
StartDate = backtest.StartDate,
|
||||
EndDate = backtest.EndDate,
|
||||
MaxDrawdown = backtest.Statistics?.MaxDrawdown,
|
||||
Fees = backtest.Fees,
|
||||
SharpeRatio = (double?)backtest.Statistics?.SharpeRatio,
|
||||
Score = backtest.Score,
|
||||
ScoreMessage = backtest.ScoreMessage
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a TradingBotBase instance using composition for backtesting
|
||||
/// </summary>
|
||||
private async Task<TradingBotBase> CreateTradingBotInstance(TradingBotConfig config, User user = null)
|
||||
{
|
||||
// Validate configuration for backtesting
|
||||
if (config == null)
|
||||
{
|
||||
throw new InvalidOperationException("Bot configuration is not initialized");
|
||||
}
|
||||
|
||||
if (!config.IsForBacktest)
|
||||
{
|
||||
throw new InvalidOperationException("BacktestTradingBotGrain can only be used for backtesting");
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends notification if backtest meets criteria (following Backtester.cs pattern)
|
||||
/// </summary>
|
||||
private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (backtest.Score > 60)
|
||||
{
|
||||
// Note: In a real implementation, you would inject IMessengerService
|
||||
// For now, we'll just log the notification
|
||||
_logger.LogInformation("Backtest {BacktestId} scored {Score} - notification criteria met",
|
||||
backtest.Id, backtest.Score);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send backtest notification for backtest {Id}", backtest.Id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates indicator values (following Backtester.cs pattern)
|
||||
/// </summary>
|
||||
private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues(
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues,
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> botStrategiesValues)
|
||||
{
|
||||
var result = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
foreach (var indicator in indicatorsValues)
|
||||
{
|
||||
result[indicator.Key] = indicator.Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets indicators values (following Backtester.cs pattern)
|
||||
/// </summary>
|
||||
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
|
||||
List<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();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error building indicator {IndicatorType}", indicator.Type);
|
||||
}
|
||||
}
|
||||
|
||||
return indicatorsValues;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_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();
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@ namespace Managing.Application.Bots
|
||||
{
|
||||
public class SimpleBot : Bot
|
||||
{
|
||||
public readonly ILogger<TradingBot> Logger;
|
||||
public readonly ILogger<TradingBotBase> Logger;
|
||||
private readonly IBotService _botService;
|
||||
private readonly IBackupBotService _backupBotService;
|
||||
private Workflow _workflow;
|
||||
|
||||
public SimpleBot(string name, ILogger<TradingBot> logger, Workflow workflow, IBotService botService,
|
||||
public SimpleBot(string name, ILogger<TradingBotBase> logger, Workflow workflow, IBotService botService,
|
||||
IBackupBotService backupBotService) :
|
||||
base(name)
|
||||
{
|
||||
|
||||
@@ -22,9 +22,9 @@ using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Bots;
|
||||
|
||||
public class TradingBot : Bot, ITradingBot
|
||||
public class TradingBotBase : Bot, ITradingBot
|
||||
{
|
||||
public readonly ILogger<TradingBot> Logger;
|
||||
public readonly ILogger<TradingBotBase> Logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public TradingBotConfig Config { get; set; }
|
||||
@@ -41,8 +41,8 @@ public class TradingBot : Bot, ITradingBot
|
||||
|
||||
public int _maxSignals = 10; // Maximum number of signals to keep in memory
|
||||
|
||||
public TradingBot(
|
||||
ILogger<TradingBot> logger,
|
||||
public TradingBotBase(
|
||||
ILogger<TradingBotBase> logger,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
TradingBotConfig config
|
||||
)
|
||||
@@ -71,7 +71,9 @@ public class TradingBot : Bot, ITradingBot
|
||||
// Load indicators if scenario is provided in config
|
||||
if (Config.Scenario != null)
|
||||
{
|
||||
LoadIndicators(Config.Scenario);
|
||||
// Convert LightScenario to full Scenario for indicator loading
|
||||
var fullScenario = Config.Scenario.ToScenario();
|
||||
LoadIndicators(fullScenario);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -151,8 +153,6 @@ public class TradingBot : Bot, ITradingBot
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public async Task LoadAccount()
|
||||
@@ -185,8 +185,8 @@ public class TradingBot : Bot, ITradingBot
|
||||
}
|
||||
else
|
||||
{
|
||||
// Store the scenario in config and load indicators
|
||||
Config.Scenario = scenario;
|
||||
// Convert full Scenario to LightScenario for storage and load indicators
|
||||
Config.Scenario = LightScenario.FromScenario(scenario);
|
||||
LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario));
|
||||
|
||||
Logger.LogInformation($"Loaded scenario '{scenario.Name}' with {Indicators.Count} indicators");
|
||||
@@ -1594,6 +1594,9 @@ public class TradingBot : Bot, ITradingBot
|
||||
|
||||
public override async Task SaveBackup()
|
||||
{
|
||||
if (Config.IsForBacktest)
|
||||
return;
|
||||
|
||||
var data = new TradingBotBackup
|
||||
{
|
||||
Config = Config,
|
||||
@@ -1908,7 +1911,9 @@ public class TradingBot : Bot, ITradingBot
|
||||
{
|
||||
if (newConfig.Scenario != null)
|
||||
{
|
||||
LoadScenario(newConfig.Scenario);
|
||||
// Convert LightScenario to full Scenario for loading
|
||||
var fullScenario = newConfig.Scenario.ToScenario();
|
||||
LoadScenario(fullScenario);
|
||||
|
||||
// Compare indicators after scenario change
|
||||
var newIndicators = Indicators?.ToList() ?? new List<IIndicator>();
|
||||
@@ -2068,14 +2073,14 @@ public class TradingBot : Bot, ITradingBot
|
||||
}
|
||||
|
||||
var isInCooldown = positionClosingDate >= cooldownCandle.Date;
|
||||
|
||||
|
||||
if (isInCooldown)
|
||||
{
|
||||
var intervalMilliseconds = CandleExtensions.GetIntervalFromTimeframe(Config.Timeframe);
|
||||
var intervalMinutes = intervalMilliseconds / (1000.0 * 60.0); // Convert milliseconds to minutes
|
||||
var cooldownEndTime = cooldownCandle.Date.AddMinutes(intervalMinutes * Config.CooldownPeriod);
|
||||
var remainingTime = cooldownEndTime - DateTime.UtcNow;
|
||||
|
||||
|
||||
Logger.LogWarning(
|
||||
$"⏳ **Cooldown Period Active**\n" +
|
||||
$"Cannot open new positions\n" +
|
||||
117
src/Managing.Application/Bots/TradingBotGrainState.cs
Normal file
117
src/Managing.Application/Bots/TradingBotGrainState.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Domain.Users;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Bots;
|
||||
|
||||
/// <summary>
|
||||
/// Orleans grain state for TradingBot.
|
||||
/// This class represents the persistent state of a trading bot grain.
|
||||
/// All properties must be serializable for Orleans state management.
|
||||
/// </summary>
|
||||
[GenerateSerializer]
|
||||
public class TradingBotGrainState
|
||||
{
|
||||
/// <summary>
|
||||
/// The trading bot configuration
|
||||
/// </summary>
|
||||
[Id(0)]
|
||||
public TradingBotConfig Config { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Collection of trading signals generated by the bot
|
||||
/// </summary>
|
||||
[Id(1)]
|
||||
public HashSet<LightSignal> Signals { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of trading positions opened by the bot
|
||||
/// </summary>
|
||||
[Id(2)]
|
||||
public List<Position> Positions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Historical wallet balances tracked over time
|
||||
/// </summary>
|
||||
[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>
|
||||
[Id(5)]
|
||||
public DateTime StartupTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// When the bot was created
|
||||
/// </summary>
|
||||
[Id(6)]
|
||||
public DateTime CreateDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// The user who owns this bot
|
||||
/// </summary>
|
||||
[Id(7)]
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bot execution counter
|
||||
/// </summary>
|
||||
[Id(8)]
|
||||
public long ExecutionCount { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Bot identifier/name
|
||||
/// </summary>
|
||||
[Id(9)]
|
||||
public string Identifier { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Bot display name
|
||||
/// </summary>
|
||||
[Id(10)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Preload start date for candles
|
||||
/// </summary>
|
||||
[Id(11)]
|
||||
public DateTime PreloadSince { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Number of preloaded candles
|
||||
/// </summary>
|
||||
[Id(12)]
|
||||
public int PreloadedCandlesCount { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Timer interval for bot execution
|
||||
/// </summary>
|
||||
[Id(13)]
|
||||
public int Interval { get; set; } = 60000; // Default 1 minute
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of signals to keep in memory
|
||||
/// </summary>
|
||||
[Id(14)]
|
||||
public int MaxSignals { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the bot has been initialized
|
||||
/// </summary>
|
||||
[Id(15)]
|
||||
public bool IsInitialized { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Last time the bot state was persisted
|
||||
/// </summary>
|
||||
[Id(16)]
|
||||
public DateTime LastBackupTime { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
Reference in New Issue
Block a user