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.

5
Portfolio Buckets
13
Agent Node Kinds
12
Market Data Sources
1,200+
Tests

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

React Dashboard

Vite · TypeScript · React Flow

HTTP + WebSocket

FastAPI Backend

Python 3.11 · Pydantic v2 · SQLAlchemy 2

TimescaleDB

Postgres 16 · hypertables

Celery Worker + Beat

RedBeat · US/Eastern

LangGraph

Stop-Loss Monitor

Schwab WS · real-time

Redis 7

Broker · Pub/Sub · Sentinel

All environments run the same Docker Compose definition — differences are config only

03 — PORTFOLIO

The five buckets

Hard-coded target weights with bucket-specific risk rules. Drift beyond ±10% triggers an automatic rebalance proposal.

index_core 45%

Passive SPY / QQQ / BND — rebalanced on drift

largecap_growth 20%

LLM-selected large-caps · 3–6 month horizon

microcap_highvol 20%

Catalyst-driven microcaps

thematic_etfs 10%

Sector rotation (BOTZ, XBI, SOXX, ICLN…)

cash 5%

SGOV / T-bills buffer

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

data_fetcher
macro_context

Analyze

event_classifier
thesis_validator
post_event_analyzer
news_triage
pre_event_watcher

Risk

risk_python
risk_llm
risk_hybrid

Decide

decision_integrator

Execute

executor

Audit

audit_logger

Pipeline templates

daily_cycle

Pre-market briefing for largecap and thematic buckets · fires 9:35 AM ET

event_driven_core

Reactive pipeline · fires on news triage hits for watchlist tickers

microcap_restricted

Hard-gated for microcaps — lot splits, position caps, tight stops

pre_event_watcher

Pre-earnings / FDA decision posture · builds thesis before the event

post_event_analyzer

Beat/miss interpretation with deterministic math precomputed

protective_exit_only

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.

01

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

02

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

03

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%

04

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

05

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

Celery beat · 09:35 ET
Kill switch check
asyncio.gather — price · macro · screener · news triage
Index Agent
Largecap Agent
Microcap Agent
Thematic Agent
ProposedTrade list

Risk Agent

Python gates → LLM veto

Order Shaper
Executor
Audit Logger

07 — STOP-LOSS

Real-time protection

Per-position state

entry_price $50.00
stop_loss_pct 0.10 (10%)
take_profit_pct 0.25 (25%)
current_stop $45.00
peak_price $52.00 (high-water)
protection_active true

Proximity bands

> 5% above stop SAFE
3–5% above stop WARNING
< 3% above stop DANGER
< stop TRIGGERED

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_width = max(
  trail_pct × price,
  atr_mult × ATR(14),
  min_trail_pct × price
)
stop = max(stop, price − trail_width)

08 — DATA

12 market data integrations

Schwab Primary

OAuth REST + WebSocket ticks · real-time stop-loss feed

AlphaVantage News

Premium news + sentiment · 75 req/min cap · 5-min poll

SEC EDGAR Filings

8-K Atom feed · 30 s poll · uuid5 dedup (ON CONFLICT DO NOTHING)

FMP Fundamentals

Earnings, revenue, financial statements

FRED Macro

VIX · 10Y yield · DXY · cash-bucket scaler · 6 h sync

openFDA Biotech

Drug approvals → sponsor-ticker map → ticker pipeline

ClinicalTrials Biotech

Trial events routed via sponsor-ticker registry

Newswire Wire

Paid earnings + regulatory wire · 30 s poll market hours only

Alpaca Broker

Paper trading execution + crypto bridge

iShares / yFinance Fallback

ETF composition + historical bars when Schwab rate-limited

Finnhub Supplemental

Additional market data supplemental feed

IBKR TWS Broker

Interactive Brokers paper trading via TWS API sidecar

09 — STACK

The technology

Backend

Python 3.11
FastAPI + Uvicorn
Pydantic v2
SQLAlchemy 2.0 async

Database

TimescaleDB (Postgres 16)
Hypertables for time-series
Alembic (114 migrations)
asyncpg

Queue / Cache

Redis 7
Celery 5
RedBeat dynamic scheduler
Pub/Sub event bus

AI / Agents

LangGraph 0.2
langgraph-checkpoint-postgres
OpenAI + Anthropic
Ollama (local) + Mock client

Frontend

React 18 + TypeScript 5
Vite 5 + Zustand
Recharts + Lightweight Charts
React Flow + Dagre

Observability

Prometheus + Grafana
Langfuse LLM tracing
Discord webhook alerts
Per-component cooldown dedup

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.