European Central Bank
Euro Yield Curve
Introduction
The Euro Area Yield Curve dataset by the European Central Bank tracks the term structure of euro area government bond yields: spot rates, par yields and instantaneous forward rates at ten standard maturities from three months to thirty years. The data covers 6 yield curves, starts in September 2004, and is delivered on a daily frequency. This dataset is created by processing the ECB Data Portal's public SDMX API.
The ECB fits the curve to the prices of euro area government bonds with the Svensson model and publishes it around midday Frankfurt time on the business day after the one it measures. Its shape is the market's view of what the ECB will do with rates, which makes its slope the European counterpart of the US Treasury curve slope. Two baskets are published side by side, one fitted to triple A rated issuers and one to every euro area government issuer, and the gap between them is the credit spread the market charges the weaker sovereigns.
For more information about the Euro Yield Curve dataset, including CLI commands and pricing, see the dataset listing.
About the Provider
The European Central Bank is the central bank of the euro area, based in Frankfurt and operating since 1998. It sets monetary policy for the member states that use the euro, and publishes the yield curve daily as part of its statistical mandate, alongside policy rates, monetary aggregates and financial stability indicators. The ECB Data Portal is free to access and requires no API key and no registration.
QuantConnect processes and caches these curves so they reach your algorithm stamped at the moment the ECB actually released them, rather than at the close of the day they measure. A curve is never available on its own observation date.
Getting Started
The following snippet demonstrates how to request data from the Euro Area Yield Curve dataset:
self.curve = self.add_data(ECBYieldCurve, ECB.YieldCurves.AAA_SPOT, Resolution.DAILY).symbol self.all_issuers = self.add_data(ECBYieldCurve, ECB.YieldCurves.ALL_ISSUERS_SPOT, Resolution.DAILY).symbol
_curve = AddData<ECBYieldCurve>(ECB.YieldCurves.AaaSpot, Resolution.Daily).Symbol; _allIssuers = AddData<ECBYieldCurve>(ECB.YieldCurves.AllIssuersSpot, Resolution.Daily).Symbol;
Requesting Data
To add Euro Area Yield Curve data to your algorithm, call the AddDataadd_data method. The dataset is unlinked, so instead of a security Symbol you pass the curve you want. Save a reference to the dataset Symbol so you can access the data later in your algorithm.
class ECBYieldCurveDataAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2018, 1, 1)
self.set_end_date(2021, 3, 31)
self.set_cash(100000)
self.equity = self.add_equity("SPY", Resolution.DAILY).symbol
self.curve = self.add_data(ECBYieldCurve, ECB.YieldCurves.AAA_SPOT, Resolution.DAILY).symbol
self.all_issuers = self.add_data(ECBYieldCurve, ECB.YieldCurves.ALL_ISSUERS_SPOT, Resolution.DAILY).symbol public class ECBYieldCurveDataAlgorithm : QCAlgorithm
{
private Symbol _equity, _curve, _allIssuers;
public override void Initialize()
{
SetStartDate(2018, 1, 1);
SetEndDate(2021, 3, 31);
SetCash(100000);
_equity = AddEquity("SPY", Resolution.Daily).Symbol;
_curve = AddData<ECBYieldCurve>(ECB.YieldCurves.AaaSpot, Resolution.Daily).Symbol;
_allIssuers = AddData<ECBYieldCurve>(ECB.YieldCurves.AllIssuersSpot, Resolution.Daily).Symbol;
}
}
A curve reaches your algorithm on the business day after the one it measures, at midday Frankfurt time, which is still hours before the US cash session opens. An algorithm pairing this data with a US equity should wait for that security's bar before trading, or it will order against a security that has no price yet.
Accessing Data
To get the current Euro Area Yield Curve data, index the current Slice with the dataset Symbol. Slice objects deliver unique events to your algorithm as they happen, but the Slice may not contain data for your dataset at every time step. The curve only prints on TARGET business days, so weekends and euro area holidays carry nothing. To avoid issues, check if the Slice contains the data you want before you index it, and check the maturity you want against nullNone before you act on it.
def on_data(self, slice: Slice) -> None:
curves = slice.get(ECBYieldCurve)
if self.curve in curves:
data_point = curves[self.curve]
if data_point.ten_year is not None:
self.log(f"{self.curve} 10Y at {slice.time}: {data_point.ten_year}, 2Y: {data_point.two_year}") public override void OnData(Slice slice)
{
var curves = slice.Get<ECBYieldCurve>();
if (curves.ContainsKey(_curve))
{
var dataPoint = curves[_curve];
if (dataPoint.TenYear.HasValue)
{
Log($"{_curve} 10Y at {slice.Time}: {dataPoint.TenYear}, 2Y: {dataPoint.TwoYear}");
}
}
}
To iterate through all of the curves you subscribed to in the current Slice, call the Getget method.
def on_data(self, slice: Slice) -> None:
for dataset_symbol, data_point in slice.get(ECBYieldCurve).items():
self.log(f"{dataset_symbol} 10Y at {slice.time}: {data_point.ten_year}") public override void OnData(Slice slice)
{
foreach (var kvp in slice.Get<ECBYieldCurve>())
{
var datasetSymbol = kvp.Key;
var dataPoint = kvp.Value;
Log($"{datasetSymbol} 10Y at {slice.Time}: {dataPoint.TenYear}");
}
}
The Value property is the ten year rate, the benchmark point of the curve, falling back to zero when the ECB did not fit it. Read the typed property if you need to tell a missing reading from a real one.
Historical Data
To get historical Euro Area Yield Curve data, call the Historyhistory method with the dataset Symbol. If there is no data in the period you request, the history result is empty. Ask for a period rather than a bar count: a count is read in daily bars and includes the weekends and holidays the curve does not print on, so 100 of them come back as about 68 curve readings. A year of curve is roughly 255 readings.
# DataFrame history_df = self.history(self.curve, timedelta(days=365), Resolution.DAILY) # Dataset objects history_bars = self.history[ECBYieldCurve](self.curve, timedelta(days=365), Resolution.DAILY)
var history = History<ECBYieldCurve>(_curve, TimeSpan.FromDays(365), Resolution.Daily);
For more information about historical data, see History Requests.
Supported Curves
The ticker of a subscription is the curve you want. The ECB.YieldCurves helper gives each one a readable name. The basket determines which bonds the curve is fitted to, and the curve type determines what the rate means:
| Curve | Ticker | Constant |
|---|---|---|
Triple A rated issuers | ||
| Spot rate | AAA_SPOT | ECB.YieldCurves.AaaSpotECB.YieldCurves.AAA_SPOT |
| Par yield | AAA_PAR | ECB.YieldCurves.AaaParECB.YieldCurves.AAA_PAR |
| Instantaneous forward | AAA_FORWARD | ECB.YieldCurves.AaaForwardECB.YieldCurves.AAA_FORWARD |
All euro area government issuers | ||
| Spot rate | ALL_SPOT | ECB.YieldCurves.AllIssuersSpotECB.YieldCurves.ALL_ISSUERS_SPOT |
| Par yield | ALL_PAR | ECB.YieldCurves.AllIssuersParECB.YieldCurves.ALL_ISSUERS_PAR |
| Instantaneous forward | ALL_FORWARD | ECB.YieldCurves.AllIssuersForwardECB.YieldCurves.ALL_ISSUERS_FORWARD |
The spot rate is the yield on a zero coupon bond maturing at that point and is the usual choice. The par yield is the coupon a bond issued today at that maturity would carry. The instantaneous forward is the rate the curve implies for that future moment, which is the one to read for what the market expects the ECB to do.
Each curve carries ten maturities: 3M, 6M, 1Y, 2Y, 3Y, 5Y, 7Y, 10Y, 20Y and 30Y. The euro area curve begins at three months, so unlike the US Treasury curve there is no one month or two month rate.
Example Applications
The Euro Area Yield Curve dataset lets you read European rate expectations directly rather than inferring them from prices. Examples include the following strategies:
- Reading the slope of the curve, the ten year rate against the two year, as a regime signal for European equity exposure.
- Trading the spread between the all issuers and triple A curves as a gauge of sovereign stress.
- Anticipating European Central Bank decisions from the instantaneous forward curve, which prices what the market expects rates to be at a future moment.
- Timing rate sensitive sectors from moves at the short end, where policy expectations show up first.
- Comparing the euro area term structure against the US Treasury curve for cross market positioning.
- Building a European risk free curve for discounting or for pricing options on euro denominated assets.
Classic Algorithm Example
The following example algorithm reads the credit spread the market charges the weaker euro area sovereigns, which is the all issuers curve minus the triple A curve at ten years. It holds SPY while the spread is tight and steps aside while it is wide, so the European sovereign market drives the risk decision.
from AlgorithmImports import *
class ECBYieldCurveExampleAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2018, 1, 1)
self.set_end_date(2021, 3, 31)
self.set_cash(100000)
self.equity = self.add_equity("SPY", Resolution.DAILY).symbol
self.triple_a = self.add_data(ECBYieldCurve, ECB.YieldCurves.AAA_SPOT, Resolution.DAILY).symbol
self.all_issuers = self.add_data(ECBYieldCurve, ECB.YieldCurves.ALL_ISSUERS_SPOT, Resolution.DAILY).symbol
history = self.history[ECBYieldCurve](self.triple_a, timedelta(days=365), Resolution.DAILY)
self.debug(f"Got {len(list(history))} historical curve readings")
self.triple_a_ten_year = None
self.all_issuers_ten_year = None
def on_data(self, slice):
curves = slice.get(ECBYieldCurve)
# The two curves need not arrive in the same slice, so the latest reading of each is kept.
if self.triple_a in curves:
self.triple_a_ten_year = curves[self.triple_a].ten_year
if self.all_issuers in curves:
self.all_issuers_ten_year = curves[self.all_issuers].ten_year
if self.triple_a_ten_year is None or self.all_issuers_ten_year is None:
return
# The curve lands hours before the US session, so wait for the bar before trading.
if self.equity not in slice.bars:
return
spread = (self.all_issuers_ten_year - self.triple_a_ten_year) * 100
if spread > 60:
if self.portfolio[self.equity].invested:
self.liquidate(self.equity)
elif not self.portfolio[self.equity].invested:
self.set_holdings(self.equity, 1) public class ECBYieldCurveExampleAlgorithm : QCAlgorithm
{
private Symbol _equity, _tripleA, _allIssuers;
private decimal? _tripleATenYear, _allIssuersTenYear;
public override void Initialize()
{
SetStartDate(2018, 1, 1);
SetEndDate(2021, 3, 31);
SetCash(100000);
_equity = AddEquity("SPY", Resolution.Daily).Symbol;
_tripleA = AddData<ECBYieldCurve>(ECB.YieldCurves.AaaSpot, Resolution.Daily).Symbol;
_allIssuers = AddData<ECBYieldCurve>(ECB.YieldCurves.AllIssuersSpot, Resolution.Daily).Symbol;
var history = History<ECBYieldCurve>(_tripleA, TimeSpan.FromDays(365), Resolution.Daily);
Debug($"Got {history.Count()} historical curve readings");
}
public override void OnData(Slice slice)
{
var curves = slice.Get<ECBYieldCurve>();
// The two curves need not arrive in the same slice, so the latest reading of each is kept.
if (curves.ContainsKey(_tripleA))
{
_tripleATenYear = curves[_tripleA].TenYear;
}
if (curves.ContainsKey(_allIssuers))
{
_allIssuersTenYear = curves[_allIssuers].TenYear;
}
if (!_tripleATenYear.HasValue || !_allIssuersTenYear.HasValue)
{
return;
}
// The curve lands hours before the US session, so wait for the bar before trading.
if (!slice.Bars.ContainsKey(_equity))
{
return;
}
var spread = (_allIssuersTenYear.Value - _tripleATenYear.Value) * 100m;
if (spread > 60m)
{
if (Portfolio[_equity].Invested)
{
Liquidate(_equity);
}
}
else if (!Portfolio[_equity].Invested)
{
SetHoldings(_equity, 1);
}
}
}
Framework Algorithm Example
The following example algorithm trades a manually selected universe from the shape of the curve. An alpha model subscribes to the triple A spot curve and reads its slope, the ten year rate minus the two year, emitting an up insight while the curve is upward sloping and a flat one once it inverts, which is the classic recession signal read on the European curve instead of the US one.
from AlgorithmImports import *
class ECBYieldCurveFrameworkAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2018, 1, 1)
self.set_end_date(2021, 3, 31)
self.set_cash(100000)
self.universe_settings.resolution = Resolution.DAILY
symbols = [Symbol.create("SPY", SecurityType.EQUITY, Market.USA)]
self.set_universe_selection(ManualUniverseSelectionModel(symbols))
self.add_alpha(ECBCurveSlopeAlphaModel(self))
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
self.set_execution(ImmediateExecutionModel())
class ECBCurveSlopeAlphaModel(AlphaModel):
"""Emits insights from the slope of the euro area yield curve."""
def __init__(self, algorithm):
self._curve = algorithm.add_data(ECBYieldCurve, ECB.YieldCurves.AAA_SPOT, Resolution.DAILY).symbol
history = algorithm.history[ECBYieldCurve](self._curve, timedelta(days=365), Resolution.DAILY)
algorithm.debug(f"Got {len(list(history))} historical curve readings")
self._slope = None
self._symbols = []
def update(self, algorithm, data):
curves = data.get(ECBYieldCurve)
if self._curve in curves:
data_point = curves[self._curve]
if data_point.ten_year is not None and data_point.two_year is not None:
self._slope = data_point.ten_year - data_point.two_year
if self._slope is None:
return []
# An upward sloping curve is the expansion read; an inversion steps aside.
direction = InsightDirection.UP if self._slope > 0 else InsightDirection.FLAT
# The curve lands hours before the US session, so an insight waits for the bar.
return [Insight.price(symbol, timedelta(days=30), direction)
for symbol in self._symbols if symbol in data.bars]
def on_securities_changed(self, algorithm, changes):
for security in changes.added_securities:
self._symbols.append(security.symbol)
for security in changes.removed_securities:
if security.symbol in self._symbols:
self._symbols.remove(security.symbol) public class ECBYieldCurveFrameworkAlgorithm : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2018, 1, 1);
SetEndDate(2021, 3, 31);
SetCash(100000);
UniverseSettings.Resolution = Resolution.Daily;
var symbols = new[] { QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA) };
SetUniverseSelection(new ManualUniverseSelectionModel(symbols));
AddAlpha(new ECBCurveSlopeAlphaModel(this));
SetPortfolioConstruction(new EqualWeightingPortfolioConstructionModel());
SetExecution(new ImmediateExecutionModel());
}
}
public class ECBCurveSlopeAlphaModel : AlphaModel
{
private readonly Symbol _curve;
private readonly List<Symbol> _symbols = new();
private decimal? _slope;
public ECBCurveSlopeAlphaModel(QCAlgorithm algorithm)
{
_curve = algorithm.AddData<ECBYieldCurve>(ECB.YieldCurves.AaaSpot, Resolution.Daily).Symbol;
var history = algorithm.History<ECBYieldCurve>(_curve, TimeSpan.FromDays(365), Resolution.Daily);
algorithm.Debug($"Got {history.Count()} historical curve readings");
}
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
var curves = data.Get<ECBYieldCurve>();
if (curves.ContainsKey(_curve))
{
var dataPoint = curves[_curve];
if (dataPoint.TenYear.HasValue && dataPoint.TwoYear.HasValue)
{
_slope = dataPoint.TenYear.Value - dataPoint.TwoYear.Value;
}
}
if (!_slope.HasValue)
{
return new List<Insight>();
}
// An upward sloping curve is the expansion read; an inversion steps aside.
var direction = _slope > 0m ? InsightDirection.Up : InsightDirection.Flat;
// The curve lands hours before the US session, so an insight waits for the bar.
return _symbols
.Where(symbol => data.Bars.ContainsKey(symbol))
.Select(symbol => Insight.Price(symbol, TimeSpan.FromDays(30), direction))
.ToList();
}
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
foreach (var security in changes.AddedSecurities)
{
_symbols.Add(security.Symbol);
}
foreach (var security in changes.RemovedSecurities)
{
_symbols.Remove(security.Symbol);
}
}
}
Data Point Attributes
The Euro Area Yield Curve dataset provides ECBYieldCurve objects, which have the following attributes:
Every maturity is nullable. A maturity the ECB did not fit on a given day is empty rather than zero, because zero is a rate euro area curves genuinely printed for years and the two must not be confused.