6.8 KB
raw
-- finance: market-watching schema.
-- All *_at columns are UTC epoch-milliseconds (see db::now_ms).
-- Trading dates ("d") are TEXT YYYY-MM-DD: calendar days, not instants.
-- ── symbols: the watched universe (stocks, ETFs, indexes) ──
CREATE TABLE symbols (
ticker TEXT PRIMARY KEY, -- uppercase, e.g. AAPL, ^SPX
name TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL DEFAULT 'stock', -- stock | etf | index
exchange TEXT,
currency TEXT NOT NULL DEFAULT 'USD',
cik TEXT, -- 10-digit zero-padded SEC CIK; NULL for ETF/index
sector TEXT,
industry TEXT,
is_seeded INTEGER NOT NULL DEFAULT 0, -- member of the curated starter list
is_watched INTEGER NOT NULL DEFAULT 0, -- in at least one watchlist (denormalized)
history_synced_at INTEGER,
history_first_date TEXT,
history_last_date TEXT,
fundamentals_synced_at INTEGER,
filings_synced_at INTEGER,
-- denormalized latest snapshot for fast list rendering and SSE seeding
last_price REAL,
prev_close REAL,
last_quote_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX symbols_kind ON symbols(kind);
CREATE INDEX symbols_is_watched ON symbols(is_watched);
CREATE INDEX symbols_name ON symbols(name);
-- ── daily_prices: deep historical OHLCV from Stooq. Permanent, never pruned. ──
CREATE TABLE daily_prices (
ticker TEXT NOT NULL REFERENCES symbols(ticker) ON DELETE CASCADE,
d TEXT NOT NULL, -- YYYY-MM-DD trading date
open REAL NOT NULL,
high REAL NOT NULL,
low REAL NOT NULL,
close REAL NOT NULL,
volume INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (ticker, d)
);
CREATE INDEX daily_prices_d ON daily_prices(d);
-- ── intraday_bars: today's ~15-min-delayed bars from Yahoo. Pruned to recent days. ──
CREATE TABLE intraday_bars (
ticker TEXT NOT NULL REFERENCES symbols(ticker) ON DELETE CASCADE,
ts INTEGER NOT NULL, -- bar start, UTC epoch-ms
open REAL NOT NULL,
high REAL NOT NULL,
low REAL NOT NULL,
close REAL NOT NULL,
volume INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (ticker, ts)
);
CREATE INDEX intraday_bars_ts ON intraday_bars(ts);
-- ── quotes: latest live quote snapshot per symbol (one row per ticker, upserted) ──
CREATE TABLE quotes (
ticker TEXT PRIMARY KEY REFERENCES symbols(ticker) ON DELETE CASCADE,
price REAL NOT NULL,
prev_close REAL,
open REAL,
day_high REAL,
day_low REAL,
volume INTEGER,
market_state TEXT, -- PRE | REGULAR | POST | CLOSED (source-reported)
source TEXT NOT NULL DEFAULT 'yahoo',
source_time INTEGER, -- the source's own timestamp (epoch-ms)
fetched_at INTEGER NOT NULL
);
-- ── fundamentals: one row per (ticker, metric, fiscal period) from SEC XBRL facts ──
-- Long/narrow so new XBRL concepts need no schema change. Stocks only.
CREATE TABLE fundamentals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticker TEXT NOT NULL REFERENCES symbols(ticker) ON DELETE CASCADE,
metric TEXT NOT NULL, -- revenue | net_income | eps_diluted | shares_diluted
-- | dividends_per_share | assets | liabilities | equity
period TEXT NOT NULL, -- 'FY2024' or 'Q3-2024'
fiscal_year INTEGER NOT NULL,
fiscal_qtr INTEGER, -- NULL for a full-year figure
period_end TEXT NOT NULL, -- YYYY-MM-DD
value REAL NOT NULL,
unit TEXT, -- USD | USD/shares | shares
form TEXT, -- 10-K | 10-Q
filed_at TEXT, -- YYYY-MM-DD
UNIQUE (ticker, metric, period_end)
);
CREATE INDEX fundamentals_ticker_metric ON fundamentals(ticker, metric, period_end DESC);
-- ── filings: SEC filing history (10-K / 10-Q / 8-K and friends) ──
CREATE TABLE filings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticker TEXT NOT NULL REFERENCES symbols(ticker) ON DELETE CASCADE,
accession TEXT NOT NULL,
form TEXT NOT NULL,
filed_at TEXT NOT NULL, -- YYYY-MM-DD
period_of_report TEXT, -- YYYY-MM-DD
primary_doc TEXT,
url TEXT NOT NULL, -- full EDGAR filing-index URL
description TEXT,
UNIQUE (ticker, accession)
);
CREATE INDEX filings_ticker_filed ON filings(ticker, filed_at DESC);
-- ── watchlists ──
CREATE TABLE watchlists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE, -- URL-safe, e.g. tech-megacaps
position INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE watchlist_items (
watchlist_id INTEGER NOT NULL REFERENCES watchlists(id) ON DELETE CASCADE,
ticker TEXT NOT NULL REFERENCES symbols(ticker) ON DELETE CASCADE,
position INTEGER NOT NULL DEFAULT 0,
added_at INTEGER NOT NULL,
PRIMARY KEY (watchlist_id, ticker)
);
CREATE INDEX watchlist_items_ticker ON watchlist_items(ticker);
-- ── fetch_log: append-only history of every background fetch. Drives the data-status UI. ──
CREATE TABLE fetch_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job TEXT NOT NULL, -- seed | history | intraday | fundamentals | filings | prune
provider TEXT NOT NULL, -- stooq | yahoo | sec | -
ticker TEXT, -- NULL for bulk jobs
status TEXT NOT NULL, -- ok | error | skipped
detail TEXT,
rows INTEGER,
duration_ms INTEGER,
started_at INTEGER NOT NULL,
finished_at INTEGER NOT NULL
);
CREATE INDEX fetch_log_started ON fetch_log(started_at DESC);
CREATE INDEX fetch_log_job ON fetch_log(job, started_at DESC);
-- ── data_status: one row per job, current state, for the live status pill ──
CREATE TABLE data_status (
job TEXT PRIMARY KEY, -- seed | history | intraday | fundamentals | filings
state TEXT NOT NULL, -- idle | fetching | ok | stale | error
last_ok_at INTEGER,
last_error TEXT,
last_error_at INTEGER,
next_run_at INTEGER,
updated_at INTEGER NOT NULL
);
-- ── meta: one-off key-value settings (seed_completed flag, etc.) ──
CREATE TABLE meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);