Refactoring TradingBotBase.cs + clean architecture (#38)
* Refactoring TradingBotBase.cs + clean architecture * Fix basic tests * Fix tests * Fix workers * Fix open positions * Fix closing position stucking the grain * Fix comments * Refactor candle handling to use IReadOnlyList for chronological order preservation across various components
This commit is contained in:
@@ -568,7 +568,7 @@ public class BacktestController : BaseController
|
|||||||
Timeframe = request.Config.Timeframe,
|
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,
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -1,534 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Managing.Application.Abstractions.Grains;
|
|
||||||
using Managing.Application.Abstractions.Services;
|
|
||||||
using Managing.Application.Orleans;
|
|
||||||
using Managing.Core;
|
|
||||||
using Managing.Domain.Accounts;
|
|
||||||
using Managing.Domain.Backtests;
|
|
||||||
using Managing.Domain.Bots;
|
|
||||||
using Managing.Domain.MoneyManagements;
|
|
||||||
using Managing.Domain.Scenarios;
|
|
||||||
using Managing.Domain.Strategies;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Orleans.Concurrency;
|
|
||||||
using static Managing.Common.Enums;
|
|
||||||
|
|
||||||
namespace Managing.Application.Grains;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stateless worker grain for processing bundle backtest requests
|
|
||||||
/// Uses the bundle request ID as the primary key (Guid)
|
|
||||||
/// Implements IRemindable for automatic retry of failed bundles
|
|
||||||
/// Uses custom compute placement with random fallback.
|
|
||||||
/// </summary>
|
|
||||||
[StatelessWorker]
|
|
||||||
[TradingPlacement] // Use custom compute placement with random fallback
|
|
||||||
public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable
|
|
||||||
{
|
|
||||||
private readonly ILogger<BundleBacktestGrain> _logger;
|
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
|
||||||
|
|
||||||
// Reminder configuration
|
|
||||||
private const string RETRY_REMINDER_NAME = "BundleBacktestRetry";
|
|
||||||
private static readonly TimeSpan RETRY_INTERVAL = TimeSpan.FromMinutes(30);
|
|
||||||
|
|
||||||
public BundleBacktestGrain(
|
|
||||||
ILogger<BundleBacktestGrain> logger,
|
|
||||||
IServiceScopeFactory scopeFactory)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_scopeFactory = scopeFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ProcessBundleRequestAsync()
|
|
||||||
{
|
|
||||||
// Get the RequestId from the grain's primary key
|
|
||||||
var bundleRequestId = this.GetPrimaryKey();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Create a new service scope to get fresh instances of services with scoped DbContext
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
|
||||||
var backtester = scope.ServiceProvider.GetRequiredService<IBacktester>();
|
|
||||||
var messengerService = scope.ServiceProvider.GetRequiredService<IMessengerService>();
|
|
||||||
|
|
||||||
// Get the specific bundle request by ID
|
|
||||||
var bundleRequest = await GetBundleRequestById(backtester, bundleRequestId);
|
|
||||||
if (bundleRequest == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("Bundle request {RequestId} not found", bundleRequestId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process only this specific bundle request
|
|
||||||
await ProcessBundleRequest(bundleRequest, backtester, messengerService);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in BundleBacktestGrain for request {RequestId}", bundleRequestId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<BundleBacktestRequest> GetBundleRequestById(IBacktester backtester, Guid bundleRequestId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Get pending and failed bundle backtest requests for retry capability
|
|
||||||
var pendingRequests =
|
|
||||||
await backtester.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Pending);
|
|
||||||
var failedRequests =
|
|
||||||
await backtester.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Failed);
|
|
||||||
|
|
||||||
var allRequests = pendingRequests.Concat(failedRequests);
|
|
||||||
return allRequests.FirstOrDefault(r => r.RequestId == bundleRequestId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to get bundle request {RequestId}", bundleRequestId);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessBundleRequest(
|
|
||||||
BundleBacktestRequest bundleRequest,
|
|
||||||
IBacktester backtester,
|
|
||||||
IMessengerService messengerService)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Starting to process bundle backtest request {RequestId}", bundleRequest.RequestId);
|
|
||||||
|
|
||||||
// Update status to running
|
|
||||||
bundleRequest.Status = BundleBacktestRequestStatus.Running;
|
|
||||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
|
||||||
|
|
||||||
// Generate backtest requests from variant configuration
|
|
||||||
var backtestRequests = await GenerateBacktestRequestsFromVariants(bundleRequest);
|
|
||||||
if (backtestRequests == null || !backtestRequests.Any())
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Failed to generate backtest requests from variants");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process each backtest request sequentially
|
|
||||||
for (int i = 0; i < backtestRequests.Count; i++)
|
|
||||||
{
|
|
||||||
await ProcessSingleBacktest(backtester, backtestRequests[i], bundleRequest, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update final status and send notifications
|
|
||||||
await UpdateFinalStatus(bundleRequest, backtester, messengerService);
|
|
||||||
|
|
||||||
_logger.LogInformation("Completed processing bundle backtest request {RequestId} with status {Status}",
|
|
||||||
bundleRequest.RequestId, bundleRequest.Status);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error processing bundle backtest request {RequestId}", bundleRequest.RequestId);
|
|
||||||
SentrySdk.CaptureException(ex);
|
|
||||||
await HandleBundleRequestError(bundleRequest, backtester, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generates individual backtest requests from variant configuration
|
|
||||||
/// </summary>
|
|
||||||
private async Task<List<RunBacktestRequest>> GenerateBacktestRequestsFromVariants(
|
|
||||||
BundleBacktestRequest bundleRequest)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Deserialize the variant configurations
|
|
||||||
var universalConfig =
|
|
||||||
JsonSerializer.Deserialize<BundleBacktestUniversalConfig>(bundleRequest.UniversalConfigJson);
|
|
||||||
var dateTimeRanges = JsonSerializer.Deserialize<List<DateTimeRange>>(bundleRequest.DateTimeRangesJson);
|
|
||||||
var moneyManagementVariants =
|
|
||||||
JsonSerializer.Deserialize<List<MoneyManagementVariant>>(bundleRequest.MoneyManagementVariantsJson);
|
|
||||||
var tickerVariants = JsonSerializer.Deserialize<List<Ticker>>(bundleRequest.TickerVariantsJson);
|
|
||||||
|
|
||||||
if (universalConfig == null || dateTimeRanges == null || moneyManagementVariants == null ||
|
|
||||||
tickerVariants == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("Failed to deserialize variant configurations for bundle request {RequestId}",
|
|
||||||
bundleRequest.RequestId);
|
|
||||||
return new List<RunBacktestRequest>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the first account for the user using AccountService
|
|
||||||
var firstAccount = await ServiceScopeHelpers.WithScopedService<IAccountService, Account?>(
|
|
||||||
_scopeFactory,
|
|
||||||
async service =>
|
|
||||||
{
|
|
||||||
var accounts =
|
|
||||||
await service.GetAccountsByUserAsync(bundleRequest.User, hideSecrets: true, getBalance: false);
|
|
||||||
return accounts.FirstOrDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (firstAccount == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("No accounts found for user {UserId} in bundle request {RequestId}",
|
|
||||||
bundleRequest.User.Id, bundleRequest.RequestId);
|
|
||||||
return new List<RunBacktestRequest>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var backtestRequests = new List<RunBacktestRequest>();
|
|
||||||
|
|
||||||
foreach (var dateRange in dateTimeRanges)
|
|
||||||
{
|
|
||||||
foreach (var mmVariant in moneyManagementVariants)
|
|
||||||
{
|
|
||||||
foreach (var ticker in tickerVariants)
|
|
||||||
{
|
|
||||||
var config = new TradingBotConfigRequest
|
|
||||||
{
|
|
||||||
AccountName = firstAccount.Name,
|
|
||||||
Ticker = ticker,
|
|
||||||
Timeframe = universalConfig.Timeframe,
|
|
||||||
IsForWatchingOnly = universalConfig.IsForWatchingOnly,
|
|
||||||
BotTradingBalance = universalConfig.BotTradingBalance,
|
|
||||||
Name =
|
|
||||||
$"{universalConfig.BotName}_{ticker}_{dateRange.StartDate:yyyyMMdd}_{dateRange.EndDate:yyyyMMdd}",
|
|
||||||
FlipPosition = universalConfig.FlipPosition,
|
|
||||||
CooldownPeriod = universalConfig.CooldownPeriod,
|
|
||||||
MaxLossStreak = universalConfig.MaxLossStreak,
|
|
||||||
Scenario = universalConfig.Scenario,
|
|
||||||
ScenarioName = universalConfig.ScenarioName,
|
|
||||||
MoneyManagement = mmVariant.MoneyManagement,
|
|
||||||
MaxPositionTimeHours = universalConfig.MaxPositionTimeHours,
|
|
||||||
CloseEarlyWhenProfitable = universalConfig.CloseEarlyWhenProfitable,
|
|
||||||
FlipOnlyWhenInProfit = universalConfig.FlipOnlyWhenInProfit,
|
|
||||||
UseSynthApi = universalConfig.UseSynthApi,
|
|
||||||
UseForPositionSizing = universalConfig.UseForPositionSizing,
|
|
||||||
UseForSignalFiltering = universalConfig.UseForSignalFiltering,
|
|
||||||
UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss
|
|
||||||
};
|
|
||||||
|
|
||||||
var backtestRequest = new RunBacktestRequest
|
|
||||||
{
|
|
||||||
Config = config,
|
|
||||||
StartDate = dateRange.StartDate,
|
|
||||||
EndDate = dateRange.EndDate,
|
|
||||||
Balance = universalConfig.BotTradingBalance,
|
|
||||||
WatchOnly = universalConfig.WatchOnly,
|
|
||||||
Save = universalConfig.Save,
|
|
||||||
WithCandles = false, // Bundle backtests never return candles
|
|
||||||
MoneyManagement = mmVariant.MoneyManagement != null
|
|
||||||
? new MoneyManagement
|
|
||||||
{
|
|
||||||
Name = mmVariant.MoneyManagement.Name,
|
|
||||||
Timeframe = mmVariant.MoneyManagement.Timeframe,
|
|
||||||
StopLoss = mmVariant.MoneyManagement.StopLoss,
|
|
||||||
TakeProfit = mmVariant.MoneyManagement.TakeProfit,
|
|
||||||
Leverage = mmVariant.MoneyManagement.Leverage
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
};
|
|
||||||
|
|
||||||
backtestRequests.Add(backtestRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return backtestRequests;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error generating backtest requests from variants for bundle request {RequestId}",
|
|
||||||
bundleRequest.RequestId);
|
|
||||||
return new List<RunBacktestRequest>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessSingleBacktest(
|
|
||||||
IBacktester backtester,
|
|
||||||
RunBacktestRequest runBacktestRequest,
|
|
||||||
BundleBacktestRequest bundleRequest,
|
|
||||||
int index)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Calculate total count from the variant configuration
|
|
||||||
var totalCount = bundleRequest.TotalBacktests;
|
|
||||||
|
|
||||||
// Update current backtest being processed
|
|
||||||
bundleRequest.CurrentBacktest = $"Backtest {index + 1} of {totalCount}";
|
|
||||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
|
||||||
|
|
||||||
bundleRequest.User.Accounts = await ServiceScopeHelpers.WithScopedService<IAccountService, List<Account>>(
|
|
||||||
_scopeFactory,
|
|
||||||
async service => { return (await service.GetAccountsByUserAsync(bundleRequest.User, true)).ToList(); });
|
|
||||||
// Run the backtest directly with the strongly-typed request
|
|
||||||
var backtestId = await RunSingleBacktest(backtester, runBacktestRequest, bundleRequest, index);
|
|
||||||
if (!string.IsNullOrEmpty(backtestId))
|
|
||||||
{
|
|
||||||
bundleRequest.Results.Add(backtestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update progress
|
|
||||||
bundleRequest.CompletedBacktests++;
|
|
||||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
|
||||||
|
|
||||||
_logger.LogInformation("Completed backtest {Index} for bundle request {RequestId}",
|
|
||||||
index + 1, bundleRequest.RequestId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error processing backtest {Index} for bundle request {RequestId}",
|
|
||||||
index + 1, bundleRequest.RequestId);
|
|
||||||
bundleRequest.FailedBacktests++;
|
|
||||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
|
||||||
SentrySdk.CaptureException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> RunSingleBacktest(
|
|
||||||
IBacktester backtester,
|
|
||||||
RunBacktestRequest runBacktestRequest,
|
|
||||||
BundleBacktestRequest bundleRequest,
|
|
||||||
int index)
|
|
||||||
{
|
|
||||||
if (runBacktestRequest?.Config == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("Invalid RunBacktestRequest in bundle (null config)");
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map MoneyManagement
|
|
||||||
MoneyManagement moneyManagement = null;
|
|
||||||
if (!string.IsNullOrEmpty(runBacktestRequest.Config.MoneyManagementName))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("MoneyManagementName provided but cannot resolve in grain context: {Name}",
|
|
||||||
runBacktestRequest.Config.MoneyManagementName);
|
|
||||||
}
|
|
||||||
else if (runBacktestRequest.Config.MoneyManagement != null)
|
|
||||||
{
|
|
||||||
var mmReq = runBacktestRequest.Config.MoneyManagement;
|
|
||||||
moneyManagement = new MoneyManagement
|
|
||||||
{
|
|
||||||
Name = mmReq.Name,
|
|
||||||
Timeframe = mmReq.Timeframe,
|
|
||||||
StopLoss = mmReq.StopLoss,
|
|
||||||
TakeProfit = mmReq.TakeProfit,
|
|
||||||
Leverage = mmReq.Leverage
|
|
||||||
};
|
|
||||||
moneyManagement.FormatPercentage();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map Scenario
|
|
||||||
LightScenario scenario = null;
|
|
||||||
if (runBacktestRequest.Config.Scenario != null)
|
|
||||||
{
|
|
||||||
var sReq = runBacktestRequest.Config.Scenario;
|
|
||||||
scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod)
|
|
||||||
{
|
|
||||||
Indicators = sReq.Indicators?.Select(i => new LightIndicator(i.Name, i.Type)
|
|
||||||
{
|
|
||||||
MinimumHistory = i.MinimumHistory,
|
|
||||||
Period = i.Period,
|
|
||||||
FastPeriods = i.FastPeriods,
|
|
||||||
SlowPeriods = i.SlowPeriods,
|
|
||||||
SignalPeriods = i.SignalPeriods,
|
|
||||||
Multiplier = i.Multiplier,
|
|
||||||
SmoothPeriods = i.SmoothPeriods,
|
|
||||||
StochPeriods = i.StochPeriods,
|
|
||||||
CyclePeriods = i.CyclePeriods
|
|
||||||
}).ToList() ?? new List<LightIndicator>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map TradingBotConfig
|
|
||||||
var backtestConfig = new TradingBotConfig
|
|
||||||
{
|
|
||||||
AccountName = runBacktestRequest.Config.AccountName,
|
|
||||||
MoneyManagement = moneyManagement,
|
|
||||||
Ticker = runBacktestRequest.Config.Ticker,
|
|
||||||
ScenarioName = runBacktestRequest.Config.ScenarioName,
|
|
||||||
Scenario = scenario,
|
|
||||||
Timeframe = runBacktestRequest.Config.Timeframe,
|
|
||||||
IsForWatchingOnly = runBacktestRequest.Config.IsForWatchingOnly,
|
|
||||||
BotTradingBalance = runBacktestRequest.Config.BotTradingBalance,
|
|
||||||
IsForBacktest = true,
|
|
||||||
CooldownPeriod = runBacktestRequest.Config.CooldownPeriod ?? 1,
|
|
||||||
MaxLossStreak = runBacktestRequest.Config.MaxLossStreak,
|
|
||||||
MaxPositionTimeHours = runBacktestRequest.Config.MaxPositionTimeHours,
|
|
||||||
FlipOnlyWhenInProfit = runBacktestRequest.Config.FlipOnlyWhenInProfit,
|
|
||||||
FlipPosition = runBacktestRequest.Config.FlipPosition,
|
|
||||||
Name = $"{bundleRequest.Name} #{index + 1}",
|
|
||||||
CloseEarlyWhenProfitable = runBacktestRequest.Config.CloseEarlyWhenProfitable,
|
|
||||||
UseSynthApi = runBacktestRequest.Config.UseSynthApi,
|
|
||||||
UseForPositionSizing = runBacktestRequest.Config.UseForPositionSizing,
|
|
||||||
UseForSignalFiltering = runBacktestRequest.Config.UseForSignalFiltering,
|
|
||||||
UseForDynamicStopLoss = runBacktestRequest.Config.UseForDynamicStopLoss
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run the backtest
|
|
||||||
var result = await backtester.RunTradingBotBacktest(
|
|
||||||
backtestConfig,
|
|
||||||
runBacktestRequest.StartDate,
|
|
||||||
runBacktestRequest.EndDate,
|
|
||||||
bundleRequest.User,
|
|
||||||
true,
|
|
||||||
runBacktestRequest.WithCandles,
|
|
||||||
bundleRequest.RequestId.ToString()
|
|
||||||
);
|
|
||||||
|
|
||||||
_logger.LogInformation("Processed backtest for bundle request {RequestId}", bundleRequest.RequestId);
|
|
||||||
return result.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateFinalStatus(
|
|
||||||
BundleBacktestRequest bundleRequest,
|
|
||||||
IBacktester backtester,
|
|
||||||
IMessengerService messengerService)
|
|
||||||
{
|
|
||||||
if (bundleRequest.FailedBacktests == 0)
|
|
||||||
{
|
|
||||||
bundleRequest.Status = BundleBacktestRequestStatus.Completed;
|
|
||||||
await NotifyUser(bundleRequest, messengerService);
|
|
||||||
}
|
|
||||||
else if (bundleRequest.CompletedBacktests == 0)
|
|
||||||
{
|
|
||||||
bundleRequest.Status = BundleBacktestRequestStatus.Failed;
|
|
||||||
bundleRequest.ErrorMessage = "All backtests failed";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bundleRequest.Status = BundleBacktestRequestStatus.Completed;
|
|
||||||
bundleRequest.ErrorMessage = $"{bundleRequest.FailedBacktests} backtests failed";
|
|
||||||
await NotifyUser(bundleRequest, messengerService);
|
|
||||||
}
|
|
||||||
|
|
||||||
bundleRequest.CompletedAt = DateTime.UtcNow;
|
|
||||||
bundleRequest.CurrentBacktest = null;
|
|
||||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
|
||||||
|
|
||||||
// Unregister retry reminder since bundle completed
|
|
||||||
await UnregisterRetryReminder();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleBundleRequestError(
|
|
||||||
BundleBacktestRequest bundleRequest,
|
|
||||||
IBacktester backtester,
|
|
||||||
Exception ex)
|
|
||||||
{
|
|
||||||
bundleRequest.Status = BundleBacktestRequestStatus.Failed;
|
|
||||||
bundleRequest.ErrorMessage = ex.Message;
|
|
||||||
bundleRequest.CompletedAt = DateTime.UtcNow;
|
|
||||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
|
||||||
|
|
||||||
// Register retry reminder for failed bundle
|
|
||||||
await RegisterRetryReminder();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task NotifyUser(BundleBacktestRequest bundleRequest, IMessengerService messengerService)
|
|
||||||
{
|
|
||||||
if (bundleRequest.User?.TelegramChannel != null)
|
|
||||||
{
|
|
||||||
var message = bundleRequest.FailedBacktests == 0
|
|
||||||
? $"✅ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed successfully."
|
|
||||||
: $"⚠️ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed with {bundleRequest.FailedBacktests} failed backtests.";
|
|
||||||
|
|
||||||
await messengerService.SendMessage(message, bundleRequest.User.TelegramChannel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IRemindable Implementation
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles reminder callbacks for automatic retry of failed bundle backtests
|
|
||||||
/// </summary>
|
|
||||||
public async Task ReceiveReminder(string reminderName, TickStatus status)
|
|
||||||
{
|
|
||||||
if (reminderName != RETRY_REMINDER_NAME)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Unknown reminder {ReminderName} received", reminderName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bundleRequestId = this.GetPrimaryKey();
|
|
||||||
_logger.LogInformation("Retry reminder triggered for bundle request {RequestId}", bundleRequestId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
|
||||||
var backtester = scope.ServiceProvider.GetRequiredService<IBacktester>();
|
|
||||||
|
|
||||||
// Get the bundle request
|
|
||||||
var bundleRequest = await GetBundleRequestById(backtester, bundleRequestId);
|
|
||||||
if (bundleRequest == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Bundle request {RequestId} not found during retry", bundleRequestId);
|
|
||||||
await UnregisterRetryReminder();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if bundle is still failed
|
|
||||||
if (bundleRequest.Status != BundleBacktestRequestStatus.Failed)
|
|
||||||
{
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Bundle request {RequestId} is no longer failed (status: {Status}), unregistering reminder",
|
|
||||||
bundleRequestId, bundleRequest.Status);
|
|
||||||
await UnregisterRetryReminder();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry the bundle processing
|
|
||||||
_logger.LogInformation("Retrying failed bundle request {RequestId}", bundleRequestId);
|
|
||||||
|
|
||||||
// Reset status to pending for retry
|
|
||||||
bundleRequest.Status = BundleBacktestRequestStatus.Pending;
|
|
||||||
bundleRequest.ErrorMessage = null;
|
|
||||||
bundleRequest.CurrentBacktest = null;
|
|
||||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
|
||||||
|
|
||||||
// Process the bundle again
|
|
||||||
await ProcessBundleRequestAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error during bundle backtest retry for request {RequestId}", bundleRequestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers a retry reminder for this bundle request
|
|
||||||
/// </summary>
|
|
||||||
private async Task RegisterRetryReminder()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await this.RegisterOrUpdateReminder(RETRY_REMINDER_NAME, RETRY_INTERVAL, RETRY_INTERVAL);
|
|
||||||
_logger.LogInformation("Registered retry reminder for bundle request {RequestId}", this.GetPrimaryKey());
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to register retry reminder for bundle request {RequestId}",
|
|
||||||
this.GetPrimaryKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unregisters the retry reminder for this bundle request
|
|
||||||
/// </summary>
|
|
||||||
private async Task UnregisterRetryReminder()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var reminder = await this.GetReminder(RETRY_REMINDER_NAME);
|
|
||||||
if (reminder != null)
|
|
||||||
{
|
|
||||||
await this.UnregisterReminder(reminder);
|
|
||||||
_logger.LogInformation("Unregistered retry reminder for bundle request {RequestId}",
|
|
||||||
this.GetPrimaryKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to unregister retry reminder for bundle request {RequestId}",
|
|
||||||
this.GetPrimaryKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
using Managing.Application.Abstractions.Grains;
|
|
||||||
using Managing.Application.Abstractions.Services;
|
|
||||||
using Managing.Application.Orleans;
|
|
||||||
using Managing.Core;
|
|
||||||
using Managing.Domain.Accounts;
|
|
||||||
using Managing.Domain.Backtests;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Orleans.Concurrency;
|
|
||||||
|
|
||||||
namespace Managing.Application.Grains;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stateless worker grain for processing genetic backtest requests.
|
|
||||||
/// Uses the genetic request ID (string) as the primary key.
|
|
||||||
/// Uses custom compute placement with random fallback.
|
|
||||||
/// </summary>
|
|
||||||
[StatelessWorker]
|
|
||||||
[TradingPlacement] // Use custom compute placement with random fallback
|
|
||||||
public class GeneticBacktestGrain : Grain, IGeneticBacktestGrain
|
|
||||||
{
|
|
||||||
private readonly ILogger<GeneticBacktestGrain> _logger;
|
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
|
||||||
|
|
||||||
public GeneticBacktestGrain(
|
|
||||||
ILogger<GeneticBacktestGrain> logger,
|
|
||||||
IServiceScopeFactory scopeFactory)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_scopeFactory = scopeFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ProcessGeneticRequestAsync()
|
|
||||||
{
|
|
||||||
var requestId = this.GetPrimaryKeyString();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
|
||||||
var geneticService = scope.ServiceProvider.GetRequiredService<IGeneticService>();
|
|
||||||
|
|
||||||
// Load the request by status lists and filter by ID (Pending first, then Failed for retries)
|
|
||||||
var pending = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Pending);
|
|
||||||
var failed = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Failed);
|
|
||||||
var request = pending.Concat(failed).FirstOrDefault(r => r.RequestId == requestId);
|
|
||||||
|
|
||||||
if (request == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("[GeneticBacktestGrain] Request {RequestId} not found among pending/failed.",
|
|
||||||
requestId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark running
|
|
||||||
request.Status = GeneticRequestStatus.Running;
|
|
||||||
await geneticService.UpdateGeneticRequestAsync(request);
|
|
||||||
|
|
||||||
request.User.Accounts = await ServiceScopeHelpers.WithScopedService<IAccountService, List<Account>>(
|
|
||||||
_scopeFactory,
|
|
||||||
async accountService => (await accountService.GetAccountsByUserAsync(request.User)).ToList());
|
|
||||||
|
|
||||||
// Run GA
|
|
||||||
var result = await geneticService.RunGeneticAlgorithm(request, CancellationToken.None);
|
|
||||||
|
|
||||||
// Update final state
|
|
||||||
request.Status = GeneticRequestStatus.Completed;
|
|
||||||
request.CompletedAt = DateTime.UtcNow;
|
|
||||||
request.BestFitness = result.BestFitness;
|
|
||||||
request.BestIndividual = result.BestIndividual;
|
|
||||||
request.ProgressInfo = result.ProgressInfo;
|
|
||||||
await geneticService.UpdateGeneticRequestAsync(request);
|
|
||||||
|
|
||||||
_logger.LogInformation("[GeneticBacktestGrain] Completed request {RequestId}", requestId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
SentrySdk.CaptureException(ex);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
|
||||||
var geneticService = scope.ServiceProvider.GetRequiredService<IGeneticService>();
|
|
||||||
var running = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Running);
|
|
||||||
var req = running.FirstOrDefault(r => r.RequestId == requestId) ?? new GeneticRequest(requestId);
|
|
||||||
req.Status = GeneticRequestStatus.Failed;
|
|
||||||
req.ErrorMessage = ex.Message;
|
|
||||||
req.CompletedAt = DateTime.UtcNow;
|
|
||||||
await geneticService.UpdateGeneticRequestAsync(req);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -187,7 +187,7 @@ namespace Managing.Application.ManageBot
|
|||||||
MasterBotUserId = masterBot.User.Id,
|
MasterBotUserId = masterBot.User.Id,
|
||||||
|
|
||||||
// Set computed/default properties
|
// Set computed/default properties
|
||||||
IsForBacktest = false,
|
TradingType = TradingType.Futures,
|
||||||
Name = masterConfig.Name
|
Name = masterConfig.Name
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Managing.Domain.Trades;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Managing.Application.Trading.Commands
|
||||||
|
{
|
||||||
|
public class CloseBacktestFuturesPositionCommand : IRequest<Position>
|
||||||
|
{
|
||||||
|
public CloseBacktestFuturesPositionCommand(Position position, int accountId, decimal? executionPrice = null)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
AccountId = accountId;
|
||||||
|
ExecutionPrice = executionPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Position Position { get; }
|
||||||
|
public int AccountId { get; }
|
||||||
|
public decimal? ExecutionPrice { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Managing.Domain.Trades;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Managing.Application.Trading.Commands
|
||||||
|
{
|
||||||
|
public class CloseFuturesPositionCommand : IRequest<Position>
|
||||||
|
{
|
||||||
|
public CloseFuturesPositionCommand(Position position, int accountId, decimal? executionPrice = null)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
AccountId = accountId;
|
||||||
|
ExecutionPrice = executionPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Position Position { get; }
|
||||||
|
public int AccountId { get; }
|
||||||
|
public decimal? ExecutionPrice { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@ namespace Managing.Application.Trading.Commands
|
|||||||
bool isForPaperTrading = false,
|
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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using Managing.Application.Abstractions;
|
||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Application.Trading.Commands;
|
||||||
|
using Managing.Common;
|
||||||
|
using Managing.Domain.Shared.Helpers;
|
||||||
|
using Managing.Domain.Trades;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Application.Trading.Handlers;
|
||||||
|
|
||||||
|
public class CloseBacktestFuturesPositionCommandHandler(
|
||||||
|
IExchangeService exchangeService,
|
||||||
|
IAccountService accountService,
|
||||||
|
ITradingService tradingService,
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
ILogger<CloseBacktestFuturesPositionCommandHandler> logger = null)
|
||||||
|
: ICommandHandler<CloseBacktestFuturesPositionCommand, Position>
|
||||||
|
{
|
||||||
|
public async Task<Position> Handle(CloseBacktestFuturesPositionCommand request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// For backtest, use execution price directly
|
||||||
|
var lastPrice = request.ExecutionPrice.GetValueOrDefault();
|
||||||
|
|
||||||
|
// Calculate closing direction (opposite of opening direction)
|
||||||
|
var direction = request.Position.OriginDirection == TradeDirection.Long
|
||||||
|
? TradeDirection.Short
|
||||||
|
: TradeDirection.Long;
|
||||||
|
|
||||||
|
// Build the closing trade directly for backtest (no exchange call needed)
|
||||||
|
var closedTrade = exchangeService.BuildEmptyTrade(
|
||||||
|
request.Position.Open.Ticker,
|
||||||
|
lastPrice,
|
||||||
|
request.Position.Open.Quantity,
|
||||||
|
direction,
|
||||||
|
request.Position.Open.Leverage,
|
||||||
|
TradeType.Market,
|
||||||
|
request.Position.Open.Date,
|
||||||
|
TradeStatus.Filled);
|
||||||
|
|
||||||
|
// Update position status and calculate PnL
|
||||||
|
request.Position.Status = PositionStatus.Finished;
|
||||||
|
request.Position.ProfitAndLoss =
|
||||||
|
TradingBox.GetProfitAndLoss(request.Position, closedTrade.Quantity, lastPrice,
|
||||||
|
request.Position.Open.Leverage);
|
||||||
|
|
||||||
|
// Add UI fees for closing the position
|
||||||
|
var closingPositionSizeUsd = (lastPrice * closedTrade.Quantity) * request.Position.Open.Leverage;
|
||||||
|
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||||
|
request.Position.AddUiFees(closingUiFees);
|
||||||
|
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||||
|
|
||||||
|
// For backtest, skip database update
|
||||||
|
|
||||||
|
return request.Position;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger?.LogError(ex, "Error closing backtest futures position: {Message} \n Stacktrace : {StackTrace}", ex.Message,
|
||||||
|
ex.StackTrace);
|
||||||
|
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
using Managing.Application.Abstractions;
|
||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Application.Trading.Commands;
|
||||||
|
using Managing.Common;
|
||||||
|
using Managing.Domain.Accounts;
|
||||||
|
using Managing.Domain.Shared.Helpers;
|
||||||
|
using Managing.Domain.Trades;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Application.Trading.Handlers;
|
||||||
|
|
||||||
|
public class CloseFuturesPositionCommandHandler(
|
||||||
|
IExchangeService exchangeService,
|
||||||
|
IAccountService accountService,
|
||||||
|
ITradingService tradingService,
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
ILogger<CloseFuturesPositionCommandHandler> logger = null)
|
||||||
|
: ICommandHandler<CloseFuturesPositionCommand, Position>
|
||||||
|
{
|
||||||
|
public async Task<Position> Handle(CloseFuturesPositionCommand request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (request.Position == null)
|
||||||
|
{
|
||||||
|
logger?.LogWarning("Attempted to close position but position is null for account {AccountId}", request.AccountId);
|
||||||
|
throw new ArgumentNullException(nameof(request.Position), "Position cannot be null for closing");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This handler should ONLY handle live trading positions
|
||||||
|
// Backtest/paper trading positions must use CloseBacktestFuturesPositionCommandHandler
|
||||||
|
if (request.Position.TradingType == TradingType.BacktestFutures ||
|
||||||
|
request.Position.Initiator == PositionInitiator.PaperTrading)
|
||||||
|
{
|
||||||
|
logger?.LogError(
|
||||||
|
"CloseFuturesPositionCommandHandler received a backtest/paper trading position. " +
|
||||||
|
"Position: {PositionId}, TradingType: {TradingType}, Initiator: {Initiator}. " +
|
||||||
|
"Use CloseBacktestFuturesPositionCommandHandler instead.",
|
||||||
|
request.Position.Identifier, request.Position.TradingType, request.Position.Initiator);
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"CloseFuturesPositionCommandHandler cannot handle backtest/paper trading positions. " +
|
||||||
|
$"Position {request.Position.Identifier} has TradingType={request.Position.TradingType} and Initiator={request.Position.Initiator}. " +
|
||||||
|
$"Use CloseBacktestFuturesPositionCommandHandler instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Account account = await accountService.GetAccountById(request.AccountId, false, false);
|
||||||
|
|
||||||
|
// For live trading, always get price from exchange
|
||||||
|
var lastPrice = await exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow);
|
||||||
|
|
||||||
|
// Check if position still open on broker
|
||||||
|
var p = (await exchangeService.GetBrokerPositions(account))
|
||||||
|
.FirstOrDefault(x => x.Ticker == request.Position.Ticker);
|
||||||
|
|
||||||
|
// Position not available on the broker, so be sure to update the status
|
||||||
|
if (p == null)
|
||||||
|
{
|
||||||
|
request.Position.Status = PositionStatus.Finished;
|
||||||
|
request.Position.ProfitAndLoss =
|
||||||
|
TradingBox.GetProfitAndLoss(request.Position, request.Position.Open.Quantity, lastPrice,
|
||||||
|
request.Position.Open.Leverage);
|
||||||
|
|
||||||
|
// Add UI fees for closing the position (broker closed it)
|
||||||
|
var closingPositionSizeUsd =
|
||||||
|
(lastPrice * request.Position.Open.Quantity) * request.Position.Open.Leverage;
|
||||||
|
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||||
|
request.Position.AddUiFees(closingUiFees);
|
||||||
|
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||||
|
|
||||||
|
await tradingService.UpdatePositionAsync(request.Position);
|
||||||
|
return request.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
var closeRequestedOrders = true; // TODO: For gmx no need to close orders since they are closed automatically
|
||||||
|
|
||||||
|
// Close market
|
||||||
|
var closedPosition =
|
||||||
|
await exchangeService.ClosePosition(account, request.Position, lastPrice);
|
||||||
|
|
||||||
|
if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled))
|
||||||
|
{
|
||||||
|
request.Position.Status = PositionStatus.Finished;
|
||||||
|
request.Position.ProfitAndLoss =
|
||||||
|
TradingBox.GetProfitAndLoss(request.Position, closedPosition.Quantity, lastPrice,
|
||||||
|
request.Position.Open.Leverage);
|
||||||
|
|
||||||
|
// Add UI fees for closing the position
|
||||||
|
var closingPositionSizeUsd = (lastPrice * closedPosition.Quantity) * request.Position.Open.Leverage;
|
||||||
|
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||||
|
request.Position.AddUiFees(closingUiFees);
|
||||||
|
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||||
|
|
||||||
|
await tradingService.UpdatePositionAsync(request.Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.Position;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger?.LogError(ex, "Error closing futures position: {Message} \n Stacktrace : {StackTrace}", ex.Message,
|
||||||
|
ex.StackTrace);
|
||||||
|
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -34,8 +34,6 @@ public class ClosePositionCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var isForPaperTrading = request.IsForBacktest;
|
|
||||||
|
|
||||||
var lastPrice = request.Position.Initiator == PositionInitiator.PaperTrading
|
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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Managing.Infrastructure.Databases.src.Managing.Infrastructure.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddTradingTypeToPositions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "TradingType",
|
||||||
|
table: "Positions",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Bots_MasterBotUserId",
|
||||||
|
table: "Bots",
|
||||||
|
column: "MasterBotUserId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Bots_Users_MasterBotUserId",
|
||||||
|
table: "Bots",
|
||||||
|
column: "MasterBotUserId",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Bots_Users_MasterBotUserId",
|
||||||
|
table: "Bots");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Bots_MasterBotUserId",
|
||||||
|
table: "Bots");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "TradingType",
|
||||||
|
table: "Positions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -121,26 +121,14 @@ namespace Managing.Infrastructure.Exchanges
|
|||||||
reduceOnly: true);
|
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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
@@ -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
|
||||||
|
|||||||
|
Reference in New Issue
Block a user