
研报复现:技术分析算法、框架与实战
《破解“看图”之谜:技术分析算法、框架与实战》是中泰证券于2021年8月发布的一份金融工程报告,旨在以科学方法系统化、自动化地识别技术图形,并验证其在实际市场中的有效性。
研报简读
一、研究背景与意义
技术分析长期被视为主观性强、缺乏统一标准的分析方法。报告指出,其不被广泛接受的主要原因有二:一是缺乏固定算法,导致不同分析者对同一图表解读不一;二是多数文献未系统论证技术分析适用的市场环境与实践效果。为此,报告借鉴Andrew W Lo等学者在2000年提出的方法,构建了一套科学、可重复的技术分析框架。
二、算法原理:核回归与极值点识别
报告采用核回归(Nadaraya-Watson估计) 对价格序列进行平滑处理,以消除噪声并准确提取极值点。通过高斯核函数和交叉验证法选择最优带宽,确保平滑效果既不过度也不不足。极值点通过导数符号变化识别,进而根据其排列与高低关系定义各类技术形态(如头肩底、双顶、三角形等)。
三、形态定义与识别
报告系统定义了几类常见技术形态(头肩顶/底、发散形态、三角形、矩形、双顶/底),并通过五个连续极值点的相对位置和幅度关系进行数学刻画。识别过程包括:平滑价格序列 → 求导找极值 → 按定义匹配形态。
四、实证分析:头肩底形态的有效性
报告重点验证了“头肩底(IHS)”形态在行业指数和宽基指数上的表现(2010–2021年数据),发现:
- 在指数层面,多数行业在形态出现后10日内获得正收益,胜率普遍超过50%,部分行业(如化工、汽车、电气设备)胜率可达70%以上。
- 结合均线条件(如MA5>MA20) 可进一步提升胜率。
- 在个股层面,头肩底形态表现不稳定,胜率分布接近随机(以50%为中位数),提示技术分析在个股择时中需谨慎使用。
五、结论与风险提示
技术分析在指数择时中具有显著价值,尤其在中期(10日)展望中表现稳健;但在个股层面效果有限,需结合其他分析方法。报告强调,所有结论基于历史数据统计,存在滞后性、模型局限性和市场环境变化风险,投资者应谨慎参考。
总结
该报告通过算法化、系统化的方法,为技术分析提供了科学的实证基础,打破了其“主观艺术”的传统印象,尤其在指数投资中展现出实用价值,为量化投资与技术分析的结合提供了重要参考。
代码复现
论文介绍:《Foundations of Technical Analysis》
Andrew W Lo、Harry Mamaysky和王江提出了一种系统化和自动化的方法,使用非参数核回归方法来进行模式识别,并将该方法应用于1962年至 1996 年的美股数据,以评估技术分析的有效性。通过将股票收益的无条件经验分布与条件分布(给定特定的技术指标,例如:头肩底形态、双底形态)作比较,发现在 31 年的样本期 间内,部分技术指标确实提供了有用的信息,具有实用价值。 与基本面分析不同,技术分析一直以来饱受争议。
然而一些学术研究表明,技术分析能从市场价格中提取 有用的信息。例如,Lo and MacKinlay (1988, 1999)证实了每周的美股指数并非随机游走,过去的价格可以在某种 程度上预测未来收益。技术分析和传统金融工程的一个重要区别在于,技术分析主要通过观察图表进行,而量化金融则依赖于相对完善的数值法。因此,技术分析利用几何工具和形态识别,而量化金融运用数学分析和概率统计。
随着近年来金融工程、计算机技术和数值算法等领域的突破,金融工程可以逐步取代不那么严谨的技术分析。技术分析虽饱受质疑却仍能占据一席之地,归功于其视觉分析模式更贴近直观认知,而且在过去,要进行技术分析,首先要认识到价格过程是非线性的,且包含一定的规律和模式。为了定量地捕捉这种规 律,我们首先假定价格过程尚未有严格的算法取代传统的技术分析,如今,成熟的统计算法能够取代传统的几何画图,让技术分析继续以 更新、更严谨的方式服务投资者,同时金融工程领域在分析范式上也得到了丰富。
形态识别算法
核估计方法
假定价格过程Pt有如下表达形式:
其中Xt是状态变量,m(Xt)是任意固定但位置的非线性函数,
为白噪声。
为了识别模式,我们令状态变量等于时间Xt = t,此时,需要用一个光滑函数
。近似价格过程为了与核回归估计文献中的符号保持一致,我们仍将状态变量记为Xt。
还需要一个形态识别的算法,以自动识别指数指标。一旦有了算法,就可以应用于不同时段的资产价格,从而评估不同技术指标的有效性。
平滑估计量
其中离x较近的Xt对应的Pt拥有较大的权重
。对于距离的选择,太宽会导致估计量过于平滑而无法显示出
真正的特性,太窄又会导致估计量的波动较大,无法排除噪声的影响。因此需要通过选择合适的权重
来平衡以上两点。
核回归
形态识别算法
1、头肩形态(头肩顶和头肩底)
2、发散形态(顶部发散和底部发散)
3、三角形
4、矩形
形态识别测试
中国宝安
data1 = get_price('000009.XSHE', start_date='2021-01-21', end_date='2021-12-31',
fields=['open', 'close', 'low', 'high'], panel=False)
patterns_record1 = rolling_patterns2pool(data1['close'],n=35)
plot_patterns_chart(data1,patterns_record1,True,False)
plt.title('中国宝安')
plot_patterns_chart(data1,patterns_record1,True,True);
江淮汽车
data2 = get_price('600418.XSHG', start_date='2021-01-21', end_date='2021-12-31',
fields=['open', 'close', 'low', 'high'], panel=False)
patterns_record2 = rolling_patterns2pool(data1['close'],n=35)
plot_patterns_chart(data1,patterns_record2,True,False)
plt.title('江淮汽车')
plot_patterns_chart(data1,patterns_record2,True,True);
沪深300
hs300 = get_price('000300.XSHG', start_date='2014-01-01', end_date='2021-12-31',
fields=['open', 'close', 'low', 'high'], panel=False)
# 这里周期周期较长 加入reset_window设置字典更新频率
patterns_record3 = rolling_patterns2pool(hs300['close'],n=35,reset_window=120)
plot_patterns_chart(hs300,patterns_record3,True,False)
plt.title('沪深300')
plot_patterns_chart(hs300,patterns_record3,True,True);
申万一级行业形态识别情况
def patterns_res2json(dic: Dict) -> str:
"""将结果转为json
Args:
dic (Dict): 结果字典
Returns:
str
"""
def json_serial(obj):
"""JSON serializer for objects not serializable by default json code"""
if isinstance(obj, np.datetime64):
return pd.to_datetime(obj).strftime('%Y-%m-%d')
if isinstance(obj, np.ndarray):
return list(map(lambda x: pd.to_datetime(x).strftime('%Y-%m-%d'), obj.tolist()))
return json.dumps(dic, default=json_serial, ensure_ascii=False)
def pretreatment_events(factor: pd.DataFrame, returns: pd.DataFrame, before: int, after: int) -> pd.DataFrame:
"""预处理事件,将其拉到同一时间
Args:
factor (pd.DataFrame): MuliIndex level0-date level1-asset
returns (pd.DataFrame): index-datetime columns-asset
before (int): 事件前N日
after (int): 事件后N日
Returns:
pd.DataFrame: [description]
"""
all_returns = []
for timestamp, df in factor.groupby(level='date'):
equities = df.index.get_level_values('asset')
try:
day_zero_index = returns.index.get_loc(timestamp)
except KeyError:
continue
starting_index = max(day_zero_index - before, 0)
ending_index = min(day_zero_index + after + 1,
len(returns.index))
equities_slice = set(equities)
series = returns.loc[returns.index[starting_index:ending_index],
equities_slice]
series.index = range(starting_index - day_zero_index,
ending_index - day_zero_index)
all_returns.append(series)
return pd.concat(all_returns, axis=1)
def get_event_cumreturns(pretreatment_events: pd.DataFrame) -> pd.DataFrame:
"""以事件当日为基准的累计收益计算
Args:
pretreatment_events (pd.DataFrame): index-事件前后日 columns-asset
Returns:
pd.DataFrame
"""
df = pd.DataFrame(index=pretreatment_events.index,
columns=pretreatment_events.columns)
df.loc[:0] = pretreatment_events.loc[:0] / pretreatment_events.loc[0] - 1
df.loc[1:] = pretreatment_events.loc[0:] / pretreatment_events.loc[0] - 1
return df
def get_industry_price(codes: Union[str, List], start: str, end: str) -> pd.DataFrame:
"""获取行业指数日度数据. 限制获取条数Limit=4000
Args:
codes (Union[str,List]): 行业指数代码
start (str): 起始日
end (str): 结束日
Returns:
pd.DataFrame: 日度数据
"""
def query_func(code: str, start: str, end: str) -> pd.Series:
return finance.run_query(query(finance.SW1_DAILY_PRICE).filter(finance.SW1_DAILY_PRICE.code == code,
finance.SW1_DAILY_PRICE.date >= start,
finance.SW1_DAILY_PRICE.date <= end))
if isinstance(codes, str):
codes = [codes]
return pd.concat((query_func(code, start, end) for code in codes))
def calc_events_ret(ser: pd.Series, pricing: pd.DataFrame, before: int = 3, end: int = 10, group: bool = True) -> Union[pd.Series, pd.DataFrame]:
""" 计算形态识别前累计收益率情况
Args:
ser (pd.Series): _description_
pricing (pd.DataFrame): 价格数据 index-date columns-指数
before (int, optional): 识别前N日. Defaults to 3.
end (int, optional): 识别后N日. Defaults to 10.
group (bool, optional): 是否分组. Defaults to True.
Returns:
Union[pd.Series, pd.DataFrame]
"""
events = pretreatment_events(ser, pricing, before, end)
rets = get_event_cumreturns(events)
if group:
return rets.mean(axis=1)
else:
return rets
def get_win_rate(df: pd.DataFrame) -> pd.DataFrame:
"""计算胜率
Args:
df (pd.DataFrame): index-days columns
Returns:
pd.DataFrame
"""
return df.apply(lambda x: np.sum(np.where(x > 0, 1, 0)) / x.count(), axis=1)
def get_pl(df:pd.DataFrame)->pd.DataFrame:
"""计算盈亏比
Returns:
pd.DataFrame
"""
return df.apply(lambda x:x[x>0].mean() / x[x<0].mean(),axis=1)
def plot_events_ret(ser: pd.Series, title: str = '', ax=None):
"""绘制事件收益率图
Args:
ser (pd.Series): 收益率序列
ax (_type_, optional):Defaults to None.
Returns:
ax
"""
if ax is None:
fig, ax = plt.figure(figsize=(18, 4))
line_ax = ser.plot(ax=ax, marker='o', title=title)
line_ax.yaxis.set_major_formatter(
mpl.ticker.FuncFormatter(lambda x, pos: '%.2f%%' % (x * 100)))
line_ax.set_xlabel('天')
line_ax.set_ylabel('平均收益率')
ax.axvline(0, ls='--', color='black')
return ax
# 获取申万一级行业列表
indstries_frame = get_industries(name='sw_l1', date=None)
industry_price = get_industry_price(indstries_frame.index.tolist(),'2014-01-01','2022-02-18')
# 数据储存
industry_price.to_csv('sw_lv1.csv')
# 读取申万一级行业数据
industry_price = pd.read_csv('sw_lv1.csv', index_col=[
'name', 'date'], parse_dates=True).drop(columns='Unnamed: 0')
industry_price.head()
# time 2:14:46
## 形态识别数量受窗口期 及 更新字典的窗口 大小影响
dic = {} # 储存形态识别结果
for name,df in tqdm(industry_price.groupby(level='name')):
if len(df) > 120:
dic[name] = rolling_patterns2pool(df.loc[name,'close'],35,reset_window = 120)._asdict()
# 将结果储存为json
res_json =patterns_res2json(dic)
# 数据储存
with open('res_json.json','w',encoding='utf-8') as file:
json.dump(res_json,file)
# 读取 形态识别后的文件
with open('res_json.json','r',encoding='utf-8') as file:
res_json = json.load(file)
# 获取交易日历
trade_calendar = get_trade_days('2013-06-01','2022-02-21')
idx = pd.to_datetime(trade_calendar)
row_data = [] # 获取形态识别时的时点
res_dic = json.loads(res_json) # json转为字典
for code,res1 in res_dic.items():
for pattern_name,point_tuple in res1['patterns'].items():
for p1,p2 in point_tuple:
watch_date = idx.get_loc(p2) + 3 # 模拟三天后识别形态
row_data.append([code,pattern_name,idx[watch_date]])
# 转为frame格式
stats_df = pd.DataFrame(row_data,columns=['指数','形态','时间'])
stats_df['value'] = 1
factor_df = pd.pivot_table(stats_df,index=['时间','指数'],columns=['形态'],values='value')
factor_df = factor_df.sort_index()
factor_df.index.names = ['date','asset']
pricing = pd.pivot_table(industry_price.reset_index(),
index='date', columns='name', values='close')
形态识别后,前后平均收益情况
# TODO:收益减去指数自身 评价其超额情况 否则无法真实评价
group_ret = factor_df.groupby(level=0, axis=1).apply(
lambda x: calc_events_ret(x.dropna(), pricing))
size = group_ret.shape[1]
fig, axes = plt.subplots(size, figsize=(18, 4 * size))
axes = axes.flatten()
for ax, (name, ser) in zip(axes, group_ret.items()):
plot_events_ret(ser, name, ax)
plt.subplots_adjust(hspace=0.4)
# 计算胜率
evet_ret = factor_df.groupby(level=0, axis=1).apply(
lambda x: calc_events_ret(x.dropna(), pricing, group=False))
grouped = evet_ret.groupby(level=[0, 1], axis=1)
# 计算胜率
win_ratio = grouped.apply(get_win_rate).loc[[3, 5, 10]].T.swaplevel().sort_index()
# 计算盈亏比
pl_df = grouped.apply(get_pl).loc[[3, 5, 10]].T.swaplevel().sort_index()
win_ratio.columns = pd.MultiIndex.from_tuples([('胜率',3),('胜率',5),('胜率',10)])
pl_df.columns = pd.MultiIndex.from_tuples([('盈亏比',3),('盈亏比',5),('盈亏比',10)])
pattern_count = factor_df.groupby(level=1).sum().stack()
stats = pd.concat((win_ratio,pl_df),axis=1)
stats[('识别次数','All')] = pattern_count
图片
本文转载自灵度智能,作者:灵度智能
