├── borsdata ├── __init__.py ├── constants.py ├── excel_exporter.py ├── borsdata_client.py └── borsdata_api.py ├── .gitignore ├── requirements.txt └── README.md /borsdata/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | borsdata/__pycache__ 2 | .idea -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | pandas 4 | requests 5 | openpyxl -------------------------------------------------------------------------------- /borsdata/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | User constants 3 | """ 4 | API_KEY = 'xxxx' 5 | EXPORT_PATH = 'file_exports/' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## BorsdataAPI 2 | Our simple Python console test client to get a quick start with Börsdata API. 3 | [More details about API is found here](https://github.com/Borsdata-Sweden/API) 4 | 5 | ## Api Key 6 | If you dont have an API Key, you need to Apply for an API key on [Börsdata webbpage](https://borsdata.se/). 7 | You need to be a Pro member to get Access to API. 8 | 9 | ## Installation 10 | Python 3+ needed! 11 | Use the package manager [pip](https://pip.pypa.io/en/stable/) to install dependencies. 12 | ```bash 13 | pip3 install -r requirements.txt 14 | ``` 15 | 16 | ## How to get started with Client 17 | Download project and run it from a terminal or any Python-IDE [PyCharm](https://www.jetbrains.com/pycharm/). 18 | In constants.py you replace xxxx with your unique API Key. 19 | Run borsdata_client.py (or excel_exporter.py) 20 | 21 | ## License 22 | [MIT](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /borsdata/excel_exporter.py: -------------------------------------------------------------------------------- 1 | from borsdata.borsdata_api import * 2 | import pandas as pd 3 | import os 4 | import datetime as dt 5 | from borsdata import constants as constants 6 | 7 | 8 | class ExcelExporter: 9 | """ 10 | A small example class that uses the BorsdataAPI to fetch and concatenate 11 | instrument data into excel-files. 12 | """ 13 | def __init__(self): 14 | self._api = BorsdataAPI(constants.API_KEY) 15 | self._instruments = self._api.get_instruments() 16 | self._markets = self._api.get_markets() 17 | self._countries = self._api.get_countries() 18 | 19 | def create_excel_files(self): 20 | # looping through all instruments 21 | for index, instrument in self._instruments.iterrows(): 22 | stock_prices = self._api.get_instrument_stock_prices(instrument['insId']) 23 | reports_quarter, reports_year, reports_r12 = self._api.get_instrument_reports(instrument['insId']) 24 | # map the instruments market/country id (integer) to its string representation in the market/country-table 25 | market = self._markets.loc[self._markets['id'] == instrument['marketId']]['name'].values[0].lower().replace(' ', '_') 26 | country = self._countries.loc[self._countries['id'] == instrument['countryId']]['name'].values[0].lower().replace(' ', '_') 27 | export_path = constants.EXPORT_PATH + f"{dt.datetime.now().date()}/{country}/{market}/" 28 | instrument_name = instrument['name'].lower().replace(' ', '_') 29 | # creating necessary folders if they do not exist 30 | if not os.path.exists(export_path): 31 | os.makedirs(export_path) 32 | # creating the writer with export location 33 | excel_writer = pd.ExcelWriter(export_path + instrument_name + ".xlsx") 34 | stock_prices.to_excel(excel_writer, 'stock_prices') 35 | reports_quarter.to_excel(excel_writer, 'reports_quarter') 36 | reports_year.to_excel(excel_writer, 'reports_year') 37 | reports_r12.to_excel(excel_writer, 'reports_r12') 38 | excel_writer.save() 39 | print(f'Excel exported: {export_path + instrument_name + ".xlsx"}') 40 | 41 | 42 | if __name__ == "__main__": 43 | excel = ExcelExporter() 44 | excel.create_excel_files() 45 | -------------------------------------------------------------------------------- /borsdata/borsdata_client.py: -------------------------------------------------------------------------------- 1 | # importing the borsdata_api 2 | from borsdata_api import BorsdataAPI 3 | # pandas is a data-analysis library for python (data frames) 4 | import pandas as pd 5 | # matplotlib for visual-presentations (plots) 6 | import matplotlib.pylab as plt 7 | # datetime for date- and time-stuff 8 | import datetime as dt 9 | # user constants 10 | from borsdata import constants as constants 11 | import numpy as np 12 | import os 13 | 14 | # pandas options for string representation of data frames (print) 15 | pd.set_option('display.max_columns', None) 16 | pd.set_option('display.max_rows', None) 17 | 18 | 19 | class BorsdataClient: 20 | def __init__(self): 21 | self._borsdata_api = BorsdataAPI(constants.API_KEY) 22 | self._instruments_with_meta_data = pd.DataFrame() 23 | 24 | def instruments_with_meta_data(self): 25 | """ 26 | creating a csv and xlsx of the APIs instrument-data (including meta-data) 27 | and saves it to path defined in constants (default ../file_exports/) 28 | :return: pd.DataFrame of instrument-data with meta-data 29 | """ 30 | if len(self._instruments_with_meta_data) > 0: 31 | return self._instruments_with_meta_data 32 | else: 33 | self._borsdata_api = BorsdataAPI(constants.API_KEY) 34 | # fetching data from api 35 | countries = self._borsdata_api.get_countries() 36 | branches = self._borsdata_api.get_branches() 37 | sectors = self._borsdata_api.get_sectors() 38 | markets = self._borsdata_api.get_markets() 39 | instruments = self._borsdata_api.get_instruments() 40 | # instrument type dict for conversion (https://github.com/Borsdata-Sweden/API/wiki/Instruments) 41 | instrument_type_dict = {0: 'Aktie', 1: 'Pref', 2: 'Index', 3: 'Stocks2', 4: 'SectorIndex', 42 | 5: 'BranschIndex', 8: 'SPAC', 13: 'Index GI'} 43 | # creating an empty dataframe 44 | instrument_df = pd.DataFrame() 45 | # loop through the whole dataframe (table) i.e. row-wise-iteration. 46 | for index, instrument in instruments.iterrows(): 47 | ins_id = index 48 | name = instrument['name'] 49 | ticker = instrument['ticker'] 50 | isin = instrument['isin'] 51 | # locating meta-data in various ways 52 | # dictionary-lookup 53 | instrument_type = instrument_type_dict[instrument['instrument']] 54 | # .loc locates the rows where the criteria (inside the brackets, []) is fulfilled 55 | # located rows (should be only one) get the column 'name' and return its value-array 56 | # take the first value in that array ([0], should be only one value) 57 | market = markets.loc[markets.index == instrument['marketId']]['name'].values[0] 58 | country = countries.loc[countries.index == instrument['countryId']]['name'].values[0] 59 | sector = 'N/A' 60 | branch = 'N/A' 61 | # index-typed instruments does not have a sector or branch 62 | if market.lower() != 'index': 63 | sector = sectors.loc[sectors.index == instrument['sectorId']]['name'].values[0] 64 | branch = branches.loc[branches.index == instrument['branchId']]['name'].values[0] 65 | # appending current data to dataframe, i.e. adding a row to the table. 66 | df_temp = pd.DataFrame([{'name': name, 'ins_id': ins_id, 'ticker': ticker, 'isin': isin, 67 | 'instrument_type': instrument_type, 68 | 'market': market, 'country': country, 'sector': sector, 'branch': branch}]) 69 | instrument_df = pd.concat([instrument_df, df_temp], ignore_index=True) 70 | # create directory if it do not exist 71 | if not os.path.exists(constants.EXPORT_PATH): 72 | os.makedirs(constants.EXPORT_PATH) 73 | # to csv 74 | instrument_df.to_csv(constants.EXPORT_PATH + 'instrument_with_meta_data.csv') 75 | # creating excel-document 76 | excel_writer = pd.ExcelWriter(constants.EXPORT_PATH + 'instrument_with_meta_data.xlsx') 77 | # adding one sheet 78 | instrument_df.to_excel(excel_writer, 'instruments_with_meta_data') 79 | # saving the document 80 | excel_writer.save() 81 | self._instruments_with_meta_data = instrument_df 82 | return instrument_df 83 | 84 | def plot_stock_prices(self, ins_id): 85 | """ 86 | Plotting a matplotlib chart for ins_id 87 | :param ins_id: instrument id to plot 88 | :return: 89 | """ 90 | # creating api-object 91 | # using api-object to get stock prices from API 92 | stock_prices = self._borsdata_api.get_instrument_stock_prices(ins_id) 93 | # calculating/creating a new column named 'sma50' in the table and 94 | # assigning the 50 day rolling mean to it 95 | stock_prices['sma50'] = stock_prices['close'].rolling(window=50).mean() 96 | # filtering out data after 2015 for plot 97 | filtered_data = stock_prices[stock_prices.index > dt.datetime(2015, 1, 1)] 98 | # plotting 'close' (with 'date' as index) 99 | plt.plot(filtered_data['close'], color='blue', label='close') 100 | # plotting 'sma50' (with 'date' as index) 101 | plt.plot(filtered_data['sma50'], color='black', label='sma50') 102 | # show legend 103 | plt.legend() 104 | # show plot 105 | plt.show() 106 | 107 | def top_performers(self, market, country, number_of_stocks=5, percent_change=1): 108 | """ 109 | function that prints top performers for given parameters in the terminal 110 | :param market: which market to search in e.g. 'Large Cap' 111 | :param country: which country to search in e.g. 'Sverige' 112 | :param number_of_stocks: number of stocks to print, default 5 (top5) 113 | :param percent_change: number of days for percent change calculation 114 | :return: pd.DataFrame 115 | """ 116 | # creating api-object 117 | # using defined function above to retrieve dataframe of all instruments 118 | instruments = self.instruments_with_meta_data() 119 | # filtering out the instruments with correct market and country 120 | filtered_instruments = instruments.loc[(instruments['market'] == market) & (instruments['country'] == country)] 121 | # creating new, empty dataframe 122 | stock_prices = pd.DataFrame() 123 | # looping through all rows in filtered dataframe 124 | for index, instrument in filtered_instruments.iterrows(): 125 | # fetching the stock prices for the current instrument 126 | instrument_stock_price = self._borsdata_api.get_instrument_stock_prices(int(instrument['ins_id'])) 127 | instrument_stock_price.sort_index(inplace=True) 128 | # calculating the current instruments percent change 129 | instrument_stock_price['pct_change'] = instrument_stock_price['close'].pct_change(percent_change) 130 | # getting the last row of the dataframe, i.e. the last days values 131 | last_row = instrument_stock_price.iloc[[-1]] 132 | # appending the instruments name and last days percent change to new dataframe 133 | df_temp = pd.DataFrame([{'stock': instrument['name'], 'pct_change': round(last_row['pct_change'].values[0] * 100, 2)}]) 134 | stock_prices = pd.concat([stock_prices, df_temp], ignore_index=True) 135 | # printing the top sorted by pct_change-column 136 | print(stock_prices.sort_values('pct_change', ascending=False).head(number_of_stocks)) 137 | return stock_prices 138 | 139 | def history_kpi(self, kpi, market, country, year): 140 | """ 141 | gathers and concatenates historical kpi-values for provided kpi, market and country 142 | :param kpi: kpi id see https://github.com/Borsdata-Sweden/API/wiki/KPI-History 143 | :param market: market to gather kpi-values from 144 | :param country: country to gather kpi-values from 145 | :param year: year for terminal print of kpi-values 146 | :return: pd.DataFrame of historical kpi-values 147 | """ 148 | # creating api-object 149 | # using defined function above to retrieve data frame of all instruments 150 | instruments = self.instruments_with_meta_data() 151 | # filtering out the instruments with correct market and country 152 | filtered_instruments = instruments.loc[(instruments['market'] == market) & (instruments['country'] == country)] 153 | # creating empty array (to hold data frames) 154 | frames = [] 155 | # looping through all rows in filtered data frame 156 | for index, instrument in filtered_instruments.iterrows(): 157 | # fetching the stock prices for the current instrument 158 | instrument_kpi_history = self._borsdata_api.get_kpi_history(int(instrument['ins_id']), kpi, 'year', 'mean') 159 | # check to see if response holds any data. 160 | if len(instrument_kpi_history) > 0: 161 | # resetting index and adding name as a column 162 | instrument_kpi_history.reset_index(inplace=True) 163 | instrument_kpi_history.set_index('year', inplace=True) 164 | instrument_kpi_history['name'] = instrument['name'] 165 | # appending data frame to array 166 | frames.append(instrument_kpi_history.copy()) 167 | # creating concatenated data frame with concat 168 | symbols_df = pd.concat(frames) 169 | # the data frame has the columns ['year', 'period', 'kpi_value', 'name'] 170 | # show year ranked from highest to lowest, show top 5 171 | print(symbols_df[symbols_df.index == year].sort_values('kpiValue', ascending=False).head(5)) 172 | return symbols_df 173 | 174 | def get_latest_pe(self, ins_id): 175 | """ 176 | Prints the PE-ratio of the provided instrument id 177 | :param ins_id: ins_id which PE-ratio will be calculated for 178 | :return: 179 | """ 180 | # creating api-object 181 | # fetching all instrument data 182 | reports_quarter, reports_year, reports_r12 = self._borsdata_api.get_instrument_reports(3) 183 | # getting the last reported eps-value 184 | reports_r12.sort_index(inplace=True) 185 | print(reports_r12.tail()) 186 | last_eps = reports_r12['earningsPerShare'].values[-1] 187 | # getting the stock prices 188 | stock_prices = self._borsdata_api.get_instrument_stock_prices(ins_id) 189 | stock_prices.sort_index(inplace=True) 190 | # getting the last close 191 | last_close = stock_prices['close'].values[-1] 192 | # getting the last date 193 | last_date = stock_prices.index.values[-1] 194 | # getting instruments data to retrieve the name of the ins_id 195 | instruments = self._borsdata_api.get_instruments() 196 | instrument_name = instruments[instruments.index == ins_id]['name'].values[0] 197 | # printing the name and calculated PE-ratio with the corresponding date. (array slicing, [:10]) 198 | print(f"PE for {instrument_name} is {round(last_close / last_eps, 1)} with data from {str(last_date)[:10]}") 199 | 200 | def breadth_large_cap_sweden(self): 201 | """ 202 | plots the breadth (number of stocks above moving-average 40) for Large Cap Sweden compared 203 | to Large Cap Sweden Index 204 | """ 205 | # creating api-object 206 | # using defined function above to retrieve data frame of all instruments 207 | instruments = self.instruments_with_meta_data() 208 | # filtering out the instruments with correct market and country 209 | filtered_instruments = instruments.loc[ 210 | (instruments['market'] == "Large Cap") & (instruments['country'] == "Sverige")] 211 | # creating empty array (to hold data frames) 212 | frames = [] 213 | # looping through all rows in filtered data frame 214 | for index, instrument in filtered_instruments.iterrows(): 215 | # fetching the stock prices for the current instrument 216 | instrument_stock_prices = self._borsdata_api.get_instrument_stock_prices(int(instrument['ins_id'])) 217 | # using numpy's where function to create a 1 if close > ma40, else a 0 218 | instrument_stock_prices[f'above_ma40'] = np.where( 219 | instrument_stock_prices['close'] > instrument_stock_prices['close'].rolling(window=40).mean(), 1, 0) 220 | instrument_stock_prices['name'] = instrument['name'] 221 | # check to see if response holds any data. 222 | if len(instrument_stock_prices) > 0: 223 | # appending data frame to array 224 | frames.append(instrument_stock_prices.copy()) 225 | # creating concatenated data frame with concat 226 | symbols_df = pd.concat(frames) 227 | symbols_df = symbols_df.groupby('date').sum() 228 | # fetching OMXSLCPI data from api 229 | omx = self._borsdata_api.get_instrument_stock_prices(643) 230 | # aligning data frames 231 | omx = omx[omx.index > '2015-01-01'] 232 | symbols_df = symbols_df[symbols_df.index > '2015-01-01'] 233 | # creating subplot 234 | fig, (ax1, ax2) = plt.subplots(2, sharex=True) 235 | # plotting 236 | ax1.plot(omx['close'], label="OMXSLCPI") 237 | ax2.plot(symbols_df[f'above_ma40'], label="number of stocks above ma40") 238 | # show legend 239 | ax1.legend() 240 | ax2.legend() 241 | plt.show() 242 | 243 | 244 | if __name__ == "__main__": 245 | # Main, call functions here. 246 | # creating BorsdataClient-instance 247 | borsdata_client = BorsdataClient() 248 | # calling some methods 249 | borsdata_client.breadth_large_cap_sweden() 250 | borsdata_client.get_latest_pe(87) 251 | borsdata_client.instruments_with_meta_data() 252 | borsdata_client.plot_stock_prices(3) # ABB 253 | borsdata_client.history_kpi(2, 'Large Cap', 'Sverige', 2020) # 2 == Price/Earnings (PE) 254 | borsdata_client.top_performers('Large Cap', 'Sverige', 10, 255 | 5) # showing top10 performers based on 5 day return (1 week) for Large Cap Sverige. 256 | -------------------------------------------------------------------------------- /borsdata/borsdata_api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pandas as pd 3 | import time 4 | from borsdata import constants as constants 5 | 6 | # pandas options for string representation of data frames (print) 7 | pd.set_option("display.max_columns", None) 8 | pd.set_option("display.max_rows", None) 9 | 10 | 11 | class BorsdataAPI: 12 | def __init__(self, _api_key): 13 | self._api_key = _api_key 14 | self._url_root = "https://apiservice.borsdata.se/v1/" 15 | self._last_api_call = 0 16 | self._api_calls_per_second = 10 17 | self._params = {'authKey': self._api_key, 'maxYearCount': 20, 'maxR12QCount': 40, 'maxCount': 20} 18 | 19 | def _call_api(self, url, **kwargs): 20 | """ 21 | Internal function for API calls 22 | :param url: URL add to URL root 23 | :params: Additional URL parameters 24 | :return: JSON-encoded content, if any 25 | """ 26 | current_time = time.time() 27 | time_delta = current_time - self._last_api_call 28 | if time_delta < 1 / self._api_calls_per_second: 29 | time.sleep(1 / self._api_calls_per_second - time_delta) 30 | response = requests.get(self._url_root + url, self._get_params(**kwargs)) 31 | print(response.url) 32 | self._last_api_call = time.time() 33 | if response.status_code != 200: 34 | print(f"API-Error, status code: {response.status_code}") 35 | return response 36 | return response.json() 37 | 38 | def _get_params(self, **kwargs): 39 | params = self._params.copy() 40 | for key, value in kwargs.items(): 41 | if value is not None: 42 | # fix for reserved keyword 'from' in python. 43 | if key == "from_date": 44 | params['from'] = value 45 | elif key == "to" or key == "date": 46 | params[key] = value 47 | elif key == "instList": 48 | params[key] = ",".join(str(stock_id) for stock_id in value) 49 | else: 50 | print(f"BorsdataAPI >> Unknown param: {key}={value}") 51 | return params 52 | 53 | @staticmethod 54 | def _set_index(df, index, ascending=True): 55 | """ 56 | Set index(es) and sort by index 57 | :param df: pd.DataFrame 58 | :param index: Column name to set to index 59 | :param ascending: True to sort index ascending 60 | """ 61 | if type(index) == list: 62 | for idx in index: 63 | if not idx in df.columns.array: 64 | return 65 | else: 66 | if not index in df.columns: 67 | return 68 | 69 | df.set_index(index, inplace=True) 70 | df.sort_index(inplace=True, ascending=ascending) 71 | 72 | @staticmethod 73 | def _parse_date(df, key): 74 | """ 75 | Parse date string as pd.datetime, if available 76 | :param df: pd.DataFrame 77 | :param key: Column name 78 | """ 79 | if key in df: 80 | df[key] = pd.to_datetime(df[key]) 81 | 82 | def _get_base_params(self): 83 | """ 84 | Get URL parameter base 85 | :return: Parameters dict 86 | """ 87 | return { 88 | "authKey": self._api_key, 89 | "version": 1, 90 | 'maxYearCount': 20, 91 | 'maxR12QCount': 40, 92 | 'maxCount': 20 93 | } 94 | 95 | """ 96 | Instrument Metadata 97 | """ 98 | 99 | def get_branches(self): 100 | """ 101 | Get branch data 102 | :return: pd.DataFrame 103 | """ 104 | url = "branches" 105 | json_data = self._call_api(url) 106 | df = pd.json_normalize(json_data["branches"]) 107 | self._set_index(df, "id") 108 | return df 109 | 110 | def get_countries(self): 111 | """ 112 | Get country data 113 | :return: pd.DataFrame 114 | """ 115 | url = "countries" 116 | json_data = self._call_api(url) 117 | df = pd.json_normalize(json_data["countries"]) 118 | self._set_index(df, "id") 119 | return df 120 | 121 | def get_markets(self): 122 | """ 123 | Get market data 124 | :return: pd.DataFrame 125 | """ 126 | url = "markets" 127 | json_data = self._call_api(url) 128 | df = pd.json_normalize(json_data["markets"]) 129 | self._set_index(df, "id") 130 | return df 131 | 132 | def get_sectors(self): 133 | """ 134 | Get sector data 135 | :return: pd.DataFrame 136 | """ 137 | url = "sectors" 138 | json_data = self._call_api(url) 139 | df = pd.json_normalize(json_data["sectors"]) 140 | self._set_index(df, "id") 141 | return df 142 | 143 | def get_translation_metadata(self): 144 | """ 145 | Get translation metadata 146 | :return: pd.DataFrame 147 | """ 148 | url = "translationmetadata" 149 | json_data = self._call_api(url) 150 | df = pd.json_normalize(json_data["translationMetadatas"]) 151 | self._set_index(df, "translationKey") 152 | return df 153 | 154 | """ 155 | Instruments 156 | """ 157 | 158 | def get_instruments(self): 159 | """ 160 | Get instrument data 161 | :return: pd.DataFrame 162 | """ 163 | url = "instruments" 164 | json_data = self._call_api(url) 165 | df = pd.json_normalize(json_data["instruments"]) 166 | self._parse_date(df, "listingDate") 167 | self._set_index(df, "insId") 168 | return df 169 | 170 | def get_instruments_updated(self): 171 | """ 172 | Get all updated instruments 173 | :return: pd.DataFrame 174 | """ 175 | url = "instruments/updated" 176 | json_data = self._call_api(url) 177 | df = pd.json_normalize(json_data["instruments"]) 178 | self._parse_date(df, "updatedAt") 179 | self._set_index(df, "insId") 180 | return df 181 | 182 | """ 183 | KPIs 184 | """ 185 | 186 | def get_kpi_history(self, ins_id, kpi_id, report_type, price_type, max_count=None): 187 | """ 188 | Get KPI history for an instrument 189 | :param ins_id: Instrument ID 190 | :param kpi_id: KPI ID 191 | :param report_type: ['quarter', 'year', 'r12'] 192 | :param price_type: ['mean', 'high', 'low'] 193 | :param max_count: Max. number of history (quarters/years) to get 194 | :return: pd.DataFrame 195 | """ 196 | url = f"instruments/{ins_id}/kpis/{kpi_id}/{report_type}/{price_type}/history" 197 | 198 | params = self._get_base_params() 199 | if max_count is not None: 200 | params["maxCount"] = max_count 201 | 202 | json_data = self._call_api(url) 203 | df = pd.json_normalize(json_data["values"]) 204 | df.rename(columns={"y": "year", "p": "period", "v": "kpiValue"}, inplace=True) 205 | self._set_index(df, ["year", "period"], ascending=False) 206 | return df 207 | 208 | def get_kpi_summary(self, ins_id, report_type, max_count=None): 209 | """ 210 | Get KPI summary for instrument 211 | :param ins_id: Instrument ID 212 | :param report_type: Report type ['quarter', 'year', 'r12'] 213 | :param max_count: Max. number of history (quarters/years) to get 214 | :return: pd.DataFrame 215 | """ 216 | url = f"instruments/{ins_id}/kpis/{report_type}/summary" 217 | if max_count is not None: 218 | self._params["maxCount"] = max_count 219 | json_data = self._call_api(url) 220 | df = pd.json_normalize(json_data["kpis"], record_path="values", meta="KpiId") 221 | df.rename( 222 | columns={"y": "year", "p": "period", "v": "kpiValue", "KpiId": "kpiId"}, 223 | inplace=True, 224 | ) 225 | df = df.pivot_table( 226 | index=["year", "period"], columns="kpiId", values="kpiValue" 227 | ) 228 | self._set_index(df, ["year", "period"], ascending=False) 229 | return df 230 | 231 | def get_kpi_data_instrument(self, ins_id, kpi_id, calc_group, calc): 232 | """ 233 | Get screener data, for more information: https://github.com/Borsdata-Sweden/API/wiki/KPI-Screener 234 | :param ins_id: Instrument ID 235 | :param kpi_id: KPI ID 236 | :param calc_group: ['1year', '3year', '5year', '7year', '10year', '15year'] 237 | :param calc: ['high', 'latest', 'mean', 'low', 'sum', 'cagr'] 238 | :return: pd.DataFrame 239 | """ 240 | url = f"instruments/{ins_id}/kpis/{kpi_id}/{calc_group}/{calc}" 241 | json_data = self._call_api(url) 242 | df = pd.json_normalize(json_data["value"]) 243 | df.rename( 244 | columns={"i": "insId", "n": "valueNum", "s": "valueStr"}, 245 | inplace=True, 246 | ) 247 | self._set_index(df, "insId") 248 | return df 249 | 250 | def get_kpi_data_all_instruments(self, kpi_id, calc_group, calc): 251 | """ 252 | Get KPI data for all instruments 253 | :param kpi_id: KPI ID 254 | :param calc_group: ['1year', '3year', '5year', '7year', '10year', '15year'] 255 | :param calc: ['high', 'latest', 'mean', 'low', 'sum', 'cagr'] 256 | :return: pd.DataFrame 257 | """ 258 | url = f"instruments/kpis/{kpi_id}/{calc_group}/{calc}" 259 | json_data = self._call_api(url) 260 | df = pd.json_normalize(json_data["values"]) 261 | df.rename( 262 | columns={"i": "insId", "n": "valueNum", "s": "valueStr"}, 263 | inplace=True, 264 | ) 265 | self._set_index(df, "insId") 266 | return df 267 | 268 | def get_updated_kpis(self): 269 | """ 270 | Get latest calculation date and time for KPIs 271 | :return: pd.datetime 272 | """ 273 | url = "instruments/kpis/updated" 274 | json_data = self._call_api(url) 275 | return pd.to_datetime(json_data["kpisCalcUpdated"]) 276 | 277 | def get_kpi_metadata(self): 278 | """ 279 | Get KPI metadata 280 | :return: pd.DataFrame 281 | """ 282 | url = "instruments/kpis/metadata" 283 | json_data = self._call_api(url) 284 | df = pd.json_normalize(json_data["kpiHistoryMetadatas"]) 285 | self._set_index(df, "kpiId") 286 | return df 287 | 288 | """ 289 | Reports 290 | """ 291 | 292 | def get_instrument_report(self, ins_id, report_type, max_count=None): 293 | """ 294 | Get specific report data 295 | :param ins_id: Instrument ID 296 | :param report_type: ['quarter', 'year', 'r12'] 297 | :param max_count: Max. number of history (quarters/years) to get 298 | :return: pd.DataFrame of report data 299 | """ 300 | url = f"instruments/{ins_id}/reports/{report_type}" 301 | 302 | params = self._get_base_params() 303 | if max_count is not None: 304 | params["maxCount"] = max_count 305 | json_data = self._call_api(url) 306 | 307 | df = pd.json_normalize(json_data["reports"]) 308 | df.columns = [x.replace("_", "") for x in df.columns] 309 | self._parse_date(df, "reportStartDate") 310 | self._parse_date(df, "reportEndDate") 311 | self._parse_date(df, "reportDate") 312 | self._set_index(df, ["year", "period"], ascending=False) 313 | return df 314 | 315 | def get_instrument_reports(self, ins_id): 316 | """ 317 | Get all report data 318 | :param ins_id: Instrument ID 319 | :return: [pd.DataFrame quarter, pd.DataFrame year, pd.DataFrame r12] 320 | """ 321 | # constructing url for api-call, adding ins_id 322 | url = f"instruments/{ins_id}/reports" 323 | json_data = self._call_api(url) 324 | dfs = [] 325 | for report_type in ["reportsQuarter", "reportsYear", "reportsR12"]: 326 | df = pd.json_normalize(json_data[report_type]) 327 | df.columns = [x.replace("_", "") for x in df.columns] 328 | self._parse_date(df, "reportStartDate") 329 | self._parse_date(df, "reportEndDate") 330 | self._parse_date(df, "reportDate") 331 | self._set_index(df, ["year", "period"], ascending=False) 332 | dfs.append(df) 333 | return dfs 334 | 335 | def get_instrument_report_list(self, stock_id_list): 336 | """ 337 | Get all report data for Stocks in stock_id_list 338 | :param stock_id_list: Instrument ID list 339 | :return: [pd.DataFrame quarter, pd.DataFrame year, pd.DataFrame r12] 340 | """ 341 | url = f"instruments/reports" 342 | json_data = self._call_api(url, instList=stock_id_list) 343 | r12 = pd.json_normalize(json_data['reportList'], record_path="reportsR12", meta=["instrument"]) 344 | r12 = r12.rename(columns=str.lower) 345 | r12 = r12.rename(columns={'instrument': 'stock_id'}) 346 | r12.fillna(0, inplace=True) 347 | quarter = pd.json_normalize(json_data['reportList'], record_path="reportsQuarter", meta=["instrument"]) 348 | quarter = quarter.rename(columns=str.lower) 349 | quarter = quarter.rename(columns={'instrument': 'stock_id'}) 350 | quarter.fillna(0, inplace=True) 351 | year = pd.json_normalize(json_data['reportList'], record_path="reportsYear", meta=["instrument"]) 352 | year = year.rename(columns=str.lower) 353 | year = year.rename(columns={'instrument': 'stock_id'}) 354 | year.fillna(0, inplace=True) 355 | return quarter, year, r12 356 | 357 | def get_reports_metadata(self): 358 | """ 359 | Get reports metadata 360 | :return: pd.DataFrame 361 | """ 362 | url = "instruments/reports/metadata" 363 | json_data = self._call_api(url) 364 | df = pd.json_normalize(json_data["reportMetadatas"]) 365 | # Fix probable misspelling 'propery' -> 'property' 366 | df.rename( 367 | columns={"reportPropery": "reportProperty"}, 368 | inplace=True, 369 | ) 370 | df["reportProperty"] = df["reportProperty"].apply(lambda x: x.replace("_", "")) 371 | self._set_index(df, "reportProperty") 372 | return df 373 | 374 | """ 375 | Stock prices 376 | """ 377 | 378 | def get_instrument_stock_prices(self, ins_id, from_date=None, to_date=None, max_count=None): 379 | """ 380 | Get stock prices for instrument ID 381 | :param ins_id: Instrument ID 382 | :param from_date: Start date in string format, e.g. '2000-01-01' 383 | :param to_date: Stop date in string format, e.g. '2000-01-01' 384 | :param max_count: Max. number of history (quarters/years) to get 385 | :return: pd.DataFrame 386 | """ 387 | url = f"instruments/{ins_id}/stockprices" 388 | json_data = self._call_api(url, from_date=from_date, to=to_date) 389 | df = pd.json_normalize(json_data["stockPricesList"]) 390 | df.rename( 391 | columns={ 392 | "d": "date", 393 | "c": "close", 394 | "h": "high", 395 | "l": "low", 396 | "o": "open", 397 | "v": "volume", 398 | }, 399 | inplace=True, 400 | ) 401 | self._parse_date(df, "date") 402 | self._set_index(df, "date", ascending=False) 403 | return df 404 | 405 | def get_instrument_stock_prices_list(self, stock_id_list, from_date=None, to_date=None): 406 | """ 407 | Get stock prices for instrument ID 408 | :param stock_id_list: Instrument ID list 409 | :param from_date: Start date in string format, e.g. '2000-01-01' 410 | :param to_date: Stop date in string format, e.g. '2000-01-01' 411 | :return: pd.DataFrame 412 | """ 413 | url = 'instruments/stockprices' 414 | json_data = self._call_api(url, from_date=from_date, to=to_date, instList=stock_id_list) 415 | stock_prices = pd.json_normalize(json_data['stockPricesArrayList'], "stockPricesList", ['instrument']) 416 | stock_prices.rename(columns={'d': 'date', 'c': 'close', 'h': 'high', 'l': 'low', 417 | 'o': 'open', 'v': 'volume', 'instrument': 'stock_id'}, inplace=True) 418 | stock_prices.fillna(0, inplace=True) 419 | return stock_prices 420 | 421 | def get_instruments_stock_prices_last(self): 422 | """ 423 | Get last days' stock prices for all instruments 424 | :return: pd.DataFrame 425 | """ 426 | url = "instruments/stockprices/last" 427 | json_data = self._call_api(url) 428 | df = pd.json_normalize(json_data["stockPricesList"]) 429 | df.rename( 430 | columns={ 431 | "d": "date", 432 | "i": "insId", 433 | "c": "close", 434 | "h": "high", 435 | "l": "low", 436 | "o": "open", 437 | "v": "volume", 438 | }, 439 | inplace=True, 440 | ) 441 | self._parse_date(df, "date") 442 | self._set_index(df, "date", ascending=False) 443 | return df 444 | 445 | def get_stock_prices_date(self, date): 446 | """ 447 | Get all instrument stock prices for given date 448 | :param date: Date in string format, e.g. '2000-01-01' 449 | :return: pd.DataFrame 450 | """ 451 | url = "instruments/stockprices/date" 452 | 453 | json_data = self._call_api(url, date=date) 454 | df = pd.json_normalize(json_data["stockPricesList"]) 455 | df.rename( 456 | columns={ 457 | "d": "date", 458 | "i": "insId", 459 | "c": "close", 460 | "h": "high", 461 | "l": "low", 462 | "o": "open", 463 | "v": "volume", 464 | }, 465 | inplace=True, 466 | ) 467 | self._parse_date(df, "date") 468 | self._set_index(df, "insId") 469 | return df 470 | 471 | """ 472 | Stock splits 473 | """ 474 | 475 | def get_stock_splits(self): 476 | """ 477 | Get stock splits 478 | :return: pd.DataFrame 479 | """ 480 | url = "instruments/stocksplits" 481 | json_data = self._call_api(url) 482 | df = pd.json_normalize(json_data["stockSplitList"]) 483 | df.rename( 484 | columns={"instrumentId": "insId"}, 485 | inplace=True, 486 | ) 487 | self._parse_date(df, "splitDate") 488 | self._set_index(df, "insId") 489 | return df 490 | 491 | 492 | if __name__ == "__main__": 493 | # Main, call functions here. 494 | api = BorsdataAPI(constants.API_KEY) 495 | api.get_translation_metadata() 496 | api.get_instruments_updated() 497 | api.get_kpi_summary(3, "year") 498 | api.get_kpi_data_instrument(3, 10, '1year', 'mean') 499 | api.get_kpi_data_all_instruments(10, '1year', 'mean') 500 | api.get_updated_kpis() 501 | api.get_kpi_metadata() 502 | api.get_instrument_report(3, 'year') 503 | api.get_reports_metadata() 504 | api.get_stock_prices_date('2020-09-25') 505 | api.get_stock_splits() 506 | api.get_instrument_stock_prices(2, from_date="2022-01-01", to="2023-01-01") 507 | api.get_instrument_stock_prices_list([2, 3, 4, 5]) 508 | --------------------------------------------------------------------------------