Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ config.toml
# Benchmark artifacts
benchmark/results/
benchmark/data/

# Live capture output dirs
live_capture_*/
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **Automatic HTTP retries on the Historical API.** Transient failures — HTTP
`429` (rate limit) and `5xx`, plus connection/timeout errors — are now retried
with full-jitter exponential backoff. A `Retry-After` response header, when
present, overrides the computed backoff (capped to guard against hostile
values). Configurable via the new `max_retries` keyword on `Historical(...)`
(default 3); the retry budget, backoff, and sleep hook are all injectable for
testing. Once the budget is exhausted the final status is mapped to its
`BentoClientError`/`BentoServerError` as before.

### Changed
- **Breaking: renamed the time-range keyword arguments to `start_dt` / `end_dt`.**
The old `start` / `end_` pair was asymmetric (the trailing underscore only
existed because `end` is a Julia reserved word). All affected entry points are
updated: `get_range`, `foreach_record`, `submit_job`, `get_record_count`,
`get_billable_size`, `get_cost`, and the Live `subscribe!` / `live_session`
subscription / `stream_to_file` / `stream_multi_to_files` `start` keyword
(now `start_dt`). The Databento wire parameters (`start` / `end`) are
unchanged. Update call sites from `start = …, end_ = …` to
`start_dt = …, end_dt = …`.
- **`foreach_record` now applies real backpressure.** The streaming download
previously drained the connection into an unbounded `Base.BufferStream` on a
background task, which could buffer the entire compressed payload in memory and
left the producer task running (blocking until server EOF/timeout) when the
consumer aborted or finished early. It now reads the response body synchronously
on the calling task, so records are pulled from the socket only as fast as the
consumer processes them, and the connection is torn down deterministically on
every exit path — including an early return or an exception out of the callback.

## [0.1.2] - 2026-06-01

### Added
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ store = get_range(client;
dataset = "XNAS.ITCH",
schema = Schema.TRADES,
symbols = ["AAPL"],
start = DateTime(2024, 1, 2, 14, 30),
end_ = DateTime(2024, 1, 2, 14, 31),
start_dt = DateTime(2024, 1, 2, 14, 30),
end_dt = DateTime(2024, 1, 2, 14, 31),
stype_in = SType.RAW_SYMBOL)
df = to_dataframe(store)

Expand All @@ -64,7 +64,7 @@ for details.

## Status

Current version: **0.1.1**. Offline test suite: **1500+ tests**, ~30 s.
Current version: **0.1.2**. Offline test suite: **1500+ tests**, ~30 s.
Live-network smoke tests are gated behind `DATABENTO_LIVE_TESTS=1`. See
[CHANGELOG.md](CHANGELOG.md) for the full release history.

Expand Down
4 changes: 2 additions & 2 deletions benchmark/_fetch_replay_fixture.jl
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ function main(args = ARGS)
schema = Schema.CMBP_1,
symbols = split(opts["symbols"], ','),
stype_in = SType.PARENT,
start = opts["start"],
end_ = opts["end"],
start_dt = opts["start"],
end_dt = opts["end"],
encoding = "dbn", compression = "zstd")
job_id = String(job["id"])
println(" job_id = ", job_id, " cost_usd = ", job["cost_usd"])
Expand Down
4 changes: 2 additions & 2 deletions benchmark/bench_historical_decode.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ function _get_range_call(c)
store = DatabentoAPI.get_range(c;
dataset = "TEST.MOCK", schema = Schema.TRADES,
symbols = ["AAPL"],
start = "2024-01-02T14:30:00",
end_ = "2024-01-02T14:31:00",
start_dt = "2024-01-02T14:30:00",
end_dt = "2024-01-02T14:31:00",
stype_in = SType.RAW_SYMBOL,
stype_out = SType.INSTRUMENT_ID,
)
Expand Down
16 changes: 8 additions & 8 deletions docs/src/guide/historical.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ store = get_range(client;
dataset = "XNAS.ITCH",
schema = Schema.TRADES,
symbols = ["AAPL", "MSFT"],
start = DateTime(2024, 1, 2, 14, 30),
end_ = DateTime(2024, 1, 2, 15, 0),
start_dt = DateTime(2024, 1, 2, 14, 30),
end_dt = DateTime(2024, 1, 2, 15, 0),
stype_in = SType.RAW_SYMBOL)

