diff --git a/.jules/bolt.md b/.jules/bolt.md index 289fae6..cd3cb22 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -23,3 +23,6 @@ ## 2026-09-19 - Avoid chained array methods before Set initialization **Learning:** Chaining `.map().filter()` inside a `new Set()` constructor creates multiple intermediate arrays, causing unnecessary memory allocation and garbage collection overhead in hot paths (like ticker normalization during discovery). **Action:** Replace chained array methods with a direct indexed `for` loop that transforms, validates, and `.add()`s directly to a freshly initialized `Set`. +## 2026-10-01 - Optimize ticker extraction in backtest hot loop +**Learning:** In hot loops like the backtest runner, chaining declarative array and Set methods (e.g., `Array.from(new Set([...arr1, ...arr2]))`) creates unnecessary intermediate arrays and causes GC overhead. Even with small arrays, these micro-allocations add up in tight loops. +**Action:** Use imperative bounded `for` loops with early-exit conditions and a tracking `Set` to prevent unnecessary iteration and allocations. While this slightly increases code verbosity, the performance trade-off is often justified in critical simulation paths. diff --git a/src/agent/backtestRunner.ts b/src/agent/backtestRunner.ts index e53b03c..d886a63 100644 --- a/src/agent/backtestRunner.ts +++ b/src/agent/backtestRunner.ts @@ -347,10 +347,29 @@ export async function runBacktestSession( universe: BACKTEST_DISCOVERY_UNIVERSE, limit: maxCandidates, }); - const held = (await broker.snapshot()).positions.map((p) => p.ticker); - const tickers = Array.from( - new Set([...held, ...discovery.candidates.map((c) => c.ticker)]), - ).slice(0, maxCandidates); + const snapPositions = (await broker.snapshot()).positions; + const tickers: string[] = []; + const seen = new Set(); + + for (let i = 0; i < snapPositions.length; i++) { + const ticker = snapPositions[i]!.ticker; + if (!seen.has(ticker)) { + seen.add(ticker); + tickers.push(ticker); + if (tickers.length >= maxCandidates) break; + } + } + + if (tickers.length < maxCandidates) { + for (let i = 0; i < discovery.candidates.length; i++) { + const ticker = discovery.candidates[i]!.ticker; + if (!seen.has(ticker)) { + seen.add(ticker); + tickers.push(ticker); + if (tickers.length >= maxCandidates) break; + } + } + } const decisions: FinalDecision[] = []; for (const ticker of tickers) {