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,101 @@
|
|
|
1
|
+
"""本地研究检验与比较,因子值只引用,不在研究表重复保存。"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from quant_core.app.factor.interface.public.factor_diagnostics import IFactorDiagnostics
|
|
9
|
+
from quant_core.app.factor.interface.public.factor_local import LocalFactorStore
|
|
10
|
+
from quant_core.app.factor.schema.diagnostic_types import CrossSection
|
|
11
|
+
from quant_core.common.local_run import LocalRunRecord, RunStatus, json_value
|
|
12
|
+
|
|
13
|
+
from quant_cli.app.factor.local_factor_dependency import load_factor_result
|
|
14
|
+
from quant_cli.app.research.schema.local_research_schema import LocalEvaluationRequest
|
|
15
|
+
from quant_cli.common.utils.runtime_environment import runtime_versions
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LocalResearchStore(Protocol):
|
|
19
|
+
def save(self, record: LocalRunRecord) -> None: ...
|
|
20
|
+
def get(self, run_uuid: UUID) -> LocalRunRecord: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def evaluate_local_factor(
|
|
24
|
+
request: LocalEvaluationRequest, factor_store: LocalFactorStore, diagnostics: IFactorDiagnostics
|
|
25
|
+
) -> LocalRunRecord:
|
|
26
|
+
"""只接受显式未来标签,不把标签传回因子或策略输入。"""
|
|
27
|
+
record = LocalRunRecord(
|
|
28
|
+
request=request.model_dump(mode="json"), provenance={"environment": runtime_versions()}
|
|
29
|
+
)
|
|
30
|
+
try:
|
|
31
|
+
factor_run, result = load_factor_result(factor_store, request.factor_run_uuid)
|
|
32
|
+
values = result.get(request.factor_name)
|
|
33
|
+
if values is None:
|
|
34
|
+
raise ValueError("因子名称不属于指定运行")
|
|
35
|
+
revision = factor_run.provenance.get("market_revision")
|
|
36
|
+
if not revision or any(label.data_revision != revision for label in request.labels):
|
|
37
|
+
raise ValueError("研究标签与因子必须使用同一明确的行情修订")
|
|
38
|
+
seen = set()
|
|
39
|
+
grouped: dict = {}
|
|
40
|
+
for label in request.labels:
|
|
41
|
+
key = (label.decision_time, label.instrument_id)
|
|
42
|
+
if key in seen or key not in values:
|
|
43
|
+
raise ValueError("标签重复或不属于指定因子的计算样本")
|
|
44
|
+
seen.add(key)
|
|
45
|
+
scores, returns = grouped.setdefault(label.decision_time, ({}, {}))
|
|
46
|
+
value = values[key]
|
|
47
|
+
if value is not None and label.value is not None:
|
|
48
|
+
scores[label.instrument_id] = value
|
|
49
|
+
returns[label.instrument_id] = label.value
|
|
50
|
+
if not grouped or not any(scores for scores, _ in grouped.values()):
|
|
51
|
+
raise ValueError("没有可检验的因子与标签配对")
|
|
52
|
+
report = diagnostics.analyze(
|
|
53
|
+
tuple(
|
|
54
|
+
CrossSection(ts.date(), scores, returns)
|
|
55
|
+
for ts, (scores, returns) in sorted(grouped.items())
|
|
56
|
+
),
|
|
57
|
+
quantiles=request.quantiles,
|
|
58
|
+
)
|
|
59
|
+
record.provenance.update(
|
|
60
|
+
factor_run_uuid=str(factor_run.uuid),
|
|
61
|
+
input_digest=factor_run.provenance["input_digest"],
|
|
62
|
+
definition_digest=factor_run.provenance["definition_digest"],
|
|
63
|
+
market_revision=revision,
|
|
64
|
+
manifest_digest=factor_run.provenance.get("manifest_digest"),
|
|
65
|
+
)
|
|
66
|
+
record.result = json_value(asdict(report))
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
logging.getLogger(__name__).exception("本地计算失败")
|
|
69
|
+
record.status, record.error = RunStatus.FAILED, str(exc)
|
|
70
|
+
return record
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def compare_local_research(store: LocalResearchStore, run_uuids: tuple[UUID, ...]) -> dict:
|
|
74
|
+
"""展示实验结果并核对标签和样本口径,差异不静默当成可比较。"""
|
|
75
|
+
if not 2 <= len(run_uuids) <= 100:
|
|
76
|
+
raise ValueError("需选择 2 至 100 次研究运行")
|
|
77
|
+
records = [store.get(key) for key in run_uuids]
|
|
78
|
+
reasons = []
|
|
79
|
+
first = records[0]
|
|
80
|
+
for record in records:
|
|
81
|
+
if record.status != RunStatus.COMPLETED:
|
|
82
|
+
reasons.append("failed_run")
|
|
83
|
+
if record.request.get("labels") != first.request.get("labels"):
|
|
84
|
+
reasons.append("labels_differ")
|
|
85
|
+
if record.request.get("quantiles") != first.request.get("quantiles"):
|
|
86
|
+
reasons.append("quantiles_differ")
|
|
87
|
+
for field in ("manifest_digest", "market_revision"):
|
|
88
|
+
if record.provenance.get(field) != first.provenance.get(field):
|
|
89
|
+
reasons.append(f"{field}_differs")
|
|
90
|
+
for field in ("start", "end"):
|
|
91
|
+
if record.request.get(field) != first.request.get(field):
|
|
92
|
+
reasons.append(f"{field}_differs")
|
|
93
|
+
if ("labels" in record.request) != ("labels" in first.request):
|
|
94
|
+
reasons.append("research_kind_differs")
|
|
95
|
+
if record.provenance.get("input_digest") != first.provenance.get("input_digest"):
|
|
96
|
+
reasons.append("inputs_differ")
|
|
97
|
+
return {
|
|
98
|
+
"comparable": not reasons,
|
|
99
|
+
"reasons": sorted(set(reasons)),
|
|
100
|
+
"runs": [record.model_dump(mode="json") for record in records],
|
|
101
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""策略定义发布、查询、完整依赖下载和本地回测入口。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from quant_core.app.strategy.schema.python_strategy_schema import PythonStrategySpec
|
|
11
|
+
from quant_core.app.strategy.schema.strategy_publication_schema import PublishedStrategy
|
|
12
|
+
|
|
13
|
+
from quant_cli.app.backtest.local_backtest_dependency import build_local_backtest_store
|
|
14
|
+
from quant_cli.app.backtest.local_backtest_schema import LocalBacktestRequest
|
|
15
|
+
from quant_cli.app.backtest.service.local_backtest_service import run_local_backtest
|
|
16
|
+
from quant_cli.app.factor.local_factor_dependency import build_local_factor_store
|
|
17
|
+
from quant_cli.common.constants import DEFAULT_DATABASE_PATH
|
|
18
|
+
from quant_cli.common.http.definition_client import request_definition, save_definition
|
|
19
|
+
from quant_cli.common.utils.cli_utils import cli_errors, output
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(help="已发布策略管理与本地执行", no_args_is_help=True)
|
|
22
|
+
Server = Annotated[str, typer.Option(envvar="PQ_QUANT_URL")]
|
|
23
|
+
Token = Annotated[str, typer.Option(envvar="PQ_QUANT_TOKEN")]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("publish")
|
|
27
|
+
@cli_errors
|
|
28
|
+
def publish(
|
|
29
|
+
config: Annotated[Path, typer.Option(exists=True)],
|
|
30
|
+
server: Server,
|
|
31
|
+
token: Token,
|
|
32
|
+
source: Annotated[Path | None, typer.Option(exists=True)] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""发布策略定义,不执行策略。"""
|
|
35
|
+
payload = json.loads(config.read_text(encoding="utf-8"))
|
|
36
|
+
if source is not None:
|
|
37
|
+
payload["python_source"] = source.read_text(encoding="utf-8")
|
|
38
|
+
spec = PythonStrategySpec.model_validate(payload)
|
|
39
|
+
result = PublishedStrategy.model_validate(
|
|
40
|
+
request_definition(
|
|
41
|
+
server, token, "POST", "/strategy/definitions", payload=spec.model_dump(mode="json")
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
output(result.model_dump(mode="json"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command("list")
|
|
48
|
+
@cli_errors
|
|
49
|
+
def list_definitions(
|
|
50
|
+
server: Server, token: Token, strategy_id: str | None = None, limit: int = 100, offset: int = 0
|
|
51
|
+
) -> None:
|
|
52
|
+
"""分页查询策略定义。"""
|
|
53
|
+
params: dict[str, str | int] = {"limit": limit, "offset": offset}
|
|
54
|
+
if strategy_id is not None:
|
|
55
|
+
params["strategy_id"] = strategy_id
|
|
56
|
+
output(request_definition(server, token, "GET", "/strategy/definitions", params=params))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("fetch")
|
|
60
|
+
@cli_errors
|
|
61
|
+
def fetch(
|
|
62
|
+
strategy_id: str,
|
|
63
|
+
server: Server,
|
|
64
|
+
token: Token,
|
|
65
|
+
destination: Annotated[Path, typer.Option()],
|
|
66
|
+
version: str = "latest",
|
|
67
|
+
) -> None:
|
|
68
|
+
"""保存包含全部固定因子定义的离线策略包。"""
|
|
69
|
+
result = PublishedStrategy.model_validate(
|
|
70
|
+
request_definition(
|
|
71
|
+
server,
|
|
72
|
+
token,
|
|
73
|
+
"GET",
|
|
74
|
+
f"/strategy/definitions/{quote(strategy_id, safe='')}/{quote(version, safe='')}",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
save_definition(destination, result.model_dump(mode="json"))
|
|
78
|
+
output(
|
|
79
|
+
{
|
|
80
|
+
"path": str(destination.resolve()),
|
|
81
|
+
"version": result.definition.version,
|
|
82
|
+
"content_digest": result.content_digest,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command("run")
|
|
88
|
+
@cli_errors
|
|
89
|
+
def run(
|
|
90
|
+
config: Annotated[Path, typer.Option(exists=True)],
|
|
91
|
+
definition: Annotated[Path, typer.Option(exists=True)],
|
|
92
|
+
database: Annotated[Path, typer.Option(envvar="PQ_LOCAL_DATABASE")] = DEFAULT_DATABASE_PATH,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""执行已下载策略包,因子依赖本地计算,结果本地归档。"""
|
|
95
|
+
payload = json.loads(config.read_text(encoding="utf-8"))
|
|
96
|
+
if payload.get("source") or payload.get("publication"):
|
|
97
|
+
raise ValueError("策略执行配置不能同时提供源码或另一发布包")
|
|
98
|
+
payload["publication"] = PublishedStrategy.model_validate_json(
|
|
99
|
+
definition.read_text(encoding="utf-8")
|
|
100
|
+
).model_dump(mode="json")
|
|
101
|
+
request = LocalBacktestRequest.model_validate(payload)
|
|
102
|
+
record = asyncio.run(run_local_backtest(request, build_local_factor_store(database)))
|
|
103
|
+
build_local_backtest_store(database).save(record)
|
|
104
|
+
output(record)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""本地分析库短事务;所有入口共用文件锁,禁止并发进程直接写同一文件。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import duckdb
|
|
8
|
+
from filelock import FileLock
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@contextmanager
|
|
12
|
+
def local_transaction(
|
|
13
|
+
path: Path, *, read_only: bool = False
|
|
14
|
+
) -> Iterator[duckdb.DuckDBPyConnection]:
|
|
15
|
+
"""持锁后打开连接;成功提交、异常回滚,不在计算期间占用写锁。"""
|
|
16
|
+
path = path.expanduser().resolve()
|
|
17
|
+
if read_only and not path.is_file():
|
|
18
|
+
raise FileNotFoundError(f"本地研究库不存在:{path}")
|
|
19
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
with (
|
|
21
|
+
FileLock(str(path) + ".lock", timeout=10),
|
|
22
|
+
duckdb.connect(str(path), read_only=read_only) as connection,
|
|
23
|
+
):
|
|
24
|
+
connection.execute("BEGIN TRANSACTION")
|
|
25
|
+
try:
|
|
26
|
+
yield connection
|
|
27
|
+
connection.execute("COMMIT")
|
|
28
|
+
except BaseException:
|
|
29
|
+
connection.execute("ROLLBACK")
|
|
30
|
+
raise
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""定义管理 API 的轻量 JSON 传输,不加载 Web 应用或数据库。"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import urlsplit
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def request_definition(
|
|
12
|
+
server: str,
|
|
13
|
+
token: str,
|
|
14
|
+
method: str,
|
|
15
|
+
path: str,
|
|
16
|
+
*,
|
|
17
|
+
payload: dict | None = None,
|
|
18
|
+
params: dict | None = None,
|
|
19
|
+
) -> Any:
|
|
20
|
+
"""显式 URL 和 Bearer 令牌;不跟随可能泄漏令牌的重定向。"""
|
|
21
|
+
url = urlsplit(server)
|
|
22
|
+
if (
|
|
23
|
+
url.scheme not in {"http", "https"}
|
|
24
|
+
or not url.netloc
|
|
25
|
+
or url.username
|
|
26
|
+
or url.password
|
|
27
|
+
or url.query
|
|
28
|
+
or url.fragment
|
|
29
|
+
):
|
|
30
|
+
raise ValueError("服务地址必须是无凭据的 HTTP(S) 基础 URL")
|
|
31
|
+
if not token:
|
|
32
|
+
raise ValueError("请通过 PQ_QUANT_TOKEN 提供认证令牌")
|
|
33
|
+
with httpx.Client(timeout=30, follow_redirects=False) as client:
|
|
34
|
+
response = client.request(
|
|
35
|
+
method,
|
|
36
|
+
server.rstrip("/") + path,
|
|
37
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
38
|
+
json=payload,
|
|
39
|
+
params=params,
|
|
40
|
+
)
|
|
41
|
+
if response.status_code != 200:
|
|
42
|
+
raise ValueError(f"定义服务请求失败,HTTP {response.status_code}")
|
|
43
|
+
document = response.json()
|
|
44
|
+
if not isinstance(document, dict) or document.get("code") != 0 or "data" not in document:
|
|
45
|
+
raise ValueError("定义服务响应格式不合法")
|
|
46
|
+
return document["data"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def save_definition(path: Path, document: dict) -> None:
|
|
50
|
+
"""下载定义不覆盖已有文件,避免正在引用的版本被静默替换。"""
|
|
51
|
+
text = json.dumps(document, ensure_ascii=False, allow_nan=False, indent=2)
|
|
52
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
with path.open("x", encoding="utf-8") as stream:
|
|
54
|
+
stream.write(text)
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""本地 CLI 的 JSON 输出和统一错误退出;日志只输出到 stderr。"""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from quant_core.common.local_run import LocalRunRecord, RunStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def output(value: Any) -> None:
|
|
14
|
+
"""输出一次机器可读结果;失败运行保留 UUID 并返回非零退出码。"""
|
|
15
|
+
document = value.model_dump(mode="json") if isinstance(value, LocalRunRecord) else value
|
|
16
|
+
typer.echo(json.dumps(document, ensure_ascii=False, default=str, allow_nan=False))
|
|
17
|
+
if isinstance(value, LocalRunRecord) and value.status == RunStatus.FAILED:
|
|
18
|
+
raise typer.Exit(1)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cli_errors(function: Callable) -> Callable:
|
|
22
|
+
"""为各命令保留签名并翻译参数、计算或归档失败。"""
|
|
23
|
+
|
|
24
|
+
@functools.wraps(function)
|
|
25
|
+
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
|
26
|
+
try:
|
|
27
|
+
return function(*args, **kwargs)
|
|
28
|
+
except typer.Exit:
|
|
29
|
+
raise
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
logging.getLogger(__name__).debug("CLI 执行失败", exc_info=True)
|
|
32
|
+
typer.echo(
|
|
33
|
+
json.dumps({"status": "failed", "error": str(exc)}, ensure_ascii=False), err=True
|
|
34
|
+
)
|
|
35
|
+
raise typer.Exit(1) from exc
|
|
36
|
+
|
|
37
|
+
return wrapped
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""CLI 标准流配置。"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def configure_utf8_output() -> None:
|
|
7
|
+
"""统一标准输出与错误输出编码。"""
|
|
8
|
+
for stream in (sys.stdout, sys.stderr):
|
|
9
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
10
|
+
if callable(reconfigure):
|
|
11
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""CLI 发行版本与计算运行环境,独立于核心库的分发方式。"""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def runtime_versions() -> dict[str, str]:
|
|
8
|
+
"""记录实际安装工具与数值依赖版本,供结果复现。"""
|
|
9
|
+
return {
|
|
10
|
+
"python": platform.python_version(),
|
|
11
|
+
**{name: version(name) for name in ("powerquant-cli", "polars", "numpy")},
|
|
12
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""全市场缓存与本地公式验证:python -m examples.cached_factor_screen。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import time
|
|
8
|
+
from datetime import date, datetime
|
|
9
|
+
from datetime import time as day_time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
from zoneinfo import ZoneInfo
|
|
13
|
+
|
|
14
|
+
from pqdata import MarketClient
|
|
15
|
+
from quant_core.app.factor.computation.factor_expression import MA, RANK, Factor
|
|
16
|
+
|
|
17
|
+
from quant_cli.app.data.service.bar_cache_service import (
|
|
18
|
+
BarCacheQuery,
|
|
19
|
+
load_bar_snapshot,
|
|
20
|
+
prepare_bar_cache,
|
|
21
|
+
)
|
|
22
|
+
from quant_cli.app.factor.service.factor_data_prepare_service import execute_cached_factors
|
|
23
|
+
from quant_cli.common.utils.platform_utils import configure_utf8_output
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MeasuredClient(MarketClient):
|
|
27
|
+
"""记录网络查询次数与反序列化后 JSON 大小,后者不是传输层字节数。"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
super().__init__(
|
|
31
|
+
os.getenv("MARKET_BASE_URL", "http://localhost:8002/api/v1"),
|
|
32
|
+
token=os.getenv("MARKET_API_TOKEN", ""),
|
|
33
|
+
)
|
|
34
|
+
self.download_pages = 0
|
|
35
|
+
self.content_checks = 0
|
|
36
|
+
self.payload_bytes = 0
|
|
37
|
+
|
|
38
|
+
async def query_bars(self, query):
|
|
39
|
+
result = await super().query_bars(query)
|
|
40
|
+
self.download_pages += 1
|
|
41
|
+
self.payload_bytes += len(result.model_dump_json().encode())
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
async def describe_bars(self, query):
|
|
45
|
+
result = await super().describe_bars(query)
|
|
46
|
+
self.content_checks += 1
|
|
47
|
+
self.payload_bytes += len(result.model_dump_json().encode())
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def main() -> None:
|
|
52
|
+
"""只读行情服务,保存本地证据;不发布因子或提交订单。"""
|
|
53
|
+
cn = ZoneInfo("Asia/Shanghai")
|
|
54
|
+
first, last = date(2026, 9, 23), date(2026, 9, 30)
|
|
55
|
+
output = Path(".cache/v021-factor-verification")
|
|
56
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
async with MeasuredClient() as client:
|
|
58
|
+
catalog = await client.search_instruments(instrument_type="stock", limit=10000)
|
|
59
|
+
if len(catalog) >= 10000:
|
|
60
|
+
raise ValueError("目录达到上限,拒绝截断全市场")
|
|
61
|
+
unknown = [row.instrument_id for row in catalog if row.listed_date is None]
|
|
62
|
+
stocks = [
|
|
63
|
+
row
|
|
64
|
+
for row in catalog
|
|
65
|
+
if row.listed_date is not None
|
|
66
|
+
and row.listed_date <= last
|
|
67
|
+
and (row.delisted_date is None or row.delisted_date >= first)
|
|
68
|
+
]
|
|
69
|
+
calendar_rows = await client.list_trading_days("XSHG", first, last)
|
|
70
|
+
calendar = tuple(
|
|
71
|
+
datetime.combine(row.trade_date, day_time(15), cn)
|
|
72
|
+
for row in calendar_rows
|
|
73
|
+
if row.is_open
|
|
74
|
+
)
|
|
75
|
+
if len(calendar) < 5:
|
|
76
|
+
raise ValueError("交易日不足以验证五日窗口")
|
|
77
|
+
universe = {
|
|
78
|
+
timestamp: tuple(
|
|
79
|
+
row.instrument_id
|
|
80
|
+
for row in stocks
|
|
81
|
+
if row.listed_date <= timestamp.date()
|
|
82
|
+
and (row.delisted_date is None or row.delisted_date >= timestamp.date())
|
|
83
|
+
)
|
|
84
|
+
for timestamp in calendar
|
|
85
|
+
}
|
|
86
|
+
query = BarCacheQuery(
|
|
87
|
+
instrument_ids=sorted(row.instrument_id for row in stocks),
|
|
88
|
+
frequency="1d",
|
|
89
|
+
start=datetime.combine(first, day_time.min, cn),
|
|
90
|
+
end=datetime.combine(last, day_time.max, cn),
|
|
91
|
+
)
|
|
92
|
+
metrics = []
|
|
93
|
+
for label in ("initial", "repeat"):
|
|
94
|
+
before = (client.download_pages, client.content_checks, client.payload_bytes)
|
|
95
|
+
started = time.perf_counter()
|
|
96
|
+
manifest = await prepare_bar_cache(client, query, output, 200000)
|
|
97
|
+
metrics.append(
|
|
98
|
+
{
|
|
99
|
+
"label": label,
|
|
100
|
+
"seconds": time.perf_counter() - started,
|
|
101
|
+
"download_pages": client.download_pages - before[0],
|
|
102
|
+
"content_checks": client.content_checks - before[1],
|
|
103
|
+
"decoded_payload_bytes": client.payload_bytes - before[2],
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
assert metrics[1]["download_pages"] == 0
|
|
107
|
+
with patch("socket.socket", side_effect=AssertionError("offline computation used network")):
|
|
108
|
+
started = time.perf_counter()
|
|
109
|
+
result = execute_cached_factors(
|
|
110
|
+
{
|
|
111
|
+
"intraday_rank": RANK(Factor("close") / Factor("open") - 1),
|
|
112
|
+
"mean5": MA(Factor("close"), 5),
|
|
113
|
+
},
|
|
114
|
+
manifest,
|
|
115
|
+
calendar=calendar,
|
|
116
|
+
universe=universe,
|
|
117
|
+
start=calendar[-1],
|
|
118
|
+
end=calendar[-1],
|
|
119
|
+
max_rows=200000,
|
|
120
|
+
)
|
|
121
|
+
compute_seconds = time.perf_counter() - started
|
|
122
|
+
_, rows = load_bar_snapshot(manifest, 200000)
|
|
123
|
+
# 独立 Decimal 五日均值,不使用因子算子作为测试 oracle。
|
|
124
|
+
lookup = {(row.timestamp, row.instrument_id): row for row in rows}
|
|
125
|
+
checked = 0
|
|
126
|
+
for instrument in query.instrument_ids:
|
|
127
|
+
window = [lookup.get((timestamp, instrument)) for timestamp in calendar[-5:]]
|
|
128
|
+
if all(row is not None and not row.suspended for row in window):
|
|
129
|
+
expected = sum(row.close for row in window) / 5
|
|
130
|
+
actual = result.values["mean5"][(calendar[-1], instrument)]
|
|
131
|
+
if instrument in universe[calendar[-1]]:
|
|
132
|
+
assert actual is not None and abs(float(actual - expected)) < 1e-9
|
|
133
|
+
checked += 1
|
|
134
|
+
summary = {
|
|
135
|
+
"platform": platform.platform(),
|
|
136
|
+
"python": platform.python_version(),
|
|
137
|
+
"stock_catalog": len(catalog),
|
|
138
|
+
"unknown_listing": unknown,
|
|
139
|
+
"prepared_instruments": len(stocks),
|
|
140
|
+
"trading_days": len(calendar),
|
|
141
|
+
"bar_rows": len(rows),
|
|
142
|
+
"expected_grid": len(stocks) * len(calendar),
|
|
143
|
+
"missing_grid_rows": len(stocks) * len(calendar) - len(rows),
|
|
144
|
+
"independent_mean_checks": checked,
|
|
145
|
+
"non_null": {
|
|
146
|
+
name: sum(value is not None for value in values.values())
|
|
147
|
+
for name, values in result.values.items()
|
|
148
|
+
},
|
|
149
|
+
"metrics": metrics,
|
|
150
|
+
"offline_compute_seconds": compute_seconds,
|
|
151
|
+
"parquet_bytes": sum(path.stat().st_size for path in manifest.parent.glob("*.parquet")),
|
|
152
|
+
"manifest": str(manifest.resolve()),
|
|
153
|
+
"input_digest": result.input_digest,
|
|
154
|
+
"price_basis": "raw",
|
|
155
|
+
"historical_knowledge": "current revision only",
|
|
156
|
+
}
|
|
157
|
+
(output / "summary.json").write_text(
|
|
158
|
+
json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
159
|
+
)
|
|
160
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
configure_utf8_output()
|
|
165
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""最近 20 个交易日最大收盘回撤,取值范围 [-1, 0]。"""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute(bars, parameters):
|
|
7
|
+
lookback = int(parameters.get("lookback", 20))
|
|
8
|
+
if lookback < 2 or len(bars) < lookback:
|
|
9
|
+
return None
|
|
10
|
+
peak = Decimal("0")
|
|
11
|
+
worst = Decimal("0")
|
|
12
|
+
for bar in bars[-lookback:]:
|
|
13
|
+
close = Decimal(bar["close"])
|
|
14
|
+
if close <= 0:
|
|
15
|
+
return None
|
|
16
|
+
peak = max(peak, close)
|
|
17
|
+
worst = min(worst, close / peak - 1)
|
|
18
|
+
return worst
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""20 日动量:今日收盘价 / 20 个交易日前收盘价 - 1。"""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute(bars, parameters):
|
|
7
|
+
lookback = int(parameters.get("lookback", 21))
|
|
8
|
+
if len(bars) < lookback:
|
|
9
|
+
return None
|
|
10
|
+
previous = Decimal(bars[-lookback]["close"])
|
|
11
|
+
if previous == 0:
|
|
12
|
+
return None
|
|
13
|
+
return Decimal(bars[-1]["close"]) / previous - 1
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""五日量价研究配方;日内比值避免跨日复权口径混用。"""
|
|
2
|
+
|
|
3
|
+
from quant_core.app.factor.computation.factor_expression import MA, REF, STD, Factor
|
|
4
|
+
|
|
5
|
+
close = Factor("close")
|
|
6
|
+
open_price = Factor("open")
|
|
7
|
+
high = Factor("high")
|
|
8
|
+
low = Factor("low")
|
|
9
|
+
volume = Factor("volume")
|
|
10
|
+
intraday_return = close / open_price - 1
|
|
11
|
+
factors = {
|
|
12
|
+
"close_pressure_5d": MA((close * 2 - high - low) / (high - low) * volume, 5) / MA(volume, 5),
|
|
13
|
+
"intraday_strength_5d": MA(intraday_return, 5),
|
|
14
|
+
"low_intraday_volatility_5d": -STD(intraday_return, 5),
|
|
15
|
+
"volume_expansion": volume / REF(MA(volume, 4), 1),
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""20 日风险调整动量:区间收益 / 日收益总体标准差。"""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute(bars, parameters):
|
|
7
|
+
lookback = int(parameters.get("lookback", 21))
|
|
8
|
+
if lookback < 3 or len(bars) < lookback:
|
|
9
|
+
return None
|
|
10
|
+
closes = [Decimal(bar["close"]) for bar in bars[-lookback:]]
|
|
11
|
+
if any(close <= 0 for close in closes):
|
|
12
|
+
return None
|
|
13
|
+
returns = [closes[index] / closes[index - 1] - 1 for index in range(1, lookback)]
|
|
14
|
+
mean = sum(returns) / len(returns)
|
|
15
|
+
variance = sum((value - mean) ** 2 for value in returns) / len(returns)
|
|
16
|
+
if variance == 0:
|
|
17
|
+
return None
|
|
18
|
+
return (closes[-1] / closes[0] - 1) / variance.sqrt()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""5 日量比:当日成交量 / 最近 5 根日线平均成交量。"""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute(bars, parameters):
|
|
7
|
+
lookback = int(parameters.get("lookback", 5))
|
|
8
|
+
if len(bars) < lookback:
|
|
9
|
+
return None
|
|
10
|
+
volumes = [Decimal(bar["volume"]) for bar in bars[-lookback:]]
|
|
11
|
+
average = sum(volumes) / lookback
|
|
12
|
+
if average == 0:
|
|
13
|
+
return None
|
|
14
|
+
return volumes[-1] / average
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""五日成交量加权收盘强度;仅为研究配方,不代表收益有效性。"""
|
|
2
|
+
|
|
3
|
+
from quant_core.app.factor.computation.factor_expression import MA, Factor
|
|
4
|
+
|
|
5
|
+
close = Factor("close")
|
|
6
|
+
high = Factor("high")
|
|
7
|
+
low = Factor("low")
|
|
8
|
+
volume = Factor("volume")
|
|
9
|
+
|
|
10
|
+
# 日内比值避免跨日价格复权口径差异;零振幅窗口保持不可计算。
|
|
11
|
+
close_pressure = (close * 2 - high - low) / (high - low)
|
|
12
|
+
factors = {
|
|
13
|
+
"volume_weighted_close_pressure_5d": MA(close_pressure * volume, 5) / MA(volume, 5)
|
|
14
|
+
}
|