TerraFin / docs /interface.md
sk851's picture
docs(sec_edgar): document 8-K parsing + EX-99.x exhibit append
7ac9e43
---
title: Interface Layer
summary: How TerraFin's FastAPI app is started, how routes are organized, and how session-scoped state works.
read_when:
- Adding or modifying API endpoints
- Integrating with the chart or dashboard from Python
- Debugging session isolation or caching
- Building or serving the frontend
---
# Interface Layer
The interface layer exposes TerraFin through one FastAPI application, six page
routes, and several API families. It lives under `src/TerraFin/interface/`.
The key design choice is that interactive state is session-scoped. Unless a
request says otherwise, TerraFin uses the `default` session.
For deployment and upstream data-usage responsibilities, see
[License & Data Rights](./legal.md).
## Server
Source: `src/TerraFin/interface/server.py`
The server owns:
- application startup and shutdown
- router registration
- cache manager lifecycle
- readiness and health endpoints
- static frontend serving
### App factory
```python
create_app(
initial_data: TimeSeriesDataFrame | None = None,
base_path: str = "",
) -> FastAPI
```
`create_app(...)` resets session state, registers routers, installs exception
handlers, wires the private-data cache callbacks, and mounts the frontend static
assets.
### CLI
```bash
python server.py [run|start|stop|status|restart]
```
Run these commands from `src/TerraFin/interface/`.
| Command | Behavior |
|---------|----------|
| `run` | Start in the foreground |
| `start` | Start in the background and write a PID file |
| `stop` | Stop the background process if it exists |
| `status` | Show whether the background process is running |
| `restart` | Stop and start again |
Runtime config comes from `src/TerraFin/interface/config.py`.
| Field | Default | Env var | Notes |
|-------|---------|---------|-------|
| `host` | `127.0.0.1` | `TERRAFIN_HOST` | Empty values fall back to the default |
| `port` | `8001` | `TERRAFIN_PORT` | Must be an integer in `1..65535` |
| `base_path` | `""` | `TERRAFIN_BASE_PATH` | Normalized to leading slash, no trailing slash |
| `cache_timezone` | `"UTC"` | `TERRAFIN_CACHE_TIMEZONE` | Must be a valid IANA timezone; used for cache/date-bound scheduling |
### Root routes
| Method | Path | Behaviour |
|--------|------|-----------|
| `GET` | `/` | Redirect to the dashboard page, respecting `base_path` |
| `GET` | `/resolve-ticker?q=...` | Resolve a query string to a ticker symbol and company name |
| `GET` | `/health` | Multi-component status page (HTML) |
| `GET` | `/health.json` | Same data as JSON for scripting |
| `GET` | `/ready` | Readiness endpoint with cache-manager and private-data checks |
`/health`, `/health.json`, and `/ready` stay at the root even when
`TERRAFIN_BASE_PATH` is set. Feature routes are prefixed by the base path.
`/health` runs active probes on each request (no background polling) for
three components — **Agent** (provider auth env vars set), **Telegram**
(`getMe` against the configured bot token), and **Signals Provider**
(proxies the upstream monitor's `/health`, surfacing per-broker WS state
and last-tick age). Each probe has a 2 s timeout; results are cached
in-process for 30 s. Append `?refresh=1` to force a fresh probe.
### Error handling
Errors use a uniform JSON envelope:
```json
{"error": {"code": "...", "message": "...", "request_id": "..."}}
```
`details` is included when the handler has structured extra context to return.
### Session isolation
Stateful APIs read `X-Session-ID` and default to `"default"`. Chart payloads,
chart selections, and calendar selections are all stored per session.
In browser flows, TerraFin usually generates a per-tab session id and sends it
on every chart request. Notebook and direct Python helpers intentionally use the
`default` chart session unless an explicit session id is provided.
Use the accessors in `chart/state.py` and `calendar/state.py` instead of
touching their internal storage directly.
## Page routes
| Route | Purpose |
|-------|---------|
| `/chart` | Interactive chart page |
| `/dashboard` | Watchlist, breadth, valuation, and cache status |
| `/market-insights` | Regime summary, guru portfolios, top companies |
| `/calendar` | Earnings and macro event calendar |
| `/stock` and `/stock/{ticker}` | Stock Analysis page with chart-first loading |
| `/watchlist` | Personal watchlist management page |
Each page route respects `TERRAFIN_BASE_PATH`.
---
## Chart
Source: `src/TerraFin/interface/chart/`
The chart is TerraFin's main visualization surface. It stores a session-scoped
source payload, display payload, named series, pin state, and per-series
history metadata. Stock Analysis, Market Insights, the chart page, and notebook
helpers all use this same backend chart session model.
For the full processing and management flow, see
[chart-architecture.md](./chart-architecture.md).
### API endpoints
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/chart/api/chart-data` | Get the current display payload plus entries and `historyBySeries` |
| `POST` | `/chart/api/chart-data` | Set the current session from raw payload data and mark those series complete |
| `POST` | `/chart/api/chart-view` | Rebuild the display payload for a new view such as daily or monthly |
| `GET` | `/chart/api/chart-selection` | Read the current chart selection |
| `POST` | `/chart/api/chart-selection` | Save the current chart selection |
| `POST` | `/chart/api/chart-series/add` | Add a named series by TerraFin lookup name, seeded with recent history |
| `POST` | `/chart/api/chart-series/set` | Reset the session to one named series, seeded with recent history |
| `POST` | `/chart/api/chart-series/progressive/set` | Explicit progressive seed route for one named series |
| `POST` | `/chart/api/chart-series/progressive/backfill` | Backfill older history for a seeded series |
| `POST` | `/chart/api/chart-series/remove` | Remove a named series |
| `GET` | `/chart/api/chart-series/names` | List currently loaded named series |
| `GET` | `/chart/api/chart-series/search` | Search available indicator, index, and economic names |
| `GET` | `/chart` | Serve the chart page |
### Auto-computed indicators
When the payload contains exactly one candlestick series, TerraFin appends:
| Indicator | Default params | Indicator group |
|-----------|---------------|-----------------|
| Moving Averages | SMA 20, 60, 120, 200 | `ma-20`, `ma-60`, `ma-120`, `ma-200` |
| Bollinger Bands | window 20, ±2σ | `bb` |
| RSI | window 14, levels at 70/30 | `rsi` |
| MACD | fast 12, slow 26, signal 9 | `macd` |
| Realized Volatility | window 21 | `realized-vol` |
| Range Volatility | window 20 (Parkinson) | `range-vol` |
| Mandelbrot Fractal Dimension | windows 65, 130, 260 | `mfd` |
Indicator adapter source: `src/TerraFin/interface/chart/indicators/adapter.py`.
### Chart client (Python)
Source: `src/TerraFin/interface/chart/client.py`
| Function | Description |
|----------|-------------|
| `display_chart(df)` | Open chart in browser. Starts server if needed. Blocks. |
| `display_chart_notebook(data)` | Display in Jupyter notebook. Waits for readiness, seeds the default chart session, and returns an IFrame bound to that same session. |
| `update_chart(data, pinned=False, session_id=None)` | POST data to a running server. Returns `True` on success. |
| `get_chart_selection()` | GET the current selection from the server. |
These helpers accept `TimeSeriesDataFrame` or `list[TimeSeriesDataFrame]`.
Single OHLC series render as candlesticks; multi-series payloads render as
comparison lines.
Notebook and embedded use:
- `import TerraFin` does not auto-load `.env`
- env-backed features lazy-load `.env` on first use unless
`TERRAFIN_DISABLE_DOTENV=1`
- for deterministic notebook or script setup, call:
```python
from TerraFin import configure
configure()
```
- if the kernel runs outside the repo root, use
`configure(dotenv_path="/absolute/path/to/.env")`
- if you need the resolved typed settings, inspect
`load_terrafin_config()` instead of reading env vars directly
---
## Dashboard
Source: `src/TerraFin/interface/dashboard/`
The dashboard is the main consumer of `PrivateDataService`. It mixes private
source data, cache status, and a few valuation-style summary endpoints. If the
private source is unavailable, TerraFin falls back to bundled public-safe
fixtures or empty defaults for those widgets.
Important boundary:
- dashboard/widget payloads are not automatically chart-series contracts
- if a private-source feature needs optimized chart serving, promote it into
the data layer as a `TimeSeriesDataFrame` series first
- otherwise keep it as a widget payload in `PrivateDataService`
### API endpoints
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/dashboard/api/watchlist` | Watchlist snapshot (symbols, names, moves) |
| `GET` | `/dashboard/api/market-breadth` | Market breadth metrics (label, value, tone) |
| `GET` | `/dashboard/api/trailing-forward-pe-spread` | Trailing minus forward P/E spread summary and history |
| `GET` | `/dashboard/api/cape` | Current CAPE snapshot |
| `GET` | `/dashboard/api/fear-greed` | Fear and Greed summary if available |
| `GET` | `/dashboard/api/cache-status` | Status of all registered cache sources |
| `POST` | `/dashboard/api/cache-refresh` | Refresh cache sources (`?force=bool`) |
| `GET` | `/dashboard/api/gex/spx` | SPX GEX live snapshot from CBOE options (regime, spot, zero-gamma, call/put walls, per-strike and per-expiration buckets) |
| `GET` | `/dashboard/api/gex/spx/history` | SPX GEX daily history from SqueezeMetrics (2011–present) |
The practical rule for future private-source additions is:
- chart/search/progressive use case -> build a private series contract first
Examples: `Fear & Greed`, `Net Breadth`
- dashboard-only use case -> keep a private widget payload
---
## Calendar
Source: `src/TerraFin/interface/calendar/`
The calendar merges private calendar data and TerraFin-fetched macro events into
one session-aware view. Events are categorized as `earning`, `macro`, or
`event`. This page remains usable in public/demo mode because earnings and
macro events are still fetched through TerraFin's local provider paths, while
private calendar events use the same fallback chain as the dashboard. As with
the dashboard, a warmed private-source cache is not a substitute public data
source.
### API endpoints
| Method | Path | Query params | Description |
|--------|------|-------------|-------------|
| `GET` | `/calendar/api/events` | `month`, `year`, `categories`, `limit` | Filtered events |
| `POST` | `/calendar/api/events` | — | Upsert events |
| `GET` | `/calendar/api/selection` | — | Get selection state |
| `POST` | `/calendar/api/selection` | — | Set selection state |
---
## Market Insights
Source: `src/TerraFin/interface/market_insights/`
Market insights provides higher-level market context and institutional
positioning. The regime endpoint is currently a static placeholder response;
guru portfolio data is fully backed by the SEC EDGAR provider.
The top-companies widget also degrades cleanly when the private source is not
configured.
### API endpoints
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/market-insights/api/regime` | Market regime (placeholder) |
| `GET` | `/market-insights/api/macro-info` | Macro instrument summary (`?name=`) |
| `GET` | `/market-insights/api/investor-positioning/gurus` | List available guru names |
| `GET` | `/market-insights/api/investor-positioning/holdings` | Guru portfolio (`?guru=`, optional `?filing_date=`) |
| `GET` | `/market-insights/api/investor-positioning/history` | Filing index for period dropdown (`?guru=`) |
| `GET` | `/market-insights/api/top-companies` | Private-source top-companies snapshot |
### SPX Gamma Exposure
The Market Insights page includes an SPX GEX accordion panel. GEX data is
fetched eagerly at page mount (not on accordion open) via
`GET /dashboard/api/gex/spx`, so the panel renders immediately when the user
expands it. Historical data comes from `GET /dashboard/api/gex/spx/history`.
The snapshot card hides while loading or when CBOE data is unavailable
(`available: false`).
### Investor positioning loading strategy
Three-tier loading minimises time-to-first-render:
1. **Fast path** (`/holdings` without `filing_date`) — fetches exactly 2 XMLs (latest + previous quarter) so colour coding works immediately.
2. **Index only** (`/history`) — returns the SEC submissions index with no XML fetch; used to populate the period dropdown.
3. **Background prefetch** — triggered by `/history`; caches remaining quarters via `asyncio.Task` so historical periods load instantly when selected.
**Cancellation pattern** (`data_routes.py: _submit_prefetch`):
```python
_prefetch_tasks: dict[str, asyncio.Task] = {}
async def _submit_prefetch(guru, filings):
for task in _prefetch_tasks.values():
if not task.done():
task.cancel() # cancel ALL existing prefetch tasks
_prefetch_tasks.clear()
_prefetch_tasks[f"prefetch:{guru}"] = asyncio.create_task(
_prefetch_holdings_async(guru, filings)
)
```
When the user switches gurus, every in-flight prefetch is cancelled immediately. `run_in_executor` runs the blocking SEC download in a thread; `CancelledError` fires between iterations (not mid-download). To add a new cancellable background job type, follow the same pattern: key the task dict by a domain-specific string, clear all on new submission if mutual exclusion is desired, or key per-domain for independent queues.
Market Insights now uses the shared TerraFin chart routes directly:
- `POST /chart/api/chart-series/progressive/set` for initial seed
- `POST /chart/api/chart-series/add` for warm add
- `POST /chart/api/chart-series/remove` for warm remove
`macro-info` is the page-specific helper for the focused header block. The
chart session itself is no longer managed through separate `macro-focus`
routes.
---
## Stock Analysis
Source: `src/TerraFin/interface/stock/`
Stock Analysis combines a chart-first page route with a small API family for
company profile, earnings history, financials, SEC filings, and search routing.
The page itself uses the shared TerraFin chart session and progressive
`3Y -> full` history loading described in
[chart-architecture.md](./chart-architecture.md).
### Page routes
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/stock` and `/stock/` | Stock landing page |
| `GET` | `/stock/{ticker}` | Stock Analysis page for one ticker |
### API endpoints
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/stock/api/company-info` | Company profile and price summary (`?ticker=`) |
| `GET` | `/stock/api/earnings` | Earnings history (`?ticker=`) |
| `GET` | `/stock/api/financials` | Financial statements (`?ticker=`, `statement=`, `period=`) |
| `GET` | `/stock/api/fcf-history` | Annual FCF/share history + 3yr-avg/latest-annual/TTM candidates and the source the `auto` cascade would pick (`?ticker=`, `years=10`). Also returns `ttmFcfPerShare` + `ttmSource` (how the TTM value was computed: `quarterly_ttm` or `annual`). See [api-reference.md](./api-reference.md#stock-analysis). |
| `GET` / `POST` | `/stock/api/dcf` | Forward DCF. POST body accepts `projectionYears` (5/10/15), `fcfBaseSource` (`auto`/`3yr_avg`/`ttm`/`latest_annual`), and turnaround inputs (`breakevenYear`, `breakevenCashFlowPerShare`, `postBreakevenGrowthPct`) on top of the base overrides. |
| `GET` / `POST` | `/stock/api/reverse-dcf` | Reverse DCF (market-implied growth). POST accepts `projectionYears`, `growthProfile`, base overrides. |
| `GET` | `/stock/api/beta-estimate` | TerraFin's `beta_5y_monthly` estimate against the mapped benchmark. |
| `GET` | `/stock/api/gex` | GEX snapshot for a ticker (`?ticker=`). Returns regime, spot, zero-gamma strike, call/put walls, by-strike and by-expiration GEX buckets. |
| `GET` | `/stock/api/filings` | Recent 10-K / 10-Q / 8-K list with EDGAR URLs (`?ticker=`, `limit=`) |
| `GET` | `/stock/api/filing-document` | Parsed markdown + TOC for one filing (`?ticker=`, `accession=`, `primaryDocument=`, `form=`, `includeImages=`) |
| `GET` | `/resolve-ticker?q=...` | Resolve a query string to a ticker symbol and company name (root route — no `/stock/api/` prefix) |
### Page layout (`/stock/{ticker}`)
The stock-detail page is a vertical stack of sections. Row 2 (Earnings + FCF
history) is height-capped on desktop so the page stays scannable; longer
content scrolls inside the cards rather than expanding them.
| Row | Left card | Right card |
|---|---|---|
| 1 | **Market Chart** (price history + indicators) | **Overview & Valuation** (company profile, price context, key metrics) |
| 2 *(capped at 280px desktop)* | **Earnings History** (EPS estimate / reported / surprise table, vertical scroll) | **FCF / Share History** (annual FCF/share bars, latest-TTM right-gutter callout, 3yr-Avg dashed reference line) |
| 3 | **DCF Valuation** (input form + Projected FCF chart at the bottom) | **DCF Valuation Result** (intrinsic value tiles, sensitivity heatmap, projection table) |
| 4 | **Reverse DCF** *(toggled, collapsed by default)* — when expanded, shows input + result side-by-side mirroring Row 3. The Reverse DCF Result card carries its own Projected FCF chart at the bottom. |
| 5 | **SEC Filings** (US-listed issuers only; auto-hidden otherwise). |
### DCF Valuation card
The forward-DCF input card hosts:
- **Forecast Horizon** — segmented control (`5` / `10` / `15` years) and a
**Turnaround Mode** checkbox. Turnaround mode swaps `Base Growth %` for
`Breakeven Year` / `Breakeven FCF / Share` / `Post-Breakeven Growth %`
inputs.
- **FCF Base Source** — segmented control (`Auto` / `3yr Avg` / `TTM` /
`Latest Annual`). Selecting a source auto-fills the *Base FCF / Share*
field with the corresponding candidate value (read from
`/stock/api/fcf-history`'s `candidates`). If the user types over the
auto-filled value, a `↺ Revert to {source} ($X)` chip surfaces under the
field; clicking it restores the source's value.
- **Model Inputs grid** — Base FCF / Share, Base Growth %, Terminal Growth %,
Beta (with a `Compute Beta` button that runs `beta_5y_monthly`), Equity
Risk Premium %.
- **Explain inputs** toggle (top-right of the card header). OFF by default;
hides every "i" icon for clean entry by power users. ON reveals all input
hints. State is persisted in `localStorage` (`terrafin.dcf.explainInputs`)
via the `useExplainInputs` hook. Implemented through an
`InfoHintVisibilityContext` provider — the `InfoHint` component reads the
context and returns `null` when hidden.
- **Projected FCF / Share chart** — appears at the bottom after running DCF.
Bars for ≤15-year horizons; line + shaded band (bear/bull envelope, base
line) for longer horizons. In bar mode with multi-scenario data, each base
bar carries a vertical whisker from bear to bull with colored end-caps so
the scenario spread is visible. The Reverse DCF Result card uses the same
component (single-scenario, implied-schedule label).
### FCF / Share History chart
`FcfHistoryChart` renders historical annual FCF/share as filled bars
(green/red), the latest TTM as a small blue pill in the right gutter
connected by a dashed leader line to the last annual bar's top, and the 3yr
Avg as a teal dashed horizontal line with a halo'd inline label at the
left-inside of the plot. Y-axis uses nice-number ticks tightly clipped to the
data range (no forced 0 inclusion when all values share a sign). Hover on any
bar / TTM marker / 3yr Avg line shows a small white tooltip with the value.
### SEC Filings panel
The `/stock/{ticker}` page includes a **SEC Filings** card for every US-listed
issuer. The card is hidden automatically for tickers without an SEC CIK (e.g.
KOSPI / TSE / HKEX issuers) so non-US pages stay uncluttered.
For supported tickers the card surfaces:
- a form dropdown derived from `df.form.unique()` (covers 10-K, 10-Q,
amendments, 8-K, 20-F, 40-F, etc.);
- a chronological filing list with a **View on EDGAR** link per row pointing
at the SEC inline-XBRL viewer (`/ix?doc=/Archives/...`);
- a reader that opens inline below the list, with:
- a two-level accordion preserving Part I / Part II as outer collapsibles
and Items (Item 1, Item 2 MD&A, …) as nested inner collapsibles;
- a compact custom markdown renderer that handles our `parse_sec_filing`
output (`##`/`###` headings, paragraphs, GFM pipe tables, blockquote
fallbacks, inline-image placeholders) without pulling in a general
markdown dep;
- a "View source on EDGAR" pill in the reader header.
The parsed markdown is cached for 30 days via the shared `sec_filings`
CacheManager namespace (see [caching.md](./caching.md)), so reopening a filing
is free across sessions. See [data-layer.md](./data-layer.md) for the
underlying `parse_sec_filing` / `build_toc` / `fetch_and_parse_filing` helpers.
For 8-K (and 8-K/A) filings, the route returns the parsed cover doc plus any
EX-99.x exhibits (earnings press release as `## Exhibit 99.1 — Press Release`,
CFO commentary as `## Exhibit 99.2 — ...`, etc.) so the substantive content is
reachable from the sidebar TOC. The sidebar bumps to `max_level=3` for 8-Ks so
exhibit-body subheadings (e.g. `### Q1 FY27 Summary`) surface as navigable
entries.
### Agent integration
When the user opens a filing, the panel publishes the currently-focused
section to the agent side-panel via `publishAgentViewContext`. The `selection`
carries `ticker`, `form`, `accession`, `primaryDocument`, `sectionSlug`,
`sectionTitle`, a bounded `sectionExcerpt` (≤ 4 KB), and EDGAR URLs. The
hosted agent's `current_view_context` tool reads this payload, and the agent
can call `sec_filings`, `sec_filing_document`, or `sec_filing_section` to
fetch the full body when the excerpt is not enough (e.g. "summarize their
business" on a 10-Q will trigger a cross-filing pivot to the most recent
10-K's Item 1. Business). See the `sec_filings` row in the common-tasks
table at [agent/usage.md](./agent/usage.md#common-tasks).
For the view-context pipeline (how `publishAgentViewContext` reaches the
agent, session/context identity, and how `current_view_context()` reads
the current panel), see [agent/architecture.md](./agent/architecture.md)
and [agent/hosted-runtime.md](./agent/hosted-runtime.md).
---
## Watchlist
Source: `src/TerraFin/interface/watchlist/`
The watchlist page is a dedicated personal-management surface. It reuses the
dashboard watchlist API family rather than exposing a separate `/watchlist/api`
namespace.
### Page routes
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/watchlist` and `/watchlist/` | Personal watchlist page |
### API endpoints
The watchlist page uses the `/dashboard/api/watchlist` API family.
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/dashboard/api/watchlist` | Full watchlist snapshot (items with symbol, name, move %, tags) |
| `POST` | `/dashboard/api/watchlist` | Add a symbol (`body: {symbol, tags?: []}`) |
| `DELETE` | `/dashboard/api/watchlist/{symbol}` | Remove a symbol. Pass `?group=<tag>` to remove only from that group. |
| `PATCH` | `/dashboard/api/watchlist/{symbol}/tags` | Update tags (`body: {tags, mode: "set"|"add"|"remove"}`) |
| `GET` | `/dashboard/api/watchlist/groups` | List groups with item counts |
| `POST` | `/dashboard/api/watchlist/groups` | Create an empty named group (`body: {name}`) |
| `DELETE` | `/dashboard/api/watchlist/groups/{tag}` | Delete a group and remove its tag from all items |
| `POST` | `/dashboard/api/watchlist/groups/rename` | Rename a group (`body: {old, new}`) |
| `PUT` | `/dashboard/api/watchlist/groups/order` | Persist group display order (`body: {groups: [name, ...]}`) |
| `PUT` | `/dashboard/api/watchlist/groups/{group}/item-order` | Persist item order within a group (`body: {symbols: [...]}`) |
| `PUT` | `/dashboard/api/watchlist` | Bulk-update all symbols and tags (`body: {symbols: [{symbol, tags}]}`) |
### Drag reorder
The watchlist frontend uses `@dnd-kit/core` for touch- and pointer-compatible drag reorder. Groups and ticker rows are independently sortable. Order is persisted optimistically: the client applies the new order immediately via `itemOrderOverride` state, then POSTs to the backend. On error the override is cleared and the server order is restored.
---
## Agent API
Source: `src/TerraFin/interface/agent/data_routes.py`
The Agent API exposes TerraFin's optimized processing pipeline for programmatic
consumers. It is backed by the shared service in `src/TerraFin/agent/service.py`
rather than a separate simplified path.
That means:
- market and macro requests use the same progressive-history aware data contract
- view transforms match the chart stack
- indicator computation matches chart indicator math
- every response includes top-level `processing` metadata
For most consumers, prefer the Python client in `src/TerraFin/agent/client.py`
or the `terrafin-agent` CLI over calling raw routes directly.
### API endpoints
| Method | Path | Query Params | Description |
|--------|------|-------------|-------------|
| `GET` | `/agent/api/resolve` | `q` | Resolve a free-form name into TerraFin's stock or macro path |
| `GET` | `/agent/api/market-data` | `ticker`, `depth`, `view` | Market or macro series plus processing metadata |
| `GET` | `/agent/api/indicators` | `ticker`, `indicators`, `depth`, `view` | Raw indicator results computed from the shared processing pipeline |
| `GET` | `/agent/api/market-snapshot` | `ticker`, `depth`, `view` | Price action + indicator summaries + breadth + watchlist |
| `GET` | `/agent/api/company` | `ticker` | Company profile and price summary |
| `GET` | `/agent/api/earnings` | `ticker` | Earnings history |
| `GET` | `/agent/api/financials` | `ticker`, `statement`, `period` | Financial statement table |
| `GET` | `/agent/api/portfolio` | `guru` | Guru portfolio holdings |
| `GET` | `/agent/api/economic` | `indicators` (comma-separated FRED codes) | Economic indicator series |
| `GET` | `/agent/api/macro-focus` | `name`, `depth`, `view` | Macro instrument summary plus series data |
| `GET` | `/agent/api/lppl` | `name`, `depth`, `view` | LPPL bubble-confidence summary from the shared agent/chart processing pipeline |
| `GET` | `/agent/api/calendar` | `year`, `month`, `categories`, `limit` | Calendar events with processing metadata |
| `GET` | `/agent/api/runtime/agents` | - | Hosted runtime agent catalog plus exposed tools and runtime readiness metadata |
| `POST` | `/agent/api/runtime/sessions` | body: `agentName`, optional `sessionId`, `systemPrompt`, `metadata` | Create a hosted runtime conversation session when the selected hosted model is configured |
| `GET` | `/agent/api/runtime/sessions` | - | List hosted sessions from the transcript-derived session index |
| `GET` | `/agent/api/runtime/sessions/{session_id}` | - | Read hosted runtime session state, transcript-derived message history, and tools |
| `DELETE` | `/agent/api/runtime/sessions/{session_id}` | - | Archive a hosted session transcript and remove it from active history |
| `POST` | `/agent/api/runtime/sessions/{session_id}/messages` | body: `content` | Append a user turn and run the hosted model/tool loop |
| `GET` | `/agent/api/runtime/sessions/{session_id}/tasks` | - | List background tasks for a hosted session |
| `GET` | `/agent/api/runtime/sessions/{session_id}/approvals` | - | List approval requests for a hosted session |
| `GET` | `/agent/api/runtime/tasks/{task_id}` | - | Read a hosted background task |
| `POST` | `/agent/api/runtime/tasks/{task_id}/cancel` | - | Cancel a hosted background task |
| `GET` | `/agent/api/runtime/approvals/{approval_id}` | - | Read one approval request |
| `POST` | `/agent/api/runtime/approvals/{approval_id}/approve` | body: optional `note` | Approve a pending request |
| `POST` | `/agent/api/runtime/approvals/{approval_id}/deny` | body: optional `note` | Deny a pending request |
Time-series endpoints use `depth=auto|recent|full`.
- `auto` starts with the optimized recent/progressive path for market and macro series
- `full` forces complete-history loading from the start
LPPL route note:
`/agent/api/lppl` uses TerraFin's calibrated default LPPL scan from the shared
analytics helper. The full article-style 750→50 ladder is kept as a notebook /
research option via `lppl(..., n_windows=None)` and is not exposed over HTTP.
Runtime route note:
`/agent/api/runtime/*` is the stateful hosted-agent family. It uses the same
shared capability kernel as the Python client, CLI, and stateless
`/agent/api/*` routes, but persists conversation history through append-only
local transcripts plus a transcript-derived session index. Tasks, approvals,
audit, and published view context remain in the hosted runtime store. Hosted
model execution is provider-driven rather than OpenAI-only.
Supported `view` values:
- `daily`
- `weekly`
- `monthly`
- `yearly`
Supported indicator names for `/agent/api/indicators`:
- `rsi`
- `macd`
- `bb`
- `sma_N` such as `sma_20`
- `realized_vol`
- `range_vol`
- `mfd`
- `mfd_65`
- `mfd_130`
- `mfd_260`
Unknown indicator names are skipped and returned in the `unknown` field.
For chart overlays, TerraFin shows the medium-horizon `MFD 130` line by
default to preserve readability. The agent/API still exposes the explicit
`mfd_65`, `mfd_130`, and `mfd_260` series plus the aggregate `mfd` response.
### Python client and CLI
Preferred public entrypoints:
- `TerraFin.agent.TerraFinAgentClient`
- `terrafin-agent`
Both wrap the same service layer and normalized response shapes exposed by the
HTTP API.
### OpenAPI
The FastAPI schema is available at `/openapi.json`. The agent routes use
explicit response models so generic agents can inspect the contract without
reverse-engineering route handlers.
---
## Frontend
Source: `src/TerraFin/interface/frontend/`
The frontend is a React SPA. Built assets live in `frontend/build/` and are
served directly by FastAPI, so Node.js is only needed when you are editing the
frontend itself.
### Building from source
```bash
cd src/TerraFin/interface/frontend
npm install
npm run build
```
Do not commit `node_modules/`. The built output is committed so the server can
run in environments without a frontend toolchain.
## Signals
Source: `src/TerraFin/signals/` (umbrella for alerting + reports + channels), `src/TerraFin/interface/signals/` (HTTP routes for inbound webhook).
The `signals/` module groups two related output paths:
- **`signals/alerting/`** — real-time threshold alerts (RSI, breakout, etc.) pushed when an external monitoring service POSTs to TerraFin.
- **`signals/reports/`** — scheduled narrative briefings (currently weekly) generated locally and surfaced on the dashboard.
- **`signals/channels/`** — shared output sinks (Telegram, webhook, stdout) used by both.
### Alerting
TerraFin supports a push-model alert pipeline: an external real-time service monitors tickers and POSTs signals back to TerraFin, which forwards them to Telegram.
### Architecture
```
External alert API ──register tickers──▶ TerraFin (outbound)
External alert API ──POST /signals/api/signal──▶ TerraFin (inbound)
TerraFin ──sendMessage──▶ Telegram Bot API ──▶ User
```
TerraFin never handles real-time tick data. Signal computation stays in the external service.
### Inbound webhook
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/signals/api/signal` | Receive signal from external API → forward to Telegram |
| `POST` | `/alerting/api/signal` | Legacy alias for the above (kept for existing senders) |
Request body (`InboundSignal`):
```json
{
"ticker": "AAPL",
"signal": "20-day MA touch",
"severity": "high",
"signal_id": "uuid-from-sender",
"fired_at": "2026-04-30T09:00:00",
"name": "Apple Inc.",
"snapshot": {"close": 192.5, "rsi": 68.2}
}
```
- `fired_at`: optional ISO 8601 datetime string. Naive datetimes (no timezone offset) are accepted and stored as-is — TerraFin does not normalize to UTC. Omit or pass `null` if unknown.
- `signal_id` is optional but required for deduplication (sender-provided UUID, not TerraFin-generated)
- `name`: optional company/indicator display name; if blank, TerraFin enriches it from the watchlist cache before forwarding
- `snapshot`: optional open key/value map of detector context at fire time (e.g. OHLCV fields, indicator values). The Telegram formatter reads `close` to derive price direction (▲ if `close` > previous close, ▼ if lower, — if absent or equal). All other keys are informational and forwarded as-is. `snapshot: null` or omitting the field produces — for direction.
- `X-Signature` header: HMAC-SHA256 of request body, keyed with `TERRAFIN_SIGNALS_WEBHOOK_SECRET`
- If `TERRAFIN_SIGNALS_WEBHOOK_SECRET` is unset, the endpoint returns `503` and refuses all signals — the secret is required, not optional
- Per-IP rate limit: 60 requests / 60 seconds; excess returns `429`
### Env vars
| Variable | Purpose | Required |
|----------|---------|----------|
| `TERRAFIN_SIGNALS_PROVIDER_URL` | External alert API base URL | To enable outbound registration |
| `TERRAFIN_SIGNALS_PROVIDER_KEY` | Bearer token for external API | If API requires auth |
| `TERRAFIN_SIGNALS_WEBHOOK_SECRET` | HMAC secret for inbound verification | Required (endpoint returns 503 if unset) |
| `TERRAFIN_SIGNALS_CHANNEL` | `telegram` to forward via Telegram Bot | To enable Telegram |
### Telegram setup
1. Create a bot via [@BotFather](https://t.me/BotFather): `/newbot` → copy token
2. Save token:
```bash
terrafin-signals telegram setup 123456789:AAHfiq...
```
3. Pair — run the command, then DM your bot on Telegram:
```bash
terrafin-signals telegram pair
```
Chat ID is captured automatically and saved to `~/.terrafin/telegram.json`.
4. Test:
```bash
terrafin-signals telegram test
```
5. Add to `.env`:
```bash
TERRAFIN_SIGNALS_CHANNEL=telegram
TERRAFIN_SIGNALS_PROVIDER_URL=https://your-alert-api.com
TERRAFIN_SIGNALS_WEBHOOK_SECRET=your-hmac-secret
```
### Reports
Source: `src/TerraFin/signals/reports/`
The dashboard auto-generates a weekly markdown report every Friday at 16:30 ET (`TERRAFIN_CACHE_TIMEZONE`). Reports persist under `~/.terrafin/reports/weekly/<as_of>.{md,json}` indefinitely — disk cost is negligible and trend stacking is the value.
#### Pipeline
1. Universe: `watchlist_service.get_watchlist_snapshot()`. Falls back to Magnificent 7 (AAPL/MSFT/NVDA/GOOGL/AMZN/META/TSLA) when the watchlist is empty / MongoDB unavailable. M7 reports are explicitly labeled "Sample" in the title and footer CTA.
2. Per-ticker: yfinance close + volume → 5-trading-day WoW; intra-week ≥4% moves; volume ratio vs 20-day avg; ticker-relevant Google News headlines (date-ranged via `after:`/`before:`); upcoming earnings (yfinance).
3. Action wording driven by `(anomaly_flag, has_headline, vol_ratio)` decision tree.
4. Optional **agent enrichment** (when TerraFin agent runtime is configured): index context paragraph (vs ^GSPC/^SOX/^RUT), SEC 8-K drill on unattributed events, macro calendar context. Each section independently optional; failures don't block the deterministic core.
#### Dashboard surface
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/dashboard/api/reports/weekly` | List most recent reports (max 12) |
| `GET` | `/dashboard/api/reports/weekly/{as_of}` | Fetch markdown for a specific report |
| `POST` | `/dashboard/api/reports/weekly/run` | Manually trigger generation (CLI/cron) |
The dashboard top bar shows a 🔔 bell that opens a panel rendering report markdown. A red dot on the bell indicates an unread report (compared against `localStorage["tf-weekly-report-seen"]`).
#### CLI
```bash
# Generate the current week's report (writes to disk + sends to channel if TERRAFIN_SIGNALS_CHANNEL set)
terrafin-signals weekly
# Backtest: anchor to a historical date — useful for verifying pipeline determinism
terrafin-signals weekly --as-of 2026-03-13 --out /tmp/backtest.md
```
#### Telegram delivery
When `TERRAFIN_SIGNALS_CHANNEL=telegram`, the Friday scheduler also pushes the report. Markdown is converted to Telegram-flavored HTML (`<b>`, `<i>`, bullets, code) and chunked under the 4096-char per-message limit.
### Outbound alert provider
Server startup registers current watchlist with the external API and starts a 60-second heartbeat to re-register on provider restart.
### Extending
To swap the external API provider, implement `AlertProvider` from `data/contracts/alert_provider.py` and replace `HttpAlertProvider` in `server.py`. The inbound webhook and Telegram forwarding are provider-agnostic.
---
## See also
- [data-layer.md](./data-layer.md) for the provider and output model underneath these APIs
- [chart-architecture.md](./chart-architecture.md) for chart sessions, mutations, progressive history, and notebook flow
- [feature-integration.md](./feature-integration.md) for the cross-layer checklist when exposing a new feature through UI or APIs
- [analytics.md](./analytics.md) for the indicator functions used by chart and agent routes
- [caching.md](./caching.md) for cache-manager behavior exposed through the dashboard