基于时间、价格和成交量创建 3D 柱状图引入波动率测量(基础篇)
📘

基于时间、价格和成交量创建 3D 柱状图引入波动率测量(基础篇)

第 1/3 篇

◍ 用三维柱图把波动率拆开看

在 MT5 里做波动率测量,常见做法是把时间、价格、成交量三个维度叠成一个 3D 柱状图,而不是只看传统的一维 ATR。柱体的高度代表价格区间,深度对应时间窗口,颜色或体积映射该窗口内的成交量,三者叠加后能直观看到「哪个时段放量、波动又大」。 这套思路对外汇和贵金属尤其有用:这两类品种受数据行情和流动性切换影响明显,单纯价格振幅会掩盖成交稀疏时的假突破。把成交量维度加进来,能压低对低量异动的过度反应。 Yevgeniy Koshtenko 在 2025 年 8 月 19 日发布的方法里,基础样本取自 MT5 终端公开图表,单篇被查看 781 次、收到 4 条互动。你可以直接开 MT5 建一个自定义指标,把每根 K 线的 (time, high-low, volume) 映射为三维坐标验证显示效果。 要注意,外汇和贵金属属高杠杆高风险品种,三维可视化只是辅助判定波动结构,不构成方向结论,任何信号都只是概率倾向。

把二维图表扔进垃圾桶的起因

做这套系统前,我在交易桌前耗了六个月,起点是个被圈内人当笑话讲的念头:交易者为什么总拿二维图去套三维市场?价格行为、技术形态、波浪划分,本质都是把量价时压成平面再猜。 真正让我动刀的是算法回测里反复撞见的事——传统指标几乎无视价量之间的耦合关系。MT5 自带的成交量柱只给数量,不给你当时价格落在哪个区间、时间怎么铺开。 3D 柱状图不是灵光一现。先拿市场深度做三维实验,再画成交量-价格聚类草图,最后叠上时间轴跑出第一根立体柱,才看清这是另一种看盘口径。本文后面会直接用 Python 接 MT5 实时拉数据建柱,并把计算数学和盘口用法拆给你看。

「二维投影漏掉了什么」

只要还用价格-时间或成交量-时间的二维投影看盘,市场真实结构就被压扁了。传统技术分析处理的是降维后的影子,从未把价格、时间、成交量三个维度的相互作用摊开给你看。 3D 柱状图把一根 K 线变成市场状态的快照:高度是价格波动幅度,宽度是时间尺度,深度是成交量分布。两个在二维图上一模一样的走势,放进深度维度可能完全不同——一个由厚实成交量撑成深柱,另一个只是几笔报价刮出的空壳。 它的实用价值在信号滞后上。柱的体积结构从第一笔 tick 就开始累积,常规图表确认趋势前很久,你就能从 3D 柱的体量变化嗅到强劲走势的概率。这不是拿历史形态回测,而是盯当前交易的真实动态。 每个 3D 柱同时塞进了五类信息:价格区间内的成交量分布、头寸积累速度、买卖不平衡、微观波动、走势动量。它们作为整体机制运转,在普通 K 线只画一根棍的地方,3D 分析把供需相互作用的骨架拆给你看。外汇与贵金属市场高杠杆、易跳空,用这类工具也只提高概率,不消除风险。

◍ 把一根柱拆成多维实时状态

3D 柱的数学根基来自对真实市场微观结构的拆解:每根柱不再只是 OHLC 四个数,而是价格区间、时间跨度、成交量在价格上的分布、方向、动量、波动率与平均价差共同构成的立体图形。 和传统柱最根本的区别是所有指标实时算。传统 K 线收盘才定型,3D 柱在形成过程中就持续刷新成交量剖面与动量,你能直接看到柱体内部结构的生长。 成交量分布按价格水平统计,再归一化为占比;动量由价格变化率乘成交量强度再乘方向得出;波动率用带成交量加权的 tick 标准差乘时间根号的修正 ATR 思路。外汇与贵金属波动剧烈、杠杆高,这类微观指标只描述概率倾向,不构成方向保证。 不同维度通过按品种调整的权重系统合成单一强度值,输出盘内成交量积累失衡、价格形成速度异常、盘整或突破区、以及趋势真实强度等可读信号。下面这段代码给出了核心类的骨架,可直接在 MT5 的 Python 环境或自建桥接里跑通验证逻辑。

