fix: harden drift verifier and validation flow
This commit is contained in:
@@ -13,7 +13,9 @@ import { getDriftStateVerifier } from '../../../lib/monitoring/drift-state-verif
|
||||
// Mock dependencies
|
||||
jest.mock('../../../lib/drift/client')
|
||||
jest.mock('../../../lib/drift/orders')
|
||||
jest.mock('../../../lib/database/trades')
|
||||
jest.mock('../../../lib/database/trades', () => ({
|
||||
getPrismaClient: jest.fn(),
|
||||
}))
|
||||
jest.mock('../../../lib/notifications/telegram')
|
||||
|
||||
describe('Bug #80: Retry Loop Cooldown', () => {
|
||||
@@ -38,18 +40,19 @@ describe('Bug #80: Retry Loop Cooldown', () => {
|
||||
|
||||
// Mock closePosition
|
||||
const ordersModule = require('../../../lib/drift/orders')
|
||||
mockClosePosition = jest.fn().mockResolvedValue({
|
||||
mockClosePosition = ordersModule.closePosition
|
||||
mockClosePosition.mockResolvedValue({
|
||||
success: true,
|
||||
transactionSignature: 'CLOSE_TX',
|
||||
realizedPnL: -10.50
|
||||
})
|
||||
ordersModule.closePosition = mockClosePosition
|
||||
|
||||
// Mock Prisma
|
||||
const { getPrismaClient } = require('../../../lib/database/trades')
|
||||
mockPrisma = {
|
||||
trade: {
|
||||
findUnique: jest.fn(),
|
||||
findFirst: jest.fn().mockResolvedValue(null),
|
||||
update: jest.fn()
|
||||
}
|
||||
}
|
||||
@@ -214,6 +217,9 @@ describe('Bug #80: Retry Loop Cooldown', () => {
|
||||
|
||||
// But database has recent attempt
|
||||
const twoMinutesAgo = new Date(Date.now() - (2 * 60 * 1000))
|
||||
mockPrisma.trade.findFirst.mockResolvedValue({
|
||||
configSnapshot: { retryCloseTime: twoMinutesAgo.toISOString() }
|
||||
})
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
id: 'trade1',
|
||||
configSnapshot: {
|
||||
@@ -245,6 +251,9 @@ describe('Bug #80: Retry Loop Cooldown', () => {
|
||||
retryCloseTime: oneMinuteAgo.toISOString()
|
||||
}
|
||||
})
|
||||
mockPrisma.trade.findFirst.mockResolvedValue({
|
||||
configSnapshot: { retryCloseTime: oneMinuteAgo.toISOString() }
|
||||
})
|
||||
|
||||
await (verifier as any).retryClose(mismatch)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Drift State Verifier - Position Verification Tests
|
||||
*
|
||||
@@ -7,8 +8,9 @@
|
||||
* Created: Dec 10, 2025
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'
|
||||
import type { Mock } from 'jest-mock'
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals'
|
||||
import { getDriftStateVerifier } from '../../../lib/monitoring/drift-state-verifier'
|
||||
import { closePosition as importedClosePosition } from '../../../lib/drift/orders'
|
||||
|
||||
// Mock dependencies
|
||||
const mockDriftService = {
|
||||
@@ -24,8 +26,7 @@ const mockPrisma = {
|
||||
},
|
||||
}
|
||||
|
||||
const mockClosePosition = jest.fn()
|
||||
const mockSendTelegramMessage = jest.fn()
|
||||
const asMock = (fn: any) => fn as jest.Mock
|
||||
|
||||
jest.mock('../../../lib/drift/client', () => ({
|
||||
getDriftService: jest.fn(() => Promise.resolve(mockDriftService)),
|
||||
@@ -36,20 +37,25 @@ jest.mock('../../../lib/database/trades', () => ({
|
||||
}))
|
||||
|
||||
jest.mock('../../../lib/drift/orders', () => ({
|
||||
closePosition: mockClosePosition,
|
||||
closePosition: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('../../../lib/notifications/telegram', () => ({
|
||||
sendTelegramMessage: mockSendTelegramMessage,
|
||||
sendTelegramMessage: jest.fn(),
|
||||
}))
|
||||
|
||||
// Import DriftStateVerifier after mocks are set up
|
||||
// NOTE: Actual import will need to be added based on your export structure
|
||||
|
||||
describe('Drift State Verifier - Position Verification', () => {
|
||||
let verifier: any
|
||||
const mockClosePosition = importedClosePosition as jest.Mock
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks before each test
|
||||
jest.clearAllMocks()
|
||||
verifier = getDriftStateVerifier()
|
||||
verifier.recentCloseAttempts = new Map()
|
||||
})
|
||||
|
||||
describe('CRITICAL: Active Position Protection', () => {
|
||||
@@ -58,7 +64,7 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
const oldTradeExitTime = new Date('2025-12-10T10:00:00Z')
|
||||
const newTradeCreatedTime = new Date('2025-12-10T10:15:00Z')
|
||||
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'old-trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
direction: 'long',
|
||||
@@ -70,7 +76,7 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockDriftService.getPosition.mockResolvedValue({
|
||||
asMock(mockDriftService.getPosition).mockResolvedValue({
|
||||
size: 15.45, // SOL tokens
|
||||
entryPrice: 142.5, // Different price = new position!
|
||||
unrealizedPnL: 45.2,
|
||||
@@ -78,17 +84,25 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
})
|
||||
|
||||
// KEY: Database shows newer open trade
|
||||
mockPrisma.trade.findMany.mockResolvedValue([
|
||||
asMock(mockPrisma.trade.findMany).mockResolvedValue([
|
||||
{
|
||||
id: 'new-trade-456',
|
||||
createdAt: newTradeCreatedTime,
|
||||
},
|
||||
])
|
||||
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null) // No recent cooldown
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) // No recent cooldown
|
||||
|
||||
// Simulate verification call
|
||||
// NOTE: Actual test implementation depends on your DriftStateVerifier structure
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'old-trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 15.45,
|
||||
dbExitReason: 'SL',
|
||||
timeSinceExit: 30 * 60 * 1000,
|
||||
})
|
||||
|
||||
expect(mockClosePosition).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -96,7 +110,7 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
it('should NOT close when entry price differs by >2%', async () => {
|
||||
const exitTime = new Date(Date.now() - 20 * 60 * 1000) // 20 min ago (past grace period)
|
||||
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
direction: 'long',
|
||||
@@ -108,24 +122,33 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockDriftService.getPosition.mockResolvedValue({
|
||||
asMock(mockDriftService.getPosition).mockResolvedValue({
|
||||
size: 57.14, // Size matches perfectly
|
||||
entryPrice: 143.5, // But entry price 2.5% higher = different position!
|
||||
unrealizedPnL: 120.5,
|
||||
side: 'long',
|
||||
})
|
||||
|
||||
mockPrisma.trade.findMany.mockResolvedValue([]) // No newer trades
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null)
|
||||
asMock(mockPrisma.trade.findMany).mockResolvedValue([]) // No newer trades
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null)
|
||||
|
||||
// Should skip due to entry price mismatch
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 57.14,
|
||||
dbExitReason: 'TP1',
|
||||
timeSinceExit: 20 * 60 * 1000,
|
||||
})
|
||||
expect(mockClosePosition).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should NOT close when size differs by >15%', async () => {
|
||||
const exitTime = new Date(Date.now() - 20 * 60 * 1000)
|
||||
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
direction: 'long',
|
||||
@@ -137,23 +160,32 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockDriftService.getPosition.mockResolvedValue({
|
||||
asMock(mockDriftService.getPosition).mockResolvedValue({
|
||||
size: 45.0, // 45 tokens vs expected 57.14 = 79% ratio (below 85% threshold)
|
||||
entryPrice: 140.0,
|
||||
unrealizedPnL: -25.5,
|
||||
side: 'long',
|
||||
})
|
||||
|
||||
mockPrisma.trade.findMany.mockResolvedValue([])
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null)
|
||||
asMock(mockPrisma.trade.findMany).mockResolvedValue([])
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null)
|
||||
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 45.0,
|
||||
dbExitReason: 'TP1',
|
||||
timeSinceExit: 20 * 60 * 1000,
|
||||
})
|
||||
expect(mockClosePosition).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should NOT close when direction differs', async () => {
|
||||
const exitTime = new Date(Date.now() - 20 * 60 * 1000)
|
||||
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
direction: 'long', // DB says LONG
|
||||
@@ -165,23 +197,32 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockDriftService.getPosition.mockResolvedValue({
|
||||
asMock(mockDriftService.getPosition).mockResolvedValue({
|
||||
size: -57.14, // Negative = SHORT position
|
||||
entryPrice: 140.0,
|
||||
unrealizedPnL: 80.0,
|
||||
side: 'short', // Drift shows SHORT
|
||||
})
|
||||
|
||||
mockPrisma.trade.findMany.mockResolvedValue([])
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null)
|
||||
asMock(mockPrisma.trade.findMany).mockResolvedValue([])
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null)
|
||||
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: -57.14,
|
||||
dbExitReason: 'SL',
|
||||
timeSinceExit: 20 * 60 * 1000,
|
||||
})
|
||||
expect(mockClosePosition).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should NOT close within 10-minute grace period', async () => {
|
||||
const exitTime = new Date(Date.now() - 5 * 60 * 1000) // Only 5 minutes ago
|
||||
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
direction: 'long',
|
||||
@@ -193,17 +234,26 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockDriftService.getPosition.mockResolvedValue({
|
||||
asMock(mockDriftService.getPosition).mockResolvedValue({
|
||||
size: 57.14,
|
||||
entryPrice: 140.0,
|
||||
unrealizedPnL: 45.2,
|
||||
side: 'long',
|
||||
})
|
||||
|
||||
mockPrisma.trade.findMany.mockResolvedValue([])
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null)
|
||||
asMock(mockPrisma.trade.findMany).mockResolvedValue([])
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null)
|
||||
|
||||
// Should skip due to grace period
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 57.14,
|
||||
dbExitReason: 'TP2',
|
||||
timeSinceExit: 5 * 60 * 1000,
|
||||
})
|
||||
expect(mockClosePosition).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -212,7 +262,7 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
it('should close when all verification checks pass', async () => {
|
||||
const exitTime = new Date(Date.now() - 20 * 60 * 1000) // 20 min ago (past grace period)
|
||||
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'ghost-trade-789',
|
||||
symbol: 'SOL-PERP',
|
||||
direction: 'long',
|
||||
@@ -224,26 +274,34 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockDriftService.getPosition.mockResolvedValue({
|
||||
asMock(mockDriftService.getPosition).mockResolvedValue({
|
||||
size: 57.14, // Size matches within 15% tolerance
|
||||
entryPrice: 140.2, // Price matches within 2%
|
||||
unrealizedPnL: -120.5,
|
||||
side: 'long', // Direction matches
|
||||
})
|
||||
|
||||
mockPrisma.trade.findMany.mockResolvedValue([]) // No newer trades
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null) // No cooldown
|
||||
asMock(mockPrisma.trade.findMany).mockResolvedValue([]) // No newer trades
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null) // No cooldown
|
||||
|
||||
mockClosePosition.mockResolvedValue({
|
||||
asMock(mockClosePosition).mockResolvedValue({
|
||||
success: true,
|
||||
transactionSignature: '5YxABC123...',
|
||||
realizedPnL: -120.5,
|
||||
})
|
||||
|
||||
mockPrisma.trade.update.mockResolvedValue({})
|
||||
asMock(mockPrisma.trade.update).mockResolvedValue({})
|
||||
|
||||
// Simulate verification and close
|
||||
// NOTE: Actual test implementation depends on your DriftStateVerifier structure
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'ghost-trade-789',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 57.14,
|
||||
dbExitReason: 'SL',
|
||||
timeSinceExit: 20 * 60 * 1000,
|
||||
})
|
||||
|
||||
// Should have called closePosition with correct parameters
|
||||
expect(mockClosePosition).toHaveBeenCalledWith({
|
||||
@@ -264,7 +322,7 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
const exitTime = new Date(Date.now() - 20 * 60 * 1000)
|
||||
const lastAttempt = Date.now() - 2 * 60 * 1000 // Only 2 minutes ago
|
||||
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
direction: 'long',
|
||||
@@ -276,38 +334,56 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockDriftService.getPosition.mockResolvedValue({
|
||||
asMock(mockDriftService.getPosition).mockResolvedValue({
|
||||
size: 57.14,
|
||||
entryPrice: 140.0,
|
||||
unrealizedPnL: -45.2,
|
||||
side: 'long',
|
||||
})
|
||||
|
||||
mockPrisma.trade.findMany.mockResolvedValue([])
|
||||
asMock(mockPrisma.trade.findMany).mockResolvedValue([])
|
||||
|
||||
// Database shows recent cleanup attempt
|
||||
mockPrisma.trade.findFirst.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue({
|
||||
configSnapshot: {
|
||||
orphanCleanupTime: new Date(lastAttempt).toISOString(),
|
||||
},
|
||||
})
|
||||
|
||||
// Should skip due to cooldown
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 57.14,
|
||||
dbExitReason: 'SL',
|
||||
timeSinceExit: 20 * 60 * 1000,
|
||||
})
|
||||
expect(mockClosePosition).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('CRITICAL: Edge Case Handling', () => {
|
||||
it('should handle missing database trade gracefully', async () => {
|
||||
mockPrisma.trade.findUnique.mockResolvedValue(null)
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null)
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue(null)
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null)
|
||||
|
||||
// Should not attempt close if DB record missing
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 0,
|
||||
dbExitReason: null,
|
||||
timeSinceExit: 0,
|
||||
})
|
||||
expect(mockClosePosition).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle Drift position already closed', async () => {
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
direction: 'long',
|
||||
@@ -319,17 +395,26 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null)
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null)
|
||||
|
||||
// Position already closed on Drift
|
||||
mockDriftService.getPosition.mockResolvedValue(null)
|
||||
asMock(mockDriftService.getPosition).mockResolvedValue(null)
|
||||
|
||||
// Should not attempt close - already resolved
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 0,
|
||||
dbExitReason: 'TP1',
|
||||
timeSinceExit: 20 * 60 * 1000,
|
||||
})
|
||||
expect(mockClosePosition).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle unknown market index gracefully', async () => {
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'trade-123',
|
||||
symbol: 'UNKNOWN-PERP', // Invalid symbol
|
||||
direction: 'long',
|
||||
@@ -341,16 +426,25 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null)
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null)
|
||||
|
||||
// Should skip unknown markets
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'trade-123',
|
||||
symbol: 'UNKNOWN-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 0,
|
||||
dbExitReason: 'SL',
|
||||
timeSinceExit: 20 * 60 * 1000,
|
||||
})
|
||||
expect(mockClosePosition).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should log protection events to database', async () => {
|
||||
const exitTime = new Date(Date.now() - 20 * 60 * 1000)
|
||||
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
direction: 'long',
|
||||
@@ -362,18 +456,27 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockDriftService.getPosition.mockResolvedValue({
|
||||
asMock(mockDriftService.getPosition).mockResolvedValue({
|
||||
size: 57.14,
|
||||
entryPrice: 145.0, // 3.6% price difference = protection trigger
|
||||
unrealizedPnL: 180.0,
|
||||
side: 'long',
|
||||
})
|
||||
|
||||
mockPrisma.trade.findMany.mockResolvedValue([])
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null)
|
||||
mockPrisma.trade.update.mockResolvedValue({})
|
||||
asMock(mockPrisma.trade.findMany).mockResolvedValue([])
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null)
|
||||
asMock(mockPrisma.trade.update).mockResolvedValue({})
|
||||
|
||||
// Should have logged protection event
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 57.14,
|
||||
dbExitReason: 'TP1',
|
||||
timeSinceExit: 20 * 60 * 1000,
|
||||
})
|
||||
expect(mockPrisma.trade.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'trade-123' },
|
||||
@@ -392,7 +495,7 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
// Multiple ambiguous signals
|
||||
const exitTime = new Date(Date.now() - 15 * 60 * 1000) // Near grace period boundary
|
||||
|
||||
mockPrisma.trade.findUnique.mockResolvedValue({
|
||||
asMock(mockPrisma.trade.findUnique).mockResolvedValue({
|
||||
id: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
direction: 'long',
|
||||
@@ -404,18 +507,27 @@ describe('Drift State Verifier - Position Verification', () => {
|
||||
configSnapshot: {},
|
||||
})
|
||||
|
||||
mockDriftService.getPosition.mockResolvedValue({
|
||||
asMock(mockDriftService.getPosition).mockResolvedValue({
|
||||
size: 55.0, // Size 96% of expected (within tolerance but marginal)
|
||||
entryPrice: 142.5, // Price 1.8% different (within tolerance but marginal)
|
||||
unrealizedPnL: 80.0,
|
||||
side: 'long',
|
||||
})
|
||||
|
||||
mockPrisma.trade.findMany.mockResolvedValue([]) // No newer trades (but uncertain)
|
||||
mockPrisma.trade.findFirst.mockResolvedValue(null)
|
||||
asMock(mockPrisma.trade.findMany).mockResolvedValue([]) // No newer trades (but uncertain)
|
||||
asMock(mockPrisma.trade.findFirst).mockResolvedValue(null)
|
||||
|
||||
// When signals are ambiguous, should err on side of NOT closing
|
||||
// (Better to miss cleanup than close active trade)
|
||||
await verifier.processMismatch({
|
||||
tradeId: 'trade-123',
|
||||
symbol: 'SOL-PERP',
|
||||
expectedState: 'closed',
|
||||
actualState: 'open',
|
||||
driftSize: 55.0,
|
||||
dbExitReason: 'SL',
|
||||
timeSinceExit: 15 * 60 * 1000,
|
||||
})
|
||||
expect(mockClosePosition).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -62,6 +62,8 @@ describe('Bug #76: Exit Orders Validation', () => {
|
||||
|
||||
const result = await placeExitOrders(options)
|
||||
|
||||
console.log('RESULT success case', result)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.signatures).toHaveLength(3)
|
||||
expect(result.signatures).toEqual(['TP1_SIG', 'TP2_SIG', 'SL_SIG'])
|
||||
|
||||
Reference in New Issue
Block a user