From 3aad43d9c15e4d09055dcb6da566b7462cd77fd6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:17:39 +0000 Subject: [PATCH 1/2] perf: O(log n) lookups for time-series data using binary search - Add reusable `findLastBarIndex` for O(log n) searches on sorted bar data. - Refactor `clipBars` to use binary search and slice instead of O(n) filter. - Refactor backtester inner loop lookups to use binary search. Co-authored-by: toreleon <42534763+toreleon@users.noreply.github.com> --- .jules/bolt.md | 3 +++ src/agent/backtestRunner.ts | 13 ++++++++----- src/data/sources/dnsePublic.ts | 28 +++++++++++++++++++++++++++- 3 files changed, 38 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..001c472 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2026-07-16 - O(log n) lookups with binary search +**Learning:** Found O(n) array filtering inside the backtest loop (`vnindex.filter` and `bars[sym].filter`) and within the dnse `clipBars` utility for finding chronological subsets of market data. +**Action:** Replaced O(n) array filtering with a reusable O(log n) binary search utility (`findLastBarIndex`) to quickly identify the index of the latest valid bar. This improved nested backtest loop performance from O(m*n) to O(m*log n) for price lookups. diff --git a/src/agent/backtestRunner.ts b/src/agent/backtestRunner.ts index e91de2c..48a74df 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,9 @@ 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; + // ⚡ Bolt: O(log n) lookup instead of O(n) array filter + const lastIdx = findLastBarIndex(vnindex, asOf); + return lastIdx !== -1 ? vnindex[lastIdx]!.close : null; }; const vnindexBaseline = vnindexAt(intervalTurns[0]!); if (vnindexBaseline == null) throw new Error(`no VNINDEX data at first ${interval.label} turn`); @@ -312,8 +313,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] ?? []; + // ⚡ Bolt: O(log n) lookup instead of O(n) array filter + const lastIdx = findLastBarIndex(series, asOf); + return lastIdx !== -1 ? series[lastIdx]!.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..63ed961 100644 --- a/src/data/sources/dnsePublic.ts +++ b/src/data/sources/dnsePublic.ts @@ -61,6 +61,28 @@ export function seriesToBars(s: OhlcvSeries): Bar[] { return out; } +/** + * ⚡ Bolt: O(log n) binary search to find the index of the last bar <= time. + * Assumes the bars array is chronologically sorted. + */ +export function findLastBarIndex(bars: Bar[], targetTime: number): number { + let low = 0; + let high = bars.length - 1; + let result = -1; + + while (low <= high) { + const mid = (low + high) >>> 1; + if (bars[mid]!.time <= targetTime) { + result = mid; + low = mid + 1; // look for a potentially later bar that still <= targetTime + } else { + high = mid - 1; // targetTime is smaller, look in the left half + } + } + + return result; +} + function clipBars(bars: Bar[]): Bar[] { // Clip when an as-of clock is active (ALS or module override). When neither // is set, fall through unchanged — DNSE only returns historical data anyway. @@ -68,7 +90,11 @@ function clipBars(bars: Bar[]): Bar[] { asOfClock.getStore()?.asOfSec != null || isAsOfOverridden(); if (!hasOverride) return bars; const asOf = nowSec(); - return bars.filter((b) => b.time <= asOf); + + // ⚡ Bolt: Use O(log n) binary search instead of O(n) array filter + const lastIdx = findLastBarIndex(bars, asOf); + if (lastIdx === -1) return []; + return bars.slice(0, lastIdx + 1); } export async function getStockOhlcv( From 36947c583be4a844b1c044d45eaee7670cfda07f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:47:42 +0000 Subject: [PATCH 2/2] perf: O(log n) lookups for time-series data using binary search - Add reusable `findLastBarIndex` for O(log n) searches on sorted bar data. - Refactor `clipBars` to use binary search and slice instead of O(n) filter. - Refactor backtester inner loop lookups to use binary search. - Bump undici to address security vulnerabilities Co-authored-by: toreleon <42534763+toreleon@users.noreply.github.com> --- package.json | 12 +++---- pnpm-lock.yaml | 95 +++++++++++++++++++++++++------------------------- 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index dd2a5c4..74c8e15 100644 --- a/package.json +++ b/package.json @@ -54,14 +54,14 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.0", - "better-sqlite3": "^11.5.0", - "cheerio": "^1.0.0", + "better-sqlite3": "^12.11.1", + "cheerio": "^1.2.0", "ink": "^5.0.1", - "ink-spinner": "^5.0.0", - "ink-text-input": "^6.0.0", + "ink-spinner": "5.0.0", + "ink-text-input": "6.0.0", "react": "^18.3.1", "technicalindicators": "^3.1.0", - "undici": "^6.20.0", + "undici": "^8.7.0", "yaml": "^2.6.0", "zod": "^3.23.8" }, @@ -80,5 +80,5 @@ "better-sqlite3" ] }, - "packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8" + "packageManager": "pnpm@11.13.1+sha512.b2fc7683b8a6525414e7d13e1ba28caaddde96bf66ec540bfaeb7e702b81f3e0be4d1f295edf7f9fe0396740a8dce4509c582ddf79891f4543fea32d37645f25" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e3f135..bf2c3e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,20 +12,20 @@ importers: specifier: ^0.1.0 version: 0.1.77(zod@3.25.76) better-sqlite3: - specifier: ^11.5.0 - version: 11.10.0 + specifier: ^12.11.1 + version: 12.11.1 cheerio: - specifier: ^1.0.0 + specifier: ^1.2.0 version: 1.2.0 ink: specifier: ^5.0.1 - version: 5.2.1(@types/react@18.3.28)(react@18.3.1) + version: 5.0.1(@types/react@18.3.28)(react@18.3.1) ink-spinner: - specifier: ^5.0.0 - version: 5.0.0(ink@5.2.1(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) + specifier: 5.0.0 + version: 5.0.0(ink@5.0.1(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) ink-text-input: - specifier: ^6.0.0 - version: 6.0.0(ink@5.2.1(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) + specifier: 6.0.0 + version: 6.0.0(ink@5.0.1(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -33,8 +33,8 @@ importers: specifier: ^3.1.0 version: 3.1.0 undici: - specifier: ^6.20.0 - version: 6.25.0 + specifier: ^8.7.0 + version: 8.7.0 yaml: specifier: ^2.6.0 version: 2.8.3 @@ -788,8 +788,9 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} - better-sqlite3@11.10.0: - resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + better-sqlite3@12.11.1: + resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -952,9 +953,6 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-toolkit@1.46.1: - resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} - esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -1098,8 +1096,8 @@ packages: ink: '>=5' react: '>=18' - ink@5.2.1: - resolution: {integrity: sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==} + ink@5.0.1: + resolution: {integrity: sha512-ae4AW/t8jlkj/6Ou21H2av0wxTk8vrGzXv+v2v7j4in+bl1M5XRMVbfNghzhBokV++FjF8RBDJvYo+ttR9YVRg==} engines: {node: '>=18'} peerDependencies: '@types/react': '>=18.0.0' @@ -1127,8 +1125,8 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-in-ci@1.0.0: - resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==} + is-in-ci@0.1.0: + resolution: {integrity: sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ==} engines: {node: '>=18'} hasBin: true @@ -1168,6 +1166,9 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1516,14 +1517,14 @@ 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'} + undici@8.7.0: + resolution: {integrity: sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==} + engines: {node: '>=22.19.0'} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -1622,8 +1623,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 @@ -1639,8 +1640,8 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yoga-layout@3.2.1: - resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + yoga-wasm-web@0.3.3: + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -2219,7 +2220,7 @@ snapshots: dependencies: is-windows: 1.0.2 - better-sqlite3@11.10.0: + better-sqlite3@12.11.1: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 @@ -2281,7 +2282,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: {} @@ -2387,8 +2388,6 @@ snapshots: es-module-lexer@1.7.0: {} - es-toolkit@1.46.1: {} - esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -2548,24 +2547,24 @@ snapshots: ini@1.3.8: {} - ink-spinner@5.0.0(ink@5.2.1(@types/react@18.3.28)(react@18.3.1))(react@18.3.1): + ink-spinner@5.0.0(ink@5.0.1(@types/react@18.3.28)(react@18.3.1))(react@18.3.1): dependencies: cli-spinners: 2.9.2 - ink: 5.2.1(@types/react@18.3.28)(react@18.3.1) + ink: 5.0.1(@types/react@18.3.28)(react@18.3.1) react: 18.3.1 ink-testing-library@4.0.0(@types/react@18.3.28): optionalDependencies: '@types/react': 18.3.28 - ink-text-input@6.0.0(ink@5.2.1(@types/react@18.3.28)(react@18.3.1))(react@18.3.1): + ink-text-input@6.0.0(ink@5.0.1(@types/react@18.3.28)(react@18.3.1))(react@18.3.1): dependencies: chalk: 5.6.2 - ink: 5.2.1(@types/react@18.3.28)(react@18.3.1) + ink: 5.0.1(@types/react@18.3.28)(react@18.3.1) react: 18.3.1 type-fest: 4.41.0 - ink@5.2.1(@types/react@18.3.28)(react@18.3.1): + ink@5.0.1(@types/react@18.3.28)(react@18.3.1): dependencies: '@alcalzone/ansi-tokenize': 0.1.3 ansi-escapes: 7.3.0 @@ -2576,9 +2575,9 @@ snapshots: cli-cursor: 4.0.0 cli-truncate: 4.0.0 code-excerpt: 4.0.0 - es-toolkit: 1.46.1 indent-string: 5.0.0 - is-in-ci: 1.0.0 + is-in-ci: 0.1.0 + lodash: 4.18.1 patch-console: 2.0.0 react: 18.3.1 react-reconciler: 0.29.2(react@18.3.1) @@ -2590,8 +2589,8 @@ snapshots: type-fest: 4.41.0 widest-line: 5.0.0 wrap-ansi: 9.0.2 - ws: 8.20.0 - yoga-layout: 3.2.1 + ws: 8.21.1 + yoga-wasm-web: 0.3.3 optionalDependencies: '@types/react': 18.3.28 transitivePeerDependencies: @@ -2610,7 +2609,7 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-in-ci@1.0.0: {} + is-in-ci@0.1.0: {} is-number@7.0.0: {} @@ -2643,6 +2642,8 @@ snapshots: lodash.startcase@4.4.0: {} + lodash@4.18.1: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -2986,9 +2987,9 @@ snapshots: undici-types@6.21.0: {} - undici@6.25.0: {} + undici@7.28.0: {} - undici@7.25.0: {} + undici@8.7.0: {} universalify@0.1.2: {} @@ -3083,10 +3084,10 @@ snapshots: wrappy@1.0.2: {} - ws@8.20.0: {} + ws@8.21.1: {} yaml@2.8.3: {} - yoga-layout@3.2.1: {} + yoga-wasm-web@0.3.3: {} zod@3.25.76: {}