diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index a4af84a..967bad7 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -83,7 +83,8 @@ public class TradingBot : Bot, ITradingBot } else { - throw new ArgumentException("Scenario object must be provided in TradingBotConfig. ScenarioName alone is not sufficient."); + throw new ArgumentException( + "Scenario object must be provided in TradingBotConfig. ScenarioName alone is not sufficient."); } if (!Config.IsForBacktest) @@ -105,22 +106,50 @@ public class TradingBot : Bot, ITradingBot // This is just a safety check if (Config.Scenario == null || !Indicators.Any()) { - throw new InvalidOperationException("Scenario or indicators not loaded properly in constructor. This indicates a configuration error."); + throw new InvalidOperationException( + "Scenario or indicators not loaded properly in constructor. This indicates a configuration error."); } - + PreloadCandles().GetAwaiter().GetResult(); CancelAllOrders().GetAwaiter().GetResult(); - try + // Send startup message only for fresh starts (not reboots) + var isReboot = Signals.Any() || Positions.Any(); + if (!isReboot) { - // await MessengerService.SendMessage( - // $"Hey everyone! I'm about to start {Name}. šŸš€\n" + - // $"I'll post an update here each time a signal is triggered by the following strategies: {string.Join(", ", Strategies.Select(s => s.Name))}." - // ); + try + { + var indicatorNames = Indicators.Select(i => i.Type.ToString()).ToList(); + var startupMessage = $"šŸš€ **Bot Started Successfully!**\n\n" + + $"šŸ“Š **Trading Setup:**\n" + + $"šŸŽÆ Ticker: `{Config.Ticker}`\n" + + $"ā° Timeframe: `{Config.Timeframe}`\n" + + $"šŸŽ® Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" + + $"šŸ’° Balance: `${Config.BotTradingBalance:F2}`\n" + + $"šŸ‘€ Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\n" + + $"šŸ“ˆ **Active Indicators:** `{string.Join(", ", indicatorNames)}`\n\n" + + $"āœ… Ready to monitor signals and execute trades!\n" + + $"šŸ“¢ I'll notify you when signals are triggered."; + + LogInformation(startupMessage).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Logger.LogError(ex, ex.Message); + } } - catch (Exception ex) + else { - Logger.LogError(ex, ex.Message); + try + { + LogInformation($"šŸ”„ **Bot Restarted**\n" + + $"šŸ“Š Resuming operations with {Signals.Count} signals and {Positions.Count} positions\n" + + $"āœ… Ready to continue trading").GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Logger.LogError(ex, ex.Message); + } } InitWorker(Run).GetAwaiter().GetResult(); @@ -144,14 +173,13 @@ public class TradingBot : Bot, ITradingBot } - public void LoadScenario(Scenario scenario) { if (scenario == null) { var errorMessage = "Null scenario provided"; Logger.LogWarning(errorMessage); - + // If called during construction, throw exception instead of Stop() if (Status == BotStatus.Down) { @@ -167,7 +195,7 @@ public class TradingBot : Bot, ITradingBot // Store the scenario in config and load indicators Config.Scenario = scenario; LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario)); - + Logger.LogInformation($"Loaded scenario '{scenario.Name}' with {Indicators.Count} indicators"); } } @@ -181,12 +209,12 @@ public class TradingBot : Bot, ITradingBot { // Clear existing indicators to prevent duplicates Indicators.Clear(); - + foreach (var indicator in indicators) { Indicators.Add(indicator); } - + Logger.LogInformation($"Loaded {Indicators.Count} indicators for bot '{Name}'"); } @@ -1469,22 +1497,154 @@ public class TradingBot : Bot, ITradingBot throw new ArgumentException("Scenario object must be provided in configuration"); } + // Track changes for logging + var changes = new List(); + + // Check for changes and build change list + if (Config.BotTradingBalance != newConfig.BotTradingBalance) + { + changes.Add($"šŸ’° Balance: ${Config.BotTradingBalance:F2} → ${newConfig.BotTradingBalance:F2}"); + } + + if (Config.MaxPositionTimeHours != newConfig.MaxPositionTimeHours) + { + var oldTime = Config.MaxPositionTimeHours?.ToString() + "h" ?? "Disabled"; + var newTime = newConfig.MaxPositionTimeHours?.ToString() + "h" ?? "Disabled"; + changes.Add($"ā±ļø Max Time: {oldTime} → {newTime}"); + } + + if (Config.FlipOnlyWhenInProfit != newConfig.FlipOnlyWhenInProfit) + { + var oldFlip = Config.FlipOnlyWhenInProfit ? "āœ…" : "āŒ"; + var newFlip = newConfig.FlipOnlyWhenInProfit ? "āœ…" : "āŒ"; + changes.Add($"šŸ“ˆ Flip Only in Profit: {oldFlip} → {newFlip}"); + } + + if (Config.CooldownPeriod != newConfig.CooldownPeriod) + { + changes.Add($"ā³ Cooldown: {Config.CooldownPeriod} → {newConfig.CooldownPeriod} candles"); + } + + if (Config.MaxLossStreak != newConfig.MaxLossStreak) + { + changes.Add($"šŸ“‰ Max Loss Streak: {Config.MaxLossStreak} → {newConfig.MaxLossStreak}"); + } + + if (Config.FlipPosition != newConfig.FlipPosition) + { + var oldFlipPos = Config.FlipPosition ? "āœ…" : "āŒ"; + var newFlipPos = newConfig.FlipPosition ? "āœ…" : "āŒ"; + changes.Add($"šŸ”„ Flip Position: {oldFlipPos} → {newFlipPos}"); + } + + if (Config.CloseEarlyWhenProfitable != newConfig.CloseEarlyWhenProfitable) + { + var oldCloseEarly = Config.CloseEarlyWhenProfitable ? "āœ…" : "āŒ"; + var newCloseEarly = newConfig.CloseEarlyWhenProfitable ? "āœ…" : "āŒ"; + changes.Add($"ā° Close Early When Profitable: {oldCloseEarly} → {newCloseEarly}"); + } + + if (Config.UseSynthApi != newConfig.UseSynthApi) + { + var oldSynth = Config.UseSynthApi ? "āœ…" : "āŒ"; + var newSynth = newConfig.UseSynthApi ? "āœ…" : "āŒ"; + changes.Add($"šŸ”— Use Synth API: {oldSynth} → {newSynth}"); + } + + if (Config.UseForPositionSizing != newConfig.UseForPositionSizing) + { + var oldPositionSizing = Config.UseForPositionSizing ? "āœ…" : "āŒ"; + var newPositionSizing = newConfig.UseForPositionSizing ? "āœ…" : "āŒ"; + changes.Add($"šŸ“ Use Synth for Position Sizing: {oldPositionSizing} → {newPositionSizing}"); + } + + if (Config.UseForSignalFiltering != newConfig.UseForSignalFiltering) + { + var oldSignalFiltering = Config.UseForSignalFiltering ? "āœ…" : "āŒ"; + var newSignalFiltering = newConfig.UseForSignalFiltering ? "āœ…" : "āŒ"; + changes.Add($"šŸ” Use Synth for Signal Filtering: {oldSignalFiltering} → {newSignalFiltering}"); + } + + if (Config.UseForDynamicStopLoss != newConfig.UseForDynamicStopLoss) + { + var oldDynamicStopLoss = Config.UseForDynamicStopLoss ? "āœ…" : "āŒ"; + var newDynamicStopLoss = newConfig.UseForDynamicStopLoss ? "āœ…" : "āŒ"; + changes.Add($"šŸŽÆ Use Synth for Dynamic Stop Loss: {oldDynamicStopLoss} → {newDynamicStopLoss}"); + } + + if (Config.IsForWatchingOnly != newConfig.IsForWatchingOnly) + { + var oldWatch = Config.IsForWatchingOnly ? "āœ…" : "āŒ"; + var newWatch = newConfig.IsForWatchingOnly ? "āœ…" : "āŒ"; + changes.Add($"šŸ‘€ Watch Only: {oldWatch} → {newWatch}"); + } + + if (Config.MoneyManagement?.GetType().Name != newConfig.MoneyManagement?.GetType().Name) + { + var oldMM = Config.MoneyManagement?.GetType().Name ?? "None"; + var newMM = newConfig.MoneyManagement?.GetType().Name ?? "None"; + changes.Add($"šŸ’° Money Management: {oldMM} → {newMM}"); + } + + if (Config.RiskManagement != newConfig.RiskManagement) + { + // Compare risk management by serializing (complex object comparison) + var oldRiskSerialized = JsonConvert.SerializeObject(Config.RiskManagement, Formatting.None); + var newRiskSerialized = JsonConvert.SerializeObject(newConfig.RiskManagement, Formatting.None); + + if (oldRiskSerialized != newRiskSerialized) + { + changes.Add($"āš ļø Risk Management: Configuration Updated"); + } + } + + if (Config.ScenarioName != newConfig.ScenarioName) + { + changes.Add($"šŸ“‹ Scenario Name: {Config.ScenarioName ?? "None"} → {newConfig.ScenarioName ?? "None"}"); + } + + if (allowNameChange && Config.Name != newConfig.Name) + { + changes.Add($"šŸ·ļø Name: {Config.Name} → {newConfig.Name}"); + } + + if (Config.AccountName != newConfig.AccountName) + { + changes.Add($"šŸ‘¤ Account: {Config.AccountName} → {newConfig.AccountName}"); + } + + if (Config.Ticker != newConfig.Ticker) + { + changes.Add($"šŸ“Š Ticker: {Config.Ticker} → {newConfig.Ticker}"); + } + + if (Config.Timeframe != newConfig.Timeframe) + { + changes.Add($"šŸ“ˆ Timeframe: {Config.Timeframe} → {newConfig.Timeframe}"); + } + + // Capture current indicators before any changes for scenario comparison + var oldIndicators = Indicators?.ToList() ?? new List(); + + // Check if the actual Scenario object changed (not just the name) + var scenarioChanged = false; + if (Config.Scenario != newConfig.Scenario) + { + var oldScenarioSerialized = JsonConvert.SerializeObject(Config.Scenario, Formatting.None); + var newScenarioSerialized = JsonConvert.SerializeObject(newConfig.Scenario, Formatting.None); + + if (oldScenarioSerialized != newScenarioSerialized) + { + scenarioChanged = true; + changes.Add( + $"šŸŽÆ Scenario: {Config.Scenario?.Name ?? "None"} → {newConfig.Scenario?.Name ?? "None"}"); + } + } + // Protect critical properties that shouldn't change for running bots var protectedIsForBacktest = Config.IsForBacktest; var protectedName = allowNameChange ? newConfig.Name : Config.Name; - // Log the configuration update (before changing anything) - await LogInformation("āš™ļø **Configuration Update**\n" + - "šŸ“Š **Previous Settings:**\n" + - $"šŸ’° Balance: ${Config.BotTradingBalance:F2}\n" + - $"ā±ļø Max Time: {(Config.MaxPositionTimeHours?.ToString() + "h" ?? "Disabled")}\n" + - $"šŸ“ˆ Flip Only in Profit: {(Config.FlipOnlyWhenInProfit ? "āœ…" : "āŒ")}\n" + - $"ā³ Cooldown: {Config.CooldownPeriod} candles\n" + - $"šŸ“‰ Max Loss Streak: {Config.MaxLossStreak}" + - (allowNameChange && newConfig.Name != Config.Name - ? $"\nšŸ·ļø Name: {Config.Name} → {newConfig.Name}" - : "")); - // Update the configuration Config = newConfig; @@ -1505,14 +1665,21 @@ public class TradingBot : Bot, ITradingBot await LoadAccount(); } - // If scenario changed, reload it - var currentScenario = Config.Scenario?.Name; - var newScenario = newConfig.Scenario?.Name; - if (newScenario != currentScenario) + // If scenario changed, reload it and track indicator changes + if (scenarioChanged) { if (newConfig.Scenario != null) { LoadScenario(newConfig.Scenario); + + // Compare indicators after scenario change + var newIndicators = Indicators?.ToList() ?? new List(); + var indicatorChanges = CompareIndicators(oldIndicators, newIndicators); + + if (indicatorChanges.Any()) + { + changes.AddRange(indicatorChanges); + } } else { @@ -1520,13 +1687,17 @@ public class TradingBot : Bot, ITradingBot } } - await LogInformation("āœ… **Configuration Applied**\n" + - "šŸ”§ **New Settings:**\n" + - $"šŸ’° Balance: ${Config.BotTradingBalance:F2}\n" + - $"ā±ļø Max Time: {(Config.MaxPositionTimeHours?.ToString() + "h" ?? "Disabled")}\n" + - $"šŸ“ˆ Flip Only in Profit: {(Config.FlipOnlyWhenInProfit ? "āœ…" : "āŒ")}\n" + - $"ā³ Cooldown: {Config.CooldownPeriod} candles\n" + - $"šŸ“‰ Max Loss Streak: {Config.MaxLossStreak}"); + // Only log if there are actual changes + if (changes.Any()) + { + var changeMessage = "āš™ļø **Configuration Updated**\n" + string.Join("\n", changes); + await LogInformation(changeMessage); + } + else + { + await LogInformation( + "āš™ļø **Configuration Update**\nāœ… No changes detected - configuration already up to date"); + } // Save the updated configuration as backup if (!Config.IsForBacktest) @@ -1555,6 +1726,7 @@ public class TradingBot : Bot, ITradingBot MoneyManagement = Config.MoneyManagement, Ticker = Config.Ticker, ScenarioName = Config.ScenarioName, + Scenario = Config.Scenario, Timeframe = Config.Timeframe, IsForWatchingOnly = Config.IsForWatchingOnly, BotTradingBalance = Config.BotTradingBalance, @@ -1567,8 +1739,70 @@ public class TradingBot : Bot, ITradingBot Name = Config.Name, CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable, UseSynthApi = Config.UseSynthApi, + UseForPositionSizing = Config.UseForPositionSizing, + UseForSignalFiltering = Config.UseForSignalFiltering, + UseForDynamicStopLoss = Config.UseForDynamicStopLoss, + RiskManagement = Config.RiskManagement, }; } + + /// + /// Compares two lists of indicators and returns a list of changes (added, removed, modified). + /// + /// The previous list of indicators + /// The new list of indicators + /// A list of change descriptions + private List CompareIndicators(List oldIndicators, List newIndicators) + { + var changes = new List(); + + // Create dictionaries for easier comparison using Type as key + var oldIndicatorDict = oldIndicators.ToDictionary(i => i.Type, i => i); + var newIndicatorDict = newIndicators.ToDictionary(i => i.Type, i => i); + + // Find removed indicators + var removedTypes = oldIndicatorDict.Keys.Except(newIndicatorDict.Keys); + foreach (var removedType in removedTypes) + { + var indicator = oldIndicatorDict[removedType]; + changes.Add($"āž– **Removed Indicator:** {removedType} ({indicator.GetType().Name})"); + } + + // Find added indicators + var addedTypes = newIndicatorDict.Keys.Except(oldIndicatorDict.Keys); + foreach (var addedType in addedTypes) + { + var indicator = newIndicatorDict[addedType]; + changes.Add($"āž• **Added Indicator:** {addedType} ({indicator.GetType().Name})"); + } + + // Find modified indicators (same type but potentially different configuration) + var commonTypes = oldIndicatorDict.Keys.Intersect(newIndicatorDict.Keys); + foreach (var commonType in commonTypes) + { + var oldIndicator = oldIndicatorDict[commonType]; + var newIndicator = newIndicatorDict[commonType]; + + // Compare indicators by serializing them (simple way to detect configuration changes) + var oldSerialized = JsonConvert.SerializeObject(oldIndicator, Formatting.None); + var newSerialized = JsonConvert.SerializeObject(newIndicator, Formatting.None); + + if (oldSerialized != newSerialized) + { + changes.Add($"šŸ”„ **Modified Indicator:** {commonType} ({newIndicator.GetType().Name})"); + } + } + + // Add summary if there are changes + if (changes.Any()) + { + var summary = + $"šŸ“Š **Indicator Changes:** {addedTypes.Count()} added, {removedTypes.Count()} removed, {commonTypes.Count(c => JsonConvert.SerializeObject(oldIndicatorDict[c]) != JsonConvert.SerializeObject(newIndicatorDict[c]))} modified"; + changes.Insert(0, summary); + } + + return changes; + } } public class TradingBotBackup