外汇交易的基础数学·进阶篇
📐

外汇交易的基础数学·进阶篇

(2/3)·为什么人眼能认趋势却总慢半拍?从订单流与离散价格看真实市况

新手友好 第 2/3 篇
很多交易者把「看出趋势」当成掌握了规律,其实你看到的是已触发之后的图形。限价单和市价单构成的市场深度,才是价格加速或反转的真正伏笔,只是肉眼看不见。

「把K线序列倒过来存,再按步长切节点」

这段逻辑先把当前图表的高、低、tick量、时间一股脑拷进数组,然后全部用 ArraySetAsSeries(...,true) 改成时间序列倒序——索引0就是最新一根,老手在MT5里验证时直接 Print(High[0]) 就能看到实时最高价,不用再算偏移。 CopyHigh(_Symbol,_Period,0,BarsI,High) 表示从当前品种当前周期、从0号柱开始取 BarsI 根到 High 数组;CopyTickVolume 同理拿tick成交量,外汇和贵金属的tick量只是报价次数、不代表真实成交手数,解读时得留个心眼,这类品种杠杆高、滑点可能吞掉你的节点逻辑。 节点结构 Target 存了四个字段:Price0 节点价、Time0 时间、Direction 涨跌方向、bActive 是否激活。UpdatePoints 是核心——当价格离 StartTick 的绝对点数差 >= StepPoints 时才落一个新节点,并把老节点整体后移一位。 StartCalculations 用已收盘的 Close[j] 从 BarsI-2 往回扫,提前把历史节点铺好;注意它跳过了最后一根,实盘里最新未收杆不会进初始计算,避免未来函数。StepPoints 设多大直接决定节点密度,EURUSD 设 50 点可能一晚上十几个节点,设 200 点就稀疏很多,开MT5改这个参数就能感受差异。

MQL5 / C++
CopyHigh(_Symbol,_Period,class="num">0,BarsI,High);
CopyLow(_Symbol,_Period,class="num">0,BarsI,Low);
CopyTickVolume(_Symbol,_Period,class="num">0,BarsI,Volume);
CopyTime(_Symbol,_Period,class="num">0,BarsI,Time);
}
ArraySetAsSeries(Close,true);
ArraySetAsSeries(Open,true);
ArraySetAsSeries(High,true);
ArraySetAsSeries(Low,true);
ArraySetAsSeries(Volume,true);
ArraySetAsSeries(Time,true);
SymbolInfoTick(Symbol(),TickAlphaPsi);
Bid=TickAlphaPsi.bid;
Ask=TickAlphaPsi.ask;
}
class=class="str">"cmt">////////////////////////////////////////////////////////////
class="kw">struct Targetclass=class="str">"cmt">//structure for storing node data
  {
  class="type">class="kw">double Price0;class=class="str">"cmt">//node price
  class="type">class="kw">datetime Time0;class=class="str">"cmt">//node price
  class="type">bool Direction;class=class="str">"cmt">//direction of a step ending at the current node
  class="type">bool bActive;class=class="str">"cmt">//whether the node is active
  };

class="type">class="kw">double StartTick;class=class="str">"cmt">//initial tick price
Target Targets[];class=class="str">"cmt">//destination point ticks(points located from the previous one by StepPoints)
class="type">bool UpdatePoints(class="type">class="kw">double Price00,class="type">class="kw">datetime Time00)class=class="str">"cmt">//update the node array and class="kw">return &class="macro">#x27;true&class="macro">#x27; in case of a new node
  {
  if ( MathAbs(Price00-StartTick)/Point >= StepPoints )class=class="str">"cmt">//if the step size reaches the required one, write it and shift the array back
    {
    for(class="type">int i=ArraySize(Targets)-class="num">1;i>class="num">0;i--)class=class="str">"cmt">//first move everything back
      {
      Targets[i]=Targets[i-class="num">1];
      }
    class=class="str">"cmt">//after that, generate a new node
    Targets[class="num">0].bActive=true;
    Targets[class="num">0].Time0=Time00;
    Targets[class="num">0].Price0=Price00;
    Targets[class="num">0].Direction= Price00 > StartTick ? true : false;
    class=class="str">"cmt">//finally, redefine the initial tick to track the next node
    StartTick=Price00;
    class="kw">return true;
    }
  else class="kw">return false;
  }
class="type">void StartCalculations()class=class="str">"cmt">//approximate initial calculations(by bar closing prices)
  {
  for(class="type">int j=class="type">int(BarsI)-class="num">2;j>class="num">0;j--)
    {
    UpdatePoints(Close[j],Time[j]);
    }
  }
class="type">int S[];class=class="str">"cmt">//array of final upward steps

◍ 步数拆分与概率数组的底层构造

做随机游走式的路径统计,第一步是把总步数拆成上行和下行两个序列。CalcNumSteps 用整除判断奇偶:偶数步返回 Steps0/2,奇数步返回 (Steps0-1)/2,这决定了后续数组的维度上限。 ReadyArrays 负责按算出的 Size 重设 S、U、D、P 四个数组并清零。注意这里 S 数组在原文外层声明但未贴全,本地若直接编译会报未声明,需补一句 double S[]; 才能跑通。 CalculateAllArrays 是核心:先拿 CombTotal(Steps0) 算总组合数 CT,再循环填 S[i](净位移)、U[i](上行步数)、D[i](下行步数)、P[i]=C(Steps0,U[i])/CT(该路径概率)。例如 Steps0=10 时,中间 S[5]=10、U[5]=10、D[5]=0 的概率为 1/1024,两端对称。

CalculateBettaNeutral 根据 S[0] 是否为 0 分两支累加S[i]*P[i],实质是在算中性 Betta 的加权绝对位移期望。外汇与贵金属杠杆高,这类期望只是路径可能性的度量,不等于价格会朝哪边走。
MQL5 / C++
class="type">int U[];class=class="str">"cmt">//array of upward steps
class="type">class="kw">double P[];class=class="str">"cmt">//array of particular outcome probabilities
class="type">class="kw">double KBettaMid;class=class="str">"cmt">//neutral Betta ratio value
class="type">class="kw">double KBettaMax;class=class="str">"cmt">//maximum Betta ratio value
class=class="str">"cmt">//minimum Betta = class="num">0, there is no point in setting it
class="type">class="kw">double KAlphaMax;class=class="str">"cmt">//maximum Alpha ratio value
class="type">class="kw">double KAlphaMin;class=class="str">"cmt">//minimum Alpha ratio value
class=class="str">"cmt">//average Alpha = class="num">0, there is no point in setting it
class="type">int CalcNumSteps(class="type">int Steps0)class=class="str">"cmt">//calculate the number of steps
  {
  if ( Steps0/class="num">2.0-MathFloor(Steps0/class="num">2.0) == class="num">0 ) class="kw">return class="type">int(Steps0/class="num">2.0);
  else class="kw">return class="type">int((Steps0-class="num">1)/class="num">2.0);
  }
class="type">void ReadyArrays(class="type">int Size0,class="type">int Steps0)class=class="str">"cmt">//prepare the arrays
  {
  class="type">int Size=CalcNumSteps(Steps0);
  ArrayResize(S,Size);
  ArrayResize(U,Size);
  ArrayResize(D,Size);
  ArrayResize(P,Size);
  ArrayFill(S,class="num">0,ArraySize(S),class="num">0);class=class="str">"cmt">//clear
  ArrayFill(U,class="num">0,ArraySize(U),class="num">0);
  ArrayFill(D,class="num">0,ArraySize(D),class="num">0);
  ArrayFill(P,class="num">0,ArraySize(P),class="num">0.0);
  }
