KB / ARTICLE

未命名文章

机器学习修订 R1发布于 2026-09-18

期魔方证券策略编写文档


适用对象:使用期魔方编写股票、ETF 等证券策略的用户
策略形式:继承 Strategy 的类策略
适用版本:当前期魔方证券策略接口
更新日期:2026-09-04

本文档介绍基于期魔方平台的证券量化策略编写。策略不同于指标:指标是量化的基础工具(如均线),仅用于数据度量;量化策略是整合指标、开平仓条件、仓位管理、报单撤单等的完整交易逻辑(如双均线开平仓策略)。

文档覆盖策略编写、回测、模拟与实盘任务等核心流程,并说明 A 股 T+1、交易单位、可卖持仓、实盘回调差异等证券侧约束。


策略基础

核心概念

术语 说明
Bar(K 线) 某一时间段内的行情,包含开盘价、最高价、最低价、收盘价、成交量
Tick 逐笔或快照报价。Tick 策略调用频率高于 Bar 策略
Strategy(策略) 继承 Strategy 的交易逻辑。核心工作是看行情,再决定买还是卖
持仓 Position 当前持有的股票或 ETF 数量。证券现券账户中,正数表示多头,0 表示空仓
可卖持仓 T+1 下当天买入部分通常不可卖。卖出必须看 get_available_position()
回测 Backtest 用历史数据模拟策略,检验过去这样做会有怎样的盈亏路径
OrderReceipt 下单回执。表示请求已提交,不表示已经成交

编写策略的基本规则

  • 策略必须继承 Strategy。
  • 回调方法的名称和参数必须按本文书写,例如 on_bar(self, bar)。
  • 用户参数用内联字段声明(IntParam / FloatParam 等),运行中通过 self.params.xxx 读取。
  • 自定义状态保存为 self.xxx,不要使用模块级全局变量保存交易状态。
  • 行情事件中不要编写无限循环、长时间等待或阻塞式任务。
  • 下单后不代表立即成交。订单状态看 on_order(),实际成交看 on_trade()。
  • 股票卖出前优先使用 get_available_position() 检查可卖数量。
  • 多标的策略应根据 bar.symbol 或 tick.symbol 区分当前事件属于哪个标的。
  • 普通 A 股现券账户不要调用 short() 开空;平掉已有多头请用 sell() 或 close_position()。

策略结构与生命周期

事件驱动是什么意思

策略不需要自己写 while 循环。期魔方收到行情或交易事件后,会自动调用策略类中的相应方法:

创建策略实例
    ↓
执行参数注入(self.params 就绪)
    ↓
执行 __init__()(如有)
    ↓
若从快照恢复:先执行 on_resume()
    ↓
执行 on_start()
    ↓
反复接收事件
    ├─ 盘前钩子           → on_pre_open()(仅回测;实盘不会触发)
    ├─ 交易日开始         → on_before_trading()
    ├─ K 线到达           → on_bar()
    ├─ Tick 到达          → on_tick()
    ├─ 横截面切片就绪     → on_cross_section()
    ├─ 定时器触发         → on_timer()
    ├─ 订单状态变化       → on_order() / on_reject()
    ├─ 订单成交           → on_trade()
    ├─ 账户变化           → on_portfolio_update()
    └─ 交易日结束         → on_after_trading()
    ↓
执行 on_stop()

只需重写实际需要的回调。没有重写的方法保持默认空实现,不影响策略运行。

类策略骨架

一个完整的证券策略一般包含:参数声明、on_start、on_bar(或 on_tick),以及按需实现的 on_order / on_trade / on_reject / on_stop。

from qmfquant import IntParam, Strategy


class DemoStrategy(Strategy):
    fast_period = IntParam(5, ge=2, le=200, title="快线周期")
    slow_period = IntParam(20, ge=3, le=500, title="慢线周期")
    quantity = IntParam(100, ge=100, title="交易数量")

    def on_start(self):
        self.set_history_depth(int(self.params.slow_period) + 1)
        self.log("策略启动")

    def on_bar(self, bar):
        pass

    def on_order(self, order):
        pass

    def on_trade(self, trade):
        pass

    def on_reject(self, order):
        pass

    def on_stop(self):
        pass

回调速查表

回调 何时触发 典型用途 回测 实盘
on_start 策略实例启动后 订阅标的、设置历史深度、注册定时器 是 是
on_resume 热启动恢复时,且早于 on_start 恢复连接、处理快照续跑逻辑 是 视任务
on_bar 每根 K 线闭合时 主交易逻辑、指标更新、信号计算 是 是
on_tick 每个 Tick 到达时 高频/盘口响应、逐笔监控 是 是
on_order 订单状态变化时 跟踪下单生命周期 是 是
on_trade 收到成交回报时 成交日志、成交后状态更新 是 是
on_reject 订单首次进入 Rejected 记录拒单原因、降级处理 是 是
on_before_trading 本地交易日首次进入常规会话 盘前检查、生成交易日级信号 是 是
on_pre_open 每个交易日首个常规行情前 盘前信号、开盘成交语义 仅回测 不触发
on_cross_section 当日首个跨标的完整 bar 切片后 横截面同周期调仓 是 视数据切片
on_after_trading 离开常规会话时 日终统计、收盘后清理 是 是
on_portfolio_update 账户快照变化时 监控现金/权益变化 是 是
on_timer 定时器到点时 定时调仓、盘前任务、节律检查 是 是
on_error 用户回调抛异常时 记录异常源 是 是
on_stop 策略停止时 汇总统计、资源释放 是 是

【重要】实盘下 on_pre_open 不会触发。该回调依赖回测交易日历推导的盘前时点,实盘无此数据源。请改用 schedule_daily(...) 在盘前时点触发 on_timer,或把盘前逻辑放到 on_before_trading 中。

回调触发顺序

对于每个 bar / tick / timer 事件,框架大致按以下顺序分发:

  1. on_order / on_trade(若拒单则额外触发 on_reject)
  2. 框架钩子(on_before_trading / on_after_trading、on_cross_section、on_portfolio_update)
  3. 用户事件回调(on_bar / on_tick / on_timer)

说明:

  • on_reject 对同一订单 id 只触发一次。
  • on_before_trading 始终按“前一交易日/前一时点信息可见”工作;其中 get_history()、get_account()、equity 不应看到当日新 bar 或当日更新后的账户视图。
  • on_cross_section 在见到当日首个跨标的完整切片后触发;可以看到当日历史和当前账户快照。每天最多一次,适合选股轮动、同周期调仓。
  • on_after_trading 是结束/收尾型钩子,适合日终统计、清理与归档。若意图是“收盘决策、次日开盘成交”,更清晰的写法是在 on_bar 中按下一根开盘成交,或在回测中使用 on_pre_open;实盘请用 schedule_daily。
  • on_portfolio_update 采用增量触发:初始化时触发一次,后续仅在订单/成交或持仓相关价格变化时触发。可用 self.portfolio_update_eps 过滤微小资产波动(默认 0.0,不过滤)。
  • 停止阶段会在 on_stop 之前补发待触发的 on_after_trading。
  • on_error 参数为 (error, source, payload)。推荐通过 self.error_mode = "raise" | "continue" 控制行为(默认 raise)。

