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.
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.
Repository interface (repository.py):
save_scorecard(session, run_id, run_date, scorecard)— persists all scores from a scoring sessionget_score_history(session, symbol, metric, since, limit)— trend queries, newest firstget_latest_composite(session, symbol)— most recent composite for a symbol
File-based persistence:
investments/reports/weekly/{date}.json+.md— weekly review artifactsinvestments/theses/{SYMBOL}.yaml— investment thesis definitionsinvestments/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.
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.
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 querycommand 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