Files
qlib/backend/app/api/deps.py
T
Simon 40bd603b44 feat(backend): 策略库重构为「选股策略 + 公共配置 + 回测组合」三件套
按用户目标把原来「一个策略 = 全套参数」拆开(已确认的设计决策):
- 公共配置 GlobalConfig(全局唯一):佣金/印花税/滑点/最低佣金/复权口径/基准
- 选股策略 SelectionStrategy(原 StrategyDefinition 改名):只剩股票池+因子+条件,
  不再持有 selection/rebalance/costs/portfolio/区间/资金
- 回测组合 BacktestCombo:引用若干选股策略 + 回测时才定的参数
  (起始资金、持仓数 N、持仓天数区间 [Tmin,Tmax]、调仓时机 日/周/月、区间)

引擎(app/quant/combo_engine.py,新增):
- 多策略打分 = 并集 + Borda 秩和(各策略 1/名次 求和;不假设不同策略分值可比,
  能容纳各策略股票池不同);抽出纯函数 borda_combine 便于单测
- 持仓天数区间 [Tmin,Tmax]:Tmax **每个交易日**强制了结(安全阀,月频下也不超期);
  Tmin 仅在调仓日保护(掉出 TopN 但未满 Tmin 暂留,防频繁换手);调仓日为增量调仓
  (只卖超期/掉队且满 Tmin 的,从 TopN 补买至 N 只,不主动减持以尊重 Tmin)
- 调仓时机 daily/weekly/monthly(local_engine.rebalance_dates 新增日频分支)
- 产出与旧 runner 同构的 BacktestResult,前端可视化无需改动;config_snapshot 固化
  ComboRunSpec(组合+当时各策略定义+当时成本/复权)保证可复现

数据层:
- 新表 global_config(默认行:万三/hfq/最低佣金5元)、backtest_combo
- 迁移 b4c5d6e7f8a9:建两表 + 把存量 strategy.config_json 的回测参数键剥掉、
  spec_type 收敛为 selection(已在真实 MariaDB 验证:STG-16BFBF08 清洗后只剩
  universe/factors/conditions)
- 仓储 SqlAlchemyGlobalConfigRepository / SqlAlchemyComboRepository + Protocol

API:
- /api/config GET/PUT;/api/combos CRUD + /{id}/run + /run(kind=combo 异步 Job)
- job_executor 新增 combo 分支:取齐策略+读公共配置→ComboService.run,归档 kind
  记 backtest(结果结构相同)
- /api/strategies 切到 SelectionStrategy,移除已废弃的 /{id}/expand
- strategy_doc.describe_strategy 支持 SelectionStrategy(只讲「怎么选」,如实声明
  资金/持仓/调仓/成本/区间在回测组合里定)

旧的 ResearchSpec + /api/backtests 保留(因子测试与既有契约自检仍用),
作为底层 escape hatch;用户产品路径改为回测组合。

测试:新增 test_combo_engine(6)/test_combo_service(3)/test_combo_api(5),
改写 test_strategies/test_strategy_doc 适配新模型。全量 403 passed(原 388)。
2026-09-30 21:43:28 +08:00

232 lines
9.0 KiB
Python

