import akshare as ak
28 技术分析交易策略
- 时至今日,单纯的技术分析可能已经不那么好用,这里是技术分析在Python中如何实现思路的展示。
- 如果你自己有独特的技术分析策略,依然可以利用Python实现。
传统的技术分析技术怎么在Python中应用,包括绘制指标,以及交易的回测。
主要包括:
- 趋势交易:双均线系统为例
- 波动交易:RSI系统为例
- 指标综合:结合前两者
注意:以下策略均未考虑交易成本。如果要考虑,只要在交易日乘以一个系数(1-交易成本)即可。
28.1 简单移动平均策略
从最简单的双均线交易系统开始:设置2条均线,当快速均线位于慢速均线上方,则持有;反之则空仓。
- 绘制均线
- 计算交易信号
- 计算交易的收益率
- 绘制交易结果
28.1.1 读取数据
这里简单演示一下如何用akshare包来读取数据。akshare官方主页见https://www.akshare.xyz/。
如果要在你的电脑上安装akshare包(这里采用清华大学的镜像),有几种方法:
- 在jupyter notebook中,找一个python单元格,然后粘贴如下代码,并执行:
!pip install -i https://pypi.tuna.tsinghua.edu.cn/simple akshare
- 启动Anaconda Prompt(开始菜单中有),或者Mac系统的终端(Terminal),执行如下命令,则会从清华大学的镜像安装akshare到你的电脑上。
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple akshare
上述方法2选1,并且都只需要执行一次,以后都可以使用。
这里以锂电池龙头宁德时代为例。
# 读取宁德时代
# 见:https://akshare.xyz/data/stock/stock.html#id20
= ak.stock_zh_a_hist(symbol="300750",start_date='20180101',end_date='20241231', adjust='qfq')
df
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
需要前置知识:对数收益率。见上一章:组合的预期收益和方差的内容。
正如前面所说,使用涨跌幅就不用考虑分红、送股、复权等问题。
'日期'] = pd.to_datetime(df['日期'])
df['日期',inplace = True) df.set_index(
= df[['涨跌幅']]/100
df 0,inplace=True)
df.fillna( 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元到今天的结果。
'price'] = (df['涨跌幅'] + 1).cumprod() # 用涨跌幅反算出复权价,上市日前一天为1。
df[ 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()
# 计算对数收益率
'lrets'] = np.log(df['涨跌幅'] + 1)
df[ 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 |
# 计算年化对数收益率
* 250 np.mean(df.lrets)
0.5825506887464088
# 年化对数SD
* np.sqrt(250) np.std(df.lrets)
0.5810372150490056
# 简单算一下夏普比率: 每单位风险,能够获得多少超额收益。
= 0.03
rf
* 250 - rf) / (np.std(df.lrets) * np.sqrt(250)) (np.mean(df.lrets)
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个日期
= 20
sma1 = 60
sma2
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()
'sma1'] = sma(df.price,sma1)
df.loc[:,'sma2'] = sma(df.price,sma2)
df.loc[:,
'price','sma1','sma2']].plot(figsize=(8,4)); df[[
28.1.3 交易策略(多)
首先假定我们只能做多。
如果快均线高于慢均线:买入持有;反之空仓。
'pos'] = np.where(df['sma1'] > df['sma2'],1,0) # np.where()生成一个序列,条件成立则1,反之则0
df[ 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是子图的高度比例
= plt.subplots(2,1,figsize=(8,4), gridspec_kw={'height_ratios': [3, 1]},sharex = True)
fig, axes
'price','sma1','sma2']].plot(ax = axes[0])
df[['pos']].plot(ax = axes[1])
df[[
=0.05,hspace=0.05) plt.subplots_adjust(wspace
计算策略的收益率序列。
df['pos']
中,1表示持仓,0表示空仓df['lrets']
表示每天的对数收益率。df['lrets'] * df.lrets
,两个序列相乘,就可以得到执行策略的收益率序列:空仓日收益率为0,持仓日收益率为lret。- 今天出信号,收盘进行操作,实际收益在明天才能兑现。因此pos要滞后1天(重要:避免自我欺骗)
'strategy'] = df['pos'].shift(1) * df.lrets
df[
=True)
df.dropna(inplace
round(4).head() df.
涨跌幅 | 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 |
比较一下两者:
(注意我们的均线是随意选择的)
# 年化对数收益率
'lrets','strategy']].agg(np.mean)*250 df[[
/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
# 夏普比率
'lrets','strategy']].agg(sharpe_ratio) df[[
lrets 0.679061
strategy 0.619436
dtype: float64
绘制两者的对比曲线:正如均线系统的特征,能够过滤掉大幅度的下跌,对顶部和底部的判断有明显的滞后。
'lrets','strategy']].cumsum().apply(np.exp).plot(); df[[
28.1.4 使用暴力优化
参数优化:寻找最佳参数
暴力参数优化:使用穷举法,寻找最佳参数。
%%capture --no-display
# 这一行用于忽略本cell的warning
= 10
sma1 = 60
sma2 = df[['price','lrets']]
data
# def test(sma1, sma2, 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[
= True)
data.dropna(inplace
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 |
要获得这几样
- 股票原来的年化收益率和年化夏普比率
- 含有交易策略的年化收益率和年化夏普比率
= np.mean(data.lrets) * 250
asset_rets = sharpe_ratio(data.lrets)
asset_sr
= np.mean(data.strategy) * 250
strat_rets = sharpe_ratio(data.strategy)
start_sr
= pd.DataFrame(dict(asset_rets=asset_rets,
result =asset_sr,
asset_sr=strat_rets,
strat_rets=start_sr,
start_sr= strat_rets - asset_rets,
diff_rets = start_sr - asset_sr
diff_sr=[0])
), index
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):
'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[
= True)
data.dropna(inplace
= np.mean(data.lrets) * 250
asset_rets = sharpe_ratio(data.lrets)
asset_sr
= np.mean(data.strategy) * 250
strat_rets = sharpe_ratio(data.strategy)
start_sr
= pd.DataFrame(dict(sma1 = sma1 ,sma2=sma2, 股票收益率=asset_rets,
result =asset_sr,
股票sr=strat_rets,
策略收益率=start_sr,
策略sr= strat_rets - asset_rets,
收益率差异 = start_sr - asset_sr
sr差异=[0])
), index
return result
循环并罗列所有结果
%%capture --no-display
# 这一行用于忽略本cell的warning
from itertools import product
= range(10,56,5)
sma1 = range(60,251,10)
sma2
= []
result for SMA1, SMA2 in product(sma1,sma2):
#print(f"{SMA1=},{SMA2=}")
'price','lrets']]))
result.append(test(SMA1, SMA2, df[[
'策略sr',ascending=False).head(5) pd.concat(result).sort_values(
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组均线的策略收益率和策略夏普比率。可以带入前面的代码查看效果。
= 25
sma1 = 190
sma2 'sma1'] = sma(df.price,sma1)
df.loc[:,'sma2'] = sma(df.price,sma2)
df.loc[:,
'pos'] = np.where(df['sma1'] > df['sma2'],1,0)
df[
= plt.subplots(2,1,figsize=(8,4), gridspec_kw={'height_ratios': [3, 1]},sharex = True)
fig, axes 'price','sma1','sma2']].plot(ax = axes[0])
df[['pos']].plot(ax = axes[1])
df[[
=0.05,hspace=0.05) plt.subplots_adjust(wspace
'strategy2'] = df['pos'].shift(1) * df.lrets
df.loc[:,
=True)
df.dropna(inplace
round(4).head()
df.
'lrets','strategy','strategy2''']].cumsum().apply(np.exp).plot(); df[[
28.1.5 交易策略(多空)
'pos'] = np.where(df['sma1'] > df['sma2'],1,-1) # np.where()生成一个序列,条件成立则1,反之则-1
df[ 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):
'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[
= True)
data.dropna(inplace
= np.mean(data.lrets) * 250
asset_rets = sharpe_ratio(data.lrets)
asset_sr
= np.mean(data.strategy) * 250
strat_rets = sharpe_ratio(data.strategy)
start_sr
= pd.DataFrame(dict(sma1 = sma1 ,sma2=sma2, 股票收益率=asset_rets,
result =asset_sr,
股票sr=strat_rets,
策略收益率=start_sr,
策略sr= strat_rets - asset_rets,
收益率差异 = start_sr - asset_sr
sr差异=[0])
), index
return result
%%capture --no-display
# 这一行用于忽略本cell的warning
from itertools import product
= range(10,56,5)
sma1 = range(60,251,10)
sma2
= []
result for SMA1, SMA2 in product(sma1,sma2):
#print(f"{SMA1=},{SMA2=}")
'price','lrets']]))
result.append(test2(SMA1, SMA2, df[[
'策略sr',ascending=False).head(5) pd.concat(result).sort_values(
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 |
= 25
sma1 = 190
sma2 'sma1'] = sma(df.price,sma1)
df.loc[:,'sma2'] = sma(df.price,sma2)
df.loc[:,
'pos'] = np.where(df['sma1'] > df['sma2'],1,-1)
df[
'strategy3'] = df['pos'].shift(1) * df.lrets
df[
=True)
df.dropna(inplace
round(4).head()
df.
'lrets','strategy','strategy2','strategy3']].cumsum().apply(np.exp).plot(); df[[
28.1.6 注意事项
- 过去不代表未来,过去几年的最优策略,未来不一定成功(其实是基本不会成功)。
- 可以考虑采用滚动的窗口测试(略)
28.2 摆动指标
这里以RSI指标为例。
def rsi(close, periods = 14, ema = True):
"""
Returns a pd.Series with the relative strength index.
"""
= close.diff()
close_delta
# Make two series: one for lower closes and one for higher closes
= close_delta.clip(lower=0)
up = -1 * close_delta.clip(upper=0)
down
if ema == True:
# Use exponential moving average
= up.ewm(com = periods - 1, adjust=True, min_periods = periods).mean()
ma_up = down.ewm(com = periods - 1, adjust=True, min_periods = periods).mean()
ma_down else:
# Use simple moving average
= up.rolling(window = periods, adjust=False).mean()
ma_up = down.rolling(window = periods, adjust=False).mean()
ma_down
= ma_up / ma_down
rsi = 100 - (100/(1 + rsi))
rsi return rsi
# 先把数据还原一下
= df[['lrets','price']]
df 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(df.price)
rsi_14 -5:] rsi_14[
日期
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
绘图:
= plt.subplots(2,1,figsize=(8,4), gridspec_kw={'height_ratios': [3, 1]},sharex = True)
fig, axes
= axes[0])
df.price.plot(ax = axes[1])
rsi_14.plot(ax
=0.05,hspace=0.05) plt.subplots_adjust(wspace
简单的RSI指标策略:
- RSI小于某个值:买入,并持有到卖出。
- RSI大于某个值:卖出,并等待到买入。
这里随便选择2个阈值:(连同rsi的周期,现在有3个可优化的值)
对于这种“买入卖出信号”的类型,如何计算持仓状态?
- 从第一天开始,遍历每一天。
- 检查买入信号是否为1,是,则position[i] = 1;
- 检查卖出信号是否为1,是,则position[i] = 0;
- 2个信号都不存在,则保持前值。
注意:
- 这里的参数是随便选的,这里只是做演示。
- 任何其他摆动指标,比如kdj,做法类似
# 构造买卖信号
= (rsi_14 < 35)
long_signal = (rsi_14 > 70) short_signal
= np.zeros_like(rsi_14) # 创建一个rsi_14同等长度的序列,其中填满0,即默认空仓
position
# 遍历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
= 0
position[i] else:
if long_signal[i]: # 如果i这天有买入信号
= 1 # i这天的持仓为1
position[i] elif short_signal[i]: # 如果i这个天有卖出信号
= 0 # i这天的持仓为0
position[i] else:
= position[i-1] # 没信号,则持仓状态不变
position[i]
'pos'] = pd.DataFrame({'pos': position}, index=rsi_14.index) # 构造一个时间序列的DataFrame
df[ 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
绘图:
= plt.subplots(3,1,figsize=(8,5), gridspec_kw={'height_ratios': [3, 1,1]},sharex = True)
fig, axes
= axes[0])
df.price.plot(ax = axes[1])
rsi_14.plot(ax = axes[2])
df.pos.plot(ax
=0.05,hspace=0.05) plt.subplots_adjust(wspace
* df.pos.shift(1)) * 250 np.mean(df.lrets
0.07068670661700313
sharpe_ratio(df.lrets)
0.6246185760757821
* df.pos.shift(1)) sharpe_ratio(df.lrets
0.1320369440119019
'strat_rets'] = df.lrets * df.pos.shift(1) df.loc[:,
/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)
= plt.subplots(3,1,figsize=(8,5), gridspec_kw={'height_ratios': [3, 1,1]},sharex = True)
fig, axes
'lrets','strat_rets']].cumsum().agg(np.exp).plot(ax = axes[0])
df[[= axes[1])
rsi_14.plot(ax = axes[2])
df.pos.plot(ax
=0.05,hspace=0.05) plt.subplots_adjust(wspace
可以优化的参数有三个,这些参数在本案中都是随机选取的:
- 天数
- 买入阈值
- 卖出阈值
做法和均线系统完全一样,只是从2个参数变成3个参数,这里从略。
稍微扩展一下:
- 在上升周期,RSI买卖信号的两个阈值可以提高(超买可以继续涨,回调不会跌太深),避免过早卖出,或者等不到买点;反之,两个信号的阈值可以降低。
- 那么怎么区分上升下降周期?无数个方法,可以从均线想起。
28.3 技术分析策略组合
最后说一下策略的简单组合,这是最简单的组合多个指标的方法。这里以”单均线系统”和前面的RSI系统为例:
一般性做法是:
- 每个系统,计算出各自的持仓情况。
- 对不同的系统的持仓情况,求不同的逻辑组合。
例如:你可能希望“2个指标系统同时显示持有,我才持有”:
- 价格在120均线上的同时:
- RSI保持持仓状态(参数同前)
那么,只要2个持仓情况直接“逻辑与”,或者直接相乘也可以。
当然,你可以设置更复杂的系统。比如
- 首先用宏观和趋势策略,判断先在处于上升周期还是下降周期。
- 上升周期,买入更宽松:任何一个条件成立都可以买入(逻辑或)
- 下行周期,买入更严格(或者不买入):所有条件都成立才可以买入(逻辑与)
- 各个区间参数还可以不一样,考虑的指标可以不一样(不考虑的指标只要乘以0即不起作用)
等等等等,各位自行发挥。
首先,各自计算2个系统的持仓。
# 单一均线系统的持仓情况
= (df.price > sma(df.price,120)).shift(1)*1 # 注意要推迟1天 pos_ma
# rsi系统的持仓情况
= df['pos'] # 用前面的数据 pos_rsi
按前面的逻辑:
- 我们用120日均线来做多看判断(120日是随便选的)
- 如果120均线多头,那么买入更加宽松:两个系统取或。注意:这个120均线多头,那么等于全程多头,rsi系统不起作用。这里只是用于演示,你的系统可能不是这样。
- 如果120均线多头,那么买入更加严格:两个系统取与。
= pos_ma.fillna(0)
pos_ma = pd.DataFrame(np.where(pos_ma==1,
pos_final 0),pos_rsi),
np.logical_or(pos_ma.fillna(0),pos_rsi)),
np.logical_and(pos_ma.fillna(= pos_ma.index) * 1 index
/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)
= plt.subplots(3,1,figsize=(8,5), gridspec_kw={'height_ratios': [1, 1,1]},sharex = True)
fig, axes =axes[0])
pos_ma.plot(ax=axes[1])
pos_rsi.plot(ax=axes[2]) pos_final.plot(ax
'lrets_final'] = (pos_final.iloc[:,0] * df.lrets) df[
/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)
0,inplace=True)
df.fillna(
'lrets', 'lrets_final']].cumsum().agg(np.exp).tail(5) df[[
/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 |
= plt.subplots(4,1,figsize=(8,8), gridspec_kw={'height_ratios': [4, 1, 1, 1,]},sharex = True)
fig, axes 'lrets', 'lrets_final']].loc['2020-08':].cumsum().agg(np.exp).plot(ax=axes[0])
df[[=axes[1])
pos_ma.plot(ax=axes[2])
pos_rsi.plot(ax=axes[3]) pos_final.plot(ax
'lrets', 'lrets_final']].loc['2020-08':].agg(sharpe_ratio) df[[
lrets 0.373549
lrets_final 0.479779
dtype: float64
很明显,现有4个参数:均线周期,rsi周期,rsi买入和卖出的阈值,都可以进行优化,各位可以自行摸索。