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:
Executable
+64
@@ -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"
|
||||
Reference in New Issue
Block a user