solar/docs/BATTERY_TRADING.md

7.0 KiB
Raw Permalink Blame History

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 550 kWh (residential), 100+ (commercial) Energy that can be cycled (after DoD limit).
Max power (charge/discharge) power_kw 310 kW (residential) Same for charge and discharge in the simple model.
Round-trip efficiency efficiency 0.850.92 Out / in; e.g. 0.9 ⇒ 10% loss per cycle.
Depth of discharge DoD 8095% We model usable = capacity × DoD; 90% default.
Lifetime (cycles) max_cycles 50008000 For degradation cost per cycle.

2.2 Economics

Parameter Use
Capex €/kWh usable (e.g. 300600 €/kWh). Total capex = capacity_kwh × capex_per_kwh.
Degradation cost per cycle capex / max_cycles (€/cycle) as a simple proxy.
O&M Annual €; often 12% 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_kw and capacity_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 from delta_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 / 1000 for each discharge hour.
  • Total cost (€): Sum of (delta_charge / efficiency) × price / 1000 for 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, optional location_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:
      1. Build list of (datetime, price_eur_mwh) for each hour.
      2. 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.
      3. 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" }.
  • API: GET /trading/battery-backtest?zone=BE&from_date=...&to_date=...&capacity_kwh=10&power_kw=5&efficiency=0.9 (and optional location_id for 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_prices table.
  • Trading roadmap: docs/TRADING_PLATFORM.md.