Imagine a battery entering a market interval with 0.5 MWh of energy. The current price is 20 Euro/MWh, but the expected marginal value of having another MWh available in the next stage is 55 Euro/MWh.

Even though the current price is positive, the optimal decision is to charge i.e. spend money right now.

I wanted to understand where this decision comes from before letting an optimization library solve it for me. So I worked through a small three-stage battery problem using stochastic dynamic programming by hand.

The Julia package SDDP.jl models decision points as nodes. In a linear policy graph, these nodes correspond to stages along the time axis: https://sddp.dev/stable/tutorial/first_steps/

Model and information

With ete_t the SoC of the battery in MWh entering stage tt and ptp_t the grid-side battery power in MW. Grid-side means: pt>0p_t > 0 the battery discharges and vice versa.

To keep things simple, for a one hour settlement period and 100% efficiency:

et+1=et−pt e_{t+1} = e_t - p_t

Since Δt=1\Delta t = 1 h, I suppress Δt\Delta t below. More generally:

et+1=et−ptΔt e_{t+1} = e_t - p_t \Delta t

and stage P&L is πtptΔt\pi_t p_t \Delta t.

Let’s go through this for a 1 MW / 1 MWh battery:

et∈[0,1],pt∈[−1,1]. e_t \in [0,1], \qquad p_t \in [-1,1].

State (SoC) transitions are therefore:

et+1=et−pt. e_{t+1} = e_t - p_t.

At the beginning of each stage, before making our trading decision, we observe the price of the given stage πt\pi_t. Before observing the price of the stage, we track multiple price scenarios per stage, e.g.:

πt={10with probability 0.5100with probability 0.5 \pi_t = \begin{cases} 10 & \text{with probability } 0.5 \\ 100 & \text{with probability } 0.5 \end{cases}

For this toy example, assume the price scenarios are independent across stages.

Across all stages we want to maximize our expected P&L:

max⁡E[∑tπtpt]. \max \mathbb{E} \left[ \sum_t \pi_t p_t \right].

The Bellman equation or value to go at time tt, i.e. the maximal expected P&L from stage tt onward after the current price πt\pi_t has been observed:

Vt(et,πt)=max⁡pt,et+1{πtpt+Eπt+1[Vt+1(et+1,πt+1)]} V_t(e_t,\pi_t) = \max_{p_t,e_{t+1}} \left\{ \pi_t p_t + \mathbb{E}_{\pi_{t+1}} \left[ V_{t+1}(e_{t+1},\pi_{t+1}) \right] \right\}

subject to:

et+1=et−pt. e_{t+1}=e_t-p_t.

Here, we choose dispatch / market position ptp_t. Once we choose ptp_t, the SoC at the beginning of stage t+1t+1 is determined by et+1=et−pte_{t+1}=e_t-p_t, so let’s write:

Vt(et,πt)=max⁡pt{πtpt+Eπt+1[Vt+1(et−pt,πt+1)]}. V_t(e_t,\pi_t) = \max_{p_t} \left\{ \pi_t p_t + \mathbb{E}_{\pi_{t+1}} \left[ V_{t+1}(e_t-p_t,\pi_{t+1}) \right] \right\}.

Let’s introduce a new term that summarizes the expectation:

Qt+1(et+1)=Eπt+1[Vt+1(et+1,πt+1)]. Q_{t+1}(e_{t+1}) = \mathbb{E}_{\pi_{t+1}} \left[ V_{t+1}(e_{t+1},\pi_{t+1}) \right].
  • Vt(et,πt)V_t(e_t,\pi_t): I have entered stage tt, I know my SoC, and I have already observed the current price. What is the maximum P&L I can make from here onward?
  • Qt(et)Q_t(e_t): I know my SoC entering stage tt, but I have not yet observed the stage-tt price. What is my expected value from here onward?

In my notation, Qt(et)Q_t(e_t) is the expected value entering stage tt before observing πt\pi_t. Viewed from stage t−1t-1, Qt(et)Q_t(e_t) is the expected continuation value attached to the outgoing state ete_t.

SDDP.jl calls this expected successor value the value function, explicitly distinguishing it from Vt(et,πt)V_t(e_t,\pi_t).

With this we can rewrite the Bellman equation as:

Vt(et,πt)=max⁡pt{πtpt+Qt+1(et−pt)}. V_t(e_t,\pi_t) = \max_{p_t} \left\{ \pi_t p_t + Q_{t+1}(e_t-p_t) \right\}.

Let’s solve a three-stage problem using this Bellman formulation.

Suppose at stage 1 we already observed price:

π1=40 Euro/MWh. \pi_1 = 40 \text{ Euro/MWh}.

For stage 2 we have the following price scenarios:

π2={20with probability 0.580with probability 0.5 \pi_2 = \begin{cases} 20 & \text{with probability } 0.5 \\ 80 & \text{with probability } 0.5 \end{cases}

For stage 3 our price scenarios are:

π3={10with probability 0.5100with probability 0.5 \pi_3 = \begin{cases} 10 & \text{with probability } 0.5 \\ 100 & \text{with probability } 0.5 \end{cases}

Stage 3: terminal value

With dynamic programming we always start at the end of the time axis, i.e. we define the terminal continuation value right after the last stage we are interested in:

Q4(e4)=0. Q_4(e_4)=0.

So for any SoC at the end of stage 3, which would be the SoC we enter stage 4 with, we prescribe zero continuation value. We do so because in this toy problem we assign no terminal or salvage value to energy left after stage 3.

With this, we can now write the value to go for stage 3 as:

V3(e3,π3)=max⁡p3π3p3. V_3(e_3,\pi_3) = \max_{p_3} \pi_3 p_3.

At this point, the beginning of stage 3, both e3e_3 and π3\pi_3 are known since at the beginning of each stage we resolve the current-stage price uncertainty and observe the price.

The only unknown here now is p3p_3, which is constrained by the limits of the battery:

−1≤p3≤1 -1 \leq p_3 \leq 1

0≤e4=e3−p3≤1. 0 \leq e_4=e_3-p_3 \leq 1.

So we cannot simply choose an arbitrary p3p_3 here. We need to respect where the SoC of the battery comes from at the beginning of stage 3, e3e_3.

Imagine e3=0.6e_3=0.6 MWh. From the SoC constraint our feasible power range becomes:

−0.4≤p3≤0.6. -0.4 \leq p_3 \leq 0.6.

What would we choose for the different price scenarios in stage 3?

π3={10 Euro/MWh→p3=0.6 MW→V3(0.6,10)=6 Euro100 Euro/MWh→p3=0.6 MW→V3(0.6,100)=60 Euro \pi_3 = \begin{cases} 10 \text{ Euro/MWh} \rightarrow p_3=0.6 \text{ MW} \rightarrow V_3(0.6,10)=6 \text{ Euro} \\ 100 \text{ Euro/MWh} \rightarrow p_3=0.6 \text{ MW} \rightarrow V_3(0.6,100)=60 \text{ Euro} \end{cases}

So the value of the SoC entering stage 3, e3e_3, is given by how much we can discharge in stage 3, because leaving energy in the battery has no terminal value, and the price we observe in stage 3, π3\pi_3.

Before observing the price of stage 3 we can only work out the expected continuation value from and including stage 3:

Q3(e3=0.6 MWh)=Eπ3[V3(0.6,π3)]=0.5⋅6 Euro+0.5⋅60 Euro=33 Euro. \begin{aligned} Q_3(e_3=0.6\text{ MWh}) &= \mathbb{E}_{\pi_3} \left[ V_3(0.6,\pi_3) \right] \\ &= 0.5 \cdot 6 \text{ Euro} + 0.5 \cdot 60 \text{ Euro} \\ &= 33 \text{ Euro}. \end{aligned}

For an arbitrary beginning of stage 3 SoC this is:

Q3(e3)=0.5(10e3)+0.5(100e3)=55e3. Q_3(e_3) = 0.5(10e_3) + 0.5(100e_3) = 55e_3.

This is the continuation value function of stage 3. Taking its derivative with respect to the SoC at the beginning of stage 3:

∂Q3∂e3=55 Euro/MWh. \frac{\partial Q_3}{\partial e_3} = 55 \text{ Euro/MWh}.

This derivative tells us that each additional MWh stored in the battery at the beginning of stage 3 is worth an additional 55 Euro in expected future P&L. That 55 Euro/MWh is the marginal continuation value of stored energy.

Stage 2: charge or discharge?

Now let’s move to stage 2: Suppose we enter stage 2 with SoC e2=0.5e_2=0.5 MWh and observe price π2=20\pi_2=20 Euro/MWh. What does the Bellman problem tell us we should do?

The Bellman equation / the value to go for stage 2 is:

V2(e2,π2)=V2(0.5,20)=max⁡p2{20p2+Q3(e3)}=max⁡p2{20p2+Q3(e2−p2)}=max⁡p2{20p2+55(e2−p2)}=max⁡p2{55e2−35p2}=max⁡p2{27.5−35p2}. \begin{aligned} V_2(e_2,\pi_2) &= V_2(0.5,20) \\ &= \max_{p_2} \left\{ 20p_2+Q_3(e_3) \right\} \\ &= \max_{p_2} \left\{ 20p_2+Q_3(e_2-p_2) \right\} \\ &= \max_{p_2} \left\{ 20p_2+55(e_2-p_2) \right\} \\ &= \max_{p_2} \left\{ 55e_2-35p_2 \right\} \\ &= \max_{p_2} \left\{ 27.5-35p_2 \right\}. \end{aligned}

This is the objective we are maximizing at stage 2:

objective=27.5−35p2 \text{objective}=27.5-35p_2

subject to power and energy constraints:

−1≤p2≤1 -1 \leq p_2 \leq 1

and:

0≤e3=e2−p2=0.5−p2≤1. 0 \leq e_3=e_2-p_2=0.5-p_2 \leq 1.

This gives us the following constraints we need to respect:

0≤0.5−p2  ⟹  p2≤0.5 0 \leq 0.5-p_2 \implies p_2 \leq 0.5 0.5−p2≤1  ⟹  p2≥−0.5. 0.5-p_2 \leq 1 \implies p_2 \geq -0.5.

So our feasible power range in stage 2 is:

−0.5≤p2≤0.5 -0.5 \leq p_2 \leq 0.5

meaning our most extreme choices are charge, negative p2p_2, at 0.5 MW and discharge, positive p2p_2, at 0.5 MW.

The objective and constraints tell us to charge the battery in stage 2 up to its maximum SoC of 1 MWh:

p2=−0.5 MW. p_2=-0.5\text{ MW}.

This is not surprising because we already worked out that the marginal continuation value of stored energy in stage 3 is:

∂Q3∂e3=55 Euro/MWh. \frac{\partial Q_3}{\partial e_3} = 55 \text{ Euro/MWh}.

Given that charging in stage 2 costs us 20 Euro/MWh while the marginal continuation value of that extra MWh at the beginning of stage 3 is 55 Euro/MWh, we obviously charge in stage 2. Charging another MWh adds:

55−20=35 Euro/MWh 55-20=35 \text{ Euro/MWh}

of expected P&L.

Imagine we had observed π2=80\pi_2=80 Euro/MWh coming into stage 2 at the same SoC e2=0.5e_2=0.5 MWh.

Then the objective would have been to maximize:

27.5+25p2 27.5+25p_2

and it would have been more lucrative to discharge 0.5 MW in stage 2. The difference between the current market value in stage 2 and marginal continuation value of stage 3 gives the same result:

80−55=25 Euro/MWh. 80-55=25 \text{ Euro/MWh}.

Since p2>0p_2>0 means discharge, the positive coefficient tells us to discharge.

So the tradeoff we consider at each stage is: market value of our energy in the current stage vs its marginal continuation value in the next stage.

Let us formulate the pre-price value function and derive the marginal continuation value for stage 2.

For every e2∈[0,1]e_2\in[0,1], our 1 MW battery can reach either SoC boundary within the one-hour stage. At 20 Euro/MWh we therefore charge to e3=1e_3=1, which gives:

p2=e2−1. p_2=e_2-1.

At 80 Euro/MWh we discharge to e3=0e_3=0, which gives:

p2=e2. p_2=e_2.

Then:

Q2(e2)=0.5V2(e2,20)+0.5V2(e2,80)=0.5[20(e2−1)+Q3(1)]+0.5[80e2+Q3(0)]=0.5[20(e2−1)+55]+0.5[80e2]=0.5[35+20e2]+40e2=17.5+50e2. \begin{aligned} Q_2(e_2) &= 0.5V_2(e_2,20) + 0.5V_2(e_2,80) \\ &= 0.5 \left[ 20(e_2-1) + Q_3(1) \right] + 0.5 \left[ 80e_2 + Q_3(0) \right] \\ &= 0.5 \left[ 20(e_2-1)+55 \right] + 0.5 \left[ 80e_2 \right] \\ &= 0.5 \left[ 35+20e_2 \right] + 40e_2 \\ &= 17.5+50e_2. \end{aligned}

Realize that V2(e2,20)V_2(e_2,20) and V2(e2,80)V_2(e_2,80) are two different π2\pi_2 observations that lead to two different decision branches. For the stage 2 pre-price value function Q2(e2)Q_2(e_2) we average over them.

So the marginal continuation value in stage 2 is:

∂Q2∂e2=50 Euro/MWh. \frac{\partial Q_2}{\partial e_2} = 50 \text{ Euro/MWh}.

Stage 1: optimal expected P&L

Let us now go to the beginning of our optimization horizon, stage 1, where we already observed:

π1=40 Euro/MWh. \pi_1=40 \text{ Euro/MWh}.

At stage 1 the marginal continuation value of stored energy is 50 Euro/MWh, while the observed market price is 40 Euro/MWh:

∂Q2∂e2=50 Euro/MWh>40 Euro/MWh. \frac{\partial Q_2}{\partial e_2} = 50 \text{ Euro/MWh} > 40 \text{ Euro/MWh}.

So if possible given our SoC we will charge our battery in stage 1.

Let us finish the three-stage problem. Suppose we begin stage 1 with:

e1=0.5 MWh. e_1=0.5 \text{ MWh}.

The stage 2 examples above looked locally at e2=0.5e_2=0.5 MWh. Starting the complete three-stage problem from e1=0.5e_1=0.5 MWh, the Bellman problem at stage 1 is:

V1(0.5,40)=max⁡p1{40p1+Q2(0.5−p1)}=max⁡p1{40p1+17.5+50(0.5−p1)}=max⁡p1{42.5−10p1}. \begin{aligned} V_1(0.5,40) &= \max_{p_1} \left\{ 40p_1+Q_2(0.5-p_1) \right\} \\ &= \max_{p_1} \left\{ 40p_1+17.5+50(0.5-p_1) \right\} \\ &= \max_{p_1} \left\{ 42.5-10p_1 \right\}. \end{aligned}

Given e1=0.5e_1=0.5 MWh, our feasible power range is again:

−0.5≤p1≤0.5. -0.5 \leq p_1 \leq 0.5.

So we charge:

p1∗=−0.5 MW p_1^*=-0.5\text{ MW}

and enter stage 2 with:

e2=1 MWh. e_2=1\text{ MWh}.

Our optimal expected P&L for the complete three-stage problem is therefore:

V1(0.5,40)=47.5 Euro \boxed{ V_1(0.5,40)=47.5\text{ Euro} }

It has been fun working through this toy example by hand as it has taught me some intuition regarding:

  • Across our sequential decision making horizon we always need to think in terms of what info we know coming into a stage, before current-stage price uncertainty is resolved, and the state we create at the end of that stage.
  • Important objects QtQ_t, which is the expected value from stage tt before observing the stage-tt price, and VtV_t, the value from stage tt after the current-stage price has been observed.
  • At each stage our decision essentially boils down to comparing the current observed market price and the marginal continuation value of energy in the next state.

Market applications

Where can we apply this to market channel optimization for batteries and other steerable assets?

To my mind, but I need to think about this further, we can probably apply this in at least two phases:

  • The day-ahead positioning of our battery where we need to decide how much of our battery’s limited resources to allocate to market channels such as FCR, aFRR capacity, and day-ahead wholesale. The problem certainly would need to be reformulated so would look differently from the above toy example.
  • Intraday continuous / real time market trading where this could probably be helpful in making decisions near-term in the liquid phase of the limit order book and weigh those decisions against further out less liquid phases of the same limit order book.

In this toy problem we could derive the continuation value functions exactly by hand. For anything approaching a realistic multistage problem, we cannot.

To use this stochastic optimization approach for any meaningful work I will next dive into formulating the above toy example using SDDP.jl and see that this gives me the same results as I got by hand, including the 47.5 Euro optimal expected P&L.