Skip to content

Database Schema — Spec

users

Column Type Notes
id Integer PK
username String(50) Unique, indexed
hashed_password String(255) bcrypt hash
is_active Boolean False until admin approves the account
is_admin Boolean Admins can approve new user registrations
created_at DateTime UTC

One-to-many → user_sessions (cascade delete) and portfolio_snapshots (cascade delete).

user_sessions

Column Type Notes
id Integer PK
user_id FK → users.id
token String secrets.token_urlsafe(32); unique, indexed
created_at DateTime UTC
expires_at DateTime UTC; 30-day TTL set at creation

tickers

Column Type Notes
symbol String(20) Plain symbol (US) or 6-digit code (KR)
market String(5) "US" or "KR"
name String(100) Display name
sector String(50) e.g. "big_tech", "semiconductor" (nullable)
active Boolean False = soft-deleted
added_at DateTime UTC

Unique constraint on (symbol, market). Index on (market, active).
Seeded from config.py by _seed_tickers() on every init_db() call (idempotent).

stock_prices

Column Type Notes
ticker String(20) Plain symbol (US) or 6-digit code (KR)
market String(5) "US" or "KR"
date DateTime UTC
open/high/low/close/volume Float OHLCV
created_at DateTime UTC

Index: ix_stock_prices_ticker_date on (ticker, date).

news_articles

Column Type Notes
ticker String(20) Primary ticker (nullable)
market String(5) "US" or "KR"
language String(5) "en" or "ko"
title Text Original headline
title_en Text Translation cache (nullable)
title_ko Text Translation cache (nullable)
url Text Unique — prevents duplicates
published_at DateTime UTC
fetched_at DateTime UTC

One-to-one → sentiment_scores via article.sentiment.
Index: ix_news_articles_ticker_published on (ticker, published_at).

sentiment_scores

Column Type Notes
article_id FK → news_articles.id One score per article
ticker String(20) Denormalized from article
score Float -1.0 to +1.0
label String(10) "positive" / "negative" / "neutral"
confidence Float Damped by evidence count for KO
model_used String(50) "en_finbert" / "kr_finbert" / "vader_finance" / "ko_lexicon" / "none"

Index: ix_sentiment_ticker_created on (ticker, created_at).

predictions

Column Type Notes
ticker String(20)
market String(5) "US" or "KR"
horizon String(5) "1d" / "7d" / "30d"
direction String(10) "up" / "down" / "neutral"
confidence Float 0.0–1.0 (raw proba)
sentiment_score_avg Float 7-day avg sentiment at prediction time
momentum_score Float 5-day momentum at prediction time
news_volume Integer Article count in 7-day window
target_date DateTime UTC; now + horizon days

Index: ix_predictions_ticker_horizon_created on (ticker, horizon, created_at).

prediction_results

Column Type Notes
prediction_id FK → predictions.id Unique — one result per prediction
ticker String(20) Denormalized
market String(5) "US" or "KR"
horizon String(5) "1d" / "7d" / "30d"
predicted_direction String(10) Direction from the original prediction
actual_return Float Realized return: (exit - entry) / entry
correct Boolean True if predicted direction matches realized return
evaluated_at DateTime UTC

Index: ix_prediction_results_ticker_horizon on (ticker, horizon).

alerts

Column Type Notes
ticker String(20)
market String(5) "US" or "KR"
direction String(10) "up" / "down" / "neutral"
horizon String(5) "1d" / "7d" / "30d"
confidence Float Must exceed threshold in config.py
prediction_id FK → predictions.id Unique — one alert per prediction
triggered_at DateTime UTC
seen Boolean False until user dismisses in UI

Index: ix_alerts_seen on (seen).

Column Type Notes
sector String(50) Sector name
keyword String(100) Top keyword for this sector
frequency Integer Mention count in the aggregation window
avg_sentiment Float Average sentiment score across all articles
tickers_involved Text JSON list of ticker symbols
period_start / period_end DateTime UTC window
created_at DateTime UTC

fundamentals

Column Type Notes
ticker String(20)
market String(5) "US" or "KR"
period_end DateTime Quarter/year end date
revenue Float Nullable
net_income Float Nullable
eps Float Nullable
total_debt Float Nullable
total_equity Float Nullable
pe_ratio Float Nullable
fetched_at DateTime UTC

Unique constraint on (ticker, period_end). Index on (ticker, market).
Populated by the fetch_fundamentals 24-hour scheduler job via financialdatasets.ai.

daily_summaries

Column Type Notes
id Integer PK
generated_at DateTime UTC — when Claude generated this briefing
summary_text Text 3–4 sentence plain-prose market briefing
model_used String(50) Claude model ID returned by the API
input_tokens Integer Prompt token count
output_tokens Integer Completion token count
market_data_json Text JSON snapshot of signals fed to the prompt (for auditability)

One row per generation run. The API always returns the most recent row. Populated by the generate_daily_summary 24-hour scheduler job. Requires ANTHROPIC_API_KEY to be set; job is a no-op when the key is absent.

telegram_notifications

Column Type Notes
id Integer PK
kind String(20) "alert" / "sentiment_spike" / "test"
ref_id Integer Alert.id for "alert" kind; null for other kinds
ticker String(20) Nullable — set for per-ticker alerts
sector String(50) Nullable — set for sentiment spike notifications
message_text Text Full HTML message text that was sent
sent_at DateTime UTC — when the send was attempted
success Boolean True if Telegram API returned 2xx
error Text Nullable — error message on failure

