28  技术分析交易策略

Warning
  1. 时至今日,单纯的技术分析可能已经不那么好用,这里是技术分析在Python中如何实现思路的展示。
  2. 如果你自己有独特的技术分析策略,依然可以利用Python实现。

传统的技术分析技术怎么在Python中应用,包括绘制指标,以及交易的回测。

主要包括:

  1. 趋势交易:双均线系统为例
  2. 波动交易:RSI系统为例
  3. 指标综合:结合前两者

注意:以下策略均未考虑交易成本。如果要考虑,只要在交易日乘以一个系数(1-交易成本)即可。

28.1 简单移动平均策略

从最简单的双均线交易系统开始:设置2条均线,当快速均线位于慢速均线上方,则持有;反之则空仓。

  1. 绘制均线
  2. 计算交易信号
  3. 计算交易的收益率
  4. 绘制交易结果

28.1.1 读取数据

这里简单演示一下如何用akshare包来读取数据。akshare官方主页见https://www.akshare.xyz/。

如果要在你的电脑上安装akshare包(这里采用清华大学的镜像),有几种方法:

  1. 在jupyter notebook中,找一个python单元格,然后粘贴如下代码,并执行:
!pip install -i https://pypi.tuna.tsinghua.edu.cn/simple akshare
  1. 启动Anaconda Prompt(开始菜单中有),或者Mac系统的终端(Terminal),执行如下命令,则会从清华大学的镜像安装akshare到你的电脑上。
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple akshare

上述方法2选1,并且都只需要执行一次,以后都可以使用。

import akshare as ak

这里以锂电池龙头宁德时代为例。

# 读取宁德时代
# 见:https://akshare.xyz/data/stock/stock.html#id20

df = ak.stock_zh_a_hist(symbol="300750",start_date='20180101',end_date='20241231', adjust='qfq')

df
日期 股票代码 开盘 收盘 最高 最低 成交量 成交额 振幅 涨跌幅 涨跌额 换手率
0 2018-06-11 300750 9.64 12.99 12.99 9.64 788 2.845471e+06 48.98 89.91 6.15 0.04
1 2018-06-12 300750 15.00 15.00 15.00 15.00 266 1.058375e+06 0.00 15.47 2.01 0.01
2 2018-06-13 300750 17.21 17.21 17.21 17.21 450 1.972314e+06 0.00 14.73 2.21 0.02
3 2018-06-14 300750 19.64 19.64 19.64 19.64 743 3.578184e+06 0.00 14.12 2.43 0.03
4 2018-06-15 300750 22.32 22.32 22.32 22.32 2565 1.359503e+07 0.00 13.65 2.68 0.12
... ... ... ... ... ... ... ... ... ... ... ... ...
1563 2024-11-19 300750 262.02 268.72 272.00 258.00 348129 9.211079e+09 5.32 2.21 5.80 0.89
1564 2024-11-20 300750 266.95 267.55 269.90 264.60 227088 6.060616e+09 1.97 -0.44 -1.17 0.58
1565 2024-11-21 300750 265.66 268.26 271.43 263.66 205508 5.510534e+09 2.90 0.27 0.71 0.53
1566 2024-11-22 300750 267.93 259.10 269.00 258.88 225551 5.937580e+09 3.77 -3.41 -9.16 0.58
1567 2024-11-25 300750 263.06 264.03 266.56 261.18 243545 6.426257e+09 2.08 1.90 4.93 0.62

1568 rows × 12 columns

需要前置知识:对数收益率。见上一章:组合的预期收益和方差的内容。

正如前面所说,使用涨跌幅就不用考虑分红、送股、复权等问题。

df['日期'] = pd.to_datetime(df['日期'])
df.set_index('日期',inplace = True)
df = df[['涨跌幅']]/100
df.fillna(0,inplace=True)
df.tail()
涨跌幅
日期
2024-11-19 0.0221
2024-11-20 -0.0044
2024-11-21 0.0027
2024-11-22 -0.0341
2024-11-25 0.0190

从上市之日,投资1元到今天的结果。