各回调更适合处理的任务:

回调 当前行情 是否适合下单 典型用途
on_start 无 不建议依赖当前价下单 订阅、定时器、一次性准备
on_before_trading 尚未进入当日新行情 可以,但只能用前一日可见数据 盘前选股、生成当日计划
on_pre_open 尚未进入当日正常行情 回测可以;实盘不会进入 盘前决策、开盘成交
on_bar 当前 Bar 是 K 线信号和交易
on_tick 当前 Tick 是 逐笔信号和交易
on_cross_section 当日切片已齐 是 横截面调仓
on_timer 不保证有新行情 是,但应确认价格来源 定时风控、定时调仓、实盘盘前
on_order / on_trade 不应依赖当前行情 谨慎 跟踪订单与成交
on_after_trading 当日交易阶段已结束 通常不下单 日终汇总
on_stop 无新行情 不应下单 结束日志、资源清理

启动阶段细节

一般按以下顺序运行:

  1. 框架创建策略对象并注入页面参数到 self.params。
  2. 如果任务从快照恢复,调用 on_resume()。
  3. 调用 on_start()。
  4. 开始接收 Bar、Tick、订单、成交和定时器事件。

on_start() 适合订阅标的、注册定时器以及执行依赖运行环境的初始化。

热启动时不要在 on_start() 中无条件重置已经恢复的状态:

def on_start(self) -> None:
    if not self.is_restored:
        self.trade_count = 0
    self.set_history_depth(20)

Bar 与 Tick

  • Bar 策略:重写 on_bar(self, bar),每根 K 线触发一次,适合日线、分钟线和大多数选股策略。
  • Tick 策略:重写 on_tick(self, tick),每个行情事件触发一次,调用频率更高。
  • 两者可以同时使用,但必须避免同一交易条件在 Bar 和 Tick 中重复下单。

warmup_period 只限制用户的 on_bar() 在积累足够 Bar 后再开始执行。即使设置了预热期,也建议在使用历史数组前检查其中是否存在 NaN。

下单、订单和成交

策略调用 buy() / sell()
    ↓
返回 OrderReceipt(表示已提交请求)
    ↓
on_order() 接收订单状态变化
    ↓
on_trade() 接收实际成交
    ↓
持仓、现金和权益随成交更新

不要在调用 buy() 的下一行就假设持仓已经增加。需要成交确认的逻辑,应放在 on_trade() 或后续行情事件中。

订单状态流转:

New → Submitted → Filled
                ↘ PartiallyFilled → Filled
                ↘ Cancelled
                ↘ Rejected
                ↘ Expired

Filled、Cancelled、Rejected 和 Expired 是常见终态。只有订单进入终态后,才适合清除策略保存的“等待订单”标记。


外置参数

推荐用内联字段声明策略参数:直接在类体内用 IntParam / FloatParam / BoolParam / ChoiceParam / DateRangeParam 赋值。构造期完成校验,运行中通过 self.params.<name> 只读访问。

from qmfquant import BoolParam, ChoiceParam, FloatParam, IntParam, Strategy


class ConfigurableStrategy(Strategy):
    window = IntParam(20, ge=2, le=250, title="均线周期", description="计算均线使用的 K 线数量")
    quantity = IntParam(100, ge=100, title="下单数量")
    allow_trade = BoolParam(True, title="允许交易")
    mode = ChoiceParam("trend", choices=["trend", "reversal"], title="策略模式")
    stop_pct = FloatParam(5.0, ge=0, le=50, title="止损百分比")

    def on_start(self) -> None:
        self.set_history_depth(int(self.params.window))
        self.log(f"参数就绪:window={self.params.window} quantity={self.params.quantity}")

参数字段工具:

工具 主要参数 用途
IntParam(default, ge=None, le=None, title=None, description=None) 整数默认值、上下限 整数输入
FloatParam(default, ge=None, le=None, title=None, description=None) 小数默认值、上下限 小数输入
BoolParam(default, title=None, description=None) 布尔默认值 开关输入
ChoiceParam(default, choices, title=None, description=None) 默认项、可选项列表 单选输入
DateRangeParam(default=None, title=None, description=None) 日期区间默认值 日期区间输入

要点:

  • self.params 是 frozen 对象,不支持在运行期赋值修改。
  • 派生初始化(指标、缓存结构等)放在 on_start()。
  • 如果希望 IDE 能推断字段类型,可以写成 fast: int = IntParam(10, ge=2, le=200)。

自定义状态变量直接放在 self 上,并在使用前初始化:

def on_start(self) -> None:
    self.last_signal = None
    self.trade_count = 0
    self.pending_order_id = None

策略编写

框架自带变量和快捷属性

使用时先区分两类属性:

  • 只读属性:由框架维护,只能读取,不能在策略中赋值,例如 cash、equity。
  • 可配置属性:允许在on_start() 中设置,例如 warmup_period、lot_size。

属性值代表当前回调时点的状态。订单刚提交但尚未成交时,cash、positions 等值可能还没有发生最终变化。

行情与时间

变量 类型 说明
self.current_bar Bar | None 当前正在处理的 K 线;仅在 Bar 事件中保证有值
self.symbol str 当前行情事件的标的代码;没有当前 Bar/Tick 时不要依赖此属性
self.open float 当前 Bar 开盘价;Tick 模式下为 0.0
self.high float 当前 Bar 最高价;Tick 模式下为 0.0
self.low float 当前 Bar 最低价;Tick 模式下为 0.0
self.close float 当前 Bar 收盘价,或当前 Tick 最新价
self.volume float 当前 Bar 或 Tick 成交量
self.now 本地时间或 None 当前策略时间,已按 timezone 转为本地时间
self.timezone str 策略时区,默认 Asia/Shanghai
self.ctx.session 会话枚举 当前交易会话,用于按盘前/连续竞价/午休等分支

推荐优先使用回调参数中的 bar.symbol、bar.close 或 tick.price。快捷属性适合单标的策略。

账户与持仓

变量 类型 说明
self.cash float 当前现金余额,只读;冻结金额另见 get_account()["frozen_cash"]
self.equity float 当前账户总权益,只读
self.positions dict[str, float] 全部非零持仓,键为标的代码,值为数量
self.position 仓位快捷对象 当前标的仓位,可读 size、available、entry_price、avg_price
self.lot_size int | dict[str, int] 最小交易单位;可设置统一值或按标的设置
self.commission_rate float 百分比佣金率的只读视图
self.min_commission float 单笔最低佣金,只读
self.stamp_tax_rate float 卖出侧印花税比例,只读
self.transfer_fee_rate float 过户费比例,只读
self.commission_policy dict 费用模式和数值,只读

