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,61 @@
|
|
|
1
|
+
"""回放准备所需的市场事实;事件与时间片仍由量化端组织。"""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
8
|
+
|
|
9
|
+
from pqdata.schema.adjustment_schema import AdjustmentFactorResponse
|
|
10
|
+
from pqdata.schema.corporate_action_schema import CorporateActionResponse
|
|
11
|
+
from pqdata.schema.reference_schema import (
|
|
12
|
+
InstrumentResponse,
|
|
13
|
+
TradingCalendarDayResponse,
|
|
14
|
+
TradingRuleResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DailyFactResponse(BaseModel):
|
|
19
|
+
model_config = ConfigDict(from_attributes=True)
|
|
20
|
+
instrument_id: str
|
|
21
|
+
trade_date: date
|
|
22
|
+
suspension_status: Literal["none", "full_day", "intraday"] | None
|
|
23
|
+
special_type: Literal["Normal", "ST", "StarST"] | None = None
|
|
24
|
+
special_type_source: str | None = None
|
|
25
|
+
limit_up: Decimal | None = None
|
|
26
|
+
limit_dn: Decimal | None = None
|
|
27
|
+
extra: dict[str, Any] | None = None
|
|
28
|
+
source_provider: str
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def is_st(self) -> bool | None:
|
|
32
|
+
"""未知保持 None,调用方不能把缺少数据当成正常。"""
|
|
33
|
+
return None if self.special_type is None else self.special_type != "Normal"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_suspended(self) -> bool | None:
|
|
37
|
+
"""只判断全天停牌;False 不表示全天每一分钟都可交易。"""
|
|
38
|
+
return None if self.suspension_status is None else self.suspension_status == "full_day"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class HistoryFactsQuery(BaseModel):
|
|
42
|
+
instrument_id: str
|
|
43
|
+
start: date
|
|
44
|
+
end: date
|
|
45
|
+
revision: str | None = None
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="after")
|
|
48
|
+
def validate_range(self) -> "HistoryFactsQuery":
|
|
49
|
+
if not 0 <= (self.end - self.start).days <= 3660:
|
|
50
|
+
raise ValueError("History facts range must be between zero and ten years")
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class HistoryFacts(BaseModel):
|
|
55
|
+
revision: str
|
|
56
|
+
instrument: InstrumentResponse
|
|
57
|
+
calendar: list[TradingCalendarDayResponse]
|
|
58
|
+
trading_rules: list[TradingRuleResponse]
|
|
59
|
+
daily_facts: list[DailyFactResponse]
|
|
60
|
+
adjustment_factors: list[AdjustmentFactorResponse]
|
|
61
|
+
corporate_actions: list[CorporateActionResponse]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Market-owned daily price windows for downstream PIT calculations."""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
from pqdata.schema.adjustment_schema import AdjustedBarResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PITDailyWindow(BaseModel):
|
|
11
|
+
model_config = ConfigDict(frozen=True)
|
|
12
|
+
|
|
13
|
+
as_of: date
|
|
14
|
+
anchor_date: date
|
|
15
|
+
bars: tuple[AdjustedBarResponse, ...]
|
|
16
|
+
input_digest: str
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""研究与历史数据读取的显式请求契约。"""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
from pydantic import AwareDatetime, BaseModel, Field, model_validator
|
|
6
|
+
|
|
7
|
+
from pqdata.schema.adjustment_schema import BarViewRequest
|
|
8
|
+
from pqdata.schema.bar_schema import BarFrequency
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InstrumentRangeQuery(BaseModel):
|
|
12
|
+
instrument_ids: list[str] = Field(min_length=1, max_length=10000)
|
|
13
|
+
start: date
|
|
14
|
+
end: date
|
|
15
|
+
|
|
16
|
+
@model_validator(mode="after")
|
|
17
|
+
def validate_range(self) -> "InstrumentRangeQuery":
|
|
18
|
+
if self.start > self.end:
|
|
19
|
+
raise ValueError("start must not be after end")
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BarWindowQuery(BaseModel):
|
|
24
|
+
instrument_id: str
|
|
25
|
+
frequency: BarFrequency
|
|
26
|
+
start: AwareDatetime | None = None
|
|
27
|
+
end: AwareDatetime | None = None
|
|
28
|
+
limit: int = Field(default=1000, ge=1, le=100000)
|
|
29
|
+
view: BarViewRequest | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PITWindowQuery(BaseModel):
|
|
33
|
+
instrument_id: str
|
|
34
|
+
days: list[date] = Field(min_length=1, max_length=2000)
|
|
35
|
+
next_days: list[date] = Field(min_length=1, max_length=2000)
|
|
36
|
+
lookback: int = Field(ge=1, le=2000)
|
|
37
|
+
|
|
38
|
+
@model_validator(mode="after")
|
|
39
|
+
def validate_days(self) -> "PITWindowQuery":
|
|
40
|
+
if len(self.days) != len(self.next_days):
|
|
41
|
+
raise ValueError("days and next_days must have equal length")
|
|
42
|
+
if self.days != sorted(set(self.days)):
|
|
43
|
+
raise ValueError("days must be strictly increasing")
|
|
44
|
+
if any(next_day <= day for day, next_day in zip(self.days, self.next_days, strict=True)):
|
|
45
|
+
raise ValueError("next_days must follow corresponding days")
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DataHealthQuery(BaseModel):
|
|
50
|
+
instrument_id: str
|
|
51
|
+
frequency: BarFrequency = "1d"
|
|
52
|
+
start: AwareDatetime
|
|
53
|
+
end: AwareDatetime
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Market Schema - Quote / DailyBasic / Sector 的响应模型
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class QuoteResponse(BaseModel):
|
|
12
|
+
"""最新行情快照响应"""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(from_attributes=True)
|
|
15
|
+
|
|
16
|
+
instrument_id: str = Field(..., description="Canonical标的ID")
|
|
17
|
+
timestamp: datetime = Field(..., description="快照时间(事件时间)")
|
|
18
|
+
last: Decimal = Field(..., description="最新价")
|
|
19
|
+
open: Decimal | None = Field(None, description="今日开盘价")
|
|
20
|
+
high: Decimal | None = Field(None, description="今日最高价")
|
|
21
|
+
low: Decimal | None = Field(None, description="今日最低价")
|
|
22
|
+
previous_close: Decimal | None = Field(None, description="昨收价")
|
|
23
|
+
volume: int = Field(..., description="累计成交量(share)")
|
|
24
|
+
amount: Decimal | None = Field(None, description="累计成交额(CNY)")
|
|
25
|
+
bid_price_1: Decimal | None = Field(None, description="买一价")
|
|
26
|
+
bid_volume_1: int | None = Field(None, description="买一量(share)")
|
|
27
|
+
ask_price_1: Decimal | None = Field(None, description="卖一价")
|
|
28
|
+
ask_volume_1: int | None = Field(None, description="卖一量(share)")
|
|
29
|
+
status: str | None = Field(None, description="交易状态")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DailyBasicResponse(BaseModel):
|
|
33
|
+
"""日频基础指标响应"""
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(from_attributes=True)
|
|
36
|
+
|
|
37
|
+
instrument_id: str = Field(..., description="Canonical标的ID")
|
|
38
|
+
trade_date: date = Field(..., description="交易日")
|
|
39
|
+
turnover_rate: Decimal | None = Field(None, description="换手率(%,1.23=1.23%)")
|
|
40
|
+
turnover_rate_free: Decimal | None = Field(None, description="自由流通换手率(%)")
|
|
41
|
+
volume_ratio: Decimal | None = Field(None, description="量比")
|
|
42
|
+
pe: Decimal | None = Field(None, description="市盈率(NULL=不可计算,如亏损)")
|
|
43
|
+
pe_ttm: Decimal | None = Field(None, description="市盈率TTM")
|
|
44
|
+
pb: Decimal | None = Field(None, description="市净率")
|
|
45
|
+
ps: Decimal | None = Field(None, description="市销率")
|
|
46
|
+
ps_ttm: Decimal | None = Field(None, description="市销率TTM")
|
|
47
|
+
dividend_yield: Decimal | None = Field(None, description="股息率(%)")
|
|
48
|
+
dividend_yield_ttm: Decimal | None = Field(None, description="股息率TTM(%)")
|
|
49
|
+
total_shares: Decimal | None = Field(None, description="总股本(share)")
|
|
50
|
+
float_shares: Decimal | None = Field(None, description="流通股本(share)")
|
|
51
|
+
free_float_shares: Decimal | None = Field(None, description="自由流通股本(share)")
|
|
52
|
+
total_market_cap: Decimal | None = Field(None, description="总市值(CNY)")
|
|
53
|
+
circulating_market_cap: Decimal | None = Field(None, description="流通市值(CNY)")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SectorResponse(BaseModel):
|
|
57
|
+
"""板块响应"""
|
|
58
|
+
|
|
59
|
+
model_config = ConfigDict(from_attributes=True)
|
|
60
|
+
|
|
61
|
+
sector_id: str = Field(..., description="Canonical板块ID")
|
|
62
|
+
name: str = Field(..., description="名称")
|
|
63
|
+
sector_type: str = Field(..., description="类型")
|
|
64
|
+
source: str = Field(..., description="分类体系来源")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SectorMembershipResponse(BaseModel):
|
|
68
|
+
"""标的-板块关系响应"""
|
|
69
|
+
|
|
70
|
+
instrument_id: str = Field(..., description="Canonical标的ID")
|
|
71
|
+
sector_id: str = Field(..., description="Canonical板块ID")
|
|
72
|
+
effective_from: date = Field(..., description="生效日期(含)")
|
|
73
|
+
effective_to: date | None = Field(None, description="失效日期(含);NULL=当前有效")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""标的、交易规则与日历的公开数据响应。"""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InstrumentResponse(BaseModel):
|
|
10
|
+
"""标的响应"""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(from_attributes=True)
|
|
13
|
+
instrument_id: str = Field(..., description="Canonical标的ID,如CN.XSHE.000001")
|
|
14
|
+
symbol: str = Field(..., description="交易代码")
|
|
15
|
+
name: str = Field(..., description="名称")
|
|
16
|
+
instrument_type: str = Field(..., description="标的类型")
|
|
17
|
+
exchange: str = Field(..., description="交易所")
|
|
18
|
+
board: str | None = Field(None, description="板块")
|
|
19
|
+
currency: str = Field(..., description="计价货币")
|
|
20
|
+
status: str = Field(..., description="状态")
|
|
21
|
+
listed_date: date | None = Field(None, description="上市日期")
|
|
22
|
+
delisted_date: date | None = Field(None, description="退市日期")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TradingRuleResponse(BaseModel):
|
|
26
|
+
"""某标的在一个业务日期区间内生效的交易规则。"""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(from_attributes=True, frozen=True)
|
|
29
|
+
instrument_id: str
|
|
30
|
+
effective_from: date
|
|
31
|
+
effective_to: date | None = None
|
|
32
|
+
min_buy_quantity: int = Field(..., gt=0)
|
|
33
|
+
buy_quantity_step: int = Field(..., gt=0)
|
|
34
|
+
min_sell_quantity: int = Field(..., gt=0)
|
|
35
|
+
sell_quantity_step: int = Field(..., gt=0)
|
|
36
|
+
tick_size: Decimal = Field(..., gt=0)
|
|
37
|
+
settlement_days: int = Field(..., ge=0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TradingCalendarDayResponse(BaseModel):
|
|
41
|
+
"""交易日历响应"""
|
|
42
|
+
|
|
43
|
+
model_config = ConfigDict(from_attributes=True)
|
|
44
|
+
exchange: str = Field(..., description="交易所")
|
|
45
|
+
trade_date: date = Field(..., description="日期")
|
|
46
|
+
is_open: bool = Field(..., description="是否交易日")
|
|
47
|
+
previous_open_date: date | None = Field(None, description="上一个交易日")
|
|
48
|
+
next_open_date: date | None = Field(None, description="下一个交易日")
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Market Data Reliability Schema - Sync 结果与 Dataset 健康契约
|
|
3
|
+
|
|
4
|
+
SSOT 语义见 SPEC「Market Data 可靠性」与 DATA-DICTIONARY §5。
|
|
5
|
+
核心原则:
|
|
6
|
+
- "同步成功" != Provider 请求成功;必须 = 转换成功 + 数据合法 + 无重复 +
|
|
7
|
+
目标区间完整 + 结果可追溯(MarketSyncResult 结构化表达)。
|
|
8
|
+
- Verify 只证明 "Observed Canonical Data 是否覆盖 Expected Timeline",
|
|
9
|
+
不负责猜测 Missing 的业务原因(停牌/零成交属 Trading Status,future)。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from datetime import date, datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SyncIssueCode(str, Enum):
|
|
20
|
+
"""结构化问题码(issues 摘要用,禁止只返回 "sync success" 文本)"""
|
|
21
|
+
|
|
22
|
+
INVALID_OHLC = "INVALID_OHLC"
|
|
23
|
+
NON_POSITIVE_PRICE = "NON_POSITIVE_PRICE"
|
|
24
|
+
NEGATIVE_VOLUME = "NEGATIVE_VOLUME"
|
|
25
|
+
NEGATIVE_AMOUNT = "NEGATIVE_AMOUNT"
|
|
26
|
+
INVALID_TIMESTAMP = "INVALID_TIMESTAMP"
|
|
27
|
+
INVALID_INSTRUMENT_ID = "INVALID_INSTRUMENT_ID"
|
|
28
|
+
INVALID_TRADING_DAY = "INVALID_TRADING_DAY" # 交易日语义违反 TradingCalendar
|
|
29
|
+
MISSING_BAR = "MISSING_BAR" # Expected Timeline 上的缺口(unexplained)
|
|
30
|
+
MISSING_ADJUSTMENT_FACTOR = "MISSING_ADJUSTMENT_FACTOR"
|
|
31
|
+
MISSING_DATASET_COVERAGE = "MISSING_DATASET_COVERAGE"
|
|
32
|
+
DUPLICATE_BAR = "DUPLICATE_BAR" # 同批内重复(唯一键冲突,跳过)
|
|
33
|
+
PROVIDER_ERROR = "PROVIDER_ERROR"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SyncStatus(str, Enum):
|
|
37
|
+
"""Sync 结果状态
|
|
38
|
+
|
|
39
|
+
SUCCESS = 目标同步区间成功且验证通过;
|
|
40
|
+
PARTIAL = 有数据写入,但存在缺口或非法数据;
|
|
41
|
+
FAILED = 同步任务无法形成可用结果。
|
|
42
|
+
不因为写入了一部分数据就返回 SUCCESS。
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
SUCCESS = "SUCCESS"
|
|
46
|
+
PARTIAL = "PARTIAL"
|
|
47
|
+
FAILED = "FAILED"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SyncIssue(BaseModel):
|
|
51
|
+
"""单条结构化问题摘要"""
|
|
52
|
+
|
|
53
|
+
code: SyncIssueCode
|
|
54
|
+
instrument_id: str | None = None
|
|
55
|
+
timestamp: datetime | None = None
|
|
56
|
+
detail: str | None = Field(None, description="补充说明(如失败原因原文)")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MarketSyncResult(BaseModel):
|
|
60
|
+
"""Sync 稳定结果对象(可追溯:请求区间 + 各阶段计数 + 状态 + 问题摘要)
|
|
61
|
+
|
|
62
|
+
字段按真实实现裁剪:V1 Provider 为 ON CONFLICT DO NOTHING 幂等 upsert,
|
|
63
|
+
updated_count 恒为 0(重复数据走 skipped)。
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
dataset: str
|
|
67
|
+
provider: str | None = None
|
|
68
|
+
instrument_id: str | None = Field(None, description="单标的同步时有值")
|
|
69
|
+
frequency: str | None = None
|
|
70
|
+
requested_start: date | None = None
|
|
71
|
+
requested_end: date | None = None
|
|
72
|
+
fetched_count: int = Field(0, description="Provider 返回条数")
|
|
73
|
+
normalized_count: int = Field(0, description="Mapper 转换成功条数")
|
|
74
|
+
inserted_count: int = Field(0, description="新插入条数")
|
|
75
|
+
updated_count: int = Field(0, description="更新条数(V1 幂等 upsert 恒为 0)")
|
|
76
|
+
skipped_count: int = Field(0, description="重复/无需写入条数")
|
|
77
|
+
invalid_count: int = Field(0, description="校验拒绝条数")
|
|
78
|
+
missing_count: int = Field(0, description="Completeness Check 发现的缺口数")
|
|
79
|
+
started_at: datetime
|
|
80
|
+
finished_at: datetime | None = None
|
|
81
|
+
status: SyncStatus = SyncStatus.FAILED
|
|
82
|
+
issues: list[SyncIssue] = Field(default_factory=list, description="结构化问题摘要(限量)")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class DatasetHealth(str, Enum):
|
|
86
|
+
"""Dataset 健康状态(Verify 产出)
|
|
87
|
+
|
|
88
|
+
HEALTHY = 请求区间完整且合法,可以消费;
|
|
89
|
+
PARTIAL = 有数据但目标区间存在缺口(unexplained missing,含疑似停牌日);
|
|
90
|
+
STALE = 结构正确且完整,但未更新到要求的新鲜度(V1 不产出,预留枚举位);
|
|
91
|
+
INVALID = 存在违反 Canonical 数据规则的数据;
|
|
92
|
+
NOT_SYNCED = 请求区间基本没有本地数据。
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
HEALTHY = "HEALTHY"
|
|
96
|
+
PARTIAL = "PARTIAL"
|
|
97
|
+
STALE = "STALE"
|
|
98
|
+
INVALID = "INVALID"
|
|
99
|
+
NOT_SYNCED = "NOT_SYNCED"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class DatasetHealthResult(BaseModel):
|
|
103
|
+
"""Verify 结果(只检查,绝不触发 Sync)
|
|
104
|
+
|
|
105
|
+
完整性验证单位:instrument_id + frequency + requested time range。
|
|
106
|
+
missing 明细只返回限量样本(missing_count 是权威计数)。
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
dataset: str
|
|
110
|
+
instrument_id: str
|
|
111
|
+
frequency: str
|
|
112
|
+
requested_start: datetime
|
|
113
|
+
requested_end: datetime
|
|
114
|
+
health: DatasetHealth
|
|
115
|
+
expected_count: int = Field(0, description="Expected Timeline 应有 Bar 数")
|
|
116
|
+
actual_count: int = Field(0, description="本地实际 Bar 数")
|
|
117
|
+
missing_count: int = 0
|
|
118
|
+
invalid_count: int = 0
|
|
119
|
+
latest_timestamp: datetime | None = None
|
|
120
|
+
dataset_revision: str | None = Field(
|
|
121
|
+
None, description="本次验证所见内容版本;必须进入缓存键/回测结果"
|
|
122
|
+
)
|
|
123
|
+
issues: list[SyncIssue] = Field(default_factory=list)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def issue_code_from_reason(reason: str) -> SyncIssueCode:
|
|
127
|
+
"""Validator 失败原因字符串 → 结构化问题码(未知原因归入 INVALID_OHLC)"""
|
|
128
|
+
mapping: dict[str, SyncIssueCode] = {
|
|
129
|
+
"high_lt_max_open_close": SyncIssueCode.INVALID_OHLC,
|
|
130
|
+
"low_gt_min_open_close": SyncIssueCode.INVALID_OHLC,
|
|
131
|
+
"non_positive_price": SyncIssueCode.NON_POSITIVE_PRICE,
|
|
132
|
+
"negative_volume": SyncIssueCode.NEGATIVE_VOLUME,
|
|
133
|
+
"negative_amount": SyncIssueCode.NEGATIVE_AMOUNT,
|
|
134
|
+
"naive_timestamp": SyncIssueCode.INVALID_TIMESTAMP,
|
|
135
|
+
"invalid_instrument_id": SyncIssueCode.INVALID_INSTRUMENT_ID,
|
|
136
|
+
}
|
|
137
|
+
return mapping.get(reason, SyncIssueCode.INVALID_OHLC)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def to_issue(reason: str, instrument_id: str, timestamp: datetime | None) -> SyncIssue:
|
|
141
|
+
"""Validator 失败原因 → SyncIssue 的统一入口"""
|
|
142
|
+
return SyncIssue(
|
|
143
|
+
code=issue_code_from_reason(reason),
|
|
144
|
+
instrument_id=instrument_id,
|
|
145
|
+
timestamp=timestamp,
|
|
146
|
+
detail=reason,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def health_payload(result: DatasetHealthResult) -> dict[str, Any]:
|
|
151
|
+
"""DatasetHealthResult 的可 JSON 化视图(API/日志复用)"""
|
|
152
|
+
return result.model_dump(mode="json")
|
quant_cli/__init__.py
ADDED
|
File without changes
|
quant_cli/api.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Stable local Python use cases; importing performs no I/O."""
|
|
2
|
+
|
|
3
|
+
from quant_core.app.factor.schema.local_factor_schema import LocalFactorRequest
|
|
4
|
+
|
|
5
|
+
from quant_cli.app.backtest.local_backtest_schema import LocalBacktestRequest
|
|
6
|
+
from quant_cli.app.backtest.service.local_backtest_service import run_local_backtest
|
|
7
|
+
from quant_cli.app.data.schema.dataset_request_schema import DatasetPrepareRequest
|
|
8
|
+
from quant_cli.app.data.service.dataset_load_service import load_dataset
|
|
9
|
+
from quant_cli.app.data.service.dataset_prepare_service import prepare_dataset
|
|
10
|
+
from quant_cli.app.factor.local_factor_dependency import (
|
|
11
|
+
build_local_factor_store,
|
|
12
|
+
build_local_selection_store,
|
|
13
|
+
)
|
|
14
|
+
from quant_cli.app.factor.schema.local_selection_schema import (
|
|
15
|
+
LocalSelectionFactor,
|
|
16
|
+
LocalSelectionRequest,
|
|
17
|
+
)
|
|
18
|
+
from quant_cli.app.factor.service.local_factor_service import compute_local_factor, run_local_factor
|
|
19
|
+
from quant_cli.app.factor.service.local_selection_service import (
|
|
20
|
+
compute_local_selection,
|
|
21
|
+
run_local_selection,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"build_local_factor_store",
|
|
26
|
+
"build_local_selection_store",
|
|
27
|
+
"DatasetPrepareRequest",
|
|
28
|
+
"LocalBacktestRequest",
|
|
29
|
+
"prepare_dataset",
|
|
30
|
+
"load_dataset",
|
|
31
|
+
"LocalFactorRequest",
|
|
32
|
+
"LocalSelectionFactor",
|
|
33
|
+
"LocalSelectionRequest",
|
|
34
|
+
"compute_local_factor",
|
|
35
|
+
"run_local_factor",
|
|
36
|
+
"compute_local_selection",
|
|
37
|
+
"run_local_selection",
|
|
38
|
+
"run_local_backtest",
|
|
39
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""本地策略回测命令;不加载管理数据库入口。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from quant_cli.app.backtest.local_backtest_dependency import build_local_backtest_store
|
|
11
|
+
from quant_cli.app.backtest.local_backtest_schema import LocalBacktestRequest
|
|
12
|
+
from quant_cli.app.backtest.service.local_backtest_service import run_local_backtest
|
|
13
|
+
from quant_cli.app.factor.local_factor_dependency import build_local_factor_store
|
|
14
|
+
from quant_cli.common.constants import DEFAULT_DATABASE_PATH
|
|
15
|
+
from quant_cli.common.utils.cli_utils import cli_errors, output
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(help="本地策略回测", no_args_is_help=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("run")
|
|
21
|
+
@cli_errors
|
|
22
|
+
def run(
|
|
23
|
+
config: Annotated[Path, typer.Option(exists=True)],
|
|
24
|
+
source: Annotated[Path | None, typer.Option(exists=True)] = None,
|
|
25
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
26
|
+
) -> None:
|
|
27
|
+
import json
|
|
28
|
+
|
|
29
|
+
payload = json.loads(config.read_text(encoding="utf-8"))
|
|
30
|
+
if source is not None:
|
|
31
|
+
payload["source"] = source.read_text(encoding="utf-8")
|
|
32
|
+
request = LocalBacktestRequest.model_validate(payload)
|
|
33
|
+
record = asyncio.run(run_local_backtest(request, build_local_factor_store(database)))
|
|
34
|
+
build_local_backtest_store(database).save(record)
|
|
35
|
+
output(record)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("get")
|
|
39
|
+
@cli_errors
|
|
40
|
+
def get(
|
|
41
|
+
run: Annotated[UUID, typer.Option()],
|
|
42
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
43
|
+
) -> None:
|
|
44
|
+
output(build_local_backtest_store(database).get(run))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command("list")
|
|
48
|
+
@cli_errors
|
|
49
|
+
def list_runs(
|
|
50
|
+
limit: int = 20,
|
|
51
|
+
offset: int = 0,
|
|
52
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
53
|
+
) -> None:
|
|
54
|
+
output(
|
|
55
|
+
[
|
|
56
|
+
row.model_dump(mode="json")
|
|
57
|
+
for row in build_local_backtest_store(database).list_runs(limit, offset)
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command("compare")
|
|
63
|
+
@cli_errors
|
|
64
|
+
def compare(
|
|
65
|
+
run: Annotated[list[UUID], typer.Option()],
|
|
66
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""比较多次本地回测。"""
|
|
69
|
+
from quant_cli.app.backtest.service.local_backtest_service import compare_backtest_records
|
|
70
|
+
|
|
71
|
+
store = build_local_backtest_store(database)
|
|
72
|
+
output(compare_backtest_records(tuple(store.get(key) for key in run)))
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""本地回测归档组装。"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from quant_cli.app.backtest.repository.backtest_local_repository import BacktestLocalRepository
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_local_backtest_store(path: Path) -> BacktestLocalRepository:
|
|
9
|
+
"""返回本地回测存取。"""
|
|
10
|
+
return BacktestLocalRepository(path)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""本地回测输入,策略源码与数据快照显式提供。"""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
8
|
+
from quant_core.app.backtest.schema import BacktestConfig
|
|
9
|
+
from quant_core.app.data.schema.adjustment_schema import BarViewRequest
|
|
10
|
+
from quant_core.app.strategy.schema.strategy_publication_schema import PublishedStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LocalBacktestRequest(BaseModel):
|
|
14
|
+
model_config = ConfigDict(extra="forbid")
|
|
15
|
+
config: BacktestConfig
|
|
16
|
+
source: str = ""
|
|
17
|
+
publication: PublishedStrategy | None = None
|
|
18
|
+
factor_view: BarViewRequest = Field(default_factory=BarViewRequest)
|
|
19
|
+
exclude_st: bool = False
|
|
20
|
+
manifest_path: Path
|
|
21
|
+
parameters: dict = Field(default_factory=dict)
|
|
22
|
+
factor_run_uuid: UUID | None = None
|
|
23
|
+
factor_artifact_path: Path | None = None
|
|
24
|
+
slippage_bps: Decimal = Field(default=Decimal(0), ge=0, allow_inf_nan=False)
|
|
25
|
+
max_rows: int = Field(default=1000000, ge=1)
|
|
26
|
+
|
|
27
|
+
@model_validator(mode="after")
|
|
28
|
+
def one_factor_source(self) -> "LocalBacktestRequest":
|
|
29
|
+
if bool(self.source) == (self.publication is not None):
|
|
30
|
+
raise ValueError("必须且只能提供本地策略源码或已发布策略")
|
|
31
|
+
if self.publication is not None:
|
|
32
|
+
spec = self.publication.definition
|
|
33
|
+
if spec.strategy_id != self.config.strategy_id:
|
|
34
|
+
raise ValueError("回测策略 ID 与发布定义不同")
|
|
35
|
+
if self.parameters and self.parameters != spec.parameters:
|
|
36
|
+
raise ValueError("发布策略参数不可静默覆盖,请发布新版本")
|
|
37
|
+
if spec.universe and set(spec.universe) != set(self.config.instrument_ids):
|
|
38
|
+
raise ValueError("股票池与发布策略不同")
|
|
39
|
+
if not spec.factor_refs and (self.factor_run_uuid or self.factor_artifact_path):
|
|
40
|
+
raise ValueError("发布策略未声明因子依赖,不能注入因子结果")
|
|
41
|
+
if self.factor_run_uuid and self.factor_artifact_path:
|
|
42
|
+
raise ValueError("因子运行和独立产物只能指定一种")
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
@field_validator("manifest_path")
|
|
46
|
+
@classmethod
|
|
47
|
+
def absolute_manifest(cls, value: Path) -> Path:
|
|
48
|
+
"""归档固定路径,避免重启后工作目录变化影响定位。"""
|
|
49
|
+
return value.expanduser().resolve()
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""backtest 本地运行存取;短事务由本地连接上下文管理。"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from quant_core.common.local_run import LocalRunRecord
|
|
7
|
+
|
|
8
|
+
from quant_cli.common.database.duckdb_connection import local_transaction
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BacktestLocalRepository:
|
|
12
|
+
"""只维护本领域的运行记录,查询使用数据库分页。"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, path: Path) -> None:
|
|
15
|
+
self.path = path
|
|
16
|
+
|
|
17
|
+
def save(self, record: LocalRunRecord) -> None:
|
|
18
|
+
with local_transaction(self.path) as connection:
|
|
19
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS backtest_local_run (
|
|
20
|
+
id BIGINT PRIMARY KEY, uuid UUID UNIQUE NOT NULL,
|
|
21
|
+
created_at TIMESTAMPTZ NOT NULL, document JSON NOT NULL)""")
|
|
22
|
+
existing = connection.execute(
|
|
23
|
+
"SELECT document FROM backtest_local_run WHERE uuid = ?", [str(record.uuid)]
|
|
24
|
+
).fetchone()
|
|
25
|
+
if existing is not None:
|
|
26
|
+
if LocalRunRecord.model_validate_json(existing[0]) != record:
|
|
27
|
+
raise ValueError("同一运行 UUID 的记录不可覆盖")
|
|
28
|
+
return
|
|
29
|
+
connection.execute(
|
|
30
|
+
"""INSERT INTO backtest_local_run
|
|
31
|
+
SELECT COALESCE(MAX(id), 0) + 1, ?, ?, ? FROM backtest_local_run""",
|
|
32
|
+
[str(record.uuid), record.created_at, record.model_dump_json()],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def get(self, run_uuid: UUID) -> LocalRunRecord:
|
|
36
|
+
if not self.path.is_file():
|
|
37
|
+
raise ValueError("运行记录不存在")
|
|
38
|
+
with local_transaction(self.path, read_only=True) as connection:
|
|
39
|
+
if not connection.execute(
|
|
40
|
+
"SELECT 1 FROM information_schema.tables WHERE table_name = ?",
|
|
41
|
+
["backtest_local_run"],
|
|
42
|
+
).fetchall():
|
|
43
|
+
raise ValueError("运行记录不存在")
|
|
44
|
+
row = connection.execute(
|
|
45
|
+
"SELECT document FROM backtest_local_run WHERE uuid = ?", [str(run_uuid)]
|
|
46
|
+
).fetchone()
|
|
47
|
+
if row is None:
|
|
48
|
+
raise ValueError("运行记录不存在")
|
|
49
|
+
return LocalRunRecord.model_validate_json(row[0])
|
|
50
|
+
|
|
51
|
+
def list_runs(self, limit: int = 20, offset: int = 0) -> list[LocalRunRecord]:
|
|
52
|
+
if not 1 <= limit <= 1000 or offset < 0:
|
|
53
|
+
raise ValueError("分页参数非法")
|
|
54
|
+
if not self.path.is_file():
|
|
55
|
+
return []
|
|
56
|
+
with local_transaction(self.path, read_only=True) as connection:
|
|
57
|
+
if not connection.execute(
|
|
58
|
+
"SELECT 1 FROM information_schema.tables WHERE table_name = ?",
|
|
59
|
+
["backtest_local_run"],
|
|
60
|
+
).fetchall():
|
|
61
|
+
return []
|
|
62
|
+
rows = connection.execute(
|
|
63
|
+
"""SELECT document FROM backtest_local_run
|
|
64
|
+
ORDER BY id DESC LIMIT ? OFFSET ?""",
|
|
65
|
+
[limit, offset],
|
|
66
|
+
).fetchall()
|
|
67
|
+
return [LocalRunRecord.model_validate_json(row[0]) for row in rows]
|
|
File without changes
|