Skip to content

fix: handle sparse minute kline history OK-60116 - #12909

Open
limichange wants to merge 17 commits into
xfrom
fix/native-kline-sparse-history
Open

fix: handle sparse minute kline history OK-60116#12909
limichange wants to merge 17 commits into
xfrom
fix/native-kline-sparse-history

Conversation

@limichange

@limichange limichange commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

OK-60116


Summary

  • reuse the weekly/daily history-boundary lookup for sparse 1m/5m Market candles
  • progressively backfill sparse initial history across empty calendar ranges
  • preload older history when the left buffer reaches half a viewport and refill toward one viewport
  • batch daily locator candidates into at most eight minute-history requests per recovery round

Root cause

Market history treated a short or empty response as the end of history. Tokens with gaps therefore stopped pagination even when older candles existed. The first sparse recovery implementation also scanned one active day per request, which either stopped after eight empty candidates or removed the cap and could issue hundreds of sequential requests.

Impact

Sparse 1m/5m charts now resolve the real earliest date with the shared 1W/1D boundary flow, cross more than eight empty active-day candidates automatically, and keep each recovery round bounded to eight minute-history requests. CoinGecko fallback and Hyperliquid behavior remain isolated from Market sparse-history recovery.

Validation

  • yarn jest packages/kit/src/components/TradingView/TradingViewNative/data/useTradingViewNativeKLine.test.ts --runInBand (85/85)
  • yarn agent:check --profile commit
  • yarn agent:check --profile pr (all local checks passed)

Issue: OK-60116

@limichange
limichange marked this pull request as ready for review August 17, 2026 12:22
@limichange
limichange enabled auto-merge (squash) August 17, 2026 12:22
@sidmorizon

Copy link
Copy Markdown
Contributor

@codex review

@sidmorizon

Copy link
Copy Markdown
Contributor

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5cc6ccaaa9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limichange

Automated code review found blocking issues

Reviewed commit 00adb594c45f.

  • P2 · Recovery stalls after eight empty active-day probes

View the full review report

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limichange

Automated code review found blocking issues

Reviewed commit c93ecf16543e.

  • P2 · Viewport target is used as a sparse-recovery request budget

View the full review report

Copy link
Copy Markdown
Contributor Author

Addressed the remaining sparse-recovery review feedback in b57cf1bd6e:

  • initial recovery now crosses more than eight empty active-day candidates without another gesture
  • daily locator candidates are batched into a fixed maximum of eight minute-history requests per recovery round
  • added regressions for nine empty candidates before the first minute candle and for a 100-day sparse preload capped at eight recovery requests
  • focused TradingViewNative K-line suite passes 85/85

All inline review threads have been replied to and resolved.

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limichange

Automated code review found blocking issues

Reviewed commit b57cf1bd6ea1.

  • P1 · Sparse recovery skips candles when a batched day range hits the Market page cap

View the full review report

@originalix

originalix commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Automated code review found blocking issues

@limichange

Reviewed commit bfe2ae18c4e4. This report always reflects the latest reviewed changes.

Review summary

The foreground TradingViewNative history state machine now keeps address-backed Market series stable across metadata changes, selects one primary or fallback source for all intervals, and recovers sparse 1-minute and 5-minute history by locating a coarse boundary and scanning older time windows. Initial loading and viewport pagination share the recovery machinery while realtime transport and the foreground-to-background Market service boundary remain unchanged. The source-consistency changes avoid mixing incompatible candles, but the sparse recovery loop still lacks a hard total request budget.

What needs attention: Confirm that one sparse-history recovery batch cannot generate unbounded sequential foreground-to-background requests when occasional candles repeatedly reset the empty-window counter.

Issues to address

  • P1 · Consecutive-empty cap still permits oversized recovery batches
    • Impact: Whenever a sparse series returns one candle before ten consecutive windows are empty, the counter resets to zero. A single recovery batch can therefore continue until it accumulates the target points or reaches the global boundary, potentially issuing hundreds or thousands of sequential foreground-to-background requests; each request may also retry.
    • Suggested action: Add a separate total request-attempt budget covering all scanned windows and retries. When the budget is exhausted, preserve the cursor and earlier-history flag so a later viewport event can resume recovery.

Validation gaps

  • git diff --check origin/x...HEAD completed successfully.
  • The targeted Jest suites could not start because the worktree lacks Yarn's node_modules state file.
  • Native chart interaction and request-volume behavior were not smoke-tested on iOS or Android.
  • GitHub review, inline-comment, and general-comment queries failed because the GitHub API was unreachable, so the inline draft requires a duplicate-comment check before posting.

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limichange

Automated code review found blocking issues

Reviewed commit 22d94a73be79.

  • P2 · Sparse recovery mixes fallback locator candles into primary Market pagination

View the full review report

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limichange

Automated code review found blocking issues

Reviewed commit 2c7b5bf9956c.

  • P1 · Sparse recovery can launch an unbounded request chain
  • P2 · Fallback locator candles can terminate primary Market history

View the full review report

@sidmorizon

Copy link
Copy Markdown
Contributor

@cursoragent review

@cursor

cursor Bot commented Aug 18, 2026

Copy link
Copy Markdown

Taking a look!

Open in Web Open in Cursor 

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limichange

Automated code review found blocking issues

Reviewed commit 28a154061a19.

  • P1 · Sparse recovery still has no request budget
  • P2 · Fallback boundary can truncate primary Market history

View the full review report

@limichange

Copy link
Copy Markdown
Contributor Author

@originalix Regarding P1 · Sparse recovery still has no request budget:

The recovery target is intentionally based on actual viewport points, not wall-clock slots. For a sparse series, 200 one-minute points can span much more than 200 minutes. The time cursor still advances only through fully covered, contiguous time blocks (next timeTo = previous timeFrom - 1), never from the earliest returned candle. A capped response is refined inside the same block, and every returned point is retained rather than trimmed to the target.

The scan is finite and cancellable: it stops after reaching the viewport point target or the refined earliest-history boundary, and it observes the request AbortSignal. Therefore this is not an unbounded correctness loop. We agree that a distant empty span can still produce many sequential requests, but that is a request-volume/performance tradeoff.

Applying the suggested hard budget and waiting for a later viewport event would reintroduce the previous sparse-initial-load regression: if a batch finds no points, the chart has no visible-range change to trigger the next recovery round, so it can remain permanently underfilled. We therefore should not add that hard-stop behavior as suggested. Request pressure can be improved separately with adaptive time-block expansion or internally scheduled batches while preserving automatic point filling and continuous time coverage.

Please consider downgrading this from a blocking P1; it does not indicate skipped data, an infinite loop, or an incorrect pagination boundary.

@sidmorizon sidmorizon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review


Generated by Claude Code

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limichange

Automated code review found blocking issues

Reviewed commit 1aba8e862786.

  • P1 · Sparse recovery still has no request budget
  • P2 · Fallback boundary can truncate primary Market history

View the full review report

Comment on lines +794 to +797
while (
cursorTimeTo >= boundaryTimestamp &&
points.length < normalizedTargetPointCount
) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 P1: Opening a 1-minute chart on a token with old but sparse data can fire hundreds of back-to-back price requests

Severity: severe

Older price data is walked backwards one fixed time window at a time with no limit on the number of requests (while loop at packages/kit/src/components/TradingView/TradingViewNative/data/useTradingViewNativeKLine.ts:794-797) until it either collects a screenful of candles or reaches the token's very first trading day, so a token whose minute data ends long before its first trading day triggers a very long chain of requests.
Impact: Charts for older, thinly traded tokens can hammer the price service with hundreds of sequential requests, leaving the chart busy for a long time and blocking other chart loading.

