2025年1月15日 星期三

完成阿蘭安金事宜

這周最重要的工作便是要辦理吾妹阿蘭之安金事宜, 我昨日即請假先回鄉下做最後準備, 檢視老師表列之備辦品項是否齊全, 昨日下午先至觀音廟將阿蘭金罐請到墓園南邊門內安放, 這樣今日早上就可以直接去墓園, 不用一大早請觀音廟的師姐開門了. 下午整理完房間就出發前往楠梓載菁菁, 她七點水晶店闆娘要接睫毛, 等她十點下班剛好順路去高鐵載姐姐一起回鄉下. 

昨晚高雄下起小雨, 今晨起來還在下間歇的毛毛雨. 06:20 出發途經榮發舅媽的豬肉店下車取三副牲禮 (3700 元) 後前往墓園, 到土地公廟時發現泥水劉師傅已到, 趕緊先去墓園貼紅紙, 07:00 溫老師抵達後先打開北邊門再打開南邊門讓墓室通風, 調整好牲禮果品位置 (由左而右麵魚雞豬) 倒好茶點燃蠟燭後先進行落馬儀式, 以紙杯盛香爐灰做成香爐, 將金罐從南邊門捧出置於面對墓園之右側台階上, 點六支香, 三支給老師, 由老師先向祖先說明今日儀式目的, 再向阿蘭金罐講述並祈願後將金罐捧入墓室, 劉師傅已用磚頭與混泥土將座位底部墊高, 金罐置入後周圍以細沙填實, 完成後先鎖北邊內門, 再鎖南邊內門, 劉師傅任務完成奉上紅包後先行離去. 接著點香由老師引導祭拜, 拜完奉上紅包老師也先行離開, 接下來就是跟掃墓程序一樣酒過三巡燒金紙, 再次點香向祖先稟告安金儀式完成後鳴炮即可收拾祭品. 

老師離開前給了一張單子列出安金之後續辦理事項 :
  1. 安金後第二天第三天 : 須至墓園奉茶, 上香, 燒金
  2. 安金後第十二天 : 須備三份水果 (祖先, 后土, 天神) 至墓園奉茶, 上香, 燒金
  3. 安金後滿月日 : 須備五牲一副, 發粄, 紅粄各三包, 水果三份, 鮮花一對, 防風燭三對, 鞭炮, 壽金, 香, 茶至墓園祭拜
今明兩天我將從鄉下通勤, 早上六點半出發前往墓園奉茶上香燒金後再去上班. 安金後第 12 天即 1/26 (週日, 春節連假第 2 天, 年二十七) 毋須請假, 安金後滿月日是農曆 1 月 16 日 (國曆 2/13, 元宵節後一日) 是週四要請半天假. 

另有兩個注意事項 : 
  1. 四個月內不可包奠儀, 但若這之前有包過紅包則可. 
  2. 今年掃墓須在農曆 2 月 21 日 (春社日, 春分, 國曆 3/20) 之前掃墓.
每年掃墓都是農曆二月第二個周日, 今年是 3/9 日剛好在春分之前. 


2025年1月13日 星期一

Python 學習筆記 : 策略績效與風險分析套件 pyfolio

Pyfolio 是一個用來評估投資策略表現的 Python 套件, 透過 Jupyter 或 Colab 等 web 介面呈現詳細的分析數據與視覺化圖表, 讓金融投資策略開發者可快速獲得投資組合的風險與績效分析, 例如年化報酬率, 夏普比率, 最大回撤 (MDD) 與波動率等. 除了獨立進行分析, 也可與回測框架 (例如 Zipline) 搭配使用. 

Pyfolio 最早是在 2015 年由位於波士頓的對沖基金公司 Quantopian 所開發, 做為其演算法量化交易平台回測引擎 Zipline 的策略績效分析工具. 於 2017 年開放原始碼後受到金融交易社群與學術圈的喜愛. 不過由於 Quantopian 於 2022 年底停止營運, Pyfolio 的開發與維護也隨之停頓, 但 pyfolio 使用者社群仍非常活躍, 其原始碼與教學文件放在 GitHub :


網路教學文章參考 : 



1. 安裝 pyfolio 套件 : 

Pyfolio 可以直接用 pip 安裝 : 

pip install pyfolio 

這會從 PyPi 網站下載套件, 參考 : 

