海龟交易法则

海龟交易的创始人是七八十年代著名的期货投机商Richard Dennis,他相信优秀的交易员是后天培养而非天生的。他在1983年12月招聘了23名新人,昵称为海龟,并对这些交易员进行了一个趋势跟踪交易策略培训。随后给予每个新人100万美元的初始资金。经5年的运作,大部分“海龟”的业绩非常惊人,其中最好的业绩达到1.72亿美元。N年后海龟交易法则公布于世,我们才有幸看到曾名噪一时的海龟交易法则全貌。(画外音:能够开发出领先时代的交易模型,收益是多么诱人!)

海龟交易法则内容

海龟交易法则的原版书籍可以戳Turtle.

这里先画个简单的思维导图表示下海龟交易法则的基本框架:

下面一个个具体来说:

市场:

海龟们都是期货交易者,海龟们只选择有一定交易量流动性高的市场。这里我选择日指数数据CSI300.INDX,一方面是为了更好与基准比较,另一方面也是因为该标的可以不用担心流动性的问题。

头寸规模:

头寸规模是海龟交易系统最重要的部分之一。头寸规模是海龟交易系统最重要的部分之一。头寸规模是海龟交易系统最重要的部分之一。

海龟交易法则根据一个市场的绝对波动幅度来调整头寸规模,也就是将头寸的绝对波动幅度进行了标准化。比如,投资标的的价值波动性较强时,可以减少持有量,相反,当它的价值波动性较弱时候,可以增加持有量。总而言之,市场的波动性与头寸规模可以相互抵消。

海龟用一个被称为N的概念来表示某个市场根本的波动性,它表示单个交易日某个特定市场所造成的价格波动的平均范围,它同时也涵盖了开盘价的缺口。其实这个所谓的N,就是我们平常所熟悉的ATR,关于ATR的介绍,可以戳AVERAGE TRUE RANGE.

以下为计算公式:

TR=Max(H-L,H-PDC,PDC-L)
其中:
TR=真实波幅
H=当日最高价
L=当日最低价
PDC=前一日收盘价

N(即ATR)的计算公式如下(其实就是前面计算所得TR的20日移动平均):

N=(19*PDN+TR)/20
其中:
PDN=前一日N值
TR=当日的真实波动幅度

有了N之后,下一步可以计算绝对波动幅度,也就是用根本的市场价格波动性(用N值定义)表示的价值量波动性。

绝对波动幅度=N*合约每一点所代表的价值

最后,海龟按照我们所称的单位(Units)建立头寸。使1N代表帐户净值的1%。 波幅调整后的头寸单位为:

头寸规模单位=账户的1%/市场的绝对波动幅度

可以看出,使用N作为市场波动标准化的度量并以此作为开仓量及持仓量的依据,其背后的资金管理含义是,即便当日投资标的跌幅达到N(ATR)的水平,当日的损失都能控制在1%的总资产水平内。 即便当日投资标的跌幅达到N(ATR)的水平,当日的损失都能控制在1%的总资产水平内。 即便当日投资标的跌幅达到N(ATR)的水平,当日的损失都能控制在1%的总资产水平内。

以书中给的参考为例:

2003年3月份民用燃料油合约

日期最高价最低价收盘价真实波幅N值
2002/12/20.73750.72270.73590.01480.0134
2002/12/30.74470.73100.73890.01370.0134
2002/12/40.74200.71400.71620.02800.0141
-----------

根据12月4日的N值0.0141计算头寸规模如下:

N=0.0141
账户规模=1000000美元
每一点的价值=42000美元
头寸单位规模=0.011000000/0.014142000=16.88
舍去小数,得16份合约。

另外,海龟被限制在任何时间持仓的单位(Units)数目,在证券市场这种单一市场中,最多持仓的单位数设为4个单位,且海龟一般每周一计算一次N用于更新单位(Units)大小。

入市:

海龟的入市规则有两个系统,我们可以根据自己的意愿决定将净值配置在何种系统上。

系统一————以20日突破为基础的偏短线系统

突破定义为超过前20日的最高价或者最低价
海龟总是在日间突破发生时进行交易,而不会等到收盘或次日开盘
系统二————以55日突破为基础的较简单的长线系统

