Files
managing-apps/src/Managing.Application/Trading/Handlers/OpenPositionCommandHandler.cs
cryptooda 5e7b2b34d4 Refactor ETH balance and gas fee checks in SpotBot
- Updated balance checks to utilize user-defined thresholds for minimum trading and swap balances, enhancing flexibility.
- Improved gas fee validation by incorporating user settings, allowing for more personalized transaction management.
- Enhanced logging to provide clearer messages regarding balance sufficiency and gas fee limits, improving user feedback during operations.
2026-01-06 00:43:51 +07:00

157 lines
6.4 KiB
C#

using Managing.Application.Abstractions;
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;
namespace Managing.Application.Trading.Handlers
{
public class OpenPositionCommandHandler(
IExchangeService exchangeService,
IAccountService accountService,
ITradingService tradingService,
IGrainFactory grainFactory = null)
: ICommandHandler<OpenPositionRequest, Position>
{
public async Task<Position> Handle(OpenPositionRequest request)
{
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(), account.Id, request.Direction,
request.Ticker,
request.MoneyManagement,
initiator, request.Date, request.User);
if (!string.IsNullOrEmpty(request.SignalIdentifier))
{
position.SignalIdentifier = request.SignalIdentifier;
}
position.InitiatorIdentifier = request.InitiatorIdentifier;
position.TradingType = request.TradingType;
// Always use BotTradingBalance directly as the balance to risk
// Round to 2 decimal places to prevent precision errors
decimal balanceToRisk = Math.Round(request.AmountToTrade, 0, MidpointRounding.ToZero);
// Minimum check
if (balanceToRisk < Constants.GMX.Config.MinimumPositionAmount)
{
throw new Exception(
$"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 (!request.IsForPaperTrading)
{
if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2)
{
var currentGasFees = await exchangeService.GetFee(account);
// Use user's max gas fee setting, fallback to default constant
var maxGasFeeThreshold = request.User.MaxTxnGasFeePerPosition ?? Constants.GMX.Config.MaximumGasFeeUsd;
if (currentGasFees > maxGasFeeThreshold)
{
throw new InsufficientFundsException(
$"Gas fee too high for position opening: {currentGasFees:F2} USD (threshold: {maxGasFeeThreshold:F2} USD). Position opening rejected.",
InsufficientFundsType.HighNetworkFee);
}
}
}
var price = request.IsForPaperTrading && request.Price.HasValue
? request.Price.Value
: await exchangeService.GetPrice(account, request.Ticker, DateTime.Now);
var quantity = balanceToRisk / price;
var openPrice = request.IsForPaperTrading || request.Price.HasValue
? request.Price.Value
: await exchangeService.GetBestPrice(account, request.Ticker, price, quantity, request.Direction);
// Determine SL/TP Prices
var stopLossPrice = RiskHelpers.GetStopLossPrice(request.Direction, openPrice, request.MoneyManagement);
var takeProfitPrice = RiskHelpers.GetTakeProfitPrice(request.Direction, openPrice, request.MoneyManagement);
// Get user's slippage setting from IndicatorComboConfig
var config = TradingBox.CreateConfigFromUserSettings(request.User);
var allowedSlippage = request.User.GmxSlippage ?? config.GmxSlippage;
var trade = await exchangeService.OpenTrade(
account,
request.Ticker,
request.Direction,
openPrice,
quantity,
request.MoneyManagement.Leverage,
TradeType.Limit,
isForPaperTrading: request.IsForPaperTrading,
currentDate: request.Date,
stopLossPrice: stopLossPrice,
takeProfitPrice: takeProfitPrice,
allowedSlippage: allowedSlippage);
position.Open = trade;
// Calculate and set fees for the position
position.GasFees = TradingBox.CalculateOpeningGasFees();
// Set UI fees for opening
var positionSizeUsd = TradingBox.GetVolumeForPosition(position);
position.UiFees = TradingBox.CalculateOpeningUiFees(positionSizeUsd);
var closeDirection = request.Direction == TradeDirection.Long
? TradeDirection.Short
: TradeDirection.Long;
// Stop loss
position.StopLoss = exchangeService.BuildEmptyTrade(
request.Ticker,
stopLossPrice,
position.Open.Quantity,
closeDirection,
request.MoneyManagement.Leverage,
TradeType.StopLoss,
request.Date,
TradeStatus.Requested);
// Take profit
position.TakeProfit1 = exchangeService.BuildEmptyTrade(
request.Ticker,
takeProfitPrice,
quantity,
closeDirection,
request.MoneyManagement.Leverage,
TradeType.TakeProfit,
request.Date,
TradeStatus.Requested);
position.Status = IsOpenTradeHandled(position.Open.Status)
? position.Status
: PositionStatus.Rejected;
if (position.Status == PositionStatus.Rejected)
{
SentrySdk.CaptureException(
new Exception($"Position {position.Identifier} for {request.SignalIdentifier} rejected"));
}
if (!request.IsForPaperTrading)
{
await tradingService.InsertPositionAsync(position);
}
return position;
}
private static bool IsOpenTradeHandled(TradeStatus tradeStatus)
{
return tradeStatus == TradeStatus.Filled
|| tradeStatus == TradeStatus.Requested;
}
}
}