# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project intent Backtest the ROI of a home battery against real historical hourly data. The core question: > "If I had owned battery X over period Y, what would it have actually saved me?" First-class scenarios: - **No solar, just price arbitrage** — charge when day-ahead is cheap, discharge when expensive. - **With solar** — store surplus generation instead of exporting at low / negative prices. Two battery archetypes to compare side-by-side, because they have very different economics: - **Plug-in (AC-coupled, schuko-style)** — small capacity (~2 kWh typical), low continuous output (~800 W), no grid export, simple time-shifting of own consumption only. Cheap, no installer. - **Installed (hybrid inverter / DC-coupled)** — 5–15 kWh, 3–5 kW continuous, can charge from solar DC, can export to grid. Expensive, needs an electrician. Output should include: euro savings vs. counterfactual, simple payback period, and a sensitivity sweep across battery size / power rating / dispatch strategy. ## Decisions still open — do not silently assume These materially change the answer. Get the user to nail them down before writing simulation code, or state the assumption loudly in every result. - **Battery model fidelity.** Round-trip efficiency, depth-of-discharge limit, calendar + cycle degradation, idle/standby draw, inverter efficiency. Start with constants. Only add curves once results turn out sensitive to them. - **Solar PV system parameters.** Panel kWp, tilt, azimuth, inverter clipping. The horizontal irradiance from `solar_hourly.csv` is the input; PV output is computed downstream from these parameters. Default to a flat-roof horizontal panel until the user specifies otherwise. ## Decisions made - **Country / market**: Netherlands. - **Backtest window**: **2023-09-01T00:00:00Z → 2024-09-01T00:00:00Z** (8784 hours, 366 days; 99.5%+ coverage on all three signals). The next year forward had a 19-day P1 outage in March 2025 and was rejected. - **Household load profile**: real P1 meter log from user's house in InfluxDB. The relevant field (`power-current`) is **import-only** — this house has **no PV today**. For "with solar" scenarios we synthesize PV output from a horizontal irradiance signal × assumed system parameters. - **Day-ahead prices**: raw EPEX, hourly mean, in EUR/kWh, from user's InfluxDB. **No taxes, BTW, supplier markup, or saldering math is baked into the data** — those are tariff layers added downstream (UI / cost calc). Negative hours are real and preserved. - **Tariff modelling**: out of scope for this codebase. The simulator outputs energy flows (kWh imported, exported, charged/discharged) per hour. The UI / spreadsheet layered on top translates those into euros under whichever tariff structure is being explored. - **Solar gap forward**: irradiance sensor offline ~2025-09-09 → ~2026-04-29. Don't pick a window ending after Sep 2025 unless this is fixed. - **Dispatch strategy**: 1. Build a 24h-perfect-foresight oracle as the ceiling — what's the max possible savings? 2. Build rule-based dispatchers (e.g. price percentile thresholds, SoC bands) and measure how close they get to the oracle. 3. Stretch goal: an LSTM (or simpler) policy trained on historical sequences to beat the rules. Don't pull in PyTorch until the rules have a measured, reproducible gap to close. ## Data sources The user's home InfluxDB is the *origin* — what actually happened in this house. But the simulator does **not** talk to InfluxDB. The user exports CSVs once and drops them in `data/raw/`; everything downstream reads CSV. That keeps runs reproducible, reviewable in git diffs (for small fixtures), and free of network/auth concerns. What's known to exist in InfluxDB (verify before assuming): - Hourly day-ahead electricity prices (>2 years). - 15-minute electricity prices (~last 4 months). - P1 smart-meter log (>2 years), 1-min / 10-sec resolution. - Solar irradiance / PV output, with a gap from ~September 2025 to ~2026-04-29. Expected raw CSVs (one per signal, UTC timestamps, hourly resampled at the source if possible): - `data/raw/prices_hourly.csv` — `timestamp, eur_per_kwh` (or `eur_per_mwh`, document which) - `data/raw/p1_hourly.csv` — `timestamp, import_kwh, export_kwh` (or net `consumption_kwh`) - `data/raw/solar_hourly.csv` — `timestamp, pv_kwh` (and/or `irradiance_w_m2`) Document the exact columns and units in `docs/quick-index.md` once the export is in place. Anything resampled or derived from these files goes to `data/processed/` and must be regenerable. If the InfluxDB export turns out to be tedious to do by hand, write a single one-off script under `scripts/export_from_influx.py` that reads connection details from `.env`. It runs once per refresh, not every simulation run. ## Stack (assumed; change if user objects) - Python 3.12+, managed with `uv`. - `pandas` for time series; drop to `numpy` only if the inner loop becomes the bottleneck. - `pytest` for tests. - `influxdb-client` only if/when we write the optional one-off export script — not a runtime dep of the simulator. - No web framework, no ORM, no plotting library yet. Add only when there is a concrete use case. - No PyTorch / TensorFlow yet. Only after rule-based dispatchers have a measured gap to the oracle that's worth closing. ## Common commands To be filled in once the project has code. For now: ``` uv sync # install deps once pyproject.toml exists uv run pytest # run tests uv run pytest tests/test_dispatch.py::test_name # run a single test ``` ## Repo layout (target) - `src/` — simulation code - `tests/` — pytest. Unit-test the dispatch logic and tariff calc; one integration test that runs a full year on small fixture data - `data/raw/` — immutable downloads (gitignored except small fixtures) - `data/processed/` — derived, regenerable - `docs/quick-index.md` — pointer to where each concept lives in the code - `scripts/` — one-off data fetchers (ENTSO-E, KNMI, PVGIS) ## Things to resist (anti-overengineering for this project) - Don't build a `BatteryStrategy` interface with one implementation. Write the function. Add the interface when the second strategy lands with a name. - Don't build a tariff DSL or YAML config. Hard-code the NL tariff structure in a function until a second country actually shows up. - Don't build a CLI framework. `python -m batteryroi.run --year 2024` via `argparse` is enough. - Don't add caching / parquet / duckdb until a run takes >30s and the user is actively iterating on it. - Don't add a `Battery` dataclass with builders/factories. Plain kwargs into a function. - Don't pre-create empty modules "for structure". Files arrive when they have content.