European Short Position Data
policy Background
Following the global financial crisis of 2008, European Securities and Markets Authority (ESMA) launched policy EU N236/2012 on short selling and certain aspects of credit default swaps. The purpose of this EU-wide reporting regulation is to increase market stability by reducing the opaqueness of market activities through mandatory daily reporting by institutional investors.
Therefore, each participating country’s financial monitoring agency requires market participants with the significant short positions on qualifying securities to self-report the positions, and further makes such information available to the public.
technical background
As each government agency’s website carries the data differently, and often the data scraping exercise involves mimicking the “clicking” action of a mouse, the package Selenium suits the need perfectly.
However, I would like to emphasize here that this code needs human monitoring over time, as government agency’s website can change layout or the presented data might show different format. So please do not rely on the code output blindly (more likely it will crash somewhere before getting to the end!).
Our student intern Shuai Zheng contributed to this code.
This code was revamped significantly to reflect the selenium package update as well as changes in the host countries’ websites.
Update: October 2026
the data
The tables and chart below summarize the combined dataset as of the latest update (September 28, 2026): every net short position disclosure published by the 19 regulators covered, with the position holder, the company (issuer and ISIN), the net short position as a percentage of issued share capital, and the position date.
| Disclosures | Companies (ISINs) | Position holders | First position date | Last position date | |
|---|---|---|---|---|---|
| Country | |||||
| UK | 112,510 | 941 | 621 | 2012-10-31 | 2026-07-09 |
| Belgium | 81,689 | 63 | 192 | 2012-10-31 | 2026-09-16 |
| Germany | 61,498 | 1,038 | 489 | 2003-03-26 | 2026-09-21 |
| France | 47,941 | 271 | 530 | 2012-10-03 | 2026-09-18 |
| Sweden | 39,732 | 465 | 486 | 2010-05-10 | 2026-09-21 |
| Italy | 25,705 | 232 | 307 | 2012-08-30 | 2026-09-21 |
| Netherlands | 24,705 | 99 | 281 | 2012-11-01 | 2026-09-21 |
| Spain | 13,743 | 90 | 254 | 2010-06-10 | 2026-09-18 |
| Finland | 10,939 | 80 | 239 | 2012-10-31 | 2026-09-21 |
| Denmark | 9,071 | 67 | 213 | 2012-11-01 | 2026-07-31 |
| Norway | 8,565 | 143 | 179 | 2020-02-24 | 2026-09-24 |
| Austria | 2,303 | 30 | 103 | 2012-11-01 | 2026-09-21 |
| Poland | 1,326 | 48 | 79 | 2012-11-01 | 2026-09-18 |
| Ireland | 945 | 27 | 75 | 2012-11-01 | 2026-05-29 |
| Greece | 407 | 22 | 25 | 2013-05-30 | 2026-09-18 |
| Hungary | 189 | 6 | 19 | 2012-11-05 | 2018-02-02 |
| Luxembourg | 62 | 2 | 5 | 2012-11-01 | 2017-11-20 |
| Iceland | 35 | 8 | 6 | 2017-07-07 | 2026-09-16 |
| Czech | 28 | 2 | 5 | 2017-04-19 | 2018-11-30 |
| All countries | 441,393 | 3,493 | 1,559 | 2003-03-26 | 2026-09-24 |
The chart counts the companies with at least one disclosure dated in each month, which is comparable across countries; raw disclosure counts are not, since some regulators (Belgium until 2022, for example) repeat unchanged positions every day.
the code
The code runs in two parts. Part 1 downloads each regulator’s raw disclosure file, using Selenium for sites that need clicking and plain HTTP requests where a file or API address is available. Part 2 reads each country’s file, standardizes the columns, runs validation checks, and appends the new records to the previous update, since some regulators do not keep their full history online.
The example below is the Norway download, which reads the regulator’s API directly instead of clicking through a website.
example: Norway
Norwegian Finanstilsynet presents the data in the json format through its API portal after the most recent website revamp, therefore the code does not resemble the other countries’ download process.
# import packages
import json
import certifi
import pandas as pd
import urllib3http = urllib3.PoolManager(cert_reqs='CERT_REQUIRED',
ca_certs=certifi.where())
url = "https://ssr.finanstilsynet.no/api/v2/instruments"
r = http.request('GET', url)
assert r.status == 200, f"Norway API returned status {r.status}"
data = json.loads(r.data.decode('utf-8'))
df = pd.json_normalize(data, 'events', ["isin", "issuerName"],
max_level=0)
dfValid = df.loc[df.shares != 0]
print(f"Norway: {df.shape[0]} raw records, "
f"{dfValid.shape[0]} with a nonzero position")Norway: 3364 raw records, 3187 with a nonzero position
df.head()| date | shortPercent | shares | activePositions | isin | issuerName | |
|---|---|---|---|---|---|---|
| 0 | 2026-10-01T00:00:00 | 0.00 | 0 | [] | BMG9156K1018 | 2020 BULKERS |
| 1 | 2026-09-30T00:00:00 | 1.80 | 413635 | [{'date': '2026-09-30T00:00:00', 'shortPercent... | BMG9156K1018 | 2020 BULKERS |
| 2 | 2026-09-29T00:00:00 | 1.64 | 377008 | [{'date': '2026-09-29T00:00:00', 'shortPercent... | BMG9156K1018 | 2020 BULKERS |
| 3 | 2026-09-25T00:00:00 | 1.43 | 329408 | [{'date': '2026-09-25T00:00:00', 'shortPercent... | BMG9156K1018 | 2020 BULKERS |
| 4 | 2026-09-24T00:00:00 | 1.09 | 249963 | [{'date': '2026-09-24T00:00:00', 'shortPercent... | BMG9156K1018 | 2020 BULKERS |
The field activePositions is a list of dictionary items that report detail information on short sellers and the respective holdings, and one needs to further open up this field to have a long table that shows each item as a separate row.
While most records show only one item in the activePositions field (i.e. one position holder’s record), there are many cases where more than one position holder’s records are presented in the activePositions field, organized in a dictionary. And note that the variable shortPercent actually represents the aggregated shortPercent reported by each position holder in that row.
For instance, below is the content of one activePositions:
[{‘date’: ‘2023-03-03’,
‘shortPercent’: 0.68,
‘shares’: 153129,
‘positionHolder’: ‘Voleon Capital Management LP’},
{‘date’: ‘2023-03-07’,
‘shortPercent’: 0.89,
‘shares’: 198486,
‘positionHolder’: ‘WorldQuant LLC’}]
As some of the records report a complete empty activePositions field, I first remove these records from the entire sample and then parse out the position holder information from this field, and then create a new dataframe that contains all the relevant fields.
# parse out the positionHolder info from the activePositions column
dateLst, holderLst, shortPctLst = [], [], []
shareLst, isinLst, issuerLst = [], [], []
for i in range(dfValid.shape[0]):
thisIsin = dfValid.iloc[i].isin.__self__.to_dict()['isin']
thisIssuer = dfValid.iloc[i].issuerName
for pos in dfValid.iloc[i].activePositions:
holderLst.append(pos['positionHolder'])
shortPctLst.append(pos['shortPercent'])
dateLst.append(pos['date'])
shareLst.append(pos['shares'])
isinLst.append(thisIsin)
issuerLst.append(thisIssuer)
longDf = pd.DataFrame({
'position_date': dateLst, 'isin': isinLst, 'issuer': issuerLst,
'position_holder': holderLst, 'net_short_position': shortPctLst,
'NumPositions': shareLst,
})Last step is to remove the redundant rows from longDf: the reason is that the json data reports aggregate value by date per row, with each row possibly containing more than one position_holder.
When at least one of the position holders change position, it will lead to update in the aggregate short position, even though the rest of the position holders in that row might not report any changes. With that background in mind, when expanding the position_holders one by one, the ones that do not change positions will get repopulated day after day and therefore appear in duplicate records.
# the API reports one row per date with possibly several position
# holders, and holders who didn't change position get repeated on every
# subsequent date - drop those duplicates
longDf = longDf.drop_duplicates()
longDf.shape(3537, 6)
And the final output for Norway looks like the following:
longDf.sort_values(['isin', 'position_holder', 'position_date']).head()| position_date | isin | issuer | position_holder | net_short_position | NumPositions | |
|---|---|---|---|---|---|---|
| 800 | 2025-09-30T00:00:00 | BMG0451H2087 | ARCHER | WorldQuant LLC | 0.52 | 477063 |
| 799 | 2025-10-02T00:00:00 | BMG0451H2087 | ARCHER | WorldQuant LLC | 0.66 | 599968 |
| 798 | 2025-10-06T00:00:00 | BMG0451H2087 | ARCHER | WorldQuant LLC | 0.70 | 701581 |
| 797 | 2025-10-10T00:00:00 | BMG0451H2087 | ARCHER | WorldQuant LLC | 0.81 | 807536 |
| 796 | 2025-10-24T00:00:00 | BMG0451H2087 | ARCHER | WorldQuant LLC | 0.78 | 783521 |