费用属性由回测或任务配置注入。不要在策略中写 self.commission_rate = ...,会抛出 AttributeError。lot_size 是例外,允许在策略中设置:

def on_start(self) -> None:
    self.lot_size = 100
    # 或多标的:
    # self.lot_size = {"600000.SH": 100, "510300.SH": 100}
def on_bar(self, bar: Bar) -> None:
    pos = self.position
    self.log(
        f"{bar.symbol} 持仓={pos.size} 可卖={pos.available} "
        f"成本={pos.entry_price} 现金={self.cash:.2f}"
    )

运行控制

变量 默认值 说明
self.warmup_period 0 执行 on_bar() 前需要积累的 Bar 数量
self.is_restored False 当前任务是否由快照恢复,只读
self.enable_precise_day_boundary_hooks False 是否提高 on_before_trading / on_after_trading 触发精度
self.portfolio_update_eps 0.0 触发账户更新回调的变化阈值
self.error_mode "raise" "raise" 出错终止;"continue" 记录后尝试继续

error_mode="continue" 时必须确保策略状态仍然安全。基础策略通常保持默认 "raise"。


行情订阅与历史数据

subscribe(instrument_id)

订阅一个标的的行情。

参数 类型 必填 说明
instrument_id str 是 标的代码,例如 600000.SH

返回值:None。

def on_start(self) -> None:
    for symbol in ["510300.SH", "510500.SH"]:
        self.subscribe(symbol)

重复订阅同一代码不会产生重复项。多标的策略应把所有需要接收事件的标的都订阅;仅在历史查询或下单时填写代码,并不能代替行情订阅。

回测下 subscribe() 的标的会并入回测标的集合;实盘下会下发到行情通道。若任务页面已经指定运行标的,也可以在首根 on_bar 中读取 bar.symbol 再订阅。

set_history_depth(depth)

设置策略需要保留的历史 Bar 数量。

参数 类型 说明
depth int 非负整数;0 表示不保留历史 Bar

返回值:None。应不小于策略中最大的历史窗口。推荐在 on_start() 中调用,并保证 depth >= count。如果深度为 0,调用历史查询会抛出 RuntimeError。

双流历史:history(symbol, normalized_field, count, history_cutoff, freq)

双流模式(Bar 与 Tick 并存)下,获取历史数据必须显式指定 freq,以区分取的是 K 线序列还是 Tick 序列。

参数 类型 说明
symbol str 标的代码
normalized_field str 标准化字段名。K 线常用 open / high / low / close / volume;Tick 常用 price / volume
count int 需要的历史条数
history_cutoff 时间或 None 历史截止时点;默认截止到当前事件可见时点,避免读到未来数据
freq str 必选。"bar" 取 K 线;"tick" 取 Tick

返回值:按从旧到新排列的序列。values[-1] 是当前可见的最新值。

# 取最近 20 根已完成 K 线的收盘价
closes = self.history(
    symbol=bar.symbol,
    normalized_field="close",
    count=20,
    history_cutoff=None,
    freq="bar",
)

# 取最近 100 个 Tick 的最新价
ticks = self.history(
    symbol=tick.symbol,
    normalized_field="price",
    count=100,
    history_cutoff=None,
    freq="tick",
)

使用规则:

  • freq 必须与你要计算的对象一致:均线、突破、横截面评分用 "bar";盘口、逐笔监控用 "tick"。
  • 不要在 Tick 回调里用 freq="bar" 却把未收盘的当前秒误当成已完成日线。
  • history_cutoff 用于明确“算到哪一根/哪一笔为止”。盘前、定时器、on_before_trading 中尤其应避免省略截止语义后误读当日未完成数据。
  • 数据不足时不要直接交易,先检查长度或 NaN。

get_history(count, symbol=None, field="close")

取得某个字段最近 count 个 Bar 的数组。这是 Bar 策略最常用的便捷接口。

参数 类型 默认值 说明
count int 无 需要的 Bar 数量
symbol str | None None 标的代码;省略时使用当前标的
field str "close" open、high、low、close、volume 等字段

返回值:numpy.ndarray。数据不足时,数组前部会以 NaN 补齐。数组始终按从旧到新排列:values[-1] 是当前可见的最新值。

closes = self.get_history(20, bar.symbol, "close")
if np.isnan(closes).any():
    return

常见错误:

  • 没有先调用 set_history_depth():抛出 RuntimeError。
  • 在没有当前 Bar/Tick 的阶段省略 symbol:无法自动确定标的,应明确传入。
  • 请求的 count 大于已积累数据:前部出现 NaN,策略应等待。
  • 把 Tick 序列当成 K 线来算均线:双流场景请改用 history(..., freq="bar") 或 history(..., freq="tick")。

get_history_map(count, symbols, field="close")

批量取得多个标的的历史字段。返回值:dict[str, numpy.ndarray]。

history = self.get_history_map(20, ["510300.SH", "510500.SH"], field="close")
for symbol, closes in history.items():
    if np.isnan(closes).any():
        continue

传入单个字符串时也会返回字典,而不是直接返回数组。各标的历史不足时分别补 NaN。

get_history_df(count, symbol=None)

取得最近若干根 K 线表格。返回值:pandas.DataFrame,列固定为 open、high、low、close、volume,行顺序从旧到新。

标的信息查询

方法 参数 返回值 说明
get_instrument(symbol) symbol: str InstrumentSnapshot 查询单个标的静态信息;不存在时抛出 KeyError
get_instruments(symbols=None) 标的列表或 None dict[str, InstrumentSnapshot] 批量查询
get_instrument_field(symbol, field) 标的、字段名 对应字段值 字段不存在时抛出 KeyError
get_instrument_config(symbol, fields=None) 标的、字段 单值、字典或完整快照 fields=None 时返回完整快照
info = self.get_instrument(bar.symbol)
self.log(f"最小价位={info.tick_size} 每手={info.lot_size}")

这些接口在 on_start 即可使用。查询不存在的标的或字段会抛出 KeyError。


报单与成交

证券策略日常下单请使用 buy()、sell()、close_position() 和目标仓位系列。

下单参数通用规则

buy(symbol=None, quantity=None, price=None, time_in_force=None,
    trigger_price=None, tag=None, fill_policy=None,
    slippage=None, commission=None)

sell(symbol=None, quantity=None, price=None, time_in_force=None,
     trigger_price=None, tag=None, fill_policy=None,
     slippage=None, commission=None)
参数 类型 默认值 说明
symbol str | None 当前标的 交易标的代码
quantity float | None 由仓位管理器决定 数量(股数);股票策略建议明确填写
price float | None None 限价;省略通常表示市价委托
time_in_force 枚举或 None 运行配置 订单有效期
trigger_price float | None None 止损/止盈触发价
tag str | None None 用户自定义订单标签
fill_policy dict | None 运行配置 回测成交时点和价格规则
slippage float | dict | None 运行配置 本单滑点设置
commission dict | None 运行配置 本单费用设置

建议股票策略始终明确填写 symbol 和 quantity,并保证数量符合交易单位。

补充说明:

  • symbol=None 只适合当前有 Bar/Tick 的回调;盘前、启动或定时回调中应明确填写代码。
  • quantity 表示股数,不是资金金额,也不是仓位比例。
  • price=None 通常表示市价语义;填写正数表示限价。
  • tag 会进入订单和闭合交易记录,适合区分入场、止损、止盈和调仓来源。
  • fill_policy、slippage、commission 主要影响回测;实盘最终以交易通道执行结果为准。

订单类型判断:

price trigger_price 常见含义
None None 市价单
有值 None 限价单
None 有值 触发后市价单
有值 有值 触发后限价单

实盘是否支持某种订单类型,以所连接的券商或交易通道为准。

buy(...)

买入或增加多头持仓。返回值:OrderReceipt。

receipt = self.buy("600000.SH", quantity=100, tag="entry")
receipt = self.buy("600000.SH", quantity=100, price=10.50)

buy() / sell() 默认使用 position_effect="auto",执行前会按可平仓自动拆成 close + open。对普通股票多头策略,买入就是开多或加仓。

sell(...)

卖出已有多头持仓。返回值:OrderReceipt。

available = self.get_available_position("600000.SH")
if available >= 100:
    self.sell("600000.SH", quantity=100, tag="exit")

A 股现券账户受市场规则限制,只能卖出已持有且当日可卖的仓位。sell() 缺省就是自动平仓语义。若本意是平掉已有多头,使用 sell() 或 close_position(),不要写成开空。

不要用 short() 对现券账户开空

short() 默认等价于 side="Sell", position_effect="open",即融券卖空开仓。

普通 A 股现券账户(supports_short_sell=False)不支持融券卖空。调用 short() 会得到类似错误:

broker 'middleware' does not support short sell (supports_short_sell=False):
该 broker 的账户不支持融券卖空(A 股现券账户受市场规则限制, 只能卖出已持有的仓位);
若本意是平掉已有多头, 请用 position_effect='close'(或 sell() 缺省的自动平仓语义)而不是 'open'

正确对应关系:

意图 应调用
买入开多 / 加仓 buy()
卖出平多 sell() 或 close_position()
融券卖空开仓 仅信用账户且通道明确支持时才可考虑 short()
买回平空 仅在确有空头持仓时使用 cover()

可通过 self.get_execution_capabilities() 查看 supports_short_sell、account_mode 等能力。未声明支持做空时,负目标仓位也会被拒绝。

OrderReceipt

下单返回的是提交回执,不是成交结果。

属性 说明
primary 首腿订单编号,多数场景当作“这笔下单的订单号”
order_ids 全部腿的订单编号
group_id / str(receipt) 逻辑委托的客户端订单号,用于把拆出的多腿关联为同一次下单
receipt = self.buy("600000.SH", quantity=100)
self.log(f"订单号:{receipt.primary}")
self.cancel_group(receipt)  # 撤销这次逻辑委托的全部腿

关联成交回报时优先用 trade.group_id,而不是逐个比较 order_id。

开平语义

当前订单语义是 side + position_effect:

  • buy() / sell() 默认 position_effect="auto"。
  • 可平仓 = 结算持仓 − 同一根 bar 内已提交未成交的平仓/减仓单占用。因此「先 close_position() 再 buy()」这类反手写法能拆对。
  • 目标仓位系列(含 close_position())按投影持仓算差额,即结算持仓叠加同一根 bar 内全部在途单的预期效果,避免同一根 K 线内连续调用重复下单。

股票基础策略通常保持自动开平即可。


目标仓位与组合调仓

目标仓位方法描述“最终想持有多少”,框架根据当前持仓计算需要买入或卖出的差额。特别适合选股、轮动和定期调仓。

共同规则:

  • 目标方法只对“当前持仓与目标的差额”下单,不会自动撤销已有活动订单。
  • 调仓前应检查 get_open_orders(),否则未成交订单和新差额订单可能叠加。
  • 数量差额会按 lot_size 向下取整;close_position() 为完整清仓,不受整手取整限制。
  • 返回订单编号表示已经提交请求,不表示订单已经成交。

order_target(symbol=None, target=None, price=None, **kwargs)

把标的持仓数量调整到 target。已达目标时不下单。

self.order_target("600000.SH", target=1000)
self.order_target("600000.SH", target=0)

order_target_value(symbol=None, target_value=None, price=None, **kwargs)

按目标市值调仓。框架使用 目标市值 ÷ 当前价格 计算目标数量,再按交易单位调整。

self.order_target_value("510300.SH", target_value=200_000)

order_target_percent(symbol=None, target_percent=None, price=None, **kwargs)

按账户权益比例设置目标仓位。0.25 表示目标市值为当前权益的 25%。普通无杠杆股票策略通常将比例控制在 0.0 到 1.0。

self.order_target_percent("510300.SH", target_percent=0.25)

rebalance_weights(...)

按多个标的的目标权重一次性调仓。

self.rebalance_weights(
    {"510300.SH": 0.6, "510500.SH": 0.4},
    liquidate_unmentioned=True,
    rebalance_tolerance=0.01,
)
参数 默认值 说明
target_weights 无 标的到目标权重的映射,权重不能为负
liquidate_unmentioned False 是否清仓未出现在目标中的标的
allow_leverage False 是否允许总权重超过 1
rebalance_tolerance 0.0 小于该权重偏差时不调仓

同周期调仓为先减后加:先执行释放资金的腿,再执行加仓腿。默认权重和不超过 1.0。该接口更偏向只做多组合;如需正负目标仓位,使用 rebalance_positions(),并确认账户支持做空。

rebalance_positions(...)

按多个标的的目标数量调仓。allow_short 默认按当前执行环境自动推断;在 cash 或 broker 未声明支持做空时,负目标会被明确拒绝。

可通过 self.get_last_target_positions_plan() 查看最近一次调仓计划。

rebalance_to_topn(...)

根据评分选择前 N 个标的并调仓。返回值是入选标的代码,不是订单编号。

selected = self.rebalance_to_topn(
    scores={"510300.SH": 0.8, "510500.SH": 0.6, "159915.SZ": 0.9},
    top_n=2,
    weight_mode="equal",
    long_only=True,
    liquidate_unmentioned=True,
)
  • long_only=True 时只做多,这是证券策略的默认选择。
  • weight_mode="equal" 等权;"score" 按正评分分配。

close_position(symbol=None)

平掉指定标的的当前持仓。返回值:None。股票 T+1 场景下,实际能否全部卖出仍取决于可卖持仓。

if self.get_available_position(bar.symbol) > 0:
    self.close_position(bar.symbol)

撤单与复杂订单

撤单

方法 说明
cancel_order(order_id) 撤销一笔订单;传入 OrderReceipt 时只取 .primary
cancel_group(group_id) 撤销一个逻辑委托的全部订单腿
cancel_all_orders(symbol=None) 撤销指定标的或全部活动订单

撤单请求发出后,仍应通过 on_order() 确认最终状态。已成交、已撤销、已拒绝或已过期的订单不能再次撤销。

OCO、括号单与跟踪止损

  • place_oco(first_order_id, second_order_id, group_id=None):把两笔已创建订单绑定为 OCO,任一成交后撤销另一笔。
  • place_bracket(symbol, quantity, entry_price=None, stop_trigger_price=None, take_profit_price=None, ...):一次性提交入场、止损、止盈。入场成交后自动挂出退出单;止损与止盈同时存在时自动绑定 OCO。
  • place_trailing_stop(...) / place_trailing_stop_limit(...):跟踪止损市价/限价。

复杂订单在实盘中的可用性取决于交易通道,使用前查询 get_execution_capabilities()。

group_id = self.place_bracket(
    symbol="600000.SH",
    quantity=100,
    entry_price=10.00,
    stop_trigger_price=9.50,
    take_profit_price=11.00,
)

账户、持仓、订单与成交查询

持仓查询

方法或属性 返回值 说明
get_position(symbol=None) float 当前净持仓数量
get_available_position(symbol=None) float 当前可卖/可平数量
get_holding_bars(symbol=None) int 当前连续持仓经过的 Bar 数
positions dict[str, float] 全部非零持仓
position.size / available / entry_price 当前标的字段 总持仓、可卖、成本价

股票策略判断能否卖出时,应使用 get_available_position(),不能只看总持仓。

【重要】实盘通道不提供持有 Bar 数,get_holding_bars() 在实盘恒返回 0。依赖持仓时长的离场逻辑在实盘不会触发。若策略需要“持有 N 根 K 线后离场”,请自行在 on_trade() 记录入场时间或入场 bar 序号,用策略状态判断,不要依赖 get_holding_bars()。

def on_bar(self, bar: Bar) -> None:
    total = self.get_position(bar.symbol)
    available = self.get_available_position(bar.symbol)
    self.log(f"{bar.symbol} 总持仓={total} 可卖={available}")

账户查询 get_account()

无参数,返回账户快照 dict。普通股票策略最常使用 cash、equity、market_value 和 frozen_cash。

字段 说明
cash 现金
equity 账户权益
market_value 持仓市值
frozen_cash 冻结资金
unrealized_pnl 未实现盈亏
account_mode 账户模式,如 cash / margin
borrowed_cash / short_market_value 信用账户扩展字段
maintenance_ratio 维持担保比例
account = self.get_account()
free_cash = float(account.get("cash", 0)) - float(account.get("frozen_cash", 0))
self.log(f"权益={account.get('equity', 0):.2f} 估算未冻结现金={free_cash:.2f}")

账户字典是调用时点的快照。不要长期保存后当作实时账户使用。

信用账户回测若需融资/融券,应在任务或回测配置中显式切换到 margin 并打开 enable_short_sell。现券现金账户不要按信用账户语义编写。

订单与成交查询

方法 返回值 说明
get_open_orders(symbol=None) list[Order] 当前活动订单
get_order(order_id) Order | None 查询单笔订单
get_trades() list[ClosedTrade] 已完成的闭合交易记录

on_trade() 收到的是每次实际成交;get_trades() 返回的是已经完成开平配对的交易统计,两者含义不同。

推荐的防重复下单写法:

def on_bar(self, bar: Bar) -> None:
    if self.get_open_orders(bar.symbol):
        return
    # 再判断交易信号

时间、日志与定时器

log(msg, level=logging.INFO)

输出带策略时间的日志。

import logging

self.log("信号已触发")
self.log("历史数据不足", logging.WARNING)
self.log("拒单", logging.ERROR)
级别 语义 量化场景示例
DEBUG 细粒度诊断 排障分支
INFO 正常运行的关键节点 订单提交/成交
WARNING 可恢复的降级 资金不足拒单
ERROR 操作失败 回调异常
CRITICAL 系统级致命 实盘通道断连

时间转换

方法/属性 说明
to_local_time(timestamp) UTC 纳秒时间戳转为策略本地时间
format_time(timestamp, fmt="%Y-%m-%d %H:%M:%S") 格式化本地时间
now 当前策略本地时间;尚无有效运行时间时可能为 None

框架时间戳统一使用 UTC 纳秒整数,不要直接把它当作秒级 Unix 时间戳。

定时器

请根据实际情况需要,使用下列接口:

方法 说明 回测 实盘
schedule(trigger_time, payload) 指定时间点一次性触发 是 是
schedule_daily(time_str, payload) 每个交易日在指定时间触发 是 是(每日自动调度下一次)
schedule_weekly(time_str, payload) 每周首个交易日触发,节假日顺延 是 交易日历未知时不可用
schedule_monthly(time_str, payload) 每月首个交易日触发,节假日顺延 是 交易日历未知时不可用
def on_start(self) -> None:
    # 每天 14:55 收盘前检查(回测与实盘都可用)
    self.schedule_daily("14:55:00", "daily_check")
    # 实盘盘前替代 on_pre_open
    self.schedule_daily("09:25:00", "pre_open")

def on_timer(self, payload: str) -> None:
    if payload == "pre_open":
        self.log("盘前检查")
    elif payload == "daily_check":
        self.log(f"收盘前检查,当前权益:{self.equity:.2f}")

定时器使用规则:

  • 建议在 on_start() 中统一注册。
  • 多个定时器使用不同 payload,共用一个 on_timer()。
  • 定时回调中若要下单,应明确传入 symbol,不要依赖 self.symbol。
  • 定时回调中若要使用价格,应明确确认最新价来源;定时器本身不会创建新的 Bar 或 Tick。
  • 横截面/定期调仓优先使用 on_cross_section;schedule_* + on_timer 面向通用自定义时点任务。
  • 若目的是实盘盘前决策,使用 schedule_daily("09:25:00", ...) 或 on_before_trading,不要写 on_pre_open。

仓位管理器

set_sizer(sizer) 设置当下单方法未明确填写 quantity 时使用的仓位管理器。

类型 说明
FixedSize(size=100.0) 每次使用固定数量
PercentSizer(percents=10.0) 按资金百分比计算;10.0 表示 10%
AllInSizer() 按可用资金尽量买入

股票策略为了更易审查,仍建议在 buy() 和 sell() 中明确填写数量。仓位管理器只在 quantity=None 时参与计算。


框架数据对象

Bar K 线对象

字段 说明
timestamp UTC 纳秒时间戳
timestamp_iso ISO 格式时间
symbol 标的代码
open / high / low / close 开高低收
volume 成交量
extra 扩展行情字段

Tick 对象

字段 说明
timestamp / timestamp_iso 时间
symbol 标的代码
price 最新价
volume 成交量

Order 订单对象

常用字段:id、symbol、side、order_type、time_in_force、status、quantity、price、trigger_price、filled_quantity、average_filled_price、commission、position_effect、tag、reject_reason。

