CH.12 · 財金程式設計互動教材 · 最終章

投資組合風險分析與金融網站完整整合

全書最後一套金融工具——資產配置、共變異數矩陣、分散風險、VaR 與壓力測試,並把前十一章所有模組整合成完整金融知識平台。

股票 + 債券 + 現金

本章學習目標

  • 用共變異數矩陣公式計算多資產投資組合波動度
  • 理解相關係數與分散效果、系統性 vs 非系統性風險
  • 計算夏普比率、Sortino 比率、歷史法/參數法 VaR
  • 把全書 12 章所有模組整合成完整金融知識平台

01投資組合的基本概念

前十一章已完成金融網站開發所需的主要技術與工具:HTML/CSS/JavaScript、JSON、Python/Pandas、Chart.js/Plotly、GitHub/Cloudflare Pages、SEO、Streamlit 管理後台,以及債券、股票、選擇權與退休規劃分析模組。真實投資決策通常不是只持有單一資產,而是同時配置多項資產。投資組合分析的核心問題,不只是每一項資產的報酬率,而是這些資產放在一起後,整體報酬與風險會如何改變。

例如,股票與債券的波動度各自都很高,但若兩者的相關係數為負(股票下跌時債券往往上漲),組合後的整體波動度可能遠低於任一資產。這就是分散風險的基本原理。本章同時完成全書最後一項任務:將前十一章所有金融工具整合成完整的金融知識平台,並建立全站發布流程。

資產投資金額權重預期報酬加權報酬
股票600,000 元60%8%4.8%
債券300,000 元30%3%0.9%
現金100,000 元10%1%0.1%
合計1,000,000 元100%5.8%(組合報酬)
!

投資組合報酬率可以用加權平均,但投資組合波動度不能。原因是資產之間可能同向變動(正相關)、反向變動(負相關),或關係不明顯。當 ρ₁₂ < 1 時,組合波動度通常低於加權平均波動度,ρ₁₂ 越低,分散效果越明顯。

02共變異數、相關係數與分散效果

相關係數值意義分散效果投資組合風險
+1.0完全正相關(同方向移動)無分散效果= 加權平均波動度
+0.5正相關(同向但非完全)有一定分散效果< 加權平均波動度
0.0無線性關係分散效果明顯顯著低於加權平均
−0.5負相關(反向)分散效果強明顯降低
−1.0完全負相關(完全反向)最大分散效果在特定權重下可趨近 0
i

金融危機期間,多項風險資產的相關係數可能快速上升(同時被拋售),使歷史計算的分散效果失效。平時低相關不代表危機時仍然低相關。

多資產矩陣公式可以簡化多資產計算,在 Python 中使用 NumPy 的矩陣運算(@ 運算子)即可高效計算。

03可分散風險與不可分散風險

風險類型說明範例能否靠分散消除
可分散風險(非系統性)來自單一公司或產業的個別風險工廠事故、產品失敗、管理錯誤✅ 可透過持有多樣資產降低
不可分散風險(系統性)影響整體市場的共同風險金融危機、利率急升、地緣政治事件❌ 無法靠增加股票數量消除
i

持有 20 檔股票,但全部屬於同一產業,仍然高度集中。真正的分散應跨越公司、產業、地區、幣別、資產類別、利率敏感度與信用風險。

04六大風險指標速查

指標優點限制
夏普比率
(Rp−Rf)/σp
✅ 計算簡單,廣泛使用⚠️ 上漲波動也算風險;對非常態失真
Sortino比率
(Rp−R_target)/σd
✅ 只關注下行偏差,更符合投資人心理⚠️ 下行偏差樣本少時不穩定
最大回撤
min(Vt/Peak_t−1)
✅ 反映實際曾經歷的最大損失⚠️ 高度依賴觀察期間
歷史法 VaR
−PV×Quantile(q%)
✅ 不假設分布形態⚠️ 高度依賴樣本期間,尾端樣本量少
參數法 VaR
PV×(z_α×σ−μ)
✅ 計算快速,有數學基礎⚠️ 常態分布假設可能低估厚尾風險
Expected Shortfall✅ 提供尾端平均損失,比 VaR 更完整⚠️ 仍依賴歷史樣本

