交易中的数学:夏普(Sharpe)和索蒂诺(Sortino)比率·进阶篇
「用夏普比率给策略体检」
夏普比率算出来不一定是正分,策略跑亏时它直接掉到负值。把它按经典方法年化后,才有可比性:小于 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 这一种特定行情下的概率特征,换年份可能明显漂移。外汇和贵金属杠杆高,据此下单仍属高风险。
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 输出再调参数。
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 倍,说明该品种趋势收益更集中在某一周期,择时框架该往哪层靠就有了依据。
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 看看年化数值会漂移多少。
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 脚本里跑出上面那张图。
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);