"""API 依赖注入:Repository / 研究服务的装配点(composition root 的一部分)。
路由层统一使用 Annotated 注入(FastAPI 推荐写法,配合 ruff B008 无冲突)。
"""
from __future__ import annotations
from typing import Annotated
from fastapi import Depends
from sqlalchemy.orm import Session
from app.application.services.chart_service import ChartService
from app.application.services.replay_service import ReplayService
from app.application.services.selection_service import SelectionService
from app.application.services.signal_service import SignalService
from app.domain.repositories.combo import ComboRepository, GlobalConfigRepository
from app.domain.repositories.composite import CompositeRepository
from app.domain.repositories.factor import FactorRepository
from app.domain.repositories.index import IndexConstituentRepository
from app.domain.repositories.jobs import ExperimentRepository, JobRepository
from app.domain.repositories.market import (
AdjustFactorRepository,
DailyBarRepository,
DailyBasicRepository,
FinancialRepository,
StockNameHistoryRepository,
StockRepository,
)
from app.domain.repositories.selection import SelectionRepository
from app.domain.repositories.signal import SignalRepository
from app.domain.repositories.strategy import StrategyRepository
from app.infrastructure.persistence.sqlalchemy.repositories.combo_impl import (
SqlAlchemyComboRepository,
SqlAlchemyGlobalConfigRepository,
)
from app.infrastructure.persistence.sqlalchemy.repositories.composite_impl import (
SqlAlchemyCompositeRepository,
)
from app.infrastructure.persistence.sqlalchemy.repositories.factor_impl import (
SqlAlchemyFactorRepository,
)
from app.infrastructure.persistence.sqlalchemy.repositories.index_impl import (
SqlAlchemyIndexConstituentRepository,
)
from app.infrastructure.persistence.sqlalchemy.repositories.market_impl import (
SqlAlchemyAdjustFactorRepository,
SqlAlchemyDailyBarRepository,
SqlAlchemyDailyBasicRepository,
SqlAlchemyFinancialRepository,
SqlAlchemyStockNameHistoryRepository,
SqlAlchemyStockRepository,
)
from app.infrastructure.persistence.sqlalchemy.repositories.selection_impl import (
SqlAlchemySelectionRepository,
)
from app.infrastructure.persistence.sqlalchemy.repositories.signal_impl import (
SqlAlchemySignalRepository,
)
from app.infrastructure.persistence.sqlalchemy.repositories.strategy_impl import (
SqlAlchemyStrategyRepository,
)
from app.infrastructure.persistence.sqlalchemy.session import get_session
from app.quant.engine import LocalEngine, QuantEngine
from app.quant.service import ResearchService
DbSession = Annotated[Session, Depends(get_session)]
def _stock_repo_factory(session: DbSession) -> StockRepository:
return SqlAlchemyStockRepository(session)
def _daily_repo_factory(session: DbSession) -> DailyBarRepository:
return SqlAlchemyDailyBarRepository(session)
def _financial_repo_factory(session: DbSession) -> FinancialRepository:
return SqlAlchemyFinancialRepository(session)
def _adjust_repo_factory(session: DbSession) -> AdjustFactorRepository:
return SqlAlchemyAdjustFactorRepository(session)
def _daily_basic_repo_factory(session: DbSession) -> DailyBasicRepository:
return SqlAlchemyDailyBasicRepository(session)
def _chart_service_factory(
stock_repo: Annotated[StockRepository, Depends(_stock_repo_factory)],
daily_repo: Annotated[DailyBarRepository, Depends(_daily_repo_factory)],
adj_repo: Annotated[AdjustFactorRepository, Depends(_adjust_repo_factory)],
) -> ChartService:
return ChartService(stock_repo, daily_repo, adj_repo)
def _name_repo_factory(session: DbSession) -> StockNameHistoryRepository:
"""名称变更历史仓储(StockNameHistoryRepository 实现)。"""
return SqlAlchemyStockNameHistoryRepository(session)
def _index_repo_factory(session: DbSession) -> IndexConstituentRepository:
return SqlAlchemyIndexConstituentRepository(session)
def _engine_factory() -> QuantEngine:
return LocalEngine()
def _service_factory(
stock_repo: Annotated[StockRepository, Depends(_stock_repo_factory)],
daily_repo: Annotated[DailyBarRepository, Depends(_daily_repo_factory)],
engine: Annotated[QuantEngine, Depends(_engine_factory)],
index_repo: Annotated[IndexConstituentRepository, Depends(_index_repo_factory)],
basic_repo: Annotated[DailyBasicRepository, Depends(_daily_basic_repo_factory)],
financial_repo: Annotated[FinancialRepository, Depends(_financial_repo_factory)],
name_repo: Annotated[
StockNameHistoryRepository, Depends(_name_repo_factory)
] = None,
) -> ResearchService:
return ResearchService(
stock_repo,
daily_repo,
engine,
index_repo,
basic_repo=basic_repo,
financial_repo=financial_repo,
name_repo=name_repo,
)
def _replay_service_factory(
stock_repo: Annotated[StockRepository, Depends(_stock_repo_factory)],
daily_repo: Annotated[DailyBarRepository, Depends(_daily_repo_factory)],
index_repo: Annotated[IndexConstituentRepository, Depends(_index_repo_factory)],
name_repo: Annotated[StockNameHistoryRepository, Depends(_name_repo_factory)],
) -> ReplayService:
return ReplayService(stock_repo, daily_repo, index_repo, name_repo=name_repo)
def _signal_service_factory(
stock_repo: Annotated[StockRepository, Depends(_stock_repo_factory)],
daily_repo: Annotated[DailyBarRepository, Depends(_daily_repo_factory)],
index_repo: Annotated[IndexConstituentRepository, Depends(_index_repo_factory)],
name_repo: Annotated[StockNameHistoryRepository, Depends(_name_repo_factory)],
) -> SignalService:
return SignalService(stock_repo, daily_repo, index_repo, name_repo=name_repo)
def _selection_service_factory(
stock_repo: Annotated[StockRepository, Depends(_stock_repo_factory)],
daily_repo: Annotated[DailyBarRepository, Depends(_daily_repo_factory)],
financial_repo: Annotated[FinancialRepository, Depends(_financial_repo_factory)],
index_repo: Annotated[IndexConstituentRepository, Depends(_index_repo_factory)],
basic_repo: Annotated[DailyBasicRepository, Depends(_daily_basic_repo_factory)],
name_repo: Annotated[StockNameHistoryRepository, Depends(_name_repo_factory)],
) -> SelectionService:
return SelectionService(
stock_repo,
daily_repo,
financial_repo,
index_repo,
basic_repo=basic_repo,
name_repo=name_repo,
)
def _selection_repo_factory(session: DbSession) -> SelectionRepository:
return SqlAlchemySelectionRepository(session)
def _factor_repo_factory(session: DbSession) -> FactorRepository:
return SqlAlchemyFactorRepository(session)
def _composite_repo_factory(session: DbSession) -> CompositeRepository:
return SqlAlchemyCompositeRepository(session)
def _signal_repo_factory(session: DbSession) -> SignalRepository:
return SqlAlchemySignalRepository(session)
def _strategy_repo_factory(session: DbSession) -> StrategyRepository:
return SqlAlchemyStrategyRepository(session)
def _global_config_repo_factory(session: DbSession) -> GlobalConfigRepository:
return SqlAlchemyGlobalConfigRepository(session)
def _combo_repo_factory(session: DbSession) -> ComboRepository:
return SqlAlchemyComboRepository(session)
StockRepoDep = Annotated[StockRepository, Depends(_stock_repo_factory)]
DailyRepoDep = Annotated[DailyBarRepository, Depends(_daily_repo_factory)]
EngineDep = Annotated[QuantEngine, Depends(_engine_factory)]
ResearchServiceDep = Annotated[ResearchService, Depends(_service_factory)]
SelectionServiceDep = Annotated[SelectionService, Depends(_selection_service_factory)]
SelectionRepoDep = Annotated[SelectionRepository, Depends(_selection_repo_factory)]
FactorRepoDep = Annotated[FactorRepository, Depends(_factor_repo_factory)]
CompositeRepoDep = Annotated[CompositeRepository, Depends(_composite_repo_factory)]
SignalRepoDep = Annotated[SignalRepository, Depends(_signal_repo_factory)]
SignalServiceDep = Annotated[SignalService, Depends(_signal_service_factory)]
ReplayServiceDep = Annotated[ReplayService, Depends(_replay_service_factory)]
ChartServiceDep = Annotated[ChartService, Depends(_chart_service_factory)]
StrategyRepoDep = Annotated[StrategyRepository, Depends(_strategy_repo_factory)]
GlobalConfigRepoDep = Annotated[GlobalConfigRepository, Depends(_global_config_repo_factory)]
ComboRepoDep = Annotated[ComboRepository, Depends(_combo_repo_factory)]
def _job_repo_factory(session: DbSession):
from app.infrastructure.persistence.sqlalchemy.repositories.jobs_impl import (
SqlAlchemyJobRepository,
)
return SqlAlchemyJobRepository(session)
def _experiment_repo_factory(session: DbSession):
from app.infrastructure.persistence.sqlalchemy.repositories.jobs_impl import (
SqlAlchemyExperimentRepository,
)
return SqlAlchemyExperimentRepository(session)
JobRepoDep = Annotated[JobRepository, Depends(_job_repo_factory)]
ExperimentRepoDep = Annotated[ExperimentRepository, Depends(_experiment_repo_factory)]