只要有一个信号显示价格超过了前55日的最高价和最低价就建立头寸。
由于我的回测周期较长,我选择了系统二,即以55日突破为基础的较简单的长线系统。

追踪

海龟交易系统不是一有突破信号就全仓介入,而是根据最新市场价格变化进行逐步建仓。

海龟在价格突破时只建立一个单位的头寸,在建立头寸后根据前面指令的实际成交价为基础以每突破0.5N的间隔进行加仓。

例如:

黄金:N=2.5
55日突破=310
增加的第一个单位310.00
第二个单位310.00+1/2个2.5即311.25
第三个单位311.25+1/2个2.5即312.50
第四个单位312.50+1/2个2.5即313.75

海龟被告知在接受入市信号时要非常连续,因为一年中的大部分利润可能仅仅来自两三次大赢利。

止损

对大多数人来说,始终抱着亏损的交易终究会反转的愿望比干脆退出亏损头寸并承认交易失败要容易得多。长期看,不会止损的交易是不会成功的。在你建立头寸之前,你需要预先确定退出的点位。如果市场波动触及你的价位,你就必须每一次毫无例外的退出。在这一立场上摇摆不定最终会导致灾难。(画外音:前段时间大家应该体会比较深刻吧。)

止损标准

海龟以头寸风险为基础设置止损,任何一笔交易不能出现2%以上的风险,因为价格波动1N表示1%的账户净值,容许风险为2%的最大止损就是价格波动2N,为了保证全部头寸的风险最小,如果另外增加了单位,前面单位的止损需提高0.5N。

例如:

原油:N=1.255 日突破=28.30
第一单位 28.30 25.90

第一单位 28.30 26.50
第二单位 28.90 26.50

第一单位 28.30 27.10
第二单位 28.90 27.10
第三单位 29.50 27.10

离市

艰难的离市

对于大多数交易员,海龟离市规则是系统法则中唯一最难的部分。等待10或20新低出现通常意味着眼睁睁瞅着20%,40%甚至100%的利润化为泡影。

海龟交易法则对于系统一系统二有着不同的离市标准:

系统一

离市对于多头头寸为10日最低价,对于空头头寸为10日最高价。如果价格波动于头寸背离至10日突破头寸中所有单位都会退出
系统二

离市对于多头头寸为20日最低价,对于空头头寸为20日最高价,如果价格波动与头寸背离至20日突破头寸中所有单位都会退出
海龟入市时一般不会设置离市止损价,但会在日间盯着价格,一旦价格穿过离市突破价,就开始打电话下离市
指令。

规则告一段落
以上就是海龟交易系统的全套法则内容,法则的每一部分及各部分的关联构成了一个交易系统,下面,我们一步步实现......

不考虑N仅根据突破信号构建的策略

TurtleBreakthroughStrategy Code:

public class TurtleBreakthroughStrategy implements IHStrategy {

  @Override
  public void init(IHInformer informer, IHInitializers initializers) {

    //设定开平仓的观察时间
    int openObserveTime = 55;
    int closeObserveTime = 20;

     //选取合适标的,如etf、个股或组合
    //initializers.instruments((universe) -> universe.add("600000.XSHG","600030.XSHG","600036.XSHG","601169.XSHG","601318.XSHG","600958.XSHG","601328.XSHG","601601.XSHG","601688.XSHG","601166.XSHG"));
    //initializers.instruments((universe) -> universe.add("600000.XSHG"));
    initializers.instruments((universe) -> universe.add("CSI300.INDX"));

    initializers.events().statistics((stats, info, trans) -> {

      stats.each((stat) -> {


        String stockId = stat.getInstrument().getOrderBookID();

        // 将观察时间内的最高价最低价储存进对应的数组
        double[] highPrice = stats.get(stockId).history(openObserveTime+1, HPeriod.Day).getHighPrice();
        double[] lowPrice = stats.get(stockId).history(closeObserveTime+1, HPeriod.Day).getLowPrice();

        //获得过去55天最高价的最大值,相关计算示例可见 http://commons.apache.org/proper/commons-math/userguide/stat.html 中1.2节示范代码
        DescriptiveStatistics forMax = new DescriptiveStatistics();
        for (int i = 0; i < highPrice.length-1; i++) {
            forMax.addValue(highPrice[i]);
        }
        double max = forMax.getMax();

        //获得过去10天最低价的最小值
        DescriptiveStatistics forMin = new DescriptiveStatistics();
        for (int i = 0; i < lowPrice.length-1; i++) {
            forMin.addValue(lowPrice[i]);
        }
        double min = forMin.getMin();

        //获取初始资金
        double initialCash = info.portfolio().getInitialCash();
        //每次使用初始资金的1/10进行交易
        double quantity = Math.floor(initialCash * 0.1 / stats.get(stockId).getLastPrice());
        double curPosition = info.position(stockId).getNonClosedTradeQuantity();

        //如当前价格向下突破最近20日最低价,则卖出上一次买入的持仓
        if (stats.get(stockId).getLastPrice() < min) {
            if (curPosition > 0) {
                trans.sell(stockId).shares(quantity).commit();
            }
        }

        //如当前价格突破最近55日最高价,则买入1/10初始资金所对应数量的持仓
        if (stats.get(stockId).getLastPrice() > max) {
            trans.buy(stockId).shares(quantity).commit();
        }

      });

    });
  }
}

