> For the complete documentation index, see [llms.txt](https://docs.voltmasters.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.voltmasters.io/control-algorithms/cost-optimization.md).

# Cost optimization

**Cost optimization** is Voltmasters' price-based control algorithm. It minimizes the **energy cost** of the installation by deciding, every control cycle, how to charge and discharge the battery, based on **forecasted energy prices** and **forecasts of PV production and consumption**.

## How it works

Every control cycle the algorithm:

* **Looks ahead** over the forecast horizon at the dynamic (day-ahead) prices, the expected PV production and the expected consumption.
* **Plans the battery** so energy is stored when it is cheap (or from PV surplus) and used, or injected, when it is expensive. This forward plan is the [battery schedule](/control-algorithms/cost-optimization/battery-schedule.md).
* **Only acts when it pays off:** charging and discharging are planned only when the price difference is large enough to outweigh the battery's round-trip losses (a configurable minimum price difference).
* **Respects the limits:** the battery's minimum/maximum state of charge and the grid connection limits (maximum injection and consumption).

The detailed plan, the event types and a worked example are described on the [Battery schedule](/control-algorithms/cost-optimization/battery-schedule.md) page. The rest of this page explains **how the algorithm reaches those decisions**.

## Two loops: planning and acting

Cost optimization runs as **two cooperating loops** that share one artefact: the [battery schedule](/control-algorithms/cost-optimization/battery-schedule.md).

* The **planning loop** looks at the whole forecast horizon and works out the cheapest charge/discharge plan. It is the "brain". The plan is recomputed regularly, at every new 15-minute slot and whenever a fresh forecast arrives, so it always reflects the latest prices, production and the real battery state.
* The **control loop** runs every control cycle (about once per second). It reads the single schedule entry that covers *now*, turns it into concrete battery, PV and EV setpoints, and adapts those to live measurements and limits. It is the "hands".

```mermaid
flowchart LR
    subgraph inputs["Forecasts and live state"]
        P["Day-ahead prices"]
        PV["PV production forecast"]
        C["Consumption forecast"]
        B["Battery state of charge"]
    end

    GEN["Generate battery schedule<br/>planning loop"]
    SCHED[("Battery schedule")]
    READ["Read entry for now<br/>control loop, approx 1s"]
    SET["Translate to battery / PV / EV setpoints"]
    HW["Battery, PV, grid connection"]

    P --> GEN
    PV --> GEN
    C --> GEN
    B --> GEN
    GEN --> SCHED
    SCHED --> READ
    READ --> SET
    SET --> HW
    HW -. "measured actuals" .-> GEN
```

Splitting the work this way means the expensive look-ahead optimization does not have to run every second, while the fast control loop can still react to reality between plans.

## What the algorithm needs

The plan is built from a mix of **forecasts**, **live measurements** and **project configuration**:

| Input                                                          | Where it comes from                          | Role in the algorithm                                                           |
| -------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------- |
| **Day-ahead consumption & injection prices** (per 15-min slot) | Forecast provider (dynamic/day-ahead market) | Tells the plan *when* energy is cheap to buy and *when* it is lucrative to sell |
| **PV production forecast**                                     | Forecast provider (weather-based)            | Tells the plan when free/cheap solar energy is available to store               |
| **Consumption forecast**                                       | Forecast provider                            | Tells the plan when the site will need energy: the *deficits* worth covering    |
| **Battery state of charge**                                    | Live measurement per battery                 | The starting point of the projected SoC trajectory                              |
| **Battery limits & efficiency**                                | Project configuration                        | Min/max SoC, charge/discharge rate, rated power, round-trip efficiency          |
| **Grid connection limits**                                     | Project configuration                        | Maximum import (charging headroom) and maximum export (injection cap)           |
| **Minimum price difference**                                   | Project configuration                        | The spread a charge/discharge pair must beat to be worth the loss and wear      |
| **Battery injection sales**                                    | Project configuration                        | Whether selling stored energy back to the grid is allowed at all                |

## The core idea: price arbitrage within the limits

At heart the algorithm does **energy arbitrage**: store energy while it is cheap (from PV surplus or low-priced grid hours) and release it while it is expensive, either by covering the site's own consumption or by injecting it back to the grid for revenue.

Two things keep this honest:

* **Round-trip losses.** Energy lost on the way into and out of the battery means a stored kilowatt-hour delivers less than a full kilowatt-hour. The plan sizes every charge and discharge using the battery's round-trip efficiency.
* **The minimum price difference.** A charge/discharge pair is only planned when the price gap between the cheap slot and the expensive slot clears a configurable threshold. This covers the round-trip loss and battery wear, and stops the battery from cycling for a marginal gain.

Everything the optimizer plans must also fit inside the **state-of-charge bounds** and the **grid connection limits**; those constraints are threaded through every step below.

## Building the schedule, step by step

The planning loop turns the forecast horizon into a battery schedule through a fixed pipeline. Each stage adds or refines plans on top of the previous one:

```mermaid
flowchart TD
    A["Forecast horizon<br/>prices · PV · consumption"] --> S1
    S1["1 · Energy sources per slot<br/>PV overshoot + grid headroom"] --> S2
    S2["2 · Identify deficit slots<br/>consumption exceeds PV"] --> S3
    S3["3 · Plan charging for deficits<br/>cheapest affordable source first"] --> S35
    S35["3.5 · Negative-price charging<br/>charge while price is below zero"] --> S375
    S375["3.75 · Injection discharge<br/>sell at a high injection price"] --> S3875
    S3875["3.875 · Non-controllable PV storage<br/>store unavoidable PV surplus"] --> S46
    S46["4–6 · Build entries and project SoC<br/>charge, discharge, idle"] --> OUT[("Battery schedule")]
```

1. **Energy sources per slot.** For every slot the algorithm works out how much energy *could* be stored there and at what price: the **PV overshoot** (solar above the local consumption, energy that would otherwise be exported, priced at the injection price) and the **grid headroom** (how much can still be imported under the grid limit, priced at the consumption price). Both are capped so the combined charging power never exceeds what the batteries can absorb.
2. **Identify deficit slots.** Slots where forecast consumption is higher than forecast PV are **deficits**: the site will need to draw energy. These are the slots worth covering from the battery, and the expensive ones are the prime targets.
3. **Plan charging for deficits.** This is the heart of the optimizer. Walking the deficits, it searches earlier slots for the **cheapest charging sources whose price is at least the minimum price difference below the deficit's price**, so only pairs that clearly pay off are planned. PV is preferred over grid (it is cheaper); grid sources are taken cheapest-first. Every allocation is checked against the projected SoC so the battery is never planned beyond its capacity, and the matching discharge is booked into the deficit slot.
4. **Negative-price charging.** When the consumption price drops **below zero**, you are paid to consume, so the algorithm plans extra charging during those slots, while reserving room for PV that cannot be curtailed, to avoid having to *pay* to inject it later.
5. **Injection discharge.** When selling stored energy is enabled, the algorithm finds slots with an attractive **injection price** (positive and above the minimum price difference) and plans discharges there, **highest price first**. It never discharges below the reserve, and never within the **last four hours of the horizon** (too uncertain to commit storage to). An injection can also be added as a *bonus* on top of an existing deficit discharge.
6. **Non-controllable PV storage.** PV that physically cannot be curtailed, and would otherwise be exported cheaply, is captured and re-allocated to the **most valuable injection slots** instead of being spilled.
7. **Build entries and project SoC.** The plans become timed schedule entries (charge, discharge or idle), each with an **action** and a **goal** (see [Battery schedule](/control-algorithms/cost-optimization/battery-schedule.md)). Remaining slots become idle, or *compensate PV surplus* where free solar still flows into the battery. The state of charge is projected forward across the whole horizon so the complete plan stays inside the min/max bounds, and the entries are sorted by time.

{% hint style="info" %}
When the plan is stored, entries for slots that have already started are **preserved** and merged with the fresh plan, and entries older than 14 days are pruned. This keeps a record of what was actually planned and executed for each slot.
{% endhint %}

## What keeps the plan feasible

Several mechanisms run through the pipeline to keep the schedule physically sound and stable:

* **Projected SoC trajectory.** The algorithm simulates the battery's energy level slot by slot. Charging is never planned above the **maximum SoC**, and discharging never brings the projected SoC below the **effective minimum** (the configured minimum SoC plus any reserve).
* **Recharge segments.** The horizon is split into independent budgets at major PV-charging periods. A deficit *after* a sunny afternoon cannot spend energy that belongs to deficits *before* it. This is what keeps multi-day plans stable instead of shifting around on every regeneration.
* **Grid limits everywhere.** Charging power per slot is capped by the import headroom that is still free under the grid limit; injection and discharge are capped by the export limit. Both PV export and battery injection share the same export budget.
* **Reserves.** Energy set aside (for example for EV power-boost) is treated as off-limits for ordinary discharge, so the plan never spends it.

## From plan to setpoint: the control cycle

The schedule expresses **intent**; the control loop turns it into **action**. Every cycle the cost-optimization algorithm looks up the entry for *now*, reads its **action type** and planned power, and converts that into a setpoint, always clamped to the live state and the grid limits.

```mermaid
flowchart TD
    N["This control cycle"] --> LOOK["Look up schedule entry for now"]
    LOOK --> AT{"Action type?"}
    AT -->|charge_at_max_power| CMAX["Charge at max power<br/>capped by grid import + charge rate"]
    AT -->|follow_scheduled_power| CFOL["Follow planned power<br/>capped by limits"]
    AT -->|compensate_pv_surplus| CPV["Charge with live PV surplus only"]
    AT -->|compensate_production_deficit| DDEF["Discharge to cover the live deficit"]
    AT -->|idle| IDLE["No battery action"]
    CMAX --> APPLY
    CFOL --> APPLY
    CPV --> APPLY
    DDEF --> APPLY
    IDLE --> APPLY
    APPLY["Apply setpoints<br/>respect SoC and grid limits"] --> REC["Record actual vs planned energy"]
```

How each action behaves at runtime:

| Action type                     | What the control loop does                                                                     |
| ------------------------------- | ---------------------------------------------------------------------------------------------- |
| `charge_at_max_power`           | Charge at the maximum power the grid import headroom and charge rate allow                     |
| `follow_scheduled_power`        | Follow the exact planned power (grid charging, or discharging to inject), capped by the limits |
| `compensate_pv_surplus`         | Charge with the **live** PV surplus only, no grid charging                                     |
| `compensate_production_deficit` | Discharge just enough to cover the **live** production deficit                                 |
| `idle`                          | Leave the battery alone                                                                        |

In the same cycle the algorithm also sets the **controllable loads**, the **EV-charger allocation**, and the **PV-inverter setpoints** (curtailing only when needed to respect the export limit).

Because the cycle works from live measurements, it **absorbs forecast error automatically**: the `compensate_*` actions size themselves from what is actually happening right now, while `follow_scheduled_power` and `charge_at_max_power` follow the plan within the limits.

### Closing the loop

After each 15-minute slot completes, the controller records how much was actually charged (from PV and from grid) and discharged, and compares it against the plan. If less energy was stored than planned, a **shortfall adjustment** removes the shortfall from the most expensive target slots first. The next regeneration then re-plans from the **true** battery state, so the strategy continuously self-corrects as reality diverges from the forecast.

## Worked example: evening high prices

The chart below shows the **evening high prices** scenario. Electricity is cheap and sunny during the day and becomes much more expensive in the evening peak.

<figure><img src="/files/Qfd20Gj7Fp3XnYpwtiif" alt="Cost optimization, evening high prices scenario"><figcaption><p>The optimizer charges during the cheap, sunny daytime and discharges into the expensive evening peak, buying low and using high.</p></figcaption></figure>

Reading it against the pipeline: step 2 marks the evening as a string of expensive **deficit slots**; step 3 plans the daytime **charging** (mostly stored PV surplus) to cover them, because the day-to-evening price spread clears the minimum price difference; and the control loop then **discharges** through the peak, with the projected SoC climbing to its maximum during the day and falling back overnight. The full event-by-event breakdown of this scenario is on the [Battery schedule](/control-algorithms/cost-optimization/battery-schedule.md) page.

## Relationship to an external signal

Cost optimization is a **local** strategy. When an [external signal](/external-signal/external-signal.md) partner is actively steering the installation, that partner takes precedence and cost optimization does not drive the battery. When no external signal is active, or the partner is on standby, the battery follows the cost-optimization schedule.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.voltmasters.io/control-algorithms/cost-optimization.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
