Clean code, remove warning for future and spot
This commit is contained in:
@@ -2,183 +2,152 @@ using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Core.Exceptions;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Trades;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Trading.Handlers
|
||||
namespace Managing.Application.Trading.Handlers;
|
||||
|
||||
public class OpenSpotPositionCommandHandler(
|
||||
IExchangeService exchangeService,
|
||||
IAccountService accountService,
|
||||
ITradingService tradingService)
|
||||
: ICommandHandler<OpenSpotPositionRequest, Position>
|
||||
{
|
||||
public class OpenSpotPositionCommandHandler(
|
||||
IExchangeService exchangeService,
|
||||
IAccountService accountService,
|
||||
ITradingService tradingService)
|
||||
: ICommandHandler<OpenSpotPositionRequest, Position>
|
||||
public async Task<Position> Handle(OpenSpotPositionRequest request)
|
||||
{
|
||||
public async Task<Position> Handle(OpenSpotPositionRequest 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))
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
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
|
||||
: price;
|
||||
|
||||
// For spot trading, determine swap direction
|
||||
// Long: Swap USDC -> Token (buy token with USDC)
|
||||
// Short: Swap Token -> USDC (sell token for USDC)
|
||||
Ticker fromTicker;
|
||||
Ticker toTicker;
|
||||
double swapAmount;
|
||||
|
||||
if (request.Direction == TradeDirection.Long)
|
||||
{
|
||||
fromTicker = Ticker.USDC;
|
||||
toTicker = request.Ticker;
|
||||
swapAmount = (double)balanceToRisk;
|
||||
}
|
||||
else
|
||||
{
|
||||
fromTicker = request.Ticker;
|
||||
toTicker = Ticker.USDC;
|
||||
swapAmount = (double)quantity;
|
||||
}
|
||||
|
||||
// For backtest/paper trading, simulate the swap without calling the exchange
|
||||
SwapInfos swapResult;
|
||||
if (request.IsForPaperTrading)
|
||||
{
|
||||
// Simulate successful swap for backtest
|
||||
swapResult = new SwapInfos
|
||||
{
|
||||
Success = true,
|
||||
Hash = Guid.NewGuid().ToString(),
|
||||
Message = "Backtest spot position opened successfully"
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// For live trading, call SwapGmxTokensAsync
|
||||
swapResult = await tradingService.SwapGmxTokensAsync(
|
||||
request.User,
|
||||
request.AccountName,
|
||||
fromTicker,
|
||||
toTicker,
|
||||
swapAmount,
|
||||
"market",
|
||||
null,
|
||||
0.5);
|
||||
}
|
||||
|
||||
if (!swapResult.Success)
|
||||
{
|
||||
position.Status = PositionStatus.Rejected;
|
||||
throw new InvalidOperationException($"Failed to open spot position: {swapResult.Error ?? swapResult.Message}");
|
||||
}
|
||||
|
||||
// Build the opening trade
|
||||
var trade = exchangeService.BuildEmptyTrade(
|
||||
request.Ticker,
|
||||
openPrice,
|
||||
quantity,
|
||||
request.Direction,
|
||||
1, // Spot trading has no leverage
|
||||
TradeType.Market,
|
||||
request.Date,
|
||||
TradeStatus.Filled);
|
||||
|
||||
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;
|
||||
|
||||
// Determine SL/TP Prices
|
||||
var stopLossPrice = RiskHelpers.GetStopLossPrice(request.Direction, openPrice, request.MoneyManagement);
|
||||
var takeProfitPrice = RiskHelpers.GetTakeProfitPrice(request.Direction, openPrice, request.MoneyManagement);
|
||||
|
||||
// Stop loss
|
||||
position.StopLoss = exchangeService.BuildEmptyTrade(
|
||||
request.Ticker,
|
||||
stopLossPrice,
|
||||
position.Open.Quantity,
|
||||
closeDirection,
|
||||
1, // Spot trading has no leverage
|
||||
TradeType.StopLoss,
|
||||
request.Date,
|
||||
TradeStatus.Requested);
|
||||
|
||||
// Take profit
|
||||
position.TakeProfit1 = exchangeService.BuildEmptyTrade(
|
||||
request.Ticker,
|
||||
takeProfitPrice,
|
||||
quantity,
|
||||
closeDirection,
|
||||
1, // Spot trading has no 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;
|
||||
position.SignalIdentifier = request.SignalIdentifier;
|
||||
}
|
||||
|
||||
private static bool IsOpenTradeHandled(TradeStatus tradeStatus)
|
||||
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)
|
||||
{
|
||||
return tradeStatus == TradeStatus.Filled
|
||||
|| tradeStatus == TradeStatus.Requested;
|
||||
throw new Exception(
|
||||
$"Bot trading balance of {balanceToRisk} USD is less than the minimum {Constants.GMX.Config.MinimumPositionAmount} USD required to trade");
|
||||
}
|
||||
|
||||
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 ?? price
|
||||
: price;
|
||||
|
||||
SwapInfos swapResult;
|
||||
if (request.IsForPaperTrading)
|
||||
{
|
||||
// Simulate successful swap for backtest
|
||||
swapResult = new SwapInfos
|
||||
{
|
||||
Success = true,
|
||||
Hash = Guid.NewGuid().ToString(),
|
||||
Message = "Backtest spot position opened successfully"
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// For live trading, call SwapGmxTokensAsync
|
||||
swapResult = await tradingService.SwapGmxTokensAsync(
|
||||
request.User,
|
||||
request.AccountName,
|
||||
Ticker.USDC,
|
||||
request.Ticker,
|
||||
(double)balanceToRisk);
|
||||
}
|
||||
|
||||
if (!swapResult.Success)
|
||||
{
|
||||
position.Status = PositionStatus.Rejected;
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to open spot position: {swapResult.Error ?? swapResult.Message}");
|
||||
}
|
||||
|
||||
// Build the opening trade
|
||||
var trade = exchangeService.BuildEmptyTrade(
|
||||
request.Ticker,
|
||||
openPrice,
|
||||
quantity,
|
||||
request.Direction,
|
||||
1, // Spot trading has no leverage
|
||||
TradeType.Market,
|
||||
request.Date,
|
||||
TradeStatus.Filled);
|
||||
|
||||
position.Open = trade;
|
||||
|
||||
// Calculate and set fees for the position
|
||||
position.GasFees = TradingBox.CalculateOpeningGasFees();
|
||||
|
||||
// Set UI fees for opening
|
||||
var positionSizeUsd = position.Open.Quantity * position.Open.Price;
|
||||
position.UiFees = TradingBox.CalculateOpeningUiFees(positionSizeUsd);
|
||||
|
||||
// Determine SL/TP Prices
|
||||
var stopLossPrice = RiskHelpers.GetStopLossPrice(request.Direction, openPrice, request.MoneyManagement);
|
||||
var takeProfitPrice = RiskHelpers.GetTakeProfitPrice(request.Direction, openPrice, request.MoneyManagement);
|
||||
|
||||
// Stop loss
|
||||
position.StopLoss = exchangeService.BuildEmptyTrade(
|
||||
request.Ticker,
|
||||
stopLossPrice,
|
||||
position.Open.Quantity,
|
||||
TradeDirection.Short,
|
||||
1, // Spot trading has no leverage
|
||||
TradeType.StopLoss,
|
||||
request.Date,
|
||||
TradeStatus.Requested);
|
||||
|
||||
// Take profit
|
||||
position.TakeProfit1 = exchangeService.BuildEmptyTrade(
|
||||
request.Ticker,
|
||||
takeProfitPrice,
|
||||
quantity,
|
||||
TradeDirection.Short,
|
||||
1, // Spot trading has no 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 is TradeStatus.Filled or TradeStatus.Requested;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user