471 lines
18 KiB
Markdown
471 lines
18 KiB
Markdown
# Invest Copilot — Data Model
|
|||
|
|
|
||
|
|
## Entity-Relationship Overview
|
||
|
|
|
||
|
|
```
|
||
|
|
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
|
||
|
|
│ User │ │ Watchlist │ │ Screener │
|
||
|
|
│──────────────│ │──────────────│ │──────────────────│
|
||
|
|
│ id (PK) │◄──┐ │ id (PK) │ │ id (PK) │
|
||
|
|
│ email │ │ │ name │ │ name │
|
||
|
|
│ name │ │ │ user_id (FK) │ │ user_id (FK) │
|
||
|
|
│ avatar_url │ │ │ created_at │ │ created_at │
|
||
|
|
│ timezone │ │ │ updated_at │ │ conditions (JSONB)│
|
||
|
|
│ settings │ └────┬───────────┘ │ conditions_json │
|
||
|
|
│ created_at │ │ │ created_at │
|
||
|
|
└──────────────┘ │ └──────────────────┘
|
||
|
|
│
|
||
|
|
│ ┌──────────────────┐
|
||
|
|
└───►│ WatchlistItem │
|
||
|
|
│──────────────────│
|
||
|
|
│ id (PK) │
|
||
|
|
│ watchlist_id (FK)│
|
||
|
|
│ ticker │
|
||
|
|
│ type (stock/etf) │
|
||
|
|
│ added_at │
|
||
|
|
│ custom_notes │
|
||
|
|
└────────┬─────────┘
|
||
|
|
│
|
||
|
|
│ ┌──────────────────┐
|
||
|
|
└───►│ PriceHistory │
|
||
|
|
│──────────────────│
|
||
|
|
│ id (PK) │
|
||
|
|
│ ticker │
|
||
|
|
│ date (Timescale) │
|
||
|
|
│ open │
|
||
|
|
│ high │
|
||
|
|
│ low │
|
||
|
|
│ close │
|
||
|
|
│ volume │
|
||
|
|
│ adjusted_close │
|
||
|
|
└────────────────────┘
|
||
|
|
|
||
|
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||
|
|
│ Strategy │ │ WatchlistStrategy│ │ Alert │
|
||
|
|
│──────────────────│ │──────────────────│ │──────────────────│
|
||
|
|
│ id (PK) │◄────│ id (PK) │ │ id (PK) │
|
||
|
|
│ user_id (FK) │ │ watchlist_id(FK) │ │ watchlist_id(FK) │
|
||
|
|
│ name │ │ strategy_id(FK) │ │ strategy_id(FK) │
|
||
|
|
│ description │ │ is_active │ │ type │
|
||
|
|
│ type (technical/ │ │ created_at │ │ trigger_type │
|
||
|
|
│ fundamental) │ │ conditions (JSONB)│ │ message_template │
|
||
|
|
│ conditions (JSONB)│ │ created_at │ │ triggered_at │
|
||
|
|
│ backtest_result │ │ updated_at │ │ resolved_at │
|
||
|
|
│ created_at │ └────────┬─────────┘ │ resolved_at │
|
||
|
|
│ updated_at │ │ │ created_at │
|
||
|
|
└──────────────────┘ │ └──────────────────┘
|
||
|
|
│
|
||
|
|
│ ┌──────────────────┐
|
||
|
|
└───►│ SectorRotation │
|
||
|
|
│──────────────────│
|
||
|
|
│ id (PK) │
|
||
|
|
│ date │
|
||
|
|
│ sector_ticker │
|
||
|
|
│ rank_now │
|
||
|
|
│ rank_previous │
|
||
|
|
│ rank_change │
|
||
|
|
│ momentum_20d │
|
||
|
|
│ momentum_50d │
|
||
|
|
│ momentum_200d │
|
||
|
|
│ relative_strength │
|
||
|
|
│ rotation_signal │
|
||
|
|
│ macro_context │
|
||
|
|
│ analysis_summary │
|
||
|
|
└────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
## Detailed Schema
|
||
|
|
|
||
|
|
### users
|
||
|
|
```sql
|
||
|
|
CREATE TABLE users (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||
|
|
password_hash VARCHAR(255), -- NULL if OAuth-only
|
||
|
|
name VARCHAR(255),
|
||
|
|
avatar_url TEXT,
|
||
|
|
timezone VARCHAR(50) DEFAULT 'UTC',
|
||
|
|
settings JSONB DEFAULT '{}', -- {currency: 'USD', theme: 'dark', alerts_enabled: true}
|
||
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### watchlists
|
||
|
|
```sql
|
||
|
|
CREATE TABLE watchlists (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
|
|
name VARCHAR(255) NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
is_default BOOLEAN DEFAULT FALSE,
|
||
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_watchlists_user ON watchlists(user_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### watchlist_items
|
||
|
|
```sql
|
||
|
|
CREATE TABLE watchlist_items (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
watchlist_id UUID NOT NULL REFERENCES watchlists(id) ON DELETE CASCADE,
|
||
|
|
ticker VARCHAR(20) NOT NULL,
|
||
|
|
type VARCHAR(20) NOT NULL CHECK (type IN ('stock', 'etf', 'index')),
|
||
|
|
custom_notes TEXT,
|
||
|
|
added_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Prevent duplicates: a watchlist can't have the same ticker twice
|
||
|
|
CREATE UNIQUE INDEX idx_watchlist_items_unique ON watchlist_items(watchlist_id, ticker);
|
||
|
|
```
|
||
|
|
|
||
|
|
### prices (TimescaleDB hypertable)
|
||
|
|
```sql
|
||
|
|
-- Hypertable for time-series price data
|
||
|
|
CREATE TABLE prices (
|
||
|
|
ticker VARCHAR(20) NOT NULL,
|
||
|
|
date TIMESTAMPTZ NOT NULL,
|
||
|
|
open DECIMAL(15,4),
|
||
|
|
high DECIMAL(15,4),
|
||
|
|
low DECIMAL(15,4),
|
||
|
|
close DECIMAL(15,4),
|
||
|
|
volume BIGINT,
|
||
|
|
adjusted_close DECIMAL(15,4),
|
||
|
|
PRIMARY KEY (ticker, date)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Convert to hypertable (TimescaleDB extension)
|
||
|
|
SELECT create_hypertable('prices', 'date');
|
||
|
|
|
||
|
|
-- Compression for older data (automated by Timescale policy)
|
||
|
|
CREATE POLICY prices_compress_policy ON prices
|
||
|
|
FOR ALL
|
||
|
|
USING (date < NOW() - INTERVAL '90 days');
|
||
|
|
|
||
|
|
-- Continuous aggregate for common timeframes
|
||
|
|
CREATE MATERIALIZED VIEW prices_daily
|
||
|
|
WITH (timescaledb.continuous) AS
|
||
|
|
SELECT ticker,
|
||
|
|
time_bucket('1 day', date) AS bucket,
|
||
|
|
first(open, date) AS open,
|
||
|
|
max(high) AS high,
|
||
|
|
min(low) AS low,
|
||
|
|
last(close, date) AS close,
|
||
|
|
sum(volume) AS volume
|
||
|
|
FROM prices
|
||
|
|
GROUP BY ticker, bucket;
|
||
|
|
```
|
||
|
|
|
||
|
|
### stock_profiles
|
||
|
|
```sql
|
||
|
|
CREATE TABLE stock_profiles (
|
||
|
|
ticker VARCHAR(20) PRIMARY KEY,
|
||
|
|
name VARCHAR(500),
|
||
|
|
exchange VARCHAR(20),
|
||
|
|
sector VARCHAR(100),
|
||
|
|
industry VARCHAR(200),
|
||
|
|
market_cap BIGINT,
|
||
|
|
description TEXT,
|
||
|
|
website TEXT,
|
||
|
|
ceo VARCHAR(255),
|
||
|
|
employees INTEGER,
|
||
|
|
pe_ratio DECIMAL(10,2),
|
||
|
|
eps DECIMAL(10,4),
|
||
|
|
dividend_yield DECIMAL(8,4),
|
||
|
|
beta DECIMAL(6,4),
|
||
|
|
last_updated TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### sec_filings
|
||
|
|
```sql
|
||
|
|
CREATE TABLE sec_filings (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
ticker VARCHAR(20) NOT NULL,
|
||
|
|
cik VARCHAR(20),
|
||
|
|
form_type VARCHAR(10) NOT NULL, -- 10-K, 10-Q, 8-K, 4, 13F, 13D, 13G
|
||
|
|
filing_date DATE NOT NULL,
|
||
|
|
report_date DATE,
|
||
|
|
accession_number VARCHAR(50),
|
||
|
|
url TEXT,
|
||
|
|
content_summary TEXT, -- AI-generated summary
|
||
|
|
key_metrics JSONB, -- Extracted financials from 10-K/10-Q
|
||
|
|
sentiment_score DECIMAL(5,4), -- NLP sentiment: -1 to +1
|
||
|
|
tags TEXT[], -- e.g., ['earnings', 'executive_change', 'litigation']
|
||
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_sec_filings_ticker ON sec_filings(ticker);
|
||
|
|
CREATE INDEX idx_sec_filings_form ON sec_filings(form_type);
|
||
|
|
CREATE INDEX idx_sec_filings_date ON sec_filings(filing_date);
|
||
|
|
```
|
||
|
|
|
||
|
|
### insider_trades
|
||
|
|
```sql
|
||
|
|
CREATE TABLE insider_trades (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
ticker VARCHAR(20) NOT NULL,
|
||
|
|
insider_name VARCHAR(500),
|
||
|
|
insider_title VARCHAR(500),
|
||
|
|
transaction_date DATE NOT NULL,
|
||
|
|
transaction_type VARCHAR(10), -- Buy, Sell, Gift, In-Ex
|
||
|
|
shares INTEGER,
|
||
|
|
price_per_share DECIMAL(10,4),
|
||
|
|
total_value DECIMAL(15,4),
|
||
|
|
shares_owned_after INTEGER,
|
||
|
|
filing_date DATE,
|
||
|
|
source_url TEXT,
|
||
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_insider_trades_ticker ON insider_trades(ticker);
|
||
|
|
CREATE INDEX idx_insider_trades_type ON insider_trades(transaction_type);
|
||
|
|
```
|
||
|
|
|
||
|
|
### strategies
|
||
|
|
```sql
|
||
|
|
CREATE TABLE strategies (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
|
|
name VARCHAR(255) NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
type VARCHAR(20) NOT NULL CHECK (type IN ('technical', 'fundamental', 'hybrid')),
|
||
|
|
conditions JSONB NOT NULL, -- Structured strategy conditions
|
||
|
|
backtest_results JSONB, -- Last backtest result
|
||
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### strategy_conditions (JSONB schema example)
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "technical",
|
||
|
|
"rules": [
|
||
|
|
{
|
||
|
|
"indicator": "rsi",
|
||
|
|
"operator": "lt",
|
||
|
|
"value": 30,
|
||
|
|
"description": "RSI below 30 (oversold)"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"indicator": "sma",
|
||
|
|
"params": {"period": 200, "source": "close"},
|
||
|
|
"operator": "gt",
|
||
|
|
"value": null,
|
||
|
|
"description": "Price above 200-day SMA"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"indicator": "volume",
|
||
|
|
"operator": "gt",
|
||
|
|
"value": 1.5,
|
||
|
|
"description": "Volume > 1.5x average 20-day volume"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"logic": "AND"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### watchlist_strategies
|
||
|
|
```sql
|
||
|
|
CREATE TABLE watchlist_strategies (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
watchlist_id UUID NOT NULL REFERENCES watchlists(id) ON DELETE CASCADE,
|
||
|
|
strategy_id UUID NOT NULL REFERENCES strategies(id) ON DELETE CASCADE,
|
||
|
|
is_active BOOLEAN DEFAULT TRUE,
|
||
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE UNIQUE INDEX idx_watchlist_strategies_unique ON watchlist_strategies(watchlist_id, strategy_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### alerts
|
||
|
|
```sql
|
||
|
|
CREATE TABLE alerts (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
watchlist_id UUID NOT NULL REFERENCES watchlists(id) ON DELETE CASCADE,
|
||
|
|
strategy_id UUID REFERENCES strategies(id),
|
||
|
|
ticker VARCHAR(20),
|
||
|
|
type VARCHAR(30) NOT NULL, -- strategy_trigger, sec_filing, sentiment, rotation, price
|
||
|
|
trigger_type VARCHAR(50),
|
||
|
|
message TEXT NOT NULL,
|
||
|
|
severity VARCHAR(10) DEFAULT 'info' CHECK (severity IN ('info', 'warning', 'critical')),
|
||
|
|
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'resolved', 'dismissed')),
|
||
|
|
triggered_at TIMESTAMPTZ DEFAULT NOW(),
|
||
|
|
resolved_at TIMESTAMPTZ,
|
||
|
|
metadata JSONB DEFAULT '{}'
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_alerts_watchlist ON alerts(watchlist_id);
|
||
|
|
CREATE INDEX idx_alerts_status ON alerts(status);
|
||
|
|
CREATE INDEX idx_alerts_triggered ON alerts(triggered_at);
|
||
|
|
```
|
||
|
|
|
||
|
|
### sector_rotations
|
||
|
|
```sql
|
||
|
|
CREATE TABLE sector_rotations (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
detection_date DATE NOT NULL,
|
||
|
|
sector_ticker VARCHAR(20) NOT NULL, -- XLK, XLF, etc.
|
||
|
|
sector_name VARCHAR(100),
|
||
|
|
rank_now INTEGER,
|
||
|
|
rank_previous INTEGER,
|
||
|
|
rank_change INTEGER,
|
||
|
|
momentum_20d DECIMAL(8,4),
|
||
|
|
momentum_50d DECIMAL(8,4),
|
||
|
|
momentum_200d DECIMAL(8,4),
|
||
|
|
relative_strength DECIMAL(8,4),
|
||
|
|
rotation_signal VARCHAR(20), -- 'in', 'out', 'stable', 'accelerating'
|
||
|
|
macro_context JSONB,
|
||
|
|
analysis_summary TEXT,
|
||
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_sector_rotations_date ON sector_rotations(detection_date);
|
||
|
|
CREATE INDEX idx_sector_rotations_signal ON sector_rotations(rotation_signal);
|
||
|
|
```
|
||
|
|
|
||
|
|
### screeners
|
||
|
|
```sql
|
||
|
|
CREATE TABLE screeners (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
|
|
name VARCHAR(255) NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
conditions JSONB NOT NULL,
|
||
|
|
results_count INTEGER DEFAULT 0,
|
||
|
|
last_run_at TIMESTAMPTZ,
|
||
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### screener_results
|
||
|
|
```sql
|
||
|
|
CREATE TABLE screener_results (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
screener_id UUID NOT NULL REFERENCES screeners(id) ON DELETE CASCADE,
|
||
|
|
ticker VARCHAR(20) NOT NULL,
|
||
|
|
match_score DECIMAL(5,4),
|
||
|
|
ranked_position INTEGER,
|
||
|
|
result_data JSONB,
|
||
|
|
generated_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_screener_results_screener ON screener_results(screener_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### peer_groups
|
||
|
|
```sql
|
||
|
|
CREATE TABLE peer_groups (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
ticker VARCHAR(20) NOT NULL,
|
||
|
|
peer_ticker VARCHAR(20) NOT NULL,
|
||
|
|
similarity_score DECIMAL(5,4), -- Based on sector, industry, market cap
|
||
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
|
|
UNIQUE (ticker, peer_ticker)
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_peer_groups_ticker ON peer_groups(ticker);
|
||
|
|
```
|
||
|
|
|
||
|
|
### rotation_alerts_preferences
|
||
|
|
```sql
|
||
|
|
CREATE TABLE rotation_alert_preferences (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
|
|
sectors TEXT[] NOT NULL DEFAULT '{}', -- ['tech', 'healthcare', 'energy', ...]
|
||
|
|
min_rank_change INTEGER DEFAULT 2,
|
||
|
|
email_enabled BOOLEAN DEFAULT TRUE,
|
||
|
|
push_enabled BOOLEAN DEFAULT TRUE,
|
||
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
## TimescaleDB Optimization
|
||
|
|
|
||
|
|
### Compression Policy
|
||
|
|
```sql
|
||
|
|
-- Compress data older than 90 days
|
||
|
|
SELECT add_compress_policy('prices', INTERVAL '90 days');
|
||
|
|
|
||
|
|
-- Reorder by ticker for better compression
|
||
|
|
SELECT add_reorder_policy('prices', 'ticker');
|
||
|
|
```
|
||
|
|
|
||
|
|
### Continuous Aggregates
|
||
|
|
```sql
|
||
|
|
-- 1-hour aggregates for intraday analysis
|
||
|
|
CREATE MATERIALIZED VIEW prices_hourly
|
||
|
|
WITH (timescaledb.continuous) AS
|
||
|
|
SELECT ticker,
|
||
|
|
time_bucket('1 hour', date) AS bucket,
|
||
|
|
first(open, date) AS open,
|
||
|
|
max(high) AS high,
|
||
|
|
min(low) AS low,
|
||
|
|
last(close, date) AS close,
|
||
|
|
sum(volume) AS volume
|
||
|
|
FROM prices
|
||
|
|
GROUP BY ticker, bucket;
|
||
|
|
|
||
|
|
-- 1-day aggregates for trend analysis
|
||
|
|
CREATE MATERIALIZED VIEW prices_daily
|
||
|
|
WITH (timescaledb.continuous) AS
|
||
|
|
SELECT ticker,
|
||
|
|
time_bucket('1 day', date) AS bucket,
|
||
|
|
first(open, date) AS open,
|
||
|
|
max(high) AS high,
|
||
|
|
min(low) AS low,
|
||
|
|
last(close, date) AS close,
|
||
|
|
sum(volume) AS volume
|
||
|
|
FROM prices
|
||
|
|
GROUP BY ticker, bucket;
|
||
|
|
```
|
||
|
|
|
||
|
|
### Indexes for Common Queries
|
||
|
|
```sql
|
||
|
|
-- Composite indexes for frequent query patterns
|
||
|
|
CREATE INDEX idx_prices_ticker_date ON prices(ticker, date DESC);
|
||
|
|
CREATE INDEX idx_prices_date_ticker ON prices(date, ticker);
|
||
|
|
CREATE INDEX idx_sec_filings_ticker_type ON sec_filings(ticker, form_type);
|
||
|
|
CREATE INDEX idx_insider_trades_ticker_date ON insider_trades(ticker, transaction_date DESC);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Data Ingestion Pipeline
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ DATA INGESTION PIPELINE │
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
|
||
|
|
│ │ Price │ │ SEC │ │ Alternative │ │
|
||
|
|
│ │ Ingestor │ │ Filing │ │ Data │ │
|
||
|
|
│ │ (hourly) │ │ Parser │ │ Pipeline │ │
|
||
|
|
│ │ │ │ (daily) │ │ (daily) │ │
|
||
|
|
│ └──────┬──────┘ └──────┬──────┘ └────────┬────────┘ │
|
||
|
|
│ │ │ │ │
|
||
|
|
│ ▼ ▼ ▼ │
|
||
|
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ PostgreSQL + TimescaleDB │ │
|
||
|
|
│ │ (prices, sec_filings, insider_trades) │ │
|
||
|
|
│ └─────────────────────────────────────────────────────┘ │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### Ingestion Schedules
|
||
|
|
| Data | Source | Frequency | Rate Limit |
|
||
|
|
|---|---|---|---|
|
||
|
|
| OHLCV (1min) | Massive/Polygon | Every 1 min (market hours) | Unlimited (paid tier) |
|
||
|
|
| OHLCV (1day) | Massive/Polygon | Daily close | Unlimited (paid tier) |
|
||
|
|
| SEC filings | EDGAR RSS/API | Every 15 min | 10 req/sec |
|
||
|
|
| Insider trades | EDGAR | Every 6 hours | 10 req/sec |
|
||
|
|
| Institutional holdings | EDGAR 13F | Quarterly (Feb, May, Aug, Nov) | 10 req/sec |
|
||
|
|
| Company news | Finnhub | Every 5 min | 60 req/min (free) |
|
||
|
|
| Sentiment scores | Finnhub + NLP pipeline | Every hour | Internal |
|
||
|
|
| Sector rotation | Computed from prices | Daily 6 AM UTC | Internal |
|
||
|
|
| Google Trends | pytrends | Daily | Rate limited |
|
||
|
|
| Reddit sentiment | Reddit API | Every 6 hours | Rate limited |
|