VaR 的兩種計算方式比較

方法計算方式優點限制
歷史法排列歷史報酬,取第 q 百分位數不假設分布,直接用歷史資料高度依賴樣本期,尾端觀察值少時不穩
參數法PV × (z_α × σ − μ),95% z≈1.645計算快,有數學基礎常態分布假設可能低估厚尾風險
!

VaR 不是最大損失!一日 95% VaR 100 萬元,代表在模型假設下,約有 5% 的交易日損失超過 100 萬元;損失上限並非 100 萬元。應搭配 Expected Shortfall 與壓力測試。

05Python 投資組合分析模組

程式區塊python/portfolio_analysis.py(核心函數節錄)
from dataclasses import dataclass
import numpy as np
import pandas as pd
import math

def validate_weights(weights: list[float]) -> None:
    total = sum(weights)
    if not math.isclose(total, 1.0, abs_tol=1e-8):
        raise ValueError(f'權重合計必須等於 1(100%),目前為 {total:.4f}')

def calculate_portfolio_return(weights: list[float], returns: list[float]) -> float:
    return sum(w * r for w, r in zip(weights, returns))

def calculate_portfolio_volatility(weights: np.ndarray, cov_matrix: np.ndarray) -> float:
    """使用矩陣公式:σp = √(w' × Σ × w)"""
    variance = weights @ cov_matrix @ weights.T
    return math.sqrt(max(variance, 0))   # 防止浮點負值

def build_covariance_matrix(returns_df: pd.DataFrame, trading_days: int = 252) -> pd.DataFrame:
    """從歷史日報酬計算年化共變異數矩陣。"""
    daily_cov = returns_df.cov()
    return daily_cov * trading_days

def calculate_sharpe_ratio(portfolio_return: float, risk_free_rate: float, volatility: float) -> float:
    return (portfolio_return - risk_free_rate) / volatility if volatility != 0 else float('nan')

def calculate_sortino_ratio(returns: pd.Series, target_return: float, trading_days: int = 252) -> float:
    downside = returns[returns < target_return / trading_days]
    downside_std = downside.std(ddof=1) * math.sqrt(trading_days)
    annualized_return = returns.mean() * trading_days
    return (annualized_return - target_return) / downside_std if downside_std != 0 else float('nan')
程式區塊VaR、Expected Shortfall 與壓力測試
def calculate_historical_var(returns: pd.Series, confidence: float = 0.95, portfolio_value: float = 1.0) -> float:
    """歷史法 VaR:直接取歷史報酬分布的分位數。"""
    quantile = returns.quantile(1 - confidence)
    return -portfolio_value * quantile

def calculate_expected_shortfall(returns: pd.Series, confidence: float = 0.95, portfolio_value: float = 1.0) -> float:
    """ES:損失超過 VaR 門檻的平均值(條件 VaR)。"""
    threshold = returns.quantile(1 - confidence)
    tail_losses = returns[returns <= threshold]
    if len(tail_losses) < 30:
        print(f'警告:尾端樣本僅 {len(tail_losses)} 筆,結果可能不穩定')
    return -portfolio_value * tail_losses.mean()

def run_stress_test(weights: list[float], stress_returns: list[float]) -> float:
    """壓力測試:直接代入極端情境報酬率(如股票 −25%、債券 −8%)。"""
    return sum(w * r for w, r in zip(weights, stress_returns))

06三種配置情境的風險報酬特性

配置類型典型權重(股/債/現)特性
積極型80/15/5較高預期報酬、較高波動度,適合長期且風險承受度高
平衡型60/30/10報酬與風險折衷,最常見的預設配置
保守型30/50/20較低波動度、較高流動性,適合臨近退休或風險承受度低

07Streamlit 投資組合風險工具

import streamlit as st

