# 技术指标研发
# 概念定义与建模
我们在一般的交易软件中,都会用到一些常用的技术指标,例如均线、MACD、KDJ之类的。
然而如果由我们自己来实现技术代码时,该怎样定义技术指标呢?它究竟是一个值?还是一个推导公式?它应该是有状态的,还是无状态的?
在尝试给出答案之前,我们先考虑一下我们一般会怎么使用技术指标的:
如上图所示,我们可以总结抽象出以下规律:
- 所有的指标都是基于行情数据的再加工,它依赖了行情数据(仓、量、价)作为计算基础;
- 它需要一个时间跨度,也就是说任何的技术指标都不单单需要知道当前的更新值,也需要缓存一定数量的历史回溯值;
- 它需要定义一个迭代公式,用以根据行情的数据更新计算指标更新值;
- 所有的技术指标可以被分为两大类:单值指标(例如均线),多值指标(例如MACD);
针对以上需求分析,Northstar抽象出指标的实现类:Indicator。 Indicator封装了指标的通用操作,比如缓存数据、更新数据等等。不同的指标之间,其实就只剩下指标算法的区别了。 不过,我们在深入指标实现之前,还要再讲讲单值指标与多值指标的问题。
# 单值指标与多值指标的抽象统一
所谓单值指标,指的是在坐标轴上任意一个X值只对应唯一的一个Y值的指标。最典型的例子便是均线指标,比如MA5。 所谓多值指标,指的是在坐标轴上任意一个X值会对应多个Y值的指标。典型的例子有MACD、BOLL线等等。 为了把复杂问题拆解成简单问题,我们可以把多值指标看作是多个单值指标的组合。拿MACD来讲,无非就是计算两条指标线。 以下是摘自文华的指标代码案例,MACD就是通过计算DIFF线与DEA线得出。
DIFF : EMA(CLOSE,SHORT) - EMA(CLOSE,LONG);//短周期与长周期的收盘价的指数平滑移动平均值做差。
DEA : EMA(DIFF,M);//DIFF的M个周期指数平滑移动平均
因此,不管是什么指标,我们都能把它抽象成一条条的单值指标线的组合,这样我们便可以使用统一的指标抽象模型——Indicator类。
# 如何编写一个指标
正如之前提到的,不同的指标之间的区别仅仅在于指标的计算函数。因此要实现一个指标,只需要实现其计算函数,其余的通用操作全部交由Indicator类进行封装操作。这便是Northstar设计指标框架时的设计思想。 作者在设计指标框架的编程API时,借鉴了文华的函数式编程写法(如上述MACD的算法实现),独创了一套符合JAVA特色的函数式编程写法:
// 代码示例详细请参考tech.quantit.northstar.strategy.api.demo.IndicatorSampleStrategy
this.macdDiff = ctx.newIndicator("MACD_DIFF", params.indicatorSymbol, minus(EMA(12), EMA(26)));
this.macdDea = ctx.newIndicator("MACD_DEA", params.indicatorSymbol, minus(EMA(12), EMA(26)).andThen(EMA(9)));
以上两行就构建了两个指标对象。其参数列表在最精简的构造方式中,只需要三个参数:名称、绑定合约、计算函数。 我们先来看看DIFF指标的函数计算,分别算出EMA(12)与EMA(26)的值,然后再求它们之差。 在Northstar中,只需要定义两函数:
/**
* 指数加权平均EMA
* @param size
* @return
*/
static TimeSeriesUnaryOperator EMA(int size) {
final AtomicDouble ema = new AtomicDouble();
final AtomicBoolean hasInitVal = new AtomicBoolean();
final double factor = 2D / (size + 1);
return tv -> {
double val = tv.getValue();
long timestamp = tv.getTimestamp();
if(hasInitVal.get()) {
ema.set(factor * val + (1 - factor) * ema.get());
} else {
ema.set(val);
hasInitVal.set(true);
}
return new TimeSeriesValue(ema.get(), timestamp);
};
}
/**
* 函数相减
* @param fn1
* @param fn2
* @return
*/
static TimeSeriesUnaryOperator minus(TimeSeriesUnaryOperator fn1, TimeSeriesUnaryOperator fn2){
Objects.requireNonNull(fn1);
Objects.requireNonNull(fn2);
return tv -> {
TimeSeriesValue v = fn1.apply(tv);
TimeSeriesValue v0 = fn2.apply(tv);
v.setValue(v.getValue() - v0.getValue());
return v;
};
}
在编写指标函数时,可以想象成在定义Excel公式。我们拿算术平均值来举例:
用JAVA写出来是这样的:
/**
* 简单移动平均MA
* @param size
* @return
*/
static TimeSeriesUnaryOperator MA(int size) {
final double[] values = new double[size];
final AtomicInteger cursor = new AtomicInteger();
final AtomicDouble sumOfValues = new AtomicDouble();
return tv -> {
long timestamp = tv.getTimestamp();
double val = tv.getValue();
double oldVal = values[cursor.get()];
values[cursor.get()] = val;
cursor.set(cursor.incrementAndGet() % size);
sumOfValues.addAndGet(val - oldVal);
val = sumOfValues.get() / size;
return new TimeSeriesValue(val, timestamp);
};
}
调用 MA(x) 就是创建了一个函数计算对象,这个计算对象会赋值给Indicator实例进行实际运算,运算的入参与返回都是一个TimeSeriesValue对象,即一个包含了时间戳的值。 与Excel不同的是,excel函数调用时的入参是一个数组;而指标函数调用时的入参是一个单值;这就意味着函数需要保存以往的值,也就是说要构造一个有状态的函数,因此需要使用闭包。
所以要理解Northstar指标是怎么编写的,其中最关键的是理解在JAVA中怎么写一个闭包函数,然后用闭包函数把指标算法实现了就相当于把指标写好了。
# 如何在策略中使用指标
写好了指标函数后,我们只需要在交易策略中,如下面代码示例一样,调用ctx.newIndicator方法便可以创建好一个指标对象。 其中ctx是策略模组的API对象,详细可参考【编写程序化策略——编程模型】一节
// 代码示例详细请参考tech.quantit.northstar.strategy.api.demo.IndicatorSampleStrategy
this.macdDiff = ctx.newIndicator("MACD_DIFF", params.indicatorSymbol, minus(EMA(12), EMA(26)));
this.macdDea = ctx.newIndicator("MACD_DEA", params.indicatorSymbol, minus(EMA(12), EMA(26)).andThen(EMA(9)));
# 对复杂的指标作进一步的封装
一般而言,以上提到的写法,关键点在于先定义好一个值更新函数,但对于多值指标,以及指标算法比较复杂的指标而言,以上的写法会比较不容易理解。尤其是对于刚接触的朋友而言,代码的可读性不够好。因此,我们可以对指标函数做进一步封装。
拿上面的 MACD
指标为例,我们可以用以下方式进行封装:
public class MACD {
private int fast;
private int slow;
private int m;
/**
* 创建MACD指标线生成器
* @param fast 快线周期
* @param slow 慢线周期
* @param m 移动平均周期
*/
public MACD(int fast, int slow, int m) {
this.fast = fast;
this.slow = slow;
this.m = m;
}
/**
* 创建MACD指标线生成器
* @param fast
* @param slow
* @param m
* @return
*/
public static MACD of(int fast, int slow, int m) {
return new MACD(fast, slow, m);
}
/**
* 获取DIFF线计算函数
* @return
*/
public TimeSeriesUnaryOperator diff() {
final TimeSeriesUnaryOperator fastLine = EMA(this.fast);
final TimeSeriesUnaryOperator slowLine = EMA(this.slow);
return tv -> {
TimeSeriesValue v = fastLine.apply(tv);
TimeSeriesValue v0 = slowLine.apply(tv);
double val = v.getValue() - v0.getValue();
return new TimeSeriesValue(val, tv.getTimestamp());
};
}
/**
* 获取DEA线计算函数
* @return
*/
public TimeSeriesUnaryOperator dea() {
final TimeSeriesUnaryOperator fastLine = EMA(this.fast);
final TimeSeriesUnaryOperator slowLine = EMA(this.slow);
final TimeSeriesUnaryOperator ema = EMA(this.m);
return tv -> {
TimeSeriesValue v = fastLine.apply(tv);
TimeSeriesValue v0 = slowLine.apply(tv);
v.setValue(v.getValue() - v0.getValue());
return ema.apply(v);
};
}
}
定义一个MACD类,把指标函数的创建写成一个方法,这本质上是一个工厂方法模式。
在使用时,则按如下写法:
MACD macd = MACD.of(12, 26, 9);
this.macdDiff = ctx.newIndicator("MACD_DIFF", params.indicatorSymbol, macd.diff());
this.macdDea = ctx.newIndicator("MACD_DEA", params.indicatorSymbol, macd.dea());
//可以看到,与之前的写法相比,代码可读性要增强了许多
//this.macdDiff = ctx.newIndicator("MACD_DIFF", params.indicatorSymbol, minus(EMA(12), EMA(26)));
//this.macdDea = ctx.newIndicator("MACD_DEA", params.indicatorSymbol, minus(EMA(12), EMA(26)).andThen(EMA(9)));
# 如何验证指标算法
请参考【如何验证指标算法】
# 更多的技术指标
更多的指标会分享在tech.quantit.northstar.strategy.api.indicator包中,后续会陆续添加。
以下是 Northstar
已经实现了的技术指标(有的是常见指标,有的是作者自创的特色指标),以及作者对指标原理的个人见解:
如对指标的实现仍有疑问,欢迎加入社群讨论。
# 创建技术指标时的注意事项
ctx.newIndicator
这个指标创建接口有不同的参数列表,其中有一个值是控制指标对象的长度,为了确保指标被正确计算,该长度应该不少于更新函数中数组的长度。
举个例子,以上面提到的 MA
函数为例,创建时应该注意:
// n 代表的是指标对象的数组长度;m 代表的是函数内部运算的长度
// 若要确保指标的数据充分预热再使用,就要确保 n >= m
ctx.newIndicator("均线", "合约编码", n, MA(m));
对于更复杂的多值指标,例如MACD,就要确保 n 大于等于 MACD
参数列表的最大参数
// 若要确保指标的数据充分预热再使用,就要确保 n >= max(x, y, z)
ctx.newIndicator("MACD", "合约编码", n, MACD.of(x, y, z).diff());