AI PORTFOLIO MANAGEMENT SYSTEM
Apogee
A self-built AI-driven investment tool — real-time market data from 12 providers, LangGraph trading agents, five independent safety layers, and an audit trail you can hold up in court.
The name comes from the highest point in an orbit. Every position has an apogee — Apogee's job is to be there with a stop already set.
01 — PHILOSOPHY
Defense in depth, not trust
Defense in depth
Five independent safety layers — kill switch, circuit breaker, hard risk gates, LLM veto, audit checksum — each individually prevent a bad order. They don't trust each other. They don't trust the LLM. They don't trust me.
Code first, LLMs second
The LLMs propose; the Python rules dispose. Beat/miss math is computed deterministically. Order shaping picks a type from rules; the LLM only overrides within tight guardrails. Every LLM-influenced decision is logged with chosen_by.
Audit everything
Every order, every LLM call, every classifier decision, every kill switch flip writes a row. The audit log is append-only with SHA-256 row checksums. If something weird happens, the next move is opening the run trace — not asking what happened.
Default-safe boot — every flag is opt-in per environment
TRADING_ENABLED
false
LLM_MOCK_MODE
true
STOP_LOSS_ENABLED
false
LIVE_DATA_ENABLED
false
02 — ARCHITECTURE
How it's structured
03 — PORTFOLIO
The five buckets
Hard-coded target weights with bucket-specific risk rules. Drift beyond ±10% triggers an automatic rebalance proposal.
Passive SPY / QQQ / BND — rebalanced on drift
Passive SPY / QQQ / BND — rebalanced on drift
No active selection · long-only · 5% drift threshold
45%LLM-selected large-caps · 3–6 month horizon
LLM-selected large-caps · 3–6 month horizon
Concentration cap · max 5% per ticker
20%Catalyst-driven microcaps
Catalyst-driven microcaps
10% hard stop · 25% take-profit trim · trailing stop
20%Sector rotation (BOTZ, XBI, SOXX, ICLN…)
Sector rotation (BOTZ, XBI, SOXX, ICLN…)
Momentum + sentiment-driven · weekly review
10%SGOV / T-bills buffer
SGOV / T-bills buffer
Auto-scales up when VIX > 25
5%Hard limits — enforced at the executor level
MAX_SINGLE_POSITION_PCT
5%
no single ticker
MAX_BUCKET_DRIFT
±10%
before rebalance
MICROCAP_HARD_STOP
10%
below entry
MICROCAP_TAKE_PROFIT
25%
50% trim + breakeven stop
04 — AGENT MANAGER
LangGraph pipelines
Declarative NodeSpec DAGs — every node has a kind, a Pydantic config schema, and named input/output channels. 6 pipeline templates. 13 node kinds.
Ingest
Analyze
Risk
Decide
Execute
Audit
Pipeline templates
Pre-market briefing for largecap and thematic buckets · fires 9:35 AM ET
Reactive pipeline · fires on news triage hits for watchlist tickers
Hard-gated for microcaps — lot splits, position caps, tight stops
Pre-earnings / FDA decision posture · builds thesis before the event
Beat/miss interpretation with deterministic math precomputed
Read-only review mode · used in degraded or kill-switch states
05 — SAFETY
Five independent safety layers
Every one of them must fail open simultaneously for an unwanted trade to execute.
Kill Switch
Master cutoff — two sources of truth (DB row + host STOP file). Engaged manually via API or automatically by the circuit breaker / connection sentinel. Existing protective stops still fire when engaged.
POST /portfolio/kill-switch/engage · make kill-switch-engage · Discord red-embed alert
Circuit Breaker
Automatically engages the kill switch after a >3% daily portfolio loss. Resets at market open if conditions clear.
Daily loss threshold: 3% · Redis-backed state · per-environment isolation
Hard Python Risk Gates
Deterministic checks that run on every proposed trade before the LLM ever sees it. Cannot be bypassed or overridden.
Position cap 5% · bucket drift ±10% · cash floor 5% · top-3 ≤30% · VIX scaler 50% · microcap ≤2%
LLM Soft Veto
Claude reviews the trade thesis + current positions. Optional per-strategy (python / llm / hybrid modes). Vetoes logged to llm_veto_decisions for review.
RISK_AGENT_MODE: python | llm | hybrid · veto logged with full prompt + response
Audit Checksum Chain
Every order, LLM call, and kill switch flip writes an append-only row. Each row carries a SHA-256 checksum over (prev_hash, payload) — tampering with any historical row breaks the chain.
INSERT-only SQL grant · SHA-256 chain · full prompt/response in agent_evidence
06 — DAILY CYCLE
9:35 AM every market day
07 — STOP-LOSS
Real-time protection
Per-position state
Proximity bands
Stop boundary is strict less-than — a position at exactly the stop price has not broken support yet. Unit-tested in both directions.
Take-profit trim
At entry × 1.25 (25% gain):
- Trim 50% of position
- Raise stop to breakeven
- Arm trailing stop
Adaptive trailing stop
Trail width adapts to volatility — wider on volatile names so noise doesn't shake out the position.
trail_pct × price,
atr_mult × ATR(14),
min_trail_pct × price
)
stop = max(stop, price − trail_width)
08 — DATA
12 market data integrations
OAuth REST + WebSocket ticks · real-time stop-loss feed
Premium news + sentiment · 75 req/min cap · 5-min poll
8-K Atom feed · 30 s poll · uuid5 dedup (ON CONFLICT DO NOTHING)
Earnings, revenue, financial statements
VIX · 10Y yield · DXY · cash-bucket scaler · 6 h sync
Drug approvals → sponsor-ticker map → ticker pipeline
Trial events routed via sponsor-ticker registry
Paid earnings + regulatory wire · 30 s poll market hours only
Paper trading execution + crypto bridge
ETF composition + historical bars when Schwab rate-limited
Additional market data supplemental feed
Interactive Brokers paper trading via TWS API sidecar
09 — STACK
The technology
Backend
Database
Queue / Cache
AI / Agents
Frontend
Observability
10 — DECISIONS
Why these choices?
Why five safety layers instead of one well-built one?
Because I've seen the well-built one fail. Five layers with different dependency footprints are harder to fail open simultaneously. When one fails — and they will — the others catch it, and the failure surfaces in monitoring instead of in a fill report.
Why LangGraph instead of a custom DAG runtime?
LangGraph's checkpoint-postgres gives durable per-step state out of the box. Crash mid-run and the next worker resumes from the last completed step. Building that from scratch would have meant ~12,000 lines of runtime code instead of ~2,000 lines of node logic.
Why code-first, LLMs second?
Beat/miss math is computed deterministically before the LLM sees it. Order shaping picks an order type from Python rules; the LLM only overrides within tight guardrails. Every LLM-influenced decision is logged with chosen_by so an auditor can replay the path.
Why mock LLMs by default?
Three reasons: cost (10 daily-cycle debug runs = rounding error in mock, potentially hundreds of dollars live), speed (mock < 1 ms vs 2–10 s real), and safety (a mistake during local dev costs nothing). One env flag to switch; the entire pipeline exercises end-to-end.
Why TimescaleDB instead of Postgres + InfluxDB?
The query patterns are mostly relational — joining a position's KPI history against its strategy's trade ledger — with time-bucketing as a secondary concern. TimescaleDB serves both from one database. A separate time-series store would mean cross-store joins on every analytical query.
Why audit everything?
The worst day this system has is when a trade fires that I didn't expect. The difference between "I can replay every input the system saw" and "I can't" is the difference between fixing a bug and explaining a loss. Audit storage cost is rounding error.
11 — CONTACT
Want to talk systems?
Apogee is a solo project. If you want to discuss the architecture, agent design, or risk framework — reach out.