☰
量化策略的“手脚”:Ptrade交易函数下单、打新与持仓管理实战拆解
2026/10/13 4:52:58 网站建设 项目流程

从一次“裸奔式回测”说起:为什么交易函数才是策略的骨架

先讲一个我踩过的坑。早年写策略的时候,我自认为把选股逻辑研究得很透,MACD金叉、量价背离、情绪周期全都塞进信号模块,回测曲线漂亮得像艺术品。结果一出模拟盘就原形毕露——信号明明触发了,持仓却一动不动;想加仓的时候资金被冻结;想止损的时候废单提示“可用持仓不足”。最后排查了半天,问题全出在交易执行层:我把下单函数挂错了地方,成交回报没等确认就发了下一笔指令,止损单用的还是市价单,跌停板上一挂就是半个交易日。

这件事让我明白一个道理:对于量化策略来说,信号是大脑,交易函数是手脚。再聪明的脑子,手脚不听使唤,照样干不成事。Ptrade这类券商量化终端最核心的价值,就是把“下单、打新、持仓管理”这些高频操作封装成一套可调用的交易API,让策略代码能直接操作系统里的真实账户。你要做的不是从零写一个交易网关,而是把这些函数用到极致。

这篇文章我打算把Ptrade里最常用的交易类函数按“能干什么、参数怎么填、返回值是什么、实战怎么用”四条线拆开讲透,覆盖下单、目标持仓调整、撤单、打新、持仓查询与调仓五大块。内容比较长,所以拆成上下两篇,这篇先讲下单、打新和持仓管理。文中的所有函数名都基于Ptrade量化交易终端1.0及兼容版本,不同券商部署版本可能略有差异,但核心逻辑是通用的。

1. 整体设计思路:交易函数的分层逻辑与选型原则

1.1 先搞懂Ptrade策略的两个阶段:初始化与运行

任何Ptrade策略,本质上就是两个函数的事:initialize(context)负责策略启动前的准备,handle_data(context, data)负责每个周期(或每个数据更新点)的交易决策。我们平时说的“交易函数”,几乎全部在这两个阶段里被调用。

一个容易被零基础用户忽略的细节是:初始化阶段里下单是不生效的。我见过不少新手把买入指令写在initialize里,然后无比困惑地问为什么回测持仓一直是零。原因很简单——初始化阶段模型还没准备好交易环境,资金账户、证券账户都还没完成校验,此时发出的任何委托都会被静默丢弃。正确的做法是:初始化里只做参数设置、全局变量赋值、股票池筛选,真正的下单操作全部放在handle_data或自定义的定时触发函数里。

另外一个设计原则是“交易函数只负责执行,不负责判断”。很多新手喜欢在handle_data里把买卖信号和下单代码揉在一起写,结果策略逻辑一团乱麻。我的习惯是:信号模块单独拆出去,handle_data里只做三件事——读取信号、计算目标持仓、调用交易函数执行。这样的好处是后期调参和排查问题都方便,你不需要在一大坨代码里翻某个下单参数到底传了什么值。

1.2 交易函数家族图谱:下单、撤单、调仓、打新、查询

Ptrade的交易类函数看起来很多,其实按功能分类非常清晰。我用一张表把它们先串起来,后面逐个展开讲:

函数类别代表函数核心用途典型场景
按数量下单order(security, amount)以指定股数买入/卖出固定股数买卖、分批建仓
按金额下单order_value(security, value)以指定金额买入/卖出按资金比例分配仓位
目标持仓下单order_target(security, amount)调整至目标股数调仓、止盈止损
目标市值下单order_target_value(security, value)调整至目标市值按市值权重配置组合
撤单cancel_order(order_id)撤销未成交委托撤废单、撤失效单
持仓查询get_position(security)查询单只持仓明细判断当前仓位、计算可用数量
组合持仓get_positions()查询全部持仓列表批量调仓、组合风控
新股申购get_ipo_list()/buy_new_stock()查询并申购新股打新策略