df['price'] = (df['涨跌幅'] + 1).cumprod() # 用涨跌幅反算出复权价,上市日前一天为1。
df
涨跌幅 price
日期
2018-06-11 0.8991 1.899100
2018-06-12 0.1547 2.192891
2018-06-13 0.1473 2.515904
2018-06-14 0.1412 2.871149
2018-06-15 0.1365 3.263061
... ... ...
2024-11-19 0.0221 39.304715
2024-11-20 -0.0044 39.131774
2024-11-21 0.0027 39.237430
2024-11-22 -0.0341 37.899434
2024-11-25 0.0190 38.619523

1568 rows × 2 columns

df.price.plot()

# 计算对数收益率
df['lrets'] = np.log(df['涨跌幅'] + 1)
df.tail()
涨跌幅 price lrets
日期
2024-11-19 0.0221 39.304715 0.021859
2024-11-20 -0.0044 39.131774 -0.004410
2024-11-21 0.0027 39.237430 0.002696
2024-11-22 -0.0341 37.899434 -0.034695
2024-11-25 0.0190 38.619523 0.018822
# 计算年化对数收益率

np.mean(df.lrets) * 250
0.5825506887464088
# 年化对数SD
np.std(df.lrets) * np.sqrt(250)
0.5810372150490056
# 简单算一下夏普比率: 每单位风险,能够获得多少超额收益。
rf = 0.03

(np.mean(df.lrets) * 250 - rf) / (np.std(df.lrets) * np.sqrt(250))
0.9509729745964822
# 写成函数
def sharpe_ratio(lrets, rf = 0.03):
    return (np.mean(lrets) * 250 - rf) / (np.std(lrets) * np.sqrt(250))

sharpe_ratio(df.lrets)
0.9509729745964822

28.1.2 均线计算

绘制均线,先随选2个日期

sma1 = 20
sma2 = 60

def sma(x,period):
    return x.rolling(period).mean()

def ema(x,period):
    return x.ewm(span=period,min_periods=0,adjust=False,ignore_na=False).mean()

df.loc[:,'sma1'] = sma(df.price,sma1)
df.loc[:,'sma2'] = sma(df.price,sma2)

df[['price','sma1','sma2']].plot(figsize=(8,4));

28.1.3 交易策略(多)

首先假定我们只能做多。

如果快均线高于慢均线:买入持有;反之空仓。

df['pos'] = np.where(df['sma1'] > df['sma2'],1,0) # np.where()生成一个序列,条件成立则1,反之则0
df.tail()
涨跌幅 price lrets sma1 sma2 pos
日期
2024-11-19 0.0221 39.304715 0.021859 37.928572 32.749686 1
2024-11-20 -0.0044 39.131774 -0.004410 38.062127 32.973834 1
2024-11-21 0.0027 39.237430 0.002696 38.223935 33.201540 1
2024-11-22 -0.0341 37.899434 -0.034695 38.225780 33.409036 1
2024-11-25 0.0190 38.619523 0.018822 38.282939 33.616868 1

把指标和持仓情况并列绘制

# 绘制子图,见前面的章节。height_ratios是子图的高度比例
fig, axes = plt.subplots(2,1,figsize=(8,4), gridspec_kw={'height_ratios': [3, 1]},sharex = True)

df[['price','sma1','sma2']].plot(ax = axes[0])
df[['pos']].plot(ax = axes[1])

plt.subplots_adjust(wspace=0.05,hspace=0.05)

计算策略的收益率序列。

  1. df['pos']中,1表示持仓,0表示空仓
  2. df['lrets']表示每天的对数收益率。
  3. df['lrets'] * df.lrets,两个序列相乘,就可以得到执行策略的收益率序列:空仓日收益率为0,持仓日收益率为lret。
  4. 今天出信号,收盘进行操作,实际收益在明天才能兑现。因此pos要滞后1天(重要:避免自我欺骗)
df['strategy'] = df['pos'].shift(1) * df.lrets

df.dropna(inplace=True)

