Raquant-Java平台介绍

策略实现

在Raquant-Java平台,所有的策略对象都有一个公共的父类——Strategy,它是一个抽象类,有三个基本方法需要您去实现:

class MyStrategy extends Strategy { 
     /**
      * 在策略被调用时首先被执行,用来完成一些初始化配置;
      * 当策略需要以断点-增量的方式持续运行时,init()方法只会被执行一次。
      */
     public void init(BackTestContext context) throws Exception {            
     }
     /**
      * 在init()方法之后执行;
      * 当策略需要以断点-增量的方式持续运行时,prepare()方法每次被唤醒继续运行时都会被再次调用。
      */
     public void prepare(BackTestContext context) throws Exception {                  
     }
     /**
      * 每个最小测试单位都会被执行一次。如:当您先择按日测试时,它将逐日被调用。
      */
     public void handleData(BackTestContext context, BarData data) throws Exception { 
     }
}

在您新建一个策略的时候,系统会自动为您创建好以上框架,您需要在init()和prepare()方法中做好相应的准备工作,在handleData()里实现您的交易算法即可。当然,如果您并不需要在每个最小测试单位里都执行您的算法,你还可以指定周期执行,详情参见“Schedule API”章节。

策略回测

  • 继承>Strategy,实现您自己的策略;

  • 选定一个回测时间区间及最小回测单位,开始回测;

  • 引擎根据您选择的股票池和日期,取得股票数据然后每一个时间片里调用您的handleData()函数或根据您制定的Schedule执行特定任务;

  • 在回测过程中您可以通过API获得现金、持仓情况和股票在上一天或者分钟的数据,而且有大量内置指标可供您直接使用;

  • 在history()方法中,您还可以获取任意多天的历史数据;

  • 您可随时获取您当前的持仓明细和所有历史订单;

  • 您可以调用record()函数记录某些数据,它会用图表形式来辅助您做判断;

  • 您可以在任何时候调用log.info/warn/error函数来打印一些日志;

  • 回测过程中我们会画出您的收益和基准(参见benchMark )收益的曲线,记录每日持仓每日交易和一系列风险数据。

策略模拟

除了在线回测,Raquant还提供对策略的模拟,一条处于模拟状态的策略,会在每一个新的交易日被后台唤醒、执行。显然,不同于回测,策略模拟的执行是断点-增量型的,在策略运行的每一个断点都会存档,以便下一次被唤醒时能无缝的继续执行。需要注意的是,为了优化存储和执行效率,并不是所有被定义的变量和对象都会被存档,详细的内容您可以参考下一章节“API文档”中的相关介绍。

选股策略

选股策略会按照特定的选股条件,列出满足条件的股票集,并按指定的规则排序,它实际上是一个选股器,您在一些财经门户网站上可以看到类似的功能,比如创历史新高的股票,10日内涨幅最大的股票等等——那么为什么要使用Raquant平台来重复实现这些功能呢?答案是,在这里您可以自由订制选股策略,也许您需要的仅是90日内创新高的股票,也许您需要的是13日涨幅最大的股票,也许您需要的是90日内创新高且13日涨幅最大的股票。在Raquant选股,已经内置了一些选股的框架,您只需要改几个数字就可以订制您自己的选股器。当然,您也可以不用管那些内置的选股策略,创造属于您自己的选股策略。

数据

我们拥有2012至今的A股数据。

每个交易日的16:00更新完毕最新数据。

复权、停牌

您获取的数据已经是前复权的数据。停牌的数据自动用停牌前的数据来填充,但成交量为0。

交易税费

默认万三手续费,千一印花税。

滑点

因于服务器延时或其它因素造成成交价格与挂单价格不一致。在回测和模拟交易时可设定该值。本着谨慎的风格,在测试策略时,您买入的价格会根据滑点的设置被提高,即高于当前价格;您的卖出价格相应的会低于当前市场价格。

风险指标

  • 风险指标数据有利于您对策略进行一个客观的评价。

  • 注意: 无论是回测还是模拟, 所有风险指标(alpha/beta/sharpe/max_drawdown等指标)都只会每天更新一次, 也只根据每天收盘后的收益计算, 并不考虑每天盘中的收益情况。

  • 例外:

  • 分钟和TICK模拟盘每分钟会更新策略收益和基准收益;

  • 按天模拟盘每天开盘后和收盘后会更新策略收益和基准收益;

  • 那么可能会造成这种现象: 模拟时收益曲线中有回撤, 但是 max_drawdown 可能为0。

  • maxDropDown(最大回撤)

  • 描述策略可能出现的最糟糕的情况,最极端可能的亏损情况。

  • Max Drawdown=Max(Px−Py)/Px;

    Px,Py=策略某日股票和现金的总价值,y>x。

  • total returns(策略收益)

  • Total Returns=(Pend−Pstart)/Pstart∗100%;

    Pend=策略最终股票和现金的总价值;

    Pstart=策略开始股票和现金的总价值。

  • benchmark returns(基准收益)

  • Benchmark Returns=(Mend−Mstart)/Mstart∗100%;

    Mend=基准最终价值;

    Mstart=基准开始价值。

  • Sharp(夏普比率)

  • Sharpe Ratio=(Rp−Rf)/σp;

    Rp=策略年化收益率;

    Rf=无风险利率(默认0.04);

    σp=策略收益波动率。

  • VolatilityDownside Risk(下行波动率)

  • 策略收益下行波动率。和普通收益波动率相比,下行标准差区分了好的和坏的波动。

  • rp=策略每日收益率 rp=策略每日收益率 rpi¯=策略至第i日平均收益率=1i∑j=1irj rpi=策略至第i日平均收益率=1i∑j=1irj n=策略执行天数 n=策略执行天数 m=策略收益低于rpi天数 m=策略收益低于rpi天数 f(t)=1 if rp<rpi¯ f(t)=1 if rp<rpi f(t)=0 if rp>=rpi。

  • Alpha(阿尔法)

  • 投资中面临着系统性风险(即Beta)和非系统性风险(即Alpha),Alpha是投资者获得与市场波动无关的回报。比如投资者获得了15%的回报,其基准获得了10%的回报,那么Alpha或者价值增值的部分就是5%。

  • Beta=βp=Cov(Dp,Dm)/Var(Dm);

    Alpha=α=Rp−[Rf+βp(Rm−Rf)];

    Rp=策略年化收益率;

    Rm=基准年化收益率;

    Rf=无风险利率(默认0.04);

    βp=策略beta值。

  • Alpha值         解释

  • α>0     策略相对于风险,获得了超额收益;

  • α=0     策略相对于风险,获得了适当收益;

  • α<0     策略相对于风险,获得了较少收益。

  • Beta(贝塔)

  • 表示投资的系统性风险,反映了策略对大盘变化的敏感性。例如一个策略的Beta为1.5,则大盘涨1%的时候,策略可能涨1.5%,反之亦然;如果一个策略的Beta为-1.5,说明大盘涨1%的时候,策略可能跌1.5%,反之亦然。

  • Beta=βp=Cov(Dp,Dm)/Var(Dm);

    Dp=策略每日收益;

    Dp=策略每日收益;

    Dm=基准每日收益;

    Dm=基准每日收益;

    Cov(Dp,Dm)=策略每日收益与基准每日收益的协方差;

    Cov(Dp,Dm)=策略每日收益与基准每日收益的协方差;

    Var(Dm)=基准每日收益的方差。

  • Beta值  解释

  • β<0     投资组合和基准的走向通常反方向,如空头头寸类;

  • β=0     投资组合和基准的走向没有相关性,如固定收益类;

  • 0<β<1   投资组合和基准的走向相同,但是比基准的移动幅度更小;

  • β=1     投资组合和基准的走向相同,并且和基准的移动幅度贴近;

  • β>1     投资组合和基准的走向相同,但是比基准的移动幅度更大;

