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,271 @@
|
|
|
1
|
+
"""本地回测用例:已准备的数据与显式定义进入公共执行链。"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from zoneinfo import ZoneInfo
|
|
6
|
+
|
|
7
|
+
from quant_core.app.backtest.service.backtest_execution_service import execute_backtest
|
|
8
|
+
from quant_core.app.data.constants import BarFrequency
|
|
9
|
+
from quant_core.app.factor.interface.public.factor_local import LocalFactorStore
|
|
10
|
+
from quant_core.app.strategy.schema.python_strategy_schema import PythonStrategySpec
|
|
11
|
+
from quant_core.app.strategy.strategy_dependency import python_strategy
|
|
12
|
+
from quant_core.common.local_run import LocalRunRecord, RunStatus, content_digest
|
|
13
|
+
|
|
14
|
+
from quant_cli.app.backtest.local_backtest_schema import LocalBacktestRequest
|
|
15
|
+
from quant_cli.app.data.data_dependency import load_backtest_market
|
|
16
|
+
from quant_cli.app.data.schema.dataset_audit_schema import DatasetAuditRequest
|
|
17
|
+
from quant_cli.app.data.service.dataset_audit_service import audit_dataset
|
|
18
|
+
from quant_cli.app.factor.local_factor_dependency import (
|
|
19
|
+
build_local_factor_query,
|
|
20
|
+
decode_factor_artifact,
|
|
21
|
+
load_factor_result,
|
|
22
|
+
)
|
|
23
|
+
from quant_cli.common.utils.runtime_environment import runtime_versions
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def run_local_backtest(
|
|
27
|
+
request: LocalBacktestRequest, factor_store: LocalFactorStore | None = None
|
|
28
|
+
) -> LocalRunRecord:
|
|
29
|
+
"""返回可归档记录;不创建本地或远端数据库连接。"""
|
|
30
|
+
record = LocalRunRecord(
|
|
31
|
+
request=request.model_dump(mode="json"),
|
|
32
|
+
provenance={
|
|
33
|
+
"source_digest": content_digest(request.source),
|
|
34
|
+
"environment": runtime_versions(),
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
try:
|
|
38
|
+
components = load_backtest_market(request.manifest_path, request.max_rows)
|
|
39
|
+
record.provenance.update(
|
|
40
|
+
manifest_digest=content_digest(request.manifest_path.read_text(encoding="utf-8")),
|
|
41
|
+
market_revision=components.query.revision,
|
|
42
|
+
)
|
|
43
|
+
if request.config.frequency == BarFrequency.D1 and (
|
|
44
|
+
request.config.execution_frequency is None
|
|
45
|
+
or request.config.execution_frequency == BarFrequency.D1
|
|
46
|
+
):
|
|
47
|
+
audit = await audit_dataset(
|
|
48
|
+
components.query,
|
|
49
|
+
components.verification,
|
|
50
|
+
DatasetAuditRequest(
|
|
51
|
+
instrument_ids=request.config.instrument_ids,
|
|
52
|
+
start=request.config.start.astimezone(ZoneInfo("Asia/Shanghai")).date(),
|
|
53
|
+
end=request.config.end.astimezone(ZoneInfo("Asia/Shanghai")).date(),
|
|
54
|
+
require_special_type=request.exclude_st,
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
record.provenance["dataset_audit"] = {
|
|
58
|
+
**audit.model_dump(mode="json"),
|
|
59
|
+
"ready": audit.ready,
|
|
60
|
+
}
|
|
61
|
+
if not audit.ready:
|
|
62
|
+
summary = "; ".join(
|
|
63
|
+
f"{gap.instrument_id}/{gap.dataset}: {gap.reason}" for gap in audit.gaps[:20]
|
|
64
|
+
)
|
|
65
|
+
raise ValueError("回测数据检查未通过;请修复数据并重新准备快照:" + summary)
|
|
66
|
+
if request.publication is not None:
|
|
67
|
+
spec = request.publication.definition
|
|
68
|
+
record.provenance["published_strategy"] = {
|
|
69
|
+
"strategy_id": spec.strategy_id,
|
|
70
|
+
"version": spec.version,
|
|
71
|
+
"content_digest": spec.content_digest,
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
spec.factor_refs
|
|
75
|
+
and request.factor_run_uuid is None
|
|
76
|
+
and request.factor_artifact_path is None
|
|
77
|
+
):
|
|
78
|
+
from quant_core.app.factor.schema.local_factor_schema import LocalFactorRequest
|
|
79
|
+
|
|
80
|
+
from quant_cli.app.factor.local_factor_dependency import compute_local_factor
|
|
81
|
+
|
|
82
|
+
previous_days = []
|
|
83
|
+
for instrument_id in request.config.instrument_ids:
|
|
84
|
+
instrument = await components.query.get_instrument(instrument_id)
|
|
85
|
+
calendar = await components.query.list_trading_days(
|
|
86
|
+
instrument.exchange,
|
|
87
|
+
components.query.start.date(),
|
|
88
|
+
request.config.start.date(),
|
|
89
|
+
)
|
|
90
|
+
before_start = [
|
|
91
|
+
day.trade_date
|
|
92
|
+
for day in calendar
|
|
93
|
+
if day.is_open and day.trade_date < request.config.start.date()
|
|
94
|
+
]
|
|
95
|
+
if not before_start:
|
|
96
|
+
raise ValueError("发布策略缺少首个决策日前一交易日的因子输入")
|
|
97
|
+
previous_days.append(max(before_start))
|
|
98
|
+
factor_request = LocalFactorRequest(
|
|
99
|
+
definitions=request.publication.factors,
|
|
100
|
+
manifest_path=request.manifest_path,
|
|
101
|
+
instrument_ids=request.config.instrument_ids,
|
|
102
|
+
start=min(previous_days),
|
|
103
|
+
end=request.config.end.date(),
|
|
104
|
+
view=request.factor_view,
|
|
105
|
+
exclude_st=request.exclude_st,
|
|
106
|
+
max_rows=request.max_rows,
|
|
107
|
+
)
|
|
108
|
+
factor_run, factor_values = await compute_local_factor(factor_request)
|
|
109
|
+
if factor_run.status != RunStatus.COMPLETED:
|
|
110
|
+
raise ValueError(f"发布策略因子依赖计算失败: {factor_run.error}")
|
|
111
|
+
if factor_store is None:
|
|
112
|
+
raise ValueError("发布策略因子依赖需显式本地归档能力")
|
|
113
|
+
factor_store.save(factor_run, factor_values)
|
|
114
|
+
request = request.model_copy(update={"factor_run_uuid": factor_run.uuid})
|
|
115
|
+
record.request = request.model_dump(mode="json")
|
|
116
|
+
factors, aliases, revision = None, {}, None
|
|
117
|
+
if request.factor_run_uuid is not None or request.factor_artifact_path is not None:
|
|
118
|
+
if request.factor_artifact_path is not None:
|
|
119
|
+
factor_run, values = decode_factor_artifact(
|
|
120
|
+
request.factor_artifact_path.read_text(encoding="utf-8")
|
|
121
|
+
)
|
|
122
|
+
if factor_run.status != RunStatus.COMPLETED:
|
|
123
|
+
raise ValueError("独立因子产物未成功")
|
|
124
|
+
else:
|
|
125
|
+
if factor_store is None or request.factor_run_uuid is None:
|
|
126
|
+
raise ValueError("引用因子运行时必须显式提供读取能力")
|
|
127
|
+
factor_run, values = load_factor_result(factor_store, request.factor_run_uuid)
|
|
128
|
+
if factor_run.provenance["market_revision"] != components.query.revision:
|
|
129
|
+
raise ValueError("因子与回测行情修订不同")
|
|
130
|
+
if request.publication is not None:
|
|
131
|
+
if (
|
|
132
|
+
factor_run.provenance.get("manifest_digest")
|
|
133
|
+
!= record.provenance["manifest_digest"]
|
|
134
|
+
):
|
|
135
|
+
raise ValueError("因子与回测输入快照不同")
|
|
136
|
+
required = {
|
|
137
|
+
item.definition.identity.name: item.content_digest
|
|
138
|
+
for item in request.publication.factors
|
|
139
|
+
}
|
|
140
|
+
if factor_run.provenance.get("published_digests") != required:
|
|
141
|
+
raise ValueError("本地因子结果不匹配发布策略的固定依赖")
|
|
142
|
+
identities = {
|
|
143
|
+
item.definition.identity.name: item.definition.identity.model_dump(mode="json")
|
|
144
|
+
for item in request.publication.factors
|
|
145
|
+
}
|
|
146
|
+
if factor_run.provenance.get("published_definitions") != identities:
|
|
147
|
+
raise ValueError("本地因子结果的完整身份不匹配")
|
|
148
|
+
for field in ("view", "exclude_st", "instrument_ids"):
|
|
149
|
+
expected = (
|
|
150
|
+
request.factor_view.model_dump(mode="json")
|
|
151
|
+
if field == "view"
|
|
152
|
+
else request.exclude_st
|
|
153
|
+
if field == "exclude_st"
|
|
154
|
+
else list(request.config.instrument_ids)
|
|
155
|
+
)
|
|
156
|
+
if factor_run.request.get(field) != expected:
|
|
157
|
+
raise ValueError("本地因子结果的输入政策不匹配")
|
|
158
|
+
from datetime import date
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
date.fromisoformat(factor_run.request["start"]) >= request.config.start.date()
|
|
162
|
+
or date.fromisoformat(factor_run.request["end"]) < request.config.end.date()
|
|
163
|
+
):
|
|
164
|
+
raise ValueError("本地因子结果未覆盖回测窗口及首日前一交易日")
|
|
165
|
+
for instrument_id in request.config.instrument_ids:
|
|
166
|
+
instrument = await components.query.get_instrument(instrument_id)
|
|
167
|
+
calendar = await components.query.list_trading_days(
|
|
168
|
+
instrument.exchange,
|
|
169
|
+
components.query.start.date(),
|
|
170
|
+
request.config.end.date(),
|
|
171
|
+
)
|
|
172
|
+
before_start = [
|
|
173
|
+
day.trade_date
|
|
174
|
+
for day in calendar
|
|
175
|
+
if day.is_open and day.trade_date < request.config.start.date()
|
|
176
|
+
]
|
|
177
|
+
if not before_start:
|
|
178
|
+
raise ValueError("首日前一交易日缺少输入")
|
|
179
|
+
needed = {max(before_start)} | {
|
|
180
|
+
day.trade_date
|
|
181
|
+
for day in calendar
|
|
182
|
+
if day.is_open
|
|
183
|
+
and request.config.start.date()
|
|
184
|
+
<= day.trade_date
|
|
185
|
+
< request.config.end.date()
|
|
186
|
+
}
|
|
187
|
+
for name in required:
|
|
188
|
+
present = {
|
|
189
|
+
ts.date() for ts, key in values.get(name, {}) if key == instrument_id
|
|
190
|
+
}
|
|
191
|
+
if needed - present:
|
|
192
|
+
raise ValueError("本地因子结果缺少必要交易日单元,不能静默跳过")
|
|
193
|
+
factors, aliases = await build_local_factor_query(
|
|
194
|
+
factor_run, values, components.query, components.query.end.date()
|
|
195
|
+
)
|
|
196
|
+
revision = str(factor_run.uuid)
|
|
197
|
+
record.provenance["factor_run_uuid"] = revision
|
|
198
|
+
refs = tuple(aliases.values())
|
|
199
|
+
if request.publication is not None:
|
|
200
|
+
spec = request.publication.definition
|
|
201
|
+
refs = spec.factor_refs
|
|
202
|
+
aliases = spec.resolved_factors()
|
|
203
|
+
record.provenance["source_digest"] = spec.source_digest
|
|
204
|
+
else:
|
|
205
|
+
spec = PythonStrategySpec(
|
|
206
|
+
strategy_id=request.config.strategy_id,
|
|
207
|
+
version="local",
|
|
208
|
+
name=request.config.strategy_id,
|
|
209
|
+
python_source=request.source,
|
|
210
|
+
universe=request.config.instrument_ids,
|
|
211
|
+
parameters=request.parameters,
|
|
212
|
+
factor_refs=refs,
|
|
213
|
+
factor_aliases=aliases,
|
|
214
|
+
)
|
|
215
|
+
result = await execute_backtest(
|
|
216
|
+
request.config,
|
|
217
|
+
python_strategy(spec, request.config.instrument_ids),
|
|
218
|
+
components,
|
|
219
|
+
slippage_bps=request.slippage_bps,
|
|
220
|
+
factors=factors,
|
|
221
|
+
factor_refs=refs,
|
|
222
|
+
factor_aliases=aliases,
|
|
223
|
+
factor_revision=revision,
|
|
224
|
+
)
|
|
225
|
+
record.result = result.model_dump(mode="json")
|
|
226
|
+
reasons = Counter(
|
|
227
|
+
attempt.defer_reason.value
|
|
228
|
+
for attempt in result.execution_attempts
|
|
229
|
+
if attempt.defer_reason is not None
|
|
230
|
+
)
|
|
231
|
+
record.result["execution_diagnostics"] = {
|
|
232
|
+
"fill_count": len(result.fills),
|
|
233
|
+
"deferred_reasons": dict(sorted(reasons.items())),
|
|
234
|
+
"zero_fills_with_deferred": not result.fills and bool(reasons),
|
|
235
|
+
"message": "无成交且存在延后原因,请检查撮合条件与数据事实"
|
|
236
|
+
if not result.fills and reasons
|
|
237
|
+
else None,
|
|
238
|
+
}
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
logging.getLogger(__name__).exception("本地计算失败")
|
|
241
|
+
record.status, record.error = RunStatus.FAILED, str(exc)
|
|
242
|
+
return record
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def compare_backtest_records(records: tuple[LocalRunRecord, ...]) -> dict:
|
|
246
|
+
"""对照固定市场输入与账户配置,展示各次回测结果。"""
|
|
247
|
+
if not 2 <= len(records) <= 100:
|
|
248
|
+
raise ValueError("需选择 2 至 100 次回测")
|
|
249
|
+
first, reasons = records[0], set()
|
|
250
|
+
for record in records:
|
|
251
|
+
if record.status != RunStatus.COMPLETED:
|
|
252
|
+
reasons.add("failed_run")
|
|
253
|
+
if record.provenance.get("manifest_digest") != first.provenance.get("manifest_digest"):
|
|
254
|
+
reasons.add("market_snapshot_differs")
|
|
255
|
+
for key in (
|
|
256
|
+
"start",
|
|
257
|
+
"end",
|
|
258
|
+
"instrument_ids",
|
|
259
|
+
"frequency",
|
|
260
|
+
"initial_cash",
|
|
261
|
+
"execution_frequency",
|
|
262
|
+
"execution_policy",
|
|
263
|
+
"strategy_adjustment_type",
|
|
264
|
+
):
|
|
265
|
+
if record.request["config"].get(key) != first.request["config"].get(key):
|
|
266
|
+
reasons.add(f"{key}_differs")
|
|
267
|
+
return {
|
|
268
|
+
"comparable": not reasons,
|
|
269
|
+
"reasons": sorted(reasons),
|
|
270
|
+
"runs": [record.model_dump(mode="json") for record in records],
|
|
271
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""显式在线数据准备与离线快照校验。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from quant_cli.app.data.data_dependency import build_market_query
|
|
10
|
+
from quant_cli.app.data.schema.dataset_request_schema import DatasetPrepareRequest
|
|
11
|
+
from quant_cli.app.data.service.dataset_load_service import load_dataset
|
|
12
|
+
from quant_cli.app.data.service.dataset_prepare_service import prepare_dataset
|
|
13
|
+
from quant_cli.common.constants import DEFAULT_DATASETS_DIR
|
|
14
|
+
from quant_cli.common.utils.cli_utils import cli_errors, output
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="行情快照准备与校验", no_args_is_help=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("prepare")
|
|
20
|
+
@cli_errors
|
|
21
|
+
def prepare(
|
|
22
|
+
config: Annotated[Path, typer.Option(exists=True)],
|
|
23
|
+
cache_dir: Annotated[Path, typer.Option()] = DEFAULT_DATASETS_DIR,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""准备显式行情范围与预热窗口,返回实际 Manifest 路径。"""
|
|
26
|
+
request = DatasetPrepareRequest.model_validate_json(config.read_text(encoding="utf-8"))
|
|
27
|
+
|
|
28
|
+
async def execute() -> str:
|
|
29
|
+
async with build_market_query() as client:
|
|
30
|
+
result = await prepare_dataset(
|
|
31
|
+
client,
|
|
32
|
+
instrument_ids=request.instrument_ids,
|
|
33
|
+
frequency=request.frequency.value,
|
|
34
|
+
execution_frequency=request.execution_frequency.value,
|
|
35
|
+
start=request.start,
|
|
36
|
+
end=request.end,
|
|
37
|
+
adjustment_type=request.adjustment_type,
|
|
38
|
+
cache_dir=cache_dir,
|
|
39
|
+
lookback_days=request.lookback_days,
|
|
40
|
+
max_rows=request.max_rows,
|
|
41
|
+
revision=request.revision,
|
|
42
|
+
)
|
|
43
|
+
return str(result.manifest_path.resolve())
|
|
44
|
+
|
|
45
|
+
output({"manifest_path": asyncio.run(execute())})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command("verify")
|
|
49
|
+
@cli_errors
|
|
50
|
+
def verify(manifest: Annotated[Path, typer.Option(exists=True)], max_rows: int = 1000000) -> None:
|
|
51
|
+
market, _ = load_dataset(manifest, max_rows)
|
|
52
|
+
output(
|
|
53
|
+
{
|
|
54
|
+
"status": "verified",
|
|
55
|
+
"revision": market.revision,
|
|
56
|
+
"start": market.start,
|
|
57
|
+
"end": market.end,
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command("audit")
|
|
63
|
+
@cli_errors
|
|
64
|
+
def audit(
|
|
65
|
+
manifest: Annotated[Path, typer.Option(exists=True)],
|
|
66
|
+
config: Annotated[Path, typer.Option(exists=True)],
|
|
67
|
+
max_rows: int = 1000000,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""检查日线研究/回测快照的事实与覆盖;缺口以 JSON 返回并非零退出。"""
|
|
70
|
+
from quant_cli.app.data.schema.dataset_audit_schema import DatasetAuditRequest
|
|
71
|
+
from quant_cli.app.data.service.dataset_audit_service import audit_dataset
|
|
72
|
+
|
|
73
|
+
market, verification = load_dataset(manifest, max_rows)
|
|
74
|
+
request = DatasetAuditRequest.model_validate_json(config.read_text(encoding="utf-8"))
|
|
75
|
+
report = asyncio.run(audit_dataset(market, verification, request))
|
|
76
|
+
output({"ready": report.ready, **report.model_dump(mode="json")})
|
|
77
|
+
if not report.ready:
|
|
78
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pqdata import MarketClient
|
|
2
|
+
|
|
3
|
+
"""量化数据组装;数据库会话不再参与行情读取。"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from quant_core.app.backtest.backtest_input_schema import BacktestMarketComponents
|
|
8
|
+
from quant_core.app.trading_runtime.replay.exchange_state_resolver import ExchangeStateResolver
|
|
9
|
+
from quant_core.app.trading_runtime.replay.market_runtime_service import ReplayMarketEventSource
|
|
10
|
+
from quant_core.app.trading_runtime.replay.market_state_resolver import MarketStateResolver
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_backtest_market(manifest_path: Path, max_rows: int) -> BacktestMarketComponents:
|
|
14
|
+
"""从已校验本地快照组装回放输入,不访问 SDK 或数据库。"""
|
|
15
|
+
from quant_cli.app.data.service.dataset_load_service import load_dataset
|
|
16
|
+
|
|
17
|
+
market, verification = load_dataset(manifest_path, max_rows)
|
|
18
|
+
return BacktestMarketComponents(
|
|
19
|
+
market,
|
|
20
|
+
verification,
|
|
21
|
+
ReplayMarketEventSource(market),
|
|
22
|
+
MarketStateResolver(),
|
|
23
|
+
ExchangeStateResolver(),
|
|
24
|
+
manifest_path,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_market_query() -> MarketClient:
|
|
29
|
+
"""显式在线准备入口,配置仅为服务地址与令牌。"""
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
return MarketClient(
|
|
33
|
+
os.getenv("MARKET_BASE_URL", "http://localhost:8002/api/v1"),
|
|
34
|
+
token=os.getenv("MARKET_API_TOKEN", ""),
|
|
35
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""不可变表格文件与原子清单;业务键由数据集提供,不限定行情时间模型。"""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import polars as pl
|
|
13
|
+
|
|
14
|
+
from quant_cli.app.data.schema.parquet_file_schema import ParquetFile
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def file_digest(path: Path) -> str:
|
|
18
|
+
"""流式校验文件,避免为了摘要再次读入完整文件。"""
|
|
19
|
+
with path.open("rb") as stream:
|
|
20
|
+
return hashlib.file_digest(stream, "sha256").hexdigest()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def atomic_json(path: Path, payload: dict[str, Any]) -> None:
|
|
24
|
+
"""先完整刷盘再替换单个指针;引用文件必须在调用前已完成写入。"""
|
|
25
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
with tempfile.NamedTemporaryFile(dir=path.parent, suffix=".part", delete=False) as stream:
|
|
27
|
+
temporary = Path(stream.name)
|
|
28
|
+
try:
|
|
29
|
+
stream.write(json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8"))
|
|
30
|
+
stream.flush()
|
|
31
|
+
os.fsync(stream.fileno())
|
|
32
|
+
except BaseException:
|
|
33
|
+
stream.close()
|
|
34
|
+
temporary.unlink(missing_ok=True)
|
|
35
|
+
raise
|
|
36
|
+
try:
|
|
37
|
+
os.replace(temporary, path)
|
|
38
|
+
finally:
|
|
39
|
+
temporary.unlink(missing_ok=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@contextmanager
|
|
43
|
+
def cache_writer(directory: Path) -> Iterator[None]:
|
|
44
|
+
"""进程级非阻塞单写者锁;进程退出自动释放,锁文件不删除。"""
|
|
45
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
with (directory / ".writer.lock").open("a+b") as stream:
|
|
47
|
+
if stream.tell() == 0:
|
|
48
|
+
stream.write(b"0")
|
|
49
|
+
stream.flush()
|
|
50
|
+
stream.seek(0)
|
|
51
|
+
if os.name == "nt":
|
|
52
|
+
import msvcrt
|
|
53
|
+
|
|
54
|
+
msvcrt.locking(stream.fileno(), msvcrt.LK_NBLCK, 1)
|
|
55
|
+
else:
|
|
56
|
+
import fcntl
|
|
57
|
+
|
|
58
|
+
fcntl.flock(stream.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
59
|
+
try:
|
|
60
|
+
yield
|
|
61
|
+
finally:
|
|
62
|
+
stream.seek(0)
|
|
63
|
+
if os.name == "nt":
|
|
64
|
+
msvcrt.locking(stream.fileno(), msvcrt.LK_UNLCK, 1)
|
|
65
|
+
else:
|
|
66
|
+
fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def write_parquet(
|
|
70
|
+
directory: Path,
|
|
71
|
+
frame: pl.DataFrame,
|
|
72
|
+
*,
|
|
73
|
+
dataset_id: str,
|
|
74
|
+
primary_key: tuple[str, ...],
|
|
75
|
+
schema_version: int = 1,
|
|
76
|
+
) -> ParquetFile:
|
|
77
|
+
"""拒绝重复或空业务键;按键排序、压缩写入后以内容地址保存。"""
|
|
78
|
+
if not primary_key or not set(primary_key).issubset(frame.columns):
|
|
79
|
+
raise ValueError("数据集必须声明存在的业务键")
|
|
80
|
+
keys = frame.select(primary_key)
|
|
81
|
+
if any(keys.null_count().row(0)) or keys.is_duplicated().any():
|
|
82
|
+
raise ValueError("数据集业务键为空或重复")
|
|
83
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
with tempfile.NamedTemporaryFile(dir=directory, suffix=".part", delete=False) as stream:
|
|
85
|
+
temporary = Path(stream.name)
|
|
86
|
+
try:
|
|
87
|
+
frame.sort(primary_key).write_parquet(temporary, compression="zstd")
|
|
88
|
+
with temporary.open("rb+") as stream:
|
|
89
|
+
os.fsync(stream.fileno())
|
|
90
|
+
digest = file_digest(temporary)
|
|
91
|
+
path = directory / f"{digest}.parquet"
|
|
92
|
+
if path.exists():
|
|
93
|
+
if file_digest(path) != digest:
|
|
94
|
+
raise ValueError("已有内容地址文件损坏")
|
|
95
|
+
else:
|
|
96
|
+
os.replace(temporary, path)
|
|
97
|
+
return ParquetFile(
|
|
98
|
+
path=path.name,
|
|
99
|
+
dataset_id=dataset_id,
|
|
100
|
+
schema_version=schema_version,
|
|
101
|
+
primary_key=primary_key,
|
|
102
|
+
columns=tuple(frame.columns),
|
|
103
|
+
row_count=frame.height,
|
|
104
|
+
byte_count=path.stat().st_size,
|
|
105
|
+
sha256=digest,
|
|
106
|
+
)
|
|
107
|
+
finally:
|
|
108
|
+
temporary.unlink(missing_ok=True)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def read_parquet(directory: Path, receipt: ParquetFile, max_rows: int) -> pl.DataFrame:
|
|
112
|
+
"""校验完整性、容量和键后返回表格;不触发任何网络读取。"""
|
|
113
|
+
path = (directory / receipt.path).resolve()
|
|
114
|
+
if path.parent != directory.resolve() or receipt.row_count > max_rows:
|
|
115
|
+
raise ValueError("缓存路径越界或超出行数上限")
|
|
116
|
+
if path.stat().st_size != receipt.byte_count or file_digest(path) != receipt.sha256:
|
|
117
|
+
raise ValueError("缓存文件校验失败")
|
|
118
|
+
frame = pl.read_parquet(path, n_rows=max_rows + 1)
|
|
119
|
+
if frame.height != receipt.row_count or tuple(frame.columns) != receipt.columns:
|
|
120
|
+
raise ValueError("缓存表结构或行数不一致")
|
|
121
|
+
keys = frame.select(receipt.primary_key)
|
|
122
|
+
if any(keys.null_count().row(0)) or keys.is_duplicated().any():
|
|
123
|
+
raise ValueError("缓存业务键非法")
|
|
124
|
+
return frame
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""日线研究与回测共用快照的就绪检查请求及缺口。"""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DatasetAuditRequest(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
instrument_ids: tuple[str, ...] = Field(min_length=1)
|
|
11
|
+
start: date
|
|
12
|
+
end: date
|
|
13
|
+
require_special_type: bool = False
|
|
14
|
+
|
|
15
|
+
@model_validator(mode="after")
|
|
16
|
+
def validate_range(self) -> "DatasetAuditRequest":
|
|
17
|
+
if self.start > self.end or len(set(self.instrument_ids)) != len(self.instrument_ids):
|
|
18
|
+
raise ValueError("检查范围或股票池非法")
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DatasetGap(BaseModel):
|
|
23
|
+
instrument_id: str
|
|
24
|
+
dataset: str
|
|
25
|
+
reason: str
|
|
26
|
+
count: int
|
|
27
|
+
dates: tuple[date, ...] = ()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DatasetAuditReport(BaseModel):
|
|
31
|
+
revision: str
|
|
32
|
+
request: DatasetAuditRequest
|
|
33
|
+
gaps: list[DatasetGap] = Field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def ready(self) -> bool:
|
|
37
|
+
return not self.gaps
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""数据准备请求:公开快照范围与容量约束。"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
6
|
+
from quant_core.app.data.constants import BarFrequency
|
|
7
|
+
from quant_core.app.data.schema.adjustment_schema import AdjustmentType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DatasetPrepareRequest(BaseModel):
|
|
11
|
+
model_config = ConfigDict(extra="forbid")
|
|
12
|
+
instrument_ids: tuple[str, ...] = Field(min_length=1)
|
|
13
|
+
frequency: BarFrequency = BarFrequency.D1
|
|
14
|
+
execution_frequency: BarFrequency = BarFrequency.D1
|
|
15
|
+
start: datetime
|
|
16
|
+
end: datetime
|
|
17
|
+
adjustment_type: AdjustmentType = AdjustmentType.PRE
|
|
18
|
+
lookback_days: int = Field(default=30, ge=0, le=3660)
|
|
19
|
+
max_rows: int = Field(default=1000000, ge=1)
|
|
20
|
+
revision: str | None = None
|
|
21
|
+
|
|
22
|
+
@model_validator(mode="after")
|
|
23
|
+
def validate_input(self) -> "DatasetPrepareRequest":
|
|
24
|
+
if self.start.utcoffset() is None or self.end.utcoffset() is None or self.start >= self.end:
|
|
25
|
+
raise ValueError("需指定带时区的合法时间范围")
|
|
26
|
+
if len(set(self.instrument_ids)) != len(self.instrument_ids):
|
|
27
|
+
raise ValueError("股票池重复")
|
|
28
|
+
return self
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""不可变 Parquet 文件描述契约。"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ParquetFile(BaseModel):
|
|
7
|
+
"""文件的精确身份与表结构;覆盖证据由各数据集清单另行记录。"""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
10
|
+
path: str
|
|
11
|
+
dataset_id: str
|
|
12
|
+
schema_version: int = Field(ge=1)
|
|
13
|
+
primary_key: tuple[str, ...]
|
|
14
|
+
columns: tuple[str, ...]
|
|
15
|
+
row_count: int = Field(ge=0)
|
|
16
|
+
byte_count: int = Field(ge=0)
|
|
17
|
+
sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
|
|
File without changes
|