Skip to content

historical: caller-owned requests via historical_data(..).subscribe() - #1189

Merged
wboayue merged 3 commits into
mainfrom
historical-subscribe
Nov 5, 2026
Merged

wboayue merged 3 commits into
mainfrom
historical-subscribe

Conversation

@wboayue

@wboayue wboayue commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Closes #1167. Final PR of the series, after #1186, #1187 and #1188.

Why. The reporter's worker owns a finite, dated historical request. It needs the request id before anything is written, exactly one send, the raw frames kept across interrupted waits, and an explicit cancel it can settle. fetch() retries on reset under new ids and keeps its subscription internal. stream() sets keepUpToDate and rejects ending/between. As agreed on the issue, this reuses the builder and the existing Subscription, with no separate request type.

What (sync and async):

  • historical_data(..).request_id(): allocated when the builder is made, like contract_details_stream. fetch()'s first attempt and stream() now send under it, and a resend after a reset takes a new id, so request-id numbering is unchanged.
  • historical_data(..).subscribe(): sends once and never retries. ending/between and buffer_limit apply. Returns Subscription<HistoricalDataFrame>.
  • HistoricalDataFrame is public: Bars(HistoricalData), then End { start, end }. It derives Clone and PartialEq and stays exhaustive.
  • The rustdoc covers one send, trimming with HistoricalData::trim_to (historical: public HistoricalData::trim_to #1188) after between, a drop before End cancelling, and cancel_and_drain. The examples read with next() under the caller's own deadline, keeping bars across a timed-out wait.
  • Internal: a pub(crate) BarRequest with one encode method replaces six copies of the 8-argument encode call.
  • Examples: historical_data_subscribe (sync) and async_historical_data_subscribe.

Live (paper, server 226, after hours). I cancelled a running 300-day dated request right after sending it. Within about 50 ms TWS sent 162 "API historical data query cancelled: ": no bars and no end marker, 3 of 3 runs, sync and async. So cancel_and_drain returns Rejected(162), which confirms TWS is done. This is the check promised on the issue, recorded in docs/wire-facts/protocol.md and in the rustdoc. Not tested: a cancel that crosses bars already in flight, and RTH.

Verified

  • New tests (5 per client) in builder/data_tests.rs:
    • one send under request_id(), with keep_up_to_date = false, the between end date, and buffer_limit passed through;
    • Bars then End, then the stream ends, with no cancel on drop;
    • a drop before End sends one cancel;
    • a 162 read and then drained gives Rejected with no cancel;
    • a connection reset is not resent.
  • Not unit-tested: resuming after a timed-out next(), because the stub queues all frames up front. next()'s cancel-safety is a Subscription property, and both examples exercised it live.
  • Both examples ran against paper: a normal run (AAPL, 42 bars trimmed to 28) and --timeout 0 (Rejected(162)).
  • cargo fmt; clippy, rustdoc (-D warnings) and cargo build --examples on all three feature configs; just test (7191 passed); integration crates build and pass clippy; just rules-check.

fetch() retries on reset under new ids and hides its subscription; stream()
keeps bars up to date and rejects ending/between. A caller that owns a
finite dated request (#1167) needs its id up front, one send, the raw
frames, and cancel_and_drain to settle it.

- historical_data(..).request_id(): allocated with the builder. fetch()'s
  first attempt and stream() now send under it.
- historical_data(..).subscribe(): one send, no retry, ending/between and
  buffer_limit apply; yields the now-public HistoricalDataFrame.
- BarRequest folds the six copies of the encode call.
- Wire fact: TWS answers a cancel of a running dated request with 162
  "query cancelled" (live, server 226).
- Examples historical_data_subscribe / async_historical_data_subscribe.
@tradatious

Copy link
Copy Markdown
Contributor

Suggestion before merge: HistoricalDataFrame::Bars could carry Vec<Bar> instead of HistoricalData, since its start/end are epoch placeholders and the window comes in End. HistoricalBarUpdate::Historical has the same shape; I have a 6.0 PR ready that changes it to Vec<Bar>. If you'd rather merge this as is, I'll change both there.

@wboayue
wboayue merged commit bb947c5 into main Nov 5, 2026
7 checks passed
@wboayue
wboayue deleted the historical-subscribe branch November 5, 2026 22:13
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.

Historical data: expose finite request ownership and observable cancellation

2 participants