Why the walk is unbounded and how far it can run

recoverOlderHistoryFromBoundary first resolves the earliest available candle from the weekly/daily boundary page, then loops from timeTo downwards. Each iteration requests historyProvider.getHistoryRequestCandleCount(interval) candles worth of time (2000 for Market, i.e. ~33 hours for 1-minute candles, see MARKET_HISTORY_REQUEST_CANDLE_COUNT in packages/kit/src/components/TradingView/TradingViewNative/data/providers/market/marketDataProvider.ts:33). The only exit conditions are cursorTimeTo < boundaryTimestamp or points.length >= normalizedTargetPointCount. When the API has no minute candles for the older period (common when minute retention is much shorter than the token's history), every window returns zero points, the target is never reached, and the cursor only advances ~33 hours per request — e.g. a boundary 1 year older than the last minute candle produces ~260 sequential requests, each of which is additionally retried up to 3 times on failure via fetchRequiredHistoryPage (packages/kit/src/components/TradingView/TradingViewNative/data/useTradingViewNativeKLine.ts:549-570).

This path runs automatically right after the initial history load (packages/kit/src/components/TradingView/TradingViewNative/data/useTradingViewNativeKLine.ts:4079-4091), i.e. without any user interaction, and again on every older-page load. Note that all other scan loops in this file are explicitly bounded (HISTORY_GAP_EMPTY_SCAN_PAGE_COUNT, MAX_VIEWPORT_HISTORY_PAGE_COUNT, MAX_VIEWPORT_HISTORY_BOUNDARY_SEARCH_COUNT), and the PR description itself states each recovery round should be limited to about eight minute-history requests, which the code does not implement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已处理“连续全空区间可能一直请求”的情况:a17846ca28 增加了每轮最多 10 个连续空窗的停止条件,停止时保留 cursor 和 hasMoreBefore,后续 viewport load-more 可以从该 cursor 继续。bfe2ae18c4 进一步保证所有粒度共享同一个 history source,异源空页不会错误终止 primary 恢复。这里没有增加总窗口数上限;按当前产品语义,出现任意有效 K 线会重置连续空窗计数,以便继续找到真正稀疏的数据。

Comment on lines +756 to +765
const boundaryPage = await (getHistoryBoundaryPrefetchPage(seriesKey) ??
prefetchHistoryBoundaryPage({ historyProvider, seriesKey }));
if (
signal.aborted ||
!boundaryPage ||
boundaryPage.hasMoreBefore ||
boundaryPage.earliestTimestamp === undefined
) {
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earliest-date lookup is re-run from scratch on every sparse-history attempt when it cannot determine the first trading day

Severity: non-severe

The earliest-date lookup result is re-requested every time older data is recovered (getHistoryBoundaryPrefetchPage(seriesKey) ?? prefetchHistoryBoundaryPage(...) at packages/kit/src/components/TradingView/TradingViewNative/data/useTradingViewNativeKLine.ts:756-757) because inconclusive lookups are never remembered, so each attempt repeats up to twenty weekly data requests.
Impact: For tokens with very long histories, scrolling back repeatedly issues the same large batch of extra requests over and over, slowing chart loading.

Caching gap in prefetchHistoryBoundaryPage

prefetchHistoryBoundaryPage only writes to historyBoundaryPrefetchCache / historyBoundaryTimestampCache on the success path (packages/kit/src/components/TradingView/TradingViewNative/data/useTradingViewNativeKLine.ts:710-714). When the weekly scan exhausts MAX_VIEWPORT_HISTORY_PAGE_COUNT (20) pages with hasMoreBefore === true, it returns early at packages/kit/src/components/TradingView/TradingViewNative/data/useTradingViewNativeKLine.ts:652-660 without caching, and the in-flight entry is removed in the finally block. Previously this lookup only ran when the user opened the calendar; the new sparse-history recovery calls it after the initial minute-history load and on every subsequent older-page load, so the uncached 20-page weekly scan (plus retries) is repeated on each of those attempts even though its outcome cannot change within the cache TTL.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里暂不缓存 hasMoreBefore 仍为 true 的“不确定结果”,因为它不是一个可安全使用的最早历史边界,缓存后反而可能隐藏更早数据。weekly API 每页至少覆盖 200 根、合约币覆盖 299 根周 K;20 页相当于约 77–115 年历史,正常 Market 数据不会走到这个不确定分支。如果线上确实观察到该分支重复触发,可以单独增加带 TTL 的 negative/inconclusive cache,但不应把它当成真实 boundary。

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limichange

Automated code review found blocking issues

Reviewed commit 2f3ff51f35e0.

  • P1 · Sparse recovery still has no request budget
  • P2 · Fallback boundary can truncate primary Market history

View the full review report

@sidmorizon sidmorizon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review


Generated by Claude Code

});
consecutiveEmptyWindowCount = rangePoints.length
? 0
: consecutiveEmptyWindowCount + 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 P1 空窗上限被任意非空窗口清零,稀疏代币仍无请求上限

