深入解析:使用Python構建全面的市場風險監測工具
七月全球同步發生多個利空事件,市場下殺速度快到根本來不及反應,直接被套牢。事後想想,如果有個工具能在風險累積時提早示警,也許不會這麼狼狽。於是花了幾個週末做了這個 VolatilityVigil——用 Python 同時監測多個市場的風險狀態,從波動度計算到 PCA 異常偵測,最後用 K-means 把市場分群,輸出一張能看懂的熱力圖。
1. 程式概述
核心功能分五塊:
- 透過 yfinance 抓 Yahoo Finance 多市場歷史資料(美股、歐股、亞股、商品、外匯)
- 計算各市場的年化波動度
- 用 PCA 重建誤差做異常偵測
- 整合波動度與異常訊號算出風險評分
- 用 K-means 把市場狀態分群,並將群號整合進最終判讀輸出
用到的套件:pandas(資料處理)、sklearn(演算法)、yfinance(市場資料)、matplotlib 與 seaborn(視覺化)。
2. 核心功能詳解
2.1 資料獲取
先把整份程式會用到的 import 一次列齊,免得你抄到一半發現少了哪個套件:
1 | import pandas as pd |
監測標的涵蓋美國、歐洲、亞洲主要股市指數,加上商品期貨與外匯市場:
1 | symbols = [ |
接著用 yfinance 抓這些標的過去 90 天的收盤價:
1 | def get_date_range(): |
90 天是一個設計選擇:短到能反映近期市場結構,又長到在波動度計算上不至於被單一事件過度主導。如果監測目標是 flash crash 這類極短暫的異動,視窗可以縮短到 30~60 天;若要捕捉季節性循環,則需要拉長。
2.2 風險評估
風險評分由兩個訊號合成:波動度超標 + PCA 異常偵測。
波動度計算:用 20 日滾動標準差乘以 √252 換算成年化數字,把不同長度的取樣窗放到同一個年化基準上比較:
1 | def calculate_volatility(data, window=20): |
PCA 異常偵測:先用 StandardScaler 標準化,讓不同市場的數值落在相同尺度,再用 PCA 壓縮到保留 95% 變異的維度,把資料反向重建回標準化後的空間(注意是標準化空間、不是原始價格尺度),計算均方重建誤差(MSE)。MSE 高代表該日的市場結構無法被主成分良好重建,通常對應到市場走勢出現不尋常的配置——但這不等同於「MSE 高就是異常」:若多市場同步大跌,PCA 反而能用少數主成分解釋絕大部分的共同走勢,重建誤差可能偏低,訊號會靜音。這個方法對「結構性分歧」(部分市場脫鉤)更敏感,對「全球同步崩跌」的偵測力相對有限。門檻取 threshold 倍的 MSE 標準差,跟著資料的離散程度浮動,比寫死一個固定值不容易誤判:
1 | def detect_anomalies(data, threshold=3): |
這裡用 .ffill().bfill() 而不是舊的 fillna(method='ffill')——後者已經被棄用,新版 pandas 會發 FutureWarning 要你改用 .ffill() / .bfill(),趁早換掉。
風險評分整合:異常偵測只跑一次,吃整張多市場資料表,讓 PCA 在所有市場之間找結構性分歧;得到的是「某一天市場結構是否異常」的單一布林訊號。波動度超標則是逐市場各自判斷。兩個訊號相加,得到 0 / 1 / 2 三級評分:
1 | def calculate_risk_score(market_data): |
anomalies 是跨市場的共同訊號,所以每一欄的異常分量都來自同一個 PCA 判讀——這是刻意的設計。把資料切成單欄各跑一次 PCA 反而會壞掉:單一序列標準化後幾乎能被一個主成分完整重建,重建誤差趨近於零,異常永遠不會觸發。PCA 異常偵測的價值正是在多市場之間找「誰脫鉤了」,餵單欄等於把這個能力關掉。
common_index 那兩行是防禦性寫法,不是必要的對齊:波動度前 20 天是 NaN(注意是 NaN 值、不是缺列,索引仍在),而 detect_anomalies 內部用 .ffill().bfill() 也保留了完整索引,兩邊索引在這份資料上本來就一致,intersection 取完等於沒變。留著它是為了萬一日後改動讓某一邊真的丟了列時不會錯位。另外要記得:前 20 天波動度為 NaN,(NaN > 門檻) 會被判成 False,所以這段期間的評分實際只反映異常分量、不含波動度分量。
2.3 市場狀態聚類
K-means 把每天的多市場風險評分向量分成 5 群,目的是讓工具能辨識不同的市場結構——例如「全球同步緊縮」「單市場異動」「正常低波動」。K-means 不接受 NaN,所以先前向再後向填補缺值:
1 | def cluster_market_states(data, n_clusters=5): |
n_init=10 是明確指定的:sklearn 1.4 把預設值從 10 改成 'auto',而 'auto' 在 init='k-means++'(預設)下等效為 1 次初始化,比舊版少跑 9 次。顯式寫 n_init=10 是讓行為與 1.4 以前的舊版一致,確保跨版本結果穩定。
分群結果會在第 4 節的 explain_risk_score() 一起輸出,讓你同時看到當天的評分等級與所屬的市場狀態群。
3. 視覺化
四種圖表各有用途,程式碼如下:
風險評分熱力圖,一眼看出哪個市場、哪段時間風險集中:
1 | def visualize_risk_scores(risk_score): |
市場趨勢圖,疊加各市場收盤價走勢,方便對照風險訊號與價格變動的時間差:
1 | def visualize_market_trends(market_data): |
市場相關性矩陣,用相關係數顯示各市場的同步性,高度相關的市場在壓力測試時很難分散風險:
1 | def visualize_correlation_matrix(market_data): |
總體風險評分分佈圖,把各市場的每日風險評分加總,畫出時序分佈,快速抓出系統性壓力最高的時間點:
1 | def visualize_overall_risk(risk_score): |
4. 結果解釋
explain_risk_score() 接收風險評分與聚類結果,同時輸出評分描述和當天所屬的市場狀態群號。這樣就能回答「今天評分高,但這是哪一類型的高風險?」——全球同步飆升(群 0)和單一市場孤立異動(群 3)的處理策略不同:
1 | def explain_risk_score(risk_score, cluster_labels=None): |
呼叫方式:
1 | risk_score = calculate_risk_score(market_data) |
5. 使用注意事項
- 資料時效性:若最新交易日距今超過數天,通常是遇到連假或 Yahoo Finance API 暫時異常,建議手動確認資料的最新日期再解讀輸出結果。
- 缺值處理:前向填補優先(用前一個交易日的收盤價),再後向填補。程式不會印出缺值統計,若需確認哪些欄位有缺值,請自行對
market_data執行market_data.isna().sum()。 - 市場休市:不同市場的交易日曆不同,例如日本市場黃金週連休,這段期間的訊號應降低權重解讀。
- yfinance 抓取可靠性:
yf.download多標的時,部分指數(如^TWII、000001.SS)或匯率代碼偶爾抓取失敗,會靜默掉欄或整欄回傳 NaN。執行後建議用print(market_data.shape, market_data.columns.tolist())確認欄數與欄名是否如預期,再繼續後續計算。 - PCA 異常偵測的方法論限制:
detect_anomalies是對「標準化後的收盤價 level」做 PCA 重建,而波動度計算用的是報酬率。收盤價 level 本身非定態,長期趨勢會影響 PCA 重建誤差的基線,因此異常訊號偏向捕捉結構性分歧(各市場走勢脫鉤),較不適合作為精準的進出場警報,應視為輔助判讀的參考訊號。 - KMeans 收斂警告:
cluster_market_states預設n_clusters=5。若 90 天視窗內risk_score多數為 0(市場長期低波動),相異樣本點可能少於 5 個,KMeans 會拋出ConvergenceWarning,屬正常現象,可將n_clusters調小(如 3)或直接忽略此警告。
6. 結論
這工具實際跑下來,最有用的不是單一市場的風險評分,而是相關性矩陣和總體風險分佈的組合——能看出「這次是全市場同步崩」還是「只有特定資產在異動」。七月那波如果有這個,看到相關係數矩陣全部拉高、總體風險評分衝到頂,應該會比較早減倉。
目前最想補的功能是加入 Telegram 推播:當總體風險評分連續兩日超過設定門檻,直接發通知,不用每天手動跑。另一個值得做的是把 PCA 換成滾動式的(每次只用過去 N 天的資料重新 fit),讓異常偵測的基準線會隨時間漂移,對長期趨勢改變的市場更敏感。




