From f36d1f5d78b6377bddc41e2a3d005c9b749e6d7c Mon Sep 17 00:00:00 2001 From: shrey Date: Tue, 30 Jan 2024 15:09:04 +0530 Subject: [PATCH] add bhav copy feature functions, exceptions & tests --- bsedata/bhavcopy.py | 57 +++++++++++++++++++++++++++++++++++++++++++ bsedata/bse.py | 46 +++++++++++++++++++++++++++++++++- bsedata/exceptions.py | 11 +++++++++ bsedata/helpers.py | 4 +-- docs/apiref.rst | 3 +++ docs/introduction.rst | 4 +-- test_bsedata.py | 24 +++++++++++++++--- 7 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 bsedata/bhavcopy.py diff --git a/bsedata/bhavcopy.py b/bsedata/bhavcopy.py new file mode 100644 index 0000000..aec9ef4 --- /dev/null +++ b/bsedata/bhavcopy.py @@ -0,0 +1,57 @@ +import os +import io +import csv +import requests +import tempfile +import datetime +from zipfile import ZipFile +from bsedata.exceptions import BhavCopyNotFound +from bsedata.helpers import COMMON_REQUEST_HEADERS + + +def loadBhavCopyData(statsDate: datetime.date) -> list: + tempDir = os.path.join(tempfile.gettempdir(), "bsedata") + zipfileName = f"EQ{statsDate.strftime('%d%m%y')}_CSV.ZIP" + r = requests.get( + f"https://www.bseindia.com/download/BhavCopy/Equity/{zipfileName}", + headers=COMMON_REQUEST_HEADERS, + ) + + if r.status_code != 200: + raise BhavCopyNotFound() + + try: + os.makedirs(tempDir) + except FileExistsError: + pass + + f_zip = open(os.path.join(tempDir, zipfileName), "wb+") + f_zip.write(r.content) + f_zip.close() + + output = [] + + with ZipFile(os.path.join(tempDir, zipfileName)) as bhavCopyZip: + with bhavCopyZip.open(f"EQ{statsDate.strftime('%d%m%y')}.CSV") as bhavCopyFile: + reader = csv.DictReader(io.TextIOWrapper(bhavCopyFile)) + for row in reader: + output.append(mapBhavCopyRowToDict(row)) + + return output + + +def mapBhavCopyRowToDict(row: dict) -> dict: + SC_TYPE_MAP = {"B": "bond", "Q": "equity", "D": "debenture", "P": "preference"} + return { + "scrip_code": row["SC_CODE"], + "open": row["OPEN"], + "high": row["HIGH"], + "low": row["LOW"], + "close": row["CLOSE"], + "last": row["LAST"], + "prev_close": row["PREVCLOSE"], + "total_trades": row["NO_TRADES"], + "total_shares_traded": row["NO_OF_SHRS"], + "net_turnover": row["NET_TURNOV"], + "scrip_type": SC_TYPE_MAP[row["SC_TYPE"]], + } diff --git a/bsedata/bse.py b/bsedata/bse.py index a794659..9eca4f1 100644 --- a/bsedata/bse.py +++ b/bsedata/bse.py @@ -24,7 +24,8 @@ """ -from . import losers, gainers, quote, indices +from . import losers, gainers, quote, indices, bhavcopy +import datetime import requests import json @@ -78,6 +79,49 @@ def updateScripCodes(self): f_stk.write(json.dumps(r.json())) f_stk.close() return + + def getBhavCopyData(self, statsDate: datetime.date): + """ + Get historical OHLCV data from Bhav Copy released by BSE everyday after market closing. + The columns available in the data and their description is as given below. + + .. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Dictionary Field + - Description + * - scrip_code + - Unique code assigned to a scrip of a company by BSE + * - open + - The price at which the security first trades on a given trading day + * - high + - The highest intra-day price of a stock + * - low + - The lowest intra-day price of a stock + * - close + - The final price at which a security is traded on a given trading day + * - last + - The last trade price of the stock + * - prev_close + - The closing price of the stock for the previous trading day + * - total_trades + - The total number of trades of a scrip + * - total_shares_traded + - The total number of shares transacted of a scrip + * - net_turnover + - Total turnover of a scrip + * - scrip_type + - Scrip category: Equity, Preference, Debenture or Bond + + The Bhav Copy files have been mapped to the above mentioned custom fields. The complete documentation for Bhav Copy can be found here: https://www.bseindia.com/markets/MarketInfo/BhavCopy.aspx. + + + :param statsDate: A `datetime.date` object for the for which you want to fetch the data + :returns: A list of dictionaries which contains OHLCV data for that day for all scrip codes active on that day + :raises BhavCopyNotFound: Raised when Bhav Copy file is not found on BSE + """ + return bhavcopy.loadBhavCopyData(statsDate) def getScripCodes(self): """ diff --git a/bsedata/exceptions.py b/bsedata/exceptions.py index 84310d5..6fa3786 100644 --- a/bsedata/exceptions.py +++ b/bsedata/exceptions.py @@ -38,3 +38,14 @@ def __init__(self, status: str = "Inactive stock"): else: self.status = status super().__init__(self.status) + + +class BhavCopyNotFound(Exception): + """ + Exception raised when the BhavCopy file is not found on BSE website. + """ + + def __init__(self): + super().__init__( + """The BhavCopy file was not found on the BSE website. You are probably trying to get data for a trading holiday.""" + ) diff --git a/bsedata/helpers.py b/bsedata/helpers.py index dfc086e..1e67969 100644 --- a/bsedata/helpers.py +++ b/bsedata/helpers.py @@ -1,3 +1,3 @@ COMMON_REQUEST_HEADERS = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36 Edg/83.0.478.45' -} \ No newline at end of file + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36 Edg/83.0.478.45" +} diff --git a/docs/apiref.rst b/docs/apiref.rst index 8781ecb..f4e2af5 100644 --- a/docs/apiref.rst +++ b/docs/apiref.rst @@ -5,4 +5,7 @@ API Reference :members: .. autoexception:: bsedata.exceptions.InvalidStockException + :members: + +.. autoexception:: bsedata.exceptions.BhavCopyNotFound :members: \ No newline at end of file diff --git a/docs/introduction.rst b/docs/introduction.rst index 5712b2b..b9fcc49 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -36,11 +36,11 @@ bsedata is a library for collecting real-time data from Bombay Stock Exchange (I :target: https://codecov.io/gh/sdabhi23/bsedata :alt: Code Coverage -.. |testsMaster| image:: https://github.com/sdabhi23/bsedata/actions/workflows/tests.yml/badge.svg?branch=master +.. |testsMaster| image:: https://github.com/sdabhi23/bsedata/actions/workflows/dev-tests.yml/badge.svg?branch=master :target: https://github.com/sdabhi23/bsedata/actions/workflows/tests.yml :alt: Status of tests on master branch -.. |testsDev| image:: https://github.com/sdabhi23/bsedata/actions/workflows/tests.yml/badge.svg?branch=dev +.. |testsDev| image:: https://github.com/sdabhi23/bsedata/actions/workflows/dev-tests.yml/badge.svg?branch=dev :target: https://github.com/sdabhi23/bsedata/actions/workflows/tests.yml :alt: Status of tests on dev branch diff --git a/test_bsedata.py b/test_bsedata.py index 6f3d755..396641c 100644 --- a/test_bsedata.py +++ b/test_bsedata.py @@ -23,12 +23,12 @@ SOFTWARE. """ -import datetime +import time import pytest - +import datetime from bsedata.bse import BSE -from bsedata.exceptions import InvalidStockException +from bsedata.exceptions import InvalidStockException, BhavCopyNotFound b = BSE(update_codes=True) @@ -102,3 +102,21 @@ def test_getIndices(category): indices = b.getIndices(category) datetime.datetime.strptime(indices["updatedOn"], "%d %b %Y") assert len(indices["indices"]) >= 1 + time.sleep(1) + + +def test_getBhavCopyData_on_trade_holiday(): + with pytest.raises(BhavCopyNotFound): + b.getBhavCopyData(datetime.date(2024, 1, 26)) + + +def test_getBhavCopyData(): + bhavCopy = b.getBhavCopyData(datetime.date(2024, 1, 25)) + + scripCodeTypes = {x["scrip_type"] for x in bhavCopy} + + predefinedScripCodeTypes = {"equity", "debenture", "preference", "bond"} + + assert scripCodeTypes == predefinedScripCodeTypes + + assert len(bhavCopy) > 0 \ No newline at end of file