回测结果:

恩,夏普比率0.66,比之前的MACD结果差些,回撤表现也不好。这个策略中,只是粗暴地将每次买入卖出的量控制在初始资金的1/10,当突破55日高点买入一份,最新价跌破20日最低价卖出一份。反正都是追涨杀跌,大同小异。

接下来,我把上面策略其他部分保持不变,仅引入头寸规模中N的概念来计算每次交易的单位(而不是仅简单以本金1/10来控制),看看会有什么效果。

引入N根据突破信号构建的策略

public class TurtleNStrategy implements IHStrategy {
    Core talibCore;
  @Override
  public void init(IHInformer informer, IHInitializers initializers) {

    talibCore = new Core();
    //设定开平仓的观察时间
    int openObserveTime = 55;
    int closeObserveTime = 20;
    int atrTime = 20;
    MInteger atrBegin = new MInteger();
    MInteger atrLength = new MInteger();

    String stockId = "CSI300.INDX";
    initializers.instruments((universe) -> universe.add(stockId));


    initializers.events().statistics((stats, info, trans) -> {


        // 将观察时间内的最高价最低价储存进对应的数组
        double[] highPrice = stats.get(stockId).history(openObserveTime+1, HPeriod.Day).getHighPrice();
        double[] lowPriceForAtr = stats.get(stockId).history(openObserveTime+1, HPeriod.Day).getLowPrice();
        double[] lowPriceForExtremem = stats.get(stockId).history(closeObserveTime+1, HPeriod.Day).getLowPrice();
        double[] closePrice = stats.get(stockId).history(openObserveTime+2, HPeriod.Day).getClosingPrice();

        double closePriceForAtr[] = new double[closePrice.length-1];
        for (int i = 0; i < closePrice.length-1; i++) {
            closePriceForAtr[i] = closePrice[i];
        }



        double[] atrArray = new double[openObserveTime];

        RetCode retCode = talibCore.atr(0, openObserveTime-1, highPrice, lowPriceForAtr, closePriceForAtr, atrTime, atrBegin, atrLength, atrArray);
        //获得过去55天最高价的最大值,相关计算示例可见 http://commons.apache.org/proper/commons-math/userguide/stat.html 中1.2节示范代码
        DescriptiveStatistics forMax = new DescriptiveStatistics();
        for (int i = 0; i < highPrice.length-1; i++) {
            forMax.addValue(highPrice[i]);
        }
        double max = forMax.getMax();


        //获得过去10天最低价的最小值
        DescriptiveStatistics forMin = new DescriptiveStatistics();
        for (int i = 0; i < lowPriceForExtremem.length-1; i++) {
            forMin.addValue(lowPriceForExtremem[i]);
        }
        double min = forMin.getMin();

        double portfolioValue = info.portfolio().getPortfolioValue();
        double atr = atrArray[atrLength.value-1];
        informer.info(atr);
        double unit = Math.floor(portfolioValue * .01 / atr);
        //informer.info(unit);

        //获取初始资金
        double initialCash = info.portfolio().getInitialCash();
        //每次交易一个单位
        double quantity = unit;
        double curPosition = info.position(stockId).getNonClosedTradeQuantity();



        //informer.info("lastPrice is " + stats.get(stockId).getLastPrice());
        //informer.info("max is" + max);
        //如当前价格向下突破最近20日最低价,则卖出上一次买入的持仓
        if (stats.get(stockId).getLastPrice() < min) {
            if (curPosition > 0) {
                trans.sell(stockId).shares(quantity).commit();
            }
        }

        //如当前价格突破最近55日最高价,则买入一个单位
        if (stats.get(stockId).getLastPrice() > max) {
            trans.buy(stockId).shares(quantity).commit();
        }

    });
  }
}

