From 778adb7ce05ab8471c0e00deda6a666f7b22c582 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Tue, 3 Mar 2026 16:06:00 +0100 Subject: [PATCH] feat(fxt): render epoch financial metrics in watch TUI list + detail views Add Sharpe/Win% columns to training list table, financial summary lines (Sharpe, Sortino, Win Rate, Max DD, PF, Return, Avg, Trades) to the detail overview, action distribution (BUY/SELL/HOLD %) to current metrics, and four new sparklines (Sharpe, Win Rate, Max DD, Total Return) to the Metrics sub-tab. Co-Authored-By: Claude Opus 4.6 --- bin/fxt/src/commands/watch/render.rs | 45 +++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/bin/fxt/src/commands/watch/render.rs b/bin/fxt/src/commands/watch/render.rs index a96b7347c..6653716b6 100644 --- a/bin/fxt/src/commands/watch/render.rs +++ b/bin/fxt/src/commands/watch/render.rs @@ -149,6 +149,8 @@ fn render_training_list(frame: &mut Frame, area: Rect, tab: &TrainingTabState) { Cell::from("LR"), Cell::from("Batch/s"), Cell::from("Grad"), + Cell::from("Sharpe"), + Cell::from("Win%"), ]) .style( Style::default() @@ -171,6 +173,8 @@ fn render_training_list(frame: &mut Frame, area: Rect, tab: &TrainingTabState) { Cell::from(format!("{:.2e}", s.learning_rate)), Cell::from(format!("{:.1}", s.batches_per_second)), Cell::from(format!("{:.3}", s.gradient_norm)), + Cell::from(if s.epoch_sharpe != 0.0 { format!("{:.2}", s.epoch_sharpe) } else { "-".to_owned() }), + Cell::from(if s.epoch_win_rate > 0.0 { format!("{:.0}%", s.epoch_win_rate * 100.0) } else { "-".to_owned() }), ]) }) .collect(); @@ -188,6 +192,8 @@ fn render_training_list(frame: &mut Frame, area: Rect, tab: &TrainingTabState) { Constraint::Length(10), Constraint::Length(8), Constraint::Length(8), + Constraint::Length(8), + Constraint::Length(8), ], ) .header(header) @@ -351,8 +357,8 @@ fn render_detail_overview( let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(8), // key stats - Constraint::Min(0), // loss sparkline + Constraint::Length(12), // key stats (incl. financial metrics) + Constraint::Min(0), // loss sparkline ]) .split(area); @@ -377,6 +383,17 @@ fn render_detail_overview( " NaN: {} Grad Explosions: {} Feature Errors: {}", session.nan_detected, session.gradient_explosions, session.feature_errors, )), + Line::from(""), + Line::from(format!( + " Sharpe: {:.2} Sortino: {:.2} Win Rate: {:.1}% Max DD: {:.1}%", + session.epoch_sharpe, session.epoch_sortino, + session.epoch_win_rate * 100.0, session.epoch_max_drawdown * 100.0, + )), + Line::from(format!( + " PF: {:.2} Return: {:+.2}% Avg: {:+.4} Trades: {}", + session.epoch_profit_factor, session.epoch_total_return * 100.0, + session.epoch_avg_return, session.epoch_total_trades, + )), ]) .block( Block::default() @@ -454,7 +471,7 @@ fn render_detail_metrics( let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(5), // current values + Constraint::Length(7), // current values (incl. action distribution) Constraint::Min(0), // sparklines ]) .split(area); @@ -468,6 +485,12 @@ fn render_detail_metrics( " Precision: {:.4} Recall: {:.4}", session.eval_precision, session.eval_recall, )), + Line::from(format!( + " Action: BUY {:.0}% SELL {:.0}% HOLD {:.0}%", + session.action_buy_pct * 100.0, + session.action_sell_pct * 100.0, + session.action_hold_pct * 100.0, + )), ]) .block( Block::default() @@ -479,10 +502,14 @@ fn render_detail_metrics( let spark_area = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), + Constraint::Ratio(1, 8), + Constraint::Ratio(1, 8), + Constraint::Ratio(1, 8), + Constraint::Ratio(1, 8), + Constraint::Ratio(1, 8), + Constraint::Ratio(1, 8), + Constraint::Ratio(1, 8), + Constraint::Ratio(1, 8), ]) .split(chunks[1]); @@ -490,6 +517,10 @@ fn render_detail_metrics( render_sparkline_row(frame, spark_area[1], "Precision", &history.precision, 1.0, Color::Cyan); render_sparkline_row(frame, spark_area[2], "Recall", &history.recall, 1.0, Color::Magenta); render_sparkline_row(frame, spark_area[3], "F1", &history.f1, 1.0, Color::Yellow); + render_sparkline_row(frame, spark_area[4], "Sharpe", &history.sharpe, 20.0, Color::Cyan); + render_sparkline_row(frame, spark_area[5], "Win Rate", &history.win_rate, 1.0, Color::Green); + render_sparkline_row(frame, spark_area[6], "Max DD", &history.max_drawdown, 1.0, Color::Red); + render_sparkline_row(frame, spark_area[7], "Total Return", &history.total_return, 1.0, Color::Magenta); } fn render_detail_hyperopt(