Linear optimization · Market analysis

How to optimize hydro storage and trading into multiple electricity markets?

A storage reservoir moves energy through time; the transmission system moves it through space. An hourly linear program (LP) optimizes when and where to move that energy based on market price and system constraints.

Approach

British Columbia and Uruguay have electricity systems anchored by large hydroelectric facilities. Both jurisdictions trade energy across their borders, creating opportunities and challenges for optimization. The key difference between regions is the pricing strategy: Uruguay's system sets prices for sale into multiple markets, while British Columbia trades against spot prices across multiple markets.

We formulated an LP model in AMPL and solved with HiGHS / CPLEX, representing a simplified hydroelectric system trading electricity into multiple neighboring markets. The model captures the essential characteristics of the system, including storage capacity, generation limits, and market prices. For British Columbia, we compare model outputs for one reference year, FY2025 (April 2024 to March 2025), to published financial reporting to validate the model's accuracy and reliability. For Uruguay, we compare the 2023 and 2024 model outputs to published market price data. The same LP model is used for both jurisdictions, with different constraints and different input data to reflect the different system characteristics and market conditions.

Figure 1: Two hydro systems and their trading markets
AESOMid-CCAISOBritish ColumbiaAlbertaUnited StatesWECC Path 1WECC Path 354.0TWh

British Columbia FY2025, April 2024 to March 2025

British Columbia: BC Hydro Annual Report 2025/26 (Appendix D); "other" is independent power produced by wind, solar, run-of-river hydro, biomass, and other sources. BC's true hydro share is higher than its donut shows.

UruguayArgentinaBrazil14.2TWh

Uruguay 2024

Uruguay: ADME hourly generation by source, 2024; "other" is wind, solar and biomass.

  • Hydro
  • Thermal
  • Other
  • Intertie

Results

The model is handed published prices, so the question is not whether it reproduces them but whether the schedule it builds resembles the one the system actually ran. Three things can be checked against something published: the prices it realises, its monthly volumes, and — for Uruguay alone — the water value itself.

British Columbia. Its monthly hydro shape lands within 0.79 percentage points of StatCan's, inside the 1.05 points by which 2022 and 2023 differ from each other — and the within-year shape is the one series no input could have fitted, since only the annual totals are measured. Monthly net exports correlate at 0.582, at −9.1 TWh against a province-wide −7.8 TWh. Price is where it is plainly beaten: it sells into the US at $58.32/MWh against BC's realised $85.52, and buys at $23.35 against $33.87. Three markets cannot reach most of the counterparties BC actually trades with, and the capture price is where that shows up. The actuals here are province-wide rather than BC Hydro's alone, so read the levels as indicative and the shapes as the test.

Uruguay is the case with a measured counterpart for the payoff itself. Its hourly price tracks ADME's sanctioned spot at r = 0.853, averaging $90.88/MWh against $87.61, and monthly net interchange correlates at 0.829. The water value — the number the model exists to produce — tracks ADME's own published one at r = 0.938, but sits $28.80/MWh under it on average: through the drought the model holds water too cheaply. Right shape, wrong level, is the honest summary.

British Columbia's run returns a trading margin of $116M over the year. There is no reported figure for the same year to set beside it: BC Hydro's F2025 trade income was removed from the Revenue Requirements line by regulation, and the filing's system-trade schedule stops a year earlier. The nearest published comparator is the regulator's own five-year average of Powerex trade income, about $399M — a normal year rather than this one, and a figure the model comes in well under, which is the direction to expect from a three-market system that cannot reach most of the counterparties BC actually sells to.

Uruguay has no published margin at all, and in 2023 the model's objective is −$150M — a cost rather than a profit, once thermal generation is paid for in a drought year. Neither figure is what the model is scored on; the comparisons above are.

Across 8,760 hours British Columbia's water value never moves. It is one number, $41.61/MWh, for the whole year, while the system's own marginal price runs from −$34.00 to $155.20. A reservoir this large is a flywheel: it absorbs almost everything the market does, and the opportunity cost of water never notices.

That is the useful finding, and it is a trading rule rather than a curiosity. For 773 hours of the year the marginal price sits above that value. Those are the hours when releasing water is unambiguously right, and they can be identified without re-running anything.

It never moves because it is never forced to. Storage touches neither the ceiling nor the floor in any of the 8,760 hours — it runs between 6.8 and 20.8 TWh inside walls of 3 and 25 — so nothing in the year prices the water except the requirement that it end where it began. Move that target and the whole level moves with it, which is why the storage path is a consequence of an assumption rather than a prediction of one. Uruguay, where the reservoir really was scarce, is the counter-example: there the same dual moves across the year.

FY2025 (April 2024 - March 2025)One aggregate reservoir for the BC Hydro system, trading into Mid-C, CAISO and Alberta.

Figure 01 — Trading margin

