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,102 @@
|
|
|
1
|
+
"""数据库保存的不可变 Python 因子计算规格。"""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
from quant_core.app.factor.constants import (
|
|
11
|
+
FactorAuthoringSource,
|
|
12
|
+
FactorDataModality,
|
|
13
|
+
FactorDefinitionStatus,
|
|
14
|
+
FactorFrequency,
|
|
15
|
+
FactorSourceType,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FactorComputationSpec(BaseModel):
|
|
20
|
+
"""窗口或面板使用对应 Python 函数;仅运行可信本地代码。"""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
23
|
+
|
|
24
|
+
name: str = Field(..., pattern=r"^[a-z][a-z0-9_]*$", description="因子名(唯一键)")
|
|
25
|
+
version: str = Field(
|
|
26
|
+
default="1",
|
|
27
|
+
min_length=1,
|
|
28
|
+
max_length=96,
|
|
29
|
+
description="因子版本(= Canonical definition_version)",
|
|
30
|
+
)
|
|
31
|
+
frequency: FactorFrequency = Field(default=FactorFrequency.DAILY, description="结果频率")
|
|
32
|
+
description: str = Field(default="", description="因子说明")
|
|
33
|
+
source_type: FactorSourceType = Field(default=FactorSourceType.MARKET, description="信息来源")
|
|
34
|
+
data_modality: FactorDataModality = Field(
|
|
35
|
+
default=FactorDataModality.STRUCTURED, description="输入数据模态"
|
|
36
|
+
)
|
|
37
|
+
inputs: list[str] = Field(default_factory=list, description="输入数据集,如 bar/1d")
|
|
38
|
+
parameters: dict[str, Any] = Field(default_factory=dict, description="计算参数(如 lookback)")
|
|
39
|
+
input_kind: Literal["window", "panel", "expression"] = "window"
|
|
40
|
+
python_source: str = Field(..., min_length=1, max_length=65536)
|
|
41
|
+
output_type: str = Field(default="float", description="输出类型(float/score/ratio)")
|
|
42
|
+
unit: str | None = Field(default=None, description="输出单位(ratio/percent/score)")
|
|
43
|
+
lookback: int = Field(default=1, ge=1, description="最少历史观测期数")
|
|
44
|
+
status: FactorDefinitionStatus = Field(
|
|
45
|
+
default=FactorDefinitionStatus.DRAFT, description="Artifact 生命周期"
|
|
46
|
+
)
|
|
47
|
+
authoring_source: FactorAuthoringSource = Field(
|
|
48
|
+
default=FactorAuthoringSource.HUMAN, description="生产入口"
|
|
49
|
+
)
|
|
50
|
+
created_by: str | None = Field(default=None, description="生产者标识(Agent/Human 可追溯)")
|
|
51
|
+
metadata: dict[str, Any] = Field(default_factory=dict, description="扩展元数据")
|
|
52
|
+
|
|
53
|
+
def validate_definition(self) -> list[str]:
|
|
54
|
+
"""只做结构与语法校验;不把 AST 校验误称为安全沙箱。"""
|
|
55
|
+
problems: list[str] = []
|
|
56
|
+
try:
|
|
57
|
+
module = ast.parse(self.python_source)
|
|
58
|
+
if self.input_kind == "expression":
|
|
59
|
+
if not any(
|
|
60
|
+
isinstance(node, ast.Assign)
|
|
61
|
+
and any(
|
|
62
|
+
isinstance(target, ast.Name) and target.id == "factors"
|
|
63
|
+
for target in node.targets
|
|
64
|
+
)
|
|
65
|
+
for node in module.body
|
|
66
|
+
):
|
|
67
|
+
problems.append("表达式源码必须声明顶层 factors 字典")
|
|
68
|
+
if not self.inputs:
|
|
69
|
+
problems.append("表达式 inputs 必须声明输入数据需求")
|
|
70
|
+
if len(self.python_source.encode("utf-8")) > 65536:
|
|
71
|
+
problems.append("源码超过 64 KiB")
|
|
72
|
+
return problems
|
|
73
|
+
functions = [
|
|
74
|
+
node
|
|
75
|
+
for node in module.body
|
|
76
|
+
if isinstance(node, ast.FunctionDef)
|
|
77
|
+
and node.name == ("compute_panel" if self.input_kind == "panel" else "compute")
|
|
78
|
+
]
|
|
79
|
+
if len(functions) != 1 or [arg.arg for arg in functions[0].args.args] != [
|
|
80
|
+
"panel" if self.input_kind == "panel" else "bars",
|
|
81
|
+
"parameters",
|
|
82
|
+
]:
|
|
83
|
+
problems.append(
|
|
84
|
+
"源码必须定义与 input_kind 对应的同步顶层 compute/compute_panel 函数"
|
|
85
|
+
)
|
|
86
|
+
except SyntaxError as exc:
|
|
87
|
+
problems.append(f"Python 语法错误: {exc.msg}")
|
|
88
|
+
if len(self.python_source.encode("utf-8")) > 65536:
|
|
89
|
+
problems.append("源码超过 64 KiB")
|
|
90
|
+
if self.input_kind == "panel" and not self.inputs:
|
|
91
|
+
problems.append("面板 inputs 必须声明输入数据需求")
|
|
92
|
+
if self.input_kind == "window" and f"bar/{self.frequency.value}" not in self.inputs:
|
|
93
|
+
problems.append("inputs 必须声明与结果频率对应的 Bar 输入")
|
|
94
|
+
return problems
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def content_digest(self) -> str:
|
|
98
|
+
data = self.model_dump(mode="json")
|
|
99
|
+
if self.input_kind == "window":
|
|
100
|
+
data.pop("input_kind")
|
|
101
|
+
payload = json.dumps(data, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
102
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""单次因子计算的输入契约。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from quant_core.app.data.schema.market_schema import BarResponse
|
|
8
|
+
from quant_core.app.factor.schema.factor_contract import FactorIdentity
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class FactorComputeRequest:
|
|
13
|
+
"""完整身份优先;名称仅供无歧义的旧调用使用。"""
|
|
14
|
+
|
|
15
|
+
factor_name: str
|
|
16
|
+
instrument_id: str
|
|
17
|
+
event_time: datetime
|
|
18
|
+
bars: Sequence[BarResponse]
|
|
19
|
+
input_digest: str | None = None
|
|
20
|
+
identity: FactorIdentity | None = None
|
|
21
|
+
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
if self.identity is not None and self.identity.name != self.factor_name:
|
|
24
|
+
raise ValueError("因子名称与完整身份不一致")
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Canonical Factor Contract - FactorDefinition / FactorValue / FactorSnapshot(T1)
|
|
2
|
+
|
|
3
|
+
SSOT:T1-TASK-20260907-2308-factor-contract、Group 入口确认决策(Q1=A / Q2=A / Q3=A)。
|
|
4
|
+
|
|
5
|
+
概念分层(Definition / Value / Snapshot 分离,与 ARCHITECTURE 全局不变量 8 一致):
|
|
6
|
+
- FactorDefinition:因子是什么、什么口径(Artifact,含 identity 与 provenance 元描述)。
|
|
7
|
+
- FactorValue:长表一行(Q3=A)——一个 factor/source/version/instrument/as_of 的值,
|
|
8
|
+
携带 as_of(归属业务时间)与 available_at(策略最早可见时间)分离语义。
|
|
9
|
+
- FactorSnapshot:某 decision_time 的可读横截面;point-in-time 门禁在构造边界
|
|
10
|
+
自维护(available_at > decision_time 的值结构化拒绝,AC-2)。
|
|
11
|
+
|
|
12
|
+
核心规则:
|
|
13
|
+
- 不同 source / definition_version 是不同因子身份(FactorIdentity.logical_key),
|
|
14
|
+
禁止静默替换、隐式 fallback 或合并(Group 决策 4)。
|
|
15
|
+
- MISSING / UNAVAILABLE / NOT_YET_PUBLISHED 用 status 表达,禁止 0 填充。
|
|
16
|
+
- 本模块不引用任何 Provider SDK、Repository 或数据库——Provider 专有字段
|
|
17
|
+
(如 Tushare ts_code/ann_date)不得进入契约(全局不变量 2)。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from datetime import date, datetime
|
|
22
|
+
from decimal import Decimal
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
25
|
+
|
|
26
|
+
from quant_core.app.factor.constants import (
|
|
27
|
+
FactorFrequency,
|
|
28
|
+
FactorFreshness,
|
|
29
|
+
FactorSourceKind,
|
|
30
|
+
FactorStatus,
|
|
31
|
+
)
|
|
32
|
+
from quant_core.app.factor.schema.factor_computation_schema import FactorComputationSpec
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _require_tzaware(field: str, v: datetime) -> datetime:
|
|
36
|
+
if v.tzinfo is None or v.tzinfo.utcoffset(v) is None:
|
|
37
|
+
raise ValueError(f"{field} 必须是 tz-aware datetime: {v}")
|
|
38
|
+
return v
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FactorIdentity(BaseModel, frozen=True):
|
|
42
|
+
"""因子逻辑身份:不同 (name, source, definition_version, frequency) 不互认
|
|
43
|
+
|
|
44
|
+
Group 决策 4:相同因子名但 source / definition version / 计算口径不同,
|
|
45
|
+
禁止静默替换、隐式 fallback、合并成同一因子身份。
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
model_config = ConfigDict(frozen=True)
|
|
49
|
+
|
|
50
|
+
name: str = Field(..., pattern=r"^[a-z][a-z0-9_]*$", description="因子名")
|
|
51
|
+
source: str = Field(..., min_length=1, description="来源标识(如 tushare / local-python)")
|
|
52
|
+
definition_version: str = Field(..., min_length=1, max_length=96, description="口径版本")
|
|
53
|
+
frequency: FactorFrequency = Field(default=FactorFrequency.DAILY, description="因子频率")
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def logical_key(self) -> tuple[str, str, str, FactorFrequency]:
|
|
57
|
+
"""业务唯一键的身份部分(T2 长表唯一键 = logical_key + instrument_id + as_of)"""
|
|
58
|
+
return (self.name, self.source, self.definition_version, self.frequency)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FactorProvenance(BaseModel):
|
|
62
|
+
"""因子值血缘(描述性、来源无关;不含 Provider 专有 schema 字段)
|
|
63
|
+
|
|
64
|
+
版本锁定语义(R2-2):
|
|
65
|
+
- dataset_revision:数据集修订批次(quant_core.app.factor.revision 约定,可排序)。
|
|
66
|
+
同一官方修订产生的旧行在新 revision 下保留——旧回测 pin 旧 revision
|
|
67
|
+
即可复现原数据集;None = 未声明(Store 入库归一 0-legacy)。
|
|
68
|
+
- provider_revision:Provider 侧修订号;Provider 不提供时保持 None
|
|
69
|
+
(unknown 是合法状态,禁止用 mapping version 臆造)。
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
model_config = ConfigDict(frozen=True)
|
|
73
|
+
|
|
74
|
+
source_kind: FactorSourceKind = Field(..., description="生产来源类别(AC-1 三类)")
|
|
75
|
+
provider_id: str = Field(
|
|
76
|
+
..., min_length=1, description="生产者标识(provider/calculator/model)"
|
|
77
|
+
)
|
|
78
|
+
retrieval_ref: str | None = Field(
|
|
79
|
+
default=None, description="取数/计算引用(如接口名或 dotted path)"
|
|
80
|
+
)
|
|
81
|
+
content_digest: str | None = Field(
|
|
82
|
+
default=None, pattern=r"^[0-9a-f]{64}$", description="内容级 SHA-256(可选,T3 真实验证用)"
|
|
83
|
+
)
|
|
84
|
+
dataset_revision: str | None = Field(
|
|
85
|
+
default=None, description="数据集修订批次(可排序;见类注释)"
|
|
86
|
+
)
|
|
87
|
+
provider_revision: str | None = Field(
|
|
88
|
+
default=None, description="Provider 侧修订号(未知保持 None,不臆造)"
|
|
89
|
+
)
|
|
90
|
+
note: str | None = Field(default=None, description="补充说明")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class FactorDefinition(BaseModel):
|
|
94
|
+
"""因子定义(Artifact)——全系统唯一因子口径 SSOT(R1-1)
|
|
95
|
+
|
|
96
|
+
身份、口径、计算规格、值与 PIT 截面的唯一 Owner 是 app/factor。
|
|
97
|
+
本地定义附带 FactorComputationSpec;外部导入定义不携带本地公式。
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
101
|
+
|
|
102
|
+
identity: FactorIdentity = Field(..., description="因子逻辑身份(唯一键)")
|
|
103
|
+
description: str = Field(default="", description="因子说明")
|
|
104
|
+
lookback: int = Field(default=1, ge=1, description="最少历史窗口(业务期数)")
|
|
105
|
+
provenance_template: FactorProvenance | None = Field(
|
|
106
|
+
default=None, description="该定义下典型生产的 provenance 元描述"
|
|
107
|
+
)
|
|
108
|
+
computation_spec: FactorComputationSpec | None = Field(
|
|
109
|
+
default=None, description="本地计算规格;外部导入定义不携带本地公式"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@model_validator(mode="after")
|
|
113
|
+
def _matching_computation_identity(self) -> "FactorDefinition":
|
|
114
|
+
spec = self.computation_spec
|
|
115
|
+
if spec is not None and (
|
|
116
|
+
spec.name != self.identity.name
|
|
117
|
+
or spec.version != self.identity.definition_version
|
|
118
|
+
or spec.frequency != self.identity.frequency
|
|
119
|
+
):
|
|
120
|
+
raise ValueError("计算规格的名称、版本和频率必须与因子身份一致")
|
|
121
|
+
if (
|
|
122
|
+
spec is not None
|
|
123
|
+
and self.provenance_template is not None
|
|
124
|
+
and self.provenance_template.content_digest != spec.content_digest
|
|
125
|
+
):
|
|
126
|
+
raise ValueError("因子定义摘要与计算规格不一致")
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class FactorValue(BaseModel):
|
|
131
|
+
"""长表一行(Q3=A):一个 factor/instrument/as_of/version 的值
|
|
132
|
+
|
|
133
|
+
- as_of:因子值归属的业务时间(日频因子 = 交易日收盘口径时点);
|
|
134
|
+
- available_at:策略最早可见时间(Q2=A:Tushare 日频因子缺权威发布时间时,
|
|
135
|
+
从下一交易日开盘起可读——由 Adapter 在入库前计算,本模型只强制
|
|
136
|
+
available_at >= as_of);
|
|
137
|
+
- status 表达缺失/不可用/未发布;value 为 None,禁止 0 填充;
|
|
138
|
+
- 整行 frozen(R1-2):FactorSnapshot 的 PIT 门禁在构造边界校验后,
|
|
139
|
+
行内字段(如 available_at)不得被事后修改——否则嵌套可变性会绕过
|
|
140
|
+
快照级校验造成未来值泄漏;修正必须以 model_copy 产生新行并重新过门。
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
model_config = ConfigDict(frozen=True)
|
|
144
|
+
|
|
145
|
+
identity: FactorIdentity = Field(..., description="因子逻辑身份")
|
|
146
|
+
instrument_id: str = Field(..., min_length=1, description="Canonical 标的 ID")
|
|
147
|
+
as_of: datetime = Field(..., description="归属业务时间")
|
|
148
|
+
available_at: datetime = Field(..., description="策略最早可见时间")
|
|
149
|
+
value: Decimal | None = Field(default=None, description="因子值;非 AVAILABLE 状态必须为 None")
|
|
150
|
+
status: FactorStatus = Field(..., description="值状态(缺失不用 0 表达)")
|
|
151
|
+
provenance: FactorProvenance = Field(..., description="血缘")
|
|
152
|
+
|
|
153
|
+
@field_validator("as_of", "available_at")
|
|
154
|
+
@classmethod
|
|
155
|
+
def _tzaware(cls, v: datetime) -> datetime:
|
|
156
|
+
return _require_tzaware("datetime", v)
|
|
157
|
+
|
|
158
|
+
@model_validator(mode="after")
|
|
159
|
+
def _value_status_consistency(self) -> "FactorValue":
|
|
160
|
+
if self.available_at < self.as_of:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"available_at {self.available_at} 不得早于 as_of {self.as_of}"
|
|
163
|
+
"(可见时间不得早于归属业务时间)"
|
|
164
|
+
)
|
|
165
|
+
if self.status is FactorStatus.AVAILABLE and self.value is None:
|
|
166
|
+
raise ValueError("AVAILABLE 状态必须携带非 None value")
|
|
167
|
+
if self.status is not FactorStatus.AVAILABLE and self.value is not None:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"status={self.status.value} 禁止携带 value {self.value}"
|
|
170
|
+
"(缺失/不可用/未发布不用数值伪装)"
|
|
171
|
+
)
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def logical_key(self) -> tuple[str, str, str, FactorFrequency, str, datetime]:
|
|
176
|
+
"""长表业务唯一键:identity + instrument_id + as_of(T2 唯一约束依据)"""
|
|
177
|
+
return (*self.identity.logical_key, self.instrument_id, self.as_of)
|
|
178
|
+
|
|
179
|
+
def _conflict_payload(self) -> tuple:
|
|
180
|
+
"""判定两行是否同一事实的载荷(任一不同即冲突,禁止静默取舍)"""
|
|
181
|
+
return (self.value, self.status, self.available_at, self.provenance.model_dump_json())
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class FactorSnapshotSelection(BaseModel):
|
|
185
|
+
"""创建截面时的不可变选择条件;覆盖报告只能解释同一组查询。"""
|
|
186
|
+
|
|
187
|
+
model_config = ConfigDict(frozen=True)
|
|
188
|
+
|
|
189
|
+
identities: tuple[FactorIdentity, ...]
|
|
190
|
+
instrument_ids: tuple[str, ...]
|
|
191
|
+
target_as_of: date
|
|
192
|
+
freshness: FactorFreshness
|
|
193
|
+
dataset_revision: str | None = None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class FactorSnapshot(BaseModel):
|
|
197
|
+
"""某 decision_time 的因子横截面(Strategy/Selection 的只读消费形态)
|
|
198
|
+
|
|
199
|
+
point-in-time 门禁(模型边界自维护,AC-2 / Group 决策 6):
|
|
200
|
+
- 任何 available_at > decision_time 的值在构造(或事后修改)时结构化拒绝——
|
|
201
|
+
查询侧不存在「事后泄漏」路径;
|
|
202
|
+
- 同一逻辑键重复:值完全一致 → 幂等去重;存在差异 → ValueError
|
|
203
|
+
(禁止静默 first/last wins,AC-3);
|
|
204
|
+
- 行与身份均为 frozen(R1-2):构造后不存在任何修改行内
|
|
205
|
+
available_at/status/value 的合法路径,PIT 门禁不可被嵌套可变性绕过;
|
|
206
|
+
- 行序确定性:按 (identity.logical_key, instrument_id, as_of) 稳定排序;
|
|
207
|
+
- 序列化确定性:to_canonical_json()(sorted keys、ISO 时间、Decimal 字符串)。
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
model_config = ConfigDict(frozen=True)
|
|
211
|
+
|
|
212
|
+
decision_time: datetime = Field(..., description="决策时点(point-in-time 门禁基准)")
|
|
213
|
+
values: tuple[FactorValue, ...] = Field(
|
|
214
|
+
default_factory=tuple, description="可读因子值(已排序去重)"
|
|
215
|
+
)
|
|
216
|
+
selection: FactorSnapshotSelection | None = Field(
|
|
217
|
+
default=None, description="查询选择上下文;手工构造截面不携带可验证覆盖范围"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@field_validator("decision_time")
|
|
221
|
+
@classmethod
|
|
222
|
+
def _tzaware_decision(cls, v: datetime) -> datetime:
|
|
223
|
+
return _require_tzaware("decision_time", v)
|
|
224
|
+
|
|
225
|
+
@model_validator(mode="after")
|
|
226
|
+
def _enforce_pit_and_uniqueness(self) -> "FactorSnapshot":
|
|
227
|
+
seen: dict[tuple, FactorValue] = {}
|
|
228
|
+
for v in self.values:
|
|
229
|
+
if v.available_at > self.decision_time:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"available_at {v.available_at} 晚于 decision_time "
|
|
232
|
+
f"{self.decision_time}:未来可见的值不得进入 FactorSnapshot"
|
|
233
|
+
)
|
|
234
|
+
prev = seen.get(v.logical_key)
|
|
235
|
+
if prev is None:
|
|
236
|
+
seen[v.logical_key] = v
|
|
237
|
+
elif prev._conflict_payload() != v._conflict_payload():
|
|
238
|
+
raise ValueError(f"同一逻辑键 {v.logical_key} 出现冲突值,拒绝歧义快照")
|
|
239
|
+
# 绕过 validate_assignment 就地归一(避免校验器递归触发);归一发生在
|
|
240
|
+
# 构造边界,事后只读消费无法再引入未过门的值
|
|
241
|
+
object.__setattr__(
|
|
242
|
+
self,
|
|
243
|
+
"values",
|
|
244
|
+
tuple(
|
|
245
|
+
sorted(
|
|
246
|
+
seen.values(),
|
|
247
|
+
key=lambda x: (x.identity.logical_key, x.instrument_id, x.as_of),
|
|
248
|
+
)
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
def to_canonical_json(self) -> str:
|
|
254
|
+
"""确定性序列化(sorted keys + ISO 时间;Decimal 以字符串表达精度)"""
|
|
255
|
+
return json.dumps(
|
|
256
|
+
self.model_dump(mode="json"), ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
257
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""本地面板输入需求;参与股票池与输出股票集合分别声明。"""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
7
|
+
|
|
8
|
+
from quant_core.app.data.schema.adjustment_schema import BarViewRequest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FactorInputRequest(BaseModel):
|
|
12
|
+
"""lookback 包含当前交易日;价格政策显式传入,不自动填充缺失行情。"""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
15
|
+
instrument_ids: tuple[str, ...] = Field(min_length=1)
|
|
16
|
+
start: date
|
|
17
|
+
end: date
|
|
18
|
+
exclude_st: bool = False
|
|
19
|
+
fields: tuple[str, ...] = Field(min_length=1)
|
|
20
|
+
lookback: int = Field(ge=1)
|
|
21
|
+
view: BarViewRequest
|
|
22
|
+
missing_policy: Literal["null", "error"] = "null"
|
|
23
|
+
|
|
24
|
+
@model_validator(mode="after")
|
|
25
|
+
def validate_request(self) -> "FactorInputRequest":
|
|
26
|
+
if self.start > self.end:
|
|
27
|
+
raise ValueError("输出开始日期晚于结束日期")
|
|
28
|
+
if len(set(self.instrument_ids)) != len(self.instrument_ids):
|
|
29
|
+
raise ValueError("参与股票池不能重复")
|
|
30
|
+
if len(set(self.fields)) != len(self.fields):
|
|
31
|
+
raise ValueError("输入字段不能重复")
|
|
32
|
+
return self
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""面板输入契约:统一字段与口径、逐行可见时间、历史股票池和修订证据。"""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FactorPanelRow(BaseModel):
|
|
12
|
+
"""同一业务时点的一只证券;未入池行保留时序历史,但不参与横截面。"""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(frozen=True)
|
|
15
|
+
instrument_id: str = Field(min_length=1)
|
|
16
|
+
as_of: datetime
|
|
17
|
+
available_at: datetime
|
|
18
|
+
eligible: bool
|
|
19
|
+
fields: dict[str, Decimal | None]
|
|
20
|
+
|
|
21
|
+
@model_validator(mode="after")
|
|
22
|
+
def validate_row(self) -> "FactorPanelRow":
|
|
23
|
+
for value in (self.as_of, self.available_at):
|
|
24
|
+
if value.utcoffset() is None:
|
|
25
|
+
raise ValueError("面板时间必须带时区")
|
|
26
|
+
if self.available_at < self.as_of:
|
|
27
|
+
raise ValueError("输入可见时间不得早于业务时间")
|
|
28
|
+
if any(value is not None and not value.is_finite() for value in self.fields.values()):
|
|
29
|
+
raise ValueError("面板字段必须为有限数值或缺值")
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FactorPanel(BaseModel):
|
|
34
|
+
"""一次计算的不可变输入快照;股票池及价格口径参与身份绑定和摘要。"""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(frozen=True)
|
|
37
|
+
decision_time: datetime
|
|
38
|
+
frequency: str = "1d"
|
|
39
|
+
price_basis: str = Field(min_length=1)
|
|
40
|
+
universe_ref: str = Field(min_length=1)
|
|
41
|
+
universe_revision: str = Field(min_length=1)
|
|
42
|
+
market_revision: str = Field(min_length=1)
|
|
43
|
+
units: dict[str, str]
|
|
44
|
+
rows: tuple[FactorPanelRow, ...]
|
|
45
|
+
|
|
46
|
+
@model_validator(mode="after")
|
|
47
|
+
def validate_panel(self) -> "FactorPanel":
|
|
48
|
+
if self.decision_time.utcoffset() is None:
|
|
49
|
+
raise ValueError("面板决策时间必须带时区")
|
|
50
|
+
keys = [(row.as_of, row.instrument_id) for row in self.rows]
|
|
51
|
+
if len(keys) != len(set(keys)):
|
|
52
|
+
raise ValueError("面板不能包含重复业务时间与证券")
|
|
53
|
+
if any(row.available_at > self.decision_time for row in self.rows):
|
|
54
|
+
raise ValueError("面板包含决策时点尚不可见的输入")
|
|
55
|
+
if any(set(row.fields) - self.units.keys() for row in self.rows):
|
|
56
|
+
raise ValueError("每个面板输入字段都必须声明单位")
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def input_digest(self) -> str:
|
|
61
|
+
payload = self.model_dump(mode="json")
|
|
62
|
+
payload["rows"] = sorted(
|
|
63
|
+
payload["rows"], key=lambda row: (row["as_of"], row["instrument_id"])
|
|
64
|
+
)
|
|
65
|
+
return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""可离线执行的已发布因子定义文件,摘要需在读取时校验。"""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
6
|
+
|
|
7
|
+
from quant_core.app.factor.constants import LOCAL_FACTOR_SOURCE, FactorDefinitionStatus
|
|
8
|
+
from quant_core.app.factor.schema.factor_contract import FactorDefinition
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PublishedFactor(BaseModel):
|
|
12
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
13
|
+
format_version: Literal[1] = 1
|
|
14
|
+
definition: FactorDefinition
|
|
15
|
+
content_digest: str
|
|
16
|
+
|
|
17
|
+
@model_validator(mode="after")
|
|
18
|
+
def verify(self) -> "PublishedFactor":
|
|
19
|
+
spec = self.definition.computation_spec
|
|
20
|
+
if spec is None or spec.status != FactorDefinitionStatus.APPROVED:
|
|
21
|
+
raise ValueError("需要已发布且可执行的因子定义")
|
|
22
|
+
if spec.version == "latest" or spec.content_digest != self.content_digest:
|
|
23
|
+
raise ValueError("因子版本未固定或文件摘要不匹配")
|
|
24
|
+
identity = self.definition.identity
|
|
25
|
+
provenance = self.definition.provenance_template
|
|
26
|
+
if (
|
|
27
|
+
identity.source != LOCAL_FACTOR_SOURCE
|
|
28
|
+
or self.definition.lookback != spec.lookback
|
|
29
|
+
or provenance is None
|
|
30
|
+
or provenance.content_digest != self.content_digest
|
|
31
|
+
):
|
|
32
|
+
raise ValueError("因子来源、窗口或血缘摘要不匹配")
|
|
33
|
+
if (identity.name, identity.definition_version, identity.frequency) != (
|
|
34
|
+
spec.name,
|
|
35
|
+
spec.version,
|
|
36
|
+
spec.frequency,
|
|
37
|
+
):
|
|
38
|
+
raise ValueError("因子身份与计算定义不匹配")
|
|
39
|
+
if spec.validate_definition():
|
|
40
|
+
raise ValueError("因子定义结构非法")
|
|
41
|
+
return self
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""因子时点查询状态探查的结果值。"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from quant_core.app.factor.constants import FactorStatus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ProbeCellStatus:
|
|
11
|
+
statuses: frozenset[FactorStatus]
|
|
12
|
+
latest_as_of: datetime | None = None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""本地因子运行请求:所有输入显式指定,不依赖管理库。"""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
7
|
+
|
|
8
|
+
from quant_core.app.data.schema.adjustment_schema import BarViewRequest
|
|
9
|
+
from quant_core.app.factor.schema.factor_publication_schema import PublishedFactor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LocalFactorRequest(BaseModel):
|
|
13
|
+
model_config = ConfigDict(extra="forbid")
|
|
14
|
+
source: str = ""
|
|
15
|
+
definitions: tuple[PublishedFactor, ...] = ()
|
|
16
|
+
manifest_path: Path
|
|
17
|
+
instrument_ids: tuple[str, ...]
|
|
18
|
+
start: date
|
|
19
|
+
end: date
|
|
20
|
+
exclude_st: bool = False
|
|
21
|
+
view: BarViewRequest = Field(default_factory=BarViewRequest)
|
|
22
|
+
max_rows: int = Field(default=1000000, ge=1)
|
|
23
|
+
|
|
24
|
+
@model_validator(mode="after")
|
|
25
|
+
def validate_range(self) -> "LocalFactorRequest":
|
|
26
|
+
if bool(self.source) == bool(self.definitions):
|
|
27
|
+
raise ValueError("必须且只能提供本地源码或已发布定义")
|
|
28
|
+
if len({item.definition.identity.name for item in self.definitions}) != len(
|
|
29
|
+
self.definitions
|
|
30
|
+
):
|
|
31
|
+
raise ValueError("单次计算不能包含同名多版本;分别计算后按策略别名绑定")
|
|
32
|
+
if self.start > self.end or not self.instrument_ids:
|
|
33
|
+
raise ValueError("需指定合法范围和非空股票池")
|
|
34
|
+
if len(set(self.instrument_ids)) != len(self.instrument_ids):
|
|
35
|
+
raise ValueError("股票池不得重复")
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
@field_validator("manifest_path")
|
|
39
|
+
@classmethod
|
|
40
|
+
def absolute_manifest(cls, value: Path) -> Path:
|
|
41
|
+
"""归档固定路径,避免重启后工作目录变化影响定位。"""
|
|
42
|
+
return value.expanduser().resolve()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""当前行情面板的公开口径标识,复用 Canonical 股与人民币单位。"""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
PRICE_BASIS = "pre-next-open"
|
|
6
|
+
PANEL_UNITS = {name: "CNY/share" for name in ("open", "high", "low", "close", "vwap")}
|
|
7
|
+
PANEL_UNITS.update({"volume": "share", "amount": "CNY"})
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def listed_roster_ref(instrument_ids: tuple[str, ...]) -> str:
|
|
11
|
+
"""固定候选集合叠加逐日上市/退市门禁;不声称是历史指数成分。"""
|
|
12
|
+
digest = hashlib.sha256("\n".join(sorted(set(instrument_ids))).encode()).hexdigest()
|
|
13
|
+
return f"listed-roster:{digest}"
|