diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9de1af4..061d102 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: working-directory: ${{ runner.temp }} env: RISKLABAI_REPOSITORY_ROOT: ${{ github.workspace }} - run: python -B -c "import os; from pathlib import Path; import RiskLabAI; origin = Path(RiskLabAI.__file__).resolve(); repository = Path(os.environ['RISKLABAI_REPOSITORY_ROOT']).resolve(); assert not origin.is_relative_to(repository), (origin, repository); assert RiskLabAI.__version__ == '3.0.0'; print(origin)" + run: python -B -c "import os; from pathlib import Path; import RiskLabAI; origin = Path(RiskLabAI.__file__).resolve(); repository = Path(os.environ['RISKLABAI_REPOSITORY_ROOT']).resolve(); assert not origin.is_relative_to(repository), (origin, repository); assert RiskLabAI.__version__ == '3.1.0'; print(origin)" - name: Run the installed base package tests working-directory: ${{ runner.temp }} run: python -B -m pytest -q -p no:cacheprovider -c "$GITHUB_WORKSPACE/pyproject.toml" "$GITHUB_WORKSPACE/test" @@ -91,7 +91,7 @@ jobs: working-directory: ${{ runner.temp }} env: RISKLABAI_REPOSITORY_ROOT: ${{ github.workspace }} - run: python -B -c "import os; from pathlib import Path; import RiskLabAI; origin = Path(RiskLabAI.__file__).resolve(); repository = Path(os.environ['RISKLABAI_REPOSITORY_ROOT']).resolve(); assert not origin.is_relative_to(repository), (origin, repository); assert RiskLabAI.__version__ == '3.0.0'; print(origin)" + run: python -B -c "import os; from pathlib import Path; import RiskLabAI; origin = Path(RiskLabAI.__file__).resolve(); repository = Path(os.environ['RISKLABAI_REPOSITORY_ROOT']).resolve(); assert not origin.is_relative_to(repository), (origin, repository); assert RiskLabAI.__version__ == '3.1.0'; print(origin)" - name: Run the installed optional-feature tests working-directory: ${{ runner.temp }} run: python -B -m pytest -q -p no:cacheprovider -c "$GITHUB_WORKSPACE/pyproject.toml" "$GITHUB_WORKSPACE/test" diff --git a/CHANGELOG.md b/CHANGELOG.md index f613a01..0935005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,29 @@ All notable changes to RiskLabAI.py are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning: [SemVer](https://semver.org/). +## [3.1.0] - Unreleased + +### Added + +- Preserved the released 57-name `RiskLabAI.causal_factor_analysis` contract + and added 30 paper-derived names, producing an 87-name causal API with + matching Julia concepts. +- Added general-variance factor-mirage coefficients, + allocation-misspecification diagnostics, accepted-DAG factor roles, + deterministic structural-model evaluation, and separate family- and + selection-level false-discovery analytics for searched trials. +- Added direct analytical examples, independent mathematical and graph oracles, + randomized and stability checks, validation boundaries, and a shared + numerical Python-Julia fixture. + +### Documentation + +- Classified every relevant causal-factor source found in the systematic + library review. Thirteen contradictory or under-specified method units remain + explicitly source-blocked rather than being approximated or guessed. +- Froze the additive candidate metadata at 3.1.0 for final local integration; + version-control, publication, and release actions remain human-controlled. + ## [2.0.1] ### Fixed diff --git a/INSTALLATION.md b/INSTALLATION.md index f86df4e..8a3f100 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -2,7 +2,7 @@ ## Supported environments -RiskLabAI 3.0.0 supports: +RiskLabAI 3.1.0 supports: - CPython 3.12, 3.13, and 3.14 - NumPy `>=2.2,<3` diff --git a/MANIFEST.in b/MANIFEST.in index f572cee..278de6c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,7 +7,7 @@ include PACKAGE_FILES.json include PACKAGE_INSPECTION.md include .github/workflows/ci.yml recursive-include src *.py -recursive-include test *.py +recursive-include test *.py *.tsv recursive-include docs *.md recursive-include examples *.py global-exclude __pycache__ diff --git a/PACKAGE_FILES.json b/PACKAGE_FILES.json index 9f4dd1c..51da517 100644 --- a/PACKAGE_FILES.json +++ b/PACKAGE_FILES.json @@ -1,27 +1,152 @@ { - "schema_version": 1, + "schema_version": 2, "document_type": "RISKLABAI_PYTHON_INTENDED_PACKAGE_FILES", "product": "RiskLabAI", "language": "Python", - "version": "3.0.0", + "version": "3.1.0", + "version_note": "Final local integration candidate; package artifacts are inspection-only and every version-control and release action remains human-controlled.", "artifact_types": [ "wheel", "source_distribution" ], - "candidate_file_count": 216, + "candidate_file_count": 353, "candidate_files": [ ".github/workflows/ci.yml", + ".github/workflows/publish.yml", + ".gitignore", + "CHANGELOG.md", + "EXTENDING.md", + "INSTALLATION.md", "LICENSE", "MANIFEST.in", + "NAMING_CANON_2.0.0.md", "PACKAGE_FILES.json", - "PACKAGE_IDENTITY.json", "PACKAGE_INSPECTION.md", "PUBLIC_API.json", "README.md", "RELEASE_NOTES_NEXT_MAJOR.md", + "RiskLabAI/__init__.py", + "RiskLabAI/_deprecation.py", + "RiskLabAI/backtest/__init__.py", + "RiskLabAI/backtest/advanced_bet_sizing.py", + "RiskLabAI/backtest/backtest_overfitting_simulation.py", + "RiskLabAI/backtest/backtest_statistics.py", + "RiskLabAI/backtest/backtest_synthetic_data.py", + "RiskLabAI/backtest/bet_sizing.py", + "RiskLabAI/backtest/multiple_testing.py", + "RiskLabAI/backtest/ou_trading_rules.py", + "RiskLabAI/backtest/probabilistic_sharpe_ratio.py", + "RiskLabAI/backtest/probability_of_backtest_overfitting.py", + "RiskLabAI/backtest/robust_statistics.py", + "RiskLabAI/backtest/sharpe_inference.py", + "RiskLabAI/backtest/strategy_risk.py", + "RiskLabAI/backtest/test_set_overfitting.py", + "RiskLabAI/backtest/validation/__init__.py", + "RiskLabAI/backtest/validation/adaptive_combinatorial_purged.py", + "RiskLabAI/backtest/validation/bagged_combinatorial_purged.py", + "RiskLabAI/backtest/validation/combinatorial_purged.py", + "RiskLabAI/backtest/validation/cross_validator_controller.py", + "RiskLabAI/backtest/validation/cross_validator_factory.py", + "RiskLabAI/backtest/validation/cross_validator_interface.py", + "RiskLabAI/backtest/validation/kfold.py", + "RiskLabAI/backtest/validation/leakage_aware_hpo.py", + "RiskLabAI/backtest/validation/path_adaptive_cpcv.py", + "RiskLabAI/backtest/validation/path_bagged_cpcv.py", + "RiskLabAI/backtest/validation/purged_kfold.py", + "RiskLabAI/backtest/validation/walk_forward.py", + "RiskLabAI/cluster/__init__.py", + "RiskLabAI/cluster/clustering.py", + "RiskLabAI/controller/__init__.py", + "RiskLabAI/controller/bars_initializer.py", + "RiskLabAI/controller/data_structure_controller.py", + "RiskLabAI/core/__init__.py", + "RiskLabAI/core/_builtins.py", + "RiskLabAI/core/base.py", + "RiskLabAI/core/registry.py", + "RiskLabAI/data/__init__.py", + "RiskLabAI/data/denoise/__init__.py", + "RiskLabAI/data/denoise/denoising.py", + "RiskLabAI/data/denoise/nercome.py", + "RiskLabAI/data/differentiation/__init__.py", + "RiskLabAI/data/differentiation/adaptive_differentiation.py", + "RiskLabAI/data/differentiation/differentiation.py", + "RiskLabAI/data/distance/__init__.py", + "RiskLabAI/data/distance/codependence.py", + "RiskLabAI/data/distance/distance_metric.py", + "RiskLabAI/data/labeling/__init__.py", + "RiskLabAI/data/labeling/financial_labels.py", + "RiskLabAI/data/labeling/labeling.py", + "RiskLabAI/data/structures/__init__.py", + "RiskLabAI/data/structures/abstract_bars.py", + "RiskLabAI/data/structures/abstract_imbalance_bars.py", + "RiskLabAI/data/structures/abstract_information_driven_bars.py", + "RiskLabAI/data/structures/abstract_run_bars.py", + "RiskLabAI/data/structures/hardened_information_driven_bars.py", + "RiskLabAI/data/structures/imbalance_bars.py", + "RiskLabAI/data/structures/run_bars.py", + "RiskLabAI/data/structures/standard_bars.py", + "RiskLabAI/data/structures/time_bars.py", + "RiskLabAI/data/synthetic_data/__init__.py", + "RiskLabAI/data/synthetic_data/drift_burst_hypothesis.py", + "RiskLabAI/data/synthetic_data/simulation.py", + "RiskLabAI/data/synthetic_data/synthetic_controlled_environment.py", + "RiskLabAI/data/weights/__init__.py", + "RiskLabAI/data/weights/sample_weights.py", + "RiskLabAI/ensemble/__init__.py", + "RiskLabAI/ensemble/bagging_classifier_accuracy.py", + "RiskLabAI/ensemble/empirical_bagging_accuracy.py", + "RiskLabAI/features/__init__.py", + "RiskLabAI/features/entropy_features/__init__.py", + "RiskLabAI/features/entropy_features/bias_corrected.py", + "RiskLabAI/features/entropy_features/kontoyiannis.py", + "RiskLabAI/features/entropy_features/lempel_ziv.py", + "RiskLabAI/features/entropy_features/plug_in.py", + "RiskLabAI/features/entropy_features/pmf.py", + "RiskLabAI/features/entropy_features/shannon.py", + "RiskLabAI/features/feature_importance/__init__.py", + "RiskLabAI/features/feature_importance/clustered_feature_importance_mda.py", + "RiskLabAI/features/feature_importance/clustered_feature_importance_mdi.py", + "RiskLabAI/features/feature_importance/debiased_importance.py", + "RiskLabAI/features/feature_importance/feature_importance_controller.py", + "RiskLabAI/features/feature_importance/feature_importance_factory.py", + "RiskLabAI/features/feature_importance/feature_importance_mda.py", + "RiskLabAI/features/feature_importance/feature_importance_mdi.py", + "RiskLabAI/features/feature_importance/feature_importance_sfi.py", + "RiskLabAI/features/feature_importance/feature_importance_strategy.py", + "RiskLabAI/features/feature_importance/generate_synthetic_data.py", + "RiskLabAI/features/feature_importance/orthogonal_features.py", + "RiskLabAI/features/feature_importance/weighted_tau.py", + "RiskLabAI/features/microstructural_features/__init__.py", + "RiskLabAI/features/microstructural_features/bekker_parkinson_volatility_estimator.py", + "RiskLabAI/features/microstructural_features/corwin_schultz.py", + "RiskLabAI/features/microstructural_features/edge.py", + "RiskLabAI/features/structural_breaks/__init__.py", + "RiskLabAI/features/structural_breaks/pelt_change_points.py", + "RiskLabAI/features/structural_breaks/structural_breaks.py", + "RiskLabAI/features/structural_breaks/volatility_robust_sadf.py", + "RiskLabAI/hpc/__init__.py", + "RiskLabAI/hpc/hpc.py", + "RiskLabAI/optimization/__init__.py", + "RiskLabAI/optimization/hedging.py", + "RiskLabAI/optimization/hrp.py", + "RiskLabAI/optimization/hyper_parameter_tuning.py", + "RiskLabAI/optimization/nco.py", + "RiskLabAI/pde/__init__.py", + "RiskLabAI/pde/equation.py", + "RiskLabAI/pde/model.py", + "RiskLabAI/pde/solver.py", + "RiskLabAI/utils/__init__.py", + "RiskLabAI/utils/constants.py", + "RiskLabAI/utils/ewma.py", + "RiskLabAI/utils/momentum_mean_reverting_strategy_sides.py", + "RiskLabAI/utils/progress.py", + "RiskLabAI/utils/publication_plots.py", + "RiskLabAI/utils/update_figure_layout.py", "TEST_INVENTORY.json", "docs/causal_factor_analysis.md", "docs/compatibility.md", + "docs/delete", + "documenter.py", "examples/causal_factor_analysis_quickstart.py", "pyproject.toml", "src/RiskLabAI/__init__.py", @@ -54,11 +179,15 @@ "src/RiskLabAI/backtest/validation/purged_kfold.py", "src/RiskLabAI/backtest/validation/walk_forward.py", "src/RiskLabAI/causal_factor_analysis/__init__.py", + "src/RiskLabAI/causal_factor_analysis/allocation_diagnostics.py", "src/RiskLabAI/causal_factor_analysis/factor_mirage.py", "src/RiskLabAI/causal_factor_analysis/graph_identification.py", + "src/RiskLabAI/causal_factor_analysis/graph_roles.py", "src/RiskLabAI/causal_factor_analysis/optimizer.py", "src/RiskLabAI/causal_factor_analysis/protocol.py", + "src/RiskLabAI/causal_factor_analysis/search_adjusted_fdr.py", "src/RiskLabAI/causal_factor_analysis/specification_diagnostics.py", + "src/RiskLabAI/causal_factor_analysis/structural_models.py", "src/RiskLabAI/causal_factor_analysis/treatment_effects.py", "src/RiskLabAI/cluster/__init__.py", "src/RiskLabAI/cluster/clustering.py", @@ -148,6 +277,7 @@ "src/RiskLabAI/utils/progress.py", "src/RiskLabAI/utils/publication_plots.py", "src/RiskLabAI/utils/update_figure_layout.py", + "style_guide.md", "test/backtest/test_advanced_bet_sizing.py", "test/backtest/test_backtest_overfitting_simulation.py", "test/backtest/test_backtest_statistics.py", @@ -173,12 +303,19 @@ "test/backtest/validation/test_path_bagged_cpcv.py", "test/backtest/validation/test_purged_kfold.py", "test/backtest/validation/test_walk_forward.py", + "test/causal_factor_analysis/fixtures/additive_numeric_parity.tsv", + "test/causal_factor_analysis/test_additive_numeric_parity.py", + "test/causal_factor_analysis/test_allocation_diagnostics.py", "test/causal_factor_analysis/test_cfi_book_graph_examples.py", "test/causal_factor_analysis/test_factor_mirage.py", + "test/causal_factor_analysis/test_generalized_factor_mirage.py", "test/causal_factor_analysis/test_graph_identification.py", + "test/causal_factor_analysis/test_graph_roles.py", "test/causal_factor_analysis/test_minimum_variance.py", "test/causal_factor_analysis/test_protocol.py", + "test/causal_factor_analysis/test_search_adjusted_fdr.py", "test/causal_factor_analysis/test_specification_diagnostics.py", + "test/causal_factor_analysis/test_structural_models.py", "test/causal_factor_analysis/test_treatment_effects.py", "test/cluster/test_clustering.py", "test/controller/test_bars_initializer.py", @@ -225,12 +362,229 @@ "test/test_performance.py", "test/utils/test_ewma.py", "test/utils/test_momentum_mean_reverting_strategy_sides.py", - "test/utils/test_progress.py" + "test/utils/test_progress.py", + "tree.py" ], - "local_control_files_excluded_from_artifacts": [ - "PACKAGE_IDENTITY.json" + "candidate_only_control_files": [], + "candidate_only_control_policy": "Analytical ledgers and verification records remain outside the candidate root.", + "baseline_active_release_files": [ + ".github/workflows/ci.yml", + "LICENSE", + "MANIFEST.in", + "PACKAGE_FILES.json", + "PACKAGE_INSPECTION.md", + "PUBLIC_API.json", + "README.md", + "RELEASE_NOTES_NEXT_MAJOR.md", + "TEST_INVENTORY.json", + "docs/causal_factor_analysis.md", + "docs/compatibility.md", + "examples/causal_factor_analysis_quickstart.py", + "pyproject.toml", + "src/RiskLabAI/__init__.py", + "src/RiskLabAI/_deprecation.py", + "src/RiskLabAI/backtest/__init__.py", + "src/RiskLabAI/backtest/advanced_bet_sizing.py", + "src/RiskLabAI/backtest/backtest_overfitting_simulation.py", + "src/RiskLabAI/backtest/backtest_statistics.py", + "src/RiskLabAI/backtest/backtest_synthetic_data.py", + "src/RiskLabAI/backtest/bet_sizing.py", + "src/RiskLabAI/backtest/multiple_testing.py", + "src/RiskLabAI/backtest/ou_trading_rules.py", + "src/RiskLabAI/backtest/probabilistic_sharpe_ratio.py", + "src/RiskLabAI/backtest/probability_of_backtest_overfitting.py", + "src/RiskLabAI/backtest/robust_statistics.py", + "src/RiskLabAI/backtest/sharpe_inference.py", + "src/RiskLabAI/backtest/strategy_risk.py", + "src/RiskLabAI/backtest/test_set_overfitting.py", + "src/RiskLabAI/backtest/validation/__init__.py", + "src/RiskLabAI/backtest/validation/adaptive_combinatorial_purged.py", + "src/RiskLabAI/backtest/validation/bagged_combinatorial_purged.py", + "src/RiskLabAI/backtest/validation/combinatorial_purged.py", + "src/RiskLabAI/backtest/validation/cross_validator_controller.py", + "src/RiskLabAI/backtest/validation/cross_validator_factory.py", + "src/RiskLabAI/backtest/validation/cross_validator_interface.py", + "src/RiskLabAI/backtest/validation/kfold.py", + "src/RiskLabAI/backtest/validation/leakage_aware_hpo.py", + "src/RiskLabAI/backtest/validation/path_adaptive_cpcv.py", + "src/RiskLabAI/backtest/validation/path_bagged_cpcv.py", + "src/RiskLabAI/backtest/validation/purged_kfold.py", + "src/RiskLabAI/backtest/validation/walk_forward.py", + "src/RiskLabAI/causal_factor_analysis/__init__.py", + "src/RiskLabAI/causal_factor_analysis/factor_mirage.py", + "src/RiskLabAI/causal_factor_analysis/graph_identification.py", + "src/RiskLabAI/causal_factor_analysis/optimizer.py", + "src/RiskLabAI/causal_factor_analysis/protocol.py", + "src/RiskLabAI/causal_factor_analysis/specification_diagnostics.py", + "src/RiskLabAI/causal_factor_analysis/treatment_effects.py", + "src/RiskLabAI/cluster/__init__.py", + "src/RiskLabAI/cluster/clustering.py", + "src/RiskLabAI/controller/__init__.py", + "src/RiskLabAI/controller/bars_initializer.py", + "src/RiskLabAI/controller/data_structure_controller.py", + "src/RiskLabAI/core/__init__.py", + "src/RiskLabAI/core/_builtins.py", + "src/RiskLabAI/core/base.py", + "src/RiskLabAI/core/registry.py", + "src/RiskLabAI/data/__init__.py", + "src/RiskLabAI/data/denoise/__init__.py", + "src/RiskLabAI/data/denoise/denoising.py", + "src/RiskLabAI/data/denoise/nercome.py", + "src/RiskLabAI/data/differentiation/__init__.py", + "src/RiskLabAI/data/differentiation/adaptive_differentiation.py", + "src/RiskLabAI/data/differentiation/differentiation.py", + "src/RiskLabAI/data/distance/__init__.py", + "src/RiskLabAI/data/distance/codependence.py", + "src/RiskLabAI/data/distance/distance_metric.py", + "src/RiskLabAI/data/labeling/__init__.py", + "src/RiskLabAI/data/labeling/financial_labels.py", + "src/RiskLabAI/data/labeling/labeling.py", + "src/RiskLabAI/data/structures/__init__.py", + "src/RiskLabAI/data/structures/abstract_bars.py", + "src/RiskLabAI/data/structures/abstract_imbalance_bars.py", + "src/RiskLabAI/data/structures/abstract_information_driven_bars.py", + "src/RiskLabAI/data/structures/abstract_run_bars.py", + "src/RiskLabAI/data/structures/hardened_information_driven_bars.py", + "src/RiskLabAI/data/structures/imbalance_bars.py", + "src/RiskLabAI/data/structures/run_bars.py", + "src/RiskLabAI/data/structures/standard_bars.py", + "src/RiskLabAI/data/structures/time_bars.py", + "src/RiskLabAI/data/synthetic_data/__init__.py", + "src/RiskLabAI/data/synthetic_data/drift_burst_hypothesis.py", + "src/RiskLabAI/data/synthetic_data/simulation.py", + "src/RiskLabAI/data/synthetic_data/synthetic_controlled_environment.py", + "src/RiskLabAI/data/weights/__init__.py", + "src/RiskLabAI/data/weights/sample_weights.py", + "src/RiskLabAI/ensemble/__init__.py", + "src/RiskLabAI/ensemble/bagging_classifier_accuracy.py", + "src/RiskLabAI/ensemble/empirical_bagging_accuracy.py", + "src/RiskLabAI/features/__init__.py", + "src/RiskLabAI/features/entropy_features/__init__.py", + "src/RiskLabAI/features/entropy_features/bias_corrected.py", + "src/RiskLabAI/features/entropy_features/kontoyiannis.py", + "src/RiskLabAI/features/entropy_features/lempel_ziv.py", + "src/RiskLabAI/features/entropy_features/plug_in.py", + "src/RiskLabAI/features/entropy_features/pmf.py", + "src/RiskLabAI/features/entropy_features/shannon.py", + "src/RiskLabAI/features/feature_importance/__init__.py", + "src/RiskLabAI/features/feature_importance/clustered_feature_importance_mda.py", + "src/RiskLabAI/features/feature_importance/clustered_feature_importance_mdi.py", + "src/RiskLabAI/features/feature_importance/debiased_importance.py", + "src/RiskLabAI/features/feature_importance/feature_importance_controller.py", + "src/RiskLabAI/features/feature_importance/feature_importance_factory.py", + "src/RiskLabAI/features/feature_importance/feature_importance_mda.py", + "src/RiskLabAI/features/feature_importance/feature_importance_mdi.py", + "src/RiskLabAI/features/feature_importance/feature_importance_sfi.py", + "src/RiskLabAI/features/feature_importance/feature_importance_strategy.py", + "src/RiskLabAI/features/feature_importance/generate_synthetic_data.py", + "src/RiskLabAI/features/feature_importance/orthogonal_features.py", + "src/RiskLabAI/features/feature_importance/weighted_tau.py", + "src/RiskLabAI/features/microstructural_features/__init__.py", + "src/RiskLabAI/features/microstructural_features/bekker_parkinson_volatility_estimator.py", + "src/RiskLabAI/features/microstructural_features/corwin_schultz.py", + "src/RiskLabAI/features/microstructural_features/edge.py", + "src/RiskLabAI/features/structural_breaks/__init__.py", + "src/RiskLabAI/features/structural_breaks/pelt_change_points.py", + "src/RiskLabAI/features/structural_breaks/structural_breaks.py", + "src/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py", + "src/RiskLabAI/hpc/__init__.py", + "src/RiskLabAI/hpc/hpc.py", + "src/RiskLabAI/optimization/__init__.py", + "src/RiskLabAI/optimization/hedging.py", + "src/RiskLabAI/optimization/hrp.py", + "src/RiskLabAI/optimization/hyper_parameter_tuning.py", + "src/RiskLabAI/optimization/nco.py", + "src/RiskLabAI/pde/__init__.py", + "src/RiskLabAI/pde/equation.py", + "src/RiskLabAI/pde/model.py", + "src/RiskLabAI/pde/solver.py", + "src/RiskLabAI/utils/__init__.py", + "src/RiskLabAI/utils/constants.py", + "src/RiskLabAI/utils/ewma.py", + "src/RiskLabAI/utils/momentum_mean_reverting_strategy_sides.py", + "src/RiskLabAI/utils/progress.py", + "src/RiskLabAI/utils/publication_plots.py", + "src/RiskLabAI/utils/update_figure_layout.py", + "test/backtest/test_advanced_bet_sizing.py", + "test/backtest/test_backtest_overfitting_simulation.py", + "test/backtest/test_backtest_statistics.py", + "test/backtest/test_backtest_synthetic_data.py", + "test/backtest/test_bet_sizing.py", + "test/backtest/test_multiple_testing.py", + "test/backtest/test_ou_trading_rules.py", + "test/backtest/test_probabilistic_sharpe_ratio.py", + "test/backtest/test_probability_of_backtest_overfitting.py", + "test/backtest/test_robust_statistics.py", + "test/backtest/test_sharpe_inference.py", + "test/backtest/test_strategy_risk.py", + "test/backtest/test_technical_indicators.py", + "test/backtest/test_test_set_overfitting.py", + "test/backtest/validation/test_adaptive_combinatorial_purged.py", + "test/backtest/validation/test_bagged_combinatorial_purged.py", + "test/backtest/validation/test_combinatorial_purged.py", + "test/backtest/validation/test_cross_validator_controller.py", + "test/backtest/validation/test_cross_validator_factory.py", + "test/backtest/validation/test_kfold.py", + "test/backtest/validation/test_leakage_aware_hpo.py", + "test/backtest/validation/test_path_adaptive_cpcv.py", + "test/backtest/validation/test_path_bagged_cpcv.py", + "test/backtest/validation/test_purged_kfold.py", + "test/backtest/validation/test_walk_forward.py", + "test/causal_factor_analysis/test_cfi_book_graph_examples.py", + "test/causal_factor_analysis/test_factor_mirage.py", + "test/causal_factor_analysis/test_graph_identification.py", + "test/causal_factor_analysis/test_minimum_variance.py", + "test/causal_factor_analysis/test_protocol.py", + "test/causal_factor_analysis/test_specification_diagnostics.py", + "test/causal_factor_analysis/test_treatment_effects.py", + "test/cluster/test_clustering.py", + "test/controller/test_bars_initializer.py", + "test/controller/test_data_structure_controller.py", + "test/core/test_base.py", + "test/core/test_builtin_parity.py", + "test/core/test_registry.py", + "test/data/denoise/test_denoising.py", + "test/data/denoise/test_nercome.py", + "test/data/differentiation/test_adaptive_differentiation.py", + "test/data/differentiation/test_differentiation.py", + "test/data/distance/test_codependence.py", + "test/data/distance/test_distance_metric.py", + "test/data/labeling/test_financial_labels.py", + "test/data/labeling/test_labeling.py", + "test/data/structures/test_hardened_information_driven_bars.py", + "test/data/structures/test_imbalance_bars.py", + "test/data/structures/test_run_bars.py", + "test/data/structures/test_standard_bars.py", + "test/data/structures/test_time_bars.py", + "test/data/synthetic_data/test_drift_burst_hypothesis.py", + "test/data/synthetic_data/test_synthetic_controlled_environment.py", + "test/data/weights/test_sample_weights.py", + "test/ensemble/test_bagging_classifier_accuracy.py", + "test/features/entropy_features/test_entropy.py", + "test/features/feature_importance/test_debiased_importance.py", + "test/features/feature_importance/test_feature_importance.py", + "test/features/feature_importance/test_generate_synthetic_data.py", + "test/features/feature_importance/test_orthogonal_features.py", + "test/features/feature_importance/test_weighted_tau.py", + "test/features/microstructural_features/test_edge.py", + "test/features/microstructural_features/test_microstructure.py", + "test/features/structural_breaks/test_pelt_change_points.py", + "test/features/structural_breaks/test_structural_breaks.py", + "test/features/structural_breaks/test_volatility_robust_sadf.py", + "test/hpc/test_hpc.py", + "test/optimization/test_hedging.py", + "test/optimization/test_hrp.py", + "test/optimization/test_hyper_parameter_tuning.py", + "test/optimization/test_nco.py", + "test/pde/test_pde_solver.py", + "test/test_consolidation.py", + "test/test_deprecation.py", + "test/test_performance.py", + "test/utils/test_ewma.py", + "test/utils/test_momentum_mean_reverting_strategy_sides.py", + "test/utils/test_progress.py" ], - "release_source_file_count": 215, + "release_source_file_count": 226, "release_source_files": [ ".github/workflows/ci.yml", "LICENSE", @@ -275,11 +629,15 @@ "src/RiskLabAI/backtest/validation/purged_kfold.py", "src/RiskLabAI/backtest/validation/walk_forward.py", "src/RiskLabAI/causal_factor_analysis/__init__.py", + "src/RiskLabAI/causal_factor_analysis/allocation_diagnostics.py", "src/RiskLabAI/causal_factor_analysis/factor_mirage.py", "src/RiskLabAI/causal_factor_analysis/graph_identification.py", + "src/RiskLabAI/causal_factor_analysis/graph_roles.py", "src/RiskLabAI/causal_factor_analysis/optimizer.py", "src/RiskLabAI/causal_factor_analysis/protocol.py", + "src/RiskLabAI/causal_factor_analysis/search_adjusted_fdr.py", "src/RiskLabAI/causal_factor_analysis/specification_diagnostics.py", + "src/RiskLabAI/causal_factor_analysis/structural_models.py", "src/RiskLabAI/causal_factor_analysis/treatment_effects.py", "src/RiskLabAI/cluster/__init__.py", "src/RiskLabAI/cluster/clustering.py", @@ -394,12 +752,19 @@ "test/backtest/validation/test_path_bagged_cpcv.py", "test/backtest/validation/test_purged_kfold.py", "test/backtest/validation/test_walk_forward.py", + "test/causal_factor_analysis/fixtures/additive_numeric_parity.tsv", + "test/causal_factor_analysis/test_additive_numeric_parity.py", + "test/causal_factor_analysis/test_allocation_diagnostics.py", "test/causal_factor_analysis/test_cfi_book_graph_examples.py", "test/causal_factor_analysis/test_factor_mirage.py", + "test/causal_factor_analysis/test_generalized_factor_mirage.py", "test/causal_factor_analysis/test_graph_identification.py", + "test/causal_factor_analysis/test_graph_roles.py", "test/causal_factor_analysis/test_minimum_variance.py", "test/causal_factor_analysis/test_protocol.py", + "test/causal_factor_analysis/test_search_adjusted_fdr.py", "test/causal_factor_analysis/test_specification_diagnostics.py", + "test/causal_factor_analysis/test_structural_models.py", "test/causal_factor_analysis/test_treatment_effects.py", "test/cluster/test_clustering.py", "test/controller/test_bars_initializer.py", @@ -448,7 +813,1906 @@ "test/utils/test_momentum_mean_reverting_strategy_sides.py", "test/utils/test_progress.py" ], - "runtime_source_file_count": 124, + "release_file_list_sha256": "4F54899CA32A5EDFC37C774D001A3D902FD5AA952FDF5320F4C623FA5244CA5C", + "release_file_records_excluding_self": [ + { + "path": ".github/workflows/ci.yml", + "size": 4352, + "sha256": "BF972FE1D397A2FCEFE7ED097F749A27D6194D71F8EF83D1662959B05B94ED78" + }, + { + "path": "LICENSE", + "size": 1524, + "sha256": "C218B2652D7FC88A70415780B13ABC03BDCE419197CBA3C47FBBAFEF396686EA" + }, + { + "path": "MANIFEST.in", + "size": 435, + "sha256": "D54C377EA2FE5E079FBEFD9E61776CCED83627AAA891D3E3268C7DC2F901AF16" + }, + { + "path": "PACKAGE_INSPECTION.md", + "size": 3799, + "sha256": "D2FE6D170C5DC6ECD5118642A05D15342F11B6877D6B53A7456DC3BB1240F044" + }, + { + "path": "PUBLIC_API.json", + "size": 200159, + "sha256": "4DAB507FFCF205F0181ED87D9F76063A6CEA7E4265ACF95E28226AE48D061A5B" + }, + { + "path": "README.md", + "size": 6615, + "sha256": "09D70FA159B02A994D74775BA695BAECF2B9253EAA3ED0665F4E9AE484032273" + }, + { + "path": "RELEASE_NOTES_NEXT_MAJOR.md", + "size": 2403, + "sha256": "F19797FFBB789C6861DE2D2F3C4620CDD3BD36439A981C675EC73F7FCAD8E262" + }, + { + "path": "TEST_INVENTORY.json", + "size": 22570, + "sha256": "2E2EF942FE59EBE41A75C55532F3E8012657EFDCA6C9931870B77C34A94D58FE" + }, + { + "path": "docs/causal_factor_analysis.md", + "size": 9249, + "sha256": "06EA68B7D9F21808025877F76A3C74CAE12B510BD99AAE69077CBCB82B5E4E9E" + }, + { + "path": "docs/compatibility.md", + "size": 6667, + "sha256": "12F5CEA996314FA8D96383BFB73B59C8B9231C26359DC76292C6F58B6BDF51FD" + }, + { + "path": "examples/causal_factor_analysis_quickstart.py", + "size": 1932, + "sha256": "9C091DB31054D145AFDF6D52AE11DA38C569C59A530FFD7B5D9CEB1B959073D8" + }, + { + "path": "pyproject.toml", + "size": 2878, + "sha256": "7691CCCB4F26525405F7ED9AC90CC76625DD8DA9E06401DEB7C69644BF8720F2" + }, + { + "path": "src/RiskLabAI/__init__.py", + "size": 1095, + "sha256": "1CB2A7E2B804267FFEBE4FF124F01AA095DC19F82492DB862C9A4D97C2A1AF0F" + }, + { + "path": "src/RiskLabAI/_deprecation.py", + "size": 3889, + "sha256": "F44F03C24759B46E69B0BD0241B51FF34454A9AAA1CD9A0498364284D5FB9498" + }, + { + "path": "src/RiskLabAI/backtest/__init__.py", + "size": 6204, + "sha256": "59475AA703033C284FED55C308AB258B79AC6E31A8EE5164B01AF6E9396E4C8C" + }, + { + "path": "src/RiskLabAI/backtest/advanced_bet_sizing.py", + "size": 10098, + "sha256": "1D90B97962BED3711265488A7921C651585FBF0D366A412607944EAF44B9C810" + }, + { + "path": "src/RiskLabAI/backtest/backtest_overfitting_simulation.py", + "size": 61737, + "sha256": "F435D84F28A7ACFB3237D329BE0FF82B7F60B1729810E4B8936A324CAA8EF595" + }, + { + "path": "src/RiskLabAI/backtest/backtest_statistics.py", + "size": 9157, + "sha256": "D6365C332C46BE6E6A176B39D3CC70DA170301362A974609613FA370D710E643" + }, + { + "path": "src/RiskLabAI/backtest/backtest_synthetic_data.py", + "size": 3424, + "sha256": "F67B8239A8E5F9EF5BA73F84938614F211C71197CD87AAFCFE10A8EDEBABC627" + }, + { + "path": "src/RiskLabAI/backtest/bet_sizing.py", + "size": 15577, + "sha256": "40704B327DF86A3901B684D6085DDC503947B5641E081BFCBB9B80CBBE1BE889" + }, + { + "path": "src/RiskLabAI/backtest/multiple_testing.py", + "size": 7855, + "sha256": "FAA5E3C9E225869FC204BB5DA4C88830A81989A18FC927706FBC3BCCDEE7936B" + }, + { + "path": "src/RiskLabAI/backtest/ou_trading_rules.py", + "size": 12573, + "sha256": "A202998DC64A9C3F68544AFFF3DA48798C99C4B19A3B9600F5E1030247551182" + }, + { + "path": "src/RiskLabAI/backtest/probabilistic_sharpe_ratio.py", + "size": 4538, + "sha256": "3CD56F7C02E0D42CE04E395737DB6039FAEA617B367E6BFBC2F0A74768FA7B49" + }, + { + "path": "src/RiskLabAI/backtest/probability_of_backtest_overfitting.py", + "size": 5085, + "sha256": "C14BB1E1179AAC1D61D56ACF6BF282BF2B9593B143A43E357DC7D9181C7DA373" + }, + { + "path": "src/RiskLabAI/backtest/robust_statistics.py", + "size": 11911, + "sha256": "9432CBA7E36A8C1F9BA69D6F912D226FB78FEC4069E940E8380EFB1543696974" + }, + { + "path": "src/RiskLabAI/backtest/sharpe_inference.py", + "size": 7721, + "sha256": "58A436922785F2D087FC6C40C1881228E84AAB7BC96B4D8C2C0161FA2F05E0A4" + }, + { + "path": "src/RiskLabAI/backtest/strategy_risk.py", + "size": 10670, + "sha256": "00D0E6574F81E68051A95CD762158E7BA52BB8109D8E47FB723C02C06C476447" + }, + { + "path": "src/RiskLabAI/backtest/test_set_overfitting.py", + "size": 8865, + "sha256": "2C813D80E85ED2717F4C3B991244B23DEE1253807C8FFA9EA71A36F37E9DDED0" + }, + { + "path": "src/RiskLabAI/backtest/validation/__init__.py", + "size": 1925, + "sha256": "18D99DF58743C28835ACD00F7B307014D919C5FC322D6A639AE126E44AFDFFB5" + }, + { + "path": "src/RiskLabAI/backtest/validation/adaptive_combinatorial_purged.py", + "size": 18395, + "sha256": "B466BD0C43297F0654467E1A91B135CCB7958CD856A1A45BAFBCDBE372F7FAA0" + }, + { + "path": "src/RiskLabAI/backtest/validation/bagged_combinatorial_purged.py", + "size": 9472, + "sha256": "A881EDEBB301C8C5184248AA37EDCCE94159A9314BA5369026E0C423C5C73B12" + }, + { + "path": "src/RiskLabAI/backtest/validation/combinatorial_purged.py", + "size": 14461, + "sha256": "8CC827C13CE4DE02A70C5A942A663CC2B704BD0EF5B251CDE78F59CDB9F375DB" + }, + { + "path": "src/RiskLabAI/backtest/validation/cross_validator_controller.py", + "size": 1233, + "sha256": "FE635E694640176AA96872AAE26046E4707C0F5FBE8F8AE79E2197BC0A4D6C51" + }, + { + "path": "src/RiskLabAI/backtest/validation/cross_validator_factory.py", + "size": 2650, + "sha256": "EAEECAE7A1F73789F72BA9E5FBF03FE48CC20487AAFF5404613B82F41DE96C77" + }, + { + "path": "src/RiskLabAI/backtest/validation/cross_validator_interface.py", + "size": 7967, + "sha256": "A6FB69106047B94AF0E7ECCDE88FBDCCB5178BE08ED8EE8E03B04564F8E50A7E" + }, + { + "path": "src/RiskLabAI/backtest/validation/kfold.py", + "size": 12431, + "sha256": "BBA92A01B9EA320C89DB7CEBFED147EDBDDAE9CB7FE2D550FF2F2ECFE7ACC15B" + }, + { + "path": "src/RiskLabAI/backtest/validation/leakage_aware_hpo.py", + "size": 9930, + "sha256": "FF0040F22C4710918CD39CF3A08BC48293ED48A70A8F46898B3C4686122B6D2C" + }, + { + "path": "src/RiskLabAI/backtest/validation/path_adaptive_cpcv.py", + "size": 10437, + "sha256": "4257BE918DA88233CE4ABA8AD2A0BAB2859C9B711AE1A591ADABDA33C7312668" + }, + { + "path": "src/RiskLabAI/backtest/validation/path_bagged_cpcv.py", + "size": 5699, + "sha256": "195191CEDCC24386D2862C886EA323E86F9B74371379B5C825CB9EFB069382A2" + }, + { + "path": "src/RiskLabAI/backtest/validation/purged_kfold.py", + "size": 19267, + "sha256": "8E2385391624CFB31E3B9C97B0D8E27B774E1985DF6ED5997F59A1D49ACFF902" + }, + { + "path": "src/RiskLabAI/backtest/validation/walk_forward.py", + "size": 6474, + "sha256": "47C0786EAF7B5F4D774B24537ADEEDC9E80AAC3604FA3D14C52A17864FEC9B7A" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/__init__.py", + "size": 6656, + "sha256": "96B6E483A72D5CBD00B5EDC5762345E7D31F31F6255B91C793C00B9B2BE62CA4" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/allocation_diagnostics.py", + "size": 11041, + "sha256": "089677E3EF24979A421676BD4253EA684CB6819D7D6405FB78A1575F2F47C35C" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/factor_mirage.py", + "size": 18292, + "sha256": "57D44C3EF8608CD9A6E917589E3B6BD37A2DDAF300A7BF983EFBD8206EF2AC6D" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/graph_identification.py", + "size": 37121, + "sha256": "E34CED76FDA172EB688B7F45D610F3297332AE43D24639860325CE38E0A180BE" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/graph_roles.py", + "size": 6324, + "sha256": "415086CF1403FB0EE955184F224C9B11ACC7047943D70A39AB969A1C0CD793AD" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/optimizer.py", + "size": 8835, + "sha256": "C83DFD2630E82E3D31D45E47E3895FD8926CB2FC67A66842C4C25BDEEF2379A5" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/protocol.py", + "size": 64712, + "sha256": "97A78E100A01192A9C7511DC8C18CF54300F46BF36187A87E3FF607A5C91011B" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/search_adjusted_fdr.py", + "size": 28380, + "sha256": "ADA0F9B213BC90CC3B313EAC8C58DCC50082A2992618C8EAEE99736A6DF5C972" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/specification_diagnostics.py", + "size": 9595, + "sha256": "5AF8F882F798FF9D0AC8F3955A9A2C9533589AA328B929030CF16930FA28FA65" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/structural_models.py", + "size": 6877, + "sha256": "CC69BC922ADF6A4292B637CF97E96B607903608529056C03F66E041107BE345D" + }, + { + "path": "src/RiskLabAI/causal_factor_analysis/treatment_effects.py", + "size": 11832, + "sha256": "B3D980B969803C12F0F3C5A6C6C55C37D844827131F45924F53E629F02A1E980" + }, + { + "path": "src/RiskLabAI/cluster/__init__.py", + "size": 642, + "sha256": "1313FCD1DFC97508FB0ECAE567B46BAF5CE9997748C302ABACD0AA4606D931B3" + }, + { + "path": "src/RiskLabAI/cluster/clustering.py", + "size": 14275, + "sha256": "C9ADF94D36C3B1257D33E0DBC7D38243C4876BF1921B72A9092FE81A745610B2" + }, + { + "path": "src/RiskLabAI/controller/__init__.py", + "size": 73, + "sha256": "0DED3F4FAB2B94562CC9337E7D1A170B6ACA95D4F4477C2A6730DF29ABE0A8FF" + }, + { + "path": "src/RiskLabAI/controller/bars_initializer.py", + "size": 13700, + "sha256": "6C45A2CACEB4BD9AAB6B08EA0EDD240E4144CF823FCE361C5297F2DC9FFF9022" + }, + { + "path": "src/RiskLabAI/controller/data_structure_controller.py", + "size": 5914, + "sha256": "4D4CAFBA0B0059A89398A1CD453F57A803E80466C93DC3AC4A0F11BB894DDF8C" + }, + { + "path": "src/RiskLabAI/core/__init__.py", + "size": 1933, + "sha256": "1F57377F2CDCD8958910376A2615F45A23FD96954FDE05FFBEDC8FDC20AD1DE8" + }, + { + "path": "src/RiskLabAI/core/_builtins.py", + "size": 7247, + "sha256": "5D754E23EF41F580D0A4B5CB635DFA95A5A1AB20E3A500B1D4B9F9A75EB333A7" + }, + { + "path": "src/RiskLabAI/core/base.py", + "size": 6270, + "sha256": "47F3B61B748629A7AFBB1209A701A8F6614F2474EA9919B298B4574D3E757C5E" + }, + { + "path": "src/RiskLabAI/core/registry.py", + "size": 14263, + "sha256": "2A48E47D33ED1E2EE074DEE4933C363884433226EB42CA4BFCA949E33AAC1016" + }, + { + "path": "src/RiskLabAI/data/__init__.py", + "size": 630, + "sha256": "BF1701C8581DC602A5D37E10BAE6CDB837D489BD8791781B5F50BA4D067876FD" + }, + { + "path": "src/RiskLabAI/data/denoise/__init__.py", + "size": 854, + "sha256": "DE4D01EA8A0323F1FC39DA81B487F5770D77B4D08DB6BE2F127638B5592CB7EE" + }, + { + "path": "src/RiskLabAI/data/denoise/denoising.py", + "size": 15138, + "sha256": "4F78D615A8B429309E2840AFBE1429D6340231F5DE73BEAA93C4741545AAA06E" + }, + { + "path": "src/RiskLabAI/data/denoise/nercome.py", + "size": 6469, + "sha256": "AA11FB31EBE08631B7D979562E1A568E49B70FE9FD2CBA1304EA12908AD434B8" + }, + { + "path": "src/RiskLabAI/data/differentiation/__init__.py", + "size": 196, + "sha256": "51D73513B8108EF8835E5113E337F336DD62CBC4006BBBD486B50623401B670B" + }, + { + "path": "src/RiskLabAI/data/differentiation/adaptive_differentiation.py", + "size": 11222, + "sha256": "CB0DF7A28212D4CE0B15AF7023235B6AABD0EA9077D64572F3E5EB45D9A6A066" + }, + { + "path": "src/RiskLabAI/data/differentiation/differentiation.py", + "size": 11859, + "sha256": "4E9E45C90AEB0B8949E5F6F774D1AF20AA5973188F5FD23FFB4344E6EF5C5E9F" + }, + { + "path": "src/RiskLabAI/data/distance/__init__.py", + "size": 102, + "sha256": "5BA3546F05199F587C42C2C8A4E80B541616EA3F80A93654B7F2E8D8F548F296" + }, + { + "path": "src/RiskLabAI/data/distance/codependence.py", + "size": 7268, + "sha256": "9033470188D780110795D53CEFD230BC3D5AE54BD8BA945908CB73F3C1B48B9A" + }, + { + "path": "src/RiskLabAI/data/distance/distance_metric.py", + "size": 8073, + "sha256": "89073FAA39E9FF195C7028BEA970410562745534AE52AAFD310C5499E4DC914D" + }, + { + "path": "src/RiskLabAI/data/labeling/__init__.py", + "size": 1144, + "sha256": "CBDD997697143DFFD4287A26E568C257FDE2634BDBA325955438353D65DC86F7" + }, + { + "path": "src/RiskLabAI/data/labeling/financial_labels.py", + "size": 4424, + "sha256": "A2471BEAFFB28F2339D05DAED1811C4384C702CC0602638230D0FC03A4BE6A9C" + }, + { + "path": "src/RiskLabAI/data/labeling/labeling.py", + "size": 14680, + "sha256": "36687544C68B3016B5E4CFDE12ADE104971F45F79D4AC9FEB99BEB298F8E1418" + }, + { + "path": "src/RiskLabAI/data/structures/__init__.py", + "size": 883, + "sha256": "051BD4E2CF067134B61FC3CCEA16A9F20B94676F5C3D0DD01041F28541132956" + }, + { + "path": "src/RiskLabAI/data/structures/abstract_bars.py", + "size": 7436, + "sha256": "F775F93AE69A634E22EA324A0AB8603E729786CB72A684741D2111E090DA6014" + }, + { + "path": "src/RiskLabAI/data/structures/abstract_imbalance_bars.py", + "size": 6210, + "sha256": "E7A54B451B412FD1F2463E59E8706131A1D7AD8C84D3FE6CC1E2B5A3DC8B1B99" + }, + { + "path": "src/RiskLabAI/data/structures/abstract_information_driven_bars.py", + "size": 4021, + "sha256": "8601CD7E405A10F66BF6A82192E8EA261F9D71CA2A8C85E48FFA334EBC00D8DF" + }, + { + "path": "src/RiskLabAI/data/structures/abstract_run_bars.py", + "size": 9943, + "sha256": "1B7E12ED9C1B2505F014FE5487CF1AFC938463198C8B40F7360ED1739E035EFD" + }, + { + "path": "src/RiskLabAI/data/structures/hardened_information_driven_bars.py", + "size": 6880, + "sha256": "E5CF32132DADEF61624F24E2AC42E673C0B4B274DC317DD6488539A3FC0F1741" + }, + { + "path": "src/RiskLabAI/data/structures/imbalance_bars.py", + "size": 4741, + "sha256": "D355A01123732210788FAB14EDC8D68ABC6FA4E1035AC5BD636BD11DD4E210AC" + }, + { + "path": "src/RiskLabAI/data/structures/run_bars.py", + "size": 4515, + "sha256": "7FE0F23DF5BE39B475073CA69FABD6FDE7126EC6E4289C2D84652D930D251900" + }, + { + "path": "src/RiskLabAI/data/structures/standard_bars.py", + "size": 2669, + "sha256": "C4142C10BEE79EF96E8A9DA31F1444EE062933D587630EB567E0A5CE3C81C362" + }, + { + "path": "src/RiskLabAI/data/structures/time_bars.py", + "size": 4544, + "sha256": "F9126A81DA595A4E1D87400164C09071833222CD9C1546D69164F643D0AC582F" + }, + { + "path": "src/RiskLabAI/data/synthetic_data/__init__.py", + "size": 857, + "sha256": "BE3B0444592D9B45C9439E57210EB713C761D4B21CE497609B2B63E955F70768" + }, + { + "path": "src/RiskLabAI/data/synthetic_data/drift_burst_hypothesis.py", + "size": 3115, + "sha256": "3A613F5FE50555654556236EF409BC4F7FB83252CDF3502CA60D46EB29F4BC4C" + }, + { + "path": "src/RiskLabAI/data/synthetic_data/simulation.py", + "size": 3392, + "sha256": "09394D81D758211140D894B5CB4CB6289EACEC862638006C8A7D9A79FD16DC55" + }, + { + "path": "src/RiskLabAI/data/synthetic_data/synthetic_controlled_environment.py", + "size": 12391, + "sha256": "6D5DFCF4C4430A7BAE367A79CB156D3D6DF3D24457C1D60B68A1A4D556CF4AD4" + }, + { + "path": "src/RiskLabAI/data/weights/__init__.py", + "size": 621, + "sha256": "7275D7101AE1F1DC977EF7B6C53819139AC9F46F629742178AA0AA453390A523" + }, + { + "path": "src/RiskLabAI/data/weights/sample_weights.py", + "size": 6991, + "sha256": "4DA73877840EF49BDFF35B087CE1FC52354D14E9136217D16B1948C5577A9BB4" + }, + { + "path": "src/RiskLabAI/ensemble/__init__.py", + "size": 707, + "sha256": "2301547AD658B8DD96941FD1098FBDA9AC3464E487F2A9F1811C6BD656F8F46A" + }, + { + "path": "src/RiskLabAI/ensemble/bagging_classifier_accuracy.py", + "size": 1255, + "sha256": "9B2150DA7DB358161927137E5E2ED9C3FA0249FF71E86401F29C9F96258B839C" + }, + { + "path": "src/RiskLabAI/ensemble/empirical_bagging_accuracy.py", + "size": 11622, + "sha256": "E679640BE29A4986F0E67AF7F5A060894A72EDCD581C3F2B059D9C4177B7415F" + }, + { + "path": "src/RiskLabAI/features/__init__.py", + "size": 117, + "sha256": "CE30ECC88CBFF140893C4FADFBE38DC90EE954024126EF66F1A5200AE0146C2B" + }, + { + "path": "src/RiskLabAI/features/entropy_features/__init__.py", + "size": 883, + "sha256": "2DB4F80E1037C4562E8034FF3120C8DF41DE54D85C7F8E1CF04628BBA9A52374" + }, + { + "path": "src/RiskLabAI/features/entropy_features/bias_corrected.py", + "size": 12893, + "sha256": "40B156C643E48DAE6F06440D9FD1BE2ED651F18FA8C7A2BD0DB569795E44DCFE" + }, + { + "path": "src/RiskLabAI/features/entropy_features/kontoyiannis.py", + "size": 2793, + "sha256": "A69F1FEB3DAC762724F5ED9F17BCAD0E9D9EC88B5946348FA80EA4059AD04119" + }, + { + "path": "src/RiskLabAI/features/entropy_features/lempel_ziv.py", + "size": 976, + "sha256": "1F015F187028C1957CC91BC91D944B10202D7D3EF37F3CAAF29D4CFB238C8FF8" + }, + { + "path": "src/RiskLabAI/features/entropy_features/plug_in.py", + "size": 942, + "sha256": "799C9ABB1AD8A864BF15E5F1E3267C5FCB482B83C38A4F0FCF9E4C5F2C81B763" + }, + { + "path": "src/RiskLabAI/features/entropy_features/pmf.py", + "size": 1798, + "sha256": "55A1BA2FAC7BDC384C15E36B7874E01C0DD0B40D5D8FF6E6113972B81FED8D24" + }, + { + "path": "src/RiskLabAI/features/entropy_features/shannon.py", + "size": 689, + "sha256": "FBA55BE6A0B6E529ABD77BE038F5F885945B034A04DA042CFFD99A278735B37E" + }, + { + "path": "src/RiskLabAI/features/feature_importance/__init__.py", + "size": 2086, + "sha256": "E3BD3D4CDB85D31F9096C459F8468DF540D41D4CF619B73A9CBBFF4DFBB1A75D" + }, + { + "path": "src/RiskLabAI/features/feature_importance/clustered_feature_importance_mda.py", + "size": 5361, + "sha256": "7BF977CB4D65189BB478CAA8378D3094E7D0A8FD672C498AC3BD881FF0B818A6" + }, + { + "path": "src/RiskLabAI/features/feature_importance/clustered_feature_importance_mdi.py", + "size": 3403, + "sha256": "4B2B7A7F98BA4030A8B0B885C2D9077F22445F3D5B07FF470D1E247BF4346516" + }, + { + "path": "src/RiskLabAI/features/feature_importance/debiased_importance.py", + "size": 11396, + "sha256": "299068C70E8551367167F221055F52BF54848D048A6C5B12EAF9BAC08727045C" + }, + { + "path": "src/RiskLabAI/features/feature_importance/feature_importance_controller.py", + "size": 1399, + "sha256": "52907FFC70EE8654B29E2552FE03AD22D494DFFC9E4777021BE0E2A199E8966A" + }, + { + "path": "src/RiskLabAI/features/feature_importance/feature_importance_factory.py", + "size": 1620, + "sha256": "5D44788D9C58D531D4913E7C9D8E4C9BCEB2A634ED3CF05EF60D882EE1084A26" + }, + { + "path": "src/RiskLabAI/features/feature_importance/feature_importance_mda.py", + "size": 4515, + "sha256": "872D141D6D96B4CED9660710ACE3EFBFE4FCCB74065BEEA6FEF5344DF1A61861" + }, + { + "path": "src/RiskLabAI/features/feature_importance/feature_importance_mdi.py", + "size": 2651, + "sha256": "C201C6FA3986268AE49D7D2AFE177DBB468AA4E5D32CD9B9CA805DAEAA9EE5AE" + }, + { + "path": "src/RiskLabAI/features/feature_importance/feature_importance_sfi.py", + "size": 3865, + "sha256": "69ED5D5AEA7F977E72459492D278ED5A1A3B0B5A7099CED6C96D1B5EE26C4833" + }, + { + "path": "src/RiskLabAI/features/feature_importance/feature_importance_strategy.py", + "size": 857, + "sha256": "0B5BF2F9EA40AEF208FAF010308BE4E0AF32B9378B395A8B555CF2CB1D97CCAA" + }, + { + "path": "src/RiskLabAI/features/feature_importance/generate_synthetic_data.py", + "size": 2535, + "sha256": "67342708D90B9AC31BF31FF851E341D122600D7A75818F20CEFF31CB39E41747" + }, + { + "path": "src/RiskLabAI/features/feature_importance/orthogonal_features.py", + "size": 3140, + "sha256": "0250FFD2E9B7E2D1B5BFBF960D63EA32CBB0CD9335249222E37A66724B6B0FF9" + }, + { + "path": "src/RiskLabAI/features/feature_importance/weighted_tau.py", + "size": 974, + "sha256": "706F53A5531A3B5089A9A252883558C7561C9B1B0DB6B251904EE80A8961C098" + }, + { + "path": "src/RiskLabAI/features/microstructural_features/__init__.py", + "size": 708, + "sha256": "5A47422A191048EB40286C2BA1169BA3FF04396BFA08F9582439A26521A06353" + }, + { + "path": "src/RiskLabAI/features/microstructural_features/bekker_parkinson_volatility_estimator.py", + "size": 2058, + "sha256": "BC606B1E368BC738846DCA08DE31C99A0A42D91F0CF5F8BB686EDE14021821FF" + }, + { + "path": "src/RiskLabAI/features/microstructural_features/corwin_schultz.py", + "size": 3471, + "sha256": "9120D5D7F9763E017A896E5F7A3288F02112E1A72AA92C42717E6B2506070532" + }, + { + "path": "src/RiskLabAI/features/microstructural_features/edge.py", + "size": 6268, + "sha256": "98A0A232CDB6D1ECB2C268B0BBAFCACF23981CDE9D998D8936D7F2A3265C38D7" + }, + { + "path": "src/RiskLabAI/features/structural_breaks/__init__.py", + "size": 1134, + "sha256": "EC77F8B7B857C0898C23D9B3968EA957E4A281643644B7911B14E94F0057FA3D" + }, + { + "path": "src/RiskLabAI/features/structural_breaks/pelt_change_points.py", + "size": 4257, + "sha256": "7D84550BB9E246055031963755D99BC180793C92D839BFE4D29ECBB0EFC1D337" + }, + { + "path": "src/RiskLabAI/features/structural_breaks/structural_breaks.py", + "size": 25436, + "sha256": "F81089327CA345631BBBB943BD431E7F91B2AF30728946602C86B115C1759032" + }, + { + "path": "src/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py", + "size": 5957, + "sha256": "8DE6D754B46AC7BE870ABC0D44F92B7EC633255CF4B0D11E402579EF4187DD56" + }, + { + "path": "src/RiskLabAI/hpc/__init__.py", + "size": 511, + "sha256": "C08896EA7791033B286FAB38AFDBD8EC5A79F562316AA470DD071F90C6F51384" + }, + { + "path": "src/RiskLabAI/hpc/hpc.py", + "size": 11306, + "sha256": "CC6377F6E22F2BB25D87723DE0D6C36DEB555F4D08C19BDD4B55C64D5FABEED1" + }, + { + "path": "src/RiskLabAI/optimization/__init__.py", + "size": 1211, + "sha256": "8B178E855B01B7AAA4A6988060A26B794A9E4A867CF5E5F3125D1CBC0AE6900C" + }, + { + "path": "src/RiskLabAI/optimization/hedging.py", + "size": 1962, + "sha256": "4A87276BD9FBAF8726F8646E57C6B27E6274C3481692532A04F67CC0B206E149" + }, + { + "path": "src/RiskLabAI/optimization/hrp.py", + "size": 6108, + "sha256": "5DD1818F65CDCE5C07870D96353627600C325ADE1C4297AAB933AB5954538047" + }, + { + "path": "src/RiskLabAI/optimization/hyper_parameter_tuning.py", + "size": 6152, + "sha256": "DCAFAFB9A5541BA40FE48FC8DBDD57E804BBD9DC5C22430F7979B0080AD81D29" + }, + { + "path": "src/RiskLabAI/optimization/nco.py", + "size": 3992, + "sha256": "4FB1EE548E62CEF2BDD71E289F8F3DAD403F92AEE95820DA2BF1DA9409CAAA53" + }, + { + "path": "src/RiskLabAI/pde/__init__.py", + "size": 1833, + "sha256": "C7B95E7E7DF3A7BDD666D940EE776A5D8349ADE4CEE33D8FC453C7C56189C4CA" + }, + { + "path": "src/RiskLabAI/pde/equation.py", + "size": 10999, + "sha256": "4A9A073721FAAC4FFAC3513A61B08586027204248EE8A47F184E5F174DD86E06" + }, + { + "path": "src/RiskLabAI/pde/model.py", + "size": 12264, + "sha256": "98F8C874C57C04E28883BB9B4CFD526C0D1622E27C84F723E5883D30CBFF7001" + }, + { + "path": "src/RiskLabAI/pde/solver.py", + "size": 13859, + "sha256": "A7F2A65833339E5E9258F165B0AC0BA9F37130061F456C12465C81179B740756" + }, + { + "path": "src/RiskLabAI/utils/__init__.py", + "size": 3409, + "sha256": "DE1F4F4832425C9B8FE275BF48A9621ABA3525BB219F43C9D9E38DBBCEAE0582" + }, + { + "path": "src/RiskLabAI/utils/constants.py", + "size": 3003, + "sha256": "751B87152BF9EABFE4AE5BB9CF35F38FBD56E4FE2691BF52B749CC3DAFAACE5A" + }, + { + "path": "src/RiskLabAI/utils/ewma.py", + "size": 2118, + "sha256": "48306B7F06272DFBABFB0CFE2B147F232038884BFCE77FEE072583A8F8F6355B" + }, + { + "path": "src/RiskLabAI/utils/momentum_mean_reverting_strategy_sides.py", + "size": 2156, + "sha256": "F19E1691AD40DF94640AEFBFA06431B39538444139277568B8A6AE247FB48509" + }, + { + "path": "src/RiskLabAI/utils/progress.py", + "size": 1945, + "sha256": "74B4D2EA57526348055B657EB254D92606C00C7A49DBC2B122D5DA34A44F75D2" + }, + { + "path": "src/RiskLabAI/utils/publication_plots.py", + "size": 5850, + "sha256": "B00FD12382E692BB3F74F2B37EFFAB1D95BC3641679F588BBB60FB0C203764D8" + }, + { + "path": "src/RiskLabAI/utils/update_figure_layout.py", + "size": 1324, + "sha256": "8C5C6DAEFE392D1AD59AF598B1D8D76732FD4B0A7A437AB3C4AB160CB1CDFFB2" + }, + { + "path": "test/backtest/test_advanced_bet_sizing.py", + "size": 3779, + "sha256": "73E02C09002FE941032602FAF26E6013261D0C996EBB747A87E984E1F2040095" + }, + { + "path": "test/backtest/test_backtest_overfitting_simulation.py", + "size": 2489, + "sha256": "195EE1482CC4C4F1EC9346D1C8C0A600EBB14898D331BC7E37F0FB29D3E1A68A" + }, + { + "path": "test/backtest/test_backtest_statistics.py", + "size": 4198, + "sha256": "16185AE1BEC34431ECD85EE1BE4E1A49C622C9CDB8B59CA4427148B1B6C2BA0A" + }, + { + "path": "test/backtest/test_backtest_synthetic_data.py", + "size": 2147, + "sha256": "4E81268462F0321FDE82215768BC6ACAD07D5F70F881C5D74CCE0991A1BD6435" + }, + { + "path": "test/backtest/test_bet_sizing.py", + "size": 5392, + "sha256": "7CA2E228BD7421C87F15BFABFD6C79E529D239F34A685ACBF5AEE1620EE1898C" + }, + { + "path": "test/backtest/test_multiple_testing.py", + "size": 3034, + "sha256": "AA6E928AD7A211C4C3D38525CC6FAFF1DF4D5F513C8EA97927D3DEFBF1672802" + }, + { + "path": "test/backtest/test_ou_trading_rules.py", + "size": 4413, + "sha256": "38187A1F848B6C31B6245D45C619AD25278D7B2BF2F36F331BFB1C838E09FC50" + }, + { + "path": "test/backtest/test_probabilistic_sharpe_ratio.py", + "size": 3242, + "sha256": "A62D3309AE74178AA80065FF9CFF4A96CE33A701AEB2D6F54127074875F91CC7" + }, + { + "path": "test/backtest/test_probability_of_backtest_overfitting.py", + "size": 3715, + "sha256": "178F73E2D1E3175C51EDCC0D990DFA49FB21C2843E4CF15D3726E370A42DBCBA" + }, + { + "path": "test/backtest/test_robust_statistics.py", + "size": 4684, + "sha256": "F1118D013AE5BE984BE313A2C4DCB45E76A715F1AE63AA1CD9F1069751EF5255" + }, + { + "path": "test/backtest/test_sharpe_inference.py", + "size": 3456, + "sha256": "814021459A2904B6B5C5C9ADC90EB9E503D5437C1764CFDA431291132ED692E2" + }, + { + "path": "test/backtest/test_strategy_risk.py", + "size": 3708, + "sha256": "E9696D0CDA4EAAFF01F536AD31E611658B36236BCADB5F470F0949EE4CE33820" + }, + { + "path": "test/backtest/test_technical_indicators.py", + "size": 3947, + "sha256": "63D92133A4731E007902E5FD73344F4B2FD6D3238E3124E2B3BD2C44B967B38C" + }, + { + "path": "test/backtest/test_test_set_overfitting.py", + "size": 3145, + "sha256": "C9E6DF7D60DA22FFC20FD9C4386B25A83DE9C2F24BAA7C2EC6985FF0B91949FC" + }, + { + "path": "test/backtest/validation/test_adaptive_combinatorial_purged.py", + "size": 3326, + "sha256": "E63E3168B1C24A68A7A9B97BD6AE86D653781CABBB75B612973D6E526F74A576" + }, + { + "path": "test/backtest/validation/test_bagged_combinatorial_purged.py", + "size": 2500, + "sha256": "36CD1050290F37C76B94776F7EF62D883205350373B2C251963EB3D8F424A8D5" + }, + { + "path": "test/backtest/validation/test_combinatorial_purged.py", + "size": 4996, + "sha256": "2A7176CB64BC3117914123472650C2E28675E127AF721507FEC708A977AB014E" + }, + { + "path": "test/backtest/validation/test_cross_validator_controller.py", + "size": 1836, + "sha256": "B27F524D6BFF839D9D11A2AF3C6F69C4E8725149DA3FF4BE7D47EE4ACFF78664" + }, + { + "path": "test/backtest/validation/test_cross_validator_factory.py", + "size": 3073, + "sha256": "059213105205BA8A4B1EADD6923C513980EB38E7D48AF2ED9261841819E7AD04" + }, + { + "path": "test/backtest/validation/test_kfold.py", + "size": 4242, + "sha256": "42FFA8774B3398D0B2D4F6ABCEEE8934D9A600AA7A2C859E56F958F3291B2409" + }, + { + "path": "test/backtest/validation/test_leakage_aware_hpo.py", + "size": 3951, + "sha256": "7D689924DA7A7C3797E9E59B3BF8AFC14CF1893D28F27BE50D2B89FBBB6917E7" + }, + { + "path": "test/backtest/validation/test_path_adaptive_cpcv.py", + "size": 9013, + "sha256": "AE54E8F04CEA04C46613F7542E3EC22979745F82FC3E7871F36A821516C61858" + }, + { + "path": "test/backtest/validation/test_path_bagged_cpcv.py", + "size": 4004, + "sha256": "E4A0B4DCBAF8D3BA9D3E5242A9E92D8F4800D01CEE76CE9739BEBDD79679E12F" + }, + { + "path": "test/backtest/validation/test_purged_kfold.py", + "size": 5007, + "sha256": "C64CB49639B8AB7E3878AE0A541F42CE3D3853405E8763B9331F6A2A0EA3153D" + }, + { + "path": "test/backtest/validation/test_walk_forward.py", + "size": 4290, + "sha256": "DB6846CCB5C99C47B07F2C54C405C28F2E903E4D5EAB68405DAD4F7B37C27564" + }, + { + "path": "test/causal_factor_analysis/fixtures/additive_numeric_parity.tsv", + "size": 1200, + "sha256": "E2B46AB385DB863D9F4C764F3754D6C0931ED7C62DCB2C589927DB924986DD7E" + }, + { + "path": "test/causal_factor_analysis/test_additive_numeric_parity.py", + "size": 7060, + "sha256": "40EBB18368636E2293C695A8FB3BA89973CD3F28DBFFA174E4EC48B236459F7B" + }, + { + "path": "test/causal_factor_analysis/test_allocation_diagnostics.py", + "size": 8402, + "sha256": "4393191DFF603085216902051FF9CB0EAAEBF4E5CD6CC2F394C4381A07E2C760" + }, + { + "path": "test/causal_factor_analysis/test_cfi_book_graph_examples.py", + "size": 3644, + "sha256": "195A3291A6CB6EAEE929F02951BAA45BD6E933AB751CCC30469402BBDA12DE9A" + }, + { + "path": "test/causal_factor_analysis/test_factor_mirage.py", + "size": 16311, + "sha256": "B64153FE7B39D103022268E503A132F76E967A05DB103D3339A662A9DA0A890C" + }, + { + "path": "test/causal_factor_analysis/test_generalized_factor_mirage.py", + "size": 8498, + "sha256": "16BEA6313136F0BB2D8EEEA4D79C4BF90C265E17AA03BBE823CA9A6381A2739C" + }, + { + "path": "test/causal_factor_analysis/test_graph_identification.py", + "size": 35155, + "sha256": "16CA24092CF9D05D2A7718FF1BACBF050592EFDDE18C7B7CCF2B20121250F789" + }, + { + "path": "test/causal_factor_analysis/test_graph_roles.py", + "size": 5190, + "sha256": "70C2A24EBD466D461F1C54A8DFFC26AA7F7027ECEE2F1B7F7C8377DDE53F9117" + }, + { + "path": "test/causal_factor_analysis/test_minimum_variance.py", + "size": 17057, + "sha256": "9B2D772C018F99D358168AF9462DC48E0B7C53C924370A8B564AC1C4C66CE1FB" + }, + { + "path": "test/causal_factor_analysis/test_protocol.py", + "size": 38965, + "sha256": "E1E456BE476A5D990F781E240868B6C344FA0345FC78DC3C027BE63271BCA240" + }, + { + "path": "test/causal_factor_analysis/test_search_adjusted_fdr.py", + "size": 28388, + "sha256": "510F7D5D0FC64786D496EB9B72EA508AD21F82360E1C7FFEC086C618A20D55B7" + }, + { + "path": "test/causal_factor_analysis/test_specification_diagnostics.py", + "size": 9088, + "sha256": "6FD3A8C9F1DFDC665AE9DCD80E41798D5D1EAE48BB9B2F4EE5AAB61B1B3393D4" + }, + { + "path": "test/causal_factor_analysis/test_structural_models.py", + "size": 5760, + "sha256": "7231FF45CA7273A9687096349A30CC8FC865DC85ABADE17A2AB69CB624D5684C" + }, + { + "path": "test/causal_factor_analysis/test_treatment_effects.py", + "size": 5955, + "sha256": "434750E2D2889FA64E49BAE08372D0E08C9DC145BE88AB5CD58B942B4D1F5B6B" + }, + { + "path": "test/cluster/test_clustering.py", + "size": 3269, + "sha256": "33F301B160E90B4FA6BA369524E6A3D30F3BB63887B4898D6DC6708CCD900A7A" + }, + { + "path": "test/controller/test_bars_initializer.py", + "size": 2484, + "sha256": "13B2E36A9D10E25284C3CDCE19F04320524623790BE64F79C851D31703D3724C" + }, + { + "path": "test/controller/test_data_structure_controller.py", + "size": 2833, + "sha256": "9510A0FBD3B714FE87742519A9200CBFD0D6CEB99C14A9ED369F51EA7E3AAA56" + }, + { + "path": "test/core/test_base.py", + "size": 2701, + "sha256": "0BFC387B4C7A94205708140A7485F1AF1D3BFA1FC9472A1898ACBCEABA2DFE64" + }, + { + "path": "test/core/test_builtin_parity.py", + "size": 5095, + "sha256": "560CB3D389459FEA8572E7C71B5F66563A1EF17C4545B782A989CD8A67FCAD64" + }, + { + "path": "test/core/test_registry.py", + "size": 5789, + "sha256": "C6F667A94DB9803219591A3E4E8F963E820E5508C996A9850386BD4E8A8FED03" + }, + { + "path": "test/data/denoise/test_denoising.py", + "size": 6035, + "sha256": "4CAE78A888AEEE68C65C7A4625208E5EFC0CAD101D7DE05C35D2C0B4F0272FFE" + }, + { + "path": "test/data/denoise/test_nercome.py", + "size": 4273, + "sha256": "A20954A1B85C178B4E3561E38B5ECC7F6D35FF2201D2BD34E62EB2EAACCF7632" + }, + { + "path": "test/data/differentiation/test_adaptive_differentiation.py", + "size": 3844, + "sha256": "940CA6705C77B82ABD55B2B500067B0ED376276ED1D8D6B9234AC3504D540E27" + }, + { + "path": "test/data/differentiation/test_differentiation.py", + "size": 3712, + "sha256": "C95ECCBBC869053624EB81A990F91003C835B4CCB1ABFEA56B01ECA347C54C33" + }, + { + "path": "test/data/distance/test_codependence.py", + "size": 3374, + "sha256": "146693136A612F70CFA26E6DF9AF4EE897F3E09862B6BDDF4A682642A18D9CC8" + }, + { + "path": "test/data/distance/test_distance_metric.py", + "size": 2758, + "sha256": "C20662C35D3253A3C0157456B3385523DDF6042D74C0BD031CF0DAE168D57589" + }, + { + "path": "test/data/labeling/test_financial_labels.py", + "size": 2624, + "sha256": "5B194D19D2565708359FB005A22336DD9C82F6BEED562D8B5E91DCE08387F8FD" + }, + { + "path": "test/data/labeling/test_labeling.py", + "size": 4598, + "sha256": "571BEBC1BBD55A1E1F3F2163B79C1B5D2BB3F07F355A850B5E11106EA28B33E3" + }, + { + "path": "test/data/structures/test_hardened_information_driven_bars.py", + "size": 4765, + "sha256": "19ED1A20708BA49B3294C8406B49BD5E4261938983A37FCCF2301319E76EEB39" + }, + { + "path": "test/data/structures/test_imbalance_bars.py", + "size": 3432, + "sha256": "95B11988BA1E13F67868EDB84E361290E8206214E67A2ADD368EC50A04035678" + }, + { + "path": "test/data/structures/test_run_bars.py", + "size": 2298, + "sha256": "AA84352C3434FA6BE5B36E1E04506FFFB04A71B723F52D1E76E3684AB76F5FD5" + }, + { + "path": "test/data/structures/test_standard_bars.py", + "size": 2573, + "sha256": "7FBDEE31397875FD3CD6AA0755F79376C8298C623EDCEFEC62FCBE02801FDE81" + }, + { + "path": "test/data/structures/test_time_bars.py", + "size": 1948, + "sha256": "777F07CBF7B1285660BE260CF28DAEDD8409119B580220789C06C571E83012A9" + }, + { + "path": "test/data/synthetic_data/test_drift_burst_hypothesis.py", + "size": 2496, + "sha256": "D36F02ED24979C2AC250DA8F78DCF00E84AC15629A66FF0F49B864DC7F29234F" + }, + { + "path": "test/data/synthetic_data/test_synthetic_controlled_environment.py", + "size": 2706, + "sha256": "3EDD684CD47C177AC3B14201D00F92D58B653F80015FBBF5FE8B199681A9CFF3" + }, + { + "path": "test/data/weights/test_sample_weights.py", + "size": 3920, + "sha256": "6A682B9C8273A9480CC40EDEF503F4ADB254FDE03A7C6846A6576B2844177DA5" + }, + { + "path": "test/ensemble/test_bagging_classifier_accuracy.py", + "size": 1055, + "sha256": "B9E9FCDEC62CCA57773D775874B1A49FE9FFDBA70AE75C1A5B44D21BE5F42F70" + }, + { + "path": "test/features/entropy_features/test_entropy.py", + "size": 8265, + "sha256": "EB0639713017FA205239818CF5EB33B37488D1F5015E41BE572DF9B3DC4B92A3" + }, + { + "path": "test/features/feature_importance/test_debiased_importance.py", + "size": 3145, + "sha256": "767C8AC265903BFADCC0CB358C04380F7CC1B080E249CF2605D7C36054DA7841" + }, + { + "path": "test/features/feature_importance/test_feature_importance.py", + "size": 4539, + "sha256": "1CC36BB1739EEDBA96BC9CFE6C784F4B10796AD8B7030D2423BFADDA83AC4BBC" + }, + { + "path": "test/features/feature_importance/test_generate_synthetic_data.py", + "size": 1297, + "sha256": "60E8710D8FB14D2DB9C8A8561B760A7C82E78A40B58D050DE4B454FAD9D09A19" + }, + { + "path": "test/features/feature_importance/test_orthogonal_features.py", + "size": 1377, + "sha256": "2FFF242081857CAE3677849ED9615F812C02C4F8142C2BE3F165EB7F6163E43F" + }, + { + "path": "test/features/feature_importance/test_weighted_tau.py", + "size": 717, + "sha256": "DE4E792D04BE710A70F0531FB3BA4CF6F4FD281CD1CCB0AF66775B2AFF9DEB5B" + }, + { + "path": "test/features/microstructural_features/test_edge.py", + "size": 2005, + "sha256": "E7F97BFC152F1ABC9CDA7ABACCAA96F424E2F5F60178AB358B282944AF7BB1CE" + }, + { + "path": "test/features/microstructural_features/test_microstructure.py", + "size": 2089, + "sha256": "FAF86E2B87ED6C052039270C8FDD500399B71AFFAB9B3833ABD01C2EB7065A93" + }, + { + "path": "test/features/structural_breaks/test_pelt_change_points.py", + "size": 2490, + "sha256": "ADE35736D9A8B8A0621A0D1687CB0EF567D2ACC1AFD1E100F1D3FE5ED43307A8" + }, + { + "path": "test/features/structural_breaks/test_structural_breaks.py", + "size": 9753, + "sha256": "DC76EF79F06F577A1BAA8FE2AA8AC53BD5888C347CE46E595766490E2F72F75D" + }, + { + "path": "test/features/structural_breaks/test_volatility_robust_sadf.py", + "size": 2742, + "sha256": "11A8A9D0446F666FB6B7BCF0B857414EBD6583931CD1CA199CDC2C7ECD37B58F" + }, + { + "path": "test/hpc/test_hpc.py", + "size": 2213, + "sha256": "2E3414DD74A72FD86FED54059515E2A088F99F4C23492A1992A55A5F311F09A8" + }, + { + "path": "test/optimization/test_hedging.py", + "size": 2026, + "sha256": "C34C92CCD66E0B895140B2D45270AC94803AB408532888E04C6FABC9C444BC22" + }, + { + "path": "test/optimization/test_hrp.py", + "size": 3689, + "sha256": "08A03D26DDCA53A9793C46D76AFEC5EA2D1203B8A833895A93C29E0EBCD6A4C7" + }, + { + "path": "test/optimization/test_hyper_parameter_tuning.py", + "size": 3405, + "sha256": "C756502F9BA1FFC0EAFCF2F0EE3E73F500CB07D956378FFADE018CC610AEC650" + }, + { + "path": "test/optimization/test_nco.py", + "size": 1875, + "sha256": "07393543334EED0500954A475403AFDF68BFDB5D9AEC14768E5D21D91E41A2AE" + }, + { + "path": "test/pde/test_pde_solver.py", + "size": 1764, + "sha256": "9606F9460D87E1268B55A2EC5FC8AD7BE92B4E9DB64F219E0C444343CDB763DD" + }, + { + "path": "test/test_consolidation.py", + "size": 3427, + "sha256": "653D2C917A29D41030CE835E18736ED607BCBBC9E7A75B1A16FEFC286F391F8F" + }, + { + "path": "test/test_deprecation.py", + "size": 1746, + "sha256": "1318D33E6B9790C48F3E366568733FB14574D0D7ECCF7086D5EF5D8BE8545930" + }, + { + "path": "test/test_performance.py", + "size": 6986, + "sha256": "857264FBB2C1507995BB18CBD2E9081849D9C3F4EECA73F58FBC0371FAE42F73" + }, + { + "path": "test/utils/test_ewma.py", + "size": 920, + "sha256": "A75275626813BC8DFE6A0A792370FC1B013ED8A5398232468CE7E871137F70D6" + }, + { + "path": "test/utils/test_momentum_mean_reverting_strategy_sides.py", + "size": 2321, + "sha256": "EEBBFAC83B87A3A2BF72C9D9B203DB11D8794D9CBE7CA5125DEBE141C10E3889" + }, + { + "path": "test/utils/test_progress.py", + "size": 1022, + "sha256": "2CC8FBD465B007948A09C0E8975288A13D46590113FB9DA6AA441C07C55F23BA" + } + ], + "self_hash_policy": "PACKAGE_FILES.json is listed but omitted from its own hash records to avoid a self-referential digest.", + "preserved_repository_only_file_count": 127, + "preserved_repository_only_files": [ + ".github/workflows/publish.yml", + ".gitignore", + "CHANGELOG.md", + "EXTENDING.md", + "INSTALLATION.md", + "NAMING_CANON_2.0.0.md", + "RiskLabAI/__init__.py", + "RiskLabAI/_deprecation.py", + "RiskLabAI/backtest/__init__.py", + "RiskLabAI/backtest/advanced_bet_sizing.py", + "RiskLabAI/backtest/backtest_overfitting_simulation.py", + "RiskLabAI/backtest/backtest_statistics.py", + "RiskLabAI/backtest/backtest_synthetic_data.py", + "RiskLabAI/backtest/bet_sizing.py", + "RiskLabAI/backtest/multiple_testing.py", + "RiskLabAI/backtest/ou_trading_rules.py", + "RiskLabAI/backtest/probabilistic_sharpe_ratio.py", + "RiskLabAI/backtest/probability_of_backtest_overfitting.py", + "RiskLabAI/backtest/robust_statistics.py", + "RiskLabAI/backtest/sharpe_inference.py", + "RiskLabAI/backtest/strategy_risk.py", + "RiskLabAI/backtest/test_set_overfitting.py", + "RiskLabAI/backtest/validation/__init__.py", + "RiskLabAI/backtest/validation/adaptive_combinatorial_purged.py", + "RiskLabAI/backtest/validation/bagged_combinatorial_purged.py", + "RiskLabAI/backtest/validation/combinatorial_purged.py", + "RiskLabAI/backtest/validation/cross_validator_controller.py", + "RiskLabAI/backtest/validation/cross_validator_factory.py", + "RiskLabAI/backtest/validation/cross_validator_interface.py", + "RiskLabAI/backtest/validation/kfold.py", + "RiskLabAI/backtest/validation/leakage_aware_hpo.py", + "RiskLabAI/backtest/validation/path_adaptive_cpcv.py", + "RiskLabAI/backtest/validation/path_bagged_cpcv.py", + "RiskLabAI/backtest/validation/purged_kfold.py", + "RiskLabAI/backtest/validation/walk_forward.py", + "RiskLabAI/cluster/__init__.py", + "RiskLabAI/cluster/clustering.py", + "RiskLabAI/controller/__init__.py", + "RiskLabAI/controller/bars_initializer.py", + "RiskLabAI/controller/data_structure_controller.py", + "RiskLabAI/core/__init__.py", + "RiskLabAI/core/_builtins.py", + "RiskLabAI/core/base.py", + "RiskLabAI/core/registry.py", + "RiskLabAI/data/__init__.py", + "RiskLabAI/data/denoise/__init__.py", + "RiskLabAI/data/denoise/denoising.py", + "RiskLabAI/data/denoise/nercome.py", + "RiskLabAI/data/differentiation/__init__.py", + "RiskLabAI/data/differentiation/adaptive_differentiation.py", + "RiskLabAI/data/differentiation/differentiation.py", + "RiskLabAI/data/distance/__init__.py", + "RiskLabAI/data/distance/codependence.py", + "RiskLabAI/data/distance/distance_metric.py", + "RiskLabAI/data/labeling/__init__.py", + "RiskLabAI/data/labeling/financial_labels.py", + "RiskLabAI/data/labeling/labeling.py", + "RiskLabAI/data/structures/__init__.py", + "RiskLabAI/data/structures/abstract_bars.py", + "RiskLabAI/data/structures/abstract_imbalance_bars.py", + "RiskLabAI/data/structures/abstract_information_driven_bars.py", + "RiskLabAI/data/structures/abstract_run_bars.py", + "RiskLabAI/data/structures/hardened_information_driven_bars.py", + "RiskLabAI/data/structures/imbalance_bars.py", + "RiskLabAI/data/structures/run_bars.py", + "RiskLabAI/data/structures/standard_bars.py", + "RiskLabAI/data/structures/time_bars.py", + "RiskLabAI/data/synthetic_data/__init__.py", + "RiskLabAI/data/synthetic_data/drift_burst_hypothesis.py", + "RiskLabAI/data/synthetic_data/simulation.py", + "RiskLabAI/data/synthetic_data/synthetic_controlled_environment.py", + "RiskLabAI/data/weights/__init__.py", + "RiskLabAI/data/weights/sample_weights.py", + "RiskLabAI/ensemble/__init__.py", + "RiskLabAI/ensemble/bagging_classifier_accuracy.py", + "RiskLabAI/ensemble/empirical_bagging_accuracy.py", + "RiskLabAI/features/__init__.py", + "RiskLabAI/features/entropy_features/__init__.py", + "RiskLabAI/features/entropy_features/bias_corrected.py", + "RiskLabAI/features/entropy_features/kontoyiannis.py", + "RiskLabAI/features/entropy_features/lempel_ziv.py", + "RiskLabAI/features/entropy_features/plug_in.py", + "RiskLabAI/features/entropy_features/pmf.py", + "RiskLabAI/features/entropy_features/shannon.py", + "RiskLabAI/features/feature_importance/__init__.py", + "RiskLabAI/features/feature_importance/clustered_feature_importance_mda.py", + "RiskLabAI/features/feature_importance/clustered_feature_importance_mdi.py", + "RiskLabAI/features/feature_importance/debiased_importance.py", + "RiskLabAI/features/feature_importance/feature_importance_controller.py", + "RiskLabAI/features/feature_importance/feature_importance_factory.py", + "RiskLabAI/features/feature_importance/feature_importance_mda.py", + "RiskLabAI/features/feature_importance/feature_importance_mdi.py", + "RiskLabAI/features/feature_importance/feature_importance_sfi.py", + "RiskLabAI/features/feature_importance/feature_importance_strategy.py", + "RiskLabAI/features/feature_importance/generate_synthetic_data.py", + "RiskLabAI/features/feature_importance/orthogonal_features.py", + "RiskLabAI/features/feature_importance/weighted_tau.py", + "RiskLabAI/features/microstructural_features/__init__.py", + "RiskLabAI/features/microstructural_features/bekker_parkinson_volatility_estimator.py", + "RiskLabAI/features/microstructural_features/corwin_schultz.py", + "RiskLabAI/features/microstructural_features/edge.py", + "RiskLabAI/features/structural_breaks/__init__.py", + "RiskLabAI/features/structural_breaks/pelt_change_points.py", + "RiskLabAI/features/structural_breaks/structural_breaks.py", + "RiskLabAI/features/structural_breaks/volatility_robust_sadf.py", + "RiskLabAI/hpc/__init__.py", + "RiskLabAI/hpc/hpc.py", + "RiskLabAI/optimization/__init__.py", + "RiskLabAI/optimization/hedging.py", + "RiskLabAI/optimization/hrp.py", + "RiskLabAI/optimization/hyper_parameter_tuning.py", + "RiskLabAI/optimization/nco.py", + "RiskLabAI/pde/__init__.py", + "RiskLabAI/pde/equation.py", + "RiskLabAI/pde/model.py", + "RiskLabAI/pde/solver.py", + "RiskLabAI/utils/__init__.py", + "RiskLabAI/utils/constants.py", + "RiskLabAI/utils/ewma.py", + "RiskLabAI/utils/momentum_mean_reverting_strategy_sides.py", + "RiskLabAI/utils/progress.py", + "RiskLabAI/utils/publication_plots.py", + "RiskLabAI/utils/update_figure_layout.py", + "docs/delete", + "documenter.py", + "style_guide.md", + "tree.py" + ], + "preserved_repository_only_records": [ + { + "path": ".github/workflows/publish.yml", + "size": 973, + "sha256": "B0F075E6AC7F4D6C192F4C85D033268F97C908D1DC8BD19B922599AB0F6AAA6E" + }, + { + "path": ".gitignore", + "size": 191, + "sha256": "371B312B403BCFA6632199A28D57E50EFC3EE9F865E25BF0BEA01CC42CDF6892" + }, + { + "path": "CHANGELOG.md", + "size": 10907, + "sha256": "B607A87ABAA51A2862F08B5C4285B252C8FCFA9FFC09831042366E74C30F2D53" + }, + { + "path": "EXTENDING.md", + "size": 6426, + "sha256": "C2CD3D7E8AB84E70FF641A0A636FA4472AD79D3CC73D903CCAF07C7DE4250FBB" + }, + { + "path": "INSTALLATION.md", + "size": 3477, + "sha256": "EBE7C2C5A3AAC586D67936682674FFD94CF9C2FC2907BFDBF901D76330D6F5A5" + }, + { + "path": "NAMING_CANON_2.0.0.md", + "size": 8885, + "sha256": "598BA7789FA91B7B1820182F4FD7B2D0D259C23C4E1B475D41F9BA25E1091072" + }, + { + "path": "RiskLabAI/__init__.py", + "size": 2937, + "sha256": "8062C354011E87CEA7349680B9AA3F9AA825758B82742D8AA78B0DCA92FA037E" + }, + { + "path": "RiskLabAI/_deprecation.py", + "size": 3889, + "sha256": "4AA73DE361877A289248332ED5AF7EC0DE302ABEFE5C4B05B4A2500CB2C2D293" + }, + { + "path": "RiskLabAI/backtest/__init__.py", + "size": 6414, + "sha256": "26D4E3A9972AFF0A26181731F5B1082E8A4B70ACA551D47C3BA372EE7370322C" + }, + { + "path": "RiskLabAI/backtest/advanced_bet_sizing.py", + "size": 10098, + "sha256": "1D90B97962BED3711265488A7921C651585FBF0D366A412607944EAF44B9C810" + }, + { + "path": "RiskLabAI/backtest/backtest_overfitting_simulation.py", + "size": 56278, + "sha256": "A2C487F016E7BE716CBD5277EEDE24812FC3E29B3CA8CC5E8D4A0E7F2375CA3F" + }, + { + "path": "RiskLabAI/backtest/backtest_statistics.py", + "size": 8729, + "sha256": "456C5C24F9E457CE82B3CEA214D94CC1C78169CF81E2506F8EB48E329D2902DA" + }, + { + "path": "RiskLabAI/backtest/backtest_synthetic_data.py", + "size": 3523, + "sha256": "9ACDA6D9D81702F611CBC4EBE11899C31463DA899709D9975931F592CD59AF39" + }, + { + "path": "RiskLabAI/backtest/bet_sizing.py", + "size": 15149, + "sha256": "45B32009CA72DD781BB55825CD48BAD535035A05093EDAB403F1E6B37B817C91" + }, + { + "path": "RiskLabAI/backtest/multiple_testing.py", + "size": 7855, + "sha256": "FAA5E3C9E225869FC204BB5DA4C88830A81989A18FC927706FBC3BCCDEE7936B" + }, + { + "path": "RiskLabAI/backtest/ou_trading_rules.py", + "size": 12557, + "sha256": "F4E95F65C430E4E3CC12C5A1B01906AD2E7056E7969AA70763115DF673C2387E" + }, + { + "path": "RiskLabAI/backtest/probabilistic_sharpe_ratio.py", + "size": 4538, + "sha256": "3CD56F7C02E0D42CE04E395737DB6039FAEA617B367E6BFBC2F0A74768FA7B49" + }, + { + "path": "RiskLabAI/backtest/probability_of_backtest_overfitting.py", + "size": 5085, + "sha256": "C14BB1E1179AAC1D61D56ACF6BF282BF2B9593B143A43E357DC7D9181C7DA373" + }, + { + "path": "RiskLabAI/backtest/robust_statistics.py", + "size": 11911, + "sha256": "9432CBA7E36A8C1F9BA69D6F912D226FB78FEC4069E940E8380EFB1543696974" + }, + { + "path": "RiskLabAI/backtest/sharpe_inference.py", + "size": 7721, + "sha256": "58A436922785F2D087FC6C40C1881228E84AAB7BC96B4D8C2C0161FA2F05E0A4" + }, + { + "path": "RiskLabAI/backtest/strategy_risk.py", + "size": 10429, + "sha256": "8F368EAA32155038B8E438357FF69F33E7A4245D9E1598CA4C2EDF3FF55A68E7" + }, + { + "path": "RiskLabAI/backtest/test_set_overfitting.py", + "size": 8865, + "sha256": "2C813D80E85ED2717F4C3B991244B23DEE1253807C8FFA9EA71A36F37E9DDED0" + }, + { + "path": "RiskLabAI/backtest/validation/__init__.py", + "size": 1980, + "sha256": "52069C5047BBB0F199D42C35F4D4AAC6E2D3AE6EC8BB58E49338AB6A4F2FC62A" + }, + { + "path": "RiskLabAI/backtest/validation/adaptive_combinatorial_purged.py", + "size": 18395, + "sha256": "B466BD0C43297F0654467E1A91B135CCB7958CD856A1A45BAFBCDBE372F7FAA0" + }, + { + "path": "RiskLabAI/backtest/validation/bagged_combinatorial_purged.py", + "size": 9472, + "sha256": "A881EDEBB301C8C5184248AA37EDCCE94159A9314BA5369026E0C423C5C73B12" + }, + { + "path": "RiskLabAI/backtest/validation/combinatorial_purged.py", + "size": 14461, + "sha256": "8CC827C13CE4DE02A70C5A942A663CC2B704BD0EF5B251CDE78F59CDB9F375DB" + }, + { + "path": "RiskLabAI/backtest/validation/cross_validator_controller.py", + "size": 1205, + "sha256": "B4AC5AC465BDC7CB8548CAB86020237551F3CDF653116C064515CE26E9BE5BE5" + }, + { + "path": "RiskLabAI/backtest/validation/cross_validator_factory.py", + "size": 2636, + "sha256": "40FEFA69DBE7CF76A6194C1E49CEF39663ED6809C58BCCEF2BCB4C66B64D0D6D" + }, + { + "path": "RiskLabAI/backtest/validation/cross_validator_interface.py", + "size": 8202, + "sha256": "9BB6CFBFF867C8F863CF0CA0B1FF6384463A4385D88C2FD1FEFFA0775B85FD0D" + }, + { + "path": "RiskLabAI/backtest/validation/kfold.py", + "size": 12431, + "sha256": "BBA92A01B9EA320C89DB7CEBFED147EDBDDAE9CB7FE2D550FF2F2ECFE7ACC15B" + }, + { + "path": "RiskLabAI/backtest/validation/leakage_aware_hpo.py", + "size": 9930, + "sha256": "FF0040F22C4710918CD39CF3A08BC48293ED48A70A8F46898B3C4686122B6D2C" + }, + { + "path": "RiskLabAI/backtest/validation/path_adaptive_cpcv.py", + "size": 10437, + "sha256": "4257BE918DA88233CE4ABA8AD2A0BAB2859C9B711AE1A591ADABDA33C7312668" + }, + { + "path": "RiskLabAI/backtest/validation/path_bagged_cpcv.py", + "size": 5699, + "sha256": "195191CEDCC24386D2862C886EA323E86F9B74371379B5C825CB9EFB069382A2" + }, + { + "path": "RiskLabAI/backtest/validation/purged_kfold.py", + "size": 19810, + "sha256": "86802CB8E32ECDD092A522FBAC196C2BA5F05FAAAF4227091C705EDBD2DBFCD8" + }, + { + "path": "RiskLabAI/backtest/validation/walk_forward.py", + "size": 6474, + "sha256": "47C0786EAF7B5F4D774B24537ADEEDC9E80AAC3604FA3D14C52A17864FEC9B7A" + }, + { + "path": "RiskLabAI/cluster/__init__.py", + "size": 669, + "sha256": "CC1993BE2778895842431BD777BFEF3B620341E6DB13B4BEDB5A58891CAF14DC" + }, + { + "path": "RiskLabAI/cluster/clustering.py", + "size": 14275, + "sha256": "C9ADF94D36C3B1257D33E0DBC7D38243C4876BF1921B72A9092FE81A745610B2" + }, + { + "path": "RiskLabAI/controller/__init__.py", + "size": 75, + "sha256": "875DCD9EC172311DF8E9DF665C60102BDC0F56218A5CDDE0760E2A081E369230" + }, + { + "path": "RiskLabAI/controller/bars_initializer.py", + "size": 13999, + "sha256": "28284D4877900DE9D3582C2B821FC66173E474E1F62C95F27385190C4A88426F" + }, + { + "path": "RiskLabAI/controller/data_structure_controller.py", + "size": 6102, + "sha256": "237843F3AD293C366607FA555C218F4F3CD8ACCD881F784030B3AA6B12BC8E23" + }, + { + "path": "RiskLabAI/core/__init__.py", + "size": 1933, + "sha256": "1F57377F2CDCD8958910376A2615F45A23FD96954FDE05FFBEDC8FDC20AD1DE8" + }, + { + "path": "RiskLabAI/core/_builtins.py", + "size": 7247, + "sha256": "5D754E23EF41F580D0A4B5CB635DFA95A5A1AB20E3A500B1D4B9F9A75EB333A7" + }, + { + "path": "RiskLabAI/core/base.py", + "size": 6270, + "sha256": "47F3B61B748629A7AFBB1209A701A8F6614F2474EA9919B298B4574D3E757C5E" + }, + { + "path": "RiskLabAI/core/registry.py", + "size": 14263, + "sha256": "2A48E47D33ED1E2EE074DEE4933C363884433226EB42CA4BFCA949E33AAC1016" + }, + { + "path": "RiskLabAI/data/__init__.py", + "size": 664, + "sha256": "F238157F6B54DB5E12E2F910821431BED049AB6E99665B64AD81F95EB7B1488B" + }, + { + "path": "RiskLabAI/data/denoise/__init__.py", + "size": 834, + "sha256": "E4A4B8822827C3CAA9F34A4FC3E589447C641BCFCB681AACD192D7DB2EC7ED9C" + }, + { + "path": "RiskLabAI/data/denoise/denoising.py", + "size": 11538, + "sha256": "411FFBD20F902E19426506D6C5472570960F9091E67E3D0F8626DE09E63B8B92" + }, + { + "path": "RiskLabAI/data/denoise/nercome.py", + "size": 6469, + "sha256": "AA11FB31EBE08631B7D979562E1A568E49B70FE9FD2CBA1304EA12908AD434B8" + }, + { + "path": "RiskLabAI/data/differentiation/__init__.py", + "size": 196, + "sha256": "51D73513B8108EF8835E5113E337F336DD62CBC4006BBBD486B50623401B670B" + }, + { + "path": "RiskLabAI/data/differentiation/adaptive_differentiation.py", + "size": 11222, + "sha256": "CB0DF7A28212D4CE0B15AF7023235B6AABD0EA9077D64572F3E5EB45D9A6A066" + }, + { + "path": "RiskLabAI/data/differentiation/differentiation.py", + "size": 12258, + "sha256": "FD6A1B4247CC3F7B6C794866CD1809770460BD2AFC08375F2F96B02AF0D79E02" + }, + { + "path": "RiskLabAI/data/distance/__init__.py", + "size": 102, + "sha256": "5BA3546F05199F587C42C2C8A4E80B541616EA3F80A93654B7F2E8D8F548F296" + }, + { + "path": "RiskLabAI/data/distance/codependence.py", + "size": 7268, + "sha256": "9033470188D780110795D53CEFD230BC3D5AE54BD8BA945908CB73F3C1B48B9A" + }, + { + "path": "RiskLabAI/data/distance/distance_metric.py", + "size": 8377, + "sha256": "6399EFDB1E12A82ED3BB6831FF6F985E0EAB18D2420D82670CF1599189D51E73" + }, + { + "path": "RiskLabAI/data/labeling/__init__.py", + "size": 1193, + "sha256": "E84DB73F915081EF60F6C8441D10BFE5E759037EE7D42945B1AD2FBB77A10CC3" + }, + { + "path": "RiskLabAI/data/labeling/financial_labels.py", + "size": 4424, + "sha256": "A2471BEAFFB28F2339D05DAED1811C4384C702CC0602638230D0FC03A4BE6A9C" + }, + { + "path": "RiskLabAI/data/labeling/labeling.py", + "size": 14680, + "sha256": "36687544C68B3016B5E4CFDE12ADE104971F45F79D4AC9FEB99BEB298F8E1418" + }, + { + "path": "RiskLabAI/data/structures/__init__.py", + "size": 883, + "sha256": "051BD4E2CF067134B61FC3CCEA16A9F20B94676F5C3D0DD01041F28541132956" + }, + { + "path": "RiskLabAI/data/structures/abstract_bars.py", + "size": 7436, + "sha256": "F775F93AE69A634E22EA324A0AB8603E729786CB72A684741D2111E090DA6014" + }, + { + "path": "RiskLabAI/data/structures/abstract_imbalance_bars.py", + "size": 6388, + "sha256": "350535B7AAF4E997E19242E07991EA7AB22BB41C041490D2243F25B2D4D90111" + }, + { + "path": "RiskLabAI/data/structures/abstract_information_driven_bars.py", + "size": 4150, + "sha256": "2035EA576F53B0CF6B7A4A44CEB2DDC64E531BC91E8F6C98ADACF7578A11306A" + }, + { + "path": "RiskLabAI/data/structures/abstract_run_bars.py", + "size": 10204, + "sha256": "51DDD1B7C662E2FB2D749C5C8483782C76EF00584ACC36FD8314787239DAFFED" + }, + { + "path": "RiskLabAI/data/structures/hardened_information_driven_bars.py", + "size": 6880, + "sha256": "E5CF32132DADEF61624F24E2AC42E673C0B4B274DC317DD6488539A3FC0F1741" + }, + { + "path": "RiskLabAI/data/structures/imbalance_bars.py", + "size": 4741, + "sha256": "D355A01123732210788FAB14EDC8D68ABC6FA4E1035AC5BD636BD11DD4E210AC" + }, + { + "path": "RiskLabAI/data/structures/run_bars.py", + "size": 4515, + "sha256": "7FE0F23DF5BE39B475073CA69FABD6FDE7126EC6E4289C2D84652D930D251900" + }, + { + "path": "RiskLabAI/data/structures/standard_bars.py", + "size": 2669, + "sha256": "C4142C10BEE79EF96E8A9DA31F1444EE062933D587630EB567E0A5CE3C81C362" + }, + { + "path": "RiskLabAI/data/structures/time_bars.py", + "size": 4544, + "sha256": "F9126A81DA595A4E1D87400164C09071833222CD9C1546D69164F643D0AC582F" + }, + { + "path": "RiskLabAI/data/synthetic_data/__init__.py", + "size": 857, + "sha256": "BE3B0444592D9B45C9439E57210EB713C761D4B21CE497609B2B63E955F70768" + }, + { + "path": "RiskLabAI/data/synthetic_data/drift_burst_hypothesis.py", + "size": 3115, + "sha256": "3A613F5FE50555654556236EF409BC4F7FB83252CDF3502CA60D46EB29F4BC4C" + }, + { + "path": "RiskLabAI/data/synthetic_data/simulation.py", + "size": 3392, + "sha256": "09394D81D758211140D894B5CB4CB6289EACEC862638006C8A7D9A79FD16DC55" + }, + { + "path": "RiskLabAI/data/synthetic_data/synthetic_controlled_environment.py", + "size": 11963, + "sha256": "1233C0F9B0DDAD003176DBCBDEC103B37702DFD8EC7BAAD07F2AC0CC2F9C1A2C" + }, + { + "path": "RiskLabAI/data/weights/__init__.py", + "size": 621, + "sha256": "7275D7101AE1F1DC977EF7B6C53819139AC9F46F629742178AA0AA453390A523" + }, + { + "path": "RiskLabAI/data/weights/sample_weights.py", + "size": 7225, + "sha256": "A1AEBA7541663238C20B3B7A76B8706AB62D6860CD171DB6000E2EF92C492724" + }, + { + "path": "RiskLabAI/ensemble/__init__.py", + "size": 707, + "sha256": "2301547AD658B8DD96941FD1098FBDA9AC3464E487F2A9F1811C6BD656F8F46A" + }, + { + "path": "RiskLabAI/ensemble/bagging_classifier_accuracy.py", + "size": 1300, + "sha256": "DF1570D491B4452412103BE1ECDC6B0C4CFDA3D9AAF55D9DDD7620B611134F92" + }, + { + "path": "RiskLabAI/ensemble/empirical_bagging_accuracy.py", + "size": 11622, + "sha256": "E679640BE29A4986F0E67AF7F5A060894A72EDCD581C3F2B059D9C4177B7415F" + }, + { + "path": "RiskLabAI/features/__init__.py", + "size": 123, + "sha256": "8E6EF92F8D1E0E9BCA37E3436806A37308B8B3CB08C221481F4A1261327C5A18" + }, + { + "path": "RiskLabAI/features/entropy_features/__init__.py", + "size": 883, + "sha256": "2DB4F80E1037C4562E8034FF3120C8DF41DE54D85C7F8E1CF04628BBA9A52374" + }, + { + "path": "RiskLabAI/features/entropy_features/bias_corrected.py", + "size": 12893, + "sha256": "40B156C643E48DAE6F06440D9FD1BE2ED651F18FA8C7A2BD0DB569795E44DCFE" + }, + { + "path": "RiskLabAI/features/entropy_features/kontoyiannis.py", + "size": 2902, + "sha256": "08FCF8EEEF1D9C4FFFC4F812A39B77F406050091ED287C9AE1A577E849998A4B" + }, + { + "path": "RiskLabAI/features/entropy_features/lempel_ziv.py", + "size": 1017, + "sha256": "295BFA3C0E3970C75C924C6D19E2D0B384E29A2B50AED7C8C08E1723A975BF4F" + }, + { + "path": "RiskLabAI/features/entropy_features/plug_in.py", + "size": 981, + "sha256": "90DB453E95DD416771EAC11232E33584582C7FF1EA9B79DD441588185BADC483" + }, + { + "path": "RiskLabAI/features/entropy_features/pmf.py", + "size": 1862, + "sha256": "649221CC7406A3CF9607DE10CCA44C0F7D661E2B877209626704EBFF93F0BBC9" + }, + { + "path": "RiskLabAI/features/entropy_features/shannon.py", + "size": 723, + "sha256": "B594B49278CB1904E605CC44083B0A545F71E14B2BDDA6EF0A2BE03FAE2A2DA8" + }, + { + "path": "RiskLabAI/features/feature_importance/__init__.py", + "size": 2152, + "sha256": "681CBA32A99632E6FAC08636FC4C1E9758DFE385FA9DF0C7A6A7AADD3961CE77" + }, + { + "path": "RiskLabAI/features/feature_importance/clustered_feature_importance_mda.py", + "size": 5518, + "sha256": "656BBC12FE44777CE8557AF6A0F3292DA11A69B23C0F812E10501E9AFF4E2BF1" + }, + { + "path": "RiskLabAI/features/feature_importance/clustered_feature_importance_mdi.py", + "size": 3511, + "sha256": "965BFFF4A3EF79239B54C7DD5A09B7590C3EA1DF8F2B2F10695ADFA095CA45C7" + }, + { + "path": "RiskLabAI/features/feature_importance/debiased_importance.py", + "size": 11231, + "sha256": "2041C34A27484878E4D579219C3D264D1306C3B2D9EF55132FC191D2D7BCB7F0" + }, + { + "path": "RiskLabAI/features/feature_importance/feature_importance_controller.py", + "size": 1371, + "sha256": "67E55609549A88E0340497880A9529786EDA51FE72ADED3E29E852E6AFA93D2C" + }, + { + "path": "RiskLabAI/features/feature_importance/feature_importance_factory.py", + "size": 1592, + "sha256": "CC98444936C24A94EA5E2DD41558BD4EA123428706A3DB476E1F2A02DEE6A0E0" + }, + { + "path": "RiskLabAI/features/feature_importance/feature_importance_mda.py", + "size": 4650, + "sha256": "5E22AE26672B9B3A608CC237791625386C5B500FFBF768AEEC5478154F727CF0" + }, + { + "path": "RiskLabAI/features/feature_importance/feature_importance_mdi.py", + "size": 2739, + "sha256": "82F93E0DA408EB9D4984856C3C432704DC8093ADC90FE9DC898D2109E7381A92" + }, + { + "path": "RiskLabAI/features/feature_importance/feature_importance_sfi.py", + "size": 3984, + "sha256": "67CB65A25ACA73457000C23AC4853A1D281CA3C2DB3CE6614C1262AF51B808BF" + }, + { + "path": "RiskLabAI/features/feature_importance/feature_importance_strategy.py", + "size": 894, + "sha256": "E007852540710974A4C56089DB6A5DE46F11B19760690E400BAFCE154156BD1D" + }, + { + "path": "RiskLabAI/features/feature_importance/generate_synthetic_data.py", + "size": 2535, + "sha256": "67342708D90B9AC31BF31FF851E341D122600D7A75818F20CEFF31CB39E41747" + }, + { + "path": "RiskLabAI/features/feature_importance/orthogonal_features.py", + "size": 3140, + "sha256": "0250FFD2E9B7E2D1B5BFBF960D63EA32CBB0CD9335249222E37A66724B6B0FF9" + }, + { + "path": "RiskLabAI/features/feature_importance/weighted_tau.py", + "size": 974, + "sha256": "706F53A5531A3B5089A9A252883558C7561C9B1B0DB6B251904EE80A8961C098" + }, + { + "path": "RiskLabAI/features/microstructural_features/__init__.py", + "size": 737, + "sha256": "69C079797912E65103C77DD079B00F420C30479FEFEB7A3C0C3E70AF7BCC855A" + }, + { + "path": "RiskLabAI/features/microstructural_features/bekker_parkinson_volatility_estimator.py", + "size": 2058, + "sha256": "BC606B1E368BC738846DCA08DE31C99A0A42D91F0CF5F8BB686EDE14021821FF" + }, + { + "path": "RiskLabAI/features/microstructural_features/corwin_schultz.py", + "size": 3471, + "sha256": "9120D5D7F9763E017A896E5F7A3288F02112E1A72AA92C42717E6B2506070532" + }, + { + "path": "RiskLabAI/features/microstructural_features/edge.py", + "size": 6268, + "sha256": "98A0A232CDB6D1ECB2C268B0BBAFCACF23981CDE9D998D8936D7F2A3265C38D7" + }, + { + "path": "RiskLabAI/features/structural_breaks/__init__.py", + "size": 1134, + "sha256": "EC77F8B7B857C0898C23D9B3968EA957E4A281643644B7911B14E94F0057FA3D" + }, + { + "path": "RiskLabAI/features/structural_breaks/pelt_change_points.py", + "size": 4257, + "sha256": "7D84550BB9E246055031963755D99BC180793C92D839BFE4D29ECBB0EFC1D337" + }, + { + "path": "RiskLabAI/features/structural_breaks/structural_breaks.py", + "size": 26144, + "sha256": "36A586A0651EC6D00AFA737E5EAFC2BA5B5FAEACB85B58621C2B3C6F1C98EDA0" + }, + { + "path": "RiskLabAI/features/structural_breaks/volatility_robust_sadf.py", + "size": 5957, + "sha256": "8DE6D754B46AC7BE870ABC0D44F92B7EC633255CF4B0D11E402579EF4187DD56" + }, + { + "path": "RiskLabAI/hpc/__init__.py", + "size": 511, + "sha256": "C08896EA7791033B286FAB38AFDBD8EC5A79F562316AA470DD071F90C6F51384" + }, + { + "path": "RiskLabAI/hpc/hpc.py", + "size": 11685, + "sha256": "B5AEFF775459A0AD8A5AD0E10B0806E7FC08137F1CCD0CD85966FFF60D03D1C5" + }, + { + "path": "RiskLabAI/optimization/__init__.py", + "size": 1231, + "sha256": "B72FE375FA3A5970B3CD8C14738D59D8AB69FEB0E0BC69E75255A27AD701DD46" + }, + { + "path": "RiskLabAI/optimization/hedging.py", + "size": 1962, + "sha256": "4A87276BD9FBAF8726F8646E57C6B27E6274C3481692532A04F67CC0B206E149" + }, + { + "path": "RiskLabAI/optimization/hrp.py", + "size": 6323, + "sha256": "5A5F6F3D3145C9F7870CD1FEE15BD88D11A7C7E707D4A6EDCF83F9F43F92AED1" + }, + { + "path": "RiskLabAI/optimization/hyper_parameter_tuning.py", + "size": 6335, + "sha256": "C832378FA5F978B4379984FBB14DD57431B0C5DB8A02FBB7824580CA81EE9B14" + }, + { + "path": "RiskLabAI/optimization/nco.py", + "size": 4122, + "sha256": "96D9A482B49D9E784B6790B46E28EFE706E3C63A262521D721BD75C46A59085A" + }, + { + "path": "RiskLabAI/pde/__init__.py", + "size": 1805, + "sha256": "E47D207736DC6B23D0DB7EB51A780F9C72C567F980DAA5CF65B611ECE177B7B0" + }, + { + "path": "RiskLabAI/pde/equation.py", + "size": 10999, + "sha256": "4A9A073721FAAC4FFAC3513A61B08586027204248EE8A47F184E5F174DD86E06" + }, + { + "path": "RiskLabAI/pde/model.py", + "size": 12264, + "sha256": "98F8C874C57C04E28883BB9B4CFD526C0D1622E27C84F723E5883D30CBFF7001" + }, + { + "path": "RiskLabAI/pde/solver.py", + "size": 13845, + "sha256": "AA5A5B13772CE29ABD051AA5E114E77C2C92A0C908C1D4BB6A30F9BEB4B73CD0" + }, + { + "path": "RiskLabAI/utils/__init__.py", + "size": 3395, + "sha256": "BF7033686E83AB6215472969CAA63D368CEEE42657D303BB966E2320270FC53A" + }, + { + "path": "RiskLabAI/utils/constants.py", + "size": 3062, + "sha256": "31CE9261BA913AAE7E4C187E7AAFEB2FABC9028B159EC463AB532C2546C65AA9" + }, + { + "path": "RiskLabAI/utils/ewma.py", + "size": 1754, + "sha256": "7F64DE6409360FC02D93283E31D7E26B067E53BC74172DD6897A36201A804507" + }, + { + "path": "RiskLabAI/utils/momentum_mean_reverting_strategy_sides.py", + "size": 2156, + "sha256": "F19E1691AD40DF94640AEFBFA06431B39538444139277568B8A6AE247FB48509" + }, + { + "path": "RiskLabAI/utils/progress.py", + "size": 1945, + "sha256": "74B4D2EA57526348055B657EB254D92606C00C7A49DBC2B122D5DA34A44F75D2" + }, + { + "path": "RiskLabAI/utils/publication_plots.py", + "size": 5850, + "sha256": "B00FD12382E692BB3F74F2B37EFFAB1D95BC3641679F588BBB60FB0C203764D8" + }, + { + "path": "RiskLabAI/utils/update_figure_layout.py", + "size": 1324, + "sha256": "8C5C6DAEFE392D1AD59AF598B1D8D76732FD4B0A7A437AB3C4AB160CB1CDFFB2" + }, + { + "path": "docs/delete", + "size": 1, + "sha256": "01BA4719C80B6FE911B091A7C05124B64EEECE964E09C058EF8F9805DACA546B" + }, + { + "path": "documenter.py", + "size": 4742, + "sha256": "57869DFC92E1048499FDA60BDDEDB310E8C6A2862FB49139917AF616C3A929C2" + }, + { + "path": "style_guide.md", + "size": 3172, + "sha256": "9ED0A313A5E75589CB40E65ECE10873CDB05D0BC91C696B783BE5E726BF6906C" + }, + { + "path": "tree.py", + "size": 2923, + "sha256": "1838861E627D0AFB96673CE7365D6DFEA2841DC0535C8D025DC41AF765005AFC" + } + ], + "repository_tree_file_count": 353, + "repository_tree_file_list_sha256": "FBF55A5B6C5A30E2C52C065431063E59A0280CF8F30B34BC1BC20679BDFBB289", + "repository_tree_policy": "The repository tree is the exact disjoint union of active release-source files and already-public repository-only files. Repository-only files are preserved but are not wheel payload members or active package load roots.", + "runtime_source_file_count": 128, "runtime_source_files": [ "src/RiskLabAI/__init__.py", "src/RiskLabAI/_deprecation.py", @@ -480,11 +2744,15 @@ "src/RiskLabAI/backtest/validation/purged_kfold.py", "src/RiskLabAI/backtest/validation/walk_forward.py", "src/RiskLabAI/causal_factor_analysis/__init__.py", + "src/RiskLabAI/causal_factor_analysis/allocation_diagnostics.py", "src/RiskLabAI/causal_factor_analysis/factor_mirage.py", "src/RiskLabAI/causal_factor_analysis/graph_identification.py", + "src/RiskLabAI/causal_factor_analysis/graph_roles.py", "src/RiskLabAI/causal_factor_analysis/optimizer.py", "src/RiskLabAI/causal_factor_analysis/protocol.py", + "src/RiskLabAI/causal_factor_analysis/search_adjusted_fdr.py", "src/RiskLabAI/causal_factor_analysis/specification_diagnostics.py", + "src/RiskLabAI/causal_factor_analysis/structural_models.py", "src/RiskLabAI/causal_factor_analysis/treatment_effects.py", "src/RiskLabAI/cluster/__init__.py", "src/RiskLabAI/cluster/clustering.py", @@ -575,7 +2843,7 @@ "src/RiskLabAI/utils/publication_plots.py", "src/RiskLabAI/utils/update_figure_layout.py" ], - "test_file_count": 78, + "test_file_count": 85, "test_files": [ "test/backtest/test_advanced_bet_sizing.py", "test/backtest/test_backtest_overfitting_simulation.py", @@ -602,12 +2870,19 @@ "test/backtest/validation/test_path_bagged_cpcv.py", "test/backtest/validation/test_purged_kfold.py", "test/backtest/validation/test_walk_forward.py", + "test/causal_factor_analysis/fixtures/additive_numeric_parity.tsv", + "test/causal_factor_analysis/test_additive_numeric_parity.py", + "test/causal_factor_analysis/test_allocation_diagnostics.py", "test/causal_factor_analysis/test_cfi_book_graph_examples.py", "test/causal_factor_analysis/test_factor_mirage.py", + "test/causal_factor_analysis/test_generalized_factor_mirage.py", "test/causal_factor_analysis/test_graph_identification.py", + "test/causal_factor_analysis/test_graph_roles.py", "test/causal_factor_analysis/test_minimum_variance.py", "test/causal_factor_analysis/test_protocol.py", + "test/causal_factor_analysis/test_search_adjusted_fdr.py", "test/causal_factor_analysis/test_specification_diagnostics.py", + "test/causal_factor_analysis/test_structural_models.py", "test/causal_factor_analysis/test_treatment_effects.py", "test/cluster/test_clustering.py", "test/controller/test_bars_initializer.py", @@ -687,11 +2962,15 @@ "src/RiskLabAI/backtest/validation/purged_kfold.py", "src/RiskLabAI/backtest/validation/walk_forward.py", "src/RiskLabAI/causal_factor_analysis/__init__.py", + "src/RiskLabAI/causal_factor_analysis/allocation_diagnostics.py", "src/RiskLabAI/causal_factor_analysis/factor_mirage.py", "src/RiskLabAI/causal_factor_analysis/graph_identification.py", + "src/RiskLabAI/causal_factor_analysis/graph_roles.py", "src/RiskLabAI/causal_factor_analysis/optimizer.py", "src/RiskLabAI/causal_factor_analysis/protocol.py", + "src/RiskLabAI/causal_factor_analysis/search_adjusted_fdr.py", "src/RiskLabAI/causal_factor_analysis/specification_diagnostics.py", + "src/RiskLabAI/causal_factor_analysis/structural_models.py", "src/RiskLabAI/causal_factor_analysis/treatment_effects.py", "src/RiskLabAI/cluster/__init__.py", "src/RiskLabAI/cluster/clustering.py", @@ -783,6 +3062,6 @@ "src/RiskLabAI/utils/update_figure_layout.py" ], "generated_metadata_members": { - "policy": "Generated package-manager metadata is not pre-enumerated by filename here; the future artifact inspector must validate its normalized field set and reject undeclared payload files." + "policy": "Future package-manager-generated metadata must be validated by normalized fields and must not introduce undeclared payload files." } } diff --git a/PACKAGE_INSPECTION.md b/PACKAGE_INSPECTION.md index fceac72..49faeb8 100644 --- a/PACKAGE_INSPECTION.md +++ b/PACKAGE_INSPECTION.md @@ -1,8 +1,8 @@ # Future Python package inspection specification -This specification applies only after a separate human authorization creates a -temporary RiskLabAI 3.0.0 wheel and source distribution. It does not authorize -building, installing, uploading, publishing, tagging, or releasing anything. +This specification governs temporary local artifacts built from the exact +RiskLabAI 3.1.0 additive integration candidate. It does not authorize using +version control, uploading, publishing, tagging, or releasing anything. ## Inputs @@ -11,6 +11,11 @@ The inspector must use the final, hash-locked source tree and compare it with `pyproject.toml`. It must reject a changed input, an undecodable file, an unresolved link, or a concurrent mutation before considering an artifact. +`PACKAGE_FILES.json` separately records active release-source files and +already-public repository-only files. Human repository integration preserves +both classes without deletion; only the active class may enter the source +distribution, and only its declared runtime subset may enter the wheel. + ## Archive safety and membership For both archive formats, reject absolute paths, drive-qualified paths, @@ -18,22 +23,23 @@ backslashes, traversal components, duplicate paths, case-fold or Unicode normalization collisions, control characters, trailing dots or spaces, alternate data streams, links, device files, and unexpected native binaries. -The wheel may contain only the 124 declared Python runtime modules plus the -standard metadata directory created for RiskLabAI 3.0.0. Tests, examples, -local readiness controls, caches, bytecode, credentials, and undeclared data -must not be present in the wheel. The source distribution must contain exactly -the 215 release-source members declared in `PACKAGE_FILES.json`, including the -governed `.github/workflows/ci.yml`, plus only the +The wheel may contain only the Python runtime modules declared in +`PACKAGE_FILES.json`, plus the normalized standard metadata directory for the +approved version. Tests, examples, analytical control records, caches, bytecode, +credentials, and undeclared data must not be present in the wheel. The source +distribution must contain exactly the release-source members declared in +`PACKAGE_FILES.json`, including the governed workflow, plus only the backend-generated metadata members that the inspection record enumerates and hashes explicitly. ## Metadata -The normalized package metadata must agree exactly with `pyproject.toml`: -name RiskLabAI, version 3.0.0, Python 3.12 through 3.14, BSD-3-Clause, the -declared maintainer and contact, repository, issue, and documentation links, -all required dependency clauses, and every optional group. No dynamic version, -entry point, plugin, executable script, or undeclared dependency is permitted. +The normalized package metadata must agree exactly with the human-approved +`pyproject.toml`: name RiskLabAI, the frozen additive version, Python 3.12 +through 3.14, BSD-3-Clause, the declared maintainer and contact, repository, +issue, and documentation links, all required dependency clauses, and every +optional group. No dynamic version, entry point, plugin, executable script, or +undeclared dependency is permitted. Every wheel record must have a valid cryptographic digest and size, except its own permitted empty digest fields. The license and long description must match @@ -42,12 +48,14 @@ the reviewed source bytes. ## Runtime checks In fresh, isolated environments for every supported Python and valid NumPy -lane, verify the exact module origins, `RiskLabAI.__version__ == "3.0.0"`, the -13 root exports, all module and package exports in `PUBLIC_API.json`, and the -complete 57-feature causal API. For every module that declares `__all__`, -require every listed name to be bound on that installed module and resolvable by -an explicit import; an advertised but unbound name rejects the artifact. Run -the exact package-scoped test inventory and the valid optional-feature lanes. +lane, verify the exact module origins, the approved version binding, the root +exports, all module and package exports in `PUBLIC_API.json`, and the complete +87-name causal API. Require the released 57-name causal prefix to remain exact +and the 30 additions to match the parity contract. For every module that +declares `__all__`, require every listed name to be bound on that installed +module and resolvable by an explicit import; an advertised but unbound name +rejects the artifact. Run the exact package-scoped test inventory and the valid +optional-feature lanes. Confirm that optional groups do not become base dependencies and that changepoints alone is unavailable on Python 3.14. Run installed-package tests outside the source checkout and reject any import whose resolved path remains diff --git a/PUBLIC_API.json b/PUBLIC_API.json index 026794f..0c6ab2b 100644 --- a/PUBLIC_API.json +++ b/PUBLIC_API.json @@ -1,12 +1,21 @@ { - "schema_version": 1, + "schema_version": 2, "document_type": "RISKLABAI_PYTHON_PUBLIC_API_INVENTORY", "product": "RiskLabAI", "language": "Python", - "version": "3.0.0", + "version": "3.1.0", + "baseline_contract": { + "released_version": "3.0.0", + "released_causal_export_count": 57, + "additive_export_count": 30, + "total_causal_export_count": 87, + "package_version_change_deferred": false, + "integrated_version": "3.1.0" + }, "inventory_rule": { "root_and_package_exports": "Literal __all__ when present; otherwise imported public bindings in package initializers.", - "module_definitions": "Every non-underscore top-level function or class and every public uppercase constant is frozen conservatively." + "module_definitions": "Every non-underscore top-level function or class and every public uppercase constant is recorded conservatively.", + "runtime_origin": "src/RiskLabAI/__init__.py" }, "root_exports": [ "__version__", @@ -23,13 +32,13 @@ "pde", "utils" ], - "runtime_module_count": 124, + "runtime_module_count": 128, "runtime_modules": [ { "path": "src/RiskLabAI/__init__.py", "module": "RiskLabAI", "module_kind": "package", - "sha256": "70093B30E9B7B92F555E40FB0DF936092A661BC88CA032549F1982624147A4AC", + "sha256": "1CB2A7E2B804267FFEBE4FF124F01AA095DC19F82492DB862C9A4D97C2A1AF0F", "size": 1095, "defined_public_names": [], "definitions": [] @@ -67,8 +76,8 @@ "path": "src/RiskLabAI/backtest/__init__.py", "module": "RiskLabAI.backtest", "module_kind": "package", - "sha256": "BC1D1DB5707902C0C61EFBB1DB1CDC88101388B065BCA30FE7A8CA621667058C", - "size": 6172, + "sha256": "59475AA703033C284FED55C308AB258B79AC6E31A8EE5164B01AF6E9396E4C8C", + "size": 6204, "defined_public_names": [], "definitions": [] }, @@ -76,8 +85,8 @@ "path": "src/RiskLabAI/backtest/advanced_bet_sizing.py", "module": "RiskLabAI.backtest.advanced_bet_sizing", "module_kind": "module", - "sha256": "E50F8275DC235F7DA777146D0715B945BAA3C7B91AEDA1FBDAA210D44C4A099F", - "size": 10017, + "sha256": "1D90B97962BED3711265488A7921C651585FBF0D366A412607944EAF44B9C810", + "size": 10098, "defined_public_names": [ "PlattCalibrator", "distributionally_robust_kelly_fraction", @@ -389,8 +398,8 @@ "path": "src/RiskLabAI/backtest/multiple_testing.py", "module": "RiskLabAI.backtest.multiple_testing", "module_kind": "module", - "sha256": "8B867AE2529D3A354CE0847D9C08E90DDE29E9E20042C0888683EBB98B89AAA8", - "size": 7841, + "sha256": "FAA5E3C9E225869FC204BB5DA4C88830A81989A18FC927706FBC3BCCDEE7936B", + "size": 7855, "defined_public_names": [ "benjamini_hochberg_yekutieli_adjusted_p_values", "haircut_sharpe_ratios", @@ -424,8 +433,8 @@ "path": "src/RiskLabAI/backtest/ou_trading_rules.py", "module": "RiskLabAI.backtest.ou_trading_rules", "module_kind": "module", - "sha256": "D48DFA47F2C98EF791E66AA7724C29153813F7742CD244E88FAAFD6062231742", - "size": 12532, + "sha256": "A202998DC64A9C3F68544AFFF3DA48798C99C4B19A3B9600F5E1030247551182", + "size": 12573, "defined_public_names": [ "fit_ornstein_uhlenbeck", "hit_upper_probability", @@ -523,8 +532,8 @@ "path": "src/RiskLabAI/backtest/robust_statistics.py", "module": "RiskLabAI.backtest.robust_statistics", "module_kind": "module", - "sha256": "6144EAD8326C07605EF783AFC41AC49884A4DB3BD56F6D3DA8CFBE93AD3C0F29", - "size": 11865, + "sha256": "9432CBA7E36A8C1F9BA69D6F912D226FB78FEC4069E940E8380EFB1543696974", + "size": 11911, "defined_public_names": [ "conditional_expected_drawdown", "sharpe_difference_test" @@ -546,8 +555,8 @@ "path": "src/RiskLabAI/backtest/sharpe_inference.py", "module": "RiskLabAI.backtest.sharpe_inference", "module_kind": "module", - "sha256": "CB3DE49BA2230604868ECAB11B4AA5C1C55AE4BCF47FF6ACD8FE2C71B4839E2C", - "size": 7799, + "sha256": "58A436922785F2D087FC6C40C1881228E84AAB7BC96B4D8C2C0161FA2F05E0A4", + "size": 7721, "defined_public_names": [ "lplz_sharpe_inference", "newey_west_automatic_lag", @@ -693,8 +702,8 @@ "path": "src/RiskLabAI/backtest/validation/__init__.py", "module": "RiskLabAI.backtest.validation", "module_kind": "package", - "sha256": "88C99E37BE96E2BD23FD54ED0491C962F2BDEDD696E97F9B2627A410D1F8660F", - "size": 1896, + "sha256": "18D99DF58743C28835ACD00F7B307014D919C5FC322D6A639AE126E44AFDFFB5", + "size": 1925, "defined_public_names": [], "definitions": [] }, @@ -821,8 +830,8 @@ "path": "src/RiskLabAI/backtest/validation/leakage_aware_hpo.py", "module": "RiskLabAI.backtest.validation.leakage_aware_hpo", "module_kind": "module", - "sha256": "A8D16874C56CFDB725B92906F5A08A1605DA93B263EDA210B3B561310E188410", - "size": 9891, + "sha256": "FF0040F22C4710918CD39CF3A08BC48293ED48A70A8F46898B3C4686122B6D2C", + "size": 9930, "defined_public_names": [ "deflated_sharpe_gate", "leakage_aware_hpo" @@ -844,8 +853,8 @@ "path": "src/RiskLabAI/backtest/validation/path_adaptive_cpcv.py", "module": "RiskLabAI.backtest.validation.path_adaptive_cpcv", "module_kind": "module", - "sha256": "4A1BCEDEA466B9C05165C282740CC1EDBB96174629CC377276432C3325C2E3B6", - "size": 10541, + "sha256": "4257BE918DA88233CE4ABA8AD2A0BAB2859C9B711AE1A591ADABDA33C7312668", + "size": 10437, "defined_public_names": [ "adaptive_probability_of_backtest_overfitting", "estimate_volatility_regimes" @@ -867,8 +876,8 @@ "path": "src/RiskLabAI/backtest/validation/path_bagged_cpcv.py", "module": "RiskLabAI.backtest.validation.path_bagged_cpcv", "module_kind": "module", - "sha256": "CB9FDF560D59DD7E035DD194DC1F78EDB392A9072ADE4959F6D0B55D3EFD5B83", - "size": 5682, + "sha256": "195191CEDCC24386D2862C886EA323E86F9B74371379B5C825CB9EFB069382A2", + "size": 5699, "defined_public_names": [ "bagged_probability_of_backtest_overfitting", "moving_block_bootstrap_indices" @@ -924,17 +933,40 @@ "path": "src/RiskLabAI/causal_factor_analysis/__init__.py", "module": "RiskLabAI.causal_factor_analysis", "module_kind": "package", - "sha256": "6B6153F1D4C87E8FA6DEDF123CFD27DA3AAD23ED1DEDE271684DFC93E71A3F3C", - "size": 4290, + "sha256": "96B6E483A72D5CBD00B5EDC5762345E7D31F31F6255B91C793C00B9B2BE62CA4", + "size": 6656, "defined_public_names": [], "definitions": [] }, + { + "path": "src/RiskLabAI/causal_factor_analysis/allocation_diagnostics.py", + "module": "RiskLabAI.causal_factor_analysis.allocation_diagnostics", + "module_kind": "module", + "sha256": "089677E3EF24979A421676BD4253EA684CB6819D7D6405FB78A1575F2F47C35C", + "size": 11041, + "defined_public_names": [ + "AllocationMisspecificationDiagnostics", + "allocation_misspecification_diagnostics" + ], + "definitions": [ + { + "name": "AllocationMisspecificationDiagnostics", + "kind": "class", + "signature": null + }, + { + "name": "allocation_misspecification_diagnostics", + "kind": "function", + "signature": "(covariance: ArrayLike, reference_factor_exposures: ArrayLike, reference_target_exposures: ArrayLike, misspecified_factor_exposures: ArrayLike, misspecified_target_exposures: ArrayLike)" + } + ] + }, { "path": "src/RiskLabAI/causal_factor_analysis/factor_mirage.py", "module": "RiskLabAI.causal_factor_analysis.factor_mirage", "module_kind": "module", - "sha256": "5E5C44C8F25E926E5C1528D623BAA6886548E32C1308DB65B5AFB907988C522B", - "size": 14673, + "sha256": "57D44C3EF8608CD9A6E917589E3B6BD37A2DDAF300A7BF983EFBD8206EF2AC6D", + "size": 18292, "defined_public_names": [ "ColliderCoefficients", "ColliderDiagnostics", @@ -945,7 +977,9 @@ "collider_overcontrolled_coefficients", "confounder_factor_return", "confounder_forecast_return", - "confounder_undercontrolled_coefficient" + "confounder_undercontrolled_coefficient", + "generalized_collider_overcontrolled_coefficients", + "generalized_confounder_undercontrolled_coefficient" ], "definitions": [ { @@ -968,6 +1002,11 @@ "kind": "function", "signature": "(beta: Real, gamma: Real, delta: Real)" }, + { + "name": "generalized_confounder_undercontrolled_coefficient", + "kind": "function", + "signature": "(beta: Real, gamma: Real, delta: Real, *, confounder_variance: Real, exposure_noise_variance: Real)" + }, { "name": "confounder_factor_return", "kind": "function", @@ -983,6 +1022,11 @@ "kind": "function", "signature": "(beta: Real, gamma: Real, delta: Real)" }, + { + "name": "generalized_collider_overcontrolled_coefficients", + "kind": "function", + "signature": "(beta: Real, gamma: Real, delta: Real, *, outcome_noise_variance: Real, collider_noise_variance: Real)" + }, { "name": "collider_factor_return", "kind": "function", @@ -1095,6 +1139,47 @@ } ] }, + { + "path": "src/RiskLabAI/causal_factor_analysis/graph_roles.py", + "module": "RiskLabAI.causal_factor_analysis.graph_roles", + "module_kind": "module", + "sha256": "415086CF1403FB0EE955184F224C9B11ACC7047943D70A39AB969A1C0CD793AD", + "size": 6324, + "defined_public_names": [ + "FactorControlRoles", + "TreatmentOutcomeRole", + "TreatmentOutcomeRoleEvidence", + "classify_treatment_outcome_role", + "factor_control_roles" + ], + "definitions": [ + { + "name": "FactorControlRoles", + "kind": "class", + "signature": null + }, + { + "name": "TreatmentOutcomeRole", + "kind": "class", + "signature": null + }, + { + "name": "TreatmentOutcomeRoleEvidence", + "kind": "class", + "signature": null + }, + { + "name": "factor_control_roles", + "kind": "function", + "signature": "(dag: CausalDAG, target_factor: str)" + }, + { + "name": "classify_treatment_outcome_role", + "kind": "function", + "signature": "(dag: CausalDAG, treatment: str, outcome: str, node: str)" + } + ] + }, { "path": "src/RiskLabAI/causal_factor_analysis/optimizer.py", "module": "RiskLabAI.causal_factor_analysis.optimizer", @@ -1195,6 +1280,131 @@ } ] }, + { + "path": "src/RiskLabAI/causal_factor_analysis/search_adjusted_fdr.py", + "module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr", + "module_kind": "module", + "sha256": "ADA0F9B213BC90CC3B313EAC8C58DCC50082A2992618C8EAEE99736A6DF5C972", + "size": 28380, + "defined_public_names": [ + "FDRComparisonEvidence", + "FDRNonIdentificationWitness", + "GaussianSearchAdjustedFDR", + "GaussianTrialMixture", + "MaxSelectionFamilyErrors", + "SelectionLevelProbabilityEvidence", + "compare_single_and_family_fdr", + "conditional_upper_tail_probability", + "family_level_false_discovery_rate", + "fdr_nonidentification_witness", + "gaussian_max_selection_cdf", + "gaussian_max_selection_log_density", + "gaussian_max_selection_log_likelihood", + "gaussian_search_adjusted_false_discovery_rate", + "gaussian_trial_mixture_cdf", + "max_selection_family_errors", + "max_selection_null_probability", + "maximum_mixture_cdf", + "single_trial_false_discovery_rate" + ], + "definitions": [ + { + "name": "MaxSelectionFamilyErrors", + "kind": "class", + "signature": null + }, + { + "name": "FDRComparisonEvidence", + "kind": "class", + "signature": null + }, + { + "name": "GaussianTrialMixture", + "kind": "class", + "signature": null + }, + { + "name": "GaussianSearchAdjustedFDR", + "kind": "class", + "signature": null + }, + { + "name": "FDRNonIdentificationWitness", + "kind": "class", + "signature": null + }, + { + "name": "SelectionLevelProbabilityEvidence", + "kind": "class", + "signature": null + }, + { + "name": "single_trial_false_discovery_rate", + "kind": "function", + "signature": "(type_i_error: Real, type_ii_error: Real, trial_null_probability: Real)" + }, + { + "name": "family_level_false_discovery_rate", + "kind": "function", + "signature": "(family_type_i_error: Real, family_type_ii_error: Real, trial_null_probability: Real, n_trials: Integral)" + }, + { + "name": "max_selection_family_errors", + "kind": "function", + "signature": "(trial_type_i_error: Real, trial_type_ii_error: Real, trial_null_probability: Real, n_trials: Integral)" + }, + { + "name": "compare_single_and_family_fdr", + "kind": "function", + "signature": "(trial_type_i_error: Real, trial_type_ii_error: Real, trial_null_probability: Real, n_trials: Integral, *, family_type_i_error: Real, family_type_ii_error: Real, relative_tolerance: Real=1e-12, absolute_tolerance: Real=1e-15)" + }, + { + "name": "maximum_mixture_cdf", + "kind": "function", + "signature": "(null_cdf: Real, alternative_cdf: Real, trial_null_probability: Real, n_trials: Integral)" + }, + { + "name": "conditional_upper_tail_probability", + "kind": "function", + "signature": "(cdf_at_value: Real, cdf_at_threshold: Real)" + }, + { + "name": "gaussian_trial_mixture_cdf", + "kind": "function", + "signature": "(model: GaussianTrialMixture, x: Real)" + }, + { + "name": "gaussian_max_selection_cdf", + "kind": "function", + "signature": "(model: GaussianTrialMixture, x: Real, n_trials: Integral)" + }, + { + "name": "gaussian_max_selection_log_density", + "kind": "function", + "signature": "(model: GaussianTrialMixture, x: Real, n_trials: Integral)" + }, + { + "name": "gaussian_max_selection_log_likelihood", + "kind": "function", + "signature": "(model: GaussianTrialMixture, observations: ArrayLike, n_trials: Integral)" + }, + { + "name": "gaussian_search_adjusted_false_discovery_rate", + "kind": "function", + "signature": "(model: GaussianTrialMixture, thresholds: ArrayLike, n_trials: Integral)" + }, + { + "name": "fdr_nonidentification_witness", + "kind": "function", + "signature": "(observable_cdf_at_threshold: Real, target_false_discovery_rate: Real)" + }, + { + "name": "max_selection_null_probability", + "kind": "function", + "signature": "(threshold: Real, trial_null_probability: Real, n_trials: Integral, *, null_density: Callable[[float], Real], mixture_cdf: Callable[[float], Real], absolute_tolerance: Real=1e-12, relative_tolerance: Real=1e-10, max_subintervals: Integral=200)" + } + ] + }, { "path": "src/RiskLabAI/causal_factor_analysis/specification_diagnostics.py", "module": "RiskLabAI.causal_factor_analysis.specification_diagnostics", @@ -1266,6 +1476,29 @@ } ] }, + { + "path": "src/RiskLabAI/causal_factor_analysis/structural_models.py", + "module": "RiskLabAI.causal_factor_analysis.structural_models", + "module_kind": "module", + "sha256": "CC69BC922ADF6A4292B637CF97E96B607903608529056C03F66E041107BE345D", + "size": 6877, + "defined_public_names": [ + "StructuralCausalModelResult", + "evaluate_structural_causal_model" + ], + "definitions": [ + { + "name": "StructuralCausalModelResult", + "kind": "class", + "signature": null + }, + { + "name": "evaluate_structural_causal_model", + "kind": "function", + "signature": "(dag: CausalDAG, mechanisms: Mapping[str, Callable[[NDArray[np.float64], NDArray[np.float64]], ArrayLike]], exogenous_inputs: Mapping[str, ArrayLike])" + } + ] + }, { "path": "src/RiskLabAI/causal_factor_analysis/treatment_effects.py", "module": "RiskLabAI.causal_factor_analysis.treatment_effects", @@ -1547,8 +1780,8 @@ "path": "src/RiskLabAI/data/denoise/__init__.py", "module": "RiskLabAI.data.denoise", "module_kind": "package", - "sha256": "72E4069E071BF4ED278796A6FF14751B99ED9B64D71D0175F57150C1F00427F0", - "size": 844, + "sha256": "DE4D01EA8A0323F1FC39DA81B487F5770D77B4D08DB6BE2F127638B5592CB7EE", + "size": 854, "defined_public_names": [], "definitions": [] }, @@ -1587,16 +1820,16 @@ "kind": "function", "signature": "(eigenvalues: np.ndarray, q: float, bandwidth: float)" }, - { - "name": "denoised_corr", - "kind": "function", - "signature": "(eigenvalues: np.ndarray, eigenvectors: np.ndarray, num_facts: int)" - }, - { - "name": "denoised_corr2", - "kind": "function", - "signature": "(eigenvalues: np.ndarray, eigenvectors: np.ndarray, num_factors: int, alpha: float=0.0)" - }, + { + "name": "denoised_corr", + "kind": "function", + "signature": "(eigenvalues: np.ndarray, eigenvectors: np.ndarray, num_facts: int)" + }, + { + "name": "denoised_corr2", + "kind": "function", + "signature": "(eigenvalues: np.ndarray, eigenvectors: np.ndarray, num_factors: int, alpha: float=0.0)" + }, { "name": "pca", "kind": "function", @@ -1633,8 +1866,8 @@ "path": "src/RiskLabAI/data/denoise/nercome.py", "module": "RiskLabAI.data.denoise.nercome", "module_kind": "module", - "sha256": "81FEA0C543026F0C927DBC54CAC60DA5F3829DAFD84EAB788B59F7F8462BAFBB", - "size": 6386, + "sha256": "AA11FB31EBE08631B7D979562E1A568E49B70FE9FD2CBA1304EA12908AD434B8", + "size": 6469, "defined_public_names": [ "nercome_denoised_covariance" ], @@ -1659,8 +1892,8 @@ "path": "src/RiskLabAI/data/differentiation/adaptive_differentiation.py", "module": "RiskLabAI.data.differentiation.adaptive_differentiation", "module_kind": "module", - "sha256": "BD56143C44222FBACDF490FA366063CA04559FD180FF4B62E667F9288139F610", - "size": 11374, + "sha256": "CB0DF7A28212D4CE0B15AF7023235B6AABD0EA9077D64572F3E5EB45D9A6A066", + "size": 11222, "defined_public_names": [ "DEFAULT_ADF_MAXLAG", "DEFAULT_ADF_SIGNIFICANCE", @@ -1766,8 +1999,8 @@ "path": "src/RiskLabAI/data/distance/codependence.py", "module": "RiskLabAI.data.distance.codependence", "module_kind": "module", - "sha256": "DF6E672A5D25468FCE63B36C6AFAAF55B6AD14486E34D4CC42D84D7B43F278DA", - "size": 7241, + "sha256": "9033470188D780110795D53CEFD230BC3D5AE54BD8BA945908CB73F3C1B48B9A", + "size": 7268, "defined_public_names": [ "distance_correlation", "ksg_mutual_information" @@ -1927,8 +2160,8 @@ "path": "src/RiskLabAI/data/structures/__init__.py", "module": "RiskLabAI.data.structures", "module_kind": "package", - "sha256": "F0E79621D4AF2B9E8C8BE1EBEF26EBE66536A5A3A50CF09E1A16BAEA94834398", - "size": 873, + "sha256": "051BD4E2CF067134B61FC3CCEA16A9F20B94676F5C3D0DD01041F28541132956", + "size": 883, "defined_public_names": [], "definitions": [] }, @@ -2004,8 +2237,8 @@ "path": "src/RiskLabAI/data/structures/hardened_information_driven_bars.py", "module": "RiskLabAI.data.structures.hardened_information_driven_bars", "module_kind": "module", - "sha256": "48BCDD8B73D5DED1815505696C36D56528ECBAED3A295EDCE31DE56AB2B3B8EB", - "size": 6781, + "sha256": "E5CF32132DADEF61624F24E2AC42E673C0B4B274DC317DD6488539A3FC0F1741", + "size": 6880, "defined_public_names": [ "HardenedExpectedImbalanceBars", "HardenedExpectedRunBars" @@ -2326,8 +2559,8 @@ "path": "src/RiskLabAI/features/entropy_features/bias_corrected.py", "module": "RiskLabAI.features.entropy_features.bias_corrected", "module_kind": "module", - "sha256": "B6E66D401B569B26452EC1129A8FB5F0B150A990595BEEBFA92437457B1DAC1D", - "size": 12824, + "sha256": "40B156C643E48DAE6F06440D9FD1BE2ED651F18FA8C7A2BD0DB569795E44DCFE", + "size": 12893, "defined_public_names": [ "grassberger_entropy", "miller_madow_entropy", @@ -2452,8 +2685,8 @@ "path": "src/RiskLabAI/features/feature_importance/__init__.py", "module": "RiskLabAI.features.feature_importance", "module_kind": "package", - "sha256": "24C80AA3C46B3237D6FBC513C4E4A7EED88A54304C94792E5F24F295A00AFB73", - "size": 2076, + "sha256": "E3BD3D4CDB85D31F9096C459F8468DF540D41D4CF619B73A9CBBFF4DFBB1A75D", + "size": 2086, "defined_public_names": [], "definitions": [] }, @@ -2495,8 +2728,8 @@ "path": "src/RiskLabAI/features/feature_importance/debiased_importance.py", "module": "RiskLabAI.features.feature_importance.debiased_importance", "module_kind": "module", - "sha256": "49B8D749041A18283724E5563E1FBB70F4143BECE89C109EA70B2D1BC7BB5750", - "size": 11360, + "sha256": "299068C70E8551367167F221055F52BF54848D048A6C5B12EAF9BAC08727045C", + "size": 11396, "defined_public_names": [ "conditional_predictive_impact", "mdi_plus_importance" @@ -2738,8 +2971,8 @@ "path": "src/RiskLabAI/features/microstructural_features/edge.py", "module": "RiskLabAI.features.microstructural_features.edge", "module_kind": "module", - "sha256": "D9C99CB172B1AA9B6F33AF39723D9BE4880214466F863F6177D7B028D7F73B61", - "size": 6251, + "sha256": "98A0A232CDB6D1ECB2C268B0BBAFCACF23981CDE9D998D8936D7F2A3265C38D7", + "size": 6268, "defined_public_names": [ "edge_estimator" ], @@ -2755,8 +2988,8 @@ "path": "src/RiskLabAI/features/structural_breaks/__init__.py", "module": "RiskLabAI.features.structural_breaks", "module_kind": "package", - "sha256": "46AA8C0C7DA4955E2A6397B7FCCC51B6D48C3A7016E9413A4D356EDF22D69C2F", - "size": 1122, + "sha256": "EC77F8B7B857C0898C23D9B3968EA957E4A281643644B7911B14E94F0057FA3D", + "size": 1134, "defined_public_names": [], "definitions": [] }, @@ -2764,8 +2997,8 @@ "path": "src/RiskLabAI/features/structural_breaks/pelt_change_points.py", "module": "RiskLabAI.features.structural_breaks.pelt_change_points", "module_kind": "module", - "sha256": "1F913C5EE9BEF2144D687068F0526C8A0F0AE5A9B0A51991E5EC686701307070", - "size": 4218, + "sha256": "7D84550BB9E246055031963755D99BC180793C92D839BFE4D29ECBB0EFC1D337", + "size": 4257, "defined_public_names": [ "pelt_change_points" ], @@ -2781,8 +3014,8 @@ "path": "src/RiskLabAI/features/structural_breaks/structural_breaks.py", "module": "RiskLabAI.features.structural_breaks.structural_breaks", "module_kind": "module", - "sha256": "0C562C044C063DFA0606EB88D74A33B54B08A69FA19A126D1B8DB7FCD3276242", - "size": 25396, + "sha256": "F81089327CA345631BBBB943BD431E7F91B2AF30728946602C86B115C1759032", + "size": 25436, "defined_public_names": [ "compute_beta", "get_bsadf_sequence", @@ -2858,8 +3091,8 @@ "path": "src/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py", "module": "RiskLabAI.features.structural_breaks.volatility_robust_sadf", "module_kind": "module", - "sha256": "69B7F026E05E66DE4755E1A591602A59AD3B5CFE49B29DE03724F37919BDD27E", - "size": 5904, + "sha256": "8DE6D754B46AC7BE870ABC0D44F92B7EC633255CF4B0D11E402579EF4187DD56", + "size": 5957, "defined_public_names": [ "volatility_robust_sadf" ], @@ -3560,7 +3793,37 @@ "minimum_variance_factor_weights", "randomized_mean_difference", "treatment_effect_decomposition", - "validate_causal_factor_protocol" + "validate_causal_factor_protocol", + "AllocationMisspecificationDiagnostics", + "FDRComparisonEvidence", + "FDRNonIdentificationWitness", + "FactorControlRoles", + "GaussianSearchAdjustedFDR", + "GaussianTrialMixture", + "MaxSelectionFamilyErrors", + "SelectionLevelProbabilityEvidence", + "StructuralCausalModelResult", + "TreatmentOutcomeRole", + "TreatmentOutcomeRoleEvidence", + "allocation_misspecification_diagnostics", + "classify_treatment_outcome_role", + "compare_single_and_family_fdr", + "conditional_upper_tail_probability", + "evaluate_structural_causal_model", + "factor_control_roles", + "family_level_false_discovery_rate", + "fdr_nonidentification_witness", + "gaussian_max_selection_cdf", + "gaussian_max_selection_log_density", + "gaussian_max_selection_log_likelihood", + "gaussian_search_adjusted_false_discovery_rate", + "gaussian_trial_mixture_cdf", + "generalized_collider_overcontrolled_coefficients", + "generalized_confounder_undercontrolled_coefficient", + "max_selection_family_errors", + "max_selection_null_probability", + "maximum_mixture_cdf", + "single_trial_false_discovery_rate" ] }, { @@ -3903,7 +4166,7 @@ } ], "causal_factor_analysis": { - "export_count": 57, + "export_count": 87, "ordered_exports": [ "BackdoorAdjustmentEvidence", "BacktestStage", @@ -3961,14 +4224,45 @@ "minimum_variance_factor_weights", "randomized_mean_difference", "treatment_effect_decomposition", - "validate_causal_factor_protocol" + "validate_causal_factor_protocol", + "AllocationMisspecificationDiagnostics", + "FDRComparisonEvidence", + "FDRNonIdentificationWitness", + "FactorControlRoles", + "GaussianSearchAdjustedFDR", + "GaussianTrialMixture", + "MaxSelectionFamilyErrors", + "SelectionLevelProbabilityEvidence", + "StructuralCausalModelResult", + "TreatmentOutcomeRole", + "TreatmentOutcomeRoleEvidence", + "allocation_misspecification_diagnostics", + "classify_treatment_outcome_role", + "compare_single_and_family_fdr", + "conditional_upper_tail_probability", + "evaluate_structural_causal_model", + "factor_control_roles", + "family_level_false_discovery_rate", + "fdr_nonidentification_witness", + "gaussian_max_selection_cdf", + "gaussian_max_selection_log_density", + "gaussian_max_selection_log_likelihood", + "gaussian_search_adjusted_false_discovery_rate", + "gaussian_trial_mixture_cdf", + "generalized_collider_overcontrolled_coefficients", + "generalized_confounder_undercontrolled_coefficient", + "max_selection_family_errors", + "max_selection_null_probability", + "maximum_mixture_cdf", + "single_trial_false_discovery_rate" ], - "ordered_export_sha256": "6EF5A71729FA52EF02B2275A9E10868F3284ADDDA3C21585AA9D3074BDB751F1", + "ordered_export_sha256": "31F7167C72B0EC62C6D6BC6ACE66D3DF79C9C6296B4541E22CBC05D7A9D42CC0", + "canonical_name_set_sha256": "F8CFCE9626450B46CB9C094625F7B6AB5B99B830954CC2B962EF744B1E5380E8", "entries": [ { "name": "BackdoorAdjustmentEvidence", "qualified_name": "RiskLabAI.causal_factor_analysis.BackdoorAdjustmentEvidence", - "kind": "record_type", + "kind": "named_tuple", "signature": "(treatment: ForwardRef('str'), outcome: ForwardRef('str'), adjustment_set: ForwardRef('NodeTuple'), all_observed: ForwardRef('bool'), endpoints_in_set: ForwardRef('NodeTuple'), descendants_in_set: ForwardRef('NodeTuple'), open_backdoor_paths: ForwardRef('tuple[PathEvidence, ...]'), admissible: ForwardRef('bool'))", "implementation_module": "RiskLabAI.causal_factor_analysis.graph_identification", "record_fields": [ @@ -3985,14 +4279,22 @@ { "name": "BacktestStage", "qualified_name": "RiskLabAI.causal_factor_analysis.BacktestStage", - "kind": "class", + "kind": "dataclass", "signature": "(method_labels: 'tuple[str, ...]', trial_family_id: 'str', declared_trial_ids: 'tuple[str, ...]', validation: 'Optional[ValidationEvidence]' = None, monte_carlo_dgp: 'Optional[str]' = None) -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "method_labels", + "trial_family_id", + "declared_trial_ids", + "validation", + "monte_carlo_dgp" + ], + "frozen": true }, { "name": "CausalDAG", "qualified_name": "RiskLabAI.causal_factor_analysis.CausalDAG", - "kind": "record_type", + "kind": "named_tuple", "signature": "(nodes, directed_edges, observed_nodes)", "implementation_module": "RiskLabAI.causal_factor_analysis.graph_identification", "record_fields": [ @@ -4004,56 +4306,146 @@ { "name": "CausalAdjustmentSetStage", "qualified_name": "RiskLabAI.causal_factor_analysis.CausalAdjustmentSetStage", - "kind": "class", + "kind": "dataclass", "signature": "(treatment: 'str', outcome: 'str', method_label: 'str', identified: 'bool', admissible_adjustment_sets: 'tuple[tuple[str, ...], ...]', selected_adjustment_set: 'tuple[str, ...]', confounders: 'tuple[str, ...]', descendants: 'tuple[str, ...]', mediators: 'tuple[str, ...]', colliders: 'tuple[str, ...]', instruments: 'tuple[str, ...]', control_justifications: 'tuple[tuple[str, str], ...]', open_backdoor_paths: 'tuple[str, ...]', frontdoor_criteria_satisfied: 'Optional[bool]' = None, instrument_relevance_satisfied: 'Optional[bool]' = None, instrument_exclusion_satisfied: 'Optional[bool]' = None, instrument_exogeneity_satisfied: 'Optional[bool]' = None) -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "treatment", + "outcome", + "method_label", + "identified", + "admissible_adjustment_sets", + "selected_adjustment_set", + "confounders", + "descendants", + "mediators", + "colliders", + "instruments", + "control_justifications", + "open_backdoor_paths", + "frontdoor_criteria_satisfied", + "instrument_relevance_satisfied", + "instrument_exclusion_satisfied", + "instrument_exogeneity_satisfied" + ], + "frozen": true }, { "name": "ColliderCoefficients", "qualified_name": "RiskLabAI.causal_factor_analysis.ColliderCoefficients", - "kind": "class", + "kind": "dataclass", "signature": "(beta_hat: 'float', theta_hat: 'float') -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.factor_mirage" + "implementation_module": "RiskLabAI.causal_factor_analysis.factor_mirage", + "record_fields": [ + "beta_hat", + "theta_hat" + ], + "frozen": true }, { "name": "ColliderDiagnostics", "qualified_name": "RiskLabAI.causal_factor_analysis.ColliderDiagnostics", - "kind": "class", + "kind": "dataclass", "signature": "(residual_variance: 'float', outcome_variance: 'float', correct_r_squared: 'float', overcontrolled_r_squared: 'float', correct_adjusted_r_squared: 'float', overcontrolled_adjusted_r_squared: 'float', correct_beta_variance: 'float', overcontrolled_beta_variance: 'float', collider_coefficient_variance: 'float', correct_beta_t_statistic: 'float', overcontrolled_beta_t_statistic: 'float', collider_t_statistic: 'float', adjusted_r_squared_prefers_overcontrolled: 'bool', absolute_beta_t_prefers_overcontrolled: 'bool') -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.factor_mirage" + "implementation_module": "RiskLabAI.causal_factor_analysis.factor_mirage", + "record_fields": [ + "residual_variance", + "outcome_variance", + "correct_r_squared", + "overcontrolled_r_squared", + "correct_adjusted_r_squared", + "overcontrolled_adjusted_r_squared", + "correct_beta_variance", + "overcontrolled_beta_variance", + "collider_coefficient_variance", + "correct_beta_t_statistic", + "overcontrolled_beta_t_statistic", + "collider_t_statistic", + "adjusted_r_squared_prefers_overcontrolled", + "absolute_beta_t_prefers_overcontrolled" + ], + "frozen": true }, { "name": "CausalDiscoveryStage", "qualified_name": "RiskLabAI.causal_factor_analysis.CausalDiscoveryStage", - "kind": "class", + "kind": "dataclass", "signature": "(method_labels: 'tuple[str, ...]', graph_id: 'str', graph_nodes: 'tuple[str, ...]', directed_edges: 'tuple[tuple[str, str], ...]', graph_kind: 'str', ambiguous_edges: 'tuple[tuple[str, str], ...]', assumptions: 'tuple[str, ...]') -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "method_labels", + "graph_id", + "graph_nodes", + "directed_edges", + "graph_kind", + "ambiguous_edges", + "assumptions" + ], + "frozen": true }, { "name": "CausalExplanatoryAndPredictivePowerStage", "qualified_name": "RiskLabAI.causal_factor_analysis.CausalExplanatoryAndPredictivePowerStage", - "kind": "class", + "kind": "dataclass", "signature": "(task_types: 'tuple[str, ...]', estimator_label: 'str', explanatory_metric_labels: 'tuple[str, ...]', predictive_metric_labels: 'tuple[str, ...]', explanatory_evidence_id: 'Optional[str]', predictive_evidence_id: 'Optional[str]', naive_benchmark_id: 'str', validation: 'ValidationEvidence', multiclass_encoding: 'Optional[str]' = None, averaging_method: 'Optional[str]' = None) -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "task_types", + "estimator_label", + "explanatory_metric_labels", + "predictive_metric_labels", + "explanatory_evidence_id", + "predictive_evidence_id", + "naive_benchmark_id", + "validation", + "multiclass_encoding", + "averaging_method" + ], + "frozen": true }, { "name": "CausalFactorProtocolReport", "qualified_name": "RiskLabAI.causal_factor_analysis.CausalFactorProtocolReport", - "kind": "class", + "kind": "dataclass", "signature": "(trial_family_id: 'str', declared_trial_ids: 'tuple[str, ...]', variable_selection: 'VariableSelectionStage', causal_discovery: 'CausalDiscoveryStage', causal_adjustment_set: 'CausalAdjustmentSetStage', causal_explanatory_and_predictive_power: 'CausalExplanatoryAndPredictivePowerStage', causal_portfolio_construction: 'CausalPortfolioConstructionStage', backtest: 'BacktestStage', multiple_testing_adjustments: 'MultipleTestingAdjustmentsStage') -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "trial_family_id", + "declared_trial_ids", + "variable_selection", + "causal_discovery", + "causal_adjustment_set", + "causal_explanatory_and_predictive_power", + "causal_portfolio_construction", + "backtest", + "multiple_testing_adjustments", + "source_oracle", + "source_locator" + ], + "frozen": true }, { "name": "CausalPortfolioConstructionStage", "qualified_name": "RiskLabAI.causal_factor_analysis.CausalPortfolioConstructionStage", - "kind": "class", + "kind": "dataclass", "signature": "(method_labels: 'tuple[str, ...]', causal_exposures: 'tuple[str, ...]', controlled_unintended_exposures: 'tuple[str, ...]', cost_model_id: 'str', constraint_set_id: 'str', economic_rationale: 'str', fragility_scenarios: 'tuple[str, ...]', transfer_coefficient: 'Real') -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "method_labels", + "causal_exposures", + "controlled_unintended_exposures", + "cost_model_id", + "constraint_set_id", + "economic_rationale", + "fragility_scenarios", + "transfer_coefficient" + ], + "frozen": true }, { "name": "DSeparationEvidence", "qualified_name": "RiskLabAI.causal_factor_analysis.DSeparationEvidence", - "kind": "record_type", + "kind": "named_tuple", "signature": "(left: ForwardRef('NodeTuple'), right: ForwardRef('NodeTuple'), conditioned: ForwardRef('NodeTuple'), separated: ForwardRef('bool'), paths: ForwardRef('tuple[PathEvidence, ...]'))", "implementation_module": "RiskLabAI.causal_factor_analysis.graph_identification", "record_fields": [ @@ -4067,7 +4459,7 @@ { "name": "DifferenceInDifferencesEstimate", "qualified_name": "RiskLabAI.causal_factor_analysis.DifferenceInDifferencesEstimate", - "kind": "record_type", + "kind": "named_tuple", "signature": "(treated_change: ForwardRef('float'), control_change: ForwardRef('float'), estimate: ForwardRef('float'))", "implementation_module": "RiskLabAI.causal_factor_analysis.treatment_effects", "record_fields": [ @@ -4079,21 +4471,38 @@ { "name": "EventHorizon", "qualified_name": "RiskLabAI.causal_factor_analysis.EventHorizon", - "kind": "class", + "kind": "dataclass", "signature": "(observation_id: 'str', start: 'Real', end: 'Real') -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "observation_id", + "start", + "end" + ], + "frozen": true }, { "name": "FoldEvidence", "qualified_name": "RiskLabAI.causal_factor_analysis.FoldEvidence", - "kind": "class", + "kind": "dataclass", "signature": "(fold_id: 'str', path_id: 'str', train_ids: 'tuple[str, ...]', test_ids: 'tuple[str, ...]', fit_ids: 'tuple[str, ...]', prediction_ids: 'tuple[str, ...]', refit_id: 'str', refit_components: 'tuple[str, ...]') -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "fold_id", + "path_id", + "train_ids", + "test_ids", + "fit_ids", + "prediction_ids", + "refit_id", + "refit_components" + ], + "frozen": true }, { "name": "FrontdoorAdjustmentEvidence", "qualified_name": "RiskLabAI.causal_factor_analysis.FrontdoorAdjustmentEvidence", - "kind": "record_type", + "kind": "named_tuple", "signature": "(treatment: ForwardRef('str'), outcome: ForwardRef('str'), mediator_set: ForwardRef('NodeTuple'), all_observed: ForwardRef('bool'), endpoints_in_set: ForwardRef('NodeTuple'), directed_path_exists: ForwardRef('bool'), unintercepted_directed_paths: ForwardRef('tuple[PathEvidence, ...]'), open_treatment_mediator_backdoor_paths: ForwardRef('tuple[PathEvidence, ...]'), open_mediator_outcome_backdoor_paths_given_treatment: ForwardRef('tuple[PathEvidence, ...]'), graphically_admissible: ForwardRef('bool'), positivity_required: ForwardRef('bool'))", "implementation_module": "RiskLabAI.causal_factor_analysis.graph_identification", "record_fields": [ @@ -4113,7 +4522,7 @@ { "name": "InstrumentEvidence", "qualified_name": "RiskLabAI.causal_factor_analysis.InstrumentEvidence", - "kind": "record_type", + "kind": "named_tuple", "signature": "(instrument: ForwardRef('str'), treatment: ForwardRef('str'), outcome: ForwardRef('str'), conditioned: ForwardRef('NodeTuple'), all_observed: ForwardRef('bool'), conditioning_disjoint: ForwardRef('bool'), conditioning_unaffected_by_treatment: ForwardRef('bool'), pearl_relevance: ForwardRef('bool'), pearl_exclusion_exogeneity: ForwardRef('bool'), pearl_graphical: ForwardRef('bool'), cfi_direct_relevance: ForwardRef('bool'), cfi_full_mediation: ForwardRef('bool'), cfi_exogeneity: ForwardRef('bool'), cfi_simple: ForwardRef('bool'))", "implementation_module": "RiskLabAI.causal_factor_analysis.graph_identification", "record_fields": [ @@ -4136,14 +4545,32 @@ { "name": "MultipleTestingAdjustmentsStage", "qualified_name": "RiskLabAI.causal_factor_analysis.MultipleTestingAdjustmentsStage", - "kind": "class", + "kind": "dataclass", "signature": "(method_labels: 'tuple[str, ...]', trial_family_id: 'str', declared_trial_ids: 'tuple[str, ...]', family_partitions: 'tuple[tuple[str, tuple[str, ...]], ...]', alpha: 'Real', backtests_independent: 'bool', p_value_estimator: 'Optional[str]' = None, time_dependence_model: 'Optional[str]' = None, p_value_inputs_assume_independence: 'Optional[bool]' = None, sharpe_variance: 'Optional[Real]' = None, effective_trials: 'Optional[Real]' = None, sample_length: 'Optional[Integral]' = None, skewness: 'Optional[Real]' = None, kurtosis: 'Optional[Real]' = None, selection_bias_evidence_id: 'Optional[str]' = None) -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "method_labels", + "trial_family_id", + "declared_trial_ids", + "family_partitions", + "alpha", + "backtests_independent", + "p_value_estimator", + "time_dependence_model", + "p_value_inputs_assume_independence", + "sharpe_variance", + "effective_trials", + "sample_length", + "skewness", + "kurtosis", + "selection_bias_evidence_id" + ], + "frozen": true }, { "name": "NodeRoleEvidence", "qualified_name": "RiskLabAI.causal_factor_analysis.NodeRoleEvidence", - "kind": "record_type", + "kind": "named_tuple", "signature": "(node: ForwardRef('str'), treatment: ForwardRef('str'), outcome: ForwardRef('str'), collider_on_paths: ForwardRef('tuple[PathTuple, ...]'), noncollider_on_paths: ForwardRef('tuple[PathTuple, ...]'), mediator_on_directed_paths: ForwardRef('tuple[PathTuple, ...]'), backdoor_noncollider_on_paths: ForwardRef('tuple[PathTuple, ...]'), common_cause_paths: ForwardRef('tuple[PathTuple, ...]'), descendant_of_treatment: ForwardRef('bool'), ancestor_of_outcome: ForwardRef('bool'))", "implementation_module": "RiskLabAI.causal_factor_analysis.graph_identification", "record_fields": [ @@ -4162,7 +4589,7 @@ { "name": "PathEvidence", "qualified_name": "RiskLabAI.causal_factor_analysis.PathEvidence", - "kind": "record_type", + "kind": "named_tuple", "signature": "(nodes: ForwardRef('PathTuple'), colliders: ForwardRef('NodeTuple'), noncolliders: ForwardRef('NodeTuple'), conditioned_noncolliders: ForwardRef('NodeTuple'), activated_colliders: ForwardRef('NodeTuple'), blocking_colliders: ForwardRef('NodeTuple'), is_open: ForwardRef('bool'), is_directed_from_left: ForwardRef('bool'), is_backdoor_from_left: ForwardRef('bool'))", "implementation_module": "RiskLabAI.causal_factor_analysis.graph_identification", "record_fields": [ @@ -4180,7 +4607,7 @@ { "name": "PopulationRegressionDiagnostics", "qualified_name": "RiskLabAI.causal_factor_analysis.PopulationRegressionDiagnostics", - "kind": "record_type", + "kind": "named_tuple", "signature": "(coefficient_names: ForwardRef('Tuple[str, ...]'), coefficients: ForwardRef('Tuple[float, ...]'), r_squared: ForwardRef('float'))", "implementation_module": "RiskLabAI.causal_factor_analysis.specification_diagnostics", "record_fields": [ @@ -4192,7 +4619,7 @@ { "name": "RegressionDiagnostics", "qualified_name": "RiskLabAI.causal_factor_analysis.RegressionDiagnostics", - "kind": "record_type", + "kind": "named_tuple", "signature": "(coefficient_names: ForwardRef('Tuple[str, ...]'), coefficients: ForwardRef('Tuple[float, ...]'), standard_errors: ForwardRef('Tuple[float, ...]'), t_statistics: ForwardRef('Tuple[float, ...]'), r_squared: ForwardRef('float'), adjusted_r_squared: ForwardRef('float'), residual_variance: ForwardRef('float'), n_observations: ForwardRef('int'))", "implementation_module": "RiskLabAI.causal_factor_analysis.specification_diagnostics", "record_fields": [ @@ -4209,7 +4636,7 @@ { "name": "SpecificationExperimentResult", "qualified_name": "RiskLabAI.causal_factor_analysis.SpecificationExperimentResult", - "kind": "record_type", + "kind": "named_tuple", "signature": "(structure: ForwardRef('str'), conditioning_variable_role: ForwardRef('str'), unconditioned: ForwardRef('RegressionDiagnostics'), conditioned: ForwardRef('RegressionDiagnostics'))", "implementation_module": "RiskLabAI.causal_factor_analysis.specification_diagnostics", "record_fields": [ @@ -4222,7 +4649,7 @@ { "name": "SpecificationPopulationResult", "qualified_name": "RiskLabAI.causal_factor_analysis.SpecificationPopulationResult", - "kind": "record_type", + "kind": "named_tuple", "signature": "(structure: ForwardRef('str'), conditioning_variable_role: ForwardRef('str'), unconditioned: ForwardRef('PopulationRegressionDiagnostics'), conditioned: ForwardRef('PopulationRegressionDiagnostics'))", "implementation_module": "RiskLabAI.causal_factor_analysis.specification_diagnostics", "record_fields": [ @@ -4235,14 +4662,19 @@ { "name": "StrategyPerformance", "qualified_name": "RiskLabAI.causal_factor_analysis.StrategyPerformance", - "kind": "class", + "kind": "dataclass", "signature": "(correct: 'float', misspecified: 'float') -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.factor_mirage" + "implementation_module": "RiskLabAI.causal_factor_analysis.factor_mirage", + "record_fields": [ + "correct", + "misspecified" + ], + "frozen": true }, { "name": "TreatmentEffectDecomposition", "qualified_name": "RiskLabAI.causal_factor_analysis.TreatmentEffectDecomposition", - "kind": "record_type", + "kind": "named_tuple", "signature": "(observed_difference: ForwardRef('float'), average_treatment_effect_on_treated: ForwardRef('float'), sample_selection_bias: ForwardRef('float'))", "implementation_module": "RiskLabAI.causal_factor_analysis.treatment_effects", "record_fields": [ @@ -4254,16 +4686,32 @@ { "name": "ValidationEvidence", "qualified_name": "RiskLabAI.causal_factor_analysis.ValidationEvidence", - "kind": "class", + "kind": "dataclass", "signature": "(method_labels: 'tuple[str, ...]', event_horizons: 'tuple[EventHorizon, ...]', folds: 'tuple[FoldEvidence, ...]', embargo: 'Real' = 0.0) -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "method_labels", + "event_horizons", + "folds", + "embargo" + ], + "frozen": true }, { "name": "VariableSelectionStage", "qualified_name": "RiskLabAI.causal_factor_analysis.VariableSelectionStage", - "kind": "class", + "kind": "dataclass", "signature": "(purpose: 'str', selected_variables: 'tuple[str, ...]', method_labels: 'tuple[str, ...]', overlapping_returns: 'bool', strong_time_dependence: 'bool', validation: 'Optional[ValidationEvidence]') -> None", - "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + "implementation_module": "RiskLabAI.causal_factor_analysis.protocol", + "record_fields": [ + "purpose", + "selected_variables", + "method_labels", + "overlapping_returns", + "strong_time_dependence", + "validation" + ], + "frozen": true }, { "name": "average_treatment_effect", @@ -4474,6 +4922,326 @@ "kind": "function", "signature": "(report: 'CausalFactorProtocolReport') -> 'CausalFactorProtocolReport'", "implementation_module": "RiskLabAI.causal_factor_analysis.protocol" + }, + { + "name": "AllocationMisspecificationDiagnostics", + "qualified_name": "RiskLabAI.causal_factor_analysis.AllocationMisspecificationDiagnostics", + "kind": "dataclass", + "signature": "(reference_weights: 'NDArray[np.float64]', misspecified_weights: 'NDArray[np.float64]', reference_target_exposures: 'NDArray[np.float64]', reference_achieved_exposures: 'NDArray[np.float64]', misspecified_reference_exposures: 'NDArray[np.float64]', misspecified_model_target_exposures: 'NDArray[np.float64]', misspecified_model_achieved_exposures: 'NDArray[np.float64]', reference_exposure_error: 'NDArray[np.float64]', weight_sign_reversals: 'NDArray[np.bool_]', reference_variance: 'float', misspecified_variance: 'float') -> None", + "implementation_module": "RiskLabAI.causal_factor_analysis.allocation_diagnostics", + "record_fields": [ + "reference_weights", + "misspecified_weights", + "reference_target_exposures", + "reference_achieved_exposures", + "misspecified_reference_exposures", + "misspecified_model_target_exposures", + "misspecified_model_achieved_exposures", + "reference_exposure_error", + "weight_sign_reversals", + "reference_variance", + "misspecified_variance" + ], + "frozen": true + }, + { + "name": "FDRComparisonEvidence", + "qualified_name": "RiskLabAI.causal_factor_analysis.FDRComparisonEvidence", + "kind": "dataclass", + "signature": "(n_trials: 'int', single_trial_false_discovery_rate: 'float', family_level_false_discovery_rate: 'float', log_observed_type_i_inflation: 'float', log_required_type_i_inflation: 'float', equation_13_log_gap: 'float', identification_condition_holds: 'bool', single_trial_upper_bounds_family: 'bool') -> None", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr", + "record_fields": [ + "n_trials", + "single_trial_false_discovery_rate", + "family_level_false_discovery_rate", + "log_observed_type_i_inflation", + "log_required_type_i_inflation", + "equation_13_log_gap", + "identification_condition_holds", + "single_trial_upper_bounds_family" + ], + "frozen": true + }, + { + "name": "FDRNonIdentificationWitness", + "qualified_name": "RiskLabAI.causal_factor_analysis.FDRNonIdentificationWitness", + "kind": "dataclass", + "signature": "(n_trials: 'int', target_false_discovery_rate: 'float', trial_null_probability: 'float', observable_cdf_at_threshold: 'float', latent_trial_cdf_at_threshold: 'float', family_null_probability: 'float', family_type_i_error: 'float', family_type_ii_error: 'float', family_false_discovery_rate: 'float', reconstructed_observable_cdf_at_threshold: 'float') -> None", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr", + "record_fields": [ + "n_trials", + "target_false_discovery_rate", + "trial_null_probability", + "observable_cdf_at_threshold", + "latent_trial_cdf_at_threshold", + "family_null_probability", + "family_type_i_error", + "family_type_ii_error", + "family_false_discovery_rate", + "reconstructed_observable_cdf_at_threshold" + ], + "frozen": true + }, + { + "name": "FactorControlRoles", + "qualified_name": "RiskLabAI.causal_factor_analysis.FactorControlRoles", + "kind": "dataclass", + "signature": "(target_factor: 'str', ancestor_factors: 'tuple[str, ...]', descendant_factors: 'tuple[str, ...]', other_factors: 'tuple[str, ...]', unobserved_ancestors: 'tuple[str, ...]', unobserved_descendants: 'tuple[str, ...]') -> None", + "implementation_module": "RiskLabAI.causal_factor_analysis.graph_roles", + "record_fields": [ + "target_factor", + "ancestor_factors", + "descendant_factors", + "other_factors", + "unobserved_ancestors", + "unobserved_descendants" + ], + "frozen": true + }, + { + "name": "GaussianSearchAdjustedFDR", + "qualified_name": "RiskLabAI.causal_factor_analysis.GaussianSearchAdjustedFDR", + "kind": "dataclass", + "signature": "(n_trials: 'int', thresholds: 'NDArray[np.float64]', trial_type_i_errors: 'NDArray[np.float64]', trial_type_ii_errors: 'NDArray[np.float64]', family_type_i_errors: 'NDArray[np.float64]', family_type_ii_errors: 'NDArray[np.float64]', mean_family_type_i_error: 'float', mean_family_type_ii_error: 'float', family_null_probability: 'float', false_discovery_rate: 'float') -> None", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr", + "record_fields": [ + "n_trials", + "thresholds", + "trial_type_i_errors", + "trial_type_ii_errors", + "family_type_i_errors", + "family_type_ii_errors", + "mean_family_type_i_error", + "mean_family_type_ii_error", + "family_null_probability", + "false_discovery_rate" + ], + "frozen": true + }, + { + "name": "GaussianTrialMixture", + "qualified_name": "RiskLabAI.causal_factor_analysis.GaussianTrialMixture", + "kind": "dataclass", + "signature": "(trial_null_probability: 'float', null_standard_deviation: 'float', alternative_mean: 'float', alternative_standard_deviation: 'float') -> None", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr", + "record_fields": [ + "trial_null_probability", + "null_standard_deviation", + "alternative_mean", + "alternative_standard_deviation" + ], + "frozen": true + }, + { + "name": "MaxSelectionFamilyErrors", + "qualified_name": "RiskLabAI.causal_factor_analysis.MaxSelectionFamilyErrors", + "kind": "dataclass", + "signature": "(n_trials: 'int', trial_null_probability: 'float', trial_type_i_error: 'float', trial_type_ii_error: 'float', family_null_probability: 'float', family_type_i_error: 'float', family_type_ii_error: 'float') -> None", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr", + "record_fields": [ + "n_trials", + "trial_null_probability", + "trial_type_i_error", + "trial_type_ii_error", + "family_null_probability", + "family_type_i_error", + "family_type_ii_error" + ], + "frozen": true + }, + { + "name": "SelectionLevelProbabilityEvidence", + "qualified_name": "RiskLabAI.causal_factor_analysis.SelectionLevelProbabilityEvidence", + "kind": "dataclass", + "signature": "(joint_null_and_selection_probability: 'float', selection_probability: 'float', conditional_null_probability: 'float', quadrature_absolute_error: 'float') -> None", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr", + "record_fields": [ + "joint_null_and_selection_probability", + "selection_probability", + "conditional_null_probability", + "quadrature_absolute_error" + ], + "frozen": true + }, + { + "name": "StructuralCausalModelResult", + "qualified_name": "RiskLabAI.causal_factor_analysis.StructuralCausalModelResult", + "kind": "dataclass", + "signature": "(node_order: 'tuple[str, ...]', values: 'NDArray[np.float64]') -> None", + "implementation_module": "RiskLabAI.causal_factor_analysis.structural_models", + "record_fields": [ + "node_order", + "values" + ], + "frozen": true + }, + { + "name": "TreatmentOutcomeRole", + "qualified_name": "RiskLabAI.causal_factor_analysis.TreatmentOutcomeRole", + "kind": "enum", + "signature": "(*values)", + "implementation_module": "RiskLabAI.causal_factor_analysis.graph_roles", + "enum_values": { + "CAUSE_OF_TREATMENT": "cause_of_treatment", + "CONSEQUENCE_OF_TREATMENT": "consequence_of_treatment", + "CAUSE_OF_OUTCOME": "cause_of_outcome", + "CONSEQUENCE_OF_OUTCOME": "consequence_of_outcome", + "CONFOUNDER": "confounder", + "COLLIDER": "collider", + "MEDIATOR": "mediator", + "INDEPENDENT": "independent" + } + }, + { + "name": "TreatmentOutcomeRoleEvidence", + "qualified_name": "RiskLabAI.causal_factor_analysis.TreatmentOutcomeRoleEvidence", + "kind": "dataclass", + "signature": "(node: 'str', treatment: 'str', outcome: 'str', role: 'TreatmentOutcomeRole', node_to_treatment: 'bool', treatment_to_node: 'bool', node_to_outcome: 'bool', outcome_to_node: 'bool') -> None", + "implementation_module": "RiskLabAI.causal_factor_analysis.graph_roles", + "record_fields": [ + "node", + "treatment", + "outcome", + "role", + "node_to_treatment", + "treatment_to_node", + "node_to_outcome", + "outcome_to_node" + ], + "frozen": true + }, + { + "name": "allocation_misspecification_diagnostics", + "qualified_name": "RiskLabAI.causal_factor_analysis.allocation_misspecification_diagnostics", + "kind": "function", + "signature": "(covariance: 'ArrayLike', reference_factor_exposures: 'ArrayLike', reference_target_exposures: 'ArrayLike', misspecified_factor_exposures: 'ArrayLike', misspecified_target_exposures: 'ArrayLike') -> 'AllocationMisspecificationDiagnostics'", + "implementation_module": "RiskLabAI.causal_factor_analysis.allocation_diagnostics" + }, + { + "name": "classify_treatment_outcome_role", + "qualified_name": "RiskLabAI.causal_factor_analysis.classify_treatment_outcome_role", + "kind": "function", + "signature": "(dag: 'CausalDAG', treatment: 'str', outcome: 'str', node: 'str') -> 'TreatmentOutcomeRoleEvidence'", + "implementation_module": "RiskLabAI.causal_factor_analysis.graph_roles" + }, + { + "name": "compare_single_and_family_fdr", + "qualified_name": "RiskLabAI.causal_factor_analysis.compare_single_and_family_fdr", + "kind": "function", + "signature": "(trial_type_i_error: 'Real', trial_type_ii_error: 'Real', trial_null_probability: 'Real', n_trials: 'Integral', *, family_type_i_error: 'Real', family_type_ii_error: 'Real', relative_tolerance: 'Real' = 1e-12, absolute_tolerance: 'Real' = 1e-15) -> 'FDRComparisonEvidence'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "conditional_upper_tail_probability", + "qualified_name": "RiskLabAI.causal_factor_analysis.conditional_upper_tail_probability", + "kind": "function", + "signature": "(cdf_at_value: 'Real', cdf_at_threshold: 'Real') -> 'float'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "evaluate_structural_causal_model", + "qualified_name": "RiskLabAI.causal_factor_analysis.evaluate_structural_causal_model", + "kind": "function", + "signature": "(dag: 'CausalDAG', mechanisms: 'Mapping[str, Callable[[NDArray[np.float64], NDArray[np.float64]], ArrayLike]]', exogenous_inputs: 'Mapping[str, ArrayLike]') -> 'StructuralCausalModelResult'", + "implementation_module": "RiskLabAI.causal_factor_analysis.structural_models" + }, + { + "name": "factor_control_roles", + "qualified_name": "RiskLabAI.causal_factor_analysis.factor_control_roles", + "kind": "function", + "signature": "(dag: 'CausalDAG', target_factor: 'str') -> 'FactorControlRoles'", + "implementation_module": "RiskLabAI.causal_factor_analysis.graph_roles" + }, + { + "name": "family_level_false_discovery_rate", + "qualified_name": "RiskLabAI.causal_factor_analysis.family_level_false_discovery_rate", + "kind": "function", + "signature": "(family_type_i_error: 'Real', family_type_ii_error: 'Real', trial_null_probability: 'Real', n_trials: 'Integral') -> 'float'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "fdr_nonidentification_witness", + "qualified_name": "RiskLabAI.causal_factor_analysis.fdr_nonidentification_witness", + "kind": "function", + "signature": "(observable_cdf_at_threshold: 'Real', target_false_discovery_rate: 'Real') -> 'FDRNonIdentificationWitness'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "gaussian_max_selection_cdf", + "qualified_name": "RiskLabAI.causal_factor_analysis.gaussian_max_selection_cdf", + "kind": "function", + "signature": "(model: 'GaussianTrialMixture', x: 'Real', n_trials: 'Integral') -> 'float'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "gaussian_max_selection_log_density", + "qualified_name": "RiskLabAI.causal_factor_analysis.gaussian_max_selection_log_density", + "kind": "function", + "signature": "(model: 'GaussianTrialMixture', x: 'Real', n_trials: 'Integral') -> 'float'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "gaussian_max_selection_log_likelihood", + "qualified_name": "RiskLabAI.causal_factor_analysis.gaussian_max_selection_log_likelihood", + "kind": "function", + "signature": "(model: 'GaussianTrialMixture', observations: 'ArrayLike', n_trials: 'Integral') -> 'float'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "gaussian_search_adjusted_false_discovery_rate", + "qualified_name": "RiskLabAI.causal_factor_analysis.gaussian_search_adjusted_false_discovery_rate", + "kind": "function", + "signature": "(model: 'GaussianTrialMixture', thresholds: 'ArrayLike', n_trials: 'Integral') -> 'GaussianSearchAdjustedFDR'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "gaussian_trial_mixture_cdf", + "qualified_name": "RiskLabAI.causal_factor_analysis.gaussian_trial_mixture_cdf", + "kind": "function", + "signature": "(model: 'GaussianTrialMixture', x: 'Real') -> 'float'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "generalized_collider_overcontrolled_coefficients", + "qualified_name": "RiskLabAI.causal_factor_analysis.generalized_collider_overcontrolled_coefficients", + "kind": "function", + "signature": "(beta: 'Real', gamma: 'Real', delta: 'Real', *, outcome_noise_variance: 'Real', collider_noise_variance: 'Real') -> 'ColliderCoefficients'", + "implementation_module": "RiskLabAI.causal_factor_analysis.factor_mirage" + }, + { + "name": "generalized_confounder_undercontrolled_coefficient", + "qualified_name": "RiskLabAI.causal_factor_analysis.generalized_confounder_undercontrolled_coefficient", + "kind": "function", + "signature": "(beta: 'Real', gamma: 'Real', delta: 'Real', *, confounder_variance: 'Real', exposure_noise_variance: 'Real') -> 'float'", + "implementation_module": "RiskLabAI.causal_factor_analysis.factor_mirage" + }, + { + "name": "max_selection_family_errors", + "qualified_name": "RiskLabAI.causal_factor_analysis.max_selection_family_errors", + "kind": "function", + "signature": "(trial_type_i_error: 'Real', trial_type_ii_error: 'Real', trial_null_probability: 'Real', n_trials: 'Integral') -> 'MaxSelectionFamilyErrors'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "max_selection_null_probability", + "qualified_name": "RiskLabAI.causal_factor_analysis.max_selection_null_probability", + "kind": "function", + "signature": "(threshold: 'Real', trial_null_probability: 'Real', n_trials: 'Integral', *, null_density: 'Callable[[float], Real]', mixture_cdf: 'Callable[[float], Real]', absolute_tolerance: 'Real' = 1e-12, relative_tolerance: 'Real' = 1e-10, max_subintervals: 'Integral' = 200) -> 'SelectionLevelProbabilityEvidence'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "maximum_mixture_cdf", + "qualified_name": "RiskLabAI.causal_factor_analysis.maximum_mixture_cdf", + "kind": "function", + "signature": "(null_cdf: 'Real', alternative_cdf: 'Real', trial_null_probability: 'Real', n_trials: 'Integral') -> 'float'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" + }, + { + "name": "single_trial_false_discovery_rate", + "qualified_name": "RiskLabAI.causal_factor_analysis.single_trial_false_discovery_rate", + "kind": "function", + "signature": "(type_i_error: 'Real', type_ii_error: 'Real', trial_null_probability: 'Real') -> 'float'", + "implementation_module": "RiskLabAI.causal_factor_analysis.search_adjusted_fdr" } ] } diff --git a/README.md b/README.md index 575f476..44b2634 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,20 @@ implementations of methods associated with Marcos López de Prado's *Advances in Financial Machine Learning*, *Machine Learning for Asset Managers*, and *Causal Factor Investing*. -RiskLabAI 3.0.0 preserves the previously published Python library and adds a -clean causal-factor-analysis namespace. The companion +RiskLabAI 3.1.0 preserves the clean 57-name causal-factor-analysis namespace +released in 3.0.0 and adds 30 paper-derived names, producing an 87-name causal +API. The companion [RiskLabAI.jl](https://github.com/RiskLabAI/RiskLabAI.jl) package independently -implements the same 57 causal concepts. This parity statement applies to the -causal API, not to every Python module. +implements the same concepts. This parity statement applies to the causal API, +not to every Python module. ## What is included - **Causal factor analysis** - constrained minimum-variance allocation, - factor-mirage diagnostics, graphical identification, treatment-effect - formulas, specification experiments, and evidence records for the - seven-stage causal-factor protocol + factor-mirage and allocation-misspecification diagnostics, graphical + identification and factor roles, deterministic structural-model evaluation, + treatment-effect formulas, search-adjusted false discovery, specification + experiments, and evidence records for the seven-stage causal-factor protocol - **Financial data structures** - tick, volume, dollar, imbalance, run, and time bars - **Labeling and sample weights** - triple-barrier, meta-labeling, @@ -40,8 +42,9 @@ causal API, not to every Python module. ## Compatibility -RiskLabAI 3.0.0 supports CPython 3.12, 3.13, and 3.14 with NumPy -`>=2.2,<3`. The complete tested policy and feature-specific limitations are in +RiskLabAI 3.1.0 supports CPython 3.12, +3.13, and 3.14 with NumPy `>=2.2,<3`. The complete tested policy and +feature-specific limitations are in [`docs/compatibility.md`](https://github.com/RiskLabAI/RiskLabAI.py/blob/main/docs/compatibility.md). ## Installation diff --git a/RELEASE_NOTES_NEXT_MAJOR.md b/RELEASE_NOTES_NEXT_MAJOR.md index cfcdc1d..c83a61e 100644 --- a/RELEASE_NOTES_NEXT_MAJOR.md +++ b/RELEASE_NOTES_NEXT_MAJOR.md @@ -1,44 +1,43 @@ -# RiskLabAI Python next-major release notes — blocked draft - -Target version: **3.0.0**. The owner approved this exact version, but assigning -it does not authorize a build, artifact, publication, upload, or release. - -The next major Python release preserves the prior public repository source -modules and adds the verified 57-concept `RiskLabAI.causal_factor_analysis` -namespace. It intentionally drops support for Python 3.9-3.11 and NumPy 1.x; -the supported policy is CPython 3.12-3.14 with NumPy `>=2.2,<3`. - -Compatibility repairs include NumPy 2 trapezoidal integration, public -scikit-learn bootstrap logic, optional Numba acceleration, lazy optional -dependencies, a Python-3.14-specific Joblib floor, and self-contained standard -technical indicators. Python 3.14 remains supported when the optional +# RiskLabAI Python 3.1.0 additive causal release notes + +Version: **3.1.0**. The version is frozen for local integration and artifact +inspection. Version-control, publication, upload, and release actions remain +human-controlled and are not authorized by this document. + +The candidate starts from RiskLabAI 3.0.0 and preserves its complete public +library and released 57-name `RiskLabAI.causal_factor_analysis` contract. It +adds 30 paper-derived names, producing an 87-name causal namespace with a +matching Julia implementation. No released causal name, signature, default, +or behavior is removed or silently changed. + +The support policy remains CPython 3.12-3.14 with NumPy `>=2.2,<3`. The +additions require no new Python dependency and retain all required and +feature-specific optional dependency boundaries established in 3.0.0. + +The additive analytical families cover general-variance factor-mirage +coefficients, allocation-misspecification evidence, accepted-DAG factor roles, +deterministic structural-model evaluation, and family- and selection-level +false-discovery calculations for searched trials. The two false-discovery +estimands remain explicitly separate. + +Every implemented method is linked to an authoritative public source and checked +against direct analytical examples, independent mathematical or graph oracles, +validation boundaries, stability cases, and—in addition—shared numerical +Python-Julia fixtures. Cross-language agreement is not used as the sole +correctness authority. + +Thirteen method units remain source-blocked because the papers conflict or omit +necessary mathematics, data, trained assets, or reproducibility settings. +Those omissions are documented rather than guessed. Excluded legacy causal +implementations are neither modified nor used as correctness authorities. + +The complete preserved library, additive causal tests, formatting and static +checks, six approved base runtime/NumPy endpoints, and three optional-feature +runtime lines are verified. Python 3.14 remains supported when the optional changepoint backend is unavailable. -The preserved targeted-shrinkage API `RiskLabAI.data.denoise.denoised_corr2` -is restored after a refactor left its public export without the corresponding -implementation. Its historical parameter contract and published analytical -formula are covered by exact matrix, boundary, validation, and export-binding -tests. - -Compatibility aliases that remain present in 3.0.0 now consistently warn of -removal in 4.0.0. No alias is silently deleted, and the warning contract is -locked by package-scoped tests. - -The release metadata uses the PEP 639 `BSD-3-Clause` expression and license -file without the superseded license classifier. The governed CI workflow now -installs the distribution, rejects imports shadowed by the preserved repository -root in both parent and child interpreters, tests the six approved base -runtime/NumPy combinations, exercises feature-specific optional lanes, and -isolates the Python 3.14 changepoint limitation. Pinned static checks cover the -clean causal source and tests. - -No source-conflicted causal result is admitted. The metadata contract is -complete; artifacts, publication, upload, and release remain blocked. - -The static distributable metadata, exact 124-module runtime inventory, -78-file package-scoped test inventory, public API inventory, documentation, -examples, dependency groups, governed CI workflow, and 215-file intended -source-distribution list are now frozen. No retained package artifact has been -created for this corrected source state. The remaining gates are future -artifact inspection and separate human authorization for version-control, -publication, and release actions. +Package metadata is frozen at 3.1.0 for the exact additive integration +candidate. The exact inventories, complete compatibility matrix, and temporary +wheel and source-distribution inspection must all pass before any human Git +handoff. Version control, publication, upload, and release remain human-only +and separately authorized. diff --git a/RiskLabAI/backtest/__init__.py b/RiskLabAI/backtest/__init__.py index 1050609..9f75a45 100644 --- a/RiskLabAI/backtest/__init__.py +++ b/RiskLabAI/backtest/__init__.py @@ -117,12 +117,12 @@ # Define what `from RiskLabAI.backtest import *` will import __all__ = [ "validation", - # from advanced_bet_sizing (Appraisal 18 admits) + # from advanced_bet_sizing (independent validation includes) "PlattCalibrator", "distributionally_robust_kelly_fraction", "expected_calibration_error", "kelly_bet_fraction", - # from robust_statistics + ou_trading_rules (Appraisal 22, 23 admits) + # from robust_statistics + ou_trading_rules (independent validation, validated additions) "conditional_expected_drawdown", "sharpe_difference_test", "optimal_ou_trading_rule", diff --git a/RiskLabAI/backtest/advanced_bet_sizing.py b/RiskLabAI/backtest/advanced_bet_sizing.py index 5172948..1b8719d 100644 --- a/RiskLabAI/backtest/advanced_bet_sizing.py +++ b/RiskLabAI/backtest/advanced_bet_sizing.py @@ -6,14 +6,14 @@ - **Platt calibration before sizing** (Meyer-Barziy-Joubert 2023). A classifier's raw probability is usually miscalibrated, so sizing straight from it over- or under-bets. Fitting a Platt (logistic) - calibrator on a held-out block and sizing from the calibrated probability recovers near-optimal sizing + calibrator on a validation sample block and sizing from the calibrated probability recovers near-optimal sizing when the input is miscalibrated, and is a safe no-op when it is already calibrated. - **Distributionally-robust Kelly** (Sun-Boyd 2018). Point-estimate Kelly is fragile to estimation error. Sizing at the pessimistic edge of a confidence box on the win probability gives near-optimal growth with materially lower drawdown under estimation uncertainty, and converges to full Kelly as the box collapses with abundant data. -Admitted in Appraisal 18 (CONTRIBUTIONS_LEDGER 2026-06-27). Scope tags, verbatim from the verdict: +Included after independent validation (the public method specification). Scope guidance: Platt calibration: calibrate the meta-label probability before sizing when it is miscalibrated (measurable by ECE); use Platt, not isotonic, which overfits well-calibrated inputs; the benefit is @@ -23,12 +23,12 @@ (small samples, noisy edge): near-optimal growth with lower drawdown; converges to Kelly with abundant data. -Held-out confirmed (appraisals/18_results, HELDOUT.md): on the sealed high-miscalibration corner Platt +validation sample confirmed (the documented validation evidence, independent validation): on the sealed high-miscalibration corner Platt closes the growth gap to the Kelly optimum distinguishably more than the de Prado sigmoid (paired 0.0032 [0.0030, 0.0034]); on the sealed tight-drawdown / small-sample corner DR-Kelly has lower from-initial drawdown than full Kelly (0.144 vs 0.409) with near-optimal growth. Real-data net-of-cost -confirmation for both is a tracked obligation (`REAL_DATA_FOLLOWUPS.md`). Isotonic calibration and -risk-constrained Kelly were NOT admitted (shelved). Evidence and caveats: appraisals/18_verdict.md. +confirmation for both is a future validation work (documented follow-up work). Isotonic calibration and +risk-constrained Kelly were NOT admitted (shelved). Evidence and caveats: the documented validation evidence. The DR-Kelly box-ambiguity worst case has a closed form (Kelly at the pessimistic probability, because a long favorable bet's log-growth is monotone in the win probability), so no convex solver is required here; @@ -124,15 +124,15 @@ def expected_calibration_error(probabilities, outcomes, n_bins: int = 10) -> flo class PlattCalibrator: r""" - Platt scaling: a logistic recalibration of predicted probabilities, fit on a held-out block. + Platt scaling: a logistic recalibration of predicted probabilities, fit on a validation sample block. Fits a logistic regression of the binary outcome on ``logit(p_hat)``; ``transform`` maps a raw probability to the calibrated one. Prefer Platt over isotonic calibration: isotonic overfits the calibration map and hurts already-calibrated inputs, whereas Platt's two-parameter logistic form is a safe near-no-op when the input is already calibrated (the characterized failure mode in the - verdict). Calibrate before sizing (feed the calibrated probability into the de Prado sigmoid or a - Kelly sizer) when ECE indicates miscalibration. See the module docstring for the verbatim scope tag - and appraisals/18_verdict.md. + documented guidance). Calibrate before sizing (feed the calibrated probability into the de Prado sigmoid or a + Kelly sizer) when ECE indicates miscalibration. See the module docstring for the scope guidance + and the documented validation evidence. Examples -------- @@ -191,7 +191,7 @@ def distributionally_robust_kelly_fraction( Prefer DR-Kelly over de Prado sigmoid / full Kelly when estimation uncertainty is material (small samples, noisy edge): near-optimal growth with lower drawdown; converges to Kelly with abundant data. - See the module docstring for the verbatim scope tag and appraisals/18_verdict.md. + See the module docstring for the scope guidance and the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/backtest/multiple_testing.py b/RiskLabAI/backtest/multiple_testing.py index 9968f8d..4820e92 100644 --- a/RiskLabAI/backtest/multiple_testing.py +++ b/RiskLabAI/backtest/multiple_testing.py @@ -9,7 +9,7 @@ single best strategy survives its trial count, while these screen a whole family with a stated error-control target. -Admitted in Appraisal 07 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: To judge a family of screened strategies or factors (not just the single best), prefer Holm when you must control the chance of any false positive across the family (FWER), and BHY when you want @@ -18,7 +18,7 @@ count. Bonferroni is dominated by Holm; the t>3 hurdle over-rejects; the double-bootstrap does not control its stated error as implemented and is not admitted. -Evidence and caveats: appraisals/07_verdict.md. +Evidence and caveats: the documented validation evidence. References ---------- @@ -146,8 +146,8 @@ def haircut_sharpe_ratios( Prefer Holm when you must control the chance of any false positive across the family (FWER), and BHY when you want to bound the expected fraction of false discoveries (FDR) and can accept lower power. The Deflated Sharpe remains the tool when the question is whether one specific best strategy - survives its trial count. See the module docstring for the full regime tag and - appraisals/07_verdict.md. + survives its trial count. See the module docstring for the full usage guidance and + the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/backtest/ou_trading_rules.py b/RiskLabAI/backtest/ou_trading_rules.py index ce160f3..9ebd72b 100644 --- a/RiskLabAI/backtest/ou_trading_rules.py +++ b/RiskLabAI/backtest/ou_trading_rules.py @@ -17,18 +17,18 @@ - cost``, and the objective is the expected net return per unit time ``E[gain] / E[tau]`` (the well-posed optimal-trading-rule objective; the raw per-trade Sharpe is degenerate, maximized by profit_take -> 0). -Admitted in Appraisal 23 (CONTRIBUTIONS_LEDGER 2026-06-27). Scope tag, verbatim from the verdict: +Included after independent validation (the public method specification). Scope guidance: prefer the closed-form OU rule over the Monte-Carlo PT/SL grid whenever the OU model is used - it gives the exact optimum with no simulation noise at a fraction of the compute; both are only as good as the OU assumption, so check the OU fit at decision time. -Held-out confirmed (appraisals/23_results, HELDOUT.md): on the sealed OU corner the closed form agrees +validation sample confirmed (the documented validation evidence, independent validation): on the sealed OU corner the closed form agrees with the Monte-Carlo grid optimum within grid resolution (the grid pick's regret on the exact surface stays ~0.0002-0.0008 and tightens as the grid refines) at a compute speed-up that grows with resolution (3.4x -> 16x), and it degrades in step with the grid off-model (no false out-of-model win). The advantage is exactness and speed under the OU model, not robustness to misspecification, so check the OU fit at -decision time. The de Prado Monte-Carlo PT/SL grid is left unchanged. Evidence: appraisals/23_verdict.md. +decision time. The de Prado Monte-Carlo PT/SL grid is left unchanged. Evidence: the documented validation evidence. References ---------- @@ -210,7 +210,7 @@ def optimal_ou_trading_rule( box. Prefer this over the Monte-Carlo PT/SL grid whenever the OU model is used: it gives the exact optimum with no simulation noise at a fraction of the compute. Both are only as good as the OU assumption, so check the OU fit at decision time (see :func:`fit_ornstein_uhlenbeck` and the module - docstring for the verbatim scope tag and appraisals/23_verdict.md). + docstring for the scope guidance and the documented validation evidence). Parameters ---------- diff --git a/RiskLabAI/backtest/robust_statistics.py b/RiskLabAI/backtest/robust_statistics.py index 89c224b..9f56041 100644 --- a/RiskLabAI/backtest/robust_statistics.py +++ b/RiskLabAI/backtest/robust_statistics.py @@ -14,7 +14,7 @@ a studentized circular block bootstrap, which holds nominal size under serial dependence where the naive test inflates. -Admitted in Appraisal 22 (CONTRIBUTIONS_LEDGER 2026-06-27). Scope tags, verbatim from the verdict: +Included after independent validation (the public method specification). Scope guidance: CED: prefer CED over max-drawdown as a drawdown-risk statistic: lower estimator variance and better ranking of true drawdown risk, most of all on short/heavy-tailed tracks; converges to max-DD on @@ -24,11 +24,11 @@ serial dependence / heavy tails (it holds nominal size where the naive test inflates ~3x); converges on i.i.d. returns. -Held-out confirmed (appraisals/22_results, HELDOUT.md): on the sealed short-sample x heavy-tail corner CED +validation sample confirmed (the documented validation evidence, independent validation): on the sealed short-sample x heavy-tail corner CED has lower estimator variance (CV 0.30-0.32 vs max-drawdown 0.40-0.42) and better rank-recovery (Spearman 0.42-0.78 vs 0.31-0.69); the LW test holds nominal size (0.056) under AR(0.3) where the naive test rejects 0.161 (~3x), and converges on i.i.d. returns. CDaR@0.95 was NOT admitted (no variance/rank advantage over -max-drawdown as a descriptive statistic). Evidence and caveats: appraisals/22_verdict.md. +max-drawdown as a descriptive statistic). Evidence and caveats: the documented validation evidence. References ---------- @@ -102,7 +102,7 @@ def conditional_expected_drawdown( factor-attributable. Prefer CED over max-drawdown as a drawdown-risk statistic (see the module docstring for the full - verbatim scope tag and appraisals/22_verdict.md). The de Prado max-drawdown / time-under-water + scope guidance and the documented validation evidence). The de Prado max-drawdown / time-under-water statistics are unchanged. Parameters @@ -209,7 +209,7 @@ def sharpe_difference_test( the naive test inflates (~3x under AR), and converges to the naive test on i.i.d. returns. Prefer the Ledoit-Wolf test when comparing two Sharpes under serial dependence / heavy tails (see the - module docstring for the full verbatim scope tag and appraisals/22_verdict.md). The Sharpe ratio uses + module docstring for the full scope guidance and the documented validation evidence). The Sharpe ratio uses the population standard deviation, matching ``backtest_statistics.sharpe_ratio``. Parameters diff --git a/RiskLabAI/backtest/sharpe_inference.py b/RiskLabAI/backtest/sharpe_inference.py index cdbdbb5..cec3925 100644 --- a/RiskLabAI/backtest/sharpe_inference.py +++ b/RiskLabAI/backtest/sharpe_inference.py @@ -19,7 +19,7 @@ :math:`1 - S\,\widehat{SR} + \tfrac{K-1}{4}\widehat{SR}^2` (exactly the PSR denominator), so the method converges to the PSR when there is no serial correlation. -Admitted in Appraisal 08 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: Prefer the LPLZ (2025) Sharpe inference - a HAC of the Sharpe influence function - when returns show material serial correlation and/or non-normality (estimable from the sample): it restores @@ -29,13 +29,13 @@ (not fully to) nominal at strong autocorrelation, and wider CIs - the honest power cost. Lo (2002) is the autocorrelation-only intermediate, dominated by LPLZ under non-normality. -Evidence and caveats: appraisals/08_verdict.md. +Evidence and caveats: the documented validation evidence. References ---------- Lopez de Prado, M., Lipton, A. and Zoonekynd, V. (2025) Sharpe-ratio inference under non-normal, - serially-correlated returns. (Open code at github.com/zoonek/2025-sharpe-ratio - NOT consulted or - vendored; this is reimplemented clean-room from the influence-function / HAC math, GOVERNANCE 3.) + serially-correlated returns. This implementation follows the cited influence-function and HAC + formulas. Newey, W. K. and West, K. D. (1987) A simple, positive semi-definite, heteroskedasticity and autocorrelation consistent covariance matrix. Econometrica, 55(3), 703-708. Lo, A. W. (2002) The statistics of Sharpe ratios. Financial Analysts Journal, 58(4), 36-52. @@ -135,7 +135,7 @@ def lplz_sharpe_inference( (estimable from the sample): it restores near-nominal CI coverage and test size where the PSR under-covers and over-rejects, and converges to the PSR on near-normal i.i.d. returns with no over-coverage. The honest cost is a wider CI under autocorrelation. See the module docstring for the - full regime tag and appraisals/08_verdict.md. + full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/backtest/validation/__init__.py b/RiskLabAI/backtest/validation/__init__.py index d5480b1..e0b15c4 100644 --- a/RiskLabAI/backtest/validation/__init__.py +++ b/RiskLabAI/backtest/validation/__init__.py @@ -40,13 +40,13 @@ "CombinatorialPurged", # <-- Fix "BaggedCombinatorialPurged", # <-- Fix "AdaptiveCombinatorialPurged", # <-- Fix - # Path-level bagged CPCV PBO (Appraisal 09) + # Path-level bagged CPCV PBO (independent validation) "bagged_probability_of_backtest_overfitting", "moving_block_bootstrap_indices", - # Path-level adaptive (regime-weighted) CPCV PBO (Appraisal 09b) + # Path-level adaptive (regime-weighted) CPCV PBO (independent validation) "adaptive_probability_of_backtest_overfitting", "estimate_volatility_regimes", - # Leakage-aware HPO methodology (Appraisal 20) + # Leakage-aware HPO methodology (independent validation) "leakage_aware_hpo", "deflated_sharpe_gate", # Utilities diff --git a/RiskLabAI/backtest/validation/leakage_aware_hpo.py b/RiskLabAI/backtest/validation/leakage_aware_hpo.py index e299bd6..7ce9f19 100644 --- a/RiskLabAI/backtest/validation/leakage_aware_hpo.py +++ b/RiskLabAI/backtest/validation/leakage_aware_hpo.py @@ -11,8 +11,8 @@ per-trial score is leakage-controlled) and provides a Deflated-Sharpe gate that deflates the selected model's out-of-sample Sharpe by the HPO-inclusive trial count. -Admitted in Appraisal 20 (CONTRIBUTIONS_LEDGER 2026-06-27) as a methodology / infrastructure admit, NOT a -performance claim. Scope tag, verbatim from the verdict: +Included after independent validation (the public method specification) as a methodology / infrastructure admit, NOT a +performance claim. Scope guidance: Optuna (TPE/CMA-ES) wired through PurgedKFold/CPCV with PBO/DSR gating, preferred over grid/random for search efficiency and over naive-CV tuning for leakage-safety, and carrying the honest, characterized @@ -20,11 +20,11 @@ the trial count, never trusted on the in-sample HPO score. **Plainly: this improves search efficiency and leakage-safety, NOT out-of-sample performance. Tuning does -not create edge.** In the appraisal the principled-HPO real-leg gain did not survive the Deflated Sharpe at +not create edge.** during independent validation the principled-HPO real-leg gain did not survive the Deflated Sharpe at the HPO-inclusive trial count (DSR 0.44 / 0.34 < 0.5, PBO 0.67 / 0.69, OOS at the base rate); the value is that the search reaches the optimum in fewer trials and that purged CV removes the leakage a naive k-fold inflates. Always gate the selected model by :func:`deflated_sharpe_gate` at the HPO trial count; never -trust the in-sample HPO score. Evidence and caveats: appraisals/20_verdict.md. +trust the in-sample HPO score. Evidence and caveats: the documented validation evidence. References ---------- @@ -87,7 +87,7 @@ def leakage_aware_hpo( and LEAKAGE-SAFETY (the per-trial score is purged, not a leaky shuffled k-fold). It does NOT improve out-of-sample performance - tuning does not create edge - so the selected model must be gated by :func:`deflated_sharpe_gate` at the returned ``n_trials``; never trust ``best_score`` directly. See the - module docstring for the verbatim scope tag and appraisals/20_verdict.md. + module docstring for the scope guidance and the documented validation evidence. Parameters ---------- @@ -186,9 +186,9 @@ def deflated_sharpe_gate( The benchmark is the expected maximum Sharpe over ``n_trials`` independent trials (with the observed cross-trial Sharpe dispersion ``trial_sharpe_std``); the Deflated Sharpe is the probability that the realized OOS Sharpe exceeds that benchmark. A selection passes only if the Deflated Sharpe exceeds - ``threshold`` (0.5). This is the decisive control: in the appraisal the principled-HPO gain did not + ``threshold`` (0.5). This is the decisive control: during independent validation the principled-HPO gain did not pass it - tuning yielded no out-of-sample edge after deflation. See the module docstring for the - verbatim scope tag and appraisals/20_verdict.md. + scope guidance and the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/backtest/validation/path_adaptive_cpcv.py b/RiskLabAI/backtest/validation/path_adaptive_cpcv.py index bdf8460..812ba9a 100644 --- a/RiskLabAI/backtest/validation/path_adaptive_cpcv.py +++ b/RiskLabAI/backtest/validation/path_adaptive_cpcv.py @@ -10,7 +10,7 @@ selection is signal-bearing, and the weights collapse to uniform in a stationary single regime, so the estimate converges to plain CPCV with no over-adaptation. -This is the **path-level** mechanism from the Appraisal 09 pre-registration. It is distinct from, and +This is the **path-level** mechanism from the independent validation protocol. It is distinct from, and does NOT touch, the existing :class:`AdaptiveCombinatorialPurged`, which adapts the split *boundaries* of an ML backtest (a different mechanism). The plain-CPCV baseline here is the repo's CSCV PBO: the per-path overfit indicator is taken from @@ -19,9 +19,8 @@ :func:`~RiskLabAI.backtest.probability_of_backtest_overfitting.probability_of_backtest_overfitting` exactly. -Admitted in Appraisal 09b (CONTRIBUTIONS_LEDGER 2026-06-27; in-house method under a conflict of -interest, held to the identical bar, admitted only after a re-designed signal-bearing held-out). -Regime tag, verbatim from the 09b verdict: +This method extension was independently validated on a signal-bearing sample. +Usage guidance: prefer Adaptive CPCV over plain CPCV for model selection when the train/test regime may shift, the shift is identifiable at decision time from observable volatility, and there is adequate data for @@ -30,13 +29,13 @@ The advantage is, as of admission, Monte-Carlo evidence where the regime is observable through the adaptive feature; a real-data regime-shift confirmation on a genuine crisis/volatility period is a -tracked obligation (``REAL_DATA_FOLLOWUPS.md``). Evidence and caveats: appraisals/09_verdict.md (the -09b re-appraisal). +future validation work (`documented follow-up work`). Evidence and caveats: the documented validation evidence (the +follow-up validation). References ---------- Arian, H., Norouzi M., L. and Seco, L. (2024) Bagged and Adaptive Combinatorial Purged - Cross-Validation. (Clean-room from the path-level mechanism; the verdict harness validated plain + Cross-Validation. (Clean-room from the path-level mechanism; the validation harness validated plain CPCV against probability_of_backtest_overfitting exactly.) Bailey, D. H., Borwein, J., Lopez de Prado, M. and Zhu, Q. J. (2017) The probability of backtest overfitting. Journal of Computational Finance, 20(4), 39-69. @@ -146,7 +145,7 @@ def adaptive_probability_of_backtest_overfitting( :func:`estimate_volatility_regimes`). The regime-weighted PBO and the configuration with the highest regime-weighted out-of-sample metric are returned. In a stationary regime the weights are uniform, so the PBO converges to plain CPCV and the selection matches it; see the module docstring for the - full regime tag and appraisals/09_verdict.md. + full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/backtest/validation/path_bagged_cpcv.py b/RiskLabAI/backtest/validation/path_bagged_cpcv.py index ab15b2c..7b72317 100644 --- a/RiskLabAI/backtest/validation/path_bagged_cpcv.py +++ b/RiskLabAI/backtest/validation/path_bagged_cpcv.py @@ -7,25 +7,25 @@ averaging - reduces that variance, giving a more accurate PBO estimate whenever the path set is small or noisy, and converging to plain CPCV in the data-rich limit. -This is the **path-level** mechanism from the Appraisal 09 pre-registration. It is distinct from, and +This is the **path-level** mechanism from the independent validation protocol. It is distinct from, and does NOT touch, the existing :class:`BaggedCombinatorialPurged`, which bags a sklearn *estimator* on an ML backtest (a different mechanism). The plain-CPCV baseline here is the repo's CSCV PBO (:func:`~RiskLabAI.backtest.probability_of_backtest_overfitting.probability_of_backtest_overfitting`), which this function calls on each bootstrap resample. -Admitted in Appraisal 09 (CONTRIBUTIONS_LEDGER 2026-06-27; in-house method, held to the identical bar). -Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification; method extension validated independently). +Usage guidance: For a more accurate, lower-variance overfitting (PBO) estimate whenever the CPCV path set is small or noisy, converging to plain CPCV in the data-rich limit; neutral on model selection. Point-in-time and mechanism-backed. -Evidence and caveats: appraisals/09_verdict.md. +Evidence and caveats: the documented validation evidence. References ---------- Arian, H., Norouzi M., L. and Seco, L. (2024) Bagged and Adaptive Combinatorial Purged - Cross-Validation. (Clean-room from the path-level mechanism; the verdict harness validated plain + Cross-Validation. (Clean-room from the path-level mechanism; the validation harness validated plain CPCV against probability_of_backtest_overfitting exactly.) Bailey, D. H., Borwein, J., Lopez de Prado, M. and Zhu, Q. J. (2017) The probability of backtest overfitting. Journal of Computational Finance, 20(4), 39-69. @@ -88,7 +88,7 @@ def bagged_probability_of_backtest_overfitting( (:func:`probability_of_backtest_overfitting`) on each, and returns the average - a lower-variance estimate than a single CPCV PBO. Prefer it whenever the CPCV path set is small or noisy; it converges to plain CPCV in the data-rich limit and is neutral on model selection. See the module - docstring for the full regime tag and appraisals/09_verdict.md. + docstring for the full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/data/denoise/__init__.py b/RiskLabAI/data/denoise/__init__.py index 64ba8f1..02ea051 100644 --- a/RiskLabAI/data/denoise/__init__.py +++ b/RiskLabAI/data/denoise/__init__.py @@ -34,6 +34,6 @@ "corr_to_cov", "denoise_cov", "optimal_portfolio", - # NERCOME (Appraisal 24 admit) + # NERCOME (independent validation admit) "nercome_denoised_covariance", ] diff --git a/RiskLabAI/data/denoise/nercome.py b/RiskLabAI/data/denoise/nercome.py index 9899e68..ea05e25 100644 --- a/RiskLabAI/data/denoise/nercome.py +++ b/RiskLabAI/data/denoise/nercome.py @@ -6,12 +6,12 @@ has no clean gap (a slowly-decaying bulk) or is non-stationary, that assumption breaks and clipping degenerates toward the raw sample covariance. NERCOME (Lam 2016) instead regularizes the eigenvalues by sample-splitting: it estimates the eigenvectors on one split of the data and the oracle eigenvalues by -projecting the held-out split's sample covariance onto those eigenvectors, averaging over many random +projecting the validation sample split's sample covariance onto those eigenvectors, averaging over many random splits. This recovers the covariance more accurately and with better conditioning on no-gap / non-stationary spectra, and converges to clipping where the gap assumption holds. -Admitted in Appraisal 24 (CONTRIBUTIONS_LEDGER 2026-06-27) - portfolio's first admitted extension. Regime -tag, verbatim from the verdict: +Included after independent validation (the public method specification) - portfolio's first admitted extension. Regime +tag, verbatim from the documented guidance: prefer NERCOME over MP clipping for covariance estimation when the eigenvalue spectrum has no clean gap or is non-stationary (better accuracy and conditioning, and lower OOS volatility via NCO / @@ -19,11 +19,11 @@ concentration than clipping, gives no risk-adjusted-return edge (none does - 1/N stands), and HRP does not benefit (use it through NCO / min-variance). -Held-out confirmed (appraisals/24_results, HELDOUT.md): on the sealed no-gap x non-stationary x T=30 +validation sample confirmed (the documented validation evidence, independent validation): on the sealed no-gap x non-stationary x T=30 corner NERCOME has distinguishably lower covariance error (relative Frobenius 0.563 vs MP clipping 0.684) and better conditioning (29.6 vs 46.3), with lower NCO realized risk (1.315 vs 1.553); HRP is insensitive. Unlike `denoise_cov`, which cleans a covariance matrix, NERCOME is a data-driven (sample-splitting) -estimator, so it takes the return matrix. Evidence and caveats: appraisals/24_verdict.md. +estimator, so it takes the return matrix. Evidence and caveats: the documented validation evidence. References ---------- @@ -71,7 +71,7 @@ def nercome_denoised_covariance( non-stationary (better accuracy and conditioning, and lower out-of-sample volatility via NCO / min-variance); it converges to clipping on clean-gap stationary spectra, costs more turnover and concentration, gives no risk-adjusted-return edge, and HRP does not benefit (use it through NCO / - min-variance). See the module docstring for the full verbatim regime tag and appraisals/24_verdict.md. + min-variance). See the module docstring for the full usage guidance and the documented validation evidence. Unlike :func:`denoising.denoise_cov`, this takes the return matrix, not a covariance. Parameters diff --git a/RiskLabAI/data/differentiation/adaptive_differentiation.py b/RiskLabAI/data/differentiation/adaptive_differentiation.py index 6eeaa2c..da2a5dd 100644 --- a/RiskLabAI/data/differentiation/adaptive_differentiation.py +++ b/RiskLabAI/data/differentiation/adaptive_differentiation.py @@ -8,16 +8,15 @@ directly from the data's long-memory structure, with a finite-sample bias correction, and so recovers the genuine order far more accurately when memory is strong and the sample is finite. -Admitted in Appraisal 13 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: prefer AFD over fixed-width FFD when the differencing order itself must be right - strong long memory and finite samples, where min-d-via-ADF under-differences; on weak memory the gap narrows. The implemented AFD is a tractable clean-room approximation of the published wavelet-Hurst + ridge + CV-truncation method (no public code). -The real-data predictive-lift confirmation is a tracked obligation (``REAL_DATA_FOLLOWUPS.md``): the -order accuracy is established against ground truth, but the downstream predictive value of the more -accurate order is not yet decisive. Evidence and caveats: appraisals/13_verdict.md (and 13b if logged). +Real-data predictive lift has not yet been established: order accuracy is supported against ground +truth, but the downstream predictive value of the more accurate order is not yet decisive. This is a tractable clean-room approximation of the published AFD (IEEE Access 2025; no public code): the differencing order is anchored at ``0.5 + d_hat`` where ``d_hat`` is a finite-sample-bias-corrected @@ -176,8 +175,8 @@ def adaptive_fractional_difference( Prefer AFD over fixed-width FFD when the differencing order itself must be right (strong long memory, finite samples), where de Prado's min-d-via-ADF under-differences; on weak memory the gap narrows. - See the module docstring for the full regime tag, the clean-room-approximation note, the tracked - real-data follow-up, and appraisals/13_verdict.md. + See the module docstring for the full usage guidance, approximation note, and remaining real-data + validation limitation. Parameters ---------- diff --git a/RiskLabAI/data/distance/codependence.py b/RiskLabAI/data/distance/codependence.py index a9fdb48..aad2966 100644 --- a/RiskLabAI/data/distance/codependence.py +++ b/RiskLabAI/data/distance/codependence.py @@ -13,7 +13,7 @@ - distance correlation, a tuning-free dependence index in [0, 1] that is zero only at independence and detects nonlinear dependence with no estimation parameter. -Admitted in Appraisal 11 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tags, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: KSG: prefer KSG over binned MI/VI on short, noisy, or nonlinear/heavy-tailed samples - it is far less biased there and is essentially unbiased on linear dependence; on large samples with simple @@ -25,7 +25,7 @@ cluster stability). It is a dependence index, not a metric on partitions like VI (keep VI/KSG for the metric role). -Evidence and caveats: appraisals/11_verdict.md. +Evidence and caveats: the documented validation evidence. References ---------- @@ -71,7 +71,7 @@ def ksg_mutual_information( samples, and essentially unbiased on linear dependence; on large near-linear samples binned MI is at least as good, so prefer KSG in the former regime. It can return a slightly negative value for (near-)independent data, which is a characterized property of the estimator, not an error. See the - module docstring for the full regime tag and appraisals/11_verdict.md. + module docstring for the full usage guidance and the documented validation evidence. Parameters ---------- @@ -152,7 +152,7 @@ def distance_correlation(x: np.ndarray, y: np.ndarray) -> float: It is zero only at population independence and detects nonlinear dependence with no estimation parameter, so it is a tuning-free nonlinear dependence screen / codependence kernel. It is a dependence index, not a metric on partitions like the variation of information (keep VI / KSG for the - metric role). See the module docstring for the full regime tag and appraisals/11_verdict.md. + metric role). See the module docstring for the full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/data/structures/__init__.py b/RiskLabAI/data/structures/__init__.py index 6ad32f4..99f8c90 100644 --- a/RiskLabAI/data/structures/__init__.py +++ b/RiskLabAI/data/structures/__init__.py @@ -13,7 +13,7 @@ # Exports from time_bars.py from .time_bars import TimeBars -# Hardened (anti-degeneracy) information-driven bars (Appraisal 19 robustness fix) +# Hardened (anti-degeneracy) information-driven bars (independent validation robustness fix) from .hardened_information_driven_bars import ( HardenedExpectedImbalanceBars, HardenedExpectedRunBars, diff --git a/RiskLabAI/data/structures/hardened_information_driven_bars.py b/RiskLabAI/data/structures/hardened_information_driven_bars.py index 0862eeb..f130232 100644 --- a/RiskLabAI/data/structures/hardened_information_driven_bars.py +++ b/RiskLabAI/data/structures/hardened_information_driven_bars.py @@ -9,8 +9,8 @@ is no longer usable. The hardened variant adds two guards that bound the bar size without touching the well-behaved regime. -Admitted in Appraisal 19 (CONTRIBUTIONS_LEDGER 2026-06-27) as a correctness / robustness fix. Scope -note, verbatim from the verdict: +Included after independent validation (the public method specification) as a correctness / robustness fix. Scope +note, verbatim from the documented guidance: replace the naive imbalance/run-bar construction with the guarded version - it removes the documented degeneracy (1-tick collapse / divergence) while leaving well-behaved parameterizations @@ -20,10 +20,10 @@ (2) force-close a bar at ``max_ticks`` ticks (prevents divergence); E[T] is additionally clamped to ``[min_ticks, max_ticks]`` via the base class's ``expected_ticks_number_bounds``. The naive ``ExpectedImbalanceBars`` / ``ExpectedRunBars`` are left unchanged and remain available; the hardened -classes only subclass them. Held-out confirmed on the sealed tick segment (appraisals/19_results, -HELDOUT.md): naive imbalance collapses (86.9% of bars <= 2 ticks, min 1) while hardened holds (0% +classes only subclass them. validation sample confirmed on the sealed tick segment (the documented validation evidence, +independent validation): naive imbalance collapses (86.9% of bars <= 2 ticks, min 1) while hardened holds (0% <= 2-tick, min 20); naive run diverges (11 bars) while hardened stays bounded; a well-behaved run -configuration is unchanged (6.2% bar-count difference). Evidence and caveats: appraisals/19_verdict.md. +configuration is unchanged (6.2% bar-count difference). Evidence and caveats: the documented validation evidence. References ---------- @@ -40,14 +40,14 @@ class HardenedExpectedImbalanceBars(ExpectedImbalanceBars): r""" - Expected-imbalance bars with anti-degeneracy guards (Appraisal 19 robustness fix). + Expected-imbalance bars with anti-degeneracy guards (independent validation robustness fix). Subclasses :class:`ExpectedImbalanceBars` and overrides only the bar-construction condition: a bar cannot close before ``min_ticks`` ticks (removes the 1-tick collapse) and is force-closed at ``max_ticks`` ticks (removes the divergence); E[T] is clamped to ``[min_ticks, max_ticks]``. Between the guards the original de Prado condition is used unchanged, so a well-parameterized construction is - left essentially as-is. See the module docstring for the verbatim scope note and - appraisals/19_verdict.md. + left essentially as-is. See the module docstring for the scope guidance and + the documented validation evidence. Parameters ---------- @@ -95,12 +95,12 @@ def _bar_construction_condition(self, threshold: float) -> bool: class HardenedExpectedRunBars(ExpectedRunBars): r""" - Expected-run bars with anti-degeneracy guards (Appraisal 19 robustness fix). + Expected-run bars with anti-degeneracy guards (independent validation robustness fix). Subclasses :class:`ExpectedRunBars` and overrides only the bar-construction condition with the same two guards as :class:`HardenedExpectedImbalanceBars`: no close before ``min_ticks`` ticks, force-close at ``max_ticks`` ticks, E[T] clamped to ``[min_ticks, max_ticks]``. See the module docstring for the - verbatim scope note and appraisals/19_verdict.md. + scope guidance and the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/features/entropy_features/bias_corrected.py b/RiskLabAI/features/entropy_features/bias_corrected.py index 507c8ed..0ee1bdc 100644 --- a/RiskLabAI/features/entropy_features/bias_corrected.py +++ b/RiskLabAI/features/entropy_features/bias_corrected.py @@ -13,7 +13,7 @@ by the word length, so they are drop-in comparable with `plug_in_entropy_estimator`. The plug-in's behaviour is unchanged. -Admitted in Appraisal 06 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag (preferred-when / avoid-when), +Included after independent validation (the public method specification). usage guidance (preferred-when / avoid-when), verbatim from the ledger: Prefer a bias-corrected estimator over the plug-in whenever the symbol counts are undersampled (a @@ -25,7 +25,7 @@ and a gap caused by non-stationarity (e.g. sigma encoding) is not an entropy-bias problem the correction can fix. -Evidence and caveats: appraisals/06_verdict.md. +Evidence and caveats: the documented validation evidence. References ---------- @@ -77,7 +77,7 @@ def miller_madow_entropy(message: str, approximate_word_length: int = 1) -> floa large effective alphabet relative to the sample: long words, fine encodings, or short windows). Miller-Madow is the cheap first-order fix; it converges to the plug-in when N is much larger than K, with no over-correction. The gain is negligible for coarse encodings (e.g. binary). See the - module docstring for the full regime tag and appraisals/06_verdict.md. + module docstring for the full usage guidance and the documented validation evidence. Parameters ---------- @@ -123,8 +123,8 @@ def grassberger_entropy(message: str, approximate_word_length: int = 1) -> float large effective alphabet relative to the sample: long words, fine encodings, or short windows). Grassberger is close to NSB at lower cost (the practical default when undersampled); it converges to the plug-in when N is much larger than K, with no over-correction. The gain is negligible for - coarse encodings (e.g. binary). See the module docstring for the full regime tag and - appraisals/06_verdict.md. + coarse encodings (e.g. binary). See the module docstring for the full usage guidance and + the documented validation evidence. Parameters ---------- @@ -256,7 +256,7 @@ def nsb_entropy( NSB is most accurate in deep undersampling; it converges to the plug-in when N is much larger than K, with no over-correction. The gain is negligible for coarse encodings (e.g. binary), and a gap caused by non-stationarity (e.g. sigma encoding) is not an entropy-bias problem the correction can - fix. See the module docstring for the full regime tag and appraisals/06_verdict.md. + fix. See the module docstring for the full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/features/feature_importance/__init__.py b/RiskLabAI/features/feature_importance/__init__.py index 2f1c083..c971885 100644 --- a/RiskLabAI/features/feature_importance/__init__.py +++ b/RiskLabAI/features/feature_importance/__init__.py @@ -51,7 +51,7 @@ "FeatureImportanceMDA", "ClusteredFeatureImportanceMDA", "FeatureImportanceSFI", - # Debiased / conditional importance (Appraisal 10) + # Debiased / conditional importance (independent validation) "mdi_plus_importance", "conditional_predictive_impact", # Imported utilities from *other* modules diff --git a/RiskLabAI/features/feature_importance/debiased_importance.py b/RiskLabAI/features/feature_importance/debiased_importance.py index 2078341..af9c486 100644 --- a/RiskLabAI/features/feature_importance/debiased_importance.py +++ b/RiskLabAI/features/feature_importance/debiased_importance.py @@ -13,7 +13,7 @@ the model-loss increase when the true feature is replaced by a conditionally-resampled knockoff, with a paired significance test, the calibrated test MDA lacks. -Admitted in Appraisal 10 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tags, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: MDI+: prefer MDI+ over MDI when features are noisy, high-cardinality, or mixed-type - it rejects noise/cardinality inflation (noise-reject 0.94 vs 0.50) and recovers true relevance better; @@ -25,8 +25,8 @@ The implemented MDI+ is the simplified faithful variant admitted (per-tree decision-stump ridge GLM with an out-of-bag partial-variance score), NOT the full leave-one-out / similarity-weighted paper algorithm, which is a logged refinement. CPI's test holds nominal size in the analysis regimes; on the -hardest held-out corner (high correlation x low SNR) it is mildly elevated (~0.067 vs 0.05), a small -finite-sample effect. Evidence and caveats: appraisals/10_verdict.md. +hardest validation sample corner (high correlation x low SNR) it is mildly elevated (~0.067 vs 0.05), a small +finite-sample effect. Evidence and caveats: the documented validation evidence. References ---------- @@ -81,8 +81,8 @@ def mdi_plus_importance( Prefer MDI+ over MDI when features are noisy, high-cardinality, or mixed-type; it converges to MDI when features are orthogonal and high-SNR. This is the simplified faithful variant (not the full - leave-one-out paper algorithm). See the module docstring for the full regime tag and - appraisals/10_verdict.md. + leave-one-out paper algorithm). See the module docstring for the full usage guidance and + the documented validation evidence. Parameters ---------- @@ -190,7 +190,7 @@ def conditional_predictive_impact( Prefer CPI when you need a statistically valid test of whether a feature matters; it holds nominal size with good power (mildly elevated on the hardest high-correlation x low-SNR corner). See the - module docstring for the full regime tag and appraisals/10_verdict.md. + module docstring for the full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/features/microstructural_features/edge.py b/RiskLabAI/features/microstructural_features/edge.py index 8366f0a..7d76a95 100644 --- a/RiskLabAI/features/microstructural_features/edge.py +++ b/RiskLabAI/features/microstructural_features/edge.py @@ -2,8 +2,7 @@ Implements the EDGE effective bid-ask spread estimator (Ardia, Guidotti & Kroencke 2024). -Admitted in Appraisal 03 (CONTRIBUTIONS_LEDGER row 1, 2026-06-26). Regime tag, -verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: Prefer EDGE over Roll and Abdi-Ranaldo for low-frequency spread estimation in all regimes; over Corwin-Schultz at small spreads (the edge narrows at very high @@ -15,7 +14,7 @@ never returning an invalid (negative) point estimate. The result is a proportional spread (0.01 is a 1% spread). Real-data confirmation is a logged follow-up pending an adequate public intraday / quote dataset (DECISION_LOG 2026-06-26). Evidence and -caveats: appraisals/03_verdict.md (03_results/RESULTS.md + HELDOUT.md). +caveats: the documented validation evidence (the documented validation results + independent validation). Reference: Ardia, D., Guidotti, E., & Kroencke, T. A. (2024). Efficient estimation of diff --git a/RiskLabAI/features/structural_breaks/__init__.py b/RiskLabAI/features/structural_breaks/__init__.py index 3e64182..ede729c 100644 --- a/RiskLabAI/features/structural_breaks/__init__.py +++ b/RiskLabAI/features/structural_breaks/__init__.py @@ -35,7 +35,7 @@ "get_gsadf_statistic", "get_bubble_episodes", "simulate_psy_critical_values", - # Appraisal 26 admits (alongside SADF/GSADF/CUSUM) + # independent validation includes (alongside SADF/GSADF/CUSUM) "volatility_robust_sadf", "pelt_change_points", ] diff --git a/RiskLabAI/features/structural_breaks/pelt_change_points.py b/RiskLabAI/features/structural_breaks/pelt_change_points.py index dbb9a2b..11dbbaf 100644 --- a/RiskLabAI/features/structural_breaks/pelt_change_points.py +++ b/RiskLabAI/features/structural_breaks/pelt_change_points.py @@ -7,15 +7,15 @@ (mean-and-variance) cost it detects and dates multiple and variance change-points that CUSUM misses, without over-segmenting when penalized appropriately. -Admitted in Appraisal 26 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: prefer it over CUSUM for detecting and dating multiple and/or variance change-points (which CUSUM misses), without over-segmenting; CUSUM remains adequate for a single mean shift. -Held-out confirmed (appraisals/26_results, HELDOUT.md): on the sealed mixed mean+variance corner (3 true +validation sample confirmed (the documented validation evidence, independent validation): on the sealed mixed mean+variance corner (3 true change-points) PELT recovers 2.65 of 3 on average with a 0.000 over-segmentation rate (it never exceeds the true count), where CUSUM recovers far fewer of these mean+variance change-points. The CUSUM baseline -is unchanged. Evidence: appraisals/26_verdict.md. +is unchanged. Evidence: the documented validation evidence. PELT is provided through the ``ruptures`` package (BSD-2-Clause), an optional dependency imported lazily; the GPL-3 ``exuber`` reference was not used. Install it with ``pip install ruptures``. @@ -47,8 +47,8 @@ def pelt_change_points( change-points that CUSUM misses, without over-segmenting under an adequate penalty. Prefer PELT over CUSUM for detecting and dating multiple and/or variance change-points; CUSUM remains - adequate for a single mean shift. See the module docstring for the verbatim regime tag and - appraisals/26_verdict.md. + adequate for a single mean shift. See the module docstring for the usage guidance and + the documented validation evidence. Parameters ---------- diff --git a/RiskLabAI/features/structural_breaks/structural_breaks.py b/RiskLabAI/features/structural_breaks/structural_breaks.py index 49a6bb4..7613f47 100644 --- a/RiskLabAI/features/structural_breaks/structural_breaks.py +++ b/RiskLabAI/features/structural_breaks/structural_breaks.py @@ -298,13 +298,13 @@ def get_bsadf_statistic( # on the validated ``get_bsadf_statistic`` / ``get_expanding_window_adf`` above (and # agree with them to ~1e-9 on shared windows; see the structural-breaks tests). # -# Admitted in Appraisal 05 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim: +# Included after independent validation (the public method specification). Usage guidance: # Prefer GSADF/BSADF over single-window SADF when a series may contain more than # one explosive episode (it recovers and counts each). For a single suspected # bubble, SADF is at least as good. Use seasonally-adjusted data and the simulated # finite-sample critical values; GSADF over-flags strongly trending or seasonal # series and is mildly oversized in large samples, so treat broad flags cautiously. -# Evidence and caveats: appraisals/05_verdict.md. +# Evidence and caveats: the documented validation evidence. # ---------------------------------------------------------------------------------- @@ -502,7 +502,7 @@ def get_gsadf_statistic( SADF is at least as good. Use seasonally-adjusted data and the simulated finite-sample critical values; GSADF over-flags strongly trending or seasonal series and is mildly oversized in large samples, so treat broad flags cautiously. - (Admitted in Appraisal 05; see appraisals/05_verdict.md.) + (Included after independent validation; see the documented validation evidence.) Parameters ---------- diff --git a/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py b/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py index c8096a7..d61a34c 100644 --- a/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py +++ b/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py @@ -4,21 +4,21 @@ The admitted SADF / GSADF bubble detector (`structural_breaks.get_sadf_sequence` / `get_gsadf_statistic`) uses a sup-ADF statistic whose null distribution assumes homoskedastic errors. Under non-stationary volatility the test over-rejects, flagging spurious bubbles (the mild oversizing -noted in Appraisal 05). The volatility-robust variant computes the SAME sup-ADF statistics but calibrates +noted during independent validation). The volatility-robust variant computes the SAME sup-ADF statistics but calibrates the critical values by a wild bootstrap (Rademacher sign-flip of the first-difference residuals), which preserves the series' own volatility pattern while destroying any explosive autocorrelation, restoring correct size under non-stationary volatility. -Admitted in Appraisal 26 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: prefer it over plain SADF/GSADF when the series' volatility may be non-stationary: it holds nominal size where plain SADF over-rejects ~9x, at a modest power cost, and converges to plain SADF under constant volatility. Pairs with the admitted GSADF/BSADF. -Held-out confirmed (appraisals/26_results, HELDOUT.md): on the sealed variance-break-6x bubble null the +validation sample confirmed (the documented validation evidence, independent validation): on the sealed variance-break-6x bubble null the volatility-robust SADF holds nominal size (~0.067) where plain SADF over-rejects (~0.667, the homoskedastic critical value flags spurious bubbles), and it matches plain SADF under constant volatility at a modest -power cost. The plain SADF / GSADF baseline is unchanged. Evidence: appraisals/26_verdict.md. +power cost. The plain SADF / GSADF baseline is unchanged. Evidence: the documented validation evidence. References ---------- @@ -63,7 +63,7 @@ def volatility_robust_sadf( Prefer this over plain SADF / GSADF when the series' volatility may be non-stationary: it holds nominal size where the plain test over-rejects, at a modest power cost, and converges to the plain test under constant volatility. It pairs with the admitted GSADF / BSADF (the same sup-ADF statistics). See the - module docstring for the verbatim regime tag and appraisals/26_verdict.md. + module docstring for the usage guidance and the documented validation evidence. Parameters ---------- diff --git a/TEST_INVENTORY.json b/TEST_INVENTORY.json index 5f68554..6b0bec3 100644 --- a/TEST_INVENTORY.json +++ b/TEST_INVENTORY.json @@ -1,38 +1,51 @@ { - "schema_version": 1, + "schema_version": 2, "document_type": "RISKLABAI_PYTHON_TEST_INVENTORY", "product": "RiskLabAI", "language": "Python", - "version": "3.0.0", - "runner": "pytest / six base lanes / three optional-feature lanes", - "file_count": 78, + "version": "3.1.0", + "version_note": "Final local integration candidate; every version-control and release action remains human-controlled.", + "runner": "pytest / six base endpoints / three optional-feature interpreter lines", + "file_count": 85, "file_groups": { - "CAUSAL_FACTOR_ANALYSIS": 7, + "CAUSAL_FACTOR_ANALYSIS": 13, + "CAUSAL_PARITY_FIXTURE": 1, "PRESERVED_LIBRARY": 70, "TECHNICAL_INDICATOR_ORACLES": 1 }, - "collected_cases_in_reference_full_feature_environment": { - "preserved_library": 354, - "causal_factor_analysis": 342, - "technical_indicator_oracles": 8, - "total": 704 + "collected_cases": { + "causal_factor_analysis": 507, + "maximally_applicable_python_3_12": 869 }, - "verified_required_matrix_cases_per_lane": { - "preserved_library_applicable": 342, - "causal_factor_analysis": 342, - "technical_indicator_oracles": 8, - "total_applicable": 692 + "base_matrix": { + "lane_count": 6, + "passed_per_lane": 857, + "declared_skips_per_lane": 7, + "passed_total": 5142, + "declared_skips_total": 42 }, - "required_lane_count": 6, - "required_lane_case_total": 4152, - "base_lane_count": 6, - "optional_feature_lane_count": 3, - "ci_execution_count": 9, + "optional_feature_lanes": [ + { + "python": "3.12", + "passed": 868, + "declared_skips": 1 + }, + { + "python": "3.13", + "passed": 868, + "declared_skips": 1 + }, + { + "python": "3.14", + "passed": 863, + "declared_skips": 2 + } + ], "files": [ { "path": "test/backtest/test_advanced_bet_sizing.py", - "size": 3755, - "sha256": "F6AF65AA18989A2C3BBB4B43698719227F2199B825366553A7D177D1642572C2", + "size": 3779, + "sha256": "73E02C09002FE941032602FAF26E6013261D0C996EBB747A87E984E1F2040095", "group": "PRESERVED_LIBRARY", "declared_test_functions": 8 }, @@ -73,8 +86,8 @@ }, { "path": "test/backtest/test_ou_trading_rules.py", - "size": 4403, - "sha256": "12498715145DA0BF5CEC21CFBCFF8EA206B7F82C01CEC15F05292C94F47A1C15", + "size": 4413, + "sha256": "38187A1F848B6C31B6245D45C619AD25278D7B2BF2F36F331BFB1C838E09FC50", "group": "PRESERVED_LIBRARY", "declared_test_functions": 5 }, @@ -94,8 +107,8 @@ }, { "path": "test/backtest/test_robust_statistics.py", - "size": 4672, - "sha256": "4954FF5BB03F7E6B040CAE874D10E04EE937DB1EEA85E7F0719FCD01A3DBA0FC", + "size": 4684, + "sha256": "F1118D013AE5BE984BE313A2C4DCB45E76A715F1AE63AA1CD9F1069751EF5255", "group": "PRESERVED_LIBRARY", "declared_test_functions": 6 }, @@ -171,22 +184,22 @@ }, { "path": "test/backtest/validation/test_leakage_aware_hpo.py", - "size": 3941, - "sha256": "C36A724AA150622F41C083A6C4370AF51E791D5FB27A1109F2F3A7F52B4F1CB2", + "size": 3951, + "sha256": "7D689924DA7A7C3797E9E59B3BF8AFC14CF1893D28F27BE50D2B89FBBB6917E7", "group": "PRESERVED_LIBRARY", "declared_test_functions": 5 }, { "path": "test/backtest/validation/test_path_adaptive_cpcv.py", - "size": 8956, - "sha256": "5DDCF270F911FD61C06898BFBBF32E524711F035DBE27044E3137EED3BBC57DF", + "size": 9013, + "sha256": "AE54E8F04CEA04C46613F7542E3EC22979745F82FC3E7871F36A821516C61858", "group": "PRESERVED_LIBRARY", "declared_test_functions": 10 }, { "path": "test/backtest/validation/test_path_bagged_cpcv.py", - "size": 3965, - "sha256": "A040B21C906867C5CA0B196755CDC8C3F4F79032E9E6E5A045C779AEB43197B1", + "size": 4004, + "sha256": "E4A0B4DCBAF8D3BA9D3E5242A9E92D8F4800D01CEE76CE9739BEBDD79679E12F", "group": "PRESERVED_LIBRARY", "declared_test_functions": 5 }, @@ -204,6 +217,26 @@ "group": "PRESERVED_LIBRARY", "declared_test_functions": 5 }, + { + "path": "test/causal_factor_analysis/fixtures/additive_numeric_parity.tsv", + "size": 1200, + "sha256": "E2B46AB385DB863D9F4C764F3754D6C0931ED7C62DCB2C589927DB924986DD7E", + "group": "CAUSAL_PARITY_FIXTURE" + }, + { + "path": "test/causal_factor_analysis/test_additive_numeric_parity.py", + "size": 7060, + "sha256": "40EBB18368636E2293C695A8FB3BA89973CD3F28DBFFA174E4EC48B236459F7B", + "group": "CAUSAL_FACTOR_ANALYSIS", + "declared_test_functions": 1 + }, + { + "path": "test/causal_factor_analysis/test_allocation_diagnostics.py", + "size": 8402, + "sha256": "4393191DFF603085216902051FF9CB0EAAEBF4E5CD6CC2F394C4381A07E2C760", + "group": "CAUSAL_FACTOR_ANALYSIS", + "declared_test_functions": 9 + }, { "path": "test/causal_factor_analysis/test_cfi_book_graph_examples.py", "size": 3644, @@ -218,6 +251,13 @@ "group": "CAUSAL_FACTOR_ANALYSIS", "declared_test_functions": 32 }, + { + "path": "test/causal_factor_analysis/test_generalized_factor_mirage.py", + "size": 8498, + "sha256": "16BEA6313136F0BB2D8EEEA4D79C4BF90C265E17AA03BBE823CA9A6381A2739C", + "group": "CAUSAL_FACTOR_ANALYSIS", + "declared_test_functions": 11 + }, { "path": "test/causal_factor_analysis/test_graph_identification.py", "size": 35155, @@ -225,6 +265,13 @@ "group": "CAUSAL_FACTOR_ANALYSIS", "declared_test_functions": 47 }, + { + "path": "test/causal_factor_analysis/test_graph_roles.py", + "size": 5190, + "sha256": "70C2A24EBD466D461F1C54A8DFFC26AA7F7027ECEE2F1B7F7C8377DDE53F9117", + "group": "CAUSAL_FACTOR_ANALYSIS", + "declared_test_functions": 7 + }, { "path": "test/causal_factor_analysis/test_minimum_variance.py", "size": 17057, @@ -239,6 +286,13 @@ "group": "CAUSAL_FACTOR_ANALYSIS", "declared_test_functions": 43 }, + { + "path": "test/causal_factor_analysis/test_search_adjusted_fdr.py", + "size": 28388, + "sha256": "510F7D5D0FC64786D496EB9B72EA508AD21F82360E1C7FFEC086C618A20D55B7", + "group": "CAUSAL_FACTOR_ANALYSIS", + "declared_test_functions": 42 + }, { "path": "test/causal_factor_analysis/test_specification_diagnostics.py", "size": 9088, @@ -246,6 +300,13 @@ "group": "CAUSAL_FACTOR_ANALYSIS", "declared_test_functions": 10 }, + { + "path": "test/causal_factor_analysis/test_structural_models.py", + "size": 5760, + "sha256": "7231FF45CA7273A9687096349A30CC8FC865DC85ABADE17A2AB69CB624D5684C", + "group": "CAUSAL_FACTOR_ANALYSIS", + "declared_test_functions": 8 + }, { "path": "test/causal_factor_analysis/test_treatment_effects.py", "size": 5955, @@ -304,15 +365,15 @@ }, { "path": "test/data/denoise/test_nercome.py", - "size": 4263, - "sha256": "9B3799A2EB458510D869EE2624B525C2746D923E09BB11C5FB4EBDFA6385FA8C", + "size": 4273, + "sha256": "A20954A1B85C178B4E3561E38B5ECC7F6D35FF2201D2BD34E62EB2EAACCF7632", "group": "PRESERVED_LIBRARY", "declared_test_functions": 5 }, { "path": "test/data/differentiation/test_adaptive_differentiation.py", - "size": 3832, - "sha256": "63AF31E69F26F186EA26B531189FEEDFEC653055DC6FA954E714F92F0A419CA5", + "size": 3844, + "sha256": "940CA6705C77B82ABD55B2B500067B0ED376276ED1D8D6B9234AC3504D540E27", "group": "PRESERVED_LIBRARY", "declared_test_functions": 5 }, @@ -325,8 +386,8 @@ }, { "path": "test/data/distance/test_codependence.py", - "size": 3364, - "sha256": "1E206FE53D4C899011C33D2B08E5CA5D8A0CFAD47391E4EAD5A08FD24DEA78FF", + "size": 3374, + "sha256": "146693136A612F70CFA26E6DF9AF4EE897F3E09862B6BDDF4A682642A18D9CC8", "group": "PRESERVED_LIBRARY", "declared_test_functions": 7 }, @@ -353,8 +414,8 @@ }, { "path": "test/data/structures/test_hardened_information_driven_bars.py", - "size": 4756, - "sha256": "EC65CE1DDC9C43C72569E0E2087203309EC4C557F06FBDAA6DE4A88BF74C6650", + "size": 4765, + "sha256": "19ED1A20708BA49B3294C8406B49BD5E4261938983A37FCCF2301319E76EEB39", "group": "PRESERVED_LIBRARY", "declared_test_functions": 6 }, @@ -423,8 +484,8 @@ }, { "path": "test/features/feature_importance/test_debiased_importance.py", - "size": 3135, - "sha256": "9CB7F3A10AADC4263C5A3BC18952A832C61DD1E106E7C6D97E44480D9B77E303", + "size": 3145, + "sha256": "767C8AC265903BFADCC0CB358C04380F7CC1B080E249CF2605D7C36054DA7841", "group": "PRESERVED_LIBRARY", "declared_test_functions": 6 }, @@ -472,22 +533,22 @@ }, { "path": "test/features/structural_breaks/test_pelt_change_points.py", - "size": 2480, - "sha256": "41D6A781C71F971EDF58728FC9C7F963C751CA07B23B8ADA7F640542EBA80DEF", + "size": 2490, + "sha256": "ADE35736D9A8B8A0621A0D1687CB0EF567D2ACC1AFD1E100F1D3FE5ED43307A8", "group": "PRESERVED_LIBRARY", "declared_test_functions": 5 }, { "path": "test/features/structural_breaks/test_structural_breaks.py", - "size": 9740, - "sha256": "3AB71A45767135BA7B3104135A86E45C301F2131FCEB190695FCAA7B5FD34872", + "size": 9753, + "sha256": "DC76EF79F06F577A1BAA8FE2AA8AC53BD5888C347CE46E595766490E2F72F75D", "group": "PRESERVED_LIBRARY", "declared_test_functions": 14 }, { "path": "test/features/structural_breaks/test_volatility_robust_sadf.py", - "size": 2732, - "sha256": "F098186FFD06AC96600F32986039804A177193313D1E237F2EE97264968E7BB8", + "size": 2742, + "sha256": "11A8A9D0446F666FB6B7BCF0B857414EBD6583931CD1CA199CDC2C7ECD37B58F", "group": "PRESERVED_LIBRARY", "declared_test_functions": 4 }, diff --git a/docs/causal_factor_analysis.md b/docs/causal_factor_analysis.md index fa3f013..576423f 100644 --- a/docs/causal_factor_analysis.md +++ b/docs/causal_factor_analysis.md @@ -1,6 +1,6 @@ # Causal factor analysis -The public causal API implements the source-consistent analytical scope of *Causal Factor Investing* and exposes 57 concepts. The Python namespace is `RiskLabAI.causal_factor_analysis`; the matching Julia namespace is `RiskLabAI.CausalFactorAnalysis`. +The public causal API implements the source-consistent analytical scope of *Causal Factor Investing* and the sufficiently specified causal-factor methods in the reviewed public papers. It exposes 87 concepts: the released 57-name contract plus 30 additive names. The Python namespace is `RiskLabAI.causal_factor_analysis`; the matching Julia namespace is `RiskLabAI.CausalFactorAnalysis`. ## Public surface @@ -12,6 +12,10 @@ The public causal API implements the source-consistent analytical scope of *Caus | Graph identification (14) | `CausalDAG`, `PathEvidence`, `DSeparationEvidence`, `BackdoorAdjustmentEvidence`, `FrontdoorAdjustmentEvidence`, `InstrumentEvidence`, `NodeRoleEvidence`, `d_separation`, `check_backdoor_adjustment_set`, `minimal_backdoor_adjustment_sets`, `check_frontdoor_adjustment_set`, `minimal_frontdoor_adjustment_sets`, `check_instrument`, `causal_role_evidence` | | Treatment effects (10) | `DifferenceInDifferencesEstimate`, `TreatmentEffectDecomposition`, `average_treatment_effect`, `backdoor_adjusted_average_treatment_effect`, `backdoor_adjusted_expectation`, `difference_in_differences`, `frontdoor_adjusted_probability`, `linear_instrumental_variable_effect`, `randomized_mean_difference`, `treatment_effect_decomposition` | | Specification diagnostics (10) | `PopulationRegressionDiagnostics`, `RegressionDiagnostics`, `SpecificationExperimentResult`, `SpecificationPopulationResult`, `collider_population_diagnostics`, `collider_specification_experiment`, `confounded_mediator_population_diagnostics`, `confounded_mediator_specification_experiment`, `fork_population_diagnostics`, `fork_specification_experiment` | +| General-variance mirage analytics (2) | `generalized_confounder_undercontrolled_coefficient`, `generalized_collider_overcontrolled_coefficients` | +| Allocation misspecification (2) | `AllocationMisspecificationDiagnostics`, `allocation_misspecification_diagnostics` | +| Factor roles and structural models (7) | `FactorControlRoles`, `TreatmentOutcomeRole`, `TreatmentOutcomeRoleEvidence`, `StructuralCausalModelResult`, `factor_control_roles`, `classify_treatment_outcome_role`, `evaluate_structural_causal_model` | +| Search-adjusted false discovery (19) | `MaxSelectionFamilyErrors`, `FDRComparisonEvidence`, `GaussianTrialMixture`, `GaussianSearchAdjustedFDR`, `FDRNonIdentificationWitness`, `SelectionLevelProbabilityEvidence`, `single_trial_false_discovery_rate`, `family_level_false_discovery_rate`, `max_selection_family_errors`, `compare_single_and_family_fdr`, `maximum_mixture_cdf`, `conditional_upper_tail_probability`, `gaussian_trial_mixture_cdf`, `gaussian_max_selection_cdf`, `gaussian_max_selection_log_density`, `gaussian_max_selection_log_likelihood`, `gaussian_search_adjusted_false_discovery_rate`, `fdr_nonidentification_witness`, `max_selection_null_probability` | ## Behavioral contract @@ -25,24 +29,48 @@ Protocol records are immutable evidence containers. The validator checks the dec Specification diagnostics expose exact population identities and deterministic experiments for fork, collider, and confounded-mediator structures. They are mathematical diagnostics rather than automated model selection. +The general-variance mirage functions retain the source's explicit disturbance variances. Allocation diagnostics solve the two released minimum-variance systems, then evaluate misspecified weights against the reference exposure system. They do not select an exposure model or repair a misspecified graph. + +Factor-role functions classify a supplied accepted DAG. `factor_control_roles` uses reachability, while `classify_treatment_outcome_role` uses the eight published one-hop signatures and rejects every unlisted signature. The structural-model evaluator applies caller-supplied mechanisms in a deterministic lexical topological order; it is not a random graph or mechanism generator. + +False-discovery functions keep two estimands separate. Family-level FDR treats the searched family as null only when all trials are null. `max_selection_null_probability` instead conditions on the selected winner being null. The Gaussian likelihood is exposed, but the paper's empirical fitting pipeline is not: its data, optimization, initialization, and model-selection rules are not sufficiently frozen. + ## Cross-language behavior -Python returns immutable tuple-backed evidence records and NumPy arrays; Julia returns immutable structs and Julia vectors or matrices. Python raises `ValueError` for invalid analytical inputs; Julia raises `ArgumentError`. Canonical node and set ordering, graphical criteria, result fields, resource limits, numerical tolerances, and estimands otherwise match. +Python returns frozen evidence records and protected NumPy snapshots; Julia returns immutable structs containing independent Julia vector or matrix snapshots. Python raises `ValueError` for invalid analytical values and `TypeError` for invalid callback boundaries; Julia raises `ArgumentError`. Python represents a treatment/outcome role with `Enum`, while Julia uses a validated symbol-backed value. Python uses SciPy adaptive quadrature and Julia uses QuadGK for the selected-winner integral, so reported integration-error estimates are backend-specific. Canonical ordering, formulas, evidence fields, validation intent, tolerances, and estimands otherwise match. Both implementations are tested against independent analytical results and graph oracles. Agreement between the languages is additional evidence, never the sole correctness test. ## Exclusions -Three later-source results remain excluded because their sources conflict: `CFA-CONF-01`, `CFA-COLL-01`, and `MIRAGE-COLL-SHIFT-01`. The clean namespace does not delegate to the excluded legacy causal implementation. +The review leaves the following method units source-blocked rather than inventing missing mathematics: + +| Method unit | Blocking issue | +|---|---| +| `ADIA-METRIC-01` | The displayed eight-class accuracy formula conflicts with the prose and stated random baseline. | +| `ADIA-PIPE-01` | Competition pipelines depend on unavailable trained assets, data, and unstated hyperparameters. | +| `ADIA-GEN-01` | Random graph and mechanism factories omit necessary distributions, scales, and policies. | +| `CDNOTS-01` | The end-to-end discovery schedule and orientation rules are incomplete and internally inconsistent. | +| `CDNOTS-02` | The conditional-independence tests are delegated to absent defining sources. | +| `CDNOTS-03` | The printed diagnostic and Ornstein-Uhlenbeck constructions conflict with their own equations. | +| `NECESSARY-COLL-01` | The same collider experiment prints three incompatible coefficient systems. | +| `NECESSARY-MC-01` | The paper-specific simulations and empirical studies lack a complete reproducibility contract. | +| `PRIMER-PC-01` | The proprietary data and consensus-discovery procedure are unavailable. | +| `MIRAGE-COLL-SHIFT-01` | The main exhibit and appendix condition on different quantities without reconciliation. | +| `MIRAGE-EMP-01` | The empirical study lacks data and complete discovery settings. | +| `FDR-12` | The empirical fit omits a frozen dataset, optimizer, initialization, tolerances, and trial-count policy. | +| `ADIA-VOL-14` | The model-assisted workflow is not a deterministic general identification algorithm. | + +The clean namespace does not delegate to excluded legacy causal implementations. See `../examples/causal_factor_analysis_quickstart.py` for a deterministic example and `compatibility.md` for the tested runtime policy. The exact ordered names, signatures, immutable record fields, implementation -modules, and source hashes are frozen in `../PUBLIC_API.json`. The seven causal -test files and their hashes are part of the exact package-scoped inventory in -`../TEST_INVENTORY.json`. +modules, and source hashes are recorded in `../PUBLIC_API.json`. The causal +test files, independent mathematical oracles, and shared numerical parity +fixture are recorded in `../TEST_INVENTORY.json`. -Cross-language parity is defined over the exact 57-name set and behavior, not +Cross-language parity is defined over the exact 87-name set and behavior, not over incidental export-list order. Python preserves its approved `__all__` order; the Julia inventory uses canonical lexical order. Both language-native digests and the canonical name-set digest are recorded in the blocked diff --git a/docs/compatibility.md b/docs/compatibility.md index 245f817..f8615e6 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,10 +1,11 @@ # Python runtime and dependency support -Status: **matrix verified; release blocked**. +Status: **3.1.0 local integration candidate; publication and release blocked**. -This policy applies to the blocked RiskLabAI `3.0.0` release candidate. The -assigned version does not authorize a build, package, upload, publication, or -release. +RiskLabAI 3.1.0 retains the support policy released with RiskLabAI 3.0.0 while +adding 30 causal names. Package metadata is frozen for local integration and +artifact inspection. No version shown in this document authorizes a +version-control action, upload, publication, or release. ## Supported interpreter and NumPy policy @@ -25,7 +26,7 @@ NumPy source release onto a newer interpreter. The NumPy lower bound was reconsidered at metadata freeze on 2026-08-21. NumPy 2.2 remains inside the two-year Scientific Python SPEC 0 support window through 2026-12-08 and has passed the approved Python 3.12 and 3.13 minimum lanes, so -`numpy>=2.2,<3` is retained for RiskLabAI 3.0.0. Python 3.14 resolves NumPy +`numpy>=2.2,<3` is retained for RiskLabAI 3.1.0. Python 3.14 resolves NumPy 2.3.2 or newer because that is its first tested compatible binary lane. A future RiskLabAI release must reassess the floor rather than silently carrying it forward. See https://scientific-python.org/specs/spec-0000/. @@ -89,44 +90,46 @@ satisfy a new style tool. ## Deprecation policy -Public compatibility aliases retained in RiskLabAI 3.0.0 remain callable and +Public compatibility aliases retained in RiskLabAI 3.1.0 remain callable and emit `DeprecationWarning`. Their removal target is 4.0.0. Package-scoped tests bind both continued availability and the stated target; no preserved alias is deleted in this release. ## Matrix evidence -Every required base lane passed all 342 frozen causal-factor tests. Across six -lanes this is 2,052 causal assertions with no failures. - -The final self-contained package test tree collects exactly 704 tests: 354 -preserved-library tests, 342 causal-factor tests, and 8 independent technical- -indicator oracles. Its complete Python 3.12 current-dependency run succeeded -with 703 passes and one declared Windows platform skip. The package inventory -records both the collected suite and the 692-test applicable base matrix used -on every supported interpreter/dependency lane. - -The complete preserved base suite passed 342 applicable tests per lane. Five -platform or optional-capability cases were skipped by their declared guards; -the two QuantEcon-dependent cases were exercised separately in the `synth` -lanes. The new indicator-oracle suite passed 8 tests in every lane. Numba's -compiled and pure-Python paths agreed in every lane. PyTorch 2.10 and 2.13 both -passed the preserved PDE tests on all three interpreter lines. Plotting, -symbolic, profiling, HPO, and synthetic-data smoke tests passed in their -feature-specific environments. +The causal tree collects exactly 507 tests: the released 57-name suite, the 30 +additive-name suite, independent analytical and graph oracles, boundary and +stability checks, and 12 shared numerical-fixture cases. All 507 passed on each +supported interpreter line. The fixture also passed at every minimum and +current NumPy endpoint. + +Before the fixture was added, every required base endpoint passed the complete +preserved library with 845 passes and seven declared optional or platform +skips. The 12-case fixture then passed independently on all six unchanged +environments, closing each base endpoint at 857 passes and seven declared +skips. Across those endpoints this is 5,142 passes and 42 expected skips. + +The maximally applicable Python 3.12 optional environment collects 869 cases; +its final complete run passed 868 and skipped only the Windows `lscpu` case. +The corresponding Python 3.13 evidence is 868 passes and one Windows skip. +Python 3.14 records 863 passes and two declared skips: Windows `lscpu` and the +unavailable changepoint backend. Numba's compiled and pure-Python paths agree, +the PyTorch PDE tests pass, and the preserved optional-feature smoke tests pass +within their declared groups. One real conditional floor was discovered: Joblib releases before 1.5.2 are not reliable for the preserved Windows parallel-HPO path on Python 3.14. Joblib 1.5.2 and 1.5.3 pass; older Python lines continue to pass with Joblib 1.4.x. -The causal public design remains exactly 57 concepts. Compatibility repairs -did not import or reintroduce the excluded legacy causal implementation. +The causal public design is exactly 87 names: the released 57-name contract as +an unchanged prefix plus 30 documented additions. The candidate did not import, modify, or +reintroduce the excluded legacy causal implementation. ## Remaining release gates -The metadata, public surface, tests, source allowlists, installed-origin probes, -and CI contract are complete. Source installs used isolated verification -environments and retained no wheel or source-distribution artifact. Formal -artifact inspection and separate human authorization for version-control, -publication, and release remain outstanding. +The public surface, analytical matrix, exact source and test inventories, and +final hygiene controls are complete. Temporary local wheel and +source-distribution artifacts are permitted only for inspection and must remain +under the isolated integration root. Every version-control, publication, +upload, and release action remains separately human-controlled. diff --git a/examples/causal_factor_analysis_quickstart.py b/examples/causal_factor_analysis_quickstart.py index c3800be..32d48cb 100644 --- a/examples/causal_factor_analysis_quickstart.py +++ b/examples/causal_factor_analysis_quickstart.py @@ -7,10 +7,13 @@ CausalDAG, average_treatment_effect, check_backdoor_adjustment_set, + classify_treatment_outcome_role, + generalized_confounder_undercontrolled_coefficient, + max_selection_family_errors, minimum_variance_factor_weights, ) -assert RiskLabAI.__version__ == "3.0.0" +assert RiskLabAI.__version__ == "3.1.0" covariance = np.diag([1.0, 2.0, 4.0]) @@ -31,7 +34,29 @@ ) assert not check_backdoor_adjustment_set(dag, "T", "Y").admissible assert check_backdoor_adjustment_set(dag, "T", "Y", ("U",)).admissible +role = classify_treatment_outcome_role(dag, "T", "Y", "U") +assert role.role.value == "confounder" + +generalized_coefficient = generalized_confounder_undercontrolled_coefficient( + 2.0, + 3.0, + 4.0, + confounder_variance=5.0, + exposure_noise_variance=6.0, +) +assert generalized_coefficient == 116.0 / 43.0 + +family_errors = max_selection_family_errors( + 0.024997895148220373, + 0.9515427737332771, + 0.95, + 10, +) +assert np.isclose(family_errors.family_type_i_error, 0.22365361940347483) print("minimum-variance weights:", weights) print("average treatment effect:", effect) print("back-door adjustment set: ('U',)") +print("one-hop role for U:", role.role.value) +print("general-variance confounder coefficient:", generalized_coefficient) +print("ten-trial family Type-I error:", family_errors.family_type_i_error) diff --git a/pyproject.toml b/pyproject.toml index 154c63b..8e2d302 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "RiskLabAI" -version = "3.0.0" +version = "3.1.0" description = "Financial machine-learning and causal-factor analysis tools for research and model validation." readme = "README.md" requires-python = ">=3.12,<3.15" diff --git a/src/RiskLabAI/__init__.py b/src/RiskLabAI/__init__.py index 2d2afd3..edb201f 100644 --- a/src/RiskLabAI/__init__.py +++ b/src/RiskLabAI/__init__.py @@ -4,7 +4,7 @@ from . import causal_factor_analysis -__version__ = "3.0.0" +__version__ = "3.1.0" _LEGACY_SUBMODULES = frozenset( { diff --git a/src/RiskLabAI/backtest/__init__.py b/src/RiskLabAI/backtest/__init__.py index 614e601..b8e1f27 100644 --- a/src/RiskLabAI/backtest/__init__.py +++ b/src/RiskLabAI/backtest/__init__.py @@ -117,12 +117,12 @@ # Define what `from RiskLabAI.backtest import *` will import __all__ = [ "validation", - # from advanced_bet_sizing (Appraisal 18 admits) + # from advanced_bet_sizing (independent validation includes) "PlattCalibrator", "distributionally_robust_kelly_fraction", "expected_calibration_error", "kelly_bet_fraction", - # from robust_statistics + ou_trading_rules (Appraisal 22, 23 admits) + # from robust_statistics + ou_trading_rules (independent validation, validated additions) "conditional_expected_drawdown", "sharpe_difference_test", "optimal_ou_trading_rule", diff --git a/src/RiskLabAI/backtest/advanced_bet_sizing.py b/src/RiskLabAI/backtest/advanced_bet_sizing.py index 5172948..1b8719d 100644 --- a/src/RiskLabAI/backtest/advanced_bet_sizing.py +++ b/src/RiskLabAI/backtest/advanced_bet_sizing.py @@ -6,14 +6,14 @@ - **Platt calibration before sizing** (Meyer-Barziy-Joubert 2023). A classifier's raw probability is usually miscalibrated, so sizing straight from it over- or under-bets. Fitting a Platt (logistic) - calibrator on a held-out block and sizing from the calibrated probability recovers near-optimal sizing + calibrator on a validation sample block and sizing from the calibrated probability recovers near-optimal sizing when the input is miscalibrated, and is a safe no-op when it is already calibrated. - **Distributionally-robust Kelly** (Sun-Boyd 2018). Point-estimate Kelly is fragile to estimation error. Sizing at the pessimistic edge of a confidence box on the win probability gives near-optimal growth with materially lower drawdown under estimation uncertainty, and converges to full Kelly as the box collapses with abundant data. -Admitted in Appraisal 18 (CONTRIBUTIONS_LEDGER 2026-06-27). Scope tags, verbatim from the verdict: +Included after independent validation (the public method specification). Scope guidance: Platt calibration: calibrate the meta-label probability before sizing when it is miscalibrated (measurable by ECE); use Platt, not isotonic, which overfits well-calibrated inputs; the benefit is @@ -23,12 +23,12 @@ (small samples, noisy edge): near-optimal growth with lower drawdown; converges to Kelly with abundant data. -Held-out confirmed (appraisals/18_results, HELDOUT.md): on the sealed high-miscalibration corner Platt +validation sample confirmed (the documented validation evidence, independent validation): on the sealed high-miscalibration corner Platt closes the growth gap to the Kelly optimum distinguishably more than the de Prado sigmoid (paired 0.0032 [0.0030, 0.0034]); on the sealed tight-drawdown / small-sample corner DR-Kelly has lower from-initial drawdown than full Kelly (0.144 vs 0.409) with near-optimal growth. Real-data net-of-cost -confirmation for both is a tracked obligation (`REAL_DATA_FOLLOWUPS.md`). Isotonic calibration and -risk-constrained Kelly were NOT admitted (shelved). Evidence and caveats: appraisals/18_verdict.md. +confirmation for both is a future validation work (documented follow-up work). Isotonic calibration and +risk-constrained Kelly were NOT admitted (shelved). Evidence and caveats: the documented validation evidence. The DR-Kelly box-ambiguity worst case has a closed form (Kelly at the pessimistic probability, because a long favorable bet's log-growth is monotone in the win probability), so no convex solver is required here; @@ -124,15 +124,15 @@ def expected_calibration_error(probabilities, outcomes, n_bins: int = 10) -> flo class PlattCalibrator: r""" - Platt scaling: a logistic recalibration of predicted probabilities, fit on a held-out block. + Platt scaling: a logistic recalibration of predicted probabilities, fit on a validation sample block. Fits a logistic regression of the binary outcome on ``logit(p_hat)``; ``transform`` maps a raw probability to the calibrated one. Prefer Platt over isotonic calibration: isotonic overfits the calibration map and hurts already-calibrated inputs, whereas Platt's two-parameter logistic form is a safe near-no-op when the input is already calibrated (the characterized failure mode in the - verdict). Calibrate before sizing (feed the calibrated probability into the de Prado sigmoid or a - Kelly sizer) when ECE indicates miscalibration. See the module docstring for the verbatim scope tag - and appraisals/18_verdict.md. + documented guidance). Calibrate before sizing (feed the calibrated probability into the de Prado sigmoid or a + Kelly sizer) when ECE indicates miscalibration. See the module docstring for the scope guidance + and the documented validation evidence. Examples -------- @@ -191,7 +191,7 @@ def distributionally_robust_kelly_fraction( Prefer DR-Kelly over de Prado sigmoid / full Kelly when estimation uncertainty is material (small samples, noisy edge): near-optimal growth with lower drawdown; converges to Kelly with abundant data. - See the module docstring for the verbatim scope tag and appraisals/18_verdict.md. + See the module docstring for the scope guidance and the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/backtest/multiple_testing.py b/src/RiskLabAI/backtest/multiple_testing.py index 9968f8d..4820e92 100644 --- a/src/RiskLabAI/backtest/multiple_testing.py +++ b/src/RiskLabAI/backtest/multiple_testing.py @@ -9,7 +9,7 @@ single best strategy survives its trial count, while these screen a whole family with a stated error-control target. -Admitted in Appraisal 07 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: To judge a family of screened strategies or factors (not just the single best), prefer Holm when you must control the chance of any false positive across the family (FWER), and BHY when you want @@ -18,7 +18,7 @@ count. Bonferroni is dominated by Holm; the t>3 hurdle over-rejects; the double-bootstrap does not control its stated error as implemented and is not admitted. -Evidence and caveats: appraisals/07_verdict.md. +Evidence and caveats: the documented validation evidence. References ---------- @@ -146,8 +146,8 @@ def haircut_sharpe_ratios( Prefer Holm when you must control the chance of any false positive across the family (FWER), and BHY when you want to bound the expected fraction of false discoveries (FDR) and can accept lower power. The Deflated Sharpe remains the tool when the question is whether one specific best strategy - survives its trial count. See the module docstring for the full regime tag and - appraisals/07_verdict.md. + survives its trial count. See the module docstring for the full usage guidance and + the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/backtest/ou_trading_rules.py b/src/RiskLabAI/backtest/ou_trading_rules.py index 7e25287..fcb4478 100644 --- a/src/RiskLabAI/backtest/ou_trading_rules.py +++ b/src/RiskLabAI/backtest/ou_trading_rules.py @@ -17,18 +17,18 @@ - cost``, and the objective is the expected net return per unit time ``E[gain] / E[tau]`` (the well-posed optimal-trading-rule objective; the raw per-trade Sharpe is degenerate, maximized by profit_take -> 0). -Admitted in Appraisal 23 (CONTRIBUTIONS_LEDGER 2026-06-27). Scope tag, verbatim from the verdict: +Included after independent validation (the public method specification). Scope guidance: prefer the closed-form OU rule over the Monte-Carlo PT/SL grid whenever the OU model is used - it gives the exact optimum with no simulation noise at a fraction of the compute; both are only as good as the OU assumption, so check the OU fit at decision time. -Held-out confirmed (appraisals/23_results, HELDOUT.md): on the sealed OU corner the closed form agrees +validation sample confirmed (the documented validation evidence, independent validation): on the sealed OU corner the closed form agrees with the Monte-Carlo grid optimum within grid resolution (the grid pick's regret on the exact surface stays ~0.0002-0.0008 and tightens as the grid refines) at a compute speed-up that grows with resolution (3.4x -> 16x), and it degrades in step with the grid off-model (no false out-of-model win). The advantage is exactness and speed under the OU model, not robustness to misspecification, so check the OU fit at -decision time. The de Prado Monte-Carlo PT/SL grid is left unchanged. Evidence: appraisals/23_verdict.md. +decision time. The de Prado Monte-Carlo PT/SL grid is left unchanged. Evidence: the documented validation evidence. References ---------- @@ -210,7 +210,7 @@ def optimal_ou_trading_rule( box. Prefer this over the Monte-Carlo PT/SL grid whenever the OU model is used: it gives the exact optimum with no simulation noise at a fraction of the compute. Both are only as good as the OU assumption, so check the OU fit at decision time (see :func:`fit_ornstein_uhlenbeck` and the module - docstring for the verbatim scope tag and appraisals/23_verdict.md). + docstring for the scope guidance and the documented validation evidence). Parameters ---------- diff --git a/src/RiskLabAI/backtest/robust_statistics.py b/src/RiskLabAI/backtest/robust_statistics.py index 89c224b..9f56041 100644 --- a/src/RiskLabAI/backtest/robust_statistics.py +++ b/src/RiskLabAI/backtest/robust_statistics.py @@ -14,7 +14,7 @@ a studentized circular block bootstrap, which holds nominal size under serial dependence where the naive test inflates. -Admitted in Appraisal 22 (CONTRIBUTIONS_LEDGER 2026-06-27). Scope tags, verbatim from the verdict: +Included after independent validation (the public method specification). Scope guidance: CED: prefer CED over max-drawdown as a drawdown-risk statistic: lower estimator variance and better ranking of true drawdown risk, most of all on short/heavy-tailed tracks; converges to max-DD on @@ -24,11 +24,11 @@ serial dependence / heavy tails (it holds nominal size where the naive test inflates ~3x); converges on i.i.d. returns. -Held-out confirmed (appraisals/22_results, HELDOUT.md): on the sealed short-sample x heavy-tail corner CED +validation sample confirmed (the documented validation evidence, independent validation): on the sealed short-sample x heavy-tail corner CED has lower estimator variance (CV 0.30-0.32 vs max-drawdown 0.40-0.42) and better rank-recovery (Spearman 0.42-0.78 vs 0.31-0.69); the LW test holds nominal size (0.056) under AR(0.3) where the naive test rejects 0.161 (~3x), and converges on i.i.d. returns. CDaR@0.95 was NOT admitted (no variance/rank advantage over -max-drawdown as a descriptive statistic). Evidence and caveats: appraisals/22_verdict.md. +max-drawdown as a descriptive statistic). Evidence and caveats: the documented validation evidence. References ---------- @@ -102,7 +102,7 @@ def conditional_expected_drawdown( factor-attributable. Prefer CED over max-drawdown as a drawdown-risk statistic (see the module docstring for the full - verbatim scope tag and appraisals/22_verdict.md). The de Prado max-drawdown / time-under-water + scope guidance and the documented validation evidence). The de Prado max-drawdown / time-under-water statistics are unchanged. Parameters @@ -209,7 +209,7 @@ def sharpe_difference_test( the naive test inflates (~3x under AR), and converges to the naive test on i.i.d. returns. Prefer the Ledoit-Wolf test when comparing two Sharpes under serial dependence / heavy tails (see the - module docstring for the full verbatim scope tag and appraisals/22_verdict.md). The Sharpe ratio uses + module docstring for the full scope guidance and the documented validation evidence). The Sharpe ratio uses the population standard deviation, matching ``backtest_statistics.sharpe_ratio``. Parameters diff --git a/src/RiskLabAI/backtest/sharpe_inference.py b/src/RiskLabAI/backtest/sharpe_inference.py index cdbdbb5..cec3925 100644 --- a/src/RiskLabAI/backtest/sharpe_inference.py +++ b/src/RiskLabAI/backtest/sharpe_inference.py @@ -19,7 +19,7 @@ :math:`1 - S\,\widehat{SR} + \tfrac{K-1}{4}\widehat{SR}^2` (exactly the PSR denominator), so the method converges to the PSR when there is no serial correlation. -Admitted in Appraisal 08 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: Prefer the LPLZ (2025) Sharpe inference - a HAC of the Sharpe influence function - when returns show material serial correlation and/or non-normality (estimable from the sample): it restores @@ -29,13 +29,13 @@ (not fully to) nominal at strong autocorrelation, and wider CIs - the honest power cost. Lo (2002) is the autocorrelation-only intermediate, dominated by LPLZ under non-normality. -Evidence and caveats: appraisals/08_verdict.md. +Evidence and caveats: the documented validation evidence. References ---------- Lopez de Prado, M., Lipton, A. and Zoonekynd, V. (2025) Sharpe-ratio inference under non-normal, - serially-correlated returns. (Open code at github.com/zoonek/2025-sharpe-ratio - NOT consulted or - vendored; this is reimplemented clean-room from the influence-function / HAC math, GOVERNANCE 3.) + serially-correlated returns. This implementation follows the cited influence-function and HAC + formulas. Newey, W. K. and West, K. D. (1987) A simple, positive semi-definite, heteroskedasticity and autocorrelation consistent covariance matrix. Econometrica, 55(3), 703-708. Lo, A. W. (2002) The statistics of Sharpe ratios. Financial Analysts Journal, 58(4), 36-52. @@ -135,7 +135,7 @@ def lplz_sharpe_inference( (estimable from the sample): it restores near-nominal CI coverage and test size where the PSR under-covers and over-rejects, and converges to the PSR on near-normal i.i.d. returns with no over-coverage. The honest cost is a wider CI under autocorrelation. See the module docstring for the - full regime tag and appraisals/08_verdict.md. + full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/backtest/validation/__init__.py b/src/RiskLabAI/backtest/validation/__init__.py index 385b4a4..abc97fb 100644 --- a/src/RiskLabAI/backtest/validation/__init__.py +++ b/src/RiskLabAI/backtest/validation/__init__.py @@ -40,13 +40,13 @@ "CombinatorialPurged", # <-- Fix "BaggedCombinatorialPurged", # <-- Fix "AdaptiveCombinatorialPurged", # <-- Fix - # Path-level bagged CPCV PBO (Appraisal 09) + # Path-level bagged CPCV PBO (independent validation) "bagged_probability_of_backtest_overfitting", "moving_block_bootstrap_indices", - # Path-level adaptive (regime-weighted) CPCV PBO (Appraisal 09b) + # Path-level adaptive (regime-weighted) CPCV PBO (independent validation) "adaptive_probability_of_backtest_overfitting", "estimate_volatility_regimes", - # Leakage-aware HPO methodology (Appraisal 20) + # Leakage-aware HPO methodology (independent validation) "leakage_aware_hpo", "deflated_sharpe_gate", # Utilities diff --git a/src/RiskLabAI/backtest/validation/leakage_aware_hpo.py b/src/RiskLabAI/backtest/validation/leakage_aware_hpo.py index e299bd6..7ce9f19 100644 --- a/src/RiskLabAI/backtest/validation/leakage_aware_hpo.py +++ b/src/RiskLabAI/backtest/validation/leakage_aware_hpo.py @@ -11,8 +11,8 @@ per-trial score is leakage-controlled) and provides a Deflated-Sharpe gate that deflates the selected model's out-of-sample Sharpe by the HPO-inclusive trial count. -Admitted in Appraisal 20 (CONTRIBUTIONS_LEDGER 2026-06-27) as a methodology / infrastructure admit, NOT a -performance claim. Scope tag, verbatim from the verdict: +Included after independent validation (the public method specification) as a methodology / infrastructure admit, NOT a +performance claim. Scope guidance: Optuna (TPE/CMA-ES) wired through PurgedKFold/CPCV with PBO/DSR gating, preferred over grid/random for search efficiency and over naive-CV tuning for leakage-safety, and carrying the honest, characterized @@ -20,11 +20,11 @@ the trial count, never trusted on the in-sample HPO score. **Plainly: this improves search efficiency and leakage-safety, NOT out-of-sample performance. Tuning does -not create edge.** In the appraisal the principled-HPO real-leg gain did not survive the Deflated Sharpe at +not create edge.** during independent validation the principled-HPO real-leg gain did not survive the Deflated Sharpe at the HPO-inclusive trial count (DSR 0.44 / 0.34 < 0.5, PBO 0.67 / 0.69, OOS at the base rate); the value is that the search reaches the optimum in fewer trials and that purged CV removes the leakage a naive k-fold inflates. Always gate the selected model by :func:`deflated_sharpe_gate` at the HPO trial count; never -trust the in-sample HPO score. Evidence and caveats: appraisals/20_verdict.md. +trust the in-sample HPO score. Evidence and caveats: the documented validation evidence. References ---------- @@ -87,7 +87,7 @@ def leakage_aware_hpo( and LEAKAGE-SAFETY (the per-trial score is purged, not a leaky shuffled k-fold). It does NOT improve out-of-sample performance - tuning does not create edge - so the selected model must be gated by :func:`deflated_sharpe_gate` at the returned ``n_trials``; never trust ``best_score`` directly. See the - module docstring for the verbatim scope tag and appraisals/20_verdict.md. + module docstring for the scope guidance and the documented validation evidence. Parameters ---------- @@ -186,9 +186,9 @@ def deflated_sharpe_gate( The benchmark is the expected maximum Sharpe over ``n_trials`` independent trials (with the observed cross-trial Sharpe dispersion ``trial_sharpe_std``); the Deflated Sharpe is the probability that the realized OOS Sharpe exceeds that benchmark. A selection passes only if the Deflated Sharpe exceeds - ``threshold`` (0.5). This is the decisive control: in the appraisal the principled-HPO gain did not + ``threshold`` (0.5). This is the decisive control: during independent validation the principled-HPO gain did not pass it - tuning yielded no out-of-sample edge after deflation. See the module docstring for the - verbatim scope tag and appraisals/20_verdict.md. + scope guidance and the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/backtest/validation/path_adaptive_cpcv.py b/src/RiskLabAI/backtest/validation/path_adaptive_cpcv.py index bdf8460..812ba9a 100644 --- a/src/RiskLabAI/backtest/validation/path_adaptive_cpcv.py +++ b/src/RiskLabAI/backtest/validation/path_adaptive_cpcv.py @@ -10,7 +10,7 @@ selection is signal-bearing, and the weights collapse to uniform in a stationary single regime, so the estimate converges to plain CPCV with no over-adaptation. -This is the **path-level** mechanism from the Appraisal 09 pre-registration. It is distinct from, and +This is the **path-level** mechanism from the independent validation protocol. It is distinct from, and does NOT touch, the existing :class:`AdaptiveCombinatorialPurged`, which adapts the split *boundaries* of an ML backtest (a different mechanism). The plain-CPCV baseline here is the repo's CSCV PBO: the per-path overfit indicator is taken from @@ -19,9 +19,8 @@ :func:`~RiskLabAI.backtest.probability_of_backtest_overfitting.probability_of_backtest_overfitting` exactly. -Admitted in Appraisal 09b (CONTRIBUTIONS_LEDGER 2026-06-27; in-house method under a conflict of -interest, held to the identical bar, admitted only after a re-designed signal-bearing held-out). -Regime tag, verbatim from the 09b verdict: +This method extension was independently validated on a signal-bearing sample. +Usage guidance: prefer Adaptive CPCV over plain CPCV for model selection when the train/test regime may shift, the shift is identifiable at decision time from observable volatility, and there is adequate data for @@ -30,13 +29,13 @@ The advantage is, as of admission, Monte-Carlo evidence where the regime is observable through the adaptive feature; a real-data regime-shift confirmation on a genuine crisis/volatility period is a -tracked obligation (``REAL_DATA_FOLLOWUPS.md``). Evidence and caveats: appraisals/09_verdict.md (the -09b re-appraisal). +future validation work (`documented follow-up work`). Evidence and caveats: the documented validation evidence (the +follow-up validation). References ---------- Arian, H., Norouzi M., L. and Seco, L. (2024) Bagged and Adaptive Combinatorial Purged - Cross-Validation. (Clean-room from the path-level mechanism; the verdict harness validated plain + Cross-Validation. (Clean-room from the path-level mechanism; the validation harness validated plain CPCV against probability_of_backtest_overfitting exactly.) Bailey, D. H., Borwein, J., Lopez de Prado, M. and Zhu, Q. J. (2017) The probability of backtest overfitting. Journal of Computational Finance, 20(4), 39-69. @@ -146,7 +145,7 @@ def adaptive_probability_of_backtest_overfitting( :func:`estimate_volatility_regimes`). The regime-weighted PBO and the configuration with the highest regime-weighted out-of-sample metric are returned. In a stationary regime the weights are uniform, so the PBO converges to plain CPCV and the selection matches it; see the module docstring for the - full regime tag and appraisals/09_verdict.md. + full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/backtest/validation/path_bagged_cpcv.py b/src/RiskLabAI/backtest/validation/path_bagged_cpcv.py index ab15b2c..7b72317 100644 --- a/src/RiskLabAI/backtest/validation/path_bagged_cpcv.py +++ b/src/RiskLabAI/backtest/validation/path_bagged_cpcv.py @@ -7,25 +7,25 @@ averaging - reduces that variance, giving a more accurate PBO estimate whenever the path set is small or noisy, and converging to plain CPCV in the data-rich limit. -This is the **path-level** mechanism from the Appraisal 09 pre-registration. It is distinct from, and +This is the **path-level** mechanism from the independent validation protocol. It is distinct from, and does NOT touch, the existing :class:`BaggedCombinatorialPurged`, which bags a sklearn *estimator* on an ML backtest (a different mechanism). The plain-CPCV baseline here is the repo's CSCV PBO (:func:`~RiskLabAI.backtest.probability_of_backtest_overfitting.probability_of_backtest_overfitting`), which this function calls on each bootstrap resample. -Admitted in Appraisal 09 (CONTRIBUTIONS_LEDGER 2026-06-27; in-house method, held to the identical bar). -Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification; method extension validated independently). +Usage guidance: For a more accurate, lower-variance overfitting (PBO) estimate whenever the CPCV path set is small or noisy, converging to plain CPCV in the data-rich limit; neutral on model selection. Point-in-time and mechanism-backed. -Evidence and caveats: appraisals/09_verdict.md. +Evidence and caveats: the documented validation evidence. References ---------- Arian, H., Norouzi M., L. and Seco, L. (2024) Bagged and Adaptive Combinatorial Purged - Cross-Validation. (Clean-room from the path-level mechanism; the verdict harness validated plain + Cross-Validation. (Clean-room from the path-level mechanism; the validation harness validated plain CPCV against probability_of_backtest_overfitting exactly.) Bailey, D. H., Borwein, J., Lopez de Prado, M. and Zhu, Q. J. (2017) The probability of backtest overfitting. Journal of Computational Finance, 20(4), 39-69. @@ -88,7 +88,7 @@ def bagged_probability_of_backtest_overfitting( (:func:`probability_of_backtest_overfitting`) on each, and returns the average - a lower-variance estimate than a single CPCV PBO. Prefer it whenever the CPCV path set is small or noisy; it converges to plain CPCV in the data-rich limit and is neutral on model selection. See the module - docstring for the full regime tag and appraisals/09_verdict.md. + docstring for the full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/causal_factor_analysis/__init__.py b/src/RiskLabAI/causal_factor_analysis/__init__.py index e2b800e..28a336b 100644 --- a/src/RiskLabAI/causal_factor_analysis/__init__.py +++ b/src/RiskLabAI/causal_factor_analysis/__init__.py @@ -6,6 +6,11 @@ namespaces. """ +from .allocation_diagnostics import ( + AllocationMisspecificationDiagnostics, + allocation_misspecification_diagnostics, +) + from .factor_mirage import ( ColliderCoefficients, ColliderDiagnostics, @@ -17,6 +22,8 @@ confounder_factor_return, confounder_forecast_return, confounder_undercontrolled_coefficient, + generalized_collider_overcontrolled_coefficients, + generalized_confounder_undercontrolled_coefficient, ) from .graph_identification import ( BackdoorAdjustmentEvidence, @@ -34,6 +41,13 @@ minimal_backdoor_adjustment_sets, minimal_frontdoor_adjustment_sets, ) +from .graph_roles import ( + FactorControlRoles, + TreatmentOutcomeRole, + TreatmentOutcomeRoleEvidence, + classify_treatment_outcome_role, + factor_control_roles, +) from .optimizer import minimum_variance_factor_weights from .protocol import ( BacktestStage, @@ -61,6 +75,31 @@ fork_population_diagnostics, fork_specification_experiment, ) +from .search_adjusted_fdr import ( + FDRComparisonEvidence, + FDRNonIdentificationWitness, + GaussianSearchAdjustedFDR, + GaussianTrialMixture, + MaxSelectionFamilyErrors, + SelectionLevelProbabilityEvidence, + compare_single_and_family_fdr, + conditional_upper_tail_probability, + family_level_false_discovery_rate, + fdr_nonidentification_witness, + gaussian_max_selection_cdf, + gaussian_max_selection_log_density, + gaussian_max_selection_log_likelihood, + gaussian_search_adjusted_false_discovery_rate, + gaussian_trial_mixture_cdf, + max_selection_family_errors, + max_selection_null_probability, + maximum_mixture_cdf, + single_trial_false_discovery_rate, +) +from .structural_models import ( + StructuralCausalModelResult, + evaluate_structural_causal_model, +) from .treatment_effects import ( DifferenceInDifferencesEstimate, TreatmentEffectDecomposition, @@ -132,4 +171,34 @@ "randomized_mean_difference", "treatment_effect_decomposition", "validate_causal_factor_protocol", + "AllocationMisspecificationDiagnostics", + "FDRComparisonEvidence", + "FDRNonIdentificationWitness", + "FactorControlRoles", + "GaussianSearchAdjustedFDR", + "GaussianTrialMixture", + "MaxSelectionFamilyErrors", + "SelectionLevelProbabilityEvidence", + "StructuralCausalModelResult", + "TreatmentOutcomeRole", + "TreatmentOutcomeRoleEvidence", + "allocation_misspecification_diagnostics", + "classify_treatment_outcome_role", + "compare_single_and_family_fdr", + "conditional_upper_tail_probability", + "evaluate_structural_causal_model", + "factor_control_roles", + "family_level_false_discovery_rate", + "fdr_nonidentification_witness", + "gaussian_max_selection_cdf", + "gaussian_max_selection_log_density", + "gaussian_max_selection_log_likelihood", + "gaussian_search_adjusted_false_discovery_rate", + "gaussian_trial_mixture_cdf", + "generalized_collider_overcontrolled_coefficients", + "generalized_confounder_undercontrolled_coefficient", + "max_selection_family_errors", + "max_selection_null_probability", + "maximum_mixture_cdf", + "single_trial_false_discovery_rate", ] diff --git a/src/RiskLabAI/causal_factor_analysis/allocation_diagnostics.py b/src/RiskLabAI/causal_factor_analysis/allocation_diagnostics.py new file mode 100644 index 0000000..389e5fe --- /dev/null +++ b/src/RiskLabAI/causal_factor_analysis/allocation_diagnostics.py @@ -0,0 +1,286 @@ +r"""Diagnostics for allocations built from a misspecified exposure system. + +The reference and misspecified portfolios each solve the released +minimum-variance equality-constrained problem. The diagnostic then evaluates +the misspecified weights against the reference exposure matrix, which makes +risk-attribution errors explicit without assuming how either exposure system +was estimated. + +This generic comparison implements the unambiguous allocation procedure in +López de Prado, Lipton, and Zoonekynd (2025). It does not instantiate either +paper-specific collider appendix because those printed coefficient systems are +mutually inconsistent. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +from .optimizer import minimum_variance_factor_weights + +__all__ = [ + "AllocationMisspecificationDiagnostics", + "allocation_misspecification_diagnostics", +] + + +def _as_finite_real_array(values: ArrayLike, name: str) -> NDArray[np.float64]: + """Return an independent finite real ``float64`` array.""" + try: + raw = np.asarray(values) + except (TypeError, ValueError, OverflowError) as exc: + raise ValueError(f"{name} must be a numeric array") from exc + if raw.dtype.kind == "b" or ( + raw.dtype.kind == "O" + and any(isinstance(value, (bool, np.bool_)) for value in raw.flat) + ): + raise ValueError(f"{name} must not contain boolean values") + if np.iscomplexobj(raw): + raise ValueError(f"{name} must be real-valued") + try: + result = np.array(values, dtype=np.float64, copy=True) + except (TypeError, ValueError, OverflowError) as exc: + raise ValueError(f"{name} must be a numeric array") from exc + if not np.all(np.isfinite(result)): + raise ValueError(f"{name} must contain only finite values") + return result + + +def _readonly_float_vector(values: ArrayLike, name: str) -> NDArray[np.float64]: + result = _as_finite_real_array(values, name) + if result.ndim != 1: + raise ValueError(f"{name} must be a one-dimensional vector") + result.setflags(write=False) + return result + + +def _readonly_bool_vector(values: ArrayLike, name: str) -> NDArray[np.bool_]: + try: + result = np.array(values, dtype=np.bool_, copy=True) + except (TypeError, ValueError, OverflowError) as exc: + raise ValueError(f"{name} must be a boolean vector") from exc + if result.ndim != 1: + raise ValueError(f"{name} must be a one-dimensional vector") + result.setflags(write=False) + return result + + +def _as_nonnegative_finite_scalar(value: float, name: str) -> float: + if isinstance(value, bool): + raise ValueError(f"{name} must be a nonnegative finite scalar") + try: + result = float(value) + except (TypeError, ValueError, OverflowError) as exc: + raise ValueError(f"{name} must be a nonnegative finite scalar") from exc + if not math.isfinite(result) or result < 0.0: + raise ValueError(f"{name} must be a nonnegative finite scalar") + return result + + +@dataclass(frozen=True, slots=True, eq=False) +class AllocationMisspecificationDiagnostics: + """Immutable evidence comparing reference and misspecified allocations. + + Every array is an independent, read-only snapshot. The reference exposure + error is the exposure produced by the misspecified weights in the reference + system minus the requested reference target. + """ + + reference_weights: NDArray[np.float64] + misspecified_weights: NDArray[np.float64] + reference_target_exposures: NDArray[np.float64] + reference_achieved_exposures: NDArray[np.float64] + misspecified_reference_exposures: NDArray[np.float64] + misspecified_model_target_exposures: NDArray[np.float64] + misspecified_model_achieved_exposures: NDArray[np.float64] + reference_exposure_error: NDArray[np.float64] + weight_sign_reversals: NDArray[np.bool_] + reference_variance: float + misspecified_variance: float + + def __post_init__(self) -> None: + float_vectors = ( + "reference_weights", + "misspecified_weights", + "reference_target_exposures", + "reference_achieved_exposures", + "misspecified_reference_exposures", + "misspecified_model_target_exposures", + "misspecified_model_achieved_exposures", + "reference_exposure_error", + ) + for field_name in float_vectors: + object.__setattr__( + self, + field_name, + _readonly_float_vector(getattr(self, field_name), field_name), + ) + object.__setattr__( + self, + "weight_sign_reversals", + _readonly_bool_vector(self.weight_sign_reversals, "weight_sign_reversals"), + ) + object.__setattr__( + self, + "reference_variance", + _as_nonnegative_finite_scalar( + self.reference_variance, "reference_variance" + ), + ) + object.__setattr__( + self, + "misspecified_variance", + _as_nonnegative_finite_scalar( + self.misspecified_variance, "misspecified_variance" + ), + ) + + +def _validate_shapes( + covariance: NDArray[np.float64], + reference_factor_exposures: NDArray[np.float64], + reference_target_exposures: NDArray[np.float64], + misspecified_factor_exposures: NDArray[np.float64], + misspecified_target_exposures: NDArray[np.float64], +) -> None: + if covariance.ndim != 2 or covariance.shape[0] == 0: + raise ValueError("covariance must be a non-empty 2-D square matrix") + n_assets, covariance_columns = covariance.shape + if covariance_columns != n_assets: + raise ValueError("covariance must be a non-empty 2-D square matrix") + for name, exposures, targets in ( + ( + "reference", + reference_factor_exposures, + reference_target_exposures, + ), + ( + "misspecified", + misspecified_factor_exposures, + misspecified_target_exposures, + ), + ): + if exposures.ndim != 2: + raise ValueError(f"{name}_factor_exposures must be a 2-D matrix") + if exposures.shape[0] != n_assets: + raise ValueError( + f"{name}_factor_exposures must have the same number of rows " + "as covariance" + ) + n_factors = exposures.shape[1] + if n_factors == 0: + raise ValueError( + f"{name}_factor_exposures must contain at least one factor column" + ) + if n_factors > n_assets: + raise ValueError( + f"{name}_factor_exposures cannot have more columns than rows" + ) + if targets.ndim != 1: + raise ValueError(f"{name}_target_exposures must be a 1-D vector") + if targets.shape[0] != n_factors: + raise ValueError( + f"{name}_target_exposures length must equal the number of " + f"{name} factor columns" + ) + + +def _portfolio_variance( + covariance: NDArray[np.float64], + weights: NDArray[np.float64], + name: str, +) -> float: + with np.errstate(over="ignore", invalid="ignore"): + result = float(weights @ covariance @ weights) + if not math.isfinite(result) or result < 0.0: + raise ValueError(f"{name} cannot be represented as a nonnegative finite float") + return result + + +def allocation_misspecification_diagnostics( + covariance: ArrayLike, + reference_factor_exposures: ArrayLike, + reference_target_exposures: ArrayLike, + misspecified_factor_exposures: ArrayLike, + misspecified_target_exposures: ArrayLike, +) -> AllocationMisspecificationDiagnostics: + r"""Compare constrained allocations from two exposure systems. + + ``reference_factor_exposures`` and ``misspecified_factor_exposures`` must + have the same number of asset rows, but they may contain different numbers + of factor columns. Each allocation is solved against its corresponding + target. The misspecified weights are then evaluated against the reference + exposure matrix and covariance. + + A weight is marked as sign-reversed exactly when its reference and + misspecified values have a strictly negative product; zero weights are not + reversals. + """ + + covariance_array = _as_finite_real_array(covariance, "covariance") + reference_exposure_array = _as_finite_real_array( + reference_factor_exposures, "reference_factor_exposures" + ) + reference_target_array = _as_finite_real_array( + reference_target_exposures, "reference_target_exposures" + ) + misspecified_exposure_array = _as_finite_real_array( + misspecified_factor_exposures, "misspecified_factor_exposures" + ) + misspecified_target_array = _as_finite_real_array( + misspecified_target_exposures, "misspecified_target_exposures" + ) + _validate_shapes( + covariance_array, + reference_exposure_array, + reference_target_array, + misspecified_exposure_array, + misspecified_target_array, + ) + + reference_weights = minimum_variance_factor_weights( + covariance_array, reference_exposure_array, reference_target_array + ) + misspecified_weights = minimum_variance_factor_weights( + covariance_array, misspecified_exposure_array, misspecified_target_array + ) + + with np.errstate(over="ignore", invalid="ignore"): + reference_achieved_exposures = reference_exposure_array.T @ reference_weights + misspecified_reference_exposures = ( + reference_exposure_array.T @ misspecified_weights + ) + misspecified_model_achieved_exposures = ( + misspecified_exposure_array.T @ misspecified_weights + ) + reference_exposure_error = ( + misspecified_reference_exposures - reference_target_array + ) + + weight_sign_reversals = ( + (reference_weights != 0.0) + & (misspecified_weights != 0.0) + & (np.signbit(reference_weights) != np.signbit(misspecified_weights)) + ) + + return AllocationMisspecificationDiagnostics( + reference_weights=reference_weights, + misspecified_weights=misspecified_weights, + reference_target_exposures=reference_target_array, + reference_achieved_exposures=reference_achieved_exposures, + misspecified_reference_exposures=misspecified_reference_exposures, + misspecified_model_target_exposures=misspecified_target_array, + misspecified_model_achieved_exposures=(misspecified_model_achieved_exposures), + reference_exposure_error=reference_exposure_error, + weight_sign_reversals=weight_sign_reversals, + reference_variance=_portfolio_variance( + covariance_array, reference_weights, "reference_variance" + ), + misspecified_variance=_portfolio_variance( + covariance_array, misspecified_weights, "misspecified_variance" + ), + ) diff --git a/src/RiskLabAI/causal_factor_analysis/factor_mirage.py b/src/RiskLabAI/causal_factor_analysis/factor_mirage.py index 08ddade..fba052c 100644 --- a/src/RiskLabAI/causal_factor_analysis/factor_mirage.py +++ b/src/RiskLabAI/causal_factor_analysis/factor_mirage.py @@ -5,6 +5,10 @@ returns condition on the supplied exposures. Forecast returns integrate over the exposure distribution assumed by the article. +The generalized coefficient functions retain the disturbance variances from +the population normal equations. The released standardized functions remain +the unit-variance special cases with unchanged signatures. + Appendix F is not exposed as a separate parameter-shift model because its equations contain only the training-regime parameters. """ @@ -29,6 +33,8 @@ "confounder_factor_return", "confounder_forecast_return", "confounder_undercontrolled_coefficient", + "generalized_collider_overcontrolled_coefficients", + "generalized_confounder_undercontrolled_coefficient", ] @@ -98,6 +104,13 @@ def _fraction(value: float) -> Fraction: return Fraction.from_float(value) +def _positive_fraction(name: str, value: Real) -> Fraction: + result = _as_finite_real(name, value) + if result <= 0.0: + raise ValueError(f"{name} must be strictly positive") + return _fraction(result) + + def _as_result(name: str, value: Fraction) -> float: try: result = float(value) @@ -161,6 +174,44 @@ def confounder_undercontrolled_coefficient( ) +def generalized_confounder_undercontrolled_coefficient( + beta: Real, + gamma: Real, + delta: Real, + *, + confounder_variance: Real, + exposure_noise_variance: Real, +) -> float: + r"""Return the omitted-confounder coefficient for general variances. + + For ``X = delta * Z + v`` and ``Y = beta * X + gamma * Z + u``, with + independent centered disturbances, the population coefficient is + + ``beta + delta * gamma * Var(Z) / (delta**2 * Var(Z) + Var(v))``. + + Both variances must be strictly positive. Setting both to one reproduces + :func:`confounder_undercontrolled_coefficient` exactly. + """ + + beta_value = _fraction(_as_finite_real("beta", beta)) + gamma_value = _fraction(_as_finite_real("gamma", gamma)) + delta_value = _fraction(_as_finite_real("delta", delta)) + confounder_variance_value = _positive_fraction( + "confounder_variance", confounder_variance + ) + exposure_noise_variance_value = _positive_fraction( + "exposure_noise_variance", exposure_noise_variance + ) + denominator = ( + delta_value * delta_value * confounder_variance_value + + exposure_noise_variance_value + ) + coefficient = beta_value + ( + delta_value * gamma_value * confounder_variance_value / denominator + ) + return _as_result("generalized undercontrolled coefficient", coefficient) + + def confounder_factor_return( x: Real, z: Real, @@ -254,6 +305,51 @@ def collider_overcontrolled_coefficients( ) +def generalized_collider_overcontrolled_coefficients( + beta: Real, + gamma: Real, + delta: Real, + *, + outcome_noise_variance: Real, + collider_noise_variance: Real, +) -> ColliderCoefficients: + r"""Return collider-conditioned coefficients for general variances. + + For ``Y = beta * X + u`` and ``Z = gamma * Y + delta * X + v``, with + independent centered disturbances, the population coefficients are + + ``beta_hat = (beta * Var(v) - delta * gamma * Var(u)) / + (Var(v) + gamma**2 * Var(u))`` and + ``theta_hat = gamma * Var(u) / (Var(v) + gamma**2 * Var(u))``. + + Both variances must be strictly positive. Setting both to one reproduces + :func:`collider_overcontrolled_coefficients` exactly. + """ + + beta_value = _fraction(_as_finite_real("beta", beta)) + gamma_value = _fraction(_as_finite_real("gamma", gamma)) + delta_value = _fraction(_as_finite_real("delta", delta)) + outcome_noise_variance_value = _positive_fraction( + "outcome_noise_variance", outcome_noise_variance + ) + collider_noise_variance_value = _positive_fraction( + "collider_noise_variance", collider_noise_variance + ) + denominator = ( + collider_noise_variance_value + + gamma_value * gamma_value * outcome_noise_variance_value + ) + beta_hat = ( + beta_value * collider_noise_variance_value + - delta_value * gamma_value * outcome_noise_variance_value + ) / denominator + theta_hat = gamma_value * outcome_noise_variance_value / denominator + return ColliderCoefficients( + beta_hat=_as_result("generalized overcontrolled beta coefficient", beta_hat), + theta_hat=_as_result("generalized collider coefficient", theta_hat), + ) + + def collider_factor_return( x: Real, collider_proxy: Real, diff --git a/src/RiskLabAI/causal_factor_analysis/graph_roles.py b/src/RiskLabAI/causal_factor_analysis/graph_roles.py new file mode 100644 index 0000000..8dd8184 --- /dev/null +++ b/src/RiskLabAI/causal_factor_analysis/graph_roles.py @@ -0,0 +1,186 @@ +"""Accepted-DAG factor roles and one-hop treatment/outcome roles. + +These routines classify implications of a supplied :class:`CausalDAG`. They +do not discover a graph or assert that every ancestor is a sufficient or +minimal adjustment set. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + +from .graph_identification import CausalDAG + +__all__ = [ + "FactorControlRoles", + "TreatmentOutcomeRole", + "TreatmentOutcomeRoleEvidence", + "classify_treatment_outcome_role", + "factor_control_roles", +] + + +@dataclass(frozen=True, slots=True) +class FactorControlRoles: + """Reachability roles for factors relative to one observed target.""" + + target_factor: str + ancestor_factors: tuple[str, ...] + descendant_factors: tuple[str, ...] + other_factors: tuple[str, ...] + unobserved_ancestors: tuple[str, ...] + unobserved_descendants: tuple[str, ...] + + +class TreatmentOutcomeRole(Enum): + """The eight admitted one-hop treatment/outcome roles.""" + + CAUSE_OF_TREATMENT = "cause_of_treatment" + CONSEQUENCE_OF_TREATMENT = "consequence_of_treatment" + CAUSE_OF_OUTCOME = "cause_of_outcome" + CONSEQUENCE_OF_OUTCOME = "consequence_of_outcome" + CONFOUNDER = "confounder" + COLLIDER = "collider" + MEDIATOR = "mediator" + INDEPENDENT = "independent" + + +@dataclass(frozen=True, slots=True) +class TreatmentOutcomeRoleEvidence: + """Direct-edge evidence for one admitted treatment/outcome role.""" + + node: str + treatment: str + outcome: str + role: TreatmentOutcomeRole + node_to_treatment: bool + treatment_to_node: bool + node_to_outcome: bool + outcome_to_node: bool + + +_ROLE_BY_SIGNATURE = { + (True, False, False, False): TreatmentOutcomeRole.CAUSE_OF_TREATMENT, + (False, True, False, False): TreatmentOutcomeRole.CONSEQUENCE_OF_TREATMENT, + (False, False, True, False): TreatmentOutcomeRole.CAUSE_OF_OUTCOME, + (False, False, False, True): TreatmentOutcomeRole.CONSEQUENCE_OF_OUTCOME, + (True, False, True, False): TreatmentOutcomeRole.CONFOUNDER, + (False, True, False, True): TreatmentOutcomeRole.COLLIDER, + (False, True, True, False): TreatmentOutcomeRole.MEDIATOR, + (False, False, False, False): TreatmentOutcomeRole.INDEPENDENT, +} + + +def _validate_dag(dag: CausalDAG) -> CausalDAG: + if not isinstance(dag, CausalDAG): + raise TypeError("dag must be a CausalDAG") + return dag + + +def _require_observed_node(dag: CausalDAG, value: str, subject: str) -> str: + if not isinstance(value, str): + raise TypeError(f"{subject} must be a string node identifier") + if not value.strip(): + raise ValueError(f"{subject} must be nonblank") + if value not in dag.nodes: + raise ValueError(f"{subject} must be a known graph node") + if value not in dag.observed_nodes: + raise ValueError(f"{subject} must be an observed graph node") + return value + + +def _adjacency(dag: CausalDAG, *, reverse: bool) -> dict[str, set[str]]: + adjacency = {node: set() for node in dag.nodes} + for source, target in dag.directed_edges: + left, right = (target, source) if reverse else (source, target) + adjacency[left].add(right) + return adjacency + + +def _reachable(adjacency: dict[str, set[str]], start: str) -> set[str]: + visited: set[str] = set() + pending = list(adjacency[start]) + while pending: + node = pending.pop() + if node in visited: + continue + visited.add(node) + pending.extend(adjacency[node] - visited) + return visited + + +def factor_control_roles(dag: CausalDAG, target_factor: str) -> FactorControlRoles: + """Partition factors by reachability in an accepted DAG. + + Observed non-target nodes are partitioned into ancestors, descendants, and + all remaining factors. Latent ancestors and descendants are reported + separately as evidence that may limit adjustment or interpretation. + """ + + dag = _validate_dag(dag) + target_factor = _require_observed_node(dag, target_factor, "target_factor") + + ancestors = _reachable(_adjacency(dag, reverse=True), target_factor) + descendants = _reachable(_adjacency(dag, reverse=False), target_factor) + observed_candidates = set(dag.observed_nodes) - {target_factor} + unobserved = set(dag.nodes) - set(dag.observed_nodes) + + ancestor_factors = observed_candidates & ancestors + descendant_factors = observed_candidates & descendants + other_factors = observed_candidates - ancestor_factors - descendant_factors + + return FactorControlRoles( + target_factor=target_factor, + ancestor_factors=tuple(sorted(ancestor_factors)), + descendant_factors=tuple(sorted(descendant_factors)), + other_factors=tuple(sorted(other_factors)), + unobserved_ancestors=tuple(sorted(unobserved & ancestors)), + unobserved_descendants=tuple(sorted(unobserved & descendants)), + ) + + +def classify_treatment_outcome_role( + dag: CausalDAG, + treatment: str, + outcome: str, + node: str, +) -> TreatmentOutcomeRoleEvidence: + """Classify one observed node using the frozen four-edge signature. + + Only the eight published one-hop signatures are admitted. Reverse + mediation and every other signature fail closed with ``ValueError``. + """ + + dag = _validate_dag(dag) + treatment = _require_observed_node(dag, treatment, "treatment") + outcome = _require_observed_node(dag, outcome, "outcome") + node = _require_observed_node(dag, node, "node") + if len({treatment, outcome, node}) != 3: + raise ValueError("treatment, outcome, and node must be distinct") + + edges = set(dag.directed_edges) + signature = ( + (node, treatment) in edges, + (treatment, node) in edges, + (node, outcome) in edges, + (outcome, node) in edges, + ) + try: + role = _ROLE_BY_SIGNATURE[signature] + except KeyError as exc: + raise ValueError( + "the direct treatment/outcome edge signature is not one of the " + "eight admitted roles" + ) from exc + + return TreatmentOutcomeRoleEvidence( + node=node, + treatment=treatment, + outcome=outcome, + role=role, + node_to_treatment=signature[0], + treatment_to_node=signature[1], + node_to_outcome=signature[2], + outcome_to_node=signature[3], + ) diff --git a/src/RiskLabAI/causal_factor_analysis/search_adjusted_fdr.py b/src/RiskLabAI/causal_factor_analysis/search_adjusted_fdr.py new file mode 100644 index 0000000..ac65181 --- /dev/null +++ b/src/RiskLabAI/causal_factor_analysis/search_adjusted_fdr.py @@ -0,0 +1,763 @@ +"""False-discovery probabilities under latent specification search. + +This module implements the identified analytical results in Lopez de Prado +and Fabozzi (2026). Family-level false discovery treats the whole searched +family as null only when every candidate is null. Selection-level false +discovery instead asks whether the selected maximum itself came from a null +candidate. The two estimands are intentionally exposed by different +functions and result records. + +The empirical parameter-fitting procedure is not included because its data, +optimizer, initialization, and model-selection contract are not fully +specified by the source. The likelihood and search-adjustment mappings are +complete without inventing those missing choices. +""" + +from __future__ import annotations + +import math +import warnings +from dataclasses import dataclass +from numbers import Integral, Real +from typing import Callable + +import numpy as np +from numpy.typing import ArrayLike, NDArray +from scipy.integrate import IntegrationWarning, quad +from scipy.special import log_ndtr, logsumexp, ndtr + +__all__ = [ + "FDRComparisonEvidence", + "FDRNonIdentificationWitness", + "GaussianSearchAdjustedFDR", + "GaussianTrialMixture", + "MaxSelectionFamilyErrors", + "SelectionLevelProbabilityEvidence", + "compare_single_and_family_fdr", + "conditional_upper_tail_probability", + "family_level_false_discovery_rate", + "fdr_nonidentification_witness", + "gaussian_max_selection_cdf", + "gaussian_max_selection_log_density", + "gaussian_max_selection_log_likelihood", + "gaussian_search_adjusted_false_discovery_rate", + "gaussian_trial_mixture_cdf", + "max_selection_family_errors", + "max_selection_null_probability", + "maximum_mixture_cdf", + "single_trial_false_discovery_rate", +] + + +@dataclass(frozen=True, slots=True) +class MaxSelectionFamilyErrors: + """Family error probabilities induced by selecting a maximum.""" + + n_trials: int + trial_null_probability: float + trial_type_i_error: float + trial_type_ii_error: float + family_null_probability: float + family_type_i_error: float + family_type_ii_error: float + + +@dataclass(frozen=True, slots=True) +class FDRComparisonEvidence: + """Log-domain evidence for the source's identification conditions.""" + + n_trials: int + single_trial_false_discovery_rate: float + family_level_false_discovery_rate: float + log_observed_type_i_inflation: float + log_required_type_i_inflation: float + equation_13_log_gap: float + identification_condition_holds: bool + single_trial_upper_bounds_family: bool + + +@dataclass(frozen=True, slots=True) +class GaussianTrialMixture: + """Two-component Gaussian model for one candidate specification. + + The null mean is fixed at zero, as in Equations 33-36. Boundary mixture + weights are valid for distribution evaluation, although methods that + condition on a non-null searched family require a strictly interior + ``trial_null_probability``. + """ + + trial_null_probability: float + null_standard_deviation: float + alternative_mean: float + alternative_standard_deviation: float + + def __post_init__(self) -> None: + probability = _as_probability( + "trial_null_probability", self.trial_null_probability + ) + null_scale = _as_positive_real( + "null_standard_deviation", self.null_standard_deviation + ) + alternative_mean = _as_finite_real("alternative_mean", self.alternative_mean) + alternative_scale = _as_positive_real( + "alternative_standard_deviation", self.alternative_standard_deviation + ) + object.__setattr__(self, "trial_null_probability", probability) + object.__setattr__(self, "null_standard_deviation", null_scale) + object.__setattr__(self, "alternative_mean", alternative_mean) + object.__setattr__(self, "alternative_standard_deviation", alternative_scale) + + +@dataclass(frozen=True, slots=True) +class GaussianSearchAdjustedFDR: + """Per-threshold and aggregate Gaussian search-adjustment evidence.""" + + n_trials: int + thresholds: NDArray[np.float64] + trial_type_i_errors: NDArray[np.float64] + trial_type_ii_errors: NDArray[np.float64] + family_type_i_errors: NDArray[np.float64] + family_type_ii_errors: NDArray[np.float64] + mean_family_type_i_error: float + mean_family_type_ii_error: float + family_null_probability: float + false_discovery_rate: float + + def __post_init__(self) -> None: + array_fields = ( + "thresholds", + "trial_type_i_errors", + "trial_type_ii_errors", + "family_type_i_errors", + "family_type_ii_errors", + ) + lengths: set[int] = set() + for field_name in array_fields: + values = _as_finite_vector(field_name, getattr(self, field_name)) + lengths.add(values.size) + object.__setattr__(self, field_name, _readonly_copy(values)) + if lengths == {0}: + raise ValueError("threshold evidence arrays must be nonempty") + if len(lengths) != 1: + raise ValueError("threshold evidence arrays must have equal lengths") + + +@dataclass(frozen=True, slots=True) +class FDRNonIdentificationWitness: + """Two-trial equal-component construction proving nonidentification.""" + + n_trials: int + target_false_discovery_rate: float + trial_null_probability: float + observable_cdf_at_threshold: float + latent_trial_cdf_at_threshold: float + family_null_probability: float + family_type_i_error: float + family_type_ii_error: float + family_false_discovery_rate: float + reconstructed_observable_cdf_at_threshold: float + + +@dataclass(frozen=True, slots=True) +class SelectionLevelProbabilityEvidence: + """Probability evidence that a selected maximum is a null candidate.""" + + joint_null_and_selection_probability: float + selection_probability: float + conditional_null_probability: float + quadrature_absolute_error: float + + +def _as_finite_real(name: str, value: Real) -> float: + if isinstance(value, bool) or not isinstance(value, Real): + raise ValueError(f"{name} must be a real scalar") + try: + result = float(value) + except (TypeError, ValueError, OverflowError) as error: + raise ValueError(f"{name} must be a representable real scalar") from error + if not math.isfinite(result): + raise ValueError(f"{name} must be finite") + return result + + +def _as_probability(name: str, value: Real, *, strict: bool = False) -> float: + result = _as_finite_real(name, value) + valid = 0.0 < result < 1.0 if strict else 0.0 <= result <= 1.0 + if not valid: + interval = "strictly between zero and one" if strict else "between zero and one" + raise ValueError(f"{name} must be {interval}") + return result + + +def _as_positive_real(name: str, value: Real) -> float: + result = _as_finite_real(name, value) + if result <= 0.0: + raise ValueError(f"{name} must be positive") + return result + + +def _as_nonnegative_real(name: str, value: Real) -> float: + result = _as_finite_real(name, value) + if result < 0.0: + raise ValueError(f"{name} must be nonnegative") + return result + + +def _as_positive_integer(name: str, value: Integral) -> int: + if isinstance(value, bool) or not isinstance(value, Integral): + raise ValueError(f"{name} must be an integer") + result = int(value) + if result <= 0: + raise ValueError(f"{name} must be positive") + return result + + +def _as_finite_vector(name: str, values: ArrayLike) -> NDArray[np.float64]: + try: + raw = np.asarray(values) + except (TypeError, ValueError) as error: + raise ValueError(f"{name} must be a one-dimensional real array") from error + if raw.ndim != 1: + raise ValueError(f"{name} must be one-dimensional") + if raw.dtype.kind in {"b", "c"}: + raise ValueError(f"{name} must contain real, non-boolean values") + if raw.dtype.kind not in {"i", "u", "f"}: + converted = np.empty(raw.size, dtype=np.float64) + for index, value in enumerate(raw): + converted[index] = _as_finite_real(f"{name}[{index}]", value) + else: + try: + converted = raw.astype(np.float64, copy=True) + except (TypeError, ValueError, OverflowError) as error: + raise ValueError(f"{name} must contain real values") from error + if not np.all(np.isfinite(converted)): + raise ValueError(f"{name} must contain only finite values") + return converted + + +def _readonly_copy(values: NDArray[np.float64]) -> NDArray[np.float64]: + result = np.array(values, dtype=np.float64, copy=True) + result.setflags(write=False) + return result + + +def _log_probability(value: float) -> float: + return -math.inf if value == 0.0 else math.log(value) + + +def _log_complement(value: float) -> float: + return -math.inf if value == 1.0 else math.log1p(-value) + + +def _log_one_minus_exp(log_value: float) -> float: + """Return ``log(1-exp(log_value))`` for ``log_value <= 0``.""" + + if log_value == -math.inf: + return 0.0 + if log_value == 0.0: + return -math.inf + if log_value < -math.log(2.0): + return math.log1p(-math.exp(log_value)) + return math.log(-math.expm1(log_value)) + + +def _probability_power(probability: float, n_trials: int) -> float: + if probability == 0.0: + return 0.0 + if probability == 1.0: + return 1.0 + return math.exp(n_trials * math.log(probability)) + + +def _one_minus_probability_power(probability: float, n_trials: int) -> float: + if probability == 0.0: + return 1.0 + if probability == 1.0: + return 0.0 + return -math.expm1(n_trials * math.log(probability)) + + +def _posterior_from_log_terms(log_null: float, log_alternative: float) -> float: + if log_null == -math.inf and log_alternative == -math.inf: + raise ValueError("the discovery probability must be positive") + if log_null == -math.inf: + return 0.0 + if log_alternative == -math.inf: + return 1.0 + log_denominator = float(logsumexp((log_null, log_alternative))) + return math.exp(log_null - log_denominator) + + +def _log_weighted_pair( + weight: float, first_log_value: float, second_log_value: float +) -> float: + if weight == 0.0: + return second_log_value + if weight == 1.0: + return first_log_value + return float( + np.logaddexp( + math.log(weight) + first_log_value, + math.log1p(-weight) + second_log_value, + ) + ) + + +def single_trial_false_discovery_rate( + type_i_error: Real, + type_ii_error: Real, + trial_null_probability: Real, +) -> float: + r"""Return the single-trial posterior FDR from Equation 6. + + The three probabilities may include their boundaries. A configuration + with zero probability of a discovery is rejected because the posterior is + then undefined. + """ + + alpha = _as_probability("type_i_error", type_i_error) + beta = _as_probability("type_ii_error", type_ii_error) + pi_zero = _as_probability("trial_null_probability", trial_null_probability) + log_null = _log_probability(alpha) + _log_probability(pi_zero) + log_alternative = _log_complement(beta) + _log_complement(pi_zero) + return _posterior_from_log_terms(log_null, log_alternative) + + +def family_level_false_discovery_rate( + family_type_i_error: Real, + family_type_ii_error: Real, + trial_null_probability: Real, + n_trials: Integral, +) -> float: + r"""Return the family-level search-adjusted FDR from Equations 9-10.""" + + alpha_family = _as_probability("family_type_i_error", family_type_i_error) + beta_family = _as_probability("family_type_ii_error", family_type_ii_error) + pi_zero = _as_probability("trial_null_probability", trial_null_probability) + trials = _as_positive_integer("n_trials", n_trials) + log_family_null = trials * _log_probability(pi_zero) + log_family_alternative = _log_one_minus_exp(log_family_null) + log_null = _log_probability(alpha_family) + log_family_null + log_alternative = _log_complement(beta_family) + log_family_alternative + return _posterior_from_log_terms(log_null, log_alternative) + + +def max_selection_family_errors( + trial_type_i_error: Real, + trial_type_ii_error: Real, + trial_null_probability: Real, + n_trials: Integral, +) -> MaxSelectionFamilyErrors: + r"""Return maximum-selection family errors from Equations 17-18.""" + + alpha = _as_probability("trial_type_i_error", trial_type_i_error) + beta = _as_probability("trial_type_ii_error", trial_type_ii_error) + pi_zero = _as_probability( + "trial_null_probability", trial_null_probability, strict=True + ) + trials = _as_positive_integer("n_trials", n_trials) + + family_null = _probability_power(pi_zero, trials) + family_alpha = _one_minus_probability_power(1.0 - alpha, trials) + first_base = math.fsum((pi_zero * (1.0 - alpha), (1.0 - pi_zero) * beta)) + second_base = pi_zero * (1.0 - alpha) + log_denominator = _log_one_minus_exp(trials * math.log(pi_zero)) + + if first_base == 0.0 or first_base == second_base: + family_beta = 0.0 + else: + log_first_power = trials * math.log(first_base) + if second_base == 0.0: + log_numerator = log_first_power + else: + log_ratio = trials * (math.log(second_base) - math.log(first_base)) + log_numerator = log_first_power + _log_one_minus_exp(log_ratio) + family_beta = math.exp(log_numerator - log_denominator) + family_beta = min(1.0, max(0.0, family_beta)) + + return MaxSelectionFamilyErrors( + n_trials=trials, + trial_null_probability=pi_zero, + trial_type_i_error=alpha, + trial_type_ii_error=beta, + family_null_probability=family_null, + family_type_i_error=family_alpha, + family_type_ii_error=family_beta, + ) + + +def compare_single_and_family_fdr( + trial_type_i_error: Real, + trial_type_ii_error: Real, + trial_null_probability: Real, + n_trials: Integral, + *, + family_type_i_error: Real, + family_type_ii_error: Real, + relative_tolerance: Real = 1e-12, + absolute_tolerance: Real = 1e-15, +) -> FDRComparisonEvidence: + r"""Evaluate the equality and upper-bound conditions in Equations 13-15.""" + + alpha = _as_probability("trial_type_i_error", trial_type_i_error, strict=True) + beta = _as_probability("trial_type_ii_error", trial_type_ii_error, strict=True) + pi_zero = _as_probability( + "trial_null_probability", trial_null_probability, strict=True + ) + trials = _as_positive_integer("n_trials", n_trials) + alpha_family = _as_probability( + "family_type_i_error", family_type_i_error, strict=True + ) + beta_family = _as_probability( + "family_type_ii_error", family_type_ii_error, strict=True + ) + relative = _as_nonnegative_real("relative_tolerance", relative_tolerance) + absolute = _as_nonnegative_real("absolute_tolerance", absolute_tolerance) + + log_observed = math.log(alpha_family) - math.log(alpha) + log_family_null = trials * math.log(pi_zero) + log_required = ( + math.log1p(-beta_family) + - math.log1p(-beta) + + _log_one_minus_exp(log_family_null) + - (trials - 1) * math.log(pi_zero) + - math.log1p(-pi_zero) + ) + gap = log_observed - log_required + scale_tolerance = max( + absolute, relative * max(abs(log_observed), abs(log_required)) + ) + equality = math.isclose( + log_observed, log_required, rel_tol=relative, abs_tol=absolute + ) + + return FDRComparisonEvidence( + n_trials=trials, + single_trial_false_discovery_rate=single_trial_false_discovery_rate( + alpha, beta, pi_zero + ), + family_level_false_discovery_rate=family_level_false_discovery_rate( + alpha_family, beta_family, pi_zero, trials + ), + log_observed_type_i_inflation=log_observed, + log_required_type_i_inflation=log_required, + equation_13_log_gap=gap, + identification_condition_holds=equality, + single_trial_upper_bounds_family=gap <= scale_tolerance, + ) + + +def maximum_mixture_cdf( + null_cdf: Real, + alternative_cdf: Real, + trial_null_probability: Real, + n_trials: Integral, +) -> float: + r"""Return the CDF of the maximum in Equation 16 from component CDFs.""" + + null_value = _as_probability("null_cdf", null_cdf) + alternative_value = _as_probability("alternative_cdf", alternative_cdf) + pi_zero = _as_probability("trial_null_probability", trial_null_probability) + trials = _as_positive_integer("n_trials", n_trials) + mixture_value = math.fsum( + (pi_zero * null_value, (1.0 - pi_zero) * alternative_value) + ) + mixture_value = min(1.0, max(0.0, mixture_value)) + return _probability_power(mixture_value, trials) + + +def conditional_upper_tail_probability( + cdf_at_value: Real, cdf_at_threshold: Real +) -> float: + r"""Return ``P[X >= x | X >= c]`` from Equation 26 for ``x >= c``.""" + + at_value = _as_probability("cdf_at_value", cdf_at_value) + at_threshold = _as_probability("cdf_at_threshold", cdf_at_threshold) + if at_threshold == 1.0: + raise ValueError("cdf_at_threshold must be less than one") + if at_value < at_threshold: + raise ValueError("cdf_at_value must not be below cdf_at_threshold") + return (1.0 - at_value) / (1.0 - at_threshold) + + +def gaussian_trial_mixture_cdf(model: GaussianTrialMixture, x: Real) -> float: + r"""Return the Gaussian trial-mixture CDF in Equations 33-35.""" + + if not isinstance(model, GaussianTrialMixture): + raise TypeError("model must be a GaussianTrialMixture") + value = _as_finite_real("x", x) + null_cdf = float(ndtr(value / model.null_standard_deviation)) + alternative_cdf = float( + ndtr((value - model.alternative_mean) / model.alternative_standard_deviation) + ) + result = math.fsum( + ( + model.trial_null_probability * null_cdf, + (1.0 - model.trial_null_probability) * alternative_cdf, + ) + ) + return min(1.0, max(0.0, result)) + + +def gaussian_max_selection_cdf( + model: GaussianTrialMixture, x: Real, n_trials: Integral +) -> float: + r"""Return the selected Gaussian maximum CDF in Equation 35.""" + + trials = _as_positive_integer("n_trials", n_trials) + return _probability_power(gaussian_trial_mixture_cdf(model, x), trials) + + +def _gaussian_component_logs( + model: GaussianTrialMixture, x: float +) -> tuple[float, float, float, float]: + null_z = x / model.null_standard_deviation + alternative_z = (x - model.alternative_mean) / model.alternative_standard_deviation + null_log_cdf = float(log_ndtr(null_z)) + alternative_log_cdf = float(log_ndtr(alternative_z)) + log_two_pi = math.log(2.0 * math.pi) + null_log_density = ( + -0.5 * null_z * null_z + - math.log(model.null_standard_deviation) + - 0.5 * log_two_pi + ) + alternative_log_density = ( + -0.5 * alternative_z * alternative_z + - math.log(model.alternative_standard_deviation) + - 0.5 * log_two_pi + ) + return ( + null_log_cdf, + alternative_log_cdf, + null_log_density, + alternative_log_density, + ) + + +def gaussian_max_selection_log_density( + model: GaussianTrialMixture, x: Real, n_trials: Integral +) -> float: + r"""Return the log density of the selected maximum from Equation 36.""" + + if not isinstance(model, GaussianTrialMixture): + raise TypeError("model must be a GaussianTrialMixture") + value = _as_finite_real("x", x) + trials = _as_positive_integer("n_trials", n_trials) + ( + null_log_cdf, + alternative_log_cdf, + null_log_density, + alternative_log_density, + ) = _gaussian_component_logs(model, value) + log_mixture_cdf = _log_weighted_pair( + model.trial_null_probability, null_log_cdf, alternative_log_cdf + ) + log_mixture_density = _log_weighted_pair( + model.trial_null_probability, + null_log_density, + alternative_log_density, + ) + return math.log(trials) + (trials - 1) * log_mixture_cdf + log_mixture_density + + +def gaussian_max_selection_log_likelihood( + model: GaussianTrialMixture, + observations: ArrayLike, + n_trials: Integral, +) -> float: + r"""Return the Equation 37 log-likelihood for selected maxima.""" + + if not isinstance(model, GaussianTrialMixture): + raise TypeError("model must be a GaussianTrialMixture") + values = _as_finite_vector("observations", observations) + if values.size == 0: + raise ValueError("observations must be nonempty") + trials = _as_positive_integer("n_trials", n_trials) + terms = [ + gaussian_max_selection_log_density(model, value, trials) for value in values + ] + result = math.fsum(terms) + if math.isnan(result) or result == math.inf: + raise ValueError("log-likelihood could not be evaluated") + return result + + +def gaussian_search_adjusted_false_discovery_rate( + model: GaussianTrialMixture, + thresholds: ArrayLike, + n_trials: Integral, +) -> GaussianSearchAdjustedFDR: + r"""Apply Equations 38-40 to a vector of Gaussian thresholds.""" + + if not isinstance(model, GaussianTrialMixture): + raise TypeError("model must be a GaussianTrialMixture") + threshold_values = _as_finite_vector("thresholds", thresholds) + if threshold_values.size == 0: + raise ValueError("thresholds must be nonempty") + trials = _as_positive_integer("n_trials", n_trials) + + null_z = threshold_values / model.null_standard_deviation + alternative_z = ( + threshold_values - model.alternative_mean + ) / model.alternative_standard_deviation + trial_alpha = np.asarray(ndtr(-null_z), dtype=np.float64) + trial_beta = np.asarray(ndtr(alternative_z), dtype=np.float64) + family_alpha = np.empty_like(trial_alpha) + family_beta = np.empty_like(trial_beta) + for index, (alpha, beta) in enumerate(zip(trial_alpha, trial_beta, strict=True)): + evidence = max_selection_family_errors( + alpha, beta, model.trial_null_probability, trials + ) + family_alpha[index] = evidence.family_type_i_error + family_beta[index] = evidence.family_type_ii_error + + mean_family_alpha = float(np.mean(family_alpha)) + mean_family_beta = float(np.mean(family_beta)) + family_null = _probability_power(model.trial_null_probability, trials) + false_discovery_rate = family_level_false_discovery_rate( + mean_family_alpha, + mean_family_beta, + model.trial_null_probability, + trials, + ) + return GaussianSearchAdjustedFDR( + n_trials=trials, + thresholds=threshold_values, + trial_type_i_errors=trial_alpha, + trial_type_ii_errors=trial_beta, + family_type_i_errors=family_alpha, + family_type_ii_errors=family_beta, + mean_family_type_i_error=mean_family_alpha, + mean_family_type_ii_error=mean_family_beta, + family_null_probability=family_null, + false_discovery_rate=false_discovery_rate, + ) + + +def fdr_nonidentification_witness( + observable_cdf_at_threshold: Real, target_false_discovery_rate: Real +) -> FDRNonIdentificationWitness: + r"""Return the exact two-trial witness from Equations 41-50. + + The construction belongs to the unrestricted model class in which the + null and alternative component CDFs may be equal. It does not assert + nonidentification under a separated parametric mixture restriction. + """ + + observable_cdf = _as_probability( + "observable_cdf_at_threshold", observable_cdf_at_threshold, strict=True + ) + target = _as_probability( + "target_false_discovery_rate", target_false_discovery_rate, strict=True + ) + trials = 2 + trial_null = math.sqrt(target) + latent_cdf = math.sqrt(observable_cdf) + family_null = trial_null**trials + family_alpha = 1.0 - observable_cdf + family_beta = observable_cdf + family_fdr = family_level_false_discovery_rate( + family_alpha, family_beta, trial_null, trials + ) + reconstructed = latent_cdf**trials + return FDRNonIdentificationWitness( + n_trials=trials, + target_false_discovery_rate=target, + trial_null_probability=trial_null, + observable_cdf_at_threshold=observable_cdf, + latent_trial_cdf_at_threshold=latent_cdf, + family_null_probability=family_null, + family_type_i_error=family_alpha, + family_type_ii_error=family_beta, + family_false_discovery_rate=family_fdr, + reconstructed_observable_cdf_at_threshold=reconstructed, + ) + + +def max_selection_null_probability( + threshold: Real, + trial_null_probability: Real, + n_trials: Integral, + *, + null_density: Callable[[float], Real], + mixture_cdf: Callable[[float], Real], + absolute_tolerance: Real = 1e-12, + relative_tolerance: Real = 1e-10, + max_subintervals: Integral = 200, +) -> SelectionLevelProbabilityEvidence: + r"""Return the selected-winner null probability from footnote 25. + + The numerator is the joint probability that a null candidate wins and + the selected maximum exceeds ``threshold``. Adaptive quadrature is + performed only over the caller-supplied null density and trial-mixture + CDF; neither callback is treated as an unverified probability oracle. + """ + + threshold_value = _as_finite_real("threshold", threshold) + pi_zero = _as_probability("trial_null_probability", trial_null_probability) + trials = _as_positive_integer("n_trials", n_trials) + if not callable(null_density): + raise TypeError("null_density must be callable") + if not callable(mixture_cdf): + raise TypeError("mixture_cdf must be callable") + absolute = _as_positive_real("absolute_tolerance", absolute_tolerance) + relative = _as_positive_real("relative_tolerance", relative_tolerance) + subdivisions = _as_positive_integer("max_subintervals", max_subintervals) + + def checked_cdf(value: float) -> float: + return _as_probability("mixture_cdf return value", mixture_cdf(value)) + + mixture_at_threshold = checked_cdf(threshold_value) + selection_probability = _one_minus_probability_power(mixture_at_threshold, trials) + if selection_probability == 0.0: + raise ValueError("the selection probability must be positive") + + if pi_zero == 0.0: + return SelectionLevelProbabilityEvidence( + joint_null_and_selection_probability=0.0, + selection_probability=selection_probability, + conditional_null_probability=0.0, + quadrature_absolute_error=0.0, + ) + + def integrand(value: float) -> float: + density = _as_finite_real("null_density return value", null_density(value)) + if density < 0.0: + raise ValueError("null_density return value must be nonnegative") + cdf_value = checked_cdf(value) + return density * _probability_power(cdf_value, trials - 1) + + try: + with warnings.catch_warnings(): + warnings.simplefilter("error", IntegrationWarning) + integral, error = quad( + integrand, + threshold_value, + np.inf, + epsabs=absolute, + epsrel=relative, + limit=subdivisions, + ) + except IntegrationWarning as warning: + raise ValueError("selection-level quadrature did not converge") from warning + if not math.isfinite(integral) or not math.isfinite(error) or integral < 0.0: + raise ValueError("selection-level quadrature returned invalid evidence") + + scale = trials * pi_zero + joint_probability = scale * integral + absolute_error = scale * error + comparison_tolerance = max( + absolute, + relative * selection_probability, + 8.0 * np.finfo(np.float64).eps, + ) + if joint_probability > selection_probability + comparison_tolerance: + raise ValueError("callbacks imply a null probability above one") + joint_probability = min(selection_probability, max(0.0, joint_probability)) + conditional = joint_probability / selection_probability + return SelectionLevelProbabilityEvidence( + joint_null_and_selection_probability=joint_probability, + selection_probability=selection_probability, + conditional_null_probability=conditional, + quadrature_absolute_error=absolute_error, + ) diff --git a/src/RiskLabAI/causal_factor_analysis/structural_models.py b/src/RiskLabAI/causal_factor_analysis/structural_models.py new file mode 100644 index 0000000..2de39ee --- /dev/null +++ b/src/RiskLabAI/causal_factor_analysis/structural_models.py @@ -0,0 +1,186 @@ +"""Deterministic evaluation of supplied structural causal mechanisms.""" + +from __future__ import annotations + +import heapq +from collections.abc import Callable, Mapping +from dataclasses import dataclass + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +from .graph_identification import CausalDAG + +__all__ = ["StructuralCausalModelResult", "evaluate_structural_causal_model"] + + +@dataclass(frozen=True, slots=True, eq=False) +class StructuralCausalModelResult: + """Canonical node order and sample-by-node structural values.""" + + node_order: tuple[str, ...] + values: NDArray[np.float64] + + +def _validate_dag(dag: CausalDAG) -> CausalDAG: + if not isinstance(dag, CausalDAG): + raise TypeError("dag must be a CausalDAG") + return dag + + +def _readonly_snapshot(values: NDArray[np.float64]) -> NDArray[np.float64]: + """Return a C-contiguous array backed by immutable independent bytes.""" + + contiguous = np.ascontiguousarray(values, dtype=np.float64) + snapshot = np.frombuffer(contiguous.tobytes(order="C"), dtype=np.float64) + return snapshot.reshape(contiguous.shape) + + +def _as_finite_vector( + values: ArrayLike, + subject: str, + *, + expected_length: int | None = None, +) -> NDArray[np.float64]: + try: + raw = np.asarray(values) + except (TypeError, ValueError, OverflowError) as exc: + raise ValueError(f"{subject} must be a real numeric vector") from exc + if raw.ndim != 1: + raise ValueError(f"{subject} must be a one-dimensional vector") + if raw.dtype.kind == "b": + raise ValueError(f"{subject} must not be boolean-valued") + if np.iscomplexobj(raw): + raise ValueError(f"{subject} must be real-valued") + if raw.dtype.kind in {"S", "U", "V"}: + raise ValueError(f"{subject} must be a real numeric vector") + try: + vector = np.asarray(raw, dtype=np.float64) + except (TypeError, ValueError, OverflowError) as exc: + raise ValueError(f"{subject} must be a real numeric vector") from exc + if expected_length is not None and vector.shape[0] != expected_length: + raise ValueError(f"{subject} must have length {expected_length}") + if not np.all(np.isfinite(vector)): + raise ValueError(f"{subject} must contain only finite values") + return _readonly_snapshot(vector) + + +def _mapping_keys(mapping: Mapping, subject: str, nodes: set[str]) -> None: + try: + keys = set(mapping.keys()) + except (AttributeError, TypeError) as exc: + raise TypeError(f"{subject} must expose hashable mapping keys") from exc + if keys != nodes: + raise ValueError(f"{subject} must contain exactly one entry per graph node") + + +def _parent_map(dag: CausalDAG) -> dict[str, tuple[str, ...]]: + parents: dict[str, list[str]] = {node: [] for node in dag.nodes} + for source, target in dag.directed_edges: + parents[target].append(source) + return {node: tuple(sorted(values)) for node, values in parents.items()} + + +def _lexical_topological_order( + dag: CausalDAG, parents: dict[str, tuple[str, ...]] +) -> tuple[str, ...]: + children: dict[str, list[str]] = {node: [] for node in dag.nodes} + indegree = {node: len(parents[node]) for node in dag.nodes} + for source, target in dag.directed_edges: + children[source].append(target) + for values in children.values(): + values.sort() + + ready = [node for node in dag.nodes if indegree[node] == 0] + heapq.heapify(ready) + ordered: list[str] = [] + while ready: + node = heapq.heappop(ready) + ordered.append(node) + for child in children[node]: + indegree[child] -= 1 + if indegree[child] == 0: + heapq.heappush(ready, child) + if len(ordered) != len(dag.nodes): + raise ValueError("dag must be acyclic") + return tuple(ordered) + + +def evaluate_structural_causal_model( + dag: CausalDAG, + mechanisms: Mapping[ + str, + Callable[[NDArray[np.float64], NDArray[np.float64]], ArrayLike], + ], + exogenous_inputs: Mapping[str, ArrayLike], +) -> StructuralCausalModelResult: + """Evaluate one deterministic structural mechanism per graph node. + + Nodes are evaluated in a lexical tie-broken topological order. A node's + mechanism receives an ``(n_samples, n_parents)`` parent matrix whose + columns follow lexical parent order and an ``n_samples`` exogenous vector. + Both inputs are protected independent snapshots. The mechanism must + return one finite vector of length ``n_samples``. + """ + + dag = _validate_dag(dag) + if not isinstance(mechanisms, Mapping): + raise TypeError("mechanisms must be a mapping") + if not isinstance(exogenous_inputs, Mapping): + raise TypeError("exogenous_inputs must be a mapping") + + graph_nodes = set(dag.nodes) + _mapping_keys(mechanisms, "mechanisms", graph_nodes) + _mapping_keys(exogenous_inputs, "exogenous_inputs", graph_nodes) + + mechanism_snapshot = {node: mechanisms[node] for node in dag.nodes} + for node, mechanism in mechanism_snapshot.items(): + if not callable(mechanism): + raise TypeError(f"mechanism for node {node!r} must be callable") + + exogenous_snapshot: dict[str, NDArray[np.float64]] = {} + n_samples: int | None = None + for node in dag.nodes: + vector = _as_finite_vector( + exogenous_inputs[node], f"exogenous input for node {node!r}" + ) + if n_samples is None: + n_samples = vector.shape[0] + if n_samples == 0: + raise ValueError( + "exogenous input vectors must contain at least one sample" + ) + elif vector.shape[0] != n_samples: + raise ValueError("all exogenous input vectors must have equal length") + exogenous_snapshot[node] = vector + + if n_samples is None: + raise ValueError("dag must contain at least one node") + + parents = _parent_map(dag) + node_order = _lexical_topological_order(dag, parents) + evaluated: dict[str, NDArray[np.float64]] = {} + for node in node_order: + parent_nodes = parents[node] + if parent_nodes: + parent_matrix = np.column_stack( + tuple(evaluated[parent] for parent in parent_nodes) + ) + else: + parent_matrix = np.empty((n_samples, 0), dtype=np.float64) + parent_snapshot = _readonly_snapshot(parent_matrix) + raw_result = mechanism_snapshot[node]( + parent_snapshot, + exogenous_snapshot[node], + ) + evaluated[node] = _as_finite_vector( + raw_result, + f"mechanism result for node {node!r}", + expected_length=n_samples, + ) + + values = np.column_stack(tuple(evaluated[node] for node in node_order)) + return StructuralCausalModelResult( + node_order=node_order, + values=_readonly_snapshot(values), + ) diff --git a/src/RiskLabAI/data/denoise/__init__.py b/src/RiskLabAI/data/denoise/__init__.py index 3506d53..20adb39 100644 --- a/src/RiskLabAI/data/denoise/__init__.py +++ b/src/RiskLabAI/data/denoise/__init__.py @@ -35,6 +35,6 @@ "corr_to_cov", "denoise_cov", "optimal_portfolio", - # NERCOME (Appraisal 24 admit) + # NERCOME (independent validation admit) "nercome_denoised_covariance", ] diff --git a/src/RiskLabAI/data/denoise/nercome.py b/src/RiskLabAI/data/denoise/nercome.py index 9899e68..ea05e25 100644 --- a/src/RiskLabAI/data/denoise/nercome.py +++ b/src/RiskLabAI/data/denoise/nercome.py @@ -6,12 +6,12 @@ has no clean gap (a slowly-decaying bulk) or is non-stationary, that assumption breaks and clipping degenerates toward the raw sample covariance. NERCOME (Lam 2016) instead regularizes the eigenvalues by sample-splitting: it estimates the eigenvectors on one split of the data and the oracle eigenvalues by -projecting the held-out split's sample covariance onto those eigenvectors, averaging over many random +projecting the validation sample split's sample covariance onto those eigenvectors, averaging over many random splits. This recovers the covariance more accurately and with better conditioning on no-gap / non-stationary spectra, and converges to clipping where the gap assumption holds. -Admitted in Appraisal 24 (CONTRIBUTIONS_LEDGER 2026-06-27) - portfolio's first admitted extension. Regime -tag, verbatim from the verdict: +Included after independent validation (the public method specification) - portfolio's first admitted extension. Regime +tag, verbatim from the documented guidance: prefer NERCOME over MP clipping for covariance estimation when the eigenvalue spectrum has no clean gap or is non-stationary (better accuracy and conditioning, and lower OOS volatility via NCO / @@ -19,11 +19,11 @@ concentration than clipping, gives no risk-adjusted-return edge (none does - 1/N stands), and HRP does not benefit (use it through NCO / min-variance). -Held-out confirmed (appraisals/24_results, HELDOUT.md): on the sealed no-gap x non-stationary x T=30 +validation sample confirmed (the documented validation evidence, independent validation): on the sealed no-gap x non-stationary x T=30 corner NERCOME has distinguishably lower covariance error (relative Frobenius 0.563 vs MP clipping 0.684) and better conditioning (29.6 vs 46.3), with lower NCO realized risk (1.315 vs 1.553); HRP is insensitive. Unlike `denoise_cov`, which cleans a covariance matrix, NERCOME is a data-driven (sample-splitting) -estimator, so it takes the return matrix. Evidence and caveats: appraisals/24_verdict.md. +estimator, so it takes the return matrix. Evidence and caveats: the documented validation evidence. References ---------- @@ -71,7 +71,7 @@ def nercome_denoised_covariance( non-stationary (better accuracy and conditioning, and lower out-of-sample volatility via NCO / min-variance); it converges to clipping on clean-gap stationary spectra, costs more turnover and concentration, gives no risk-adjusted-return edge, and HRP does not benefit (use it through NCO / - min-variance). See the module docstring for the full verbatim regime tag and appraisals/24_verdict.md. + min-variance). See the module docstring for the full usage guidance and the documented validation evidence. Unlike :func:`denoising.denoise_cov`, this takes the return matrix, not a covariance. Parameters diff --git a/src/RiskLabAI/data/differentiation/adaptive_differentiation.py b/src/RiskLabAI/data/differentiation/adaptive_differentiation.py index 6eeaa2c..da2a5dd 100644 --- a/src/RiskLabAI/data/differentiation/adaptive_differentiation.py +++ b/src/RiskLabAI/data/differentiation/adaptive_differentiation.py @@ -8,16 +8,15 @@ directly from the data's long-memory structure, with a finite-sample bias correction, and so recovers the genuine order far more accurately when memory is strong and the sample is finite. -Admitted in Appraisal 13 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: prefer AFD over fixed-width FFD when the differencing order itself must be right - strong long memory and finite samples, where min-d-via-ADF under-differences; on weak memory the gap narrows. The implemented AFD is a tractable clean-room approximation of the published wavelet-Hurst + ridge + CV-truncation method (no public code). -The real-data predictive-lift confirmation is a tracked obligation (``REAL_DATA_FOLLOWUPS.md``): the -order accuracy is established against ground truth, but the downstream predictive value of the more -accurate order is not yet decisive. Evidence and caveats: appraisals/13_verdict.md (and 13b if logged). +Real-data predictive lift has not yet been established: order accuracy is supported against ground +truth, but the downstream predictive value of the more accurate order is not yet decisive. This is a tractable clean-room approximation of the published AFD (IEEE Access 2025; no public code): the differencing order is anchored at ``0.5 + d_hat`` where ``d_hat`` is a finite-sample-bias-corrected @@ -176,8 +175,8 @@ def adaptive_fractional_difference( Prefer AFD over fixed-width FFD when the differencing order itself must be right (strong long memory, finite samples), where de Prado's min-d-via-ADF under-differences; on weak memory the gap narrows. - See the module docstring for the full regime tag, the clean-room-approximation note, the tracked - real-data follow-up, and appraisals/13_verdict.md. + See the module docstring for the full usage guidance, approximation note, and remaining real-data + validation limitation. Parameters ---------- diff --git a/src/RiskLabAI/data/distance/codependence.py b/src/RiskLabAI/data/distance/codependence.py index a9fdb48..aad2966 100644 --- a/src/RiskLabAI/data/distance/codependence.py +++ b/src/RiskLabAI/data/distance/codependence.py @@ -13,7 +13,7 @@ - distance correlation, a tuning-free dependence index in [0, 1] that is zero only at independence and detects nonlinear dependence with no estimation parameter. -Admitted in Appraisal 11 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tags, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: KSG: prefer KSG over binned MI/VI on short, noisy, or nonlinear/heavy-tailed samples - it is far less biased there and is essentially unbiased on linear dependence; on large samples with simple @@ -25,7 +25,7 @@ cluster stability). It is a dependence index, not a metric on partitions like VI (keep VI/KSG for the metric role). -Evidence and caveats: appraisals/11_verdict.md. +Evidence and caveats: the documented validation evidence. References ---------- @@ -71,7 +71,7 @@ def ksg_mutual_information( samples, and essentially unbiased on linear dependence; on large near-linear samples binned MI is at least as good, so prefer KSG in the former regime. It can return a slightly negative value for (near-)independent data, which is a characterized property of the estimator, not an error. See the - module docstring for the full regime tag and appraisals/11_verdict.md. + module docstring for the full usage guidance and the documented validation evidence. Parameters ---------- @@ -152,7 +152,7 @@ def distance_correlation(x: np.ndarray, y: np.ndarray) -> float: It is zero only at population independence and detects nonlinear dependence with no estimation parameter, so it is a tuning-free nonlinear dependence screen / codependence kernel. It is a dependence index, not a metric on partitions like the variation of information (keep VI / KSG for the - metric role). See the module docstring for the full regime tag and appraisals/11_verdict.md. + metric role). See the module docstring for the full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/data/structures/__init__.py b/src/RiskLabAI/data/structures/__init__.py index 6ad32f4..99f8c90 100644 --- a/src/RiskLabAI/data/structures/__init__.py +++ b/src/RiskLabAI/data/structures/__init__.py @@ -13,7 +13,7 @@ # Exports from time_bars.py from .time_bars import TimeBars -# Hardened (anti-degeneracy) information-driven bars (Appraisal 19 robustness fix) +# Hardened (anti-degeneracy) information-driven bars (independent validation robustness fix) from .hardened_information_driven_bars import ( HardenedExpectedImbalanceBars, HardenedExpectedRunBars, diff --git a/src/RiskLabAI/data/structures/hardened_information_driven_bars.py b/src/RiskLabAI/data/structures/hardened_information_driven_bars.py index 0862eeb..f130232 100644 --- a/src/RiskLabAI/data/structures/hardened_information_driven_bars.py +++ b/src/RiskLabAI/data/structures/hardened_information_driven_bars.py @@ -9,8 +9,8 @@ is no longer usable. The hardened variant adds two guards that bound the bar size without touching the well-behaved regime. -Admitted in Appraisal 19 (CONTRIBUTIONS_LEDGER 2026-06-27) as a correctness / robustness fix. Scope -note, verbatim from the verdict: +Included after independent validation (the public method specification) as a correctness / robustness fix. Scope +note, verbatim from the documented guidance: replace the naive imbalance/run-bar construction with the guarded version - it removes the documented degeneracy (1-tick collapse / divergence) while leaving well-behaved parameterizations @@ -20,10 +20,10 @@ (2) force-close a bar at ``max_ticks`` ticks (prevents divergence); E[T] is additionally clamped to ``[min_ticks, max_ticks]`` via the base class's ``expected_ticks_number_bounds``. The naive ``ExpectedImbalanceBars`` / ``ExpectedRunBars`` are left unchanged and remain available; the hardened -classes only subclass them. Held-out confirmed on the sealed tick segment (appraisals/19_results, -HELDOUT.md): naive imbalance collapses (86.9% of bars <= 2 ticks, min 1) while hardened holds (0% +classes only subclass them. validation sample confirmed on the sealed tick segment (the documented validation evidence, +independent validation): naive imbalance collapses (86.9% of bars <= 2 ticks, min 1) while hardened holds (0% <= 2-tick, min 20); naive run diverges (11 bars) while hardened stays bounded; a well-behaved run -configuration is unchanged (6.2% bar-count difference). Evidence and caveats: appraisals/19_verdict.md. +configuration is unchanged (6.2% bar-count difference). Evidence and caveats: the documented validation evidence. References ---------- @@ -40,14 +40,14 @@ class HardenedExpectedImbalanceBars(ExpectedImbalanceBars): r""" - Expected-imbalance bars with anti-degeneracy guards (Appraisal 19 robustness fix). + Expected-imbalance bars with anti-degeneracy guards (independent validation robustness fix). Subclasses :class:`ExpectedImbalanceBars` and overrides only the bar-construction condition: a bar cannot close before ``min_ticks`` ticks (removes the 1-tick collapse) and is force-closed at ``max_ticks`` ticks (removes the divergence); E[T] is clamped to ``[min_ticks, max_ticks]``. Between the guards the original de Prado condition is used unchanged, so a well-parameterized construction is - left essentially as-is. See the module docstring for the verbatim scope note and - appraisals/19_verdict.md. + left essentially as-is. See the module docstring for the scope guidance and + the documented validation evidence. Parameters ---------- @@ -95,12 +95,12 @@ def _bar_construction_condition(self, threshold: float) -> bool: class HardenedExpectedRunBars(ExpectedRunBars): r""" - Expected-run bars with anti-degeneracy guards (Appraisal 19 robustness fix). + Expected-run bars with anti-degeneracy guards (independent validation robustness fix). Subclasses :class:`ExpectedRunBars` and overrides only the bar-construction condition with the same two guards as :class:`HardenedExpectedImbalanceBars`: no close before ``min_ticks`` ticks, force-close at ``max_ticks`` ticks, E[T] clamped to ``[min_ticks, max_ticks]``. See the module docstring for the - verbatim scope note and appraisals/19_verdict.md. + scope guidance and the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/features/entropy_features/bias_corrected.py b/src/RiskLabAI/features/entropy_features/bias_corrected.py index 507c8ed..0ee1bdc 100644 --- a/src/RiskLabAI/features/entropy_features/bias_corrected.py +++ b/src/RiskLabAI/features/entropy_features/bias_corrected.py @@ -13,7 +13,7 @@ by the word length, so they are drop-in comparable with `plug_in_entropy_estimator`. The plug-in's behaviour is unchanged. -Admitted in Appraisal 06 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag (preferred-when / avoid-when), +Included after independent validation (the public method specification). usage guidance (preferred-when / avoid-when), verbatim from the ledger: Prefer a bias-corrected estimator over the plug-in whenever the symbol counts are undersampled (a @@ -25,7 +25,7 @@ and a gap caused by non-stationarity (e.g. sigma encoding) is not an entropy-bias problem the correction can fix. -Evidence and caveats: appraisals/06_verdict.md. +Evidence and caveats: the documented validation evidence. References ---------- @@ -77,7 +77,7 @@ def miller_madow_entropy(message: str, approximate_word_length: int = 1) -> floa large effective alphabet relative to the sample: long words, fine encodings, or short windows). Miller-Madow is the cheap first-order fix; it converges to the plug-in when N is much larger than K, with no over-correction. The gain is negligible for coarse encodings (e.g. binary). See the - module docstring for the full regime tag and appraisals/06_verdict.md. + module docstring for the full usage guidance and the documented validation evidence. Parameters ---------- @@ -123,8 +123,8 @@ def grassberger_entropy(message: str, approximate_word_length: int = 1) -> float large effective alphabet relative to the sample: long words, fine encodings, or short windows). Grassberger is close to NSB at lower cost (the practical default when undersampled); it converges to the plug-in when N is much larger than K, with no over-correction. The gain is negligible for - coarse encodings (e.g. binary). See the module docstring for the full regime tag and - appraisals/06_verdict.md. + coarse encodings (e.g. binary). See the module docstring for the full usage guidance and + the documented validation evidence. Parameters ---------- @@ -256,7 +256,7 @@ def nsb_entropy( NSB is most accurate in deep undersampling; it converges to the plug-in when N is much larger than K, with no over-correction. The gain is negligible for coarse encodings (e.g. binary), and a gap caused by non-stationarity (e.g. sigma encoding) is not an entropy-bias problem the correction can - fix. See the module docstring for the full regime tag and appraisals/06_verdict.md. + fix. See the module docstring for the full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/features/feature_importance/__init__.py b/src/RiskLabAI/features/feature_importance/__init__.py index 118b1cc..911d8c3 100644 --- a/src/RiskLabAI/features/feature_importance/__init__.py +++ b/src/RiskLabAI/features/feature_importance/__init__.py @@ -51,7 +51,7 @@ "FeatureImportanceMDA", "ClusteredFeatureImportanceMDA", "FeatureImportanceSFI", - # Debiased / conditional importance (Appraisal 10) + # Debiased / conditional importance (independent validation) "mdi_plus_importance", "conditional_predictive_impact", # Imported utilities from *other* modules diff --git a/src/RiskLabAI/features/feature_importance/debiased_importance.py b/src/RiskLabAI/features/feature_importance/debiased_importance.py index a601ebc..f6af307 100644 --- a/src/RiskLabAI/features/feature_importance/debiased_importance.py +++ b/src/RiskLabAI/features/feature_importance/debiased_importance.py @@ -13,7 +13,7 @@ the model-loss increase when the true feature is replaced by a conditionally-resampled knockoff, with a paired significance test, the calibrated test MDA lacks. -Admitted in Appraisal 10 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tags, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: MDI+: prefer MDI+ over MDI when features are noisy, high-cardinality, or mixed-type - it rejects noise/cardinality inflation (noise-reject 0.94 vs 0.50) and recovers true relevance better; @@ -25,8 +25,8 @@ The implemented MDI+ is the simplified faithful variant admitted (per-tree decision-stump ridge GLM with an out-of-bag partial-variance score), NOT the full leave-one-out / similarity-weighted paper algorithm, which is a logged refinement. CPI's test holds nominal size in the analysis regimes; on the -hardest held-out corner (high correlation x low SNR) it is mildly elevated (~0.067 vs 0.05), a small -finite-sample effect. Evidence and caveats: appraisals/10_verdict.md. +hardest validation sample corner (high correlation x low SNR) it is mildly elevated (~0.067 vs 0.05), a small +finite-sample effect. Evidence and caveats: the documented validation evidence. References ---------- @@ -99,8 +99,8 @@ def mdi_plus_importance( Prefer MDI+ over MDI when features are noisy, high-cardinality, or mixed-type; it converges to MDI when features are orthogonal and high-SNR. This is the simplified faithful variant (not the full - leave-one-out paper algorithm). See the module docstring for the full regime tag and - appraisals/10_verdict.md. + leave-one-out paper algorithm). See the module docstring for the full usage guidance and + the documented validation evidence. Parameters ---------- @@ -195,7 +195,7 @@ def conditional_predictive_impact( Prefer CPI when you need a statistically valid test of whether a feature matters; it holds nominal size with good power (mildly elevated on the hardest high-correlation x low-SNR corner). See the - module docstring for the full regime tag and appraisals/10_verdict.md. + module docstring for the full usage guidance and the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/features/microstructural_features/edge.py b/src/RiskLabAI/features/microstructural_features/edge.py index 8366f0a..7d76a95 100644 --- a/src/RiskLabAI/features/microstructural_features/edge.py +++ b/src/RiskLabAI/features/microstructural_features/edge.py @@ -2,8 +2,7 @@ Implements the EDGE effective bid-ask spread estimator (Ardia, Guidotti & Kroencke 2024). -Admitted in Appraisal 03 (CONTRIBUTIONS_LEDGER row 1, 2026-06-26). Regime tag, -verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: Prefer EDGE over Roll and Abdi-Ranaldo for low-frequency spread estimation in all regimes; over Corwin-Schultz at small spreads (the edge narrows at very high @@ -15,7 +14,7 @@ never returning an invalid (negative) point estimate. The result is a proportional spread (0.01 is a 1% spread). Real-data confirmation is a logged follow-up pending an adequate public intraday / quote dataset (DECISION_LOG 2026-06-26). Evidence and -caveats: appraisals/03_verdict.md (03_results/RESULTS.md + HELDOUT.md). +caveats: the documented validation evidence (the documented validation results + independent validation). Reference: Ardia, D., Guidotti, E., & Kroencke, T. A. (2024). Efficient estimation of diff --git a/src/RiskLabAI/features/structural_breaks/__init__.py b/src/RiskLabAI/features/structural_breaks/__init__.py index 3e64182..ede729c 100644 --- a/src/RiskLabAI/features/structural_breaks/__init__.py +++ b/src/RiskLabAI/features/structural_breaks/__init__.py @@ -35,7 +35,7 @@ "get_gsadf_statistic", "get_bubble_episodes", "simulate_psy_critical_values", - # Appraisal 26 admits (alongside SADF/GSADF/CUSUM) + # independent validation includes (alongside SADF/GSADF/CUSUM) "volatility_robust_sadf", "pelt_change_points", ] diff --git a/src/RiskLabAI/features/structural_breaks/pelt_change_points.py b/src/RiskLabAI/features/structural_breaks/pelt_change_points.py index dbb9a2b..11dbbaf 100644 --- a/src/RiskLabAI/features/structural_breaks/pelt_change_points.py +++ b/src/RiskLabAI/features/structural_breaks/pelt_change_points.py @@ -7,15 +7,15 @@ (mean-and-variance) cost it detects and dates multiple and variance change-points that CUSUM misses, without over-segmenting when penalized appropriately. -Admitted in Appraisal 26 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: prefer it over CUSUM for detecting and dating multiple and/or variance change-points (which CUSUM misses), without over-segmenting; CUSUM remains adequate for a single mean shift. -Held-out confirmed (appraisals/26_results, HELDOUT.md): on the sealed mixed mean+variance corner (3 true +validation sample confirmed (the documented validation evidence, independent validation): on the sealed mixed mean+variance corner (3 true change-points) PELT recovers 2.65 of 3 on average with a 0.000 over-segmentation rate (it never exceeds the true count), where CUSUM recovers far fewer of these mean+variance change-points. The CUSUM baseline -is unchanged. Evidence: appraisals/26_verdict.md. +is unchanged. Evidence: the documented validation evidence. PELT is provided through the ``ruptures`` package (BSD-2-Clause), an optional dependency imported lazily; the GPL-3 ``exuber`` reference was not used. Install it with ``pip install ruptures``. @@ -47,8 +47,8 @@ def pelt_change_points( change-points that CUSUM misses, without over-segmenting under an adequate penalty. Prefer PELT over CUSUM for detecting and dating multiple and/or variance change-points; CUSUM remains - adequate for a single mean shift. See the module docstring for the verbatim regime tag and - appraisals/26_verdict.md. + adequate for a single mean shift. See the module docstring for the usage guidance and + the documented validation evidence. Parameters ---------- diff --git a/src/RiskLabAI/features/structural_breaks/structural_breaks.py b/src/RiskLabAI/features/structural_breaks/structural_breaks.py index 4ce4a5e..66318bb 100644 --- a/src/RiskLabAI/features/structural_breaks/structural_breaks.py +++ b/src/RiskLabAI/features/structural_breaks/structural_breaks.py @@ -298,13 +298,13 @@ def get_bsadf_statistic( # on the validated ``get_bsadf_statistic`` / ``get_expanding_window_adf`` above (and # agree with them to ~1e-9 on shared windows; see the structural-breaks tests). # -# Admitted in Appraisal 05 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim: +# Included after independent validation (the public method specification). Usage guidance: # Prefer GSADF/BSADF over single-window SADF when a series may contain more than # one explosive episode (it recovers and counts each). For a single suspected # bubble, SADF is at least as good. Use seasonally-adjusted data and the simulated # finite-sample critical values; GSADF over-flags strongly trending or seasonal # series and is mildly oversized in large samples, so treat broad flags cautiously. -# Evidence and caveats: appraisals/05_verdict.md. +# Evidence and caveats: the documented validation evidence. # ---------------------------------------------------------------------------------- @@ -502,7 +502,7 @@ def get_gsadf_statistic( SADF is at least as good. Use seasonally-adjusted data and the simulated finite-sample critical values; GSADF over-flags strongly trending or seasonal series and is mildly oversized in large samples, so treat broad flags cautiously. - (Admitted in Appraisal 05; see appraisals/05_verdict.md.) + (Included after independent validation; see the documented validation evidence.) Parameters ---------- diff --git a/src/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py b/src/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py index c8096a7..d61a34c 100644 --- a/src/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py +++ b/src/RiskLabAI/features/structural_breaks/volatility_robust_sadf.py @@ -4,21 +4,21 @@ The admitted SADF / GSADF bubble detector (`structural_breaks.get_sadf_sequence` / `get_gsadf_statistic`) uses a sup-ADF statistic whose null distribution assumes homoskedastic errors. Under non-stationary volatility the test over-rejects, flagging spurious bubbles (the mild oversizing -noted in Appraisal 05). The volatility-robust variant computes the SAME sup-ADF statistics but calibrates +noted during independent validation). The volatility-robust variant computes the SAME sup-ADF statistics but calibrates the critical values by a wild bootstrap (Rademacher sign-flip of the first-difference residuals), which preserves the series' own volatility pattern while destroying any explosive autocorrelation, restoring correct size under non-stationary volatility. -Admitted in Appraisal 26 (CONTRIBUTIONS_LEDGER 2026-06-27). Regime tag, verbatim from the verdict: +Included after independent validation (the public method specification). Usage guidance: prefer it over plain SADF/GSADF when the series' volatility may be non-stationary: it holds nominal size where plain SADF over-rejects ~9x, at a modest power cost, and converges to plain SADF under constant volatility. Pairs with the admitted GSADF/BSADF. -Held-out confirmed (appraisals/26_results, HELDOUT.md): on the sealed variance-break-6x bubble null the +validation sample confirmed (the documented validation evidence, independent validation): on the sealed variance-break-6x bubble null the volatility-robust SADF holds nominal size (~0.067) where plain SADF over-rejects (~0.667, the homoskedastic critical value flags spurious bubbles), and it matches plain SADF under constant volatility at a modest -power cost. The plain SADF / GSADF baseline is unchanged. Evidence: appraisals/26_verdict.md. +power cost. The plain SADF / GSADF baseline is unchanged. Evidence: the documented validation evidence. References ---------- @@ -63,7 +63,7 @@ def volatility_robust_sadf( Prefer this over plain SADF / GSADF when the series' volatility may be non-stationary: it holds nominal size where the plain test over-rejects, at a modest power cost, and converges to the plain test under constant volatility. It pairs with the admitted GSADF / BSADF (the same sup-ADF statistics). See the - module docstring for the verbatim regime tag and appraisals/26_verdict.md. + module docstring for the usage guidance and the documented validation evidence. Parameters ---------- diff --git a/test/backtest/test_advanced_bet_sizing.py b/test/backtest/test_advanced_bet_sizing.py index 231e8a2..92c1153 100644 --- a/test/backtest/test_advanced_bet_sizing.py +++ b/test/backtest/test_advanced_bet_sizing.py @@ -20,7 +20,7 @@ def test_kelly_fraction_closed_form(): def test_platt_reduces_ece_under_miscalibration(): """ - Replication of the Appraisal 18 mechanism: Platt calibration sharply lowers ECE on a miscalibrated + Replication of the independent validation mechanism: Platt calibration sharply lowers ECE on a miscalibrated probability (the high-miscalibration regime where it should help). """ rng = np.random.default_rng(0) @@ -36,7 +36,7 @@ def test_platt_reduces_ece_under_miscalibration(): def test_platt_is_safe_no_op_on_calibrated_input(): - """The verdict's no-op side: on already-calibrated probabilities Platt barely changes ECE.""" + """The documented guidance's no-op side: on already-calibrated probabilities Platt barely changes ECE.""" rng = np.random.default_rng(1) p_hat = rng.uniform(0.05, 0.95, 6000) y = (rng.uniform(size=p_hat.size) < p_hat).astype(int) # calibrated by construction @@ -63,7 +63,7 @@ def test_dr_kelly_converges_to_full_kelly_with_data(): def test_dr_kelly_lower_drawdown_proxy_than_full_kelly(): """ - Replication of the held-out mechanism direction: at small n DR-Kelly sizes strictly below full Kelly + Replication of the validation mechanism direction: at small n DR-Kelly sizes strictly below full Kelly (a smaller fraction is the lever for its lower from-initial drawdown). """ for p in (0.55, 0.62): diff --git a/test/backtest/test_ou_trading_rules.py b/test/backtest/test_ou_trading_rules.py index 578def4..8d7d9f8 100644 --- a/test/backtest/test_ou_trading_rules.py +++ b/test/backtest/test_ou_trading_rules.py @@ -61,7 +61,7 @@ def test_hit_probability_and_exit_time_sane(): def test_closed_form_matches_monte_carlo_grid_within_resolution(): """ - Replication of the Appraisal 23 mechanism: where the price is OU, the closed-form optimal profit-take + Replication of the independent validation mechanism: where the price is OU, the closed-form optimal profit-take agrees with the Monte-Carlo grid argmax within the grid spacing. """ theta, sigma, gap, cost, dt = theta_from_half_life(10.0), 0.10, 1.0, 0.0, 1.0 diff --git a/test/backtest/test_robust_statistics.py b/test/backtest/test_robust_statistics.py index aac972d..18a88ad 100644 --- a/test/backtest/test_robust_statistics.py +++ b/test/backtest/test_robust_statistics.py @@ -32,7 +32,7 @@ def test_ced_in_unit_interval_and_monotone_in_alpha(): def test_ced_lower_estimator_variance_than_maxdrawdown(): """ - Replication of the Appraisal 22 mechanism: on short heavy-tailed tracks CED (a tail mean of the + Replication of the independent validation mechanism: on short heavy-tailed tracks CED (a tail mean of the max-drawdown distribution) has lower across-seed estimator variance than max-drawdown (a single extreme order statistic). """ @@ -75,7 +75,7 @@ def _ar_pair(rng, n, phi, scale=0.01): def test_lw_holds_size_where_naive_overrejects_under_dependence(): """ - Replication of the held-out mechanism: under AR(0.3) the naive Sharpe-difference test over-rejects + Replication of the validation mechanism: under AR(0.3) the naive Sharpe-difference test over-rejects the (true) null while the Ledoit-Wolf test holds roughly nominal size. """ n_sims = 80 diff --git a/test/backtest/validation/test_leakage_aware_hpo.py b/test/backtest/validation/test_leakage_aware_hpo.py index 1f46a91..a239a71 100644 --- a/test/backtest/validation/test_leakage_aware_hpo.py +++ b/test/backtest/validation/test_leakage_aware_hpo.py @@ -14,7 +14,7 @@ def test_gate_erases_no_edge_series_at_high_trial_count(): """ - Replication of the Appraisal 20 honest finding: a no-edge out-of-sample return series does NOT pass + Replication of the independent validation honest finding: a no-edge out-of-sample return series does NOT pass the Deflated-Sharpe gate once it is deflated by a large HPO trial count (tuning yields no edge). """ rng = np.random.default_rng(0) diff --git a/test/backtest/validation/test_path_adaptive_cpcv.py b/test/backtest/validation/test_path_adaptive_cpcv.py index c5b03cb..fc8b55f 100644 --- a/test/backtest/validation/test_path_adaptive_cpcv.py +++ b/test/backtest/validation/test_path_adaptive_cpcv.py @@ -1,7 +1,7 @@ """ Tests for backtest/validation/path_adaptive_cpcv.py (path-level Adaptive CPCV PBO). -Covers: plain-CPCV PBO parity with the repo (diff 0), the Appraisal-09b replication (lower selection +Covers: plain-CPCV PBO parity with the repo (diff 0), the independent validation replication (lower selection error than plain CPCV on a signal-bearing switching set; convergence with no over-adaptation on a stationary set), and edge cases (single config, degenerate volatility, too few paths). """ @@ -43,10 +43,10 @@ def _switching_panel(seed, t_len=480, n_strategies=8, target=0.25): return perf, n_strategies - 1 -# Appraisal-09b Monte-Carlo switching DGP (clean-room from harness.py): K configs with known true +# independent validation Monte-Carlo switching DGP (clean-room from harness.py): K configs with known true # per-regime Sharpes; the forward regime (regime 1, the recent half) is a higher-volatility stress # regime, so the shift is observable from volatility. Selection error is the true forward-Sharpe -# shortfall of the selected config, the continuous metric the appraisal scored. +# shortfall of the selected config, the continuous metric the independent validation scored. def _switching_truth_panel(seed, t_len=960, n_configs=12, spread=0.12): rng = np.random.default_rng(seed) sr_uncond = np.linspace(-spread, spread, n_configs).copy() @@ -94,9 +94,9 @@ def test_uniform_weights_converge_to_plain_pbo(): assert np.isclose(adaptive_pbo, plain_pbo, atol=1e-12) -# ------------------------------------------------------------- Appraisal-09b mechanism replication +# ------------------------------------------------------------- independent validation mechanism replication def test_lower_selection_error_on_switching_regime(): - """Replicates 09b on its Monte-Carlo switching DGP: under a signal-bearing, volatility-observable + """Replicates follow-up validation on its Monte-Carlo switching DGP: under a signal-bearing, volatility-observable regime shift, the regime-weighted selection has a lower mean true-forward-Sharpe shortfall than plain CPCV (which targets the unconditional best), and it is essentially never worse path by path. """ diff --git a/test/backtest/validation/test_path_bagged_cpcv.py b/test/backtest/validation/test_path_bagged_cpcv.py index 8cffa63..085109c 100644 --- a/test/backtest/validation/test_path_bagged_cpcv.py +++ b/test/backtest/validation/test_path_bagged_cpcv.py @@ -14,7 +14,7 @@ def _noisy_panel(seed, t_len=240, n_strategies=12): - """A noisy strategy panel with a faint mean spread (the appraisal-style controlled setup).""" + """A noisy strategy panel with a faint mean spread (the independent validation-style controlled setup).""" rng = np.random.default_rng(seed) means = np.linspace(-0.04, 0.04, n_strategies) return rng.standard_normal((t_len, n_strategies)) + means @@ -40,7 +40,7 @@ def test_bagged_pbo_in_unit_interval_and_shapes(): def test_bagging_reduces_variance_vs_plain_pbo(): """ The core mechanism: across independent panels the bagged PBO has lower variance than the single - plain CSCV PBO (replication of the appraisal's variance-reduction finding). + plain CSCV PBO (replication of the independent validation's variance-reduction finding). """ plain, bagged = [], [] for seed in range(40): @@ -74,7 +74,7 @@ def _true_pbo(true_sharpes, t_len, n_partitions, n_sims): def test_bagged_pbo_closer_to_truth_than_plain(): """ - Replication of the appraisal's headline: the bagged PBO has lower estimation error against the + Replication of the independent validation's headline: the bagged PBO has lower estimation error against the known true PBO than the single plain CSCV PBO (variance reduction -> lower error). """ true_sharpes = np.linspace( diff --git a/test/causal_factor_analysis/fixtures/additive_numeric_parity.tsv b/test/causal_factor_analysis/fixtures/additive_numeric_parity.tsv new file mode 100644 index 0000000..09b5877 --- /dev/null +++ b/test/causal_factor_analysis/fixtures/additive_numeric_parity.tsv @@ -0,0 +1,14 @@ +case_id method arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 expected1 expected2 expected3 +mirage_confounder_general generalized_confounder 2 3 4 5 6 2.697674418604651 +mirage_collider_general generalized_collider 2 3 4 5 6 -0.9411764705882353 0.29411764705882354 +fdr_single_source_b single_trial_fdr 0.024997895148220373 0.9515427737332771 0.15 0.08344067427559433 +fdr_max_source_a max_selection_errors 0.024997895148220373 0.9515427737332771 0.95 10 0.5987369392383787 0.22365361940347483 0.7531960884251255 +fdr_max_sensitivity max_selection_errors 0.05 0.9 0.9 5 0.5904900000000001 0.2262190625000002 0.7245771630882027 +maximum_mixture_simple maximum_mixture_cdf 0.4 0.8 0.25 3 0.343 +conditional_tail_simple conditional_upper_tail 0.75 0.25 0.3333333333333333 +gaussian_identical_cdf gaussian_max_cdf 0.37 1 0 1 0 3 0.125 +gaussian_identical_log_density gaussian_max_log_density 0.37 1 0 1 0 3 -1.2066206056564532 +nonidentification_simple nonidentification_witness 0.36 0.49 0.7 0.6 0.49 +selection_exponential selection_exponential 0.4 0.63 5 0.9961054013913372 0.6275464028765424 0.63 +allocation_two_asset allocation_two_asset 0.5 -0.5 -1 + diff --git a/test/causal_factor_analysis/test_additive_numeric_parity.py b/test/causal_factor_analysis/test_additive_numeric_parity.py new file mode 100644 index 0000000..e414ad4 --- /dev/null +++ b/test/causal_factor_analysis/test_additive_numeric_parity.py @@ -0,0 +1,189 @@ +from __future__ import annotations + +import csv +import math +from pathlib import Path + +import numpy as np +import pytest + +from RiskLabAI.causal_factor_analysis import ( + GaussianTrialMixture, + allocation_misspecification_diagnostics, + conditional_upper_tail_probability, + fdr_nonidentification_witness, + gaussian_max_selection_cdf, + gaussian_max_selection_log_density, + generalized_collider_overcontrolled_coefficients, + generalized_confounder_undercontrolled_coefficient, + max_selection_family_errors, + max_selection_null_probability, + maximum_mixture_cdf, + single_trial_false_discovery_rate, +) + +_FIXTURE_PATH = Path(__file__).with_name("fixtures") / "additive_numeric_parity.tsv" + + +def _load_cases() -> list[dict[str, str]]: + with _FIXTURE_PATH.open(encoding="utf-8", newline="") as stream: + cases = list(csv.DictReader(stream, delimiter="\t")) + assert [case["case_id"] for case in cases] == [ + "mirage_confounder_general", + "mirage_collider_general", + "fdr_single_source_b", + "fdr_max_source_a", + "fdr_max_sensitivity", + "maximum_mixture_simple", + "conditional_tail_simple", + "gaussian_identical_cdf", + "gaussian_identical_log_density", + "nonidentification_simple", + "selection_exponential", + "allocation_two_asset", + ] + return cases + + +def _number(case: dict[str, str], name: str) -> float: + value = case[name] + assert value + return float(value) + + +@pytest.mark.parametrize("case", _load_cases(), ids=lambda case: case["case_id"]) +def test_additive_numeric_cross_language_fixture(case: dict[str, str]) -> None: + method = case["method"] + if method == "generalized_confounder": + actual = generalized_confounder_undercontrolled_coefficient( + _number(case, "arg1"), + _number(case, "arg2"), + _number(case, "arg3"), + confounder_variance=_number(case, "arg4"), + exposure_noise_variance=_number(case, "arg5"), + ) + assert actual == pytest.approx(_number(case, "expected1"), abs=1e-15) + elif method == "generalized_collider": + actual = generalized_collider_overcontrolled_coefficients( + _number(case, "arg1"), + _number(case, "arg2"), + _number(case, "arg3"), + outcome_noise_variance=_number(case, "arg4"), + collider_noise_variance=_number(case, "arg5"), + ) + assert actual.beta_hat == pytest.approx(_number(case, "expected1"), abs=1e-15) + assert actual.theta_hat == pytest.approx(_number(case, "expected2"), abs=1e-15) + elif method == "single_trial_fdr": + actual = single_trial_false_discovery_rate( + _number(case, "arg1"), + _number(case, "arg2"), + _number(case, "arg3"), + ) + assert actual == pytest.approx(_number(case, "expected1"), abs=2e-15) + elif method == "max_selection_errors": + actual = max_selection_family_errors( + _number(case, "arg1"), + _number(case, "arg2"), + _number(case, "arg3"), + int(_number(case, "arg4")), + ) + assert actual.family_null_probability == pytest.approx( + _number(case, "expected1"), abs=2e-15 + ) + assert actual.family_type_i_error == pytest.approx( + _number(case, "expected2"), abs=2e-15 + ) + assert actual.family_type_ii_error == pytest.approx( + _number(case, "expected3"), abs=2e-14 + ) + elif method == "maximum_mixture_cdf": + actual = maximum_mixture_cdf( + _number(case, "arg1"), + _number(case, "arg2"), + _number(case, "arg3"), + int(_number(case, "arg4")), + ) + assert actual == pytest.approx(_number(case, "expected1"), abs=1e-15) + elif method == "conditional_upper_tail": + actual = conditional_upper_tail_probability( + _number(case, "arg1"), _number(case, "arg2") + ) + assert actual == pytest.approx(_number(case, "expected1"), abs=1e-15) + elif method in {"gaussian_max_cdf", "gaussian_max_log_density"}: + model = GaussianTrialMixture( + _number(case, "arg1"), + _number(case, "arg2"), + _number(case, "arg3"), + _number(case, "arg4"), + ) + evaluator = ( + gaussian_max_selection_cdf + if method == "gaussian_max_cdf" + else gaussian_max_selection_log_density + ) + actual = evaluator(model, _number(case, "arg5"), int(_number(case, "arg6"))) + assert actual == pytest.approx(_number(case, "expected1"), abs=2e-15) + elif method == "nonidentification_witness": + actual = fdr_nonidentification_witness( + _number(case, "arg1"), _number(case, "arg2") + ) + assert actual.trial_null_probability == pytest.approx( + _number(case, "expected1"), abs=1e-15 + ) + assert actual.latent_trial_cdf_at_threshold == pytest.approx( + _number(case, "expected2"), abs=1e-15 + ) + assert actual.family_false_discovery_rate == pytest.approx( + _number(case, "expected3"), abs=1e-15 + ) + elif method == "selection_exponential": + threshold = _number(case, "arg1") + + def density(value: float) -> float: + return math.exp(-value) if value >= 0.0 else 0.0 + + def distribution(value: float) -> float: + return 1.0 - math.exp(-value) if value > 0.0 else 0.0 + + actual = max_selection_null_probability( + threshold, + _number(case, "arg2"), + int(_number(case, "arg3")), + null_density=density, + mixture_cdf=distribution, + ) + assert actual.selection_probability == pytest.approx( + _number(case, "expected1"), abs=2e-14 + ) + assert actual.joint_null_and_selection_probability == pytest.approx( + _number(case, "expected2"), abs=2e-11 + ) + assert actual.conditional_null_probability == pytest.approx( + _number(case, "expected3"), abs=2e-11 + ) + elif method == "allocation_two_asset": + actual = allocation_misspecification_diagnostics( + np.eye(2), + np.array([[1.0], [1.0]]), + np.array([1.0]), + np.array([[1.0], [-1.0]]), + np.array([1.0]), + ) + np.testing.assert_allclose( + actual.reference_weights, + [_number(case, "expected1"), _number(case, "expected1")], + atol=1e-14, + ) + np.testing.assert_allclose( + actual.misspecified_weights, + [_number(case, "expected1"), _number(case, "expected2")], + atol=1e-14, + ) + np.testing.assert_allclose( + actual.reference_exposure_error, + [_number(case, "expected3")], + atol=1e-14, + ) + assert actual.weight_sign_reversals.tolist() == [False, True] + else: + raise AssertionError(f"unhandled parity method: {method}") diff --git a/test/causal_factor_analysis/test_allocation_diagnostics.py b/test/causal_factor_analysis/test_allocation_diagnostics.py new file mode 100644 index 0000000..bb044e2 --- /dev/null +++ b/test/causal_factor_analysis/test_allocation_diagnostics.py @@ -0,0 +1,242 @@ +"""Independent tests for generic allocation-misspecification diagnostics.""" + +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import numpy as np +import pytest + +from RiskLabAI.causal_factor_analysis.allocation_diagnostics import ( + AllocationMisspecificationDiagnostics, + allocation_misspecification_diagnostics, +) + + +def _kkt_weights( + covariance: np.ndarray, + factor_exposures: np.ndarray, + target_exposures: np.ndarray, +) -> np.ndarray: + """Solve the first-order conditions independently as one block system.""" + n_assets, n_factors = factor_exposures.shape + system = np.block( + [ + [2.0 * covariance, factor_exposures], + [factor_exposures.T, np.zeros((n_factors, n_factors))], + ] + ) + right_hand_side = np.concatenate( + [np.zeros(n_assets, dtype=np.float64), target_exposures] + ) + return np.linalg.solve(system, right_hand_side)[:n_assets] + + +def _example_inputs(): + covariance = np.diag([1.0, 2.0, 4.0]) + reference_factor_exposures = np.array([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]) + reference_target_exposures = np.array([0.0, 1.0]) + misspecified_factor_exposures = np.array([[1.0], [1.0], [-2.0]]) + misspecified_target_exposures = np.array([1.0]) + return ( + covariance, + reference_factor_exposures, + reference_target_exposures, + misspecified_factor_exposures, + misspecified_target_exposures, + ) + + +def test_diagnostics_match_independent_kkt_and_matrix_oracles(): + inputs = _example_inputs() + ( + covariance, + reference_exposures, + reference_target, + misspecified_exposures, + misspecified_target, + ) = inputs + result = allocation_misspecification_diagnostics(*inputs) + + reference_weights = _kkt_weights(covariance, reference_exposures, reference_target) + misspecified_weights = _kkt_weights( + covariance, misspecified_exposures, misspecified_target + ) + reference_achieved = reference_exposures.T @ reference_weights + misspecified_reference = reference_exposures.T @ misspecified_weights + misspecified_model_achieved = misspecified_exposures.T @ misspecified_weights + + assert isinstance(result, AllocationMisspecificationDiagnostics) + np.testing.assert_allclose(result.reference_weights, reference_weights, atol=2e-15) + np.testing.assert_allclose( + result.misspecified_weights, misspecified_weights, atol=2e-15 + ) + np.testing.assert_allclose( + result.reference_target_exposures, reference_target, atol=0.0 + ) + np.testing.assert_allclose( + result.reference_achieved_exposures, reference_achieved, atol=2e-15 + ) + np.testing.assert_allclose( + result.misspecified_reference_exposures, + misspecified_reference, + atol=2e-15, + ) + np.testing.assert_allclose( + result.misspecified_model_target_exposures, + misspecified_target, + atol=0.0, + ) + np.testing.assert_allclose( + result.misspecified_model_achieved_exposures, + misspecified_model_achieved, + atol=2e-15, + ) + np.testing.assert_allclose( + result.reference_exposure_error, + misspecified_reference - reference_target, + atol=2e-15, + ) + np.testing.assert_array_equal( + result.weight_sign_reversals, np.array([True, False, True]) + ) + assert result.reference_variance == pytest.approx( + reference_weights @ covariance @ reference_weights, abs=2e-15 + ) + assert result.misspecified_variance == pytest.approx( + misspecified_weights @ covariance @ misspecified_weights, abs=2e-15 + ) + + +def test_each_allocation_satisfies_only_its_declared_constraints(): + result = allocation_misspecification_diagnostics(*_example_inputs()) + + np.testing.assert_allclose( + result.reference_achieved_exposures, + result.reference_target_exposures, + atol=2e-15, + ) + np.testing.assert_allclose( + result.misspecified_model_achieved_exposures, + result.misspecified_model_target_exposures, + atol=2e-15, + ) + assert not np.allclose( + result.misspecified_reference_exposures, + result.reference_target_exposures, + atol=1e-12, + ) + np.testing.assert_allclose(result.reference_exposure_error, [0.2, -1.0]) + + +def test_sign_reversal_is_strict_and_zero_is_not_negative(): + covariance = np.eye(2) + exposures = np.eye(2) + + strict = allocation_misspecification_diagnostics( + covariance, exposures, [1.0, 0.0], exposures, [-1.0, 0.0] + ) + zero_boundary = allocation_misspecification_diagnostics( + covariance, exposures, [0.0, 1.0], exposures, [-1.0, 1.0] + ) + + np.testing.assert_array_equal(strict.weight_sign_reversals, [True, False]) + np.testing.assert_array_equal(zero_boundary.weight_sign_reversals, [False, False]) + + +def test_covariance_unit_change_preserves_weights_and_scales_variances(): + inputs = _example_inputs() + baseline = allocation_misspecification_diagnostics(*inputs) + rescaled = allocation_misspecification_diagnostics(7.5 * inputs[0], *inputs[1:]) + + np.testing.assert_allclose(rescaled.reference_weights, baseline.reference_weights) + np.testing.assert_allclose( + rescaled.misspecified_weights, baseline.misspecified_weights + ) + assert rescaled.reference_variance == pytest.approx( + 7.5 * baseline.reference_variance, rel=2e-15 + ) + assert rescaled.misspecified_variance == pytest.approx( + 7.5 * baseline.misspecified_variance, rel=2e-15 + ) + + +def test_result_arrays_are_independent_read_only_snapshots(): + inputs = tuple(value.copy() for value in _example_inputs()) + result = allocation_misspecification_diagnostics(*inputs) + snapshots = { + name: getattr(result, name).copy() + for name in ( + "reference_weights", + "misspecified_weights", + "reference_target_exposures", + "reference_achieved_exposures", + "misspecified_reference_exposures", + "misspecified_model_target_exposures", + "misspecified_model_achieved_exposures", + "reference_exposure_error", + "weight_sign_reversals", + ) + } + + for value in inputs: + value[...] = 99.0 + + for name, expected in snapshots.items(): + observed = getattr(result, name) + np.testing.assert_array_equal(observed, expected) + assert not observed.flags.writeable + with pytest.raises(ValueError, match="read-only"): + observed[0] = observed[0] + + +def test_result_dataclass_is_frozen_and_array_equality_is_not_implicit(): + result = allocation_misspecification_diagnostics(*_example_inputs()) + duplicate = allocation_misspecification_diagnostics(*_example_inputs()) + + with pytest.raises(FrozenInstanceError): + result.reference_variance = 0.0 + assert result is not duplicate + assert result != duplicate + + +@pytest.mark.parametrize( + "replacement_index,replacement", + [ + (0, np.array([[True, False], [False, True]])), + (0, np.array([[1.0, 0.0], [0.0, np.nan]])), + (0, np.array([[1.0, 1.0], [1.0, 1.0]])), + (1, np.array([[1.0 + 1.0j, 0.0], [0.0, 1.0], [1.0, 1.0]])), + (1, np.ones((2, 2))), + (2, np.ones((2, 1))), + (3, np.ones((3, 4))), + (4, np.ones((2,))), + ], +) +def test_invalid_inputs_fail_at_the_public_boundary(replacement_index, replacement): + inputs = list(_example_inputs()) + inputs[replacement_index] = replacement + with pytest.raises(ValueError): + allocation_misspecification_diagnostics(*inputs) + + +def test_redundant_exposure_constraints_are_rejected(): + covariance, reference, target, misspecified, misspecified_target = _example_inputs() + redundant_reference = np.column_stack([reference[:, 0], reference[:, 0]]) + + with pytest.raises(ValueError, match="full column rank"): + allocation_misspecification_diagnostics( + covariance, + redundant_reference, + target, + misspecified, + misspecified_target, + ) + + +def test_input_arrays_are_not_mutated(): + inputs = _example_inputs() + snapshots = tuple(value.copy() for value in inputs) + allocation_misspecification_diagnostics(*inputs) + for observed, expected in zip(inputs, snapshots): + np.testing.assert_array_equal(observed, expected) diff --git a/test/causal_factor_analysis/test_generalized_factor_mirage.py b/test/causal_factor_analysis/test_generalized_factor_mirage.py new file mode 100644 index 0000000..4a44e4e --- /dev/null +++ b/test/causal_factor_analysis/test_generalized_factor_mirage.py @@ -0,0 +1,293 @@ +"""Independent tests for the general-variance factor-mirage coefficients.""" + +from __future__ import annotations + +import inspect +from fractions import Fraction + +import numpy as np +import pytest + +from RiskLabAI.causal_factor_analysis.factor_mirage import ( + ColliderCoefficients, + collider_overcontrolled_coefficients, + confounder_undercontrolled_coefficient, + generalized_collider_overcontrolled_coefficients, + generalized_confounder_undercontrolled_coefficient, +) + + +def _fraction(value: float) -> Fraction: + return Fraction.from_float(float(value)) + + +def _confounder_fraction_oracle( + beta: float, + gamma: float, + delta: float, + confounder_variance: float, + exposure_noise_variance: float, +) -> float: + beta_q = _fraction(beta) + gamma_q = _fraction(gamma) + delta_q = _fraction(delta) + confounder_q = _fraction(confounder_variance) + noise_q = _fraction(exposure_noise_variance) + exposure_variance = delta_q**2 * confounder_q + noise_q + covariance = beta_q * exposure_variance + gamma_q * delta_q * confounder_q + return float(covariance / exposure_variance) + + +def _collider_normal_equation_oracle( + beta: float, + gamma: float, + delta: float, + outcome_noise_variance: float, + collider_noise_variance: float, +) -> np.ndarray: + collider_loading = gamma * beta + delta + regressor_covariance = np.array( + [ + [1.0, collider_loading], + [ + collider_loading, + collider_loading**2 + + gamma**2 * outcome_noise_variance + + collider_noise_variance, + ], + ] + ) + outcome_covariance = np.array( + [ + beta, + collider_loading * beta + gamma * outcome_noise_variance, + ] + ) + return np.linalg.solve(regressor_covariance, outcome_covariance) + + +def test_released_standardized_signatures_are_unchanged(): + confounder = inspect.signature(confounder_undercontrolled_coefficient) + collider = inspect.signature(collider_overcontrolled_coefficients) + + assert tuple(confounder.parameters) == ("beta", "gamma", "delta") + assert tuple(collider.parameters) == ("beta", "gamma", "delta") + assert all( + parameter.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD + for parameter in confounder.parameters.values() + ) + assert all( + parameter.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD + for parameter in collider.parameters.values() + ) + + +@pytest.mark.parametrize( + ( + "beta", + "gamma", + "delta", + "confounder_variance", + "exposure_noise_variance", + ), + [ + (0.75, -1.25, 0.5, 2.0, 3.0), + (-0.4, 2.5, -0.75, 0.25, 4.0), + (1.5, 0.0, 8.0, 3.0, 0.5), + (0.0, -3.0, 0.0, 2.0, 7.0), + ], +) +def test_generalized_confounder_matches_covariance_ratio( + beta, + gamma, + delta, + confounder_variance, + exposure_noise_variance, +): + expected = _confounder_fraction_oracle( + beta, + gamma, + delta, + confounder_variance, + exposure_noise_variance, + ) + actual = generalized_confounder_undercontrolled_coefficient( + beta, + gamma, + delta, + confounder_variance=confounder_variance, + exposure_noise_variance=exposure_noise_variance, + ) + assert actual == expected + + +@pytest.mark.parametrize( + "beta,gamma,delta", + [(1.0, 3.0, 1.0), (-0.7, 1.2, -0.4), (2.5, 0.0, -8.0)], +) +def test_generalized_confounder_unit_variances_equal_released_result( + beta, gamma, delta +): + assert generalized_confounder_undercontrolled_coefficient( + beta, + gamma, + delta, + confounder_variance=1.0, + exposure_noise_variance=1.0, + ) == confounder_undercontrolled_coefficient(beta, gamma, delta) + + +def test_generalized_confounder_is_invariant_to_common_variance_units(): + baseline = generalized_confounder_undercontrolled_coefficient( + 0.8, + -1.4, + 0.6, + confounder_variance=2.5, + exposure_noise_variance=0.75, + ) + rescaled = generalized_confounder_undercontrolled_coefficient( + 0.8, + -1.4, + 0.6, + confounder_variance=25.0, + exposure_noise_variance=7.5, + ) + assert rescaled == baseline + + +@pytest.mark.parametrize( + ( + "beta", + "gamma", + "delta", + "outcome_noise_variance", + "collider_noise_variance", + ), + [ + (0.8, 1.2, -0.4, 2.0, 3.0), + (-0.5, 0.75, 2.0, 0.25, 4.0), + (2.0, -1.0, -0.25, 3.5, 0.5), + (1.25, 0.0, 7.0, 5.0, 2.0), + ], +) +def test_generalized_collider_matches_independent_normal_equations( + beta, + gamma, + delta, + outcome_noise_variance, + collider_noise_variance, +): + expected = _collider_normal_equation_oracle( + beta, + gamma, + delta, + outcome_noise_variance, + collider_noise_variance, + ) + actual = generalized_collider_overcontrolled_coefficients( + beta, + gamma, + delta, + outcome_noise_variance=outcome_noise_variance, + collider_noise_variance=collider_noise_variance, + ) + assert actual.beta_hat == pytest.approx(expected[0], rel=2e-14, abs=2e-14) + assert actual.theta_hat == pytest.approx(expected[1], rel=2e-14, abs=2e-14) + + +@pytest.mark.parametrize( + "beta,gamma,delta", + [(0.5, 1.0, 0.8), (1.0, -2.0, 0.25), (-0.7, 0.4, -1.2)], +) +def test_generalized_collider_unit_variances_equal_released_result(beta, gamma, delta): + assert generalized_collider_overcontrolled_coefficients( + beta, + gamma, + delta, + outcome_noise_variance=1.0, + collider_noise_variance=1.0, + ) == collider_overcontrolled_coefficients(beta, gamma, delta) + + +def test_generalized_collider_is_invariant_to_common_variance_units(): + baseline = generalized_collider_overcontrolled_coefficients( + 0.8, + 1.2, + -0.4, + outcome_noise_variance=2.0, + collider_noise_variance=3.0, + ) + rescaled = generalized_collider_overcontrolled_coefficients( + 0.8, + 1.2, + -0.4, + outcome_noise_variance=20.0, + collider_noise_variance=30.0, + ) + assert rescaled == baseline + + +def test_zero_collider_link_has_the_expected_general_variance_limit(): + result = generalized_collider_overcontrolled_coefficients( + 0.7, + 0.0, + 4.0, + outcome_noise_variance=8.0, + collider_noise_variance=0.5, + ) + assert result == ColliderCoefficients(beta_hat=0.7, theta_hat=0.0) + + +@pytest.mark.parametrize( + "function,keywords", + [ + ( + generalized_confounder_undercontrolled_coefficient, + {"confounder_variance": 0.0, "exposure_noise_variance": 1.0}, + ), + ( + generalized_confounder_undercontrolled_coefficient, + {"confounder_variance": 1.0, "exposure_noise_variance": -1.0}, + ), + ( + generalized_collider_overcontrolled_coefficients, + {"outcome_noise_variance": 0.0, "collider_noise_variance": 1.0}, + ), + ( + generalized_collider_overcontrolled_coefficients, + {"outcome_noise_variance": 1.0, "collider_noise_variance": -1.0}, + ), + ], +) +def test_generalized_coefficients_reject_nonpositive_variances(function, keywords): + with pytest.raises(ValueError, match="strictly positive"): + function(0.5, 1.0, -0.25, **keywords) + + +@pytest.mark.parametrize("invalid", [True, np.nan, np.inf, 1.0 + 2.0j, "1"]) +def test_generalized_coefficients_reject_invalid_scalars(invalid): + with pytest.raises(ValueError): + generalized_confounder_undercontrolled_coefficient( + invalid, + 1.0, + 0.5, + confounder_variance=1.0, + exposure_noise_variance=1.0, + ) + with pytest.raises(ValueError): + generalized_collider_overcontrolled_coefficients( + 0.5, + 1.0, + 0.5, + outcome_noise_variance=invalid, + collider_noise_variance=1.0, + ) + + +def test_generalized_variances_are_required_keyword_only_arguments(): + with pytest.raises(TypeError): + generalized_confounder_undercontrolled_coefficient(1.0, 2.0, 3.0) + with pytest.raises(TypeError): + generalized_collider_overcontrolled_coefficients(1.0, 2.0, 3.0) + with pytest.raises(TypeError): + generalized_confounder_undercontrolled_coefficient(1.0, 2.0, 3.0, 1.0, 1.0) diff --git a/test/causal_factor_analysis/test_graph_roles.py b/test/causal_factor_analysis/test_graph_roles.py new file mode 100644 index 0000000..0389b66 --- /dev/null +++ b/test/causal_factor_analysis/test_graph_roles.py @@ -0,0 +1,157 @@ +"""Independent tests for accepted-DAG and one-hop graph roles.""" + +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from RiskLabAI.causal_factor_analysis.graph_identification import CausalDAG +from RiskLabAI.causal_factor_analysis.graph_roles import ( + FactorControlRoles, + TreatmentOutcomeRole, + TreatmentOutcomeRoleEvidence, + classify_treatment_outcome_role, + factor_control_roles, +) + + +def _dag(edges=(), *, nodes=("N", "X", "Y"), observed=None): + if observed is None: + observed = nodes + return CausalDAG(nodes, edges, observed) + + +def test_factor_control_roles_partition_observed_and_report_latent_reachability(): + graph = CausalDAG( + ("A", "D", "L_A", "L_D", "O", "T"), + (("A", "T"), ("D", "L_D"), ("L_A", "A"), ("T", "D")), + ("A", "D", "O", "T"), + ) + + roles = factor_control_roles(graph, "T") + + assert roles == FactorControlRoles( + target_factor="T", + ancestor_factors=("A",), + descendant_factors=("D",), + other_factors=("O",), + unobserved_ancestors=("L_A",), + unobserved_descendants=("L_D",), + ) + assert not hasattr(roles, "__dict__") + with pytest.raises(FrozenInstanceError): + roles.target_factor = "O" + + +def test_factor_control_roles_is_invariant_to_declaration_order(): + edges = (("Z", "T"), ("T", "B"), ("A", "Z")) + first = CausalDAG(("A", "B", "T", "X", "Z"), edges, ("A", "B", "T", "X", "Z")) + second = CausalDAG( + ("X", "T", "Z", "B", "A"), + tuple(reversed(edges)), + ("Z", "X", "T", "B", "A"), + ) + assert factor_control_roles(first, "T") == factor_control_roles(second, "T") + + +@pytest.mark.parametrize( + "edges,role,signature", + [ + ((("N", "X"),), TreatmentOutcomeRole.CAUSE_OF_TREATMENT, (1, 0, 0, 0)), + ((("X", "N"),), TreatmentOutcomeRole.CONSEQUENCE_OF_TREATMENT, (0, 1, 0, 0)), + ((("N", "Y"),), TreatmentOutcomeRole.CAUSE_OF_OUTCOME, (0, 0, 1, 0)), + ((("Y", "N"),), TreatmentOutcomeRole.CONSEQUENCE_OF_OUTCOME, (0, 0, 0, 1)), + ( + (("N", "X"), ("N", "Y")), + TreatmentOutcomeRole.CONFOUNDER, + (1, 0, 1, 0), + ), + ( + (("X", "N"), ("Y", "N")), + TreatmentOutcomeRole.COLLIDER, + (0, 1, 0, 1), + ), + ( + (("X", "N"), ("N", "Y")), + TreatmentOutcomeRole.MEDIATOR, + (0, 1, 1, 0), + ), + ((), TreatmentOutcomeRole.INDEPENDENT, (0, 0, 0, 0)), + ], +) +def test_all_eight_one_hop_signatures(edges, role, signature): + evidence = classify_treatment_outcome_role(_dag(edges), "X", "Y", "N") + + assert isinstance(evidence, TreatmentOutcomeRoleEvidence) + assert evidence.node == "N" + assert evidence.treatment == "X" + assert evidence.outcome == "Y" + assert evidence.role is role + assert ( + evidence.node_to_treatment, + evidence.treatment_to_node, + evidence.node_to_outcome, + evidence.outcome_to_node, + ) == tuple(bool(value) for value in signature) + + +def test_role_classifier_rejects_reverse_mediation(): + graph = _dag((("N", "X"), ("Y", "N"))) + with pytest.raises(ValueError, match="eight admitted roles"): + classify_treatment_outcome_role(graph, "X", "Y", "N") + + +def test_role_classifier_uses_direct_edges_only(): + graph = CausalDAG( + ("A", "N", "X", "Y"), + (("N", "A"), ("A", "X"), ("N", "Y")), + ("A", "N", "X", "Y"), + ) + evidence = classify_treatment_outcome_role(graph, "X", "Y", "N") + assert evidence.role is TreatmentOutcomeRole.CAUSE_OF_OUTCOME + assert not evidence.node_to_treatment + + +@pytest.mark.parametrize( + "operation,exception", + [ + (lambda: factor_control_roles(object(), "X"), TypeError), + (lambda: factor_control_roles(_dag(), 1), TypeError), + (lambda: factor_control_roles(_dag(), "missing"), ValueError), + ( + lambda: factor_control_roles( + _dag(nodes=("L", "N", "X", "Y"), observed=("N", "X", "Y")), + "L", + ), + ValueError, + ), + (lambda: classify_treatment_outcome_role(_dag(), "X", "X", "N"), ValueError), + (lambda: classify_treatment_outcome_role(_dag(), "X", "Y", 1), TypeError), + ( + lambda: classify_treatment_outcome_role( + _dag(nodes=("L", "N", "X", "Y"), observed=("N", "X", "Y")), + "X", + "Y", + "L", + ), + ValueError, + ), + ], +) +def test_graph_role_boundary_validation(operation, exception): + with pytest.raises(exception): + operation() + + +def test_treatment_outcome_role_values_are_frozen(): + assert [(role.name, role.value) for role in TreatmentOutcomeRole] == [ + ("CAUSE_OF_TREATMENT", "cause_of_treatment"), + ("CONSEQUENCE_OF_TREATMENT", "consequence_of_treatment"), + ("CAUSE_OF_OUTCOME", "cause_of_outcome"), + ("CONSEQUENCE_OF_OUTCOME", "consequence_of_outcome"), + ("CONFOUNDER", "confounder"), + ("COLLIDER", "collider"), + ("MEDIATOR", "mediator"), + ("INDEPENDENT", "independent"), + ] diff --git a/test/causal_factor_analysis/test_search_adjusted_fdr.py b/test/causal_factor_analysis/test_search_adjusted_fdr.py new file mode 100644 index 0000000..bcc0d1a --- /dev/null +++ b/test/causal_factor_analysis/test_search_adjusted_fdr.py @@ -0,0 +1,779 @@ +"""Mathematical tests for false-discovery adjustment under search.""" + +from __future__ import annotations + +import itertools +import math +from dataclasses import FrozenInstanceError + +import numpy as np +import pytest + +from RiskLabAI.causal_factor_analysis.search_adjusted_fdr import ( + FDRComparisonEvidence, + FDRNonIdentificationWitness, + GaussianSearchAdjustedFDR, + GaussianTrialMixture, + MaxSelectionFamilyErrors, + SelectionLevelProbabilityEvidence, + compare_single_and_family_fdr, + conditional_upper_tail_probability, + family_level_false_discovery_rate, + fdr_nonidentification_witness, + gaussian_max_selection_cdf, + gaussian_max_selection_log_density, + gaussian_max_selection_log_likelihood, + gaussian_search_adjusted_false_discovery_rate, + gaussian_trial_mixture_cdf, + max_selection_family_errors, + max_selection_null_probability, + maximum_mixture_cdf, + single_trial_false_discovery_rate, +) + +PUBLIC_NAMES = { + "FDRComparisonEvidence", + "FDRNonIdentificationWitness", + "GaussianSearchAdjustedFDR", + "GaussianTrialMixture", + "MaxSelectionFamilyErrors", + "SelectionLevelProbabilityEvidence", + "compare_single_and_family_fdr", + "conditional_upper_tail_probability", + "family_level_false_discovery_rate", + "fdr_nonidentification_witness", + "gaussian_max_selection_cdf", + "gaussian_max_selection_log_density", + "gaussian_max_selection_log_likelihood", + "gaussian_search_adjusted_false_discovery_rate", + "gaussian_trial_mixture_cdf", + "max_selection_family_errors", + "max_selection_null_probability", + "maximum_mixture_cdf", + "single_trial_false_discovery_rate", +} + + +def _normal_cdf(value: float) -> float: + return 0.5 * math.erfc(-value / math.sqrt(2.0)) + + +def _normal_density(value: float) -> float: + return math.exp(-0.5 * value * value) / math.sqrt(2.0 * math.pi) + + +def _enumerated_family_beta( + alpha: float, beta: float, pi_zero: float, n_trials: int +) -> float: + numerator = 0.0 + for alternatives in range(1, n_trials + 1): + probability_of_state = ( + math.comb(n_trials, alternatives) + * pi_zero ** (n_trials - alternatives) + * (1.0 - pi_zero) ** alternatives + ) + no_discovery = (1.0 - alpha) ** (n_trials - alternatives) * (beta**alternatives) + numerator += probability_of_state * no_discovery + return numerator / (1.0 - pi_zero**n_trials) + + +def _enumerated_maximum_cdf( + null_cdf: float, alternative_cdf: float, pi_zero: float, n_trials: int +) -> float: + result = 0.0 + for alternatives in range(n_trials + 1): + result += ( + math.comb(n_trials, alternatives) + * pi_zero ** (n_trials - alternatives) + * (1.0 - pi_zero) ** alternatives + * null_cdf ** (n_trials - alternatives) + * alternative_cdf**alternatives + ) + return result + + +def test_module_exposes_exact_frozen_fdr_family(): + from RiskLabAI.causal_factor_analysis import search_adjusted_fdr + + assert set(search_adjusted_fdr.__all__) == PUBLIC_NAMES + assert len(search_adjusted_fdr.__all__) == 19 + + +def test_single_trial_fdr_matches_bayes_table_and_published_case_b(): + alpha = 0.024997895148220373 + beta = 0.9515427737332771 + pi_zero = 0.15 + expected = alpha * pi_zero / (alpha * pi_zero + (1.0 - beta) * (1.0 - pi_zero)) + result = single_trial_false_discovery_rate(alpha, beta, pi_zero) + assert result == pytest.approx(expected, rel=2e-15) + assert result == pytest.approx(0.08344067427559432, rel=2e-15) + + +@pytest.mark.parametrize( + "alpha,beta,pi_zero,expected", + [ + (0.0, 0.2, 0.7, 0.0), + (0.2, 1.0, 0.7, 1.0), + (0.4, 0.6, 0.5, 0.5), + (1.0, 0.0, 0.0, 0.0), + (1.0, 1.0, 1.0, 1.0), + ], +) +def test_single_trial_fdr_boundary_identities(alpha, beta, pi_zero, expected): + assert single_trial_false_discovery_rate(alpha, beta, pi_zero) == expected + + +def test_single_trial_fdr_remains_stable_for_tiny_probabilities(): + result = single_trial_false_discovery_rate(1e-300, 0.5, 0.5) + assert result == pytest.approx(2e-300, rel=1e-12, abs=0.0) + + +def test_family_level_fdr_matches_published_case_a(): + alpha_family = 0.22365361940347483 + beta_family = 0.7531960884251256 + result = family_level_false_discovery_rate(alpha_family, beta_family, 0.95, 10) + assert result == pytest.approx(0.5748603608683863, rel=2e-15) + + +def test_family_level_fdr_reduces_to_single_trial_at_one_trial(): + for alpha, beta, pi_zero in ( + (0.05, 0.2, 0.7), + (0.4, 0.6, 0.5), + (0.9, 0.1, 0.02), + ): + assert family_level_false_discovery_rate(alpha, beta, pi_zero, 1) == ( + pytest.approx(single_trial_false_discovery_rate(alpha, beta, pi_zero)) + ) + + +def test_family_level_fdr_uses_all_null_family_prior(): + alpha_family, beta_family, pi_zero, n_trials = 0.3, 0.4, 0.8, 3 + family_prior = pi_zero**n_trials + expected = ( + alpha_family + * family_prior + / (alpha_family * family_prior + (1.0 - beta_family) * (1.0 - family_prior)) + ) + assert family_level_false_discovery_rate( + alpha_family, beta_family, pi_zero, n_trials + ) == pytest.approx(expected, rel=2e-15) + + +def test_maximum_selection_errors_match_published_examples(): + case_a = max_selection_family_errors( + 0.024997895148220373, 0.9515427737332771, 0.95, 10 + ) + sensitivity = max_selection_family_errors(0.05, 0.90, 0.90, 5) + + assert case_a.family_type_i_error == pytest.approx(0.22365361940347483, rel=2e-15) + assert case_a.family_type_ii_error == pytest.approx(0.7531960884251256, rel=2e-15) + assert case_a.family_null_probability == pytest.approx( + 0.5987369392383787, rel=2e-15 + ) + assert sensitivity.family_type_i_error == pytest.approx( + 0.22621906250000023, rel=2e-15 + ) + assert sensitivity.family_type_ii_error == pytest.approx( + 0.7245771630882027, rel=2e-15 + ) + assert sensitivity.family_null_probability == pytest.approx( + 0.5904900000000001, rel=2e-15 + ) + assert family_level_false_discovery_rate( + sensitivity.family_type_i_error, + sensitivity.family_type_ii_error, + 0.90, + 5, + ) == pytest.approx(0.5421963202650197, rel=2e-15) + + +def test_maximum_selection_errors_match_binomial_state_enumeration(): + rng = np.random.default_rng(99117) + for _ in range(100): + alpha, beta, pi_zero = rng.uniform(1e-4, 1.0 - 1e-4, size=3) + n_trials = int(rng.integers(1, 9)) + result = max_selection_family_errors(alpha, beta, pi_zero, n_trials) + assert result.family_type_i_error == pytest.approx( + 1.0 - (1.0 - alpha) ** n_trials, rel=2e-13, abs=2e-15 + ) + assert result.family_type_ii_error == pytest.approx( + _enumerated_family_beta(alpha, beta, pi_zero, n_trials), + rel=2e-13, + abs=2e-15, + ) + + +def test_maximum_selection_errors_one_trial_identity_and_record_fields(): + result = max_selection_family_errors(0.12, 0.37, 0.61, 1) + assert result == MaxSelectionFamilyErrors( + n_trials=1, + trial_null_probability=0.61, + trial_type_i_error=0.12, + trial_type_ii_error=0.37, + family_null_probability=0.61, + family_type_i_error=pytest.approx(0.12), + family_type_ii_error=pytest.approx(0.37), + ) + + +def test_maximum_selection_errors_are_stable_near_boundaries(): + result = max_selection_family_errors(1e-14, 1.0 - 1e-14, 0.999999, 100_000) + assert result.family_type_i_error == pytest.approx(1e-9, rel=5e-8) + assert 0.0 <= result.family_type_ii_error <= 1.0 + assert result.family_null_probability == pytest.approx( + math.exp(100_000 * math.log(0.999999)), rel=2e-15 + ) + + +def test_equation_13_equality_and_both_inequality_directions(): + alpha, beta, pi_zero, n_trials = 0.05, 0.2, 0.8, 2 + beta_family = 0.3 + required_ratio = ( + (1.0 - beta_family) + / (1.0 - beta) + * (1.0 - pi_zero**n_trials) + / (pi_zero ** (n_trials - 1) * (1.0 - pi_zero)) + ) + equality_alpha = alpha * required_ratio + equal = compare_single_and_family_fdr( + alpha, + beta, + pi_zero, + n_trials, + family_type_i_error=equality_alpha, + family_type_ii_error=beta_family, + ) + lower = compare_single_and_family_fdr( + alpha, + beta, + pi_zero, + n_trials, + family_type_i_error=equality_alpha * 0.8, + family_type_ii_error=beta_family, + ) + upper = compare_single_and_family_fdr( + alpha, + beta, + pi_zero, + n_trials, + family_type_i_error=equality_alpha * 1.2, + family_type_ii_error=beta_family, + ) + + assert isinstance(equal, FDRComparisonEvidence) + assert equal.equation_13_log_gap == pytest.approx(0.0, abs=2e-15) + assert equal.identification_condition_holds is True + assert equal.single_trial_upper_bounds_family is True + assert equal.single_trial_false_discovery_rate == pytest.approx( + equal.family_level_false_discovery_rate, rel=2e-15 + ) + assert lower.equation_13_log_gap < 0.0 + assert lower.single_trial_upper_bounds_family is True + assert ( + lower.family_level_false_discovery_rate + < lower.single_trial_false_discovery_rate + ) + assert upper.equation_13_log_gap > 0.0 + assert upper.single_trial_upper_bounds_family is False + assert ( + upper.family_level_false_discovery_rate + > upper.single_trial_false_discovery_rate + ) + + +def test_maximum_mixture_cdf_matches_latent_state_enumeration(): + for null_cdf, alternative_cdf, pi_zero, n_trials in ( + (0.4, 0.8, 0.7, 1), + (0.4, 0.8, 0.7, 4), + (0.99, 0.2, 0.01, 8), + ): + expected = _enumerated_maximum_cdf(null_cdf, alternative_cdf, pi_zero, n_trials) + assert maximum_mixture_cdf( + null_cdf, alternative_cdf, pi_zero, n_trials + ) == pytest.approx(expected, rel=3e-15, abs=1e-16) + + +def test_maximum_mixture_cdf_boundary_identities(): + assert maximum_mixture_cdf(0.2, 0.7, 1.0, 3) == pytest.approx(0.2**3) + assert maximum_mixture_cdf(0.2, 0.7, 0.0, 3) == pytest.approx(0.7**3) + assert maximum_mixture_cdf(0.0, 0.0, 0.4, 5) == 0.0 + assert maximum_mixture_cdf(1.0, 1.0, 0.4, 5) == 1.0 + + +def test_conditional_upper_tail_matches_uniform_and_exponential_oracles(): + assert conditional_upper_tail_probability(0.8, 0.3) == pytest.approx(2.0 / 7.0) + threshold, value = 0.4, 1.7 + exponential_result = conditional_upper_tail_probability( + 1.0 - math.exp(-value), 1.0 - math.exp(-threshold) + ) + assert exponential_result == pytest.approx( + math.exp(-(value - threshold)), rel=2e-15 + ) + assert conditional_upper_tail_probability(0.3, 0.3) == 1.0 + assert conditional_upper_tail_probability(1.0, 0.3) == 0.0 + + +def test_gaussian_trial_mixture_matches_erfc_oracle(): + model = GaussianTrialMixture(0.75, 1.0, 2.0, 0.5) + for value in (-3.0, 0.0, 1.25, 4.0): + expected = 0.75 * _normal_cdf(value) + 0.25 * _normal_cdf((value - 2.0) / 0.5) + assert gaussian_trial_mixture_cdf(model, value) == pytest.approx( + expected, rel=2e-15, abs=1e-16 + ) + + +def test_gaussian_maximum_cdf_and_log_density_match_direct_formula(): + model = GaussianTrialMixture(0.65, 1.2, 1.4, 0.8) + value, n_trials = 0.7, 5 + null_z = value / 1.2 + alternative_z = (value - 1.4) / 0.8 + mixture_cdf = 0.65 * _normal_cdf(null_z) + 0.35 * _normal_cdf(alternative_z) + mixture_density = 0.65 * _normal_density(null_z) / 1.2 + 0.35 * ( + _normal_density(alternative_z) / 0.8 + ) + expected_density = n_trials * mixture_cdf ** (n_trials - 1) * mixture_density + + assert gaussian_max_selection_cdf(model, value, n_trials) == pytest.approx( + mixture_cdf**n_trials, rel=2e-15 + ) + assert gaussian_max_selection_log_density(model, value, n_trials) == pytest.approx( + math.log(expected_density), rel=2e-15, abs=2e-15 + ) + + +def test_gaussian_log_density_matches_numerical_cdf_derivative(): + model = GaussianTrialMixture(0.4, 0.9, 1.2, 1.7) + n_trials = 4 + for value in (-1.0, 0.5, 2.0): + step = 1e-5 + derivative = ( + gaussian_max_selection_cdf(model, value + step, n_trials) + - gaussian_max_selection_cdf(model, value - step, n_trials) + ) / (2.0 * step) + density = math.exp(gaussian_max_selection_log_density(model, value, n_trials)) + assert density == pytest.approx(derivative, rel=2e-9, abs=2e-12) + + +def test_gaussian_log_density_is_stable_when_cdf_underflows(): + model = GaussianTrialMixture(0.5, 1.0, 3.0, 2.0) + assert gaussian_max_selection_cdf(model, -40.0, 10) == 0.0 + log_density = gaussian_max_selection_log_density(model, -40.0, 10) + assert math.isfinite(log_density) + assert log_density < -1_000.0 + + +def test_gaussian_log_likelihood_is_direct_sum_and_permutation_invariant(): + model = GaussianTrialMixture(0.7, 1.1, 1.8, 0.9) + observations = np.array([-0.2, 0.7, 1.3, 2.8]) + expected = math.fsum( + gaussian_max_selection_log_density(model, value, 6) for value in observations + ) + actual = gaussian_max_selection_log_likelihood(model, observations, 6) + reversed_actual = gaussian_max_selection_log_likelihood( + model, observations[::-1], 6 + ) + assert actual == pytest.approx(expected, rel=2e-15) + assert reversed_actual == pytest.approx(actual, rel=2e-15) + + +def test_gaussian_search_adjustment_matches_direct_equations_38_to_40(): + model = GaussianTrialMixture(0.82, 1.1, 0.6, 0.7) + thresholds = np.array([-0.2, 0.4, 1.0, 1.6]) + n_trials = 7 + result = gaussian_search_adjusted_false_discovery_rate(model, thresholds, n_trials) + + expected_alpha = np.array([1.0 - _normal_cdf(value / 1.1) for value in thresholds]) + expected_beta = np.array([_normal_cdf((value - 0.6) / 0.7) for value in thresholds]) + expected_family = [ + max_selection_family_errors(alpha, beta, 0.82, n_trials) + for alpha, beta in zip(expected_alpha, expected_beta, strict=True) + ] + expected_family_alpha = np.array( + [evidence.family_type_i_error for evidence in expected_family] + ) + expected_family_beta = np.array( + [evidence.family_type_ii_error for evidence in expected_family] + ) + expected_fdr = family_level_false_discovery_rate( + float(np.mean(expected_family_alpha)), + float(np.mean(expected_family_beta)), + 0.82, + n_trials, + ) + + assert isinstance(result, GaussianSearchAdjustedFDR) + assert result.n_trials == n_trials + assert result.thresholds == pytest.approx(thresholds) + assert result.trial_type_i_errors == pytest.approx(expected_alpha, rel=2e-15) + assert result.trial_type_ii_errors == pytest.approx(expected_beta, rel=2e-15) + assert result.family_type_i_errors == pytest.approx( + expected_family_alpha, rel=2e-15 + ) + assert result.family_type_ii_errors == pytest.approx( + expected_family_beta, rel=2e-15 + ) + assert result.mean_family_type_i_error == pytest.approx( + float(np.mean(expected_family_alpha)), rel=2e-15 + ) + assert result.mean_family_type_ii_error == pytest.approx( + float(np.mean(expected_family_beta)), rel=2e-15 + ) + assert result.family_null_probability == pytest.approx(0.82**n_trials) + assert result.false_discovery_rate == pytest.approx(expected_fdr, rel=2e-15) + + +def test_constant_threshold_calibration_reduces_to_one_family_error_pair(): + model = GaussianTrialMixture(0.75, 1.0, 0.5, 1.4) + result = gaussian_search_adjusted_false_discovery_rate(model, [0.8, 0.8, 0.8], 4) + direct = max_selection_family_errors( + result.trial_type_i_errors[0], + result.trial_type_ii_errors[0], + 0.75, + 4, + ) + assert np.all(result.family_type_i_errors == result.family_type_i_errors[0]) + assert np.all(result.family_type_ii_errors == result.family_type_ii_errors[0]) + assert result.mean_family_type_i_error == pytest.approx(direct.family_type_i_error) + assert result.mean_family_type_ii_error == pytest.approx( + direct.family_type_ii_error + ) + + +@pytest.mark.parametrize( + "observable_cdf,target", + [(0.2, 0.1), (0.63, 0.4), (0.999, 0.95)], +) +def test_nonidentification_witness_reconstructs_observable_and_target( + observable_cdf, target +): + result = fdr_nonidentification_witness(observable_cdf, target) + assert isinstance(result, FDRNonIdentificationWitness) + assert result.n_trials == 2 + assert result.trial_null_probability == pytest.approx(math.sqrt(target)) + assert result.latent_trial_cdf_at_threshold == pytest.approx( + math.sqrt(observable_cdf) + ) + assert result.family_null_probability == pytest.approx(target) + assert result.family_type_i_error == pytest.approx(1.0 - observable_cdf) + assert result.family_type_ii_error == pytest.approx(observable_cdf) + assert result.family_false_discovery_rate == pytest.approx(target, rel=2e-15) + assert result.reconstructed_observable_cdf_at_threshold == pytest.approx( + observable_cdf, rel=2e-15 + ) + + +def test_selection_level_probability_is_pi_zero_for_identical_components(): + pi_zero, n_trials, threshold = 0.37, 4, 0.7 + + def density(value: float) -> float: + return math.exp(-value) if value >= 0.0 else 0.0 + + def cdf(value: float) -> float: + return 1.0 - math.exp(-value) if value >= 0.0 else 0.0 + + result = max_selection_null_probability( + threshold, + pi_zero, + n_trials, + null_density=density, + mixture_cdf=cdf, + ) + expected_selection = 1.0 - cdf(threshold) ** n_trials + assert isinstance(result, SelectionLevelProbabilityEvidence) + assert result.selection_probability == pytest.approx(expected_selection, rel=2e-15) + assert result.joint_null_and_selection_probability == pytest.approx( + pi_zero * expected_selection, rel=2e-12 + ) + assert result.conditional_null_probability == pytest.approx(pi_zero, rel=2e-12) + assert result.quadrature_absolute_error >= 0.0 + + +def test_selection_level_one_trial_matches_bayes_tail_oracle(): + pi_zero, threshold = 0.6, 0.5 + + def null_density(value: float) -> float: + return math.exp(-value) if value >= 0.0 else 0.0 + + def mixture_cdf(value: float) -> float: + if value < 0.0: + return 0.0 + return pi_zero * (1.0 - math.exp(-value)) + (1.0 - pi_zero) * ( + 1.0 - math.exp(-2.0 * value) + ) + + result = max_selection_null_probability( + threshold, + pi_zero, + 1, + null_density=null_density, + mixture_cdf=mixture_cdf, + ) + null_tail = math.exp(-threshold) + alternative_tail = math.exp(-2.0 * threshold) + expected_selection = pi_zero * null_tail + (1.0 - pi_zero) * alternative_tail + expected_posterior = pi_zero * null_tail / expected_selection + assert result.selection_probability == pytest.approx(expected_selection, rel=2e-15) + assert result.conditional_null_probability == pytest.approx( + expected_posterior, rel=2e-12 + ) + + +def test_selection_level_zero_null_prior_avoids_unnecessary_quadrature(): + result = max_selection_null_probability( + 0.0, + 0.0, + 3, + null_density=lambda _: (_ for _ in ()).throw(AssertionError()), + mixture_cdf=lambda value: 0.5 if value == 0.0 else 1.0, + ) + assert result.joint_null_and_selection_probability == 0.0 + assert result.conditional_null_probability == 0.0 + assert result.quadrature_absolute_error == 0.0 + + +def test_result_records_are_frozen_slotted_and_arrays_are_read_only(): + family = max_selection_family_errors(0.05, 0.2, 0.7, 3) + gaussian = gaussian_search_adjusted_false_discovery_rate( + GaussianTrialMixture(0.7, 1.0, 1.0, 1.0), [0.0, 1.0], 3 + ) + with pytest.raises(FrozenInstanceError): + family.n_trials = 4 + with pytest.raises(ValueError, match="read-only"): + gaussian.thresholds[0] = 9.0 + assert not hasattr(family, "__dict__") + assert not hasattr(gaussian, "__dict__") + for values in ( + gaussian.thresholds, + gaussian.trial_type_i_errors, + gaussian.trial_type_ii_errors, + gaussian.family_type_i_errors, + gaussian.family_type_ii_errors, + ): + assert values.flags.writeable is False + + +@pytest.mark.parametrize( + "invalid", [True, -0.1, 1.1, math.nan, math.inf, -math.inf, 1 + 2j] +) +def test_invalid_probabilities_are_rejected(invalid): + calls = ( + lambda: single_trial_false_discovery_rate(invalid, 0.2, 0.7), + lambda: family_level_false_discovery_rate(0.2, invalid, 0.7, 2), + lambda: maximum_mixture_cdf(0.2, 0.3, invalid, 2), + lambda: GaussianTrialMixture(invalid, 1.0, 0.0, 1.0), + ) + for call in calls: + with pytest.raises(ValueError, match="real scalar|finite|between"): + call() + + +@pytest.mark.parametrize("invalid", [True, 0, -1, 2.0, 1.5]) +def test_invalid_trial_counts_are_rejected(invalid): + calls = ( + lambda: family_level_false_discovery_rate(0.2, 0.3, 0.7, invalid), + lambda: max_selection_family_errors(0.2, 0.3, 0.7, invalid), + lambda: maximum_mixture_cdf(0.2, 0.3, 0.7, invalid), + lambda: gaussian_max_selection_cdf( + GaussianTrialMixture(0.7, 1.0, 0.0, 1.0), 0.0, invalid + ), + ) + for call in calls: + with pytest.raises(ValueError, match="integer|positive"): + call() + + +def test_undefined_discovery_posteriors_are_rejected(): + with pytest.raises(ValueError, match="discovery probability"): + single_trial_false_discovery_rate(0.0, 1.0, 0.5) + with pytest.raises(ValueError, match="discovery probability"): + family_level_false_discovery_rate(0.0, 1.0, 0.5, 3) + + +@pytest.mark.parametrize("pi_zero", [0.0, 1.0]) +def test_maximum_family_error_requires_interior_trial_prior(pi_zero): + with pytest.raises(ValueError, match="strictly"): + max_selection_family_errors(0.1, 0.2, pi_zero, 2) + + +def test_comparison_requires_interior_probabilities_and_valid_tolerances(): + with pytest.raises(ValueError, match="strictly"): + compare_single_and_family_fdr( + 0.0, + 0.2, + 0.7, + 2, + family_type_i_error=0.1, + family_type_ii_error=0.2, + ) + with pytest.raises(ValueError, match="nonnegative"): + compare_single_and_family_fdr( + 0.1, + 0.2, + 0.7, + 2, + family_type_i_error=0.1, + family_type_ii_error=0.2, + relative_tolerance=-1.0, + ) + + +def test_conditional_tail_rejects_invalid_order_or_empty_condition(): + with pytest.raises(ValueError, match="below"): + conditional_upper_tail_probability(0.2, 0.3) + with pytest.raises(ValueError, match="less than one"): + conditional_upper_tail_probability(1.0, 1.0) + + +@pytest.mark.parametrize( + "arguments", + [ + (0.5, 0.0, 0.0, 1.0), + (0.5, -1.0, 0.0, 1.0), + (0.5, 1.0, 0.0, 0.0), + (0.5, 1.0, math.nan, 1.0), + ], +) +def test_gaussian_model_rejects_invalid_parameters(arguments): + with pytest.raises(ValueError, match="positive|finite"): + GaussianTrialMixture(*arguments) + + +@pytest.mark.parametrize( + "values,match", + [ + ([], "nonempty"), + ([[1.0, 2.0]], "one-dimensional"), + ([1.0, math.nan], "finite"), + ([True, False], "non-boolean"), + ([1.0 + 2.0j], "non-boolean"), + ], +) +def test_gaussian_likelihood_rejects_invalid_observations(values, match): + model = GaussianTrialMixture(0.5, 1.0, 1.0, 1.0) + with pytest.raises(ValueError, match=match): + gaussian_max_selection_log_likelihood(model, values, 2) + + +def test_gaussian_calibration_rejects_empty_or_boundary_mixture_prior(): + with pytest.raises(ValueError, match="nonempty"): + gaussian_search_adjusted_false_discovery_rate( + GaussianTrialMixture(0.5, 1.0, 1.0, 1.0), [], 2 + ) + with pytest.raises(ValueError, match="strictly"): + gaussian_search_adjusted_false_discovery_rate( + GaussianTrialMixture(1.0, 1.0, 1.0, 1.0), [0.0], 2 + ) + + +@pytest.mark.parametrize("observable,target", [(0.0, 0.3), (1.0, 0.3), (0.4, 0.0)]) +def test_nonidentification_witness_requires_interior_values(observable, target): + with pytest.raises(ValueError, match="strictly"): + fdr_nonidentification_witness(observable, target) + + +def test_selection_level_validation_fails_closed(): + def valid_density(value: float) -> float: + return math.exp(-value) if value >= 0.0 else 0.0 + + def valid_cdf(value: float) -> float: + return 1.0 - math.exp(-value) if value >= 0.0 else 0.0 + + with pytest.raises(TypeError, match="null_density"): + max_selection_null_probability( + 0.0, 0.5, 2, null_density=1.0, mixture_cdf=valid_cdf + ) + with pytest.raises(TypeError, match="mixture_cdf"): + max_selection_null_probability( + 0.0, 0.5, 2, null_density=valid_density, mixture_cdf=1.0 + ) + with pytest.raises(ValueError, match="selection probability"): + max_selection_null_probability( + 0.0, + 0.5, + 2, + null_density=valid_density, + mixture_cdf=lambda _: 1.0, + ) + with pytest.raises(ValueError, match="between"): + max_selection_null_probability( + 0.0, + 0.5, + 2, + null_density=valid_density, + mixture_cdf=lambda _: 1.1, + ) + with pytest.raises(ValueError, match="nonnegative"): + max_selection_null_probability( + 0.0, + 0.5, + 2, + null_density=lambda _: -1.0, + mixture_cdf=valid_cdf, + ) + with pytest.raises(ValueError, match="positive"): + max_selection_null_probability( + 0.0, + 0.5, + 2, + null_density=valid_density, + mixture_cdf=valid_cdf, + absolute_tolerance=0.0, + ) + + +def test_gaussian_distribution_boundaries_are_valid_for_evaluation(): + pure_null = GaussianTrialMixture(1.0, 2.0, 7.0, 3.0) + pure_alternative = GaussianTrialMixture(0.0, 2.0, 7.0, 3.0) + for value in (-1.0, 0.0, 2.0): + assert gaussian_trial_mixture_cdf(pure_null, value) == pytest.approx( + _normal_cdf(value / 2.0), rel=2e-15 + ) + assert gaussian_trial_mixture_cdf(pure_alternative, value) == pytest.approx( + _normal_cdf((value - 7.0) / 3.0), rel=2e-15 + ) + + +def test_randomized_gaussian_maximum_cdf_is_monotone_and_bounded(): + rng = np.random.default_rng(3101) + for _ in range(25): + model = GaussianTrialMixture( + float(rng.uniform()), + float(rng.uniform(0.2, 3.0)), + float(rng.normal()), + float(rng.uniform(0.2, 3.0)), + ) + n_trials = int(rng.integers(1, 20)) + values = np.linspace(-8.0, 8.0, 101) + cdf_values = np.array( + [gaussian_max_selection_cdf(model, value, n_trials) for value in values] + ) + assert np.all(cdf_values >= 0.0) + assert np.all(cdf_values <= 1.0) + assert np.all(np.diff(cdf_values) >= -2e-16) + + +def test_record_types_are_distinct_for_family_and_selection_estimands(): + family = max_selection_family_errors(0.05, 0.2, 0.7, 3) + selection = max_selection_null_probability( + 0.0, + 0.7, + 3, + null_density=lambda value: math.exp(-value) if value >= 0.0 else 0.0, + mixture_cdf=lambda value: 1.0 - math.exp(-value) if value >= 0.0 else 0.0, + ) + assert isinstance(family, MaxSelectionFamilyErrors) + assert isinstance(selection, SelectionLevelProbabilityEvidence) + assert not set(family.__slots__).intersection( + {"conditional_null_probability", "selection_probability"} + ) + + +def test_complete_four_state_bayes_table_matches_single_trial_formula(): + alpha, beta, pi_zero = 0.08, 0.25, 0.6 + states = list(itertools.product((False, True), repeat=2)) + assert len(states) == 4 + false_discovery_mass = pi_zero * alpha + discovery_mass = false_discovery_mass + (1.0 - pi_zero) * (1.0 - beta) + assert single_trial_false_discovery_rate(alpha, beta, pi_zero) == pytest.approx( + false_discovery_mass / discovery_mass, rel=2e-15 + ) diff --git a/test/causal_factor_analysis/test_structural_models.py b/test/causal_factor_analysis/test_structural_models.py new file mode 100644 index 0000000..9ca136e --- /dev/null +++ b/test/causal_factor_analysis/test_structural_models.py @@ -0,0 +1,180 @@ +"""Independent tests for deterministic structural-model evaluation.""" + +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import numpy as np +import pytest + +from RiskLabAI.causal_factor_analysis.graph_identification import CausalDAG +from RiskLabAI.causal_factor_analysis.structural_models import ( + StructuralCausalModelResult, + evaluate_structural_causal_model, +) + + +def _chain(): + return CausalDAG(("X", "Y", "Z"), (("X", "Y"), ("Y", "Z")), ("X", "Y", "Z")) + + +def test_chain_matches_hand_structural_equations(): + graph = _chain() + exogenous = { + "X": np.array([1.0, 2.0, 3.0]), + "Y": np.array([0.5, -0.5, 1.0]), + "Z": np.array([2.0, 2.0, 2.0]), + } + mechanisms = { + "X": lambda parents, noise: noise, + "Y": lambda parents, noise: 2.0 * parents[:, 0] + noise, + "Z": lambda parents, noise: parents[:, 0] ** 2 + noise, + } + + result = evaluate_structural_causal_model(graph, mechanisms, exogenous) + + x = exogenous["X"] + y = 2.0 * x + exogenous["Y"] + z = y**2 + exogenous["Z"] + assert result.node_order == ("X", "Y", "Z") + np.testing.assert_allclose(result.values, np.column_stack((x, y, z))) + + +def test_lexical_topological_and_parent_order_are_deterministic(): + graph = CausalDAG( + ("D", "C", "B", "A"), + (("B", "D"), ("A", "D")), + ("D", "C", "B", "A"), + ) + parent_inputs = [] + + def d_mechanism(parents, noise): + parent_inputs.append(parents.copy()) + return parents[:, 0] + 10.0 * parents[:, 1] + noise + + result = evaluate_structural_causal_model( + graph, + { + "D": d_mechanism, + "C": lambda parents, noise: noise, + "B": lambda parents, noise: noise, + "A": lambda parents, noise: noise, + }, + { + "D": [0.0, 0.0], + "C": [30.0, 31.0], + "B": [20.0, 21.0], + "A": [1.0, 2.0], + }, + ) + + assert result.node_order == ("A", "B", "C", "D") + np.testing.assert_array_equal(parent_inputs[0], [[1.0, 20.0], [2.0, 21.0]]) + np.testing.assert_array_equal(result.values[:, 3], [201.0, 212.0]) + + +def test_mechanisms_receive_protected_independent_snapshots(): + graph = _chain() + captured = [] + + def protected_root(parents, noise): + captured.extend((parents, noise)) + assert parents.shape == (2, 0) + assert not parents.flags.writeable + assert not noise.flags.writeable + with pytest.raises(ValueError): + noise.setflags(write=True) + return noise + + result = evaluate_structural_causal_model( + graph, + { + "X": protected_root, + "Y": lambda parents, noise: parents[:, 0] + noise, + "Z": lambda parents, noise: parents[:, 0] + noise, + }, + {"X": [1.0, 2.0], "Y": [0.0, 0.0], "Z": [0.0, 0.0]}, + ) + + assert captured + assert not result.values.flags.writeable + with pytest.raises(ValueError): + result.values.setflags(write=True) + with pytest.raises(ValueError): + result.values[0, 0] = -1.0 + + +def test_inputs_and_mechanism_outputs_are_snapshotted(): + source = np.array([1.0, 2.0]) + output = source.copy() + + def root(parents, noise): + return output + + graph = CausalDAG(("X",), (), ("X",)) + result = evaluate_structural_causal_model(graph, {"X": root}, {"X": source}) + source[:] = 10.0 + output[:] = 20.0 + np.testing.assert_array_equal(result.values[:, 0], [1.0, 2.0]) + + +def test_result_is_frozen_and_slotted(): + graph = CausalDAG(("X",), (), ("X",)) + result = evaluate_structural_causal_model( + graph, {"X": lambda parents, noise: noise}, {"X": [1.0]} + ) + assert isinstance(result, StructuralCausalModelResult) + assert not hasattr(result, "__dict__") + with pytest.raises(FrozenInstanceError): + result.node_order = () + + +@pytest.mark.parametrize( + "mechanisms,exogenous,exception", + [ + ((), {"X": [1.0]}, TypeError), + ({"X": lambda parents, noise: noise}, (), TypeError), + ({}, {"X": [1.0]}, ValueError), + ({"X": lambda parents, noise: noise}, {}, ValueError), + ({"X": 1}, {"X": [1.0]}, TypeError), + ({"X": lambda parents, noise: noise}, {"X": []}, ValueError), + ({"X": lambda parents, noise: noise}, {"X": [[1.0]]}, ValueError), + ({"X": lambda parents, noise: noise}, {"X": [True]}, ValueError), + ({"X": lambda parents, noise: noise}, {"X": [np.inf]}, ValueError), + ], +) +def test_structural_model_boundary_validation(mechanisms, exogenous, exception): + graph = CausalDAG(("X",), (), ("X",)) + with pytest.raises(exception): + evaluate_structural_causal_model(graph, mechanisms, exogenous) + + +@pytest.mark.parametrize( + "mechanism", + [ + lambda parents, noise: 1.0, + lambda parents, noise: [[1.0]], + lambda parents, noise: [1.0, 2.0], + lambda parents, noise: [np.nan], + lambda parents, noise: [1.0 + 1.0j], + lambda parents, noise: [True], + ], +) +def test_invalid_mechanism_results_fail_closed(mechanism): + graph = CausalDAG(("X",), (), ("X",)) + with pytest.raises(ValueError): + evaluate_structural_causal_model(graph, {"X": mechanism}, {"X": [0.0]}) + + +def test_exogenous_lengths_must_match_before_any_mechanism_runs(): + graph = CausalDAG(("X", "Y"), (("X", "Y"),), ("X", "Y")) + called = [] + mechanisms = { + "X": lambda parents, noise: called.append("X") or noise, + "Y": lambda parents, noise: called.append("Y") or noise, + } + with pytest.raises(ValueError, match="equal length"): + evaluate_structural_causal_model( + graph, mechanisms, {"X": [1.0], "Y": [1.0, 2.0]} + ) + assert called == [] diff --git a/test/data/denoise/test_nercome.py b/test/data/denoise/test_nercome.py index 693f63f..acceb1d 100644 --- a/test/data/denoise/test_nercome.py +++ b/test/data/denoise/test_nercome.py @@ -40,7 +40,7 @@ def _rel_frobenius(estimate, true): def test_nercome_lower_covariance_error_on_no_gap_spectrum(): """ - Replication of the Appraisal 24 mechanism: on a no-gap (slowly-decaying) spectrum NERCOME recovers + Replication of the independent validation mechanism: on a no-gap (slowly-decaying) spectrum NERCOME recovers the covariance with lower relative Frobenius error than MP eigenvalue clipping. """ p = 20 diff --git a/test/data/differentiation/test_adaptive_differentiation.py b/test/data/differentiation/test_adaptive_differentiation.py index 38d7bcb..2fe2002 100644 --- a/test/data/differentiation/test_adaptive_differentiation.py +++ b/test/data/differentiation/test_adaptive_differentiation.py @@ -63,7 +63,7 @@ def test_hurst_sanity_on_known_d(): def test_afd_recovers_order_better_than_ffd_strong_memory(): """ - Replication of the Appraisal 13 held-out mechanism: on strong-memory short samples (d=0.45, N=250), + Replication of the independent validation validation mechanism: on strong-memory short samples (d=0.45, N=250), posed on the integrated price (true boundary order tau = 1 + d = 1.45), AFD recovers the order with a smaller mean error than de Prado's min-d-via-ADF baseline. """ diff --git a/test/data/distance/test_codependence.py b/test/data/distance/test_codependence.py index 8ecbed7..cfe55ae 100644 --- a/test/data/distance/test_codependence.py +++ b/test/data/distance/test_codependence.py @@ -30,7 +30,7 @@ def test_ksg_matches_gaussian_closed_form(): def test_ksg_beats_binned_on_short_nonlinear_sample(): """ - Replication of the Appraisal 11 mechanism: on short nonlinear samples KSG has lower error against + Replication of the independent validation mechanism: on short nonlinear samples KSG has lower error against the true MI than the binned baseline (averaged over seeds). """ rho = 0.6 diff --git a/test/data/structures/test_hardened_information_driven_bars.py b/test/data/structures/test_hardened_information_driven_bars.py index e9dd679..3cb7d96 100644 --- a/test/data/structures/test_hardened_information_driven_bars.py +++ b/test/data/structures/test_hardened_information_driven_bars.py @@ -91,7 +91,7 @@ def test_guard_condition_logic(): def test_well_behaved_unchanged_when_guards_do_not_bind(): """ - Replication of the held-out "well-behaved unchanged" criterion: with loose guards the hardened bar + Replication of the validation sample "well-behaved unchanged" criterion: with loose guards the hardened bar count matches the naive ExpectedImbalanceBars count (the guards never trigger). """ ticks = _synthetic_ticks() diff --git a/test/features/feature_importance/test_debiased_importance.py b/test/features/feature_importance/test_debiased_importance.py index e258727..434e4c1 100644 --- a/test/features/feature_importance/test_debiased_importance.py +++ b/test/features/feature_importance/test_debiased_importance.py @@ -37,7 +37,7 @@ def test_mdi_plus_ranks_signal_top(): def test_mdi_plus_rejects_high_cardinality_noise(): """ - Replication of the Appraisal 10 mechanism: MDI+ does not let a high-cardinality noise feature + Replication of the independent validation mechanism: MDI+ does not let a high-cardinality noise feature out-rank the true signal (the inflation MDI is prone to). """ rng = np.random.default_rng(1) diff --git a/test/features/structural_breaks/test_pelt_change_points.py b/test/features/structural_breaks/test_pelt_change_points.py index 5968f20..5ba46c8 100644 --- a/test/features/structural_breaks/test_pelt_change_points.py +++ b/test/features/structural_breaks/test_pelt_change_points.py @@ -18,7 +18,7 @@ def _near(cps, target, tol=15): def test_recovers_multiple_mean_shifts(): """ - Replication of the Appraisal 26 mechanism: PELT recovers multiple mean change-points (which CUSUM, + Replication of the independent validation mechanism: PELT recovers multiple mean change-points (which CUSUM, targeting a single shift, misses). """ rng = np.random.default_rng(0) diff --git a/test/features/structural_breaks/test_structural_breaks.py b/test/features/structural_breaks/test_structural_breaks.py index 466f67a..7e68348 100644 --- a/test/features/structural_breaks/test_structural_breaks.py +++ b/test/features/structural_breaks/test_structural_breaks.py @@ -170,7 +170,7 @@ def test_psy_minimum_window(): def test_bsadf_sequence_matches_get_bsadf_statistic(random_walk): """ Replication: the fast BSADF sequence agrees with the validated per-endpoint - ``get_bsadf_statistic`` on shared windows (the appraisal saw ~1e-14). + ``get_bsadf_statistic`` on shared windows (the independent validation saw ~1e-14). """ nmin = psy_minimum_window(len(random_walk)) seq = get_bsadf_sequence(random_walk, nmin) diff --git a/test/features/structural_breaks/test_volatility_robust_sadf.py b/test/features/structural_breaks/test_volatility_robust_sadf.py index 6392555..417bf36 100644 --- a/test/features/structural_breaks/test_volatility_robust_sadf.py +++ b/test/features/structural_breaks/test_volatility_robust_sadf.py @@ -27,7 +27,7 @@ def _bubble(t, rng, magnitude=1.06): def test_holds_size_on_variance_break_null(): """ - Replication of the Appraisal 26 mechanism: under a non-stationary-volatility (variance-break) random + Replication of the independent validation mechanism: under a non-stationary-volatility (variance-break) random walk with NO bubble, the volatility-robust SADF does not spuriously reject at anything like the rate a homoskedastic test would (it holds roughly nominal size). """