Database#
How data is stored, moved and configured, and the schema of each database. The clean tier is split across four logical databases, each with its own SQLAlchemy engine and declarative base. This folder documents the architecture and setup, each database’s schema, and the decisions that cut across them.
Architecture and setup#
Document |
Covers |
|---|---|
Data tiers (Redis, PostgreSQL, DuckDB), why SQLAlchemy, why four databases |
|
Connection settings, running on PostgreSQL, running Redis in Docker on Windows |
Schemas#
Database |
Document |
Owns |
|---|---|---|
Reference |
Instruments (all asset classes), books, traders |
|
Market |
Daily OHLC prices, labelled observed or simulated |
|
Trade |
Trades (the blotter) and EOD P&L |
|
Risk |
Position limits, EOD risk metrics, limit breaches |
Related documents:
Multi-asset design: how the schema extends to FX, crypto, bonds and equities.
Schema hardening roadmap: schema changes required before real use.
Conventions#
snake_case names, plural table names, singular model class names.
idor<entity>_idsurrogate integer primary keys.symbolis the business key that links data across databases.No foreign keys across databases. Foreign keys within one database are used where they exist (e.g.
fx_instruments.instrument_id).Models live in
src/trade_engine/models/, one module per database. SQLite files are created underdb/.Until Alembic is adopted, tables are created with
create_all, which does not alter existing tables. After a schema change, delete the olddb/*.dbfiles and runscripts/init_db.pyagain.
Cross-cutting design decisions#
Decision |
Reason |
Consequence |
|---|---|---|
Neutral core tables plus one-to-one subtype tables for class-specific attributes |
Add asset classes without touching trades, prices, P&L or risk; each subtype enforces its own constraints |
Reading class-specific attributes needs a join; only code that needs them pays for it. |
Surrogate integer primary keys |
Stable row identity that never changes even if a business attribute (name, symbol) is corrected; small, fast join/index key |
Business uniqueness is enforced separately (unique constraints). Growth tables should use |
Business keys ( |
No cross-database foreign keys, so ids from another database would be meaningless to join; the value is also a self-describing snapshot that stays readable in reports and exports |
These keys are immutable codes; retire with |
EOD results ( |
Reports and the dashboard are cheap reads; results are an auditable snapshot of what was calculated on that date, independent of later trade or price changes |
Reruns replace that date’s rows. The history of reruns is not kept until the run ledger exists. |
Prices and EOD results keyed by date, one row per |
Matches the daily batch grain; makes idempotent replace-by-date trivial and keeps range scans by date cheap |
Intraday data needs a different table (time-series), not extra columns here. |
Provenance columns on prices ( |
Observed, vendor and simulated data must never be confusable; |
Loaders must state the origin explicitly; there is no default. |
P&L stored with its currency ( |
Multi-asset books hold several currencies; an amount without a currency is ambiguous |
Aggregating across currencies requires conversion (not built). |
|
Timestamps are set by the database, not by a possibly skewed client |
Currently timezone-naive; should become timezone-aware UTC (roadmap). |
Money as |
Simple and adequate for the single-pair v1 workflow |
Binary floating point cannot exactly represent decimal prices, and crypto needs far more decimal places; must change to |
Indexing and scalability#
Access patterns that drive the schema
Query |
Served by |
|---|---|
Latest close at or before date D for a symbol; closes up to D (mark and VaR) |
unique |
Latest loaded date (default EOD date); recent prices |
index on |
All non-cancelled trades up to D in replay order |
indexes on |
Blotter: most recent trades first |
index on |
P&L, risk and breaches for one date; recent history; replace-by-date |
indexes on |
Instruments of one class |
index on |
Active limits |
small table; scanned |
Expected growth. Row counts follow the grain, not the code. Prices grow by
roughly one row per symbol per business day (crypto: per calendar day).
pnl_eod and risk_metrics_eod grow by one row per open (symbol, book) per
day. trades grows with booking activity. At v1 scope (one pair, a few books)
every table is small by database standards. Adding asset classes grows the
instrument count, and with it prices and EOD results; reference and trade tables
grow far more slowly, which is part of why they are separate databases.
Scaling levers, in order of cost
Indexes and query shape. All hot queries are bounded by
calc_dateor(symbol, date), so they stay index seeks as tables grow.Read replicas for reporting. The dashboard, blotter and history views only read, and the reporting service is the only read-side entry point (see Reporting).
Partition the append-only EOD tables by ``calc_date`` (Postgres range partitioning) once retention and vacuum cost matter. Old partitions can be archived or dropped cheaply, which the date grain makes natural.
Move databases to separate hosts. Possible today by changing connection strings; the design already assumes no shared transactions.
Intraday or tick data (out of scope; likely for crypto and equities) belongs in a time-series store, not in
prices_eod.