PTrade 量化交易入门:从第一行代码到实盘运行

✍️ 听雨量化 ·

PTrade量化交易入门:从第一行代码到实盘运行

股票的量化交易,自然是选择PTrade和QMT两个平台。QMT我个人用得比较多,但PTrade给客户写得比较多。

这篇教程不讲概念,直接从第一行代码带你跑通PTrade。包括常用的函数、官方文档里没写清楚的细节,以及回测转实盘的注意事项。PTrade的门槛确实不低,各种函数的坑也比较多,如果你看完还是拿不准,别硬扛,找我来代写就行,收费不贵。微信搜「听雨量化」。


一、PTrade是什么

PTrade是恒生电子的量化交易系统,部署在券商服务器上,代码在服务器端运行,通过API下单到交易所。

它的特点是:

  • 支持股票、可转债、ETF、期货、融资融券
  • Python 3.11环境,预装了275个第三方库(包括numpy、pandas、scikit-learn、tensorflow、TA-Lib等)
  • 交易模式下最低可做到3秒级别的tick策略
  • 回测和实盘共用一套代码框架

核心优势:代码在券商机房执行,延迟远低于本地电脑下单。

但要注意:代码运行在券商的封闭服务器上,不能访问外部网络,不能调用需要联网的API(比如tushare、akshare等),也不能读取你本地电脑的文件。 所有数据和逻辑都必须在服务器内部完成。


二、策略的基本骨架

策略骨架

一个PTrade策略只需要两个函数:

def initialize(context):
    set_universe('600570.SS')

def handle_data(context, data):
    pass

initialize —— 策略启动时运行一次,用于初始化变量。

handle_data —— 策略主逻辑,每个周期执行一次。

日线策略每天执行一次,分钟策略每分钟执行一次。

一个完整的可交易策略:

def initialize(context):
    g.security = '600570.SS'
    g.flag = False
    set_universe(g.security)

def handle_data(context, data):
    if not g.flag:
        order(g.security, 1000)
        g.flag = True

这段代码的含义:买入1000股恒生电子,只买一次。

就这么简单。


三、事件函数详解

PTrade以事件驱动为基础,完整的事件框架:

事件函数必选/可选运行时机说明
initialize必选策略启动时一次初始化全局变量
before_trading_start可选盘前(9:10)每天盘前准备数据
handle_data必选每个周期策略主逻辑
after_trading_end可选盘后(15:30)每天收盘后操作
tick_data可选每3秒仅交易模式,tick级策略
on_order_response可选委托回调委托状态变化时触发
on_trade_response可选成交回调成交时触发

注意:tick_data只能在交易模式下使用,回测不支持。

before_trading_start:盘前准备

def before_trading_start(context, data):
    # 获取过去10天的收盘价,供当天策略使用
    history = get_history(10, '1d', 'close', g.security, fq='pre', include=False)
    g.close_array = history['close'].values

回测中该函数在8:30执行,实盘中在9:10执行(可由券商配置)。

after_trading_end:盘后操作

def after_trading_end(context, data):
    # 记录当天持仓
    log.info('持仓市值: %s' % context.portfolio.portfolio_value)

四、定时任务

如果handle_data的周期不满足需求,可以用定时任务:

run_daily:每天定时执行

def initialize(context):
    # 每天9:23执行集合竞价逻辑
    run_daily(context, aggregate_auction_func, time='9:23')

def aggregate_auction_func(context):
    stock = g.security
    snapshot = get_snapshot(stock)
    price = snapshot[stock]['last_px']
    up_limit = snapshot[stock]['up_px']
    if float(price) >= float(up_limit):
        order(g.security, 100, limit_price=up_limit)

注意:回测中日线周期无论time设什么值,都只在15:00执行。分钟线周期time可在09:3111:30和13:0015:00之间。交易中不受此限制,可设00:00~23:59。

run_interval:按秒执行(仅交易模式)

def initialize(context):
    # 每3秒执行一次
    run_interval(context, tick_handle, seconds=3)

坑:交易中定时任务线程数限制为5个。run_daily和run_interval累计超过5次会导致部分任务不触发。

