Refactoring TradingBotBase.cs + clean architecture (#38)
* Refactoring TradingBotBase.cs + clean architecture * Fix basic tests * Fix tests * Fix workers * Fix open positions * Fix closing position stucking the grain * Fix comments * Refactor candle handling to use IReadOnlyList for chronological order preservation across various components
This commit is contained in:
@@ -568,7 +568,7 @@ public class BacktestController : BaseController
|
||||
Timeframe = request.Config.Timeframe,
|
||||
IsForWatchingOnly = request.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = request.Config.BotTradingBalance,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = request.Config.CooldownPeriod ?? 1,
|
||||
MaxLossStreak = request.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = request.Config.MaxPositionTimeHours,
|
||||
|
||||
@@ -814,7 +814,7 @@ public class BotController : BaseController
|
||||
UseForSignalFiltering = request.Config.UseForSignalFiltering,
|
||||
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
|
||||
// Set computed/default properties
|
||||
IsForBacktest = false,
|
||||
TradingType = TradingType.Futures,
|
||||
FlipPosition = request.Config.FlipPosition,
|
||||
Name = request.Config.Name
|
||||
};
|
||||
@@ -976,7 +976,7 @@ public class BotController : BaseController
|
||||
UseForSignalFiltering = request.Config.UseForSignalFiltering,
|
||||
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
|
||||
// Set computed/default properties
|
||||
IsForBacktest = false,
|
||||
TradingType = TradingType.Futures,
|
||||
FlipPosition = request.Config.FlipPosition,
|
||||
Name = request.Config.Name
|
||||
};
|
||||
|
||||
@@ -350,7 +350,9 @@ public class DataController : ControllerBase
|
||||
{
|
||||
// Map ScenarioRequest to domain Scenario object
|
||||
var domainScenario = MapScenarioRequestToScenario(request.Scenario);
|
||||
indicatorsValues = TradingBox.CalculateIndicatorsValues(domainScenario, candles);
|
||||
// Convert to ordered List to preserve chronological order for indicators
|
||||
var candlesList = candles.OrderBy(c => c.Date).ToList();
|
||||
indicatorsValues = TradingBox.CalculateIndicatorsValues(domainScenario, candlesList);
|
||||
}
|
||||
|
||||
return Ok(new CandlesWithIndicatorsResponse
|
||||
|
||||
@@ -25,7 +25,8 @@ namespace Managing.Api.Controllers;
|
||||
public class TradingController : BaseController
|
||||
{
|
||||
private readonly ICommandHandler<OpenPositionRequest, Position> _openTradeCommandHandler;
|
||||
private readonly ICommandHandler<ClosePositionCommand, Position> _closeTradeCommandHandler;
|
||||
private readonly ICommandHandler<CloseBacktestFuturesPositionCommand, Position> _closeBacktestFuturesCommandHandler;
|
||||
private readonly ICommandHandler<CloseFuturesPositionCommand, Position> _closeFuturesCommandHandler;
|
||||
private readonly ITradingService _tradingService;
|
||||
private readonly IMoneyManagementService _moneyManagementService;
|
||||
private readonly IMediator _mediator;
|
||||
@@ -50,7 +51,8 @@ public class TradingController : BaseController
|
||||
public TradingController(
|
||||
ILogger<TradingController> logger,
|
||||
ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler,
|
||||
ICommandHandler<ClosePositionCommand, Position> closeTradeCommandHandler,
|
||||
ICommandHandler<CloseBacktestFuturesPositionCommand, Position> closeBacktestFuturesCommandHandler,
|
||||
ICommandHandler<CloseFuturesPositionCommand, Position> closeFuturesCommandHandler,
|
||||
ITradingService tradingService,
|
||||
IMediator mediator, IMoneyManagementService moneyManagementService,
|
||||
IUserService userService, IAdminConfigurationService adminService,
|
||||
@@ -60,7 +62,8 @@ public class TradingController : BaseController
|
||||
{
|
||||
_logger = logger;
|
||||
_openTradeCommandHandler = openTradeCommandHandler;
|
||||
_closeTradeCommandHandler = closeTradeCommandHandler;
|
||||
_closeBacktestFuturesCommandHandler = closeBacktestFuturesCommandHandler;
|
||||
_closeFuturesCommandHandler = closeFuturesCommandHandler;
|
||||
_tradingService = tradingService;
|
||||
_mediator = mediator;
|
||||
_moneyManagementService = moneyManagementService;
|
||||
@@ -98,20 +101,6 @@ public class TradingController : BaseController
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes a position identified by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="identifier">The unique identifier of the position to close.</param>
|
||||
/// <returns>The closed position.</returns>
|
||||
[HttpPost("ClosePosition")]
|
||||
public async Task<ActionResult<Position>> ClosePosition(Guid identifier)
|
||||
{
|
||||
var position = await _tradingService.GetPositionByIdentifierAsync(identifier);
|
||||
|
||||
var result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position, position.AccountId));
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a new position based on the provided parameters.
|
||||
/// </summary>
|
||||
|
||||
@@ -23,5 +23,5 @@ public interface IBacktestTradingBotGrain : IGrainWithGuidKey
|
||||
/// <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>
|
||||
Task<LightBacktest> RunBacktestAsync(TradingBotConfig config, HashSet<Candle> candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null);
|
||||
Task<LightBacktest> RunBacktestAsync(TradingBotConfig config, IReadOnlyList<Candle> candles, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public interface IExchangeService
|
||||
decimal takeProfitPrice,
|
||||
decimal quantity, bool isForPaperTrading = false, DateTime? currentDate = null);
|
||||
|
||||
Task<Trade> ClosePosition(Account account, Position position, decimal lastPrice, bool isForPaperTrading = false);
|
||||
Task<Trade> ClosePosition(Account account, Position position, decimal lastPrice);
|
||||
decimal GetVolume(Account account, Ticker ticker);
|
||||
Task<List<Trade>> GetTrades(Account account, Ticker ticker);
|
||||
Task<bool> CancelOrder(Account account, Ticker ticker);
|
||||
|
||||
@@ -169,7 +169,7 @@ public class BacktestTests : BaseTests
|
||||
Timeframe = Timeframe.FifteenMinutes,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
@@ -182,7 +182,7 @@ public class BacktestTests : BaseTests
|
||||
// Act - Call BacktestTradingBotGrain directly (no Orleans needed)
|
||||
var backtestResult = await _backtestGrain.RunBacktestAsync(
|
||||
config,
|
||||
candles.ToHashSet(),
|
||||
candles, // candles is already a List, no conversion needed
|
||||
_testUser,
|
||||
save: false,
|
||||
withCandles: false);
|
||||
@@ -212,19 +212,19 @@ public class BacktestTests : BaseTests
|
||||
Assert.NotNull(backtestResult);
|
||||
|
||||
// Financial metrics - using decimal precision
|
||||
Assert.Equal(-17.74m, Math.Round(backtestResult.FinalPnl, 2));
|
||||
Assert.Equal(-77.71m, Math.Round(backtestResult.NetPnl, 2));
|
||||
Assert.Equal(59.97m, Math.Round(backtestResult.Fees, 2));
|
||||
Assert.Equal(8.79m, Math.Round(backtestResult.FinalPnl, 2));
|
||||
Assert.Equal(-61.36m, Math.Round(backtestResult.NetPnl, 2));
|
||||
Assert.Equal(66.46m, Math.Round(backtestResult.Fees, 2));
|
||||
Assert.Equal(1000.0m, backtestResult.InitialBalance);
|
||||
|
||||
// Performance metrics
|
||||
Assert.Equal(32, backtestResult.WinRate);
|
||||
Assert.Equal(-1.77m, Math.Round(backtestResult.GrowthPercentage, 2));
|
||||
Assert.Equal(31, backtestResult.WinRate);
|
||||
Assert.Equal(-6.14m, Math.Round(backtestResult.GrowthPercentage, 2));
|
||||
Assert.Equal(-0.67m, Math.Round(backtestResult.HodlPercentage, 2));
|
||||
|
||||
// Risk metrics
|
||||
Assert.Equal(158.79m, Math.Round(backtestResult.MaxDrawdown.Value, 2));
|
||||
Assert.Equal(-0.004, Math.Round(backtestResult.SharpeRatio.Value, 3));
|
||||
Assert.Equal(202.29m, Math.Round(backtestResult.MaxDrawdown.Value, 2));
|
||||
Assert.Equal(-0.015, Math.Round(backtestResult.SharpeRatio.Value, 3));
|
||||
Assert.True(Math.Abs(backtestResult.Score - 0.0) < 0.001,
|
||||
$"Score {backtestResult.Score} should be within 0.001 of expected value 0.0");
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Managing.Application.Tests
|
||||
// Act
|
||||
foreach (var candle in _candles)
|
||||
{
|
||||
var signals = rsiStrategy.Run(new HashSet<Candle> { candle });
|
||||
var signals = rsiStrategy.Run(new List<Candle> { candle });
|
||||
}
|
||||
|
||||
if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
|
||||
@@ -48,7 +48,7 @@ namespace Managing.Application.Tests
|
||||
// Act
|
||||
foreach (var candle in _candles)
|
||||
{
|
||||
var signals = macdStrategy.Run(new HashSet<Candle> { candle });
|
||||
var signals = macdStrategy.Run(new List<Candle> { candle });
|
||||
}
|
||||
|
||||
if (macdStrategy.Signals != null && macdStrategy.Signals.Count > 0)
|
||||
@@ -69,7 +69,7 @@ namespace Managing.Application.Tests
|
||||
// Act
|
||||
foreach (var candle in _candles)
|
||||
{
|
||||
var signals = superTrendStrategy.Run(new HashSet<Candle> { candle });
|
||||
var signals = superTrendStrategy.Run(new List<Candle> { candle });
|
||||
}
|
||||
|
||||
if (superTrendStrategy.Signals != null && superTrendStrategy.Signals.Count > 0)
|
||||
@@ -90,7 +90,7 @@ namespace Managing.Application.Tests
|
||||
// Act
|
||||
foreach (var candle in _candles)
|
||||
{
|
||||
var signals = chandelierExitStrategy.Run(new HashSet<Candle> { candle });
|
||||
var signals = chandelierExitStrategy.Run(new List<Candle> { candle });
|
||||
}
|
||||
|
||||
if (chandelierExitStrategy.Signals is { Count: > 0 })
|
||||
@@ -111,7 +111,7 @@ namespace Managing.Application.Tests
|
||||
// Act
|
||||
foreach (var candle in _candles)
|
||||
{
|
||||
var signals = emaTrendStrategy.Run(new HashSet<Candle> { candle });
|
||||
var signals = emaTrendStrategy.Run(new List<Candle> { candle });
|
||||
}
|
||||
|
||||
if (emaTrendStrategy.Signals != null && emaTrendStrategy.Signals.Count > 0)
|
||||
@@ -133,7 +133,7 @@ namespace Managing.Application.Tests
|
||||
// Act
|
||||
foreach (var candle in _candles)
|
||||
{
|
||||
var signals = stochRsiStrategy.Run(new HashSet<Candle> { candle });
|
||||
var signals = stochRsiStrategy.Run(new List<Candle> { candle });
|
||||
}
|
||||
|
||||
if (stochRsiStrategy.Signals != null && stochRsiStrategy.Signals.Count > 0)
|
||||
|
||||
@@ -50,14 +50,15 @@ public class PositionTests : BaseTests
|
||||
PositionInitiator.User,
|
||||
DateTime.UtcNow, new User())
|
||||
{
|
||||
Open = openTrade
|
||||
Open = openTrade,
|
||||
TradingType = TradingType.Futures // Set trading type for the position
|
||||
};
|
||||
var command = new ClosePositionCommand(position, 1);
|
||||
_ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<Guid>())).ReturnsAsync(position);
|
||||
_ = _tradingService.Setup(m => m.GetPositionByIdentifierAsync(It.IsAny<Guid>())).ReturnsAsync(position);
|
||||
|
||||
var mockScope = new Mock<IServiceScopeFactory>();
|
||||
var handler = new ClosePositionCommandHandler(
|
||||
var command = new CloseFuturesPositionCommand(position, 1);
|
||||
var handler = new CloseFuturesPositionCommandHandler(
|
||||
_exchangeService,
|
||||
_accountService.Object,
|
||||
_tradingService.Object,
|
||||
|
||||
@@ -129,7 +129,7 @@ public class BacktestExecutor
|
||||
/// <returns>The lightweight backtest result</returns>
|
||||
public async Task<LightBacktest> ExecuteAsync(
|
||||
TradingBotConfig config,
|
||||
HashSet<Candle> candles,
|
||||
IReadOnlyList<Candle> candles,
|
||||
User user,
|
||||
bool save = false,
|
||||
bool withCandles = false,
|
||||
@@ -166,7 +166,9 @@ public class BacktestExecutor
|
||||
|
||||
// Create a fresh TradingBotBase instance for this backtest
|
||||
var tradingBot = CreateTradingBotInstance(config);
|
||||
tradingBot.Account = user.Accounts.First();
|
||||
var account = user.Accounts.First();
|
||||
account.User = user; // Ensure Account.User is set for backtest
|
||||
tradingBot.Account = account;
|
||||
|
||||
var totalCandles = candles.Count;
|
||||
var currentCandle = 0;
|
||||
@@ -220,8 +222,9 @@ public class BacktestExecutor
|
||||
// The signal calculation depends on rolling window state and cannot be pre-calculated effectively
|
||||
|
||||
// Use optimized rolling window approach - TradingBox.GetSignal only needs last 600 candles
|
||||
// Use List<Candle> directly to preserve chronological order and enable incremental updates
|
||||
const int RollingWindowSize = 600; // TradingBox.GetSignal only needs last 600 candles
|
||||
var rollingWindowCandles = new Queue<Candle>(RollingWindowSize);
|
||||
var rollingWindowCandles = new List<Candle>(RollingWindowSize); // Pre-allocate capacity for performance
|
||||
var candlesProcessed = 0;
|
||||
|
||||
// Signal caching optimization - reduce signal update frequency for better performance
|
||||
@@ -253,21 +256,20 @@ public class BacktestExecutor
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Maintain rolling window of last 600 candles to prevent exponential memory growth
|
||||
rollingWindowCandles.Enqueue(candle);
|
||||
if (rollingWindowCandles.Count > RollingWindowSize)
|
||||
// Incremental updates: remove oldest if at capacity, then add newest
|
||||
// This preserves chronological order and avoids expensive HashSet recreation
|
||||
if (rollingWindowCandles.Count >= RollingWindowSize)
|
||||
{
|
||||
rollingWindowCandles.Dequeue(); // Remove oldest candle
|
||||
rollingWindowCandles.RemoveAt(0); // Remove oldest candle (O(n) but only 600 items max)
|
||||
}
|
||||
rollingWindowCandles.Add(candle); // Add newest candle (O(1) amortized)
|
||||
|
||||
tradingBot.LastCandle = candle;
|
||||
|
||||
// Run with optimized backtest path (minimize async calls)
|
||||
var signalUpdateStart = Stopwatch.GetTimestamp();
|
||||
// Convert rolling window to HashSet for TradingBot.UpdateSignals compatibility
|
||||
// NOTE: Recreating HashSet each iteration is necessary to maintain correct enumeration order
|
||||
// Incremental updates break business logic (changes PnL results)
|
||||
var fixedCandles = new HashSet<Candle>(rollingWindowCandles);
|
||||
await tradingBot.UpdateSignals(fixedCandles, preCalculatedIndicatorValues);
|
||||
// Pass List<Candle> directly - no conversion needed, order is preserved
|
||||
await tradingBot.UpdateSignals(rollingWindowCandles, preCalculatedIndicatorValues);
|
||||
signalUpdateTotalTime += Stopwatch.GetElapsedTime(signalUpdateStart);
|
||||
|
||||
var backtestStepStart = Stopwatch.GetTimestamp();
|
||||
@@ -542,7 +544,7 @@ public class BacktestExecutor
|
||||
throw new InvalidOperationException("Bot configuration is not initialized");
|
||||
}
|
||||
|
||||
if (!config.IsForBacktest)
|
||||
if (config.TradingType != TradingType.BacktestFutures)
|
||||
{
|
||||
throw new InvalidOperationException("BacktestExecutor can only be used for backtesting");
|
||||
}
|
||||
@@ -550,7 +552,7 @@ public class BacktestExecutor
|
||||
// Create the trading bot instance
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
|
||||
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
|
||||
var tradingBot = new BacktestFuturesBot(logger, _scopeFactory, config);
|
||||
return tradingBot;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,19 +42,22 @@ public class BacktestExecutorAdapter : IBacktester
|
||||
object metadata = null)
|
||||
{
|
||||
// Load candles using ExchangeService
|
||||
var candles = await _exchangeService.GetCandlesInflux(
|
||||
var candlesHashSet = await _exchangeService.GetCandlesInflux(
|
||||
TradingExchanges.Evm,
|
||||
config.Ticker,
|
||||
startDate,
|
||||
config.Timeframe,
|
||||
endDate);
|
||||
|
||||
if (candles == null || candles.Count == 0)
|
||||
if (candlesHashSet == null || candlesHashSet.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No candles found for {config.Ticker} on {config.Timeframe} from {startDate} to {endDate}");
|
||||
}
|
||||
|
||||
// Convert to ordered List to preserve chronological order for backtest
|
||||
var candles = candlesHashSet.OrderBy(c => c.Date).ToList();
|
||||
|
||||
// Execute using BacktestExecutor
|
||||
var result = await _executor.ExecuteAsync(
|
||||
config,
|
||||
@@ -73,12 +76,15 @@ public class BacktestExecutorAdapter : IBacktester
|
||||
|
||||
public async Task<LightBacktest> RunTradingBotBacktest(
|
||||
TradingBotConfig config,
|
||||
HashSet<Candle> candles,
|
||||
HashSet<Candle> candlesHashSet,
|
||||
User user = null,
|
||||
bool withCandles = false,
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
{
|
||||
// Convert to ordered List to preserve chronological order for backtest
|
||||
var candles = candlesHashSet.OrderBy(c => c.Date).ToList();
|
||||
|
||||
// Execute using BacktestExecutor
|
||||
var result = await _executor.ExecuteAsync(
|
||||
config,
|
||||
|
||||
@@ -197,7 +197,7 @@ public class JobService
|
||||
Timeframe = backtestRequest.Config.Timeframe,
|
||||
IsForWatchingOnly = backtestRequest.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = backtestRequest.Config.BotTradingBalance,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = backtestRequest.Config.CooldownPeriod ?? 1,
|
||||
MaxLossStreak = backtestRequest.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = backtestRequest.Config.MaxPositionTimeHours,
|
||||
|
||||
@@ -1,5 +1,332 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Application.Trading.Handlers;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Domain.Users;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Orleans.Streams;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Bots;
|
||||
|
||||
public class BacktestFuturesBot
|
||||
public class BacktestFuturesBot : TradingBotBase, ITradingBot
|
||||
{
|
||||
public BacktestFuturesBot(
|
||||
ILogger<TradingBotBase> logger,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
TradingBotConfig config,
|
||||
IStreamProvider? streamProvider = null
|
||||
) : base(logger, scopeFactory, config, streamProvider)
|
||||
{
|
||||
// Backtest-specific initialization
|
||||
Config.TradingType = TradingType.BacktestFutures;
|
||||
}
|
||||
|
||||
public override async Task Start(BotStatus previousStatus)
|
||||
{
|
||||
// Backtest mode: Skip account loading and broker initialization
|
||||
// Just log basic startup info
|
||||
await LogInformation($"🔬 Backtest Bot Started\n" +
|
||||
$"📊 Testing Setup:\n" +
|
||||
$"🎯 Ticker: `{Config.Ticker}`\n" +
|
||||
$"⏰ Timeframe: `{Config.Timeframe}`\n" +
|
||||
$"🎮 Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" +
|
||||
$"💰 Initial Balance: `${Config.BotTradingBalance:F2}`\n" +
|
||||
$"✅ Ready to run backtest simulation");
|
||||
}
|
||||
|
||||
public override async Task Run()
|
||||
{
|
||||
// Backtest signal update is handled in BacktestExecutor loop
|
||||
// No need to call UpdateSignals() here
|
||||
|
||||
if (!Config.IsForWatchingOnly)
|
||||
await ManagePositions();
|
||||
|
||||
UpdateWalletBalances();
|
||||
|
||||
// Backtest logging - simplified, no account dependency
|
||||
ExecutionCount++;
|
||||
Logger.LogInformation(
|
||||
"[Backtest][{BotName}] Execution {ExecutionCount} - LastCandleDate: {LastCandleDate}, Signals: {SignalCount}, Positions: {PositionCount}",
|
||||
Config.Name, ExecutionCount, LastCandle?.Date, Signals.Count, Positions.Count);
|
||||
}
|
||||
|
||||
|
||||
protected override async Task<Position> GetInternalPositionForUpdate(Position position)
|
||||
{
|
||||
// In backtest mode, return the position as-is (no database lookup needed)
|
||||
return position;
|
||||
}
|
||||
|
||||
protected override async Task<List<Position>> GetBrokerPositionsForUpdate(Account account)
|
||||
{
|
||||
// In backtest mode, return empty list (no broker positions to check)
|
||||
return new List<Position>();
|
||||
}
|
||||
|
||||
protected override async Task UpdatePositionWithBrokerData(Position position, List<Position> brokerPositions)
|
||||
{
|
||||
// In backtest mode, skip broker synchronization
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task<Candle> GetCurrentCandleForPositionClose(Account account, string ticker)
|
||||
{
|
||||
// In backtest mode, use LastCandle
|
||||
return LastCandle;
|
||||
}
|
||||
|
||||
protected override async Task<bool> CanOpenPositionWithBrokerChecks(LightSignal signal)
|
||||
{
|
||||
// In backtest mode, skip broker position checks
|
||||
return await CanOpenPosition(signal);
|
||||
}
|
||||
|
||||
protected override async Task LoadAccountAsync()
|
||||
{
|
||||
// In backtest mode, skip account loading
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task VerifyAndUpdateBalanceAsync()
|
||||
{
|
||||
// In backtest mode, skip balance verification
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task SendPositionToCopyTradingStream(Position position)
|
||||
{
|
||||
// In backtest mode, skip copy trading stream
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task NotifyAgentAndPlatformAsync(NotificationEventType eventType, Position position)
|
||||
{
|
||||
// In backtest mode, skip notifications
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task UpdatePositionInDatabaseAsync(Position position)
|
||||
{
|
||||
// In backtest mode, skip database updates
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task SendClosedPositionToMessenger(Position position, User user)
|
||||
{
|
||||
// In backtest mode, skip messenger updates
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task CancelAllOrdersAsync()
|
||||
{
|
||||
// In backtest mode, no orders to cancel
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task LogInformationAsync(string message)
|
||||
{
|
||||
// In backtest mode, skip user notifications, just log to system
|
||||
if (Config.TradingType == TradingType.BacktestFutures)
|
||||
return;
|
||||
|
||||
await base.LogInformationAsync(message);
|
||||
}
|
||||
|
||||
protected override async Task LogWarningAsync(string message)
|
||||
{
|
||||
// In backtest mode, skip user notifications, just log to system
|
||||
if (Config.TradingType == TradingType.BacktestFutures)
|
||||
return;
|
||||
|
||||
await base.LogWarningAsync(message);
|
||||
}
|
||||
|
||||
protected override async Task LogDebugAsync(string message)
|
||||
{
|
||||
// In backtest mode, skip messenger debug logs
|
||||
if (Config.TradingType == TradingType.BacktestFutures)
|
||||
return;
|
||||
|
||||
await base.LogDebugAsync(message);
|
||||
}
|
||||
|
||||
protected override async Task SendTradeMessageAsync(string message, bool isBadBehavior)
|
||||
{
|
||||
// In backtest mode, skip trade messages
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task UpdateSignalsCore(IReadOnlyList<Candle> candles,
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues = null)
|
||||
{
|
||||
// Call base implementation for common logic (flip check, cooldown check)
|
||||
await base.UpdateSignalsCore(candles, preCalculatedIndicatorValues);
|
||||
|
||||
// For backtest, if no candles provided (called from Run()), skip signal generation
|
||||
// Signals are generated in BacktestExecutor with rolling window candles
|
||||
if (candles == null || candles.Count == 0)
|
||||
return;
|
||||
|
||||
if (Config.Scenario == null)
|
||||
throw new ArgumentNullException(nameof(Config.Scenario), "Config.Scenario cannot be null");
|
||||
|
||||
// Use TradingBox.GetSignal for backtest with pre-calculated indicators
|
||||
var backtestSignal = TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod,
|
||||
preCalculatedIndicatorValues);
|
||||
if (backtestSignal == null) return;
|
||||
|
||||
await AddSignal(backtestSignal);
|
||||
}
|
||||
|
||||
protected override async Task<decimal> GetLastPriceForPositionOpeningAsync()
|
||||
{
|
||||
// For backtest, use LastCandle close price
|
||||
return LastCandle?.Close ?? 0;
|
||||
}
|
||||
|
||||
protected override async Task<bool> CanOpenPosition(LightSignal signal)
|
||||
{
|
||||
// Backtest-specific logic: only check cooldown and loss streak
|
||||
// No broker checks, no synth risk assessment, no startup cycle check needed
|
||||
return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal);
|
||||
}
|
||||
|
||||
protected override async Task<Position> HandleFlipPosition(LightSignal signal, Position openedPosition,
|
||||
LightSignal previousSignal, decimal lastPrice)
|
||||
{
|
||||
// Backtest-specific flip logic
|
||||
if (Config.FlipPosition)
|
||||
{
|
||||
var isPositionInProfit = (openedPosition.ProfitAndLoss?.Realized ?? 0) > 0;
|
||||
var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit;
|
||||
|
||||
if (shouldFlip)
|
||||
{
|
||||
var flipReason = Config.FlipOnlyWhenInProfit
|
||||
? "current position is in profit"
|
||||
: "FlipOnlyWhenInProfit is disabled";
|
||||
|
||||
await LogInformationAsync(
|
||||
$"🔄 Position Flip Initiated\nFlipping position due to opposite signal\nReason: {flipReason}");
|
||||
await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true);
|
||||
await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped);
|
||||
var newPosition = await OpenPosition(signal);
|
||||
await LogInformationAsync(
|
||||
$"✅ Position Flipped\nPosition: `{previousSignal.Identifier}` → `{signal.Identifier}`\nPrice: `${lastPrice}`");
|
||||
return newPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentPnl = openedPosition.ProfitAndLoss?.Realized ?? 0;
|
||||
await LogInformationAsync(
|
||||
$"💸 Flip Blocked - Not Profitable\nPosition `{previousSignal.Identifier}` PnL: `${currentPnl:F2}`\nSignal `{signal.Identifier}` will wait for profitability");
|
||||
|
||||
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await LogInformationAsync(
|
||||
$"🚫 Flip Disabled\nPosition already open for: `{previousSignal.Identifier}`\nFlipping disabled, new signal expired");
|
||||
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task<Position> ExecuteOpenPosition(LightSignal signal, decimal lastPrice)
|
||||
{
|
||||
// Backtest-specific position opening: no balance verification, no exchange calls
|
||||
if (Account == null || Account.User == null)
|
||||
{
|
||||
throw new InvalidOperationException("Account and Account.User must be set before opening a position");
|
||||
}
|
||||
|
||||
var command = new OpenPositionRequest(
|
||||
Config.AccountName,
|
||||
Config.MoneyManagement,
|
||||
signal.Direction,
|
||||
Config.Ticker,
|
||||
PositionInitiator.Bot,
|
||||
signal.Date,
|
||||
Account.User,
|
||||
Config.BotTradingBalance,
|
||||
isForPaperTrading: true, // Backtest is always paper trading
|
||||
lastPrice,
|
||||
signalIdentifier: signal.Identifier,
|
||||
initiatorIdentifier: Identifier,
|
||||
tradingType: Config.TradingType);
|
||||
|
||||
var position = await ServiceScopeHelpers
|
||||
.WithScopedServices<IExchangeService, IAccountService, ITradingService, Position>(
|
||||
_scopeFactory,
|
||||
async (exchangeService, accountService, tradingService) =>
|
||||
{
|
||||
return await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
|
||||
.Handle(command);
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
public override async Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice,
|
||||
bool tradeClosingPosition = false, bool forceMarketClose = false)
|
||||
{
|
||||
await LogInformationAsync(
|
||||
$"🔧 Closing {position.OriginDirection} Trade\nTicker: `{Config.Ticker}`\nPrice: `${lastPrice}`\n📋 Type: `{tradeToClose.TradeType}`\n📊 Quantity: `{tradeToClose.Quantity:F5}`");
|
||||
|
||||
// Backtest-specific: no exchange quantity check, no grace period, direct close
|
||||
var command = new CloseBacktestFuturesPositionCommand(position, position.AccountId, lastPrice);
|
||||
try
|
||||
{
|
||||
Position closedPosition = null;
|
||||
await ServiceScopeHelpers.WithScopedServices<IExchangeService, IAccountService, ITradingService>(
|
||||
_scopeFactory, async (exchangeService, accountService, tradingService) =>
|
||||
{
|
||||
closedPosition =
|
||||
await new CloseBacktestFuturesPositionCommandHandler(exchangeService, accountService, tradingService,
|
||||
_scopeFactory)
|
||||
.Handle(command);
|
||||
});
|
||||
|
||||
if (closedPosition.Status == PositionStatus.Finished || closedPosition.Status == PositionStatus.Flipped)
|
||||
{
|
||||
if (tradeClosingPosition)
|
||||
{
|
||||
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
|
||||
}
|
||||
|
||||
await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null,
|
||||
forceMarketClose);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Wrong position status : {closedPosition.Status}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await LogWarningAsync($"Position {signal.Identifier} not closed : {ex.Message}");
|
||||
|
||||
if (position.Status == PositionStatus.Canceled || position.Status == PositionStatus.Rejected)
|
||||
{
|
||||
// Trade close on exchange => Should close trade manually
|
||||
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
|
||||
// Ensure trade dates are properly updated even for canceled/rejected positions
|
||||
await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null,
|
||||
forceMarketClose);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,7 +53,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
/// <returns>The complete backtest result</returns>
|
||||
public async Task<LightBacktest> RunBacktestAsync(
|
||||
TradingBotConfig config,
|
||||
HashSet<Candle> candles,
|
||||
IReadOnlyList<Candle> candles,
|
||||
User user = null,
|
||||
bool save = false,
|
||||
bool withCandles = false,
|
||||
@@ -67,7 +67,9 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
|
||||
// Create a fresh TradingBotBase instance for this backtest
|
||||
var tradingBot = await CreateTradingBotInstance(config);
|
||||
tradingBot.Account = user.Accounts.First();
|
||||
var account = user.Accounts.First();
|
||||
account.User = user; // Ensure Account.User is set for backtest
|
||||
tradingBot.Account = account;
|
||||
|
||||
var totalCandles = candles.Count;
|
||||
var currentCandle = 0;
|
||||
@@ -81,7 +83,9 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
|
||||
var initialBalance = config.BotTradingBalance;
|
||||
|
||||
var fixedCandles = new HashSet<Candle>();
|
||||
const int RollingWindowSize = 600; // TradingBox.GetSignal only needs last 600 candles
|
||||
// Use List<Candle> directly to preserve chronological order and enable incremental updates
|
||||
var rollingWindowCandles = new List<Candle>(RollingWindowSize); // Pre-allocate capacity for performance
|
||||
var lastYieldTime = DateTime.UtcNow;
|
||||
const int yieldIntervalMs = 5000; // Yield control every 5 seconds to prevent timeout
|
||||
const int candlesPerBatch = 100; // Process in batches to allow Orleans to check for cancellation
|
||||
@@ -89,11 +93,19 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
// Process all candles following the exact pattern from GetBacktestingResult
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
fixedCandles.Add(candle);
|
||||
// Maintain rolling window: remove oldest if at capacity, then add newest
|
||||
// This preserves chronological order and avoids expensive HashSet recreation
|
||||
if (rollingWindowCandles.Count >= RollingWindowSize)
|
||||
{
|
||||
rollingWindowCandles.RemoveAt(0); // Remove oldest candle (O(n) but only 600 items max)
|
||||
}
|
||||
rollingWindowCandles.Add(candle); // Add newest candle (O(1) amortized)
|
||||
|
||||
tradingBot.LastCandle = candle;
|
||||
|
||||
// Update signals manually only for backtesting
|
||||
await tradingBot.UpdateSignals(fixedCandles);
|
||||
// Update signals manually only for backtesting with rolling window
|
||||
// Pass List<Candle> directly - no conversion needed, order is preserved
|
||||
await tradingBot.UpdateSignals(rollingWindowCandles);
|
||||
await tradingBot.Run();
|
||||
|
||||
currentCandle++;
|
||||
@@ -132,11 +144,12 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
|
||||
_logger.LogInformation("Backtest processing completed. Calculating final results...");
|
||||
|
||||
var finalPnl = TradingBox.GetTotalNetPnL(tradingBot.Positions);
|
||||
var realizedPnl = TradingBox.GetTotalRealizedPnL(tradingBot.Positions); // PnL before fees
|
||||
var netPnl = TradingBox.GetTotalNetPnL(tradingBot.Positions); // PnL after fees
|
||||
var winRate = TradingBox.GetWinRate(tradingBot.Positions);
|
||||
var stats = TradingBox.GetStatistics(tradingBot.WalletBalances);
|
||||
var growthPercentage =
|
||||
TradingBox.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, finalPnl);
|
||||
TradingBox.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, netPnl);
|
||||
var hodlPercentage = TradingBox.GetHodlPercentage(candles.First(), candles.Last());
|
||||
|
||||
var fees = TradingBox.GetTotalFees(tradingBot.Positions);
|
||||
@@ -145,7 +158,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
growthPercentage: (double)growthPercentage,
|
||||
hodlPercentage: (double)hodlPercentage,
|
||||
winRate: winRate,
|
||||
totalPnL: (double)finalPnl,
|
||||
totalPnL: (double)realizedPnl,
|
||||
fees: (double)fees,
|
||||
tradeCount: tradingBot.Positions.Count,
|
||||
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
|
||||
@@ -166,7 +179,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
// Create backtest result with conditional candles and indicators values
|
||||
var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals)
|
||||
{
|
||||
FinalPnl = finalPnl,
|
||||
FinalPnl = realizedPnl, // Realized PnL before fees
|
||||
WinRate = winRate,
|
||||
GrowthPercentage = growthPercentage,
|
||||
HodlPercentage = hodlPercentage,
|
||||
@@ -180,7 +193,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
StartDate = candles.FirstOrDefault()!.OpenTime,
|
||||
EndDate = candles.LastOrDefault()!.OpenTime,
|
||||
InitialBalance = initialBalance,
|
||||
NetPnl = finalPnl - fees,
|
||||
NetPnl = netPnl, // Net PnL after fees
|
||||
};
|
||||
|
||||
if (save && user != null)
|
||||
@@ -233,14 +246,14 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
throw new InvalidOperationException("Bot configuration is not initialized");
|
||||
}
|
||||
|
||||
if (!config.IsForBacktest)
|
||||
if (config.TradingType != TradingType.BacktestFutures)
|
||||
{
|
||||
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);
|
||||
var tradingBot = new BacktestFuturesBot(logger, _scopeFactory, config);
|
||||
return tradingBot;
|
||||
}
|
||||
|
||||
@@ -284,7 +297,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
/// Gets indicators values (following Backtester.cs pattern)
|
||||
/// </summary>
|
||||
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<LightIndicator> indicators,
|
||||
HashSet<Candle> candles)
|
||||
IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
||||
throw new InvalidOperationException("Bot configuration is not properly initialized");
|
||||
}
|
||||
|
||||
if (config.IsForBacktest)
|
||||
if (config.TradingType == TradingType.BacktestFutures)
|
||||
{
|
||||
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
|
||||
}
|
||||
@@ -531,7 +531,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
|
||||
var streamProvider = this.GetStreamProvider("ManagingStreamProvider");
|
||||
var tradingBot = new TradingBotBase(logger, _scopeFactory, config, streamProvider);
|
||||
var tradingBot = new FuturesBot(logger, _scopeFactory, config, streamProvider);
|
||||
|
||||
// Load state into the trading bot instance
|
||||
LoadStateIntoTradingBot(tradingBot);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -955,7 +955,7 @@ public class TradingBotChromosome : ChromosomeBase
|
||||
Ticker = request.Ticker,
|
||||
Timeframe = request.Timeframe,
|
||||
BotTradingBalance = request.Balance,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
IsForWatchingOnly = false,
|
||||
CooldownPeriod = Convert.ToInt32(genes[2].Value),
|
||||
MaxLossStreak = Convert.ToInt32(genes[3].Value),
|
||||
@@ -1104,7 +1104,7 @@ public class TradingBotFitness : IFitness
|
||||
_serviceScopeFactory,
|
||||
async executor => await executor.ExecuteAsync(
|
||||
config,
|
||||
_candles,
|
||||
_candles.OrderBy(c => c.Date).ToList(),
|
||||
_request.User,
|
||||
save: true,
|
||||
withCandles: false,
|
||||
|
||||
@@ -1,534 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Orleans;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Strategies;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Orleans.Concurrency;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// Stateless worker grain for processing bundle backtest requests
|
||||
/// Uses the bundle request ID as the primary key (Guid)
|
||||
/// Implements IRemindable for automatic retry of failed bundles
|
||||
/// Uses custom compute placement with random fallback.
|
||||
/// </summary>
|
||||
[StatelessWorker]
|
||||
[TradingPlacement] // Use custom compute placement with random fallback
|
||||
public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable
|
||||
{
|
||||
private readonly ILogger<BundleBacktestGrain> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
// Reminder configuration
|
||||
private const string RETRY_REMINDER_NAME = "BundleBacktestRetry";
|
||||
private static readonly TimeSpan RETRY_INTERVAL = TimeSpan.FromMinutes(30);
|
||||
|
||||
public BundleBacktestGrain(
|
||||
ILogger<BundleBacktestGrain> logger,
|
||||
IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
public async Task ProcessBundleRequestAsync()
|
||||
{
|
||||
// Get the RequestId from the grain's primary key
|
||||
var bundleRequestId = this.GetPrimaryKey();
|
||||
|
||||
try
|
||||
{
|
||||
// Create a new service scope to get fresh instances of services with scoped DbContext
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var backtester = scope.ServiceProvider.GetRequiredService<IBacktester>();
|
||||
var messengerService = scope.ServiceProvider.GetRequiredService<IMessengerService>();
|
||||
|
||||
// Get the specific bundle request by ID
|
||||
var bundleRequest = await GetBundleRequestById(backtester, bundleRequestId);
|
||||
if (bundleRequest == null)
|
||||
{
|
||||
_logger.LogError("Bundle request {RequestId} not found", bundleRequestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process only this specific bundle request
|
||||
await ProcessBundleRequest(bundleRequest, backtester, messengerService);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in BundleBacktestGrain for request {RequestId}", bundleRequestId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<BundleBacktestRequest> GetBundleRequestById(IBacktester backtester, Guid bundleRequestId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get pending and failed bundle backtest requests for retry capability
|
||||
var pendingRequests =
|
||||
await backtester.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Pending);
|
||||
var failedRequests =
|
||||
await backtester.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Failed);
|
||||
|
||||
var allRequests = pendingRequests.Concat(failedRequests);
|
||||
return allRequests.FirstOrDefault(r => r.RequestId == bundleRequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get bundle request {RequestId}", bundleRequestId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessBundleRequest(
|
||||
BundleBacktestRequest bundleRequest,
|
||||
IBacktester backtester,
|
||||
IMessengerService messengerService)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Starting to process bundle backtest request {RequestId}", bundleRequest.RequestId);
|
||||
|
||||
// Update status to running
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Running;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
// Generate backtest requests from variant configuration
|
||||
var backtestRequests = await GenerateBacktestRequestsFromVariants(bundleRequest);
|
||||
if (backtestRequests == null || !backtestRequests.Any())
|
||||
{
|
||||
throw new InvalidOperationException("Failed to generate backtest requests from variants");
|
||||
}
|
||||
|
||||
// Process each backtest request sequentially
|
||||
for (int i = 0; i < backtestRequests.Count; i++)
|
||||
{
|
||||
await ProcessSingleBacktest(backtester, backtestRequests[i], bundleRequest, i);
|
||||
}
|
||||
|
||||
// Update final status and send notifications
|
||||
await UpdateFinalStatus(bundleRequest, backtester, messengerService);
|
||||
|
||||
_logger.LogInformation("Completed processing bundle backtest request {RequestId} with status {Status}",
|
||||
bundleRequest.RequestId, bundleRequest.Status);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing bundle backtest request {RequestId}", bundleRequest.RequestId);
|
||||
SentrySdk.CaptureException(ex);
|
||||
await HandleBundleRequestError(bundleRequest, backtester, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates individual backtest requests from variant configuration
|
||||
/// </summary>
|
||||
private async Task<List<RunBacktestRequest>> GenerateBacktestRequestsFromVariants(
|
||||
BundleBacktestRequest bundleRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Deserialize the variant configurations
|
||||
var universalConfig =
|
||||
JsonSerializer.Deserialize<BundleBacktestUniversalConfig>(bundleRequest.UniversalConfigJson);
|
||||
var dateTimeRanges = JsonSerializer.Deserialize<List<DateTimeRange>>(bundleRequest.DateTimeRangesJson);
|
||||
var moneyManagementVariants =
|
||||
JsonSerializer.Deserialize<List<MoneyManagementVariant>>(bundleRequest.MoneyManagementVariantsJson);
|
||||
var tickerVariants = JsonSerializer.Deserialize<List<Ticker>>(bundleRequest.TickerVariantsJson);
|
||||
|
||||
if (universalConfig == null || dateTimeRanges == null || moneyManagementVariants == null ||
|
||||
tickerVariants == null)
|
||||
{
|
||||
_logger.LogError("Failed to deserialize variant configurations for bundle request {RequestId}",
|
||||
bundleRequest.RequestId);
|
||||
return new List<RunBacktestRequest>();
|
||||
}
|
||||
|
||||
// Get the first account for the user using AccountService
|
||||
var firstAccount = await ServiceScopeHelpers.WithScopedService<IAccountService, Account?>(
|
||||
_scopeFactory,
|
||||
async service =>
|
||||
{
|
||||
var accounts =
|
||||
await service.GetAccountsByUserAsync(bundleRequest.User, hideSecrets: true, getBalance: false);
|
||||
return accounts.FirstOrDefault();
|
||||
});
|
||||
|
||||
if (firstAccount == null)
|
||||
{
|
||||
_logger.LogError("No accounts found for user {UserId} in bundle request {RequestId}",
|
||||
bundleRequest.User.Id, bundleRequest.RequestId);
|
||||
return new List<RunBacktestRequest>();
|
||||
}
|
||||
|
||||
var backtestRequests = new List<RunBacktestRequest>();
|
||||
|
||||
foreach (var dateRange in dateTimeRanges)
|
||||
{
|
||||
foreach (var mmVariant in moneyManagementVariants)
|
||||
{
|
||||
foreach (var ticker in tickerVariants)
|
||||
{
|
||||
var config = new TradingBotConfigRequest
|
||||
{
|
||||
AccountName = firstAccount.Name,
|
||||
Ticker = ticker,
|
||||
Timeframe = universalConfig.Timeframe,
|
||||
IsForWatchingOnly = universalConfig.IsForWatchingOnly,
|
||||
BotTradingBalance = universalConfig.BotTradingBalance,
|
||||
Name =
|
||||
$"{universalConfig.BotName}_{ticker}_{dateRange.StartDate:yyyyMMdd}_{dateRange.EndDate:yyyyMMdd}",
|
||||
FlipPosition = universalConfig.FlipPosition,
|
||||
CooldownPeriod = universalConfig.CooldownPeriod,
|
||||
MaxLossStreak = universalConfig.MaxLossStreak,
|
||||
Scenario = universalConfig.Scenario,
|
||||
ScenarioName = universalConfig.ScenarioName,
|
||||
MoneyManagement = mmVariant.MoneyManagement,
|
||||
MaxPositionTimeHours = universalConfig.MaxPositionTimeHours,
|
||||
CloseEarlyWhenProfitable = universalConfig.CloseEarlyWhenProfitable,
|
||||
FlipOnlyWhenInProfit = universalConfig.FlipOnlyWhenInProfit,
|
||||
UseSynthApi = universalConfig.UseSynthApi,
|
||||
UseForPositionSizing = universalConfig.UseForPositionSizing,
|
||||
UseForSignalFiltering = universalConfig.UseForSignalFiltering,
|
||||
UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss
|
||||
};
|
||||
|
||||
var backtestRequest = new RunBacktestRequest
|
||||
{
|
||||
Config = config,
|
||||
StartDate = dateRange.StartDate,
|
||||
EndDate = dateRange.EndDate,
|
||||
Balance = universalConfig.BotTradingBalance,
|
||||
WatchOnly = universalConfig.WatchOnly,
|
||||
Save = universalConfig.Save,
|
||||
WithCandles = false, // Bundle backtests never return candles
|
||||
MoneyManagement = mmVariant.MoneyManagement != null
|
||||
? new MoneyManagement
|
||||
{
|
||||
Name = mmVariant.MoneyManagement.Name,
|
||||
Timeframe = mmVariant.MoneyManagement.Timeframe,
|
||||
StopLoss = mmVariant.MoneyManagement.StopLoss,
|
||||
TakeProfit = mmVariant.MoneyManagement.TakeProfit,
|
||||
Leverage = mmVariant.MoneyManagement.Leverage
|
||||
}
|
||||
: null
|
||||
};
|
||||
|
||||
backtestRequests.Add(backtestRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backtestRequests;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error generating backtest requests from variants for bundle request {RequestId}",
|
||||
bundleRequest.RequestId);
|
||||
return new List<RunBacktestRequest>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessSingleBacktest(
|
||||
IBacktester backtester,
|
||||
RunBacktestRequest runBacktestRequest,
|
||||
BundleBacktestRequest bundleRequest,
|
||||
int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Calculate total count from the variant configuration
|
||||
var totalCount = bundleRequest.TotalBacktests;
|
||||
|
||||
// Update current backtest being processed
|
||||
bundleRequest.CurrentBacktest = $"Backtest {index + 1} of {totalCount}";
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
bundleRequest.User.Accounts = await ServiceScopeHelpers.WithScopedService<IAccountService, List<Account>>(
|
||||
_scopeFactory,
|
||||
async service => { return (await service.GetAccountsByUserAsync(bundleRequest.User, true)).ToList(); });
|
||||
// Run the backtest directly with the strongly-typed request
|
||||
var backtestId = await RunSingleBacktest(backtester, runBacktestRequest, bundleRequest, index);
|
||||
if (!string.IsNullOrEmpty(backtestId))
|
||||
{
|
||||
bundleRequest.Results.Add(backtestId);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
bundleRequest.CompletedBacktests++;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
_logger.LogInformation("Completed backtest {Index} for bundle request {RequestId}",
|
||||
index + 1, bundleRequest.RequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing backtest {Index} for bundle request {RequestId}",
|
||||
index + 1, bundleRequest.RequestId);
|
||||
bundleRequest.FailedBacktests++;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
SentrySdk.CaptureException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> RunSingleBacktest(
|
||||
IBacktester backtester,
|
||||
RunBacktestRequest runBacktestRequest,
|
||||
BundleBacktestRequest bundleRequest,
|
||||
int index)
|
||||
{
|
||||
if (runBacktestRequest?.Config == null)
|
||||
{
|
||||
_logger.LogError("Invalid RunBacktestRequest in bundle (null config)");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Map MoneyManagement
|
||||
MoneyManagement moneyManagement = null;
|
||||
if (!string.IsNullOrEmpty(runBacktestRequest.Config.MoneyManagementName))
|
||||
{
|
||||
_logger.LogWarning("MoneyManagementName provided but cannot resolve in grain context: {Name}",
|
||||
runBacktestRequest.Config.MoneyManagementName);
|
||||
}
|
||||
else if (runBacktestRequest.Config.MoneyManagement != null)
|
||||
{
|
||||
var mmReq = runBacktestRequest.Config.MoneyManagement;
|
||||
moneyManagement = new MoneyManagement
|
||||
{
|
||||
Name = mmReq.Name,
|
||||
Timeframe = mmReq.Timeframe,
|
||||
StopLoss = mmReq.StopLoss,
|
||||
TakeProfit = mmReq.TakeProfit,
|
||||
Leverage = mmReq.Leverage
|
||||
};
|
||||
moneyManagement.FormatPercentage();
|
||||
}
|
||||
|
||||
// Map Scenario
|
||||
LightScenario scenario = null;
|
||||
if (runBacktestRequest.Config.Scenario != null)
|
||||
{
|
||||
var sReq = runBacktestRequest.Config.Scenario;
|
||||
scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod)
|
||||
{
|
||||
Indicators = sReq.Indicators?.Select(i => new LightIndicator(i.Name, i.Type)
|
||||
{
|
||||
MinimumHistory = i.MinimumHistory,
|
||||
Period = i.Period,
|
||||
FastPeriods = i.FastPeriods,
|
||||
SlowPeriods = i.SlowPeriods,
|
||||
SignalPeriods = i.SignalPeriods,
|
||||
Multiplier = i.Multiplier,
|
||||
SmoothPeriods = i.SmoothPeriods,
|
||||
StochPeriods = i.StochPeriods,
|
||||
CyclePeriods = i.CyclePeriods
|
||||
}).ToList() ?? new List<LightIndicator>()
|
||||
};
|
||||
}
|
||||
|
||||
// Map TradingBotConfig
|
||||
var backtestConfig = new TradingBotConfig
|
||||
{
|
||||
AccountName = runBacktestRequest.Config.AccountName,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = runBacktestRequest.Config.Ticker,
|
||||
ScenarioName = runBacktestRequest.Config.ScenarioName,
|
||||
Scenario = scenario,
|
||||
Timeframe = runBacktestRequest.Config.Timeframe,
|
||||
IsForWatchingOnly = runBacktestRequest.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = runBacktestRequest.Config.BotTradingBalance,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = runBacktestRequest.Config.CooldownPeriod ?? 1,
|
||||
MaxLossStreak = runBacktestRequest.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = runBacktestRequest.Config.MaxPositionTimeHours,
|
||||
FlipOnlyWhenInProfit = runBacktestRequest.Config.FlipOnlyWhenInProfit,
|
||||
FlipPosition = runBacktestRequest.Config.FlipPosition,
|
||||
Name = $"{bundleRequest.Name} #{index + 1}",
|
||||
CloseEarlyWhenProfitable = runBacktestRequest.Config.CloseEarlyWhenProfitable,
|
||||
UseSynthApi = runBacktestRequest.Config.UseSynthApi,
|
||||
UseForPositionSizing = runBacktestRequest.Config.UseForPositionSizing,
|
||||
UseForSignalFiltering = runBacktestRequest.Config.UseForSignalFiltering,
|
||||
UseForDynamicStopLoss = runBacktestRequest.Config.UseForDynamicStopLoss
|
||||
};
|
||||
|
||||
// Run the backtest
|
||||
var result = await backtester.RunTradingBotBacktest(
|
||||
backtestConfig,
|
||||
runBacktestRequest.StartDate,
|
||||
runBacktestRequest.EndDate,
|
||||
bundleRequest.User,
|
||||
true,
|
||||
runBacktestRequest.WithCandles,
|
||||
bundleRequest.RequestId.ToString()
|
||||
);
|
||||
|
||||
_logger.LogInformation("Processed backtest for bundle request {RequestId}", bundleRequest.RequestId);
|
||||
return result.Id;
|
||||
}
|
||||
|
||||
private async Task UpdateFinalStatus(
|
||||
BundleBacktestRequest bundleRequest,
|
||||
IBacktester backtester,
|
||||
IMessengerService messengerService)
|
||||
{
|
||||
if (bundleRequest.FailedBacktests == 0)
|
||||
{
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Completed;
|
||||
await NotifyUser(bundleRequest, messengerService);
|
||||
}
|
||||
else if (bundleRequest.CompletedBacktests == 0)
|
||||
{
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Failed;
|
||||
bundleRequest.ErrorMessage = "All backtests failed";
|
||||
}
|
||||
else
|
||||
{
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Completed;
|
||||
bundleRequest.ErrorMessage = $"{bundleRequest.FailedBacktests} backtests failed";
|
||||
await NotifyUser(bundleRequest, messengerService);
|
||||
}
|
||||
|
||||
bundleRequest.CompletedAt = DateTime.UtcNow;
|
||||
bundleRequest.CurrentBacktest = null;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
// Unregister retry reminder since bundle completed
|
||||
await UnregisterRetryReminder();
|
||||
}
|
||||
|
||||
private async Task HandleBundleRequestError(
|
||||
BundleBacktestRequest bundleRequest,
|
||||
IBacktester backtester,
|
||||
Exception ex)
|
||||
{
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Failed;
|
||||
bundleRequest.ErrorMessage = ex.Message;
|
||||
bundleRequest.CompletedAt = DateTime.UtcNow;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
// Register retry reminder for failed bundle
|
||||
await RegisterRetryReminder();
|
||||
}
|
||||
|
||||
private async Task NotifyUser(BundleBacktestRequest bundleRequest, IMessengerService messengerService)
|
||||
{
|
||||
if (bundleRequest.User?.TelegramChannel != null)
|
||||
{
|
||||
var message = bundleRequest.FailedBacktests == 0
|
||||
? $"✅ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed successfully."
|
||||
: $"⚠️ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed with {bundleRequest.FailedBacktests} failed backtests.";
|
||||
|
||||
await messengerService.SendMessage(message, bundleRequest.User.TelegramChannel);
|
||||
}
|
||||
}
|
||||
|
||||
#region IRemindable Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Handles reminder callbacks for automatic retry of failed bundle backtests
|
||||
/// </summary>
|
||||
public async Task ReceiveReminder(string reminderName, TickStatus status)
|
||||
{
|
||||
if (reminderName != RETRY_REMINDER_NAME)
|
||||
{
|
||||
_logger.LogWarning("Unknown reminder {ReminderName} received", reminderName);
|
||||
return;
|
||||
}
|
||||
|
||||
var bundleRequestId = this.GetPrimaryKey();
|
||||
_logger.LogInformation("Retry reminder triggered for bundle request {RequestId}", bundleRequestId);
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var backtester = scope.ServiceProvider.GetRequiredService<IBacktester>();
|
||||
|
||||
// Get the bundle request
|
||||
var bundleRequest = await GetBundleRequestById(backtester, bundleRequestId);
|
||||
if (bundleRequest == null)
|
||||
{
|
||||
_logger.LogWarning("Bundle request {RequestId} not found during retry", bundleRequestId);
|
||||
await UnregisterRetryReminder();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if bundle is still failed
|
||||
if (bundleRequest.Status != BundleBacktestRequestStatus.Failed)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Bundle request {RequestId} is no longer failed (status: {Status}), unregistering reminder",
|
||||
bundleRequestId, bundleRequest.Status);
|
||||
await UnregisterRetryReminder();
|
||||
return;
|
||||
}
|
||||
|
||||
// Retry the bundle processing
|
||||
_logger.LogInformation("Retrying failed bundle request {RequestId}", bundleRequestId);
|
||||
|
||||
// Reset status to pending for retry
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Pending;
|
||||
bundleRequest.ErrorMessage = null;
|
||||
bundleRequest.CurrentBacktest = null;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
// Process the bundle again
|
||||
await ProcessBundleRequestAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during bundle backtest retry for request {RequestId}", bundleRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a retry reminder for this bundle request
|
||||
/// </summary>
|
||||
private async Task RegisterRetryReminder()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.RegisterOrUpdateReminder(RETRY_REMINDER_NAME, RETRY_INTERVAL, RETRY_INTERVAL);
|
||||
_logger.LogInformation("Registered retry reminder for bundle request {RequestId}", this.GetPrimaryKey());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to register retry reminder for bundle request {RequestId}",
|
||||
this.GetPrimaryKey());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the retry reminder for this bundle request
|
||||
/// </summary>
|
||||
private async Task UnregisterRetryReminder()
|
||||
{
|
||||
try
|
||||
{
|
||||
var reminder = await this.GetReminder(RETRY_REMINDER_NAME);
|
||||
if (reminder != null)
|
||||
{
|
||||
await this.UnregisterReminder(reminder);
|
||||
_logger.LogInformation("Unregistered retry reminder for bundle request {RequestId}",
|
||||
this.GetPrimaryKey());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to unregister retry reminder for bundle request {RequestId}",
|
||||
this.GetPrimaryKey());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Orleans;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Backtests;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Orleans.Concurrency;
|
||||
|
||||
namespace Managing.Application.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// Stateless worker grain for processing genetic backtest requests.
|
||||
/// Uses the genetic request ID (string) as the primary key.
|
||||
/// Uses custom compute placement with random fallback.
|
||||
/// </summary>
|
||||
[StatelessWorker]
|
||||
[TradingPlacement] // Use custom compute placement with random fallback
|
||||
public class GeneticBacktestGrain : Grain, IGeneticBacktestGrain
|
||||
{
|
||||
private readonly ILogger<GeneticBacktestGrain> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public GeneticBacktestGrain(
|
||||
ILogger<GeneticBacktestGrain> logger,
|
||||
IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
public async Task ProcessGeneticRequestAsync()
|
||||
{
|
||||
var requestId = this.GetPrimaryKeyString();
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var geneticService = scope.ServiceProvider.GetRequiredService<IGeneticService>();
|
||||
|
||||
// Load the request by status lists and filter by ID (Pending first, then Failed for retries)
|
||||
var pending = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Pending);
|
||||
var failed = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Failed);
|
||||
var request = pending.Concat(failed).FirstOrDefault(r => r.RequestId == requestId);
|
||||
|
||||
if (request == null)
|
||||
{
|
||||
_logger.LogWarning("[GeneticBacktestGrain] Request {RequestId} not found among pending/failed.",
|
||||
requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark running
|
||||
request.Status = GeneticRequestStatus.Running;
|
||||
await geneticService.UpdateGeneticRequestAsync(request);
|
||||
|
||||
request.User.Accounts = await ServiceScopeHelpers.WithScopedService<IAccountService, List<Account>>(
|
||||
_scopeFactory,
|
||||
async accountService => (await accountService.GetAccountsByUserAsync(request.User)).ToList());
|
||||
|
||||
// Run GA
|
||||
var result = await geneticService.RunGeneticAlgorithm(request, CancellationToken.None);
|
||||
|
||||
// Update final state
|
||||
request.Status = GeneticRequestStatus.Completed;
|
||||
request.CompletedAt = DateTime.UtcNow;
|
||||
request.BestFitness = result.BestFitness;
|
||||
request.BestIndividual = result.BestIndividual;
|
||||
request.ProgressInfo = result.ProgressInfo;
|
||||
await geneticService.UpdateGeneticRequestAsync(request);
|
||||
|
||||
_logger.LogInformation("[GeneticBacktestGrain] Completed request {RequestId}", requestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SentrySdk.CaptureException(ex);
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var geneticService = scope.ServiceProvider.GetRequiredService<IGeneticService>();
|
||||
var running = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Running);
|
||||
var req = running.FirstOrDefault(r => r.RequestId == requestId) ?? new GeneticRequest(requestId);
|
||||
req.Status = GeneticRequestStatus.Failed;
|
||||
req.ErrorMessage = ex.Message;
|
||||
req.CompletedAt = DateTime.UtcNow;
|
||||
await geneticService.UpdateGeneticRequestAsync(req);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ namespace Managing.Application.ManageBot
|
||||
MasterBotUserId = masterBot.User.Id,
|
||||
|
||||
// Set computed/default properties
|
||||
IsForBacktest = false,
|
||||
TradingType = TradingType.Futures,
|
||||
Name = masterConfig.Name
|
||||
};
|
||||
|
||||
|
||||
@@ -61,16 +61,19 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
|
||||
try
|
||||
{
|
||||
var candlesHashSet = await GetCandlesAsync(tradingExchanges, config);
|
||||
if (candlesHashSet.LastOrDefault()!.Date <= candle.Date)
|
||||
// Convert to ordered List to preserve chronological order for indicators
|
||||
var candlesList = candlesHashSet.OrderBy(c => c.Date).ToList();
|
||||
|
||||
if (candlesList.LastOrDefault()!.Date <= candle.Date)
|
||||
{
|
||||
_logger.LogWarning($"No new candles for {config.Ticker} for {config.Name}");
|
||||
return null; // No new candles, no need to generate a signal
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Fetched {candlesHashSet.Count} candles for {config.Ticker} for {config.Name}");
|
||||
_logger.LogInformation($"Fetched {candlesList.Count} candles for {config.Ticker} for {config.Name}");
|
||||
|
||||
var signal = TradingBox.GetSignal(
|
||||
candlesHashSet,
|
||||
candlesList,
|
||||
config.Scenario,
|
||||
previousSignals,
|
||||
config.Scenario?.LoopbackPeriod ?? 1);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Managing.Domain.Trades;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.Trading.Commands
|
||||
{
|
||||
public class CloseBacktestFuturesPositionCommand : IRequest<Position>
|
||||
{
|
||||
public CloseBacktestFuturesPositionCommand(Position position, int accountId, decimal? executionPrice = null)
|
||||
{
|
||||
Position = position;
|
||||
AccountId = accountId;
|
||||
ExecutionPrice = executionPrice;
|
||||
}
|
||||
|
||||
public Position Position { get; }
|
||||
public int AccountId { get; }
|
||||
public decimal? ExecutionPrice { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Managing.Domain.Trades;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.Trading.Commands
|
||||
{
|
||||
public class CloseFuturesPositionCommand : IRequest<Position>
|
||||
{
|
||||
public CloseFuturesPositionCommand(Position position, int accountId, decimal? executionPrice = null)
|
||||
{
|
||||
Position = position;
|
||||
AccountId = accountId;
|
||||
ExecutionPrice = executionPrice;
|
||||
}
|
||||
|
||||
public Position Position { get; }
|
||||
public int AccountId { get; }
|
||||
public decimal? ExecutionPrice { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace Managing.Application.Trading.Commands
|
||||
bool isForPaperTrading = false,
|
||||
decimal? price = null,
|
||||
string signalIdentifier = null,
|
||||
Guid? initiatorIdentifier = null)
|
||||
Guid? initiatorIdentifier = null,
|
||||
TradingType tradingType = TradingType.Futures)
|
||||
{
|
||||
AccountName = accountName;
|
||||
MoneyManagement = moneyManagement;
|
||||
@@ -43,6 +44,7 @@ namespace Managing.Application.Trading.Commands
|
||||
InitiatorIdentifier = initiatorIdentifier ??
|
||||
throw new ArgumentNullException(nameof(initiatorIdentifier),
|
||||
"InitiatorIdentifier is required");
|
||||
TradingType = tradingType;
|
||||
}
|
||||
|
||||
public string SignalIdentifier { get; set; }
|
||||
@@ -57,5 +59,6 @@ namespace Managing.Application.Trading.Commands
|
||||
public PositionInitiator Initiator { get; }
|
||||
public User User { get; }
|
||||
public Guid InitiatorIdentifier { get; }
|
||||
public TradingType TradingType { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Trades;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Trading.Handlers;
|
||||
|
||||
public class CloseBacktestFuturesPositionCommandHandler(
|
||||
IExchangeService exchangeService,
|
||||
IAccountService accountService,
|
||||
ITradingService tradingService,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<CloseBacktestFuturesPositionCommandHandler> logger = null)
|
||||
: ICommandHandler<CloseBacktestFuturesPositionCommand, Position>
|
||||
{
|
||||
public async Task<Position> Handle(CloseBacktestFuturesPositionCommand request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// For backtest, use execution price directly
|
||||
var lastPrice = request.ExecutionPrice.GetValueOrDefault();
|
||||
|
||||
// Calculate closing direction (opposite of opening direction)
|
||||
var direction = request.Position.OriginDirection == TradeDirection.Long
|
||||
? TradeDirection.Short
|
||||
: TradeDirection.Long;
|
||||
|
||||
// Build the closing trade directly for backtest (no exchange call needed)
|
||||
var closedTrade = exchangeService.BuildEmptyTrade(
|
||||
request.Position.Open.Ticker,
|
||||
lastPrice,
|
||||
request.Position.Open.Quantity,
|
||||
direction,
|
||||
request.Position.Open.Leverage,
|
||||
TradeType.Market,
|
||||
request.Position.Open.Date,
|
||||
TradeStatus.Filled);
|
||||
|
||||
// Update position status and calculate PnL
|
||||
request.Position.Status = PositionStatus.Finished;
|
||||
request.Position.ProfitAndLoss =
|
||||
TradingBox.GetProfitAndLoss(request.Position, closedTrade.Quantity, lastPrice,
|
||||
request.Position.Open.Leverage);
|
||||
|
||||
// Add UI fees for closing the position
|
||||
var closingPositionSizeUsd = (lastPrice * closedTrade.Quantity) * request.Position.Open.Leverage;
|
||||
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||
request.Position.AddUiFees(closingUiFees);
|
||||
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||
|
||||
// For backtest, skip database update
|
||||
|
||||
return request.Position;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Error closing backtest futures position: {Message} \n Stacktrace : {StackTrace}", ex.Message,
|
||||
ex.StackTrace);
|
||||
|
||||
SentrySdk.CaptureException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Trades;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Trading.Handlers;
|
||||
|
||||
public class CloseFuturesPositionCommandHandler(
|
||||
IExchangeService exchangeService,
|
||||
IAccountService accountService,
|
||||
ITradingService tradingService,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<CloseFuturesPositionCommandHandler> logger = null)
|
||||
: ICommandHandler<CloseFuturesPositionCommand, Position>
|
||||
{
|
||||
public async Task<Position> Handle(CloseFuturesPositionCommand request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (request.Position == null)
|
||||
{
|
||||
logger?.LogWarning("Attempted to close position but position is null for account {AccountId}", request.AccountId);
|
||||
throw new ArgumentNullException(nameof(request.Position), "Position cannot be null for closing");
|
||||
}
|
||||
|
||||
// This handler should ONLY handle live trading positions
|
||||
// Backtest/paper trading positions must use CloseBacktestFuturesPositionCommandHandler
|
||||
if (request.Position.TradingType == TradingType.BacktestFutures ||
|
||||
request.Position.Initiator == PositionInitiator.PaperTrading)
|
||||
{
|
||||
logger?.LogError(
|
||||
"CloseFuturesPositionCommandHandler received a backtest/paper trading position. " +
|
||||
"Position: {PositionId}, TradingType: {TradingType}, Initiator: {Initiator}. " +
|
||||
"Use CloseBacktestFuturesPositionCommandHandler instead.",
|
||||
request.Position.Identifier, request.Position.TradingType, request.Position.Initiator);
|
||||
throw new InvalidOperationException(
|
||||
$"CloseFuturesPositionCommandHandler cannot handle backtest/paper trading positions. " +
|
||||
$"Position {request.Position.Identifier} has TradingType={request.Position.TradingType} and Initiator={request.Position.Initiator}. " +
|
||||
$"Use CloseBacktestFuturesPositionCommandHandler instead.");
|
||||
}
|
||||
|
||||
Account account = await accountService.GetAccountById(request.AccountId, false, false);
|
||||
|
||||
// For live trading, always get price from exchange
|
||||
var lastPrice = await exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow);
|
||||
|
||||
// Check if position still open on broker
|
||||
var p = (await exchangeService.GetBrokerPositions(account))
|
||||
.FirstOrDefault(x => x.Ticker == request.Position.Ticker);
|
||||
|
||||
// Position not available on the broker, so be sure to update the status
|
||||
if (p == null)
|
||||
{
|
||||
request.Position.Status = PositionStatus.Finished;
|
||||
request.Position.ProfitAndLoss =
|
||||
TradingBox.GetProfitAndLoss(request.Position, request.Position.Open.Quantity, lastPrice,
|
||||
request.Position.Open.Leverage);
|
||||
|
||||
// Add UI fees for closing the position (broker closed it)
|
||||
var closingPositionSizeUsd =
|
||||
(lastPrice * request.Position.Open.Quantity) * request.Position.Open.Leverage;
|
||||
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||
request.Position.AddUiFees(closingUiFees);
|
||||
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||
|
||||
await tradingService.UpdatePositionAsync(request.Position);
|
||||
return request.Position;
|
||||
}
|
||||
|
||||
var closeRequestedOrders = true; // TODO: For gmx no need to close orders since they are closed automatically
|
||||
|
||||
// Close market
|
||||
var closedPosition =
|
||||
await exchangeService.ClosePosition(account, request.Position, lastPrice);
|
||||
|
||||
if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled))
|
||||
{
|
||||
request.Position.Status = PositionStatus.Finished;
|
||||
request.Position.ProfitAndLoss =
|
||||
TradingBox.GetProfitAndLoss(request.Position, closedPosition.Quantity, lastPrice,
|
||||
request.Position.Open.Leverage);
|
||||
|
||||
// Add UI fees for closing the position
|
||||
var closingPositionSizeUsd = (lastPrice * closedPosition.Quantity) * request.Position.Open.Leverage;
|
||||
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||
request.Position.AddUiFees(closingUiFees);
|
||||
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||
|
||||
await tradingService.UpdatePositionAsync(request.Position);
|
||||
}
|
||||
|
||||
return request.Position;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Error closing futures position: {Message} \n Stacktrace : {StackTrace}", ex.Message,
|
||||
ex.StackTrace);
|
||||
|
||||
SentrySdk.CaptureException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ public class ClosePositionCommandHandler(
|
||||
}
|
||||
}
|
||||
|
||||
var isForPaperTrading = request.IsForBacktest;
|
||||
|
||||
var lastPrice = request.Position.Initiator == PositionInitiator.PaperTrading
|
||||
? request.ExecutionPrice.GetValueOrDefault()
|
||||
: await exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow);
|
||||
@@ -72,7 +70,7 @@ public class ClosePositionCommandHandler(
|
||||
|
||||
// Close market
|
||||
var closedPosition =
|
||||
await exchangeService.ClosePosition(account, request.Position, lastPrice, isForPaperTrading);
|
||||
await exchangeService.ClosePosition(account, request.Position, lastPrice);
|
||||
|
||||
if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled))
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Managing.Application.Trading.Handlers
|
||||
}
|
||||
|
||||
position.InitiatorIdentifier = request.InitiatorIdentifier;
|
||||
position.TradingType = request.TradingType;
|
||||
|
||||
// Always use BotTradingBalance directly as the balance to risk
|
||||
// Round to 2 decimal places to prevent precision errors
|
||||
|
||||
@@ -287,7 +287,7 @@ public class StatisticService : IStatisticService
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = true,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
|
||||
@@ -280,12 +280,15 @@ public class BacktestComputeWorker : BackgroundService
|
||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(_options.JobTimeoutMinutes));
|
||||
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
|
||||
// Convert HashSet to List - candles are already ordered from repository
|
||||
var candlesList = candles.ToList();
|
||||
|
||||
LightBacktest result;
|
||||
try
|
||||
{
|
||||
result = await executor.ExecuteAsync(
|
||||
config,
|
||||
candles,
|
||||
candlesList,
|
||||
user,
|
||||
save: true,
|
||||
withCandles: false,
|
||||
|
||||
@@ -253,7 +253,7 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
|
||||
Timeframe = runBacktestRequest.Config.Timeframe,
|
||||
IsForWatchingOnly = runBacktestRequest.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = runBacktestRequest.Config.BotTradingBalance,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = runBacktestRequest.Config.CooldownPeriod ?? 1,
|
||||
MaxLossStreak = runBacktestRequest.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = runBacktestRequest.Config.MaxPositionTimeHours,
|
||||
|
||||
@@ -396,6 +396,9 @@ public static class ApiBootstrap
|
||||
services.AddScoped<ISynthApiClient, SynthApiClient>();
|
||||
services.AddScoped<IPricesService, PricesService>();
|
||||
services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>();
|
||||
services.AddTransient<ICommandHandler<CloseBacktestFuturesPositionCommand, Position>, CloseBacktestFuturesPositionCommandHandler>();
|
||||
services.AddTransient<ICommandHandler<CloseFuturesPositionCommand, Position>, CloseFuturesPositionCommandHandler>();
|
||||
// Keep old handler for backward compatibility
|
||||
services.AddTransient<ICommandHandler<ClosePositionCommand, Position>, ClosePositionCommandHandler>();
|
||||
|
||||
// Processors
|
||||
|
||||
@@ -606,4 +606,20 @@ public static class Enums
|
||||
/// </summary>
|
||||
Genetic
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of trading mode for trading bots
|
||||
/// </summary>
|
||||
public enum TradingType
|
||||
{
|
||||
/// <summary>
|
||||
/// Live futures trading mode
|
||||
/// </summary>
|
||||
Futures,
|
||||
|
||||
/// <summary>
|
||||
/// Backtest futures trading mode
|
||||
/// </summary>
|
||||
BacktestFutures
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public class IndicatorTests
|
||||
public void CalculateIndicatorsValues_WithNullScenario_ReturnsEmptyDictionary()
|
||||
{
|
||||
// Arrange
|
||||
var candles = new HashSet<Candle> { CreateTestCandle() };
|
||||
var candles = new List<Candle> { CreateTestCandle() };
|
||||
|
||||
// Act
|
||||
var result = TradingBox.CalculateIndicatorsValues(null, candles);
|
||||
@@ -56,7 +56,7 @@ public class IndicatorTests
|
||||
{
|
||||
// Arrange
|
||||
var scenario = new Scenario(name: "TestScenario");
|
||||
var candles = new HashSet<Candle> { CreateTestCandle() };
|
||||
var candles = new List<Candle> { CreateTestCandle() };
|
||||
|
||||
// Act
|
||||
var result = TradingBox.CalculateIndicatorsValues(scenario, candles);
|
||||
@@ -70,7 +70,7 @@ public class IndicatorTests
|
||||
{
|
||||
// Arrange
|
||||
var scenario = new Scenario(name: "TestScenario") { Indicators = null };
|
||||
var candles = new HashSet<Candle> { CreateTestCandle() };
|
||||
var candles = new List<Candle> { CreateTestCandle() };
|
||||
|
||||
// Act
|
||||
var result = TradingBox.CalculateIndicatorsValues(scenario, candles);
|
||||
@@ -83,7 +83,7 @@ public class IndicatorTests
|
||||
public void CalculateIndicatorsValues_WithValidScenario_DoesNotThrow()
|
||||
{
|
||||
// Arrange - Create more realistic candle data
|
||||
var candles = new HashSet<Candle>();
|
||||
var candles = new List<Candle>();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var date = TestDate.AddMinutes(i * 5);
|
||||
@@ -120,7 +120,7 @@ public class IndicatorTests
|
||||
public void CalculateIndicatorsValues_WithMultipleIndicators_DoesNotThrow()
|
||||
{
|
||||
// Arrange - Create more realistic candle data
|
||||
var candles = new HashSet<Candle>();
|
||||
var candles = new List<Candle>();
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var date = TestDate.AddMinutes(i * 5);
|
||||
@@ -160,7 +160,7 @@ public class IndicatorTests
|
||||
public void CalculateIndicatorsValues_WithExceptionInIndicator_CatchesAndContinues()
|
||||
{
|
||||
// Arrange - Create realistic candle data
|
||||
var candles = new HashSet<Candle>();
|
||||
var candles = new List<Candle>();
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
candles.Add(CreateTestCandle(date: TestDate.AddMinutes(i)));
|
||||
@@ -370,7 +370,7 @@ public class IndicatorTests
|
||||
public void CalculateIndicatorsValues_HandlesLargeCandleSets()
|
||||
{
|
||||
// Arrange
|
||||
var candles = new HashSet<Candle>();
|
||||
var candles = new List<Candle>();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var date = TestDate.AddMinutes(i * 2);
|
||||
@@ -427,8 +427,8 @@ public class IndicatorTests
|
||||
public void CalculateIndicatorsValues_DoesNotModifyInputCandles()
|
||||
{
|
||||
// Arrange
|
||||
var originalCandles = new HashSet<Candle> { CreateTestCandle() };
|
||||
var candlesCopy = new HashSet<Candle>(originalCandles.Select(c => new Candle
|
||||
var originalCandles = new List<Candle> { CreateTestCandle() };
|
||||
var candlesCopy = new List<Candle>(originalCandles.Select(c => new Candle
|
||||
{
|
||||
Open = c.Open,
|
||||
High = c.High,
|
||||
|
||||
@@ -22,11 +22,8 @@ public class RunIndicatorsBase
|
||||
/// <returns>List of signals generated by the indicator</returns>
|
||||
protected List<LightSignal> RunIndicatorAndGetSignals(IndicatorBase indicator)
|
||||
{
|
||||
// Convert list to HashSet as expected by the Run method
|
||||
var candleSet = TestCandles.ToHashSet();
|
||||
|
||||
// Run the indicator
|
||||
var signals = indicator.Run(candleSet);
|
||||
// Use List directly - preserves chronological order required for indicators
|
||||
var signals = indicator.Run(TestCandles);
|
||||
|
||||
return signals ?? new List<LightSignal>();
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public class SignalProcessingTests : TradingBoxTests
|
||||
public void GetSignal_WithNullScenario_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var candles = new HashSet<Candle> { CreateTestCandle() };
|
||||
var candles = new List<Candle> { CreateTestCandle() };
|
||||
var signals = new Dictionary<string, LightSignal>();
|
||||
|
||||
// Act & Assert
|
||||
@@ -98,7 +98,7 @@ public class SignalProcessingTests : TradingBoxTests
|
||||
public void GetSignal_WithEmptyCandles_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var candles = new HashSet<Candle>();
|
||||
var candles = new List<Candle>();
|
||||
var scenario = CreateTestScenario(CreateTestIndicator());
|
||||
var signals = new Dictionary<string, LightSignal>();
|
||||
|
||||
@@ -113,7 +113,7 @@ public class SignalProcessingTests : TradingBoxTests
|
||||
public void GetSignal_WithScenarioHavingNoIndicators_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var candles = new HashSet<Candle> { CreateTestCandle() };
|
||||
var candles = new List<Candle> { CreateTestCandle() };
|
||||
var scenario = CreateTestScenario(); // Empty indicators
|
||||
var signals = new Dictionary<string, LightSignal>();
|
||||
|
||||
@@ -348,8 +348,8 @@ public class SignalProcessingTests : TradingBoxTests
|
||||
testCandles.Should().NotBeNull();
|
||||
testCandles.Should().NotBeEmpty();
|
||||
|
||||
// Use last 100 candles for the test
|
||||
var candles = testCandles.TakeLast(100).ToHashSet();
|
||||
// Use last 100 candles for the test (preserve order)
|
||||
var candles = testCandles.TakeLast(100).ToList();
|
||||
var scenario = CreateTestScenario(CreateTestIndicator(IndicatorType.Stc, "StcIndicator"));
|
||||
var signals = new Dictionary<string, LightSignal>();
|
||||
|
||||
@@ -373,8 +373,8 @@ public class SignalProcessingTests : TradingBoxTests
|
||||
testCandles.Should().NotBeNull();
|
||||
testCandles.Should().NotBeEmpty();
|
||||
|
||||
// Use last 500 candles for the test
|
||||
var candles = testCandles.TakeLast(500).ToHashSet();
|
||||
// Use last 500 candles for the test (preserve order)
|
||||
var candles = testCandles.TakeLast(500).ToList();
|
||||
var scenario = CreateTestScenario(CreateTestIndicator(IndicatorType.Stc, "StcIndicator"));
|
||||
var signals = new Dictionary<string, LightSignal>();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Managing.Domain.Bots;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Domain.Users;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Backtests;
|
||||
|
||||
@@ -65,7 +66,7 @@ public class Backtest
|
||||
Timeframe = Config.Timeframe,
|
||||
IsForWatchingOnly = false, // Always start as active bot
|
||||
BotTradingBalance = initialTradingBalance,
|
||||
IsForBacktest = false, // Always false for live bots
|
||||
TradingType = TradingType.Futures, // Always Futures for live bots
|
||||
CooldownPeriod = Config.CooldownPeriod,
|
||||
MaxLossStreak = Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = Config.MaxPositionTimeHours, // Properly copy nullable value
|
||||
@@ -99,7 +100,7 @@ public class Backtest
|
||||
Timeframe = Config.Timeframe,
|
||||
IsForWatchingOnly = Config.IsForWatchingOnly,
|
||||
BotTradingBalance = balance,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = Config.CooldownPeriod,
|
||||
MaxLossStreak = Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = Config.MaxPositionTimeHours,
|
||||
|
||||
@@ -21,7 +21,7 @@ public class TradingBotConfig
|
||||
|
||||
[Id(5)] [Required] public decimal BotTradingBalance { get; set; }
|
||||
|
||||
[Id(6)] [Required] public bool IsForBacktest { get; set; }
|
||||
[Id(6)] [Required] public TradingType TradingType { get; set; }
|
||||
|
||||
[Id(7)] [Required] public int CooldownPeriod { get; set; }
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public abstract class BollingerBandsBase : IndicatorBase
|
||||
StDev = stdev;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
@@ -45,7 +45,7 @@ public abstract class BollingerBandsBase : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
@@ -81,7 +81,7 @@ public abstract class BollingerBandsBase : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -93,7 +93,7 @@ public abstract class BollingerBandsBase : IndicatorBase
|
||||
/// <summary>
|
||||
/// Abstract method for processing Bollinger Bands signals - implemented by child classes
|
||||
/// </summary>
|
||||
protected abstract void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, HashSet<Candle> candles);
|
||||
protected abstract void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, IReadOnlyList<Candle> candles);
|
||||
|
||||
/// <summary>
|
||||
/// Maps Bollinger Bands results to candle objects with all BollingerBandsResult properties
|
||||
|
||||
@@ -20,7 +20,7 @@ public class BollingerBandsVolatilityProtection : BollingerBandsBase
|
||||
/// </summary>
|
||||
/// <param name="bbResults">List of Bollinger Bands calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
protected override void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, HashSet<Candle> candles)
|
||||
protected override void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var bbCandles = MapBollingerBandsToCandle(bbResults, candles.TakeLast(Period.Value)).ToList();
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public class StDevContext : IndicatorBase
|
||||
Period = period;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
@@ -44,7 +44,7 @@ public class StDevContext : IndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated StdDev values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
@@ -85,7 +85,7 @@ public class StDevContext : IndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="stDev">List of StdDev calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessStDevSignals(List<StdDevResult> stDev, HashSet<Candle> candles)
|
||||
private void ProcessStDevSignals(List<StdDevResult> stDev, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var stDevCandles = MapStDev(stDev, candles.TakeLast(Period.Value));
|
||||
|
||||
@@ -126,7 +126,7 @@ public class StDevContext : IndicatorBase
|
||||
AddSignal(lastCandle, TradeDirection.None, confidence);
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var test = new IndicatorsResultBase()
|
||||
{
|
||||
|
||||
@@ -21,17 +21,17 @@ namespace Managing.Domain.Strategies
|
||||
double? KFactor { get; set; }
|
||||
double? DFactor { get; set; }
|
||||
|
||||
List<LightSignal> Run(HashSet<Candle> candles);
|
||||
List<LightSignal> Run(IReadOnlyList<Candle> candles);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated indicator values for performance optimization.
|
||||
/// If pre-calculated values are not available or not applicable, falls back to regular Run().
|
||||
/// </summary>
|
||||
/// <param name="candles">The candles to process</param>
|
||||
/// <param name="candles">The candles to process (must be ordered chronologically)</param>
|
||||
/// <param name="preCalculatedValues">Pre-calculated indicator values (optional)</param>
|
||||
/// <returns>List of signals generated by the indicator</returns>
|
||||
List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues);
|
||||
List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues);
|
||||
|
||||
IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles);
|
||||
IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ namespace Managing.Domain.Strategies
|
||||
|
||||
public User User { get; set; }
|
||||
|
||||
public virtual List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public virtual List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -64,14 +64,16 @@ namespace Managing.Domain.Strategies
|
||||
/// Runs the indicator using pre-calculated values if available, otherwise falls back to regular Run().
|
||||
/// Default implementation falls back to regular Run() - override in derived classes to use pre-calculated values.
|
||||
/// </summary>
|
||||
public virtual List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
/// <param name="candles">The candles to process (must be ordered chronologically)</param>
|
||||
/// <param name="preCalculatedValues">Pre-calculated indicator values (optional)</param>
|
||||
public virtual List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
// Default implementation: ignore pre-calculated values and use regular Run()
|
||||
// Derived classes should override this to use pre-calculated values for performance
|
||||
return Run(candles);
|
||||
}
|
||||
|
||||
public virtual IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public virtual IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class BollingerBandsPercentBMomentumBreakout : BollingerBandsBase
|
||||
/// Long signals: %B crosses above 0.8 after being below (strong upward momentum)
|
||||
/// Short signals: %B crosses below 0.2 after being above (strong downward momentum)
|
||||
/// </summary>
|
||||
protected override void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, HashSet<Candle> candles)
|
||||
protected override void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var bbCandles = MapBollingerBandsToCandle(bbResults, candles.TakeLast(Period.Value)).ToList();
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
|
||||
MinimumHistory = 1 + Period.Value;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= MinimumHistory)
|
||||
{
|
||||
@@ -43,7 +43,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated Chandelier values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= MinimumHistory)
|
||||
{
|
||||
@@ -88,7 +88,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
|
||||
/// This method is shared between the regular Run() and optimized Run() methods.
|
||||
/// </summary>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessChandelierSignals(HashSet<Candle> candles)
|
||||
private void ProcessChandelierSignals(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
GetSignals(ChandelierType.Long, candles);
|
||||
GetSignals(ChandelierType.Short, candles);
|
||||
@@ -103,13 +103,13 @@ public class ChandelierExitIndicatorBase : IndicatorBase
|
||||
private void ProcessChandelierSignalsWithPreCalculated(
|
||||
List<ChandelierResult> chandelierLong,
|
||||
List<ChandelierResult> chandelierShort,
|
||||
HashSet<Candle> candles)
|
||||
IReadOnlyList<Candle> candles)
|
||||
{
|
||||
GetSignalsWithPreCalculated(ChandelierType.Long, chandelierLong, candles);
|
||||
GetSignalsWithPreCalculated(ChandelierType.Short, chandelierShort, candles);
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -118,7 +118,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
|
||||
};
|
||||
}
|
||||
|
||||
private void GetSignals(ChandelierType chandelierType, HashSet<Candle> candles)
|
||||
private void GetSignals(ChandelierType chandelierType, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var chandelier = candles.GetChandelier(Period.Value, Multiplier.Value, chandelierType)
|
||||
.Where(s => s.ChandelierExit.HasValue).ToList();
|
||||
@@ -126,7 +126,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
|
||||
}
|
||||
|
||||
private void GetSignalsWithPreCalculated(ChandelierType chandelierType, List<ChandelierResult> chandelier,
|
||||
HashSet<Candle> candles)
|
||||
IReadOnlyList<Candle> candles)
|
||||
{
|
||||
ProcessChandelierSignalsForType(chandelier, chandelierType, candles);
|
||||
}
|
||||
@@ -139,7 +139,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
|
||||
/// <param name="chandelierType">Type of Chandelier (Long or Short)</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessChandelierSignalsForType(List<ChandelierResult> chandelier, ChandelierType chandelierType,
|
||||
HashSet<Candle> candles)
|
||||
IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var chandelierCandle = MapChandelierToCandle(chandelier, candles.TakeLast(MinimumHistory));
|
||||
if (chandelierCandle.Count == 0)
|
||||
|
||||
@@ -21,7 +21,7 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
MinimumHistory = Math.Max(fastPeriod, slowPeriod) * 2;
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -30,7 +30,7 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
};
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= MinimumHistory)
|
||||
{
|
||||
@@ -58,7 +58,7 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated EMA values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= MinimumHistory)
|
||||
{
|
||||
@@ -105,7 +105,7 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
/// <param name="fastEma">List of Fast EMA calculation results</param>
|
||||
/// <param name="slowEma">List of Slow EMA calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessDualEmaCrossSignals(List<EmaResult> fastEma, List<EmaResult> slowEma, HashSet<Candle> candles)
|
||||
private void ProcessDualEmaCrossSignals(List<EmaResult> fastEma, List<EmaResult> slowEma, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var dualEmaCandles = MapDualEmaToCandle(fastEma, slowEma, candles.TakeLast(MinimumHistory));
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
|
||||
Period = period;
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -26,7 +26,7 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
|
||||
};
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
@@ -52,7 +52,7 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated EMA values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
@@ -93,7 +93,7 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="ema">List of EMA calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessEmaCrossSignals(List<EmaResult> ema, HashSet<Candle> candles)
|
||||
private void ProcessEmaCrossSignals(List<EmaResult> ema, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value));
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
Period = period;
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -26,7 +26,7 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
};
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
@@ -52,7 +52,7 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated EMA values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
@@ -93,7 +93,7 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="ema">List of EMA calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessEmaCrossSignals(List<EmaResult> ema, HashSet<Candle> candles)
|
||||
private void ProcessEmaCrossSignals(List<EmaResult> ema, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value).ToHashSet());
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ public class LaggingSTC : IndicatorBase
|
||||
CyclePeriods = cyclePeriods;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
|
||||
{
|
||||
@@ -54,7 +54,7 @@ public class LaggingSTC : IndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated STC values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
|
||||
{
|
||||
@@ -95,7 +95,7 @@ public class LaggingSTC : IndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="stc">List of STC calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessLaggingStcSignals(List<StcResult> stc, HashSet<Candle> candles)
|
||||
private void ProcessLaggingStcSignals(List<StcResult> stc, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var stcCandles = MapStcToCandle(stc, candles.TakeLast(CyclePeriods.Value * 3));
|
||||
|
||||
@@ -142,7 +142,7 @@ public class LaggingSTC : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
|
||||
return new IndicatorsResultBase
|
||||
|
||||
@@ -21,7 +21,7 @@ public class MacdCrossIndicatorBase : IndicatorBase
|
||||
SignalPeriods = signalPeriods;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= 2 * (SlowPeriods + SignalPeriods))
|
||||
{
|
||||
@@ -47,7 +47,7 @@ public class MacdCrossIndicatorBase : IndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated MACD values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= 2 * (SlowPeriods + SignalPeriods))
|
||||
{
|
||||
@@ -88,7 +88,7 @@ public class MacdCrossIndicatorBase : IndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="macd">List of MACD calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessMacdSignals(List<MacdResult> macd, HashSet<Candle> candles)
|
||||
private void ProcessMacdSignals(List<MacdResult> macd, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var macdCandle = MapMacdToCandle(macd, candles.TakeLast(SignalPeriods.Value));
|
||||
|
||||
@@ -114,7 +114,7 @@ public class MacdCrossIndicatorBase : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
|
||||
/// Get RSI signals
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
@@ -48,7 +48,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated RSI values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= Period)
|
||||
{
|
||||
@@ -90,7 +90,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="rsiResult">List of RSI calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessRsiDivergenceConfirmSignals(List<RsiResult> rsiResult, HashSet<Candle> candles)
|
||||
private void ProcessRsiDivergenceConfirmSignals(List<RsiResult> rsiResult, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
|
||||
|
||||
@@ -101,7 +101,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
|
||||
GetShortSignals(candlesRsi, candles);
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -109,7 +109,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
|
||||
};
|
||||
}
|
||||
|
||||
private void GetLongSignals(List<CandleRsi> candlesRsi, HashSet<Candle> candles)
|
||||
private void GetLongSignals(List<CandleRsi> candlesRsi, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
// Set the low and high for first candle
|
||||
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0);
|
||||
@@ -182,7 +182,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
private void GetShortSignals(List<CandleRsi> candlesRsi, HashSet<Candle> candles)
|
||||
private void GetShortSignals(List<CandleRsi> candlesRsi, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
// Set the low and high for first candle
|
||||
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0);
|
||||
@@ -256,7 +256,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckIfConfimation(CandleRsi currentCandle, TradeDirection direction, HashSet<Candle> candles)
|
||||
private void CheckIfConfimation(CandleRsi currentCandle, TradeDirection direction, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var lastCandleOnPeriod = candles.TakeLast(Period.Value).ToList();
|
||||
var signalsOnPeriod = Signals.Where(s => s.Date >= lastCandleOnPeriod[0].Date
|
||||
|
||||
@@ -25,7 +25,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
|
||||
/// Get RSI signals
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (!Period.HasValue || candles.Count <= Period)
|
||||
{
|
||||
@@ -51,7 +51,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated RSI values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (!Period.HasValue || candles.Count <= Period)
|
||||
{
|
||||
@@ -93,7 +93,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="rsiResult">List of RSI calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessRsiDivergenceSignals(List<RsiResult> rsiResult, HashSet<Candle> candles)
|
||||
private void ProcessRsiDivergenceSignals(List<RsiResult> rsiResult, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
|
||||
|
||||
@@ -104,7 +104,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
|
||||
GetShortSignals(candlesRsi, candles);
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -112,7 +112,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
|
||||
};
|
||||
}
|
||||
|
||||
private void GetLongSignals(List<CandleRsi> candlesRsi, HashSet<Candle> candles)
|
||||
private void GetLongSignals(List<CandleRsi> candlesRsi, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
// Set the low and high for first candle
|
||||
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0);
|
||||
@@ -183,7 +183,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
private void GetShortSignals(List<CandleRsi> candlesRsi, HashSet<Candle> candles)
|
||||
private void GetShortSignals(List<CandleRsi> candlesRsi, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
// Set the low and high for first candle
|
||||
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0);
|
||||
@@ -255,7 +255,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, HashSet<Candle> candles)
|
||||
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var signal = new LightSignal(candleSignal.Ticker, direction, Confidence.Low,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
|
||||
@@ -21,7 +21,7 @@ public class StcIndicatorBase : IndicatorBase
|
||||
CyclePeriods = cyclePeriods;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
|
||||
{
|
||||
@@ -50,7 +50,7 @@ public class StcIndicatorBase : IndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated STC values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
|
||||
{
|
||||
@@ -85,7 +85,7 @@ public class StcIndicatorBase : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (FastPeriods != null && SlowPeriods != null)
|
||||
{
|
||||
@@ -105,7 +105,7 @@ public class StcIndicatorBase : IndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="stc">List of STC calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessStcSignals(List<StcResult> stc, HashSet<Candle> candles)
|
||||
private void ProcessStcSignals(List<StcResult> stc, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (CyclePeriods == null)
|
||||
return;
|
||||
|
||||
@@ -28,7 +28,7 @@ public class StochasticCrossIndicator : IndicatorBase
|
||||
DFactor = dFactor;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= 10 * StochPeriods.Value + 50)
|
||||
{
|
||||
@@ -55,7 +55,7 @@ public class StochasticCrossIndicator : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= 10 * StochPeriods.Value + 50)
|
||||
{
|
||||
@@ -95,7 +95,7 @@ public class StochasticCrossIndicator : IndicatorBase
|
||||
/// Long signals: %K crosses above %D when both lines are below 20 (oversold)
|
||||
/// Short signals: %K crosses below %D when both lines are above 80 (overbought)
|
||||
/// </summary>
|
||||
private void ProcessStochasticSignals(List<StochResult> stochResults, HashSet<Candle> candles)
|
||||
private void ProcessStochasticSignals(List<StochResult> stochResults, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var stochCandles = MapStochToCandle(stochResults, candles.TakeLast(StochPeriods.Value));
|
||||
|
||||
@@ -132,7 +132,7 @@ public class StochasticCrossIndicator : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ public class SuperTrendCrossEma : IndicatorBase
|
||||
MinimumHistory = 100 + Period.Value;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
// Validate sufficient historical data for all indicators
|
||||
const int emaPeriod = 50;
|
||||
@@ -89,7 +89,7 @@ public class SuperTrendCrossEma : IndicatorBase
|
||||
/// Runs the indicator using pre-calculated SuperTrend values for performance optimization.
|
||||
/// Note: EMA50 and ADX are still calculated on-the-fly as they're not part of the standard indicator values.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
// Validate sufficient historical data for all indicators
|
||||
const int emaPeriod = 50;
|
||||
@@ -157,7 +157,7 @@ public class SuperTrendCrossEma : IndicatorBase
|
||||
List<SuperTrendResult> superTrend,
|
||||
List<EmaResult> ema50,
|
||||
List<AdxResult> adxResults,
|
||||
HashSet<Candle> candles,
|
||||
IReadOnlyList<Candle> candles,
|
||||
int minimumRequiredHistory,
|
||||
int adxThreshold)
|
||||
{
|
||||
@@ -237,7 +237,7 @@ public class SuperTrendCrossEma : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ public class SuperTrendIndicatorBase : IndicatorBase
|
||||
MinimumHistory = 100 + Period.Value;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= MinimumHistory)
|
||||
{
|
||||
@@ -48,7 +48,7 @@ public class SuperTrendIndicatorBase : IndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated SuperTrend values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= MinimumHistory)
|
||||
{
|
||||
@@ -89,7 +89,7 @@ public class SuperTrendIndicatorBase : IndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="superTrend">List of SuperTrend calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessSuperTrendSignals(List<SuperTrendResult> superTrend, HashSet<Candle> candles)
|
||||
private void ProcessSuperTrendSignals(List<SuperTrendResult> superTrend, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var superTrendCandle = MapSuperTrendToCandle(superTrend, candles.TakeLast(MinimumHistory));
|
||||
|
||||
@@ -112,7 +112,7 @@ public class SuperTrendIndicatorBase : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Managing.Domain.Strategies.Signals
|
||||
|
||||
public TradeDirection Direction { get; }
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var signals = new List<LightSignal>();
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Managing.Domain.Strategies.Signals
|
||||
/// Note: ThreeWhiteSoldiers is a pattern-based indicator that doesn't use traditional indicator calculations,
|
||||
/// so pre-calculated values are not applicable. This method falls back to regular Run().
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
// ThreeWhiteSoldiers doesn't use traditional indicators, so pre-calculated values don't apply
|
||||
// Fall back to regular calculation
|
||||
@@ -55,7 +55,7 @@ namespace Managing.Domain.Strategies.Signals
|
||||
/// This method is shared between the regular Run() and optimized Run() methods.
|
||||
/// </summary>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessThreeWhiteSoldiersSignals(HashSet<Candle> candles)
|
||||
private void ProcessThreeWhiteSoldiersSignals(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var lastFourCandles = candles.TakeLast(4);
|
||||
Candle previousCandles = null;
|
||||
@@ -75,7 +75,7 @@ namespace Managing.Domain.Strategies.Signals
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase
|
||||
Period = period;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= 2 * Period)
|
||||
{
|
||||
@@ -44,7 +44,7 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated EMA values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= 2 * Period)
|
||||
{
|
||||
@@ -85,7 +85,7 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="ema">List of EMA calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessEmaTrendSignals(List<EmaResult> ema, HashSet<Candle> candles)
|
||||
private void ProcessEmaTrendSignals(List<EmaResult> ema, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value));
|
||||
|
||||
@@ -108,7 +108,7 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ public class IchimokuKumoTrend : IndicatorBase
|
||||
ChikouOffset = chikouOffset; // Separate offset for Chikou span
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
// Need at least the greater of tenkanPeriods, kijunPeriods, senkouBPeriods, and all offset periods
|
||||
var maxOffset = Math.Max(Math.Max(OffsetPeriods.Value, SenkouOffset ?? OffsetPeriods.Value), ChikouOffset ?? OffsetPeriods.Value);
|
||||
@@ -56,7 +56,7 @@ public class IchimokuKumoTrend : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
// Need at least the greater of tenkanPeriods, kijunPeriods, senkouBPeriods, and all offset periods
|
||||
var maxOffset = Math.Max(Math.Max(OffsetPeriods.Value, SenkouOffset ?? OffsetPeriods.Value), ChikouOffset ?? OffsetPeriods.Value);
|
||||
@@ -160,7 +160,7 @@ public class IchimokuKumoTrend : IndicatorBase
|
||||
return candleIchimokuResults;
|
||||
}
|
||||
|
||||
private void ProcessKumoTrendSignals(List<CandleIchimoku> ichimokuResults, HashSet<Candle> candles)
|
||||
private void ProcessKumoTrendSignals(List<CandleIchimoku> ichimokuResults, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var mappedData = ichimokuResults;
|
||||
|
||||
@@ -193,7 +193,7 @@ public class IchimokuKumoTrend : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessKumoTrendSignalsFromResults(List<IchimokuResult> ichimokuResults, HashSet<Candle> candles)
|
||||
private void ProcessKumoTrendSignalsFromResults(List<IchimokuResult> ichimokuResults, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (ichimokuResults.Count == 0)
|
||||
return;
|
||||
@@ -229,7 +229,7 @@ public class IchimokuKumoTrend : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
IEnumerable<IchimokuResult> ichimokuResults;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
|
||||
Period = period;
|
||||
}
|
||||
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
if (candles.Count <= 10 * Period + 50)
|
||||
{
|
||||
@@ -55,7 +55,7 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
|
||||
/// <summary>
|
||||
/// Runs the indicator using pre-calculated StochRsi values for performance optimization.
|
||||
/// </summary>
|
||||
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
public override List<LightSignal> Run(IReadOnlyList<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
||||
{
|
||||
if (candles.Count <= 10 * Period + 50)
|
||||
{
|
||||
@@ -96,7 +96,7 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
|
||||
/// </summary>
|
||||
/// <param name="stochRsi">List of StochRsi calculation results</param>
|
||||
/// <param name="candles">Candles to process</param>
|
||||
private void ProcessStochRsiTrendSignals(List<StochRsiResult> stochRsi, HashSet<Candle> candles)
|
||||
private void ProcessStochRsiTrendSignals(List<StochRsiResult> stochRsi, IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var stochRsiCandles = MapStochRsiToCandle(stochRsi, candles.TakeLast(Period.Value));
|
||||
|
||||
@@ -119,7 +119,7 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
||||
public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
|
||||
@@ -55,13 +55,13 @@ public static class TradingBox
|
||||
{
|
||||
private static readonly IndicatorComboConfig _defaultConfig = new();
|
||||
|
||||
public static LightSignal GetSignal(HashSet<Candle> newCandles, LightScenario scenario,
|
||||
public static LightSignal GetSignal(IReadOnlyList<Candle> newCandles, LightScenario scenario,
|
||||
Dictionary<string, LightSignal> previousSignal, int? loopbackPeriod = 1)
|
||||
{
|
||||
return GetSignal(newCandles, scenario, previousSignal, _defaultConfig, loopbackPeriod, null);
|
||||
}
|
||||
|
||||
public static LightSignal GetSignal(HashSet<Candle> newCandles, LightScenario scenario,
|
||||
public static LightSignal GetSignal(IReadOnlyList<Candle> newCandles, LightScenario scenario,
|
||||
Dictionary<string, LightSignal> previousSignal, int? loopbackPeriod,
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
|
||||
{
|
||||
@@ -69,13 +69,13 @@ public static class TradingBox
|
||||
preCalculatedIndicatorValues);
|
||||
}
|
||||
|
||||
public static LightSignal GetSignal(HashSet<Candle> newCandles, LightScenario lightScenario,
|
||||
public static LightSignal GetSignal(IReadOnlyList<Candle> newCandles, LightScenario lightScenario,
|
||||
Dictionary<string, LightSignal> previousSignal, IndicatorComboConfig config, int? loopbackPeriod = 1)
|
||||
{
|
||||
return GetSignal(newCandles, lightScenario, previousSignal, config, loopbackPeriod, null);
|
||||
}
|
||||
|
||||
public static LightSignal GetSignal(HashSet<Candle> newCandles, LightScenario lightScenario,
|
||||
public static LightSignal GetSignal(IReadOnlyList<Candle> newCandles, LightScenario lightScenario,
|
||||
Dictionary<string, LightSignal> previousSignal, IndicatorComboConfig config, int? loopbackPeriod,
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
|
||||
{
|
||||
@@ -127,12 +127,11 @@ public static class TradingBox
|
||||
continue;
|
||||
}
|
||||
|
||||
var limitedCandles = newCandles.ToList();
|
||||
// Optimized: limitedCandles is already ordered, no need to re-order
|
||||
// newCandles is already a List and ordered chronologically
|
||||
var loopback = loopbackPeriod.HasValue && loopbackPeriod > 1 ? loopbackPeriod.Value : 1;
|
||||
var candleLoopback = limitedCandles.Count > loopback
|
||||
? limitedCandles.Skip(limitedCandles.Count - loopback).ToList()
|
||||
: limitedCandles;
|
||||
var candleLoopback = newCandles.Count > loopback
|
||||
? newCandles.Skip(newCandles.Count - loopback).ToList()
|
||||
: newCandles.ToList();
|
||||
|
||||
if (!candleLoopback.Any())
|
||||
{
|
||||
@@ -920,7 +919,7 @@ public static class TradingBox
|
||||
/// <returns>A dictionary of indicator types to their calculated values.</returns>
|
||||
public static Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValues(
|
||||
Scenario scenario,
|
||||
HashSet<Candle> candles)
|
||||
IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
|
||||
|
||||
@@ -84,6 +84,12 @@ namespace Managing.Domain.Trades
|
||||
[Id(18)]
|
||||
public bool RecoveryAttempted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The trading type for this position (BacktestFutures or Futures)
|
||||
/// </summary>
|
||||
[Id(19)]
|
||||
public TradingType TradingType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return true if position is finished even if the position was canceled or rejected
|
||||
/// </summary>
|
||||
|
||||
@@ -372,6 +372,8 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
b.HasIndex("Identifier")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("MasterBotUserId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
@@ -929,6 +931,9 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("TradingType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("UiFees")
|
||||
.HasColumnType("decimal(18,8)");
|
||||
|
||||
@@ -1571,12 +1576,19 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
|
||||
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b =>
|
||||
{
|
||||
b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "MasterBotUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("MasterBotUserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MasterBotUser");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
|
||||
@@ -57,4 +57,9 @@ public class PositionEntity
|
||||
[ForeignKey("TakeProfit2TradeId")] public virtual TradeEntity? TakeProfit2Trade { get; set; }
|
||||
|
||||
[Column(TypeName = "decimal(18,8)")] public decimal NetPnL { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The trading type for this position (BacktestFutures or Futures)
|
||||
/// </summary>
|
||||
public TradingType TradingType { get; set; }
|
||||
}
|
||||
@@ -331,6 +331,13 @@ public static class PostgreSqlMappers
|
||||
{
|
||||
if (backtest == null) return null;
|
||||
|
||||
// Configure JSON serializer to handle circular references
|
||||
var jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
PreserveReferencesHandling = PreserveReferencesHandling.None
|
||||
};
|
||||
|
||||
return new BacktestEntity
|
||||
{
|
||||
Identifier = backtest.Id,
|
||||
@@ -339,20 +346,20 @@ public static class PostgreSqlMappers
|
||||
WinRate = backtest.WinRate,
|
||||
GrowthPercentage = backtest.GrowthPercentage,
|
||||
HodlPercentage = backtest.HodlPercentage,
|
||||
ConfigJson = JsonConvert.SerializeObject(backtest.Config),
|
||||
ConfigJson = JsonConvert.SerializeObject(backtest.Config, jsonSettings),
|
||||
Name = backtest.Config?.Name ?? string.Empty,
|
||||
Ticker = backtest.Config?.Ticker.ToString() ?? string.Empty,
|
||||
Timeframe = (int)backtest.Config.Timeframe,
|
||||
IndicatorsCsv = string.Join(',', backtest.Config.Scenario.Indicators.Select(i => i.Type.ToString())),
|
||||
IndicatorsCount = backtest.Config.Scenario.Indicators.Count,
|
||||
PositionsJson = JsonConvert.SerializeObject(backtest.Positions.Values.ToList()),
|
||||
SignalsJson = JsonConvert.SerializeObject(backtest.Signals.Values.ToList()),
|
||||
PositionsJson = JsonConvert.SerializeObject(backtest.Positions.Values.ToList(), jsonSettings),
|
||||
SignalsJson = JsonConvert.SerializeObject(backtest.Signals.Values.ToList(), jsonSettings),
|
||||
StartDate = backtest.StartDate,
|
||||
EndDate = backtest.EndDate,
|
||||
Duration = backtest.EndDate - backtest.StartDate,
|
||||
MoneyManagementJson = JsonConvert.SerializeObject(backtest.Config?.MoneyManagement),
|
||||
MoneyManagementJson = JsonConvert.SerializeObject(backtest.Config?.MoneyManagement, jsonSettings),
|
||||
UserId = backtest.User?.Id ?? 0,
|
||||
StatisticsJson = backtest.Statistics != null ? JsonConvert.SerializeObject(backtest.Statistics) : null,
|
||||
StatisticsJson = backtest.Statistics != null ? JsonConvert.SerializeObject(backtest.Statistics, jsonSettings) : null,
|
||||
SharpeRatio = backtest.Statistics?.SharpeRatio ?? 0m,
|
||||
MaxDrawdown = backtest.Statistics?.MaxDrawdown ?? 0m,
|
||||
MaxDrawdownRecoveryTime = backtest.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero,
|
||||
@@ -615,7 +622,8 @@ public static class PostgreSqlMappers
|
||||
{
|
||||
Status = entity.Status,
|
||||
SignalIdentifier = entity.SignalIdentifier,
|
||||
InitiatorIdentifier = entity.InitiatorIdentifier
|
||||
InitiatorIdentifier = entity.InitiatorIdentifier,
|
||||
TradingType = entity.TradingType
|
||||
};
|
||||
|
||||
// Set ProfitAndLoss with proper type
|
||||
@@ -657,6 +665,7 @@ public static class PostgreSqlMappers
|
||||
SignalIdentifier = position.SignalIdentifier,
|
||||
UserId = position.User?.Id ?? 0,
|
||||
InitiatorIdentifier = position.InitiatorIdentifier,
|
||||
TradingType = position.TradingType,
|
||||
MoneyManagementJson = position.MoneyManagement != null
|
||||
? JsonConvert.SerializeObject(position.MoneyManagement)
|
||||
: null,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Managing.Infrastructure.Databases.src.Managing.Infrastructure.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTradingTypeToPositions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "TradingType",
|
||||
table: "Positions",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Bots_MasterBotUserId",
|
||||
table: "Bots",
|
||||
column: "MasterBotUserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Bots_Users_MasterBotUserId",
|
||||
table: "Bots",
|
||||
column: "MasterBotUserId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Bots_Users_MasterBotUserId",
|
||||
table: "Bots");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Bots_MasterBotUserId",
|
||||
table: "Bots");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TradingType",
|
||||
table: "Positions");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,26 +121,14 @@ namespace Managing.Infrastructure.Exchanges
|
||||
reduceOnly: true);
|
||||
}
|
||||
|
||||
public async Task<Trade> ClosePosition(Account account, Position position, decimal lastPrice,
|
||||
bool isForPaperTrading = false)
|
||||
public async Task<Trade> ClosePosition(Account account, Position position, decimal lastPrice)
|
||||
{
|
||||
var direction = position.OriginDirection == TradeDirection.Long
|
||||
? TradeDirection.Short
|
||||
: TradeDirection.Long;
|
||||
|
||||
if (isForPaperTrading)
|
||||
{
|
||||
var fake = BuildEmptyTrade(position.Open.Ticker,
|
||||
lastPrice,
|
||||
position.Open.Quantity,
|
||||
direction,
|
||||
position.Open.Leverage,
|
||||
TradeType.Market,
|
||||
position.Open.Date,
|
||||
TradeStatus.Filled);
|
||||
return fake;
|
||||
}
|
||||
|
||||
// Paper trading logic has been moved to CloseBacktestFuturesPositionCommandHandler
|
||||
// This method now only handles live trading
|
||||
var processor = GetProcessor(account);
|
||||
var closedTrade = await processor.OpenTrade(
|
||||
account,
|
||||
|
||||
@@ -306,10 +306,19 @@ namespace Managing.Infrastructure.Messengers.Discord
|
||||
await component.RespondAsync("Alright, let met few seconds to close this position");
|
||||
var position = await tradingService.GetPositionByIdentifierAsync(Guid.Parse(parameters[1]));
|
||||
|
||||
var command = new ClosePositionCommand(position, position.AccountId);
|
||||
var result =
|
||||
await new ClosePositionCommandHandler(exchangeService, accountService, tradingService, scopeFactory)
|
||||
Position result;
|
||||
if (position.TradingType == TradingType.BacktestFutures)
|
||||
{
|
||||
var command = new CloseBacktestFuturesPositionCommand(position, position.AccountId);
|
||||
result = await new CloseBacktestFuturesPositionCommandHandler(exchangeService, accountService, tradingService, scopeFactory)
|
||||
.Handle(command);
|
||||
}
|
||||
else
|
||||
{
|
||||
var command = new CloseFuturesPositionCommand(position, position.AccountId);
|
||||
result = await new CloseFuturesPositionCommandHandler(exchangeService, accountService, tradingService, scopeFactory)
|
||||
.Handle(command);
|
||||
}
|
||||
var fields = new List<EmbedFieldBuilder>()
|
||||
{
|
||||
new EmbedFieldBuilder
|
||||
|
||||
@@ -295,15 +295,15 @@ function UserInfoSettings() {
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered w-full"
|
||||
{...registerTelegram('telegramChannel', {
|
||||
{...registerTelegram('telegramChannel', {
|
||||
required: 'Telegram channel is required',
|
||||
pattern: {
|
||||
value: /^[0-9]{5,15}$/,
|
||||
message: 'Enter numeric channel ID (5-15 digits)'
|
||||
value: /^(?:-?[0-9]{5,15}|@[a-zA-Z0-9_]{5,32}|https?:\/\/(?:t\.me|telegram\.me)\/[a-zA-Z0-9_]{5,32}|https?:\/\/web\.telegram\.org\/k\/#-?[0-9]{5,15})$/,
|
||||
message: 'Enter channel ID, @username, or Telegram URL (t.me, telegram.me, or web.telegram.org)'
|
||||
}
|
||||
})}
|
||||
defaultValue={user?.telegramChannel || ''}
|
||||
placeholder="2828543022"
|
||||
placeholder="-3368031621 or @channelname or https://t.me/channelname"
|
||||
/>
|
||||
{telegramErrors.telegramChannel && (
|
||||
<label className="label">
|
||||
|
||||
@@ -123,7 +123,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
Timeframe = Timeframe.FifteenMinutes,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
@@ -136,7 +136,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
// Act
|
||||
var result = await _backtestExecutor.ExecuteAsync(
|
||||
config,
|
||||
candles.ToHashSet(),
|
||||
candles, // candles is already a List, no conversion needed
|
||||
_testUser,
|
||||
save: false,
|
||||
withCandles: false,
|
||||
@@ -172,16 +172,16 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<LightBacktest>(result);
|
||||
|
||||
// Validate key metrics - Updated after bug fix in executor
|
||||
// Validate key metrics - Updated after refactoring
|
||||
Assert.Equal(1000.0m, result.InitialBalance);
|
||||
Assert.Equal(45.30m, Math.Round(result.FinalPnl, 2));
|
||||
Assert.Equal(32, result.WinRate);
|
||||
Assert.Equal(-1.77m, Math.Round(result.GrowthPercentage, 2));
|
||||
Assert.Equal(8.79m, Math.Round(result.FinalPnl, 2));
|
||||
Assert.Equal(31, result.WinRate);
|
||||
Assert.Equal(-6.14m, Math.Round(result.GrowthPercentage, 2));
|
||||
Assert.Equal(-0.67m, Math.Round(result.HodlPercentage, 2));
|
||||
Assert.Equal(59.97m, Math.Round(result.Fees, 2));
|
||||
Assert.Equal(-17.74m, Math.Round(result.NetPnl, 2));
|
||||
Assert.Equal(158.79m, Math.Round((decimal)result.MaxDrawdown, 2));
|
||||
Assert.Equal(-0.004, Math.Round((double)(result.SharpeRatio ?? 0), 3));
|
||||
Assert.Equal(66.46m, Math.Round(result.Fees, 2));
|
||||
Assert.Equal(-61.36m, Math.Round(result.NetPnl, 2));
|
||||
Assert.Equal(202.29m, Math.Round((decimal)result.MaxDrawdown, 2));
|
||||
Assert.Equal(-0.015, Math.Round((double)(result.SharpeRatio ?? 0), 3));
|
||||
Assert.True(Math.Abs(result.Score - 0.0) < 0.001,
|
||||
$"Score {result.Score} should be within 0.001 of expected value 0.0");
|
||||
|
||||
@@ -218,7 +218,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
Timeframe = Timeframe.FifteenMinutes,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 100000, // Increased balance for testing more candles
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
@@ -231,7 +231,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
// Act
|
||||
var result = await _backtestExecutor.ExecuteAsync(
|
||||
config,
|
||||
candles.ToHashSet(),
|
||||
candles, // candles is already a List, no conversion needed
|
||||
_testUser,
|
||||
save: false,
|
||||
withCandles: false,
|
||||
@@ -264,16 +264,16 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<LightBacktest>(result);
|
||||
|
||||
// Validate key metrics - Updated after bug fix in executor
|
||||
// Validate key metrics - Updated after refactoring
|
||||
Assert.Equal(100000.0m, result.InitialBalance);
|
||||
Assert.Equal(-33978.09m, Math.Round(result.FinalPnl, 2));
|
||||
Assert.Equal(21, result.WinRate);
|
||||
Assert.Equal(-52.16m, Math.Round(result.GrowthPercentage, 2));
|
||||
Assert.Equal(-17671.68m, Math.Round(result.FinalPnl, 2));
|
||||
Assert.Equal(25, result.WinRate);
|
||||
Assert.Equal(-39.91m, Math.Round(result.GrowthPercentage, 2));
|
||||
Assert.Equal(-12.87m, Math.Round(result.HodlPercentage, 2));
|
||||
Assert.Equal(18207.71m, Math.Round(result.Fees, 2));
|
||||
Assert.Equal(-52156.26m, Math.Round(result.NetPnl, 2));
|
||||
Assert.Equal(54523.55m, Math.Round((decimal)result.MaxDrawdown, 2));
|
||||
Assert.Equal(-0.037, Math.Round((double)(result.SharpeRatio ?? 0), 3));
|
||||
Assert.Equal(22285.98m, Math.Round(result.Fees, 2));
|
||||
Assert.Equal(-39910.92m, Math.Round(result.NetPnl, 2));
|
||||
Assert.Equal(40416.59m, Math.Round((decimal)result.MaxDrawdown, 2));
|
||||
Assert.Equal(-0.023, Math.Round((double)(result.SharpeRatio ?? 0), 3));
|
||||
Assert.True(Math.Abs(result.Score - 0.0) < 0.001,
|
||||
$"Score {result.Score} should be within 0.001 of expected value 0.0");
|
||||
|
||||
@@ -308,7 +308,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
Timeframe = Timeframe.FifteenMinutes,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 100000,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
@@ -324,7 +324,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
// Act
|
||||
var result = await _backtestExecutor.ExecuteAsync(
|
||||
config,
|
||||
candles.ToHashSet(),
|
||||
candles, // candles is already a List, no conversion needed
|
||||
_testUser,
|
||||
save: false,
|
||||
withCandles: false,
|
||||
@@ -398,7 +398,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
Timeframe = Timeframe.FifteenMinutes,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 100000,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
@@ -414,7 +414,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
// Act
|
||||
var result = await _backtestExecutor.ExecuteAsync(
|
||||
config,
|
||||
candles.ToHashSet(),
|
||||
candles, // candles is already a List, no conversion needed
|
||||
_testUser,
|
||||
save: false,
|
||||
withCandles: false,
|
||||
@@ -451,12 +451,12 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
Console.WriteLine("Two-Scenarios Backtest Results:");
|
||||
Console.WriteLine(json);
|
||||
|
||||
// Business Logic Baseline Assertions - Updated after bug fix in executor
|
||||
// Business Logic Baseline Assertions - Updated after refactoring
|
||||
// These values establish the expected baseline for the two-scenarios test
|
||||
const decimal expectedFinalPnl = -35450.45m;
|
||||
const decimal expectedFinalPnl = -30567.20m;
|
||||
const double expectedScore = 0.0;
|
||||
const int expectedWinRatePercent = 20; // 20% win rate
|
||||
const decimal expectedGrowthPercentage = -49.76m;
|
||||
const decimal expectedGrowthPercentage = -45.32m;
|
||||
|
||||
// Allow small tolerance for floating-point precision variations
|
||||
const decimal pnlTolerance = 0.01m;
|
||||
|
||||
@@ -24,3 +24,7 @@ DateTime,TestName,CandlesCount,ExecutionTimeSeconds,ProcessingRateCandlesPerSec,
|
||||
2025-11-15T07:22:05Z,Telemetry_ETH_RSI_EMACROSS,5760,10.71,537.9,28.81,18.06,33.84,0.0,0,0.0,0.0,0.0,0.0,-35450.45,20,-49.76,0.00,49a693b4,dev,development
|
||||
2025-11-17T16:35:10Z,Telemetry_ETH_RSI_EMACROSS,5760,5.88,979.2,28.79,17.97,33.77,0.0,0,0.0,0.0,0.0,0.0,-35450.45,20,-49.76,0.00,091f617e,dev,development
|
||||
2025-11-17T16:49:22Z,Telemetry_ETH_RSI_EMACROSS,5760,4.61,1249.2,28.80,17.29,33.78,0.0,0,0.0,0.0,0.0,0.0,-35450.45,20,-49.76,0.00,091f617e,dev,development
|
||||
2025-12-01T10:46:58Z,Telemetry_ETH_RSI_EMACROSS,5760,2.76,2088.5,28.93,37.61,39.82,0.0,0,0.0,0.0,0.0,0.0,-30567.20,20,-45.32,0.00,93dc3e37,refactor-trading-bot,development
|
||||
2025-12-01T10:49:46Z,Telemetry_ETH_RSI_EMACROSS,5760,2.94,1962.1,28.94,37.41,39.55,0.0,0,0.0,0.0,0.0,0.0,-30567.20,20,-45.32,0.00,93dc3e37,refactor-trading-bot,development
|
||||
2025-12-01T10:50:15Z,Telemetry_ETH_RSI_EMACROSS,5760,2.98,1935.6,28.91,37.35,39.49,0.0,0,0.0,0.0,0.0,0.0,-30567.20,20,-45.32,0.00,93dc3e37,refactor-trading-bot,development
|
||||
2025-12-01T10:50:46Z,Telemetry_ETH_RSI_EMACROSS,5760,2.30,2508.3,28.92,37.35,39.50,0.0,0,0.0,0.0,0.0,0.0,-30567.20,20,-45.32,0.00,93dc3e37,refactor-trading-bot,development
|
||||
|
||||
|
@@ -69,3 +69,7 @@ DateTime,TestName,CandlesCount,ExecutionTimeSeconds,ProcessingRateCandlesPerSec,
|
||||
2025-11-15T07:22:05Z,Telemetry_ETH_RSI,5760,7.49,766.2,28.80,20.86,34.90,5992.19,0,0.0,916.71,0.00,0.16,-30689.97,24,-51.70,0.00,49a693b4,dev,development
|
||||
2025-11-17T16:35:10Z,Telemetry_ETH_RSI,5760,4.18,1373.1,29.03,20.63,36.17,3521.98,0,0.0,486.12,0.00,0.08,-30689.97,24,-51.70,0.00,091f617e,dev,development
|
||||
2025-11-17T16:49:22Z,Telemetry_ETH_RSI,5760,2.885,1990.6,29.02,20.35,35.08,2530.49,0,0.0,226.92,0.00,0.04,-30689.97,24,-51.70,0.00,091f617e,dev,development
|
||||
2025-12-01T10:46:58Z,Telemetry_ETH_RSI,5760,3.48,1653.0,28.92,24.25,41.11,3070.34,0,0.0,300.72,0.00,0.05,-9933.44,26,-36.30,0.00,93dc3e37,refactor-trading-bot,development
|
||||
2025-12-01T10:49:46Z,Telemetry_ETH_RSI,5760,1.585,3624.2,28.91,24.57,40.82,1458.82,0,0.0,81.56,0.00,0.01,-9933.44,26,-36.30,0.00,93dc3e37,refactor-trading-bot,development
|
||||
2025-12-01T10:50:15Z,Telemetry_ETH_RSI,5760,1.565,3670.7,28.90,24.34,41.31,1457.61,0,0.0,66.48,0.00,0.01,-9933.44,26,-36.30,0.00,93dc3e37,refactor-trading-bot,development
|
||||
2025-12-01T10:50:46Z,Telemetry_ETH_RSI,5760,1.67,3442.1,28.90,23.95,41.13,1548.30,0,0.0,78.60,0.00,0.01,-9933.44,26,-36.30,0.00,93dc3e37,refactor-trading-bot,development
|
||||
|
||||
|
Reference in New Issue
Block a user