From f795eb2f5227f933e329ec61fdc804d3d95e0a73 Mon Sep 17 00:00:00 2001 From: geositta Date: Thu, 16 Apr 2026 01:26:14 -0500 Subject: [PATCH] fix(perps): use live price for reverse position orders --- .../perps/services/TradingService.test.ts | 68 +++++-------------- .../perps/services/TradingService.ts | 33 +++------ 2 files changed, 26 insertions(+), 75 deletions(-) diff --git a/app/controllers/perps/services/TradingService.test.ts b/app/controllers/perps/services/TradingService.test.ts index c31b5c5d894..0a950e62d42 100644 --- a/app/controllers/perps/services/TradingService.test.ts +++ b/app/controllers/perps/services/TradingService.test.ts @@ -1982,18 +1982,6 @@ describe('TradingService', () => { stopLossCount: 0, }; - const mockAccountState = { - availableBalance: '10000', - equity: '15000', - marginUsed: '5000', - }; - - beforeEach(() => { - mockProvider.getAccountState = jest - .fn() - .mockResolvedValue(mockAccountState); - }); - it('places order with 2x position size to flip position', async () => { const mockResult: OrderResult = { success: true, @@ -2054,31 +2042,7 @@ describe('TradingService', () => { ); }); - it('returns error when insufficient balance for fees', async () => { - // Set very low available balance - mockProvider.getAccountState = jest.fn().mockResolvedValue({ - ...mockAccountState, - availableBalance: '1', // $1 balance, insufficient for fees - }); - - await expect( - tradingService.flipPosition({ - provider: mockProvider, - position: mockPosition, - context: mockContext, - }), - ).rejects.toThrow(/Insufficient balance for flip fees/); - }); - - it('allows flip when balance covers 1x notional fee estimate', async () => { - // position: size=0.5, entryPrice=50000 - // estimatedFees = positionSize * entryPrice * ESTIMATED_FEE_RATE - // = 0.5 * 50000 * 0.0009 = $22.50 (1x notional, correct) - // pre-fix would compute 2x: 1.0 * 50000 * 0.0009 = $45 → would block this user - mockProvider.getAccountState = jest.fn().mockResolvedValue({ - ...mockAccountState, - availableBalance: '30', // $30 > $22.50, sufficient with 1x - }); + it('does not pass entry price as currentPrice to the provider', async () => { mockProvider.placeOrder.mockResolvedValue({ success: true, orderId: 'flip-balance-fixed', @@ -2093,18 +2057,18 @@ describe('TradingService', () => { }); expect(result.success).toBe(true); - }); - - it('throws error when account state cannot be retrieved', async () => { - mockProvider.getAccountState = jest.fn().mockResolvedValue(null); - - await expect( - tradingService.flipPosition({ - provider: mockProvider, - position: mockPosition, - context: mockContext, + expect(mockProvider.placeOrder).toHaveBeenCalledWith({ + symbol: 'BTC', + isBuy: false, + size: '1', + orderType: 'market', + leverage: 10, + }); + expect(mockProvider.placeOrder).not.toHaveBeenCalledWith( + expect.objectContaining({ + currentPrice: expect.any(Number), }), - ).rejects.toThrow('Failed to get account state'); + ); }); it('returns error when order placement fails', async () => { @@ -2125,7 +2089,11 @@ describe('TradingService', () => { }); it('tracks analytics on success', async () => { - const mockResult: OrderResult = { success: true, orderId: 'flip-123' }; + const mockResult: OrderResult = { + success: true, + orderId: 'flip-123', + averagePrice: '60000', + }; mockProvider.placeOrder.mockResolvedValue(mockResult); await tradingService.flipPosition({ @@ -2138,6 +2106,7 @@ describe('TradingService', () => { PerpsAnalyticsEvent.TradeTransaction, expect.objectContaining({ status: 'executed', + order_value: 30000, }), ); }); @@ -2209,7 +2178,6 @@ describe('TradingService', () => { size: '1', orderType: 'market', leverage: 10, - currentPrice: 50000, }); }); }); diff --git a/app/controllers/perps/services/TradingService.ts b/app/controllers/perps/services/TradingService.ts index 87bcf56a6d3..ac6aeb7b737 100644 --- a/app/controllers/perps/services/TradingService.ts +++ b/app/controllers/perps/services/TradingService.ts @@ -6,7 +6,6 @@ import { PERPS_EVENT_PROPERTY, PERPS_EVENT_VALUE, } from '../constants/eventNames'; -import { ESTIMATED_FEE_RATE } from '../constants/hyperLiquidConfig'; import { isTPSLOrder } from '../constants/orderTypes'; import { PerpsMeasurementName } from '../constants/performanceMetrics'; import { PERPS_CONSTANTS } from '../constants/perpsConfig'; @@ -1942,37 +1941,18 @@ export class TradingService { const isCurrentlyLong = parseFloat(position.size) > 0; const oppositeDirection = !isCurrentlyLong; - // Validate available balance for fees - const accountState = await provider.getAccountState?.(); - if (!accountState) { - throw new Error('Failed to get account state'); - } - - const availableBalance = parseFloat(accountState.availableBalance); - - // Estimate fees: ESTIMATED_FEE_RATE (0.09%) already accounts for both legs - // (close at 0.045% + open at 0.045% = 0.09% of position notional). - // Apply to 1x notional (positionSize * entryPrice), not 2x (flipSize * entryPrice). - const entryPrice = parseFloat(position.entryPrice); const flipSize = positionSize * 2; - const notionalValue = positionSize * entryPrice; - const estimatedFees = notionalValue * ESTIMATED_FEE_RATE; - - if (estimatedFees > availableBalance) { - throw new Error( - `Insufficient balance for flip fees. Need $${estimatedFees.toFixed(2)}, have $${availableBalance.toFixed(2)}`, - ); - } // Create order params for flip - // Use 2x position size: 1x to close current position + 1x to open opposite position + // Use 2x position size: 1x to close current position + 1x to open opposite position. + // Do not pass the position entry price as currentPrice: the provider must fetch + // live market data for validation and IOC pricing. const orderParams: OrderParams = { symbol: position.symbol, isBuy: oppositeDirection, size: flipSize.toString(), orderType: 'market', leverage: position.leverage?.value, - currentPrice: entryPrice, }; // Place flip order (HyperLiquid handles margin transfer automatically) @@ -1980,6 +1960,10 @@ export class TradingService { const completionDuration = this.#deps.performance.now() - startTime; + const executedPrice = parseFloat( + result.averagePrice ?? position.entryPrice, + ); + if (result.success) { // Update state on success if (context.stateManager) { @@ -2006,8 +1990,7 @@ export class TradingService { [PERPS_EVENT_PROPERTY.ORDER_SIZE]: positionSize, [PERPS_EVENT_PROPERTY.COMPLETION_DURATION]: completionDuration, [PERPS_EVENT_PROPERTY.ACTION]: flipAction, - [PERPS_EVENT_PROPERTY.ORDER_VALUE]: - positionSize * parseFloat(position.entryPrice), + [PERPS_EVENT_PROPERTY.ORDER_VALUE]: positionSize * executedPrice, }, );