交易中的数学:夏普(Sharpe)和索蒂诺(Sortino)比率·综合运用
(3/3)· 从 GBPUSD 到 EURUSD 的真实计算,搞懂两个比率到底差在哪、怎么用
「把多周期统计塞进结构体再统一打印」
这段逻辑干的事很直接:遍历时间框架数组,把每个周期的统计量填进一个 Stats 结构体,最后用 ArrayPrint 一次性吐到日志。先看核心填充代码。 //--- fill the statistics structure Stats row; string tf_str = EnumToString(timeframes[i]); StringReplace(tf_str,"PERIOD_",""); row.TF = tf_str; row.Minutes = PeriodSeconds(timeframes[i]) / 60; row.Rates = ArraySize(returns); row.Avg = avr; row.Std = std; row.SharpeTF = sharpe; row.SharpeAnnual = sharpe_annual; //--- add a row for the timeframe statistics stats[i] = row; } //--- print statistics on all timeframes to log ArrayPrint(stats,8); 逐行拆:EnumToString 把周期枚举转成 "PERIOD_M1" 这类字符串,StringReplace 去掉前缀只留 "M1";PeriodSeconds 取秒数除以 60 得到分钟数;ArraySize(returns) 是该周期收益率样本数;Avg、Std、SharpeTF、SharpeAnnual 分别是均值、标准差和两类夏普,由前面算好直接挂上;stats[i]=row 把该行存进数组;循环结束后 ArrayPrint(stats,8) 按 8 位精度打印全部。 实跑样本里,M1 有 373023 根回报率、年化夏普约 1.03,而 MN1 只有 12 根、年化夏普冲到 1.49。样本量越小年化夏普越飘,外汇和贵金属杠杆高,这种统计在外盘实盘里可能严重过拟合,开 MT5 把这段代码挂自己品种上跑一遍最实在。
class=class="str">"cmt">//--- fill the statistics structure Stats row; class="type">class="kw">string tf_str = EnumToString(timeframes[i]); StringReplace(tf_str,"PERIOD_",""); row.TF = tf_str; row.Minutes = PeriodSeconds(timeframes[i]) / class="num">60; row.Rates = ArraySize(returns); row.Avg = avr; row.Std = std; row.SharpeTF = sharpe; row.SharpeAnnual = sharpe_annual; class=class="str">"cmt">//--- add a row for the timeframe statistics stats[i] = row; } class=class="str">"cmt">//--- print statistics on all timeframes to log ArrayPrint(stats,class="num">8);
◍ 低周期夏普更稳:三对直盘实测
把同样的夏普比率算法套到 GBPUSD、USDJPY、USDCHF 上,结论和前面 EURUSD 类似但细节有分化。GBPUSD 在 M1 到 H12 全周期里夏普值都近似,没有随周期拉长而塌掉;USDJPY 在 M1 至 H12 区间数值落在 -0.56 到 -0.60 之间,负得均匀。 USDCHF 只在 M1 到 M30 的小周期里拿到近似值,时间帧一放大,夏普就开始上下波动,不再收敛。外汇与贵金属属高风险品种,负夏普不代表必然亏损,只说明该周期样本内回报风险比偏负。 把四对主要货币对放一起看,M1 至 M30 算出来的夏普最不容易随周期漂移。想横向比不同品种的策略质量,用低时间帧回报去算这个比率,比用 H1 以上周期更不容易被周期噪声带偏。 开 MT5 调一下品种列表和周期参数,把夏普计算脚本从 H1 降到 M15 跑一遍,能直接看到波动收敛的差别。
EURUSD 2020 月度夏普在跨周期下的稳定性
取 2020 年 EURUSD 逐月回报,在 M1 到 H1 共 12 个时间帧上分别跑年度夏普比率。实测结果是:同一月份在不同周期算出的年度值非常接近,曲线在 3D 示意图里几乎贴在一起。 这说明年度夏普对柱线数量敏感,但对周期选择不敏感——只要你塞进去的回报数组够长,M1 和 H1 给出的结论倾向一致。外汇与贵金属属高风险品种,该数值仅反映历史波动特征,不预示未来收益。 脚本核心是先按时间帧拉回报,再对 1—12 月逐月切片算年化。下面这段是原文主逻辑,逐行拆一下关键结构。
class=class="str">"cmt">//--- structure to store returns class="kw">struct Return { class="type">class="kw">double ret; class=class="str">"cmt">// class="kw">return class="type">class="kw">datetime time; class=class="str">"cmt">// date class="type">int month; class=class="str">"cmt">// month }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { SharpeMonths sharpe_by_months[]; class=class="str">"cmt">//--- arrays of timeframes on which the Sharpe coefficient will be calculated ENUM_TIMEFRAMES timeframes[] = {PERIOD_M1,PERIOD_M2,PERIOD_M3,PERIOD_M4,PERIOD_M5, PERIOD_M6,PERIOD_M10,PERIOD_M12,PERIOD_M15,PERIOD_M20, PERIOD_M30,PERIOD_H1 }; ArrayResize(sharpe_by_months,ArraySize(timeframes)); class=class="str">"cmt">//--- timeseries request parameters class="type">class="kw">string symbol = Symbol(); class="type">class="kw">datetime from = D&class="macro">#x27;class="num">01.01.class="num">2020&class="macro">#x27;; class="type">class="kw">datetime to = D&class="macro">#x27;class="num">01.01.class="num">2021&class="macro">#x27;; Print("Calculate Sharpe Annual on ",symbol, " for class="num">2020 year"); for(class="type">int i = class="num">0; i < ArraySize(timeframes); i++) { class=class="str">"cmt">//--- get the array of returns on the specified timeframe Return returns[]; GetReturns(symbol,timeframes[i],from,to,returns); class="type">class="kw">double avr,std,sharpe; class=class="str">"cmt">//--- Calculate statistics for the year GetStats(returns,avr,std,sharpe); class="type">class="kw">string tf_str = EnumToString(timeframes[i]); class=class="str">"cmt">//--- calculate the annual Sharpe ratio for each month SharpeMonths sharpe_months_on_tf; sharpe_months_on_tf.SetTimeFrame(tf_str); class=class="str">"cmt">//--- select returns for i-th month for(class="type">int m = class="num">1; m <= class="num">12; m++) { Return month_returns[]; GetReturnsByMonth(returns,m,month_returns); class=class="str">"cmt">//--- Calculate statistics for the year class="type">class="kw">double sharpe_annual = CalculateSharpeAnnual(timeframes[i],month_returns); sharpe_months_on_tf.Sharpe(m,sharpe_annual); } class=class="str">"cmt">//--- add Sharpe ratio for class="num">12 months on timeframe i sharpe_by_months[i] = sharpe_months_on_tf; } class=class="str">"cmt">//--- display the table of annual Sharpe values by months on all timeframes ArrayPrint(sharpe_by_months,class="num">3); }
「EURUSD 2020 年化夏普逐月透视」
把 2020 年 EURUSD 按不同重采样周期算年化夏普,能直接看到趋势结构对风险调整收益的影响。M1 到 H1 共 12 档周期,每一行是该周期下 1—12 月的年化夏普值。 数据里最扎眼的是 7 月:全周期夏普集体冲高,M30 达到 7.346、H1 也有 7.311,而 1 月全周期均为负,M15 低至 -3.053。说明 2020 年 EURUSD 的月度风险回报分布极不均匀,单纯看年夏普会掩盖这种断层。 对外汇与贵金属交易者而言,这类品种高杠杆、高波动,夏普为负月份意味着策略样本内都可能持续回撤。建议把这段矩阵直接丢进 MT5 脚本复算,验证自己策略在 2020 年各月是否也呈现夏普集中于夏秋、年初普跌的特征。
[ class="num">0] "PERIOD_M1" -class="num">2.856 -class="num">1.340 class="num">0.120 -class="num">0.929 class="num">2.276 class="num">1.534 class="num">6.836 class="num">2.154 -class="num">2.697 -class="num">1.194 class="num">3.891 class="num">4.140 [ class="num">1] "PERIOD_M2" -class="num">2.919 -class="num">1.348 class="num">0.119 -class="num">0.931 class="num">2.265 class="num">1.528 class="num">6.854 class="num">2.136 -class="num">2.717 -class="num">1.213 class="num">3.845 class="num">4.125 [ class="num">2] "PERIOD_M3" -class="num">2.965 -class="num">1.340 class="num">0.118 -class="num">0.937 class="num">2.276 class="num">1.543 class="num">6.920 class="num">2.159 -class="num">2.745 -class="num">1.212 class="num">3.912 class="num">4.121 [ class="num">3] "PERIOD_M4" -class="num">2.980 -class="num">1.341 class="num">0.119 -class="num">0.937 class="num">2.330 class="num">1.548 class="num">6.830 class="num">2.103 -class="num">2.765 -class="num">1.219 class="num">3.937 class="num">4.110 [ class="num">4] "PERIOD_M5" -class="num">2.929 -class="num">1.312 class="num">0.120 -class="num">0.935 class="num">2.322 class="num">1.550 class="num">6.860 class="num">2.123 -class="num">2.729 -class="num">1.239 class="num">3.971 class="num">4.076 [ class="num">5] "PERIOD_M6" -class="num">2.945 -class="num">1.364 class="num">0.119 -class="num">0.945 class="num">2.273 class="num">1.573 class="num">6.953 class="num">2.144 -class="num">2.768 -class="num">1.239 class="num">3.979 class="num">4.082 [ class="num">6] "PERIOD_M10" -class="num">3.033 -class="num">1.364 class="num">0.119 -class="num">0.934 class="num">2.361 class="num">1.584 class="num">6.789 class="num">2.063 -class="num">2.817 -class="num">1.249 class="num">4.087 class="num">4.065 [ class="num">7] "PERIOD_M12" -class="num">2.952 -class="num">1.358 class="num">0.118 -class="num">0.956 class="num">2.317 class="num">1.609 class="num">6.996 class="num">2.070 -class="num">2.933 -class="num">1.271 class="num">4.115 class="num">4.014 [ class="num">8] "PERIOD_M15" -class="num">3.053 -class="num">1.367 class="num">0.118 -class="num">0.945 class="num">2.377 class="num">1.581 class="num">7.132 class="num">2.078 -class="num">2.992 -class="num">1.274 class="num">4.029 class="num">4.047 [ class="num">9] "PERIOD_M20" -class="num">2.998 -class="num">1.394 class="num">0.117 -class="num">0.920 class="num">2.394 class="num">1.532 class="num">6.884 class="num">2.065 -class="num">3.010 -class="num">1.326 class="num">4.074 class="num">4.040 [class="num">10] "PERIOD_M30" -class="num">3.008 -class="num">1.359 class="num">0.116 -class="num">0.957 class="num">2.379 class="num">1.585 class="num">7.346 class="num">2.084 -class="num">2.934 -class="num">1.323 class="num">4.139 class="num">4.034 [class="num">11] "PERIOD_H1" -class="num">2.815 -class="num">1.373 class="num">0.116 -class="num">0.966 class="num">2.398 class="num">1.601 class="num">7.311 class="num">2.221 -class="num">3.136 -class="num">1.374 class="num">4.309 class="num">4.284
◍ 用下行偏差替代全波动:索蒂诺比率怎么算
夏普比率把报价上下波动全算作风险,等于把资产上涨也当成了惩罚项,对交易者其实是一种风险高估。弗兰克·索蒂诺在 90 年代初给出的解法很直接:只取资产减少那段半波动率(也叫下行偏差、负波动率)当分母,分子仍是平均回报。 代码层面,先算全样本均值与标准差得到夏普,再把正回报置零、只留负回报算 semistd,用 avr/semistd 得到索蒂诺。这样分母变小,比率权重自然抬升,但含义从“每单位波动的回报”变成了“每单位下行风险的回报”。 回测脚本对 2020 年 EURUSD 各周期跑出的结果很直观:M1 的 SharpeAnnual 为 1.0177,SortinoAnnual 为 1.6161,比值约 1.588;M6 的 Sharpe 1.0335、Sortino 1.6379,比值 1.585。几乎所有时间帧里索蒂诺值都是夏普的 1.60 倍左右,这是价格序列对称波动带来的现象,实盘交易结果不会这么整齐。 别把历史回测当保本凭证 两个指标都基于历史报价,外汇与贵金属属高风险品种,即便 EURUSD 年化索蒂诺看着漂亮,也只代表过去下行风险下的回报倾向,不保证未来盈利。比较策略时同时列夏普与索蒂诺,比单看其中一个更有参考价值。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Calculates Sharpe and Sortino ratios | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GetStats(ENUM_TIMEFRAMES timeframe, class="kw">const class="type">class="kw">double & returns[], class="type">class="kw">double & avr, class="type">class="kw">double & std, class="type">class="kw">double & sharpe, class="type">class="kw">double & sortino) { avr = ArrayMean(returns); std = ArrayStd(returns); sharpe = (std == class="num">0) ? class="num">0 : avr / std; class=class="str">"cmt">//--- now, remove negative returns and calculate the Sortino ratio class="type">class="kw">double negative_only[]; class="type">int size = ArraySize(returns); ArrayResize(negative_only,size); ZeroMemory(negative_only); class=class="str">"cmt">//--- copy only negative returns for(class="type">int i = class="num">0; i < size; i++) negative_only[i] = (returns[i] > class="num">0) ? class="num">0 : returns[i]; class="type">class="kw">double semistd = ArrayStd(negative_only); sortino = avr / semistd; class="kw">return; }
周期拉长后正态偏离在放大
上面这组扫描把 M10 到 D1 共 13 个周期排开,样本数从 M10 的 37349 个一路掉到 D1 的 259 个,采样密度直接砍掉两个数量级。 看第三、四列:每根 K 线的平均绝对偏差从 M10 的 0.00000239 爬到 D1 的 0.00035582,放大约 149 倍;标准差从 0.00044072 升到 0.00470188,约 10.7 倍。说明周期越大,价格相对均值的离散度越宽。 偏度列(倒数第二列)在 H12 出现 2.11045830、D1 为 2.04624198,明显偏离 0;峰度列(末列)H12 达 1.75245371、D1 为 1.70331429,也都高于正态基准。外汇与贵金属属高风险品种,这种厚尾现象意味着小周期近似正态、大周期更易出极端跳空,回测里按正态假设算止损间距可能偏窄。 开 MT5 把这段周期表抄进 excel,对自己常做的品种跑一遍同样统计,重点核对 H12 与 D1 的偏度是否也大于 1.5。
「周线与月线周期的分布实测」
把 W1 与 MN1 两个大周期拉出来看,样本量分别是 51 和 12,周期分钟数对应 10080 与 43200。W1 的均值约 0.00193、标准差 0.0135,偏度 1.032、峰度 1.804;MN1 均值升到 0.00766、标准差 0.01776,偏度 1.493、峰度 5.010。 月线样本只有 12 个,峰度超 3 说明尾部比正态厚得多,直接拿正态假设去算置信区间会低估极端周月线波动的概率。 开 MT5 把这段周期参数表贴进实验脚本,对比你自己品种的返回值,重点看 MN1 峰度是否也破 5,再决定大周期止损带宽怎么放。
[class="num">19] "W1" class="num">10080 class="num">51 class="num">0.00193306 class="num">0.01350157 class="num">1.03243721 class="num">1.80369984 class="num">1.74703102 [class="num">20] "MN1" class="num">43200 class="num">12 class="num">0.00765726 class="num">0.01776075 class="num">1.49349076 class="num">5.00964481 class="num">3.35431926
◍ 用对数回报在 MT5 里算夏普
夏普比率原本面向每日调仓的股票组合,但 EA 多数时间空仓,账户净值不变。若把空仓柱线算作零回报,比率会失真。正确做法是只在净值变化的柱线取最后一次报价算回报,这样任意即时报价模式下都能跑。 相对回报有个坑:价格跌 5% 再涨 5% 回不到原点,线性百分比会引入偏差。统计上改用对数回报 ln(Current/Previous) = ln(Current) − ln(Previous),它可加总,对数和等价于相对回报乘积,没有复位错误。 下面这段是核心循环:遍历净值数组,仅当权益较前一刻变化才记入对数回报,并累加求平均、填充数组。样本不足 2 个直接返回 0,避免标准差无解。 [CODE] Log_Return =ln(Current/Previous) = ln(Current) — ln(Previous) //--- calculate the logarithms of increments using the equity array for(int i = 1; i < m_bars_counter; i++) { //--- only add if equity has changed if(m_equities[i] != prev_equity) { log_return = MathLog(m_equities[i] / prev_equity); // increment logarithm aver += log_return; // average logarithm of increments AddReturn(log_return); // fill the array of increment logarithms counter++; // counter of returns } prev_equity = m_equities[i]; } //--- if values are not enough for Sharpe calculation, return 0 if(counter <= 1) return(0); //--- average value of the increment logarithm aver /= counter; //--- calculate standard deviation for(int i = 0; i < counter; i++) std += (m_returns[i] - aver) * (m_returns[i] - aver); std /= counter; std = MathSqrt(std); //--- Sharpe ratio on the current timeframe double sharpe = aver / std; #define MACD_MAGIC 1234502 //--- #include <Trade\Trade.mqh> #include <Trade\SymbolInfo.mqh> #include <Trade\PositionInfo.mqh> #include <Trade\AccountInfo.mqh> #include "Sharpe.mqh" //--- input double InpLots = 0.1;// Lots input int InpTakeProfit = 50; // Take Profit (in pips) input int InpTrailingStop = 30; // Trailing Stop Level (in pips) input int InpMACDOpenLevel = 3; // MACD open level (in pips) input int InpMACDCloseLevel = 2; // MACD close level (in pips) input int InpMATrendPeriod = 26; // MA trend period //--- int ExtTimeOut = 10; // time out in seconds between trade operations CReturns returns; .... //+------------------------------------------------------------------+
| // | Expert new tick handling function |
|---|
把 Sharpe.mqh 挂到 MACD Sample 上,加一行 CReturns returns; 并 include 头文件,就能以自定义优化准则跑夏普。对 EURUSD M10、2020 年做遗传优化,测试器给出的准则值与手算夏普一致。 夏普最高的参数未必利润最大,但净值曲线往往更平、回撤更小。外汇与贵金属属高风险品种,用夏普做优化只是提升参数稳健性的一种概率倾向,不预示未来收益。
Log_Return =ln(Current/Previous) = ln(Current) — ln(Previous) class=class="str">"cmt">//--- calculate the logarithms of increments using the equity array for(class="type">int i = class="num">1; i < m_bars_counter; i++) { class=class="str">"cmt">//--- only add if equity has changed if(m_equities[i] != prev_equity) { log_return = MathLog(m_equities[i] / prev_equity); class=class="str">"cmt">// increment logarithm aver += log_return; class=class="str">"cmt">// average logarithm of increments AddReturn(log_return); class=class="str">"cmt">// fill the array of increment logarithms counter++; class=class="str">"cmt">// counter of returns } prev_equity = m_equities[i]; } class=class="str">"cmt">//--- if values are not enough for Sharpe calculation, class="kw">return class="num">0 if(counter <= class="num">1) class="kw">return(class="num">0); class=class="str">"cmt">//--- average value of the increment logarithm aver /= counter; class=class="str">"cmt">//--- calculate standard deviation for(class="type">int i = class="num">0; i < counter; i++) std += (m_returns[i] - aver) * (m_returns[i] - aver); std /= counter; std = MathSqrt(std); class=class="str">"cmt">//--- Sharpe ratio on the current timeframe class="type">class="kw">double sharpe = aver / std; class="macro">#define MACD_MAGIC class="num">1234502 class=class="str">"cmt">//--- class="macro">#include <Trade\Trade.mqh> class="macro">#include <Trade\SymbolInfo.mqh> class="macro">#include <Trade\PositionInfo.mqh> class="macro">#include <Trade\AccountInfo.mqh> class="macro">#include "Sharpe.mqh" class=class="str">"cmt">//--- class="kw">input class="type">class="kw">double InpLots = class="num">0.1;class=class="str">"cmt">// Lots class="kw">input class="type">int InpTakeProfit = class="num">50; class=class="str">"cmt">// Take Profit(in pips) class="kw">input class="type">int InpTrailingStop = class="num">30; class=class="str">"cmt">// Trailing Stop Level(in pips) class="kw">input class="type">int InpMACDOpenLevel = class="num">3; class=class="str">"cmt">// MACD open level(in pips) class="kw">input class="type">int InpMACDCloseLevel = class="num">2; class=class="str">"cmt">// MACD close level(in pips) class="kw">input class="type">int InpMATrendPeriod = class="num">26; class=class="str">"cmt">// MA trend period class=class="str">"cmt">//--- class="type">int ExtTimeOut = class="num">10; class=class="str">"cmt">// time out in seconds between trade operations CReturns returns; .... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert new tick handling function |
在 OnTick 里喂数据算夏普
EA 跑起来后,真正决定夏普比率准不准的,是 OnTick 里有没有把权益流喂给统计对象。下面这段把每次 tick 的权益塞进 returns 数组,超时机制则避免同一根 bar 里反复触发交易逻辑。 [CODE] void OnTick(void) { static datetime limit_time = 0; // last trade processing time + timeout //--- add current equity to the array to calculate the Sharpe ratio MqlTick tick; SymbolInfoTick(_Symbol, tick); returns.OnTick(tick.time, AccountInfoDouble(ACCOUNT_EQUITY)); //--- don't process if timeout if(TimeCurrent() >= limit_time) { //--- check for data if(Bars(Symbol(), Period()) > 2 * InpMATrendPeriod) { //--- change limit time by timeout in seconds if processed if(ExtExpert.Processing()) limit_time = TimeCurrent() + ExtTimeOut; } } } //+------------------------------------------------------------------+
| // | Tester function |
|---|
//+------------------------------------------------------------------+ double OnTester(void) { //--- calculate Sharpe ratio double sharpe = returns.OnTester(); return(sharpe); } //+------------------------------------------------------------------+ [/CODE] 逐行看:static datetime limit_time 记录上次交易处理后的锁定时刻;MqlTick tick 取当前品种 tick,SymbolInfoTick 填充时间;returns.OnTick 以 tick 时间和账户权益更新收益序列。若 TimeCurrent() 超过 limit_time 且 Bars 数大于 2 倍均线周期,才允许 ExtExpert.Processing() 跑一次,成功就把 limit_time 推后 ExtTimeOut 秒。 回测结束时 OnTester 直接调 returns.OnTester() 返回夏普值,MT5 优化器就能按这个数值排序。外汇与贵金属波动剧烈,夏普高只代表历史样本里回撤相对收益更平滑,未来仍可能失效,实盘前务必在策略测试器里用不同年份数据复算。
class="type">void OnTick(class="type">void) { class="kw">static class="type">class="kw">datetime limit_time = class="num">0; class=class="str">"cmt">// last trade processing time + timeout class=class="str">"cmt">//--- add current equity to the array to calculate the Sharpe ratio class="type">MqlTick tick; SymbolInfoTick(_Symbol, tick); returns.OnTick(tick.time, AccountInfoDouble(ACCOUNT_EQUITY)); class=class="str">"cmt">//--- don&class="macro">#x27;t process if timeout if(TimeCurrent() >= limit_time) { class=class="str">"cmt">//--- check for data if(Bars(Symbol(), Period()) > class="num">2 * InpMATrendPeriod) { class=class="str">"cmt">//--- change limit time by timeout in seconds if processed if(ExtExpert.Processing()) limit_time = TimeCurrent() + ExtTimeOut; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Tester function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester(class="type">void) { class=class="str">"cmt">//--- calculate Sharpe ratio class="type">class="kw">double sharpe = returns.OnTester(); class="kw">return(sharpe); } class=class="str">"cmt">//+------------------------------------------------------------------+
「一点提醒」
夏普与索蒂诺比率的核心价值,是让你用同一把尺子量不同资产的风险回报——黄金和白银能直接比,单个策略或组合也能比,不依赖外部基准。但这两把尺子默认回报服从正态分布,实盘里多数品种不认这个假设,外汇和贵金属的高波动更常出现肥尾,读数只能当参考而非结论。 上面那段 MQL5 片段暴露了一个容易高估夏普的坑:只在权益变化的 Bar 上累加对数收益,计数器 counter 只数这些 Bar,但年化时却乘当前周期对 D1 的总 Bar 数平方根(factor 用 PeriodSeconds(D1)/PeriodSeconds(tf) 算,再乘 sqrt(252))。这意味着分母少计了零变动 Bar,年度值可能被推高,回测里看到 2.08 甚至 3.83 的夏普时要先怀疑算法口径。 开 MT5 把 CalculateSharpe_All_TF.mq5 拖进策略测试器,改一句让零变动 Bar 也进 counter,对比前后年度夏普差多少,比盯着一个数字更有用。外汇贵金属杠杆风险高,比率再漂亮也只代表历史样本的可能倾向。
class=class="str">"cmt">//--- add only if equity has changed if(m_equities[i] != prev_equity) { log_return = MathLog(m_equities[i] / prev_equity); class=class="str">"cmt">// 递增对数 aver += log_return; class=class="str">"cmt">// 增量的平均对数 AddReturn(log_return); class=class="str">"cmt">// 填充递增对数数组 counter++; class=class="str">"cmt">// 返回计数器 } prev_equity = m_equities[i]; class=class="str">"cmt">//--- 增量对数的平均值 aver /= counter; class=class="str">"cmt">//---------------重新计算夏普比率,在所有其他情况下都改为年值 class=class="str">"cmt">//--- how many periods of the current timeframe fit into D1 class="type">class="kw">double factor = class="type">class="kw">double(PeriodSeconds(PERIOD_D1)) / PeriodSeconds(timeframe); sharpe = sharpe * MathSqrt(factor); class=class="str">"cmt">// 重新计算每日值 sharpe = sharpe * MathSqrt(class="num">252); class=class="str">"cmt">// 从每日数据中获取年度数据