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.
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.
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.
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
Net position: model −9,083 GWh against −7,838 reported
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.
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.
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.
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
Net position: model −450 GWh against −1,159 reported
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.
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.
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.
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.
Price at each market in each hour $/MWh parameter · price
Exports sold to each market MW variable · X
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.
Wheeling cost to or from each market $/MWh parameter · wheel
Exports sold to each market MW variable · X
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.
Discount on each tranche of the offer curve $/MWh parameter · disc
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.
Premium on each tranche of the bid curve $/MWh parameter · bid_prem
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.
Penalty per MWh of storage shortfall $/MWh parameter · short_penalty
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.
Running cost of each thermal unit $/MWh parameter · th_cost
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.
Penalty per MWh of the deepest reserve draw $/MWh parameter · reserve_penalty
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.
Token cost on water released without generating $/MWh parameter · spill_cost
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.
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.
Flat water value $/MWh parameter · wv
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.
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
- Hydro generation MWFrom the must-run minimum to capacity
- Storage at the end of the hour MWhBetween the reservoir floor and ceiling
- Water released without generating MWh/hUp to the spill limit, where a case sets one
- Exports sold to each market MWUp to that market's export limit
- Imports bought from each market MWUp to that market's import limit
- Exports at each tranche of the offer curve MWUp to the tranche's depth
- Imports at each tranche of the bid curve MWUp to the tranche's depth
- Thermal generation, by unit MWUp to the unit's capacity
- Must-run energy curtailed rather than generated MWUp to that hour's must-run surplus
Once per run
- Energy the run ends below its storage target MWhUp to the target itself
- The deepest draw below the reserve floor, across all hours MWhUp to the reserve itself
- 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.
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
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.