最小间隔:期货策略最小1秒,股票等其他策略最小3秒。低于最小间隔的值会被系统自动调整为最小间隔。


五、下单函数详解

下单函数

PTrade提供了5种下单方式:

order:按数量买卖

order('600570.SS', 100)          # 买入100股,按最新价
order('600570.SS', 100, limit_price=39)  # 买入100股,限价39元
order('600570.SS', -100)         # 卖出100股

正数买入,负数卖出。支持国债逆回购(amount为负,最小10张/1000元)。

注意:交易场景如果不传limit_price,系统会默认用行情快照的最新价报单。如果行情快照获取失败,委托会直接失败,日志中会有提醒。

order_value:按金额买卖

order_value('600570.SS', 10000)   # 买入价值1万元的股票
order_value('600570.SS', -10000)  # 卖出价值1万元的股票

order_target:调整到目标数量

order_target('600570.SS', 100)  # 买入或卖出到持有100股
order_target('600570.SS', 0)    # 清仓

⚠️ 实盘慎用。交易模式下持仓同步有时滞(约6秒),短时间内连续调用order_target可能重复下单。

具体原因:

  1. 柜台返回持仓体现当日变化:6秒内连续下单,持仓数量不会瞬时更新
  2. 第一笔委托未完全成交时再调order_target,引擎不会计算在途委托,也会重复下单
  3. 柜台返回持仓不体现当日变化:持仓信息一天只同步一次,必然重复下单

解决方案:用order按数量买卖,自己管理持仓状态。

order_target_value:调整到目标市值

order_target_value('600570.SS', 10000)  # 调整持仓市值到1万元
order_target_value('600570.SS', 0)      # 清仓

⚠️ 同样有实盘重复下单风险。

order_tick:tick级买卖(仅交易模式)

order_tick(g.security, 100, "1")     # 以买一档价格买入100股
order_tick(g.security, 100, "-2")    # 以卖二档价格买入100股
order_tick(g.security, 100, limit_price=56.5)  # 指定价格

盘口档位:level1行情支持15档,level2行情支持110档。不支持可转债。委托上证股票时limit_price是必传字段。


六、获取行情数据

get_history:获取最近N条K线

# 获取过去10天的收盘价
df = get_history(10, '1d', 'close', '600570.SS', fq=None, include=False)

# 获取五日均线
ma5 = df['close'][-5:].mean()

参数说明:

  • count:K线数量,大于0
  • frequency:支持1m/5m/15m/30m/60m/120m/1d/1w/mo/1q/1y等
  • field:支持open/high/low/close/volume/money/price等,可传列表['open','close']
  • fq:pre前复权,post后复权,dypre动态前复权,None不复权
  • include:是否包含当前周期,默认False
  • fill:'pre'用上一分钟数据填充缺失,'nan'用NaN填充(默认)
  • is_dict:返回dict格式取数速度更快,推荐大数据量时使用

⚠️ include=True 详解(重点)

include参数是PTrade中最容易踩坑的参数,理解它的行为对分钟线策略至关重要。

include=False(默认):不包含当前周期

# 在handle_data中(假设当前是第60根分钟K线)
df = get_history(10, '1m', 'close', g.security, include=False)
# 返回第50~59根K线的数据,不包含当前正在形成的第60根

include=True:包含当前周期

# 在handle_data中(假设当前是第60根分钟K线)
df = get_history(10, '1m', 'close', g.security, include=True)
# 返回第51~60根K线的数据,包含当前正在形成的第60根

⚠️ 最大的坑:分钟线策略中调用日线数据

这是PTrade中最容易引入未来数据的地方,一定要格外注意。

假设你写了一个5分钟策略,想看日线级别的均线:

def handle_data(context, data):
    # 错误!include=True会拿到当前正在形成的日线的"收盘价"
    df = get_history(10, '1d', 'close', g.security, include=True)
    # 盘中10:30执行时,df[-1]返回的不是昨天的收盘价
    # 而是今天此时此刻的最新价!
    ma5 = df['close'][-5:].mean()

问题在哪? 日线的include=True意味着”包含当前正在形成的日线”。盘中10:30,今天的日线还在形成中,所谓的”收盘价”实际上是10:30的最新价——这就是未来数据。你用了一个尚不确定的值来做决策。