st.set_page_config(page_title='投資組合風險分析', page_icon='📉', layout='wide')
st.title('投資組合報酬與風險分析器')

tab1, tab2, tab3, tab4 = st.tabs(['資產配置', '風險指標', 'VaR 與壓力測試', '情境比較'])

with tab1:
    uploaded = st.file_uploader('上傳多資產歷史報酬 CSV', type=['csv'])
    # ... 讀取資料、驗證權重、計算共變異數矩陣 ...

with tab2:
    # 顯示夏普比率、Sortino 比率、最大回撤
    ...

with tab3:
    confidence = st.slider('信賴水準', 0.90, 0.99, 0.95)
    # 歷史法 VaR、參數法 VaR、Expected Shortfall、壓力測試情境輸入
    ...

with tab4:
    # 積極型/平衡型/保守型並排比較
    ...

08JavaScript 前台三資產配置工具

前台版本計算組合預期報酬與波動度(使用共變異數矩陣公式,但以使用者輸入的假設值取代歷史資料),適合直接整合至 Cloudflare Pages 靜態網站。完整程式碼可以到「示範分頁」看實際運作結果,也是本章挑戰任務要動手修改的對象。

// ── 三資產投資組合波動度(使用共變異數矩陣公式)──
function calculatePortfolioVolatility(w, sigmas, correlations) {
  // w[0/1/2]:股票/債券/現金權重(小數)
  // sigmas[0/1/2]:各資產年化波動度(小數)
  // correlations = [rho_01, rho_02, rho_12]
  const [w0,w1,w2] = w;
  const [s0,s1,s2] = sigmas;
  const [r01,r02,r12] = correlations;
  // 共變異數矩陣各項(已設定 Cov_ij = σᵢ×σⱼ×ρᵢⱼ)
  const var0 = w0*w0*s0*s0;
  const var1 = w1*w1*s1*s1;
  const var2 = w2*w2*s2*s2;
  const cov01 = 2*w0*w1*s0*s1*r01;
  const cov02 = 2*w0*w2*s0*s2*r02;
  const cov12 = 2*w1*w2*s1*s2*r12;
  const variance = var0 + var1 + var2 + cov01 + cov02 + cov12;
  return Math.sqrt(Math.max(variance, 0));   // 防止浮點負值
}

function handlePortfolioCalculation() {
  const weights = [get('stockWeight')/100, get('bondWeight')/100, get('cashWeight')/100];
  const returns = [get('stockReturn')/100, get('bondReturn')/100, get('cashReturn')/100];
  const sigmas = [get('stockVolatility')/100, get('bondVolatility')/100, get('cashVolatility')/100];
  const cors = [get('stockBondCorrelation'), get('stockCashCorrelation'), get('bondCashCorrelation')];

  // 驗證:權重合計必須為 1,相關係數必須介於 -1 與 1 之間
  const wtTotal = weights.reduce((a,b)=>a+b,0);
  if (Math.abs(wtTotal - 1) > 0.001) { /* 顯示錯誤 */ return; }

  const portReturn = weights.reduce((sum, w, i) => sum + w * returns[i], 0);
  const portVol = calculatePortfolioVolatility(weights, sigmas, cors);
  // ...寫入 DOM...
}

09完整金融平台整合架構

全書 12 章完整成果對照

