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>
84 lines
2.7 KiB
Python
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()
|