df.round(4).head()
涨跌幅 price lrets sma1 sma2 pos strategy
日期
2018-09-03 -0.0424 4.0969 -0.0433 4.6636 4.7089 0 -0.0
2018-09-04 0.0118 4.1452 0.0117 4.6233 4.7463 0 0.0
2018-09-05 -0.0483 3.9450 -0.0495 4.5821 4.7755 0 -0.0
2018-09-06 0.0093 3.9817 0.0093 4.5325 4.7999 0 0.0
2018-09-07 0.0301 4.1016 0.0297 4.4874 4.8204 0 0.0

比较一下两者:

(注意我们的均线是随意选择的)

# 年化对数收益率
df[['lrets','strategy']].agg(np.mean)*250
/tmp/ipykernel_1619145/3403197569.py:2: FutureWarning: The provided callable <function mean at 0x7f48901454e0> is currently using DataFrame.mean. In a future version of pandas, the provided callable will be used directly. To keep current behavior pass the string "mean" instead.
  df[['lrets','strategy']].agg(np.mean)*250
lrets       0.364513
strategy    0.270553
dtype: float64
# 夏普比率
df[['lrets','strategy']].agg(sharpe_ratio)
lrets       0.679061
strategy    0.619436
dtype: float64

绘制两者的对比曲线:正如均线系统的特征,能够过滤掉大幅度的下跌,对顶部和底部的判断有明显的滞后。

df[['lrets','strategy']].cumsum().apply(np.exp).plot();

28.1.4 使用暴力优化

参数优化:寻找最佳参数

暴力参数优化:使用穷举法,寻找最佳参数。

%%capture --no-display 
# 这一行用于忽略本cell的warning

sma1 = 10
sma2 = 60
data = df[['price','lrets']]

# def test(sma1, sma2, data):
data['sma1'] = sma(data.price, sma1)
data['sma2'] = sma(data.price, sma2)

data['pos'] = np.where(data['sma1'] > data['sma2'], 1, 0)

data['strategy'] = data['pos'].shift(1) * data.lrets

data.dropna(inplace = True)

data.head()
price lrets sma1 sma2 pos strategy
日期
2018-12-03 5.524623 0.012027 5.261774 4.840392 1 0.000000
2018-12-04 5.642850 0.021174 5.298584 4.866158 1 0.021174
2018-12-05 5.566671 -0.013592 5.320604 4.889848 1 -0.013592
2018-12-06 5.375178 -0.035006 5.329461 4.913684 1 -0.035006
2018-12-07 5.430542 0.010247 5.375893 4.937831 1 0.010247

要获得这几样

  1. 股票原来的年化收益率和年化夏普比率
  2. 含有交易策略的年化收益率和年化夏普比率
asset_rets = np.mean(data.lrets) * 250
asset_sr = sharpe_ratio(data.lrets)

strat_rets = np.mean(data.strategy) * 250
start_sr = sharpe_ratio(data.strategy)

result = pd.DataFrame(dict(asset_rets=asset_rets,
                           asset_sr=asset_sr,
                           strat_rets=strat_rets,
                           start_sr=start_sr,
                           diff_rets = strat_rets - asset_rets,
                           diff_sr= start_sr - asset_sr
                           ), index=[0])

result
asset_rets asset_sr strat_rets start_sr diff_rets diff_sr
0 0.33734 0.638879 0.283323 0.66336 -0.054017 0.024481
%%capture --no-display 
# 这一行用于忽略本cell的warning

def test(sma1, sma2, data):
    data['sma1'] = sma(data.price, sma1)
    data['sma2'] = sma(data.price, sma2)

    data['pos'] = np.where(data['sma1'] > data['sma2'], 1, 0)

    data['strategy'] = data['pos'].shift(1) * data.lrets

    data.dropna(inplace = True)

    asset_rets = np.mean(data.lrets) * 250
    asset_sr = sharpe_ratio(data.lrets)

    strat_rets = np.mean(data.strategy) * 250
    start_sr = sharpe_ratio(data.strategy)

    result = pd.DataFrame(dict(sma1 = sma1 ,sma2=sma2, 股票收益率=asset_rets,
                            股票sr=asset_sr,
                            策略收益率=strat_rets,
                            策略sr=start_sr,
                            收益率差异 = strat_rets - asset_rets,
                            sr差异= start_sr - asset_sr
                            ), index=[0])

    return result

