From 8b931764cfb747b1fd074690f51740c4ae839860 Mon Sep 17 00:00:00 2001 From: Pete Bentley Date: Sat, 1 Aug 2026 16:04:35 +0100 Subject: [PATCH] Fix historical price resolutions on pandas 3 pandas 3.0 removes the 'H' and 'M' frequency aliases, so conv_resol() will raise ValueError for 1H, 2H, 3H, 4H and M, the resolutions that fetch_historical_prices docstrings advertise. This change normalises them to 'h' and 'ME' before calling to_offset(), so both spellings work on pandas 2 and 3, and neither triggers the pandas 2 deprecation warning. Partially fixes #375 Noted while testing that conv_resol fails anyway on pandas 2.0 and 2.1 so I'm planning to bump the minimum in the next (test matrix) PR as it doesn't make anything worse --- tests/test_utils.py | 77 ++++++++++++++++++++++++++++++++++++++++++++- trading_ig/rest.py | 10 +++--- trading_ig/utils.py | 11 ++++++- 3 files changed, 92 insertions(+), 6 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5d8ea60..50ca169 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,15 @@ -from trading_ig.utils import conv_datetime +import warnings + +import pytest + +from trading_ig.utils import conv_datetime, conv_resol + +try: + import pandas # noqa + + pandas_installed = True +except ImportError: + pandas_installed = False """ Unit tests for utils module @@ -17,3 +28,67 @@ def test_conv_datetime_format_2(self): def test_conv_datetime_format_3(self): result = conv_datetime("2020/03/01", 3) assert result == "2020/03/01 00:00:00" + + @pytest.mark.skipif(not pandas_installed, reason="Requires pandas") + @pytest.mark.parametrize( + "resolution,expected", + [ + ("1s", "SECOND"), + ("1Min", "MINUTE"), + ("2Min", "MINUTE_2"), + ("3Min", "MINUTE_3"), + ("5Min", "MINUTE_5"), + ("10Min", "MINUTE_10"), + ("15Min", "MINUTE_15"), + ("30Min", "MINUTE_30"), + ("1h", "HOUR"), + ("2h", "HOUR_2"), + ("3h", "HOUR_3"), + ("4h", "HOUR_4"), + ("D", "DAY"), + ("W", "WEEK"), + ("ME", "MONTH"), + ], + ) + def test_conv_resol(self, resolution, expected): + assert conv_resol(resolution) == expected + + @pytest.mark.skipif(not pandas_installed, reason="Requires pandas") + @pytest.mark.parametrize( + "resolution,expected", + [ + ("1H", "HOUR"), + ("2H", "HOUR_2"), + ("3H", "HOUR_3"), + ("4H", "HOUR_4"), + ("M", "MONTH"), + ], + ) + def test_conv_resol_legacy_alias(self, resolution, expected): + """The 'H' and 'M' aliases were removed in pandas 3.0, but they are the + spellings this library has always documented, so they must keep working""" + assert conv_resol(resolution) == expected + + @pytest.mark.skipif(not pandas_installed, reason="Requires pandas") + def test_conv_resol_legacy_alias_does_not_warn(self): + """Legacy aliases are normalised before pandas sees them, so they don't + trigger the pandas 2 deprecation warning""" + with warnings.catch_warnings(): + warnings.simplefilter("error", FutureWarning) + assert conv_resol("1H") == "HOUR" + + @pytest.mark.skipif(not pandas_installed, reason="Requires pandas") + @pytest.mark.parametrize("resolution", ["2D", "2W", "12h"]) + def test_conv_resol_unsupported(self, resolution): + """A valid pandas offset that IG has no resolution for is returned + unchanged""" + assert conv_resol(resolution) == resolution + + @pytest.mark.skipif(not pandas_installed, reason="Requires pandas") + @pytest.mark.parametrize("resolution", ["banana", "", "X"]) + def test_conv_resol_invalid(self, resolution): + """Anything pandas can't parse at all raises, rather than being passed + on to the API. Used e.g. in + test_historical_prices_v3_num_points_bad_resolution""" + with pytest.raises(ValueError): + conv_resol(resolution) diff --git a/trading_ig/rest.py b/trading_ig/rest.py index 692c3e4..2dceb4c 100644 --- a/trading_ig/rest.py +++ b/trading_ig/rest.py @@ -1791,8 +1791,9 @@ def fetch_historical_prices_by_epic( :param epic: (str) The epic key for which historical prices are being requested :param resolution: (str, optional) timescale resolution. Expected values - are 1Min, 2Min, 3Min, 5Min, 10Min, 15Min, 30Min, 1H, 2H, 3H, 4H, D, - W, M. Default is 1Min + are 1Min, 2Min, 3Min, 5Min, 10Min, 15Min, 30Min, 1h, 2h, 3h, 4h, D, + W, ME. Default is 1Min. The pre-pandas 3 spellings 1H, 2H, 3H, 4H + and M are also accepted :param start_date: (datetime, optional) date range start, format yyyy-MM-dd'T'HH:mm:ss :param end_date: (datetime, optional) date range end, format @@ -1884,8 +1885,9 @@ def fetch_historical_prices_by_epic_and_date_range( and date range. Supports both versions 1 and 2 :param epic: IG epic :type epic: str - :param resolution: timescale for returned data. Expected values 'M', 'D', - '1H' etc + :param resolution: timescale for returned data. Expected values 'ME', + 'D', '1h' etc. The pre-pandas 3 spellings 'M' and '1H' are also + accepted :type resolution: str :param start_date: start date for returned data. For v1, format '2020:09:01-00:00:00', for v2 use '2020-09-01 00:00:00' diff --git a/trading_ig/utils.py b/trading_ig/utils.py index 5cc25e2..c1a3238 100644 --- a/trading_ig/utils.py +++ b/trading_ig/utils.py @@ -54,7 +54,16 @@ def conv_resol(resolution): to_offset("W"): "WEEK", to_offset("ME"): "MONTH", } - offset = to_offset(resolution) + # pandas 3.0 removes the 'H' and 'M' frequency aliases, in favour of + # 'h' and 'ME' so normalise them before calling to_offset + alias = resolution + if isinstance(alias, str): + if alias.endswith("H"): + alias = alias[:-1] + "h" + elif alias in ("M", "1M"): + alias = "ME" + + offset = to_offset(alias) if offset in d: return d[offset] else: