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:
Oda
2025-12-01 19:32:06 +07:00
committed by GitHub
parent ab26260f6d
commit 9d536ea49e
74 changed files with 4525 additions and 2350 deletions

View File

@@ -568,7 +568,7 @@ public class BacktestController : BaseController
Timeframe = request.Config.Timeframe, Timeframe = request.Config.Timeframe,
IsForWatchingOnly = request.Config.IsForWatchingOnly, IsForWatchingOnly = request.Config.IsForWatchingOnly,
BotTradingBalance = request.Config.BotTradingBalance, BotTradingBalance = request.Config.BotTradingBalance,
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
CooldownPeriod = request.Config.CooldownPeriod ?? 1, CooldownPeriod = request.Config.CooldownPeriod ?? 1,
MaxLossStreak = request.Config.MaxLossStreak, MaxLossStreak = request.Config.MaxLossStreak,
MaxPositionTimeHours = request.Config.MaxPositionTimeHours, MaxPositionTimeHours = request.Config.MaxPositionTimeHours,

View File

@@ -814,7 +814,7 @@ public class BotController : BaseController
UseForSignalFiltering = request.Config.UseForSignalFiltering, UseForSignalFiltering = request.Config.UseForSignalFiltering,
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss, UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
// Set computed/default properties // Set computed/default properties
IsForBacktest = false, TradingType = TradingType.Futures,
FlipPosition = request.Config.FlipPosition, FlipPosition = request.Config.FlipPosition,
Name = request.Config.Name Name = request.Config.Name
}; };
@@ -976,7 +976,7 @@ public class BotController : BaseController
UseForSignalFiltering = request.Config.UseForSignalFiltering, UseForSignalFiltering = request.Config.UseForSignalFiltering,
UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss, UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss,
// Set computed/default properties // Set computed/default properties
IsForBacktest = false, TradingType = TradingType.Futures,
FlipPosition = request.Config.FlipPosition, FlipPosition = request.Config.FlipPosition,
Name = request.Config.Name Name = request.Config.Name
}; };

View File

@@ -350,7 +350,9 @@ public class DataController : ControllerBase
{ {
// Map ScenarioRequest to domain Scenario object // Map ScenarioRequest to domain Scenario object
var domainScenario = MapScenarioRequestToScenario(request.Scenario); 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 return Ok(new CandlesWithIndicatorsResponse

View File

@@ -25,7 +25,8 @@ namespace Managing.Api.Controllers;
public class TradingController : BaseController public class TradingController : BaseController
{ {
private readonly ICommandHandler<OpenPositionRequest, Position> _openTradeCommandHandler; 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 ITradingService _tradingService;
private readonly IMoneyManagementService _moneyManagementService; private readonly IMoneyManagementService _moneyManagementService;
private readonly IMediator _mediator; private readonly IMediator _mediator;
@@ -50,7 +51,8 @@ public class TradingController : BaseController
public TradingController( public TradingController(
ILogger<TradingController> logger, ILogger<TradingController> logger,
ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler, ICommandHandler<OpenPositionRequest, Position> openTradeCommandHandler,
ICommandHandler<ClosePositionCommand, Position> closeTradeCommandHandler, ICommandHandler<CloseBacktestFuturesPositionCommand, Position> closeBacktestFuturesCommandHandler,
ICommandHandler<CloseFuturesPositionCommand, Position> closeFuturesCommandHandler,
ITradingService tradingService, ITradingService tradingService,
IMediator mediator, IMoneyManagementService moneyManagementService, IMediator mediator, IMoneyManagementService moneyManagementService,
IUserService userService, IAdminConfigurationService adminService, IUserService userService, IAdminConfigurationService adminService,
@@ -60,7 +62,8 @@ public class TradingController : BaseController
{ {
_logger = logger; _logger = logger;
_openTradeCommandHandler = openTradeCommandHandler; _openTradeCommandHandler = openTradeCommandHandler;
_closeTradeCommandHandler = closeTradeCommandHandler; _closeBacktestFuturesCommandHandler = closeBacktestFuturesCommandHandler;
_closeFuturesCommandHandler = closeFuturesCommandHandler;
_tradingService = tradingService; _tradingService = tradingService;
_mediator = mediator; _mediator = mediator;
_moneyManagementService = moneyManagementService; _moneyManagementService = moneyManagementService;
@@ -98,20 +101,6 @@ public class TradingController : BaseController
return Ok(result); 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> /// <summary>
/// Opens a new position based on the provided parameters. /// Opens a new position based on the provided parameters.
/// </summary> /// </summary>

View File

@@ -23,5 +23,5 @@ public interface IBacktestTradingBotGrain : IGrainWithGuidKey
/// <param name="requestId">The request ID to associate with this backtest</param> /// <param name="requestId">The request ID to associate with this backtest</param>
/// <param name="metadata">Additional metadata to associate with this backtest</param> /// <param name="metadata">Additional metadata to associate with this backtest</param>
/// <returns>The complete backtest result</returns> /// <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);
} }

View File

@@ -41,7 +41,7 @@ public interface IExchangeService
decimal takeProfitPrice, decimal takeProfitPrice,
decimal quantity, bool isForPaperTrading = false, DateTime? currentDate = null); 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); decimal GetVolume(Account account, Ticker ticker);
Task<List<Trade>> GetTrades(Account account, Ticker ticker); Task<List<Trade>> GetTrades(Account account, Ticker ticker);
Task<bool> CancelOrder(Account account, Ticker ticker); Task<bool> CancelOrder(Account account, Ticker ticker);

View File

@@ -169,7 +169,7 @@ public class BacktestTests : BaseTests
Timeframe = Timeframe.FifteenMinutes, Timeframe = Timeframe.FifteenMinutes,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 1000, BotTradingBalance = 1000,
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,
FlipPosition = false, FlipPosition = false,
@@ -182,7 +182,7 @@ public class BacktestTests : BaseTests
// Act - Call BacktestTradingBotGrain directly (no Orleans needed) // Act - Call BacktestTradingBotGrain directly (no Orleans needed)
var backtestResult = await _backtestGrain.RunBacktestAsync( var backtestResult = await _backtestGrain.RunBacktestAsync(
config, config,
candles.ToHashSet(), candles, // candles is already a List, no conversion needed
_testUser, _testUser,
save: false, save: false,
withCandles: false); withCandles: false);
@@ -212,19 +212,19 @@ public class BacktestTests : BaseTests
Assert.NotNull(backtestResult); Assert.NotNull(backtestResult);
// Financial metrics - using decimal precision // Financial metrics - using decimal precision
Assert.Equal(-17.74m, Math.Round(backtestResult.FinalPnl, 2)); Assert.Equal(8.79m, Math.Round(backtestResult.FinalPnl, 2));
Assert.Equal(-77.71m, Math.Round(backtestResult.NetPnl, 2)); Assert.Equal(-61.36m, Math.Round(backtestResult.NetPnl, 2));
Assert.Equal(59.97m, Math.Round(backtestResult.Fees, 2)); Assert.Equal(66.46m, Math.Round(backtestResult.Fees, 2));
Assert.Equal(1000.0m, backtestResult.InitialBalance); Assert.Equal(1000.0m, backtestResult.InitialBalance);
// Performance metrics // Performance metrics
Assert.Equal(32, backtestResult.WinRate); Assert.Equal(31, backtestResult.WinRate);
Assert.Equal(-1.77m, Math.Round(backtestResult.GrowthPercentage, 2)); Assert.Equal(-6.14m, Math.Round(backtestResult.GrowthPercentage, 2));
Assert.Equal(-0.67m, Math.Round(backtestResult.HodlPercentage, 2)); Assert.Equal(-0.67m, Math.Round(backtestResult.HodlPercentage, 2));
// Risk metrics // Risk metrics
Assert.Equal(158.79m, Math.Round(backtestResult.MaxDrawdown.Value, 2)); Assert.Equal(202.29m, Math.Round(backtestResult.MaxDrawdown.Value, 2));
Assert.Equal(-0.004, Math.Round(backtestResult.SharpeRatio.Value, 3)); Assert.Equal(-0.015, Math.Round(backtestResult.SharpeRatio.Value, 3));
Assert.True(Math.Abs(backtestResult.Score - 0.0) < 0.001, Assert.True(Math.Abs(backtestResult.Score - 0.0) < 0.001,
$"Score {backtestResult.Score} should be within 0.001 of expected value 0.0"); $"Score {backtestResult.Score} should be within 0.001 of expected value 0.0");

View File

@@ -26,7 +26,7 @@ namespace Managing.Application.Tests
// Act // Act
foreach (var candle in _candles) 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) if (rsiStrategy.Signals != null && rsiStrategy.Signals.Count > 0)
@@ -48,7 +48,7 @@ namespace Managing.Application.Tests
// Act // Act
foreach (var candle in _candles) 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) if (macdStrategy.Signals != null && macdStrategy.Signals.Count > 0)
@@ -69,7 +69,7 @@ namespace Managing.Application.Tests
// Act // Act
foreach (var candle in _candles) 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) if (superTrendStrategy.Signals != null && superTrendStrategy.Signals.Count > 0)
@@ -90,7 +90,7 @@ namespace Managing.Application.Tests
// Act // Act
foreach (var candle in _candles) 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 }) if (chandelierExitStrategy.Signals is { Count: > 0 })
@@ -111,7 +111,7 @@ namespace Managing.Application.Tests
// Act // Act
foreach (var candle in _candles) 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) if (emaTrendStrategy.Signals != null && emaTrendStrategy.Signals.Count > 0)
@@ -133,7 +133,7 @@ namespace Managing.Application.Tests
// Act // Act
foreach (var candle in _candles) 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) if (stochRsiStrategy.Signals != null && stochRsiStrategy.Signals.Count > 0)

View File

@@ -50,14 +50,15 @@ public class PositionTests : BaseTests
PositionInitiator.User, PositionInitiator.User,
DateTime.UtcNow, new 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);
_ = _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 mockScope = new Mock<IServiceScopeFactory>();
var handler = new ClosePositionCommandHandler( var command = new CloseFuturesPositionCommand(position, 1);
var handler = new CloseFuturesPositionCommandHandler(
_exchangeService, _exchangeService,
_accountService.Object, _accountService.Object,
_tradingService.Object, _tradingService.Object,

View File

@@ -129,7 +129,7 @@ public class BacktestExecutor
/// <returns>The lightweight backtest result</returns> /// <returns>The lightweight backtest result</returns>
public async Task<LightBacktest> ExecuteAsync( public async Task<LightBacktest> ExecuteAsync(
TradingBotConfig config, TradingBotConfig config,
HashSet<Candle> candles, IReadOnlyList<Candle> candles,
User user, User user,
bool save = false, bool save = false,
bool withCandles = false, bool withCandles = false,
@@ -166,7 +166,9 @@ public class BacktestExecutor
// Create a fresh TradingBotBase instance for this backtest // Create a fresh TradingBotBase instance for this backtest
var tradingBot = CreateTradingBotInstance(config); 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 totalCandles = candles.Count;
var currentCandle = 0; var currentCandle = 0;
@@ -220,8 +222,9 @@ public class BacktestExecutor
// The signal calculation depends on rolling window state and cannot be pre-calculated effectively // 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 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 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; var candlesProcessed = 0;
// Signal caching optimization - reduce signal update frequency for better performance // Signal caching optimization - reduce signal update frequency for better performance
@@ -253,21 +256,20 @@ public class BacktestExecutor
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Maintain rolling window of last 600 candles to prevent exponential memory growth // Maintain rolling window of last 600 candles to prevent exponential memory growth
rollingWindowCandles.Enqueue(candle); // Incremental updates: remove oldest if at capacity, then add newest
if (rollingWindowCandles.Count > RollingWindowSize) // 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; tradingBot.LastCandle = candle;
// Run with optimized backtest path (minimize async calls) // Run with optimized backtest path (minimize async calls)
var signalUpdateStart = Stopwatch.GetTimestamp(); var signalUpdateStart = Stopwatch.GetTimestamp();
// Convert rolling window to HashSet for TradingBot.UpdateSignals compatibility // Pass List<Candle> directly - no conversion needed, order is preserved
// NOTE: Recreating HashSet each iteration is necessary to maintain correct enumeration order await tradingBot.UpdateSignals(rollingWindowCandles, preCalculatedIndicatorValues);
// Incremental updates break business logic (changes PnL results)
var fixedCandles = new HashSet<Candle>(rollingWindowCandles);
await tradingBot.UpdateSignals(fixedCandles, preCalculatedIndicatorValues);
signalUpdateTotalTime += Stopwatch.GetElapsedTime(signalUpdateStart); signalUpdateTotalTime += Stopwatch.GetElapsedTime(signalUpdateStart);
var backtestStepStart = Stopwatch.GetTimestamp(); var backtestStepStart = Stopwatch.GetTimestamp();
@@ -542,7 +544,7 @@ public class BacktestExecutor
throw new InvalidOperationException("Bot configuration is not initialized"); 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"); throw new InvalidOperationException("BacktestExecutor can only be used for backtesting");
} }
@@ -550,7 +552,7 @@ public class BacktestExecutor
// Create the trading bot instance // Create the trading bot instance
using var scope = _scopeFactory.CreateScope(); using var scope = _scopeFactory.CreateScope();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>(); var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, config); var tradingBot = new BacktestFuturesBot(logger, _scopeFactory, config);
return tradingBot; return tradingBot;
} }

View File

@@ -42,19 +42,22 @@ public class BacktestExecutorAdapter : IBacktester
object metadata = null) object metadata = null)
{ {
// Load candles using ExchangeService // Load candles using ExchangeService
var candles = await _exchangeService.GetCandlesInflux( var candlesHashSet = await _exchangeService.GetCandlesInflux(
TradingExchanges.Evm, TradingExchanges.Evm,
config.Ticker, config.Ticker,
startDate, startDate,
config.Timeframe, config.Timeframe,
endDate); endDate);
if (candles == null || candles.Count == 0) if (candlesHashSet == null || candlesHashSet.Count == 0)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"No candles found for {config.Ticker} on {config.Timeframe} from {startDate} to {endDate}"); $"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 // Execute using BacktestExecutor
var result = await _executor.ExecuteAsync( var result = await _executor.ExecuteAsync(
config, config,
@@ -73,12 +76,15 @@ public class BacktestExecutorAdapter : IBacktester
public async Task<LightBacktest> RunTradingBotBacktest( public async Task<LightBacktest> RunTradingBotBacktest(
TradingBotConfig config, TradingBotConfig config,
HashSet<Candle> candles, HashSet<Candle> candlesHashSet,
User user = null, User user = null,
bool withCandles = false, bool withCandles = false,
string requestId = null, string requestId = null,
object metadata = 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 // Execute using BacktestExecutor
var result = await _executor.ExecuteAsync( var result = await _executor.ExecuteAsync(
config, config,

View File

@@ -197,7 +197,7 @@ public class JobService
Timeframe = backtestRequest.Config.Timeframe, Timeframe = backtestRequest.Config.Timeframe,
IsForWatchingOnly = backtestRequest.Config.IsForWatchingOnly, IsForWatchingOnly = backtestRequest.Config.IsForWatchingOnly,
BotTradingBalance = backtestRequest.Config.BotTradingBalance, BotTradingBalance = backtestRequest.Config.BotTradingBalance,
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
CooldownPeriod = backtestRequest.Config.CooldownPeriod ?? 1, CooldownPeriod = backtestRequest.Config.CooldownPeriod ?? 1,
MaxLossStreak = backtestRequest.Config.MaxLossStreak, MaxLossStreak = backtestRequest.Config.MaxLossStreak,
MaxPositionTimeHours = backtestRequest.Config.MaxPositionTimeHours, MaxPositionTimeHours = backtestRequest.Config.MaxPositionTimeHours,

View File

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

View File

@@ -53,7 +53,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// <returns>The complete backtest result</returns> /// <returns>The complete backtest result</returns>
public async Task<LightBacktest> RunBacktestAsync( public async Task<LightBacktest> RunBacktestAsync(
TradingBotConfig config, TradingBotConfig config,
HashSet<Candle> candles, IReadOnlyList<Candle> candles,
User user = null, User user = null,
bool save = false, bool save = false,
bool withCandles = false, bool withCandles = false,
@@ -67,7 +67,9 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Create a fresh TradingBotBase instance for this backtest // Create a fresh TradingBotBase instance for this backtest
var tradingBot = await CreateTradingBotInstance(config); 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 totalCandles = candles.Count;
var currentCandle = 0; var currentCandle = 0;
@@ -81,7 +83,9 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance); tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
var initialBalance = 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; var lastYieldTime = DateTime.UtcNow;
const int yieldIntervalMs = 5000; // Yield control every 5 seconds to prevent timeout 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 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 // Process all candles following the exact pattern from GetBacktestingResult
foreach (var candle in candles) 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; tradingBot.LastCandle = candle;
// Update signals manually only for backtesting // Update signals manually only for backtesting with rolling window
await tradingBot.UpdateSignals(fixedCandles); // Pass List<Candle> directly - no conversion needed, order is preserved
await tradingBot.UpdateSignals(rollingWindowCandles);
await tradingBot.Run(); await tradingBot.Run();
currentCandle++; currentCandle++;
@@ -132,11 +144,12 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
_logger.LogInformation("Backtest processing completed. Calculating final results..."); _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 winRate = TradingBox.GetWinRate(tradingBot.Positions);
var stats = TradingBox.GetStatistics(tradingBot.WalletBalances); var stats = TradingBox.GetStatistics(tradingBot.WalletBalances);
var growthPercentage = 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 hodlPercentage = TradingBox.GetHodlPercentage(candles.First(), candles.Last());
var fees = TradingBox.GetTotalFees(tradingBot.Positions); var fees = TradingBox.GetTotalFees(tradingBot.Positions);
@@ -145,7 +158,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
growthPercentage: (double)growthPercentage, growthPercentage: (double)growthPercentage,
hodlPercentage: (double)hodlPercentage, hodlPercentage: (double)hodlPercentage,
winRate: winRate, winRate: winRate,
totalPnL: (double)finalPnl, totalPnL: (double)realizedPnl,
fees: (double)fees, fees: (double)fees,
tradeCount: tradingBot.Positions.Count, tradeCount: tradingBot.Positions.Count,
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime, maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
@@ -166,7 +179,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Create backtest result with conditional candles and indicators values // Create backtest result with conditional candles and indicators values
var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals) var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals)
{ {
FinalPnl = finalPnl, FinalPnl = realizedPnl, // Realized PnL before fees
WinRate = winRate, WinRate = winRate,
GrowthPercentage = growthPercentage, GrowthPercentage = growthPercentage,
HodlPercentage = hodlPercentage, HodlPercentage = hodlPercentage,
@@ -180,7 +193,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
StartDate = candles.FirstOrDefault()!.OpenTime, StartDate = candles.FirstOrDefault()!.OpenTime,
EndDate = candles.LastOrDefault()!.OpenTime, EndDate = candles.LastOrDefault()!.OpenTime,
InitialBalance = initialBalance, InitialBalance = initialBalance,
NetPnl = finalPnl - fees, NetPnl = netPnl, // Net PnL after fees
}; };
if (save && user != null) if (save && user != null)
@@ -233,14 +246,14 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
throw new InvalidOperationException("Bot configuration is not initialized"); 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"); throw new InvalidOperationException("BacktestTradingBotGrain can only be used for backtesting");
} }
// Create the trading bot instance // Create the trading bot instance
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>(); var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, config); var tradingBot = new BacktestFuturesBot(logger, _scopeFactory, config);
return tradingBot; return tradingBot;
} }
@@ -284,7 +297,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// Gets indicators values (following Backtester.cs pattern) /// Gets indicators values (following Backtester.cs pattern)
/// </summary> /// </summary>
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<LightIndicator> indicators, private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<LightIndicator> indicators,
HashSet<Candle> candles) IReadOnlyList<Candle> candles)
{ {
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>(); var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();

View File

@@ -72,7 +72,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
throw new InvalidOperationException("Bot configuration is not properly initialized"); 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"); throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
} }
@@ -531,7 +531,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
using var scope = _scopeFactory.CreateScope(); using var scope = _scopeFactory.CreateScope();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>(); var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var streamProvider = this.GetStreamProvider("ManagingStreamProvider"); 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 // Load state into the trading bot instance
LoadStateIntoTradingBot(tradingBot); LoadStateIntoTradingBot(tradingBot);