The model trades more, in both directions, than BC did

Gross volumes by market against BC’s reported trade, exports up and imports down. Rest of the US has no model market, so the model trades nothing there against a reported 1,371 GWh of exports.

Net position: model −9,083 GWh against −7,838 reported

GWH — EXPORTS UP, IMPORTS DOWN−15,000−10,000−5,00005,000Mid-C / Pacific NorthwestCAISO / CaliforniaAlberta(interprovincial)Rest of the US(no model market)Mid-C / Pacific NorthwestModel exports3,951 GWhModel imports12,152 GWhReported exports3,267 GWhReported imports9,026 GWhNet, model−8,200 GWhNet, reported−5,759 GWhCAISO / CaliforniaModel exports3,569 GWhModel imports1,108 GWhReported exports1,595 GWhReported imports2,469 GWhNet, model+2,460 GWhNet, reported−874 GWhAlbertaModel exports1,579 GWhModel imports4,922 GWhReported exports239 GWhReported imports2,394 GWhNet, model−3,343 GWhNet, reported−2,156 GWhRest of the USModel exportsModel importsReported exports1,371 GWhReported imports420 GWhNet, modelNet, reported+951 GWhGWH — EXPORTS UP, IMPORTS DOWN−15,000−10,000−5,00005,000Mid-C / Pacific NorthwestCAISO / CaliforniaAlberta(interprovincial)Rest of the US(no model market)Mid-C / Pacific NorthwestModel exports3,951 GWhModel imports12,152 GWhReported exports3,267 GWhReported imports9,026 GWhNet, model−8,200 GWhNet, reported−5,759 GWhCAISO / CaliforniaModel exports3,569 GWhModel imports1,108 GWhReported exports1,595 GWhReported imports2,469 GWhNet, model+2,460 GWhNet, reported−874 GWhAlbertaModel exports1,579 GWhModel imports4,922 GWhReported exports239 GWhReported imports2,394 GWhNet, model−3,343 GWhNet, reported−2,156 GWhRest of the USModel exportsModel importsReported exports1,371 GWhReported imports420 GWhNet, modelNet, reported+951 GWh
Model exportsModel importsReported exportsReported imports

Net position: model −9,083 GWh against −7,838 reported

NET EXPORTS, GWH−10,000−7,500−5,000−2,50002,500Mid-C / Pacific NorthwestCAISO / CaliforniaAlberta(interprovincial)Rest of the US(no model market)Mid-C / Pacific NorthwestModel exports3,951 GWhModel imports12,152 GWhReported exports3,267 GWhReported imports9,026 GWhNet, model−8,200 GWhNet, reported−5,759 GWhCAISO / CaliforniaModel exports3,569 GWhModel imports1,108 GWhReported exports1,595 GWhReported imports2,469 GWhNet, model+2,460 GWhNet, reported−874 GWhAlbertaModel exports1,579 GWhModel imports4,922 GWhReported exports239 GWhReported imports2,394 GWhNet, model−3,343 GWhNet, reported−2,156 GWhRest of the USModel exportsModel importsReported exports1,371 GWhReported imports420 GWhNet, modelNet, reported+951 GWhNET EXPORTS, GWH−10,000−7,500−5,000−2,50002,500Mid-C / Pacific NorthwestCAISO / CaliforniaAlberta(interprovincial)Rest of the US(no model market)Mid-C / Pacific NorthwestModel exports3,951 GWhModel imports12,152 GWhReported exports3,267 GWhReported imports9,026 GWhNet, model−8,200 GWhNet, reported−5,759 GWhCAISO / CaliforniaModel exports3,569 GWhModel imports1,108 GWhReported exports1,595 GWhReported imports2,469 GWhNet, model+2,460 GWhNet, reported−874 GWhAlbertaModel exports1,579 GWhModel imports4,922 GWhReported exports239 GWhReported imports2,394 GWhNet, model−3,343 GWhNet, reported−2,156 GWhRest of the USModel exportsModel importsReported exports1,371 GWhReported imports420 GWhNet, modelNet, reported+951 GWh
Model netReported net

Figure 02 — Reservoir storage

The model fills and drains when the reservoirs do

Measured levels are metres and the model is TWh of energy; nothing public converts one to the other, so each series is normalised over its own range and only the timing compares. Williston tracks at r = 0.98.

0%25%50%75%100%Apr2024MayJunJulAugSepOctNovDecJan2025FebMar0%25%50%75%100%Apr2024JunAugOctDecJan2025Feb
0102030Apr2024MayJunJulAugSepOctNovDecJan2025FebMarSmax 25 TWh, assumedSmin 3 TWh, assumed0102030Apr2024JunAugOctDecJan2025FebSmax 25 TWh, assumedSmin 3 TWh, assumed

Figure 03 — Water value

The water value holds flat; the price wanders