回测结果:

额,夏普比率终于过1了,与之前没有引入N的策略相比,表现有了很大提高,最大回撤也由47降低到28,这说明,海龟体系中引入的N,即根据过去价格波动幅度而调节开平仓的单位,是经受得住数据考验的。

海龟策略完全体系构建

最后的最后,放上海龟体系的完全版,Code里面包含了上一节中提到的所有环节,注意本海龟策略是基于体系二的,感兴趣的朋友可以尝试构建体系一。

TurtleOriginalStrategy Code:


public class TurtleOriginalStrategy implements IHStrategy {
  Core talibCore;

//定义全局变量
  static int tradedayNum = 0;
  static double unit = 0;
  static double atr = 0;
  static String tradingSignal = "start";
  static String preTradingSignal = "";
  static int units_hold_max = 4;
  static int units_hold = 0;
  static double quantity = 0;
  static double max_add = 0;
  static double firstOpenPrice = 0;

  //计算最大最小值
  public double[] getExtremem(double[] arrayHighPriceResult, double[] arrayLowPriceResult) {
      DescriptiveStatistics forMax = new DescriptiveStatistics();
      for (int i = 0; i < arrayHighPriceResult.length-1; i++) {
          forMax.addValue(arrayHighPriceResult[i]);
      }
      double maxResult = forMax.getMax();

      DescriptiveStatistics forMin = new DescriptiveStatistics();
      for (int i = 0; i < arrayLowPriceResult.length-1; i++) {
          forMin.addValue(arrayLowPriceResult[i]);
      }
      double minResult = forMin.getMin();

      double[] forExtremum = new double[2];
      forExtremum[0] = maxResult;
      forExtremum[1] = minResult;
      return forExtremum;
  }
  //计算Atr以及单位
  public double[] getAtrAndUnit(double[] atrArrayResult, MInteger atrLengthResult, double portfolioValueResult) {
      double atr = atrArrayResult[atrLengthResult.value-1];
      double unit = Math.floor(portfolioValueResult * .01 / atr);
      double[] atrAndUnit = new double[2];
      atrAndUnit[0] = atr;
      atrAndUnit[1] = unit;
      return atrAndUnit;
  }
  //计算止损线价位
  public double getStopPrice(double firstOpenPriceResult, int units_hold_result, double atrResult) {
      double stopPrice =  firstOpenPriceResult - 2*atrResult + (units_hold_result-1)*0.5*atrResult;
      return stopPrice;
  }



