Data architecture#
How data is stored and moved: the three tiers (Redis, PostgreSQL, DuckDB), why the code uses SQLAlchemy, and why the clean tier is split into four logical databases. Table-level detail is in the per-database pages listed in Database; configuration and running the servers is in Setup: configuration, PostgreSQL and Redis.
Data tiers#
Market data moves through three stores. Each has one job and data only flows forward (Redis -> PostgreSQL -> DuckDB).
Tier |
Store |
Holds |
Written by |
|---|---|---|---|
Raw / staging |
Redis Stream ( |
Unvalidated CSV rows exactly as read (strings), one stream entry per file |
|
Clean / structured |
PostgreSQL (four databases); SQLite files by default for local development and tests |
Validated, typed, provenance-labelled data: reference, market, trades, P&L, risk |
|
Local analytics |
DuckDB file ( |
A read-only snapshot of the clean tables for research and backtests |
|
How the staging step behaves:
Staging does not validate prices. Only the file’s origin, source and readability are checked up front. Prices are validated when the batch is processed, using the same rules as before (see Market database (market.db)).
A batch is committed as a unit. Processing validates the whole file, inserts new dates in one transaction, and only then acknowledges the batch. A crash before the acknowledgement re-delivers the batch after 60 seconds; reloading is safe because dates already stored are skipped.
Invalid batches are dead-lettered. A batch that fails validation moves, with its raw rows and the reason, to
trade_engine:staging:market:rejectedand is never retried automatically.Redis is not a system of record. Anything that matters is in PostgreSQL once processed. Redis persistence (AOF/RDB) only protects batches that are staged but not yet processed.
The analytics export replaces its tables each run and never writes back to the clean databases. It keeps
data_originon prices so simulated and observed data stay distinguishable in research.
Why SQLAlchemy#
SQLAlchemy is the layer the code talks to, so the same code runs on SQLite (local
development and tests) and PostgreSQL (the clean layer). Plain sqlite3 would
work for a single local file, but the design is meant to be built on (see the
quality bar), so SQLAlchemy is used for these reasons:
The engine owns the connection. One engine per connection URL (for example
sqlite:///db/trade.db) holds the connection pool; sessions,create_alland queries all go through it.The database stays swappable. Code sees ORM models and sessions, not SQLite-specific SQL, so moving a database to PostgreSQL is a connection-string change (see Setup: configuration, PostgreSQL and Redis).
One engine per logical database. The four databases below each need their own engine and declarative base, whichever server hosts them.
Explicit transactions. Sessions make every write an explicit, rolled-back-on-failure unit of work, as the EOD batch and trade booking require.
Why 4 databases#
Each logical database has its own SQLAlchemy engine and declarative base
(SQLite file under db/ by default).
DB |
File |
Owns |
|---|---|---|
Reference |
|
Instruments (all asset classes), books, traders; static and slow-changing |
Market |
|
Daily OHLC prices per instrument |
Trade |
|
Trades (the blotter) and computed EOD P&L |
Risk |
|
Position limits, EOD risk metrics, limit breaches |
Reasons for the split (each maps to a real operational need):
Reason |
Effect |
|---|---|
Different owners and change rates |
Reference data is edited rarely by ops; market data is loaded by a feed job; trades are written by users; risk is written by a batch. Each can be deployed, migrated and versioned independently. |
Different growth and retention |
Market and EOD tables grow with time and can be archived or partitioned without touching the small, critical trade and reference tables. Adding crypto or equities multiplies market-data volume, not trade or reference volume. |
Independent scaling |
A database can move to its own host, or get read replicas for reporting, without moving the others. |
Access control |
Grants can differ per database (e.g. risk can read trades but not write them). |
Failure isolation |
A market-data reload or risk recalculation cannot lock the trade tables. |
Trade-offs accepted
No cross-database foreign keys (SQLite cannot enforce them, and most multi-system deployments cannot either). Linkage is by business keys (
symbol,book,trader), joined at the application and reporting layer. Referential checks live in services (e.g. a trade is only booked for an existing, active instrument, book and trader). Foreign keys within one database are used.No cross-database transactions. The EOD batch writes P&L (trade DB) and then risk (risk DB) in separate commits. Each step is safe to re-run because it replaces that
calc_date’s rows, but a crash between the two leaves them temporarily inconsistent. The plannedeod_runsledger (roadmap item 8) makes this explicit and detectable. If operational simplicity later outweighs the isolation benefits, the databases can be consolidated into one Postgres database with four schemas; the per-database engines and the repository layer make that a configuration change.
Details per database: Database.