The value of stored water — the shadow price of the storage balance — is one number all year, $41.61/MWh, while the Mid-C price moves day to day around it. A run that ends where it started and never touches a reservoir bound has nothing to reprice.

The water value is one number all year, $41.61. The Mid-C price moves $93.80 across the same days.

RESERVOIR STORAGE, TWHWATER VALUE AGAINST THE MID-C PRICE, $/MWH0102030−250255075100Apr2024MayJunJulAugSepOctNovDecJan2025FebMarfulloperating floorRESERVOIR STORAGE, TWHWATER VALUE VS PRICE, $/MWH0102030−250255075100Apr2024JunAugOctDecJan2025Febfulloperating floor

Figure 04 — Sensitivity

The water value does not depend on storage

Each point is an hour. The shadow price sits on the water value whatever the reservoir holds; the 4,633 hours below it are hours on the minimum-generation bound, where the fleet must run whether or not the price justifies it.

HOURLY SHADOW PRICE AGAINST STORAGEBY STORAGE BAND, 2 TWH WIDE−500501001502000204060681012141618202279111315171921Storage (TWh)Band centre (TWh)water value $41.61/MWh, the same in all 8,760 hoursStorage 6–8 TWhMean shadow price$32.63Mean Mid-C price$27.095th–95th percentile$14 – $42Water value$41.61Water value − mean shadow$8.98Storage 8–10 TWhMean shadow price$31.11Mean Mid-C price$30.085th–95th percentile$3 – $44Water value$41.61Water value − mean shadow$10.50Storage 10–12 TWhMean shadow price$32.43Mean Mid-C price$37.715th–95th percentile$5 – $50Water value$41.61Water value − mean shadow$9.18Storage 12–14 TWhMean shadow price$34.74Mean Mid-C price$36.185th–95th percentile$11 – $44Water value$41.61Water value − mean shadow$6.87Storage 14–16 TWhMean shadow price$35.29Mean Mid-C price$31.735th–95th percentile$13 – $42Water value$41.61Water value − mean shadow$6.32Storage 16–18 TWhMean shadow price$36.85Mean Mid-C price$36.055th–95th percentile$23 – $43Water value$41.61Water value − mean shadow$4.76Storage 18–20 TWhMean shadow price$36.36Mean Mid-C price$37.835th–95th percentile$21 – $44Water value$41.61Water value − mean shadow$5.25Storage 20–22 TWhMean shadow price$33.06Mean Mid-C price$34.095th–95th percentile$18 – $43Water value$41.61Water value − mean shadow$8.55HOURLY SHADOW PRICE AGAINST STORAGEBY STORAGE BAND, 2 TWH WIDE−500501001502000204060681012141618202279111315171921Storage (TWh)Band centre (TWh)water value $41.61/MWh, the same in all 8,760 hoursStorage 6–8 TWhMean shadow price$32.63Mean Mid-C price$27.095th–95th percentile$14 – $42Water value$41.61Water value − mean shadow$8.98Storage 8–10 TWhMean shadow price$31.11Mean Mid-C price$30.085th–95th percentile$3 – $44Water value$41.61Water value − mean shadow$10.50Storage 10–12 TWhMean shadow price$32.43Mean Mid-C price$37.715th–95th percentile$5 – $50Water value$41.61Water value − mean shadow$9.18Storage 12–14 TWhMean shadow price$34.74Mean Mid-C price$36.185th–95th percentile$11 – $44Water value$41.61Water value − mean shadow$6.87Storage 14–16 TWhMean shadow price$35.29Mean Mid-C price$31.735th–95th percentile$13 – $42Water value$41.61Water value − mean shadow$6.32Storage 16–18 TWhMean shadow price$36.85Mean Mid-C price$36.055th–95th percentile$23 – $43Water value$41.61Water value − mean shadow$4.76Storage 18–20 TWhMean shadow price$36.36Mean Mid-C price$37.835th–95th percentile$21 – $44Water value$41.61Water value − mean shadow$5.25Storage 20–22 TWhMean shadow price$33.06Mean Mid-C price$34.095th–95th percentile$18 – $43Water value$41.61Water value − mean shadow$8.55
Mean shadow priceMean Mid-C priceWater value

2023 · rolling 7-day windowThe Rio Negro cascade as one reservoir, trading into Brazil and Argentina.

Figure 01 — Trading margin

The model sells into Argentina where ADME did not

Volumes by interconnection against ADME’s own metered interchange. The model exported 968 GWh to Argentina against a reported 211: Argentine prices were high in the drought and the LP sold into them, while ADME’s mandate is supply security and not margin.

Net position: model −450 GWh against −1,159 reported