  @Override
  public void init(IHInformer informer, IHInitializers initializers) {

    talibCore = new Core();


    int openObserveTime = 55;
    int closeObserveTime = 20;
    int atrTime = 20;
    MInteger atrBegin = new MInteger();
    MInteger atrLength = new MInteger();


    String stockId = "CSI300.INDX";
    initializers.instruments((universe) -> universe.add(stockId));


    initializers.events().statistics((stats, info, trans) -> {

        //获取组合总价值,包含市场价值与剩余资金
        double portfolioValue = info.portfolio().getPortfolioValue();


        double[] highPrice = stats.get(stockId).history(openObserveTime+1, HPeriod.Day).getHighPrice();
        double[] lowPriceForAtr = stats.get(stockId).history(openObserveTime+1, HPeriod.Day).getLowPrice();
        double[] lowPriceForExtremem = stats.get(stockId).history(closeObserveTime+1, HPeriod.Day).getLowPrice();
        double[] closePrice = stats.get(stockId).history(openObserveTime+2, HPeriod.Day).getClosingPrice();

        double closePriceForAtr[] = new double[closePrice.length-1];
        for (int i = 0; i < closePrice.length-1; i++) {
            closePriceForAtr[i] = closePrice[i];
        }


        double[] atrArray = new double[openObserveTime];
        //Talib计算N即ATR
        RetCode retCode = talibCore.atr(0, openObserveTime-1, highPrice, lowPriceForAtr, closePriceForAtr, atrTime, atrBegin, atrLength, atrArray);


        double max = getExtremem(highPrice, lowPriceForExtremem)[0];
        double min = getExtremem(highPrice, lowPriceForExtremem)[1];


        double atr = atrArray[atrLength.value-1];

        if (tradingSignal != "start") {
            if (units_hold != 0) {
            max_add += 0.5 * getAtrAndUnit(atrArray, atrLength, portfolioValue)[0];
            }
        } else {
            max_add = stats.get(stockId).getLastPrice();
        }

        informer.info(units_hold);

        double curPosition = info.position(stockId).getNonClosedTradeQuantity();
        double availableCash = info.portfolio().getAvailableCash();
        double marketValue = info.portfolio().getMarketValue();


        if (curPosition > 0 & stats.get(stockId).getLastPrice() < getStopPrice(firstOpenPrice, units_hold, atr)) {
            tradingSignal = "stop";
        } else {
            if (curPosition > 0 & stats.get(stockId).getLastPrice() < min) {
                tradingSignal = "exit";
            } else {
                if (stats.get(stockId).getLastPrice() > max_add & units_hold != 0 & units_hold < units_hold_max & availableCash > stats.get(stockId).getLastPrice()*unit) {
                    tradingSignal = "entry_add";
                } else {
                    if (stats.get(stockId).getLastPrice() > max & units_hold == 0) {
                        max_add = stats.get(stockId).getLastPrice();
                        tradingSignal = "entry";
                    }
                }
            }
        }

        //informer.info(tradingSignal);

        atr = getAtrAndUnit(atrArray, atrLength, portfolioValue)[0];
        if (tradedayNum % 5 == 0) {
            unit = getAtrAndUnit(atrArray, atrLength, portfolioValue)[1];
        }
        tradedayNum += 1;

        double quantity = unit;


        if (tradingSignal != preTradingSignal | (units_hold < units_hold_max & units_hold > 1) | tradingSignal == "stop") {


            if (tradingSignal == "entry") {
                quantity = unit;
                if (availableCash > stats.get(stockId).getLastPrice()*quantity) {
                    trans.buy(stockId).shares(quantity).commit();
                    firstOpenPrice = stats.get(stockId).getLastPrice();
                    units_hold = 1;
                    informer.info("entrybuy" + quantity);
                }
            }
            if (tradingSignal == "entry_add") {
                quantity = unit;
                trans.buy(stockId).shares(quantity).commit();
                units_hold += 1;
                informer.info("entry_addbuy" + quantity);
            }


            if (tradingSignal == "stop") {
                if (/*curPosition marketValue*/ units_hold > 0) {
                    trans.sell(stockId).shares(quantity).commit();
                    units_hold -= 1;
                    informer.info("stop" + quantity);
                }
            }
            if (tradingSignal == "exit") {
                if (curPosition > 0) {
                    trans.sell(stockId).shares(curPosition).commit();
                    units_hold = 0;
                    informer.info("exitsell" + curPosition);
                }
            }

        }

        preTradingSignal = tradingSignal;

    });

  }
}

回测结果:

opps吐血,虽然代码蛮长运行还是挺快哒,终于搞定,完整版的策略包括了按照海龟中的进场、追踪、止损、离场所有细节,然而并没有什么卵用,结果没有想象那么漂亮,不过继续在之前的基础上把最大回撤降低到22,我想这还是因为止损大法好的原因,宽慰一点点。

ref:

https://zrainx.gitbooks.io/study-of-rq/content/qu-shi-ce-lve-xiao-shi-niu-dao-ff0c-hai-gui-jiao-yi-ti-xi-de-gou-jian.html

0 0 投票数
Article Rating
订阅评论
提醒
guest
1 评论
最旧
最新 最多投票
内联反馈
查看所有评论
1
0
希望看到您的想法,请您发表评论x