# 技术指标研发

# 概念定义与建模

我们在一般的交易软件中,都会用到一些常用的技术指标,例如均线、MACD、KDJ之类的。 然而如果由我们自己来实现技术代码时,该怎样定义技术指标呢?它究竟是一个值?还是一个推导公式?它应该是有状态的,还是无状态的? 在尝试给出答案之前,我们先考虑一下我们一般会怎么使用技术指标的:
技术指标示例

如上图所示,我们可以总结抽象出以下规律:

  1. 所有的指标都是基于行情数据的再加工,它依赖了行情数据(仓、量、价)作为计算基础;
  2. 它需要一个时间跨度,也就是说任何的技术指标都不单单需要知道当前的更新值,也需要缓存一定数量的历史回溯值;
  3. 它需要定义一个迭代公式,用以根据行情的数据更新计算指标更新值;
  4. 所有的技术指标可以被分为两大类:单值指标(例如均线),多值指标(例如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公式。我们拿算术平均值来举例:
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());