European Central Bank
Policy Rates
Introduction
The Euro Area Policy Rates dataset by the European Central Bank tracks the three rates the ECB sets for the euro area from January 1999 to the present, alongside the euro short-term rate, the overnight rate the market actually trades at. The data covers one euro area series and carries a row for every weekday, including the TARGET holidays the system closes on.
The three policy rates form a corridor the ECB steers liquidity within. The deposit facility rate is its floor, paid on funds banks park at the ECB overnight. The marginal lending facility rate is its ceiling, charged on overnight credit against collateral. The main refinancing operations rate sits between them and is the headline rate quoted when the ECB is said to have raised or cut. The euro short-term rate is where unsecured overnight borrowing settles inside that corridor, so the gap between it and the deposit facility is a direct read on how loose funding conditions are.
For more information about the Policy Rates 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 its rates as part of its statistical mandate. The ECB Data Portal is free to access and requires no API key and no registration.
Data Summary
The following table describes the dataset properties:
| Property | Value |
|---|---|
| Start Date | January 1999 |
| Asset Coverage | 1 Euro Area Series |
| Resolution | Daily* |
| Timezone | Berlin |
* One row per weekday, including TARGET holidays. A row is released at 08:00 Frankfurt time on the next day the overnight rate publishes, or on the next weekday for the years before that rate existed. Rows that share a release arrive as one point, carrying the most recent of them.
Requesting Data
To add Euro Area Policy Rates data to your algorithm, call the AddDataadd_data method. Save a reference to the dataset Symbol so you can access the data later in your algorithm.
class ECBPolicyRatesExampleAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2020, 1, 1)
self.set_end_date(2021, 1, 1)
self._rates = self.add_data(ECBPolicyRates, ECB.PolicyRates.EURO_AREA).symbol public class ECBPolicyRatesExampleAlgorithm : QCAlgorithm
{
private Symbol _rates;
public override void Initialize()
{
SetStartDate(2020, 1, 1);
SetEndDate(2021, 1, 1);
_rates = AddData<ECBPolicyRates>(ECB.PolicyRates.EuroArea).Symbol;
}
}
Accessing Data
To get the current Euro Area Policy Rates 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. To avoid issues, check if the Slice contains the data you want before you index it.
The euro short-term rate is empty before October 2019 and on days the TARGET system is closed, so check it against null before acting on it.
def on_data(self, slice: Slice) -> None:
rates = slice.get(ECBPolicyRates)
if self._rates in rates:
point = rates[self._rates]
if point.euro_short_term_rate is not None:
self.log(f"{point.end_time} overnight {point.euro_short_term_rate} against a "
f"deposit floor of {point.deposit_facility_rate}") public override void OnData(Slice slice)
{
var rates = slice.Get<ECBPolicyRates>();
if (rates.ContainsKey(_rates))
{
var point = rates[_rates];
if (point.EuroShortTermRate.HasValue)
{
Log($"{point.EndTime} overnight {point.EuroShortTermRate} against a "
+ $"deposit floor of {point.DepositFacilityRate}");
}
}
}
The Value property is the main refinancing rate, the headline of the corridor.
Historical Data
To get historical Euro Area Policy Rates 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 the dataset does not report on.
history = self.history[ECBPolicyRates](self._rates, timedelta(days=365), Resolution.DAILY)
var history = History<ECBPolicyRates>(_rates, TimeSpan.FromDays(365), Resolution.Daily);
For more information about historical data, see History Requests.
Supported Rates
A single file carries all four rates for each day. The three policy rates run from the start of the euro; the overnight rate begins when the ECB started publishing it.
| Rate | Property | Coverage |
|---|---|---|
| Main refinancing operations | MainRefinancingRate | January 1999 onward |
| Deposit facility | DepositFacilityRate | January 1999 onward |
| Marginal lending facility | MarginalLendingRate | January 1999 onward |
| Euro short-term rate | EuroShortTermRate | October 2019 onward |
Example Applications
The Euro Area Policy Rates dataset lets you read the ECB's stance directly rather than inferring it from prices. Examples include the following strategies:
- Tracking the gap between the overnight rate and the deposit facility as a funding stress signal.
- Trading the run-up to Governing Council decisions from where the corridor sits.
- Building a euro funding cost into carry and financing calculations.
- Comparing the ECB's stance against the Federal Reserve's for cross-currency positioning.
Classic Algorithm Example
Trades on where the market's overnight rate sits against the floor of the ECB's corridor. The euro short-term rate trades below the deposit facility, because non-banks cannot park cash at the ECB, and that gap widens when liquidity is abundant. When it closes back toward the floor, funding is tightening ahead of any policy move, which is the moment to step out of risk.
from AlgorithmImports import *
class ECBPolicyRatesAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2019, 1, 1)
self.set_end_date(2021, 3, 31)
self.set_cash(100000)
self.equity = self.add_equity("SPY", Resolution.DAILY).symbol
self.rates = self.add_data(ECBPolicyRates, ECB.PolicyRates.EURO_AREA, Resolution.DAILY).symbol
self.spread = None
def on_data(self, slice: Slice) -> None:
rates = slice.get(ECBPolicyRates)
if self.rates in rates:
point = rates[self.rates]
if point.euro_short_term_rate is not None and point.deposit_facility_rate is not None:
self.log(f"{point.end_time} corridor: deposit {point.deposit_facility_rate}, "
f"main {point.main_refinancing_rate}, lending {point.marginal_lending_rate}, "
f"overnight {point.euro_short_term_rate}")
# How far the overnight rate trades below the deposit facility, which is the floor
# of the corridor and the rate that steers euro money markets. The gap is negative
# because non-banks cannot park cash at the ECB, so a wide one is abundant
# liquidity and a gap closing back toward the floor is funding tightening.
self.spread = point.euro_short_term_rate - point.deposit_facility_rate
if self.spread is None:
return
# The rates land at 08:00 in Frankfurt, hours before the US session opens, so the latest
# reading is kept and acted on once the bar arrives. Ordering in the slice that carried the
# rates would order against a security that has no price yet.
if self.equity not in slice.bars:
return
if self.spread > -0.05:
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 ECBPolicyRatesAlgorithm : QCAlgorithm
{
private Symbol _equity;
private Symbol _rates;
private decimal? _spread;
public override void Initialize()
{
SetStartDate(2019, 1, 1);
SetEndDate(2021, 3, 31);
SetCash(100000);
_equity = AddEquity("SPY", Resolution.Daily).Symbol;
_rates = AddData<ECBPolicyRates>(ECB.PolicyRates.EuroArea, Resolution.Daily).Symbol;
}
public override void OnData(Slice slice)
{
var rates = slice.Get<ECBPolicyRates>();
if (rates.ContainsKey(_rates))
{
var point = rates[_rates];
if (point.EuroShortTermRate.HasValue && point.DepositFacilityRate.HasValue)
{
Log($"{point.EndTime} corridor: deposit {point.DepositFacilityRate}, "
+ $"main {point.MainRefinancingRate}, lending {point.MarginalLendingRate}, "
+ $"overnight {point.EuroShortTermRate}");
// How far the overnight rate trades below the deposit facility, which is the
// floor of the corridor and the rate that steers euro money markets. The gap
// is negative because non-banks cannot park cash at the ECB, so a wide one is
// abundant liquidity and a gap closing back toward the floor is funding
// tightening.
_spread = point.EuroShortTermRate.Value - point.DepositFacilityRate.Value;
}
}
if (!_spread.HasValue)
{
return;
}
// The rates land at 08:00 in Frankfurt, hours before the US session opens, so the
// latest reading is kept and acted on once the bar arrives. Ordering in the slice that
// carried the rates would order against a security that has no price yet.
if (!slice.Bars.ContainsKey(_equity))
{
return;
}
if (_spread > -0.05m)
{
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 where the market's overnight rate sits against the floor of the ECB's corridor. An alpha model subscribes to the euro area rates and measures how far the euro short-term rate trades below the deposit facility, emitting an up insight while that gap is wide and a flat one once it closes back toward the floor.
from AlgorithmImports import *
class ECBPolicyRatesFrameworkAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2019, 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(ECBCorridorAlphaModel(self))
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
self.set_execution(ImmediateExecutionModel())
class ECBCorridorAlphaModel(AlphaModel):
"""Emits insights from where the overnight rate sits against the corridor floor."""
def __init__(self, algorithm):
self._rates = algorithm.add_data(ECBPolicyRates, ECB.PolicyRates.EURO_AREA, Resolution.DAILY).symbol
history = algorithm.history[ECBPolicyRates](self._rates, timedelta(days=365), Resolution.DAILY)
algorithm.debug(f"Got {len(list(history))} historical rate readings")
self._spread = None
self._symbols = []
def update(self, algorithm, data):
rates = data.get(ECBPolicyRates)
if self._rates in rates:
data_point = rates[self._rates]
if data_point.euro_short_term_rate is not None and data_point.deposit_facility_rate is not None:
# How far the overnight rate trades below the deposit facility, the floor of the
# corridor. The gap is negative, and it widens when liquidity is abundant.
self._spread = data_point.euro_short_term_rate - data_point.deposit_facility_rate
if self._spread is None:
return []
# A gap closing back toward the floor is funding tightening ahead of any policy move.
direction = InsightDirection.UP if self._spread <= -0.05 else InsightDirection.FLAT
# The rates land at 08:00 in Frankfurt, hours before the US session, so an insight waits
# for the bar rather than returning early and never reaching the slice that carries it.
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 ECBPolicyRatesFrameworkAlgorithm : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2019, 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 ECBCorridorAlphaModel(this));
SetPortfolioConstruction(new EqualWeightingPortfolioConstructionModel());
SetExecution(new ImmediateExecutionModel());
}
}
public class ECBCorridorAlphaModel : AlphaModel
{
private readonly Symbol _rates;
private readonly List<Symbol> _symbols = new();
private decimal? _spread;
public ECBCorridorAlphaModel(QCAlgorithm algorithm)
{
_rates = algorithm.AddData<ECBPolicyRates>(ECB.PolicyRates.EuroArea, Resolution.Daily).Symbol;
var history = algorithm.History<ECBPolicyRates>(_rates, TimeSpan.FromDays(365), Resolution.Daily);
algorithm.Debug($"Got {history.Count()} historical rate readings");
}
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
var rates = data.Get<ECBPolicyRates>();
if (rates.ContainsKey(_rates))
{
var dataPoint = rates[_rates];
if (dataPoint.EuroShortTermRate.HasValue && dataPoint.DepositFacilityRate.HasValue)
{
// How far the overnight rate trades below the deposit facility, the floor of the
// corridor. The gap is negative, and it widens when liquidity is abundant.
_spread = dataPoint.EuroShortTermRate.Value - dataPoint.DepositFacilityRate.Value;
}
}
if (!_spread.HasValue)
{
return Enumerable.Empty<Insight>();
}
// A gap closing back toward the floor is funding tightening ahead of any policy move.
var direction = _spread <= -0.05m ? InsightDirection.Up : InsightDirection.Flat;
// The rates land at 08:00 in Frankfurt, hours before the US session, so an insight waits
// for the bar rather than returning early and never reaching the slice that carries it.
return _symbols.Where(symbol => data.Bars.ContainsKey(symbol))
.Select(symbol => Insight.Price(symbol, TimeSpan.FromDays(30), direction));
}
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);
}
}
}