---
title: "Get Backtest Summary"
method: GET
path: "/api/backtest/summary"
tags: ["backtest"]
---

# Get Backtest Summary

`GET /api/backtest/summary`

## Request

- Base URL: `https://api.equity-rank.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.equity-rank.com/api/backtest/summary`
- Auth: none declared

Return the latest BacktestRunner result.

Public — no auth. The runner self-caches for 6 hours; the response
is built from whatever the runner returns (including the empty-shell
`sufficient_data=False` result when there's < 30 valuation_history rows).

## Response `200`

Successful Response

- AppRoutersBacktestBacktestSummaryResponse: Response model for GET /api/backtest/summary.
  - `schema_version` string
  - `sufficient_data` boolean, required
  - `real_data_since` string, nullable
  - `period_start` string, nullable
  - `period_end` string, nullable
  - `n_rebalances` integer
  - `annualized_metrics_suppressed` boolean
  - `max_drawdown_suppressed` boolean
  - `benchmark_degraded` boolean
  - `price_basis` string, nullable
  - `coverage` CoverageBlock: MDD-01 (2026-10-09): per-step coverage floor and what it removed. A step is measured only when both of its monthly snapshots hold at least `min_snapshot_universe` distinct scored symbols AND at least max(`min_basket_names`, `min_priced_fraction` x basket) of the basket is priced at both ends. Any other step abstains: it is absent from the curves and from every figure (never published as a flat 0.00% month) and is listed in `abstained`. `measured_steps_contiguous` is False when an abstained step sits BETWEEN measured ones, i.e. the curve joins non-adjacent periods.
    - `min_snapshot_universe` integer
    - `min_priced_fraction` number
    - `min_basket_names` integer
    - `steps_attempted` integer
    - `steps_measured` integer
    - `steps_abstained` integer
    - `measured_steps_contiguous` boolean
    - `abstained` AbstainedStep[]
      - `entry_date` string, required
      - `exit_date` string, required
      - `reason` string, nullable
      - `entry_universe` integer
      - `exit_universe` integer
      - `basket_size` integer
      - `priced` integer
  - `equity_curve` EquityCurveBlock: Per-series equity curve segment — normalized to 100.0 at index 0. P0-3 (2026-08-03): `universe` is the equal-weight mean of every scored symbol over the same rebalance dates — the like-for-like comparator. `qqq` is retired (always empty, kept for response-shape compatibility). An empty `spy` with `benchmark_degraded=True` on the response means the external feed failed — never rendered as a flat 0% line.
    - `dates` string[]
    - `strategy` number[]
    - `spy` number[]
    - `qqq` number[]
    - `universe` number[]
  - `strategy` BacktestMetricsBlock: Headline metrics — None when sufficient_data=False. P0-3: cagr / sharpe / alpha_vs_spy are additionally None until n_rebalances >= 24 (annualizing a ~4-month window is statistically meaningless — df=3). MDD-01 (2026-10-09): max_drawdown is gated the same way (a sample maximum is most biased on a short panel); the response's `max_drawdown_suppressed` says so. `alpha_vs_spy` is a CAGR DIFFERENCE, not risk-adjusted alpha; surfaces must label it "CAGR gap vs SPY".
    - `cagr` number, nullable
    - `sharpe` number, nullable
    - `max_drawdown` number, nullable
    - `total_return` number, nullable
    - `alpha_vs_spy` number, nullable
    - `alpha_vs_qqq` number, nullable
  - `spy` BenchmarkBlock: Per-benchmark figures over the SAME measured steps as the strategy. MDD-01e (2026-10-09): `max_drawdown` is published exactly when the strategy's is (n_rebalances >= 24 measured steps) so the strategy drawdown is never shown without its comparator; None otherwise, and None for SPY when the feed degraded.
    - `total_return` number, nullable
    - `max_drawdown` number, nullable
  - `qqq` BenchmarkBlock: Per-benchmark figures over the SAME measured steps as the strategy. MDD-01e (2026-10-09): `max_drawdown` is published exactly when the strategy's is (n_rebalances >= 24 measured steps) so the strategy drawdown is never shown without its comparator; None otherwise, and None for SPY when the feed degraded.
    - `total_return` number, nullable
    - `max_drawdown` number, nullable
  - `universe` BenchmarkBlock: Per-benchmark figures over the SAME measured steps as the strategy. MDD-01e (2026-10-09): `max_drawdown` is published exactly when the strategy's is (n_rebalances >= 24 measured steps) so the strategy drawdown is never shown without its comparator; None otherwise, and None for SPY when the feed degraded.
    - `total_return` number, nullable
    - `max_drawdown` number, nullable
  - `validation` ValidationGateBlock: D3 / VALGATE hard gates (item 37, .planning/PRICE-PREDICTION-TIMING-PLAN-2026-09-03.md). Deflated Sharpe Ratio + Probability of Backtest Overfitting (Bailey & López de Prado 2014 — computed once in app/services/factor_ic_significance.py and never re-derived here) applied to the persisted strategy result. `dsr`/`pbo`/`passes_gates` are all None and `validation_status` stays 'not_evaluated' until the annualized Sharpe itself is populated (fewer than MIN_REBALANCES_FOR_ANNUALIZED periods — nothing to deflate yet). `passes_gates` is true only when dsr >= 0.95 AND, where PBO is applicable, pbo < 0.35; a False or None value here means this result must NOT be read as validated, regardless of how the headline metrics look. `pbo_applicable` (2026-10-08): PBO measures selection among >= 2 candidate configurations. This strategy is one fixed configuration, so `pbo` is null and `pbo_applicable` is false: no overfitting-of-selection test was run, and the gate rests on the DSR alone. It is never published as 0.0. `pbo_n_candidates` is the number of configurations compared. `dsr` IS A PROBABILITY IN [0, 1] AS OF 2026-10-07, NOT A RATIO. It estimates P(true Sharpe > the Sharpe the best of n_trials null configurations would be expected to show by luck), so 0.95 is a one-sided 95% confidence level. The field previously carried a ratio-valued statistic whose documented 1.5 threshold was unreachable at any trial count of 4 or more (DSR-01); it was never non-null on this surface, because the live result has had fewer than MIN_REBALANCES_FOR_ANNUALIZED rebalances throughout, so no published value changed. Both fields stay None rather than 0.0 so "not measurable" can never read as "measured".
    - `dsr` number, nullable
    - `pbo` number, nullable
    - `pbo_applicable` boolean, nullable
    - `pbo_n_candidates` integer, nullable
    - `passes_gates` boolean, nullable
    - `validation_status` string
  - `compliance` BacktestComplianceBlock: Compliance copy surfaced to the frontend (BACK-05). All language locks to "historical model performance" framing and explicitly negates "forecast" / "recommendation" / "guarantee".
    - `framing` string
    - `look_ahead_controls` string
    - `live_history_disclaimer` string
    - `survivorship_bias_note` string

---

[API](https://skmtc.dev/equity-rank/apis/equity-rank-api.md) · [All operations](https://skmtc.dev/equity-rank/apis/equity-rank-api/llms.txt) · [OpenAPI document](https://skmtc.dev/equity-rank/apis/equity-rank-api/revisions/c2f2463fee33?raw)
