Skip to content

策略接入分层方案

本文档定义 zelqor 下一阶段的策略接入方案。

目标不是把所有策略都强行收敛成同一种接口,而是明确区分不同复杂度策略的接入层级,让仓库同时具备:

  • 标准化运行能力
  • 复杂策略表达能力
  • 研究脚本保留空间

当前仓库已经具备:

  • SelectorContext
  • DailyPoolSelector
  • build_daily_pools(...)
  • run_selector_backtest(...)
  • step_backtest_day(...)

但还缺少一套清晰的“策略接入分层模型”,导致当前策略能力与运行方式之间存在脱节:

  • 简单 selector 已有雏形
  • 复杂跨日状态策略还没有正式接口
  • 研究脚本只能通过手写入口运行

因此下一阶段建议把策略接入分成三类。

当前落地状态

本页中的方案已经部分落地。

当前已经完成:

  • SelectorContext
  • DailyPoolSelector
  • run_selector_backtest(...)
  • StrategyStepContext
  • StrategyDayDecision
  • StatefulStepStrategy
  • run_stateful_step_backtest(...)
  • 基于 Python 文件或模块路径的最小 strategy loader
  • run_backtest(...) 对 selector / stateful step 的统一分发
  • examples/run_stateful_step_limit_up_then_limit_up.py 示例
  • 最小可加载策略示例:
  • examples/loadable_selector_strategy.py
  • examples/loadable_stateful_step_strategy.py
  • zelqor.strategy.examples.loadable_selector_module

当前仍未完成:

  • 更完整的 loader 导出约定、示例与错误诊断

1. 设计目标

这一阶段希望解决三个问题:

  1. 让简单策略可以被统一加载和批量运行
  2. 让复杂策略不必为了适配最窄接口而牺牲表达力
  3. 保留脚本式研究和演示入口,避免过早把探索型开发锁死

一句话总结:

策略加载的目标不是减少策略自由度,而是把“运行方式”标准化,同时按复杂度保留不同层级的表达空间。

2. 三类策略形态

2.1 selector 型策略

selector 型策略负责:

  • 在某个交易日读取上下文
  • 返回当天股票池代码列表

典型接口形态:

class DailyPoolSelector(Protocol):
    def select(self, context: SelectorContext) -> list[str]:
        ...

适用场景:

  • 截面选股
  • 因子打分后取前 N
  • 当天满足条件就进入股票池
  • 无需维护复杂跨日内部状态的日频策略

优点:

  • 接口最简单
  • 最容易批量运行
  • 最适合接入 CLI / API / 参数扫描
  • 与现有 run_selector_backtest(...) 最契合

边界:

  • selector 只负责“今天选谁”
  • 持仓、成交、资金和持有期仍由回测引擎处理
  • 不适合承载复杂观察池、待确认池、超期淘汰等跨日状态机

2.2 stateful step 型策略

stateful step 型策略负责:

  • 维护自身跨日状态
  • 在每个交易日推进策略状态机
  • 产出当天可交给回测引擎执行的决策结果

它与 selector 的最大区别是:

  • selector 强调“今日选股结果”
  • stateful step 强调“今日状态推进 + 今日决策输出”

这类策略通常会维护:

  • 观察池
  • 待确认池
  • 信号冷却期
  • 过期淘汰记录
  • 其他需要跨日累积的中间状态

推荐接口形态:

@dataclass(slots=True)
class StrategyDayDecision:
    pool_codes: list[str]
    notes: list[str] = field(default_factory=list)
    debug: dict[str, object] = field(default_factory=dict)


class StatefulStepStrategy(Protocol):
    def on_day(self, context: StrategyStepContext) -> StrategyDayDecision:
        ...

其中 StrategyStepContext 推荐包含:

  • trade_date
  • trade_dates
  • date_to_index
  • provider
  • params
  • 可选的只读回测状态摘要

推荐职责划分:

  • 策略对象负责决定今天哪些标的进入买入池
  • 回测引擎负责成交、持仓、资金和权益计算
  • 策略对象只维护“策略自己的状态”
  • 引擎只维护“账户和交易执行状态”

这种划分尤其适合当前仓库内的 stateful step 示例,因为它已经具备:

  • watchlist
  • pending_watchlist
  • pending_confirmation_watchlist
  • 超期移除
  • 买入后从观察池移除

也就是说,这份策略本质上更像:

  • 有状态逐日推进策略

而不是:

  • select(context) -> list[str] selector

2.3 script/demo 型入口

script/demo 型入口不是统一协议,而是一种明确保留的自由运行方式。

它的特点是:

  • 可以直接写脚本
  • 可以手动构造 provider
  • 可以手动控制参数、日志、调试输出
  • 可以直接驱动 step_backtest_day(...)
  • 不要求一定实现统一策略接口

典型形态就是当前仓库内:

  • strategies/double_limit_next_limit/run_double_limit_next_limit_step.py

适用场景:

  • 研究验证
  • 原型开发
  • API 示例
  • 调试某类数据查询或状态推进逻辑

保留这类入口的原因是:

  • 不是所有研究逻辑都应该第一时间沉淀成框架接口
  • 复杂策略在定型前需要足够高的实验自由度
  • 文档和示例也需要真实可运行脚本作为教学材料