class="type">void CalculateAllArrays(class="type">int Size0,class="type">int Steps0)class=class="str">"cmt">//calculate all arrays
  {
  ReadyArrays(Size0,Steps0);
  class="type">class="kw">double CT=CombTotal(Steps0);class=class="str">"cmt">//number of combinations
  for(class="type">int i=class="num">0;i<ArraySize(S);i++)
    {
    S[i]=Steps0/class="num">2.0-MathFloor(Steps0/class="num">2.0) == class="num">0 ? i*class="num">2 : i*class="num">2+class="num">1 ;
    U[i]=class="type">int((S[i]+Steps0)/class="num">2.0);
    D[i]=Steps0-U[i];
    P[i]=C(Steps0,U[i])/CT;
    }
  }
class="type">void CalculateBettaNeutral()class=class="str">"cmt">//calculate all Alpha and Betta ratios
  {
  KBettaMid=class="num">0.0;
  if ( S[class="num">0]==class="num">0 )
    {
    for(class="type">int i=class="num">0;i<ArraySize(S);i++)
      {
      KBettaMid+=MathAbs(S[i])*P[i];
      }
    for(class="type">int i=class="num">1;i<ArraySize(S);i++)
      {
      KBettaMid+=MathAbs(-S[i])*P[i];
      }
    }
  else
    {
    for(class="type">int i=class="num">0;i<ArraySize(S);i++)
      {
      KBettaMid+=MathAbs(S[i])*P[i];
      }
    for(class="type">int i=class="num">0;i<ArraySize(S);i++)
      {
      KBettaMid+=MathAbs(-S[i])*P[i];
      }
    }
  }

组合数与概率数组的底子怎么打

这套算法里先补了两个数学底函数:阶乘 Factorial(n) 和组合 C(n,k),后者直接拿阶乘相除得到「从 n 里取 k 种」的理论数。CombTotal(n) 则用 MathPow(2.0,n) 给出总可能数——比如 n=10 时就是 1024 种路径,这是后面算胜率分母的硬基准。 真正落地的核心是 ReadyMainArrays(),它按步数数组 S 的首项是否为 0 分两套尺寸去 resize。S[0]==0 时,Np、Pp 长度和 S 一致,而 Nm、Pm、Sm 少一个元素;否则全部和 S 等长。Sm 里存的是各步亏损步数,取 -S[i+1] 或 -S[i],符号翻转是为了后面统一用正数做计数。 初始化全靠 ArrayFill 把 Np/Nm/Pp/Pm 清零,避免上一轮回测残留污染新标的。外汇与贵金属波动跳空频繁,跑之前务必确认 S 数组已按品种特性填好,否则这些概率数组从根上就偏了。

MQL5 / C++
class="type">class="kw">double Factorial(class="type">int n)class=class="str">"cmt">//factorial of n value
  {
  class="type">class="kw">double Rez=class="num">1.0;
  for(class="type">int i=class="num">1;i<=n;i++)
    {
    Rez*=class="type">class="kw">double(i);
    }
  class="kw">return Rez;
  }

class="type">class="kw">double C(class="type">int n,class="type">int k)class=class="str">"cmt">//combinations from n by k
  {
  class="kw">return Factorial(n)/(Factorial(k)*Factorial(n-k));
  }

class="type">class="kw">double CombTotal(class="type">int n)class=class="str">"cmt">//number of combinations in total
  {
  class="kw">return MathPow(class="num">2.0,n);
  }
