量化交易学习

量化交易是指以先进的数学模型代替人为的主观判断,利用计算机技术,从庞大的历史数据中筛选出能带来超额收益的多种大概率事件以制定策略,极大地减少投资者情绪波动所带来的影响,避免在市场极度狂热或悲观的情况下做出非理性的投资决策的一种交易方式。其核心思想是通过系统性的方法捕捉市场中的价格和波动性变化,从而实现更稳定和可持续的投资回报。在量化交易中,交易决策通常基于大量的历史市场数据,利用计算机程序来自动执行交易。

量化交易的基本步骤包括数据收集、模型构建、策略测试和实际交易。首先,交易者需要收集各种市场数据;然后,利用统计学和机器学习技术来构建数学模型,分析市场的历史行为、寻找规律和模式。接下来,交易者会通过历史数据模拟不同的交易策略,以评估其在不同市场环境下的表现。一旦找到有效的策略,交易者就会将其应用于实际交易中。

量化交易的优势在于它能够消除情绪和主观因素对交易决策的影响,从而减少人为错误。它具有较强的纪律性,能克制人性中的贪婪、恐惧和侥幸心理等弱点。此外,量化交易可以在瞬间执行大量交易,实现高效的交易操作。不过,量化交易也面临着一些挑战,包括数据质量、模型过拟合、市场变化等,需要我们实时监控策略的有效性。

以下内容转载自https://www.joinquant.com/

初识量化交易

为什么需要量化交易?

  • 它能让你的交易效率提高百倍,量化交易之于传统交易方法,如同大型收割机之于锄头镰刀,机枪大炮之于刀剑棍棒。
  • 也就是是说,传统交易方法是这样的:
    而量化交易是这样的:

在金融最为发达的美国,量化交易已大行其道,占据了70%以上的股市成交量。可以说量化交易是未来的趋势。当然,只言片语不能解释清楚,接下来,我们具体地介绍下量化交易。

量化交易是做什么?

量化交易是指借助现代统计学和数学的方法,利用计算机技术来进行交易的证券投资方式。便于理解的说,量化交易主要是做这样的事:

  1. 从一个灵感开始
    • 灵感就是指那些你想验证的可能会盈利的方法,比如银行股可能是良好的投资品种、一旦跨过20日均线后股价会继续涨、流传许久的羊驼交易法等等。灵感获取的方式可以是阅读、听人说、自己悟等等。
    • 这里我们以一个简单的情况为例进行讲解。比如你的灵感是这样的:
      1
      2
      如果股价显著低于近几日的平均价,则买入
      如果股价显著高于近几日的平均价,则卖出
    • 现在,你想知道这样操作究竟会不会赚钱?
  2. 把灵感细化成明确的可执行的交易策略
    • 一般灵感都很模糊,需要将其细化成明确的可执行的策略,目的是为了能得到确定的结果,以及为后续程序化准备。比如,你通过阅读了解到索罗斯的反身性概念,想将它应用到股市,这个反身性就很模糊,就需要明确什么条件下买卖,买卖什么品种,买卖多少量等,从而形成一个明确的交易策略,让不同人根据你的描述在相同情形下都能做出相同的操作。
    • 继续以之前那个关于平均价的灵感为例:
      1
      2
      如果股价显著低于近几日的平均价,则买入
      如果股价显著高于近几日的平均价,则卖出
    • 显然它是不够明确的。比如多低叫显著低于?多高叫显著高于?近几日究竟是几日?买入卖出是买卖多少?我们把它细化:
      1
      2
      如果股价低于近20日平均价10%,则用全部可用资金买入
      如果股价高于近20日平均价10%,则卖出全部所持的该股票
    • 还有一点不明确的地方,买卖哪个股票呢?我们认为这个交易方法盈利与否应该跟交易哪个股票关系不大,但st股票除外(知道st股票是一类有风险特别大的股票就好,详情请百度),所以股票的选择范围是除st股外的国内A股的所有股票。所以我们进一步细化:
      1
      2
      3
      每个交易日监测是除st股外的国内A股的所有股票的股价
      如果股价低于近20日平均价10%,则用全部可用资金买入该股票
      如果股价高于近20日平均价10%,则卖出全部所持有的该股票
    • 现在我们基本已经把之前的灵感细化成明确的可执行的交易策略。当然,可能还有些地方不够明确,也可能有些细节还不确定要改动,这些可以随时想到随时再改,不必一次做到完美。
  3. 把策略转成程序
    • 这一步就是把明确后的策略通过编程转成程序,好让计算机能根据历史数据模拟执行该策略,以及能根据实际行情进行反应并模拟交易或真实交易。
    • 简言之,就是把刚刚的策略翻译成计算机可识别的代码语言,即把这个:
      1
      2
      3
      每个交易日监测是除st股外的国内A股的所有股票的股价
      如果股价低于近20日平均价10%,则用全部可用资金买入该股票
      如果股价高于近20日平均价10%,则卖出全部所持有的该股票
      写成类似这样的代码(下面的代码并不完全符合,只是展示下大概的样子):
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      def initialize(context):
      g.security = ['002043.XSHE','002582.XSHE']

      def handle_data(context, data):
      for i in g.security:
      last_price = data[i].close
      average_price = data[i].mavg(20, "close")
      cash = context.portfolio.cash
      if last_price > average_price:
      order_value(i, cash)
      elif last_price < average_price:
      order_target(i, 0)
    • 这样一来,就把刚才细化好策略转成了代码程序,计算机就能运行了。这个过程你可以理解成用计算机能听懂的语言(代码),把你的策略告诉给计算机了。
  4. 检验策略效果
    • 现在计算机理解了你的策略,你现在可以借助计算机的力量来验证你的策略了。基本的检验策略方法有回测和模拟交易两种方法。
    • 回测是让计算机能根据一段时间的历史数据模拟执行该策略,根据结果评价并改进策略。继续之前的那个均价的策略例子的话就是这样的:
      • 设定初始的虚拟资产比如500000元、一个时期比如20060101到20160101,把这一时期的各种数据如估计股价行情等发给计算机,计算机会利用这些数据模仿真实的市场,执行你刚才告诉它的策略程序。最后计算机会给你一份报告,根据这个报告你就会知道,在20060101的500000元,按照你的策略交易到20160101,会怎样?一般包括盈亏情况,下单情况,持仓变化,以及一些统计指标等,从而你能据此评估交易策略的好坏。
      • 如果结果不好,则需要分析原因并改进。如果结果不错,则可以考虑用模拟交易进一步验证。
    • 模拟交易是让计算机能根据实际行情模拟执行该策略一段时间,根据结果评价并改进策略。与回测不同,回测是用历史数据模拟,模拟交易使用实际的实时行情来模拟执行策略的。举例就是这样:
      • 设定初始的虚拟资产比如500000元,选择开始执行模拟交易的时间点,比如明天。那么从明天开始,股市开始交易,真实的行情数据就会实时地发送到计算机,计算机会利用真实的数据模仿真实的市场,执行你的策略程序。同时,你会得到一份实时更新的报告。这报告类似于回测得到的报告,不同的是会根据实际行情变化更新。同样你能据此评估交易策略的好坏。
  5. 进行实盘交易并不断维护修正
    • 实盘交易就是让计算机能自动根据实际行情,用真金白银自动执行策略,进行下单交易。注意,这时不再是用虚拟资产模拟交易,亏损和盈利都是真钱。实盘交易一般也会给出一份类似模拟交易的会不断更新的报告,从而不断要观察策略的实盘表现并及时调整与改进策略,使之持续平稳盈利。

量化交易的价值何在?

量化交易的价值有很多,只提下最突出的价值所在。

  • 可以利用大量历史数据检验策略,效率提升百倍。当我们想验证交易策略的时候,一个基本的想法是想知道它在历史上表现如何,这往往需要大量的历史数据与计算量,量化交易做一次回测可能几分钟就可以得到结果了,相比于传统人工做法效率的提升是成百倍的。
  • 更科学更客观的衡量交易策略的效果。比如一个关于某技术指标的策略,人工的进行了10个交易日的验证,效果都不错,但这就能说明这指标不错吗?不,10次太少了,你需要更多的验证,比如1000个交易日,人工验证不可行,量化交易则又快又准。而且量化交易还可以利用数学与统计学自动给出客观的结果,比如年化收益率、最大回撤率、夏普比率等。
  • 全市场实时捕捉交易机会。当你知道一个盈利条件,当股价一旦满足这条件,你就可以操作盈利。问题是,市场几千个股票,股价时时刻刻都在变动,你能盯住几个,你会错失多少个机会。但量化交易可以利用计算机全市场实时盯盘,可以不错过任何交易机会,加倍你的盈利能力。
  • 更多的盈利机会。量化交易可以利用计算机对海量数据分析得到常人难以发现的盈利机会,而且有些机会只有量化交易才能利用。比如你发现一种交易方法,其特点是盈亏的额度相等,但盈利的概率是55%,亏损概率45%。首先这种小差距的概率规律,非量化交易不能发现,其次,要利用这个规律盈利需要大量次数的交易才能稳定盈利,这也非量化交易不可。

做量化交易需要什么?

  • 要有各种数据。要有能方便使用的各种投资相关的数据。这要考虑到各种数据的收集、存储、清洗、更新,以及数据取用时的便捷、速度、稳定。
  • 还要有一套量化交易的系统,要有能编写策略、执行策略、评测策略的系统。这要考虑到系统对各种策略编写的支持、系统进行回测与模拟的高仿真、系统执行策略的高速、系统评测策略的科学可靠全方面。

聚宽是什么?

