# Epoch-Level Financial Metrics in fxt

**Date**: 2026-03-03
**Status**: Design

## Problem

Training produces per-epoch financial metrics (Sharpe, win rate, max DD) internally,
but none are exposed through the Prometheus → monitoring service → fxt pipeline.
The fxt watch TUI and `fxt train monitor` can only show loss, accuracy, and RL
diagnostics — not the trading-relevant metrics that matter.

## Scope

- Add ~11 new Prometheus gauges for epoch-level financial metrics
- Add epoch-level backtest evaluation for supervised models (TFT, Mamba2, etc.)
- Add epoch history ring buffer (last 50 epochs) in monitoring service
- Wire through proto → monitoring service → fxt state → fxt render
- Fix: action distribution already tracked but not exposed

## Architecture

```
Trainer (DQN/PPO/TFT/Mamba2/...)
  │ Epoch end: run mini-backtest on validation window
  │ set_epoch_financial_metrics("dqn", "fold_0", sharpe, sortino, ...)
  │ set_action_distribution_pct("dqn", "fold_0", buy%, sell%, hold%)
  v
Prometheus (foxhunt_training_epoch_{sharpe,sortino,win_rate,...})
  v
Monitoring Service
  │ scrapes + maps to proto fields
  │ stores ring buffer: HashMap<session_key, VecDeque<EpochSnapshot>>
  v
GetLiveTrainingMetrics: latest snapshot (existing streaming)
GetEpochHistory: full ring buffer for selected session (new RPC)
  v
fxt watch TUI: detail view shows financial table + sparklines
fxt train monitor: new financial metrics section
```

## New Prometheus Gauges

All with `{model, fold}` labels, pushed at epoch end:

| Gauge | Description |
|---|---|
| `foxhunt_training_epoch_sharpe` | Sharpe ratio |
| `foxhunt_training_epoch_sortino` | Sortino ratio |
| `foxhunt_training_epoch_win_rate` | Win rate (0-1) |
| `foxhunt_training_epoch_max_drawdown` | Max drawdown (0-1) |
| `foxhunt_training_epoch_profit_factor` | Gross profit / gross loss |
| `foxhunt_training_epoch_total_return` | Total return (fractional) |
| `foxhunt_training_epoch_avg_return` | Avg return per trade |
| `foxhunt_training_epoch_total_trades` | Trade count in eval |
| `foxhunt_training_epoch_action_buy_pct` | BUY action % |
| `foxhunt_training_epoch_action_sell_pct` | SELL action % |
| `foxhunt_training_epoch_action_hold_pct` | HOLD action % |

Convenience functions:
- `set_epoch_financial_metrics(model, fold, sharpe, sortino, win_rate, max_dd, profit_factor, total_return, avg_return, total_trades)`
- `set_action_distribution_pct(model, fold, buy_pct, sell_pct, hold_pct)`

## Proto Changes

### monitoring.proto — TrainingSession (new fields 36-46)

```protobuf
// Epoch-level financial metrics (from backtest evaluation)
float epoch_sharpe = 36;
float epoch_sortino = 37;
float epoch_win_rate = 38;
float epoch_max_drawdown = 39;
float epoch_profit_factor = 40;
float epoch_total_return = 41;
float epoch_avg_return = 42;
uint32 epoch_total_trades = 43;
// Action distribution (aggregated %)
float action_buy_pct = 44;
float action_sell_pct = 45;
float action_hold_pct = 46;
```

### monitoring.proto — New RPC + messages

```protobuf
rpc GetEpochHistory(GetEpochHistoryRequest)
    returns (GetEpochHistoryResponse);

message GetEpochHistoryRequest {
  string model = 1;
  string fold = 2;
  uint32 max_epochs = 3;  // 0 = all available (up to 50)
}

message EpochFinancialSnapshot {
  uint32 epoch = 1;
  float sharpe = 2;
  float sortino = 3;
  float win_rate = 4;
  float max_drawdown = 5;
  float profit_factor = 6;
  float total_return = 7;
  float avg_return = 8;
  uint32 total_trades = 9;
  float loss = 10;
  float val_loss = 11;
  float learning_rate = 12;
  float action_buy_pct = 13;
  float action_sell_pct = 14;
  float action_hold_pct = 15;
}

message GetEpochHistoryResponse {
  string model = 1;
  string fold = 2;
  repeated EpochFinancialSnapshot epochs = 3;
}
```

