Featured Photo by Pixabay

**Table of Contents**:

- SeekingAlpha Long Ideas
- TradingView Insights
- IEA Mid-Term Outlook
- SARIMAX WTI Forecast
- SARIMAX Brent Forecast
- Summary
- Explore More
- Embed Socials

Let’s perform SARIMAX X-validation of EIA WTI and Brent oil prices forecast in the 2nd half of 2023. Recall that SARIMAX (Seasonal Autoregressive Integrated Moving Average with eXogenous factors) is an updated version of the ARIMA model for time series forecasting. SARIMAX is a seasonal equivalent to SARIMA that can deal with external temporal effects.

In fact, our Python forecast workflow implements the Time Series Analysis (TSA) approach where a series of data points are studied for a particular interval of time.

Conventionally, we download the input commodity dataset into Python with yahoo finance.

Before diving into the details of our algorithm, we need first to summarize the current state-of-the-art and a short-term outlook of the energy market.

Oil & Companies News 24/01/2023:

- According to Kamco Invest, oil prices continued to remain volatile at the start of 2023 and went below the $80 per barrel mark after steep declines on the first two consecutive days at the start of the year.
- The Paris-based EIA significantly lowered its price forecast for Brent crude oil for 2023 and 2024. The agency in its short term energy outlook lowered Brent price forecast to $83.1 per barrel for 2023 versus its previous forecast of $92.3 per barrel. This compares to the 2022 average price of $100.94 per barrel i.e. a decline of 18 per cent in 2023. The forecast for 2024 was further lower at $77.57, a y-o-y decline of 6.6 per cent. In terms of monthly trend, Brent crude averaged at $80.4 during December-2022 after witnessing a monthly decline of 11.8 per cent, the biggest decline since April-2020. The decline in Opec crude basket was similar at 11.2 per cent to average at $79.7 per barrel.

## SeekingAlpha Long Ideas

- Crude oil prices have continued to decline in the last six months.
- Chevron’s high profitability will not stand in a market of lower crude oil prices.
- Management is making a big mistake repurchasing stock at top dollar prices.

## TradingView Insights

WTI 1Y Chart:

Technical analysis summary for Light Crude Oil Futures:

Brent 1Y Chart:

Short Ideas:

## IEA Mid-Term Outlook

We forecast the Brent price will stay relatively flat through 2Q23, averaging $85/b, and then decline through the end of 2024. We expect the Brent price will average $83/b in 2023 and $78/b in 2024, down from $101/b in 2022. The West Texas Intermediate (WTI) price (the U.S. benchmark price) is forecast to generally follow a similar path, averaging $77/b in 2023 and $72/b 2024.

**Principal contributor: **Matthew French

**Tags: **production/supply, consumption/demand, spot prices, STEO (Short-Term Energy Outlook), Brent, crude oil, oil/petroleum, WTI (West Texas Intermediate)

## SARIMAX WTI Forecast

Let’s set the working directory YOURPATH

import os

os.chdir(‘YOURPATH’)

os. getcwd()

and import the key libraries

import numpy as np

import pandas as pd

import yfinance as yf

from matplotlib import pyplot as plt

from statsmodels.tsa.stattools import adfuller

from statsmodels.tsa.seasonal import seasonal_decompose

from statsmodels.tsa.arima_model import ARIMA

from pandas.plotting import register_matplotlib_converters

register_matplotlib_converters()

Let’s read the input data

df = yf.download(‘CL=F’, ‘2022-02-03’)

[*********************100%***********************] 1 of 1 completed

and check the content as

df.tail()

Let’s drop the unwanted columns to focus on “Adj Close”

df=df.drop([‘Open’, ‘High’, ‘Low’, ‘Close’,’Volume’], axis=1)

df.tail()

and check the null values if any

df.isnull().sum()

Adj Close 0 dtype: int64

Let’s perform the ETS decomposition of this column with model=’additive’ and period=30 (1 month)