GWH — EXPORTS UP, IMPORTS DOWN−1,500−1,000−50005001,000Brazil(Melo + Rivera)ArgentinaBrazilModel exports11 GWhModel imports1,427 GWhReported exports17 GWhReported imports1,367 GWhNet, model−1,416 GWhNet, reported−1,350 GWhArgentinaModel exports968 GWhModel imports2 GWhReported exports211 GWhReported imports19 GWhNet, model+966 GWhNet, reported+192 GWhGWH — EXPORTS UP, IMPORTS DOWN−1,500−1,000−50005001,000Brazil(Melo + Rivera)ArgentinaBrazilModel exports11 GWhModel imports1,427 GWhReported exports17 GWhReported imports1,367 GWhNet, model−1,416 GWhNet, reported−1,350 GWhArgentinaModel exports968 GWhModel imports2 GWhReported exports211 GWhReported imports19 GWhNet, model+966 GWhNet, reported+192 GWh
Model exportsModel importsReported exportsReported imports

Net position: model −450 GWh against −1,159 reported

NET EXPORTS, GWH−1,500−1,000−50005001,000Brazil(Melo + Rivera)ArgentinaBrazilModel exports11 GWhModel imports1,427 GWhReported exports17 GWhReported imports1,367 GWhNet, model−1,416 GWhNet, reported−1,350 GWhArgentinaModel exports968 GWhModel imports2 GWhReported exports211 GWhReported imports19 GWhNet, model+966 GWhNet, reported+192 GWhNET EXPORTS, GWH−1,500−1,000−50005001,000Brazil(Melo + Rivera)ArgentinaBrazilModel exports11 GWhModel imports1,427 GWhReported exports17 GWhReported imports1,367 GWhNet, model−1,416 GWhNet, reported−1,350 GWhArgentinaModel exports968 GWhModel imports2 GWhReported exports211 GWhReported imports19 GWhNet, model+966 GWhNet, reported+192 GWh
Model netReported net

Figure 02 — Reservoir storage

The rolling run tracks the reservoir, not just its shape

Unlike British Columbia, the measured counterpart here is in the model’s own units — Bonete’s level converted to cascade energy — so the levels are comparable and not only the timing. The run follows the drought down at r = 0.94.

0%25%50%75%100%Jan2023FebMarAprMayJunJulAugSepOctNovDec0%25%50%75%100%Jan2023MarMayJulSepNov
0.00.51.01.52.02.5Jan2023FebMarAprMayJunJulAugSepOctNovDecSmax 1.861 TWh, assumedSmin 0 TWh, assumed0.00.51.01.52.02.5Jan2023MarMayJulSepNovSmax 1.861 TWh, assumedSmin 0 TWh, assumed

Figure 03 — Water value

The water value moves, because the reservoir does

The surface reprices storage every week rather than holding one number all year, so the water value runs from $-0 to $222/MWh against ADME’s sanctioned spot. It is the same dual as British Columbia’s, in a year where storage was actually scarce.

The water value moves $222.01 across the year. The adme spot moves $250.00, about 1 times as far.

RESERVOIR STORAGE, TWHWATER VALUE AGAINST THE ADME SPOT, $/MWH0.01.02.03.0−1000100200300Jan2023FebMarAprMayJunJulAugSepOctNovDecfulloperating floorRESERVOIR STORAGE, TWHWATER VALUE VS PRICE, $/MWH0.01.02.03.0−1000100200300Jan2023MarMayJulSepNovfulloperating floor

Figure 04 — Sensitivity

Here the water value does depend on storage

Each point is an hour. The mean shadow price is $149/MWh with the reservoir near 0.5 TWh and $3 with it near 1.9 — the relationship British Columbia’s flat case cannot show, and the reason the two runs are worth reading together.