def handle_data(context, data):
    # 正确!include=False确保只拿已完成的日线
    df = get_history(10, '1d', 'close', g.security, include=False)
    # df[-1]是昨天的收盘价,df[-2]是前天的,数据是确定的
    ma5 = df['close'][-5:].mean()

规则:分钟线策略中调用日线数据,永远用include=False。 否则你拿到的”收盘价”实际上是盘中的最新价,是未来数据。

日线策略中,include=False和include=True的区别相对不那么致命,但绝大多数日线示例代码都用include=False,因为日线handle_data在15:00触发时当前日K线还在形成中。

分钟线策略中,include=True在同级周期中有实际意义:

def handle_data(context, data):
    # 分钟线策略中,5分钟K线用include=True
    # 因为handle_data每分钟执行,5分钟K线在整5分钟时已经完成
    h = get_history(100, '5m', field=['close', 'volume'], security_list=g.security,
                    fq='dypre', include=True, is_dict=True)
    close_array_5m = h[g.security]['close']
    
    # 但日线一定用include=False!
    h_daily = get_history(10, '1d', 'close', security_list=g.security,
                         fq='dypre', include=False)

坑:get_history和get_price不支持多线程同时调用。在run_daily或run_interval中不要与handle_data同一时刻调用,否则会偶现数据为空。

坑:停牌时数据怎么处理? PTrade不跳过停牌日期,时间轴为交易日日历,停牌时用停牌前的数据填充,成交量为0。可以用成交量=0来过滤停牌日。

get_price:获取指定时间段数据

# 获取2025年全年日线
df = get_price('600570.SS', start_date='2025-01-01', end_date='2025-12-31', frequency='1d')

注意:start_date与count必须且只能选一个。返回内容不包括当天数据。get_price同样有include参数,但start_date/end_date组合不支持周线/月线/季线/年线。

get_snapshot:获取实时快照

snapshot = get_snapshot('600570.SS')
price = snapshot['600570.SS']['last_px']   # 最新价
up_limit = snapshot['600570.SS']['up_px']  # 涨停价
down_limit = snapshot['600570.SS']['down_px']  # 跌停价

get_snapshot只在交易模块可用,回测和研究模块不可用。

get_tick_direction:获取分时成交数据(仅交易模式)

# 获取当前分时成交数据
data = get_tick_direction([g.security])
# 返回字典格式,包含时间戳、价格、成交量、成交方向等

需要level2行情才能获取逐笔委托和逐笔成交数据,否则无数据返回。

get_current_kline_count:获取当前K线编号(仅交易模式)

# 获取当前是第几根分钟K线(从9:30开始计数)
k_num = get_current_kline_count()
if k_num >= 238:  # 接近收盘
    # 收盘前恢复持仓
    order_target(g.security, g.amount)

七、常用函数速查

证券信息类

函数说明场景
get_stock_name(stocks)获取证券名称回测/交易/研究
get_stock_info(stocks, fields)获取上市日期、退市日期等基础信息回测/交易/研究
get_stock_status(stocks, type)判断ST/停牌/退市状态,返回True/False回测/交易/研究
get_stock_blocks(stock_code)获取所属行业、概念、地域板块回测/交易/研究
get_stock_exrights(stock_code)获取除权除息信息(送股/配股/分红/复权因子)回测/交易/研究
# 判断是否ST
st_status = get_stock_status(g.security, 'ST')
if st_status[g.security] is not True:
    log.info('%s 不是ST股' % g.security)

# 判断是否停牌
halt_status = get_stock_status(g.security, 'HALT', '20180312')

# 获取所属板块
blocks = get_stock_blocks('600570.SS')
# 返回: {'HY': [['710200.XBHS', '计算机应用']], 'GN': [['003800.XBHS', '人工智能']], ...}

# 获取除权除息数据(含复权因子)
exrights = get_stock_exrights('600570.SS')
# 返回DataFrame,含allotted_ps/rationed_ps/bonus_ps/exer_forward_a等字段

