Back to Tutorials

Price Forecasting

ML-powered day-ahead price predictions

Overview

Our ML-powered price forecasting model predicts UK electricity prices for the next day, providing traders, energy managers, and developers with actionable insights for decision-making. The model uses XGBoost trained on historical prices, gas costs, and carbon intensity data.

Key Features

Day-Ahead Forecast

Predictions for all 48 settlement periods of the next day, updated regularly.

Gas Price Correlation

Model incorporates SAP gas prices - the primary driver of UK electricity costs.

Carbon Intensity

Factors in grid carbon intensity as a proxy for generation mix and demand patterns.

Summary Statistics

Average, min, max prices plus model accuracy metrics for risk assessment.

Model Accuracy & Limitations

Current model performance: MAPE ~24% (Mean Absolute Percentage Error). This means predictions are typically within ±24% of actual prices.

  • • Best for relative comparisons (peak vs off-peak timing)
  • • Less reliable during extreme market events (price spikes)
  • • Model trained on prices £20-200/MWh; extreme prices outside this range less accurate
  • • Always use alongside other market intelligence for critical decisions

API Endpoint

Fetch the day-ahead forecast with a simple GET request.

bash
# Get day-ahead price forecast
curl -X GET "https://api.energyoracle.io/uk/prices/forecast/day-ahead" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json"

# Response
{
  "target_date": "2025-01-12",
  "generated_at": "2025-01-11T16:30:00Z",
  "forecasts": [
    { "settlement_period": 1, "predicted_price": 72.45, "confidence": 0.7, "unit": "GBP/MWh" },
    { "settlement_period": 2, "predicted_price": 71.20, "confidence": 0.7, "unit": "GBP/MWh" },
    // ... 48 settlement periods
  ],
  "summary": {
    "average_price": 78.50,
    "min_price": 65.30,
    "max_price": 105.20,
    "settlement_periods": 48,
    "unit": "GBP/MWh"
  },
  "inputs": {
    "gas_sap_price": 2.85,
    "gas_7day_avg": 2.90,
    "carbon_intensity": 185
  },
  "model": {
    "type": "XGBoost",
    "trained_at": "2025-01-10T14:30:00Z",
    "metrics": { "cv_mape_mean": 24.3, "cv_mae_mean": 12.50 }
  }
}

JavaScript/TypeScript

Find optimal consumption windows using the forecast data.

typescript
import { EnergyOracle } from '@energyoracle/sdk';

const oracle = new EnergyOracle({ apiKey: 'your-key' });

// Get day-ahead forecast
const forecast = await oracle.uk.prices.forecast.dayAhead();

console.log(`Tomorrow's average: £${forecast.summary.average_price}/MWh`);
console.log(`Price range: £${forecast.summary.min_price} - £${forecast.summary.max_price}`);

// Find optimal time windows for high consumption
const cheapestPeriods = forecast.forecasts
  .sort((a, b) => a.predicted_price - b.predicted_price)
  .slice(0, 6);  // Cheapest 3 hours

console.log('Best time to consume:');
cheapestPeriods.forEach(p => {
  const hour = Math.floor((p.settlement_period - 1) / 2);
  const min = ((p.settlement_period - 1) % 2) * 30;
  console.log(`  ${hour.toString().padStart(2, '0')}:${min.toString().padStart(2, '0')} - £${p.predicted_price}/MWh`);
});

Python with Pandas

Analyze peak and off-peak pricing with pandas for load shifting decisions.

python
from energyoracle import EnergyOracle
import pandas as pd

oracle = EnergyOracle(api_key="your-key")

# Get forecast for tomorrow
forecast = oracle.uk.prices.forecast.day_ahead()

# Convert to DataFrame for analysis
df = pd.DataFrame(forecast.forecasts)

# Add time column
df['hour'] = (df['settlement_period'] - 1) // 2
df['minute'] = ((df['settlement_period'] - 1) % 2) * 30
df['time'] = df.apply(lambda x: f"{x['hour']:02d}:{x['minute']:02d}", axis=1)

# Peak period analysis (7am-10am and 4:30pm-9pm)
morning_peak = df[(df['settlement_period'] >= 14) & (df['settlement_period'] <= 20)]
evening_peak = df[(df['settlement_period'] >= 33) & (df['settlement_period'] <= 42)]
off_peak = df[~df.index.isin(morning_peak.index) & ~df.index.isin(evening_peak.index)]

print(f"Morning Peak Avg: £{morning_peak['predicted_price'].mean():.2f}/MWh")
print(f"Evening Peak Avg: £{evening_peak['predicted_price'].mean():.2f}/MWh")
print(f"Off-Peak Avg: £{off_peak['predicted_price'].mean():.2f}/MWh")

# Calculate potential savings by shifting load
peak_avg = pd.concat([morning_peak, evening_peak])['predicted_price'].mean()
savings_per_mwh = peak_avg - off_peak['predicted_price'].mean()
print(f"Potential savings by load shifting: £{savings_per_mwh:.2f}/MWh")

Use Cases

1. Trading Signal Generation

Compare forecast vs current prices to identify trading opportunities.

typescript
// Trading decision support based on forecast
async function evaluateTradingOpportunity(oracle) {
  const forecast = await oracle.uk.prices.forecast.dayAhead();
  const currentPrice = await oracle.uk.prices.latest();

  const signals = [];

  // Compare forecast average vs current
  const priceDelta = forecast.summary.average_price - currentPrice.price;

  if (Math.abs(priceDelta) > 5) {
    signals.push({
      type: priceDelta > 0 ? 'BUY_NOW' : 'WAIT',
      reason: `Tomorrow expected ${priceDelta > 0 ? 'higher' : 'lower'} by £${Math.abs(priceDelta).toFixed(2)}/MWh`,
      confidence: forecast.model.metrics.cv_mape_mean < 15 ? 'HIGH' : 'MEDIUM'
    });
  }

  // Identify extreme price periods
  const highPricePeriods = forecast.forecasts
    .filter(f => f.predicted_price > forecast.summary.average_price * 1.3);

  if (highPricePeriods.length > 0) {
    signals.push({
      type: 'SELL_OPPORTUNITY',
      reason: `${highPricePeriods.length} periods expected 30%+ above average`,
      periods: highPricePeriods.map(p => p.settlement_period)
    });
  }

  return signals;
}

2. Risk Management

Calculate uncertainty bands based on model accuracy for hedging decisions.

typescript
// Risk management with forecast confidence
function assessForecastRisk(forecast) {
  const mape = forecast.model.metrics.cv_mape_mean;

  // Calculate price uncertainty band
  const avgPrice = forecast.summary.average_price;
  const uncertainty = avgPrice * (mape / 100);

  return {
    expectedPrice: avgPrice,
    lowerBound: avgPrice - uncertainty,
    upperBound: avgPrice + uncertainty,
    confidenceLevel: mape < 15 ? 'HIGH' : mape < 25 ? 'MEDIUM' : 'LOW',

    // Risk metrics
    maxExposure: forecast.summary.max_price * 1.1,  // 10% buffer on max
    valueAtRisk: uncertainty * 2,  // 2-sigma VaR approximation

    recommendation: mape > 25
      ? 'Consider hedging - model uncertainty is elevated'
      : 'Normal market conditions'
  };
}

// Example usage
const risk = assessForecastRisk(forecast);
console.log(`Expected: £${risk.expectedPrice.toFixed(2)} (±£${risk.valueAtRisk.toFixed(2)})`);
console.log(`Confidence: ${risk.confidenceLevel}`);
console.log(`Recommendation: ${risk.recommendation}`);

3. Load Scheduling (EV Charging)

Optimize flexible loads like EV charging to minimize costs.

typescript
// Optimal EV charging schedule based on forecast
async function createChargingSchedule(oracle, requiredKwh, maxPowerKw) {
  const forecast = await oracle.uk.prices.forecast.dayAhead();

  // Calculate how many periods needed
  const periodsNeeded = Math.ceil(requiredKwh / (maxPowerKw * 0.5)); // 30-min periods

  // Sort by price (cheapest first)
  const ranked = [...forecast.forecasts]
    .sort((a, b) => a.predicted_price - b.predicted_price);

  // Select cheapest periods
  const schedule = ranked.slice(0, periodsNeeded).map(p => {
    const hour = Math.floor((p.settlement_period - 1) / 2);
    const min = ((p.settlement_period - 1) % 2) * 30;
    return {
      start: `${hour.toString().padStart(2, '0')}:${min.toString().padStart(2, '0')}`,
      end: `${min === 0 ? hour.toString().padStart(2, '0') : (hour + 1).toString().padStart(2, '0')}:${min === 0 ? '30' : '00'}`,
      powerKw: maxPowerKw,
      expectedCost: p.predicted_price * (maxPowerKw * 0.5) / 1000,
      period: p.settlement_period
    };
  }).sort((a, b) => a.period - b.period);

  const totalCost = schedule.reduce((sum, s) => sum + s.expectedCost, 0);
  const avgPriceAll = forecast.summary.average_price;
  const savings = (avgPriceAll * requiredKwh / 1000) - totalCost;

  return {
    schedule,
    totalCost,
    savings,
    savingsPercent: (savings / (avgPriceAll * requiredKwh / 1000)) * 100
  };
}

// Example: Charge 50 kWh at max 7 kW
const plan = await createChargingSchedule(oracle, 50, 7);
console.log(`Charging schedule saves £${plan.savings.toFixed(2)} (${plan.savingsPercent.toFixed(1)}%)`);

Model Details

AlgorithmXGBoost Regressor
Training Data~35,000 samples (prices £20-200/MWh)
Validation5-fold Time Series Cross-Validation
Key FeaturesGas SAP price, Carbon intensity, Time patterns, Price lags
MAPE~24% (on validation set)
MAE~£12.50/MWh

The model is retrained regularly as new data becomes available. Check the model.trained_at field in the response for the last training timestamp.

Best Practices

1. Use for Timing, Not Absolute Values

The forecast is most reliable for comparing relative prices across periods. Use it to identify when prices will be higher or lower, not to predict exact values.

2. Monitor Model Metrics

Check model.metrics.cv_mape_mean in the response. Higher MAPE indicates increased uncertainty - consider wider hedging margins.

3. Combine with Market Context

The forecast uses historical patterns. For unusual events (outages, weather extremes, holidays), supplement with your own market knowledge and other data sources.

4. Cache Appropriately

Forecasts are updated periodically (check generated_at). Cache responses for 15-30 minutes to reduce API calls while maintaining freshness.

Next Steps