184 lines
5.6 KiB
TypeScript
184 lines
5.6 KiB
TypeScript
/**
|
|
* Bug #76 Test: Stop-Loss Placement Validation
|
|
*
|
|
* Tests that placeExitOrders returns failure when SL signature is missing
|
|
* and logs critical errors appropriately.
|
|
*
|
|
* Created: Dec 9, 2025
|
|
* Reason: Bug #76 - placeExitOrders() can return SUCCESS with missing SL orders
|
|
*/
|
|
|
|
import { placeExitOrders, PlaceExitOrdersOptions } from '../../../lib/drift/orders'
|
|
|
|
// Mock dependencies
|
|
jest.mock('../../../lib/drift/client')
|
|
jest.mock('../../../lib/utils/logger')
|
|
jest.mock('../../../config/trading')
|
|
|
|
describe('Bug #76: Exit Orders Validation', () => {
|
|
let mockDriftClient: any
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
|
|
// Mock Drift service and client
|
|
mockDriftClient = {
|
|
placePerpOrder: jest.fn()
|
|
}
|
|
|
|
const { getDriftService } = require('../../../lib/drift/client')
|
|
getDriftService.mockReturnValue({
|
|
getClient: () => mockDriftClient
|
|
})
|
|
|
|
// Mock trading config
|
|
const { getMarketConfig } = require('../../../config/trading')
|
|
getMarketConfig.mockReturnValue({
|
|
driftMarketIndex: 0,
|
|
minOrderSize: 0.1
|
|
})
|
|
})
|
|
|
|
describe('Single Stop System', () => {
|
|
it('should return success when all 3 orders placed (TP1 + TP2 + SL)', async () => {
|
|
// Mock successful order placements
|
|
mockDriftClient.placePerpOrder
|
|
.mockResolvedValueOnce('TP1_SIG') // TP1
|
|
.mockResolvedValueOnce('TP2_SIG') // TP2
|
|
.mockResolvedValueOnce('SL_SIG') // SL
|
|
|
|
const options: PlaceExitOrdersOptions = {
|
|
symbol: 'SOL-PERP',
|
|
positionSizeUSD: 8000,
|
|
entryPrice: 140.00,
|
|
tp1Price: 141.20,
|
|
tp2Price: 142.41,
|
|
stopLossPrice: 138.71,
|
|
tp1SizePercent: 75,
|
|
tp2SizePercent: 0,
|
|
direction: 'long',
|
|
useDualStops: false
|
|
}
|
|
|
|
const result = await placeExitOrders(options)
|
|
|
|
expect(result.success).toBe(true)
|
|
expect(result.signatures).toHaveLength(3)
|
|
expect(result.signatures).toEqual(['TP1_SIG', 'TP2_SIG', 'SL_SIG'])
|
|
})
|
|
|
|
it('should return failure when SL placement fails', async () => {
|
|
// Mock TP1 and TP2 success, but SL fails
|
|
mockDriftClient.placePerpOrder
|
|
.mockResolvedValueOnce('TP1_SIG') // TP1
|
|
.mockResolvedValueOnce('TP2_SIG') // TP2
|
|
.mockRejectedValueOnce(new Error('Rate limit exceeded')) // SL fails
|
|
|
|
const options: PlaceExitOrdersOptions = {
|
|
symbol: 'SOL-PERP',
|
|
positionSizeUSD: 8000,
|
|
entryPrice: 140.00,
|
|
tp1Price: 141.20,
|
|
tp2Price: 142.41,
|
|
stopLossPrice: 138.71,
|
|
tp1SizePercent: 75,
|
|
tp2SizePercent: 0,
|
|
direction: 'long',
|
|
useDualStops: false
|
|
}
|
|
|
|
const result = await placeExitOrders(options)
|
|
|
|
expect(result.success).toBe(false)
|
|
expect(result.error).toContain('Stop loss placement failed')
|
|
})
|
|
})
|
|
|
|
describe('Dual Stop System', () => {
|
|
it('should return success when all 4 orders placed (TP1 + TP2 + Soft + Hard)', async () => {
|
|
// Mock successful order placements
|
|
mockDriftClient.placePerpOrder
|
|
.mockResolvedValueOnce('TP1_SIG') // TP1
|
|
.mockResolvedValueOnce('TP2_SIG') // TP2
|
|
.mockResolvedValueOnce('SOFT_SIG') // Soft Stop
|
|
.mockResolvedValueOnce('HARD_SIG') // Hard Stop
|
|
|
|
const options: PlaceExitOrdersOptions = {
|
|
symbol: 'SOL-PERP',
|
|
positionSizeUSD: 8000,
|
|
entryPrice: 140.00,
|
|
tp1Price: 141.20,
|
|
tp2Price: 142.41,
|
|
stopLossPrice: 138.71,
|
|
tp1SizePercent: 75,
|
|
tp2SizePercent: 0,
|
|
direction: 'long',
|
|
useDualStops: true,
|
|
softStopPrice: 139.00,
|
|
hardStopPrice: 138.50
|
|
}
|
|
|
|
const result = await placeExitOrders(options)
|
|
|
|
expect(result.success).toBe(true)
|
|
expect(result.signatures).toHaveLength(4)
|
|
expect(result.signatures).toEqual(['TP1_SIG', 'TP2_SIG', 'SOFT_SIG', 'HARD_SIG'])
|
|
})
|
|
|
|
it('should return failure when soft stop fails', async () => {
|
|
mockDriftClient.placePerpOrder
|
|
.mockResolvedValueOnce('TP1_SIG') // TP1
|
|
.mockResolvedValueOnce('TP2_SIG') // TP2
|
|
.mockRejectedValueOnce(new Error('Soft stop failed')) // Soft Stop
|
|
|
|
const options: PlaceExitOrdersOptions = {
|
|
symbol: 'SOL-PERP',
|
|
positionSizeUSD: 8000,
|
|
entryPrice: 140.00,
|
|
tp1Price: 141.20,
|
|
tp2Price: 142.41,
|
|
stopLossPrice: 138.71,
|
|
tp1SizePercent: 75,
|
|
tp2SizePercent: 0,
|
|
direction: 'long',
|
|
useDualStops: true,
|
|
softStopPrice: 139.00,
|
|
hardStopPrice: 138.50
|
|
}
|
|
|
|
const result = await placeExitOrders(options)
|
|
|
|
expect(result.success).toBe(false)
|
|
expect(result.error).toContain('Soft stop placement failed')
|
|
})
|
|
|
|
it('should return failure when hard stop fails', async () => {
|
|
mockDriftClient.placePerpOrder
|
|
.mockResolvedValueOnce('TP1_SIG') // TP1
|
|
.mockResolvedValueOnce('TP2_SIG') // TP2
|
|
.mockResolvedValueOnce('SOFT_SIG') // Soft Stop
|
|
.mockRejectedValueOnce(new Error('Hard stop failed')) // Hard Stop
|
|
|
|
const options: PlaceExitOrdersOptions = {
|
|
symbol: 'SOL-PERP',
|
|
positionSizeUSD: 8000,
|
|
entryPrice: 140.00,
|
|
tp1Price: 141.20,
|
|
tp2Price: 142.41,
|
|
stopLossPrice: 138.71,
|
|
tp1SizePercent: 75,
|
|
tp2SizePercent: 0,
|
|
direction: 'long',
|
|
useDualStops: true,
|
|
softStopPrice: 139.00,
|
|
hardStopPrice: 138.50
|
|
}
|
|
|
|
const result = await placeExitOrders(options)
|
|
|
|
expect(result.success).toBe(false)
|
|
expect(result.error).toContain('Hard stop placement failed')
|
|
})
|
|
})
|
|
})
|