入坑Ptrade量化交易的朋友,十有八九不是栽在策略思路上,而是栽在交易函数上。明明回测收益挺好看,一到仿真或者实盘,要么委托废单,要么仓位调错,要么打新漏申购,问题基本都出在下单、打新、持仓管理这几个环节的函数用法上。这篇整理的是我自己实际用下来最常用的Ptrade交易函数,把零基础最容易踩的坑一并说清楚。适合刚写完第一个策略、准备跑仿真和实盘的初学者,也适合把回测代码迁移到当前撮合环境的老手参考。
先说清楚,本文不聊选股因子和参数优化,只讲交易落地:下单怎么下、打新怎么打、持仓怎么管。看完你至少能做到:看懂别人的下单代码、自己写出按金额和按目标仓位下单的模板、把打新流程自动化、不因为T+1和可用数量这类规则把仓位搞乱。
1. Ptrade交易函数的基础运行逻辑
1.1 交易函数不是普通Python函数
很多新手在策略里直接调用order()就以为下单成功并且立刻成交了,这是个认知偏差。Ptrade里的交易函数本质上是向柜台系统提交委托的接口。你调用order(),系统返回的是一个委托编号,不代表已经成交。真实成交还要经过交易所撮合,成交回报会异步推送到策略里,最终反映到持仓数据上。
给每个底层API一个直观类比:把Ptrade的交易接口想象成一条“委托流水线”,你写order()就像把指令投进流水线入口。返回的字符串是订单ID,之后要主动查get_open_orders()看它是否在排队,再查get_trades()看是否已经成交。那些“一发入魂”“下一秒就更新持仓”的预期,都得改过来。
1.2 交易代码应该写在哪个回调函数里
策略执行流程一般分三层:initialize启动初始化、run_daily定时任务、盘中事件回调。最常见的问题,是有人把一堆交易代码直接怼进initialize里,策略一启动就跑出一大串order(),甚至在下单前还没等到足够行情数据,委托价格异常,直接废单。
正确做法是让交易动作放到定时任务中:
def initialize(context): run_daily(open_trade, '09:40') run_daily(ipo_task, '10:00') def open_trade(context): order('600000.SS', 100) def ipo_task(context): # 申购逻辑,见第三部分 pass有人会问为什么选09:40而不是09:30。集合竞价刚结束的那几分钟市场波动很大,很多策略会主动避开前五分钟,给行情一个稳定窗口。这个时间可以根据自己的策略调整,关键是理解:run_daily决定交易函数什么时候被系统调用,不是你在函数里手动调用它就立刻跑。
1.3 回测、仿真、实盘的环境差异
同一段代码在三种环境下行为可能不一样。回测是内部撮合模拟,委托大概率按历史的盘中或收盘价格序列匹配成交;仿真和实盘接的是真实行情、真实柜台,只是资金是否真实。打新额度、可转债申购、新股申购这类数据接口,有些只在仿真或实盘环境中可用,回测环境会返回空列表,这是环境差异,不是策略写错。
所以排查函数报错时,先分清当前运行环境。不同环境返回的数据对象字段可能不完全一致。上线前,我会用print()把返回对象的真实字段打出来看一遍,避免代码里想当然的字段在实盘环境里变成None。
2. 下单函数:把核心的五个函数玩明白
2.1 order():按股数下单
order(security, amount)是最基础的委托函数。security写证券代码,格式类似'600000.SS'(上交所)、'000001.SZ'(深交所)。amount为正数代表买入数量,负数代表卖出数量。函数返回值是订单编号,后面撤单会用到。
举个例子,一个ETF轮动策略,开盘决定买入某ETF 10000份:
def buy_etf(context): order('510300.SS', 10000)卖出直接写负数:
order('510300.SS', -10000)注意,卖出委托如果超过“可卖数量”会废单。这里的可卖数量不是总持仓,而是扣除T+1限制、已挂委托占用之后的数量。A股T日买入的股票T+1日才可卖出,所以策略里不能拿总持仓数直接卖。这个规则在开盘前十分钟尤其容易踩,因为没有更新的持仓数据,系统还在用昨天的可用数量校验。
2.2 order_value():按金额下单
有些场景你不关心精确股数,只关心投入金额,比如定投策略每周买2000元。这时候用order_value(security, value)按金额下单,系统会按当前价格自动换算股数。
def weekly_invest(context): order_value('510500.SS', 2000)value为正代表买入该金额,为负代表卖出该金额对应的市值。这里有个细节:按金额买入时,系统通常会先把股数折算出来,再做整手修正,所以实际成交金额可能略小于2000元;卖出时一般向上凑整,保证卖出的价值不小于目标金额。别看到2000元就以为账户里必须有2000元整,单子实际可能是1900元或2100元附近,设计仓位时先接受这个误差。
2.3 order_target() / order_target_value():目标调仓函数
这是最实用的仓位管理函数。order_target(security, amount)把某标的持仓调整到指定股数;order_target_value(security, value)把市值调整到指定值。它们内部会计算当前持仓和目标之间的差额,再自动发委托。
举个股债平衡的例子,股票仓位占50%,债券占50%,每日再平衡一次:
def rebalance(context): acct = get_account() target_stock = acct.total_value * 0.5 order_target_value('600000.SS', target_stock) order_target_value('511010.SS', acct.total_value - target_stock)这里用order_target_value的好处是不会算错买卖方向。你只需要给目标,系统自己判断要买还是要卖。这个函数在再平衡策略里出现频率极高,建议理解成“仓位跳转到目标位置”,而不是普通买卖。
2.4 撤单和委托状态管理
如果下单后价格偏离很快,你可能想撤掉重下。撤单需要订单编号,就是order()的返回值:
oid = order('600000.SS', 100) if need_cancel(oid): cancel_order(oid)批量撤掉当日所有未成交委托:
for order_id in get_open_orders(): cancel_order(order_id)注意,get_open_orders()在不同环境里返回的可能只是订单编号列表,也可能是包含订单详情的列表。先用print()确认再处理。撤单本身也是异步操作,撤单指令发出时,原委托可能已经在交易所成交了一部分,这种部分成交后再撤单的“残单”,做T+0品种时要额外处理。
下面把四个核心下单函数的差异收成一张表:
| 函数 | 传入参数 | 适用场景 | 注意事项 |
|---|---|---|---|
| order | 证券代码、股数 | 精确股数买卖 | 数量为负表示卖出 |
| order_value | 证券代码、金额 | 定投、固定金额投入 | 买入自动向下取整手数 |
| order_target | 证券代码、目标股数 | 调仓到目标持仓量 | 自动计算买卖差额 |
| order_target_value | 证券代码、目标市值 | 再平衡、目标仓位 | 按目标市值跳转,适合权重管理 |
3. 打新(新股申购)函数与自动化
3.1 把打新业务逻辑拆成代码能跑的流程
打新是A股一种靠持仓市值参与新股摇号申购、中签后按规定缴款、上市后获取溢价的增强收益方式。把打新思路代码化,需要解决几个问题:今天有没有新股可打?有没有额度?能申购多少?什么时候提交委托?中签后怎么处理?
完整流程放代码里通常分三段:
- 启动时或定时获取当日可申购新股列表。
- 根据账户的可申购额度,对每只新股提交申购委托。
- 申购后系统参与摇号,中签结果在T+2日出来,再检查持仓和资金。
3.2 申购额度与申购单位
打新额度不是想买多少就买多少。沪市和深市市值分别计算:上海市场每1万元市值对应1000股申购额度,深圳市场每5000元市值对应500股额度,单只新股不能超过申购上限。额度按T-2日前20个交易日的日均市值计算。
两个市场的申购单位差别要记住:
| 市场 | 市值与额度关系 | 申报单位 |
|---|---|---|
| 沪市 | 每1万元市值对应1000股额度 | 1000股整数倍;科创板按500股整数倍 |
| 深市 | 每5000元市值对应500股额度 | 500股整数倍 |
在策略代码里,最稳妥的方式是直接读取账户接口返回的可申购额度,不要自己拿当前总资产估算。当前市值、可用资金和打新额度经常不是同一个数,尤其账户里有ETF、融资融券等资产时,市值归属规则更复杂。如果当前环境没有直接提供额度接口,宁可在界面里人工确认一次,也别在两个市场的配比上拍脑袋。
3.3 自动打新的通用模板
Ptrade不同券商版本对打新接口的封装差异很大,有的环境提供专门的打新函数或数据接口,有的只能用申购代码走普通买入委托。下面给一个通用模板,重点是先把流程写顺,再替换成当前环境实际提供的函数:
def ipo_task(context): # 获取当日可申购新股列表,函数名随环境调整 ipo_list = get_subscribe_list() if not ipo_list: return for item in ipo_list: code = item.get('subscribe_code') max_qty = item.get('max_qty') available_qty = get_subscribe_quota(code) if available_qty <= 0: continue order(code, min(max_qty, available_qty))要特别提醒,打新提交的证券代码通常是“申购代码”,不是上市后的交易代码。有些票两个代码不同,写成交易代码直接废单。新股申购委托要在规定交易时段内提交,早了晚了都不行。同一只新股、同一个账户只能提交一次,不能像普通交易那样分批加仓。
3.4 打新自动化中的风控细节
打新看起来简单,坑一点不少。申报数量超过单只新股申购上限会被判定为废单;如果你按“满额申购”去写,要确保取到的上限字段是股数而不是金额。打新申报时不冻结资金,但中签后必须保证T+2日规定时间前账户有足够资金缴款,否则视为放弃。连续12个月内累计3次中签不缴款,会被列入限制名单,6个月内不能打新。
还有一类容易漏掉的业务细节:不同市场的打新规则差异很大,申购规则和资金占用方式不同。写打新模板前,先确认你的账户在哪些市场具备打新资格,只对已开通权限的市场做自动化。另外,参与打新不等于无风险,新股上市也有破发可能,策略里最好设定可接受的申购范围,别把所有账户资金都压在打新上。
4. 持仓管理:给交易代码装上眼睛
4.1 读取持仓的函数
持仓管理是很多人忽略的一块。策略需要知道“我到底有什么,有多少,成本多少,可卖多少”,对应这些查询函数:
- get_positions():获取全部持仓列表,通常返回可迭代的持仓对象集合。
- get_position(code):获取单个标的持仓对象;如果该代码没有持仓,返回空或None,要自行判断。
持仓对象常见字段包括总持仓数量、可用可卖数量、持仓成本价、当前市价、持仓市值、浮动盈亏等。不同环境字段名可能略有区别,建议拿到对象后用print()打印真实字段,不要凭记忆硬取。
def check_position(context): p = get_position('600000.SS') if p is None: return print(p.amount, p.closeable_amount)这里最重要的一条经验:下单之后,持仓不会立即变化。委托先处于未成交状态,成交回报是异步的,可能延迟几百毫秒甚至更久。如果策略下单后马上get_position(),拿到的往往是下单前的旧数据。要等委托状态变为已成交,或至少等到下一轮run_daily循环再调仓。
4.2 账户资产查询
做仓位规划离不开总资产和可用资金。Ptrade里常见的账户函数是get_account(),返回对象的常见字段有:
- total_value:总资产,包括股票市值、可用资金和冻结资金。
- available:当前可用资金。
- position_value:持仓市值。
仓位控制策略根据总资产决定下单金额的写法:
def position_control(context): acct = get_account() plan_value = acct.available * 0.8 order_value('510500.SS', plan_value)有个细节要注意:直接用available乘0.8,看起来是只动用80%可用资金,但买入委托成交后还有手续费、印花税等费用,冻结资金往往高于订单金额,预留margin不要卡太满。我一般预留20%以上,开盘波动大的时候能避免因费用估算不足导致废单。
4.3 动态调仓:先卖后买还是目标调仓
换仓策略经常要卖出一批股票,同时买入另一批。新手最容易犯的错是:第一轮循环里先写卖出逻辑,紧接着写买入逻辑,结果卖出还没成交,可用资金不够,买入委托直接废单。
最简单的解法是分批。第一轮先卖,读取卖出委托状态,等到持仓里对应标的可卖数量下降、资金到账,第二轮再买。如果不想拆两轮,可以直接用order_target_value()把单只标的跳到目标市值,让系统内部处理买卖差额。不过要注意,即使用了目标调仓函数,下单金额接近账户总资产时也要控制总委托规模,系统内部还是会按委托和成交顺序逐步释放资金,并发委托太多同样可能超买。
4.4 持仓成本与除权除息
持仓成本价字段在遇到分红送股、除权除息时会被系统更新,更新后可能是摊薄成本价或调整后的成本价。如果策略用平均成本价做止损判断,除权日价格突然往下跳,容易被误判成亏损。
经验做法是:止损止盈判断尽量用现价相对持仓成本价的变动比例,而不是单纯看浮盈金额。除权除息日前后,回测结果也要打折看,系统模拟的持仓成本和税费处理可能有偏差。资金量大的账户,可以在分红公告期内临时暂停自动止损,避免被非市场行为干扰。
5. 常见问题与排查技巧实录
5.1 先给一张问题速查表
我整理了一份低频问题速查表,平时排查时比翻函数文档快:
| 现象 | 最可能原因 | 排查方向 |
|---|---|---|
| 委托一直不成交 | 限价偏离当前盘口 | 打印最新价和委托价对比,必要时撤单重下 |
| 打新列表为空 | 回测环境没打新数据 | 换仿真/实盘环境验证,检查申购时段 |
| 持仓查询返回None | 代码后缀不对或确实无持仓 | 打印证券代码格式,检查是否带后缀 |
| 全部订单被拒 | 权限未开通或委托频率超限 | 确认交易权限、程序化接入额度 |
| 下单后立刻查持仓是旧数据 | 成交回报异步 | 等待成交回报再执行下一步 |
5.2 委托一直没有成交
最常见原因是委托价格太“贪”。限价单填了你能接受的极端价,股价当天没走到,单子自然挂在上面。如果策略对成交速度要求高,可以参考盘口行情,用接近当前可成交价的价格下单。另一个极端是涨停/跌停状态:买股票碰到涨停封板,卖股票碰到跌停封板,谁也不容易成交,撤单重下也一样要排队。
5.3 打新列表为空
打新函数取不到列表,先确认当前环境是不是回测环境,很多回测引擎里根本没有打新数据。再看当前时间是否在申购时段内,新股申购有固定时段,开盘前查询基本拿不到新列表。最后看账户是否开通了对应市场的打新资格,如果券商接口本身不支持打新,只能界面里手动操作。
5.4 别把订单返回值当成布尔值
很多人写if order('600000.SS', 100)去判断是否成功,但order()返回的是订单编号字符串。空字符串代表没有生成委托,非空字符串代表委托生成成功。表面上看这种判断似乎能用,但遇到纯数字编号时,判断逻辑很容易出问题。正确做法是先取返回值,再分别判断委托是否生成、是否成交、是否部分成交。
5.5 查询对象为None,先查证券代码格式
持仓查询返回None,大概率是当前没有这个标的的持仓,但也可能是证券代码带错了后缀。Ptrade里代码后缀不对,系统根本查不到对应标的。从数据接口取代码时,统一处理成小写加后缀的格式,比如'600000.SS'。不要一会儿用'SH600000'这种带前缀代码,一会儿用'600000.SS',混用最容易出现查询结果为空。
5.6 全部委托被拒,优先查权限和频率
如果所有下单都被拒,先查账户权限。普通买入还好,涉及科创板、创业板、两融标的都要单独开通交易权限,权限没开,下单接口不会告诉你“没权限”,而是直接返回废单。程序化交易接入额度、日内委托笔数限制也是真实存在的,如果策略下单频率很高,可能需要降低频率或联系券商放开限制。
6. 把交易函数组合成一份可复用的模板
6.1 先用“最小验证模板”跑通全链路
把上面的函数串起来,就是一个最简的验证框架。我做新环境验证时,会先跑这份模板,把所有接口的真实输出打印一遍,确认字段和环境行为,再往里面填策略逻辑:
def initialize(context): run_daily(verify_trade, '10:00') run_daily(verify_position, '15:00') def verify_trade(context): acct = get_account() print('total:', acct.total_value) print('available:', acct.available) oid = order_value('510500.SS', 200) print('order_id:', oid) def verify_position(context): for pos in get_positions(): print(pos.security, pos.amount, pos.closeable_amount)建议第一次跑用仿真环境,因为仿真环境对下单、撤单、持仓更新的反馈路径接近实盘,比回测参数更有参考价值。所有函数名和字段名以自己当前环境为准,拿到环境后先打印字段,再写业务逻辑,能省下大量排查时间。
6.2 从仿真到实盘前,过一遍这五件事
- 权限:科创板、创业板、打新、两融是否都已开通。
- 代码格式:所有证券代码统一带后缀,不要混用前缀格式。
- 停牌与涨跌停:策略里是否处理了当日无法交易的情况。
- 打新接口:当前环境取列表和额度的函数名是否确认。
- 频率限制:日内委托笔数和程序化接入额度是否在合理范围内。
我实际跑新环境时的验证顺序是:先跑一次最小模板拿到账户和行情字段,再小单试一次买入和一次卖出,撤单测一次,最后在打新时段试一次申购列表读取。这一圈下来,接口行为基本就摸清了。不要一上来就把整个策略丢进去,那样报错再多你也分不清是哪一层的问题。
我的个人习惯是,先用一个很小的账户,花两个交易日把上面每一项验证完,再把策略真实仓位放大。先验证环境,再写策略,这个顺序不能反过来。因为很多诡异报错不是策略逻辑问题,而是环境字段和行为跟你以为的不一样。顺序对掉以后,排查成本会成倍增加。