这篇文章的主线是下单、打新、持仓管理,所以目标持仓下单、撤单、打新、持仓查询是重点,按金额下单和按数量下单两者之间的细微差别我会在下单模块里讲透。

2. 下单函数全面拆解:从order到order_value再到order_target

2.1 基础下单函数:order与order_value的适用范围

先看最基础的两个:order和order_value。它们的区别一句话就能说清——order按“股数”下单,order_value按“金额”下单。

order(security, amount)的典型用法是:

# 买入某股票100股 order('600000.XSHG', 100) # 卖出某股票200股 order('600000.XSHG', -200)

注意amount的正负号代表买卖方向,正数买入、负数卖出。这一点非常容易踩坑。有次我一个朋友写止损逻辑,想卖出持仓,结果因为负号没加对,系统直接给他反向开仓了一笔,原本止损变成了加仓,心态直接崩了。所以在写止盈止损的时候,我习惯先定义一个signed_amount变量,明确标注正负方向,代码可读性也更好。

order_value(security, value)的用法是按人民币金额下单:

# 买入某股票,金额为10万元 order_value('600000.XSHG', 100000) # 卖出某股票,金额为5万元(按持仓的市值计算,不够会报错) order_value('600000.XSHG', -50000)

这里有个新手常问的问题:按金额买入的时候,系统怎么确定股数?答案是系统会用金额除以当前价格,然后按100股(A股)的整数倍向下取整。所以你可能发现“我下了10万元的买单,结果成交了9万8千多”,这是正常的,零头被取整规则吃掉了。

2.2 目标持仓函数:order_target和order_target_value的“终局思维”

如果说order和order_value是“手动挡”,那么order_target和order_target_value就是“自动挡”。它们的核心逻辑不是“买多少”,而是“最终手里要有多少”。

# 调整某股票持仓到1000股(如果当前持有500股,就买入500股) order_target('600000.XSHG', 1000) # 调整某股票持仓市值到10万元 order_target_value('600000.XSHG', 100000)

这个“目标持仓”的设计思路非常符合量化调仓的场景。你想啊,策略算出来“今天收盘后某某股票应该占组合的20%”,如果用手动挡去算“现在持仓多少、目标多少、差额多少”,代码里免不了一堆减法逻辑,还容易因为价格波动算错。而order_target直接把“最终结果”告诉系统,系统自动算差额、自动补单,省心太多。

但“自动”也意味着“你要对终局负责”。一次我调仓的时候,本意是“把A股票仓位从500股调到1000股”,结果因为当日涨停导致无法买入,系统自动撤单后我的持仓就停在了500股,可策略以为已经调到1000股,后续的卖出逻辑全部基于错误仓位运行,差点酿成大错。所以用目标持仓函数之后,一定要结合持仓查询函数做二次校验,确认实际调仓结果。

2.3 下单参数细节:市价与限价、整手规则与废单判定

Ptrade的下单函数还支持额外的参数来控制委托类型和价格,完整签名大致是order(security, amount, style=None),其中style可以传MarketOrder()(市价单)或LimitOrder(price)(限价单)。如果不传,默认是市价单。

市价单和限价单的选择是个老生常谈但依然值得强调的话题:

  • 市价单的优势是成交快、几乎没有“挂不上单”的问题,劣势是极端行情下成交价格可能偏离预期,尤其在开盘瞬间或临近收盘时段,价格飞一样地跳动,市价单容易“买在最高点、卖在最低点”。
  • 限价单的优势是价格可控,劣势是可能一整根K线都成交不了。限价单的price参数必须用触发时刻的实时价格,如果你用昨收价去挂限价单,很可能因为价格偏离而被拒单。

关于整手交易的规则,A股买入必须是100股(1手)的整数倍,卖出则可以卖出不足100股的零股。Ptrade作为券商终端,在回测和模拟盘中基本遵循了真实柜台规则。也就是说,你order('600000.XSHG', 150)买150股,大概率会被废单或者自动调整到100股;但order('600000.XSHG', -150)卖150股,因为有零股存在,是可以成交的。写策略的时候尽量避开对“非整手买入”的依赖,除非你做的是港股、美股这类没有整手限制的市场。

还有一个极容易被忽略的参数是“是否允许卖空”或者说“是否检查可用持仓”。Ptrade默认情况下不会让你卖出超过当前可用持仓的数量,一旦超出就会废单。所以我建议在下单逻辑里加上持仓数量校验,后面我会在持仓管理那部分给出一个完整的模板。

2.4 下单前必查:资金与持仓状态的联动检查

很多新手下单后收到“资金不足”的提示,第一反应是“我明明有10万可用资金,买8万的股票怎么会不足?”问题大概率出在一个环节:可用资金是T+1日才能使用的卖出回款,再加上冻结资金、手续费、印花税等,真实可用的钱和你账户里显示的“总资产”不是一回事。

还有一种情况是买入金额算上手续费后会超过可用资金。比如你可用资金100000元,买入价值99900元的股票,理论上够,但实际成交还需要缴纳佣金和印花税(买入有佣金,卖出有佣金+印花税),如果券商系统按“买入成本+手续费”一起校验,你这单就可能被拒。

所以我的习惯是:下单金额最多占可用资金的97%~98%,留出滑点和手续费的富余量。特别是做网格策略或者频繁调仓时,这个“余量思维”能避开大量废单。

3. 撤单与订单管理:把交易流程的“后悔药”吃明白

3.1 撤单函数:cancel_order的用法与订单生命周期

撤单是交易里最容易被低估的操作。实盘里信号变化极快,前一秒想买、后一秒可能就想止损离场,或者你挂了个限价单半天没成交,股价已经一路下跌,你不再想以那个价格买入,就必须撤掉那张单子。

Ptrade的cancel_order(order_id)需要传入订单编号,这个编号是下单函数的返回值之一。所以正确的姿势是把下单函数的返回值存下来:

# 下单并保存订单编号 order_id = order('600000.XSHG', 100, style=LimitOrder(price=10.50)) # 在需要撤单的时候调用 cancel_order(order_id)

需要注意,撤单只能撤销尚未成交的委托。如果订单已经全部成交,再调cancel_order不会产生任何作用;如果订单部分成交,cancel_order会撤销剩余未成交的部分。下单之后、撤单之前的这个时间窗口,订单处于“已报、待成交”状态,Ptrade内部会自动轮询柜台系统获取订单状态,所以你不需要自己写状态机去跟踪,但下单返回的订单编号你一定要妥善保存。

3.2 批量撤单的技巧:别被“永无止境的未成交单”拖死

有一个场景我印象很深。某次策略在开盘一分钟内连发了五笔买单,结果股价迅速拉升,我的限价单全部成了“高射炮”——价格挂低了,一直没成交。如果不撤单,这些委托会一直挂在盘面上,如果行情回调到挂单价附近,就会瞬间成交,完全打乱我的建仓计划。

彼时的策略代码里没有批量撤单的逻辑,我只能手动去操作终端逐笔撤单,手忙脚乱。后来我学乖了,写了一个cancel_all_pending_orders()的辅助函数,把所有未成交订单编号存进一个全局列表,需要时统一撤掉:

# 全局变量记录所有未成交订单 pending_orders = [] def place_order_with_tracking(security, amount, style=None): order_id = order(security, amount, style) if order_id is not None: pending_orders.append(order_id) return order_id def cancel_all_pending_orders(): for oid in pending_orders: cancel_order(oid) pending_orders.clear()