问题

新增的 MAX_SPARSE_HISTORY_CONSECUTIVE_EMPTY_WINDOW_COUNT = 10 只统计连续空窗,任意一个窗口返回哪怕 1 根 K 线就把计数清零:

consecutiveEmptyWindowCount = rangePoints.length
  ? 0
  : consecutiveEmptyWindowCount + 1;

窗口宽度是 getHistoryRequestCandleCount * interval.seconds,Market 为 2000 * 60 = 120000 秒(1m33.3 小时)与 2000 * 300 = 600000 秒(5m6.9 天)。也就是说该上限要求连续约 13.9 天(1m)或约 69.4 天(5m)完全没有任何分钟 K 线才会生效。而本 PR 针对的正是"稀疏"而非"整段为空"的分钟历史:长尾代币典型形态是每隔数天出现一次成交爆发、每次只有几根 1m K 线,这种数据每隔几个窗口就清零一次计数,主循环退出条件重新退回到 points.length >= normalizedTargetPointCountcursorTimeTo < boundaryTimestamp

影响

在稀疏但非全空的代币上,单轮 recovery 的请求数仍无上限。初始路径目标是 198 - receivedHistoryPointCount,若每个 33.3 小时窗口平均只有 1 根 K 线,凑满目标需要约 198 个串行窗口;若先到达 boundaryTimestamp,上限就是整段跨度除以 33.3 小时(边界在两年前时约 525 次)。这期间 pagination.isLoading 恒为 truehandleVisiblePointRangeChange 全程提前返回,缺口回填与新数据加载都被冻结。另外 fetchRequiredHistoryPage1s/3s 重试不计入窗口数,单个窗口失败时最多 3 次请求,因此即使命中上限,一轮的实际请求数也可达 30 次。

新增用例只覆盖全空窗口(stops a recovery batch after ten consecutive empty time windows)与"第 10、20 个窗口各返回 1 根"(resets the consecutive empty-window limit after receiving a candle,断言恰好 21 次请求),后者恰恰证明了清零语义会让请求数随稀疏程度线性增长,但没有任何用例给整轮请求数设上界。

建议

在连续空窗计数之外,再给整轮 recovery 加一个窗口总数上限,命中后同样以 hasMoreBefore: true 返回当前 cursorTimestamp,语义与现有空窗上限一致:

const MAX_SPARSE_HISTORY_RECOVERY_WINDOW_COUNT = 24;
let windowCount = 0;
while (
  cursorTimeTo >= boundaryTimestamp &&
  points.length < normalizedTargetPointCount &&
  consecutiveEmptyWindowCount < MAX_SPARSE_HISTORY_CONSECUTIVE_EMPTY_WINDOW_COUNT &&
  windowCount < MAX_SPARSE_HISTORY_RECOVERY_WINDOW_COUNT
) {
  windowCount += 1;
  // ...
}