result = seasonal_decompose(df, model=’additive’,period=30)

result.plot()

Recall that ETS stands for Error-Trend-Seasonality and is a model used for the time series decomposition. It decomposes the series into the error, trend and seasonality component. It is a univariate forecasting model used when dealing with time-series data. It focuses on trend and seasonal components.

Let’s try ETS with model=’multiplicative’

result = seasonal_decompose(df, model=’multiplicative’,period=30)

result.plot()

Let’s check the ADF test

adfuller(df[‘Adj Close’])

(-0.9251431077004176, 0.7795797169900887, 6, 247, {'1%': -3.457105309726321, '5%': -2.873313676101283, '10%': -2.5730443824681606}, 1205.1876481202412)

Recall that ADF (Augmented Dickey-Fuller) test is a statistical significance test which means the test will give results in hypothesis tests with null and alternative hypotheses. As a result, we will have a p-value from which we will need to make inferences about the time series, whether it is stationary or not.

**Remark:**

Following the TSA guide, we can verify our results using the TSA 1-day difference in the logarithmic scale

df[‘logarithm_base1’] = np.log2(df[‘Adj Close’])

data_d=df.diff(axis = 0, periods = 1)

Now let’s install pmarima

!pip install pmdarima

and import the library

from pmdarima import auto_arima

while ignoring harmless warnings

import warnings

warnings.filterwarnings(“ignore”)

Let’s fit auto_arima function to dataset

stepwise_fit = auto_arima(df[‘Adj Close’], start_p = 1, start_q = 1,

max_p = 3, max_q = 3, m = 12,

start_P = 0, seasonal = True,

d = None, D = 1, trace = True,

error_action =’ignore’,

suppress_warnings = True,

stepwise = True)

Let’s print the summary

stepwise_fit.summary()

stepwise_fit.summary()

Performing stepwise search to minimize aic ARIMA(1,0,1)(0,1,1)[12] intercept : AIC=1265.376, Time=0.49 sec ARIMA(0,0,0)(0,1,0)[12] intercept : AIC=1683.058, Time=0.01 sec ARIMA(1,0,0)(1,1,0)[12] intercept : AIC=1322.398, Time=0.22 sec ARIMA(0,0,1)(0,1,1)[12] intercept : AIC=1504.076, Time=0.25 sec ARIMA(0,0,0)(0,1,0)[12] : AIC=1682.663, Time=0.05 sec ARIMA(1,0,1)(0,1,0)[12] intercept : AIC=1370.656, Time=0.07 sec ARIMA(1,0,1)(1,1,1)[12] intercept : AIC=inf, Time=0.69 sec ARIMA(1,0,1)(0,1,2)[12] intercept : AIC=inf, Time=1.50 sec ARIMA(1,0,1)(1,1,0)[12] intercept : AIC=1323.947, Time=0.26 sec ARIMA(1,0,1)(1,1,2)[12] intercept : AIC=inf, Time=1.82 sec ARIMA(1,0,0)(0,1,1)[12] intercept : AIC=1264.151, Time=0.27 sec ARIMA(1,0,0)(0,1,0)[12] intercept : AIC=1368.690, Time=0.04 sec ARIMA(1,0,0)(1,1,1)[12] intercept : AIC=inf, Time=0.52 sec ARIMA(1,0,0)(0,1,2)[12] intercept : AIC=inf, Time=1.34 sec ARIMA(1,0,0)(1,1,2)[12] intercept : AIC=inf, Time=1.53 sec ARIMA(0,0,0)(0,1,1)[12] intercept : AIC=1684.885, Time=0.16 sec ARIMA(2,0,0)(0,1,1)[12] intercept : AIC=1265.457, Time=0.43 sec ARIMA(2,0,1)(0,1,1)[12] intercept : AIC=inf, Time=0.75 sec ARIMA(1,0,0)(0,1,1)[12] : AIC=1264.289, Time=0.21 sec Best model: ARIMA(1,0,0)(0,1,1)[12] intercept Total fit time: 10.635 seconds

