Skip to main content

Buy Low, Sell High - Optimal Electricity Storage Cost Arbitrage

As more and more solar and wind generation capacity is integrated into a power grid without sufficient storage, the intra-day electricity market prices tend to fluctuate between near-zero when there is an over-supply of renewable energy and quite expensive when fossil fuel power plants need to be brought online to cover the demand. The fewer hours these plants run and the more expensive the fossil fuel becomes the more the range of this short term fluctuations will increase, unless they can potentially be substituted by a cheaper solution. 

Using electricity market data from the ENTSO-E transparency platform, we can determine what would be the maximum profit the operator of a hypothetical storage unit could make by deploying an optimal strategy of buying low and selling high.

The storage is assumed to have one unit of power, i.e. in this case it could either charge or discharge one MWh of energy in a given hour. The storage system can have a capacity of n hours and a round-trip efficiency of e, where half of which is assumed to be dissipated during charging and discharging respectively. We are also ignoring any effects of losses or congestion in the transmission network.

We are also assuming that the storage system is small enough to be able to follow along the market without moving it and it can use the perfect knowledge of the day-ahead forward market to choose the optimal charge and discharge points. While perfect market knowledge might not be realistic, renewable energy production in aggregate is relatively predictable - following roughly the accuracy of weather forecasting.

The following shows the maximum profit a storage system of capacity 1 (1MW/1MWh) and efficiency of 90% could have made in a few different European markets during the years 2019 and 2022 respectively:


Country Profit Cycles Present value
(10 y, 5%)
Germany €11,707.56 731 €90,402.67
Switzerland €6,672.77 610 €51,525.36
Spain €5,103.18 570 €39,405.40
France €10,890.36 795 €84,092.47
Netherlands €10,438.41 733 €80,602.64
Denmark (East) €9,032.15 626 €69,743.87


Country Profit Cycles Present value
(10 y, 5%)
Germany €75,791.35 729 €585,240.71
Switzerland €44,048.61 639 €340,131.69
Spain €36,046.15 701 €278,338.82
France €68,775.37 782 €531,065.18
Netherlands €89,301.90 846 €689,565.60
Denmark (East) €79,355.55 749 €612,762.52

The year 2019 represents the last year of the cheap and abundant energy in Europe, not disrupted by pandemic or war, while 2022 was characterised by supply shortages and high fossil fuel prices. While in the years up to 2019 electricity would have been to cheap for any existing storage technology to be profitable for short-term storage arbitrage, the price landscape of 2022 would allow to quickly amortise even the still relatively expensive lithium-ion batteries, which today are mostly deployed at grid scale to provide more lucrative ancilliary services.

For the foreseeable future, times of cheap and abundant natural gas will not likely return in Europe, even if prices drop from the 2022 peaks. From this we can anticipate that electricity arbitrage could be a profitable use-case for a stand-alone battery electric storage systems in many European power grids - as long as this would be permitted by the local market regulation. Given that wind and solar producers will face increasing levels of curtailment of their potential production which cannot be absorbed by the transmission networks, storage will likely be co-located to form hybrid wind/solar + storage power plants that connect to the grid with a firmer delivery profile.

In order to determine the optimal charge & discharge intervals in the market data time series, we use the dynamic programming optimisation algorithm below:

# Stores the intermediate results of the optimisation process
# value: profit from completed trades so far, minus the cost base of current charge
# cost: cost base for current charge in storage
# count: number of charge or discharge events so far
OptimisationState = namedtuple("OptimisationState", "value cost count")

def max_value(a, b):
    return max(a,b, key= lambda item : item.value if item else -math.inf)

def charge(state, rate, efficiency):
    # Assuming half of the storage round-trip inefficiency is lost during charging,
    # leading to a higher cost
    charge_cost = rate * (1 + (1 - efficiency) / 2)
    return OptimisationState(state.value - charge_cost,
                             state.cost + charge_cost, state.count + 1)

def discharge(state, rate, efficiency, soc):
    # Assuming half of the storage round-trip inefficiency is lost during discharge,
    # leading to a lower profit
    discharge_proceeds = rate * (1 - (1 - efficiency) / 2)
    # Use averaging for cost base, dividing total cost by the current state of charge
    return OptimisationState(state.value + discharge_proceeds,
                             state.cost - state.cost / soc, state.count + 1)

def simulate_interval(prev, rate, efficiency):
    """Simulate storage system optimisation for one step of the time series.

     Storage can either hold, charge or discharge one unit of energy
     during this interval.

            prev: state of the optimisation before this interval
            rate: unit cost of energy for this interval
            efficiency: round-trip storage efficiency
             new state of the optimisation after this interval
    # simulate all holds (nothing changes)
    current = prev.copy()

    # simulate all charges
    for idx, elem in enumerate(prev[:-1]):
        if elem:
            current[idx + 1] = max_value(current[idx + 1],
                                         charge(elem, rate, efficiency))

    # simulate all discharges
    for idx, elem in enumerate(prev[1:], start=1):
        if elem:
            current[idx - 1] = max_value(current[idx - 1],
                                         discharge(elem, rate, efficiency, idx))

    return current

def maximize_profit(time_series, capacity, efficiency):
    """Maximize profit from energy arbitrage.

        Using an idealised storage model and perfect knowledge of the market.

            time_series: Pandas time-series of day-ahead electricity market prices.
            capacity:  Storage capacity in multiples of the energy unit of the time series.
            efficiency: Round-trip storage efficiency
            Tuple of profit and number of charge/discharge cycles,
            resulting from optimal charge/discharge pattern
    # initialise empty system state
    # (One OptimisationState record per possible state-of-charge value)
    state = [None] * (capacity + 1)
    state[0] = OptimisationState(0, 0, 0)

    for index, value in time_series.items():
        state = simulate_interval(state, value, efficiency)
        #print (index, value, state)
    return (state[0].value, state[0].count/2)