一次委托可能多次触发 on_order()。trade.quantity 是本次成交数量,订单累计成交数量应查看 Order.filled_quantity。

Trade 与 ClosedTrade

  • Trade:单次成交回报,适合在 on_trade() 中做实时响应。
  • ClosedTrade:已完成开平配对的交易统计,由 get_trades() 返回;未平仓时不会出现在这个列表里。

InstrumentSnapshot

常用字段:symbol、asset_type、tick_size、lot_size、multiplier。asset_type 是策略侧名称,例如 "STOCK"、"FUND"。


框架常量与枚举

推荐从 qmfquant 导入后比较,不要比较中文显示文本。

枚举 常用值
OrderSide Buy、Sell
OrderStatus New、Submitted、PartiallyFilled、Filled、Cancelled、Rejected、Expired
OrderType Market、Limit、StopMarket、StopLimit
TimeInForce GTC、IOC、FOK、Day
PositionEffect Auto、Open、Close
AssetType Stock、Fund
TradingSession PreOpen、Continuous、CallAuction、Break、Closed
from qmfquant import Order, OrderSide, OrderStatus, Strategy, TradingSession


class EnumUsageStrategy(Strategy):
    def on_order(self, order: Order) -> None:
        if order.side == OrderSide.Buy:
            self.log("这是一笔买单")
        if order.status in {
            OrderStatus.Filled,
            OrderStatus.Cancelled,
            OrderStatus.Rejected,
            OrderStatus.Expired,
        }:
            self.log("订单已经结束")

    def on_bar(self, bar) -> None:
        if self.ctx.session == TradingSession.Continuous:
            pass

TimeInForce.Day:日终结算会把当日仍未成交的 Day 单置为 Expired。若希望“收盘决策、次日成交”的委托不受当日过期约束,可使用默认 GTC。


回测成交、滑点与费用

一般应在回测或任务配置页面统一设置成交规则、费用和滑点。只有确实需要单笔覆盖时,才在下单方法中传入字典。

fill_policy

目的 配置
下一根 Bar 开盘价 {"price_basis": "open", "bar_offset": 1, "temporal": "same_cycle"}
当前 Bar 收盘价 {"price_basis": "close", "bar_offset": 0, "temporal": "same_cycle"}
下一根 Bar 收盘价 {"price_basis": "close", "bar_offset": 1, "temporal": "same_cycle"}

策略在当前 Bar 收到收盘信号后,使用下一根 Bar 开盘价成交通常更接近实际可执行过程。当前收盘成交属于较乐观的回测假设。

回测中 on_pre_open 内若直接调用 buy / sell / order_target_* 且未显式传成交模式,框架会按下一根 open 成交处理。这是框架侧时序语义,不等同于券商柜台已经实现集合竞价专用报单。

slippage 与 commission

滑点推荐始终使用字典,避免把 0.2 误写成 20%:

self.buy(
    "600000.SH",
    quantity=100,
    slippage={"type": "percent", "value": 0.0005},
    commission={"type": "percent", "value": 0.0003},
)

股票费用通常还包括最低佣金、卖出印花税和过户费,应优先在回测任务中统一配置。


A 股证券策略必须注意的规则

T+1 与可卖数量

A 股普通股票通常当日买入、下一交易日才可卖。总持仓不等于可卖持仓:

position = self.get_position(symbol)
available = self.get_available_position(symbol)

if position > 0 and available > 0:
    self.sell(symbol, quantity=available)

在 T+1 模式下尝试卖出超过 available 的数量,订单会被拒绝,并提示可用持仓不足。

交易单位

买入数量通常按 100 股整数倍。卖出清仓可能涉及零股,推荐使用 close_position() 让框架根据实际可用持仓处理。可在策略中设置 self.lot_size = 100。

资金和冻结

提交委托后可能冻结资金或持仓。重复下单前应检查活动订单:

if not self.get_open_orders(symbol):
    self.buy(symbol, quantity=100)

停牌、涨跌停和成交量

信号出现不保证订单一定成交。停牌、涨跌停、成交量不足、资金不足、T+1、风控限制或通道限制都可能导致未成交或拒单。策略应实现 on_order() 和 on_reject() 记录实际结果。

避免未来数据

  • 不要使用当前时点之后的数据。
  • 使用收盘价产生信号时,应合理设置成交时点,避免假设能够以已经确定的同一收盘价成交。
  • 调仓前确认该时点所需 Bar 已经完成。
  • 多标的数据应按标的和时间正确对齐。
  • 双流模式下用 history(..., freq=...) 明确取 Bar 还是 Tick,并用 history_cutoff 卡住可见终点。

现券账户不做空

现券账户只能卖出已持有仓位。不要调用 short(),也不要给 rebalance_positions / rebalance_to_topn 传入负仓目标。需要融券时,必须确认任务账户为信用账户且 supports_short_sell=True。

回测可写、实盘不可依赖的能力

能力 回测 实盘 建议
on_pre_open 触发 不触发 改用 schedule_daily 或 on_before_trading
get_holding_bars 正常计数 恒为 0 自行记录入场时间
schedule_weekly / schedule_monthly 可用 交易日历未知时不可用 实盘用 schedule_daily 自行判断
自定义 client_order_id 不支持 由通道分配 不要在策略里填写
short() 仅信用账户 现券账户拒绝 平多用 sell()

新股/新债打新不属于当前统一下单接口的默认承诺范围。


横截面与多标的

on_bar 按单事件流逐条触发。多标的轮动、排序、打分不要假设一次 on_bar 就代表全部标的已经同时更新。

推荐:日界钩子统一调仓

  • 用 on_before_trading 做“前一交易日信息可见”的盘前横截面准备与调仓。
  • 用 on_cross_section 做“看到当日所有标的的当拍 bar 后再同周期调仓”。
class CrossSectionStrategy(Strategy):
    lookback = IntParam(20, ge=5, le=120, title="回看窗口")

    def on_start(self) -> None:
        self.universe = ["600519.SH", "000858.SZ", "601318.SH"]
        self.warmup_period = int(self.params.lookback) + 1
        self.set_history_depth(int(self.params.lookback))
        for symbol in self.universe:
            self.subscribe(symbol)

    def on_before_trading(self, trading_date, timestamp) -> None:
        history_map = self.get_history_map(
            count=int(self.params.lookback),
            symbols=self.universe,
            field="close",
        )
        scores = {}
        for symbol, closes in history_map.items():
            if len(closes) < int(self.params.lookback) or np.isnan(closes).any():
                continue
            scores[symbol] = (closes[-1] - closes[0]) / closes[0]
        if not scores:
            return
        self.rebalance_to_topn(
            scores=scores,
            top_n=2,
            weight_mode="equal",
            long_only=True,
            liquidate_unmentioned=True,
        )

备选:收齐同一 timestamp 后再执行

没有固定调仓时点时,可在 on_bar 中先缓存同一时间片的标的,收齐后再执行一次横截面逻辑。停牌或缺失数据时,方案可能不触发;可设置“有效样本数达阈值即执行”。

