From 6b42e0498791cad7977adc633c9f7d77e97bfcd9 Mon Sep 17 00:00:00 2001 From: mx57 <38256814+mx57@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:27:20 +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 - Implemented dynamic adjustment of DCA price steps in DcaProcessor. - Added volatilityMultiplier calculated from CurrentSpread and signal volatility, capped at [1.0, 5.0]. - Added unit tests in TradingTimedTaskTests.cs to verify the widening behavior. --- .../TradingTimedTaskTests.cs | 113 ++++++++++++++++++ .../Processors/DcaProcessor.cs | 69 ++++++++++- magda_agent_system/agent_tasks.json | 2 +- 3 files changed, 181 insertions(+), 3 deletions(-) diff --git a/IntelliTrader.Trading.Tests/TradingTimedTaskTests.cs b/IntelliTrader.Trading.Tests/TradingTimedTaskTests.cs index b65ec46..6abf53f 100644 --- a/IntelliTrader.Trading.Tests/TradingTimedTaskTests.cs +++ b/IntelliTrader.Trading.Tests/TradingTimedTaskTests.cs @@ -240,5 +240,118 @@ public void DcaProcessor_ScalesCostBasedOnGlobalRating() opt.Metadata != null && opt.Metadata.BoughtGlobalRating == 0.5)), Times.Once()); } + + [Fact] + public void DcaProcessor_WidensSpacingWithHighSignalVolatility() + { + // 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()); + + _tradingService.Setup(s => s.GetPairConfig(pair)).Returns(pairConfig.Object); + + var tradingPair = new Mock(); + tradingPair.Setup(p => p.Pair).Returns(pair); + // With Volatility = 8.0, multiplier should be 8.0 / 4.0 = 2.0x. + // EffectiveNextDCAMargin = -3.0m * 2.0 = -6.0m. + // Setting CurrentMargin to -5.0m (which would normally trigger DCA because -5.0m <= -3.0m) + // But now with widening, -5.0m is NOT <= -6.0m, so it should NOT trigger. + tradingPair.Setup(p => p.CurrentMargin).Returns(-5.0m); + tradingPair.Setup(p => p.CurrentSpread).Returns(0.1m); + 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); + + var signal = new Mock(); + signal.Setup(s => s.Volatility).Returns(8.0); + _signalsService.Setup(s => s.GetSignalsByPair(pair)).Returns(new List { signal.Object }); + + var task = new TradingTimedTask( + _loggingService.Object, + _notificationService.Object, + _healthCheckService.Object, + _signalsService.Object, + _orderingService.Object, + _tradingService.Object); + + string outMsg = ""; + _tradingService.Setup(s => s.CanBuy(It.IsAny(), out outMsg)).Returns(true); + + // Act - Step 1: Process when CurrentMargin is -5.0m (should NOT trigger) + task.ProcessTradingPairs(); + _orderingService.Verify(o => o.PlaceBuyOrder(It.IsAny()), Times.Never()); + + // Act - Step 2: Drop price further so CurrentMargin is -7.0m (which is <= -6.0m effective margin) + tradingPair.Setup(p => p.CurrentMargin).Returns(-7.0m); + task.ProcessTradingPairs(); + + // Assert - Should trigger DCA + _orderingService.Verify(o => o.PlaceBuyOrder(It.IsAny()), Times.Once()); + } + + [Fact] + public void DcaProcessor_WidensSpacingWithHighSpread() + { + // 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, + PauseOnHighSpread = false // Do not pause, just let it use spread for widening multiplier + }; + 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); + // With CurrentSpread = 1.0m and MaxTrailingSpread = 0.5m, multiplier is 1.0 / 0.5 = 2.0x. + // EffectiveNextDCAMargin = -3.0m * 2.0 = -6.0m. + // Setting CurrentMargin to -5.0m (which would normally trigger DCA because -5.0m <= -3.0m) + // But now with widening, -5.0m is NOT <= -6.0m, so it should NOT trigger. + tradingPair.Setup(p => p.CurrentMargin).Returns(-5.0m); + tradingPair.Setup(p => p.CurrentSpread).Returns(1.0m); + 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); + + var task = new TradingTimedTask( + _loggingService.Object, + _notificationService.Object, + _healthCheckService.Object, + _signalsService.Object, + _orderingService.Object, + _tradingService.Object); + + string outMsg = ""; + _tradingService.Setup(s => s.CanBuy(It.IsAny(), out outMsg)).Returns(true); + + // Act - Step 1: Process when CurrentMargin is -5.0m (should NOT trigger) + task.ProcessTradingPairs(); + _orderingService.Verify(o => o.PlaceBuyOrder(It.IsAny()), Times.Never()); + + // Act - Step 2: Drop price further so CurrentMargin is -7.0m (which is <= -6.0m effective margin) + tradingPair.Setup(p => p.CurrentMargin).Returns(-7.0m); + task.ProcessTradingPairs(); + + // Assert - Should trigger DCA + _orderingService.Verify(o => o.PlaceBuyOrder(It.IsAny()), Times.Once()); + } } } diff --git a/IntelliTrader.Trading/Processors/DcaProcessor.cs b/IntelliTrader.Trading/Processors/DcaProcessor.cs index 41d5265..9d04e74 100644 --- a/IntelliTrader.Trading/Processors/DcaProcessor.cs +++ b/IntelliTrader.Trading/Processors/DcaProcessor.cs @@ -24,7 +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) + decimal volatilityMultiplier = 1.0m; + + // 1. CurrentSpread relative to MaxTrailingSpread or 0.2% base + decimal spreadBase = 0.2m; + if (pairConfig.TrailingSafety?.MaxTrailingSpread > 0) + { + spreadBase = pairConfig.TrailingSafety.MaxTrailingSpread; + } + + if (spreadBase > 0) + { + decimal spreadMultiplier = tradingPair.CurrentSpread / spreadBase; + if (spreadMultiplier > volatilityMultiplier) + { + volatilityMultiplier = spreadMultiplier; + } + } + + // 2. Signal volatility relative to a 4.0 base + double? maxSignalVolatility = null; + var signals = signalsService.GetSignalsByPair(tradingPair.Pair); + if (signals != null) + { + foreach (var sig in signals) + { + if (sig.Volatility.HasValue && !double.IsNaN(sig.Volatility.Value) && !double.IsInfinity(sig.Volatility.Value)) + { + if (maxSignalVolatility == null || sig.Volatility.Value > maxSignalVolatility.Value) + { + maxSignalVolatility = sig.Volatility.Value; + } + } + } + } + + if (maxSignalVolatility.HasValue) + { + decimal signalMultiplier = (decimal)(maxSignalVolatility.Value / 4.0); + if (signalMultiplier > volatilityMultiplier) + { + volatilityMultiplier = signalMultiplier; + } + } + + // Limit the multiplier to [1.0, 5.0] + if (volatilityMultiplier > 5.0m) + { + volatilityMultiplier = 5.0m; + } + if (volatilityMultiplier < 1.0m) + { + volatilityMultiplier = 1.0m; + } + + decimal effectiveNextDCAMargin = pairConfig.NextDCAMargin.Value; + if (effectiveNextDCAMargin < 0) + { + effectiveNextDCAMargin *= volatilityMultiplier; + } + else + { + effectiveNextDCAMargin /= volatilityMultiplier; + } + + if (tradingPair.CurrentMargin <= effectiveNextDCAMargin) { // Enforce MaxTrailingSpread safety checks to prevent buying on high-volatility spikes var safety = pairConfig.TrailingSafety; @@ -68,7 +132,8 @@ 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: {pairConfig.NextDCAMargin:0.00}, Effective Level: {effectiveNextDCAMargin:0.00} (Volatility Multiplier: {volatilityMultiplier:0.00}x), " + + $"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",