Add ETH and USDC balance check before start/restart bot and autoswap
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using Managing.Application.Bots.Models;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Trades;
|
||||
using static Managing.Common.Enums;
|
||||
@@ -36,12 +38,19 @@ public interface IBotService
|
||||
/// <param name="sortDirection">Sort direction ("Asc" or "Desc")</param>
|
||||
/// <returns>Tuple containing the bots for the current page and total count</returns>
|
||||
Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
|
||||
int pageNumber,
|
||||
int pageSize,
|
||||
BotStatus? status = null,
|
||||
string? name = null,
|
||||
string? ticker = null,
|
||||
string? agentName = null,
|
||||
string sortBy = "CreateDate",
|
||||
int pageNumber,
|
||||
int pageSize,
|
||||
BotStatus? status = null,
|
||||
string? name = null,
|
||||
string? ticker = null,
|
||||
string? agentName = null,
|
||||
string sortBy = "CreateDate",
|
||||
string sortDirection = "Desc");
|
||||
|
||||
/// <summary>
|
||||
/// Checks USDC and ETH balances for EVM/GMX V2 accounts
|
||||
/// </summary>
|
||||
/// <param name="account">The account to check balances for</param>
|
||||
/// <returns>Balance check result</returns>
|
||||
Task<BalanceCheckResult> CheckAccountBalancesAsync(Account account);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Bots.Models;
|
||||
using Managing.Application.Orleans;
|
||||
using Managing.Common;
|
||||
using Managing.Core.Exceptions;
|
||||
using Managing.Domain.Statistics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
@@ -237,184 +238,193 @@ public class AgentGrain : Grain, IAgentGrain
|
||||
|
||||
public async Task<BalanceCheckResult> CheckAndEnsureEthBalanceAsync(Guid requestingBotId, string accountName)
|
||||
{
|
||||
try
|
||||
// Check if a swap is already in progress
|
||||
if (_state.State.IsSwapInProgress)
|
||||
{
|
||||
// Check if a swap is already in progress
|
||||
if (_state.State.IsSwapInProgress)
|
||||
{
|
||||
_logger.LogInformation("Swap already in progress for agent {UserId}, bot {RequestingBotId} will wait",
|
||||
this.GetPrimaryKeyLong(), requestingBotId);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.SwapInProgress,
|
||||
Message = "Swap operation already in progress",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
|
||||
// Check cooldown period (5 minutes between swaps)
|
||||
if (_state.State.LastSwapTime.HasValue &&
|
||||
DateTime.UtcNow - _state.State.LastSwapTime.Value < TimeSpan.FromMinutes(5))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Swap cooldown period active for agent {UserId}, bot {RequestingBotId} will wait",
|
||||
this.GetPrimaryKeyLong(), requestingBotId);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.SwapCooldownActive,
|
||||
Message = "Swap cooldown period active",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
|
||||
// Get or refresh cached balance data
|
||||
var balanceData = await GetOrRefreshBalanceDataAsync(accountName);
|
||||
if (balanceData == null)
|
||||
{
|
||||
_logger.LogError("Failed to get balance data for account {AccountName}, user {UserId}",
|
||||
accountName, this.GetPrimaryKeyLong());
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.BalanceFetchError,
|
||||
Message = "Failed to fetch balance data",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Agent {UserId} balance check - ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD (cached: {IsCached})",
|
||||
this.GetPrimaryKeyLong(), balanceData.EthValueInUsd, balanceData.UsdcValue,
|
||||
_state.State.CachedBalanceData?.IsValid == true);
|
||||
|
||||
// Check USDC minimum balance first (this will stop the bot if insufficient)
|
||||
if (balanceData.UsdcValue < Constants.GMX.Config.MinimumPositionAmount)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"USDC balance is below minimum required amount - ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD (minimum: {Minimum})",
|
||||
balanceData.EthValueInUsd, balanceData.UsdcValue, Constants.GMX.Config.MinimumPositionAmount);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.InsufficientUsdcBelowMinimum,
|
||||
Message =
|
||||
$"USDC balance below minimum required amount ({Constants.GMX.Config.MinimumPositionAmount} USD)",
|
||||
ShouldStopBot = true
|
||||
};
|
||||
}
|
||||
|
||||
// If ETH balance is sufficient, return success
|
||||
if (balanceData.EthValueInUsd >= Constants.GMX.Config.MinimumEthBalance)
|
||||
{
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = true,
|
||||
FailureReason = BalanceCheckFailureReason.None,
|
||||
Message = "Balance check successful",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
|
||||
// Check if we have enough USDC for swap (need at least 5 USD for swap)
|
||||
if (balanceData.UsdcValue <
|
||||
(Constants.GMX.Config.MinimumPositionAmount + (decimal)Constants.GMX.Config.AutoSwapAmount))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Insufficient USDC balance for swap - ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD (need {AutoSwapAmount} USD for swap)",
|
||||
balanceData.EthValueInUsd, balanceData.UsdcValue, Constants.GMX.Config.AutoSwapAmount);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.InsufficientUsdcForSwap,
|
||||
Message = $"Insufficient USDC balance for swap (need {Constants.GMX.Config.AutoSwapAmount} USD)",
|
||||
ShouldStopBot = true
|
||||
};
|
||||
}
|
||||
|
||||
// Mark swap as in progress
|
||||
_state.State.IsSwapInProgress = true;
|
||||
await _state.WriteStateAsync();
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Initiating USDC to ETH swap for agent {UserId} - swapping 5 USDC",
|
||||
this.GetPrimaryKeyLong());
|
||||
|
||||
// Get user for the swap
|
||||
var userId = (int)this.GetPrimaryKeyLong();
|
||||
var user = await _userService.GetUserByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogError("User {UserId} not found for swap operation", userId);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.BalanceFetchError,
|
||||
Message = "User not found for swap operation",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
|
||||
// Perform the swap
|
||||
var swapInfo = await _accountService.SwapGmxTokensAsync(user, accountName,
|
||||
Ticker.USDC, Ticker.ETH, Constants.GMX.Config.AutoSwapAmount);
|
||||
|
||||
if (swapInfo.Success)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Successfully swapped 5 USDC to ETH for agent {UserId}, transaction hash: {Hash}",
|
||||
userId, swapInfo.Hash);
|
||||
|
||||
// Update last swap time and invalidate cache
|
||||
_state.State.LastSwapTime = DateTime.UtcNow;
|
||||
_state.State.CachedBalanceData = null; // Invalidate cache after successful swap
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = true,
|
||||
FailureReason = BalanceCheckFailureReason.None,
|
||||
Message = "Swap completed successfully",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Failed to swap USDC to ETH for agent {UserId}: {Error}",
|
||||
userId, swapInfo.Error ?? swapInfo.Message);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.SwapExecutionError,
|
||||
Message = swapInfo.Error ?? swapInfo.Message ?? "Swap execution failed",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Always clear the swap in progress flag
|
||||
_state.State.IsSwapInProgress = false;
|
||||
await _state.WriteStateAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error checking/ensuring ETH balance for agent {UserId}, bot {RequestingBotId}",
|
||||
_logger.LogInformation("Swap already in progress for agent {UserId}, bot {RequestingBotId} will wait",
|
||||
this.GetPrimaryKeyLong(), requestingBotId);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.SwapInProgress,
|
||||
Message = "Swap operation already in progress",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
|
||||
// Clear swap in progress flag on error
|
||||
_state.State.IsSwapInProgress = false;
|
||||
await _state.WriteStateAsync();
|
||||
// Check cooldown period (5 minutes between swaps)
|
||||
if (_state.State.LastSwapTime.HasValue &&
|
||||
DateTime.UtcNow - _state.State.LastSwapTime.Value < TimeSpan.FromMinutes(5))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Swap cooldown period active for agent {UserId}, bot {RequestingBotId} will wait",
|
||||
this.GetPrimaryKeyLong(), requestingBotId);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.SwapCooldownActive,
|
||||
Message = "Swap cooldown period active",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
|
||||
// Get or refresh cached balance data
|
||||
var balanceData = await GetOrRefreshBalanceDataAsync(accountName);
|
||||
if (balanceData == null)
|
||||
{
|
||||
_logger.LogError("Failed to get balance data for account {AccountName}, user {UserId}",
|
||||
accountName, this.GetPrimaryKeyLong());
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.BalanceFetchError,
|
||||
Message = ex.Message,
|
||||
Message = "Failed to fetch balance data",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Agent {UserId} balance check - ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD (cached: {IsCached})",
|
||||
this.GetPrimaryKeyLong(), balanceData.EthValueInUsd, balanceData.UsdcValue,
|
||||
_state.State.CachedBalanceData?.IsValid == true);
|
||||
|
||||
// Check USDC minimum balance first (this will stop the bot if insufficient)
|
||||
if (balanceData.UsdcValue < Constants.GMX.Config.MinimumPositionAmount)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"USDC balance is below minimum required amount - ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD (minimum: {Minimum})",
|
||||
balanceData.EthValueInUsd, balanceData.UsdcValue, Constants.GMX.Config.MinimumPositionAmount);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.InsufficientUsdcBelowMinimum,
|
||||
Message =
|
||||
$"USDC balance below minimum required amount ({Constants.GMX.Config.MinimumPositionAmount} USD)",
|
||||
ShouldStopBot = true
|
||||
};
|
||||
}
|
||||
|
||||
// If ETH balance is sufficient, return success
|
||||
if (balanceData.EthValueInUsd >= Constants.GMX.Config.MinimumTradeEthBalanceUsd)
|
||||
{
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = true,
|
||||
FailureReason = BalanceCheckFailureReason.None,
|
||||
Message = "Balance check successful - Enough ETH balance for trading",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
|
||||
if (balanceData.EthValueInUsd < Constants.GMX.Config.MinimumSwapEthBalanceUsd)
|
||||
{
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.InsufficientEthBelowMinimum,
|
||||
Message = "ETH balance below minimum required amount",
|
||||
ShouldStopBot = true
|
||||
};
|
||||
}
|
||||
|
||||
// Check if we have enough USDC for swap (need at least 5 USD for swap)
|
||||
if (balanceData.UsdcValue <
|
||||
(Constants.GMX.Config.MinimumPositionAmount + (decimal)Constants.GMX.Config.AutoSwapAmount))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Insufficient USDC balance for swap - ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD (need {AutoSwapAmount} USD for swap)",
|
||||
balanceData.EthValueInUsd, balanceData.UsdcValue, Constants.GMX.Config.AutoSwapAmount);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.InsufficientUsdcForSwap,
|
||||
Message = $"Insufficient USDC balance for swap (need {Constants.GMX.Config.AutoSwapAmount} USD)",
|
||||
ShouldStopBot = true
|
||||
};
|
||||
}
|
||||
|
||||
// Mark swap as in progress
|
||||
_state.State.IsSwapInProgress = true;
|
||||
await _state.WriteStateAsync();
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Initiating USDC to ETH swap for agent {UserId} - swapping 5 USDC",
|
||||
this.GetPrimaryKeyLong());
|
||||
|
||||
// Get user for the swap
|
||||
var userId = (int)this.GetPrimaryKeyLong();
|
||||
var user = await _userService.GetUserByIdAsync(userId);
|
||||
|
||||
// Perform the swap
|
||||
var swapInfo = await _accountService.SwapGmxTokensAsync(user, accountName,
|
||||
Ticker.USDC, Ticker.ETH, Constants.GMX.Config.AutoSwapAmount);
|
||||
|
||||
if (swapInfo.Success)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Successfully swapped 5 USDC to ETH for agent {UserId}, transaction hash: {Hash}",
|
||||
userId, swapInfo.Hash);
|
||||
|
||||
// Update last swap time and invalidate cache
|
||||
_state.State.LastSwapTime = DateTime.UtcNow;
|
||||
_state.State.CachedBalanceData = null; // Invalidate cache after successful swap
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = true,
|
||||
FailureReason = BalanceCheckFailureReason.None,
|
||||
Message = "Swap completed successfully",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Failed to swap USDC to ETH for agent {UserId}: {Error}",
|
||||
userId, swapInfo.Error ?? swapInfo.Message);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.SwapExecutionError,
|
||||
Message = swapInfo.Error ?? swapInfo.Message ?? "Swap execution failed",
|
||||
ShouldStopBot = true
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.InnerException is InsufficientFundsException insufficientFundsEx)
|
||||
{
|
||||
// Handle allowance exception (insufficient ETH for gas fees)
|
||||
_logger.LogError(insufficientFundsEx,
|
||||
"Insufficient funds during autoswap for agent {UserId}: {ErrorMessage}",
|
||||
this.GetPrimaryKeyLong(), insufficientFundsEx.Message);
|
||||
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.SwapExecutionError,
|
||||
Message = insufficientFundsEx.UserMessage ??
|
||||
"Insufficient ETH for gas fees during autoswap. Bot cannot continue trading.",
|
||||
ShouldStopBot = true
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during autoswap for agent {UserId}, bot {RequestingBotId}",
|
||||
this.GetPrimaryKeyLong(), requestingBotId);
|
||||
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.SwapExecutionError,
|
||||
Message = ex.Message,
|
||||
ShouldStopBot = true
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Always clear the swap in progress flag
|
||||
_state.State.IsSwapInProgress = false;
|
||||
await _state.WriteStateAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,94 +1,110 @@
|
||||
#nullable enable
|
||||
namespace Managing.Application.Bots.Models
|
||||
{
|
||||
[GenerateSerializer]
|
||||
public class AgentGrainState
|
||||
{
|
||||
public string AgentName { get; set; }
|
||||
public HashSet<Guid> BotIds { get; set; } = new HashSet<Guid>();
|
||||
|
||||
[Id(0)] public string AgentName { get; set; } = string.Empty;
|
||||
[Id(1)] public HashSet<Guid> BotIds { get; set; } = new HashSet<Guid>();
|
||||
|
||||
/// <summary>
|
||||
/// Tracks if a swap operation is currently in progress to prevent multiple simultaneous swaps
|
||||
/// </summary>
|
||||
[Id(2)]
|
||||
public bool IsSwapInProgress { get; set; } = false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of the last swap operation to implement cooldown period
|
||||
/// </summary>
|
||||
[Id(3)]
|
||||
public DateTime? LastSwapTime { get; set; } = null;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Cached balance data to reduce external API calls
|
||||
/// </summary>
|
||||
[Id(4)]
|
||||
public CachedBalanceData? CachedBalanceData { get; set; } = null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Cached balance data to avoid repeated external API calls
|
||||
/// </summary>
|
||||
[GenerateSerializer]
|
||||
public class CachedBalanceData
|
||||
{
|
||||
/// <summary>
|
||||
/// When the balance was last fetched
|
||||
/// </summary>
|
||||
[Id(0)]
|
||||
public DateTime LastFetched { get; set; } = DateTime.UtcNow;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The account name this balance data is for
|
||||
/// </summary>
|
||||
[Id(1)]
|
||||
public string AccountName { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// ETH balance in USD
|
||||
/// </summary>
|
||||
[Id(2)]
|
||||
public decimal EthValueInUsd { get; set; } = 0;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// USDC balance value
|
||||
/// </summary>
|
||||
[Id(3)]
|
||||
public decimal UsdcValue { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the cached data is still valid (less than 1 minute old)
|
||||
/// </summary>
|
||||
public bool IsValid => DateTime.UtcNow - LastFetched < TimeSpan.FromMinutes(1.5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a balance check operation
|
||||
/// </summary>
|
||||
public class BalanceCheckResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the balance check was successful
|
||||
/// </summary>
|
||||
public bool IsSuccessful { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The reason for failure if not successful
|
||||
/// </summary>
|
||||
public BalanceCheckFailureReason FailureReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional details about the result
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the bot should stop due to this result
|
||||
/// </summary>
|
||||
public bool ShouldStopBot { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Whether the cached data is still valid (less than 1 minute old)
|
||||
/// </summary>
|
||||
public bool IsValid => DateTime.UtcNow - LastFetched < TimeSpan.FromMinutes(1.5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reasons why a balance check might fail
|
||||
/// </summary>
|
||||
public enum BalanceCheckFailureReason
|
||||
{
|
||||
None,
|
||||
InsufficientUsdcBelowMinimum,
|
||||
InsufficientUsdcForSwap,
|
||||
SwapInProgress,
|
||||
SwapCooldownActive,
|
||||
BalanceFetchError,
|
||||
SwapExecutionError
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Result of a balance check operation
|
||||
/// </summary>
|
||||
[GenerateSerializer]
|
||||
public class BalanceCheckResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the balance check was successful
|
||||
/// </summary>
|
||||
[Id(0)]
|
||||
public bool IsSuccessful { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The reason for failure if not successful
|
||||
/// </summary>
|
||||
[Id(1)]
|
||||
public BalanceCheckFailureReason FailureReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional details about the result
|
||||
/// </summary>
|
||||
[Id(2)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the bot should stop due to this result
|
||||
/// </summary>
|
||||
[Id(3)]
|
||||
public bool ShouldStopBot { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reasons why a balance check might fail
|
||||
/// </summary>
|
||||
public enum BalanceCheckFailureReason
|
||||
{
|
||||
None,
|
||||
InsufficientUsdcBelowMinimum,
|
||||
InsufficientUsdcForSwap,
|
||||
SwapInProgress,
|
||||
SwapCooldownActive,
|
||||
BalanceFetchError,
|
||||
SwapExecutionError,
|
||||
InsufficientEthBelowMinimum
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using Managing.Application.Trading.Commands;
|
||||
using Managing.Application.Trading.Handlers;
|
||||
using Managing.Common;
|
||||
using Managing.Core;
|
||||
using Managing.Core.Exceptions;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
@@ -822,6 +823,17 @@ public class TradingBotBase : ITradingBot
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (InsufficientFundsException ex)
|
||||
{
|
||||
// Handle insufficient funds errors with user-friendly messaging
|
||||
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
||||
await LogWarning(ex.UserMessage);
|
||||
|
||||
// Log the technical details for debugging
|
||||
Logger.LogError(ex, "Insufficient funds error for signal {SignalId}: {ErrorMessage}", signal.Identifier, ex.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
||||
|
||||
@@ -3,7 +3,10 @@ using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Bots;
|
||||
using Managing.Application.Bots.Models;
|
||||
using Managing.Common;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
@@ -103,12 +106,31 @@ namespace Managing.Application.ManageBot
|
||||
}
|
||||
|
||||
var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||
|
||||
// Check balances for EVM/GMX V2 bots before starting/restarting
|
||||
var botConfig = await botGrain.GetConfiguration();
|
||||
var account = await ServiceScopeHelpers.WithScopedService<IAccountService, Account>(
|
||||
_scopeFactory,
|
||||
async accountService => await accountService.GetAccount(botConfig.AccountName, true, false));
|
||||
|
||||
if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2)
|
||||
{
|
||||
var balanceCheckResult = await CheckAccountBalancesAsync(account);
|
||||
if (!balanceCheckResult.IsSuccessful)
|
||||
{
|
||||
_tradingBotLogger.LogWarning(
|
||||
"Bot {Identifier} restart blocked due to insufficient balances: {Message}",
|
||||
identifier, balanceCheckResult.Message);
|
||||
throw new InvalidOperationException(balanceCheckResult.Message);
|
||||
}
|
||||
}
|
||||
|
||||
var grainState = await botGrain.GetBotDataAsync();
|
||||
|
||||
if (previousStatus == BotStatus.Saved)
|
||||
{
|
||||
// First time startup
|
||||
await botGrain.StartAsync();
|
||||
var grainState = await botGrain.GetBotDataAsync();
|
||||
var account = await botGrain.GetAccount();
|
||||
var startupMessage = $"🚀 **Bot Started**\n\n" +
|
||||
$"🎯 **Agent:** {account.User.AgentName}\n" +
|
||||
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
|
||||
@@ -122,8 +144,6 @@ namespace Managing.Application.ManageBot
|
||||
{
|
||||
// Restart (bot was previously down)
|
||||
await botGrain.RestartAsync();
|
||||
var grainState = await botGrain.GetBotDataAsync();
|
||||
var account = await botGrain.GetAccount();
|
||||
var restartMessage = $"🔄 **Bot Restarted**\n\n" +
|
||||
$"🎯 **Agent:** {account.User.AgentName}\n" +
|
||||
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
|
||||
@@ -138,8 +158,8 @@ namespace Managing.Application.ManageBot
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_tradingBotLogger.LogError(e, "Error restarting bot {Identifier}", identifier);
|
||||
return BotStatus.Stopped;
|
||||
SentrySdk.CaptureException(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +316,6 @@ namespace Managing.Application.ManageBot
|
||||
|
||||
var existingBot = await _botRepository.GetBotByIdentifierAsync(bot.Identifier);
|
||||
|
||||
|
||||
|
||||
// Check if bot already exists in database
|
||||
await ServiceScopeHelpers.WithScopedService<IBotRepository>(
|
||||
@@ -313,13 +332,13 @@ namespace Managing.Application.ManageBot
|
||||
"Created new bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
|
||||
bot.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
|
||||
}
|
||||
else if (existingBot.Status != bot.Status
|
||||
|| existingBot.Pnl != Math.Round(bot.Pnl, 8)
|
||||
|| existingBot.Roi != Math.Round(bot.Roi, 8)
|
||||
|| existingBot.Volume != Math.Round(bot.Volume, 8)
|
||||
|| existingBot.Fees != Math.Round(bot.Fees, 8)
|
||||
|| existingBot.LongPositionCount != bot.LongPositionCount
|
||||
|| existingBot.ShortPositionCount != bot.ShortPositionCount)
|
||||
else if (existingBot.Status != bot.Status
|
||||
|| existingBot.Pnl != Math.Round(bot.Pnl, 8)
|
||||
|| existingBot.Roi != Math.Round(bot.Roi, 8)
|
||||
|| existingBot.Volume != Math.Round(bot.Volume, 8)
|
||||
|| existingBot.Fees != Math.Round(bot.Fees, 8)
|
||||
|| existingBot.LongPositionCount != bot.LongPositionCount
|
||||
|| existingBot.ShortPositionCount != bot.ShortPositionCount)
|
||||
{
|
||||
_tradingBotLogger.LogInformation("Update bot statistics for bot {BotId}",
|
||||
bot.Identifier);
|
||||
@@ -365,5 +384,79 @@ namespace Managing.Application.ManageBot
|
||||
sortDirection);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks USDC and ETH balances for EVM/GMX V2 accounts
|
||||
/// </summary>
|
||||
/// <param name="account">The account to check balances for</param>
|
||||
/// <returns>Balance check result</returns>
|
||||
public async Task<BalanceCheckResult> CheckAccountBalancesAsync(Account account)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await ServiceScopeHelpers
|
||||
.WithScopedServices<IExchangeService, IAccountService, BalanceCheckResult>(
|
||||
_scopeFactory,
|
||||
async (exchangeService, accountService) =>
|
||||
{
|
||||
// Get current balances
|
||||
var balances = await exchangeService.GetBalances(account);
|
||||
var ethBalance = balances.FirstOrDefault(b => b.TokenName?.ToUpper() == "ETH");
|
||||
var usdcBalance = balances.FirstOrDefault(b => b.TokenName?.ToUpper() == "USDC");
|
||||
|
||||
var ethValueInUsd = ethBalance?.Amount * ethBalance?.Price ?? 0;
|
||||
var usdcValue = usdcBalance?.Value ?? 0;
|
||||
|
||||
_tradingBotLogger.LogInformation(
|
||||
"Balance check for bot restart - Account: {AccountName}, ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD",
|
||||
account.Name, ethValueInUsd, usdcValue);
|
||||
|
||||
// Check USDC minimum balance
|
||||
if (usdcValue < Constants.GMX.Config.MinimumPositionAmount)
|
||||
{
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.InsufficientUsdcBelowMinimum,
|
||||
Message =
|
||||
$"USDC balance ({usdcValue:F2} USD) is below minimum required amount ({Constants.GMX.Config.MinimumPositionAmount} USD). Please add more USDC to restart the bot.",
|
||||
ShouldStopBot = true
|
||||
};
|
||||
}
|
||||
|
||||
// Check ETH minimum balance for trading
|
||||
if (ethValueInUsd < Constants.GMX.Config.MinimumTradeEthBalanceUsd)
|
||||
{
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.InsufficientEthBelowMinimum,
|
||||
Message =
|
||||
$"ETH balance ({ethValueInUsd:F2} USD) is below minimum required amount ({Constants.GMX.Config.MinimumTradeEthBalanceUsd} USD) for trading. Please add more ETH to restart the bot.",
|
||||
ShouldStopBot = true
|
||||
};
|
||||
}
|
||||
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = true,
|
||||
FailureReason = BalanceCheckFailureReason.None,
|
||||
Message = "Balance check successful - Sufficient USDC and ETH balances",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_tradingBotLogger.LogError(ex, "Error checking balances for account {AccountName}", account.Name);
|
||||
return new BalanceCheckResult
|
||||
{
|
||||
IsSuccessful = false,
|
||||
FailureReason = BalanceCheckFailureReason.BalanceFetchError,
|
||||
Message = $"Failed to check balances: {ex.Message}",
|
||||
ShouldStopBot = false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.ManageBot.Commands;
|
||||
using Managing.Common;
|
||||
@@ -11,12 +12,14 @@ namespace Managing.Application.ManageBot
|
||||
{
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IGrainFactory _grainFactory;
|
||||
private readonly IBotService _botService;
|
||||
|
||||
public StartBotCommandHandler(
|
||||
IAccountService accountService, IGrainFactory grainFactory)
|
||||
IAccountService accountService, IGrainFactory grainFactory, IBotService botService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_grainFactory = grainFactory;
|
||||
_botService = botService;
|
||||
}
|
||||
|
||||
public async Task<BotStatus> Handle(StartBotCommand request, CancellationToken cancellationToken)
|
||||
@@ -39,20 +42,34 @@ namespace Managing.Application.ManageBot
|
||||
$"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
|
||||
}
|
||||
|
||||
var account = await _accountService.GetAccount(request.Config.AccountName, true, true);
|
||||
var account = await _accountService.GetAccount(request.Config.AccountName, true, false);
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
throw new Exception($"Account {request.Config.AccountName} not found");
|
||||
}
|
||||
|
||||
var usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString());
|
||||
|
||||
if (usdcBalance == null ||
|
||||
usdcBalance.Value < Constants.GMX.Config.MinimumPositionAmount ||
|
||||
usdcBalance.Value < request.Config.BotTradingBalance)
|
||||
// Check balances for EVM/GMX V2 accounts before starting
|
||||
if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2)
|
||||
{
|
||||
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
|
||||
var balanceCheckResult = await _botService.CheckAccountBalancesAsync(account);
|
||||
if (!balanceCheckResult.IsSuccessful)
|
||||
{
|
||||
throw new InvalidOperationException(balanceCheckResult.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// For other exchanges, keep the original USDC balance check
|
||||
if (account.Exchange != TradingExchanges.Evm && account.Exchange != TradingExchanges.GmxV2)
|
||||
{
|
||||
var usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString());
|
||||
|
||||
if (usdcBalance == null ||
|
||||
usdcBalance.Value < Constants.GMX.Config.MinimumPositionAmount ||
|
||||
usdcBalance.Value < request.Config.BotTradingBalance)
|
||||
{
|
||||
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Core.Exceptions;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Trades;
|
||||
using static Managing.Common.Enums;
|
||||
@@ -19,7 +20,6 @@ namespace Managing.Application.Trading.Handlers
|
||||
{
|
||||
var account = await accountService.GetAccount(request.AccountName, hideSecrets: false, getBalance: false);
|
||||
|
||||
|
||||
var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator;
|
||||
var position = new Position(Guid.NewGuid(), request.AccountName, request.Direction,
|
||||
request.Ticker,
|
||||
@@ -44,6 +44,18 @@ namespace Managing.Application.Trading.Handlers
|
||||
$"Bot trading balance of {balanceToRisk} USD is less than the minimum {Constants.GMX.Config.MinimumPositionAmount} USD required to trade");
|
||||
}
|
||||
|
||||
// Gas fee check for EVM exchanges
|
||||
if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2)
|
||||
{
|
||||
var gasFeeUsd = await exchangeService.GetFee(account);
|
||||
if (gasFeeUsd > Constants.GMX.Config.MaximumGasFeeUsd)
|
||||
{
|
||||
throw new InsufficientFundsException(
|
||||
$"Gas fee too high for position opening: {gasFeeUsd:F2} USD (threshold: {Constants.GMX.Config.MaximumGasFeeUsd} USD). Position opening cancelled.",
|
||||
InsufficientFundsType.InsufficientEth);
|
||||
}
|
||||
}
|
||||
|
||||
var price = request.IsForPaperTrading && request.Price.HasValue
|
||||
? request.Price.Value
|
||||
: await exchangeService.GetPrice(account, request.Ticker, DateTime.Now);
|
||||
|
||||
Reference in New Issue
Block a user