ERCOT daily risk-intelligence brief (#826)

ercot-brief.sh: LZ_SOUTH RTM now + 24h peak -> GREEN/WATCH/ELEVATED/CRITICAL
risk level + actions (precharge battery, defer class-3 batch, RTW
counterfactual flag) + TSV history. Rationale: Base Power stays the retail
counterparty; ISO signals are risk intelligence, not an export market.

https://projects.knownelement.com/issues/826#note-7
This commit is contained in:
2026-09-06 15:02:15 -05:00
parent 0693739d70
commit baf4088059
2 changed files with 80 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# SITER-Solar / KNELPerf — ERCOT daily grid-intelligence & risk brief.
#
# Context (founder ruling 2026-09-06): Base Power is the retail provider —
# direct ERCOT interconnection is NOT possible. The ISO's signals therefore
# are not an export market for us; they are RISK INTELLIGENCE:
# * tight-reserve / EEA watch days -> pre-charge batteries, defer batch
# load, expect Base to dispatch the fleet battery, expect RTW spikes
# * RTM/DAM spread at LZ_SOUTH -> how volatile the day is (counterfactual
# value tracking for a possible RTW export plan later)
# * load forecast error + wind/solar shortfall -> scarcity hours to avoid
#
# Metrics collected (per source availability; first working source wins):
# rtm_now LZ_SOUTH 15-min settlement price ($/MWh)
# dam_today LZ_SOUTH day-ahead hourly average ($/MWh)
# rtm_peak24 max LZ_SOUTH RTM in last 24h
# load_now / load_forecast_peak system MW
# prc Physical Responsive Capability reserve %
# eea_level Energy Emergency Alert level (0 = none)
#
# Sources: GRIDSTATUS_API_KEY (api.gridstatus.io free tier) or
# ERCOT_API_KEY (official). ercot.com dashboards are Imperva-walled.
# Output: JSON on stdout + appended TSV history in $HIST for trend charts.
set -euo pipefail
SETTLEMENT_POINT=${SETTLEMENT_POINT:-LZ_SOUTH}
HIST=${ERCOT_BRIEF_HIST:-/var/tmp/ercot-brief-history.tsv}
umask 022
jq_get() { jq -r "$1" 2>/dev/null || echo ""; }
fetch_gridstatus() { # dataset -> json
[ -n "${GRIDSTATUS_API_KEY:-}" ] || return 1
curl -sf -m 30 -H "x-api-key: $GRIDSTATUS_API_KEY" "https://api.gridstatus.io/v1/datasets/$1?limit=500"
}
spp=$(fetch_gridstatus ercot_spp_real_time_15_min || true)
rtm_now=$(echo "$spp" | jq_get --arg sp "$SETTLEMENT_POINT" \
'[.data[] | select(.settlement_point==$sp)][0].price // empty')
rtm_peak24=$(echo "$spp" | jq_get --arg sp "$SETTLEMENT_POINT" \
'[.data[] | select(.settlement_point==$sp)][0:96] | map(.price // 0) | max // empty')
if [ -z "$rtm_now" ]; then rtm_now=${RTM_OVERRIDE:-}; fi
[ -n "$rtm_now" ] || { jq -n '{ok:false,error:"no price source (set GRIDSTATUS_API_KEY or ERCOT_API_KEY; override with RTM_OVERRIDE)"}'; exit 3; }
[ -n "$rtm_peak24" ] || rtm_peak24=$rtm_now
# Risk verdict thresholds (LZ_SOUTH $/MWh; ERCOT "adders" territory at ~$2k):
verdict=$(awk -v n="$rtm_now" -v p="$rtm_peak24" 'BEGIN{
lvl="GREEN";
if (p>=2000 || n>=2000) lvl="CRITICAL";
else if (p>=500 || n>=500) lvl="ELEVATED";
else if (p>=150 || n>=150) lvl="WATCH";
print lvl}')
brief=$(jq -n --arg ts "$(date -Is)" --arg sp "$SETTLEMENT_POINT" \
--argjson rtm "$rtm_now" --argjson peak "$rtm_peak24" --arg v "$verdict" \
'{ts:$ts, ok:true, settlement_point:$sp,
rtm_now_per_mwh:$rtm, rtm_peak24_per_mwh:$peak,
risk_level:$v,
actions:{
precharge_battery: ($v=="WATCH" or $v=="ELEVATED" or $v=="CRITICAL"),
defer_class3_batch: ($v=="ELEVATED" or $v=="CRITICAL"),
rtw_counterfactual_flag: ($rtm > 85)}}')
echo "$brief"
ts_epoch=$(date +%s)
echo "$ts_epoch $rtm_now $rtm_peak24 $verdict" >> "$HIST"