基于聚宽量化交易平台实现量化交易策略
一、入门量化策略
JoinQuant聚宽API文档:https://www.joinquant.com/help/api/help?name=api
1、获取要操作的股票或指数成分股
# 导入函数库 import jqdata # 初始化函数,设定基准 def initialize(context): # 定义一个全局变量, 保存要操作的股票 # 方式一:操作一只股票 # g.security = \'601318.XSHG\' # 中国平安股票 # 方式二:操作多只股票 # g.security = [\'601101.XSHG\', \'601106.XSHG\'] # 方式三:操作指数成分股 g.security = get_index_stocks(\'000300.XSHG\') # 沪深300 print(g.security)
执行显示沪深300指数成分股:
2、开启动态复权模式(真实价格)
开启真实价格回测功能很简单,只需一步即可搞定:在initialize中使用set_option。
(1)开启动态复权测试
# 导入函数库 import jqdata # 初始化函数,设定基准 def initialize(context): # 定义一个全局变量, 保存要操作的股票 # 方式一:操作一只股票 # g.security = \'601318.XSHG\' # 中国平安股票 # 方式二:操作多只股票 # g.security = [\'601101.XSHG\', \'601106.XSHG\'] # 方式三:操作指数成分股 g.security = get_index_stocks(\'000300.XSHG\') # 沪深300 set_option(\'use_real_price\', True)
由于沪深300不存在分红和股票拆合,显示效果和上图一致。
(2)开启动态复权(真实价格)模式对模拟交易的影响
在模拟交易中,在未开启动态复权(真实价格)模式时,我们是使用基于模拟交易创建日期的后复权价格。
后复权模式示意图如下图所示:
不开启真实价格模拟盘的运算结果是没有错误,只是会理解起来更费劲一些。
如果想知道今天的真实价格,还需知道模拟创建的日期,并进行复权计算。为了让用户使用更便于理解、更真实的模拟系统,强烈建议开启动态复权(真实价格)模式。开启方式:在代码中调用set_option(\'use_real_price\', True)。
开启动态复权(真实价格)模式示意图如下图所示:
开启动态复权(真实)模式后,看到的价格都是最新的,每到新的一天, 如果持仓中有股票发生了拆合或者分红或者其他可能影响复权因子的情形, 会根据复权因子自动调整股票的数量.。但不要跨日期缓存这些 API 返回的结果。
3、设置佣金/印花税
交易税费包含券商手续费和印花税。可以通过 set_order_cost 来设置具体的交易税费的参数。
set_order_cost(cost, type, ref=None)
修改代码如下所示:
# 导入函数库 import jqdata # 初始化函数,设定基准 def initialize(context): # 定义一个全局变量, 保存要操作的股票 # 方式一:操作一只股票 # g.security = \'601318.XSHG\' # 中国平安股票 # 方式二:操作多只股票 # g.security = [\'601101.XSHG\', \'601106.XSHG\'] # 方式三:操作指数成分股 g.security = get_index_stocks(\'000300.XSHG\') # 沪深300 set_option(\'use_real_price\', True) # 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱 set_order_cost( OrderCost( # OrderCost 对象 open_tax=0, # 买入时印花税 close_tax=0.001, # 卖出时印花税 open_commission=0.0003, # 买入时佣金 close_commission=0.0003, # 卖出时佣金 close_today_commission=0, # 平今日仓佣金 min_commission=5 # 最低佣金 ), type=\'stock\' # 股票 )
(1)券商手续费
中国A股市场目前为双边收费,券商手续费系默认值为万分之三,即0.03%,最少5元。
(2)印花税
印花税对卖方单边征收,对买方不再征收,系统默认为千分之一,即0.1%。
(3)参数
-
cost: OrderCost 对象
-
open_tax,买入时印花税 (只股票类标的收取,基金与期货不收)
-
close_tax,卖出时印花税 (只股票类标的收取,基金与期货不收)
-
open_commission,买入时佣金,申购场外基金的手续费
-
close_commission, 卖出时佣金,赎回场外基金的手续费
-
close_today_commission, 平今仓佣金
-
min_commission, 最低佣金,不包含印花税
-
type: 股票、场内基金、场内交易的货币基金、分级A基金、分级B基金、分级母基金、金融期货、期货、债券基金、股票基金、QDII 基金、场外交易的货币基金、混合基金、场外基金,\’stock\’/ \’fund\’ / \’mmf\’ /\’fja\’/\’fjb\’/ \’fjm\’/ \’index_futures\’ / \’futures\’ / \’bond_fund\’ / \’stock_fund\’ / \’QDII_fund\’ / \’money_market_fund\’ / ‘mixture_fund\’ / \’open_fund\’
-
ref: 参考代码,支持股票代码/基金代码/期货合约代码,以及期货的品种,如 \’000001.XSHE\’/\’510180.XSHG\’/\’IF1709\’/\’IF\’/\’000300.OF\’
注意:针对特定的交易品种类别设置手续费时,必须将ref设为None;若针对特定的交易品种或者标的,需要将type设置为对应的交易品种类别,将ref设置为对应的交易品种或者标的。
(4)常用示例
# 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱 set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5), type=\'stock\') # 期货类每笔交易时的手续费是:买入时万分之0.23,卖出时万分之0.23,平今仓为万分之23 set_order_cost(OrderCost(open_tax=0, close_tax=0, open_commission=0.000023, close_commission=0.000023, close_today_commission=0.0023, min_commission=0), type=\'index_futures\') # 单独设置 000300.XSHG 的费用 set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5), type=\'stock\', ref=\'000300.XSHG\') # 设置所有期货(包括金融指数期货)的费用 set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5), type=\'futures\') # 对 IF/IH/IC 三个品种有效 set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5), type=\'index_futures\') # 单独设置AU期货品种的费用 set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5), type=\'futures\', ref=\'AU\') # 单独设置AU1709合约的费用 set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5), type=\'futures\', ref=\'AU1709\')
二、单位时间运行策略函数
handle_data:该函数每个单位时间会调用一次, 如果按天回测,则每天调用一次,如果按分钟,则每分钟调用一次。
该函数依据的时间是股票的交易时间,即 9:30 – 15:00。 期货应使用定时函数。
该函数在回测中的非交易日是不会触发的(如回测结束日期为2016年1月5日,则程序在2016年1月1日-3日时,handle_data不会运行,4日继续运行)。
对于使用当日开盘价撮合的日级模拟盘,在9:25集合竞价完成时就可以获取到开盘价,出于减少并发运行模拟盘数量的目的,我们会提前到9:27~9:30之间运行, 策略内获取到逻辑时间(context.current_dt)仍然是 9:30。
# 导入函数库 import jqdata # 初始化函数,设定基准 def initialize(context): # 定义一个全局变量, 保存要操作的股票 # 方式一:操作一只股票 # g.security = \'601318.XSHG\' # 中国平安股票 # 方式二:操作多只股票 # g.security = [\'601101.XSHG\', \'601106.XSHG\'] # 方式三:操作指数成分股 g.security = get_index_stocks(\'000300.XSHG\') # 沪深300 set_option(\'use_real_price\', True) # 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱 set_order_cost( OrderCost( # OrderCost 对象 open_tax=0, # 买入时印花税 close_tax=0.001, # 卖出时印花税 open_commission=0.0003, # 买入时佣金 close_commission=0.0003, # 卖出时佣金 close_today_commission=0, # 平今日仓佣金 min_commission=5 # 最低佣金 ), type=\'stock\' # 股票 ) def handle_data(context, data): print(\'Hello\')
执行效果如下所示:
1、获取当前时间数据
get_current_data():
获取当前单位时间(当天/当前分钟)的涨跌停价, 是否停牌,当天的开盘价等。
该方法返回值是一个dict, 其中 key 是股票代码, value 是拥有最新价、涨停价、跌停价等属性对象。
(1)返回字典key值对应value所拥有的属性对象
value值对应的属性对象:
- last_price : 最新价
- high_limit: 涨停价
- low_limit: 跌停价
- paused: 是否停止或者暂停了交易, 当停牌、未上市或者退市后返回 True
- is_st: 是否是 ST(包括ST, *ST),是则返回 True,否则返回 False
- day_open: 当天开盘价
- name: 股票现在的名称, 可以用这个来判断股票当天是否是 ST, *ST, 是否快要退市
- industry_code: 股票现在所属行业代码, 参见 行业概念数据
(2)示例
def handle_data(context, data): print( get_current_data()[\'601318.XSHG\'].name, get_current_data()[\'601318.XSHG\'].industry_code, get_current_data()[\'601318.XSHG\'].day_open, get_current_data()[\'601318.XSHG\'].high_limit, get_current_data()[\'601318.XSHG\'].low_limit, )
输出结果如下所示:
2、获取历史数据
attribute_history:获取历史数据,可查询单个标的多个数据字段,返回数据格式为 DataFrame 或 Dict(字典)。
attribute_history(security, count, unit=\'1d\', fields=[\'open\', \'close\', \'high\', \'low\', \'volume\', \'money\'], skip_paused=True, df=True, fq=\'pre\')
查看某一支股票的历史数据, 可以选这只股票的多个属性, 默认跳过停牌日期。当取天数据时, 不包括当天的, 即使是在收盘后;分钟数据包括当前分钟的数据;
def handle_data(context, data): # print( # get_current_data()[\'601318.XSHG\'].name, # get_current_data()[\'601318.XSHG\'].industry_code, # get_current_data()[\'601318.XSHG\'].day_open, # get_current_data()[\'601318.XSHG\'].high_limit, # get_current_data()[\'601318.XSHG\'].low_limit, # ) print(attribute_history( \'601318.XSHG\', # 股票代码 5 # 每天返回前5天的历史数据 ))
执行显示效果如下:
3、按股数下单
按股数下单。
order(security, amount, style=None, side=\'long\', pindex=0, close_today=False)
买卖标的。调用成功后, 您将可以调用[get_open_orders]取得所有未完成的交易, 也可以调用[cancel_order]取消交易。
(1)参数
- security: 标的代码 - amount: 交易数量, 正数表示买入, 负数表示卖出 - style: 参见[OrderStyle](#OrderStyle), None代表MarketOrder - side: \'long\'/\'short\',操作多单还是空单。默认为多单,**股票、基金暂不支持开空单**。 - pindex: 在使用set_subportfolios创建了多个仓位时,指定subportfolio 的序号, 从 0 开始, 比如 0 指定第一个 subportfolio, 1 指定第二个 subportfolio,**默认为0**。 - close_today: 平今字段,close_today: 平今字段,仅对上海国际能源中心,上海期货交易所,中金所生效,其他交易所将会报错(其他交易所没有区分平今与平昨,均按照先开先平的方法处理)。 - 对上海国际能源中心,上海期货交易所,中金所的标的: - close_today = True, 只平今仓 - close_today = False, 优先平昨仓,昨仓不足部分平进仓 **返回** Order对象或者None, 如果创建订单成功, 则返回Order对象, 失败则返回None
(2)买入下单示例
def handle_data(context, data): # 每天买入100股 order(\'601318.XSHG\', 100)
显示效果:
需要注意,开仓股票数量必须是100的整数倍,如果设置为110,会自动调整为100。
4、按价值下单
order_value(security, value, style=None, side=\'long\', pindex=0, close_today=False)
(1)参数
- security: 股票名字 - value: 股票价值,value = 最新价 * 手数 * 保证金率(股票为1) * 乘数(股票为100) - style: 参见[OrderStyle](#OrderStyle), None代表MarketOrder - side: \'long\'/\'short\',操作多单还是空单。默认为多单。默认为多单,**股票、基金暂不支持开空单**。 - pindex: 在使用set_subportfolios创建了多个仓位时,指定subportfolio 的序号, 从 0 开始, 比如 0为 指定第一个 subportfolio, 1 为指定第二个 subportfolio,**默认为0**。 - close_today: 平今字段,close_today: 平今字段,仅对上海国际能源中心,上海期货交易所,中金所生效,其他交易所将会报错(其他交易所没有区分平今与平昨,均按照先开先平的方法处理)。 - 对上海国际能源中心,上海期货交易所,中金所的标的: - close_today = True, 只平今仓 - close_today = False, 优先平昨仓,昨仓不足部分平进仓 **返回** Order对象或者None, 如果创建委托成功, 则返回Order对象, 失败则返回None
(2)示例
def handle_data(context, data): # 每天花10000买股票 order_value(\'601318.XSHG\', 10000)
显示效果:
5、目标股数(买到数量)下单
order_target(security, amount, style=None, side=\'long\', pindex=0, close_today=False)
买卖标的, 使最终标的的数量达到指定的amount,注意使用此接口下单时若指定的标的有未完成的订单,则先前未完成的订单将会被取消。
(1)参数
- security: 标的代码 - amount: 期望的最终数量 - style: 参见[OrderStyle](#OrderStyle), None代表MarketOrder - side: \'long\'/\'short\',操作多单还是空单。默认为多单。默认为多单,**股票、基金暂不支持开空单**。 - pindex: 在使用set_subportfolios创建了多个仓位时,指定subportfolio 的序号, 从 0 开始, 比如 0为 指定第一个 subportfolio, 1 为指定第二个 subportfolio,**默认为0**。 - close_today: 平今字段,close_today: 平今字段,仅对上海国际能源中心,上海期货交易所,中金所生效,其他交易所将会报错(其他交易所没有区分平今与平昨,均按照先开先平的方法处理)。 - 对上海国际能源中心,上海期货交易所,中金所的标的: - close_today = True, 只平今仓 - close_today = False, 优先平昨仓,昨仓不足部分平进仓 **返回** Order对象或者None, 如果创建委托成功, 则返回Order对象, 失败则返回None
(2)示例
# 卖出平安银行所有股票 order_target(\'000001.XSHE\', 0) # 买入平安银行所有股票到100股 order_target(\'000001.XSHE\', 100)
6、目标价值下单
order_target_value(security, value, style=None, side=\'long\', pindex=0, close_today=False)
调整标的仓位到value价值,金融期货暂不支持该API。
注意使用此接口下单时若指定的标的有未完成的订单,则先前未完成的订单将会被取消。
(1)参数
- security: 标的名字 - value: 期望的标的最终价值,value = 最新价 * 手数 * 保证金率(股票为1) * 乘数(股票为100) - style: 参见[OrderStyle](#OrderStyle), None代表MarketOrder - side: \'long\'/\'short\',操作多单还是空单。默认为多单。 - pindex: 在使用set_subportfolios创建了多个仓位时,指定subportfolio 的序号, 从 0 开始, 比如 0为 指定第一个 subportfolio, 1 为指定第二个 subportfolio,**默认为0**。
(2)示例
#卖出平安银行所有股票 order_target_value(\'000001.XSHE\', 0) #调整平安银行股票仓位到10000元价值 order_target_value(\'000001.XSHE\', 10000)
三、实现一个简单量化策略
1、策略内容
设置股票池为沪深300的所有成分股
如果当前股价小于10元/股且当前不持仓,则买入;
如果当前股价比买入时上涨了25%,则清仓止盈;
如果当前股价比买入时下跌了10%,则清仓止损。
2、Context——策略信息总览
Context对象:策略信息总览,包含账户、时间等信息。
(1)对象属性
- subportfolios: 当前单个操作仓位的资金、标的信息,是一个SubPortfolio 的数组 - portfolio: 账户信息,即subportfolios 的汇总信息, Portfolio对象,单个操作仓位时,portfolio 指向 subportfolios[0] - current_dt: 当前单位时间的开始时间, [datetime.datetime]对象 - previous_date: 前一个交易日, [datetime.date]对象, 注意, 这是一个日期, 是 date, 而不是 datetime - universe: 查询set_universe()设定的股票池, 比如: [\'000001.XSHE\', \'600000.XSHG\'] - run_params: 表示此次运行的参数, 有如下属性 start_date: 回测/模拟开始日期, [datetime.date]对象 end_date: 回测/模拟结束日期, [datetime.date]对象 type: 运行方式, 如下四个字符串之一 \'simple_backtest\': 回测, 通过点击\'编译运行\'运行 \'full_backtest\': 回测, 通过点击\'运行回测\'运行 \'sim_trade\': 模拟交易 \'live_trade\': 实盘交易 frequency: 运行频率, 如下三个字符串之一 \'day\' \'minute\' \'tick\' - 为了让从其他平台迁移过来的同学更顺手的使用系统, 我们对此对象也做了和 [g] 一样的处理: - 可以添加自己的变量, 每次进程关闭时持久保存, 进程重启时恢复. - 以 \'__\' 开头的变量不会被持久保存 - 如果添加的变量与系统的冲突, 将覆盖掉系统变量, 如果想恢复系统变量, 请删除自己的变量.
(2)示例
def handle_data(context, data): # 执行下面的语句之后, context.portfolio 的整数 1 context.portfolio = 1 log.info(context.portfolio) # 要恢复系统的变量, 只需要使用下面的语句即可 del context.portfolio # 此时, context.portfolio 将变成账户信息. log.info(context.portfolio.total_value)
3、策略实现——股价小于10元且当前不持仓则买入
# 导入函数库 import jqdata # 初始化函数,设定基准 def initialize(context): g.security = get_index_stocks(\'000300.XSHG\') # 沪深300 set_option(\'use_real_price\', True) # 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱 set_order_cost( OrderCost( # OrderCost 对象 open_tax=0, # 买入时印花税 close_tax=0.001, # 卖出时印花税 open_commission=0.0003, # 买入时佣金 close_commission=0.0003, # 卖出时佣金 close_today_commission=0, # 平今日仓佣金 min_commission=5 # 最低佣金 ), type=\'stock\' # 股票 ) def handle_data(context, data): # 要购买的股票 tobuy = [] # 遍历沪深300成分股 for stock in g.security: p = get_current_data()[stock].day_open # 开盘价 # 总仓位和可卖出仓位的差异是来自于T+1制度 amount = context.portfolio.positions[stock].total_amount # 这只股票总仓位(不包含挂单冻结仓位) amount2 = context.portfolio.positions[stock].closeable_amount # 这只股票可卖出仓位 if p <= 10.0 and amount == 0: # 符合条件加入购买列表 tobuy.append(stock) # 每个股票投入的资金 cash_per_stock = context.portfolio.available_cash / len(tobuy) for stock in tobuy: # 根据价值买入 order_value(stock cash_per_stock)
4、策略实现——卖出实现止盈止损
一般在实现量化交易时,都是先卖后买。
# 导入函数库 import jqdata # 初始化函数,设定基准 def initialize(context): # 定义一个全局变量, 保存要操作的股票 g.security = get_index_stocks(\'000300.XSHG\') # 沪深300 set_option(\'use_real_price\', True) # 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱 set_order_cost( OrderCost( # OrderCost 对象 open_tax=0, # 买入时印花税 close_tax=0.001, # 卖出时印花税 open_commission=0.0003, # 买入时佣金 close_commission=0.0003, # 卖出时佣金 close_today_commission=0, # 平今日仓佣金 min_commission=5 # 最低佣金 ), type=\'stock\' # 股票 ) def handle_data(context, data): # 要购买的股票 tobuy = [] # 遍历沪深300成分股 for stock in g.security: p = get_current_data()[stock].day_open # 开盘价 amount = context.portfolio.positions[stock].total_amount # 这只股票总仓位(不包含挂单冻结仓位) # 平均持仓成本 cost = context.portfolio.positions[stock].avg_cost if amount > 0 and p >= cost * 1.25: order_target(stock, 0) # 止盈,全部卖出 if amount > 0 and p <= cost * 0.9: order_target(stock, 0) # 止损,全部卖出 if p <= 10.0 and amount == 0: # 符合条件加入购买列表 tobuy.append(stock) # 每个股票投入的资金 cash_per_stock = context.portfolio.available_cash / len(tobuy) for stock in tobuy: # 根据价值买入 order_value(stock, cash_per_stock)
执行效果如下所示:
红线是基准收益,
蓝线是策略收益,在这里可以很清楚看到策略收益小于基准收益,说明该交易策略不可行。