Architecture
Layers
Section titled “Layers”| Layer | Stack |
|---|---|
| Backend | Python 3.11+, FastAPI 0.138.2, Uvicorn 0.48.0 |
| Data | SQLite, SQLAlchemy 2.0.51, Pydantic 2.13.4 |
| Market data | yfinance 1.5.1 and Yahoo Finance |
| Public data | SEC EDGAR (filings) and US Treasury (yield curve) — keyless, public domain |
| AI | Anthropic SDK 0.105.2, optional Claude calls (Haiku 4.5 + Sonnet 4.5), local deterministic fallback |
| Frontend | Single HTML shell, Bootstrap 5.3.2, Bootstrap Icons, Chart.js 4.4.0, vanilla JavaScript |
| Desktop | PyInstaller + pywebview native window, in-app auto-update |
| Quality | pytest, Pylint, compileall, pip-audit, Dependency Review, CodeQL |
Request flow
Section titled “Request flow”The browser dashboard talks to the FastAPI app over HTTP/JSON. Routers are thin HTTP adapters: they validate request shapes, call domain modules, and translate domain errors into responses. The modules read/write SQLite, call Yahoo Finance for market data, read public filings and rates from SEC EDGAR and the US Treasury, and optionally call Anthropic Claude when narration is requested and a key is configured.
Each outside source sits behind exactly one module — market_data for Yahoo,
edgar_service for the SEC, treasury_yield_curve for the curve — so a provider can be
swapped, throttled, or degraded in one place instead of at every call site. edgar_service
owns the SEC’s two hard rules: a declared contact address, and a ceiling of ten requests a
second.
market_data is the only module that names yfinance. It exposes nine ticker-keyed
accessors (get_info, get_fast_info, get_history, get_closes, get_news,
get_earnings_estimates, get_earnings_calendar, get_fund_holdings, search) under two
contracts: no accessor raises — a vendor error, a missing package, and an unknown symbol all
read as the empty value of the return type — and nothing vendor-shaped escapes, so DataFrames,
FastInfo bags, and NaN never reach a caller. set_adapter() swaps the whole vendor surface,
which is how the test suite runs without a socket. It deliberately does no caching: TTL policy
belongs to each caller, expressed with the ttl_cache decorator, so a memoised fetcher’s
lifetime is declared at the fetcher instead of hand-rolled beside it.
Portfolio integrity modules
Section titled “Portfolio integrity modules”Financial state and review orchestration are concentrated behind focused interfaces so callers cannot accidentally use different rules for the same Portfolio:
| Module | Owns | Important invariant |
|---|---|---|
portfolio_lifecycle.py |
Default creation, list/create/rename/delete, owned-record cleanup | Deleting a Portfolio removes its holdings, trades, snapshots, verdict history, DCA ledger, and BOOK:<id> narratives together. |
portfolio_valuation.py |
Live valuation, cost basis, realized + unrealized return, quote quality, daily history | Watchlist rows never enter totals; zero, non-finite, and missing quotes make quality explicit; total-return percentage uses open plus sold cost basis; only complete valuations may update daily snapshots. |
dca_ledger.py |
Plan persistence, catch-up, pending/applied/dismissed transitions, exact apply/undo | Catch-up is idempotent; no simulated buy mutates a holding until the user applies it; a plan with applied buys cannot be deleted until those buys are undone. |
narrative_cache.py |
Portfolio and ticker cache reads/writes, TTL/price freshness, verdict encoding, JSON validation | Portfolio narratives are isolated under BOOK:<portfolio_id>; stale or corrupt rows regenerate safely; incomplete valuations never produce confident Portfolio-level Claude narration. |
portfolio_review.py |
Review Inbox, trust coverage, period reports, type-aware research comparison, thesis cadence state | Reuses valuation/fee/income/overlap contracts; value change is not called investment return; Compare accepts only research-mode tickers of one security type. |
portfolio_planning.py |
Target eligibility and replacement, target-versus-actual drift, read-only buy rehearsal, all-Portfolio known-value pulse | Targets are integer basis points over the full eligible set; drift requires complete usable USD quotes; rehearsals write nothing and use pre-buy known value plus external cash. |
portfolio_records.py |
Annual realized recap and deterministically ordered portable ZIP export | Sale rows use stored facts; one read transaction materializes ordered CSVs and a checksummed manifest; limits are enforced before any response is emitted. |
backup_service.py |
WAL-safe snapshots, immutable read-only vault inspection, manual freshness, opt-in automatic policy, queued restore and safety copy | Cross-process creation/verification/pruning is serialized; publication never replaces a destination; automatic retention is limited to verified auto-* artifacts. |
Pure DCA scheduling and cost math remains in dca_service.py, while routers and AI/analytics
call these interfaces instead of reaching into each other’s private helpers.
Intelligence Engine
Section titled “Intelligence Engine”The service layer’s core is a modular Intelligence Engine split into two halves:
- Local Intelligence — always available, with deterministic evaluation performed on
your machine. It covers
investment_signal,market_regime,portfolio_exposure, andevent_calendar, producing verdicts, scenarios, regime classification, exposure breakdowns, peer comparisons, and calibration data. Its inputs can still be refreshed through FolioOrb’s bounded Yahoo Finance, SEC, and US Treasury lookups; it sends no Claude prompt. - Claude AI — optional. Lives in
ai_service.py, exposinggenerate_action_plan()andgenerate_news_themes(), backed by a 24-hour SQLite cache and live token cost tracking. It uses Claude Haiku 4.5 for fast narration and Claude Sonnet 4.5 for action plans.
Reading the book and narrating it are separate modules, so the AI router stays an HTTP adapter rather than the place the engine lives:
| Module | Interface |
|---|---|
verdict_pipeline.py |
scan_portfolio(), scan_ticker(), book_exposure() — one scan of a Portfolio, returned as a ScanResult the narration stages read. |
action_plan.py |
cache_type(), build_snapshot(), build_plan(), build_fallback() — turns a ScanResult into a Sonnet prompt and back, with a deterministic plan when Claude is unavailable. |
portfolio_briefing.py |
cache_type(), build_snapshot(), build_local(), build_briefing(), build_fallback() — the same shape for the daily briefing. |
ai_narrative.py |
narrative(), valuation_is_complete() — the cache-then-generate-then-fall-back sequence every narrated endpoint repeated. |
api_key_store.py |
save(), clear(), InvalidKeyError, KeyStorageError — rejects malformed keys before disk, persists a well-formed key with restricted permissions, hot-swaps the client, then reports connected only after a live heartbeat succeeds. |
Two supporting modules keep repeated mechanics in one place: holdings_repository.py
(active(), active_by_ticker(), active_tickers(), active_tickers_or_default(),
meta_map()) is the only place an active-holdings query is spelled out, and ttl_cache.py
(ttl_cache(), clear_all()) replaces the hand-rolled (expiry, value) dictionaries that
each fetcher used to carry.
The browser view is identical whether you open localhost:8000 yourself or launch the desktop
app — the desktop build (PyInstaller + pywebview) runs the same FastAPI server in-process behind
a native window, and can update itself in place.
Repository layout
Section titled “Repository layout”app/├── main.py FastAPI app, middleware, static assets, startup warmup├── config.py Environment-backed settings├── database.py SQLite engine/session and startup migrations├── models.py SQLAlchemy ORM models├── schemas.py Pydantic request/response contracts├── routers/ Thin HTTP adapters: stocks, portfolio, DCA, AI, news, review, system└── services/ Lifecycle, valuation, review, planning, records, backup, DCA, cache, analytics, signals, updates
desktop/ Desktop entry point (uvicorn + native window)packaging/ PyInstaller spec, Inno Setup script, app icons
templates/└── index.html Dashboard shell
static/├── css/style.css Dashboard design system├── js/core.js Escaping, fetch + endpoint cache, holding-panel registry├── js/dashboard.js Main dashboard behavior├── js/review-orbit.js Review workspace, planning, records, reports, compare, thesis, and vault UI├── js/analytics-charts.js Chart.js analytics widgets└── js/updates.js In-app update check and download