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);
}
protected override async Task<decimal> GetLastPriceForPositionOpeningAsync()
{
// For backtest, use LastCandle close price
return LastCandle?.Close ?? 0;
}
protected override async Task<bool> CanOpenPosition(LightSignal signal)
{
// Backtest-specific logic: only check cooldown and loss streak

View File

@@ -193,12 +193,6 @@ public class BacktestSpotBot : TradingBotBase, ITradingBot
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)
{
// 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,
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
// 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<IExchangeService, List<Balance>>(
var tokenBalance = await ServiceScopeHelpers.WithScopedService<IExchangeService, Balance?>(
_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<IExchangeService, List<Balance>>(
var tokenBalance = await ServiceScopeHelpers.WithScopedService<IExchangeService, Balance?>(
_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<IExchangeService, decimal>(
_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<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,
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
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
}
}
}
}
}

View File

@@ -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<decimal> GetLastPriceForPositionOpeningAsync()
protected async Task<decimal> GetLastPriceForPositionOpeningAsync()
{
// Default implementation - subclasses should override
return 0;
if (TradingBox.IsLiveTrading(Config.TradingType))
{
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)

View File

@@ -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;
}
}
}
}

View File

@@ -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')
})
})

View File

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