D:\python\test>pip install pyfolio    
Collecting pyfolio
  Downloading pyfolio-0.9.2.tar.gz (91 kB)
  Preparing metadata (setup.py) ... done
Requirement already satisfied: ipython>=3.2.3 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pyfolio) (8.15.0)
Requirement already satisfied: matplotlib>=1.4.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pyfolio) (3.7.2)
Requirement already satisfied: numpy>=1.11.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pyfolio) (1.24.3)
Requirement already satisfied: pandas>=0.18.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pyfolio) (2.0.3)
Requirement already satisfied: pytz>=2014.10 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pyfolio) (2023.3)
Requirement already satisfied: scipy>=0.14.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pyfolio) (1.11.2)
Requirement already satisfied: scikit-learn>=0.16.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pyfolio) (1.3.0)
Requirement already satisfied: seaborn>=0.7.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pyfolio) (0.12.2)
Collecting empyrical>=0.5.0 (from pyfolio)
  Downloading empyrical-0.5.5.tar.gz (52 kB)
  Preparing metadata (setup.py) ... done
Collecting pandas-datareader>=0.2 (from empyrical>=0.5.0->pyfolio)
  Downloading pandas_datareader-0.10.0-py3-none-any.whl.metadata (2.9 kB)
Requirement already satisfied: backcall in c:\users\tony1\appdata\roaming\python\python310\site-packages (from ipython>=3.2.3->pyfolio) (0.2.0)
Requirement already satisfied: decorator in c:\users\tony1\appdata\roaming\python\python310\site-packages (from ipython>=3.2.3->pyfolio) (5.1.1)
Requirement already satisfied: jedi>=0.16 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from ipython>=3.2.3->pyfolio) (0.18.2)
Requirement already satisfied: matplotlib-inline in c:\users\tony1\appdata\roaming\python\python310\site-packages (from ipython>=3.2.3->pyfolio) (0.1.6)
Requirement already satisfied: pickleshare in c:\users\tony1\appdata\roaming\python\python310\site-packages (from ipython>=3.2.3->pyfolio) (0.7.5)
Requirement already satisfied: prompt-toolkit!=3.0.37,<3.1.0,>=3.0.30 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from ipython>=3.2.3->pyfolio) (3.0.39)
Requirement already satisfied: pygments>=2.4.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from ipython>=3.2.3->pyfolio) (2.16.1)
Requirement already satisfied: stack-data in c:\users\tony1\appdata\roaming\python\python310\site-packages (from ipython>=3.2.3->pyfolio) (0.6.2)
Requirement already satisfied: traitlets>=5 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from ipython>=3.2.3->pyfolio) (5.9.0)
Requirement already satisfied: exceptiongroup in c:\users\tony1\appdata\roaming\python\python310\site-packages (from ipython>=3.2.3->pyfolio) (1.1.3)
Requirement already satisfied: colorama in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from ipython>=3.2.3->pyfolio) (0.4.6)
Requirement already satisfied: contourpy>=1.0.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from matplotlib>=1.4.0->pyfolio) (1.1.0)
Requirement already satisfied: cycler>=0.10 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from matplotlib>=1.4.0->pyfolio) (0.11.0)
Requirement already satisfied: fonttools>=4.22.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from matplotlib>=1.4.0->pyfolio) (4.42.1)
Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from matplotlib>=1.4.0->pyfolio) (1.4.5)
Requirement already satisfied: packaging>=20.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from matplotlib>=1.4.0->pyfolio) (23.1)
Requirement already satisfied: pillow>=6.2.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from matplotlib>=1.4.0->pyfolio) (9.5.0)
Requirement already satisfied: pyparsing<3.1,>=2.3.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from matplotlib>=1.4.0->pyfolio) (3.0.9)
Requirement already satisfied: python-dateutil>=2.7 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from matplotlib>=1.4.0->pyfolio) (2.8.2)
Requirement already satisfied: tzdata>=2022.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pandas>=0.18.1->pyfolio) (2023.3)
Requirement already satisfied: joblib>=1.1.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from scikit-learn>=0.16.1->pyfolio) (1.3.2)
Requirement already satisfied: threadpoolctl>=2.0.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from scikit-learn>=0.16.1->pyfolio) (3.2.0)
Requirement already satisfied: parso<0.9.0,>=0.8.0 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from jedi>=0.16->ipython>=3.2.3->pyfolio) (0.8.3)
Requirement already satisfied: lxml in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pandas-datareader>=0.2->empyrical>=0.5.0->pyfolio) (4.9.3)
Requirement already satisfied: requests>=2.19.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pandas-datareader>=0.2->empyrical>=0.5.0->pyfolio) (2.31.0)
Requirement already satisfied: wcwidth in c:\users\tony1\appdata\roaming\python\python310\site-packages (from prompt-toolkit!=3.0.37,<3.1.0,>=3.0.30->ipython>=3.2.3->pyfolio) (0.2.6)
Requirement already satisfied: six>=1.5 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from python-dateutil>=2.7->matplotlib>=1.4.0->pyfolio) (1.16.0)
Requirement already satisfied: executing>=1.2.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from stack-data->ipython>=3.2.3->pyfolio) (1.2.0)
Requirement already satisfied: asttokens>=2.1.0 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from stack-data->ipython>=3.2.3->pyfolio) (2.2.1)
Requirement already satisfied: pure-eval in c:\users\tony1\appdata\roaming\python\python310\site-packages (from stack-data->ipython>=3.2.3->pyfolio) (0.2.2)
Requirement already satisfied: charset-normalizer<4,>=2 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests>=2.19.0->pandas-datareader>=0.2->empyrical>=0.5.0->pyfolio) (3.2.0)
Requirement already satisfied: idna<4,>=2.5 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests>=2.19.0->pandas-datareader>=0.2->empyrical>=0.5.0->pyfolio) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from requests>=2.19.0->pandas-datareader>=0.2->empyrical>=0.5.0->pyfolio) (1.26.19)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests>=2.19.0->pandas-datareader>=0.2->empyrical>=0.5.0->pyfolio) (2023.7.22)
Downloading pandas_datareader-0.10.0-py3-none-any.whl (109 kB)
Building wheels for collected packages: pyfolio, empyrical
  Building wheel for pyfolio (setup.py) ... done
  Created wheel for pyfolio: filename=pyfolio-0.9.2-py3-none-any.whl size=88689 sha256=cc4a1c12fb59a822d23492165f319d973d85870c01bda5b80915ced28a199a88
  Stored in directory: c:\users\tony1\appdata\local\pip\cache\wheels\71\38\bc\e53700cfd8b0ad6b539d2fbaaf060ed8a299e7622a5b86ef42
  Building wheel for empyrical (setup.py) ... done
  Created wheel for empyrical: filename=empyrical-0.5.5-py3-none-any.whl size=39779 sha256=7780681258103e51def4b2e78230f7d9e84dddf677cb7e840be6b1abf4701561
  Stored in directory: c:\users\tony1\appdata\local\pip\cache\wheels\0e\2e\f2\d6d2d9a1eb8fbbd9949bb5d4c00f753e3b74e5bd7ed10b1d36
