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,84 @@
|
|
|
1
|
+
"""研究专用的未来收益标签读取,绝不注入 StrategyContext。"""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from datetime import 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.research.schema.dataset_schema import ResearchLabel, ResearchSampleRequest
|
|
11
|
+
|
|
12
|
+
_CN = ZoneInfo("Asia/Shanghai")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MarketLabelQuery:
|
|
16
|
+
"""以决策日收盘到 N 个交易日后收盘的后复权收益作标签。"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, market: IMarketDataQuery) -> None:
|
|
19
|
+
self._market = market
|
|
20
|
+
|
|
21
|
+
async def forward_returns(
|
|
22
|
+
self, samples: tuple[ResearchSampleRequest, ...], horizon: int
|
|
23
|
+
) -> tuple[ResearchLabel, ...]:
|
|
24
|
+
if horizon < 1:
|
|
25
|
+
raise ValueError("标签窗口必须为正整数")
|
|
26
|
+
if not samples:
|
|
27
|
+
return ()
|
|
28
|
+
first = min(item.decision_time.astimezone(_CN).date() for item in samples)
|
|
29
|
+
last = max(item.decision_time.astimezone(_CN).date() for item in samples)
|
|
30
|
+
calendar = await self._market.list_trading_days(
|
|
31
|
+
"XSHG", first, last + timedelta(days=horizon * 3 + 30)
|
|
32
|
+
)
|
|
33
|
+
days = sorted({row.trade_date for row in calendar if row.is_open})
|
|
34
|
+
ends = {}
|
|
35
|
+
for sample in samples:
|
|
36
|
+
day = sample.decision_time.astimezone(_CN).date()
|
|
37
|
+
if day not in days or days.index(day) + horizon >= len(days):
|
|
38
|
+
raise ValueError("交易日历不足以覆盖研究标签")
|
|
39
|
+
# 日收盘后的决策不得使用已经过去的入场价。
|
|
40
|
+
if sample.decision_time >= datetime.combine(day, time(15), tzinfo=_CN):
|
|
41
|
+
raise ValueError("该标签口径只适用于决策日收盘前的决策")
|
|
42
|
+
ends[sample.decision_time] = days[days.index(day) + horizon]
|
|
43
|
+
prices = {}
|
|
44
|
+
revisions = {}
|
|
45
|
+
for instrument in sorted(
|
|
46
|
+
{instrument for sample in samples for instrument in sample.universe}
|
|
47
|
+
):
|
|
48
|
+
series = await self._market.get_adjusted_bars(
|
|
49
|
+
instrument,
|
|
50
|
+
"1d",
|
|
51
|
+
BarViewRequest(adjustment_type=AdjustmentType.POST),
|
|
52
|
+
datetime.combine(first, time.min, tzinfo=_CN),
|
|
53
|
+
datetime.combine(max(ends.values()), time.max, tzinfo=_CN),
|
|
54
|
+
100000,
|
|
55
|
+
)
|
|
56
|
+
if len(series.bars) >= 100000:
|
|
57
|
+
raise ValueError("标签行情达到查询上限,拒绝静默截断")
|
|
58
|
+
prices[instrument] = {
|
|
59
|
+
bar.timestamp.astimezone(_CN).date(): bar.close for bar in series.bars
|
|
60
|
+
}
|
|
61
|
+
revisions[instrument] = hashlib.sha256(series.model_dump_json().encode()).hexdigest()
|
|
62
|
+
labels = []
|
|
63
|
+
for sample in samples:
|
|
64
|
+
day = sample.decision_time.astimezone(_CN).date()
|
|
65
|
+
end = ends[sample.decision_time]
|
|
66
|
+
for instrument in sample.universe:
|
|
67
|
+
initial = prices[instrument].get(day)
|
|
68
|
+
final = prices[instrument].get(end)
|
|
69
|
+
value = (
|
|
70
|
+
final / initial - Decimal(1)
|
|
71
|
+
if initial is not None and final is not None and initial > 0
|
|
72
|
+
else None
|
|
73
|
+
)
|
|
74
|
+
labels.append(
|
|
75
|
+
ResearchLabel(
|
|
76
|
+
sample.decision_time,
|
|
77
|
+
instrument,
|
|
78
|
+
datetime.combine(end, time(15), tzinfo=_CN),
|
|
79
|
+
value,
|
|
80
|
+
"post-adjusted-close-to-close",
|
|
81
|
+
revisions[instrument],
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return tuple(labels)
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""研究数据集输入和标签构造能力,禁止标签流入因子查询接口。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from quant_core.app.factor.interface.public.factor_query import IFactorQuery
|
|
7
|
+
from quant_core.app.factor.schema.factor_contract import FactorIdentity
|
|
8
|
+
from quant_core.app.research.schema.dataset_schema import (
|
|
9
|
+
ResearchDataset,
|
|
10
|
+
ResearchLabel,
|
|
11
|
+
ResearchSampleRequest,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IResearchLabelQuery(Protocol):
|
|
16
|
+
async def forward_returns(
|
|
17
|
+
self, samples: tuple[ResearchSampleRequest, ...], horizon: int
|
|
18
|
+
) -> tuple[ResearchLabel, ...]: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IResearchDatasetBuilder(Protocol):
|
|
22
|
+
revisions: dict[str, str]
|
|
23
|
+
|
|
24
|
+
async def build(
|
|
25
|
+
self,
|
|
26
|
+
identities: tuple[FactorIdentity, ...],
|
|
27
|
+
samples: tuple[ResearchSampleRequest, ...],
|
|
28
|
+
*,
|
|
29
|
+
factor_revision: str,
|
|
30
|
+
label_horizon: int | None = None,
|
|
31
|
+
) -> ResearchDataset: ...
|
|
32
|
+
|
|
33
|
+
def iter_batches(
|
|
34
|
+
self,
|
|
35
|
+
identities: tuple[FactorIdentity, ...],
|
|
36
|
+
samples: tuple[ResearchSampleRequest, ...],
|
|
37
|
+
*,
|
|
38
|
+
factor_revision: str,
|
|
39
|
+
batch_size: int = 32,
|
|
40
|
+
label_horizon: int | None = None,
|
|
41
|
+
) -> AsyncIterator[ResearchDataset]:
|
|
42
|
+
"""按决策时点分块读取,不在构建器内保留全部特征与标签。"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class IResearchDatasetFactory(Protocol):
|
|
47
|
+
def __call__(self, factors: IFactorQuery) -> IResearchDatasetBuilder: ...
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""数据集评价接口,训练标签只进入评价端。"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from quant_core.app.factor.schema.diagnostic_types import FactorDiagnostics
|
|
7
|
+
from quant_core.app.factor.schema.selection_types import SelectionDefinition, SelectionExposure
|
|
8
|
+
from quant_core.app.research.schema.dataset_schema import ResearchDataset
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IResearchEvaluation(Protocol):
|
|
12
|
+
def evaluate(
|
|
13
|
+
self,
|
|
14
|
+
dataset: ResearchDataset,
|
|
15
|
+
definition: SelectionDefinition,
|
|
16
|
+
*,
|
|
17
|
+
exposures: dict[datetime, dict[str, SelectionExposure]] | None = None,
|
|
18
|
+
quantiles: int = 5,
|
|
19
|
+
) -> FactorDiagnostics: ...
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""参数实验比较投影,结果只引用已经持久化的运行证据。"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from quant_core.app.research.schema.research_schema import ResearchRunRecord
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResearchComparison(BaseModel):
|
|
9
|
+
model_config = ConfigDict(frozen=True)
|
|
10
|
+
runs: tuple[ResearchRunRecord, ...]
|
|
11
|
+
comparable: bool
|
|
12
|
+
reasons: tuple[str, ...]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""研究特征与前瞻标签分离;样本切分负责标签跨界清除。"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
|
|
7
|
+
from quant_core.app.factor.schema.factor_contract import FactorSnapshot
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ResearchSampleRequest:
|
|
12
|
+
as_of: datetime
|
|
13
|
+
decision_time: datetime
|
|
14
|
+
universe: tuple[str, ...]
|
|
15
|
+
|
|
16
|
+
def __post_init__(self) -> None:
|
|
17
|
+
if self.as_of.utcoffset() is None or self.decision_time.utcoffset() is None:
|
|
18
|
+
raise ValueError("研究时间必须带时区")
|
|
19
|
+
if self.as_of > self.decision_time or len(self.universe) != len(set(self.universe)):
|
|
20
|
+
raise ValueError("研究业务时点或股票池非法")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class FeatureSlice:
|
|
25
|
+
request: ResearchSampleRequest
|
|
26
|
+
snapshot: FactorSnapshot
|
|
27
|
+
|
|
28
|
+
def __post_init__(self) -> None:
|
|
29
|
+
if self.snapshot.decision_time != self.request.decision_time:
|
|
30
|
+
raise ValueError("特征截面与样本决策时间不一致")
|
|
31
|
+
if any(row.as_of.date() != self.request.as_of.date() for row in self.snapshot.values):
|
|
32
|
+
raise ValueError("特征截面业务日期与请求不一致")
|
|
33
|
+
if any(row.instrument_id not in self.request.universe for row in self.snapshot.values):
|
|
34
|
+
raise ValueError("特征截面包含样本股票池之外的证券")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ResearchLabel:
|
|
39
|
+
decision_time: datetime
|
|
40
|
+
instrument_id: str
|
|
41
|
+
end_time: datetime
|
|
42
|
+
value: Decimal | None
|
|
43
|
+
price_basis: str
|
|
44
|
+
data_revision: str
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
if self.decision_time.utcoffset() is None or self.end_time.utcoffset() is None:
|
|
48
|
+
raise ValueError("标签时间必须带时区")
|
|
49
|
+
if self.end_time <= self.decision_time:
|
|
50
|
+
raise ValueError("前瞻标签终点必须晚于决策时间")
|
|
51
|
+
if self.value is not None and not self.value.is_finite():
|
|
52
|
+
raise ValueError("标签必须为有限值或缺值")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class DatasetPartition:
|
|
57
|
+
name: str
|
|
58
|
+
start: datetime
|
|
59
|
+
end: datetime
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
if (
|
|
63
|
+
not self.name
|
|
64
|
+
or self.start.utcoffset() is None
|
|
65
|
+
or self.end.utcoffset() is None
|
|
66
|
+
or self.start >= self.end
|
|
67
|
+
):
|
|
68
|
+
raise ValueError("研究分区需要名称及合法的带时区半开区间")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class ResearchDataset:
|
|
73
|
+
"""特征可交给共享评分器;标签只能交给研究评价器。"""
|
|
74
|
+
|
|
75
|
+
features: tuple[FeatureSlice, ...]
|
|
76
|
+
labels: tuple[ResearchLabel, ...] = ()
|
|
77
|
+
|
|
78
|
+
def __post_init__(self) -> None:
|
|
79
|
+
times = [item.request.decision_time for item in self.features]
|
|
80
|
+
if times != sorted(set(times)):
|
|
81
|
+
raise ValueError("研究特征必须按唯一决策时间排序")
|
|
82
|
+
valid = {
|
|
83
|
+
(item.request.decision_time, instrument)
|
|
84
|
+
for item in self.features
|
|
85
|
+
for instrument in item.request.universe
|
|
86
|
+
}
|
|
87
|
+
keys = [(label.decision_time, label.instrument_id) for label in self.labels]
|
|
88
|
+
if len(keys) != len(set(keys)) or set(keys) - valid:
|
|
89
|
+
raise ValueError("标签重复或不属于特征样本")
|
|
90
|
+
|
|
91
|
+
def split(self, partitions: tuple[DatasetPartition, ...]) -> dict[str, "ResearchDataset"]:
|
|
92
|
+
"""半开时间分区;标签跨越分区末端时清除该证券的标签,保留特征。"""
|
|
93
|
+
ordered = sorted(partitions, key=lambda part: part.start)
|
|
94
|
+
if len({part.name for part in partitions}) != len(partitions):
|
|
95
|
+
raise ValueError("分区名称重复")
|
|
96
|
+
if any(left.end > right.start for left, right in zip(ordered, ordered[1:], strict=False)):
|
|
97
|
+
raise ValueError("训练/验证/测试分区不得重叠")
|
|
98
|
+
return {
|
|
99
|
+
part.name: ResearchDataset(
|
|
100
|
+
features=tuple(
|
|
101
|
+
item
|
|
102
|
+
for item in self.features
|
|
103
|
+
if part.start <= item.request.decision_time < part.end
|
|
104
|
+
),
|
|
105
|
+
labels=tuple(
|
|
106
|
+
label
|
|
107
|
+
for label in self.labels
|
|
108
|
+
if part.start <= label.decision_time < part.end and label.end_time < part.end
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
for part in partitions
|
|
112
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""可编辑研究与不可变运行证据的输入输出契约。"""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import hashlib
|
|
5
|
+
from datetime import date
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResearchWorkspace(BaseModel):
|
|
13
|
+
model_config = ConfigDict(frozen=True)
|
|
14
|
+
|
|
15
|
+
research_id: str = Field(pattern=r"^[a-z][a-z0-9_]*$", max_length=64)
|
|
16
|
+
name: str = Field(min_length=1, max_length=128)
|
|
17
|
+
description: str = ""
|
|
18
|
+
python_source: str = Field(min_length=1, max_length=65536)
|
|
19
|
+
default_parameters: dict[str, Any] = Field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def source_digest(self) -> str:
|
|
23
|
+
return hashlib.sha256(self.python_source.encode("utf-8")).hexdigest()
|
|
24
|
+
|
|
25
|
+
def validate_source(self) -> None:
|
|
26
|
+
module = ast.parse(self.python_source)
|
|
27
|
+
callbacks = [
|
|
28
|
+
node
|
|
29
|
+
for node in module.body
|
|
30
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == "run"
|
|
31
|
+
]
|
|
32
|
+
if len(callbacks) != 1:
|
|
33
|
+
raise ValueError("研究源码必须定义唯一的 run(context, parameters)")
|
|
34
|
+
node = callbacks[0]
|
|
35
|
+
if (
|
|
36
|
+
len(node.args.posonlyargs) + len(node.args.args) != 2
|
|
37
|
+
or node.args.vararg is not None
|
|
38
|
+
or node.args.kwarg is not None
|
|
39
|
+
or node.args.kwonlyargs
|
|
40
|
+
):
|
|
41
|
+
raise ValueError("run 回调必须声明 context、parameters 两个位置参数")
|
|
42
|
+
if len(self.python_source.encode("utf-8")) > 65536:
|
|
43
|
+
raise ValueError("研究源码超过 64 KiB")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ResearchMetric(BaseModel):
|
|
47
|
+
key: str
|
|
48
|
+
label: str
|
|
49
|
+
value: Decimal
|
|
50
|
+
unit: Literal["percent", "cny", "count", "ratio"]
|
|
51
|
+
explanation: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ResearchEvidence(BaseModel):
|
|
55
|
+
"""研究实验的可读摘要与可检查的逐样本证据。"""
|
|
56
|
+
|
|
57
|
+
question: str = Field(min_length=1)
|
|
58
|
+
conclusion: str = Field(min_length=1)
|
|
59
|
+
methodology: str = Field(min_length=1)
|
|
60
|
+
sample_start: date
|
|
61
|
+
sample_end: date
|
|
62
|
+
candidate_count: int = Field(ge=0)
|
|
63
|
+
observations: list[dict[str, Any]] = Field(default_factory=list)
|
|
64
|
+
exclusions: list[dict[str, Any]] = Field(default_factory=list)
|
|
65
|
+
metrics: list[ResearchMetric] = Field(default_factory=list)
|
|
66
|
+
limitations: list[str] = Field(min_length=1)
|
|
67
|
+
kind: Literal["event_study", "theoretical_scenario", "general"] = "general"
|
|
68
|
+
|
|
69
|
+
@model_validator(mode="after")
|
|
70
|
+
def check_samples(self) -> "ResearchEvidence":
|
|
71
|
+
if self.sample_start > self.sample_end:
|
|
72
|
+
raise ValueError("研究样本起点不得晚于终点")
|
|
73
|
+
if self.candidate_count != len(self.observations) + len(self.exclusions):
|
|
74
|
+
raise ValueError("候选数必须等于纳入数加排除数")
|
|
75
|
+
keys = [item.key for item in self.metrics]
|
|
76
|
+
if len(keys) != len(set(keys)):
|
|
77
|
+
raise ValueError("研究指标 key 不得重复")
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ResearchRunRequest(BaseModel):
|
|
82
|
+
research_id: str | None = None
|
|
83
|
+
python_source: str | None = None
|
|
84
|
+
start: date
|
|
85
|
+
end: date
|
|
86
|
+
parameters: dict[str, Any] = Field(default_factory=dict)
|
|
87
|
+
|
|
88
|
+
@model_validator(mode="after")
|
|
89
|
+
def check_source(self) -> "ResearchRunRequest":
|
|
90
|
+
if (self.research_id is None) == (self.python_source is None):
|
|
91
|
+
raise ValueError("research_id 与临时 python_source 必须且只能提供一个")
|
|
92
|
+
if self.start >= self.end:
|
|
93
|
+
raise ValueError("研究区间至少跨两个日期")
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ResearchRunRecord(BaseModel):
|
|
98
|
+
model_config = ConfigDict(from_attributes=True)
|
|
99
|
+
|
|
100
|
+
uuid: str
|
|
101
|
+
status: Literal["completed", "failed"]
|
|
102
|
+
research_id: str | None
|
|
103
|
+
source_kind: Literal["workspace", "temporary"]
|
|
104
|
+
source_snapshot: str
|
|
105
|
+
source_digest: str
|
|
106
|
+
config_snapshot: dict[str, Any]
|
|
107
|
+
data_revisions: dict[str, str]
|
|
108
|
+
result: ResearchEvidence | None
|
|
109
|
+
error: str | None
|
|
110
|
+
|
|
111
|
+
@model_validator(mode="after")
|
|
112
|
+
def check_snapshot(self) -> "ResearchRunRecord":
|
|
113
|
+
digest = hashlib.sha256(self.source_snapshot.encode("utf-8")).hexdigest()
|
|
114
|
+
if digest != self.source_digest:
|
|
115
|
+
raise ValueError("研究运行源码摘要不匹配")
|
|
116
|
+
if (self.result is None) == (self.error is None):
|
|
117
|
+
raise ValueError("研究运行必须恰有结果或错误")
|
|
118
|
+
if (self.status == "completed") != (self.result is not None):
|
|
119
|
+
raise ValueError("研究运行状态与结果不一致")
|
|
120
|
+
return self
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""逐样本 PIT 特征装配;标签在独立步骤读取,不参与特征计算。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
|
|
5
|
+
from quant_core.app.factor.interface.public.factor_query import IFactorQuery
|
|
6
|
+
from quant_core.app.factor.schema.factor_contract import FactorIdentity
|
|
7
|
+
from quant_core.app.research.interface.research_dataset import IResearchLabelQuery
|
|
8
|
+
from quant_core.app.research.schema.dataset_schema import (
|
|
9
|
+
FeatureSlice,
|
|
10
|
+
ResearchDataset,
|
|
11
|
+
ResearchSampleRequest,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResearchDatasetService:
|
|
16
|
+
"""固定因子修订与完整身份,每个决策时间只查询一次完整截面。"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, factors: IFactorQuery, labels: IResearchLabelQuery) -> None:
|
|
19
|
+
self._factors = factors
|
|
20
|
+
self._labels = labels
|
|
21
|
+
self.revisions: dict[str, str] = {}
|
|
22
|
+
|
|
23
|
+
async def build(
|
|
24
|
+
self,
|
|
25
|
+
identities: tuple[FactorIdentity, ...],
|
|
26
|
+
samples: tuple[ResearchSampleRequest, ...],
|
|
27
|
+
*,
|
|
28
|
+
factor_revision: str,
|
|
29
|
+
label_horizon: int | None = None,
|
|
30
|
+
) -> ResearchDataset:
|
|
31
|
+
if not identities or len(set(identities)) != len(identities) or not factor_revision:
|
|
32
|
+
raise ValueError("数据集必须指定唯一完整因子身份和固定修订")
|
|
33
|
+
if label_horizon is not None and label_horizon < 1:
|
|
34
|
+
raise ValueError("标签窗口必须为正整数")
|
|
35
|
+
self.revisions["dataset_factor"] = factor_revision
|
|
36
|
+
features = []
|
|
37
|
+
for sample in sorted(samples, key=lambda item: item.decision_time):
|
|
38
|
+
snapshot = await self._factors.get_snapshot(
|
|
39
|
+
list(identities),
|
|
40
|
+
list(sample.universe),
|
|
41
|
+
sample.decision_time,
|
|
42
|
+
target_as_of=sample.as_of.date(),
|
|
43
|
+
dataset_revision=factor_revision,
|
|
44
|
+
)
|
|
45
|
+
if any(row.identity not in identities for row in snapshot.values):
|
|
46
|
+
raise ValueError("查询返回非请求身份")
|
|
47
|
+
features.append(FeatureSlice(sample, snapshot))
|
|
48
|
+
# 先验证特征时间键,避免无效样本触发昂贵的前瞻读取。
|
|
49
|
+
dataset = ResearchDataset(tuple(features))
|
|
50
|
+
if label_horizon is None:
|
|
51
|
+
return dataset
|
|
52
|
+
labels = await self._labels.forward_returns(samples, label_horizon)
|
|
53
|
+
for label in labels:
|
|
54
|
+
self.revisions[f"label:{label.instrument_id}:{label.end_time.isoformat()}"] = (
|
|
55
|
+
label.data_revision
|
|
56
|
+
)
|
|
57
|
+
return ResearchDataset(dataset.features, labels)
|
|
58
|
+
|
|
59
|
+
async def iter_batches(
|
|
60
|
+
self,
|
|
61
|
+
identities: tuple[FactorIdentity, ...],
|
|
62
|
+
samples: tuple[ResearchSampleRequest, ...],
|
|
63
|
+
*,
|
|
64
|
+
factor_revision: str,
|
|
65
|
+
batch_size: int = 32,
|
|
66
|
+
label_horizon: int | None = None,
|
|
67
|
+
) -> AsyncIterator[ResearchDataset]:
|
|
68
|
+
"""复用统一构建路径;跨分块标签按真实远期窗口读取,不在分块边缘截断。"""
|
|
69
|
+
if batch_size < 1:
|
|
70
|
+
raise ValueError("研究数据分块大小必须为正整数")
|
|
71
|
+
ordered = tuple(sorted(samples, key=lambda sample: sample.decision_time))
|
|
72
|
+
if len({sample.decision_time for sample in ordered}) != len(ordered):
|
|
73
|
+
raise ValueError("研究特征必须按唯一决策时间排序")
|
|
74
|
+
for start in range(0, len(ordered), batch_size):
|
|
75
|
+
yield await self.build(
|
|
76
|
+
identities,
|
|
77
|
+
ordered[start : start + batch_size],
|
|
78
|
+
factor_revision=factor_revision,
|
|
79
|
+
label_horizon=label_horizon,
|
|
80
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""共享因子评分后的 IC、分层与换手评价,不另写一套评分。"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from quant_core.app.factor.interface.public.factor_diagnostics import IFactorDiagnostics
|
|
6
|
+
from quant_core.app.factor.interface.public.factor_selection import IFactorSelection
|
|
7
|
+
from quant_core.app.factor.schema.diagnostic_types import CrossSection, FactorDiagnostics
|
|
8
|
+
from quant_core.app.factor.schema.selection_types import SelectionDefinition, SelectionExposure
|
|
9
|
+
from quant_core.app.research.schema.dataset_schema import ResearchDataset
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResearchEvaluationService:
|
|
13
|
+
def __init__(self, selection: IFactorSelection, diagnostics: IFactorDiagnostics) -> None:
|
|
14
|
+
self._selection = selection
|
|
15
|
+
self._diagnostics = diagnostics
|
|
16
|
+
|
|
17
|
+
def evaluate(
|
|
18
|
+
self,
|
|
19
|
+
dataset: ResearchDataset,
|
|
20
|
+
definition: SelectionDefinition,
|
|
21
|
+
*,
|
|
22
|
+
exposures: dict[datetime, dict[str, SelectionExposure]] | None = None,
|
|
23
|
+
quantiles: int = 5,
|
|
24
|
+
) -> FactorDiagnostics:
|
|
25
|
+
"""全截面评分进入诊断,TopN 仅用于选股换手,不截断 IC 样本。"""
|
|
26
|
+
sections = []
|
|
27
|
+
selections = []
|
|
28
|
+
labels = {
|
|
29
|
+
(label.decision_time, label.instrument_id): label.value for label in dataset.labels
|
|
30
|
+
}
|
|
31
|
+
for feature in dataset.features:
|
|
32
|
+
timestamp = feature.request.decision_time
|
|
33
|
+
result = self._selection.select(
|
|
34
|
+
definition,
|
|
35
|
+
feature.request.universe,
|
|
36
|
+
feature.snapshot,
|
|
37
|
+
(exposures or {}).get(timestamp),
|
|
38
|
+
)
|
|
39
|
+
scores = {item.instrument_id: item.score for item in result.selected}
|
|
40
|
+
scores.update(
|
|
41
|
+
{
|
|
42
|
+
item.instrument_id: item.score
|
|
43
|
+
for item in result.excluded
|
|
44
|
+
if item.reason == "top_n" and item.score is not None
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
returns = {
|
|
48
|
+
instrument: value
|
|
49
|
+
for instrument in feature.request.universe
|
|
50
|
+
if (value := labels.get((timestamp, instrument))) is not None
|
|
51
|
+
}
|
|
52
|
+
sections.append(CrossSection(timestamp.date(), scores, returns))
|
|
53
|
+
selections.append(tuple(item.instrument_id for item in result.selected))
|
|
54
|
+
return self._diagnostics.analyze(
|
|
55
|
+
tuple(sections), quantiles=quantiles, selections=tuple(selections)
|
|
56
|
+
)
|