通常一个投资者做量化交易所需要做的准备,就如同让一个农民自己去造一个大型收割机,而且还是从挖矿开始做起,极度困难,所以量化交易最初在金融与科技最为发达的美国由少数顶级精英发起的。

聚宽是一家量化投研平台,为投资者提供做量化交易的工具与服务,帮助投资者更好地做量化交易。也就是说,在聚宽量化投研平台,“大型收割机”已经为你准备好了,不需要你自己造了,你只需要学会使用它。

量化交易策略基本框架

通过前文对量化交易有了一个基本认识之后,我们开始学习做量化交易。毕竟就像学游泳,有些东西讲是讲不懂,做过就会懂。

由于本教程是基于聚宽量化投研平台,所以为了后续的学习,最好去注册一个聚宽量化投研平台的账号。

从一个非常简单的交易策略开始

先看一个非常简单的交易策略:

1
每天买100股的平安银行。

为了让这个策略能让计算机执行,首先,要使策略符合“初始化+周期循环”框架,像这样:

1
2
初始化:选定要交易的股票为平安银行
每天循环:买100股的平安银行

什么是“初始化+周期循环”框架?

为了将投资灵感高效地转化成计算机可执行的量化策略,必须基于一种模式来写,框架就是指这种模式。而此框架包含两个部分即初始化与周期循环:

  • 初始化即指策略最开始运行前要做的事。比如,准备好要交易的股票。
  • 周期循环即指策略开始后,随着时间一周期一周期地流逝时,每个周期要做的事。如例中,周期为天,周期循环的则是每天买100股的平安银行。

能帮助你理解这一框架的是,其实人本身日常做交易就是符合“初始化+周期循环”框架的,初始化就是已存在人脑的交易思想与知识,周期循环就是每天或每分钟地查看行情、判断、下单等行为。

如何把策略变成计算机可执行的程序?

  • 通过编程将策略写成计算机可识别的代码,具体说,我们这里是用python这门编程语言。
  • 另外可以用聚宽的向导式策略生成器,这种方法是不需编程的,但灵活性上难免是远不如写代码的。

那么如何将策略写成代码?

这并非三言两语就能说清,尤其是对于没有编程基础的人。所以我们将通过后续的内容逐步地介绍。首先我们将学习“初始化+周期循环”框架代码的写法。

  • 写法一
    1
    2
    3
    4
    5
    def initialize(context):
    这里是用来写初始化代码的地方,例子中就是选定要交易的股票为平安银行

    def handle_data(context, data):
    这里是用来写周期循环代码的地方,例子中就是买100股的平安银行
  • 写法二
    1
    2
    3
    4
    5
    6
    def initialize(context):
    run_daily(period, time='every_bar')
    这里是用来写初始化代码的地方,例子中就是选定要交易的股票为平安银行

    def period(context):
    这里是用来写周期循环代码的地方,例子中就是买100股的平安银行

两种写法用哪个好?

  • 写法一是从前的老写法,将逐步弃用,写法二是聚宽系统改进后的新写法,推荐使用写法二。

代码应该往哪里写?

  • 来到聚宽网站后,通过导航栏-我的策略-我的策略进入策略列表,点击新建策略。
  • 进入策略编辑页,左侧就是策略代码编辑区域,初始会默认给你提供代码模板,全删除后写入我们的代码就好了。

框架写成代码了,那例子的完整的代码该怎么写呢?

剩下的两行代码这么写。完全理解需要学习后续的内容,此处不要求理解。知道大概什么样子往哪里写即可。

  • 选定要交易的股票为平安银行:
    1
    g.security = '000001.XSHE'
  • 买100股的平安银行(市价单写法):
    1
    order(g.security, 100)
  • 以写法二为例把剩下的代码补上后,完整代码为:
    1
    2
    3
    4
    5
    6
    def initialize(context):
    run_daily(period, time='every_bar')
    g.security = '000001.XSHE'

    def period(context):
    order(g.security, 100)

那么现在这些代码就可以运行了吗?

是的。以写法二为例,如图把代码写到策略编辑区,设置好初始资金起止时间(比如初始资金100000元,起止时间20160601-20161231),频率设置成天。点击编译运行,运行结束后就可以看到结果了。

可以看到,若你20160601有初始资金100000元,每个交易日尝试买100股的平安银行,到20161231,你的收益曲线将如图中蓝线般增长。图中红线是基准收益(默认是沪深300指数,代表整个市场增长水平)

接下来,点击运行回测,运行结束后就可以看到更为详细的结果,包括下单记录、持仓记录等。

策略出错不能运行?

策略不能运行时,日志中会报错并给出一定的提示信息,像这样:

首先注意,右上角的箭头按钮能展开运行日志。看到日志中,最后一行是错误的提示信息:

1
2
3
SyntaxError: invalid syntax

汉义是 语法错误:不合法的语法。

最后一行之前的是错误的位置信息,一般只看后面就行。

1
2
3
File "user_code.py", line 1
def initialize(context)
^

意思是文件user_code.py(就是你的策略代码)的第一行,“^”符号指向的位置有错。你到代码中的这个位置看下,会发现少个冒号。

为了顺利运行策略,需要耐心解决错误,但错误的原因极度的复杂多样(所以日志的报错信息也多种多样,不止图上一种),故在此只针对例子讲下新手容易犯的错误:

  • 符号要用英文输入法。下图,代码第一行的冒号是中文的,所以出错
  • 拼写不要错。下图,security拼写错了
  • 缩进要对齐。下图,缩进没对齐。缩进的时候可以按键盘tab键或四个空格。

编程界往往把错误叫bug,而不断调试去除错误的过程叫debug,做量化时也是时常听到的说法,大家应该知道下。

而且debug通常就是要耗费不低于写bug写代码的时间的,所以会debug是很重要的能力,大家平时debug的时候不妨多思考下,如何更有效率的debug。当然,我们后续也会介绍些debug的技巧。

回测、编译运行、运行回测都是什么意思?

像刚刚那样,用一段时间内的历史的真实行情数据,来验证一个确定的交易策略在这段时间表现如何,这个过程叫回测。

  • 运行回测就是是字面意思,让计算机运行这次回测,运行后会告诉你策略在这段时间表现情况,比如收益率、年化收益率、最大回撤、夏普比率等指标,而且一般也会包括下单记录、持仓记录等。
  • 编译运行其实也是让计算机运行这次回测,不过相比于点击运行回测,编译运行的结果比运行回测要简单,只有收益率等指标,因此也速度更快。所以,当还不必要得到详细的结果时,或只是想调试下策略的代码,看是否无误可运行时,编译运行就比运行回测更方便。

周期循环具体是什么时候开始的呢?

  • 如果策略频率为天,是每个交易日开始生效,从9:30直到15:00(从股市开市到收市),所以例子中是每个交易日9:30开市循环就开始,一天一次地循环执行买入股票的操作。
  • 如果策略频率为分钟,是每个分钟开始时执行,所以例子中的买入股票的操作是每个交易日从9:30:00开始,然后9:31:00,直到14:59:00。接着下一天9:30:00,如此一分钟一次地循环执行的。

虽然频率只有为分钟和每天可选,但通过不同的代码可以实现按周按月周期循环,而且分钟级别里下单时间也是可以自己选的,不过代码的写法则与写法一和写法二那样略有不同,后面会讲到。

python基本语法与变量

python是什么

python是与计算机交流的一种语言。我们把想让计算机做的事情用python写出来,就如同前文那样的一行行代码,从而,计算机才能理解并去按我们的想法去做。这是一种通俗易懂的理解,但已经足够了。想了解更多专业角度的介绍就自行搜索了解吧。

Python2与Python3

  • Python语言本身也是如同自然语言般在不断变化的,升级到python3.0版本时出现了较大的变化,以至于python分为了python2与python3两个不互相兼容的版本。
  • 由于世界上有很多流行功能函数库对python3的支持并非很好,而有些量化过程中策略或系统可能会用到,所以我们用python2来写策略,而且聚宽做策略回测代码也只支持python2。(聚宽投资研究功能中支持使用python3)
  • 不过对写策略来说,python2与python3的区别并不明显。具体区别见python官方文档

