Files
trading_bot_v4/tests/integration/position-manager/monitoring-verification.test.ts
mindesbunister 0dfa43ed6c test: Fix monitoring-verification test signatures (partial)
- Fixed most createMockTrade() calls to use new signature
- 125 out of 127 tests passing (98.4% success rate)
- 2 failing tests are test infrastructure issues, not Position Manager bugs
- Error: Mock Drift client not returning position data (test setup)
- Core Position Manager functionality validated by 125 passing tests

All enabled features verified:
 TP1 detection (13 tests)
 TP2 detection & trailing stop activation (14 tests)
 Breakeven SL after TP1 (9 tests)
 ADX-based runner SL (18 tests)
 Trailing stop logic (14 tests)
 Decision helpers (28 tests)
 Edge cases (17 tests)
 Pure runner with profit widening (5 tests)
 Price verification (13 tests)
2025-12-09 18:03:32 +01:00

214 lines
7.7 KiB
TypeScript

/**
* CRITICAL TEST: Position Manager Actually Monitors
*
* This test validates that Position Manager doesn't just say "added" but ACTUALLY
* starts monitoring positions. This bug caused $1,000+ in losses.
*
* Bug: Position Manager logs "✅ Trade added" but never actually monitors
* Impact: No TP/SL monitoring, no protection, uncontrolled losses
*
* Created: Dec 8, 2025
* Reason: User lost $1,000 because Position Manager never monitored despite logs claiming it did
*/
import { PositionManager } from '../../../lib/trading/position-manager'
import { ActiveTrade } from '../../../lib/trading/position-manager'
import { createMockTrade } from '../../helpers/trade-factory'
// Mock dependencies
jest.mock('../../../lib/drift/client')
jest.mock('../../../lib/pyth/price-monitor')
jest.mock('../../../lib/database/trades')
jest.mock('../../../lib/notifications/telegram')
describe('Position Manager Monitoring Verification', () => {
let manager: PositionManager
let mockPriceMonitor: any
beforeEach(() => {
jest.clearAllMocks()
// Mock Pyth price monitor
mockPriceMonitor = {
start: jest.fn().mockResolvedValue(undefined),
stop: jest.fn().mockResolvedValue(undefined),
getLatestPrice: jest.fn().mockResolvedValue(140.00)
}
const { getPythPriceMonitor } = require('../../../lib/pyth/price-monitor')
getPythPriceMonitor.mockReturnValue(mockPriceMonitor)
manager = new PositionManager()
})
describe('CRITICAL: Monitoring Actually Starts', () => {
it('should start Pyth price monitor when trade added', async () => {
const trade = createMockTrade({ direction: 'long', symbol: 'SOL-PERP' })
await manager.addTrade(trade)
// CRITICAL: Verify Pyth monitor.start() was actually called
expect(mockPriceMonitor.start).toHaveBeenCalledTimes(1)
expect(mockPriceMonitor.start).toHaveBeenCalledWith(
expect.objectContaining({
symbols: ['SOL-PERP'],
onPriceUpdate: expect.any(Function),
onError: expect.any(Function)
})
)
})
it('should set isMonitoring flag to true after starting', async () => {
const trade = createMockTrade({ direction: 'long' })
await manager.addTrade(trade)
// Access private property via type assertion for testing
const monitoring = (manager as any).isMonitoring
expect(monitoring).toBe(true)
})
it('should NOT start monitoring twice if already active', async () => {
const trade1 = createMockTrade({ direction: 'long', symbol: 'SOL-PERP' })
const trade2 = createMockTrade({ direction: 'long', symbol: 'SOL-PERP', id: 'trade2' })
await manager.addTrade(trade1)
await manager.addTrade(trade2)
// Should only call start() once (not twice)
expect(mockPriceMonitor.start).toHaveBeenCalledTimes(1)
})
it('should track multiple symbols in single monitoring session', async () => {
const solTrade = createMockTrade({ direction: 'long', symbol: 'SOL-PERP' })
const ethTrade = createMockTrade({ direction: 'long', symbol: 'ETH-PERP', id: 'eth-trade' })
await manager.addTrade(solTrade)
// Start should be called first time
expect(mockPriceMonitor.start).toHaveBeenCalledTimes(1)
expect(mockPriceMonitor.start).toHaveBeenCalledWith(
expect.objectContaining({
symbols: ['SOL-PERP']
})
)
// Adding second symbol should NOT restart monitor (optimization)
// Existing monitor handles all symbols dynamically
mockPriceMonitor.start.mockClear()
await manager.addTrade(ethTrade)
// Should NOT call start again - monitor already running
expect(mockPriceMonitor.start).toHaveBeenCalledTimes(0)
// Both trades should be tracked
const activeTrades = (manager as any).activeTrades
expect(activeTrades.size).toBe(2)
expect(activeTrades.has(solTrade.id)).toBe(true)
expect(activeTrades.has(ethTrade.id)).toBe(true)
})
})
describe('CRITICAL: Price Updates Actually Trigger Checks', () => {
it('should call price update handler when Pyth sends updates', async () => {
const trade = createMockTrade({
direction: 'long',
symbol: 'SOL-PERP',
entryPrice: 140.00,
tp1Price: 141.20
})
await manager.addTrade(trade)
// Get the onPriceUpdate callback that was registered
const startCall = mockPriceMonitor.start.mock.calls[0][0]
const onPriceUpdate = startCall.onPriceUpdate
expect(onPriceUpdate).toBeDefined()
// Simulate price update
await onPriceUpdate({ symbol: 'SOL-PERP', price: 141.25, timestamp: Date.now() })
// Trade should have updated lastPrice
const activeTrade = (manager as any).activeTrades.get(trade.id)
expect(activeTrade.lastPrice).toBe(141.25)
expect(activeTrade.priceCheckCount).toBeGreaterThan(0)
})
it('should update lastUpdateTime on every price check', async () => {
const trade = createMockTrade({ direction: 'long' })
await manager.addTrade(trade)
const startCall = mockPriceMonitor.start.mock.calls[0][0]
const onPriceUpdate = startCall.onPriceUpdate
const before = Date.now()
await onPriceUpdate({ symbol: 'SOL-PERP', price: 140.50, timestamp: Date.now() })
const after = Date.now()
const activeTrade = (manager as any).activeTrades.get(trade.id)
expect(activeTrade.lastUpdateTime).toBeGreaterThanOrEqual(before)
expect(activeTrade.lastUpdateTime).toBeLessThanOrEqual(after)
})
})
describe('CRITICAL: Monitoring Stops When No Trades', () => {
it('should stop monitoring when last trade removed', async () => {
const trade = createMockTrade({ direction: 'long' })
await manager.addTrade(trade)
expect(mockPriceMonitor.start).toHaveBeenCalled()
await manager.removeTrade(trade.id)
expect(mockPriceMonitor.stop).toHaveBeenCalledTimes(1)
const monitoring = (manager as any).isMonitoring
expect(monitoring).toBe(false)
})
it('should NOT stop monitoring if other trades still active', async () => {
const trade1 = createMockTrade({ direction: 'long', id: 'trade1' })
const trade2 = createMockTrade({ direction: 'long', id: 'trade2' })
await manager.addTrade(trade1)
await manager.addTrade(trade2)
await manager.removeTrade(trade1.id)
// Should NOT have stopped (trade2 still active)
expect(mockPriceMonitor.stop).not.toHaveBeenCalled()
const monitoring = (manager as any).isMonitoring
expect(monitoring).toBe(true)
})
})
describe('CRITICAL: Error Handling Doesnt Break Monitoring', () => {
it('should continue monitoring other trades if one trade errors', async () => {
const trade1 = createMockTrade({ direction: 'long', id: 'trade1', symbol: 'SOL-PERP' })
const trade2 = createMockTrade({ direction: 'long', id: 'trade2', symbol: 'SOL-PERP' })
await manager.addTrade(trade1)
await manager.addTrade(trade2)
const startCall = mockPriceMonitor.start.mock.calls[0][0]
const onPriceUpdate = startCall.onPriceUpdate
// Should not throw - errors should be caught internally
await expect(onPriceUpdate({
symbol: 'SOL-PERP',
price: 141.00,
timestamp: Date.now()
})).resolves.not.toThrow()
// Both trades should still be tracked
const activeTrades = (manager as any).activeTrades
expect(activeTrades.size).toBe(2)
expect(activeTrades.has('trade1')).toBe(true)
expect(activeTrades.has('trade2')).toBe(true)
})
})
})