期魔方证券策略编写文档
适用对象:使用期魔方编写股票、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 事件,框架大致按以下顺序分发:
on_order/on_trade(若拒单则额外触发on_reject)- 框架钩子(
on_before_trading/on_after_trading、on_cross_section、on_portfolio_update) - 用户事件回调(
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 |
无新行情 | 不应下单 | 结束日志、资源清理 |
启动阶段细节
一般按以下顺序运行:
- 框架创建策略对象并注入页面参数到
self.params。 - 如果任务从快照恢复,调用
on_resume()。 - 调用
on_start()。 - 开始接收 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() 没有执行
- 是否在
on_start()中订阅了正确标的,或任务是否指定了标的。 - 回测数据是否包含该标的和所选时间范围。
warmup_period是否大于实际数据量。- 方法名和参数是否准确写成
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() 的筛选结果 |
该列表不是订单编号 |
判断交易是否真正完成:
- 保存
OrderReceipt.primary。 - 在
on_order()中观察是否进入终态。 - 在
on_trade()中记录每次实际成交。 - 使用
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()。 - 成交时点没有使用未来数据。
- 佣金、最低佣金、印花税、过户费和滑点设置合理。
- 已在多个市场阶段完成回测,并经过模拟交易验证后再考虑连接实盘账户。
风险提示:历史回测和模拟交易结果不代表未来收益。实盘前应充分验证策略、数据、费用、成交假设、风控限制和交易通道能力。

评论
登录后参与讨论,与站内账号体系共用。