├── bf4py ├── __init__.py ├── BF4Py.py ├── company.py ├── connector.py ├── _utils.py ├── general.py ├── bonds.py ├── live_data.py ├── equities.py ├── news.py └── derivatives.py ├── LICENSE ├── .gitignore └── README.md /bf4py/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from .BF4Py import BF4Py -------------------------------------------------------------------------------- /bf4py/BF4Py.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from .equities import Equities 6 | from .news import News 7 | from .derivatives import Derivatives 8 | from .general import General 9 | from .company import Company 10 | from .live_data import LiveData 11 | 12 | from .connector import BF4PyConnector 13 | 14 | 15 | class BF4Py(): 16 | def __init__(self, default_isin=None, default_mic=None): 17 | self.default_isin = default_isin 18 | self.default_mic = default_mic 19 | 20 | self.connector = BF4PyConnector() 21 | 22 | self.equities = Equities(self.connector, self.default_isin) 23 | self.news = News(self.connector, self.default_isin) 24 | self.company = Company(self.connector, self.default_isin) 25 | self.derivatives = Derivatives(self.connector, self.default_isin) 26 | self.general = General(self.connector, self.default_isin) 27 | self.live_data = LiveData(self.connector, self.default_isin) 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 joqueka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /bf4py/company.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from .connector import BF4PyConnector 5 | 6 | class Company(): 7 | def __init__(self, connector: BF4PyConnector = None, default_isin = None): 8 | self.default_isin = default_isin 9 | 10 | if connector is None: 11 | self.connector = BF4PyConnector() 12 | else: 13 | self.connector = connector 14 | 15 | def upcoming_events(self, isin:str = None, limit:int = 100): 16 | """ 17 | Get all upcoming events for given company (by ISIN) 18 | 19 | Parameters 20 | ---------- 21 | isin : str 22 | Desired company ISIN. ISIN must be of type EQUITY or BOND, see instrument_information() -> instrumentTypeKey 23 | limit : int, optional 24 | Maximum count of events. The default is 100. 25 | 26 | Returns 27 | ------- 28 | TYPE 29 | List of dicts. 30 | 31 | """ 32 | if isin is None: 33 | isin = self.default_isin 34 | assert isin is not None, 'No ISIN given' 35 | 36 | params = {'isin': isin, 37 | 'limit': limit} 38 | 39 | return self.connector.data_request('upcoming_events', params) 40 | 41 | def about(self, isin:str = None): 42 | """ 43 | Get company description. 44 | 45 | Parameters 46 | ---------- 47 | isin : str 48 | Desired company ISIN. ISIN must be of type EQUITY or BOND, see instrument_information() -> instrumentTypeKey 49 | 50 | Returns 51 | ------- 52 | TYPE 53 | Dict with description. 54 | 55 | """ 56 | if isin is None: 57 | isin = self.default_isin 58 | assert isin is not None, 'No ISIN given' 59 | 60 | params = {'isin': isin} 61 | 62 | return self.connector.data_request('about_the_company', params) 63 | 64 | def contact_information(self, isin:str = None): 65 | """ 66 | Get contact information for specific company 67 | 68 | Parameters 69 | ---------- 70 | isin : str 71 | Desired company ISIN. ISIN must be of type EQUITY or BOND, see instrument_information() -> instrumentTypeKey 72 | 73 | Returns 74 | ------- 75 | TYPE 76 | Dict with contact information. 77 | 78 | """ 79 | if isin is None: 80 | isin = self.default_isin 81 | assert isin is not None, 'No ISIN given' 82 | 83 | params = {'isin': isin} 84 | 85 | return self.connector.data_request('contact_information', params) 86 | 87 | def company_information(self, isin:str = None): 88 | """ 89 | Get basic information about specific company 90 | 91 | Parameters 92 | ---------- 93 | isin : str 94 | Desired company ISIN. ISIN must be of type EQUITY or BOND, see instrument_information() -> instrumentTypeKey 95 | 96 | Returns 97 | ------- 98 | TYPE 99 | Dict with detailed information. 100 | 101 | """ 102 | if isin is None: 103 | isin = self.default_isin 104 | assert isin is not None, 'No ISIN given' 105 | 106 | params = {'isin': isin} 107 | 108 | return self.connector.data_request('corporate_information', params) 109 | 110 | def ipo_details(self, isin:str = None): 111 | """ 112 | Get details about company's IPO. This information is not always available! 113 | 114 | Parameters 115 | ---------- 116 | isin : str 117 | Desired company ISIN. ISIN must be of type EQUITY or BOND, see instrument_information() -> instrumentTypeKey 118 | 119 | Returns 120 | ------- 121 | TYPE 122 | Dict with IPO details. 123 | 124 | """ 125 | if isin is None: 126 | isin = self.default_isin 127 | assert isin is not None, 'No ISIN given' 128 | 129 | params = {'isin': isin} 130 | 131 | return self.connector.data_request('ipo_company_data', params) -------------------------------------------------------------------------------- /bf4py/connector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | class BF4PyConnector(): 5 | def __init__(self, salt: str=None): 6 | import requests, re 7 | 8 | self.session = requests.Session() 9 | 10 | self.session.headers.update({'authority': 'api.boerse-frankfurt.de', 11 | 'origin': 'https://www.boerse-frankfurt.de', 12 | 'referer': 'https://www.boerse-frankfurt.de/',}) 13 | 14 | if salt is None: 15 | # Step 1: Get Homepage and extract main-es2015 Javascript file 16 | response = self.session.get('https://www.boerse-frankfurt.de/') 17 | if response.status_code != 200: 18 | raise Exception('Could not connect to boerse-frankfurt.de') 19 | file = re.findall('(?<=src=")main\.\w*\.js', response.text) 20 | if len(file) != 1: 21 | raise Exception('Could not find ECMA Script name') 22 | 23 | # Step 2: Get Javascript file and extract salt 24 | response = self.session.get('https://www.boerse-frankfurt.de/'+file[0]) 25 | if response.status_code != 200: 26 | raise Exception('Could not connect to boerse-frankfurt.de') 27 | salt_list = re.findall('(?<=salt:")\w*', response.text) 28 | if len(salt_list) != 1: 29 | raise Exception('Could not find tracing-salt') 30 | self.salt = salt_list[0] 31 | else: 32 | self.salt = salt 33 | 34 | def __del__(self): 35 | self.session.close() 36 | 37 | def _create_ids(self, url): 38 | import hashlib 39 | from datetime import datetime 40 | timeutc = datetime.utcnow() 41 | timelocal = datetime.now() 42 | timestr = timeutc.isoformat(timespec='milliseconds') + 'Z' 43 | 44 | traceidbase = timestr + url + self.salt 45 | encoded = traceidbase.encode() 46 | traceid = hashlib.md5(encoded).hexdigest() 47 | 48 | xsecuritybase = timelocal.strftime("%Y%m%d%H%M") 49 | encoded = xsecuritybase.encode() 50 | xsecurity = hashlib.md5(encoded).hexdigest() 51 | 52 | return {'client-date':timestr, 'x-client-traceid':traceid, 'x-security':xsecurity} 53 | 54 | 55 | 56 | def _get_data_url(self, function: str, params:dict): 57 | import urllib 58 | baseurl = "https://api.boerse-frankfurt.de/v1/data/" 59 | p_string = urllib.parse.urlencode(params) 60 | return baseurl + function + '?' + p_string 61 | 62 | 63 | def data_request(self, function: str, params: dict): 64 | import json 65 | 66 | url = self._get_data_url(function, params) 67 | header = self._create_ids(url) 68 | header['accept'] = 'application/json, text/plain, */*' 69 | req = self.session.get(url, headers=header, timeout=(3.5, 15)) 70 | 71 | if req.text is None: 72 | raise Exception('Boerse Frankfurt returned no data, check parameters, especially period!') 73 | 74 | data = json.loads(req.text) 75 | 76 | if 'messages' in data: 77 | raise Exception('Boerse Frankfurt did not process request:', *data['messages']) 78 | 79 | return data 80 | 81 | def _get_search_url(self, function: str, params:dict): 82 | import urllib 83 | baseurl = "https://api.boerse-frankfurt.de/v1/search/" 84 | p_string = urllib.parse.urlencode(params) 85 | return baseurl + function + ('?' + p_string if p_string != '' else '') 86 | 87 | def search_request(self, function: str, params: dict): 88 | import json 89 | 90 | url = self._get_search_url(function, {}) 91 | header = self._create_ids(url) 92 | header['accept'] = 'application/json, text/plain, */*' 93 | header['content-type'] = 'application/json; charset=UTF-8' 94 | req = self.session.post(url, headers=header, timeout=(3.5, 15), json=params) 95 | data = json.loads(req.text) 96 | 97 | return data 98 | 99 | # Functions for STREAM requests 100 | 101 | def stream_request(self, function: str, params: dict): 102 | import sseclient, requests 103 | 104 | url = self._get_data_url(function, params) 105 | header = self._create_ids(url) 106 | header['accept'] = 'text/event-stream' 107 | header['cache-control'] = 'no-cache, no-store, must-revalidate, max-age=0' 108 | 109 | socket = requests.get(url, stream=True, headers=header, timeout=(3.5, 5)) 110 | client = sseclient.SSEClient(socket) 111 | 112 | return client 113 | 114 | -------------------------------------------------------------------------------- /bf4py/_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | def _get_salt(): 5 | import re, requests 6 | result = requests.get('https://www.boerse-frankfurt.de/main-es2015.ac96265ebda80215a714.js') 7 | salt = re.findall('(?<=salt:")\w*', result.text) 8 | return salt[0] 9 | 10 | def _create_header(url): 11 | ids = _create_ids(url=url) 12 | headers = {'authority': 'api.boerse-frankfurt.de', 13 | 'origin': 'https://www.boerse-frankfurt.de', 14 | 'referer': 'https://www.boerse-frankfurt.de/', 15 | 'client-date': '' + ids['timestr'], 16 | 'x-client-traceid': '' + ids['traceid'], 17 | 'x-security': '' + ids['xsecurity']} 18 | 19 | return headers 20 | 21 | def _create_ids(url): 22 | from datetime import datetime 23 | import hashlib 24 | 25 | salt="w4ivc1ATTGta6njAZzMbkL3kJwxMfEAKDa3MNr" 26 | 27 | timeutc=datetime.utcnow() 28 | timelocal=datetime.now() 29 | timestr = timeutc.isoformat(timespec='milliseconds') + 'Z' 30 | 31 | traceidbase = timestr + url + salt 32 | encoded = traceidbase.encode() 33 | traceid = hashlib.md5(encoded).hexdigest() 34 | 35 | xsecuritybase = timelocal.strftime("%Y%m%d%H%M") 36 | encoded = xsecuritybase.encode() 37 | xsecurity = hashlib.md5(encoded).hexdigest() 38 | 39 | return {'timestr':timestr, 'traceid':traceid, 'xsecurity':xsecurity} 40 | 41 | # Functions for retrieving DATA 42 | 43 | def _get_data_url(function: str, params:dict): 44 | import urllib 45 | baseurl = "https://api.boerse-frankfurt.de/v1/data/" 46 | p_string = urllib.parse.urlencode(params) 47 | return baseurl + function + '?' + p_string 48 | 49 | def _data_request(function: str, params: dict): 50 | import requests, json 51 | 52 | url = _get_data_url(function, params) 53 | header = _create_header(url) 54 | header['accept'] = 'application/json, text/plain, */*' 55 | req = requests.get(url, headers=header, timeout=(3.5, 15)) 56 | data = json.loads(req.text) 57 | 58 | return data 59 | 60 | # Functions for retrieving SEARCH requests 61 | 62 | def _get_search_url(function: str, params:dict): 63 | import urllib 64 | baseurl = "https://api.boerse-frankfurt.de/v1/search/" 65 | p_string = urllib.parse.urlencode(params) 66 | return baseurl + function + ('?' + p_string if p_string != '' else '') 67 | 68 | def _search_request(function: str, params: dict): 69 | import requests, json 70 | 71 | url = _get_search_url(function, {}) 72 | header = _create_header(url) 73 | header['accept'] = 'application/json, text/plain, */*' 74 | header['content-type'] = 'application/json; charset=UTF-8' 75 | req = requests.post(url, headers=header, timeout=(3.5, 15), json=params) 76 | data = json.loads(req.text) 77 | 78 | return data 79 | 80 | # Functions for STREAM requests 81 | 82 | def _stream_request(function: str, params: dict): 83 | import sseclient, requests 84 | 85 | url = _get_data_url(function, params) 86 | header = _create_header(url) 87 | header['accept'] = 'text/event-stream' 88 | header['cache-control'] = 'no-cache, no-store, must-revalidate, max-age=0' 89 | 90 | socket = requests.get(url, stream=True, headers=header, timeout=(3.5, 5)) 91 | client = sseclient.SSEClient(socket) 92 | 93 | return client 94 | 95 | def _read_chunked(request:callable, function:str, args:dict, CHUNK_SIZE:int=1000, 96 | identifiers:dict={'data':'data', 'numberElements':'totalCount'}): 97 | 98 | if 'limit' in args: 99 | limit = args['limit'] 100 | else: 101 | limit = 0 102 | if 'offset' in args: 103 | offset = args['offset'] 104 | else: 105 | offset = 0 106 | 107 | if limit > 0: 108 | maxCount = limit + offset 109 | else: 110 | maxCount = offset + CHUNK_SIZE + 1 #Initialwert, wird überschrieben 111 | 112 | result_list = [] 113 | position = offset 114 | #i = 0 115 | 116 | while position < maxCount: 117 | print('read from position', position, 'size', CHUNK_SIZE) 118 | args['offset'] = position 119 | args['limit'] = min(CHUNK_SIZE, maxCount - position) 120 | 121 | data = request(function, args) 122 | 123 | if limit == 0: 124 | maxCount = data[identifiers['numberElements']] 125 | 126 | result_list += data[identifiers['data']] 127 | 128 | #i += 1 129 | position += CHUNK_SIZE 130 | 131 | return result_list, data[identifiers['numberElements']] 132 | 133 | def _get_name(name_dict): 134 | name = name_dict['originalValue'] 135 | if 'translations' in name_dict: 136 | if 'de' in name_dict['translations']: 137 | name = name_dict['translations']['de'] 138 | if 'others' in name_dict['translations']: 139 | name = name_dict['translations']['others'] 140 | 141 | return name -------------------------------------------------------------------------------- /bf4py/general.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from datetime import date 5 | 6 | from .connector import BF4PyConnector 7 | 8 | class General(): 9 | def __init__(self, connector: BF4PyConnector = None, default_isin = None): 10 | self.default_isin = default_isin 11 | 12 | if connector is None: 13 | self.connector = BF4PyConnector() 14 | else: 15 | self.connector = connector 16 | 17 | def eod_data(self, min_date: date, max_date: date=date.today(), isin: str = None, mic:str='XETR'): 18 | """ 19 | Function for retrieving OHLC data including volume by pieces and cash amount (Euro) for selected date range. 20 | 21 | Parameters 22 | ---------- 23 | isin : str 24 | Desired ISIN. 25 | mic : str 26 | Exchange where instrument is listed (see instrument_information(isin) for available mic strings) 27 | min_date : date, optional 28 | Must be set to desired date, at least yesterday, because API returns no elements if min_date == max_date == today. 29 | max_date : date, optional 30 | The default is date.today(). 31 | 32 | Returns 33 | ------- 34 | TYPE 35 | Returns list of dicts with trading data. Elements are OHLC, date and turnover in Euro and Pieces. 36 | 37 | """ 38 | if isin is None: 39 | isin = self.default_isin 40 | assert isin is not None, 'No ISIN given' 41 | 42 | date_delta = max_date - min_date 43 | params = {'isin' : isin, 44 | 'mic': mic, 45 | 'minDate': min_date.strftime("%Y-%m-%d"), 46 | 'maxDate': max_date.strftime("%Y-%m-%d"), 47 | 'limit': date_delta.days, 48 | 'cleanSplit': False, 49 | 'cleanPayout': False, 50 | 'cleanSubscription': False} 51 | 52 | data = self.connector.data_request('price_history', params) 53 | 54 | return data['data'] 55 | 56 | def data_sheet_header(self, isin:str = None): 57 | """ 58 | Function for retrieving basic information about an instrument. 59 | 60 | Parameters 61 | ---------- 62 | isin : str 63 | Desired ISIN. 64 | 65 | Returns 66 | ------- 67 | data : TYPE 68 | Returns dict with basic information about given ISIN. 69 | 70 | """ 71 | if isin is None: 72 | isin = self.default_isin 73 | assert isin is not None, 'No ISIN given' 74 | 75 | params = {'isin': isin} 76 | 77 | data = self.connector.data_request('data_sheet_header', params) 78 | 79 | return data 80 | 81 | 82 | def instrument_information(self, isin:str = None): 83 | """ 84 | Function for retrieving extended trading information about selected instrument. 85 | 86 | Parameters 87 | ---------- 88 | isin : str 89 | Desired ISIN. 90 | 91 | Returns 92 | ------- 93 | data : TYPE 94 | Returns dict with trading data about given ISIN. 95 | 96 | """ 97 | if isin is None: 98 | isin = self.default_isin 99 | assert isin is not None, 'No ISIN given' 100 | 101 | params = {'isin': isin} 102 | 103 | data = self.connector.data_request('instrument_information', params) 104 | 105 | return data 106 | 107 | def index_instruments(self, isin: str = 'DE0008469008'): 108 | """ 109 | Function for retrieving all instruments in given index isin. 110 | 111 | Parameters 112 | ---------- 113 | isin : str, optional 114 | Desired Index ISIN. The default is 'DE0008469008' (DAX). 115 | 116 | Returns 117 | ------- 118 | instrument_list : TYPE 119 | List of dicts for every instrument in index. Every dict has values name, isin, wkn 120 | 121 | """ 122 | params = {'indices' : [isin], 123 | 'lang': 'de', 124 | 'offset': 0, 125 | 'limit': 1000, 126 | 'sorting': 'NAME', 127 | 'sortOrder': 'ASC'} 128 | 129 | data = self.connector.search_request('equity_search', params) 130 | 131 | #Reorganize data 132 | instrument_list = [] 133 | for e in data['data']: 134 | i = {'name': self._get_name(e['name']), 135 | 'isin': e['isin'], 136 | 'wkn': e['wkn'] 137 | } 138 | instrument_list.append(i) 139 | 140 | return instrument_list 141 | 142 | def _get_name(self, name_dict): 143 | name = name_dict['originalValue'] 144 | if 'translations' in name_dict: 145 | if 'de' in name_dict['translations']: 146 | name = name_dict['translations']['de'] 147 | if 'others' in name_dict['translations']: 148 | name = name_dict['translations']['others'] 149 | 150 | return name 151 | -------------------------------------------------------------------------------- /bf4py/bonds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from .connector import BF4PyConnector 5 | from datetime import date, datetime, timezone, time 6 | 7 | class Bonds(): 8 | def __init__(self, connector: BF4PyConnector = None, default_isin = None): 9 | self.default_isin = default_isin 10 | 11 | if connector is None: 12 | self.connector = BF4PyConnector() 13 | else: 14 | self.connector = connector 15 | 16 | 17 | def bond_data(self, isin:str = None, mic:str = None): 18 | """ 19 | Returns all information about given bond ISIN. 20 | 21 | Parameters 22 | ---------- 23 | isin : str 24 | ISIN of valid bond. 25 | 26 | Returns 27 | ------- 28 | data : TYPE 29 | Dict with information. 30 | 31 | """ 32 | if isin is None: 33 | isin = self.default_isin 34 | assert isin is not None, 'No ISIN given' 35 | if mic is None: 36 | mic = self.default_mic 37 | assert mic is not None, 'No mic (Exchange) given' 38 | 39 | params = {'isin': isin, 40 | 'mic': mic} 41 | 42 | data = self.connector.data_request('master_data_bond', params) 43 | 44 | return data 45 | 46 | 47 | def search_criteria(self): 48 | """ 49 | Returns all multi-option criteria lists for bond search 50 | 51 | Returns 52 | ------- 53 | data : TYPE 54 | Dict. 55 | 56 | """ 57 | params = {'lang': 'de'} 58 | 59 | data = self.connector.search_get_request('bond_search_criteria_data', params) 60 | 61 | return data 62 | 63 | @staticmethod 64 | def search_parameter_template(): 65 | """ 66 | Returns an empty template for searching bonds. Possible parameter values for list parameters can be obtained using search_criteria() 67 | 68 | Returns 69 | ------- 70 | params : dict 71 | Search-parameter template. 72 | 73 | """ 74 | params = { 75 | "greenBond": None, #boolean 76 | "newIssue": None, #boolean 77 | "callableByIssuer": None, #boolean 78 | "subordinated": None, #boolean 79 | "indexed": None, #boolean 80 | "market": None, #string 81 | "issuers": [], #list of string 82 | "issuerTypes": [], #list of string 83 | "bondTypes": [], #list of string 84 | "countries": [], #list of string 85 | "minimumInvestment": None, #int 86 | "currencies": [], #list of string 87 | "segments": [], #list of string 88 | "interestTypes": [], #list of string 89 | "termToMaturityMin": None, #int 90 | "termToMaturityMax": None, #int 91 | "couponMin": None, #int 92 | "couponMax": None, #int 93 | "yieldMin": None, #int 94 | "yieldMax": None, #int 95 | "durationMin": None, #float 96 | "durationMax": None, #int 97 | "modifiedDurationMin": None, #float 98 | "modifiedDurationMax": None, #int 99 | "accruedInterestMin": None, #int 100 | "accruedInterestMax": None, #int 101 | "maturityDateMin": None, #int (year) 102 | "maturityDateMax": None, #int (year) 103 | "turnoverPrevDayMin": None, #int 104 | "turnoverPrevDayMax": None, #int 105 | "issueVolumeMin": None, #int 106 | "issueVolumeMax": None, #int 107 | "lang": "de", 108 | "offset": 0, 109 | "limit": 25, 110 | "sorting": "NAME", 111 | "sortOrder": "DESC" 112 | } 113 | 114 | return params 115 | 116 | 117 | def search(self, params): 118 | """ 119 | Searches for bonds using specified parameters. 120 | 121 | Parameters 122 | ---------- 123 | params : dict 124 | Dict with parameters for bond search. Use search_parameter_template() to get a params template. 125 | Note that providing a parameter that is not intended for the bond type may lead to empty results. 126 | 127 | Returns 128 | ------- 129 | bonds_list : list of dicts 130 | Returns a list of bonds matching the search criterias. 131 | 132 | """ 133 | CHUNK_SIZE = 1000 134 | i = 0 135 | maxCount = CHUNK_SIZE + 1 136 | params['limit'] = CHUNK_SIZE 137 | 138 | bonds_list = [] 139 | 140 | while i * CHUNK_SIZE < maxCount: 141 | params['offset'] = i * CHUNK_SIZE 142 | 143 | data = self.connector.search_request('bond_search', params) 144 | 145 | maxCount = data['recordsTotal'] 146 | bonds_list += data['data'] 147 | 148 | i += 1 149 | 150 | return bonds_list -------------------------------------------------------------------------------- /bf4py/live_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import threading, json 5 | 6 | from .connector import BF4PyConnector 7 | 8 | class LiveData(): 9 | def __init__(self, connector: BF4PyConnector = None, default_isin: str = None): 10 | self.default_isin = default_isin 11 | self.streaming_clients = [] 12 | 13 | if connector is None: 14 | self.connector = BF4PyConnector() 15 | else: 16 | self.connector = connector 17 | 18 | 19 | def __del__(self): 20 | for client in self.streaming_clients: 21 | client.close() 22 | 23 | def price_information(self, isin:str=None, callback:callable=print, mic:str='XETR', cache_data=False): 24 | """ 25 | This function streams latest available price information of one instrument. 26 | 27 | Parameters 28 | ---------- 29 | isin : str 30 | Desired isin. 31 | callback : callable, optional 32 | Callback function to evaluate received data. It will get one argument containing JSON data. The default is print. 33 | mic : str, optional 34 | Provide appropriate exchange if symbol is not in XETRA. The default is 'XETR'. 35 | 36 | Returns 37 | ------- 38 | client : BFStreamClient 39 | return parameterized BFStreamClient. Use BFStreamClient.open_stream() to start receiving data. 40 | 41 | """ 42 | return self._generate_client('price_information', isin, callback, mic, cache_data) 43 | 44 | 45 | def bid_ask_overview(self, isin:str=None, callback:callable=print, mic:str='XETR', cache_data=False): 46 | """ 47 | This function streams top ten bid and ask quotes for given instrument. 48 | 49 | Parameters 50 | ---------- 51 | isin : str 52 | Desired isin. 53 | callback : callable, optional 54 | Callback function to evaluate received data. It will get one argument containing JSON data. The default is print. 55 | mic : str, optional 56 | Provide appropriate exchange if symbol is not in XETRA. The default is 'XETR'. 57 | 58 | Returns 59 | ------- 60 | client : BFStreamClient 61 | return parameterized BFStreamClient. Use BFStreamClient.open_stream() to start receiving data. 62 | 63 | """ 64 | return self._generate_client('bid_ask_overview', isin, callback, mic, cache_data) 65 | 66 | 67 | def live_quotes(self, isin:str=None, callback:callable=print, mic:str='XETR', cache_data=False): 68 | """ 69 | This function streams latest price quotes from bid and ask side. 70 | 71 | Parameters 72 | ---------- 73 | isin : str 74 | Desired isin. 75 | callback : callable, optional 76 | Callback function to evaluate received data. It will get one argument containing JSON data. The default is print. 77 | mic : str, optional 78 | Provide appropriate exchange if symbol is not in XETRA. The default is 'XETR'. 79 | 80 | Returns 81 | ------- 82 | client : BFStreamClient 83 | return parameterized BFStreamClient. Use BFStreamClient.open_stream() to start receiving data. 84 | 85 | """ 86 | return self._generate_client('quote_box', isin, callback, mic, cache_data) 87 | 88 | 89 | 90 | def _generate_client(self, function, isin, callback, mic, cache_data): 91 | if isin is None: 92 | isin = self.default_isin 93 | assert isin is not None, 'No ISIN given' 94 | 95 | params = {'isin': isin, 96 | 'mic': mic} 97 | 98 | client = BFStreamClient(function, params, callback=callback, connector=self.connector, cache_data=cache_data) 99 | self.streaming_clients.append(client) 100 | return client 101 | 102 | 103 | class BFStreamClient(): 104 | def __init__(self, function: str, params: dict, callback:callable=None, connector: BF4PyConnector = None, cache_data=False): 105 | self.active = False 106 | self.stop = False 107 | self.endpoint = function 108 | self.params = params 109 | self.callback = callback 110 | self.receiver_thread = None 111 | self.cache_data = cache_data 112 | self.data = [] 113 | 114 | if connector is None: 115 | self.connector = BF4PyConnector() 116 | else: 117 | self.connector = connector 118 | 119 | def __del__(self): 120 | self.close() 121 | 122 | def open_stream(self): 123 | if not self.active and self.receiver_thread is None: 124 | self.data = [] 125 | self.stop = False 126 | thread = threading.Thread(target = self.receive_data, name='bf4py.BFStreamClient_'+self.endpoint+'_'+self.params['isin']) 127 | thread.daemon = True 128 | thread.start() 129 | self.receiver_thread = thread 130 | self.active = True 131 | 132 | def receive_data(self): 133 | self.client = self.connector.stream_request(self.endpoint, self.params) 134 | try: 135 | for event in self.client.events(): 136 | if self.stop: 137 | break 138 | if event.event == 'message': 139 | try: 140 | data = json.loads(event.data) 141 | if self.cache_data: 142 | self.data.append(data) 143 | else: 144 | self.data = [data] 145 | 146 | if self.callback is not None: 147 | self.callback(data) 148 | except: 149 | continue 150 | except: 151 | print('bf4py Stream Client unintentionally stopped for', self.params['isin']) 152 | #self.client.close() 153 | self.active = False 154 | 155 | def close(self): 156 | if self.receiver_thread is not None: 157 | self.stop = True 158 | self.receiver_thread.join() 159 | self.receiver_thread = None 160 | self.active = False 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bf4py 2 | *A Python Package for retrieving data from boerse-frankfurt.de* 3 | 4 | **New methods:** Now you can search derivatives using specified parameters. 5 | 6 | **Latest update:** A lot of improvements were made under the hood. It's now object oriented resulting in better perfomance since http-sessions are reused for consecutive calls. Functions are now capsuled in classes but calls stay the same. However you must adapt function parameters since `isin` is not a positional argument anymore but an optional keyword-argument. 7 | 8 | ## Description 9 | 10 | This package uses an undocumented API to retrieve data that is available on [www.boerse-frankfurt.de](https://www.boerse-frankfurt.de) (no web-scraping!). It can be used by everyone who is interested in the german stock market as an alternative to available packages like [yfinance](https://github.com/ranaroussi/yfinance) which provides data for the US stock market. 11 | 12 | So far functions for retrieving basic data are available, especially for **equities**, **company information** and **news**. Some more for **bonds**, **funds**, **commodities** and **derivatives** will come in future depending on demand, since most of the data from the website is provided via API. 13 | 14 | ### Important notice 15 | Data is usually delayed by 15Min by the provider and some data like bid/ask history are available only for short periods of history. 16 | 17 | ## API Reference 18 | 19 | Functions are encapsuled in submodules. See docstrings for details about parameters. Return values are always dicts with self-describing keys, so just try out. 20 | 21 | ### bf4py.BF4Py() - *NEW* :rotating_light: 22 | This new class holds the https connection and provides functions via submodules like the previous version. Furthermore a default isin can be set, if not you have to provide it with every function call as before. 23 | 24 | bf4py = BF4Py(default_isin='...', default_mic='...') 25 | 26 | 27 | ### bf4py.general 28 | 29 | .eod_data(...) 30 | .data_sheet_header(...) 31 | .instrument_information(...) 32 | .index_instruments(...) 33 | 34 | ### bf4py.equities 35 | 36 | .equity_details(...) 37 | .key_data(...) 38 | .bid_ask_history(...) 39 | .times_sales(...) 40 | .related_indices(...) 41 | 42 | ### bf4py.company 43 | 44 | .about(...) 45 | .upcoming_events(...) 46 | .contact_information(...) 47 | .company_information(...) 48 | .ipo_details(...) 49 | 50 | ### bf4py.news 51 | 52 | .news_by_category(...) 53 | .news_by_isin(...) 54 | .news_by_id(...) 55 | .get_categories() 56 | 57 | ### bf4py.derivatives 58 | 59 | .trade_history(...) 60 | .instrument_data(...) 61 | .search_parameter_template() 62 | .search_derivatives(...) 63 | 64 | ### bf4py.live_data 65 | .price_information(...) 66 | .live_quotes(...) 67 | .bid_ask_overview(...) 68 | 69 | ## Examples 70 | 71 | from bf4py import BF4Py 72 | from datetime import datetime, timedelta 73 | 74 | bf4py = BF4Py(default_isin='DE0005190003') # Default BMW 75 | 76 | Get some basic data about *BMW* stock: 77 | 78 | bf4py.general.data_sheet_header() # Get info for BMW 79 | 80 | Yields in: 81 | 82 | {'participationCertificate': False, 83 | 'isin': 'DE0005190003', 84 | 'wkn': '519000', 85 | 'instrumentName': {'originalValue': 'BAY.MOTOREN WERKE AG ST', 86 | 'translations': {'others': 'BMW AG St'}}, 87 | 'exchangeSymbol': 'BMW', 88 | 'instrumentTypeKey': 'equity', 89 | 'underlyingValueList': None, 90 | 'issuer': None, 91 | 'companyIcon': 'https://erscontent.deutsche-boerse.com/erscontentXML/logo/204.jpg', 92 | 'isParticipationCertificate': False} 93 | 94 | If you want to get data about another instrument just provide the ISIN: 95 | 96 | 97 | bf4py.general.data_sheet_header(isin='DE0005190003') # Get info for Mercedes Benz 98 | 99 | Get the *daily OHLC data* of default stock on XETRA for one year: 100 | 101 | end_date = date.today() 102 | start_date = end_date - timedelta(days=365) 103 | 104 | history = bf4py.general.eod_data(start_date, end_date) 105 | 106 | Returns: 107 | 108 | [{'date': '2022-03-29', 109 | 'open': 217.3, 110 | 'close': 218.55, 111 | 'high': 220.45, 112 | 'low': 216.4, 113 | 'turnoverPieces': 1242298, 114 | 'turnoverEuro': 271453308.35 115 | }, ...] 116 | 117 | Get the *times and sales* list of that stock on XETRA: 118 | 119 | start_date = datetime.now() - timedelta(days=1) 120 | end_date = datetime.now() 121 | 122 | ts = bf4py.equities.times_sales(start_date, end_date) 123 | 124 | Result is a list of dicts where each dict is an executed trade: 125 | 126 | [{'time': '2022-02-18T17:37:22+01:00', 127 | 'price': 214.1, 128 | 'turnover': 567076.0, 129 | 'turnoverInEuro': 121410971.6}, ...] 130 | 131 | **Get live-data** 132 | 133 | For getting live data just create an receiver-client and start streaming: 134 | 135 | client = bf4py.live_data.live_quotes(isin) client.open_stream() 136 | 137 | Print output will be like: 138 | 139 | {'isin': 'DE0008404005', 'bidLimit': 191.76, 'askLimit': 191.8, 'bidSize': 1135.0, 'askSize': 109.0, 'lastPrice': 191.78, 'timestampLastPrice': '2022-06-08T15:06:07+02:00', 'changeToPrevDayAbsolute': -4.38, 'changeToPrevDayInPercent': -2.2328711257, 'spreadAbsolute': 0.04, 'spreadRelative': 0.0208594076, 'timestamp': '2022-06-08T15:06:12+02:00', 'nominal': False, 'tradingStatus': 'CONTINUOUS'} 140 | 141 | Finally stop transmission 142 | 143 | client.close() 144 | 145 | Notes: 146 | 147 | - By default received data is sent to `print()` function but you can provide your own callback function for data evaluation 148 | - Received data can be stored in `client.data`, use flag `cache_data=True` 149 | - Cached data is cleared with every call of `.open_stream()` 150 | - Sometimes it will need some seconds to start receiving data continuously 151 | - Now **you can reuse** a client after a connection was closed by intend or error 152 | - You can check client's status by `client.active` 153 | 154 | 155 | ## Requirements 156 | 157 | urllib 158 | hashlib 159 | requests 160 | json 161 | sseclient 162 | 163 | 164 | ## Misc 165 | Börse Frankfurt, Allianz and XETRA are registered brand names of their respective owners. I'm not connected in any way to one of the mentioned companys. Beyond that, I give no guarantee for the future functioning of the package as this depends on the technical framework of the provider. 166 | 167 | Furthermore I'm not a developer, I'm just interested in exploring data. If you have an idea for improvement just let me know. -------------------------------------------------------------------------------- /bf4py/equities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | 6 | from datetime import datetime, timezone 7 | from .connector import BF4PyConnector 8 | 9 | class Equities(): 10 | def __init__(self, connector: BF4PyConnector = None, default_isin = None): 11 | self.default_isin = default_isin 12 | 13 | if connector is None: 14 | self.connector = BF4PyConnector() 15 | else: 16 | self.connector = connector 17 | 18 | def equity_details(self, isin:str = None): 19 | """ 20 | Get basic data about specific equity (by ISIN). 21 | 22 | Parameters 23 | ---------- 24 | isin : str 25 | Desired ISIN. 26 | 27 | Returns 28 | ------- 29 | data : TYPE 30 | Dict with data. 31 | 32 | """ 33 | if isin is None: 34 | isin = self.default_isin 35 | assert isin is not None, 'No ISIN given' 36 | 37 | params = {'isin': isin} 38 | 39 | data = self.connector.data_request('equity_master_data', params) 40 | 41 | return data 42 | 43 | 44 | def key_data(self, isin:str = None): 45 | """ 46 | Get key/technical data about specific equity (by ISIN). 47 | 48 | Parameters 49 | ---------- 50 | isin : str 51 | Desired ISIN. 52 | 53 | Returns 54 | ------- 55 | data : TYPE 56 | Dict with data. 57 | 58 | """ 59 | if isin is None: 60 | isin = self.default_isin 61 | assert isin is not None, 'No ISIN given' 62 | 63 | params = {'isin': isin} 64 | 65 | data = self.connector.data_request('equity_key_data', params) 66 | 67 | return data 68 | 69 | 70 | 71 | def bid_ask_history(self, start: datetime, end: datetime=datetime.now(), isin:str = None): 72 | """ 73 | Get best bid/ask price history of specific equity (by ISIN). This usually works for about the last two weeks. 74 | 75 | Parameters 76 | ---------- 77 | isin : str 78 | Desired ISIN. 79 | start : datetime 80 | Startng date. Should not be more than two weeks ago 81 | end : datetime 82 | End date. 83 | 84 | Returns 85 | ------- 86 | ba_history : TYPE 87 | List of dicts with bid/ask data. 88 | 89 | """ 90 | if isin is None: 91 | isin = self.default_isin 92 | assert isin is not None, 'No ISIN given' 93 | 94 | #Initializing 95 | ba_history = [] 96 | i = 0 97 | CHUNK_SIZE = 1000 98 | maxCount = CHUNK_SIZE + 1 99 | 100 | params = {'limit': CHUNK_SIZE, 101 | 'offset': 0, 102 | 'isin': isin, 103 | 'mic': 'XETR', 104 | 'from': start.astimezone(timezone.utc).isoformat().replace('+00:00','Z'), 105 | 'to': end.astimezone(timezone.utc).isoformat().replace('+00:00','Z')} 106 | 107 | while i * CHUNK_SIZE < maxCount: 108 | params['offset'] = i * CHUNK_SIZE 109 | 110 | data = self.connector.data_request('bid_ask_history', params) 111 | 112 | maxCount = data['totalCount'] 113 | ba_history += data['data'] 114 | 115 | i += 1 116 | 117 | return ba_history 118 | 119 | def times_sales(self, start: datetime, end: datetime=None, isin: str = None): 120 | """ 121 | Get time/sales history of specific equity (by ISIN) from XETRA. This usually works for about the last two weeks. 122 | 123 | Parameters 124 | ---------- 125 | isin : str 126 | Desired ISIN. 127 | start : datetime 128 | Startng date. Should not be more than two weeks ago 129 | end : datetime 130 | End date. 131 | 132 | Returns 133 | ------- 134 | ts_list : TYPE 135 | List of dicts with time/sales data. 136 | 137 | """ 138 | if isin is None: 139 | isin = self.default_isin 140 | assert isin is not None, 'No ISIN given' 141 | 142 | if end is None: 143 | end = datetime.now() 144 | 145 | ts_list = [] 146 | i = 0 147 | CHUNK_SIZE = 10000 148 | maxCount = CHUNK_SIZE + 1 149 | 150 | params = {'isin': isin, 151 | 'limit': CHUNK_SIZE, 152 | 'offset': 0, 153 | 'mic': 'XETR', 154 | 'minDateTime': start.astimezone(timezone.utc).isoformat().replace('+00:00','Z'), 155 | 'maxDateTime': end.astimezone(timezone.utc).isoformat().replace('+00:00','Z')} 156 | 157 | while i * CHUNK_SIZE < maxCount: 158 | params['offset'] = i * CHUNK_SIZE 159 | 160 | data = self.connector.data_request('tick_data', params) 161 | 162 | maxCount = data['totalCount'] 163 | ts_list += data['ticks'] 164 | 165 | i += 1 166 | return ts_list 167 | 168 | def related_indices(self, isin:str = None): 169 | """ 170 | Get list of indices in which equity is listed. 171 | 172 | Parameters 173 | ---------- 174 | isin : str 175 | Desired ISIN. 176 | 177 | Returns 178 | ------- 179 | data : TYPE 180 | List of dicts with basic information about related indices. 181 | 182 | """ 183 | if isin is None: 184 | isin = self.default_isin 185 | assert isin is not None, 'No ISIN given' 186 | 187 | params = {'isin': isin} 188 | 189 | data = self.connector.data_request('related_indices', params) 190 | 191 | return data 192 | 193 | # def equity_search(limit: int = 25, search_params:dict = None): 194 | 195 | # params = {'limit': limit} 196 | # if search_params: 197 | # params.update(search_params) 198 | 199 | # #data = _read_chunked(_search_request, 'equity_search', params) 200 | # data = _search_request('equity_search', params) 201 | 202 | # return data 203 | 204 | -------------------------------------------------------------------------------- /bf4py/news.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from .connector import BF4PyConnector 6 | 7 | 8 | class News(): 9 | from datetime import datetime 10 | 11 | def __init__(self, connector: BF4PyConnector = None, default_isin = None): 12 | self.default_isin = default_isin 13 | 14 | self.category_list = ['ALL', 15 | 'EQUITY_MARKET_REPORT', 16 | 'BOERSE_FRANKFURT_NEWS_ALL', 17 | 'COMPANY_NEWS', 18 | 'BOERSE_FRANKFURT_KOLUMNEN', 19 | 'EQUITY_AD_HOC', 20 | 'EQUITY_ALL', 21 | 'BOND', 22 | 'BOERSE_FRANKFURT_NEWS_ETP', 23 | 'FUND', 24 | 'BOERSE_FRANKFURT_NEWS_DERIVATIVES', 25 | 'COMMODITY', 26 | 'CURRENCY'] 27 | 28 | if connector is None: 29 | self.connector = BF4PyConnector() 30 | else: 31 | self.connector = connector 32 | 33 | def news_by_id(self, news_id: str): 34 | """ 35 | Retrieving detail information about specific news-id. 36 | 37 | Parameters 38 | ---------- 39 | news_id : str 40 | Desired news id. 41 | 42 | Returns 43 | ------- 44 | data : TYPE 45 | Dict with news details. 46 | 47 | """ 48 | params = {'id': news_id} 49 | data = self.connector.data_request('news', params) 50 | 51 | return data 52 | 53 | def news_by_category(self, news_type: str ='ALL', limit: int=0, end_date: datetime = None): 54 | """ 55 | Retrieve a list of news for all or a specific category. 56 | Note that end_date defines the earliest time to which news should be fetched, as they're always loaded until the current time 57 | 58 | Parameters 59 | ---------- 60 | news_type : str, optional 61 | Desired category or all categories. The default is 'ALL'. 62 | limit : int, optional 63 | Maximum count of news to get. The default is 0 (=unlimited). 64 | end_date : datetime, optional 65 | Earliest date up to which news should be loaded. The default is None (=unlimited). 66 | 67 | Returns 68 | ------- 69 | news_list : TYPE 70 | List of dicts with basic information about news, including id, time, headline and source. 71 | 72 | """ 73 | 74 | assert news_type in self.category_list 75 | 76 | news_list = [] 77 | i = 0 78 | CHUNK_SIZE = 1000 79 | if limit == 0: 80 | maxCount = CHUNK_SIZE + 1 81 | else: 82 | maxCount = limit 83 | flag = True 84 | c = 0 85 | 86 | while i * CHUNK_SIZE < maxCount and flag: 87 | params = {'withPaging': True, 88 | 'lang': 'de', 89 | 'offset': i * CHUNK_SIZE, 90 | 'limit': CHUNK_SIZE, 91 | 'newsType': news_type} 92 | data = self.connector.data_request('category_news', params) 93 | i += 1 94 | 95 | if limit == 0: 96 | maxCount = data['totalCount'] 97 | 98 | for n in data['data']: 99 | #Check if either user given limit or end-date are reached 100 | if end_date is not None: 101 | if datetime.fromisoformat(n['time']).replace(tzinfo=None) < end_date: 102 | flag = False 103 | break 104 | if limit > 0: 105 | if c >= limit: 106 | flag = False 107 | break 108 | 109 | news_list.append(n) 110 | c += 1 111 | 112 | return news_list 113 | 114 | def news_by_isin(self, isin:str = None, limit:int=0, end_date: datetime = None): 115 | """ 116 | Retrieve all news related to a specific ISIN. 117 | 118 | Parameters 119 | ---------- 120 | isin : str 121 | Desired ISIN. 122 | limit : int, optional 123 | Maximum count of news to get. The default is 0 (=unlimited). 124 | end_date : datetime, optional 125 | Earliest date up to which news should be loaded. The default is None (=unlimited). 126 | 127 | Returns 128 | ------- 129 | news_list : TYPE 130 | List of dicts with basic information about news, consisting of id, time and headline. 131 | 132 | """ 133 | if isin is None: 134 | isin = self.default_isin 135 | assert isin is not None, 'No ISIN given' 136 | 137 | news_list = [] 138 | i = 0 139 | CHUNK_SIZE = 1000 140 | if limit == 0: 141 | maxCount = CHUNK_SIZE + 1 142 | else: 143 | maxCount = limit 144 | flag = True 145 | c = 0 146 | 147 | while i * CHUNK_SIZE < maxCount and flag: 148 | params = {'withPaging': True, 149 | 'lang': 'de', 150 | 'offset': i * CHUNK_SIZE, 151 | 'limit': CHUNK_SIZE, 152 | 'isin': isin, 153 | 'newsType': 'ALL'} 154 | data = self.connector.data_request('instrument_news', params) 155 | i += 1 156 | 157 | if limit == 0: 158 | maxCount = data['totalCount'] 159 | 160 | for n in data['data']: 161 | #Check if either user given limit or end-date are reached 162 | if end_date is not None: 163 | if datetime.fromisoformat(n['time']).replace(tzinfo=None) < end_date: 164 | flag = False 165 | break 166 | if limit > 0: 167 | if c >= limit: 168 | flag = False 169 | break 170 | 171 | news_list.append(n) 172 | c += 1 173 | 174 | return news_list 175 | 176 | 177 | def get_categories(self): 178 | """ 179 | Returns a list with all available news categories known to the author. 180 | 181 | Returns 182 | ------- 183 | TYPE 184 | List of categories. 185 | 186 | """ 187 | return self.category_list -------------------------------------------------------------------------------- /bf4py/derivatives.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from datetime import date, datetime, timezone, time 6 | 7 | from .connector import BF4PyConnector 8 | 9 | class Derivatives(): 10 | def __init__(self, connector: BF4PyConnector = None, default_isin = None, default_mic = 'XETR'): 11 | self.default_isin = default_isin 12 | self.default_mic = default_mic 13 | 14 | if connector is None: 15 | self.connector = BF4PyConnector() 16 | else: 17 | self.connector = connector 18 | 19 | 20 | def trade_history(self, search_date:date): 21 | """ 22 | Returns the times/sales list of every traded derivative for given day. 23 | Works for a wide range of dates, however details on instruments get less the more you move to history. 24 | 25 | Parameters 26 | ---------- 27 | search_date : date 28 | Date for which derivative trades should be received. 29 | 30 | Returns 31 | ------- 32 | tradelist : TYPE 33 | A list of dicts with details about trade and instrument. 34 | 35 | """ 36 | CHUNK_SIZE = 1000 37 | i = 0 38 | maxCount = CHUNK_SIZE + 1 39 | 40 | params = {'from': datetime.combine(search_date, time(8,0,0)).astimezone(timezone.utc).isoformat().replace('+00:00','Z'), 41 | 'to': datetime.combine(search_date, time(22,0,0)).astimezone(timezone.utc).isoformat().replace('+00:00','Z'), 42 | 'limit': CHUNK_SIZE, 43 | 'offset': 0, 44 | 'includePricesWithoutTurnover': False} 45 | 46 | tradelist = [] 47 | 48 | while i * CHUNK_SIZE < maxCount: 49 | params['offset'] = i * CHUNK_SIZE 50 | 51 | data = self.connector.data_request('derivatives_trade_history', params) 52 | 53 | maxCount = data['totalElements'] 54 | tradelist += data['data'] 55 | 56 | i += 1 57 | 58 | return tradelist 59 | 60 | def instrument_data(self, isin:str = None, mic:str = None): 61 | """ 62 | Returns all information about given derivative ISIN. 63 | 64 | Parameters 65 | ---------- 66 | isin : str 67 | ISIN ov valid derivative. 68 | 69 | Returns 70 | ------- 71 | data : TYPE 72 | Dict with information. 73 | 74 | """ 75 | if isin is None: 76 | isin = self.default_isin 77 | assert isin is not None, 'No ISIN given' 78 | if mic is None: 79 | mic = self.default_mic 80 | assert mic is not None, 'No mic (Exchange) given' 81 | 82 | params = {'isin': isin, 83 | 'mic': mic} 84 | 85 | data = self.connector.data_request('derivatives_master_data', params) 86 | 87 | return data 88 | 89 | 90 | def search_criteria(self): 91 | """ 92 | Returns all multi-option criteria lists for derivatives search (not implemented yet) 93 | 94 | Returns 95 | ------- 96 | data : TYPE 97 | Dict. 98 | 99 | """ 100 | params = {'lang': 'de', 101 | 'offset': 0, 102 | 'limit': 0, 103 | 'types': []} 104 | 105 | data = self.connector.search_request('derivative_search_criteria_data', params) 106 | 107 | return data 108 | 109 | @staticmethod 110 | def search_params(): 111 | """ 112 | Returns an empty template for searching derivatives. Possible parameter values can be obtained using search_criteria() 113 | 114 | Returns 115 | ------- 116 | params : dict 117 | Search-parameter template. 118 | 119 | """ 120 | params = { 121 | "barrierMax": None, 122 | "barrierMin": None, 123 | "bonusLevelMax": None, 124 | "bonusLevelMin": None, 125 | "capitalGuaranteeRelMax": None, 126 | "capitalGuaranteeRelMin": None, 127 | "deltaMax": None, 128 | "deltaMin": None, 129 | "evaluationDayMax": None, 130 | "evaluationDayMin": None, 131 | "isBarrierReached": None, 132 | "isBidOnly": None, 133 | "isKnockedOut": None, 134 | "isOpenEnd": None, 135 | "isPremiumSegment": None, 136 | "isQuanto": None, 137 | "isStopLevel": None, 138 | "issuers": None, 139 | "knockoutMax": None, 140 | "knockoutMin": None, 141 | "knockoutRelMax": None, 142 | "knockoutRelMin": None, 143 | "lang": "de", 144 | "leverageMax": None, 145 | "leverageMin": None, 146 | "omegaMax": None, 147 | "omegaMin": None, 148 | "origins": [], 149 | "participationMax": None, 150 | "participationMin": None, 151 | "participations": [], 152 | "rangeLowerMax": None, 153 | "rangeLowerMin": None, 154 | "rangeUpperMax": None, 155 | "rangeUpperMin": None, 156 | "sorting": "ASK", 157 | "sortOrder": "DESC", 158 | "strikeMax": None, 159 | "strikeMin": None, 160 | "subTypes": None, 161 | "topics": [], 162 | "tradingTimeEnd": None, 163 | "tradingTimeStart": None, 164 | "types": [], 165 | "underlyingFreeField": "", 166 | "underlyings": None, 167 | "units": None, 168 | "upperBarrierMax": None, 169 | "upperBarrierMin": None, 170 | 171 | } 172 | 173 | return params 174 | 175 | 176 | def search_derivatives(self, params): 177 | """ 178 | Searches for derivatives using specified parameters. 179 | 180 | Parameters 181 | ---------- 182 | params : dict 183 | Dict with parameters for derivatives search. Use search_params() to get a params template. 184 | Note that providing a parameter that is not intended for the derivative type (e.g. knock-out for regular option) may lead to empty results. 185 | 186 | Returns 187 | ------- 188 | derivatives_list : list of dicts 189 | Returns a list of derivatives matching the search criterias. 190 | 191 | """ 192 | CHUNK_SIZE = 1000 193 | i = 0 194 | maxCount = CHUNK_SIZE + 1 195 | params['limit'] = CHUNK_SIZE 196 | 197 | derivatives_list = [] 198 | 199 | while i * CHUNK_SIZE < maxCount: 200 | params['offset'] = i * CHUNK_SIZE 201 | 202 | data = self.connector.search_request('derivative_search', params) 203 | 204 | maxCount = data['recordsTotal'] 205 | derivatives_list += data['data'] 206 | 207 | i += 1 208 | 209 | return derivatives_list --------------------------------------------------------------------------------