The ETFs with multiple tickers
Thanks for this excellent work!
For the ETFs with multiple tickers in different exchanges, the current code seems to use ISIN as the unique identifier and only provides one ticker. This would raise some inconvenience if I want to query for information by ticker if that ticker is not in the overview df.
Do you consider adding a column to store all the tickers for one ETF?
Hi @ffffuturexu, do you have any example, so I could check the behavior?
For example, I am interested in this ETF Global X Nasdaq 100 Covered Call UCITS ETF D. It has three tickers (QYLD, QYLE, and QYLP) in different exchanges. While the overview df only stores QYLE in the column ticker.
In this case, if I want to query this ETF by ticker QYLD from the overview df, nothing will be found.
Hi @ffffuturexu,
there are indeed multiple tickers for the same ISIN. The only way I see to retrieve this info is to parse HTML page (e.g.).
What I noticed though is that for different countries the ticker is changing, e.g. justetf_scraping.load_overview(isin="IE00BM8R0J59", local_country="IT"). Oddly the ticker isn't changing with a particular exchange/currency. Would it help you or you rather need the whole overview over all possible combinations for each of ISINs you are interested in?
FYI, I get the following results for different countries:
| Country | Ticker |
|---|---|
| DE | QYLE |
| AT | QYLE |
| CH | QYLD |
| GB | QYLP |
| IT | QYLD |
| FR | QYLE |
| ES | QYLE |
| NL | QYLD |
| BE | - |
Hi @druzsan
Thanks for the solution. This solution can help me with the specific mapping request. I think I can manually map them by iterating the country codes. Therefore, the mapping of "isin"-"country"-"ticker" works.
While the ticker seems to only link to the country rather than the exchange and the currency, which means the currency I want cannot be confirmed for my interested ETF when I run load_chart(). For the same example, QYLD and QYLP are both listed in the London exchange but with two different currencies, and QYLD is also listed in the Italian exchange.
So, the "isin"-"country"-"ticker"-"exchange"-"currency" might be a comprehensive mapping. Maybe the only way is to parse the HTML as you mentioned. (this is not a request BTW, just for discussion :-)
Hi @ffffuturexu,
from my experience the country is the only parameter that changes ticker. Unfortunately, changing exchange and/or currency didn't really changed the ticker for me.
BTW, did you double-check it outside of justETF? Are there really two different tickers for the London exchange with different currencies but the same ISIN? Sounds strange to me, but I have only a limited overview over this.
In this case parsing the HTML would be indeed the only option.
Thanks@druzsan
First, I have developed my own function to query information by ticker using requests, by referring to your source code in overview.py. This ultimately solved my initial problem.
Regarding the ISIN-ticker-currency-exchange mapping, I attached two screenshots from trading212, showing the complicated situation of mapping.
Hi @ffffuturexu,
little late, but anyway.
First, I have developed my own function to query information by ticker using requests, by referring to your source code in overview.py. This ultimately solved my initial problem.
Can you post it here? Or just tell, which query key do you use? I tried ...&ticker=QYLE, but it just gives me the whole overview table.
Hi
The basic idea is to query the ISIN using the ticker firstly. Once the unique identifier ISIN is acquired, then other queries can be done.
1. Get the ISIN using the ticker
I create a function: def ticker_to_isin(ticker: str, locale: str) -> str:
within which I did a post query that is similar to the function def get_raw_overview
with requests.Session() as session:
# 1. Load the initial page to set cookies and parse the dynamic counter.
session.headers["User-Agent"] = USER_AGENT
initial_url = f"{BASE_URL}?search=ETFS"
html_response = session.get(initial_url)
if html_response.status_code != 200:
raise Exception(f"Failed to load initial page: status code {html_response.status_code}")
if match := PATTERN.search(html_response.text):
counter = match.group(1)
# print(f"Found dynamic counter: {counter}")
else:
warnings.warn("Cannot parse dynamic counter from HTML page, assuming 0.")
counter = "0"
# 2. Construct the AJAX (Wicket) URL using the dynamic counter and including the ticker.
ajax_url = (
f"{BASE_URL}?{counter}-1.0-container-tabsContentContainer-tabsContentRepeater-1-container-content-etfsTablePanel"
f"&query={ticker}&search=ETFS&_wicket=1"
)
# 3. Prepare POST data.
# Here we include minimal parameters; the ticker can be passed in the POST payload as well if needed.
data = {
**BASE_DATA,
"lang": "en",
"country": {locale},
"universeType": "private",
"defaultCurrency": "EUR",
# Including etfsParams here is optional since the ticker is in the URL,
# but you can add more filters if required.
"etfsParams": f"search=ETFS&query={ticker}",
}
# 4. Send the POST request.
response = session.post(ajax_url, data=data)
if response.status_code != 200:
raise Exception(f"Failed to fetch ETF data: status code {response.status_code}")
try:
json_data = response.json()
# print(json_data)
except json.JSONDecodeError as e:
raise Exception("Failed to parse JSON response") from e
# Debug print: show all tickers in the response
# for etf in json_data.get("data", []):
# print("Record:", etf)
# 5. Search through the returned data for the row matching the ticker.
# The JSON structure is assumed to have a key 'data' which is a list of ETF records.
etfs = json_data.get("data", [])
for etf in etfs:
isin = etf.get("isin")
if isin:
return isin
else:
raise Exception(f"ETF found for ticker {ticker} but no ISIN present")
raise Exception(f"ETF with ticker '{ticker}' not found in the response")
2. Get the quotes using the ISIN (or your load_charts, etc.)
After I get the ISIN, I use an API to get the latest quotes
BASE_QUOTE_URL = 'https://www.justetf.com/api/etfs/{isin}/quote?locale={locale}¤cy={currency}'
For example the QYLD (IE00BM8R0J59) https://www.justetf.com/api/etfs/IE00BM8R0J59/quote?locale=IT¤cy=EUR
The locale could be DE, IT, FR, etc...
This will return a json to me