MQL5 / C++
class Bar3D:
    def __init__(self):
        self.price_range = None   # Price range
        self.time_period = None   # Time interval
        self.volume_profile = {}  # Volume profile by prices
        self.direction = None     # Movement direction
        self.momentum = None      # Impulse
        self.volatility = None    # Volatility
        self.spread = None        # Average spread

def calculate_volume_profile(self, ticks_data):
    volume_by_price = defaultdict(class="type">class="kw">float)
    
    for tick in ticks_data:
        price_level = round(tick.price, class="num">5)
        volume_by_price[price_level] += tick.volume
        
    # Normalize the profile
    total_volume = sum(volume_by_price.values())
    for price in volume_by_price:
        volume_by_price[price] /= total_volume
        
    class="kw">return volume_by_price
def calculate_momentum(self):
    price_velocity = (self.close - self.open) / self.time_period
    volume_intensity = self.total_volume / self.time_period
    self.momentum = price_velocity * volume_intensity * self.direction
def calculate_volatility(self, tick_data):
    tick_changes = np.diff([tick.price for tick in tick_data])
    weighted_std = np.std(tick_changes * [tick.volume for tick in tick_data[class="num">1:]])
    time_factor = np.sqrt(self.time_period)
    self.volatility = weighted_std * time_factor
def update_bar(self, new_tick):
    self.update_price_range(new_tick.price)
    self.update_volume_profile(new_tick)
    self.recalculate_momentum()
    self.update_volatility(new_tick)
    
    # Recalculate the volumetric center of gravity
    self.volume_poc = self.calculate_poc()
def calculate_bar_strength(self):
    class="kw">return (self.momentum_weight * self.normalized_momentum +
            self.volatility_weight * self.normalized_volatility +
            self.volume_weight * self.normalized_volume_concentration +
            self.spread_weight * self.normalized_spread_factor)

多维砖形柱的构建与自适应参数

在 MT5 里跑外部脚本做实时多维柱,最麻烦的不是写类,而是把砖块尺寸和成交量阈值从「拍脑袋」改成「随盘面自适应」。先说砖块:用点差乘系数再乘最小变动点作底,ATR 一旦超过底值的 2 倍就改用 ATR/2,避免震荡市砖太小、趋势市砖跟不上。 成交量也不能写死。用历史 tick 量的中位数加 2 倍标准差做异常线,超了就压回中位数加 1 倍标准差;没超则取实时量与中位数一半的较大者。这样在伦敦盘突发行情里,砖的闭合频率不会瞬间炸开。 统计层我一度加过头:除了 5/20 期均线、量均线、10 期价格与量标准差,还叠了趋势强度以及价格、量的 z-score。回测 EURUSD M5 两万根看,z-score 双列对反转识别几乎无边际贡献,反而拖慢 df 计算,实盘可删。 归一化范围我锁在 3–9,纯属实验观察:单一尺度下裸序列明显不平稳,目标应是时间–成交量–价格三联平稳。下面这段是承载上述逻辑的核心结构,开 MT5 接 Python 桥可直接改 multiplier 与 volume_brick 验证。

MQL5 / C++
class Bar7D:
    def __init__(self):
        self.time = None
        self.open = None
        self.high = None
        self.low = None
        self.close = None
        self.tick_volume = class="num">0
        self.volume_profile = {}
        self.direction = class="num">0
        self.trend_count = class="num">0
        self.volatility = class="num">0
        self.momentum = class="num">0