循环并罗列所有结果

%%capture --no-display 
# 这一行用于忽略本cell的warning

from itertools import product

sma1 = range(10,56,5) 
sma2 = range(60,251,10)

result = []
for SMA1, SMA2 in product(sma1,sma2):
    #print(f"{SMA1=},{SMA2=}")
    result.append(test(SMA1, SMA2, df[['price','lrets']]))

pd.concat(result).sort_values('策略sr',ascending=False).head(5)
sma1 sma2 股票收益率 股票sr 策略收益率 策略sr 收益率差异 sr差异
0 25 190 0.397790 0.755347 0.413784 0.960895 0.015994 0.205548
0 25 200 0.400890 0.759518 0.395225 0.908041 -0.005665 0.148523
0 30 200 0.400890 0.759518 0.391900 0.898265 -0.008991 0.138746
0 30 220 0.385459 0.724569 0.392378 0.893800 0.006919 0.169231
0 20 210 0.398149 0.752332 0.389805 0.888462 -0.008344 0.136131

利用暴力求解(穷举法),可以得到任何2组均线的策略收益率和策略夏普比率。可以带入前面的代码查看效果。

sma1 = 25
sma2 = 190
df.loc[:,'sma1'] = sma(df.price,sma1)
df.loc[:,'sma2'] = sma(df.price,sma2)

df['pos'] = np.where(df['sma1'] > df['sma2'],1,0)

fig, axes = plt.subplots(2,1,figsize=(8,4), gridspec_kw={'height_ratios': [3, 1]},sharex = True)
df[['price','sma1','sma2']].plot(ax = axes[0])
df[['pos']].plot(ax = axes[1])

plt.subplots_adjust(wspace=0.05,hspace=0.05)

df.loc[:,'strategy2'] = df['pos'].shift(1) * df.lrets

df.dropna(inplace=True)

df.round(4).head()

df[['lrets','strategy','strategy2''']].cumsum().apply(np.exp).plot();

28.1.5 交易策略(多空)

df['pos'] = np.where(df['sma1'] > df['sma2'],1,-1) # np.where()生成一个序列,条件成立则1,反之则-1
df.tail()
涨跌幅 price lrets sma1 sma2 pos strategy strategy2
日期
2024-11-19 0.0221 39.304715 0.021859 37.475094 28.582953 1 0.021859 0.021859
2024-11-20 -0.0044 39.131774 -0.004410 37.672332 28.678097 1 -0.004410 -0.004410
2024-11-21 0.0027 39.237430 0.002696 37.887477 28.776124 1 0.002696 0.002696
2024-11-22 -0.0341 37.899434 -0.034695 37.941567 28.862542 1 -0.034695 -0.034695
2024-11-25 0.0190 38.619523 0.018822 38.004139 28.947832 1 0.018822 0.018822
def test2(sma1, sma2, data):
    data['sma1'] = sma(data.price, sma1)
    data['sma2'] = sma(data.price, sma2)

    data['pos'] = np.where(data['sma1'] > data['sma2'], 1, -1) # 这里改了一下

    data['strategy'] = data['pos'].shift(1) * data.lrets

    data.dropna(inplace = True)

    asset_rets = np.mean(data.lrets) * 250
    asset_sr = sharpe_ratio(data.lrets)

    strat_rets = np.mean(data.strategy) * 250
    start_sr = sharpe_ratio(data.strategy)

    result = pd.DataFrame(dict(sma1 = sma1 ,sma2=sma2, 股票收益率=asset_rets,
                            股票sr=asset_sr,
                            策略收益率=strat_rets,
                            策略sr=start_sr,
                            收益率差异 = strat_rets - asset_rets,
                            sr差异= start_sr - asset_sr
                            ), index=[0])

    return result
%%capture --no-display 
# 这一行用于忽略本cell的warning

