Skip to content

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.