trader.persistence.bars module¶
Repository for cached OHLCV bars and their coverage ranges.
- class trader.persistence.bars.BarRepository(session_factory)[source]¶
Bases:
objectReads and writes cached bars, and tracks which ranges were fetched.
- Parameters:
session_factory (sessionmaker[Session])
- save_bars(symbol, interval, bars, source=None)[source]¶
Insert bars, skipping any timestamp already stored. Returns inserted count.
source (issue #41) tags every row this call inserts — None for the live path (BarCache, which never passes it), a vendor tag such as seed_import.SOURCE_YFINANCE_SEED for trader seed-bars. It is per-call, not per-bar: one call always describes bars pulled from one file of one known provenance, so there is nothing to gain from letting individual Bar`s disagree, and the domain `Bar type itself carries no such field — source is a fact about how the row entered this table, not a fact yfinance or Interactive Brokers ever reports back.
- Parameters:
symbol (str)
interval (str)
bars (Sequence[Bar])
source (str | None)
- Return type:
int
- delete_bars(symbol, interval, start, end)[source]¶
Delete cached bars in the window. Returns how many were removed.
Required for refresh: save_bars deliberately skips timestamps it already holds, so re-fetching revised data would otherwise be a no-op that still stamps a fresh fetched_at.
Scoped to source IS NULL (issue #41) — a seeded row (non-NULL source) is never deleted by this method, at any call site, including the trading path’s automatic tail refresh (BarCache._refresh_tail_or_degrade) and the operator-invoked trader fetch-bars –refresh. Seed data is imported once, from a file that will not change; the trading path’s own tail-refresh policy exists specifically because its bars can still be corrected by a later fetch, which is not true of a historical import. Without this guard, a live refresh window that happened to overlap an imported symbol’s dates would silently replace validated backtest input with a differently-adjusted live fetch — exactly what the seed importer exists to avoid needing in the first place.
- Parameters:
symbol (str)
interval (str)
start (datetime)
end (datetime)
- Return type:
int
- load_bars(symbol, interval, start, end)[source]¶
Return cached bars in the window, oldest first.
- Parameters:
symbol (str)
interval (str)
start (datetime)
end (datetime)
- Return type:
list[Bar]
- covered_ranges(symbol, interval)[source]¶
Merged, sorted ranges known to have been fetched.
- Parameters:
symbol (str)
interval (str)
- Return type:
list[tuple[datetime, datetime]]
- record_coverage(symbol, interval, start, end)[source]¶
Record a fetched range, merging it into any it overlaps or abuts.
Read, delete, and insert happen in one transaction: splitting them lets two concurrent callers each rewrite the row set from a stale read, and the later commit silently discards the other’s range.
- Parameters:
symbol (str)
interval (str)
start (datetime)
end (datetime)
- Return type:
None
- missing_ranges(symbol, interval, start, end)[source]¶
Sub-ranges of [start, end] that have never been fetched.
The window is snapped to the bar grid first, so the answer depends only on which bars are wanted and not on the clock time of the call.
Adjacency is one bar-width, not one day. Two intraday ranges an hour apart are genuinely not contiguous, and merging them would report the hole between as covered — after which nothing would ever re-fetch it.
- Parameters:
symbol (str)
interval (str)
start (datetime)
end (datetime)
- Return type:
list[tuple[datetime, datetime]]
- trader.persistence.bars.bar_width(interval)[source]¶
How long one bar of interval spans.
Used for two things that must agree: snapping coverage onto the bar grid, and deciding whether two coverage ranges abut. They were allowed to disagree until 2026-08-04, when coverage recorded at instant precision against midnight-dated daily bars made missing_ranges answer differently depending on the time of day, and a live limit order was priced off the previous day’s close.
- Raises:
ValueError – the interval is not one this repository can reason about. A guessed width silently corrupts coverage, which is worse than refusing, so there is no default.
- Parameters:
interval (str)
- Return type:
timedelta