1. 形态匹配:让历史K线成为你的“预言家”
朋友们,你们有没有过这样的经历?盯着一只股票的K线图,看着眼前这个似曾相识的形态,心里直犯嘀咕:“这个走势,我好像在哪里见过?” 那种感觉,就像在记忆的迷宫里寻找一个模糊的影子。传统的技术指标,像MACD、KDJ,它们是基于数学公式的计算,告诉你“金叉”、“死叉”,但很少能回答一个更本质的问题:历史上,当出现和现在“长得像”的K线组合后,后续走势大概率会怎样?
这就是形态匹配技术要解决的核心问题。它不关心复杂的公式,只关心“形状”。它的逻辑朴素而强大:市场行为会重复,人性亘古不变。 贪婪与恐惧编织出的价格轨迹,总会在历史的长河中留下相似的波纹。我们所要做的,就是用一个“标尺”,从海量的历史数据里,把这些相似的波纹找出来,然后统计它们之后发生了什么。
听起来很玄乎?其实原理就像图片搜索。你上传一张小狗的照片,搜索引擎会在图库里找到所有看起来像狗的照片。形态匹配也一样,你框选一段当前的K线走势(我们称之为“查询形态”),程序就会在成千上万只股票、长达数年的历史数据中,滑动一个同样大小的“窗口”,逐一比对,找出那些“长相”最接近的历史片段。
这个“长相”的评判标准,就是相似度。它不是简单比较价格的绝对高低,而是比较价格序列的“相对形状”。比如,一段先跌后涨再盘整的“N字形”走势,无论它发生在10块钱的股票上还是100块的股票上,只要波动轮廓相似,就应该被识别为同类。这背后通常需要对价格序列进行标准化处理,消除绝对价格的影响,只保留波动形态。
我最初接触这个想法,是因为受够了事后诸葛亮的分析。每次大涨大跌后,总有人跳出来指着图表说:“看!这就是经典的‘头肩底’!” 但问题是,在形态完全走出来的那一刻,你才能确认它是“头肩底”。我们真正需要的是,在形态正在形成或刚刚形成时,就能从历史中找到类似的案例,看看接下来上涨的概率有多大。这才是具有实战意义的“数据挖掘”。
所以,今天我要分享的,就是如何利用我们手边最易得的工具——通达信的日线数据,结合Python和一些核心算法,搭建一个属于自己的、能“看懂”K线形态的智能扫描工具。这不仅仅是学习一个工具,更是建立一种“历史会说话”的数据思维。
2. 实战准备:获取数据与搭建环境
工欲善其事,必先利其器。我们的“矿石”是通达信数据,“熔炉”是Python环境。别担心,整个过程我会一步步拆解,即便你是编程新手,跟着做也能搞定。
2.1 获取“原料”:导出通达信日线数据
通达信软件本身并不直接提供批量导出历史数据的功能,但其本地目录里其实藏着宝库。通常,通达信的日线数据存放在安装目录下的 vipdoc 文件夹里。里面按照市场(如sh、sz)和数据类型(如lday存放日线)有详细的子目录。
日线数据文件是.day格式,这是一种通达信自定义的二进制格式。每个文件对应一只股票,文件名就是股票代码。文件里面按顺序存储了日期、开盘价、最高价、最低价、收盘价、成交额、成交量等信息。
我们的第一步,就是读取这些.day文件,并把它们转换成我们熟悉的、便于处理的格式,比如CSV,或者直接存入数据库。这里我强烈推荐使用SQLite数据库,它轻量、无需安装服务器,一个文件就是整个数据库,管理和迁移都非常方便。
我写了一个Python函数来处理这个转换过程。你需要先安装几个必要的库:struct(Python自带,用于解析二进制)、pandas和sqlite3。
import struct
import pandas as pd
import sqlite3
import os
from datetime import datetime
def tdx_day_file_to_df(file_path):
"""
读取通达信.day文件,返回包含日期和价格信息的DataFrame。
"""
data_list = []
with open(file_path, 'rb') as f:
buffer = f.read()
# 每32个字节为一条记录
record_size = 32
for i in range(0, len(buffer), record_size):
record = buffer[i:i+record_size]
if len(record) < record_size:
break
# 使用struct解包二进制数据,注意字节顺序是little-endian
# 格式:日期(4字节), 开盘价(4字节), 最高价, 最低价, 收盘价, 成交额, 成交量, 保留
date_raw, open_price, high, low, close, amount, vol, _ = struct.unpack('<IIIIIfII', record)
# 处理日期:通达信日期是YYYYMMDD格式的整数
year = date_raw // 10000
month = (date_raw % 10000) // 100
day = date_raw % 100
date_str = f"{year:04d}-{month:02d}-{day:02d}"
# 价格单位通常是0.01元(分),需要除以100
data_list.append({
'date': date_str,
'open': open_price * 0.01,
'high': high * 0.01,
'low': low * 0.01,
'close': close * 0.01,
'amount': amount, # 成交额,单位元
'volume': vol # 成交量,单位手
})
df = pd.DataFrame(data_list)
df['date'] = pd.to_datetime(df['date'])
df.set_index('date', inplace=True)
return df
def import_all_stock_data(tdx_data_dir, db_path='stock_data.db'):
"""
批量导入指定目录下所有.day文件到SQLite数据库。
tdx_data_dir: 通达信vipdoc/lday目录路径,例如 'D:/new_tdx/vipdoc/sh/lday'
"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 创建表
cursor.execute('''
CREATE TABLE IF NOT EXISTS tdx_stock_daily (
code TEXT,
trade_date TEXT,
open REAL,
high REAL,
low REAL,
close REAL,
amount REAL,
volume INTEGER,
PRIMARY KEY (code, trade_date)
)
''')
imported_count = 0
for filename in os.listdir(tdx_data_dir):
if filename.endswith('.day'):
stock_code = filename.replace('.day', '')
file_path = os.path.join(tdx_data_dir, filename)
try:
df = tdx_day_file_to_df(file_path)
# 为DataFrame添加股票代码列
df['code'] = stock_code
df.reset_index(inplace=True)
# 写入数据库
df.to_sql('tdx_stock_daily', conn, if_exists='append', index=False)
imported_count += 1
print(f"已导入: {stock_code}, 数据条数: {len(df)}")
except Exception as e:
print(f"导入{stock_code}失败: {e}")
conn.commit()
conn.close()
print(f"全部导入完成!共导入{imported_count}只股票数据。")
# 使用示例:请将路径替换为你自己的通达信数据路径
# import_all_stock_data('D:/new_tdx/vipdoc/sh/lday', 'data.db')
# import_all_stock_data('D:/new_tdx/vipdoc/sz/lday', 'data.db') # 深圳市场
运行这个脚本,喝杯茶的功夫,你本地就会生成一个包含了所有股票历史日线数据的 data.db 文件。这就是我们后续进行数据挖掘的“金矿”。
2.2 搭建“熔炉”:Python环境与核心库
有了数据,我们需要一个强大的分析引擎。Python是首选,因为它有极其丰富的数据科学库。你需要确保安装以下库:
- pandas: 数据处理和分析的瑞士军刀。
- numpy: 提供高效的数组运算,是很多科学计算库的基础。
- matplotlib: 用于绘制K线图和结果可视化。
- scipy 或 dtaidistance: 用于计算序列之间的距离或相似度。我这里更推荐
dtaidistance,因为它对动态时间规整(DTW)算法有很好的实现,而DTW在衡量两个长度可能略有差异的时间序列相似性时特别有效。
安装命令很简单,打开你的命令行(CMD或终端),输入:
pip install pandas numpy matplotlib dtaidistance
如果安装速度慢,可以使用国内镜像源,比如清华源:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pandas numpy matplotlib dtaidistance。
环境搭好,数据入库,我们的“矿场”和“精炼厂”就准备就绪了。接下来,就是最核心的部分:如何定义和计算两根K线序列的“相似度”。
3. 核心算法:如何量化K线的“相似”
当我们说两段K线“像”的时候,到底在指什么?是每天涨跌幅一模一样吗?那几乎不可能。我们指的是它们波动的“轮廓”、“趋势”和“相对位置”相似。比如,都是一段温和上涨后急跌,再缓慢修复。这就需要一种算法,能忽略细微的日期错位和幅度差异,抓住形态的本质。
3.1 数据标准化:消除绝对价格的干扰
第一步永远是标准化。我们不能直接比较贵州茅台(股价上千)和某只ST股(股价几块)的收盘价序列。我们需要把它们的价格波动“缩放”到同一个可比较的尺度上。最常用的方法是Z-score标准化,也叫标准差标准化。它使得处理后的数据均值为0,标准差为1,完美地保留了原始序列的形状。
import numpy as np
def normalize_sequence(price_sequence):
"""
对价格序列进行Z-score标准化。
"""
arr = np.array(price_sequence, dtype=np.float64)
mean = np.mean(arr)
std = np.std(arr)
# 防止标准差为0的情况(例如序列所有值相同)
if std == 0:
return np.zeros_like(arr)
return (arr - mean) / std
经过标准化,一段从100涨到110的序列,和一段从10涨到11的序列,会被处理成几乎一样的形状。我们比较的,纯粹是“波动模式”。
3.2 相似度度量:从欧氏距离到动态时间规整(DTW)
最简单的比较方法是计算欧氏距离。把两个标准化后的序列看成两个向量,计算它们对应点差值的平方和再开方。距离越小,越相似。但这种方法要求两个序列严格等长且对齐。现实中,我们框选的形态和历史上某个形态,可能上涨的节奏快慢稍有不同,欧氏距离就会认为它们很不像。
这就需要引入更强大的算法——动态时间规整(DTW)。DTW的神奇之处在于,它允许序列在时间轴上进行非线性的“拉伸”或“压缩”,以找到最佳的对齐方式,然后再计算距离。这完美契合了K线形态比较的需求:一段用5天完成的上涨,和另一段用7天完成的类似上涨,DTW能识别出它们的相似性。
使用 dtaidistance 库计算DTW距离非常方便:
from dtaidistance import dtw
def calculate_similarity_dtw(seq1, seq2):
"""
使用DTW计算两个序列的距离,并将其转换为0-1之间的相似度分数。
DTW距离越小,序列越相似。我们将其映射为相似度(值越大越相似)。
"""
# 计算DTW距离
distance = dtw.distance(seq1, seq2)
# 简单的距离转相似度:相似度 = 1 / (1 + 距离)
# 也可以使用指数衰减等更复杂的方法
similarity = 1.0 / (1.0 + distance)
return similarity
这里我把距离转换成了0到1之间的相似度分数,1表示完全相同,越接近0差异越大。这个转换方式你可以根据实际效果调整。在实际应用中,我发现对于标准化后的序列,DTW距离在0-5之间比较常见,所以用 1/(1+distance) 映射效果还不错。
3.3 滑动窗口搜索:遍历历史的每一个角落
现在,我们有了“查询形态”(一段标准化后的价格序列),也有了衡量相似度的尺子(DTW)。接下来,就要在浩瀚的历史数据中大海捞针了。
方法就是滑动窗口。对于数据库里的每一只股票,我们从其最早的数据开始,截取一段和查询形态长度完全相同的窗口,计算相似度。然后窗口向后滑动一天,再计算,如此循环,直到这只股票的数据末尾。
def sliding_window_search(stock_data_series, query_pattern, threshold=0.8):
"""
在单只股票的价格序列上进行滑动窗口匹配。
stock_data_series: 单只股票标准化后的收盘价序列(一维数组)。
query_pattern: 查询形态(标准化后的序列)。
threshold: 相似度阈值,高于此值才记录。
"""
window_size = len(query_pattern)
data_length = len(stock_data_series)
matches = []
if data_length < window_size:
return matches
for start_idx in range(data_length - window_size + 1):
window = stock_data_series[start_idx: start_idx + window_size]
similarity = calculate_similarity_dtw(query_pattern, window)
if similarity >= threshold:
matches.append({
'start_index': start_idx,
'similarity': similarity,
'window_data': window.tolist() # 可选,记录匹配到的窗口数据
})
return matches
这个过程计算量很大。假设有4000只股票,每只股票有3000个交易日,查询形态长度为20,那么就需要进行大约 4000 * (3000-20) ≈ 1200万次 的DTW计算。DTW计算本身也不是常数时间。所以,性能优化是关键。我们可以通过多进程并行(multiprocessing库)来加速,每只股票或每个股票池分配一个进程独立计算。
4. 从匹配到策略:统计分析与实战应用
找到了所有相似的形态,工作只完成了一半。更重要的是分析这些“历史重演”之后,发生了什么。这才是形态匹配的价值所在——基于历史数据的概率统计。
4.1 定义“成功”与统计概率
什么是成功的交易?这个定义因人而异。在我的实践中,我通常关注匹配形态出现后,未来N个交易日(比如5天、10天)内的表现。可以设定几个维度的统计:
- 上涨概率:未来N日收盘价高于匹配形态结束日收盘价的次数占比。
- 平均收益率:未来N日的平均涨跌幅。
- 超额收益概率:未来N日涨幅超过某个阈值(如3%、5%)的次数占比。
- 最大回撤:评估潜在风险。
我们需要为前面找到的每一个匹配结果,计算其后续表现。这里有一个细节要注意:匹配到的历史形态可能发生在很久以前,我们必须确保在计算未来收益时,有足够的历史数据(即匹配结束日期之后至少有N个交易日的数据)。
def calculate_future_performance(db_conn, stock_code, match_end_date, future_days=5):
"""
计算匹配形态结束后未来N日的表现。
返回一个字典,包含上涨与否、收益率、是否达到目标涨幅等。
"""
cursor = db_conn.cursor()
# 获取匹配结束日的收盘价
cursor.execute('''
SELECT close FROM tdx_stock_daily
WHERE code = ? AND trade_date = ?
''', (stock_code, match_end_date))
row = cursor.fetchone()
if not row:
return None
base_close = row[0]
# 获取未来N日的收盘价
cursor.execute('''
SELECT trade_date, close FROM tdx_stock_daily
WHERE code = ? AND trade_date > ?
ORDER BY trade_date LIMIT ?
''', (stock_code, match_end_date, future_days))
future_rows = cursor.fetchall()
if len(future_rows) < future_days:
return None # 数据不足,无法计算
future_closes = [row[1] for row in future_rows]
future_dates = [row[0] for row in future_rows]
# 计算未来每日收益率
daily_returns = [(close - base_close) / base_close * 100 for close in future_closes]
max_return = max(daily_returns) if daily_returns else 0
min_return = min(daily_returns) if daily_returns else 0
# 未来第N日收益率
final_return = daily_returns[-1] if daily_returns else 0
# 是否上涨
is_rise = final_return > 0
# 是否在N日内达到过X%的涨幅
target_gain = 5.0 # 目标涨幅5%
hit_target = any(r >= target_gain for r in daily_returns)
return {
'base_close': base_close,
'future_dates': future_dates,
'future_closes': future_closes,
'daily_returns': daily_returns,
'final_return': final_return,
'max_return': max_return,
'min_return': min_return,
'is_rise': is_rise,
'hit_target': hit_target
}
然后,我们遍历所有匹配结果,调用这个函数,并汇总统计:
def analyze_matches_statistics(all_matches_with_future):
"""
分析所有匹配结果的未来表现统计。
all_matches_with_future: 列表,每个元素是一个包含匹配信息和未来表现数据的字典。
"""
total_matches = len(all_matches_with_future)
if total_matches == 0:
return {}
rise_count = sum(1 for m in all_matches_with_future if m['future_data']['is_rise'])
hit_target_count = sum(1 for m in all_matches_with_future if m['future_data']['hit_target'])
avg_final_return = np.mean([m['future_data']['final_return'] for m in all_matches_with_future])
avg_max_return = np.mean([m['future_data']['max_return'] for m in all_matches_with_future])
avg_min_return = np.mean([m['future_data']['min_return'] for m in all_matches_with_future])
stats = {
'total_samples': total_matches,
'rise_probability': rise_count / total_matches * 100, # 上涨概率
'target_hit_probability': hit_target_count / total_matches * 100, # 触及目标涨幅概率
'avg_final_return': avg_final_return,
'avg_max_return': avg_max_return,
'avg_max_drawdown': avg_min_return, # 平均最大回撤(用平均最小收益率近似)
}
return stats
最终,你会得到一份报告:“历史上,当出现与你当前所选形态相似度超过XX的走势后,未来5天内上涨的概率是YY%,平均收益是ZZ%,有WW%的概率出现超过5%的涨幅。” 这个数据,比任何主观臆断都更有分量。
4.2 实战案例:寻找“黄金坑”后的机会
让我分享一个我实际跑过的例子。我曾在某只科技股上观察到一个典型的“黄金坑”形态:股价经过一段时间的盘整或阴跌后,突然出现一根放量中长阴线,随后几天小阴小阳缩量止跌,不再创新低。我认为这可能是一个洗盘结束的信号。
于是,我手动在软件的K线图上框选了包含那根阴线及其后3-4天的K线组合,大约7根K线。设置相似度阈值为0.75(这个值需要反复测试,太高则样本太少,太低则匹配结果质量差),在近5年全市场数据中搜索。
程序运行了大约20分钟,返回了80多个相似度高于0.75的历史案例。统计结果显示,在这些案例出现后:
- 未来5个交易日内,股价上涨的概率为68%。
- 平均收益率为2.1%。
- 有45%的概率在5日内出现超过5%的涨幅。
- 平均最大回撤为-1.5%。
这个数据给了我很大的信心。它告诉我,这种形态后,短期上涨的概率确实大于下跌,且潜在的收益风险比(2.1%/1.5% ≈ 1.4)看起来也还可以。当然,这绝不是买入的“圣杯”,而是将一种模糊的盘感,转化为了一个可量化、可回溯的概率优势。在实际操作中,我还会结合当时的市场情绪、板块效应和个股基本面来综合决策。
5. 优化、陷阱与进阶思考
任何量化模型都不是一劳永逸的。形态匹配工具搭建好后,真正的功夫在于如何用好它,以及如何理解它的局限性。
5.1 性能优化与工程实践
全市场历史数据扫描非常耗时。除了使用多进程,还有几个优化方向:
- 预计算与索引:如果形态长度固定(比如只搜索10日或20日形态),可以预先计算所有股票所有时间窗口的标准化序列,甚至提前计算好某些特征向量(如通过主成分分析PCA降维),存入数据库并建立索引。这样查询时不再是实时计算DTW,而是变成了特征向量的快速比对。
- 分段与抽样:不必每次都扫描全部历史。可以只扫描最近3年的数据,或者只在特定的市场阶段(如牛市、熊市、震荡市)中搜索,使匹配结果更具时效性和针对性。
- 相似度算法调优:DTW虽然好,但计算慢。可以尝试其他相似度度量,如皮尔逊相关系数(衡量趋势方向一致性)或余弦相似度,它们计算更快,有时也能达到不错的效果。也可以对DTW加入窗口约束,加速计算。
5.2 警惕“数据挖掘的陷阱”
这是所有量化研究者都必须面对的“心魔”。
- 过拟合(Overfitting):这是最大的陷阱。如果你为了找到某个“神奇形态”而不断调整参数(形态长度、相似度阈值、未来统计天数),直到它在历史数据上表现完美,那么这个策略大概率在未来会失效。因为它可能只是完美拟合了历史噪音。对抗过拟合,一定要使用样本外测试。例如,用2015-2020年的数据挖掘形态,用2021-2023年的数据来验证其表现。
- 幸存者偏差(Survivorship Bias):我们的数据库里只有目前还存在的股票。那些已经退市的股票,其历史上失败的、导致退市的形态,并没有被包含进来。这会导致我们统计出的成功概率偏高。一个解决办法是使用包含已退市股票的全量历史数据库,但这种数据获取成本较高。
- 未来函数(Look-ahead Bias):在计算匹配时,一定要确保只使用匹配点之前的历史数据。在代码实现中,滑动窗口的结束日期必须早于“当前”分析日期,绝不能用到未来的信息。
5.3 从形态匹配到模式识别
基础的形态匹配是基于“形状”的模糊匹配。我们可以将其升级为更精确的模式识别。
- 结合量能:不仅匹配价格形态,也匹配成交量形态。一个价升量增的突破形态,和一个价升量缩的形态,意义截然不同。可以在标准化价格序列的同时,也对成交量序列进行标准化和匹配,给“价量齐升”的匹配结果更高的权重。
- 引入技术指标:将MACD的金叉/死叉位置、RSI的超买超卖状态等,作为形态的附加特征进行匹配。例如,寻找“价格形态相似且同时出现MACD底背离”的历史案例。
- 机器学习分类:将问题从“找相似的”转变为“分好坏的”。我们可以手动(或基于规则)标注大量历史形态片段,标记其后市表现是“好”、“一般”还是“差”。然后使用机器学习算法(如随机森林、支持向量机甚至简单的神经网络)来训练一个分类器。当新的形态出现时,分类器可以直接给出其属于“好”类的概率。这比单纯的相似度匹配更进了一步。
这条路走下去,你会发现,你不仅仅是在做一个工具,而是在构建一套属于自己的、基于历史数据的市场行为分析框架。每一次回测,每一次参数调整,都是你对市场理解的一次深化。工具本身不会让你稳定盈利,但工具背后那种“让数据说话,用概率决策”的思维方式,才是你在投资路上最宝贵的收获。我至今仍在不断迭代我的脚本,每次加入新的想法,看到回测曲线发生微妙变化,都感觉像是在和市场进行一场无声而有趣的对话。
7043



被折叠的 条评论
为什么被折叠?



