3D 柱线上的趋势强度和方向指标·进阶篇
用三投影把趋势力读成桥
早期用 MatPlotLib 和 Plotly 堆 3D 图,结果不是像圣诞树就是缺关键细节,对交易毫无助益。后来借女儿积木桥的启发,放弃花哨立体图,改成同一时空的三层投影来排布信息。 最上层是常规蜡烛,仅作背景;中间层是趋势力(Trend Force)曲线,实测两个阈值最关键:等级 3 附近属于湍流区,趋势可能加速也可能逆转;等级 6 被突破时,大概率出现强劲单边走势。 底层把趋势方向与力值相乘画成柱线:绿色柱顶着高推力往上,是偏多的入场倾向;若力值已高于 6 且红色柱线攀升,则偏空信号更明确。外汇与贵金属波动剧烈,此类信号仅作概率参考,实盘须严控仓位。 下面这段 Python 用 make_subplots 把三窗口叠好,你可直接抄去连自己的 df 验证: <span class="keyword">def</span> create_visualization(self, df: pd.DataFrame = <span class="literal">None</span>): <span class="keyword">if</span> df <span class="keyword">is</span> <span class="literal">None</span>: df = self.analyze_market() df = df.reset_index() <span class="comment"># Three projections of our "bridge"</span> fig = make_subplots(rows=<span class="number">3</span>, cols=<span class="number">1</span>, shared_xaxes=<span class="literal">True</span>, subplot_titles=(<span class="string">'Price'</span>, <span class="string">'Trend Force'</span>, <span class="string">'Trend Direction'</span>), row_heights=[<span class="number">0.5</span>, <span class="number">0.25</span>, <span class="number">0.25</span>], vertical_spacing=<span class="number">0.05</span>) <span class="comment"># Main chart - classic candles</span> fig.add_trace( go.Candlestick( x=df[<span class="string">'time'</span>], <span class="built_in">open</span>=df[<span class="string">'open'</span>], high=df[<span class="string">'high'</span>], low=df[<span class="string">'low'</span>], close=df[<span class="string">'close'</span>], name=<span class="string">'OHLC'</span> ), row=<span class="number">1</span>, col=<span class="number">1</span> ) <span class="comment"># The trend force is our main feature</span> fig.add_trace( go.Scatter( x=df[<span class="string">'time'</span>], y=df[<span class="string">'trend_force_adjusted'</span>], mode=<span class="string">'lines'</span>, line=<span class="built_in">dict</span>(color=<span class="string">'blue'</span>, width=<span class="number">2</span>), name=<span class="string">'Trend Force'</span> ), row=<span class="number">2</span>, col=<span class="number">1</span> ) <span class="comment"># Reference levels</span> fig.add_hline(y=<span class="number">3</span>, line_dash=<span class="string">"dash"</span>, line_color=<span class="string">"yellow"</span>, row=<span class="number">2</span>, col=<span class="number">1</span>) fig.add_hline(y=<span class="number">6</span>, line_dash=<span class="string">"dash"</span>, line_color=<span class="string">"green"</span>, row=<span class="number">2</span>, col=<span class="number">1</span>) <span class="comment"># Trend direction as a derivative of force</span> fig.add_trace( go.Bar( x=df[<span class="string">'time'</span>], y=df[<span class="string">'trend_direction'</span>] * df[<span class="string">'trend_force_adjusted'</span>], name=<span class="string">'Trend Direction'</span>, marker_color=np.where(df[<span class="string">'trend_direction'</span>] > <span class="number">0</span>, <span class="string">'green'</span>, <span class="string">'red'</span>) ), row=<span class="number">3</span>, col=<span class="number">1</span> ) 逐行看:函数先判 df 为空就跑 analyze_market 拿数据,reset_index 把时间提出来;make_subplots 建 3 行 1 列、高度比 0.5/0.25/0.25,共享 X 轴。第 1 行塞蜡烛图,第 2 行画 trend_force_adjusted 蓝线,并加 y=3 黄虚线、y=6 绿虚线作阈值。第 3 行用方向乘力值画柱,正为绿、负为红,把「力+方向」合成一眼可读的底栏。
<span class="keyword">def</span> create_visualization(self, df: pd.DataFrame = <span class="literal">None</span>): <span class="keyword">if</span> df <span class="keyword">is</span> <span class="literal">None</span>: df = self.analyze_market() df = df.reset_index() <span class="comment"># Three projections of our "bridge"</span> fig = make_subplots(rows=<span class="number">class="num">3</span>, cols=<span class="number">class="num">1</span>, shared_xaxes=<span class="literal">True</span>, subplot_titles=(<span class="class="type">class="kw">string">&class="macro">#x27;Price&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;Trend Force&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;Trend Direction&class="macro">#x27;</span>), row_heights=[<span class="number">class="num">0.5</span>, <span class="number">class="num">0.25</span>, <span class="number">class="num">0.25</span>], vertical_spacing=<span class="number">class="num">0.05</span>) <span class="comment"># Main chart - classic candles</span> fig.add_trace( go.Candlestick( x=df[<span class="class="type">class="kw">string">&class="macro">#x27;time&class="macro">#x27;</span>], <span class="built_in">open</span>=df[<span class="class="type">class="kw">string">&class="macro">#x27;open&class="macro">#x27;</span>], high=df[<span class="class="type">class="kw">string">&class="macro">#x27;high&class="macro">#x27;</span>], low=df[<span class="class="type">class="kw">string">&class="macro">#x27;low&class="macro">#x27;</span>], close=df[<span class="class="type">class="kw">string">&class="macro">#x27;close&class="macro">#x27;</span>], name=<span class="class="type">class="kw">string">&class="macro">#x27;OHLC&class="macro">#x27;</span> ), row=<span class="number">class="num">1</span>, col=<span class="number">class="num">1</span> ) <span class="comment"># The trend force is our main feature</span> fig.add_trace( go.Scatter( x=df[<span class="class="type">class="kw">string">&class="macro">#x27;time&class="macro">#x27;</span>], y=df[<span class="class="type">class="kw">string">&class="macro">#x27;trend_force_adjusted&class="macro">#x27;</span>], mode=<span class="class="type">class="kw">string">&class="macro">#x27;lines&class="macro">#x27;</span>, line=<span class="built_in">dict</span>(class="type">class="kw">color=<span class="class="type">class="kw">string">&class="macro">#x27;blue&class="macro">#x27;</span>, width=<span class="number">class="num">2</span>), name=<span class="class="type">class="kw">string">&class="macro">#x27;Trend Force&class="macro">#x27;</span> ), row=<span class="number">class="num">2</span>, col=<span class="number">class="num">1</span> ) <span class="comment"># Reference levels</span> fig.add_hline(y=<span class="number">class="num">3</span>, line_dash=<span class="class="type">class="kw">string">"dash"</span>, line_color=<span class="class="type">class="kw">string">"yellow"</span>, row=<span class="number">class="num">2</span>, col=<span class="number">class="num">1</span>) fig.add_hline(y=<span class="number">class="num">6</span>, line_dash=<span class="class="type">class="kw">string">"dash"</span>, line_color=<span class="class="type">class="kw">string">"green"</span>, row=<span class="number">class="num">2</span>, col=<span class="number">class="num">1</span>) <span class="comment"># Trend direction as a derivative of force</span> fig.add_trace( go.Bar( x=df[<span class="class="type">class="kw">string">&class="macro">#x27;time&class="macro">#x27;</span>], y=df[<span class="class="type">class="kw">string">&class="macro">#x27;trend_direction&class="macro">#x27;</span>] * df[<span class="class="type">class="kw">string">&class="macro">#x27;trend_force_adjusted&class="macro">#x27;</span>], name=<span class="class="type">class="kw">string">&class="macro">#x27;Trend Direction&class="macro">#x27;</span>, marker_color=np.where(df[<span class="class="type">class="kw">string">&class="macro">#x27;trend_direction&class="macro">#x27;</span>] > <span class="number">class="num">0</span>, <span class="class="type">class="kw">string">&class="macro">#x27;green&class="macro">#x27;</span>, <span class="class="type">class="kw">string">&class="macro">#x27;red&class="macro">#x27;</span>) ), row=<span class="number">class="num">3</span>, col=<span class="number">class="num">1</span> )
「成交量在不同时段的分量差异」
很多做价格行为的老手盯着形态和指标,却忘了市场是按时区呼吸的。同一个成交量 spike,放在亚洲时段和欧洲时段,对价格的推动效率完全不是一个量级。 把回测数据摊开看会更直白:亚洲盘(0–8点)我给的 session_coef 是 0.7,欧洲盘(8–16点)是 1.0,美盘(16–24点)是 0.9。也就是说,亚洲时段里小幅放量就显眼,欧洲区得有更明显的偏差才够触发有效信号。 新时段开始前 30–40 分钟,指标常常走得很‘别扭’,像是市场在给新玩家腾位置。把时段系数叠到体量曲线上,原本模糊的形态会突然清晰——同样一手单,权重随时段漂移。 下面这段是用 pandas 给 K 线打时段系数的核心逻辑,直接能抄去验证:
<span class="comment"># See how easy it is to take into account the impact of sessions</span> hour = df.index.hour <span class="comment"># Asian session is the calmest</span> asian_mask = (hour >= <span class="number">class="num">0</span>) & (hour < <span class="number">class="num">8</span>) df.loc[asian_mask, <span class="class="type">class="kw">string">&class="macro">#x27;session_coef&class="macro">#x27;</span>] = <span class="number">class="num">0.7</span> <span class="comment"># Europe - peak activity</span> european_mask = (hour >= <span class="number">class="num">8</span>) & (hour < <span class="number">class="num">16</span>) df.loc[european_mask, <span class="class="type">class="kw">string">&class="macro">#x27;session_coef&class="macro">#x27;</span>] = <span class="number">class="num">1.0</span> <span class="comment"># America - still active, but not as strong</span> american_mask = (hour >= <span class="number">class="num">16</span>) & (hour < <span class="number">class="num">24</span>) df.loc[american_mask, <span class="class="type">class="kw">string">&class="macro">#x27;session_coef&class="macro">#x27;</span>] = <span class="number">class="num">0.9</span>
◍ 波动率乘量价动量凑出的趋势强度尺
有个反直觉的现象:把波动率、成交量趋势和动量绝对值三者直接相乘,理论上是三股乱流搅一起,图形却意外干净。在 EURUSD 日线回测里,这个乘积在强劲走势启动前 3~5 根柱就出现清晰峰值,而不是走完才反应。 用 MinMaxScaler 把结果压到 3~9 区间后,读数有了可直接用的分层:低于 3 市场偏沉睡;3 到 6 是走势萌芽区;高于 6 趋势拿到完整强度。叠加时段系数后,信号在伦敦-纽约重叠段的误报明显少于亚盘。 外汇和贵金属杠杆高、滑点跳空频繁,这类读数只作概率参考,不能当进场保证。打开 MT5 用自定义指标算这三个分量乘积累加,先把阈值 3/6 标成水平线,观察你常做的品种是否也提前反应。
def _calculate_components(self, df: pd.DataFrame) -> pd.DataFrame: # Here it is, that very "error" - the multiplication of three components df[&class="macro">#x27;trend_force&class="macro">#x27;] = df[&class="macro">#x27;volatility&class="macro">#x27;] * df[&class="macro">#x27;volume_trend&class="macro">#x27;] * abs(df[&class="macro">#x27;momentum&class="macro">#x27;]) # Normalize the result to a range of class="num">3 to class="num">9 df[&class="macro">#x27;trend_force_norm&class="macro">#x27;] = self.scaler.fit_transform( df[&class="macro">#x27;trend_force&class="macro">#x27;].values.reshape(-class="num">1, class="num">1) ).flatten() # Final adjustments considering sessions df[&class="macro">#x27;trend_force_adjusted&class="macro">#x27;] = df[&class="macro">#x27;trend_force_norm&class="macro">#x27;] * df[&class="macro">#x27;session_coef&class="macro">#x27;]
强弱失稳叠加放量才认逆转
在归一化后的强弱指标里,趋势强度突破等级 6 但方向开始乱抖,往往比单纯看回落更早露出反转苗头。作者在 H1 和 H4 上重测了两周,这两个周期的信号最干净,市场节奏像更讲理一些。 真正的过滤条件在体量:当上述形态出现时,若成交量超过 20 周期均量的 1.5 倍,反转概率明显抬升;量能跟不上,多半只是横盘整理。外汇与贵金属波动大、滑点凶,这种信号只作概率参考,实盘必须带止损。 误报难免,所以得用分拣逻辑把“强确认”和“弱确认”分开看。下面的片段就是方向判定与量能确认的核心计算,直接能丢进 Python 回测框架里跑。
# Determining the trend direction df[&class="macro">#x27;trend_direction&class="macro">#x27;] = np.sign(df[&class="macro">#x27;momentum&class="macro">#x27;]) # This is where the magic begins df[&class="macro">#x27;direction_strength&class="macro">#x27;] = df[&class="macro">#x27;trend_direction&class="macro">#x27;] * df[&class="macro">#x27;trend_force_adjusted&class="macro">#x27;] # Looking for reversal patterns df[&class="macro">#x27;reversal_pattern&class="macro">#x27;] = np.where( (df[&class="macro">#x27;trend_force_adjusted&class="macro">#x27;] > class="num">6) & # Strong trend(df[&class="macro">#x27;direction_strength&class="macro">#x27;].diff().rolling(class="num">3).std() > class="num">1.5), # Directional instability class="num">1, class="num">0 ) df[&class="macro">#x27;volume_confirmation&class="macro">#x27;] = np.where( (df[&class="macro">#x27;reversal_pattern&class="macro">#x27;] == class="num">1) & (df[&class="macro">#x27;volume_trend&class="macro">#x27;] > df[&class="macro">#x27;volume_trend&class="macro">#x27;].rolling(class="num">20).mean() * class="num">1.5), &class="macro">#x27;Strong&class="macro">#x27;, &class="macro">#x27;Weak&class="macro">#x27; )
「把信号画成能悬停看懂的图」
指标算完若只丢给交易者一张表或一串方程,多数人根本无从下手。作者试过纯 MatPlotLib,找信号极其反人类,前后换过十几种绘图库,最终落在 Plotly 上,核心卖点就是交互:光标移到某根蜡烛,趋势推力、成交量、方向立刻浮层显示。 配色单独耗了一周,只为降低视觉疲劳;趋势方向被做成柱线——高度是强度、颜色分多空。有交易员朋友盯着图点了五分钟,冒出一句“新手也能看清市场往哪走”。真实反馈里,有人终于不被信号绕晕,有人省下整天盯屏的工时。 下面这段 Python(Plotly)是三级图层的骨架:主图 K 线、中图趋势推力线、底图方向柱。外汇与贵金属波动剧烈、杠杆高风险,这类图只辅助读势,不替代仓位纪律。 悬停模板里 Force 保留两位小数,底图用 numpy 按方向染绿红,row_heights 把主图占到 50% 视高,符合“价格为主、指标为辅”的盯盘习惯。你可直接把 df 换成 MT5 导出的分钟级数据跑一遍,验证柱线方向是否和肉眼趋势一致。
def create_visualization(self, df: pd.DataFrame = None): if df is None: df = self.analyze_market() fig = make_subplots(rows=class="num">3, cols=class="num">1, shared_xaxes=True, subplot_titles=(&class="macro">#x27;Price&class="macro">#x27;, &class="macro">#x27;Trend Force&class="macro">#x27;, &class="macro">#x27;Trend Direction&class="macro">#x27;), row_heights=[class="num">0.5, class="num">0.25, class="num">0.25], vertical_spacing=class="num">0.05) # Main chart - candles with signal highlighting fig.add_trace( go.Candlestick( x=df[&class="macro">#x27;time&class="macro">#x27;], open=df[&class="macro">#x27;open&class="macro">#x27;], high=df[&class="macro">#x27;high&class="macro">#x27;], low=df[&class="macro">#x27;low&class="macro">#x27;], close=df[&class="macro">#x27;close&class="macro">#x27;], name=&class="macro">#x27;OHLC&class="macro">#x27;, hoverlabel=dict( bgcolor=&class="macro">#x27;white&class="macro">#x27;, font=dict(size=class="num">12) ) ), row=class="num">1, col=class="num">1 ) fig.add_trace( go.Scatter( x=df[&class="macro">#x27;time&class="macro">#x27;], y=df[&class="macro">#x27;trend_force_adjusted&class="macro">#x27;], mode=&class="macro">#x27;lines&class="macro">#x27;, line=dict(class="type">class="kw">color=&class="macro">#x27;blue&class="macro">#x27;, width=class="num">2), name=&class="macro">#x27;Trend Force&class="macro">#x27;, hovertemplate="<br>".join([ "Time: %{x}", "Force: %{y:.2f}", "<extra></extra>" ]) ), row=class="num">2, col=class="num">1 ) fig.add_trace( go.Bar( x=df[&class="macro">#x27;time&class="macro">#x27;], y=df[&class="macro">#x27;trend_direction&class="macro">#x27;] * df[&class="macro">#x27;trend_force_adjusted&class="macro">#x27;], name=&class="macro">#x27;Trend Direction&class="macro">#x27;, marker_color=np.where(df[&class="macro">#x27;trend_direction&class="macro">#x27;] > class="num">0, &class="macro">#x27;green&class="macro">#x27;, &class="macro">#x27;red&class="macro">#x27;), ), row=class="num">3, col=class="num">1 )