import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { config } from '../src/config/index.js'; import { runBacktest } from '../src/backtest/index.js'; import type { BacktestRequest } from '../src/backtest/types.js'; import logger from '../src/utils/logger.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '..'); const makeCandle = (timestamp: number, close: number) => ({ timestamp, open: close * 0.999, high: close * 1.002, low: close * 0.998, close, volume: 100 }); const build15mSeries = (startTs: number, count: number): Array> => { const out: Array> = []; let price = 100; for (let i = 0; i < count; i++) { if (i < Math.floor(count * 0.6)) { price += 0.5; } else { price -= 0.65; } out.push(makeCandle(startTs + i * 15 * 60 * 1000, Number(price.toFixed(6)))); } return out; }; const buildRequest = (): BacktestRequest => { const candles = build15mSeries(Date.UTC(2025, 0, 1, 0, 0, 0), 700); const from = new Date(candles[220].timestamp).toISOString(); const to = new Date(candles[candles.length - 1].timestamp).toISOString(); return { mode: 'backtest', symbols: ['BTC/USDT'], timeframe: '15m', dateRange: { from, to }, dataSource: { type: 'json', payload: { candles: { 'BTC/USDT': { '15m': candles } } } }, strategyConfig: { rules: [ { ruleId: 'TrendBiasRule', enabled: true, ruleType: 'voting', params: { fastPeriod: 10, slowPeriod: 20 } }, { ruleId: 'SessionRule', enabled: true, ruleType: 'mandatory', params: { sessions: '24/7' } }, { ruleId: 'RiskManagementRule', enabled: true, ruleType: 'mandatory', params: { atrPeriod: 14, maxRisk: 10 } } ], riskLimits: { maxDailyLossUsd: 5000, dailyProfitTargetUsd: 5000, maxOpenTrades: 3, maxConsecutiveLosses: 10 }, execution: { orderType: 'market', cooldownMinutes: 0, minRulePassRatio: 1, entryMode: 'both' } }, execution: { initialCapitalUsd: 10_000, orderType: 'market', slippageBps: 5, partialFillPct: 1, enforceWarmup: true, allowNegativeCash: false, forceCloseAtWindowEnd: false } }; }; const verifySourceIsolation = (): void => { const backtestRoot = path.join(repoRoot, 'src', 'backtest'); const files = fs.readdirSync(backtestRoot, { recursive: true }) .filter((entry) => String(entry).endsWith('.ts')) .map((entry) => path.join(backtestRoot, String(entry))); assert(files.length > 0, 'Expected backtest source files to exist.'); const forbiddenPatterns = [ /from ['"]\.\.\/services\/TradeExecutor/i, /from ['"]\.\.\/services\/AutoTrader/i, /from ['"]\.\.\/services\/CapitalLedger/i, /from ['"]\.\.\/connectors\/alpaca/i, /from ['"]\.\.\/connectors\/ccxt/i, /\.from\(['"](orders|trade_history|capital_ledgers)['"]\)/ ]; for (const filePath of files) { const source = fs.readFileSync(filePath, 'utf8'); for (const pattern of forbiddenPatterns) { assert(!pattern.test(source), `Isolation violation in ${path.relative(repoRoot, filePath)} (${pattern})`); } } }; const verifyBacktestBehavior = async (): Promise => { const originalFlag = config.ENABLE_BACKTEST; const originalInfo = (logger as any).info?.bind(logger); const originalWarn = (logger as any).warn?.bind(logger); const originalError = (logger as any).error?.bind(logger); config.ENABLE_BACKTEST = true; (logger as any).info = () => undefined; (logger as any).warn = () => undefined; (logger as any).error = () => undefined; try { const request = buildRequest(); const first = await runBacktest(request, { profileSettings: { allocated_capital: 10_000, risk_per_trade_percent: 1, strategy_config: request.strategyConfig } }); const second = await runBacktest(request, { profileSettings: { allocated_capital: 10_000, risk_per_trade_percent: 1, strategy_config: request.strategyConfig } }); assert.deepEqual(first, second, 'Backtest must be deterministic for identical inputs.'); const fromTs = Date.parse(request.dateRange.from); const toTs = Date.parse(request.dateRange.to); assert.equal(first.window.endOfWindowPolicy, 'OPEN_AT_END', 'Default end-of-window behavior must be OPEN_AT_END.'); assert.equal(first.window.fromTimestamp, fromTs, 'Window.fromTimestamp must match request.from.'); assert.equal(first.window.toTimestamp, toTs, 'Window.toTimestamp must match request.to.'); assert(first.window.includesWarmupCandles, 'Warm-up candles should be loaded before replay start.'); assert(first.warmup.endTimestamp >= first.warmup.startTimestamp, 'Warm-up report must be valid.'); assert(first.trades.every((trade) => trade.entryTimestamp >= fromTs), 'No trades allowed before replay window start.'); assert(first.trades.every((trade) => trade.entryTimestamp <= toTs), 'No trades allowed after replay window end.'); assert(first.trades.every((trade) => trade.exitTimestamp <= toTs), 'No exits allowed after replay window end.'); assert(first.trades.every((trade) => trade.entryTimestamp >= first.warmup.endTimestamp), 'No trades allowed before warm-up completion.'); assert(first.timeline.every((point) => point.cashUsd >= -1e-6), 'Cash must never be negative with allowNegativeCash=false.'); assert(first.timeline.every((point) => point.reservedUsd <= point.cashUsd + 1e-6), 'Reserved capital cannot exceed cash.'); assert.equal(first.diagnostics.connectorCalls.placeOrder, 0, 'Backtest must never call placeOrder.'); const forcedWindowCloseRequest: BacktestRequest = { ...request, execution: { ...(request.execution || {}), forceCloseAtWindowEnd: true } }; const forcedCloseResult = await runBacktest(forcedWindowCloseRequest, { profileSettings: { allocated_capital: 10_000, risk_per_trade_percent: 1, strategy_config: request.strategyConfig } }); assert.equal(forcedCloseResult.window.endOfWindowPolicy, 'FORCE_CLOSE', 'Force-close mode must be reported in window metadata.'); assert.equal(forcedCloseResult.openPositionsAtEnd.length, 0, 'Force-close mode should not leave OPEN_AT_END positions.'); assert.equal(forcedCloseResult.diagnostics.connectorCalls.placeOrder, 0, 'Force-close mode must never call placeOrder.'); } finally { config.ENABLE_BACKTEST = originalFlag; (logger as any).info = originalInfo; (logger as any).warn = originalWarn; (logger as any).error = originalError; } }; verifySourceIsolation(); await verifyBacktestBehavior(); console.log('[backtest-isolation] OK: warm-up, capital, determinism, and isolation guards validated');