sharpebench-core 0.28.0

Deterministic, luck-robust quantitative evaluation kernel for trading agents.
Documentation
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
//! Opt-in Sharpe diagnostics for a scored board, computed beside it and never
//! folded into it.
//!
//! The three estimators are in [`sharpebench_stats::opt_in_diagnostics`]: an
//! autocorrelation-aware PSR, the PSR with its standard error evaluated under
//! the null, and the manipulation-proof performance measure. This module runs
//! the requested ones on the same pooled track the board's PSR and DSR read,
//! so a diagnostic and the board column it shadows differ only by the
//! estimator. A fourth, `expected-shortfall`, shadows no column: it reports
//! the historical expected shortfall of the same track with the count of
//! observations in its tail, and the fraction of observations below zero
//! ([`sharpebench_stats::tail_risk`]), which the board's downside deviation
//! mixes together. A [`SharpeDiagnostics`] is a separate record: it is not a field
//! of [`CompositeScore`], and no gate, eligibility rule or rank predicate
//! reads it. A caller that does not ask for diagnostics gets exactly the board
//! it got before this module existed.

use serde::Serialize;
use sharpebench_stats::{
    first_order_autocorrelation, manipulation_proof_performance,
    probabilistic_sharpe_ratio_autocorrelated, StandardErrorAt, DEFAULT_MPPM_RISK_AVERSION,
};
use sharpebench_stats::{
    historical_expected_shortfall, loss_frequency, tail_size, DEFAULT_MIN_TAIL_OBSERVATIONS,
    DEFAULT_TAIL_LEVEL,
};

use crate::composite::{
    pooled_returns, restrict_to_shared_positions, window_tracks, AgentSubmission, CompositeScore,
    ScoreConfig,
};

/// One opt-in diagnostic, by the identifier the command line accepts.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum SharpeDiagnostic {
    /// `autocorrelated-psr`: the board's PSR and DSR recomputed with the
    /// pooled track's own first-order autocorrelation in the Sharpe variance
    /// (López de Prado, Lipton and Zoonekynd 2026, eqs. 2 and 3, p. 9), the
    /// standard error still at the observed Sharpe. Only the autocorrelation
    /// term differs from the board.
    AutocorrelatedPsr,
    /// `null-se-psr`: the board's PSR and DSR with the standard error evaluated
    /// at the benchmark rather than the observed Sharpe (the same paper, eqs. 4
    /// and 5, p. 10), serial independence kept. Only the evaluation point
    /// differs from the board.
    NullSePsr,
    /// `mppm`: the manipulation-proof performance measure of Goetzmann,
    /// Ingersoll, Spiegel and Welch (2007, eq. 18) at risk aversion 3 and a
    /// zero risk-free rate, annualized at the board's periods per year.
    Mppm,
    /// `expected-shortfall`: the historical expected shortfall of the pooled
    /// track at tail level 0.05, the number of observations in that tail, and
    /// the fraction of observations below zero. Downside deviation mixes how
    /// often a track loses with how much; this reports the two apart. The
    /// shortfall is unavailable, with the reason, when the tail holds fewer
    /// than 10 whole observations.
    ExpectedShortfall,
    /// `between-window-variance`: the share of the pooled track's variance
    /// that lies between its windows rather than within them, `SSB / SST`
    /// from the one-way decomposition `SST = SSB + SSW` over the window
    /// segments the pooled track is concatenated from. Near 1 is the shape of
    /// a track whose dispersion is mostly level differences between windows,
    /// the shape a window-constant track and its near relatives produce;
    /// honest traded tracks sit near 0. Published so that a gate, if one is
    /// ever wanted, can be set from its distribution across honest fields
    /// rather than from a threshold chosen in advance.
    BetweenWindowVariance,
}

impl SharpeDiagnostic {
    /// Every diagnostic, in report order.
    pub const ALL: [SharpeDiagnostic; 5] = [
        SharpeDiagnostic::AutocorrelatedPsr,
        SharpeDiagnostic::NullSePsr,
        SharpeDiagnostic::Mppm,
        SharpeDiagnostic::ExpectedShortfall,
        SharpeDiagnostic::BetweenWindowVariance,
    ];