一个简单的策略

class MyStrategy extends Strategy {
      public void init(BackTestContext context) throws Exception {
          
      }
      public void prepare(BackTestContext context) throws Exception {
    
      }
      public void handleData(BackTestContext context, BarData data) throws Exception {
          log.info("current close price:" + data.get("sha-601318").close);
          record("close", data.get("sha-601318").close);
    }
}

它实现了在每一个时间片打印我们加入股票的收盘价, 同时通过record方法绘制出了close的价格曲线。

这是非常简单的策略,它只是将一支股票的价格以文字和图片的形式表现出来。

添加交易逻辑

class MyStrategy extends Strategy {
    public void init(BackTestContext context) {

    }
    public void prepare(BackTestContext context) throws Exception {
      
    }
    public void handleData(BackTestContext context, BarData data) throws Exception {
        if (context.portfolio.hasPosition("sha-601318")) {
            orderValue("sha-601318", 0, "sell all");
        } else {
            orderValue("sha-601318", 500000, "buy");
        }
    }
}

这个策略判断当没有持仓的时候全仓买入"sha-60131",如果有股票持仓则全部卖出,这个策略非常简单,它选择在当天买入第二天卖出。下面我们来看一个更复杂的例子。

更复杂的例子

class MyStrategy extends Strategy {
    public void init(BackTestContext context) {

    }
    public void prepare(BackTestContext context) throws Exception {
     
    }
    public void handleData(BackTestContext context, BarData data) throws Exception {
        if (context.portfolio.hasPosition("sha-601318")) {
            orderValue("sha-601318", 0, "Sell all");
        } else if (data.get("sha-601318").close > data.get("sha-601318").mavg(5, "close")) {
            orderValue("sha-601318", 500000, "buy");
        }
    }
}

上面的策略选择在上一日的收盘价超出前五日的均价时买入,次日卖出。

API 文档

基本方法和数据结构

init()

Init()是Strategy的一个抽象方法,它被声明为:

public abstract void init(BackTestContext context) throws Exception;

您可以根据需要自己实现这个方法,比如:

public void init(BackTestContext context) throws Exception {
    context.universe.add("sha-601318");
    context.universe.add("sha-600036");
    context.universe.add("sza-000878");
    context.universe.add("sha-600503");
    context.universe.add("sza-002278");
    context.universe.add("sza-002364");
}

在init()方法里可以选择自己想要操作的股票,加进universe这个List里;也可以设置一些参数,如基准线(比如设置上证指数、hs300为基准)、手续费等等。在context中还可以为自己的投资组合设定基准,下面的代码则设置了中国平安(601318)为参考基准。

context.bench_mark="sha-601318";

注意,在策略用于模拟时,init仅在策略首次运行时被调用,当新的行情数据到来时,系统会加载之前存档的context,并继续运行。所以在init()方法里,尽量只对context进行操作,即使您暂时不打算将该策略用作模拟——以免日后改写或使用该策略时造成困扰。您额外需要持久化的变量或对象,可以通过context.memo存储和加载。

handleData()

它是一个抽象方法,您可以定义它的具体实现方法:

public abstract void handleData(BackTestContext context, BarData piece) throws Exception;

注意,这个方法将在每个时间片被调用;对于无需每个周期执行的任务,可以通过制订任务计划完成,具体实现方法请参考Schedule API。

BarData

从handleData方法传入的BarData可获取当前时间片的数据:

CandleData candle=data.get("sha-601318");

上面代码获得一个CandleData对象。

CandleData

每个CandleData包含以下数据:

open当前时间片开盘价;

close当前时间片收盘价;

high当前时间片最高价;

low当前时间片最低价;

dt当前时间片日期;

volume当前时间片成交量;

volume_money当前时间片成交金额。

您可以通过下面的方法获得一支股票当前的行情:

public void handleData(BackTestContext context, BarData data) throws Exception {        
        CandleData candle = data.get("sha-601318");
        double open = candle.open;
        double high = candle.high;
        double low = candle.low;
        double close = candle.close;
        double volume = candle.volume;
        double volumeMoney = candle.volume_money;
   }

DataFrame

DataFrame实际上就是一个数据表格,我们可以像查询表格一样获取它的数据。DataFrame的行数是固定的,在构造一个DataFrame的时候必需给出它各行的行索引, 而行索引的数量决定了数据表格的行数

List<String> stockList = new ArrayList<String>();
stockList.add("sha-601318");
stockList.add("sha-601988");
DataFrame data = new DataFrame(stockList);

DataFrame的列数是可以变化的,比如上面代码构造了一个DataFrame对象,它由两行构成,行名分别是两支股票的名称;其各列就可以存放同一支股票的各类数据,比如多项指标等。

作为一个数据表格,相应地,它提供了以下方法可以查询数据:

// 取数据表格的一列数据, name 为列名。
public List<Comparable> getColumn( String name );
// 取第idx行的行名。
public String getRowKey( int idx );
// 取所有的行名。
public Set<String> getRowKeys();
// 取所有的列名。
public Set<String> getColumnKeys();
// 取DateFrame的前n行。
public DataFrame headRows(int n);
    
public int rowSize();
public int columnSize();

// 选中一列进行排序,当desc为true时降序排列,反之则升序排列。
public DataFrame sortBy(String name, boolean desc);

下面是一个使用DataFrame的示例:

 public void handleData(BackTestContext context, BarData data) throws Exception {  
        // 构造第一个列,用股票代码作为各行的索引名称。
        List<String> rowNames = new ArrayList<String>();
        rowNames.add("sha-601318");
        rowNames.add("sha-601988");
        rowNames.add("sha-601389");
        // 构造第二个列,存入对应股票的收盘价
        List<Float> prices = new ArrayList<Float>();
        prices.add(data.get("sha-601318").close);
        prices.add(data.get("sha-601988").close);
        prices.add(data.get("sha-601389").close);
        
        // 构建一个DataFrame,确定各行的名称。
        DataFrame df = new DataFrame(rowNames);
        // 增加一列,内容为各支股票对应的收盘价。
        df.addColumn("close", prices);
        
        /*
         * 至此,一个三行两列的DataFrame被构造成功。
         * 接下来我们看看它有哪些方法可以使用。
         */
        
        // 把刚才存入的prices又取了出来,注意,这里得用List<Float>转一下类型。
        List<Float> gotPrices = (List<Float>) df.getColumn("close");  
        
        // 将表格根据收盘价排序。
        df.sortBy("close");        
        // 我们只关心价格排名最高的两支股票
        DataFrame topOfdf = df.top(2);        
        // 看看排名最高的是哪支股票
        String topOne = topOfdf.getRowName(0);
        
        // 或许我们可以直接这样写:
        topOne = df.sortBy("close").top(2).getRowName(0);        
    }

