2323from textual .widgets import Static , TabbedContent , TabPane
2424
2525from tui import analytics
26+ from tui .render import sparkline as sp
2627from tui .ui .screens .base import BaseScreen
2728
2829MAJORS = ["BTC" , "ETH" , "SOL" , "BNB" , "XRP" , "DOGE" ]
@@ -159,60 +160,96 @@ def _render(
159160 self ._health .update (self ._render_health (health or {}))
160161
161162 def _render_regime (self , selected , closes , snapshot ):
162- series = closes .get (selected ) or next (iter (closes .values ()), [])
163- sym = selected if closes .get (selected ) else (
164- next (iter (closes ), selected )
165- )
166- if not series :
163+ if not closes :
167164 return Text ("No candle data yet." , style = "dim" )
165+ sym = selected if closes .get (selected ) else next (iter (closes ), selected )
166+ series = closes .get (sym , [])
168167 r = analytics .regime (series )
169168 trend_color = {"up" : "green" , "down" : "red" , "flat" : "yellow" }.get (
170169 r ["trend" ], "white"
171170 )
172- t = Table .grid (padding = (0 , 2 ))
173- t .add_column (justify = "right" , style = "bold" )
174- t .add_column ()
175- t .add_row ("Symbol" , sym )
176- t .add_row ("Trend" , Text (str (r ["trend" ]).upper (), style = trend_color ))
177- t .add_row ("Volatility" , f"{ r ['vol_state' ]} (pct { r ['vol_pct' ]:.0%} )" )
178- t .add_row ("Realized vol" , f"{ r ['vol' ]:.4%} " )
179- t .add_row ("Drift vs SMA" , f"{ r .get ('drift' , 0.0 ):+.2%} " )
180- return t
171+ stats = Table .grid (padding = (0 , 2 ))
172+ stats .add_column (justify = "right" , style = "bold" )
173+ stats .add_column ()
174+ stats .add_row ("Symbol" , sym )
175+ stats .add_row ("Price 48×1h" , Text (sp .sparkline (series , width = 48 ), style = trend_color ))
176+ stats .add_row ("Trend" , Text (str (r ["trend" ]).upper (), style = trend_color ))
177+ stats .add_row (
178+ "Volatility" ,
179+ f"{ r ['vol_state' ]:<7} { sp .gauge (r ['vol_pct' ], 0 , 1 , width = 14 )} { r ['vol_pct' ]:.0%} " ,
180+ )
181+ stats .add_row ("Realized vol" , f"{ r ['vol' ]:.4%} " )
182+ stats .add_row ("Drift vs SMA" , Text (f"{ r .get ('drift' , 0.0 ):+.2%} " , style = trend_color ))
183+
184+ # Watchlist: price sparkline per major (the MVP "Price Action" feature).
185+ watch = Table (title = "Watchlist · 48×1h" , box = box .ASCII , expand = False )
186+ watch .add_column ("sym" , style = "bold" )
187+ watch .add_column ("last" , justify = "right" )
188+ watch .add_column ("chg" , justify = "right" )
189+ watch .add_column ("spark" )
190+ for s in sorted (closes ):
191+ cs = closes [s ]
192+ chg = (cs [- 1 ] - cs [0 ]) / cs [0 ] if cs and cs [0 ] else 0.0
193+ col = "green" if chg >= 0 else "red"
194+ watch .add_row (
195+ s , f"{ cs [- 1 ]:,.6g} " ,
196+ Text (f"{ chg :+.2%} " , style = col ),
197+ Text (sp .sparkline (cs , width = 32 ), style = col ),
198+ )
199+ grid = Table .grid (padding = (1 , 0 ))
200+ grid .add_row (stats )
201+ grid .add_row (watch )
202+ return grid
181203
182204 def _render_correlation (self , closes ):
183205 if len (closes ) < 2 :
184206 return Text ("Need ≥2 symbols with candles for correlation." , style = "dim" )
185207 symbols , matrix = analytics .correlation_matrix (closes )
186208 table = Table (
187- title = "1h return correlation" ,
188- show_lines = False ,
189- expand = False ,
190- box = box .ASCII ,
209+ title = "1h return correlation · heatmap (green=+ red=- intensity=|r|)" ,
210+ show_lines = False , expand = False , box = box .ASCII ,
191211 )
192212 table .add_column ("" )
193213 for s in symbols :
194- table .add_column (s , justify = "right " )
214+ table .add_column (s , justify = "center " )
195215 for i , a in enumerate (symbols ):
196216 cells = [Text (a , style = "bold" )]
197217 for j , _ in enumerate (symbols ):
198218 v = matrix [i ][j ]
199219 if v is None :
200- cells .append (Text ("· " , style = "dim" ))
220+ cells .append (Text (" " , style = "dim" ))
201221 else :
202- style = "green" if v > 0.5 else "red" if v < - 0.5 else "white"
203- cells .append (Text (f" { v :+.2f } " , style = style ))
222+ color = "green" if v > 0 else "red" if v < 0 else "white"
223+ cells .append (Text (sp . heat_cell ( v , 1.0 ) * 2 , style = color ))
204224 table .add_row (* cells )
205225 return table
206226
207227 def _render_flow (self , snapshot ):
208228 if not snapshot :
209229 return Text ("No market snapshot yet." , style = "dim" )
210- ext = analytics .funding_extremes (snapshot , n = 5 )
211- oi = analytics .oi_leaders (snapshot , n = 5 )
212- table = Table .grid (padding = (0 , 3 ))
213- table .add_column ()
214- table .add_column ()
215- table .add_column ()
230+ ext = analytics .funding_extremes (snapshot , n = 6 )
231+ oi = analytics .oi_leaders (snapshot , n = 6 )
232+ oi_max = max ((v for _ , v in oi ), default = 1.0 )
233+ out = Table .grid (padding = (1 , 0 ))
234+
235+ # Funding heatmap strip across all symbols (intensity=|funding|, sign-colored).
236+ fr : list [tuple [str , float ]] = []
237+ for r in snapshot :
238+ try :
239+ fr .append ((str (r .get ("symbol" ) or "?" ), float (r .get ("funding" ))))
240+ except (TypeError , ValueError ):
241+ continue
242+ if fr :
243+ fmax = max (abs (f ) for _ , f in fr ) or 1.0
244+ strip = Text ("Funding heatmap " , style = "dim" )
245+ for _s , f in fr [:48 ]:
246+ strip .append (sp .heat_cell (f , fmax ), style = "green" if f >= 0 else "red" )
247+ out .add_row (strip )
248+
249+ cols = Table .grid (padding = (0 , 3 ))
250+ cols .add_column ()
251+ cols .add_column ()
252+ cols .add_column ()
216253
217254 def col (title , pairs , fmt ):
218255 inner = Table (title = title , show_edge = False , box = box .ASCII )
@@ -222,12 +259,13 @@ def col(title, pairs, fmt):
222259 inner .add_row (sym , fmt (val ))
223260 return inner
224261
225- table .add_row (
262+ cols .add_row (
226263 col ("Funding +" , ext ["most_positive" ], lambda v : Text (f"{ v :+.4%} " , style = "green" )),
227264 col ("Funding -" , ext ["most_negative" ], lambda v : Text (f"{ v :+.4%} " , style = "red" )),
228- col ("Open interest" , oi , lambda v : f"{ v :,.0f} " ),
265+ col ("Open interest" , oi , lambda v : Text ( f"{ sp . heat_bar ( v , oi_max , width = 10 ) } { v :,.0f} " ) ),
229266 )
230- return table
267+ out .add_row (cols )
268+ return out
231269
232270 def _render_health (self , health ):
233271 if not health :
0 commit comments