-- Investor Flow — SQLite schema. Engineering contract: DESIGN.md §1 (Tier A/B/C/D), -- §1.5 (LLM provenance), §2.6 (ticker-data dedupe), + locked review deltas (price_adjustments, trade_key_levels). -- Tiers: A shared market cache (ownerId NULL) | B shared content | C per-user (ownerId NOT NULL) | D system. -- Conventions: timestamps ISO8601 TEXT; observed_at/fetched_at provenance per P4. -- Idempotent: CREATE ... IF NOT EXISTS. Safe to re-run on every boot. PRAGMA foreign_keys = ON; -- ===== Tier D — System / Identity ===== CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, -- uuid email TEXT UNIQUE NOT NULL, pw_hash TEXT NOT NULL, -- scrypt(verifiable); design specified argon2id, adapted to built-in scrypt (reversible) totp_secret TEXT, -- 2FA secret, encrypted at rest (slice 2) is_2fa_enabled INTEGER NOT NULL DEFAULT 0, oauth_subject TEXT, -- nullable; social OAuth link (slice 2) oauth_provider TEXT, -- 'github' | 'google' | NULL complexity TEXT NOT NULL DEFAULT 'beginner', -- beginner|intermediate|advanced risk_tolerance TEXT NOT NULL DEFAULT 'moderate', drawdown_tolerance REAL, -- onboarding max-drawdown % (beginner -20, etc.) convexity_posture TEXT NOT NULL DEFAULT 'off', -- off|covered_income|cash_secured_entry|insurance_sleeve|leaps_conviction backup_codes_hashed TEXT, -- JSON array of scrypt-hashed backup codes (slice 2) created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS sizing_unlocks ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, unlock TEXT NOT NULL, -- 'tier_a_star' | 'tier_a' | ('covered_income'|...) earned_at TEXT NOT NULL, evidence TEXT, -- stats that justified unlock PRIMARY KEY (user_id, unlock) ); -- ===== Tier A — Shared market cache (no ownerId) ===== CREATE TABLE IF NOT EXISTS symbols ( symbol TEXT PRIMARY KEY, name TEXT, sector TEXT, industry TEXT, exchange TEXT, ticker_kind TEXT NOT NULL, -- equity|crypto|etf|index (gates module applicability) peers TEXT, -- JSON array updated_at TEXT ); CREATE TABLE IF NOT EXISTS price_candles ( symbol TEXT NOT NULL, timeframe TEXT NOT NULL, -- '1m'|'5m'|'1h'|'1d'|'1w' ts TEXT NOT NULL, o REAL, h REAL, l REAL, c REAL, v REAL, adj_close REAL, -- continuous split+dividend-adjusted close for backtests observed_at TEXT NOT NULL, -- when fetched (provenance) PRIMARY KEY (symbol, timeframe, ts) ); CREATE TABLE IF NOT EXISTS quotes ( symbol TEXT PRIMARY KEY, price REAL, bid REAL, ask REAL, change REAL, change_pct REAL, iv REAL, -- implied vol when available observed_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS kv_cache ( key TEXT PRIMARY KEY, value TEXT NOT NULL, observed_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS options_chains ( symbol TEXT NOT NULL, expiry TEXT NOT NULL, strike REAL NOT NULL, type TEXT NOT NULL, -- call|put bid REAL, ask REAL, iv REAL, delta REAL, gamma REAL, theta REAL, vega REAL, rho REAL, open_interest INTEGER, volume INTEGER, ts TEXT NOT NULL, -- snapshot timestamp PRIMARY KEY (symbol, expiry, strike, type, ts) ); CREATE TABLE IF NOT EXISTS filings ( accession TEXT PRIMARY KEY, -- SEC accession number form TEXT NOT NULL, -- 10-K|10-Q|8-K|13F-HR|13D|13G|4|... symbol TEXT NOT NULL, filed_at TEXT NOT NULL, period TEXT, primary_doc_url TEXT, items TEXT, -- JSON: list of items (8-K materiality heuristics) fetched_at TEXT NOT NULL, raw_payload TEXT -- cached full XBRL/parsed doc ); CREATE TABLE IF NOT EXISTS institution_filings ( filer_cik TEXT NOT NULL, -- institution's CIK filer_name TEXT, filer_sic TEXT, -- SIC code -> classifies MM/broker-dealer (ADR Q2) symbol TEXT NOT NULL, form TEXT NOT NULL, -- 13F-HR|13D|13G shares REAL, -- shares reported value_usd REAL, reported_quarter TEXT NOT NULL, -- calendar quarter positions are "as of" filed_at TEXT NOT NULL, fetched_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS insider_transactions ( form4_id TEXT NOT NULL, -- accession + offset symbol TEXT NOT NULL, insider_name TEXT NOT NULL, insider_role TEXT, -- officer|director|ten_pct_holder tx_date TEXT NOT NULL, tx_code TEXT NOT NULL, -- P|S|M|A|... (SEC codes) tx_type TEXT NOT NULL, -- buy|sell|exercise|grant|vesting shares REAL, price REAL, is_10b5_1 INTEGER NOT NULL DEFAULT 0, -- 1 = under 10b5-1 plan (Routine) classification TEXT NOT NULL, -- informed_buy|informed_sell|routine filed_at TEXT NOT NULL, fetched_at TEXT NOT NULL, PRIMARY KEY (form4_id, symbol, insider_name, tx_date) ); CREATE TABLE IF NOT EXISTS sector_map ( symbol TEXT NOT NULL, sector TEXT NOT NULL, as_of TEXT NOT NULL, rs_ratio REAL, -- relative strength vs SPX rs_slope REAL, -- slope over N weeks (incipient detection) breadth_thrust REAL, -- % members beating SPX PRIMARY KEY (symbol, sector, as_of) ); CREATE TABLE IF NOT EXISTS macro_events ( event_id TEXT PRIMARY KEY, -- e.g. '2026-06-30-FOMC' name TEXT NOT NULL, event_ts TEXT NOT NULL, impact TEXT, -- high|medium|low forecast TEXT, previous TEXT, actual TEXT, -- populated after release source TEXT -- 'FRED'|'econoday'|... ); CREATE TABLE IF NOT EXISTS macro_regimes ( as_of TEXT PRIMARY KEY, regime TEXT NOT NULL, -- trending_up|trending_down|range_bound|structurally_shifting signals TEXT -- JSON: yield curve, inflation, rate path, liquidity, breadth ); CREATE TABLE IF NOT EXISTS price_adjustments ( symbol TEXT NOT NULL, ex_date TEXT NOT NULL, -- date the price line shifted type TEXT NOT NULL, -- 'split'|'dividend' ratio REAL NOT NULL, -- split ratio (3.0 = 3-for-1) or dividend amount ($/share) PRIMARY KEY (symbol, ex_date, type) ); -- ===== Tier B — Shared content ===== CREATE TABLE IF NOT EXISTS threads ( source TEXT NOT NULL, -- x|reddit source_id TEXT NOT NULL, -- tweet id / reddit post id symbol TEXT NOT NULL, author TEXT NOT NULL, body TEXT, ts TEXT NOT NULL, sentiment TEXT, -- bullish|bearish|neutral (computed) llm_summary_hash TEXT, -- cross-ref to llm_summaries fetched_at TEXT NOT NULL, PRIMARY KEY (source, source_id) ); CREATE TABLE IF NOT EXISTS adapter_queue ( key TEXT PRIMARY KEY, -- e.g. "yfinance:quote:NVDA" status TEXT NOT NULL, -- pending|in_flight|done|backoff last_attempt TEXT, retry_count INTEGER NOT NULL DEFAULT 0, backoff_until TEXT ); CREATE TABLE IF NOT EXISTS symbol_demand ( symbol TEXT PRIMARY KEY, refcount INTEGER NOT NULL DEFAULT 0, ticker_kind TEXT NOT NULL, in_demand INTEGER NOT NULL DEFAULT 1, -- gate flag for the adapter queue last_refreshed_at TEXT ); -- ===== Tier C — Per-user (ownerId NOT NULL) ===== CREATE TABLE IF NOT EXISTS watchlists ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, symbols TEXT NOT NULL, -- JSON array created_at TEXT NOT NULL, sort_order INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS portfolio_holdings ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, symbol TEXT NOT NULL, qty REAL NOT NULL, avg_cost REAL NOT NULL, acquired_at TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'open' -- open|closed ); CREATE TABLE IF NOT EXISTS trades ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, trade_id TEXT NOT NULL, symbol TEXT NOT NULL, date TEXT NOT NULL, status TEXT NOT NULL, -- planned|active|closed environment TEXT, -- trending|ranging|transitioning tier TEXT NOT NULL, -- A_STAR|A|B|C (Conviction Tier) strategy_id TEXT, -- ref to strategies (nullable) confluence_score INTEGER, risk_pct REAL, position_size REAL, -- computed by SizingEngine entry_price REAL, stop_loss REAL, targets TEXT, -- JSON [{price, reason}] justifications TEXT, -- JSON {entryWhy, stopWhy, targetWhy} checklist_completed INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS strategies ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, regime_gate TEXT NOT NULL, -- JSON: gate config setup TEXT NOT NULL, -- JSON: entry/exit checklist w/ params risk_policy TEXT NOT NULL, -- JSON: sizing rules created_at TEXT NOT NULL, is_public INTEGER NOT NULL DEFAULT 0 -- shareable read-only (later phase) ); CREATE TABLE IF NOT EXISTS screener_filters ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, expressions TEXT NOT NULL, -- JSON: filter expression set universe_kind TEXT NOT NULL, -- watchlist|portfolio|broader_sector created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS reports ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, scope TEXT NOT NULL, -- symbol|watchlist|portfolio|rotation|sizing_year|risk_posture range_start TEXT, range_end TEXT, format TEXT NOT NULL, -- html|markdown|csv|json payload_ref TEXT NOT NULL, -- file path in local reports dir generated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS alerts ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, symbol TEXT, alert_type TEXT NOT NULL, -- informed_buy|informed_sell|new_13da|rotation_incipient|regime_shift|conviction_unlock|... params TEXT NOT NULL, -- JSON thresholds last_fired TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS trusted_accounts ( owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, source TEXT NOT NULL, -- x|reddit handle TEXT NOT NULL, PRIMARY KEY (owner_id, source, handle) ); CREATE TABLE IF NOT EXISTS saved_posts ( owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, source TEXT NOT NULL, source_id TEXT NOT NULL, note TEXT, linked_trade_id TEXT REFERENCES trades(id) ON DELETE SET NULL, saved_at TEXT NOT NULL, PRIMARY KEY (owner_id, source, source_id) ); CREATE TABLE IF NOT EXISTS trade_key_levels ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, symbol TEXT NOT NULL, type TEXT NOT NULL, -- swing_high|swing_low|supply|demand|support|resistance price REAL NOT NULL, timeframe TEXT, -- '1H'|'4H'|'1D'|... strength TEXT, -- low|medium|high created_at TEXT NOT NULL ); -- ===== Shared LLM cache (output deterministic by prompt-hash; not per-user) ===== CREATE TABLE IF NOT EXISTS llm_summaries ( hash TEXT PRIMARY KEY, -- hash(prompt + source-payload + complexity) feature TEXT NOT NULL, -- filing_summary|post_summary|dashboard_rollup|explainer|qa|sizing_explain|macro_commentary|... complexity TEXT NOT NULL, output TEXT NOT NULL, cited_sources TEXT, -- JSON refs to cached rows (provenance) created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS rotation_signals ( id TEXT PRIMARY KEY, sector TEXT NOT NULL, detected_at TEXT NOT NULL, phase TEXT NOT NULL, -- accumulation|expansion|distribution|markdown confidence REAL, price_resolved INTEGER NOT NULL DEFAULT 0, -- follows through within N=4 weeks? inst_resolved INTEGER NOT NULL DEFAULT 0, -- 13F/insider flow later confirms? resolved_at TEXT, verdict TEXT -- real|false_alarm (gamma two-stage) ); -- ===== Section 1.5 — LLM provenance (ADR-0006). Operator-managed, NOT user-editable. ===== CREATE TABLE IF NOT EXISTS llm_providers ( id TEXT PRIMARY KEY, -- 'local_ollama' | 'local_vllm' | 'zen_glm_5_2' | ... base_url TEXT NOT NULL, is_local INTEGER NOT NULL DEFAULT 0, -- 1 if localhost/127.0.0.1/::1/LAN subnet review_adr_ref TEXT, -- ADR-XXXX or note documenting provenance review date enabled INTEGER NOT NULL DEFAULT 0 -- only one with enabled=1 by default (local) ); CREATE TABLE IF NOT EXISTS llm_dispatch_audit ( id TEXT PRIMARY KEY, feature TEXT NOT NULL, payload_class TEXT NOT NULL, -- public_safe|sensitive provider_id TEXT, status TEXT NOT NULL, -- sent_local|sent_external|blocked_sensitive_external ts TEXT NOT NULL ); -- ===== Slice 16 — Sentiment / X-cookie / Reddit tables ===== -- sentiment_score: -1.0 (bearish) to +1.0 (bullish), computed by LLM post_summary or heuristic. -- attribution: author_handle preserved on every cached post per M8. -- cached_until: 7d rolling TTL; rows become stale after this window. CREATE TABLE IF NOT EXISTS x_cookie_posts ( post_id TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'x', author_handle TEXT NOT NULL, cashtag TEXT, -- e.g. 'NVDA' — null if not a cashtag search body_text TEXT, posted_at TEXT NOT NULL, engagement INTEGER NOT NULL DEFAULT 0, -- likes + retweets + replies sentiment_score REAL, -- -1.0 to +1.0 attribution TEXT, -- original author handle (may differ from author_handle for quotes/retweets) cached_until TEXT NOT NULL, -- 7d rolling; rows stale after this PRIMARY KEY (post_id) ); CREATE TABLE IF NOT EXISTS reddit_posts ( post_id TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'reddit', author_handle TEXT NOT NULL, subreddit TEXT NOT NULL, body_text TEXT, posted_at TEXT NOT NULL, engagement INTEGER NOT NULL DEFAULT 0, -- upvotes + comments sentiment_score REAL, -- -1.0 to +1.0 attribution TEXT, -- original author handle (may differ for cross-posts) cached_until TEXT NOT NULL, -- 7d rolling PRIMARY KEY (post_id) ); -- Indexes for slice-16 sentiment tables CREATE INDEX IF NOT EXISTS idx_x_cookie_cashtag ON x_cookie_posts(cashtag, posted_at DESC); CREATE INDEX IF NOT EXISTS idx_reddit_subreddit ON reddit_posts(subreddit, posted_at DESC); -- ===== Indexes (existing) ===== CREATE INDEX IF NOT EXISTS idx_pc_symbol_tf_ts ON price_candles(symbol, timeframe, ts); CREATE INDEX IF NOT EXISTS idx_quote_obs ON quotes(observed_at); CREATE INDEX IF NOT EXISTS idx_filing_symbol_form ON filings(symbol, form); CREATE INDEX IF NOT EXISTS idx_instfil_sym_q ON institution_filings(symbol, reported_quarter); CREATE INDEX IF NOT EXISTS idx_insider_sym_date ON insider_transactions(symbol, tx_date); CREATE INDEX IF NOT EXISTS idx_threads_symbol_ts ON threads(symbol, ts); CREATE INDEX IF NOT EXISTS idx_trades_owner ON trades(owner_id); CREATE INDEX IF NOT EXISTS idx_alerts_owner_sym ON alerts(owner_id, symbol); CREATE INDEX IF NOT EXISTS idx_signals_sector ON rotation_signals(sector);