Historical Oil Price Data API Guide: Best Sources for Backtesting
Learn how to access historical oil price data via API for backtesting trading strategies, trend analysis, and academic research. Compare data sources (OilPriceAPI, EIA, Bloomberg), understand pricing and frequency, and see code examples for fetching and analyzing decades of crude oil prices.
Why Historical Oil Price Data Matters
If you're building trading algorithms, analyzing energy markets, or conducting academic research, you need historical oil price data. Real-time prices tell you what's happening now. Historical data tells you what patterns exist, how strategies would have performed, and what volatility to expect.
Without historical data, you're trading blind. You can't backtest moving average crossovers, can't calculate historical volatility for risk models, and can't validate that your strategy would have been profitable over the past decade. Historical data turns guesswork into data-driven decision making.
What You'll Learn
- ✓ Compare 5 major historical oil price data sources (cost, coverage, frequency)
- ✓ Understand API endpoints and query parameters for historical data
- ✓ Fetch years of WTI and Brent prices with Python and Node.js
- ✓ Calculate moving averages, volatility, and correlations
- ✓ Backtest trading strategies with real historical data
- ✓ Best practices for caching, validation, and timezone handling
Key Use Cases for Historical Oil Price Data
1. Backtesting Trading Strategies
Test your trading algorithm against years of historical data to see if it would have been profitable. Calculate metrics like Sharpe ratio, maximum drawdown, and win rate.
- • Test moving average crossover strategies (MA50 vs MA200)
- • Validate momentum indicators (RSI, MACD)
- • Backtest spread trading (WTI-Brent arbitrage)
- • Optimize entry/exit parameters
2. Trend Analysis & Forecasting
Identify long-term trends, seasonal patterns, and cyclical behavior to forecast future prices.
- • Seasonal decomposition (summer gasoline demand spikes)
- • ARIMA models for price prediction
- • Long-term trend identification (10-year moving averages)
- • Correlation with macroeconomic indicators (USD, interest rates)
3. Risk Modeling
Calculate historical volatility, Value at Risk (VaR), and other risk metrics for portfolio management.
- • Historical volatility calculations (30-day, 90-day, annual)
- • VaR and CVaR for risk assessment
- • Monte Carlo simulations using historical distribution
- • Stress testing portfolios against historical crashes
4. Academic Research
Publish papers on energy markets, test economic theories, and validate financial models.
- • Energy economics research (supply/demand elasticity)
- • Correlation studies (oil vs stock markets, currencies)
- • Geopolitical event impact analysis
- • Thesis/dissertation data collection
5. Financial Modeling
Build DCF models, valuation analyses, and scenario planning for oil companies.
- • Oil company valuation (revenue projections based on historical prices)
- • Scenario analysis (high/medium/low price scenarios)
- • Break-even analysis for drilling projects
- • Merger & acquisition modeling
Data Source Comparison Table
Not all historical data sources are equal. Here's an honest comparison of the five major options:
| Source | History Depth | Frequency | Cost | Format | Best For |
|---|---|---|---|---|---|
| OilPriceAPI | Multi-year (varies by commodity) | Source-dependent intervals | $19-$299/mo | JSON, CSV | Intraday backtesting |
| EIA | Decades (1980s+) | Daily observations, released weekly (1-8 days behind) | Free | JSON | Long-term analysis |
| Quandl | Varies by dataset | Daily to tick | $50-$500+/mo | JSON, CSV | Academic research |
| Bloomberg | Decades | Tick-level | $24k/year | Excel, API | Institutional trading |
| Refinitiv | Decades | Tick-level | $20-22k/year | Excel, API | Enterprise trading |
Our Recommendation
For most traders and analysts, start with the EIA for free daily data to validate your approach. Once you need intraday frequency for backtesting, upgrade to OilPriceAPI ($19-$299/month) for multi-year, source-timestamped intraday data.
Bloomberg/Refinitiv are only worth the $20k+/year cost if you need tick-level data forhigh-frequency trading or have institutional requirements.
How to Access Historical Data via API
Most historical data APIs use similar query patterns: specify a commodity, date range, and optional interval. Here's how it works across different providers.
REST Endpoint Structure
# OilPriceAPI GET /v1/prices/historical ?by_code=WTI_USD &start_date=2024-01-01 &end_date=2024-12-31 &interval=daily # EIA GET /v2/petroleum/pri/spt/data/ ?api_key=YOUR_KEY &frequency=daily &data[0]=value &facets[product][]=EPCBRENT &start=2024-01-01 &end=2024-12-31
Common Query Parameters
- commodity / by_code: Which oil benchmark (WTI_USD, BRENT_CRUDE_USD)
- start_date / end_date: Date range (ISO 8601 format: YYYY-MM-DD)
- interval / frequency: Data granularity (daily, hourly, 5-minute)
- limit / offset: Pagination for large datasets
- format: Response format (json, csv, xml)
Pagination for Large Datasets
Fetching years of intraday data returns millions of records. Most APIs paginate responses:
# Request page 1
GET /v1/prices/historical?by_code=WTI_USD&start_date=2020-01-01&end_date=2024-12-31&limit=10000&offset=0
# Response includes pagination
{
"data": [...10000 records...],
"pagination": {
"total": 105120,
"limit": 10000,
"offset": 0,
"next": "/v1/prices/historical?...&offset=10000"
}
}
# Request page 2
GET /v1/prices/historical?...&offset=10000Code Examples: Data Fetching
Example 1: Python - Fetch 5 Years of WTI Data
from oilpriceapi import Client
from datetime import datetime, timedelta
import pandas as pd
client = Client(api_key="your_api_key_here")
# Define date range
end_date = datetime.now()
start_date = end_date - timedelta(days=365 * 5) # 5 years
print(f"Fetching WTI data from {start_date.date()} to {end_date.date()}...")
# Fetch historical data
historical_data = client.get_historical_prices(
by_code="WTI_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d")
)
# Convert to DataFrame
df = pd.DataFrame(historical_data['data'])
df['created_at'] = pd.to_datetime(df['created_at'])
df = df.sort_values('created_at')
print(f"\nFetched {len(df)} data points")
print(f"Date range: {df['created_at'].min()} to {df['created_at'].max()}")
print(f"Price range: $"+f"{df['price'].min():.2f} to $"+f"{df['price'].max():.2f}")
# Save to CSV
df.to_csv('wti_5year_history.csv', index=False)
print("\nData saved to wti_5year_history.csv")Example 2: Node.js - Async Batch Fetching
const { OilPriceAPIClient } = require('@oilpriceapi/client');
const fs = require('fs');
const client = new OilPriceAPIClient({
apiKey: process.env.OILPRICEAPI_KEY
});
async function fetchHistoricalData(commodity, years) {
const endDate = new Date();
const startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - years);
console.log(`Fetching \${years} years of \${commodity} data...`);
const data = await client.getHistoricalPrices({
code: commodity,
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0]
});
console.log(`Fetched \${data.data.length} records`);
// Save to JSON file
const filename = `\${commodity}_\${years}year_history.json`;
fs.writeFileSync(filename, JSON.stringify(data, null, 2));
console.log(`Saved to \${filename}`);
return data;
}
// Fetch multiple commodities in parallel
async function fetchMultipleCommodities() {
const commodities = ['WTI_USD', 'BRENT_CRUDE_USD', 'NATURAL_GAS_USD'];
const promises = commodities.map(code =>
fetchHistoricalData(code, 3)
);
await Promise.all(promises);
console.log('\nAll data fetched successfully!');
}
fetchMultipleCommodities();Example 3: cURL - Command-Line Access
# Fetch 1 year of Brent data as CSV curl -X GET "https://api.oilpriceapi.com/v1/prices/historical" \ -H "Authorization: Token YOUR_API_KEY" \ -G \ --data-urlencode "by_code=BRENT_CRUDE_USD" \ --data-urlencode "start_date=2024-01-01" \ --data-urlencode "end_date=2024-12-31" \ --data-urlencode "format=csv" \ > brent_2024.csv echo "Data saved to brent_2024.csv" # Preview first 10 lines head -n 10 brent_2024.csv
Example 4: Excel add-in formula path
=OILPRICE.GET("/v1/prices/historical", "by_code=WTI_USD&start_date=2020-01-01&end_date=2024-12-31")Code Examples: Data Analysis
Example 5: Calculate Moving Averages
import pandas as pd
from oilpriceapi import Client
from datetime import datetime, timedelta
client = Client(api_key="your_api_key_here")
# Fetch 1 year of WTI data
end_date = datetime.now()
start_date = end_date - timedelta(days=365)
data = client.get_historical_prices(
by_code="WTI_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d")
)
# Create DataFrame
df = pd.DataFrame(data['data'])
df['created_at'] = pd.to_datetime(df['created_at'])
df = df.set_index('created_at').sort_index()
# Calculate moving averages
df['MA_7'] = df['price'].rolling(window=7).mean()
df['MA_30'] = df['price'].rolling(window=30).mean()
df['MA_90'] = df['price'].rolling(window=90).mean()
# Current values
print("WTI Moving Averages")
print("=" * 50)
print(f"Current Price: \\$\{df['price'].iloc[-1]:.2f\}")
print(f"7-day MA: \\$\{df['MA_7'].iloc[-1]:.2f\}")
print(f"30-day MA: \\$\{df['MA_30'].iloc[-1]:.2f\}")
print(f"90-day MA: \\$\{df['MA_90'].iloc[-1]:.2f\}")
# Trend identification
if df['MA_7'].iloc[-1] > df['MA_30'].iloc[-1] > df['MA_90'].iloc[-1]:
print("\nTrend: STRONG UPTREND (all MAs ascending)")
elif df['MA_7'].iloc[-1] < df['MA_30'].iloc[-1] < df['MA_90'].iloc[-1]:
print("\nTrend: STRONG DOWNTREND (all MAs descending)")
else:
print("\nTrend: MIXED (choppy market)")Example 6: Volatility Calculation
import pandas as pd
import numpy as np
from oilpriceapi import Client
from datetime import datetime, timedelta
client = Client(api_key="your_api_key_here")
# Fetch 2 years of data
end_date = datetime.now()
start_date = end_date - timedelta(days=730)
data = client.get_historical_prices(
by_code="BRENT_CRUDE_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d")
)
df = pd.DataFrame(data['data'])
df['created_at'] = pd.to_datetime(df['created_at'])
df = df.set_index('created_at').sort_index()
# Calculate daily returns
df['returns'] = df['price'].pct_change()
# Rolling volatility (annualized)
df['volatility_30d'] = df['returns'].rolling(window=30).std() * np.sqrt(252)
df['volatility_90d'] = df['returns'].rolling(window=90).std() * np.sqrt(252)
print("Brent Crude Volatility Analysis")
print("=" * 50)
print(f"30-day volatility: {df['volatility_30d'].iloc[-1]:.2%}")
print(f"90-day volatility: {df['volatility_90d'].iloc[-1]:.2%}")
print(f"1-year volatility: {df['returns'].std() * np.sqrt(252):.2%}")
# Value at Risk (95% confidence)
var_95 = df['returns'].quantile(0.05)
print(f"\nValue at Risk (95%): {var_95:.2%} daily loss")
print(f"Dollar VaR on $100k position: \\$\{abs(var_95 * 100000):.2f\}")Example 7: Seasonal Decomposition
import pandas as pd
from statsmodels.tsa.seasonal import seasonal_decompose
from oilpriceapi import Client
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
client = Client(api_key="your_api_key_here")
# Fetch 3 years of daily data
end_date = datetime.now()
start_date = end_date - timedelta(days=365 * 3)
data = client.get_historical_prices(
by_code="WTI_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d")
)
df = pd.DataFrame(data['data'])
df['created_at'] = pd.to_datetime(df['created_at'])
df = df.set_index('created_at').sort_index()
# Resample to daily if intraday data
df_daily = df['price'].resample('D').mean().dropna()
# Seasonal decomposition
decomposition = seasonal_decompose(df_daily, model='additive', period=365)
# Plot components
fig, axes = plt.subplots(4, 1, figsize=(12, 10))
df_daily.plot(ax=axes[0], title='Original Price')
decomposition.trend.plot(ax=axes[1], title='Trend')
decomposition.seasonal.plot(ax=axes[2], title='Seasonal')
decomposition.resid.plot(ax=axes[3], title='Residual')
plt.tight_layout()
plt.savefig('seasonal_decomposition.png')
print("Seasonal decomposition saved to seasonal_decomposition.png")
# Print seasonal insights
seasonal = decomposition.seasonal
print("\nSeasonal Insights:")
print(f"Peak seasonal effect: {seasonal.max():.2f} in {seasonal.idxmax().strftime('%B')}")
print(f"Low seasonal effect: {seasonal.min():.2f} in {seasonal.idxmin().strftime('%B')}")Example 8: Backtest Moving Average Crossover
import pandas as pd
from oilpriceapi import Client
from datetime import datetime, timedelta
client = Client(api_key="your_api_key_here")
# Fetch 5 years for backtesting
end_date = datetime.now()
start_date = end_date - timedelta(days=365 * 5)
data = client.get_historical_prices(
by_code="WTI_USD",
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d")
)
df = pd.DataFrame(data['data'])
df['created_at'] = pd.to_datetime(df['created_at'])
df = df.set_index('created_at').sort_index()
# Calculate moving averages
df['MA_50'] = df['price'].rolling(window=50).mean()
df['MA_200'] = df['price'].rolling(window=200).mean()
# Generate signals
df['signal'] = 0
df.loc[df['MA_50'] > df['MA_200'], 'signal'] = 1 # Long signal
df.loc[df['MA_50'] < df['MA_200'], 'signal'] = -1 # Short signal
# Calculate position changes (trades)
df['position'] = df['signal'].shift()
df['trade'] = df['signal'].diff()
# Calculate returns
df['returns'] = df['price'].pct_change()
df['strategy_returns'] = df['position'] * df['returns']
# Performance metrics
total_return = (1 + df['strategy_returns']).prod() - 1
buy_hold_return = (df['price'].iloc[-1] / df['price'].iloc[0]) - 1
num_trades = (df['trade'] != 0).sum()
print("Backtest Results: MA(50) vs MA(200) Crossover")
print("=" * 50)
print(f"Strategy Return: {total_return:.2%}")
print(f"Buy & Hold Return: {buy_hold_return:.2%}")
print(f"Outperformance: {(total_return - buy_hold_return):.2%}")
print(f"Number of Trades: {num_trades}")
# Sharpe Ratio (annualized)
sharpe = df['strategy_returns'].mean() / df['strategy_returns'].std() * (252**0.5)
print(f"Sharpe Ratio: {sharpe:.2f}")
# Max Drawdown
cumulative = (1 + df['strategy_returns']).cumprod()
running_max = cumulative.expanding().max()
drawdown = (cumulative - running_max) / running_max
max_drawdown = drawdown.min()
print(f"Max Drawdown: {max_drawdown:.2%}")Best Practices for Historical Data
1. Cache Historical Data Locally
Historical data doesn't change. Fetch once, cache locally, and only update for new data:
import os
import pandas as pd
from datetime import datetime
def get_or_fetch_historical_data(commodity, years):
filename = f"{commodity}_{years}year_cache.csv"
# Check if cached file exists and is recent
if os.path.exists(filename):
df = pd.read_csv(filename, parse_dates=['created_at'])
print(f"Loaded {len(df)} cached records")
return df
# Fetch from API
print("Cache miss - fetching from API...")
data = client.get_historical_prices(...)
df = pd.DataFrame(data['data'])
# Save cache
df.to_csv(filename, index=False)
return df2. Handle Missing Data Points
Markets close on weekends/holidays. Fill gaps or filter them:
# Option 1: Forward fill (carry last known price) df['price'] = df['price'].fillna(method='ffill') # Option 2: Remove non-trading days df = df[df['price'].notna()] # Option 3: Interpolate df['price'] = df['price'].interpolate(method='linear')
3. Timezone Considerations
Oil markets trade across timezones. Always work in UTC:
# Convert to UTC
df['created_at'] = pd.to_datetime(df['created_at']).dt.tz_localize('UTC')
# Or convert to specific timezone
df['created_at_est'] = df['created_at'].dt.tz_convert('America/New_York')4. Data Validation
Always validate data quality before analysis:
# Check for unrealistic prices
assert df['price'].min() > 0, "Negative prices found!"
assert df['price'].max() < 300, "Unrealistic high prices!"
# Check for duplicates
duplicates = df[df.duplicated(subset=['created_at'], keep=False)]
if len(duplicates) > 0:
print(f"Warning: {len(duplicates)} duplicate timestamps found")
# Check date range
expected_days = (end_date - start_date).days
actual_days = (df['created_at'].max() - df['created_at'].min()).days
print(f"Expected {expected_days} days, got {actual_days} days")5. Use Batch Requests Efficiently
For very large datasets, batch requests by year:
import pandas as pd
from datetime import datetime
def fetch_data_in_batches(commodity, start_year, end_year):
all_data = []
for year in range(start_year, end_year + 1):
print(f"Fetching {year}...")
data = client.get_historical_prices(
by_code=commodity,
start_date=f"{year}-01-01",
end_date=f"{year}-12-31"
)
all_data.extend(data['data'])
return pd.DataFrame(all_data)
# Fetch 10 years in yearly batches
df = fetch_data_in_batches("WTI_USD", 2015, 2024)Frequently Asked Questions
Q: Where can I get historical oil price data?
A: Historical oil price data is available from APIs like OilPriceAPI (multi-year history, source-dependent intervals), EIA (decades, daily), Quandl/Nasdaq Data Link (varies), Bloomberg Terminal (decades, tick-level), and Refinitiv Eikon. Free options include EIA for daily data; paid APIs offer higher frequency and longer history.
Q: How far back does historical oil price data go?
A: It depends on the source. OilPriceAPI provides multi-year history at source-dependent intervals (depth varies by commodity). EIA offers decades of daily data back to the 1980s. Bloomberg Terminal has tick-level data going back 20+ years. Quandl datasets vary from a few years to several decades depending on the specific dataset.
Q: Is historical oil price data free?
A: Partial. The EIA provides free daily historical oil price data with no limits. Most other sources require paid subscriptions: OilPriceAPI starts at $19/month for historical data, Quandl datasets range from $50-$500+/month, and Bloomberg Terminal costs $24,000/year. Free tiers often include current prices but not extensive historical datasets.
Q: What is the best API for backtesting oil trading strategies?
A: OilPriceAPI is best for most traders with multi-year, source-timestamped data ($19-$299/month). For free daily data, use EIA. For institutional-grade tick-level data, Bloomberg Terminal is the gold standard ($24,000/year). The right choice depends on your backtesting frequency requirements (daily vs intraday) and budget.
Start Backtesting with Historical Data Today
You now know how to access, fetch, and analyze historical oil price data from multiple sources. Whether you're backtesting trading strategies, conducting academic research, or building financial models, the right historical data gives you the edge.
Explore Historical Oil Price Data
OilPriceAPI provides documented historical ranges for WTI, Brent, and other commodity values. Coverage and granularity vary by price code, source, dataset, account, and plan.
Documented historical ranges • Broad commodity coverage • JSON & CSV formats