Symptom
GET /api/v1/tariffs/dashboard's projection for the user's current
half-hourly tariff (AGILE-24-10-01) was £108.27/month, while the
real realised cost for the SAME month was £78.43. The engine over-
states Agile by ~38 %.
=== ACTUAL realised May cost (from /energy/period — real half-hourly) ===
import £69.43 + standing £19.29 - export £10.29 = NET £78.43
=== Tariffs dashboard projection for Agile (same month) ===
total = £108.27
unit_rate avg = 29.59p · standing = 62p/d
Effect: the comparison table showed Agile costing MORE than BG Fixed
v58 (£80.56), when in fact the user beats BG every month. Users on
solar+battery+TOU tariffs see a comparison that contradicts their
real bills.
Likely cause
The engine appears to use a flat-rate model:
total_pence = consumption_kwh × unit_rate_pence + days × standing_per_day
For flat tariffs (BG Fixed, Cosy Fixed) this is accurate.
For half-hourly tariffs (Agile, Intelligent Flux) it's wrong because:
- It bills CONSUMPTION (total load) instead of IMPORT (what actually
came from the grid) — ignoring battery + solar self-use.
- It uses the AVERAGE rate instead of the per-slot rate — ignoring the
user's deliberate strategy of charging the battery during cheap
slots and discharging during peak slots.
The dashboard's pricing field already distinguishes
half_hourly vs time_of_use vs flat — the engine could
branch on this and replay the half-hour profile against the tariff's
own half-hourly rates (analogous to _realised_import_pence in
src/analytics/pnl.py but indexed by the candidate tariff's
agile_rates rather than the active one).
Workaround shipped (UI)
src/components/home/TariffComparisonWidget.tsx now overrides the
current row's projected total with the realised cost from
/energy/period?period=month (real half-hourly × measured import).
Other rows keep the engine projection, with foot text noting the basis
difference:
Your current tariff = real realised cost (half-hourly Agile ×
measured grid import). Others = engine projection (flat avg rate ×
usage, no battery-arbitrage credit), so they over-state TOU tariffs.
Asks
- Backend tariff engine — replay measured
grid_import_kw half-hour
buckets against each tariff's own half-hourly rates (when
pricing in {"half_hourly","time_of_use"}) instead of
consumption × avg rate. Use db.half_hourly_grid_import_kwh_for_day
the same way _realised_import_pence does. Standing + export
remain straightforward.
- Where a TOU tariff has time-of-use bands (Octopus Go cheap nights
etc.), replay each slot against the band the slot falls into.
- Remove the UI workaround once the engine reports realistic numbers.
Related
Symptom
GET /api/v1/tariffs/dashboard's projection for the user's currenthalf-hourly tariff (
AGILE-24-10-01) was £108.27/month, while thereal realised cost for the SAME month was £78.43. The engine over-
states Agile by ~38 %.
Effect: the comparison table showed Agile costing MORE than BG Fixed
v58 (£80.56), when in fact the user beats BG every month. Users on
solar+battery+TOU tariffs see a comparison that contradicts their
real bills.
Likely cause
The engine appears to use a flat-rate model:
total_pence = consumption_kwh × unit_rate_pence + days × standing_per_dayFor flat tariffs (BG Fixed, Cosy Fixed) this is accurate.
For half-hourly tariffs (Agile, Intelligent Flux) it's wrong because:
came from the grid) — ignoring battery + solar self-use.
user's deliberate strategy of charging the battery during cheap
slots and discharging during peak slots.
The dashboard's
pricingfield already distinguisheshalf_hourlyvstime_of_usevsflat— the engine couldbranch on this and replay the half-hour profile against the tariff's
own half-hourly rates (analogous to
_realised_import_penceinsrc/analytics/pnl.pybut indexed by the candidate tariff'sagile_ratesrather than the active one).Workaround shipped (UI)
src/components/home/TariffComparisonWidget.tsxnow overrides thecurrent row's projected total with the realised cost from
/energy/period?period=month(real half-hourly × measured import).Other rows keep the engine projection, with foot text noting the basis
difference:
Asks
grid_import_kwhalf-hourbuckets against each tariff's own half-hourly rates (when
pricing in {"half_hourly","time_of_use"}) instead ofconsumption × avg rate. Usedb.half_hourly_grid_import_kwh_for_daythe same way
_realised_import_pencedoes. Standing + exportremain straightforward.
etc.), replay each slot against the band the slot falls into.
Related
src/api/main.py:3104—/tariffs/dashboardendpointsrc/energy/tariff_engine.py— likely engine locationc7293b1)