3. 三类策略的职责边界

推荐按下表理解:

形态 策略负责什么 引擎负责什么 标准化程度
selector 今天选谁 买卖、持仓、资金、权益
stateful step 今天状态怎么推进、今天买谁 买卖、持仓、资金、权益
script/demo 全部由脚本自行组织 只提供底层能力

更直观地说:

  • selector:今天选谁
  • stateful step:今天状态怎么推进,今天买谁
  • script/demo:今天整套怎么跑都由脚本自己控制

4. 推荐的正式分层

下一阶段建议正式形成下面三层:

4.1 第一层:Selector API

继续保留并完善现有:

  • SelectorContext
  • DailyPoolSelector
  • build_daily_pools(...)
  • run_selector_backtest(...)

这层作为:

  • 最低成本接入方式
  • 默认推荐接入方式
  • 文档与示例的基础形态

4.2 第二层:Stateful Step Strategy API

新增正式接口,例如:

@dataclass(slots=True)
class StrategyStepContext:
    trade_date: str
    trade_dates: list[str]
    date_to_index: dict[str, int]
    provider: MarketDataProvider
    params: dict[str, object] = field(default_factory=dict)


@dataclass(slots=True)
class StrategyDayDecision:
    pool_codes: list[str] = field(default_factory=list)
    notes: list[str] = field(default_factory=list)
    debug: dict[str, object] = field(default_factory=dict)


class StatefulStepStrategy(Protocol):
    def on_day(self, context: StrategyStepContext) -> StrategyDayDecision:
        ...

同时新增对应驱动器,例如:

def run_stateful_step_backtest(
    *,
    trade_dates: list[str],
    strategy: StatefulStepStrategy,
    provider: MarketDataProvider,
    initial_cash: float,
    ...
) -> BacktestResult:
    ...

它的内部职责可以明确为:

  1. 初始化回测 state
  2. 遍历交易日
  3. 每天调用 strategy.on_day(...)
  4. decision.pool_codes 交给 step_backtest_day(...)
  5. 汇总 BacktestResult

4.3 第三层:Script / Demo Entrypoint

明确允许继续存在:

  • examples/*.py
  • strategies/*/run_*.py

这层不作为统一加载目标,而作为:

  • 研究脚本
  • 手工演示入口
  • 策略孵化入口

建议文档里明确其定位:

  • 合法
  • 推荐用于探索
  • 不等于正式标准接入

5. 与策略加载的关系

策略加载并不要求三类策略都用同一种加载方式。

推荐分层加载:

5.1 selector 加载

支持:

  • 从 Python 文件加载 selector 对象
  • 从模块路径加载 selector 对象
  • 从工厂函数构造 selector

推荐约定之一:

  • 导出 selector
  • 或导出 build_selector(params) -> DailyPoolSelector

5.2 stateful step strategy 加载

支持:

  • 从 Python 文件加载策略对象
  • 从模块路径加载策略类或工厂

推荐约定之一:

  • 导出 strategy
  • 或导出 build_strategy(params) -> StatefulStepStrategy

5.3 script/demo 入口

不强行纳入统一 loader。

原因:

  • 它本来就是自由脚本
  • 目标是演示和研究,而不是作为稳定宿主协议

也就是说:

  • loader 应服务标准接口
  • script/demo 应继续允许存在,但不要求被标准 loader 接管

6. 对当前仓库内策略的判断

当前仓库内的 double_limit_next_limitexamples/run_stateful_step_limit_up_then_limit_up.py 都可以这样理解:

  • 策略内容形态:stateful step 型策略
  • 当前落地方式:script/demo 型入口

这意味着它的合理演进路径不是:

  • 强行改写成最简单 selector

而是:

  1. 先把跨日状态机从脚本中抽离成正式策略对象
  2. 保留一个薄的运行脚本作为 demo 入口
  3. 再为这类策略接入标准化驱动器和加载器

7. 推荐实施顺序

建议按下面顺序推进:

  1. 文档化三类策略边界与定位
  2. 保持现有 selector API 不变
  3. 新增 StatefulStepStrategy 相关接口定义
  4. 新增 run_stateful_step_backtest(...)
  5. 新增一个完整的 stateful step 示例脚本
  6. 保留现有 run_double_limit_next_limit_step.py 作为原策略 demo 脚本
  7. 最后实现基于文件或模块路径的 loader

这样做的好处是:

  • 先把接口分层设计清楚
  • 再做实现
  • 避免为了 loader 反向把策略能力削平

8. 非目标

这一阶段不建议做:

  • 把所有策略统一成一个最窄协议
  • 让 script/demo 必须适配 loader
  • 为了抽象而重写现有策略脚本
  • 在没有 stateful strategy 正式接口前,强行把复杂策略塞进 selector

9. 结论

zelqor 下一阶段的策略接入不应只有一条路,而应形成:

  • selector 作为标准默认入口
  • stateful step 作为复杂日频策略正式入口
  • script/demo 作为研究与教学入口继续保留

这三层共同构成一个渐进式策略接入模型:

  • 从研究脚本出发
  • 向正式 stateful strategy 演进
  • 对简单场景则直接落在 selector

这样既不会过早牺牲策略表达能力,也能为后续 loader、CLI、批量回测和模拟盘演进建立清晰边界。