diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 68a51d3..9307a51 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -420,5 +420,79 @@ can do the following. . } +Getting a Stock Futures Quote +------------------------------ + +You can get the current Futures quote for any stock, given its stock code and expiry date using the command below. Here, we fetch the current futures quote of State Bank of India. The NSE stock code for +State Bank of India is **SBIN**. + +>>> fq = nse.get_futures_quote('sbin',expiry='29OCT2020') +>>> from pprint import pprint +>>> pprint(fq) +{'annualisedVolatility': 54.99, + 'bestBuy': 2.84, + 'bestSell': 3.57, + 'buyPrice1': 197.95, + 'buyPrice2': 197.85, + 'buyPrice3': 197.0, + 'buyPrice4': 196.7, + 'buyPrice5': 196.35, + 'buyQuantity1': 3000.0, + 'buyQuantity2': 3000.0, + 'buyQuantity3': 3000.0, + 'buyQuantity4': 9000.0, + 'buyQuantity5': 3000.0, + 'change': '-5.20', + 'changeinOpenInterest': 87000.0, + 'clientWisePositionLimits': None, + 'closePrice': 198.05, + 'dailyVolatility': 2.88, + 'expiryDate': '29OCT2020', + 'highPrice': 205.2, + 'impliedVolatility': None, + 'instrumentType': 'FUTSTK', + 'lastPrice': 198.1, + 'lowPrice': 196.0, + 'ltp': 3.2, + 'marketLot': 3000.0, + 'marketWidePositionLimits': 746624593.0, + 'numberOfContractsTraded': 134.0, + 'openInterest': 591000.0, + 'openPrice': 205.05, + 'optionType': None, + 'pChange': '-2.56', + 'pchangeinOpenInterest': 17.26, + 'premiumTurnover': None, + 'prevClose': 203.3, + 'sellPrice1': 198.25, + 'sellPrice2': 198.3, + 'sellPrice3': 199.45, + 'sellPrice4': 199.5, + 'sellPrice5': 200.2, + 'sellQuantity1': 3000.0, + 'sellQuantity2': 3000.0, + 'sellQuantity3': 9000.0, + 'sellQuantity4': 45000.0, + 'sellQuantity5': 6000.0, + 'settlementPrice': 198.05, + 'strikePrice': None, + 'totalBuyQuantity': 78000.0, + 'totalSellQuantity': 129000.0, + 'turnoverinRsLakhs': 805.81, + 'underlying': 'SBIN', + 'underlyingValue': 196.8, + 'vwap': 200.45} +>>> + +.. note:: + + The argument expiry is optional. You can also pass only Stock code as an argument. + In that case, it will fetch the result for the next upcoming expiry date. + +.. warning:: + + Always pass a valid expiry date. Otherwise the result will be a blank dict. + Also, take care of the proper format of expiry date. + .. disqus:: diff --git a/nse.py b/nse.py index 84136f0..ac4fcb0 100644 --- a/nse.py +++ b/nse.py @@ -57,6 +57,7 @@ def __init__(self): self.headers = self.nse_headers() # URL list self.get_quote_url = 'https://www1.nseindia.com/live_market/dynaContent/live_watch/get_quote/GetQuote.jsp?' + self.get_derivative_quote_url = 'https://www1.nseindia.com/live_market/dynaContent/live_watch/get_quote/GetQuoteFO.jsp?' self.stocks_csv_url = 'http://www1.nseindia.com/content/equities/EQUITY_L.csv' self.top_gainer_url = 'http://www1.nseindia.com/live_market/dynaContent/live_analysis/gainers/niftyGainers1.json' self.top_loser_url = 'http://www1.nseindia.com/live_market/dynaContent/live_analysis/losers/niftyLosers1.json' @@ -143,6 +144,19 @@ def is_valid_code(self, code): else: return False + def is_valid_fno_code(self, code): + """ + Returns true if a stock has Futures and Options derivative + :param code: a string stock code + :return: Boolean + """ + if code: + stock_codes = self.get_fno_lot_sizes() + if code.upper() in stock_codes.keys(): + return True + else: + return False + def get_quote(self, code, as_json=False): """ gets the quote for a given stock code @@ -182,6 +196,46 @@ def get_quote(self, code, as_json=False): else: return None + def get_futures_quote(self, code, expiry='NODATEGIVEN', as_json=False): + """ + gets the quote of futures derivative for a given stock code + :param code: + :return: dict or None + :raises: HTTPError, URLError + """ + code = code.upper() + expiry = expiry.upper() + if self.is_valid_fno_code(code): + url = self.build_url_for_futures_quote(code, expiry) + req = Request(url, None, self.headers) + # this can raise HTTPError and URLError, but we are not handling it + # north bound APIs should use it for exception handling + res = self.opener.open(req) + + # for py3 compat covert byte file like object to + # string file like object + res = byte_adaptor(res) + res = res.read() + # Now parse the response to get the relevant data + match = re.search(\ + r'(.*?)', + res, re.S + ) + try: + buffer = match.group(1).strip() + # commenting following two lines because now we are not using ast and instead + # relying on json's ability to do parsing. Should be much faster and more + # reliable. + #buffer = js_adaptor(buffer) + #response = self.clean_server_response(ast.literal_eval(buffer)['data'][0]) + response = self.clean_server_response(json.loads(buffer)['data'][0]) + except SyntaxError as err: + raise Exception('ill formatted response') + else: + return self.render_response(response, as_json) + else: + return None + def get_top_gainers(self, as_json=False): """ :return: a list of dictionaries containing top gainers of the day @@ -380,6 +434,21 @@ def build_url_for_quote(self, code): else: raise Exception('code must be string') + def build_url_for_futures_quote(self, code, expiry): + """ + builds a url which can be requested for futures dericative of given stock code + :param code: string containing stock code. + :return: a url object + """ + if code is not None and type(code) is str: + if expiry=='NODATEGIVEN': + encoded_args = urlencode([('underlying', code), ('instrument', 'FUTSTK')]) + else: + encoded_args = urlencode([('underlying', code), ('instrument', 'FUTSTK'), ('expiry', expiry)]) + return self.get_derivative_quote_url + encoded_args + else: + raise Exception('code must be string') + def clean_server_response(self, resp_dict): """cleans the server reponse by replacing: '-' -> None diff --git a/tests/nse_tests.py b/tests/nse_tests.py index 0879815..12378e6 100644 --- a/tests/nse_tests.py +++ b/tests/nse_tests.py @@ -47,6 +47,18 @@ def test_negative_build_url_for_quote(self): for test_code in negative_codes: url = self.nse.build_url_for_quote(test_code) + def test_build_url_for_futures_quote(self): + test_code = 'infy' + url = self.nse.build_url_for_futures_quote(test_code) + # 'test_code' should be present in the url + self.assertIsNotNone(re.search(test_code, url)) + + def test_negative_build_url_for_futures_quote(self): + negative_codes = [1, None] + with self.assertRaises(Exception): + for test_code in negative_codes: + url = self.nse.build_url_for_futures_quote(test_code) + def test_response_cleaner(self): test_dict = { 'a': '10', @@ -111,14 +123,34 @@ def test_get_quote(self): # the price changed in that very moment. self.assertDictEqual(resp, json.loads(json_resp)) + def test_get_futures_quote(self): + code = 'sbin' + resp = self.nse.get_futures_quote(code) + self.assertIsInstance(resp, dict) + # test json response + json_resp = self.nse.get_futures_quote(code, as_json=True) + self.assertIsInstance(json_resp, str) + # reconstruct the original dict from json + # this test may raise false alarms in case the + # the price changed in that very moment. + self.assertDictEqual(resp, json.loads(json_resp)) + def test_is_valid_code(self): code = 'infy' self.assertTrue(self.nse.is_valid_code(code)) + def test_is_valid_fno_code(self): + code = 'infy' + self.assertTrue(self.nse.is_valid_fno_code(code)) + def test_negative_is_valid_code(self): wrong_code = 'in' self.assertFalse(self.nse.is_valid_code(wrong_code)) + def test_negative_is_valid_fno_code(self): + wrong_code = 'in' + self.assertFalse(self.nse.is_valid_fno_code(wrong_code)) + def test_get_top_gainers(self): res = self.nse.get_top_gainers() self.assertIsInstance(res, list)