top of page
Market Risk Managementสารบัญ
Market Risk Managementพิมพ์หน้านี้

ภาคผนวก A–B · Python และ Excel

โค้ดและสูตรในหน้านี้คงไว้ตามต้นฉบับเพื่อให้ทำซ้ำได้ คำอธิบายเป็นภาษาไทย ส่วนชื่อฟังก์ชันและคอมเมนต์ในโค้ดเป็นภาษาอังกฤษตามต้นฉบับ

อ้างอิงต้นฉบับ: หน้า 47–48 · เปิด PDF หน้า 47

ภาคผนวก A · Python reference implementation

ต้นฉบับระบุว่าโค้ดด้านล่างรันได้ทันที (numpy, pandas, scipy) และทำซ้ำผลทั้งหมดของ §9.2 (T=998 เพราะการจัดแนวคลาดหนึ่งวัน; จำนวน breach เท่ากัน) รวมถึง reverse stress test ของ §11.3, liquidity coverage ของ §11.4 และสถิติ drawdown ของบทที่ 13 กฎการออกแบบ: การพยากรณ์ของวัน tt ใช้ข้อมูลถึง t−1t-1 เท่านั้น; ขอบเขตของเขตใช้ “ต่ำกว่า ppf อย่างเคร่งครัด”

ส่วน __main__ ต้องใช้ไฟล์ sp500.csv จาก workbook ของคอร์สต้นทาง ซึ่งไม่ได้แจกจ่ายในเว็บนี้ ฟังก์ชันที่ไม่ต้องใช้ข้อมูลตลาดถูกเรียกใช้ใน Jupyter Notebook ของเว็บนี้ เพื่อตรวจตัวอย่างคำนวณ

import numpy as np, pandas as pd
from scipy.stats import norm, binom, chi2

def log_returns(px: pd.Series) -> pd.Series:
    return np.log(px).diff().dropna()

def ewma_sigma(r: pd.Series, lam=0.94, seed_n=250) -> pd.Series:
    """sigma_{t|t-1}: forecast for day t using returns up to t-1."""
    v = np.empty(len(r)); v[0] = np.mean(r.values[:seed_n] ** 2)
    for t in range(1, len(r)):
        v[t] = lam * v[t-1] + (1 - lam) * r.values[t-1] ** 2
    return pd.Series(np.sqrt(v), index=r.index)

def cf_factor(p, S, K):
    z = norm.ppf(p)
    return z + (z*z-1)*S/6 + (z**3-3*z)*K/24 - (2*z**3-5*z)*S*S/36

def normal_es_factor(c):
    return -norm.pdf(norm.ppf(1-c)) / (1-c)  # -2.6652 at c = 0.99

def var_forecasts(r, c=0.99, window=250, lam=0.94):
    p, z = 1 - c, norm.ppf(1 - c)
    sig = ewma_sigma(r, lam)
    out = pd.DataFrame(index=r.index)
    out["roll21"] = z * r.rolling(21).std().shift(1)
    out["ewma"]   = z * sig
    out["hs"]     = r.rolling(window).quantile(p).shift(1)
    resid = r / sig  # devolatilise
    out["fhs"]    = resid.rolling(window).quantile(p).shift(1) * sig  # revolatilise
    return out

def backtest(r, var, p=0.01):
    m = var.notna(); hit = (r[m] < var[m]).astype(int).values
    T, x = len(hit), hit.sum(); ph = x / T
    ll = lambda q, k0, k1: (k0*np.log(1-q) if k0 else 0) + (k1*np.log(q) if k1 else 0)
    pof = -2 * (ll(p, T-x, x) - ll(ph, T-x, x))
    a, b = hit[:-1], hit[1:]
    n00, n01 = ((a==0)&(b==0)).sum(), ((a==0)&(b==1)).sum()
    n10, n11 = ((a==1)&(b==0)).sum(), ((a==1)&(b==1)).sum()
    pi0, pi1 = n01/(n00+n01), (n11/(n10+n11) if n10+n11 else 0.0)
    pi = (n01+n11)/(n00+n01+n10+n11)
    ind = -2 * (ll(pi, n00+n10, n01+n11) - ll(pi0, n00, n01) - ll(pi1, n10, n11))
    k250 = hit[-250:].sum()
    g, y = binom.ppf(0.95, 250, p), binom.ppf(0.9999, 250, p)  # 5, 10
    zone = "green" if k250 < g else ("yellow" if k250 < y else "red")
    return dict(T=T, x=int(x), rate=ph, LR_pof=pof, p_pof=chi2.sf(pof, 1),
                LR_ind=ind, p_ind=chi2.sf(ind, 1), cc=pof+ind, p_cc=chi2.sf(pof+ind, 2),
                pi1=pi1, last250=int(k250), zone=zone)