from itertools import product

sma1 = range(10,56,5) 
sma2 = range(60,251,10)

result = []
for SMA1, SMA2 in product(sma1,sma2):
    #print(f"{SMA1=},{SMA2=}")
    result.append(test2(SMA1, SMA2, df[['price','lrets']]))

pd.concat(result).sort_values('策略sr',ascending=False).head(5)
sma1 sma2 股票收益率 股票sr 策略收益率 策略sr 收益率差异 sr差异
0 25 190 0.329685 0.624619 0.481318 0.941669 0.151634 0.317050
0 25 200 0.335092 0.634279 0.447805 0.869269 0.112712 0.234990
0 30 200 0.335092 0.634279 0.435820 0.844256 0.100728 0.209977
0 20 200 0.335092 0.634279 0.414993 0.800806 0.079901 0.166527
0 15 200 0.335092 0.634279 0.401893 0.773485 0.066800 0.139206
sma1 = 25
sma2 = 190
df.loc[:,'sma1'] = sma(df.price,sma1)
df.loc[:,'sma2'] = sma(df.price,sma2)

df['pos'] = np.where(df['sma1'] > df['sma2'],1,-1)

df['strategy3'] = df['pos'].shift(1) * df.lrets

df.dropna(inplace=True)

df.round(4).head()

df[['lrets','strategy','strategy2','strategy3']].cumsum().apply(np.exp).plot();

28.1.6 注意事项

  1. 过去不代表未来,过去几年的最优策略,未来不一定成功(其实是基本不会成功)。
  2. 可以考虑采用滚动的窗口测试(略)

28.2 摆动指标

这里以RSI指标为例。

def rsi(close, periods = 14, ema = True):
    """
    Returns a pd.Series with the relative strength index.
    """
    close_delta = close.diff()

    # Make two series: one for lower closes and one for higher closes
    up = close_delta.clip(lower=0)
    down = -1 * close_delta.clip(upper=0)
    
    if ema == True:
        # Use exponential moving average
        ma_up = up.ewm(com = periods - 1, adjust=True, min_periods = periods).mean()
        ma_down = down.ewm(com = periods - 1, adjust=True, min_periods = periods).mean()
    else:
        # Use simple moving average
        ma_up = up.rolling(window = periods, adjust=False).mean()
        ma_down = down.rolling(window = periods, adjust=False).mean()
        
    rsi = ma_up / ma_down
    rsi = 100 - (100/(1 + rsi))
    return rsi
# 先把数据还原一下
df = df[['lrets','price']]
df
lrets price
日期
2020-03-27 -0.008637 8.616057
2020-03-30 0.005783 8.666030
2020-03-31 0.010049 8.753557
2020-04-01 0.000000 8.753557
2020-04-02 0.029267 9.013537
... ... ...
2024-11-19 0.021859 39.304715
2024-11-20 -0.004410 39.131774
2024-11-21 0.002696 39.237430
2024-11-22 -0.034695 37.899434
2024-11-25 0.018822 38.619523

1131 rows × 2 columns

RSI指标的周期,我们暂定为14天。(显然这个值也是可以被优化的)

rsi_14 = rsi(df.price)
rsi_14[-5:]
日期
2024-11-19    58.364785
2024-11-20    57.588244
2024-11-21    57.956284
2024-11-22    51.823190
2024-11-25    54.607269
Name: price, dtype: float64

绘图:

fig, axes = plt.subplots(2,1,figsize=(8,4), gridspec_kw={'height_ratios': [3, 1]},sharex = True)

df.price.plot(ax = axes[0])
rsi_14.plot(ax = axes[1])

plt.subplots_adjust(wspace=0.05,hspace=0.05)

简单的RSI指标策略:

  1. RSI小于某个值:买入,并持有到卖出。
  2. RSI大于某个值:卖出,并等待到买入。

这里随便选择2个阈值:(连同rsi的周期,现在有3个可优化的值)

