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));