File size: 37,536 Bytes
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9fe4fad
 
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782ac40
23c102d
6e13ca4
 
36dada9
 
6e13ca4
 
 
 
 
 
 
 
 
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782ac40
36dada9
 
782ac40
 
 
36dada9
 
782ac40
36dada9
 
 
 
782ac40
36dada9
782ac40
 
36dada9
 
 
 
 
 
 
 
782ac40
 
 
36dada9
 
 
 
782ac40
 
 
36dada9
 
 
 
 
 
 
 
 
 
 
 
23c102d
 
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
085d910
 
36dada9
 
23c102d
 
 
 
 
 
 
 
 
085d910
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b8e8bb
 
 
 
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23c102d
a746412
 
 
23c102d
7b8e8bb
 
23c102d
36dada9
a746412
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b8e8bb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7ac9e43
 
 
 
 
 
 
 
7b8e8bb
 
 
 
 
 
 
 
 
 
 
4819a70
 
7b8e8bb
4819a70
 
 
 
7b8e8bb
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23c102d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0225cde
 
9fe4fad
 
 
 
23c102d
9fe4fad
 
 
 
 
 
36dada9
 
 
 
 
 
 
 
 
 
 
9fe4fad
 
 
 
 
 
 
 
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcf5c4c
085d910
dcf5c4c
 
 
 
 
 
 
 
 
085d910
 
 
 
 
 
 
dcf5c4c
085d910
 
 
 
 
 
 
 
 
dcf5c4c
 
085d910
 
 
 
 
dcf5c4c
085d910
 
23c102d
 
 
085d910
 
 
23c102d
085d910
23c102d
 
6e13ca4
 
dcf5c4c
085d910
 
 
 
 
6e13ca4
 
 
 
085d910
 
 
 
 
 
 
dcf5c4c
085d910
 
 
 
dcf5c4c
085d910
 
 
 
 
dcf5c4c
085d910
 
 
 
6e13ca4
 
 
085d910
 
dcf5c4c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e13ca4
dcf5c4c
 
 
 
 
 
 
 
6e13ca4
dcf5c4c
 
 
085d910
 
 
 
 
 
 
 
36dada9
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
---
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