    /// The command-line identifier.
    pub fn id(self) -> &'static str {
        match self {
            SharpeDiagnostic::AutocorrelatedPsr => "autocorrelated-psr",
            SharpeDiagnostic::NullSePsr => "null-se-psr",
            SharpeDiagnostic::Mppm => "mppm",
            SharpeDiagnostic::ExpectedShortfall => "expected-shortfall",
            SharpeDiagnostic::BetweenWindowVariance => "between-window-variance",
        }
    }

    /// Parse a comma-separated list such as `autocorrelated-psr,mppm`. Order
    /// and repetition do not matter; the result is sorted and deduplicated. An
    /// empty list or an unknown identifier is refused by name, so a typo
    /// cannot silently request nothing.
    pub fn parse_list(list: &str) -> Result<Vec<SharpeDiagnostic>, String> {
        let mut out = Vec::new();
        for raw in list.split(',') {
            let id = raw.trim();
            let found = SharpeDiagnostic::ALL
                .into_iter()
                .find(|d| d.id() == id)
                .ok_or_else(|| {
                    let known: Vec<&str> = SharpeDiagnostic::ALL.iter().map(|d| d.id()).collect();
                    format!(
                        "unknown diagnostic `{id}`; expected a comma-separated list of {}",
                        known.join(", ")
                    )
                })?;
            out.push(found);
        }
        out.sort();
        out.dedup();
        Ok(out)
    }
}

/// A PSR diagnostic against the board's two benchmarks.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct PsrDiagnostic {
    /// The first-order autocorrelation used in the Sharpe variance: the pooled
    /// track's own estimate for `autocorrelated-psr`, 0 for `null-se-psr`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rho: Option<f64>,
    /// Against a zero Sharpe: the counterpart of the board's `psr`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub psr: Option<f64>,
    /// Against the row's `deflation_bar_per_period`: the counterpart of the
    /// board's `deflated_sharpe`. Absent when the row has a `deflation_error`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub psr_at_deflation_bar: Option<f64>,
    /// Why the diagnostic could not be computed. When present, the values it
    /// explains are absent.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// The manipulation-proof performance measure for one agent.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct MppmDiagnostic {
    /// Relative risk aversion, the measure's `rho`.
    pub risk_aversion: f64,
    /// The periods per year the measure is annualized at (`dt = 1 / this`).
    pub periods_per_year: f64,
    /// The annualized certainty-equivalent excess return. Absent on error.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub annualized: Option<f64>,
    /// Why the measure could not be computed.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// The historical expected shortfall and loss frequency of one agent's pooled
/// track, computed by [`sharpebench_stats::tail_risk`].
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct TailRiskDiagnostic {
    /// The tail fraction, 0.05: the worst 5% of the pooled observations.
    pub level: f64,
    /// The fewest whole tail observations a shortfall is reported from, 10.
    pub min_tail_observations: usize,
    /// `T = pooled_observations * level`, the tail size the tail sum is
    /// divided by, fractional when the product is.
    pub tail_size: f64,
    /// The observations that enter the tail with a positive weight,
    /// `ceil(T)`, reported whether or not the shortfall is.
    pub tail_observations: usize,
    /// The expected shortfall: the mean return over the tail, with the
    /// observation that straddles a fractional boundary weighted by the part
    /// of it inside. A return, not a positive loss: -0.04 means the tail lost
    /// 4% a period on average. Absent when `error` is present.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tail_mean_return: Option<f64>,
    /// Pooled observations strictly below zero. Absent for a track with no
    /// observations.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub losses: Option<usize>,
    /// `losses / pooled_observations`, in [0, 1]. Absent with `losses`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub loss_frequency: Option<f64>,
    /// Why `tail_mean_return` is absent, such as a tail with fewer than
    /// `min_tail_observations` whole observations.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// The requested diagnostics for one board row. Diagnostics that were not
/// requested are absent, not null.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct SharpeDiagnostics {
    pub agent_id: String,
    /// Always `false`: recorded so a reader of the JSON alone cannot mistake a
    /// diagnostic for a board column.
    pub used_by_gate: bool,
    /// Observations in the pooled track the diagnostics were computed on, the
    /// same track as the row's `pooled_observations`.
    pub pooled_observations: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub autocorrelated_psr: Option<PsrDiagnostic>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub null_se_psr: Option<PsrDiagnostic>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mppm: Option<MppmDiagnostic>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expected_shortfall: Option<TailRiskDiagnostic>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub between_window_variance: Option<BetweenWindowVarianceDiagnostic>,
}