注意:get_stock_blocks获取的是当下的数据,回测中会变成”未来函数”。回测场景注意避免使用。

指数与股票池类

函数说明场景
get_index_stocks(index_code, date)获取指数成分股回测/交易/研究
get_industry_stocks(industry_code)获取行业成分股(尾缀.XBHS)回测/交易/研究
get_Ashares(date)获取指定日期全部A股代码列表回测/交易/研究
# 获取沪深300成分股
stocks = get_index_stocks('000300.XBHS')

# 获取指定日期的成分股(回测中常用)
stocks = get_index_stocks('000300.XBHS', '20160620')

# 获取全部A股
all_stocks = get_Ashares()
log.info('A股数量: %s' % len(all_stocks))

# 获取农业板块成分股
agri_stocks = get_industry_stocks('A01000.XBHS')

注意:get_industry_stocks同样是获取当下数据,回测中为未来函数。

持仓与账户类

函数说明场景
get_position(security)获取单只标的持仓(Position对象)回测/交易
get_positions(security)获取多只标的持仓(dict)回测/交易
get_all_positions()获取全部持仓(仅交易)交易
get_orders()获取当日全部委托回测/交易
get_orders(order_id)获取指定委托回测/交易
get_open_orders(order_id)获取未完成的委托回测/交易
# 获取单只持仓
pos = get_position('600570.SS')
log.info('持仓数量: %s' % pos.amount)        # 总持仓
log.info('可用数量: %s' % pos.enable_amount)  # 可卖数量
log.info('持仓成本: %s' % pos.cost_basis)    # 成本价

# 获取全部持仓(交易模式)
all_pos = get_all_positions()

# 获取账户信息
cash = context.portfolio.cash                # 可用资金
total = context.portfolio.portfolio_value    # 总资产
returns = context.portfolio.returns          # 收益比例

注意:交易模式中,Portfolio对象的数据更新周期默认为6秒(具体配置需咨询券商)。

技术指标计算类

PTrade内置了4个常用技术指标函数,输入numpy数组,返回numpy数组:

函数说明参数
get_MACD(close, short=12, long=26, m=9)MACD指标close: ndarray
get_KDJ(high, low, close, n=9, m1=3, m2=3)KDJ随机指标high/low/close: ndarray
get_RSI(close, n=6)RSI相对强弱close: ndarray
get_CCI(high, low, close, n=14)CCI顺势指标high/low/close: ndarray
def handle_data(context, data):
    h = get_history(100, '1d', ['close','high','low'], security_list=g.security)
    close_data = h['close'].values
    high_data = h['high'].values
    low_data = h['low'].values
    
    # MACD
    dif, dea, macd = get_MACD(close_data, 12, 26, 9)
    
    # KDJ
    k, d, j = get_KDJ(high_data, low_data, close_data, 9, 3, 3)
    
    # RSI
    rsi = get_RSI(close_data, 6)
    
    # CCI
    cci = get_CCI(high_data, low_data, close_data, 14)
    
    log.info('MACD DIF: %.3f, KDJ K: %.3f, RSI: %.3f' % (dif[-1], k[-1], rsi[-1]))

注意:以上指标函数仅在回测、交易模块可用,研究模块不可用。

财务数据类

get_fundamentals是一个功能强大的函数,可获取财务三大报表+估值数据+五类能力指标。

# 获取资产负债表
data = get_fundamentals('600570.SS', 'balance_statement', 'total_assets')

# 获取指定日期的财务数据
data = get_fundamentals(stocks, 'balance_statement', 'total_assets', '20160628')

# 获取年度财报
data = get_fundamentals(stocks, 'balance_statement', 'total_assets', '20160628', report_types='4')

支持的财务表:

表名内容
valuation估值数据(PE/PB/PS等)
balance_statement资产负债表
income_statement利润表
cashflow_statement现金流量表
growth_ability成长能力指标
profit_ability盈利能力指标
eps每股指标
operating_ability营运能力指标
debt_paying_ability偿债能力指标

坑:成长能力等五张表是非pit类型数据(按日期请求返回最近发布的财务数据)。如果某股票在查询日期还没发布该期财报,会返回None。比如请求20240301的数据,但年报到0319才发布,03月1日请求就会返回空。

