7.0 KiB
7.0 KiB
Battery trading – design and profitability
This doc describes the battery arbitrage feature: store energy when prices are low (or from solar surplus), discharge when prices are high, and measure profitability via backtest and simple economics.
1. Objective
- Revenue: Sell stored energy at high-price hours (or avoid buying at high price).
- Cost: Energy used to charge (buy or solar opportunity cost), efficiency losses, degradation.
- Feature: Backtest over historical prices (and optionally solar) to estimate €/year arbitrage, cycles/year, and payback for a given battery size and strategy.
2. Assumptions and parameters
2.1 Battery (technical)
| Parameter | Symbol / name | Typical range | Notes |
|---|---|---|---|
| Usable capacity | capacity_kwh |
5–50 kWh (residential), 100+ (commercial) | Energy that can be cycled (after DoD limit). |
| Max power (charge/discharge) | power_kw |
3–10 kW (residential) | Same for charge and discharge in the simple model. |
| Round-trip efficiency | efficiency |
0.85–0.92 | Out / in; e.g. 0.9 ⇒ 10% loss per cycle. |
| Depth of discharge | DoD | 80–95% | We model usable = capacity × DoD; 90% default. |
| Lifetime (cycles) | max_cycles |
5000–8000 | For degradation cost per cycle. |
2.2 Economics
| Parameter | Use |
|---|---|
| Capex | €/kWh usable (e.g. 300–600 €/kWh). Total capex = capacity_kwh × capex_per_kwh. |
| Degradation cost per cycle | capex / max_cycles (€/cycle) as a simple proxy. |
| O&M | Annual €; often 1–2% of capex. |
| Grid fees / taxes | Per zone: can reduce net spread (e.g. injection fee, different buy/sell tariff). |
2.3 Data (existing)
- Hourly day-ahead prices (€/MWh) per zone →
electricity_prices. - Hourly solar production (kWh) per location →
hourly_metrics+estimate_kwh_from_hourly_w_per_m2. - Zone → from location via
get_zone_for_location(location_id).
3. Strategy (simple rule-based)
3.1 Pure price arbitrage (no solar)
- Charge in the cheapest hours until battery is full (subject to power and capacity).
- Discharge in the dearest hours until battery is empty.
- Implementation: sort hours in the day (or rolling window) by price; charge in order from lowest price, discharge in order from highest price. Respect
power_kwandcapacity_kwh(and SoC 0..100%).
3.2 Solar-coupled (optional)
- Charge from solar first: For hours with solar surplus (production > household load), store surplus in battery (up to power and capacity). Opportunity cost = price at that hour (we could have sold instead).
- Charge from grid: When price < threshold (e.g. P30 or a fixed €/MWh), charge from grid.
- Discharge: When price > threshold (e.g. P70) or when solar is zero and we want to avoid buying.
For a first backtest, pure arbitrage is enough; solar-coupled can be a second phase.
3.3 Constraints (per hour)
- State of charge (SoC): 0 ≤ SoC ≤ capacity_kwh (or capacity × DoD).
- Charge:
delta_charge ≤ power_kw × 1 h,delta_charge ≤ (capacity − SoC); after efficiency,SoC += delta_charge × sqrt(efficiency)(or one-way efficiency for charge). - Discharge:
delta_discharge ≤ power_kw × 1 h,delta_discharge ≤ SoC;SoC -= delta_discharge; revenue fromdelta_discharge × price_discharge / 1000(€). - Efficiency: Common model: charge efficiency η_c, discharge η_d, round-trip = η_c × η_d. For simplicity we can use
sqrt(round_trip)each way, or one-way 0.95.
4. Profitability formulae
4.1 Per cycle (one full charge + one full discharge)
- Energy out:
E_out = capacity_kwh × DoD(usable per cycle). - Energy in:
E_in = E_out / round_trip_efficiency. - Revenue:
E_out × price_discharge_avg / 1000(€). - Cost of charge:
E_in × price_charge_avg / 1000(€). - Gross margin (€/cycle):
margin = (E_out × P_high − E_in × P_low) / 1000
with P_high and P_low the average sell and buy prices.
Using round-trip η:
margin ≈ (E_out × (P_high − P_low/η) / 1000). - Degradation cost (€/cycle):
capex / max_cycles. - Net margin (€/cycle):
margin − degradation_cost.
4.2 Payback (simplified)
- Annual net profit:
cycles_per_year × net_margin_per_cycle − O&M_annual. - Payback (years):
capex / annual_net_profit.
4.3 Backtest output (what we compute)
- Total revenue (€): Sum over backtest period of
delta_discharge × price / 1000for each discharge hour. - Total cost (€): Sum of
(delta_charge / efficiency) × price / 1000for each charge hour (energy “consumed” to fill the battery). - Arbitrage profit (€):
total_revenue − total_cost. - Cycles: Full equivalent cycles (total kWh discharged / capacity_kwh).
- Average spread captured:
(total_revenue − total_cost) / total_kwh_discharged × 1000(€/MWh).
5. Implementation sketch (codebase)
-
New module:
backend/app/battery_trading.py- Inputs:
zone,from_date,to_date,capacity_kwh,power_kw,efficiency, optionallocation_id(for solar-coupled later). - Data: Load hourly prices via
get_electricity_prices(session, zone, from_date, to_date); aggregate to one value per hour (average if 15-min). - Algorithm:
- Build list of (datetime, price_eur_mwh) for each hour.
- For each day (or sliding window): sort hours by price; charge in cheapest hours (up to power and capacity), discharge in dearest hours; update SoC and accumulate revenue/cost.
- Return time series of SoC, charge/discharge per hour, and totals (revenue, cost, profit, cycles).
- Output:
{ "zone", "from_date", "to_date", "params", "daily": [ { "date", "revenue_eur", "cost_eur", "profit_eur", "kwh_charged", "kwh_discharged" } ], "total_revenue_eur", "total_cost_eur", "total_profit_eur", "total_cycles", "avg_spread_eur_mwh" }.
- Inputs:
-
API:
GET /trading/battery-backtest?zone=BE&from_date=...&to_date=...&capacity_kwh=10&power_kw=5&efficiency=0.9(and optionallocation_idfor future solar mode). -
Frontend (later): Form: zone (or location), date range, battery params; run backtest; show table + summary (profit, cycles, payback if user enters capex).
6. Sensitivity and risks
- Spread: If more batteries and flexibility enter the market, price spreads can shrink → lower €/cycle.
- Capex: Payback is very sensitive to €/kWh; small changes move payback by years.
- Regulation: Grid fees, taxes, and rules on injection/withdrawal vary by country; we can add a “grid cost” assumption per zone later.
- Strategy: Real systems use day-ahead optimization; our rule-based backtest is for feasibility and comparison, not optimal dispatch.
7. References
- Existing trading:
backend/app/trading.py(production value series, backtest, expected earnings). - Prices:
get_electricity_prices,electricity_pricestable. - Trading roadmap:
docs/TRADING_PLATFORM.md.