章節主要內容核心成果
第一~二章HTML/CSS/JavaScript + 複利計算器前台基礎與互動設計
第三章JSON/Markdown 與金融資料管理資料結構與資料庫設計
第四~五章Python/Pandas + Chart.js/Plotly資料分析與視覺化
第六~七章GitHub/Cloudflare + UI/RWD/SEO部署流程與可用性最佳化
第八章Streamlit 管理後台內容 CRUD 與 GitHub API 發布
第九~十二章債券/股票選擇權/退休/投資組合五大金融專業分析模組
程式區塊完整 financial-platform/ 資料夾結構
financial-platform/
├─ index.html                  ← 首頁(定位/工具入口/熱門文章)
├─ tools.html                  ← 金融工具中心
├─ bond-calculator.html        ← 債券評價(第九章)
├─ stock-analysis.html         ← 股票與基本面(第十章)
├─ option-payoff.html          ← 選擇權損益(第十章)
├─ retirement-calculator.html  ← 退休規劃(第十一章)
├─ portfolio-calculator.html   ← 投資組合風險(第十二章)
├─ articles.html / exam.html / books.html / about.html
├─ robots.txt / sitemap.xml
├─ css/  main.css / navigation.css / tools.css / responsive.css
├─ js/   navigation.js / bond-calculator.js / option-payoff.js
│        retirement-calculator.js / portfolio-calculator.js / analytics.js
├─ data/ articles.json / bonds.json / questions.json / backups/
├─ images/ / output/(Plotly HTML)
├─ python/ bond_analysis.py / stock_option_analysis.py
│          retirement_analysis.py / portfolio_analysis.py
│          bond_app.py / stock_option_app.py / retirement_app.py / portfolio_app.py
├─ admin/ app.py               ← 第八章 Streamlit 後台
├─ .streamlit/ secrets.toml    ← 機密(加入 .gitignore)
└─ .gitignore / README.md / requirements.txt
管理者Streamlit 後台 CRUD
GitHub版本控制
Cloudflare Pages自動部署
使用者閱讀/測驗/使用工具

10整站發布流程與全站檢查表

# 步驟 1:本機功能驗證
python -m http.server 8000

# 步驟 2:安全確認
git grep -i "token"      # 確認無 Token 外洩
git grep -i "secret"     # 確認無機密外洩
git status                # 確認 secrets.toml 不在待提交清單

# 步驟 3:提交與推送
git add .
git commit -m "完成投資組合工具與全站整合"
git push

# 步驟 4:確認 Cloudflare 部署成功
# 步驟 5:全站功能測試(逐一測試每個金融工具)

# 步驟 6:建立正式版本標籤
git tag -a v1.0 -m "金融分析平台完整版本"
git push origin v1.0
類別檢查項目
① 程式功能複利/債券/股票/選擇權/退休/組合計算正確、CSV可上傳、圖表正常、F12無錯誤
② 介面品質桌面/平板/手機均正常、導覽清楚、按鈕高度≥44px、表單標籤清楚
③ 資安確認無Token/密碼/API金鑰、secrets.toml在.gitignore、HTTPS正常
④ 部署完成GitHub最新commit正確、Cloudflare部署成功、圖片/JSON無404、git tag v1.0已建立

11實務案例

(一)單一產業集中風險

投資人持有 80% 的科技股(15 檔),以為充分分散。但當科技股因估值修正、利率上升或監管收緊而同步下跌時,這 15 檔股票的相關係數大幅上升,分散效果大幅降低。分散應跨越產業、地區、資產類別,而非只是增加股票數量。

(二)危機期間相關係數上升

平時股票(σ=20%)、公司債(σ=6%)與 REITs(σ=15%)的相關係數分別為 0.3、0.1 和 0.4。金融危機時投資人同步拋售風險資產,相關係數可能升至 0.7–0.9,歷史計算的分散效果大幅失效。

(三)VaR 低估尾端損失

某投資組合一日 95% VaR = 50 萬元,管理者誤以為最大損失只有 50 萬元。實際上,VaR 只代表在 95% 的交易日損失不超過 50 萬元。剩下的 5% 交易日,損失可能是 100 萬、300 萬甚至更多。VaR 不是最大損失,必須搭配 ES 和壓力測試。

(五)Python 工具與靜態前台的正確分工

Cloudflare Pages 部署的是靜態檔案,不會直接執行 Python。正確分工:靜態前台使用 JavaScript 計算(複利、退休試算、組合報酬/波動度估算);Python 用於本機深度分析(VaR、ES、相關係數矩陣),透過 Streamlit 提供互動介面。

12常見錯誤與除錯

