01股票報酬率的基本概念
第九章完成債券定價、YTM、存續期間與凸性分析。本章進入股票與選擇權工具的整合實作。股票與債券的現金流特性不同:債券有較明確的票息與到期本金;普通股沒有固定到期日,股利也不一定固定。股票價格受企業獲利、產業、利率、景氣、市場情緒等多重因素影響,分析不能只看目前股價,也不能只依賴單一指標。
| 指標 | 公式 | 注意事項 |
|---|---|---|
| 價格報酬率 | Rₜ = Pₜ/Pₜ₋₁ − 1 | 未含股利,長期可能低估總報酬 |
| 總報酬率 | Rₜ = (Pₜ − Pₜ₋₁ + Dₜ)/Pₜ₋₁ | 含股利的完整單期報酬 |
| 對數報酬率 | rₜ = ln(Pₜ/Pₜ₋₁) | 可加總,常用於統計模型 |
| 累積報酬率 | (1+R₁)(1+R₂)...(1+Rₙ) − 1 | 多期連乘,不可直接相加(10%+(−10%)≠0%,正確答案是−1%) |
| 年化幾何報酬 | (1+R_total)^(1/T) − 1 | 複合年均成長率(CAGR),T需足夠長期才有意義 |
| 算術平均報酬 | ΣRₜ/n | 永遠 ≥ 幾何平均(Jensen 不等式) |
若股票發放股利、股票股利或進行股票分割,未調整收盤價可能出現看似大跌的缺口,導致報酬率計算嚴重失真。長期報酬分析應使用「調整後收盤價(adjusted_close)」,並在報告中標明使用的是價格報酬還是總報酬。
02股票風險指標
波動度(年化)——日波動度為每日報酬率的樣本標準差(ddof=1)。乘以 √252 後得到年化波動度。波動度是對稱指標,上漲與下跌的波動都被計入,不區分方向。
最大回撤——衡量資產由歷史最高點下跌至後續最低點的最大跌幅。例如資產由 120 萬元跌至 84 萬元:最大回撤 = 84/120 − 1 = −30%。
最大回撤通常為負值。若程式顯示正值(如 +25%),需檢查符號:公式是 V_t/peak_t − 1,不是 peak_t/V_t − 1。
夏普比率的限制
| 限制 | 說明 |
|---|---|
| 上漲/下跌均視為風險 | 用標準差衡量,不區分上漲波動(對投資人有利)與下跌波動 |
| 對非常態報酬可能失真 | 尖峰厚尾的報酬分布,標準差可能低估真實風險 |
| 過去不代表未來 | 高夏普比率的歷史策略,未來不一定維持 |
高勝率不代表高總報酬。若 90% 的交易小幅獲利 1%,但 10% 的交易虧損 15%,期望報酬 ≈ 0.9×1% + 0.1×(−15%) = −0.6%,仍為負報酬。
03股票基本面指標
| 指標 | 公式 | 單一指標的限制 |
|---|---|---|
| EPS 每股盈餘 | (淨利 − 特別股股利) ÷ 加權平均股數 | ⚠️ EPS 為負時,P/E 無估值意義 |
| P/E 本益比 | 每股市價 ÷ EPS | ⚠️ 景氣高峰 EPS 偏高,P/E 看似低但可能是陷阱 |
| P/B 股價淨值比 | 每股市價 ÷ 每股淨值 | ⚠️ 需考慮資產品質、減損與無形資產 |
| 股利殖利率 | 每股現金股利 ÷ 每股市價 | ⚠️ 高殖利率可能反映股價大跌,而非高品質 |
| ROE 股東權益報酬率 | 稅後淨利 ÷ 平均股東權益 | ⚠️ 高 ROE 可能來自高槓桿,非真實競爭力 |
| FCF 自由現金流 | 營業活動現金流 − 資本支出 | ⚠️ 淨利≠現金;長期 FCF 偏低需深入分析 |
四個指標的交叉判讀陷阱
| 常見誤解 | 正確思維 |
|---|---|
| 低 P/E 一定便宜 | 景氣循環高峰時 EPS 偏高,P/E 看似低,但未來 EPS 可能大跌 |
| 高 ROE 一定高品質 | 大量舉債可使股東權益縮小,ROE 提高,需同時檢視負債比率 |
| 估值倍數跨產業比較 | 科技、銀行、製造業資產結構不同,同一倍數無法直接比較 |
04技術分析與移動平均
簡單移動平均(SMA)例如五日收盤價為 100、102、101、104、108,五日均線 = (100+102+101+104+108)/5 = 103。
| 訊號 | 條件 | 意義 | 重要限制 |
|---|---|---|---|
| 黃金交叉 | 短期均線由下向上穿越長期均線 | 可能代表短期趨勢轉強 | 落後指標,可能是事後確認,不保證上漲 |
| 死亡交叉 | 短期均線由上向下穿越長期均線 | 可能代表短期趨勢轉弱 | 盤整市場會產生頻繁假訊號 |
均線是落後指標(Lagging Indicator),使用歷史資料計算,無法預測未來。盤整市場中若依每次交叉交易,可能頻繁買賣並因手續費、滑價虧損。
05Python 股票分析模組
import numpy as np
import pandas as pd
def calculate_simple_returns(prices: pd.Series) -> pd.Series:
return prices.pct_change(fill_method=None)
def calculate_log_returns(prices: pd.Series) -> pd.Series:
return np.log(prices / prices.shift(1))
def calculate_cumulative_return(returns: pd.Series) -> float:
valid = returns.dropna()
return float((1 + valid).prod() - 1) # 連乘,不可用 sum()
def calculate_annualized_return(cumulative_return: float, years: float) -> float:
return (1 + cumulative_return) ** (1 / years) - 1
def calculate_volatility(returns: pd.Series, trading_days: int = 252) -> float:
return float(returns.dropna().std(ddof=1) * np.sqrt(trading_days))
def calculate_max_drawdown(prices: pd.Series) -> float:
running_max = prices.cummax()
drawdown = prices / running_max - 1 # 注意:現值 ÷ 峰值 − 1(不是反過來)
return float(drawdown.min())
def calculate_sharpe_ratio(returns: pd.Series, risk_free_rate: float, trading_days: int = 252) -> float:
excess_return = returns.dropna().mean() * trading_days - risk_free_rate
volatility = calculate_volatility(returns, trading_days)
return excess_return / volatility if volatility != 0 else float('nan')
from dataclasses import dataclass
@dataclass(frozen=True)
class FundamentalInput:
stock_price: float
net_income: float
shares_outstanding: float
shareholders_equity: float
dividend_per_share: float = 0.0
operating_cash_flow: float = 0.0
capital_expenditure: float = 0.0
def calculate_eps(fund: FundamentalInput) -> float:
return fund.net_income / fund.shares_outstanding
def calculate_pe_ratio(fund: FundamentalInput) -> float | None:
eps = calculate_eps(fund)
if eps <= 0: return None # EPS 不為正時,P/E 無估值意義
return fund.stock_price / eps
def calculate_roe(fund: FundamentalInput) -> float:
return fund.net_income / fund.shareholders_equity
def calculate_free_cash_flow(fund: FundamentalInput) -> float:
return fund.operating_cash_flow - fund.capital_expenditure
06選擇權的基本概念
| 類型 | 賦予買方的權利 | 到期內含價值公式 |
|---|---|---|
| 買權(Call) | 以履約價格 K 買入標的的權利 | max(S_T − K, 0) |
| 賣權(Put) | 以履約價格 K 賣出標的的權利 | max(K − S_T, 0) |
| 術語 | 說明 |
|---|---|
| 履約價格 K | 選擇權契約規定的買賣價格,固定,由契約決定 |
| 權利金 | 買方支付的選擇權成本 |
| 內含價值 | 立即履約可獲得的價值 |
| 損益兩平點 | 買權買方:K+Premium;賣權買方:K−Premium |
四種部位到期損益
| 部位 | 最大損失 | 最大獲利 | 損益兩平點 |
|---|---|---|---|
| 買權買方(Long Call) | −Premium(有限損失) | 無固定上限(無限獲利) | K + Premium |
| 買權賣方(Short Call) | 無固定上限(無限損失) | Premium(有限獲利) | K + Premium |
| 賣權買方(Long Put) | −Premium(有限損失) | K − Premium(當 S_T=0) | K − Premium |
| 賣權賣方(Short Put) | K − Premium(S_T=0 時最慘) | Premium(有限獲利) | K − Premium |
賣出裸買權(Short Naked Call)的理論損失沒有上限,因為標的價格可以無限上漲。網站工具若提供賣方損益圖,必須加入明確風險警示。
07Black-Scholes 選擇權定價模型
| 符號 | 意義 | 程式轉換 |
|---|---|---|
| S | 標的目前市場價格 | 直接輸入 |
| K | 履約價格 | 直接輸入 |
| r | 連續複利無風險利率 | r = 輸入% / 100 |
| σ | 年化波動度 | σ = 輸入% / 100(如 25% → 0.25) |
| T | 到期年數(剩餘) | T = 月數 / 12 或天數 / 365 |
| N(·) | 標準常態累積分配函數 | Python: math.erf(x/√2)/2 + 0.5 |
八項模型假設(初學者必知)
- 標的服從幾何布朗運動——市場可能有跳躍、極端事件(黑天鵝)
- 波動度固定——隱含波動度依履約價格和期間不同(波動度微笑/偏斜)
- 選擇權為歐式——美式選擇權有提前履約價值,需其他模型
- 股利為連續固定率——實際股利為離散現金發放
Black-Scholes 計算的是「理論公允價值」,不是市場成交價。市場價格受供需、隱含波動度變化、流動性等影響,可能與理論價格存在差距。
08希臘字母(Greeks)——選擇權敏感度指標
| Greek | 衡量對象 | 買權方向 | 賣權方向 |
|---|---|---|---|
| Delta Δ | 選擇權價格對標的價格的敏感度 | 0 到 1 之間 | −1 到 0 之間 |
| Gamma Γ | Delta 對標的價格的變化速度 | 正值 | 正值(與買權相同) |
| Theta Θ | 時間流逝對選擇權價值的影響 | 通常為負值 | 複雜(視條件) |
| Vega ν | 波動度變化對選擇權價值的影響 | 通常為正值 | 通常為正值 |
買賣權平價(Put-Call Parity)可用來驗算計算程式的一致性:若左側等於右側,表示買權與賣權計算正確。差值接近零(如 < 0.001)表示無數值誤差。
09Python 選擇權分析函數
from dataclasses import dataclass
import math
@dataclass(frozen=True)
class OptionInput:
spot_price: float # 標的目前價格 S
strike_price: float # 履約價格 K
time_to_maturity: float # 到期年數 T(3個月 = 0.25)
risk_free_rate: float # 無風險利率 r(小數,2% → 0.02)
volatility: float # 年化波動度 σ(小數,25% → 0.25)
dividend_yield: float = 0.0
def normal_cdf(x: float) -> float: # N(x):標準常態累積分配
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def calculate_d1_d2(opt: OptionInput) -> tuple[float, float]:
numerator = (math.log(opt.spot_price / opt.strike_price) +
(opt.risk_free_rate - opt.dividend_yield + 0.5 * opt.volatility**2)
* opt.time_to_maturity)
denom = opt.volatility * math.sqrt(opt.time_to_maturity)
d1 = numerator / denom
d2 = d1 - opt.volatility * math.sqrt(opt.time_to_maturity)
return d1, d2
def black_scholes_call(opt: OptionInput) -> float:
d1, d2 = calculate_d1_d2(opt)
disc_s = opt.spot_price * math.exp(-opt.dividend_yield * opt.time_to_maturity)
disc_k = opt.strike_price * math.exp(-opt.risk_free_rate * opt.time_to_maturity)
return disc_s * normal_cdf(d1) - disc_k * normal_cdf(d2)
def black_scholes_put(opt: OptionInput) -> float:
d1, d2 = calculate_d1_d2(opt)
disc_s = opt.spot_price * math.exp(-opt.dividend_yield * opt.time_to_maturity)
disc_k = opt.strike_price * math.exp(-opt.risk_free_rate * opt.time_to_maturity)
return disc_k * normal_cdf(-d2) - disc_s * normal_cdf(-d1)
def check_put_call_parity(opt: OptionInput) -> dict:
call = black_scholes_call(opt)
put = black_scholes_put(opt)
disc_k = opt.strike_price * math.exp(-opt.risk_free_rate * opt.time_to_maturity)
disc_s = opt.spot_price * math.exp(-opt.dividend_yield * opt.time_to_maturity)
left = call + disc_k # C + Ke^(−rT)
right = put + disc_s # P + Se^(−qT)
return {'call': call, 'put': put, 'difference': left - right}
def calculate_option_greeks(opt: OptionInput) -> dict:
d1, d2 = calculate_d1_d2(opt)
disc_div = math.exp(-opt.dividend_yield * opt.time_to_maturity)
sqrt_T = math.sqrt(opt.time_to_maturity)
call_delta = disc_div * normal_cdf(d1) # 介於 0~1
put_delta = disc_div * (normal_cdf(d1) - 1) # 介於 −1~0
gamma = disc_div * normal_pdf(d1) / (opt.spot_price * opt.volatility * sqrt_T)
vega_per_pct = opt.spot_price * disc_div * normal_pdf(d1) * sqrt_T / 100
return {
'call_delta': call_delta, 'put_delta': put_delta,
'gamma': gamma, 'vega_per_percent': vega_per_pct,
}
10Streamlit 整合工具(三分頁架構)
建立 stock_option_app.py,使用 st.tabs 分為「股票分析」「基本面」「選擇權」三個分頁,共用同一個上傳的 CSV 資料或手動輸入的參數。
import streamlit as st
st.set_page_config(page_title='股票與選擇權分析', page_icon='📊', layout='wide')
st.title('股票報酬、基本面與選擇權分析')
tab1, tab2, tab3 = st.tabs(['股票報酬與風險', '基本面指標', '選擇權分析'])
with tab1:
uploaded = st.file_uploader('上傳股價 CSV(含 adjusted_close 欄位)', type=['csv'])
# ... 讀取、清理、計算報酬率與風險指標 ...
with tab2:
stock_price = st.number_input('股價', value=120.0)
net_income = st.number_input('淨利(億元)', value=8.0)
# ... 計算 EPS、P/E、ROE、FCF ...
with tab3:
option_type = st.radio('選擇權類型', ['買權', '賣權'])
# ... Black-Scholes 定價與 Greeks ...
11JavaScript 前台選擇權損益計算器
前台版本計算到期損益(不含 Black-Scholes),適合直接整合至 Cloudflare Pages 靜態網站。完整程式碼可以到「示範分頁」看實際運作結果,也是本章挑戰任務要動手修改的對象。
function calculateIntrinsicValue(optionType, terminalPrice, strikePrice) {
if (optionType === 'call') return Math.max(terminalPrice - strikePrice, 0);
return Math.max(strikePrice - terminalPrice, 0); // put
}
function calculateBreakEven(optionType, strikePrice, premium) {
return optionType === 'call' ? strikePrice + premium : strikePrice - premium;
}
function handleOptionPayoff() {
const optionType = document.getElementById('optionType').value;
const position = document.getElementById('optionPosition').value;
const strikePrice = Number(document.getElementById('strikePrice').value);
const premium = Number(document.getElementById('optionPremium').value);
const terminalP = Number(document.getElementById('terminalPrice').value);
const multiplier = Number(document.getElementById('contractMultiplier').value);
// ...輸入驗證...
const intrinsic = calculateIntrinsicValue(optionType, terminalP, strikePrice);
const longProfit = intrinsic - premium;
const unitProfit = position === 'long' ? longProfit : -longProfit; // 賣方反向
const totalProfit = unitProfit * multiplier;
const breakEven = calculateBreakEven(optionType, strikePrice, premium);
document.getElementById('intrinsicValue').textContent = intrinsic.toFixed(2);
document.getElementById('unitProfit').textContent = unitProfit.toFixed(2);
document.getElementById('contractProfit').textContent = totalProfit.toFixed(2);
document.getElementById('breakEvenPrice').textContent = breakEven.toFixed(2);
}
document.getElementById('calculateOptionPayoff').addEventListener('click', handleOptionPayoff);
12操作實務與測試案例
| 測試 | 輸入條件 | 預期結果 |
|---|---|---|
| 股票總報酬 | 期初 100、期末 120、兩年 | 總報酬 20%;年化 ≈ 9.54% |
| 累積報酬 | 第一期 +10%,第二期 −10% | 累積 = −1%(非 0%) |
| 最大回撤 | 路徑:100→120→110→90→105 | 最大回撤 = −25%(90/120−1) |
| 買權損益 | K=100、Premium=5、S_T=120 | 內含=20;買方損益=15;兩平=105 |
| 賣權損益 | K=100、Premium=4、S_T=80 | 內含=20;買方損益=16;兩平=96 |
| 買賣權平價 | 任意合法 OptionInput | 平價關係誤差接近 0(< 0.001) |
13實務案例
(一)高報酬但高回撤
甲股票三年總報酬 60%,但最大回撤 −45%;乙股票三年總報酬 40%,最大回撤 −15%。只看總報酬會偏好甲,但風險承受度較低的投資人可能無法承受 45% 的帳面損失。績效分析應同時呈現總報酬、年化報酬、波動度、最大回撤、夏普比率與投資期間。
(三)低 P/E 陷阱(景氣循環股)
景氣高峰時某企業 EPS 大幅增加,股價尚未上升,P/E 看似很低。但若未來商品價格下跌,EPS 可能快速縮減,原本的低 P/E 不代表便宜。景氣循環股的估值應觀察週期均化盈餘,而非當期 EPS。
(六)買權價格上升不等於標的上漲
投資人買入買權後,標的小幅上漲,但選擇權價格反而下降。可能原因:隱含波動度大幅下降(Vega 效應蓋過 Delta)、時間價值衰減(Theta 侵蝕)、標的上漲幅度不足。選擇權價格由多個因素共同決定,不只是標的方向。
(七)波動度微笑(Black-Scholes 的限制)
Black-Scholes 假設所有履約價格使用相同固定波動度,但市場中不同履約價格的隱含波動度常不相同,形成「波動度微笑」或「偏斜」。使用歷史固定波動度計算的理論價,可能與市場成交價存在系統性差距。
14常見錯誤與除錯
| 錯誤類型 | 症狀 | 正確做法 |
|---|---|---|
| 使用未調整收盤價 | 除權息被誤判為大跌 | 使用 adjusted_close 或另行加回股利 |
| 直接相加多期報酬 | (10%+(−10%)=0%) 錯誤 | 連乘:(1.10)(0.90)−1=−1% |
| 最大回撤方向錯誤 | 顯示正 25%(應為 −25%) | drawdown = wealth/peak − 1 |
| 負 EPS 計算 P/E | 顯示 −15 倍,無估值意義 | EPS≤0 時回傳 NaN 或顯示「不適用」 |
| 波動度輸入 25 未除 100 | BS 理論價嚴重失真(σ=25 當成 2500%) | σ = volatility_input / 100 |
| 到期期間單位錯誤 | 三個月輸入 3(應為 0.25) | T = months/12 或 T = days/365 |
15模型限制與風險揭露
股票報酬與基本面分析受資料期間、會計政策、一次性損益、除權息處理、產業差異、市場情緒等因素影響。過去績效不代表未來結果。技術指標來自歷史價格,不能保證預測未來。
Black-Scholes 限制——波動度非固定(波動度微笑/偏斜);市場可能出現價格跳躍(尾部風險);美式選擇權有提前履約價值,需二項樹等其他模型;股利為離散現金,非連續固定率。
網站工具應顯示:「本工具依歷史資料、公開財務數據及簡化模型進行分析。股票價格、盈餘、波動度與選擇權價值均可能快速變化。計算結果不構成投資建議、交易訊號或報酬保證。」
16關鍵字
17實作練習
- 讀取股價 CSV,完成清理並計算每日簡單報酬率
- 計算累積報酬率,手動驗算連乘而非相加
- 計算年化波動度與最大回撤(確認符號為負)
- 建立 5 日與 20 日移動平均,觀察是否出現黃金交叉
- 計算買權與賣權各四種部位損益,驗算損益兩平點
- 計算 Black-Scholes 買賣權理論價格,驗證買賣權平價誤差接近 0
- 計算 Delta、Gamma、Theta、Vega,確認各值在合理範圍
- 使用二分法反推隱含波動度(輸入市場價格,求解 σ)
- 建立履約價格情境表(K 從 80 到 120,步距 5)
- 建立跨式策略(Long Straddle:同時買入同履約價買權與賣權)
- 建立牛市價差(Bull Spread:買低履約買權 + 賣高履約買權)
- 在 JavaScript 前台加入簡單的 Black-Scholes 買權定價(不含 Greeks)
股票報酬/基本面分析與 Black-Scholes/Greeks 是 Python 模組,教材導讀已完整呈現。JavaScript 前台的「選擇權到期損益計算器」是純前端計算,可以在瀏覽器裡真的執行,所以本章挑戰任務(04 分頁)聚焦在這個部分,延續即時執行模式。
本章結語——本章完成了股票與選擇權的整合分析工具。在股票績效方面,建立了涵蓋簡單/對數報酬率、累積報酬、年化報酬、波動度、最大回撤、夏普比率與勝率的完整分析模組;在基本面方面,計算 EPS、P/E、P/B、ROE、股利殖利率、自由現金流,並說明各指標單獨判讀的陷阱;在技術分析方面,建立了移動平均與交叉訊號。
選擇權部分從最基礎的買權/賣權定義,到到期損益計算、損益兩平點、Black-Scholes 理論定價、買賣權平價驗證,以及 Delta/Gamma/Theta/Vega 希臘字母,所有計算同時提供 Python 模組版、Streamlit 互動工具與 JavaScript 前台版。
下一章進入複利、年金與退休規劃工具。讀者將建立定期定額、普通年金、期初年金、退休資金缺口、通膨調整、退休提領與資金耗盡情境分析,並整合至金融知識網站的退休規劃專區。
—選擇權到期損益計算器(教材原始範例)
選擇買權或賣權、買方或賣方、輸入履約價格、權利金與到期標的價格,點擊按鈕即時計算到期內含價值、每單位損益、契約損益與損益兩平點。這就是課本第十一節的 option-calculator.html/option-calculator.js 完全對應的成品,等一下的挑戰任務就是在這份程式碼上動手修改。
—觀念測驗
共 15 題選擇題,涵蓋本章所有核心觀念。每題作答後會立即顯示解析。
挑戰任務:修改程式碼,讓工具自己告訴你哪裡錯
選擇權損益計算器是純前端 JavaScript,可以在瀏覽器中真的執行。下面三個練習都是「真的會執行」的程式碼,修改完成後按下「執行測試」,系統會把你的程式碼放進真實瀏覽器環境中運行,並用白話文告訴你哪裡不對。
任務一:功能修改
對應教材「十七、實作練習」
- 在結果區新增顯示「權利金總額」(權利金 × 契約乘數)
提示(卡住了再打開)
handleOptionPayoff() 函式最後(resEl.classList.remove("hidden") 之前),加入一行把計算結果寫進指定的空欄位。
💡 查看範例解答(建議自己先試過再打開)
看完解答後,可以按「還原初始程式碼」重新自己動手做一次,或按「換一題再練習」挑戰另一個類似的任務。
任務二:抓出隱藏的 id 錯誤
對應教材「十四、常見錯誤與除錯」
- 這份程式碼交接自另一位同學,執行後完全沒有反應(或沒有正確顯示結果)
- 找出問題並修正,讓計算器恢復正常運作
提示(卡住了再打開)
document.getElementById("...") 抓到的是 null。逐一比對 HTML 裡每個 id="..." 跟 JS 裡 getElementById("...") 的拼字是否一模一樣。
💡 查看範例解答(建議自己先試過再打開)
看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一個 id 錯誤。
任務三:邏輯錯誤(賣方方向 / 內含價值公式)
對應教材「六、選擇權的基本概念」— 買賣雙方零和關係
- 這份程式碼可以正常執行、不會報錯,但算出來的數字不合理
- 找出問題所在並修正
提示(卡住了再打開)
💡 查看範例解答(建議自己先試過再打開)
看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一種邏輯錯誤。