授人以鱼,不如授人以渔。本专栏不做任何直接投资建议,仅做量化技术研究和探讨!
文末惊喜!一个超出老Q自己预期的有趣的自研指标,送给大家继续研究!
我们在前两节课讲了SMA和EMA,也说明了EMA在SMA基础之上的改进是什么,更适用于什么场景。
其实说到底,SMA就是给所有价格赋予了同等的权重,而EMA就是给不同时期的价格赋予了不同的权重,越靠近当前日期的权重越高。所以,我们完全可以说,SMA和EMA都是特殊的加权移动平均,即WMA(Weighted Moving Average)。
WMA的核心在于加权,因为权重可能由很多因素来决定,不同权重下的WMA都有可能具备参考价值。因此WMA有很多不同的变种,甚至我们可以结合自己的经验和思考创造出新的加权指标。
今天,我们就来讲讲不同的WMA的算法是什么,在Python中应该怎么实现,以及如何创造新的、更具实战价值的WMA指标。
末日加权认为最新一天的价格应该具备更高的价值和意义,所以在计算均线时,它所占的权重应该是其他价格权重的两倍。即:
线性加权假设价格的重要性会随着时间而衰减,距离当前时间越久,权重越低,反之权重则越高。假设计算n
日WMA,那么当天价格的权重为n
,前一天价格的权重为n-1
,n-1
天前价格的权重为1。
相比于末日加权,远端价格的权重更低,近期价格的权重更高。而且在线性加权中,近期所有的价格权重都会提升,而非仅有最后一天。
梯形加权会进一步放大远端价格权重和近端价格权重之间的差距,但是最后一天的权重相对来说反而没有那么突出。这个指标的意义在我的理解中不是那么大,主要是这个权重设置背后的逻辑我认为存在一些不好解释的地方。有可能它在历史某段时间中比较有效吧,毕竟存在即是合理。
平方系数加权实际上就是在线性加权思想上迭代的更激进的版本。我们把线性加权中,每个价格的权重系数变成它的平方,那它就变成了平方系数加权。
它适合我们需要均线更紧密地贴合近端价格的情况。
接下来我们看下应该如何计算不同口径下的WMA。
def wma1(series, N):
# 末日加权
weights = np.array([1] * (N - 1) + [2])
weights_total = weights.sum()
result = []
for i, price in enumerate(series):
if i < N - 1:
result.append(None)
else:
wma = np.sum(weights * series[i-N+1: i+1]) / weights_total
result.append(wma)
result = np.array(result)
return result
def wma2(series, N):
# 线性加权
weights = np.arange(1, N+1, 1)
weights_total = weights.sum()
result = []
for i, price in enumerate(series):
if i < N - 1:
result.append(None)
else:
wma = np.sum(weights * series[i-N+1: i+1]) / weights_total
result.append(wma)
result = np.array(result)
return result
def wma3(series, N):
# 梯形加权
weights = np.array(list(range(1, 2*N-2, 2)) + [N-1])
weights_total = weights.sum()
result = []
for i, price in enumerate(series):
if i < N - 1:
result.append(None)
else:
wma = np.sum(weights * series[i-N+1: i+1]) / weights_total
result.append(wma)
result = np.array(result)
return result
def wma4(series, N):
# 平方系数加权
weights = np.array([i ** 2 for i in range(1, N+1)])
weights_total = weights.sum()
result = []
for i, price in enumerate(series):
if i < N - 1:
result.append(None)
else:
wma = np.sum(weights * series[i-N+1: i+1]) / weights_total
result.append(wma)
result = np.array(result)
return result
df['WMA-5'] = talib.WMA(df.close, 5)
df['WMA-5-1'] = wma1(df.close, 5)
df['WMA-5-2'] = wma2(df.close, 5)
df['WMA-5-3'] = wma3(df.close, 5)
df['WMA-5-4'] = wma4(df.close, 5)
df[['code', 'date', 'close', 'WMA-5', 'WMA-5-1', 'WMA-5-2', 'WMA-5-3', 'WMA-5-4']].tail(10)
我们注意到,TAlib
中的WMA的计算结果和我们的线性加权WMA的计算结果是一致的,事实上,线性加权WMA也是应用最多的WMA。
那么下一步,我们看一下如何改进WMA指标使其变得更强大。
我们知道,收盘价仅仅是单纯的价格信息,并没有体现出在不同的价格到底成交了多少钱。然而从逻辑上来考虑,在同一个价位,不同的成交额代表的信息是不一样的,换手越充分、交易额越大,这个位置就越有可能代表着关键价位。
我们知道很多人都有着锚定的心理。假如一个投资者在指数1000点时买入,然后一路下跌到了800。如果他有着锚定心理,他就会一直考虑自己的成本。那么在指数反弹到800、900、950时,因为并没有回本,所以他很可能并不会选择卖掉,哪怕是对后市的预期不是很好。然而当指数接近1000点时,他很可能会有这样的想法:“终于回本了,老子不玩了!”
这会导致什么呢?对这个投资者来说,1000点就是他要卖掉的点位,800、900,都不是。一个人影响比较小,但是如果大量的投资者在同一个点位成交,那么这个点位代表的意义就不一样了。成千上万的人要在同一个价位卖出,这需要很强的承接才可以突破。
这就是为什么我们将成交量或者成交额作为权重,对收盘价求加权平均,可以起到反映筹码信息的作用。
然而,仅考虑成交的密集情况并不够。我们知道,越是远端的买入,当前已经被卖出的概率就越大。也就是说,越久远的筹码,还在持有中的概率就越低。虽然它具体到某一只股票、某一天、某一个投资者会有差异,但是从整体情况来看,这个规律一定是成立的。
所以说,筹码的分布不光和成交量有关系,还和这些成交距离当前的时间有关系。毕竟,能拿到天荒地老的终究是少数,股神拿了那么多年的BYD终究也是要卖掉了。
那么这个时间衰减的系数应该怎么确定呢?如果我们是交易所、券商或者同花顺之类的投顾软件,那么我们可能有大量的报单或者交易记录数据。在这些数据中,我们可以轻易地知道每个账户在每只股票上的持有周期,对这个数据进行统计,我们就可以轻易地得到持有股票不同周期的概率是多少,这个概率,就可以作为我们的权重系数。因为每家手中的数据不一样,所以他们计算得到的筹码情况也是不一样的,这就是为什么不同App中展示的筹码分布不太一样的原因。
然而我们没有这些数据。所以我们可以考虑做一些假设,比如线性衰减、倒数衰减等。
线性衰减在这里是指筹码每天以等比例卖出,比如我们可以假设某天买入的所有筹码,会在100天内均匀地卖掉,然后就全部以新的价格出现在了后续承接筹码的投资者手里。当然,这一价格并不合理,因为大家的投资习惯并不是均匀分布的。
更符合大家认知的,应该是中短线交易者较多,长线交易者较少。也就是说,在前期,筹码松动得比较快,在后期,筹码松动得相对迟缓。倒数曲线就符合这一规律。我们画个图来直观感受一下。
trace1 = go.Scatter(
x=np.arange(1, 11, 1),
y=np.arange(10, 0, -1) / 10 ,
name='线性衰减'
)
trace2 = go.Scatter(
x=np.arange(1, 11, 1),
y=1 / np.arange(1, 11, 1),
name='倒数衰减'
)
fig = go.Figure(data=[trace1, trace2])
fig.update_layout(
width=800, height=600,
title='线性衰减和倒数衰减对比',
title_x=0.5,
legend_orientation='h',
legend_x=0.6,
legend_y=0.8
)
fig.show()
可以看到,线性衰减会在可预见的周期内均匀下降到0,而倒数衰减则是前期迅速衰减、后期平稳下滑。
那么我们就来看一下,如何以成交额和倒数衰减相乘为权重,计算我们定制的WMA指标。
我们暂且假设大部分筹码会在250天内卖完,所以我们筹码的计算周期就选择250个交易日。
# df = df.query(''2020-10-31' <= date <= '2021-02-18'')
all_dates = pd.date_range(start_date, end_date)
trade_dates = list(df.date.unique())
dt_breaks = [i for i in all_dates if i not in trade_dates]
def generate_kline(df):
trace = go.Candlestick(
x=df.date,
open=df.open,
high=df.high,
low=df.low,
close=df.close,
name='K线',
opacity=0.8
)
return trace
def generate_sma_line(df, i):
trace = go.Scatter(
x=df.date,
y=df['SMA-{0}'.format(i)],
name='SMA-{0}'.format(i),
)
return trace
def generate_wma_chip_line(df, i=250):
trace = go.Scatter(
x=df.date,
y=df['WMA-CHIP-{0}'.format(i)],
name='WMA-CHIP-{0}'.format(i)
)
return trace
def plot_kline(df):
trace_kline = [generate_kline(df)]
periods = [30, 60, 120, 250]
trace_wma_chip_lines = [generate_wma_chip_line(df)]
trace_sma_lines = [generate_sma_line(df, i) for i in periods]
fig = go.Figure(data=trace_kline + trace_sma_lines + trace_wma_chip_lines)
fig.update_xaxes(
showspikes=True,
spikesnap='cursor',
spikemode='across',
spikethickness=2,
rangebreaks=[dict(values=dt_breaks)],
)
fig.update_yaxes(
showspikes=True,
spikemode='across',
spikethickness=2,
fixedrange=False,
)
fig.update_layout(
title=code,
title_x=0.5,
spikedistance=1000,
hoverdistance=1000,
hovermode='x unified',
height=800,
width=1600,
xaxis_rangeslider_visible=False
)
return fig
fig = plot_kline(df)
fig.show()
其实我们都清楚,直接用自然数的倒数来作为衰减系数,肯定是比真实情况衰减得比较快。所以这里我们看到以250为周期的自定义的WMA指标明显跟最新价格更加贴合。
但是这里我意外地得到了一个惊喜!
看起来这种计算方式得到的线在趋势中有着非常明显且有效的支撑或者阻力作用。在20年的这一波上涨行情中,K线多次回踩这条线都拉了起来;而在22年的慢熊中,K线多次在碰到这条线附近又回落了下去。反观简单移动平均的30日线、60日线、120日线和250日线,都没有表现出这么好的性能!
今天在跟大家分享的同时,老Q貌似收获了一个值得继续挖掘的点。后边老Q会继续调整参数,看能否得到一个优秀的均线参考指标。
好了,今天的分享就到这里,希望大家都能多动脑子,勇于探索属于自己的量化指标!
联系客服