# ---------- Part II: stress testing ----------
def reverse_stress(w, cov, loss):
    """Most plausible scenario x* (min Mahalanobis) with w'x = -loss."""
    w, cov = np.asarray(w, float), np.asarray(cov, float)
    var_p = w @ cov @ w
    x = -loss * cov @ w / var_p
    return x, loss / np.sqrt(var_p)  # scenario, Mahalanobis distance

def coverage(buckets, redemption, horizon, stress_mult=1.0):
    """buckets: list of (share_of_nav, days_to_liquidate)."""
    avail = sum(a for a, d in buckets if d * stress_mult <= horizon)
    return avail / redemption

# ---------- Part III: drawdown ----------
def drawdown_stats(nav: pd.Series) -> dict:
    peak = nav.cummax(); dd = nav / peak - 1
    trough = dd.idxmin(); start = nav.loc[:trough].idxmax()
    after = nav.loc[trough:]; rec = after[after >= nav.loc[start]]
    yrs = (nav.index[-1] - nav.index[0]).days / 365.25
    cagr = (nav.iloc[-1] / nav.iloc[0]) ** (1 / yrs) - 1
    return dict(mdd=dd.min(), peak=start, trough=trough,
                recovery=rec.index[0] if len(rec) else None,
                calmar=cagr / -dd.min(), ulcer=np.sqrt((dd ** 2).mean()))

def mc_mdd(sigma=0.16, mu=0.0, years=3, n_paths=10000, steps=252, seed=1):
    rng = np.random.default_rng(seed)
    lr = (mu - 0.5 * sigma**2) / steps + sigma / np.sqrt(steps) * rng.standard_normal((n_paths, years * steps))
    lv = np.concatenate([np.zeros((n_paths, 1)), np.cumsum(lr, axis=1)], axis=1)
    mdd = 1 - np.exp(lv - np.maximum.accumulate(lv, axis=1))
    return mdd.max(axis=1)  # distribution of MDD

if __name__ == "__main__":
    px = pd.read_csv("sp500.csv", index_col=0, parse_dates=True)["Px"]
    r = log_returns(px)
    f = var_forecasts(r).iloc[251:]
    for col in f:
        print(col, {k: (round(v, 4) if isinstance(v, float) else v)
                    for k, v in backtest(r.loc[f.index], f[col]).items()})
    print(drawdown_stats(px))
    m = mc_mdd(years=3, mu=0.06); print("3y MDD median/95%:", np.median(m).round(3), np.quantile(m, .95).round(3))

ภาคผนวก B · สูตร Excel ที่ใช้บ่อย

การใช้งาน สูตร หมายเหตุ
Log-return =LN(B8/B7)
Normal factor =NORM.S.INV(0.01) −2.32635
Normal ES factor =-NORM.S.DIST(NORM.S.INV(0.01),FALSE)/0.01 −2.6652
σ แบบหน้าต่าง 21 ค่า =STDEV.S(C8:C28) ค่าแรก แล้วเลื่อนลงทีละแถว
VaR 10 วัน =STDEV.S(C8:C28)*SQRT(10)*Factor_VaR Factor_VaR เป็นเซลล์ที่ตั้งชื่อไว้
ผลตอบแทนไปข้างหน้า 10 วัน =LN(B39/B29) จัดให้อยู่แถวเดียวกับ VaR
Breach flag =IF(E28<D28,1,0) ใช้ธรรมเนียมเครื่องหมายลบ
ความแปรปรวน EWMA =E5*$I$2+D5*(1-$I$2) คอลัมน์ D เก็บ u², I2 เก็บ λ
HS VaR =PERCENTILE.INC(C8:C257,0.01) หรือ SMALL(range,k)
Binomial CDF สะสม =BINOM.DIST(k,250,0.01,TRUE) สำหรับสัญญาณไฟจราจร
Kupiec p =CHISQ.DIST.RT(LR,1)
ความเบ้ / excess kurtosis =SKEW(range), =KURT(range) KURT ของ Excel เป็น excess kurtosis แล้ว

ผู้เขียนต้นฉบับ

Yini Gao

ผู้เขียน Market Risk Handbook (English edition, Version 2.1, กันยายน 2026) ซึ่งเป็นต้นฉบับของบันทึกการเรียนภาษาไทยชุดนี้

อ่านบทนี้แล้วอยากเรียนรู้ต่อ? ติดตาม QuantCorner ได้ที่นี่

Market Risk Managementโดย Yini Gao

ค้นหาในสมุดบันทึก

ปิด
คำค้นหา

© 2023 by QuantCorner, Thailand.

bottom of page