import assert from 'node:assert/strict'; import { config } from '../src/config/index.js'; import type { Candle, IExchangeConnector } from '../src/connectors/types.js'; import { TradeExecutor } from '../src/services/TradeExecutor.js'; import { capitalLedger } from '../src/services/CapitalLedger.js'; import { distributedLockService } from '../src/services/distributedLockService.js'; import { supabaseService } from '../src/services/SupabaseService.js'; import { SignalDirection } from '../src/strategies/rules/types.js'; class MockExchange implements IExchangeConnector { public placedQty = 0; public placedPrice = 0; private orderSeq = 0; getCapabilities() { return { fetchOpenOrders: true, fetchClosedOrders: true, shorting: true }; } async fetchOHLCV(_symbol: string, _timeframe: string, _limit?: number): Promise { return [{ timestamp: Date.now(), open: 100, high: 100, low: 100, close: 100, volume: 1 }]; } async placeOrder( symbol: string, side: 'buy' | 'sell', qty: number, _type: 'market' | 'limit', price?: number ): Promise { this.orderSeq += 1; this.placedQty = Number(qty); this.placedPrice = Number(price || 100); return { id: `strict-cap-${this.orderSeq}`, symbol, side, qty, status: 'filled', filled_avg_price: this.placedPrice, filled_qty: String(qty) }; } async getPosition(_symbol: string): Promise { return null; } async getOrder(orderId: string): Promise { return { id: orderId, status: 'filled', filled_avg_price: 100, filled_qty: String(this.placedQty || 1) }; } } const saveConfig = () => ({ ENTRY_CAPITAL_BUFFER_PCT: config.ENTRY_CAPITAL_BUFFER_PCT, ENABLE_STRICT_CAPITAL_GUARD: config.ENABLE_STRICT_CAPITAL_GUARD, STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT: config.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT, STRICT_CAPITAL_FEE_BUFFER_PCT: config.STRICT_CAPITAL_FEE_BUFFER_PCT, STRICT_CAPITAL_MIN_RESERVE_USD: config.STRICT_CAPITAL_MIN_RESERVE_USD }); type Stubbed = { hasActiveOrderForTradeId: any; hasFinalizedTradeHistory: any; getVirtualOpenPosition: any; getPendingOrdersForProfile: any; updateOrderStatus: any; logOrder: any; logTransaction: any; reserveForOrder: any; releaseOrderReservation: any; adjustPositionReservation: any; recordRealizedPnl: any; getAvailableCapital: any; tryAcquireRowLock: any; releaseRowLock: any; isEntryInProgress: any; }; const stubDependencies = (availableCapital: number, reservationLog: number[]): Stubbed => { const originals: Stubbed = { hasActiveOrderForTradeId: (supabaseService as any).hasActiveOrderForTradeId, hasFinalizedTradeHistory: (supabaseService as any).hasFinalizedTradeHistory, getVirtualOpenPosition: (supabaseService as any).getVirtualOpenPosition, getPendingOrdersForProfile: (supabaseService as any).getPendingOrdersForProfile, updateOrderStatus: (supabaseService as any).updateOrderStatus, logOrder: (supabaseService as any).logOrder, logTransaction: (supabaseService as any).logTransaction, reserveForOrder: (capitalLedger as any).reserveForOrder, releaseOrderReservation: (capitalLedger as any).releaseOrderReservation, adjustPositionReservation: (capitalLedger as any).adjustPositionReservation, recordRealizedPnl: (capitalLedger as any).recordRealizedPnl, getAvailableCapital: (capitalLedger as any).getAvailableCapital, tryAcquireRowLock: (distributedLockService as any).tryAcquireRowLock, releaseRowLock: (distributedLockService as any).releaseRowLock, isEntryInProgress: (distributedLockService as any).isEntryInProgress }; (supabaseService as any).hasActiveOrderForTradeId = async () => false; (supabaseService as any).hasFinalizedTradeHistory = async () => false; (supabaseService as any).getVirtualOpenPosition = async () => null; (supabaseService as any).getPendingOrdersForProfile = async () => []; (supabaseService as any).updateOrderStatus = async () => { }; (supabaseService as any).logOrder = async () => { }; (supabaseService as any).logTransaction = async () => { }; (capitalLedger as any).getAvailableCapital = async () => availableCapital; (capitalLedger as any).reserveForOrder = async (_profileId: string, amount: number) => { reservationLog.push(Number(amount)); return true; }; (capitalLedger as any).releaseOrderReservation = async () => { }; (capitalLedger as any).adjustPositionReservation = async () => { }; (capitalLedger as any).recordRealizedPnl = async () => { }; (distributedLockService as any).tryAcquireRowLock = async () => true; (distributedLockService as any).releaseRowLock = async () => true; (distributedLockService as any).isEntryInProgress = async () => false; return originals; }; const restoreDependencies = (originals: Stubbed) => { (supabaseService as any).hasActiveOrderForTradeId = originals.hasActiveOrderForTradeId; (supabaseService as any).hasFinalizedTradeHistory = originals.hasFinalizedTradeHistory; (supabaseService as any).getVirtualOpenPosition = originals.getVirtualOpenPosition; (supabaseService as any).getPendingOrdersForProfile = originals.getPendingOrdersForProfile; (supabaseService as any).updateOrderStatus = originals.updateOrderStatus; (supabaseService as any).logOrder = originals.logOrder; (supabaseService as any).logTransaction = originals.logTransaction; (capitalLedger as any).reserveForOrder = originals.reserveForOrder; (capitalLedger as any).releaseOrderReservation = originals.releaseOrderReservation; (capitalLedger as any).adjustPositionReservation = originals.adjustPositionReservation; (capitalLedger as any).recordRealizedPnl = originals.recordRealizedPnl; (capitalLedger as any).getAvailableCapital = originals.getAvailableCapital; (distributedLockService as any).tryAcquireRowLock = originals.tryAcquireRowLock; (distributedLockService as any).releaseRowLock = originals.releaseRowLock; (distributedLockService as any).isEntryInProgress = originals.isEntryInProgress; }; const apiServerStub = { updatePositions: () => { }, updateOrders: () => { }, addHistory: () => { }, recordOrderFailure: () => { }, getState: () => ({ accountSnapshot: { buying_power: 10_000 } }), updateAccountSnapshot: () => { }, updateAccountSnapshotCache: () => { } }; async function runCase(strictEnabled: boolean): Promise<{ placedQty: number; reservedAmount: number }> { const originalConfig = saveConfig(); const reservations: number[] = []; const originals = stubDependencies(100, reservations); try { config.ENTRY_CAPITAL_BUFFER_PCT = 0; config.ENABLE_STRICT_CAPITAL_GUARD = strictEnabled; config.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT = 1; config.STRICT_CAPITAL_FEE_BUFFER_PCT = 0.15; config.STRICT_CAPITAL_MIN_RESERVE_USD = 0; const exchange = new MockExchange(); const executor = new TradeExecutor( exchange, apiServerStub as any, 'global', strictEnabled ? 'profile-strict-on' : 'profile-strict-off' ); const result = await executor.openPosition('BTC/USD', SignalDirection.BUY, 1, 'limit', 100); executor.dispose(); assert.equal(result.success, true, `Expected entry to succeed (strict=${strictEnabled})`); assert(exchange.placedQty > 0, 'Expected exchange order to be placed'); assert(reservations.length > 0, 'Expected capital reservation to be recorded'); return { placedQty: Number(exchange.placedQty), reservedAmount: Number(reservations[0]) }; } finally { config.ENTRY_CAPITAL_BUFFER_PCT = originalConfig.ENTRY_CAPITAL_BUFFER_PCT; config.ENABLE_STRICT_CAPITAL_GUARD = originalConfig.ENABLE_STRICT_CAPITAL_GUARD; config.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT = originalConfig.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT; config.STRICT_CAPITAL_FEE_BUFFER_PCT = originalConfig.STRICT_CAPITAL_FEE_BUFFER_PCT; config.STRICT_CAPITAL_MIN_RESERVE_USD = originalConfig.STRICT_CAPITAL_MIN_RESERVE_USD; restoreDependencies(originals); } } async function run() { const relaxed = await runCase(false); const strict = await runCase(true); assert.strictEqual(relaxed.placedQty, 1, 'Without strict guard, qty should use full available notional.'); assert( strict.placedQty < relaxed.placedQty, `Strict guard must reduce qty. relaxed=${relaxed.placedQty} strict=${strict.placedQty}` ); assert( strict.reservedAmount <= 100 + 1e-6, `Strict reservation should remain within available capital envelope. strict=${strict.reservedAmount}` ); assert( strict.reservedAmount > (strict.placedQty * 100), `Strict reservation should include slippage/fee buffer. qty=${strict.placedQty} reserved=${strict.reservedAmount}` ); console.log('[strict-capital-guard] OK: strict guard reduces qty and raises reservation envelope'); } await run();