diff --git a/README.md b/README.md index 82ca7f8..a17abc0 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The project relies mainly on: - `pandas` - `requests` -- `xlrd` +- `openpyxl` Some MCX datasets are published as Excel files, so spreadsheet-reading support is required for part of the API. diff --git a/mcxlib/libutil.py b/mcxlib/libutil.py index 792ff29..4ad19ad 100644 --- a/mcxlib/libutil.py +++ b/mcxlib/libutil.py @@ -56,16 +56,15 @@ def validate_date_param(start_date:str, end_date:str): if not start_date or not end_date: raise ValueError(' Please provide the valid parameters') try: - start_date = datetime.strptime(start_date, '%Y%m%d') - end_date = datetime.strptime(end_date, '%Y%m%d') - time_delta = (end_date - start_date).days - if time_delta < 1: - raise ValueError(f'end_date should greater than start_date ') - elif time_delta > 365: - raise ValueError(f'Date range cannot be greater than 365 days') - except Exception as e: - print(e) + start = datetime.strptime(start_date, '%Y%m%d') + end = datetime.strptime(end_date, '%Y%m%d') + except (TypeError, ValueError): raise ValueError(f'either or both start_date = {start_date} || end_date = {end_date} are not valid value') + time_delta = (end - start).days + if time_delta < 0: + raise ValueError('end_date should not be earlier than start_date') + if time_delta > 365: + raise ValueError('Date range cannot be greater than 365 days') def get_mcxlib_path(): diff --git a/mcxlib/market_data.py b/mcxlib/market_data.py index 65d3d60..0b9a54b 100644 --- a/mcxlib/market_data.py +++ b/mcxlib/market_data.py @@ -235,8 +235,11 @@ def get_most_active_puts_calls(option_type:str = 'PE', """ headers = get_headers(use_for='most-active-puts-calls') url = "https://www.mcxindia.com/backpage.aspx/GetMostActiveOptionsContractsByVolume" - payload_param = {'OptionType':f'{option_type}','Product':f'{product}','InstrumentType':f'{instrument}'} - payload = f"{payload_param}" + payload = json.dumps({ + "OptionType": f"{option_type}", + "Product": f"{product}", + "InstrumentType": f"{instrument}" + }) try: data_dict = post_json(url, headers=headers, payload=payload) data_df = pd.DataFrame.from_dict(data_dict['d']['Data']) @@ -256,8 +259,10 @@ def get_bhav_copy(trade_date:str = '20230102', """ headers = get_headers(use_for='bhavcopy') url = "https://www.mcxindia.com/backpage.aspx/GetDateWiseBhavCopy" - payload_param = {'Date': f'{trade_date}', 'InstrumentName': f'{instrument}'} - payload = f"{payload_param}" + payload = json.dumps({ + "Date": f"{trade_date}", + "InstrumentName": f"{instrument}" + }) try: data_dict = post_json(url, headers=headers, payload=payload) data_df = pd.DataFrame.from_dict(data_dict['d']['Data']) @@ -352,8 +357,10 @@ def get_option_chain(commodity:str = 'CRUDEOIL', expiry:str = '15NOV2023') -> pd """ headers = get_headers(use_for='option-chain') url = "https://www.mcxindia.com/backpage.aspx/GetOptionChain" - payload_param = {'Commodity':f'{commodity}','Expiry':f'{expiry}'} - payload = f"{payload_param}" + payload = json.dumps({ + "Commodity": f"{commodity}", + "Expiry": f"{expiry}" + }) try: data_dict = post_json(url, headers=headers, payload=payload) data_df = pd.DataFrame.from_dict(data_dict['d']['Data']) @@ -464,7 +471,6 @@ def get_trading_statistics(year:int = 2023, month_number:int = 9) -> pd.DataFram try: url = (f"https://www.mcxindia.com/docs/default-source/market-data/historicaldata/" f"{year}/{month_long}/trading-statistics-{month_short}-{year}.xlsx") - print(url) data_df = pd.read_excel(url, skipfooter=5) except Exception as e: raise ValueError(f" apply valid parameter : MCX error:{e}") diff --git a/requirements.txt b/requirements.txt index b693fc6..caac613 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ pandas>=2.0.0 requests>=2.31.0 -xlrd>=2.0.1 \ No newline at end of file +openpyxl>=3.1.0 \ No newline at end of file diff --git a/setup.py b/setup.py index e3af164..826dfc0 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ long_description_content_type="text/markdown", author='RuchiTanmay', author_email='ruchitanmay@gmail.com', url='https://github.com/RuchiTanmay/mcxlib', - install_requires=['requests', 'pandas'], + install_requires=['requests', 'pandas', 'openpyxl'], keywords=['mcx', 'mcx india', 'python', 'mcx data', 'mcx history data', 'commodity', 'mcx python', 'mcx python library', 'mcx library'], classifiers=[ diff --git a/tests/test_libutil.py b/tests/test_libutil.py new file mode 100644 index 0000000..badc4e7 --- /dev/null +++ b/tests/test_libutil.py @@ -0,0 +1,34 @@ +import unittest + +from mcxlib.libutil import validate_date_param + + +class ValidateDateParamTest(unittest.TestCase): + def test_accepts_valid_range(self): + self.assertIsNone(validate_date_param('20230101', '20230331')) + + def test_accepts_single_day_range(self): + self.assertIsNone(validate_date_param('20230101', '20230101')) + + def test_rejects_end_date_before_start_date(self): + with self.assertRaises(ValueError) as ctx: + validate_date_param('20230201', '20230101') + self.assertIn('earlier than start_date', str(ctx.exception)) + + def test_rejects_range_longer_than_365_days(self): + with self.assertRaises(ValueError) as ctx: + validate_date_param('20230101', '20250101') + self.assertIn('365 days', str(ctx.exception)) + + def test_rejects_invalid_date_format(self): + with self.assertRaises(ValueError) as ctx: + validate_date_param('2023-01-01', '20230331') + self.assertIn('not valid value', str(ctx.exception)) + + def test_rejects_missing_dates(self): + with self.assertRaises(ValueError): + validate_date_param('', '20230331') + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_market_data.py b/tests/test_market_data.py index c4b9e07..3690e1f 100644 --- a/tests/test_market_data.py +++ b/tests/test_market_data.py @@ -1,4 +1,5 @@ from datetime import datetime, timezone +import json import unittest from unittest.mock import patch @@ -127,5 +128,73 @@ def test_get_available_contracts_is_exported(self): self.assertIs(mcxlib.get_available_contracts, market_data.get_available_contracts) +class RequestPayloadTest(unittest.TestCase): + def _capture_payload(self, func, response, **kwargs): + with patch.object(market_data, "post_json", return_value=response) as mock_post: + func(**kwargs) + return mock_post.call_args.kwargs["payload"] + + def test_get_bhav_copy_sends_valid_json_payload(self): + response = {"d": {"Data": [{"__type": "BhavCopy", "Symbol": "GOLD", "Open": 60000.0}]}} + + payload = self._capture_payload( + market_data.get_bhav_copy, + response, + trade_date="20230102", + instrument="ALL", + ) + + self.assertEqual(json.loads(payload), {"Date": "20230102", "InstrumentName": "ALL"}) + + def test_get_option_chain_sends_valid_json_payload(self): + response = { + "d": { + "Data": [ + { + "ExtensionData": None, + "PE_LTT": "", + "CE_LTT": "", + "LTT": "", + "Symbol": "CRUDEOIL", + "CE_OpenInterest": 10, + "PE_OpenInterest": 0, + "StrikePrice": 6000, + } + ] + } + } + + payload = self._capture_payload( + market_data.get_option_chain, + response, + commodity="CRUDEOIL", + expiry="15NOV2023", + ) + + self.assertEqual(json.loads(payload), {"Commodity": "CRUDEOIL", "Expiry": "15NOV2023"}) + + def test_get_most_active_puts_calls_sends_valid_json_payload(self): + response = { + "d": { + "Data": [ + {"ExtensionData": None, "LTT": "", "Symbol": "CRUDEOIL", "Volume": 100} + ] + } + } + + payload = self._capture_payload( + market_data.get_most_active_puts_calls, + response, + option_type="PE", + product="ALL", + instrument="OPTFUT", + ) + + self.assertEqual( + json.loads(payload), + {"OptionType": "PE", "Product": "ALL", "InstrumentType": "OPTFUT"}, + ) + + if __name__ == "__main__": unittest.main()