Design Doc: Streamlit → TypeScript Migration¶
Overview¶
Replace src/app.py (Streamlit) with a React/TypeScript SPA backed by a FastAPI service. The FastAPI layer also serves as the foundation for the Trade API (Phase 7), so both backtest UI and algo execution share one backend.
┌──────────────────┐ REST / WebSocket ┌──────────────────────┐
│ React / TS SPA │ ◄──────────────────────────► │ FastAPI (Python) │
│ (Vite + Plotly) │ │ backtest + trade │
└──────────────────┘ └──────────┬───────────┘
│
┌──────────▼───────────┐
│ Existing src/ │
│ data, strat, perf, │
│ param_opt, │
│ walk_forward │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ PostgreSQL (quantdb)│
└──────────────────────┘
1. Why Migrate¶
| Limitation in Streamlit | TypeScript solution |
|---|---|
| No interactive row-click on the Top 10 table to drill into equity curves | DataGrid with onRowClick → fetch + render equity curve for that param combo |
| Full page re-run on every interaction (Streamlit execution model) | SPA with granular state — only affected components re-render |
| No WebSocket for long-running optimization progress | FastAPI WebSocket endpoint streams trial-by-trial progress |
| Limited control over layout, routing, theming | Full React component tree with CSS/Tailwind |
| Tight coupling: UI + compute in one Python process | Decoupled: FastAPI scales independently; TS frontend is static |
| Cannot share backend with Trade API | FastAPI serves both backtest endpoints and trade endpoints |
2. Architecture¶
2.1 Directory Layout¶
frontend/ # React / TypeScript SPA (Vite + Tailwind + MUI)
src/
api/
client.ts # Axios instance (baseURL /api/v1, proxied to :8000)
backtest.ts # runOptimize(), runPerformance()
refdata.ts # useIndicators(), useSignalTypes(), useAssetTypes(), useConjunctions()
components/
ConfigDrawer.tsx # Left-side MUI Drawer — all form fields + Run button
Top10Table.tsx # MUI DataGrid — param rows + View Analysis / ★ Best buttons
MetricsCards.tsx # Strategy vs Buy & Hold metric cards (side-by-side)
HeatmapChart.tsx # Plotly heatmap (Sharpe vs window/signal grid)
EquityCurveChart.tsx # Plotly cumulative return + drawdown charts
pages/
BacktestPage.tsx # Single-page layout: topbar, drawer, results, analysis
types/
backtest.ts # BacktestConfig (form state) + API request/response types
refdata.ts # IndicatorRow, SignalTypeRow, AssetTypeRow, ConjunctionRow
App.tsx # → BacktestPage only
main.tsx # StrictMode + QueryClientProvider
index.css # @import "tailwindcss"
vite.config.ts # Tailwind plugin + /api proxy → localhost:8000
tsconfig.json
package.json
api/ # FastAPI backend
main.py # uvicorn entrypoint
routers/
backtest.py # /api/v1/backtest/*
strategies.py # /api/v1/strategies/* (Phase 7)
deployments.py # /api/v1/deployments/* (Phase 7)
schemas/
backtest.py # Pydantic request/response models
services/
backtest.py # Calls src/ modules (ParametersOptimization, Performance, WalkForward)
ws/
progress.py # WebSocket for optimization progress streaming
2.2 Shared Backend¶
The FastAPI service imports src/ modules directly (same Python process):
# api/services/backtest.py
from src.param_opt import ParametersOptimization
from src.perf import Performance
from src.walk_forward import WalkForward
from src.strat import StrategyConfig, SubStrategy, SignalDirection
from src.data import YahooFinance
No duplication of backtest logic — the TypeScript frontend replaces only the presentation layer.
3. API Endpoints — Backtest¶
All endpoints under /api/v1/backtest/. Trade API endpoints (from design-trade-api.md) live under /api/v1/strategies/ and /api/v1/deployments/.
3.1 Fetch Data¶
Request:
Response:
{
"rows": 3743,
"start_date": "2016-01-01",
"end_date": "2026-03-31",
"data": [
{"datetime": "2016-01-01", "price": 430.72},
...
]
}
3.2 Run Optimization¶
Request:
{
"symbol": "BTC-USD",
"start": "2016-01-01",
"end": "2026-04-01",
"mode": "single",
"indicator": "get_bollinger_band",
"strategy": "momentum_band_signal",
"trading_period": 365,
"fee_bps": 5.0,
"window_range": {"min": 5, "max": 100, "step": 5},
"signal_range": {"min": 0.25, "max": 2.50, "step": 0.25}
}
Multi-factor request:
{
"symbol": "BTC-USD",
"start": "2016-01-01",
"end": "2026-04-01",
"mode": "multi",
"trading_period": 365,
"fee_bps": 5.0,
"conjunction": "AND",
"factors": [
{
"indicator": "get_bollinger_band",
"strategy": "momentum_band_signal",
"data_column": "price",
"window_range": {"min": 10, "max": 100, "step": 5},
"signal_range": {"min": 0.25, "max": 2.50, "step": 0.25}
},
{
"indicator": "get_rsi",
"strategy": "reversion_band_signal",
"data_column": "price",
"window_range": {"min": 5, "max": 50, "step": 1},
"signal_range": {"min": 10.0, "max": 40.0, "step": 5.0}
}
]
}
Response:
{
"total_trials": 190,
"valid": 188,
"best": {
"window": 20,
"signal": 1.0,
"sharpe": 1.35
},
"top10": [
{"window": 20, "signal": 1.0, "sharpe": 1.35},
{"window": 25, "signal": 0.75, "sharpe": 1.28},
...
],
"grid": [
{"window": 5, "signal": 0.25, "sharpe": 0.42},
...
]
}
3.3 Optimization Progress (WebSocket)¶
Client sends the same optimize request JSON on connect. Server streams:
{"type": "progress", "completed": 42, "total": 190}
{"type": "progress", "completed": 43, "total": 190}
...
{"type": "complete", "result": { ... same as REST response ... }}
3.4 Run Single Performance¶
Request:
{
"symbol": "BTC-USD",
"start": "2016-01-01",
"end": "2026-04-01",
"mode": "single",
"indicator": "get_bollinger_band",
"strategy": "momentum_band_signal",
"trading_period": 365,
"fee_bps": 5.0,
"window": 20,
"signal": 1.0
}
Response:
{
"strategy_metrics": {
"Total Return": 1.45,
"Annualized Return": 0.12,
"Sharpe Ratio": 1.35,
"Max Drawdown": 0.23,
"Calmar Ratio": 0.52
},
"buy_hold_metrics": {
"Total Return": 2.10,
"Annualized Return": 0.18,
"Sharpe Ratio": 0.85,
"Max Drawdown": 0.55,
"Calmar Ratio": 0.33
},
"equity_curve": [
{"datetime": "2016-02-01", "cumu": 0.012, "buy_hold_cumu": 0.015, "dd": 0.0, "buy_hold_dd": 0.0},
...
]
}
3.5 Run Walk-Forward¶
Request: same as optimize + split_ratio field.
Response:
{
"best_window": 20,
"best_signal": 1.0,
"is_metrics": { "Sharpe Ratio": 1.50, ... },
"oos_metrics": { "Sharpe Ratio": 1.10, ... },
"overfitting_ratio": 0.27,
"equity_curve": [
{"datetime": "2016-02-01", "cumu": 0.012, "buy_hold_cumu": 0.015},
...
],
"split_date": "2021-02-15"
}
4. Streamlit → React Component Mapping¶
| Streamlit (app.py) | React Component | Notes |
|---|---|---|
| Sidebar: symbol, dates, asset type, fee | <Sidebar> |
Controlled form state |
| Sidebar: indicator, strategy, window, signal | <Sidebar> single-factor section |
|
| Sidebar: grid search ranges (win/sig min/max/step) | <Sidebar> grid section |
|
| Sidebar: multi-factor config (Add/Remove Factor) | <Sidebar> → dynamic <FactorRow> list |
|
| Sidebar: conjunction radio (AND/OR) | <Sidebar> radio group |
|
| Sidebar: walk-forward checkbox + split slider | <Sidebar> checkboxes section |
|
| Mode radio (Single/Multi) | <Sidebar> or page-level toggle |
|
| "Run Pipeline" button | <PipelineRunner> |
Triggers POST or WebSocket |
| Progress bar (trial N / total) | <PipelineRunner> → progress bar |
Fed by WebSocket stream |
| Best params success banner | <OptGrid> header alert |
|
| Top 10 DataGrid | <OptGrid> with MUI DataGrid |
Row-click → fetch equity curve |
| Sharpe Heatmap (single-factor) | <Heatmap> with Plotly Heatmap trace |
|
| Slice Heatmaps (multi-factor) | <Heatmap> per factor |
|
| Optuna contour / parallel-coords / importances | <OptunaViz> tabs |
Render Plotly JSON from backend |
| Strategy / Buy-Hold metrics side-by-side | <EquityCurve> metrics cards |
|
| Cumulative return line chart | <EquityCurve> Plotly Scatter |
|
| Drawdown area chart | <EquityCurve> Plotly Scatter fill |
|
| Walk-forward IS vs OOS table | <WalkForward> metrics table |
|
| Walk-forward equity curve + split vline | <WalkForward> Plotly chart |
|
| Overfitting ratio badge (green/yellow/red) | <OverfitBadge> |
|
| CSV download buttons | Browser <a download> from in-memory blob |
5. Row-Click Drill-In (Key New Feature)¶
The Top 10 table currently shows window | signal | sharpe rows. In TypeScript:
- User clicks a row in
<OptGrid>. - Frontend calls
POST /api/v1/backtest/performancewith that row'swindow+signal. - Response contains full equity curve + metrics.
<EquityCurve>re-renders below the table with the selected params.- A badge shows "Viewing: window=20, signal=1.0" above the chart.
For multi-factor, the row has window_0, signal_0, ..., window_N, signal_N. The request sends tuples.
This is the single most impactful UX improvement over Streamlit — interactive parameter exploration without re-running the full optimization.
6. Tech Stack¶
| Layer | Choice | Rationale |
|---|---|---|
| Frontend framework | React 19 + TypeScript | Ecosystem, type safety |
| Build tool | Vite | Fast HMR, simple config |
| Charts | Plotly.js (react-plotly.js) |
Already used in Streamlit; same trace types |
| Data table | MUI DataGrid | Row-click, sorting, filtering built-in |
| Styling | Tailwind CSS | Utility-first, consistent with modern React |
| State management | React Query (TanStack Query) | Server-state caching, auto-refetch |
| API client | Generated from OpenAPI spec | Type-safe, auto-sync with backend |
| Backend | FastAPI (Python) | Already in stack, async, auto-OpenAPI |
| Backend → src/ | Direct Python import | No duplication of backtest logic |
| WebSocket | FastAPI WebSocket | Optimization progress streaming |
7. Migration Phases¶
M-1: FastAPI Backtest Endpoints¶
Goal: All app.py computation accessible via REST.
- Create
api/directory with FastAPI app. - Implement
POST /api/v1/backtest/data(fetch data). - Implement
POST /api/v1/backtest/optimize(grid search, returns full grid + best + top10). - Implement
POST /api/v1/backtest/performance(single run, returns metrics + equity curve). - Implement
POST /api/v1/backtest/walk-forward(WF test, returns IS/OOS metrics + equity curve). - Pydantic request/response models with OpenAPI schema auto-generation.
- Unit tests for each endpoint (mock
src/modules, test HTTP contract). - CORS middleware configured for
localhost:5173(Vite dev server).
M-2: WebSocket Progress¶
Goal: Real-time optimization progress.
WS /api/v1/backtest/optimize/ws— streams{completed, total}per trial.- Backend uses optuna callback (same pattern as
_on_trialin app.py) → sends JSON frame. - Frontend
<PipelineRunner>connects, renders progress bar, closes oncompletemessage.
M-3: React Scaffold + Single-Page UI ✅¶
Goal: Full single-page application with collapsible config drawer.
Implemented:
- Vite + React + TypeScript scaffold in
frontend/ - Tailwind CSS v4 (
@tailwindcss/vite) + MUI v7 (@mui/material,@mui/x-data-grid) <ConfigDrawer>— slides in from left on "⚙ Configure" click- All form fields: symbol, date range, asset type (from
REFDATA.ASSET_TYPE), fee bps - Mode toggle: Single Factor / Multi Factor
- Indicator + Strategy dropdowns from
REFDATA.INDICATOR/REFDATA.SIGNAL_TYPE - Window/Signal ranges auto-fill from
REFDATA.INDICATOR.WIN_MINetc. on indicator change - Multi-factor: dynamic Add/Remove Factor rows with per-factor indicator, strategy, ranges
<BacktestPage>single-page layout:- Topbar with "⚙ Configure" button
- On Run: drawer closes, optimization runs, best params auto-selected, analysis renders immediately
- Top 10 table (MUI DataGrid) — each row has [View Analysis] or ★ Best button
- Clicking any row →
POST /backtest/performance→ replaces analysis section with 1 API call - Analysis section: metric cards + Sharpe heatmap + equity curve + drawdown charts
- Vite dev server proxy:
/api→http://localhost:8000(no CORS issues in dev) - All dropdowns sourced exclusively from
GET /api/v1/refdata/{table}(TanStack Query, staleTime=Infinity)
M-4: Results Display¶
Goal: Full parity with current Streamlit output.
<OptGrid>— Top 10 MUI DataGrid with row-click wiring.<Heatmap>— Plotly heatmap (single-factor) and slice heatmaps (multi-factor).<EquityCurve>— cumulative return + drawdown charts, strategy vs buy-hold metrics.<WalkForward>— IS vs OOS table, overfitting badge, equity curve with split line.- CSV download via in-memory Blob →
<a download>.
M-5: Row-Click Drill-In¶
Goal: The key new feature.
- Click any row in
<OptGrid>→POST /backtest/performancewith that row's params. <EquityCurve>below the table updates to show that combo's equity curve.- Multi-factor: sends tuple of windows/signals from the row's
window_N/signal_Ncolumns.
M-6: Polish + Cutover¶
Goal: Replace Streamlit as default UI.
- Remove
app.pyfromsrc/(or keep as legacy fallback). - Update
README.md— new launch instructions:cd api && uvicorn main:app+cd frontend && npm run dev. - Update
setup.shto install Node.js deps. - Update
.github/copilot-instructions.mdandAGENTS.mdwith new layout.
8. Pydantic Models (Backend)¶
# api/schemas/backtest.py
from pydantic import BaseModel
class RangeParam(BaseModel):
min: float
max: float
step: float
class FactorConfig(BaseModel):
indicator: str # e.g. "get_bollinger_band"
strategy: str # e.g. "momentum_band_signal"
data_column: str = "price"
window_range: RangeParam
signal_range: RangeParam
class OptimizeRequest(BaseModel):
symbol: str
start: str # "YYYY-MM-DD"
end: str
mode: str # "single" | "multi"
trading_period: int # 365 | 252
fee_bps: float = 5.0
# Single-factor fields
indicator: str | None = None
strategy: str | None = None
window_range: RangeParam | None = None
signal_range: RangeParam | None = None
# Multi-factor fields
conjunction: str | None = None # "AND" | "OR"
factors: list[FactorConfig] | None = None
class PerformanceRequest(BaseModel):
symbol: str
start: str
end: str
mode: str
trading_period: int
fee_bps: float = 5.0
# Single-factor
indicator: str | None = None
strategy: str | None = None
window: int | None = None
signal: float | None = None
# Multi-factor
conjunction: str | None = None
factors: list[FactorConfig] | None = None
windows: list[int] | None = None
signals: list[float] | None = None
class WalkForwardRequest(OptimizeRequest):
split_ratio: float = 0.5
class GridRow(BaseModel):
sharpe: float | None
# Dynamic keys: window/signal (single) or window_0/signal_0/... (multi)
# Use dict for flexibility
params: dict
class OptimizeResponse(BaseModel):
total_trials: int
valid: int
best: dict
top10: list[dict]
grid: list[dict]
class EquityPoint(BaseModel):
datetime: str
cumu: float
buy_hold_cumu: float
dd: float
buy_hold_dd: float
class PerformanceResponse(BaseModel):
strategy_metrics: dict
buy_hold_metrics: dict
equity_curve: list[EquityPoint]
class WalkForwardResponse(BaseModel):
best_window: int | list[int]
best_signal: float | list[float]
is_metrics: dict
oos_metrics: dict
overfitting_ratio: float
equity_curve: list[EquityPoint]
split_date: str
9. Open Questions¶
| # | Question | Options |
|---|---|---|
| 1 | Optuna plots — render server-side (return Plotly JSON) or client-side (return raw data)? | Server-side is simpler (Optuna has plot_contour etc. that return Plotly figures). Serialize fig.to_json() and render with react-plotly.js. |
| 2 | Auth — needed for MVP? | No. Add JWT/session auth when multi-user or Trade API goes live. |
| 3 | Deployment — single container or split? | Single Docker image with both FastAPI + static frontend for MVP. Split later for scale. |
| 4 | Keep Streamlit? — as fallback during migration? | Yes. Remove after M-6 is verified. |
10. Decommission Plan — app.py (Streamlit)¶
src/app.py stays in the repo throughout migration. When the TS frontend reaches parity (M-6), everything below can be removed in one go.
10.1 Deco Tag: [DECO:STREAMLIT]¶
All files and artefacts that exist solely for the Streamlit UI are tagged here. Nothing in src/ library modules (data.py, strat.py, perf.py, param_opt.py, walk_forward.py) is tagged — those are shared with the FastAPI backend.
| Artefact | Type | Reason |
|---|---|---|
src/app.py |
File | Streamlit UI — entire file |
streamlit |
pip dep | Only used by app.py |
INDICATORS dict in app.py |
Code | Hardcoded registry → replaced by REFDATA.INDICATOR |
STRATEGY_FUNCS / STRATEGY_NAMES dicts in app.py |
Code | Hardcoded registry → replaced by REFDATA.SIGNAL_TYPE |
ASSET_TYPES dict in app.py |
Code | Hardcoded registry → replaced by REFDATA.ASSET_TYPE |
DATA_COLUMNS dict in app.py |
Code | Hardcoded registry → replaced by REFDATA.DATA_COLUMN |
INDICATOR_DEFAULTS in src/strat.py |
Code | Hardcoded grid defaults → replaced by WIN_MIN..SIG_STEP columns on REFDATA.INDICATOR |
Conjunction ["AND", "OR"] literals in app.py |
Code | Hardcoded → replaced by REFDATA.CONJUNCTION |
10.2 One-Go Removal Checklist¶
When M-6 is verified:
- Delete
src/app.py - Remove
streamlitfromrequirements.txt - Remove
INDICATOR_DEFAULTSfromsrc/strat.py(backend reads from REFDATA cache) - Remove Streamlit launch instructions from
README.md - Update
setup.sh— drop streamlit, add Node.js frontend build - Update
AGENTS.md/.github/copilot-instructions.mdlayout table
11. REFDATA-Driven Dropdowns¶
Every dropdown / radio / selectbox in the current Streamlit UI maps to a REFDATA table. The TS frontend and FastAPI backend both consume REFDATA — the frontend fetches it via REST, the backend caches it at startup.
11.1 Mapping: Hardcoded → REFDATA¶
| UI Element | Hardcoded in app.py |
REFDATA Table | Key Columns |
|---|---|---|---|
| Indicator selectbox | INDICATORS dict |
REFDATA.INDICATOR |
DISPLAY_NAME (label), METHOD_NAME (value) |
| Strategy selectbox | STRATEGY_FUNCS / STRATEGY_NAMES |
REFDATA.SIGNAL_TYPE |
DISPLAY_NAME (label), FUNC_NAME (value) |
| Asset type selectbox | ASSET_TYPES dict |
REFDATA.ASSET_TYPE |
DISPLAY_NAME (label), TRADING_PERIOD (value) |
| Data column selectbox | DATA_COLUMNS dict |
REFDATA.DATA_COLUMN |
DISPLAY_NAME (label), COLUMN_NAME (value) |
| Conjunction radio | ["AND", "OR"] literal |
REFDATA.CONJUNCTION |
DISPLAY_NAME (label), NAME (value) |
| Grid search defaults | INDICATOR_DEFAULTS in strat.py |
REFDATA.INDICATOR |
WIN_MIN, WIN_MAX, WIN_STEP, SIG_MIN, SIG_MAX, SIG_STEP (columns on same table) |
| Broker selectbox (Phase 7) | not yet built | REFDATA.APP |
NAME (label) |
| Ticker mapping (Phase 7) | not yet built | REFDATA.TICKER_MAPPING |
DATA_TICKER → BROKER_TICKER |
11.2 REFDATA REST Endpoint¶
Returns all rows from the named REFDATA table. The backend validates table_name against an allow-list (same pattern as SP_GET_ENUM).
Response example (GET /api/v1/refdata/indicator):
[
{"indicator_id": 1, "name": "bollinger", "display_name": "Bollinger Band (z-score)", "method_name": "get_bollinger_band"},
{"indicator_id": 2, "name": "sma", "display_name": "SMA", "method_name": "get_sma"},
{"indicator_id": 3, "name": "ema", "display_name": "EMA", "method_name": "get_ema"},
{"indicator_id": 4, "name": "rsi", "display_name": "RSI", "method_name": "get_rsi"}
]
Joined endpoint for indicator defaults:
[
{
"indicator_id": 1, "name": "bollinger", "display_name": "Bollinger Band (z-score)",
"method_name": "get_bollinger_band",
"defaults": {"win_min": 10, "win_max": 100, "win_step": 5, "sig_min": 0.25, "sig_max": 2.50, "sig_step": 0.25}
},
...
]
12. REFDATA Caching Layer¶
12.1 Problem¶
REFDATA tables are small (< 100 rows each), rarely change, and are read on every request (dropdown population, parameter validation, indicator lookup). Hitting PostgreSQL on every API call is unnecessary.
12.2 Design¶
┌──────────────┐
│ PostgreSQL │
│ :5433 │
└──────┬───────┘
│ startup + refresh
┌──────▼───────┐
│ RefDataCache │ (in-process dict)
│ {table: rows}│
└──────┬───────┘
│ cache hit
┌────────────┼────────────┐
│ │ │
┌───────▼──┐ ┌─────▼────┐ ┌───▼──────┐
│ /refdata │ │ optimize │ │ validate │
│ endpoint │ │ service │ │ config │
└──────────┘ └──────────┘ └──────────┘
12.3 Implementation¶
# api/services/refdata_cache.py
import logging
from functools import lru_cache
from typing import Any
import psycopg
logger = logging.getLogger(__name__)
# Allow-list of tables the cache can load (prevents SQL injection)
REFDATA_TABLES = frozenset({
"indicator", "signal_type", "asset_type", "data_column",
"conjunction", "ticker_mapping", "app",
"api_limit", "tm_interval", "order_state", "trans_state",
})
class RefDataCache:
"""In-process cache for REFDATA tables.
Loaded once at startup, refreshable via .refresh().
Thread-safe for reads (dict is immutable after load).
"""
def __init__(self, conninfo: str):
self._conninfo = conninfo
self._store: dict[str, list[dict[str, Any]]] = {}
def load_all(self) -> None:
"""Fetch all REFDATA tables into memory."""
with psycopg.connect(self._conninfo) as conn:
for table in REFDATA_TABLES:
self._store[table] = self._fetch_table(conn, table)
logger.info("RefDataCache loaded %d tables", len(self._store))
def get(self, table: str) -> list[dict[str, Any]]:
"""Return cached rows for a REFDATA table."""
if table not in REFDATA_TABLES:
raise ValueError(f"Unknown REFDATA table: {table}")
return self._store.get(table, [])
def get_by_id(self, table: str, id_col: str, id_val: int) -> dict[str, Any] | None:
"""Lookup a single row by its ID column."""
for row in self.get(table):
if row.get(id_col) == id_val:
return row
return None
def get_indicator_defaults(self) -> dict[str, dict]:
"""Return {method_name: {win_min, win_max, ...}} for backward compat."""
result = {}
for r in self.get("indicator"):
result[r["method_name"]] = {
"win_min": r["win_min"], "win_max": r["win_max"],
"win_step": r["win_step"], "sig_min": float(r["sig_min"]),
"sig_max": float(r["sig_max"]), "sig_step": float(r["sig_step"]),
}
return result
def refresh(self) -> None:
"""Re-fetch all tables (call after REFDATA changes)."""
self.load_all()
@staticmethod
def _fetch_table(conn, table: str) -> list[dict[str, Any]]:
with conn.cursor() as cur:
cur.execute(f"SELECT * FROM refdata.{table}") # table is from allow-list
cols = [desc.name for desc in cur.description]
return [dict(zip(cols, row)) for row in cur.fetchall()]
12.4 FastAPI Integration¶
# api/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends
from api.services.refdata_cache import RefDataCache
cache = RefDataCache(conninfo="host=localhost port=5433 dbname=quantdb ...")
@asynccontextmanager
async def lifespan(app: FastAPI):
cache.load_all() # warm cache at startup
yield # app runs
# no cleanup needed
app = FastAPI(lifespan=lifespan)
def get_cache() -> RefDataCache:
return cache
@app.get("/api/v1/refdata/{table_name}")
def get_refdata(table_name: str, cache: RefDataCache = Depends(get_cache)):
return cache.get(table_name)
@app.post("/api/v1/refdata/refresh")
def refresh_refdata(cache: RefDataCache = Depends(get_cache)):
cache.refresh()
return {"status": "ok"}
12.5 Cache Properties¶
| Property | Value |
|---|---|
| Storage | In-process dict[str, list[dict]] |
| Warmup | load_all() at FastAPI startup via lifespan |
| Staleness | Acceptable — REFDATA changes are rare (admin-only). Manual refresh via POST /refdata/refresh. |
| Thread safety | Reads are safe (immutable snapshot). refresh() replaces the whole dict atomically. |
| DB target | localhost:5433 (PostgreSQL via AWS SSM port-forward) |
| Fallback | If DB unreachable at startup, log error and fail fast — REFDATA is required for the app to function. |
| No TTL | No automatic expiry. REFDATA is admin-managed; changes are deployed then POST /refdata/refresh. |