python的基础语法

  • 大小写敏感:比较容易理解,就是字母的大写与小写是区分的,所以如果你把前文例子的代码中的若干个字母从小写变成大写,系统会报错。
  • 要用英文符号:之前讲过,冒号、逗号、分号、括号、引号等各种符号必须用英文的,中文的会不识别而报错。
  • 注释:为了让代码的便于交流理解,通常都会在代码中写入注释用以说明代码,注释是给人看的,所以计算机会忽略(顺便提下,空行也会被忽略),用中文记录思路也没关系。强烈建议养成写注释的好习惯。注释的方法有二:
    • (#)会把所在行的其后的所有内容设定为注释,如下
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      # 注释样例
      # 这是一一个每天买平安银行的策略

      # 初始化
      def initialize(context):
      run_daily(period,time='every_bar')
      # 设定要买入的股票是平安银行
      g.security = '000001.XSHE'
      # 周期循环
      def period(context):
      #买入100股平安银行
      order(g.security, 100)
    • 三个单引号(''')或三个双引号(""")会把之间的内容设定为注释,以单引号为例如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      '''
      注释样例
      这是一一个每天买平安银行的策略
      是我写的:)
      '''
      '''初始化'''
      def initialize(context):
      run_daily(period,time='every_bar')
      g.security = '000001.XSHE'
      '''周期循环'''
      def period(context):
      '''买入100股平安银行'''
      order(g.security, 100)
  • 行与缩进:之前讲过,代码缩进的时候要对齐,缩进方法是四个空格或一个tab键(推荐用tab),不要混着用。比如例子中周期循环部分除第一句都是要缩进的。缩进的含义是这样的,有些语句是要包含其他连续若干条语句才成立的,这些语句通过缩进表示这种被包含的关系。如下:
    1
    2
    3
    4
    5
    # initialize这条语句包含了其下的两条语句
    def initialize(context):
    # 这两条语句是要被其上的initialize包含的,要缩进
    run_daily(period,time='every_bar')
    g.security = '000001.XSHE'
  • 一行写多条语句:一般习惯是一行只写一条语句,如果要一行写多条语句要用分号隔开,不常用但要认识,如下,我把例子中原本的第二行与第三行写在一行了(比较长排版可能会自动换行显示)。
    1
    2
    def initialize(context):
    run_daily(period,time='every_bar');g.security = '000001.XSHE'
  • 一条语句写在多行:有时一条语句可能就会很长,为了便于阅读会用斜杠(不是除号,是从左上到右下的)分隔后写在多行。如下,例子的第二行代码被斜杠分隔后写在两行。
    1
    2
    3
    4
    def initialize(context):
    run_daily(period,\
    time='every_bar')
    g.security = '000001.XSHE'

变量与赋值

我们在之前的例子中见过这样一行语句

1
g.security = '000001.XSHE'

当时没细讲,含义是把'000001.XSHE'这个字符串赋值给名为g.security的变量(security是英文证券的意思)。

  • 变量通俗的理解是,计算机中存放数据的有名字的盒子。另外变量名字是在赋值语句中定义的。
  • 赋值,即赋予变量数据,写法是等号,含义是把等号右边的数据(或变量里的数据)放入左边的变量中去。

Python 保留字符

有些名字被系统所占用不能用作变量名,或任何其他标识符名称,如下:

1
2
3
4
5
and        exec    not     assert    finally    continue
break for pass class from print
or global raise def if return
del import try elif in while
else is with except lambda yield

打印 print

print是非常常用而重要的语句,它能把变量里的内容在日志中打印输出出来,通过它我们能了解程序运行的细节。 用法如下:

1
2
3
4
5
# 用法: print(变量名)
a=1
print(a)
b='你好'
print(b)

如下图,把代码放到周期循环里后,点编译运行执行代码,每个交易日都打印了a、b,因为运行了两个交易日,所以打印了2组a、b。注意,后面的例子都可以这个方法来执行。

print也可以直接打印数据,如下

1
2
3
# 用法: print(数据)
print(1)
print('你好')

为了能在日志中看出打印内容的含义,可以采用如下方法,此方法经常用于记录策略的运行情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 用法:print("说明、解释等,用%s表示变量的位置" % (变量或数据))

a=1
b='hello'

print("a=%s" % (a))
print("b=%s" % (b))
print("%s是你好的意思" % (b))
#\n是在所在位置换行的意思,能让日志在日期信息的下一行开始打印
print("\na=%s" % (a))

# 用之前的方法执行后结果如下:
2016-06-01 09:30:00 - INFO - a=1
2016-06-01 09:30:00 - INFO - b=hello
2016-06-01 09:30:00 - INFO - hello是你好的意思
2016-06-01 09:30:00 - INFO -
a=1

全局变量

你可能会发现初始化里的变量与周期循环里的变量是不通的,比如你运行如下的代码会报错:

1
2
3
4
5
6
def initialize(context):
run_daily(period,time='every_bar')
a=1

def period(context):
print(a)

报错信息如下,含义是a没有被定义

1
NameError: global name 'a' is not defined

为了让变量能在全局被使用,需要在变量前加'g.',使之成为全局变量。所以,把刚刚的代码中的a改为全局变量就能正确运行了。

1
2
3
4
5
6
def initialize(context):
run_daily(period,time='every_bar')
g.a=1

def period(context):
print(g.a)

这里全局变量的用法是JointQuant平台特有的,在一般的Python中全局变量用法需要另外定义。

基本数据类型-数字与字符串

对计算机来说,不同的数据往往需要不同的操作与存储要求,因此在赋值时python会自动为数据分类,从而对不同的数据采取不同的应对方法。比如,数字可以数学运算,但文本就不可以,字母可以转换大小写,数字不行。

  • 数字(Number):数字就是数字,可以做诸如加减乘除的计算操作,具体可分为多种类型,比如股价一般就是浮点数型。因为在赋值变量的时候,python会自动调整变量类型。所以需要关注数字类型的时候并不多。 数字具体分为int(整数)、float(浮点数,即 包含小数位)、bool(布尔值,即True和False,True是1,False是0)等。
    1
    2
    3
    a = 3      # 整数
    b = 3.1415 # 浮点数
    c = True # 布尔值
  • 字符串(String):字符串可以理解为文本或文字,不能像数字进行数学运算,有其特别的操作,比如股票代码、股票名称一般都是字符串。 Python 可使用引号( ' )、双引号( " )、三引号( ''' 或 """ ) 来表示字符串,引号的开始与结束必须的相同类型的。如下,不妨用刚讲的print打印下看看。
    1
    2
    3
    4
    5
    6
    # 其中三引号可以由多行组成来编写多行文本
    a = '九歌'
    b = "袅袅兮秋风"
    c ="""袅袅兮秋风,
    洞庭波兮木叶下。
    ——屈原《九歌》"""

算术运算

数字变量之间是可以进行算术运算的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
a=3.0
b=2.0

# 为了查看结果我用了print打印
# 加
print("a+b=%s" % (a+b))
# 减
print("a-b=%s" % (a-b))
# 乘
print("a*b=%s" % (a*b))
# 除
print("a/b=%s" % (a/b))
# a除以b的商的整数部分
print("a//b=%s" % (a//b))
# a的b次幂,即指数运算
print("a**b=%s" % (a**b))
# a除以b的余数,即取余运算,为了打印“%”百分号要用两个百分号代表“%”百分号
print("a%%b=%s" % (a%b))

# 用之前的方法执行后结果如下,日期信息省去了
a+b=5.0
a-b=1.0
a*b=6.0
a/b=1.5
a//b=1.0
a**b=9.0
a%b=1.0

注意如果两个整数类型进行计算,结果默认还是整数。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 这样写没有.0,系统会默认当成整数
a=3
b=2

# 为了查看结果我用了print打印
# 加
print("a+b=%s" % (a+b))
# 减
print("a-b=%s" % (a-b))
# 乘
print("a*b=%s" % (a*b))
# 除
print("a/b=%s" % (a/b))
# a除以b的商的整数部分
print("a//b=%s" % (a//b))
# a的b次幂,即指数运算
print("a**b=%s" % (a**b))
# a除以b的余数,即取余运算,为了打印“%”百分号要用两个百分号代表“%”百分号
print("a%%b=%s" % (a%b))

# 用之前的方法执行后结果如下,日期信息省去了
a+b=5
a-b=1
a*b=6
a/b=1 # 3/2=1.5 .5被省略了
a//b=1
a**b=9
a%b=1

查看数据类型 type

type语句可以告诉我们变量里存放的是什么类型的数据。用法如下:

1
2
3
4
5
6
# 用法:type(变量名)
a=1
b='1'
# 为了看到结果需要用print把结果在日志中打印
print(type(a))
print(type(b))

数据类型-列表与字典

为了更方便的取用数据,在最基本的数据类型-数字与字符串基础上,还有其他的数据类型,他们往往具有更复杂的结构更便捷的功能。比如接下来要介绍的列表(List)、字典(Dictionary),不过这里的内容实在是繁多,此处只介绍最常用的内容,其他内容后续用到再讲。

  • 列表(list):列表数据类型能方便我们操作一组数据。比如一组股价、一组股票名等。
    • 建立方法如下:

      1
      2
      3
      4
      5
      6
      7
      8
      # 建立一个list: 变量名=[数据或变量名,数据或变量名,......]

      a=[1,1,2,3,5,8,13,21]
      b=['000001.XSHE','002043.XSHE','002582.XSHE','600000.XSHG']
      c=[1,2,'good',[1,2,'luck'],a,b]

      # 值得注意的是例子中的c,c是一个list,其中的包含了6个元素,其中有数字(1,2),有字符串('good'),以及三个list([1,2,'luck'],a,b)。
      # 因此你应该知道,list中可混合的存放多种数据类型,list中放一个list也行。

    • 选取list中的某个元素的用法如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      # 方法: list类型的变量[位置(或称下标或索引)]
      # 索引从0开始
      # 可以用负数代表倒数第几

      c=[1,2,3,4]

      # 为了看到结果我们用print打印
      print(c[0])
      print(c[1])
      print(c[2])
      print(c[-1])

      # 用之前的方法执行后结果如下:(前面的日期以后就不写了)
      1
      2
      3
      4

    • 选取list中的一段的用法如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      # 方法: list类型的变量[起点索引:终点索引]
      # 起点索引省略则默认为0
      # 终点索引省略则默认为最后的索引
      # 注意此时的结果仍是一个list

      c=[1,2,3,4]

      # 为了看到结果我们用print打印
      print(c[2:3])
      print(c[:-1])
      print(c[3:])
      print(c[:])

      # 执行后结果如下:
      [3]
      [1, 2, 3]
      [4]
      [1, 2, 3, 4]

  • 字典(dictionary):字典数据类型同样能方便我们操作一组数据,与list不同的是我们可以为这组数据自定义地建立索引。
    • 建立方法如下:

      1
      2
      3
      4
      5
      # 建立方法: 变量名={索引名:数据,索引名:数据,....}
      # dict中的索引也叫键(key),数据也叫值(value)

      a={'平安银行':'000001.XSHE','浦发银行':'600000.XSHG'}
      b={'开盘价':10.0,'收盘价':11.0,'涨跌幅':0.10}

      • 选取dict中的某个key的值方法如下:

        1
        2
        3
        4
        5
        6
        7
        8
        # 选取方法 dict类型的变量[key]
        a={'平安银行':'000001.XSHE','浦发银行':'600000.XSHG'}

        # 为了看到结果我们用print打印
        print(a['平安银行'])

        # 执行后结果如下:
        000001.XSHE

    • 选取dict中的所有key与所有value

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      # 选取dict中的所有key: dict类型变量.keys()
      # 选取dict中的所有value: dict类型变量.values()
      # 注意返回的结果是list类型的

      a={'平安银行':'000001.XSHE','浦发银行':'600000.XSHG'}
      b=a.keys()
      c=a.values()

      # 为了看到结果我们用print打印
      print("a.keys()=%s" % (a.keys()))
      print("b=%s" % (b))
      print("c=%s" % (c))

下单、函数、API

我们继续以前文策略代码为例进行讲解,如下:

1
2
3
4
5
6
def initialize(context):
run_daily(period, time='every_bar')
g.security = '000001.XSHE'

def period(context):
order(g.security, 100)

通过前文讲解,现在这段代码中就剩这句下单语句还没讲解了。为了理解这条语句,需要学习下python中函数的知识。

1
order(g.security, 100)

函数与API

函数是封装好的,可重复使用的 ,用来实现专一功能的代码段。函数能使代码易于维护与交流,提高编写策略的效率。通俗的理解是,把一系列代码指令包起来就是一个函数,起个名字就是函数名,之后用这个函数名,就知道这个名字指代那被包起来的一系列代码指令了。

Python语言自带了许多内建函数,比如之前见过的print()、type()都是Python自带的函数,可以直接用。你也可以自己创建函数自己用,这被叫做自定义函数。比如如下这段框架代码其实就是自定义了一个名为period的函数,该函数内包了一个聚宽系统自带的函数order():

1
2
def period(context):
order(g.security, 100)

order()的准确称谓其实是API(application programming interface,即应用程序编程接口),API的含义与函数有所不同,解释起来略复杂。不过实际使用中跟函数几乎没有差别,可以理解成聚宽平台基于python封装而成的函数。在聚宽的API文档中你可以看到除order()外其他API。

使用一个函数

在使用函数的时候,通常需要提供一些参数(也有可能不需要),函数根据提供的参数,执行一系列的函数作者设计好的操作,往往也会根据提供的参数返回结果(也可能返回为空,即不返回),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 用法: 函数名(参数,参数,......)
# 例子如下:

# 提供了两个参数g.security和100,执行了买入g.security中数据对应的股票100股的操作
order(g.security, 100)

# 提供了一个参数"你好",执行了打印"你好"的操作
print("你好")

# 提供了一个参数"1"给type()函数,type函数执行了识别"1"数据类型的操作,并返回了"1"数据类型为结果。
# type返回的结果被当做参数提供给了print(),print执行了打印type返回的结果的操作
# type与print的嵌套使用,实现了打印"1"数据类型的操作。
print(type("1"))

可见,函数的功能多种多样,需要参数、返回的结果亦不尽相同,所以具体怎么用需要看函数作者提供的说明文档,或者看函数内的设计代码自己推断。函数内的代码不见得看得到,看到不一定看得懂,想看懂也可能很辛苦。所以一般函数的用法要看函数作者提供的说明文档。

聚宽设计的函数(如前文所说准确叫法是API)的用法都写在API文档里,位置在聚宽网站导航栏-帮助-API文档。

接下来以order为例讲下文档怎么看。在API文档中找到 order - 按股数下单的说明,如下:

可以看到,order可接受的参数有5个,分别是security,amount,style,side,pindex,这五个参数的名字与含义是函数作者设计的。意思是你使用order提供参数的时候,被提供参数将按提供的顺序依次对应这5个参数。比如下面的写法就是错误的。

1
2
# 函数会按顺序把100对应为security,即股票代码,把"000001.XSHE"对应为amount,即要交易的数量。所以就会错。
order(100,"000001.XSHE")

如果需要不按顺序输入参数,则需要用如下写法:

1
2
3
# 用等号表示对应关系,参数名写前,要当做参数的变量或数据写在后
# 如下是把100当做amount参数,把"000001.XSHE"当做security参数。
order(amount=100,security="000001.XSHE")

可以发现有些参数后面有等号,如style=None,含义style参数不提供的话,会被默认是None,其他的side='long', pindex=0也是一样的道理,如果不提供会被默认是等号后面的内容。所以前文order()只写了两个参数也不会错。注意,security和amount后面没有等号,即没有默认值,则必须提供参数不能省略。

1
2
3
# 以下两句含义相同
order("000001.XSHE",100)
order("000001.XSHE",100,None,'long',0)

细说下order的各个参数

  • security:标的代码,数据类型要求是字符串,想知道基金、期货、指数的代码都是什么,可以在这里看。比如聚宽数据-向下滚动页面-点击指数数据,可以看到各指数的代码。特别的是股票代码目前没有页面,但只需在平时使用的股票代码后面加后缀就好了。

    深交所(深交所股票0开头)股票代码后缀为.XSHE,如000001.XSHE。 上交所股票代码(上交所股票6开头)后缀为.XSHG 如600000.XSHG。

  • amount:交易数量,正数表示买入,负数表示卖出。
  • style:订单类型,有市单价和限单价,默认市单价。
  • side:开空单还是多单,默认为多单。
  • pindex:选择资金仓位的参数。

根据说明文档,order函数是有返回值的,如果创建订单成功, 则返回Order对象, 失败则返回None。有返回值不一定要用,比如前文的例子都没用到这个返回值,实际上策略做的相当完备的时候才可能用到。一般用法是,根据返回值是否是None,判断是否下单成功,成功时,根据返回值可以查询订单或取消订单等。不过具体实现方法、以及Order对象是什么,还需要学习很多的知识,后续可能会讲到。

自定义函数

Python 定义函数使用 def 关键字,一般格式如下:

1
2
def 函数名(参数列表):
函数体

函数名即为该函数起的名字,函数体即包在函数中的一系列操作的代码,参数列表即使用函数需要提供的参数,比如一个根据圆半径求周长的函数如下:

1
2
3
4
5
# 根据圆半径r求周长l
def yuan(r):
p=3.14
l=2*p*r
return l

return的含义是结束函数的运行并返回一个值,如上例子中就是返回了算好的周长l。如果不写return,函数体运行完后,自动return None。

至此,你应该意识到,函数内部是相对独立的,数据想进来要通过参数传进来,想出去要通过返回值传出去,函数从获得参数到返回值的过程中所产生的数据与变量中没通过返回值传出去的,在函数运行结束后(即返回值后)都将被计算机释放不再存储。如果想函数间通用某变量可以考虑用之前讲的全局变量。

如前文讲使用函数时看到的,可以用等号给参数附加默认值,而且可以用逗号分隔多个参数,例子如下:

1
2
3
4
5
# 根据圆半径r求周长l的k分之一
def yuan(r,k=1):
p=3.14
l=2*p*r/k
return l

使用自定义函数的方法跟前文讲的使用函数的方法一致,需要说明的是定义函数的代码放的位置,如下:

常用的下单函数

常用的下单函数有四个,使用方法和order()差不多,可能有人自己看API文档就能学会了。接下来我们分别介绍下基本用法,同样的不讲style,side,pindex这三个参数。

  • order(security,amount),刚刚细讲过,含义是买卖一定数量的(单位:股)股票。security是股票代码,amount是数量,amount为负数时就是代表卖出了,需要知道的是,国内股票买入最小单位是1手即100股。例子如下:
    1
    2
    3
    4
    # 买入100股平安银行
    order("000001.XSHE",100)
    # 卖出100股平安银行
    order("000001.XSHE",-100)
  • order_target(security,amount),含义是通过买卖,将股票仓位调整至一定数量(单位:股)。security是股票代码,amount是数量。例子如下:
    1
    2
    3
    # 调整平安银行的持股数量至1000股
    # 即,如果目前平安银行的持股数量低于1000股就买入,高于就是卖出,不高不低就不动。
    order_target("000001.XSHE",1000)
  • order_value(security,value),含义是买卖一定价值量(单位:元)股票。security是股票代码,value是价值量。value为负数时就是代表卖出了。例子如下:
    1
    2
    3
    4
    5
    6
    7
    # 买入10000元的平安银行
    # 如果当前股票市价是10元,则代表买入1000股
    # 如果除不开系统会自动调整成相近的合理数量。卖出时也会。
    order_value("000001.XSHE",10000)
    # 卖出10000元的平安银行
    # 如果当前股票市价是100元,则代表卖出100股
    order_value("000001.XSHE",-10000)
  • order_target_value(security,value),通过买卖,将股票仓位调整至一定价值量(单位:元)。security是股票代码,value是价值量。例子如下:
    1
    2
    3
    # 调整平安银行的持股价值量至10000元
    # 即,如果目前平安银行的持股价值量(按股票市价算)低于10000元就买入,高于就是卖出,不高不低就不动。
    order_target_value("000001.XSHE",10000)

读者在尝试练习使用这些语句的时候,可以点击运行回测,通过查看回测结果页中的交易详情来看语句的执行效果,同时也可以看下日志。如下:

股票拆分合并和分红,交易的税费,下单导致成交价向不利的方向波动,这些因素系统都是默认考虑并仿真处理的了,具体的详情以及下的订单系统是如何模拟真实情况撮合成交的,可以看下API文档订单处理。其实新手不用太关注 这些订单处理的细节,不核心,目前也不容易理解,可以等以后自己比较熟悉了再看。

读取context中的数据与条件判断

通过前文的讲解,我们已经能理解最开始的那个简单的策略例子了

1
2
3
4
5
6
def initialize(context):
run_daily(period, time='every_bar')
g.security = '000001.XSHE'

def period(context):
order(g.security, 100)

接下来,我们将在此基础上进行改进与举例,学习新内容。

context的结构

context是一个回测系统建立的Context类型的对象,其中存储了如当前策略运行的时间点、所持有的股票、数量、持仓成本等数据。

对象可以理解为特殊类型的变量,对象的结构往往比我们之前见过的list与dict更复杂,被定义好的对象是有名字的,比如context是一个变量,它的变量类型是一个Context类型的对象,就像dict包括key与value,Context类型的对象也包括很多属性,而且可以嵌套另一个种类型的对象,结构见下图。图中只包括了主要与常用的内容,详细介绍可以看API文档:Context对象

关于对象的知识非常复杂繁多,目前我们只需学习如何取用context中的数据就好。

context中的数据取用方法

获取对象类型变量内包含的数据方法是用英文句号隔开,而当包含的是另一个对象时,只需再应用英文句号隔开即可,例子如下:

1
2
3
4
5
6
7
8
9
10
11
# 打印可用资金
print(context.portfolio.available_cash)
# 打印运行频率
print(context.run_params.frequency)
# 打印当前单位时间的开始时间
print(context.current_dt)

# 执行后日志内容如下
# 1000000.0
# day
# 2016-06-01 09:30:00

当要获取的对象内的数据是另一种有结构的变量类型时,比如dict或list,正常按照该变量类型进一步取用数据即可。例如context.portfolio.positions是一个dict,我们就可以应用之前讲过的dict 的用法来使用它,例子如下,这次给出了完整代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# context.portfolio.positions的含义是仓位信息,所以为了让它有数据,需要在取之前买入并持有股票。

def initialize(context):
run_daily(period,time='every_bar')
g.security = '000001.XSHE'

def period(context):
order(g.security, 100)
# 打印所有键
print(context.portfolio.positions.keys())
# 打印所有值
print(context.portfolio.positions.values())
# 打印g.security的开仓均价
print(context.portfolio.positions[g.security].avg_cost)

# 执行后日志内容如下
# ['000001.XSHE']
# [UserPosition({'avg_cost': 8.539999999999997, 'security': '000001.XSHE', 'closeable_amount': 0, 'price': 8.53, 'total_amount': 100})]
# 8.54

常用的context数据写法如下,推荐自己动手试下。

  • 当前时间 context.current_dt
  • 当前时间的“年-月-日”的字符串格式 context.current_dt.strftime("%Y-%m-%d")
  • 前一个交易日 context.previous_date
  • 当前可用资金 context.portfolio.available_cash
  • 持仓价值 context.portfolio.positions_value
  • 累计收益 context.portfolio.returns
  • 当前持有股票 context.portfolio.positions.keys()
  • 当前持有的某股票的开仓均价 context.portfolio.positions['xxxxxx.xxxx'].avg_cost
  • 当前持有的某股票的可卖持仓量 context.portfolio.positions['xxxxxx.xxxx'].closeable_amount

条件判断

能够获取context的数据后,我们会考虑利用这些数据丰富策略的逻辑,但在此之前我们还要学习if条件判断语句,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 如果 条件1成立为 True 将执行代码块1
# 如果 条件1不成立为False,将判断条件2
# 如果 条件2成立为 True 将执行代码块2
# 如果 条件2还不成立为False,将执行代码块3

if 条件1:
代码块1
elif 条件2:
代码块2
else:
代码块3

# 注意
# elif 可以有多个连续写
# 且elif和else都可以省略
# 条件判断语句中可以嵌套条件判断语句

举几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 打印a、b中最大值
if a>=b:
print(a)
else:
print(b)

# 判断a的正负性
if a>0:
print('正')
elif a<0:
print('负')
elif a==0:
print('零')

# 如果当前是2018-05-04,则下单买入100股平安银行
date=context.current_dt.strftime("%Y-%m-%d")
if date=='2018-05-04':
order('000001.XSHE',100)

# 判断a大小情况
if a>0:
if a<1:
print('a大于0且小于1')
else:
print('a大于等于1')
else:
print('a小于等于0')

条件判断语句比较简单,但还需说明的是条件的写法中用到的运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 写条件常用运算符:
# < 小于
# > 大于
# <= 小于等于
# >= 大于等于
# == 等于
# != 不等于
# and 与,即and两边条件同为真,则真
# or 或,即or两边条件任意一个为真,则真
# not 非,即not右侧条件为真,则假,not右侧条件为假,则真

# 以判断a是否为0的几个写法为例
# 写法1
if a!=0:
print('否')
else:
print('是')
# 写法2
if a>0 or a<0:
print('否')
else:
print('是')
# 写法2
if a>=0 and a=<0:
print('是')
else:
print('否')
# 写法3
if not a==0:
print('否')
else:
print('是')

止损

狭义的止损是指当亏损达到一定幅度后下单卖出该股票的操作,目的是减少进一步的亏损。广义则指在狭义的思路上衍生的复杂的减少亏损的方法。更多的情况下指狭义的止损。综合运用前文的讲过的内容我们已经可以实现当亏损达到一定幅度后下单卖出该股票的止损操作了,不妨先自己思考下再继续学习。

通过context的数据可以得到持有股票的成本和现价,从而可以算出该股票的盈亏情况,运用条件判断语句根据盈亏情况从而决定是否卖出股票,从而实现止损操作,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def initialize(context):
run_daily(period, time='every_bar')
g.security = '000001.XSHE'

def period(context):
# 买入股票
order(g.security, 100)
# 获得股票持仓成本
cost = context.portfolio.positions['000001.XSHE'].avg_cost
# 获得股票现价
price = context.portfolio.positions['000001.XSHE'].last_price
# 计算收益率
ret = price/cost - 1
# 打印日志
print("成本价: %s" % cost)
print("现价: %s" % price)
print("收益率: %s" % ret)
# 如果收益率小于-0.01,即亏损达到1%则卖出
if ret < -0.01:
order_target('000001.XSHE', 0)
print("触发止损")

设置回测时间为从2017-03-01到2017-03-31,初始资金为100000,频率为天。回测发现会在2017-03-20触发止损。

循环、多股票策略

我们继续以如下这个简单的策略为例,进一步在策略中操作多个股票。

1
2
3
4
5
6
def initialize(context):
run_daily(period, time='every_bar')
g.security = '000001.XSHE'

def period(context):
order(g.security, 100)

用list数据类型存储多个股票

事实上,根据前面的所学我们是可以写多个股票的策略的,无非是把原来单个股票的操作类似地再写几遍,比如下面这个策略就在操作两个股票。

1
2
3
4
5
6
7
def initialize(context):
run_daily(period,time='every_bar')
g.security1 = '000001.XSHE'
g.security2 = '000002.XSHE'
def period(context):
order(g.security1, 100)
order(g.security2, 100)

显然的问题是,当股票比较多的时候,就要写很多遍,这样的写法就会很麻烦,看着也会比较乱。因此我们要学习其他的写法。首先我们先学习用list数据类型存储多个股票,如下:

1
2
3
4
def initialize(context):
run_daily(period,time='every_bar')
# 把两个股票代码作为list存入g.security中
g.security = ['000001.XSHE','000002.XSHE']

循环语句

for循环可以遍历任何序列的项目,比如一个list,一般用法如下:

1
2
3
# 含义是依次把序列中的元素赋值给for后的变量,并执行循环语句
for 变量 in 一个序列:
要循环的语句,也叫循环体

来看个使用for的例子:

1
2
3
4
5
6
7
8
9
10
for k in ['大卫','查理曼','凯撒','亚历山大']:
print(k)

# 执行后日志如下:
# 大卫
# 查理曼
# 凯撒
# 亚历山大

# 可见,for语句的运行过程是,取出list中第一个元素'大卫'并将其赋值给k,然后执行print(k)即在日志中打印k,,此时k中是'大卫',之后,取出list中第二个元素'查理曼'并将其赋值给k,然后执行print(k)即在日志中打印k,此时k中是'查理曼',以此类推,直到'亚历山大'被打印。

使用for语句时有一个常见一起使用的语句range(),它的功能是生成等差数列的,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
range(首项,上限,步长)

# 首项 就是这个数列的第一项,可省略,省略后默认为0
# 步长 就是数列的公差、间隔,可省略,省略后默认为1
# 上限 是用来限制数列长度的,即数列不得大于或等于上限。不可省略。
# 另外,python2中range产生的是list,但python3中产生的不是list,但可以用list()这个语句把结果转成list类型,比如list(range(1,7,2))。我们策略编辑环境是python2。

# 一个例子
for j in range(1,7,2):
print(j)
print(range(1,7,2))

# 执行后日志如下:
# 1
# 3
# 5
# [1, 3, 5]

continue与break是重要的修饰循环执行流程的语句,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# break的作用是写在循环体中用来跳出当前的整个循环过程
# continue的作用是写在循环体中用来跳出当前的这一次的循环过程
# 通过一个例子应该就能明白两者的作用与区别

# 一个简单的循环例子
for t in range(4):
print(t)

# 执行的结果是
# 0
# 1
# 2
# 3

# 在例子中使用break。可以看到当循环到2的时候,打印omg后,执行break,终止了整个循环过程,不再继续循环3了,所以omg后就什么都没了。
for t in range(4):
if t == 2:
print('omg')
break
print(t)

# 执行的结果是
# 0
# 1
# omg

# 在例子中使用continue。可以看到当循环到2的时候,打印omg后,执行continue,跳过了当前正循环的t为2这个循环过程的余下部分,不在继续执行之后的语句(即print(t),此时t等于2),而继续循环3了,所以omg后有打印3。
for t in range(4):
if t == 2:
print('omg')
continue
print(t)

# 执行的结果是
# 0
# 1
# omg
# 3

写一个简单多股票策略

用刚学的知识把之前简单的策略例子改写成多股票版本,如下。

1
2
3
4
5
6
7
8
def initialize(context):
run_daily(period, time='every_bar')
g.security = ['000001.XSHE','000002.XSHE']

def period(context):
# 每个股票买100股
for stk in g.security:
order(stk, 100)

其实运用所学的知识已经可以进一步的加入很多东西了,比如在这个多股票的基础上在加入之前讲过的止损。不妨自己先尝试下再看下面的样例代码。样例代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def initialize(context):
run_daily(period, time='every_bar')
# 把两个股票代码作为list存入g.security中
g.security = ['000001.XSHE','000002.XSHE']

def period(context):
for stk in g.security:
order(stk, 100)
# 获得股票持仓成本
cost = context.portfolio.positions[stk].avg_cost
# 获得股票现价
price = context.portfolio.positions[stk].price
# 计算收益率
ret = price / cost - 1
# 如果收益率小于-0.01,即亏损达到1%则卖出股票,幅度可以自己调,一般10%
if ret < -0.01:
order_target(stk, 0)
print("股票%s触发止损" % stk)

获取典型常用数据

聚宽数据

聚宽数据这个页面可以看到聚宽平台集成好的各大类数据,如下图,点击可以查看详情与用法。

但实际上可能有些数据要在API文档里才比较容易找到,比如龙虎榜数据等。

接下来会介绍几种常用数据的取用方法,这些取用方法比较典型,掌握后能覆盖基本的数据需求,同时能够学会使用其他数据。

获取指数成分股

指数成分股:为了衡量故事中某一大类股票整体的涨跌情况,通常会用这一类股票加权平均编制出一个指数,而这些股票则叫做该指数的成分股,指数的成分股的选取会发生变动。 比如上证指数是用所有上交所的股票编制而成,可以衡量上交所股票整体的涨跌情况,有的股票退市了就会被剔除出成分股。比较常见的指数有上证指数、深证板指、沪深300指数、中证500指数、上证50指数等。可以在数据-指数数据-指数列表中找到聚宽支持的指数及其指数代码。

获取指数成分股需要用到的API为get_index_stocks

之前讲过怎么看API文档以及函数参数的含义,现在应该能直接看说明使用了。补充一个更详细点的例子:

1
2
3
4
5
6
7
8
9
# 获取20180301时,上证50指数(000016.XSHG)成分股
t = get_index_stocks("000016.XSHG", "2018-03-01")
print(t[0])
print(t)

# 打印日志如下。股票代码在list中被打印出来前面会带有的u代表是对字符串进行unicode编码(略复杂,不懂没关系),只是显示效果,单独打印t[0]时就没有u。

# 600000.XSHG
# [u'600000.XSHG', u'600016.XSHG', u'600019.XSHG', u'600028.XSHG', u'600029.XSHG', u'600030.XSHG', u'600036.XSHG', u'600048.XSHG', u'600050.XSHG', u'600104.XSHG', u'600111.XSHG', u'600309.XSHG', u'600340.XSHG', u'600518.XSHG', u'600519.XSHG', u'600547.XSHG', u'600606.XSHG', u'600837.XSHG', u'600887.XSHG', u'600919.XSHG', u'600958.XSHG', u'600999.XSHG', u'601006.XSHG', u'601088.XSHG', u'601166.XSHG', u'601169.XSHG', u'601186.XSHG', u'601211.XSHG', u'601229.XSHG', u'601288.XSHG', u'601318.XSHG', u'601328.XSHG', u'601336.XSHG', u'601390.XSHG', u'601398.XSHG', u'601601.XSHG', u'601628.XSHG', u'601668.XSHG', u'601669.XSHG', u'601688.XSHG', u'601766.XSHG', u'601800.XSHG', u'601818.XSHG', u'601857.XSHG', u'601878.XSHG', u'601881.XSHG', u'601985.XSHG', u'601988.XSHG', u'601989.XSHG', u'603993.XSHG']

获取股票行情数据

这里股票行情数据包括很多项,以聚宽数据SecurityUnitData类为例,包含如下数据:

  • open: 时间段开始时价格
  • close: 时间段结束时价格
  • low: 最低价
  • high: 最高价
  • volume: 成交的股票数量
  • money: 成交的金额
  • factor: 前复权因子
  • avg: 这段时间的平均价
  • pre_close: 前一个单位时间结束时的价格
  • paused: 这只股票是否停牌,是则为1,否则为0

同时,还有其他接口可以获取股票行情数据:

  • history: API文档:history
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 例子 df=True,返回dataframe类型
    w=history(count=3, field='money', security_list=['000001.XSHE','000002.XSHE'])
    print(w)

    # 结果如下:
    # 000001.XSHE 000002.XSHE
    # 2016-08-29 5.322954e+08 1.796321e+09
    # 2016-08-30 5.618541e+08 2.072873e+09
    # 2016-08-31 4.638758e+08 5.748581e+09

    # 例子 df=False,返回dict类型
    w=history(count=3, field='money', security_list=['000001.XSHE','000002.XSHE'],df=False)
    print(w)

    # 结果如下:
    # {'000001.XSHE': array([ 5.32295362e+08, 5.61854066e+08, 4.63875763e+08]), '000002.XSHE': array([ 1.79632055e+09, 2.07287325e+09, 5.74858107e+09])}
  • attribute_history: API文档:attribute_history
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 例子
    w=attribute_history(security='000001.XSHE',count=3, fields=['money','high'])
    print(w)

    # 结果如下:
    # money high
    # 2016-08-29 5.322954e+08 9.31
    # 2016-08-30 5.618541e+08 9.33
    # 2016-08-31 4.638758e+08 9.36

DataFrame是一种二维表结构的功能强大的数据类型,常用与数据处理与分析。 包括index(行标签、索引)、columns(列标签)、values(值)三个部分。取用方法如下,注意三个部分的数据类型不是固定的,因此功能很灵活但也更难使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 一个dataframe类型的例子
w=attribute_history(security='000001.XSHE',count=3, fields=['money','high','open'])
print(w)

# 结果如下:
# money high open
# 2016-08-30 5.618541e+08 9.33 9.29
# 2016-08-31 4.638758e+08 9.36 9.32
# 2016-09-01 4.548486e+08 9.38 9.35

# 获取index
print(w.index)
# 结果如下,是datatimeindex类型,很特殊,不常用,建议新手回避。
# DatetimeIndex(['2016-08-30', '2016-08-31', '2016-09-01'], dtype='datetime64[ns]', freq=None, tz=None)

# 获取columns
print(w.columns)
# 结果如下,是index类型
# Index([u'money', u'high', u'open'], dtype='object')

# 可以用list()将其转成list
print(list(w.columns))
# 结果如下
# ['money', 'high', 'open']

# 获取values
print(w.values)
# 结果如下,是一个嵌套的list
# [[ 5.61854066e+08 9.33000000e+00 9.29000000e+00]
# [ 4.63875763e+08 9.36000000e+00 9.32000000e+00]
# [ 4.54848634e+08 9.38000000e+00 9.35000000e+00]]

  • 选择dataframe某几列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    # 按标签获取某几列.loc[:,[列标签名,...]]
    print(w.loc[:,['open','high']])
    # 结果如下
    # open high
    # 2016-08-29 9.28 9.31
    # 2016-08-30 9.29 9.33
    # 2016-08-31 9.32 9.36

    # 按位置获取某几列.iloc[:,[位置,...]],位置的含义是第几个,从0开始。下文同。
    print(w.iloc[:,[0,2]])
    # 结果如下
    # money open
    # 2016-08-29 5.322954e+08 9.28
    # 2016-08-30 5.618541e+08 9.29
    # 2016-08-31 4.638758e+08 9.32

    # : 即冒号,可以代表全部,iloc或loc都可以。
    print(w.iloc[:,:])
    # 结果如下
    # money high open
    # 2016-08-29 5.322954e+08 9.31 9.28
    # 2016-08-30 5.618541e+08 9.33 9.29
    # 2016-08-31 4.638758e+08 9.36 9.32

    # 选择后的数据依然是dataframe类型,用.values可以获取数据。对后文的行情况也成立。
    print(w.iloc[:,[0,2]].values)
    # 结果如下,是个list
    # [[ 5.61854066e+08 9.29000000e+00]
    # [ 4.63875763e+08 9.32000000e+00]
    # [ 4.54848634e+08 9.35000000e+00]]

  • 选择dataframe某几行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 按标签获取某几行.loc[[行标签名,...],:]
    print(w.loc[['2016-08-29','2016-08-31'],:])
    # 此处这样写会报错,原因是当前的行标签类型是DatetimeIndex,不是字符串,所以使用标签名时要注意数据类型。而时间类型的数据处理往往非常麻烦,因此行或列标签名是日期情况下建议新手回避,改使用位置获取。

    # 按位置获取某几行.iloc[[位置,...],:]
    print(w.iloc[[0,2],:])
    # 结果如下
    # money high open
    # 2016-08-29 5.322954e+08 9.31 9.28
    # 2016-08-31 4.638758e+08 9.36 9.32

    # : 即冒号,行情况下依然可以代表全部
    print(w.loc[:,:])
    # 结果如下
    # money high open
    # 2016-08-29 5.322954e+08 9.31 9.28
    # 2016-08-30 5.618541e+08 9.33 9.29
    # 2016-08-31 4.638758e+08 9.36 9.32

  • dataframe 行列转置

    1
    2
    3
    4
    5
    6
    7
    # 行列转置的意思就是按对角线行列反转,方法是.T
    print(w.T)
    # 结果如下
    # 2016-08-29 2016-08-30 2016-08-31
    # money 5.322954e+08 5.618541e+08 4.638758e+08
    # high 9.310000e+00 9.330000e+00 9.360000e+00
    # open 9.280000e+00 9.290000e+00 9.320000e+00

pandas是一个模块或者叫库,可以让我们直接利用其中包含的已经设计好的函数或数据类型,加快我们的工作效率。pandas主要功能是数据处理与分析,其中dataframe就是属于pandas的,是原生的python语言没有的。随着深入的学习,你会遇到其他的功能模块,一般来说要使用一个模块是要用一行代码加载导入的,但pandas聚宽系统已经自动加载了,不必额外写代码导入了。

获取股票财务数据

股票财务数据这里是指发股票的公司发布的财务报表中的数据。可以在聚宽数据-股票财务数据查看数据详情。

财务报表简称财报,是用来向股东汇报企业经营情况的,上市公司必须按季度公布财报,一年有四季所以财报依发布次序为一季报、半年报(也称中报)、三季报、年报。而具体的发布日期在一定期限内即可,并非固定,年报要求年度结束四个月内披露,半年报是上半年结束后两个月内,一季报与三季报是季度结束后一个月内。特别的是像总市值、市盈率这种跟股价挂钩的市值数据是每天更新的。

获取股票财务数据需要用到的API为get_fundamentals

未来函数是什么?

  • 我们做回测去验证策略时,其实是用历史数据去模拟当时的市场从而得知策略在历史上表现如何,但是如果策略利用了历史当时无法得到的信息,往往就会造成回测结果极大失真,这时我们会说这个策略有未来函数。
  • 举一个典型的有未来函数的策略:每天买明天涨停的股票。 事实上你是不能知道明天哪个股票涨停的,所以现实中是不能实现的,但是我们做回测是用的历史数据,所以我们其实是能实现用2012年的数据对这个买明日涨停股的策略做回测的,毕竟现在已经过了2012年,2012年每天哪个股票会涨都是已经知道的了。这样的有未来函数的回测结果肯定是没价值的,因为现实中不能实现,尽管回测结果有时特别喜人。

单季度与报告期。

  • 之前讲过,财务数据按季度发布,一般财经网站上提供的财务数据是默认按报告期提供的,即每季度统计的周期跨度分别为第一季度、前两个季度、前三个季度、前四个季度(全年)。
  • 而聚宽考虑到量化分析,提供的财务数据全是单季度的,即每季度统计的周期跨度分别为第一季度、第二季度、第三季度、第四季度。
  • 因此,当你发现聚宽财务数据比财经网站的财务数据差的很多时,很可能是单季度与报告期的差别造成的。

本地获取聚宽数据

申请地址:https://www.joinquant.com/default/index/sdk?f=home&m=banner 安装方法: 调用方法:

1
2
3
4
5
from jqdatasdk import *
import jqdatasdk as jq
jq.auth('手机号', '密码')
df = jq.get_price("000001.XSHE")
print(df)

综合之前所学写一个策略

通过前文基础知识的学习,读者可以应用所学写成一个策略。如果发现某些知识忘了很正常,回头再看就行,用到什么去学什么学习的效率更高。

灵感细化

之前提到过策略灵感的来源多种多样,可能是通过阅读、通过与人交流、或是通过自己感悟与研究等等。灵感最初可能只是模糊的感觉或者疑问比如“感觉低市盈率的股票好像长期收益更好”、“当股价一旦超过整百的时候会不会更容易继续涨一段”、“这个股票和那个股票的股价数据看起来好像符合某种统计规律”等等。验证灵感的一个基本方法是把灵感细化,写成策略做回测。

现在你听说了这样一件事,小市值股票过去很长一段时间内收益特别好,但最近不太行了。你觉得这件事比较有价值,想要写成策略来回测验证下。请思考下,应该写一个什么样的策略来验证这件事呢?

为了验证灵感,我们把灵感细化成内容如下的这样一个策略。

1
2
3
每天找出市值排名最小的前10只股票作为要买入的股票。
若已持有的股票的市值已经不够小而不在要买入的股票中,则卖出这些股票。
买入要买入的股票,买入金额为当前可用资金的10分之一。

考虑到不一定要选10个股票,股票数量应该是个可以方便调节的变量,因此策略内容改成如下这样更好。

1
2
3
4
设定好要交易的股票数量stocksnum 
每天找出市值排名最小的前stocksnum只股票作为要买入的股票。
若已持有的股票的市值已经不够小而不在要买入的股票中,则卖出这些股票。
买入要买入的股票,买入金额为当前可用资金的stocksnum分之一。

逐步实现

因为最终目的是要写成代码交给计算机回测,因此要逐步把文字的意思用代码实现,首先要把这个策略放到之前讲过的初始化与周期循环的策略框架中,如下。

1
2
3
4
5
6
7
8
def initialize(context):
run_daily(period, time='every_bar')
# 设定好要交易的股票数量stocksnum

def period(context):
# 代码:找出市值排名最小的前stocksnum只股票作为要买入的股票
# 代码:若已持有的股票的市值已经不够小而不在要买入的股票中,则卖出这些股票
# 代码:买入要买入的股票,买入金额为可用资金的stocksnum分之一

接下来,你只需要逐步的把策略的全部内容用代码实现出来,技巧是把复杂的内容拆分成多个简单的内容,逐步实现,对于不确定的东西print打印出来看看。往下读之前,建议自己独立实现下试试,基本都是用讲过的内容。遇到困难可以看下我下面给出提示,所有提示后面会给出参考代码。

提示

  • 代码:设定好要交易的股票数量stocksnum。这句非常简单,需要注意的是要用到之前讲过的全局变量。
  • 代码:找出市值排名最小的前stocksnum只股票作为要买入的股票。首先使用get_all_securities取其index得到股票列表。然后,使用获取财务数据的方法找出当前全市场股票中市值最小的前stocksnum个的股票代码。
  • 代码:若已持有的股票的市值已经不够小而不在要买入的股票中,则卖出这些股票。使用context数据获取当前持仓情况,用for循环语句与if判断语句判断股票是否在当前持仓中,用in判断是否一个元素在某list中,用下单API实现卖出操作。
  • 代码:买入要买入的股票,买入金额为可用资金的stocksnum分之一。使用context数据获取当前可用资金总量,用for循环与下单API实现买入每个要买入的股票。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def initialize(context):
run_daily(period, time='every_bar')
g.stocksnum = 10

def period(context):
# 代码:找出市值排名最小的前stocksnum只股票作为要买入的股票
# 获取当天的股票列表
scu = get_all_securities(date=context.current_dt).index.tolist()
# 选出在scu内的市值排名最小的前stocksnum只股票
q = query(valuation.code).filter(valuation.code.in_(scu)).order_by(valuation.market_cap.asc()).limit(g.stocksnum)
df = get_fundamentals(q)
# 选取股票代码并转为list
buylist=list(df['code'])

# 代码:若已持有的股票的市值已经不够小而不在要买入的股票中,则卖出这些股票。
# 对于每个当下持有的股票进行判断:现在是否已经不在buylist里,如果是则卖出
for stock in context.portfolio.positions:
if stock not in buylist: # 如果stock不在buylist
order_target(stock, 0) # 调整stock的持仓为0,即卖出

# 代码:买入要买入的股票,买入金额为可用资金的stocksnum分之一
# 将资金分成g.stocksnum份
position_per_stk = context.portfolio.cash / g.stocksnum
for stock in buylist:
order_value(stock, position_per_stk)

调整与改进

至此这已经是一个完整可运行的策略了,你可以试试看,回测结果应该已经可以一定程度上验证灵感了。不过虽然策略完成,我们却发现现在策略是每天进行一次选股并交易,我们觉得这太频繁了,希望能实现通过一个变量period控制操作的周期,即每period天进行一次选股并交易。

依然建议先试着自己做下,提示如下,提示之后是参考代码。

  • 像stocksnum那样用全局变量的方式建立period变量
  • 用一个变量记录策略运行天数
  • 用取余运算配合if判断语句判断是否又经过period天

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def initialize(context):
run_daily(period, time='every_bar')
# 设定好要交易的股票数量
g.stocksnum = 7
# 设定交易周期
g.period = 13
# 记录策略进行天数
g.days = 0

def period(context):
# 判断策略进行天数是否能被轮动频率整除余0
if g.days % g.period == 0:
# 代码:找出市值排名最小的前stocksnum只股票作为要买入的股票
# 获取当天的股票列表
scu = get_all_securities(date=context.current_dt).index.tolist()
# 选出在scu内的市值排名最小的前stocksnum只股票
q = query(valuation.code).filter(valuation.code.in_(scu)).order_by(valuation.market_cap.asc()).limit(g.stocksnum)
df = get_fundamentals(q)
# 选取股票代码并转为list
buylist=list(df['code'])

# 代码:若已持有的股票的市值已经不够小而不在要买入的股票中,则卖出这些股票。
# 对于每个当下持有的股票进行判断:现在是否已经不在buylist里,如果是则卖出
for stock in context.portfolio.positions:
if stock not in buylist: # 如果stock不在buylist
order_target(stock, 0) # 调整stock的持仓为0,即卖出

# 代码:买入要买入的股票,买入金额为可用资金的stocksnum分之一
# 将资金分成g.stocksnum份
position_per_stk = context.portfolio.cash / g.stocksnum
for stock in buylist:
order_value(stock, position_per_stk)
# 策略进行天数增加1
g.days = g.days + 1

回测结果

策略初步写完,把g.period设为13,g.stocksnum设为7,初始资金设为100000,频率为天,回测起止日期为20150101-20180627,然后进行回测,回测结果如下: 可见15年到16年该策略表现貌似不错,但随后17年至今则表现平平。

策略评价与建立模拟

在学习了如何编写策略后,我们将介绍下评价策略回测的指标,如何建立模拟交易,以及除回测之外还有哪些需要关注的方面。

策略回测指标

如下图,一个策略回测后会给出一些指标,可以在API文档:风险指标查看这些指标的公式及基本说明。下文将补充介绍下几个重要指标。

  • 策略收益。这是最基础的指标,衡量回测期间策略收益率的。
  • 基准收益。基准默认是沪深300指数,所以此指标是回测期间衡量基准收益率的。一般来说,基准收益代表市场整体的收益情况,所以如果策略收益长期低于基准收益,往往意味着策略是失败的。通过set_benchmark()这个API可以自定义基准。
  • 年化收益率。年化收益率是一个衡量策略盈利能力的重要指标,越大越好。刚刚讲的策略收益这个指标是和回测时间长短强相关的,比如一个普通策略运行10年肯定比优秀的策略跑半年策略收益高,但这样就不利于比较策略的盈利能力。因此,通过数学方法,把策略收益统一互相化归为一年时间的收益率,比如10年的变为平均每年的收益率,半年的变为以这半年盈利能力运行一年的收益率,如此一来,让策略盈利能力在比较时有了一个大致等同的时间标准。
  • 最大回撤率。最大回撤率是一个衡量策略风险的重要指标,越小越好。用人眼一般很容易找到是哪段,而且聚宽的回测图中也标出了,如下图。
  • 交易次数。交易次数其实是一个可以初步衡量策略回测结果是否可靠的指标,过少往往意味着回测结果不可靠。试想这样一种情况,别人给你推荐一个策略,策略进行了10年历史数据的回测,年化收益非常高,最大回撤非常小,你很高兴,但仔细一看,交易次数只有2次,此时,你愿意用真金白银去使用这个策略吗?你难免会想可能只是这2次操作运气好而已,这样的回测结果虽好但是不可信不可靠。其实这基于一个简单统计学思想,样本过少,则统计结果不可靠,所以足够多的交易次数才能让回测结果有说服力。目前,回测结果中不能直接看到交易次数了,可以通过回测结果页面的其他指标中的盈利次数与亏损次数相加得到,也可以通过回测结果图表下面的每日买卖大致看出,位置如下图。
  • Alpha(阿尔法)与Beta(贝塔)。在资本资产定价模型(CAPM)中,投资组合的收益被分为和市场系统风险相关与和市场系统风险无关的两部分,而Beta与Alpha这两个希腊字母则是该模型中的两个重要系数,分别代表这相关部分与无关部分。其实策略持有的股票可以看成一个投资组合,基准收益作为市场系统收益,Beta则是代表相关部分的策略收益相对市场波动的倍率,如Beta为2则代表市场涨1%,相关部分的策略收益波动涨大概2%(统计意义上并非实时精确),beta为负数代表与市场反向变动。而Alpha则代表独立于市场波动不受其影响的无关部分的策略收益,越大越好,所以如果策略年化收益为负但Alpha为正而且很大,说明策略有超过市场的盈利能力,不过策略整体盈利被与市场相关部分拉下来了。为了便于理解,Alpha与Beta的含义讲的很粗暴,建议数理基础不错的有志者有空去自学下Alpha与Beta的构造思路与过程。
  • 夏普比率(Sharpe Ratio)。代表所承担的单位风险所带来的收益,越大越好。夏普比率是在资本资产定价模型进一步发展得来的,不展开讲。

建立模拟交易

之前讲过回测是用历史数据模拟执行策略,模拟交易是用未来的的实际行情模拟执行策略,因此当策略完善的自以为差不多没什么问题时,建议建立一个模拟交易观察一段时间,当作进一步的检验。建立的模拟交易的方法很简单,点击回测结果界面,如下图,右上部红色模拟交易按钮,即可新建模拟交易。

建立模拟交易成功后,点击聚宽导航栏我的交易,可以看到创建的模拟交易,如下图。

点击右边的微信通知开关,将OFF调到ON,按照指示扫描二维码,绑定微信,就能微信接收交易信号了。当策略买卖操作,微信会收到信号提醒类似下图。自定义消息内容请看API send_message

未来函数

未来函数的前文讲过,即指策略利用了历史当时无法得到的信息,造成回测结果极大失真。未来函数排查方法一般是人工查看,重点看一切跟时间有关的地方,尤其注意各个API关于时间的默认处理方法。当然有时未来函数隐藏的很隐蔽,而更好但稍花时间的方法是用策略建立模拟交易,一般让模拟交易运行几天,多数未来函数问题都能被发现,因为模拟交易是不可能引入未来数据的,所以往往引入未来函数的策略无法成功运行模拟交易。

运行过慢

策略的运行效率也是需要关注的问题,尽管新手几乎不会遇到,但需要简单了解下,有个意识。有时策略比较复杂,计算量会很大,极端时可能会造成交易延迟,延误买股票的时机,分钟级策略尤其需要关注下耗时问题,而相关函数就是enable_profile()

用法就是把enable_profile()这行代码复制粘贴放到策略代码的第一行。然后你成功回测后可以在回测详情页面查看性能分析的结果,如下图,从而可以查看哪行代码耗时比较多,从而有目的性的去改进。

过拟合

过拟合(overfitting)常用于描述这样的情况。策略一般都有一些参数,如持股数量、交易频率等,选择不同的参数,固定的一份历史数据下,策略的回测结果好坏也不同,人们往往会选回测结果最好的参数作为策略的参数使用,但随后若换了一份历史数据(换一个时间段)做回测或随后用现实数据运行模拟或实盘,发现效果远不如之前的回测结果,此时很可能策略的参数过拟合了,或说之前选回测结果最好的参数这一行为使参数过拟合了。当参数多的时候,更容易发生。

过拟合的核心思想是,过度细致的解读样本数据,从而没有认识到本质的规律,从而使策略或系统失去了普适性,对原样本数据表现极其优异,但对非原样本数据外情况的有效性大大降低。

一个关于帮助理解过拟合的比喻是,老师拿一个试卷(样本数据)考学生(策略),学生成绩不理想,老师要教学生(调整参数),此时老师不是教学生学科原理,而是教学生背试卷的答案(过度拟合),当然结果会导致,当再考同一个试卷时学生肯定表现极度优异,但因为只背了答案而没理解原理,所以当换套题目或应用时学生就表现极差了。

策略失效

策略一般是有时效的。当你的策略十分完善,并且模拟效果理想,实盘效果也很理想,不要以为策略就会像印钞机一样一直赚钱,策略可以失效的,比如当策略运行中出现历史上罕见的情形时往往就要警惕了,比如最大回撤创历史新高,策略收益率不再增加甚至减少等。如何判断策略是否失效以及找出失效的原因并无通法,但策略失效的原因可能有以下几种,可供参考。

  • 策略生效的逻辑基础不再成立。比如策略的有效性是建立在涨跌停制度下的、或是建立在某行业不断成长前提下的、或是建立在全球某资源持续稀缺前提下的等,当这些制度或前提不再成立,如制度调整、新政发布、科技进步等,那么策略自然也就失效了。因此,理解策略有效的逻辑是十分重要的。
  • 操作资金量过大。更大的操作资金,会导致更大的冲击成本,即使买入时价更高、卖出时价更低,而当操作资金过大使市场流动性不足承载时,冲击成本会极大的变高,大大降低利润,甚至导致亏损。所以策略是有资金容量的,建议逐步增大策略操作资金量。
  • 市场上运行的相似策略过多。同类相似的策略都想赚市场上的同一份钱,然而这份钱是有限的,所以这些策略彼此间会竞争,导致策略赚钱变难,甚至完全失效赚不到钱。具体的表现可能是要买的股票买不到、想卖的股票卖不到理想价位等。因此,交易行业是非常注意保密且不适合分享的行业,而有志者则要注重培养自学能力。
  • 市场出现了寄生策略。当你的策略被发现市场中的有心人发现并足够程度的监测时,他可以写出一个针对你策略的策略,从而寄生在你的策略上,比如在比你买之前买入,在你卖后拉升股价后卖。这种针对你策略的寄生策略,往往会压缩你策略的盈利空间,使策略失效。

收益与风险的取舍

往往策略的收益能力与抗风险能力是互相制约不能兼顾的,两者之间如何取舍建议是,达到基本的收益能力后,极力追求低风险,理由是盈利水平往往可以通过增加资金量来提高。具体的讲就是,策略a是一个年化收益率300%,最大回撤率50%的策略,策略b是一个年化收益率30%,最大回撤率5%的策略,只要给策略b提供相当于策略a的10倍的资金量,两者盈利能力就是一样的,但很难让策略a有像策略b一样的抗风险能力。