context

context.universe

universe是一个扩展的List<String>类型,所以它支持所有List类提供的方法,比如通过:

context.universe.add("sha-601318");
context.universe.remove("sha-601318");

或者

context.universe.addAll({"sha-600001","sha-601318"});
context.universe.addAll(findByGroup('all-stocks'));
context.universe.clear();

上述方法修改自己想操作的证券列表。(注:镭矿系统里的沪市代码都带有前缀“sha-”,深市代码里都带有前缀“sza-”。)

同时,它还支持一些扩展的方法让我们可以更方法的使用:

// 加入所有股票 
public StockList addAll() throws Exception 
// 加入指定分类股票集
public StockList addGroup(String groupIdOrName) throws Exception
// 删除指定股票集
public StockList removeGroup(String groupIdOrName) throws Exception
// 删除指定的股票名单
public StockList filter(List<String> list)
// 只保留指定的股票集
public StockList keepOnly(String groupIdOrName) throws Exception

另外,context.universe是默认被持久化的,您在写模拟交易策略时不用担心该策略在新的交易日被唤醒时会丢失上次的股票集信息。

context.benchMark

conext.benchMark="sha-601988";

在init方法里面可以改变测试基准证券

context.commision

它是一个实现了TradeCommissionDecider接口的实例,系统默认买入收取0.00003, 卖出收取0.0013的佣金。您可以通过实现这个接口以满足你个性化的需求。

public interface TradeCommissionDecider {
    // 根据买入量评估佣金
    public double evaluateBuyCommission(double cost);
    // 根据卖出量评估佣金
    public double evaluateSellCommission(double cost);
}

在init()方法里面可以通过设置context.commition来使用您指定的佣金计算器。  

public void init(BackTestContext context) throws Exception {
    context.setCommission(new FloatingCommission(0.00003f, 0.0013f));
}

context.slippage

它是一个实现了SlippageDecider接口的实例,系统默认会有0.3%的滑点,您可以实现这个接口以满足您个性化的需求。每当买入或卖出时,下面接口中的两个方法会相应的修改当前的价格以模拟滑点的产生。

public interface SlippageDecider {
    public float modifySellPrice(float currentPrice);
    public float modifyBuyPrice(float currentPrice);
}

在init()方法里面可以设置滑点:

public void init(BackTestContext context) throws Exception {
    context.setSlippage(new Slippage().setFixedPercent(0.003f));
}

context.portfolio

context.portfolio用来管理股票的持仓,它提供了以下方法:

public float     getCash();                           // 获取当前的现金数量。
public float     getAsset();                          // 获取当前持仓股票的价值。
public float     getAsset(String security);           // 获取当前指定持仓股票的价值。
public float     getAverageCost(String security);     // 获取指定持仓股票的持仓均价。
public float     getGains(String security);           // 获取指定持股股票的当前浮动收益。
public float     getTotalCapital();                   // 获取当前总资产的价值,即现金+持仓价值。
public int       getAmount(String security);          // 获取当前某支股票的持仓数量。
public boolean   hasPosition(String security);        // 判断当前是否持有某支股票。
public List<Position> getHoldForDays(int days);       // 获取最后一次买入该股票已有"days"天的所有股票。
public Stream<Position> positionStream();             // 获取一个Position的Stream,为Lambda编程提供。

context.memo

Memo是一个可供您持久化私有数据的容器。在模拟时,系统会默认保存context中的一些重要变量,但可能无法满足您的全部需求,这时,您可以使用Memo来维护这些数据。一个Memo下面可以有多个Notes,而每一个Notes实际上是一个扩展的List,它可以存储多条同类型的数据。

class MyStrategy extends Strategy {
    // 在声明Notes的时候要显式的给出它要存储的对象的类型,如Notes<Date>;
    public Notes<Date> rank = null;    
    public void init(BackTestContext context) throws Exception {
        // 在init中创建一个新的Notes,当该策略作为模拟运行时,也能保证只会被创建一次。
        context.memo.newNotes("myList");        
    }
    public void prepare(BackTestContext context) throws Exception {
        // 在prepare中获取一个Notes的引用,写在这里是为了让策略作为模拟运行时,每
        // 次从断点继续运行时都能正确的初始化rank,若写在init中则无此效果。
        rank = (Notes<Date>) context.memo.getNotes("myList");
    }
    public void handleData(BackTestContext context, BarData data) throws Exception {
        // rank已经初始化好,您对它所作的任何更改都会被保存。
        rank.add(context.now);
        // 当然,像下面这样使用是跟上面的方式等价的,您可以选择适合您的风格。
        ((Notes<Date>) context.memo.getNotes("myList")).add(context.now);
    }
}

context的临时变量

context.now当前BarData的时间标签,您可以在handleData()或Schedule里面使用它获取当前数据的时间标签,它在策略的运行过程中会实时更新;请不要在init()、prepare()或其它方法中使用它。

交易API

order

public boolean order(String stock, int num) throws Exception;
public boolean order(String stock, int num, String comment) throws Exception;

num为正则买入一定数量num的证券。

num为负则卖出一定数量num的证券。

orderValue

public boolean orderValue(String stock, double value) throws Exception;
public boolean orderValue(String stock, double value, String comment) throws Exception;

num为正则买入价值金额为value的证券。

num为负则卖出价值金额为value的证券。

orderPercent

public boolean orderPercent(String stock, float percent);
public boolean orderPercent(String stock, float percent, String comment);

num为正则买入一定百分比num的证券。

num为负则卖出一定百分比num的证券。

orderTarget

public boolean orderTarget(String stock, int num) throws Exception;
public boolean orderTarget(String stock, int num, String comment) throws Exception;

下单使某只股票到达指定数量(num)。

orderTargetPercent

public boolean orderTargetValue(String stock, double value) throws Exception;
public boolean orderTargetValue(String stock, double value, String comment) throws Exception;

下单使某只股票到达指定仓位。

orderTargetValue

public boolean orderTargetPercent(String stock, float percent) throws Exception;
public boolean orderTargetPercent(String stock, float percent, String comment) throws Exception;

下单使某只股票到达指定金额。

Schedule API

并不是所有任务都需要在handleData()中执行——事实上,如果在分钟级别上进行回测,那就几乎没有多少任务非得在handleData()中执行了。handleData()中的代码会在每个回测时间单位中被执行一遍,但是,例如15日均线只需要每天计算一次,每分钟都执行这项计算不仅没有必要,而且这样计算的结果也是不正确的。或者,在需要使用季度财务数据的时候,也没必要每天都去查看一下。总之,像我们安排日常的工作一样,策略中也需要制定任务计划,在需要的时间做适当做的事情。

在RaQuant中,这种计划被定义为Schedule,每个Schedule由两项基本要素所定义:Rule和Task其中Rule是一项规则,它在每个指定的时间点会被触发;而Task是具体要执行的任务。Rule并不知道自己将被用来触发什么任务,Task也不知道自己会在什么时间执行,这些事情要由Schedule来实现。

一个完整的Rule由一条DateRule和一条TimeRule组成。DateRule规定了任务执行的日期,而TimeRule指定了任务执行的具体时间点。构建它们的方法都是通过设定周期单位,并指定偏移量。

DateRule

DateRule的执行周期可使用三个单位:DAY、WEEK、MONTH,其中DAY比较特殊,它不允许设置偏移量。对于WEEK和MONTH,可以指定偏移,设置为每周期的第几个交易日执行,如:

DateRule dRule = DateRule.month(1);

创建了一个日规则,指定要在每个月的第一个交易日执行。同样,也支持倒数的方式,设置为倒数第几个交易日执行,例如:

DateRule dRule = DateRule.month(-1);

这个规则指定了要在每月的最后一个交易日执行。注意,越界的偏移量将被修整,比如:

DateRule dRule = DateRule.month(100);
DateRule dRule = DateRule.month(-100);

也分别会在每个月的最后一个和第一个交易日触发,但是,好像并没有非得这样做的理由;提供这种修整的方法只是为了容错而已。

返回规则

创建方法

publicstatic DateRule

everyDay()

创建一个每日执行的规则。

publicstatic DateRule

week( int daysOffset )

创建一个每周执行的规则,具体执行日由偏移值确定。

publicstatic DateRule

month( int daysOffset)

创建一个每月执行的规则,具体执行日由偏移值确定。

TimeRule

TimeRule没有执行周期单位,但是它有两个参考点,marketOpen和marketClose,可以指定开盘后的第几个小时第几分钟执行;或收盘前的第几个小时第几分钟执行。注意,这里给出了两个方向,所以在参数中使用负数是非法的。

返回规则

创建方法

publicstatic TimeRule

marketOpen( int hours, intminutes)

publicstatic TimeRule

marketClose( int hours,intminutes)

Rule

制定好了DateRule和TimeRule,就可以直接创建Rule了:

Rule rule = Rule.createRules(DateRule.month(1), TimeRule.marketClose(1, 1));

上面定义了一个在每月的第一天,收盘前一个小时零1分钟执行的规则。注意,当回测周期粒度比实际定义的要大时,更小的单位上的值并不会被参考。比如,按日回测时,整个TimeRule都不会被检查,无论它设置的规则是怎样的。

创建Task

创建Task很简单,只需要实现Task类的doTask()方法就可以了,doTask方法的参数与handleData()是一致的:

Task task = new Task() {
    public boolean doTask(BackTestContext context, BarData piece) {
        log.info("Doing task 1.");
        return true;
    }
};

创建Schedule

绑定一个规则和一个任务,就可以生成一条新的Schedule,同一个任务可以有不同的执行规则,当然,同一规则下面也可能有多个任务需要执行:

Rule rule1 = Rule.createRules(DateRule.month(1), TimeRule.marketClose(1, 1));
Rule rule2 = Rule.createRules(DateRule.month(-1), TimeRule.marketClose(1, 1));
Rule rule3 = Rule.createRules(DateRule.week(-1), TimeRule.marketOpen(1, 0));         

addTask(rule1, task1);
addTask(rule2, task2);
addTask(rule3, task2);

Schdule应用实例

class TestUserFactor extends Strategy {
    Task task1 = new Task() {
        public boolean doTask(BackTestContext context, BarData piece) {
            log.info("Doing task 1 in " + context.now);
            return true;
        }
    };
    Task task2 = new Task() {
        public boolean doTask(BackTestContext context, BarData piece) {
            log.info("Doing task 2 in " + context.now);
            return true;
        }
    };
    public void init(BackTestContext context) throws Exception {
        context.universe.add("sha-601318");       
    }
    public void prepare(BackTestContext context) throws Exception {
        // 应为Schedule没有被持久化,为了使代码兼容模拟的需要,在prepare中构建任务。
         Rule rule1 = Rule.createRules(DateRule.month(1), TimeRule.marketClose(1, 1));
         Rule rule2 = Rule.createRules(DateRule.month(-1), TimeRule.marketClose(1, 1));
         addTask(rule1, task1);
         addTask(rule2, task2);
    }
    public void handleData(BackTestContext context, BarData piece) throws Exception {
        // Nothing to do.
    }
}

其他API

history()

public Map<String, double[]> history(int tickNums, String field) throws Exception

field 可为 “open”,”close”,”high”,”low”,”volume”,”volume_money”。

获得universe里面证券的一定范围内(num天)的历史数据,返回结果是一个Map。

使用universe里面的股票代码即可得到相应股票的history。

例如history(30,"open").get("sha-601318")获得的是一个长度为31的数组。

数组里面保存了sha-601318的前30天开盘价(含当前时间片)。所有获取hisroy数据的方法如下:

// 取得指定股票的上一个最小测试单位及之前的历史数据,共取tickNums个
public double[] history(String stock, int tickNums, String field) throws Exception
// 取得指定股票的上backOffset个最小测试单位及之前的历史数据,共取tickNums个,相对于上面的方法,往前做了backOffset个偏移。
public double[] history(String stock, int tickNums, String field, int backOffset)

// 取得所有股票的上一个最小测试单位及之前的历史数据,共取tickNums个
public Map<String, double[]> history(int tickNums, String field) throws Exception
// 取得所有股票的上backOffset个最小测试单位及之前的历史数据,共取tickNums个,相对于上面的方法,往前做了backOffset个偏移。
public Map<String, double[]> history(int tickNums, String field, int backOffset) throws Exception

// 下面两个方法与上面的方法类似,但它不管最小测试单位是天还是小时,都按日取历史数据。
public double[] dayHistory(String stock, int tickNums, String field, int backOffset) throws Exception
public double[] dayHistory(String stock, int tickNums, String field) throws Exception

record()

record("kpi",kpiValue);

record可以记录回测过程中的变量,并展现到界面上。

kpiValue为一个double、int或float。

log

log.info("some text");
log.debug("this one:${data.get('sha-601318').close}");
log.debug("<font color='red'>close price:</font>${data.get('sha-601318').close}");

getTrades()

public List<Trade> getTrades()

获取所有当前已经成交的交易。该方法返回一个List。

public List<Trade> getTrades(String name)

获取指定股票当前已经成交的交易。该方法返回一个List。

getOpenAssets()

public List<Trade> getTrades()

获取所有当前已买入仍未卖出的股票交易信息。该方法返回一个List。

public List<Trade> getTrades(String name)

获取指定股票当前已买入仍未卖出的股票交易信息。该方法返回一个List。

findByGroup()

findByGroup(String groupName);

根据股票组名称寻找到相应的股票。

选股器

在所有的策略中,均可访问到选股器selector,它是一个扩展的Map类:

public class StockSelector extends HashMap<Date,List<SelectedStock>>

它将每个选股周期内选中的股票,按时间标签分类放到一个List中。可以通过下面方法设定每个周期最终选定的股票集的数量:

// 每一轮选股最后保留前10支。
 selector.setLimit(10);

当您按日进行模拟或回测时,Map的Key就是当日的时间标签,您可以通过context.now获取当前时间标签。提供被选中的股票代码,及一个score字段,即可构造一个SelectedStock。

SelectedStock stk = new SelectedStock("sha-601318", 10.5);

上面假设该股票的score为10.6,当日切换或手动调用selector.captureTop()时,selector会将所有被选中的股票按score降序排列,并按之前设置的限值,截取前面的部分股票集。下面是一个示例:

class MyStrategy extends BackTestTradingStrategy  {
    Factor fMax = new MAXFactor(90);
    Factor fSma = new SMAFactor(15);
    @Override
    public void init(BackTestContext context) throws Exception {
        context.benchMark = "sha-600498";
        context.universe.addGroup("all-stocks");
    }
    @Override
    public void prepare(BackTestContext context) throws Exception {
        addPipeLine("max", fMax);
        addPipeLine("sma", fSma);
    }
    @Override
    public void handleData(BackTestContext context, BarData data) throws Exception {
        log.info(context.currentBarTime.toString());
        for (String stock : universe) {
            double max = fMax.getValue(stock);
            double sma = fSma.getValue(stock);
            if (max <= data.get(stock).close) {
                SelectedStock st = new SelectedStock(stock, (float) ((max - sma) / sma) * 100);
                selector.add(st);
            }
        }
        selector.captureTop();
        log.info(selector.get(context.now));
    }
}

上面的策略选取当日创90日内最高值的股票,并按最高值超过15日均值的程度设置score,并最终得到排名前10的股票集。

管道

如果你在每个时间片中预先需要做一些列操作,然后对某些指标进行排序,你可以考虑使用管道(pipe)。我们通过addPipeLine()向添加一个管道,它支持两种方式,一种是添加Factor对象,另一种是添加文本表达式。例如:

SMAFactor sma5 = new SMAFactor(5, "close");
addPipeLine("sma5", sma5);

则添加了一个SMA Factor,我们可以通过下面的方法在handleData()中获取它的值:

double val = getAllPipeLines().get("sma5").getPriorValue(stock1);

或通过获取一个DataFrame,检索出一个Factor的值,而且得到的DataFrame已经排序,当需要从多个股票的指标中进行筛选操作时,我们可以使用getPipeLineOutput(),例如我们通过下面的方法取得当前选中的股票的sma5的最大值:

float highestRate = (float) getPipeLineOutput().getColumn("sma5").get(0);

一个DataFrame就是一个数据表格,我们可以任意取其行列,每个列是同一类值,它一有个列名,可以通过列名取得该列;同一例的各行数据按升序排列,可以通过序号访问它,如上面代码中的get(0)。

有时候,现有的指标或许仍不能满足我们的需求,我们需要将多个Factor做一些简单的运算来得到我们想要的结果。求比率就是常见的一种应用场景,这时,我们可以通过文本表达式来增加一个pipe——实际上,当addPipeLine()的第二个参数是文本表达式的时候,它的返回值是一个Factor。

addPipeLine("sma5", sma5);
addPipeLine("sma20", sma20);
Factor rate = addPipeLine("rate", "100*sma5/sma20");

所以,表达式与Factor没有什么不同,它们都会在每个时间点给出当时的统计数值,您可以通过getAllPipeLines()或getPipeLineOutput()取其值,也可以当它是一个普通的Factor使用它的get()系列方法。

class RandomTest extends Strategy {
    SMAFactor sma5 = new SMAFactor(5, "close");
    SMAFactor sma20 = new SMAFactor(20, "close");
    Factor rate = null;

    public void init(BackTestContext context) throws Exception {
        context.universe.addAll();
    }

    public void prepare(BackTestContext context) throws Exception {
        addPipeLine("sma5", sma5);
        addPipeLine("sma20", sma20);
        rate = addPipeLine("rate", "100*sma5/sma20");
    }
    
    public void handleData(BackTestContext context, BarData data) throws Exception {
        float highestRate = (float) getPipeLineOutput().getColumn("rate").get(0);
        record("rate_high", highestRate);
        record("rate", rate.getCurrentValue(context.benchMark));
    }
}

全部Factor列表

点击这里可以查看所有Factor的详细文档。我们拥有越来越多的Factor可供你直接使用。

获取财务数据

getFundamentals(String field, String dateStr) 返回 指定时间,所有股票集的指定财务数据字段。

Note

简单来说就是以DataFrame存储所有股票相关指标的指定时间的数据信息,例如,查询股票sha-600000,2016年02月02号的income_statDate,balance_capital_reserve_fund,valuation_pe_ratio字段信息,则可以调用此接口实现。查询到的有些结果是当天能看到的数据,而并不一定是当天发布的数据。比如一些季度数据,只是季度末发布。但你用任何日期都可以查询到结果,结果返回的就是你能看到的最近的季度数据。

  • 参数:

·field_list:字符串类型,需要查询的字段列表,例 :”income_statDate,valuation_pe_ratio”

·date_str:字符串类型,指定查询日期,格式为:20151231

  • 返回:

·返回类型DataFrame结构中,示例如下:

             income_stateDate    valuation_pe_ratio
sza-000001   xxxxxx              xx.xx
sha-600000   xxxxxx              xx.xx


主要财务指标
indicator_tot_asset 总资产
indicator_owner_interests 所有者权益
indicator_bps 每股净资产BPS
indicator_op_profit 营业利润
indicator_all_profit 利润总额
indicator_eps 基本每股收益
indicator_diluted_eps 稀释每股收益
indicator_roe_full 净资产收益率%(全面摊薄%)
indicator_roe_weighted 净资产收益率%(加权平均%)
indicator_er 股东权益比率
indicator_alr 资产负债比率
indicator_cr 流动比率
indicator_qr 速动比率
indicator_arr 资产报酬比率
市值数据
valuation_pb_rat PB
valuation_static_eps 静态EPS
valuation_static_pe 静态PE
valuation_ps_ratio 基本市销率
资产负债  
balance_retained_profit 未分配利润
balance_capital_reserve_fund 资本公积
balance_code 股票代码
balance_total_assets 资产总计
balance_total_liability 负债合计
balance_total_owner_equities 所有者权益(或股东权益)总计
balance_statDate 截止日期
现金流量  
cash_flow_invest_cash_paid 投资支付的现金
cash_flow_borrowing_repayment 偿还债务支付的现金
cash_flow_tax_payments 支付的各项税费
cash_flow_staff_behalf_paid 支付给职工及为职工支付的现金
cash_flow_other_operate_cash_paid 支付其他与经营活动有关的现金
cash_flow_subtotal_operate_cash_outflow 经营活动现金流出小计
cash_flow_invest_withdrawal_cash 收回投资收到的现金
cash_flow_invest_proceeds 取得投资收益收到的现金
cash_flow_other_cash_from_invest_act 收到其他与投资活动有关的现金
cash_flow_subtotal_invest_cash_inflow 投资活动现金流入小计
cash_flow_subtotal_invest_cash_outflow 投资活动现金流出小计
cash_flow_cash_from_invest 吸收投资收到的现金
cash_flow_cash_from_mino_s_invest_sub 其中:子公司吸收少数股东投资收到的现金
cash_flow_cash_from_borrowing 取得借款收到的现金
cash_flow_subtotal_finance_cash_inflow 筹资活动现金流入小计
cash_flow_dividend_interest_payment 分配股利、利润或偿付利息支付的现金
cash_flow_other_finance_act_payment 支付其他与筹资活动有关的现金
cash_flow_subtotal_finance_cash_outflow 筹资活动现金流出小计
cash_flow_cash_equivalents_at_beginning 期初现金及现金等价物余额
cash_flow_cash_and_equivalents_at_end 期末现金及现金等价物余额
利润  
income_operating 营业收入
income_operating_cost 营业成本
income_operating_tax_surcharges 营业税金及附加
income_sale_expense 销售费用
income_administration_expense 管理费用
income_financial_expense 财务费用(收益以“-”号填列)
income_operating_profit 营业利润(亏损以“-”号列)
income_non_operating_revenue 加:营业外收入
income_non_operating_expense 减:营业外支出
income_disposal_loss_non_current_liability 其中:非流动资产处置净损失(净收益以“-”号填列)
income_total_profit 利润总额(亏损总额以“-”号填列)
income_income_tax_expense 减:所得税
income_net_profit 净利润(净亏损以“-”号填列)
income_basic_eps 基本每股收益(EPS)
income_diluted_eps 稀释每股收益
股本  
share_capitalization 总股本
share_circulating_cap 已流通股
share_circulating_a 流通A股
share_limit_sale 有限售条件股份
share_limit_sale_country 国家持股
share_limit_sale_country_legal_person 国家法人持股
share_limit_sale_other_domestic 其他内资持股
share_limit_oversea 外资持股
share_before_reform 股改前限售流通股

内部 Factor 使用说明

Factor 概述

Raquant提供了一百多种内建指标(并且还在增加),并使得用户可以方便的调用这些指标,所有被回入universe的股票都能得到相应的指标。定义一个Factor很简单:

Factor ma5 = new MAFactor(5,OHLCV.CLOSE);

以上代码定义了一个5日均值的指标。要使用指标的值也很简单,对于单值指标(即该指标的计算值只有一个),可以使用第1组方法;对于多值指标,则必须指定返回值哪一个,即使用第2组方法。

方法

 

 

1

publicdouble getPriorValue( String stock)

publicdouble getCurrentValue( String stock)

public double getValue( String stock, int offset)

public double[] getAllValues( String stock )

 

 

2

publicdouble getPriorValue( String stock, final String retType)

publicdouble getCurrentValue( String stock, final String retType)

publicdouble getValue( String stock,final String retType,int offset)

publicdouble[] getAllValues( String stock, final String retType)

布林通道有三条线,它就是一个典型的多值指标:

class MyStrategy extends Strategy {
    String stock1 = "sha-601318";
    BBANDSFactor bbands = new BBANDSFactor();
    public void init(BackTestContext context) {
        universe.add(stock1);
    }

    public void prepare(BackTestContext context) {
        addPipeLine("bbands", bbands);
    }

    public void handleData(BackTestContext context, BarData data) throws Exception {
        double upper = bbands.getPriorValue(stock1, BBANDSFactor.RET_UPPER_BAND);
        double middle = bbands.getPriorValue(stock1, BBANDSFactor.RET_MIDDLE_BAND);
        double lower = bbands.getPriorValue(stock1, BBANDSFactor.RET_LOWER_BAND);
    }
}

所以在使用布林通道的时候,要指明当前获取哪条线的值。对于单值指标来说则不需要,比如上面定义的ma5,5日均线仅有一条,我们可以使用下面的方法取值:

double maVal = ma5.getPriorValue(stock1);

注意到上面使用的方法都是getPriorValue(),取到的值是上一交易日的值,因为当天的值还未产生;但我们仍然提供了获取“未来”数据的方法,getCurrentValue()会取到当天的值,即使当天还没收盘,您可以使用这个方法绘图而不至于错位。

每个Factor都是在一定的计算周期上执行运算的,比如要得到日均线,需要每天计算一次;要得到小时均线,则需要每个小时计算一次。这个周期是与回测的时间单位无关的。在RaQuant平台,所有的Factor都是默认按日计算的,除非显式的声明其计算单位。指定Factor的计算周期的方式有两种,一种是使用SetUnit()方法,另一种是在加入Pipeline的时候指明计算周期单位:

OBVFactor obv = new OBVFactor(OHLCV.CLOSE);
// 使用Factor的方法设定计算周期单位
obv.setUnit(PeriodUnit.DAY);
// 委托给addPipeLine方法去设置
addPipeLine( "obv", obv, PeriodUnit.HOUR );

注意,每个Factor对象只能有一个固定的计算周期,如果在回测前多次指定其周期单位,其实际值为最后一次设置的值。不要在回测期间(如Shecule或handleData())修改Factor的周期单位,事实上在指标初始化完成后,再试图修改Factor的周期单位总是会失败。

有些Factor的计算过程中需要移动平均值作为辅助,浏览下面的Factor可以发现,其中一些构造方法需要指定MA的类型,即以什么样的算法计算均值,目前Raquant支持以下移动平均类型:

public class MaType {
    public static final String Sma = "Sma";
    public static final String Ema =  "Ema";
    public static final String Wma =  "Wma";
    public static final String Dema =  "Dema";
    public static final String Tema =  "Tema";
    public static final String Trima =  "Trima";
    public static final String Kama =  "Kama";
    public static final String Mama =  "Mama";
    public static final String T3 =  "T3";
}

例如,下面使用简单移动平均构造了一个APOFactor:

APOFactor fApo = new APOFactor( 12, 26, MaType.Sma, OHLCV.CLOSE );

开始写策略吧!

整理思路

无论作为资深股民,还是股市新手,您在进行股票交易时,总会有一些想法在背后支撑。它们有时候非常有效,有时候也许会把您带向亏损;有时候您的想法很好,但您不能自信的执行下去;有时候您干脆只想凭灵感,跟着感觉走,但灵感好像很久没来光顾过了——那么,有没有一种基于理性的方法,让您的想法可以被固化、进而优化,您成功的交易方法的每个细节都可以被记录,而且有办法客观评价您的操作水平的方法?答案是肯定的,那就是使用量化交易策略。以前老张曾经说,“上次我通过买强势上涨的股票,赚了一大笔”——听上去很诱人,但是,我们将尽量避免使用这样的描述,这样的道理您听再多也过不好这红红绿绿的人生。这里没有股市“专家”和“老师”,只有类似于这样的描述在Raquant才上可以被接受的:“2016年第二季度,我通过买入10日内涨幅排名在前10以内的股票,持股2天内售出,共获得了相当于本金的67%的收益,这中间我承受的最大亏损(回撤)是我本金的15%......”,很简单是吧,我已经感觉到您已经秒懂了老张的方法,但遗憾的是,没那么简单!我只能告诉您,这个方法在14年到15年上半年您用了会说好,但在今年都跑不过余额宝,不过我很确信老张并不担心,因为他在潜心研究别的策略。我为什么知道的这么清楚?您不用怀疑,因为我就是老张。

有人说,凭盘(yun)感(qi)炒股比较容易,我有大量的操盘失败的经历,这些经验难以被量化。确实,有些“感觉”确实很难被量化,但这些“感觉”往往是不需要被量化的,甚至应当被丢弃的,因为它们可能不仅没有用,还容易造成幻觉——您成功的时候觉得感觉很准,失败的时候就把责任推给“股市太诡异”,或者“我没有严格的按自己的感觉走,没有纪律性”。事实是,根本没有任何一种方法能有效的评价一个人“感觉”的优劣。没有原则没有规律的炒股,就像是猴子掷石头,相信总有一天它能刚好掷出一座房子来,但我们很难活那么久。

完全跟感觉走的股民也是极少数,大多数股民都有自己的套路,而且有的还非常有效,也许您就有不少,如果您的套路能:

    1. 可精确的重复

    2. 可以被预期和客观评价

那么,您的交易方法就已经被量化了。上面两点的意思是,首先,当遇到您认为跟您以前成功获利的股市形态相似的时候,您的套路能随时拿出来用;其次,你有方法知道您的套路的预期收益和可能承担的风险。如果您的交易方法不能做到以上两点,就意味着您多多少少还是在依赖感觉,应当突破这一点,变“当时那把剑离我只有一丢丢远”为“当时那把剑离我只有0.01公分”——你看,星爷也是采用了量化的思维,从而突显了当时的紧张气氛。

从套路化到量化的距离也只不过是0.01公分的距离。

选对股票很重要

在一个只能做多的股票交易市场怎么才能赚到钱?答案将我们逼入一个角落:买那些会上涨的股票。所以,Raquant特别注重选股,甚至将先股策略单独作为一个策略类型来对待。各大财经门户网站上大多都有自己的选股器,但使用都并不多。首先,选股类型和参数固定化,无法满足用户的个性化需求,也许我并不想看哪支股票创了历史新高,我就想知道哪支股票现在创了它一周以来的新高;同时它还不能是能源版块的,因为我对石油双熊有阴影;同时它一个月内的涨幅要控制在15%以内;同时它连续上涨要超过两天;并且,我要按自己的方法按以上特征综合选出排名最高的前10支股票。其次,那些选股器难以实时对接到你的策略实现中去,不能有效利用的信息,就失去了它的价值。

Raquant平台支持您任意定制您自己的选股策略,这些选股策略很容易被应用到您的交易策略中去,接下来我们会展示这一点。而且,Raquant平台已经实现了一些选股器,并且提供策略代码,您只需要简单的修改一下参数就能得到您个性化的选股器。

实现选股策略

接下来,我们要完成一个简单的选股策略,它选出近期连续上涨的股票,并根据连涨天数排名,最终选出10支股票。

首先,我们得借助于一个指标:RISINGFactor,它为我们提供两组数据,一组是连续上涨天数;另一组是连续上涨幅度。可以看出,它总是在一个趋势上做统计,如果一支股票连续上涨了10天,但今天跌了,它的上涨天数就变成了-1;昨天,它的上涨幅度是连续上涨10天的总涨幅,但今天跌了,它的涨幅就变成了今天下跌的程度,数值也从正值变成了负值。

借助于指标,能简化我们的代码,使我们的思路集中到要实现的策略上去——而不是花费精力去对接一下第三方库提供的API,比如常用的TA-lib,我们已经内置了TA-Lib的所有指标,您可以在FACOTR看到它们。我们非常愿意按您的想法为您实现一些指标,如果您同意的话我们还将有可能将它内置到我们的指标库中。总之,我们可以为大家完成的工作,不希望大家每个人都再重复一遍。

class MyStrategy extends BackTestTradingStrategy {
    // 声明一个指标
    Factor fRise = new RISINGFactor();
    @Override
    public void init(BackTestContext context) throws Exception {
    }
    @Override
    public void prepare(BackTestContext context) throws Exception {
        // 选择所有股票
        context.universe.addGroup("all-stocks");
        // 注册一个指标让后台去计算,它可以计算出两类数值,持续上涨天数和持续上涨幅度。
        addPipeLine("rise", fRise);
        // 每一轮选股最后保留前10支。
        selector.setLimit(10);
    }
    @Override
    public void handleData(BackTestContext context, BarData data) throws Exception {
        for (String stock : context.universe) {
            // RISINGFactor.RET_RISING_COUNTS -> 持续上涨天数,若是下跌则count为负,持平则为0.
            double count = fRise.getValue(stock, RISINGFactor.RET_RISING_COUNTS);
            
            // RISINGFactor.RET_RISING_COUNTS -> 持续上涨期间的涨幅,若是下跌则scope为负,持平则为0.
            //double scope = fRise.getValue(stock, RISINGFactor.RET_RISING_SCOPE);
            
            // 增加一支股票到选股器,它的分数是count。context.universe中的两千多支股票都会被加入。
            selector.add(new SelectedStock(stock, count));            
        }
        
        // 截取前10支股票,其它两千多支count值较低的股票就落选了
        // 每次调用这个方法的时候,表示本轮选股结束,它可以通过时间标签被再获取到。
        selector.captureTop();
        
        // 获取本轮选股的结果,当然,selector的get方法也能获取到上一轮的选股结果——如果有的话
        // selector.get(context.priorDay(-1));
        log.info(selector.get(context.now));
    }
}

实现交易策略

选好看涨的股票后,只需要加一些简单的操作就可以变成一个交易策略了。接下来我们买入选中的10支股票,并将他们持有一天并卖出。这个策略很简单,但在某些行情下也具有很强的赢利能力。事实上,没有谁能规定必须足够复杂的策略才能赢利,策略是您的经验和智慧在代码上的实现,质能方程比牛顿定律写起来还简短不是吗。您的头脑中可能有许多成熟的或不成熟的策略构思,那么,不妨先从容易实现的开始做起。

class MyStrategy extends BackTestTradingStrategy {
    // 声明一个指标
    Factor fRise = new RISINGFactor();
    @Override
    public void init(BackTestContext context) throws Exception {
    }

    @Override
    public void prepare(BackTestContext context) throws Exception {
        // 选择所有股票
        context.universe.addGroup("all-stocks");
        // 注册一个指标让后台去计算,它可以计算出两类数值,持续上涨天数和持续上涨幅度。
        addPipeLine("rise", fRise);
        // 每一轮选股最后保留前10支。
        selector.setLimit(10);
    }

    @Override
    public void handleData(BackTestContext context, BarData data) throws Exception {
        
        // 为了最大程度利用资金,请记得在某一时刻应该先卖再买;顺序反了的话可能买的时候没有资金,但实际上卖出了部分持仓后还是会有充足资金的。
        for(Position pos : context.portfolio.getAllPositions()) {
            if(pos.isEmpty()) continue;
            if(daysBetween(pos.lastBuyDate, context.now) > 1) {
                orderTarget(pos.security,0);
            }
        }
        
        for (String stock : context.universe) {
            // RISINGFactor.RET_RISING_COUNTS -> 持续上涨天数,若是下跌则count为负,持平则为0.
            double count = fRise.getValue(stock, RISINGFactor.RET_RISING_COUNTS);
            
            // RISINGFactor.RET_RISING_COUNTS -> 持续上涨期间的涨幅,若是下跌则scope为负,持平则为0.
            //double scope = fRise.getValue(stock, RISINGFactor.RET_RISING_SCOPE);
            
            // 增加一支股票到选股器,它的分数是count。context.universe中的两千多支股票都会被加入。
            selector.add(new SelectedStock(stock, count));            
        }
        
        // 截取前10支股票,其它两千多支count值较低的股票就落选了
        // 每次调用这个方法的时候,表示本轮选股结束,它可以通过时间标签被再获取到。
        selector.captureTop();
        
        // 获取本轮选股的结果,当然,selector的get方法也能获取到上一轮的选股结果——如果有的话
        // selector.get(context.priorDay(-1));
        log.info(selector.get(context.now));
        
        for(SelectedStock st :selector.get(context.now)) {
            // 买入股票
            this.order(st.getSecurity(), 10000);
        }
    }
}

回测检验

1. 我们的目标

策略写完之后,可以放到平台上去检验一下效果如何了。简短来说,我们希望收益越多越好,回撤越小越好,alpha越大越好,beta越小越好;翻译成普通话就是,我们希望赚到钱,并且在这个过程中不能有太大的暂时性的亏损——首先我们的心脏可能受不了,另外,即使我们的心脏受的了,你的经理人也可能会强制平仓;我们希望我们持续的赚钱,我们赚钱的能力不会因为股市行情的好坏而受到太大影响。一个优秀的策略总是要在这些风险指标中作平衡。

2. 数据陷阱

第一点是讲我们的目标是各种风险指标都很理想,收益也可观,那么,最终我们得到了满意的数据,就可以实盘交易了吗?没那么简单。

在量化平台上有很多策略,收益率高的吓人,各像指标也很出色——但是,请您记住,能经得处模拟盘考验的策略您才可以考虑放到实盘。在实现策略时,您可能会有很多可调参数,比如持仓时间、买入卖出时机等,您在调优的时候有时候会做出一些不恰当的调整,这些调整可能会人为地躲过一些灾难点,贴近于一些强势上涨点,最后造成的结果可能是,以历史行情过度拟合,却不具备普遍的适应性。

测试覆盖率要有整有零。即,要放到历史的长河中去检验,也要放到历史的具体时段来检验。很多策略在某段历史行情中跑得很出色,但是,历史不会倒流,如果历史能倒流的话,这些策略也没有用,买彩票更有效。所以,先要看长期表现,是否有足够的赢利;再进行详细测试页面,分析这赢利是不是在特定的一个时间段累积的;再测一下近期表现,看表现是否良好。有些策略对特定的行情特别敏感,能在短时间内创造大量的收益,但在其它行情中表现却不好,您要学会分辨。那么,我们是否可以等到下一次这种行情到来时再使用这个策略呢?实际上很困难,因为我们总是在这波行情结束了之后才会说这是一个什么样的行情,要提前作出判断的话,还是有一定风险的。

3. 理性的对待量化交易

量化交易显然不是凭感觉,一种交易策略也不会是一成不变的,它需要不断应对外界的变化,则时也需要完善自身的技术细节。

参考策略案例

双均线策略

class Myclass extends Strategy {
    private String stock1 = "sha-601318";
    MAFactor ma5 = new MAFactor(5, MaType.Sma, OHLCV.CLOSE);
    MAFactor ma10 = new MAFactor(10, MaType.Sma, OHLCV.CLOSE);
  
    public void init(BackTestContext context) {
        context.universe.add("sha-601318");        
    }
  
    public void prepare(BackTestContext context) {
        addPipeLine("ma_5", ma5);
        addPipeLine("ma_10", ma10);
    }
    public void handleData(BackTestContext context, BarData data) throws Exception {
        double fMa5 = ma5.getPriorValue(stock1);
        double fMa10 = ma10.getPriorValue(stock1);
  
        if (fMa5 > fMa10) {
            log.info("buy 10000");
            order(stock1, 10000, "Buy in");
        } else if (fMa5 < fMa10 && context.portfolio.getAmount(stock1) > 0) {
            log.info("sell all");
            orderTargetValue(stock1, 0, "Sell out.");
        }
        record("capital", context.portfolio.getTotalCapital());
    }
}

均线回归策略

class MyStrategy extends Strategy {
    private String sza1 = "sha-601318";
    MAFactor ma60 = new MAFactor(60, MaType.Sma, OHLCV.CLOSE);
  
    public void init(BackTestContext context) {
       context.universe.add(sza1);
    }
    
    public void prepare(BackTestContext context) {
        addPipeLine("ma60", ma60);
    }
      
    public void handleData(BackTestContext context, BarData data) throws Exception {
        double avg60 = ma60.getPriorValue(sza1);
        float close = data.get(sza1).getClose();
        float cash = context.portfolio.getCash();
        if (close < avg60 * 0.95) {
            orderValue(sza1, cash, "Buy in.");
        } else if (close > avg60 && context.portfolio.hasPosition(sza1)) {
            orderTargetValue(sza1, 0, "Sell all.");
        }
        record("close", close);
        record("avg60", avg60);
    }
}

MACD策略

class MyStrategy extends Strategy {
    private double prevDelta = 0;
    String stock1 = "sha-601318";
    MACDFactor fMacd = new MACDFactor(12, 26, 9);
 
    public void init(BackTestContext context) {
        universe.add(stock1);
    }
    
    public void prepare(BackTestContext context) {
        addPipeLine("macd", fMacd);
    }
    
    public void handleData(BackTestContext context, BarData data) throws Exception {
        double macdOut = fMacd.getPriorValue(stock1, MACDFactor.RET_MACD);
        double macdSignal = fMacd.getPriorValue(stock1, MACDFactor.RET_MACD_SIGNAL);
 
        record("macd", macdOut);
        record("macd_signal", macdSignal);
 
        double delta = macdOut - macdSignal;
        if (prevDelta > 0 && delta < 0) {
            log.info("Sell at "+delta);
            orderTargetPercent(stock1, 0, "Sell all.");
        } else if (prevDelta < 0 && delta > 0) {
            log.info("Buy at "+delta);
            orderPercent(stock1, 80, "Buy in.");
        }
        prevDelta = delta;
    }
}

RSI策略

class MyStrategy extends Strategy {
    String stock1 = "sha-601318";
    RSIFactor fRsi = new RSIFactor();
 
    public void init(BackTestContext context) {
        universe.add("sha-601318");
    }
    
    public void prepare(BackTestContext context) {
        addPipeLine("rsi", fRsi);
    }
    
    public void handleData(BackTestContext context, BarData data) throws Exception {
        double rsi = fRsi.getPriorValue(stock1);
        record("rsi", rsi);
        if (rsi > 50 && rsi < 80) {
            orderPercent("sha-601318", 10, "Buy");
        } else {
            orderTargetPercent("sha-601318", 0, "Clear");
        }
    }
}