坑:get_fundamentals有流控限制——每秒不得调用超过100次,单次最大调用量500条数据。大量股票数据获取时需要time.sleep(1)分批获取。

设置函数类

函数说明场景
set_universe(list)设置股票池(get_history的默认security_list)回测/交易
set_benchmark(sid)设置基准指数(默认沪深300)回测/交易
set_commission(ratio, min_comm, type)设置佣金费率仅回测
set_fixed_slippage(value)设置固定滑点仅回测
set_slippage(ratio)设置滑点比例仅回测
set_volume_ratio(ratio)设置成交比例(默认0.25)仅回测
set_limit_mode(mode)设置成交数量限制模式仅回测
def initialize(context):
    # 佣金:万三,最低3元
    set_commission(commission_ratio=0.0003, min_commission=3.0)
    # 固定滑点0.2元
    set_fixed_slippage(fixedslippage=0.2)
    # 成交比例0.5(每周期最多成交市场总量的一半)
    set_volume_ratio(volume_ratio=0.5)

注意:回测手续费计算 = 佣金 + 经手费(万0.487)+ 印花税(千1,仅卖出)。


八、常用对象速查

全局对象 g

def initialize(context):
    g.security = '600570.SS'  # 全局变量,跨函数共享
    g.count = 1
    g.flag = False

PTrade会自动用pickle持久化g对象。规则:

  • 以__开头的变量不会被保存
  • IO对象(文件、类实例)不能被序列化
  • 服务器重启后恢复到上次持久化状态

Context对象

context.portfolio.cash              # 可用资金
context.portfolio.portfolio_value  # 总资产
context.portfolio.returns          # 收益比例
context.current_dt                  # 当前时间
context.previous_date               # 前一个交易日

BarData对象(data参数)

data[g.security].close       # 当前周期收盘价(盘中=最新价)
data[g.security].open        # 开盘价
data[g.security].high        # 最高价
data[g.security].low         # 最低价
data[g.security].volume      # 成交量
data[g.security].money       # 成交额
data[g.security].price       # 最新价
data[g.security].preclose     # 昨收盘价(仅日线)
data[g.security].high_limit  # 涨停价(仅日线)
data[g.security].low_limit   # 跌停价(仅日线)

Order对象(委托)

order.id           # 订单号
order.symbol       # 标的代码(尾缀为四位XSHG/XSHE)
order.amount       # 下单数量
order.filled       # 成交数量
order.limit        # 指定价格
order.status       # 委托状态

委托状态:

status值含义
0未报
1待报
2已报
3已报待撤
4部成待撤
5部成
6已成
7已撤

Position对象(持仓)

position.amount        # 总持仓数量
position.enable_amount # 可用数量
position.cost_basis    # 持仓成本
position.last_sale_price  # 最新价
position.today_amount   # 今日开仓数量

九、回测转实盘的8个常见坑

回测转实盘坑点

坑1:order_target的持仓同步问题

回测中持仓更新是瞬时的,实盘中依赖柜台返回数据,有约6秒时滞。

解决方案:用order按数量买卖,自己管理持仓状态。

坑2:价格精度不同

品种精度示例
股票2位10.25
可转债/ETF/LOF3位105.125
股指期货1位3800.5

限价单精度不对会导致委托失败。

坑3:tick_data只在交易模式可用

回测中写了tick_data函数不会执行,也不会报错——只是静默忽略。

坑4:get_history只能获取2005年后的数据

策略需要更早的数据,需要借助其他数据源。

坑5:全局变量g的持久化规则

  • 以__开头的变量不会被保存
  • IO对象不能被序列化
  • 服务器异常重启后恢复到上次状态

关键状态不要只存在内存中。

坑6:逆回购的最小单位

最小1000元(10张),amount≥10且为负数。

order('204001.SS', -10)  # 逆回购1000元

坑7:set_universe的实际用途

set_universe只用于设定get_history函数的默认security_list入参,除此之外并无其他用处。不调用也不影响策略运行。

坑8:滑点设置的下限

固定滑点如果不足品种的最小价差,将不会生效。比如沪深300期IF最小价差0.2,固定滑点设0.3(单边0.15)不足0.2,滑点设置无效。


