powerquant-cli 0.2.6__tar.gz
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/.env.example +7 -0
- powerquant_cli-0.2.6/.gitignore +22 -0
- powerquant_cli-0.2.6/LICENSE +36 -0
- powerquant_cli-0.2.6/PKG-INFO +62 -0
- powerquant_cli-0.2.6/README.md +41 -0
- powerquant_cli-0.2.6/bundled/pqdata/__init__.py +6 -0
- powerquant_cli-0.2.6/bundled/pqdata/client.py +389 -0
- powerquant_cli-0.2.6/bundled/pqdata/download.py +60 -0
- powerquant_cli-0.2.6/bundled/pqdata/exceptions.py +14 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/__init__.py +1 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/adjustment_schema.py +67 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/bar_content_schema.py +24 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/bar_schema.py +56 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/corporate_action_schema.py +46 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/history_schema.py +61 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/pit_daily_window.py +16 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/query_schema.py +53 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/quote_schema.py +73 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/reference_schema.py +48 -0
- powerquant_cli-0.2.6/bundled/pqdata/schema/reliability_schema.py +152 -0
- powerquant_cli-0.2.6/bundled/quant_core/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/SPEC.md +18 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/SPEC.md +65 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/__init__.py +1 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/backtest_input_schema.py +20 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/backtest_run.py +393 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/data_guard.py +77 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/historical_clock.py +26 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/legacy_result_schema.py +34 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/metrics.py +194 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/performance_report.py +61 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/report_schema.py +64 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/schema.py +477 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/service/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/backtest/service/backtest_execution_service.py +114 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/constants.py +284 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/domain/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/domain/trading_quantity_value.py +57 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/exceptions.py +95 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/interface/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/interface/adjustment_factor_query.py +21 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/interface/market_data_query.py +71 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/interface/market_data_verification.py +20 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/interface/market_event_source.py +11 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/interface/market_revision_query.py +8 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/interface/market_runtime_repository.py +44 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/interface/market_time_slice_source.py +11 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/repository/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/__init__.py +1 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/adjustment_schema.py +67 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/corporate_action_schema.py +46 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/event_schema.py +506 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/exchange_operating_state.py +37 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/exchange_state_schema.py +106 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/history_schema.py +48 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/instrument_identity.py +69 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/market_schema.py +128 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/pit_daily_window.py +16 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/quote_schema.py +73 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/reliability_schema.py +152 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/snapshot_schema.py +82 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/schema/trading_state_schema.py +211 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/service/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/service/calendar_coverage_service.py +63 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/service/prepared_data_verification_service.py +40 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/service/prepared_market_service.py +189 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/data/service/price_adjustment_service.py +235 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/LICENSE.vnpy +21 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/__init__.py +13 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/builtin/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/builtin/alpha101_recipe.py +1095 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/builtin/alpha158_recipe.py +1284 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/builtin/factor_recipe_catalog.py +100 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/factor_batch_evaluator.py +141 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/factor_expression.py +106 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/factor_source_loader.py +24 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/operators/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/operators/arithmetic_operator.py +182 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/operators/cross_section_operator.py +66 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/operators/time_series_operator.py +342 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/panel_evaluator.py +59 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/panel_expression.py +193 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/python_factor_executor.py +87 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/computation/universe_filter.py +25 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/constants.py +114 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/exceptions.py +35 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/factor_dependency.py +65 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/factor_compute.py +12 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/factor_execution.py +15 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/factor_local_data.py +42 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/factor_panel_compute.py +18 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/factor_panel_input.py +12 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/factor_query_store.py +39 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/public/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/public/factor_diagnostics.py +18 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/public/factor_exposure_query.py +12 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/public/factor_local.py +15 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/public/factor_query.py +57 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/interface/public/factor_selection.py +20 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/model/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/repository/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/__init__.py +1 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/diagnostic_types.py +42 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/factor_artifact_schema.py +36 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/factor_computation_schema.py +102 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/factor_compute_request.py +24 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/factor_contract.py +257 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/factor_input_schema.py +32 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/factor_panel.py +65 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/factor_publication_schema.py +41 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/factor_query_result_schema.py +12 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/factor_result_schema.py +6 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/local_factor_schema.py +42 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/panel_policy.py +13 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/selection_report.py +116 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/schema/selection_types.py +135 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/__init__.py +1 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/factor_compute_service.py +140 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/factor_diagnostic_runner.py +134 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/factor_diagnostics_service.py +140 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/factor_exposure_service.py +24 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/factor_input_service.py +258 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/factor_local_execution_service.py +51 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/factor_panel_service.py +86 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/factor_runtime_service.py +306 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/factor_selection_runner.py +88 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/published_factor_execution_service.py +160 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/service/selection_service.py +234 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/factor/visibility.py +22 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/opportunity/SPEC.md +45 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/opportunity/__init__.py +1 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/SPEC.md +64 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/__init__.py +1 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/bar_matcher.py +55 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/constants.py +142 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/domain/__init__.py +45 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/domain/execution_batch.py +108 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/domain/order_entity.py +197 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/domain/order_event.py +87 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/domain/order_policy.py +29 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/domain/order_value.py +142 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/fee_model.py +60 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/interface/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/interface/order_account.py +10 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/interface/order_trading_rules.py +10 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/interface/public/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/interface/public/execution_account.py +53 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/interface/public/order_events.py +19 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/interface/public/order_execution.py +107 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/interface/public/order_runtime.py +30 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/interface/public/order_submission.py +17 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/order_event_bus.py +28 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/order_execution_dependency.py +48 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/order_submission_service.py +301 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/schema/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/schema/order_request_schema.py +88 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/simulation_broker.py +883 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/slippage_model.py +40 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/tradability_policy.py +174 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/order_execution/validators.py +40 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/SPEC.md +69 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/domain/__init__.py +32 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/domain/entitlement_entity.py +45 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/domain/portfolio_entity.py +511 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/domain/portfolio_event.py +59 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/domain/portfolio_value.py +21 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/domain/position_entity.py +165 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/domain/reservation_entity.py +69 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/interface/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/interface/public/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/interface/public/portfolio.py +77 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/interface/public/simulation_account.py +16 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/ledger_schema.py +130 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/order_account_listener.py +36 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/portfolio_dependency.py +21 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/portfolio_schema.py +204 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/portfolio/simulated_portfolio.py +427 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/SPEC.md +57 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/adapter/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/adapter/market_label_query.py +84 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/interface/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/interface/research_dataset.py +47 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/interface/research_evaluation.py +19 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/schema/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/schema/comparison_schema.py +12 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/schema/dataset_schema.py +112 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/schema/research_schema.py +120 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/service/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/service/research_dataset_service.py +80 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/service/research_evaluation_service.py +56 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/research/service/research_service.py +171 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/SPEC.md +215 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/__init__.py +1 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/constants.py +87 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/exceptions.py +32 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/interface/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/interface/public/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/interface/public/portfolio_construction.py +19 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/interface/public/strategy.py +30 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/interface/public/strategy_market_query.py +53 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/interface/public/strategy_portfolio_reader.py +33 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/interface/strategy_factor_reader.py +19 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/interface/strategy_order_reader.py +14 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/schema/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/schema/portfolio_schema.py +28 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/schema/portfolio_target.py +70 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/schema/python_strategy_schema.py +94 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/schema/screener_schema.py +51 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/schema/signal_schema.py +33 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/schema/strategy_publication_schema.py +32 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/schema/strategy_schema.py +96 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/service/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/service/factor_top_n_strategy.py +103 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/service/portfolio_construction_service.py +142 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/service/python_strategy_service.py +65 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/service/screener_service.py +77 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/service/store_factor_reader.py +120 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/service/strategy_market_view.py +120 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/service/strategy_runtime_service.py +319 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/service/strategy_scheduler.py +50 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/strategies/__init__.py +7 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/strategies/momentum_volume_breakout.py +157 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/strategy_context.py +316 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/strategy/strategy_dependency.py +101 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/SPEC.md +70 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/__init__.py +1 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/domain/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/replay/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/replay/daily_state_expand.py +49 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/replay/exchange_state_resolver.py +90 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/replay/market_runtime_service.py +573 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/replay/market_state_resolver.py +150 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/runtime_ports.py +64 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/runtime_schema.py +129 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/runtime_session.py +361 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/strategy_portfolio_adapter.py +58 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/trading_rule_book.py +37 -0
- powerquant_cli-0.2.6/bundled/quant_core/app/trading_runtime/trading_rule_query.py +12 -0
- powerquant_cli-0.2.6/bundled/quant_core/common/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/common/exceptions/__init__.py +0 -0
- powerquant_cli-0.2.6/bundled/quant_core/common/exceptions/base.py +89 -0
- powerquant_cli-0.2.6/bundled/quant_core/common/local_run.py +45 -0
- powerquant_cli-0.2.6/examples/cached_factor_screen.py +165 -0
- powerquant_cli-0.2.6/examples/factors/ma_close_10.py +10 -0
- powerquant_cli-0.2.6/examples/factors/max_drawdown_20d.py +18 -0
- powerquant_cli-0.2.6/examples/factors/momentum_20d.py +13 -0
- powerquant_cli-0.2.6/examples/factors/multi_intraday_screen.py +16 -0
- powerquant_cli-0.2.6/examples/factors/risk_adjusted_momentum_20d.py +18 -0
- powerquant_cli-0.2.6/examples/factors/volume_ratio_5d.py +14 -0
- powerquant_cli-0.2.6/examples/factors/volume_weighted_close_pressure.py +14 -0
- powerquant_cli-0.2.6/examples/full_market_screen.py +189 -0
- powerquant_cli-0.2.6/examples/local_quant_validation.py +196 -0
- powerquant_cli-0.2.6/examples/published_definition_validation.py +219 -0
- powerquant_cli-0.2.6/examples/research/factor_pit_coverage.py +87 -0
- powerquant_cli-0.2.6/examples/research/ipo_first_day.py +348 -0
- powerquant_cli-0.2.6/examples/selection/README.md +59 -0
- powerquant_cli-0.2.6/examples/selection/backtest.json +18 -0
- powerquant_cli-0.2.6/examples/selection/buy_hold.py +7 -0
- powerquant_cli-0.2.6/examples/selection/compute.json +15 -0
- powerquant_cli-0.2.6/examples/selection/prepare.json +13 -0
- powerquant_cli-0.2.6/examples/selection/select.json +44 -0
- powerquant_cli-0.2.6/examples/strategies/bar_roundtrip.py +12 -0
- powerquant_cli-0.2.6/examples/strategies/buy_and_hold.py +11 -0
- powerquant_cli-0.2.6/examples/strategies/contrarian_timing.py +14 -0
- powerquant_cli-0.2.6/examples/strategies/factor_gate_vwap.py +22 -0
- powerquant_cli-0.2.6/examples/strategies/ipo_next_day_vwap.py +31 -0
- powerquant_cli-0.2.6/examples/strategies/momentum_timing.py +14 -0
- powerquant_cli-0.2.6/examples/strategies/monthly_equal_weight.py +19 -0
- powerquant_cli-0.2.6/examples/strategies/session_roundtrip.py +16 -0
- powerquant_cli-0.2.6/examples/strategies/trend_drawdown_guard.py +22 -0
- powerquant_cli-0.2.6/examples/strategies/weekly_risk_adjusted_rotation.py +61 -0
- powerquant_cli-0.2.6/examples/unified_snapshot_validation.py +260 -0
- powerquant_cli-0.2.6/hatch_build.py +22 -0
- powerquant_cli-0.2.6/licenses/LICENSE.vnpy +21 -0
- powerquant_cli-0.2.6/pyproject.toml +84 -0
- powerquant_cli-0.2.6/src/quant_cli/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/api.py +39 -0
- powerquant_cli-0.2.6/src/quant_cli/app/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/backtest/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/backtest/cli/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/backtest/cli/local_backtest_cli.py +72 -0
- powerquant_cli-0.2.6/src/quant_cli/app/backtest/local_backtest_dependency.py +10 -0
- powerquant_cli-0.2.6/src/quant_cli/app/backtest/local_backtest_schema.py +49 -0
- powerquant_cli-0.2.6/src/quant_cli/app/backtest/repository/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/backtest/repository/backtest_local_repository.py +67 -0
- powerquant_cli-0.2.6/src/quant_cli/app/backtest/service/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/backtest/service/local_backtest_service.py +271 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/cli/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/cli/data_cli.py +78 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/data_dependency.py +35 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/repository/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/repository/parquet_repository.py +124 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/schema/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/schema/dataset_audit_schema.py +37 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/schema/dataset_request_schema.py +28 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/schema/parquet_file_schema.py +17 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/service/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/service/bar_cache_service.py +287 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/service/dataset_audit_service.py +137 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/service/dataset_load_service.py +115 -0
- powerquant_cli-0.2.6/src/quant_cli/app/data/service/dataset_prepare_service.py +187 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/cli/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/cli/factor_definition_cli.py +81 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/cli/local_factor_cli.py +102 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/cli/local_selection_cli.py +58 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/interface/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/interface/local_selection_store.py +12 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/local_factor_dependency.py +38 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/repository/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/repository/factor_local_repository.py +154 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/repository/selection_local_repository.py +67 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/schema/local_selection_schema.py +56 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/service/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/service/factor_data_prepare_service.py +181 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/service/local_factor_query_service.py +146 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/service/local_factor_service.py +120 -0
- powerquant_cli-0.2.6/src/quant_cli/app/factor/service/local_selection_service.py +139 -0
- powerquant_cli-0.2.6/src/quant_cli/app/job/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/job/cli/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/job/cli/local_job_cli.py +69 -0
- powerquant_cli-0.2.6/src/quant_cli/app/job/schema/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/job/schema/local_job_schema.py +11 -0
- powerquant_cli-0.2.6/src/quant_cli/app/job/service/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/job/service/local_job_service.py +60 -0
- powerquant_cli-0.2.6/src/quant_cli/app/quant_cli.py +49 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/cli/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/cli/local_research_cli.py +93 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/local_research_dependency.py +10 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/repository/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/repository/research_local_repository.py +67 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/schema/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/schema/local_research_schema.py +26 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/service/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/service/local_research_execution_service.py +105 -0
- powerquant_cli-0.2.6/src/quant_cli/app/research/service/local_research_service.py +101 -0
- powerquant_cli-0.2.6/src/quant_cli/app/strategy/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/strategy/cli/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/app/strategy/cli/strategy_cli.py +104 -0
- powerquant_cli-0.2.6/src/quant_cli/common/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/common/constants.py +7 -0
- powerquant_cli-0.2.6/src/quant_cli/common/database/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/common/database/duckdb_connection.py +30 -0
- powerquant_cli-0.2.6/src/quant_cli/common/http/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/common/http/definition_client.py +54 -0
- powerquant_cli-0.2.6/src/quant_cli/common/utils/__init__.py +0 -0
- powerquant_cli-0.2.6/src/quant_cli/common/utils/cli_utils.py +37 -0
- powerquant_cli-0.2.6/src/quant_cli/common/utils/platform_utils.py +11 -0
- powerquant_cli-0.2.6/src/quant_cli/common/utils/runtime_environment.py +12 -0
- powerquant_cli-0.2.6/tests/__init__.py +0 -0
- powerquant_cli-0.2.6/tests/conftest.py +10 -0
- powerquant_cli-0.2.6/tests/factor_fixtures.py +51 -0
- powerquant_cli-0.2.6/tests/integration/__init__.py +0 -0
- powerquant_cli-0.2.6/tests/integration/test_local_quant_storage.py +176 -0
- powerquant_cli-0.2.6/tests/integration/test_local_selection.py +234 -0
- powerquant_cli-0.2.6/tests/unit/__init__.py +1 -0
- powerquant_cli-0.2.6/tests/unit/test_alpha_backtest_reports.py +59 -0
- powerquant_cli-0.2.6/tests/unit/test_alpha_identity.py +47 -0
- powerquant_cli-0.2.6/tests/unit/test_alpha_market_input.py +65 -0
- powerquant_cli-0.2.6/tests/unit/test_alpha_research.py +226 -0
- powerquant_cli-0.2.6/tests/unit/test_alpha_top_n.py +42 -0
- powerquant_cli-0.2.6/tests/unit/test_backtest_run_result.py +1160 -0
- powerquant_cli-0.2.6/tests/unit/test_bar_cache.py +203 -0
- powerquant_cli-0.2.6/tests/unit/test_coarse_execution_capability.py +409 -0
- powerquant_cli-0.2.6/tests/unit/test_corporate_action_replay.py +62 -0
- powerquant_cli-0.2.6/tests/unit/test_d1_replay_execution_chain.py +244 -0
- powerquant_cli-0.2.6/tests/unit/test_daily_state_expand.py +313 -0
- powerquant_cli-0.2.6/tests/unit/test_daily_state_snapshot.py +220 -0
- powerquant_cli-0.2.6/tests/unit/test_daily_vwap_execution.py +57 -0
- powerquant_cli-0.2.6/tests/unit/test_data_guard.py +72 -0
- powerquant_cli-0.2.6/tests/unit/test_dataset_audit.py +246 -0
- powerquant_cli-0.2.6/tests/unit/test_domain_dependency_boundaries.py +39 -0
- powerquant_cli-0.2.6/tests/unit/test_domain_interface_boundary.py +66 -0
- powerquant_cli-0.2.6/tests/unit/test_exchange_state_execution.py +211 -0
- powerquant_cli-0.2.6/tests/unit/test_exchange_state_replay.py +413 -0
- powerquant_cli-0.2.6/tests/unit/test_exchange_state_resolver.py +144 -0
- powerquant_cli-0.2.6/tests/unit/test_exchange_state_schema.py +151 -0
- powerquant_cli-0.2.6/tests/unit/test_factor_contract.py +449 -0
- powerquant_cli-0.2.6/tests/unit/test_factor_diagnostic_runner.py +111 -0
- powerquant_cli-0.2.6/tests/unit/test_factor_diagnostics.py +30 -0
- powerquant_cli-0.2.6/tests/unit/test_factor_expression.py +107 -0
- powerquant_cli-0.2.6/tests/unit/test_factor_local_runner.py +45 -0
- powerquant_cli-0.2.6/tests/unit/test_factor_runtime_service.py +498 -0
- powerquant_cli-0.2.6/tests/unit/test_factor_selection.py +177 -0
- powerquant_cli-0.2.6/tests/unit/test_factor_selection_runner.py +219 -0
- powerquant_cli-0.2.6/tests/unit/test_ipo_vwap_study.py +149 -0
- powerquant_cli-0.2.6/tests/unit/test_local_quant_boundary.py +133 -0
- powerquant_cli-0.2.6/tests/unit/test_market_replay.py +586 -0
- powerquant_cli-0.2.6/tests/unit/test_market_trading_state.py +773 -0
- powerquant_cli-0.2.6/tests/unit/test_order_domain.py +140 -0
- powerquant_cli-0.2.6/tests/unit/test_order_submission.py +185 -0
- powerquant_cli-0.2.6/tests/unit/test_portfolio_construction.py +83 -0
- powerquant_cli-0.2.6/tests/unit/test_portfolio_contracts.py +325 -0
- powerquant_cli-0.2.6/tests/unit/test_python_strategy_runtime.py +80 -0
- powerquant_cli-0.2.6/tests/unit/test_replay_state_boundaries.py +592 -0
- powerquant_cli-0.2.6/tests/unit/test_risk_adjusted_strategy_example.py +100 -0
- powerquant_cli-0.2.6/tests/unit/test_simulated_order_execution.py +1603 -0
- powerquant_cli-0.2.6/tests/unit/test_simulated_portfolio.py +961 -0
- powerquant_cli-0.2.6/tests/unit/test_store_factor_reader.py +209 -0
- powerquant_cli-0.2.6/tests/unit/test_strategy_adjusted_view.py +122 -0
- powerquant_cli-0.2.6/tests/unit/test_strategy_domain.py +946 -0
- powerquant_cli-0.2.6/tests/unit/test_strategy_lifecycle.py +98 -0
- powerquant_cli-0.2.6/tests/unit/test_tradability_policy.py +539 -0
- powerquant_cli-0.2.6/tests/unit/test_trading_runtime_contracts.py +262 -0
- powerquant_cli-0.2.6/tests/unit/test_trading_runtime_session.py +294 -0
- powerquant_cli-0.2.6/uv.lock +793 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# CLI uses HTTP endpoints and local workspace; no database credentials.
|
|
2
|
+
PQ_QUANT_URL=http://localhost:8001/api/v1
|
|
3
|
+
PQ_QUANT_TOKEN=
|
|
4
|
+
# 默认 ~/.powerquant/workspace.duckdb;需要覆盖时设置 PQ_LOCAL_DATABASE
|
|
5
|
+
# PQ_LOCAL_DATABASE=/path/to/workspace.duckdb
|
|
6
|
+
MARKET_BASE_URL=http://localhost:8002/api/v1
|
|
7
|
+
MARKET_API_TOKEN=
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 运行时日志与文件锁(应用启动时自动创建 logs/ 目录)
|
|
2
|
+
logs/
|
|
3
|
+
|
|
4
|
+
# 本地环境配置(含数据库连接串、密钥等),只提交 .env.example
|
|
5
|
+
.env
|
|
6
|
+
.env.*
|
|
7
|
+
!.env.example
|
|
8
|
+
|
|
9
|
+
# Python 运行时产物
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.py[cod]
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.cache/
|
|
15
|
+
.venv/
|
|
16
|
+
dist/
|
|
17
|
+
build/
|
|
18
|
+
tasks/draft/TASK-20260906-1704-akshare-minute-ingestion/samples/
|
|
19
|
+
tasks/approved/TASK-20260906-2119-real-market-integration-review-fixes/samples/
|
|
20
|
+
|
|
21
|
+
node_modules/
|
|
22
|
+
apps/quant_frontend/public/data-dictionary.md
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Power Quant Proprietary License
|
|
2
|
+
Copyright (c) 2026 Power Quant contributors. All rights reserved.
|
|
3
|
+
|
|
4
|
+
1. Permission to use
|
|
5
|
+
You may install and use this software, inspect its source code, and make
|
|
6
|
+
local copies and modifications for your own use. Permitted uses include
|
|
7
|
+
connecting to Power Quant services and performing research and backtesting.
|
|
8
|
+
Commercial use of the software is permitted under these same conditions.
|
|
9
|
+
|
|
10
|
+
2. Redistribution and resale
|
|
11
|
+
Unless you have prior written authorization from the copyright holders,
|
|
12
|
+
you may not redistribute, sublicense, publish, or resell this software or
|
|
13
|
+
its modified versions, including as part of another software product.
|
|
14
|
+
The copyright holders may distribute the software through their designated
|
|
15
|
+
release channels, including PyPI. This does not authorize recipients to
|
|
16
|
+
redistribute it.
|
|
17
|
+
|
|
18
|
+
3. Service access and results
|
|
19
|
+
This license does not grant access to Power Quant services or market data.
|
|
20
|
+
Service access requires a valid access token and remains subject to the
|
|
21
|
+
applicable service and data terms. These redistribution restrictions apply
|
|
22
|
+
to the software, not to your own research code or results; rights in market
|
|
23
|
+
data remain governed by the applicable data terms.
|
|
24
|
+
|
|
25
|
+
4. Third-party components
|
|
26
|
+
Third-party components retain their own licenses and copyright notices.
|
|
27
|
+
The restrictions above do not override rights granted by those licenses.
|
|
28
|
+
In particular, components identified as derived from vn.py remain subject
|
|
29
|
+
to the accompanying MIT license. Separately installed dependencies are
|
|
30
|
+
subject to their own licenses.
|
|
31
|
+
|
|
32
|
+
5. Disclaimer
|
|
33
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
34
|
+
OR IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
35
|
+
AND NON-INFRINGEMENT. TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE
|
|
36
|
+
COPYRIGHT HOLDERS ARE NOT LIABLE FOR DAMAGES ARISING FROM ITS USE.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: powerquant-cli
|
|
3
|
+
Version: 0.2.6
|
|
4
|
+
Summary: Power Quant local research CLI
|
|
5
|
+
Project-URL: Repository, https://github.com/Kepler452Byte/power-quant
|
|
6
|
+
License-Expression: LicenseRef-PowerQuant-Proprietary AND MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
License-File: licenses/LICENSE.vnpy
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: duckdb<2,>=1.4
|
|
11
|
+
Requires-Dist: filelock<4,>=3.20
|
|
12
|
+
Requires-Dist: httpx<1,>=0.28
|
|
13
|
+
Requires-Dist: numpy<3,>=2
|
|
14
|
+
Requires-Dist: pandas<4,>=3
|
|
15
|
+
Requires-Dist: polars<2,>=1.36
|
|
16
|
+
Requires-Dist: pydantic<3,>=2.5
|
|
17
|
+
Requires-Dist: scipy<2,>=1.17
|
|
18
|
+
Requires-Dist: typer>=0.27.2
|
|
19
|
+
Requires-Dist: tzdata>=2026.3
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Power Quant CLI
|
|
23
|
+
|
|
24
|
+
本地行情准备、因子计算、选股、研究及策略回测;结果保存在本地 DuckDB 和文件。
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
推荐通过 uv 安装独立工具环境:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
uv tool install --python 3.12 powerquant-cli
|
|
32
|
+
pq --help
|
|
33
|
+
uv tool upgrade powerquant-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
也支持在 Python 3.12+ 环境通过 `pip install powerquant-cli` 安装。
|
|
37
|
+
|
|
38
|
+
## 接入行情
|
|
39
|
+
|
|
40
|
+
向管理员获取行情读取密钥,无需注册账户。服务当前仅在 LAN/WireGuard 内可达。
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
export MARKET_BASE_URL='http://服务器地址/market/api/v1'
|
|
44
|
+
export MARKET_API_TOKEN='管理员提供的行情密钥'
|
|
45
|
+
pq examples --directory powerquant-examples
|
|
46
|
+
cd powerquant-examples/selection
|
|
47
|
+
pq data prepare --config prepare.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
PowerShell 使用 `$env:MARKET_BASE_URL="..."` 与 `$env:MARKET_API_TOKEN="..."`。
|
|
51
|
+
|
|
52
|
+
按实际覆盖调整 prepare.json 的股票和日期,将返回 manifest_path 写入 compute.json、select.json;按示例 README 执行 compute、select 和 backtest。不同使用目的的快照须分别通过完整性检查。
|
|
53
|
+
|
|
54
|
+
密钥到期或撤销后在线准备会失败,已下载快照仍可离线使用。行情密钥不用于定义发布;发布定义另使用 PQ_QUANT_URL/PQ_QUANT_TOKEN。默认本地目录为 ~/.powerquant。
|
|
55
|
+
|
|
56
|
+
## Python
|
|
57
|
+
|
|
58
|
+
公开请求模型与调用入口位于 quant_cli.api,与 CLI 使用同一应用服务。
|
|
59
|
+
|
|
60
|
+
## 使用许可
|
|
61
|
+
|
|
62
|
+
自有代码采用随包附带的专有许可证:允许安装、查看源码、为自用进行修改,以及商业使用;未经书面授权不得再分发或转售本软件及其修改版本。第三方组件保留各自许可证。行情访问仍需要有效 Token;本许可证不授予行情数据的使用或再分发权。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Power Quant CLI
|
|
2
|
+
|
|
3
|
+
本地行情准备、因子计算、选股、研究及策略回测;结果保存在本地 DuckDB 和文件。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
推荐通过 uv 安装独立工具环境:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
uv tool install --python 3.12 powerquant-cli
|
|
11
|
+
pq --help
|
|
12
|
+
uv tool upgrade powerquant-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
也支持在 Python 3.12+ 环境通过 `pip install powerquant-cli` 安装。
|
|
16
|
+
|
|
17
|
+
## 接入行情
|
|
18
|
+
|
|
19
|
+
向管理员获取行情读取密钥,无需注册账户。服务当前仅在 LAN/WireGuard 内可达。
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
export MARKET_BASE_URL='http://服务器地址/market/api/v1'
|
|
23
|
+
export MARKET_API_TOKEN='管理员提供的行情密钥'
|
|
24
|
+
pq examples --directory powerquant-examples
|
|
25
|
+
cd powerquant-examples/selection
|
|
26
|
+
pq data prepare --config prepare.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
PowerShell 使用 `$env:MARKET_BASE_URL="..."` 与 `$env:MARKET_API_TOKEN="..."`。
|
|
30
|
+
|
|
31
|
+
按实际覆盖调整 prepare.json 的股票和日期,将返回 manifest_path 写入 compute.json、select.json;按示例 README 执行 compute、select 和 backtest。不同使用目的的快照须分别通过完整性检查。
|
|
32
|
+
|
|
33
|
+
密钥到期或撤销后在线准备会失败,已下载快照仍可离线使用。行情密钥不用于定义发布;发布定义另使用 PQ_QUANT_URL/PQ_QUANT_TOKEN。默认本地目录为 ~/.powerquant。
|
|
34
|
+
|
|
35
|
+
## Python
|
|
36
|
+
|
|
37
|
+
公开请求模型与调用入口位于 quant_cli.api,与 CLI 使用同一应用服务。
|
|
38
|
+
|
|
39
|
+
## 使用许可
|
|
40
|
+
|
|
41
|
+
自有代码采用随包附带的专有许可证:允许安装、查看源码、为自用进行修改,以及商业使用;未经书面授权不得再分发或转售本软件及其修改版本。第三方组件保留各自许可证。行情访问仍需要有效 Token;本许可证不授予行情数据的使用或再分发权。
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""有界、强类型行情客户端;不引入 ORM、供应商 SDK 或应用配置。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Any, Self
|
|
9
|
+
from urllib.parse import quote
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from pydantic import TypeAdapter, ValidationError
|
|
13
|
+
|
|
14
|
+
from pqdata.exceptions import MarketAPIError, MarketProtocolError
|
|
15
|
+
from pqdata.schema.adjustment_schema import AdjustedBarSeries, BarViewRequest
|
|
16
|
+
from pqdata.schema.bar_content_schema import BarContent
|
|
17
|
+
from pqdata.schema.bar_schema import BarFrequency, BarPage, BarQuery, BarResponse
|
|
18
|
+
from pqdata.schema.history_schema import HistoryFacts, HistoryFactsQuery
|
|
19
|
+
from pqdata.schema.pit_daily_window import PITDailyWindow
|
|
20
|
+
from pqdata.schema.query_schema import (
|
|
21
|
+
BarWindowQuery,
|
|
22
|
+
DataHealthQuery,
|
|
23
|
+
InstrumentRangeQuery,
|
|
24
|
+
PITWindowQuery,
|
|
25
|
+
)
|
|
26
|
+
from pqdata.schema.quote_schema import DailyBasicResponse
|
|
27
|
+
from pqdata.schema.reference_schema import (
|
|
28
|
+
InstrumentResponse,
|
|
29
|
+
TradingCalendarDayResponse,
|
|
30
|
+
TradingRuleResponse,
|
|
31
|
+
)
|
|
32
|
+
from pqdata.schema.reliability_schema import DatasetHealthResult
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MarketClient:
|
|
36
|
+
"""持有一个连接池;通过 async with 或 aclose 显式释放连接。"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
base_url: str,
|
|
41
|
+
*,
|
|
42
|
+
token: str | None = None,
|
|
43
|
+
timeout: float = 60.0,
|
|
44
|
+
max_retries: int = 2,
|
|
45
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
if not 0 <= max_retries <= 5:
|
|
48
|
+
raise ValueError("max_retries must be between 0 and 5")
|
|
49
|
+
self._max_retries = max_retries
|
|
50
|
+
self._http = httpx.AsyncClient(
|
|
51
|
+
base_url=base_url.rstrip("/") + "/",
|
|
52
|
+
headers={"Authorization": f"Bearer {token}"} if token else {},
|
|
53
|
+
timeout=timeout,
|
|
54
|
+
transport=transport,
|
|
55
|
+
follow_redirects=False,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def __aenter__(self) -> Self:
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
async def __aexit__(
|
|
62
|
+
self,
|
|
63
|
+
exc_type: type[BaseException] | None,
|
|
64
|
+
exc: BaseException | None,
|
|
65
|
+
traceback: TracebackType | None,
|
|
66
|
+
) -> None:
|
|
67
|
+
await self.aclose()
|
|
68
|
+
|
|
69
|
+
async def aclose(self) -> None:
|
|
70
|
+
await self._http.aclose()
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def source_id(self) -> str:
|
|
74
|
+
"""服务地址的稳定摘要,用于隔离不同数据源的本地缓存。"""
|
|
75
|
+
return sha256(str(self._http.base_url).encode("utf-8")).hexdigest()
|
|
76
|
+
|
|
77
|
+
async def _request(
|
|
78
|
+
self,
|
|
79
|
+
method: str,
|
|
80
|
+
path: str,
|
|
81
|
+
response_type: Any,
|
|
82
|
+
*,
|
|
83
|
+
params: dict[str, Any] | None = None,
|
|
84
|
+
body: Any = None,
|
|
85
|
+
) -> Any:
|
|
86
|
+
# 所有当前端点均为只读查询;未来写入端点不能复用此重试路径。
|
|
87
|
+
for attempt in range(self._max_retries + 1):
|
|
88
|
+
try:
|
|
89
|
+
response = await self._http.request(method, path, params=params, json=body)
|
|
90
|
+
except httpx.TransportError:
|
|
91
|
+
if attempt == self._max_retries:
|
|
92
|
+
raise
|
|
93
|
+
await asyncio.sleep(min(0.25 * 2**attempt, 2.0))
|
|
94
|
+
continue
|
|
95
|
+
if response.status_code not in {429, 502, 503, 504} or attempt == self._max_retries:
|
|
96
|
+
break
|
|
97
|
+
delay = min(0.25 * 2**attempt, 2.0)
|
|
98
|
+
retry_after = response.headers.get("retry-after", "")
|
|
99
|
+
if retry_after.isdecimal():
|
|
100
|
+
delay = min(max(delay, int(retry_after)), 5.0)
|
|
101
|
+
await response.aclose()
|
|
102
|
+
await asyncio.sleep(delay)
|
|
103
|
+
if not response.is_success:
|
|
104
|
+
raise MarketAPIError(response.status_code, response.headers.get("x-request-id"))
|
|
105
|
+
try:
|
|
106
|
+
return TypeAdapter(response_type).validate_json(response.content)
|
|
107
|
+
except ValidationError as error:
|
|
108
|
+
raise MarketProtocolError("Invalid market response") from error
|
|
109
|
+
|
|
110
|
+
async def get_instrument(self, instrument_id: str) -> InstrumentResponse:
|
|
111
|
+
return await self._request(
|
|
112
|
+
"GET",
|
|
113
|
+
f"instruments/{quote(instrument_id, safe='')}",
|
|
114
|
+
InstrumentResponse,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def get_history_facts(self, query: HistoryFactsQuery) -> HistoryFacts:
|
|
118
|
+
result = await self._request(
|
|
119
|
+
"POST", "history/facts", HistoryFacts, body=query.model_dump(mode="json")
|
|
120
|
+
)
|
|
121
|
+
if query.revision is not None and result.revision != query.revision:
|
|
122
|
+
raise MarketProtocolError("History facts revision changed")
|
|
123
|
+
if result.instrument.instrument_id != query.instrument_id:
|
|
124
|
+
raise MarketProtocolError("History facts instrument differs from query")
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
async def get_bars(
|
|
128
|
+
self,
|
|
129
|
+
instrument_id: str,
|
|
130
|
+
frequency: str,
|
|
131
|
+
start: datetime | None = None,
|
|
132
|
+
end: datetime | None = None,
|
|
133
|
+
limit: int = 1000,
|
|
134
|
+
) -> list[BarResponse]:
|
|
135
|
+
query = BarWindowQuery(
|
|
136
|
+
instrument_id=instrument_id,
|
|
137
|
+
frequency=TypeAdapter(BarFrequency).validate_python(frequency),
|
|
138
|
+
start=start,
|
|
139
|
+
end=end,
|
|
140
|
+
limit=limit,
|
|
141
|
+
)
|
|
142
|
+
return await self._request(
|
|
143
|
+
"POST", "bars/window", list[BarResponse], body=query.model_dump(mode="json")
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
async def get_adjusted_bars(
|
|
147
|
+
self,
|
|
148
|
+
instrument_id: str,
|
|
149
|
+
frequency: str,
|
|
150
|
+
view: BarViewRequest,
|
|
151
|
+
start: datetime | None = None,
|
|
152
|
+
end: datetime | None = None,
|
|
153
|
+
limit: int = 1000,
|
|
154
|
+
) -> AdjustedBarSeries:
|
|
155
|
+
query = BarWindowQuery(
|
|
156
|
+
instrument_id=instrument_id,
|
|
157
|
+
frequency=TypeAdapter(BarFrequency).validate_python(frequency),
|
|
158
|
+
start=start,
|
|
159
|
+
end=end,
|
|
160
|
+
limit=limit,
|
|
161
|
+
view=BarViewRequest.model_validate(view.model_dump(mode="json")),
|
|
162
|
+
)
|
|
163
|
+
return await self._request(
|
|
164
|
+
"POST", "bars/view", AdjustedBarSeries, body=query.model_dump(mode="json")
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def verify_bars(
|
|
168
|
+
self,
|
|
169
|
+
instrument_id: str,
|
|
170
|
+
frequency: str,
|
|
171
|
+
start: datetime,
|
|
172
|
+
end: datetime,
|
|
173
|
+
) -> DatasetHealthResult:
|
|
174
|
+
query = DataHealthQuery(
|
|
175
|
+
instrument_id=instrument_id,
|
|
176
|
+
frequency=TypeAdapter(BarFrequency).validate_python(frequency),
|
|
177
|
+
start=start,
|
|
178
|
+
end=end,
|
|
179
|
+
)
|
|
180
|
+
return await self._request(
|
|
181
|
+
"POST", "verify/bars", DatasetHealthResult, body=query.model_dump(mode="json")
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def verify_adjustment_factors(
|
|
185
|
+
self,
|
|
186
|
+
instrument_id: str,
|
|
187
|
+
frequency: str,
|
|
188
|
+
start: datetime,
|
|
189
|
+
end: datetime,
|
|
190
|
+
) -> DatasetHealthResult:
|
|
191
|
+
query = DataHealthQuery(
|
|
192
|
+
instrument_id=instrument_id,
|
|
193
|
+
frequency=TypeAdapter(BarFrequency).validate_python(frequency),
|
|
194
|
+
start=start,
|
|
195
|
+
end=end,
|
|
196
|
+
)
|
|
197
|
+
return await self._request(
|
|
198
|
+
"POST",
|
|
199
|
+
"verify/adjustment-factors",
|
|
200
|
+
DatasetHealthResult,
|
|
201
|
+
body=query.model_dump(mode="json"),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async def verify_corporate_actions(
|
|
205
|
+
self,
|
|
206
|
+
instrument_id: str,
|
|
207
|
+
start: datetime,
|
|
208
|
+
end: datetime,
|
|
209
|
+
) -> DatasetHealthResult:
|
|
210
|
+
query = DataHealthQuery(instrument_id=instrument_id, start=start, end=end)
|
|
211
|
+
return await self._request(
|
|
212
|
+
"POST",
|
|
213
|
+
"verify/corporate-actions",
|
|
214
|
+
DatasetHealthResult,
|
|
215
|
+
body=query.model_dump(mode="json"),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
async def snapshot(self, start: date, end: date) -> dict[str, str]:
|
|
219
|
+
return await self._request(
|
|
220
|
+
"GET",
|
|
221
|
+
"revisions",
|
|
222
|
+
dict[str, str],
|
|
223
|
+
params={
|
|
224
|
+
"start": start.isoformat(),
|
|
225
|
+
"end": end.isoformat(),
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
async def search_instruments(
|
|
230
|
+
self,
|
|
231
|
+
keyword: str | None = None,
|
|
232
|
+
instrument_type: str | None = None,
|
|
233
|
+
exchange: str | None = None,
|
|
234
|
+
limit: int = 100,
|
|
235
|
+
) -> list[InstrumentResponse]:
|
|
236
|
+
params = {
|
|
237
|
+
key: value
|
|
238
|
+
for key, value in {
|
|
239
|
+
"keyword": keyword,
|
|
240
|
+
"instrument_type": instrument_type,
|
|
241
|
+
"exchange": exchange,
|
|
242
|
+
"limit": limit,
|
|
243
|
+
}.items()
|
|
244
|
+
if value is not None
|
|
245
|
+
}
|
|
246
|
+
return await self._request("GET", "instruments", list[InstrumentResponse], params=params)
|
|
247
|
+
|
|
248
|
+
async def list_instruments_on(
|
|
249
|
+
self, on_date: date, limit: int = 10000
|
|
250
|
+
) -> list[InstrumentResponse]:
|
|
251
|
+
return await self._request(
|
|
252
|
+
"GET",
|
|
253
|
+
"universe",
|
|
254
|
+
list[InstrumentResponse],
|
|
255
|
+
params={
|
|
256
|
+
"on_date": on_date.isoformat(),
|
|
257
|
+
"limit": limit,
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
async def list_trading_rules(
|
|
262
|
+
self,
|
|
263
|
+
instrument_ids: list[str],
|
|
264
|
+
start: date,
|
|
265
|
+
end: date,
|
|
266
|
+
) -> list[TradingRuleResponse]:
|
|
267
|
+
query = InstrumentRangeQuery(instrument_ids=instrument_ids, start=start, end=end)
|
|
268
|
+
return await self._request(
|
|
269
|
+
"POST",
|
|
270
|
+
"trading-rules/query",
|
|
271
|
+
list[TradingRuleResponse],
|
|
272
|
+
body=query.model_dump(mode="json"),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async def list_trading_days(
|
|
276
|
+
self,
|
|
277
|
+
exchange: str,
|
|
278
|
+
start: date,
|
|
279
|
+
end: date,
|
|
280
|
+
) -> list[TradingCalendarDayResponse]:
|
|
281
|
+
return await self._request(
|
|
282
|
+
"GET",
|
|
283
|
+
"calendar",
|
|
284
|
+
list[TradingCalendarDayResponse],
|
|
285
|
+
params={
|
|
286
|
+
"exchange": exchange,
|
|
287
|
+
"start": start.isoformat(),
|
|
288
|
+
"end": end.isoformat(),
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
async def is_trading_day(self, exchange: str, trade_date: date) -> bool:
|
|
293
|
+
return await self._request(
|
|
294
|
+
"GET",
|
|
295
|
+
"calendar/is-open",
|
|
296
|
+
bool,
|
|
297
|
+
params={
|
|
298
|
+
"exchange": exchange,
|
|
299
|
+
"trade_date": trade_date.isoformat(),
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
async def list_daily_basic_at(
|
|
304
|
+
self,
|
|
305
|
+
decision_time: datetime,
|
|
306
|
+
limit: int = 10000,
|
|
307
|
+
) -> list[DailyBasicResponse]:
|
|
308
|
+
return await self._request(
|
|
309
|
+
"GET",
|
|
310
|
+
"daily-basic/visible",
|
|
311
|
+
list[DailyBasicResponse],
|
|
312
|
+
params={
|
|
313
|
+
"decision_time": decision_time.isoformat(),
|
|
314
|
+
"limit": limit,
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
async def get_pit_daily_windows(
|
|
319
|
+
self,
|
|
320
|
+
instrument_id: str,
|
|
321
|
+
days: list[date],
|
|
322
|
+
next_days: list[date],
|
|
323
|
+
lookback: int,
|
|
324
|
+
) -> tuple[PITDailyWindow, ...]:
|
|
325
|
+
query = PITWindowQuery(
|
|
326
|
+
instrument_id=instrument_id,
|
|
327
|
+
days=days,
|
|
328
|
+
next_days=next_days,
|
|
329
|
+
lookback=lookback,
|
|
330
|
+
)
|
|
331
|
+
return await self._request(
|
|
332
|
+
"POST",
|
|
333
|
+
"bars/pit-windows",
|
|
334
|
+
tuple[PITDailyWindow, ...],
|
|
335
|
+
body=query.model_dump(mode="json"),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
async def query_bars(self, query: BarQuery) -> BarPage:
|
|
339
|
+
"""读取一页数据;错误内容不混入行情结果。"""
|
|
340
|
+
page = await self._request(
|
|
341
|
+
"POST", "bars/query", BarPage, body=query.model_dump(mode="json")
|
|
342
|
+
)
|
|
343
|
+
if query.revision is not None and page.revision != query.revision:
|
|
344
|
+
raise MarketProtocolError("Market revision changed during pagination")
|
|
345
|
+
if len(page.items) > query.page_size:
|
|
346
|
+
raise MarketProtocolError("Server exceeded requested page size")
|
|
347
|
+
previous: tuple | None = None
|
|
348
|
+
for bar in page.items:
|
|
349
|
+
key = (bar.timestamp, bar.instrument_id)
|
|
350
|
+
if (
|
|
351
|
+
bar.instrument_id not in query.instrument_ids
|
|
352
|
+
or bar.frequency != query.frequency
|
|
353
|
+
or not query.start <= bar.timestamp <= query.end
|
|
354
|
+
or (previous is not None and key <= previous)
|
|
355
|
+
):
|
|
356
|
+
raise MarketProtocolError("Bar page violates query range or ordering")
|
|
357
|
+
previous = key
|
|
358
|
+
return page
|
|
359
|
+
|
|
360
|
+
async def describe_bars(self, query: BarQuery) -> BarContent:
|
|
361
|
+
"""只传输范围摘要;服务端仍需扫描数据,不能视为常数成本的查询。"""
|
|
362
|
+
if query.cursor is not None:
|
|
363
|
+
raise ValueError("Content queries must start at the first page")
|
|
364
|
+
result = await self._request(
|
|
365
|
+
"POST", "bars/content", BarContent, body=query.model_dump(mode="json")
|
|
366
|
+
)
|
|
367
|
+
if query.revision is not None and result.revision != query.revision:
|
|
368
|
+
raise MarketProtocolError("Content revision differs from query")
|
|
369
|
+
return result
|
|
370
|
+
|
|
371
|
+
async def iter_bar_pages(self, query: BarQuery) -> AsyncIterator[BarPage]:
|
|
372
|
+
"""逐页消费,固定 revision,检测重复游标与跨页重复数据。"""
|
|
373
|
+
seen: set[str] = {query.cursor} if query.cursor else set()
|
|
374
|
+
previous: tuple | None = None
|
|
375
|
+
while True:
|
|
376
|
+
page = await self.query_bars(query)
|
|
377
|
+
if previous is not None and page.items:
|
|
378
|
+
first = page.items[0]
|
|
379
|
+
if (first.timestamp, first.instrument_id) <= previous:
|
|
380
|
+
raise MarketProtocolError("Bar pages overlap or are out of order")
|
|
381
|
+
if page.next_cursor and (page.next_cursor in seen or not page.items):
|
|
382
|
+
raise MarketProtocolError("Pagination did not advance")
|
|
383
|
+
yield page
|
|
384
|
+
if page.next_cursor is None:
|
|
385
|
+
return
|
|
386
|
+
last = page.items[-1]
|
|
387
|
+
previous = (last.timestamp, last.instrument_id)
|
|
388
|
+
seen.add(page.next_cursor)
|
|
389
|
+
query = query.model_copy(update={"cursor": page.next_cursor, "revision": page.revision})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""流式落盘;完整下载后才替换目标文件,失败保留原文件。"""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from pqdata.client import MarketClient
|
|
11
|
+
from pqdata.schema.bar_schema import BarQuery
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DownloadReceipt(BaseModel):
|
|
15
|
+
"""供本地缓存记录查询条件、版本及内容校验和。"""
|
|
16
|
+
|
|
17
|
+
query: BarQuery
|
|
18
|
+
revision: str
|
|
19
|
+
row_count: int = Field(ge=0)
|
|
20
|
+
byte_count: int = Field(ge=0)
|
|
21
|
+
sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def download_bars(
|
|
25
|
+
client: MarketClient, query: BarQuery, destination: Path
|
|
26
|
+
) -> DownloadReceipt:
|
|
27
|
+
"""完整范围以 JSON Lines 落盘;内存上限为一页数据。"""
|
|
28
|
+
if query.cursor is not None:
|
|
29
|
+
raise ValueError("Downloads must start at the first page")
|
|
30
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
temporary: Path | None = None
|
|
32
|
+
digest = hashlib.sha256()
|
|
33
|
+
row_count = byte_count = 0
|
|
34
|
+
revision = ""
|
|
35
|
+
try:
|
|
36
|
+
with tempfile.NamedTemporaryFile(
|
|
37
|
+
mode="wb", dir=destination.parent, prefix=".market-", suffix=".part", delete=False
|
|
38
|
+
) as output:
|
|
39
|
+
temporary = Path(output.name)
|
|
40
|
+
async for page in client.iter_bar_pages(query):
|
|
41
|
+
revision = page.revision
|
|
42
|
+
for bar in page.items:
|
|
43
|
+
line = (bar.model_dump_json() + "\n").encode("utf-8")
|
|
44
|
+
output.write(line)
|
|
45
|
+
digest.update(line)
|
|
46
|
+
row_count += 1
|
|
47
|
+
byte_count += len(line)
|
|
48
|
+
output.flush()
|
|
49
|
+
os.fsync(output.fileno())
|
|
50
|
+
os.replace(temporary, destination)
|
|
51
|
+
finally:
|
|
52
|
+
if temporary is not None:
|
|
53
|
+
temporary.unlink(missing_ok=True)
|
|
54
|
+
return DownloadReceipt(
|
|
55
|
+
query=query,
|
|
56
|
+
revision=revision,
|
|
57
|
+
row_count=row_count,
|
|
58
|
+
byte_count=byte_count,
|
|
59
|
+
sha256=digest.hexdigest(),
|
|
60
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""客户端错误不依赖服务端异常实现。"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MarketAPIError(Exception):
|
|
5
|
+
"""服务拒绝请求;保留状态码和可追踪的请求 ID,不暴露凭据。"""
|
|
6
|
+
|
|
7
|
+
def __init__(self, status_code: int, request_id: str | None = None) -> None:
|
|
8
|
+
self.status_code = status_code
|
|
9
|
+
self.request_id = request_id
|
|
10
|
+
super().__init__(f"Market request failed (HTTP {status_code}, request_id={request_id})")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MarketProtocolError(Exception):
|
|
14
|
+
"""响应不符合契约或下载内容不一致,禁止继续消费。"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""客户端公开数据契约。"""
|