这个辅助函数虽然简单,但给我的实盘策略加了很强的“容错保险”。特别是在策略运行到盘尾要清仓的时候,如果没有这套批量撤单逻辑,挂着的一堆未成交单会让账户状态和策略内部状态严重脱节。

3.3 撤单时机的选择:哪些时段撤单容易失效

撤单不是“想撤就能立刻撤成”的。Ptrade的撤单指令发到柜台后,需要等待柜台处理并返回确认信息,这中间有时间差。我实测下来,撤单最顺畅的窗口是连续竞价时段(上午9:30-11:30、下午13:00-15:00),开盘集合竞价阶段(9:15-9:25)和收盘集合竞价阶段(14:57-15:00)相对容易出现问题:有的券商在集合竞价阶段不接受撤单,有的会延迟到开盘后才生效。

所以设计策略时,优先在连续竞价时段发出可撤的委托,尽量避免在集合竞价阶段挂单。尤其是做日内短线、打板这种对时效要求高的策略,更要避开竞价窗口下的“盲操作”。

4. 打新策略实战:从新股列表查询到一键申购

4.1 新股申购的核心逻辑:为什么打新在量化里这么好用

打新(新股申购)是A股市场里一种典型低风险操作,策略逻辑也不复杂:查询当日可申购的新股列表,在申购额度允许范围内提交申购委托,然后等待中签结果。因为中签率低,很多人觉得打新“看运气”,但从量化角度看,打新是一种“低成本、高赔率”的附加收益来源——你不需要投入太多精力研究个股基本面,只需要踩准申购时间窗口,把申购额度用满就行。

有人会问:打新要不要预先买入底仓市值?这个问题分市场看。主板和科创板打新通常需要持有一定市值的股票,创业板规则也有市值要求,所以“市值打新”才是主流玩法。你的量化策略如果本身就有底仓持仓,打新几乎就是“白捡”的收益增强项——当然,随着新股破发常态化,打新也不是零风险,需要对新股基本面做个最低限度的筛选,比如避开高市盈率发行且行业景气度差的新股。

4.2 打新相关的函数用法:get_ipo_list与buy_new_stock

Ptrade里打新涉及的主要函数有两个:get_ipo_list()用于获取当日可申购的新股列表,buy_new_stock(ipo_code, amount)用于提交申购委托。

基本用法示例:

def initialize(context): # 设置每日打新开关 g.ipo_switch = True def handle_data(context, data): # 只在每日开盘后执行一次打新判断 if g.ipo_switch: ipo_list = get_ipo_list() for ipo in ipo_list: # 每个新股申购数量上限以ipo对象返回的信息为准 buy_new_stock(ipo.code, ipo.max_buy_amount) g.ipo_switch = False

这里需要强调g.ipo_switch这个全局变量的作用。如果没有这个开关,handle_data会在每个数据更新点重复触发打新函数,导致重复申购。同一只新股一天只能申购一次,重复申购轻则废单、重则影响其他标的的资金安排。我在实际项目中还见过因为忘了设置开关,模拟盘里同一只票被申购了十几次的“奇观”。

另外,新股申购的额度规则与账户市值挂钩,申购数量上限由柜台按持仓市值计算。直接用ipo.max_buy_amount作为申购数量,可以保证“用满额度”。如果你希望保守一些,也可以手动打七折或者五折,但对大多数用户来说,用满额度才是打新收益最大化的正确姿势。

4.3 打新常见坑:重复申购、额度不足、申购时间窗口

打新策略看着简单,实际操作中至少有四个教训值得记录。

第一,申购时间是硬约束。A股打新的申购窗口通常是从申购日9:30到15:00,但不同板块、不同券商的处理方式略有差异。如果你策略的触发频率很低,可能在下午才跑handle_data,导致错过申购窗口。稳妥做法是每天开盘后尽快执行打新逻辑,越早越不容易出幺蛾子。

