简介
PTrade,由恒生电子开发的量化软件。
在右上角的"帮助-帮助文档"处,可以查看除量化之外的文档。
在量化模块的帮助处,可以查看和量化相关的文档。
在工具模块中,有一些预设的策略。
但是,需要注意的是,在工具模块中的策略,运行在本地;在量化中的策略,运行在券商的云端。
这也是PTrade和QMT的区别之一,PTrade和QMT都是面向股票场景的量化投资平台。
- PTrade中的策略是运行在券商的云端服务器。
- QMT中的策略是运行在本地终端。
策略
结构
在PTrade中,有且仅有一种结构,如图所示。
initialize
,只会在策略启动的时候,被执行一次。
例如,日启动策略,会在启动的时候运行initialize
;之后即使在日,也不会再执行initialize
。before_trading_start
,每个交易日都会被执行。
例如,日启动策略,日会执行before_trading_start
,日会再次执行before_trading_start
。- 盘中相关的方法:
handle_data
、tick_data
,按照其固定的周期运行。on_order_response
和on_trade_response
两个函数,不受其运行周期控制,是回调函数,被回调(触发),就会运行。run_interval
和run_daily
是定时器函数run_interval
可以指定间隔秒(最快3秒)运行,也可以作为盘中函数。run_daily
在进行开盘集合竞价的时候会用到。
initialize
和handle_data
是两个必须有的函数,其他可选。
如果我们不需要在这两个函数中执行任何操作,可以采取如下的方式1
2def handle_data(context, data):
pass
handle_data
通过data获取行情
handle_data
方法有两个入参,context
和data
。我们可以在handle_data
方法中,直接读取data
中的数据,这样获取数据会更快。
data
,字典(dict),key
是标的代码,value
是当时的SecurityUnitData
对象,存放当前周期的数据。data
中的数据都是没有复权的数据。
示例代码:
1 | def initialize(context): |
运行结果:
1 | 2023-08-02 15:29:09 开始运行回测, 策略名称: 测试 |
关于data的歧义
在官方文档中,关于data
有如下的论述。
“为了加速,data中的数据只包含股票池中所订阅标的的信息,可使用data[security]的方式来获取当前周期对应的标的信息;”
根据这个理解,只有在set_universe
中的股票才能获取。
在下例中,我没有进行set_universe,却可以获取恒生电子(在g.security中)和浦发银行(不在g.security中)。
实际上,应该是:
为了加速,data中的数据只包含股票池中所订阅标的的信息。
若想获取股票池外的股票信息可以使用data[security]的方式来获取。
我们可以验证一下,确实如此。直接打印data
,内容是BarDict(600570.SS)
,600570.SS
是我们通过set_universe
订阅过的。
handle_data被调用的时间
在有些资料中,说日线级别中的handle_data:在回测中,是每天15:00执行;但是在交易中,需要第二天执行。
实际上,关于该部分,在官方文档中有如下的论述:
在交易环境中,取决于券商实际配置时间,默认是14:50。
如果不清楚券商配置的时间,或者想自定义日线策略的交易时间。可以直接采用分钟级别的,每分钟调用一次handle_data
。
然后在handle_data
内部做逻辑判断,判断时间。有三种思路:
- 用一个全局变量记录
handle_data
的被调用的次数,当该值等于某个设定的值,说明到了预定的时间,然后执行后续逻辑。
这种方法需要注意,策略盘中启动的话,记录可能不准确。 - 计算K线的长度,如果K线的长度等于某个设定的值,说明到了预定的时间,然后执行后续逻辑。
关于K线长度的计算,可以参考本文最后的"一阳穿三线(案例)"。 - 或者直接通过代码获取当前时间,进行判断。
PTrade的策略是运行在券商的服务器,通过代码获取当前时间,获取的是券商服务器的本地时间。这种情况需要注意,券商服务器的本地时间需要和交易所的时间同步。
handle_data中的data的内容
有些资料会说,handle_data
中的data
,在回测环境是一分钟的K线,在交易环境是行情快照。
我发现实际不是。
如图,是在交易环境中,我们看到有最高价、最低价这些。
即,在交易环境中也是K线。
特别的,我们可以看到,时间是没有"+8"的。
tick_data(交易)
tick_data
,仅交易模块可用。
注意:
- 该函数中的data和handle_data函数中的data是不一样的,不要搞混了。
- 当调用
set_parameters()
并设置tick_data_no_l2="1"
时,参数data中将不包含逐笔委托、逐笔成交字段。当券商有l2行情、并且我们不需要时,配置该参数可提升data取速。
关于set_parameters()
的用法,可以参考官方文档的介绍,一般放在initialize
中执行。
关于如何获取买一价、买二价等,参考官方文档。
其中一个重要的函数是eval,关于eval,可以参考《基于Python的后端开发入门:1.基础语法》的"数据类型转换"部分。
有些资料说,在该函数中,只可以调用order_tick
。实际上该部分已经修改了。
回测
添加策略
点击如图所示处的+
,添加策略,在弹框处输入策略名称和业务类型。
与在QMT中一样,新添加的策略会有一些预设的代码。
功能介绍
- 在
①
处,可以选择回测的时间范围、初始资金、业绩基准、回测周期等。 - 点击
②
的回测
,进行回测。 - 在
③
处,可以查看日志。 - 在
④
可以查看回测结果,从上到下,包括:- 收益、基准收益、Alpha比率等
- 收益率曲线
- 每日盈亏
- 当日买卖
- 点击
⑤
处,可以查看回测详情。
点击回测详情后,分别可以查看汇总记录和某一天的记录,点击左侧选择具体看哪一天。
性能分析
在回测期间,可以勾线"性能分析"。
然后在"回测详情"中,勾选Tab页,可以看到每个函数包括每个语句的运行时间。
交易
新增交易
点击新增,即可新增交易,然后在弹窗的下拉框,选择回测中的策略。
确认后,交易会立即开始运行。
交易频率
- 日级别
如果回测中的策略是日级别的,那么基于其新增的交易,也会是日级别的。 - 分钟级别
如果回测中的策略是分钟级别的,那么基于其新增的交易,也会是分钟级别的。 - tick级别
对于tick级别策略,基于tick_data
或者run_interval
实现,不用在意页面显示的策略是日线或者分钟。
需要注意的是,对于日线级别的,在回测和交易中,handle_data
被调用的时间是不一样的,这可能会导致我们的策略在回测和交易上,存在一些逻辑差异。
具体可以参考上文关于handle_data
的讨论。
数据
股票池
全市场股票池
get_Ashares
,获取指定日期A股代码列表。
按指数获取股票池
get_index_stocks
get_index_stocks
,获取指数成分股。
关于尾缀的歧义
根据官方文档的说法,尾缀必须是.SS
。
该部分存在表述不清。
我们可以用创业板指(399006)试一下,发现不是这样的。
特别的,对于沪深300,我们会发现只支持上交所的沪深300指数。
总之,建议,对于该部分,进行足够的验证和测试。
按行业获取股票池
get_industry_stocks
,获取行业成份股。
行情数据
handle_data和tick_data
在上文我们讨论过handle_data
和tick_data
,这两个方法都自带入参data
,data
中就是行情数据。
除了上述两种方法,这里再讨论三种方法:
- get_history
- get_price
- get_snapshot
get_history
get_history
,获取历史行情。
需要注意的是:
- 入数
fq
,数据复权选项,支持包括:pre
(前复权)、post
(后复权)、dypre
(动态前复权)和None
(不复权)。
dypre
(动态前复权),是站在回测日的当天,以回测日之前的除权因子,进行前复权,不考虑回测日之后的除权因子。
相比pre
(前复权),dypre
(动态前复权)更贴合真实交易场景。 - 入参
is_dict
,返回是否是字典(dict)格式,True
(是),False
(不是);
返回为字典格式取数速度相对较快; - 针对停牌场景,没有跳过停牌的日期,无论对单只股票还是多只股票进行调用,时间轴均为二级市场交易日日历,停牌时使用停牌前的数据填充,成交量为0,日K线可使用成交量为0的逻辑进行停牌日过滤。
- 数据返回内容可以包括当前周期的数据,基于
include
字段。
获取单支股票,is_dict=True
,每日周期,回测,示例代码:
1 | def initialize(context): |
运行结果:
1 | 2023-08-02 10:48:16 开始运行回测, 策略名称: 测试 |
获取多只股票,is_dict=True
,每日周期,回测,示例代码:
1 | def initialize(context): |
运行结果:
1 | 2023-08-02 10:53:39 开始运行回测, 策略名称: 测试 |
根据PTrade官方文档的介绍,is_dict=True
,在取数的时候会更快,本文讨论的也都是is_dict=True
的情况。关于is_dict=False
,可以参考官方文档。
get_price
使用方法
get_price
,获取历史数据。
需要注意的是:
start_date
与count
必须且只能选择输入一个,不能同时输入或者同时都不输入。
count
只在某些频率下有效,具体参考官方文档。
有某些频率,不支持start_date
和end_date
组合的入参,只支持end_date
和count
组合的入参形式,具体参考官方文档。- 针对停牌场景,没有跳过停牌的日期,无论对单只股票还是多只股票进行调用,时间轴均为二级市场交易日日历,停牌时使用停牌前的数据填充,成交量为0,日K线可使用成交量为0的逻辑进行停牌日过滤。
- 数据返回内容不包括当天数据。
security的参数为字符串
security
的参数为字符串,示例代码:
1 | get_price(security='600570.SS',start_date='20170201',end_date='20170213',frequency='1d') |
运行结果:
1 | open close high low volume price money preclose high_limit low_limit unlimited |
security的参数为列表
security
的参数为列表,示例代码:
1 | get_price(security=['600570.SS'],start_date='20170201',end_date='20170213',frequency='1d') |
运行结果:
1 | Out[12]: |
在解析时,注意iters
为行情字段('open'
、'close'
等),示例代码:
1 | p = get_price(security=['600570.SS'],start_date='20170201',end_date='20170213',frequency='1d') |
运行结果:
1 | 600570.SS |
如果需要将items索引由行情字段转换成股票代码,可以通过swapaxes
方法转换。示例代码:
1 | p = p.swapaxes("minor_axis", "items") |
关于Panel的更多,可以参考《用Python分析数据的方法和技巧:3.pandas》的"Panel"部分。
不包含当天的
例如,我们在20230803这一天,获取这一天的分钟数据。示例代码:
1 | df = get_price(security='600570.SS',start_date='20230803',end_date='20230803',frequency='1m') |
运行结果:
1 | (0, 7) |
get_snapshot
get_snapshot
,取行情快照,仅在交易模块可用。
get_snapshot
通常和run_interval
配合;run_interval
的作用是按设定周期定时运行;这样可以定时获取行情快照。
关于get_snapshot
和run_interval
的具体使用方法,参考官方文档。
一个需要注意的点:
财报数据
get_fundamentals
通过get_fundamentals
,可以获取财报数据。
需要注意的是:
- 该接口可能存在因网络拥堵等原因导致应答失败的情况,如果返回数据结果为空请多次尝试,策略中请增加保护机制。
- 该接口有流量限制,具体参考官方文档。
- 入参中有一个字段
report_types
,财报类型。
如果为年份查询模式(start_year
/end_year
),不输入report_types
返回当年可查询到的全部类型财报。
如果为日期查询模式(date
),不输入report_types返回距离指定日期最近( 不含指定日期 )一份财报(str)。
例子
日期查询模式
日期查询模式(date
),不输入report_types返回距离指定日期最近( 不含指定日期 )一份财报(str)。示例代码:
1 | df = get_fundamentals('600000.SS','balance_statement',date='20211231') |
运行结果:
1 | (1, 115) |
年份查询模式
年份查询模式(start_year
/end_year
参数模式),返回数据类型为pandas.Panel类型,索引为股票代码。不输入report_types
返回当年可查询到的全部类型财报。示例代码:
1 | p = get_fundamentals('600000.SS', 'balance_statement', start_year='2021', end_year='2021') |
运行结果:
1 | account_receivable accounts_payable advance_insurance advance_payment advance_receipts bill_receivable biological_assets bonds_payable borrowing_capital borrowing_from_centralbank bought_sellback_assets capital_reserve_fund cash_equivalents client_deposit client_provi commission_payable company_type compensation_payable constru_in_process construction_materials deferred_tax_assets deferred_tax_liability deposit deposit_in_interbank deposit_of_interbank deposits_received derivative_assets derivative_liability development_expenditure dividend_payable dividend_receivable estimate_liability fixed_assets fixed_assets_liquidation fixed_deposit foreign_currency_report_conv_diff good_will hold_for_sale_assets hold_to_maturity_investments impawned_loan independence_account_assets independence_liability insurance_receivables insurer_deposit_investment insurer_impawn_loan intangible_assets interest_payable interest_receivable inventories investment_property lend_capital life_insurance_reserve loan_and_advance long_defer_income long_deferred_expense long_salaries_pay longterm_account_payable longterm_equity_invest longterm_loan longterm_receivable_account lt_health_insurance_lr minority_interests non_current_asset_in_one_year non_current_liability_in_one_year notes_payable oil_gas_assets ordinary_risk_reserve_fund other_assets other_composite_income other_current_assets other_current_liability other_equityinstruments other_liability other_non_current_assets other_non_current_liability other_payable other_receivable outstanding_claim_reserve paidin_capital policy_dividend_payable proxy_secu_proceeds publ_date r_metal receivable_claims_r receivable_life_r receivable_lt_health_r receivable_subrogation_fee receivable_unearned_r refundable_capital_deposit refundable_deposit reinsurance_payables reinsurance_receivables retained_profit salaries_payable se_without_mi seat_costs secu_abbr settlement_provi shortterm_loan sold_buyback_secu_proceeds specific_account_payable specific_reserves sub_issue_secu_proceeds surplus_reserve_fund taxs_payable total_current_assets total_current_liability total_non_current_assets total_non_current_liability total_shareholder_equity trading_assets trading_liability treasury_stock unearned_premium_reserve |
publ_date
特别的,我们可以看看publ_date
这个字段。示例代码:
1 | p = get_fundamentals('600000.SS', 'balance_statement', start_year='2021', end_year='2021') |
运行结果:
1 | end_date |
我们看到,publ_date
的数据,一般晚于财报的日期。
这个在回测过程中,需要特别注意。在回测中,获取的财报数据,一定要根据publ_date
进行一次筛选,否则会导致"数据泄漏",回测结果不准确。
账户信息
账户信息通过context
上下文对象获取,在官方文档,首字母是大写的,实际应该小写。
context
对象的内容很多,具体可以参考官方文档。
示例代码:
1 | def initialize(context): |
运行结果:
1 | 2023-08-02 14:59:34 开始运行回测, 策略名称: 测试 |
持仓信息
通过get_position()
可以获取一个position
类(持仓类)的对象。
关于get_position()
方法和position
类(持仓类)的对象,都可以参考官方文档。
示例代码:
1 | def initialize(context): |
运行结果:
1 | 2023-08-02 15:03:51 开始运行回测, 策略名称: 测试 |
下单
指定数量买卖
order
,按指定数量买卖。
注意:
- 如果是卖出的话,
amount
填负数。 - 回测场景下,
amount
有最小下单数量校验;交易场景接口不做amount
校验,直接报柜台。 - 交易场景如果
limit_price
字段不入参,系统会默认用行情快照数据最新价报单,如果行情快照获取失败会导致委托失败,系统会在日志中增加提醒。
指定持仓数量买卖
order_target
,指定持仓数量买卖。
该接口用于买卖股票,直到持有的股票最终数量达到指定的amount
需要注意的是,在交易中谨慎使用该接口,因为容易导致重复下单。具体原因如下:
- 柜台返回持仓数据体现当日变化(由柜台配置决定):交易场景中持仓信息同步有时滞,一般在6秒左右,假如在这6秒之内连续下单两笔或更多order_target委托,由于持仓数量不会瞬时更新,会造成重复下单。
- 柜台返回持仓数据体现当日变化(由柜台配置决定):第一笔委托未完全成交,如果不对第一笔做撤单再次order_target相同的委托目标数量,引擎不会计算包括在途的总委托数量,也会造成重复下单。
- 柜台返回持仓数据不体现当日变化(由柜台配置决定):这种情况下持仓数量只会一天同步一次,必然会造成重复下单。
所以,如果需要在交易场景使用该接口,首先要确定券商柜台的配置,是否实时更新持仓情况,其次需要增加订单和持仓同步的管理,来配合order_target
使用。
指定金额买卖
order_value
,指定金额买卖。
指定持仓市值买卖
order_target_value
,指定持仓市值买卖。
注意!该函数同样存在可能造成重复下单,原因也相同。建议在交易中谨慎使用该接口。
order_market
order_market
,按市价进行委托。
关于order_market
的具体用法,可以参考官方文档。
关于什么是市价委托,可以参考《金融产品与金融市场:A股的交易规则》的"申报方式"部分,需要注意市价委托的适用时间范围。
order_tick
order_tick
,tick行情触发买卖。
能且仅能在tick_data
中使用。
获取订单
get_open_orders
,获取未完成订单。1
get_open_orders(security=None)
get_order
,获取指定订单。1
get_order(order_id)
get_orders
,获取全部订单(策略内所有订单)。1
get_orders(security=None)
get_all_orders
,获取账户当日所有订单(包含非本交易的订单记录)。1
get_all_orders(security=None)
撤单
cancel_order
,撤单。
在PTrade中,订单状态有:'0'(未报)
、'1'(待报)
、'2'(已报)
、'3'(已报待撤)
、'4'(部成待撤)
、'5'(部撤)
、'6'(已撤)
、'7'(部成)
、'8'(已成)
、'9'(废单)
、'+'(已受理)
、'-'(已确认)
、'V'(已确认)
。
其中,可以进行撤单的订单状态有:'2'(已报)
、'7'(部成)
。
委托下单时间
在QMT和TqSdk中,有所谓的"K线走完(Bar结束)下单"和"立即下单"。
这个在PTrade中不存在,会直接报到柜台。
工作经验
全局对象
在使用全局对象的时候,一定要记住,前面要加g.
。
获取股票池
在上文,我们讨论过:
initialize
,只会在策略启动的时候,被执行一次。before_trading_start
,每天都会被执行。
所以我们获取股票池(获取全市场股票、获取没有停牌的股票、获取指数的成分股)等,尤其是我们的策略需要跨日运行的话,应该在before_trading_start
方法中获取股票池,而不是initialize
方法中获取股票池。
财报数据的获取(回测)
该部分在上文有过讨论。
在回测中,我们获取的财报数据,一定要根据publ_date
进行一次筛选,否则可能会导致获取的回测数据不准确。
通过上下文对象context
的blotter.current_dt
,可以获取当前回测的时间。
不一定五档都有
不一定五档的价格都有,可能会只有买一和卖一,其他档位都没有。这时候我们获取其他档位的价格会报错。
可以先做一个判断。
因为异常数据缺失
在tick_data()
中,不排除因为异常,导致获取不到最新价(last_px
)。
可以参考如下的方式,进行判断。
1 | last_px = snapshot['last_px'] |
控制调用顺序
一般情况下,都是先运行initialize
和before_trading_start
,然后再运行handle_data
等。
但是,如果我们是盘中新开的交易、或者交易在盘中重启等情况。可能会存在先运行了handle_data
、再运行before_trading_start
。
所以,需要我们人为的强制调用顺序。
可以利用全局的变量,记录运行状态。后者运行的前提条件必须是前者已经运行。示例代码:
1 | def initialize(context): |
做持久化
什么是持久化
持久化,这个是数据库中的术语,是指将数据从内存持久化到磁盘。
在这里的持久化,指的是将交易期间的一些状态,保存至本地。
为什么需要持久化
因为策略可能会中断,因为券商重启PTrade、或者因为我们自己重启策略等各种原因。
如何实现持久化
- 可以利用
pickle
。
关于pickle
,可以参考《基于Python的后端开发入门:3.拷贝、类型注解、闭包、装饰器和一些常用的包》的pickle
部分。 - 可以利用
SQLite
。
争议(回测期间持仓修改时间)
在有些资料中会说,回测期间,不要用持仓做成交判断,因为持仓数据在下一个回测周期才会被修改。我发现这个BUG,应该已经被修复了。
回测周期选分钟。示例代码:
1 | def initialize(context): |
运行结果:
1 | 2023-08-02 16:33:58 开始运行回测, 策略名称: 测试 |
我们看到,在09:31:00
,执行order(security='600000.SS', amount=100)
之后,其持仓数据就已经修改了。
集合竞价打板(案例)
思路
集合竞价的时间是9:15至9:25,而handle_data
需要在9:30分才会被执行。
思路是利用run_daily
这个方法。
run_daily
run_daily
,按日周期处理。
关于该函数的更多用法,可以参考官方文档,这里论述一个注意点。
入参time
,指定周期运行具体触发运行时间点:
- 交易场景可设置范围:。
- 回测场景可设置范围:
- 当回测周期为分钟时,time的取值指定在与之间。
- 当回测周期为日时,无论设定值是多少都只会在执行。
实现代码
1 | def initialize(context): |
一阳穿三线(案例)
策略思路
一阳穿三线,是一种形态。指的是一根阳线,从开盘到收盘,突破了3根不同周期的均线。
实现代码
1 | import pandas as pd |
解释说明:
get_K_count
:获取当前K线数。replace
:保留两位小数,filter_limitup_stock
:剔除涨停。