HOURLY SHADOW PRICE AGAINST STORAGEBY STORAGE BAND, 0.2 TWH WIDE−1000100200300−10001002003000.40.60.81.01.21.41.61.82.00.50.70.91.11.31.51.71.9Storage (TWh)Band centre (TWh)mean water value $94.85/MWh across 8,760 hoursStorage 0.4–0.6 TWhMean shadow price$149.32Mean ADME spot price$140.155th–95th percentile$36 – $222Water value$94.85Water value − mean shadow$−54.47Storage 0.6–0.8 TWhMean shadow price$130.77Mean ADME spot price$118.605th–95th percentile$21 – $222Water value$94.85Water value − mean shadow$−35.92Storage 0.8–1.0 TWhMean shadow price$95.64Mean ADME spot price$103.255th–95th percentile$29 – $189Water value$94.85Water value − mean shadow$−0.79Storage 1.0–1.2 TWhMean shadow price$91.43Mean ADME spot price$88.965th–95th percentile$19 – $167Water value$94.85Water value − mean shadow$3.42Storage 1.2–1.4 TWhMean shadow price$14.80Mean ADME spot price$3.495th–95th percentile$10 – $31Water value$94.85Water value − mean shadow$80.05Storage 1.4–1.6 TWhMean shadow price$4.92Mean ADME spot price$0.005th–95th percentile$0 – $10Water value$94.85Water value − mean shadow$89.93Storage 1.6–1.8 TWhMean shadow price$4.19Mean ADME spot price$6.715th–95th percentile$0 – $28Water value$94.85Water value − mean shadow$90.66Storage 1.8–2.0 TWhMean shadow price$3.46Mean ADME spot price$2.925th–95th percentile$0 – $6Water value$94.85Water value − mean shadow$91.39HOURLY SHADOW PRICE AGAINST STORAGEBY STORAGE BAND, 0.2 TWH WIDE−1000100200300−10001002003000.40.60.81.01.21.41.61.82.00.50.70.91.11.31.51.71.9Storage (TWh)Band centre (TWh)mean water value $94.85/MWh across 8,760 hoursStorage 0.4–0.6 TWhMean shadow price$149.32Mean ADME spot price$140.155th–95th percentile$36 – $222Water value$94.85Water value − mean shadow$−54.47Storage 0.6–0.8 TWhMean shadow price$130.77Mean ADME spot price$118.605th–95th percentile$21 – $222Water value$94.85Water value − mean shadow$−35.92Storage 0.8–1.0 TWhMean shadow price$95.64Mean ADME spot price$103.255th–95th percentile$29 – $189Water value$94.85Water value − mean shadow$−0.79Storage 1.0–1.2 TWhMean shadow price$91.43Mean ADME spot price$88.965th–95th percentile$19 – $167Water value$94.85Water value − mean shadow$3.42Storage 1.2–1.4 TWhMean shadow price$14.80Mean ADME spot price$3.495th–95th percentile$10 – $31Water value$94.85Water value − mean shadow$80.05Storage 1.4–1.6 TWhMean shadow price$4.92Mean ADME spot price$0.005th–95th percentile$0 – $10Water value$94.85Water value − mean shadow$89.93Storage 1.6–1.8 TWhMean shadow price$4.19Mean ADME spot price$6.715th–95th percentile$0 – $28Water value$94.85Water value − mean shadow$90.66Storage 1.8–2.0 TWhMean shadow price$3.46Mean ADME spot price$2.925th–95th percentile$0 – $6Water value$94.85Water value − mean shadow$91.39
Mean shadow priceMean ADME spot priceWater value

Methodology

The LP model is a stylized representation of hydro systems that trade into neighboring markets. Results are illustrative rather than predictive. The model objective is to maximize annual trading margin over 8,760 hourly periods. This is achieved by scheduling hydro generation, other generation, imports and exports, and storage levels while respecting physical and operational constraints. In total, the model has 78,840 variables, and a full year solves in 1.8 to 4.5 seconds across the five full-year runs behind this case study and its companions.

Model formulation

The model formulation is:

maximize TradingMargin:

+
+

Revenue and trading costs

Trading revenue: Exports sold and imports bought at each market's hourly price, which can be negative.

pm,t

Price at each market in each hour $/MWh parameter · price

Xm,t

Exports sold to each market MW variable · X

Ym,t

Imports bought from each market MW variable · Y

Revenue and trading costs

Wheeling cost: The all-in cost of scheduling to or from a market, paid on flows both ways.

wm

Wheeling cost to or from each market $/MWh parameter · wheel

Xm,t

Exports sold to each market MW variable · X

Ym,t

Imports bought from each market MW variable · Y

Revenue and trading costs

Offer-curve discount: Each deeper tranche of exports sells at a wider discount to the price.

δm,k

Discount on each tranche of the offer curve $/MWh parameter · disc

Xkm,k,t

Exports at each tranche of the offer curve MW variable · Xk

Revenue and trading costs

Bid-curve premium: Each deeper tranche of imports costs a higher premium over the price.

βm,b

Premium on each tranche of the bid curve $/MWh parameter · bid_prem

Ykm,b,t

Imports at each tranche of the bid curve MW variable · Yk

Operating costs and penalties

Shortfall penalty: A penalty on each MWh the run ends below its storage target.

πshort

Penalty per MWh of storage shortfall $/MWh parameter · short_penalty

Short

Energy the run ends below its storage target MWh variable · Short

Operating costs and penalties

Thermal running cost: The running cost of each thermal unit's output.

cu

Running cost of each thermal unit $/MWh parameter · th_cost

Thu,t

Thermal generation, by unit MW variable · Th

Operating costs and penalties

Reserve penalty: A penalty on each MWh of the deepest draw below the reserve floor.

πreserve

Penalty per MWh of the deepest reserve draw $/MWh parameter · reserve_penalty

Below

The deepest draw below the reserve floor, across all hours MWh variable · Below

Operating costs and penalties

Spill tie-breaker: A token cost on water released without generating, so that storing it wins a tie with spilling it.

cspill

Token cost on water released without generating $/MWh parameter · spill_cost

Spillt

Water released without generating MWh/h variable · Spill

