单一资产交易顺序中的风险评估. 续篇·进阶篇
(2/3)· 当交易序列不再独立同分布,如何用简化模型与代码把隐藏风险量化出来
「蒙特卡洛下的回撤约束求解」
这段逻辑用蒙特卡洛思路在 MT5 里搜最优风险比例 r。先以 MathSrand(GetTickCount()) 播种随机序列,避免每次回测同一条伪随机路径;NB 次模拟中,每条路径按 pk 概率给资产赋值 kt,否则记 -1,构成收益序列 a[]。 核心循环里,A 是序列均值、A0 是最小项。若 A<G0(期望收益门槛)或 A0>0(无亏损样本),直接跳过该路径给 ropt 赋保守值;否则从 r=stp 起逐步放大仓位比例,用连乘 cn2 算组合净值,一旦净值回撤跌破 D0 或几何收益 g 低于 G0 就截断,记录可行 r。 dcalc() 是回撤计算器:传入累计净值 c[],从 c[0] 起步跟踪 mx 峰值与 mn 谷值,任意 c[i]≤0 直接返回 0(爆仓),否则返回 mn/mx 的最小比值作为最小收益 d。外汇与贵金属杠杆品种用这套,需明白随机路径对结果扰动大,结论仅具概率意义、实盘属高风险。 跑完 NB 条路径后 ArraySort(ropt) 取分位 nr=(uint)(dlt*NB),打印 ropt[nr] 作为分位最优 r。你可以把 dlt 从 0.1 调到 0.5,看 MT5 专家日志里推荐仓位比例如何随分位上移。
}
if(nmin<=NX) Print("nmin = ",nmin);
else Print("nmin 大于 ",NX);
class="kw">return;
}
class=class="str">"cmt">// 蒙特卡洛方法
MathSrand(GetTickCount());
class="type">class="kw">double a[],c[],ropt[NB],A0,A,r,g,d,cn1,cn2,stp=class="num">1.0/NR;
class="type">uint ir;
ArrayResize(a,na);
ArrayResize(c,na);
for(class="type">uint ib=class="num">0; ib<NB;++ib)
{
for(class="type">uint i=class="num">0; i<na;++i) if(MathRandomNonZero()<=pk) a[i]=kt; else a[i]=-class="num">1;
A=MathMean(a); A0=a[ArrayMinimum(a)];
if(A<G0) {ropt[ib]=class="num">0; class="kw">continue;}
if(A0>class="num">0) {ropt[ib]=class="num">1-stp; class="kw">continue;}
cn1=class="num">1;
for(ir=class="num">1;ir<NR;++ir)
{
r=ir*stp;
cn2=class="num">1; for(class="type">uint i=class="num">0; i<na;++i) cn2*=class="num">1+r*a[i];
if(cn2<cn1) {ropt[ib]=r-stp; class="kw">break;}
cn1=cn2;
g=(MathPow(cn2,class="num">1.0/na)-class="num">1)/r;
if(g<G0) {ropt[ib]=r-stp; class="kw">break;}
c[class="num">0]=class="num">1+r*a[class="num">0]; for(class="type">uint i=class="num">1; i<na;++i) c[i]=c[i-class="num">1]*(class="num">1+r*a[i]);
d=dcalc(c);
if(d<D0) {ropt[ib]=r-stp; class="kw">break;}
}
}
ArraySort(ropt);
class="type">uint nr=(class="type">uint)(dlt*NB);
Print("ropt = ",ropt[nr]);
}
class=class="str">"cmt">// dcalc() 函数接受 c1, c2, ... cN 数组值而且
class=class="str">"cmt">// 返回最小收益 d. Assume that c0==class="num">1
class="type">class="kw">double dcalc(class="type">class="kw">double &c[])
{
if(c[class="num">0]<=class="num">0) class="kw">return class="num">0;
class="type">class="kw">double d=c[class="num">0], mx=c[class="num">0], mn=c[class="num">0];
for(class="type">uint i=class="num">1; i<na;++i)
{
if(c[i]<=class="num">0) class="kw">return class="num">0;
if(c[i]<mn) {mn=c[i]; d=MathMin(d,mn/mx);}
else {if(c[i]>mx) mx=mn=c[i];}
}
class="kw">return d;
}
class=class="str">"cmt">//+------------------------------------------------------------------+用指数分布给跟踪止损系统做显著性检验
把固定的跟踪止损看成价格沿随机游走移动后的退出机制:进场价与初始止损差记为 Δp,当价格从最高点回撤 Δp 时平仓。若价格序列近似连续时间线性偏移随机游走,单笔收益分布会收敛到左移 1 的指数分布,分布函数写作 P(x)=1-exp(-θ(x+1))(x≥-1),平盘时 θ=1、上升 θ∈(0,1)、下跌 θ>1,期望 M=1/θ-1。 实盘里我们拿历史收益 a_i(均 >-1,滑点先忽略)估 θ。先算样本均值 M_apr,若 ≤0 直接弃用;若 >0 再做原假设 M=0 的检验:统计量 S=M_apr+1 在 M=0 时服从 Γ(n,1/n),需满足 P(S)≥P(1)+δ/2 才能否定原假设。 过了显著性,再估 θ_apr=1/(1+M_apr),此时 Λ 服从左移 1 的 Γ(n,θ_apr/n)。若其 δ 分位 ≥ G0+1,系统才满足最低收益约束;不满足就递增 n 找最小样本量 n_min。文中用 30 笔序列试算,结论是该跟踪止损序列在默认 (n,δ,G0,D0) 下仍不达标,即便给所有 a_i 加小正数消点差也一样。 MQL5 统计库的指数分布用期望 μ 作参,对应 μ=1/θ=M+1。默认参数算得 ropt=0.015,即单笔约冒 1.1% 权益。若脚本报错误 4,多半是终端版本旧,把 21–42 行伽玛计算注释掉即可,这部分可丢进 R 补算。外汇与贵金属杠杆高,此类统计结论只描述概率倾向,实盘前请在 MT5 用历史数据复跑。
class="macro">#include <Math\Stat\Gamma.mqh> class="macro">#include <Math\Stat\Exponential.mqh> class="macro">#class="kw">property script_show_inputs class="kw">input class="type">uint na=class="num">50; class=class="str">"cmt">// 序列中的交易数量 class="kw">input class="type">class="kw">double tht=class="num">0.65; class=class="str">"cmt">// 指数分布参数 class="num">0<tht<class="num">1 class="macro">#define NX class="num">300 class=class="str">"cmt">// 序列中交易的最大数量 (用于搜索 nmin) class="macro">#define NB class="num">10000 class=class="str">"cmt">// 用于蒙特卡洛方法生成样本的数量 class="macro">#define NR class="num">1000 class=class="str">"cmt">// 风险间隔分区的数量 class="type">class="kw">double G0=class="num">0.15; class=class="str">"cmt">// 最小平均收益 class="type">class="kw">double D0=class="num">0.9; class=class="str">"cmt">// 最小最低收益 class="type">class="kw">double dlt=class="num">0.05; class=class="str">"cmt">// 显著性水平 class="type">void OnStart() { if(D0<=class="num">0 || D0>=class="num">1) {Print("D0 必须为正且小于1"); class="kw">return;} class="type">class="kw">double m=class="num">1/tht-class="num">1; class=class="str">"cmt">// 期望值 if(m<=class="num">0) {Print("期望值必须为正"); class="kw">return;} class="type">int ner; class="type">class="kw">double p=MathCumulativeDistributionGamma(m+class="num">1,na,class="num">1.0/na,ner); if(!MathIsValidNumber(p)) {Print("MathIsValidNumber(p) error ",ner); class="kw">return;} class="type">class="kw">double p0=MathCumulativeDistributionGamma(class="num">1,na,class="num">1.0/na,ner); if(!MathIsValidNumber(p0)) {Print("MathIsValidNumber(p0) error ",ner); class="kw">return;} if(p<=p0+dlt/class="num">2) {Print("expected value is close to zero"); class="kw">return;} class="type">class="kw">double q=MathQuantileGamma(dlt,na,class="num">1/tht/na,ner),q0=G0+class="num">1; if(!MathIsValidNumber(q)) {Print("MathIsValidNumber(q) 错误 ",ner); class="kw">return;} if(q0>q) { Print("G0 大于 dlt 收益算数平均分值"); class="type">uint nmin=na+class="num">1; for(;nmin<=NX;++nmin) { q=MathQuantileGamma(dlt,nmin,class="num">1/tht/nmin,ner); q0=G0+class="num">1; if(!MathIsValidNumber(q)) {Print("MathIsValidNumber(q) 错误 ",ner); class="kw">return;} if(q0<=q) class="kw">break; } if(nmin<=NX) Print("nmin = ",nmin); else Print("nmin 大于 ",NX); class="kw">return; } class=class="str">"cmt">// 蒙特卡洛方法 MathSrand(GetTickCount()); class="type">class="kw">double a[],c[],ropt[NB],A0,A,r,g,d,cn1,cn2,stp=class="num">1.0/NR; class="type">uint ir; ArrayResize(a,na); ArrayResize(c,na); for(class="type">uint ib=class="num">0; ib<NB;++ib) { for(class="type">uint i=class="num">0; i<na;++i) {
◍ 从指数抽样到最优杠杆的逐行实现
下面这段 MQL5 片段把前面讲的指数分布收益抽样,直接落到可跑的寻优循环里。它先对 na=50 个样本做指数随机,再算均值与最小项,用来快速剔除明显不合规的仓位比例。 循环里以 stp 为步长扫描 r,把累计乘积 cn2 与上一轮 cn1 比较,一旦下滑就回退一步定下 ropt[ib]。同时用几何均值反推收益率 g,低于 G0=0.15 也立刻 break,相当于给贵金属或外汇网格策略设了一道盈利底线——这类品种波动大,杠杆越界可能直接爆仓。 dcalc() 是回撤核心:它顺着 c 数组跟踪 mx 与 mn,遇到新低就刷新 d=mn/mx,返回整段最小收益比。注意 c[0] 必须为正,否则直接 return 0,意味着该路径已破净。 最后 ArraySort(ropt) 后取 nr=(uint)(dlt*NB) 分位,打印 ropt[nr] 作为建议杠杆下界。配合文末 R 校验段(na=50, tht=0.65, dlt=0.05),若 l1&l3 为真会提示“G0 和 na 是可以的”,否则可能需把 na 提到 nmin 以上才稳。
a[i]=MathRandomExponential(m+class="num">1,ner); if(!MathIsValidNumber(a[i])) {Print("MathRandomExponential() 错误",ner); class="kw">return;} a[i]-=class="num">1.0; } A=MathMean(a); A0=a[ArrayMinimum(a)]; if(A<G0) {ropt[ib]=class="num">0; class="kw">continue;} if(A0>class="num">0) {ropt[ib]=class="num">1-stp; class="kw">continue;} cn1=class="num">1; for(ir=class="num">1;ir<NR;++ir) { r=ir*stp; cn2=class="num">1; for(class="type">uint i=class="num">0; i<na;++i) cn2*=class="num">1+r*a[i]; if(cn2<cn1) {ropt[ib]=r-stp; class="kw">break;} cn1=cn2; g=(MathPow(cn2,class="num">1.0/na)-class="num">1)/r; if(g<G0) {ropt[ib]=r-stp; class="kw">break;} c[class="num">0]=class="num">1+r*a[class="num">0]; for(class="type">uint i=class="num">1; i<na;++i) c[i]=c[i-class="num">1]*(class="num">1+r*a[i]); d=dcalc(c); if(d<D0) {ropt[ib]=r-stp; class="kw">break;} } } ArraySort(ropt); class="type">uint nr=(class="type">uint)(dlt*NB); Print("ropt = ",ropt[nr]); } class=class="str">"cmt">// dcalc() 函数接受 c1, c2, ... cN 数组值而且 class=class="str">"cmt">// 返回最小收益 d. Assume that c0==class="num">1 class="type">class="kw">double dcalc(class="type">class="kw">double &c[]) { if(c[class="num">0]<=class="num">0) class="kw">return class="num">0; class="type">class="kw">double d=c[class="num">0], mx=c[class="num">0], mn=c[class="num">0]; for(class="type">uint i=class="num">1; i<na;++i) { if(c[i]<=class="num">0) class="kw">return class="num">0; if(c[i]<mn) {mn=c[i]; d=MathMin(d,mn/mx);} else {if(c[i]>mx) mx=mn=c[i];} } class="kw">return d; } class=class="str">"cmt">//+------------------------------------------------------------------+ na=class="num">50; tht=class="num">0.65; dlt=class="num">0.05; G0=class="num">0.15; nx=class="num">300 m=class="num">1/tht-class="num">1; l1=l2=l3=FALSE l1 = m > class="num">0 if (l1) l2 = pgamma(m+class="num">1,na,scale = class="num">1/na) > pgamma(class="num">1,na,scale = class="num">1/na)+dlt/class="num">2 if (l2) l3 = G0+class="num">1 <= qgamma(dlt,na,scale = class="num">1/tht/na) if(!l1) cat("m 必须大于 class="num">0\n") if(l1 & l3) cat("G0 和 na 是可以的\n") else cat("na 太小或者 G0 太大了\n") if(l1 & l2 & !l3) {for (nmin in(na+class="num">1):(nx+class="num">1)) {if(G0+class="num">1 <= qgamma(dlt,nmin,scale = class="num">1/tht/nmin)) class="kw">break()} if (nmin<=nx) {cat("nmin =",nmin)} else {cat("nmin >",nx)} }
「把正态假设当起点而非终点」
上面这套点估计思路只是个朴素入口,它默认每笔交易独立且同分布,实际上这个前提在真实账户里大概率站不住。把收益率硬塞进某个参数族(比如正态或近正态)能跑通回测,但参数成员本身是否随品种、时段漂移,还得单独验证。 我们只做了分布参数的点估计,没碰置信区间。一旦你把均值和方差的波动区间画出来,系统盈利稳定性就会直接暴露——参数跨出区间时策略可能失效,这是 MT5 里用 OnTester 跑多组样本能复现的现象。 多种概率工具不必同时堆上实盘,但得在工具箱里备着。比起被随机波动愚弄,更划算的做法是开 MT5 策略测试器,对同一 EA 切 3~5 个不重叠年份样本,看分布参数点估计是否跳变。