单一资产交易顺序中的风险评估. 续篇·进阶篇
📉

单一资产交易顺序中的风险评估. 续篇·进阶篇

(2/3)· 当交易序列不再独立同分布,如何用简化模型与代码把隐藏风险量化出来

含代码示例实战向 第 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 专家日志里推荐仓位比例如何随分位上移。

MQL5 / C++
   }
      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 用历史数据复跑。

MQL5 / C++
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 以上才稳。

MQL5 / C++
   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 个不重叠年份样本,看分布参数点估计是否跳变。

把序列诊断交给小布
这些子系统拆分与分布一致性判断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到分段统计,你只需决定要不要按简化版模型重算仓位。

常见问题

实践里序列常违反独立同分布,整体均值会掩盖子系统差异,导致实盘风险被低估,需先拆分或简化。
将存在依赖或分步加仓的系统切成满足条件的片段,如奇偶单分开或每步加仓视为独立系统,再综合成微型投资组合。
跟踪止损让退出价随行情移动,收益分布不再固定两值,需用路径模拟或简化假设来近似量化尾部风险。
目前内置的 AIGC 会对成交序列做分段一致性检验并提示可疑依赖,但是否采用简化模型仍由你定。
实战中可取所有交易的平均典型 k 值构建简化系统,在牺牲部分精度下换得可计算的分离分布风险估计。