交易中的数学:夏普(Sharpe)和索蒂诺(Sortino)比率·进阶篇
📘

交易中的数学:夏普(Sharpe)和索蒂诺(Sortino)比率·进阶篇

第 2/3 篇

「用夏普比率给策略体检」

夏普比率算出来不一定是正分,策略跑亏时它直接掉到负值。把它按经典方法年化后,才有可比性:小于 0 说明策略无利可图;0 到 1.0 之间属于风险没被回报覆盖,没别的选择才勉强考虑;≥1.0 才算良好,意味着风险大概率得到了足够补偿;冲到 ≥3.0 就是优秀,单笔交易踩亏损的概率非常低。 但夏普本质只是个统计变量,刻量的是回报与波动的比率。横向比策略时,别盯着绝对值,要跟基准线或同周期的其他组合放一起看,才有意义。外汇与贵金属杠杆高,夏普再漂亮也可能被极端跳空打穿,MT5 里跑完回测先盯这条曲线再下单。

◍ EURUSD 在 H1 与 D1 上的年度夏普差了 0.1

夏普比率本是用来衡量股票组合回报波动比的,但把 EURUSD 的收盘价序列当成‘资产价格’喂进去,也能算出它的风险调整收益。我们取 2020 全年(2020.01.01–2021.01.01)的收盘价,分别在 H1 和 D1 两个周期算原始夏普,再乘上对应回报笔数的平方根年化。 跑出来的结果是:H1 年化夏普 1.117708,D1 年化夏普 1.217900,差了约 0.10。两者用的价格源头一致,差异只来自采样粒度和回报笔数,这说明周期压缩会改变对外汇品种‘稳定性’的观感。 下面这段 MT5 脚本直接调 CopyClose 拉 EURUSD 数据,用 ArrayMean / ArrayStd 算均值和标准差,再乘 MathSqrt(笔数) 年化。你可以原样丢进 MT5 脚本里跑,把 Print 出来的 H1 / D1 值对照看。 别把正态当圣经 夏普隐含‘回报近似正态’前提,而 EURUSD 跳空和时段流动性差异会扭曲 std,所以年化值只反映 2020 这一种特定行情下的概率特征,换年份可能明显漂移。外汇和贵金属杠杆高,据此下单仍属高风险。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   class="type">class="kw">double H1_close[],D1_close[];
   class="type">class="kw">double h1_returns[],d1_returns[];
   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;;
   class="type">int bars = CopyClose("EURUSD",PERIOD_H1,from,to,H1_close);
   if(bars == -class="num">1)
      Print("CopyClose("EURUSD",PERIOD_H1,class="num">01.01.class="num">2020,class="num">01.01.class="num">2021 failed. Error ",GetLastError());
   else
     {
      Print("\nCalculate the mean and standard deviation of returns on H1 bars");
      Print("H1 bars=",ArraySize(H1_close));
      GetReturns(H1_close,h1_returns);
      class="type">class="kw">double average = ArrayMean(h1_returns);
      PrintFormat("H1 average=%G",average);
      class="type">class="kw">double std = ArrayStd(h1_returns);
      PrintFormat("H1 std=%G",std);
      class="type">class="kw">double sharpe_H1 = average / std;
      PrintFormat("H1 Sharpe=%G",sharpe_H1);
      class="type">class="kw">double sharpe_annual_H1 = sharpe_H1 * MathSqrt(ArraySize(h1_returns));
      Print("Sharpe_annual(H1)=", sharpe_annual_H1);
     }
   bars = CopyClose("EURUSD",PERIOD_D1,from,to,D1_close);
   if(bars == -class="num">1)
      Print("CopyClose("EURUSD",PERIOD_D1,class="num">01.01.class="num">2020,class="num">01.01.class="num">2021 failed. Error ",GetLastError());
   else
     {
      Print("\nCalculate the mean and standard deviation of returns on D1 bars");      
      Print("D1 bars=",ArraySize(D1_close));
      GetReturns(D1_close,d1_returns);
      class="type">class="kw">double average = ArrayMean(d1_returns);
      PrintFormat("D1 average=%G",average);
      class="type">class="kw">double std = ArrayStd(d1_returns);
      PrintFormat("D1 std=%G",std);
      class="type">class="kw">double sharpe_D1 = average / std;
      class="type">class="kw">double sharpe_annual_D1 = sharpe_D1 * MathSqrt(ArraySize(d1_returns));
      Print("Sharpe_annual(H1)=", sharpe_annual_D1);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Fills the returns[] array of returns                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void GetReturns(class="kw">const class="type">class="kw">double & values[], class="type">class="kw">double & returns[])
  {
   class="type">int size = ArraySize(values);

收益率序列与夏普的底层换算

算收益序列时,样本数少于 2 就直接把 returns 清空并返回,避免除零和空数组污染后续统计。正常情况则按 returns[i-1] = (values[i]-values[i-1]) / values[i-1] 逐根填收益率,这一步是后面所有波动指标的地基。 均值和标准差用两个独立函数算:ArrayMean 把数组求和再除以 size;ArrayStd 先取均值,再累加 (x-mean)^2 后除以 size 并开平方。注意这里分母是 size 而非 size-1,属总体标准差口径,和样本标准差的回测数值会有细微差别。 实跑 H1 共 6226 根bar,收益率均值 1.44468E-05,标准差 0.00101979,年化夏普约 1.1177;D1 仅 260 根bar,均值 0.000355823,标准差 0.00470188,年化夏普约 1.2179。周期越长 bar 数越少,但年化夏普反而略高,说明低频持仓的回撤波动可能被平滑。 外汇与贵金属杠杆高,夏普只是历史波动画像,不代表未来收益倾向,上 MT5 把这段直接贴进脚本跑一遍,对比你常看品种的 H1/D1 输出再调参数。

MQL5 / C++
if(size < class="num">2)
  {
   ArrayResize(returns,class="num">0);
   PrintFormat("%s: Error. ArraySize(values)=%d",size);
   class="kw">return;
  }
else
  {
   class=class="str">"cmt">//--- fill returns in a loop
   ArrayResize(returns, size - class="num">1);
   class="type">class="kw">double delta;
   for(class="type">int i = class="num">1; i < size; i++)
     {
      returns[i - class="num">1] = class="num">0;
      if(values[i - class="num">1] != class="num">0)
        {
         delta = values[i] - values[i - class="num">1];
         returns[i - class="num">1] = delta / values[i - class="num">1];
        }
     }
  }

class="type">class="kw">double ArrayMean(class="kw">const class="type">class="kw">double &array[])
  {
   class="type">int size = ArraySize(array);
   if(size < class="num">1)
     {
      PrintFormat("%s: Error, array is empty",__FUNCTION__);
      class="kw">return(class="num">0);
     }
   class="type">class="kw">double mean = class="num">0;
   for(class="type">int i = class="num">0; i < size; i++)
      mean += array[i];
   mean /= size;
   class="kw">return(mean);
  }

class="type">class="kw">double ArrayStd(class="kw">const class="type">class="kw">double &array[])
  {
   class="type">int size = ArraySize(array);
   if(size < class="num">1)
     {
      PrintFormat("%s: Error, array is empty",__FUNCTION__);
      class="kw">return(class="num">0);
     }
   class="type">class="kw">double mean = ArrayMean(array);
   class="type">class="kw">double std = class="num">0;
   for(class="type">int i = class="num">0; i < size; i++)
      std += (array[i] - mean) * (array[i] - mean);
   std /= size;
   std = MathSqrt(std);
   class="kw">return(std);
  }

「用 Python 拉 EURUSD 算双周期夏普」

想横向比 H1 和 D1 的收益率质量,先得从 MT5 终端把数据规整拉出来。下面这段 Python 脚本直接连本地 MT5,取 2020 全年 EURUSD 的 H1 与 D1 棒线,关连接后转 DataFrame 算逐根收益率。 核心逻辑是 return[i] = close[i]/close[i-1] - 1,跳过首行零值后求均值与总体标准差(ddof=0),夏普 = 均值/标准差,再乘 sqrt(样本数-1) 年化。2020 年 EURUSD 的 H1 棒约 8700+ 根,D1 为 366 根,两个样本量差异本身就会让年化夏普的波动特征明显不同。 外汇与贵金属属高风险品种,历史夏普仅反映过去波动结构,样本外可能大幅衰减。跑完这段代码,你会拿到 H1 与 D1 各自的 Sharpe_annual,若两者背离超过 1 倍,说明该品种趋势收益更集中在某一周期,择时框架该往哪层靠就有了依据。

MQL5 / C++
class="kw">import pandas as pd
pd.set_option(&class="macro">#x27;display.max_columns&class="macro">#x27;, class="num">50)  # how many columns to show
pd.set_option(&class="macro">#x27;display.width&class="macro">#x27;, class="num">1500)  # max width of the table to show
class="kw">import pytz
if not mt5.initialize():
    print("initialize() failed")
    mt5.shutdown()
timezone = pytz.timezone("Etc/UTC")
utc_from = class="type">class="kw">datetime(class="num">2020, class="num">1, class="num">1, tzinfo=timezone)
utc_to = class="type">class="kw">datetime(class="num">2020, class="num">12, class="num">31, hour=class="num">23, minute=class="num">59, second=class="num">59, tzinfo=timezone)
rates_H1 = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)
rates_D1 = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, utc_from, utc_to)
mt5.shutdown()
rates_frame = pd.DataFrame(rates_H1)
rates_frame[&class="macro">#x27;class="kw">return&class="macro">#x27;] = class="num">0.0
prev_close = class="num">0.0
for i, row in rates_frame.iterrows():
    close = row[&class="macro">#x27;close&class="macro">#x27;]
    rates_frame.at[i, &class="macro">#x27;class="kw">return&class="macro">#x27;] = close / prev_close - class="num">1 if prev_close != class="num">0.0 else class="num">0.0
    prev_close = close
print("\nCalculate the mean and standard deviation of returns on H1 bars")
print(&class="macro">#x27;H1 rates:&class="macro">#x27;, rates_frame.shape[class="num">0])
ret_average = rates_frame[class="num">1:][&class="macro">#x27;class="kw">return&class="macro">#x27;].mean()
print(&class="macro">#x27;H1 class="kw">return average=&class="macro">#x27;, ret_average)
ret_std = rates_frame[class="num">1:][&class="macro">#x27;class="kw">return&class="macro">#x27;].std(ddof=class="num">0)
print(&class="macro">#x27;H1 class="kw">return std =&class="macro">#x27;, ret_std)
sharpe_H1 = ret_average / ret_std
print(&class="macro">#x27;H1 Sharpe = Average/STD = &class="macro">#x27;, sharpe_H1)
sharpe_annual_H1 = sharpe_H1 * math.sqrt(rates_H1.shape[class="num">0]-class="num">1)
print(&class="macro">#x27;Sharpe_annual(H1) =&class="macro">#x27;, sharpe_annual_H1)
rates_daily = pd.DataFrame(rates_D1)
rates_daily[&class="macro">#x27;class="kw">return&class="macro">#x27;] = class="num">0.0
prev_return = class="num">0.0
for i, row in rates_daily.iterrows():
    close = row[&class="macro">#x27;close&class="macro">#x27;]
    rates_daily.at[i, &class="macro">#x27;class="kw">return&class="macro">#x27;] = close / prev_return - class="num">1 if prev_return != class="num">0.0 else class="num">0.0
    prev_return = close
print("\nCalculate the mean and standard deviation of returns on D1 bars")
print(&class="macro">#x27;D1 rates:&class="macro">#x27;, rates_daily.shape[class="num">0])
daily_average = rates_daily[class="num">1:][&class="macro">#x27;class="kw">return&class="macro">#x27;].mean()

◍ H1 与 D1 的夏普年化差了一截

把 D1 周期的日收益率序列取出来,先算均值再算总体标准差(ddof=0),两者相除得到日夏普,再乘上样本数减一的平方根做年化。代码跑出来的结果是:D1 共 260 根 K 线,日收益均值约 0.000356,标准差约 0.00470,日夏普 0.0757,年化夏普约 1.218。 对比同品种 H1 周期:6226 根 K 线,日收益均值 1.44e-05,标准差 0.00102,日夏普 0.0142,年化夏普约 1.118。D1 的年化夏普比 H1 高约 0.10,说明在该样本区间内低频持仓的回撤波动相对更可控。 外汇与贵金属属高风险品种,夏普仅代表历史样本波动比,样本外可能明显衰减,切勿直接当作仓位依据。打开 MT5 的 Python 环境或自行导出 rates 复算一遍,把 ddof 改成 1 看看年化数值会漂移多少。

MQL5 / C++
print(&class="macro">#x27;D1 class="kw">return average=&class="macro">#x27;, daily_average)
daily_std = rates_daily[class="num">1:][&class="macro">#x27;class="kw">return&class="macro">#x27;].std(ddof=class="num">0)
print(&class="macro">#x27;D1 class="kw">return std =&class="macro">#x27;, daily_std)
sharpe_daily = daily_average / daily_std
print(&class="macro">#x27;D1 Sharpe =&class="macro">#x27;, sharpe_daily)
sharpe_annual_D1 = sharpe_daily * math.sqrt(rates_daily.shape[class="num">0]-class="num">1)
print(&class="macro">#x27;Sharpe_annual(D1) =&class="macro">#x27;, sharpe_annual_D1)

EURUSD 跨周期年度夏普实测

把各周期 2020 年 EURUSD 的柱线回报摊开算,核心字段就七项:周期代号、含多少分钟、全年柱数、单根平均回报百分比、单根波动标准差百分比、周期夏普、以及年化夏普。年化夏普用周期夏普乘该周期全年柱数平方根得出,逻辑直接对应「把短周期风险收益拉伸到年度基准」。 实测分布很有意思:M1 到 M30 的年化夏普挤在 1.03–1.08 的窄区间,说明短周期里 EURUSD 那年的风险adjusted 收益高度稳定。但切到 H12、D1、W1、MN1 后数值发散明显,长周期样本少、单根波动占比跳变,年化外推的置信度要打问号。 外汇与贵金属属高杠杆品种,夏普仅是历史回测截面,样本年换做 2022 加息周期结论可能完全反转,拿来定位周期偏好可以,直接当仓位依据不行。 下面这段是统计结构的骨架与遍历入口,挂上 GetReturns / GetStats 两个外部函数就能在 MT5 脚本里跑出上面那张图。

MQL5 / C++
class=class="str">"cmt">//--- structure to print statistics to log
class="kw">struct Stats
  {
   class="type">class="kw">string                TF;
   class="type">int                   Minutes;
   class="type">int                   Rates;
   class="type">class="kw">double                Avg;
   class="type">class="kw">double                Std;
   class="type">class="kw">double                SharpeTF;
   class="type">class="kw">double                SharpeAnnual;
  };
class=class="str">"cmt">//--- array of statistics by timeframes
Stats stats[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- arrays for close prices
   class="type">class="kw">double H1_close[],D1_close[];
class=class="str">"cmt">//--- arrays of returns
   class="type">class="kw">double h1_returns[],d1_returns[];
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,PERIOD_H2,PERIOD_H3,PERIOD_H4,
                                   PERIOD_H6,PERIOD_H8,PERIOD_H12,PERIOD_D1,PERIOD_W1,PERIOD_MN1
                                   };
   ArrayResize(stats,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(symbol);
   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
      class="type">class="kw">double returns[];
      GetReturns(symbol,timeframes[i],from,to,returns);
      class=class="str">"cmt">//--- calculate statistics
      GetStats(returns,avr,std,sharpe);
      class="type">class="kw">double sharpe_annual = sharpe * MathSqrt(ArraySize(returns));
      PrintFormat("%s  aver=%G%%  std=%G%%  sharpe=%G  sharpe_annual=%G",
                   EnumToString(timeframes[i]), avr * class="num">100,std * class="num">100,sharpe,sharpe_annual);

常见问题

把策略在不同品种和周期上跑一遍夏普,若只在某一周期极高、其他周期转负,大概率过拟合;健康策略夏普应在多周期都为正且波动不大。
实测中 EURUSD 跨周期年度夏普确有约 0.1 差距,属正常波动;若差超过 0.3 就要检查周期逻辑是否冲突。
小布可对接你的账户和品种,直接输出 H1、D1 等周期的夏普与索蒂诺对比,不用自己写代码拉数据。
用周期收益均值减无风险利率再除以收益标准差;外汇实操常取 0 或隔夜拆借近似值,重点看相对变化而非绝对值。
H1 噪声多、回撤频繁,年化时波动被放大;D1 过滤了日内噪音,夏普通常更平滑,不一定是样本量问题。