- 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)
214 lines
7.7 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|