/// How one agent's pooled variance splits between and within its windows.
///
/// Rank-neutral like every diagnostic here: no gate, eligibility rule or rank
/// predicate reads it, and it refuses nobody.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct BetweenWindowVarianceDiagnostic {
    /// Windows with at least one observation, after the shared-cell
    /// restriction and replicate averaging the board applies.
    pub windows: usize,
    /// `SSB / (SSB + SSW)`, in [0, 1]. `SSB` sums, over windows, the
    /// observations in the window times its mean's squared distance from the
    /// pooled mean; `SSW` sums squared distances from each window's own mean.
    /// Absent when `error` is present.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub between_window_share: Option<f64>,
    /// Why the share could not be computed: no submission for the row, fewer
    /// than two windows, a non-finite observation, or a pooled track with no
    /// variance at all.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// Compute `requested` for every row of `board`, in board order.
///
/// `subs` and `cfg` must be the field and configuration the board was ranked
/// from. The pooled track is rebuilt as [`crate::rank_declared`] builds it: the
/// shared-cell restriction when `cfg.shared_run_set` is on, then
/// [`pooled_returns`] at `cfg.execution_seeds_per_window`. Each row is matched
/// to its submission by `agent_id`, first match, as
/// [`crate::rank_certified`] does. The board is read, never written.
pub fn sharpe_diagnostics(
    subs: &[AgentSubmission],
    board: &[CompositeScore],
    cfg: &ScoreConfig,
    requested: &[SharpeDiagnostic],
) -> Vec<SharpeDiagnostics> {
    let restricted;
    let field: &[AgentSubmission] = if cfg.shared_run_set {
        restricted = restrict_to_shared_positions(subs);
        &restricted
    } else {
        subs
    };
    board
        .iter()
        .map(|row| {
            let submission = field.iter().find(|s| s.agent_id == row.agent_id);
            let pooled = submission
                .map(|s| pooled_returns(s, cfg.execution_seeds_per_window))
                .unwrap_or_default();
            let bar = row
                .deflation_error
                .is_none()
                .then_some(row.deflation_bar_per_period);
            let wants = |d| requested.contains(&d);
            SharpeDiagnostics {
                agent_id: row.agent_id.clone(),
                used_by_gate: false,
                pooled_observations: pooled.len(),
                autocorrelated_psr: wants(SharpeDiagnostic::AutocorrelatedPsr).then(|| {
                    match first_order_autocorrelation(&pooled) {
                        Ok(rho) => psr_pair(&pooled, rho, StandardErrorAt::Observed, bar),
                        Err(e) => PsrDiagnostic {
                            rho: None,
                            psr: None,
                            psr_at_deflation_bar: None,
                            error: Some(e.to_string()),
                        },
                    }
                }),
                null_se_psr: wants(SharpeDiagnostic::NullSePsr)
                    .then(|| psr_pair(&pooled, 0.0, StandardErrorAt::Benchmark, bar)),
                mppm: wants(SharpeDiagnostic::Mppm).then(|| {
                    let measured = manipulation_proof_performance(
                        &pooled,
                        DEFAULT_MPPM_RISK_AVERSION,
                        cfg.periods_per_year,
                    );
                    MppmDiagnostic {
                        risk_aversion: DEFAULT_MPPM_RISK_AVERSION,
                        periods_per_year: cfg.periods_per_year,
                        annualized: measured.as_ref().ok().copied(),
                        error: measured.err().map(|e| e.to_string()),
                    }
                }),
                expected_shortfall: wants(SharpeDiagnostic::ExpectedShortfall)
                    .then(|| tail_risk(&pooled)),
                between_window_variance: wants(SharpeDiagnostic::BetweenWindowVariance).then(
                    || match submission {
                        Some(s) => between_window_variance(&window_tracks(
                            s,
                            cfg.execution_seeds_per_window,
                        )),
                        None => BetweenWindowVarianceDiagnostic {
                            windows: 0,
                            between_window_share: None,
                            error: Some("no submission in the field has this agent_id".into()),
                        },
                    },
                ),
            }
        })
        .collect()
}

