股票与 ETF
下单、撤单与回报
股票与 ETF 二级交易接口:order_shares、order_values、target_stock_group_order、cancel_order 的参数与返回,市价与限价样式,申报数量规则与费用;订单对象、成交对象与订单状态,以及在回报中管理订单和成交去重。
本页讲股票与 ETF 的二级市场买卖:怎么下单、怎么撤单,以及怎么从回报里确认到底成交了多少。ETF 一级申购赎回用另一组接口,见 ETF 申购与赎回。
返回订单不等于成交。列表非空也可能是拒单;前置检查没通过时,返回的还可能是空列表或 None。每张订单都要看 status 和 message,实际成交看成交回报。
一、按数量交易
order_shares(
account_id, id_or_ins, quantity, style=None, retry_num=0,
remark=None, margin_usage="CASH"
)
| 参数 | 类型、默认值 | 用法 |
|---|---|---|
account_id |
字符串,必填 | 统一证券账户编号 |
id_or_ins |
字符串,必填 | 完整股票或 ETF 代码 |
quantity |
整数,必填 | 正数买入、负数卖出;股票为股、ETF 为份,不传零 |
style |
订单样式,默认 None |
不传或 MarketOrderStyle() 为市价;限价见下文 |
retry_num |
整数,默认 0 |
追单计数参数,普通策略保留默认值;不是「自动重试次数」 |
remark |
字符串,默认 None |
自定义备注,会带进订单和成交回报 |
margin_usage |
字符串,默认 "CASH" |
普通现金买卖保留默认;两融使用专用方法 |
可选参数用关键字传入,例如 style=MarketOrderStyle()。返回订单对象列表,可以用 for order in orders or [] 遍历。最终受理数量读 order.quantity,实际成交数量读 trade.volume。
二、按金额交易
order_values(
account_id, id_or_ins, amount, style=None, retry_num=0,
remark=None, margin_usage="CASH"
)
amount 是本次交易金额,单位元,正数买入、负数卖出;它不是目标持仓市值,也不是目标权重。其余参数和返回与 order_shares 相同。
买入金额用于估算可买数量及费用,受申报数量单位影响,实际成交金额可能小于传入金额,不保证用完;卖出仍受可卖量限制,不会变成融券卖出。交易时间以外调用返回空列表。
三、市价与限价
| 写法 | 用法 |
|---|---|
MarketOrderStyle() 或不传 style |
市价,不传价格 |
LimitOrderStyle(limit_price) |
限价;limit_price 为正价格,元/股或元/份 |
LimitOrderStyle 的完整构造参数是 limit_price, time_condition=TimeConditionType.IOC, gtd_date=None,回测中保留后两个默认值。当前股票下单接口不提供可依赖的跨 Bar 挂单行为,不要靠给 LimitOrderStyle 传有效期来设计挂单策略。价格要符合目标证券的价格档位,不要把 ETF 价格统一截成两位小数。
下面三笔是相互独立的调用示例,按自己的信号选用,不要无条件放进每次行情回调:
def buy_stock(context):
return order_shares(
context.account, '600000.SH', 100,
style=MarketOrderStyle(), remark='买入股票',
)
def buy_etf_with_limit(context, limit_price):
return order_shares(
context.account, '510300.SH', 1000,
style=LimitOrderStyle(limit_price), remark='限价买入ETF',
)
def buy_etf_by_amount(context):
return order_values(
context.account, '159919.SZ', 5000.0,
style=MarketOrderStyle(), remark='按金额买入ETF',
)
四、按预算限价买入
下面的辅助函数用于 handle_data,演示沪深普通股票按现金预算限价买入:预算与可用现金取小,按限价加 2% 余量折算股数,再向下取整到 100 股。调用方负责每日去重和订单跟踪。
from panda_backtest.api.api import order_shares, LimitOrderStyle
def buy_with_budget(context, symbol, budget, limit_price):
if not (0 < limit_price < float("inf")):
raise ValueError("限价必须是有限正数")
account_id = context.run_info.stock_account
account = context.stock_account_dict[account_id]
usable = min(float(budget), max(0.0, account.cash - 10.0))
quantity = int(usable / (limit_price * 1.02) / 100) * 100
if quantity < 100:
return []
return order_shares(account_id, symbol, quantity,
style=LimitOrderStyle(limit_price), remark="预算限价买入")
五、申报数量与费用
数量。 普通沪深股票和本页的 ETF 示例以 100 股/份为数量基数;科创板要结合证券资料里的最小申报量处理。北交所买入通常至少 100 股,个别证券可设置更高的最小申报量,超出部分按 1 股递增,例如最小量为 100 股时可以报 235 股;不足 100 股的卖出须一次卖出全部可卖余额。不要对北交所委托和部分成交一律向下取整到 100 股,也不要把普通 A 股示例中的取整函数直接套到所有证券。
价格。 ETF 价格通常精确到 0.001 元,以订单上的 price_tick 为准。
成交。 限价约束成交价格,但不保证成交。停牌、缺少有效价格、价格超出限制、数量不合法、资金或可卖数量不足,都会让申请无法成交;存在成交量限制时,一笔委托可能只成交一部分。回测用的是历史日线或分钟数据,不是逐笔盘口排队的还原。
费用。 ETF 二级买卖收取成交费用,不计股票卖出印花税;一级申赎另计申购、赎回费用。任务设置的费用和滑点会影响可买数量与收益。二级实际费用读 trade.cost,策略里不要再手工从现金扣费。
先卖后买。 只需按金额调仓时可用 order_values;要严格控制每只 ETF 的份额,就用 order_shares 自己算数量。先卖后买时,要在卖出回报之后重新读取 account.cash,不要把尚未成交的卖出金额提前计入预算。
六、按目标数量调仓
target_stock_group_order(account, symbol_dict, price_type=0):
| 参数 | 用法 |
|---|---|
account |
账户编号,不是账户对象 |
symbol_dict |
{完整证券代码: 目标持仓数量};目标数量为非负整数股/份 |
price_type |
默认 0,保留默认 |
返回 None,逐笔委托和成交通过二级回调跟踪。
这个方法按整个账户的目标持仓调仓:字典里没列出的现有持仓也会被卖出,空字典就是对全部股票持仓发出清仓委托(受 T+1、停牌、成交量和价格限制,不保证立即清空)。只管理账户中一部分证券时,改用 order_shares 逐笔下单,不要传不完整的目标字典。
七、撤单
cancel_order(account_id, order_id) 用于二级订单,传订单对象的 order_id(不是 id)。
它不返回撤单是否成功,返回值为 None,需要通过订单回报和 stock_order_cancel 确认。只能撤未结束的部分,已成交的数量不会撤回;不要用它撤 ETF 一级申请。
八、订单对象
order_shares、order_values 和两融买卖方法返回列表里的元素,以及 on_stock_order_rtn、stock_order_cancel 收到的参数,都是订单对象。
| 属性 | 类型、单位 | 含义 |
|---|---|---|
order_id |
字符串 | 订单编号 |
account |
字符串 | 账户编号 |
order_book_id |
字符串 | 完整证券代码 |
order_book_name |
字符串 | 证券名称 |
datetime |
datetime |
委托时点 |
side |
整数 | SIDE_BUY=0 买入,SIDE_SELL=1 卖出 |
price_type |
整数 | MARKET=1 市价,LIMIT=2 限价 |
price |
数值,元/股或元/份 | 委托价格或市价参考价格,不代表实际成交价 |
quantity |
整数,股/份 | 委托数量,取正值;方向看 side |
filled_quantity |
整数,股/份 | 本单累计已成交数量 |
unfilled_quantity |
整数,股/份 | 本单未成交数量;终态后也可能大于零 |
cur_filled_quantity |
整数,股/份 | 本次成交更新的数量;统计成交优先用成交回报 |
transaction_cost |
数值,元 | 委托时的预估费用,不作为实际费用累计 |
status |
整数 | 订单状态,见下表 |
message |
字符串 | 状态说明或拒绝原因 |
remark |
字符串或 None |
下单时传入的备注 |
stock_type |
整数 | 普通股票 0、ETF 2 |
settlement |
字符串 | 可卖规则标记,如 T0、T1;实际卖出读持仓的 sellable |
price_tick |
数值,元 | 该证券最小价格变动单位 |
asset_source |
字符串 | 二级交易为 SECONDARY |
margin_usage |
字符串 | 现金或信用用途,见融资融券 |
force_liquidation |
布尔值 | 是否为自动强平订单 |
被前置拒绝的订单,部分属性可能保留空值或零值:先读 status、message,不要用它的价格计算收益。
九、订单状态
| 状态常量 | 数值 | 含义 | 是否结束 |
|---|---|---|---|
| — | 0 |
待报 | 否 |
ACTIVE |
1 |
已受理(已报)、待处理 | 否 |
PartTradedQueueing |
4 |
部分成交,余量仍在队列 | 否 |
NoTradeQueueing |
6 |
尚未成交,仍在队列 | 否 |
FILLED |
2 |
全部成交 | 是 |
CANCELLED |
3 |
已撤销剩余数量 | 是 |
PartTradedNotQueueing |
5 |
部分成交,余量结束 | 是 |
NoTradeNotQueueing |
7 |
未成交,订单结束 | 是 |
REJECTED |
-1 |
拒绝 | 是 |
| — | 8 / 9 |
未触发 / 已触发 | 不据此认定完成 |
状态 5 既有实际成交、又有未成交余量:既不能当成全部成交,也不能当成完全没成交。撤单回调也可能携带拒单状态,所以要同时读 status 和 message。
十、成交对象
on_stock_trade_rtn(context, trade) 收到的是成交对象。股票、ETF 和两融买卖都用这组属性:
| 属性 | 类型、单位 | 含义 |
|---|---|---|
trade_id |
字符串 | 成交编号,可用于去重 |
order_id |
字符串 | 对应订单编号 |
account_id |
字符串 | 账户编号(注意不是 account) |
contract_code |
字符串 | 完整证券代码 |
contract_name |
字符串 | 证券名称 |
business |
整数 | 0 买入,1 卖出 |
volume |
整数,股/份 | 本笔实际成交数量 |
price |
数值,元/股或元/份 | 本笔实际成交价 |
cost |
数值,元 | 本笔实际成交费用 |
trade_date |
字符串 | 交易日,YYYYMMDD |
gmt_create |
字符串 | 自然日,YYYYMMDD,不是完整时间戳 |
gmt_create_time |
字符串 | 成交时分秒,HHMMSS |
order_remark |
字符串或 None |
原订单备注 |
stock_type |
整数 | 普通股票 0、ETF 2 |
settlement |
字符串 | 对应可卖规则标记 |
asset_source |
字符串 | 二级交易为 SECONDARY |
margin_usage |
字符串 | 本笔交易的现金或信用用途 |
force_liquidation |
布尔值 | 是否来自自动强平 |
status |
整数 | 该笔成交对应的订单状态 |
成交对象还提供兼容属性 order_book_id(同 contract_code)、quantity(同 volume)、side(同 business)、transaction_cost(同 cost)。同一份策略里固定用一套名称。
订单和成交对象的字段不能互换,最容易写错的几处:
| 内容 | 订单 order |
成交 trade |
|---|---|---|
| 账户 | account |
account_id |
| 证券代码 | order_book_id |
contract_code |
| 方向 | side |
business |
| 数量 | quantity 委托数量、filled_quantity 累计成交 |
volume 本次成交,按 trade_id 去重后累计 |
| 价格 | price 委托价,不是成交价 |
price 本次成交价 |
| 费用 | transaction_cost 委托阶段预估 |
cost 本次实际费用 |
十一、在回报中管理状态
- 订单按
(账户, 订单编号)跟踪;成交按(账户, 交易日, 订单编号, 成交编号)去重。 - 已记录为终态的订单,不因较早到达的活动回报、或下单函数的返回值而重新加入待处理表。
- 成交统计只累计
trade.volume和trade.cost,不要同时累计订单上的费用。 - 下单返回后要继续用资金或证券时,重新读取账户;不要把预计的卖出收入或买入数量提前当作已到账资产。
- 撤单不会抹掉已经发生的成交;部分成交时只按实际成交数量更新统计。
- 自动强平同样走这些回调,用
force_liquidation区分。
如果需要按订单号查询本策略提交过的订单,在初始化时建一张自己的订单表,在订单、撤单回调中更新,以 (account, order_id) 为键。一级订单另存一张表,避免编号撞车:
def remember_secondary_order(context, order):
if order.account == context.account:
context.orders_by_id[(order.account, order.order_id)] = order
def lookup_secondary_order(context, order_id):
return context.orders_by_id.get((context.account, order_id))
使用前在 initialize 中创建 context.orders_by_id = {},并在二级订单和撤单回调里调用 remember_secondary_order。这两个是示例自定义函数,不是交易 API。表里保存的是订单的当前对象;要保留每次状态变化,需另行记录当次的属性值。
完整示例中的 pending_orders 是另一种用法:只保存待处理订单,进入终态后就移除。完整写法见股票与 ETF 示例。
