import dotenv from 'dotenv'; dotenv.config(); import { Alpaca } from './alpaca'; import { Bot } from './bot'; import { MomentumStrategy } from './momentum-strategy'; import { BacktestClient, Bar } from './backtest-client'; import { setWaitFn, resetWaitFn } from './trading'; import { logger } from './logger'; // eslint-disable-next-line @typescript-eslint/no-require-imports const AlpacaJS = require('@alpacahq/alpaca-trade-api'); interface Args { start: string; end: string; capital: number; } function parseArgs(): Args { const args = process.argv.slice(2); let start = ''; let end = ''; let capital = 100_000; for (let i = 0; i < args.length; i++) { if (args[i] === '--start' && args[i + 1]) start = args[++i]; else if (args[i] === '--end' && args[i + 1]) end = args[++i]; else if (args[i] === '--capital' && args[i + 1]) capital = parseFloat(args[++i]); } if (!start || !end) { console.error('Usage: npm run backtest -- --start YYYY-MM-DD --end YYYY-MM-DD [--capital N]'); process.exit(1); } return { start, end, capital }; } async function fetchBars(symbols: string[], start: string, end: string): Promise> { const client = new AlpacaJS({ keyId: process.env.ALPACA_KEY_ID, secretKey: process.env.ALPACA_SECRET_KEY, paper: true, }); const allBars = new Map(); for (const symbol of symbols) { logger.info(`fetching bars for ${symbol} from ${start} to ${end}`); const bars: Bar[] = []; const resp = client.getBarsV2(symbol, { start, end, timeframe: '1Min', limit: 10000, feed: 'sip', }); for await (const bar of resp) { bars.push({ Timestamp: bar.Timestamp, OpenPrice: bar.OpenPrice, HighPrice: bar.HighPrice, LowPrice: bar.LowPrice, ClosePrice: bar.ClosePrice, Volume: bar.Volume, }); } bars.sort((a, b) => new Date(a.Timestamp).getTime() - new Date(b.Timestamp).getTime()); allBars.set(symbol, bars); logger.info(`${symbol}: ${bars.length} bars loaded`); } return allBars; } function getTradingDays(bars: Map): Date[] { const daySet = new Set(); for (const symbolBars of bars.values()) { for (const bar of symbolBars) { const d = new Date(bar.Timestamp); const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; daySet.add(dateStr); } } return [...daySet].sort().map((d) => { const date = new Date(d + 'T09:30:00-05:00'); return date; }); } async function main() { const { start, end, capital } = parseArgs(); const symbols = ['QQQ', 'TQQQ', 'SQQQ']; const bars = await fetchBars(symbols, start, end); const startTime = new Date(start + 'T09:30:00-05:00').getTime(); const backtestClient = new BacktestClient(bars, capital, startTime); const alpaca = new Alpaca(false, backtestClient); setWaitFn(async (ms: number) => { backtestClient.advanceTime(ms); }); const originalDateNow = Date.now; Date.now = () => backtestClient.getSimulatedTime(); try { const tradingDays = getTradingDays(bars); logger.info(`backtest: ${tradingDays.length} trading days from ${start} to ${end}`); const momentum = new MomentumStrategy(); const bot = new Bot(alpaca, [{ strategy: momentum, capitalAllocation: 1.0 }]); for (const day of tradingDays) { const dayStr = day.toISOString().split('T')[0]; logger.info(`--- day: ${dayStr} ---`); backtestClient.resetCursorsForDay(day.getTime()); await bot.runDay(); } printResults(backtestClient, capital); } finally { Date.now = originalDateNow; resetWaitFn(); } } function printResults(client: BacktestClient, startingCapital: number) { const fills = client.fills; const finalValue = client.getTotalValue(); const pnl = finalValue - startingCapital; const pnlPct = (pnl / startingCapital) * 100; console.log('\n=== BACKTEST RESULTS ===\n'); console.log(`Starting capital: $${startingCapital.toFixed(2)}`); console.log(`Final value: $${finalValue.toFixed(2)}`); console.log(`P&L: $${pnl.toFixed(2)} (${pnlPct.toFixed(2)}%)`); console.log(`Total fills: ${fills.length}`); let wins = 0; let losses = 0; for (let i = 0; i < fills.length - 1; i += 2) { const buy = fills[i]; const sell = fills[i + 1]; if (buy && sell && buy.side === 'buy' && sell.side === 'sell') { if (sell.price > buy.price) wins++; else losses++; } } if (wins + losses > 0) { console.log(`Win/Loss: ${wins}W / ${losses}L (${((wins / (wins + losses)) * 100).toFixed(1)}%)`); } console.log('\n--- Trade Log ---'); for (const fill of fills) { console.log(` ${fill.time} ${fill.side.toUpperCase().padEnd(4)} ${fill.symbol.padEnd(5)} ${fill.qty.toFixed(4)} @ $${fill.price.toFixed(4)} ($${fill.notional.toFixed(2)})`); } console.log(); } main().catch((e) => { console.error('Backtest error:', e); process.exit(1); });