錯誤症狀正確做法
權重未除以 100輸入 60(應為 0.60),計算結果嚴重失真weights = input% / 100;驗證 Σw = 1
相關係數超出 [-1,1]程式接受 1.5 的相關係數,結果無意義validate_correlation() 檢查;超出範圍拒絕
矩陣法出現負變異數浮點誤差造成 σ² = −0.000001portfolio_volatility = √max(σ², 0)
VaR 解讀為最大損失「VaR 50 萬 = 最大損失 50 萬」(錯誤)VaR 是門檻,搭配 ES 評估尾端平均損失
前台 JS 與後台 Python 公式不一致相同輸入得到不同答案建立統一公式文件,加入測試案例
Token 寫入程式碼app.py 中有明文 Token 被 push 至 GitHub所有機密使用 secrets.toml,加入 .gitignore

13模型治理、資料隱私與倫理原則

原則說明實作方式
模型透明清楚說明使用何種公式、假設與限制每個工具附上公式說明與模型假設區塊
避免保證性文字不使用「一定獲利」「保證退休」等文字工具下方加免責聲明,並避免情緒化行銷詞
資料最小收集不收集超出計算需要的個人資料GA4 只傳送匿名化事件,不傳送精確金額
隱私保護不任意收集姓名、身分證、銀行帳號明確的隱私政策;計算在瀏覽器端本地執行

14金融平台的後續營運方向

營運面向核心行動
內容營運定期更新文章、案例、過時資料;建立主題系列;工具連結教材、教材連結題庫
考照營運提供免費試題、分章練習、解析說明、統計常錯題目、付費完整題庫
工具優化追蹤工具使用次數與錯誤輸入;改善操作流程;加入報告下載;定期驗證公式正確性
出版與課程免費文章 → 互動工具 → 題庫 → PDF 教材 → 課程或書籍;核心仍是提供真實學習價值

15關鍵字

投資組合資產配置權重 期望報酬變異數共變異數 相關係數共變異數矩陣分散風險 系統性風險非系統性風險夏普比率 Sortino 比率下行偏差最大回撤 VaR歷史法 VaR參數法 VaR Expected Shortfall尾端風險壓力測試 情境分析再平衡矩陣運算 NumPy完整整合模型治理 資料隱私免責聲明

16實作練習

(一)基礎操作
  • 建立三項資產(股票/債券/現金)的 portfolio-returns.csv 示範資料
  • 建立共變異數矩陣與相關係數矩陣,確認對角線為 1
  • 使用矩陣公式計算平衡型(60/30/10)的年化波動度
  • 計算夏普比率、Sortino 比率(目標 0%)
  • 計算 95% 歷史法 VaR 與 Expected Shortfall
  • 設定壓力情境(股票 −25%、債券 −8%),計算組合損益
  • 比較積極型/平衡型/保守型三種配置的風險報酬
(二)完整整合挑戰
  • 確認 tools.html 六個工具卡片連結全部有效
  • 確認 sitemap.xml 涵蓋所有新增的工具頁面
  • 執行全站功能測試清單(四大類 checkbox)
  • 建立 git tag v1.0,推送至 GitHub,確認 Cloudflare 自動重新部署
  • 在 GA4 確認各工具的事件追蹤
(三)進階挑戰
  • 建立隨機權重模擬(蒙地卡羅)來視覺化效率前緣
  • 建立最小變異數投資組合(scipy.optimize.minimize)
  • 建立滾動相關係數圖(觀察危機前後的變化)
  • 整合退休規劃與投資組合分析(退休資產的波動度 vs 提領需求)

JavaScript 前台三資產配置工具是純前端計算,可以在瀏覽器中真的執行,所以本章挑戰任務(04 分頁)延續即時執行模式:修改真正會執行的程式碼,系統會即時告訴你哪裡出錯。

本章結語——本章完成了全書最後一套金融專業工具:投資組合報酬與風險分析器。讀者已學會資產配置與權重驗證、期望報酬計算、共變異數矩陣、相關係數矩陣、矩陣公式計算組合波動度、分散風險原理、夏普比率、Sortino 比率、最大回撤、歷史法 VaR、參數法 VaR、Expected Shortfall、壓力測試、多配置情境比較與再平衡計算。