常见坑

  • 停牌/缺失数据:某些标的当日无 Bar 时,收齐方案可能不触发。
  • Universe 漂移:成分股调整后若仍用旧列表,会出现权重与真实池不一致。
  • 调仓时点与成交策略错配:收盘信号配下一根开盘成交时,不要按当日收盘价假设已经换完仓。
  • 历史长度不足:新上市或停牌恢复标的数据窗口不完整,评分前检查长度和 NaN。
  • 仓位未收敛:多标的先卖后买若资金未及时释放,可能导致买入不足;目标仓位 API 会先减后加,下一时点可二次收敛。

策略案例

双均线策略(股票 T+1)

import logging

import numpy as np

from qmfquant import Bar, IntParam, Order, OrderStatus, Strategy, Trade


class DoubleMovingAverageStrategy(Strategy):
    """金叉买入,死叉卖出可卖仓位。"""

    fast_window = IntParam(5, ge=2, le=100, title="短均线周期")
    slow_window = IntParam(20, ge=3, le=250, title="长均线周期")
    quantity = IntParam(100, ge=100, title="每次交易股数")

    def on_start(self) -> None:
        if int(self.params.fast_window) >= int(self.params.slow_window):
            raise ValueError("fast_window 必须小于 slow_window")

        self.target_symbol = None
        self.pending_order_id = None
        self.warmup_period = int(self.params.slow_window) + 1
        self.set_history_depth(int(self.params.slow_window) + 1)
        self.lot_size = 100
        self.schedule_daily("14:50:00", "daily-risk-check")
        self.log("双均线策略启动")

    def on_bar(self, bar: Bar) -> None:
        if self.target_symbol is None:
            self.target_symbol = bar.symbol
            self.subscribe(bar.symbol)

        if bar.symbol != self.target_symbol:
            return
        if self.get_open_orders(bar.symbol):
            return

        closes = self.get_history(
            int(self.params.slow_window) + 1,
            bar.symbol,
            field="close",
        )
        if np.isnan(closes).any():
            return

        fast = int(self.params.fast_window)
        slow = int(self.params.slow_window)
        fast_prev = float(np.mean(closes[-fast - 1:-1]))
        fast_now = float(np.mean(closes[-fast:]))
        slow_prev = float(np.mean(closes[-slow - 1:-1]))
        slow_now = float(np.mean(closes[-slow:]))

        golden_cross = fast_prev <= slow_prev and fast_now > slow_now
        death_cross = fast_prev >= slow_prev and fast_now < slow_now

        if golden_cross and self.get_position(bar.symbol) == 0:
            receipt = self.buy(
                bar.symbol,
                quantity=int(self.params.quantity),
                tag="golden-cross",
            )
            self.pending_order_id = receipt.primary
            self.log(f"金叉,提交买入:{receipt.primary}")
        elif death_cross and self.get_available_position(bar.symbol) > 0:
            self.close_position(bar.symbol)
            self.log("死叉,提交清仓")

    def on_timer(self, payload: str) -> None:
        if payload == "daily-risk-check":
            account = self.get_account()
            self.log(
                f"每日检查:权益={account.get('equity', 0):.2f} "
                f"现金={account.get('cash', 0):.2f}"
            )

    def on_order(self, order: Order) -> None:
        self.log(
            f"订单状态:{order.id} {order.status} "
            f"{order.filled_quantity}/{order.quantity}"
        )
        if order.id == self.pending_order_id and order.status in {
            OrderStatus.Filled,
            OrderStatus.Cancelled,
            OrderStatus.Rejected,
            OrderStatus.Expired,
        }:
            self.pending_order_id = None

    def on_trade(self, trade: Trade) -> None:
        self.log(
            f"成交回报:{trade.symbol} {trade.side} "
            f"{trade.quantity}@{trade.price}"
        )

    def on_reject(self, order: Order) -> None:
        self.log(f"拒单:{order.id},原因:{order.reject_reason}", logging.ERROR)

    def on_stop(self) -> None:
        self.log(f"策略停止:权益={self.equity:.2f} 持仓={self.positions}")

固定比例止盈止损

from qmfquant import Bar, FloatParam, IntParam, OrderSide, Strategy


class StopTakeStrategy(Strategy):
    quantity = IntParam(100, ge=100, title="买入数量")
    take_pct = FloatParam(8.0, ge=0.1, le=100, title="止盈百分比")
    stop_pct = FloatParam(4.0, ge=0.1, le=50, title="止损百分比")

    def on_start(self) -> None:
        self.entry_price = {}

    def on_bar(self, bar: Bar) -> None:
        symbol = bar.symbol
        if self.get_open_orders(symbol):
            return

        pos = self.get_position(symbol)
        available = self.get_available_position(symbol)
        entry = self.entry_price.get(symbol) or self.position.entry_price

        if pos == 0:
            if bar.close > bar.open:
                self.buy(symbol, quantity=int(self.params.quantity), tag="entry")
            return

        if entry and entry > 0 and available > 0:
            pnl_pct = (bar.close - entry) / entry * 100
            if pnl_pct >= float(self.params.take_pct) or pnl_pct <= -float(self.params.stop_pct):
                self.close_position(symbol)
                self.log(f"{symbol} 触发止盈止损,浮动={pnl_pct:.2f}%")

    def on_trade(self, trade) -> None:
        if trade.side == OrderSide.Buy:
            self.entry_price[trade.symbol] = trade.price

成本价也可直接读 self.position.entry_price / self.position.avg_price。不要用 get_holding_bars() 做实盘持仓天数判断。

实盘盘前检查(替代 on_pre_open)

from qmfquant import Strategy


class PreOpenLiveStrategy(Strategy):
    def on_start(self) -> None:
        self.schedule_daily("09:25:00", "pre_open")
        self.schedule_daily("14:55:00", "before_close")

    def on_before_trading(self, trading_date, timestamp) -> None:
        self.log(f"交易日开始:{trading_date},此处只能看到前一日信息")

    def on_timer(self, payload: str) -> None:
        if payload == "pre_open":
            account = self.get_account()
            self.log(f"盘前资金检查:现金={account.get('cash', 0):.2f}")
        elif payload == "before_close":
            for symbol, qty in self.positions.items():
                available = self.get_available_position(symbol)
                self.log(f"收盘前 {symbol} 持仓={qty} 可卖={available}")

回测若确需“盘前决策、本次 open 成交”,可以另外实现 on_pre_open;上线实盘时必须保证同一套逻辑也能通过 on_timer 或 on_before_trading 走通。


常见问题

编译与回测

为什么策略修改后无法重新回测

修改策略后,在策略回测页面点击编辑参数,点击保存,即可重新回测。

为什么回测详情无数据

可能原因:策略没有产生任何成交;回调签名写错导致主逻辑从未执行;所选时间范围没有对应标的数据。

为什么回测很慢

运行周期过小(如 1 分钟)会显著增加事件数量。日频策略建议先用日线验证逻辑。

