From 9819fe014e5e34fac2c05cafc2d1dbc166725b4b Mon Sep 17 00:00:00 2001 From: Oda <102867384+CryptoOda@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:47:24 +0700 Subject: [PATCH] Backtest opti (#10) * Opti backtest * clean --- scripts/build_and_run.sh | 2 +- src/Managing.Api/Controllers/BotController.cs | 2 +- .../StrategyTests.cs | 14 ++-- .../Abstractions/ITradingBot.cs | 6 +- .../Backtesting/Backtester.cs | 4 +- src/Managing.Application/Bots/TradingBot.cs | 69 +++++++++++-------- .../FixedSizedQueue/FixedSizeQueue.cs | 13 ++++ .../Shared/Helpers/TradingBox.cs | 3 +- src/Managing.Domain/Strategies/Strategy.cs | 12 ++-- 9 files changed, 78 insertions(+), 47 deletions(-) create mode 100644 src/Managing.Core/FixedSizedQueue/FixedSizeQueue.cs diff --git a/scripts/build_and_run.sh b/scripts/build_and_run.sh index be69230..9dc9c05 100644 --- a/scripts/build_and_run.sh +++ b/scripts/build_and_run.sh @@ -10,4 +10,4 @@ docker build -t managing.api -f Managing.Api/Dockerfile . --no-cache docker build -t managing.api.workers -f Managing.Api.Workers/Dockerfile . --no-cache # Start up the project using docker-compose -docker-compose -f Managing.Docker/docker-compose.yml -f Managing.Docker/docker-compose.sandbox.yml up -d \ No newline at end of file +docker compose -f Managing.Docker/docker-compose.yml -f Managing.Docker/docker-compose.local.yml up -d \ No newline at end of file diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs index 219dde7..5c07735 100644 --- a/src/Managing.Api/Controllers/BotController.cs +++ b/src/Managing.Api/Controllers/BotController.cs @@ -198,7 +198,7 @@ public class BotController : ControllerBase { Status = item.GetStatus(), Name = item.GetName(), - Candles = item.Candles.ToList(), + Candles = item.OptimizedCandles.ToList(), Positions = item.Positions, Signals = item.Signals.ToList(), WinRate = item.GetWinRate(), diff --git a/src/Managing.Application.Tests/StrategyTests.cs b/src/Managing.Application.Tests/StrategyTests.cs index 7a5ef2f..b9af446 100644 --- a/src/Managing.Application.Tests/StrategyTests.cs +++ b/src/Managing.Application.Tests/StrategyTests.cs @@ -29,7 +29,7 @@ namespace Managing.Application.Tests // Act foreach (var candle in candles) { - rsiStrategy.Candles.Add(candle); + rsiStrategy.Candles.Enqueue(candle); var signals = rsiStrategy.Run(); } @@ -63,7 +63,7 @@ namespace Managing.Application.Tests // Act foreach (var candle in candles) { - rsiStrategy.Candles.Add(candle); + rsiStrategy.Candles.Enqueue(candle); var signals = rsiStrategy.Run(); } @@ -90,7 +90,7 @@ namespace Managing.Application.Tests // Act foreach (var candle in candles) { - rsiStrategy.Candles.Add(candle); + rsiStrategy.Candles.Enqueue(candle); var signals = rsiStrategy.Run(); } @@ -117,7 +117,7 @@ namespace Managing.Application.Tests // Act foreach (var candle in candles) { - superTrendStrategy.Candles.Add(candle); + superTrendStrategy.Candles.Enqueue(candle); var signals = superTrendStrategy.Run(); } @@ -144,7 +144,7 @@ namespace Managing.Application.Tests // Act foreach (var candle in candles) { - chandelierExitStrategy.Candles.Add(candle); + chandelierExitStrategy.Candles.Enqueue(candle); var signals = chandelierExitStrategy.Run(); } @@ -171,7 +171,7 @@ namespace Managing.Application.Tests // Act foreach (var candle in candles) { - emaTrendSrategy.Candles.Add(candle); + emaTrendSrategy.Candles.Enqueue(candle); var signals = emaTrendSrategy.Run(); } @@ -203,7 +203,7 @@ namespace Managing.Application.Tests // Act foreach (var candle in candles) { - stochRsiStrategy.Candles.Add(candle); + stochRsiStrategy.Candles.Enqueue(candle); var signals = stochRsiStrategy.Run(); } diff --git a/src/Managing.Application/Abstractions/ITradingBot.cs b/src/Managing.Application/Abstractions/ITradingBot.cs index a62d70d..0bc76ad 100644 --- a/src/Managing.Application/Abstractions/ITradingBot.cs +++ b/src/Managing.Application/Abstractions/ITradingBot.cs @@ -1,4 +1,5 @@ -using Managing.Domain.Bots; +using Managing.Core.FixedSizedQueue; +using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.MoneyManagements; using Managing.Domain.Strategies; @@ -11,6 +12,7 @@ namespace Managing.Application.Abstractions { HashSet Signals { get; set; } List Positions { get; set; } + FixedSizeQueue OptimizedCandles { get; set; } HashSet Candles { get; set; } Timeframe Timeframe { get; set; } HashSet Strategies { get; set; } @@ -30,4 +32,4 @@ namespace Managing.Application.Abstractions decimal GetTotalFees(); void LoadStrategies(IEnumerable strategies); } -} +} \ No newline at end of file diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index 5f5ff64..201764b 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -142,10 +142,12 @@ namespace Managing.Application.Backtesting bot.WalletBalances.Add(candles.FirstOrDefault().Date, balance); foreach (var candle in candles) { - bot.Candles.Add(candle); + bot.OptimizedCandles.Enqueue(candle); bot.Run(); } + bot.Candles = new HashSet(candles); + var finalPnl = bot.GetProfitAndLoss(); var winRate = bot.GetWinRate(); var optimizedMoneyManagement = TradingBox.GetBestMoneyManagement(candles, bot.Positions, moneyManagement); diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index cc0e18d..6f1b54b 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -2,6 +2,7 @@ using Managing.Application.Abstractions.Services; using Managing.Application.Trading; using Managing.Application.Trading.Commands; +using Managing.Core.FixedSizedQueue; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Candles; @@ -27,6 +28,7 @@ public class TradingBot : Bot, ITradingBot public Account Account { get; set; } public HashSet Strategies { get; set; } + public FixedSizeQueue OptimizedCandles { get; set; } public HashSet Candles { get; set; } public HashSet Signals { get; set; } public List Positions { get; set; } @@ -80,6 +82,7 @@ public class TradingBot : Bot, ITradingBot Strategies = new HashSet(); Signals = new HashSet(); + OptimizedCandles = new FixedSizeQueue(600); Candles = new HashSet(); Positions = new List(); WalletBalances = new Dictionary(); @@ -160,13 +163,13 @@ public class TradingBot : Bot, ITradingBot Logger.LogInformation( $"Time : {DateTime.Now} - Server time {DateTime.Now.ToUniversalTime()} - Bot : {Name} - Type {BotType} - Ticker : {Ticker}"); - var previousCandleCount = Candles.Count; + var previousCandleCount = OptimizedCandles.Count; if (!IsForBacktest) await UpdateCandles(); - if (Candles.Count > previousCandleCount || IsForBacktest) - await UpdateSignals(Candles); + if (OptimizedCandles.Count > previousCandleCount || IsForBacktest) + await UpdateSignals(OptimizedCandles); else Logger.LogInformation($"No need to update signals for {Ticker}"); @@ -177,35 +180,38 @@ public class TradingBot : Bot, ITradingBot SaveBackup(); await UpdateWalletBalances(); - Logger.LogInformation($"Candles : {Candles.Count}"); - Logger.LogInformation($"Signals : {Signals.Count}"); - Logger.LogInformation($"ExecutionCount : {ExecutionCount}"); - Logger.LogInformation($"Positions : {Positions.Count}"); - Logger.LogInformation("__________________________________________________"); + if (OptimizedCandles.Count % 100 == 0) // Log every 10th execution + { + Logger.LogInformation($"Candle date : {OptimizedCandles.Last().Date:u}"); + Logger.LogInformation($"Signals : {Signals.Count}"); + Logger.LogInformation($"ExecutionCount : {ExecutionCount}"); + Logger.LogInformation($"Positions : {Positions.Count}"); + Logger.LogInformation("__________________________________________________"); + } } private async Task PreloadCandles() { - if (Candles.Any()) + if (OptimizedCandles.Any()) return; var candles = await ExchangeService.GetCandlesInflux(Account.Exchange, Ticker, PreloadSince, Timeframe); foreach (var candle in candles.Where(c => c.Date < DateTime.Now.ToUniversalTime())) { - if (!Candles.Any(c => c.Date == candle.Date)) + if (!OptimizedCandles.Any(c => c.Date == candle.Date)) { - Candles.Add(candle); - await UpdateSignals(Candles); + OptimizedCandles.Enqueue(candle); + await UpdateSignals(OptimizedCandles); } } - PreloadedCandlesCount = Candles.Count(); + PreloadedCandlesCount = OptimizedCandles.Count(); } - private async Task UpdateSignals(HashSet candles) + private async Task UpdateSignals(FixedSizeQueue candles) { - var signal = TradingBox.GetSignal(candles, Strategies, Signals); + var signal = TradingBox.GetSignal(candles.ToHashSet(), Strategies, Signals); if (signal == null) return; @@ -236,15 +242,15 @@ public class TradingBot : Bot, ITradingBot protected async Task UpdateCandles() { - if (Candles.Count == 0 || ExecutionCount == 0) + if (OptimizedCandles.Count == 0 || ExecutionCount == 0) return; - var lastCandle = Candles.Last(); + var lastCandle = OptimizedCandles.Last(); var newCandle = await ExchangeService.GetCandlesInflux(Account.Exchange, Ticker, lastCandle.Date, Timeframe); foreach (var candle in newCandle.Where(c => c.Date < DateTime.Now.ToUniversalTime())) { - Candles.Add(candle); + OptimizedCandles.Enqueue(candle); } } @@ -269,17 +275,25 @@ public class TradingBot : Bot, ITradingBot private async Task UpdateWalletBalances() { + var lastCandle = OptimizedCandles.LastOrDefault(); + if (lastCandle == null) return; + + var date = lastCandle.Date; + if (WalletBalances.Count == 0) { - WalletBalances.Add(Candles.LastOrDefault().Date, await ExchangeService.GetBalance(Account, IsForBacktest)); + WalletBalances[date] = await ExchangeService.GetBalance(Account, IsForBacktest); + return; } - else if (!WalletBalances.Any(w => w.Key == Candles.LastOrDefault().Date)) + + if (!WalletBalances.ContainsKey(date)) { - var walletBalance = WalletBalances.FirstOrDefault().Value + GetProfitAndLoss(); - WalletBalances.Add(Candles.LastOrDefault().Date, walletBalance); + var previousBalance = WalletBalances.First().Value; + WalletBalances[date] = previousBalance + GetProfitAndLoss(); } } + private async Task UpdatePosition(Signal signal, Position positionForSignal) { try @@ -293,7 +307,6 @@ public class TradingBot : Bot, ITradingBot if (position.Status == (PositionStatus.Finished | PositionStatus.Flipped)) { await HandleClosedPosition(positionForSignal); - return; } else if (position.Status == PositionStatus.Filled) { @@ -301,7 +314,7 @@ public class TradingBot : Bot, ITradingBot // check if position is still open // Check status, if still open update the status of the position var lastCandle = IsForBacktest - ? Candles.Last() + ? OptimizedCandles.Last() : ExchangeService.GetCandle(Account, Ticker, DateTime.UtcNow); if (positionForSignal.OriginDirection == TradeDirection.Long) @@ -401,7 +414,7 @@ public class TradingBot : Bot, ITradingBot && p.SignalIdentifier != signal.Identifier); var lastPrice = IsForBacktest - ? Candles.Last().Close + ? OptimizedCandles.Last().Close : ExchangeService.GetPrice(Account, Ticker, DateTime.UtcNow); // If position open @@ -511,7 +524,7 @@ public class TradingBot : Bot, ITradingBot if (lastPosition == null) return true; - var tenCandleAgo = Candles.TakeLast(10).First(); + var tenCandleAgo = OptimizedCandles.TakeLast(10).First(); var positionSignal = Signals.FirstOrDefault(s => s.Identifier == lastPosition.SignalIdentifier); return positionSignal.Date < tenCandleAgo.Date; @@ -543,8 +556,8 @@ public class TradingBot : Bot, ITradingBot try { var closedPosition = - await (new ClosePositionCommandHandler(ExchangeService, AccountService, TradingService)) - .Handle(command); + (new ClosePositionCommandHandler(ExchangeService, AccountService, TradingService) + .Handle(command)).Result; if (closedPosition.Status == (PositionStatus.Finished | PositionStatus.Flipped)) { diff --git a/src/Managing.Core/FixedSizedQueue/FixedSizeQueue.cs b/src/Managing.Core/FixedSizedQueue/FixedSizeQueue.cs new file mode 100644 index 0000000..5adead8 --- /dev/null +++ b/src/Managing.Core/FixedSizedQueue/FixedSizeQueue.cs @@ -0,0 +1,13 @@ +namespace Managing.Core.FixedSizedQueue; + +public class FixedSizeQueue : Queue +{ + private readonly int _maxSize; + public FixedSizeQueue(int maxSize) => _maxSize = maxSize; + + public new void Enqueue(T item) + { + while (Count >= _maxSize) Dequeue(); + base.Enqueue(item); + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index b7637aa..b405bf1 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -13,9 +13,10 @@ public static class TradingBox HashSet previousSignal) { var signalOnCandles = new HashSet(); + var limitedCandles = newCandles.ToList().TakeLast(600).ToList(); foreach (var strategy in strategies) { - strategy.UpdateCandles(newCandles); + strategy.UpdateCandles(limitedCandles.ToHashSet()); var signals = strategy.Run(); if (signals == null || signals.Count == 0) continue; diff --git a/src/Managing.Domain/Strategies/Strategy.cs b/src/Managing.Domain/Strategies/Strategy.cs index cf6aae3..eaaada8 100644 --- a/src/Managing.Domain/Strategies/Strategy.cs +++ b/src/Managing.Domain/Strategies/Strategy.cs @@ -1,7 +1,7 @@ -using Managing.Domain.Candles; -using Managing.Core; -using static Managing.Common.Enums; +using Managing.Core.FixedSizedQueue; +using Managing.Domain.Candles; using Managing.Domain.Scenarios; +using static Managing.Common.Enums; namespace Managing.Domain.Strategies { @@ -11,14 +11,14 @@ namespace Managing.Domain.Strategies { Name = name; Timeframe = timeframe; - Candles = new List(); + Candles = new FixedSizeQueue(600); Type = type; SignalType = ScenarioHelpers.GetSignalType(type); } public string Name { get; set; } public Timeframe Timeframe { get; set; } - public List Candles { get; set; } + public FixedSizeQueue Candles { get; set; } public StrategyType Type { get; set; } public SignalType SignalType { get; set; } public int MinimumHistory { get; set; } @@ -49,7 +49,7 @@ namespace Managing.Domain.Strategies { if (Candles.All(c => c.Date != item.Date)) { - Candles.AddItem(item); + Candles.Enqueue(item); } } }