Successfully built pyfolio empyrical
Installing collected packages: pandas-datareader, empyrical, pyfolio
Successfully installed empyrical-0.5.5 pandas-datareader-0.10.0 pyfolio-0.9.2

可見 Pyfolio 是在 Pandas, Numpy, Matplotlib + Seaborn 等套件基礎上建構的, 主要依賴 Matplotlib 作為繪圖核心. 匯入 pyfolio 模組檢視版本 (通常取簡名 pf) :

>>> import pyfolio as pf    
C:\Users\tony1\AppData\Local\Programs\Thonny\lib\site-packages\pyfolio\pos.py:26: UserWarning: Module "zipline.assets" not found; mutltipliers will not be applied to position notionals.
  warnings.warn(

出現這個 warning 的原因是因為 Pyfolio 通常與回測工具 Zipline 搭配使用, 其原始碼中有一部分預設會匯入其處理資產權重的 assets 模組, 因沒有安裝 Zipline 套件所以找不到此模組. 因 Zipline 並非 Pyfolio 必要之相依套件因此可忽略之, 也可以用 warnings 模組的 filterwarnings() 函式將其濾掉 :

>>> import warnings   
>>> warnings.filterwarnings('ignore')   

再次匯入 pyfolio 就不會有 warning 了 : 

>>> import pyfolio as pf   
>>> pf.__version__   
'0.9.2'


>>> list_members(pf)    
APPROX_BDAYS_PER_MONTH <class 'int'>
FACTOR_PARTITIONS <class 'dict'>
FigureCanvasAgg <class 'type'>
FuncFormatter <class 'type'>
MM_DISPLAY_UNIT <class 'float'>
Markdown <class 'type'>
OrderedDict <class 'type'>
STAT_FUNCS_PCT <class 'list'>
axes_style <class 'function'>
capacity <class 'module'>
create_bayesian_tear_sheet <class 'function'>
create_capacity_tear_sheet <class 'function'>
create_full_tear_sheet <class 'function'>
create_interesting_times_tear_sheet <class 'function'>
create_perf_attrib_tear_sheet <class 'function'>
create_position_tear_sheet <class 'function'>
create_returns_tear_sheet <class 'function'>
create_risk_tear_sheet <class 'function'>
create_round_trip_tear_sheet <class 'function'>
create_simple_tear_sheet <class 'function'>
create_txn_tear_sheet <class 'function'>
customize <class 'function'>
datetime <class 'module'>
deprecate <class 'module'>
display <class 'function'>
division <class '__future__._Feature'>
ep <class 'module'>
figure <class 'module'>
gridspec <class 'module'>
have_bayesian <class 'bool'>
interesting_periods <class 'module'>
matplotlib <class 'module'>
np <class 'module'>
patches <class 'module'>
pd <class 'module'>
perf_attrib <class 'module'>
plot_annual_returns <class 'function'>
plot_capacity_sweep <class 'function'>
plot_cones <class 'function'>
plot_daily_turnover_hist <class 'function'>
plot_daily_volume <class 'function'>
plot_drawdown_periods <class 'function'>
plot_drawdown_underwater <class 'function'>
plot_exposures <class 'function'>
plot_gross_leverage <class 'function'>
plot_holdings <class 'function'>
plot_long_short_holdings <class 'function'>
plot_max_median_position_concentration <class 'function'>
plot_monthly_returns_dist <class 'function'>
plot_monthly_returns_heatmap <class 'function'>
plot_monthly_returns_timeseries <class 'function'>
plot_perf_stats <class 'function'>
plot_prob_profit_trade <class 'function'>
plot_return_quantiles <class 'function'>
plot_returns <class 'function'>
plot_rolling_beta <class 'function'>
plot_rolling_returns <class 'function'>
plot_rolling_sharpe <class 'function'>
plot_rolling_volatility <class 'function'>
plot_round_trip_lifetimes <class 'function'>
plot_sector_allocations <class 'function'>
plot_slippage_sensitivity <class 'function'>
plot_slippage_sweep <class 'function'>
plot_turnover <class 'function'>
plot_txn_time_hist <class 'function'>
plotting <class 'module'>
plotting_context <class 'function'>
plt <class 'module'>
pos <class 'module'>
pytz <class 'module'>
risk <class 'module'>
round_trips <class 'module'>
scipy <class 'module'>
show_and_plot_top_positions <class 'function'>
show_perf_stats <class 'function'>
show_profit_attribution <class 'function'>
show_worst_drawdown_periods <class 'function'>
sns <class 'module'>
sp <class 'module'>
tears <class 'module'>
time <class 'builtin_function_or_method'>
timer <class 'function'>
timeseries <class 'module'>
txn <class 'module'>
utils <class 'module'>
warnings <class 'module'>
wraps <class 'function'>

雖然 Pyfolio 提供玲瑯滿目的函式與模組, 但我們通常只會用到它的快捷函式 create_returns_tear_sheet(), 它會自動產生績效分析與風險評估的全部數據與圖表. 此函式必須傳入一個 Series 物件, 且其索引須為日期時間類型, 值通常為代表報酬率 (return) 的小數, 這可以利用 Series 物件的 pct_change() 方法求得. 


2. 在 web 介面上執行 pyfolio : 

Pyfolio 必須在 web 介面例如 Jupyter Lab, Jupyter Notebook 或 Colab 上執行, 因為它要透過網頁繪製數據分析的圖表, 命令列執行環境無法滿足此要求. 

先在本機用 Jupyter Lab 測試, 先在命令列輸入 jupyter lab 啟動 web 介面 : 

D:\python\test>jupyter lab     




匯入 Pyfolio : import pyfolio as pf 




以下以買入 0050 一股持有不賣策略為例測試 pyfolio 用法, 先用 yfinance 下載股票資料 : 

import yfinance as yf   
df=yf.download('0050.TW')   




檢視前後五筆資料 : 

df.head()    

                 Open       High        Low      Close  Adj Close  Volume
Date                                                                     
2008-01-02  60.009998  60.009998  60.009998  60.009998  42.407536       0
2008-01-03  58.889999  58.889999  58.889999  58.889999  41.616066       0
2008-01-04  59.009998  59.009998  59.009998  59.009998  41.700867       0
2008-01-07  56.389999  56.389999  56.389999  56.389999  39.849377       0
2008-01-08  56.980000  56.980000  56.980000  56.980000  40.266319       0

df.tail()   

                  Open        High  ...   Adj Close    Volume
Date                                ...                      
2025-01-06  198.649994  202.300003  ...  202.149994  24627793
2025-01-07  204.949997  206.000000  ...  204.399994  15943844
2025-01-08  202.649994  203.350006  ...  201.350006  11855717
2025-01-09  200.199997  201.149994  ...  199.050003   8430655
2025-01-10  198.250000  198.250000  ...  198.250000         0

然後利用此股票資料中的調整後收盤價 Adj Close 計算此策略之報酬率, 如果時間較短用 Close 尚可, 但期間長達 15 年應該用調整後價格較準確. 計算報酬率可以呼叫 Series 物件的 pct_change() 方法達成, 此方法是將今日價減昨日價後再除以昨日價得到今日報酬率 :

returns=df['Adj Close'].pct_change()  
print(returns)  



 
可知傳回值為一個以日期時間為索引的 Series 物件, 符合 Pyfolio 的要求, 將此 returns 傳給 pf.create_returns_tear_sheet() 即可得到分析結果 : 

pf.create_returns_tear_sheet(returns)   

但執行卻出現下列錯誤訊息 : 

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[23], line 2
      1 returns.dropna()
----> 2 pf.create_returns_tear_sheet(returns.dropna()) 

File ~\AppData\Local\Programs\Thonny\lib\site-packages\pyfolio\plotting.py:52, in customize.<locals>.call_w_context(*args, **kwargs)
     50 if set_context:
     51     with plotting_context(), axes_style():
---> 52         return func(*args, **kwargs)
     53 else:
     54     return func(*args, **kwargs)

File ~\AppData\Local\Programs\Thonny\lib\site-packages\pyfolio\tears.py:496, in create_returns_tear_sheet(returns, positions, transactions, live_start_date, cone_std, benchmark_rets, bootstrap, turnover_denom, header_rows, return_fig)
    493 if benchmark_rets is not None:
    494     returns = utils.clip_returns_to_benchmark(returns, benchmark_rets)
--> 496 plotting.show_perf_stats(returns, benchmark_rets,
    497                          positions=positions,
    498                          transactions=transactions,
    499                          turnover_denom=turnover_denom,
    500                          bootstrap=bootstrap,
    501                          live_start_date=live_start_date,
    502                          header_rows=header_rows)
    504 plotting.show_worst_drawdown_periods(returns)
    506 vertical_sections = 11

File ~\AppData\Local\Programs\Thonny\lib\site-packages\pyfolio\plotting.py:648, in show_perf_stats(returns, factor_returns, positions, transactions, turnover_denom, live_start_date, bootstrap, header_rows)
    645     perf_stats = pd.DataFrame(perf_stats_all, columns=['Backtest'])
    647 for column in perf_stats.columns:
--> 648     for stat, value in perf_stats[column].iteritems():
    649         if stat in STAT_FUNCS_PCT:
    650             perf_stats.loc[stat, column] = str(np.round(value * 100,
    651                                                         1)) + '%'

File ~\AppData\Roaming\Python\Python310\site-packages\pandas\core\generic.py:5989, in NDFrame.__getattr__(self, name)
   5982 if (
   5983     name not in self._internal_names_set
   5984     and name not in self._metadata
   5985     and name not in self._accessors
   5986     and self._info_axis._can_hold_identifiers_and_holds_name(name)
   5987 ):
   5988     return self[name]
-> 5989 return object.__getattribute__(self, name)

AttributeError: 'Series' object has no attribute 'iteritems'

將錯誤訊息丟給 ChatGPT, 發現原因是與 Pandas 版本不相容, 檢查目前版本是 2.0.3, 而 Pyfolio 因為使用 Pandas v1 版, 所用的 iteritems() 函式已被廢棄導致錯誤, ChatGPT 建議的解決辦法之一是把 Pandas 版本降回 1.2.x 版, 例如 1.2.5 : 

pip install pandas==1.2.5

第二個辦法是到 Pyfolio 的安裝路徑下找出 plotting.py 檔, 將第 648 列的 iteritem() 改為 item() 即可, 利用 .__file__ 屬性即可查到此路徑 :

print(pf.__file__)   

C:\Users\tony1\AppData\Local\Programs\Thonny\lib\site-packages\pyfolio\__init__.py 



 
改好後再次執行出現另一個錯誤訊息 : 


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[5], line 1
----> 1 pf.create_returns_tear_sheet(returns)

File ~\AppData\Local\Programs\Thonny\lib\site-packages\pyfolio\plotting.py:52, in customize.<locals>.call_w_context(*args, **kwargs)
     50 if set_context:
     51     with plotting_context(), axes_style():
---> 52         return func(*args, **kwargs)
     53 else:
     54     return func(*args, **kwargs)

File ~\AppData\Local\Programs\Thonny\lib\site-packages\pyfolio\tears.py:504, in create_returns_tear_sheet(returns, positions, transactions, live_start_date, cone_std, benchmark_rets, bootstrap, turnover_denom, header_rows, return_fig)
    494     returns = utils.clip_returns_to_benchmark(returns, benchmark_rets)
    496 plotting.show_perf_stats(returns, benchmark_rets,
    497                          positions=positions,
    498                          transactions=transactions,
   (...)
    501                          live_start_date=live_start_date,
    502                          header_rows=header_rows)
--> 504 plotting.show_worst_drawdown_periods(returns)
    506 vertical_sections = 11
    508 if live_start_date is not None:

File ~\AppData\Local\Programs\Thonny\lib\site-packages\pyfolio\plotting.py:1664, in show_worst_drawdown_periods(returns, top)
   1648 def show_worst_drawdown_periods(returns, top=5):
   1649     """
   1650     Prints information about the worst drawdown periods.
   1651 
   (...)
   1661         Amount of top drawdowns periods to plot (default 5).
   1662     """
-> 1664     drawdown_df = timeseries.gen_drawdown_table(returns, top=top)
   1665     utils.print_table(
   1666         drawdown_df.sort_values('Net drawdown in %', ascending=False),
   1667         name='Worst drawdown periods',
   1668         float_format='{0:.2f}'.format,
   1669     )

File ~\AppData\Local\Programs\Thonny\lib\site-packages\pyfolio\timeseries.py:1008, in gen_drawdown_table(returns, top)
   1003     df_drawdowns.loc[i, 'Duration'] = len(pd.date_range(peak,
   1004                                                         recovery,
   1005                                                         freq='B'))
   1006 df_drawdowns.loc[i, 'Peak date'] = (peak.to_pydatetime()
   1007                                     .strftime('%Y-%m-%d'))
-> 1008 df_drawdowns.loc[i, 'Valley date'] = (valley.to_pydatetime()
   1009                                       .strftime('%Y-%m-%d'))
   1010 if isinstance(recovery, float):
   1011     df_drawdowns.loc[i, 'Recovery date'] = recovery

AttributeError: 'numpy.int64' object has no attribute 'to_pydatetime'

詢問 ChatGPT 回覆錯誤原因同樣是 Pandas 版本不相容所致, 建議修改 Pyfolio 的 timeseries.py 檔, 將其中第 1008~1009 列改為如下寫法 :

df_drawdowns.loc[i, 'Valley date'] = pd.Timestamp(valley).strftime('%Y-%m-%d')




舊版寫法是 :

df_drawdowns.loc[i, 'Valley date'] = (valley.to_pydatetime()
                                      .strftime('%Y-%m-%d'))

改好後重新執行又出現另一個錯誤 : 



原因還是 Pandas 版本不相容, 同樣是 timeseries.py 這檔案, 問題出現在 1014~1015 列的 gen_drawdown_table() 函式寫法, 原本寫法是 :

df_drawdowns.loc[i, 'Net drawdown in %'] = (
    (df_cum.loc[peak] - df_cum.loc[valley]) / df_cum.loc[peak]) * 100




ChatGPT 建議改為如下 : 

try:
    df_drawdowns.loc[i, 'Net drawdown in %'] = (
        (df_cum.loc[peak] - df_cum.loc[valley]) / df_cum.loc[peak]) * 100
except KeyError:
    # 如果索引找不到,設置為 NaN 或其他處理方式
    df_drawdowns.loc[i, 'Net drawdown in %'] = float('nan')

改好後就可以順利執行, 結果會產生下面的 13 張分析圖表 :

第一張圖表包含年化報酬率, 累積報酬率, 最大回撤率 (MDD, Maximum Draw Down) 等統計數值之摘要報告 :




MDD 表示資產淨值從最高峰到最低谷的最大跌幅, 是評估投資組合在特定期間內抗風險能力的重要指標, 它反映了投資者在最糟糕情況下可能面臨的損失比例 (最大曝險比率), 是考量曝險能力的重要數據. MDD 的計算通常用資產最大值與最小值之差除以最大值而得, 也可以用累積報酬率的峰值與谷值來算.  

第二張圖表顯示歷次回撤的區間與波峰波谷時間 : 




第三張圖表 Cumulative returns 是累積報酬率折線圖 :




第四張圖表 Cumulative returns volatility mathed to benchmark 是累積報酬率與基準報酬率 (例如大盤) 的比較, 可看出此策略之投資組合是否跑贏基準報酬率 : 



 
第五張圖表 Cumulative returns on logorithmic scale 則是以對數刻度來顯示累積報酬率與基準報酬率之比較 : 




第六張圖表 Returns 是每日報酬率折線圖 : 




第七張圖表 Rolling volatility (6-month) 顯示六個月為窗口 (window) 的移動波動率的變化, 以標準差為單位來衡量投資組合報酬率之波動程度 :




第八張圖表 Rolling Sharpe ratio (6-month) 顯示 6 個月為窗口 (window) 的夏普比率變化, 用來衡量報酬率相對於風險的效率. 夏普比的定義是單位風險所能獲得的報酬率, 基本上低於 0.5 的策略不要考慮, 大於 1 就算是很不錯的策略了. 




第九張圖表 Top 5 Drawdown periods 顯示最大的前五個回撤區間 : 





第十張圖表 Underwater plot 顯示投資組合的回撤深度, 頻率和恢復時間, 讓投資者可觀察策略在不同行情下的表現, 了解該策略是否符合自身的風險承受能力 : 




第十張圖表 Monthly returns 依照年月順序 (年份為垂直軸由上而下, 月份為水平軸由左至右) 以不同顏色在熱力圖中顯示每個月的報酬率表現, 正報酬以綠色系, 顏色越深報酬越大; 負報酬以紅色系表示, 越紅越負, 可藉顏色與深淺不同來觀察每年的季節性或趨勢性表現, 找出表現特別好或特別差的月份 : 




第十一張圖表 Annual returns 由上而下依照年度以水平長條圖顯示各年度之報酬率 : 




第十二張圖表 Distribution of monthly returns 以直方圖呈現投資組合的每月報酬率分佈狀況, 可讓投資者了解報酬率的集中程度, 對稱性, 以及是否存在異常值. 直方圖的橫軸是報酬率的分布範圍, 它將將整個報酬率範圍分為若干個區間 (bins), 例如每 2%. 而縱軸則表示每個報酬率區間發生的次數, 高度越高表示該區間內的報酬率出現得越頻繁 : 




第十三張圖表 Return quantiles 呈現投資組合報酬率在不同時間範圍內的分佈情況, 它用類似箱型圖的形式來顯示報酬率在各個時間段的分佈範圍, 橫軸表示日, 周, 月等不同時間範圍; 縱軸表示報酬率, 箱型圖最中間的線代表報酬率之中位數, 箱子的範圍 (上下四分位距) 表示 25% 到 75% 分位數的報酬率範圍 :




投資者可從箱型圖的範圍變化評估策略的穩定性和表現特徵, 若箱型圖的範圍隨著時間窗口增加而變窄, 表示投資組合在更長的時間範圍時表現更加穩定; 如果範圍隨著時間增加而變寬則表示隨著時間窗口拉長, 報酬率可能更加不穩定或波動變大. 

我將上面修改好的 plotting.py 與 timeseries.py 這兩個檔案放在 GitHub, 如果 Pandas 版本是 v2 以上的話, 只要將此兩檔案下載後複製到 Pyfolio 安裝目錄下覆蓋舊檔案即可 :


查詢 Pyfolio 安裝路徑的方法 :

import pyfolio as pf
print(pf.__file__)