Fix a bit the spot trading

This commit is contained in:
2025-12-10 23:16:46 +07:00
parent 931af3d3af
commit 8ff9437400
8 changed files with 44 additions and 113 deletions

View File

@@ -189,12 +189,6 @@ public class BacktestFuturesBot : TradingBotBase, ITradingBot
await AddSignal(backtestSignal); await AddSignal(backtestSignal);
} }
protected override async Task<decimal> GetLastPriceForPositionOpeningAsync()
{
// For backtest, use LastCandle close price
return LastCandle?.Close ?? 0;
}
protected override async Task<bool> CanOpenPosition(LightSignal signal) protected override async Task<bool> CanOpenPosition(LightSignal signal)
{ {
// Backtest-specific logic: only check cooldown and loss streak // Backtest-specific logic: only check cooldown and loss streak

View File

@@ -193,12 +193,6 @@ public class BacktestSpotBot : TradingBotBase, ITradingBot
await AddSignal(backtestSignal); await AddSignal(backtestSignal);
} }
protected override async Task<decimal> GetLastPriceForPositionOpeningAsync()
{
// For backtest, use LastCandle close price
return LastCandle?.Close ?? 0;
}
protected override async Task<bool> CanOpenPosition(LightSignal signal) protected override async Task<bool> CanOpenPosition(LightSignal signal)
{ {
// For spot trading, only LONG signals can open positions // For spot trading, only LONG signals can open positions

View File

@@ -1133,13 +1133,6 @@ public class FuturesBot : TradingBotBase, ITradingBot
} }
} }
protected override async Task<decimal> GetLastPriceForPositionOpeningAsync()
{
// For live trading, get current price from exchange
return await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>(_scopeFactory,
async exchangeService => { return await exchangeService.GetCurrentPrice(Account, Config.Ticker); });
}
protected override async Task<Position> HandleFlipPosition(LightSignal signal, Position openedPosition, protected override async Task<Position> HandleFlipPosition(LightSignal signal, Position openedPosition,
LightSignal previousSignal, decimal lastPrice) LightSignal previousSignal, decimal lastPrice)
{ {

View File

@@ -114,7 +114,8 @@ public class SpotBot : TradingBotBase, ITradingBot
// Calculate PnL based on current token balance and current price // Calculate PnL based on current token balance and current price
// For LONG spot position: PnL = (currentPrice - openPrice) * tokenBalance // For LONG spot position: PnL = (currentPrice - openPrice) * tokenBalance
var openPrice = position.Open.Price; 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 // Update position PnL
UpdatePositionPnl(position.Identifier, pnlBeforeFees); UpdatePositionPnl(position.Identifier, pnlBeforeFees);
@@ -162,13 +163,9 @@ public class SpotBot : TradingBotBase, ITradingBot
// For spot trading, check token balances to verify position status // For spot trading, check token balances to verify position status
try try
{ {
var balances = await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Balance>>( var tokenBalance = await ServiceScopeHelpers.WithScopedService<IExchangeService, Balance?>(
_scopeFactory, _scopeFactory,
async exchangeService => { return await exchangeService.GetBalances(Account); }); async exchangeService => await exchangeService.GetBalance(Account, Config.Ticker));
var tickerString = Config.Ticker.ToString();
var tokenBalance = balances.FirstOrDefault(b =>
b.TokenName?.Equals(tickerString, StringComparison.OrdinalIgnoreCase) == true);
var hasOpenPosition = Positions.Values.Any(p => p.IsOpen()); 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 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 // We have a token balance but no internal position - orphaned position
await LogWarningAsync( await LogWarningAsync(
@@ -235,26 +232,21 @@ public class SpotBot : TradingBotBase, ITradingBot
// For spot trading, fetch token balance directly and verify/match with internal position // For spot trading, fetch token balance directly and verify/match with internal position
try try
{ {
var balances = await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Balance>>( var tokenBalance = await ServiceScopeHelpers.WithScopedService<IExchangeService, Balance?>(
_scopeFactory, _scopeFactory,
async exchangeService => { return await exchangeService.GetBalances(Account); }); async exchangeService => await exchangeService.GetBalance(Account, Config.Ticker));
// Find the token balance for the ticker
var tickerString = Config.Ticker.ToString();
var tokenBalance = balances.FirstOrDefault(b =>
b.TokenName?.Equals(tickerString, StringComparison.OrdinalIgnoreCase) == true);
if (tokenBalance != null && tokenBalance.Amount > 0) if (tokenBalance != null && tokenBalance.Amount > 0)
{ {
// Verify that the token balance matches the position amount with 0.1% tolerance // Verify that the token balance matches the position amount with 0.1% tolerance
var positionQuantity = internalPosition.Open.Quantity; var positionQuantity = internalPosition.Open.Quantity;
var tokenBalanceAmount = tokenBalance.Amount; var tokenBalanceAmount = tokenBalance.Amount;
if (positionQuantity > 0) 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); var difference = Math.Abs(tokenBalanceAmount - positionQuantity);
if (difference > tolerance) if (difference > tolerance)
{ {
await LogWarningAsync( await LogWarningAsync(
@@ -301,13 +293,17 @@ public class SpotBot : TradingBotBase, ITradingBot
{ {
currentPrice = await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>( currentPrice = await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>(
_scopeFactory, _scopeFactory,
async exchangeService => { return await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); async exchangeService =>
{
return await exchangeService.GetCurrentPrice(Account, Config.Ticker);
});
} }
if (currentPrice > 0) if (currentPrice > 0)
{ {
var openPrice = internalPosition.Open.Price; 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); UpdatePositionPnl(positionForSignal.Identifier, pnlBeforeFees);
var totalFees = internalPosition.GasFees + internalPosition.UiFees; var totalFees = internalPosition.GasFees + internalPosition.UiFees;
@@ -584,13 +580,6 @@ public class SpotBot : TradingBotBase, ITradingBot
return (closingPrice, pnlCalculated); return (closingPrice, pnlCalculated);
} }
protected override async Task<decimal> GetLastPriceForPositionOpeningAsync()
{
// For live trading, get current price from exchange
return await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>(_scopeFactory,
async exchangeService => { return await exchangeService.GetCurrentPrice(Account, Config.Ticker); });
}
protected override async Task UpdateSignalsCore(IReadOnlyList<Candle> candles, protected override async Task UpdateSignalsCore(IReadOnlyList<Candle> candles,
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues = null) Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues = null)
{ {
@@ -690,7 +679,8 @@ public class SpotBot : TradingBotBase, ITradingBot
// Spot-specific position opening: includes balance verification and live exchange calls // Spot-specific position opening: includes balance verification and live exchange calls
if (signal.Direction != TradeDirection.Long) 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) if (Account == null || Account.User == null)
@@ -776,5 +766,4 @@ public class SpotBot : TradingBotBase, ITradingBot
} }
} }
} }
} }

View File

@@ -1,4 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
@@ -852,10 +852,15 @@ public abstract class TradingBotBase : ITradingBot
async tradingService => { await tradingService.UpdatePositionAsync(position); }); async tradingService => { await tradingService.UpdatePositionAsync(position); });
} }
protected virtual async Task<decimal> GetLastPriceForPositionOpeningAsync() protected async Task<decimal> GetLastPriceForPositionOpeningAsync()
{ {
// Default implementation - subclasses should override if (TradingBox.IsLiveTrading(Config.TradingType))
return 0; {
return await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>(_scopeFactory,
async exchangeService => await exchangeService.GetCurrentPrice(Account, Config.Ticker));
}
return LastCandle?.Close ?? 0;
} }
protected async Task<Position> OpenPosition(LightSignal signal) protected async Task<Position> OpenPosition(LightSignal signal)

View File

@@ -29,32 +29,11 @@ public class CloseSpotPositionCommandHandler(
? TradeDirection.Short ? TradeDirection.Short
: TradeDirection.Long; : 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 // For backtest/paper trading, simulate the swap without calling the exchange
SwapInfos swapResult; SwapInfos swapResult;
if (request.Position.TradingType == TradingType.BacktestSpot) var isForBacktest = request.Position.TradingType == TradingType.BacktestSpot;
if (isForBacktest)
{ {
// Simulate successful swap for backtest // Simulate successful swap for backtest
swapResult = new SwapInfos swapResult = new SwapInfos
@@ -71,9 +50,9 @@ public class CloseSpotPositionCommandHandler(
swapResult = await tradingService.SwapGmxTokensAsync( swapResult = await tradingService.SwapGmxTokensAsync(
request.Position.User, request.Position.User,
account.Name, account.Name,
fromTicker, request.Position.Ticker,
toTicker, Ticker.USDC,
swapAmount, (double)request.Position.Open.Quantity,
"market", "market",
null, null,
0.5); 0.5);
@@ -81,7 +60,8 @@ public class CloseSpotPositionCommandHandler(
if (!swapResult.Success) 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) // Build the closing trade directly for backtest (no exchange call needed)
@@ -107,7 +87,10 @@ public class CloseSpotPositionCommandHandler(
request.Position.AddUiFees(closingUiFees); request.Position.AddUiFees(closingUiFees);
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction); request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
// For backtest, skip database update if (!isForBacktest)
{
await tradingService.UpdatePositionAsync(request.Position);
}
return request.Position; return request.Position;
} }
@@ -120,5 +103,4 @@ public class CloseSpotPositionCommandHandler(
throw; throw;
} }
} }
} }

View File

@@ -12,8 +12,8 @@ test('GMX get spot position history - Market swaps', async (t) => {
0, // pageIndex 0, // pageIndex
100, // pageSize 100, // pageSize
Ticker.BTC, // ticker Ticker.BTC, // ticker
'2025-12-04T00:00:00.000Z', // fromDateTime '2025-12-09T00:00:00.000Z', // fromDateTime
'2025-12-07T00:00:00.000Z' // toDateTime '2025-12-11T00:00:00.000Z' // toDateTime
) )
console.log('\n📊 Spot Swap History Summary:') 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(result, 'Spot position history result should be defined')
assert.ok(Array.isArray(result), 'Spot position history should be an array') 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')
})
}) })

View File

@@ -15,7 +15,7 @@ describe('swap tokens implementation', () => {
sdk, sdk,
Ticker.BTC, Ticker.BTC,
Ticker.USDC, Ticker.USDC,
0.00006733 0.00007555
) )
assert.strictEqual(typeof result, 'string') assert.strictEqual(typeof result, 'string')