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,187 @@
|
|
|
1
|
+
from quant_core.app.data.schema.history_schema import HistoryFacts
|
|
2
|
+
|
|
3
|
+
"""通过 SDK 准备固定版本的本地回放数据;完成后关闭网络客户端。"""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime, time, timedelta
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pqdata import MarketClient
|
|
12
|
+
from pqdata.schema.bar_schema import BarFrequency, BarQuery
|
|
13
|
+
from pqdata.schema.history_schema import HistoryFactsQuery
|
|
14
|
+
from pydantic import TypeAdapter
|
|
15
|
+
from quant_core.app.data.schema.adjustment_schema import AdjustmentType
|
|
16
|
+
from quant_core.app.data.schema.market_schema import BarResponse, InstrumentResponse
|
|
17
|
+
from quant_core.app.data.service.prepared_data_verification_service import PreparedDataVerification
|
|
18
|
+
from quant_core.app.data.service.prepared_market_service import PreparedMarketData
|
|
19
|
+
|
|
20
|
+
from quant_cli.app.data.repository.parquet_repository import atomic_json, write_parquet
|
|
21
|
+
from quant_cli.app.data.service.bar_cache_service import (
|
|
22
|
+
BarCacheQuery,
|
|
23
|
+
bars_frame,
|
|
24
|
+
load_bar_snapshot,
|
|
25
|
+
prepare_bar_cache,
|
|
26
|
+
)
|
|
27
|
+
from quant_cli.app.data.service.dataset_load_service import load_dataset, manifest_digest
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class PreparedDataset:
|
|
32
|
+
market: PreparedMarketData
|
|
33
|
+
verification: PreparedDataVerification
|
|
34
|
+
manifest_path: Path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def prepare_dataset(
|
|
38
|
+
client: MarketClient,
|
|
39
|
+
*,
|
|
40
|
+
instrument_ids: tuple[str, ...],
|
|
41
|
+
frequency: str,
|
|
42
|
+
execution_frequency: str,
|
|
43
|
+
start: datetime,
|
|
44
|
+
end: datetime,
|
|
45
|
+
adjustment_type: AdjustmentType,
|
|
46
|
+
cache_dir: Path,
|
|
47
|
+
lookback_days: int,
|
|
48
|
+
max_rows: int,
|
|
49
|
+
revision: str | None = None,
|
|
50
|
+
) -> PreparedDataset:
|
|
51
|
+
"""先固定事实版本,再校验并下载;版本冲突或超出容量时直接失败。"""
|
|
52
|
+
history_start = start - timedelta(days=lookback_days)
|
|
53
|
+
first = await client.get_history_facts(
|
|
54
|
+
HistoryFactsQuery(
|
|
55
|
+
instrument_id=instrument_ids[0],
|
|
56
|
+
start=history_start.date(),
|
|
57
|
+
end=end.date(),
|
|
58
|
+
revision=revision,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
market = PreparedMarketData(history_start, end, first.revision)
|
|
62
|
+
instruments = await client.search_instruments(limit=10000)
|
|
63
|
+
if len(instruments) >= 10000:
|
|
64
|
+
raise ValueError("标的目录达到单次读取上限,无法证明目录完整")
|
|
65
|
+
market.instruments = [InstrumentResponse.model_validate(row) for row in instruments]
|
|
66
|
+
verification = PreparedDataVerification()
|
|
67
|
+
cache_key = hashlib.sha256(
|
|
68
|
+
json.dumps(
|
|
69
|
+
{
|
|
70
|
+
"format_version": 2,
|
|
71
|
+
"source": client.source_id,
|
|
72
|
+
"revision": first.revision,
|
|
73
|
+
"instruments": sorted(instrument_ids),
|
|
74
|
+
"frequency": frequency,
|
|
75
|
+
"execution_frequency": execution_frequency,
|
|
76
|
+
"start": start.isoformat(),
|
|
77
|
+
"end": end.isoformat(),
|
|
78
|
+
"lookback_days": lookback_days,
|
|
79
|
+
"adjustment_type": adjustment_type.value,
|
|
80
|
+
},
|
|
81
|
+
sort_keys=True,
|
|
82
|
+
).encode()
|
|
83
|
+
).hexdigest()
|
|
84
|
+
directory = cache_dir / cache_key
|
|
85
|
+
manifest_path = directory / "manifest.json"
|
|
86
|
+
if manifest_path.exists():
|
|
87
|
+
prepared, evidence = load_dataset(manifest_path, max_rows)
|
|
88
|
+
return PreparedDataset(prepared, evidence, manifest_path)
|
|
89
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
manifest = {
|
|
91
|
+
"format_version": 2,
|
|
92
|
+
"source": client.source_id,
|
|
93
|
+
"frequencies": sorted({frequency, execution_frequency}),
|
|
94
|
+
"revision": first.revision,
|
|
95
|
+
"start": history_start.isoformat(),
|
|
96
|
+
"end": end.isoformat(),
|
|
97
|
+
"files": [],
|
|
98
|
+
"facts": {},
|
|
99
|
+
"health": [],
|
|
100
|
+
}
|
|
101
|
+
total_rows = 0
|
|
102
|
+
cached_bars = {}
|
|
103
|
+
for value in sorted({frequency, execution_frequency}):
|
|
104
|
+
path = await prepare_bar_cache(
|
|
105
|
+
client,
|
|
106
|
+
BarCacheQuery(
|
|
107
|
+
instrument_ids=list(instrument_ids),
|
|
108
|
+
frequency=TypeAdapter(BarFrequency).validate_python(value),
|
|
109
|
+
start=history_start,
|
|
110
|
+
end=end,
|
|
111
|
+
revision=first.revision,
|
|
112
|
+
),
|
|
113
|
+
cache_dir,
|
|
114
|
+
max_rows,
|
|
115
|
+
)
|
|
116
|
+
_, rows = load_bar_snapshot(path, max_rows)
|
|
117
|
+
cached_bars[value] = rows
|
|
118
|
+
total_rows += len(rows)
|
|
119
|
+
if total_rows > max_rows:
|
|
120
|
+
raise ValueError("回放数据超过配置行数上限")
|
|
121
|
+
for instrument_id in instrument_ids:
|
|
122
|
+
facts = await client.get_history_facts(
|
|
123
|
+
HistoryFactsQuery(
|
|
124
|
+
instrument_id=instrument_id,
|
|
125
|
+
start=history_start.date(),
|
|
126
|
+
end=end.date(),
|
|
127
|
+
revision=first.revision,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
market.facts[instrument_id] = HistoryFacts.model_validate(facts.model_dump())
|
|
131
|
+
manifest["facts"][instrument_id] = facts.model_dump(mode="json")
|
|
132
|
+
verification_start = start
|
|
133
|
+
if facts.instrument.listed_date is not None and facts.instrument.listed_date > start.date():
|
|
134
|
+
verification_start = datetime.combine(
|
|
135
|
+
facts.instrument.listed_date, time(9, 30), tzinfo=start.tzinfo
|
|
136
|
+
)
|
|
137
|
+
if verification_start <= end:
|
|
138
|
+
for value in sorted({frequency, execution_frequency}):
|
|
139
|
+
result = await client.verify_bars(instrument_id, value, verification_start, end)
|
|
140
|
+
verification.add("bar", instrument_id, value, verification_start, end, result)
|
|
141
|
+
manifest["health"].append(result.model_dump(mode="json"))
|
|
142
|
+
if adjustment_type is not AdjustmentType.NONE:
|
|
143
|
+
result = await client.verify_adjustment_factors(
|
|
144
|
+
instrument_id, frequency, verification_start, end
|
|
145
|
+
)
|
|
146
|
+
verification.add(
|
|
147
|
+
"adjustment_factor", instrument_id, frequency, verification_start, end, result
|
|
148
|
+
)
|
|
149
|
+
manifest["health"].append(result.model_dump(mode="json"))
|
|
150
|
+
result = await client.verify_corporate_actions(instrument_id, verification_start, end)
|
|
151
|
+
verification.add(
|
|
152
|
+
"corporate_action", instrument_id, "1d", verification_start, end, result
|
|
153
|
+
)
|
|
154
|
+
manifest["health"].append(result.model_dump(mode="json"))
|
|
155
|
+
for value in sorted({frequency, execution_frequency}):
|
|
156
|
+
query = BarQuery(
|
|
157
|
+
instrument_ids=[instrument_id],
|
|
158
|
+
frequency=TypeAdapter(BarFrequency).validate_python(value),
|
|
159
|
+
start=history_start,
|
|
160
|
+
end=end,
|
|
161
|
+
revision=first.revision,
|
|
162
|
+
)
|
|
163
|
+
selected = [row for row in cached_bars[value] if row.instrument_id == instrument_id]
|
|
164
|
+
receipt = write_parquet(
|
|
165
|
+
directory,
|
|
166
|
+
bars_frame(selected),
|
|
167
|
+
dataset_id="bar.raw",
|
|
168
|
+
primary_key=("timestamp", "instrument_id", "frequency"),
|
|
169
|
+
)
|
|
170
|
+
bars = [BarResponse.model_validate(row) for row in selected]
|
|
171
|
+
market.add_bars(instrument_id, value, bars)
|
|
172
|
+
manifest["files"].append(
|
|
173
|
+
{"query": query.model_dump(mode="json"), **receipt.model_dump(mode="json")}
|
|
174
|
+
)
|
|
175
|
+
# 校验元数据、覆盖校验和最后一页之间没有并发提交。
|
|
176
|
+
await client.get_history_facts(
|
|
177
|
+
HistoryFactsQuery(
|
|
178
|
+
instrument_id=instrument_ids[0],
|
|
179
|
+
start=history_start.date(),
|
|
180
|
+
end=end.date(),
|
|
181
|
+
revision=first.revision,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
manifest["instruments"] = [row.model_dump(mode="json") for row in market.instruments]
|
|
185
|
+
manifest["content_digest"] = manifest_digest(manifest)
|
|
186
|
+
atomic_json(manifest_path, manifest)
|
|
187
|
+
return PreparedDataset(market, verification, manifest_path)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""因子定义发布和下载命令;与本地运行记录命令分组。"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from quant_core.app.factor.schema.factor_computation_schema import FactorComputationSpec
|
|
10
|
+
from quant_core.app.factor.schema.factor_publication_schema import PublishedFactor
|
|
11
|
+
|
|
12
|
+
from quant_cli.common.http.definition_client import request_definition, save_definition
|
|
13
|
+
from quant_cli.common.utils.cli_utils import cli_errors, output
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="因子定义发布、查询与获取", no_args_is_help=True)
|
|
16
|
+
Server = Annotated[str, typer.Option(envvar="PQ_QUANT_URL")]
|
|
17
|
+
Token = Annotated[str, typer.Option(envvar="PQ_QUANT_TOKEN", help="建议由环境变量提供令牌")]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("publish")
|
|
21
|
+
@cli_errors
|
|
22
|
+
def publish(
|
|
23
|
+
config: Annotated[Path, typer.Option(exists=True)],
|
|
24
|
+
server: Server,
|
|
25
|
+
token: Token,
|
|
26
|
+
source: Annotated[Path | None, typer.Option(exists=True)] = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""发布 JSON 定义,支持从 Python 文件读取源码,不执行它。"""
|
|
29
|
+
payload = json.loads(config.read_text(encoding="utf-8"))
|
|
30
|
+
if source is not None:
|
|
31
|
+
payload["python_source"] = source.read_text(encoding="utf-8")
|
|
32
|
+
spec = FactorComputationSpec.model_validate(payload)
|
|
33
|
+
result = PublishedFactor.model_validate(
|
|
34
|
+
request_definition(
|
|
35
|
+
server, token, "POST", "/factor/definitions", payload=spec.model_dump(mode="json")
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
output(result.model_dump(mode="json"))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("list")
|
|
42
|
+
@cli_errors
|
|
43
|
+
def list_definitions(
|
|
44
|
+
server: Server, token: Token, name: str | None = None, limit: int = 100, offset: int = 0
|
|
45
|
+
) -> None:
|
|
46
|
+
"""分页列出发布定义。"""
|
|
47
|
+
params: dict[str, str | int] = {"limit": limit, "offset": offset}
|
|
48
|
+
if name is not None:
|
|
49
|
+
params["name"] = name
|
|
50
|
+
output(request_definition(server, token, "GET", "/factor/definitions", params=params))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("fetch")
|
|
54
|
+
@cli_errors
|
|
55
|
+
def fetch(
|
|
56
|
+
name: str,
|
|
57
|
+
server: Server,
|
|
58
|
+
token: Token,
|
|
59
|
+
destination: Annotated[Path, typer.Option()],
|
|
60
|
+
version: str = "latest",
|
|
61
|
+
source: str = "local-python",
|
|
62
|
+
frequency: str = "1d",
|
|
63
|
+
) -> None:
|
|
64
|
+
"""获取并校验完整身份的定义文件,latest 在此固定。"""
|
|
65
|
+
result = PublishedFactor.model_validate(
|
|
66
|
+
request_definition(
|
|
67
|
+
server,
|
|
68
|
+
token,
|
|
69
|
+
"GET",
|
|
70
|
+
f"/factor/definitions/{quote(name, safe='')}/{quote(version, safe='')}",
|
|
71
|
+
params={"source": source, "frequency": frequency},
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
save_definition(destination, result.model_dump(mode="json"))
|
|
75
|
+
output(
|
|
76
|
+
{
|
|
77
|
+
"path": str(destination.resolve()),
|
|
78
|
+
"identity": result.definition.identity.model_dump(mode="json"),
|
|
79
|
+
"content_digest": result.content_digest,
|
|
80
|
+
}
|
|
81
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
from quant_core.app.factor.schema.local_factor_schema import LocalFactorRequest
|
|
10
|
+
|
|
11
|
+
from quant_cli.app.factor.cli.factor_definition_cli import app as definitions
|
|
12
|
+
from quant_cli.app.factor.cli.local_selection_cli import register_selection_commands
|
|
13
|
+
from quant_cli.app.factor.local_factor_dependency import build_local_factor_store
|
|
14
|
+
from quant_cli.app.factor.service.local_factor_service import run_local_factor
|
|
15
|
+
from quant_cli.common.constants import DEFAULT_DATABASE_PATH
|
|
16
|
+
from quant_cli.common.utils.cli_utils import cli_errors, output
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="本地因子计算与归档", no_args_is_help=True)
|
|
19
|
+
|
|
20
|
+
app.add_typer(definitions, name="definition")
|
|
21
|
+
register_selection_commands(app)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command("compute")
|
|
25
|
+
@cli_errors
|
|
26
|
+
def compute(
|
|
27
|
+
config: Annotated[Path, typer.Option(exists=True)],
|
|
28
|
+
source: Annotated[Path | None, typer.Option(exists=True)] = None,
|
|
29
|
+
definition: Annotated[list[Path] | None, typer.Option(exists=True)] = None,
|
|
30
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""从 JSON 请求和源码或下载定义执行本地因子,并默认归档。"""
|
|
33
|
+
import json
|
|
34
|
+
|
|
35
|
+
payload = json.loads(config.read_text(encoding="utf-8"))
|
|
36
|
+
if source is not None:
|
|
37
|
+
payload["source"] = source.read_text(encoding="utf-8")
|
|
38
|
+
if definition:
|
|
39
|
+
from quant_core.app.factor.schema.factor_publication_schema import PublishedFactor
|
|
40
|
+
|
|
41
|
+
if payload.get("source") or payload.get("definitions"):
|
|
42
|
+
raise ValueError("发布定义与源码/内嵌定义不能同时提供")
|
|
43
|
+
payload["definitions"] = [
|
|
44
|
+
PublishedFactor.model_validate_json(path.read_text(encoding="utf-8")).model_dump(
|
|
45
|
+
mode="json"
|
|
46
|
+
)
|
|
47
|
+
for path in definition
|
|
48
|
+
]
|
|
49
|
+
request = LocalFactorRequest.model_validate(payload)
|
|
50
|
+
output(asyncio.run(run_local_factor(request, build_local_factor_store(database))))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("get")
|
|
54
|
+
@cli_errors
|
|
55
|
+
def get(
|
|
56
|
+
run: Annotated[UUID, typer.Option()],
|
|
57
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
58
|
+
) -> None:
|
|
59
|
+
output(build_local_factor_store(database).get(run))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command("list")
|
|
63
|
+
@cli_errors
|
|
64
|
+
def list_runs(
|
|
65
|
+
limit: int = 20,
|
|
66
|
+
offset: int = 0,
|
|
67
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
68
|
+
) -> None:
|
|
69
|
+
output(
|
|
70
|
+
[
|
|
71
|
+
row.model_dump(mode="json")
|
|
72
|
+
for row in build_local_factor_store(database).list_runs(limit, offset)
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command("import")
|
|
78
|
+
@cli_errors
|
|
79
|
+
def import_artifact(
|
|
80
|
+
artifact: Annotated[Path, typer.Option(exists=True)],
|
|
81
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""由单一归档入口导入独立 Worker 产物。"""
|
|
84
|
+
from quant_cli.app.factor.service.local_factor_service import import_factor_artifact
|
|
85
|
+
|
|
86
|
+
output(
|
|
87
|
+
import_factor_artifact(
|
|
88
|
+
artifact.read_text(encoding="utf-8"), build_local_factor_store(database)
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command("values")
|
|
94
|
+
@cli_errors
|
|
95
|
+
def values(
|
|
96
|
+
run: Annotated[UUID, typer.Option()],
|
|
97
|
+
limit: int = 100,
|
|
98
|
+
offset: int = 0,
|
|
99
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""分页查询归档因子值。"""
|
|
102
|
+
output(build_local_factor_store(database).list_values(run, limit, offset))
|
|
@@ -0,0 +1,58 @@
|
|
|
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.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 LocalSelectionRequest
|
|
15
|
+
from quant_cli.app.factor.service.local_selection_service import run_local_selection
|
|
16
|
+
from quant_cli.common.constants import DEFAULT_DATABASE_PATH
|
|
17
|
+
from quant_cli.common.utils.cli_utils import cli_errors, output
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def register_selection_commands(app: typer.Typer) -> None:
|
|
21
|
+
@app.command("select")
|
|
22
|
+
@cli_errors
|
|
23
|
+
def select(
|
|
24
|
+
config: Annotated[Path, typer.Option(exists=True)],
|
|
25
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
26
|
+
) -> None:
|
|
27
|
+
request = LocalSelectionRequest.model_validate_json(config.read_text(encoding="utf-8"))
|
|
28
|
+
output(
|
|
29
|
+
asyncio.run(
|
|
30
|
+
run_local_selection(
|
|
31
|
+
request,
|
|
32
|
+
build_local_factor_store(database),
|
|
33
|
+
build_local_selection_store(database),
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@app.command("selection-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_selection_store(database).get(run))
|
|
45
|
+
|
|
46
|
+
@app.command("selection-list")
|
|
47
|
+
@cli_errors
|
|
48
|
+
def list_runs(
|
|
49
|
+
limit: int = 20,
|
|
50
|
+
offset: int = 0,
|
|
51
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
52
|
+
) -> None:
|
|
53
|
+
output(
|
|
54
|
+
[
|
|
55
|
+
row.model_dump(mode="json")
|
|
56
|
+
for row in build_local_selection_store(database).list_runs(limit, offset)
|
|
57
|
+
]
|
|
58
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""本地选股运行存取协议。"""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from quant_core.common.local_run import LocalRunRecord
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LocalSelectionStore(Protocol):
|
|
10
|
+
def save(self, record: LocalRunRecord) -> None: ...
|
|
11
|
+
def get(self, run_uuid: UUID) -> LocalRunRecord: ...
|
|
12
|
+
def list_runs(self, limit: int = 20, offset: int = 0) -> list[LocalRunRecord]: ...
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""本地因子组装,不加载远端存储实现。"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from quant_core.app.factor.interface.public.factor_local import LocalFactorStore
|
|
6
|
+
|
|
7
|
+
from quant_cli.app.factor.interface.local_selection_store import LocalSelectionStore
|
|
8
|
+
from quant_cli.app.factor.repository.factor_local_repository import FactorLocalRepository
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_local_factor_store(path: Path) -> LocalFactorStore:
|
|
12
|
+
"""返回本地因子结果存取。"""
|
|
13
|
+
return FactorLocalRepository(path)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# 公开函数入口不包一层重复转发;内部实现只由本域组装模块暴露。
|
|
17
|
+
from quant_cli.app.factor.service.local_factor_query_service import (
|
|
18
|
+
build_local_factor_query as build_local_factor_query,
|
|
19
|
+
)
|
|
20
|
+
from quant_cli.app.factor.service.local_factor_service import (
|
|
21
|
+
compute_local_factor as compute_local_factor,
|
|
22
|
+
)
|
|
23
|
+
from quant_cli.app.factor.service.local_factor_service import (
|
|
24
|
+
decode_factor_artifact as decode_factor_artifact,
|
|
25
|
+
)
|
|
26
|
+
from quant_cli.app.factor.service.local_factor_service import (
|
|
27
|
+
import_factor_artifact as import_factor_artifact,
|
|
28
|
+
)
|
|
29
|
+
from quant_cli.app.factor.service.local_factor_service import (
|
|
30
|
+
load_factor_result as load_factor_result,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_local_selection_store(path: Path) -> LocalSelectionStore:
|
|
35
|
+
"""按显式路径组装选股归档能力。"""
|
|
36
|
+
from quant_cli.app.factor.repository.selection_local_repository import SelectionLocalRepository
|
|
37
|
+
|
|
38
|
+
return SelectionLocalRepository(path)
|
|
File without changes
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""factor 本地运行存取;短事务由本地连接上下文管理。"""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from quant_core.common.local_run import LocalRunRecord
|
|
8
|
+
|
|
9
|
+
from quant_cli.common.database.duckdb_connection import local_transaction
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FactorLocalRepository:
|
|
13
|
+
"""只维护本领域的运行记录,查询使用数据库分页。"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, path: Path) -> None:
|
|
16
|
+
self.path = path
|
|
17
|
+
|
|
18
|
+
def save(self, record: LocalRunRecord, values: dict | None = None) -> None:
|
|
19
|
+
with local_transaction(self.path) as connection:
|
|
20
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS factor_local_run (
|
|
21
|
+
id BIGINT PRIMARY KEY, uuid UUID UNIQUE NOT NULL,
|
|
22
|
+
created_at TIMESTAMPTZ NOT NULL, document JSON NOT NULL)""")
|
|
23
|
+
existing = connection.execute(
|
|
24
|
+
"SELECT document FROM factor_local_run WHERE uuid = ?", [str(record.uuid)]
|
|
25
|
+
).fetchone()
|
|
26
|
+
if existing is not None:
|
|
27
|
+
if LocalRunRecord.model_validate_json(existing[0]) != record:
|
|
28
|
+
raise ValueError("同一运行 UUID 的记录不可覆盖")
|
|
29
|
+
from decimal import Decimal
|
|
30
|
+
|
|
31
|
+
stored = connection.execute(
|
|
32
|
+
"""SELECT name, as_of AT TIME ZONE 'UTC', instrument_id, value
|
|
33
|
+
FROM factor_local_value AS v JOIN factor_local_run AS r ON r.id = v.run_id
|
|
34
|
+
WHERE r.uuid = ?""",
|
|
35
|
+
[str(record.uuid)],
|
|
36
|
+
).fetchall()
|
|
37
|
+
actual = {
|
|
38
|
+
(name, timestamp.replace(tzinfo=UTC), instrument): Decimal(value)
|
|
39
|
+
if value is not None
|
|
40
|
+
else None
|
|
41
|
+
for name, timestamp, instrument, value in stored
|
|
42
|
+
}
|
|
43
|
+
expected = {
|
|
44
|
+
(name, timestamp, instrument): value
|
|
45
|
+
for name, cells in (values or {}).items()
|
|
46
|
+
for (timestamp, instrument), value in cells.items()
|
|
47
|
+
}
|
|
48
|
+
if actual != expected:
|
|
49
|
+
raise ValueError("同一运行 UUID 的因子值不可覆盖")
|
|
50
|
+
return
|
|
51
|
+
connection.execute(
|
|
52
|
+
"""INSERT INTO factor_local_run
|
|
53
|
+
SELECT COALESCE(MAX(id), 0) + 1, ?, ?, ? FROM factor_local_run""",
|
|
54
|
+
[str(record.uuid), record.created_at, record.model_dump_json()],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS factor_local_value (
|
|
58
|
+
run_id BIGINT NOT NULL REFERENCES factor_local_run(id),
|
|
59
|
+
name VARCHAR NOT NULL, as_of TIMESTAMPTZ NOT NULL,
|
|
60
|
+
instrument_id VARCHAR NOT NULL, value VARCHAR,
|
|
61
|
+
PRIMARY KEY (run_id, name, as_of, instrument_id))""")
|
|
62
|
+
identity = connection.execute(
|
|
63
|
+
"SELECT id FROM factor_local_run WHERE uuid = ?", [str(record.uuid)]
|
|
64
|
+
).fetchone()
|
|
65
|
+
assert identity is not None
|
|
66
|
+
if values:
|
|
67
|
+
connection.executemany(
|
|
68
|
+
"INSERT INTO factor_local_value VALUES (?, ?, ?, ?, ?)",
|
|
69
|
+
[
|
|
70
|
+
(
|
|
71
|
+
identity[0],
|
|
72
|
+
name,
|
|
73
|
+
timestamp,
|
|
74
|
+
instrument,
|
|
75
|
+
None if value is None else str(value),
|
|
76
|
+
)
|
|
77
|
+
for name, cells in values.items()
|
|
78
|
+
for (timestamp, instrument), value in cells.items()
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def get(self, run_uuid: UUID) -> LocalRunRecord:
|
|
83
|
+
if not self.path.is_file():
|
|
84
|
+
raise ValueError("运行记录不存在")
|
|
85
|
+
with local_transaction(self.path, read_only=True) as connection:
|
|
86
|
+
if not connection.execute(
|
|
87
|
+
"SELECT 1 FROM information_schema.tables WHERE table_name = ?",
|
|
88
|
+
["factor_local_run"],
|
|
89
|
+
).fetchall():
|
|
90
|
+
raise ValueError("运行记录不存在")
|
|
91
|
+
row = connection.execute(
|
|
92
|
+
"SELECT document FROM factor_local_run WHERE uuid = ?", [str(run_uuid)]
|
|
93
|
+
).fetchone()
|
|
94
|
+
if row is None:
|
|
95
|
+
raise ValueError("运行记录不存在")
|
|
96
|
+
return LocalRunRecord.model_validate_json(row[0])
|
|
97
|
+
|
|
98
|
+
def list_runs(self, limit: int = 20, offset: int = 0) -> list[LocalRunRecord]:
|
|
99
|
+
if not 1 <= limit <= 1000 or offset < 0:
|
|
100
|
+
raise ValueError("分页参数非法")
|
|
101
|
+
if not self.path.is_file():
|
|
102
|
+
return []
|
|
103
|
+
with local_transaction(self.path, read_only=True) as connection:
|
|
104
|
+
if not connection.execute(
|
|
105
|
+
"SELECT 1 FROM information_schema.tables WHERE table_name = ?",
|
|
106
|
+
["factor_local_run"],
|
|
107
|
+
).fetchall():
|
|
108
|
+
return []
|
|
109
|
+
rows = connection.execute(
|
|
110
|
+
"""SELECT document FROM factor_local_run
|
|
111
|
+
ORDER BY id DESC LIMIT ? OFFSET ?""",
|
|
112
|
+
[limit, offset],
|
|
113
|
+
).fetchall()
|
|
114
|
+
return [LocalRunRecord.model_validate_json(row[0]) for row in rows]
|
|
115
|
+
|
|
116
|
+
def load_values(self, run_uuid: UUID) -> dict:
|
|
117
|
+
from decimal import Decimal
|
|
118
|
+
|
|
119
|
+
values: dict = {}
|
|
120
|
+
with local_transaction(self.path, read_only=True) as connection:
|
|
121
|
+
rows = connection.execute(
|
|
122
|
+
"""SELECT name, as_of AT TIME ZONE 'UTC', instrument_id, value
|
|
123
|
+
FROM factor_local_value AS v JOIN factor_local_run AS r ON r.id = v.run_id
|
|
124
|
+
WHERE r.uuid = ? ORDER BY as_of, instrument_id""",
|
|
125
|
+
[str(run_uuid)],
|
|
126
|
+
).fetchall()
|
|
127
|
+
for name, timestamp, instrument, value in rows:
|
|
128
|
+
values.setdefault(name, {})[(timestamp.replace(tzinfo=UTC), instrument)] = (
|
|
129
|
+
Decimal(value) if value is not None else None
|
|
130
|
+
)
|
|
131
|
+
return values
|
|
132
|
+
|
|
133
|
+
def list_values(self, run_uuid: UUID, limit: int = 100, offset: int = 0) -> list[dict]:
|
|
134
|
+
"""分页返回因子值;不在 CLI 中一次展开全市场结果。"""
|
|
135
|
+
if not 1 <= limit <= 10000 or offset < 0:
|
|
136
|
+
raise ValueError("因子值分页参数非法")
|
|
137
|
+
self.get(run_uuid)
|
|
138
|
+
with local_transaction(self.path, read_only=True) as connection:
|
|
139
|
+
rows = connection.execute(
|
|
140
|
+
"""SELECT name, as_of AT TIME ZONE 'UTC', instrument_id, value
|
|
141
|
+
FROM factor_local_value AS v JOIN factor_local_run AS r ON r.id = v.run_id
|
|
142
|
+
WHERE r.uuid = ? ORDER BY name, as_of, instrument_id
|
|
143
|
+
LIMIT ? OFFSET ?""",
|
|
144
|
+
[str(run_uuid), limit, offset],
|
|
145
|
+
).fetchall()
|
|
146
|
+
return [
|
|
147
|
+
{
|
|
148
|
+
"name": name,
|
|
149
|
+
"as_of": timestamp.replace(tzinfo=UTC).isoformat(),
|
|
150
|
+
"instrument_id": instrument,
|
|
151
|
+
"value": value,
|
|
152
|
+
}
|
|
153
|
+
for name, timestamp, instrument, value in rows
|
|
154
|
+
]
|