How to select ETFs - Simple Technical & Fundamental Scanner with Quant Trading Framework
Introduction
The Exchange-Traded Funds (ETFs) have become increasingly popular among investors seeking diversification, low costs, and exposure to specific sectors or investment strategies. However, with a vast array of ETFs available, it can be challenging to identify the most promising opportunities. This is where we applied our Quant Trading Framework (QuantJP) library to come into play, providing a powerful toolkit for quantitative analysis and trading strategies.
In this post, we'll explore an ETF Scanner, which aims to identify top long and short candidates among a set of ETFs based on technical and fundamental analysis. We'll delve into the code, explaining each step and the rationale behind the various parameters and functions used.
Setting Up the ETF Strategy Class
The ETFStrategy
class serves as the backbone of our ETF Scanner. It encapsulates the necessary functionality to fetch data, calculate technical and fundamental metrics, and generate buy/sell signals based on predefined conditions.
class ETFStrategy:
def __init__(self, dc, tickers, sectors, start_date, end_date, params=None):
self.data = None
self.dc = dc
self.tickers = tickers
self.sectors = sectors
self.start_date = start_date
self.end_date = end_date
self.params = params if params is not None else {}
The __init__
method initializes the class with the following parameters:
dc
: ADataConnector
object from the QuantJP library, used to fetch financial data.tickers
: A list of ETF tickers to be analyzed.sectors
: A list of corresponding sectors for each ETF ticker.start_date
andend_date
: The date range for which data should be fetched.params
: A dictionary containing conditions for generating long and short signals (more on this later).
The provided code allows to create table plots as below for any ETFs - we used the following ones from the following sectors:
tickers = ['XLE', 'XLY', 'XLB', 'RSPT', 'XLK', 'XLV', 'RSPS', 'XLRE', 'RSPH', 'RSPU', 'RSPN', 'XLP', 'RSPD', 'RSPM', 'XLI', 'RSPG', 'XLF', 'XLU', 'XLC', 'RSPF', 'RSPR', 'XME', 'BLOK', 'BUZZ', 'XBI', 'PBW', 'GBTC', 'DRIV', 'XOP', 'FCG']
sectors = ['Energy', 'Consumer Dicr', 'Materials', 'TechnologyEW', 'Technology', 'Healthcare' ,'Consumer Staples EW', 'Real Estate', 'Healthcare EW', 'Utilities EW', 'Industrial EW', 'Consumer Staples', 'Consumer Discr EW', 'Materiars EW', 'Industrial', 'Energy EW', 'Financial', 'Utilities', 'Communication', 'Financial EW', 'Real Estate EW', 'Metal & Minings', 'Crypto Related', 'Social Sentiment', 'Biotech', 'Clean Energy', 'Bitcoin Trust', 'Automation & EV', 'Oil & Gas', 'Natural Gas']
Advanced filtering is also possible, and available as any combination of any Technical and Fundamental data per each ETF, e.g.
The params
dictionary we have used in the code for selecting top 5 ETFs to Long, and top 5 ETFs for Short, contains conditions for both long and short signals, specified as key-value pairs, e.g.
params = {
'long': {
'RSI': (operator.lt, 30),
'MACD_Hist': (operator.lt, -0.5),
'Drawdown': (operator.lt, 0.5),
'Volatility': (operator.lt, 0.5),
'P/E': (operator.lt, 15)
},
'short': {
'RSI': (operator.gt, 70),
'MACD_Hist': (operator.gt, 0.5),
'Drawdown': (operator.lt, 0.4),
'Volatility': (operator.lt, 0.5),
'P/E': (operator.gt, 30)
}
}
Fetching Data
The fetch_data
method retrieves the historical Open-High-Low-Close-Volume (OHLCV) data for the specified tickers and date range using the DataConnector
object. It is utilizing etfs.py and function get_etf_ohlcv, for set period of time (period_starts, period_ends).
It uses batch fetching, allowing us to specify multiple tickers and retrieve them in a single request.
async def fetch_data(self):
"""
Fetch data for the given tickers and date range.
params:
dc: DataConnector object
tickers: list of tickers
start_date: start date in the format 'YYYY-MM-DD'
end_date: end date in the format 'YYYY-MM-DD'
"""
exchanges = ["US"]*len(self.tickers)
granularity = '1d'
period_starts = [self.start_date]*len(self.tickers)
period_ends = [self.end_date]*len(self.tickers)
try:
data_list = await self.dc.etfs.get_etf_ohlcv( tickers=self.tickers,
exchanges=exchanges,
granularity=granularity,
period_starts=period_starts,
period_ends=period_ends)
except http.client.HTTPException as e:
if e.code == 404:
print("Data for some tickers not found")
else:
raise e
else:
data = [df.set_index("datetime") for df in data_list]
data = [df.assign(Ticker=ticker) for df, ticker in zip(data, self.tickers)]
combined_data = pd.concat(data)
return combined_data
The complete code is available to paid subscribers on GitHub. Please support me and Subscribe for more.
The fetched data is combined into a single pandas DataFrame, with each row representing a specific date and ticker, and columns containing the OHLCV values.