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,189 @@
|
|
|
1
|
+
"""全市场收盘后因子试算;只通过 SDK 读取,不发布因子、不下单。
|
|
2
|
+
|
|
3
|
+
在 apps/quant_cli 运行:python -m examples.full_market_screen --day YYYY-MM-DD
|
|
4
|
+
该示例使用当前修订的数据,不构成历史 PIT 回测或可成交性保证。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import csv
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from datetime import date, datetime, time, timedelta
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from zoneinfo import ZoneInfo
|
|
17
|
+
|
|
18
|
+
from pqdata.schema.bar_schema import BarQuery
|
|
19
|
+
from quant_core.app.factor.computation.python_factor_executor import execute_python_panel
|
|
20
|
+
from quant_core.app.factor.schema.factor_computation_schema import FactorComputationSpec
|
|
21
|
+
from quant_core.app.factor.schema.factor_panel import FactorPanel, FactorPanelRow
|
|
22
|
+
|
|
23
|
+
from quant_cli.app.data.data_dependency import build_market_query
|
|
24
|
+
from quant_cli.common.utils.platform_utils import configure_utf8_output
|
|
25
|
+
|
|
26
|
+
SOURCE = """def compute_panel(panel, parameters):
|
|
27
|
+
result = {}
|
|
28
|
+
for row in panel.rows:
|
|
29
|
+
f = row.fields
|
|
30
|
+
result[(row.as_of, row.instrument_id)] = (
|
|
31
|
+
f["close"] / f["open"] - 1 if row.eligible else None
|
|
32
|
+
)
|
|
33
|
+
return result
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def run(day: date, output: Path, minimum_amount: Decimal, top: int) -> dict:
|
|
38
|
+
"""固定行情修订批量读取,调用现有面板执行器并保存完整审计结果。"""
|
|
39
|
+
cn = ZoneInfo("Asia/Shanghai")
|
|
40
|
+
start = datetime.combine(day, time.min, cn)
|
|
41
|
+
end = datetime.combine(day, time.max, cn)
|
|
42
|
+
observed_at = datetime.now(cn)
|
|
43
|
+
if observed_at <= end:
|
|
44
|
+
raise ValueError("只支持已结束的历史交易日")
|
|
45
|
+
async with build_market_query() as client:
|
|
46
|
+
revision = (await client.snapshot(day, day))["canonical"]
|
|
47
|
+
catalog = await client.search_instruments(limit=10000)
|
|
48
|
+
if len(catalog) >= 10000:
|
|
49
|
+
raise ValueError("标的目录可能截断,拒绝冒充全市场")
|
|
50
|
+
stocks = [i for i in catalog if i.instrument_type == "stock"]
|
|
51
|
+
unknown_listing = [i.instrument_id for i in stocks if i.listed_date is None]
|
|
52
|
+
universe = {
|
|
53
|
+
i.instrument_id: i
|
|
54
|
+
for i in stocks
|
|
55
|
+
if i.listed_date is not None
|
|
56
|
+
and i.listed_date <= day
|
|
57
|
+
and (i.delisted_date is None or i.delisted_date >= day)
|
|
58
|
+
}
|
|
59
|
+
ids = sorted(universe)
|
|
60
|
+
bars = {}
|
|
61
|
+
for offset in range(0, len(ids), 1000):
|
|
62
|
+
query = BarQuery(
|
|
63
|
+
instrument_ids=ids[offset : offset + 1000],
|
|
64
|
+
frequency="1d",
|
|
65
|
+
start=start,
|
|
66
|
+
end=end,
|
|
67
|
+
revision=revision,
|
|
68
|
+
)
|
|
69
|
+
async for page in client.iter_bar_pages(query):
|
|
70
|
+
for bar in page.items:
|
|
71
|
+
if bar.instrument_id in bars:
|
|
72
|
+
raise ValueError("同一证券出现重复日线")
|
|
73
|
+
bars[bar.instrument_id] = bar
|
|
74
|
+
print(f"已读取 {min(offset + 1000, len(ids))}/{len(ids)} 只候选", flush=True)
|
|
75
|
+
if (await client.snapshot(day, day))["canonical"] != revision:
|
|
76
|
+
raise ValueError("读取期间数据修订变化,请重新运行")
|
|
77
|
+
if not bars:
|
|
78
|
+
raise ValueError("当日无日线数据")
|
|
79
|
+
reasons = dict.fromkeys(unknown_listing, "unknown_listing_date")
|
|
80
|
+
rows = []
|
|
81
|
+
for instrument_id in ids:
|
|
82
|
+
instrument = universe[instrument_id]
|
|
83
|
+
bar = bars.get(instrument_id)
|
|
84
|
+
reason = "eligible"
|
|
85
|
+
if bar is None:
|
|
86
|
+
reason = "missing_bar"
|
|
87
|
+
elif bar.suspended or bar.volume <= 0:
|
|
88
|
+
reason = "suspended_or_no_volume"
|
|
89
|
+
elif (
|
|
90
|
+
bar.open <= 0
|
|
91
|
+
or bar.low <= 0
|
|
92
|
+
or not (bar.low <= min(bar.open, bar.close) <= max(bar.open, bar.close) <= bar.high)
|
|
93
|
+
):
|
|
94
|
+
reason = "invalid_ohlc"
|
|
95
|
+
elif day - instrument.listed_date < timedelta(days=120):
|
|
96
|
+
reason = "listed_less_than_120_calendar_days"
|
|
97
|
+
elif bar.amount is None or bar.amount < minimum_amount:
|
|
98
|
+
reason = "insufficient_amount"
|
|
99
|
+
reasons[instrument_id] = reason
|
|
100
|
+
rows.append(
|
|
101
|
+
FactorPanelRow(
|
|
102
|
+
instrument_id=instrument_id,
|
|
103
|
+
as_of=datetime.combine(day, time(15), cn),
|
|
104
|
+
available_at=observed_at,
|
|
105
|
+
eligible=reason == "eligible",
|
|
106
|
+
fields={"open": bar.open if bar else None, "close": bar.close if bar else None},
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
universe_digest = hashlib.sha256(
|
|
110
|
+
json.dumps(
|
|
111
|
+
[universe[i].model_dump(mode="json") for i in ids],
|
|
112
|
+
sort_keys=True,
|
|
113
|
+
).encode()
|
|
114
|
+
).hexdigest()
|
|
115
|
+
panel = FactorPanel(
|
|
116
|
+
decision_time=observed_at,
|
|
117
|
+
price_basis="raw_same_day_ratio",
|
|
118
|
+
universe_ref="listed_a_share_catalog",
|
|
119
|
+
universe_revision=universe_digest,
|
|
120
|
+
market_revision=revision,
|
|
121
|
+
units={"open": "CNY", "close": "CNY"},
|
|
122
|
+
rows=tuple(rows),
|
|
123
|
+
)
|
|
124
|
+
spec = FactorComputationSpec(
|
|
125
|
+
name="intraday_strength",
|
|
126
|
+
input_kind="panel",
|
|
127
|
+
inputs=["bar/1d"],
|
|
128
|
+
python_source=SOURCE,
|
|
129
|
+
description="收盘价 / 开盘价 - 1;成交额与上市时间过滤",
|
|
130
|
+
)
|
|
131
|
+
values = execute_python_panel(spec, panel)
|
|
132
|
+
ranked = sorted(
|
|
133
|
+
[(i, score) for (_, i), score in values.items() if score is not None],
|
|
134
|
+
key=lambda item: (-item[1], item[0]),
|
|
135
|
+
)
|
|
136
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
with (output / "ranking.csv").open("w", encoding="utf-8-sig", newline="") as stream:
|
|
138
|
+
writer = csv.writer(stream)
|
|
139
|
+
writer.writerow(["rank", "instrument_id", "name", "intraday_return", "amount_cny"])
|
|
140
|
+
for rank, (i, score) in enumerate(ranked, 1):
|
|
141
|
+
writer.writerow([rank, i, universe[i].name, score, bars[i].amount])
|
|
142
|
+
audit = {
|
|
143
|
+
"day": str(day),
|
|
144
|
+
"observed_at": observed_at.isoformat(),
|
|
145
|
+
"revision": revision,
|
|
146
|
+
"input_digest": panel.input_digest,
|
|
147
|
+
"definition_digest": spec.content_digest,
|
|
148
|
+
"catalog_stocks": len(stocks),
|
|
149
|
+
"universe": len(ids),
|
|
150
|
+
"bars": len(bars),
|
|
151
|
+
"filter_counts": dict(Counter(reasons.values())),
|
|
152
|
+
"minimum_amount": str(minimum_amount),
|
|
153
|
+
"top": [{"id": i, "name": universe[i].name, "score": str(s)} for i, s in ranked[:top]],
|
|
154
|
+
"limitations": [
|
|
155
|
+
"当前修订快照,不是历史PIT回测",
|
|
156
|
+
"未过滤历史ST和涨跌停,非可成交名单",
|
|
157
|
+
"仅验证选股链路,未验证收益预测能力",
|
|
158
|
+
],
|
|
159
|
+
}
|
|
160
|
+
for name, content in {
|
|
161
|
+
"summary.json": audit,
|
|
162
|
+
"exclusions.json": {i: r for i, r in reasons.items() if r != "eligible"},
|
|
163
|
+
"bars.json": [bars[i].model_dump(mode="json") for i in sorted(bars)],
|
|
164
|
+
"universe.json": [universe[i].model_dump(mode="json") for i in ids],
|
|
165
|
+
"factor.json": spec.model_dump(mode="json"),
|
|
166
|
+
}.items():
|
|
167
|
+
(output / name).write_text(
|
|
168
|
+
json.dumps(content, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
169
|
+
)
|
|
170
|
+
return audit
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
configure_utf8_output()
|
|
175
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
176
|
+
parser.add_argument("--day", type=date.fromisoformat, required=True)
|
|
177
|
+
parser.add_argument("--output", type=Path, default=Path(".cache/full_market_screen"))
|
|
178
|
+
parser.add_argument("--minimum-amount", type=Decimal, default=Decimal("20000000"))
|
|
179
|
+
parser.add_argument("--top", type=int, default=20)
|
|
180
|
+
args = parser.parse_args()
|
|
181
|
+
if args.minimum_amount < 0 or args.top < 1:
|
|
182
|
+
parser.error("成交额不能为负,top 必须为正数")
|
|
183
|
+
print(
|
|
184
|
+
json.dumps(
|
|
185
|
+
asyncio.run(run(args.day, args.output, args.minimum_amount, args.top)),
|
|
186
|
+
ensure_ascii=False,
|
|
187
|
+
indent=2,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""显式本地快照验证:因子独立复算、研究归档、真实交易回放与重启读取。"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import date, datetime, time
|
|
8
|
+
from decimal import Decimal
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
from zoneinfo import ZoneInfo
|
|
12
|
+
|
|
13
|
+
from quant_core.app.backtest.schema import BacktestConfig, BacktestResult
|
|
14
|
+
from quant_core.app.factor.factor_dependency import build_factor_diagnostics
|
|
15
|
+
from quant_core.app.factor.schema.local_factor_schema import LocalFactorRequest
|
|
16
|
+
from quant_core.app.research.schema.dataset_schema import ResearchLabel
|
|
17
|
+
from quant_core.common.local_run import RunStatus
|
|
18
|
+
|
|
19
|
+
from quant_cli.app.backtest.local_backtest_dependency import build_local_backtest_store
|
|
20
|
+
from quant_cli.app.backtest.local_backtest_schema import LocalBacktestRequest
|
|
21
|
+
from quant_cli.app.backtest.service.local_backtest_service import run_local_backtest
|
|
22
|
+
from quant_cli.app.data.service.dataset_load_service import load_dataset
|
|
23
|
+
from quant_cli.app.factor.local_factor_dependency import (
|
|
24
|
+
build_local_factor_query,
|
|
25
|
+
build_local_factor_store,
|
|
26
|
+
)
|
|
27
|
+
from quant_cli.app.factor.service.local_factor_service import run_local_factor
|
|
28
|
+
from quant_cli.app.research.local_research_dependency import build_local_research_store
|
|
29
|
+
from quant_cli.app.research.schema.local_research_schema import LocalEvaluationRequest
|
|
30
|
+
from quant_cli.app.research.service.local_research_service import (
|
|
31
|
+
compare_local_research,
|
|
32
|
+
evaluate_local_factor,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
CN = ZoneInfo("Asia/Shanghai")
|
|
36
|
+
SOURCE = (
|
|
37
|
+
"from quant_core.app.factor.computation.factor_expression import MA, Factor\n"
|
|
38
|
+
"factors = {'reversal5': -MA(Factor('close') / Factor('open') - 1, 5)}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def validate(manifest: Path, database: Path, backtest_manifest: Path) -> dict:
|
|
43
|
+
assert "quant_backend.common.database.database_session" not in sys.modules
|
|
44
|
+
factor_store = build_local_factor_store(database)
|
|
45
|
+
research_store = build_local_research_store(database)
|
|
46
|
+
with patch("socket.socket", side_effect=AssertionError("离线计算禁止网络")):
|
|
47
|
+
market, _ = load_dataset(manifest, 1000000)
|
|
48
|
+
ids = tuple(sorted(market.facts))
|
|
49
|
+
request = LocalFactorRequest(
|
|
50
|
+
source=SOURCE,
|
|
51
|
+
manifest_path=manifest,
|
|
52
|
+
instrument_ids=ids,
|
|
53
|
+
start=date(2026, 9, 8),
|
|
54
|
+
end=date(2026, 9, 24),
|
|
55
|
+
)
|
|
56
|
+
factor_run = await run_local_factor(request, factor_store)
|
|
57
|
+
assert factor_run.status == RunStatus.COMPLETED, factor_run.error
|
|
58
|
+
values = factor_store.load_values(factor_run.uuid)["reversal5"]
|
|
59
|
+
maximum_error = Decimal(0)
|
|
60
|
+
labels = []
|
|
61
|
+
for instrument in ids:
|
|
62
|
+
info = await market.get_instrument(instrument)
|
|
63
|
+
calendar = await market.list_trading_days(
|
|
64
|
+
info.exchange, market.start.date(), market.end.date()
|
|
65
|
+
)
|
|
66
|
+
days = sorted(x.trade_date for x in calendar if x.is_open)
|
|
67
|
+
bars = await market.list_bars(instrument, "1d", market.start, market.end, 10000)
|
|
68
|
+
lookup = {bar.timestamp.astimezone(CN).date(): bar for bar in bars}
|
|
69
|
+
for (ts, key), actual in values.items():
|
|
70
|
+
if key != instrument:
|
|
71
|
+
continue
|
|
72
|
+
day = ts.astimezone(CN).date()
|
|
73
|
+
index = days.index(day)
|
|
74
|
+
window = [lookup.get(d) for d in days[index - 4 : index + 1]]
|
|
75
|
+
expected = None
|
|
76
|
+
if len(window) == 5 and all(b and not b.suspended and b.open > 0 for b in window):
|
|
77
|
+
expected = -sum((b.close / b.open - 1 for b in window), Decimal(0)) / 5
|
|
78
|
+
assert (expected is None) == (actual is None)
|
|
79
|
+
if expected is not None:
|
|
80
|
+
maximum_error = max(maximum_error, abs(expected - actual))
|
|
81
|
+
assert abs(expected - actual) < Decimal("1e-12")
|
|
82
|
+
if index + 2 < len(days):
|
|
83
|
+
buy, sell = lookup.get(days[index + 1]), lookup.get(days[index + 2])
|
|
84
|
+
# 显式原始价格标签,只用于流程验证,不宣称投资有效性。
|
|
85
|
+
value = (
|
|
86
|
+
sell.close / buy.close - 1
|
|
87
|
+
if buy and sell and not buy.suspended and not sell.suspended
|
|
88
|
+
else None
|
|
89
|
+
)
|
|
90
|
+
labels.append(
|
|
91
|
+
ResearchLabel(
|
|
92
|
+
ts,
|
|
93
|
+
instrument,
|
|
94
|
+
datetime.combine(days[index + 2], time(15), CN),
|
|
95
|
+
value,
|
|
96
|
+
"raw-next-session-close-to-following-close",
|
|
97
|
+
market.revision,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
evaluation = LocalEvaluationRequest(
|
|
101
|
+
factor_run_uuid=factor_run.uuid, factor_name="reversal5", labels=tuple(labels)
|
|
102
|
+
)
|
|
103
|
+
research_runs = []
|
|
104
|
+
for _ in range(2):
|
|
105
|
+
record = evaluate_local_factor(evaluation, factor_store, build_factor_diagnostics())
|
|
106
|
+
assert record.status == RunStatus.COMPLETED, record.error
|
|
107
|
+
research_store.save(record)
|
|
108
|
+
research_runs.append(record.uuid)
|
|
109
|
+
assert compare_local_research(research_store, tuple(research_runs))["comparable"]
|
|
110
|
+
last = max(ts for ts, _ in values)
|
|
111
|
+
top = tuple(
|
|
112
|
+
key
|
|
113
|
+
for key, value in sorted(
|
|
114
|
+
[
|
|
115
|
+
(key, value)
|
|
116
|
+
for (ts, key), value in values.items()
|
|
117
|
+
if ts == last and value is not None
|
|
118
|
+
],
|
|
119
|
+
key=lambda item: (-item[1], item[0]),
|
|
120
|
+
)[:5]
|
|
121
|
+
)
|
|
122
|
+
query, aliases = await build_local_factor_query(
|
|
123
|
+
factor_run, {"reversal5": values}, market, market.end.date()
|
|
124
|
+
)
|
|
125
|
+
before = await query.get_snapshot(list(aliases.values()), list(ids), last)
|
|
126
|
+
assert all(row.as_of < last for row in before.values)
|
|
127
|
+
after = await query.get_snapshot(
|
|
128
|
+
list(aliases.values()), list(ids), datetime(2026, 9, 28, 9, 30, tzinfo=CN)
|
|
129
|
+
)
|
|
130
|
+
assert after.values and all(row.available_at <= after.decision_time for row in after.values)
|
|
131
|
+
source = """def handle_bar(context, bars):
|
|
132
|
+
if context.event_time.date().isoformat() == "2026-09-28":
|
|
133
|
+
for key in context.universe:
|
|
134
|
+
context.order_shares(key, 200)
|
|
135
|
+
elif context.event_time.date().isoformat() == "2026-09-30":
|
|
136
|
+
for key in context.universe:
|
|
137
|
+
context.order_target(key, 0)
|
|
138
|
+
"""
|
|
139
|
+
backtest_request = LocalBacktestRequest(
|
|
140
|
+
source=source,
|
|
141
|
+
manifest_path=backtest_manifest,
|
|
142
|
+
parameters={"selection_run_uuid": str(factor_run.uuid)},
|
|
143
|
+
config=BacktestConfig(
|
|
144
|
+
strategy_id="local_validation",
|
|
145
|
+
instrument_ids=top,
|
|
146
|
+
frequency="1d",
|
|
147
|
+
start=datetime(2026, 9, 28, 9, 30, tzinfo=CN),
|
|
148
|
+
end=datetime(2026, 9, 30, 15, tzinfo=CN),
|
|
149
|
+
initial_cash=Decimal(1000000),
|
|
150
|
+
strategy_adjustment_type="none",
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
backtest = await run_local_backtest(backtest_request, factor_store)
|
|
154
|
+
assert backtest.status == RunStatus.COMPLETED, backtest.error
|
|
155
|
+
build_local_backtest_store(database).save(backtest)
|
|
156
|
+
result = BacktestResult.model_validate(
|
|
157
|
+
build_local_backtest_store(database).get(backtest.uuid).result
|
|
158
|
+
)
|
|
159
|
+
assert len(result.fills) == 10
|
|
160
|
+
expected_cash = Decimal(1000000) + sum(
|
|
161
|
+
(
|
|
162
|
+
(1 if f.side.value == "sell" else -1) * f.price * f.quantity - f.fees.total
|
|
163
|
+
for f in result.fills
|
|
164
|
+
),
|
|
165
|
+
Decimal(0),
|
|
166
|
+
)
|
|
167
|
+
assert result.final_portfolio_snapshot.cash.total_cash == expected_cash
|
|
168
|
+
assert "quant_backend.common.database.database_session" not in sys.modules
|
|
169
|
+
return {
|
|
170
|
+
"status": "passed",
|
|
171
|
+
"factor_run": str(factor_run.uuid),
|
|
172
|
+
"research_runs": [str(x) for x in research_runs],
|
|
173
|
+
"backtest_run": str(backtest.uuid),
|
|
174
|
+
"instruments": len(ids),
|
|
175
|
+
"cells": len(values),
|
|
176
|
+
"maximum_error": str(maximum_error),
|
|
177
|
+
"fills": len(result.fills),
|
|
178
|
+
"final_cash": str(expected_cash),
|
|
179
|
+
"database": str(database),
|
|
180
|
+
"factor_market_revision": factor_run.provenance["market_revision"],
|
|
181
|
+
"backtest_market_revision": backtest.provenance["market_revision"],
|
|
182
|
+
"labels": "raw-next-session-close-to-following-close; pipeline validation only",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
parser = argparse.ArgumentParser()
|
|
188
|
+
parser.add_argument("--manifest", type=Path, required=True)
|
|
189
|
+
parser.add_argument("--database", type=Path, required=True)
|
|
190
|
+
parser.add_argument("--backtest-manifest", type=Path, required=True)
|
|
191
|
+
args = parser.parse_args()
|
|
192
|
+
report = asyncio.run(validate(args.manifest, args.database, args.backtest_manifest))
|
|
193
|
+
(args.database.parent / "validation_report.json").write_text(
|
|
194
|
+
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
195
|
+
)
|
|
196
|
+
print(json.dumps(report, ensure_ascii=False))
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""固定五股样本:下载定义后,断网 CLI 计算、依赖复用与独立数值/成交复核。"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import date, datetime
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from zoneinfo import ZoneInfo
|
|
12
|
+
|
|
13
|
+
from quant_core.app.backtest.schema import BacktestResult
|
|
14
|
+
|
|
15
|
+
from quant_cli.app.data.service.dataset_load_service import load_dataset
|
|
16
|
+
|
|
17
|
+
CN = ZoneInfo("Asia/Shanghai")
|
|
18
|
+
BOOTSTRAP = """
|
|
19
|
+
import asyncio, sys
|
|
20
|
+
from importlib.abc import MetaPathFinder
|
|
21
|
+
from unittest.mock import patch
|
|
22
|
+
class Guard(MetaPathFinder):
|
|
23
|
+
def find_spec(self, fullname, path=None, target=None):
|
|
24
|
+
if fullname.startswith('quant_backend'):
|
|
25
|
+
raise AssertionError('Remote infrastructure import: ' + fullname)
|
|
26
|
+
sys.meta_path.insert(0, Guard())
|
|
27
|
+
from quant_cli.app.quant_cli import main
|
|
28
|
+
with asyncio.Runner() as runner:
|
|
29
|
+
runner.get_loop()
|
|
30
|
+
with (
|
|
31
|
+
patch('socket.socket', side_effect=AssertionError('Offline network access')),
|
|
32
|
+
patch('asyncio.run', runner.run),
|
|
33
|
+
):
|
|
34
|
+
main()
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate(manifest: Path, factor: Path, strategy: Path, output: Path) -> dict:
|
|
39
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
database = str(output / "research.duckdb")
|
|
41
|
+
market, _ = load_dataset(manifest, 1000000)
|
|
42
|
+
ids = sorted(market.facts)
|
|
43
|
+
|
|
44
|
+
def command(*args, expected=0):
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
[os.environ.get("PQ_VALIDATION_CLIENT_PYTHON", sys.executable), "-c", BOOTSTRAP, *args],
|
|
47
|
+
cwd=output,
|
|
48
|
+
capture_output=True,
|
|
49
|
+
text=True,
|
|
50
|
+
encoding="utf-8",
|
|
51
|
+
timeout=180,
|
|
52
|
+
env={**os.environ, "PYTHONIOENCODING": "utf-8"},
|
|
53
|
+
)
|
|
54
|
+
assert result.returncode == expected, result.stdout + result.stderr
|
|
55
|
+
return json.loads(result.stdout)
|
|
56
|
+
|
|
57
|
+
def config(name, payload):
|
|
58
|
+
path = output / f"{name}.json"
|
|
59
|
+
path.write_text(
|
|
60
|
+
json.dumps(payload, default=str, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
61
|
+
)
|
|
62
|
+
return str(path)
|
|
63
|
+
|
|
64
|
+
factor_request = {
|
|
65
|
+
"manifest_path": str(manifest),
|
|
66
|
+
"instrument_ids": ids,
|
|
67
|
+
"start": "2026-09-08",
|
|
68
|
+
"end": "2026-09-30",
|
|
69
|
+
}
|
|
70
|
+
computed = command(
|
|
71
|
+
"factor",
|
|
72
|
+
"compute",
|
|
73
|
+
"--database",
|
|
74
|
+
database,
|
|
75
|
+
"--definition",
|
|
76
|
+
str(factor),
|
|
77
|
+
"--config",
|
|
78
|
+
config("factor-request", factor_request),
|
|
79
|
+
)
|
|
80
|
+
cells = command(
|
|
81
|
+
"factor", "values", "--run", computed["uuid"], "--limit", "10000", "--database", database
|
|
82
|
+
)
|
|
83
|
+
error, selection = Decimal(0), []
|
|
84
|
+
for cell in cells:
|
|
85
|
+
instrument = cell["instrument_id"]
|
|
86
|
+
day = datetime.fromisoformat(cell["as_of"]).astimezone(CN).date()
|
|
87
|
+
days = sorted(row.trade_date for row in market.facts[instrument].calendar if row.is_open)
|
|
88
|
+
bars = {bar.timestamp.astimezone(CN).date(): bar for bar in market.bars[(instrument, "1d")]}
|
|
89
|
+
index = days.index(day)
|
|
90
|
+
window = [bars.get(d) for d in days[max(0, index - 4) : index + 1]]
|
|
91
|
+
expected = None
|
|
92
|
+
if len(window) == 5 and all(bar and not bar.suspended and bar.open > 0 for bar in window):
|
|
93
|
+
expected = -sum((bar.close / bar.open - 1 for bar in window), Decimal(0)) / 5
|
|
94
|
+
actual = None if cell["value"] is None else Decimal(cell["value"])
|
|
95
|
+
assert (actual is None) == (expected is None)
|
|
96
|
+
if expected is not None:
|
|
97
|
+
error = max(error, abs(actual - expected))
|
|
98
|
+
assert abs(actual - expected) < Decimal("1e-12")
|
|
99
|
+
if day == date(2026, 9, 24) and expected is not None:
|
|
100
|
+
selection.append((instrument, expected))
|
|
101
|
+
top = [key for key, value in sorted(selection, key=lambda row: (-row[1], row[0]))[:3]]
|
|
102
|
+
bundle = json.loads(strategy.read_text(encoding="utf-8"))
|
|
103
|
+
backtest_request = {
|
|
104
|
+
"manifest_path": str(manifest),
|
|
105
|
+
"config": {
|
|
106
|
+
"strategy_id": bundle["definition"]["strategy_id"],
|
|
107
|
+
"instrument_ids": ids,
|
|
108
|
+
"frequency": "1d",
|
|
109
|
+
"start": "2026-09-28T09:30:00+08:00",
|
|
110
|
+
"end": "2026-09-30T15:00:00+08:00",
|
|
111
|
+
"initial_cash": "1000000",
|
|
112
|
+
"strategy_adjustment_type": "none",
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
automatic = command(
|
|
116
|
+
"strategy",
|
|
117
|
+
"run",
|
|
118
|
+
"--database",
|
|
119
|
+
database,
|
|
120
|
+
"--definition",
|
|
121
|
+
str(strategy),
|
|
122
|
+
"--config",
|
|
123
|
+
config("automatic-request", backtest_request),
|
|
124
|
+
)
|
|
125
|
+
reused = command(
|
|
126
|
+
"strategy",
|
|
127
|
+
"run",
|
|
128
|
+
"--database",
|
|
129
|
+
database,
|
|
130
|
+
"--definition",
|
|
131
|
+
str(strategy),
|
|
132
|
+
"--config",
|
|
133
|
+
config(
|
|
134
|
+
"reuse-request",
|
|
135
|
+
{
|
|
136
|
+
**backtest_request,
|
|
137
|
+
"factor_run_uuid": computed["uuid"],
|
|
138
|
+
},
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
result = BacktestResult.model_validate(automatic["result"])
|
|
142
|
+
again = BacktestResult.model_validate(reused["result"])
|
|
143
|
+
assert len(result.fills) == 6
|
|
144
|
+
assert sorted(
|
|
145
|
+
fill.instrument_id for fill in result.fills if fill.side.value == "buy"
|
|
146
|
+
) == sorted(top)
|
|
147
|
+
cash = Decimal(1000000)
|
|
148
|
+
for fill in result.fills:
|
|
149
|
+
bars = {
|
|
150
|
+
bar.timestamp.astimezone(CN).date(): bar
|
|
151
|
+
for bar in market.bars[(fill.instrument_id, "1d")]
|
|
152
|
+
}
|
|
153
|
+
day = date(2026, 9, 28) if fill.side.value == "buy" else date(2026, 9, 30)
|
|
154
|
+
assert fill.quantity == 200
|
|
155
|
+
assert fill.price == bars[day].close
|
|
156
|
+
notional = fill.price * 200
|
|
157
|
+
fees = max(notional * Decimal("0.0003"), Decimal(5))
|
|
158
|
+
if fill.side.value == "sell":
|
|
159
|
+
fees += notional * Decimal("0.0005")
|
|
160
|
+
assert fill.fees.total == fees
|
|
161
|
+
cash += (1 if fill.side.value == "sell" else -1) * notional - fees
|
|
162
|
+
assert result.final_portfolio_snapshot.cash.total_cash == cash
|
|
163
|
+
assert again.final_portfolio_snapshot.cash.total_cash == cash
|
|
164
|
+
|
|
165
|
+
def business(r):
|
|
166
|
+
return [(f.instrument_id, f.side, f.quantity, f.price, f.fees.total) for f in r.fills]
|
|
167
|
+
|
|
168
|
+
assert business(result) == business(again)
|
|
169
|
+
for kind, record in (("factor", computed), ("backtest", automatic), ("backtest", reused)):
|
|
170
|
+
assert command(kind, "get", "--run", record["uuid"], "--database", database) == record
|
|
171
|
+
# 用同一包再次执行;进程重启后固定版本和业务结果仍稳定。
|
|
172
|
+
repeated = command(
|
|
173
|
+
"strategy",
|
|
174
|
+
"run",
|
|
175
|
+
"--database",
|
|
176
|
+
database,
|
|
177
|
+
"--definition",
|
|
178
|
+
str(strategy),
|
|
179
|
+
"--config",
|
|
180
|
+
str(output / "automatic-request.json"),
|
|
181
|
+
)
|
|
182
|
+
assert business(BacktestResult.model_validate(repeated["result"])) == business(result)
|
|
183
|
+
report = {
|
|
184
|
+
"status": "passed",
|
|
185
|
+
"environment": computed["provenance"]["environment"],
|
|
186
|
+
"offline": True,
|
|
187
|
+
"manifest": str(manifest),
|
|
188
|
+
"factor_version": computed["provenance"]["published_definitions"],
|
|
189
|
+
"strategy": automatic["provenance"]["published_strategy"],
|
|
190
|
+
"cells": len(cells),
|
|
191
|
+
"maximum_error": str(error),
|
|
192
|
+
"selected": top,
|
|
193
|
+
"fills": len(result.fills),
|
|
194
|
+
"final_cash": str(cash),
|
|
195
|
+
"automatic": automatic["uuid"],
|
|
196
|
+
"reuse": reused["uuid"],
|
|
197
|
+
"repeat": repeated["uuid"],
|
|
198
|
+
"limitations": ["五股固定工程样本,非全市场或投资收益验收"],
|
|
199
|
+
}
|
|
200
|
+
config("validation-report", report)
|
|
201
|
+
return report
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
parser = argparse.ArgumentParser()
|
|
206
|
+
for name in ("manifest", "factor", "strategy", "output"):
|
|
207
|
+
parser.add_argument(f"--{name}", required=True, type=Path)
|
|
208
|
+
args = parser.parse_args()
|
|
209
|
+
print(
|
|
210
|
+
json.dumps(
|
|
211
|
+
validate(
|
|
212
|
+
args.manifest.resolve(),
|
|
213
|
+
args.factor.resolve(),
|
|
214
|
+
args.strategy.resolve(),
|
|
215
|
+
args.output.resolve(),
|
|
216
|
+
),
|
|
217
|
+
ensure_ascii=False,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""研究因子历史值与可见时间的覆盖,不推断收益预测能力。"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, time
|
|
4
|
+
from zoneinfo import ZoneInfo
|
|
5
|
+
|
|
6
|
+
from quant_core.app.factor.constants import FactorFrequency, FactorStatus
|
|
7
|
+
from quant_core.app.factor.schema.factor_contract import FactorIdentity
|
|
8
|
+
|
|
9
|
+
CN = ZoneInfo("Asia/Shanghai")
|
|
10
|
+
NAMES = ("momentum_20d", "volume_ratio_5d", "ma_close_10")
|
|
11
|
+
STOCKS = (
|
|
12
|
+
"CN.XSHE.000001",
|
|
13
|
+
"CN.XSHE.300750",
|
|
14
|
+
"CN.XSHG.600519",
|
|
15
|
+
"CN.XSHG.688981",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def run(context, parameters):
|
|
20
|
+
if context.factors is None:
|
|
21
|
+
raise ValueError("研究运行未注入因子查询能力")
|
|
22
|
+
start = datetime.combine(context.start, time.min, CN)
|
|
23
|
+
end = datetime.combine(context.end, time.max, CN)
|
|
24
|
+
decision_time = datetime.combine(context.end, time(15), CN)
|
|
25
|
+
observations = []
|
|
26
|
+
exclusions = []
|
|
27
|
+
values_total = 0
|
|
28
|
+
for name in NAMES:
|
|
29
|
+
identity = FactorIdentity(
|
|
30
|
+
name=name,
|
|
31
|
+
source="local-python",
|
|
32
|
+
definition_version="1",
|
|
33
|
+
frequency=FactorFrequency.DAILY,
|
|
34
|
+
)
|
|
35
|
+
for instrument_id in STOCKS:
|
|
36
|
+
values = await context.factors.get_series(
|
|
37
|
+
identity, instrument_id, start, end, decision_time
|
|
38
|
+
)
|
|
39
|
+
available = [row for row in values if row.status is FactorStatus.AVAILABLE]
|
|
40
|
+
if not available:
|
|
41
|
+
exclusions.append(
|
|
42
|
+
{
|
|
43
|
+
"factor": name,
|
|
44
|
+
"instrument_id": instrument_id,
|
|
45
|
+
"reason": "决策时点无可见因子值",
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
continue
|
|
49
|
+
assert all(row.available_at <= decision_time for row in values)
|
|
50
|
+
values_total += len(available)
|
|
51
|
+
observations.append(
|
|
52
|
+
{
|
|
53
|
+
"factor": name,
|
|
54
|
+
"instrument_id": instrument_id,
|
|
55
|
+
"available_count": len(available),
|
|
56
|
+
"first_as_of": min(row.as_of for row in available).isoformat(),
|
|
57
|
+
"last_as_of": max(row.as_of for row in available).isoformat(),
|
|
58
|
+
"latest_available_at": max(row.available_at for row in available).isoformat(),
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
return {
|
|
62
|
+
"question": "本地三因子对四只股票的 PIT 历史覆盖是否足以开展收益研究?",
|
|
63
|
+
"conclusion": (
|
|
64
|
+
f"12 个因子×股票组合中 {len(observations)} 个有可见值,"
|
|
65
|
+
f"共 {values_total} 个可见因子值;当前只能验收覆盖和可见时间,"
|
|
66
|
+
"样本不足以判断预测能力。"
|
|
67
|
+
),
|
|
68
|
+
"methodology": "按因子身份和股票分别读取截至样本结束日 15:00 已可见的历史值;不填补缺失。",
|
|
69
|
+
"sample_start": context.start,
|
|
70
|
+
"sample_end": context.end,
|
|
71
|
+
"candidate_count": len(NAMES) * len(STOCKS),
|
|
72
|
+
"observations": observations,
|
|
73
|
+
"exclusions": exclusions,
|
|
74
|
+
"metrics": [
|
|
75
|
+
{
|
|
76
|
+
"key": "available_values",
|
|
77
|
+
"label": "可见因子值行数",
|
|
78
|
+
"value": values_total,
|
|
79
|
+
"unit": "count",
|
|
80
|
+
"explanation": "按因子身份×标的×归属日期统计;不是独立收益样本数。",
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"limitations": [
|
|
84
|
+
"只检查三因子、四只股票和短窗口,不能外推至全市场。",
|
|
85
|
+
"未计算未来收益、IC、分层表现或样本外结果,不构成因子有效性证明。",
|
|
86
|
+
],
|
|
87
|
+
}
|