From f5cd603df54bfbf62770204fd67d6b485803e48a Mon Sep 17 00:00:00 2001 From: Gregory Morse Date: Thu, 9 Apr 2026 11:04:38 +0200 Subject: [PATCH] feat: accept string aliases for method/extmethod constants (closes #14) calc_support_resistance and get_extrema now accept string names such as 'METHOD_NCUBED' in addition to the integer constants METHOD_NCUBED. An unrecognised string raises a clear ValueError naming the bad value. Fully backward-compatible: integer constants continue to work as before. --- CHANGELOG.md | 7 ++++ setup.py | 2 +- tests/test_trendln.py | 75 +++++++++++++++++++++++++++++++++++++++++++ trendln/__init__.py | 25 +++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71db3d8..9f5d034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Change Log =========== +0.1.12 +------- +- `calc_support_resistance` and `get_extrema` now accept string names for their + integer method constants (e.g. ``method='METHOD_NCUBED'`` in addition to + ``method=METHOD_NCUBED``); invalid strings raise a clear ``ValueError`` + (closes #14) + 0.1.11 ------- - Added `title`, `y_axis_label`, and `series_label` parameters to `plot_support_resistance` and `plot_sup_res_date` for customizable plot titles, y-axis labels, and series legend labels (thanks xeonvs) diff --git a/setup.py b/setup.py index 9eacdc7..4a24b65 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setup( name='trendln', - version="0.1.11", + version="0.1.12", description='Support and Resistance Trend lines Calculator for Financial Analysis', long_description=long_description, long_description_content_type='text/markdown', diff --git a/tests/test_trendln.py b/tests/test_trendln.py index eac5ea3..d316f36 100644 --- a/tests/test_trendln.py +++ b/tests/test_trendln.py @@ -276,3 +276,78 @@ def test_mismatched_tuple_lengths(self): def test_tuple_both_none_raises(self): with pytest.raises(ValueError): calc_support_resistance((None, None)) + + +# --------------------------------------------------------------------------- +# String aliases for method / extmethod constants (issue #14) +# --------------------------------------------------------------------------- + +class TestStringMethodAliases: + """calc_support_resistance and get_extrema accept string names for their + integer method constants, e.g. 'METHOD_NCUBED' as well as METHOD_NCUBED.""" + + # --- method parameter --- + + def test_method_string_ncubed(self): + assert_result( + calc_support_resistance(DATA_SIMPLE, method='METHOD_NCUBED'), + calc_support_resistance(DATA_SIMPLE, method=METHOD_NCUBED), + ) + + def test_method_string_nsquredlogn(self): + assert_result( + calc_support_resistance(DATA_SIMPLE, method='METHOD_NSQUREDLOGN'), + calc_support_resistance(DATA_SIMPLE, method=METHOD_NSQUREDLOGN), + ) + + def test_method_string_houghpoints(self): + assert_result( + calc_support_resistance(DATA_SIMPLE, method='METHOD_HOUGHPOINTS'), + calc_support_resistance(DATA_SIMPLE, method=METHOD_HOUGHPOINTS), + ) + + def test_method_string_houghlines(self): + assert_result( + calc_support_resistance(DATA_SIMPLE, method='METHOD_HOUGHLINES'), + calc_support_resistance(DATA_SIMPLE, method=METHOD_HOUGHLINES), + ) + + def test_method_string_probhough(self): + assert_result( + calc_support_resistance(DATA_SIMPLE, method='METHOD_PROBHOUGH'), + calc_support_resistance(DATA_SIMPLE, method=METHOD_PROBHOUGH), + ) + + # --- extmethod parameter --- + + def test_extmethod_string_naive(self): + assert_result( + calc_support_resistance(DATA_SIMPLE, extmethod='METHOD_NAIVE'), + calc_support_resistance(DATA_SIMPLE, extmethod=METHOD_NAIVE), + ) + + def test_extmethod_string_naiveconsec(self): + assert_result( + calc_support_resistance(DATA_SIMPLE, extmethod='METHOD_NAIVECONSEC'), + calc_support_resistance(DATA_SIMPLE, extmethod=METHOD_NAIVECONSEC), + ) + + def test_extmethod_string_numdiff(self): + assert_result( + calc_support_resistance(DATA_SIMPLE, extmethod='METHOD_NUMDIFF'), + calc_support_resistance(DATA_SIMPLE, extmethod=METHOD_NUMDIFF), + ) + + def test_get_extrema_string_extmethod(self): + assert get_extrema(DATA_SIMPLE, extmethod='METHOD_NAIVE') == \ + get_extrema(DATA_SIMPLE, extmethod=METHOD_NAIVE) + + # --- invalid string raises clearly --- + + def test_invalid_method_string_raises(self): + with pytest.raises(ValueError, match='METHOD_BOGUS'): + calc_support_resistance(DATA_SIMPLE, method='METHOD_BOGUS') + + def test_invalid_extmethod_string_raises(self): + with pytest.raises(ValueError, match='METHOD_BOGUS'): + get_extrema(DATA_SIMPLE, extmethod='METHOD_BOGUS') diff --git a/trendln/__init__.py b/trendln/__init__.py index fa89709..acf17e5 100644 --- a/trendln/__init__.py +++ b/trendln/__init__.py @@ -395,6 +395,29 @@ def test_sup_res(curdir): return None METHOD_NAIVE, METHOD_NAIVECONSEC, METHOD_NUMDIFF = 0, 1, 2 METHOD_NCUBED, METHOD_NSQUREDLOGN, METHOD_HOUGHPOINTS, METHOD_HOUGHLINES, METHOD_PROBHOUGH = 0, 1, 2, 3, 4 +_EXTMETHOD_NAMES = { + 'METHOD_NAIVE': METHOD_NAIVE, + 'METHOD_NAIVECONSEC': METHOD_NAIVECONSEC, + 'METHOD_NUMDIFF': METHOD_NUMDIFF, +} +_METHOD_NAMES = { + 'METHOD_NCUBED': METHOD_NCUBED, + 'METHOD_NSQUREDLOGN': METHOD_NSQUREDLOGN, + 'METHOD_HOUGHPOINTS': METHOD_HOUGHPOINTS, + 'METHOD_HOUGHLINES': METHOD_HOUGHLINES, + 'METHOD_PROBHOUGH': METHOD_PROBHOUGH, +} +def _resolve_name(value, lookup, param): + """Allow string aliases for integer method constants. + e.g. 'METHOD_NCUBED' is accepted in addition to METHOD_NCUBED. + """ + if isinstance(value, str): + if value not in lookup: + raise ValueError( + f'{param} string {value!r} is not a valid name; ' + f'use one of {list(lookup)} or the corresponding integer constant') + return lookup[value] + return value def check_num_alike(h): if type(h) is list and all([isinstance(x, (bool, int, float)) for x in h]): return True elif type(h) is np.ndarray and h.ndim==1 and h.dtype.kind in 'biuf': return True @@ -403,6 +426,7 @@ def check_num_alike(h): if type(h) is pd.Series and h.dtype.kind in 'biuf': return True else: return False def get_extrema(h, extmethod=METHOD_NUMDIFF, accuracy=2): + extmethod = _resolve_name(extmethod, _EXTMETHOD_NAMES, 'extmethod') #h must be single dimensional array-like object e.g. List, np.ndarray, pd.Series if type(h) is tuple and len(h) == 2 and (h[0] is None or check_num_alike(h[0])) and (h[1] is None or check_num_alike(h[1])) and (not h[0] is None or not h[1] is None): hmin, hmax = h[0], h[1] @@ -467,6 +491,7 @@ def numdiff_extrema(func): def calc_support_resistance(h, extmethod = METHOD_NUMDIFF, method=METHOD_NSQUREDLOGN, window=125, errpct=0.005, hough_scale=0.01, hough_prob_iter=10, sortError=False, accuracy=2): + method = _resolve_name(method, _METHOD_NAMES, 'method') if not type(window) is int: raise ValueError('window must be of type int') if not type(errpct) is float: