From cd3479d8914a0a14a29ab518fea4d8c7f06ad850 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:39:31 +0000 Subject: [PATCH] perf: replace O(n) array lookups with O(log n) binary search in backtest lookups Replaces expensive `Array.prototype.filter` iterations with a fast binary search utility (`findLastBarIndex`) for locating the correct historical price bars by timestamp in `clipBars` and the core `backtestRunner.ts` loop. This drastically improves lookup performance and significantly reduces memory allocation overhead in extended backtests involving numerous tickers. Co-authored-by: toreleon <42534763+toreleon@users.noreply.github.com> --- .jules/bolt.md | 3 +++ src/agent/backtestRunner.ts | 12 +++++++----- src/data/sources/dnsePublic.ts | 30 +++++++++++++++++++++++++++++- 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..32dead0 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2026-06-15 - Optimize Array Lookups with Binary Search +**Learning:** Backtests in this codebase loop over thousands of intervals across dozens of tickers. Using O(n) `Array.prototype.filter()` for chronologically sorted time-series lookups (like finding the latest price before `asOf`) is a major performance bottleneck due to CPU cycles and garbage collection from new array allocations. +**Action:** Replace `array.filter(b => b.time <= asOf)` with an O(log n) binary search utility `findLastBarIndex` when searching sorted chronological series like OHLCV data. diff --git a/src/agent/backtestRunner.ts b/src/agent/backtestRunner.ts index e91de2c..968e123 100644 --- a/src/agent/backtestRunner.ts +++ b/src/agent/backtestRunner.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import { loadConfig } from "../config/loader.js"; import { getDb } from "../storage/db.js"; import { getBacktestBroker } from "../broker/index.js"; -import { getStockOhlcv, getIndexOhlcv, type Bar } from "../data/sources/dnsePublic.js"; +import { getStockOhlcv, getIndexOhlcv, findLastBarIndex, type Bar } from "../data/sources/dnsePublic.js"; import { DISCOVERY_UNIVERSE, discoverTickers } from "../tools/discover.js"; import { setActiveAsOf } from "./clock.js"; import { runTeamAnalysis } from "./team/index.js"; @@ -298,8 +298,8 @@ export async function runBacktestSession( ); const vnindexAt = (asOf: number): number | null => { - const series = vnindex.filter((b) => b.time <= asOf); - return series.length ? series[series.length - 1]!.close : null; + const idx = findLastBarIndex(vnindex, asOf); + return idx !== -1 ? vnindex[idx]!.close : null; }; const vnindexBaseline = vnindexAt(intervalTurns[0]!); if (vnindexBaseline == null) throw new Error(`no VNINDEX data at first ${interval.label} turn`); @@ -312,8 +312,10 @@ export async function runBacktestSession( throwIfAborted(cb.signal); const dateIso = ictLabel(asOf); const priceOverride = (sym: string): number | null => { - const series = bars[sym]?.filter((b) => b.time <= asOf) ?? []; - return series.length ? series[series.length - 1]!.close : null; + const symBars = bars[sym]; + if (!symBars || symBars.length === 0) return null; + const idx = findLastBarIndex(symBars, asOf); + return idx !== -1 ? symBars[idx]!.close : null; }; broker.setPriceOverride(priceOverride); cb.onTurnStart?.({ asOf, dateIso }); diff --git a/src/data/sources/dnsePublic.ts b/src/data/sources/dnsePublic.ts index 151733c..b73b679 100644 --- a/src/data/sources/dnsePublic.ts +++ b/src/data/sources/dnsePublic.ts @@ -24,6 +24,31 @@ export interface Bar { volume: number; } +/** + * Finds the index of the latest bar whose time is <= timeSec. + * Returns -1 if no such bar exists. + * Assumes `bars` is sorted chronologically by time. + */ +export function findLastBarIndex(bars: Bar[], timeSec: number): number { + let left = 0; + let right = bars.length - 1; + let ans = -1; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const midTime = bars[mid]!.time; + + if (midTime <= timeSec) { + ans = mid; + left = mid + 1; // Try to find a later bar that is still <= timeSec + } else { + right = mid - 1; + } + } + + return ans; +} + async function fetchOhlcs( kind: "stock" | "index", symbol: string, @@ -68,7 +93,10 @@ function clipBars(bars: Bar[]): Bar[] { asOfClock.getStore()?.asOfSec != null || isAsOfOverridden(); if (!hasOverride) return bars; const asOf = nowSec(); - return bars.filter((b) => b.time <= asOf); + + const idx = findLastBarIndex(bars, asOf); + if (idx === -1) return []; + return bars.slice(0, idx + 1); } export async function getStockOhlcv(