Combining BLS Fuel Oil Prices with Brent Crude Futures
Summary
This example shows how to use U.S. Bureau of Labor Statistics average fuel oil prices as a macroeconomic signal alongside Brent crude futures. It demonstrates inspecting datasets and metadata, filtering for national series with long histories, loading monthly observations, and removing annual-average records. The strategy then compares recent fuel oil price changes with Brent futures price movement: rising fuel prices plus a recent futures rise produce long exposure, while a sequence of fuel price declines plus a longer futures decline produces short exposure. Otherwise, it returns no exposure, with an initial buy-and-hold weight used when state is unset.
The example runs through a futures backtester with a 365-day lookback and starts in 2006. It explains the data selection and signal construction, but provides no reported returns, risk metrics, or evidence that the relationship is predictive. The code relies on publication dates and macro series availability, so timing and release delays would need attention in a realistic evaluation. The small set of hand-written conditions is illustrative rather than a validated general strategy.
Key ideas
- BLS average fuel oil price data can be paired with Brent crude futures in a macro trading signal.
- The example filters the data to national series with long histories and removes annual-average observations.
- Rising fuel oil prices and recent Brent strength trigger long exposure; sustained declines alongside futures weakness trigger short exposure.
- The backtest uses a 365-day lookback and begins in 2006, but reports no performance results.
- Publication timing and availability of macroeconomic observations matter when evaluating the method.
Tags
Full text
# strategy-futures-bls
# strategy-futures-bls
## Source (MIT)
```python
# # Using BLS Data
#
# The [**U.S. Bureau of Labor Statistics**](https://www.bls.gov) is the principal agency for the U.S. government in the field of labor economics and statistics. It provides macroeconomic data in several interesting categories: prices, employment and unemployment, compensation and working conditions and productivity.
#
# Quantiacs has implemented these datasets on its cloud and makes them also available for local use on your machine.
#
# In this template we show how to use the BLS data for creating a trading algorithm.
#
# **Need help?** Check the [**Documentation**](https://quantiacs.com/documentation/en/) and find solutions/report problems in the [**Forum**](https://quantiacs.com/community/categories) section.
#
# **More help with Jupyter?** Check the official [**Jupyter**](https://jupyter.org/) page.
#
# **Check the BLS documentation** on the Quantiacs [**macroeconomics help page**](https://quantiacs.com/documentation/en/user_guide/macro.html).
#
# Once you are done, click on **Submit to the contest** and take part to our competitions.
#
# API reference:
#
# * **data**: check how to work with [data](https://quantiacs.com/documentation/en/reference/data_load_functions.html);
#
# * **backtesting**: read how to run the [simulation](https://quantiacs.com/documentation/en/reference/evaluation.html) and check the results.
#
# Need to use the optimizer function to automate tedious tasks?
#
# * **optimization**: read more on our [article](https://quantiacs.com/community/topic/29/optimizing-and-monitoring-a-trading-system-with-quantiacs).
import pandas as pd
import numpy as np
import qnt.data as qndata
%%javascript
window.IPython && (IPython.OutputArea.prototype._should_scroll = function(lines) { return false; })
// disable widget scrolling
# First of all we list the 34 available datasets and inspect them:
dbs = qndata.blsgov.load_db_list()
display(pd.DataFrame(dbs)) # convert to pandas for better formatting
# For each dataset you can see the identifier, the name and the date of the last available update. Each dataset contains several time series which can be used as indicators.
#
# In this example we use `AP`. Average consumer Prices are calculated for household fuel, motor fuel and food items from prices collected for the Consumer Price Index (CPI). The full description is available in the [metadata](#Inspect-the-metadata).
#
# Let us load and display the time series contained in the `AP` dataset:
series_list = list(qndata.blsgov.load_series_list('AP'))
display(pd.DataFrame(series_list).set_index('id')) # convert to pandas for better formatting
# As you see, the `AP` Average Price Data dataset contains 1479 time series.
#
# Let us see how we can learn the meaning of the 8 columns. Some of them are obvious, like `series_title`, `begin_year` or `end_year`, but others are not, like `area_code`, `item_code`, `begin_period`, `end_period`.
#
# ### Inspect the metadata
#
# The Quantiacs toolbox allows you to inspect the meaning of all fields:
meta = qndata.blsgov.load_db_meta('AP')
for k in meta.keys():
print('### ' + k + " ###")
m = meta[k]
if type(m) == str:
# Show only the first line if this is a text entry.
print(m.split('\n')[0])
print('...')
# Uncomment the next line to see the full text. It will give you more details about the database.
# print(m)
if type(m) == dict:
# convert dictionaries to pandas DataFrame for better formatting:
df = pd.DataFrame(meta[k].values())
df = df.set_index(np.array(list(meta[k].keys())))
display(df)
# These tables allows you to quickly understand the meaning of the fields for each times series in the Average Price Data.
#
# The `area_code` column reflects the U.S. area connected to the time series, for example 0000 for the entire U.S.
#
# Let us select only time series related to the entire U.S.:
us_series_list = [s for s in series_list if s['area_code'] == '0000']
display(pd.DataFrame(us_series_list).set_index('id')) # convert to pandas for better formatting
# We have 160 time series out of the original 1479. These are global U.S. time series which are more relevant for forecasting global financial markets. Let us select time series which are currently being updated and have at least 20 years of history:
actual_us_series_list = [s for s in us_series_list if s['begin_year'] <= '2000' and s['end_year'] == '2021' ]
display(pd.DataFrame(actual_us_series_list).set_index('id')) # convert to pandas for better formatting
len(actual_us_series_list)
# We have 55 time series whose history is long enough for our purpose. Now we can load one of these series and use it for our strategy. Let us focus on energy markets. We consider fuel oil `APU000072511` on a monthly basis:
series_data = qndata.blsgov.load_series_data('APU000072511', tail = 30*365)
# convert to pandas.DataFrame
series_data = pd.DataFrame(series_data)
series_data = series_data.set_index('pub_date')
# remove yearly average data, see period dictionary
series_data = series_data[series_data['period'] != 'M13']
series_data
# Next, let us consider Futures contracts in the Energy sector:
futures_list = qndata.futures_load_list()
energy_futures_list = [f for f in futures_list if f['sector'] == 'Energy']
pd.DataFrame(energy_futures_list)
# We consider Brent Crude Oil, `F_BC`, and define a strategy using a multi-pass approach:
import xarray as xr
import numpy as np
import pandas as pd
import qnt.ta as qnta
import qnt.backtester as qnbt
import qnt.data as qndata
def load_data(period):
futures = qndata.futures_load_data(assets=['F_BC'], tail=period, dims=('time','field','asset'))
ap = qndata.blsgov.load_series_data('APU000072511', tail=period)
# convert to pandas.DataFrame
ap = pd.DataFrame(ap)
ap = ap.set_index('pub_date')
# remove yearly average data, see period dictionary
ap = ap[ap['period'] != 'M13']
# convert to xarray
ap = ap['value'].to_xarray().rename(pub_date='time').assign_coords(time=pd.to_datetime(ap.index.values))
# return both time series
return dict(ap=ap, futures=futures), futures.time.values
def window(data, max_date: np.datetime64, lookback_period: int):
# the window function isolates data which are needed for one iteration
# of the backtester call
min_date = max_date - np.timedelta64(lookback_period, 'D')
return dict(
futures = data['futures'].sel(time=slice(min_date, max_date)),
ap = data['ap'].sel(time=slice(min_date, max_date))
)
def strategy(data, state):
close = data['futures'].sel(field='close')
ap = data['ap']
# the strategy complements indicators based on the Futures price with macro data
# and goes long/short or takes no exposure:
if ap.isel(time=-1) > ap.isel(time=-2) \
and close.isel(time=-1) > close.isel(time=-20):
return xr.ones_like(close.isel(time=-1)), 1
elif ap.isel(time=-1) < ap.isel(time=-2) \
and ap.isel(time=-2) < ap.isel(time=-3) \
and ap.isel(time=-3) < ap.isel(time=-4) \
and close.isel(time=-1) < close.isel(time=-40):
return -xr.ones_like(close.isel(time=-1)), 1
# When the state is None, we are in the beginning and no weights were generated.
# We use buy'n'hold to fill these first days.
elif state is None:
return xr.ones_like(close.isel(time=-1)), None
else:
return xr.zeros_like(close.isel(time=-1)), 1
weights, state = qnbt.backtest(
competition_type='futures',
load_data=load_data,
window=window,
lookback_period=365,
start_date="2006-01-01",
strategy=strategy,
analyze=True,
build_plots=True
)
```Shown in full with attribution under the source's licence. Licence: MIT
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.