diff --git a/src/Managing.Application/Bots/BacktestFuturesBot.cs b/src/Managing.Application/Bots/BacktestFuturesBot.cs index ccd60caa..05e4f984 100644 --- a/src/Managing.Application/Bots/BacktestFuturesBot.cs +++ b/src/Managing.Application/Bots/BacktestFuturesBot.cs @@ -189,12 +189,6 @@ public class BacktestFuturesBot : TradingBotBase, ITradingBot await AddSignal(backtestSignal); } - protected override async Task GetLastPriceForPositionOpeningAsync() - { - // For backtest, use LastCandle close price - return LastCandle?.Close ?? 0; - } - protected override async Task CanOpenPosition(LightSignal signal) { // Backtest-specific logic: only check cooldown and loss streak diff --git a/src/Managing.Application/Bots/BacktestSpotBot.cs b/src/Managing.Application/Bots/BacktestSpotBot.cs index d4ceea77..7fe79bf7 100644 --- a/src/Managing.Application/Bots/BacktestSpotBot.cs +++ b/src/Managing.Application/Bots/BacktestSpotBot.cs @@ -193,12 +193,6 @@ public class BacktestSpotBot : TradingBotBase, ITradingBot await AddSignal(backtestSignal); } - protected override async Task GetLastPriceForPositionOpeningAsync() - { - // For backtest, use LastCandle close price - return LastCandle?.Close ?? 0; - } - protected override async Task CanOpenPosition(LightSignal signal) { // For spot trading, only LONG signals can open positions diff --git a/src/Managing.Application/Bots/FuturesBot.cs b/src/Managing.Application/Bots/FuturesBot.cs index ea12c84d..654f8137 100644 --- a/src/Managing.Application/Bots/FuturesBot.cs +++ b/src/Managing.Application/Bots/FuturesBot.cs @@ -1133,13 +1133,6 @@ public class FuturesBot : TradingBotBase, ITradingBot } } - protected override async Task GetLastPriceForPositionOpeningAsync() - { - // For live trading, get current price from exchange - return await ServiceScopeHelpers.WithScopedService(_scopeFactory, - async exchangeService => { return await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); - } - protected override async Task HandleFlipPosition(LightSignal signal, Position openedPosition, LightSignal previousSignal, decimal lastPrice) { diff --git a/src/Managing.Application/Bots/SpotBot.cs b/src/Managing.Application/Bots/SpotBot.cs index a13004ce..afc4eafb 100644 --- a/src/Managing.Application/Bots/SpotBot.cs +++ b/src/Managing.Application/Bots/SpotBot.cs @@ -114,7 +114,8 @@ public class SpotBot : TradingBotBase, ITradingBot // Calculate PnL based on current token balance and current price // For LONG spot position: PnL = (currentPrice - openPrice) * tokenBalance var openPrice = position.Open.Price; - var pnlBeforeFees = TradingBox.CalculatePnL(openPrice, currentPrice, tokenBalance.Amount, 1, TradeDirection.Long); + var pnlBeforeFees = + TradingBox.CalculatePnL(openPrice, currentPrice, tokenBalance.Amount, 1, TradeDirection.Long); // Update position PnL UpdatePositionPnl(position.Identifier, pnlBeforeFees); @@ -162,13 +163,9 @@ public class SpotBot : TradingBotBase, ITradingBot // For spot trading, check token balances to verify position status try { - var balances = await ServiceScopeHelpers.WithScopedService>( + var tokenBalance = await ServiceScopeHelpers.WithScopedService( _scopeFactory, - async exchangeService => { return await exchangeService.GetBalances(Account); }); - - var tickerString = Config.Ticker.ToString(); - var tokenBalance = balances.FirstOrDefault(b => - b.TokenName?.Equals(tickerString, StringComparison.OrdinalIgnoreCase) == true); + async exchangeService => await exchangeService.GetBalance(Account, Config.Ticker)); var hasOpenPosition = Positions.Values.Any(p => p.IsOpen()); @@ -195,7 +192,7 @@ public class SpotBot : TradingBotBase, ITradingBot return false; // Don't allow opening new position until resolved } } - else if (tokenBalance != null && tokenBalance.Amount > 0) + else if (tokenBalance != null && tokenBalance.Value > 1m) { // We have a token balance but no internal position - orphaned position await LogWarningAsync( @@ -235,26 +232,21 @@ public class SpotBot : TradingBotBase, ITradingBot // For spot trading, fetch token balance directly and verify/match with internal position try { - var balances = await ServiceScopeHelpers.WithScopedService>( + var tokenBalance = await ServiceScopeHelpers.WithScopedService( _scopeFactory, - async exchangeService => { return await exchangeService.GetBalances(Account); }); - - // Find the token balance for the ticker - var tickerString = Config.Ticker.ToString(); - var tokenBalance = balances.FirstOrDefault(b => - b.TokenName?.Equals(tickerString, StringComparison.OrdinalIgnoreCase) == true); + async exchangeService => await exchangeService.GetBalance(Account, Config.Ticker)); if (tokenBalance != null && tokenBalance.Amount > 0) { // Verify that the token balance matches the position amount with 0.1% tolerance var positionQuantity = internalPosition.Open.Quantity; var tokenBalanceAmount = tokenBalance.Amount; - + if (positionQuantity > 0) { - var tolerance = positionQuantity * 0.001m; // 0.1% tolerance + var tolerance = positionQuantity * 0.003m; // 0.3% tolerance var difference = Math.Abs(tokenBalanceAmount - positionQuantity); - + if (difference > tolerance) { await LogWarningAsync( @@ -301,13 +293,17 @@ public class SpotBot : TradingBotBase, ITradingBot { currentPrice = await ServiceScopeHelpers.WithScopedService( _scopeFactory, - async exchangeService => { return await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); + async exchangeService => + { + return await exchangeService.GetCurrentPrice(Account, Config.Ticker); + }); } if (currentPrice > 0) { var openPrice = internalPosition.Open.Price; - var pnlBeforeFees = TradingBox.CalculatePnL(openPrice, currentPrice, actualTokenBalance, 1, TradeDirection.Long); + var pnlBeforeFees = TradingBox.CalculatePnL(openPrice, currentPrice, actualTokenBalance, 1, + TradeDirection.Long); UpdatePositionPnl(positionForSignal.Identifier, pnlBeforeFees); var totalFees = internalPosition.GasFees + internalPosition.UiFees; @@ -584,13 +580,6 @@ public class SpotBot : TradingBotBase, ITradingBot return (closingPrice, pnlCalculated); } - protected override async Task GetLastPriceForPositionOpeningAsync() - { - // For live trading, get current price from exchange - return await ServiceScopeHelpers.WithScopedService(_scopeFactory, - async exchangeService => { return await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); - } - protected override async Task UpdateSignalsCore(IReadOnlyList candles, Dictionary preCalculatedIndicatorValues = null) { @@ -690,7 +679,8 @@ public class SpotBot : TradingBotBase, ITradingBot // Spot-specific position opening: includes balance verification and live exchange calls if (signal.Direction != TradeDirection.Long) { - throw new InvalidOperationException($"Only LONG signals can open positions in spot trading. Received: {signal.Direction}"); + throw new InvalidOperationException( + $"Only LONG signals can open positions in spot trading. Received: {signal.Direction}"); } if (Account == null || Account.User == null) @@ -776,5 +766,4 @@ public class SpotBot : TradingBotBase, ITradingBot } } } -} - +} \ No newline at end of file diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index 01c82599..0211975f 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; @@ -852,10 +852,15 @@ public abstract class TradingBotBase : ITradingBot async tradingService => { await tradingService.UpdatePositionAsync(position); }); } - protected virtual async Task GetLastPriceForPositionOpeningAsync() + protected async Task GetLastPriceForPositionOpeningAsync() { - // Default implementation - subclasses should override - return 0; + if (TradingBox.IsLiveTrading(Config.TradingType)) + { + return await ServiceScopeHelpers.WithScopedService(_scopeFactory, + async exchangeService => await exchangeService.GetCurrentPrice(Account, Config.Ticker)); + } + + return LastCandle?.Close ?? 0; } protected async Task OpenPosition(LightSignal signal) diff --git a/src/Managing.Application/Trading/Handlers/CloseSpotPositionCommandHandler.cs b/src/Managing.Application/Trading/Handlers/CloseSpotPositionCommandHandler.cs index f7a885f3..5d33fa16 100644 --- a/src/Managing.Application/Trading/Handlers/CloseSpotPositionCommandHandler.cs +++ b/src/Managing.Application/Trading/Handlers/CloseSpotPositionCommandHandler.cs @@ -29,32 +29,11 @@ public class CloseSpotPositionCommandHandler( ? TradeDirection.Short : TradeDirection.Long; - // For spot trading, determine swap direction for closing - // Long position: Swap Token -> USDC (sell token for USDC) - // Short position: Swap USDC -> Token (buy token with USDC) - Ticker fromTicker; - Ticker toTicker; - double swapAmount; - - if (request.Position.OriginDirection == TradeDirection.Long) - { - fromTicker = request.Position.Ticker; - toTicker = Ticker.USDC; - swapAmount = (double)request.Position.Open.Quantity; - } - else - { - fromTicker = Ticker.USDC; - toTicker = request.Position.Ticker; - // For short, we need to calculate how much USDC to swap back - // This should be the original amount + profit/loss - var originalAmount = request.Position.Open.Price * request.Position.Open.Quantity; - swapAmount = (double)originalAmount; - } - // For backtest/paper trading, simulate the swap without calling the exchange SwapInfos swapResult; - if (request.Position.TradingType == TradingType.BacktestSpot) + var isForBacktest = request.Position.TradingType == TradingType.BacktestSpot; + + if (isForBacktest) { // Simulate successful swap for backtest swapResult = new SwapInfos @@ -71,9 +50,9 @@ public class CloseSpotPositionCommandHandler( swapResult = await tradingService.SwapGmxTokensAsync( request.Position.User, account.Name, - fromTicker, - toTicker, - swapAmount, + request.Position.Ticker, + Ticker.USDC, + (double)request.Position.Open.Quantity, "market", null, 0.5); @@ -81,7 +60,8 @@ public class CloseSpotPositionCommandHandler( if (!swapResult.Success) { - throw new InvalidOperationException($"Failed to close spot position: {swapResult.Error ?? swapResult.Message}"); + throw new InvalidOperationException( + $"Failed to close spot position: {swapResult.Error ?? swapResult.Message}"); } // Build the closing trade directly for backtest (no exchange call needed) @@ -107,7 +87,10 @@ public class CloseSpotPositionCommandHandler( request.Position.AddUiFees(closingUiFees); request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction); - // For backtest, skip database update + if (!isForBacktest) + { + await tradingService.UpdatePositionAsync(request.Position); + } return request.Position; } @@ -120,5 +103,4 @@ public class CloseSpotPositionCommandHandler( throw; } } -} - +} \ No newline at end of file diff --git a/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts b/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts index c966520c..f4c45c0d 100644 --- a/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts +++ b/src/Managing.Web3Proxy/test/plugins/get-spot-position-history.test.ts @@ -12,8 +12,8 @@ test('GMX get spot position history - Market swaps', async (t) => { 0, // pageIndex 100, // pageSize Ticker.BTC, // ticker - '2025-12-04T00:00:00.000Z', // fromDateTime - '2025-12-07T00:00:00.000Z' // toDateTime + '2025-12-09T00:00:00.000Z', // fromDateTime + '2025-12-11T00:00:00.000Z' // toDateTime ) console.log('\n📊 Spot Swap History Summary:') @@ -23,31 +23,5 @@ test('GMX get spot position history - Market swaps', async (t) => { assert.ok(result, 'Spot position history result should be defined') assert.ok(Array.isArray(result), 'Spot position history should be an array') }) - - await t.test('should get spot swaps within date range', async () => { - const sdk = await getClientForAddress('0x932167388dD9aad41149b3cA23eBD489E2E2DD78') - - const toDate = new Date() - const fromDate = new Date(toDate.getTime() - (60 * 60 * 1000)) // last 1 hour - - const fromDateTime = fromDate.toISOString() - const toDateTime = toDate.toISOString() - - const result = await getSpotPositionHistoryImpl( - sdk, - 0, - 50, - Ticker.BTC, - fromDateTime, - toDateTime - ) - - console.log(`\n📅 Spot swaps in last 1 hour: ${result.length}`) - console.log(`From: ${fromDateTime}`) - console.log(`To: ${toDateTime}`) - - assert.ok(result, 'Spot position history result should be defined') - assert.ok(Array.isArray(result), 'Spot position history should be an array') - }) }) diff --git a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts index 0a039c9c..302a7ed9 100644 --- a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts +++ b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts @@ -15,7 +15,7 @@ describe('swap tokens implementation', () => { sdk, Ticker.BTC, Ticker.USDC, - 0.00006733 + 0.00007555 ) assert.strictEqual(typeof result, 'string')