length(store.records) # Vector{DBN.DBNRecord}
Expand All @@ -57,8 +57,8 @@ n_trades = Ref(0)
foreach_record(client, DBN.TradeMsg;
dataset = "XNAS.ITCH", schema = Schema.TRADES,
symbols = ["AAPL"],
start = DateTime(2024, 1, 2, 14, 30),
end_ = DateTime(2024, 1, 2, 20, 0)) do trade
start_dt = DateTime(2024, 1, 2, 14, 30),
end_dt = DateTime(2024, 1, 2, 20, 0)) do trade
n_trades[] += 1
end
@show n_trades[]
Expand All @@ -75,15 +75,15 @@ Before issuing a large `get_range`, preview the query:

```julia
get_record_count(client; dataset = "XNAS.ITCH", schema = Schema.TRADES,
symbols = ["AAPL"], start = "2024-01-02", end_ = "2024-01-03")
symbols = ["AAPL"], start_dt = "2024-01-02", end_dt = "2024-01-03")
# → integer record count

get_billable_size(client; dataset = "XNAS.ITCH", schema = Schema.TRADES,
symbols = ["AAPL"], start = "2024-01-02", end_ = "2024-01-03")
symbols = ["AAPL"], start_dt = "2024-01-02", end_dt = "2024-01-03")
# → integer billable bytes

get_cost(client; dataset = "XNAS.ITCH", schema = Schema.TRADES,
symbols = ["AAPL"], start = "2024-01-02", end_ = "2024-01-03",
symbols = ["AAPL"], start_dt = "2024-01-02", end_dt = "2024-01-03",
mode = FeedMode.HISTORICAL)
# → USD cost
```
Expand All @@ -108,7 +108,7 @@ Submit an asynchronous job, poll for completion, then download:
job = submit_job(client;
dataset = "XNAS.ITCH", schema = Schema.MBO,
symbols = ["AAPL"],
start = "2024-01-02", end_ = "2024-01-09")
start_dt = "2024-01-02", end_dt = "2024-01-09")

job_id = job["id"]

Expand Down
4 changes: 2 additions & 2 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ store = get_range(client;
dataset = "XNAS.ITCH",
schema = Schema.TRADES,
symbols = ["AAPL"],
start = DateTime(2024, 1, 2, 14, 30),
end_ = DateTime(2024, 1, 2, 14, 31),
start_dt = DateTime(2024, 1, 2, 14, 30),
end_dt = DateTime(2024, 1, 2, 14, 31),
stype_in = SType.RAW_SYMBOL)
df = to_dataframe(store)

Expand Down
4 changes: 2 additions & 2 deletions docs/src/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ store = get_range(client;
dataset = "XNAS.ITCH",
schema = Schema.TRADES,
symbols = ["AAPL"],
start = DateTime(2024, 1, 2, 14, 30),
end_ = DateTime(2024, 1, 2, 14, 31),
start_dt = DateTime(2024, 1, 2, 14, 30),
end_dt = DateTime(2024, 1, 2, 14, 31),
stype_in = SType.RAW_SYMBOL)

df = to_dataframe(store)
Expand Down
6 changes: 3 additions & 3 deletions docs/src/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ If `get_range` exhausts memory or returns extremely slowly, switch to
Always preview before issuing a large `get_range`:

```julia
get_record_count(client; dataset, schema, symbols, start, end_)
get_billable_size(client; dataset, schema, symbols, start, end_)
get_cost(client; dataset, schema, symbols, start, end_, mode = FeedMode.HISTORICAL)
get_record_count(client; dataset, schema, symbols, start_dt, end_dt)
get_billable_size(client; dataset, schema, symbols, start_dt, end_dt)
get_cost(client; dataset, schema, symbols, start_dt, end_dt, mode = FeedMode.HISTORICAL)
```