File diff suppressed because it is too large Load Diff

View File

@@ -955,7 +955,7 @@ public class TradingBotChromosome : ChromosomeBase
Ticker = request.Ticker, Ticker = request.Ticker,
Timeframe = request.Timeframe, Timeframe = request.Timeframe,
BotTradingBalance = request.Balance, BotTradingBalance = request.Balance,
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
IsForWatchingOnly = false, IsForWatchingOnly = false,
CooldownPeriod = Convert.ToInt32(genes[2].Value), CooldownPeriod = Convert.ToInt32(genes[2].Value),
MaxLossStreak = Convert.ToInt32(genes[3].Value), MaxLossStreak = Convert.ToInt32(genes[3].Value),
@@ -1104,7 +1104,7 @@ public class TradingBotFitness : IFitness
_serviceScopeFactory, _serviceScopeFactory,
async executor => await executor.ExecuteAsync( async executor => await executor.ExecuteAsync(
config, config,
_candles, _candles.OrderBy(c => c.Date).ToList(),
_request.User, _request.User,
save: true, save: true,
withCandles: false, withCandles: false,

View File

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

View File

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

View File

@@ -187,7 +187,7 @@ namespace Managing.Application.ManageBot
MasterBotUserId = masterBot.User.Id, MasterBotUserId = masterBot.User.Id,
// Set computed/default properties // Set computed/default properties
IsForBacktest = false, TradingType = TradingType.Futures,
Name = masterConfig.Name Name = masterConfig.Name
}; };

View File

@@ -61,16 +61,19 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
try try
{ {
var candlesHashSet = await GetCandlesAsync(tradingExchanges, config); 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}"); _logger.LogWarning($"No new candles for {config.Ticker} for {config.Name}");
return null; // No new candles, no need to generate a signal 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( var signal = TradingBox.GetSignal(
candlesHashSet, candlesList,
config.Scenario, config.Scenario,
previousSignals, previousSignals,
config.Scenario?.LoopbackPeriod ?? 1); config.Scenario?.LoopbackPeriod ?? 1);

View File

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

View File

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

View File

@@ -20,7 +20,8 @@ namespace Managing.Application.Trading.Commands
bool isForPaperTrading = false, bool isForPaperTrading = false,
decimal? price = null, decimal? price = null,
string signalIdentifier = null, string signalIdentifier = null,
Guid? initiatorIdentifier = null) Guid? initiatorIdentifier = null,
TradingType tradingType = TradingType.Futures)
{ {
AccountName = accountName; AccountName = accountName;
MoneyManagement = moneyManagement; MoneyManagement = moneyManagement;
@@ -43,6 +44,7 @@ namespace Managing.Application.Trading.Commands
InitiatorIdentifier = initiatorIdentifier ?? InitiatorIdentifier = initiatorIdentifier ??
throw new ArgumentNullException(nameof(initiatorIdentifier), throw new ArgumentNullException(nameof(initiatorIdentifier),
"InitiatorIdentifier is required"); "InitiatorIdentifier is required");
TradingType = tradingType;
} }
public string SignalIdentifier { get; set; } public string SignalIdentifier { get; set; }
@@ -57,5 +59,6 @@ namespace Managing.Application.Trading.Commands
public PositionInitiator Initiator { get; } public PositionInitiator Initiator { get; }
public User User { get; } public User User { get; }
public Guid InitiatorIdentifier { get; } public Guid InitiatorIdentifier { get; }
public TradingType TradingType { get; }
} }
} }

View File

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

View File

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

View File

@@ -34,8 +34,6 @@ public class ClosePositionCommandHandler(
} }
} }
var isForPaperTrading = request.IsForBacktest;
var lastPrice = request.Position.Initiator == PositionInitiator.PaperTrading var lastPrice = request.Position.Initiator == PositionInitiator.PaperTrading
? request.ExecutionPrice.GetValueOrDefault() ? request.ExecutionPrice.GetValueOrDefault()
: await exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow); : await exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow);
@@ -72,7 +70,7 @@ public class ClosePositionCommandHandler(
// Close market // Close market
var closedPosition = 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)) if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled))
{ {

View File

@@ -32,6 +32,7 @@ namespace Managing.Application.Trading.Handlers
} }
position.InitiatorIdentifier = request.InitiatorIdentifier; position.InitiatorIdentifier = request.InitiatorIdentifier;
position.TradingType = request.TradingType;
// Always use BotTradingBalance directly as the balance to risk // Always use BotTradingBalance directly as the balance to risk
// Round to 2 decimal places to prevent precision errors // Round to 2 decimal places to prevent precision errors

View File

@@ -287,7 +287,7 @@ public class StatisticService : IStatisticService
Timeframe = timeframe, Timeframe = timeframe,
IsForWatchingOnly = true, IsForWatchingOnly = true,
BotTradingBalance = 1000, BotTradingBalance = 1000,
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,
FlipPosition = false, FlipPosition = false,

View File

@@ -280,12 +280,15 @@ public class BacktestComputeWorker : BackgroundService
var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(_options.JobTimeoutMinutes)); var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(_options.JobTimeoutMinutes));
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
// Convert HashSet to List - candles are already ordered from repository
var candlesList = candles.ToList();
LightBacktest result; LightBacktest result;
try try
{ {
result = await executor.ExecuteAsync( result = await executor.ExecuteAsync(
config, config,
candles, candlesList,
user, user,
save: true, save: true,
withCandles: false, withCandles: false,

View File

@@ -253,7 +253,7 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
Timeframe = runBacktestRequest.Config.Timeframe, Timeframe = runBacktestRequest.Config.Timeframe,
IsForWatchingOnly = runBacktestRequest.Config.IsForWatchingOnly, IsForWatchingOnly = runBacktestRequest.Config.IsForWatchingOnly,
BotTradingBalance = runBacktestRequest.Config.BotTradingBalance, BotTradingBalance = runBacktestRequest.Config.BotTradingBalance,
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
CooldownPeriod = runBacktestRequest.Config.CooldownPeriod ?? 1, CooldownPeriod = runBacktestRequest.Config.CooldownPeriod ?? 1,
MaxLossStreak = runBacktestRequest.Config.MaxLossStreak, MaxLossStreak = runBacktestRequest.Config.MaxLossStreak,
MaxPositionTimeHours = runBacktestRequest.Config.MaxPositionTimeHours, MaxPositionTimeHours = runBacktestRequest.Config.MaxPositionTimeHours,

View File

@@ -396,6 +396,9 @@ public static class ApiBootstrap
services.AddScoped<ISynthApiClient, SynthApiClient>(); services.AddScoped<ISynthApiClient, SynthApiClient>();
services.AddScoped<IPricesService, PricesService>(); services.AddScoped<IPricesService, PricesService>();
services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>(); 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>(); services.AddTransient<ICommandHandler<ClosePositionCommand, Position>, ClosePositionCommandHandler>();
// Processors // Processors

View File

@@ -606,4 +606,20 @@ public static class Enums
/// </summary> /// </summary>
Genetic 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
}
} }

View File

@@ -42,7 +42,7 @@ public class IndicatorTests
public void CalculateIndicatorsValues_WithNullScenario_ReturnsEmptyDictionary() public void CalculateIndicatorsValues_WithNullScenario_ReturnsEmptyDictionary()
{ {
// Arrange // Arrange
var candles = new HashSet<Candle> { CreateTestCandle() }; var candles = new List<Candle> { CreateTestCandle() };
// Act // Act
var result = TradingBox.CalculateIndicatorsValues(null, candles); var result = TradingBox.CalculateIndicatorsValues(null, candles);
@@ -56,7 +56,7 @@ public class IndicatorTests
{ {
// Arrange // Arrange
var scenario = new Scenario(name: "TestScenario"); var scenario = new Scenario(name: "TestScenario");
var candles = new HashSet<Candle> { CreateTestCandle() }; var candles = new List<Candle> { CreateTestCandle() };
// Act // Act
var result = TradingBox.CalculateIndicatorsValues(scenario, candles); var result = TradingBox.CalculateIndicatorsValues(scenario, candles);
@@ -70,7 +70,7 @@ public class IndicatorTests
{ {
// Arrange // Arrange
var scenario = new Scenario(name: "TestScenario") { Indicators = null }; var scenario = new Scenario(name: "TestScenario") { Indicators = null };
var candles = new HashSet<Candle> { CreateTestCandle() }; var candles = new List<Candle> { CreateTestCandle() };
// Act // Act
var result = TradingBox.CalculateIndicatorsValues(scenario, candles); var result = TradingBox.CalculateIndicatorsValues(scenario, candles);
@@ -83,7 +83,7 @@ public class IndicatorTests
public void CalculateIndicatorsValues_WithValidScenario_DoesNotThrow() public void CalculateIndicatorsValues_WithValidScenario_DoesNotThrow()
{ {
// Arrange - Create more realistic candle data // Arrange - Create more realistic candle data
var candles = new HashSet<Candle>(); var candles = new List<Candle>();
for (int i = 0; i < 20; i++) for (int i = 0; i < 20; i++)
{ {
var date = TestDate.AddMinutes(i * 5); var date = TestDate.AddMinutes(i * 5);
@@ -120,7 +120,7 @@ public class IndicatorTests
public void CalculateIndicatorsValues_WithMultipleIndicators_DoesNotThrow() public void CalculateIndicatorsValues_WithMultipleIndicators_DoesNotThrow()
{ {
// Arrange - Create more realistic candle data // Arrange - Create more realistic candle data
var candles = new HashSet<Candle>(); var candles = new List<Candle>();
for (int i = 0; i < 30; i++) for (int i = 0; i < 30; i++)
{ {
var date = TestDate.AddMinutes(i * 5); var date = TestDate.AddMinutes(i * 5);
@@ -160,7 +160,7 @@ public class IndicatorTests
public void CalculateIndicatorsValues_WithExceptionInIndicator_CatchesAndContinues() public void CalculateIndicatorsValues_WithExceptionInIndicator_CatchesAndContinues()
{ {
// Arrange - Create realistic candle data // Arrange - Create realistic candle data
var candles = new HashSet<Candle>(); var candles = new List<Candle>();
for (int i = 0; i < 25; i++) for (int i = 0; i < 25; i++)
{ {
candles.Add(CreateTestCandle(date: TestDate.AddMinutes(i))); candles.Add(CreateTestCandle(date: TestDate.AddMinutes(i)));
@@ -370,7 +370,7 @@ public class IndicatorTests
public void CalculateIndicatorsValues_HandlesLargeCandleSets() public void CalculateIndicatorsValues_HandlesLargeCandleSets()
{ {
// Arrange // Arrange
var candles = new HashSet<Candle>(); var candles = new List<Candle>();
for (int i = 0; i < 100; i++) for (int i = 0; i < 100; i++)
{ {
var date = TestDate.AddMinutes(i * 2); var date = TestDate.AddMinutes(i * 2);
@@ -427,8 +427,8 @@ public class IndicatorTests
public void CalculateIndicatorsValues_DoesNotModifyInputCandles() public void CalculateIndicatorsValues_DoesNotModifyInputCandles()
{ {
// Arrange // Arrange
var originalCandles = new HashSet<Candle> { CreateTestCandle() }; var originalCandles = new List<Candle> { CreateTestCandle() };
var candlesCopy = new HashSet<Candle>(originalCandles.Select(c => new Candle var candlesCopy = new List<Candle>(originalCandles.Select(c => new Candle
{ {
Open = c.Open, Open = c.Open,
High = c.High, High = c.High,

View File

@@ -22,11 +22,8 @@ public class RunIndicatorsBase
/// <returns>List of signals generated by the indicator</returns> /// <returns>List of signals generated by the indicator</returns>
protected List<LightSignal> RunIndicatorAndGetSignals(IndicatorBase indicator) protected List<LightSignal> RunIndicatorAndGetSignals(IndicatorBase indicator)
{ {
// Convert list to HashSet as expected by the Run method // Use List directly - preserves chronological order required for indicators
var candleSet = TestCandles.ToHashSet(); var signals = indicator.Run(TestCandles);
// Run the indicator
var signals = indicator.Run(candleSet);
return signals ?? new List<LightSignal>(); return signals ?? new List<LightSignal>();
} }

View File

@@ -84,7 +84,7 @@ public class SignalProcessingTests : TradingBoxTests
public void GetSignal_WithNullScenario_ThrowsArgumentNullException() public void GetSignal_WithNullScenario_ThrowsArgumentNullException()
{ {
// Arrange // Arrange
var candles = new HashSet<Candle> { CreateTestCandle() }; var candles = new List<Candle> { CreateTestCandle() };
var signals = new Dictionary<string, LightSignal>(); var signals = new Dictionary<string, LightSignal>();
// Act & Assert // Act & Assert
@@ -98,7 +98,7 @@ public class SignalProcessingTests : TradingBoxTests
public void GetSignal_WithEmptyCandles_ReturnsNull() public void GetSignal_WithEmptyCandles_ReturnsNull()
{ {
// Arrange // Arrange
var candles = new HashSet<Candle>(); var candles = new List<Candle>();
var scenario = CreateTestScenario(CreateTestIndicator()); var scenario = CreateTestScenario(CreateTestIndicator());
var signals = new Dictionary<string, LightSignal>(); var signals = new Dictionary<string, LightSignal>();
@@ -113,7 +113,7 @@ public class SignalProcessingTests : TradingBoxTests
public void GetSignal_WithScenarioHavingNoIndicators_ReturnsNull() public void GetSignal_WithScenarioHavingNoIndicators_ReturnsNull()
{ {
// Arrange // Arrange
var candles = new HashSet<Candle> { CreateTestCandle() }; var candles = new List<Candle> { CreateTestCandle() };
var scenario = CreateTestScenario(); // Empty indicators var scenario = CreateTestScenario(); // Empty indicators
var signals = new Dictionary<string, LightSignal>(); var signals = new Dictionary<string, LightSignal>();
@@ -348,8 +348,8 @@ public class SignalProcessingTests : TradingBoxTests
testCandles.Should().NotBeNull(); testCandles.Should().NotBeNull();
testCandles.Should().NotBeEmpty(); testCandles.Should().NotBeEmpty();
// Use last 100 candles for the test // Use last 100 candles for the test (preserve order)
var candles = testCandles.TakeLast(100).ToHashSet(); var candles = testCandles.TakeLast(100).ToList();
var scenario = CreateTestScenario(CreateTestIndicator(IndicatorType.Stc, "StcIndicator")); var scenario = CreateTestScenario(CreateTestIndicator(IndicatorType.Stc, "StcIndicator"));
var signals = new Dictionary<string, LightSignal>(); var signals = new Dictionary<string, LightSignal>();
@@ -373,8 +373,8 @@ public class SignalProcessingTests : TradingBoxTests
testCandles.Should().NotBeNull(); testCandles.Should().NotBeNull();
testCandles.Should().NotBeEmpty(); testCandles.Should().NotBeEmpty();
// Use last 500 candles for the test // Use last 500 candles for the test (preserve order)
var candles = testCandles.TakeLast(500).ToHashSet(); var candles = testCandles.TakeLast(500).ToList();
var scenario = CreateTestScenario(CreateTestIndicator(IndicatorType.Stc, "StcIndicator")); var scenario = CreateTestScenario(CreateTestIndicator(IndicatorType.Stc, "StcIndicator"));
var signals = new Dictionary<string, LightSignal>(); var signals = new Dictionary<string, LightSignal>();

View File

@@ -5,6 +5,7 @@ using Managing.Domain.Bots;
using Managing.Domain.Indicators; using Managing.Domain.Indicators;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Domain.Users; using Managing.Domain.Users;
using static Managing.Common.Enums;
namespace Managing.Domain.Backtests; namespace Managing.Domain.Backtests;
@@ -65,7 +66,7 @@ public class Backtest
Timeframe = Config.Timeframe, Timeframe = Config.Timeframe,
IsForWatchingOnly = false, // Always start as active bot IsForWatchingOnly = false, // Always start as active bot
BotTradingBalance = initialTradingBalance, BotTradingBalance = initialTradingBalance,
IsForBacktest = false, // Always false for live bots TradingType = TradingType.Futures, // Always Futures for live bots
CooldownPeriod = Config.CooldownPeriod, CooldownPeriod = Config.CooldownPeriod,
MaxLossStreak = Config.MaxLossStreak, MaxLossStreak = Config.MaxLossStreak,
MaxPositionTimeHours = Config.MaxPositionTimeHours, // Properly copy nullable value MaxPositionTimeHours = Config.MaxPositionTimeHours, // Properly copy nullable value
@@ -99,7 +100,7 @@ public class Backtest
Timeframe = Config.Timeframe, Timeframe = Config.Timeframe,
IsForWatchingOnly = Config.IsForWatchingOnly, IsForWatchingOnly = Config.IsForWatchingOnly,
BotTradingBalance = balance, BotTradingBalance = balance,
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
CooldownPeriod = Config.CooldownPeriod, CooldownPeriod = Config.CooldownPeriod,
MaxLossStreak = Config.MaxLossStreak, MaxLossStreak = Config.MaxLossStreak,
MaxPositionTimeHours = Config.MaxPositionTimeHours, MaxPositionTimeHours = Config.MaxPositionTimeHours,

View File

@@ -21,7 +21,7 @@ public class TradingBotConfig
[Id(5)] [Required] public decimal BotTradingBalance { get; set; } [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; } [Id(7)] [Required] public int CooldownPeriod { get; set; }

View File

@@ -18,7 +18,7 @@ public abstract class BollingerBandsBase : IndicatorBase
StDev = stdev; StDev = stdev;
} }
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (candles.Count <= Period) 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) 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() return new IndicatorsResultBase()
{ {
@@ -93,7 +93,7 @@ public abstract class BollingerBandsBase : IndicatorBase
/// <summary> /// <summary>
/// Abstract method for processing Bollinger Bands signals - implemented by child classes /// Abstract method for processing Bollinger Bands signals - implemented by child classes
/// </summary> /// </summary>
protected abstract void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, HashSet<Candle> candles); protected abstract void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, IReadOnlyList<Candle> candles);
/// <summary> /// <summary>
/// Maps Bollinger Bands results to candle objects with all BollingerBandsResult properties /// Maps Bollinger Bands results to candle objects with all BollingerBandsResult properties

View File

@@ -20,7 +20,7 @@ public class BollingerBandsVolatilityProtection : BollingerBandsBase
/// </summary> /// </summary>
/// <param name="bbResults">List of Bollinger Bands calculation results</param> /// <param name="bbResults">List of Bollinger Bands calculation results</param>
/// <param name="candles">Candles to process</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(); var bbCandles = MapBollingerBandsToCandle(bbResults, candles.TakeLast(Period.Value)).ToList();

View File

@@ -18,7 +18,7 @@ public class StDevContext : IndicatorBase
Period = period; Period = period;
} }
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (candles.Count <= Period) if (candles.Count <= Period)
{ {
@@ -44,7 +44,7 @@ public class StDevContext : IndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated StdDev values for performance optimization. /// Runs the indicator using pre-calculated StdDev values for performance optimization.
/// </summary> /// </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) if (candles.Count <= Period)
{ {
@@ -85,7 +85,7 @@ public class StDevContext : IndicatorBase
/// </summary> /// </summary>
/// <param name="stDev">List of StdDev calculation results</param> /// <param name="stDev">List of StdDev calculation results</param>
/// <param name="candles">Candles to process</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)); var stDevCandles = MapStDev(stDev, candles.TakeLast(Period.Value));
@@ -126,7 +126,7 @@ public class StDevContext : IndicatorBase
AddSignal(lastCandle, TradeDirection.None, confidence); AddSignal(lastCandle, TradeDirection.None, confidence);
} }
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles) public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
{ {
var test = new IndicatorsResultBase() var test = new IndicatorsResultBase()
{ {

View File

@@ -21,17 +21,17 @@ namespace Managing.Domain.Strategies
double? KFactor { get; set; } double? KFactor { get; set; }
double? DFactor { get; set; } double? DFactor { get; set; }
List<LightSignal> Run(HashSet<Candle> candles); List<LightSignal> Run(IReadOnlyList<Candle> candles);
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated indicator values for performance optimization. /// 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(). /// If pre-calculated values are not available or not applicable, falls back to regular Run().
/// </summary> /// </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> /// <param name="preCalculatedValues">Pre-calculated indicator values (optional)</param>
/// <returns>List of signals generated by the indicator</returns> /// <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);
} }
} }

View File

@@ -55,7 +55,7 @@ namespace Managing.Domain.Strategies
public User User { get; set; } public User User { get; set; }
public virtual List<LightSignal> Run(HashSet<Candle> candles) public virtual List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
throw new NotImplementedException(); 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(). /// 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. /// Default implementation falls back to regular Run() - override in derived classes to use pre-calculated values.
/// </summary> /// </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() // Default implementation: ignore pre-calculated values and use regular Run()
// Derived classes should override this to use pre-calculated values for performance // Derived classes should override this to use pre-calculated values for performance
return Run(candles); return Run(candles);
} }
public virtual IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles) public virtual IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@@ -19,7 +19,7 @@ public class BollingerBandsPercentBMomentumBreakout : BollingerBandsBase
/// Long signals: %B crosses above 0.8 after being below (strong upward momentum) /// 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) /// Short signals: %B crosses below 0.2 after being above (strong downward momentum)
/// </summary> /// </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(); var bbCandles = MapBollingerBandsToCandle(bbResults, candles.TakeLast(Period.Value)).ToList();

View File

@@ -21,7 +21,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
MinimumHistory = 1 + Period.Value; MinimumHistory = 1 + Period.Value;
} }
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (candles.Count <= MinimumHistory) if (candles.Count <= MinimumHistory)
{ {
@@ -43,7 +43,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated Chandelier values for performance optimization. /// Runs the indicator using pre-calculated Chandelier values for performance optimization.
/// </summary> /// </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) if (candles.Count <= MinimumHistory)
{ {
@@ -88,7 +88,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
/// This method is shared between the regular Run() and optimized Run() methods. /// This method is shared between the regular Run() and optimized Run() methods.
/// </summary> /// </summary>
/// <param name="candles">Candles to process</param> /// <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.Long, candles);
GetSignals(ChandelierType.Short, candles); GetSignals(ChandelierType.Short, candles);
@@ -103,13 +103,13 @@ public class ChandelierExitIndicatorBase : IndicatorBase
private void ProcessChandelierSignalsWithPreCalculated( private void ProcessChandelierSignalsWithPreCalculated(
List<ChandelierResult> chandelierLong, List<ChandelierResult> chandelierLong,
List<ChandelierResult> chandelierShort, List<ChandelierResult> chandelierShort,
HashSet<Candle> candles) IReadOnlyList<Candle> candles)
{ {
GetSignalsWithPreCalculated(ChandelierType.Long, chandelierLong, candles); GetSignalsWithPreCalculated(ChandelierType.Long, chandelierLong, candles);
GetSignalsWithPreCalculated(ChandelierType.Short, chandelierShort, candles); GetSignalsWithPreCalculated(ChandelierType.Short, chandelierShort, candles);
} }
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles) public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
{ {
return new IndicatorsResultBase() 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) var chandelier = candles.GetChandelier(Period.Value, Multiplier.Value, chandelierType)
.Where(s => s.ChandelierExit.HasValue).ToList(); .Where(s => s.ChandelierExit.HasValue).ToList();
@@ -126,7 +126,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
} }
private void GetSignalsWithPreCalculated(ChandelierType chandelierType, List<ChandelierResult> chandelier, private void GetSignalsWithPreCalculated(ChandelierType chandelierType, List<ChandelierResult> chandelier,
HashSet<Candle> candles) IReadOnlyList<Candle> candles)
{ {
ProcessChandelierSignalsForType(chandelier, chandelierType, 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="chandelierType">Type of Chandelier (Long or Short)</param>
/// <param name="candles">Candles to process</param> /// <param name="candles">Candles to process</param>
private void ProcessChandelierSignalsForType(List<ChandelierResult> chandelier, ChandelierType chandelierType, private void ProcessChandelierSignalsForType(List<ChandelierResult> chandelier, ChandelierType chandelierType,
HashSet<Candle> candles) IReadOnlyList<Candle> candles)
{ {
var chandelierCandle = MapChandelierToCandle(chandelier, candles.TakeLast(MinimumHistory)); var chandelierCandle = MapChandelierToCandle(chandelier, candles.TakeLast(MinimumHistory));
if (chandelierCandle.Count == 0) if (chandelierCandle.Count == 0)

View File

@@ -21,7 +21,7 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
MinimumHistory = Math.Max(fastPeriod, slowPeriod) * 2; MinimumHistory = Math.Max(fastPeriod, slowPeriod) * 2;
} }
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles) public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
{ {
return new IndicatorsResultBase() 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) if (candles.Count <= MinimumHistory)
{ {
@@ -58,7 +58,7 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated EMA values for performance optimization. /// Runs the indicator using pre-calculated EMA values for performance optimization.
/// </summary> /// </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) if (candles.Count <= MinimumHistory)
{ {
@@ -105,7 +105,7 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
/// <param name="fastEma">List of Fast EMA calculation results</param> /// <param name="fastEma">List of Fast EMA calculation results</param>
/// <param name="slowEma">List of Slow EMA calculation results</param> /// <param name="slowEma">List of Slow EMA calculation results</param>
/// <param name="candles">Candles to process</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)); var dualEmaCandles = MapDualEmaToCandle(fastEma, slowEma, candles.TakeLast(MinimumHistory));

View File

@@ -18,7 +18,7 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
Period = period; Period = period;
} }
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles) public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
{ {
return new IndicatorsResultBase() 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) if (candles.Count <= Period)
{ {
@@ -52,7 +52,7 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated EMA values for performance optimization. /// Runs the indicator using pre-calculated EMA values for performance optimization.
/// </summary> /// </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) if (candles.Count <= Period)
{ {
@@ -93,7 +93,7 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
/// </summary> /// </summary>
/// <param name="ema">List of EMA calculation results</param> /// <param name="ema">List of EMA calculation results</param>
/// <param name="candles">Candles to process</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)); var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value));

View File

@@ -18,7 +18,7 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
Period = period; Period = period;
} }
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles) public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
{ {
return new IndicatorsResultBase() 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) if (candles.Count <= Period)
{ {
@@ -52,7 +52,7 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated EMA values for performance optimization. /// Runs the indicator using pre-calculated EMA values for performance optimization.
/// </summary> /// </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) if (candles.Count <= Period)
{ {
@@ -93,7 +93,7 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
/// </summary> /// </summary>
/// <param name="ema">List of EMA calculation results</param> /// <param name="ema">List of EMA calculation results</param>
/// <param name="candles">Candles to process</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()); var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value).ToHashSet());

View File

@@ -28,7 +28,7 @@ public class LaggingSTC : IndicatorBase
CyclePeriods = cyclePeriods; CyclePeriods = cyclePeriods;
} }
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (candles.Count <= 2 * (SlowPeriods + CyclePeriods)) if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
{ {
@@ -54,7 +54,7 @@ public class LaggingSTC : IndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated STC values for performance optimization. /// Runs the indicator using pre-calculated STC values for performance optimization.
/// </summary> /// </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)) if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
{ {
@@ -95,7 +95,7 @@ public class LaggingSTC : IndicatorBase
/// </summary> /// </summary>
/// <param name="stc">List of STC calculation results</param> /// <param name="stc">List of STC calculation results</param>
/// <param name="candles">Candles to process</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)); 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(); var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
return new IndicatorsResultBase return new IndicatorsResultBase

View File

@@ -21,7 +21,7 @@ public class MacdCrossIndicatorBase : IndicatorBase
SignalPeriods = signalPeriods; SignalPeriods = signalPeriods;
} }
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (candles.Count <= 2 * (SlowPeriods + SignalPeriods)) if (candles.Count <= 2 * (SlowPeriods + SignalPeriods))
{ {
@@ -47,7 +47,7 @@ public class MacdCrossIndicatorBase : IndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated MACD values for performance optimization. /// Runs the indicator using pre-calculated MACD values for performance optimization.
/// </summary> /// </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)) if (candles.Count <= 2 * (SlowPeriods + SignalPeriods))
{ {
@@ -88,7 +88,7 @@ public class MacdCrossIndicatorBase : IndicatorBase
/// </summary> /// </summary>
/// <param name="macd">List of MACD calculation results</param> /// <param name="macd">List of MACD calculation results</param>
/// <param name="candles">Candles to process</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)); 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() return new IndicatorsResultBase()
{ {

View File

@@ -22,7 +22,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
/// Get RSI signals /// Get RSI signals
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (candles.Count <= Period) if (candles.Count <= Period)
{ {
@@ -48,7 +48,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated RSI values for performance optimization. /// Runs the indicator using pre-calculated RSI values for performance optimization.
/// </summary> /// </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) if (candles.Count <= Period)
{ {
@@ -90,7 +90,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
/// </summary> /// </summary>
/// <param name="rsiResult">List of RSI calculation results</param> /// <param name="rsiResult">List of RSI calculation results</param>
/// <param name="candles">Candles to process</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)); var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
@@ -101,7 +101,7 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
GetShortSignals(candlesRsi, candles); GetShortSignals(candlesRsi, candles);
} }
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles) public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
{ {
return new IndicatorsResultBase() 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 // Set the low and high for first candle
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0); 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 // Set the low and high for first candle
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0); 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 lastCandleOnPeriod = candles.TakeLast(Period.Value).ToList();
var signalsOnPeriod = Signals.Where(s => s.Date >= lastCandleOnPeriod[0].Date var signalsOnPeriod = Signals.Where(s => s.Date >= lastCandleOnPeriod[0].Date

View File

@@ -25,7 +25,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
/// Get RSI signals /// Get RSI signals
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (!Period.HasValue || candles.Count <= Period) if (!Period.HasValue || candles.Count <= Period)
{ {
@@ -51,7 +51,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated RSI values for performance optimization. /// Runs the indicator using pre-calculated RSI values for performance optimization.
/// </summary> /// </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) if (!Period.HasValue || candles.Count <= Period)
{ {
@@ -93,7 +93,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
/// </summary> /// </summary>
/// <param name="rsiResult">List of RSI calculation results</param> /// <param name="rsiResult">List of RSI calculation results</param>
/// <param name="candles">Candles to process</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)); var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
@@ -104,7 +104,7 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
GetShortSignals(candlesRsi, candles); GetShortSignals(candlesRsi, candles);
} }
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles) public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList<Candle> candles)
{ {
return new IndicatorsResultBase() 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 // Set the low and high for first candle
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0); 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 // Set the low and high for first candle
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0); 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, var signal = new LightSignal(candleSignal.Ticker, direction, Confidence.Low,
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);

View File

@@ -21,7 +21,7 @@ public class StcIndicatorBase : IndicatorBase
CyclePeriods = cyclePeriods; CyclePeriods = cyclePeriods;
} }
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (candles.Count <= 2 * (SlowPeriods + CyclePeriods)) if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
{ {
@@ -50,7 +50,7 @@ public class StcIndicatorBase : IndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated STC values for performance optimization. /// Runs the indicator using pre-calculated STC values for performance optimization.
/// </summary> /// </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)) 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) if (FastPeriods != null && SlowPeriods != null)
{ {
@@ -105,7 +105,7 @@ public class StcIndicatorBase : IndicatorBase
/// </summary> /// </summary>
/// <param name="stc">List of STC calculation results</param> /// <param name="stc">List of STC calculation results</param>
/// <param name="candles">Candles to process</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) if (CyclePeriods == null)
return; return;

View File

@@ -28,7 +28,7 @@ public class StochasticCrossIndicator : IndicatorBase
DFactor = dFactor; 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) 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) 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) /// 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) /// Short signals: %K crosses below %D when both lines are above 80 (overbought)
/// </summary> /// </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)); 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() return new IndicatorsResultBase()
{ {

View File

@@ -20,7 +20,7 @@ public class SuperTrendCrossEma : IndicatorBase
MinimumHistory = 100 + Period.Value; 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 // Validate sufficient historical data for all indicators
const int emaPeriod = 50; const int emaPeriod = 50;
@@ -89,7 +89,7 @@ public class SuperTrendCrossEma : IndicatorBase
/// Runs the indicator using pre-calculated SuperTrend values for performance optimization. /// 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. /// Note: EMA50 and ADX are still calculated on-the-fly as they're not part of the standard indicator values.
/// </summary> /// </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 // Validate sufficient historical data for all indicators
const int emaPeriod = 50; const int emaPeriod = 50;
@@ -157,7 +157,7 @@ public class SuperTrendCrossEma : IndicatorBase
List<SuperTrendResult> superTrend, List<SuperTrendResult> superTrend,
List<EmaResult> ema50, List<EmaResult> ema50,
List<AdxResult> adxResults, List<AdxResult> adxResults,
HashSet<Candle> candles, IReadOnlyList<Candle> candles,
int minimumRequiredHistory, int minimumRequiredHistory,
int adxThreshold) 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() return new IndicatorsResultBase()
{ {

View File

@@ -20,7 +20,7 @@ public class SuperTrendIndicatorBase : IndicatorBase
MinimumHistory = 100 + Period.Value; MinimumHistory = 100 + Period.Value;
} }
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (candles.Count <= MinimumHistory) if (candles.Count <= MinimumHistory)
{ {
@@ -48,7 +48,7 @@ public class SuperTrendIndicatorBase : IndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated SuperTrend values for performance optimization. /// Runs the indicator using pre-calculated SuperTrend values for performance optimization.
/// </summary> /// </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) if (candles.Count <= MinimumHistory)
{ {
@@ -89,7 +89,7 @@ public class SuperTrendIndicatorBase : IndicatorBase
/// </summary> /// </summary>
/// <param name="superTrend">List of SuperTrend calculation results</param> /// <param name="superTrend">List of SuperTrend calculation results</param>
/// <param name="candles">Candles to process</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)); 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() return new IndicatorsResultBase()
{ {

View File

@@ -17,7 +17,7 @@ namespace Managing.Domain.Strategies.Signals
public TradeDirection Direction { get; } 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>(); 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, /// 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(). /// so pre-calculated values are not applicable. This method falls back to regular Run().
/// </summary> /// </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 // ThreeWhiteSoldiers doesn't use traditional indicators, so pre-calculated values don't apply
// Fall back to regular calculation // 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. /// This method is shared between the regular Run() and optimized Run() methods.
/// </summary> /// </summary>
/// <param name="candles">Candles to process</param> /// <param name="candles">Candles to process</param>
private void ProcessThreeWhiteSoldiersSignals(HashSet<Candle> candles) private void ProcessThreeWhiteSoldiersSignals(IReadOnlyList<Candle> candles)
{ {
var lastFourCandles = candles.TakeLast(4); var lastFourCandles = candles.TakeLast(4);
Candle previousCandles = null; 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(); throw new NotImplementedException();
} }

View File

@@ -18,7 +18,7 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase
Period = period; Period = period;
} }
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (candles.Count <= 2 * Period) if (candles.Count <= 2 * Period)
{ {
@@ -44,7 +44,7 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated EMA values for performance optimization. /// Runs the indicator using pre-calculated EMA values for performance optimization.
/// </summary> /// </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) if (candles.Count <= 2 * Period)
{ {
@@ -85,7 +85,7 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase
/// </summary> /// </summary>
/// <param name="ema">List of EMA calculation results</param> /// <param name="ema">List of EMA calculation results</param>
/// <param name="candles">Candles to process</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)); 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() return new IndicatorsResultBase()
{ {

View File

@@ -30,7 +30,7 @@ public class IchimokuKumoTrend : IndicatorBase
ChikouOffset = chikouOffset; // Separate offset for Chikou span 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 // 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); 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 // 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); var maxOffset = Math.Max(Math.Max(OffsetPeriods.Value, SenkouOffset ?? OffsetPeriods.Value), ChikouOffset ?? OffsetPeriods.Value);
@@ -160,7 +160,7 @@ public class IchimokuKumoTrend : IndicatorBase
return candleIchimokuResults; return candleIchimokuResults;
} }
private void ProcessKumoTrendSignals(List<CandleIchimoku> ichimokuResults, HashSet<Candle> candles) private void ProcessKumoTrendSignals(List<CandleIchimoku> ichimokuResults, IReadOnlyList<Candle> candles)
{ {
var mappedData = ichimokuResults; 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) if (ichimokuResults.Count == 0)
return; 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; IEnumerable<IchimokuResult> ichimokuResults;

View File

@@ -26,7 +26,7 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
Period = period; Period = period;
} }
public override List<LightSignal> Run(HashSet<Candle> candles) public override List<LightSignal> Run(IReadOnlyList<Candle> candles)
{ {
if (candles.Count <= 10 * Period + 50) if (candles.Count <= 10 * Period + 50)
{ {
@@ -55,7 +55,7 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
/// <summary> /// <summary>
/// Runs the indicator using pre-calculated StochRsi values for performance optimization. /// Runs the indicator using pre-calculated StochRsi values for performance optimization.
/// </summary> /// </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) if (candles.Count <= 10 * Period + 50)
{ {
@@ -96,7 +96,7 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
/// </summary> /// </summary>
/// <param name="stochRsi">List of StochRsi calculation results</param> /// <param name="stochRsi">List of StochRsi calculation results</param>
/// <param name="candles">Candles to process</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)); 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() return new IndicatorsResultBase()
{ {

View File

@@ -55,13 +55,13 @@ public static class TradingBox
{ {
private static readonly IndicatorComboConfig _defaultConfig = new(); 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) Dictionary<string, LightSignal> previousSignal, int? loopbackPeriod = 1)
{ {
return GetSignal(newCandles, scenario, previousSignal, _defaultConfig, loopbackPeriod, null); 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<string, LightSignal> previousSignal, int? loopbackPeriod,
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues) Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
{ {
@@ -69,13 +69,13 @@ public static class TradingBox
preCalculatedIndicatorValues); 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) Dictionary<string, LightSignal> previousSignal, IndicatorComboConfig config, int? loopbackPeriod = 1)
{ {
return GetSignal(newCandles, lightScenario, previousSignal, config, loopbackPeriod, null); 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<string, LightSignal> previousSignal, IndicatorComboConfig config, int? loopbackPeriod,
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues) Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
{ {
@@ -127,12 +127,11 @@ public static class TradingBox
continue; continue;
} }
var limitedCandles = newCandles.ToList(); // newCandles is already a List and ordered chronologically
// Optimized: limitedCandles is already ordered, no need to re-order
var loopback = loopbackPeriod.HasValue && loopbackPeriod > 1 ? loopbackPeriod.Value : 1; var loopback = loopbackPeriod.HasValue && loopbackPeriod > 1 ? loopbackPeriod.Value : 1;
var candleLoopback = limitedCandles.Count > loopback var candleLoopback = newCandles.Count > loopback
? limitedCandles.Skip(limitedCandles.Count - loopback).ToList() ? newCandles.Skip(newCandles.Count - loopback).ToList()
: limitedCandles; : newCandles.ToList();
if (!candleLoopback.Any()) if (!candleLoopback.Any())
{ {
@@ -920,7 +919,7 @@ public static class TradingBox
/// <returns>A dictionary of indicator types to their calculated values.</returns> /// <returns>A dictionary of indicator types to their calculated values.</returns>
public static Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValues( public static Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValues(
Scenario scenario, Scenario scenario,
HashSet<Candle> candles) IReadOnlyList<Candle> candles)
{ {
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>(); var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();

View File

@@ -84,6 +84,12 @@ namespace Managing.Domain.Trades
[Id(18)] [Id(18)]
public bool RecoveryAttempted { get; set; } public bool RecoveryAttempted { get; set; }
/// <summary>
/// The trading type for this position (BacktestFutures or Futures)
/// </summary>
[Id(19)]
public TradingType TradingType { get; set; }
/// <summary> /// <summary>
/// Return true if position is finished even if the position was canceled or rejected /// Return true if position is finished even if the position was canceled or rejected
/// </summary> /// </summary>

View File

@@ -372,6 +372,8 @@ namespace Managing.Infrastructure.Databases.Migrations
b.HasIndex("Identifier") b.HasIndex("Identifier")
.IsUnique(); .IsUnique();
b.HasIndex("MasterBotUserId");
b.HasIndex("Status"); b.HasIndex("Status");
b.HasIndex("UserId"); b.HasIndex("UserId");
@@ -929,6 +931,9 @@ namespace Managing.Infrastructure.Databases.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<int>("TradingType")
.HasColumnType("integer");
b.Property<decimal>("UiFees") b.Property<decimal>("UiFees")
.HasColumnType("decimal(18,8)"); .HasColumnType("decimal(18,8)");
@@ -1571,12 +1576,19 @@ namespace Managing.Infrastructure.Databases.Migrations
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => 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") b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.SetNull) .OnDelete(DeleteBehavior.SetNull)
.IsRequired(); .IsRequired();
b.Navigation("MasterBotUser");
b.Navigation("User"); b.Navigation("User");
}); });

View File

@@ -57,4 +57,9 @@ public class PositionEntity
[ForeignKey("TakeProfit2TradeId")] public virtual TradeEntity? TakeProfit2Trade { get; set; } [ForeignKey("TakeProfit2TradeId")] public virtual TradeEntity? TakeProfit2Trade { get; set; }
[Column(TypeName = "decimal(18,8)")] public decimal NetPnL { 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; }
} }

View File

@@ -331,6 +331,13 @@ public static class PostgreSqlMappers
{ {
if (backtest == null) return null; 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 return new BacktestEntity
{ {
Identifier = backtest.Id, Identifier = backtest.Id,
@@ -339,20 +346,20 @@ public static class PostgreSqlMappers
WinRate = backtest.WinRate, WinRate = backtest.WinRate,
GrowthPercentage = backtest.GrowthPercentage, GrowthPercentage = backtest.GrowthPercentage,
HodlPercentage = backtest.HodlPercentage, HodlPercentage = backtest.HodlPercentage,
ConfigJson = JsonConvert.SerializeObject(backtest.Config), ConfigJson = JsonConvert.SerializeObject(backtest.Config, jsonSettings),
Name = backtest.Config?.Name ?? string.Empty, Name = backtest.Config?.Name ?? string.Empty,
Ticker = backtest.Config?.Ticker.ToString() ?? string.Empty, Ticker = backtest.Config?.Ticker.ToString() ?? string.Empty,
Timeframe = (int)backtest.Config.Timeframe, Timeframe = (int)backtest.Config.Timeframe,
IndicatorsCsv = string.Join(',', backtest.Config.Scenario.Indicators.Select(i => i.Type.ToString())), IndicatorsCsv = string.Join(',', backtest.Config.Scenario.Indicators.Select(i => i.Type.ToString())),
IndicatorsCount = backtest.Config.Scenario.Indicators.Count, IndicatorsCount = backtest.Config.Scenario.Indicators.Count,
PositionsJson = JsonConvert.SerializeObject(backtest.Positions.Values.ToList()), PositionsJson = JsonConvert.SerializeObject(backtest.Positions.Values.ToList(), jsonSettings),
SignalsJson = JsonConvert.SerializeObject(backtest.Signals.Values.ToList()), SignalsJson = JsonConvert.SerializeObject(backtest.Signals.Values.ToList(), jsonSettings),
StartDate = backtest.StartDate, StartDate = backtest.StartDate,
EndDate = backtest.EndDate, EndDate = backtest.EndDate,
Duration = backtest.EndDate - backtest.StartDate, Duration = backtest.EndDate - backtest.StartDate,
MoneyManagementJson = JsonConvert.SerializeObject(backtest.Config?.MoneyManagement), MoneyManagementJson = JsonConvert.SerializeObject(backtest.Config?.MoneyManagement, jsonSettings),
UserId = backtest.User?.Id ?? 0, 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, SharpeRatio = backtest.Statistics?.SharpeRatio ?? 0m,
MaxDrawdown = backtest.Statistics?.MaxDrawdown ?? 0m, MaxDrawdown = backtest.Statistics?.MaxDrawdown ?? 0m,
MaxDrawdownRecoveryTime = backtest.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero, MaxDrawdownRecoveryTime = backtest.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero,
@@ -615,7 +622,8 @@ public static class PostgreSqlMappers
{ {
Status = entity.Status, Status = entity.Status,
SignalIdentifier = entity.SignalIdentifier, SignalIdentifier = entity.SignalIdentifier,
InitiatorIdentifier = entity.InitiatorIdentifier InitiatorIdentifier = entity.InitiatorIdentifier,
TradingType = entity.TradingType
}; };
// Set ProfitAndLoss with proper type // Set ProfitAndLoss with proper type
@@ -657,6 +665,7 @@ public static class PostgreSqlMappers
SignalIdentifier = position.SignalIdentifier, SignalIdentifier = position.SignalIdentifier,
UserId = position.User?.Id ?? 0, UserId = position.User?.Id ?? 0,
InitiatorIdentifier = position.InitiatorIdentifier, InitiatorIdentifier = position.InitiatorIdentifier,
TradingType = position.TradingType,
MoneyManagementJson = position.MoneyManagement != null MoneyManagementJson = position.MoneyManagement != null
? JsonConvert.SerializeObject(position.MoneyManagement) ? JsonConvert.SerializeObject(position.MoneyManagement)
: null, : null,

View File

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

View File

@@ -121,26 +121,14 @@ namespace Managing.Infrastructure.Exchanges
reduceOnly: true); reduceOnly: true);
} }
public async Task<Trade> ClosePosition(Account account, Position position, decimal lastPrice, public async Task<Trade> ClosePosition(Account account, Position position, decimal lastPrice)
bool isForPaperTrading = false)
{ {
var direction = position.OriginDirection == TradeDirection.Long var direction = position.OriginDirection == TradeDirection.Long
? TradeDirection.Short ? TradeDirection.Short
: TradeDirection.Long; : TradeDirection.Long;
if (isForPaperTrading) // Paper trading logic has been moved to CloseBacktestFuturesPositionCommandHandler
{ // This method now only handles live trading
var fake = BuildEmptyTrade(position.Open.Ticker,
lastPrice,
position.Open.Quantity,
direction,
position.Open.Leverage,
TradeType.Market,
position.Open.Date,
TradeStatus.Filled);
return fake;
}
var processor = GetProcessor(account); var processor = GetProcessor(account);
var closedTrade = await processor.OpenTrade( var closedTrade = await processor.OpenTrade(
account, account,

View File

@@ -306,10 +306,19 @@ namespace Managing.Infrastructure.Messengers.Discord
await component.RespondAsync("Alright, let met few seconds to close this position"); await component.RespondAsync("Alright, let met few seconds to close this position");
var position = await tradingService.GetPositionByIdentifierAsync(Guid.Parse(parameters[1])); var position = await tradingService.GetPositionByIdentifierAsync(Guid.Parse(parameters[1]));
var command = new ClosePositionCommand(position, position.AccountId); Position result;
var result = if (position.TradingType == TradingType.BacktestFutures)
await new ClosePositionCommandHandler(exchangeService, accountService, tradingService, scopeFactory) {
var command = new CloseBacktestFuturesPositionCommand(position, position.AccountId);
result = await new CloseBacktestFuturesPositionCommandHandler(exchangeService, accountService, tradingService, scopeFactory)
.Handle(command); .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>() var fields = new List<EmbedFieldBuilder>()
{ {
new EmbedFieldBuilder new EmbedFieldBuilder

View File

@@ -298,12 +298,12 @@ function UserInfoSettings() {
{...registerTelegram('telegramChannel', { {...registerTelegram('telegramChannel', {
required: 'Telegram channel is required', required: 'Telegram channel is required',
pattern: { pattern: {
value: /^[0-9]{5,15}$/, 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 numeric channel ID (5-15 digits)' message: 'Enter channel ID, @username, or Telegram URL (t.me, telegram.me, or web.telegram.org)'
} }
})} })}
defaultValue={user?.telegramChannel || ''} defaultValue={user?.telegramChannel || ''}
placeholder="2828543022" placeholder="-3368031621 or @channelname or https://t.me/channelname"
/> />
{telegramErrors.telegramChannel && ( {telegramErrors.telegramChannel && (
<label className="label"> <label className="label">

View File

@@ -123,7 +123,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
Timeframe = Timeframe.FifteenMinutes, Timeframe = Timeframe.FifteenMinutes,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 1000, BotTradingBalance = 1000,
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,
FlipPosition = false, FlipPosition = false,
@@ -136,7 +136,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
// Act // Act
var result = await _backtestExecutor.ExecuteAsync( var result = await _backtestExecutor.ExecuteAsync(
config, config,
candles.ToHashSet(), candles, // candles is already a List, no conversion needed
_testUser, _testUser,
save: false, save: false,
withCandles: false, withCandles: false,
@@ -172,16 +172,16 @@ public class BacktestExecutorTests : BaseTests, IDisposable
Assert.NotNull(result); Assert.NotNull(result);
Assert.IsType<LightBacktest>(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(1000.0m, result.InitialBalance);
Assert.Equal(45.30m, Math.Round(result.FinalPnl, 2)); Assert.Equal(8.79m, Math.Round(result.FinalPnl, 2));
Assert.Equal(32, result.WinRate); Assert.Equal(31, result.WinRate);
Assert.Equal(-1.77m, Math.Round(result.GrowthPercentage, 2)); Assert.Equal(-6.14m, Math.Round(result.GrowthPercentage, 2));
Assert.Equal(-0.67m, Math.Round(result.HodlPercentage, 2)); Assert.Equal(-0.67m, Math.Round(result.HodlPercentage, 2));
Assert.Equal(59.97m, Math.Round(result.Fees, 2)); Assert.Equal(66.46m, Math.Round(result.Fees, 2));
Assert.Equal(-17.74m, Math.Round(result.NetPnl, 2)); Assert.Equal(-61.36m, Math.Round(result.NetPnl, 2));
Assert.Equal(158.79m, Math.Round((decimal)result.MaxDrawdown, 2)); Assert.Equal(202.29m, Math.Round((decimal)result.MaxDrawdown, 2));
Assert.Equal(-0.004, Math.Round((double)(result.SharpeRatio ?? 0), 3)); Assert.Equal(-0.015, Math.Round((double)(result.SharpeRatio ?? 0), 3));
Assert.True(Math.Abs(result.Score - 0.0) < 0.001, Assert.True(Math.Abs(result.Score - 0.0) < 0.001,
$"Score {result.Score} should be within 0.001 of expected value 0.0"); $"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, Timeframe = Timeframe.FifteenMinutes,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 100000, // Increased balance for testing more candles BotTradingBalance = 100000, // Increased balance for testing more candles
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,
FlipPosition = false, FlipPosition = false,
@@ -231,7 +231,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
// Act // Act
var result = await _backtestExecutor.ExecuteAsync( var result = await _backtestExecutor.ExecuteAsync(
config, config,
candles.ToHashSet(), candles, // candles is already a List, no conversion needed
_testUser, _testUser,
save: false, save: false,
withCandles: false, withCandles: false,
@@ -264,16 +264,16 @@ public class BacktestExecutorTests : BaseTests, IDisposable
Assert.NotNull(result); Assert.NotNull(result);
Assert.IsType<LightBacktest>(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(100000.0m, result.InitialBalance);
Assert.Equal(-33978.09m, Math.Round(result.FinalPnl, 2)); Assert.Equal(-17671.68m, Math.Round(result.FinalPnl, 2));
Assert.Equal(21, result.WinRate); Assert.Equal(25, result.WinRate);
Assert.Equal(-52.16m, Math.Round(result.GrowthPercentage, 2)); Assert.Equal(-39.91m, Math.Round(result.GrowthPercentage, 2));
Assert.Equal(-12.87m, Math.Round(result.HodlPercentage, 2)); Assert.Equal(-12.87m, Math.Round(result.HodlPercentage, 2));
Assert.Equal(18207.71m, Math.Round(result.Fees, 2)); Assert.Equal(22285.98m, Math.Round(result.Fees, 2));
Assert.Equal(-52156.26m, Math.Round(result.NetPnl, 2)); Assert.Equal(-39910.92m, Math.Round(result.NetPnl, 2));
Assert.Equal(54523.55m, Math.Round((decimal)result.MaxDrawdown, 2)); Assert.Equal(40416.59m, Math.Round((decimal)result.MaxDrawdown, 2));
Assert.Equal(-0.037, Math.Round((double)(result.SharpeRatio ?? 0), 3)); Assert.Equal(-0.023, Math.Round((double)(result.SharpeRatio ?? 0), 3));
Assert.True(Math.Abs(result.Score - 0.0) < 0.001, Assert.True(Math.Abs(result.Score - 0.0) < 0.001,
$"Score {result.Score} should be within 0.001 of expected value 0.0"); $"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, Timeframe = Timeframe.FifteenMinutes,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 100000, BotTradingBalance = 100000,
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,
FlipPosition = false, FlipPosition = false,
@@ -324,7 +324,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
// Act // Act
var result = await _backtestExecutor.ExecuteAsync( var result = await _backtestExecutor.ExecuteAsync(
config, config,
candles.ToHashSet(), candles, // candles is already a List, no conversion needed
_testUser, _testUser,
save: false, save: false,
withCandles: false, withCandles: false,
@@ -398,7 +398,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
Timeframe = Timeframe.FifteenMinutes, Timeframe = Timeframe.FifteenMinutes,
IsForWatchingOnly = false, IsForWatchingOnly = false,
BotTradingBalance = 100000, BotTradingBalance = 100000,
IsForBacktest = true, TradingType = TradingType.BacktestFutures,
CooldownPeriod = 1, CooldownPeriod = 1,
MaxLossStreak = 0, MaxLossStreak = 0,
FlipPosition = false, FlipPosition = false,
@@ -414,7 +414,7 @@ public class BacktestExecutorTests : BaseTests, IDisposable
// Act // Act
var result = await _backtestExecutor.ExecuteAsync( var result = await _backtestExecutor.ExecuteAsync(
config, config,
candles.ToHashSet(), candles, // candles is already a List, no conversion needed
_testUser, _testUser,
save: false, save: false,
withCandles: false, withCandles: false,
@@ -451,12 +451,12 @@ public class BacktestExecutorTests : BaseTests, IDisposable
Console.WriteLine("Two-Scenarios Backtest Results:"); Console.WriteLine("Two-Scenarios Backtest Results:");
Console.WriteLine(json); 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 // 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 double expectedScore = 0.0;
const int expectedWinRatePercent = 20; // 20% win rate 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 // Allow small tolerance for floating-point precision variations
const decimal pnlTolerance = 0.01m; const decimal pnlTolerance = 0.01m;

View File

@@ -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-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: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-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
1 DateTime TestName CandlesCount ExecutionTimeSeconds ProcessingRateCandlesPerSec MemoryStartMB MemoryEndMB MemoryPeakMB SignalUpdatesCount SignalUpdatesSkipped SignalUpdateEfficiencyPercent BacktestStepsCount AverageSignalUpdateMs AverageBacktestStepMs FinalPnL WinRatePercent GrowthPercentage Score CommitHash GitBranch Environment
24 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
25 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
26 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
27 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
28 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
29 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
30 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

View File

@@ -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-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: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-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
1 DateTime TestName CandlesCount ExecutionTimeSeconds ProcessingRateCandlesPerSec MemoryStartMB MemoryEndMB MemoryPeakMB SignalUpdatesCount SignalUpdatesSkipped SignalUpdateEfficiencyPercent BacktestStepsCount AverageSignalUpdateMs AverageBacktestStepMs FinalPnL WinRatePercent GrowthPercentage Score CommitHash GitBranch Environment
69 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
70 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
71 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
72 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
73 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
74 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
75 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