Fix a bit the spot trading
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user