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,142 @@
|
|
|
1
|
+
"""等权目标组合及显式风险约束;资金占用、T+1 与成交归后续执行链。"""
|
|
2
|
+
|
|
3
|
+
from decimal import ROUND_DOWN, Decimal
|
|
4
|
+
|
|
5
|
+
from quant_core.app.factor.schema.selection_types import SelectionResult
|
|
6
|
+
from quant_core.app.strategy.schema.portfolio_target import PortfolioConstraints, TargetPortfolio
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PortfolioConstructionService:
|
|
10
|
+
def build(
|
|
11
|
+
self,
|
|
12
|
+
selection: SelectionResult,
|
|
13
|
+
constraints: PortfolioConstraints,
|
|
14
|
+
*,
|
|
15
|
+
current_weights: dict[str, Decimal] | None = None,
|
|
16
|
+
industries: dict[str, str] | None = None,
|
|
17
|
+
holding_sessions: dict[str, int] | None = None,
|
|
18
|
+
) -> TargetPortfolio:
|
|
19
|
+
"""约束后的未分配预算留现金,不隐式放松上限或补选证券。"""
|
|
20
|
+
if current_weights is not None and (
|
|
21
|
+
any(not weight.is_finite() or weight < 0 for weight in current_weights.values())
|
|
22
|
+
or sum(current_weights.values(), Decimal(0)) > 1
|
|
23
|
+
):
|
|
24
|
+
raise ValueError("当前账户权重非法")
|
|
25
|
+
if holding_sessions is not None and any(age < 0 for age in holding_sessions.values()):
|
|
26
|
+
raise ValueError("持仓交易日龄不得为负")
|
|
27
|
+
scores = {item.instrument_id: item.score for item in selection.selected}
|
|
28
|
+
scores.update(
|
|
29
|
+
{
|
|
30
|
+
item.instrument_id: item.score
|
|
31
|
+
for item in selection.excluded
|
|
32
|
+
if item.reason == "top_n" and item.score is not None
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
ranked = sorted(scores, key=lambda key: (-scores[key], key))
|
|
36
|
+
capacity = min(constraints.max_positions, len(selection.selected))
|
|
37
|
+
held = {key for key, weight in (current_weights or {}).items() if weight > 0}
|
|
38
|
+
reasons: list[str] = []
|
|
39
|
+
selected: list[str] = []
|
|
40
|
+
retention_enabled = (
|
|
41
|
+
constraints.retention_rank is not None
|
|
42
|
+
or constraints.max_replacements is not None
|
|
43
|
+
or constraints.min_holding_sessions > 0
|
|
44
|
+
)
|
|
45
|
+
if retention_enabled and current_weights is None:
|
|
46
|
+
raise ValueError("持仓保留规则需要当前真实账户权重")
|
|
47
|
+
if constraints.min_holding_sessions:
|
|
48
|
+
if holding_sessions is None or held - holding_sessions.keys():
|
|
49
|
+
raise ValueError("最低持有期需要真实持仓批次的交易日龄")
|
|
50
|
+
selected = sorted(
|
|
51
|
+
key for key in held if holding_sessions[key] < constraints.min_holding_sessions
|
|
52
|
+
)
|
|
53
|
+
reasons.extend(f"minimum_holding:{key}" for key in selected)
|
|
54
|
+
if len(selected) > constraints.max_positions:
|
|
55
|
+
raise ValueError("最低持有期与最大持仓数冲突")
|
|
56
|
+
capacity = max(capacity, len(selected))
|
|
57
|
+
retained = [key for key in ranked if key in held]
|
|
58
|
+
if constraints.retention_rank is not None:
|
|
59
|
+
for key in ranked[: constraints.retention_rank]:
|
|
60
|
+
if key in held and key not in selected and len(selected) < capacity:
|
|
61
|
+
selected.append(key)
|
|
62
|
+
reasons.append(f"retained_rank:{key}")
|
|
63
|
+
if constraints.max_replacements is not None:
|
|
64
|
+
keep_count = max(0, len(retained) - constraints.max_replacements)
|
|
65
|
+
for key in retained:
|
|
66
|
+
if len(set(selected) & held) >= keep_count or len(selected) >= capacity:
|
|
67
|
+
break
|
|
68
|
+
if key not in selected:
|
|
69
|
+
selected.append(key)
|
|
70
|
+
reasons.append(f"replacement_limit:{key}")
|
|
71
|
+
reasons.extend(
|
|
72
|
+
f"forced_exit_missing_or_filtered:{key}"
|
|
73
|
+
for key in sorted(held - set(ranked) - set(selected))
|
|
74
|
+
)
|
|
75
|
+
for key in ranked:
|
|
76
|
+
if len(selected) >= capacity:
|
|
77
|
+
break
|
|
78
|
+
if key not in selected:
|
|
79
|
+
selected.append(key)
|
|
80
|
+
weights: dict[str, Decimal] = {}
|
|
81
|
+
if selected:
|
|
82
|
+
per_stock = min(
|
|
83
|
+
(Decimal(1) - constraints.cash_buffer) / len(selected), constraints.max_weight
|
|
84
|
+
)
|
|
85
|
+
weights = {
|
|
86
|
+
key: per_stock.quantize(Decimal("1e-18"), rounding=ROUND_DOWN) for key in selected
|
|
87
|
+
}
|
|
88
|
+
if constraints.industry_caps:
|
|
89
|
+
if set(weights) - (industries or {}).keys():
|
|
90
|
+
raise ValueError("行业上限缺少证券行业信息")
|
|
91
|
+
for industry, cap in constraints.industry_caps:
|
|
92
|
+
members = [key for key in weights if (industries or {})[key] == industry]
|
|
93
|
+
total = sum((weights[key] for key in members), Decimal(0))
|
|
94
|
+
if total > cap:
|
|
95
|
+
for key in members:
|
|
96
|
+
weights[key] = (weights[key] * cap / total).quantize(
|
|
97
|
+
Decimal("1e-18"), rounding=ROUND_DOWN
|
|
98
|
+
)
|
|
99
|
+
reasons.append(f"industry_cap:{industry}")
|
|
100
|
+
if constraints.max_turnover is not None:
|
|
101
|
+
if current_weights is None:
|
|
102
|
+
raise ValueError("换手约束必须提供当前账户权重")
|
|
103
|
+
union = weights.keys() | current_weights.keys()
|
|
104
|
+
changes = {
|
|
105
|
+
key: weights.get(key, Decimal(0)) - current_weights.get(key, Decimal(0))
|
|
106
|
+
for key in union
|
|
107
|
+
}
|
|
108
|
+
turnover = max(
|
|
109
|
+
sum((max(delta, Decimal(0)) for delta in changes.values()), Decimal(0)),
|
|
110
|
+
sum((max(-delta, Decimal(0)) for delta in changes.values()), Decimal(0)),
|
|
111
|
+
)
|
|
112
|
+
if turnover > constraints.max_turnover:
|
|
113
|
+
scale = constraints.max_turnover / turnover
|
|
114
|
+
weights = {
|
|
115
|
+
key: current_weights.get(key, Decimal(0)) + change * scale
|
|
116
|
+
for key, change in changes.items()
|
|
117
|
+
}
|
|
118
|
+
reasons.append("turnover_cap")
|
|
119
|
+
weights = {key: weight for key, weight in weights.items() if weight > 0}
|
|
120
|
+
cash = Decimal(1) - sum(weights.values(), Decimal(0))
|
|
121
|
+
if (
|
|
122
|
+
len(weights) > constraints.max_positions
|
|
123
|
+
or any(weight > constraints.max_weight for weight in weights.values())
|
|
124
|
+
or cash < constraints.cash_buffer
|
|
125
|
+
):
|
|
126
|
+
raise ValueError("换手约束与持仓/现金上限冲突,无法构造合法目标")
|
|
127
|
+
for industry, cap in constraints.industry_caps:
|
|
128
|
+
if set(weights) - (industries or {}).keys():
|
|
129
|
+
raise ValueError("保留持仓缺少行业信息")
|
|
130
|
+
if (
|
|
131
|
+
sum(
|
|
132
|
+
(
|
|
133
|
+
weight
|
|
134
|
+
for key, weight in weights.items()
|
|
135
|
+
if (industries or {})[key] == industry
|
|
136
|
+
),
|
|
137
|
+
Decimal(0),
|
|
138
|
+
)
|
|
139
|
+
> cap
|
|
140
|
+
):
|
|
141
|
+
raise ValueError("换手约束与行业上限冲突")
|
|
142
|
+
return TargetPortfolio(tuple(sorted(weights.items())), cash, tuple(reasons))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""One loading path for persisted and temporary trusted Python strategies."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from quant_core.app.strategy.constants import StrategyType
|
|
7
|
+
from quant_core.app.strategy.schema.python_strategy_schema import PythonStrategySpec
|
|
8
|
+
from quant_core.app.strategy.schema.strategy_schema import StrategyDefinition
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PythonStrategy:
|
|
12
|
+
"""Adapt module-level callbacks to the existing Strategy Runtime contract."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, spec: PythonStrategySpec, universe: tuple[str, ...] = ()) -> None:
|
|
15
|
+
spec.validate_source()
|
|
16
|
+
self.spec = spec
|
|
17
|
+
self._universe = universe or spec.universe
|
|
18
|
+
namespace: dict[str, Any] = {"__name__": "strategy_user_code"}
|
|
19
|
+
exec(compile(spec.python_source, "<strategy>", "exec"), namespace)
|
|
20
|
+
self._callbacks = namespace
|
|
21
|
+
|
|
22
|
+
def definition(self) -> StrategyDefinition:
|
|
23
|
+
return StrategyDefinition(
|
|
24
|
+
strategy_id=self.spec.strategy_id,
|
|
25
|
+
version=self.spec.version,
|
|
26
|
+
name=self.spec.name,
|
|
27
|
+
description=self.spec.description,
|
|
28
|
+
strategy_type=StrategyType.EVENT_DRIVEN,
|
|
29
|
+
universe=list(self._universe),
|
|
30
|
+
required_factors=list(self.spec.resolved_factors()),
|
|
31
|
+
parameters=dict(self.spec.parameters),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
async def _call(self, name: str, *args: Any) -> list:
|
|
35
|
+
callback = self._callbacks.get(name)
|
|
36
|
+
if callback is None:
|
|
37
|
+
return []
|
|
38
|
+
result = callback(*args)
|
|
39
|
+
if inspect.isawaitable(result):
|
|
40
|
+
result = await result
|
|
41
|
+
return list(result or ())
|
|
42
|
+
|
|
43
|
+
async def on_bar(self, event, context) -> list:
|
|
44
|
+
return await self._call("on_bar", event, context)
|
|
45
|
+
|
|
46
|
+
async def init(self, context) -> list:
|
|
47
|
+
return await self._call("init", context)
|
|
48
|
+
|
|
49
|
+
async def on_slice(self, time_slice, context) -> list:
|
|
50
|
+
if "handle_bar" in self._callbacks:
|
|
51
|
+
from quant_core.app.data.schema.event_schema import BarEvent
|
|
52
|
+
|
|
53
|
+
if any(isinstance(event, BarEvent) for event in time_slice.events):
|
|
54
|
+
return await self._call("handle_bar", context, context.current_data)
|
|
55
|
+
return []
|
|
56
|
+
return await self._call("on_slice", time_slice, context)
|
|
57
|
+
|
|
58
|
+
async def on_session(self, event, context) -> list:
|
|
59
|
+
return await self._call("on_session", event, context)
|
|
60
|
+
|
|
61
|
+
async def before_trading(self, context) -> list:
|
|
62
|
+
return await self._call("before_trading", context)
|
|
63
|
+
|
|
64
|
+
async def after_trading(self, context) -> list:
|
|
65
|
+
return await self._call("after_trading", context)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Screener Service - 从 Universe 中筛选和排名候选标的
|
|
3
|
+
|
|
4
|
+
链路:Universe → Factor Values → Filter → Rank → Top N → ScreenerResult。
|
|
5
|
+
运行契约就是 Canonical `ScreenerSpec`(来自 StrategyDefinition.screening_rules)——
|
|
6
|
+
Definition 是 Runtime 的真实输入契约,不存在第二套临时 dict 模型。
|
|
7
|
+
纯内存运算:输入是已算好的 Canonical FactorValue(因子计算归属 FactorRuntime,
|
|
8
|
+
R2-1:唯一因子值形态),本 Service 不取数、不算因子。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from decimal import Decimal
|
|
12
|
+
|
|
13
|
+
from quant_core.app.factor.constants import FactorStatus as CanonicalFactorStatus
|
|
14
|
+
from quant_core.app.factor.schema.factor_contract import FactorValue
|
|
15
|
+
from quant_core.app.strategy.exceptions import StrategyValidationException
|
|
16
|
+
from quant_core.app.strategy.schema.screener_schema import (
|
|
17
|
+
ScreenerCandidate,
|
|
18
|
+
ScreenerRequest,
|
|
19
|
+
ScreenerResult,
|
|
20
|
+
)
|
|
21
|
+
from quant_core.app.strategy.schema.strategy_schema import RuleCondition
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def screen(request: ScreenerRequest) -> ScreenerResult:
|
|
25
|
+
"""过滤(全部满足)→ 按 spec.rank_by 降序 → 取前 spec.top_n
|
|
26
|
+
|
|
27
|
+
因子值缺失(None/非 AVAILABLE/未算)的标的被过滤掉,不参与排名。
|
|
28
|
+
"""
|
|
29
|
+
spec = request.spec
|
|
30
|
+
ranked: list[ScreenerCandidate] = []
|
|
31
|
+
for instrument_id in request.universe:
|
|
32
|
+
values: dict[str, Decimal | None] = {}
|
|
33
|
+
for factor_name in request.factor_values:
|
|
34
|
+
result = _find(request.factor_values[factor_name], instrument_id)
|
|
35
|
+
values[factor_name] = (
|
|
36
|
+
result.value
|
|
37
|
+
if result and result.status is CanonicalFactorStatus.AVAILABLE
|
|
38
|
+
else None
|
|
39
|
+
)
|
|
40
|
+
if not all(_satisfied(c, values) for c in spec.filters):
|
|
41
|
+
continue
|
|
42
|
+
score = values.get(spec.rank_by or "")
|
|
43
|
+
if score is None:
|
|
44
|
+
continue
|
|
45
|
+
ranked.append(
|
|
46
|
+
ScreenerCandidate(instrument_id=instrument_id, score=score, factor_values=values)
|
|
47
|
+
)
|
|
48
|
+
ranked.sort(key=lambda c: c.score, reverse=True)
|
|
49
|
+
return ScreenerResult(
|
|
50
|
+
event_time=request.event_time,
|
|
51
|
+
candidates=ranked[: spec.top_n],
|
|
52
|
+
metadata={"universe_size": len(request.universe), "filter_count": len(spec.filters)},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _satisfied(condition: RuleCondition, values: dict[str, Decimal | None]) -> bool:
|
|
57
|
+
"""单条 RuleCondition 求值(左值 = 因子值;右值仅支持数值字面量)"""
|
|
58
|
+
left = values.get(condition.left)
|
|
59
|
+
if left is None:
|
|
60
|
+
return False
|
|
61
|
+
if not isinstance(condition.right, (int, float)):
|
|
62
|
+
raise StrategyValidationException(f"V1 Screener 仅支持数值阈值: {condition.right}")
|
|
63
|
+
right = Decimal(str(condition.right))
|
|
64
|
+
op = condition.operator
|
|
65
|
+
if op == "gt":
|
|
66
|
+
return left > right
|
|
67
|
+
if op == "gte":
|
|
68
|
+
return left >= right
|
|
69
|
+
if op == "lt":
|
|
70
|
+
return left < right
|
|
71
|
+
if op == "lte":
|
|
72
|
+
return left <= right
|
|
73
|
+
raise StrategyValidationException(f"Screener 未支持的操作符: {op}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _find(results: list[FactorValue], instrument_id: str) -> FactorValue | None:
|
|
77
|
+
return next((r for r in results if r.instrument_id == instrument_id), None)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Strategy 消费已发布 Factor Store 的只读桥。
|
|
2
|
+
|
|
3
|
+
Factor 身份、修订和新鲜度由组装点指定;缺行显式返回 MISSING,
|
|
4
|
+
不会回退其他来源或版本,也不在 Strategy 内计算因子。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, time
|
|
8
|
+
from zoneinfo import ZoneInfo
|
|
9
|
+
|
|
10
|
+
from quant_core.app.factor.constants import FactorFreshness, FactorSourceKind, FactorStatus
|
|
11
|
+
from quant_core.app.factor.exceptions import InsufficientFactorHistoryError
|
|
12
|
+
from quant_core.app.factor.interface.public.factor_query import IFactorQuery
|
|
13
|
+
from quant_core.app.factor.schema.factor_contract import (
|
|
14
|
+
FactorIdentity,
|
|
15
|
+
FactorProvenance,
|
|
16
|
+
FactorValue,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_CN = ZoneInfo("Asia/Shanghai")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StoreFactorReader:
|
|
23
|
+
"""按显式身份读取 Canonical Factor Store 的 PIT 值。"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
query: IFactorQuery | None,
|
|
28
|
+
identities: dict[str, FactorIdentity],
|
|
29
|
+
*,
|
|
30
|
+
dataset_revision: str | None = None,
|
|
31
|
+
freshness: FactorFreshness = FactorFreshness.EXACT,
|
|
32
|
+
) -> None:
|
|
33
|
+
if identities and query is None:
|
|
34
|
+
raise ValueError("声明因子依赖时必须提供 Factor Query")
|
|
35
|
+
self._query = query
|
|
36
|
+
self._identities = dict(identities)
|
|
37
|
+
self._dataset_revision = dataset_revision
|
|
38
|
+
self._freshness = freshness
|
|
39
|
+
self._cache: dict[tuple[datetime, str, str], FactorValue] = {}
|
|
40
|
+
|
|
41
|
+
async def read_many(
|
|
42
|
+
self,
|
|
43
|
+
factor_names: tuple[str, ...],
|
|
44
|
+
instrument_ids: tuple[str, ...],
|
|
45
|
+
decision_time: datetime,
|
|
46
|
+
) -> dict[str, dict[str, FactorValue]]:
|
|
47
|
+
"""一次读取完整截面,缓存仅保留当前决策时点,避免按格查询。"""
|
|
48
|
+
unknown = set(factor_names) - self._identities.keys()
|
|
49
|
+
if unknown:
|
|
50
|
+
raise ValueError(f"因子未在 StoreFactorReader 显式声明身份: {sorted(unknown)}")
|
|
51
|
+
if not factor_names or not instrument_ids:
|
|
52
|
+
return {}
|
|
53
|
+
self._cache = {key: value for key, value in self._cache.items() if key[0] == decision_time}
|
|
54
|
+
missing = [
|
|
55
|
+
(name, iid)
|
|
56
|
+
for name in factor_names
|
|
57
|
+
for iid in instrument_ids
|
|
58
|
+
if (decision_time, name, iid) not in self._cache
|
|
59
|
+
]
|
|
60
|
+
if missing:
|
|
61
|
+
assert self._query is not None
|
|
62
|
+
identities = list(dict.fromkeys(self._identities[name] for name, _ in missing))
|
|
63
|
+
instruments = sorted({iid for _, iid in missing})
|
|
64
|
+
try:
|
|
65
|
+
snapshot = await self._query.get_snapshot(
|
|
66
|
+
identities,
|
|
67
|
+
instruments,
|
|
68
|
+
decision_time,
|
|
69
|
+
freshness=self._freshness,
|
|
70
|
+
dataset_revision=self._dataset_revision,
|
|
71
|
+
)
|
|
72
|
+
cells = {(row.identity, row.instrument_id): row for row in snapshot.values}
|
|
73
|
+
if len(cells) != len(snapshot.values):
|
|
74
|
+
raise ValueError("因子截面同格多行,拒绝隐式覆盖")
|
|
75
|
+
except InsufficientFactorHistoryError:
|
|
76
|
+
cells = {}
|
|
77
|
+
for name, iid in missing:
|
|
78
|
+
identity = self._identities[name]
|
|
79
|
+
value = cells.get((identity, iid))
|
|
80
|
+
self._cache[(decision_time, name, iid)] = (
|
|
81
|
+
value if value is not None else self._missing_row(identity, iid, decision_time)
|
|
82
|
+
)
|
|
83
|
+
return {
|
|
84
|
+
name: {iid: self._cache[(decision_time, name, iid)] for iid in instrument_ids}
|
|
85
|
+
for name in factor_names
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async def read(
|
|
89
|
+
self,
|
|
90
|
+
factor_name: str,
|
|
91
|
+
instrument_id: str,
|
|
92
|
+
decision_time: datetime,
|
|
93
|
+
) -> FactorValue:
|
|
94
|
+
"""单格查询复用批量查询契约。"""
|
|
95
|
+
rows = await self.read_many((factor_name,), (instrument_id,), decision_time)
|
|
96
|
+
return rows[factor_name][instrument_id]
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _missing_row(
|
|
100
|
+
identity: FactorIdentity, instrument_id: str, event_time: datetime
|
|
101
|
+
) -> FactorValue:
|
|
102
|
+
"""Store 无行的显式缺失行(value=None;不冒充任何 Provider 产出)
|
|
103
|
+
|
|
104
|
+
as_of = available_at 取决策日 CN 零点,区别于真实日线归属时间。
|
|
105
|
+
"""
|
|
106
|
+
midnight = datetime.combine(event_time.astimezone(_CN).date(), time.min, tzinfo=_CN)
|
|
107
|
+
return FactorValue(
|
|
108
|
+
identity=identity,
|
|
109
|
+
instrument_id=instrument_id,
|
|
110
|
+
as_of=midnight,
|
|
111
|
+
available_at=midnight,
|
|
112
|
+
value=None,
|
|
113
|
+
status=FactorStatus.MISSING,
|
|
114
|
+
provenance=FactorProvenance(
|
|
115
|
+
source_kind=FactorSourceKind.EXTERNAL_PROVIDER,
|
|
116
|
+
provider_id="factor-store",
|
|
117
|
+
note="store 无该格数据行(合成 MISSING,非 Provider 产出)",
|
|
118
|
+
dataset_revision=None,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Strategy 专用价格视图:锁定决策时点动态前复权,禁止旁路读取 raw 历史。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
|
|
6
|
+
from quant_core.app.data.interface.market_data_query import IMarketDataQuery
|
|
7
|
+
from quant_core.app.data.schema.adjustment_schema import (
|
|
8
|
+
AdjustedBarSeries,
|
|
9
|
+
AdjustmentAnchorMode,
|
|
10
|
+
AdjustmentType,
|
|
11
|
+
BarViewRequest,
|
|
12
|
+
)
|
|
13
|
+
from quant_core.app.data.schema.market_schema import (
|
|
14
|
+
BarResponse,
|
|
15
|
+
InstrumentResponse,
|
|
16
|
+
TradingCalendarDayResponse,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StrategyMarketView:
|
|
21
|
+
"""把 IMarketDataQuery 收窄为当前决策时点的 Strategy 观察面。"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
market: IMarketDataQuery,
|
|
26
|
+
decision_time: datetime,
|
|
27
|
+
adjustment_type: AdjustmentType,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._market = market
|
|
30
|
+
self._decision_time = decision_time
|
|
31
|
+
self._adjustment_type = adjustment_type
|
|
32
|
+
|
|
33
|
+
async def get_bars(
|
|
34
|
+
self,
|
|
35
|
+
instrument_id: str,
|
|
36
|
+
frequency: str,
|
|
37
|
+
start: datetime | None = None,
|
|
38
|
+
end: datetime | None = None,
|
|
39
|
+
limit: int = 1000,
|
|
40
|
+
) -> Sequence[BarResponse]:
|
|
41
|
+
if end is not None and end > self._decision_time:
|
|
42
|
+
raise ValueError("Strategy 行情窗口 end 不得晚于当前 decision_time")
|
|
43
|
+
if self._adjustment_type is AdjustmentType.NONE:
|
|
44
|
+
return await self._market.get_bars(
|
|
45
|
+
instrument_id, frequency, start, end or self._decision_time, limit
|
|
46
|
+
)
|
|
47
|
+
series = await self.get_adjusted_bars(
|
|
48
|
+
instrument_id,
|
|
49
|
+
frequency,
|
|
50
|
+
BarViewRequest(
|
|
51
|
+
adjustment_type=self._adjustment_type,
|
|
52
|
+
anchor_mode=AdjustmentAnchorMode.DECISION_TIME,
|
|
53
|
+
decision_time=self._decision_time,
|
|
54
|
+
),
|
|
55
|
+
start,
|
|
56
|
+
end or self._decision_time,
|
|
57
|
+
limit,
|
|
58
|
+
)
|
|
59
|
+
return series.bars
|
|
60
|
+
|
|
61
|
+
async def get_adjusted_bars(
|
|
62
|
+
self,
|
|
63
|
+
instrument_id: str,
|
|
64
|
+
frequency: str,
|
|
65
|
+
view: BarViewRequest,
|
|
66
|
+
start: datetime | None = None,
|
|
67
|
+
end: datetime | None = None,
|
|
68
|
+
limit: int = 1000,
|
|
69
|
+
) -> AdjustedBarSeries:
|
|
70
|
+
if view.adjustment_type is not self._adjustment_type:
|
|
71
|
+
raise ValueError("Strategy 不得覆盖 Backtest 锁定的 adjustment_type")
|
|
72
|
+
if view.anchor_mode is not AdjustmentAnchorMode.DECISION_TIME:
|
|
73
|
+
raise ValueError("Strategy 回测只允许 DECISION_TIME 动态锚点")
|
|
74
|
+
if view.decision_time != self._decision_time:
|
|
75
|
+
raise ValueError("Strategy 不得覆盖当前 decision_time")
|
|
76
|
+
if end is not None and end > self._decision_time:
|
|
77
|
+
raise ValueError("Strategy 行情窗口 end 不得晚于当前 decision_time")
|
|
78
|
+
return await self._market.get_adjusted_bars(
|
|
79
|
+
instrument_id,
|
|
80
|
+
frequency,
|
|
81
|
+
view,
|
|
82
|
+
start,
|
|
83
|
+
end or self._decision_time,
|
|
84
|
+
limit,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def get_instrument(self, instrument_id: str) -> InstrumentResponse:
|
|
88
|
+
item = await self._market.get_instrument(instrument_id)
|
|
89
|
+
if item.listed_date is not None and item.listed_date > self._decision_time.date():
|
|
90
|
+
raise ValueError("当前决策时点不可见未来上市标的")
|
|
91
|
+
return item
|
|
92
|
+
|
|
93
|
+
async def list_instruments_on(
|
|
94
|
+
self, on_date: date, limit: int = 10000
|
|
95
|
+
) -> list[InstrumentResponse]:
|
|
96
|
+
if on_date > self._decision_time.date():
|
|
97
|
+
raise ValueError("股票池日期不得晚于当前决策日期")
|
|
98
|
+
return await self._market.list_instruments_on(on_date, limit)
|
|
99
|
+
|
|
100
|
+
async def search_instruments(
|
|
101
|
+
self,
|
|
102
|
+
keyword: str | None = None,
|
|
103
|
+
instrument_type: str | None = None,
|
|
104
|
+
exchange: str | None = None,
|
|
105
|
+
limit: int = 100,
|
|
106
|
+
) -> list[InstrumentResponse]:
|
|
107
|
+
items = await self._market.search_instruments(keyword, instrument_type, exchange, limit)
|
|
108
|
+
return [
|
|
109
|
+
item
|
|
110
|
+
for item in items
|
|
111
|
+
if item.listed_date is None or item.listed_date <= self._decision_time.date()
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
async def is_trading_day(self, exchange: str, trade_date: date) -> bool:
|
|
115
|
+
return await self._market.is_trading_day(exchange, trade_date)
|
|
116
|
+
|
|
117
|
+
async def list_trading_days(
|
|
118
|
+
self, exchange: str, start: date, end: date
|
|
119
|
+
) -> list[TradingCalendarDayResponse]:
|
|
120
|
+
return await self._market.list_trading_days(exchange, start, end)
|