外汇交易的基础数学·进阶篇
(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改这个参数就能感受差异。
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 的加权绝对位移期望。外汇与贵金属杠杆高,这类期望只是路径可能性的度量,不等于价格会朝哪边走。 |
|---|
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 数组已按品种特性填好,否则这些概率数组从根上就偏了。
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); // 频率化
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 两个常量,能直接观察阈值对读数的拉伸效果。
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 验证一次。
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); }