#!/usr/bin/env python3
"""
Build a HISTORICAL option book from Deribit's free public history API.

No account, no API key, no paid data vendor, no NDA. The endpoint used here
(history.deribit.com) serves executed option trades and is open to anyone.

What this produces is a TRADE BLOTTER, not a chain snapshot: every row is one
option trade that actually printed on the venue on the chosen date, carrying the
index level, implied vol and size recorded at the moment it traded. That is the
shape an auditor asks about, because the question is never "what is this worth
now", it is "reproduce the valuation of what we traded that day".

Deribit BTC options are European exercise, cash settled, which is the model class
LuxiBook implements.

Column semantics written to the CSV:
  id     instrument name plus the venue's own trade_id, so every row is traceable
  S      Deribit index_price AT THAT TRADE'S TIMESTAMP. This is the spot index,
         NOT the per-expiry forward. See the honesty note below.
  K      strike, parsed from the instrument name
  T      years from the trade timestamp to expiry at 08:00 UTC, 365-day basis
  r      0.0, because Deribit reports interest_rate = 0.0 on these instruments
  sigma  the venue's own recorded implied vol for that trade, as a decimal
  qty    trade size in BTC (the `amount` field)
  cp     C or P
  q      0
  model  black76

HONESTY NOTE ON S. The live chain endpoint publishes `underlying_price`, the
per-expiry forward. The historical trade endpoint does not; it publishes
`index_price`, the spot index. So S here is spot, and any comparison against
Deribit's own `mark_price` will carry a bias equal to the basis. That does not
affect the reproducibility claim, which depends only on the bytes of this CSV.
Do not use this file to make a precision claim against venue marks.

Usage:
  python3 build_historical_book.py 2025-02-18 > book.csv
  python3 build_historical_book.py 2025-02-18 --raw trades.json > book.csv
"""
import datetime
import json
import sys
import time
import urllib.request

MON = {m: i + 1 for i, m in enumerate(
    "JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC".split())}

ENDPOINT = (
    "https://history.deribit.com/api/v2/public/"
    "get_last_trades_by_currency_and_time"
    "?currency=%s&kind=option&start_timestamp=%d&end_timestamp=%d"
    "&count=1000&sorting=asc"
)
YEAR_SECONDS = 365.0 * 86400.0


def to_ms(iso):
    dt = datetime.datetime.fromisoformat(iso).replace(
        tzinfo=datetime.timezone.utc)
    return int(dt.timestamp() * 1000)


def fetch_day(currency, date_str, max_calls=80):
    """Walk the whole UTC day forward in pages. Returns trades sorted by time."""
    start = to_ms(date_str + "T00:00:00")
    end = to_ms(date_str + "T23:59:59.999")
    seen = {}
    cursor, calls = start, 0
    while calls < max_calls:
        url = ENDPOINT % (currency, cursor, end)
        with urllib.request.urlopen(url, timeout=60) as resp:
            result = json.load(resp)["result"]
        trades = result["trades"]
        calls += 1
        if not trades:
            break
        for t in trades:
            seen[t["trade_id"]] = t
        newest = max(t["timestamp"] for t in trades)
        if not result.get("has_more") or newest >= end:
            break
        cursor = newest + 1
        time.sleep(0.2)
    return sorted(seen.values(), key=lambda t: (t["timestamp"], t["trade_id"]))


def parse_expiry(code):
    """DDMMMYY -> datetime at 08:00 UTC, Deribit's settlement time."""
    return datetime.datetime(
        2000 + int(code[-2:]), MON[code[-5:-2]], int(code[:-5]),
        8, 0, tzinfo=datetime.timezone.utc)


def build_rows(trades):
    rows, dropped = [], {"no_iv": 0, "expired": 0, "bad_size": 0, "parse": 0}
    for t in trades:
        try:
            _, exp_code, strike, cp = t["instrument_name"].split("-")
            expiry = parse_expiry(exp_code)
        except Exception:
            dropped["parse"] += 1
            continue
        iv = t.get("iv")
        if iv is None or iv <= 0:
            dropped["no_iv"] += 1
            continue
        traded_at = datetime.datetime.fromtimestamp(
            t["timestamp"] / 1000.0, datetime.timezone.utc)
        T = (expiry - traded_at).total_seconds() / YEAR_SECONDS
        if T <= 0:
            dropped["expired"] += 1
            continue
        qty = t.get("amount")
        if not qty or qty <= 0:
            dropped["bad_size"] += 1
            continue
        rows.append("%s_%s,%.17g,%.17g,%.17g,0,%.17g,%.17g,%s,0,black76" % (
            t["instrument_name"].replace("-", "_"), t["trade_id"],
            t["index_price"], float(strike), T, iv / 100.0, qty, cp))
    return rows, dropped


def main():
    if len(sys.argv) < 2:
        sys.exit("usage: build_historical_book.py YYYY-MM-DD [--raw out.json]")
    date_str = sys.argv[1]
    raw_path = None
    if "--raw" in sys.argv:
        raw_path = sys.argv[sys.argv.index("--raw") + 1]

    trades = fetch_day("BTC", date_str)
    if raw_path:
        with open(raw_path, "w") as fh:
            json.dump(trades, fh)

    rows, dropped = build_rows(trades)
    instruments = len({r.split(",")[0].rsplit("_", 1)[0] for r in rows})

    w = sys.stdout.write
    w("# HISTORICAL TRADE BLOTTER - Deribit executed BTC option trades.\n")
    w("# source: history.deribit.com public API, no auth, no key, no purchase.\n")
    w("# trade date %s UTC (full 24h), trades fetched %d, rows kept %d\n"
      % (date_str, len(trades), len(rows)))
    w("# distinct instruments %d, dropped: %s\n" % (instruments, dropped))
    w("# S = Deribit index_price at that trade's timestamp (SPOT INDEX, not the\n")
    w("#     per-expiry forward). sigma = the venue's own recorded iv for the\n")
    w("#     trade. qty = executed size in BTC. r = 0 (Deribit interest_rate=0).\n")
    w("# Deribit BTC options are European exercise, cash settled.\n")
    w("id,S,K,T,r,sigma,qty,cp,q,model\n")
    w("\n".join(rows) + "\n")

    sys.stderr.write("rows=%d instruments=%d dropped=%s\n"
                     % (len(rows), instruments, dropped))


if __name__ == "__main__":
    main()