class="type">class="kw">double AlphaPercent;class=class="str">"cmt">//alpha trend percentage
class="type">class="kw">double BettaPercent;class=class="str">"cmt">//betta trend percentage
class="type">int ActionsTotal;class=class="str">"cmt">//total number of unique cases in the Array of steps considering the number of steps for checking the option
class="type">int Np[];class=class="str">"cmt">//number of actual profitable outcomes of a specific case
class="type">int Nm[];class=class="str">"cmt">//number of actual losing outcomes of a specific case
class="type">class="kw">double Pp[];class=class="str">"cmt">//probability of a specific profitable step
class="type">class="kw">double Pm[];class=class="str">"cmt">//probability of a specific losing step
class="type">int Sm[];class=class="str">"cmt">//number of losing steps
class="type">void ReadyMainArrays()class=class="str">"cmt">//prepare the main arrays
  {
  if ( S[class="num">0]==class="num">0 )
    {
    ArrayResize(Np,ArraySize(S));
    ArrayResize(Nm,ArraySize(S)-class="num">1);
    ArrayResize(Pp,ArraySize(S));
    ArrayResize(Pm,ArraySize(S)-class="num">1);    
    ArrayResize(Sm,ArraySize(S)-class="num">1);
    for(class="type">int i=class="num">0;i<ArraySize(Sm);i++)
      {
      Sm[i]=-S[i+class="num">1];
      }
    ArrayFill(Np,class="num">0,ArraySize(Np),class="num">0);class=class="str">"cmt">//clear
    ArrayFill(Nm,class="num">0,ArraySize(Nm),class="num">0);
    ArrayFill(Pp,class="num">0,ArraySize(Pp),class="num">0);
    ArrayFill(Pm,class="num">0,ArraySize(Pm),class="num">0);    
    }
  else
    {
    ArrayResize(Np,ArraySize(S));
    ArrayResize(Nm,ArraySize(S));
    ArrayResize(Pp,ArraySize(S));
    ArrayResize(Pm,ArraySize(S));    
    ArrayResize(Sm,ArraySize(S));
    for(class="type">int i=class="num">0;i<ArraySize(Sm);i++)
      {
      Sm[i]=-S[i];
      }
    ArrayFill(Np,class="num">0,ArraySize(Np),class="num">0);class=class="str">"cmt">//clear

「路径计数与概率分布的数组实现」

在二叉树式价格路径推演里,先要把上一轮累积的 Np、Nm、Pp、Pm 四个数组清零,否则旧计数会污染本轮统计。用 ArrayFill 按 ArraySize 长度填 0 是最直接的做法,比循环赋值少写几行也更快。 CalculateActionsTotal 给出的 ActionsTotal = (Size0-1)-(Steps0-1),这是从总步数里扣除末端窗口后「还能摆动」的路径总数。比如 Size0=10、Steps0=3 时,ActionsTotal 算出来是 6,意味着外层循环只跑 1 到 6。 CalculateMainArrays 里对每条路径先数出向上步 U0 和向下步 D0,再用 S0=U0-D0 得到净位移。拿着 S0 去和预设的 S[]、Sm[] 档位比对,命中哪个档就把对应 Np[k] 或 Nm[k] 加一,break 退出避免重复计数。 最后一段把 Np[k] 除以 ActionsTotal 强转 double,得到该净位移档的出现频率 Pp[k]。外汇与贵金属杠杆高,这类路径概率只反映模型假设下的分布倾向,实盘价格可能明显偏离。 下面这段是原文核心逻辑,逐行拆一下: ArrayFill(Np,0,ArraySize(Np),0); // 从下标0开始,把Np整个数组填0,清空上一轮向上计数 ArrayFill(Nm,0,ArraySize(Nm),0); // 同理清空向下计数 ArrayFill(Pp,0,ArraySize(Pp),0); // 清空向上概率 ArrayFill(Pm,0,ArraySize(Pm),0); // 清空向下概率 for(int i=1;i<=ActionsTotal;i++) // 遍历每条可能路径编号 U0=0; D0=0; S0=0; // 每条路径初值归零 for(int j=0;j<Steps0;j++) // 在窗口内逐步看方向 if(Targets[ArraySize(Targets)-1-i-j].Direction) U0++; // 方向为真计向上 else D0++; // 否则计向下 S0=U0-D0; // 净步数差 for(int k=0;k<ArraySize(S);k++) // 比对向上档位 if(S[k]==S0){ Np[k]++; break; } // 命中则计数+1并跳出 for(int k=0;k<ArraySize(Sm);k++) // 比对向下档位 if(Sm[k]==S0){ Nm[k]++; break; } for(int k=0;k<ArraySize(S);k++) Pp[k]=Np[k]/double(ActionsTotal); // 频率化

MQL5 / C++
ArrayFill(Np,class="num">0,ArraySize(Np),class="num">0);
ArrayFill(Nm,class="num">0,ArraySize(Nm),class="num">0);
ArrayFill(Pp,class="num">0,ArraySize(Pp),class="num">0);
ArrayFill(Pm,class="num">0,ArraySize(Pm),class="num">0);
for(class="type">int i=class="num">1;i<=ActionsTotal;i++)
  {
  U0=class="num">0;
  D0=class="num">0;
  S0=class="num">0;
  for(class="type">int j=class="num">0;j<Steps0;j++)
    {
    if(Targets[ArraySize(Targets)-class="num">1-i-j].Direction) U0++;
    else D0++;
    }
  S0=U0-D0;
  for(class="type">int k=class="num">0;k<ArraySize(S);k++)
    {
    if(S[k]==S0){ Np[k]++; break; }
    }
  for(class="type">int k=class="num">0;k<ArraySize(Sm);k++)
    {
    if(Sm[k]==S0){ Nm[k]++; break; }
    }
  }
for(class="type">int k=class="num">0;k<ArraySize(S);k++)
  {
  Pp[k]=Np[k]/class="type">class="kw">double(ActionsTotal);
  }

◍ Alpha 与 Betta 百分比的归一化输出

在统计完多空节点分布后,需要把原始加权值压缩成 0~100 的区间量,方便直接在图表上读数。下面这段逻辑先把 Sm 数组的每个元素除以总动作数 ActionsTotal,得到归一化概率 Pm[k],再分别用 S 和 Sm 与对应概率做加权累加。 AlphaPercent 用 KAlphaMax 做分母折算成百分比,BettaPercent 则相对 KBettaMid 做偏移归一:大于等于中点时按 (KBettaMax-KBettaMid) 缩放,小于时按 KBettaMid 缩放。这种非对称处理让中性区的波动不会吃掉极端区的显示空间。 最后用 Comment 把两个值打印在左上角,格式固定为「Alpha = x % / Betta = y %」。外汇与贵金属市场杠杆高、跳空频繁,这类统计指标只反映历史节点密度,对未来方向仅是概率倾向,实盘需自担风险。 初始化函数 OnInit 里依次完成缓冲区映射、数组维度分配、基准价记录与多轮预计算;OnCalculate 每次 tick 重算 MQL5 行情值后,即可驱动上述百分比刷新。打开 MT5 把这段接进你自己的指标,改 KAlphaMax 与 KBettaMid 两个常量,能直接观察阈值对读数的拉伸效果。

MQL5 / C++
for(class="type">int k=class="num">0;k<ArraySize(Sm);k++)
  {
  Pm[k]=Nm[k]/class="type">class="kw">double(ActionsTotal);
  }   

AlphaPercent=class="num">0.0;
BettaPercent=class="num">0.0;
for(class="type">int k=class="num">0;k<ArraySize(S);k++)
  {
  AlphaPercent+=S[k]*Pp[k];
  BettaPercent+=MathAbs(S[k])*Pp[k];
  }
for(class="type">int k=class="num">0;k<ArraySize(Sm);k++)
  {
  AlphaPercent+=Sm[k]*Pm[k];
  BettaPercent+=MathAbs(Sm[k])*Pm[k];
  }
AlphaPercent= (AlphaPercent/KAlphaMax)*class="num">100;
BettaPercent= (BettaPercent-KBettaMid) >= class="num">0.0 ? ((BettaPercent-KBettaMid)/(KBettaMax-KBettaMid))*class="num">100 : ((BettaPercent-KBettaMid)/KBettaMid)*class="num">100;

Comment(StringFormat("Alpha = %.f %%\nBetta = %.f %%",AlphaPercent,BettaPercent));class=class="str">"cmt">//display these numbers on the screen
class="kw">return true;
  }
 else class="kw">return false;
 }