十、支持的业务类型

业务类型单位回测交易
股票股✅✅
可转债张,T+0✅✅
ETF股✅✅
融资融券股✅✅
期货手,T+0✅✅
LOF基金股✅✅
ETF申赎套利份❌✅
国债逆回购份✅✅

十一、股票代码格式

市场尾缀简称尾缀全称示例
上海SSXSHG600570.SS
深圳SZXSHE000001.SZ
指数—XBHS000300.XBHS
中金所期货—CCFXIF2506.CCFX

注意:Order对象的symbol字段尾缀为四位(XSHG/XSHE),与代码尾缀(SS/SZ)不同,需要做兼容处理。


十二、完整实战示例:双均线策略

完整日线策略示例

def initialize(context):
    g.security = '600570.SS'
    set_universe(g.security)

def handle_data(context, data):
    security = g.security

    # 获取10天历史收盘价(日线策略用include=False,不含当天)
    df = get_history(10, '1d', 'close', security, fq=None, include=False)

    # 计算均线
    ma5 = round(df['close'][-5:].mean(), 3)
    ma10 = round(df['close'][-10:].mean(), 3)

    # 获取昨天收盘价
    current_price = data[security]['close']
    cash = context.portfolio.cash

    # 金叉买入
    if ma5 > ma10:
        order_value(g.security, cash)
        log.info('买入 %s' % g.security)

    # 死叉卖出
    elif ma5 < ma10 and get_position(security).amount > 0:
        order_target(g.security, 0)
        log.info('卖出 %s' % g.security)

这段策略的逻辑:五日均线突破十日均线时全仓买入,五日均线跌破十日均线时清仓。


十三、完整实战示例:分钟级日内做T策略

分钟级日内做T策略

def initialize(context):
    g.security = '600570.SS'
    g.amount = 100
    g.L = 70  # RSI阈值
    g.S = 80
    set_universe(g.security)
    g.B_T_flag = False
    g.S_T_flag = False
    g.ini_buy_flag = False

def handle_data(context, data):
    k_num = get_current_kline_count()
    if not g.ini_buy_flag:
        order(g.security, g.amount)
        g.ini_buy_flag = True
    
    if k_num <= 30:
        return
    
    # 每5分钟整点判断
    if k_num % 5 == 0:
        # 5分钟K线——include=True拿到最新完成的K线
        h = get_history(100, '5m', field=['close', 'volume'], security_list=g.security,
                        fq='dypre', include=True, is_dict=True)
        close_array_5m = h[g.security]['close']
        
        # 15分钟K线——include=False + 手动拼接当前价
        h = get_history(100, '15m', field=['close', 'volume'], security_list=g.security,
                        fq='dypre', include=False, is_dict=True)
        close_array_15m = h[g.security]['close']
        current_price = data[g.security].close
        close_array_15m = np.concatenate((close_array_15m, np.array([current_price])), axis=0)
        
        # 做T判断
        rsi_5m = get_rsi(close_array_5m, 11)[-1]
        rsi_15m = get_rsi(close_array_15m, 11)[-1]
        
        if rsi_15m > g.L and rsi_5m > g.S:
            if get_position(g.security).enable_amount == g.amount and not g.B_T_flag:
                order(g.security, g.amount)
                log.info('正T买入')
                g.B_T_flag = True
                g.B_T_cost = data[g.security].price
    
    # 收盘前恢复持仓
    if k_num >= 238:
        order_target(g.security, g.amount)

这个示例展示了include=True和include=False在分钟策略中的典型混用方式。


总结:PTrade的API文档很全,但坑也很多。include参数是数据获取中最容易出错的细节——分钟线策略中调用日线数据,永远用include=False,否则”收盘价”实际上是盘中最新价,这就是未来数据。回测和实盘的差异是最大的坑,写好异常处理和持仓管理,是策略能稳定运行的关键。

PTrade需要一定的技术门槛,AI撰写往往不能正确使用平台的函数。如果你看完还是拿不准,别硬扛,找我代写就行,收费不贵。

微信搜「听雨量化」联系我们。