434 lines
13 KiB
SQL

-- =========================================
-- Extensions (optional but useful)
-- =========================================
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- for gen_random_uuid() if you want it later
-- =========================================
-- 1) Identity & Sessions
-- =========================================
CREATE TABLE IF NOT EXISTS app_user (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS app_session (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL,
last_seen_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_app_session_user_id ON app_session(user_id);
CREATE INDEX IF NOT EXISTS idx_app_session_expires_at ON app_session(expires_at);
-- =========================================
-- 2) Broker links + Zerodha auth artifacts
-- Mirrors: user_brokers.json, zerodha_sessions.json, zerodha_request_tokens.json
-- =========================================
-- Current connected broker record per user (plus "pending" fields)
CREATE TABLE IF NOT EXISTS user_broker (
user_id TEXT PRIMARY KEY REFERENCES app_user(id) ON DELETE CASCADE,
broker TEXT,
connected BOOLEAN NOT NULL DEFAULT FALSE,
access_token TEXT,
connected_at TIMESTAMPTZ,
api_key TEXT,
user_name TEXT,
broker_user_id TEXT,
-- pending fields (as you described)
pending_broker TEXT,
pending_api_key TEXT,
pending_api_secret TEXT,
pending_started_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_user_broker_broker ON user_broker(broker);
CREATE INDEX IF NOT EXISTS idx_user_broker_connected ON user_broker(connected);
-- Zerodha session history (can be multiple per user)
CREATE TABLE IF NOT EXISTS zerodha_session (
id BIGSERIAL PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
linked_at TIMESTAMPTZ NOT NULL,
api_key TEXT,
access_token TEXT,
request_token TEXT,
user_name TEXT,
broker_user_id TEXT
);
CREATE INDEX IF NOT EXISTS idx_zerodha_session_user_id ON zerodha_session(user_id);
CREATE INDEX IF NOT EXISTS idx_zerodha_session_linked_at ON zerodha_session(linked_at);
-- Latest/active request token per user (mirrors zerodha_request_tokens.json)
CREATE TABLE IF NOT EXISTS zerodha_request_token (
user_id TEXT PRIMARY KEY REFERENCES app_user(id) ON DELETE CASCADE,
request_token TEXT NOT NULL
);
-- =========================================
-- 3) Strategy config + engine status
-- Mirrors: strategy_config.json, engine_status.json
-- =========================================
-- Keep it simple: single-row table (config "current")
CREATE TABLE IF NOT EXISTS strategy_config (
id SMALLINT PRIMARY KEY DEFAULT 1,
strategy TEXT,
sip_amount NUMERIC,
sip_frequency_value INTEGER,
sip_frequency_unit TEXT,
mode TEXT, -- paper/live
broker TEXT,
-- legacy/optional fields
active BOOLEAN,
frequency TEXT,
frequency_days INTEGER,
unit TEXT,
next_run TIMESTAMPTZ
);
-- single-row engine status
CREATE TABLE IF NOT EXISTS engine_status (
id SMALLINT PRIMARY KEY DEFAULT 1,
status TEXT NOT NULL,
last_updated TIMESTAMPTZ NOT NULL
);
-- =========================================
-- 4) Logs / event streams
-- Mirrors: strategy_logs.json, engine.log (JSONL)
-- =========================================
-- strategy_logs.json
CREATE TABLE IF NOT EXISTS strategy_log (
seq BIGINT PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL,
level TEXT,
category TEXT,
event TEXT,
message TEXT,
run_id TEXT,
meta JSONB
);
CREATE INDEX IF NOT EXISTS idx_strategy_log_ts ON strategy_log(ts);
CREATE INDEX IF NOT EXISTS idx_strategy_log_run_id ON strategy_log(run_id);
CREATE INDEX IF NOT EXISTS idx_strategy_log_event ON strategy_log(event);
-- engine.log JSONL
CREATE TABLE IF NOT EXISTS engine_event (
id BIGSERIAL PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL,
event TEXT,
data JSONB,
message TEXT,
meta JSONB
);
CREATE INDEX IF NOT EXISTS idx_engine_event_ts ON engine_event(ts);
CREATE INDEX IF NOT EXISTS idx_engine_event_event ON engine_event(event);
-- =========================================
-- 5) Engine state (paper + derived)
-- Mirrors: state_paper.json, state.json
-- =========================================
-- state_paper.json
CREATE TABLE IF NOT EXISTS engine_state_paper (
id SMALLINT PRIMARY KEY DEFAULT 1,
initial_cash NUMERIC,
cash NUMERIC,
total_invested NUMERIC,
nifty_units NUMERIC,
gold_units NUMERIC,
last_sip_ts TIMESTAMPTZ,
last_run TIMESTAMPTZ,
sip_frequency_value INTEGER,
sip_frequency_unit TEXT
);
-- state.json (lighter)
CREATE TABLE IF NOT EXISTS engine_state (
id SMALLINT PRIMARY KEY DEFAULT 1,
total_invested NUMERIC,
nifty_units NUMERIC,
gold_units NUMERIC,
last_sip_ts TIMESTAMPTZ,
last_run TIMESTAMPTZ
);
-- =========================================
-- 6) Paper broker (positions, orders, trades, equity curve)
-- Mirrors: paper_broker.json
-- =========================================
-- overall cash snapshot (paper_broker.json top-level cash)
CREATE TABLE IF NOT EXISTS paper_broker_account (
id SMALLINT PRIMARY KEY DEFAULT 1,
cash NUMERIC NOT NULL
);
-- positions map: symbol -> qty, avg_price, last_price
CREATE TABLE IF NOT EXISTS paper_position (
symbol TEXT PRIMARY KEY,
qty NUMERIC NOT NULL,
avg_price NUMERIC,
last_price NUMERIC,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- orders list
CREATE TABLE IF NOT EXISTS paper_order (
id TEXT PRIMARY KEY,
symbol TEXT NOT NULL,
side TEXT NOT NULL, -- buy/sell
qty NUMERIC NOT NULL,
price NUMERIC,
status TEXT NOT NULL, -- new/filled/cancelled/rejected etc
timestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_paper_order_ts ON paper_order(timestamp);
CREATE INDEX IF NOT EXISTS idx_paper_order_symbol ON paper_order(symbol);
CREATE INDEX IF NOT EXISTS idx_paper_order_status ON paper_order(status);
-- trades list
CREATE TABLE IF NOT EXISTS paper_trade (
id TEXT PRIMARY KEY,
order_id TEXT REFERENCES paper_order(id) ON DELETE SET NULL,
symbol TEXT NOT NULL,
side TEXT NOT NULL,
qty NUMERIC NOT NULL,
price NUMERIC NOT NULL,
timestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_paper_trade_ts ON paper_trade(timestamp);
CREATE INDEX IF NOT EXISTS idx_paper_trade_symbol ON paper_trade(symbol);
-- equity_curve list
CREATE TABLE IF NOT EXISTS paper_equity_curve (
timestamp TIMESTAMPTZ PRIMARY KEY,
equity NUMERIC NOT NULL,
pnl NUMERIC
);
CREATE INDEX IF NOT EXISTS idx_paper_equity_curve_ts ON paper_equity_curve(timestamp);
-- =========================================
-- 7) MTM ledger + event ledger
-- Mirrors: mtm_ledger.csv, ledger.csv
-- =========================================
CREATE TABLE IF NOT EXISTS mtm_ledger (
timestamp TIMESTAMPTZ PRIMARY KEY,
nifty_units NUMERIC,
gold_units NUMERIC,
nifty_price NUMERIC,
gold_price NUMERIC,
nifty_value NUMERIC,
gold_value NUMERIC,
portfolio_value NUMERIC,
total_invested NUMERIC,
pnl NUMERIC
);
CREATE INDEX IF NOT EXISTS idx_mtm_ledger_ts ON mtm_ledger(timestamp);
CREATE TABLE IF NOT EXISTS event_ledger (
id BIGSERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
event TEXT NOT NULL,
nifty_units NUMERIC,
gold_units NUMERIC,
nifty_price NUMERIC,
gold_price NUMERIC,
amount NUMERIC
);
CREATE INDEX IF NOT EXISTS idx_event_ledger_ts ON event_ledger(timestamp);
CREATE INDEX IF NOT EXISTS idx_event_ledger_event ON event_ledger(event);
-- =========================================
-- 8) Market data cache (History Cache CSVs)
-- Mirrors: SYMBOL.csv {Date, Close}
-- =========================================
CREATE TABLE IF NOT EXISTS market_close (
symbol TEXT NOT NULL,
date DATE NOT NULL,
close NUMERIC NOT NULL,
PRIMARY KEY (symbol, date)
);
CREATE INDEX IF NOT EXISTS idx_market_close_symbol ON market_close(symbol);
CREATE INDEX IF NOT EXISTS idx_market_close_date ON market_close(date);
-- =========================================
-- 9) Bootstrap compatibility patch
-- Keeps bootstrap schema aligned with later migrations.
-- =========================================
ALTER TABLE app_session
ADD COLUMN IF NOT EXISTS ip TEXT,
ADD COLUMN IF NOT EXISTS user_agent TEXT;
ALTER TABLE user_broker
ADD COLUMN IF NOT EXISTS api_secret TEXT,
ADD COLUMN IF NOT EXISTS auth_state TEXT;
CREATE TABLE IF NOT EXISTS password_reset_otp (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
otp_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_password_reset_otp_email
ON password_reset_otp(email);
CREATE INDEX IF NOT EXISTS idx_password_reset_otp_expires_at
ON password_reset_otp(expires_at);
CREATE TABLE IF NOT EXISTS support_ticket (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
email TEXT NOT NULL,
subject TEXT NOT NULL,
message TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'NEW',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_support_ticket_email
ON support_ticket(email);
CREATE INDEX IF NOT EXISTS idx_support_ticket_created_at
ON support_ticket(created_at DESC);
CREATE TABLE IF NOT EXISTS broker_callback_state (
id TEXT PRIMARY KEY,
state_hash TEXT NOT NULL UNIQUE,
user_id TEXT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
session_id TEXT NOT NULL REFERENCES app_session(id) ON DELETE CASCADE,
broker TEXT NOT NULL,
flow TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_broker_callback_state_lookup
ON broker_callback_state(user_id, session_id, broker, flow, expires_at DESC);
CREATE TABLE IF NOT EXISTS execution_claim (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
run_id TEXT NOT NULL REFERENCES strategy_run(run_id) ON DELETE CASCADE,
mode TEXT NOT NULL,
logical_time TIMESTAMPTZ NOT NULL,
claimed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_execution_claim_scope
ON execution_claim(user_id, run_id, logical_time);
CREATE INDEX IF NOT EXISTS idx_execution_claim_run_claimed
ON execution_claim(run_id, claimed_at DESC);
CREATE TABLE IF NOT EXISTS run_leases (
run_id TEXT PRIMARY KEY REFERENCES strategy_run(run_id) ON DELETE CASCADE,
owner_id TEXT NOT NULL,
leased_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
heartbeat_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_run_leases_owner_expires
ON run_leases(owner_id, expires_at DESC);
CREATE TABLE IF NOT EXISTS support_request_audit (
id BIGSERIAL PRIMARY KEY,
endpoint TEXT NOT NULL,
ip_hash TEXT,
email_hash TEXT,
ticket_hash TEXT,
blocked BOOLEAN NOT NULL DEFAULT FALSE,
reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_support_request_audit_endpoint_ip_created
ON support_request_audit(endpoint, ip_hash, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_support_request_audit_ticket_created
ON support_request_audit(ticket_hash, created_at DESC);
ALTER TABLE live_equity_snapshot
ADD COLUMN IF NOT EXISTS positions_adjustment_value NUMERIC NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS broker_order_state (
local_order_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
run_id TEXT NOT NULL REFERENCES strategy_run(run_id) ON DELETE CASCADE,
logical_time TIMESTAMPTZ NOT NULL,
broker TEXT NOT NULL,
symbol TEXT NOT NULL,
side TEXT NOT NULL,
broker_order_id TEXT,
requested_qty NUMERIC NOT NULL,
filled_qty NUMERIC NOT NULL DEFAULT 0,
accounted_fill_qty NUMERIC NOT NULL DEFAULT 0,
requested_price NUMERIC,
average_price NUMERIC,
status TEXT NOT NULL,
broker_status TEXT,
status_message TEXT,
needs_reconciliation BOOLEAN NOT NULL DEFAULT FALSE,
last_checked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT chk_broker_order_state_status
CHECK (status IN ('PENDING','PARTIAL','FILLED','REJECTED','CANCELLED','UNKNOWN'))
);
CREATE INDEX IF NOT EXISTS idx_broker_order_state_user_run_status
ON broker_order_state(user_id, run_id, status);
CREATE INDEX IF NOT EXISTS idx_broker_order_state_reconcile
ON broker_order_state(user_id, run_id, needs_reconciliation, last_checked_at);
CREATE INDEX IF NOT EXISTS idx_broker_order_state_broker_order
ON broker_order_state(broker_order_id);
CREATE INDEX IF NOT EXISTS idx_broker_order_state_logical_time
ON broker_order_state(run_id, logical_time);