Indexes: ix_tgnotif_kind_ref on (kind, ref_id) for deduplication lookups; ix_tgnotif_sent_at on (sent_at) for last-sent queries.
Used by notifications/telegram.py. Every send attempt is recorded regardless of success — failures have success=False and a non-null error. The "alert" dedup guard queries by (kind, ref_id, success=True). The "sentiment_spike" dedup guard queries by (kind, sector, sent_at >= now-12h, success=True).

earnings_events

Column Type Notes
id Integer PK
ticker String(20) US symbol (e.g. "AAPL")
market String(5) "US" only — KRX earnings not fetched via yfinance
earnings_date DateTime UTC midnight on earnings release date
eps_estimate Float Analyst consensus EPS estimate; nullable
eps_actual Float Reported EPS; null until the quarter has been reported
surprise_pct Float (actual - estimate) / |estimate| * 100; null when either value is missing
fetched_at DateTime UTC — when this row was last refreshed

Unique constraint uq_earnings_ticker_date on (ticker, earnings_date) enables clean upserts.
Indexes: ix_earnings_ticker_date on (ticker, earnings_date); ix_earnings_date on (earnings_date) for calendar-window queries.
Populated by the fetch_earnings 24-hour scheduler job via data/earnings_fetcher.py (yfinance get_earnings_dates()). Covers ~3 months back and ~4 months forward.

political_trades

Column Type Notes
id Integer PK
trader_name String(100) Full name; from POLITICAL_WATCHLIST in config.py
trader_role String(100) E.g. "House (D-CA)", "DOGE / Tesla insider" (nullable)
ticker String(20) US symbol (e.g. "NVDA")
market String(5) "US" (only US disclosures tracked currently)
trade_type String(10) "buy" or "sell"
amount_low Float Lower bound of STOCK Act–disclosed range (USD); nullable
amount_high Float Upper bound; nullable; equals amount_low for EDGAR exact amounts
traded_at DateTime UTC midnight on trade date
filed_at DateTime UTC midnight on STOCK Act filing date; nullable
source String(50) "capitol_trades" or "edgar_form4"
source_url Text Direct link to the filing or politician page; nullable
fetched_at DateTime UTC — when this row was scraped

Unique constraint uq_political_trade on (trader_name, ticker, traded_at, trade_type) prevents duplicate inserts across polling cycles.
Indexes: ix_political_trades_ticker_traded on (ticker, traded_at); ix_political_trades_trader on (trader_name).
Populated by the fetch_political_trades 6-hour scheduler job (scraper/political_trades.py). Congressional trades are delayed up to 45 days by STOCK Act law; EDGAR Form 4 insiders must file within 2 business days.

portfolio_snapshots

Column Type Notes
id Integer PK
user_id FK → users.id Nullable
imported_at DateTime UTC
total_assets_krw Float Total portfolio value in KRW
today_pnl_krw / today_pnl_pct Float Day P&L
kr_purchase_krw / kr_current_krw / kr_pnl_krw / kr_return_pct Float Korean holdings summary
us_purchase_krw / us_current_krw / us_pnl_krw / us_return_pct Float US holdings summary (KRW-converted)
cash_krw / cash_usd Float Cash balances
usd_krw_rate Float USD/KRW FX rate at snapshot time
tokens_input / tokens_output / tokens_total Integer Gemini token counts

One-to-many → portfolio_holdings (cascade delete).

portfolio_holdings

Column Type Notes
id Integer PK
snapshot_id FK → portfolio_snapshots.id
market String(5) "US" or "KR"
ticker String(20) KRX code or US symbol; None if unmappable
name_kr String(100) Korean name as shown in brokerage app
quantity Integer
current_price / avg_cost Float Local currency
pnl_local / pnl_pct Float P&L in local currency and %
current_value_krw / purchase_value_krw Float KRW-converted values

Index: ix_portfolio_holdings_snapshot_market on (snapshot_id, market).

trade_logs

Column Type Notes
id Integer PK
user_id FK → users.id Nullable
ticker String(20)
market String(5) "US" or "KR"
action String(10) "buy" or "sell"
quantity Float
price Float Local currency
trade_date Date
notes Text Nullable — free-text journal entry
created_at DateTime UTC

Indexes: ix_trade_logs_user_ticker on (user_id, ticker); ix_trade_logs_user_date on (user_id, trade_date).
Populated by the Trade Journal UI.

macro_indicators

Column Type Notes
id Integer PK
date Date Unique; one row per trading day
vix Float CBOE VIX closing value; nullable
yield_10y Float 10-year Treasury yield (%); nullable
yield_3m Float 3-month Treasury bill yield (%); nullable
yield_spread Float yield_10y − yield_3m; nullable
fetched_at DateTime UTC

Unique constraint on date. Index on date.
Populated by the fetch_macro 24-hour scheduler job via data/macro_fetcher.py (yfinance ^VIX, ^TNX, ^IRX). On first run (empty table) the job backfills 180 days of history so training samples have aligned macro context. Feeds vix_level and yield_spread_norm features in models/predictor.py (US tickers only).