Warnings: Covariance matrix calculated using the outer product of gradients (complex-step).

Let’s split our data into train / test sets

train = df.iloc[:len(df)-12]

test = df.iloc[len(df)-12:] # set one year(12 months) for testing

Let’s fit a SARIMAX(0, 1, 1)x(2, 1, 1, 12) on the training set:

from statsmodels.tsa.statespace.sarimax import SARIMAX

model = SARIMAX(train[‘Adj Close’],

order = (0, 1, 1),

seasonal_order =(2, 1, 1, 12))

result = model.fit()

result.summary()

Let’s check 1Y predictions against the test set

start = len(train)

end = len(train) + len(test) – 1

predictions = result.predict(start, end,

typ = ‘levels’).rename(“Predictions”)

predictions.plot(legend = True)

test[‘Adj Close’].plot(legend = True)

Let’s check the 1Y start/end monthly time stamps

print (start,end)

242 253

The X-plot test[‘Adj Close’] vs predictions is

We have the following table

print(test[‘Adj Close’],predictions)

Date 2023-01-19 80.330002 2023-01-20 81.309998 2023-01-23 81.620003 2023-01-24 80.129997 2023-01-25 80.150002 2023-01-26 81.010002 2023-01-27 79.680000 2023-01-30 77.900002 2023-01-31 78.870003 2023-02-01 76.410004 2023-02-02 75.879997 2023-02-03 73.230003 Name: Adj Close, dtype: float64 242 78.798398 243 78.018756 244 79.072597 245 78.460463 246 78.198427 247 78.346828 248 77.993672 249 77.691316 250 77.831578 251 78.591275 252 78.351060 253 77.758151 Name: Predictions, dtype: float64

In principle, we can obtain m (slope) and b(intercept) of a linear regression line

x=test[‘Adj Close’]

y=predictions

plt.plot(x,y, ‘o’, color=’green’)

m, b = np.polyfit(x, y, 1)

and use red as color for our linear regression line

plt.plot(x, m*x+b, color=’red’)

Let’s load the 2 evaluation metrics

from sklearn.metrics import mean_squared_error

from statsmodels.tools.eval_measures import rmse

to calculate the root mean squared (RMS) error

rmse(test[“Adj Close”], predictions)

2.3925166812892513

and the mean squared error (MSE)

mean_squared_error(test[“Adj Close”], predictions)

5.724136070247333

Let’s train the model on the full dataset

model = model = SARIMAX(df[‘Adj Close’],

order = (0, 1, 1),

seasonal_order =(2, 1, 1, 12))

result = model.fit()

forecast = result.predict(start = len(df),

end = (len(df)-1) + 1 * 12,

typ = ‘levels’).rename(‘Forecast’)

f[‘Adj Close’].plot(figsize = (2, 5), legend = True)

forecast.plot(legend = True)

Let’s print 1Y forecast

print(forecast)

54 73.326667 255 73.101625 256 73.790496 257 73.141656 258 72.914942 259 73.351366 260 72.530161 261 72.079887 262 72.225980 263 72.528791 264 71.986863 265 71.282966

and plot it

plt.plot(forecast)

plt.xlabel(“254+(Month Number 0-11)”);

plt.ylabel(“Predicted Oil Price $”);

Recall that

len(forecast)

12

This prediction is within the IEA forecast range 70 < WTI price < Brent price < 80 USD in 2023. Recall the IEA forecast: the average WTI price (the U.S. benchmark price) is ca. $77/b in 2023.

The above plot suggests that WTI price ~ 73 +/- 6 USD.

Indeed our forecast error is ca. 6 USD.

## SARIMAX Brent Forecast

Let’s look at the Brent Crude Oil (BZ=F)

f = yf.download(‘BZ=F’, ‘2022-02-03’)

