142 lines
5.7 KiB
C#
142 lines
5.7 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;
|
|
|
|
// Always use BotTradingBalance directly as the balance to risk
|
|
// Round to 2 decimal places to prevent precision errors
|
|
decimal balanceToRisk = Math.Round(request.AmountToTrade, 2, 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);
|
|
if (currentGasFees > Constants.GMX.Config.MaximumGasFeeUsd)
|
|
{
|
|
throw new InsufficientFundsException(
|
|
$"Gas fee too high for position opening: {currentGasFees:F2} USD (threshold: {Constants.GMX.Config.MaximumGasFeeUsd} 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);
|
|
|
|
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);
|
|
|
|
position.Open = trade;
|
|
|
|
// Calculate and set fees for the position
|
|
|
|
position.GasFees = TradingHelpers.CalculateOpeningGasFees();
|
|
|
|
// Set UI fees for opening
|
|
var positionSizeUsd = TradingHelpers.GetVolumeForPosition(position);
|
|
position.UiFees = TradingHelpers.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 (!request.IsForPaperTrading)
|
|
{
|
|
await tradingService.InsertPositionAsync(position);
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
private static bool IsOpenTradeHandled(TradeStatus tradeStatus)
|
|
{
|
|
return tradeStatus == TradeStatus.Filled
|
|
|| tradeStatus == TradeStatus.Requested;
|
|
}
|
|
}
|
|
} |