All three are free and run server-side.
Expand Down
4 changes: 2 additions & 2 deletions src/DatabentoAPI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ store = get_range(client;
dataset = "XNAS.ITCH",
schema = Schema.TRADES,
symbols = ["AAPL"],
start = DateTime(2024, 1, 2, 14, 30),
end_ = DateTime(2024, 1, 2, 14, 31),
start_dt = DateTime(2024, 1, 2, 14, 30),
end_dt = DateTime(2024, 1, 2, 14, 31),
stype_in = SType.RAW_SYMBOL)
df = to_dataframe(store)

Expand Down
4 changes: 2 additions & 2 deletions src/conversion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ paths; for large stores prefer running `DBN.dbn_to_csv` directly on a captured

```julia
store = get_range(client; dataset = "XNAS.ITCH", schema = Schema.TRADES,
symbols = ["AAPL"], start = DateTime(2024,1,2,14,30),
end_ = DateTime(2024,1,2,14,31), stype_in = SType.RAW_SYMBOL)
symbols = ["AAPL"], start_dt = DateTime(2024,1,2,14,30),
end_dt = DateTime(2024,1,2,14,31), stype_in = SType.RAW_SYMBOL)
to_csv(store, "trades.csv")
```
"""
Expand Down
4 changes: 3 additions & 1 deletion src/errors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ end

Raised when an HTTP request to the Historical API returns a 5xx status
(gateway timeout, internal error, etc.). Same field layout as
[`BentoClientError`](@ref); retrying after backoff is usually appropriate.
[`BentoClientError`](@ref). The client already retries 5xx (and 429) with
backoff up to its `max_retries` budget, so seeing this means the server kept
failing across every retry.
"""
struct BentoServerError <: BentoHttpError
status::Int
Expand Down
10 changes: 5 additions & 5 deletions src/historical/batch.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Batch endpoints — submit asynchronous jobs and download results.

"""
submit_job(client; dataset, symbols, schema, start, end_=nothing, ...) -> Dict
submit_job(client; dataset, symbols, schema, start_dt, end_dt=nothing, ...) -> Dict

Submit an asynchronous batch job. The Historical API queues the request and
returns a `job_id`; poll [`list_jobs`](@ref) until the [`JobState`](@ref) is
Expand Down Expand Up @@ -29,8 +29,8 @@ function submit_job(c::Historical;
dataset::AbstractString,
symbols,
schema::Schema.T,
start,
end_ = nothing,
start_dt,
end_dt = nothing,
encoding::Union{Encoding.T,AbstractString} = "dbn",
compression::Union{Compression.T,AbstractString} = "zstd",
stype_in::SType.T = SType.RAW_SYMBOL,
Expand All @@ -54,8 +54,8 @@ function submit_job(c::Historical;
dataset = String(dataset),
symbols = symbols_str(symbols),
schema = schema_str(schema),
start = ts_str(start),
end_ = ts_str(end_),
start = ts_str(start_dt),
end_ = ts_str(end_dt),
encoding = enc_v,
compression = cmp_v,
stype_in = stype_str(stype_in),
Expand Down
14 changes: 10 additions & 4 deletions src/historical/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Client for the Databento Historical (HTTP) API.
```julia
client = Historical()
store = get_range(client; dataset="XNAS.ITCH", schema=Schema.TRADES,
symbols=["AAPL"], start=DateTime(2024,1,2,14,30),
end_=DateTime(2024,1,2,14,31), stype_in=SType.RAW_SYMBOL)
symbols=["AAPL"], start_dt=DateTime(2024,1,2,14,30),
end_dt=DateTime(2024,1,2,14,31), stype_in=SType.RAW_SYMBOL)
```
"""
struct Historical
Expand All @@ -25,13 +25,19 @@ function Historical(key::Union{Nothing,AbstractString} = nothing;
timeout::Integer = DEFAULT_TIMEOUT,
connect_timeout::Integer = DEFAULT_CONNECT_TIMEOUT,
user_agent::AbstractString = USER_AGENT,
dispatcher::Function = _default_http_request)
dispatcher::Function = _default_http_request,
max_retries::Integer = DEFAULT_MAX_RETRIES,
retry_sleep::Function = sleep,
stream_opener::Function = _default_http_stream)
api_key = load_api_key(key)
return Historical(HTTPClient(api_key, gateway;
timeout = timeout,
connect_timeout = connect_timeout,
user_agent = user_agent,
dispatcher = dispatcher))
dispatcher = dispatcher,
max_retries = max_retries,
retry_sleep = retry_sleep,
stream_opener = stream_opener))
end