[*********************100%***********************] 1 of 1 completed

df.tail()

The target variable is

df=df.drop([‘Open’, ‘High’, ‘Low’, ‘Close’,’Volume’], axis=1)

df.tail()

with no null values

df.isnull().sum()

Adj Close 0 dtype: int64

The ETS Decomposition is

result = seasonal_decompose(df, model=’additive’,period=30)

result.plot()

result = seasonal_decompose(df, model=’multiplicative’,period=30)

result.plot()

The ADF test is

adfuller(df[‘Adj Close’])

(-0.8024986505767407, 0.8183870058478762, 10, 243, {'1%': -3.4575505077947746, '5%': -2.8735087323013526, '10%': -2.573148434859185}, 1216.2416870507893)

Let’s fit auto_arima function to our dataset

stepwise_fit = auto_arima(df[‘Adj Close’], start_p = 1, start_q = 1,

max_p = 3, max_q = 3, m = 12,

start_P = 0, seasonal = True,

d = None, D = 1, trace = True,

error_action =’ignore’,

suppress_warnings = True,

stepwise = True)

stepwise_fit.summary()

Performing stepwise search to minimize aic ARIMA(1,1,1)(0,1,1)[12] : AIC=inf, Time=0.67 sec ARIMA(0,1,0)(0,1,0)[12] : AIC=1399.118, Time=0.01 sec ARIMA(1,1,0)(1,1,0)[12] : AIC=1346.586, Time=0.08 sec ARIMA(0,1,1)(0,1,1)[12] : AIC=inf, Time=0.71 sec ARIMA(1,1,0)(0,1,0)[12] : AIC=1399.664, Time=0.02 sec ARIMA(1,1,0)(2,1,0)[12] : AIC=1313.799, Time=0.22 sec ARIMA(1,1,0)(2,1,1)[12] : AIC=inf, Time=1.75 sec ARIMA(1,1,0)(1,1,1)[12] : AIC=inf, Time=0.63 sec ARIMA(0,1,0)(2,1,0)[12] : AIC=1311.810, Time=0.14 sec ARIMA(0,1,0)(1,1,0)[12] : AIC=1344.608, Time=0.05 sec ARIMA(0,1,0)(2,1,1)[12] : AIC=inf, Time=1.53 sec ARIMA(0,1,0)(1,1,1)[12] : AIC=inf, Time=0.52 sec ARIMA(0,1,1)(2,1,0)[12] : AIC=1313.799, Time=0.22 sec ARIMA(1,1,1)(2,1,0)[12] : AIC=1315.738, Time=0.46 sec ARIMA(0,1,0)(2,1,0)[12] intercept : AIC=1313.689, Time=0.58 sec Best model: ARIMA(0,1,0)(2,1,0)[12] Total fit time: 7.603 seconds

Let’s split our data into the train / test sets

train = df.iloc[:len(df)-12]

test = df.iloc[len(df)-12:] # set one year(12 months) for testing

Fit the best SARIMAX model to the training set

model = SARIMAX(train[‘Adj Close’],

order = (0, 1, 0),

seasonal_order =(2, 1, 0, 12))

result = model.fit()

result.summary()

Let’s make our predictions for one-year against the test set

start = len(train)

end = len(train) + len(test) – 1

predictions = result.predict(start, end,

typ = ‘levels’).rename(“Predictions”)

predictions.plot(legend = True)

test[‘Adj Close’].plot(legend = True)

let’s check the time/month stamp

print (start,end)

242 253

The X-plot predicted vs observed prices test data is

plt.scatter(test[‘Adj Close’],predictions)

plt.xlabel(‘Adj Close Test’)

plt.ylabel(‘Predictions’)

The corresponding table is

print(test[‘Adj Close’],predictions)

