Skip to content

Architecture

Overview

bo-finance is a personal investment operations platform with an agent-external design: the CLI is a structured data tool that fetches, computes, stores, and renders. It never calls LLM APIs. All interpretation — evidence extraction, thesis evaluation, narrative generation — happens in the calling agent session.

graph TB subgraph Agents["Agent Layer (external)"] CC[Claude Code] SDK[Agent SDK App] Human[Human Operator] end subgraph Interface["Interface Layer"] CLI["Click CLI (bof)"] API["Web API (planned)"] MCP["MCP Server (planned)"] end subgraph Application["Application Layer"] FA[FinanceApplication] WR[WeeklyReviewOrchestrator] SS[ScoringService] TS[ThesisService] EA[EventServiceApplication] end subgraph Integration["Integration Layer"] Finnhub[market_data.py<br/>Finnhub] ST[snaptrade_sdk.py<br/>SnapTrade] FRED[macro_data.py<br/>FRED] SEC[sec_data.py<br/>SEC EDGAR] Brave[brave_search.py<br/>Brave Search] end subgraph Persistence["Persistence Layer"] DB[(SQLite<br/>artifacts/bof.db)] YAML[YAML Config<br/>investments/] Reports[Reports<br/>JSON + Markdown] end Agents --> Interface Interface --> Application Application --> Integration Application --> Persistence

Any agent works — Claude Code, a custom Agent SDK app, or a human can drive the same workflows. The CLI handles everything deterministically; agents provide judgment through score assignment, evidence recording, and narrative annotation.

Project Structure

src/bo_finance/
  cli/           # Interface adapter (Click CLI)
  services/      # Application use-cases and orchestration
  libs/          # External provider adapters
  models/        # Pydantic contracts and typed DTOs
  db/            # SQLAlchemy ORM, engine, repository
  migrations/    # Alembic schema migrations
  utils/         # Shared parsing, formatting, constants, redaction

investments/
  watchlist.txt  # Tracked symbols (one per line)
  theses/        # Per-symbol investment thesis YAML
  scoring/       # Rubric YAML definitions
  reports/       # Persisted weekly review artifacts (JSON + MD)

artifacts/
  bof.db         # SQLite database (WAL mode)

Layer Responsibilities

Interface Layer (cli/)

Parses input, calls application services, renders output. Owns no business logic or provider-specific code.

bof account   list
bof portfolio positions
bof order     list
bof quote     get
bof market    overview | macro
bof watchlist list | add | remove
bof earnings  list
bof news      get | watchlist
bof search    web | symbol
bof thesis    list | show | check
bof sec       filings | read
bof review    weekly | rubric | score | evidence | annotate | finalize
bof db        migrate | score-history | latest | query | tables
bof service   run

The CLI is async-first: FinanceApplication and EventServiceApplication are async context managers, and a run_async() helper bridges Click's sync commands to the async runtime.

Current adapter: Click CLI (bof). Planned adapters: FastAPI/HTTP, MCP server — all sharing the same application services.

Application Layer (services/)

Owns use-cases and orchestration. Aggregates providers and returns typed application models. Interface-agnostic.

Service File Responsibility
FinanceApplication finance_application.py Core use-cases: accounts, positions, orders, quotes, market overview, watchlist, earnings, news, search, thesis loading
WeeklyReviewOrchestrator weekly_review.py Data gathering, rubric prompt rendering, report persistence
ScoringService scoring.py YAML rubric loading/validation, composite score computation, action threshold mapping
ThesisService thesis.py Thesis YAML loading, condition evaluation against live quotes
EventServiceApplication event_application.py Event source wiring, bus setup, projector/sink registration, runtime launch

Integration Layer (libs/)

Encapsulates external system calls. Each adapter owns its rate limiting, error handling, and response parsing. Returns domain models from models/, never raw API responses.

Adapter Source Data
market_data.py Finnhub Quotes, index/sector performance, earnings, news
snaptrade_sdk.py SnapTrade Brokerage accounts, positions, orders
macro_data.py FRED Fed funds rate, treasury yields, CPI, PCE, unemployment, jobless claims
sec_data.py SEC EDGAR Filing index, filing text, HTML cleaning, section extraction
brave_search.py Brave Search Web search, symbol-focused multi-query search

All adapters degrade gracefully — missing API keys or failed requests produce empty results, not crashes.

Models Layer (models/)

Pydantic models are first-class contracts at every boundary.

Module Key Models
brokerage.py BrokerageAccount, Position, Order, AccountReport, PositionReport, OrderReport
market.py QuoteSnapshot, IndexPerformance, SectorPerformance, EarningsEntry, NewsArticle, SearchResult
macro.py MacroIndicator, MacroSnapshot
sec.py FilingEntry, FilingSection, FilingDocument
report.py WeeklyReport, Decision, EvidenceRecord, RubricScore, SymbolScorecard, RiskSnapshot, ActionItem
events.py Event, CronSchedule, WorkflowRequest
application.py OperationResult, MarketOverview, EventServiceConfig, report wrappers

Persistence Layer (db/)

SQLite with WAL mode for concurrent reads. Managed by SQLAlchemy ORM + Alembic migrations.

erDiagram RubricScoreRow { int id PK string run_id date run_date string symbol string metric float score string rationale float composite_score float composite_confidence string action datetime created_at }

Repository interface (repository.py):

  • save_scorecard(session, run_id, run_date, scorecard) — persists all scores from a scoring session
  • get_score_history(session, symbol, metric, since, limit) — trend queries, newest first
  • get_latest_composite(session, symbol) — most recent composite for a symbol