第二,额度不足是高频废单原因。新股申购有申购上限,这个上限由你的市值决定。如果你的策略是轻仓或者空仓状态,市值不够,申购必然失败。所以量化打新策略要跟你的底仓策略联动,底仓规模决定了打新额度的天花板。

第三,申购代码要格外小心。新股申购代码有时与股票代码不一致(沪市的主板新股申购代码和上市代码规则不同),如果函数需要你传的是“申购代码”,而你传成了“股票代码”,很可能被拒。调用get_ipo_list()返回的对象里通常直接带了可用的申购代码字段,直接用字段值,不要自己拼接。

第四,中签后的缴款资金要提前备好。申购本身不占用资金,中签之后才需要缴款。如果你中签了但账户余额不足,会直接影响信用记录甚至被限制打新。量化策略里建议预留一定比例的现金作为“打新备用金”,我个人的习惯是总资产中至少留3%~5%的现金,既能应对中签缴款,也能应对盘中突发的补仓需求。

5. 持仓管理与查询:你连自己手里有什么都不知道,怎么调仓?

5.1 持仓查询函数详解:get_position与get_positions

持仓管理是所有交易流程的“眼睛”。你下单之前要知道自己手里有什么、可用数量是多少、成本价是多少;调仓之后要确认仓位是否按预期变化。Ptrade提供了两个核心查询函数:get_position(security)查询单只股票持仓,get_positions()查询全部持仓列表。

# 查询单只持仓 pos = get_position('600000.XSHG') print(pos.amount) # 总持仓股数 print(pos.available_amount) # 可用持仓股数(T+1规则下可能小于总持仓) print(pos.price) # 当前市价 print(pos.avg_cost) # 平均成本价 # 查询全部持仓 all_positions = get_positions() for p in all_positions: print(p.security, p.amount, p.available_amount, p.avg_cost)

这里最关键的字段是available_amount。A股实行T+1交易制度,当天买入的股票当天不能卖出,所以“总持仓股数”和“可用持仓股数”是两码事。你要是用amount去计算可卖数量,就会在下单时频繁收到“可用持仓不足”的废单,然后一脸茫然。我早期就犯过这个错,当时还以为是券商系统有bug,后来仔细看了文档才反应过来:卖出的数量必须以available_amount为准。

另一个实用技巧是,get_position在没有任何持仓时返回的是“空持仓对象”还是None,不同版本处理不一致。稳妥写法是先判断持仓是否存在,再取字段:

pos = get_position('600000.XSHG') if pos is not None and pos.amount > 0: # 有持仓才执行后续逻辑 pass

用这个写法可以顺带过滤掉“想卖空却手里没货”的尴尬情况。

5.2 持仓管理的实战框架:从“裸持仓”到风控型仓位管理

把查询函数用起来的正确姿势,是写一套“持仓感知”的调仓流程。我早期写策略是典型的“裸持仓”——每次调仓都假设当前持仓与策略预期一致,后来发现实盘里滑点、废单、部分成交会导致持仓与预期严重偏离,不用查询函数去纠正,策略就会越跑越歪。

一个比较稳健的流程是这样的:

  1. 调仓前,调用get_positions()获取全部持仓快照,构建一个“证券代码 -> 持仓对象”的字典。
  2. 根据策略信号,计算每只股票的目标持仓股数(或目标市值)。
  3. 对于需要调整的股票,用目标值与当前值做差,得到“应买/应卖数量”。
  4. 调用order_target或手动order执行调仓。
  5. 调仓完成后,再次调用get_positions()校验实际持仓,并记录到日志里。

这个流程看起来多了一步“二次校验”,似乎有点冗余,但实盘中的价值非常大。尤其当天存在涨停无法买入、跌停无法卖出的极端情况时,二次校验能让你立刻发现问题,而不是让策略带着错误的仓位预期继续运行。

5.3 调仓与下单的联动经验:避免重复建仓和“僵尸仓位”