Date 2023-01-19 86.160004 2023-01-20 87.629997 2023-01-23 88.190002 2023-01-24 86.129997 2023-01-25 86.120003 2023-01-26 87.470001 2023-01-27 86.660004 2023-01-30 84.900002 2023-01-31 84.489998 2023-02-01 82.839996 2023-02-02 82.169998 2023-02-03 79.760002 Name: Adj Close, dtype: float64 242 84.737661 243 83.124420 244 84.063293 245 83.656751 246 83.973558 247 84.280618 248 84.402494 249 84.300495 250 84.832498 251 85.114817 252 84.739011 253 84.193353 Name: Predictions, dtype: float64

we can obtain m (slope) and b(intercept) of a linear regression line

x=test[‘Adj Close’]

y=predictions

plt.plot(x,y, ‘o’, color=’green’)

m, b = np.polyfit(x, y, 1)

using red as a color for our linear regression line

plt.plot(x, m*x+b, color=’red’)

plt.xlabel(‘Adj Close Test’)

plt.ylabel(‘Predictions’)

Let’s calculate the RMSE

rmse(test[“Adj Close”], predictions)

3.672425747417604

and the corresponding MSE

mean_squared_error(test[“Adj Close”], predictions)

13.486710870295747

Let’s train the model on the full dataset

model = model = SARIMAX(df[‘Adj Close’],

order = (0, 1, 1),

seasonal_order =(2, 1, 1, 12))

result = model.fit()

forecast = result.predict(start = len(df),

end = (len(df)-1) + 1 * 12,

typ = ‘levels’).rename(‘Forecast’)

df[‘Adj Close’].plot(figsize = (2, 5), legend = True)

forecast.plot(legend = True)

We can print the table

print(forecast)

254 80.319296 255 79.393284 256 80.083311 257 79.554758 258 79.718730 259 80.245928 260 79.837283 261 79.581158 262 79.910810 263 80.090715 264 79.351650 265 78.977657 Name: Forecast, dtype: float64

and plot the corresponding 1Y forecast

plt.plot(forecast)

plt.xlabel(“254+(Month Number 0-11)”);

plt.ylabel(“Predicted Brent Oil Price $”);

Recall that IEA expect the Brent price will average $83/b in 2023. This plot suggests that the average Brent price would be 80 +/-13 USD.

## Summary

- We have downgraded the EIA’s 2023 WTI/Brent oil price forecast. This appears to be consistent with the JPMorgan adjustment of its forecast. However, a fully effective transatlantic embargo would pull a considerable amount of oil from the market, leading to higher oil prices.

- Due to vulnerability to geopolitical developments, the oil market is characterized by significant uncertainties in 2023 and beyond. It turns out that relatively large error bars of our SARIMAX predicted oil price values support the expected volatility of the energy market in a single graph.

- A U.S. recession in 2023 could spark a new crude oil bear market, posing a significant challenge to Big Oil earnings and cash flow growth.
- Generally, as the economy transitions from peak productivity, high inflation, and labor shortages to a recession-like environment with softer economic activity, lower employment numbers, and potentially evaporating demand for crude oil and other energy sources, the market must expect an ongoing decline in profitability.

**Bottom Line:** Crude Oil Prices Are Set For Mean Reversion.

## Explore More

(S)ARIMA(X) TSA Forecasting, QC and Visualization of E-Commerce Food Delivery Sales

Stock Market ’22 Round Up & ’23 Outlook: Zacks Strategy vs Seeking Alpha Tactics

XOM SMA-EMA-RSI Golden Crosses ’22

Energy E&P: XOM Technical Analysis Nov ’22

The Zacks Market Outlook Nov ’22 – Energy

OXY Stock Technical Analysis 17 May 2022

## Embed Socials

#### Make a one-time donation

#### Make a monthly donation

#### Make a yearly donation

Choose an amount

Or enter a custom amount

Your contribution is appreciated.

Your contribution is appreciated.

Your contribution is appreciated.

DonateDonate monthlyDonate yearly