Clean code, remove warning for future and spot
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
@@ -19,7 +18,7 @@ using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Bots;
|
||||
|
||||
public class FuturesBot : TradingBotBase, ITradingBot
|
||||
public class FuturesBot : TradingBotBase
|
||||
{
|
||||
public FuturesBot(
|
||||
ILogger<TradingBotBase> logger,
|
||||
@@ -72,24 +71,14 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
// For live trading, get position from database via trading service
|
||||
return await ServiceScopeHelpers.WithScopedService<ITradingService, Position>(
|
||||
_scopeFactory,
|
||||
async tradingService => { return await tradingService.GetPositionByIdentifierAsync(position.Identifier); });
|
||||
}
|
||||
|
||||
protected override async Task UpdatePositionWithBrokerData(Position position, List<Position> brokerPositions)
|
||||
{
|
||||
// Live trading broker position synchronization logic is handled in the base UpdatePosition method
|
||||
// This override allows for any futures-specific synchronization if needed
|
||||
await base.UpdatePositionWithBrokerData(position, brokerPositions);
|
||||
async tradingService => await tradingService.GetPositionByIdentifierAsync(position.Identifier));
|
||||
}
|
||||
|
||||
protected override async Task<Candle> GetCurrentCandleForPositionClose(Account account, string ticker)
|
||||
{
|
||||
// For live trading, get real-time candle from exchange
|
||||
return await ServiceScopeHelpers.WithScopedService<IExchangeService, Candle>(_scopeFactory,
|
||||
async exchangeService =>
|
||||
{
|
||||
return await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
|
||||
});
|
||||
async exchangeService => await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow));
|
||||
}
|
||||
|
||||
protected override async Task<bool> CheckBrokerPositions()
|
||||
@@ -170,7 +159,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
}
|
||||
else
|
||||
{
|
||||
// Broker has a position but we don't have any internal tracking
|
||||
// Broker has a position, but we don't have any internal tracking
|
||||
Logger.LogWarning(
|
||||
$"⚠️ Orphaned Broker Position Detected\n" +
|
||||
$"Broker has position for {Config.Ticker} ({brokerPositionForTicker.OriginDirection})\n" +
|
||||
@@ -196,7 +185,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
if (Config.TradingType == TradingType.BacktestFutures) return;
|
||||
await ServiceScopeHelpers.WithScopedService<IAccountService>(_scopeFactory, async accountService =>
|
||||
{
|
||||
var account = await accountService.GetAccountByAccountName(Config.AccountName, false, false);
|
||||
var account = await accountService.GetAccountByAccountName(Config.AccountName, false);
|
||||
Account = account;
|
||||
});
|
||||
}
|
||||
@@ -297,7 +286,6 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
$"Cannot verify if position is closed\n" +
|
||||
$"Will retry on next execution cycle");
|
||||
// Don't change position status, wait for next cycle
|
||||
return;
|
||||
}
|
||||
else if (existsInHistory)
|
||||
{
|
||||
@@ -309,7 +297,6 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
|
||||
internalPosition.Status = PositionStatus.Finished;
|
||||
await HandleClosedPosition(internalPosition);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -347,11 +334,11 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
}
|
||||
|
||||
var orders = await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Trade>>(_scopeFactory,
|
||||
async exchangeService => { return [.. await exchangeService.GetOpenOrders(Account, Config.Ticker)]; });
|
||||
async exchangeService => [.. await exchangeService.GetOpenOrders(Account, Config.Ticker)]);
|
||||
|
||||
if (orders.Any())
|
||||
if (orders.Count != 0)
|
||||
{
|
||||
var ordersCount = orders.Count();
|
||||
var ordersCount = orders.Count;
|
||||
if (ordersCount >= 3)
|
||||
{
|
||||
var currentTime = DateTime.UtcNow;
|
||||
@@ -386,7 +373,6 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
positionForSignal.TakeProfit1.SetStatus(TradeStatus.Cancelled);
|
||||
|
||||
await UpdatePositionDatabase(positionForSignal);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -538,7 +524,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
/// </summary>
|
||||
/// <param name="position">The position to check</param>
|
||||
/// <returns>True if position found in exchange history with PnL, false otherwise; hadError indicates Web3/infra issues</returns>
|
||||
protected async Task<(bool found, bool hadError)> CheckPositionInExchangeHistory(Position position)
|
||||
private async Task<(bool found, bool hadError)> CheckPositionInExchangeHistory(Position position)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -564,7 +550,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
.OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (recentPosition != null && recentPosition.ProfitAndLoss != null)
|
||||
if (recentPosition is { ProfitAndLoss: not null })
|
||||
{
|
||||
await LogDebugAsync(
|
||||
$"✅ Position Found in Exchange History\n" +
|
||||
@@ -757,7 +743,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
.OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (brokerPosition != null && brokerPosition.ProfitAndLoss != null)
|
||||
if (brokerPosition is { ProfitAndLoss: not null })
|
||||
{
|
||||
await LogDebugAsync(
|
||||
$"✅ Broker Position History Found\n" +
|
||||
@@ -934,7 +920,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
// Fallback to current candle if available
|
||||
if (currentCandle != null)
|
||||
{
|
||||
recentCandles = new List<Candle> { currentCandle };
|
||||
recentCandles = [currentCandle];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -950,8 +936,8 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
var minPriceRecent = recentCandles.Min(c => c.Low);
|
||||
var maxPriceRecent = recentCandles.Max(c => c.High);
|
||||
|
||||
bool wasStopLossHit = false;
|
||||
bool wasTakeProfitHit = false;
|
||||
var wasStopLossHit = false;
|
||||
var wasTakeProfitHit = false;
|
||||
|
||||
if (position.OriginDirection == TradeDirection.Long)
|
||||
{
|
||||
@@ -1193,7 +1179,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
signal.Date,
|
||||
Account.User,
|
||||
Config.BotTradingBalance,
|
||||
isForPaperTrading: false, // Futures is live trading
|
||||
isForPaperTrading: false,
|
||||
lastPrice,
|
||||
signalIdentifier: signal.Identifier,
|
||||
initiatorIdentifier: Identifier,
|
||||
@@ -1203,10 +1189,8 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
.WithScopedServices<IExchangeService, IAccountService, ITradingService, Position>(
|
||||
_scopeFactory,
|
||||
async (exchangeService, accountService, tradingService) =>
|
||||
{
|
||||
return await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
|
||||
.Handle(command);
|
||||
});
|
||||
await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
|
||||
.Handle(command));
|
||||
|
||||
return position;
|
||||
}
|
||||
@@ -1230,7 +1214,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
if (quantity == 0)
|
||||
{
|
||||
await LogDebugAsync($"✅ Trade already closed on exchange for position: `{position.Identifier}`");
|
||||
await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose);
|
||||
await HandleClosedPosition(position, forceMarketClose ? lastPrice : null, forceMarketClose);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1258,7 +1242,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
|
||||
}
|
||||
|
||||
await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null,
|
||||
await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : null,
|
||||
forceMarketClose);
|
||||
}
|
||||
else
|
||||
@@ -1275,7 +1259,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
|
||||
// Trade close on exchange => Should close trade manually
|
||||
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
|
||||
// Ensure trade dates are properly updated even for canceled/rejected positions
|
||||
await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null,
|
||||
await HandleClosedPosition(position, forceMarketClose ? lastPrice : null,
|
||||
forceMarketClose);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public class SpotBot : TradingBotBase
|
||||
// Try to get current price from exchange
|
||||
currentPrice = await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>(
|
||||
_scopeFactory,
|
||||
async exchangeService => { return await exchangeService.GetCurrentPrice(Account, Config.Ticker); });
|
||||
async exchangeService => await exchangeService.GetCurrentPrice(Account, Config.Ticker));
|
||||
}
|
||||
|
||||
if (currentPrice == 0)
|
||||
@@ -144,10 +144,7 @@ public class SpotBot : TradingBotBase
|
||||
{
|
||||
// For live trading, get real-time candle from exchange
|
||||
return await ServiceScopeHelpers.WithScopedService<IExchangeService, Candle>(_scopeFactory,
|
||||
async exchangeService =>
|
||||
{
|
||||
return await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
|
||||
});
|
||||
async exchangeService => await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow));
|
||||
}
|
||||
|
||||
protected override async Task<bool> CheckBrokerPositions()
|
||||
@@ -164,7 +161,7 @@ public class SpotBot : TradingBotBase
|
||||
if (hasOpenPosition)
|
||||
{
|
||||
// We have an internal position - verify it matches broker balance
|
||||
if (tokenBalance != null && tokenBalance.Amount > 0)
|
||||
if (tokenBalance is { Amount: > 0 })
|
||||
{
|
||||
await LogDebugAsync(
|
||||
$"✅ Spot Position Verified\n" +
|
||||
@@ -174,17 +171,16 @@ public class SpotBot : TradingBotBase
|
||||
$"Position matches broker balance");
|
||||
return false; // Position already open, cannot open new one
|
||||
}
|
||||
else
|
||||
{
|
||||
await LogWarningAsync(
|
||||
$"⚠️ Position Mismatch\n" +
|
||||
$"Ticker: {Config.Ticker}\n" +
|
||||
$"Internal position exists but no token balance found\n" +
|
||||
$"Position may need synchronization");
|
||||
return false; // Don't allow opening new position until resolved
|
||||
}
|
||||
|
||||
await LogWarningAsync(
|
||||
$"⚠️ Position Mismatch\n" +
|
||||
$"Ticker: {Config.Ticker}\n" +
|
||||
$"Internal position exists but no token balance found\n" +
|
||||
$"Position may need synchronization");
|
||||
return false; // Don't allow opening new position until resolved
|
||||
}
|
||||
else if (tokenBalance != null && tokenBalance.Value > 1m)
|
||||
|
||||
if (tokenBalance is { Value: > 1m })
|
||||
{
|
||||
// We have a token balance but no internal position - orphaned position
|
||||
await LogWarningAsync(
|
||||
@@ -212,7 +208,7 @@ public class SpotBot : TradingBotBase
|
||||
if (Config.TradingType == TradingType.BacktestSpot) return;
|
||||
await ServiceScopeHelpers.WithScopedService<IAccountService>(_scopeFactory, async accountService =>
|
||||
{
|
||||
var account = await accountService.GetAccountByAccountName(Config.AccountName, false, false);
|
||||
var account = await accountService.GetAccountByAccountName(Config.AccountName, false);
|
||||
Account = account;
|
||||
});
|
||||
}
|
||||
@@ -227,7 +223,7 @@ public class SpotBot : TradingBotBase
|
||||
_scopeFactory,
|
||||
async exchangeService => await exchangeService.GetBalance(Account, Config.Ticker));
|
||||
|
||||
if (tokenBalance != null && tokenBalance.Amount > 0)
|
||||
if (tokenBalance is { Amount: > 0 })
|
||||
{
|
||||
// Verify that the token balance matches the position amount with 0.1% tolerance
|
||||
var positionQuantity = internalPosition.Open.Quantity;
|
||||
@@ -281,7 +277,7 @@ public class SpotBot : TradingBotBase
|
||||
// Calculate and update PnL based on current price
|
||||
var currentPrice = await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>(
|
||||
_scopeFactory,
|
||||
async exchangeService => { return await exchangeService.GetCurrentPrice(Account, Config.Ticker); });
|
||||
async exchangeService => await exchangeService.GetCurrentPrice(Account, Config.Ticker));
|
||||
|
||||
if (currentPrice > 0)
|
||||
{
|
||||
@@ -353,17 +349,16 @@ public class SpotBot : TradingBotBase
|
||||
await LogDebugAsync(
|
||||
$"🔍 Checking Spot Position History for Position: `{position.Identifier}`\nTicker: `{Config.Ticker}`");
|
||||
|
||||
List<Position> positionHistory = null;
|
||||
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory,
|
||||
var positionHistory = await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Position>>(
|
||||
_scopeFactory,
|
||||
async exchangeService =>
|
||||
{
|
||||
var fromDate = DateTime.UtcNow.AddHours(-24);
|
||||
var toDate = DateTime.UtcNow;
|
||||
positionHistory =
|
||||
await exchangeService.GetSpotPositionHistory(Account, Config.Ticker, fromDate, toDate);
|
||||
return await exchangeService.GetSpotPositionHistory(Account, Config.Ticker, fromDate, toDate);
|
||||
});
|
||||
|
||||
if (positionHistory != null && positionHistory.Any())
|
||||
if (positionHistory != null && positionHistory.Count != 0)
|
||||
{
|
||||
var recentPosition = positionHistory
|
||||
.OrderByDescending(p => p.Date)
|
||||
@@ -415,28 +410,28 @@ public class SpotBot : TradingBotBase
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task MonitorSynthRisk(LightSignal signal, Position position)
|
||||
protected override Task MonitorSynthRisk(LightSignal signal, Position position)
|
||||
{
|
||||
// Spot trading doesn't use Synth risk monitoring (futures-specific feature)
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async Task<bool> RecoverOpenPositionFromBroker(LightSignal signal, Position positionForSignal)
|
||||
protected override Task<bool> RecoverOpenPositionFromBroker(LightSignal signal, Position positionForSignal)
|
||||
{
|
||||
// Spot trading doesn't have broker positions to recover
|
||||
// Positions are token balances, not tracked positions
|
||||
return false;
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
protected override async Task<bool> ReconcileWithBrokerHistory(Position position, Candle currentCandle)
|
||||
protected override Task<bool> ReconcileWithBrokerHistory(Position position, Candle currentCandle)
|
||||
{
|
||||
// Spot trading doesn't have broker position history like futures
|
||||
// Return false to continue with candle-based calculation
|
||||
return false;
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
protected override async Task<(decimal closingPrice, bool pnlCalculated)> CalculatePositionClosingFromCandles(
|
||||
Position position, Candle currentCandle, bool forceMarketClose, decimal? forcedClosingPrice)
|
||||
protected override Task<(decimal closingPrice, bool pnlCalculated)> CalculatePositionClosingFromCandles(
|
||||
Position position, Candle? currentCandle, bool forceMarketClose, decimal? forcedClosingPrice)
|
||||
{
|
||||
decimal closingPrice = 0;
|
||||
bool pnlCalculated = false;
|
||||
@@ -529,7 +524,7 @@ public class SpotBot : TradingBotBase
|
||||
? closingPrice > position.Open.Price
|
||||
: closingPrice < position.Open.Price;
|
||||
|
||||
if (isManualCloseProfitable)
|
||||
if (isManualCloseProfitable && position.TakeProfit1 != null)
|
||||
{
|
||||
position.TakeProfit1.SetPrice(closingPrice, 2);
|
||||
position.TakeProfit1.SetDate(currentCandle.Date);
|
||||
@@ -542,9 +537,9 @@ public class SpotBot : TradingBotBase
|
||||
}
|
||||
else
|
||||
{
|
||||
position.StopLoss.SetPrice(closingPrice, 2);
|
||||
position.StopLoss.SetDate(currentCandle.Date);
|
||||
position.StopLoss.SetStatus(TradeStatus.Filled);
|
||||
position.StopLoss?.SetPrice(closingPrice, 2);
|
||||
position.StopLoss?.SetDate(currentCandle.Date);
|
||||
position.StopLoss?.SetStatus(TradeStatus.Filled);
|
||||
|
||||
if (position.TakeProfit1 != null)
|
||||
{
|
||||
@@ -561,11 +556,11 @@ public class SpotBot : TradingBotBase
|
||||
pnlCalculated = true;
|
||||
}
|
||||
|
||||
return (closingPrice, pnlCalculated);
|
||||
return Task.FromResult((closingPrice, pnlCalculated));
|
||||
}
|
||||
|
||||
protected override async Task UpdateSignalsCore(IReadOnlyList<Candle> candles,
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues = null)
|
||||
Dictionary<IndicatorType, IndicatorsResultBase>? preCalculatedIndicatorValues = null)
|
||||
{
|
||||
// For spot trading, always fetch signals regardless of open positions
|
||||
// Check if we're in cooldown period
|
||||
@@ -625,7 +620,7 @@ public class SpotBot : TradingBotBase
|
||||
return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal);
|
||||
}
|
||||
|
||||
protected override async Task<Position> HandleFlipPosition(LightSignal signal, Position openedPosition,
|
||||
protected override async Task<Position?> HandleFlipPosition(LightSignal signal, Position openedPosition,
|
||||
LightSignal previousSignal, decimal lastPrice)
|
||||
{
|
||||
// For spot trading, SHORT signals should close the open LONG position
|
||||
@@ -694,10 +689,8 @@ public class SpotBot : TradingBotBase
|
||||
.WithScopedServices<IExchangeService, IAccountService, ITradingService, Position>(
|
||||
_scopeFactory,
|
||||
async (exchangeService, accountService, tradingService) =>
|
||||
{
|
||||
return await new OpenSpotPositionCommandHandler(exchangeService, accountService, tradingService)
|
||||
.Handle(command);
|
||||
});
|
||||
await new OpenSpotPositionCommandHandler(exchangeService, accountService, tradingService)
|
||||
.Handle(command));
|
||||
|
||||
return position;
|
||||
}
|
||||
@@ -725,7 +718,7 @@ public class SpotBot : TradingBotBase
|
||||
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
|
||||
}
|
||||
|
||||
await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null,
|
||||
await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : null,
|
||||
forceMarketClose);
|
||||
}
|
||||
else
|
||||
@@ -742,7 +735,7 @@ public class SpotBot : TradingBotBase
|
||||
// Trade close on exchange => Should close trade manually
|
||||
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
|
||||
// Ensure trade dates are properly updated even for canceled/rejected positions
|
||||
await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null,
|
||||
await HandleClosedPosition(position, forceMarketClose ? lastPrice : null,
|
||||
forceMarketClose);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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