对于这种“买入卖出信号”的类型,如何计算持仓状态?

  1. 从第一天开始,遍历每一天。
  2. 检查买入信号是否为1,是,则position[i] = 1;
  3. 检查卖出信号是否为1,是,则position[i] = 0;
  4. 2个信号都不存在,则保持前值。

注意:

  1. 这里的参数是随便选的,这里只是做演示。
  2. 任何其他摆动指标,比如kdj,做法类似
# 构造买卖信号
long_signal = (rsi_14 < 35)
short_signal = (rsi_14 > 70)
position = np.zeros_like(rsi_14)  # 创建一个rsi_14同等长度的序列,其中填满0,即默认空仓


# 遍历rsi_14,这里其实只是要个序号i = [0,1,2,3, .... ]
for i in range(len(rsi_14)):
    if np.isnan(long_signal[i]) or np.isnan(short_signal[i]):  # 2个信号有1个是na,就保持0
        position[i] = 0
    else:
        if long_signal[i]:  # 如果i这天有买入信号
            position[i] = 1  # i这天的持仓为1
        elif short_signal[i]:  # 如果i这个天有卖出信号
            position[i] = 0  # i这天的持仓为0
        else:
            position[i] = position[i-1] # 没信号,则持仓状态不变

df['pos'] = pd.DataFrame({'pos': position}, index=rsi_14.index) # 构造一个时间序列的DataFrame
df
/tmp/ipykernel_1619145/3571062792.py:6: FutureWarning: Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`
  if np.isnan(long_signal[i]) or np.isnan(short_signal[i]):  # 2个信号有1个是na,就保持0
/tmp/ipykernel_1619145/3571062792.py:9: FutureWarning: Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`
  if long_signal[i]:  # 如果i这天有买入信号