持仓管理和下单函数联动时,最典型的错误是“重复建仓”。有些新手在策略里写了两个触发条件,都调用了order买入同一只股票,结果一个周期内被买入两次,仓位直接翻倍。用目标持仓函数order_target就天然规避了这个问题——因为它只关心“最终持仓数量”,你反复调用它,只要目标值不变,系统就不会重复下单。

另一个常见问题是“僵尸仓位”——策略已经平掉某只股票,但代码里没有清除持仓状态变量,之后某天又错误地触发了卖出逻辑。我的习惯是:所有基于持仓的判断,都实时从get_position读取,而不是依赖自己在全局变量里维护的持仓字典。全局变量记录状态可以作为参考,但权威数据必须以系统查询结果为准。这句话值得反复念三遍,因为很多模拟盘和实盘的灾难都是从“状态不同步”开始的。

6. 常见错误与排查速查表:下单失败、持仓对不上、打新异常

我把这些年用Ptrade常见的交易类问题整理成了一张速查表,方便你排查问题时直接对照:

错误现象大概率原因解决方案
买卖指令发出但一直没成交市价单遇到涨跌停/停牌;限价单价格远离当前价;集合竞价阶段委托未释放检查股票是否停牌、是否涨跌停;限价单价格改为现价附近;避开集合竞价时段下单
资金充足却提示“资金不足”T+1资金未回笼;手续费/滑点挤压可用余额;重复下单导致冻结资金叠加下单金额预留2%~3%余量;检查冻结资金明细;避免同一时段反复下单
卖出提示“可用持仓不足”使用了总持仓而非可用持仓;当日买入未到T+1可卖状态;持仓已被其他逻辑卖出卖出数量用available_amount字段;检查策略是否有重复卖出逻辑
订单已提交但需要撤销限价单长时间未成交;信号反转;开盘前挂的单位于盘面保存订单编号并调用cancel_order;批量撤单用统一辅助函数
打新提示“申购额度不足”市值不够;重复申购;申购代码错误提前配置底仓市值;用开关控制每日一次申购;直接使用get_ipo_list返回的代码
持仓查询返回空对象股票代码格式不对;未持该股;查询时点账户尚未同步代码格式使用XXXXXX.XSHG/XSHE标准格式;先判断pos is not None再取字段

排查的思路总结起来就一句话:下单类问题先查代码格式与参数,成交类问题先查行情与时段,持仓类问题先查可用与冻结。按这个顺序排查,大部分问题都能在十分钟内定位。

7. 收尾实践:给交易函数做个“小门卫”

讲了这么多函数和技巧,最后分享一个我目前在项目里一直沿用的经验:给所有交易函数加一层“门卫”封装。所谓门卫,就是在调下单函数之前,统一检查市场状态、账户资金、持仓情况、当日上下限等条件,不满足就直接拦截,不真正发出委托。

def guarded_order(security, amount, style=None, max_ratio=0.98): if amount > 0: # 买入前检查可用资金 available_cash = context.portfolio.available_cash cash_need = amount * get_current_price(security) / max_ratio if cash_need > available_cash: log.info('资金不足,跳过买入:%s', security) return None else: # 卖出前检查可用持仓 pos = get_position(security) if pos is None or pos.available_amount < abs(amount): log.info('可用持仓不足,跳过卖出:%s', security) return None return order(security, amount, style)

这层封装看似多写了几个if,实际效果非常明显——废单率直线下降,模拟盘与实盘的表现差距也缩小了。很多时候策略回测完美但实盘拉胯,差的不是信号质量,而是交易执行的鲁棒性。交易函数是所有策略的“最后一公里”,它稳健了,你的策略才能稳稳地把纸上收益变成账户余额。这也是我为什么一直强调“吃透交易函数比追求花哨指标更重要”的原因。希望这篇拆解能帮你把Ptrade的下单、打新、持仓管理串成一条清晰的链路,少踩几个我用真金白银换来的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询