A quant portfolio management terminal: build strategies with an alpha layer on top of a beta (broad market index) layer, backtest them fast on real Norgate data, mix multiple strategies together, optimize allocations under risk constraints, and see the same code path run live.
Named for the Norse watchman who sees across all realms — the goal is full visibility into a strategy, both ex ante (forecasted risk/exposures before the fact) and ex post (realized performance attribution after the fact).
Functional. Backend engine, data pipeline, and all five frontend sections (Overview, Strategy Console, Portfolio Builder, Risk & Optimization, Live Trading) are wired up and verified against real Norgate data. Live Trading connects to Interactive Brokers paper accounts only — see Roadmap for what's still open.
Prerequisites: Python 3.12+, Node 22+, and the Norgate Data Updater running locally (Windows only — see Data & the Norgate cache). A Norgate Data subscription is required for real price/index data — see that section for what runs without one.
# Backend
cd backend
python -m venv .venv
.venv/Scripts/pip install -e ".[dev]" # .venv/bin/pip on macOS/Linux
cp .env.example .env # optional — only needed for live trading
.venv/Scripts/python -m uvicorn app.main:app --port 8000
# Frontend (separate terminal)
cd frontend
npm install
npm run devOpen http://localhost:3000. The frontend expects the backend at http://localhost:8000/api
by default — override with NEXT_PUBLIC_API_URL if needed.
Or run the whole stack in Docker (backend + frontend + the IB Gateway in paper mode; Linux containers, no host venv/node/Gateway needed):
cp .env.example .env # optional: IBKR credentials for the containerized Gateway
docker compose up --build # backend on :8000, UI on :3000, Gateway console on :6080/app/data (settings, saved strategies/portfolios, the Norgate Parquet cache, the blotter)
lives in a native Docker volume, not a host bind mount — every read/write stays inside the
Linux VM instead of crossing the Windows↔container filesystem bridge, which measured 300-800x
slower per file access than native (see docs/ENGINE_ISSUES.md §12).
Norgate sync runs via the container's own daily auto-sync (talks to NDU over HTTP — see below);
a host-side scripts/sync_norgate_cache.py run is no longer a supported path for the Docker
setup specifically (run it inside the container instead:
docker compose exec backend python scripts/sync_norgate_cache.py ...), though running the
whole backend natively (not in Docker, per the Quickstart above) still works exactly as before —
there's no bridge to worry about outside Docker. The MCP server runs inside the container too —
see docs/MCP.md for how a local agent registers it. The IB Gateway now runs
inside the stack (paper mode) — see Live trading in Docker below
for how login/2FA works.
Everything above gets you backtesting, strategy building, portfolio construction, and risk/ optimization. Two pieces are optional and only matter if you need them:
- Live paper trading needs an Interactive Brokers Gateway — either the one bundled in the Docker stack (see below) or TWS/Gateway running on the host — see backend/README.md.
- A real historical risk-free rate series (
scripts/sync_risk_free_rates.py) is a nice-to- have one-time backfill from Norgate (%3MTCM); everything falls back to a flat Settings value without it.
docker compose up starts an ibgateway service: IB Gateway (paper) inside a container via
IBC (the standard automation layer). You don't log into
the container interactively — the pieces are:
- Credentials — put
IBKR_USERNAME/IBKR_PASSWORD(your IBKR paper-account login) in the project-root.env(copy.env.example). IBC submits them automatically at container start, so the Gateway logs itself in. - The 2FA prompt — open http://localhost:6080 (the
novncbrowser console; password =IBKR_VNC_PASSWORDfrom.env) and you'll see the Gateway's desktop. When IBKR pushes the two-factor prompt, click Accept there. That's the whole login flow:.envcreds + one click in the browser console. - Key store / settings persist in
data/ibgateway(mounted, gitignored). If your account uses IBKR's Secure Login System, download the.jksfrom IBKR Client Portal → Settings → Secure Login System and drop it into that folder before first start.
With empty credentials the container still boots — it just sits at the login screen, which you
can log into manually through the same console. The backend is pre-wired to the container
Gateway (HEIMDALL_IBKR_HOST=ibgateway, port 4004). Host-installed TWS/Gateway remains a
supported alternative — see backend/README.md.
Details, port map, and caveats (trusted IPs, read-only 2FA) are in
docs/FUTURE_IDEAS.md.
| Section | Route | What it does |
|---|---|---|
| Overview | / |
Dashboard: AUM/exposure/revolver header stats, gross-vs-net-vs-benchmark chart, cost waterfall, NAV/financing chart, net-of-cost analytics, saved strategies |
| Strategy Console | /strategies |
Write an on_rebalance strategy in Python — against a fixed symbol list or a dynamic index universe — run it against real Norgate data, and save it to the strategy library. See docs/WRITING_STRATEGIES.md for the full guide: the contract, what's in scope, performance dos/don'ts, and worked examples |
| Portfolio Builder | /portfolio |
Mix multiple strategy sleeves (pick saved strategies or write ad-hoc code) with an allocation dial each, see the blended portfolio net of costs vs. benchmark |
| Risk & Optimization | /risk |
Bounds-constrained mean-variance/risk-budgeted/Treynor-Black optimizer across sleeves, with risk-profile presets, an efficient frontier chart, stress testing, and net-of-cost performance |
| Live Trading | /live |
Connect to an IBKR paper account, see current holdings vs. target weights, and place holdings-aware rebalancing orders through the same strategy/portfolio code path used in backtests |
| Settings | /settings |
Universe, benchmark, rebalance timing, leverage cap, mode, and the cost/financing model defaults (AUM, commission, slippage, MER, financing rate/spread, collateral haircut) |
| MCP (agent API) | python -m app.mcp |
Model Context Protocol server for local AI assistants: list strategies/portfolios, check data coverage, backtest strategies or saved portfolios, optimize, pull tear sheets — read-only by default (stdio); write tools (save/update/delete strategies, update settings) opt-in behind HEIMDALL_MCP_ENABLE_WRITE_TOOLS with an append-only audit log. See docs/MCP.md |
Every backtest is run net of a real institutional cost model, not just gross returns:
- Trading costs: commission + slippage, charged in bps of turnover at each rebalance.
- MER (management fee): accrued daily against NAV, same convention as a mutual fund.
- Financing: any gross exposure beyond 1.0x NAV is treated as financed via a modeled revolver, collateralized at 102% — the standard US equity securities-lending margin and the Canadian mutual-fund regulatory minimum for repo collateral — at a SOFR-proxy base rate plus a spread.
- TER (total expense ratio) is defined here as MER + realized trading expense ratio + realized financing expense ratio, annualized — deliberately explicit since the term is used inconsistently across the industry.
All of it is configurable in Settings (with sensible institutional defaults) and overridable per API call. See docs/ARCHITECTURE.md for the full mechanics and the research behind the default numbers.
A strategy can target a fixed symbol list, or a Norgate index/watchlist — in which case the engine restricts each day's snapshot to that day's actual point-in-time constituents (survivorship-bias-free), not today's list. Pick this in the Strategy Console, Portfolio Builder, or Risk & Optimization via the "Index universe" toggle.
Save a strategy once in the Strategy Console (name, code, and either fixed symbols or a
dynamic universe), then pick any number of saved strategies as sleeves in Portfolio Builder
or Risk & Optimization — no more re-pasting code per sleeve. Backed by
GET/POST/PUT/DELETE /api/strategy-library.
See docs/API.md for the full endpoint reference and docs/ARCHITECTURE.md for the design decisions behind the engine.
Most backtesting tools either fake speed by ignoring realistic execution timing, or bolt analytics on as an afterthought. Heimdall is built around a few constraints instead:
- No lookahead by construction. Norgate provides EOD data only. A signal computed on the
close of day N can only act at the close (MOC/auction) of day N+1, and only earns a
return starting N+1 → N+2. This is enforced in
BacktestEngine.run, not just documented as a convention. - Alpha is measured against beta, always. Every strategy return stream is decomposed
against its benchmark: active return, tracking error, information ratio, hit rate,
drawdown — not just standalone Sharpe/CAGR. See
ex_post.py. - One code path, backtest and live. A strategy is a function from data → target weights
(the
Strategyprotocol instrategy.py). Backtest and live execution are swappable adapters underneath that same function, not two separate implementations that can drift apart.
backend/
app/
data/ # DataFeed protocol + adapters: NorgateFeed (live), DuckDBFeed (Parquet cache),
# CompositeFeed (cache-first w/ live fallback)
engine/ # Strategy protocol, BacktestEngine (N -> N+1 timing + cost/NAV walk),
# strategy code runner, sleeves.py (multi-sleeve blending)
analytics/ # ex_post.py (realized metrics), ex_ante.py (Ledoit-Wolf shrinkage covariance +
# forecasted TE/VaR/CVaR), optimizer.py (mean-variance, risk-budgeted,
# Michaud resampling), fundamental_law.py, attribution.py (Brinson-Fachler),
# crowding.py, liquidity.py, stress_testing.py, treynor_black.py,
# costs.py (CostModel: commission/slippage/MER/financing/collateral)
live/ # IBKR paper-trading execution adapter (ibkr_client.py wraps ib_async),
# holdings-aware rebalancing, client-id pooling
reporting/ # Tear-sheet export (PDF via reportlab/matplotlib, Excel via openpyxl)
api/ # FastAPI routes — one module per resource, schemas.py for shared response shapes
core/ # Settings persistence, strategy/portfolio library persistence, blotter store,
# app config
scripts/ # Norgate smoke tests + the cache sync ETL job
tests/ # pytest suite (synthetic data, no live Norgate connection required)
frontend/
src/app/
page.tsx, strategies/, portfolio/, risk/, live/, settings/ # the six sections
components/ # NavShell, PerformanceChart & MultiSeriesChart (lightweight-charts),
# AllocationDial, FrontierChart, WaterfallChart, StrategyPicker, StatTile
src/lib/api.ts # fetch helpers, reads NEXT_PUBLIC_API_URL
docs/
ARCHITECTURE.md # design decisions, trust model, timing model, cost model
API.md # endpoint reference
WRITING_STRATEGIES.md # how to write on_rebalance code: contract, scope, performance, examples
The norgatedata Python library talks to the Norgate Data Updater over local IPC — that
updater only runs on Windows, and repeated IPC calls are too slow for a backtest loop.
Heimdall splits acquisition from consumption.
Windows: scripts/setup/download_norgate_data_updater.ps1 downloads the Data Updater installer
and launches it (skips the download if it's already installed, checked via the Windows registry;
pass -Force to reinstall anyway). Installing needs no login — a paid, active Norgate Data
subscription is only needed afterward, to actually sync real data through it.
- Acquisition (Windows-only):
backend/scripts/sync_norgate_cache.pypulls EOD bars and index membership vianorgatedataand writes them to Parquet underbackend/data/cache/. Run it once per close — matches the EOD-only rebalance timing anyway. - Consumption (portable):
DuckDBFeedreads that Parquet cache as one bulk multi-symbol SQL scan;CompositeFeed(what every API route actually uses, seedata/default_feed.py) tries the cache first and falls back to live Norgate on a miss. This is what lets the engine/backend run on Linux — only the sync step needs Windows.
cd backend
.venv/Scripts/python scripts/sync_norgate_cache.py --symbols SPY AAPL MSFT \
--watchlist "Dow Jones Industrial Average Current & Past" \
--index "Dow Jones Industrial Average"cd backend
.venv/Scripts/python -m pytest -q # unit tests, synthetic data, no Norgate required
.venv/Scripts/python -m ruff check . # lint
cd frontend
npx eslint . # lint
npx tsc --noEmit # typecheckSee docs/ROADMAP.md for the full history of what's shipped and
docs/ENGINE_ISSUES.md / docs/FUTURE_IDEAS.md for
open work. As of 2026-08-08, the audit's Phase E performance pass is done (see
docs/AUDIT_PLAN.md), the MCP server ships with all three phases —
read-only tools, write tools (HEIMDALL_MCP_ENABLE_WRITE_TOOLS, append-only audit log), and
live gating (HEIMDALL_MCP_ENABLE_LIVE_TOOLS, two-step rebalance confirmation) — with a global
background-job concurrency cap, and the Phase F path-traversal guards + auto-sync filesystem
lock are closed. What remains open: rate limiting (Phase F — deliberately deferred while
localhost-only), plus:
- Full process isolation for strategy code — the execution-timeout work above bounds slow
code (a wall-clock deadline between rebalance days, plus a background-job runtime limit), but
neither can forcibly kill a single
on_rebalancecall that itself hangs (an actual infinite loop). That needs subprocess-based execution with a real OS-level kill signal — a bigger architectural change, only worth it if this is ever exposed beyond a single trusted operator (see docs/ARCHITECTURE.md). - A GICS fundamental factor model as a v2 beyond the PCA statistical factor model, if PCA's purely statistical decomposition proves insufficient in practice (see docs/FUTURE_IDEAS.md #5).
This software is provided as-is, with no warranty of any kind, and no guarantee of
fitness for any purpose — see the Apache 2.0 license's warranty and liability terms below,
which govern in full. It is a research/engineering tool, not a broker, advisor, or regulated
financial product. Nothing in this repository — code, documentation, sample strategies,
backtest output, or Live Trading functionality — constitutes investment, tax, legal, or other
professional advice in any jurisdiction, and none of it should be relied on to make a real
financial decision. Backtested and simulated results do not represent actual trading and have
material limitations; past performance (real or simulated) is not indicative of future
results. Live Trading only ever connects to Interactive Brokers paper-trading ports (see
app/live/ibkr_client.py's PAPER_PORTS guard) — using it against real capital is outside
its designed and tested scope and is done entirely at your own risk. The authors and
contributors accept no liability for any loss, financial or otherwise, arising from the use
of this software.
Norgate Data is an independent, third-party data vendor and is used here only because it was the author's personal pick for a survivorship-bias-free EOD data source — this project is not sponsored by, affiliated with, or endorsed by Norgate Investor Services Pty Ltd. "Norgate Data" and "Norgate" are trademarks of their respective owner, referenced here solely to describe interoperability. No Norgate data is bundled with or redistributed by this repository (see Data & the Norgate cache) — a subscription and the Norgate Data Updater are required to pull real data yourself, subject to Norgate's own terms of service.
Apache 2.0 — see LICENSE. Note that a Norgate Data subscription (and its Windows-only Data Updater) is required to pull real price/index data; that's a separate paid third-party service, not covered by this repo's license.