利用文斯 (Vince) 进行资金管理。 作为 MQL5 向导模块实现(基础篇)
「把文斯仓位法做成 MT5 向导模块」
文斯(Vince)资金管理核心是用固定分数模型控制每笔风险,避免在连亏期把本金打穿。2018 年 4 月发布的这套实现把它做成了 MQL5 向导模块,交易者建 EA 时可直接勾选,不用手写道仓逻辑。 该模块在原始发布页累计被查看 10961 次、收到 8 条讨论,说明手动派和量化派都常卡在仓位系数落地这一步。外汇与贵金属杠杆高,连亏放大时爆仓概率陡增,任何仓位法都只是降低风险而非消除风险。 想验证就开 MT5:调出 EA 向导 → 选信号类 → 在资金管理里找 Vince 实现 → 把风险分数先设 0.01 跑历史。看净值曲线回撤是否比你原先的固定手数更平,再决定实盘分数。
手数比指标更先决定生死
很多交易者把精力全花在找买卖信号上,却忽略了一个更底层的变量:每笔交易下多少手。拉尔夫·文斯在《资金管理中的数学》里讲得很直白——执行交易的资金额度,和入场信号本身一样关键,甚至更先决定账户能否活下去。 两个被大众奉为常识的说法,文斯直接戳破。其一是“风险越高利润越高”,认为潜在利润是潜在风险的线性函数,这不成立;杠杆堆出来的暴露,在外汇和贵金属这种高波动品种里更可能一次性吞掉本金。 其二是“多元化能减少亏损”。文斯的原话是:多元化能减亏,但只在很有限的程度内,远低于大多数交易者的想象。把欧元、黄金、原油都开一遍,并不等于把风险摊薄了。 打开 MT5 的“交易”面板,把每笔 0.1 手和 0.5 手的回撤曲线拉出来对比,你会看到手数缩放对净值曲线的塑形效果,往往比换一个指标更猛。
◍ 复利下注为何会反噬盈利系统
用条件系统跑两笔交易就能看清问题:第一笔赚 50%,第二笔亏 40%。若不动用利润再投,总资产从 100 到 110,净赚 10%;若全程复利再投,第二笔按 150 的 40% 亏掉 60,账户只剩 90,净亏 10%。交易顺序不影响这个结果,说明复利模式下的手数逻辑必须和固定手数策略彻底分开。 文斯资金管理的核心,就是在复利框架里找最优手数系数。先从最简单的旋转硬币模型入手:赢拿 2 美元、输赔 1 美元,胜负概率各 1/2,本金 100。若一把梭 100,赢则 200,输则归零且无法续局;在无限游戏里重仓必死。把下注比例压到 0.1、0.2、0.5、0.7、1.0 分别跑 +2、-1 交替序列,期末资本依次是 126、141、100、37、0——存在某个中间系数使利润最大化。 不变赢率和赔率时,凯利公式直接给解:f=((B+1)*P-1)/B,B 为赢亏比,P 为胜率。实战中盈亏幅度乱跳,凯利失效,只能对历史成交流做数值搜索。文斯引入 HPR(持有期回报),单笔 HPR=1+f*回报/最大可能亏损,全局目标变为最大化连乘 HPR1*HPR2*…*HPRn。 下面这段 MQL5 用 {+2,-1} 数组扫 f,从 0.01 到 1.00 步进,抓出令终值最大的 K。可丢进 MT5 脚本跑,验证凯利结果。 别把高期望当护身符 流 {-1,1,1,1,1,1,1,1,1,1} 的数学期望极漂亮,算出来 f=0.8。意思是按统计最优应允许单次回撤 80% 本金——真撞上那根阴线,账户直接残废。外汇和贵金属自带高杠杆高风险,f 越大越可能一夜回到解放前,心理承受力得先于公式入场。 优化只对正期望系统有意义:亏损系统最优 f=0,手数管理救不活负期望;若历史全盈利无回撤,f=1 满仓即可。下方代码即搜索逻辑,改 arr[] 就能测你自己的成交序列。
class="type">class="kw">double PL[]={class="num">9,class="num">18,class="num">7,class="num">1,class="num">10,-class="num">5,-class="num">3,-class="num">17,-class="num">7}; class=class="str">"cmt">// 来自本书的利润/损失数组 class="type">class="kw">double Arr[]={class="num">2,-class="num">1}; class="type">void OnStart() { SearchMaxFactor(Arr); class=class="str">"cmt">//盈亏或任何其它数组 } class="type">void SearchMaxFactor(class="type">class="kw">double &arr[]) { class="type">class="kw">double MaxProfit=class="num">0,K=class="num">0; class=class="str">"cmt">// 最大盈利 class=class="str">"cmt">// 和利润对应的比率 for(class="type">int i=class="num">1;i<=class="num">100;i++) { class="type">class="kw">double k,profit,min; min =MathAbs(arr[ArrayMinimum(arr)]); class=class="str">"cmt">// 查找数组中的最大亏损 k =i*class="num">0.01; class=class="str">"cmt">// 系数从0.01步进到1.class="num">00 profit =class="num">1; class=class="str">"cmt">// 用设定的系数查找回报 for(class="type">int j=class="num">0;j<ArraySize(arr);j++) { profit =profit*(class="num">1+k*arr[j]/min); } class=class="str">"cmt">// 比较最大利润 if(profit>MaxProfit) { MaxProfit =profit; K=k; } } Print("优化 K ",K," 盈利 ",NormalizeDouble(MaxProfit,class="num">2)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Graphic.mq5 | class=class="str">"cmt">//| Orangetree | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Orangetree"
「用凯利变体给两笔盈亏找最优仓位系数」
这段脚本把「固定分数仓位」的优化思路直接落到 MT5 脚本里:给定一组盈亏序列 PL,遍历 0.01~1.00 的仓位系数 k,算出每个 k 下账户的累计增长倍数,再挑出使倍数最大的那个 k。 原文示例只放了 double PL[]={2,-1}; 两笔数据,即先赚 2 个单位、再亏 1 个单位。代码里先把 PL 的最小值取绝对值当成归一化分母 min,避免除零;若最小盈亏 ≥0 则直接判定 f=1 满仓并退出。 核心双层循环在 X 上扫 100 个 k 值,内层把 profit 初始化为 1,再对每个 PL 元素做 profit*=(1+k*PL[i]/min)。最终 Y 数组存每个 k 的终值,maxX/maxY 记录峰值。注意外汇与贵金属杠杆高,这种回测只反映历史序列特征,实盘可能严重偏离。 最后用 CGraphic 把 X-Y 曲线画成蓝点线,峰值标红点,并在日志 Print 出「最大因子 f」。你把这串代码贴进 MT5 脚本跑一遍,改 PL 数组就能看自己交易记录的最优 f 落在哪。
class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include<Graphics\Graphic.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序 start 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//class="type">class="kw">double PL[]={class="num">9,class="num">18,class="num">7,class="num">1,class="num">10,-class="num">5,-class="num">3,-class="num">17,-class="num">7}; // 来自本书的利润/损失数组 class="type">class="kw">double PL[]={class="num">2,-class="num">1}; class="type">void OnStart() { class="type">class="kw">double X[class="num">100]={class="num">0}; for(class="type">int i=class="num">1;i<=class="num">100;i++) X[i-class="num">1]=i*class="num">0.01; class="type">class="kw">double Y[class="num">100]; class="type">class="kw">double min =PL[ArrayMinimum(PL)]; if(min>=class="num">0){Comment("f=class="num">1");class="kw">return;} min =MathAbs(min); class="type">int n = ArraySize(X); class="type">class="kw">double maxX[class="num">1]= {class="num">0}; class="type">class="kw">double maxY[class="num">1] ={class="num">0}; for(class="type">int j=class="num">0;j<n;j++) { class="type">class="kw">double k =X[j]; class="type">class="kw">double profit =class="num">1; for(class="type">int i=class="num">0;i<ArraySize(PL);i++) { profit =profit*(class="num">1+k*PL[i]/min); } Y[j] =profit; if(maxY[class="num">0]<profit) { maxY[class="num">0] =profit; maxX[class="num">0] =k; } } CGraphic Graphic; Graphic.Create(class="num">0,"Graphic",class="num">0,class="num">30,class="num">30,class="num">630,class="num">330); CCurve *Curve=Graphic.CurveAdd(X,Y,ColorToARGB(clrBlue,class="num">255),CURVE_LINES,"Profit"); Curve.LinesStyle(STYLE_DOT); class=class="str">"cmt">//如果需要,图形可以平滑 class=class="str">"cmt">/*Curve.LinesSmooth(true); Curve.LinesSmoothTension(class="num">0.8); Curve.LinesSmoothStep(class="num">0.2);*/ CCurve *MAX =Graphic.CurveAdd(maxX,maxY,ColorToARGB(clrBlue,class="num">255),CURVE_POINTS,"Maximum"); MAX.PointsSize(class="num">8); MAX.PointsFill(true); MAX.PointsColor(ColorToARGB(clrRed,class="num">255)); Graphic.XAxis().MaxLabels(class="num">100); Graphic.TextAdd(class="num">30,class="num">30,"Text",class="num">255); Graphic.CurvePlotAll(); Graphic.Update(); Print("最大因子 f = ", maxX[class="num">0]); }
相关性怎么吃掉复利多元化的收益
拿两个参数完全相同的策略 A、B 来拆:盈亏分布都是 (+2, -1),单系统优化仓位 f=0.25,账户本金对半分。先把两系统相关性拉到 1.0——四笔交替交易后合并账户从 100 走到 126.56,盈利 26.56,和拿全部资本跑单个策略没区别。 把相关性降到 0,同样四笔交易里 A、B 盈亏错开,合并账户终值 142.39,盈利 42.39,比完全同步走高出约 15.8 个点。再极端一点,相关性 -1:A 赚时 B 必亏、反之亦然,四笔跑完账户到 160.18,盈利 60.18,是三种情形里最高的。 这组数说清一件事:复利下做多元化,分散的是相关性而不是亏损本身。f=0.25 时最大单笔回撤依旧在,只有相关系数恰好 -1 的那种互斥组合能绕开——而实盘里严格 -1 相关的系统基本不存在,近似情况等同于同一品种双向锁仓,外汇和贵金属市场里这种结构极脆弱、高风险。 文斯在原著里的判断很直:多元化若做得好,是抬升回报的手法,却不必然压缩最坏情形的回撤,这和圈内「分散必降风险」的流行认知正好相反。
◍ 用 Z 分数和序列相关扒开连盈亏的底牌
在动手拟合 f 系数之前,先看清盈亏流本身有没有记忆性。连盈之后还连盈、连亏之后还连亏,这种依赖关系用肉眼看回测曲线容易误判,得靠两个统计量说话。 Z 分数衡量的是实际连串长度偏离正态假设多少个标准差。MT5 策略测试器的回测报告里直接给了这个字段:负值意味着连续盈利或连续亏损的 streak 比随机分布少,也就是盈后易亏、亏后易盈的概率偏高;正值则相反,趋势性 streak 更密集。 序列相关算的是错位对齐后的相关系数。把 {1,2,3,4,5,6,7,8,9,10} 错一位变成 {1..9} 对 {2..10},两者相关系数就是一阶序列相关。若结果显著不为 0,说明上笔盈亏对下笔有线性牵引,单一 f 值资金管理可能不够用,得考虑双系数模块。 下面这段 MQL5 把两个指标都实现了,arr 里塞每笔盈亏(正为盈、负为亏)即可直接跑。Z 函数先数胜负笔数与连串数 R,再套公式;AutoCorr 则算错位协方差除以各自离差。 double Z(double &arr[]) { int n =ArraySize(arr); int W,L,X,R=1; if(arr[0]>0) { W=1; L=0; } else { W=0; L=1; } for(int i=1;i<n;i++) { if(arr[i]>0) { W++; if(arr[i-1]<=0){R++;} } else { L++; if(arr[i-1]>0){R++;} } } X =2*W*L; double x=(n*(R-0.5)-X); double y =X*(X-n); y=y/(n-1); double Z=(n*(R-0.5)-X)/pow(y,0.5); Print(Z); return Z; } double AutoCorr(double &arr[]) { int n =ArraySize(arr); double avr0 =0; for(int i=0;i<n-1;i++) { avr0=avr0+arr[i]; } avr0=avr0/(n-1); double avr1 =0; for(int i=1;i<n;i++) { avr1=avr1+arr[i]; } avr1=avr1/(n-1); double D0 =0; double sum =0.0; for(int i=0;i<n-1;i++) { sum =sum+(arr[i]-avr0)*(arr[i]-avr0); } D0 =MathSqrt(sum); double D1 =0; sum =0.0; for(int i=1;i<n;i++) { sum =sum+(arr[i]-avr1)*(arr[i]-avr1); } D1 =MathSqrt(sum); sum =0.0; for(int i=0;i<n-1;i++) { sum =sum +(arr[i]-avr0)*(arr[i+1]-avr1); }
| if(D0==0 | D1==0) return 1; |
|---|
double k=sum/(D0*D1); return k; } 实盘前先把历史 tick 跑出来的 arr 喂进去,Z 接近 0 且 AutoCorr 贴近 0,才谈得上近似独立。外汇和贵金属杠杆高,序列相关一旦被证实,原仓位公式可能放大回撤,调参前务必用 MT5 重测。
Z=(N(R-class="num">0.5)-Х)/((Х(Х-N))/(N-class="num">1))^(class="num">1/class="num">2) class="type">class="kw">double Z(class="type">class="kw">double &arr[]) { class="type">int n =ArraySize(arr); class="type">int W,L,X,R=class="num">1; if(arr[class="num">0]>class="num">0) { W=class="num">1; L=class="num">0; } else { W=class="num">0; L=class="num">1; } for(class="type">int i=class="num">1;i<n;i++) { if(arr[i]>class="num">0) { W++; if(arr[i-class="num">1]<=class="num">0){R++;} } else { L++; if(arr[i-class="num">1]>class="num">0){R++;} } } X =class="num">2*W*L; class="type">class="kw">double x=(n*(R-class="num">0.5)-X); class="type">class="kw">double y =X*(X-n); y=y/(n-class="num">1); class="type">class="kw">double Z=(n*(R-class="num">0.5)-X)/pow(y,class="num">0.5); Print(Z); class="kw">return Z; } class="type">class="kw">double AutoCorr(class="type">class="kw">double &arr[]) { class="type">int n =ArraySize(arr); class="type">class="kw">double avr0 =class="num">0; for(class="type">int i=class="num">0;i<n-class="num">1;i++) { avr0=avr0+arr[i]; } avr0=avr0/(n-class="num">1); class="type">class="kw">double avr1 =class="num">0; for(class="type">int i=class="num">1;i<n;i++) { avr1=avr1+arr[i]; } avr1=avr1/(n-class="num">1); class="type">class="kw">double D0 =class="num">0; class="type">class="kw">double sum =class="num">0.0; for(class="type">int i=class="num">0;i<n-class="num">1;i++) { sum =sum+(arr[i]-avr0)*(arr[i]-avr0); } D0 =MathSqrt(sum); class="type">class="kw">double D1 =class="num">0; sum =class="num">0.0; for(class="type">int i=class="num">1;i<n;i++) { sum =sum+(arr[i]-avr1)*(arr[i]-avr1); } D1 =MathSqrt(sum); sum =class="num">0.0; for(class="type">int i=class="num">0;i<n-class="num">1;i++) { sum =sum +(arr[i]-avr0)*(arr[i+class="num">1]-avr1); } if(D0==class="num">0||D1==class="num">0) class="kw">return class="num">1; class="type">class="kw">double k=sum/(D0*D1); class="kw">return k; }