Files
foxhunt/scripts/surfer/fetch_caiso.py
jgrusewski 244ccaaf0b feat(harvest): premium-harvesting tested — simple 60/40 beats the sophisticated system
The strategy class we never tried: harvest structural premia + risk system, not predict.
Diversified futures + risk-parity/vol-target/trend; clean 5-asset-class version (ES/ZN/GC/CL/BTC).
Result: plain 60/40 (Sharpe +0.72, 2010-2026) BEATS risk-parity+vol-target (+0.46) and RP+trend
(+0.33, trend hurts); 5-asset+crypto RP only ties 60/40 and loses to buy-hold equity. Meta-pattern
now complete in BOTH games: simple beats/equals sophisticated in prediction AND harvesting.
Constructive deliverable: a simple premium harvest (60/40 / risk-parity) IS a real deployable
robust strategy (~0.5-0.72 Sharpe, low DD, no prediction, minimal complexity). The engine's
sophistication was never the return-generator -- deployable path is light (harvest + risk overlay),
engine's value is infra/discipline/product. (Also: CAISO intraday gate blocked by OASIS plumbing.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 00:42:32 +02:00

84 lines
2.7 KiB
Python

#!/usr/bin/env python3
"""Fetch CAISO day-ahead (DAM) + real-time (RTM) hourly LMP for one hub (free OASIS, no key).
The RT-vs-DA spread is the forecast-error-driven, less-seasonal intraday opportunity. Monthly
chunks with backoff (OASIS rate-limits hard). Aggregates RT to hourly. Caches per chunk.
Saves data/surfer/caiso/{dam,rtm}.json as {hour_key: price}.
"""
import io
import json
import os
import time
import urllib.request
import zipfile
NODE = "TH_NP15_GEN-APND"
OUT = "data/surfer/caiso"
MONTHS = [(2024, m) for m in range(1, 13)]
def oasis(qn, market, y, m):
s = f"{y}{m:02d}01T08:00-0000"
ny, nm = (y + 1, 1) if m == 12 else (y, m + 1)
e = f"{ny}{nm:02d}01T08:00-0000"
u = (f"http://oasis.caiso.com/oasisapi/SingleZip?queryname={qn}&startdatetime={s}"
f"&enddatetime={e}&version=1&market_run_id={market}&node={NODE}&resultformat=6")
for a in range(5):
try:
raw = urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=90).read()
z = zipfile.ZipFile(io.BytesIO(raw))
txt = z.read(z.namelist()[0]).decode()
return txt
except Exception as ex:
time.sleep(12 * (a + 1))
return None
def parse_hourly(txt):
lines = txt.strip().split("\n")
hdr = lines[0].split(",")
if "LMP_TYPE" not in hdr:
return {} # error report / wrong format
iT = hdr.index("LMP_TYPE")
iV = hdr.index("MW") if "MW" in hdr else (hdr.index("PRC") if "PRC" in hdr else -1)
if iV < 0:
return {}
iD = hdr.index("OPR_DT"); iH = hdr.index("OPR_HR")
agg = {}
for ln in lines[1:]:
c = ln.split(",")
if len(c) <= max(iT, iV, iD, iH):
continue
if c[iT] != "LMP":
continue
k = f"{c[iD]}H{int(c[iH]):02d}"
agg.setdefault(k, []).append(float(c[iV]))
return {k: sum(v) / len(v) for k, v in agg.items()}
def fetch(market, qn):
os.makedirs(OUT, exist_ok=True)
out = {}
for y, m in MONTHS:
cf = f"{OUT}/{market}_{y}{m:02d}.json"
if os.path.exists(cf):
out.update(json.load(open(cf))); continue
txt = oasis(qn, market, y, m)
if not txt:
print(f" {market} {y}-{m:02d}: FAILED"); continue
h = parse_hourly(txt)
json.dump(h, open(cf, "w")); out.update(h)
print(f" {market} {y}-{m:02d}: {len(h)} hrs")
time.sleep(6)
return out
def main():
dam = fetch("DAM", "PRC_LMP"); rtm = fetch("RTPD", "PRC_RTPD_LMP") # 15-min RT pre-dispatch
json.dump(dam, open(f"{OUT}/dam.json", "w")); json.dump(rtm, open(f"{OUT}/rtm.json", "w"))
print(f"DONE: DAM {len(dam)} hrs, RTM {len(rtm)} hrs")
if __name__ == "__main__":
main()