fn tail_risk(pooled: &[f64]) -> TailRiskDiagnostic {
    let size = tail_size(pooled.len(), DEFAULT_TAIL_LEVEL).expect("the default level is in (0, 1]");
    let shortfall =
        historical_expected_shortfall(pooled, DEFAULT_TAIL_LEVEL, DEFAULT_MIN_TAIL_OBSERVATIONS);
    let losses = loss_frequency(pooled).ok();
    TailRiskDiagnostic {
        level: DEFAULT_TAIL_LEVEL,
        min_tail_observations: DEFAULT_MIN_TAIL_OBSERVATIONS,
        tail_size: size,
        tail_observations: size.ceil() as usize,
        tail_mean_return: shortfall.as_ref().ok().map(|es| es.tail_mean_return),
        losses: losses.map(|l| l.losses),
        loss_frequency: losses.map(|l| l.frequency),
        error: shortfall
            .err()
            .map(|e| format!("tail at level {DEFAULT_TAIL_LEVEL}: {e}")),
    }
}

/// The one-way split `SST = SSB + SSW` of a pooled track over its windows.
///
/// The share is taken over `SSB + SSW` rather than over a separately computed
/// `SST`: the two are equal in exact arithmetic, and dividing by the sum keeps
/// the share inside [0, 1] under rounding.
fn between_window_variance(windows: &[Vec<f64>]) -> BetweenWindowVarianceDiagnostic {
    let present: Vec<&Vec<f64>> = windows.iter().filter(|w| !w.is_empty()).collect();
    let refuse = |why: &str| BetweenWindowVarianceDiagnostic {
        windows: present.len(),
        between_window_share: None,
        error: Some(why.to_string()),
    };
    if present.len() < 2 {
        return refuse("fewer than two windows: there is no between-window variance to split");
    }
    if present
        .iter()
        .flat_map(|w| w.iter())
        .any(|x| !x.is_finite())
    {
        return refuse("a pooled observation is not finite");
    }
    let count: usize = present.iter().map(|w| w.len()).sum();
    let grand = present.iter().flat_map(|w| w.iter()).sum::<f64>() / count as f64;
    let (mut between, mut within) = (0.0_f64, 0.0_f64);
    for window in &present {
        let mean = window.iter().sum::<f64>() / window.len() as f64;
        between += window.len() as f64 * (mean - grand) * (mean - grand);
        within += window.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>();
    }
    let total = between + within;
    if !total.is_finite() || total <= 0.0 {
        return refuse("the pooled track has no variance to split");
    }
    BetweenWindowVarianceDiagnostic {
        windows: present.len(),
        between_window_share: Some(between / total),
        error: None,
    }
}

