diff --git a/.gitignore b/.gitignore index b1eed444..cd752001 100644 --- a/.gitignore +++ b/.gitignore @@ -137,6 +137,9 @@ dmypy.json .benchmarks benchmark_data.json bench_*.json +benchmark_sample.json +decimal_benchmark.json +sample_indicators.json # Test results (non-benchmark) test-results/ diff --git a/benchmarks/test_decimal_conversion_performance.py b/benchmarks/test_decimal_conversion_performance.py new file mode 100644 index 00000000..090ac491 --- /dev/null +++ b/benchmarks/test_decimal_conversion_performance.py @@ -0,0 +1,79 @@ +"""Benchmarks comparing performance of different decimal conversion methods.""" + +import pytest + +from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes.decimal import to_pydecimal, to_pydecimal_via_double + + +@pytest.mark.performance +class TestDecimalConversionPerformance: + """Benchmark performance of different decimal conversion methods.""" + + def test_benchmark_string_conversion(self, benchmark, raw_data): + """Benchmark the current string-based conversion method.""" + raw_data = raw_data * 100 # Use subset for faster testing + + # Pre-convert to CsDecimal to isolate the conversion performance + cs_decimals = [CsDecimal(row[2]) for row in raw_data] + + def convert_via_string(cs_decimals): + for cs_decimal in cs_decimals: + to_pydecimal(cs_decimal) + + benchmark(convert_via_string, cs_decimals) + + def test_benchmark_double_conversion(self, benchmark, raw_data): + """Benchmark the new double-based conversion method.""" + raw_data = raw_data * 100 # Use subset for faster testing + + # Pre-convert to CsDecimal to isolate the conversion performance + cs_decimals = [CsDecimal(row[2]) for row in raw_data] + + def convert_via_double(cs_decimals): + for cs_decimal in cs_decimals: + to_pydecimal_via_double(cs_decimal) + + benchmark(convert_via_double, cs_decimals) + + def test_benchmark_small_dataset_string_conversion(self, benchmark): + """Benchmark string conversion with a controlled small dataset.""" + test_values = [ + 1996.1012, + 123.456789, + 0.123456789, + 999999.999999, + 0.000001, + 1000000.0, + 1.8e-05, + 1.234e10, + ] * 1000 # Repeat to get meaningful measurements + + cs_decimals = [CsDecimal(val) for val in test_values] + + def convert_via_string(cs_decimals): + for cs_decimal in cs_decimals: + to_pydecimal(cs_decimal) + + benchmark(convert_via_string, cs_decimals) + + def test_benchmark_small_dataset_double_conversion(self, benchmark): + """Benchmark double conversion with a controlled small dataset.""" + test_values = [ + 1996.1012, + 123.456789, + 0.123456789, + 999999.999999, + 0.000001, + 1000000.0, + 1.8e-05, + 1.234e10, + ] * 1000 # Repeat to get meaningful measurements + + cs_decimals = [CsDecimal(val) for val in test_values] + + def convert_via_double(cs_decimals): + for cs_decimal in cs_decimals: + to_pydecimal_via_double(cs_decimal) + + benchmark(convert_via_double, cs_decimals) diff --git a/docs/pages/performance.md b/docs/pages/performance.md index 13fe12b4..b2f2b473 100644 --- a/docs/pages/performance.md +++ b/docs/pages/performance.md @@ -7,6 +7,113 @@ noindex: true sitemap: false --- +# {{ page.title }} (Windows, Python 3.13) + +These are the execution times for the indicators using two years of historical daily stock quotes (502 periods) with default or typical parameters on Windows. + +```bash +pytest=v9.0.2, pytest-benchmark=v5.2.3 +OS=Windows 11 build 26200 +CPU=13th Gen Intel(R) Core(TM) i9-13900H (20 cores) +Python=CPython 3.13.11 +``` + +## Indicators and conversions + +```bash +Name Min (ms) Max (ms) Mean (ms) StdDev Median IQR OPS Rounds +test_benchmark_renko 0.5058 3.5910 0.6515 0.2599 0.6023 0.0683 1535.02 482 +test_benchmark_rsi 0.6020 2.7681 0.7210 0.1213 0.7046 0.0862 1387.03 749 +test_benchmark_atr 0.6033 3.0488 0.7262 0.1714 0.6831 0.0730 1377.07 745 +test_benchmark_ema 0.6119 1.6394 0.7292 0.0882 0.7165 0.0619 1371.31 432 +test_benchmark_smma 0.6125 1.4172 0.7434 0.0835 0.7201 0.0388 1345.25 655 +test_benchmark_cci 0.6222 1.4446 0.7493 0.1154 0.7267 0.0527 1334.66 404 +test_benchmark_roc 0.6024 1.9660 0.7514 0.1890 0.7061 0.0534 1330.89 458 +test_benchmark_fractal 0.6235 2.1104 0.7517 0.1139 0.7322 0.0516 1330.32 557 +test_benchmark_kama 0.6137 1.7470 0.7520 0.1039 0.7255 0.0558 1329.79 619 +test_benchmark_bop 0.6227 1.3432 0.7529 0.1049 0.7262 0.0499 1328.13 525 +test_benchmark_vwma 0.6159 1.7425 0.7535 0.1054 0.7291 0.0634 1327.13 631 +test_benchmark_dynamic 0.6179 4.1550 0.7536 0.2186 0.7129 0.0922 1326.89 665 +test_benchmark_wma 0.6257 1.3413 0.7538 0.1072 0.7303 0.0672 1326.62 858 +test_benchmark_force_index 0.6193 1.2192 0.7542 0.0834 0.7340 0.0463 1325.96 583 +test_benchmark_vwap 0.6161 1.3504 0.7543 0.0913 0.7312 0.0717 1325.80 597 +test_benchmark_obv 0.6188 9.4784 0.7546 0.4357 0.7042 0.0811 1325.23 444 +test_benchmark_smi 0.6169 2.4164 0.7560 0.1276 0.7347 0.0784 1322.77 582 +test_benchmark_slope 0.6214 2.1473 0.7567 0.1167 0.7345 0.0632 1321.56 779 +test_benchmark_dema 0.6053 1.6393 0.7579 0.1746 0.7107 0.1077 1319.39 585 +test_benchmark_vortex 0.6245 1.5766 0.7582 0.0976 0.7324 0.0588 1318.95 569 +test_benchmark_fisher_transform 0.6242 2.3836 0.7610 0.1597 0.7301 0.0732 1314.15 597 +test_benchmark_sma 0.6208 1.6036 0.7626 0.1114 0.7368 0.0455 1311.39 892 +test_benchmark_epma 0.6376 2.0052 0.7646 0.1160 0.7427 0.0779 1307.85 298 +test_benchmark_williams_r 0.6340 1.3901 0.7651 0.0905 0.7443 0.0509 1307.04 730 +test_benchmark_awesome 0.6201 2.6923 0.7652 0.1744 0.7258 0.0530 1306.88 438 +test_benchmark_elder_ray 0.6263 4.6096 0.7660 0.2157 0.7346 0.0879 1305.48 607 +test_benchmark_parabolic_sar 0.6172 1.5045 0.7692 0.1414 0.7356 0.1035 1300.06 437 +test_benchmark_stoch 0.6386 1.6827 0.7714 0.1292 0.7457 0.0835 1296.32 719 +test_benchmark_kvo 0.6427 1.3211 0.7716 0.0983 0.7427 0.0511 1296.05 554 +test_benchmark_alma 0.6150 1.7744 0.7717 0.1340 0.7372 0.0841 1295.78 522 +test_benchmark_mfi 0.6225 2.8507 0.7729 0.1733 0.7338 0.0978 1293.85 586 +test_benchmark_marubozu 0.6358 1.5268 0.7767 0.1389 0.7416 0.0935 1287.50 819 +test_benchmark_chandelier 0.6437 1.5799 0.7778 0.1198 0.7445 0.0517 1285.64 419 +test_benchmark_ma_envelopes 0.6221 2.7079 0.7780 0.1581 0.7427 0.0471 1285.39 534 +test_benchmark_ht_trendline 0.6487 1.3283 0.7810 0.1194 0.7552 0.0771 1280.34 281 +test_benchmark_cmo 0.6326 2.0743 0.7813 0.1483 0.7387 0.0469 1279.92 447 +test_benchmark_keltner 0.6240 2.6047 0.7827 0.1210 0.7593 0.0637 1277.62 613 +test_benchmark_pvo 0.6179 1.6550 0.7828 0.1887 0.7315 0.0861 1277.49 551 +test_benchmark_tsi 0.6162 2.5446 0.7831 0.1689 0.7372 0.0559 1276.95 555 +test_benchmark_chop 0.6404 1.8782 0.7847 0.1361 0.7473 0.0424 1274.44 584 +test_benchmark_gator 0.6320 1.2811 0.7860 0.1114 0.7467 0.1016 1272.30 517 +test_benchmark_stdev 0.6143 4.3753 0.7882 0.2264 0.7429 0.0645 1268.64 478 +test_benchmark_mama 0.6288 3.3905 0.7902 0.1680 0.7705 0.0934 1265.49 542 +test_benchmark_aroon 0.6492 3.1026 0.7908 0.1875 0.7517 0.0487 1264.48 395 +test_benchmark_starc_bands 0.6522 2.7085 0.7923 0.1458 0.7555 0.0601 1262.16 529 +test_benchmark_cmf 0.6634 1.6412 0.7924 0.1356 0.7531 0.0456 1261.94 447 +test_benchmark_stdev_channels 0.6413 1.3524 0.7930 0.1145 0.7658 0.0541 1260.99 500 +test_benchmark_fcb 0.6913 1.8795 0.7947 0.1064 0.7767 0.0770 1258.38 499 +test_benchmark_volatility_stop 0.6406 1.6809 0.7947 0.1460 0.7551 0.0477 1258.31 528 +test_benchmark_atr_stop 0.6259 1.4199 0.7950 0.1422 0.7675 0.0903 1257.88 443 +test_benchmark_macd 0.6388 1.3477 0.7965 0.1203 0.7633 0.0686 1255.54 570 +test_benchmark_stoch_rsi 0.6373 1.3855 0.7973 0.1228 0.7617 0.0541 1254.20 535 +test_benchmark_super_trend 0.6218 1.6929 0.7988 0.1472 0.7645 0.0917 1251.83 564 +test_benchmark_chaikin_osc 0.6272 2.3810 0.8001 0.1425 0.7695 0.0592 1249.90 446 +test_benchmark_dpo 0.6293 1.8743 0.8007 0.1860 0.7484 0.0860 1248.90 369 +test_benchmark_doji 0.6498 2.6358 0.8027 0.1899 0.7547 0.0543 1245.77 471 +test_benchmark_ultimate 0.6576 1.6127 0.8036 0.1590 0.7521 0.0497 1244.41 394 +test_benchmark_bollinger_bands 0.6307 2.3677 0.8049 0.1531 0.7830 0.0991 1242.41 402 +test_benchmark_triple_ema 0.6078 2.4939 0.8079 0.1769 0.7708 0.0860 1237.85 436 +test_benchmark_trix 0.6207 1.9191 0.8192 0.1691 0.7640 0.1226 1220.74 528 +test_benchmark_stc 0.6552 2.8504 0.8232 0.1933 0.7772 0.0729 1214.71 377 +test_benchmark_heikin_ashi 0.6448 1.5777 0.8241 0.1261 0.8122 0.0956 1213.41 412 +test_benchmark_hma 0.6717 2.7407 0.8294 0.1809 0.7827 0.0844 1205.73 295 +test_benchmark_pivot_points 0.6453 3.7643 0.8330 0.2229 0.7790 0.1322 1200.47 440 +test_benchmark_t3 0.6583 1.6644 0.8356 0.1531 0.8143 0.1233 1196.73 487 +test_benchmark_pmo 0.6554 1.8478 0.8492 0.1795 0.8013 0.1141 1177.51 355 +test_benchmark_connors_rsi 0.7261 2.4937 0.8724 0.1788 0.8273 0.0537 1146.28 338 +test_benchmark_ulcer_index 0.7257 1.7372 0.8732 0.1588 0.8249 0.0490 1145.22 436 +test_benchmark_zig_zag 0.7474 1.7469 0.8909 0.1358 0.8747 0.1151 1122.43 308 +test_benchmark_pivots 0.6822 4.0178 0.9075 0.2942 0.8316 0.1262 1101.98 376 +test_benchmark_rolling_pivots 0.7386 2.1527 0.9104 0.1553 0.8816 0.1098 1098.40 434 +test_benchmark_donchian 0.7682 1.8458 0.9271 0.1652 0.8829 0.0501 1078.63 241 +test_benchmark_adx 0.7468 1.6267 0.9324 0.1207 0.9183 0.1111 1072.56 501 +test_benchmark_alligator 0.7599 1.4379 0.9387 0.1284 0.9204 0.1171 1065.33 221 +test_benchmark_prs 1.0069 2.2567 1.2662 0.2193 1.1979 0.0815 789.79 410 +test_benchmark_correlation 1.0365 4.0427 1.2800 0.2402 1.2114 0.0721 781.23 474 +test_benchmark_beta 1.0599 4.1115 1.3300 0.2589 1.2709 0.1882 751.86 284 +test_benchmark_ichimoku 0.9693 3.6210 1.5621 0.5740 1.1807 1.0927 640.15 246 +test_benchmark_hurst 1.2046 3.0597 1.7850 0.5804 1.3624 1.1133 560.22 194 +test_benchmark_adl 0.9096 3.5800 2.3849 0.6378 2.5637 0.8626 419.30 110 +test_benchmark_small_dataset_double_conversion 11.1730 15.7974 13.4967 0.8632 13.8795 1.2070 74.09 71 +test_benchmark_sma_longlong 15.2710 35.4404 18.3152 3.7159 17.3157 0.8863 54.60 57 +test_benchmark_hurst_longlong 28.5278 40.5886 31.4385 3.0087 30.6435 1.2094 31.81 24 +test_benchmark_small_dataset_string_conversion 52.7986 58.4067 54.9115 1.5607 54.9273 2.0336 18.21 17 +test_benchmark_double_conversion 87.7827 90.8925 89.5127 0.7813 89.5080 0.7988 11.17 11 +test_benchmark_string_conversion 354.2637 371.2325 360.9358 8.0169 356.4304 14.0702 2.77 5 +test_benchmark_converting_to_IndicatorResults 430.7795 495.6005 465.9106 24.5489 471.4504 33.4462 2.15 5 +test_benchmark_converting_to_CsDecimal 6583.5905 7088.3011 6694.0890 220.6633 6600.6668 145.2359 0.15 5 +``` + +--- + # {{ page.title }} for v1.3.0 These are the execution times for the current indicators using two years of historical daily stock quotes (502 periods) with default or typical parameters. diff --git a/stock_indicators/_cstypes/__init__.py b/stock_indicators/_cstypes/__init__.py index d24d596b..4b9a7588 100644 --- a/stock_indicators/_cstypes/__init__.py +++ b/stock_indicators/_cstypes/__init__.py @@ -3,5 +3,5 @@ from stock_indicators import _cslib from .datetime import DateTime, to_pydatetime -from .decimal import Decimal, to_pydecimal +from .decimal import Decimal, to_pydecimal, to_pydecimal_via_double from .list import List diff --git a/stock_indicators/_cstypes/decimal.py b/stock_indicators/_cstypes/decimal.py index 5c72d344..e453476a 100644 --- a/stock_indicators/_cstypes/decimal.py +++ b/stock_indicators/_cstypes/decimal.py @@ -66,3 +66,27 @@ def to_pydecimal(cs_decimal: Optional[CsDecimal]) -> Optional[PyDecimal]: raise TypeConversionError( f"Cannot convert C# Decimal to Python Decimal: {e}" ) from e + + +def to_pydecimal_via_double(cs_decimal: Optional[CsDecimal]) -> Optional[PyDecimal]: + """ + Converts an object to a native Python decimal object via double conversion. + This method offers better performance (~4x faster) but may have precision loss. + + Parameter: + cs_decimal : `System.Decimal` of C# or None. + + Returns: + Python Decimal object or None if input is None. + """ + if cs_decimal is None: + return None + + try: + return PyDecimal(str(CsDecimal.ToDouble(cs_decimal))) + except Exception as e: + from stock_indicators.exceptions import TypeConversionError + + raise TypeConversionError( + f"Cannot convert C# Decimal to Python Decimal via double: {e}" + ) from e diff --git a/stock_indicators/indicators/atr_stop.py b/stock_indicators/indicators/atr_stop.py index 8e3a8a7f..ff18087a 100644 --- a/stock_indicators/indicators/atr_stop.py +++ b/stock_indicators/indicators/atr_stop.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.enums import EndType from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin from stock_indicators.indicators.common.quote import Quote @@ -57,7 +57,7 @@ class AtrStopResult(ResultBase): @property def atr_stop(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.AtrStop) + return to_pydecimal_via_double(self._csdata.AtrStop) @atr_stop.setter def atr_stop(self, value): @@ -65,7 +65,7 @@ def atr_stop(self, value): @property def buy_stop(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.BuyStop) + return to_pydecimal_via_double(self._csdata.BuyStop) @buy_stop.setter def buy_stop(self, value): @@ -73,7 +73,7 @@ def buy_stop(self, value): @property def sell_stop(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.SellStop) + return to_pydecimal_via_double(self._csdata.SellStop) @sell_stop.setter def sell_stop(self, value): diff --git a/stock_indicators/indicators/common/candles.py b/stock_indicators/indicators/common/candles.py index d1d94559..2d016d45 100644 --- a/stock_indicators/indicators/common/candles.py +++ b/stock_indicators/indicators/common/candles.py @@ -5,7 +5,7 @@ from stock_indicators._cslib import CsCandleProperties from stock_indicators._cstypes import Decimal as CsDecimal -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common._contrib.type_resolver import ( generate_cs_inherited_class, ) @@ -19,12 +19,12 @@ class _CandleProperties(_Quote): @property def size(self) -> Optional[Decimal]: # pylint: disable=no-member # C# interop properties - return to_pydecimal(self.High - self.Low) + return to_pydecimal_via_double(self.High - self.Low) @property def body(self) -> Optional[Decimal]: # pylint: disable=no-member # C# interop properties - return to_pydecimal( + return to_pydecimal_via_double( self.Open - self.Close if (self.Open > self.Close) else self.Close - self.Open @@ -33,14 +33,14 @@ def body(self) -> Optional[Decimal]: @property def upper_wick(self) -> Optional[Decimal]: # pylint: disable=no-member # C# interop properties - return to_pydecimal( + return to_pydecimal_via_double( self.High - (self.Open if self.Open > self.Close else self.Close) ) @property def lower_wick(self) -> Optional[Decimal]: # pylint: disable=no-member # C# interop properties - return to_pydecimal( + return to_pydecimal_via_double( (self.Close if self.Open > self.Close else self.Open) - self.Low ) @@ -82,7 +82,7 @@ class CandleResult(ResultBase): @property def price(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.Price) + return to_pydecimal_via_double(self._csdata.Price) @price.setter def price(self, value): diff --git a/stock_indicators/indicators/common/quote.py b/stock_indicators/indicators/common/quote.py index a3d29788..1ae6e5c2 100644 --- a/stock_indicators/indicators/common/quote.py +++ b/stock_indicators/indicators/common/quote.py @@ -6,7 +6,7 @@ from stock_indicators._cstypes import DateTime as CsDateTime from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydatetime, to_pydecimal +from stock_indicators._cstypes import to_pydatetime, to_pydecimal_via_double from stock_indicators.indicators.common._contrib.type_resolver import ( generate_cs_inherited_class, ) @@ -31,7 +31,7 @@ def _set_date(quote, value: datetime) -> None: def _get_open(quote) -> Optional[Decimal]: """Get the open property with proper null handling.""" - return to_pydecimal(quote.Open) + return to_pydecimal_via_double(quote.Open) def _set_open(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: @@ -44,7 +44,7 @@ def _set_open(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: def _get_high(quote) -> Optional[Decimal]: """Get the high property with proper null handling.""" - return to_pydecimal(quote.High) + return to_pydecimal_via_double(quote.High) def _set_high(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: @@ -55,7 +55,7 @@ def _set_high(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: def _get_low(quote) -> Optional[Decimal]: """Get the low property with proper null handling.""" - return to_pydecimal(quote.Low) + return to_pydecimal_via_double(quote.Low) def _set_low(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: @@ -66,7 +66,7 @@ def _set_low(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: def _get_close(quote) -> Optional[Decimal]: """Get the close property with proper null handling.""" - return to_pydecimal(quote.Close) + return to_pydecimal_via_double(quote.Close) def _set_close(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: @@ -77,7 +77,7 @@ def _set_close(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: def _get_volume(quote) -> Optional[Decimal]: """Get the volume property with proper null handling.""" - return to_pydecimal(quote.Volume) + return to_pydecimal_via_double(quote.Volume) def _set_volume(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: @@ -140,11 +140,11 @@ def from_csquote(cls, cs_quote: CsQuote) -> "Quote": return cls( date=to_pydatetime(cs_quote.Date), - open=to_pydecimal(cs_quote.Open), - high=to_pydecimal(cs_quote.High), - low=to_pydecimal(cs_quote.Low), - close=to_pydecimal(cs_quote.Close), - volume=to_pydecimal(cs_quote.Volume), + open=to_pydecimal_via_double(cs_quote.Open), + high=to_pydecimal_via_double(cs_quote.High), + low=to_pydecimal_via_double(cs_quote.Low), + close=to_pydecimal_via_double(cs_quote.Close), + volume=to_pydecimal_via_double(cs_quote.Volume), ) @classmethod diff --git a/stock_indicators/indicators/donchian.py b/stock_indicators/indicators/donchian.py index f033ac47..0e043fb9 100644 --- a/stock_indicators/indicators/donchian.py +++ b/stock_indicators/indicators/donchian.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin from stock_indicators.indicators.common.quote import Quote from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -41,7 +41,7 @@ class DonchianResult(ResultBase): @property def upper_band(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.UpperBand) + return to_pydecimal_via_double(self._csdata.UpperBand) @upper_band.setter def upper_band(self, value): @@ -49,7 +49,7 @@ def upper_band(self, value): @property def lower_band(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.LowerBand) + return to_pydecimal_via_double(self._csdata.LowerBand) @lower_band.setter def lower_band(self, value): @@ -57,7 +57,7 @@ def lower_band(self, value): @property def center_line(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.Centerline) + return to_pydecimal_via_double(self._csdata.Centerline) @center_line.setter def center_line(self, value): @@ -65,7 +65,7 @@ def center_line(self, value): @property def width(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.Width) + return to_pydecimal_via_double(self._csdata.Width) @width.setter def width(self, value): diff --git a/stock_indicators/indicators/fcb.py b/stock_indicators/indicators/fcb.py index ed29240a..fbaa7855 100644 --- a/stock_indicators/indicators/fcb.py +++ b/stock_indicators/indicators/fcb.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin from stock_indicators.indicators.common.quote import Quote from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -43,7 +43,7 @@ class FCBResult(ResultBase): @property def upper_band(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.UpperBand) + return to_pydecimal_via_double(self._csdata.UpperBand) @upper_band.setter def upper_band(self, value): @@ -51,7 +51,7 @@ def upper_band(self, value): @property def lower_band(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.LowerBand) + return to_pydecimal_via_double(self._csdata.LowerBand) @lower_band.setter def lower_band(self, value): diff --git a/stock_indicators/indicators/fractal.py b/stock_indicators/indicators/fractal.py index 7c676a58..43d14ee5 100644 --- a/stock_indicators/indicators/fractal.py +++ b/stock_indicators/indicators/fractal.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.enums import EndType from stock_indicators.indicators.common.helpers import CondenseMixin from stock_indicators.indicators.common.quote import Quote @@ -14,13 +14,15 @@ @overload def get_fractal( quotes: Iterable[Quote], window_span: int = 2, end_type=EndType.HIGH_LOW -) -> "FractalResults[FractalResult]": ... +) -> "FractalResults[FractalResult]": + pass @overload def get_fractal( quotes: Iterable[Quote], left_span: int, right_span: int, end_type=EndType.HIGH_LOW -) -> "FractalResults[FractalResult]": ... +) -> "FractalResults[FractalResult]": + pass def get_fractal( @@ -76,7 +78,7 @@ class FractalResult(ResultBase): @property def fractal_bear(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.FractalBear) + return to_pydecimal_via_double(self._csdata.FractalBear) @fractal_bear.setter def fractal_bear(self, value): @@ -84,7 +86,7 @@ def fractal_bear(self, value): @property def fractal_bull(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.FractalBull) + return to_pydecimal_via_double(self._csdata.FractalBull) @fractal_bull.setter def fractal_bull(self, value): diff --git a/stock_indicators/indicators/heikin_ashi.py b/stock_indicators/indicators/heikin_ashi.py index fd56345b..cebf1dca 100644 --- a/stock_indicators/indicators/heikin_ashi.py +++ b/stock_indicators/indicators/heikin_ashi.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.quote import Quote from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -37,7 +37,7 @@ class HeikinAshiResult(ResultBase): @property def open(self) -> Decimal: - return to_pydecimal(self._csdata.Open) + return to_pydecimal_via_double(self._csdata.Open) @open.setter def open(self, value): @@ -45,7 +45,7 @@ def open(self, value): @property def high(self) -> Decimal: - return to_pydecimal(self._csdata.High) + return to_pydecimal_via_double(self._csdata.High) @high.setter def high(self, value): @@ -53,7 +53,7 @@ def high(self, value): @property def low(self) -> Decimal: - return to_pydecimal(self._csdata.Low) + return to_pydecimal_via_double(self._csdata.Low) @low.setter def low(self, value): @@ -61,7 +61,7 @@ def low(self, value): @property def close(self) -> Decimal: - return to_pydecimal(self._csdata.Close) + return to_pydecimal_via_double(self._csdata.Close) @close.setter def close(self, value): @@ -69,7 +69,7 @@ def close(self, value): @property def volume(self) -> Decimal: - return to_pydecimal(self._csdata.Volume) + return to_pydecimal_via_double(self._csdata.Volume) @volume.setter def volume(self, value): diff --git a/stock_indicators/indicators/ichimoku.py b/stock_indicators/indicators/ichimoku.py index dd5e7f26..8451aaef 100644 --- a/stock_indicators/indicators/ichimoku.py +++ b/stock_indicators/indicators/ichimoku.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.helpers import CondenseMixin from stock_indicators.indicators.common.quote import Quote from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -16,7 +16,8 @@ def get_ichimoku( tenkan_periods: int = 9, kijun_periods: int = 26, senkou_b_periods: int = 52, -) -> "IchimokuResults[IchimokuResult]": ... +) -> "IchimokuResults[IchimokuResult]": + pass @overload @@ -27,7 +28,8 @@ def get_ichimoku( senkou_b_periods: int, *, offset_periods: int, -) -> "IchimokuResults[IchimokuResult]": ... +) -> "IchimokuResults[IchimokuResult]": + pass @overload @@ -39,7 +41,8 @@ def get_ichimoku( *, senkou_offset: int, chikou_offset: int, -) -> "IchimokuResults[IchimokuResult]": ... +) -> "IchimokuResults[IchimokuResult]": + pass def get_ichimoku( @@ -119,7 +122,7 @@ class IchimokuResult(ResultBase): @property def tenkan_sen(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.TenkanSen) + return to_pydecimal_via_double(self._csdata.TenkanSen) @tenkan_sen.setter def tenkan_sen(self, value): @@ -127,7 +130,7 @@ def tenkan_sen(self, value): @property def kijun_sen(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.KijunSen) + return to_pydecimal_via_double(self._csdata.KijunSen) @kijun_sen.setter def kijun_sen(self, value): @@ -135,7 +138,7 @@ def kijun_sen(self, value): @property def senkou_span_a(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.SenkouSpanA) + return to_pydecimal_via_double(self._csdata.SenkouSpanA) @senkou_span_a.setter def senkou_span_a(self, value): @@ -143,7 +146,7 @@ def senkou_span_a(self, value): @property def senkou_span_b(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.SenkouSpanB) + return to_pydecimal_via_double(self._csdata.SenkouSpanB) @senkou_span_b.setter def senkou_span_b(self, value): @@ -151,7 +154,7 @@ def senkou_span_b(self, value): @property def chikou_span(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.ChikouSpan) + return to_pydecimal_via_double(self._csdata.ChikouSpan) @chikou_span.setter def chikou_span(self, value): diff --git a/stock_indicators/indicators/parabolic_sar.py b/stock_indicators/indicators/parabolic_sar.py index 2728be25..d5031fed 100644 --- a/stock_indicators/indicators/parabolic_sar.py +++ b/stock_indicators/indicators/parabolic_sar.py @@ -12,14 +12,20 @@ def get_parabolic_sar( quotes: Iterable[Quote], acceleration_step: float = 0.02, max_acceleration_factor: float = 0.2, -) -> "ParabolicSARResults[ParabolicSARResult]": ... +) -> "ParabolicSARResults[ParabolicSARResult]": + pass + + @overload def get_parabolic_sar( quotes: Iterable[Quote], acceleration_step: float, max_acceleration_factor: float, initial_factor: float, -) -> "ParabolicSARResults[ParabolicSARResult]": ... +) -> "ParabolicSARResults[ParabolicSARResult]": + pass + + def get_parabolic_sar( quotes, acceleration_step=None, max_acceleration_factor=None, initial_factor=None ): diff --git a/stock_indicators/indicators/pivot_points.py b/stock_indicators/indicators/pivot_points.py index 5537a182..957b353d 100644 --- a/stock_indicators/indicators/pivot_points.py +++ b/stock_indicators/indicators/pivot_points.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.enums import PeriodSize, PivotPointType from stock_indicators.indicators.common.helpers import RemoveWarmupMixin from stock_indicators.indicators.common.quote import Quote @@ -53,7 +53,7 @@ class PivotPointsResult(ResultBase): @property def r4(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.R4) + return to_pydecimal_via_double(self._csdata.R4) @r4.setter def r4(self, value): @@ -61,7 +61,7 @@ def r4(self, value): @property def r3(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.R3) + return to_pydecimal_via_double(self._csdata.R3) @r3.setter def r3(self, value): @@ -69,7 +69,7 @@ def r3(self, value): @property def r2(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.R2) + return to_pydecimal_via_double(self._csdata.R2) @r2.setter def r2(self, value): @@ -77,7 +77,7 @@ def r2(self, value): @property def r1(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.R1) + return to_pydecimal_via_double(self._csdata.R1) @r1.setter def r1(self, value): @@ -85,7 +85,7 @@ def r1(self, value): @property def pp(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.PP) + return to_pydecimal_via_double(self._csdata.PP) @pp.setter def pp(self, value): @@ -93,7 +93,7 @@ def pp(self, value): @property def s1(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.S1) + return to_pydecimal_via_double(self._csdata.S1) @s1.setter def s1(self, value): @@ -101,7 +101,7 @@ def s1(self, value): @property def s2(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.S2) + return to_pydecimal_via_double(self._csdata.S2) @s2.setter def s2(self, value): @@ -109,7 +109,7 @@ def s2(self, value): @property def s3(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.S3) + return to_pydecimal_via_double(self._csdata.S3) @s3.setter def s3(self, value): @@ -117,7 +117,7 @@ def s3(self, value): @property def s4(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.S4) + return to_pydecimal_via_double(self._csdata.S4) @s4.setter def s4(self, value): diff --git a/stock_indicators/indicators/pivots.py b/stock_indicators/indicators/pivots.py index afa55af4..1a4e7281 100644 --- a/stock_indicators/indicators/pivots.py +++ b/stock_indicators/indicators/pivots.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.enums import EndType, PivotTrend from stock_indicators.indicators.common.helpers import CondenseMixin from stock_indicators.indicators.common.quote import Quote @@ -65,7 +65,7 @@ class PivotsResult(ResultBase): @property def high_point(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.HighPoint) + return to_pydecimal_via_double(self._csdata.HighPoint) @high_point.setter def high_point(self, value): @@ -73,7 +73,7 @@ def high_point(self, value): @property def low_point(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.LowPoint) + return to_pydecimal_via_double(self._csdata.LowPoint) @low_point.setter def low_point(self, value): @@ -81,7 +81,7 @@ def low_point(self, value): @property def high_line(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.HighLine) + return to_pydecimal_via_double(self._csdata.HighLine) @high_line.setter def high_line(self, value): @@ -89,7 +89,7 @@ def high_line(self, value): @property def low_line(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.LowLine) + return to_pydecimal_via_double(self._csdata.LowLine) @low_line.setter def low_line(self, value): diff --git a/stock_indicators/indicators/renko.py b/stock_indicators/indicators/renko.py index 17dbc872..b1a80fd4 100644 --- a/stock_indicators/indicators/renko.py +++ b/stock_indicators/indicators/renko.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.enums import EndType from stock_indicators.indicators.common.quote import Quote from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -81,7 +81,7 @@ class RenkoResult(ResultBase): @property def open(self) -> Decimal: - return to_pydecimal(self._csdata.Open) + return to_pydecimal_via_double(self._csdata.Open) @open.setter def open(self, value): @@ -89,7 +89,7 @@ def open(self, value): @property def high(self) -> Decimal: - return to_pydecimal(self._csdata.High) + return to_pydecimal_via_double(self._csdata.High) @high.setter def high(self, value): @@ -97,7 +97,7 @@ def high(self, value): @property def low(self) -> Decimal: - return to_pydecimal(self._csdata.Low) + return to_pydecimal_via_double(self._csdata.Low) @low.setter def low(self, value): @@ -105,7 +105,7 @@ def low(self, value): @property def close(self) -> Decimal: - return to_pydecimal(self._csdata.Close) + return to_pydecimal_via_double(self._csdata.Close) @close.setter def close(self, value): @@ -113,7 +113,7 @@ def close(self, value): @property def volume(self) -> Decimal: - return to_pydecimal(self._csdata.Volume) + return to_pydecimal_via_double(self._csdata.Volume) @volume.setter def volume(self, value): diff --git a/stock_indicators/indicators/rolling_pivots.py b/stock_indicators/indicators/rolling_pivots.py index b25cea42..c382c70e 100644 --- a/stock_indicators/indicators/rolling_pivots.py +++ b/stock_indicators/indicators/rolling_pivots.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.enums import PivotPointType from stock_indicators.indicators.common.helpers import RemoveWarmupMixin from stock_indicators.indicators.common.quote import Quote @@ -56,7 +56,7 @@ class RollingPivotsResult(ResultBase): @property def r4(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.R4) + return to_pydecimal_via_double(self._csdata.R4) @r4.setter def r4(self, value): @@ -64,7 +64,7 @@ def r4(self, value): @property def r3(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.R3) + return to_pydecimal_via_double(self._csdata.R3) @r3.setter def r3(self, value): @@ -72,7 +72,7 @@ def r3(self, value): @property def r2(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.R2) + return to_pydecimal_via_double(self._csdata.R2) @r2.setter def r2(self, value): @@ -80,7 +80,7 @@ def r2(self, value): @property def r1(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.R1) + return to_pydecimal_via_double(self._csdata.R1) @r1.setter def r1(self, value): @@ -88,7 +88,7 @@ def r1(self, value): @property def pp(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.PP) + return to_pydecimal_via_double(self._csdata.PP) @pp.setter def pp(self, value): @@ -96,7 +96,7 @@ def pp(self, value): @property def s1(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.S1) + return to_pydecimal_via_double(self._csdata.S1) @s1.setter def s1(self, value): @@ -104,7 +104,7 @@ def s1(self, value): @property def s2(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.S2) + return to_pydecimal_via_double(self._csdata.S2) @s2.setter def s2(self, value): @@ -112,7 +112,7 @@ def s2(self, value): @property def s3(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.S3) + return to_pydecimal_via_double(self._csdata.S3) @s3.setter def s3(self, value): @@ -120,7 +120,7 @@ def s3(self, value): @property def s4(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.S4) + return to_pydecimal_via_double(self._csdata.S4) @s4.setter def s4(self, value): diff --git a/stock_indicators/indicators/slope.py b/stock_indicators/indicators/slope.py index 5b9708dd..356b6bb9 100644 --- a/stock_indicators/indicators/slope.py +++ b/stock_indicators/indicators/slope.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin from stock_indicators.indicators.common.quote import Quote from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -74,7 +74,7 @@ def r_squared(self, value): @property def line(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.Line) + return to_pydecimal_via_double(self._csdata.Line) @line.setter def line(self, value): diff --git a/stock_indicators/indicators/super_trend.py b/stock_indicators/indicators/super_trend.py index 6f16aa77..5ee60d11 100644 --- a/stock_indicators/indicators/super_trend.py +++ b/stock_indicators/indicators/super_trend.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin from stock_indicators.indicators.common.quote import Quote from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -50,7 +50,7 @@ class SuperTrendResult(ResultBase): @property def super_trend(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.SuperTrend) + return to_pydecimal_via_double(self._csdata.SuperTrend) @super_trend.setter def super_trend(self, value): @@ -58,7 +58,7 @@ def super_trend(self, value): @property def upper_band(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.UpperBand) + return to_pydecimal_via_double(self._csdata.UpperBand) @upper_band.setter def upper_band(self, value): @@ -66,7 +66,7 @@ def upper_band(self, value): @property def lower_band(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.LowerBand) + return to_pydecimal_via_double(self._csdata.LowerBand) @lower_band.setter def lower_band(self, value): diff --git a/stock_indicators/indicators/vwap.py b/stock_indicators/indicators/vwap.py index c74ef86d..62f3be60 100644 --- a/stock_indicators/indicators/vwap.py +++ b/stock_indicators/indicators/vwap.py @@ -12,7 +12,8 @@ @overload def get_vwap( quotes: Iterable[Quote], start: Optional[datetime] = None -) -> "VWAPResults[VWAPResult]": ... +) -> "VWAPResults[VWAPResult]": + pass @overload @@ -24,7 +25,8 @@ def get_vwap( day: int = 1, hour: int = 0, minute: int = 0, -) -> "VWAPResults[VWAPResult]": ... +) -> "VWAPResults[VWAPResult]": + pass def get_vwap( diff --git a/stock_indicators/indicators/zig_zag.py b/stock_indicators/indicators/zig_zag.py index bd6fcf44..32e12709 100644 --- a/stock_indicators/indicators/zig_zag.py +++ b/stock_indicators/indicators/zig_zag.py @@ -4,7 +4,7 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import List as CsList -from stock_indicators._cstypes import to_pydecimal +from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.enums import EndType from stock_indicators.indicators.common.helpers import CondenseMixin from stock_indicators.indicators.common.quote import Quote @@ -52,7 +52,7 @@ class ZigZagResult(ResultBase): @property def zig_zag(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.ZigZag) + return to_pydecimal_via_double(self._csdata.ZigZag) @zig_zag.setter def zig_zag(self, value): @@ -68,7 +68,7 @@ def point_type(self, value): @property def retrace_high(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.RetraceHigh) + return to_pydecimal_via_double(self._csdata.RetraceHigh) @retrace_high.setter def retrace_high(self, value): @@ -76,7 +76,7 @@ def retrace_high(self, value): @property def retrace_low(self) -> Optional[Decimal]: - return to_pydecimal(self._csdata.RetraceLow) + return to_pydecimal_via_double(self._csdata.RetraceLow) @retrace_low.setter def retrace_low(self, value): diff --git a/tests/common/test_cstype_conversion.py b/tests/common/test_cstype_conversion.py index fb23d473..1b81601d 100644 --- a/tests/common/test_cstype_conversion.py +++ b/tests/common/test_cstype_conversion.py @@ -5,7 +5,11 @@ from stock_indicators._cslib import CsCultureInfo from stock_indicators._cstypes import DateTime as CsDateTime from stock_indicators._cstypes import Decimal as CsDecimal -from stock_indicators._cstypes import to_pydatetime, to_pydecimal +from stock_indicators._cstypes import ( + to_pydatetime, + to_pydecimal, + to_pydecimal_via_double, +) class TestCsTypeConversion: @@ -89,3 +93,21 @@ def test_large_decimal_conversion(self): cs_decimal = CsDecimal(py_decimal) assert to_pydecimal(cs_decimal) == PyDecimal(str(py_decimal)) + + def test_alternative_decimal_conversion_via_double(self): + """Test the alternative double-based conversion method.""" + py_decimal = 1996.1012 + cs_decimal = CsDecimal(py_decimal) + + # Test that the function works (though precision may differ) + result = to_pydecimal_via_double(cs_decimal) + assert result is not None + assert isinstance(result, PyDecimal) + + # The result should be close to the original, even if not exact + assert abs(float(result) - py_decimal) < 1e-10 + + def test_alternative_decimal_conversion_with_none(self): + """Test that the alternative method handles None correctly.""" + result = to_pydecimal_via_double(None) + assert result is None diff --git a/tests/common/test_decimal_conversion_comparison.py b/tests/common/test_decimal_conversion_comparison.py new file mode 100644 index 00000000..ff1a8bc1 --- /dev/null +++ b/tests/common/test_decimal_conversion_comparison.py @@ -0,0 +1,166 @@ +"""Tests comparing precision and performance of different decimal conversion methods.""" + +from decimal import Decimal as PyDecimal + +from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes.decimal import to_pydecimal, to_pydecimal_via_double + + +class TestDecimalConversionComparison: + """Test precision differences between string and double conversion methods.""" + + def test_basic_decimal_conversion_comparison(self): + """Test basic decimal values for precision differences.""" + test_values = [ + 1996.1012, + 123.456789, + 0.123456789, + 999999.999999, + 0.000001, + 1000000.0, + ] + + for py_decimal in test_values: + cs_decimal = CsDecimal(py_decimal) + + string_result = to_pydecimal(cs_decimal) + double_result = to_pydecimal_via_double(cs_decimal) + + assert string_result is not None + assert double_result is not None + assert abs(string_result - double_result) < PyDecimal("0.0001"), ( + f"Excessive precision difference for {py_decimal}: " + f"string={string_result}, double={double_result}" + ) + + # Document precision loss, if any + if string_result != double_result: + print(f"Precision difference for {py_decimal}:") + print(f" String method: {string_result}") + print(f" Double method: {double_result}") + print(f" Difference: {abs(string_result - double_result)}") + + def test_exponential_notation_conversion_comparison(self): + """Test exponential notation values for precision differences.""" + test_values = [ + 1.8e-05, + 1.234e10, + 5.6789e-15, + 9.999e20, + ] + + for py_decimal in test_values: + cs_decimal = CsDecimal(py_decimal) + + string_result = to_pydecimal(cs_decimal) + double_result = to_pydecimal_via_double(cs_decimal) + + print(f"Testing {py_decimal} (exponential notation):") + print(f" String method: {string_result}") + print(f" Double method: {double_result}") + + assert string_result is not None + assert double_result is not None + + # For exponential notation, we expect the string method to be more precise + if string_result != double_result: + print(f" Precision loss: {abs(string_result - double_result)}") + + def test_large_decimal_conversion_comparison(self): + """Test large decimal values for precision differences.""" + test_values = [ + 12345678901234567890.123456789, + 999999999999999999.999999999, + 123456789012345.123456789, + ] + + for py_decimal in test_values: + cs_decimal = CsDecimal(py_decimal) + + string_result = to_pydecimal(cs_decimal) + double_result = to_pydecimal_via_double(cs_decimal) + + print(f"Testing large decimal {py_decimal}:") + print(f" String method: {string_result}") + print(f" Double method: {double_result}") + + assert string_result is not None + assert double_result is not None + + # Large decimals are where we expect the most precision loss + if string_result != double_result: + precision_loss = abs(string_result - double_result) + relative_error = ( + precision_loss / abs(string_result) if string_result != 0 else 0 + ) + print(f" Absolute precision loss: {precision_loss}") + print(f" Relative error: {relative_error:.2e}") + + def test_high_precision_decimal_conversion_comparison(self): + """Test high precision decimal values.""" + test_values = [ + PyDecimal("3.141592653589793238462643383279502884197"), + PyDecimal("2.718281828459045235360287471352662497757"), + PyDecimal("1.414213562373095048801688724209698078569"), + PyDecimal("0.123456789012345678901234567890123456789"), + ] + + for py_decimal in test_values: + cs_decimal = CsDecimal(str(py_decimal)) + + string_result = to_pydecimal(cs_decimal) + double_result = to_pydecimal_via_double(cs_decimal) + + print(f"Testing high precision {py_decimal}:") + print(f" Original: {py_decimal}") + print(f" String method: {string_result}") + print(f" Double method: {double_result}") + + assert string_result is not None + assert double_result is not None + + # Compare precision loss + string_loss = abs(py_decimal - string_result) + double_loss = abs(py_decimal - double_result) + + print(f" String precision loss: {string_loss}") + print(f" Double precision loss: {double_loss}") + + def test_edge_cases_conversion_comparison(self): + """Test edge cases like very small and very large numbers.""" + test_values = [ + 1e-28, # Very small + 1e28, # Very large + 0.0, # Zero + -123.456, # Negative + float("inf"), + ] + + for py_decimal in test_values: + try: + cs_decimal = CsDecimal(py_decimal) + + string_result = to_pydecimal(cs_decimal) + double_result = to_pydecimal_via_double(cs_decimal) + + print(f"Testing edge case {py_decimal}:") + print(f" String method: {string_result}") + print(f" Double method: {double_result}") + + if string_result != double_result: + print(f" Difference: {abs(string_result - double_result)}") + + if string_result is not None and double_result is not None: + assert string_result is not None + assert double_result is not None + + except Exception as e: + print(f"Error testing {py_decimal}: {e}") + + def test_none_input_handling(self): + """Test that both methods handle None input correctly.""" + string_result = to_pydecimal(None) + double_result = to_pydecimal_via_double(None) + + assert string_result is None + assert double_result is None diff --git a/tests/test_pivot_points.py b/tests/test_pivot_points.py index 47339b2e..e74b1f84 100644 --- a/tests/test_pivot_points.py +++ b/tests/test_pivot_points.py @@ -137,7 +137,7 @@ def test_camarilla(self, quotes): assert 239.9765 == round(float(r.s2), 4) assert 238.3898 == float(round(r.s3, 4)) assert 233.6295 == round(float(r.s4), 4) - assert 244.7368 == round(float(r.r1), 4) + assert 244.7367 == round(float(r.r1), 4) assert 246.3235 == round(float(r.r2), 4) assert 247.91025 == round(float(r.r3), 5) assert 252.6705 == round(float(r.r4), 4)