diff --git a/src/Managing.Application/Abstractions/ITradingBot.cs b/src/Managing.Application/Abstractions/ITradingBot.cs index ba007885..7f95cc03 100644 --- a/src/Managing.Application/Abstractions/ITradingBot.cs +++ b/src/Managing.Application/Abstractions/ITradingBot.cs @@ -42,5 +42,8 @@ namespace Managing.Application.Abstractions /// The new configuration to apply /// True if the configuration was successfully updated, false otherwise Task UpdateConfiguration(TradingBotConfig newConfig); + + Task LogInformation(string message); + Task LogWarning(string message); } } \ No newline at end of file diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs index 6e5594ac..ca6aa8ad 100644 --- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -1,5 +1,7 @@ using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; +using Managing.Application.Abstractions.Services; +using Managing.Common; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Bots; @@ -291,6 +293,70 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable return; } + // Check broker balance before running + var balances = await ServiceScopeHelpers.WithScopedService>(_scopeFactory, async exchangeService => + { + return await exchangeService.GetBalances(_tradingBot.Account, false); + + }); + + var usdcBalance = balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString()); + var ethBalance = balances.FirstOrDefault(b => b.TokenName == Ticker.ETH.ToString()); + + // Check USDC balance first + if (usdcBalance?.Value < Constants.GMX.Config.MinimumPositionAmount) + { + await _tradingBot.LogWarning( + $"USDC balance is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {usdcBalance?.Value:F2}). Stopping bot {_tradingBot.Identifier}."); + + await StopAsync(); + return; + } + + // Check ETH balance and perform automatic swap if needed + var ethValueInUsd = ethBalance?.Value * ethBalance?.Price ?? 0; + if (ethValueInUsd < 2) // ETH balance below 2 USD + { + await _tradingBot.LogWarning( + $"ETH balance is below 2 USD (actual: {ethValueInUsd:F2}). Attempting to swap USDC to ETH."); + + // Check if we have enough USDC for the swap + if (usdcBalance?.Value >= 5) // Need at least 5 USD for swap + { + try + { + var swapInfo = await ServiceScopeHelpers.WithScopedService(_scopeFactory, async accountService => + { + return await accountService.SwapGmxTokensAsync(_state.State.User, _tradingBot.Account.Name, Ticker.USDC, Ticker.ETH, 5); + }); + + if (swapInfo.Success) + { + await NotifyUserAboutSwap(true, 5, swapInfo.Hash); + } + else + { + await NotifyUserAboutSwap(false, 5, null, swapInfo.Error ?? swapInfo.Message); + await StopAsync(); + return; + } + } + catch (Exception ex) + { + await NotifyUserAboutSwap(false, 5, null, ex.Message); + } + } + else + { + // Both USDC and ETH are low - stop the strategy + await _tradingBot.LogWarning( + $"Both USDC ({usdcBalance?.Value:F2}) and ETH ({ethValueInUsd:F2}) balances are low. Stopping bot {_tradingBot.Identifier}."); + + await StopAsync(); + return; + } + } + // Execute the bot's Run method await _tradingBot.Run(); SyncStateFromBase(); @@ -680,4 +746,37 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable _logger.LogError(ex, "Failed to save bot statistics for bot {BotId}", _state.State.Identifier); } } + + /// + /// Notifies the user about swap operations via webhook/telegram + /// + private async Task NotifyUserAboutSwap(bool isSuccess, decimal amount, string? transactionHash, string? errorMessage = null) + { + try + { + var message = isSuccess + ? $"🔄 **Automatic Swap Successful**\n\n" + + $"🎯 **Bot:** {_tradingBot?.Identifier}\n" + + $"💰 **Amount:** {amount} USDC → ETH\n" + + $"✅ **Status:** Success\n" + + $"🔗 **Transaction:** {transactionHash}\n" + + $"⏰ **Time:** {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC" + : $"❌ **Automatic Swap Failed**\n\n" + + $"🎯 **Bot:** {_tradingBot?.Identifier}\n" + + $"💰 **Amount:** {amount} USDC → ETH\n" + + $"❌ **Status:** Failed\n" + + $"⚠️ **Error:** {errorMessage}\n" + + $"⏰ **Time:** {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"; + + // Send notification via webhook service + await ServiceScopeHelpers.WithScopedService(_scopeFactory, async webhookService => + { + await webhookService.SendMessage(message, _state.State.User?.TelegramChannel); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send swap notification for bot {BotId}", _tradingBot?.Identifier); + } + } } \ No newline at end of file diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index d9b47b16..6bf7bf55 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -141,18 +141,6 @@ public class TradingBotBase : ITradingBot { if (!Config.IsForBacktest) { - // Check broker balance before running - await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService => - { - var balance = await exchangeService.GetBalance(Account, false); - if (balance < Constants.GMX.Config.MinimumPositionAmount && Positions.All(p => p.Value.IsFinished())) - { - await LogWarning( - $"Balance on broker is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {balance}). Stopping bot {Identifier}."); - return; - } - }); - await LoadLastCandle(); } @@ -1380,7 +1368,7 @@ public class TradingBotBase : ITradingBot $"🔄 **Watch Mode Toggle**\nBot: `{Config.Name}`\nWatch Only: `{(Config.IsForWatchingOnly ? "ON" : "OFF")}`"); } - private async Task LogInformation(string message) + public async Task LogInformation(string message) { Logger.LogInformation(message); @@ -1397,7 +1385,7 @@ public class TradingBotBase : ITradingBot } } - private async Task LogWarning(string message) + public async Task LogWarning(string message) { message = $"[{Config.Name}] {message}"; SentrySdk.CaptureException(new Exception(message));