Architecture¶
Overview¶
openfin is a personal investment operations platform. Core invariant: Collect → Persist → Reason → Decide. Every data fetch writes to SQLite before rendering or analysis. The CLI and Telegram bot share rendering through presenters, the web dashboard reads the same SQLite, and AI agents drive interpretation through structured handoff points.
The system is agent-external: the CLI fetches, computes, stores, and renders deterministically. It never calls LLM APIs. All interpretation happens in the calling agent session.
Project Structure¶
src/openfin/
cli/ # Click CLI — parse input, call FinanceApplication, print via presenter
api/ # FastAPI — read-only views into SQLite and report artifacts
services/ # Service protocols, implementations, orchestration, presenters
protocols.py # Protocol classes (one per service boundary)
finance_application.py # Composition root — delegates to injected services
brokerage.py # SnapTradeBrokerageService
market.py # FinnhubMarketDataService
news.py # FinnhubNewsService
search_service.py# BraveSearchService
social.py # DiscordSocialService
macro.py # FredMacroService
sec.py # EdgarSECService
thesis.py # YamlThesisService + thesis models + condition evaluation
demo.py # DemoBrokerageService (fixture data)
presenters.py # Domain model → Markdown (shared by CLI + Telegram)
weekly_review.py # WeeklyReviewOrchestrator
daily_brief.py # DailyBriefOrchestrator
scoring.py # Rubric loading, composite scores
event_application.py # Event bus, Telegram bot, cron
libs/ # Low-level API adapters (wrapped by services/)
models/ # Pydantic domain models — contracts at every boundary
db/ # SQLAlchemy ORM, engine, repository
migrations/ # Alembic schema migrations
utils/ # Shared parsing, formatting, constants
frontend/ # React SPA (TanStack Router + Query, Recharts, Tailwind)
src/client/ # Auto-generated TypeScript from OpenAPI schema
artifacts/
openfin.db # SQLite (WAL mode)
reports/ # JSON + Markdown review artifacts
Change Propagation¶
When you change something, other layers are affected. This section tells you what else to check.
Adding/modifying a Pydantic model (models/)¶
Models flow through the entire stack. A field change can silently break 4+ consumers.
models/ (Pydantic)
→ services/ (return types, snapshot serialization)
→ services/presenters.py (rendering)
→ api/schemas/ (import, don't redefine)
→ api/routers/ (serialization)
→ FastAPI OpenAPI spec
→ frontend/src/client/ (regenerate with @hey-api/openapi-ts)
→ db/payload_version.py (bump version if shape stored in data_snapshots)
Checklist:
1. Update the model in models/
2. Check presenters.py for rendering that references changed fields
3. Check api/routers/ if the model is served via API
4. Regenerate TS client if API schema changed
5. If stored in data_snapshots: bump version in db/payload_version.py, add upgrade function
Adding a new external data source¶
libs/{provider}.py # Low-level adapter (HTTP calls, parsing, rate limiting)
→ services/{name}.py # Service class (owns credentials, wraps adapter)
→ services/protocols.py # Add Protocol definition
→ services/finance_application.py # Add kwarg + _default_ factory + delegation methods
→ models/{domain}.py # Return types
→ cli/{group}.py # CLI command(s)
→ services/presenters.py # Rendering
Adding a new CLI command¶
cli/{group}.py # Command function (parse args, call app, render)
uses: cli/_common.py # make_app(ctx), run_async(), _privacy()
uses: services/finance_application.py # Business logic
uses: services/presenters.py # Output rendering
Commands should be thin: parse input → make_app(ctx).method() → render_*(result) → click.echo(). Most are 3–5 lines.
Adding a Telegram bot command¶
services/event_application.py # Subscribe handler to command.{name} event
uses: services/finance_application.py
uses: services/presenters.py
uses: services/telegram_formatter.py # Markdown → Telegram HTML
Modifying the weekly/daily review pipeline¶
services/weekly_review.py # Data gathering, context building, artifact persistence
reads: services/finance_application.py (all fetch methods)
reads: db/repository.py (prior scores, snapshots)
writes: artifacts/reports/weekly/{DATE}/{RUN_ID}/
writes: db tables (review_runs, data_snapshots)
See docs/dev/data-flow.md for the full artifact tree and phase breakdown.
Service Architecture¶
FinanceApplication is the composition root. Interface layers (CLI, Telegram, API) never call providers directly.
# Production — CLI commands use this (reads --demo flag from click context):
app = FinanceApplication.from_ctx(ctx)
# Production — non-CLI callers (services from env credentials):
app = FinanceApplication.from_env()
# Demo mode (fixture brokerage data, no credentials):
app = FinanceApplication.demo()
# Testing (inject stubs — all 8 services are required):
app = FinanceApplication(brokerage=FakeBrokerage(), market_data=..., thesis=StubThesis(), ...)
Service → Protocol → Adapter mapping¶
| Protocol | Service impl | Adapter (libs/) |
Data |
|---|---|---|---|
BrokerageService |
SnapTradeBrokerageService |
snaptrade_sdk.py |
Accounts, positions, orders |
MarketDataService |
FinnhubMarketDataService |
market_data.py |
Quotes, overview, forex, commodities, earnings |
NewsService |
FinnhubNewsService |
market_data.py |
Per-symbol, watchlist, general news |
SearchService |
BraveSearchService |
brave_search.py |
Web search, symbol research |
SocialService |
DiscordSocialService |
discord_signals.py |
Discord signals, radar |
MacroService |
FredMacroService |
macro_data.py |
Fed funds, treasury, CPI, unemployment |
SECService |
EdgarSECService |
sec_data.py |
Filing index, 10-K/10-Q text |
ThesisService |
YamlThesisService |
— (file I/O) | Thesis CRUD, watchlist derivation |
Snapshot persistence¶
Every FinanceApplication method delegates to a service, then calls _store_snapshots(). Services don't know about persistence — it's a cross-cutting concern on the application layer.
async def get_quotes(self, symbols):
report = QuoteReport(entries=await self._market_data.fetch_quotes(self.client, symbols))
self._store_snapshots("quote", [(e.symbol, e.model_dump_json()) for e in report.entries])
return report
Models (models/)¶
Pydantic models are contracts at every boundary. extra="forbid" catches bad data at construction.
| Module | Key Models |
|---|---|
brokerage.py |
BrokerageAccount, Position, Order, AccountReport, PositionReport, OrderReport |
market.py |
QuoteSnapshot, IndexPerformance, SectorPerformance, EarningsEntry, NewsArticle, SearchResult |
macro.py |
MacroIndicator, MacroSnapshot |
application.py |
MarketOverview, QuoteReport, WatchlistReport, EarningsReport, NewsReport, SearchReport |
report.py |
WeeklyReport, Decision, EvidenceRecord, RubricScore, SymbolScorecard |
social.py |
SocialSignalSummary, ChannelSignal, RadarItem |
daily_brief.py |
DailyBrief, ThesisDailySection, CatalystAlert |
sec.py |
FilingEntry, FilingDocument |
Persistence¶
SQLite (artifacts/openfin.db)¶
WAL mode. SQLAlchemy ORM + Alembic migrations.
| Table | Purpose |
|---|---|
data_snapshots |
Every fetch writes here (JSON payload, timestamped, batch_id grouped). Dashboard reads time-series from this. |
rubric_scores |
Per-symbol scores from agent review (metric, score, rationale, composite, action) |
decision_evidence |
Evidence audit trail (claim, source URL, attribution) |
review_runs |
Review run metadata (status, dates) |
thesis_snapshots |
Thesis audit log (full YAML payload per save) |
review_annotations |
Agent narrative annotations (field, value) |
File artifacts¶
artifacts/reports/weekly/{DATE}/{RUN_ID}/
inputs.json # Raw collected data + portfolio context
overview.md # Compact overview
theses/{SLUG}.md # Per-thesis scoring packets
holdings/{SYM}.md # Per-holding scoring packets
scoring.json # Scores + evidence
decisions.json # Final decisions + risk snapshot
report.md # Final report
~/.openfin/theses/{slug}.yaml # Investment thesis definitions (user data)
~/.openfin/scoring/*.yaml # Rubric definitions
Environment & Credentials¶
Credentials live in ~/.openfin/credentials.toml (TOML sections map to env vars). load_credentials() is called at CLI/API startup. Use openfin --demo to bypass credentials with fixture data.
| Source | Env Var | Service |
|---|---|---|
| Finnhub | FINNHUB_API_KEY |
FinnhubMarketDataService, FinnhubNewsService |
| SnapTrade | SNAPTRADE_* (4 vars) |
SnapTradeBrokerageService |
| FRED | FRED_API_KEY |
FredMacroService |
| SEC EDGAR | SEC_USER_AGENT |
EdgarSECService |
| Brave Search | BRAVE_API_KEY |
BraveSearchService |
| Telegram | TELEGRAM_BOT_TOKEN |
EventServiceApplication |
| Cloudflare D1 | CLOUDFLARE_* (3 vars) |
DiscordSocialService |
All optional — missing keys produce empty results, not crashes.