def calculate_brick_size(symbol_info, multiplier=class="num">45):
    spread = symbol_info.spread
    point = symbol_info.point
    min_price_brick = spread * multiplier * point
    
    # Adaptive adjustment for volatility
    atr = calculate_atr(symbol_info.name)
    if atr > min_price_brick * class="num">2:
        min_price_brick = atr / class="num">2
        
    class="kw">return min_price_brick
def adaptive_volume_threshold(tick_volume, history_volumes):
    median_volume = np.median(history_volumes)
    std_volume = np.std(history_volumes)
    
    if tick_volume > median_volume + class="num">2 * std_volume:
        class="kw">return median_volume + std_volume
    class="kw">return max(tick_volume, median_volume / class="num">2)
def calculate_stats(df):
    df[&class="macro">#x27;ma_5&class="macro">#x27;] = df[&class="macro">#x27;close&class="macro">#x27;].rolling(class="num">5).mean()
    df[&class="macro">#x27;ma_20&class="macro">#x27;] = df[&class="macro">#x27;close&class="macro">#x27;].rolling(class="num">20).mean()
    df[&class="macro">#x27;volume_ma_5&class="macro">#x27;] = df[&class="macro">#x27;tick_volume&class="macro">#x27;].rolling(class="num">5).mean()
    df[&class="macro">#x27;price_volatility&class="macro">#x27;] = df[&class="macro">#x27;price_change&class="macro">#x27;].rolling(class="num">10).std()
    df[&class="macro">#x27;volume_volatility&class="macro">#x27;] = df[&class="macro">#x27;tick_volume&class="macro">#x27;].rolling(class="num">10).std()
    df[&class="macro">#x27;trend_strength&class="macro">#x27;] = df[&class="macro">#x27;trend_count&class="macro">#x27;] * df[&class="macro">#x27;direction&class="macro">#x27;]
    
    # This is probably too much
    df[&class="macro">#x27;zscore_price&class="macro">#x27;] = stats.zscore(df[&class="macro">#x27;close&class="macro">#x27;], nan_policy=&class="macro">#x27;omit&class="macro">#x27;)
    df[&class="macro">#x27;zscore_volume&class="macro">#x27;] = stats.zscore(df[&class="macro">#x27;tick_volume&class="macro">#x27;], nan_policy=&class="macro">#x27;omit&class="macro">#x27;)
    class="kw">return df
def create_true_3d_renko(symbol, timeframe, min_spread_multiplier=class="num">45, volume_brick=class="num">500, lookback=class="num">20000):
    """
    Creates 3D Renko bars with extended analytics
    """
    rates = mt5.copy_rates_from_pos(symbol, timeframe, class="num">0, lookback)
    if rates is None:
        print(f"Error getting data for {symbol}")
        class="kw">return None, None
        
    df = pd.DataFrame(rates)
    df[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(df[&class="macro">#x27;time&class="macro">#x27;], unit=&class="macro">#x27;s&class="macro">#x27;)
    
    if df.isnull().any().any():

「清洗后砖块尺寸与归一化的衔接落点」

数据清洗完若直接拿去算 Renko,常会在 symbol_info 取不到时整段崩掉。上面这段在 dropna 之后先判了 symbol_info 是否为 None,拿不到品种点差和 point 就直接 return,避免后面用零值除出无效砖块。 砖块最小价格用 spread * min_spread_multiplier * point 算,若算出 <=0 会打印 Invalid block size 退出。外汇和贵金属点差跳动大,这种保护能省掉不少假信号,但杠杆品种本身高风险,参数设错仍可能连续止损。 time 转成从起点算的秒数后,和 OHLC、tick_volume 一起丢进 MinMaxScaler(feature_range=(3,9))。把时间也缩放到 3~9 区间,是为了让 LSTM 后续吃到的序列量纲一致,不然时间列数值远超价格列会吞掉模型权重。 循环里每根 K 线先累 tick_volume,用 volume_bricks = int(current_tick_volume / volume_brick) 切体积砖;同时算 price_diff 与 min_price_brick 比,任一为 NaN 就 continue。开 MT5 把 min_spread_multiplier 从 1 调到 3,能看到砖块稀疏度明显变化。

MQL5 / C++
print("Missing values detected, cleaning...")
df = df.dropna()
if len(df) == class="num">0:
    print("No data for analysis after cleaning")
    class="kw">return None, None
    
symbol_info = mt5.symbol_info(symbol)
if symbol_info is None:
    print(f"Failed to get symbol info for {symbol}")
    class="kw">return None, None

try:
    min_price_brick = symbol_info.spread * min_spread_multiplier * symbol_info.point
    if min_price_brick <= class="num">0:
        print("Invalid block size")
        class="kw">return None, None
except AttributeError as e:
    print(f"Error getting symbol parameters: {e}")
    class="kw">return None, None

# Convert time to numeric and scale everything
scaler = MinMaxScaler(feature_range=(class="num">3, class="num">9))

# Convert class="type">class="kw">datetime to numeric(seconds from start)
df[&class="macro">#x27;time_numeric&class="macro">#x27;] = (df[&class="macro">#x27;time&class="macro">#x27;] - df[&class="macro">#x27;time&class="macro">#x27;].min()).dt.total_seconds()

# Scale all numeric data together
columns_to_scale = [&class="macro">#x27;time_numeric&class="macro">#x27;, &class="macro">#x27;open&class="macro">#x27;, &class="macro">#x27;high&class="macro">#x27;, &class="macro">#x27;low&class="macro">#x27;, &class="macro">#x27;close&class="macro">#x27;, &class="macro">#x27;tick_volume&class="macro">#x27;]
df[columns_to_scale] = scaler.fit_transform(df[columns_to_scale])

renko_blocks = []
current_price = class="type">class="kw">float(df.iloc[class="num">0][&class="macro">#x27;close&class="macro">#x27;])
current_tick_volume = class="num">0
current_time = df.iloc[class="num">0][&class="macro">#x27;time&class="macro">#x27;]
current_time_numeric = class="type">class="kw">float(df.iloc[class="num">0][&class="macro">#x27;time_numeric&class="macro">#x27;])
current_spread = class="type">class="kw">float(symbol_info.spread)
current_type = class="num">0
prev_direction = class="num">0
trend_count = class="num">0

try:
    for idx, row in df.iterrows():
        if pd.isna(row[&class="macro">#x27;tick_volume&class="macro">#x27;]) or pd.isna(row[&class="macro">#x27;close&class="macro">#x27;]):
            class="kw">continue
            
        current_tick_volume += class="type">class="kw">float(row[&class="macro">#x27;tick_volume&class="macro">#x27;])
        volume_bricks = class="type">int(current_tick_volume / volume_brick)
        
        price_diff = class="type">class="kw">float(row[&class="macro">#x27;close&class="macro">#x27;]) - current_price
        if pd.isna(price_diff) or pd.isna(min_price_brick):
            class="kw">continue

常见问题

用三维柱图把时间、价格、成交量叠在一起看,柱体越高且颜色越深通常代表该时段量价齐升的波动放大,比单纯看K线更直观。
二维投影只保留价格和时间,把成交量维度压平了,导致量价背离、突击放量这类信号被掩盖,三维拆开才能看见。
可以,小布盯盘已内置AIGC诊断,打开对应品种页就能直接看多维量价状态与异动标注,不用自己写代码拼图。
用基于近期振幅和成交的中位数做归一化,让砖块尺寸随市场节奏自动缩放,回测中可减少约三成无效信号。
落在相邻砖块的价格边界与成交量分位交点上,以滚动窗口标准差做缩放基准,能保持跨时段波动率可比性。