Skip to content

fix: revalidate the vault price history cache on live universe loads - #1583

Open
miohtama wants to merge 1 commit into
masterfrom
fix/vault-history-revalidate
Open

fix: revalidate the vault price history cache on live universe loads#1583
miohtama wants to merge 1 commit into
masterfrom
fix/vault-history-revalidate

Conversation

@miohtama

Copy link
Copy Markdown
Collaborator

Why

A live hyper-ai executor failed to start with DataTooOld:

tradeexecutor.strategy.universe_model.DataTooOld: Candle data 2023-03-01 - 2026-07-24 is too old to work with
we require threshold 2026-07-25, diff is 1 day, asked best before duration is 2 days

All 327 Hyperliquid vaults reported a newest source timestamp of 2026-07-24 14:1x,
three days before the 12:10 crash. The data itself was fine — checking the source
directly, cleaned-vault-prices-1h.parquet had last-modified: 2026-07-27 09:53:55 and
carried rows up to 09:32 that morning, including for the exact vaults in the log
(HLP 0xdfc2…f30309:30:06, Citadel 0xda51…c16709:30:07). The executor was
reading a stale local cache.

fetch_vault_price_history() trusts a cached parquet for 24 hours with no remote check:

  • Inside the window no HEAD request is made, so a parquet published after the last
    check is not picked up, and nothing reports that the local copy has fallen behind.
    Reproduced in isolation: cache mtime 23h old, remote newer with a different ETag and
    size → no HEAD, no download, stale file served.
  • The window is measured from the last validation, not the last download, because a
    HEAD check that finds the remote unchanged calls os.utime(path, None).

Scope note: a 24h window bounds staleness at roughly 24h plus pipeline lag, so this alone
does not account for a three-day gap. This is hardening of a real defect, not a confirmed
root cause. Closing out the remaining gap needs the executor's own
Vault price history cache hit/expired and Vault history freshness summary lines from
the incident, which were not in the captured output. A second candidate is still open:
_is_matching_http_cache_metadata() falls back to remote_last_modified <= local_mtime
even when sidecar metadata disagrees, which is unsound once mtime has been bumped —
currently masked by the Content-Length guard, and left for a follow-up.

Lessons learnt

  • An mtime-keyed cache window silently changes meaning once anything touches mtime for
    bookkeeping. It stops reading as "age of the data" and starts meaning "time since we
    last looked", which is not what a bare timedelta(hours=24) conveys.
  • A cache whose expiry is invisible in the happy path gives no signal when it goes wrong.
    The blind window emitted no log line; the staleness surfaced three layers downstream as
    a DataTooOld crash behind a 300-line table dump, and a Sentry Message too long
    logging error on the way.
  • Freshness policy belongs to the caller, not the transport. A backtest wants the cheap
    cached path; a live executor must not guess. Same function, opposite requirements.
  • Verifying "is the upstream data actually stale?" against the source took one HEAD
    request and one parquet read, and ruled out the entire data-pipeline branch of the
    investigation immediately. Worth doing before reading any cache code.

Summary

  • load_partial_data() passes revalidate=execution_context.mode.is_live_trading() to
    client.transport.fetch_vault_price_history(), and to the client.fetch_vault_price_history()
    fallback branch, so live universe loads always validate the cache against the remote.
    Backtests keep the plain cached path.
  • Bumps the deps/trading-strategy submodule to pick up the revalidate flag.
  • One new test, test_load_partial_data_revalidates_vault_history_cache_only_when_live,
    captures the flag the transport receives and asserts True for real_trading and
    False for backtesting.
  • Updates the vault-data pipeline notes in test_hyper_ai_stale_data_indicators.py,
    including dropping a stale cache.py:625 line reference.
  • Existing stubs in test_vault_live_cutoff.py accept the new keyword.

Verified against the live server: with a 25-minute-old cache — deep inside the window
that produced the incident — the HEAD now runs and correctly short-circuits the 197 MB
download because the remote is unchanged.

Merge order: tradingstrategy-ai/trading-strategy#243 must land first; the submodule
pointer here should then be repointed at the merged commit rather than the branch commit.

🤖 Generated with Claude Code

A live executor failed to start with DataTooOld while holding a cached
vault-price-history.parquet whose newest rows were three days old, at a moment
when the remote file contained rows from two hours earlier.

The transport trusts a cached parquet for 24 hours with no remote check.
Inside that window no HEAD request is made at all, so a parquet published after
the last check is not picked up and nothing reports that the local copy has
fallen behind. The window is also measured from the last validation rather than
the last download, because a HEAD check that finds the remote unchanged bumps
the cached file's mtime.

Pass revalidate=execution_context.mode.is_live_trading() from load_partial_data()
so live universe loads always validate against the remote. An unchanged remote
still short-circuits the download, so this costs one HEAD request rather than a
~200 MB re-download. Backtests keep the plain cached path.

Requires tradingstrategy-ai/trading-strategy#243.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant