在上一篇文章中,我们已经对Dresdon所提供的功能进行了简单的介绍。在这篇文章中,我们将介绍如何基于Dresdon进行二次开发。
Dresdon的扩展点
就像上一篇文章所介绍的那样,Dresdon主要是一个量化引擎。用户可以通过脚本或者Java编程的方式来描述模型的买卖条件,并进一步通过扫描该模型在所有股票中的所有匹配来评估该模型的具体表现。通过这种方式,用户可以很大程度地优化自己的交易系统,从而实现稳定盈利。
通过脚本来描述股票的买入卖出条件十分简单:
// 当日和前日股价上涨 $isRaisingUp = growth(close()) > 0.01 && growth(close(), 1) > 0.01 // 5日前存在着一个长度至少为30,震荡幅度小于5%的平台 $isPlatform = platform_exists(0.05, 30, 5) // 在平台前存在长度至少为20日,最大上涨幅度为12%的缓慢增长 $isSlowRaiseBeforePlatform = is_slow_raise(0.12, 20, platform_start(0.05, 30, 5)) …… $buyCondition = $isRaisingUp && $isPlatform && $isSlowRaiseBeforePlatform && …… $sellCondition = close(0) < ma5(0) && ……
接下里用户就可以通过扫描2006年1月到2020年4月之间所有匹配来统计该模型的表现:
{ "averageBenefit" : 0.049035519980520418, // 平均单笔收益为4.9%左右 "maxBenefit" : 74.86122362293308, // 最高收益为74.9% "minBenefit" : -4.000000000000014, // 最大止损为4% "totalCount" : 313, // 2006.01 – 2020.04之间匹配313次 "averageDayCount" : 11.875656742556918, // 平均持股时间为11.9天 "successRatio" : 0.46059544658493873 // 成功率为46%左右 }
当然,如果用户会Java,那么他还可以将模型写成一个Java类,进而得到编译器的强类型支持:
// 当日和前日股价上涨 BooleanHolder isRaisingUp = and(greaterThan(growth(close()), 0), greaterThan(growth(close(), 1), 0)); // 5日前存在着一个长度至少为30,震荡幅度小于5%的平台 BooleanHolder platformExists = platformExists(0.05, 30, 5) // 在平台前存在长度至少为20日,最大上涨幅度为12%的缓慢增长 IntHolder platformStart = platformStart(0.05, 30, 5); BooleanHolder isSlowRaiseBeforePlatform = isSlowRaise(0.12, 20, platformStart); …… BooleanHolder condition = and(isRaisingUp, platformExists, isSlowRaiseBeforePlatform, ……);
除了添加自定义模型之外,用户还可以添加自定义函数。这些函数可以用来判断某日K线的特征,或者拟合特定K线形态。例如下面就是一个用来计算指定K线震动幅度的函数:
public static Value.Shrink shrink(int index) { return new Value.Shrink(index); } @Operation(key = KEY_SHRINK, resultType = DOUBLE, arguments = { @Arguments(paramTypes = { INT }) }) public static class Shrink extends HolderBase<Double> implements DoubleHolder { protected IntHolder index; protected Integer indexValue; public Shrink(int index) { this(new IntValue(index)); } public Shrink(IntHolder index) { super(KEY_SHRINK); this.index = index; } @Override public void preprocess(QuantContext context) { super.preprocess(context); preprocess(context, index); } @Override public boolean needRefresh(QuantContext context) { return !equals(indexValue, index, context); } @Override protected Double recalculate(QuantContext context) { indexValue = index.getValue(context); if (indexValue == null) { return null; } DailyTrading dailyTrading = context.getTradings().get(indexValue); double blockSize = Math.abs(dailyTrading.getClose() - dailyTrading.getOpen()); double totalVariation = dailyTrading.getHigh() - dailyTrading.getLow(); this.value = blockSize > SMALL_DOUBLE_VALUE ? totalVariation / blockSize : Double.MAX_VALUE; return value; } @Override public void persist(StringBuilder builder) { builder.append(getKey()); builder.append(INDEX_START); getIndex().persist(builder); builder.append(INDEX_END); } }
在后面的章节中,我们将详细讲解上面模型中各个买入卖出条件的意义。
添加自定义模型
下面就让我们从添加自定义模型开始。抽取一个模型常常需要经过以下一系列步骤:
1. 确定模型形态。用户首先需要确定需要匹配的模型的大致形态有哪些,如起涨阶段的线形是什么样子的,整理期是以什么形态呈现的,甚至之前筹码是如何收集的等等。
2. 初筛并收集目标匹配。用户需要为该模型定义一个大致的匹配条件,然后运行引擎。此时得到的结果可能存在着大量的噪音,因此统计数据常常并不好看。但其中也会包含大量的具有较高准确度的匹配。而这些匹配常常是模型的目标匹配。
3. 细化模型。添加其它条件逐渐祛除噪音,以提高模型正确匹配的比率。
4. 细化卖出条件。添加其它卖出提交,以提高模型的收益率及成功率。
当然,凡事都有一个从陌生到熟悉的过程。在添加了几个模型之后,用户可能就能摸到其中的诀窍,进而大大提高模型抽取的效率。在这里给大家列出来我在抽取模型过程中最常使用的一系列经验型策略,避免大家重走我之前的弯路。
首先,模型的买入特征线型要明显,近端的辅助判断逻辑要严格,而远端的辅助判断逻辑要具有较高的容错性。可以说,所谓的股票拉升实际上就是股票价格的异动,而该异动的阻力则很大程度上决定了股票行情到底能走多远。因此起涨阶段线形的略微不同都可能导致量化结果产生非常大的差异。比如都是上涨5%,一个有长上影的K线就远不如没有长上影的K线。反之离当前交易日越远的交易,其对当前股价的影响越小,因此远端的辅助判断逻辑不宜非常严格。
其次,要对常见线形所代表的意义有正确的理解。同样的K线在不同的位置其意义常常并不相同。例如一般来说,低位揉搓线常常是一个好的K线组合,而高位揉搓线,尤其是放量揉搓线则很可能代表一段行情将要终结。
最后,筛选条件常常是可以通用的。就像第一条所说的那样,我们要将买入的特征线形严格地区分。比如拉升是通过一根阳线完成的,和拉升是通过三根K线形成的组合K线完成的效果类似。但是它们的筛选逻辑则常常有一个为2的索引差:一根阳线完成的拉升,我们要从前一天的K线检查,而三根K线组成的拉升,则需要从三天前的交易开始检查。只不过这些检查的参数有些不太相同而已。
添加自定义函数
在编写一段时间的模型之后,用户可能就会感觉到引擎内建的各个表达式很难表现一些特定的限制条件。例如他可能常常需要通过如下表达式来限制K线的波动情况:
$noBigShrink = abs(close(0) – open(0)) * 5 < high(0) – low(0)
甚至用Java编写出来的表达式的可读性更差:
BooleanHolder noBigShrink = lessThan(multiply(abs(minus(close(0), open(0))), 5), minus(high(0), low(0)));
而这部分的逻辑仅仅是在判断当日K线的实体是否过小,进而呈现十字星或锤头线等形态。此时用户就可以在Plugin里面添加自定义的表达式:
public static Value.Shrink shrink(int index) { return new Value.Shrink(index); } @Operation(key = KEY_SHRINK, resultType = DOUBLE, arguments = { @Arguments(paramTypes = { INT }) }) public static class Shrink extends HolderBase<Double> implements DoubleHolder { protected IntHolder index; protected Integer indexValue; public Shrink(int index) { this(new IntValue(index)); } public Shrink(IntHolder index) { super(KEY_SHRINK); this.index = index; } @Override public void preprocess(QuantContext context) { super.preprocess(context); preprocess(context, index); } @Override public boolean needRefresh(QuantContext context) { return !equals(indexValue, index, context); } @Override protected Double recalculate(QuantContext context) { indexValue = index.getValue(context); if (indexValue == null) { return null; } DailyTrading dailyTrading = context.getTradings().get(indexValue); double blockSize = Math.abs(dailyTrading.getClose() - dailyTrading.getOpen()); double totalVariation = dailyTrading.getHigh() - dailyTrading.getLow(); this.value = blockSize > SMALL_DOUBLE_VALUE ? totalVariation / blockSize : Double.MAX_VALUE; return value; } @Override public void persist(StringBuilder builder) { builder.append(getKey()); builder.append(INDEX_START); getIndex().persist(builder); builder.append(INDEX_END); } }
下面就让我们一行行地讲解这些代码的含义。首先是一个静态函数:
public static Value.Shrink shrink(int index) { return new Value.Shrink(index); }
通过该静态函数,用户可以更直观地描述模型逻辑,属于一种语法糖:
new lessThan(new Shrink(0), 5) vs. lessThan(shrink(0), 5)
接下来我们则通过@Operation来标明当前类中包含的逻辑是一个引擎操作的定义。该操作的key为KEY_SHRINK,带有一个Integer类型的参数,返回值的类型为Double:
@Operation(key = KEY_SHRINK, resultType = DOUBLE, arguments = { @Arguments(paramTypes = { INT }) }) public static class Shrink extends HolderBase<Double> implements DoubleHolder {
这里有一个概念,那就是Holder。马上您就会看到,Shrink类实例上并没有记录和交易相关的数据,它仅仅用来承载计算逻辑。也就是说,它相当于一个占位符。实际上,Dresdon支持的所有运算符都是一个Holder,内部只记录算法,不记录任何数据。
那么交易相关的数据都记录在哪里呢?答案是Context。用户可以通过各个holder的getValue()函数来得到各个holder的当前值。现在就让我们看看Shrink类的recalculate()函数的是如何使用它的:
@Override protected Double recalculate(QuantContext context) { indexValue = index.getValue(context); if (indexValue == null) { return null; } DailyTrading dailyTrading = context.getTradings().get(indexValue); double blockSize = Math.abs(dailyTrading.getClose() - dailyTrading.getOpen()); double totalVariation = dailyTrading.getHigh() - dailyTrading.getLow(); this.value = blockSize > SMALL_DOUBLE_VALUE ? totalVariation / blockSize : Double.MAX_VALUE; return value; }
可以看到,recalculate()函数传入了一个QuantContext类型的实例。接下来,该函数的实现通过调用index的getValue()函数得到了index的实际值。接下来,我们就从context中取得了目标交易数据(dailyTrading),并依次通过计算目标K线的实体大小(blockSize),当日最高价和最低价之差(totalVariation)来计算当日的波动情况。这里需要注意的是,计算结果将被首先记录在value这个域中,然后才被该函数返回。
为了提高计算的性能,我们引入了两个机制:refresh和preprocess。前者通过判断参数的值是否变化来确定是否需要运行recalculate()函数。毕竟该函数所包含的计算逻辑可能相当复杂。在其它属性没有发生变化的时候,我们可以通过直接返回value这个缓存域中记录的值来提高运行性能:
@Override public boolean needRefresh(QuantContext context) { return !equals(indexValue, index, context); }
另一种情况则是对预处理的支持。其主要用来提高拟合功能的性能。让我们以一只股票在多年的交易中存在着一系列盘整平台的情况为例。如果我们针对不同的日期都计算一次拟合逻辑,那么引擎的性能将变得很差。毕竟在平台内部的各个交易日对应的是同一个平台。为了解决这个问题,我们添加了预处理步骤。该步骤允许引擎对所有交易日进行一次扫描,并将其扫描结果存储在Context中。在需要的时候从Context中取出相应的预处理结果即可:
@Override public void preprocess(QuantContext context) { …… PlatformExtractor extractor = new PlatformExtractor(symbol, ma5s, rangeValue, minLengthValue); List<PlatformInfo> platforms = extractor.extractPlatforms(); context.getVariableMap().setVariable(key, new ObjectWrapper(symbol, platforms)); } protected PlatformInfo getCurrentPlatform(QuantContext context) { …… ValueHolder<?> variable = context.getVariableMap().getVariable(key); List<PlatformInfo> platforms = (List<PlatformInfo>)(((ObjectWrapper)variable).getValue()); return platforms.stream().filter(platform -> platform.getStartDate().compareTo(seedDate) < 0 && platform.getEndDate().compareTo(seedDate) > 0).findFirst().orElse(null); }
通过这种方法,用户就可以自行创建更高级的函数,进而使得自己的模型变得更为简洁。
转载请注明原文地址并标明转载:https://www.cnblogs.com/loveis715/p/13324937.html
商业转载请事先与我联系:[email protected]
公众号一定帮忙别标成原创,因为协调起来太麻烦了。。。