alphai-tui 0.34.0

Terminal stock dashboard: live quotes and charts plus AI-scored financial news and SEC Form 4 insider activity from AlphAI
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
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
# alphai-tui

[![CI](https://github.com/makeev/alphai-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/makeev/alphai-tui/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/alphai-tui.svg)](https://crates.io/crates/alphai-tui)
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/makeev/alphai-tui/blob/main/LICENSE)

An open-source, Bloomberg-style stock dashboard for the terminal that also
answers why the price moved. Live quotes and candlestick charts on one side;
on the other, for the same ticker, AI-scored news with a full analysis of
each story, the SEC Form 4 insider filings and a structured read of the last
earnings report. A watchlist calendar brings upcoming company reports and
US macro releases into the same workspace. One Rust binary built on
[ratatui](https://ratatui.rs), no browser tab, and no account needed for the
prices. Coming from tickrs or ticker? See [how it compares](#how-it-compares).

![alphai-tui demo: the split dashboard with the quote rail, the news view with the chart over the list and the full AI analysis card beside it, the story read in place under the chart, the market-wide scope, a year of SEC Form 4 insider filings, the earnings read, the summary grid and the candlestick chart with moving averages, volume and RSI](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/demo.gif)

```sh
brew install makeev/tap/alphai-tui   # also cargo, apt, AUR, x-cmd, or a prebuilt binary
alphai-tui NVDA AVGO AAPL MSFT META TSLA AMZN GOOGL BTC-USD
```

Quotes and charts run keyless on Yahoo, or on Finnhub, Alpaca or Tiingo
with their own keys. The News, Insider, Earnings and Calendar views run on a free
[AlphAI](https://alphai.io?utm_source=alphai-tui&utm_medium=referral) key
that you paste once in the settings screen. The first run walks you through
both, and after that a bare `alphai-tui` reopens your watchlist.

## The screens

Nine views, one keystroke apart (`1` to `9`, or Tab). Some follow the
selected ticker; Summary, Portfolio and Calendar cover several names.
One line under the tabs carries the selected ticker's price into all of them.
The screenshots use the `dracula` preset.

### The quote rail, in every view

![alphai-tui quote rail: symbol and price, the day's change, what the holding in that ticker has made, the session badge with a countdown to the opening bell, the day range with the price marked in it, a sparkline and the rest of the watchlist](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/rail.png)

Price, the change on the day, the extended-hours print measured against the
regular close, what you are up or down on the ticker if you hold any of it,
what the US market is doing right now (pre, live, post or closed) and how
long until the next bell, whether the feed is delayed, where the price sits
between the day's low and high, and, where the source reports them and the
terminal is wide enough, the year's range and the day's volume. The rest of
the watchlist follows as percentages.

The main price is labelled `quote`, with its currency. When the provider
supplies a trade timestamp and feed, those appear beside the quote before
optional holdings and session details. This is the time of the trade, not
the last refresh. The header keeps navigation and connection status.

A flag shows the next high-importance macro event within seven days, or a
confirmed report date for the selected ticker within seven ET calendar
days. The ticker's report takes priority. It uses fresh cached dates only;
the flag never fetches a company's report dates itself. Estimated macro
dates keep an `est.` label, and postponed or cancelled events do not flag.

So the News, Insider and Earnings views are never a ticker name with no
price attached, and moving between tickers with the arrow keys is not a
blind jump. Parts drop one at a time as the terminal narrows, the symbol and
the price surviving to the last; `[ui] quote_rail = false` turns the line
off, and a terminal under 12 rows gives the row back to the view.

### 1 Split: the default view

![alphai-tui split view: a full-width candlestick chart with moving averages, news marks and shaded pre-market and after-hours sessions on top, the watchlist and the scored news feed side by side underneath](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/split.png)

The chart across the top 70%, the watchlist and the news feed side by
side in the bottom 30%. One screen that says what you follow, what it is
doing and what is being said about it; the chart gets the full width, so
it shows twice the candles a half-width one could. On very small terminals
the feed steps aside and the watchlist and the chart share the row.

`a` adds a ticker without leaving the app (type the symbol, Enter), `d`
drops the selected one. Both write the watchlist to the config straight
away, the way a holding is saved. A session started on tickers from the
command line (`alphai-tui TSLA`) leaves a saved watchlist alone and says
so in the footer; Save in the settings screen keeps such a list on purpose.

`/` finds a ticker in your watchlist from any view. Type part of its symbol
or company name, use `↑`/`↓` to choose a result, then `Enter` to switch;
`Esc` cancels without losing your place. Exact symbols come first. The
results show regular-session prices and daily changes, with cached or stale
quotes labelled. Names are available once a Yahoo quote has loaded (including
from the cache); other sources support symbol search. Searching is local,
does not change the watchlist order and requests no additional data until
you select a ticker. `Ctrl-U` clears the query; `PgUp`/`PgDn` and
`Home`/`End` move through a long list.
In Portfolio, selecting a held ticker focuses its row; an unheld ticker
opens in Chart.

Enter ticker symbols rather than company or index names. Names containing
spaces are rejected locally, including in command-line arguments and the
saved watchlist, before any data request. Ticker punctuation such as `^`,
`.` and `-` is preserved; support still depends on the selected data source.
If a saved watchlist contains a name, startup identifies the invalid entry.
Correct it in the config file, or pass ticker symbols on the command line
to use a different list for that session.

### 2 News: the story and what it means

![alphai-tui news view: the ticker's chart over the article list with the selected story marked on it, and the full AI analysis card on the right with sentiment, price impact, trading value and context](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/news.png)

Every article carries a per-ticker analysis: the expected price impact and
the confidence behind it, relevance and novelty scores, how actionable it
is, the background context, the entities involved and a contrarian view. A
seven-day bullish/bearish rollup tops the ticker scope.

The card starts with the selected ticker's AI impact and explanation,
followed by the general summary and other companies. The selected ticker
keeps its place even when it appears later in the provider's analysis.

The feed shows articles scoring 7 and up by default. `+` and `-` move that
bar live, the server does the filtering, so nothing you filtered out eats a
slot on the page.

Every row names its category: earnings, corporate, m&a, movers, tech,
macro, other and so on. `C` opens a checklist of all fourteen with their
full names: `space` ticks one on or off, `o` keeps only the one under the
cursor, `a` brings them all back, and `Enter` applies the choice with a
single refetch. The server filters here too, and the panel title says what is
left (`without movers, other`, or `earnings, m&a`). The choice covers the
ticker and market scopes; trending is a curated top ten and stays as it is.
To start every session with a choice, set `news_categories` or
`news_exclude_categories` in the config.

Articles fresher than 15 minutes light up their age, and
rows that arrived since you last looked carry a `●` that goes out once the
cursor rests on them. New arrivals are placed at the top of the list even
when the rows below them are newer: a story reaches the feed a while after
it was published, and at its publish position it would land below the fold
and never be seen.

The ticker's chart sits over the list and the card beside them. The article
you are reading is the highlighted mark on the chart and the chart's bottom
line names it, so the story and the move it came with are on one screen.
That needs a terminal at least 120 columns wide and the ticker scope;
elsewhere the list and the card stand side by side. `x` cycles to list over
card and to list beside card without the chart, and the News layout row in
the settings screen saves the choice. `v` opens the article for reading:
under the chart in that layout, over the whole screen in the others.
`Enter` opens the article in the browser, and down on the last row loads
the next page.

![alphai-tui news view in the market-wide scope: filings marked 8-K and 6-K, insider rows, reprints collapsed with an outlet count, and an earnings read available for the selected filing](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/market.png)

`f` cycles the scope: the selected ticker, the whole market, or the 48-hour
trending top ten. The market-wide feed collapses syndicated reprints into
one row and says how many outlets carry the story (`×4`), marks an earnings
filing as `8-K` or `6-K` rather than as coverage of one, and mixes in the
insider rows.

### 3 Table: the watchlist, full width

The watchlist alone, full width: price, change in dollars and percent, the
extended-hours change, the day's range and a sparkline of the session. The
extended column appears only when some row actually has a print outside the
session, so it costs no width during the trading day.

### 4 Chart: candles, averages, volume, RSI

![alphai-tui chart view: NVDA 15-minute candles across the pre-market, regular and after-hours sessions, each shaded apart, with SMA20 and SMA100, news marks, matching volume bars and RSI underneath](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/chart.png)

Candlesticks at half-block resolution with a previous-close reference line,
20 and 100 period moving average overlays threaded through them as thin
braille lines, a volume panel whose bars sit in their candles' own columns
and take their color, and an RSI(14) panel. `c` switches to a Braille line
chart, `m`, `i` and `b` toggle the overlays and the panels, `e` averages
simple or exponential, `t` cycles interval presets, and `E` draws the pre
and post market candles too (Yahoo, Alpaca and Tiingo), enabled by default. The
chart title keeps the switch beside the source: `EXT: Yahoo (Shift+E: off)`
or `EXT: off (Shift+E: on)` when hidden. The hint follows custom keybindings.
Where there is nothing to draw, `E` says why in the footer: daily candles,
crypto that trades around the clock, a listing outside the US, or Finnhub,
which has no candle history (its chart title reads `EXT: n/a`).
Charts start with five days of `15m` candles; set the top-level `range` and
`interval` in the config to choose another window or size. The chart title
shows both values, `5d / 15m`, because `t` and `T` cycle forward and
backward through presets that change the window as well as the candle size.
The `Last bar` line shows the newest candle's close, feed and start time.
The price marker is labelled `quote`, `PRE`, `AH` or `bar` to distinguish
a trade from a candle close. With the quote rail hidden, the chart title
also carries the quote, currency and daily change.

The ticker's news is marked on the candles it was published in: `▲` and
`▼` for the AI sentiment call, `◆` when it is neutral, brighter for a
higher relevance score, and the freshest of them named on the bottom
border. So the move and its reason share a column instead of living in
different views. `n` turns the marks off. They are drawn from the news the
app already holds for that ticker, which the Split and News views keep
fresh, so they never cost an API request and they are simply absent until
one of those views has loaded that ticker's feed.

The client fetches extra history for indicator warm-up. Pre-market has a
quiet warm background, after-hours a cool one, and regular trading keeps
the terminal background. Price, volume and RSI share the same session
columns and vertical grid in both candle and line mode. Aggregation never
combines different sessions or feeds into one candle. These session rules
apply to US stocks on intraday intervals, including scheduled 13:00 closes
and 17:00 after-hours closes on half days; crypto remains 24/7.

A bar is its interval at every width. When the plot is too narrow for
every bar of the window, the newest bars that fit are drawn and the title
counts the rest: `last 70 of 192 bars`. Bars are never merged into larger
candles, so the half-width chart in the split view draws the same bars as
the chart view, fewer of them, and the price axis follows the bars on
screen. To see more of the window, widen the terminal or pick a coarser
interval with `t`.

The time axis adapts its label spacing to the terminal width, gives the
opening and closing bells priority, and puts dates on a second row. US
stocks use New York time (`ET`) by default; `[chart] timezone = "local"`
or `"utc"` changes the labels. Other instruments use local time under the
`"exchange"` default. Future labels in the right margin skip closed US
sessions, weekends and holidays, including daylight-saving changes.
`session_shading = false` and `time_grid = false` disable those layers.

The right margin carries the latest relevant price. A timestamped quote
only updates a candle from the same source, interval and session: the
regular close cannot overwrite a pre-market candle, and an IEX quote cannot
rewrite a delayed SIP bar. Extended quotes at 0% still appear. The rail
shows their source, timestamp and age; a new premarket retires the previous
after-hours quote even if the first new trade has not arrived yet.

### 5 Insider: what the people inside the company did

![alphai-tui insider view: a year of Broadcom Form 4 events as a log-scale scatter over weekly dollar bars, the filing stream underneath and the selected filing's full card beside it](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/insider.png)

SEC Form 4 activity for the selected ticker: what its own officers and
directors did with their shares. A 12-month rollup (buys against
sells, dollar volumes, the share done under pre-arranged 10b5-1 plans, the
most active insiders) sits above a trades chart and the stream of filings.

The filing list is a ledger: transaction date, side, value, reporting owner
and a `plan` flag for 10b5-1 trades. Unknown transaction dates show `?`;
legacy rows keep their headline when the owner's name is unavailable.
Only the side uses buy/sell color. The card leads with the company and
owner, side and value, plan, stake change and tranche count, followed by
trade details and the separately labelled AI impact.

Every event in the window is a triangle placed by date on a log dollar
scale (`▲` buy, `▼` sale, a hollow `▽` for shares sold back to the issuer,
dimmed when the trade ran under a plan), with weekly buy and sell dollar
bars underneath. The mark of the filing selected in the list renders
inverted, so the list and the chart always point at each other. Beside the
list, the selected filing's full card: the trade with shares, price and
code, who made it, the AI read, and what share of the insider's stake the
event moved, how many tranches the filing folded into it and whether it was
filed late. `v` hides the card for a full-width list and brings it back.
`g` cycles the window between 3 months, 12 months and off; `+` and `-`
filter the stream by trade size.

### 6 Earnings: the filing, read

![alphai-tui earnings view: NVIDIA's second quarter fiscal 2027 read with the verdict, the summary and a metric table carrying the prior quarter, the prior year and both changes](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/earnings.png)

AlphAI's structured read of the ticker's own earnings filing, the way the
company reported it: the verdict and why, the metric table with the prior
quarter, the prior year and both changes, segments, the outlook, concerns,
what to watch and several paragraphs of analysis. Every figure was checked
against the filing text before it was published, and it prints exactly as
the filing wrote it, units shortened and nothing rounded into a new number.
American and foreign filings both (an 8-K item 2.02, or a foreign private
issuer's 6-K with its half years and its own currency).

`←` `→` walk the watchlist, older quarters continue below the newest read.
When a company has not reported since AlphAI began reading filings, the
view says so and gives the date of its next report if the company has
confirmed one. The bottom line carries the next couple of US macro releases
(CPI, the jobs report, an FOMC decision), which is the other half of what
moves a price you are about to read about.

### 7 Summary: the whole watchlist at once

![alphai-tui summary view: nine watchlist tickers as small charts in a three by three grid, each card with its price and the day's change](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/summary.png)

Every ticker you follow as its own chart, in a grid sized to the terminal.
The table's one-row sparkline says up or down; this spends real rows on each
name, so one glance covers the shape of the session across the whole list.
`↑` `↓` move between cards and page the grid when the watchlist outgrows the
screen.

### 8 Portfolio: what you hold and what it did

![alphai-tui portfolio view: three holdings with quantity, average price, last price, value, the day's move in money, profit and loss with its percentage and the share of the portfolio, and a total row underneath](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/portfolio.png)

What the watchlist cannot answer: the price times what you own of it. One
row per holding with its quantity, average price, value, the day's move in
money, the profit or loss since you bought and the share of the portfolio
it carries, and a total underneath. `p` opens a one-line prompt on the
ticker under the cursor, prefilled with what is held, so a correction is
two keystrokes and an emptied line drops the holding; the quantity and the
price are written to the config file right away rather than waiting for
Save. A holding that is not on the watchlist is polled all the same, so
every row has a price, and a row still waiting for its first one says so
instead of counting as zero. The same numbers turn up as two extra columns
in the Table view and as a zone in the quote rail, but only for the
tickers you actually hold.

Holdings use the premarket or after-hours price when the source reports
one, falling back to the regular quote otherwise. This applies to `Last`,
value, P&L, totals and the holding figures in the table and quote rail,
independently of the `E` candle toggle. An extended `Last` carries a `*`.
During premarket, `Day` starts at the latest regular close; after hours,
it includes both the regular session and the extended move.

There is no currency conversion here and there is not going to be one: if
the holdings quote in more than one currency, the total says `mixed
currencies` rather than pretending the sum means something.

### 9 Calendar: what is scheduled

![alphai-tui calendar: US macro releases and confirmed watchlist report dates in one agenda, with importance, countdowns, source details and report-date coverage](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/calendar.png)

US macro releases and confirmed report dates from your watchlist in one
agenda: the past seven days and the next 45 days. The cursor starts at the
first upcoming event, a `now` line separates it from the past, and new
arrivals keep the event you selected in place. High-importance releases
and company reports stand out; `estimated`, `postponed` and `cancelled`
remain explicit even when the terminal is too narrow for the Details column.
Event names use up to 48 columns when needed, while shorter names leave
more space for Details. Color highlights the event and its importance;
dates and source details keep a quieter style.

At the top, `Today (ET)` and `Next 7d (ET)` count the remaining scheduled
macro events, the high-importance ones and confirmed watchlist reports.
The seven-day window includes today and the following six ET dates.
Estimated schedules count too; cancelled, postponed and undated events
do not. A report with no time stays in today's count for the whole day.
`known` marks counts based on incomplete checks or cached data; an unloaded
macro schedule shows loading or unavailable instead of a zero.

When there is room below the list, the selected event shows its full name,
schedule status, reference period, press-conference time, source and data
freshness. Macro timestamps use your chosen timezone; the overview always
uses ET. Short panes give that space back to the agenda, and narrow panes
show just the high-importance macro count in the overview.

`↑` / `↓` select an event, `PgUp` / `PgDn` move ten events, and `Enter`
opens a macro event's source or the company's Earnings view. That view
contains published reads, so it may show the previous quarter until a new
read is available. Calendar is an agenda, not confirmation that a release
has been published; it has no actual, forecast or previous macro figures.

Macro times follow `[chart] timezone` (ET by default). Company report dates
stay in ET in every timezone and show `—` for time: the API confirms the
day, not the hour. A company without a confirmed date simply has no report
row; this does not mean it will not report. Coverage is partial. Historical
company dates are limited to the next-report dates still in the cache.

Dates are checked one company at a time while Calendar is open, at least
four seconds apart. The progress line distinguishes unconfirmed dates,
unchecked names and failed checks. A failed macro update keeps the last
successful rows, marked as cached, alongside any available company dates.

`r` refreshes the macro window and resumes missing, stale or failed report
date checks. It keeps fresh successful company dates, so it is not a full
watchlist refresh. To recheck one fresh company date, open its Earnings
view and press `r` there. An access or rate-limit error pauses the date
sweep until a manual retry, rather than repeating it for every company.

### Everywhere: help, settings, themes

![alphai-tui help overlay: the full key table with the config name of every action next to it, drawn over the summary grid](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/help.png)

`?` lists every action with the keys currently bound to it and the name to
use in the config to rebind it. `s` opens the settings, grouped into
prices (the source, where pre and after hours come from, the poll
interval), API keys, news and look. Each key row says what the current
choices use it for, and the bottom of the box explains the row under the
cursor: what it covers, what each choice means and what it costs. Save
writes all of it, plus the watchlist on screen, to the config file. `}` and `{` walk the color presets live, `z` hides the header
and the footer for a tmux pane that carries its own status bar, and `r`
refreshes prices and the visible data.

Price changes fade back over 900 ms without changing the text's weight or
decoration. The ASCII spinner beside the source runs only while
requests are in progress. The Animations row in settings switches these
effects off live; Save remembers it as `[ui] animations = false`. With
animations off, a steady `*` still indicates requests in progress.

The renderer wakes on input and data, with up to 30 frames per second for
short effects and a one-second clock for ages and scheduled work when idle.
RGB themes fade between their explicit colors; the default ANSI theme uses
a brief color highlight and keeps the terminal's palette. On Unix, a valid
`Sync` terminfo capability enables synchronized frames. Other terminals,
including Windows without that detection, keep ordinary frame output.
`[ui] synchronized_output = false` disables the extension altogether.

## How it compares

There are good terminal stock tools already. The two you are most likely
to be choosing between are [tickrs](https://github.com/tarkah/tickrs),
which is the closest in shape (Rust, ratatui, charts per ticker), and
[ticker](https://github.com/achannarasappa/ticker), which is the most
widely used and is built around tracking what you own.

The short version: both are built around the price. This one puts the
filings and the scored news next to it, and pays for that with no options
chain and a simpler idea of a position.

| | alphai-tui | tickrs | ticker |
|---|---|---|---|
| Price charts | candles, line, SMA/EMA, RSI, volume | line, candle, kagi, volume | none |
| Whole watchlist at once | summary grid, table with sparklines | summary pane | quote table |
| News with per-article analysis | yes, and marked on the price chart | no | no |
| SEC Form 4 insider activity | chart and filing stream | no | no |
| Earnings filing reads | yes | no | no |
| Extended hours | price and candles | candles | price |
| Options chain | no | yes | no |
| Positions and P&L | quantity and average price, in a view of its own | quantity and average price | cost-basis lots, groups, currencies |
| Export for scripts | `--once` text, `--json` | no | CSV and JSON |
| Price sources | Yahoo, Finnhub, Alpaca, Tiingo | Yahoo | Yahoo, Coinbase |
| A source that stops answering | cached start, automatic switch | no | no |
| Add or remove a ticker in the app | yes | yes | no |
| Rebindable keys | any action, in the config | vim keys | no |

**What they do better.** tickrs has an options chain with calls and puts
by expiry, which this has nothing to answer with, and a kagi chart if that
is how you read price. ticker still has the most complete position
tracking of the three: several cost-basis lots per holding, named groups
and currency conversion, where this one keeps a single average price per
ticker and sums in whatever currency the quotes come back in. If you track
lots across currencies, ticker is the one to reach for.

**One thing worth knowing.** Yahoo rate-limits by IP, and every tool here
depends on it, this one included. Four price sources is the hedge: if
Yahoo starts refusing, `s` switches to Finnhub, Alpaca or Tiingo without
leaving the app.

## Install

Homebrew (macOS and Linux):

```sh
brew install makeev/tap/alphai-tui
```

Arch Linux, from the AUR:

```sh
paru -S alphai-tui-bin
```

Debian and Ubuntu, from the apt repository (amd64 and arm64, Ubuntu 22.04 and
Debian 12 or newer):

```sh
sudo install -d -m 0755 /etc/apt/keyrings
sudo curl -fsSL https://makeev.github.io/alphai-tui-apt/alphai-tui.gpg \
  -o /etc/apt/keyrings/alphai-tui.gpg
echo "deb [signed-by=/etc/apt/keyrings/alphai-tui.gpg] https://makeev.github.io/alphai-tui-apt stable main" \
  | sudo tee /etc/apt/sources.list.d/alphai-tui.list
sudo apt update && sudo apt install alphai-tui
```

Single `.deb` files, for an install without the repository, are linked from
[the repository landing page](https://makeev.github.io/alphai-tui-apt/).

Through [x-cmd](https://www.x-cmd.com/install/alphai-tui/), which picks
whichever of the methods above your system already has:

```sh
x install alphai-tui
```

Prebuilt binaries for macOS, Linux and Windows, no Rust needed:

```sh
curl -LsSf https://github.com/makeev/alphai-tui/releases/latest/download/alphai-tui-installer.sh | sh
```

```powershell
# Windows
powershell -ExecutionPolicy Bypass -c "irm https://github.com/makeev/alphai-tui/releases/latest/download/alphai-tui-installer.ps1 | iex"
```

Archives for every platform, with checksums, live on the
[releases page](https://github.com/makeev/alphai-tui/releases).

With a Rust toolchain (1.89+):

```sh
cargo install alphai-tui
```

Or straight from the repository:

```sh
cargo install --git https://github.com/makeev/alphai-tui
```

Or from a clone:

```sh
git clone https://github.com/makeev/alphai-tui
cd alphai-tui
cargo run --release -- AAPL MSFT NVDA BTC-USD
```

## Quick start

```sh
alphai-tui NVDA AVGO AAPL MSFT META TSLA AMZN GOOGL BTC-USD
```

The first run opens the settings screen: pick a price source and paste your
AlphAI key (get one free at [alphai.io](https://alphai.io?utm_source=alphai-tui&utm_medium=referral), Account >
API keys). Leave it empty if you only want quotes and charts. Your watchlist
and options persist in the config file, so next time plain `alphai-tui` works.

```sh
alphai-tui --once AAPL      # print quotes to stdout and exit (for scripts)
alphai-tui --json AAPL      # the same run as JSON, for a status bar
alphai-tui -s finnhub NVDA  # explicit source for one run
```

## Options

| Flag | Default | Meaning |
|------|---------|---------|
| `-s, --source` | `yahoo` | Price source: `yahoo`, `finnhub`, `alpaca` or `tiingo` |
| `-e, --every` | `15` | Poll interval, seconds (also a settings row, applied live) |
| `-r, --range` | `5d` | History window: `1d 5d 1mo 3mo 6mo 1y 2y` |
| `-i, --interval` | `15m` | Candle size: `1m 2m 5m 15m 30m 60m 1d` |
| `--theme` | `default` | Color preset, e.g. `catppuccin-mocha` (also a key and a settings row) |
| `--bare` | off | Start without the header and footer, for a tmux pane (`z` toggles it live) |
| `--once` | | Print quotes to stdout and exit |
| `--json` | | Print those quotes as JSON instead of a text table (implies `--once`) |
| `--earnings TICKER` | | Print the latest earnings read to stdout and exit (needs an AlphAI key; one request) |
| `--config` | | Use an alternate config file (Save writes back to it) |

`-r` and `-i` set the startup window; the `t` key cycles the preset
combinations (configurable as `[chart] presets`) for the session without
persisting them.

### Quotes as JSON

`--json` prints one object per symbol, in the order they were asked for, so
a status bar or a cron job can read the numbers instead of parsing a table:

```sh
alphai-tui --json AAPL NVDA
alphai-tui --json AAPL | jq -r '.[0] | "\(.symbol) \(.price) \(.change_pct)%"'
```

```json
[
  {
    "candles": 79,
    "change": 11.23,
    "change_pct": 3.5612,
    "currency": "USD",
    "day_range": { "high": 326.68, "low": 316.57 },
    "extended": { "change": -1.07, "change_pct": -0.3276, "price": 325.5 },
    "fetched": "2026-09-11T09:31:38Z",
    "fifty_two_week": { "high": 344.57, "low": 226.65 },
    "prev_close": 315.34,
    "price": 326.57,
    "source": "yahoo",
    "symbol": "AAPL",
    "volume": 69820744.0
  }
]
```

A ticker you hold also carries a `position` object with `qty`,
`avg_price`, `cost`, `price` (the price used to value the holding,
including extended trading), `value`, `pnl`, and `pnl_pct` and `day_pnl` when
those can be worked out, so a status bar can show the money rather than
the price:

```sh
alphai-tui --json AAPL | jq -r '.[0].position | "\(.pnl) (\(.pnl_pct)%)"'
```

`symbol` and `price` are always there; the rest depends on what the source
answers, and a figure it does not answer is left out rather than sent as
null. `change` and `change_pct` count from the previous close, while the
`extended` object measures its own move from the regular session's close,
the way a broker screen does. A symbol that failed still gets a row, as
`{"symbol": "…", "error": "…"}`, so a watchlist of four always prints four.
Warnings go to stderr, so stdout stays a valid JSON document.

Without symbols it prints the watchlist you saved in the app, and `-s`,
`-r` and `-i` work the same as for a normal run:

```sh
alphai-tui --json                    # whatever the config file holds
alphai-tui --json -s alpaca AAPL     # another source for this one run

# a row per ticker for awk, a spreadsheet or a database
alphai-tui --json | jq -r '.[] | [.symbol, .price, .change_pct] | @tsv'

# append a snapshot to a log you can chart later; every row carries `fetched`
alphai-tui --json | jq -c '.[]' >> quotes.jsonl

# report only what broke, since the exit code is 0 either way
alphai-tui --json | jq -r '.[] | select(.error) | "\(.symbol): \(.error)"'

# watch a level from cron, printing nothing until it breaks
alphai-tui --json NVDA | jq -e '.[0].price > 200' >/dev/null &&
  echo 'NVDA above 200'
```

For a status bar, call a small wrapper instead of inlining the pipeline,
because a jq filter quoted inside `tmux.conf` or an i3blocks config turns
unreadable fast:

```sh
#!/bin/sh
# ~/bin/quote-bar
alphai-tui --json AAPL NVDA |
  jq -r 'map(select(.error | not)
             | "\(.symbol) \(.price) \(.change_pct * 100 | round / 100)%")
         | join("  ")'
```

```tmux
set -g status-interval 60
set -g status-right '#(~/bin/quote-bar)'
```

Dropping the error rows there keeps a dead symbol from writing `null` into
the bar, and the rounding trims `-6.1302` to the two decimals a bar has
room for.

One run costs one request per symbol (two on alpaca; on tiingo one or two
for the whole list plus up to three per ticker), so give the loop an
interval rather than letting the bar refresh as fast as it likes. Yahoo
throttles by IP address and answers a burst with 429s for several minutes
afterwards, which is long enough to lose the pane you built. A minute
between runs is plenty for a status bar; below that, use a keyed source.

CLI arguments win over the config file; the config file wins over built-in
defaults. API keys can also come from env vars, which win over the config:
`ALPHAI_API_KEY`, `FINNHUB_API_KEY`, `APCA_API_KEY_ID`, `APCA_API_SECRET_KEY`,
`TIINGO_API_KEY`.

## Keys

| Key | Where | Action |
|-----|-------|--------|
| `Tab` / `1`..`9` | everywhere | switch view |
| `↑` `↓` / `j` `k` | table, chart, split | select ticker |
| `a` | everywhere | add a ticker: type the symbol, `Enter` adds it, `Esc` cancels |
| `/` | everywhere | find a watchlist ticker by symbol or loaded company name; `↑`/`↓` choose, `Enter` selects, `Esc` cancels |
| `d` | everywhere | remove the selected ticker (the last one stays) |
| `p` | everywhere | set what you hold of the ticker: `qty avg`, `Enter` saves it to the config, an empty line clears it |
| `↑` `↓` / `j` `k` | news, insider | scroll articles |
| `↑` `↓` / `j` `k` | earnings | scroll the read |
| `↑` `↓` / `j` `k` | calendar | select event |
| `←` `→` / `h` `l` | news, insider, earnings | switch ticker |
| `Enter` / `o` | news, insider | open article in browser |
| `Enter` / `o` | earnings | open the read on alphai.io |
| `Enter` / `o` | calendar | open macro source or the company's Earnings view |
| `v` | news, insider | fullscreen article card; scroll with `↑` `↓`, `Esc` closes |
| `E` | everywhere | draw pre and post market candles too (Yahoo, Alpaca and Tiingo); says why when there are none |
| `x` | news | cycle the layout: chart over the list, stacked, or side-by-side |
| `PgUp` `PgDn` | news | scroll the article card pane |
| `PgUp` `PgDn` | earnings | page through the read |
| `PgUp` `PgDn` | calendar | move ten events |
| `↓` / `j` on the last row | news, insider | load the next page of the feed |
| `f` | news, split | cycle news scope: selected ticker, whole market, trending |
| `+` / `-` | news, insider, split | raise / lower the visible feed's score filter (news: relevance, starts at 7; insider: trade size, starts at 4) |
| `C` | news, split | pick the news categories to show: `space` toggles, `o` only this one, `a` all, `Enter` applies, `Esc` cancels |
| `g` | insider | cycle the trades chart window: 3 months, 12 months, off |
| `c` | chart, split | toggle candlestick / line chart |
| `m` | chart, split | toggle the two moving average overlays |
| `e` | chart, split | average them simple (SMA) or exponential (EMA) |
| `i` | chart, split | toggle the RSI(14) panel |
| `b` | chart, split | toggle the volume panel |
| `n` | chart, split | mark the ticker's cached news on the candles |
| `t` / `T` | everywhere | cycle candle interval presets forward / back (each interval with a matching history window; the list is configurable as `[chart] presets`) |
| `r` | everywhere | refresh prices and visible data; Calendar refetches macro and retries missing, stale or failed dates |
| `z` | everywhere | bare mode: hide the header and footer, giving both rows to the view |
| `}` / `{` | everywhere | next / previous color preset (session-only until Save) |
| `s` | everywhere | settings |
| `?` | everywhere | help overlay: every action with its current keys |
| `q` / `Esc` / `Ctrl-C` | everywhere | quit |

## A tmux workspace

alphai-tui is a single self-contained process, so a terminal multiplexer
(tmux, zellij, screen, or your terminal's own splits) turns it into a
custom trading workspace: run one instance per pane and switch each pane
to the view you want with `1`..`9`.

```sh
tmux new-session -d -s market 'alphai-tui --bare NVDA'
tmux split-window -h -t market 'alphai-tui --bare AAPL'   # news pane on the right
tmux select-pane -t market -L
tmux split-window -v -t market 'alphai-tui --bare AVGO'
tmux split-window -v -t market 'alphai-tui --bare TSLA'
tmux attach -t market
```

`--bare` drops the header and the key hints, which a pane with tmux's own
status bar has little use for, and hands both rows to the view; `z` toggles
it in a running instance and `[ui] bare = true` makes it the default. The
quote rail stays, so a bare pane still names its ticker and its price.

Tiled panes put frames next to frames, so the shot below also sets
`[ui] borders = "none"`: each panel becomes a tinted surface with a small
gutter and its title above, and the panes read as one desk. The Panels row
in the settings screen switches it live.

Press `4` in the three chart panes and `2` in the tall one, and you get a
wall of charts next to a live scored feed:

![four alphai-tui instances in tmux panes: three bare panes with candlestick charts and their own quote rails, next to a full-height pane showing the scored news feed](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/tmux.png)

Two things the instances share. The config file is one: the last pane to
save settings wins, so set things up once and let the other panes just
read it. Your AlphAI key's rate budget is the other: every pane showing
news or insider data spends requests from the same per-key allowance, so
on a free key keep an eye on how many such panes you open.

The same trick turns the terminal into a full trading desk with an AI
analyst on staff. Run an agent such as
[Claude Code](https://claude.com/claude-code) in the pane next to
alphai-tui and connect it to the
[AlphAI MCP server](https://alphai.io/mcp?utm_source=alphai-tui&utm_medium=referral), which serves the same news,
sentiment and insider data as the dashboard. You watch the tape on one
side while the agent digs through whatever the tape surfaces: ask it for
the last insider sells and the news that moved the stock this week, and
get a sourced brief without leaving the terminal.

![Claude Code next to alphai-tui in tmux: the agent summarizes CRWV insider selling and the week's dominant story while the dashboard shows the candlestick chart and the scored news feed](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/agent.png)

## Data sources

**Prices**

- `yahoo`: no API key, intraday quote and history in one request. Timing
  varies by exchange; extended quotes use the source's timestamps, never
  the time a response was fetched. Crypto and FX tickers work as `BTC-USD`,
  `EURUSD=X`. Includes extended hours, 52-week range and full market volume.
  A daily chart may make an additional cached intraday request to timestamp
  its extended quote. `E` controls which candles are drawn.
- `finnhub`: needs a key (free at [finnhub.io]https://finnhub.io).
  Real-time-ish quotes; historical candles are premium-only there, so charts
  build up from quotes collected during the session and reset on restart.
  Range/interval switching with `t` does not apply to that synthetic
  history, and candles degrade to flat marks.
  Free tier is 60 req/min, one request per ticker per poll; the app warns
  on startup and in the settings screen when the watchlist and the poll
  interval together go over that.
  Crypto needs exchange-prefixed symbols (`BINANCE:BTCUSDT`).
  Its quote endpoint covers the regular session only, so no 52 week range
  or volume, and the extended-hours price is borrowed (see
  "Pre and after hours" below).
- `alpaca`: needs a key id and secret (free at
  [alpaca.markets]https://alpaca.markets). Realtime quotes from the IEX
  feed plus real historical bars, so charts are complete right after start
  instead of growing over the session. Crypto works in the usual `BTC-USD`
  form. IEX covers one exchange, with sparse pre-market trading from
  08:00 ET and after-hours until 17:00 ET on normal days, so by default
  the extended quote and candles come from consolidated SIP data delayed
  15 minutes, then Yahoo if SIP is unavailable or missing the relevant
  session (see "Pre and after hours" below).
  Regular candles and the headline quote remain IEX.
  The chart labels the extended feed, and the rail labels the quote's own
  source and age. Volume bars from different feeds are not consolidated
  into a single session; IEX's total is not presented as whole-market volume.
  Once extended candles are on the chart, regular IEX candles take their
  volume from the same consolidated bars, so both sessions share one
  scale. The newest 15 minutes have no volume bar until the delayed feed
  covers them. An intraday chart with extended hours off shows IEX's own
  counts and labels them `IEX only`. Daily charts take the consolidated
  daily bars whole, so the price range and the volume are the whole
  market's, while the current day keeps its live IEX close. That costs one
  request per ticker a minute, on top of the two per poll. If it is
  refused, the chart keeps IEX's daily bars and labels them `IEX only`.

  `ALPACA_FEED=delayed_sip` uses consolidated data for both regular and
  extended sessions with a 15-minute delay. `ALPACA_FEED=sip` uses realtime
  SIP and requires a paid subscription. The delayed mode uses
  `feed=delayed_sip` for snapshots and `feed=sip` for historical bars; the
  client sets the historical end to 15 minutes ago on every subscription.
  During that lag after the opening bell, today's delayed pre-market quote
  remains visible until regular data arrives, with its PRE label and age.
  Getting free keys:
  1. Sign up at [alpaca.markets]https://alpaca.markets. Email is enough;
     market data and paper trading need no KYC.
  2. The free Basic data plan is enabled by default.
  3. In the dashboard switch the environment to Paper (fine for data), then
     Home > API Keys > Generate. Copy the Key ID and the Secret; the secret
     is shown only once.
  4. Paste both in the settings screen (`s`) or export `APCA_API_KEY_ID`
     and `APCA_API_SECRET_KEY`.

  Free plan notes: the IEX feed is realtime but thin (roughly 2 to 3 percent
  of market volume, so charts of illiquid names can be sparse), and the API
  allows 200 requests/min. The app makes 2 requests per ticker per poll,
  plus up to 2 per minute for the SIP extended hours when those come from
  Alpaca. The startup and settings warnings include that allowance when
  suggesting a polling interval. Changing candle presets can trigger a fresh cache fill.
  Alpaca sizes a bars page by the minute bars behind it, roughly two weeks
  of a liquid name's extended hours, so the first fill of a chart window
  follows up to three more pages to reach the start of the IEX series;
  later refreshes take the newest page only.
- `tiingo`: needs a key from [tiingo.com]https://www.tiingo.com, free or
  paid. Quotes and intraday bars come from IEX in real time, pre and post
  market included from 08:00 to 17:30 ET. Daily charts use Tiingo's
  consolidated end-of-day history, split adjusted, with the whole market's
  volume. Crypto works in the usual `BTC-USD` form. Coverage is US stocks,
  ETFs and mutual funds (funds have daily bars only); listings outside the
  US, indices and FX are not available here.
  During the session the price is Tiingo's IEX reference price: since
  February 2025 IEX's own last trade needs an exchange agreement, and the
  reference price stays within a few cents of it. IEX's volume is one
  venue's, so the quote carries none during the day and an intraday chart
  labels its volume `IEX only`. After the close Tiingo publishes the
  official close and the consolidated volume, and the rail switches to
  them; with an intraday chart open, the last after-hours price stays
  beside the close overnight. Before the open and after the bell IEX
  barely trades, so by default the extended quote comes from the whole
  market (see "Pre and after hours" below). With
  `extended_source = "same"` it is the IEX price beside the last close,
  dated by the bar that traded it rather than by the request.
  One request per poll quotes the whole watchlist, two when crypto is on
  it. Bars are refreshed once a minute per ticker and daily history every
  15 minutes; in between, the live price moves the last candle, and a new
  candle has no volume bar until the next refresh.
  The free plan allows 50 requests an hour and 1,000 a day. That covers a
  couple of tickers polled every few minutes, not a live dashboard: the
  quote request alone is 240 an hour at the default 15 seconds. The paid
  Power plan allows 10,000 an hour. A key does not say which plan it is
  on, so the app does not warn about the budget ahead of time. When an
  allowance runs out, the error names it, and the automatic switch below
  moves to another source.

**Pre and after hours**

IEX, the feed behind `alpaca` on its free plan and behind `tiingo`, is one
exchange, and before the open it barely trades: on 30 September 2026 by
08:52 ET Tiingo had one CoreWeave trade of 11 shares at 85.63, while the
whole market had traded 581,000 shares and stood at 87.09. `finnhub` has
no extended prices at all. So where the premarket and after-hours price
comes from is a setting of its own, `extended_source` in the config and
the Pre/after hours row in the settings screen:

- `auto`, the default: when the price source sees one exchange or none,
  the extended quote and candles come from Alpaca's consolidated SIP feed,
  15 minutes behind, if Alpaca keys are set, with Yahoo as the backup.
  Without Alpaca keys they come from Yahoo. `yahoo`, and `alpaca` on a SIP
  feed, keep their own.
- `same`: only what the price source reports itself, no extra requests.
- `alpaca`: always the consolidated SIP feed, with no Yahoo backup. It
  needs the Alpaca keys, whatever the price source is.
- `yahoo`: always Yahoo, keyless and closer to real time, but subject to
  its IP blocks.

A borrowed print replaces the source's own whenever it has one for the
session under way, and the rail and the chart title name its feed
(`SIP · delayed 15m`, `Yahoo`) and its age. Regular-session prices always
stay the price source's. Borrowing costs about two Alpaca requests or one
Yahoo request per ticker a minute: results are cached for 60 seconds per
ticker and chart window, and an empty or failed refresh keeps the session
already on screen without failing the price poll. A Yahoo IP block pauses
the borrowed Yahoo requests across the watchlist for 30 minutes. Extended
quotes stay available with `E` off, independently of the candle setting.

**When a source stops answering**

Every keyless quote feed throttles by IP sooner or later, and Yahoo's
blocks are long: measured from one address, the first arrived after about
ten requests and held for 19 minutes, the second came after eight and held
for over an hour. No client-side retry shortens that, which is why the
usual report about tools in this category is that they just stop working.
Three things happen here instead:

- Startup is never empty. The last good quotes and candles of every ticker
  are kept in `<cache dir>/alphai-tui/quotes.json` (`~/.cache` on Linux,
  `~/Library/Caches` on macOS), written at most once a minute, and put on
  screen while the first poll is in flight. The quote rail labels them
  (`cached 2h ago`) until live data replaces them, so old prices are never
  passed off as current. Entries older than a week, or taken with a
  different range and interval, are ignored rather than drawn.
- A `429` from Yahoo says what it actually is: an IP block that lasts tens
  of minutes, with the advice to switch source. It is also the one refusal
  the client does not retry, since another request only feeds the counter
  holding the block open.
- If every ticker keeps failing for 45 seconds, the app switches to another
  source that has its credentials and says so in the footer. It keeps the
  rows already on screen, never switches back on its own (probing a
  throttled feed is how a block gets extended) and never returns to a
  source that failed this session. The header always names the source in
  use, `s` picks another by hand, and `source_fallback = false` in the
  config turns the whole thing off.

**News, sentiment, insider**

- [AlphAI]https://alphai.io?utm_source=alphai-tui&utm_medium=referral: AI-enriched financial news feed. Every
  article carries validated tickers, a category, a deterministic 1 to 10
  relevance score and a full per-ticker AI analysis (sentiment, price
  impact, confidence, novelty, actionability); insider rows are generated
  from SEC EDGAR Form 4 filings, one row per economic event. The free tier
  (no card) allows 20 requests/min and 100/day. The app is careful with
  that budget: news and insider feeds fetch only what the visible view needs
  (the trending scope is one extra request), and cache each response for 5 minutes
  (`[ui] alphai_ttl_secs` in the config changes that), loads
  further pages only when you ask for them, and the article card reuses
  data already fetched with the list. The refresh at the end of that cache
  window asks the server what has arrived since the previous check rather
  than re-reading the newest page, which costs the same single request,
  keeps the pages you loaded and the row you are on, and is the only way to
  see an article that entered the feed behind its own publish time. The
  relevance and category filters are applied by the server, so
  filtered-out articles never occupy page slots; moving the score with
  `+`/`-` refetches the visible feed, one request per press at most, and a
  new choice of categories refetches it once, when it is applied.
  The Insider view's rollup and trades chart arrive as one bundle
  alongside the feed's first page and live in the same cache, so the
  chart costs no extra requests and the `g` window switch is free.
  The Earnings view costs one request per ticker, made only while that
  view is on screen and cached for an hour, because a read is published
  once a quarter and never changes afterwards. That single response
  carries the whole history of reads for the ticker and its next
  confirmed report date, which is also what fills the read shown in the
  News card, so opening the card still costs nothing. The macro calendar
  is shared by all views, including the quote-rail flag, and costs one
  request per six hours by default. Calendar checks company dates using
  the same earnings response, one company at a time, and keeps them for
  six hours. These intervals are `alphai_ttl_secs * 72`; the Earnings
  view uses `* 12`. At the minimum setting of 30 seconds, Calendar's
  interval is 36 minutes, not six hours.
  Ten companies with Calendar continuously open for 24 hours cost roughly
  44 requests at the default TTL, including the macro window. Twenty-five
  cost roughly 104, before other activity, so a larger list needs a longer
  TTL. There is no daily quota limiter, and restarting loses these caches.
  The four-second date-check pace limits this sweep, not other requests or
  other processes using the key. Manual refresh adds a macro request and
  any missing, stale or failed company checks. Errors wait for a manual
  retry; successful cached rows remain visible in Calendar.
  Feeds page 20 articles at a time, the most every plan allows (50 on Pro
  keys, detected automatically). Paging back past your plan's
  archive horizon (30 days on Free, 90 on Basic) shows an upgrade hint
  instead of older articles. Full API reference:
  [alphai.io/developers]https://alphai.io/developers?utm_source=alphai-tui&utm_medium=referral.

Ticker forms follow the US/Yahoo convention (`AAPL`, `BTC-USD`, `VOD.L`),
which is also what AlphAI uses. Finnhub-specific symbols like
`BINANCE:BTCUSDT` will not have news attached.

## Configuration

`~/.config/alphai-tui/config.toml` on Linux and macOS (`%APPDATA%` on
Windows), created by the settings screen with mode 0600 since it can hold
keys; `--config PATH` points at a different file. Saving the settings also
persists the watchlist on screen, and `a` and `d` write it straight away.
`a`, `d` and `p` apply their one change to the file as it is on disk,
under a lock file beside it, so several tmux panes on one config do not
undo each other. Every key is optional. A misspelled value
in the `[ui]`, `[chart]`, `[theme]` or `[keybindings]` sections prints a
warning on startup and keeps that entry's default, and a broken
`[[positions]]` entry is skipped on its own. A file that does not parse at
all, say after a TOML syntax error, runs the session on the defaults, says
so in the footer and is not written to until it loads again; Save in the
settings screen keeps a copy as `config.toml.broken` before replacing it.
Fixed while the app runs, it is picked up by a restart. The `[ui]` and `[chart]` sections set startup
defaults; the session keys (`x`, `f`, `g`, `+`, `-`, `C`, `c`, `m`, `i`, `b`, `e`, `n`, `t`)
still change everything live without persisting it:

```toml
source = "yahoo"
watchlist = ["AAPL", "MSFT", "NVDA", "BTC-USD"]
every = 15
range = "5d"         # startup history window
interval = "15m"     # startup candle size; t cycles the chart presets live
news_open = "alphai"  # where enter opens news: "alphai" or "original"
source_fallback = true  # switch source when this one stops answering
extended_source = "auto"  # pre and after hours: auto | same | alpaca | yahoo (also a settings row)

[keys]
alphai = "ak_live_..."
finnhub = ""
alpaca_key_id = ""
alpaca_secret = ""
tiingo = ""

[ui]
default_view = "split"    # split | news | table | chart | insider | earnings | summary | portfolio | calendar
quote_rail = true         # the price line under the tabs
bare = false              # start with no header and no footer (--bare, z)
animations = true         # price color fades and refresh spinner; also in settings
synchronized_output = true # use synchronized frames only when terminfo advertises support
news_layout = "chart"     # chart | stacked | side (also a row in the settings screen)
news_scope = "ticker"     # ticker | market | trending
borders = "rounded"       # frame lines: rounded | plain, or none for tinted panels (also a settings row)
news_min_score = 7        # minimum relevance score in news feeds, 1 to 10
news_categories = []      # only these categories, e.g. ["earnings", "m&a"]; empty means all
news_exclude_categories = []  # or all but these, e.g. ["crypto", "other"]
insider_min_score = 4     # insider feed filter; the score tracks trade size
insider_chart = "3m"      # insider trades chart window at start: 3m | 12m | off
alphai_ttl_secs = 300     # news/sentiment/insider cache lifetime, 30 to 86400

[chart]
style = "candles"         # candles | line
sma = true                # moving average overlays visible at start
ma_type = "sma"           # sma | ema, both using the periods below
rsi = true                # RSI panel visible at start
volume = true             # volume panel visible at start
extended_hours = true     # draw pre and post market candles; Shift+E toggles live
timezone = "exchange"    # exchange (ET for US stocks), local, utc
session_shading = true   # warm pre-market / cool after-hours backgrounds
time_grid = true         # vertical grid shared by price, volume and RSI
news_markers = true       # mark the ticker's cached news on the candles
sma_fast = 20             # 2 to 250
sma_slow = 100            # 2 to 250; also sizes the history warm-up
rsi_period = 14           # 2 to 100
right_margin_pct = 20     # free space right of the newest candle, 0 to 50
presets = [               # the combos the t and T keys cycle
  ["1d", "5m"],
  ["5d", "15m"],
  ["1mo", "60m"],
  ["6mo", "1d"],
  ["1y", "1d"],
]

# What you hold, one entry per ticker. The p key writes these for you and
# saves them here immediately; editing them by hand works just as well.
# A ticker listed here is polled even when it is not on the watchlist.
[[positions]]
symbol = "AAPL"
qty = 12
avg_price = 182.31

[[positions]]
symbol = "BTC-USD"
qty = 0.25
avg_price = 58200
```

A quantity may be fractional, for crypto or for a broker that sells
slices, and negative for a short, in which case a falling price is a
profit. `avg_price` is what one unit cost on average: this is not a ledger
and it does not keep lots, so a second buy means updating the average
yourself (or letting `p` overwrite the line).

### Colors

`[theme] preset` swaps in a ready-made palette:

```toml
[theme]
preset = "catppuccin-mocha"
```

Each preset on a terminal wearing the matching palette (`default` on a plain
dark one, since it takes the terminal's own colors):

<table>
<tr>
<td align="center"><img src="https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/gallerydefault.png" alt="alphai-tui split view in the default preset" width="100%"><br><code>default</code></td>
<td align="center"><img src="https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/gallerydracula.png" alt="alphai-tui split view in the dracula preset" width="100%"><br><code>dracula</code></td>
<td align="center"><img src="https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/gallerycatppuccinmocha.png" alt="alphai-tui split view in the catppuccin-mocha preset" width="100%"><br><code>catppuccin-mocha</code></td>
</tr>
<tr>
<td align="center"><img src="https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/gallerycatppuccinmacchiato.png" alt="alphai-tui split view in the catppuccin-macchiato preset" width="100%"><br><code>catppuccin-macchiato</code></td>
<td align="center"><img src="https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/gallerycatppuccinfrappe.png" alt="alphai-tui split view in the catppuccin-frappe preset" width="100%"><br><code>catppuccin-frappe</code></td>
<td align="center"><img src="https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/gallerygruvboxdark.png" alt="alphai-tui split view in the gruvbox-dark preset" width="100%"><br><code>gruvbox-dark</code></td>
</tr>
<tr>
<td align="center"><img src="https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/gallerynord.png" alt="alphai-tui split view in the nord preset" width="100%"><br><code>nord</code></td>
<td align="center"><img src="https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/gallerycatppuccinlatte.png" alt="alphai-tui split view in the catppuccin-latte preset" width="100%"><br><code>catppuccin-latte</code></td>
<td align="center"><img src="https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/gallerygruvboxlight.png" alt="alphai-tui split view in the gruvbox-light preset" width="100%"><br><code>gruvbox-light</code></td>
</tr>
</table>

The panel look is a separate choice. Frame lines are the default (`rounded`,
or `plain`); `[ui] borders = "none"`, or the Panels row in the settings
screen, draws tinted panels with a gutter instead:

![alphai-tui split view in the dracula preset with borders = "none": tinted panels separated by a small gutter, each titled above, in place of frame lines](https://raw.githubusercontent.com/makeev/alphai-tui/main/assets/borderless.png)

`}` and `{` walk the presets live, `--theme catppuccin-mocha` picks one
for a single run, and the Theme row in the settings screen (`s`) does
both: `←` `→` cycle it with a live preview, Save writes it here.

Available: `default`, `catppuccin-mocha`, `catppuccin-macchiato`,
`catppuccin-frappe`, `catppuccin-latte`, `dracula`, `gruvbox-dark`,
`gruvbox-light`, `nord`. Presets are written in hex, so they want a
terminal with 24-bit color. With `borders = "none"` they tint the panels
one step off the palette's background, the regular session sits on that
tint, and the pre-market and after-hours bands are drawn from it, so the
three stay apart. `default` uses ANSI foregrounds, no panel tint and
subtle RGB session backgrounds;
`session_shading = false` preserves a fully transparent chart background. The two
light ones (`catppuccin-latte`, `gruvbox-light`) expect a light terminal
background.

`pre_market_bg` and `post_market_bg` are the session background slots;
each accepts the same color formats as the foreground slots. Setting either
to `"reset"` uses the terminal background for that session.

Every color the views draw comes from a named slot, and the optional
`[theme]` table recolors any of them, over the preset when there is one.
Values are ANSI color names (case-insensitive, `light-blue`, `grey`),
`#RRGGBB` hex, or an ANSI-256 index written as a string like `"245"`. A
bad color, a misspelled slot or an unknown preset prints a warning on
startup and keeps the default; it never breaks the config file. The
defaults are the values shown:

```toml
[theme]
accent = "cyan"          # header title, active tab, overlay borders, headings
accent_text = "black"    # text on the active tab
up = "green"             # price up: candles, deltas, sparklines
down = "red"             # price down
flat = "gray"            # unchanged / no data
pos = "green"            # bullish sentiment, insider buys
neg = "red"              # bearish sentiment, insider sells
error = "red"            # error messages
warn = "yellow"          # notices and the editing highlight
score_high = "yellow"    # relevance score 8 to 10
sma_fast = "yellow"      # fast moving average overlay
sma_slow = "magenta"     # slow moving average overlay
rsi_line = "cyan"        # RSI line
ref_line = "darkgray"    # previous close and RSI 30/70 reference lines
border = "reset"         # panel frames; reset keeps the terminal's foreground
text = "reset"           # body text on tinted panels; reset keeps the terminal's foreground
subtle = "reset"         # sources, ages, axis labels, key hints; reset means the terminal's dim
faint = "reset"          # separators and inactive text; reset means the terminal's dim
selection = "reset"      # background of the cursor row; reset means reverse video
surface = "reset"        # panel background (borders = "none"); reset paints none
```

The presets fill the last five from their own palettes: secondary text in
the family's subtext shade, a cursor row that keeps each column's color,
and a panel tint one step off the background. With `default` they follow
the terminal, which is the only way to stay readable on both a dark and a
light background.

### Custom keybindings

The optional `[keybindings]` table rebinds any action. An action you list
replaces its default keys entirely; actions you leave out keep theirs. The
value is one key or a list of keys:

```toml
[keybindings]
quit = "ctrl-q"
open = ["enter", "w"]
next_preset = "]"
prev_preset = "["
```

A key is written as `[ctrl-][alt-][shift-]<base>`, where base is a single
character or one of the named keys: `esc`, `enter`, `tab`, `backtab`,
`space`, `up`, `down`, `left`, `right`, `home`, `end`, `pgup`, `pgdn`,
`backspace`, `delete`, `insert`, `f1` to `f12`. `shift-` plus a letter
means the uppercase letter (`shift-t` equals `T`), and `shift-tab` equals
`backtab`.

The actions: `quit`, `next_view`, `prev_view`, `settings`, `help`,
`refresh`, `up`, `down`, `left`, `right`, `page_up`, `page_down`, `open`,
`card`, `cycle_scope`, `cycle_layout`, `score_up`, `score_down`,
`insider_chart`, `chart_style`, `toggle_sma`, `toggle_rsi`,
`toggle_volume`, `news_markers`, `ma_type`, `next_preset`, `prev_preset`,
`next_theme`, `prev_theme`, `toggle_bare`, `add_ticker`, `remove_ticker`,
`position`, `extended_hours`, `categories`, `find_ticker`.
The `?` help overlay shows this list with the current keys next to it.

Reserved and never remappable: `ctrl-c` (force quit), `esc`, the digits
`1` to `9` (view hotkeys), the keys inside the settings form, and `space`,
`o`, `a` and `enter` inside the category picker. The ticker search uses fixed
arrow, paging and text-editing keys while open; letters always type the query.
A bad or reserved key, an unknown action, or a key claimed by two actions prints a
warning on startup and falls back safely; the footer always shows the keys
that are actually bound.

### Not configurable on purpose

The AlphAI response cache (5 minutes), the feed page sizes, the 2 second
poll floor and the chart warm-up factors are fixed. They keep the app a
fair citizen of the free API tiers, and a config knob for them would turn
an innocent-looking file into an abuse vector. `ALPACA_FEED`,
`ALPHAI_API_URL`, `ALPACA_DATA_URL`, `YAHOO_CHART_URL` and `TIINGO_API_URL`
stay env-only debug overrides for the same reason.

## Architecture

```
src/
  domain.rs      Quote, Candle, TickerData, Range/Interval
  config.rs      config file load/save (CLI > env > file > defaults)
  source/        DataSource trait + implementations
    registry.rs  the one place a new source registers; CLI, settings and keys derive from it
    http.rs      shared client builder, JSON fetching and error helpers
    yahoo.rs     Yahoo v8 chart endpoint (quote + history in one call)
    finnhub.rs   Finnhub /quote with synthetic session history
    alpaca.rs    snapshot + real historical bars (IEX/SIP feeds, crypto)
    tiingo.rs    one IEX quote request per poll, cached IEX bars and daily history, crypto
  alphai.rs      AlphAI API client + demand-driven fetch task (TTL cache)
  keymap.rs      semantic actions + the key table (footer hints derive from it)
  theme.rs       semantic color palette ([theme] overrides)
  indicators.rs  SMA, EMA and RSI (Wilder smoothing)
  poller.rs      fetches all symbols concurrently on a timer -> mpsc channel
  app.rs         App state, event loop, key handling
    app/feeds.rs     feed cache and every AlphAI request-budget guard
    app/settings.rs  settings overlay state, rows derived from the registry
  ui/            View trait + implementations
    table.rs     watchlist table
    chart.rs     candlestick + line chart, SMA overlays, volume and RSI panels
    split.rs     table + chart
    news.rs      article list + sentiment rollup + detail pane
    insider.rs   Form 4 rollup + filing list
    insider_chart.rs  log-scale trades scatter + weekly dollar bars
    earnings.rs  structured earnings read + the schedule line
    calendar.rs  watchlist agenda, report-date progress and event flags
    article.rs   modal full-article card (AI analysis, context)
    settings.rs  modal settings overlay
```

Data flows one way: background tasks (price poller, AlphAI fetcher) push
events over an mpsc channel into `App::apply`; views are stateless renderers
over `&mut App`. The UI never blocks on the network, and every AlphAI
request-budget guard lives in one file (`app/feeds.rs`).

### Adding a price source

1. Implement `source::DataSource` (one async `fetch` returning quote plus
   candles) in a new module under `src/source/`. The helpers in
   `source/http.rs` cover the client, JSON fetching and error plumbing.
   A provider that quotes many tickers in one request can also implement
   `begin_cycle`, which hears the poll's whole symbol list before its
   `fetch` calls start, and answer all of them from one response (see
   `source/tiingo.rs`).
2. Append one `SourceInfo` entry to `source/registry.rs`: id, aliases, a
   settings hint, the key fields it needs and a constructor. The `--source`
   help and error list, the settings screen rows and picker cycle, config
   `[keys]` persistence and env-var overrides all derive from that entry,
   and the registry tests check it.
3. Describe the source in this README.

### Adding a view

1. Implement `ui::View` as a unit struct in a new module under `src/ui/`: a
   stateless `render` over `&mut App`, a new `ViewId` variant, a footer hint
   line, and the capability methods (`feed_shown`, `navigates_articles`,
   `has_chart_panel`, `shows_earnings`, `shows_calendar`) that opt into shared
   key handling and demand-driven AlphAI fetching. Views never fetch anything themselves.
2. Add it to `ui::VIEWS`. Order in that array defines the tab cycle and the
   `1`..`9` hotkeys; the header pills and the footer hints derive from it.

## Development

```sh
cargo test          # unit + TestBackend rendering tests
cargo clippy --all-targets
cargo fmt --all -- --check
cargo run -- --once AAPL             # network smoke test without a TTY
ALPHAI_API_KEY=ak_live_... cargo test live_calendar_smoke -- --ignored  # 1 request
ALPHAI_API_KEY=ak_live_... cargo test live_api -- --ignored   # 14 requests
```

CI runs the first three on every push and pull request, on Linux, macOS
and Windows. What changed in each release is in
[CHANGELOG.md](CHANGELOG.md).

Issues and PRs are welcome.

## License

MIT. Not investment advice; data comes from third-party sources and can be
delayed or wrong. Respect the terms of the data providers you enable.