← Files DayTrading.MonsterARCHIVED FILE

skills/us-stock-gamma-moomoo/scripts/option_scenario_table.py

4.33 KB · Oct 4, 2026 · 12:33 UTC

↓ Download file

#!/usr/bin/env python3
"""Build a short-dated option value scenario table.

This is intentionally data-provider agnostic. Obtain option IV/bid/ask/spot
from an available authorized source, then pass IV, strike, expiry, and spot grid.
"""

from __future__ import annotations

import argparse
import math
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo


def norm_cdf(x: float) -> float:
    return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))


def bs_price(spot: float, strike: float, years: float, rate: float, vol: float, kind: str) -> float:
    intrinsic = max(spot - strike, 0.0) if kind == "C" else max(strike - spot, 0.0)
    if years <= 0 or vol <= 0:
        return intrinsic
    sqrt_t = math.sqrt(years)
    d1 = (math.log(spot / strike) + (rate + 0.5 * vol * vol) * years) / (vol * sqrt_t)
    d2 = d1 - vol * sqrt_t
    if kind == "C":
        return spot * norm_cdf(d1) - strike * math.exp(-rate * years) * norm_cdf(d2)
    return strike * math.exp(-rate * years) * norm_cdf(-d2) - spot * norm_cdf(-d1)


def parse_dt(value: str, tz: ZoneInfo) -> datetime:
    dt = datetime.fromisoformat(value)
    if dt.tzinfo is None:
        return dt.replace(tzinfo=tz)
    return dt.astimezone(tz)


def parse_spots(args: argparse.Namespace) -> list[float]:
    if args.spots:
        return [float(part.strip()) for part in args.spots.split(",") if part.strip()]
    if args.spot_min is None or args.spot_max is None:
        raise SystemExit("Pass either --spots or both --spot-min and --spot-max.")
    step = args.spot_step
    n = int(round((args.spot_max - args.spot_min) / step))
    return [round(args.spot_min + i * step, 4) for i in range(n + 1)]


def build_times(asof: datetime, expiry: datetime, interval_min: int) -> list[datetime]:
    times: list[datetime] = []
    t = asof
    while t < expiry:
        times.append(t)
        t += timedelta(minutes=interval_min)
    if not times or times[-1] != expiry:
        times.append(expiry)
    return times


def fmt_price(value: float) -> str:
    if value >= 10:
        return f"{value:.1f}"
    if value >= 1:
        return f"{value:.2f}"
    return f"{value:.3f}"


def main() -> None:
    parser = argparse.ArgumentParser(description="Create a markdown scenario table for a short-dated option.")
    parser.add_argument("--kind", choices=["C", "P"], required=True, help="Option type: C or P.")
    parser.add_argument("--strike", type=float, required=True)
    parser.add_argument("--iv", type=float, required=True, help="Annualized IV. Accepts 0.16 or 16 for 16%%.")
    parser.add_argument("--expiry", required=True, help="ISO datetime. Naive values are interpreted in --timezone.")
    parser.add_argument("--asof", help="ISO datetime. Defaults to now in --timezone.")
    parser.add_argument("--timezone", default="Asia/Tokyo")
    parser.add_argument("--interval-min", type=int, default=30)
    parser.add_argument("--rate", type=float, default=0.04, help="Annual risk-free rate. Accepts 0.04 or 4 for 4%%.")
    parser.add_argument("--spots", help="Comma-separated underlying prices.")
    parser.add_argument("--spot-min", type=float)
    parser.add_argument("--spot-max", type=float)
    parser.add_argument("--spot-step", type=float, default=10.0)
    args = parser.parse_args()

    tz = ZoneInfo(args.timezone)
    asof = parse_dt(args.asof, tz) if args.asof else datetime.now(tz)
    expiry = parse_dt(args.expiry, tz)
    if expiry < asof:
        raise SystemExit("Expiry is before as-of time.")

    vol = args.iv / 100.0 if args.iv > 3 else args.iv
    rate = args.rate / 100.0 if args.rate > 1 else args.rate
    spots = parse_spots(args)
    times = build_times(asof, expiry, args.interval_min)

    headers = ["Spot"] + [t.strftime("%H:%M") for t in times]
    print("| " + " | ".join(headers) + " |")
    print("| " + " | ".join(["---"] * len(headers)) + " |")
    for spot in spots:
        row = [f"{spot:g}"]
        for t in times:
            years = max((expiry - t).total_seconds(), 0.0) / (365.0 * 24.0 * 60.0 * 60.0)
            row.append(fmt_price(bs_price(spot, args.strike, years, rate, vol, args.kind)))
        print("| " + " | ".join(row) + " |")

    print()
    print(f"Assumptions: {args.kind} strike {args.strike:g}, IV {vol:.2%}, rate {rate:.2%}, timezone {args.timezone}.")
    print("Real fills can differ because IV, spread, and liquidity change intraday.")


if __name__ == "__main__":
    main()

SHA-256: a064df6caa92be90fad6032e0aa9988b20a1a9f1a598f0198171f1db98ebc444