全書從第一章的簡單複利計算器出發,逐步完成:HTML/CSS/JavaScript 前端設計、JSON/Markdown 資料管理、Python/Pandas 分析、Chart.js/Plotly 視覺化、GitHub 版本控制、Cloudflare Pages 部署、UI/RWD/SEO 最佳化、Streamlit 管理後台,以及債券、股票、選擇權、退休規劃與投資組合五大金融工具模組。

這套架構的真正價值,不只是完成一個網站,而是建立一套能將金融知識轉化為互動工具、教材、題庫與服務的完整方法。金融程式設計的核心不是讓程式取代金融判斷,而是讓公式更容易驗證、讓風險更容易被看見、讓使用者能調整假設,並理解每一個結果背後的條件與限制。

完成全書後,讀者已具備建立金融知識平台的完整基礎。後續可依實際需求,進一步發展會員系統、付費題庫、自動報告、資料庫整合、API 設計、AI 金融助理、投資組合最佳化(效率前緣)與個人化學習功能。感謝完成這十二章的學習旅程。

三資產配置與波動度試算(教材原始範例)

輸入股票、債券、現金的權重、預期報酬、波動度與兩兩相關係數,點擊按鈕即時計算投資組合的預期報酬與波動度(共變異數矩陣公式)。這就是課本第八節的 portfolio-calculator.html/portfolio-calculator.js 完全對應的成品,等一下的挑戰任務就是在這份程式碼上動手修改。

  portfolio-calculator.html — 在瀏覽器中執行

觀念測驗

共 15 題選擇題,涵蓋本章所有核心觀念。每題作答後會立即顯示解析。

0 / 15 已作答

挑戰任務:修改程式碼,讓工具自己告訴你哪裡錯

投資組合計算器是純前端 JavaScript,可以在瀏覽器中真的執行。下面三個練習都是「真的會執行」的程式碼,修改完成後按下「執行測試」,系統會把你的程式碼放進真實瀏覽器環境中運行,並用白話文告訴你哪裡不對。

任務一:功能修改

對應教材「十六、實作練習」

難度:入門
  • 在結果區新增顯示「加權平均波動度」(不考慮相關係數的簡單加權平均)
js/portfolio-calculator.js — 可直接編輯
提示(卡住了再打開)
handlePortfolioCalculation() 函式最後(resEl.classList.remove("hidden") 之前),加入一行把計算結果寫進指定的空欄位。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己動手做一次,或按「換一題再練習」挑戰另一個類似的任務。

任務二:抓出隱藏的 id 錯誤

對應教材「十二、常見錯誤與除錯」

難度:除錯
  • 這份程式碼交接自另一位同學,執行後完全沒有反應(或沒有正確顯示結果)
  • 找出問題並修正,讓計算器恢復正常運作
js/portfolio-calculator.js — 藏有 1 個 bug
提示(卡住了再打開)
F12 → Console 如果出現「Cannot read properties of null」,代表 document.getElementById("...") 抓到的是 null。逐一比對 HTML 裡每個 id="..." 跟 JS 裡 getElementById("...") 的拼字是否一模一樣。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一個 id 錯誤。

任務三:權重/波動度換算錯誤

對應教材「十二、常見錯誤與除錯」— 權重未除以100 / 波動度未除以100

難度:除錯
  • 這份程式碼的問題不太一樣:一種會讓工具完全跑不出結果,一種會跑出不合理的數字
  • 正確的投資組合波動度應約為 12.49%
  • 找出問題所在並修正
js/portfolio-calculator.js — 換算錯誤
提示(卡住了再打開)
回頭檢查每一個百分比欄位(權重、波動度)有沒有正確除以 100 轉換成小數。有一個變體會被工具自己的驗證機制擋下來(完全沒有結果),另一個則會算出明顯離譜的數字。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一種換算錯誤。