From 9bc5f2eb19781f1f8b4e4659ccc2105a4e61497f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:33:04 +0000 Subject: [PATCH 1/2] perf: replace O(n) array filter with O(log n) binary search Replaces the O(n) `.filter()` array scanning with an O(log n) `findLastBarIndex()` binary search utility for time-series lookups in `backtestRunner.ts` and `dnsePublic.ts`. This vastly improves the algorithmic time complexity of lookup routines within the simulation loop. Co-authored-by: toreleon <42534763+toreleon@users.noreply.github.com> --- .jules/bolt.md | 3 +++ src/agent/backtestRunner.ts | 12 +++++++----- src/data/sources/dnsePublic.ts | 25 ++++++++++++++++++++++++- 3 files changed, 34 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..8a352df --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-07-19 - Fast Time-Series Lookups via Binary Search +**Learning:** The `backtestRunner` loops repeatedly over historical time-series data. Previous logic used `.filter((b) => b.time <= asOf)` to get the price of a stock at a specific turn. This caused an $O(N)$ operation inside an already heavy simulation loop, leading to $O(T \times N)$ time complexity for $T$ turns. +**Action:** Since time-series bar data (like `OHLCV`) is naturally sorted chronologically, use `findLastBarIndex` (binary search) to retrieve the required index in $O(\log N)$ time. I have added this reusable utility to `src/data/sources/dnsePublic.ts`. Prefer this utility over full array slicing/filtering for performance-critical lookups on time-sorted arrays. diff --git a/src/agent/backtestRunner.ts b/src/agent/backtestRunner.ts index e91de2c..4fbd0ae 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 series = bars[sym]; + if (!series || series.length === 0) return null; + const idx = findLastBarIndex(series, asOf); + return idx !== -1 ? series[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..09038c4 100644 --- a/src/data/sources/dnsePublic.ts +++ b/src/data/sources/dnsePublic.ts @@ -24,6 +24,27 @@ export interface Bar { volume: number; } +/** + * Finds the index of the last bar with time <= targetTime. + * Requires the bars array to be sorted chronologically by time. + * Returns -1 if no such bar exists. + */ +export function findLastBarIndex(bars: Bar[], targetTime: number): number { + let left = 0; + let right = bars.length - 1; + let ans = -1; + while (left <= right) { + const mid = Math.floor((left + right) / 2); + if (bars[mid]!.time <= targetTime) { + ans = mid; + left = mid + 1; + } else { + right = mid - 1; + } + } + return ans; +} + async function fetchOhlcs( kind: "stock" | "index", symbol: string, @@ -68,7 +89,9 @@ 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( From ba061afa8376f49f860e81f8361ea759a3506efa Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:43:58 +0000 Subject: [PATCH 2/2] perf: replace O(n) array filter with O(log n) binary search Replaces the O(n) `.filter()` array scanning with an O(log n) `findLastBarIndex()` binary search utility for time-series lookups in `backtestRunner.ts` and `dnsePublic.ts`. This vastly improves the algorithmic time complexity of lookup routines within the simulation loop. Also adds pnpm overrides for vulnerable ws and undici packages to fix the failing audit CI check. Co-authored-by: toreleon <42534763+toreleon@users.noreply.github.com> --- package.json | 4 ++++ pnpm-lock.yaml | 30 ++++++++++++++---------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index dd2a5c4..6d0265e 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,10 @@ "vitest": "^2.1.3" }, "pnpm": { + "overrides": { + "undici": ">=7.28.0 <8.0.0", + "ws": ">=8.21.0" + }, "onlyBuiltDependencies": [ "better-sqlite3" ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e3f135..abb401c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,10 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + undici: '>=7.28.0 <8.0.0' + ws: '>=8.21.0' + importers: .: @@ -33,8 +37,8 @@ importers: specifier: ^3.1.0 version: 3.1.0 undici: - specifier: ^6.20.0 - version: 6.25.0 + specifier: '>=7.28.0 <8.0.0' + version: 7.28.0 yaml: specifier: ^2.6.0 version: 2.8.3 @@ -1516,12 +1520,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@6.25.0: - resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==} - engines: {node: '>=18.17'} - - undici@7.25.0: - resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} universalify@0.1.2: @@ -1622,8 +1622,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -2281,7 +2281,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.25.0 + undici: 7.28.0 whatwg-mimetype: 4.0.0 chownr@1.1.4: {} @@ -2590,7 +2590,7 @@ snapshots: type-fest: 4.41.0 widest-line: 5.0.0 wrap-ansi: 9.0.2 - ws: 8.20.0 + ws: 8.21.1 yoga-layout: 3.2.1 optionalDependencies: '@types/react': 18.3.28 @@ -2986,9 +2986,7 @@ snapshots: undici-types@6.21.0: {} - undici@6.25.0: {} - - undici@7.25.0: {} + undici@7.28.0: {} universalify@0.1.2: {} @@ -3083,7 +3081,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.20.0: {} + ws@8.21.1: {} yaml@2.8.3: {}