在上一篇文章《投資組合的評價和可視化(上)》中,我們了解了幾種常見的投資組合評價指標的計算方法,並且通過一個實例,一步步根據這個投資組合在過去十年的模擬交易結果,計算出了它的各項指標,接下來,我們將一步步實現所有指標的可視化。
在這篇文章裡,我們使用matplotlib來實現可視化。我們使用前一篇文章裡計算完成的數據,把他們組合顯示在一張圖表中(如下圖):
看上去圖表比較復雜,但是我們會一步步將它們實現。圖表中的原始數據可以在這裡下載,原始數據包含一個大小盤輪動策略在過去十年裡的模擬交易結果。在上一篇文章中,我們在原始數據的基礎上一步步計算了投資組合的所有相關評價指標,包括:
下面,我們就來一步步把上面的信息通過matplotlib可視化出來。不過,本文所有代碼都是基於上一篇文章的結果,因此,還沒有讀上一篇文章的同學,需要復制完整代碼以確保上述的計算都已經完成。
由於需要顯示的內容非常多,一張簡單的圖表是無法表現所有內容的,因此我們需要一張組合圖表。在Matplotlib中,一張圖表被稱為一個figure,在一個figure上可以放置若干個繪圖區域Axes,每個繪圖區域內都有自己獨立的數據、網格、標題等等元素,我們要在一個figure上有計劃第放置多個Axes來制作組合圖表。
我們首先需要加載matplotlib模塊:
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()
我們希望整張圖表既能夠展現所有的內容,但是又主次分明,因此需要對圖表的格式做出一個大致的規劃:
首先,圖表應該表現三大類內容:
根據上面的規劃,我們可以按下面的格式在一個figure上創建九張圖表,並調整它們的位置。留出最大的一張表,位於視覺重心處,用於顯示最重要的收益率曲線。
chart_width = 0.88
## 圖表布局規劃
fig = plt.figure(figsize=(12, 15), facecolor=(0.82, 0.83, 0.85))
ax1 = fig.add_axes([0.05, 0.67, 0.88, 0.20])
ax2 = fig.add_axes([0.05, 0.57, 0.88, 0.08], sharex=ax1)
ax3 = fig.add_axes([0.05, 0.49, 0.88, 0.06], sharex=ax1)
ax4 = fig.add_axes([0.05, 0.41, 0.88, 0.06], sharex=ax1)
ax5 = fig.add_axes([0.05, 0.33, 0.88, 0.06], sharex=ax1)
ax6 = fig.add_axes([0.05, 0.25, 0.88, 0.06], sharex=ax1)
ax7 = fig.add_axes([0.05, 0.04, 0.35, 0.16])
ax8 = fig.add_axes([0.45, 0.04, 0.15, 0.16])
ax9 = fig.add_axes([0.64, 0.04, 0.29, 0.16])
使用plt.show()
或者fig.show()
即可看到圖表的外觀如下。
同學們可以自行微調
add_axes()
裡列表中的四個數字,來調整每個表的位置大小,這四個數字分別代表:[圖表左下角X坐標,圖表左下角Y坐標,圖表的寬度,圖表的高度]
每個數字都是一個小數,單位為整個figure
的高度或寬度,例如,[0.64, 0.04, 0.29, 0.16]
表示圖> 表的左下角位於figure
寬度的64%處、高度的4%處,寬度為figure
的29%,高度為figure
的16%
為了圖表的美觀起見,我們還可以調整一下各個圖表的格式,例如,前六張圖表可以共享一個X軸,因此圖表的X軸坐標便可以隱藏,另外,我們希望在圖表左邊放置名稱或說明,那麼Y軸坐標就可以放在右邊,等等。
這部分代碼可以放置在最後。
# 設置所有圖表的基本格式:
for ax in [ax1, ax2, ax3, ax4, ax5, ax6]:
ax.yaxis.tick_right()
ax.xaxis.set_ticklabels([])
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.grid(True)
# 設置圖表的格式
years = mdates.YearLocator() # every year
months = mdates.MonthLocator() # every month
weekdays = mdates.WeekdayLocator() # every weekday
years_fmt = mdates.DateFormatter('%Y')
month_fmt_none = mdates.DateFormatter('')
month_fmt_l = mdates.DateFormatter('%y/%m')
month_fmt_s = mdates.DateFormatter('%m')
# 調整主圖表的日期格式
major_locator = years
major_formatter = years_fmt
minor_locator = months
minor_formatter = month_fmt_none
# 前五個主表的時間軸共享,因此只需要設置最下方表的時間軸即可
ax6.xaxis.set_major_locator(major_locator)
ax6.xaxis.set_major_formatter(major_formatter)
ax6.xaxis.set_minor_locator(minor_locator)
ax6.xaxis.set_minor_formatter(minor_formatter)
for ax in [ax1, ax2, ax3, ax4, ax5]:
plt.setp(ax.get_xticklabels(), visible=False)
圖表的格式設置完畢後,我們可以先把所有文字信息輸出到圖表上的空白處。
在這裡,我們需要計算出需要顯示的所有數據:
# 持股數量變動量,當持股數量發生變動時,判斷產生買賣行為
change = (looped_value[stock_holdings] - looped_value[stock_holdings].shift(1)).sum(1)
# 計算回測記錄第一天的回測結果和參考指數價格,以此計算後續的收益率曲線
start_point = looped_value['value'].iloc[0]
ref_start = looped_value['benchmark'].iloc[0]
# 計算回測結果的每日回報率
ret = looped_value['value'] - looped_value['value'].shift(1)
position = 1 - (looped_value['cash'] / looped_value['value'])
beta = looped_value['beta']
alpha = looped_value['alpha']
volatility = looped_value['volatility']
sharp = looped_value['sharp']
underwater = looped_value['underwater']
drawdowns = dd_df
# 回測結果和參考指數的總體回報率曲線
return_rate = (looped_value.value - start_point) / start_point * 100
ref_rate = (looped_value.benchmark - ref_start) / ref_start * 100
# 將benchmark的起始資產總額調整到與回測資金初始值一致,一遍生成可以比較的benchmark資金曲線
# 這個資金曲線用於顯示"以對數比例顯示的資金變化曲線"圖
adjusted_bench_start = looped_value.benchmark / ref_start * start_point
使用fig.suptitle()
設置圖表的大標題
而其他所有的文字都可以通過fig.text()
來輸出。fig.text()
的頭兩個參數是文字的坐標,需要注意,文字的坐標是以文字塊左下角的位置來定義的,因此如果文字中包含換行符,第一行文字就會被“頂到”上面去。
請注意,下面的代碼中使用了fontname='pingfang HK'
以使用中文字體,否則中文會顯示為亂碼
## 2,顯示投資回測摘要信息
title_asset_pool = '滬深300/創業板指 大小盤輪動策略'
fig.suptitle(f'回測交易結果: {
title_asset_pool} - 業績基准: 滬深300指數',
fontsize=14,
fontweight=10,
fontname='pingfang HK')
# 投資回測結果的評價指標全部被打印在圖表上,所有的指標按照表格形式打印
# 為了實現表格效果,指標的標簽和值分成兩列打印,每一列的打印位置相同
fig.text(0.07, 0.955, f'回測期長: {
total_years:3.1f} 年, '
f' 從: {
looped_value.index[0].date()} 起至 {
looped_value.index[-1].date()}止',
fontname='pingfang HK')
fig.text(0.21, 0.90, f'交易操作匯總:\n\n'
f'投資總金額:\n'
f'期末總資產:', ha='right',
fontname='pingfang HK')
fig.text(0.23, 0.90, f'{
op_counts.buy.sum()} 次買入 \n'
f'{
op_counts.sell.sum()} 次賣出\n'
f'¥{
total_invest:13,.2f}\n'
f'¥{
final_value:13,.2f}',
fontname='pingfang HK')
fig.text(0.50, 0.90, f'總投資收益率:\n'
f'平均年化收益率:\n'
f'基准投資收益率:\n'
f'基准投資平均年化收益率:\n'
f'最大回撤:\n', ha='right',
fontname='pingfang HK')
fig.text(0.52, 0.90, f'{
total_return:.2%} \n'
f'{
annual_return: .2%} \n'
f'{
ref_return:.2%} \n'
f'{
ref_annual_rtn:.2%}\n'
f'{
mdd:.1%} \n'
f' 底部日期 {
mdd_date.date()}',
fontname='pingfang HK')
fig.text(0.82, 0.90, f'alpha / 阿爾法系數:\n'
f'Beta / 貝塔系數:\n'
f'Sharp ratio / 夏普率:\n'
f'Calmar ratio / 卡爾瑪比率:\n'
f'250-日滾動波動率:', ha='right',
fontname='pingfang HK')
fig.text(0.84, 0.90, f'{
avg_alpha:.3f} \n'
f'{
avg_beta:.3f} \n'
f'{
avg_sharp:.3f} \n'
f'{
avg_calmar:.3f} \n'
f'{
avg_volatility:.3f}',
fontname='pingfang HK')
如果輸入的文字正常,顯示效果應該如下圖所示:
有的朋友在運行上述代碼時,可能會遇到錯誤說使用的中文字體不存在,因而中文顯示為亂碼,這裡給出一個解決方案供大家參考:
為了顯示系統中有哪些中文字體,可以先導入
matplotlib
的FontManager
類,調用這個類的ttflist
屬性,就可以看到系統中已經存在的所有可以被matplotlib
使用的字體了,選擇其中的中文字體即可(中文字體名稱中一般都帶有拼音,或者含有TC
、SC
之類的關鍵字:>>> from matplotlib.font_manager import FontManager >>> fm = FontManager() >>> fm.ttflist Out: [<Font 'STIXSizeOneSym' (STIXSizOneSymBol.ttf) normal normal 700 normal>, <Font 'STIXSizeOneSym' (STIXSizOneSymReg.ttf) normal normal 400 normal>, ... <Font 'PingFang HK' (PingFang.ttc) normal normal 400 normal>, ... <Font 'STIXIntegralsUpD' (STIXIntUpDReg.otf) normal normal 400 normal>, <Font 'Apple Braille' (Apple Braille Pinpoint 6 Dot.ttf) normal normal 400 normal>]
清單中的字體可能會比較多,也有多種中文字體,比如上面例子中的
PingFang HK
就是中文字體,將字體名稱PingFang HK
用於font
就可以了:font_name='PingFang HK'
接下來進入重頭戲,各個圖表的繪制。由於每張圖表數據區別甚大,因此我們分別來看。
收益率曲線圖是整張圖表的視覺重心,也是一張頗為復雜的復合圖表,我們希望它能體現出最重要的關鍵信息,因此信息集成度比較高。
這張圖表由三部分組成:
上面三部分我們逐步繪制。
收益率曲線用最簡單的axex.plot()
函數即可實現,傳入必要的參數如顏色color
,線形linestyle
、透明度alpha
等即可:
# 3。1, 繪制回測結果的收益率曲線圖
ax1.set_title('總收益率、基准收益率和交易歷史', fontname='pingfang HK')
ax1.plot(looped_value.index, ref_rate, linestyle='-',
color=(0.4, 0.6, 0.8), alpha=0.85, label='Benchmark')
ax1.plot(looped_value.index, return_rate, linestyle='-',
color=(0.8, 0.2, 0.0), alpha=0.85, label='Return')
ax1.set_ylabel('收益率', fontname='pingfang HK')
ax1.yaxis.set_major_formatter(mtick.PercentFormatter())
用axes.fill_between()
在參考收益率的正負區間分別填充紅色和綠色,使它更顯眼。
# 填充參考收益率的正負區間,綠色填充正收益率,紅色填充負收益率
ax1.fill_between(looped_value.index, 0, ref_rate,
where=ref_rate >= 0,
facecolor=(0.4, 0.6, 0.2), alpha=0.35)
ax1.fill_between(looped_value.index, 0, ref_rate,
where=ref_rate < 0,
facecolor=(0.8, 0.2, 0.0), alpha=0.35)
繪圖效果如下,紅色的投資曲線和紅綠填充的基准曲線都顯示在圖表上了。
買賣區間可以有兩種不同的方式表示:
我們可以用axes.axvspan()
來填充縱向條紋,以表現某一個時間區間的狀態,不過所有的條紋需要逐個填充,因此我們使用for
循環來填充所有持股區間。下面代碼中change表示股票倉位發生變化的時間點,找出所有這樣的時間點,再根據當時的倉位填充顏色就可以了。
注意facecolor=((1 - 0.6 * long_short), (1 - 0.4 * long_short), (1 - 0.8 * long_short))
這行代碼,實際上是根據倉位的高低來計算一個RGB
顏色值,當倉位為0
(空倉)時計算結果為(1.0, 1.0, 1.0)
即純白色,而倉位為1
(滿倉)時,計算結果為(0.4, 0.6, 0.2)
即綠色。因此條紋的顏色隨倉位而變,倉位越高,顏色越深。
設置alpha=0.2
是為了確保填充的條紋足夠透明,不會遮擋曲線圖。
position_bounds = [looped_value.index[0]]
position_bounds.extend(looped_value.loc[change != 0].index)
position_bounds.append(looped_value.index[-1])
for first, second, long_short in zip(position_bounds[:-2], position_bounds[1:],
position.loc[position_bounds[:-2]]):
# 分別使用綠色、紅色填充交易回測歷史中的多頭和空頭區間
if long_short > 0:
# 用不同深淺的綠色填充多頭區間, 0 < long_short < 1
if long_short > 1:
long_short = 1
ax1.axvspan(first, second,
facecolor=((1 - 0.6 * long_short), (1 - 0.4 * long_short), (1 - 0.8 * long_short)),
alpha=0.2)
else:
# 用不同深淺的紅色填充空頭區間, -1 < long_short < 0
if long_short < -1:
long_short = -1
ax1.axvspan(first, second,
facecolor=((1 + 0.2 * long_short), (1 + 0.8 * long_short), (1 + long_short)),
alpha=0.2)
填充的效果如下:
axes.scatter
可以在圖表上任意坐標顯示一個圖形,marker="^"
表示向上箭頭,marker="v"
表示向下箭頭。因此,我們可以遍歷歷史上的所有買入和賣出點,在買入點曲線上繪制一個向上綠色箭頭,在賣出點曲線上繪制一個向下紅色箭頭,這樣也能展示出買賣點了。不過,如果買賣點特別密集的情況下,買賣點是無法分辨清楚的。buy_points= np.where(change > 0, ref_rate, np.nan)
sell_points = np.where(change < 0, ref_rate, np.nan)
ax1.scatter(looped_value.index, buy_points, color='green',
label='Buy', marker='^', alpha=0.9)
ax1.scatter(looped_value.index, sell_points, color='red',
label='Sell', marker='v', alpha=0.9)
如下圖所示,買賣點過於密集,難以分辨。
朋友們可以根據具體情況選擇不同的買賣區間展示方式。
使用axes.annotate()
函數可以在圖表上繪制箭頭,通過xy
參數可以指定箭頭指向的位置、而xytext
的位置是箭頭的出發(文字)坐標。在這裡指定坐標非常方便,只需要按照數據坐標指定即可,例如,需要箭頭指向圖表中(2012年3月17日,125%)所在的點,需要傳入的坐標就是:(2012-3-17, 1.25)
ax1.annotate(f"{
mdd_date.date()}",
xy=(mdd_date, return_rate[mdd_date]),
xycoords='data',
xytext=(mdd_peak, return_rate[mdd_peak]),
textcoords='data',
arrowprops=dict(width=1, headwidth=3, facecolor='black', shrink=0.),
ha='right',
va='bottom')
if pd.notna(mdd_recover):
ax1.annotate(f"-{
mdd:.1%}\n{
mdd_date.date()}",
xy=(mdd_recover, return_rate[mdd_recover]),
xycoords='data',
xytext=(mdd_date, return_rate[mdd_date]),
textcoords='data',
arrowprops=dict(width=1, headwidth=3, facecolor='black', shrink=0.),
ha='right',
va='top')
else:
ax1.text(x=mdd_date,
y=return_rate[mdd_date],
s=f"-{
mdd:.1%}\nnot recovered",
ha='right',
va='top')
ax1.legend()
如下圖所示,最大回撤區間已經在圖表上標記出來了。
有了上面這張圖,整個投資過程中最關鍵的幾個信息都已經完整展現出來了。不過,考慮到投資的復利效應,如果我們的投資組合非常給力,後期收益率非常高時,往往前期的收益會被壓縮成一條直線,很難分辨,甚至有時基准首頁也被壓縮成直線了,這時我們可以使用一張對數比例的收益率曲線圖來放大低收益區間的圖形,以便看清整個投資區間的變化。
繪制對數比例曲線圖很簡單,只需要設置axes.set_yscale('log')
即可:
ax2.set_title('對數比例回測收益率與基准收益率', fontname='pingfang HK')
ax2.plot(looped_value.index, adjusted_bench_start, linestyle='-',
color=(0.4, 0.6, 0.8), alpha=0.85, label='Benchmark')
ax2.plot(looped_value.index, looped_value.value, linestyle='-',
color=(0.8, 0.2, 0.0), alpha=0.85, label='Cum Value')
ax2.set_ylabel('收益率\n對數比例', fontname='pingfang HK')
ax2.yaxis.set_major_formatter(mtick.PercentFormatter())
ax2.set_yscale('log') #設置對數比例
ax2.legend()
對比線性比例和對數比例的兩張圖,是否發現對數比例下,早期的投資收益情況更加清楚了?這是由於投資收益本身就是自帶“指數”效應的,使用對數比例抵消指數效應,可以更清晰地展示整個投資全過程的收益情況。
接下來幾張圖表都比較簡單,每日收益額可以用柱狀圖來展現,axes.bar()
函數專門用來顯示柱狀圖:
ax3.set_title('收益額', fontname='pingfang HK')
ax3.bar(looped_value.index, ret)
ax3.set_ylabel('收益額', fontname='pingfang HK')
投資組合的盈利能力通過alpha
和sharp
兩個指標來體現,它們本身就是滾動數據,因此使用線圖axes.plot()
來顯示非常合適:
ax4.set_title('投資組合盈利能力: 滾動阿爾法系數和夏普率', fontname='pingfang HK')
ax4.plot(looped_value.index, sharp, label='sharp')
ax4.plot(looped_value.index, alpha, label='alpha')
ax4.set_ylabel('盈利能力', fontname='pingfang HK')
ax4.legend()
與前面一張表相似,波動率和貝塔系數都體現了投資組合的風險敞口,可以放到同一張圖表中用線圖表示,別忘記繪制圖例axes.legend()
即可:
ax5.set_title('投資組合風險敞口: 滾動波動率和貝塔系數', fontname='pingfang HK')
ax5.plot(looped_value.index, volatility, label='volatility')
ax5.plot(looped_value.index, beta, label='beta')
ax5.set_ylabel('風險敞口', fontname='pingfang HK')
ax5.legend()
完成上面幾張圖的繪制後,使用plt.show()可以看到圖表如下:
作為歷史曲線圖的最後一張圖表,回撤區間圖包含兩部分:
歷史回撤比例圖顯示了歷史上每一天相對於前期高點的回撤比例,由於最大值為0(無回撤),最小值為1
,而曲線總是在0
以下的某個位置波動,很像一條魚只能在水下活動,不能超出水面,因此也叫潛水圖。潛水圖的數據已經存放在underwater
數據中了,因此可以直接調用,為了突出數據,可以把曲線和0之間的部分填充紅色(使用axes.fill_between()
)
ax6.set_title('歷史最大回撤及收益率潛水圖', fontname='pingfang HK')
ax6.plot(underwater, label='underwater')
ax6.set_ylabel('潛水圖', fontname='pingfang HK')
ax6.set_xlabel('date')
ax6.set_ylim(-1, 0)
ax6.fill_between(looped_value.index, 0, underwater,
where=underwater < 0,
facecolor=(0.8, 0.2, 0.0), alpha=0.35)
dd_starts = drawdowns['peak_date'].head().values
dd_ends = drawdowns['recover_date'].head().values
dd_valley = drawdowns['valley_date'].head().values
dd_value = drawdowns['drawdown'].head().values
填充後的回撤比例圖(潛水圖)如下圖所示:
緊接著我們要填充歷史上的五個最大回撤區間。
使用for循環,找到歷史上的最大五個回撤區間的開始、結束時間,使用axes.axvspan()
填充縱向條紋,同時使用axes.text()
標注最大回撤的比例,如下圖所示:
for start, end, valley, dd in zip(dd_starts, dd_ends, dd_valley, dd_value):
if np.isnan(end):
end = looped_value.index[-1]
ax6.axvspan(start, end,
facecolor='grey',
alpha=0.3)
if dd > -0.6:
ax6.text(x=valley,
y=dd - 0.05,
s=f"-{
dd:.1%}\n",
ha='center',
va='top')
else:
ax6.text(x=valley,
y=dd + 0.15,
s=f"-{
dd:.1%}\n",
ha='center',
va='bottom')
這樣,五個最大回撤區間就直觀地展現出來了:
至此,我們已經完成了6張歷史曲線圖表的繪制。完整地展現了整個投資歷史中的重要信息。不過,如果我們還想以月為單位,了解整個投資過程中收益率的統計信息,例如,月度收益率的分布情況如何?收益率平均分布,還是兩極分化?這些信息都不容易從歷史曲線圖中看出來,這是我們就需要更多的統計圖表了。
熱力圖的好處是可以讓人一目了然地同時看到所有年份所有月份的收益率,而且收益率以顏色來展示,非常直觀。我們在前一篇文章中,計算了歷史所有月份的收益率,並存儲在了一個DataFrame中:
monthly_return_df
Out[50]:
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec y-cum
2011 -0.029565 0.004999 -0.082161 -0.027755 0.000771 0.017896 0.036120 -0.089740 0.009318 0.001401 -0.029218 0.002102 -0.134492
2012 0.029766 0.043612 0.010853 0.008814 -0.004902 -0.036098 -0.031729 -0.039526 -0.020958 -0.022452 -0.013789 0.085873 0.053817
2013 0.077273 0.033820 -0.020762 0.002204 0.173830 -0.024849 0.026676 -0.074971 0.030109 -0.077782 0.048875 -0.054761 0.154530
2014 0.121668 -0.063620 -0.009860 -0.002996 0.009583 0.064157 0.048142 -0.015106 0.060307 -0.028095 0.117326 0.252034 0.751125
2015 -0.060772 0.135744 0.174663 0.064227 0.216783 -0.143777 0.045193 0.048436 -0.019001 0.131076 0.098636 0.002394 1.135973
2016 -0.028169 -0.009662 0.079805 -0.040826 0.007602 0.013114 0.026142 -0.011543 -0.046850 -0.007132 0.053011 -0.043511 -0.036378
2017 0.015494 0.034207 -0.013918 -0.017658 0.018582 0.047976 0.023719 0.007475 0.003254 0.027097 0.002371 -0.010655 0.184567
2018 0.043260 -0.044899 0.057105 -0.040944 -0.041885 0.021316 -0.028817 -0.048421 0.049822 0.002025 -0.014282 -0.028081 -0.114206
2019 0.006190 0.153495 0.079587 -0.026437 0.001902 0.033116 -0.009870 0.008138 -0.002944 0.011165 0.009196 0.041282 0.456783
2020 0.051435 0.152352 -0.081691 0.056878 -0.011090 0.128821 0.164158 -0.031281 -0.041442 -0.057777 0.039970 0.031110 0.548550
這樣我們就可以繪制一張熱力圖,包含十行、十二列色塊,每行代表一年,每列代表一個月,共計120個色塊,每個色塊的顏色代表當月的收益率大小。由於我們本來就已經用了DataFrame
格式,數據已經存儲在行列中,因此我們用axes.imshow()
就可以輕松顯示出熱力圖。cmap='RdYlGn'
表示熱力圖的塗色方案,表示“red-yellow-green”紅黃綠漸變著色,紅色是最低收益率,綠色為最高收益率,如果要反過來,可以用cmap='GnYlRd'
。更多的塗色方案可以參考matplotlib
的文檔。
monthly_df = monthly_return_df[['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']]
return_years = monthly_df.index
return_months = monthly_df.columns
return_values = monthly_df.values
c = ax7.imshow(return_values, cmap='RdYlGn')
ax7.set_title('月度收益熱力圖', fontname='pingfang HK')
ax7.set_xticks(np.arange(len(return_months)))
ax7.set_yticks(np.arange(len(return_years)))
ax7.set_xticklabels(return_months, rotation=45)
ax7.set_yticklabels(return_years)
base_aspect_ratio = 0.72
if len(return_years) <= 12:
aspect_ratio = base_aspect_ratio
else:
aspect_ratio = base_aspect_ratio * 12 / len(return_years)
ax7.set_aspect(aspect_ratio)
ax7.grid(False)
fig.colorbar(c, ax=ax7)
每年的收益率可以顯示為柱狀圖,為了確保每根柱子與表7裡每一行(每一年)對齊,我們可以把柱狀圖顯示為水平柱子,因此,不要使用axes.bar(),而是使用axes.barh()圖顯示水平柱狀圖。
y_cum = monthly_return_df['y-cum']
y_count = len(return_years)
pos_y_cum = np.where(y_cum >= 0, y_cum, 0)
neg_y_cum = np.where(y_cum < 0, y_cum, 0)
return_years = y_cum.index
ax8.barh(np.arange(y_count), pos_y_cum, 1, align='center', facecolor='green', alpha=0.85)
ax8.barh(np.arange(y_count), neg_y_cum, 1, align='center', facecolor='red', alpha=0.85)
ax8.set_yticks(np.arange(y_count))
ax8.set_ylim(y_count - 0.5, -0.5)
ax8.set_yticklabels(list(return_years))
ax8.set_title('年度收益率', fontname='pingfang HK')
ax8.grid(False)
最後是月度收益率的直方圖,通過這張直方圖,我們可以看到收益率的概率分布,從而判斷收益率分布均勻,還是兩極分化。
ax9.set_title('月度收益山積圖', fontname='pingfang HK')
ax9.hist(monthly_return_df.values.flatten(), bins=18, alpha=0.5,
label='monthly returns')
ax9.grid(False)
最終效果如下:
至此,我們的整個圖表就繪制完成了。通過這兩篇文章,我們通過一個例子介紹了投資組合歷史回測結果的評價,並且通過一張可視化圖表展示了投資組合的結果。
完整的代碼如下(包括前一篇文章中的代碼),示例數據在這裡下載:
if __name__ == '__main__':
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.ticker as mtick
from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()
looped_value = pd.read_csv('example_data.csv', index_col=0)
looped_value.index = pd.to_datetime(looped_value.index)
total_rounds = len(looped_value.index)
total_days = (looped_value.index[-1] - looped_value.index[0]).days
total_years = total_days / 365.
total_months = int(np.round(total_days / 30))
total_invest = looped_value.iloc[0].cash
final_value = looped_value.iloc[-1].value
# 建立一個新的DataFrame,刪除不需要的列
holding_stocks = looped_value.copy()
holding_stocks.drop(columns=['cash', 'value', 'benchmark'], inplace=True)
# 計算股票每一輪交易後的變化,增加者為買入,減少者為賣出
holding_movements = holding_stocks - holding_stocks.shift(1)
# 分別標記多倉/空倉,買入/賣出的位置,全部取sign()以便後續方便加總統計數量
holding_long = np.where(holding_stocks > 0, np.sign(holding_stocks), 0)
holding_short = np.where(holding_stocks < 0, np.sign(holding_stocks), 0)
holding_inc = np.where(holding_movements > 0, np.sign(holding_movements), 0)
holding_dec = np.where(holding_movements < 0, np.sign(holding_movements), 0)
# 統計數量
sell_counts = -holding_dec.sum(axis=0)
buy_counts = holding_inc.sum(axis=0)
long_percent = holding_long.sum(axis=0) / total_rounds
short_percent = -holding_short.sum(axis=0) / total_rounds
op_counts = pd.DataFrame(sell_counts, index=holding_stocks.columns, columns=['sell'])
op_counts['buy'] = buy_counts
op_counts['total'] = op_counts.buy + op_counts.sell
op_counts['long'] = long_percent
op_counts['short'] = short_percent
op_counts['empty'] = 1 - op_counts.long - op_counts.short
# 計算總收益率和年化收益率
looped_value['invest'] = total_invest
looped_value['rtn'] = looped_value.value / looped_value['invest'] - 1
total_return = looped_value['rtn'].iloc[-1]
ys = (looped_value.index - looped_value.index[0]).days / 365.
looped_value['annual_rtn'] = (looped_value.rtn + 1) ** (1 / ys) - 1
annual_return = looped_value['annual_rtn'].iloc[-1]
looped_value['pct_change'] = looped_value.value / looped_value.value.shift(1) - 1
ref_return = looped_value.benchmark.iloc[-1] / looped_value.benchmark.iloc[0]
ref_annual_rtn = (ref_return + 1) ** (1 / ys[-1]) - 1
# 計算月度歷史收益率
first_year = looped_value.index[0].year
last_year = looped_value.index[-1].year
starts = pd.date_range(start=str(first_year - 1) + '1231',
end=str(last_year) + '1130',
freq='M') + pd.Timedelta(1, 'd')
ends = pd.date_range(start=str(first_year) + '0101',
end=str(last_year) + '1231',
freq='M')
# 計算每個月的收益率
monthly_returns = list()
for start, end in zip(starts, ends):
val = looped_value['value'].loc[start:end]
if len(val) > 0:
monthly_returns.append(val.iloc[-1] / val.iloc[0] - 1)
else:
monthly_returns.append(np.nan)
year_count = len(monthly_returns) // 12
monthly_returns = np.array(monthly_returns).reshape(year_count, 12)
monthly_return_df = pd.DataFrame(monthly_returns,
columns=['Jan', 'Feb', 'Mar', 'Apr',
'May', 'Jun', 'Jul', 'Aug',
'Sep', 'Oct', 'Nov', 'Dec'],
index=range(first_year, last_year + 1))
# 計算每年的收益率
starts = pd.date_range(start=str(first_year - 1) + '1231',
end=str(last_year) + '1130',
freq='Y') + pd.Timedelta(1, 'd')
ends = pd.date_range(start=str(first_year) + '0101',
end=str(last_year) + '1231',
freq='Y')
# 組裝出月度、年度收益率矩陣
yearly_returns = []
for start, end in zip(starts, ends):
val = looped_value['value'].loc[start:end]
if len(val) > 0:
yearly_returns.append(val.iloc[-1] / val.iloc[0] - 1)
else:
yearly_returns.append(np.nan)
monthly_return_df['y-cum'] = yearly_returns
# 計算Volatility
ret = (looped_value['value'] / looped_value['value'].shift(1)) - 1
volatility = ret.rolling(250).std() * np.sqrt(250)
looped_value['volatility'] = volatility
avg_volatility = looped_value.volatility.mean()
# 生成MDD DataFrame
cummax = looped_value['value'].cummax()
looped_value['underwater'] = (looped_value['value'] - cummax) / cummax
drawdown_sign = np.sign(looped_value.underwater)
diff = drawdown_sign - drawdown_sign.shift(1)
drawdown_starts = np.where(diff == -1)[0]
drawdown_ends = np.where(diff == 1)[0]
drawdown_count = min(len(drawdown_starts), len(drawdown_ends))
all_drawdowns = []
for i_start, i_end in zip(drawdown_starts[:drawdown_count], drawdown_ends[:drawdown_count]):
dd_start = looped_value.index[i_start - 1]
dd_end = looped_value.index[i_end]
dd_min = looped_value['underwater'].iloc[i_start:i_end].idxmin()
dd = looped_value['underwater'].loc[dd_min]
all_drawdowns.append((dd_start, dd_min, dd_end, dd))
if len(drawdown_starts) > drawdown_count:
dd_start = looped_value.index[drawdown_starts[-1] - 1]
dd_end = np.nan
dd_min = looped_value['underwater'].iloc[drawdown_starts[-1]:].idxmin()
dd = looped_value['underwater'].loc[dd_min]
all_drawdowns.append((dd_start, dd_min, dd_end, dd))
# 生成包含所有回撤的DataFrame
dd_df = pd.DataFrame(all_drawdowns, columns=['peak_date', 'valley_date', 'recover_date', 'drawdown'])
dd_df.sort_values(by='drawdown', inplace=True)
mdd = dd_df.iloc[0].drawdown
mdd_date = dd_df.iloc[0].valley_date
mdd_peak = dd_df.iloc[0].peak_date
mdd_recover = dd_df.iloc[0].recover_date
# 計算sharp率
loop_len = len(looped_value)
# 計算年化收益,如果回測期間大於一年,直接計算滾動年收益率(250天)
ret = looped_value['value'] / looped_value['value'].shift(1) - 1
roll_yearly_return = ret.rolling(250).mean() * 250
looped_value['sharp'] = (roll_yearly_return - 0.035) / looped_value['volatility']
avg_sharp = looped_value.sharp.mean()
# 計算卡爾瑪比率
value = looped_value['value']
cummax = value.cummax()
drawdown = (cummax - value) / cummax
ret = value / value.shift(250) - 1
looped_value['calmar'] = ret / drawdown.rolling(250).max()
avg_calmar = looped_value['calmar'].mean()
# 計算貝塔系數:
# 獲取基准組合組合的收益率(在這裡為滬深300指數的價格)
ref = looped_value['benchmark']
ref_ret = (ref / ref.shift(1)) - 1
ret_dev = looped_value['pct_change'].rolling(250).var()
looped_value['beta'] = looped_value['pct_change'].rolling(250).cov(ref_ret) / ret_dev
avg_beta = looped_value['beta'].mean()
# 計算alpha系數
loop_len = len(looped_value)
# 計算年化收益,如果回測期間大於一年,直接計算250日滾動收益率
year_ret = looped_value.value / looped_value['value'].shift(250) - 1
bench = looped_value['benchmark']
bench_ret = (bench / bench.shift(250)) - 1
looped_value['alpha'] = (year_ret - 0.035) - looped_value['beta'] * (bench_ret - 0.035)
avg_alpha = looped_value['alpha'].mean()
################################################ previous section
chart_width = 0.88
## 圖表布局規劃
fig = plt.figure(figsize=(12, 15), facecolor=(0.82, 0.83, 0.85))
ax1 = fig.add_axes([0.05, 0.67, 0.88, 0.20])
ax2 = fig.add_axes([0.05, 0.57, 0.88, 0.08], sharex=ax1)
ax3 = fig.add_axes([0.05, 0.49, 0.88, 0.06], sharex=ax1)
ax4 = fig.add_axes([0.05, 0.41, 0.88, 0.06], sharex=ax1)
ax5 = fig.add_axes([0.05, 0.33, 0.88, 0.06], sharex=ax1)
ax6 = fig.add_axes([0.05, 0.25, 0.88, 0.06], sharex=ax1)
ax7 = fig.add_axes([0.05, 0.04, 0.35, 0.16])
ax8 = fig.add_axes([0.45, 0.04, 0.15, 0.16])
ax9 = fig.add_axes([0.64, 0.04, 0.29, 0.16])
# 設置所有圖表的基本格式:
for ax in [ax1, ax2, ax3, ax4, ax5, ax6]:
ax.yaxis.tick_right()
ax.xaxis.set_ticklabels([])
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.grid(True)
# process plot figure and axes formatting
years = mdates.YearLocator() # every year
months = mdates.MonthLocator() # every month
weekdays = mdates.WeekdayLocator() # every weekday
years_fmt = mdates.DateFormatter('%Y')
month_fmt_none = mdates.DateFormatter('')
month_fmt_l = mdates.DateFormatter('%y/%m')
month_fmt_s = mdates.DateFormatter('%m')
result_columns = looped_value.columns
fixed_column_items = ['fee', 'cash', 'value', 'reference', 'ref', 'ret',
'invest', 'underwater', 'volatility', 'pct_change',
'beta', 'sharp', 'alpha']
stock_holdings = [item for
item in
result_columns if
item not in fixed_column_items and
item[-2:] != '_p']
# 為了確保回測結果和參考價格在同一個水平線上比較,需要將他們的起點"重合"在一起,否則
# 就會出現兩者無法比較的情況。
# 持股數量變動量,當持股數量發生變動時,判斷產生買賣行為
change = (looped_value[stock_holdings] - looped_value[stock_holdings].shift(1)).sum(1)
# 計算回測記錄第一天的回測結果和參考指數價格,以此計算後續的收益率曲線
start_point = looped_value['value'].iloc[0]
ref_start = looped_value['benchmark'].iloc[0]
# 計算回測結果的每日回報率
ret = looped_value['value'] - looped_value['value'].shift(1)
position = 1 - (looped_value['cash'] / looped_value['value'])
beta = looped_value['beta']
alpha = looped_value['alpha']
volatility = looped_value['volatility']
sharp = looped_value['sharp']
underwater = looped_value['underwater']
drawdowns = dd_df
# 回測結果和參考指數的總體回報率曲線
return_rate = (looped_value.value - start_point) / start_point * 100
ref_rate = (looped_value.benchmark - ref_start) / ref_start * 100
# 將benchmark的起始資產總額調整到與回測資金初始值一致,一遍生成可以比較的benchmark資金曲線
# 這個資金曲線用於顯示"以對數比例顯示的資金變化曲線"圖
adjusted_bench_start = looped_value.benchmark / ref_start * start_point
# 2,顯示投資回測摘要信息
title_asset_pool = '滬深300/創業板指 大小盤輪動策略'
fig.suptitle(f'回測交易結果: {
title_asset_pool} - 業績基准: 滬深300指數',
fontsize=14,
fontweight=10,
fontname='pingfang HK')
# 投資回測結果的評價指標全部被打印在圖表上,所有的指標按照表格形式打印
# 為了實現表格效果,指標的標簽和值分成兩列打印,每一列的打印位置相同
fig.text(0.07, 0.955, f'回測期長: {
total_years:3.1f} 年, '
f' 從: {
looped_value.index[0].date()} 起至 {
looped_value.index[-1].date()}止',
fontname='pingfang HK')
fig.text(0.21, 0.90, f'交易操作匯總:\n\n\n'
f'投資總金額:\n'
f'期末總資產:', ha='right',
fontname='pingfang HK')
fig.text(0.23, 0.90, f'{
op_counts.buy.sum():.0f} 次買入 \n'
f'{
op_counts.sell.sum():.0f} 次賣出\n\n'
f'¥{
total_invest:13,.2f}\n'
f'¥{
final_value:13,.2f}',
fontname='pingfang HK')
fig.text(0.50, 0.90, f'總投資收益率:\n'
f'平均年化收益率:\n'
f'基准投資收益率:\n'
f'基准投資平均年化收益率:\n'
f'最大回撤:\n', ha='right',
fontname='pingfang HK')
fig.text(0.52, 0.90, f'{
total_return:.2%} \n'
f'{
annual_return: .2%} \n'
f'{
ref_return:.2%} \n'
f'{
ref_annual_rtn:.2%}\n'
f'{
mdd:.1%} \n'
f' 底部日期 {
mdd_date.date()}',
fontname='pingfang HK')
fig.text(0.82, 0.90, f'alpha / 阿爾法系數:\n'
f'Beta / 貝塔系數:\n'
f'Sharp ratio / 夏普率:\n'
f'Calmar ratio / 卡爾瑪比率:\n'
f'250-日滾動波動率:', ha='right',
fontname='pingfang HK')
fig.text(0.84, 0.90, f'{
avg_alpha:.3f} \n'
f'{
avg_beta:.3f} \n'
f'{
avg_sharp:.3f} \n'
f'{
avg_calmar:.3f} \n'
f'{
avg_volatility:.3f}',
fontname='pingfang HK')
# 3,繪制基准數據的收益率曲線圖
ax1.set_title('總收益率、基准收益率和交易歷史', fontname='pingfang HK')
ax1.plot(looped_value.index, ref_rate, linestyle='-',
color=(0.4, 0.6, 0.8), alpha=0.85, label='Benchmark')
# 3。1, 繪制回測結果的收益率曲線圖
ax1.plot(looped_value.index, return_rate, linestyle='-',
color=(0.8, 0.2, 0.0), alpha=0.85, label='Return')
ax1.set_ylabel('收益率', fontname='pingfang HK')
ax1.yaxis.set_major_formatter(mtick.PercentFormatter())
# 填充參考收益率的正負區間,綠色填充正收益率,紅色填充負收益率
ax1.fill_between(looped_value.index, 0, ref_rate,
where=ref_rate >= 0,
facecolor=(0.4, 0.6, 0.2), alpha=0.35)
ax1.fill_between(looped_value.index, 0, ref_rate,
where=ref_rate < 0,
facecolor=(0.8, 0.2, 0.0), alpha=0.35)
# 3。2,顯示持股倉位區間(效果是在回測區間上用綠色帶表示多頭倉位,紅色表示空頭倉位,顏色越深倉位越高)
# 查找每次買進和賣出的時間點並將他們存儲在一個列表中,用於標記買賣時機
position_bounds = [looped_value.index[0]]
position_bounds.extend(looped_value.loc[change != 0].index)
position_bounds.append(looped_value.index[-1])
for first, second, long_short in zip(position_bounds[:-2], position_bounds[1:],
position.loc[position_bounds[:-2]]):
# 分別使用綠色、紅色填充交易回測歷史中的多頭和空頭區間
if long_short > 0:
# 用不同深淺的綠色填充多頭區間, 0 < long_short < 1
if long_short > 1:
long_short = 1
ax1.axvspan(first, second,
facecolor=((1 - 0.6 * long_short), (1 - 0.4 * long_short), (1 - 0.8 * long_short)),
alpha=0.2)
else:
# 用不同深淺的紅色填充空頭區間, -1 < long_short < 0
if long_short < -1:
long_short = -1
ax1.axvspan(first, second,
facecolor=((1 + 0.2 * long_short), (1 + 0.8 * long_short), (1 + long_short)),
alpha=0.2)
#
# 3。2b,顯示買賣時機的另一種方法,使用buy / sell 來存儲買賣點
# buy_point是當持股數量增加時為買點,sell_points是當持股數量下降時
# 在買賣點當天寫入的數據是參考數值,這是為了使用散點圖畫出買賣點的位置
# 繪制買賣點散點圖(效果是在ref線上使用紅綠箭頭標識買賣點)
#
# buy_points = np.where(change > 0, ref_rate, np.nan)
# sell_points = np.where(change < 0, ref_rate, np.nan)
# ax1.scatter(looped_value.index, buy_points, color='green',
# label='Buy', marker='^', alpha=0.9)
# ax1.scatter(looped_value.index, sell_points, color='red',
# label='Sell', marker='v', alpha=0.9)
#
# 3。3, 使用箭頭標記最大回撤區間,箭頭從最高起點開始,指向最低點,第二個箭頭從最低點開始,指向恢復點
ax1.annotate(f"{
mdd_date.date()}",
xy=(mdd_date, return_rate[mdd_date]),
xycoords='data',
xytext=(mdd_peak, return_rate[mdd_peak]),
textcoords='data',
arrowprops=dict(width=1, headwidth=3, facecolor='black', shrink=0.),
ha='right',
va='bottom')
if pd.notna(mdd_recover):
ax1.annotate(f"-{
mdd:.1%}\n{
mdd_date.date()}",
xy=(mdd_recover, return_rate[mdd_recover]),
xycoords='data',
xytext=(mdd_date, return_rate[mdd_date]),
textcoords='data',
arrowprops=dict(width=1, headwidth=3, facecolor='black', shrink=0.),
ha='right',
va='top')
else:
ax1.text(x=mdd_date,
y=return_rate[mdd_date],
s=f"-{
mdd:.1%}\nnot recovered",
ha='right',
va='top')
ax1.legend()
#
# # 4,繪制參考數據的收益率曲線圖
ax2.set_title('對數比例回測收益率與基准收益率', fontname='pingfang HK')
ax2.plot(looped_value.index, adjusted_bench_start, linestyle='-',
color=(0.4, 0.6, 0.8), alpha=0.85, label='Benchmark')
ax2.plot(looped_value.index, looped_value.value, linestyle='-',
color=(0.8, 0.2, 0.0), alpha=0.85, label='Cum Value')
ax2.set_ylabel('收益率\n對數比例', fontname='pingfang HK')
ax2.yaxis.set_major_formatter(mtick.PercentFormatter())
ax2.set_yscale('log')
ax2.legend()
ax3.set_title('收益額', fontname='pingfang HK')
ax3.bar(looped_value.index, ret)
ax3.set_ylabel('收益額', fontname='pingfang HK')
ax4.set_title('投資組合盈利能力: 滾動阿爾法系數和夏普率', fontname='pingfang HK')
ax4.plot(looped_value.index, sharp, label='sharp')
ax4.plot(looped_value.index, alpha, label='alpha')
ax4.set_ylabel('盈利能力', fontname='pingfang HK')
ax4.legend()
ax5.set_title('投資組合風險敞口: 滾動波動率和貝塔系數', fontname='pingfang HK')
ax5.plot(looped_value.index, volatility, label='volatility')
ax5.plot(looped_value.index, beta, label='beta')
ax5.set_ylabel('風險敞口', fontname='pingfang HK')
ax5.legend()
#
# # 表6, 繪制underwater圖(drawdown可視化圖表)
ax6.set_title('歷史最大回撤及收益率潛水圖', fontname='pingfang HK')
ax6.plot(underwater, label='underwater')
ax6.set_ylabel('潛水圖', fontname='pingfang HK')
ax6.set_xlabel('date')
ax6.set_ylim(-1, 0)
ax6.fill_between(looped_value.index, 0, underwater,
where=underwater < 0,
facecolor=(0.8, 0.2, 0.0), alpha=0.35)
dd_starts = drawdowns['peak_date'].head().values
dd_ends = drawdowns['recover_date'].head().values
dd_valley = drawdowns['valley_date'].head().values
dd_value = drawdowns['drawdown'].head().values
#
# # 表6。1, 逐個填充歷史最大的幾個回撤區間
for start, end, valley, dd in zip(dd_starts, dd_ends, dd_valley, dd_value):
if np.isnan(end):
end = looped_value.index[-1]
ax6.axvspan(start, end,
facecolor='grey',
alpha=0.3)
if dd > -0.6:
ax6.text(x=valley,
y=dd - 0.05,
s=f"-{
dd:.1%}\n",
ha='center',
va='top')
else:
ax6.text(x=valley,
y=dd + 0.15,
s=f"-{
dd:.1%}\n",
ha='center',
va='bottom')
#
# # 表7:繪制收益率熱力圖
monthly_df = monthly_return_df[['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']]
return_years = monthly_df.index
return_months = monthly_df.columns
return_values = monthly_df.values
c = ax7.imshow(return_values, cmap='RdYlGn')
ax7.set_title('月度收益熱力圖', fontname='pingfang HK')
ax7.set_xticks(np.arange(len(return_months)))
ax7.set_yticks(np.arange(len(return_years)))
ax7.set_xticklabels(return_months, rotation=45)
ax7.set_yticklabels(return_years)
base_aspect_ratio = 0.72
if len(return_years) <= 12:
aspect_ratio = base_aspect_ratio
else:
aspect_ratio = base_aspect_ratio * 12 / len(return_years)
ax7.set_aspect(aspect_ratio)
ax7.grid(False)
fig.colorbar(c, ax=ax7)
#
# # 繪制年度收益率柱狀圖
y_cum = monthly_return_df['y-cum']
y_count = len(return_years)
pos_y_cum = np.where(y_cum >= 0, y_cum, 0)
neg_y_cum = np.where(y_cum < 0, y_cum, 0)
return_years = y_cum.index
ax8.barh(np.arange(y_count), pos_y_cum, 1, align='center', facecolor='green', alpha=0.85)
ax8.barh(np.arange(y_count), neg_y_cum, 1, align='center', facecolor='red', alpha=0.85)
ax8.set_yticks(np.arange(y_count))
ax8.set_ylim(y_count - 0.5, -0.5)
ax8.set_yticklabels(list(return_years))
ax8.set_title('年度收益率', fontname='pingfang HK')
ax8.grid(False)
#
# # 繪制月度收益率Histo直方圖
ax9.set_title('月度收益山積圖', fontname='pingfang HK')
ax9.hist(monthly_return_df.values.flatten(), bins=18, alpha=0.5,
label='monthly returns')
ax9.grid(False)
# 調整主圖表的日期格式
major_locator = years
major_formatter = years_fmt
minor_locator = months
minor_formatter = month_fmt_none
# 前五個主表的時間軸共享,因此只需要設置最下方表的時間軸即可
ax6.xaxis.set_major_locator(major_locator)
ax6.xaxis.set_major_formatter(major_formatter)
ax6.xaxis.set_minor_locator(minor_locator)
ax6.xaxis.set_minor_formatter(minor_formatter)
for ax in [ax1, ax2, ax3, ax4, ax5]:
plt.setp(ax.get_xticklabels(), visible=False)
plt.show()