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,319 @@
|
|
|
1
|
+
"""策略生命周期调度与 PIT 数据装配;交易调用同步交给注入的公开 API。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Iterable, Mapping
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
from zoneinfo import ZoneInfo
|
|
7
|
+
|
|
8
|
+
from quant_core.app.data.constants import BarFrequency, MarketEventType
|
|
9
|
+
from quant_core.app.data.interface.market_data_query import IMarketDataQuery
|
|
10
|
+
from quant_core.app.data.schema.adjustment_schema import (
|
|
11
|
+
AdjustmentAnchorMode,
|
|
12
|
+
AdjustmentType,
|
|
13
|
+
BarViewRequest,
|
|
14
|
+
)
|
|
15
|
+
from quant_core.app.data.schema.event_schema import (
|
|
16
|
+
BarEvent,
|
|
17
|
+
MarketTimeSlice,
|
|
18
|
+
TradingSessionEvent,
|
|
19
|
+
market_event_order_key,
|
|
20
|
+
)
|
|
21
|
+
from quant_core.app.factor.interface.public.factor_exposure_query import IFactorExposureQuery
|
|
22
|
+
from quant_core.app.factor.interface.public.factor_selection import IFactorSelection
|
|
23
|
+
from quant_core.app.order_execution.interface.public.order_submission import IOrderSubmission
|
|
24
|
+
from quant_core.app.strategy.interface.public.portfolio_construction import IPortfolioConstruction
|
|
25
|
+
from quant_core.app.strategy.interface.public.strategy import IStrategy
|
|
26
|
+
from quant_core.app.strategy.interface.public.strategy_portfolio_reader import (
|
|
27
|
+
IStrategyPortfolioReader,
|
|
28
|
+
)
|
|
29
|
+
from quant_core.app.strategy.interface.strategy_factor_reader import IStrategyFactorReader
|
|
30
|
+
from quant_core.app.strategy.interface.strategy_order_reader import IStrategyOrderReader
|
|
31
|
+
from quant_core.app.strategy.schema.portfolio_target import PortfolioDecision
|
|
32
|
+
from quant_core.app.strategy.schema.signal_schema import Signal
|
|
33
|
+
from quant_core.app.strategy.service.strategy_market_view import StrategyMarketView
|
|
34
|
+
from quant_core.app.strategy.service.strategy_scheduler import StrategyScheduler
|
|
35
|
+
from quant_core.app.strategy.strategy_context import StrategyContext
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def _aiter(items: list):
|
|
39
|
+
"""list → async iterator(时间片同步列表与异步流统一消费)"""
|
|
40
|
+
for item in items:
|
|
41
|
+
yield item
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class StrategyRuntimeService:
|
|
45
|
+
"""策略运行时:以时间片为调度边界消费市场事件流,装配上下文并调度策略回调"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
strategy: IStrategy,
|
|
50
|
+
market: IMarketDataQuery,
|
|
51
|
+
factor_reader: IStrategyFactorReader,
|
|
52
|
+
portfolio: IStrategyPortfolioReader | None = None,
|
|
53
|
+
order_reader: IStrategyOrderReader | None = None,
|
|
54
|
+
order_api: IOrderSubmission | None = None,
|
|
55
|
+
adjustment_type: AdjustmentType = AdjustmentType.NONE,
|
|
56
|
+
observation_frequency: BarFrequency | None = None,
|
|
57
|
+
listing_dates: Mapping[str, date] | None = None,
|
|
58
|
+
factor_selection: IFactorSelection | None = None,
|
|
59
|
+
exposure_query: IFactorExposureQuery | None = None,
|
|
60
|
+
portfolio_construction: IPortfolioConstruction | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
self.strategy = strategy
|
|
63
|
+
self.market = market
|
|
64
|
+
self.factor_reader = factor_reader
|
|
65
|
+
self.portfolio = portfolio # 只读视图;修改持仓归属 Backtest/Execution
|
|
66
|
+
self.order_reader = order_reader
|
|
67
|
+
self._order_api = order_api
|
|
68
|
+
self.decisions: tuple[PortfolioDecision, ...] = ()
|
|
69
|
+
self.adjustment_type = adjustment_type
|
|
70
|
+
self.observation_frequency = observation_frequency
|
|
71
|
+
self._listing_dates = dict(listing_dates or {})
|
|
72
|
+
self._factor_selection = factor_selection
|
|
73
|
+
self._exposure_query = exposure_query
|
|
74
|
+
self._portfolio_construction = portfolio_construction
|
|
75
|
+
self.state: dict[str, Any] = {} # strategy-local state,禁存 cash/position
|
|
76
|
+
self._scheduler = StrategyScheduler()
|
|
77
|
+
self._active_universe: tuple[str, ...] | None = None
|
|
78
|
+
self._active_day = None
|
|
79
|
+
self._initialized = False
|
|
80
|
+
self._after_trading_days: set = set()
|
|
81
|
+
|
|
82
|
+
async def run_slice(self, time_slice: MarketTimeSlice) -> list[Signal]:
|
|
83
|
+
"""单时间片执行入口(Backtest 逐片编排的边界):执行一个时间片并立即返回该片 Signal
|
|
84
|
+
|
|
85
|
+
对 MarketTimeSlice:
|
|
86
|
+
1. 以 slice.event_time 为上界装配全部 universe 窗口与 Canonical FactorValue
|
|
87
|
+
(完整信息集,先于任何策略回调);
|
|
88
|
+
2. 校验片内每个 BarEvent 的标的窗口包含当根 Bar(数据截断/排序异常
|
|
89
|
+
显式失败,不静默算陈旧数据);
|
|
90
|
+
3. 按时间片内的稳定顺序(边界优先级 + 标的字典序,仅确定性语义)
|
|
91
|
+
逐事件调度策略回调;
|
|
92
|
+
4. 该时间片产生的 Signal 整批收齐返回;交易 API 可在回调内产生订单与成交,
|
|
93
|
+
账户由事件监听器更新。
|
|
94
|
+
|
|
95
|
+
返回后调用方(Backtest)可执行撮合并更新 Portfolio,再处理下一时间片;
|
|
96
|
+
下一片的 StrategyContext 经只读 IStrategyPortfolioReader 读到新状态。
|
|
97
|
+
"""
|
|
98
|
+
slice_frequency = self._slice_frequency(time_slice)
|
|
99
|
+
frequency = (
|
|
100
|
+
self.observation_frequency.value
|
|
101
|
+
if self.observation_frequency is not None
|
|
102
|
+
else slice_frequency
|
|
103
|
+
)
|
|
104
|
+
context, windows = await self._prepare(time_slice.event_time, frequency)
|
|
105
|
+
# 按稳定技术顺序调度(边界优先级 + 标的字典序,仅确定性语义),
|
|
106
|
+
# 输入排列不影响策略调用顺序与 Signal 输出顺序
|
|
107
|
+
ordered = sorted(time_slice.events, key=market_event_order_key)
|
|
108
|
+
signals: list[Signal] = []
|
|
109
|
+
day = time_slice.event_time.date()
|
|
110
|
+
if not self._initialized:
|
|
111
|
+
context._orders_allowed = False
|
|
112
|
+
signals.extend(await self._invoke_optional("init", context))
|
|
113
|
+
self._initialized = True
|
|
114
|
+
context._orders_allowed = True
|
|
115
|
+
if self._active_day != day:
|
|
116
|
+
self._active_day = day
|
|
117
|
+
context._orders_allowed = False
|
|
118
|
+
signals.extend(await self._invoke_optional("before_trading", context))
|
|
119
|
+
context._orders_allowed = True
|
|
120
|
+
for callback in self._scheduler.due(time_slice.event_time):
|
|
121
|
+
signals.extend(await self._invoke_optional(callback.callback_name, context))
|
|
122
|
+
for event in ordered:
|
|
123
|
+
if isinstance(event, BarEvent):
|
|
124
|
+
if (
|
|
125
|
+
self.observation_frequency is not None
|
|
126
|
+
and event.payload.frequency != self.observation_frequency.value
|
|
127
|
+
and not self._has_observation_bar(
|
|
128
|
+
windows, event.payload.instrument_id, time_slice.event_time
|
|
129
|
+
)
|
|
130
|
+
):
|
|
131
|
+
continue
|
|
132
|
+
self._require_event_bar(windows, event.payload.instrument_id, time_slice.event_time)
|
|
133
|
+
event = event.model_copy(
|
|
134
|
+
update={"payload": windows[event.payload.instrument_id][-1]}
|
|
135
|
+
)
|
|
136
|
+
signals.extend(await self._dispatch(event, context))
|
|
137
|
+
slice_callback = getattr(self.strategy, "on_slice", None)
|
|
138
|
+
if slice_callback is not None:
|
|
139
|
+
signals.extend(await slice_callback(time_slice, context) or ())
|
|
140
|
+
if day not in self._after_trading_days and self._has_session_close(time_slice):
|
|
141
|
+
context._orders_allowed = False
|
|
142
|
+
signals.extend(await self._invoke_optional("after_trading", context))
|
|
143
|
+
self._after_trading_days.add(day)
|
|
144
|
+
self.decisions = tuple(context.decisions)
|
|
145
|
+
return signals
|
|
146
|
+
|
|
147
|
+
async def run_slices(
|
|
148
|
+
self, slices: Iterable[MarketTimeSlice] | AsyncIterator[MarketTimeSlice]
|
|
149
|
+
) -> list[Signal]:
|
|
150
|
+
"""批量便利入口:逐时间片调用 run_slice(复用同一调度逻辑,不复制业务路径)。
|
|
151
|
+
|
|
152
|
+
仅适合一次性回放到结束的场景;需要在时间片之间撮合/更新 Portfolio 的
|
|
153
|
+
Backtest 必须自己逐片调用 run_slice。
|
|
154
|
+
"""
|
|
155
|
+
stream = slices if isinstance(slices, AsyncIterator) else _aiter(list(slices))
|
|
156
|
+
signals: list[Signal] = []
|
|
157
|
+
async for time_slice in stream:
|
|
158
|
+
signals.extend(await self.run_slice(time_slice))
|
|
159
|
+
return signals
|
|
160
|
+
|
|
161
|
+
async def run(self, events) -> list[Signal]:
|
|
162
|
+
"""按市场时间顺序消费原子事件流直到结束(单事件入口,复用同一调度逻辑)"""
|
|
163
|
+
signals: list[Signal] = []
|
|
164
|
+
async for event in events:
|
|
165
|
+
signals.extend(await self.handle(event))
|
|
166
|
+
return signals
|
|
167
|
+
|
|
168
|
+
async def handle(self, event) -> list[Signal]:
|
|
169
|
+
"""处理单个事件:装配 StrategyContext → 调度策略回调"""
|
|
170
|
+
if isinstance(event, (BarEvent, TradingSessionEvent)):
|
|
171
|
+
return await self.run_slice(
|
|
172
|
+
MarketTimeSlice(event_time=event.event_time, events=[event])
|
|
173
|
+
)
|
|
174
|
+
return [] # 未知事件按契约忽略(Runtime 不做 Bar-centric 假设)
|
|
175
|
+
|
|
176
|
+
async def _dispatch(self, event, context: StrategyContext) -> list[Signal]:
|
|
177
|
+
"""单事件 → 策略回调分发(原子流与时间片流共用,不复制业务逻辑)"""
|
|
178
|
+
if isinstance(event, BarEvent):
|
|
179
|
+
return await self.strategy.on_bar(event, context)
|
|
180
|
+
if isinstance(event, TradingSessionEvent):
|
|
181
|
+
return await self.strategy.on_session(event, context)
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
async def _invoke_optional(self, callback_name: str, context: StrategyContext) -> list[Signal]:
|
|
185
|
+
callback = getattr(self.strategy, callback_name, None)
|
|
186
|
+
if callback is None:
|
|
187
|
+
return []
|
|
188
|
+
result = await callback(context)
|
|
189
|
+
return list(result or ())
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _has_session_close(time_slice: MarketTimeSlice) -> bool:
|
|
193
|
+
return any(
|
|
194
|
+
isinstance(event, TradingSessionEvent)
|
|
195
|
+
and event.payload.session is MarketEventType.SESSION_CLOSE
|
|
196
|
+
for event in time_slice.events
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _set_universe(self, universe: tuple[str, ...]) -> None:
|
|
200
|
+
self._active_universe = universe
|
|
201
|
+
|
|
202
|
+
async def _prepare(
|
|
203
|
+
self, event_time: datetime, frequency: str = "1d"
|
|
204
|
+
) -> tuple[StrategyContext, dict[str, list]]:
|
|
205
|
+
"""装配完整上下文:每个 universe 标的截至 event_time(含)的窗口 + Canonical FactorValue
|
|
206
|
+
|
|
207
|
+
返回 (context, windows);因子历史与发布状态归 Factor Store。
|
|
208
|
+
windows 供调用方做事件标的数据一致性防御校验
|
|
209
|
+
(窗口末根晚于/缺于事件时间 = 查询异常)。窗口上界永远 ≤ event_time,
|
|
210
|
+
Strategy 不可能读到未来数据。
|
|
211
|
+
"""
|
|
212
|
+
definition = self.strategy.definition()
|
|
213
|
+
windows: dict[str, list] = {}
|
|
214
|
+
factors: dict[str, dict] = {}
|
|
215
|
+
universe = self._active_universe or tuple(self._universe(definition))
|
|
216
|
+
self._active_universe = universe
|
|
217
|
+
visible_universe = tuple(
|
|
218
|
+
instrument_id
|
|
219
|
+
for instrument_id in universe
|
|
220
|
+
if self._listing_dates.get(instrument_id) is None
|
|
221
|
+
or self._listing_dates[instrument_id] <= event_time.date()
|
|
222
|
+
)
|
|
223
|
+
for instrument_id in visible_universe:
|
|
224
|
+
listed_date = self._listing_dates.get(instrument_id)
|
|
225
|
+
if (
|
|
226
|
+
listed_date == event_time.date()
|
|
227
|
+
and frequency == BarFrequency.D1.value
|
|
228
|
+
and event_time.astimezone(ZoneInfo("Asia/Shanghai")).hour < 15
|
|
229
|
+
):
|
|
230
|
+
windows[instrument_id] = []
|
|
231
|
+
continue
|
|
232
|
+
window = await self._bar_window(instrument_id, frequency, event_time)
|
|
233
|
+
windows[instrument_id] = window
|
|
234
|
+
if not window:
|
|
235
|
+
continue # 停牌/未同步标的当天无 Bar 属正常语义
|
|
236
|
+
if definition.required_factors and visible_universe:
|
|
237
|
+
factors = await self.factor_reader.read_many(
|
|
238
|
+
tuple(definition.required_factors), visible_universe, event_time
|
|
239
|
+
)
|
|
240
|
+
for rows in factors.values():
|
|
241
|
+
if any(result.available_at > event_time for result in rows.values()):
|
|
242
|
+
raise ValueError("Factor Reader 返回未来可见值,拒绝注入策略")
|
|
243
|
+
context = StrategyContext(
|
|
244
|
+
event_time=event_time,
|
|
245
|
+
market=StrategyMarketView(self.market, event_time, self.adjustment_type),
|
|
246
|
+
factors=factors,
|
|
247
|
+
parameters=definition.parameters,
|
|
248
|
+
state=self.state,
|
|
249
|
+
portfolio=self.portfolio,
|
|
250
|
+
strategy_id=definition.strategy_id,
|
|
251
|
+
order_reader=self.order_reader,
|
|
252
|
+
order_api=self._order_api,
|
|
253
|
+
current_data={key: value[-1] for key, value in windows.items() if value},
|
|
254
|
+
universe=visible_universe,
|
|
255
|
+
universe_setter=self._set_universe,
|
|
256
|
+
run_daily=self._scheduler.run_daily,
|
|
257
|
+
run_weekly=self._scheduler.run_weekly,
|
|
258
|
+
factor_selection=self._factor_selection,
|
|
259
|
+
exposure_query=self._exposure_query,
|
|
260
|
+
portfolio_construction=self._portfolio_construction,
|
|
261
|
+
)
|
|
262
|
+
return context, windows
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def _has_observation_bar(
|
|
266
|
+
windows: dict[str, list], instrument_id: str, event_time: datetime
|
|
267
|
+
) -> bool:
|
|
268
|
+
window = windows.get(instrument_id, [])
|
|
269
|
+
return bool(window and window[-1].timestamp == event_time)
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _require_event_bar(windows: dict[str, list], instrument_id: str, event_time) -> None:
|
|
273
|
+
"""事件标的的窗口必须包含 event_time 当根 Bar(Replay 事件来自 DB Bar)"""
|
|
274
|
+
window = windows.get(instrument_id, [])
|
|
275
|
+
if not window or window[-1].timestamp != event_time:
|
|
276
|
+
actual = window[-1].timestamp if window else "无数据"
|
|
277
|
+
raise ValueError(
|
|
278
|
+
f"Bar 窗口末根 {actual} 与事件时间 {event_time} 不一致"
|
|
279
|
+
f"(instrument={instrument_id}),疑似查询截断/排序异常"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _slice_frequency(time_slice: MarketTimeSlice) -> str:
|
|
284
|
+
"""时间片 Bar 频率取片内 BarEvent 的 frequency(无 Bar 默认日线)"""
|
|
285
|
+
for event in time_slice.events:
|
|
286
|
+
if isinstance(event, BarEvent):
|
|
287
|
+
return event.payload.frequency
|
|
288
|
+
return "1d"
|
|
289
|
+
|
|
290
|
+
async def _bar_window(self, instrument_id: str, frequency: str, event_time, limit: int = 100):
|
|
291
|
+
"""截至 event_time(含)的 Canonical Bar 窗口(旧→新;上界不含未来数据)"""
|
|
292
|
+
if self.adjustment_type is AdjustmentType.NONE:
|
|
293
|
+
return await self.market.get_bars(
|
|
294
|
+
instrument_id,
|
|
295
|
+
frequency,
|
|
296
|
+
None,
|
|
297
|
+
event_time,
|
|
298
|
+
limit,
|
|
299
|
+
)
|
|
300
|
+
result = await self.market.get_adjusted_bars(
|
|
301
|
+
instrument_id,
|
|
302
|
+
frequency,
|
|
303
|
+
BarViewRequest(
|
|
304
|
+
adjustment_type=self.adjustment_type,
|
|
305
|
+
anchor_mode=AdjustmentAnchorMode.DECISION_TIME,
|
|
306
|
+
decision_time=event_time,
|
|
307
|
+
),
|
|
308
|
+
None,
|
|
309
|
+
event_time,
|
|
310
|
+
limit,
|
|
311
|
+
)
|
|
312
|
+
return result.bars
|
|
313
|
+
|
|
314
|
+
@staticmethod
|
|
315
|
+
def _universe(definition) -> list[str]:
|
|
316
|
+
universe = list(definition.universe)
|
|
317
|
+
if not universe:
|
|
318
|
+
raise ValueError("StrategyDefinition.universe 为空且未外部指定标的池")
|
|
319
|
+
return universe
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Deterministic strategy callback schedules driven only by market time."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import date, datetime, time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class ScheduledCallback:
|
|
9
|
+
callback_name: str
|
|
10
|
+
target_time: time
|
|
11
|
+
weekday: int | None = None
|
|
12
|
+
|
|
13
|
+
def __post_init__(self) -> None:
|
|
14
|
+
if not self.callback_name.strip():
|
|
15
|
+
raise ValueError("callback_name 不能为空")
|
|
16
|
+
if self.weekday is not None and self.weekday not in range(7):
|
|
17
|
+
raise ValueError("weekday 必须位于 [0, 6]")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StrategyScheduler:
|
|
21
|
+
"""Registry and once-per-trading-day trigger state."""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._callbacks: list[ScheduledCallback] = []
|
|
25
|
+
self._fired: set[tuple[date, ScheduledCallback]] = set()
|
|
26
|
+
|
|
27
|
+
def run_daily(self, callback_name: str, target_time: time) -> None:
|
|
28
|
+
self._register(ScheduledCallback(callback_name, target_time))
|
|
29
|
+
|
|
30
|
+
def run_weekly(self, callback_name: str, weekday: int, target_time: time) -> None:
|
|
31
|
+
self._register(ScheduledCallback(callback_name, target_time, weekday))
|
|
32
|
+
|
|
33
|
+
def due(self, event_time: datetime) -> tuple[ScheduledCallback, ...]:
|
|
34
|
+
day = event_time.date()
|
|
35
|
+
due: list[ScheduledCallback] = []
|
|
36
|
+
for callback in self._callbacks:
|
|
37
|
+
key = (day, callback)
|
|
38
|
+
if key in self._fired:
|
|
39
|
+
continue
|
|
40
|
+
if callback.weekday is not None and day.weekday() != callback.weekday:
|
|
41
|
+
continue
|
|
42
|
+
if event_time.timetz().replace(tzinfo=None) < callback.target_time:
|
|
43
|
+
continue
|
|
44
|
+
self._fired.add(key)
|
|
45
|
+
due.append(callback)
|
|
46
|
+
return tuple(due)
|
|
47
|
+
|
|
48
|
+
def _register(self, callback: ScheduledCallback) -> None:
|
|
49
|
+
if callback not in self._callbacks:
|
|
50
|
+
self._callbacks.append(callback)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Momentum Volume Breakout - P1 Demo Strategy(Python Native)
|
|
3
|
+
|
|
4
|
+
规则(目标不是盈利,是验证 Market → Factor → Strategy → Signal 边界正确):
|
|
5
|
+
- Entry:momentum_20d > 阈值 AND volume_ratio_5d > 1.5 → ENTER LONG
|
|
6
|
+
- Exit:close 跌破 ma_close_10 → EXIT
|
|
7
|
+
|
|
8
|
+
边界收口:
|
|
9
|
+
- 全部数值判断消费 Canonical FactorValue(因子计算只发生在 Factor 模块,Strategy
|
|
10
|
+
不重复计算任何因子,包括 MA);
|
|
11
|
+
- 持仓经 context.portfolio 只读获取(get_position),不维护 cash/position;
|
|
12
|
+
context.state 只允许策略内部状态(如 last_signal_time);
|
|
13
|
+
- 公式逻辑移植自 FactorHub(pct_change 动量 + 量比),按本项目规范以
|
|
14
|
+
Canonical Definition + StrategyProtocol 实现,不复用其 pandas 向量化引擎。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from decimal import Decimal
|
|
18
|
+
|
|
19
|
+
from quant_core.app.data.schema.event_schema import BarEvent, TradingSessionEvent
|
|
20
|
+
from quant_core.app.factor.constants import (
|
|
21
|
+
FACTOR_MA_CLOSE_10,
|
|
22
|
+
FACTOR_MOMENTUM_20D,
|
|
23
|
+
FACTOR_VOLUME_RATIO_5D,
|
|
24
|
+
)
|
|
25
|
+
from quant_core.app.strategy.constants import (
|
|
26
|
+
AuthoringSource,
|
|
27
|
+
SignalSide,
|
|
28
|
+
SignalType,
|
|
29
|
+
StrategyStatus,
|
|
30
|
+
StrategyStyle,
|
|
31
|
+
StrategyType,
|
|
32
|
+
)
|
|
33
|
+
from quant_core.app.strategy.schema.signal_schema import Signal
|
|
34
|
+
from quant_core.app.strategy.schema.strategy_schema import RuleCondition, StrategyDefinition
|
|
35
|
+
from quant_core.app.strategy.strategy_context import StrategyContext
|
|
36
|
+
|
|
37
|
+
_IMPLEMENTATION_REF = (
|
|
38
|
+
"quant_core.app.strategy.strategies.momentum_volume_breakout.MomentumVolumeBreakoutStrategy"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MomentumVolumeBreakoutStrategy:
|
|
43
|
+
"""放量突破短线策略(long_only,A股 V1 语义)"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
universe: list[str],
|
|
48
|
+
momentum_threshold: float = 0.05,
|
|
49
|
+
volume_ratio_threshold: float = 1.5,
|
|
50
|
+
strategy_id: str = "momentum_volume_breakout",
|
|
51
|
+
version: str = "1",
|
|
52
|
+
) -> None:
|
|
53
|
+
self._universe = universe
|
|
54
|
+
self._momentum_threshold = Decimal(str(momentum_threshold))
|
|
55
|
+
self._volume_ratio_threshold = Decimal(str(volume_ratio_threshold))
|
|
56
|
+
self._definition = StrategyDefinition(
|
|
57
|
+
strategy_id=strategy_id,
|
|
58
|
+
version=version,
|
|
59
|
+
name="放量突破短线",
|
|
60
|
+
description="20 日动量突破且放量入场;跌破 10 日均线出场",
|
|
61
|
+
strategy_type=StrategyType.TIMING,
|
|
62
|
+
styles=[StrategyStyle.MOMENTUM.value, StrategyStyle.BREAKOUT.value],
|
|
63
|
+
universe=universe,
|
|
64
|
+
required_factors=[FACTOR_MOMENTUM_20D, FACTOR_VOLUME_RATIO_5D, FACTOR_MA_CLOSE_10],
|
|
65
|
+
entry_rules=[
|
|
66
|
+
RuleCondition(left=FACTOR_MOMENTUM_20D, operator="gt", right=momentum_threshold),
|
|
67
|
+
RuleCondition(
|
|
68
|
+
left=FACTOR_VOLUME_RATIO_5D, operator="gt", right=volume_ratio_threshold
|
|
69
|
+
),
|
|
70
|
+
],
|
|
71
|
+
exit_rules=[
|
|
72
|
+
RuleCondition(left="close", operator="cross_below", right=FACTOR_MA_CLOSE_10),
|
|
73
|
+
],
|
|
74
|
+
holding_rules=["long_only;单标的全仓进出(V1 demo,不含仓位管理)"],
|
|
75
|
+
screening_rules=None,
|
|
76
|
+
position_intent="long_only",
|
|
77
|
+
parameters={
|
|
78
|
+
"momentum_threshold": momentum_threshold,
|
|
79
|
+
"volume_ratio_threshold": volume_ratio_threshold,
|
|
80
|
+
},
|
|
81
|
+
status=StrategyStatus.DRAFT,
|
|
82
|
+
authoring_source=AuthoringSource.PYTHON,
|
|
83
|
+
created_by="p1-demo",
|
|
84
|
+
parent_version=None,
|
|
85
|
+
implementation_ref=_IMPLEMENTATION_REF,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def definition(self) -> StrategyDefinition:
|
|
89
|
+
return self._definition
|
|
90
|
+
|
|
91
|
+
async def on_bar(self, event: BarEvent, context: StrategyContext) -> list[Signal]:
|
|
92
|
+
instrument_id = event.payload.instrument_id
|
|
93
|
+
momentum = context.factor_value(FACTOR_MOMENTUM_20D, instrument_id)
|
|
94
|
+
volume_ratio = context.factor_value(FACTOR_VOLUME_RATIO_5D, instrument_id)
|
|
95
|
+
ma = context.factor_value(FACTOR_MA_CLOSE_10, instrument_id)
|
|
96
|
+
position = (
|
|
97
|
+
await context.portfolio.get_position(instrument_id)
|
|
98
|
+
if context.portfolio is not None
|
|
99
|
+
else None
|
|
100
|
+
)
|
|
101
|
+
held = position is not None and position.quantity > 0
|
|
102
|
+
|
|
103
|
+
entry_ok = (
|
|
104
|
+
not held
|
|
105
|
+
and momentum is not None
|
|
106
|
+
and volume_ratio is not None
|
|
107
|
+
and momentum > self._momentum_threshold
|
|
108
|
+
and volume_ratio > self._volume_ratio_threshold
|
|
109
|
+
)
|
|
110
|
+
if entry_ok:
|
|
111
|
+
return [
|
|
112
|
+
self._signal(
|
|
113
|
+
SignalType.ENTER,
|
|
114
|
+
instrument_id,
|
|
115
|
+
event.event_time,
|
|
116
|
+
reason=f"momentum={momentum} > {self._momentum_threshold} "
|
|
117
|
+
f"且 volume_ratio={volume_ratio} > {self._volume_ratio_threshold}",
|
|
118
|
+
factor_refs=[FACTOR_MOMENTUM_20D, FACTOR_VOLUME_RATIO_5D],
|
|
119
|
+
)
|
|
120
|
+
]
|
|
121
|
+
if held and ma is not None and event.payload.close < ma:
|
|
122
|
+
return [
|
|
123
|
+
self._signal(
|
|
124
|
+
SignalType.EXIT,
|
|
125
|
+
instrument_id,
|
|
126
|
+
event.event_time,
|
|
127
|
+
reason=f"close={event.payload.close} 跌破 {FACTOR_MA_CLOSE_10}={ma}",
|
|
128
|
+
factor_refs=[FACTOR_MA_CLOSE_10],
|
|
129
|
+
)
|
|
130
|
+
]
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
async def on_session(
|
|
134
|
+
self, event: TradingSessionEvent, context: StrategyContext
|
|
135
|
+
) -> list[Signal]:
|
|
136
|
+
return [] # V1 demo 不使用 Session 事件决策
|
|
137
|
+
|
|
138
|
+
def _signal(
|
|
139
|
+
self,
|
|
140
|
+
signal_type: SignalType,
|
|
141
|
+
instrument_id: str,
|
|
142
|
+
event_time,
|
|
143
|
+
reason: str,
|
|
144
|
+
factor_refs: list[str],
|
|
145
|
+
) -> Signal:
|
|
146
|
+
side = SignalSide.LONG if signal_type == SignalType.ENTER else SignalSide.NONE
|
|
147
|
+
return Signal(
|
|
148
|
+
strategy_id=self._definition.strategy_id,
|
|
149
|
+
strategy_version=self._definition.version,
|
|
150
|
+
instrument_id=instrument_id,
|
|
151
|
+
event_time=event_time,
|
|
152
|
+
signal_type=signal_type,
|
|
153
|
+
side=side,
|
|
154
|
+
score=None,
|
|
155
|
+
reason=reason,
|
|
156
|
+
factor_refs=factor_refs,
|
|
157
|
+
)
|