I verified AERON’s January 2026 live retrieval against raw Elexon API payloads inspected with curl: it returned 1,488 FUELHH WIND records and 22,776 WINDFOR records. For delivery period 2026-01-15T00:00Z with a 12-hour cutoff, the API independently confirmed selection of the 2026-01-14T11:30Z publication at 15,813 MW—the latest publication at or before the cutoff. If all publications for a delivery period are later than its cutoff, I label it missing_forecast / published_after_cutoff rather than borrowing a later forecast.
My recorded verification run passed all 71 backend tests (pytest, 4.3 s); vite build succeeded with 2230 modules and app/react/charts bundles; and check-imports.mjs confirmed that all 33 relative imports resolve. All four pages rendered in headless Chromium against the live backend with zero console errors and zero page errors. I exercised the full-month range, fixed-cutoff mode, cutoff and reset-filters controls, status and direction filters, sorting, row-selection drawer, CSV download (aeron_wind_2026-01-15_lead12h_live.csv), and export confirmation toast.
I also verified resilience, accessibility, responsive behavior, caching, and sample mode. Deliberately aborting /api/overview produced a structured error and working retry button while navigation remained usable. Keyboard navigation reached the brand, all four navigation items, refresh, date selector, and range toggle with visible focus rings. At a 414 px viewport, all four pages had zero horizontal overflow. A warm restart served the chunk plan and record counts from disk without re-downloading; sample mode displayed its warning and produced metrics identical to live mode.
In this development-environment verification run, I independently compared the January 2026 Elexon retrieval with raw API payloads inspected using curl. The 12-hour-cutoff check for 2026-01-15T00:00Z selected the 2026-01-14T11:30Z publication at 15,813 MW; periods with only later publications were marked missing_forecast / published_after_cutoff. The run passed 71 backend tests (pytest, 4.3 s), built the frontend with vite build (2230 modules), and verified all 33 relative imports. All four pages rendered in headless Chromium against the live backend without console or page errors. I also exercised the date range, cutoff modes, filters, sorting, selection drawer, CSV export and confirmation toast; an aborted /api/overview request produced a structured error and working retry; keyboard focus was visible; and all four pages had no horizontal overflow at 414 px. A warm restart served cached data without re-downloading, and sample mode produced metrics identical to live mode while displaying its warning. These checks were run in development; Docker image/container operation, public hosting, and Windows browser rendering remain unverified.
I interpret the results within the data’s limits: WINDFOR targets are hourly while FUELHH actuals are half-hourly, and each half-hour is matched to its containing forecast hour. The requested cutoff is enforced exactly, but irregular publication cadence makes realised lead time vary by 2–4 hours around the requested value; publishTime is publication time, not model initialisation time. Retrieval is chunked because requests are limited to seven days; partial failures reduce coverage and are reported. I use the latest retrieved FUELHH value as the actual rather than reconstructing later settlement revisions, and reject dates outside January 2026. The filesystem cache is ephemeral on free hosting, so a restart or spin-down loses it; a cache miss re-downloads January data in a few seconds. A Render free instance may take about a minute to wake, with a retryable network error if the first request times out. AERON measures forecast accuracy only, not procurement requirements, prices, or imbalance exposure.
I have not verified a Docker image build or container run because Docker was not installed in the execution environment; docker compose up --build remains the recommended local confirmation. I accessed no Vercel or Render hosting accounts, created no public URL, and tested no hosted deployment. Browser checks ran in headless Chromium on Linux, not Windows. The verification record does not state the filesystem cache TTL.
I keep several data and analysis limitations explicit. WINDFOR targets are hourly and FUELHH actuals are half-hourly, so each half-hour actual is compared with the forecast for its containing hour. Cutoffs are honoured exactly, but irregular publication cadence makes realised lead time vary between 2 and 4 hours around the requested value; I report it per period. publishTime records when Elexon published a record, not when the numerical model was initialised. Retrieval has a seven-day request ceiling and uses multiple chunks; partial failure is reported rather than hidden, and missing chunks reduce coverage. FUELHH has its own publishTime; I use the latest retrieved actual and do not reconstruct later settlement revisions. Dates outside January 2026 are rejected. AERON measures forecast accuracy, not procurement requirements, prices, or imbalance exposure.
I use rolling lead time as the default because it gives a full matched sample for any delivery date and is a clean starting point for a demonstration; fixed-instant cutoffs instead represent a single gate-closure-style decision. Neither mode is presented as the official deadline. I build the half-hourly grid from London midnight to handle daylight-saving changes without assuming 48 periods per day. Metrics use only matched rows and return null rather than 0 when data is missing, so missing observations cannot silently improve accuracy. Cutoff comparisons use a common sample so that differences do not arise from different sample composition. I chose a filesystem cache with TTL rather than a database for zero cost, no credentials, compatibility with ephemeral free-tier filesystems, and a cache miss that takes only a few seconds; a bundled sample snapshot supports a no-network demonstration while live integration remains the default.
I use custom CSS design tokens to give AERON a distinctive energy-trading identity rather than a generic admin-template appearance. Tailwind supports utility composition, while the visual system is defined in styles/index.css. Records-table filtering is server-side to reduce payload; sorting is client-side because sorting up to 1,488 rows is instant and requires no extra request. The application contains public sector information made available by Elexon Limited under the Elexon Open Data Licence. Its data source is the Elexon BMRS Insights Solution FUELHH and WINDFOR datasets; Elexon is not affiliated with or endorsing the application.
I configure AERON’s backend through backend/.env. DATA_MODE defaults to live (fetching Elexon); sample uses the bundled snapshot. The Elexon API root is https://data.elexon.co.uk/bmrs/api/v1. Requests time out after 30 seconds and retry transient 429, 5xx, and timeout errors up to 3 times, using exponential backoff based on 1.5 seconds and capped at 8 seconds. I limit request windows to 7 days because the API rejects larger ones. The default analysis window is 2026-01-01 through 2026-01-31; publication-history margins are 7 days before and 2 days after it. The default lead is 12 hours, and the interface offers 4,8,12,24,36,48 hours. ^Backend cache defaults are enabled, use /tmp/aeron-cache, and have a 24-hour TTL; the ephemeral filesystem is acceptable for this cache design. CORS_ORIGINS accepts comma-separated exact origins and defaults to local development origins; CORS_ORIGIN_REGEX defaults to https://([a-z0-9-]+\.)*vercel\.app for Vercel previews. Logging defaults to INFO.
I configure the frontend through frontend/.env. VITE_API_BASE_URL defaults to empty, which means same-origin requests handled by the Vite proxy and Docker setup. VITE_PROXY_TARGET is for the development or preview proxy only (the template shows http://<redacted>:8000). Every VITE_* value is public and inlined into the browser bundle, so I never put secrets there. AERON needs no frontend secrets: Elexon’s public API requires no API keys, tokens, or credentials, and none are present in the repository, Docker images, or environment templates.
For a free personal, non-commercial deployment, I can host the frontend on Vercel Hobby; vercel.json configures the SPA rewrite. The backend option is a Render Free web service with 512 MB RAM, 0.1 CPU, and 750 instance hours per workspace per month. Render spins down after 15 minutes of inactivity and takes about a minute to wake. Its filesystem is ephemeral, which the cache design accommodates. My deployment URLs remain placeholders because I have not performed a public deployment: https://<your-vercel-project>.vercel.app, https://<your-render-service>.onrender.com, and the health endpoint https://<your-render-service>.onrender.com/health. I should replace them with the URLs for my own deployed services. Full step-by-step deployment instructions, including GitHub publication, are in DEPLOYMENT.md.
I built AERON – Wind Forecast Intelligence to analyze wind power forecasts against actual generation, turning raw energy data into actionable intelligence for better energy‑trading decisions.
I built AERON as a trading-terminal-grade analytics application for a UK electricity utility buying electricity in the day-ahead wholesale market. It evaluates the national wind generation forecast available at the purchasing decision against realised wind generation: what the trading desk could have seen, what happened, and how wrong that forecast was. The analysis uses Elexon BMRS data for January 2026 and covers a full month of half-hourly settlement periods.
- I designed the analysis around the trading team's need to see the forecast as it stood at decision time, quantify reliability in MW, distinguish systematic over- or under-forecasting from noisy errors, identify the hardest delivery periods, and verify the provenance of every number.
I deliberately do not use the latest available forecast for a historical period. Instead, I reconstruct the information set available at a configurable decision cutoff using Elexon publishTime timestamps, so the evaluation reflects the forecast available when the purchasing decision was made rather than one published later. I compare that forecast with actual wind generation and score it using MAE, RMSE, bias, and WAPE.
I keep AERON's scope to national wind-generation forecast accuracy. Wind generation shortfall is not a utility's total procurement requirement: consumer demand, other generation sources, and wholesale prices are outside the analysis. I repeat this boundary in the application interface. The project is a Python repository on my github profile, started and last updated in 2026-10.
The available README material does not give metric values, forecast or actual-generation values, the hardest delivery periods, or the mechanism used to verify provenance. It also does not specify the configurable cutoff values or default, or define the formulas for MAE, RMSE, bias, and WAPE.
I designed the application around four views. The overview dashboard selects delivery dates from 1–31 January 2026 or the full month, supports a rolling decision cutoff (C = T − lead_hours) or a fixed decision instant, and shows dataset and API status. It charts forecast against actual generation, optionally with a retrospective error envelope, and presents MAE, RMSE, bias, WAPE, matched-period count, overforecast percentage, and the largest absolute error with its publication time and realised lead time. Analytical interpretations should be generated from selected records, never hardcoded.
In the Forecast Explorer, each delivery-period row shows the selected publication, forecast, actual, signed and absolute error, and realised lead time. I retain and label missing forecasts and actuals rather than silently dropping them. The explorer filters server-side by observation status, error direction, and delivery timestamp; sorts every numeric column client-side; supports keyboard-navigable rows and a detail drawer; and exports exactly the selected records with download confirmation. The Accuracy Lab reports MAE, RMSE, bias, WAPE, over/underforecast counts, and mean actual and forecast levels, alongside error by delivery period and the absolute-error distribution.
For cutoff comparisons, I use the identical set of common delivery periods and disclose native and common observation counts separately; I do not declare a winner when the difference is not material. The interface should state metric formulas and the error sign convention explicitly. The Data Provenance view documents the Elexon endpoints, parameters, and fields actually used; retrieval timestamp and duration; per-chunk, raw, and used record counts; matched and missing observations; duplicates and invalid records removed; non-WIND rows dropped and negative values flagged; latest relevant publication time and distinct publication count; cache status, backend, TTL, and age; and data-quality warnings, failures, and limitations.
My interface requirements include custom AERON branding, an icon system, obsidian and graphite surfaces with chartreuse accents, loading skeletons, empty states, structured retryable errors, CSV-export toasts, and tooltips defining every metric. I also require full keyboard navigation with visible focus rings, respect for prefers-reduced-motion, working reset-filters actions, and responsive layouts down to a 414 px viewport without horizontal overflow.
| Layer | Choice | Version |
|---|---|---|
| Frontend framework | React | 18.3.1 |
| Build tool | Vite | 6.0.7 |
| Styling | Tailwind CSS (design tokens in CSS variables) | 3.4.17 |
| Charts | Recharts | 2.15.0 |
| Icons | Lucide React | 0.468.0 |
| Routing | React Router | 6.28.0 |
| Backend framework | FastAPI | 0.115.6 |
| ASGI server | Uvicorn | 0.34.0 |
| Data processing | Pandas / NumPy | 2.2.3 / 2.2.1 |
| HTTP client | HTTPX | 0.28.1 |
| Validation / settings | Pydantic / pydantic-settings | 2.10.4 / 2.7.0 |
| Tests | Pytest | 8.3.4 |
| Containers | Docker, Docker Compose | any current release |
My browser app is built with React and Vite. Its Overview, Explorer, Accuracy Lab, and Provenance views use a central api.js client configured through VITE_API_BASE_URL. The backend is organized so api/routes.py handles HTTP validation and status codes, services/analysis.py assembles responses, analytics/selection.py selects forecasts at the cutoff and builds the delivery-period grid, and analytics/metrics.py computes MAE, RMSE, bias, and WAPE.
For each delivery period T and decision cutoff C, I consider only WINDFOR records for the corresponding delivery period, exclude any publication with publishTime > C, and select the remaining record with the latest publishTime. A publication exactly at the cutoff is eligible; one published even a second later is not. This prevents historical evaluation from using information that was unavailable at decision time. The cutoff is an analytical convention for evaluating forecast skill, not an official market deadline, and I state that wherever the cutoff is described.
I support two cutoff modes. Rolling lead is the default: C = T − lead_hours, with choices of 4, 8, 12, 24, 36, or 48 hours, which suits a desk transacting a fixed number of hours ahead. Fixed instant applies one absolute instant to every delivery period, for a single day-ahead gate-closure-style decision. WINDFOR publishes every 2–4 hours, so a requested 12-hour cutoff typically selects a publication issued 12.0–15.5 hours before delivery. I report realised lead time per period—median, minimum, and maximum—rather than imply that the nominal horizon is exact.
- I build each London delivery day from local midnight to the next local midnight in 30-minute steps, using
Europe/Londonmidnight boundaries converted to UTC; I do not assume 48 periods per day. Spring-forward days have 46 periods and autumn-back days have 50, and automated tests cover this. The January analysis window has no DST transition, but the logic is intended to work for any window.
I define signed error in MW as error = forecast_generation − actual_generation, or eᵢ = fᵢ − aᵢ for each matched period i. Positive error means an overforecast (forecast above actual); negative error means an underforecast. For n matched observations, MAE is mean(|eᵢ|) in MW and describes the direction-agnostic average miss; RMSE is sqrt(mean(eᵢ²)) in MW and penalises large misses; and bias is mean(eᵢ) in MW, with positive bias indicating systematic overforecast. WAPE is Σ|eᵢ| / Σaᵢ × 100 percent; when Σaᵢ = 0, it is undefined and returned as null, never zero or infinity. The matched-period count is the n used by every metric, and overforecast percentage is count(eᵢ > 0) / n × 100. Supporting statistics include absolute-error p50 and p90, signed-error p05 and p95, largest absolute error, mean actual and forecast, over/under/exact counts, and coverage percentage.
When I compare cutoffs, I recompute metrics on the intersection of delivery periods matched under every selected cutoff. I show both each cutoff’s native observation count and the common observation count, and state a “lowest MAE” verdict only for the common sample. If the MAE difference is below 2%, I explicitly say that I do not claim any cutoff is more accurate. Elexon generation values are average MW over a delivery period; I label them MW throughout, never MWh.
- I de-duplicate actuals by delivery timestamp, keeping the last record, and report the duplicate count on Provenance. I de-duplicate forecasts by
(publishTime, startTime)and report that count as well. - I drop non-
WINDrows after filtering and report their count. I drop non-numeric or nullgenerationvalues as invalid records rather than coerce them to zero. I drop malformed timestamps, count them, and surface parsing errors as warnings. - I retain negative generation values because Elexon can publish them, and explicitly flag them as a warning. If no records are selected, metrics are
nulland the interface shows an explanatory empty state. - If retrieval is partial, I record the failed chunk, use the remaining chunks, and display the limitation in both the Provenance failures panel and quality warnings. If the backend is unavailable, I show a structured error containing the backend message and a working retry button while keeping navigation usable. I reject an unexpected response envelope as a validation error rather than silently parsing it.
- I parse timestamps as timezone-aware instants and normalise them to UTC for comparison and storage; I display them in
Europe/London. - I map each hourly WINDFOR delivery target to the half-hourly grid by flooring a delivery period to its containing hour, retaining every candidate publication for that hour. I filter candidates by the cutoff, select the latest eligible publication for each period, retrieve actuals filtered to
fuelType = WIND, and match forecasts to actuals by exact UTC delivery-timestamp equality. - I retain unmatched periods with status
missing_forecast,missing_actual, ormissing_both, and usepublished_after_cutofforno_publication_in_windowas machine-readable missing-forecast reasons. Onlymatchedrows contribute to metrics; I never silently fill missing values with zero.
I mediate data access through services/repository.py, which provides memory and filesystem caching and connects to analytics/normalize.py for cleaning and UTC normalisation and services/elexon_client.py for retrieval and retries. The Elexon BMRS Insights API provides /datasets/FUELHH for actual wind generation and /datasets/WINDFOR for forecast publications. I retrieve external data in 7-day windows. In DATA_MODE=sample, the repository can use a bundled .csv.gz snapshot for January 2026.
I use the official Elexon BMRS Insights API at https://data.elexon.co.uk/bmrs/api/v1, and checked its documentation against the live API contract. For actual wind generation, I request GET /datasets/FUELHH with settlementDateFrom, settlementDateTo, and fuelType=WIND; I consume startTime, publishTime, settlementDate, settlementPeriod, fuelType, and generation. These observations are half-hourly average MW over each settlement period, and I filter to fuelType = WIND. For forecasts, I request GET /datasets/WINDFOR with publishDateTimeFrom and publishDateTimeTo, consuming startTime, publishTime, and generation; its delivery targets are hourly.
Both dataset endpoints reject date ranges exceeding 7 days with HTTP 400 and the message must not exceed 7 days, so I retrieve data in windows of no more than 7 days and display the chunk plan on the Provenance page. I use the dataset endpoints for historical retrieval: /FUELHH/stream and /WINDFOR/stream expose only approximately the most recent 24 hours and ignore date parameters. I send only documented query parameters and show requests verbatim in the UI.
WINDFOR startTime values fall on the hour, while FUELHH values fall on :00 and :30. I align each half-hour actual with the forecast published for the hour containing it, and disclose that behavior in the UI. In January, I observed 280 distinct publication timestamps, spaced 2–4 hours apart and always at :25 or :30. Since realised lead time varies by delivery period, I report it rather than assume it is constant. Each publication carries 73 hourly values, roughly 46–67 hours ahead; long lead cutoffs can remove delivery periods, so I disclose observation counts.
The API is live and served January 2026 records as historical data, so I treat January 2026 strictly as history, never as a live forecast. I bundled a snapshot of those same records at backend/app/data/sample/ as a 127 KB gzipped CSV; it is read only when DATA_MODE=sample. Sample mode persistently displays a SAMPLE DATA chip in the status bar and an explicit warning in the quality panel. The live Elexon integration is fully implemented and remains the default.
I organize AERON as a Python/FastAPI backend and a Vite/React frontend. The backend contains API routes and dependency providers, analytics for metrics, normalization and forecast selection, environment-backed settings, response schemas, analysis and CSV services, an Elexon client, a memory and filesystem cache, timezone helpers, sample data, and tests. The frontend contains reusable brand, chart, control, layout, panel, table and UI components; navigation and data-fetching hooks; Overview, Explorer, Accuracy Lab, Provenance and 404 pages; a central API client; styling and formatting utilities; and Docker/nginx deployment configuration. The repository also includes Docker Compose, a Render blueprint, Vercel settings, and deployment documentation.
I use this directory map to find the main implementation areas. In the backend, app/api/ handles routes and dependencies; app/analytics/ handles metrics, normalization, and forecast selection; app/services/ assembles analysis, exports CSV, retrieves Elexon data, and manages the repository cache; and app/utils/timezones.py handles timezone and grid helpers. Backend tests are grouped by metrics, normalization, selection, Elexon retrieval, and API behavior. In the frontend, reusable components live under src/components/, views under src/pages/, state and fetching hooks under src/hooks/, and API calls in src/services/api.js. The root contains the Docker Compose setup, Render blueprint, environment template, README, and deployment guide.
For local development, I use Windows PowerShell and run commands from the extracted project folder. The documented minimums are Python 3.11 (3.12 recommended), Node.js 20 LTS, npm 10, and Git 2.30; Docker Desktop is needed only if I use the Docker path. I can open the project in VS Code and install the recommended Python, Docker, and Tailwind CSS IntelliSense extensions.
For an offline demonstration or deterministic API checks, I set DATA_MODE to sample before starting the backend. The bundled January 2026 snapshot avoids live requests while preserving the same metrics as the live records; the interface indicates sample mode with a SAMPLE DATA chip. I can regenerate the bundled snapshot from the backend with python scripts\build_sample_data.py.
When running the Docker Compose setup, I set VITE_API_BASE_URL before building if the frontend needs to call a remote backend, because Vite embeds that value into the built image. Compose waits for the backend health check before starting the frontend. The local Compose frontend is at http://localhost:8080, and the backend health endpoint is at http://localhost:8000/health.
For local development, I use Windows PowerShell, a backend .venv, and separate backend and frontend terminals. I need Python 3.11 or newer (3.12 recommended), Node.js 20 LTS, npm 10 and Git 2.30; Docker Desktop is needed only for the Docker path. I extract the project under C:\Projects, open it in VS Code, and can install the recommended Python, Docker and Tailwind CSS IntelliSense extensions. If PowerShell blocks virtual-environment activation, I use a process-scoped execution-policy bypass rather than changing the policy permanently.
# Backend, from the project root
cd backend
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -r requirements.txt
copy .env.example .env
python -m uvicorn app.main:app --reload --port 8000
# If PowerShell blocks activation, run in that process
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
# Frontend, in a second terminal
cd frontend
npm install
copy .env.example .env
npm run dev
My local backend runs on port 8000, with health at http://localhost:8000/health, OpenAPI documentation at http://localhost:8000/docs, and the example overview at http://localhost:8000/api/overview?delivery_date=2026-01-15&lead_hours=12. The frontend runs at http://localhost:5173. Its .env.example sets VITE_API_BASE_URL=http://localhost:8000 so the browser can call the backend directly; leaving the value empty also works because the Vite development server proxies /api and /health. The first live request fetches January 2026 from Elexon in five FUELHH chunks and six WINDFOR chunks, taking roughly 3 seconds, then persists data under backend\.cache\aeron\. Later requests use that cache until its TTL expires or Refresh is pressed; the guide does not specify the TTL or Refresh behavior.
For an offline demonstration, I set DATA_MODE to sample before starting the backend. The bundled January 2026 fuelhh_wind_2026_01.csv.gz contains 1,488 actual-wind rows and windfor_2026_01.csv.gz contains 22,776 WINDFOR rows. Sample mode avoids live fetching, produces the same metrics, and visibly marks the UI with a SAMPLE DATA chip. The API tests also use sample mode, so they need no network access and are deterministic.
# Backend tests and bundled sample-data regeneration
cd backend
.\.venv\Scripts\Activate.ps1
python -m pytest
python -m pytest -v
python -m pytest tests/test_selection.py -v
python scripts\build_sample_data.py
# Frontend build, import check, and preview
cd frontend
npm run build
npm run lint:imports
npm run preview
The guide describes 71 backend tests: 18 metric tests, 11 normalization tests, 14 selection tests, 10 Elexon-client tests, and 18 API tests. The API tests run against the bundled sample snapshot. The frontend build must finish without errors, and npm run lint:imports checks that each relative import resolves to a module.
# From the project root, after copying .env.example to .env
docker compose up --build
# Frontend: http://localhost:8080
# Backend health: http://localhost:8000/health
# Set this before building if the frontend must call a remote backend
$env:VITE_API_BASE_URL="https://your-backend.example.com"
docker compose up --build
docker compose up -d --build
docker compose ps
docker compose logs -f backend
docker compose logs --tail 100
docker compose exec backend python -m pytest
docker compose down
docker compose down -v
docker compose build --no-cache
docker compose config
# Diagnose a backend container that exits immediately
docker compose logs backend
docker compose run --rm backend python -c "import app.main; print('import ok')"
In Docker, I set VITE_API_BASE_URL before building because Vite inlines it at build time. Compose waits for the backend health check before starting the frontend, so the first page load does not race a cold API. For repository and project context, see my github profile.
- When a trader changes the delivery date or cutoff, the React page sends
GET /api/overviewwithdelivery_dateandlead_hours. FastAPI callssnapshot()on the repository, which reads cached January files. A fresh cache hit returns normalised datasets; on a cache miss, stale cache, or explicit refresh, the repository retrieves data from Elexon in 7-day chunks and persistsCSV.gz + metadata. It then returns actuals and all forecast publications. The API builds the delivery-period grid, applies the cutoff, matches data, computes metrics, and returns metrics, series, quality, and insights; the frontend updates the charts, readouts, and interpretation.
The application requirements specify no LLM API, paid API, paid database, or third-party service; the only external runtime dependency is the public Elexon BMRS Insights API. The captured requirements leave the exact endpoints, parameters, fields, metric formulas, error sign convention, and cache settings unspecified, and provide no measured application results. Separately, the document context records a January 2026 implementation using FUELHH actuals and WINDFOR forecasts, seven-day retrieval chunks, and rolling cutoffs defaulting to 12 hours with choices of 4, 8, 12, 24, 36, or 48 hours. It reports 71 passing backend tests, 1,488 actuals, and 22,776 forecasts, with a comparison rule that claims no winner when MAE differs by less than 2%. AERON measures forecast accuracy only—not procurement needs, prices, or imbalance exposure.
I am currently focused on AERON – Wind Forecast Intelligence and its role in supporting energy-trading decisions.
AERON is built using React, TypeScript, Python, FastAPI, and Pandas, with forecast validation, statistical accuracy metrics (MAE, RMSE, WAPE), and interactive comparisons.
The primary accuracy metrics are MAE, RMSE, WAPE.
48/48 forecast matches validated.
84 tests passed.