diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..46c0ce6 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-19 - Fast Lookups in Sorted Timeseries +**Learning:** O(n) operations like `Array.filter` inside hot loops for timeseries lookups (e.g., retrieving prices `asOf` a timestamp during thousands of backtest intervals) can create severe bottlenecks. The time-series data is naturally sorted by time, which allows for `O(log n)` lookups. +**Action:** When searching for the most recent data point up to a certain time in a sorted array, always use binary search instead of `filter()`. It provides a massive performance improvement. diff --git a/src/agent/backtestRunner.ts b/src/agent/backtestRunner.ts index e91de2c..a34e2e1 100644 --- a/src/agent/backtestRunner.ts +++ b/src/agent/backtestRunner.ts @@ -297,10 +297,24 @@ export async function runBacktestSession( Math.floor(Date.now() / 1000), ); - const vnindexAt = (asOf: number): number | null => { - const series = vnindex.filter((b) => b.time <= asOf); - return series.length ? series[series.length - 1]!.close : null; + const findPriceAt = (barsList: Bar[] | undefined, asOf: number): number | null => { + if (!barsList || barsList.length === 0) return null; + let l = 0; + let r = barsList.length - 1; + let ans: number | null = null; + while (l <= r) { + const m = (l + r) >> 1; + if (barsList[m]!.time <= asOf) { + ans = barsList[m]!.close; + l = m + 1; + } else { + r = m - 1; + } + } + return ans; }; + + const vnindexAt = (asOf: number): number | null => findPriceAt(vnindex, asOf); const vnindexBaseline = vnindexAt(intervalTurns[0]!); if (vnindexBaseline == null) throw new Error(`no VNINDEX data at first ${interval.label} turn`); @@ -311,10 +325,7 @@ export async function runBacktestSession( for (const asOf of intervalTurns) { 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 priceOverride = (sym: string): number | null => findPriceAt(bars[sym], asOf); broker.setPriceOverride(priceOverride); cb.onTurnStart?.({ asOf, dateIso });