你有没有注意过,每个做技术分析的人,画的趋势线都不一样?
同一张K线图,张三画的支撑线在100块,李四画的在98块,王五画的在102块。三个人都说"我画的是对的"。但到底谁是对的?没人说得清。
更诡异的是,你昨天画的线和今天画的线,有时候也不完全一样。因为随着新的K线出现,你可能会微调一下角度,觉得"这条线更贴切"。
这个"贴切"两个字,就是问题所在。
什么叫贴切?是 touches 更多?还是斜率更自然?还是看起来更顺眼?
在主观交易的世界里,这些问题没有标准答案。每个人凭自己的经验和感觉来。但量化交易的世界里,没有"感觉"这个东西。你必须把"趋势线"变成一个精确的数学定义——什么是、什么不是,清清楚楚,没有灰色地带。
问题是:趋势线这个东西,真的能自动化识别吗?
答案是:能,但不像均线那么简单。均线是一个确定的数学公式,给什么数据出什么结果,没有歧义。趋势线不一样——它的定义本身就包含了主观判断的成分。什么叫"有效的支撑点"? touches 多少次算"多次"?两个 touches 够了,还是三个? touches 的容差是多少?
所以趋势线识别,是技术分析自动化里最有挑战性的课题之一。
但"有挑战"不等于"做不了"。今天我们就来拆解,怎么用程序自动画趋势线。从最基础的几何原理,到 touches 识别,到斜率分析,到突破确认,到最后怎么变成一个可以回测的策略。
Why:为什么要把趋势线量化?
趋势线的本质:一条"主观"的客观工具
先退一步想想,趋势线到底是什么?
在技术分析教科书里,趋势线的定义很简单:
• 上升趋势线:连接两个或多个依次抬高的低点
• 下降趋势线:连接两个或多个依次降低的高点
听起来很明确。但实际操作中,充满了模糊地带。
第一,"依次抬高的低点"——多高算抬高?第二个低点比第一个高了0.01元,算不算抬高?
第二,"连接两个或多个"——两个 touches 够不够?还是要三个才有效? touches 越多越有效,那"多"到什么程度?
第三,touches 的精度——价格必须精确碰到那条线才算 touches,还是差一点也算?差多少算"差一点"?
这些问题,主观交易的人凭经验判断。但量化的人必须给出精确的数字。
主观趋势线的三个致命问题
如果你一直用手动画趋势线来做交易,有三个问题可能你从来没认真想过。
第一个问题:不可复现。
你今天画的线,明天可能微调。你画的线,换一个人画不出来。不可复现意味着不可回测,不可回测意味着你永远不知道这条线在过去到底管不管用。
第二个问题:确认偏误。
人有一种本能:画了一条趋势线之后,你会不自觉地关注那些 touches 了这条线的地方,忽略那些没有 touches 的地方。你觉得这条线"很准",但可能只是你的大脑在帮你筛选证据。
第三个问题:无法系统化。
主观趋势线没法写进策略。你没法跟电脑说"画一条看起来最顺眼的线"。既然没法写进策略,就没法自动化执行,也没法回测验证。
量化的好处
把趋势线量化之后,这三个问题全部解决。
第一,可复现。同样的数据、同样的参数,出来的线永远一样。
第二,无偏误。程序不挑证据,touches 了几次就是几次,不给你留"我觉得"的空间。
第三,可回测。你可以拿过去十年的数据跑一遍,看看你的趋势线策略到底管不管用。
还有一个隐藏的好处:可优化。
主观画线的时候,你觉得"差不多"就行了。但量化的趋势线有精确的参数—— touches 次数、斜率、容差、突破确认规则——每一个参数都可以调,都可以做敏感性分析。你能清楚地知道哪个参数对结果影响最大。
What:趋势线识别的核心原理
几何基础:两点定线
趋势线的数学起点很简单——两点确定一条直线。
给定点
和
,直线方程为:
斜率为:
这个没问题。问题是——P_1和P_2怎么选?
支撑点和压力点的识别
在上升趋势线的场景下,我们需要找到价格序列中的"局部最低点"(swing low)。
什么是 swing low?
最直观的定义:如果某一天的最低价,比它前后各 N 天的最低价都低,那这一天就是一个 swing low。
// python
def find_swing_lows(low_prices, order=5):
"""寻找局部最低点(swing low)"""
swing_lows = []
for i in range(order, len(low_prices) - order):
# 当前点的最低价
current_low = low_prices[i]
# 检查前后各 order 天的最低价
left_window = low_prices[i-order:i]
right_window = low_prices[i+1:i+order+1]
# 如果当前最低价是窗口内最低的
if (current_low <= min(left_window) and
current_low <= min(right_window)):
swing_lows.append((i, current_low))
return swing_lows
这个 order 参数很重要。它决定了 swing low 的"粒度"。
• order=3:非常灵敏,会找到很多小幅度的低点,适合短期趋势线
• order=10:比较迟钝,只找大的低点,适合长期趋势线
• order=20:非常迟钝,只找主要的低点
不同的 order 出来的 swing low 数量和质量完全不同。这也是参数优化的一部分。
趋势线的构建:从 touches 到有效线
找到了 swing low 之后,怎么把它们连成趋势线?
最暴力的方法:穷举所有两两组合,对每一对 swing low 画一条线,然后检查这条线未来被 touches 了多少次。 touches 越多的线越"有效"。
但这个方法计算量太大。如果有 50 个 swing low,组合数是
条线。每条线还要遍历后面的所有点来检查 touches,计算量是
更高效的方法是分步筛选:
// python
def build_trendlines(swing_lows, prices, tolerance=0.02, min_touches=3):
"""构建有效趋势线"""
valid_lines = []
for i in range(len(swing_lows)):
for j in range(i+1, len(swing_lows)):
t1, p1 = swing_lows[i]
t2, p2 = swing_lows[j]
# 上升趋势线要求后面的低点更高
if p2 <= p1:
continue
# 计算斜率
slope = (p2 - p1) / (t2 - t1)
# 检查这条线在 t2 之后被 touches 了多少次
touches = 0
valid = True
for k in range(t2+1, len(prices)):
# 趋势线在 k 时刻的价格
line_price = p2 + slope * (k - t2)
# 检查 k 时刻的最低价是否在容差范围内 touches 趋势线
if abs(prices[k] - line_price) / line_price <= tolerance:
touches += 1
# 趋势线不能被跌破(上升趋势线的支撑不能破)
if prices[k] < line_price * (1 - tolerance * 2):
valid = False
break
if valid and touches >= min_touches - 2: # -2 因为两个端点已经算 touches
total_touches = touches + 2
valid_lines.append({
'start_time': t1,
'start_price': p1,
'end_time': t2,
'end_price': p2,
'slope': slope,
'touches': total_touches,
'valid': True
})
# 按 touches 数量排序,touches 越多越有效
valid_lines.sort(key=lambda x: x['touches'], reverse=True)
return valid_lines
斜率与趋势强度
趋势线的斜率不只是一个几何参数,它直接反映了趋势的强度。
斜率大(角度陡):趋势强劲,价格上升很快。但这种趋势往往不可持续,容易被打破。
斜率小(角度平缓):趋势温和,上升缓慢。这种趋势往往更持久,但利润空间也小。
最健康的斜率:根据经验,大约 30-45 度的趋势线最健康、最持久。太陡了容易断,太缓了没利润。
怎么把角度和斜率对应起来?
角度 = arctan(斜率) × 180 / π
但这里有个坑:斜率取决于你用的时间单位。日线图和分钟图的斜率完全不同。所以更实用的做法是看"标准化斜率"——把价格和时间都标准化到 [0, 1] 区间,再算斜率。
// python
def normalized_slope(slope, price_range, time_range):
"""将斜率标准化到可比较的范围"""
# 标准化斜率 = 斜率 × (时间范围 / 价格范围)
return slope * time_range / price_range
标准化之后,不同品种、不同时间框架的斜率就可以横向比较了。
趋势线突破的量化确认
趋势线被突破了,是趋势反转的信号。但"突破"怎么定义?
主观交易的人会说"价格穿过了那条线"。但量化需要更精确。
有几种常见的突破确认方法:
方法一:收盘价突破
只看收盘价是否突破了趋势线,不看盘中价格。盘中可能只是毛刺,收盘价才是真突破。
// python
def close_breakout(prices, times, trendline, current_time):
"""收盘价突破确认"""
line_price = trendline.price_at(current_time)
return prices[current_time] < line_price # 上升趋势线被跌破
方法二:连续 N 日突破
收盘价突破了不算,要连续 N 天都在趋势线之外才算有效突破。这个方法能过滤掉大部分假突破。
// python
def consecutive_breakout(prices, times, trendline, current_time, n_days=2):
"""连续N日突破确认"""
if current_time < n_days:
return False
for i in range(n_days):
t = current_time - i
line_price = trendline.price_at(t)
if prices[t] >= line_price: # 只要有一天收回来就不算突破
return False
return True
方法三:幅度突破
突破不算,要突破一定幅度才算。比如收盘价低于趋势线 1% 以上。
// python
def magnitude_breakout(prices, times, trendline, current_time, threshold=0.01):
"""幅度突破确认"""
line_price = trendline.price_at(current_time)
break_pct = (line_price - prices[current_time]) / line_price
return break_pct > threshold
方法四:成交量确认
突破的时候成交量要放大。如果价格突破了但成交量没有放大,可能是假突破。
// python
def volume_confirmed_breakout(prices, volumes, trendline, current_time, vol_multiplier=1.5):
"""成交量确认的突破"""
line_price = trendline.price_at(current_time)
# 价格突破
if prices[current_time] >= line_price:
return False
# 成交量放大
avg_volume = np.mean(volumes[current_time-20:current_time])
return volumes[current_time] > avg_volume * vol_multiplier
实操中,建议组合使用多种确认方法。比如"收盘价突破 + 成交量放大"或者"连续2日突破 + 幅度超过1%"。多重确认能大幅降低假突破的比例。
How:从零构建趋势线自动识别系统
第一步:数据准备和 swing point 识别
// python
import numpy as np
import pandas as pd
def prepare_data(df):
"""准备数据"""
df = df.copy()
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date').reset_index(drop=True)
df['time_index'] = range(len(df))
return df
def find_swing_points(df, order=5):
"""同时识别 swing high 和 swing low"""
highs = []
lows = []
for i in range(order, len(df) - order):
# Swing High
current_high = df['high'].iloc[i]
left_highs = df['high'].iloc[i-order:i].values
right_highs = df['high'].iloc[i+1:i+order+1].values
if (current_high >= max(left_highs) and
current_high >= max(right_highs)):
highs.append((i, current_high))
# Swing Low
current_low = df['low'].iloc[i]
left_lows = df['low'].iloc[i-order:i].values
right_lows = df['low'].iloc[i+1:i+order+1].values
if (current_low <= min(left_lows) and
current_low <= min(right_lows)):
lows.append((i, current_low))
return highs, lows
第二步:趋势线候选生成
// python
class TrendLine:
"""趋势线类"""
def __init__(self, t1, p1, t2, p2, line_type='support'):
self.t1, self.p1 = t1, p1
self.t2, self.p2 = t2, p2
self.line_type = line_type
self.slope = (p2 - p1) / (t2 - t1)
self.touches = [(t1, p1), (t2, p2)]
self.broken = False
self.broken_time = None
def price_at(self, t):
"""计算趋势线在时间 t 的价格"""
return self.p1 + self.slope * (t - self.t1)
def check_touch(self, t, price, tolerance=0.02):
"""检查某个点是否 touches 趋势线"""
line_price = self.price_at(t)
return abs(price - line_price) / line_price <= tolerance
def check_break(self, t, price):
"""检查趋势线是否被突破"""
line_price = self.price_at(t)
if self.line_type == 'support':
return price < line_price
else: # resistance
return price > line_price
def angle_degrees(self, price_range, time_range):
"""计算趋势线的角度(标准化后)"""
norm_slope = self.slope * time_range / price_range
return np.degrees(np.arctan(norm_slope))
def generate_candidate_lines(swing_points, line_type='support'):
"""生成候选趋势线"""
candidates = []
for i in range(len(swing_points)):
for j in range(i+1, len(swing_points)):
t1, p1 = swing_points[i]
t2, p2 = swing_points[j]
# 上升趋势线要求后面的低点更高
if line_type == 'support' and p2 <= p1:
continue
# 下降趋势线要求后面的高点更低
if line_type == 'resistance' and p2 >= p1:
continue
line = TrendLine(t1, p1, t2, p2, line_type)
candidates.append(line)
return candidates
第三步:趋势线评分和筛选
// python
def score_trendline(line, df, tolerance=0.02, penalty_factor=2.0):
"""对趋势线评分"""
score = 0
touches_count = len(line.touches)
# 基础分:touches 数量
score += touches_count * 10
# 遍历所有点,检查 touches 和 validity
for t in range(line.t1, len(df)):
low_price = df['low'].iloc[t]
high_price = df['high'].iloc[t]
close_price = df['close'].iloc[t]
line_price = line.price_at(t)
# 检查 touches
if line.line_type == 'support':
if abs(low_price - line_price) / line_price <= tolerance:
if (t, low_price) not in line.touches:
line.touches.append((t, low_price))
score += 10
# 检查是否被跌破(惩罚项)
if close_price < line_price * (1 - tolerance * penalty_factor):
score -= 20 # 被跌破扣分
if not line.broken:
line.broken = True
line.broken_time = t
else: # resistance
if abs(high_price - line_price) / line_price <= tolerance:
if (t, high_price) not in line.touches:
line.touches.append((t, high_price))
score += 10
if close_price > line_price * (1 + tolerance * penalty_factor):
score -= 20
if not line.broken:
line.broken = True
line.broken_time = t
# 角度惩罚:太陡的线扣分
price_range = df['high'].max() - df['low'].min()
time_range = len(df)
angle = abs(line.angle_degrees(price_range, time_range))
if angle > 60:
score -= 15 # 太陡
elif angle < 10:
score -= 10 # 太平
else:
score += 5 # 角度健康
return score
def filter_best_lines(candidates, df, top_n=5):
"""筛选最优趋势线"""
scored_lines = []
for line in candidates:
score = score_trendline(line, df)
scored_lines.append((line, score))
# 按评分排序
scored_lines.sort(key=lambda x: x[1], reverse=True)
# 去重:如果两条线的斜率很接近,只保留评分更高的
final_lines = []
for line, score in scored_lines[:top_n * 3]: # 先取更多,再去重
is_duplicate = False
for existing_line, _ in final_lines:
if abs(line.slope - existing_line.slope) / abs(existing_line.slope) < 0.1:
is_duplicate = True
break
if not is_duplicate:
final_lines.append((line, score))
if len(final_lines) >= top_n:
break
return final_lines
第四步:突破信号生成
// python
def generate_breakout_signals(df, trendlines, breakout_method='close',
confirm_days=2, magnitude_threshold=0.01,
volume_multiplier=1.5):
"""生成趋势线突破信号"""
signals = []
for t in range(len(df)):
for line, score in trendlines:
if line.broken:
continue
if t <= line.t2: # 趋势线还没开始
continue
line_price = line.price_at(t)
close_price = df['close'].iloc[t]
low_price = df['low'].iloc[t]
if line.line_type == 'support':
# 上升趋势线被跌破 → 做空信号
if breakout_method == 'close':
if close_price < line_price:
signals.append({
'time': t,
'type': 'sell',
'line': line,
'line_price': line_price,
'break_pct': (line_price - close_price) / line_price,
'score': score
})
elif breakout_method == 'consecutive':
if t >= confirm_days:
all_broken = True
for d in range(confirm_days):
if df['close'].iloc[t-d] >= line.price_at(t-d):
all_broken = False
break
if all_broken:
signals.append({
'time': t,
'type': 'sell',
'line': line,
'line_price': line_price,
'break_pct': (line_price - close_price) / line_price,
'score': score
})
elif breakout_method == 'magnitude':
break_pct = (line_price - close_price) / line_price
if break_pct > magnitude_threshold:
signals.append({
'time': t,
'type': 'sell',
'line': line,
'line_price': line_price,
'break_pct': break_pct,
'score': score
})
elif line.line_type == 'resistance':
# 下降趋势线被突破 → 做多信号
if breakout_method == 'close':
if close_price > line_price:
signals.append({
'time': t,
'type': 'buy',
'line': line,
'line_price': line_price,
'break_pct': (close_price - line_price) / line_price,
'score': score
})
return signals
第五步:完整系统封装
// python
class AutoTrendLineSystem:
"""趋势线自动识别系统"""
def __init__(self, swing_order=5, tolerance=0.02,
min_touches=3, breakout_method='consecutive',
confirm_days=2):
self.swing_order = swing_order
self.tolerance = tolerance
self.min_touches = min_touches
self.breakout_method = breakout_method
self.confirm_days = confirm_days
self.support_lines = []
self.resistance_lines = []
self.signals = []
def analyze(self, df):
"""完整分析流程"""
# 1. 寻找 swing points
swing_highs, swing_lows = find_swing_points(df, self.swing_order)
print(f"找到 {len(swing_highs)} 个 swing high, {len(swing_lows)} 个 swing low")
# 2. 生成候选趋势线
support_candidates = generate_candidate_lines(swing_lows, 'support')
resistance_candidates = generate_candidate_lines(swing_highs, 'resistance')
print(f"支撑线候选: {len(support_candidates)}, 压力线候选: {len(resistance_candidates)}")
# 3. 评分和筛选
self.support_lines = filter_best_lines(support_candidates, df, top_n=3)
self.resistance_lines = filter_best_lines(resistance_candidates, df, top_n=3)
# 4. 生成突破信号
all_lines = self.support_lines + self.resistance_lines
self.signals = generate_breakout_signals(
df, all_lines,
breakout_method=self.breakout_method,
confirm_days=self.confirm_days
)
# 5. 输出分析结果
self._print_summary()
return self.signals
def _print_summary(self):
"""打印分析摘要"""
print("\n=== 趋势线分析结果 ===")
print(f"\n支撑线 (Top {len(self.support_lines)}):")
for i, (line, score) in enumerate(self.support_lines):
angle = line.angle_degrees(100, 252) # 示例标准化
print(f" #{i+1}: 起始={line.t1}, 终点={line.t2}, "
f"touches={len(line.touches)}, 评分={score:.0f}, "
f"角度={angle:.1f}°")
print(f"\n压力线 (Top {len(self.resistance_lines)}):")
for i, (line, score) in enumerate(self.resistance_lines):
angle = line.angle_degrees(100, 252)
print(f" #{i+1}: 起始={line.t1}, 终点={line.t2}, "
f"touches={len(line.touches)}, 评分={score:.0f}, "
f"角度={angle:.1f}°")
print(f"\n突破信号: {len(self.signals)} 个")
buy_signals = [s for s in self.signals if s['type'] == 'buy']
sell_signals = [s for s in self.signals if s['type'] == 'sell']
print(f" 做多: {len(buy_signals)}, 做空: {len(sell_signals)}")
def get_current_lines(self, current_time):
"""获取当前有效的趋势线"""
valid_support = [(l, s) for l, s in self.support_lines
if not l.broken and l.t2 <= current_time]
valid_resistance = [(l, s) for l, s in self.resistance_lines
if not l.broken and l.t2 <= current_time]
return valid_support, valid_resistance
def get_current_prices(self, current_time):
"""获取当前趋势线在指定时间的价格"""
prices = {}
for line, score in self.support_lines:
if not line.broken and line.t2 <= current_time:
prices[f"support_{line.t1}"] = line.price_at(current_time)
for line, score in self.resistance_lines:
if not line.broken and line.t2 <= current_time:
prices[f"resistance_{line.t1}"] = line.price_at(current_time)
return prices
使用示例和参数建议
// python
# 使用示例
system = AutoTrendLineSystem(
swing_order=5, # swing point 的灵敏度
tolerance=0.02, # touches 容差 2%
min_touches=3, # 最少 3 次 touches
breakout_method='consecutive', # 连续突破确认
confirm_days=2 # 连续 2 日确认
)
signals = system.analyze(df)
参数建议:
| 场景 | swing_order | tolerance | breakout_method | confirm_days | | --- | --- | --- | --- | --- | | 日线短期 | 3-5 | 0.01-0.02 | consecutive | 2 | | 日线中期 | 5-10 | 0.02-0.03 | consecutive | 2-3 | | 周线长期 | 5-8 | 0.03-0.05 | close | 1 | | 分钟线 | 10-20 | 0.005-0.01 | magnitude | - |
趋势线策略的局限性
说了这么多好处,也得说说趋势线策略的短板。不回避问题,才能用好它。
局限一:滞后性
趋势线需要至少两个 swing point 才能画出来。而 swing point 本身需要等价格反转后才能确认(order 参数越大,滞后越严重)。
所以当你"确认"了一条趋势线的时候,趋势可能已经走了好一段了。你画的那条线,可能已经是趋势的中后期了。
这就是为什么趋势线策略的胜率通常不高——你总是比别人慢半拍。
局限二:假突破多
即使你用了多种确认方法,趋势线突破的假信号依然不少。有研究显示,日线级别的趋势线突破,大约有 40-60% 是假突破。
这个比例比通道突破还高。因为趋势线本身就带有一定的主观性,不同的参数出来的线不一样,信号自然也不同。
局限三:参数敏感
swing_order 从 5 改成 6,可能出来的趋势线就完全不一样了。tolerance 从 0.02 改成 0.025,touches 数量可能翻一番。
参数敏感意味着过拟合风险高。你在历史数据上优化出来的参数,未来不一定好用。
局限四:不适合震荡市
趋势线是趋势跟踪工具。在震荡行情里,价格反复穿越趋势线,你会被反复打脸。
所以趋势线策略必须配合趋势过滤。比如 ADX 指标低于 20 的时候,不要画趋势线,也不要做趋势线突破。
局限五:回测困难
趋势线的自动识别本身就包含了很多主观判断(什么是 swing point、什么算 touches)。不同的实现方式出来的结果可能大相径庭。
这导致趋势线策略的回测结果,不同人之间很难比较。你回测出来年化 15%,我回测出来年化 5%,不是因为策略不同,而是因为我们"画线"的方式不同。
总结
今天我们从零开始,把趋势线识别这个"最主观"的技术分析工具,变成了可以用代码实现、可以回测验证、可以参数优化的量化系统。
回顾一下今天的核心内容:
第一,趋势线为什么需要量化。 主观画线不可复现、有确认偏误、无法系统化。量化之后,这些问题全部解决。
第二,趋势线怎么自动识别。 从 swing point 识别到候选线生成,到评分筛选,到突破确认,完整链路。
第三,趋势线的斜率和角度。 斜率反映趋势强度。30-45 度最健康。太陡容易断,太平没利润。
第四,突破确认方法。 收盘价突破、连续 N 日突破、幅度突破、成交量确认。建议组合使用。
第五,策略的局限性。 滞后性、假突破多、参数敏感、不适合震荡市、回测困难。
趋势线识别是技术分析自动化里最有挑战性的课题之一,因为它本身带有主观性。但"有主观性"不意味着"不能量化"——只要你能把主观判断变成精确的参数和规则,计算机就能帮你执行。
最后说一句:趋势线只是工具,不是圣杯。它最大的价值不是帮你赚钱,是帮你把模糊的"看图感觉"变成精确的"数字信号"。有了数字信号,你就能回测、能验证、能改进。
这就是量化的意义。
趋势线识别系列核心要点:
1. 趋势线的本质是连接多个 touches 的直线,量化的关键是精确定义 touches
2. swing point 识别是第一步,order 参数控制灵敏度
3. 趋势线评分要综合考虑 touches 数量、角度健康度、是否被跌破
4. 突破确认建议组合多种方法,降低假突破
5. 趋势线策略有明确局限:滞后、假突破多、参数敏感
6. 必须配合趋势过滤(如 ADX)使用,震荡市暂停
下一期,我们进入趋势过滤的话题。趋势线告诉你"线在哪",趋势过滤告诉你"现在该不该做"。怎么避免被假突破反复打脸?ADX 指标怎么用?双滤心法则是什么?下期见。