或对空窗采用倍增窗口宽度(每次空窗把 requestCandleCount2,非空时复位),使跨越长空档从线性降到对数级。回归用例建议用真实的 getHistoryRequestCandleCount = 2000、边界在数年前、每隔 9 个窗口返回 1 根 K 线,断言 mockFetchHistory 调用次数不超过该上限。

Claude session ↗


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个行为是有意保留的产品取舍:单轮按实际 K 线根数补足下一屏,连续 10 个空窗才暂停;任意非空窗口会重置计数。增加固定总窗口上限会在仍能找到稀疏 K 线时提前停止,而且初始图表未填满时不一定会产生新的 viewport 事件来自动续跑。当前实现仍是有限且可取消的:达到目标、真实边界或 10 个连续空窗都会退出,并保留 cursor 供后续 load-more。bfe2ae18c4 没有改变这一语义。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你对固定总窗口上限的反对成立,我接受:命中上限时若初始图尚未填满,确实可能没有新的 viewport 事件来续跑,把「请求过多」换成「图表欠填且不自愈」是更差的结果。这一点我原建议里没有考虑到。

不过这条反对只适用于固定上限,不适用于我在另一条线程提过、你自己也提到过的倍增窗口方案,这条线程里还没讨论过它:空窗时把 requestCandleCount2(非空即复位),不设任何提前停止条件。退出条件仍然只有目标点数、真实边界、10 个连续空窗三个,所以不存在提前停止导致欠填的问题;差别只是跨越空档的请求数从线性降到对数级——1m 下两年空档从约 525 次降到约 10 次,且因为窗口只在空窗后才扩大,稀疏但有数据的区间宽度不变,不会跳过任何 K 线。

代价是单个请求的时间跨度变大,若某个扩大后的窗口恰好命中密集区,返回可能被 299/200 截断;此时按返回的最早 K 线设游标并复位窗口宽度即可,不会丢数据。

不主张一定要改——按当前语义它是有限且可取消的,这是你的取舍。只是想说明「提前停止」这一条不构成对倍增方案的反对,如果你之前是基于这一点一并排除了它,可以再单独评估一次。

Claude session ↗


Generated by Claude Code

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limichange

Automated code review found blocking issues

Reviewed commit a17846ca28a8.

  • P1 · Consecutive-empty cap still permits oversized recovery batches
  • P2 · Daily fallback is cached as a primary history boundary
  • P1 · A rejected fallback page ends primary sparse recovery

View the full review report

@limichange

Copy link
Copy Markdown
Contributor Author

@originalix Addressed the two history-source transition findings in bfe2ae1:

  • History-source ownership is now per provider/series and shared by every interval. Once primary wins, weekly and daily requests cannot fall back independently; once fallback wins, every interval stays on fallback.
  • Source selection is first-winner safe across concurrent interval requests. A later mismatched response becomes an empty response with the already-selected provenance.
  • Therefore a rejected fallback page after primary selection is treated as a primary empty window and sparse recovery continues, while a fallback daily page can no longer be cached as a primary boundary.
  • Added regressions for cross-interval primary/fallback locking, concurrent selection, and continued sparse recovery after a dropped fallback response.

Validation: focused provider and K-line suites pass 101/101; yarn agent:check --profile commit passes lint, format, and TypeScript checks.

The separate total-window budget was intentionally not added. The current product rule remains: scan fixed candle-count windows and pause after 10 consecutive empty windows; any non-empty window resets that counter. This preserves discovery of genuinely sparse candles while bounding fully empty gaps. Please re-review bfe2ae1.

@originalix originalix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@limichange

Automated code review found blocking issues

Reviewed commit bfe2ae18c4e4.

  • P1 · Consecutive-empty cap still permits oversized recovery batches

View the full review report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants