Refactoring TradingBotBase.cs + clean architecture (#38)
* Refactoring TradingBotBase.cs + clean architecture * Fix basic tests * Fix tests * Fix workers * Fix open positions * Fix closing position stucking the grain * Fix comments * Refactor candle handling to use IReadOnlyList for chronological order preservation across various components
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
using Managing.Domain.Trades;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.Trading.Commands
|
||||
{
|
||||
public class CloseBacktestFuturesPositionCommand : IRequest<Position>
|
||||
{
|
||||
public CloseBacktestFuturesPositionCommand(Position position, int accountId, decimal? executionPrice = null)
|
||||
{
|
||||
Position = position;
|
||||
AccountId = accountId;
|
||||
ExecutionPrice = executionPrice;
|
||||
}
|
||||
|
||||
public Position Position { get; }
|
||||
public int AccountId { get; }
|
||||
public decimal? ExecutionPrice { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Managing.Domain.Trades;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.Trading.Commands
|
||||
{
|
||||
public class CloseFuturesPositionCommand : IRequest<Position>
|
||||
{
|
||||
public CloseFuturesPositionCommand(Position position, int accountId, decimal? executionPrice = null)
|
||||
{
|
||||
Position = position;
|
||||
AccountId = accountId;
|
||||
ExecutionPrice = executionPrice;
|
||||
}
|
||||
|
||||
public Position Position { get; }
|
||||
public int AccountId { get; }
|
||||
public decimal? ExecutionPrice { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace Managing.Application.Trading.Commands
|
||||
bool isForPaperTrading = false,
|
||||
decimal? price = null,
|
||||
string signalIdentifier = null,
|
||||
Guid? initiatorIdentifier = null)
|
||||
Guid? initiatorIdentifier = null,
|
||||
TradingType tradingType = TradingType.Futures)
|
||||
{
|
||||
AccountName = accountName;
|
||||
MoneyManagement = moneyManagement;
|
||||
@@ -43,6 +44,7 @@ namespace Managing.Application.Trading.Commands
|
||||
InitiatorIdentifier = initiatorIdentifier ??
|
||||
throw new ArgumentNullException(nameof(initiatorIdentifier),
|
||||
"InitiatorIdentifier is required");
|
||||
TradingType = tradingType;
|
||||
}
|
||||
|
||||
public string SignalIdentifier { get; set; }
|
||||
@@ -57,5 +59,6 @@ namespace Managing.Application.Trading.Commands
|
||||
public PositionInitiator Initiator { get; }
|
||||
public User User { get; }
|
||||
public Guid InitiatorIdentifier { get; }
|
||||
public TradingType TradingType { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Trades;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Trading.Handlers;
|
||||
|
||||
public class CloseBacktestFuturesPositionCommandHandler(
|
||||
IExchangeService exchangeService,
|
||||
IAccountService accountService,
|
||||
ITradingService tradingService,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<CloseBacktestFuturesPositionCommandHandler> logger = null)
|
||||
: ICommandHandler<CloseBacktestFuturesPositionCommand, Position>
|
||||
{
|
||||
public async Task<Position> Handle(CloseBacktestFuturesPositionCommand request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// For backtest, use execution price directly
|
||||
var lastPrice = request.ExecutionPrice.GetValueOrDefault();
|
||||
|
||||
// Calculate closing direction (opposite of opening direction)
|
||||
var direction = request.Position.OriginDirection == TradeDirection.Long
|
||||
? TradeDirection.Short
|
||||
: TradeDirection.Long;
|
||||
|
||||
// Build the closing trade directly for backtest (no exchange call needed)
|
||||
var closedTrade = exchangeService.BuildEmptyTrade(
|
||||
request.Position.Open.Ticker,
|
||||
lastPrice,
|
||||
request.Position.Open.Quantity,
|
||||
direction,
|
||||
request.Position.Open.Leverage,
|
||||
TradeType.Market,
|
||||
request.Position.Open.Date,
|
||||
TradeStatus.Filled);
|
||||
|
||||
// Update position status and calculate PnL
|
||||
request.Position.Status = PositionStatus.Finished;
|
||||
request.Position.ProfitAndLoss =
|
||||
TradingBox.GetProfitAndLoss(request.Position, closedTrade.Quantity, lastPrice,
|
||||
request.Position.Open.Leverage);
|
||||
|
||||
// Add UI fees for closing the position
|
||||
var closingPositionSizeUsd = (lastPrice * closedTrade.Quantity) * request.Position.Open.Leverage;
|
||||
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||
request.Position.AddUiFees(closingUiFees);
|
||||
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||
|
||||
// For backtest, skip database update
|
||||
|
||||
return request.Position;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Error closing backtest futures position: {Message} \n Stacktrace : {StackTrace}", ex.Message,
|
||||
ex.StackTrace);
|
||||
|
||||
SentrySdk.CaptureException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Trades;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Trading.Handlers;
|
||||
|
||||
public class CloseFuturesPositionCommandHandler(
|
||||
IExchangeService exchangeService,
|
||||
IAccountService accountService,
|
||||
ITradingService tradingService,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<CloseFuturesPositionCommandHandler> logger = null)
|
||||
: ICommandHandler<CloseFuturesPositionCommand, Position>
|
||||
{
|
||||
public async Task<Position> Handle(CloseFuturesPositionCommand request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (request.Position == null)
|
||||
{
|
||||
logger?.LogWarning("Attempted to close position but position is null for account {AccountId}", request.AccountId);
|
||||
throw new ArgumentNullException(nameof(request.Position), "Position cannot be null for closing");
|
||||
}
|
||||
|
||||
// This handler should ONLY handle live trading positions
|
||||
// Backtest/paper trading positions must use CloseBacktestFuturesPositionCommandHandler
|
||||
if (request.Position.TradingType == TradingType.BacktestFutures ||
|
||||
request.Position.Initiator == PositionInitiator.PaperTrading)
|
||||
{
|
||||
logger?.LogError(
|
||||
"CloseFuturesPositionCommandHandler received a backtest/paper trading position. " +
|
||||
"Position: {PositionId}, TradingType: {TradingType}, Initiator: {Initiator}. " +
|
||||
"Use CloseBacktestFuturesPositionCommandHandler instead.",
|
||||
request.Position.Identifier, request.Position.TradingType, request.Position.Initiator);
|
||||
throw new InvalidOperationException(
|
||||
$"CloseFuturesPositionCommandHandler cannot handle backtest/paper trading positions. " +
|
||||
$"Position {request.Position.Identifier} has TradingType={request.Position.TradingType} and Initiator={request.Position.Initiator}. " +
|
||||
$"Use CloseBacktestFuturesPositionCommandHandler instead.");
|
||||
}
|
||||
|
||||
Account account = await accountService.GetAccountById(request.AccountId, false, false);
|
||||
|
||||
// For live trading, always get price from exchange
|
||||
var lastPrice = await exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow);
|
||||
|
||||
// Check if position still open on broker
|
||||
var p = (await exchangeService.GetBrokerPositions(account))
|
||||
.FirstOrDefault(x => x.Ticker == request.Position.Ticker);
|
||||
|
||||
// Position not available on the broker, so be sure to update the status
|
||||
if (p == null)
|
||||
{
|
||||
request.Position.Status = PositionStatus.Finished;
|
||||
request.Position.ProfitAndLoss =
|
||||
TradingBox.GetProfitAndLoss(request.Position, request.Position.Open.Quantity, lastPrice,
|
||||
request.Position.Open.Leverage);
|
||||
|
||||
// Add UI fees for closing the position (broker closed it)
|
||||
var closingPositionSizeUsd =
|
||||
(lastPrice * request.Position.Open.Quantity) * request.Position.Open.Leverage;
|
||||
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||
request.Position.AddUiFees(closingUiFees);
|
||||
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||
|
||||
await tradingService.UpdatePositionAsync(request.Position);
|
||||
return request.Position;
|
||||
}
|
||||
|
||||
var closeRequestedOrders = true; // TODO: For gmx no need to close orders since they are closed automatically
|
||||
|
||||
// Close market
|
||||
var closedPosition =
|
||||
await exchangeService.ClosePosition(account, request.Position, lastPrice);
|
||||
|
||||
if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled))
|
||||
{
|
||||
request.Position.Status = PositionStatus.Finished;
|
||||
request.Position.ProfitAndLoss =
|
||||
TradingBox.GetProfitAndLoss(request.Position, closedPosition.Quantity, lastPrice,
|
||||
request.Position.Open.Leverage);
|
||||
|
||||
// Add UI fees for closing the position
|
||||
var closingPositionSizeUsd = (lastPrice * closedPosition.Quantity) * request.Position.Open.Leverage;
|
||||
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||
request.Position.AddUiFees(closingUiFees);
|
||||
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||
|
||||
await tradingService.UpdatePositionAsync(request.Position);
|
||||
}
|
||||
|
||||
return request.Position;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Error closing futures position: {Message} \n Stacktrace : {StackTrace}", ex.Message,
|
||||
ex.StackTrace);
|
||||
|
||||
SentrySdk.CaptureException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ public class ClosePositionCommandHandler(
|
||||
}
|
||||
}
|
||||
|
||||
var isForPaperTrading = request.IsForBacktest;
|
||||
|
||||
var lastPrice = request.Position.Initiator == PositionInitiator.PaperTrading
|
||||
? request.ExecutionPrice.GetValueOrDefault()
|
||||
: await exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow);
|
||||
@@ -72,7 +70,7 @@ public class ClosePositionCommandHandler(
|
||||
|
||||
// Close market
|
||||
var closedPosition =
|
||||
await exchangeService.ClosePosition(account, request.Position, lastPrice, isForPaperTrading);
|
||||
await exchangeService.ClosePosition(account, request.Position, lastPrice);
|
||||
|
||||
if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled))
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Managing.Application.Trading.Handlers
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -287,7 +287,7 @@ public class StatisticService : IStatisticService
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = true,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
|
||||
Reference in New Issue
Block a user