File-based persistence:

  • investments/reports/weekly/{date}.json + .md — weekly review artifacts
  • investments/theses/{SYMBOL}.yaml — investment thesis definitions
  • investments/scoring/*.yaml — rubric definitions
  • .scoring-{run_id}.json — temporary session state during interactive scoring

Data Flow

Weekly Review Pipeline

The weekly review is the primary agentic workflow. The CLI gathers data deterministically; the agent applies judgment at defined handoff points.

sequenceDiagram participant Agent participant CLI as bof CLI participant Services participant Providers as External APIs participant DB as SQLite participant Files as File System Agent->>CLI: bof review weekly --dry-run CLI->>Services: WeeklyReviewOrchestrator.run() Services->>Providers: Fetch positions, market, macro, news, search Providers-->>Services: Typed domain models Services->>Services: Load theses + rubrics, render prompts Services-->>CLI: WeeklyReport (JSON + MD) CLI-->>Agent: Rubric prompts + context Agent->>Agent: Reason over each symbol Agent->>CLI: bof review score SYMBOL --metric ... --score ... CLI->>Services: ScoringService.compute_composite() Services-->>CLI: Composite score + action Agent->>CLI: bof review evidence SYMBOL --claim ... Agent->>CLI: bof review annotate --run-id ID ... Agent->>CLI: bof review finalize --run-id ID CLI->>DB: save_scorecard() CLI->>Files: Persist final report

Scoring System

YAML rubrics define weighted evaluation dimensions. Three rubrics ship by default:

Rubric Weight Evaluates
valuation_signal 0.30 P/E, 52w range, fundamental alignment
news_sentiment 0.35 Headline sentiment, narrative shifts
thesis_alignment 0.35 Qualitative/quantitative thesis confirmation

Composite scoring: 1. Each score is normalized to [0, 1] per rubric's defined scale 2. Weighted average across submitted rubrics 3. Confidence = sum(submitted weights) / sum(all weights)

Action thresholds:

Composite Action
>= 0.70 BUY_MORE
>= 0.40 HOLD
>= 0.20 TRIM
< 0.20 EXIT

Thesis System

Per-symbol YAML files define investment theses with machine-evaluable conditions:

symbol: NVDA
status: active          # active | closed | paused
conviction: full        # full | half | starter
thesis: "AI compute leader..."
conditions:
  add:
    pe_ratio: { operator: "<", value: 40 }
  trim:
    pe_ratio: { operator: ">", value: 80 }
targets:
  price_target: 200
  stop_loss: 100

bof thesis check SYMBOL evaluates conditions against live quote data and reports pass/fail per condition.

Event Architecture

The event subsystem is asyncio-based and loosely coupled. It enables reactive workflows triggered by external signals.

graph LR subgraph Sources Cron[CronEventSource<br/>interval tickers] TG[TelegramPollingSource<br/>bot messages] end subgraph Bus["AsyncEventBus"] Q[In-memory queue<br/>wildcard topic matching] end subgraph Processing WP[WorkflowProjector<br/>normalize to workflow.request] end subgraph Sinks Console[ConsoleEventSink<br/>stdout JSON] JSONL[JsonlEventSink<br/>audit log] end Sources --> Q Q --> WP Q --> Sinks WP --> Q

Event envelope: event_type, source, payload, metadata, event_id (UUID), created_at (UTC).

Projector: WorkflowProjector translates source-specific events into normalized workflow.request events with trigger context (telegram message text, cron schedule name, etc.).

Extensibility: implement EventSource.run(), emit Event envelopes, register in EventServiceApplication. No CLI changes needed unless exposing new config flags.

Inputs and Outputs

External Inputs

Source Env Var Data Provided
Finnhub FINNHUB_API_KEY Real-time quotes, index/sector performance, earnings calendar, news headlines
SnapTrade SNAPTRADE_* (4 vars) Brokerage accounts, positions, order history
FRED FRED_API_KEY Fed funds rate, treasury yields (2Y/10Y), CPI, PCE, unemployment, jobless claims
SEC EDGAR SEC_USER_AGENT Filing index, 10-K/10-Q full text with section extraction
Brave Search BRAVE_API_KEY Web search results, symbol-focused research queries
Telegram TELEGRAM_BOT_TOKEN Bot messages as event triggers

Outputs

Output Format Location
CLI display Markdown tables, structured text stdout
Weekly report JSON + Markdown investments/reports/weekly/{date}.*
Score history SQLite rows artifacts/bof.db
Event audit log JSONL configurable path via --event-log
Rubric prompts Structured text with context stdout (for agent consumption)

Traceability and Security

  • Redaction: event serialization strips API keys, tokens, credentials, chat IDs, and user IDs
  • Audit log: optional JSONL sink with append-only event stream
  • Event lineage: source payloads and projections include parent event IDs
  • Run IDs: every review session gets a UUID for end-to-end tracing
  • Read-only DB queries: the bof db query command validates SQL is SELECT-only
  • Graceful degradation: missing credentials produce empty results, never leak errors or secrets

Known Constraints

  • Event bus is in-memory (single-process, not distributed)
  • SnapTrade SDK requires 4 env vars for brokerage access
  • Finnhub free tier lacks candle/historical data — period performance uses day change only
  • Brave Search free tier has 2,000 queries/month limit
  • SEC EDGAR rate limit is 10 requests/second per their fair access policy