## Supervised Model Evaluation

Supervised models (TFT, Mamba2, TGGN, TLOB, LNN, KAN, xLSTM, Diffusion) produce
price/return predictions, not discrete actions.

**Mini-backtest at epoch end:**
1. Run inference on validation window bars
2. Convert predictions to signals: prediction > threshold → BUY, < -threshold → SELL, else HOLD
3. Simulate trades using existing `EvaluationEngine` (ml/src/evaluation/)
4. Extract Sharpe, max DD, win rate, total return, trade count
5. Push to Prometheus gauges

This reuses `UnifiedTrainable::evaluate()` which already returns `EvaluationResult`
with financial metrics. The trainers just need to call it at epoch end and push metrics.

## Monitoring Service Changes

### Metric mapper (service.rs)

Add 11 new match arms:
```rust
"foxhunt_training_epoch_sharpe" => session.epoch_sharpe = s.value as f32,
"foxhunt_training_epoch_sortino" => session.epoch_sortino = s.value as f32,
// ... etc.
```

### Epoch history store

```rust
struct EpochHistoryStore {
    histories: HashMap<String, VecDeque<EpochFinancialSnapshot>>,
}
```

- Key: `"{model}/{fold}"`
- Capacity: 50 epochs per session
- On each Prometheus scrape that has new `current_epoch`: snapshot all financial
  fields into the ring buffer
- Served via `GetEpochHistory` RPC

## fxt Changes

### State (state.rs)

Add 11 fields to `TrainingSession`:
```rust
pub epoch_sharpe: f32,
pub epoch_sortino: f32,
pub epoch_win_rate: f32,
pub epoch_max_drawdown: f32,
pub epoch_profit_factor: f32,
pub epoch_total_return: f32,
pub epoch_avg_return: f32,
pub epoch_total_trades: u32,
pub action_buy_pct: f32,
pub action_sell_pct: f32,
pub action_hold_pct: f32,
```

Add to `SessionHistory`:
```rust
pub sharpe: Vec<f64>,
pub win_rate: Vec<f64>,
pub max_drawdown: Vec<f64>,
pub total_return: Vec<f64>,
```

### Render (render.rs)

**List view**: Add Sharpe + Win Rate columns to the session table.

**Detail view — Metrics sub-tab**: Full financial metrics table:
| Metric | Value |
|---|---|
| Sharpe Ratio | 2.31 (green/yellow/red) |
| Sortino Ratio | 3.12 |
| Win Rate | 55.2% |
| Max Drawdown | 8.1% |
| Profit Factor | 1.84 |
| Total Return | +12.4% |
| Avg Return/Trade | +0.3% |
| Total Trades | 142 |

**Detail view — Loss sub-tab**: Add Sharpe sparkline.

**Action distribution**: Bar chart in Metrics or RL sub-tab.

### fxt train monitor

Add `print_financial_metrics()` section after the session table:
```
Financial (dqn): Sharpe=2.31 WinRate=55.2% MaxDD=8.1% PF=1.84 Return=+12.4%
```

## Files Changed

| File | Change |
|---|---|
| `crates/common/src/metrics/training_metrics.rs` | +11 gauges, +2 convenience fns |
| `crates/ml/src/trainers/dqn/trainer.rs` | Push financial metrics at epoch end |
| `crates/ml/src/trainers/ppo.rs` | Push financial metrics at epoch end |
| `crates/ml/src/training_pipeline.rs` | Add eval step for supervised trainers |
| `bin/fxt/proto/monitoring.proto` | +11 fields + new RPC + messages |
| `services/monitoring_service/proto/monitoring.proto` | Same |
| `services/monitoring_service/src/service.rs` | +11 match arms + epoch history store + new RPC |
| `bin/fxt/src/commands/watch/state.rs` | +11 fields on TrainingSession, +4 on SessionHistory |
| `bin/fxt/src/commands/watch/streams.rs` | Map new proto fields |
| `bin/fxt/src/commands/watch/render.rs` | Financial table + sparklines + list columns |
| `bin/fxt/src/commands/train/monitor.rs` | `print_financial_metrics()` section |
| `bin/fxt/src/client/ml_training_client.rs` | `get_epoch_history()` client method |