fn psr_pair(pooled: &[f64], rho: f64, at: StandardErrorAt, bar: Option<f64>) -> PsrDiagnostic {
    let against_zero = probabilistic_sharpe_ratio_autocorrelated(pooled, 0.0, rho, at);
    let against_bar =
        bar.map(|bar| probabilistic_sharpe_ratio_autocorrelated(pooled, bar, rho, at));
    let error = against_zero
        .as_ref()
        .err()
        .or_else(|| against_bar.as_ref().and_then(|r| r.as_ref().err()))
        .map(ToString::to_string);
    PsrDiagnostic {
        rho: Some(rho),
        psr: against_zero.ok(),
        psr_at_deflation_bar: against_bar.and_then(Result::ok),
        error,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::composite::{rank, Run};
    use crate::process::Trace;

    fn sub(id: &str, returns: Vec<f64>) -> AgentSubmission {
        AgentSubmission {
            agent_id: id.into(),
            runs: vec![Run {
                returns,
                trace: Trace::default(),
                confidences: vec![],
                outcomes: vec![],
                cost: 0.0,
            }],
            in_sample_trials: 0,
            candidates: vec![],
        }
    }

    fn field() -> Vec<AgentSubmission> {
        let wave: Vec<f64> = (0..200)
            .map(|t| {
                let t = t as f64;
                0.001 + 0.01 * (1.3 * t).sin() + 0.004 * (0.21 * t).cos()
            })
            .collect();
        let steady: Vec<f64> = (0..200)
            .map(|i| 0.001 + 0.0002 * ((i % 7) as f64 - 3.0))
            .collect();
        vec![sub("wave", wave), sub("steady", steady)]
    }

    #[test]
    fn parse_list_accepts_known_ids_in_any_order_and_refuses_the_rest() {
        assert_eq!(
            SharpeDiagnostic::parse_list("mppm,autocorrelated-psr,mppm"),
            Ok(vec![
                SharpeDiagnostic::AutocorrelatedPsr,
                SharpeDiagnostic::Mppm
            ])
        );
        assert_eq!(
            SharpeDiagnostic::parse_list(" null-se-psr "),
            Ok(vec![SharpeDiagnostic::NullSePsr])
        );
        assert_eq!(
            SharpeDiagnostic::parse_list("expected-shortfall,mppm"),
            Ok(vec![
                SharpeDiagnostic::Mppm,
                SharpeDiagnostic::ExpectedShortfall
            ])
        );
        for bad in [
            "",
            "psr",
            "mppm,",
            "MPPM",
            "autocorrelated_psr",
            "expected_shortfall",
            "es",
            "cvar",
        ] {
            assert!(SharpeDiagnostic::parse_list(bad).is_err(), "{bad}");
        }
    }

    /// The diagnostics read the board and change nothing on it: ranking the
    /// same field again gives a byte-identical board. Each diagnostic is its
    /// estimator on the row's own pooled track and deflation bar: at rho = 0
    /// with the standard error at the observed Sharpe the same function
    /// reproduces the row's `psr` and `deflated_sharpe` bit for bit, and the
    /// autocorrelated stream's diagnostic departs from its board PSR.
    #[test]
    fn diagnostics_shadow_the_board_columns_without_touching_them() {
        let subs = field();
        let cfg = ScoreConfig::default();
        let board = rank(&subs, &cfg);
        let before = serde_json::to_string(&board).unwrap();
        let diags = sharpe_diagnostics(&subs, &board, &cfg, &SharpeDiagnostic::ALL);
        assert_eq!(serde_json::to_string(&rank(&subs, &cfg)).unwrap(), before);
        assert_eq!(diags.len(), board.len());

        for (row, d) in board.iter().zip(&diags) {
            assert_eq!(d.agent_id, row.agent_id);
            assert!(!d.used_by_gate);
            assert_eq!(d.pooled_observations, row.pooled_observations);
            let pooled =
                pooled_returns(subs.iter().find(|s| s.agent_id == row.agent_id).unwrap(), 1);
            // rho = 0 at the observed Sharpe is the board, bit for bit.
            let iid = |b| {
                probabilistic_sharpe_ratio_autocorrelated(
                    &pooled,
                    b,
                    0.0,
                    StandardErrorAt::Observed,
                )
                .unwrap()
            };
            assert_eq!(iid(0.0).to_bits(), row.psr.to_bits());
            assert_eq!(
                iid(row.deflation_bar_per_period).to_bits(),
                row.deflated_sharpe.to_bits()
            );
            let ac = d.autocorrelated_psr.as_ref().unwrap();
            assert_eq!(ac.error, None);
            let rho = ac.rho.unwrap();
            assert_eq!(rho, first_order_autocorrelation(&pooled).unwrap());
            assert_eq!(
                ac.psr,
                Some(
                    probabilistic_sharpe_ratio_autocorrelated(
                        &pooled,
                        0.0,
                        rho,
                        StandardErrorAt::Observed
                    )
                    .unwrap()
                )
            );
            let null = d.null_se_psr.as_ref().unwrap();
            assert_eq!(null.rho, Some(0.0));
            assert_eq!(
                null.psr_at_deflation_bar,
                Some(
                    probabilistic_sharpe_ratio_autocorrelated(
                        &pooled,
                        row.deflation_bar_per_period,
                        0.0,
                        StandardErrorAt::Benchmark
                    )
                    .unwrap()
                )
            );
            let mppm = d.mppm.as_ref().unwrap();
            assert_eq!(mppm.risk_aversion, 3.0);
            assert_eq!(mppm.periods_per_year, cfg.periods_per_year);
            assert_eq!(
                mppm.annualized,
                Some(manipulation_proof_performance(&pooled, 3.0, cfg.periods_per_year).unwrap())
            );
            // 200 pooled observations at 5% is a tail of exactly ten.
            let es = d.expected_shortfall.as_ref().unwrap();
            let expected = historical_expected_shortfall(&pooled, 0.05, 10).unwrap();
            assert_eq!(es.level, 0.05);
            assert_eq!(es.min_tail_observations, 10);
            assert_eq!(es.tail_size, 10.0);
            assert_eq!(es.tail_observations, 10);
            assert_eq!(es.tail_mean_return, Some(expected.tail_mean_return));
            let losses = loss_frequency(&pooled).unwrap();
            assert_eq!(es.losses, Some(losses.losses));
            assert_eq!(es.loss_frequency, Some(losses.frequency));
            assert_eq!(es.error, None);
        }
        // The autocorrelated stream's diagnostic departs from its board PSR.
        let wave = board.iter().position(|r| r.agent_id == "wave").unwrap();
        let ac = diags[wave].autocorrelated_psr.as_ref().unwrap();
        assert!(ac.rho.unwrap() > 0.3);
        assert!(ac.psr.unwrap() < board[wave].psr);
    }

    #[test]
    fn only_requested_diagnostics_are_present_and_errors_are_typed() {
        let mut subs = field();
        subs.push(sub("flat", vec![0.0; 200]));
        let cfg = ScoreConfig::default();
        let board = rank(&subs, &cfg);
        let diags = sharpe_diagnostics(&subs, &board, &cfg, &[SharpeDiagnostic::Mppm]);
        for d in &diags {
            assert!(d.autocorrelated_psr.is_none());
            assert!(d.null_se_psr.is_none());
            assert!(d.mppm.is_some());
            assert!(d.expected_shortfall.is_none());
            let json = serde_json::to_value(d).unwrap();
            assert!(json.get("autocorrelated_psr").is_none());
            assert!(json.get("null_se_psr").is_none());
            assert!(json.get("expected_shortfall").is_none());
        }
        let all = sharpe_diagnostics(&subs, &board, &cfg, &SharpeDiagnostic::ALL);
        let flat = all.iter().find(|d| d.agent_id == "flat").unwrap();
        let ac = flat.autocorrelated_psr.as_ref().unwrap();
        assert_eq!(ac.rho, None);
        assert_eq!(ac.psr, None);
        assert!(ac.error.as_deref().unwrap().contains("constant"));
        // A flat track has an MPPM of exactly zero and a defined null-SE PSR.
        assert_eq!(flat.mppm.as_ref().unwrap().annualized, Some(0.0));
        assert!(flat.null_se_psr.as_ref().unwrap().psr.is_some());
        // A flat track's tail is its zeros, and none of them is a loss.
        let tail = flat.expected_shortfall.as_ref().unwrap();
        assert_eq!(tail.tail_mean_return, Some(0.0));
        assert_eq!(tail.losses, Some(0));
        assert_eq!(tail.loss_frequency, Some(0.0));
        let only_tail =
            sharpe_diagnostics(&subs, &board, &cfg, &[SharpeDiagnostic::ExpectedShortfall]);
        for d in &only_tail {
            assert!(d.expected_shortfall.is_some());
            assert!(d.mppm.is_none() && d.autocorrelated_psr.is_none() && d.null_se_psr.is_none());
        }
        assert!(sharpe_diagnostics(&subs, &board, &cfg, &[])
            .iter()
            .all(|d| d.autocorrelated_psr.is_none()
                && d.null_se_psr.is_none()
                && d.mppm.is_none()
                && d.expected_shortfall.is_none()));
    }

    fn repeat(pattern: &[f64], times: usize) -> Vec<f64> {
        pattern
            .iter()
            .copied()
            .cycle()
            .take(pattern.len() * times)
            .collect()
    }

    /// Two agents with the same mean and the same board downside deviation,
    /// one losing 0.02 on half its bars, one losing 0.04 on one bar in eight.
    /// The board cannot tell them apart on the downside; the diagnostic
    /// reports twice the shortfall and a quarter of the loss frequency, and
    /// the board bytes do not move.
    #[test]
    fn expected_shortfall_separates_what_the_board_downside_deviation_cannot() {
        let subs = vec![
            sub("often", repeat(&[-0.02, 0.02], 120)),
            sub(
                "rarely",
                repeat(&[-0.04, 0.04, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 30),
            ),
        ];
        let cfg = ScoreConfig::default();
        let board = rank(&subs, &cfg);
        let before = serde_json::to_string(&board).unwrap();
        let row = |id: &str| board.iter().find(|r| r.agent_id == id).unwrap();
        let (often, rarely) = (row("often"), row("rarely"));
        assert_eq!(often.raw_mean_return, 0.0);
        assert_eq!(rarely.raw_mean_return, 0.0);
        assert!((often.downside_deviation - rarely.downside_deviation).abs() <= 1e-15);

        let diags = sharpe_diagnostics(&subs, &board, &cfg, &[SharpeDiagnostic::ExpectedShortfall]);
        assert_eq!(serde_json::to_string(&board).unwrap(), before);
        let tail = |id: &str| {
            diags
                .iter()
                .find(|d| d.agent_id == id)
                .unwrap()
                .expected_shortfall
                .clone()
                .unwrap()
        };
        let (often, rarely) = (tail("often"), tail("rarely"));
        for t in [&often, &rarely] {
            assert_eq!(t.tail_size, 12.0);
            assert_eq!(t.tail_observations, 12);
            assert_eq!(t.error, None);
        }
        assert!((often.tail_mean_return.unwrap() - -0.02).abs() <= 1e-15);
        assert!((rarely.tail_mean_return.unwrap() - -0.04).abs() <= 1e-15);
        assert_eq!((often.losses, often.loss_frequency), (Some(120), Some(0.5)));
        assert_eq!(
            (rarely.losses, rarely.loss_frequency),
            (Some(30), Some(0.125))
        );
    }

    /// Below ten whole tail observations the shortfall is absent with the
    /// reason, the tail count and the loss frequency are still reported, and
    /// the JSON carries no number in its place. 199 observations make a tail
    /// of 9.95: ten observations carry weight, only nine whole ones.
    #[test]
    fn a_short_tail_is_unavailable_and_says_why() {
        for (n, reported) in [(100, false), (199, false), (200, true)] {
            let returns = repeat(&[-0.03, 0.01, 0.02, -0.01], 50)[..n].to_vec();
            let subs = vec![sub("short", returns)];
            let cfg = ScoreConfig::default();
            let board = rank(&subs, &cfg);
            let d = sharpe_diagnostics(&subs, &board, &cfg, &[SharpeDiagnostic::ExpectedShortfall])
                .remove(0);
            let tail = d.expected_shortfall.unwrap();
            assert_eq!(tail.tail_mean_return.is_some(), reported, "{n}");
            assert_eq!(tail.error.is_none(), reported, "{n}");
            assert_eq!(tail.tail_observations, (n as f64 * 0.05).ceil() as usize);
            let losses = (0..n).filter(|i| i % 4 == 0 || i % 4 == 3).count();
            assert_eq!(tail.losses, Some(losses), "{n}");
            let json = serde_json::to_value(&tail).unwrap();
            assert_eq!(json.get("tail_mean_return").is_some(), reported, "{n}");
            if !reported {
                let error = tail.error.unwrap();
                assert!(error.starts_with("tail at level 0.05: "), "{error}");
                assert!(error.contains("at least 10 observations"), "{error}");
                assert!(error.contains(&format!("got {}", n / 20)), "{error}");
            }
        }
        // An agent with no pooled observations has no loss frequency either.
        let subs = vec![sub("empty", vec![])];
        let cfg = ScoreConfig::default();
        let board = rank(&subs, &cfg);
        let tail = sharpe_diagnostics(&subs, &board, &cfg, &[SharpeDiagnostic::ExpectedShortfall])
            .remove(0)
            .expected_shortfall
            .unwrap();
        assert_eq!((tail.tail_size, tail.tail_observations), (0.0, 0));
        assert_eq!((tail.losses, tail.loss_frequency), (None, None));
        assert!(tail.error.is_some());
    }

    /// The track is the board's: with the shared-cell restriction on, an agent
    /// that completed an extra run is scored on the shared cells only, and so
    /// are its diagnostics; with it off, both see every run.
    #[test]
    fn diagnostics_follow_the_shared_cell_restriction() {
        let mut subs = field();
        let extra: Vec<f64> = (0..50).map(|i| 0.002 * ((i % 3) as f64 - 1.0)).collect();
        subs[0].runs.push(Run {
            returns: extra,
            ..Run::default()
        });
        for shared in [true, false] {
            let cfg = ScoreConfig {
                shared_run_set: shared,
                ..ScoreConfig::default()
            };
            let board = rank(&subs, &cfg);
            let diags = sharpe_diagnostics(&subs, &board, &cfg, &SharpeDiagnostic::ALL);
            for (row, d) in board.iter().zip(&diags) {
                assert_eq!(d.pooled_observations, row.pooled_observations, "{shared}");
            }
            let wave = diags.iter().find(|d| d.agent_id == "wave").unwrap();
            assert_eq!(wave.pooled_observations, if shared { 200 } else { 250 });
        }
    }
}