Base.show(io::IO, c::Historical) =
Expand Down
32 changes: 16 additions & 16 deletions src/historical/metadata.jl
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ list_unit_prices(c::Historical; dataset::AbstractString) =
get_dataset_range(client; dataset)

Earliest and latest available timestamps for `dataset`. Use the result to
bound `start`/`end_` on subsequent [`get_range`](@ref) or [`submit_job`](@ref)
bound `start_dt`/`end_dt` on subsequent [`get_range`](@ref) or [`submit_job`](@ref)
calls. Wraps `GET /v0/metadata.get_dataset_range`.
"""
get_dataset_range(c::Historical; dataset::AbstractString) =
Expand All @@ -89,7 +89,7 @@ function get_dataset_condition(c::Historical;
end

"""
get_record_count(client; dataset, symbols, schema, start, end_=nothing,
get_record_count(client; dataset, symbols, schema, start_dt, end_dt=nothing,
stype_in=SType.RAW_SYMBOL, limit=nothing)

Count of records the same query would return from [`get_range`](@ref) without
Expand All @@ -101,17 +101,17 @@ function get_record_count(c::Historical;
dataset::AbstractString,
symbols,
schema::Schema.T,
start,
end_ = nothing,
start_dt,
end_dt = nothing,
stype_in::SType.T = SType.RAW_SYMBOL,
limit::Union{Nothing,Integer} = nothing)
qpairs = _clean_params((
dataset = String(dataset),
symbols = symbols_str(symbols),
schema = schema_str(schema),
stype_in = stype_str(stype_in),
start = ts_str(start),
end_ = ts_str(end_),
start = ts_str(start_dt),
end_ = ts_str(end_dt),
limit = limit,
))
for (i, (k, v)) in enumerate(qpairs)
Expand All @@ -121,7 +121,7 @@ function get_record_count(c::Historical;
end

"""
get_billable_size(client; dataset, symbols, schema, start, end_=nothing,
get_billable_size(client; dataset, symbols, schema, start_dt, end_dt=nothing,
stype_in=SType.RAW_SYMBOL, limit=nothing)

Billable size in bytes (uncompressed CSV-equivalent) the same query would
Expand All @@ -133,17 +133,17 @@ function get_billable_size(c::Historical;
dataset::AbstractString,
symbols,
schema::Schema.T,
start,
end_ = nothing,
start_dt,
end_dt = nothing,
stype_in::SType.T = SType.RAW_SYMBOL,
limit::Union{Nothing,Integer} = nothing)
qpairs = _clean_params((
dataset = String(dataset),
symbols = symbols_str(symbols),
schema = schema_str(schema),
stype_in = stype_str(stype_in),
start = ts_str(start),
end_ = ts_str(end_),
start = ts_str(start_dt),
end_ = ts_str(end_dt),
limit = limit,
))
for (i, (k, v)) in enumerate(qpairs)
Expand All @@ -153,7 +153,7 @@ function get_billable_size(c::Historical;
end

"""
get_cost(client; dataset, symbols, schema, start, end_=nothing,
get_cost(client; dataset, symbols, schema, start_dt, end_dt=nothing,
mode=nothing, stype_in=SType.RAW_SYMBOL, limit=nothing)

Dollar cost preview for the same query as [`get_range`](@ref) under the
Expand All @@ -165,8 +165,8 @@ function get_cost(c::Historical;
dataset::AbstractString,
symbols,
schema::Schema.T,
start,
end_ = nothing,
start_dt,
end_dt = nothing,
mode::Union{Nothing,FeedMode.T,AbstractString} = nothing,
stype_in::SType.T = SType.RAW_SYMBOL,
limit::Union{Nothing,Integer} = nothing)
Expand All @@ -177,8 +177,8 @@ function get_cost(c::Historical;
symbols = symbols_str(symbols),
schema = schema_str(schema),
stype_in = stype_str(stype_in),
start = ts_str(start),
end_ = ts_str(end_),
start = ts_str(start_dt),
end_ = ts_str(end_dt),
mode = mode_v,
limit = limit,
))
Expand Down
Loading
Loading