Terminal value

Terminal value, curve: The water left at the end, valued on a concave curve of final storage.

Tv

Value of the water left at the end $ variable · Tv

Terminal value

Terminal value, flat: Or valued at a flat rate per MWh. A run uses one terminal value or neither, never both; full-year runs use neither and hold the storage target instead.

v

Flat water value $/MWh parameter · wv

SN

Storage at the end of the last hour MWh variable · S

Revenue and trading costs

4 terms: What the schedule earns at market, less what moving it there costs.

Operating costs and penalties

4 terms: Fuel, and the soft limits a run may buy its way past.

Terminal value

2 terms: What the water left in storage is worth when the run stops.

Indices: m market, t hour from 1 to N, k and b tranches of the offer and bid curves, u thermal unit. A term is zero in any case that supplies no data for it; the constraints below show which cases do.
Learn more about the model variables, constraints, and assumptions

Model Variables

The model includes 78840 variables to represent the electric system and trading environment across two jurisdictions.

Every hour

Gt
Hydro generation MWFrom the must-run minimum to capacity
St
Storage at the end of the hour MWhBetween the reservoir floor and ceiling
Spillt
Water released without generating MWh/hUp to the spill limit, where a case sets one
Xm,t
Exports sold to each market MWUp to that market's export limit
Ym,t
Imports bought from each market MWUp to that market's import limit
Xkm,k,t
Exports at each tranche of the offer curve MWUp to the tranche's depth
Ykm,b,t
Imports at each tranche of the bid curve MWUp to the tranche's depth
Thu,t
Thermal generation, by unit MWUp to the unit's capacity
Curtailt
Must-run energy curtailed rather than generated MWUp to that hour's must-run surplus

Once per run

Short
Energy the run ends below its storage target MWhUp to the target itself
Below
The deepest draw below the reserve floor, across all hours MWhUp to the reserve itself
Tv
Value of the water left at the end $No more than the lowest cut at final storage, and zero when a run has no cuts

Constraints

Model constraints define the feasible solution space the model can solve for. Our model includes 12 constraints to represent the electric system and trading environment across two jurisdictions.

Constraints

All model runs: British Columbia and Uruguay

Storage balance

Water carried hour to hour: storage plus inflow, less generation and spill. Its shadow price is the value of stored energy — the number the model exists to produce.

In the model

subject to StorageBalance {t in T}:
    S[t] = (if t = 1 then S0 else S[t-1]) + inflow[t] - G[t] - Spill[t];
Nature
Physical
If pushed past
Hard — always holds
Shadow price
Value of stored energy

All model runs: British Columbia and Uruguay

Energy balance

Hydro and thermal generation, must-run supply net of curtailment, and imports meet load plus exports in every hour. Its shadow price is the system marginal price. In British Columbia only the hydro and trade terms are non-zero.

In the model

subject to EnergyBalance {t in T}:
    G[t] + sum {u in U} Th[u,t] + surplus[t] - Curtail[t]
      + sum {m in M} Y[m,t] = load[t] + sum {m in M} X[m,t];
Nature
Physical
If pushed past
Hard — always holds
Shadow price
System marginal price

All model runs: British Columbia and Uruguay

Shared path limits

Markets that ride the same physical intertie compete for it. In British Columbia 3 markets share 2 paths, so the California and Mid-C schedules are not independent.

In the model

subject to PathExport {p in P, t in T}:
    sum {m in M: path_of[m] = p} X[m,t] <= path_exp_cap[p];
subject to PathImport {p in P, t in T}:
    sum {m in M: path_of[m] = p} Y[m,t] <= path_imp_cap[p];
Nature
Physical
If pushed past
Hard — always holds

All model runs: British Columbia and Uruguay

Ramp limits

Output cannot move more than a set amount from one hour to the next — 2,500 MW in British Columbia — which is what stops the schedule chasing every price spike. A rolling run ramps its first hour from the last hour it actually committed, so windows join without a jump.

In the model

subject to RampUp {t in T: t > 1 or has_prev = 1}:
    G[t] - (if t = 1 then G_prev else G[t-1]) <= ramp_max;
subject to RampDown {t in T: t > 1 or has_prev = 1}:
    (if t = 1 then G_prev else G[t-1]) - G[t] <= ramp_max;
Nature
Operating
If pushed past
Hard — always holds

All model runs: British Columbia and Uruguay

End-of-horizon storage

The run must finish with at least a target volume in storage. Without it the model empties the reservoir into the last high-price hours and books the proceeds. Falling short is allowed at a penalty per MWh, so a dry year finishes low rather than making the problem infeasible.

In the model

var Short >= 0, <= S_end_min;
subject to EndStorage:
    S[N] + Short >= S_end_min;
Nature
Planning target
If pushed past
Soft — penalised per MWh short

Uruguay

Reserve floor

