import assert from 'node:assert/strict'; import { ManualTrader } from '../src/services/ManualTrader.js'; import { SignalDirection } from '../src/strategies/rules/types.js'; import { supabaseService } from '../src/services/SupabaseService.js'; class MockExecutor { public positions = new Map(); public calls: Array<{ symbol: string; side: SignalDirection; qty: number; type: 'market' | 'limit'; price?: number }> = []; getAllPositions() { return this.positions; } getProfileId() { return 'profile-cap-1'; } getUserId() { return 'user-cap-1'; } getActivePosition(_symbol: string) { return null; } async closePosition(_symbol: string, _reason: string) { return { success: true }; } async openPosition( symbol: string, side: SignalDirection, qty: number, type: 'market' | 'limit', price?: number ) { this.calls.push({ symbol, side, qty, type, price }); return { success: true, orderId: `mock-${Date.now()}` }; } } async function testScaleToRemainingCapital() { const executor = new MockExecutor(); executor.positions.set('BTC/USD', { entryPrice: 100, size: 9 // committed = 900 }); const trader = new ManualTrader(executor as any); const result = await trader.executeRequest('ETH/USD', 'buy', 2, 'market', undefined, 100, 'user-cap-1'); assert.equal(result.success, true, 'Manual trade should succeed when some capital remains.'); assert.equal(Number(result.adjustedQty), 1, 'Quantity should be reduced to remaining capital capacity.'); assert.equal(executor.calls.length, 1, 'openPosition should be called once.'); assert.equal(Number(executor.calls[0].qty), 1, 'Executor should receive scaled quantity.'); } async function testWaitForCapitalRelease() { const executor = new MockExecutor(); executor.positions.set('BTC/USD', { entryPrice: 100, size: 10 // committed = 1000, no immediate room }); const trader = new ManualTrader(executor as any); setTimeout(() => { executor.positions.set('BTC/USD', { entryPrice: 100, size: 9 // remaining = 100 after release }); }, 1200); const start = Date.now(); const result = await trader.executeRequest('SOL/USD', 'buy', 1, 'market', undefined, 100, 'user-cap-1'); const elapsed = Date.now() - start; assert.equal(result.success, true, 'Manual trade should proceed once capital is released.'); assert(elapsed >= 1000, 'Manual trader should wait for capital release window.'); assert.equal(executor.calls.length, 1, 'openPosition should execute after wait.'); assert.equal(Number(executor.calls[0].qty), 1, 'Released capital should allow requested quantity.'); } async function run() { const originalGetProfileCapital = (supabaseService as any).getProfileCapital; (supabaseService as any).getProfileCapital = async () => ({ allocatedCapital: 1000, isActive: true, userId: 'user-cap-1' }); try { await testScaleToRemainingCapital(); await testWaitForCapitalRelease(); } finally { (supabaseService as any).getProfileCapital = originalGetProfileCapital; } console.log('[manual-capital-guard] OK: scale-to-remaining and wait-for-release paths validated'); } await run();