/tmp/ipykernel_1619145/3571062792.py:11: FutureWarning: Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`
  elif short_signal[i]:  # 如果i这个天有卖出信号
/tmp/ipykernel_1619145/3571062792.py:16: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df['pos'] = pd.DataFrame({'pos': position}, index=rsi_14.index) # 构造一个时间序列的DataFrame
lrets price pos
日期
2020-03-27 -0.008637 8.616057 0.0
2020-03-30 0.005783 8.666030 0.0
2020-03-31 0.010049 8.753557 0.0
2020-04-01 0.000000 8.753557 0.0
2020-04-02 0.029267 9.013537 0.0
... ... ... ...
2024-11-19 0.021859 39.304715 0.0
2024-11-20 -0.004410 39.131774 0.0
2024-11-21 0.002696 39.237430 0.0
2024-11-22 -0.034695 37.899434 0.0
2024-11-25 0.018822 38.619523 0.0

1131 rows × 3 columns

绘图:

fig, axes = plt.subplots(3,1,figsize=(8,5), gridspec_kw={'height_ratios': [3, 1,1]},sharex = True)

df.price.plot(ax = axes[0])
rsi_14.plot(ax = axes[1])
df.pos.plot(ax = axes[2])

plt.subplots_adjust(wspace=0.05,hspace=0.05)

np.mean(df.lrets * df.pos.shift(1)) * 250
0.07068670661700313
sharpe_ratio(df.lrets)
0.6246185760757821
sharpe_ratio(df.lrets * df.pos.shift(1))
0.1320369440119019
df.loc[:,'strat_rets'] = df.lrets * df.pos.shift(1)
/tmp/ipykernel_1619145/678097258.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df.loc[:,'strat_rets'] = df.lrets * df.pos.shift(1)
fig, axes = plt.subplots(3,1,figsize=(8,5), gridspec_kw={'height_ratios': [3, 1,1]},sharex = True)

df[['lrets','strat_rets']].cumsum().agg(np.exp).plot(ax = axes[0])
rsi_14.plot(ax = axes[1])
df.pos.plot(ax = axes[2])

plt.subplots_adjust(wspace=0.05,hspace=0.05)

可以优化的参数有三个,这些参数在本案中都是随机选取的:

  1. 天数
  2. 买入阈值
  3. 卖出阈值

做法和均线系统完全一样,只是从2个参数变成3个参数,这里从略。

稍微扩展一下:

  1. 在上升周期,RSI买卖信号的两个阈值可以提高(超买可以继续涨,回调不会跌太深),避免过早卖出,或者等不到买点;反之,两个信号的阈值可以降低。
  2. 那么怎么区分上升下降周期?无数个方法,可以从均线想起。

28.3 技术分析策略组合

最后说一下策略的简单组合,这是最简单的组合多个指标的方法。这里以”单均线系统”和前面的RSI系统为例:

一般性做法是:

  1. 每个系统,计算出各自的持仓情况。
  2. 对不同的系统的持仓情况,求不同的逻辑组合。

例如:你可能希望“2个指标系统同时显示持有,我才持有”:

  1. 价格在120均线上的同时:
  2. RSI保持持仓状态(参数同前)

那么,只要2个持仓情况直接“逻辑与”,或者直接相乘也可以。

当然,你可以设置更复杂的系统。比如

  1. 首先用宏观和趋势策略,判断先在处于上升周期还是下降周期。
  2. 上升周期,买入更宽松:任何一个条件成立都可以买入(逻辑或)
  3. 下行周期,买入更严格(或者不买入):所有条件都成立才可以买入(逻辑与)
  4. 各个区间参数还可以不一样,考虑的指标可以不一样(不考虑的指标只要乘以0即不起作用)

等等等等,各位自行发挥。

首先,各自计算2个系统的持仓。

# 单一均线系统的持仓情况
pos_ma = (df.price > sma(df.price,120)).shift(1)*1 # 注意要推迟1天
# rsi系统的持仓情况
pos_rsi = df['pos'] # 用前面的数据

按前面的逻辑:

  1. 我们用120日均线来做多看判断(120日是随便选的)
  2. 如果120均线多头,那么买入更加宽松:两个系统取或。注意:这个120均线多头,那么等于全程多头,rsi系统不起作用。这里只是用于演示,你的系统可能不是这样。
  3. 如果120均线多头,那么买入更加严格:两个系统取与。
pos_ma = pos_ma.fillna(0)
pos_final = pd.DataFrame(np.where(pos_ma==1,
                     np.logical_or(pos_ma.fillna(0),pos_rsi), 
                     np.logical_and(pos_ma.fillna(0),pos_rsi)),
                     index = pos_ma.index) * 1
/tmp/ipykernel_1619145/2990259507.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
  pos_ma = pos_ma.fillna(0)
fig, axes = plt.subplots(3,1,figsize=(8,5), gridspec_kw={'height_ratios': [1, 1,1]},sharex = True)
pos_ma.plot(ax=axes[0])
pos_rsi.plot(ax=axes[1])
pos_final.plot(ax=axes[2])

df['lrets_final'] = (pos_final.iloc[:,0] * df.lrets)
/tmp/ipykernel_1619145/2944535238.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df['lrets_final'] = (pos_final.iloc[:,0] * df.lrets)

df.fillna(0,inplace=True)

df[['lrets', 'lrets_final']].cumsum().agg(np.exp).tail(5)
/tmp/ipykernel_1619145/3761499522.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df.fillna(0,inplace=True)
lrets lrets_final
日期
2024-11-19 4.522567 2.452405
2024-11-20 4.502668 2.441614
2024-11-21 4.514825 2.448206
2024-11-22 4.360869 2.364722
2024-11-25 4.443726 2.409652
fig, axes = plt.subplots(4,1,figsize=(8,8), gridspec_kw={'height_ratios': [4, 1, 1, 1,]},sharex = True)
df[['lrets', 'lrets_final']].loc['2020-08':].cumsum().agg(np.exp).plot(ax=axes[0])
pos_ma.plot(ax=axes[1])
pos_rsi.plot(ax=axes[2])
pos_final.plot(ax=axes[3])

df[['lrets', 'lrets_final']].loc['2020-08':].agg(sharpe_ratio)
lrets          0.373549
lrets_final    0.479779
dtype: float64

很明显,现有4个参数:均线周期,rsi周期,rsi买入和卖出的阈值,都可以进行优化,各位可以自行摸索。