Storage is held above an operating reserve — Rincón del Bonete kept above a set level over its physical floor. It is a policy assumption, not a physical limit, and it can be breached: the deepest draw into it is penalised per MWh.

In the model

var Below >= 0, <= reserve;
subject to ReserveFloor {t in T}:
    S[t] + Below >= Smin + reserve;
Nature
Policy assumption
If pushed past
Soft — deepest draw penalised per MWh

Uruguay

Must-run and curtailment

Salto Grande, wind, solar and biomass generate whether or not they are wanted. When they exceed domestic demand the excess is exported or curtailed, and the model may curtail no more than that excess.

In the model

var Curtail {t in T} >= 0, <= surplus[t];
Nature
Physical
If pushed past
Hard — always holds

Uruguay

Thermal dispatch

Thermal plant comes in blocks, each with its own capacity and cost, so fuel is burned only when it is cheaper than the water or the import it replaces.

In the model

var Th {u in U, T} >= 0, <= th_cap[u];
Nature
Physical
If pushed past
Hard — always holds

Uruguay

Offer and bid curves

Exports are sold down an offer curve and imports bought up a bid curve: each further tranche earns less or costs more. It relaxes the price-taker assumption — selling more lowers what the next MWh fetches.

In the model

subject to OfferLink {m in M, t in T: card(K) > 0}:
    X[m,t] = sum {k in K} Xk[m,k,t];
subject to BidLink {m in M, t in T: card(KB) > 0}:
    Y[m,t] = sum {kb in KB} Yk[m,kb,t];
Nature
Market assumption
If pushed past
Hard — always holds

Uruguay

Transit limit

Imports may not exceed domestic load in any hour. It is a stated assumption — the interties serve Uruguay, not a corridor between Brazil and Argentina — and deliberately not fitted to observed flows. Lifted, the difference is what free wheeling between the two neighbours would be worth, which is not a hydro trading result.

In the model

subject to TransitLimit {t in T}:
    sum {m in M} Y[m,t] - load[t] <= transit_cap;
Nature
Stated assumption
If pushed past
Hard — always holds

Uruguay

Spill limit

Water released without generating is capped at the largest spill the cascade has actually passed, with headroom. Spill also carries a token cost — a tie-breaker, not an economic cost — so storing beats dumping when water is otherwise free.

In the model

var Spill {T} >= 0, <= spill_max;
Nature
Physical
If pushed past
Hard — always holds

Rolling-horizon runs

Terminal water value

Water left at the end of each window is valued on a concave curve of storage, built from tangent lines, so a short window neither hoards water nor dumps it to book a quick sale.

In the model

var Tv <= (if card(C) > 0 then Infinity else 0);
subject to CutCurve {c in C}:
    Tv <= cut_slope[c] * S[N] + cut_int[c];
Nature
Valuation
If pushed past
Not a limit — prices the water left

Assumptions

The LP model is a demonstration only, not a full representation of the system. It reduces dimensionality by aggregating all the hydro plants into a single reservoir, and it ignores environmental flow constraints, head effects, and river routing.

Key assumptions of the model include:

  • The trading of gas, environmental attributes, and other commodities are excluded.
  • Only the markets modelled for a system exist. Where it actually trades beyond them, that energy has nowhere to go, which pulls the modelled capture price below the realized one.
  • Must-take supply — independent power, wind, solar, biomass, and treaty exchanges — is netted off load rather than dispatched. The model absorbs it and cannot move it.
  • Domestic load is inelastic: served in every hour, and never shed.
  • Trade is limited only by the rating of each path. There are no scheduling rules, transmission products, counterparty or credit limits, and no distinction between day-ahead and real time, so buying and selling in the same hour costs only the wheeling charge.
  • Losses are carried entirely by that flat wheeling charge, and no reserve, ancillary-service or capacity obligation applies beyond the storage floor.
  • Every hour is alike: no unit commitment, no start costs, no outages or maintenance, and nothing below hourly resolution.
  • British Columbia's storage plants are aggregated into a single reservoir, and its walls — a ceiling of 25 TWh and a floor of 3 TWh — are illustrative rather than published capacities. Neither is reached: the reference year turns between 6.8 and 20.8 TWh, so the bounds decide nothing and the storage target does.
  • Intertie ratings, wheeling charges and the 3,000 MW minimum generation floor are likewise illustrative parameters rather than filed ratings or tariffs. The floor moves the answer more than any other assumed parameter, and it is the one with no source behind it.
  • Storage is fixed at both ends of a run, so the reservoir path follows from that target rather than predicting it.
  • Where inflow is not published it is backed out of the storage balance, which makes it net of evaporation and seepage, and flat within each day.

Data sources

