From da5803a768174f9620c6a9b5196cb26c1faf027a Mon Sep 17 00:00:00 2001 From: mx57 <38256814+mx57@users.noreply.github.com> Date: Sat, 1 Aug 2026 20:41:55 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Complete:=20Optimize=20DCA=20Step?= =?UTF-8?q?=20Spacing=20based=20on=20Market=20Volatility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/bolt.md | 4 + .../TradingTimedTaskTests.cs | 111 ++++++++++++++++++ .../Processors/DcaProcessor.cs | 73 ++++++++++-- magda_agent_system/agent_tasks.json | 2 +- 4 files changed, 178 insertions(+), 12 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 329cbb0..5ade6d5 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,7 @@ +## 2026-08-02 - Динамическое расширение шагов DCA на основе волатильности +**Инсайт:** Во избежание преждевременного срабатывания усредняющих ордеров (DCA) на резких проливах рынка необходимо динамически расширять шаг DCA (делать целевую маржу NextDCAMargin более широкой/отрицательной) на основе рыночной волатильности. В качестве быстрых метрик волатильности отлично подходят текущий спред (CurrentSpread) и историческая волатильность по сигналам (Volatility). +**Действие:** При расчете условий DCA в DcaProcessor масштабировать NextDCAMargin с помощью динамического коэффициента, вычисляемого как отношение текущего спреда/волатильности к базовым значениям (0.2% для спреда и 4.0 для исторической волатильности), с ограничением максимального расширения (до 5.0x). + ## 2026-08-01 - Интеграция реального времени и Web Audio API в Web-интерфейсе **Инсайт:** Автовоспроизведение звуков в современных браузерах блокируется до совершения первого пользовательского действия (клик/интеракция). Для реализации надежного звукового оповещения о сделках необходимо: 1) Динамически генерировать звук через Web Audio API, избавляя проект от внешних аудио-файлов, снижая нагрузку и убирая риск 404 ошибок; 2) Добавлять элементы интеракции (например, кнопку проверки звука "Test Audio") для инициализации и возобновления работы AudioContext со стороны пользователя; 3) Давать возможность отключения звуков в localStorage. **Действие:** При интеграции аудио-оповещений в Razor-представления использовать чистый ES6 JS с динамическим синтезированием аудио-частот через `OscillatorNode` и `GainNode` на событии клика для обхода блокировок воспроизведения. diff --git a/IntelliTrader.Trading.Tests/TradingTimedTaskTests.cs b/IntelliTrader.Trading.Tests/TradingTimedTaskTests.cs index b65ec46..da5af1d 100644 --- a/IntelliTrader.Trading.Tests/TradingTimedTaskTests.cs +++ b/IntelliTrader.Trading.Tests/TradingTimedTaskTests.cs @@ -240,5 +240,116 @@ public void DcaProcessor_ScalesCostBasedOnGlobalRating() opt.Metadata != null && opt.Metadata.BoughtGlobalRating == 0.5)), Times.Once()); } + + [Fact] + public void DcaProcessor_WidensSpacingUnderHighSpread() + { + // Arrange + var pair = "BTCUSDT"; + var pairConfig = new Mock(); + pairConfig.Setup(c => c.NextDCAMargin).Returns(-3.0m); + pairConfig.Setup(c => c.BuyEnabled).Returns(true); + pairConfig.Setup(c => c.BuyMultiplier).Returns(1.5m); + pairConfig.Setup(c => c.BuyTrailing).Returns(0m); + pairConfig.Setup(c => c.Rules).Returns(new List()); + + var safety = new TrailingSafetyOptions + { + MaxTrailingSpread = 0.5m, // baseSpread is MaxTrailingSpread + PauseOnHighSpread = false // We don't pause, we just test widening + }; + pairConfig.Setup(c => c.TrailingSafety).Returns(safety); + + _tradingService.Setup(s => s.GetPairConfig(pair)).Returns(pairConfig.Object); + + var tradingPair = new Mock(); + tradingPair.Setup(p => p.Pair).Returns(pair); + // CurrentMargin is -4.0%. Base DCA is -3.0%. + // Since CurrentSpread (1.5%) > baseSpread (0.5%), spreadFactor = 1.0 + (1.5 - 0.5) = 2.0. + // effectiveNextDCAMargin = -3.0 * 2.0 = -6.0%. + // Since CurrentMargin (-4.0%) > effective (-6.0%), DCA should NOT trigger yet! + tradingPair.Setup(p => p.CurrentMargin).Returns(-4.0m); + tradingPair.Setup(p => p.CurrentSpread).Returns(1.5m); + tradingPair.Setup(p => p.Cost).Returns(100m); + tradingPair.Setup(p => p.Metadata).Returns(new OrderMetadata()); + + _account.Setup(a => a.GetTradingPairs(It.IsAny())).Returns(new List { tradingPair.Object }); + _tradingService.Setup(s => s.GetPrice(pair, It.IsAny(), It.IsAny())).Returns(10000m); + + _signalsService.Setup(s => s.GetGlobalRating()).Returns((double?)null); + _signalsService.Setup(s => s.GetSignalsByPair(pair)).Returns(new List()); + + string outMsg = ""; + _tradingService.Setup(s => s.CanBuy(It.IsAny(), out outMsg)).Returns(true); + + var task = new TradingTimedTask( + _loggingService.Object, + _notificationService.Object, + _healthCheckService.Object, + _signalsService.Object, + _orderingService.Object, + _tradingService.Object); + + // Act + task.ProcessTradingPairs(); + + // Assert - Should NOT trigger DCA because spacing is widened to -6.0% and CurrentMargin is -4.0% + _orderingService.Verify(o => o.PlaceBuyOrder(It.IsAny()), Times.Never()); + } + + [Fact] + public void DcaProcessor_WidensSpacingUnderHighSignalVolatility() + { + // Arrange + var pair = "BTCUSDT"; + var pairConfig = new Mock(); + pairConfig.Setup(c => c.NextDCAMargin).Returns(-3.0m); + pairConfig.Setup(c => c.BuyEnabled).Returns(true); + pairConfig.Setup(c => c.BuyMultiplier).Returns(1.5m); + pairConfig.Setup(c => c.BuyTrailing).Returns(0m); + pairConfig.Setup(c => c.Rules).Returns(new List()); + pairConfig.Setup(c => c.TrailingSafety).Returns((TrailingSafetyOptions)null!); + + _tradingService.Setup(s => s.GetPairConfig(pair)).Returns(pairConfig.Object); + + var tradingPair = new Mock(); + tradingPair.Setup(p => p.Pair).Returns(pair); + // CurrentMargin is -5.0%. Base DCA is -3.0%. + // Signal Volatility is 8.0 (baseline is 4.0). + // signalVolatilityFactor = 1.0 + (8.0 - 4.0) / 4.0 = 2.0. + // effectiveNextDCAMargin = -3.0 * 2.0 = -6.0%. + // Since CurrentMargin (-5.0%) > effective (-6.0%), DCA should NOT trigger yet! + tradingPair.Setup(p => p.CurrentMargin).Returns(-5.0m); + tradingPair.Setup(p => p.CurrentSpread).Returns(0.1m); // low spread, spread factor is 1.0 + tradingPair.Setup(p => p.Cost).Returns(100m); + tradingPair.Setup(p => p.Metadata).Returns(new OrderMetadata()); + + _account.Setup(a => a.GetTradingPairs(It.IsAny())).Returns(new List { tradingPair.Object }); + _tradingService.Setup(s => s.GetPrice(pair, It.IsAny(), It.IsAny())).Returns(10000m); + + _signalsService.Setup(s => s.GetGlobalRating()).Returns((double?)null); + + var mockSignal = new Mock(); + mockSignal.Setup(s => s.Volatility).Returns(8.0); + var signalsList = new List { mockSignal.Object }; + _signalsService.Setup(s => s.GetSignalsByPair(pair)).Returns(signalsList); + + string outMsg = ""; + _tradingService.Setup(s => s.CanBuy(It.IsAny(), out outMsg)).Returns(true); + + var task = new TradingTimedTask( + _loggingService.Object, + _notificationService.Object, + _healthCheckService.Object, + _signalsService.Object, + _orderingService.Object, + _tradingService.Object); + + // Act + task.ProcessTradingPairs(); + + // Assert - Should NOT trigger DCA because spacing is widened to -6.0% and CurrentMargin is -5.0% + _orderingService.Verify(o => o.PlaceBuyOrder(It.IsAny()), Times.Never()); + } } } diff --git a/IntelliTrader.Trading/Processors/DcaProcessor.cs b/IntelliTrader.Trading/Processors/DcaProcessor.cs index 41d5265..e96d3c2 100644 --- a/IntelliTrader.Trading/Processors/DcaProcessor.cs +++ b/IntelliTrader.Trading/Processors/DcaProcessor.cs @@ -24,22 +24,71 @@ public void Process(ITradingPair tradingPair, IPairConfig pairConfig, Concurrent if (pairConfig.NextDCAMargin != null && pairConfig.BuyEnabled && !trailingBuys.ContainsKey(tradingPair.Pair) && !trailingSells.ContainsKey(tradingPair.Pair)) { - if (tradingPair.CurrentMargin <= pairConfig.NextDCAMargin) + // Enforce MaxTrailingSpread safety checks to prevent buying on high-volatility spikes + var safety = pairConfig.TrailingSafety; + if (safety != null && safety.MaxTrailingSpread > 0 && tradingPair.CurrentSpread > safety.MaxTrailingSpread) { - // Enforce MaxTrailingSpread safety checks to prevent buying on high-volatility spikes - var safety = pairConfig.TrailingSafety; - if (safety != null && safety.MaxTrailingSpread > 0 && tradingPair.CurrentSpread > safety.MaxTrailingSpread) + if (safety.PauseOnHighSpread) { - if (safety.PauseOnHighSpread) + if (task.LoggingEnabled) { - if (task.LoggingEnabled) - { - loggingService.Info($"DCA paused for {tradingPair.FormattedName} due to high spread: {tradingPair.CurrentSpread:0.00}%"); - } - return; + loggingService.Info($"DCA paused for {tradingPair.FormattedName} due to high spread: {tradingPair.CurrentSpread:0.00}%"); } + return; + } + } + + // Dynamic adjustment of DCA price steps based on CurrentSpread or Average True Range (ATR) / Signal Volatility to prevent premature DCA buys in extremely volatile markets + decimal effectiveNextDCAMargin = pairConfig.NextDCAMargin.Value; + decimal spreadFactor = 1.0m; + if (tradingPair.CurrentSpread > 0) + { + decimal baseSpread = 0.2m; + if (pairConfig.TrailingSafety != null && pairConfig.TrailingSafety.MaxTrailingSpread > 0) + { + baseSpread = pairConfig.TrailingSafety.MaxTrailingSpread; + } + if (tradingPair.CurrentSpread > baseSpread) + { + spreadFactor = 1.0m + (tradingPair.CurrentSpread - baseSpread); } + } + decimal signalVolatilityFactor = 1.0m; + var signals = signalsService.GetSignalsByPair(tradingPair.Pair); + double maxSignalVolatility = 0; + if (signals != null) + { + foreach (var signal in signals) + { + if (signal.Volatility.HasValue && signal.Volatility.Value > maxSignalVolatility) + { + maxSignalVolatility = signal.Volatility.Value; + } + } + } + if (maxSignalVolatility > 0) + { + decimal baseVolatility = 4.0m; + if ((decimal)maxSignalVolatility > baseVolatility) + { + signalVolatilityFactor = 1.0m + (((decimal)maxSignalVolatility - baseVolatility) / baseVolatility); + } + } + + decimal volatilityFactor = Math.Max(spreadFactor, signalVolatilityFactor); + if (volatilityFactor > 5.0m) + { + volatilityFactor = 5.0m; + } + + if (volatilityFactor > 1.0m) + { + effectiveNextDCAMargin = effectiveNextDCAMargin * volatilityFactor; + } + + if (tradingPair.CurrentMargin <= effectiveNextDCAMargin) + { // Dynamically scale DCA orders based on the global rating double? globalRating = signalsService.GetGlobalRating(); decimal scalingFactor = 1.0m; @@ -68,7 +117,9 @@ public void Process(ITradingPair tradingPair, IPairConfig pairConfig, Concurrent if (task.LoggingEnabled) { loggingService.Info($"DCA triggered for {tradingPair.FormattedName}. Margin: {tradingPair.CurrentMargin:0.00}, " + - $"Level: {pairConfig.NextDCAMargin:0.00}, Multiplier: {pairConfig.BuyMultiplier}, " + + $"Level (Base): {pairConfig.NextDCAMargin:0.00}, Level (Effective): {effectiveNextDCAMargin:0.00}, " + + $"Volatility Factor: {volatilityFactor:0.00} (Spread: {tradingPair.CurrentSpread:0.00}%, Signal Vol: {maxSignalVolatility:0.00}), " + + $"Multiplier: {pairConfig.BuyMultiplier}, " + $"Global Rating: {(globalRating.HasValue ? globalRating.Value.ToString("0.00") : "N/A")}, " + $"Scaling Factor: {scalingFactor:0.00}, Base Cost: {tradingPair.Cost * pairConfig.BuyMultiplier:0.00}, Scaled Cost: {buyOptions.MaxCost:0.00}"); } diff --git a/magda_agent_system/agent_tasks.json b/magda_agent_system/agent_tasks.json index 61dca11..9c1488e 100644 --- a/magda_agent_system/agent_tasks.json +++ b/magda_agent_system/agent_tasks.json @@ -320,7 +320,7 @@ }, { "id": "trading-strategy-dca-scaling", - "status": "todo", + "status": "done", "area": "trading", "risk": "medium", "title": "Optimize DCA Step Spacing based on Market Volatility",