Skip to content

Feat/Expose regime-conditional performance through the validation stack - #33

Open
SivanRP wants to merge 2 commits into
mainfrom
feat/regime-detection
Open

SivanRP wants to merge 2 commits into
mainfrom
feat/regime-detection

Conversation

@SivanRP

@SivanRP SivanRP commented Sep 22, 2026

Copy link
Copy Markdown

What this does

regime_conditional_performance() and RegimeResult came in with the overfitting port in #24 and have never been reachable — exported from validation/domain/__init__.py, called by nothing, no tests, absent from the guides. This wires them through the layers so the "does this strategy only work in calm markets?" check is actually usable.

No new statistics. Riley's numerics are untouched: robustness.py is +26 lines, 0 deletions, and the only addition is a to_metrics() accessor.

Changes

Layer | Change -- | -- validation/domain/statistics/robustness.py | RegimeResult.to_metrics() — lossless export keyed by ValidationMetricKey validation/domain/validation_metric.py | REGIME_COUNTS, WORST_REGIME_NAME, WORKS_IN_ALL_REGIMES so every computed field has a canonical name (R4/V3) validation/application/analyze_regimes.py (new) | AnalyzeRegimes use case — input coercion, parameter validation, alignment and sufficiency guards interfaces/api.py | AlgoSystem.analyze_regimes() interfaces/cli/main.py | algosystem regimes command docs | New sections in VALIDATION_GUIDE, API_GUIDE, CLI_GUIDE, README

Open to Review

The domain statistics degrade quietly, which makes a data shortage read like a finding. Two guards in the use case, both of which I'd want a second opinion on:

Positional alignment. Regimes are assigned by position, so a strategy dated 2020 paired with a benchmark dated 2010 silently classified against the wrong observations. Checking length alone is not enough. _require_matching_dates() now compares the converted returns indices when both inputs carry a DatetimeIndex; undated arrays still fall back to the length check. The CLI restricts strategy and benchmark to the dates they share.

Insufficient data. A bucket under five observations scores 0.0, and a series no longer than vol_window collapses to a single "all" regime. A 30-row CSV therefore printed "every regime Sharpe 0.0, REGIME-DEPENDENT" with exit 0 for a strategy whose true Sharpe was ~93. _require_classifiable() rejects input it cannot describe rather than emitting that. The guard is necessary but not sufficient — percentile thresholds can still leave one bucket thin — which is why regime_counts is exposed in to_metrics(): it distinguishes an empty bucket from a genuine loss.

Design decisions

No facade.py entry point. validation/facade.py exists to compose infrastructure — detect_overfitting builds a PassRunner and resolves strategies from validation.infrastructure.strategies. AnalyzeRegimes composes nothing and takes no constructor dependency, so a facade wrapper would be a pure pass-through.

The CLI calls the use case directly rather than AlgoSystem. AlgoSystem.__init__ eagerly builds a quantstats calculator that regime analysis never uses. validate-strategies and benchmarks already import their dependencies directly, so there is precedent, but the underlying eagerness is a pre-existing issue worth its own ticket (see follow-ups).

Testing

58 new test functions across domain, application, facade and CLI.

The classification test measures realized volatility inside each bucket rather than bucket sizes — terciles are a third each by construction, so asserting sizes passes on IID noise with no regime structure at all. It now separates 8.01x on a regime fixture vs 0.97x on noise.

Local run: 301 passed, 22 failed, 1 error. All 22 failures and the collection error reproduce identically on main — verified in a clean worktree. They are quantstats missing from my environment plus one sandbox parquet_cache failure. No regressions.

black --check, isort --check and the domain purity test pass. I could not run ruff or import-linter locally (not installed), so CI is the authoritative check on the import contracts — please don't merge on my local results alone.

regime_conditional_performance and RegimeResult were ported with the
overfitting statistics but never reachable: exported from the domain
package and called by nothing, with no tests.

Wire them through the layers:

- AnalyzeRegimes use case in validation/application, converting
  EquityCurve/pandas/array input via the existing returns bridge
- AlgoSystem.analyze_regimes() and an `algosystem regimes` CLI command
- RegimeResult.to_metrics(), keyed by ValidationMetricKey

The statistics degrade quietly on thin input: a series no longer than
vol_window collapses to a single "all" regime, and any bucket under five
observations scores 0.0, so a data shortage reads like a finding
("Sharpe 0.0, regime-dependent"). AnalyzeRegimes rejects input it cannot
describe instead. Regimes are assigned positionally, so dated series are
checked for matching indices rather than matching length alone; the CLI
restricts strategy and benchmark to the dates they share.

Add REGIME_COUNTS, WORST_REGIME_NAME and WORKS_IN_ALL_REGIMES to
ValidationMetricKey so to_metrics() is lossless — the counts are what
distinguish an empty bucket from a genuine loss.

The ported numerics are unchanged.
The demo builds two strategies calibrated to the same overall Sharpe and
shows that only the regime breakdown separates them: one holds its edge
across all volatility buckets, the other earns everything while calm and
gives it back once volatility arrives. It also exercises both guardrails
(short series, mismatched dates).

The notes record what was already in the tree before this work, the
design decisions behind the new use case, the defects found in review,
and the known follow-ups.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Public input coercion still leaks raw conversion errors and accepts boolean annualization factors.

Review effort: Lite
Findings: None

What changed in this PR

Exposes regime-conditional performance analysis through the validation domain, application, API, CLI, documentation, and tests.

Changes:

  • Adds AnalyzeRegimes with input validation, date alignment, and sufficiency checks.
  • Exposes regime metrics through public APIs and the regimes CLI command.
  • Adds documentation, examples, fixtures, and comprehensive tests.
File Change
tests/​validation/​domain/​test_robustness_regimes.py Domain regime-statistics tests
tests/​validation/​conftest.py Shared regime fixtures
tests/​validation/​application/​test_analyze_regimes.py Use-case tests
tests/​validation/​application/​test_analyze_regimes_api.py API tests
tests/​test_cli.py CLI coverage
tests/​test_cli_integration.py CLI integration tests
REGIME_DETECTION_HANDOFF.md Implementation handoff notes
README.md Regime CLI usage
examples/​regime_detection_demo.py Regime-analysis example
docs/​VALIDATION_GUIDE.md Validation documentation
docs/​CLI_GUIDE.md CLI documentation
docs/​API_GUIDE.md API documentation
CHANGELOG.md Feature changelog entry
algosystem/​validation/​domain/​validation_metric.py Canonical regime metric keys
algosystem/​validation/​domain/​statistics/​robustness.py Lossless metrics export
algosystem/​validation/​application/​analyze_regimes.py Regime-analysis use case
algosystem/​validation/​application/​__init__.py Application export
algosystem/​validation/​__init__.py Validation exports
algosystem/​interfaces/​cli/​main.py regimes CLI command
algosystem/​interfaces/​api.py Public API method
algosystem/​__init__.py Top-level result export

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants