Most PV design tools optimize a plant for energy yield (kWh). This project asks a different question: what design maximizes revenue in a wholesale electricity market, where the price moves with the sun? The two answers are not the same, because a panel that generates at noon sells into the hours it has itself made cheap.
It simulates the hourly output of a PV plant with pvlib and values every design against real German day-ahead prices from SMARD.de, comparing fixed orientations, an east-west layout, and a single-axis tracker on market value rather than yield alone.
Project developed July 2026.
Munich, 2019–2023, valued against DE/LU day-ahead prices, per kWp of DC nameplate:
| Design | Yield (kWh/kWp) | Capture price (EUR/MWh) | Value factor | Revenue (EUR/kWp) |
|---|---|---|---|---|
| Fixed south 35° (yield-optimal) | 5,981 | 86.9 | 0.877 | 520 |
| East-west 15° | 5,041 | 88.2 | 0.890 | 445 |
| Single-axis tracker | 6,525 | 91.4 | 0.923 | 597 |
Three findings, each a little against the grain:
-
A single-axis tracker wins on both counts. It produces more energy and sells it at a higher average price (+5% capture price), for about +15% revenue over the yield-optimal fixed array. Tracking is not just a yield upgrade, it is a market-timing upgrade.
-
East-west earns a higher price per MWh but less revenue. Its flatter, twin-shouldered profile aligns better with prices (capture price 88.2 vs 86.9), but the ~16% yield penalty outweighs the price gain, so revenue is lower. A higher capture price is not the same as more money.
-
"Face west for the evening peak" does not pay for a fixed array. The revenue-optimal orientation stays close to due south; the modest evening price premium never overcomes the yield lost by turning away from the sun.
The mechanism in one chart: the fixed-south array (black) spikes at midday, straight into the price trough the midday solar fleet creates. The tracker (blue) spreads generation into the higher-priced morning and afternoon shoulders. That shift is what raises its capture price.
Splitting the value factor by year shows the price-cannibalization effect deepening as solar penetration rises: the fixed array's value factor falls from about 0.94 (2019) to 0.78 (2023), while the tracker holds a consistent premium. The more midday solar cannibalizes midday prices, the more a design that generates away from midday is worth.
For any array design the pipeline is a standard pvlib chain, expressed per kWp of DC nameplate so results are size-independent:
resource (GHI, DHI, air temp, wind) from PVGIS
-> solar position pvlib.solarposition.get_solarposition
-> DNI from GHI, DHI, zenith pvlib.irradiance.dni
-> plane-of-array irradiance (Hay-Davies) pvlib.irradiance.get_total_irradiance
-> cell temperature pvlib.temperature.faiman
-> DC power (with system + inverter loss) pvlib.pvsystem.pvwatts_dc
-> AC power, clipped at the inverter limit
The single-axis tracker adds pvlib.tracking.singleaxis to compute the panel
angles each hour before transposition. Solar geometry and DNI do not depend on
orientation, so they are computed once and reused across all 99 orientations in
the sweep.
Valuation. The key metric is the capture price, the volume-weighted price a generator actually receives:
capture_price = Sum(price[t] * generation[t]) / Sum(generation[t])
value_factor = capture_price / baseload_price (< 1 means cannibalization)
revenue = Sum(price[t] * generation[t]) (EUR per kWp)
System model. PVWatts DC with a −0.4%/°C temperature coefficient, 14% DC
system losses (soiling, mismatch, wiring, availability), a Faiman cell-temperature
model, 97% inverter efficiency, and a 1.2 DC/AC ratio with AC clipping. Every
parameter lives in config/plant.yaml.
See RUNNING.md for full setup, including a VS Code walk-through.
pip install -r requirements.txt
python scripts/run_all.py # downloads data once, then runs the analysisTests:
pytestThis project uses pvlib as a dependency rather than modifying it. The functions
exercised: iotools.get_pvgis_hourly, solarposition.get_solarposition,
irradiance.dni, irradiance.get_extra_radiation,
irradiance.get_total_irradiance (Hay-Davies transposition),
temperature.faiman, pvsystem.pvwatts_dc, and tracking.singleaxis.
| Field | Source |
|---|---|
| Hourly GHI, DHI, air temperature, wind speed | PVGIS (SARAH3), via pvlib |
| Day-ahead price (DE/LU) | SMARD.de, series 4169 (EPEX SPOT) |
- PVGIS (EU Joint Research Centre Photovoltaic Geographical Information System): https://joint-research-centre.ec.europa.eu/photovoltaic-geographical-information-system-pvgis_en
- SMARD.de (Bundesnetzagentur), day-ahead prices: https://www.smard.de/en
- SMARD download centre and terms: https://www.smard.de/en/downloadcenter/download-market-data/
Irradiance and prices are aligned in UTC (PVGIS timestamps are floored to the hour) so each generation hour is valued at its own clearing price.
pv-market-value/
├── config/plant.yaml # site, system losses, sweep, tracker, east-west parameters
├── src/pvmarket/
│ ├── config.py # typed configuration
│ ├── data/pvgis.py # PVGIS irradiance via pvlib (cached)
│ ├── data/prices.py # SMARD day-ahead price client (cached)
│ ├── model.py # the pvlib generation pipeline (fixed / east-west / tracker)
│ ├── valuation.py # capture price, value factor, revenue
│ ├── sweep.py # orientation sweep and design comparison
│ └── plots.py # charts
├── scripts/ # download, run_analysis, run_all
├── tests/ # physics sanity, capture-price maths, config validation
├── notebooks/ # exploratory scripts
└── results/ # generated charts and CSVs
- One site (Munich) and the 2019–2023 window PVGIS SARAH3 covers. The framework runs for any European location by changing the config.
- Price-taker: the plant earns the day-ahead clearing price and does not move it.
- Day-ahead only; no intraday, balancing, or PPA structures, and no grid or curtailment constraints.
- PVWatts-level system model, not a module-and-inverter-specific SAPM model.
- A tracker's higher capex and land use are not modelled here; the comparison is on gross market value per kWp, which is the input to that decision, not the decision itself.
- PV plus battery: feed this generation into a storage dispatch optimizer to co-optimize the hybrid against prices (connects to a companion BESS project).
- Cross-country: repeat for markets with steeper evening ramps, where west-facing and tracking should pay more.
- Bifacial and different tracker geometries, and a full LCOE / capex-aware ranking rather than gross revenue.
Mohammad Faisal, M.Sc. Power Engineering (Renewable Energy)
- GitHub: github.com/CodingxFaisal
- Email: mohammad.faisal@gmx.de
Released under the MIT License. See LICENSE.
Built with pvlib python (Holmgren, Hansen, and Mikofski, Journal of Open Source Software, 2018, doi:10.21105/joss.00884). Irradiance data from PVGIS (© European Union); price data from SMARD.de / Bundesnetzagentur, used under their terms.