Our LP model is built on and verified against published data, using synthetic data where necessary. British Columbia's within-year inflow shape is synthetic, and its reservoir bounds, intertie ratings, wheeling charges and 3,000 MW generation floor are illustrative parameters rather than measurements. Uruguay's inflow is back-calculated from observed levels, generation and spill. Our model is built from operator and regulator data availalbe at the sources in the list below. here a series is published, back-calculated from the observed balance where it is not (inflow, in all three), and synthetic only in BC's within-year shapes, whose annual totals come from the audited Annual Report. Assumed parameters are labelled as assumptions and are never fitted to the series used to validate. The model code is checked against thirteen fixtures with answers computed outside AMPL; the results are scored against published prices, published monthly volumes, and — for Uruguay — ADME's own published water value.

Expand the list of references and data sources

The table below identified data sources used for this case study with links to the original source.

Hourly market data

  • [1]

    CAISO OASIS — Malin 500 kV node MALIN_5_N101, day-ahead

    The hourly California price. OASIS keeps only a rolling window, so the archive here cannot be re-fetched.

  • [2]

    CAISO OASIS — BPA EIM aggregation point ELAP_BPAT-APND, real-time

    The Mid-Columbia price, and the model's largest market.

  • [3]

    AESO public pool price API

    The hourly Alberta price, published in Canadian dollars.

  • [4]

    BC Hydro balancing authority hourly load report

    The hourly load shape, rescaled to BC Hydro's own integrated system.

Published accounts

  • [5]

    BC Hydro: Annual Report 2025/26

    Every annual energy figure the build uses, FY2022 to FY2026, and hydro generation and capacity plant by plant.

  • [6]

    BC Hydro: F2026–F2027 Revenue Requirements Application

    Appendix F Attachment 1, Schedule 4.0 gives system trade at transfer prices, a cross-check on the annual report. Appendix A gives Powerex trade income by year, as a comparator only.

  • [7]

    BC Hydro: Service Plan 2026/27 to 2028/29

    BC Hydro's own exchange-rate and Mid-Columbia forecast assumptions.

  • [8]

    BC Hydro: IPP Supply Lists

    Splits independent power into run-of-river and everything else.

Currency

  • [9]

    Bank of Canada Valet, series FXUSDCAD

    Converts the Alberta pool price to US dollars, day by day. Every price the model sees is in US dollars.

Monthly yardstick (scoring only)

  • [10]

    Statistics Canada tables 25-10-0015-01 and 25-10-0016-01

    Monthly generation by type and producer class, and monthly receipts, deliveries and availability: the hydro shape test, and the interprovincial and US flows.

  • [11]

    Canada Energy Regulator, monthly electricity exports and imports

    The only public monthly value of British Columbia's exports.

ADME, the system operator

  • [12]

    ADME datos abiertos

    Hourly generation, demand and interchange; Río Negro plant telemetry, which gives storage, generation and inflow; thermal output plant by plant; declared plant costs; the sanctioned spot price every validation scores against; ADME's own water value; and Salto Grande flows.

  • [13]

    ADME procedures — P004 on export offers, and Liquidación SPOT y Facturación

    Why a neighbour's price is treated as a ceiling rather than a market, and the basis for the reported price cap. Followed, not fetched.

Neighbouring markets

  • [14]

    ONS open data, CMO semi-horário, southern submarket — one file a year

    Brazil's marginal cost of operation, which is the ceiling on an export offer.

  • [15]

    CAMMESA, Costos Marginales Horarios Operados

    Argentina's hourly marginal cost, served one day to a request.

Currency

  • [16]

    Banco Central do Brasil, SGS series 1 — PTAX sell

    Brazilian reais to US dollars, at the daily rate rather than an annual average.

  • [17]

    BCRA, Comunicación A 3500 wholesale reference

    Argentine pesos to US dollars.

  • [18]

    Banco Central del Uruguay, interbank reference

    Uruguayan pesos to US dollars.

Model Improvements

The most important number on this page is not a result. It is a bound.

  • Perfect foresight

    The model sees every future price before it schedules anything. Its $116M trading margin is therefore an upper bound on what any strategy could have captured, not a forecast, a target, or an achievable figure. Its value is as a ceiling to measure real decisions against. Rolling-horizon and stochastic versions are the obvious next step, and they will return a smaller number.

  • Higher resolution of reservoir inflow and dispatch within the energy system

    The model includes no cascade, no head effect, no river routing, and no environmental flow constraints beyond a flat minimum. Real systems are a network of reservoirs with local conditions that bind at different times.

  • Gross-flow path limits

    Counter-flows are not netted and losses are not modelled, so the interties are treated more strictly than they behave.

  • Price-taker

    9,099 GWh moves into these markets without shifting a single price. At this volume that is the least defensible assumption here, and it biases the margin upward on top of the foresight.

More information

The code behind this case study — the model formulation in AMPL, the Python wrappers that build its inputs and iterate it, and the results of the runs reported here — is on GitHub.

Let's connect if you want to discuss how LP can be applied to your own optimization problems.