alphaengine 1.1.1__tar.gz → 1.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {alphaengine-1.1.1 → alphaengine-1.1.2}/CHANGELOG.md +11 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/PKG-INFO +1 -1
- {alphaengine-1.1.1 → alphaengine-1.1.2}/SECURITY.md +4 -5
- {alphaengine-1.1.1 → alphaengine-1.1.2}/pyproject.toml +1 -1
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/cli.py +7 -42
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/allocate.py +8 -1
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/backtest.py +2 -2
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/pairs.py +3 -1
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/performance.py +13 -8
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/process.py +8 -1
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/risk.py +8 -10
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/technical.py +3 -1
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/validation.py +29 -11
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/watch.py +3 -2
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_cli.py +16 -11
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_math_audit.py +81 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_project.py +20 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/.github/ISSUE_TEMPLATE/data_shape.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/.github/ISSUE_TEMPLATE/method_request.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/.github/dependabot.yml +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/.github/workflows/ci.yml +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/.github/workflows/codeql.yml +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/.github/workflows/publish.yml +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/.gitignore +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/AGENTS.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/CODE_OF_CONDUCT.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/CONTRIBUTING.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/LICENSE +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/README.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/assets/banner.png +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/assets/data_boundary.png +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/assets/session.png +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/figure_contract.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/free-vs-firm.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/plugins.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/project-format.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/result-format.md +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/workflows/evaluate_process.json +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/workflows/research_week.json +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/docs/workflows/stress_study.json +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/examples/process_stress_monitor.yaml +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/examples/screen_validate_size.yaml +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/scripts/gen_docs.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/__init__.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/__main__.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/_version.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/agent/__init__.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/agent/answer.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/agent/bakeoff.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/agent/brief.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/agent/chart.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/agent/driver.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/agent/sim.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/auth.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/book.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/charts.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/client/__init__.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/client/agent.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/client/executor.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/client/session.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/commands.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/complete.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/connectors/__init__.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/context.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/contracts/__init__.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/contracts/hash_golden.json +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/__init__.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/active.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/bakeoff.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/covariance.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/cross_section.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/factors.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/market.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/options.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/panel.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/portfolio.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/profile.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/rates.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/regime.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/relations.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/screen.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/series_shapes.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/signals.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/stress.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/trace.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/core/walkforward.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/datasets.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/demo.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/demo_book.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/demo_returns.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/demo_signal.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/demo_universe.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/envelope.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/events.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/ideas.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/io.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/ledger.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/loaders.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/memo.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/model.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/pipeline.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/plugins.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/project.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/py.typed +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/repl.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/study/__init__.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/study/report.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/study/schema.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/sweep/__init__.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/sweep/runner.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/sync.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/tui/__init__.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/tui/app.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/tui/dispatch.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/tui/mark.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/tui/stage.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/src/alphaengine/tui/typeface.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_agent.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_answer.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_bakeoff.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_book.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_client.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_commands.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_context.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_coverage_paths.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_events.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_executor_ops.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_goldens.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_io.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_loaders.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_model.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_portfolio.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_process.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_quant_maths.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_relations.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_screen.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_signals.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_sim.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_smoke.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_sweep.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_trace.py +0 -0
- {alphaengine-1.1.1 → alphaengine-1.1.2}/tests/test_tui.py +0 -0
|
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to Semantic Versioning (with computed golden values
|
|
7
7
|
treated as part of the public contract).
|
|
8
8
|
|
|
9
|
+
## [1.1.2] - 2026-10-24
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **Array-like inputs across validation and metrics (`core/validation.py`, `core/performance.py`, `core/risk.py`):** Replaced truthiness checks (`returns or []`, `if equity_curve:`, `if benchmark_returns:`, `portfolio_returns or []`) with `np.asarray` conversion so `deflated_sharpe`, `min_track_record_length`, `combinatorial_oos_stability` / `cpcv_score`, `pbo_cscv`, `performance_report`, and `compute_var_cvar` accept Python `list`, NumPy `ndarray`, and pandas `Series` / `DataFrame` without raising `ValueError: The truth value of an array/Series is ambiguous`.
|
|
13
|
+
- **Complete `_GATED_COMMANDS` and unified 401/403 refusal (`cli.py`):** Added `workflows`, `runs`, `gaps`, and `tonight` to `_GATED_COMMANDS` so every gated command goes through `require_firm_key()`, and replaced the separate `"Not authenticated"` refusal block with `FIRM_KEY_ERROR_MESSAGE`.
|
|
14
|
+
- **Live-monitor trial accounting (`watch.py`):** `ae monitor <run> --live <file>` (`monitor_live`) now records `n_trials=0` (`already_counted_in_ledger=True`) so out-of-sample live drift checks never inflate the project's cumulative trial count `N`.
|
|
15
|
+
- **Clean SLSQP bounds clipping (`core/process.py`, `core/allocate.py`):** Suppressed the benign SciPy SLSQP bounds-clipping `RuntimeWarning` during GARCH(1,1) and risk-parity optimization.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Security policy (`SECURITY.md`):** Replaced personal email contact with [GitHub Private Vulnerability Reporting / Security Advisories](https://github.com/quantOSC/alphaengine/security/advisories/new).
|
|
19
|
+
|
|
9
20
|
## [1.1.1] - 2026-10-24
|
|
10
21
|
|
|
11
22
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: alphaengine
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: Validated research tooling for investment strategies: deflation, overfitting detection, and honest trial counts.
|
|
5
5
|
Project-URL: Homepage, https://qosai.app
|
|
6
6
|
Project-URL: Documentation, https://github.com/quantOSC/alphaengine#readme
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Reporting a vulnerability
|
|
4
4
|
|
|
5
|
-
Report privately through [GitHub Private Vulnerability Reporting / Security Advisories](https://github.com/quantOSC/alphaengine/security/advisories/new)
|
|
6
|
-
|
|
7
|
-
suspected vulnerability.
|
|
5
|
+
Report privately through [GitHub Private Vulnerability Reporting / Security Advisories](https://github.com/quantOSC/alphaengine/security/advisories/new).
|
|
6
|
+
Please don't open a public issue for a suspected vulnerability.
|
|
8
7
|
|
|
9
8
|
We'll acknowledge within **3 working days** and give an assessment within **10**.
|
|
10
9
|
If we accept the report we'll agree a disclosure timeline with you and credit
|
|
@@ -13,8 +12,8 @@ you in the advisory unless you'd rather we didn't.
|
|
|
13
12
|
## What's in scope
|
|
14
13
|
|
|
15
14
|
This repository: the library, its build, and its release pipeline. The hosted
|
|
16
|
-
platform is a separate system
|
|
17
|
-
so.
|
|
15
|
+
platform is a separate system; report issues there through the same private
|
|
16
|
+
vulnerability reporting channel and say so.
|
|
18
17
|
|
|
19
18
|
## How this package is built and shipped
|
|
20
19
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "alphaengine"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.2"
|
|
8
8
|
description = "Validated research tooling for investment strategies: deflation, overfitting detection, and honest trial counts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -3673,21 +3673,9 @@ def boot(url: str, *, project: str | None, data: Any, keyed: bool, session: Any
|
|
|
3673
3673
|
|
|
3674
3674
|
|
|
3675
3675
|
def refused(exc: Any) -> None:
|
|
3676
|
-
"""Report a server refusal
|
|
3676
|
+
"""Report a server refusal; 401/403 emit the canonical firm-key message."""
|
|
3677
|
+
from .auth import FIRM_KEY_ERROR_MESSAGE
|
|
3677
3678
|
|
|
3678
|
-
── WHY THIS IS NOT JUST A RED LINE ────────────────────────────────────────
|
|
3679
|
-
|
|
3680
|
-
"The server refused: Authentication required" is true, useless, and actively
|
|
3681
|
-
misleading to somebody who IS a paying user — they are signed into the
|
|
3682
|
-
portal in a browser two windows away, so the natural reading is that
|
|
3683
|
-
something is broken. It is not: a browser session is not a CLI credential
|
|
3684
|
-
and never will be, because this process has no cookie jar and should not
|
|
3685
|
-
acquire one.
|
|
3686
|
-
|
|
3687
|
-
So a 401 says what is actually wrong, where the key comes from, and offers
|
|
3688
|
-
to take it right here. Every other status keeps the plain message, because
|
|
3689
|
-
the fix for a 404 or a 422 is not a credential.
|
|
3690
|
-
"""
|
|
3691
3679
|
status = getattr(exc, "status", None)
|
|
3692
3680
|
detail = getattr(exc, "detail", str(exc))
|
|
3693
3681
|
|
|
@@ -3695,34 +3683,7 @@ def refused(exc: Any) -> None:
|
|
|
3695
3683
|
say(red(f"The server refused: {detail}"))
|
|
3696
3684
|
return
|
|
3697
3685
|
|
|
3698
|
-
say(
|
|
3699
|
-
say(
|
|
3700
|
-
yellow(" Not authenticated.")
|
|
3701
|
-
+ dim(
|
|
3702
|
-
" This command requires a portal-issued ae_live_ key; "
|
|
3703
|
-
"your browser login does not reach this process."
|
|
3704
|
-
)
|
|
3705
|
-
)
|
|
3706
|
-
say(dim(" A browser session is a cookie; this needs a key. They are different"))
|
|
3707
|
-
say(dim(" credentials on purpose."))
|
|
3708
|
-
say("")
|
|
3709
|
-
say(
|
|
3710
|
-
" " + dim("Need one:") + " a portal-issued ae_live_ key (request access at https://qosai.app/access)"
|
|
3711
|
-
)
|
|
3712
|
-
say(" " + dim("Get one:") + f" the portal {ARROW} Data {ARROW} API keys {ARROW} New key")
|
|
3713
|
-
say(" " + dim("Then:") + " " + bold("login") + dim(f" (or export {_ENV_KEY}=ae_live_...)"))
|
|
3714
|
-
say(
|
|
3715
|
-
" "
|
|
3716
|
-
+ dim("Offline:")
|
|
3717
|
-
+ " "
|
|
3718
|
-
+ bold("alphaengine demo")
|
|
3719
|
-
+ dim(", ")
|
|
3720
|
-
+ bold("alphaengine process")
|
|
3721
|
-
+ dim(", and the Python API (")
|
|
3722
|
-
+ bold("import alphaengine")
|
|
3723
|
-
+ dim(") work without an account.")
|
|
3724
|
-
)
|
|
3725
|
-
say("")
|
|
3686
|
+
say(red(FIRM_KEY_ERROR_MESSAGE))
|
|
3726
3687
|
|
|
3727
3688
|
|
|
3728
3689
|
def offer_key(which: str = "quantos") -> bool:
|
|
@@ -5603,6 +5564,10 @@ _GATED_COMMANDS: frozenset[str] = frozenset(
|
|
|
5603
5564
|
{
|
|
5604
5565
|
"run",
|
|
5605
5566
|
*QUESTION_VERBS.keys(),
|
|
5567
|
+
"workflows",
|
|
5568
|
+
"runs",
|
|
5569
|
+
"gaps",
|
|
5570
|
+
"tonight",
|
|
5606
5571
|
"init",
|
|
5607
5572
|
"log",
|
|
5608
5573
|
"show",
|
|
@@ -13,6 +13,7 @@ test, not a hope.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import math
|
|
16
|
+
import warnings
|
|
16
17
|
from typing import Any
|
|
17
18
|
|
|
18
19
|
import numpy as np
|
|
@@ -135,7 +136,13 @@ def risk_parity_weights(cov: np.ndarray, *, names: list[str] | None = None) -> d
|
|
|
135
136
|
|
|
136
137
|
bounds = [(0.0, 1.0)] * n
|
|
137
138
|
cons = {"type": "eq", "fun": lambda w: float(np.sum(w) - 1.0)}
|
|
138
|
-
|
|
139
|
+
with warnings.catch_warnings():
|
|
140
|
+
warnings.filterwarnings(
|
|
141
|
+
"ignore",
|
|
142
|
+
message="Values in x were outside bounds during a minimize step",
|
|
143
|
+
category=RuntimeWarning,
|
|
144
|
+
)
|
|
145
|
+
result = minimize(objective, w0, method="SLSQP", bounds=bounds, constraints=cons, tol=1e-12)
|
|
139
146
|
raw = np.clip(result.x if result.success else w0, 0.0, None)
|
|
140
147
|
raw = raw / raw.sum()
|
|
141
148
|
weights = _rounded_weights(raw, labels)
|
|
@@ -399,9 +399,9 @@ def _ar1_effective_n(returns: list) -> tuple:
|
|
|
399
399
|
t-stat; this discounts it. Conservative convention: capped at n, so NEGATIVE
|
|
400
400
|
autocorrelation is not credited (never claims more significance than the raw
|
|
401
401
|
sample). Returns (n_eff, r1)."""
|
|
402
|
-
|
|
402
|
+
from .validation import _as_1d_float
|
|
403
403
|
|
|
404
|
-
arr =
|
|
404
|
+
arr = _as_1d_float(returns)
|
|
405
405
|
n = arr.size
|
|
406
406
|
if n < 3:
|
|
407
407
|
return (float(n), 0.0)
|
|
@@ -113,7 +113,9 @@ def _series_to_list(s) -> list[float]:
|
|
|
113
113
|
"""A series is either a list of closes or a {date: close} dict (sorted by date)."""
|
|
114
114
|
if isinstance(s, dict):
|
|
115
115
|
return [float(s[k]) for k in sorted(s.keys())]
|
|
116
|
-
|
|
116
|
+
if s is None:
|
|
117
|
+
return []
|
|
118
|
+
return [float(x) for x in np.asarray(s).ravel() if x is not None]
|
|
117
119
|
|
|
118
120
|
|
|
119
121
|
def _align_pair(a, b) -> tuple[list[float], list[float]]:
|
|
@@ -15,11 +15,15 @@ both decimal and percent; the sign convention is documented per field.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import math
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from typing import Any
|
|
18
20
|
|
|
19
21
|
import numpy as np
|
|
22
|
+
from numpy.typing import ArrayLike
|
|
20
23
|
|
|
21
24
|
from .rates import cash_rate, rate_fields
|
|
22
25
|
from .trace import record
|
|
26
|
+
from .validation import _as_1d_float
|
|
23
27
|
|
|
24
28
|
_PPY = 252 # trading periods per year
|
|
25
29
|
|
|
@@ -45,20 +49,20 @@ def wealth_from_returns(returns: np.ndarray) -> np.ndarray:
|
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
def performance_report(
|
|
48
|
-
returns:
|
|
52
|
+
returns: ArrayLike | Sequence[float],
|
|
49
53
|
*,
|
|
50
|
-
equity_curve:
|
|
51
|
-
benchmark_returns:
|
|
54
|
+
equity_curve: ArrayLike | Sequence[float] | None = None,
|
|
55
|
+
benchmark_returns: ArrayLike | Sequence[float] | None = None,
|
|
52
56
|
risk_free_rate: float | None = None,
|
|
53
57
|
periods_per_year: int = _PPY,
|
|
54
|
-
) -> dict:
|
|
58
|
+
) -> dict[str, Any]:
|
|
55
59
|
"""Risk-adjusted performance metrics for a daily return series (decimals).
|
|
56
60
|
|
|
57
61
|
`risk_free_rate` is ANNUAL. Omitted, it is the pinned H.15 3-month
|
|
58
62
|
Treasury (see ``core.rates``). The engine does not fetch a rate. Loss
|
|
59
63
|
metrics are positive magnitudes.
|
|
60
64
|
"""
|
|
61
|
-
arr =
|
|
65
|
+
arr = _as_1d_float(returns)
|
|
62
66
|
n = arr.size
|
|
63
67
|
if n < 30:
|
|
64
68
|
return {"error": "need >= 30 observations", "n_obs": int(n)}
|
|
@@ -83,7 +87,8 @@ def performance_report(
|
|
|
83
87
|
# Drawdown reads a marked account when one was supplied. Otherwise the
|
|
84
88
|
# path is built from unit capital. Total return always comes from the
|
|
85
89
|
# return product: a dollar curve's last mark minus one is not a return.
|
|
86
|
-
|
|
90
|
+
eq_arr = _as_1d_float(equity_curve)
|
|
91
|
+
eq = eq_arr if eq_arr.size > 0 else wealth_from_returns(arr)
|
|
87
92
|
peak = np.maximum.accumulate(eq)
|
|
88
93
|
drawdown = eq / np.maximum(peak, 1e-18) - 1.0
|
|
89
94
|
max_dd = float(drawdown.min()) if drawdown.size else 0.0
|
|
@@ -115,8 +120,8 @@ def performance_report(
|
|
|
115
120
|
"alpha_annualization": "arithmetic",
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
b = _as_1d_float(benchmark_returns)
|
|
124
|
+
if b.size > 0:
|
|
120
125
|
m = min(len(b), n)
|
|
121
126
|
if m >= 30:
|
|
122
127
|
a2, b2 = arr[-m:] - rf_per, b[-m:] - rf_per
|
|
@@ -17,6 +17,7 @@ TRIAL COUNT HONESTY. A Monte Carlo stress records `n_trials = n_paths` with
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import math
|
|
20
|
+
import warnings
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
import numpy as np
|
|
@@ -330,7 +331,13 @@ def garch_calibrate(values: Any) -> dict[str, Any]:
|
|
|
330
331
|
x0 = np.array([var0 * 0.05, 0.05, 0.90], dtype=float)
|
|
331
332
|
bounds = [(1e-18, None), (0.0, 0.999), (0.0, 0.999)]
|
|
332
333
|
cons = {"type": "ineq", "fun": lambda p: 0.998 - p[1] - p[2]}
|
|
333
|
-
|
|
334
|
+
with warnings.catch_warnings():
|
|
335
|
+
warnings.filterwarnings(
|
|
336
|
+
"ignore",
|
|
337
|
+
message="Values in x were outside bounds during a minimize step",
|
|
338
|
+
category=RuntimeWarning,
|
|
339
|
+
)
|
|
340
|
+
result = minimize(nll, x0, method="SLSQP", bounds=bounds, constraints=cons, tol=1e-12)
|
|
334
341
|
omega, alpha, beta = result.x if result.success else x0
|
|
335
342
|
omega, alpha, beta = float(omega), float(alpha), float(beta)
|
|
336
343
|
persist = alpha + beta
|
|
@@ -17,11 +17,15 @@ Pure numpy/scipy. Deterministic given inputs on the pinned stack.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import math
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
from typing import Any
|
|
20
22
|
|
|
21
23
|
import numpy as np
|
|
24
|
+
from numpy.typing import ArrayLike
|
|
22
25
|
from scipy import stats
|
|
23
26
|
|
|
24
27
|
from .trace import record
|
|
28
|
+
from .validation import _as_1d_float
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
def _clean(val):
|
|
@@ -48,26 +52,20 @@ def _quantile_loss(point: float, mean: float, horizon: int) -> float:
|
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
def compute_var_cvar(
|
|
51
|
-
portfolio_returns:
|
|
55
|
+
portfolio_returns: ArrayLike | Sequence[float],
|
|
52
56
|
*,
|
|
53
57
|
confidence: float = 0.95,
|
|
54
58
|
horizon_days: int = 1,
|
|
55
59
|
portfolio_value: float = 100_000.0,
|
|
56
60
|
bootstrap_samples: int = 1000,
|
|
57
|
-
) -> dict:
|
|
61
|
+
) -> dict[str, Any]:
|
|
58
62
|
"""Portfolio VaR (parametric + Cornish-Fisher + historical) and CVaR.
|
|
59
63
|
|
|
60
64
|
`portfolio_returns`: historical daily portfolio returns as decimals. VaR is
|
|
61
65
|
reported as a positive loss fraction (and dollars at `portfolio_value`).
|
|
62
66
|
"""
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
r
|
|
66
|
-
for r in (portfolio_returns or [])
|
|
67
|
-
if r is not None and not (isinstance(r, float) and np.isnan(r))
|
|
68
|
-
],
|
|
69
|
-
dtype=float,
|
|
70
|
-
)
|
|
67
|
+
raw = _as_1d_float(portfolio_returns)
|
|
68
|
+
arr = raw[~np.isnan(raw)]
|
|
71
69
|
n_obs = arr.size
|
|
72
70
|
if n_obs < 20:
|
|
73
71
|
return {"error": "need >= 20 observations", "n_obs": int(n_obs)}
|
|
@@ -54,7 +54,9 @@ def _extract(series: Any) -> tuple[np.ndarray, np.ndarray | None, np.ndarray | N
|
|
|
54
54
|
lows = np.asarray([float(r["low"]) for r in rows], dtype=float)
|
|
55
55
|
return closes, highs, lows
|
|
56
56
|
return closes, None, None
|
|
57
|
-
|
|
57
|
+
from .validation import _as_1d_float
|
|
58
|
+
|
|
59
|
+
closes = _as_1d_float(series)
|
|
58
60
|
return closes, None, None
|
|
59
61
|
|
|
60
62
|
|
|
@@ -17,8 +17,11 @@ from __future__ import annotations
|
|
|
17
17
|
import itertools
|
|
18
18
|
import math
|
|
19
19
|
import warnings
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
from typing import Any
|
|
20
22
|
|
|
21
23
|
import numpy as np
|
|
24
|
+
from numpy.typing import ArrayLike
|
|
22
25
|
from scipy import stats
|
|
23
26
|
|
|
24
27
|
from .rates import cash_rate, rate_fields
|
|
@@ -27,6 +30,19 @@ from .trace import record
|
|
|
27
30
|
_EULER = 0.5772156649015329
|
|
28
31
|
|
|
29
32
|
|
|
33
|
+
def _as_1d_float(values: ArrayLike | Sequence[float] | None) -> np.ndarray:
|
|
34
|
+
"""Convert any 1-D array-like (list, ndarray, pandas Series) to a 1-D float ndarray, dropping None."""
|
|
35
|
+
if values is None:
|
|
36
|
+
return np.empty(0, dtype=float)
|
|
37
|
+
raw = np.asarray(values)
|
|
38
|
+
if raw.ndim == 0:
|
|
39
|
+
item = raw.item()
|
|
40
|
+
return np.empty(0, dtype=float) if item is None else np.asarray([float(item)], dtype=float)
|
|
41
|
+
if raw.dtype.kind in ("f", "i", "u"):
|
|
42
|
+
return raw.astype(float, copy=False).ravel()
|
|
43
|
+
return np.asarray([float(r) for r in raw.ravel() if r is not None], dtype=float)
|
|
44
|
+
|
|
45
|
+
|
|
30
46
|
# ── Sharpe statistics ───────────────────────────────────────────────────
|
|
31
47
|
|
|
32
48
|
|
|
@@ -78,13 +94,13 @@ def expected_max_sharpe(n_trials: int, trials_sharpe_std: float) -> float:
|
|
|
78
94
|
|
|
79
95
|
|
|
80
96
|
def deflated_sharpe(
|
|
81
|
-
returns:
|
|
97
|
+
returns: ArrayLike | Sequence[float],
|
|
82
98
|
*,
|
|
83
99
|
n_trials: int,
|
|
84
100
|
trials_sharpe_std: float | None = None,
|
|
85
101
|
risk_free_rate: float | None = None,
|
|
86
102
|
periods_per_year: int = 252,
|
|
87
|
-
) -> dict:
|
|
103
|
+
) -> dict[str, Any]:
|
|
88
104
|
"""Deflated Sharpe Ratio for a return stream.
|
|
89
105
|
|
|
90
106
|
DSR = PSR(SR0), where SR0 = expected max Sharpe across `n_trials`. A DSR
|
|
@@ -93,7 +109,7 @@ def deflated_sharpe(
|
|
|
93
109
|
caller's hypothesis ledger; when absent we estimate it from the result's
|
|
94
110
|
own sampling error (a conservative lower bound on selection variance).
|
|
95
111
|
"""
|
|
96
|
-
arr =
|
|
112
|
+
arr = _as_1d_float(returns)
|
|
97
113
|
n = arr.size
|
|
98
114
|
if n < 8:
|
|
99
115
|
return {"error": "need >= 8 observations", "n_obs": int(n)}
|
|
@@ -170,13 +186,13 @@ def deflated_sharpe(
|
|
|
170
186
|
|
|
171
187
|
|
|
172
188
|
def min_track_record_length(
|
|
173
|
-
returns:
|
|
189
|
+
returns: ArrayLike | Sequence[float],
|
|
174
190
|
*,
|
|
175
191
|
sr_benchmark: float = 0.0,
|
|
176
192
|
confidence: float = 0.95,
|
|
177
193
|
risk_free_rate: float | None = None,
|
|
178
194
|
periods_per_year: int = 252,
|
|
179
|
-
) -> dict:
|
|
195
|
+
) -> dict[str, Any]:
|
|
180
196
|
"""Minimum Track Record Length (Bailey & López de Prado, 2012): the number of
|
|
181
197
|
observations needed for the observed Sharpe to be statistically greater than
|
|
182
198
|
`sr_benchmark` at `confidence`. Answers "is this backtest long enough to trust,
|
|
@@ -185,7 +201,7 @@ def min_track_record_length(
|
|
|
185
201
|
MinTRL = 1 + [1 - γ3·SR + ((γ4-1)/4)·SR²] · (Z_α / (SR - SR*))² (per-period).
|
|
186
202
|
Returns the required count + whether the actual sample already clears it.
|
|
187
203
|
"""
|
|
188
|
-
arr =
|
|
204
|
+
arr = _as_1d_float(returns)
|
|
189
205
|
n = arr.size
|
|
190
206
|
if n < 8:
|
|
191
207
|
return {"error": "need >= 8 observations", "n_obs": int(n)}
|
|
@@ -257,6 +273,8 @@ def pbo_cscv(pnl_matrix, n_splits: int = 10, max_combos: int = 2000) -> dict:
|
|
|
257
273
|
f"n_splits must be an even integer >= 2 (got {n_splits!r}); "
|
|
258
274
|
"CSCV partitions blocks into equal in-sample and out-of-sample halves."
|
|
259
275
|
)
|
|
276
|
+
if pnl_matrix is None:
|
|
277
|
+
return {"error": "need (T>=2S observations, N>=2 configs)", "shape": [0]}
|
|
260
278
|
M = np.asarray(pnl_matrix, dtype=float)
|
|
261
279
|
if M.ndim != 2 or M.shape[1] < 2 or M.shape[0] < n_splits * 2:
|
|
262
280
|
return {"error": "need (T>=2S observations, N>=2 configs)", "shape": list(M.shape)}
|
|
@@ -313,7 +331,7 @@ def pbo_cscv(pnl_matrix, n_splits: int = 10, max_combos: int = 2000) -> dict:
|
|
|
313
331
|
|
|
314
332
|
|
|
315
333
|
def combinatorial_oos_stability(
|
|
316
|
-
returns:
|
|
334
|
+
returns: ArrayLike | Sequence[float],
|
|
317
335
|
*,
|
|
318
336
|
n_groups: int = 8,
|
|
319
337
|
n_test_groups: int = 2,
|
|
@@ -323,7 +341,7 @@ def combinatorial_oos_stability(
|
|
|
323
341
|
max_paths: int = 2000,
|
|
324
342
|
risk_free_rate: float | None = None,
|
|
325
343
|
periods_per_year: int = 252,
|
|
326
|
-
) -> dict:
|
|
344
|
+
) -> dict[str, Any]:
|
|
327
345
|
"""Combinatorial out-of-sample stability over a single pre-computed return stream.
|
|
328
346
|
|
|
329
347
|
WHAT THIS MEASURES (AND HOW IT DIFFERS FROM FULL CPCV):
|
|
@@ -342,7 +360,7 @@ def combinatorial_oos_stability(
|
|
|
342
360
|
deflated Sharpe (haircut by `n_trials`) across all held-out sub-sample
|
|
343
361
|
combinations.
|
|
344
362
|
"""
|
|
345
|
-
arr =
|
|
363
|
+
arr = _as_1d_float(returns)
|
|
346
364
|
n = arr.size
|
|
347
365
|
if n < 8:
|
|
348
366
|
return {"error": "need >= 8 observations", "n_obs": int(n)}
|
|
@@ -465,7 +483,7 @@ def combinatorial_oos_stability(
|
|
|
465
483
|
|
|
466
484
|
|
|
467
485
|
def cpcv_score(
|
|
468
|
-
returns:
|
|
486
|
+
returns: ArrayLike | Sequence[float],
|
|
469
487
|
*,
|
|
470
488
|
n_groups: int = 8,
|
|
471
489
|
n_test_groups: int = 2,
|
|
@@ -475,7 +493,7 @@ def cpcv_score(
|
|
|
475
493
|
max_paths: int = 2000,
|
|
476
494
|
risk_free_rate: float | None = None,
|
|
477
495
|
periods_per_year: int = 252,
|
|
478
|
-
) -> dict:
|
|
496
|
+
) -> dict[str, Any]:
|
|
479
497
|
"""Deprecated alias for :func:`combinatorial_oos_stability`.
|
|
480
498
|
|
|
481
499
|
Measures combinatorial out-of-sample sub-sample stability on a single return
|
|
@@ -248,12 +248,13 @@ def monitor_live_drift(
|
|
|
248
248
|
command="monitor_live",
|
|
249
249
|
params={"reference_run_id": run_rec.get("run_id"), "live_file": str(lp.name)},
|
|
250
250
|
dataset_hashes=hash_dataset_any(live_list),
|
|
251
|
-
n_trials=
|
|
251
|
+
n_trials=0,
|
|
252
252
|
n_trials_source="monitor_live",
|
|
253
253
|
figures=figures,
|
|
254
254
|
verdict=status,
|
|
255
255
|
idea_id=run_rec.get("idea_id"),
|
|
256
256
|
family=str(run_rec.get("family") or "monitor"),
|
|
257
|
+
already_counted_in_ledger=True,
|
|
257
258
|
)
|
|
258
259
|
return build_envelope(
|
|
259
260
|
command="monitor_live",
|
|
@@ -265,7 +266,7 @@ def monitor_live_drift(
|
|
|
265
266
|
family=str(rec["family"]),
|
|
266
267
|
params_hash=str(rec["params_hash"]),
|
|
267
268
|
dataset_hashes=rec["dataset_hashes"],
|
|
268
|
-
n_trials=
|
|
269
|
+
n_trials=int(rec["cumulative_trials"]),
|
|
269
270
|
n_trials_source="monitor_live",
|
|
270
271
|
cumulative_trials=int(rec["cumulative_trials"]),
|
|
271
272
|
project_trials=int(rec["project_trials"]),
|
|
@@ -238,7 +238,16 @@ def test_the_glyphs_are_the_real_ones_when_the_stream_can_take_them():
|
|
|
238
238
|
def test_offline_names_the_host_and_the_half_that_still_works():
|
|
239
239
|
"""A workflow server being absent is not an error state for this package."""
|
|
240
240
|
out = subprocess.run(
|
|
241
|
-
[
|
|
241
|
+
[
|
|
242
|
+
sys.executable,
|
|
243
|
+
"-m",
|
|
244
|
+
"alphaengine.cli",
|
|
245
|
+
"workflows",
|
|
246
|
+
"--url",
|
|
247
|
+
"http://127.0.0.1:9",
|
|
248
|
+
"--key",
|
|
249
|
+
"ae_live_test",
|
|
250
|
+
],
|
|
242
251
|
capture_output=True,
|
|
243
252
|
text=True,
|
|
244
253
|
)
|
|
@@ -578,24 +587,20 @@ class _Refusal(RuntimeError):
|
|
|
578
587
|
|
|
579
588
|
|
|
580
589
|
def test_a_401_explains_that_a_browser_login_is_not_a_cli_credential(capsys, monkeypatch):
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
process has no cookie jar and should never acquire one."""
|
|
590
|
+
from alphaengine.auth import FIRM_KEY_ERROR_MESSAGE
|
|
591
|
+
|
|
584
592
|
monkeypatch.setattr(cli, "_tty", lambda: True)
|
|
585
593
|
cli.refused(_Refusal(401, "Authentication required"))
|
|
586
594
|
out = capsys.readouterr().out
|
|
587
|
-
assert
|
|
588
|
-
assert "ae_live_" in out
|
|
589
|
-
assert "https://qosai.app/access" in out
|
|
590
|
-
assert "Data" in out and "API keys" in out, "it must say where the key comes from"
|
|
591
|
-
assert "Then:" in out and "QUANTOS_API_KEY" in out, "and how to enter one"
|
|
592
|
-
assert "alphaengine demo" in out and "alphaengine process" in out and "import alphaengine" in out
|
|
595
|
+
assert FIRM_KEY_ERROR_MESSAGE in out
|
|
593
596
|
|
|
594
597
|
|
|
595
598
|
def test_a_403_is_treated_the_same_way(capsys, monkeypatch):
|
|
599
|
+
from alphaengine.auth import FIRM_KEY_ERROR_MESSAGE
|
|
600
|
+
|
|
596
601
|
monkeypatch.setattr(cli, "_tty", lambda: True)
|
|
597
602
|
cli.refused(_Refusal(403, "Forbidden"))
|
|
598
|
-
assert
|
|
603
|
+
assert FIRM_KEY_ERROR_MESSAGE in capsys.readouterr().out
|
|
599
604
|
|
|
600
605
|
|
|
601
606
|
def test_a_non_auth_refusal_keeps_the_plain_message(capsys, monkeypatch):
|
|
@@ -349,3 +349,84 @@ def test_the_pair_screen_keeps_only_the_fdr_survivors() -> None:
|
|
|
349
349
|
# One pair is the raw test.
|
|
350
350
|
assert _benjamini_hochberg([0.04], 0.05) == [True]
|
|
351
351
|
assert _benjamini_hochberg([0.06], 0.05) == [False]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_array_like_list_ndarray_and_series_across_validation_and_metrics() -> None:
|
|
355
|
+
"""deflated_sharpe, min_track_record_length, combinatorial_oos_stability,
|
|
356
|
+
cpcv_score, pbo_cscv, performance_report, and compute_var_cvar accept
|
|
357
|
+
Python lists, numpy ndarrays, and pandas Series (whose __bool__ raises
|
|
358
|
+
ValueError) and return identical figures.
|
|
359
|
+
"""
|
|
360
|
+
from alphaengine.core import min_track_record_length
|
|
361
|
+
|
|
362
|
+
class _FakeSeries:
|
|
363
|
+
"""Mimics pandas.Series: array-like via __array__, raises ValueError on bool()."""
|
|
364
|
+
|
|
365
|
+
def __init__(self, data: list[float]) -> None:
|
|
366
|
+
self._arr = np.asarray(data, dtype=float)
|
|
367
|
+
|
|
368
|
+
def __bool__(self) -> bool:
|
|
369
|
+
raise ValueError("The truth value of a Series is ambiguous.")
|
|
370
|
+
|
|
371
|
+
def __len__(self) -> int:
|
|
372
|
+
return int(self._arr.size)
|
|
373
|
+
|
|
374
|
+
def __iter__(self):
|
|
375
|
+
return iter(self._arr)
|
|
376
|
+
|
|
377
|
+
def __array__(self, dtype=None):
|
|
378
|
+
return np.asarray(self._arr, dtype=dtype)
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
import pandas as pd
|
|
382
|
+
|
|
383
|
+
series_cls = pd.Series
|
|
384
|
+
df_cls = pd.DataFrame
|
|
385
|
+
except ImportError:
|
|
386
|
+
series_cls = _FakeSeries
|
|
387
|
+
df_cls = None
|
|
388
|
+
|
|
389
|
+
rng = np.random.default_rng(17)
|
|
390
|
+
as_list = rng.normal(0.0008, 0.012, 120).tolist()
|
|
391
|
+
as_ndarray = np.asarray(as_list, dtype=float)
|
|
392
|
+
as_series = series_cls(as_list)
|
|
393
|
+
as_fake_series = _FakeSeries(as_list)
|
|
394
|
+
|
|
395
|
+
bench_list = rng.normal(0.0004, 0.01, 120).tolist()
|
|
396
|
+
bench_ndarray = np.asarray(bench_list, dtype=float)
|
|
397
|
+
bench_series = series_cls(bench_list)
|
|
398
|
+
|
|
399
|
+
# 1. deflated_sharpe & min_track_record_length
|
|
400
|
+
dsr_list = deflated_sharpe(as_list, n_trials=9)
|
|
401
|
+
mart_list = min_track_record_length(as_list)
|
|
402
|
+
for candidate in (as_ndarray, as_series, as_fake_series):
|
|
403
|
+
assert deflated_sharpe(candidate, n_trials=9) == dsr_list
|
|
404
|
+
assert min_track_record_length(candidate) == mart_list
|
|
405
|
+
|
|
406
|
+
# 2. combinatorial_oos_stability & cpcv_score
|
|
407
|
+
stab_list = combinatorial_oos_stability(as_list, n_groups=6, n_test_groups=2, n_trials=9)
|
|
408
|
+
for candidate in (as_ndarray, as_series, as_fake_series):
|
|
409
|
+
assert combinatorial_oos_stability(candidate, n_groups=6, n_test_groups=2, n_trials=9) == stab_list
|
|
410
|
+
with pytest.deprecated_call(match="combinatorial_oos_stability"):
|
|
411
|
+
assert cpcv_score(candidate, n_groups=6, n_test_groups=2, n_trials=9) == stab_list
|
|
412
|
+
|
|
413
|
+
# 3. pbo_cscv (2-D list, 2-D ndarray, list of Series, and DataFrame if pandas present)
|
|
414
|
+
mat_list = rng.normal(0.0005, 0.01, size=(80, 6)).tolist()
|
|
415
|
+
mat_ndarray = np.asarray(mat_list, dtype=float)
|
|
416
|
+
mat_series_rows = [series_cls(row) for row in mat_list]
|
|
417
|
+
pbo_expected = pbo_cscv(mat_list, n_splits=8)
|
|
418
|
+
assert pbo_cscv(mat_ndarray, n_splits=8) == pbo_expected
|
|
419
|
+
assert pbo_cscv(mat_series_rows, n_splits=8) == pbo_expected
|
|
420
|
+
if df_cls is not None:
|
|
421
|
+
assert pbo_cscv(df_cls(mat_list), n_splits=8) == pbo_expected
|
|
422
|
+
|
|
423
|
+
# 4. performance_report & compute_var_cvar
|
|
424
|
+
perf_list = performance_report(as_list, benchmark_returns=bench_list)
|
|
425
|
+
risk_list = compute_var_cvar(as_list)
|
|
426
|
+
for candidate, bench in (
|
|
427
|
+
(as_ndarray, bench_ndarray),
|
|
428
|
+
(as_series, bench_series),
|
|
429
|
+
(as_fake_series, _FakeSeries(bench_list)),
|
|
430
|
+
):
|
|
431
|
+
assert performance_report(candidate, benchmark_returns=bench) == perf_list
|
|
432
|
+
assert compute_var_cvar(candidate) == risk_list
|
|
@@ -935,6 +935,8 @@ def test_watch_pipeline_reruns_on_dataset_change_and_monitor_live_detects_drift(
|
|
|
935
935
|
assert cli.main(["validate", "--data", str(bt_csv), "--input", "n_trials=3", "--json"]) == 0
|
|
936
936
|
bt_env = json.loads(capsys.readouterr().out)
|
|
937
937
|
bt_run_id = bt_env["run_id"]
|
|
938
|
+
bt_cum_n = bt_env["cumulative_trials"]
|
|
939
|
+
bt_proj_n = bt_env["project_trials"]
|
|
938
940
|
|
|
939
941
|
# Calm live returns within the expected range -> `within_expected_range`
|
|
940
942
|
calm_live = tmp_path / "live_calm.csv"
|
|
@@ -946,6 +948,15 @@ def test_watch_pipeline_reruns_on_dataset_change_and_monitor_live_detects_drift(
|
|
|
946
948
|
calm_env = json.loads(capsys.readouterr().out)
|
|
947
949
|
assert calm_env["verdict"] == "within_expected_range"
|
|
948
950
|
assert calm_env["figures"]["drift_flags"] == []
|
|
951
|
+
assert calm_env["figures"]["run_n_trials"] == 0
|
|
952
|
+
assert calm_env["cumulative_trials"] == bt_cum_n
|
|
953
|
+
assert calm_env["project_trials"] == bt_proj_n
|
|
954
|
+
|
|
955
|
+
# Re-running live monitor on same or updated live file never inflates cumulative N
|
|
956
|
+
assert cli.main(["monitor", bt_run_id, "--live", str(calm_live), "--json"]) == 0
|
|
957
|
+
calm_env_2 = json.loads(capsys.readouterr().out)
|
|
958
|
+
assert calm_env_2["cumulative_trials"] == bt_cum_n
|
|
959
|
+
assert calm_env_2["project_trials"] == bt_proj_n
|
|
949
960
|
|
|
950
961
|
# Severe drawdown live returns outside the backtest's bootstrapped interval -> `drift_flagged`
|
|
951
962
|
crash_live = tmp_path / "live_crash.csv"
|
|
@@ -958,6 +969,11 @@ def test_watch_pipeline_reruns_on_dataset_change_and_monitor_live_detects_drift(
|
|
|
958
969
|
assert "drift_flagged" in crash_out
|
|
959
970
|
assert "Research tool, not investment advice." in crash_out
|
|
960
971
|
|
|
972
|
+
from alphaengine.ledger import cumulative_trials_for
|
|
973
|
+
|
|
974
|
+
_, final_proj_n = cumulative_trials_for(proj)
|
|
975
|
+
assert final_proj_n == bt_proj_n
|
|
976
|
+
|
|
961
977
|
|
|
962
978
|
def test_require_firm_key_choke_point_keyring_and_no_key_leak(
|
|
963
979
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
@@ -997,6 +1013,10 @@ def test_require_firm_key_choke_point_keyring_and_no_key_leak(
|
|
|
997
1013
|
["overlap"],
|
|
998
1014
|
["size"],
|
|
999
1015
|
["monitor"],
|
|
1016
|
+
["workflows"],
|
|
1017
|
+
["runs"],
|
|
1018
|
+
["gaps"],
|
|
1019
|
+
["tonight"],
|
|
1000
1020
|
["run", "validate_study", "--url", "http://127.0.0.1:9"],
|
|
1001
1021
|
["init"],
|
|
1002
1022
|
["log"],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|