Market database (market.db)#
Daily price history for every instrument. It is the fastest-growing database
(rows scale with symbols times days), which is why it is isolated and can be
archived or partitioned independently. Models:
src/trade_engine/models/market.py; data access:
repositories/market.py; loading: services/market_data.py.
prices_eod: one daily OHLC bar per symbol#
Column |
Type |
Reason |
|---|---|---|
|
integer PK |
Surrogate key. |
|
string(32), indexed |
Business key (no foreign key to another database). |
|
date, indexed |
Daily grain. A |
|
float, not null |
Validated before insert (see below). |
|
string(128), not null |
Free-form provenance detail: vendor, file or generator label, e.g. |
|
string(16), not null, |
Whether the bar is real or generated. No default, so it is always a deliberate choice. |
|
datetime, database default |
When the bar entered the system, for audit and correction analysis. |
Constraint: unique (symbol, px_date) (uq_prices_eod_symbol_px_date).
This is the idempotency guarantee for reloads, and it makes “latest close at or
before date D” a single index seek. It is also the look-ahead guard: queries
never read beyond the calculation date.
Loading rules#
MarketDataService.load_csv(symbol, csv_path, origin, source) stages the file in
Redis (stage_csv) and then processes the queue (process_staged); the two
steps can also run separately with scripts/load_market_data.py --stage-only and
scripts/process_staging.py. See Data architecture (Data tiers).
Provenance first.
originis mandatory. A file underdata/generated/can only be loaded asSIMULATED.sourcemust be 1 to 128 characters. This is checked when the file is staged. Rows are staged as the original strings, without price validation.Validate the whole batch when it is processed (
domain/market_data.py) before writing anything:required columns
date, open, high, low, close(names are case-insensitive);rows for other symbols are filtered out when a
symbolcolumn exists, and a file with no rows for the requested symbol is an error;dates and prices must parse, be finite, and prices must be positive;
duplicate dates are an error (never silently de-duplicated);
high >= max(open, close)andlow <= min(open, close).
Insert only new dates. Dates already stored for the symbol are skipped, so reloading is safe. The result reports
loadedandskippedcounts.All or nothing. The insert runs in one transaction; an invalid batch stores nothing and is moved to the rejected stream in Redis with the reason, so
load_csvraisesDataValidationErrorfor it.
Caution: because existing dates are skipped, loading
OBSERVEDdata for a symbol and dates that already holdSIMULATEDrows does not replace them. Use a different symbol or a fresh database until price versioning exists (see Simulated data: why and how and the roadmap, item 9).
Query patterns#
Repository method |
Purpose |
|---|---|
|
Mark price: most recent close at or before |
|
Chronological closes up to |
|
Default EOD date (latest bar across all symbols). |
|
Most recent bars across symbols, for the market view. |
Bid, ask and mid (planned)#
Status: Required before relying on bid/ask-sensitive results; not implemented. Today
prices_eodholds one OHLC series with an unstated basis. Everything below describes the intended design (roadmap item 15).
What each basis is for#
|
Meaning |
Used for |
|---|---|---|
|
Price at which the market buys from you (you sell) |
Sell fills; marking long positions at liquidation value |
|
Price at which the market sells to you (you buy) |
Buy fills; marking short positions at liquidation value |
|
|
Reference valuation, unrealized P&L, VaR returns, limit notional, signals |
|
Last traded price (exchange-traded instruments with no quoted bid/ask bar) |
Equities, futures and exchange crypto where only trades are available |
Schema change#
Add one column and widen the unique key, so a symbol and date can hold one bar per
basis (long format, not eight sparse bid_*/ask_* columns):
Change |
Reason |
|---|---|
|
Same rule as |
Unique |
One bar per basis per day; reloading a basis stays idempotent. |
Existing rows are backfilled as |
Existing data keeps its meaning and no history is lost. |
A long format keeps vendors that supply only one basis (for example bid-only
retail history) representable, and adding a basis later needs no new columns.
One query reads several bases with a join or a GROUP BY on px_date; the
DuckDB analytics export keeps price_basis so it can be pivoted there.
Loading#
--price-basis BID|ASK|MID|LASTis required, exactly like--origin, and aprice_basisfield is staged with each batch. One file carries one basis.A vendor that supplies both sides is loaded as two files (a bid file and an ask file) with the same
source.Deriving mid: when
BIDandASKrows exist for a date,MIDcan be derived as(bid + ask) / 2per field and stored withsourceset toderived:<source>. Foropenandclosethis is exact. Forhighandlowit is an approximation, because the bid high and the ask high may occur at different times in the day. Derived mid bars must say so insource.Validation, in addition to the existing OHLC rules: for any date with both sides,
ask >= bidfor every field; aMIDbar lies between the bid and ask bars; the batch is rejected otherwise.data_originandprice_basisare independent: a simulated bid isSIMULATEDandBID.
Query changes#
The repository methods gain a basis argument (latest_close(symbol, as_of,
basis) and closes_through(symbol, as_of, basis)). The look-ahead guard is
unchanged: every basis is read with px_date <= as_of. A series used for VaR
returns must use one basis throughout; returns that mix bases are wrong.
Which basis each calculation uses#
Calculation |
Basis |
Note |
|---|---|---|
Unrealized P&L (default) |
|
Reference valuation; the default policy. Stored as |
Unrealized P&L (liquidation value) |
|
Optional stricter policy; shows what closing the position would realize. |
Historical VaR returns |
|
One consistent series; avoids bid/ask bounce in the returns. |
Notional and limit checks |
|
Matches the valuation mark. |
Backtest and simulated fills |
|
If only |
Booked trades |
none |
|
Growth and planned changes#
Roughly one row per symbol per business day (crypto: per calendar day).
Volume or open interest, needed for equities and crypto, would be added as
nullable columns, a safe additive migration. Price corrections as versioned rows,
and dropping the redundant single-column symbol index, are on the
roadmap.