class="type">int OnInit()
  {
  class=class="str">"cmt">//--- indicator buffers mapping
  SetIndexBuffer(class="num">0,NeutralBuffer,INDICATOR_DATA);
  SetIndexBuffer(class="num">1,CurrentBuffer,INDICATOR_DATA);
  CleanAll();
  DimensionAllMQL5Values();
  CalcAllMQL5Values();
  StartTick=Close[BarsI-class="num">1];
  ArrayResize(Targets,StepsMemoryI);class=class="str">"cmt">//maximum number of nodes
  CalculateAllArrays(StepsMemoryI,StepsI);
  CalculateBettaNeutral();
  StartCalculations();
  ReadyMainArrays();
  CalculateActionsTotal(StepsMemoryI,StepsI);
  class="kw">return(INIT_SUCCEEDED);
  }
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const class="type">int &spread[])
  {
  CalcAllMQL5Values();

把双数组拼回指标缓冲区的收尾写法

这段逻辑出现在指标计算的末尾,负责把前面拆出来的中性价与当前价两个数组,按时间顺序回填到 MT5 的缓冲区内。先以 rates_total 减去 Sm、S 两个数组长度再减 1,得到写入起始游标 iterator,避免覆盖已有柱线。 第一个循环遍历 Sm 长度,把 P 数组倒序(ArraySize(S)-1-i)写入 NeutralBuffer,把 Pm 倒序写入 CurrentBuffer;第二个循环遍历 S 长度,正序把 P[i] 和 Pp[i] 分别填入同名缓冲区。两个循环共用并自增 iterator,保证新旧两段价格序列在缓冲区内连续排列。 最后 return(rates_total) 告知终端本次重绘覆盖全部柱线。外汇与贵金属行情跳空频繁,这类缓冲区回填若游标算错,可能在高波动时段出现图形错位,建议在 MT5 用 EURUSD 的 M1 数据手动改 ArraySize 验证一次。

MQL5 / C++
  if ( UpdatePoints(Close[class="num">0],TimeCurrent()) )
    {
     if ( CalculateMainArrays(StepsI) )
      {
       if ( bDrawE ) RedrawAll();
      }
    }

   class="type">int iterator=rates_total-(ArraySize(Sm)+ArraySize(S))-class="num">1;
   for(class="type">int i=class="num">0;i<ArraySize(Sm);i++)
     {
      iterator++;
      NeutralBuffer[iterator]=P[ArraySize(S)-class="num">1-i];
      CurrentBuffer[iterator]=Pm[ArraySize(Sm)-class="num">1-i];
     }
   for(class="type">int i=class="num">0;i<ArraySize(S);i++)
     {
      iterator++;
      NeutralBuffer[iterator]=P[i];
      CurrentBuffer[iterator]=Pp[i];
     }
   class="kw">return(rates_total);
}
把盘口结构交给小布盯盘
这些有关限价簇与止损分布的盘口诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只管判断要不要跟。

常见问题

因为肉眼只能在形态触发后识别,而入场需要触发前的预期。数学上价格是离散函数,模式不保证连续,点差和限价间隙会切断你的直觉。
可以,小布盯盘的品种页会把重要价位附近的限价密度与止损墙可视化,省去你手动翻深度数据的重复劳动。
点差是买卖限价单之间的最小价格空隙,以最小变动单位计。它代表流动性摩擦,也是市价单进场即浮亏的来源之一。
分散的止损位形成价格加速或反转水平,数量与交易量决定权重。知道这些水平,才可能说价格「倾向于」在哪变向。
窗口内容是时间参数 t 的离散函数,tick 是最小不可分元素。柱线和周期只是更大粒度的聚合,底层仍是有限报价点。