diff --git a/.jules/bolt.md b/.jules/bolt.md index 989a6ff..047f501 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,7 @@ ## 2024-05-14 - Time-Series O(log n) Search Pattern **Learning:** `src/agent/backtestRunner.ts` makes heavy use of `.filter((b) => b.time <= asOf)` to get the latest close price inside loop bounds, changing potentially O(1) loop lookups or O(log n) lookups into repetitive O(n) filtering over large arrays. **Action:** Expose `findLastBarIndex` in `src/data/sources/dnsePublic.ts` and use binary search or track indices explicitly in tight loops. + +## 2024-05-14 - Time-Series Date Boundaries +**Learning:** Extracting dates with `.filter((b) => b.time >= start && b.time <= end)` from chronologically sorted OHLCV bars is a common but expensive O(n) anti-pattern. +**Action:** When determining intervals inside backtests, use binary search (`findLastBarIndex` and lower-bound equivalent) followed by `.slice()` to extract elements in O(log n + k) time. Avoid `.sort()` since bars are inherently chronologically sorted. diff --git a/src/agent/backtestRunner.ts b/src/agent/backtestRunner.ts index 610ee01..d78eda4 100644 --- a/src/agent/backtestRunner.ts +++ b/src/agent/backtestRunner.ts @@ -101,10 +101,24 @@ function parseBacktestInterval(value: string | undefined): { label: string; minu } function intervalCloses(vnindexBars: Bar[], startSec: number, endSec: number, intervalMinutes: number): number[] { - const base = vnindexBars - .filter((b) => b.time >= startSec && b.time <= endSec) - .map((b) => b.time) - .sort((a, b) => a - b); + // Optimization: Use binary search instead of O(n) filtering to find interval bounds + let startIdx = vnindexBars.length; + let low = 0, high = vnindexBars.length - 1; + while (low <= high) { + const mid = (low + high) >>> 1; + if (vnindexBars[mid]!.time >= startSec) { + startIdx = mid; + high = mid - 1; + } else { + low = mid + 1; + } + } + + const endIdx = findLastBarIndex(vnindexBars, endSec); + const base = (endIdx < startIdx || startIdx >= vnindexBars.length || endIdx < 0) + ? [] + : vnindexBars.slice(startIdx, endIdx + 1).map((b) => b.time); + const step = Math.max(1, Math.round(intervalMinutes / 30)); if (step === 1) return base; diff --git a/src/data/sources/dnsePublic.ts b/src/data/sources/dnsePublic.ts index ee39639..ff422bc 100644 --- a/src/data/sources/dnsePublic.ts +++ b/src/data/sources/dnsePublic.ts @@ -88,7 +88,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); + // Optimization: use O(log n) binary search to slice array instead of O(n) filter + const idx = findLastBarIndex(bars, asOf); + if (idx === -1) return []; + return bars.slice(0, idx + 1); } export async function getStockOhlcv(