策略文件编译失败

检查缩进、中文标点、回调函数名是否写错,以及是否把策略当成脚本直接运行。编译成功后再回测,不要在编辑器中 Run Python File。

回调与执行

为什么 on_bar() 没有执行

  1. 是否在 on_start() 中订阅了正确标的,或任务是否指定了标的。
  2. 回测数据是否包含该标的和所选时间范围。
  3. warmup_period 是否大于实际数据量。
  4. 方法名和参数是否准确写成 on_bar(self, bar)。

为什么实盘 on_pre_open 从不进入

实盘不会触发 on_pre_open。请改用 schedule_daily("09:25:00", "pre_open") 触发 on_timer,或把逻辑放到 on_before_trading。

为什么历史数组里有 NaN

请求的历史长度大于当前已积累的数据量。增加预热数据,并在计算前使用 np.isnan(...).any() 检查。双流模式下确认 freq 是否选对。

交易执行

为什么信号触发但没有持仓

信号只代表策略提交了委托。继续检查 on_order() 状态、on_reject() 原因、on_trade() 是否收到成交,以及资金、冻结资金、可卖持仓和交易单位。

为什么重复下单

行情回调可能在上一笔订单成交前再次触发。下单前检查 get_open_orders(symbol),或保存 receipt.primary,等订单进入终态后再清除等待标记。

为什么当天买入后无法卖出

这是股票 T+1 规则。使用 get_available_position() 读取当前可卖数量。

为什么调用 short() 报不支持融券卖空

现券账户不能开空。平多请用 sell() 或 close_position()。

为什么限价单没有成交

限价单只有在价格、成交量、交易状态和撮合规则都满足时才会成交。它可能继续保持活动状态,也可能最终撤销或过期。

为什么持仓满 N 根 K 线的离场在实盘不生效

get_holding_bars() 实盘恒返回 0。请自行记录入场日期或入场 bar 计数。

为什么多标的数据混在一起

每次回调都使用 bar.symbol 或 tick.symbol,调用历史、持仓和下单方法时明确传入该标的。每个标的的自定义状态保存在字典中。

日志与调试

写了 self.log 但看不到输出

确认策略已编译并实际启动回测或任务;日志在客户端退出后保存在对应 logs\backtest_server 或 logs\task_server 目录。

是否支持断点调试

策略由期魔方引擎调度,不要在编辑器中直接运行脚本调试。以日志和回测明细为主。

下单接口

能不能自己指定 client_order_id

回测不支持自定义客户订单号。策略侧使用 buy() / sell() 即可,订单号由执行层分配。不要把 submit_order() 当作常规编写接口。

buy() 返回值是成交了吗

不是。返回 OrderReceipt 只表示请求已提交。是否接受看 on_order(),是否成交看 on_trade()。


API 速查

用户重写的回调

签名 说明
on_start(self) 启动时一次:订阅、定时器、历史深度
on_resume(self) 快照恢复时,早于 on_start
on_stop(self) 结束时;不应下单
on_bar(self, bar) 每根 K 线
on_tick(self, tick) 每个 Tick
on_timer(self, payload) 定时器
on_order(self, order) 订单状态变化
on_trade(self, trade) 每次成交
on_reject(self, order) 拒单
on_error(self, error, source, payload=None) 回调异常
on_before_trading(self, trading_date, timestamp) 交易日前边界;前一日信息可见
on_pre_open(self, event) 仅回测盘前
on_cross_section(self, trading_date, timestamp) 当日完整切片后的横截面调仓
on_after_trading(self, trading_date, timestamp) 交易日后边界
on_portfolio_update(self, snapshot) 账户快照变化

用户调用的方法

行情与时间:

方法 说明
subscribe(instrument_id) 订阅行情
set_history_depth(depth) 设置历史缓存长度
history(symbol, normalized_field, count, history_cutoff, freq) 双流历史,freq 为 bar 或 tick
get_history(count, symbol=None, field="close") Bar 历史数组
get_history_map(count, symbols, field="close") 多标的历史
get_history_df(count, symbol=None) K 线表格
get_instrument(symbol) 等 标的静态信息
log(msg, level=logging.INFO) 日志
schedule / schedule_daily / schedule_weekly / schedule_monthly 定时器
set_sizer(sizer) 仓位管理器

交易与调仓:

方法 说明
buy(...) / sell(...) 买入 / 卖出已有多头
close_position(symbol=None) 清仓
order_target / order_target_value / order_target_percent 目标仓位
rebalance_weights / rebalance_positions / rebalance_to_topn 组合调仓
cancel_order / cancel_group / cancel_all_orders 撤单
place_oco / place_bracket / place_trailing_stop 复杂订单

查询:

方法 说明
get_position / get_available_position 总持仓 / 可卖
get_holding_bars 持有 Bar 数;实盘恒为 0
get_account 账户快照
get_open_orders / get_order 活动订单 / 单笔订单
get_trades 已闭合交易
get_execution_capabilities 当前通道能力

返回值判读

返回类型 表示什么 不表示什么
None 无同步结果,或通过回调异步反馈 不表示交易成功
OrderReceipt 下单请求已形成回执 不表示已成交
list[str] 订单编号 一次组合操作提交的订单编号 空列表通常表示无需下单
list[str] 入选标的 rebalance_to_topn() 的筛选结果 该列表不是订单编号

判断交易是否真正完成:

  1. 保存 OrderReceipt.primary。
  2. 在 on_order() 中观察是否进入终态。
  3. 在 on_trade() 中记录每次实际成交。
  4. 使用 get_position()、get_available_position() 和 get_account() 复核最终账户状态。

回测和上线前检查清单

  • 策略类正确继承 Strategy,回调签名与本文一致。
  • 参数用内联字段声明,有默认值、类型和范围校验。
  • on_start() 订阅了全部需要的标的,并设置了 set_history_depth()。
  • 历史数据不足或含 NaN 时不会交易。
  • 双流取数时明确 freq="bar" 或 freq="tick"。
  • 多标的逻辑始终使用事件中的 symbol。
  • 下单数量符合股票交易单位。
  • 卖出使用可卖持仓并考虑 T+1。
  • 现券账户未调用 short(),也未设置负仓目标。
  • 下单前检查活动订单,避免重复委托。
  • 已实现必要的 on_order()、on_trade() 和 on_reject() 日志。
  • 实盘盘前逻辑使用 schedule_daily 或 on_before_trading,不依赖 on_pre_open。
  • 持仓时长离场未依赖 get_holding_bars()。
  • 成交时点没有使用未来数据。
  • 佣金、最低佣金、印花税、过户费和滑点设置合理。
  • 已在多个市场阶段完成回测,并经过模拟交易验证后再考虑连接实盘账户。

风险提示:历史回测和模拟交易结果不代表未来收益。实盘前应充分验证策略、数据、费用、成交假设、风控限制和交易通道能力。

已复制到剪贴板
机器学习修订 R1

返回机器学习板块

评论