alpha-engine-lib 0.50.0__tar.gz → 0.52.0__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.
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/PKG-INFO +1 -1
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/pyproject.toml +1 -1
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/__init__.py +1 -1
- alpha_engine_lib-0.52.0/src/alpha_engine_lib/metrics.py +202 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/factor_risk_xs.py +9 -2
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/stats/__init__.py +2 -0
- alpha_engine_lib-0.52.0/src/alpha_engine_lib/quant/stats/intervals.py +220 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib.egg-info/PKG-INFO +1 -1
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib.egg-info/SOURCES.txt +4 -0
- alpha_engine_lib-0.52.0/tests/test_metrics.py +122 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_factor_risk_xs.py +12 -0
- alpha_engine_lib-0.52.0/tests/test_quant_stats_intervals.py +89 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/README.md +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/setup.cfg +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/agent_schemas.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/alerts.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/anthropic_payload.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/arcticdb.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/artifact_freshness.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/collector_results.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/cost.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/dates.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/decision_capture.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/ec2_spot.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/email_sender.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/eval_artifacts.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/http_retry.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/locks.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/logging.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/model_pricing.yaml +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/pillars.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/pipeline_status/__init__.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/pipeline_status/read.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/pipeline_status/registry.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/pipeline_status/templates.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/preflight.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/__init__.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/attribution.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/factor_risk.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/returns.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/risk_measures.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/riskstats.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/stats/dsr.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/stats/expectancy.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/stats/information_coefficient.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/stats/multiple_testing.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/stats/regime_sortino.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/stats/risk_matched_benchmark.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/rag/__init__.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/rag/db.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/rag/embeddings.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/rag/migrations/0001_content_tsv.sql +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/rag/rerank.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/rag/retrieval.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/rag/schema.sql +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/reconcile.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/secrets.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/sources/__init__.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/sources/protocols.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/ssm_dispatcher.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/ssm_log_capture.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/telegram.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/trading_calendar.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/transparency.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/transparency_inventory.yaml +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/universe.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib.egg-info/dependency_links.txt +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib.egg-info/requires.txt +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib.egg-info/top_level.txt +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_agent_schemas.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_alerts.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_anthropic_payload.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_arcticdb.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_artifact_freshness.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_collector_results.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_cost.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_dates.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_decision_capture.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_ec2_spot.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_email_sender.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_eval_artifacts.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_http_retry.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_locks.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_logging.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_pillars.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_pipeline_status_read.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_pipeline_status_registry.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_pipeline_status_templates.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_preflight.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_attribution.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_factor_risk.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_returns.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_risk_measures.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_riskstats.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_stats_dsr.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_stats_expectancy.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_stats_information_coefficient.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_stats_multiple_testing.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_stats_regime_sortino.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_stats_risk_matched_benchmark.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_rag.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_rag_rerank.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_rag_retrieval_hybrid.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_reconcile.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_secrets.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_sources_protocols.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_ssm_dispatcher.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_ssm_log_capture.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_telegram.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_trading_calendar.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_transparency.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_universe.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_version_bump_workflow.py +0 -0
- {alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_version_pin.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpha-engine-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.52.0
|
|
4
4
|
Summary: Shared utilities for the Alpha Engine modules: preflight, logging, ArcticDB, dates, decision capture, cost telemetry, Anthropic payload chokepoint, artifact freshness, RAG, agent schemas, SSM secrets, Telegram + SNS alerts, EC2 spot resilience, SSM log-capture, SSM dispatcher, Step-Functions execution-state projection, S3-conditional-PUT writer locks, and bounded-backoff HTTP retry. Full surface documented in README.
|
|
5
5
|
Author: Brian McMahon
|
|
6
6
|
License: Proprietary
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "alpha-engine-lib"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.52.0"
|
|
8
8
|
description = "Shared utilities for the Alpha Engine modules: preflight, logging, ArcticDB, dates, decision capture, cost telemetry, Anthropic payload chokepoint, artifact freshness, RAG, agent schemas, SSM secrets, Telegram + SNS alerts, EC2 spot resilience, SSM log-capture, SSM dispatcher, Step-Functions execution-state projection, S3-conditional-PUT writer locks, and bounded-backoff HTTP retry. Full surface documented in README."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# EC2 still runs Python 3.9 on the always-on micro instance (boto3 drops
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""metrics — the System Report Card v2 ``MetricRecord`` contract + status derivation.
|
|
2
|
+
|
|
3
|
+
A ``MetricRecord`` is the unit of the v2 report card: every graded component
|
|
4
|
+
(research / predictor / executor / backtester / substrate / agent / portfolio)
|
|
5
|
+
emits one, carrying not just a value but its statistical context — CI, sample
|
|
6
|
+
size vs floor, target, red-line, trend, and a derived status/letter. The letter
|
|
7
|
+
is *derived* from the status, never the source of truth (RC v2 Principle 2).
|
|
8
|
+
|
|
9
|
+
This module is the shared chokepoint: the producer (the evaluator's grading
|
|
10
|
+
layer) and every consumer (dashboard console, public site) agree on the schema
|
|
11
|
+
AND on the status semantics via the pure ``derive_*`` helpers here — so the same
|
|
12
|
+
``(value, CI, N)`` maps to the same GREEN/WATCH/RED everywhere.
|
|
13
|
+
|
|
14
|
+
The N/A taxonomy distinguishes the four engineering states that the legacy
|
|
15
|
+
"insufficient data" string conflated:
|
|
16
|
+
- ``N/A-NOT-IMPL`` grader exists, producer analysis not yet wired
|
|
17
|
+
- ``N/A-NOT-RUN`` producer implemented but did not run this cycle
|
|
18
|
+
- ``N/A-LOW-N`` ran, but N below half the floor — CI too wide to read
|
|
19
|
+
- ``N/A-MISSING-INPUT``ran, but a required upstream artifact was absent
|
|
20
|
+
|
|
21
|
+
Authoritative design: ``alpha-engine-docs/private/system-report-card-revamp-260522.md``.
|
|
22
|
+
Module-level aggregation (critical-gate module roll-up, BH-FDR over a tile's
|
|
23
|
+
component family) lives in the evaluator, not here — this is the per-component
|
|
24
|
+
contract only.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from typing import Literal
|
|
31
|
+
|
|
32
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
33
|
+
|
|
34
|
+
StatusLiteral = Literal[
|
|
35
|
+
"GREEN",
|
|
36
|
+
"WATCH",
|
|
37
|
+
"RED",
|
|
38
|
+
"N/A-NOT-IMPL",
|
|
39
|
+
"N/A-NOT-RUN",
|
|
40
|
+
"N/A-LOW-N",
|
|
41
|
+
"N/A-MISSING-INPUT",
|
|
42
|
+
]
|
|
43
|
+
CriticalityLiteral = Literal["critical", "supporting", "diagnostic"]
|
|
44
|
+
MetricTypeLiteral = Literal[
|
|
45
|
+
"ic", "lift", "ratio", "pct", "count", "duration",
|
|
46
|
+
"sharpe", "calibration", "p_value", "zscore", "log_return",
|
|
47
|
+
]
|
|
48
|
+
TrendDecorationLiteral = Literal["↑↑", "↑", "→", "↓", "↓↓"]
|
|
49
|
+
|
|
50
|
+
_NA_PREFIX = "N/A"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MetricRecord(BaseModel):
|
|
54
|
+
"""One graded component of the System Report Card v2.
|
|
55
|
+
|
|
56
|
+
``extra="allow"`` for forward-compat: a newer producer may add fields a
|
|
57
|
+
older consumer hasn't learned yet without breaking the read.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(extra="allow")
|
|
61
|
+
|
|
62
|
+
name: str = Field(description="snake_case stable key, e.g. 'predictor_meta_l2_ic'.")
|
|
63
|
+
module: str = Field(
|
|
64
|
+
description="Owning tile: portfolio | research | predictor | executor | "
|
|
65
|
+
"backtester | substrate | agent."
|
|
66
|
+
)
|
|
67
|
+
metric_type: MetricTypeLiteral
|
|
68
|
+
value: float | None = Field(default=None, description="Measured value (None when N/A-*).")
|
|
69
|
+
ci_low: float | None = Field(default=None)
|
|
70
|
+
ci_high: float | None = Field(default=None)
|
|
71
|
+
ci_method: str | None = Field(
|
|
72
|
+
default=None, description="e.g. 'bootstrap', 'newey-west', 'wilson'."
|
|
73
|
+
)
|
|
74
|
+
n_samples: int | None = Field(default=None, description="Observations behind the value.")
|
|
75
|
+
n_floor: int = Field(description="Documented minimum N for a confident reading.")
|
|
76
|
+
target: float | None = Field(default=None, description="At/beyond here = good.")
|
|
77
|
+
red_line: float | None = Field(default=None, description="At/beyond here = system-breaking.")
|
|
78
|
+
trend_4w: list[float] | None = Field(default=None)
|
|
79
|
+
trend_13w: list[float] | None = Field(default=None)
|
|
80
|
+
trend_decoration: TrendDecorationLiteral = Field(default="→")
|
|
81
|
+
status: StatusLiteral
|
|
82
|
+
status_reason: str = Field(description="One operator-readable sentence; never generic.")
|
|
83
|
+
criticality: CriticalityLiteral = Field(default="supporting")
|
|
84
|
+
source_path: str = Field(description="S3 URI / SQLite path / artifact this was read from.")
|
|
85
|
+
bh_fdr_adjusted_p: float | None = Field(default=None)
|
|
86
|
+
last_updated_utc: datetime
|
|
87
|
+
derived_letter: str = Field(default="N/A", description="Summary letter; derived from status+value.")
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_na(self) -> bool:
|
|
91
|
+
return self.status.startswith(_NA_PREFIX)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def derive_trend_decoration(
|
|
95
|
+
values: list[float] | None,
|
|
96
|
+
*,
|
|
97
|
+
higher_is_better: bool = True,
|
|
98
|
+
) -> TrendDecorationLiteral:
|
|
99
|
+
"""Map a rolling value window to a trend glyph (RC v2 Principle 5).
|
|
100
|
+
|
|
101
|
+
Looks at the last four points: ``↑↑`` if all 3 steps improved, ``↑`` if the
|
|
102
|
+
most recent 2 of those steps improved, ``↓↓``/``↓`` mirrored, else ``→``.
|
|
103
|
+
Improvement is "increase" when ``higher_is_better`` else "decrease".
|
|
104
|
+
"""
|
|
105
|
+
if not values or len(values) < 2:
|
|
106
|
+
return "→"
|
|
107
|
+
window = values[-4:]
|
|
108
|
+
steps = [b - a for a, b in zip(window, window[1:])]
|
|
109
|
+
if not higher_is_better:
|
|
110
|
+
steps = [-s for s in steps]
|
|
111
|
+
|
|
112
|
+
eps = 1e-12
|
|
113
|
+
ups = sum(1 for s in steps if s > eps)
|
|
114
|
+
downs = sum(1 for s in steps if s < -eps)
|
|
115
|
+
recent2 = steps[-2:]
|
|
116
|
+
|
|
117
|
+
if len(steps) >= 3 and ups == len(steps):
|
|
118
|
+
return "↑↑"
|
|
119
|
+
if len(steps) >= 3 and downs == len(steps):
|
|
120
|
+
return "↓↓"
|
|
121
|
+
if sum(1 for s in recent2 if s > eps) == len(recent2) and ups >= downs:
|
|
122
|
+
return "↑"
|
|
123
|
+
if sum(1 for s in recent2 if s < -eps) == len(recent2) and downs >= ups:
|
|
124
|
+
return "↓"
|
|
125
|
+
return "→"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def derive_letter(status: StatusLiteral) -> str:
|
|
129
|
+
"""Project a status onto the summary letter band (RC v2 Principle 2).
|
|
130
|
+
|
|
131
|
+
The letter is a display convenience only — ``status`` + ``value`` are the
|
|
132
|
+
source of truth. Any ``N/A-*`` projects to ``"N/A"``.
|
|
133
|
+
"""
|
|
134
|
+
if status.startswith(_NA_PREFIX):
|
|
135
|
+
return "N/A"
|
|
136
|
+
return {"GREEN": "A", "WATCH": "C", "RED": "F"}[status]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def derive_status(
|
|
140
|
+
*,
|
|
141
|
+
value: float | None,
|
|
142
|
+
n_samples: int | None,
|
|
143
|
+
n_floor: int,
|
|
144
|
+
target: float | None = None,
|
|
145
|
+
red_line: float | None = None,
|
|
146
|
+
ci_low: float | None = None,
|
|
147
|
+
ci_high: float | None = None,
|
|
148
|
+
implemented: bool = True,
|
|
149
|
+
ran: bool = True,
|
|
150
|
+
input_present: bool = True,
|
|
151
|
+
) -> StatusLiteral:
|
|
152
|
+
"""Derive the GREEN/WATCH/RED/``N/A-*`` status for one component.
|
|
153
|
+
|
|
154
|
+
Encodes RC v2 Principles 2 (status taxonomy) and 6 (sample-size discipline)
|
|
155
|
+
so producer and consumers agree. Direction is inferred from the
|
|
156
|
+
``target``/``red_line`` ordering: ``target >= red_line`` ⇒ higher-is-better,
|
|
157
|
+
else lower-is-better (e.g. max-drawdown, ECE).
|
|
158
|
+
|
|
159
|
+
The four N/A conditions take precedence in order: not-implemented →
|
|
160
|
+
not-run → missing-input → low-N. Above the floor, status follows the value
|
|
161
|
+
and (when provided) the confidence interval relative to target/red-line.
|
|
162
|
+
"""
|
|
163
|
+
if not implemented:
|
|
164
|
+
return "N/A-NOT-IMPL"
|
|
165
|
+
if not ran:
|
|
166
|
+
return "N/A-NOT-RUN"
|
|
167
|
+
if not input_present:
|
|
168
|
+
return "N/A-MISSING-INPUT"
|
|
169
|
+
if value is None or n_samples is None or n_samples < 0.5 * n_floor:
|
|
170
|
+
return "N/A-LOW-N"
|
|
171
|
+
|
|
172
|
+
higher_is_better = target is None or red_line is None or target >= red_line
|
|
173
|
+
|
|
174
|
+
def _at_or_better(a: float, b: float) -> bool:
|
|
175
|
+
return a >= b if higher_is_better else a <= b
|
|
176
|
+
|
|
177
|
+
def _at_or_worse(a: float, b: float) -> bool:
|
|
178
|
+
return a <= b if higher_is_better else a >= b
|
|
179
|
+
|
|
180
|
+
# RED: value at/below the red-line, or the CI sits entirely on the bad side.
|
|
181
|
+
if red_line is not None:
|
|
182
|
+
if _at_or_worse(value, red_line):
|
|
183
|
+
return "RED"
|
|
184
|
+
bad_bound = ci_low if higher_is_better else ci_high
|
|
185
|
+
if bad_bound is not None and _at_or_worse(bad_bound, red_line):
|
|
186
|
+
return "RED"
|
|
187
|
+
|
|
188
|
+
# Between half-floor and floor: CI too wide to claim GREEN regardless.
|
|
189
|
+
if n_samples < n_floor:
|
|
190
|
+
return "WATCH"
|
|
191
|
+
|
|
192
|
+
if target is not None:
|
|
193
|
+
if _at_or_better(value, target):
|
|
194
|
+
# GREEN needs the whole CI clear of the red-line (when both known).
|
|
195
|
+
good_bound = ci_low if higher_is_better else ci_high
|
|
196
|
+
if red_line is not None and good_bound is not None and _at_or_worse(good_bound, red_line):
|
|
197
|
+
return "WATCH"
|
|
198
|
+
return "GREEN"
|
|
199
|
+
return "WATCH"
|
|
200
|
+
|
|
201
|
+
# No target given: above red-line with adequate N ⇒ GREEN.
|
|
202
|
+
return "GREEN"
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/factor_risk_xs.py
RENAMED
|
@@ -235,8 +235,15 @@ def estimate_factor_covariance(
|
|
|
235
235
|
return pd.DataFrame(np.full((K, K), np.nan), index=cols, columns=cols)
|
|
236
236
|
|
|
237
237
|
if shrinkage == "ledoit_wolf":
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
# Single shared Ledoit-Wolf estimator (LV1-AE.a, 2026-06-03). The numpy
|
|
239
|
+
# ``quant.factor_risk.ledoit_wolf_cov`` is numerically identical to
|
|
240
|
+
# sklearn's ``LedoitWolf`` (max abs diff ~1e-21, validated across
|
|
241
|
+
# n∈[35,1000]) — both center the data and estimate the same shrinkage
|
|
242
|
+
# intensity toward a scaled-identity target. Consolidating onto the numpy
|
|
243
|
+
# impl kills the duplicate reimplementation; sklearn stays a lazy import
|
|
244
|
+
# for OAS only.
|
|
245
|
+
from .factor_risk import ledoit_wolf_cov
|
|
246
|
+
F = ledoit_wolf_cov(clean.to_numpy(), shrinkage="ledoit_wolf")
|
|
240
247
|
elif shrinkage == "oas":
|
|
241
248
|
from sklearn.covariance import OAS
|
|
242
249
|
F = OAS().fit(clean.to_numpy()).covariance_
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/stats/__init__.py
RENAMED
|
@@ -11,6 +11,7 @@ Modules:
|
|
|
11
11
|
- ``information_coefficient`` — Spearman rank IC of conviction vs forward return
|
|
12
12
|
- ``expectancy`` — hit-rate × win/loss decomposition
|
|
13
13
|
- ``multiple_testing`` — Benjamini-Hochberg FDR correction
|
|
14
|
+
- ``intervals`` — bootstrap CI, Newey-West SE, Wilson score interval
|
|
14
15
|
- ``risk_matched_benchmark`` — EW-high-vol + beta-matched-SPY baselines + IR
|
|
15
16
|
- ``regime_sortino`` — regime-stratified cross-sectional pick-alpha Sortino
|
|
16
17
|
|
|
@@ -18,6 +19,7 @@ Example::
|
|
|
18
19
|
|
|
19
20
|
from alpha_engine_lib.quant.stats.dsr import compute_dsr
|
|
20
21
|
from alpha_engine_lib.quant.stats.multiple_testing import benjamini_hochberg
|
|
22
|
+
from alpha_engine_lib.quant.stats.intervals import bootstrap_ci, wilson_score_interval
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
25
|
from __future__ import annotations
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""intervals — Bootstrap confidence intervals, Newey-West SE, Wilson score intervals.
|
|
2
|
+
|
|
3
|
+
The three inference primitives the System Report Card v2 metric records require
|
|
4
|
+
(``MetricRecord.ci_method`` ∈ {``bootstrap``, ``newey-west``, ``wilson``}):
|
|
5
|
+
|
|
6
|
+
- ``bootstrap_ci`` — percentile bootstrap CI for any statistic of a
|
|
7
|
+
sample (default the mean). The general-purpose
|
|
8
|
+
CI for ICs, lifts, hit-rates, Sharpe point
|
|
9
|
+
estimates where no closed form is convenient.
|
|
10
|
+
- ``newey_west_se`` — heteroskedasticity-and-autocorrelation-consistent
|
|
11
|
+
(HAC) standard error of a series mean, for
|
|
12
|
+
autocorrelated daily P&L where the iid SE
|
|
13
|
+
understates uncertainty.
|
|
14
|
+
- ``wilson_score_interval`` — Wilson score binomial interval for rates with
|
|
15
|
+
small N (veto-gate precision/recall, hit-rate),
|
|
16
|
+
which the normal-approximation interval handles
|
|
17
|
+
badly near 0/1.
|
|
18
|
+
|
|
19
|
+
Pure-compute; no I/O. bootstrap + Newey-West need numpy (install
|
|
20
|
+
``alpha-engine-lib[quant]``); Wilson is stdlib-only (``statistics.NormalDist``).
|
|
21
|
+
|
|
22
|
+
Reference: López de Prado, *Advances in Financial Machine Learning* (bootstrap
|
|
23
|
+
+ HAC); Wilson (1927) "Probable Inference, the Law of Succession, and
|
|
24
|
+
Statistical Inference".
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import math
|
|
30
|
+
from statistics import NormalDist
|
|
31
|
+
from typing import Callable, Sequence, TypedDict
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
35
|
+
_DEFAULT_N_RESAMPLES = 1000
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BootstrapCIResult(TypedDict, total=False):
|
|
39
|
+
status: str # "ok" | "insufficient_data"
|
|
40
|
+
n: int # observations after NaN drop
|
|
41
|
+
estimate: float # statistic on the full sample
|
|
42
|
+
ci_low: float
|
|
43
|
+
ci_high: float
|
|
44
|
+
ci_level: float # e.g. 0.95
|
|
45
|
+
method: str # "bootstrap"
|
|
46
|
+
n_resamples: int
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class NeweyWestResult(TypedDict, total=False):
|
|
50
|
+
status: str # "ok" | "insufficient_data"
|
|
51
|
+
n: int
|
|
52
|
+
estimate: float # sample mean
|
|
53
|
+
se: float # HAC standard error of the mean
|
|
54
|
+
lags: int # Bartlett-kernel lags used
|
|
55
|
+
method: str # "newey-west"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WilsonScoreResult(TypedDict, total=False):
|
|
59
|
+
status: str # "ok" | "insufficient_data"
|
|
60
|
+
n: int # trials
|
|
61
|
+
successes: int
|
|
62
|
+
rate: float # successes / trials (point estimate)
|
|
63
|
+
estimate: float # alias of rate (uniform with the other results)
|
|
64
|
+
ci_low: float
|
|
65
|
+
ci_high: float
|
|
66
|
+
ci_level: float
|
|
67
|
+
method: str # "wilson"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _clean(data: Sequence[float] | np.ndarray) -> np.ndarray:
|
|
71
|
+
"""Coerce to a 1-D float array with NaN/inf dropped."""
|
|
72
|
+
arr = np.asarray(data, dtype=float).ravel()
|
|
73
|
+
return arr[np.isfinite(arr)]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def bootstrap_ci(
|
|
77
|
+
data: Sequence[float] | np.ndarray,
|
|
78
|
+
statistic: Callable[[np.ndarray], float] = np.mean,
|
|
79
|
+
*,
|
|
80
|
+
ci_level: float = 0.95,
|
|
81
|
+
n_resamples: int = _DEFAULT_N_RESAMPLES,
|
|
82
|
+
seed: int = 0,
|
|
83
|
+
) -> BootstrapCIResult:
|
|
84
|
+
"""Percentile bootstrap confidence interval for ``statistic`` of ``data``.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
data: 1-D sample of observations (NaN/inf dropped).
|
|
88
|
+
statistic: Callable ``(np.ndarray) -> float`` to bootstrap. Defaults to
|
|
89
|
+
the mean.
|
|
90
|
+
ci_level: Confidence level in (0, 1) (default 0.95).
|
|
91
|
+
n_resamples: Number of bootstrap resamples (default 1000).
|
|
92
|
+
seed: RNG seed for reproducibility (the report card must be stable
|
|
93
|
+
across re-renders of the same cycle).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
A :class:`BootstrapCIResult`. ``status == "insufficient_data"`` when
|
|
97
|
+
fewer than 2 finite observations remain.
|
|
98
|
+
"""
|
|
99
|
+
arr = _clean(data)
|
|
100
|
+
n = int(arr.size)
|
|
101
|
+
if n < 2:
|
|
102
|
+
return {"status": "insufficient_data", "n": n}
|
|
103
|
+
|
|
104
|
+
estimate = float(statistic(arr))
|
|
105
|
+
rng = np.random.default_rng(seed)
|
|
106
|
+
idx = rng.integers(0, n, size=(n_resamples, n))
|
|
107
|
+
boot = np.fromiter(
|
|
108
|
+
(statistic(arr[row]) for row in idx), dtype=float, count=n_resamples
|
|
109
|
+
)
|
|
110
|
+
boot = boot[np.isfinite(boot)]
|
|
111
|
+
if boot.size == 0:
|
|
112
|
+
return {"status": "insufficient_data", "n": n}
|
|
113
|
+
|
|
114
|
+
tail = (1.0 - ci_level) / 2.0
|
|
115
|
+
ci_low = float(np.percentile(boot, 100.0 * tail))
|
|
116
|
+
ci_high = float(np.percentile(boot, 100.0 * (1.0 - tail)))
|
|
117
|
+
return {
|
|
118
|
+
"status": "ok",
|
|
119
|
+
"n": n,
|
|
120
|
+
"estimate": estimate,
|
|
121
|
+
"ci_low": ci_low,
|
|
122
|
+
"ci_high": ci_high,
|
|
123
|
+
"ci_level": ci_level,
|
|
124
|
+
"method": "bootstrap",
|
|
125
|
+
"n_resamples": int(boot.size),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _auto_lags(n: int) -> int:
|
|
130
|
+
"""Newey-West (1994) automatic lag selection: floor(4·(n/100)^(2/9))."""
|
|
131
|
+
return int(math.floor(4.0 * (n / 100.0) ** (2.0 / 9.0)))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def newey_west_se(
|
|
135
|
+
series: Sequence[float] | np.ndarray,
|
|
136
|
+
*,
|
|
137
|
+
max_lags: int | None = None,
|
|
138
|
+
) -> NeweyWestResult:
|
|
139
|
+
"""HAC (Newey-West, Bartlett kernel) standard error of the series mean.
|
|
140
|
+
|
|
141
|
+
For autocorrelated series (daily P&L), the iid SE ``s/√n`` understates
|
|
142
|
+
uncertainty. The HAC estimator inflates the long-run variance by the
|
|
143
|
+
Bartlett-weighted autocovariances up to ``max_lags``.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
series: 1-D series (NaN/inf dropped).
|
|
147
|
+
max_lags: Bartlett-kernel lag truncation. ``None`` ⇒ the Newey-West
|
|
148
|
+
(1994) rule ``floor(4·(n/100)^(2/9))``. Clamped to ``[0, n-1]``.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
A :class:`NeweyWestResult`. ``status == "insufficient_data"`` for n < 2.
|
|
152
|
+
"""
|
|
153
|
+
x = _clean(series)
|
|
154
|
+
n = int(x.size)
|
|
155
|
+
if n < 2:
|
|
156
|
+
return {"status": "insufficient_data", "n": n}
|
|
157
|
+
|
|
158
|
+
lags = _auto_lags(n) if max_lags is None else int(max_lags)
|
|
159
|
+
lags = max(0, min(lags, n - 1))
|
|
160
|
+
|
|
161
|
+
e = x - x.mean()
|
|
162
|
+
gamma0 = float(np.dot(e, e) / n)
|
|
163
|
+
lrv = gamma0
|
|
164
|
+
for j in range(1, lags + 1):
|
|
165
|
+
weight = 1.0 - j / (lags + 1.0)
|
|
166
|
+
gamma_j = float(np.dot(e[j:], e[:-j]) / n)
|
|
167
|
+
lrv += 2.0 * weight * gamma_j
|
|
168
|
+
lrv = max(lrv, 0.0) # Bartlett kernel guarantees PSD; clamp float error.
|
|
169
|
+
se = math.sqrt(lrv / n)
|
|
170
|
+
return {
|
|
171
|
+
"status": "ok",
|
|
172
|
+
"n": n,
|
|
173
|
+
"estimate": float(x.mean()),
|
|
174
|
+
"se": se,
|
|
175
|
+
"lags": lags,
|
|
176
|
+
"method": "newey-west",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def wilson_score_interval(
|
|
181
|
+
successes: int,
|
|
182
|
+
trials: int,
|
|
183
|
+
*,
|
|
184
|
+
ci_level: float = 0.95,
|
|
185
|
+
) -> WilsonScoreResult:
|
|
186
|
+
"""Wilson score interval for a binomial proportion.
|
|
187
|
+
|
|
188
|
+
Preferred over the normal-approximation (Wald) interval for small ``trials``
|
|
189
|
+
or rates near 0/1, where Wald produces bounds outside [0, 1] and undercovers.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
successes: Count of successes (0 ≤ successes ≤ trials).
|
|
193
|
+
trials: Total trials (> 0).
|
|
194
|
+
ci_level: Confidence level in (0, 1) (default 0.95).
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
A :class:`WilsonScoreResult`. ``status == "insufficient_data"`` for
|
|
198
|
+
``trials <= 0``. Bounds are clamped to [0, 1].
|
|
199
|
+
"""
|
|
200
|
+
if trials <= 0:
|
|
201
|
+
return {"status": "insufficient_data", "n": int(max(trials, 0))}
|
|
202
|
+
successes = max(0, min(int(successes), int(trials)))
|
|
203
|
+
|
|
204
|
+
z = NormalDist().inv_cdf(1.0 - (1.0 - ci_level) / 2.0)
|
|
205
|
+
p = successes / trials
|
|
206
|
+
z2 = z * z
|
|
207
|
+
denom = 1.0 + z2 / trials
|
|
208
|
+
center = (p + z2 / (2.0 * trials)) / denom
|
|
209
|
+
margin = (z / denom) * math.sqrt(p * (1.0 - p) / trials + z2 / (4.0 * trials * trials))
|
|
210
|
+
return {
|
|
211
|
+
"status": "ok",
|
|
212
|
+
"n": int(trials),
|
|
213
|
+
"successes": int(successes),
|
|
214
|
+
"rate": p,
|
|
215
|
+
"estimate": p,
|
|
216
|
+
"ci_low": max(0.0, center - margin),
|
|
217
|
+
"ci_high": min(1.0, center + margin),
|
|
218
|
+
"ci_level": ci_level,
|
|
219
|
+
"method": "wilson",
|
|
220
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpha-engine-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.52.0
|
|
4
4
|
Summary: Shared utilities for the Alpha Engine modules: preflight, logging, ArcticDB, dates, decision capture, cost telemetry, Anthropic payload chokepoint, artifact freshness, RAG, agent schemas, SSM secrets, Telegram + SNS alerts, EC2 spot resilience, SSM log-capture, SSM dispatcher, Step-Functions execution-state projection, S3-conditional-PUT writer locks, and bounded-backoff HTTP retry. Full surface documented in README.
|
|
5
5
|
Author: Brian McMahon
|
|
6
6
|
License: Proprietary
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib.egg-info/SOURCES.txt
RENAMED
|
@@ -16,6 +16,7 @@ src/alpha_engine_lib/eval_artifacts.py
|
|
|
16
16
|
src/alpha_engine_lib/http_retry.py
|
|
17
17
|
src/alpha_engine_lib/locks.py
|
|
18
18
|
src/alpha_engine_lib/logging.py
|
|
19
|
+
src/alpha_engine_lib/metrics.py
|
|
19
20
|
src/alpha_engine_lib/model_pricing.yaml
|
|
20
21
|
src/alpha_engine_lib/pillars.py
|
|
21
22
|
src/alpha_engine_lib/preflight.py
|
|
@@ -48,6 +49,7 @@ src/alpha_engine_lib/quant/stats/__init__.py
|
|
|
48
49
|
src/alpha_engine_lib/quant/stats/dsr.py
|
|
49
50
|
src/alpha_engine_lib/quant/stats/expectancy.py
|
|
50
51
|
src/alpha_engine_lib/quant/stats/information_coefficient.py
|
|
52
|
+
src/alpha_engine_lib/quant/stats/intervals.py
|
|
51
53
|
src/alpha_engine_lib/quant/stats/multiple_testing.py
|
|
52
54
|
src/alpha_engine_lib/quant/stats/regime_sortino.py
|
|
53
55
|
src/alpha_engine_lib/quant/stats/risk_matched_benchmark.py
|
|
@@ -75,6 +77,7 @@ tests/test_eval_artifacts.py
|
|
|
75
77
|
tests/test_http_retry.py
|
|
76
78
|
tests/test_locks.py
|
|
77
79
|
tests/test_logging.py
|
|
80
|
+
tests/test_metrics.py
|
|
78
81
|
tests/test_pillars.py
|
|
79
82
|
tests/test_pipeline_status_read.py
|
|
80
83
|
tests/test_pipeline_status_registry.py
|
|
@@ -89,6 +92,7 @@ tests/test_quant_riskstats.py
|
|
|
89
92
|
tests/test_quant_stats_dsr.py
|
|
90
93
|
tests/test_quant_stats_expectancy.py
|
|
91
94
|
tests/test_quant_stats_information_coefficient.py
|
|
95
|
+
tests/test_quant_stats_intervals.py
|
|
92
96
|
tests/test_quant_stats_multiple_testing.py
|
|
93
97
|
tests/test_quant_stats_regime_sortino.py
|
|
94
98
|
tests/test_quant_stats_risk_matched_benchmark.py
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Tests for alpha_engine_lib.metrics — MetricRecord contract + status derivation."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
8
|
+
from alpha_engine_lib.metrics import (
|
|
9
|
+
MetricRecord,
|
|
10
|
+
derive_letter,
|
|
11
|
+
derive_status,
|
|
12
|
+
derive_trend_decoration,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _record(**overrides):
|
|
17
|
+
base = dict(
|
|
18
|
+
name="predictor_meta_l2_ic",
|
|
19
|
+
module="predictor",
|
|
20
|
+
metric_type="ic",
|
|
21
|
+
n_floor=100,
|
|
22
|
+
status="GREEN",
|
|
23
|
+
status_reason="L2 IC 0.48 (CI [0.21,0.74], N=626) above target 0.05.",
|
|
24
|
+
source_path="s3://alpha-engine-research/predictor/metrics/latest.json#l2_ic",
|
|
25
|
+
last_updated_utc=datetime(2026, 6, 4, tzinfo=timezone.utc),
|
|
26
|
+
)
|
|
27
|
+
base.update(overrides)
|
|
28
|
+
return MetricRecord(**base)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestMetricRecord:
|
|
32
|
+
def test_minimal_valid_record(self):
|
|
33
|
+
r = _record(value=0.48, n_samples=626)
|
|
34
|
+
assert r.module == "predictor" and r.value == 0.48
|
|
35
|
+
assert r.trend_decoration == "→" # default
|
|
36
|
+
assert r.is_na is False
|
|
37
|
+
|
|
38
|
+
def test_na_status_flags_is_na(self):
|
|
39
|
+
assert _record(status="N/A-LOW-N").is_na is True
|
|
40
|
+
|
|
41
|
+
def test_extra_fields_allowed(self):
|
|
42
|
+
r = _record(value=0.1, n_samples=200, future_field="ok")
|
|
43
|
+
assert r.value == 0.1
|
|
44
|
+
|
|
45
|
+
def test_bad_status_rejected(self):
|
|
46
|
+
with pytest.raises(ValidationError):
|
|
47
|
+
_record(status="MAYBE")
|
|
48
|
+
|
|
49
|
+
def test_bad_metric_type_rejected(self):
|
|
50
|
+
with pytest.raises(ValidationError):
|
|
51
|
+
_record(metric_type="vibes")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestDeriveTrendDecoration:
|
|
55
|
+
def test_sustained_up_and_down(self):
|
|
56
|
+
assert derive_trend_decoration([1, 2, 3, 4]) == "↑↑"
|
|
57
|
+
assert derive_trend_decoration([4, 3, 2, 1]) == "↓↓"
|
|
58
|
+
|
|
59
|
+
def test_flat_and_too_short(self):
|
|
60
|
+
assert derive_trend_decoration([2, 2, 2, 2]) == "→"
|
|
61
|
+
assert derive_trend_decoration([1]) == "→"
|
|
62
|
+
assert derive_trend_decoration(None) == "→"
|
|
63
|
+
|
|
64
|
+
def test_recent_improvement(self):
|
|
65
|
+
# down then up-up: not sustained, but recent 2 improved.
|
|
66
|
+
assert derive_trend_decoration([5, 1, 2, 3]) == "↑"
|
|
67
|
+
|
|
68
|
+
def test_higher_is_better_false_flips(self):
|
|
69
|
+
# decreasing values are an improvement when lower is better (e.g. drawdown).
|
|
70
|
+
assert derive_trend_decoration([4, 3, 2, 1], higher_is_better=False) == "↑↑"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestDeriveLetter:
|
|
74
|
+
def test_status_to_letter(self):
|
|
75
|
+
assert derive_letter("GREEN") == "A"
|
|
76
|
+
assert derive_letter("WATCH") == "C"
|
|
77
|
+
assert derive_letter("RED") == "F"
|
|
78
|
+
assert derive_letter("N/A-NOT-IMPL") == "N/A"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestDeriveStatus:
|
|
82
|
+
def test_na_precedence(self):
|
|
83
|
+
assert derive_status(value=0.5, n_samples=200, n_floor=100, implemented=False) == "N/A-NOT-IMPL"
|
|
84
|
+
assert derive_status(value=0.5, n_samples=200, n_floor=100, ran=False) == "N/A-NOT-RUN"
|
|
85
|
+
assert derive_status(value=0.5, n_samples=200, n_floor=100, input_present=False) == "N/A-MISSING-INPUT"
|
|
86
|
+
|
|
87
|
+
def test_low_n(self):
|
|
88
|
+
assert derive_status(value=0.5, n_samples=40, n_floor=100) == "N/A-LOW-N"
|
|
89
|
+
assert derive_status(value=None, n_samples=200, n_floor=100) == "N/A-LOW-N"
|
|
90
|
+
|
|
91
|
+
def test_red_when_at_or_below_red_line(self):
|
|
92
|
+
s = derive_status(value=-0.01, n_samples=200, n_floor=100, target=0.05, red_line=0.0)
|
|
93
|
+
assert s == "RED"
|
|
94
|
+
|
|
95
|
+
def test_red_when_ci_entirely_below_red_line(self):
|
|
96
|
+
s = derive_status(
|
|
97
|
+
value=0.02, n_samples=200, n_floor=100, target=0.05, red_line=0.0,
|
|
98
|
+
ci_low=-0.03, ci_high=-0.01,
|
|
99
|
+
)
|
|
100
|
+
assert s == "RED"
|
|
101
|
+
|
|
102
|
+
def test_green_when_above_target_with_clear_ci(self):
|
|
103
|
+
s = derive_status(
|
|
104
|
+
value=0.48, n_samples=626, n_floor=100, target=0.05, red_line=0.0,
|
|
105
|
+
ci_low=0.21, ci_high=0.74,
|
|
106
|
+
)
|
|
107
|
+
assert s == "GREEN"
|
|
108
|
+
|
|
109
|
+
def test_watch_between_half_floor_and_floor(self):
|
|
110
|
+
# N=70 is above 0.5*floor (50) but below floor (100) → WATCH regardless of value.
|
|
111
|
+
s = derive_status(value=0.9, n_samples=70, n_floor=100, target=0.05, red_line=0.0)
|
|
112
|
+
assert s == "WATCH"
|
|
113
|
+
|
|
114
|
+
def test_watch_below_target_above_red_line(self):
|
|
115
|
+
s = derive_status(value=0.02, n_samples=200, n_floor=100, target=0.05, red_line=0.0)
|
|
116
|
+
assert s == "WATCH"
|
|
117
|
+
|
|
118
|
+
def test_lower_is_better_direction(self):
|
|
119
|
+
# max_drawdown: target 0.15, red_line 0.25 (lower is better).
|
|
120
|
+
assert derive_status(value=0.08, n_samples=200, n_floor=60, target=0.15, red_line=0.25) == "GREEN"
|
|
121
|
+
assert derive_status(value=0.30, n_samples=200, n_floor=60, target=0.15, red_line=0.25) == "RED"
|
|
122
|
+
assert derive_status(value=0.20, n_samples=200, n_floor=60, target=0.15, red_line=0.25) == "WATCH"
|
|
@@ -29,6 +29,18 @@ from alpha_engine_lib.quant.factor_risk_xs import ( # noqa: E402 (after import
|
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def test_ledoit_wolf_branch_uses_shared_numpy_impl():
|
|
33
|
+
"""LV1-AE.a consolidation contract: estimate_factor_covariance's ledoit_wolf
|
|
34
|
+
path is the shared numpy quant.factor_risk.ledoit_wolf_cov (one LW impl)."""
|
|
35
|
+
from alpha_engine_lib.quant.factor_risk import ledoit_wolf_cov
|
|
36
|
+
|
|
37
|
+
rng = np.random.RandomState(0)
|
|
38
|
+
panel = pd.DataFrame(rng.normal(0, 0.01, (200, 4)), columns=["market", "MOM", "VAL", "QUAL"])
|
|
39
|
+
F = estimate_factor_covariance(panel, shrinkage="ledoit_wolf", min_obs=30)
|
|
40
|
+
expected = ledoit_wolf_cov(panel.to_numpy(), shrinkage="ledoit_wolf")
|
|
41
|
+
assert np.allclose(F.to_numpy(), expected, atol=1e-15)
|
|
42
|
+
|
|
43
|
+
|
|
32
44
|
# ─── Helpers ────────────────────────────────────────────────────────────────
|
|
33
45
|
|
|
34
46
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Tests for alpha_engine_lib.quant.stats.intervals — bootstrap CI, Newey-West SE, Wilson."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from alpha_engine_lib.quant.stats.intervals import (
|
|
9
|
+
bootstrap_ci,
|
|
10
|
+
newey_west_se,
|
|
11
|
+
wilson_score_interval,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestBootstrapCI:
|
|
16
|
+
def test_insufficient_data(self):
|
|
17
|
+
assert bootstrap_ci([]) == {"status": "insufficient_data", "n": 0}
|
|
18
|
+
assert bootstrap_ci([4.2])["status"] == "insufficient_data"
|
|
19
|
+
|
|
20
|
+
def test_constant_sample_has_zero_width(self):
|
|
21
|
+
out = bootstrap_ci([5.0, 5.0, 5.0, 5.0])
|
|
22
|
+
assert out["status"] == "ok"
|
|
23
|
+
assert out["estimate"] == 5.0
|
|
24
|
+
assert out["ci_low"] == 5.0 and out["ci_high"] == 5.0
|
|
25
|
+
|
|
26
|
+
def test_ci_brackets_estimate_and_is_deterministic(self):
|
|
27
|
+
data = list(range(100))
|
|
28
|
+
a = bootstrap_ci(data, seed=7)
|
|
29
|
+
b = bootstrap_ci(data, seed=7)
|
|
30
|
+
assert a == b # seeded → reproducible (report card must be stable)
|
|
31
|
+
assert a["ci_low"] <= a["estimate"] <= a["ci_high"]
|
|
32
|
+
assert a["estimate"] == float(np.mean(data))
|
|
33
|
+
assert a["ci_level"] == 0.95 and a["method"] == "bootstrap"
|
|
34
|
+
|
|
35
|
+
def test_nan_dropped(self):
|
|
36
|
+
out = bootstrap_ci([1.0, 2.0, float("nan"), 3.0])
|
|
37
|
+
assert out["n"] == 3
|
|
38
|
+
|
|
39
|
+
def test_custom_statistic(self):
|
|
40
|
+
out = bootstrap_ci([1.0, 2.0, 3.0, 4.0, 5.0], statistic=np.median, seed=1)
|
|
41
|
+
assert out["status"] == "ok"
|
|
42
|
+
assert out["estimate"] == 3.0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestNeweyWestSE:
|
|
46
|
+
def test_insufficient_data(self):
|
|
47
|
+
assert newey_west_se([2.0])["status"] == "insufficient_data"
|
|
48
|
+
|
|
49
|
+
def test_zero_lag_matches_iid_se(self):
|
|
50
|
+
# [1..5]: mean 3, gamma0 = 10/5 = 2, se = sqrt(2/5).
|
|
51
|
+
out = newey_west_se([1.0, 2.0, 3.0, 4.0, 5.0], max_lags=0)
|
|
52
|
+
assert out["estimate"] == 3.0
|
|
53
|
+
assert out["lags"] == 0
|
|
54
|
+
assert out["se"] == math.sqrt(0.4)
|
|
55
|
+
|
|
56
|
+
def test_lags_clamped_to_n_minus_1(self):
|
|
57
|
+
out = newey_west_se([1.0, 2.0, 3.0], max_lags=99)
|
|
58
|
+
assert out["lags"] == 2
|
|
59
|
+
|
|
60
|
+
def test_auto_lags_nonnegative(self):
|
|
61
|
+
out = newey_west_se([float(x) for x in range(200)])
|
|
62
|
+
assert out["status"] == "ok"
|
|
63
|
+
assert out["lags"] >= 0
|
|
64
|
+
assert out["se"] >= 0.0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestWilsonScoreInterval:
|
|
68
|
+
def test_insufficient_data(self):
|
|
69
|
+
assert wilson_score_interval(0, 0)["status"] == "insufficient_data"
|
|
70
|
+
|
|
71
|
+
def test_known_50_of_100(self):
|
|
72
|
+
# Textbook Wilson 95% interval for 50/100 is [0.4038, 0.5962].
|
|
73
|
+
out = wilson_score_interval(50, 100)
|
|
74
|
+
assert out["rate"] == 0.5
|
|
75
|
+
assert abs(out["ci_low"] - 0.4038) < 1e-3
|
|
76
|
+
assert abs(out["ci_high"] - 0.5962) < 1e-3
|
|
77
|
+
|
|
78
|
+
def test_bounds_clamped_to_unit_interval(self):
|
|
79
|
+
# successes=0 → true lower bound is 0 (residual float noise ≈ 1e-17).
|
|
80
|
+
lo = wilson_score_interval(0, 10)
|
|
81
|
+
assert lo["ci_low"] == pytest.approx(0.0, abs=1e-9)
|
|
82
|
+
assert 0.0 < lo["ci_high"] < 1.0
|
|
83
|
+
hi = wilson_score_interval(10, 10)
|
|
84
|
+
assert hi["ci_high"] == pytest.approx(1.0, abs=1e-9)
|
|
85
|
+
assert 0.0 < hi["ci_low"] < 1.0
|
|
86
|
+
|
|
87
|
+
def test_successes_clamped_to_trials(self):
|
|
88
|
+
out = wilson_score_interval(15, 10)
|
|
89
|
+
assert out["successes"] == 10 and out["rate"] == 1.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/anthropic_payload.py
RENAMED
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/artifact_freshness.py
RENAMED
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/collector_results.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/decision_capture.py
RENAMED
|
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
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/pipeline_status/__init__.py
RENAMED
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/pipeline_status/read.py
RENAMED
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/pipeline_status/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/attribution.py
RENAMED
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/factor_risk.py
RENAMED
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/risk_measures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/quant/stats/expectancy.py
RENAMED
|
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
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/sources/__init__.py
RENAMED
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/sources/protocols.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/trading_calendar.py
RENAMED
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib/transparency_inventory.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib.egg-info/requires.txt
RENAMED
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/src/alpha_engine_lib.egg-info/top_level.txt
RENAMED
|
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
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_stats_multiple_testing.py
RENAMED
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_stats_regime_sortino.py
RENAMED
|
File without changes
|
{alpha_engine_lib-0.50.0 → alpha_engine_lib-0.52.0}/tests/test_quant_stats_risk_matched_benchmark.py
RENAMED
|
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
|