powerquant-cli 0.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- powerquant_cli-0.2.6.dist-info/METADATA +62 -0
- powerquant_cli-0.2.6.dist-info/RECORD +355 -0
- powerquant_cli-0.2.6.dist-info/WHEEL +4 -0
- powerquant_cli-0.2.6.dist-info/entry_points.txt +2 -0
- powerquant_cli-0.2.6.dist-info/licenses/LICENSE +36 -0
- powerquant_cli-0.2.6.dist-info/licenses/licenses/LICENSE.vnpy +21 -0
- pqdata/__init__.py +6 -0
- pqdata/client.py +389 -0
- pqdata/download.py +60 -0
- pqdata/exceptions.py +14 -0
- pqdata/schema/__init__.py +1 -0
- pqdata/schema/adjustment_schema.py +67 -0
- pqdata/schema/bar_content_schema.py +24 -0
- pqdata/schema/bar_schema.py +56 -0
- pqdata/schema/corporate_action_schema.py +46 -0
- pqdata/schema/history_schema.py +61 -0
- pqdata/schema/pit_daily_window.py +16 -0
- pqdata/schema/query_schema.py +53 -0
- pqdata/schema/quote_schema.py +73 -0
- pqdata/schema/reference_schema.py +48 -0
- pqdata/schema/reliability_schema.py +152 -0
- quant_cli/__init__.py +0 -0
- quant_cli/api.py +39 -0
- quant_cli/app/__init__.py +0 -0
- quant_cli/app/backtest/__init__.py +0 -0
- quant_cli/app/backtest/cli/__init__.py +0 -0
- quant_cli/app/backtest/cli/local_backtest_cli.py +72 -0
- quant_cli/app/backtest/local_backtest_dependency.py +10 -0
- quant_cli/app/backtest/local_backtest_schema.py +49 -0
- quant_cli/app/backtest/repository/__init__.py +0 -0
- quant_cli/app/backtest/repository/backtest_local_repository.py +67 -0
- quant_cli/app/backtest/service/__init__.py +0 -0
- quant_cli/app/backtest/service/local_backtest_service.py +271 -0
- quant_cli/app/data/__init__.py +0 -0
- quant_cli/app/data/cli/__init__.py +0 -0
- quant_cli/app/data/cli/data_cli.py +78 -0
- quant_cli/app/data/data_dependency.py +35 -0
- quant_cli/app/data/repository/__init__.py +0 -0
- quant_cli/app/data/repository/parquet_repository.py +124 -0
- quant_cli/app/data/schema/__init__.py +0 -0
- quant_cli/app/data/schema/dataset_audit_schema.py +37 -0
- quant_cli/app/data/schema/dataset_request_schema.py +28 -0
- quant_cli/app/data/schema/parquet_file_schema.py +17 -0
- quant_cli/app/data/service/__init__.py +0 -0
- quant_cli/app/data/service/bar_cache_service.py +287 -0
- quant_cli/app/data/service/dataset_audit_service.py +137 -0
- quant_cli/app/data/service/dataset_load_service.py +115 -0
- quant_cli/app/data/service/dataset_prepare_service.py +187 -0
- quant_cli/app/factor/__init__.py +0 -0
- quant_cli/app/factor/cli/__init__.py +0 -0
- quant_cli/app/factor/cli/factor_definition_cli.py +81 -0
- quant_cli/app/factor/cli/local_factor_cli.py +102 -0
- quant_cli/app/factor/cli/local_selection_cli.py +58 -0
- quant_cli/app/factor/interface/__init__.py +0 -0
- quant_cli/app/factor/interface/local_selection_store.py +12 -0
- quant_cli/app/factor/local_factor_dependency.py +38 -0
- quant_cli/app/factor/repository/__init__.py +0 -0
- quant_cli/app/factor/repository/factor_local_repository.py +154 -0
- quant_cli/app/factor/repository/selection_local_repository.py +67 -0
- quant_cli/app/factor/schema/local_selection_schema.py +56 -0
- quant_cli/app/factor/service/__init__.py +0 -0
- quant_cli/app/factor/service/factor_data_prepare_service.py +181 -0
- quant_cli/app/factor/service/local_factor_query_service.py +146 -0
- quant_cli/app/factor/service/local_factor_service.py +120 -0
- quant_cli/app/factor/service/local_selection_service.py +139 -0
- quant_cli/app/job/__init__.py +0 -0
- quant_cli/app/job/cli/__init__.py +0 -0
- quant_cli/app/job/cli/local_job_cli.py +69 -0
- quant_cli/app/job/schema/__init__.py +0 -0
- quant_cli/app/job/schema/local_job_schema.py +11 -0
- quant_cli/app/job/service/__init__.py +0 -0
- quant_cli/app/job/service/local_job_service.py +60 -0
- quant_cli/app/quant_cli.py +49 -0
- quant_cli/app/research/__init__.py +0 -0
- quant_cli/app/research/cli/__init__.py +0 -0
- quant_cli/app/research/cli/local_research_cli.py +93 -0
- quant_cli/app/research/local_research_dependency.py +10 -0
- quant_cli/app/research/repository/__init__.py +0 -0
- quant_cli/app/research/repository/research_local_repository.py +67 -0
- quant_cli/app/research/schema/__init__.py +0 -0
- quant_cli/app/research/schema/local_research_schema.py +26 -0
- quant_cli/app/research/service/__init__.py +0 -0
- quant_cli/app/research/service/local_research_execution_service.py +105 -0
- quant_cli/app/research/service/local_research_service.py +101 -0
- quant_cli/app/strategy/__init__.py +0 -0
- quant_cli/app/strategy/cli/__init__.py +0 -0
- quant_cli/app/strategy/cli/strategy_cli.py +104 -0
- quant_cli/common/__init__.py +0 -0
- quant_cli/common/constants.py +7 -0
- quant_cli/common/database/__init__.py +0 -0
- quant_cli/common/database/duckdb_connection.py +30 -0
- quant_cli/common/http/__init__.py +0 -0
- quant_cli/common/http/definition_client.py +54 -0
- quant_cli/common/utils/__init__.py +0 -0
- quant_cli/common/utils/cli_utils.py +37 -0
- quant_cli/common/utils/platform_utils.py +11 -0
- quant_cli/common/utils/runtime_environment.py +12 -0
- quant_cli/examples/cached_factor_screen.py +165 -0
- quant_cli/examples/factors/ma_close_10.py +10 -0
- quant_cli/examples/factors/max_drawdown_20d.py +18 -0
- quant_cli/examples/factors/momentum_20d.py +13 -0
- quant_cli/examples/factors/multi_intraday_screen.py +16 -0
- quant_cli/examples/factors/risk_adjusted_momentum_20d.py +18 -0
- quant_cli/examples/factors/volume_ratio_5d.py +14 -0
- quant_cli/examples/factors/volume_weighted_close_pressure.py +14 -0
- quant_cli/examples/full_market_screen.py +189 -0
- quant_cli/examples/local_quant_validation.py +196 -0
- quant_cli/examples/published_definition_validation.py +219 -0
- quant_cli/examples/research/factor_pit_coverage.py +87 -0
- quant_cli/examples/research/ipo_first_day.py +348 -0
- quant_cli/examples/selection/README.md +59 -0
- quant_cli/examples/selection/backtest.json +18 -0
- quant_cli/examples/selection/buy_hold.py +7 -0
- quant_cli/examples/selection/compute.json +15 -0
- quant_cli/examples/selection/prepare.json +13 -0
- quant_cli/examples/selection/select.json +44 -0
- quant_cli/examples/strategies/bar_roundtrip.py +12 -0
- quant_cli/examples/strategies/buy_and_hold.py +11 -0
- quant_cli/examples/strategies/contrarian_timing.py +14 -0
- quant_cli/examples/strategies/factor_gate_vwap.py +22 -0
- quant_cli/examples/strategies/ipo_next_day_vwap.py +31 -0
- quant_cli/examples/strategies/momentum_timing.py +14 -0
- quant_cli/examples/strategies/monthly_equal_weight.py +19 -0
- quant_cli/examples/strategies/session_roundtrip.py +16 -0
- quant_cli/examples/strategies/trend_drawdown_guard.py +22 -0
- quant_cli/examples/strategies/weekly_risk_adjusted_rotation.py +61 -0
- quant_cli/examples/unified_snapshot_validation.py +260 -0
- quant_core/__init__.py +0 -0
- quant_core/app/SPEC.md +18 -0
- quant_core/app/__init__.py +0 -0
- quant_core/app/backtest/SPEC.md +65 -0
- quant_core/app/backtest/__init__.py +1 -0
- quant_core/app/backtest/backtest_input_schema.py +20 -0
- quant_core/app/backtest/backtest_run.py +393 -0
- quant_core/app/backtest/data_guard.py +77 -0
- quant_core/app/backtest/historical_clock.py +26 -0
- quant_core/app/backtest/legacy_result_schema.py +34 -0
- quant_core/app/backtest/metrics.py +194 -0
- quant_core/app/backtest/performance_report.py +61 -0
- quant_core/app/backtest/report_schema.py +64 -0
- quant_core/app/backtest/schema.py +477 -0
- quant_core/app/backtest/service/__init__.py +0 -0
- quant_core/app/backtest/service/backtest_execution_service.py +114 -0
- quant_core/app/data/__init__.py +0 -0
- quant_core/app/data/constants.py +284 -0
- quant_core/app/data/domain/__init__.py +0 -0
- quant_core/app/data/domain/trading_quantity_value.py +57 -0
- quant_core/app/data/exceptions.py +95 -0
- quant_core/app/data/interface/__init__.py +0 -0
- quant_core/app/data/interface/adjustment_factor_query.py +21 -0
- quant_core/app/data/interface/market_data_query.py +71 -0
- quant_core/app/data/interface/market_data_verification.py +20 -0
- quant_core/app/data/interface/market_event_source.py +11 -0
- quant_core/app/data/interface/market_revision_query.py +8 -0
- quant_core/app/data/interface/market_runtime_repository.py +44 -0
- quant_core/app/data/interface/market_time_slice_source.py +11 -0
- quant_core/app/data/repository/__init__.py +0 -0
- quant_core/app/data/schema/__init__.py +1 -0
- quant_core/app/data/schema/adjustment_schema.py +67 -0
- quant_core/app/data/schema/corporate_action_schema.py +46 -0
- quant_core/app/data/schema/event_schema.py +506 -0
- quant_core/app/data/schema/exchange_operating_state.py +37 -0
- quant_core/app/data/schema/exchange_state_schema.py +106 -0
- quant_core/app/data/schema/history_schema.py +48 -0
- quant_core/app/data/schema/instrument_identity.py +69 -0
- quant_core/app/data/schema/market_schema.py +128 -0
- quant_core/app/data/schema/pit_daily_window.py +16 -0
- quant_core/app/data/schema/quote_schema.py +73 -0
- quant_core/app/data/schema/reliability_schema.py +152 -0
- quant_core/app/data/schema/snapshot_schema.py +82 -0
- quant_core/app/data/schema/trading_state_schema.py +211 -0
- quant_core/app/data/service/__init__.py +0 -0
- quant_core/app/data/service/calendar_coverage_service.py +63 -0
- quant_core/app/data/service/prepared_data_verification_service.py +40 -0
- quant_core/app/data/service/prepared_market_service.py +189 -0
- quant_core/app/data/service/price_adjustment_service.py +235 -0
- quant_core/app/factor/LICENSE.vnpy +21 -0
- quant_core/app/factor/__init__.py +13 -0
- quant_core/app/factor/builtin/__init__.py +0 -0
- quant_core/app/factor/builtin/alpha101_recipe.py +1095 -0
- quant_core/app/factor/builtin/alpha158_recipe.py +1284 -0
- quant_core/app/factor/builtin/factor_recipe_catalog.py +100 -0
- quant_core/app/factor/computation/__init__.py +0 -0
- quant_core/app/factor/computation/factor_batch_evaluator.py +141 -0
- quant_core/app/factor/computation/factor_expression.py +106 -0
- quant_core/app/factor/computation/factor_source_loader.py +24 -0
- quant_core/app/factor/computation/operators/__init__.py +0 -0
- quant_core/app/factor/computation/operators/arithmetic_operator.py +182 -0
- quant_core/app/factor/computation/operators/cross_section_operator.py +66 -0
- quant_core/app/factor/computation/operators/time_series_operator.py +342 -0
- quant_core/app/factor/computation/panel_evaluator.py +59 -0
- quant_core/app/factor/computation/panel_expression.py +193 -0
- quant_core/app/factor/computation/python_factor_executor.py +87 -0
- quant_core/app/factor/computation/universe_filter.py +25 -0
- quant_core/app/factor/constants.py +114 -0
- quant_core/app/factor/exceptions.py +35 -0
- quant_core/app/factor/factor_dependency.py +65 -0
- quant_core/app/factor/interface/__init__.py +0 -0
- quant_core/app/factor/interface/factor_compute.py +12 -0
- quant_core/app/factor/interface/factor_execution.py +15 -0
- quant_core/app/factor/interface/factor_local_data.py +42 -0
- quant_core/app/factor/interface/factor_panel_compute.py +18 -0
- quant_core/app/factor/interface/factor_panel_input.py +12 -0
- quant_core/app/factor/interface/factor_query_store.py +39 -0
- quant_core/app/factor/interface/public/__init__.py +0 -0
- quant_core/app/factor/interface/public/factor_diagnostics.py +18 -0
- quant_core/app/factor/interface/public/factor_exposure_query.py +12 -0
- quant_core/app/factor/interface/public/factor_local.py +15 -0
- quant_core/app/factor/interface/public/factor_query.py +57 -0
- quant_core/app/factor/interface/public/factor_selection.py +20 -0
- quant_core/app/factor/model/__init__.py +0 -0
- quant_core/app/factor/repository/__init__.py +0 -0
- quant_core/app/factor/schema/__init__.py +1 -0
- quant_core/app/factor/schema/diagnostic_types.py +42 -0
- quant_core/app/factor/schema/factor_artifact_schema.py +36 -0
- quant_core/app/factor/schema/factor_computation_schema.py +102 -0
- quant_core/app/factor/schema/factor_compute_request.py +24 -0
- quant_core/app/factor/schema/factor_contract.py +257 -0
- quant_core/app/factor/schema/factor_input_schema.py +32 -0
- quant_core/app/factor/schema/factor_panel.py +65 -0
- quant_core/app/factor/schema/factor_publication_schema.py +41 -0
- quant_core/app/factor/schema/factor_query_result_schema.py +12 -0
- quant_core/app/factor/schema/factor_result_schema.py +6 -0
- quant_core/app/factor/schema/local_factor_schema.py +42 -0
- quant_core/app/factor/schema/panel_policy.py +13 -0
- quant_core/app/factor/schema/selection_report.py +116 -0
- quant_core/app/factor/schema/selection_types.py +135 -0
- quant_core/app/factor/service/__init__.py +1 -0
- quant_core/app/factor/service/factor_compute_service.py +140 -0
- quant_core/app/factor/service/factor_diagnostic_runner.py +134 -0
- quant_core/app/factor/service/factor_diagnostics_service.py +140 -0
- quant_core/app/factor/service/factor_exposure_service.py +24 -0
- quant_core/app/factor/service/factor_input_service.py +258 -0
- quant_core/app/factor/service/factor_local_execution_service.py +51 -0
- quant_core/app/factor/service/factor_panel_service.py +86 -0
- quant_core/app/factor/service/factor_runtime_service.py +306 -0
- quant_core/app/factor/service/factor_selection_runner.py +88 -0
- quant_core/app/factor/service/published_factor_execution_service.py +160 -0
- quant_core/app/factor/service/selection_service.py +234 -0
- quant_core/app/factor/visibility.py +22 -0
- quant_core/app/opportunity/SPEC.md +45 -0
- quant_core/app/opportunity/__init__.py +1 -0
- quant_core/app/order_execution/SPEC.md +64 -0
- quant_core/app/order_execution/__init__.py +1 -0
- quant_core/app/order_execution/bar_matcher.py +55 -0
- quant_core/app/order_execution/constants.py +142 -0
- quant_core/app/order_execution/domain/__init__.py +45 -0
- quant_core/app/order_execution/domain/execution_batch.py +108 -0
- quant_core/app/order_execution/domain/order_entity.py +197 -0
- quant_core/app/order_execution/domain/order_event.py +87 -0
- quant_core/app/order_execution/domain/order_policy.py +29 -0
- quant_core/app/order_execution/domain/order_value.py +142 -0
- quant_core/app/order_execution/fee_model.py +60 -0
- quant_core/app/order_execution/interface/__init__.py +0 -0
- quant_core/app/order_execution/interface/order_account.py +10 -0
- quant_core/app/order_execution/interface/order_trading_rules.py +10 -0
- quant_core/app/order_execution/interface/public/__init__.py +0 -0
- quant_core/app/order_execution/interface/public/execution_account.py +53 -0
- quant_core/app/order_execution/interface/public/order_events.py +19 -0
- quant_core/app/order_execution/interface/public/order_execution.py +107 -0
- quant_core/app/order_execution/interface/public/order_runtime.py +30 -0
- quant_core/app/order_execution/interface/public/order_submission.py +17 -0
- quant_core/app/order_execution/order_event_bus.py +28 -0
- quant_core/app/order_execution/order_execution_dependency.py +48 -0
- quant_core/app/order_execution/order_submission_service.py +301 -0
- quant_core/app/order_execution/schema/__init__.py +0 -0
- quant_core/app/order_execution/schema/order_request_schema.py +88 -0
- quant_core/app/order_execution/simulation_broker.py +883 -0
- quant_core/app/order_execution/slippage_model.py +40 -0
- quant_core/app/order_execution/tradability_policy.py +174 -0
- quant_core/app/order_execution/validators.py +40 -0
- quant_core/app/portfolio/SPEC.md +69 -0
- quant_core/app/portfolio/__init__.py +0 -0
- quant_core/app/portfolio/domain/__init__.py +32 -0
- quant_core/app/portfolio/domain/entitlement_entity.py +45 -0
- quant_core/app/portfolio/domain/portfolio_entity.py +511 -0
- quant_core/app/portfolio/domain/portfolio_event.py +59 -0
- quant_core/app/portfolio/domain/portfolio_value.py +21 -0
- quant_core/app/portfolio/domain/position_entity.py +165 -0
- quant_core/app/portfolio/domain/reservation_entity.py +69 -0
- quant_core/app/portfolio/interface/__init__.py +0 -0
- quant_core/app/portfolio/interface/public/__init__.py +0 -0
- quant_core/app/portfolio/interface/public/portfolio.py +77 -0
- quant_core/app/portfolio/interface/public/simulation_account.py +16 -0
- quant_core/app/portfolio/ledger_schema.py +130 -0
- quant_core/app/portfolio/order_account_listener.py +36 -0
- quant_core/app/portfolio/portfolio_dependency.py +21 -0
- quant_core/app/portfolio/portfolio_schema.py +204 -0
- quant_core/app/portfolio/simulated_portfolio.py +427 -0
- quant_core/app/research/SPEC.md +57 -0
- quant_core/app/research/__init__.py +0 -0
- quant_core/app/research/adapter/__init__.py +0 -0
- quant_core/app/research/adapter/market_label_query.py +84 -0
- quant_core/app/research/interface/__init__.py +0 -0
- quant_core/app/research/interface/research_dataset.py +47 -0
- quant_core/app/research/interface/research_evaluation.py +19 -0
- quant_core/app/research/schema/__init__.py +0 -0
- quant_core/app/research/schema/comparison_schema.py +12 -0
- quant_core/app/research/schema/dataset_schema.py +112 -0
- quant_core/app/research/schema/research_schema.py +120 -0
- quant_core/app/research/service/__init__.py +0 -0
- quant_core/app/research/service/research_dataset_service.py +80 -0
- quant_core/app/research/service/research_evaluation_service.py +56 -0
- quant_core/app/research/service/research_service.py +171 -0
- quant_core/app/strategy/SPEC.md +215 -0
- quant_core/app/strategy/__init__.py +1 -0
- quant_core/app/strategy/constants.py +87 -0
- quant_core/app/strategy/exceptions.py +32 -0
- quant_core/app/strategy/interface/__init__.py +0 -0
- quant_core/app/strategy/interface/public/__init__.py +0 -0
- quant_core/app/strategy/interface/public/portfolio_construction.py +19 -0
- quant_core/app/strategy/interface/public/strategy.py +30 -0
- quant_core/app/strategy/interface/public/strategy_market_query.py +53 -0
- quant_core/app/strategy/interface/public/strategy_portfolio_reader.py +33 -0
- quant_core/app/strategy/interface/strategy_factor_reader.py +19 -0
- quant_core/app/strategy/interface/strategy_order_reader.py +14 -0
- quant_core/app/strategy/schema/__init__.py +0 -0
- quant_core/app/strategy/schema/portfolio_schema.py +28 -0
- quant_core/app/strategy/schema/portfolio_target.py +70 -0
- quant_core/app/strategy/schema/python_strategy_schema.py +94 -0
- quant_core/app/strategy/schema/screener_schema.py +51 -0
- quant_core/app/strategy/schema/signal_schema.py +33 -0
- quant_core/app/strategy/schema/strategy_publication_schema.py +32 -0
- quant_core/app/strategy/schema/strategy_schema.py +96 -0
- quant_core/app/strategy/service/__init__.py +0 -0
- quant_core/app/strategy/service/factor_top_n_strategy.py +103 -0
- quant_core/app/strategy/service/portfolio_construction_service.py +142 -0
- quant_core/app/strategy/service/python_strategy_service.py +65 -0
- quant_core/app/strategy/service/screener_service.py +77 -0
- quant_core/app/strategy/service/store_factor_reader.py +120 -0
- quant_core/app/strategy/service/strategy_market_view.py +120 -0
- quant_core/app/strategy/service/strategy_runtime_service.py +319 -0
- quant_core/app/strategy/service/strategy_scheduler.py +50 -0
- quant_core/app/strategy/strategies/__init__.py +7 -0
- quant_core/app/strategy/strategies/momentum_volume_breakout.py +157 -0
- quant_core/app/strategy/strategy_context.py +316 -0
- quant_core/app/strategy/strategy_dependency.py +101 -0
- quant_core/app/trading_runtime/SPEC.md +70 -0
- quant_core/app/trading_runtime/__init__.py +1 -0
- quant_core/app/trading_runtime/domain/__init__.py +0 -0
- quant_core/app/trading_runtime/replay/__init__.py +0 -0
- quant_core/app/trading_runtime/replay/daily_state_expand.py +49 -0
- quant_core/app/trading_runtime/replay/exchange_state_resolver.py +90 -0
- quant_core/app/trading_runtime/replay/market_runtime_service.py +573 -0
- quant_core/app/trading_runtime/replay/market_state_resolver.py +150 -0
- quant_core/app/trading_runtime/runtime_ports.py +64 -0
- quant_core/app/trading_runtime/runtime_schema.py +129 -0
- quant_core/app/trading_runtime/runtime_session.py +361 -0
- quant_core/app/trading_runtime/strategy_portfolio_adapter.py +58 -0
- quant_core/app/trading_runtime/trading_rule_book.py +37 -0
- quant_core/app/trading_runtime/trading_rule_query.py +12 -0
- quant_core/common/__init__.py +0 -0
- quant_core/common/exceptions/__init__.py +0 -0
- quant_core/common/exceptions/base.py +89 -0
- quant_core/common/local_run.py +45 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""面向 CLI/Agent 的版本化选股报告契约。"""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict
|
|
9
|
+
|
|
10
|
+
from quant_core.app.factor.schema.factor_contract import FactorIdentity, FactorSnapshot
|
|
11
|
+
from quant_core.app.factor.schema.selection_types import SelectionDefinition, SelectionResult
|
|
12
|
+
|
|
13
|
+
SCHEMA_VERSION = "power-quant.selection.v1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContributionReport(BaseModel):
|
|
17
|
+
model_config = ConfigDict(frozen=True)
|
|
18
|
+
|
|
19
|
+
factor: FactorIdentity
|
|
20
|
+
value: Decimal
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SelectedReport(BaseModel):
|
|
24
|
+
model_config = ConfigDict(frozen=True)
|
|
25
|
+
|
|
26
|
+
instrument_id: str
|
|
27
|
+
rank: int
|
|
28
|
+
score: Decimal
|
|
29
|
+
contributions: tuple[ContributionReport, ...]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ExcludedReport(BaseModel):
|
|
33
|
+
model_config = ConfigDict(frozen=True)
|
|
34
|
+
|
|
35
|
+
instrument_id: str
|
|
36
|
+
reason: str
|
|
37
|
+
factor: FactorIdentity | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SelectionReport(BaseModel):
|
|
41
|
+
model_config = ConfigDict(frozen=True)
|
|
42
|
+
|
|
43
|
+
schema_version: str = SCHEMA_VERSION
|
|
44
|
+
selection_id: str
|
|
45
|
+
decision_time: datetime
|
|
46
|
+
universe_size: int
|
|
47
|
+
selected_count: int
|
|
48
|
+
excluded_count: int
|
|
49
|
+
dataset_revisions: tuple[str, ...]
|
|
50
|
+
selected: tuple[SelectedReport, ...]
|
|
51
|
+
excluded: tuple[ExcludedReport, ...]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_selection_report(
|
|
55
|
+
definition: SelectionDefinition,
|
|
56
|
+
universe: tuple[str, ...] | list[str],
|
|
57
|
+
snapshot: FactorSnapshot,
|
|
58
|
+
result: SelectionResult,
|
|
59
|
+
) -> SelectionReport:
|
|
60
|
+
payload = {
|
|
61
|
+
"factors": [
|
|
62
|
+
{
|
|
63
|
+
"identity": factor.identity.model_dump(mode="json"),
|
|
64
|
+
"weight": str(factor.weight),
|
|
65
|
+
"direction": factor.direction.value,
|
|
66
|
+
"transform": {
|
|
67
|
+
"winsorization": factor.transform.winsorization.value,
|
|
68
|
+
"normalization": factor.transform.normalization.value,
|
|
69
|
+
"neutralization": factor.transform.neutralization.value,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
for factor in definition.factors
|
|
73
|
+
],
|
|
74
|
+
"top_n": definition.top_n,
|
|
75
|
+
"missing_policy": definition.missing_policy.value,
|
|
76
|
+
}
|
|
77
|
+
selection_id = hashlib.sha256(
|
|
78
|
+
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
|
79
|
+
).hexdigest()[:24]
|
|
80
|
+
revisions = tuple(
|
|
81
|
+
sorted(
|
|
82
|
+
{
|
|
83
|
+
row.provenance.dataset_revision
|
|
84
|
+
for row in snapshot.values
|
|
85
|
+
if row.provenance.dataset_revision is not None
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
return SelectionReport(
|
|
90
|
+
selection_id=selection_id,
|
|
91
|
+
decision_time=snapshot.decision_time,
|
|
92
|
+
universe_size=len(set(universe)),
|
|
93
|
+
selected_count=len(result.selected),
|
|
94
|
+
excluded_count=len(result.excluded),
|
|
95
|
+
dataset_revisions=revisions,
|
|
96
|
+
selected=tuple(
|
|
97
|
+
SelectedReport(
|
|
98
|
+
instrument_id=row.instrument_id,
|
|
99
|
+
rank=row.rank,
|
|
100
|
+
score=row.score,
|
|
101
|
+
contributions=tuple(
|
|
102
|
+
ContributionReport(factor=factor, value=value)
|
|
103
|
+
for factor, value in row.contributions
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
for row in result.selected
|
|
107
|
+
),
|
|
108
|
+
excluded=tuple(
|
|
109
|
+
ExcludedReport(
|
|
110
|
+
instrument_id=row.instrument_id,
|
|
111
|
+
reason=row.reason,
|
|
112
|
+
factor=row.factor,
|
|
113
|
+
)
|
|
114
|
+
for row in result.excluded
|
|
115
|
+
),
|
|
116
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""T5 显式配置与可审计结果;不承载 Provider/订单语义。"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from quant_core.app.factor.schema.factor_contract import FactorIdentity
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SortDirection(str, Enum):
|
|
12
|
+
ASC = "asc"
|
|
13
|
+
DESC = "desc"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MissingPolicy(str, Enum):
|
|
17
|
+
EXCLUDE = "exclude"
|
|
18
|
+
ERROR = "error"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NormalizationMethod(str, Enum):
|
|
22
|
+
NONE = "none"
|
|
23
|
+
RANK = "rank"
|
|
24
|
+
ZSCORE = "zscore"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WinsorizationMethod(str, Enum):
|
|
28
|
+
NONE = "none"
|
|
29
|
+
MAD = "mad"
|
|
30
|
+
PERCENTILE = "percentile"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NeutralizationMethod(str, Enum):
|
|
34
|
+
NONE = "none"
|
|
35
|
+
INDUSTRY = "industry"
|
|
36
|
+
SIZE = "size"
|
|
37
|
+
INDUSTRY_SIZE = "industry_size"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class SelectionExposure:
|
|
42
|
+
industry: str | None = None
|
|
43
|
+
size: Decimal | None = None
|
|
44
|
+
available_at: datetime | None = None
|
|
45
|
+
dataset_revision: str | None = None
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
if self.available_at is not None and self.available_at.utcoffset() is None:
|
|
49
|
+
raise ValueError("exposure 可见时间必须带时区")
|
|
50
|
+
if self.size is not None and (not self.size.is_finite() or self.size <= 0):
|
|
51
|
+
raise ValueError("exposure 市值必须为有限正值")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class FactorTransform:
|
|
56
|
+
"""单个因子的横截面预处理;默认保持原始值,避免隐式改变旧口径。"""
|
|
57
|
+
|
|
58
|
+
winsorization: WinsorizationMethod = WinsorizationMethod.NONE
|
|
59
|
+
normalization: NormalizationMethod = NormalizationMethod.NONE
|
|
60
|
+
lower_quantile: Decimal = Decimal("0.01")
|
|
61
|
+
upper_quantile: Decimal = Decimal("0.99")
|
|
62
|
+
mad_scale: Decimal = Decimal("3")
|
|
63
|
+
neutralization: NeutralizationMethod = NeutralizationMethod.NONE
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
if not Decimal(0) <= self.lower_quantile < self.upper_quantile <= Decimal(1):
|
|
67
|
+
raise ValueError("winsorize quantile 必须满足 0 <= lower < upper <= 1")
|
|
68
|
+
if self.mad_scale <= 0:
|
|
69
|
+
raise ValueError("mad_scale 必须大于 0")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class FactorWeight:
|
|
74
|
+
identity: FactorIdentity
|
|
75
|
+
weight: Decimal
|
|
76
|
+
direction: SortDirection = SortDirection.DESC
|
|
77
|
+
transform: FactorTransform = FactorTransform()
|
|
78
|
+
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
if not self.weight.is_finite() or self.weight <= 0:
|
|
81
|
+
raise ValueError("factor weight 必须为正的有限 Decimal")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class FactorFilter:
|
|
86
|
+
identity: FactorIdentity
|
|
87
|
+
minimum: Decimal | None = None
|
|
88
|
+
maximum: Decimal | None = None
|
|
89
|
+
|
|
90
|
+
def __post_init__(self) -> None:
|
|
91
|
+
if self.minimum is None and self.maximum is None:
|
|
92
|
+
raise ValueError("filter 至少指定 minimum 或 maximum")
|
|
93
|
+
if self.minimum is not None and self.maximum is not None and self.minimum > self.maximum:
|
|
94
|
+
raise ValueError("filter minimum 不得大于 maximum")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class SelectionDefinition:
|
|
99
|
+
factors: tuple[FactorWeight, ...]
|
|
100
|
+
top_n: int
|
|
101
|
+
filters: tuple[FactorFilter, ...] = ()
|
|
102
|
+
missing_policy: MissingPolicy = MissingPolicy.EXCLUDE
|
|
103
|
+
|
|
104
|
+
def __post_init__(self) -> None:
|
|
105
|
+
if not self.factors or self.top_n < 1:
|
|
106
|
+
raise ValueError("selection 必须有因子且 top_n >= 1")
|
|
107
|
+
keys = [factor.identity.logical_key for factor in self.factors]
|
|
108
|
+
if len(keys) != len(set(keys)):
|
|
109
|
+
raise ValueError("duplicate factor identity in selection")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class SelectedInstrument:
|
|
114
|
+
instrument_id: str
|
|
115
|
+
score: Decimal
|
|
116
|
+
contributions: tuple[tuple[FactorIdentity, Decimal], ...]
|
|
117
|
+
rank: int
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class ExcludedInstrument:
|
|
122
|
+
instrument_id: str
|
|
123
|
+
reason: str
|
|
124
|
+
factor: FactorIdentity | None = None
|
|
125
|
+
score: Decimal | None = None
|
|
126
|
+
contributions: tuple[tuple[FactorIdentity, Decimal], ...] = ()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class SelectionResult:
|
|
131
|
+
selected: tuple[SelectedInstrument, ...]
|
|
132
|
+
excluded: tuple[ExcludedInstrument, ...]
|
|
133
|
+
|
|
134
|
+
def excluded_by_id(self, instrument_id: str) -> ExcludedInstrument:
|
|
135
|
+
return next(row for row in self.excluded if row.instrument_id == instrument_id)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Factor Service - 因子同步与查询编排(T3/T4)"""
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""数据库因子定义驱动的 Canonical 因子值生产。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
|
|
6
|
+
from quant_core.app.data.schema.market_schema import BarResponse
|
|
7
|
+
from quant_core.app.factor.constants import (
|
|
8
|
+
LOCAL_DATASET_REVISION,
|
|
9
|
+
LOCAL_FACTOR_SOURCE,
|
|
10
|
+
FactorFrequency,
|
|
11
|
+
FactorSourceKind,
|
|
12
|
+
FactorStatus,
|
|
13
|
+
)
|
|
14
|
+
from quant_core.app.factor.exceptions import (
|
|
15
|
+
FactorNotRegisteredException,
|
|
16
|
+
InsufficientFactorHistoryError,
|
|
17
|
+
)
|
|
18
|
+
from quant_core.app.factor.interface.factor_execution import WindowExecutor
|
|
19
|
+
from quant_core.app.factor.schema.factor_compute_request import FactorComputeRequest
|
|
20
|
+
from quant_core.app.factor.schema.factor_contract import (
|
|
21
|
+
FactorDefinition,
|
|
22
|
+
FactorIdentity,
|
|
23
|
+
FactorProvenance,
|
|
24
|
+
FactorValue,
|
|
25
|
+
)
|
|
26
|
+
from quant_core.app.factor.visibility import next_trading_day_open
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FactorComputeService:
|
|
30
|
+
"""只接收已持久化定义;源码执行方式由注入的 Runner 决定。"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
trading_days: list[date],
|
|
35
|
+
definitions: list[FactorDefinition],
|
|
36
|
+
runner: WindowExecutor,
|
|
37
|
+
) -> None:
|
|
38
|
+
self._trading_days = sorted(trading_days)
|
|
39
|
+
self._definitions = {item.identity: item for item in definitions}
|
|
40
|
+
if len(self._definitions) != len(definitions):
|
|
41
|
+
raise ValueError("同次计算不能含重复的完整因子身份")
|
|
42
|
+
self._runner = runner
|
|
43
|
+
|
|
44
|
+
def get(self, name: str | FactorIdentity) -> FactorDefinition:
|
|
45
|
+
"""按完整身份解析;旧名称入口仅允许唯一匹配。"""
|
|
46
|
+
if isinstance(name, FactorIdentity):
|
|
47
|
+
definition = self._definitions.get(name)
|
|
48
|
+
else:
|
|
49
|
+
matches = [item for key, item in self._definitions.items() if key.name == name]
|
|
50
|
+
if len(matches) > 1:
|
|
51
|
+
raise ValueError(f"因子名称存在歧义,必须指定完整身份: {name}")
|
|
52
|
+
definition = matches[0] if matches else None
|
|
53
|
+
if definition is None:
|
|
54
|
+
raise FactorNotRegisteredException(str(name))
|
|
55
|
+
return definition
|
|
56
|
+
|
|
57
|
+
def definitions(self) -> list[FactorDefinition]:
|
|
58
|
+
return list(self._definitions.values())
|
|
59
|
+
|
|
60
|
+
async def compute(
|
|
61
|
+
self,
|
|
62
|
+
factor_name: str,
|
|
63
|
+
instrument_id: str,
|
|
64
|
+
event_time: datetime,
|
|
65
|
+
bars: list[BarResponse],
|
|
66
|
+
*,
|
|
67
|
+
dataset_revision: str = LOCAL_DATASET_REVISION,
|
|
68
|
+
input_digest: str | None = None,
|
|
69
|
+
) -> FactorValue:
|
|
70
|
+
result = await self.compute_many(
|
|
71
|
+
[FactorComputeRequest(factor_name, instrument_id, event_time, bars, input_digest)],
|
|
72
|
+
dataset_revision=dataset_revision,
|
|
73
|
+
)
|
|
74
|
+
return result[0]
|
|
75
|
+
|
|
76
|
+
async def compute_many(
|
|
77
|
+
self,
|
|
78
|
+
requests: list[FactorComputeRequest],
|
|
79
|
+
*,
|
|
80
|
+
dataset_revision: str,
|
|
81
|
+
) -> list[FactorValue]:
|
|
82
|
+
"""先完成时点/频率校验,再将所有可计算格打包交给 Runner。"""
|
|
83
|
+
prepared: list[tuple[FactorComputeRequest, FactorDefinition, datetime, int | None]] = []
|
|
84
|
+
calls: list[tuple[str, list[BarResponse], dict]] = []
|
|
85
|
+
for request in requests:
|
|
86
|
+
definition = self.get(request.identity or request.factor_name)
|
|
87
|
+
spec = definition.computation_spec
|
|
88
|
+
if spec is None or definition.provenance_template is None:
|
|
89
|
+
raise ValueError("因子定义缺少 Python 源码或摘要")
|
|
90
|
+
if definition.identity.source != LOCAL_FACTOR_SOURCE:
|
|
91
|
+
raise ValueError("只执行本地 Python 因子")
|
|
92
|
+
if spec.input_kind != "window":
|
|
93
|
+
raise ValueError("面板因子必须按完整横截面计算")
|
|
94
|
+
if spec.frequency is not FactorFrequency.DAILY:
|
|
95
|
+
raise NotImplementedError("分钟因子计算尚未实现;不能套用日频可见口径")
|
|
96
|
+
if any(bar.frequency != spec.frequency.value for bar in request.bars):
|
|
97
|
+
raise ValueError("输入 Bar 频率与因子定义不一致")
|
|
98
|
+
available_at = next_trading_day_open(request.event_time.date(), self._trading_days)
|
|
99
|
+
if available_at is None:
|
|
100
|
+
raise InsufficientFactorHistoryError(
|
|
101
|
+
f"交易日历无 {request.event_time.date()} 的下一交易日"
|
|
102
|
+
)
|
|
103
|
+
call_index = None
|
|
104
|
+
if len(request.bars) >= spec.lookback:
|
|
105
|
+
call_index = len(calls)
|
|
106
|
+
calls.append((spec.python_source, list(request.bars), spec.parameters))
|
|
107
|
+
prepared.append((request, definition, available_at, call_index))
|
|
108
|
+
computed = await asyncio.to_thread(self._runner, calls) if calls else []
|
|
109
|
+
if len(computed) != len(calls):
|
|
110
|
+
raise ValueError("因子执行结果数与请求数不一致")
|
|
111
|
+
results: list[FactorValue] = []
|
|
112
|
+
for request, definition, available_at, call_index in prepared:
|
|
113
|
+
value = computed[call_index] if call_index is not None else None
|
|
114
|
+
template = definition.provenance_template
|
|
115
|
+
assert template is not None
|
|
116
|
+
results.append(
|
|
117
|
+
FactorValue(
|
|
118
|
+
identity=definition.identity,
|
|
119
|
+
instrument_id=request.instrument_id,
|
|
120
|
+
as_of=request.event_time,
|
|
121
|
+
available_at=available_at,
|
|
122
|
+
value=value,
|
|
123
|
+
status=FactorStatus.AVAILABLE if value is not None else FactorStatus.MISSING,
|
|
124
|
+
provenance=FactorProvenance(
|
|
125
|
+
source_kind=FactorSourceKind.LOCAL_CALCULATOR,
|
|
126
|
+
provider_id=LOCAL_FACTOR_SOURCE,
|
|
127
|
+
retrieval_ref="python:compute",
|
|
128
|
+
content_digest=template.content_digest,
|
|
129
|
+
dataset_revision=dataset_revision,
|
|
130
|
+
note=(
|
|
131
|
+
f"input_sha256={request.input_digest}"
|
|
132
|
+
if request.input_digest is not None
|
|
133
|
+
else f"bar@{request.bars[-1].timestamp.isoformat()}"
|
|
134
|
+
if request.bars
|
|
135
|
+
else None
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
return results
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""以真实 PIT 截面和复权历史价构造可解释的因子研究样本。"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import date, datetime, time, timedelta
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from zoneinfo import ZoneInfo
|
|
7
|
+
|
|
8
|
+
from quant_core.app.data.interface.market_data_query import IMarketDataQuery
|
|
9
|
+
from quant_core.app.data.schema.adjustment_schema import AdjustmentType, BarViewRequest
|
|
10
|
+
from quant_core.app.factor.constants import FactorStatus
|
|
11
|
+
from quant_core.app.factor.interface.public.factor_query import IFactorQuery
|
|
12
|
+
from quant_core.app.factor.schema.diagnostic_types import CrossSection, FactorDiagnostics
|
|
13
|
+
from quant_core.app.factor.schema.factor_contract import FactorIdentity
|
|
14
|
+
from quant_core.app.factor.service.factor_diagnostics_service import FactorDiagnosticsService
|
|
15
|
+
|
|
16
|
+
CN = ZoneInfo("Asia/Shanghai")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class DiagnosticHorizon:
|
|
21
|
+
trading_days: int
|
|
22
|
+
result: FactorDiagnostics
|
|
23
|
+
paired_samples: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class FactorDiagnosticRun:
|
|
28
|
+
factor: FactorIdentity
|
|
29
|
+
start: date
|
|
30
|
+
end: date
|
|
31
|
+
universe_size: int
|
|
32
|
+
factor_cells: int
|
|
33
|
+
available_cells: int
|
|
34
|
+
price_basis: str
|
|
35
|
+
horizons: tuple[DiagnosticHorizon, ...]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FactorDiagnosticRunner:
|
|
39
|
+
"""只编排 Market/Factor 公共查询;IC 数学仍由原诊断服务完成。"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, market: IMarketDataQuery, factors: IFactorQuery) -> None:
|
|
42
|
+
self._market = market
|
|
43
|
+
self._factors = factors
|
|
44
|
+
|
|
45
|
+
async def run(
|
|
46
|
+
self,
|
|
47
|
+
identity: FactorIdentity,
|
|
48
|
+
universe: tuple[str, ...],
|
|
49
|
+
start: date,
|
|
50
|
+
end: date,
|
|
51
|
+
horizons: tuple[int, ...] = (1, 5, 10),
|
|
52
|
+
) -> FactorDiagnosticRun:
|
|
53
|
+
if start > end or not universe or not horizons or any(h < 1 for h in horizons):
|
|
54
|
+
raise ValueError("诊断需要非空股票池、有效日期区间和正的观察窗口")
|
|
55
|
+
calendar = await self._market.list_trading_days(
|
|
56
|
+
"XSHG", start, end + timedelta(days=max(horizons) * 3 + 15)
|
|
57
|
+
)
|
|
58
|
+
days = [row.trade_date for row in calendar if row.is_open]
|
|
59
|
+
anchors = [day for day in days if start <= day <= end]
|
|
60
|
+
positions = {day: index for index, day in enumerate(days)}
|
|
61
|
+
if not anchors or positions[anchors[-1]] + max(horizons) + 1 >= len(days):
|
|
62
|
+
raise ValueError("交易日历不足以覆盖因子诊断的前瞻收益窗口")
|
|
63
|
+
normalized_universe = tuple(sorted(set(universe)))
|
|
64
|
+
eligible_by_day = {}
|
|
65
|
+
for day in anchors:
|
|
66
|
+
listed = {row.instrument_id for row in await self._market.list_instruments_on(day)}
|
|
67
|
+
eligible_by_day[day] = tuple(
|
|
68
|
+
instrument_id for instrument_id in normalized_universe if instrument_id in listed
|
|
69
|
+
)
|
|
70
|
+
prices: dict[str, dict[date, Decimal]] = {}
|
|
71
|
+
last_day = days[positions[anchors[-1]] + max(horizons) + 1]
|
|
72
|
+
for instrument_id in sorted({stock for pool in eligible_by_day.values() for stock in pool}):
|
|
73
|
+
bars = await self._market.get_adjusted_bars(
|
|
74
|
+
instrument_id,
|
|
75
|
+
"1d",
|
|
76
|
+
BarViewRequest(adjustment_type=AdjustmentType.POST),
|
|
77
|
+
datetime.combine(start, time.min, tzinfo=CN),
|
|
78
|
+
datetime.combine(last_day, time.max, tzinfo=CN),
|
|
79
|
+
100000,
|
|
80
|
+
)
|
|
81
|
+
prices[instrument_id] = {
|
|
82
|
+
bar.timestamp.astimezone(CN).date(): bar.close for bar in bars.bars
|
|
83
|
+
}
|
|
84
|
+
sections = {h: [] for h in horizons}
|
|
85
|
+
available_cells = 0
|
|
86
|
+
factor_cells = 0
|
|
87
|
+
for day in anchors:
|
|
88
|
+
position = positions[day]
|
|
89
|
+
visible_day = days[position + 1]
|
|
90
|
+
decision_time = datetime.combine(visible_day, time(9, 31), tzinfo=CN)
|
|
91
|
+
eligible = eligible_by_day[day]
|
|
92
|
+
factor_cells += len(eligible)
|
|
93
|
+
snapshot = await self._factors.get_snapshot(
|
|
94
|
+
[identity], list(eligible), decision_time, target_as_of=day
|
|
95
|
+
)
|
|
96
|
+
factor_values = {
|
|
97
|
+
row.instrument_id: row.value
|
|
98
|
+
for row in snapshot.values
|
|
99
|
+
if row.status is FactorStatus.AVAILABLE and row.value is not None
|
|
100
|
+
}
|
|
101
|
+
available_cells += len(factor_values)
|
|
102
|
+
for horizon in horizons:
|
|
103
|
+
end_day = days[position + 1 + horizon]
|
|
104
|
+
returns = {}
|
|
105
|
+
for instrument_id in factor_values:
|
|
106
|
+
base = prices[instrument_id].get(visible_day)
|
|
107
|
+
final = prices[instrument_id].get(end_day)
|
|
108
|
+
if base is not None and final is not None and base > 0:
|
|
109
|
+
returns[instrument_id] = final / base - Decimal(1)
|
|
110
|
+
sections[horizon].append(CrossSection(day, factor_values, returns))
|
|
111
|
+
analyzer = FactorDiagnosticsService()
|
|
112
|
+
return FactorDiagnosticRun(
|
|
113
|
+
factor=identity,
|
|
114
|
+
start=start,
|
|
115
|
+
end=end,
|
|
116
|
+
universe_size=len(normalized_universe),
|
|
117
|
+
factor_cells=factor_cells,
|
|
118
|
+
available_cells=available_cells,
|
|
119
|
+
price_basis=(
|
|
120
|
+
"后复权收盘价:因子 T 日收盘归属、T+1 开盘可见;"
|
|
121
|
+
"收益从 T+1 收盘至 T+1+N 收盘;仅供诊断,不代表成交"
|
|
122
|
+
),
|
|
123
|
+
horizons=tuple(
|
|
124
|
+
DiagnosticHorizon(
|
|
125
|
+
trading_days=horizon,
|
|
126
|
+
result=analyzer.analyze(tuple(sections[horizon])),
|
|
127
|
+
paired_samples=sum(
|
|
128
|
+
len(section.factor_values.keys() & section.forward_returns.keys())
|
|
129
|
+
for section in sections[horizon]
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
for horizon in horizons
|
|
133
|
+
),
|
|
134
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""确定性的纯因子诊断计算;数据提取由调用方负责。"""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal, localcontext
|
|
4
|
+
|
|
5
|
+
from quant_core.app.factor.schema.diagnostic_types import (
|
|
6
|
+
CrossSection,
|
|
7
|
+
DecayPoint,
|
|
8
|
+
FactorDiagnostics,
|
|
9
|
+
IcPoint,
|
|
10
|
+
QuantileReturn,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FactorDiagnosticsService:
|
|
15
|
+
def analyze(
|
|
16
|
+
self,
|
|
17
|
+
sections: tuple[CrossSection, ...],
|
|
18
|
+
*,
|
|
19
|
+
quantiles: int = 5,
|
|
20
|
+
selections: tuple[tuple[str, ...], ...] = (),
|
|
21
|
+
comparison_values: dict[str, Decimal] | None = None,
|
|
22
|
+
horizon_sections: dict[int, tuple[CrossSection, ...]] | None = None,
|
|
23
|
+
) -> FactorDiagnostics:
|
|
24
|
+
if quantiles < 2:
|
|
25
|
+
raise ValueError("quantiles 必须至少为 2")
|
|
26
|
+
points = tuple(
|
|
27
|
+
self._ic_point(section) for section in sorted(sections, key=lambda x: x.as_of)
|
|
28
|
+
)
|
|
29
|
+
return FactorDiagnostics(
|
|
30
|
+
ic_series=points,
|
|
31
|
+
quantile_returns=self._quantile_returns(sections, quantiles),
|
|
32
|
+
decay=self._decay(horizon_sections or {}),
|
|
33
|
+
selection_turnover=self._turnover(selections),
|
|
34
|
+
factor_correlation=(
|
|
35
|
+
_correlation(sections[-1].factor_values, comparison_values)
|
|
36
|
+
if sections and comparison_values is not None
|
|
37
|
+
else None
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def _ic_point(section: CrossSection) -> IcPoint:
|
|
43
|
+
common = sorted(section.factor_values.keys() & section.forward_returns.keys())
|
|
44
|
+
factor = {key: section.factor_values[key] for key in common}
|
|
45
|
+
returns = {key: section.forward_returns[key] for key in common}
|
|
46
|
+
return IcPoint(
|
|
47
|
+
as_of=section.as_of,
|
|
48
|
+
ic=_correlation(factor, returns),
|
|
49
|
+
rank_ic=_correlation(_ranks(factor), _ranks(returns)),
|
|
50
|
+
sample_size=len(common),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _quantile_returns(
|
|
55
|
+
sections: tuple[CrossSection, ...], quantiles: int
|
|
56
|
+
) -> tuple[QuantileReturn, ...]:
|
|
57
|
+
buckets: list[list[Decimal]] = [[] for _ in range(quantiles)]
|
|
58
|
+
for section in sections:
|
|
59
|
+
common = sorted(
|
|
60
|
+
section.factor_values.keys() & section.forward_returns.keys(),
|
|
61
|
+
key=lambda key: (section.factor_values[key], key),
|
|
62
|
+
)
|
|
63
|
+
for index, instrument_id in enumerate(common):
|
|
64
|
+
bucket = min(index * quantiles // len(common), quantiles - 1) if common else 0
|
|
65
|
+
buckets[bucket].append(section.forward_returns[instrument_id])
|
|
66
|
+
return tuple(
|
|
67
|
+
QuantileReturn(
|
|
68
|
+
quantile=index + 1,
|
|
69
|
+
mean_return=(sum(values, Decimal(0)) / Decimal(len(values)) if values else None),
|
|
70
|
+
sample_size=len(values),
|
|
71
|
+
)
|
|
72
|
+
for index, values in enumerate(buckets)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _decay(
|
|
77
|
+
horizon_sections: dict[int, tuple[CrossSection, ...]],
|
|
78
|
+
) -> tuple[DecayPoint, ...]:
|
|
79
|
+
result: list[DecayPoint] = []
|
|
80
|
+
for horizon, sections in sorted(horizon_sections.items()):
|
|
81
|
+
if horizon < 1:
|
|
82
|
+
raise ValueError("decay horizon 必须大于 0")
|
|
83
|
+
values = [
|
|
84
|
+
point.rank_ic
|
|
85
|
+
for point in (FactorDiagnosticsService._ic_point(section) for section in sections)
|
|
86
|
+
if point.rank_ic is not None
|
|
87
|
+
]
|
|
88
|
+
result.append(
|
|
89
|
+
DecayPoint(
|
|
90
|
+
lag=horizon,
|
|
91
|
+
mean_rank_ic=(
|
|
92
|
+
sum(values, Decimal(0)) / Decimal(len(values)) if values else None
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
return tuple(result)
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _turnover(selections: tuple[tuple[str, ...], ...]) -> Decimal | None:
|
|
100
|
+
if len(selections) < 2:
|
|
101
|
+
return None
|
|
102
|
+
rates: list[Decimal] = []
|
|
103
|
+
for previous, current in zip(selections, selections[1:], strict=False):
|
|
104
|
+
prior = set(previous)
|
|
105
|
+
now = set(current)
|
|
106
|
+
denominator = max(len(prior), len(now))
|
|
107
|
+
rates.append(Decimal(1) - Decimal(len(prior & now)) / Decimal(denominator or 1))
|
|
108
|
+
return sum(rates, Decimal(0)) / Decimal(len(rates))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _ranks(values: dict[str, Decimal]) -> dict[str, Decimal]:
|
|
112
|
+
groups: dict[Decimal, list[str]] = {}
|
|
113
|
+
for key, value in values.items():
|
|
114
|
+
groups.setdefault(value, []).append(key)
|
|
115
|
+
result: dict[str, Decimal] = {}
|
|
116
|
+
cursor = 1
|
|
117
|
+
for value in sorted(groups):
|
|
118
|
+
members = groups[value]
|
|
119
|
+
rank = (Decimal(cursor) + Decimal(cursor + len(members) - 1)) / Decimal(2)
|
|
120
|
+
result.update({key: rank for key in members})
|
|
121
|
+
cursor += len(members)
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _correlation(left: dict[str, Decimal], right: dict[str, Decimal]) -> Decimal | None:
|
|
126
|
+
keys = sorted(left.keys() & right.keys())
|
|
127
|
+
if len(keys) < 2:
|
|
128
|
+
return None
|
|
129
|
+
left_mean = sum((left[key] for key in keys), Decimal(0)) / Decimal(len(keys))
|
|
130
|
+
right_mean = sum((right[key] for key in keys), Decimal(0)) / Decimal(len(keys))
|
|
131
|
+
covariance = sum(
|
|
132
|
+
((left[key] - left_mean) * (right[key] - right_mean) for key in keys), Decimal(0)
|
|
133
|
+
)
|
|
134
|
+
left_variance = sum(((left[key] - left_mean) ** 2 for key in keys), Decimal(0))
|
|
135
|
+
right_variance = sum(((right[key] - right_mean) ** 2 for key in keys), Decimal(0))
|
|
136
|
+
if not left_variance or not right_variance:
|
|
137
|
+
return None
|
|
138
|
+
with localcontext() as context:
|
|
139
|
+
context.prec = 28
|
|
140
|
+
return covariance / (left_variance * right_variance).sqrt()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""通过 Market 已有 PIT 基础指标读取市值暴露;不猜测历史行业分类。"""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from quant_core.app.data.interface.market_data_query import IMarketDataQuery
|
|
7
|
+
from quant_core.app.factor.schema.selection_types import SelectionExposure
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def query_exposures(
|
|
11
|
+
market: IMarketDataQuery, instrument_ids: tuple[str, ...], decision_time: datetime
|
|
12
|
+
) -> dict[str, SelectionExposure]:
|
|
13
|
+
"""查询指定时点可见市值,行业缺失保持缺值,不推测历史分类。"""
|
|
14
|
+
rows = await market.list_daily_basic_at(decision_time)
|
|
15
|
+
selected = set(instrument_ids)
|
|
16
|
+
return {
|
|
17
|
+
row.instrument_id: SelectionExposure(
|
|
18
|
+
size=row.total_market_cap,
|
|
19
|
+
available_at=decision_time,
|
|
20
|
+
dataset_revision=hashlib.sha256(row.model_dump_json().encode()).hexdigest(),
|
|
21
|
+
)
|
|
22
|
+
for row in rows
|
|
23
|
+
if row.instrument_id in selected
|
|
24
|
+
}
|