CH.10 · 財金程式設計互動教材

股票報酬、基本面與選擇權分析工具

從報酬率、風險指標、基本面估值到選擇權到期損益與 Black-Scholes 定價——把股票與衍生性商品的核心分析轉化為可操作的工具。

max(S−K, 0) − Premium

本章學習目標

  • 計算累積報酬率、年化波動度、最大回撤與夏普比率
  • 計算 EPS、P/E、ROE、自由現金流等基本面指標
  • 計算選擇權到期損益、損益兩平點與四種部位風險
  • 理解 Black-Scholes 定價與 Delta/Gamma/Theta/Vega

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 不等式)
i

若股票發放股利、股票股利或進行股票分割,未調整收盤價可能出現看似大跌的缺口,導致報酬率計算嚴重失真。長期報酬分析應使用「調整後收盤價(adjusted_close)」,並在報告中標明使用的是價格報酬還是總報酬。

02股票風險指標

波動度(年化)——日波動度為每日報酬率的樣本標準差(ddof=1)。乘以 √252 後得到年化波動度。波動度是對稱指標,上漲與下跌的波動都被計入,不區分方向。

最大回撤——衡量資產由歷史最高點下跌至後續最低點的最大跌幅。例如資產由 120 萬元跌至 84 萬元:最大回撤 = 84/120 − 1 = −30%。

!

最大回撤通常為負值。若程式顯示正值(如 +25%),需檢查符號:公式是 V_t/peak_t − 1,不是 peak_t/V_t − 1。

夏普比率的限制

限制說明
上漲/下跌均視為風險用標準差衡量,不區分上漲波動(對投資人有利)與下跌波動
對非常態報酬可能失真尖峰厚尾的報酬分布,標準差可能低估真實風險
過去不代表未來高夏普比率的歷史策略,未來不一定維持
i

高勝率不代表高總報酬。若 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。

訊號條件意義重要限制
黃金交叉短期均線由下向上穿越長期均線可能代表短期趨勢轉強落後指標,可能是事後確認,不保證上漲
死亡交叉短期均線由上向下穿越長期均線可能代表短期趨勢轉弱盤整市場會產生頻繁假訊號
i

均線是落後指標(Lagging Indicator),使用歷史資料計算,無法預測未來。盤整市場中若依每次交叉交易,可能頻繁買賣並因手續費、滑價虧損。

05Python 股票分析模組

程式區塊python/stock_option_analysis.py(報酬率核心函數)
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

八項模型假設(初學者必知)

i

Black-Scholes 計算的是「理論公允價值」,不是市場成交價。市場價格受供需、隱含波動度變化、流動性等影響,可能與理論價格存在差距。

08希臘字母(Greeks)——選擇權敏感度指標

Greek衡量對象買權方向賣權方向
Delta Δ選擇權價格對標的價格的敏感度0 到 1 之間−1 到 0 之間
Gamma ΓDelta 對標的價格的變化速度正值正值(與買權相同)
Theta Θ時間流逝對選擇權價值的影響通常為負值複雜(視條件)
Vega ν波動度變化對選擇權價值的影響通常為正值通常為正值
i

買賣權平價(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 未除 100BS 理論價嚴重失真(σ=25 當成 2500%)σ = volatility_input / 100
到期期間單位錯誤三個月輸入 3(應為 0.25)T = months/12 或 T = days/365

15模型限制與風險揭露

股票報酬與基本面分析受資料期間、會計政策、一次性損益、除權息處理、產業差異、市場情緒等因素影響。過去績效不代表未來結果。技術指標來自歷史價格,不能保證預測未來。

Black-Scholes 限制——波動度非固定(波動度微笑/偏斜);市場可能出現價格跳躍(尾部風險);美式選擇權有提前履約價值,需二項樹等其他模型;股利為離散現金,非連續固定率。

!

網站工具應顯示:「本工具依歷史資料、公開財務數據及簡化模型進行分析。股票價格、盈餘、波動度與選擇權價值均可能快速變化。計算結果不構成投資建議、交易訊號或報酬保證。」

16關鍵字

股票價格報酬率總報酬率 對數報酬率累積報酬率年化報酬 波動度(σ)最大回撤夏普比率 EPSP/EP/B 股利殖利率ROE自由現金流(FCF) SMA(移動平均)黃金交叉死亡交叉 選擇權買權(Call)賣權(Put) 履約價格(K)權利金內含價值 損益兩平點Black-Scholes買賣權平價 DeltaGammaTheta Vega隱含波動度波動度微笑

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 完全對應的成品,等一下的挑戰任務就是在這份程式碼上動手修改。

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

觀念測驗

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

0 / 15 已作答

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

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

任務一:功能修改

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

難度:入門
  • 在結果區新增顯示「權利金總額」(權利金 × 契約乘數)
js/option-calculator.js — 可直接編輯
提示(卡住了再打開)
handleOptionPayoff() 函式最後(resEl.classList.remove("hidden") 之前),加入一行把計算結果寫進指定的空欄位。
💡 查看範例解答(建議自己先試過再打開)


            

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

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

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

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


            

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

任務三:邏輯錯誤(賣方方向 / 內含價值公式)

對應教材「六、選擇權的基本概念」— 買賣雙方零和關係

難度:除錯
  • 這份程式碼可以正常執行、不會報錯,但算出來的數字不合理
  • 找出問題所在並修正
js/option-calculator.js — 數字不合理,但沒有紅字錯誤
提示(卡住了再打開)
這是「跑得動、但答案錯」的類型,找不到紅字錯誤時,回頭檢查買方跟賣方的損益是不是真的互為相反數,以及買權跟賣權的內含價值公式有沒有用錯邊。
💡 查看範例解答(建議自己先試過再打開)


            

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