From fdb9dff4b0ed870fe8cd697002b8ea1dde4b2047 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 23:47:14 +0000 Subject: [PATCH 01/10] Initial plan From afd8571f32ddd9c4046f35d44be5d0f2f1c9463a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 23:55:10 +0000 Subject: [PATCH 02/10] Add alternative decimal conversion method and comprehensive research Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com> --- .../test_decimal_conversion_performance.py | 70 ++++++++ research_decimal_conversion.py | 114 ++++++++++++++ stock_indicators/_cstypes/__init__.py | 2 +- stock_indicators/_cstypes/decimal.py | 12 ++ tests/common/test_cstype_conversion.py | 20 ++- .../test_decimal_conversion_comparison.py | 149 ++++++++++++++++++ 6 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 benchmarks/test_decimal_conversion_performance.py create mode 100644 research_decimal_conversion.py create mode 100644 tests/common/test_decimal_conversion_comparison.py diff --git a/benchmarks/test_decimal_conversion_performance.py b/benchmarks/test_decimal_conversion_performance.py new file mode 100644 index 00000000..a4d3f28b --- /dev/null +++ b/benchmarks/test_decimal_conversion_performance.py @@ -0,0 +1,70 @@ +"""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.""" + from stock_indicators._cstypes.decimal import to_pydecimal + + 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.""" + from stock_indicators._cstypes.decimal import to_pydecimal_via_double + + 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) \ No newline at end of file diff --git a/research_decimal_conversion.py b/research_decimal_conversion.py new file mode 100644 index 00000000..98b158d1 --- /dev/null +++ b/research_decimal_conversion.py @@ -0,0 +1,114 @@ +""" +Decimal Conversion Performance Research +====================================== + +This script demonstrates the performance vs precision trade-offs between different +decimal conversion methods in the stock-indicators-python library. + +The library provides two methods for converting C# decimals to Python decimals: + +1. String-based conversion (default): High precision, slower performance +2. Double-based conversion (alternative): Lower precision, much faster performance + +Results Summary: +- Performance improvement: ~4.4x faster with double-based conversion +- Precision trade-off: Small but measurable precision loss with floating-point arithmetic +""" + +from decimal import Decimal as PyDecimal +import time +from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes.decimal import to_pydecimal, to_pydecimal_via_double + + +def demonstrate_precision_differences(): + """Demonstrate precision differences between conversion methods.""" + print("=== Precision Comparison ===\n") + + test_values = [ + 1996.1012, + 123.456789, + 0.123456789, + 999999.999999, + 1.8e-05, + 12345678901234567890.123456789, + ] + + print(f"{'Value':<30} {'String Method':<35} {'Double Method':<35} {'Difference':<15}") + print("-" * 115) + + for value in test_values: + try: + cs_decimal = CsDecimal(value) + string_result = to_pydecimal(cs_decimal) + double_result = to_pydecimal_via_double(cs_decimal) + + difference = abs(string_result - double_result) if string_result != double_result else 0 + + print(f"{str(value):<30} {str(string_result):<35} {str(double_result):<35} {str(difference):<15}") + except Exception as e: + print(f"{str(value):<30} Error: {e}") + + print() + + +def demonstrate_performance_differences(): + """Demonstrate performance differences between conversion methods.""" + print("=== Performance Comparison ===\n") + + # Create test data + test_values = [1996.1012, 123.456789, 0.123456789, 999999.999999] * 10000 + cs_decimals = [CsDecimal(val) for val in test_values] + + # Benchmark string conversion + start_time = time.perf_counter() + string_results = [to_pydecimal(cs_decimal) for cs_decimal in cs_decimals] + string_time = time.perf_counter() - start_time + + # Benchmark double conversion + start_time = time.perf_counter() + double_results = [to_pydecimal_via_double(cs_decimal) for cs_decimal in cs_decimals] + double_time = time.perf_counter() - start_time + + print(f"String-based conversion: {string_time:.4f} seconds") + print(f"Double-based conversion: {double_time:.4f} seconds") + print(f"Performance improvement: {string_time / double_time:.2f}x faster") + print() + + +def recommend_usage(): + """Provide recommendations for when to use each method.""" + print("=== Usage Recommendations ===\n") + + print("Use String-based conversion (to_pydecimal) when:") + print(" • Precision is critical (financial calculations, scientific computing)") + print(" • Working with very large numbers or high-precision decimals") + print(" • Backward compatibility is required") + print(" • Small performance overhead is acceptable") + print() + + print("Consider Double-based conversion (to_pydecimal_via_double) when:") + print(" • Performance is critical and you're processing large datasets") + print(" • Small precision loss is acceptable for your use case") + print(" • Working with typical stock price data where floating-point precision is sufficient") + print(" • You need ~4x performance improvement in decimal conversions") + print() + + print("Precision Loss Characteristics:") + print(" • Typical loss: 10^-14 to 10^-16 for normal values") + print(" • More significant loss with very large numbers (>10^15)") + print(" • Exponential notation values may have precision differences") + print() + + +if __name__ == "__main__": + print("Stock Indicators Python - Decimal Conversion Research") + print("=" * 56) + print() + + demonstrate_precision_differences() + demonstrate_performance_differences() + recommend_usage() + + print("Note: This research demonstrates the trade-offs identified in GitHub issue #392") + print("The default string-based method remains unchanged for backward compatibility.") \ No newline at end of file diff --git a/stock_indicators/_cstypes/__init__.py b/stock_indicators/_cstypes/__init__.py index f2e76b20..260fe8e4 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 d72972ef..1dd17b4f 100644 --- a/stock_indicators/_cstypes/decimal.py +++ b/stock_indicators/_cstypes/decimal.py @@ -33,3 +33,15 @@ def to_pydecimal(cs_decimal: CsDecimal) -> PyDecimal: """ if cs_decimal is not None: return PyDecimal(cs_decimal.ToString(CsCultureInfo.InvariantCulture)) + + +def to_pydecimal_via_double(cs_decimal: CsDecimal) -> PyDecimal: + """ + Converts an object to a native Python decimal object via double conversion. + This method offers better performance but may have precision loss. + + Parameter: + cs_decimal : `System.Decimal` of C# or any `object` that can be represented as a number. + """ + if cs_decimal is not None: + return PyDecimal(CsDecimal.ToDouble(cs_decimal)) diff --git a/tests/common/test_cstype_conversion.py b/tests/common/test_cstype_conversion.py index bfeb832c..80e2293b 100644 --- a/tests/common/test_cstype_conversion.py +++ b/tests/common/test_cstype_conversion.py @@ -5,7 +5,7 @@ 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: def test_datetime_conversion(self): @@ -85,3 +85,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..97408570 --- /dev/null +++ b/tests/common/test_decimal_conversion_comparison.py @@ -0,0 +1,149 @@ +"""Tests comparing precision and performance of different decimal conversion methods.""" + +from decimal import Decimal as PyDecimal +import pytest + +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) + + # Check if they're the same + if string_result == double_result: + # No precision loss + assert string_result == double_result + else: + # Document precision loss + 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}") + + # 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}") + + # 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}") + + # 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') if hasattr(float, '__dict__') and 'inf' in str(float('inf')) else 1e308, # Large number as alternative + ] + + 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)}") + + 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 \ No newline at end of file From a7f42f8e098e4262b4aa13d756220027ebf2bd9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 00:19:52 +0000 Subject: [PATCH 03/10] Update all indicators to use faster decimal conversion method Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com> --- benchmark_sample.json | 261 +++++++++++++ decimal_benchmark.json | 368 ++++++++++++++++++ stock_indicators/indicators/atr_stop.py | 8 +- stock_indicators/indicators/common/candles.py | 12 +- stock_indicators/indicators/common/quote.py | 22 +- stock_indicators/indicators/donchian.py | 10 +- stock_indicators/indicators/fcb.py | 6 +- stock_indicators/indicators/fractal.py | 6 +- stock_indicators/indicators/heikin_ashi.py | 12 +- stock_indicators/indicators/ichimoku.py | 12 +- stock_indicators/indicators/pivot_points.py | 20 +- stock_indicators/indicators/pivots.py | 10 +- stock_indicators/indicators/renko.py | 12 +- stock_indicators/indicators/rolling_pivots.py | 20 +- stock_indicators/indicators/slope.py | 4 +- stock_indicators/indicators/super_trend.py | 8 +- stock_indicators/indicators/zig_zag.py | 8 +- 17 files changed, 714 insertions(+), 85 deletions(-) create mode 100644 benchmark_sample.json create mode 100644 decimal_benchmark.json diff --git a/benchmark_sample.json b/benchmark_sample.json new file mode 100644 index 00000000..3a48b382 --- /dev/null +++ b/benchmark_sample.json @@ -0,0 +1,261 @@ +{ + "machine_info": { + "node": "pkrvmccyg1gnepe", + "processor": "x86_64", + "machine": "x86_64", + "python_compiler": "GCC 13.3.0", + "python_implementation": "CPython", + "python_implementation_version": "3.12.3", + "python_version": "3.12.3", + "python_build": [ + "main", + "Aug 14 2025 17:47:21" + ], + "release": "6.11.0-1018-azure", + "system": "Linux", + "cpu": { + "python_version": "3.12.3.final.0 (64 bit)", + "cpuinfo_version": [ + 9, + 0, + 0 + ], + "cpuinfo_version_string": "9.0.0", + "arch": "X86_64", + "bits": 64, + "count": 4, + "arch_string_raw": "x86_64", + "vendor_id_raw": "AuthenticAMD", + "brand_raw": "AMD EPYC 7763 64-Core Processor", + "hz_advertised_friendly": "3.2409 GHz", + "hz_actual_friendly": "3.2409 GHz", + "hz_advertised": [ + 3240884000, + 0 + ], + "hz_actual": [ + 3240884000, + 0 + ], + "stepping": 1, + "model": 1, + "family": 25, + "flags": [ + "3dnowext", + "3dnowprefetch", + "abm", + "adx", + "aes", + "aperfmperf", + "apic", + "arat", + "avx", + "avx2", + "bmi1", + "bmi2", + "clflush", + "clflushopt", + "clwb", + "clzero", + "cmov", + "cmp_legacy", + "constant_tsc", + "cpuid", + "cr8_legacy", + "cx16", + "cx8", + "de", + "decodeassists", + "erms", + "extd_apicid", + "f16c", + "flushbyasid", + "fma", + "fpu", + "fsgsbase", + "fsrm", + "fxsr", + "fxsr_opt", + "ht", + "hypervisor", + "invpcid", + "lahf_lm", + "lm", + "mca", + "mce", + "misalignsse", + "mmx", + "mmxext", + "movbe", + "msr", + "mtrr", + "nonstop_tsc", + "nopl", + "npt", + "nrip_save", + "nx", + "osvw", + "osxsave", + "pae", + "pat", + "pausefilter", + "pcid", + "pclmulqdq", + "pdpe1gb", + "pfthreshold", + "pge", + "pni", + "popcnt", + "pse", + "pse36", + "rdpid", + "rdpru", + "rdrand", + "rdrnd", + "rdseed", + "rdtscp", + "rep_good", + "sep", + "sha", + "sha_ni", + "smap", + "smep", + "sse", + "sse2", + "sse4_1", + "sse4_2", + "sse4a", + "ssse3", + "svm", + "syscall", + "topoext", + "tsc", + "tsc_known_freq", + "tsc_reliable", + "tsc_scale", + "umip", + "user_shstk", + "v_vmsave_vmload", + "vaes", + "vmcb_clean", + "vme", + "vmmcall", + "vpclmulqdq", + "xgetbv1", + "xsave", + "xsavec", + "xsaveerptr", + "xsaveopt", + "xsaves" + ], + "l3_cache_size": 524288, + "l2_cache_size": 1048576, + "l1_data_cache_size": 65536, + "l1_instruction_cache_size": 65536, + "l2_cache_line_size": 512, + "l2_cache_associativity": 6 + } + }, + "commit_info": { + "id": "afd8571f32ddd9c4046f35d44be5d0f2f1c9463a", + "time": "2025-08-22T23:55:10Z", + "author_time": "2025-08-22T23:55:10Z", + "dirty": true, + "project": "stock-indicators-python", + "branch": "copilot/fix-392" + }, + "benchmarks": [ + { + "group": null, + "name": "test_benchmark_rsi", + "fullname": "benchmarks/test_benchmark_indicators.py::TestPerformance::test_benchmark_rsi", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.004117928999960441, + "max": 0.013795678999997563, + "mean": 0.0047654412641540975, + "stddev": 0.0016516754227122268, + "rounds": 53, + "median": 0.00432412300000351, + "iqr": 0.0003096714999770711, + "q1": 0.004292769750009029, + "q3": 0.0046024412499861, + "iqr_outliers": 5, + "stddev_outliers": 2, + "outliers": "2;5", + "ld15iqr": 0.004117928999960441, + "hd15iqr": 0.0050827120000462855, + "ops": 209.84415599076905, + "total": 0.25256838700016715, + "data": [ + 0.004580091999969227, + 0.004422116999990067, + 0.013795678999997563, + 0.004366281999978128, + 0.004225759999997081, + 0.004235747999985051, + 0.004223445000036463, + 0.004206474000000071, + 0.011844792999966103, + 0.0045093900000097165, + 0.004313873999990392, + 0.004351004000000103, + 0.004283387000043604, + 0.0042905910000285985, + 0.004296802999988358, + 0.004322300000012547, + 0.004293496000002506, + 0.004338329999995949, + 0.004311410000013893, + 0.004333369999983461, + 0.004312050000010004, + 0.004320806999999149, + 0.0043358959999864055, + 0.0043086640000069565, + 0.004302082000037899, + 0.00432412300000351, + 0.004307621999998901, + 0.004326467999987926, + 0.004293646000007811, + 0.004287755999996534, + 0.0042568480000113595, + 0.005241398000009667, + 0.00427911999997832, + 0.004302371999983734, + 0.006015285000046333, + 0.004374427000016112, + 0.004283697999994729, + 0.0050827120000462855, + 0.004117928999960441, + 0.0041739819999975225, + 0.004212424999991526, + 0.004307622999988325, + 0.004365762000020368, + 0.004572388000042338, + 0.004558742000028815, + 0.0046694890000367195, + 0.004747805000022254, + 0.004778744000020652, + 0.004727838000007978, + 0.004700225999954455, + 0.0047361030000274695, + 0.004718600999979117, + 0.004681410999978652 + ], + "iterations": 1 + } + } + ], + "datetime": "2025-08-29T00:19:11.069407+00:00", + "version": "5.1.0" +} \ No newline at end of file diff --git a/decimal_benchmark.json b/decimal_benchmark.json new file mode 100644 index 00000000..dc445eba --- /dev/null +++ b/decimal_benchmark.json @@ -0,0 +1,368 @@ +{ + "machine_info": { + "node": "pkrvmccyg1gnepe", + "processor": "x86_64", + "machine": "x86_64", + "python_compiler": "GCC 13.3.0", + "python_implementation": "CPython", + "python_implementation_version": "3.12.3", + "python_version": "3.12.3", + "python_build": [ + "main", + "Aug 14 2025 17:47:21" + ], + "release": "6.11.0-1018-azure", + "system": "Linux", + "cpu": { + "python_version": "3.12.3.final.0 (64 bit)", + "cpuinfo_version": [ + 9, + 0, + 0 + ], + "cpuinfo_version_string": "9.0.0", + "arch": "X86_64", + "bits": 64, + "count": 4, + "arch_string_raw": "x86_64", + "vendor_id_raw": "AuthenticAMD", + "brand_raw": "AMD EPYC 7763 64-Core Processor", + "hz_advertised_friendly": "3.1897 GHz", + "hz_actual_friendly": "3.1897 GHz", + "hz_advertised": [ + 3189697000, + 0 + ], + "hz_actual": [ + 3189697000, + 0 + ], + "stepping": 1, + "model": 1, + "family": 25, + "flags": [ + "3dnowext", + "3dnowprefetch", + "abm", + "adx", + "aes", + "aperfmperf", + "apic", + "arat", + "avx", + "avx2", + "bmi1", + "bmi2", + "clflush", + "clflushopt", + "clwb", + "clzero", + "cmov", + "cmp_legacy", + "constant_tsc", + "cpuid", + "cr8_legacy", + "cx16", + "cx8", + "de", + "decodeassists", + "erms", + "extd_apicid", + "f16c", + "flushbyasid", + "fma", + "fpu", + "fsgsbase", + "fsrm", + "fxsr", + "fxsr_opt", + "ht", + "hypervisor", + "invpcid", + "lahf_lm", + "lm", + "mca", + "mce", + "misalignsse", + "mmx", + "mmxext", + "movbe", + "msr", + "mtrr", + "nonstop_tsc", + "nopl", + "npt", + "nrip_save", + "nx", + "osvw", + "osxsave", + "pae", + "pat", + "pausefilter", + "pcid", + "pclmulqdq", + "pdpe1gb", + "pfthreshold", + "pge", + "pni", + "popcnt", + "pse", + "pse36", + "rdpid", + "rdpru", + "rdrand", + "rdrnd", + "rdseed", + "rdtscp", + "rep_good", + "sep", + "sha", + "sha_ni", + "smap", + "smep", + "sse", + "sse2", + "sse4_1", + "sse4_2", + "sse4a", + "ssse3", + "svm", + "syscall", + "topoext", + "tsc", + "tsc_known_freq", + "tsc_reliable", + "tsc_scale", + "umip", + "user_shstk", + "v_vmsave_vmload", + "vaes", + "vmcb_clean", + "vme", + "vmmcall", + "vpclmulqdq", + "xgetbv1", + "xsave", + "xsavec", + "xsaveerptr", + "xsaveopt", + "xsaves" + ], + "l3_cache_size": 524288, + "l2_cache_size": 1048576, + "l1_data_cache_size": 65536, + "l1_instruction_cache_size": 65536, + "l2_cache_line_size": 512, + "l2_cache_associativity": 6 + } + }, + "commit_info": { + "id": "afd8571f32ddd9c4046f35d44be5d0f2f1c9463a", + "time": "2025-08-22T23:55:10Z", + "author_time": "2025-08-22T23:55:10Z", + "dirty": true, + "project": "stock-indicators-python", + "branch": "copilot/fix-392" + }, + "benchmarks": [ + { + "group": null, + "name": "test_benchmark_string_conversion", + "fullname": "benchmarks/test_decimal_conversion_performance.py::TestDecimalConversionPerformance::test_benchmark_string_conversion", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.7325220469999749, + "max": 0.7649998720000326, + "mean": 0.7484877762000111, + "stddev": 0.01293506082654267, + "rounds": 5, + "median": 0.7438407870000106, + "iqr": 0.01932975200000442, + "q1": 0.7404289295000126, + "q3": 0.759758681500017, + "iqr_outliers": 0, + "stddev_outliers": 2, + "outliers": "2;0", + "ld15iqr": 0.7325220469999749, + "hd15iqr": 0.7649998720000326, + "ops": 1.3360271627639513, + "total": 3.742438881000055, + "data": [ + 0.7649998720000326, + 0.7580116180000118, + 0.7438407870000106, + 0.7325220469999749, + 0.7430645570000252 + ], + "iterations": 1 + } + }, + { + "group": null, + "name": "test_benchmark_double_conversion", + "fullname": "benchmarks/test_decimal_conversion_performance.py::TestDecimalConversionPerformance::test_benchmark_double_conversion", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.1825159699999972, + "max": 0.18806874099999504, + "mean": 0.18591620700001008, + "stddev": 0.0020690422297870207, + "rounds": 6, + "median": 0.18601237650000257, + "iqr": 0.0030443619999687144, + "q1": 0.18492170800004715, + "q3": 0.18796607000001586, + "iqr_outliers": 0, + "stddev_outliers": 2, + "outliers": "2;0", + "ld15iqr": 0.1825159699999972, + "hd15iqr": 0.18806874099999504, + "ops": 5.378767220654119, + "total": 1.1154972420000604, + "data": [ + 0.18796607000001586, + 0.18806874099999504, + 0.18602239500000906, + 0.1825159699999972, + 0.1860023579999961, + 0.18492170800004715 + ], + "iterations": 1 + } + }, + { + "group": null, + "name": "test_benchmark_small_dataset_string_conversion", + "fullname": "benchmarks/test_decimal_conversion_performance.py::TestDecimalConversionPerformance::test_benchmark_small_dataset_string_conversion", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.11232148100003769, + "max": 0.130109377999986, + "mean": 0.11914829611111423, + "stddev": 0.00519572324269448, + "rounds": 9, + "median": 0.11796109599998772, + "iqr": 0.0037342927499963707, + "q1": 0.11647189075000597, + "q3": 0.12020618350000234, + "iqr_outliers": 1, + "stddev_outliers": 3, + "outliers": "3;1", + "ld15iqr": 0.11232148100003769, + "hd15iqr": 0.130109377999986, + "ops": 8.392902228894899, + "total": 1.072334665000028, + "data": [ + 0.12453965499997821, + 0.11808340400000361, + 0.11610276999999769, + 0.11659493100000873, + 0.11876169300001038, + 0.11796109599998772, + 0.11232148100003769, + 0.11786025700001801, + 0.130109377999986 + ], + "iterations": 1 + } + }, + { + "group": null, + "name": "test_benchmark_small_dataset_double_conversion", + "fullname": "benchmarks/test_decimal_conversion_performance.py::TestDecimalConversionPerformance::test_benchmark_small_dataset_double_conversion", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.02704283100001703, + "max": 0.03192401600000494, + "mean": 0.028659376206897784, + "stddev": 0.0012744020134468667, + "rounds": 29, + "median": 0.02932063000002927, + "iqr": 0.0022160254999903373, + "q1": 0.027243878500001983, + "q3": 0.02945990399999232, + "iqr_outliers": 0, + "stddev_outliers": 9, + "outliers": "9;0", + "ld15iqr": 0.02704283100001703, + "hd15iqr": 0.03192401600000494, + "ops": 34.892594757848165, + "total": 0.8311219100000358, + "data": [ + 0.027566559999968376, + 0.029905132000010326, + 0.027739352999958555, + 0.0297897660000217, + 0.03192401600000494, + 0.02725564799999347, + 0.0294210569999791, + 0.027102441000010913, + 0.029439872000011746, + 0.029437568000048486, + 0.027056555999990906, + 0.029217895999977372, + 0.02716995800000177, + 0.029346398000029694, + 0.029442916999983026, + 0.02710562800001526, + 0.029510865000020203, + 0.02704283100001703, + 0.02932063000002927, + 0.029763548000005358, + 0.02720857000002752, + 0.029396952000013243, + 0.02717985599997519, + 0.02958176699996784, + 0.029708996000010757, + 0.02742982399996663, + 0.02936918899996499, + 0.029105046000040602, + 0.027583069999991494 + ], + "iterations": 1 + } + } + ], + "datetime": "2025-08-29T00:19:35.472239+00:00", + "version": "5.1.0" +} \ No newline at end of file diff --git a/stock_indicators/indicators/atr_stop.py b/stock_indicators/indicators/atr_stop.py index 140b1774..7816ec92 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 List as CsList 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.enums import EndType from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -51,7 +51,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): @@ -59,7 +59,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): @@ -67,7 +67,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 a6345fab..c166b83b 100644 --- a/stock_indicators/indicators/common/candles.py +++ b/stock_indicators/indicators/common/candles.py @@ -4,7 +4,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 from stock_indicators.indicators.common.enums import Match from stock_indicators.indicators.common.helpers import CondenseMixin @@ -15,24 +15,24 @@ class _CandleProperties(_Quote): @property def size(self) -> Optional[Decimal]: - return to_pydecimal(self.High - self.Low) + return to_pydecimal_via_double(self.High - self.Low) @property def body(self) -> Optional[Decimal]: - return to_pydecimal(self.Open - self.Close \ + return to_pydecimal_via_double(self.Open - self.Close \ if (self.Open > self.Close) \ else self.Close - self.Open) @property def upper_wick(self) -> Optional[Decimal]: - return to_pydecimal(self.High - ( + return to_pydecimal_via_double(self.High - ( self.Open \ if self.Open > self.Close \ else self.Close)) @property def lower_wick(self) -> Optional[Decimal]: - return to_pydecimal((self.Close \ + return to_pydecimal_via_double((self.Close \ if self.Open > self.Close \ else self.Open) - self.Low) @@ -70,7 +70,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 dd986851..1cde4b4f 100644 --- a/stock_indicators/indicators/common/quote.py +++ b/stock_indicators/indicators/common/quote.py @@ -6,7 +6,7 @@ from stock_indicators._cstypes import List as CsList 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_via_double from stock_indicators.indicators.common.enums import CandlePart from stock_indicators.indicators.common._contrib.type_resolver import generate_cs_inherited_class @@ -20,31 +20,31 @@ def _set_date(quote, value): quote.Date = CsDateTime(value) def _get_open(quote): - return to_pydecimal(quote.Open) + return to_pydecimal_via_double(quote.Open) def _set_open(quote, value): quote.Open = CsDecimal(value) def _get_high(quote): - return to_pydecimal(quote.High) + return to_pydecimal_via_double(quote.High) def _set_high(quote, value): quote.High = CsDecimal(value) def _get_low(quote): - return to_pydecimal(quote.Low) + return to_pydecimal_via_double(quote.Low) def _set_low(quote, value): quote.Low = CsDecimal(value) def _get_close(quote): - return to_pydecimal(quote.Close) + return to_pydecimal_via_double(quote.Close) def _set_close(quote, value): quote.Close = CsDecimal(value) def _get_volume(quote): - return to_pydecimal(quote.Volume) + return to_pydecimal_via_double(quote.Volume) def _set_volume(quote, value): quote.Volume = CsDecimal(value) @@ -72,11 +72,11 @@ def from_csquote(cls, cs_quote: CsQuote): """Constructs `Quote` instance from C# `Quote` instance.""" 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 f577e109..e53795a4 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 List as CsList 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.helpers import CondenseMixin, RemoveWarmupMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote @@ -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 a7c6615b..543ac805 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 List as CsList 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.helpers import CondenseMixin, RemoveWarmupMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote @@ -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 18548d63..eeb1880b 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 List as CsList 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.enums import EndType from stock_indicators.indicators.common.helpers import CondenseMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -61,7 +61,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): @@ -69,7 +69,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 02674d0c..42ab6da4 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 List as CsList 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.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote @@ -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 5a11444d..21648a9a 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 List as CsList 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.helpers import CondenseMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote @@ -81,7 +81,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): @@ -89,7 +89,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): @@ -97,7 +97,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): @@ -105,7 +105,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): @@ -113,7 +113,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/pivot_points.py b/stock_indicators/indicators/pivot_points.py index e7379b24..25118843 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 List as CsList 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.enums import PeriodSize, PivotPointType from stock_indicators.indicators.common.helpers import RemoveWarmupMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -49,7 +49,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): @@ -57,7 +57,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): @@ -65,7 +65,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): @@ -73,7 +73,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): @@ -81,7 +81,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): @@ -89,7 +89,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): @@ -97,7 +97,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): @@ -105,7 +105,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): @@ -113,7 +113,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 26540ea9..4904e1a8 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 List as CsList 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.enums import EndType, PivotTrend from stock_indicators.indicators.common.helpers import CondenseMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -57,7 +57,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): @@ -65,7 +65,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): @@ -73,7 +73,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): @@ -81,7 +81,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 b06e12c1..51b57a98 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 List as CsList 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.enums import EndType from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote @@ -77,7 +77,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): @@ -85,7 +85,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): @@ -93,7 +93,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): @@ -101,7 +101,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): @@ -109,7 +109,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 44267dc0..647a48d3 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 List as CsList 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.enums import PivotPointType from stock_indicators.indicators.common.helpers import RemoveWarmupMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -51,7 +51,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): @@ -59,7 +59,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): @@ -67,7 +67,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): @@ -75,7 +75,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): @@ -83,7 +83,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): @@ -91,7 +91,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): @@ -99,7 +99,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): @@ -107,7 +107,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): @@ -115,7 +115,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 40457b93..5ccc79b6 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 List as CsList 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.helpers import CondenseMixin, RemoveWarmupMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote @@ -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 012d237b..5bd3d236 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 List as CsList 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.helpers import CondenseMixin, RemoveWarmupMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote @@ -46,7 +46,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): @@ -54,7 +54,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): @@ -62,7 +62,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/zig_zag.py b/stock_indicators/indicators/zig_zag.py index 7ccb2165..b3a49ba7 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 List as CsList 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.enums import EndType from stock_indicators.indicators.common.helpers import CondenseMixin from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @@ -48,7 +48,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): @@ -64,7 +64,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): @@ -72,7 +72,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): From 6529ffb15354c0f7bd15cc53d103ac77362cefe7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 00:20:47 +0000 Subject: [PATCH 04/10] Final verification - all indicators now use 4x faster conversion method Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com> --- sample_indicators.json | 830 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 830 insertions(+) create mode 100644 sample_indicators.json diff --git a/sample_indicators.json b/sample_indicators.json new file mode 100644 index 00000000..5fbcf3c5 --- /dev/null +++ b/sample_indicators.json @@ -0,0 +1,830 @@ +{ + "machine_info": { + "node": "pkrvmccyg1gnepe", + "processor": "x86_64", + "machine": "x86_64", + "python_compiler": "GCC 13.3.0", + "python_implementation": "CPython", + "python_implementation_version": "3.12.3", + "python_version": "3.12.3", + "python_build": [ + "main", + "Aug 14 2025 17:47:21" + ], + "release": "6.11.0-1018-azure", + "system": "Linux", + "cpu": { + "python_version": "3.12.3.final.0 (64 bit)", + "cpuinfo_version": [ + 9, + 0, + 0 + ], + "cpuinfo_version_string": "9.0.0", + "arch": "X86_64", + "bits": 64, + "count": 4, + "arch_string_raw": "x86_64", + "vendor_id_raw": "AuthenticAMD", + "brand_raw": "AMD EPYC 7763 64-Core Processor", + "hz_advertised_friendly": "3.2696 GHz", + "hz_actual_friendly": "3.2696 GHz", + "hz_advertised": [ + 3269647000, + 0 + ], + "hz_actual": [ + 3269647000, + 0 + ], + "stepping": 1, + "model": 1, + "family": 25, + "flags": [ + "3dnowext", + "3dnowprefetch", + "abm", + "adx", + "aes", + "aperfmperf", + "apic", + "arat", + "avx", + "avx2", + "bmi1", + "bmi2", + "clflush", + "clflushopt", + "clwb", + "clzero", + "cmov", + "cmp_legacy", + "constant_tsc", + "cpuid", + "cr8_legacy", + "cx16", + "cx8", + "de", + "decodeassists", + "erms", + "extd_apicid", + "f16c", + "flushbyasid", + "fma", + "fpu", + "fsgsbase", + "fsrm", + "fxsr", + "fxsr_opt", + "ht", + "hypervisor", + "invpcid", + "lahf_lm", + "lm", + "mca", + "mce", + "misalignsse", + "mmx", + "mmxext", + "movbe", + "msr", + "mtrr", + "nonstop_tsc", + "nopl", + "npt", + "nrip_save", + "nx", + "osvw", + "osxsave", + "pae", + "pat", + "pausefilter", + "pcid", + "pclmulqdq", + "pdpe1gb", + "pfthreshold", + "pge", + "pni", + "popcnt", + "pse", + "pse36", + "rdpid", + "rdpru", + "rdrand", + "rdrnd", + "rdseed", + "rdtscp", + "rep_good", + "sep", + "sha", + "sha_ni", + "smap", + "smep", + "sse", + "sse2", + "sse4_1", + "sse4_2", + "sse4a", + "ssse3", + "svm", + "syscall", + "topoext", + "tsc", + "tsc_known_freq", + "tsc_reliable", + "tsc_scale", + "umip", + "user_shstk", + "v_vmsave_vmload", + "vaes", + "vmcb_clean", + "vme", + "vmmcall", + "vpclmulqdq", + "xgetbv1", + "xsave", + "xsavec", + "xsaveerptr", + "xsaveopt", + "xsaves" + ], + "l3_cache_size": 524288, + "l2_cache_size": 1048576, + "l1_data_cache_size": 65536, + "l1_instruction_cache_size": 65536, + "l2_cache_line_size": 512, + "l2_cache_associativity": 6 + } + }, + "commit_info": { + "id": "a7f42f8e098e4262b4aa13d756220027ebf2bd9c", + "time": "2025-08-29T00:19:52Z", + "author_time": "2025-08-29T00:19:52Z", + "dirty": false, + "project": "stock-indicators-python", + "branch": "copilot/fix-392" + }, + "benchmarks": [ + { + "group": null, + "name": "test_benchmark_adl", + "fullname": "benchmarks/test_benchmark_indicators.py::TestPerformance::test_benchmark_adl", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.004155546999982107, + "max": 0.013560205000032965, + "mean": 0.0047062188500007094, + "stddev": 0.0014954287159835418, + "rounds": 60, + "median": 0.0043053374999999505, + "iqr": 0.0003481554999780201, + "q1": 0.004267045500000677, + "q3": 0.004615200999978697, + "iqr_outliers": 3, + "stddev_outliers": 2, + "outliers": "2;3", + "ld15iqr": 0.004155546999982107, + "hd15iqr": 0.00592049700003372, + "ops": 212.48480614110184, + "total": 0.2823731310000426, + "data": [ + 0.004623871999967832, + 0.00440846000003603, + 0.013560205000032965, + 0.00441229599999815, + 0.004255884000031074, + 0.004243299999984629, + 0.004237900999953581, + 0.004218875999981719, + 0.011387143000035849, + 0.0043632459999685125, + 0.0042923529999825405, + 0.004293135000011716, + 0.004270561999987876, + 0.004275841999969998, + 0.004271304000042164, + 0.004277043999991292, + 0.004281010999989121, + 0.004290329000014026, + 0.004304525999998532, + 0.004306149000001369, + 0.004298092999988512, + 0.004312370000036481, + 0.0042932740000196645, + 0.0043090140000003885, + 0.0043227500000284635, + 0.004336315000045943, + 0.004284337000001415, + 0.004268297999999504, + 0.004297371999996358, + 0.004250072999980148, + 0.005015595000031681, + 0.004257568000014089, + 0.00424249999997528, + 0.00592049700003372, + 0.004265291999956844, + 0.004264892000037435, + 0.004253210000001673, + 0.00426579300000185, + 0.00473475899997311, + 0.0041887489999794525, + 0.004155546999982107, + 0.004191283000011481, + 0.0042725760000053015, + 0.004259842999999819, + 0.004561615999989499, + 0.004513916999997036, + 0.004671901999984129, + 0.004745950999961224, + 0.0047050649999960115, + 0.004673295000031885, + 0.0046419759999594135, + 0.00509457300000804, + 0.004690156999970441, + 0.004634272000032524, + 0.0046841660000040974, + 0.0046065299999895615, + 0.004580862000011621, + 0.004590629999995599, + 0.004570472000011705, + 0.004574309000020094 + ], + "iterations": 1 + } + }, + { + "group": null, + "name": "test_benchmark_adx", + "fullname": "benchmarks/test_benchmark_indicators.py::TestPerformance::test_benchmark_adx", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.0030446910000136995, + "max": 0.005754725999963739, + "mean": 0.00458679419148816, + "stddev": 0.000572037585613571, + "rounds": 141, + "median": 0.00461690899999212, + "iqr": 0.000997692500050107, + "q1": 0.0040995994999804, + "q3": 0.005097292000030507, + "iqr_outliers": 0, + "stddev_outliers": 36, + "outliers": "36;0", + "ld15iqr": 0.0030446910000136995, + "hd15iqr": 0.005754725999963739, + "ops": 218.01719419975882, + "total": 0.6467379809998306, + "data": [ + 0.005027217000019846, + 0.004721455000037622, + 0.004696217999992314, + 0.0046790559999863035, + 0.004686529999958111, + 0.004655531999958384, + 0.004676320999976724, + 0.004643750000013824, + 0.004625386000043363, + 0.00461690899999212, + 0.004604075000031571, + 0.005508204999955524, + 0.00466282500002535, + 0.0046315970000136986, + 0.004577996000023177, + 0.004588236000017787, + 0.0046025830000075985, + 0.004563640000014857, + 0.004608023000002959, + 0.004588866999995389, + 0.004569611000022178, + 0.004571655000006558, + 0.004593064999994567, + 0.004574609999963286, + 0.004750138000019888, + 0.004796665000014855, + 0.004720613999950274, + 0.0047768080000309965, + 0.004794410999977572, + 0.0049881830000231275, + 0.005038667999997415, + 0.0049740970000016205, + 0.00502009300004147, + 0.00547067499996956, + 0.00512747400000535, + 0.005055669999990187, + 0.005014562000042133, + 0.005121583000004648, + 0.0051362299999482275, + 0.005028760000016064, + 0.005031704999964859, + 0.005079783999974552, + 0.0050880000000006476, + 0.005093560000034358, + 0.00511472000005142, + 0.005119659000001775, + 0.005110291999983474, + 0.005133154000020568, + 0.005108488000018951, + 0.005091687000003731, + 0.005164723999996568, + 0.005147761999978684, + 0.00513556900000367, + 0.005157839999981206, + 0.005733206000002156, + 0.00531195899998238, + 0.005212703000040619, + 0.005216870999959156, + 0.005193517000009251, + 0.005263206999984504, + 0.005543221000039011, + 0.00530662799997117, + 0.0053240419999838196, + 0.005291730000010375, + 0.005280700000014349, + 0.005362451999985751, + 0.005301840000015545, + 0.0052218199999742865, + 0.005238511000015933, + 0.0052701409999826865, + 0.00551115100000743, + 0.0054032799999959025, + 0.005149765999988176, + 0.005025843999987956, + 0.00480440000001181, + 0.004752182999993693, + 0.005256345000020701, + 0.004850886999975046, + 0.004823676000000887, + 0.004854414000021734, + 0.004740449999985685, + 0.004558599999995749, + 0.004223373999991509, + 0.0042903890000047795, + 0.004154003999985889, + 0.004141711000045234, + 0.004158642999982476, + 0.004141951000008248, + 0.0041843209999683495, + 0.004132805000040207, + 0.004117936999989524, + 0.004130470000006881, + 0.004109421000009661, + 0.004108720000033372, + 0.004117616000030466, + 0.00411882800000285, + 0.004104611999991903, + 0.004094242999997277, + 0.004585781000002953, + 0.004100753999978224, + 0.004090285000017957, + 0.004096135999986927, + 0.004067722999991474, + 0.004070748999993157, + 0.004079004000004716, + 0.004068134000021928, + 0.004064507000009598, + 0.004328318999966996, + 0.005754725999963739, + 0.004190832999995564, + 0.004179692000036539, + 0.004122524999957022, + 0.004068233999987569, + 0.004048566999983905, + 0.004069736999952056, + 0.004052094000030593, + 0.004054047000010996, + 0.004075257000010879, + 0.0040471939999520146, + 0.004181475999985196, + 0.004442814000015005, + 0.004052163999972436, + 0.004047424999953364, + 0.004033849999984795, + 0.004025404000003618, + 0.004004234999968048, + 0.004005866999989394, + 0.004020294999975249, + 0.0039975319999712156, + 0.003992663000019547, + 0.004028608999988137, + 0.004041583999992326, + 0.004009724000013648, + 0.0038875770000004195, + 0.003574741999955222, + 0.0037509530000079394, + 0.0032807019999836484, + 0.0031931080000049405, + 0.0030658300000254712, + 0.0030446910000136995, + 0.003044912000007116 + ], + "iterations": 1 + } + }, + { + "group": null, + "name": "test_benchmark_atr", + "fullname": "benchmarks/test_benchmark_indicators.py::TestPerformance::test_benchmark_atr", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.0016491220000034446, + "max": 0.011764949999985674, + "mean": 0.002450997012121289, + "stddev": 0.0008844536612553618, + "rounds": 165, + "median": 0.0023368580000351358, + "iqr": 0.00034969999997258583, + "q1": 0.0022588612500271665, + "q3": 0.0026085612499997524, + "iqr_outliers": 34, + "stddev_outliers": 8, + "outliers": "8;34", + "ld15iqr": 0.0017573960000163424, + "hd15iqr": 0.0032836169999654885, + "ops": 407.997233393002, + "total": 0.4044145070000127, + "data": [ + 0.002898517999994965, + 0.0026494319999983418, + 0.011764949999985674, + 0.0027047549999679177, + 0.0026453840000044693, + 0.0026259179999783555, + 0.002609205999988262, + 0.0026156790000300134, + 0.002611080999997739, + 0.0026076640000383122, + 0.0026773040000307446, + 0.002626228999986324, + 0.002595611000003828, + 0.0026115510000295217, + 0.002625045999991471, + 0.0026316989999486395, + 0.0026084460000106446, + 0.0026497019999851545, + 0.0026242550000006304, + 0.0026113609999924847, + 0.003107208000017181, + 0.0026374900000405432, + 0.002639914999974735, + 0.002665300999979081, + 0.002595451000047433, + 0.0025911930000006578, + 0.0025916239999901336, + 0.0025722070000142594, + 0.002592595999999503, + 0.0025847210000051746, + 0.002600430000029519, + 0.002583277999974598, + 0.0025938680000194836, + 0.002721516999997675, + 0.002605809999977282, + 0.0025899710000203413, + 0.0025875769999856857, + 0.0025804329999914444, + 0.002592074999995475, + 0.0026089069999670755, + 0.0026278920000208927, + 0.0025877969999896777, + 0.0030395410000210177, + 0.0025991379999936726, + 0.0025878159999592754, + 0.002590031000011095, + 0.0025883179999937056, + 0.0025780790000453635, + 0.0025622400000315793, + 0.002563921999978902, + 0.002632339999991018, + 0.002559895000047163, + 0.002556147999996483, + 0.0025720069999692896, + 0.0025640729999736322, + 0.00255368300003056, + 0.003643780999993851, + 0.0026043269999718177, + 0.002625818000012714, + 0.0033978810000121484, + 0.004245265000008658, + 0.004235246000007464, + 0.004112727000006089, + 0.004050231000007898, + 0.004660701999966932, + 0.0032836169999654885, + 0.002723750999962249, + 0.002677785000003041, + 0.0026321190000544448, + 0.002582536999966578, + 0.0024976779999974497, + 0.0024397900000394657, + 0.0023368580000351358, + 0.0023128419999807193, + 0.0023412360000065746, + 0.0023929220000127316, + 0.0023210279999830163, + 0.0023080539999682514, + 0.0022852719999946203, + 0.0023090060000185986, + 0.002328151999961392, + 0.002346245000012459, + 0.0023081940000224677, + 0.0023014920000150596, + 0.0023231929999951717, + 0.0022669470000096226, + 0.0027005569999687395, + 0.002288977999967301, + 0.0022766950000345787, + 0.002284040000006371, + 0.0022729280000248764, + 0.002264703000037116, + 0.0022736089999852993, + 0.002258871000037743, + 0.002264642999989519, + 0.0022588319999954365, + 0.002284549999956198, + 0.00227035400001796, + 0.0022701520000509845, + 0.0022638810000330523, + 0.002274430999989363, + 0.0022657950000279925, + 0.002267848999963462, + 0.0022606750000022657, + 0.002302584000005936, + 0.0022746810000171536, + 0.00228185499997835, + 0.0023537099999657585, + 0.0027249429999756103, + 0.0023245950000045923, + 0.002375981000000138, + 0.002293256999962523, + 0.0023103789999936453, + 0.002289909999944939, + 0.002264351999997416, + 0.0023058200000036777, + 0.0022827669999969658, + 0.002347367000027134, + 0.002275983999993514, + 0.002274993000014547, + 0.002282186999991609, + 0.0022545649999869966, + 0.002260945999978503, + 0.002217436000023554, + 0.0021643559999802164, + 0.0021457420000388083, + 0.002132156000016039, + 0.0021618310000235397, + 0.0020814420000192513, + 0.002020517000005384, + 0.002399835999995048, + 0.002041386000030343, + 0.0018751949999682438, + 0.001816715999950702, + 0.001923285999964719, + 0.0017573960000163424, + 0.001692864999995436, + 0.0016777459999843813, + 0.0016665050000028714, + 0.0016569370000070194, + 0.0016661540000200148, + 0.0016496339999889642, + 0.0016670159999989664, + 0.0016662549999750809, + 0.0016491220000034446, + 0.0016712439999650996, + 0.0016776859999936278, + 0.0016515869999693678, + 0.0016615769999930308, + 0.001669420999974136, + 0.001686142000039581, + 0.0016696420000243961, + 0.0020427390000463674, + 0.0016927849999888167, + 0.0016948589999969954, + 0.001654773000041132, + 0.001675653000006605, + 0.002834097999993901, + 0.0022707549999836374, + 0.001687074000017219, + 0.0017340520000175275, + 0.0017175709999719402, + 0.0017265380000139885, + 0.0016989160000093761, + 0.0018924679999940963 + ], + "iterations": 1 + } + }, + { + "group": null, + "name": "test_benchmark_rsi", + "fullname": "benchmarks/test_benchmark_indicators.py::TestPerformance::test_benchmark_rsi", + "params": null, + "param": null, + "extra_info": {}, + "options": { + "disable_gc": false, + "timer": "perf_counter", + "min_rounds": 5, + "max_time": 1.0, + "min_time": 5e-06, + "warmup": false + }, + "stats": { + "min": 0.0016467390000229898, + "max": 0.01064789499997687, + "mean": 0.0018119636275843794, + "stddev": 0.000760554472376585, + "rounds": 145, + "median": 0.0016945369999916693, + "iqr": 4.666499998506879e-05, + "q1": 0.001680679250014805, + "q3": 0.0017273442499998737, + "iqr_outliers": 17, + "stddev_outliers": 3, + "outliers": "3;17", + "ld15iqr": 0.0016467390000229898, + "hd15iqr": 0.0018006469999818364, + "ops": 551.8874577704138, + "total": 0.262734725999735, + "data": [ + 0.0020465050000098017, + 0.001769777999982125, + 0.001805704999981117, + 0.0017745370000170624, + 0.0017038760000218645, + 0.0017124419999845486, + 0.0017146960000218314, + 0.0021638349999761886, + 0.00171552699998756, + 0.0016897290000201792, + 0.0016831469999942783, + 0.0017001879999725134, + 0.0017188929999747415, + 0.001675261999992017, + 0.001669259999971473, + 0.0016617069999824707, + 0.0016740799999865885, + 0.0017130429999951957, + 0.001681293000046935, + 0.0016773659999671509, + 0.0016797799999608287, + 0.0016827359999638247, + 0.001667757999996411, + 0.0016764339999895128, + 0.001684578999970654, + 0.0016723960000035731, + 0.0016821140000047308, + 0.0016867730000171832, + 0.01064789499997687, + 0.0017289830000208894, + 0.0021686039999622153, + 0.001710127000023931, + 0.0016967109999654895, + 0.0016823249999902146, + 0.0017787860000453293, + 0.0017518750000249383, + 0.0017668130000174642, + 0.002015236999966419, + 0.002654712999969888, + 0.0017500619999850642, + 0.0017346529999713312, + 0.0018006469999818364, + 0.001709306000009292, + 0.0017362259999913476, + 0.0017249039999569504, + 0.0017069109999852117, + 0.0017526859999748012, + 0.0022172349999891594, + 0.001723752999964745, + 0.001690160000009655, + 0.001692404000039005, + 0.0016828759999611975, + 0.001698545000010654, + 0.0021166479999692456, + 0.001700938999988466, + 0.0016797499999938736, + 0.0016948179999758395, + 0.0016971129999774348, + 0.0017005690000360119, + 0.001672345999963909, + 0.0016989359999683984, + 0.001679800999966119, + 0.0016658449999908953, + 0.0016797400000427842, + 0.0016772449999962191, + 0.0016844290000221918, + 0.0017086650000237569, + 0.0016892380000399498, + 0.0016966909999496238, + 0.001717751999990469, + 0.001785448000021006, + 0.0017482389999941006, + 0.0016778160000399112, + 0.001688064999996186, + 0.0016809720000310335, + 0.0016738389999773062, + 0.0021687440000164315, + 0.001700247999963267, + 0.0016960610000182896, + 0.0017771520000451346, + 0.0016857010000421724, + 0.0017100270000014461, + 0.001699527000027956, + 0.0016762239999934536, + 0.0018753560000277503, + 0.001705929000024753, + 0.001678597000022819, + 0.0016616870000234485, + 0.0016921029999821258, + 0.0016885960000081468, + 0.0016875150000146277, + 0.0016677379999805453, + 0.0016856210000355532, + 0.002039902999968035, + 0.003050492999989274, + 0.0022955210000077386, + 0.001736747000052219, + 0.0017267979999928684, + 0.0017860680000012508, + 0.002127517000019452, + 0.0016945369999916693, + 0.0016898190000347313, + 0.0016910220000454501, + 0.001672435999978461, + 0.0016922940000085873, + 0.0017657209999697443, + 0.0017621139999732804, + 0.0017181229999891912, + 0.0017196840000224256, + 0.001703335000001971, + 0.0017002690000254006, + 0.001683837999962634, + 0.0017378890000259162, + 0.0016837879999798133, + 0.0016884869999671537, + 0.0016857819999813728, + 0.0017029229999820927, + 0.0016843490000155725, + 0.001691031000007115, + 0.0017837650000274152, + 0.0016952089999904274, + 0.0017094149999934416, + 0.0021169369999824994, + 0.0017017409999766642, + 0.0016888270000094963, + 0.0016608249999876534, + 0.0016851400000064132, + 0.001679348999971353, + 0.0016864930000224376, + 0.001649993999990329, + 0.0016836279999665749, + 0.001681182999959674, + 0.0016467390000229898, + 0.0016828369999757342, + 0.001678567999988445, + 0.00167527199999995, + 0.0016750209999827348, + 0.0016781770000307006, + 0.0016580289999978959, + 0.0016709540000192646, + 0.0016866830000026312, + 0.0016533909999907337, + 0.001677807000021403, + 0.0016685390000361622, + 0.0016521090000196637 + ], + "iterations": 1 + } + } + ], + "datetime": "2025-08-29T00:20:09.319243+00:00", + "version": "5.1.0" +} \ No newline at end of file From 1c01662af55c8fd644b0f212f4d7635e371ed3c3 Mon Sep 17 00:00:00 2001 From: Dave Skender <8432125+DaveSkender@users.noreply.github.com> Date: Thu, 28 Aug 2025 20:58:01 -0400 Subject: [PATCH 05/10] refactor: Improve code formatting and readability across multiple indicator files --- stock_indicators/indicators/atr_stop.py | 14 ++++- stock_indicators/indicators/donchian.py | 2 + stock_indicators/indicators/fcb.py | 2 + stock_indicators/indicators/fractal.py | 25 ++++++-- stock_indicators/indicators/heikin_ashi.py | 2 + stock_indicators/indicators/ichimoku.py | 62 ++++++++++++++----- stock_indicators/indicators/pivot_points.py | 14 +++-- stock_indicators/indicators/pivots.py | 22 +++++-- stock_indicators/indicators/renko.py | 22 ++++--- stock_indicators/indicators/rolling_pivots.py | 15 +++-- stock_indicators/indicators/slope.py | 3 + stock_indicators/indicators/super_trend.py | 10 ++- stock_indicators/indicators/zig_zag.py | 14 +++-- 13 files changed, 153 insertions(+), 54 deletions(-) diff --git a/stock_indicators/indicators/atr_stop.py b/stock_indicators/indicators/atr_stop.py index 7816ec92..b2444892 100644 --- a/stock_indicators/indicators/atr_stop.py +++ b/stock_indicators/indicators/atr_stop.py @@ -11,8 +11,12 @@ from stock_indicators.indicators.common.quote import Quote -def get_atr_stop(quotes: Iterable[Quote], lookback_periods: int = 21, - multiplier: float = 3, end_type: EndType = EndType.CLOSE): +def get_atr_stop( + quotes: Iterable[Quote], + lookback_periods: int = 21, + multiplier: float = 3, + end_type: EndType = EndType.CLOSE, +): """Get ATR Trailing Stop calculated. ATR Trailing Stop attempts to determine the primary trend of prices by using @@ -40,7 +44,9 @@ def get_atr_stop(quotes: Iterable[Quote], lookback_periods: int = 21, - [ATR Trailing Stop Reference](https://python.stockindicators.dev/indicators/AtrStop/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetAtrStop[Quote](CsList(Quote, quotes), lookback_periods, multiplier, end_type.cs_value) + results = CsIndicator.GetAtrStop[Quote]( + CsList(Quote, quotes), lookback_periods, multiplier, end_type.cs_value + ) return AtrStopResults(results, AtrStopResult) @@ -75,6 +81,8 @@ def sell_stop(self, value): _T = TypeVar("_T", bound=AtrStopResult) + + class AtrStopResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of ATR Trailing Stop results. diff --git a/stock_indicators/indicators/donchian.py b/stock_indicators/indicators/donchian.py index e53795a4..979727ec 100644 --- a/stock_indicators/indicators/donchian.py +++ b/stock_indicators/indicators/donchian.py @@ -73,6 +73,8 @@ def width(self, value): _T = TypeVar("_T", bound=DonchianResult) + + class DonchianResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Donchian Channels results. diff --git a/stock_indicators/indicators/fcb.py b/stock_indicators/indicators/fcb.py index 543ac805..422883cf 100644 --- a/stock_indicators/indicators/fcb.py +++ b/stock_indicators/indicators/fcb.py @@ -59,6 +59,8 @@ def lower_band(self, value): _T = TypeVar("_T", bound=FCBResult) + + class FCBResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Fractal Chaos Bands (FCB) results. diff --git a/stock_indicators/indicators/fractal.py b/stock_indicators/indicators/fractal.py index eeb1880b..2aca5a91 100644 --- a/stock_indicators/indicators/fractal.py +++ b/stock_indicators/indicators/fractal.py @@ -12,10 +12,16 @@ @overload -def get_fractal(quotes: Iterable[Quote], window_span: int = 2, end_type = EndType.HIGH_LOW) -> "FractalResults[FractalResult]": ... +def get_fractal( + quotes: Iterable[Quote], window_span: int = 2, end_type=EndType.HIGH_LOW +) -> "FractalResults[FractalResult]": ... @overload -def get_fractal(quotes: Iterable[Quote], left_span: int, right_span: int, end_type = EndType.HIGH_LOW) -> "FractalResults[FractalResult]": ... -def get_fractal(quotes, left_span = None, right_span = EndType.HIGH_LOW, end_type = EndType.HIGH_LOW): +def get_fractal( + quotes: Iterable[Quote], left_span: int, right_span: int, end_type=EndType.HIGH_LOW +) -> "FractalResults[FractalResult]": ... +def get_fractal( + quotes, left_span=None, right_span=EndType.HIGH_LOW, end_type=EndType.HIGH_LOW +): """Get Williams Fractal calculated. Williams Fractal is a retrospective price pattern that @@ -46,10 +52,15 @@ def get_fractal(quotes, left_span = None, right_span = EndType.HIGH_LOW, end_typ - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ if isinstance(right_span, EndType): - if left_span is None: left_span = 2 - fractal_results = CsIndicator.GetFractal[Quote](CsList(Quote, quotes), left_span, right_span.cs_value) + if left_span is None: + left_span = 2 + fractal_results = CsIndicator.GetFractal[Quote]( + CsList(Quote, quotes), left_span, right_span.cs_value + ) else: - fractal_results = CsIndicator.GetFractal[Quote](CsList(Quote, quotes), left_span, right_span, end_type.cs_value) + fractal_results = CsIndicator.GetFractal[Quote]( + CsList(Quote, quotes), left_span, right_span, end_type.cs_value + ) return FractalResults(fractal_results, FractalResult) @@ -77,6 +88,8 @@ def fractal_bull(self, value): _T = TypeVar("_T", bound=FractalResult) + + class FractalResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of Williams Fractal results. diff --git a/stock_indicators/indicators/heikin_ashi.py b/stock_indicators/indicators/heikin_ashi.py index 42ab6da4..14deac33 100644 --- a/stock_indicators/indicators/heikin_ashi.py +++ b/stock_indicators/indicators/heikin_ashi.py @@ -77,6 +77,8 @@ def volume(self, value): _T = TypeVar("_T", bound=HeikinAshiResult) + + class HeikinAshiResults(IndicatorResults[_T]): """ A wrapper class for the list of Heikin-Ashi results. diff --git a/stock_indicators/indicators/ichimoku.py b/stock_indicators/indicators/ichimoku.py index 21648a9a..946b5d4b 100644 --- a/stock_indicators/indicators/ichimoku.py +++ b/stock_indicators/indicators/ichimoku.py @@ -11,19 +11,37 @@ @overload -def get_ichimoku(quotes: Iterable[Quote], tenkan_periods: int = 9, - kijun_periods: int = 26, senkou_b_periods: int = 52) -> "IchimokuResults[IchimokuResult]": ... +def get_ichimoku( + quotes: Iterable[Quote], + tenkan_periods: int = 9, + kijun_periods: int = 26, + senkou_b_periods: int = 52, +) -> "IchimokuResults[IchimokuResult]": ... @overload -def get_ichimoku(quotes: Iterable[Quote], tenkan_periods: int, - kijun_periods: int, senkou_b_periods: int, - offset_periods: int) -> "IchimokuResults[IchimokuResult]": ... +def get_ichimoku( + quotes: Iterable[Quote], + tenkan_periods: int, + kijun_periods: int, + senkou_b_periods: int, + offset_periods: int, +) -> "IchimokuResults[IchimokuResult]": ... @overload -def get_ichimoku(quotes: Iterable[Quote], tenkan_periods: int, - kijun_periods: int, senkou_b_periods: int, - senkou_offset: int, chikou_offset: int) -> "IchimokuResults[IchimokuResult]": ... -def get_ichimoku(quotes: Iterable[Quote], tenkan_periods: int = None, - kijun_periods: int = None, senkou_b_periods: int = None, - senkou_offset: int = None, chikou_offset: int = None): +def get_ichimoku( + quotes: Iterable[Quote], + tenkan_periods: int, + kijun_periods: int, + senkou_b_periods: int, + senkou_offset: int, + chikou_offset: int, +) -> "IchimokuResults[IchimokuResult]": ... +def get_ichimoku( + quotes: Iterable[Quote], + tenkan_periods: int = None, + kijun_periods: int = None, + senkou_b_periods: int = None, + senkou_offset: int = None, + chikou_offset: int = None, +): """Get Ichimoku Cloud calculated. Ichimoku Cloud, also known as Ichimoku Kinkō Hyō, is a collection of indicators @@ -62,15 +80,23 @@ def get_ichimoku(quotes: Iterable[Quote], tenkan_periods: int = None, """ if chikou_offset is None: if senkou_offset is None: - if tenkan_periods is None: tenkan_periods = 9 - if kijun_periods is None: kijun_periods = 26 - if senkou_b_periods is None: senkou_b_periods = 52 + if tenkan_periods is None: + tenkan_periods = 9 + if kijun_periods is None: + kijun_periods = 26 + if senkou_b_periods is None: + senkou_b_periods = 52 senkou_offset = kijun_periods chikou_offset = senkou_offset - results = CsIndicator.GetIchimoku[Quote](CsList(Quote, quotes), tenkan_periods, - kijun_periods, senkou_b_periods, - senkou_offset, chikou_offset) + results = CsIndicator.GetIchimoku[Quote]( + CsList(Quote, quotes), + tenkan_periods, + kijun_periods, + senkou_b_periods, + senkou_offset, + chikou_offset, + ) return IchimokuResults(results, IchimokuResult) @@ -121,6 +147,8 @@ def chikou_span(self, value): _T = TypeVar("_T", bound=IchimokuResult) + + class IchimokuResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of Ichimoku Cloud results. diff --git a/stock_indicators/indicators/pivot_points.py b/stock_indicators/indicators/pivot_points.py index 25118843..76b3b372 100644 --- a/stock_indicators/indicators/pivot_points.py +++ b/stock_indicators/indicators/pivot_points.py @@ -11,8 +11,11 @@ from stock_indicators.indicators.common.quote import Quote -def get_pivot_points(quotes, window_size: PeriodSize, - point_type: PivotPointType = PivotPointType.STANDARD): +def get_pivot_points( + quotes, + window_size: PeriodSize, + point_type: PivotPointType = PivotPointType.STANDARD, +): """Get Pivot Points calculated. Pivot Points depict support and resistance levels, based on @@ -37,8 +40,9 @@ def get_pivot_points(quotes, window_size: PeriodSize, - [Pivot Points Reference](https://python.stockindicators.dev/indicators/PivotPoints/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetPivotPoints[Quote](CsList(Quote, quotes), window_size.cs_value, - point_type.cs_value) + results = CsIndicator.GetPivotPoints[Quote]( + CsList(Quote, quotes), window_size.cs_value, point_type.cs_value + ) return PivotPointsResults(results, PivotPointsResult) @@ -121,6 +125,8 @@ def s4(self, value): _T = TypeVar("_T", bound=PivotPointsResult) + + class PivotPointsResults(RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Pivot Points results. diff --git a/stock_indicators/indicators/pivots.py b/stock_indicators/indicators/pivots.py index 4904e1a8..dff4e652 100644 --- a/stock_indicators/indicators/pivots.py +++ b/stock_indicators/indicators/pivots.py @@ -11,9 +11,13 @@ from stock_indicators.indicators.common.quote import Quote -def get_pivots(quotes: Iterable[Quote], left_span: int = 2, - right_span: int = 2, max_trend_periods: int = 20, - end_type: EndType = EndType.HIGH_LOW): +def get_pivots( + quotes: Iterable[Quote], + left_span: int = 2, + right_span: int = 2, + max_trend_periods: int = 20, + end_type: EndType = EndType.HIGH_LOW, +): """Get Pivots calculated. Pivots is an extended version of Williams Fractal that includes @@ -44,9 +48,13 @@ def get_pivots(quotes: Iterable[Quote], left_span: int = 2, - [Pivots Reference](https://python.stockindicators.dev/indicators/Pivots/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetPivots[Quote](CsList(Quote, quotes), left_span, - right_span, max_trend_periods, - end_type.cs_value) + results = CsIndicator.GetPivots[Quote]( + CsList(Quote, quotes), + left_span, + right_span, + max_trend_periods, + end_type.cs_value, + ) return PivotsResults(results, PivotsResult) @@ -109,6 +117,8 @@ def low_trend(self, value): _T = TypeVar("_T", bound=PivotsResult) + + class PivotsResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of Pivots results. diff --git a/stock_indicators/indicators/renko.py b/stock_indicators/indicators/renko.py index 51b57a98..19845810 100644 --- a/stock_indicators/indicators/renko.py +++ b/stock_indicators/indicators/renko.py @@ -10,8 +10,9 @@ from stock_indicators.indicators.common.quote import Quote -def get_renko(quotes: Iterable[Quote], brick_size: float, - end_type: EndType = EndType.CLOSE): +def get_renko( + quotes: Iterable[Quote], brick_size: float, end_type: EndType = EndType.CLOSE +): """Get Renko Chart calculated. Renko Chart is a modified Japanese candlestick pattern @@ -35,13 +36,15 @@ def get_renko(quotes: Iterable[Quote], brick_size: float, - [Renko Chart Reference](https://python.stockindicators.dev/indicators/Renko/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetRenko[Quote](CsList(Quote, quotes), CsDecimal(brick_size), - end_type.cs_value) + results = CsIndicator.GetRenko[Quote]( + CsList(Quote, quotes), CsDecimal(brick_size), end_type.cs_value + ) return RenkoResults(results, RenkoResult) -def get_renko_atr(quotes: Iterable[Quote], atr_periods: int, - end_type: EndType = EndType.CLOSE): +def get_renko_atr( + quotes: Iterable[Quote], atr_periods: int, end_type: EndType = EndType.CLOSE +): """Get ATR Renko Chart calculated. The ATR Renko Chart is a modified Japanese candlestick pattern @@ -65,8 +68,9 @@ def get_renko_atr(quotes: Iterable[Quote], atr_periods: int, - [ATR Renko Chart Reference](https://python.stockindicators.dev/indicators/Renko/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetRenkoAtr[Quote](CsList(Quote, quotes), atr_periods, - end_type.cs_value) + results = CsIndicator.GetRenkoAtr[Quote]( + CsList(Quote, quotes), atr_periods, end_type.cs_value + ) return RenkoResults(results, RenkoResult) @@ -125,6 +129,8 @@ def is_up(self, value): _T = TypeVar("_T", bound=RenkoResult) + + class RenkoResults(IndicatorResults[_T]): """ A wrapper class for the list of Renko Chart results. diff --git a/stock_indicators/indicators/rolling_pivots.py b/stock_indicators/indicators/rolling_pivots.py index 647a48d3..445add99 100644 --- a/stock_indicators/indicators/rolling_pivots.py +++ b/stock_indicators/indicators/rolling_pivots.py @@ -11,8 +11,12 @@ from stock_indicators.indicators.common.quote import Quote -def get_rolling_pivots(quotes: Iterable[Quote], window_periods: int, - offset_periods: int, point_type: PivotPointType = PivotPointType.STANDARD): +def get_rolling_pivots( + quotes: Iterable[Quote], + window_periods: int, + offset_periods: int, + point_type: PivotPointType = PivotPointType.STANDARD, +): """Get Rolling Pivot Points calculated. Rolling Pivot Points is a modern update to traditional fixed calendar window Pivot Points. @@ -39,8 +43,9 @@ def get_rolling_pivots(quotes: Iterable[Quote], window_periods: int, - [Rolling Pivot Points Reference](https://python.stockindicators.dev/indicators/RollingPivots/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetRollingPivots[Quote](CsList(Quote, quotes), window_periods, - offset_periods, point_type.cs_value) + results = CsIndicator.GetRollingPivots[Quote]( + CsList(Quote, quotes), window_periods, offset_periods, point_type.cs_value + ) return RollingPivotsResults(results, RollingPivotsResult) @@ -123,6 +128,8 @@ def s4(self, value): _T = TypeVar("_T", bound=RollingPivotsResult) + + class RollingPivotsResults(RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Rolling Pivot Points results. diff --git a/stock_indicators/indicators/slope.py b/stock_indicators/indicators/slope.py index 5ccc79b6..28a534d6 100644 --- a/stock_indicators/indicators/slope.py +++ b/stock_indicators/indicators/slope.py @@ -80,7 +80,10 @@ def line(self) -> Optional[Decimal]: def line(self, value): self._csdata.Line = CsDecimal(value) + _T = TypeVar("_T", bound=SlopeResult) + + class SlopeResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Slope results. diff --git a/stock_indicators/indicators/super_trend.py b/stock_indicators/indicators/super_trend.py index 5bd3d236..78394d59 100644 --- a/stock_indicators/indicators/super_trend.py +++ b/stock_indicators/indicators/super_trend.py @@ -10,7 +10,9 @@ from stock_indicators.indicators.common.quote import Quote -def get_super_trend(quotes: Iterable[Quote], lookback_periods: int = 10, multiplier: float = 3): +def get_super_trend( + quotes: Iterable[Quote], lookback_periods: int = 10, multiplier: float = 3 +): """Get SuperTrend calculated. SuperTrend attempts to determine the primary trend of Close prices by using @@ -35,7 +37,9 @@ def get_super_trend(quotes: Iterable[Quote], lookback_periods: int = 10, multipl - [SuperTrend Reference](https://python.stockindicators.dev/indicators/SuperTrend/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - super_trend_results = CsIndicator.GetSuperTrend[Quote](CsList(Quote, quotes), lookback_periods, multiplier) + super_trend_results = CsIndicator.GetSuperTrend[Quote]( + CsList(Quote, quotes), lookback_periods, multiplier + ) return SuperTrendResults(super_trend_results, SuperTrendResult) @@ -70,6 +74,8 @@ def lower_band(self, value): _T = TypeVar("_T", bound=SuperTrendResult) + + class SuperTrendResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Super Trend results. diff --git a/stock_indicators/indicators/zig_zag.py b/stock_indicators/indicators/zig_zag.py index b3a49ba7..4368a055 100644 --- a/stock_indicators/indicators/zig_zag.py +++ b/stock_indicators/indicators/zig_zag.py @@ -11,8 +11,11 @@ from stock_indicators.indicators.common.quote import Quote -def get_zig_zag(quotes: Iterable[Quote], end_type: EndType = EndType.CLOSE, - percent_change: float = 5): +def get_zig_zag( + quotes: Iterable[Quote], + end_type: EndType = EndType.CLOSE, + percent_change: float = 5, +): """Get Zig Zag calculated. Zig Zag is a price chart overlay that simplifies the up and down @@ -36,8 +39,9 @@ def get_zig_zag(quotes: Iterable[Quote], end_type: EndType = EndType.CLOSE, - [Zig Zag Reference](https://python.stockindicators.dev/indicators/ZigZag/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetZigZag[Quote](CsList(Quote, quotes), end_type.cs_value, - CsDecimal(percent_change)) + results = CsIndicator.GetZigZag[Quote]( + CsList(Quote, quotes), end_type.cs_value, CsDecimal(percent_change) + ) return ZigZagResults(results, ZigZagResult) @@ -80,6 +84,8 @@ def retrace_low(self, value): _T = TypeVar("_T", bound=ZigZagResult) + + class ZigZagResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of Zig Zag results. From 092a890c63e418ac6f10fa9e1a432a49961da05d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:54:30 +0000 Subject: [PATCH 06/10] Merge main branch and resolve conflicts for decimal conversion performance improvement Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com> --- .coderabbit.yaml | 277 ++ .devcontainer/devcontainer.json | 83 +- .devcontainer/post-create.sh | 25 + .devcontainer/setup.sh | 10 - .github/workflows/ci.yml | 63 + .github/workflows/deploy-package.yml | 6 +- .github/workflows/deploy-website.yml | 2 +- .github/workflows/lint-pull-request.yml | 52 +- .github/workflows/test-code-coverage.yml | 12 +- .github/workflows/test-indicators-full.yml | 26 +- .github/workflows/test-indicators.yml | 16 +- .github/workflows/test-localization.yml | 11 +- .github/workflows/test-performance.yml | 10 +- .github/workflows/test-website-a11y.yml | 2 +- .github/workflows/test-website-links.yml | 2 +- .markdownlint-cli2.jsonc | 57 + .pylintrc | 21 - .vscode/extensions.json | 5 +- .vscode/mcp.json | 36 + .vscode/settings.json | 47 +- .vscode/tasks.json | 212 +- README.md | 6 +- benchmarks/conftest.py | 5 +- benchmarks/test_benchmark_indicators.py | 8 +- docs/Gemfile.lock | 6 +- docs/_indicators/AtrStop.md | 1 - docs/contributing.md | 62 +- docs/pages/guide.md | 46 +- docs/pages/performance.md | 4 +- pyproject.toml | 55 +- requirements-test.txt | 3 + requirements.txt | 2 +- stock_indicators/__init__.py | 7 + stock_indicators/_cslib/__init__.py | 190 +- .../_cslib/lib/Skender.Stock.Indicators.dll | Bin 219648 -> 218112 bytes .../_cslib/lib/Skender.Stock.Indicators.xml | 2742 +++++++++++++++++ stock_indicators/_cstypes/__init__.py | 6 +- stock_indicators/_cstypes/datetime.py | 5 +- stock_indicators/_cstypes/decimal.py | 71 +- stock_indicators/_cstypes/list.py | 21 +- stock_indicators/exceptions.py | 23 + stock_indicators/indicators/__init__.py | 170 +- stock_indicators/indicators/adl.py | 4 +- stock_indicators/indicators/adx.py | 12 +- stock_indicators/indicators/alligator.py | 30 +- stock_indicators/indicators/alma.py | 13 +- stock_indicators/indicators/aroon.py | 4 +- stock_indicators/indicators/atr.py | 4 +- stock_indicators/indicators/atr_stop.py | 4 +- stock_indicators/indicators/awesome.py | 8 +- stock_indicators/indicators/basic_quotes.py | 12 +- stock_indicators/indicators/beta.py | 18 +- .../indicators/bollinger_bands.py | 12 +- stock_indicators/indicators/bop.py | 4 +- stock_indicators/indicators/cci.py | 4 +- .../indicators/chaikin_oscillator.py | 12 +- stock_indicators/indicators/chandelier.py | 17 +- stock_indicators/indicators/chop.py | 4 +- stock_indicators/indicators/cmf.py | 4 +- stock_indicators/indicators/cmo.py | 4 +- .../indicators/common/__init__.py | 27 +- .../common/_contrib/type_resolver.py | 15 +- stock_indicators/indicators/common/candles.py | 47 +- stock_indicators/indicators/common/enums.py | 13 +- stock_indicators/indicators/common/helpers.py | 52 +- stock_indicators/indicators/common/quote.py | 151 +- stock_indicators/indicators/common/results.py | 96 +- stock_indicators/indicators/connors_rsi.py | 17 +- stock_indicators/indicators/correlation.py | 14 +- stock_indicators/indicators/dema.py | 4 +- stock_indicators/indicators/doji.py | 4 +- stock_indicators/indicators/donchian.py | 4 +- stock_indicators/indicators/dpo.py | 4 +- stock_indicators/indicators/dynamic.py | 8 +- stock_indicators/indicators/elder_ray.py | 4 +- stock_indicators/indicators/ema.py | 13 +- stock_indicators/indicators/epma.py | 4 +- stock_indicators/indicators/fcb.py | 4 +- .../indicators/fisher_transform.py | 8 +- stock_indicators/indicators/force_index.py | 4 +- stock_indicators/indicators/fractal.py | 8 +- stock_indicators/indicators/gator.py | 15 +- stock_indicators/indicators/heikin_ashi.py | 4 +- stock_indicators/indicators/hma.py | 4 +- stock_indicators/indicators/ht_trendline.py | 4 +- stock_indicators/indicators/hurst.py | 4 +- stock_indicators/indicators/ichimoku.py | 40 +- stock_indicators/indicators/kama.py | 23 +- stock_indicators/indicators/keltner.py | 17 +- stock_indicators/indicators/kvo.py | 17 +- stock_indicators/indicators/ma_envelopes.py | 17 +- stock_indicators/indicators/macd.py | 24 +- stock_indicators/indicators/mama.py | 12 +- stock_indicators/indicators/mfi.py | 4 +- stock_indicators/indicators/obv.py | 4 +- stock_indicators/indicators/parabolic_sar.py | 43 +- stock_indicators/indicators/pivot_points.py | 4 +- stock_indicators/indicators/pivots.py | 4 +- stock_indicators/indicators/pmo.py | 17 +- stock_indicators/indicators/prs.py | 18 +- stock_indicators/indicators/pvo.py | 17 +- stock_indicators/indicators/renko.py | 4 +- stock_indicators/indicators/roc.py | 28 +- stock_indicators/indicators/rolling_pivots.py | 4 +- stock_indicators/indicators/rsi.py | 4 +- stock_indicators/indicators/slope.py | 4 +- stock_indicators/indicators/sma.py | 20 +- stock_indicators/indicators/smi.py | 24 +- stock_indicators/indicators/smma.py | 4 +- stock_indicators/indicators/starc_bands.py | 22 +- stock_indicators/indicators/stc.py | 17 +- stock_indicators/indicators/stdev.py | 13 +- stock_indicators/indicators/stdev_channels.py | 16 +- stock_indicators/indicators/stoch.py | 26 +- stock_indicators/indicators/stoch_rsi.py | 20 +- stock_indicators/indicators/super_trend.py | 4 +- stock_indicators/indicators/t3.py | 14 +- stock_indicators/indicators/tema.py | 4 +- stock_indicators/indicators/tr.py | 4 +- stock_indicators/indicators/trix.py | 12 +- stock_indicators/indicators/tsi.py | 17 +- stock_indicators/indicators/ulcer_index.py | 4 +- stock_indicators/indicators/ultimate.py | 17 +- .../indicators/volatility_stop.py | 14 +- stock_indicators/indicators/vortex.py | 4 +- stock_indicators/indicators/vwap.py | 117 +- stock_indicators/indicators/vwma.py | 4 +- stock_indicators/indicators/williams_r.py | 4 +- stock_indicators/indicators/wma.py | 13 +- stock_indicators/indicators/zig_zag.py | 4 +- .../common/test-dateof-roundtrip-variants.py | 5 +- tests/common/test_candle.py | 63 +- tests/common/test_common.py | 15 +- tests/common/test_cstype_conversion.py | 34 +- tests/common/test_cstype_datetime_kind.py | 24 +- tests/common/test_dateof_equivalence.py | 2 +- .../common/test_dateof_identity_roundtrip.py | 1 + tests/common/test_indicator_results.py | 57 +- tests/common/test_locale.py | 12 +- tests/common/test_quote.py | 87 +- tests/common/test_sma_roundtrip_dates.py | 8 +- tests/common/test_type_compatibility.py | 7 +- tests/conftest.py | 26 +- tests/test_adl.py | 29 +- tests/test_adx.py | 33 +- tests/test_basic_quote.py | 33 +- tests/test_connors_rsi.py | 2 - tests/test_gator.py | 31 +- tests/test_hurst.py | 4 +- tests/test_parabolic_sar.py | 22 +- tests/test_renko.py | 16 +- tests/test_sma_analysis.py | 9 +- tests/test_starc_bands.py | 1 - tests/test_stdev.py | 27 +- tests/test_stoch.py | 3 - tests/test_trix.py | 9 +- tests/test_tsi.py | 3 + tests/test_ulcer_index.py | 3 + tests/test_vwap.py | 1 - tests/utiltest.py | 13 +- typings/Skender/Stock/Indicators/__init__.pyi | 17 + .../System/Collections/Generic/__init__.pyi | 4 + typings/System/Globalization/__init__.pyi | 4 + typings/System/Threading/__init__.pyi | 3 + typings/System/__init__.pyi | 8 + typings/pythonnet/__init__.pyi | 3 + typings/stock_indicators/_cslib/__init__.pyi | 25 + 167 files changed, 5313 insertions(+), 1144 deletions(-) create mode 100644 .coderabbit.yaml create mode 100644 .devcontainer/post-create.sh delete mode 100755 .devcontainer/setup.sh create mode 100644 .github/workflows/ci.yml create mode 100644 .markdownlint-cli2.jsonc delete mode 100644 .pylintrc create mode 100644 .vscode/mcp.json create mode 100644 stock_indicators/_cslib/lib/Skender.Stock.Indicators.xml create mode 100644 stock_indicators/exceptions.py create mode 100644 typings/Skender/Stock/Indicators/__init__.pyi create mode 100644 typings/System/Collections/Generic/__init__.pyi create mode 100644 typings/System/Globalization/__init__.pyi create mode 100644 typings/System/Threading/__init__.pyi create mode 100644 typings/System/__init__.pyi create mode 100644 typings/pythonnet/__init__.pyi create mode 100644 typings/stock_indicators/_cslib/__init__.pyi diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..8f166db2 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,277 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# CodeRabbit config docs: https://docs.coderabbit.ai/reference/configuration +# Defaults and examples GitHub repo: coderabbitai/awesome-coderabbit + +# General settings +language: "en-US" # Language for reviews (default: "en-US") +tone_instructions: "" # Custom tone for reviews (default: "") +enable_free_tier: false # Enable free tier features (default: true) +inheritance: false # top-level instructions (default: false) + +# preview features +early_access: true # Enable early-access features (default: false) + +# ISSUE ENRICHMENT (preview) +issue_enrichment: + auto_enrich: + enabled: false # Enable auto issue enrichment (default: true) + planning: + enabled: true # Enable generating a planning for issues (default: true) + auto_planning: + enabled: true # Enable auto-generated plans based on labels (default: true) + labels: # Triggering labels for auto-planning (default: []) + - rabbit-plan + labeling: + auto_apply_labels: false + labeling_instructions: [] + +# REVIEWS +reviews: + profile: "chill" + review_status: true + commit_status: true + fail_commit_status: false + request_changes_workflow: false + enable_prompt_for_ai_agents: true + in_progress_fortune: false + + abort_on_close: true + disable_cache: false + + # High level summary of the changes in the PR/MR description or walkthrough. + high_level_summary: false + high_level_summary_in_walkthrough: false + high_level_summary_placeholder: "@coderabbitai summary" + + # Walkthrough + collapse_walkthrough: true + changed_files_summary: false # Generate a summary of the changed files in the walkthrough. + sequence_diagrams: false # Generate sequence diagrams in the walkthrough. + estimate_code_review_effort: false # Generate review effort in walkthrough. + assess_linked_issues: true # Assess how well the changes address the linked issues in the walkthrough. + related_issues: false # Include possibly related issues in the walkthrough. + related_prs: false # Include possibly related pull requests in the walkthrough. + poem: false # Generate a poem in the walkthrough comment. + + # Automatic review scheduling + auto_review: + enabled: true + auto_incremental_review: true # Review each push (default: true) + drafts: false # Review Draft pull requests (default: false) + base_branches: + - ".*" + + # Automatic labels + # Suggest labels based on the changes in the pull request in the walkthrough. + suggested_labels: false + auto_apply_labels: false + + # Automatic reviewers + # Suggest reviewers based on the changes in the pull request in the walkthrough. + suggested_reviewers: false + auto_assign_reviewers: false + + # Automatic PR titles + auto_title_placeholder: "@coderabbitai" + auto_title_instructions: | + Use conventional commit with short (<65 chars) sentence case Subject. See Copilot instructions. + + Title strategy: + - Focus on the PRIMARY contribution or most central feature delivered in the PR + - Do NOT base the title on the last commit message + - Do NOT choose the most verbose or largest change + - Identify the core purpose or business value of the PR + - Keep titles concise and meaningful + + Requirements: + - Use lowercase for type: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, or plan + - Subject must start with an uppercase letter + - Keep total title length ≤ 65 characters + - Use imperative mood (e.g., "Add feature" not "Added feature") + + Examples: + - `feat: Add WebSocket support` (not "Update connection manager") + - `fix: Resolve authentication timeout` (not "Fix bug in auth service") + - `docs: Improve API examples` (not "Update documentation files") + - `chore: Update dependencies` (when that's the sole purpose) + + # Path specific review instructions + path_instructions: + - path: "**/src/**" + instructions: "Ensure alignment to latest language and framework standards, and our organization's coding standards" + - path: "**/test/**" + instructions: "Focus on test coverage, edge cases, and alignment to test framework" + - path: "**/api/**" + instructions: "Ensure proper error handling and API documentation" + - path: "**/*.md" + instructions: "Check for broken links and formatting according to markdown configuration and instructions" + + # Code enhancement features + finishing_touches: + docstrings: + enabled: false # Generate docstrings for PRs/MRs (default: true) + unit_tests: + enabled: false # Generate unit tests for PRs/MRs (default: true) + + # Pre-merge validation checks + pre_merge_checks: + docstrings: # Docstring coverage check (default: warning) + mode: off + description: # PR description validation (default: warning) + mode: off + issue_assessment: # Linked issue assessment (default: warning) + mode: off + title: # PR title validation (default: warning) + mode: off + + # Files to include/exclude from reviews + path_filters: + - "**" # Include all (base) + - "!node_modules/" # NPM dependencies + - "!*-lock.json" # NPM/PNPM lock files + - "!packages.lock.json" # NuGet lock file + - "!packages/" # NuGet packages cache + - "!_cslib/" # DLL library path + - "!.jekyll-cache/" # Jekyll cache + - "!.pytest_cache/" # Python test cache + - "!__pycache__/" # Python caches + - "!.venv/" # Python virtual env + - "!.benchmarks/" # Benchmarks cache + - "!.coverage/" # Code coverage reports + - "!vendor/" # Ruby vendor and gem files + - "!Gemfile.lock" # Ruby dependencies lock + - "!.vitepress/cache/" # VitePress cache + - "!.vitepress/dist/" # VitePress build output + - "!bin/" # .NET build output + - "!obj/" # .NET build intermediate files + - "!*.g.cs" # Generated source code + - "!*.docx" # Office Word + - "!*.pptx" # Office PowerPoint + - "!*.xlsx" # Office Spreadsheets + - "!*.bak" # Backup files + - "!*.zip" # Compressed files + + # Tool integrations + tools: + + # tools: default on + actionlint: # GitHub Actions workflow static checker + enabled: true + ast-grep: # Code pattern analysis using AST + essential_rules: true + gitleaks: # Secret/credential scanner + enabled: true + github-checks: # GitHub Checks integration + enabled: true + timeout_ms: 90000 + markdownlint: # Markdown linter for consistency + enabled: true + osvScanner: # Vulnerability package scanner + enabled: true + ruff: # Python linter and formatter + enabled: true + semgrep: # Security & code quality static analysis + enabled: true + shellcheck: # Shell script linter + enabled: true + yamllint: # YAML linter + enabled: true + + # tools: default off + biome: # Fast formatter, linter, and analyzer for web + enabled: false + brakeman: # Rails security vulnerability scanner + enabled: false + buf: # Protobuf linter + enabled: false + checkov: # Infrastructure-as-code static analyzer + enabled: false + checkmake: # Makefile linter + enabled: false + circleci: # CircleCI config static checker + enabled: false + clang: # C/C++ static analysis tool + enabled: false + clippy: # Rust linter + enabled: false + cppcheck: # C/C++ static analyzer + enabled: false + detekt: # Kotlin static analyzer + enabled: false + dotenvLint: # .env file linter + enabled: false + eslint: # JavaScript/TypeScript linter + enabled: false + flake8: # Python linter + enabled: false + fortitudeLint: # Fortran linter + enabled: false + golangci-lint: # Go linter runner + enabled: false + hadolint: # Dockerfile linter + enabled: false + htmlhint: # HTML linter + enabled: false + languagetool: # Grammar and style checker (30+ languages) + enabled: false + luacheck: # Lua linter + enabled: false + oxc: # JavaScript/TypeScript linter in Rust + enabled: false + phpcs: # PHP linter and coding standard checker + enabled: false + phpmd: # PHP code quality analyzer + enabled: false + phpstan: # PHP static analysis tool + enabled: false + pmd: # Java/multilanguage static analyzer + enabled: false + prismaLint: # Prisma schema linter + enabled: false + pylint: # Python static analyzer + enabled: false + regal: # Rego linter and language server + enabled: false + rubocop: # Ruby linter and code formatter + enabled: false + shopifyThemeCheck: # Shopify Liquid theme linter + enabled: false + sqlfluff: # SQL linter + enabled: false + swiftlint: # Swift linter + enabled: false + +# CHAT FEATURES +chat: + art: false + auto_reply: true + integrations: + jira: + usage: disabled + linear: + usage: disabled + +# KNOWLEDGE BASE +knowledge_base: + opt_out: false + web_search: + enabled: true + code_guidelines: + enabled: true + filePatterns: + - "**/agents.md" + - ".github/copilot-instructions.md" + - ".github/instructions/**/*.instructions.md" + - ".github/skills/**/*skill.md" + learnings: + scope: auto + issues: + scope: auto + pull_requests: + scope: auto + mcp: + usage: auto + jira: + usage: disabled + linear: + usage: disabled diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 85748491..32609b39 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,36 +9,20 @@ */ { "name": "Stock Indicators for Python", - "image": "mcr.microsoft.com/devcontainers/python:3.12", - "forwardPorts": [], - "remoteUser": "vscode", + "image": "mcr.microsoft.com/devcontainers/python:3.13", "features": { - "ghcr.io/devcontainers/features/git:1": { - "version": "os-provided" - }, "ghcr.io/devcontainers/features/dotnet:2": { - "version": "lts" + "version": "10.0", + "additionalVersions": "9.0,8.0" }, "ghcr.io/devcontainers/features/node:1": { "version": "lts", "pnpmVersion": "none", "nvmVersion": "none" }, - "ghcr.io/devcontainers/features/github-cli:1": { - "installDirectlyFromGitHubRelease": true, - "version": "latest" - }, - "ghcr.io/devcontainers/features/azure-cli:1": { - "version": "latest" - }, + "ghcr.io/devcontainers/features/github-cli:1": {}, "ghcr.io/devcontainers/features/ruby:1": { "version": "3.3" - }, - "ghcr.io/devcontainers-extra/features/isort:2": { - "version": "latest" - }, - "ghcr.io/devcontainers-extra/features/pylint:2": { - "version": "latest" } }, // Use 'settings' to set *default* container specific settings.json @@ -48,26 +32,39 @@ // container overrides only // otherwise use .vscode/settings.json "settings": { - "pylint.importStrategy": "fromEnvironment", - "python.defaultInterpreterPath": "/usr/local/bin/python" - }, - // required extensions - // for recommended, see .vscode/extensions.json - "extensions": [ - "donjayamanne.python-extension-pack", - "DavidAnson.vscode-markdownlint", - "EditorConfig.EditorConfig", - "ms-python.black-formatter", - "ms-python.debugpy", - "ms-python.isort", - "ms-python.python", - "ms-python.pylint", - "ms-python.vscode-pylance" - ] - } - }, - // Runs after the container is created - "postCreateCommand": "chmod +x .devcontainer/setup.sh && .devcontainer/setup.sh", - // Runs every time the container starts - "postStartCommand": "echo 'Container started'" -} + "python.defaultInterpreterPath": "${containerWorkspaceFolder}/.venv/bin/python", + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports.ruff": "explicit", + "source.fixAll.ruff": "explicit" + } + }, + // required extensions + // for recommended, see .vscode/extensions.json + "extensions": [ + "charliermarsh.ruff", + "DavidAnson.vscode-markdownlint", + "EditorConfig.EditorConfig", + "ms-python.debugpy", + "ms-python.python", + "ms-python.vscode-pylance" + ] + } + }, + "forwardPorts": [ + 4000 + ], + "portsAttributes": { + "4000": { + "label": "Doc Site (Jekyll)", + "onAutoForward": "notify" + } + }, + "remoteUser": "vscode", + "postCreateCommand": ".devcontainer/post-create.sh", + "postStartCommand": "echo 'Container started'" + } diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100644 index 00000000..03560b04 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +# Create virtual environment if it doesn't exist +if [ ! -d ".venv" ]; then + echo "Creating virtual environment..." + python -m venv .venv +fi + +# Activate virtual environment +source .venv/bin/activate + +# Upgrade pip +echo "Upgrading pip..." +python -m pip install --upgrade pip + +# Install core dependencies +echo "Installing core dependencies..." +pip install -r requirements.txt + +# Install test dependencies +echo "Installing test dependencies..." +pip install -r requirements-test.txt + +echo "✓ Dev container setup complete!" diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh deleted file mode 100755 index 7ad8e56e..00000000 --- a/.devcontainer/setup.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# install or upgrade pip -python -m ensurepip --upgrade - -# install core dependencies -pip install -r requirements.txt - -# install test dependencies -pip install -r requirements-test.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..dcd91906 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + linting: + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.x + dotnet-quality: ga + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: 3.13 + cache: "pip" + + - name: Create virtual environment + run: python -m venv .venv + + - name: Install dependencies + run: | + source .venv/bin/activate + python -m pip install --upgrade pip + python -m pip install -e . + python -m pip install -r requirements-test.txt + + - name: Ruff lint + run: | + source .venv/bin/activate + ruff check . + + - name: Ruff format check + run: | + source .venv/bin/activate + ruff format --check . + + - name: Pyright + run: | + source .venv/bin/activate + pyright + + - name: Pytest + run: | + source .venv/bin/activate + pytest + + - name: pip-audit + run: | + source .venv/bin/activate + pip-audit -r requirements.txt -r requirements-test.txt diff --git a/.github/workflows/deploy-package.yml b/.github/workflows/deploy-package.yml index 2e35e2d8..22df88cb 100644 --- a/.github/workflows/deploy-package.yml +++ b/.github/workflows/deploy-package.yml @@ -19,15 +19,15 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-tags: true fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: 3.12 + python-version: 3.13 - name: Build library run: | diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index b58c2f68..96de73a9 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/lint-pull-request.yml b/.github/workflows/lint-pull-request.yml index 56118d78..95c01a2b 100644 --- a/.github/workflows/lint-pull-request.yml +++ b/.github/workflows/lint-pull-request.yml @@ -1,26 +1,48 @@ -name: Pull request +name: Lint pull request on: pull_request_target: + branches: + - "main" + - "v[0-9]*" types: - opened - edited - unlabeled + - ready_for_review + +concurrency: + group: >- + ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true permissions: pull-requests: write jobs: - main: - name: lint PR title + title: runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@v6.1.1 id: lint_pr_title env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert + plan subjectPattern: ^([A-Z]).+$ subjectPatternError: > The subject "**{subject}**" must start with an uppercase character. @@ -28,8 +50,9 @@ jobs: ignoreLabels: | bot dependencies + automated - - uses: marocchino/sticky-pull-request-comment@v2 + - uses: marocchino/sticky-pull-request-comment@v2.9.4 if: always() && (steps.lint_pr_title.outputs.error_message != null) with: header: pr-title-lint-error @@ -47,6 +70,7 @@ jobs: - `feat: Add API endpoint for market data` - `fix: Resolve WebSocket connection issues` + - `plan: Define technical implementation approach` - `chore: Update NuGet dependencies`
@@ -56,29 +80,33 @@ jobs: - `feat: Add API endpoint for market data` - `fix: Resolve WebSocket connection issues` + #### Planning & architecture + - `plan: Define technical implementation approach` + #### Code quality - `style: Format trading strategy classes` - `refactor: Restructure trading engine components` - `perf: Optimize trade order execution flow` - + #### Documentation & testing - `docs: Update API documentation` - `test: Add unit tests for sign-in flow` - + #### Infrastructure - - `build: Update .NET SDK version to 8.0` + - `build: Update .NET SDK version to 10.0` - `ci: Add workflow for performance testing` - `chore: Update NuGet dependencies` - + #### Other - `revert: Remove faulty market data provider` - - See [Conventional Commits](https://www.conventionalcommits.org) for more details. + + See [Conventional Commits](https://www.conventionalcommits.org) + for more details.
# Delete a previous comment when the issue has been resolved - if: ${{ steps.lint_pr_title.outputs.error_message == null }} - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@v2.9.4 with: header: pr-title-lint-error delete: true diff --git a/.github/workflows/test-code-coverage.yml b/.github/workflows/test-code-coverage.yml index adb0063b..bc860047 100644 --- a/.github/workflows/test-code-coverage.yml +++ b/.github/workflows/test-code-coverage.yml @@ -6,7 +6,7 @@ name: Test code coverage on: push: branches: ["main"] - pull_request_target: + pull_request: branches: ["*"] workflow_dispatch: @@ -27,20 +27,20 @@ jobs: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} steps: - - name: Checkout PR - uses: actions/checkout@v5 + - name: Checkout source + uses: actions/checkout@v6 with: # Explicitly checkout PR code ref: ${{ github.event.pull_request.head.sha }} - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 9.x + dotnet-version: 10.x dotnet-quality: ga - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.13 cache: "pip" diff --git a/.github/workflows/test-indicators-full.yml b/.github/workflows/test-indicators-full.yml index de8685d3..b802f433 100644 --- a/.github/workflows/test-indicators-full.yml +++ b/.github/workflows/test-indicators-full.yml @@ -23,26 +23,26 @@ jobs: # Primary testing on Ubuntu (free tier) - os: ubuntu-24.04-arm python-version: "3.8" - dotnet-version: "6.x" - - os: ubuntu-24.04-arm - python-version: "3.10" dotnet-version: "8.x" - os: ubuntu-24.04-arm - python-version: "3.12" + python-version: "3.10" dotnet-version: "9.x" - + - os: ubuntu-24.04-arm + python-version: "3.13" + dotnet-version: "10.x" + # Essential platform compatibility testing (reduced matrix) - os: windows-2025 python-version: "3.12" - dotnet-version: "9.x" + dotnet-version: "10.x" - os: macos-15 - python-version: "3.12" - dotnet-version: "9.x" - + python-version: "3.13" + dotnet-version: "10.x" + # Legacy support verification - os: ubuntu-22.04 python-version: "3.8" - dotnet-version: "6.x" + dotnet-version: "8.x" runs-on: ${{ matrix.os }} name: "Py${{ matrix.python-version }}/.NET${{ matrix.dotnet-version }} on ${{ matrix.os }}" @@ -54,16 +54,16 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ matrix.dotnet-version }} dotnet-quality: ga - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: "pip" diff --git a/.github/workflows/test-indicators.yml b/.github/workflows/test-indicators.yml index ca1341ff..06ead104 100644 --- a/.github/workflows/test-indicators.yml +++ b/.github/workflows/test-indicators.yml @@ -20,14 +20,14 @@ jobs: include: - # Primary testing on Ubuntu (free tier) with older configuration + # Primary testing on Ubuntu (free tier) with minimum supported .NET version - os: ubuntu-22.04 - dotnet-version: "6.x" + dotnet-version: "8.x" python-version: "3.8" # Primary testing on Ubuntu (free tier) with newer configuration - os: ubuntu-24.04-arm - dotnet-version: "9.x" + dotnet-version: "10.x" python-version: "3.13" post-summary: true @@ -46,10 +46,10 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Check debug settings - if: ${{ matrix.post-summary }} == 'true' + if: ${{ matrix['post-summary'] == true }} shell: bash run: | echo "Checking for debug logging settings in package files..." @@ -64,13 +64,13 @@ jobs: echo "✓ No debug logging settings found." - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ matrix.dotnet-version }} dotnet-quality: ga - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: "pip" @@ -97,6 +97,6 @@ jobs: - name: Post test summary uses: test-summary/action@v2 - if: ${{ matrix.post-summary }} == 'true' + if: ${{ matrix['post-summary'] == true }} with: paths: test-results.xml diff --git a/.github/workflows/test-localization.yml b/.github/workflows/test-localization.yml index 832fad9a..29e2daa3 100644 --- a/.github/workflows/test-localization.yml +++ b/.github/workflows/test-localization.yml @@ -48,16 +48,17 @@ jobs: PYTHONIOENCODING: utf-8 steps: - - uses: actions/checkout@v5 + - name: Checkout source + uses: actions/checkout@v6 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: - dotnet-version: 9.x + dotnet-version: 10.x dotnet-quality: ga - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: - python-version: 3.12 + python-version: 3.13 cache: "pip" - name: Configure Linux locale diff --git a/.github/workflows/test-performance.yml b/.github/workflows/test-performance.yml index 76e651c4..176bad1d 100644 --- a/.github/workflows/test-performance.yml +++ b/.github/workflows/test-performance.yml @@ -21,20 +21,20 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 9.x + dotnet-version: 10.x dotnet-quality: ga - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: 3.12 + python-version: 3.13 cache: "pip" - name: Setup GitVersion diff --git a/.github/workflows/test-website-a11y.yml b/.github/workflows/test-website-a11y.yml index 0f925299..ed7f6542 100644 --- a/.github/workflows/test-website-a11y.yml +++ b/.github/workflows/test-website-a11y.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/test-website-links.yml b/.github/workflows/test-website-links.yml index da87bd15..affe3233 100644 --- a/.github/workflows/test-website-links.yml +++ b/.github/workflows/test-website-links.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 00000000..d11a7dc6 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,57 @@ +{ + "globs": [ + "**/*.md", + "**/*.markdown" + ], + "ignores": [ + "**/.pytest_cache/**", + "**/.coverage/**", + "**/.git/**", + "**/.github/**", + "**/.venv/**", + "**/_site/**", + "**/__pycache__/**", + "**/_cslib/**", + "**/node_modules/**", + "**/TestResults/**", + "**/*playwright*/**", + "**/vendor/**" + ], + "config": { + "default": true, + "MD003": { + "style": "atx" + }, + "MD004": { + "style": "dash" + }, + "MD007": { + "indent": 2 + }, + "MD013": false, + "MD025": false, + "MD029": { + "style": "ordered" + }, + "MD033": { + "allowed_elements": [ + "a", + "code", + "details", + "summary", + "sub", + "sup", + "kbd", + "abbr", + "img", + "br" + ] + }, + "MD046": { + "style": "fenced" + }, + "MD048": { + "style": "backtick" + } + } +} diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 3340ad6c..00000000 --- a/.pylintrc +++ /dev/null @@ -1,21 +0,0 @@ -[MASTER] -extension-pkg-allow-list= - clr - -ignore-long-lines=yes - -ignore-imports=yes - -disable= - C0103, # Variable name doesn't conform to snake_case naming style - C0114, # Missing module docstring - C0115, # Missing class docstring - C0116, # Missing function or method docstring - C0301, # Line too long - C0321, # More than one statement on a single line - C0413, # Import should be at the top of the file - C0415, # Import outside toplevel - E0401, # Import error - W0212, # Access to a protected member of a client class - R0903, # Too few public methods - R0913 # Too many arguments diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 61fe2e05..091bc9a5 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,13 +1,10 @@ { "recommendations": [ - "donjayamanne.python-extension-pack", + "charliermarsh.ruff", "DavidAnson.vscode-markdownlint", "EditorConfig.EditorConfig", - "ms-python.black-formatter", "ms-python.debugpy", - "ms-python.isort", "ms-python.python", - "ms-python.pylint", "ms-python.vscode-pylance" ] } diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 00000000..f9052861 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,36 @@ +{ + "servers": { + "cloudflare": { + "type": "stdio", + "command": "npx", + "args": [ + "mcp-remote@latest", + "https://docs.mcp.cloudflare.com/mcp" + ] + }, + "context7": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@upstash/context7-mcp@latest" + ] + }, + "playwright": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@playwright/mcp@latest" + ] + } + }, + "inputs": [ + { + "id": "codacy_account_token", + "type": "promptString", + "description": "Your personal Codacy Account API token (from app.codacy.com → Account). Required.", + "password": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index d4ab9939..42007812 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,35 +1,44 @@ { - "isort.args": [ - "--profile", - "black" + "github.copilot.chat.githubMcpServer.enabled": true, + "github.copilot.chat.githubMcpServer.readonly": true, + "github.copilot.chat.githubMcpServer.toolsets": [ + "default", // default: context, repos, issues, pull_requests, users + "actions", + // "projects", + "copilot", + "github_support_docs_search", + "web_search" ], - "isort.check": true, - "isort.importStrategy": "fromEnvironment", // default: "useBundled" + "github.copilot.chat.tools.memory.enabled": true, "markdownlint.config": { - "default": true, // Enable all default rules - "MD013": false, // Disable line length checking entirely - "MD025": false, // Allow multiple top level headers in the same document - "MD033": { // Allow specific HTML elements + "default": true, // Enable all default rules + "MD013": false, // Disable line length checking entirely + "MD025": false, // Allow multiple top level headers in the same document + "MD033": { // Allow specific HTML elements "allowed_elements": [ "details", "summary", - "h1" // we use h1 as a Jekyll-y page title + "h1" // we use h1 as a Jekyll-y page title ] }, - "MD041": false // Allow content before first heading + "MD041": false // Allow content before first heading }, - "pylint.importStrategy": "fromEnvironment", // default: "useBundled" "python.testing.pytestArgs": [ "tests" ], + "python.defaultInterpreterPath": ".venv/bin/python", "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, - "[markdown]": { - "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", - "files.trimTrailingWhitespace": true - }, "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" - }, - "python.analysis.typeCheckingMode": "basic" + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports.ruff": "explicit", + "source.fixAll.ruff": "explicit" + }, + "[markdown]": { + "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", + "files.trimTrailingWhitespace": true + } + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 94524055..29f562d4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,60 +1,25 @@ { + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // Schema: https://code.visualstudio.com/docs/reference/tasks-appendix + "$schema": "vscode://schemas/tasks", "version": "2.0.0", + // defaults + "group": "none", + "presentation": { + "clear": true, + "echo": true, + "panel": "dedicated", + "focus": true, + "reveal": "always", + "revealProblems": "onProblem", + "showReuseMessage": false + }, + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}" + }, + // tasks "tasks": [ - { - "label": "Build", - "type": "shell", - "command": "pip install -r requirements.txt && pip install -r requirements-test.txt", - "group": "build", - "problemMatcher": [] - }, - { - "label": "Test: All", - "dependsOrder": "parallel", - "dependsOn": [ - "Test: Unit (default)", - "Test: Coverage", - "Test: Performance", - "Test: Localization" - ], - "group": "test", - "problemMatcher": [] - }, - { - "label": "Test: Unit (default)", - "type": "shell", - "command": "pytest -vr A", - "group": "test", - "problemMatcher": [] - }, - { - "label": "Test: Coverage", - "type": "shell", - "command": "pytest --cov=stock_indicators --cov-report=term-missing", - "group": "none", - "problemMatcher": [] - }, - { - "label": "Test: Performance", - "type": "shell", - "command": "pytest -m performance", - "group": "none", - "problemMatcher": [] - }, - { - "label": "Test: Performance (focus)", - "type": "shell", - "command": "pytest -m \"performance and perf_focus\"", - "group": "none", - "problemMatcher": [] - }, - { - "label": "Test: Localization", - "type": "shell", - "command": "pytest -m localization -vr A", - "group": "none", - "problemMatcher": [] - }, { "label": "Install: Ruby Packages (docs)", "type": "shell", @@ -65,23 +30,17 @@ "BUNDLE_GEMFILE": "${workspaceFolder}/docs/Gemfile", "BUNDLE_PATH": "${workspaceFolder}/docs/vendor/bundle" } - }, - "group": "none", - "problemMatcher": [] + } }, { "label": "Install: Python Packages", "type": "shell", - "command": "pip install -r requirements.txt && pip install -r requirements-test.txt", - "group": "none", - "problemMatcher": [] + "command": "python -m pip install -r requirements.txt && python -m pip install -r requirements-test.txt" }, { "label": "Update: Python Packages", "type": "shell", - "command": "pip install -U -r requirements.txt && pip install -U -r requirements-test.txt", - "group": "none", - "problemMatcher": [] + "command": "python -m pip install -U -r requirements.txt && python -m pip install -U -r requirements-test.txt" }, { "label": "Update: Ruby Packages (docs)", @@ -93,9 +52,7 @@ "BUNDLE_GEMFILE": "${workspaceFolder}/docs/Gemfile", "BUNDLE_PATH": "${workspaceFolder}/docs/vendor/bundle" } - }, - "group": "none", - "problemMatcher": [] + } }, { "label": "Update: All Packages", @@ -103,9 +60,82 @@ "dependsOn": [ "Update: Python Packages", "Update: Ruby Packages (docs)" + ] + }, + { + "label": "Build", + "type": "shell", + "command": "python -m pip install -r requirements.txt && python -m pip install -r requirements-test.txt", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Lint: Code", + "type": "shell", + "command": "python -m ruff check . && python -m ruff format --check .", + "group": "test" + }, + { + "label": "Lint: Code (fix)", + "type": "shell", + "command": "python -m ruff check --fix . && python -m ruff format ." + }, + { + "label": "Lint: Markdown files", + "detail": "Run markdownlint over documentation", + "type": "shell", + "command": "echo y | npx markdownlint-cli2", + "group": "test", + "problemMatcher": "$markdownlint" + }, + { + "label": "Lint: Markdown files (fix)", + "detail": "Auto-fix markdown formatting issues", + "type": "shell", + "command": "echo y | npx markdownlint-cli2 --fix", + "problemMatcher": "$markdownlint" + }, + { + "label": "Test: All", + "dependsOrder": "parallel", + "dependsOn": [ + "Test: Unit (default)", + "Test: Coverage", + "Test: Performance", + "Test: Localization" ], - "group": "none", - "problemMatcher": [] + "group": "test" + }, + { + "label": "Test: Unit (default)", + "type": "shell", + "command": "python -m pytest -vr A", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "Test: Coverage", + "type": "shell", + "command": "python -m pytest --cov=stock_indicators --cov-report=term-missing" + }, + { + "label": "Test: Performance", + "type": "shell", + "command": "python -m pytest -m performance" + }, + { + "label": "Test: Performance (focus)", + "type": "shell", + "command": "python -m pytest -m \"performance and perf_focus\"" + }, + { + "label": "Test: Localization", + "type": "shell", + "command": "python -m pytest -m localization -vr A" }, { "label": "Run: Doc Site with LiveReload", @@ -120,69 +150,37 @@ "BUNDLE_GEMFILE": "${workspaceFolder}/docs/Gemfile", "BUNDLE_PATH": "${workspaceFolder}/docs/vendor/bundle" } - }, - "group": "none", - "problemMatcher": [] + } }, { "label": "Benchmark: JSON (prompt filename)", "type": "shell", - "command": "pytest -m performance --benchmark-json=.benchmarks/${input:benchJsonOut}", - "options": { - "cwd": "${workspaceFolder}" - }, - "group": "none", - "problemMatcher": [] + "command": "python -m pytest -m performance --benchmark-json=.benchmarks/${input:benchJsonOut}" }, { "label": "Benchmark: Focus JSON (prompt filename)", "type": "shell", - "command": "pytest -m \"performance and perf_focus\" --benchmark-json=.benchmarks/${input:benchJsonOut}", - "options": { - "cwd": "${workspaceFolder}" - }, - "group": "none", - "problemMatcher": [] + "command": "python -m pytest -m \"performance and perf_focus\" --benchmark-json=.benchmarks/${input:benchJsonOut}" }, { "label": "Benchmark: Autosave to .benchmarks", "type": "shell", - "command": "pytest -m performance --benchmark-autosave --benchmark-save-data", - "options": { - "cwd": "${workspaceFolder}" - }, - "group": "none", - "problemMatcher": [] + "command": "python -m pytest -m performance --benchmark-autosave --benchmark-save-data" }, { "label": "Benchmark: Focus Autosave to .benchmarks", "type": "shell", - "command": "pytest -m \"performance and perf_focus\" --benchmark-autosave --benchmark-save-data", - "options": { - "cwd": "${workspaceFolder}" - }, - "group": "none", - "problemMatcher": [] + "command": "python -m pytest -m \"performance and perf_focus\" --benchmark-autosave --benchmark-save-data" }, { "label": "Benchmark: List Saved Runs", "type": "shell", - "command": "pytest-benchmark list", - "options": { - "cwd": "${workspaceFolder}" - }, - "group": "none", - "problemMatcher": [] + "command": "pytest-benchmark list" }, { "label": "Benchmark: Compare Saved Runs", "type": "shell", - "command": "pytest-benchmark compare ${input:benchBase} ${input:benchTarget}", - "options": { - "cwd": "${workspaceFolder}" - }, - "group": "none", - "problemMatcher": [] + "command": "pytest-benchmark compare ${input:benchBase} ${input:benchTarget}" } ], "inputs": [ diff --git a/README.md b/README.md index 1b32e6cc..e2d33019 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ Visit our project site for more information: ### Windows -1. Install .NET SDK (6.0 or newer): +1. Install .NET SDK (8.0 or newer): - Download from [Microsoft .NET Downloads](https://dotnet.microsoft.com/download) - - Or using winget: `winget install Microsoft.DotNet.SDK.6` + - Or using winget: `winget install Microsoft.DotNet.SDK.8` - Verify: `dotnet --info` 2. Install the package: @@ -35,7 +35,7 @@ Visit our project site for more information: ### macOS -1. Install .NET SDK (6.0 or newer): +1. Install .NET SDK (8.0 or newer): ```bash brew install dotnet-sdk diff --git a/benchmarks/conftest.py b/benchmarks/conftest.py index 28aaf09e..c6518ee3 100644 --- a/benchmarks/conftest.py +++ b/benchmarks/conftest.py @@ -35,7 +35,7 @@ def get_data_from_csv(filename): data_path = quotes_dir / f"{filename}.csv" logger.debug("Loading benchmark data from: %s", data_path) - with open(data_path, "r", newline="", encoding="utf-8") as csvfile: + with open(data_path, newline="", encoding="utf-8") as csvfile: reader = csv.reader(csvfile) data = list(reader) return data[1:] # skips the first row, those are headers @@ -62,9 +62,10 @@ def parse_date(date_str): @pytest.fixture(scope="session") -def raw_data(filename: str = 'Default'): +def raw_data(filename: str = "Default"): return get_data_from_csv(filename) + @pytest.fixture(scope="session") def quotes(days: int = 502): rows = get_data_from_csv("Default") diff --git a/benchmarks/test_benchmark_indicators.py b/benchmarks/test_benchmark_indicators.py index 5a18555e..ef3f2ca5 100644 --- a/benchmarks/test_benchmark_indicators.py +++ b/benchmarks/test_benchmark_indicators.py @@ -6,7 +6,6 @@ @pytest.mark.performance class TestPerformance: - def test_benchmark_adl(self, benchmark, quotes): benchmark(indicators.get_adl, quotes) @@ -262,17 +261,20 @@ def test_benchmark_converting_to_IndicatorResults(self, benchmark, quotes): from stock_indicators._cslib import CsIndicator from stock_indicators.indicators.common.enums import CandlePart from stock_indicators.indicators.common.quote import Quote - from stock_indicators.indicators.sma import SMAResults, SMAResult + from stock_indicators.indicators.sma import SMAResult, SMAResults candle_part: CandlePart = CandlePart.CLOSE lookback_periods = 12 - quotes = Quote.use(quotes * 1000, candle_part) # Error occurs if not assigned to local var. + quotes = Quote.use( + quotes * 1000, candle_part + ) # Error occurs if not assigned to local var. results = CsIndicator.GetSma(quotes, lookback_periods) benchmark(SMAResults, results, SMAResult) def test_benchmark_converting_to_CsDecimal(self, benchmark, raw_data): from stock_indicators._cstypes import Decimal as CsDecimal + raw_data = raw_data * 1000 def convert_to_quotes(rows): diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 62504987..30c558ea 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -40,7 +40,7 @@ GEM ffi (>= 1.15.0) eventmachine (1.2.7) execjs (2.10.0) - faraday (2.13.4) + faraday (2.14.0) faraday-net_http (>= 2.0, < 3.5) json logger @@ -261,7 +261,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rexml (3.4.1) + rexml (3.4.2) rouge (3.30.0) rubyzip (2.4.1) safe_yaml (1.0.5) @@ -286,7 +286,7 @@ GEM unicode-display_width (1.8.0) uri (1.0.3) wdm (0.2.0) - webrick (1.9.1) + webrick (1.9.2) PLATFORMS x64-mingw-ucrt diff --git a/docs/_indicators/AtrStop.md b/docs/_indicators/AtrStop.md index bcd6979e..a31a03ba 100644 --- a/docs/_indicators/AtrStop.md +++ b/docs/_indicators/AtrStop.md @@ -89,7 +89,6 @@ Created by Welles Wilder, the ATR Trailing Stop indicator attempts to determine ![chart for {{page.title}}]({{site.dotnet.charts}}/AtrStop.png) - ### Sources - [C# core]({{site.dotnet.src}}/a-d/AtrStop/AtrStop.Series.cs) diff --git a/docs/contributing.md b/docs/contributing.md index 5f9b786c..7e1b7a00 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -50,11 +50,9 @@ For new features, submit an issue with the `enhancement` label. ## Development Environment (Quick Setup) -- Recommended tools: Git, Node.js, npm, Docker, Python, Docker Desktop, Visual Studio Code (see `.vscode/extensions.json` for recommended extensions). -- This project supports [VS Code Dev Containers](https://code.visualstudio.com/docs/remote/containers) for a consistent development environment. Open the project in VS Code and select "Reopen in Container" (requires the Remote - Containers extension). -- You can test GitHub Actions workflows locally using [`act`](https://github.com/nektos/act), which is preinstalled in the devcontainer. Example: `act -l` to list workflows, `act` to run all workflows. - -For more details, see the [official documentation](https://github.com/nektos/act#readme) and the project README. +- Recommended tools: Git, Python 3.8+, Docker (optional), and Visual Studio Code (see `.vscode/extensions.json` for recommended extensions). +- This project supports [VS Code Dev Containers](https://code.visualstudio.com/docs/remote/containers) for a consistent development environment. Open the project in VS Code and select "Reopen in Container" (requires the Dev Containers extension). +- Local installs are plain `pip + venv`; no Poetry/Conda/Hatch required. --- @@ -68,10 +66,10 @@ For more details, see the [official documentation](https://github.com/nektos/act ### Windows Setup -1. Install .NET SDK (6.0 or newer): +1. Install .NET SDK (8.0 or newer): ```powershell - winget install Microsoft.DotNet.SDK.6 + winget install Microsoft.DotNet.SDK.8 # Or download from https://dotnet.microsoft.com/download ``` @@ -80,13 +78,15 @@ For more details, see the [official documentation](https://github.com/nektos/act ```powershell git clone https://github.com/facioquo/stock-indicators-python.git cd stock-indicators-python - pip install -r requirements.txt - pip install -r requirements-test.txt + python -m venv .venv + .venv\Scripts\python -m pip install --upgrade pip + .venv\Scripts\python -m pip install -e . + .venv\Scripts\python -m pip install -r requirements-test.txt ``` ### macOS Setup -1. Install .NET SDK (6.0 or newer): +1. Install .NET SDK (8.0 or newer): ```bash brew install dotnet-sdk @@ -97,13 +97,16 @@ For more details, see the [official documentation](https://github.com/nektos/act ```bash git clone https://github.com/facioquo/stock-indicators-python.git cd stock-indicators-python - pip install -r requirements.txt - pip install -r requirements-test.txt + python -m venv .venv + source .venv/bin/activate + python -m pip install --upgrade pip + python -m pip install -e . + python -m pip install -r requirements-test.txt ``` ## Testing -- We use [pytest](https://docs.pytest.org) for testing. +- We use [Ruff](https://docs.astral.sh/ruff/) for linting/formatting, [Pyright](https://microsoft.github.io/pyright/) for type checking, and [pytest](https://docs.pytest.org) for tests. `pip-audit` runs in CI. - Review the `tests` folder for examples of unit tests. Just copy one of these. - New indicators should be tested against manually calculated, proven, accurate results. It is helpful to include your manual calculations spreadsheet in the appropriate indicator folder when [submitting changes](#submitting-changes). - Historical Stock Quotes are automatically added as pytest fixtures. The various `.csv` files in the `samples` folder are used in the unit tests. See `tests/conftest.py` for their usage. A `History.xlsx` Excel file is also included in the `samples` folder that contains the same information but separated by sheets. Use this for your manual calculations to ensure that it is correct. Do not commit changes to this Excel file. @@ -112,41 +115,38 @@ For more details, see the [official documentation](https://github.com/nektos/act ### Running Tests +Common commands (after activating `.venv`): + ```bash -# install core dependencies -pip install -r requirements.txt +# lint and format +python -m ruff check . +python -m ruff format --check . -# install dependencies -pip install -r requirements-test.txt +# type-check +python -m pyright # run standard unit tests -pytest +python -m pytest ``` -To run different types of tests, use the following commands: - -- **Normal unit tests** (default): - - ```bash - pytest - ``` +To run different types of tests: - **Non-standard `localization` tests**: ```bash - pytest -m "localization" + python -m pytest -m "localization" ``` - **Performance tests**: ```bash - pytest -m "performance" + python -m pytest -m "performance" ``` - **All tests** (not recommended): ```bash - pytest -m "" + python -m pytest -m "" ``` You can also use the `-svr A` arguments with pytest to get more detailed output: @@ -161,14 +161,14 @@ pytest -svr A ### Performance benchmarking -Running the commands below in your console will show performance data. You can find the latest results [here]({{site.baseurl}}/performance/). +Running the commands below in your console will produce [benchmark performance data](https://python.stockindicators.dev/performance/) that we include on our documentation site. ```bash # install dependencies -pip install -r requirements-test.txt +python -m pip install -r requirements-test.txt # run performance tests -pytest -m "performance" +python -m pytest -m "performance" ``` ## Documentation diff --git a/docs/pages/guide.md b/docs/pages/guide.md index 1cc644c1..53c9fe5f 100644 --- a/docs/pages/guide.md +++ b/docs/pages/guide.md @@ -31,11 +31,11 @@ layout: page > Install **Python** and the **.NET SDK**. Use the latest versions for better performance. | Installer | Min | Latest | Download | - |---| :---: | :---: | --- | - | Python | 3.8 | 3.12 | [@python.org](https://www.python.org/downloads/) | - | .NET SDK | 6.0 | 8.0 | [@microsoft.com](https://dotnet.microsoft.com/en-us/download) | + | --- | :---: | :---: | --- | + | Python | 3.8 | 3.13 | [@python.org](https://www.python.org/downloads/) | + | .NET SDK | 8.0 | 10.0 | [@microsoft.com](https://dotnet.microsoft.com/en-us/download) | - Note: we do not support the open source [Mono .NET Framework](https://www.mono-project.com). + Note: we do not support the open source [Mono .NET Framework](https://www.mono-project.com). Python 3.14+ is not yet supported due to `pythonnet` compatibility. 2. Install the **stock-indicators** Python package into your environment. @@ -127,7 +127,7 @@ from stock_indicators.indicators.common.quote import Quote [[source]](https://github.com/facioquo/stock-indicators-python/blob/main/stock_indicators/indicators/common/quote.py) | name | type | notes | -| -- |-- |-- | +| ---- | ---- | ----- | | date | [`datetime.datetime`](https://docs.python.org/3.8/library/datetime.html#datetime.datetime) | Date | | open | [`decimal.Decimal`](https://docs.python.org/3.8/library/decimal.html?highlight=decimal#decimal.Decimal), Optional | Open price | | high | [`decimal.Decimal`](https://docs.python.org/3.8/library/decimal.html?highlight=decimal#decimal.Decimal), Optional | High price | @@ -289,7 +289,7 @@ from stock_indicators.indicators.common.enums import Match ``` | type | description | -|-- |:-- | +| ---- | :---------- | | `Match.BULL_CONFIRMED` | Confirmation of a prior bull Match | | `Match.BULL_SIGNAL` | Matching bullish pattern | | `Match.BULL_BASIS` | Bars supporting a bullish Match | @@ -303,23 +303,23 @@ from stock_indicators.indicators.common.enums import Match The `CandleProperties` class is an extended version of `Quote`, and contains additional calculated properties. -| name | type | notes | -| -- |-- |-- | -| `date` | datetime | Date | -| `open` | Decimal | Open price | -| `high` | Decimal | High price | -| `low` | Decimal | Low price | -| `close` | Decimal | Close price | -| `volume` | Decimal | Volume | -| `size` | Decimal, Optional | `high-low` | -| `body` | Decimal, Optional | `|open-close|` | -| `upper_wick` | Decimal, Optional | Upper wick size | -| `lower_wick` | Decimal, Optional | Lower wick size | -| `body_pct` | float, Optional | `body/size` | -| `upper_wick_pct` | float, Optional | `upper_wick/size` | -| `lower_wick_pct` | float, Optional | `lower_wick/size` | -| `is_bullish` | bool | `close>open` direction | -| `is_bearish` | bool | `closeopen` direction | +| `is_bearish` | bool | `close=3.0.0", + "pythonnet>=3.0.5", "typing_extensions>=4.4.0" ] keywords = [ @@ -61,3 +61,56 @@ exclude = ["tests*", "benchmarks*", "test_data*", "docs*"] # can be empty if no extra settings are needed, presence enables setuptools_scm [tool.setuptools_scm] local_scheme = "no-local-version" + +[tool.ruff] +target-version = "py38" +line-length = 88 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # import sorting + "B", # flake8-bugbear + "UP", # pyupgrade + "C4", # flake8-comprehensions + "TID",# flake8-tidy-imports + "PT", # flake8-pytest-style + "RUF" # Ruff-specific rules +] +ignore = [ + "B905", # allow zip without strict length check on py38 support floor + "E501" # allow longer docstrings/comments +] + +[tool.ruff.lint.isort] +known-first-party = ["stock_indicators"] + +[tool.ruff.lint.per-file-ignores] +"stock_indicators/__init__.py" = ["F401", "F403"] +"stock_indicators/_cslib/__init__.py" = ["F401"] +"stock_indicators/_cstypes/__init__.py" = ["F401"] +"stock_indicators/indicators/__init__.py" = ["F401"] + +[tool.ruff.format] +quote-style = "double" + +[tool.pyright] +include = ["stock_indicators", "tests"] +pythonVersion = "3.8" +typeCheckingMode = "standard" +useLibraryCodeForTypes = true +reportMissingImports = "none" +reportMissingModuleSource = "none" +reportMissingTypeStubs = "none" +stubPath = "typings" +exclude = ["typings"] +reportInvalidTypeForm = "none" +reportAttributeAccessIssue = "none" +reportOperatorIssue = "none" +reportReturnType = "none" +reportGeneralTypeIssues = "none" +reportArgumentType = "none" +reportInconsistentOverload = "none" +reportIndexIssue = "none" +reportCallIssue = "none" diff --git a/requirements-test.txt b/requirements-test.txt index 48573f14..90f51398 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,3 +3,6 @@ pytest pytest-cov pytest-benchmark backports.zoneinfo; python_version < '3.9' +ruff +pyright +pip-audit diff --git a/requirements.txt b/requirements.txt index 9045a0c0..91659dac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ # _cslib -pythonnet>=3.0.0 +pythonnet>=3.0.5 typing_extensions>=4.4.0 diff --git a/stock_indicators/__init__.py b/stock_indicators/__init__.py index f2d5deb1..c7561952 100644 --- a/stock_indicators/__init__.py +++ b/stock_indicators/__init__.py @@ -15,4 +15,11 @@ """ from stock_indicators import indicators +from stock_indicators.exceptions import ( + IndicatorCalculationError, + StockIndicatorsError, + StockIndicatorsInitializationError, + TypeConversionError, + ValidationError, +) from stock_indicators.indicators.common import * diff --git a/stock_indicators/_cslib/__init__.py b/stock_indicators/_cslib/__init__.py index defaa45b..90d3b8b0 100644 --- a/stock_indicators/_cslib/__init__.py +++ b/stock_indicators/_cslib/__init__.py @@ -1,96 +1,158 @@ """ Skender.Stock.Indicators ~~~~~~~~~~~~~~~~~~~~~~~~ +# pylint: disable=duplicate-code # Property patterns are expected to repeat -This module loads `Skender.Stock.Indicators.dll`(v2.6.1), which is a compiled library +This module loads `Skender.Stock.Indicators.dll`(v2.7.1), which is a compiled library package from , written in C#. -The target framework of dll is `.NET 6.0`. +The target framework of dll is `.NET 8.0`. """ import logging import platform from pathlib import Path +from typing import Any from pythonnet import load -# Setup logging +from stock_indicators.exceptions import StockIndicatorsInitializationError from stock_indicators.logging_config import configure_logging +# Setup logging configure_logging(debug=False) # Set to True if you need debug this module logger = logging.getLogger(__name__) -try: - # Load CLR - load(runtime="coreclr") - import clr - logger.debug("CLR loaded successfully on %s", platform.system()) - # Get absolute paths - base_path = Path(__file__).parent.resolve() - dll_path = base_path / "lib" / "Skender.Stock.Indicators.dll" - - # Set assembly resolve path - from System import IO, AppDomain +def _initialize_clr() -> Any: + """Initialize the CLR runtime.""" + try: + load(runtime="coreclr") + import clr as clr_module + + logger.debug("CLR loaded successfully on %s", platform.system()) + return clr_module + except Exception as e: + init_error_msg = ( + "Failed to load .NET CLR runtime.\n" + "Please ensure .NET 8.0+ is installed: https://dotnet.microsoft.com/download\n" + f"Platform: {platform.system()}\n" + f"Error: {e!s}" + ) + raise StockIndicatorsInitializationError(init_error_msg) from e + + +def _setup_assembly_probing(assembly_dll_path: Path) -> None: + """Setup assembly probing path for .NET dependency resolution.""" + try: + from System import IO, AppDomain + + current_domain = AppDomain.CurrentDomain + assembly_path = IO.Path.GetDirectoryName(str(assembly_dll_path)) + current_domain.SetData("PROBING_PATH", assembly_path) + logger.debug("Set assembly probing path to: %s", assembly_path) + except Exception as e: # pylint: disable=broad-exception-caught + # Broad exception catch is necessary for C# interop + logger.warning("Failed to set assembly probing path: %s", e) - current_domain = AppDomain.CurrentDomain - assembly_path = IO.Path.GetDirectoryName(str(dll_path)) - current_domain.SetData("PROBING_PATH", assembly_path) - logger.debug("Set assembly probing path to: %s", assembly_path) +def _load_assembly(assembly_dll_path: Path): + """Load the Stock Indicators assembly.""" try: - # Load the assembly first from System.Reflection import Assembly - logger.debug("Loading assembly from: %s", dll_path) - assembly = Assembly.LoadFile(str(dll_path)) - logger.debug("Assembly loaded: %s", assembly.FullName) + if not assembly_dll_path.exists(): + raise FileNotFoundError(f"Assembly not found at: {assembly_dll_path}") + + logger.debug("Loading assembly from: %s", assembly_dll_path) + loaded_assembly = Assembly.LoadFile(str(assembly_dll_path)) + logger.debug("Assembly loaded: %s", loaded_assembly.FullName) - # Add reference after successful load - clr.AddReference(assembly.FullName) # pylint: disable=no-member - logger.debug("Assembly reference added") + return loaded_assembly + except Exception as e: + load_error_msg = ( + f"Failed to load Stock Indicators assembly from: {assembly_dll_path}\n" + "Please ensure the .NET assembly is present and compatible.\n" + f"Error: {e!s}" + ) + raise StockIndicatorsInitializationError(load_error_msg) from e - except Exception as asm_error: - logger.error("Error loading assembly: %s", str(asm_error)) - if hasattr(asm_error, "LoaderExceptions"): - for ex in asm_error.LoaderExceptions: - logger.error("Loader exception: %s", str(ex)) - raise + +def _add_assembly_reference(loaded_assembly, clr_module) -> None: + """Add reference to the loaded assembly.""" + try: + clr_module.AddReference(loaded_assembly.FullName) # pylint: disable=no-member + logger.debug("Assembly reference added successfully") + except Exception as e: + ref_error_msg = ( + f"Failed to add reference to assembly: {loaded_assembly.FullName}\n" + f"Error: {e!s}" + ) + raise StockIndicatorsInitializationError(ref_error_msg) from e + + +try: + # Initialize CLR + clr = _initialize_clr() + + # Get assembly path + base_path = Path(__file__).parent.resolve() + dll_path = base_path / "lib" / "Skender.Stock.Indicators.dll" + + # Setup assembly probing + _setup_assembly_probing(dll_path) + + # Load assembly + assembly = _load_assembly(dll_path) + + # Add assembly reference + _add_assembly_reference(assembly, clr) except Exception as e: - logger.error("Detailed error information: %s", str(e)) - if hasattr(e, "__cause__") and e.__cause__ is not None: - logger.error("Caused by: %s", str(e.__cause__)) + # Re-raise our custom exception or wrap unexpected errors + if not isinstance(e, StockIndicatorsInitializationError): + error_msg = ( + "Stock Indicators initialization failed due to unexpected error.\n" + "Please ensure .NET 8.0+ is installed: https://dotnet.microsoft.com/download\n" + f"Error: {e!s}" + ) + raise StockIndicatorsInitializationError(error_msg) from e + raise + +# Library modules (common) - Import after successful initialization +try: + from Skender.Stock.Indicators import BetaType as CsBetaType + from Skender.Stock.Indicators import CandlePart as CsCandlePart + from Skender.Stock.Indicators import CandleProperties as CsCandleProperties + from Skender.Stock.Indicators import ChandelierType as CsChandelierType + from Skender.Stock.Indicators import EndType as CsEndType + from Skender.Stock.Indicators import Indicator as CsIndicator + from Skender.Stock.Indicators import Match as CsMatch + from Skender.Stock.Indicators import MaType as CsMaType + from Skender.Stock.Indicators import PeriodSize as CsPeriodSize + from Skender.Stock.Indicators import PivotPointType as CsPivotPointType + from Skender.Stock.Indicators import PivotTrend as CsPivotTrend + from Skender.Stock.Indicators import Quote as CsQuote + from Skender.Stock.Indicators import QuoteUtility as CsQuoteUtility + from Skender.Stock.Indicators import ResultBase as CsResultBase + from Skender.Stock.Indicators import ResultUtility as CsResultUtility + + # Built-in System types + from System import DateTime as CsDateTime + from System import Decimal as CsDecimal + from System import Enum as CsEnum + from System.Collections.Generic import IEnumerable as CsIEnumerable + from System.Collections.Generic import List as CsList + from System.Globalization import CultureInfo as CsCultureInfo + from System.Globalization import NumberStyles as CsNumberStyles + + logger.info("Stock Indicators library initialized successfully") + +except ImportError as e: error_msg = ( - "Stock Indicators initialization failed.\n" - "Please ensure .NET 6.0+ is installed: https://dotnet.microsoft.com/download\n" - f"Error: {str(e)}" + "Failed to import Stock Indicators types after successful assembly loading.\n" + "This may indicate a version mismatch or missing dependencies.\n" + f"Error: {e!s}" ) - raise ImportError(error_msg) from e - -# Library modules (common) -from Skender.Stock.Indicators import BetaType as CsBetaType -from Skender.Stock.Indicators import CandlePart as CsCandlePart -from Skender.Stock.Indicators import CandleProperties as CsCandleProperties -from Skender.Stock.Indicators import ChandelierType as CsChandelierType -from Skender.Stock.Indicators import EndType as CsEndType -from Skender.Stock.Indicators import Indicator as CsIndicator -from Skender.Stock.Indicators import Match as CsMatch -from Skender.Stock.Indicators import MaType as CsMaType -from Skender.Stock.Indicators import PeriodSize as CsPeriodSize -from Skender.Stock.Indicators import PivotPointType as CsPivotPointType -from Skender.Stock.Indicators import PivotTrend as CsPivotTrend -from Skender.Stock.Indicators import Quote as CsQuote -from Skender.Stock.Indicators import QuoteUtility as CsQuoteUtility -from Skender.Stock.Indicators import ResultBase as CsResultBase -from Skender.Stock.Indicators import ResultUtility as CsResultUtility - -# Built-in -from System import DateTime as CsDateTime -from System import Decimal as CsDecimal -from System import Enum as CsEnum -from System.Collections.Generic import IEnumerable as CsIEnumerable -from System.Collections.Generic import List as CsList -from System.Globalization import CultureInfo as CsCultureInfo -from System.Globalization import NumberStyles as CsNumberStyles + raise StockIndicatorsInitializationError(error_msg) from e diff --git a/stock_indicators/_cslib/lib/Skender.Stock.Indicators.dll b/stock_indicators/_cslib/lib/Skender.Stock.Indicators.dll index 5351569df45c68b08cd7a3f8ac9fdc0336c8fe42..454e6c608872fdc723381ee5a44256b47b881d0d 100644 GIT binary patch literal 218112 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%0AK8* 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 W*b?!@nV&v=$0h&&0{ev-QP+_Rta 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 Vi}<~~H=g+KxQ~C+wGBt${|BXex<&v1 diff --git a/stock_indicators/_cslib/lib/Skender.Stock.Indicators.xml b/stock_indicators/_cslib/lib/Skender.Stock.Indicators.xml new file mode 100644 index 00000000..23d4382f --- /dev/null +++ b/stock_indicators/_cslib/lib/Skender.Stock.Indicators.xml @@ -0,0 +1,2742 @@ + + + + Skender.Stock.Indicators + + + + + Accumulation/Distribution Line (ADL) is a rolling accumulation of Chaikin Money Flow Volume. + + See + documentation + for more information. + + + Configurable Quote type. See Guide for more information. + Historical price quotes. + Optional. Number of periods in the moving average of ADL. + Time series of ADL values. + Invalid parameter value provided. + + + + + Directional Movement Index (DMI) and Average Directional Movement Index (ADX) is a measure of price directional movement. + It includes upward and downward indicators, and is often used to measure strength of trend. + + See + documentation + for more information. + + + Configurable Quote type. See Guide for more information. + Historical price quotes. + Number of periods in the lookback window. + Time series of ADX and Plus/Minus Directional values. + Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + + Indicator + results to evaluate. + Time + series of results, pruned. + + + + + Williams Alligator is an indicator that transposes multiple moving averages, + showing chart patterns that creator Bill Williams compared to an alligator's + feeding habits when describing market movement. + + See + documentation + for more information. + + + Configurable Quote type. See Guide for more information. + Historical price quotes. + Lookback periods for the Jaw line. + Offset periods for the Jaw line. + Lookback periods for the Teeth line. + Offset periods for the Teeth line. + Lookback periods for the Lips line. + Offset periods for the Lips line. + Time series of Alligator values. + Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + + Indicator results to evaluate. + Time series of + indicator results, condensed. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Arnaud Legoux Moving Average (ALMA) is a Gaussian distribution + weighted moving average of price over a lookback window. + + See + documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Adjusts smoothness versus responsiveness. +Defines the width of the Gaussian normal distribution. +Time series of ALMA values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Aroon is a simple oscillator view of how long the new high or low price occured over a lookback window. + + See + documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of Aroon Up/Down and Oscillator values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Average True Range (ATR) is a measure of volatility that captures gaps and limits between periods. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of ATR values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + ATR Trailing Stop attempts to determine the primary trend of prices by using + Average True Range (ATR) band thresholds. It can indicate a buy/sell signal or a + trailing stop when the trend changes. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for ATR. +Multiplier sets the ATR band width. +Sets basis for stop offsets (Close or High/Low). +Time series of ATR Trailing Stop values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Awesome Oscillator (aka Super AO) is a measure of the gap between a fast and slow period modified moving average. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the Fast moving average. +Number of periods in the Slow moving average. +Time series of Awesome Oscillator values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + A simple quote transform. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +The OHLCV element or simply calculated value type. +Time series of Basic Quote values. +Invalid candle part provided. + + + + + Beta shows how strongly one stock responds to systemic volatility of the entire market. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes for Evaluation. +Historical price quotes for Market. +Number of periods in the lookback window. +Type of Beta to calculate. +Time series of Beta values. +Invalid parameter value provided. +Invalid quotes provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Bollinger Bands® depict volatility as standard deviation boundary lines from a moving average of price. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Width of bands. Number of Standard Deviations from the moving average. +Time series of Bollinger Band and %B values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Balance of Power (aka Balance of Market Power) is a momentum oscillator that depicts the strength of buying and selling pressure. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for smoothing. +Time series of BOP values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Commodity Channel Index (CCI) is an oscillator depicting deviation from typical price range, often used to identify cyclical trends. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of CCI values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Chaikin Oscillator is the difference between fast and slow Exponential Moving Averages (EMA) of the Accumulation/Distribution Line (ADL). + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the ADL fast EMA. +Number of periods for the ADL slow EMA. +Time series of Chaikin Oscillator, Money Flow Volume, and ADL values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Chandelier Exit is typically used for stop-loss and can be computed for both long or short types. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Multiplier. +Short or Long variant selection. +Time series of Chandelier Exit values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Choppiness Index (CHOP) measures the trendiness or choppiness over N lookback periods + on a scale of 0 to 100. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of CHOP values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Chaikin Money Flow (CMF) is the simple moving average of Money Flow Volume (MFV). + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the MFV moving average. +Time series of Chaikin Money Flow and MFV values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + The Chande Momentum Oscillator is a momentum indicator depicting the weighted percent of higher prices in financial markets. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of CMO values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + ConnorsRSI is a composite oscillator that incorporates RSI, winning/losing streaks, and percentile gain metrics on scale of 0 to 100. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the RSI. +Number of periods for streak RSI. +Number of periods for the percentile ranking. +Time series of ConnorsRSI, RSI, Streak RSI, and Percent Rank values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Correlation Coefficient between two quote histories, based on price. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes A for comparison. +Historical price quotes B for comparison. +Number of periods in the lookback window. + + Time series of Correlation Coefficient values. + R², Variance, and Covariance are also included. + +Invalid parameter value provided. +Invalid quotes provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Double Exponential Moving Average (DEMA) of the price. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of Double EMA values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Doji is a single candlestick pattern where open and close price are virtually identical, representing market indecision. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Optional. Maximum absolute percent difference in open and close price. +Time series of Doji values. +Invalid parameter value provided. + + + + + Doji is a single candlestick pattern where open and close price are virtually identical, representing market indecision. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Optional. Maximum absolute percent difference in open and close price. +Time series of Doji values. +Invalid parameter value provided. + + + + + Donchian Channels, also called Price Channels, are derived from highest High and lowest Low values over a lookback window. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of Donchian Channel values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Detrended Price Oscillator (DPO) depicts the difference between price and an offset simple moving average. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of DPO values. +Invalid parameter value provided. + + + + + McGinley Dynamic is a more responsive variant of exponential moving average. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Optional. Range adjustment factor. +Time series of Dynamic values. +Invalid parameter value provided. + + + + + The Elder-ray Index depicts buying and selling pressure, also known as Bull and Bear Power. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the EMA. +Time series of Elder-ray Index values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Exponential Moving Average (EMA) of price or any other specified OHLCV element. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of EMA values. +Invalid parameter value provided. + + + + + Extablish a streaming base for Exponential Moving Average (EMA). + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +EMA base that you can add Quotes to with the .Add(quote) method. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Endpoint Moving Average (EPMA), also known as Least Squares Moving Average (LSMA), plots the projected last point of a linear regression lookback window. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of Endpoint Moving Average values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Fractal Chaos Bands outline high and low price channels to depict broad less-chaotic price movements. FCB is a channelized depiction of Williams Fractals. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of span periods in the evaluation window. +Time series of Fractal Chaos Band and Oscillator values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Ehlers Fisher Transform converts prices into a Gaussian normal distribution. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of Fisher Transform values. +Invalid parameter value provided. + + + + + The Force Index depicts volume-based buying and selling pressure. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the EMA of Force Index. +Time series of Force Index values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Williams Fractal is a retrospective price pattern that identifies a central high or low point over a lookback window. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of span periods to the left and right of the evaluation period. +Determines use of Close or High/Low wicks for points. +Time series of Williams Fractal Bull/Bear values. +Invalid parameter value provided. + + + + + Williams Fractal is a retrospective price pattern that identifies a central high or low point over a lookback window. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of span periods to the left of the evaluation period. +Number of span periods to the right of the evaluation period. +Determines use of Close or High/Low wicks for points. +Time series of Williams Fractal Bull/Bear values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + + Gator Oscillator is an expanded view of Williams Alligator. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Time series of Gator values. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Heikin-Ashi is a modified candlestick pattern that uses prior day for smoothing. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Time series of Heikin-Ashi candlestick values. + + + + + Hull Moving Average (HMA) is a modified weighted average of price over N lookback periods that reduces lag. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of HMA values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Hilbert Transform Instantaneous Trendline (HTL) is a 5-period trendline of high/low price that uses signal processing to reduce noise. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Time series of HTL values and smoothed price. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Hurst Exponent is a measure of randomness, trending, and mean-reverting tendencies of incremental return values. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of lookback periods. +Time series of Hurst Exponent values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Ichimoku Cloud, also known as Ichimoku Kinkō Hyō, is a collection of indicators that depict support and resistance, momentum, and trend direction. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the Tenkan-sen midpoint evaluation. +Number of periods in the shorter Kijun-sen midpoint evaluation. This value is also used to offset Senkou and Chinkou spans. +Number of periods in the longer Senkou leading span B midpoint evaluation. +Time series of Ichimoku Cloud values. +Invalid parameter value provided. + + + + + Ichimoku Cloud, also known as Ichimoku Kinkō Hyō, is a collection of indicators that depict support and resistance, momentum, and trend direction. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the Tenkan-sen midpoint evaluation. +Number of periods in the shorter Kijun-sen midpoint evaluation. +Number of periods in the longer Senkou leading span B midpoint evaluation. +Number of periods to displace the Senkou and Chikou Spans. +Time series of Ichimoku Cloud values. +Invalid parameter value provided. + + + + + Ichimoku Cloud, also known as Ichimoku Kinkō Hyō, is a collection of indicators that depict support and resistance, momentum, and trend direction. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the Tenkan-sen midpoint evaluation. +Number of periods in the shorter Kijun-sen midpoint evaluation. +Number of periods in the longer Senkou leading span B midpoint evaluation. +Number of periods to displace the Senkou Spans. +Number of periods in displace the Chikou Span. +Time series of Ichimoku Cloud values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + + Kaufman’s Adaptive Moving Average (KAMA) is an volatility adaptive moving average of price over configurable lookback periods. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of Efficiency Ratio (volatility) periods. +Number of periods in the Fast EMA. +Number of periods in the Slow EMA. +Time series of KAMA values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Keltner Channels are based on an EMA centerline and ATR band widths. See also STARC Bands for an SMA centerline equivalent. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the centerline EMA. +ATR multiplier sets the width of the channel. +Number of periods in the ATR evaluation. +Time series of Keltner Channel values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Klinger Oscillator depicts volume-based divergence between short and long-term money flow. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the short EMA. +Number of periods for the long EMA. +Number of periods Signal line. +Time series of Klinger Oscillator values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Moving Average Convergence/Divergence (MACD) is a simple oscillator view of two converging/diverging exponential moving averages. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the Fast EMA. +Number of periods in the Slow EMA. +Number of periods for the Signal moving average. +Time series of MACD values, including MACD, Signal, and Histogram. +Invalid parameter value provided. + + + + + Moving Average Envelopes is a price band overlay that is offset from the moving average of price over a lookback window. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Percent offset for envelope width. +Moving average type (e.g. EMA, HMA, TEMA, etc.). +Time series of MA Envelopes values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + + MESA Adaptive Moving Average (MAMA) is a 5-period adaptive moving average of high/low price. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Fast limit threshold. +Slow limit threshold. +Time series of MAMA values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Marubozu is a single candlestick pattern that has no wicks, representing consistent directional movement. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Optional. Minimum candle body size as percentage. +Time series of Marubozu values. +Invalid parameter value provided. + + + + + Marubozu is a single candlestick pattern that has no wicks, representing consistent directional movement. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Optional. Minimum candle body size as percentage. +Time series of Marubozu values. +Invalid parameter value provided. + + + + + Money Flow Index (MFI) is a price-volume oscillator that shows buying and selling momentum. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of MFI values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + On-balance Volume (OBV) is a rolling accumulation of volume based on Close price direction. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Optional. Number of periods for an SMA of the OBV line. +Time series of OBV values. +Invalid parameter value provided. + + + + + Parabolic SAR (stop and reverse) is a price-time based indicator used to determine trend direction and reversals. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Incremental step size. +Maximum step threshold. +Time series of Parabolic SAR values. +Invalid parameter value provided. + + + + + Parabolic SAR (stop and reverse) is a price-time based indicator used to determine trend direction and reversals. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Incremental step size. +Maximum step threshold. +Initial starting acceleration factor. +Time series of Parabolic SAR values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Pivot Points depict support and resistance levels, based on the prior lookback window. You can specify window size (e.g. month, week, day, etc). + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Calendar size of the lookback window. +Pivot Point type. +Time series of Pivot Points values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Pivots is an extended version of Williams Fractal that includes identification of Higher High, Lower Low, Higher Low, and Lower Low trends between pivots in a lookback window. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of span periods to the left of the evaluation period. +Number of span periods to the right of the evaluation period. +Number of periods in the lookback window. +Determines use of Close or High/Low wicks for points. +Time series of Pivots values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + + Price Momentum Oscillator (PMO) is double-smoothed ROC based momentum indicator. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for ROC EMA smoothing. +Number of periods for PMO EMA smoothing. +Number of periods for Signal line EMA. +Time series of PMO values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Price Relative Strength (PRS), also called Comparative Relative Strength, + shows the ratio of two quote histories. It is often used to compare + against a market index or sector ETF. When using the optional lookbackPeriods, + this also return relative percent change over the specified periods. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes for evaluation. +This is usually market index data, but could be any baseline data that you might use for comparison. +Optional. Number of periods for % difference. +Optional. Number of periods for a PRS SMA signal line. +Time series of PRS values. +Invalid parameter value provided. +Invalid quotes provided. + + + + + Percentage Volume Oscillator (PVO) is a simple oscillator view of two converging/diverging exponential moving averages of Volume. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the Fast moving average. +Number of periods in the Slow moving average. +Number of periods for the PVO SMA signal line. +Time series of PVO values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Renko Chart is a modified Japanese candlestick pattern that uses time-lapsed bricks. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Fixed brick size ($). +End type. See documentation. +Time series of Renko Chart candlestick values. +Invalid parameter value provided. + + + + + The ATR Renko Chart is a modified Japanese candlestick pattern based on Average True Range brick size. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Lookback periods for the ATR evaluation. +End type. See documentation. +Time series of Renko Chart candlestick values. + + + + + Rate of Change (ROC), also known as Momentum Oscillator, is the percent change of price over a lookback window. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Optional. Number of periods for an ROC SMA signal line. +Time series of ROC values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Rate of Change with Bands (ROCWB) is the percent change of price over a lookback window with standard deviation bands. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Number of periods for the ROC EMA line. +Number of periods the standard deviation for upper/lower band lines. +Time series of ROCWB values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Rolling Pivot Points is a modern update to traditional fixed calendar window Pivot Points. + It depicts support and resistance levels, based on a defined rolling window and offset. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the evaluation window. +Number of periods to offset the window from the current period. +Pivot Point type. +Time series of Rolling Pivot Points values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Relative Strength Index (RSI) measures strength of the winning/losing streak over N lookback periods + on a scale of 0 to 100, to depict overbought and oversold conditions. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of RSI values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Slope of the best fit line is determined by an ordinary least-squares simple linear regression on price. + It can be used to help identify trend strength and direction. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of Slope values, including Slope, Standard Deviation, R², and a best-fit Line (for the last lookback segment). +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Simple Moving Average (SMA) of the price. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of SMA values. +Invalid parameter value provided. + + + + + Simple Moving Average (SMA) is the average of price over a lookback window. This extended variant includes mean absolute deviation (MAD), mean square error (MSE), and mean absolute percentage error (MAPE). + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of SMA, MAD, MSE, and MAPE values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Stochastic Momentum Index is a double-smoothed variant of the Stochastic Oscillator on a scale from -100 to 100. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the Stochastic lookback. +Number of periods in the first smoothing. +Number of periods in the second smoothing. +Number of periods in the EMA of SMI. +Time series of Stochastic Momentum Index values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Smoothed Moving Average (SMMA) is the average of price over a lookback window using a smoothing method. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of SMMA values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Stoller Average Range Channel (STARC) Bands, are based on an SMA centerline and ATR band widths. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the centerline SMA. +ATR multiplier sets the width of the channel. +Number of periods in the ATR evaluation. +Time series of STARC Bands values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Schaff Trend Cycle is a stochastic oscillator view of two converging/diverging exponential moving averages. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the Trend Cycle. +Number of periods in the Fast EMA. +Number of periods in the Slow EMA. +Time series of MACD values, including MACD, Signal, and Histogram. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Rolling Standard Deviation of price over a lookback window. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Optional. Number of periods in the Standard Deviation SMA signal line. +Time series of Standard Deviations values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Standard Deviation Channels are based on an linear regression centerline and standard deviations band widths. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Size of the evaluation window. +Width of bands. Number of Standard Deviations from the regression line. +Time series of Standard Deviation Channels values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Stochastic Oscillator is a momentum indicator that looks back N periods to produce a scale of 0 to 100. + %J is also included for the KDJ Index extension. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the Oscillator. +Smoothing period for the %D signal line. +Smoothing period for the %K Oscillator. Use 3 for Slow or 1 for Fast. +Time series of Stochastic Oscillator values. +Invalid parameter value provided. + + + + + Stochastic Oscillator is a momentum indicator that looks back N periods to produce a scale of 0 to 100. + %J is also included for the KDJ Index extension. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the Oscillator. +Smoothing period for the %D signal line. +Smoothing period for the %K Oscillator. Use 3 for Slow or 1 for Fast. +Weight of %K in the %J calculation. Default is 3. +Weight of %K in the %J calculation. Default is 2. +Type of moving average to use. Default is MaType.SMA. See docs for instructions and options. +Time series of Stochastic Oscillator values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Stochastic RSI is a Stochastic interpretation of the Relative Strength Index. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the RSI. +Number of periods for the Stochastic. +Number of periods for the Stochastic RSI SMA signal line. +Number of periods for Stochastic Smoothing. Use 1 for Fast or 3 for Slow. +Time series of Stochastic RSI values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + SuperTrend attempts to determine the primary trend of prices by using + Average True Range (ATR) band thresholds around an HL2 midline. It can indicate a buy/sell signal or a + trailing stop when the trend changes. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for ATR. +Multiplier sets the ATR band width. +Time series of SuperTrend values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Tillson T3 is a smooth moving average that reduces both lag and overshooting. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the EMA smoothing. +Size of the Volume Factor. +Time series of T3 values. +Invalid parameter value provided. + + + + + Triple Exponential Moving Average (TEMA) of the price. Note: TEMA is often confused with the alternative TRIX oscillator. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of Triple EMA values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + True Range (TR) is a measure of volatility that captures gaps and limits between periods. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Time series of True Range (TR) values. +Invalid parameter value provided. + + + + + Triple EMA Oscillator (TRIX) is the rate of change for a 3 EMA smoothing of the price over a lookback window. TRIX is often confused with TEMA. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Optional. Number of periods for a TRIX SMA signal line. +Time series of TRIX values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + True Strength Index (TSI) is a momentum oscillator that depicts trends in price changes. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods for the first EMA. +Number of periods in the second smoothing. +Number of periods in the TSI SMA signal line. +Time series of TSI values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Ulcer Index (UI) is a measure of downside price volatility over a lookback window. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of Ulcer Index values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Ultimate Oscillator uses several lookback periods to weigh buying power against True Range price to produce on oversold / overbought oscillator. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the smallest window. +Number of periods in the middle-sized window. +Number of periods in the largest window. +Time series of Ultimate Oscillator values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Volatility Stop is an ATR based indicator used to determine trend direction, stops, and reversals. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +ATR offset amount. +Time series of Volatility Stop values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Vortex Indicator (VI) is a measure of price directional movement. + It includes positive and negative indicators, and is often used to identify trends and reversals. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of VI+ and VI- vortex movement indicator values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Volume Weighted Average Price (VWAP) is a Volume weighted average of price, typically used on intraday data. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Optional anchor date. If not provided, the first date in quotes is used. +Time series of VWAP values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Volume Weighted Moving Average is the volume adjusted average price over a lookback window. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of Volume Weighted Moving Average values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Williams %R momentum indicator is a stochastic oscillator with scale of -100 to 0. It is exactly the same as the Fast variant of Stochastic Oscillator, but with a different scaling. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of Williams %R values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Weighted Moving Average (WMA) is the linear weighted average of price over N lookback periods. This also called Linear Weighted Moving Average (LWMA). + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Number of periods in the lookback window. +Time series of WMA values. +Invalid parameter value provided. + + + + Removes the recommended quantity of results from the beginning of the results list + using a reverse-engineering approach. See + documentation for more information. + +Indicator + results to evaluate. +Time + series of results, pruned. + + + + + Zig Zag is a price chart overlay that simplifies the up and down movements and transitions based on a percent change smoothing threshold. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Determines use of Close or High/Low wicks for extreme points. +Percent price change to set threshold for minimum size movements. +Time series of Zig Zag values. +Invalid parameter value provided. + + + + Removes non-essential records containing null values with unique consideration for + this indicator. See + documentation for more information. + +Indicator results to evaluate. +Time series of + indicator results, condensed. + + + + + Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. + + See +documentation + for more information. + + + + Standard output properties: + + +Oscillator +%K Oscillator over prior lookback periods. + + +Signal +%D Simple moving average of %K Oscillator. + + +PercentJ + + %J is the weighted divergence of %K and %D: %J=3×%K-2×%D + + + + These are the aliases of the above properties: + + +K +Same as Oscillator. + + +D +Same as Signal. + + +J +Same as PercentJ. + + + + + + + Removes a specific quantity from the beginning of the time series list. + + See documentation for more information. + + +Any series type. +Collection to evaluate. +Exact quantity to remove from the beginning of the series. +Time series, pruned. +Invalid parameter value provided. + + + + Finds time series values on a specific date. + + See documentation for more information. + + +Any series type. +Time series to evaluate. +Exact date to lookup. +First + record in the series on the date specified. + + + + + Nullable System. + functions. + + +System.Math infamously does not allow + or handle nullable input values. + Instead of adding repetitive inline defensive code, + we're using these equivalents. Most are simple wrappers. + + + + + Returns the absolute value of a nullable double. + +The nullable double value. +The absolute value, or null if the input is null. + + + + Rounds a nullable decimal value to a specified number of fractional digits. + +The nullable decimal value. +The number of fractional digits. +The rounded value, or null if the input is null. + + + + Rounds a nullable double value to a specified number of fractional digits. + +The nullable double value. +The number of fractional digits. +The rounded value, or null if the input is null. + + + + Rounds a double value to a specified number of fractional digits. + It is an extension alias of + +The double value. +The number of fractional digits. +The rounded value. + + + + Rounds a decimal value to a specified number of fractional digits. + It is an extension alias of + +The decimal value. +The number of fractional digits. +The rounded value. + + + + Converts a nullable double value to NaN if it is null. + +The nullable double value. +The value, or NaN if the input is null. + + + + Converts a nullable decimal value to NaN if it is null. + +The nullable decimal value. +The value as a double, or NaN if the input is null. + + + + Converts a nullable double value to null if it is NaN. + +The nullable double value. +The value, or null if the input is NaN. + + + + Converts a double value to null if it is NaN. + +The double value. +The value, or null if the input is NaN. + + + + Converts historical quotes into larger bar sizes. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +PeriodSize enum representing the new bar size. +Time series of historical quote values. +Invalid parameter value provided. + + + + + Converts historical quotes into larger bar sizes. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +TimeSpan representing the new bar size. +Time series of historical quote values. +Invalid parameter value provided. + + + + + Optionally select which candle part to use in the calculation. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +The OHLCV element or simply calculated value type. +Time series of Quote tuple values. +Invalid candle part provided. + + + + + Validate historical quotes. + + See +documentation + for more information. + + +Configurable Quote type. See Guide for more information. +Historical price quotes. +Time series of historical quote values. +Validation check failed. + + + + + Forces indicator results to have the same date-based records as another result baseline. + + This utility is undocumented. + + +Any indicator result series type to be transformed. +Any indicator result series type to be matched. +The indicator result series to be modified. +The indicator result series to compare for matching. +Synchronization behavior See options in SyncType enum. +Indicator result series, synchronized to a comparator match. + + Invalid parameter value provided. + + + + + Removes non-essential records containing null or NaN values. See + documentation for more information. + +Any result + type. +Indicator results to evaluate. +Time series of indicator results, + condensed. + + + +Converts results into a reusable tuple with warmup periods removed and nulls converted + to NaN. See + documentation for more information. + +Indicator results to evaluate. +Collection of non-nullable tuple time series of results, without null warmup periods. + + + +Converts results into a tuple collection with non-nullable NaN to replace null values. + See + documentation for more information. + +Indicator results to evaluate. +Collection of tuple time series of + results with specified handling of nulls, without pruning. + + + + diff --git a/stock_indicators/_cstypes/__init__.py b/stock_indicators/_cstypes/__init__.py index 260fe8e4..4b9a7588 100644 --- a/stock_indicators/_cstypes/__init__.py +++ b/stock_indicators/_cstypes/__init__.py @@ -2,6 +2,6 @@ from stock_indicators import _cslib -from .datetime import (DateTime, to_pydatetime) -from .decimal import (Decimal, to_pydecimal, to_pydecimal_via_double) -from .list import (List) +from .datetime import DateTime, to_pydatetime +from .decimal import Decimal, to_pydecimal, to_pydecimal_via_double +from .list import List diff --git a/stock_indicators/_cstypes/datetime.py b/stock_indicators/_cstypes/datetime.py index 4fd127e0..e3449933 100644 --- a/stock_indicators/_cstypes/datetime.py +++ b/stock_indicators/_cstypes/datetime.py @@ -1,5 +1,8 @@ -from datetime import datetime as PyDateTime, timezone as PyTimezone +from datetime import datetime as PyDateTime +from datetime import timezone as PyTimezone + from System import DateTimeKind # type: ignore + from stock_indicators._cslib import CsDateTime # type: ignore # Module-level constant: 1 second = 10,000,000 ticks (100ns per tick) diff --git a/stock_indicators/_cstypes/decimal.py b/stock_indicators/_cstypes/decimal.py index 1dd17b4f..3eb8f2a9 100644 --- a/stock_indicators/_cstypes/decimal.py +++ b/stock_indicators/_cstypes/decimal.py @@ -1,6 +1,7 @@ from decimal import Decimal as PyDecimal +from typing import Optional, Union -from stock_indicators._cslib import CsDecimal, CsCultureInfo, CsNumberStyles +from stock_indicators._cslib import CsCultureInfo, CsDecimal, CsNumberStyles class Decimal: @@ -8,7 +9,7 @@ class Decimal: Class for converting a number into C#'s `System.Decimal` class. Parameters: - decimal : `int`, `float` or any `object` that can be represented as a number string. + decimal : `int`, `float`, `PyDecimal`, or any `object` that can be represented as a number string. Example: Constructing `System.Decimal` from `float` of Python. @@ -17,31 +18,75 @@ class Decimal: >>> cs_decimal 2.5 """ - cs_number_styles = CsNumberStyles.AllowDecimalPoint | CsNumberStyles.AllowExponent \ - | CsNumberStyles.AllowLeadingSign | CsNumberStyles.AllowThousands - def __new__(cls, decimal) -> CsDecimal: - return CsDecimal.Parse(str(decimal), cls.cs_number_styles, CsCultureInfo.InvariantCulture) + cs_number_styles = ( + CsNumberStyles.AllowDecimalPoint + | CsNumberStyles.AllowExponent + | CsNumberStyles.AllowLeadingSign + | CsNumberStyles.AllowThousands + ) + def __new__(cls, decimal: Union[int, float, PyDecimal, str, None]) -> CsDecimal: + if decimal is None: + from stock_indicators.exceptions import ValidationError -def to_pydecimal(cs_decimal: CsDecimal) -> PyDecimal: + raise ValidationError("Cannot convert None to C# Decimal") + + # Convert to string first to preserve precision for all numeric types + try: + return CsDecimal.Parse( + str(decimal), cls.cs_number_styles, CsCultureInfo.InvariantCulture + ) + except Exception as e: + from stock_indicators.exceptions import TypeConversionError + + raise TypeConversionError( + f"Cannot convert {decimal} (type: {type(decimal)}) to C# Decimal: {e}" + ) from e + + +def to_pydecimal(cs_decimal: Optional[CsDecimal]) -> Optional[PyDecimal]: """ Converts an object to a native Python decimal object. Parameter: - cs_decimal : `System.Decimal` of C# or any `object` that can be represented as a number. + cs_decimal : `System.Decimal` of C# or None. + + Returns: + Python Decimal object or None if input is None. """ - if cs_decimal is not None: + if cs_decimal is None: + return None + + try: return PyDecimal(cs_decimal.ToString(CsCultureInfo.InvariantCulture)) + except Exception as e: + from stock_indicators.exceptions import TypeConversionError + + raise TypeConversionError( + f"Cannot convert C# Decimal to Python Decimal: {e}" + ) from e -def to_pydecimal_via_double(cs_decimal: CsDecimal) -> PyDecimal: +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 but may have precision loss. + This method offers better performance (~4x faster) but may have precision loss. Parameter: - cs_decimal : `System.Decimal` of C# or any `object` that can be represented as a number. + cs_decimal : `System.Decimal` of C# or None. + + Returns: + Python Decimal object or None if input is None. """ - if cs_decimal is not None: + if cs_decimal is None: + return None + + try: return PyDecimal(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/_cstypes/list.py b/stock_indicators/_cstypes/list.py index 9a37b694..4aa8fdbe 100644 --- a/stock_indicators/_cstypes/list.py +++ b/stock_indicators/_cstypes/list.py @@ -1,4 +1,5 @@ from collections import deque +from typing import Iterable from stock_indicators._cslib import CsList @@ -30,8 +31,20 @@ class List: >>> print(i, end='') 123 """ - def __new__(cls, generic, sequence) -> CsList: - cs_list = CsList[generic]() - deque(map(cs_list.Add, sequence), maxlen=0) - return cs_list + def __new__(cls, generic: object, sequence: Iterable) -> CsList: + if not hasattr(sequence, "__iter__"): + raise TypeError("sequence must be iterable") + + try: + cs_list = CsList[generic]() + + # Always use individual Add calls for reliability + # AddRange has issues with Python.NET type conversion in some cases + deque(map(cs_list.Add, sequence), maxlen=0) + + return cs_list + except Exception as e: + raise ValueError( + f"Cannot convert sequence to C# List[{generic}]: {e}" + ) from e diff --git a/stock_indicators/exceptions.py b/stock_indicators/exceptions.py new file mode 100644 index 00000000..913f1cf8 --- /dev/null +++ b/stock_indicators/exceptions.py @@ -0,0 +1,23 @@ +""" +Custom exceptions for Stock Indicators for Python. +""" + + +class StockIndicatorsError(Exception): + """Base exception class for all Stock Indicators errors.""" + + +class StockIndicatorsInitializationError(ImportError, StockIndicatorsError): + """Raised when the .NET library fails to initialize.""" + + +class TypeConversionError(StockIndicatorsError): + """Raised when conversion between Python and C# types fails.""" + + +class IndicatorCalculationError(StockIndicatorsError): + """Raised when indicator calculation fails.""" + + +class ValidationError(StockIndicatorsError): + """Raised when input validation fails.""" diff --git a/stock_indicators/indicators/__init__.py b/stock_indicators/indicators/__init__.py index d11e5fc6..2a415203 100644 --- a/stock_indicators/indicators/__init__.py +++ b/stock_indicators/indicators/__init__.py @@ -2,91 +2,85 @@ from stock_indicators import _cslib -from .adl import (get_adl) -from .adx import (get_adx) -from .alligator import (get_alligator) -from .alma import (get_alma) -from .aroon import (get_aroon) -from .atr_stop import (get_atr_stop) -from .atr import (get_atr) -from .awesome import (get_awesome) -from .basic_quotes import (get_basic_quote) -from .beta import (get_beta) -from .bollinger_bands import (get_bollinger_bands) -from .bop import (get_bop) -from .cci import (get_cci) -from .chaikin_oscillator import (get_chaikin_osc) -from .chandelier import (get_chandelier) -from .chop import (get_chop) -from .cmf import (get_cmf) -from .cmo import (get_cmo) -from .connors_rsi import (get_connors_rsi) -from .correlation import (get_correlation) -from .doji import (get_doji) -from .donchian import (get_donchian) -from .dema import (get_dema) -from .dpo import (get_dpo) -from .dynamic import (get_dynamic) -from .elder_ray import (get_elder_ray) -from .ema import (get_ema) -from .epma import (get_epma) -from .fcb import (get_fcb) -from .fisher_transform import (get_fisher_transform) -from .force_index import (get_force_index) -from .fractal import (get_fractal) -from .gator import (get_gator) -from .heikin_ashi import (get_heikin_ashi) -from .hma import (get_hma) -from .ht_trendline import (get_ht_trendline) -from .hurst import (get_hurst) -from .ichimoku import (get_ichimoku) -from .kama import (get_kama) -from .keltner import (get_keltner) -from .kvo import (get_kvo) -from .macd import (get_macd) -from .ma_envelopes import (get_ma_envelopes) -from .mama import (get_mama) -from .marubozu import (get_marubozu) -from .mfi import (get_mfi) -from .obv import (get_obv) -from .parabolic_sar import (get_parabolic_sar) -from .pivot_points import (get_pivot_points) -from .pivots import (get_pivots) -from .pmo import (get_pmo) -from .prs import (get_prs) -from .pvo import (get_pvo) -from .renko import ( - get_renko, - get_renko_atr) -from .roc import ( - get_roc, - get_roc_with_band) -from .rolling_pivots import (get_rolling_pivots) -from .rsi import (get_rsi) -from .slope import (get_slope) -from .sma import ( - get_sma, - get_sma_analysis) -from .smi import (get_smi) -from .smma import (get_smma) -from .starc_bands import (get_starc_bands) -from .stc import (get_stc) -from .stdev_channels import (get_stdev_channels) -from .stdev import (get_stdev) -from .stoch import (get_stoch) -from .stoch_rsi import (get_stoch_rsi) -from .super_trend import (get_super_trend) -from .t3 import (get_t3) -from .tema import (get_tema) -from .tr import (get_tr) -from .trix import (get_trix) -from .tsi import (get_tsi) -from .ulcer_index import (get_ulcer_index) -from .ultimate import (get_ultimate) -from .volatility_stop import (get_volatility_stop) -from .vortex import (get_vortex) -from .vwap import (get_vwap) -from .vwma import (get_vwma) -from .williams_r import (get_williams_r) -from .wma import (get_wma) -from .zig_zag import (get_zig_zag) +from .adl import get_adl +from .adx import get_adx +from .alligator import get_alligator +from .alma import get_alma +from .aroon import get_aroon +from .atr import get_atr +from .atr_stop import get_atr_stop +from .awesome import get_awesome +from .basic_quotes import get_basic_quote +from .beta import get_beta +from .bollinger_bands import get_bollinger_bands +from .bop import get_bop +from .cci import get_cci +from .chaikin_oscillator import get_chaikin_osc +from .chandelier import get_chandelier +from .chop import get_chop +from .cmf import get_cmf +from .cmo import get_cmo +from .connors_rsi import get_connors_rsi +from .correlation import get_correlation +from .dema import get_dema +from .doji import get_doji +from .donchian import get_donchian +from .dpo import get_dpo +from .dynamic import get_dynamic +from .elder_ray import get_elder_ray +from .ema import get_ema +from .epma import get_epma +from .fcb import get_fcb +from .fisher_transform import get_fisher_transform +from .force_index import get_force_index +from .fractal import get_fractal +from .gator import get_gator +from .heikin_ashi import get_heikin_ashi +from .hma import get_hma +from .ht_trendline import get_ht_trendline +from .hurst import get_hurst +from .ichimoku import get_ichimoku +from .kama import get_kama +from .keltner import get_keltner +from .kvo import get_kvo +from .ma_envelopes import get_ma_envelopes +from .macd import get_macd +from .mama import get_mama +from .marubozu import get_marubozu +from .mfi import get_mfi +from .obv import get_obv +from .parabolic_sar import get_parabolic_sar +from .pivot_points import get_pivot_points +from .pivots import get_pivots +from .pmo import get_pmo +from .prs import get_prs +from .pvo import get_pvo +from .renko import get_renko, get_renko_atr +from .roc import get_roc, get_roc_with_band +from .rolling_pivots import get_rolling_pivots +from .rsi import get_rsi +from .slope import get_slope +from .sma import get_sma, get_sma_analysis +from .smi import get_smi +from .smma import get_smma +from .starc_bands import get_starc_bands +from .stc import get_stc +from .stdev import get_stdev +from .stdev_channels import get_stdev_channels +from .stoch import get_stoch +from .stoch_rsi import get_stoch_rsi +from .super_trend import get_super_trend +from .t3 import get_t3 +from .tema import get_tema +from .tr import get_tr +from .trix import get_trix +from .tsi import get_tsi +from .ulcer_index import get_ulcer_index +from .ultimate import get_ultimate +from .volatility_stop import get_volatility_stop +from .vortex import get_vortex +from .vwap import get_vwap +from .vwma import get_vwma +from .williams_r import get_williams_r +from .wma import get_wma +from .zig_zag import get_zig_zag diff --git a/stock_indicators/indicators/adl.py b/stock_indicators/indicators/adl.py index 468b6d3a..0b36548a 100644 --- a/stock_indicators/indicators/adl.py +++ b/stock_indicators/indicators/adl.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_adl(quotes: Iterable[Quote], sma_periods: Optional[int] = None): @@ -70,6 +70,8 @@ def adl_sma(self, value): _T = TypeVar("_T", bound=ADLResult) + + class ADLResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of ADL(Accumulation/Distribution Line) results. diff --git a/stock_indicators/indicators/adx.py b/stock_indicators/indicators/adx.py index c7d6639b..a6755cdd 100644 --- a/stock_indicators/indicators/adx.py +++ b/stock_indicators/indicators/adx.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_adx(quotes: Iterable[Quote], lookback_periods: int = 14): @@ -53,6 +53,14 @@ def mdi(self) -> Optional[float]: def mdi(self, value): self._csdata.Mdi = value + @property + def dx(self) -> Optional[float]: + return self._csdata.Dx + + @dx.setter + def dx(self, value): + self._csdata.Dx = value + @property def adx(self) -> Optional[float]: return self._csdata.Adx @@ -71,6 +79,8 @@ def adxr(self, value): _T = TypeVar("_T", bound=ADXResult) + + class ADXResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of ADX(Average Directional Movement Index) results. diff --git a/stock_indicators/indicators/alligator.py b/stock_indicators/indicators/alligator.py index e0aaea70..74512699 100644 --- a/stock_indicators/indicators/alligator.py +++ b/stock_indicators/indicators/alligator.py @@ -3,14 +3,19 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_alligator(quotes: Iterable[Quote], - jaw_periods: int = 13, jaw_offset: int = 8, - teeth_periods: int = 8, teeth_offset: int = 5, - lips_periods: int = 5, lips_offset: int = 3): +def get_alligator( + quotes: Iterable[Quote], # pylint: disable=too-many-positional-arguments + jaw_periods: int = 13, + jaw_offset: int = 8, + teeth_periods: int = 8, + teeth_offset: int = 5, + lips_periods: int = 5, + lips_offset: int = 3, +): """Get Williams Alligator calculated. Williams Alligator is an indicator that transposes multiple moving averages, @@ -47,10 +52,15 @@ def get_alligator(quotes: Iterable[Quote], - [Williams Alligator Reference](https://python.stockindicators.dev/indicators/Alligator/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - alligator_results = CsIndicator.GetAlligator[Quote](CsList(Quote, quotes), - jaw_periods, jaw_offset, - teeth_periods, teeth_offset, - lips_periods, lips_offset) + alligator_results = CsIndicator.GetAlligator[Quote]( + CsList(Quote, quotes), + jaw_periods, + jaw_offset, + teeth_periods, + teeth_offset, + lips_periods, + lips_offset, + ) return AlligatorResults(alligator_results, AlligatorResult) @@ -85,6 +95,8 @@ def lips(self, value): _T = TypeVar("_T", bound=AlligatorResult) + + class AlligatorResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Williams Alligator results. diff --git a/stock_indicators/indicators/alma.py b/stock_indicators/indicators/alma.py index 262c7112..b0ec5366 100644 --- a/stock_indicators/indicators/alma.py +++ b/stock_indicators/indicators/alma.py @@ -7,7 +7,12 @@ from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_alma(quotes: Iterable[Quote], lookback_periods: int = 9, offset: float = .85, sigma : float = 6): +def get_alma( + quotes: Iterable[Quote], + lookback_periods: int = 9, + offset: float = 0.85, + sigma: float = 6, +): """Get ALMA calculated. Arnaud Legoux Moving Average (ALMA) is a Gaussian distribution @@ -34,7 +39,9 @@ def get_alma(quotes: Iterable[Quote], lookback_periods: int = 9, offset: float = - [ALMA Reference](https://python.stockindicators.dev/indicators/Alma/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - alma_results = CsIndicator.GetAlma[Quote](CsList(Quote, quotes), lookback_periods, offset, sigma) + alma_results = CsIndicator.GetAlma[Quote]( + CsList(Quote, quotes), lookback_periods, offset, sigma + ) return ALMAResults(alma_results, ALMAResult) @@ -53,6 +60,8 @@ def alma(self, value): _T = TypeVar("_T", bound=ALMAResult) + + class ALMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of ALMA(Arnaud Legoux Moving Average) results. diff --git a/stock_indicators/indicators/aroon.py b/stock_indicators/indicators/aroon.py index 4cf04184..a64e0cd1 100644 --- a/stock_indicators/indicators/aroon.py +++ b/stock_indicators/indicators/aroon.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_aroon(quotes: Iterable[Quote], lookback_periods: int = 25): @@ -62,6 +62,8 @@ def oscillator(self, value): _T = TypeVar("_T", bound=AroonResult) + + class AroonResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Aroon results. diff --git a/stock_indicators/indicators/atr.py b/stock_indicators/indicators/atr.py index 6e97978c..e029e57b 100644 --- a/stock_indicators/indicators/atr.py +++ b/stock_indicators/indicators/atr.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_atr(quotes: Iterable[Quote], lookback_periods: int = 14): @@ -62,6 +62,8 @@ def atrp(self, value): _T = TypeVar("_T", bound=ATRResult) + + class ATRResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of ATR(Average True Range) results. diff --git a/stock_indicators/indicators/atr_stop.py b/stock_indicators/indicators/atr_stop.py index b2444892..ff18087a 100644 --- a/stock_indicators/indicators/atr_stop.py +++ b/stock_indicators/indicators/atr_stop.py @@ -2,13 +2,13 @@ from typing import Iterable, Optional, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList 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.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_atr_stop( diff --git a/stock_indicators/indicators/awesome.py b/stock_indicators/indicators/awesome.py index 8873d29c..6877790f 100644 --- a/stock_indicators/indicators/awesome.py +++ b/stock_indicators/indicators/awesome.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_awesome(quotes: Iterable[Quote], fast_periods: int = 5, slow_periods: int = 34): @@ -31,7 +31,9 @@ def get_awesome(quotes: Iterable[Quote], fast_periods: int = 5, slow_periods: in - [Awesome Oscillator Reference](https://python.stockindicators.dev/indicators/Awesome/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - awesome_results = CsIndicator.GetAwesome[Quote](CsList(Quote, quotes), fast_periods, slow_periods) + awesome_results = CsIndicator.GetAwesome[Quote]( + CsList(Quote, quotes), fast_periods, slow_periods + ) return AwesomeResults(awesome_results, AwesomeResult) @@ -58,6 +60,8 @@ def normalized(self, value): _T = TypeVar("_T", bound=AwesomeResult) + + class AwesomeResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Awesome Oscillator (aka Super AO) results. diff --git a/stock_indicators/indicators/basic_quotes.py b/stock_indicators/indicators/basic_quotes.py index 5480c1d7..365a8b39 100644 --- a/stock_indicators/indicators/basic_quotes.py +++ b/stock_indicators/indicators/basic_quotes.py @@ -3,11 +3,13 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.enums import CandlePart -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_basic_quote(quotes: Iterable[Quote], candle_part: CandlePart = CandlePart.CLOSE): +def get_basic_quote( + quotes: Iterable[Quote], candle_part: CandlePart = CandlePart.CLOSE +): """Get Basic Quote calculated. A simple quote transform (e.g. HL2, OHL3, etc.) and isolation of individual @@ -28,7 +30,9 @@ def get_basic_quote(quotes: Iterable[Quote], candle_part: CandlePart = CandlePar - [Basic Quote Reference](https://python.stockindicators.dev/indicators/BasicQuote/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetBaseQuote[Quote](CsList(Quote, quotes), candle_part.cs_value) + results = CsIndicator.GetBaseQuote[Quote]( + CsList(Quote, quotes), candle_part.cs_value + ) return BasicQuoteResults(results, BasicQuoteResult) @@ -47,6 +51,8 @@ def jaw(self, value): _T = TypeVar("_T", bound=BasicQuoteResult) + + class BasicQuoteResults(IndicatorResults[_T]): """ A wrapper class for the list of Basic Quote results. diff --git a/stock_indicators/indicators/beta.py b/stock_indicators/indicators/beta.py index 3e317b19..ec2640eb 100644 --- a/stock_indicators/indicators/beta.py +++ b/stock_indicators/indicators/beta.py @@ -4,12 +4,16 @@ from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.enums import BetaType from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_beta(eval_quotes: Iterable[Quote], market_quotes: Iterable[Quote], - lookback_periods: int, beta_type: BetaType = BetaType.STANDARD): +def get_beta( + eval_quotes: Iterable[Quote], + market_quotes: Iterable[Quote], + lookback_periods: int, + beta_type: BetaType = BetaType.STANDARD, +): """Get Beta calculated. Beta shows how strongly one stock responds to systemic volatility of the entire market. @@ -36,8 +40,12 @@ def get_beta(eval_quotes: Iterable[Quote], market_quotes: Iterable[Quote], - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - beta_results = CsIndicator.GetBeta[Quote](CsList(Quote, eval_quotes), CsList(Quote, market_quotes), - lookback_periods, beta_type.cs_value) + beta_results = CsIndicator.GetBeta[Quote]( + CsList(Quote, eval_quotes), + CsList(Quote, market_quotes), + lookback_periods, + beta_type.cs_value, + ) return BetaResults(beta_results, BetaResult) diff --git a/stock_indicators/indicators/bollinger_bands.py b/stock_indicators/indicators/bollinger_bands.py index 9a955ce5..b2b99158 100644 --- a/stock_indicators/indicators/bollinger_bands.py +++ b/stock_indicators/indicators/bollinger_bands.py @@ -3,11 +3,13 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_bollinger_bands(quotes: Iterable[Quote], lookback_periods: int = 20, standard_deviations: float = 2): +def get_bollinger_bands( + quotes: Iterable[Quote], lookback_periods: int = 20, standard_deviations: float = 2 +): """Get Bollinger Bands® calculated. Bollinger Bands® depict volatility as standard deviation @@ -31,7 +33,9 @@ def get_bollinger_bands(quotes: Iterable[Quote], lookback_periods: int = 20, sta - [Bollinger Bands® Reference](https://python.stockindicators.dev/indicators/BollingerBands/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - bollinger_bands_results = CsIndicator.GetBollingerBands[Quote](CsList(Quote, quotes), lookback_periods, standard_deviations) + bollinger_bands_results = CsIndicator.GetBollingerBands[Quote]( + CsList(Quote, quotes), lookback_periods, standard_deviations + ) return BollingerBandsResults(bollinger_bands_results, BollingerBandsResult) @@ -90,6 +94,8 @@ def width(self, value): _T = TypeVar("_T", bound=BollingerBandsResult) + + class BollingerBandsResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Bollinger Bands results. diff --git a/stock_indicators/indicators/bop.py b/stock_indicators/indicators/bop.py index 69d3f744..bda166e3 100644 --- a/stock_indicators/indicators/bop.py +++ b/stock_indicators/indicators/bop.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_bop(quotes: Iterable[Quote], smooth_periods: int = 14): @@ -48,6 +48,8 @@ def bop(self, value): _T = TypeVar("_T", bound=BOPResult) + + class BOPResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Balance of Power (aka Balance of Market Power) results. diff --git a/stock_indicators/indicators/cci.py b/stock_indicators/indicators/cci.py index f6e151ba..2f8b752a 100644 --- a/stock_indicators/indicators/cci.py +++ b/stock_indicators/indicators/cci.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_cci(quotes: Iterable[Quote], lookback_periods: int = 20): @@ -47,6 +47,8 @@ def cci(self, value): _T = TypeVar("_T", bound=CCIResult) + + class CCIResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Commodity Channel Index (CCI) results. diff --git a/stock_indicators/indicators/chaikin_oscillator.py b/stock_indicators/indicators/chaikin_oscillator.py index 394467ad..3b879265 100644 --- a/stock_indicators/indicators/chaikin_oscillator.py +++ b/stock_indicators/indicators/chaikin_oscillator.py @@ -3,11 +3,13 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_chaikin_osc(quotes: Iterable[Quote], fast_periods: int = 3, slow_periods: int = 10): +def get_chaikin_osc( + quotes: Iterable[Quote], fast_periods: int = 3, slow_periods: int = 10 +): """Get Chaikin Oscillator calculated. Chaikin Oscillator is the difference between fast and slow @@ -31,7 +33,9 @@ def get_chaikin_osc(quotes: Iterable[Quote], fast_periods: int = 3, slow_periods - [Chaikin Oscillator Reference](https://python.stockindicators.dev/indicators/ChaikinOsc/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetChaikinOsc[Quote](CsList(Quote, quotes), fast_periods, slow_periods) + results = CsIndicator.GetChaikinOsc[Quote]( + CsList(Quote, quotes), fast_periods, slow_periods + ) return ChaikinOscResults(results, ChaikinOscResult) @@ -74,6 +78,8 @@ def oscillator(self, value): _T = TypeVar("_T", bound=ChaikinOscResult) + + class ChaikinOscResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Chaikin Oscillator results. diff --git a/stock_indicators/indicators/chandelier.py b/stock_indicators/indicators/chandelier.py index b136f231..55eef624 100644 --- a/stock_indicators/indicators/chandelier.py +++ b/stock_indicators/indicators/chandelier.py @@ -4,12 +4,16 @@ from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.enums import ChandelierType from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_chandelier(quotes: Iterable[Quote], lookback_periods: int = 22, - multiplier: float = 3, chandelier_type: ChandelierType = ChandelierType.LONG): +def get_chandelier( + quotes: Iterable[Quote], + lookback_periods: int = 22, + multiplier: float = 3, + chandelier_type: ChandelierType = ChandelierType.LONG, +): """Get Chandelier Exit calculated. Chandelier Exit is typically used for stop-loss and can be @@ -36,8 +40,9 @@ def get_chandelier(quotes: Iterable[Quote], lookback_periods: int = 22, - [Chandelier Exit Reference](https://python.stockindicators.dev/indicators/Chandelier/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetChandelier[Quote](CsList(Quote, quotes), lookback_periods, - multiplier, chandelier_type.cs_value) + results = CsIndicator.GetChandelier[Quote]( + CsList(Quote, quotes), lookback_periods, multiplier, chandelier_type.cs_value + ) return ChandelierResults(results, ChandelierResult) @@ -56,6 +61,8 @@ def chandelier_exit(self, value): _T = TypeVar("_T", bound=ChandelierResult) + + class ChandelierResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Chandelier Exit results. diff --git a/stock_indicators/indicators/chop.py b/stock_indicators/indicators/chop.py index 40bf6048..da12cf3c 100644 --- a/stock_indicators/indicators/chop.py +++ b/stock_indicators/indicators/chop.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_chop(quotes: Iterable[Quote], lookback_periods: int = 14): @@ -47,6 +47,8 @@ def chop(self, value): _T = TypeVar("_T", bound=ChopResult) + + class ChopResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Choppiness Index (CHOP) results. diff --git a/stock_indicators/indicators/cmf.py b/stock_indicators/indicators/cmf.py index a9aa24b9..878a99fe 100644 --- a/stock_indicators/indicators/cmf.py +++ b/stock_indicators/indicators/cmf.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_cmf(quotes: Iterable[Quote], lookback_periods: int = 20): @@ -63,6 +63,8 @@ def cmf(self, value): _T = TypeVar("_T", bound=CMFResult) + + class CMFResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Chaikin Money Flow (CMF) results. diff --git a/stock_indicators/indicators/cmo.py b/stock_indicators/indicators/cmo.py index 0e6ed714..45fa6c16 100644 --- a/stock_indicators/indicators/cmo.py +++ b/stock_indicators/indicators/cmo.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_cmo(quotes: Iterable[Quote], lookback_periods: int): @@ -47,6 +47,8 @@ def cmo(self, value): _T = TypeVar("_T", bound=CMOResult) + + class CMOResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Chande Momentum Oscillator (CMO) results. diff --git a/stock_indicators/indicators/common/__init__.py b/stock_indicators/indicators/common/__init__.py index 06994367..d869d097 100644 --- a/stock_indicators/indicators/common/__init__.py +++ b/stock_indicators/indicators/common/__init__.py @@ -1,35 +1,30 @@ -from .quote import Quote -from .results import ( - ResultBase, - IndicatorResults -) -from .candles import ( - CandleProperties, -) +from .candles import CandleProperties from .enums import ( BetaType, - ChandelierType, CandlePart, + ChandelierType, EndType, + Match, MAType, PeriodSize, PivotPointType, PivotTrend, - Match ) +from .quote import Quote +from .results import IndicatorResults, ResultBase __all__ = [ - "Quote", - "ResultBase", - "IndicatorResults", - "CandleProperties", "BetaType", - "ChandelierType", "CandlePart", + "CandleProperties", + "ChandelierType", "EndType", + "IndicatorResults", "MAType", + "Match", "PeriodSize", "PivotPointType", "PivotTrend", - "Match" + "Quote", + "ResultBase", ] diff --git a/stock_indicators/indicators/common/_contrib/type_resolver.py b/stock_indicators/indicators/common/_contrib/type_resolver.py index 59876c61..6bdcfdf1 100644 --- a/stock_indicators/indicators/common/_contrib/type_resolver.py +++ b/stock_indicators/indicators/common/_contrib/type_resolver.py @@ -1,5 +1,16 @@ from typing import Type, TypeVar, cast _T = TypeVar("_T") -def generate_cs_inherited_class(child: Type[_T], cs_parent: Type, class_name="_Wrapper"): - return cast(Type[_T], type(class_name, (cs_parent, ), {attr: getattr(child, attr) for attr in dir(child)})) + + +def generate_cs_inherited_class( + child: Type[_T], cs_parent: Type, class_name="_Wrapper" +): + return cast( + Type[_T], + type( + class_name, + (cs_parent,), + {attr: getattr(child, attr) for attr in dir(child)}, + ), + ) diff --git a/stock_indicators/indicators/common/candles.py b/stock_indicators/indicators/common/candles.py index c166b83b..2d016d45 100644 --- a/stock_indicators/indicators/common/candles.py +++ b/stock_indicators/indicators/common/candles.py @@ -1,11 +1,14 @@ from decimal import Decimal from typing import Optional, TypeVar + from typing_extensions import override from stock_indicators._cslib import CsCandleProperties from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes import to_pydecimal_via_double -from stock_indicators.indicators.common._contrib.type_resolver import generate_cs_inherited_class +from stock_indicators.indicators.common._contrib.type_resolver import ( + generate_cs_inherited_class, +) from stock_indicators.indicators.common.enums import Match from stock_indicators.indicators.common.helpers import CondenseMixin from stock_indicators.indicators.common.quote import _Quote @@ -15,26 +18,31 @@ class _CandleProperties(_Quote): @property def size(self) -> Optional[Decimal]: + # pylint: disable=no-member # C# interop properties return to_pydecimal_via_double(self.High - self.Low) @property def body(self) -> Optional[Decimal]: - return to_pydecimal_via_double(self.Open - self.Close \ - if (self.Open > self.Close) \ - else self.Close - self.Open) + # pylint: disable=no-member # C# interop properties + return to_pydecimal_via_double( + self.Open - self.Close + if (self.Open > self.Close) + else self.Close - self.Open + ) @property def upper_wick(self) -> Optional[Decimal]: - return to_pydecimal_via_double(self.High - ( - self.Open \ - if self.Open > self.Close \ - else self.Close)) + # pylint: disable=no-member # C# interop properties + return to_pydecimal_via_double( + self.High - (self.Open if self.Open > self.Close else self.Close) + ) @property def lower_wick(self) -> Optional[Decimal]: - return to_pydecimal_via_double((self.Close \ - if self.Open > self.Close \ - else self.Open) - self.Low) + # pylint: disable=no-member # C# interop properties + return to_pydecimal_via_double( + (self.Close if self.Open > self.Close else self.Open) - self.Low + ) @property def body_pct(self) -> Optional[float]: @@ -50,14 +58,18 @@ def lower_wick_pct(self) -> Optional[float]: @property def is_bullish(self) -> bool: + # pylint: disable=no-member # C# interop properties return self.Close > self.Open @property def is_bearish(self) -> bool: + # pylint: disable=no-member # C# interop properties return self.Close < self.Open -class CandleProperties(generate_cs_inherited_class(_CandleProperties, CsCandleProperties)): +class CandleProperties( + generate_cs_inherited_class(_CandleProperties, CsCandleProperties) +): """An extended version of Quote that contains additional calculated properties.""" @@ -87,7 +99,10 @@ def match(self, value): @property def candle(self) -> CandleProperties: if not self.__candle_prop_cache: - self.__candle_prop_cache = CandleProperties.from_csquote(self._csdata.Candle) + # pylint: disable=no-member # C# interop method + self.__candle_prop_cache = CandleProperties.from_csquote( + self._csdata.Candle + ) return self.__candle_prop_cache @@ -98,6 +113,8 @@ def candle(self, value): _T = TypeVar("_T", bound=CandleResult) + + class CandleResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of Candlestick pattern results. @@ -107,4 +124,6 @@ class CandleResults(CondenseMixin, IndicatorResults[_T]): @override def condense(self): - return self.__class__(filter(lambda x: x.match != Match.NONE, self), self._wrapper_class) + return self.__class__( + filter(lambda x: x.match != Match.NONE, self), self._wrapper_class + ) diff --git a/stock_indicators/indicators/common/enums.py b/stock_indicators/indicators/common/enums.py index 60a40c76..a9296e8f 100644 --- a/stock_indicators/indicators/common/enums.py +++ b/stock_indicators/indicators/common/enums.py @@ -1,6 +1,15 @@ from stock_indicators._cslib import ( - CsCandlePart, CsMatch, CsEnum, CsBetaType, CsChandelierType, CsMaType, - CsPivotPointType, CsPeriodSize, CsEndType, CsPivotTrend) + CsBetaType, + CsCandlePart, + CsChandelierType, + CsEndType, + CsEnum, + CsMatch, + CsMaType, + CsPeriodSize, + CsPivotPointType, + CsPivotTrend, +) from stock_indicators.indicators.common._contrib.enum import CsCompatibleIntEnum diff --git a/stock_indicators/indicators/common/helpers.py b/stock_indicators/indicators/common/helpers.py index 12343b17..d019dc14 100644 --- a/stock_indicators/indicators/common/helpers.py +++ b/stock_indicators/indicators/common/helpers.py @@ -2,37 +2,69 @@ from typing_extensions import Self -from stock_indicators._cslib import CsIndicator, CsIEnumerable, CsResultUtility +from stock_indicators._cslib import CsIEnumerable, CsIndicator, CsResultUtility from stock_indicators._cstypes import List as CsList +from stock_indicators.exceptions import IndicatorCalculationError from stock_indicators.indicators.common.results import IndicatorResults class RemoveWarmupMixin: """IndicatorResults Mixin for remove_warmup_periods().""" + @IndicatorResults._verify_data - def remove_warmup_periods(self: IndicatorResults, remove_periods: Optional[int] = None) -> Self: + def remove_warmup_periods( + self: IndicatorResults, remove_periods: Optional[int] = None + ) -> Self: """ Remove the recommended(or specified) quantity of results from the beginning of the results list. + + Args: + remove_periods: Number of periods to remove. If None, removes recommended warmup periods. + + Returns: + New IndicatorResults instance with warmup periods removed. """ if remove_periods is not None: + if not isinstance(remove_periods, int): + raise TypeError("remove_periods must be an integer") + if remove_periods < 0: + raise ValueError("remove_periods must be non-negative") return super().remove_warmup_periods(remove_periods) - removed_results = CsIndicator.RemoveWarmupPeriods(CsList(self._get_csdata_type(), self._csdata)) - return self.__class__(removed_results, self._wrapper_class) + try: + removed_results = CsIndicator.RemoveWarmupPeriods( + CsList(self._get_csdata_type(), self._csdata) + ) + return self.__class__(removed_results, self._wrapper_class) + except Exception as e: + raise IndicatorCalculationError("remove_warmup_periods failed") from e class CondenseMixin: """IndicatorResults Mixin for condense().""" + @IndicatorResults._verify_data def condense(self: IndicatorResults) -> Self: """ Removes non-essential records containing null values with unique consideration for this indicator. + + Returns: + New IndicatorResults instance with null values removed. """ cs_results_type = self._get_csdata_type() - try: # to check whether there's matched overloaded method. - condense_method = CsIndicator.Condense.Overloads[CsIEnumerable[cs_results_type]] - except TypeError: - condense_method = CsResultUtility.Condense[cs_results_type] - condensed_results = condense_method(CsList(cs_results_type, self._csdata)) - return self.__class__(condensed_results, self._wrapper_class) + try: + # Try to find the specific overloaded method first + try: + condense_method = CsIndicator.Condense.Overloads[ + CsIEnumerable[cs_results_type] + ] + except TypeError: + # Fall back to generic utility method + condense_method = CsResultUtility.Condense[cs_results_type] + + condensed_results = condense_method(CsList(cs_results_type, self._csdata)) + return self.__class__(condensed_results, self._wrapper_class) + + except Exception as e: + raise ValueError("Failed to condense results") from e diff --git a/stock_indicators/indicators/common/quote.py b/stock_indicators/indicators/common/quote.py index 1cde4b4f..1ae6e5c2 100644 --- a/stock_indicators/indicators/common/quote.py +++ b/stock_indicators/indicators/common/quote.py @@ -1,55 +1,94 @@ from datetime import datetime, timezone from decimal import Decimal -from typing import Any, Iterable, Optional +from typing import Any, Iterable, Optional, Union from stock_indicators._cslib import CsQuote, CsQuoteUtility -from stock_indicators._cstypes import List as CsList 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_via_double +from stock_indicators.indicators.common._contrib.type_resolver import ( + generate_cs_inherited_class, +) from stock_indicators.indicators.common.enums import CandlePart -from stock_indicators.indicators.common._contrib.type_resolver import generate_cs_inherited_class -def _get_date(quote): +def _get_date(quote) -> datetime: + """Get the date property with proper null handling.""" return to_pydatetime(quote.Date) -def _set_date(quote, value): + +def _set_date(quote, value: datetime) -> None: + """Set the date property with validation and timezone normalization.""" + if not isinstance(value, datetime): + raise TypeError("Date must be a datetime.datetime instance") + + # Normalize timezone-aware datetime to UTC (from main branch) if value.tzinfo is not None and value.utcoffset() is not None: value = value.astimezone(timezone.utc) quote.Date = CsDateTime(value) -def _get_open(quote): + +def _get_open(quote) -> Optional[Decimal]: + """Get the open property with proper null handling.""" return to_pydecimal_via_double(quote.Open) -def _set_open(quote, value): - quote.Open = CsDecimal(value) -def _get_high(quote): +def _set_open(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: + """Set the open property with validation.""" + if value is not None: + quote.Open = CsDecimal(value) + # Note: C# nullable decimals can't be explicitly set to None from Python.NET + # The C# property handles null values internally when not set + + +def _get_high(quote) -> Optional[Decimal]: + """Get the high property with proper null handling.""" return to_pydecimal_via_double(quote.High) -def _set_high(quote, value): - quote.High = CsDecimal(value) -def _get_low(quote): +def _set_high(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: + """Set the high property with validation.""" + if value is not None: + quote.High = CsDecimal(value) + + +def _get_low(quote) -> Optional[Decimal]: + """Get the low property with proper null handling.""" return to_pydecimal_via_double(quote.Low) -def _set_low(quote, value): - quote.Low = CsDecimal(value) -def _get_close(quote): +def _set_low(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: + """Set the low property with validation.""" + if value is not None: + quote.Low = CsDecimal(value) + + +def _get_close(quote) -> Optional[Decimal]: + """Get the close property with proper null handling.""" return to_pydecimal_via_double(quote.Close) -def _set_close(quote, value): - quote.Close = CsDecimal(value) -def _get_volume(quote): +def _set_close(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: + """Set the close property with validation.""" + if value is not None: + quote.Close = CsDecimal(value) + + +def _get_volume(quote) -> Optional[Decimal]: + """Get the volume property with proper null handling.""" return to_pydecimal_via_double(quote.Volume) -def _set_volume(quote, value): - quote.Volume = CsDecimal(value) + +def _set_volume(quote, value: Optional[Union[int, float, Decimal, str]]) -> None: + """Set the volume property with validation.""" + if value is not None: + quote.Volume = CsDecimal(value) + class _Quote: + """Internal Quote implementation with property definitions.""" + date = property(_get_date, _set_date) open = property(_get_open, _set_open) high = property(_get_high, _set_high) @@ -57,39 +96,87 @@ class _Quote: close = property(_get_close, _set_close) volume = property(_get_volume, _set_volume) - def __init__(self, date: datetime, open: Optional[Any] = None, - high: Optional[Any] = None, low: Optional[Any] = None, - close: Optional[Any] = None, volume: Optional[Any] = None): + def __init__( + self, + date: datetime, # pylint: disable=too-many-positional-arguments + open: Optional[Union[int, float, Decimal, str]] = None, # pylint: disable=redefined-builtin + high: Optional[Union[int, float, Decimal, str]] = None, + low: Optional[Union[int, float, Decimal, str]] = None, + close: Optional[Union[int, float, Decimal, str]] = None, + volume: Optional[Union[int, float, Decimal, str]] = None, + ): + """ + Initialize a Quote with OHLCV data. + + Args: + date: The date for this quote (required) + open: Opening price (optional) + high: High price (optional) + low: Low price (optional) + close: Closing price (optional) + volume: Volume (optional) + """ + if not isinstance(date, datetime): + raise TypeError("date must be a datetime.datetime instance") + self.date = date - self.open: Decimal = open if open else 0 - self.high: Decimal = high if high else 0 - self.low: Decimal = low if low else 0 - self.close: Decimal = close if close else 0 - self.volume: Decimal = volume if volume else 0 + # Only set values that are not None to avoid C# nullable issues + if open is not None: + self.open = open + if high is not None: + self.high = high + if low is not None: + self.low = low + if close is not None: + self.close = close + if volume is not None: + self.volume = volume @classmethod - def from_csquote(cls, cs_quote: CsQuote): + def from_csquote(cls, cs_quote: CsQuote) -> "Quote": """Constructs `Quote` instance from C# `Quote` instance.""" + if not isinstance(cs_quote, CsQuote): + raise TypeError("cs_quote must be a C# Quote instance") + return cls( date=to_pydatetime(cs_quote.Date), 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) + volume=to_pydecimal_via_double(cs_quote.Volume), ) @classmethod - def use(cls, quotes: Iterable["Quote"], candle_part: CandlePart): + def use(cls, quotes: Iterable["Quote"], candle_part: CandlePart) -> Any: """ Optionally select which candle part to use in the calculation. It returns C# Object. + + Args: + quotes: Collection of Quote objects + candle_part: Which part of the candle to use + + Returns: + C# collection prepared for indicator calculation """ - return CsQuoteUtility.Use[Quote](CsList(Quote, quotes), candle_part.cs_value) + if not hasattr(quotes, "__iter__"): + raise TypeError("quotes must be iterable") + if not isinstance(candle_part, CandlePart): + raise TypeError("candle_part must be a CandlePart enum value") + + try: + return CsQuoteUtility.Use[Quote]( + CsList(Quote, quotes), candle_part.cs_value + ) + except Exception as e: + raise ValueError(f"Failed to prepare quotes for calculation: {e}") from e class Quote(generate_cs_inherited_class(_Quote, CsQuote)): """ A single dated quote containing OHLCV elements. OHLCV values can be given as any object that can be represented as a number string. + + This class extends the C# Quote type to provide Python-friendly access to quote data. """ diff --git a/stock_indicators/indicators/common/results.py b/stock_indicators/indicators/common/results.py index 2424283d..d0510990 100644 --- a/stock_indicators/indicators/common/results.py +++ b/stock_indicators/indicators/common/results.py @@ -1,6 +1,5 @@ from datetime import datetime as PyDateTime from typing import Callable, Iterable, List, Optional, Type, TypeVar -from warnings import warn from stock_indicators._cslib import CsResultBase from stock_indicators._cstypes import DateTime as CsDateTime @@ -9,59 +8,63 @@ class ResultBase: """A base wrapper class for a single unit of the results.""" + def __init__(self, base_result: CsResultBase): self._csdata = base_result @property - def date(self): + def date(self) -> PyDateTime: + """Get the date of this result.""" return to_pydatetime(self._csdata.Date) @date.setter - def date(self, value): + def date(self, value: PyDateTime) -> None: + """Set the date of this result.""" + if not isinstance(value, PyDateTime): + raise TypeError("Date must be a datetime.datetime instance") self._csdata.Date = CsDateTime(value) _T = TypeVar("_T", bound=ResultBase) + + class IndicatorResults(List[_T]): """ A base wrapper class for the list of results. It provides helper methods written in CSharp implementation. """ + def __init__(self, data: Iterable, wrapper_class: Type[_T]): - super().__init__(map(wrapper_class, data)) - self._csdata = data + if not data: + super().__init__() + self._csdata = [] + else: + super().__init__(map(wrapper_class, data)) + self._csdata = data self._wrapper_class = wrapper_class - def reload(self): - """ - Reload a C# array of the results to perform more operations. - It is usually called after `done()`. - This method is deprecated. It will be removed in the next version. - """ - warn('This method is deprecated.', DeprecationWarning, stacklevel=2) - if self._csdata is None: - self._csdata = [ _._csdata for _ in self ] - return self - - def done(self): - """ - Remove a C# array of the results after finishing all operations. - It is not necessary but saves memory. - This method is deprecated. It will be removed in the next version. - """ - warn('This method is deprecated.', DeprecationWarning, stacklevel=2) - self._csdata = None - return self - def _get_csdata_type(self): """Get C# result object type.""" + if len(self) == 0: + raise ValueError("Cannot determine C# data type from empty results") return type(self[0]._csdata) - def _verify_data(func: Callable): + @staticmethod # pylint: disable=no-self-argument + def _verify_data(func: Callable) -> Callable: """Check whether `_csdata` can be passed to helper method.""" + def verify_data(self, *args): + if self._csdata is None: + # Use a generic name when func.__name__ is not available + func_name = getattr(func, "__name__", "method") + raise ValueError( + f"Cannot {func_name}() after done() has been called. Call reload() first." + ) + if not isinstance(self._csdata, Iterable) or len(self) < 1: - raise ValueError(f"Cannot {func.__name__}() an empty result.") + # Use a generic name when func.__name__ is not available + func_name = getattr(func, "__name__", "method") + raise ValueError(f"Cannot {func_name}() an empty result.") if not issubclass(self._get_csdata_type(), CsResultBase): raise TypeError( @@ -74,23 +77,52 @@ def verify_data(self, *args): @_verify_data def __add__(self, other: "IndicatorResults"): - return self.__class__(list(self._csdata).__add__(list(other._csdata)), self._wrapper_class) + """Concatenate two IndicatorResults.""" + if not isinstance(other, IndicatorResults): + raise TypeError("Can only add IndicatorResults to IndicatorResults") + return self.__class__( + list(self._csdata).__add__(list(other._csdata)), self._wrapper_class + ) @_verify_data def __mul__(self, value: int): + """Repeat IndicatorResults.""" + if not isinstance(value, int): + raise TypeError("Can only multiply IndicatorResults by integer") return self.__class__(list(self._csdata).__mul__(value), self._wrapper_class) @_verify_data - def remove_warmup_periods(self, remove_periods: int): + def remove_warmup_periods(self, remove_periods: int) -> "IndicatorResults": """Remove a specific quantity of results from the beginning of the results list.""" if not isinstance(remove_periods, int): raise TypeError("remove_periods must be an integer.") + if remove_periods < 0: + raise ValueError("remove_periods must be non-negative.") + + if remove_periods >= len(self): + return self.__class__([], self._wrapper_class) + return self.__class__(list(self._csdata)[remove_periods:], self._wrapper_class) def find(self, lookup_date: PyDateTime) -> Optional[_T]: - """Find indicator values on a specific date. It returns `None` if no result found.""" + """ + Find indicator values on a specific date. + Returns `None` if no result found. + + Args: + lookup_date: The date to search for + + Returns: + The result for the given date or None if not found + """ if not isinstance(lookup_date, PyDateTime): raise TypeError("lookup_date must be an instance of datetime.datetime.") - return next((r for r in self if r.date == lookup_date), None) + # Linear search (result sets are usually small enough that this is sufficient) + # First try matching only the calendar date (ignoring time) for convenience. + # If that attribute access fails, fall back to exact datetime comparison. + try: + return next((r for r in self if r.date.date() == lookup_date.date()), None) + except (AttributeError, TypeError): + return next((r for r in self if r.date == lookup_date), None) diff --git a/stock_indicators/indicators/connors_rsi.py b/stock_indicators/indicators/connors_rsi.py index 1e0af0ec..73295d46 100644 --- a/stock_indicators/indicators/connors_rsi.py +++ b/stock_indicators/indicators/connors_rsi.py @@ -3,12 +3,16 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_connors_rsi(quotes: Iterable[Quote], rsi_periods: int = 3, - streak_periods: int = 2, rank_periods: int = 100): +def get_connors_rsi( + quotes: Iterable[Quote], + rsi_periods: int = 3, + streak_periods: int = 2, + rank_periods: int = 100, +): """Get Connors RSI calculated. Connors RSI is a composite oscillator that incorporates @@ -35,8 +39,9 @@ def get_connors_rsi(quotes: Iterable[Quote], rsi_periods: int = 3, - [Connors RSI Reference](https://python.stockindicators.dev/indicators/ConnorsRsi/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetConnorsRsi[Quote](CsList(Quote, quotes), rsi_periods, - streak_periods, rank_periods) + results = CsIndicator.GetConnorsRsi[Quote]( + CsList(Quote, quotes), rsi_periods, streak_periods, rank_periods + ) return ConnorsRSIResults(results, ConnorsRSIResult) @@ -81,6 +86,8 @@ def connors_rsi(self, value): _T = TypeVar("_T", bound=ConnorsRSIResult) + + class ConnorsRSIResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Connors RSI results. diff --git a/stock_indicators/indicators/correlation.py b/stock_indicators/indicators/correlation.py index 630d771e..6bdbbb1b 100644 --- a/stock_indicators/indicators/correlation.py +++ b/stock_indicators/indicators/correlation.py @@ -3,12 +3,13 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_correlation(quotes_a: Iterable[Quote], quotes_b: Iterable[Quote], - lookback_periods: int): +def get_correlation( + quotes_a: Iterable[Quote], quotes_b: Iterable[Quote], lookback_periods: int +): """Get Correlation Coefficient calculated. Correlation Coefficient between two quote histories, based on Close price. @@ -31,8 +32,9 @@ def get_correlation(quotes_a: Iterable[Quote], quotes_b: Iterable[Quote], - [Correlation Coefficient Reference](https://python.stockindicators.dev/indicators/Correlation/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetCorrelation[Quote](CsList(Quote, quotes_a), CsList(Quote, quotes_b), - lookback_periods) + results = CsIndicator.GetCorrelation[Quote]( + CsList(Quote, quotes_a), CsList(Quote, quotes_b), lookback_periods + ) return CorrelationResults(results, CorrelationResult) @@ -83,6 +85,8 @@ def r_squared(self, value): _T = TypeVar("_T", bound=CorrelationResult) + + class CorrelationResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Correlation Coefficient results. diff --git a/stock_indicators/indicators/dema.py b/stock_indicators/indicators/dema.py index bc7b54af..64253d04 100644 --- a/stock_indicators/indicators/dema.py +++ b/stock_indicators/indicators/dema.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_dema(quotes: Iterable[Quote], lookback_periods: int): @@ -46,6 +46,8 @@ def dema(self, value): _T = TypeVar("_T", bound=DEMAResult) + + class DEMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Double Exponential Moving Average (DEMA) results. diff --git a/stock_indicators/indicators/doji.py b/stock_indicators/indicators/doji.py index 23874df7..aa567457 100644 --- a/stock_indicators/indicators/doji.py +++ b/stock_indicators/indicators/doji.py @@ -28,5 +28,7 @@ def get_doji(quotes: Iterable[Quote], max_price_change_percent: float = 0.1): - [Doji Reference](https://python.stockindicators.dev/indicators/Doji/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetDoji[Quote](CsList(Quote, quotes), max_price_change_percent) + results = CsIndicator.GetDoji[Quote]( + CsList(Quote, quotes), max_price_change_percent + ) return CandleResults(results, CandleResult) diff --git a/stock_indicators/indicators/donchian.py b/stock_indicators/indicators/donchian.py index 979727ec..0e043fb9 100644 --- a/stock_indicators/indicators/donchian.py +++ b/stock_indicators/indicators/donchian.py @@ -2,12 +2,12 @@ from typing import Iterable, Optional, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_donchian(quotes: Iterable[Quote], lookback_periods: int = 20): diff --git a/stock_indicators/indicators/dpo.py b/stock_indicators/indicators/dpo.py index 6d2d9123..18a96d35 100644 --- a/stock_indicators/indicators/dpo.py +++ b/stock_indicators/indicators/dpo.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_dpo(quotes: Iterable[Quote], lookback_periods: int): @@ -55,6 +55,8 @@ def dpo(self, value): _T = TypeVar("_T", bound=DPOResult) + + class DPOResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of Detrended Price Oscillator (DPO) results. diff --git a/stock_indicators/indicators/dynamic.py b/stock_indicators/indicators/dynamic.py index e6a9565c..ae3279f8 100644 --- a/stock_indicators/indicators/dynamic.py +++ b/stock_indicators/indicators/dynamic.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_dynamic(quotes: Iterable[Quote], lookback_periods: int, k_factor: float = 0.6): @@ -30,7 +30,9 @@ def get_dynamic(quotes: Iterable[Quote], lookback_periods: int, k_factor: float - [McGinley Dynamic Reference](https://python.stockindicators.dev/indicators/Dynamic/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetDynamic[Quote](CsList(Quote, quotes), lookback_periods, k_factor) + results = CsIndicator.GetDynamic[Quote]( + CsList(Quote, quotes), lookback_periods, k_factor + ) return DynamicResults(results, DynamicResult) @@ -49,6 +51,8 @@ def dynamic(self, value): _T = TypeVar("_T", bound=DynamicResult) + + class DynamicResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of McGinley Dynamic results. diff --git a/stock_indicators/indicators/elder_ray.py b/stock_indicators/indicators/elder_ray.py index 1ac3a97c..2eaa9967 100644 --- a/stock_indicators/indicators/elder_ray.py +++ b/stock_indicators/indicators/elder_ray.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_elder_ray(quotes: Iterable[Quote], lookback_periods: int = 13): @@ -62,6 +62,8 @@ def bear_power(self, value): _T = TypeVar("_T", bound=ElderRayResult) + + class ElderRayResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Elder-ray Index results. diff --git a/stock_indicators/indicators/ema.py b/stock_indicators/indicators/ema.py index 8de55047..1a097f8e 100644 --- a/stock_indicators/indicators/ema.py +++ b/stock_indicators/indicators/ema.py @@ -3,12 +3,15 @@ from stock_indicators._cslib import CsIndicator from stock_indicators.indicators.common.enums import CandlePart from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_ema(quotes: Iterable[Quote], lookback_periods: int, - candle_part: CandlePart = CandlePart.CLOSE): +def get_ema( + quotes: Iterable[Quote], + lookback_periods: int, + candle_part: CandlePart = CandlePart.CLOSE, +): """Get EMA calculated. Exponential Moving Average (EMA) of the Close price. @@ -31,7 +34,7 @@ def get_ema(quotes: Iterable[Quote], lookback_periods: int, - [EMA Reference](https://python.stockindicators.dev/indicators/Ema/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - quotes = Quote.use(quotes, candle_part) # Error occurs if not assigned to local var. + quotes = Quote.use(quotes, candle_part) # pylint: disable=no-member # Error occurs if not assigned to local var. ema_list = CsIndicator.GetEma(quotes, lookback_periods) return EMAResults(ema_list, EMAResult) @@ -51,6 +54,8 @@ def ema(self, value): _T = TypeVar("_T", bound=EMAResult) + + class EMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of EMA(Exponential Moving Average) results. diff --git a/stock_indicators/indicators/epma.py b/stock_indicators/indicators/epma.py index 5616cb83..7bc0e2cd 100644 --- a/stock_indicators/indicators/epma.py +++ b/stock_indicators/indicators/epma.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_epma(quotes: Iterable[Quote], lookback_periods: int): @@ -48,6 +48,8 @@ def epma(self, value): _T = TypeVar("_T", bound=EPMAResult) + + class EPMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Endpoint Moving Average (EPMA) results. diff --git a/stock_indicators/indicators/fcb.py b/stock_indicators/indicators/fcb.py index 422883cf..fbaa7855 100644 --- a/stock_indicators/indicators/fcb.py +++ b/stock_indicators/indicators/fcb.py @@ -2,12 +2,12 @@ from typing import Iterable, Optional, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_fcb(quotes: Iterable[Quote], window_span: int = 2): diff --git a/stock_indicators/indicators/fisher_transform.py b/stock_indicators/indicators/fisher_transform.py index b208c29d..637b7ef5 100644 --- a/stock_indicators/indicators/fisher_transform.py +++ b/stock_indicators/indicators/fisher_transform.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_fisher_transform(quotes: Iterable[Quote], lookback_periods: int = 10): @@ -29,7 +29,9 @@ def get_fisher_transform(quotes: Iterable[Quote], lookback_periods: int = 10): - [Fisher Transform Reference](https://python.stockindicators.dev/indicators/FisherTransform/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetFisherTransform[Quote](CsList(Quote, quotes), lookback_periods) + results = CsIndicator.GetFisherTransform[Quote]( + CsList(Quote, quotes), lookback_periods + ) return FisherTransformResults(results, FisherTransformResult) @@ -56,6 +58,8 @@ def trigger(self, value): _T = TypeVar("_T", bound=FisherTransformResult) + + class FisherTransformResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of Ehlers Fisher Transform results. diff --git a/stock_indicators/indicators/force_index.py b/stock_indicators/indicators/force_index.py index ec7fe444..e125dab9 100644 --- a/stock_indicators/indicators/force_index.py +++ b/stock_indicators/indicators/force_index.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_force_index(quotes: Iterable[Quote], lookback_periods: int): @@ -46,6 +46,8 @@ def force_index(self, value): _T = TypeVar("_T", bound=ForceIndexResult) + + class ForceIndexResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Force Index results. diff --git a/stock_indicators/indicators/fractal.py b/stock_indicators/indicators/fractal.py index 2aca5a91..c479096e 100644 --- a/stock_indicators/indicators/fractal.py +++ b/stock_indicators/indicators/fractal.py @@ -2,23 +2,27 @@ from typing import Iterable, Optional, TypeVar, overload from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList 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.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @overload def get_fractal( quotes: Iterable[Quote], window_span: int = 2, end_type=EndType.HIGH_LOW ) -> "FractalResults[FractalResult]": ... + + @overload def get_fractal( quotes: Iterable[Quote], left_span: int, right_span: int, end_type=EndType.HIGH_LOW ) -> "FractalResults[FractalResult]": ... + + def get_fractal( quotes, left_span=None, right_span=EndType.HIGH_LOW, end_type=EndType.HIGH_LOW ): diff --git a/stock_indicators/indicators/gator.py b/stock_indicators/indicators/gator.py index 885dcc98..a573bd96 100644 --- a/stock_indicators/indicators/gator.py +++ b/stock_indicators/indicators/gator.py @@ -4,8 +4,9 @@ from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.alligator import AlligatorResult from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase + @overload def get_gator(quotes: Iterable[Quote]) -> "GatorResults[GatorResult]": ... @@ -32,12 +33,12 @@ def get_gator(quotes): results = CsIndicator.GetGator[Quote](CsList(Quote, quotes)) else: # Get C# objects. - if isinstance(quotes, IndicatorResults) and quotes._csdata is not None: - cs_results = quotes._csdata + if isinstance(quotes, IndicatorResults): + # Use the C# data directly if available + results = CsIndicator.GetGator(quotes._csdata) else: - cs_results = [ q._csdata for q in quotes ] - - results = CsIndicator.GetGator(CsList(type(cs_results[0]), cs_results)) + cs_results = [q._csdata for q in quotes] + results = CsIndicator.GetGator(CsList(type(cs_results[0]), cs_results)) return GatorResults(results, GatorResult) @@ -80,6 +81,8 @@ def is_lower_expanding(self, value): _T = TypeVar("_T", bound=GatorResult) + + class GatorResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Gator Oscillator results. diff --git a/stock_indicators/indicators/heikin_ashi.py b/stock_indicators/indicators/heikin_ashi.py index 14deac33..cebf1dca 100644 --- a/stock_indicators/indicators/heikin_ashi.py +++ b/stock_indicators/indicators/heikin_ashi.py @@ -2,11 +2,11 @@ from typing import Iterable, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import to_pydecimal_via_double -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_heikin_ashi(quotes: Iterable[Quote]): diff --git a/stock_indicators/indicators/hma.py b/stock_indicators/indicators/hma.py index 7738ea26..9926d3f0 100644 --- a/stock_indicators/indicators/hma.py +++ b/stock_indicators/indicators/hma.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_hma(quotes: Iterable[Quote], lookback_periods: int): @@ -47,6 +47,8 @@ def hma(self, value): _T = TypeVar("_T", bound=HMAResult) + + class HMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Hull Moving Average (HMA) results. diff --git a/stock_indicators/indicators/ht_trendline.py b/stock_indicators/indicators/ht_trendline.py index 79843bb5..75a7b5fd 100644 --- a/stock_indicators/indicators/ht_trendline.py +++ b/stock_indicators/indicators/ht_trendline.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_ht_trendline(quotes: Iterable[Quote]): @@ -60,6 +60,8 @@ def smooth_price(self, value): _T = TypeVar("_T", bound=HTTrendlineResult) + + class HTTrendlineResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Hilbert Transform Instantaneous Trendline (HTL) results. diff --git a/stock_indicators/indicators/hurst.py b/stock_indicators/indicators/hurst.py index cea24f49..3b0f3c72 100644 --- a/stock_indicators/indicators/hurst.py +++ b/stock_indicators/indicators/hurst.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_hurst(quotes: Iterable[Quote], lookback_periods: int = 100): @@ -47,6 +47,8 @@ def hurst_exponent(self, value): _T = TypeVar("_T", bound=HurstResult) + + class HurstResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Hurst Exponent results. diff --git a/stock_indicators/indicators/ichimoku.py b/stock_indicators/indicators/ichimoku.py index 946b5d4b..af1e369b 100644 --- a/stock_indicators/indicators/ichimoku.py +++ b/stock_indicators/indicators/ichimoku.py @@ -2,12 +2,12 @@ from typing import Iterable, Optional, TypeVar, overload from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.helpers import CondenseMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @overload @@ -17,31 +17,41 @@ def get_ichimoku( kijun_periods: int = 26, senkou_b_periods: int = 52, ) -> "IchimokuResults[IchimokuResult]": ... + + @overload def get_ichimoku( quotes: Iterable[Quote], tenkan_periods: int, kijun_periods: int, senkou_b_periods: int, + *, offset_periods: int, ) -> "IchimokuResults[IchimokuResult]": ... + + @overload def get_ichimoku( quotes: Iterable[Quote], tenkan_periods: int, kijun_periods: int, senkou_b_periods: int, + *, senkou_offset: int, chikou_offset: int, ) -> "IchimokuResults[IchimokuResult]": ... + + def get_ichimoku( quotes: Iterable[Quote], - tenkan_periods: int = None, - kijun_periods: int = None, - senkou_b_periods: int = None, - senkou_offset: int = None, - chikou_offset: int = None, -): + tenkan_periods: int = 9, + kijun_periods: int = 26, + senkou_b_periods: int = 52, + senkou_offset: Optional[int] = None, + chikou_offset: Optional[int] = None, + *, + offset_periods: Optional[int] = None, +) -> "IchimokuResults[IchimokuResult]": # pylint: disable=too-many-positional-arguments """Get Ichimoku Cloud calculated. Ichimoku Cloud, also known as Ichimoku Kinkō Hyō, is a collection of indicators @@ -78,14 +88,16 @@ def get_ichimoku( - [Ichimoku Cloud Reference](https://python.stockindicators.dev/indicators/Ichimoku/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ + # Normalize offset_periods into senkou_offset and chikou_offset + if offset_periods is not None: + if senkou_offset is None: + senkou_offset = offset_periods + if chikou_offset is None: + chikou_offset = offset_periods + + # Apply default logic when offsets are still None if chikou_offset is None: if senkou_offset is None: - if tenkan_periods is None: - tenkan_periods = 9 - if kijun_periods is None: - kijun_periods = 26 - if senkou_b_periods is None: - senkou_b_periods = 52 senkou_offset = kijun_periods chikou_offset = senkou_offset diff --git a/stock_indicators/indicators/kama.py b/stock_indicators/indicators/kama.py index 8fefb9fc..720e3fea 100644 --- a/stock_indicators/indicators/kama.py +++ b/stock_indicators/indicators/kama.py @@ -3,15 +3,19 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_kama(quotes: Iterable[Quote], er_periods: int = 10, - fast_periods: int = 2, slow_periods: int = 30): +def get_kama( + quotes: Iterable[Quote], + er_periods: int = 10, + fast_periods: int = 2, + slow_periods: int = 30, +): """Get KAMA calculated. - Kaufman’s Adaptive Moving Average (KAMA) is an volatility + Kaufman's Adaptive Moving Average (KAMA) is an volatility adaptive moving average of Close price over configurable lookback periods. Parameters: @@ -35,14 +39,15 @@ def get_kama(quotes: Iterable[Quote], er_periods: int = 10, - [KAMA Reference](https://python.stockindicators.dev/indicators/Kama/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetKama[Quote](CsList(Quote, quotes), er_periods, - fast_periods, slow_periods) + results = CsIndicator.GetKama[Quote]( + CsList(Quote, quotes), er_periods, fast_periods, slow_periods + ) return KAMAResults(results, KAMAResult) class KAMAResult(ResultBase): """ - A wrapper class for a single unit of Kaufman’s Adaptive Moving Average (KAMA) results. + A wrapper class for a single unit of Kaufman's Adaptive Moving Average (KAMA) results. """ @property @@ -63,9 +68,11 @@ def kama(self, value): _T = TypeVar("_T", bound=KAMAResult) + + class KAMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ - A wrapper class for the list of Kaufman’s Adaptive Moving Average (KAMA) results. + A wrapper class for the list of Kaufman's Adaptive Moving Average (KAMA) results. It is exactly same with built-in `list` except for that it provides some useful helper methods written in CSharp implementation. """ diff --git a/stock_indicators/indicators/keltner.py b/stock_indicators/indicators/keltner.py index 57b79399..94920fbd 100644 --- a/stock_indicators/indicators/keltner.py +++ b/stock_indicators/indicators/keltner.py @@ -3,12 +3,16 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_keltner(quotes: Iterable[Quote], ema_periods: int = 20, - multiplier: float = 2, atr_periods: int = 10): +def get_keltner( + quotes: Iterable[Quote], + ema_periods: int = 20, + multiplier: float = 2, + atr_periods: int = 10, +): """Get Keltner Channels calculated. Keltner Channels are based on an EMA centerline andATR band widths. @@ -35,8 +39,9 @@ def get_keltner(quotes: Iterable[Quote], ema_periods: int = 20, - [Keltner Channels Reference](https://python.stockindicators.dev/indicators/Keltner/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetKeltner[Quote](CsList(Quote, quotes), ema_periods, - multiplier, atr_periods) + results = CsIndicator.GetKeltner[Quote]( + CsList(Quote, quotes), ema_periods, multiplier, atr_periods + ) return KeltnerResults(results, KeltnerResult) @@ -79,6 +84,8 @@ def width(self, value): _T = TypeVar("_T", bound=KeltnerResult) + + class KeltnerResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Keltner Channels results. diff --git a/stock_indicators/indicators/kvo.py b/stock_indicators/indicators/kvo.py index cea328dc..9bcb2d8f 100644 --- a/stock_indicators/indicators/kvo.py +++ b/stock_indicators/indicators/kvo.py @@ -3,12 +3,16 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_kvo(quotes: Iterable[Quote], fast_periods: int = 34, - slow_periods: int = 55, signal_periods: int = 13): +def get_kvo( + quotes: Iterable[Quote], + fast_periods: int = 34, + slow_periods: int = 55, + signal_periods: int = 13, +): """Get KVO calculated. Klinger Volume Oscillator (KVO) depicts volume-based divergence @@ -35,8 +39,9 @@ def get_kvo(quotes: Iterable[Quote], fast_periods: int = 34, - [KVO Reference](https://python.stockindicators.dev/indicators/Kvo/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetKvo[Quote](CsList(Quote, quotes), fast_periods, - slow_periods, signal_periods) + results = CsIndicator.GetKvo[Quote]( + CsList(Quote, quotes), fast_periods, slow_periods, signal_periods + ) return KVOResults(results, KVOResult) @@ -63,6 +68,8 @@ def signal(self, value): _T = TypeVar("_T", bound=KVOResult) + + class KVOResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Klinger Volume Oscillator (KVO) results. diff --git a/stock_indicators/indicators/ma_envelopes.py b/stock_indicators/indicators/ma_envelopes.py index 55ccccd7..5be93337 100644 --- a/stock_indicators/indicators/ma_envelopes.py +++ b/stock_indicators/indicators/ma_envelopes.py @@ -4,12 +4,16 @@ from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.enums import MAType from stock_indicators.indicators.common.helpers import CondenseMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_ma_envelopes(quotes: Iterable[Quote], lookback_periods: int, - percent_offset: float = 2.5, ma_type: MAType = MAType.SMA): +def get_ma_envelopes( + quotes: Iterable[Quote], + lookback_periods: int, + percent_offset: float = 2.5, + ma_type: MAType = MAType.SMA, +): """Get Moving Average Envelopes calculated. Moving Average Envelopes is a price band overlay that is offset @@ -36,8 +40,9 @@ def get_ma_envelopes(quotes: Iterable[Quote], lookback_periods: int, - [Moving Average Envelopes Reference](https://python.stockindicators.dev/indicators/MaEnvelopes/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetMaEnvelopes[Quote](CsList(Quote, quotes), lookback_periods, - percent_offset, ma_type.cs_value) + results = CsIndicator.GetMaEnvelopes[Quote]( + CsList(Quote, quotes), lookback_periods, percent_offset, ma_type.cs_value + ) return MAEnvelopeResults(results, MAEnvelopeResult) @@ -72,6 +77,8 @@ def lower_envelope(self, value): _T = TypeVar("_T", bound=MAEnvelopeResult) + + class MAEnvelopeResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of Moving Average Envelopes results. diff --git a/stock_indicators/indicators/macd.py b/stock_indicators/indicators/macd.py index 647537b6..df3574dd 100644 --- a/stock_indicators/indicators/macd.py +++ b/stock_indicators/indicators/macd.py @@ -3,13 +3,17 @@ from stock_indicators._cslib import CsIndicator from stock_indicators.indicators.common.enums import CandlePart from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_macd(quotes: Iterable[Quote], fast_periods: int = 12, - slow_periods: int = 26, signal_periods: int = 9, - candle_part: CandlePart = CandlePart.CLOSE): +def get_macd( + quotes: Iterable[Quote], + fast_periods: int = 12, + slow_periods: int = 26, + signal_periods: int = 9, + candle_part: CandlePart = CandlePart.CLOSE, +): """Get MACD calculated. Moving Average Convergence/Divergence (MACD) is a simple oscillator view @@ -39,9 +43,11 @@ def get_macd(quotes: Iterable[Quote], fast_periods: int = 12, - [MACD Reference](https://python.stockindicators.dev/indicators/Macd/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - quotes = Quote.use(quotes, candle_part) # Error occurs if not assigned to local var. - macd_results = CsIndicator.GetMacd(quotes, fast_periods, - slow_periods, signal_periods) + # pylint: disable=no-member # Error occurs if not assigned to local var. + quotes = Quote.use(quotes, candle_part) + macd_results = CsIndicator.GetMacd( + quotes, fast_periods, slow_periods, signal_periods + ) return MACDResults(macd_results, MACDResult) @@ -92,7 +98,9 @@ def slow_ema(self, value): _T = TypeVar("_T", bound=MACDResult) -class MACDResults(CondenseMixin, RemoveWarmupMixin ,IndicatorResults[_T]): + + +class MACDResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of MACD(Moving Average Convergence/Divergence) results. It is exactly same with built-in `list` except for that it provides diff --git a/stock_indicators/indicators/mama.py b/stock_indicators/indicators/mama.py index f48e7cde..369041d3 100644 --- a/stock_indicators/indicators/mama.py +++ b/stock_indicators/indicators/mama.py @@ -3,12 +3,13 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_mama(quotes: Iterable[Quote], fast_limit: float = 0.5, - slow_limit: float = 0.05): +def get_mama( + quotes: Iterable[Quote], fast_limit: float = 0.5, slow_limit: float = 0.05 +): """Get MAMA calculated. MESA Adaptive Moving Average (MAMA) is a 5-period @@ -32,8 +33,7 @@ def get_mama(quotes: Iterable[Quote], fast_limit: float = 0.5, - [MAMA Reference](https://python.stockindicators.dev/indicators/Mama/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetMama[Quote](CsList(Quote, quotes), fast_limit, - slow_limit) + results = CsIndicator.GetMama[Quote](CsList(Quote, quotes), fast_limit, slow_limit) return MAMAResults(results, MAMAResult) @@ -60,6 +60,8 @@ def fama(self, value): _T = TypeVar("_T", bound=MAMAResult) + + class MAMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of MESA Adaptive Moving Average (MAMA) results. diff --git a/stock_indicators/indicators/mfi.py b/stock_indicators/indicators/mfi.py index f610c4d2..90edc546 100644 --- a/stock_indicators/indicators/mfi.py +++ b/stock_indicators/indicators/mfi.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_mfi(quotes: Iterable[Quote], lookback_periods: int = 14): @@ -47,6 +47,8 @@ def mfi(self, value): _T = TypeVar("_T", bound=MFIResult) + + class MFIResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Money Flow Index (MFI) results. diff --git a/stock_indicators/indicators/obv.py b/stock_indicators/indicators/obv.py index 0d3754cc..88ed0041 100644 --- a/stock_indicators/indicators/obv.py +++ b/stock_indicators/indicators/obv.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_obv(quotes: Iterable[Quote], sma_periods: Optional[int] = None): @@ -55,6 +55,8 @@ def obv_sma(self, value): _T = TypeVar("_T", bound=OBVResult) + + class OBVResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of On-balance Volume (OBV) results. diff --git a/stock_indicators/indicators/parabolic_sar.py b/stock_indicators/indicators/parabolic_sar.py index b3bd3b79..2728be25 100644 --- a/stock_indicators/indicators/parabolic_sar.py +++ b/stock_indicators/indicators/parabolic_sar.py @@ -3,18 +3,26 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @overload -def get_parabolic_sar(quotes: Iterable[Quote], acceleration_step: float = 0.02, - max_acceleration_factor: float = 0.2) -> "ParabolicSARResults[ParabolicSARResult]": ... +def get_parabolic_sar( + quotes: Iterable[Quote], + acceleration_step: float = 0.02, + max_acceleration_factor: float = 0.2, +) -> "ParabolicSARResults[ParabolicSARResult]": ... @overload -def get_parabolic_sar(quotes: Iterable[Quote], acceleration_step: float, - max_acceleration_factor: float, initial_factor: float) -> "ParabolicSARResults[ParabolicSARResult]": ... -def get_parabolic_sar(quotes, acceleration_step = None, - max_acceleration_factor = None, initial_factor = None): +def get_parabolic_sar( + quotes: Iterable[Quote], + acceleration_step: float, + max_acceleration_factor: float, + initial_factor: float, +) -> "ParabolicSARResults[ParabolicSARResult]": ... +def get_parabolic_sar( + quotes, acceleration_step=None, max_acceleration_factor=None, initial_factor=None +): """Get Parabolic SAR calculated. Parabolic SAR (stop and reverse) is a price-time based indicator @@ -42,13 +50,20 @@ def get_parabolic_sar(quotes, acceleration_step = None, - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ if initial_factor is None: - if acceleration_step is None: acceleration_step = 0.02 - if max_acceleration_factor is None: max_acceleration_factor = 0.2 - results = CsIndicator.GetParabolicSar[Quote](CsList(Quote, quotes), acceleration_step, - max_acceleration_factor) + if acceleration_step is None: + acceleration_step = 0.02 + if max_acceleration_factor is None: + max_acceleration_factor = 0.2 + results = CsIndicator.GetParabolicSar[Quote]( + CsList(Quote, quotes), acceleration_step, max_acceleration_factor + ) else: - results = CsIndicator.GetParabolicSar[Quote](CsList(Quote, quotes), acceleration_step, - max_acceleration_factor, initial_factor) + results = CsIndicator.GetParabolicSar[Quote]( + CsList(Quote, quotes), + acceleration_step, + max_acceleration_factor, + initial_factor, + ) return ParabolicSARResults(results, ParabolicSARResult) @@ -76,6 +91,8 @@ def is_reversal(self, value): _T = TypeVar("_T", bound=ParabolicSARResult) + + class ParabolicSARResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Parabolic SAR(stop and reverse) results. diff --git a/stock_indicators/indicators/pivot_points.py b/stock_indicators/indicators/pivot_points.py index 76b3b372..957b353d 100644 --- a/stock_indicators/indicators/pivot_points.py +++ b/stock_indicators/indicators/pivot_points.py @@ -2,13 +2,13 @@ from typing import Optional, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList 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.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_pivot_points( diff --git a/stock_indicators/indicators/pivots.py b/stock_indicators/indicators/pivots.py index dff4e652..1a4e7281 100644 --- a/stock_indicators/indicators/pivots.py +++ b/stock_indicators/indicators/pivots.py @@ -2,13 +2,13 @@ from typing import Iterable, Optional, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList 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.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_pivots( diff --git a/stock_indicators/indicators/pmo.py b/stock_indicators/indicators/pmo.py index 1e42dbd9..5af2697a 100644 --- a/stock_indicators/indicators/pmo.py +++ b/stock_indicators/indicators/pmo.py @@ -3,12 +3,16 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_pmo(quotes: Iterable[Quote], time_periods: int = 35, - smooth_periods: int = 20, signal_periods: int = 10): +def get_pmo( + quotes: Iterable[Quote], + time_periods: int = 35, + smooth_periods: int = 20, + signal_periods: int = 10, +): """Get PMO calculated. Price Momentum Oscillator (PMO) is double-smoothed ROC @@ -35,8 +39,9 @@ def get_pmo(quotes: Iterable[Quote], time_periods: int = 35, - [PMO Reference](https://python.stockindicators.dev/indicators/Pmo/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetPmo[Quote](CsList(Quote, quotes), time_periods, - smooth_periods, signal_periods) + results = CsIndicator.GetPmo[Quote]( + CsList(Quote, quotes), time_periods, smooth_periods, signal_periods + ) return PMOResults(results, PMOResult) @@ -63,6 +68,8 @@ def signal(self, value): _T = TypeVar("_T", bound=PMOResult) + + class PMOResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Price Momentum Oscillator (PMO) results. diff --git a/stock_indicators/indicators/prs.py b/stock_indicators/indicators/prs.py index 9eba1070..f54ce2b1 100644 --- a/stock_indicators/indicators/prs.py +++ b/stock_indicators/indicators/prs.py @@ -3,12 +3,16 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_prs(eval_quotes: Iterable[Quote], base_quotes: Iterable[Quote], - lookback_periods: Optional[int] = None, sma_periods: Optional[int] = None): +def get_prs( + eval_quotes: Iterable[Quote], + base_quotes: Iterable[Quote], + lookback_periods: Optional[int] = None, + sma_periods: Optional[int] = None, +): """Get PRS calculated. Price Relative Strength (PRS), also called Comparative Relative Strength, @@ -40,8 +44,12 @@ def get_prs(eval_quotes: Iterable[Quote], base_quotes: Iterable[Quote], - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetPrs[Quote](CsList(Quote, eval_quotes), CsList(Quote, base_quotes), - lookback_periods, sma_periods) + results = CsIndicator.GetPrs[Quote]( + CsList(Quote, eval_quotes), + CsList(Quote, base_quotes), + lookback_periods, + sma_periods, + ) return PRSResults(results, PRSResult) diff --git a/stock_indicators/indicators/pvo.py b/stock_indicators/indicators/pvo.py index f1eba34f..46b33001 100644 --- a/stock_indicators/indicators/pvo.py +++ b/stock_indicators/indicators/pvo.py @@ -3,12 +3,16 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_pvo(quotes: Iterable[Quote], fast_periods: int = 12, - slow_periods: int = 26, signal_periods: int = 9): +def get_pvo( + quotes: Iterable[Quote], + fast_periods: int = 12, + slow_periods: int = 26, + signal_periods: int = 9, +): """Get PVO calculated. Percentage Volume Oscillator (PVO) is a simple oscillator view @@ -35,8 +39,9 @@ def get_pvo(quotes: Iterable[Quote], fast_periods: int = 12, - [PVO Reference](https://python.stockindicators.dev/indicators/Pvo/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetPvo[Quote](CsList(Quote, quotes), fast_periods, - slow_periods, signal_periods) + results = CsIndicator.GetPvo[Quote]( + CsList(Quote, quotes), fast_periods, slow_periods, signal_periods + ) return PVOResults(results, PVOResult) @@ -71,6 +76,8 @@ def histogram(self, value): _T = TypeVar("_T", bound=PVOResult) + + class PVOResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Percentage Volume Oscillator (PVO) results. diff --git a/stock_indicators/indicators/renko.py b/stock_indicators/indicators/renko.py index 19845810..b1a80fd4 100644 --- a/stock_indicators/indicators/renko.py +++ b/stock_indicators/indicators/renko.py @@ -2,12 +2,12 @@ from typing import Iterable, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.enums import EndType -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_renko( diff --git a/stock_indicators/indicators/roc.py b/stock_indicators/indicators/roc.py index 98d473f4..5738ae61 100644 --- a/stock_indicators/indicators/roc.py +++ b/stock_indicators/indicators/roc.py @@ -3,11 +3,15 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_roc(quotes: Iterable[Quote], lookback_periods: int, sma_periods: int = None): +def get_roc( + quotes: Iterable[Quote], + lookback_periods: int, + sma_periods: Optional[int] = None, +): """Get ROC calculated. Rate of Change (ROC), also known as Momentum Oscillator, is the percent change @@ -31,10 +35,18 @@ def get_roc(quotes: Iterable[Quote], lookback_periods: int, sma_periods: int = N - [ROC Reference](https://python.stockindicators.dev/indicators/Roc/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetRoc[Quote](CsList(Quote, quotes), lookback_periods, sma_periods) + results = CsIndicator.GetRoc[Quote]( + CsList(Quote, quotes), lookback_periods, sma_periods + ) return ROCResults(results, ROCResult) -def get_roc_with_band(quotes: Iterable[Quote], lookback_periods: int, ema_periods: int, std_dev_periods: int): + +def get_roc_with_band( + quotes: Iterable[Quote], + lookback_periods: int, + ema_periods: int, + std_dev_periods: int, +): """Get ROCWB calculated. Rate of Change with Bands (ROCWB) is the percent change of Close price @@ -61,7 +73,9 @@ def get_roc_with_band(quotes: Iterable[Quote], lookback_periods: int, ema_period - [ROCWB Reference](https://python.stockindicators.dev/indicators/Roc/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetRocWb[Quote](CsList(Quote, quotes), lookback_periods, ema_periods, std_dev_periods) + results = CsIndicator.GetRocWb[Quote]( + CsList(Quote, quotes), lookback_periods, ema_periods, std_dev_periods + ) return ROCWBResults(results, ROCWBResult) @@ -96,6 +110,8 @@ def roc_sma(self, value): _T = TypeVar("_T", bound=ROCResult) + + class ROCResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of ROC(Rate of Change) results. @@ -143,6 +159,8 @@ def lower_band(self, value): _T = TypeVar("_T", bound=ROCWBResult) + + class ROCWBResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of ROC(Rate of Change) with band results. diff --git a/stock_indicators/indicators/rolling_pivots.py b/stock_indicators/indicators/rolling_pivots.py index 445add99..c382c70e 100644 --- a/stock_indicators/indicators/rolling_pivots.py +++ b/stock_indicators/indicators/rolling_pivots.py @@ -2,13 +2,13 @@ from typing import Iterable, Optional, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList 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.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_rolling_pivots( diff --git a/stock_indicators/indicators/rsi.py b/stock_indicators/indicators/rsi.py index a55352d6..f6a37b95 100644 --- a/stock_indicators/indicators/rsi.py +++ b/stock_indicators/indicators/rsi.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_rsi(quotes: Iterable[Quote], lookback_periods: int = 14): @@ -47,6 +47,8 @@ def rsi(self, value): _T = TypeVar("_T", bound=RSIResult) + + class RSIResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of RSI(Relative Strength Index) results. diff --git a/stock_indicators/indicators/slope.py b/stock_indicators/indicators/slope.py index 28a534d6..356b6bb9 100644 --- a/stock_indicators/indicators/slope.py +++ b/stock_indicators/indicators/slope.py @@ -2,12 +2,12 @@ from typing import Iterable, Optional, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_slope(quotes: Iterable[Quote], lookback_periods: int): diff --git a/stock_indicators/indicators/sma.py b/stock_indicators/indicators/sma.py index 8fcd4688..c90cb5a3 100644 --- a/stock_indicators/indicators/sma.py +++ b/stock_indicators/indicators/sma.py @@ -4,12 +4,15 @@ from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.enums import CandlePart from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_sma(quotes: Iterable[Quote], lookback_periods: int, - candle_part: CandlePart = CandlePart.CLOSE): +def get_sma( + quotes: Iterable[Quote], + lookback_periods: int, + candle_part: CandlePart = CandlePart.CLOSE, +): """Get SMA calculated. Simple Moving Average (SMA) is the average of price over a lookback window. @@ -32,8 +35,8 @@ def get_sma(quotes: Iterable[Quote], lookback_periods: int, - [SMA Reference](https://python.stockindicators.dev/indicators/Sma/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - quotes = Quote.use( - quotes, candle_part) # Error occurs if not assigned to local var. + # pylint: disable=no-member # Error occurs if not assigned to local var. + quotes = Quote.use(quotes, candle_part) results = CsIndicator.GetSma(quotes, lookback_periods) return SMAResults(results, SMAResult) @@ -61,7 +64,8 @@ def get_sma_analysis(quotes: Iterable[Quote], lookback_periods: int): - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ sma_extended_list = CsIndicator.GetSmaAnalysis[Quote]( - CsList(Quote, quotes), lookback_periods) + CsList(Quote, quotes), lookback_periods + ) return SMAAnalysisResults(sma_extended_list, SMAAnalysisResult) @@ -80,6 +84,8 @@ def sma(self, value): _T = TypeVar("_T", bound=SMAResult) + + class SMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of SMA(Simple Moving Average) results. @@ -119,6 +125,8 @@ def mape(self, value): _T = TypeVar("_T", bound=SMAAnalysisResult) + + class SMAAnalysisResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of SMA Analysis results. diff --git a/stock_indicators/indicators/smi.py b/stock_indicators/indicators/smi.py index eaea2ffc..cf192ed6 100644 --- a/stock_indicators/indicators/smi.py +++ b/stock_indicators/indicators/smi.py @@ -3,13 +3,17 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_smi(quotes: Iterable[Quote], lookback_periods: int = 13, - first_smooth_periods: int = 25, second_smooth_periods: int = 2, - signal_periods: int = 3): +def get_smi( + quotes: Iterable[Quote], + lookback_periods: int = 13, + first_smooth_periods: int = 25, + second_smooth_periods: int = 2, + signal_periods: int = 3, +): """Get SMI calculated. Stochastic Momentum Index (SMI) is a double-smoothed variant of @@ -39,9 +43,13 @@ def get_smi(quotes: Iterable[Quote], lookback_periods: int = 13, - [SMI Reference](https://python.stockindicators.dev/indicators/Smi/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetSmi[Quote](CsList(Quote, quotes), lookback_periods, - first_smooth_periods, second_smooth_periods, - signal_periods) + results = CsIndicator.GetSmi[Quote]( + CsList(Quote, quotes), + lookback_periods, + first_smooth_periods, + second_smooth_periods, + signal_periods, + ) return SMIResults(results, SMIResult) @@ -68,6 +76,8 @@ def signal(self, value): _T = TypeVar("_T", bound=SMIResult) + + class SMIResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Stochastic Momentum Index (SMI) results. diff --git a/stock_indicators/indicators/smma.py b/stock_indicators/indicators/smma.py index 10a63c72..0d502e2e 100644 --- a/stock_indicators/indicators/smma.py +++ b/stock_indicators/indicators/smma.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_smma(quotes: Iterable[Quote], lookback_periods: int): @@ -47,6 +47,8 @@ def smma(self, value): _T = TypeVar("_T", bound=SMMAResult) + + class SMMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Smoothed Moving Average (SMMA) results. diff --git a/stock_indicators/indicators/starc_bands.py b/stock_indicators/indicators/starc_bands.py index 779d3f85..34f91390 100644 --- a/stock_indicators/indicators/starc_bands.py +++ b/stock_indicators/indicators/starc_bands.py @@ -1,15 +1,18 @@ from typing import Iterable, Optional, TypeVar -from warnings import warn from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_starc_bands(quotes: Iterable[Quote], sma_periods: int = None, - multiplier: float = 2, atr_periods: int = 10): +def get_starc_bands( + quotes: Iterable[Quote], + sma_periods: int = 20, + multiplier: float = 2, + atr_periods: int = 10, +): """Get STARC Bands calculated. Stoller Average Range Channel (STARC) Bands, are based @@ -36,12 +39,9 @@ def get_starc_bands(quotes: Iterable[Quote], sma_periods: int = None, - [STARC Bands Reference](https://python.stockindicators.dev/indicators/StarcBands/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - if sma_periods is None: - warn('The default value of sma_periods will be removed in the next version. Pass sma_periods explicitly.', DeprecationWarning, stacklevel=2) - sma_periods = 20 - - results = CsIndicator.GetStarcBands[Quote](CsList(Quote, quotes), sma_periods, - multiplier, atr_periods) + results = CsIndicator.GetStarcBands[Quote]( + CsList(Quote, quotes), sma_periods, multiplier, atr_periods + ) return STARCBandsResults(results, STARCBandsResult) @@ -76,6 +76,8 @@ def lower_band(self, value): _T = TypeVar("_T", bound=STARCBandsResult) + + class STARCBandsResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Stoller Average Range Channel (STARC) Bands results. diff --git a/stock_indicators/indicators/stc.py b/stock_indicators/indicators/stc.py index 92217822..edec7e4a 100644 --- a/stock_indicators/indicators/stc.py +++ b/stock_indicators/indicators/stc.py @@ -3,12 +3,16 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_stc(quotes: Iterable[Quote], cycle_periods: int = 10, - fast_periods: int = 23, slow_periods: int = 50): +def get_stc( + quotes: Iterable[Quote], + cycle_periods: int = 10, + fast_periods: int = 23, + slow_periods: int = 50, +): """Get STC calculated. Schaff Trend Cycle (STC) is a stochastic oscillator view @@ -35,8 +39,9 @@ def get_stc(quotes: Iterable[Quote], cycle_periods: int = 10, - [STC Reference](https://python.stockindicators.dev/indicators/Stc/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetStc[Quote](CsList(Quote, quotes), cycle_periods, - fast_periods, slow_periods) + results = CsIndicator.GetStc[Quote]( + CsList(Quote, quotes), cycle_periods, fast_periods, slow_periods + ) return STCResults(results, STCResult) @@ -55,6 +60,8 @@ def stc(self, value): _T = TypeVar("_T", bound=STCResult) + + class STCResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Schaff Trend Cycle (STC) results. diff --git a/stock_indicators/indicators/stdev.py b/stock_indicators/indicators/stdev.py index d7a3c767..038a0873 100644 --- a/stock_indicators/indicators/stdev.py +++ b/stock_indicators/indicators/stdev.py @@ -3,12 +3,13 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_stdev(quotes: Iterable[Quote], lookback_periods: int, - sma_periods: Optional[int] = None): +def get_stdev( + quotes: Iterable[Quote], lookback_periods: int, sma_periods: Optional[int] = None +): """Get Rolling Standard Deviation calculated. Rolling Standard Deviation of Close price over a lookback window. @@ -31,7 +32,9 @@ def get_stdev(quotes: Iterable[Quote], lookback_periods: int, - [Stdev Reference](https://python.stockindicators.dev/indicators/StdDev/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetStdDev[Quote](CsList(Quote, quotes), lookback_periods, sma_periods) + results = CsIndicator.GetStdDev[Quote]( + CsList(Quote, quotes), lookback_periods, sma_periods + ) return StdevResults(results, StdevResult) @@ -74,6 +77,8 @@ def stdev_sma(self, value): _T = TypeVar("_T", bound=StdevResult) + + class StdevResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Rolling Standard Deviation results. diff --git a/stock_indicators/indicators/stdev_channels.py b/stock_indicators/indicators/stdev_channels.py index ee62a96d..5e759f99 100644 --- a/stock_indicators/indicators/stdev_channels.py +++ b/stock_indicators/indicators/stdev_channels.py @@ -3,13 +3,15 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_stdev_channels(quotes: Iterable[Quote], - lookback_periods: Optional[int] = 20, - standard_deviations: float = 2): +def get_stdev_channels( + quotes: Iterable[Quote], + lookback_periods: Optional[int] = 20, + standard_deviations: float = 2, +): """Get Standard Deviation Channels calculated. Standard Deviation Channels are based on an linearregression centerline @@ -33,7 +35,9 @@ def get_stdev_channels(quotes: Iterable[Quote], - [Stdev Channels Reference](https://python.stockindicators.dev/indicators/StdDevChannels/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetStdDevChannels[Quote](CsList(Quote, quotes), lookback_periods, standard_deviations) + results = CsIndicator.GetStdDevChannels[Quote]( + CsList(Quote, quotes), lookback_periods, standard_deviations + ) return StdevChannelsResults(results, StdevChannelsResult) @@ -76,6 +80,8 @@ def break_point(self, value): _T = TypeVar("_T", bound=StdevChannelsResult) + + class StdevChannelsResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Standard Deviation Channels results. diff --git a/stock_indicators/indicators/stoch.py b/stock_indicators/indicators/stoch.py index 071587bb..2551699e 100644 --- a/stock_indicators/indicators/stoch.py +++ b/stock_indicators/indicators/stoch.py @@ -4,12 +4,19 @@ from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.enums import MAType from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_stoch(quotes: Iterable[Quote], lookback_periods: int = 14, signal_periods: int = 3, smooth_periods: int = 3, - k_factor: float = 3, d_factor: float = 2, ma_type: MAType = MAType.SMA): +def get_stoch( + quotes: Iterable[Quote], + lookback_periods: int = 14, # pylint: disable=too-many-positional-arguments + signal_periods: int = 3, + smooth_periods: int = 3, + k_factor: float = 3, + d_factor: float = 2, + ma_type: MAType = MAType.SMA, +): """Get Stochastic Oscillator calculated, with KDJ indexes. Stochastic Oscillatoris a momentum indicator that looks back N periods to produce a scale of 0 to 100. @@ -47,8 +54,15 @@ def get_stoch(quotes: Iterable[Quote], lookback_periods: int = 14, signal_period - [Stochastic Oscillator Reference](https://python.stockindicators.dev/indicators/Stoch/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - stoch_results = CsIndicator.GetStoch[Quote](CsList(Quote, quotes), lookback_periods, signal_periods, smooth_periods, - k_factor, d_factor, ma_type.cs_value) + stoch_results = CsIndicator.GetStoch[Quote]( + CsList(Quote, quotes), + lookback_periods, + signal_periods, + smooth_periods, + k_factor, + d_factor, + ma_type.cs_value, + ) return StochResults(stoch_results, StochResult) @@ -87,6 +101,8 @@ def percent_j(self, value): _T = TypeVar("_T", bound=StochResult) + + class StochResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Stochastic Oscillator(with KDJ Index) results. diff --git a/stock_indicators/indicators/stoch_rsi.py b/stock_indicators/indicators/stoch_rsi.py index 98221ca3..cf801555 100644 --- a/stock_indicators/indicators/stoch_rsi.py +++ b/stock_indicators/indicators/stoch_rsi.py @@ -3,11 +3,17 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_stoch_rsi(quotes: Iterable[Quote], rsi_periods: int, stoch_periods: int, signal_periods: int, smooth_periods: int = 1): +def get_stoch_rsi( + quotes: Iterable[Quote], + rsi_periods: int, + stoch_periods: int, + signal_periods: int, + smooth_periods: int = 1, +): """Get Stochastic RSI calculated. Stochastic RSI is a Stochastic interpretation of the Relative Strength Index. @@ -36,7 +42,13 @@ def get_stoch_rsi(quotes: Iterable[Quote], rsi_periods: int, stoch_periods: int, - [Stochastic RSI Reference](https://python.stockindicators.dev/indicators/StochRsi/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - stoch_rsi_results = CsIndicator.GetStochRsi[Quote](CsList(Quote, quotes), rsi_periods, stoch_periods, signal_periods, smooth_periods) + stoch_rsi_results = CsIndicator.GetStochRsi[Quote]( + CsList(Quote, quotes), + rsi_periods, + stoch_periods, + signal_periods, + smooth_periods, + ) return StochRSIResults(stoch_rsi_results, StochRSIResult) @@ -63,6 +75,8 @@ def signal(self, value): _T = TypeVar("_T", bound=StochRSIResult) + + class StochRSIResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Stochastic RSI results. diff --git a/stock_indicators/indicators/super_trend.py b/stock_indicators/indicators/super_trend.py index 78394d59..5ee60d11 100644 --- a/stock_indicators/indicators/super_trend.py +++ b/stock_indicators/indicators/super_trend.py @@ -2,12 +2,12 @@ from typing import Iterable, Optional, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import to_pydecimal_via_double from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_super_trend( diff --git a/stock_indicators/indicators/t3.py b/stock_indicators/indicators/t3.py index 4cea9652..fbd0e276 100644 --- a/stock_indicators/indicators/t3.py +++ b/stock_indicators/indicators/t3.py @@ -3,12 +3,13 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_t3(quotes: Iterable[Quote], lookback_periods: int = 5, - volume_factor: float = 0.7): +def get_t3( + quotes: Iterable[Quote], lookback_periods: int = 5, volume_factor: float = 0.7 +): """Get T3 calculated. Tillson T3 is a smooth moving average that reduces @@ -32,8 +33,9 @@ def get_t3(quotes: Iterable[Quote], lookback_periods: int = 5, - [T3 Reference](https://python.stockindicators.dev/indicators/T3/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetT3[Quote](CsList(Quote, quotes), lookback_periods, - volume_factor) + results = CsIndicator.GetT3[Quote]( + CsList(Quote, quotes), lookback_periods, volume_factor + ) return T3Results(results, T3Result) @@ -52,6 +54,8 @@ def t3(self, value): _T = TypeVar("_T", bound=T3Result) + + class T3Results(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of Tillson T3 results. diff --git a/stock_indicators/indicators/tema.py b/stock_indicators/indicators/tema.py index 1cbf93c8..f856e2e3 100644 --- a/stock_indicators/indicators/tema.py +++ b/stock_indicators/indicators/tema.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_tema(quotes: Iterable[Quote], lookback_periods: int): @@ -47,6 +47,8 @@ def tema(self, value): _T = TypeVar("_T", bound=TEMAResult) + + class TEMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Triple Exponential Moving Average (TEMA) results. diff --git a/stock_indicators/indicators/tr.py b/stock_indicators/indicators/tr.py index e5b260f9..90b99456 100644 --- a/stock_indicators/indicators/tr.py +++ b/stock_indicators/indicators/tr.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_tr(quotes: Iterable[Quote]): @@ -43,6 +43,8 @@ def tr(self, value): _T = TypeVar("_T", bound=TrResult) + + class TrResults(CondenseMixin, IndicatorResults[_T]): """ A wrapper class for the list of True Range (TR) results. diff --git a/stock_indicators/indicators/trix.py b/stock_indicators/indicators/trix.py index 4ce81b96..1d103070 100644 --- a/stock_indicators/indicators/trix.py +++ b/stock_indicators/indicators/trix.py @@ -3,11 +3,13 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_trix(quotes: Iterable[Quote], lookback_periods: int, signal_periods: Optional[int] = None): +def get_trix( + quotes: Iterable[Quote], lookback_periods: int, signal_periods: Optional[int] = None +): """Get TRIX calculated. Triple EMA Oscillator (TRIX) is the rate of change for a 3 EMA smoothing of the Close price over a lookback window. @@ -31,7 +33,9 @@ def get_trix(quotes: Iterable[Quote], lookback_periods: int, signal_periods: Opt - [TRIX Reference](https://python.stockindicators.dev/indicators/Trix/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetTrix[Quote](CsList(Quote, quotes), lookback_periods, signal_periods) + results = CsIndicator.GetTrix[Quote]( + CsList(Quote, quotes), lookback_periods, signal_periods + ) return TRIXResults(results, TRIXResult) @@ -66,6 +70,8 @@ def signal(self, value): _T = TypeVar("_T", bound=TRIXResult) + + class TRIXResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Triple EMA Oscillator (TRIX) results. diff --git a/stock_indicators/indicators/tsi.py b/stock_indicators/indicators/tsi.py index 128e6172..c87690ca 100644 --- a/stock_indicators/indicators/tsi.py +++ b/stock_indicators/indicators/tsi.py @@ -3,12 +3,16 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_tsi(quotes: Iterable[Quote], lookback_periods: int = 25, - smooth_periods: int = 13, signal_periods: int = 7): +def get_tsi( + quotes: Iterable[Quote], + lookback_periods: int = 25, + smooth_periods: int = 13, + signal_periods: int = 7, +): """Get TSI calculated. True Strength Index (TSI) is a momentum oscillator @@ -35,8 +39,9 @@ def get_tsi(quotes: Iterable[Quote], lookback_periods: int = 25, - [TSI Reference](https://python.stockindicators.dev/indicators/Tsi/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetTsi[Quote](CsList(Quote, quotes), lookback_periods, - smooth_periods, signal_periods) + results = CsIndicator.GetTsi[Quote]( + CsList(Quote, quotes), lookback_periods, smooth_periods, signal_periods + ) return TSIResults(results, TSIResult) @@ -63,6 +68,8 @@ def signal(self, value): _T = TypeVar("_T", bound=TSIResult) + + class TSIResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of True Strength Index (TSI) results. diff --git a/stock_indicators/indicators/ulcer_index.py b/stock_indicators/indicators/ulcer_index.py index 92e859ec..780fd428 100644 --- a/stock_indicators/indicators/ulcer_index.py +++ b/stock_indicators/indicators/ulcer_index.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_ulcer_index(quotes: Iterable[Quote], lookback_periods: int = 14): @@ -47,6 +47,8 @@ def ui(self, value): _T = TypeVar("_T", bound=UlcerIndexResult) + + class UlcerIndexResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Ulcer Index (UI) results. diff --git a/stock_indicators/indicators/ultimate.py b/stock_indicators/indicators/ultimate.py index 88033fa8..855e71fc 100644 --- a/stock_indicators/indicators/ultimate.py +++ b/stock_indicators/indicators/ultimate.py @@ -3,12 +3,16 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_ultimate(quotes: Iterable[Quote], short_periods: int = 7, - middle_periods: int = 14, long_periods: int = 28): +def get_ultimate( + quotes: Iterable[Quote], + short_periods: int = 7, + middle_periods: int = 14, + long_periods: int = 28, +): """Get Ultimate Oscillator calculated. Ultimate Oscillator uses several lookback periods to weigh buying power @@ -35,8 +39,9 @@ def get_ultimate(quotes: Iterable[Quote], short_periods: int = 7, - [Ultimate Oscillator Reference](https://python.stockindicators.dev/indicators/Ultimate/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetUltimate[Quote](CsList(Quote, quotes), short_periods, - middle_periods, long_periods) + results = CsIndicator.GetUltimate[Quote]( + CsList(Quote, quotes), short_periods, middle_periods, long_periods + ) return UltimateResults(results, UltimateResult) @@ -55,6 +60,8 @@ def ultimate(self, value): _T = TypeVar("_T", bound=UltimateResult) + + class UltimateResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Ultimate Oscillator results. diff --git a/stock_indicators/indicators/volatility_stop.py b/stock_indicators/indicators/volatility_stop.py index ac78d9f0..9196a633 100644 --- a/stock_indicators/indicators/volatility_stop.py +++ b/stock_indicators/indicators/volatility_stop.py @@ -3,12 +3,13 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_volatility_stop(quotes: Iterable[Quote], lookback_periods: int = 7, - multiplier: float = 3): +def get_volatility_stop( + quotes: Iterable[Quote], lookback_periods: int = 7, multiplier: float = 3 +): """Get Volatility Stop calculated. Volatility Stop is an ATR based indicator used to @@ -32,8 +33,9 @@ def get_volatility_stop(quotes: Iterable[Quote], lookback_periods: int = 7, - [Volatility Stop Reference](https://python.stockindicators.dev/indicators/VolatilityStop/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - results = CsIndicator.GetVolatilityStop[Quote](CsList(Quote, quotes), lookback_periods, - multiplier) + results = CsIndicator.GetVolatilityStop[Quote]( + CsList(Quote, quotes), lookback_periods, multiplier + ) return VolatilityStopResults(results, VolatilityStopResult) @@ -76,6 +78,8 @@ def is_stop(self, value): _T = TypeVar("_T", bound=VolatilityStopResult) + + class VolatilityStopResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Volatility Stop results. diff --git a/stock_indicators/indicators/vortex.py b/stock_indicators/indicators/vortex.py index 3ace0fe0..f2c31f72 100644 --- a/stock_indicators/indicators/vortex.py +++ b/stock_indicators/indicators/vortex.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_vortex(quotes: Iterable[Quote], lookback_periods: int): @@ -56,6 +56,8 @@ def nvi(self, value): _T = TypeVar("_T", bound=VortexResult) + + class VortexResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Vortex Indicator (VI) results. diff --git a/stock_indicators/indicators/vwap.py b/stock_indicators/indicators/vwap.py index cec0e32e..c74ef86d 100644 --- a/stock_indicators/indicators/vwap.py +++ b/stock_indicators/indicators/vwap.py @@ -1,21 +1,41 @@ from datetime import datetime -from typing import Iterable, Optional, TypeVar, overload +from typing import Iterable, Optional, TypeVar, Union, overload from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import DateTime as CsDateTime +from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase @overload -def get_vwap(quotes: Iterable[Quote], start: Optional[datetime] = None) -> "VWAPResults[VWAPResult]": ... +def get_vwap( + quotes: Iterable[Quote], start: Optional[datetime] = None +) -> "VWAPResults[VWAPResult]": ... + + @overload -def get_vwap(quotes: Iterable[Quote], year: int, - month: int = 1, day: int = 1, - hour: int = 0, minute: int = 0) -> "VWAPResults[VWAPResult]": ... -def get_vwap(quotes, start = None, month = 1, day = 1, hour = 0, minute = 0): +def get_vwap( + quotes: Iterable[Quote], + year: int, + *, + month: int = 1, + day: int = 1, + hour: int = 0, + minute: int = 0, +) -> "VWAPResults[VWAPResult]": ... + + +def get_vwap( + quotes: Iterable[Quote], + start: Union[datetime, int, None] = None, + *legacy_date_parts, + month: int = 1, + day: int = 1, + hour: int = 0, + minute: int = 0, +) -> "VWAPResults[VWAPResult]": # pylint: disable=too-many-branches,too-many-statements,keyword-arg-before-vararg """Get VWAP calculated. Volume Weighted Average Price (VWAP) is a Volume weighted average @@ -39,10 +59,81 @@ def get_vwap(quotes, start = None, month = 1, day = 1, hour = 0, minute = 0): - [VWAP Fractal Reference](https://python.stockindicators.dev/indicators/Vwap/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - if isinstance(start, int): - start = datetime(start, month, day, hour, minute) - - results = CsIndicator.GetVwap[Quote](CsList(Quote, quotes), CsDateTime(start) if start else None) + # Backward compatibility: support positional year,month,day,hour as in tests + if legacy_date_parts: + # Validate legacy_date_parts is a sequence with at most 4 items + if not isinstance(legacy_date_parts, (tuple, list)): + raise TypeError("legacy_date_parts must be a sequence") + if len(legacy_date_parts) > 4: + raise ValueError( + "Too many positional date arguments (max 4: month, day, hour, minute)" + ) + + # Interpret: start is actually the year; legacy_date_parts supply remaining + if not isinstance(start, int): + raise TypeError( + "Year must be provided as an int when using legacy positional date form" + ) + year = start + + # Fill provided parts into month, day, hour, minute if given positionally + parts = list(legacy_date_parts) + if len(parts) > 0: + if not isinstance(parts[0], int): + raise TypeError("Month must be an int") + month = parts[0] + if len(parts) > 1: + if not isinstance(parts[1], int): + raise TypeError("Day must be an int") + day = parts[1] + if len(parts) > 2: + if not isinstance(parts[2], int): + raise TypeError("Hour must be an int") + hour = parts[2] + if len(parts) > 3: + if not isinstance(parts[3], int): + raise TypeError("Minute must be an int") + minute = parts[3] + + # Validate ranges + if not 1 <= month <= 12: + raise ValueError(f"Month must be 1-12, got {month}") + if not 1 <= day <= 31: + raise ValueError(f"Day must be 1-31, got {day}") + if not 0 <= hour <= 23: + raise ValueError(f"Hour must be 0-23, got {hour}") + if not 0 <= minute <= 59: + raise ValueError(f"Minute must be 0-59, got {minute}") + + start_dt = datetime(year, month, day, hour, minute) + else: + # When not using legacy parts, validate that start is either int (year) or datetime + if start is not None: + if isinstance(start, int): + # Using keyword arguments for date parts + year = start + # Validate ranges for keyword arguments + if not 1 <= month <= 12: + raise ValueError(f"Month must be 1-12, got {month}") + if not 1 <= day <= 31: + raise ValueError(f"Day must be 1-31, got {day}") + if not 0 <= hour <= 23: + raise ValueError(f"Hour must be 0-23, got {hour}") + if not 0 <= minute <= 59: + raise ValueError(f"Minute must be 0-59, got {minute}") + start_dt = datetime(year, month, day, hour, minute) + elif isinstance(start, datetime): + start_dt = start + else: + raise TypeError( + f"start must be int (year), datetime, or None, got {type(start).__name__}" + ) + else: + start_dt = None + + results = CsIndicator.GetVwap[Quote]( + CsList(Quote, quotes), CsDateTime(start_dt) if start_dt else None + ) return VWAPResults(results, VWAPResult) @@ -61,6 +152,8 @@ def vwap(self, value): _T = TypeVar("_T", bound=VWAPResult) + + class VWAPResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Volume Weighted Average Price (VWAP) results. diff --git a/stock_indicators/indicators/vwma.py b/stock_indicators/indicators/vwma.py index 9df7abfc..fd1c642f 100644 --- a/stock_indicators/indicators/vwma.py +++ b/stock_indicators/indicators/vwma.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_vwma(quotes: Iterable[Quote], lookback_periods: int): @@ -47,6 +47,8 @@ def vwma(self, value): _T = TypeVar("_T", bound=VWMAResult) + + class VWMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Volume Weighted Moving Average (VWMA) results. diff --git a/stock_indicators/indicators/williams_r.py b/stock_indicators/indicators/williams_r.py index 2276898c..f739649b 100644 --- a/stock_indicators/indicators/williams_r.py +++ b/stock_indicators/indicators/williams_r.py @@ -3,8 +3,8 @@ from stock_indicators._cslib import CsIndicator from stock_indicators._cstypes import List as CsList from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_williams_r(quotes: Iterable[Quote], lookback_periods: int = 14): @@ -48,6 +48,8 @@ def williams_r(self, value): _T = TypeVar("_T", bound=WilliamsResult) + + class WilliamsResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Williams %R results. diff --git a/stock_indicators/indicators/wma.py b/stock_indicators/indicators/wma.py index dc34b1f6..07eb0984 100644 --- a/stock_indicators/indicators/wma.py +++ b/stock_indicators/indicators/wma.py @@ -3,12 +3,15 @@ from stock_indicators._cslib import CsIndicator from stock_indicators.indicators.common.enums import CandlePart from stock_indicators.indicators.common.helpers import CondenseMixin, RemoveWarmupMixin -from stock_indicators.indicators.common.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase -def get_wma(quotes: Iterable[Quote], lookback_periods: int, - candle_part: CandlePart = CandlePart.CLOSE): +def get_wma( + quotes: Iterable[Quote], + lookback_periods: int, + candle_part: CandlePart = CandlePart.CLOSE, +): """Get WMA calculated. Weighted Moving Average (WMA) is the linear weighted average @@ -33,7 +36,7 @@ def get_wma(quotes: Iterable[Quote], lookback_periods: int, - [WMA Reference](https://python.stockindicators.dev/indicators/Wma/#content) - [Helper Methods](https://python.stockindicators.dev/utilities/#content) """ - quotes = Quote.use(quotes, candle_part) # Error occurs if not assigned to local var. + quotes = Quote.use(quotes, candle_part) # pylint: disable=no-member # Error occurs if not assigned to local var. results = CsIndicator.GetWma(quotes, lookback_periods) return WMAResults(results, WMAResult) @@ -53,6 +56,8 @@ def wma(self, value): _T = TypeVar("_T", bound=WMAResult) + + class WMAResults(CondenseMixin, RemoveWarmupMixin, IndicatorResults[_T]): """ A wrapper class for the list of Weighted Moving Average (WMA) results. diff --git a/stock_indicators/indicators/zig_zag.py b/stock_indicators/indicators/zig_zag.py index 4368a055..32e12709 100644 --- a/stock_indicators/indicators/zig_zag.py +++ b/stock_indicators/indicators/zig_zag.py @@ -2,13 +2,13 @@ from typing import Iterable, Optional, TypeVar from stock_indicators._cslib import CsIndicator -from stock_indicators._cstypes import List as CsList from stock_indicators._cstypes import Decimal as CsDecimal +from stock_indicators._cstypes import List as CsList 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.results import IndicatorResults, ResultBase from stock_indicators.indicators.common.quote import Quote +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase def get_zig_zag( diff --git a/tests/common/test-dateof-roundtrip-variants.py b/tests/common/test-dateof-roundtrip-variants.py index 38c68e72..657a273d 100644 --- a/tests/common/test-dateof-roundtrip-variants.py +++ b/tests/common/test-dateof-roundtrip-variants.py @@ -47,7 +47,7 @@ def _mk_date_only(): @pytest.mark.parametrize( - "variant, maker", + ("variant", "maker"), [ ("utc", _mk_utc), ("offset", _mk_offset), @@ -55,8 +55,9 @@ def _mk_date_only(): ("date_only", _mk_date_only), ], ) -def test_sma_roundtrip_dates_variants(_variant, maker): +def test_sma_roundtrip_dates_variants(variant, maker): quotes = maker() + assert variant results = indicators.get_sma(quotes, 2) assert len(results) == len(quotes) diff --git a/tests/common/test_candle.py b/tests/common/test_candle.py index 7653a529..7fb02b2c 100644 --- a/tests/common/test_candle.py +++ b/tests/common/test_candle.py @@ -1,41 +1,42 @@ from stock_indicators import indicators + class TestCandleResults: def test_standard(self, quotes): results = indicators.get_doji(quotes, 0.1) - + r = results[0] assert 212.80 == round(float(r.candle.close), 2) - assert 1.83 == round(float(r.candle.size), 2) - assert 0.19 == round(float(r.candle.body), 2) - assert 0.10 == round(float(r.candle.body_pct), 2) - assert 1.09 == round(float(r.candle.lower_wick), 2) - assert 0.60 == round(float(r.candle.lower_wick_pct), 2) - assert 0.55 == round(float(r.candle.upper_wick), 2) - assert 0.30 == round(float(r.candle.upper_wick_pct), 2) - assert r.candle.is_bearish == False - assert r.candle.is_bullish == True - + assert 1.83 == round(float(r.candle.size), 2) + assert 0.19 == round(float(r.candle.body), 2) + assert 0.10 == round(float(r.candle.body_pct), 2) + assert 1.09 == round(float(r.candle.lower_wick), 2) + assert 0.60 == round(float(r.candle.lower_wick_pct), 2) + assert 0.55 == round(float(r.candle.upper_wick), 2) + assert 0.30 == round(float(r.candle.upper_wick_pct), 2) + assert not r.candle.is_bearish + assert r.candle.is_bullish + r = results[351] assert 263.16 == round(float(r.candle.close), 2) - assert 1.24 == round(float(r.candle.size), 2) - assert 0.00 == round(float(r.candle.body), 2) - assert 0.00 == round(float(r.candle.body_pct), 2) - assert 0.55 == round(float(r.candle.lower_wick), 2) - assert 0.44 == round(float(r.candle.lower_wick_pct), 2) - assert 0.69 == round(float(r.candle.upper_wick), 2) - assert 0.56 == round(float(r.candle.upper_wick_pct), 2) - assert r.candle.is_bearish == False - assert r.candle.is_bullish == False - - r = results[501] + assert 1.24 == round(float(r.candle.size), 2) + assert 0.00 == round(float(r.candle.body), 2) + assert 0.00 == round(float(r.candle.body_pct), 2) + assert 0.55 == round(float(r.candle.lower_wick), 2) + assert 0.44 == round(float(r.candle.lower_wick_pct), 2) + assert 0.69 == round(float(r.candle.upper_wick), 2) + assert 0.56 == round(float(r.candle.upper_wick_pct), 2) + assert not r.candle.is_bearish + assert not r.candle.is_bullish + + r = results[501] assert 245.28 == round(float(r.candle.close), 2) - assert 2.67 == round(float(r.candle.size), 2) - assert 0.36 == round(float(r.candle.body), 2) - assert 0.13 == round(float(r.candle.body_pct), 2) - assert 2.05 == round(float(r.candle.lower_wick), 2) - assert 0.77 == round(float(r.candle.lower_wick_pct), 2) - assert 0.26 == round(float(r.candle.upper_wick), 2) - assert 0.10 == round(float(r.candle.upper_wick_pct), 2) - assert r.candle.is_bearish == False - assert r.candle.is_bullish == True + assert 2.67 == round(float(r.candle.size), 2) + assert 0.36 == round(float(r.candle.body), 2) + assert 0.13 == round(float(r.candle.body_pct), 2) + assert 2.05 == round(float(r.candle.lower_wick), 2) + assert 0.77 == round(float(r.candle.lower_wick_pct), 2) + assert 0.26 == round(float(r.candle.upper_wick), 2) + assert 0.10 == round(float(r.candle.upper_wick_pct), 2) + assert not r.candle.is_bearish + assert r.candle.is_bullish diff --git a/tests/common/test_common.py b/tests/common/test_common.py index d52df668..afe46266 100644 --- a/tests/common/test_common.py +++ b/tests/common/test_common.py @@ -1,25 +1,28 @@ from datetime import datetime + from stock_indicators import indicators + class TestCommon: def test_find(self, quotes): results = indicators.get_bollinger_bands(quotes) - + r = results.find(datetime(2018, 12, 28)) + assert r is not None assert 252.9625 == round(float(r.sma), 4) assert 230.3495 == round(float(r.lower_band), 4) - + r = results.find(datetime(2018, 12, 31)) + assert r is not None assert 251.8600 == round(float(r.sma), 4) assert 230.0196 == round(float(r.lower_band), 4) - - + def test_remove_warmup_periods(self, quotes): results = indicators.get_adl(quotes) assert 502 == len(results) - + results = results.remove_warmup_periods(200) assert 302 == len(results) - + results = results.remove_warmup_periods(1000) assert 0 == len(results) diff --git a/tests/common/test_cstype_conversion.py b/tests/common/test_cstype_conversion.py index 80e2293b..a6094a21 100644 --- a/tests/common/test_cstype_conversion.py +++ b/tests/common/test_cstype_conversion.py @@ -1,12 +1,13 @@ from datetime import datetime -from numbers import Number from decimal import Decimal as PyDecimal +from numbers import Number 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, to_pydecimal_via_double + class TestCsTypeConversion: def test_datetime_conversion(self): py_datetime = datetime.now() @@ -22,7 +23,9 @@ def test_datetime_conversion(self): # assert py_datetime.microsecond == converted_datetime.microsecond def test_timezone_aware_datetime_conversion(self): - py_datetime = datetime.strptime('2022-06-02 10:29:00-04:00', '%Y-%m-%d %H:%M:%S%z') + py_datetime = datetime.strptime( + "2022-06-02 10:29:00-04:00", "%Y-%m-%d %H:%M:%S%z" + ) converted_datetime = to_pydatetime(CsDateTime(py_datetime)) assert py_datetime.year == converted_datetime.year @@ -38,40 +41,41 @@ def test_timezone_aware_datetime_conversion(self): def test_auto_conversion_from_double_to_float(self): from System import Double as CsDouble - cs_double = CsDouble.Parse('1996.1012', CsCultureInfo.InvariantCulture) + cs_double = CsDouble.Parse("1996.1012", CsCultureInfo.InvariantCulture) assert isinstance(cs_double, Number) assert isinstance(cs_double, float) assert 1996.1012 == cs_double def test_quote_constructor_retains_timezone(self): - from stock_indicators.indicators.common.quote import Quote from decimal import Decimal - - dt = datetime.fromisoformat('2000-03-26T23:00:00+00:00') + + from stock_indicators.indicators.common.quote import Quote + + dt = datetime.fromisoformat("2000-03-26T23:00:00+00:00") q = Quote( date=dt, - open=Decimal('23'), - high=Decimal('26'), - low=Decimal('20'), - close=Decimal('25'), - volume=Decimal('323') + open=Decimal("23"), + high=Decimal("26"), + low=Decimal("20"), + close=Decimal("25"), + volume=Decimal("323"), ) - assert str(q.date.tzinfo) == 'UTC' - assert str(q.date.time()) == '23:00:00' + assert str(q.date.tzinfo) == "UTC" + assert str(q.date.time()) == "23:00:00" def test_decimal_conversion(self): py_decimal = 1996.1012 cs_decimal = CsDecimal(py_decimal) - assert str(py_decimal) == '1996.1012' + assert str(py_decimal) == "1996.1012" assert to_pydecimal(cs_decimal) == PyDecimal(str(py_decimal)) def test_decimal_conversion_expressed_in_exponential_notation(self): py_decimal = 0.000018 cs_decimal = CsDecimal(py_decimal) - assert str(py_decimal) == '1.8e-05' + assert str(py_decimal) == "1.8e-05" assert to_pydecimal(cs_decimal) == PyDecimal(str(py_decimal)) def test_exponential_notation_decimal_conversion(self): diff --git a/tests/common/test_cstype_datetime_kind.py b/tests/common/test_cstype_datetime_kind.py index 4337ef1c..aa9b629b 100644 --- a/tests/common/test_cstype_datetime_kind.py +++ b/tests/common/test_cstype_datetime_kind.py @@ -1,5 +1,4 @@ from datetime import datetime, timezone -import pytest import pytest @@ -9,27 +8,34 @@ def test_to_pydatetime_sets_utc_for_tzaware(): # Start with an offset-aware datetime and ensure roundtrip yields UTC tz - dt = datetime.fromisoformat('2022-06-02T10:29:00-04:00') + dt = datetime.fromisoformat("2022-06-02T10:29:00-04:00") expected_utc = dt.astimezone(timezone.utc) cs_dt = CsDateTime(dt) py_dt = to_pydatetime(cs_dt) # tz-aware should come back as UTC - assert str(py_dt.tzinfo) == 'UTC' + assert str(py_dt.tzinfo) == "UTC" # Time should reflect conversion to UTC assert py_dt.replace(microsecond=0) == expected_utc.replace(microsecond=0) def test_to_pydatetime_keeps_naive_naive(): # Naive input should remain naive and preserve second-level components - dt = datetime.fromisoformat('2023-01-02T03:04:05') # no microseconds + dt = datetime.fromisoformat("2023-01-02T03:04:05") # no microseconds cs_dt = CsDateTime(dt) py_dt = to_pydatetime(cs_dt) assert py_dt.tzinfo is None - assert (py_dt.year, py_dt.month, py_dt.day, py_dt.hour, py_dt.minute, py_dt.second) == ( + assert ( + py_dt.year, + py_dt.month, + py_dt.day, + py_dt.hour, + py_dt.minute, + py_dt.second, + ) == ( dt.year, dt.month, dt.day, @@ -41,7 +47,7 @@ def test_to_pydatetime_keeps_naive_naive(): def test_csdatetime_rejects_non_datetime_input(): with pytest.raises(TypeError): - CsDateTime('2020-01-01') # type: ignore[arg-type] + CsDateTime("2020-01-01") # type: ignore[arg-type] def test_to_pydatetime_handles_system_local_timezone_roundtrip(): @@ -53,5 +59,7 @@ def test_to_pydatetime_handles_system_local_timezone_roundtrip(): py_dt = to_pydatetime(cs_dt) # Interop always returns tz-aware as UTC; ensure the instant is preserved - assert str(py_dt.tzinfo) == 'UTC' - assert py_dt.replace(microsecond=0) == dt_local.astimezone(timezone.utc).replace(microsecond=0) + assert str(py_dt.tzinfo) == "UTC" + assert py_dt.replace(microsecond=0) == dt_local.astimezone(timezone.utc).replace( + microsecond=0 + ) diff --git a/tests/common/test_dateof_equivalence.py b/tests/common/test_dateof_equivalence.py index 289bd4ac..5502343e 100644 --- a/tests/common/test_dateof_equivalence.py +++ b/tests/common/test_dateof_equivalence.py @@ -55,6 +55,6 @@ def test_equivalent_non_date_columns(name: str): assert len(rows) == len(ref_rows) # For each row, all columns except the date column (index 1) should match - for (ref_row, row) in zip(ref_rows, rows): + for ref_row, row in zip(ref_rows, rows): assert ref_row[0] == row[0] # index assert ref_row[2:] == row[2:] # all non-date price/volume columns diff --git a/tests/common/test_dateof_identity_roundtrip.py b/tests/common/test_dateof_identity_roundtrip.py index 7b9f942a..64c5181a 100644 --- a/tests/common/test_dateof_identity_roundtrip.py +++ b/tests/common/test_dateof_identity_roundtrip.py @@ -4,6 +4,7 @@ """ from datetime import timezone + import pytest from stock_indicators import indicators diff --git a/tests/common/test_indicator_results.py b/tests/common/test_indicator_results.py index a5206946..a6be2b73 100644 --- a/tests/common/test_indicator_results.py +++ b/tests/common/test_indicator_results.py @@ -1,61 +1,50 @@ from datetime import datetime -import pytest from stock_indicators import indicators + class TestIndicatorResults: def test_add_results(self, quotes): results = indicators.get_sma(quotes, 20) r4 = results + results + results + results - + assert len(r4) == len(results) * 4 - + for i in range(4): - idx = len(results)*i - assert r4[18+idx].sma is None - assert 214.5250 == round(float(r4[19+idx].sma), 4) - assert 215.0310 == round(float(r4[24+idx].sma), 4) - assert 234.9350 == round(float(r4[149+idx].sma), 4) - assert 255.5500 == round(float(r4[249+idx].sma), 4) - assert 251.8600 == round(float(r4[501+idx].sma), 4) - + idx = len(results) * i + assert r4[18 + idx].sma is None + assert 214.5250 == round(float(r4[19 + idx].sma), 4) + assert 215.0310 == round(float(r4[24 + idx].sma), 4) + assert 234.9350 == round(float(r4[149 + idx].sma), 4) + assert 255.5500 == round(float(r4[249 + idx].sma), 4) + assert 251.8600 == round(float(r4[501 + idx].sma), 4) + def test_mul_results(self, quotes): results = indicators.get_sma(quotes, 20) r4 = results * 4 - + assert len(r4) == len(results) * 4 for i in range(4): - idx = len(results)*i - assert r4[18+idx].sma is None - assert 214.5250 == round(float(r4[19+idx].sma), 4) - assert 215.0310 == round(float(r4[24+idx].sma), 4) - assert 234.9350 == round(float(r4[149+idx].sma), 4) - assert 255.5500 == round(float(r4[249+idx].sma), 4) - assert 251.8600 == round(float(r4[501+idx].sma), 4) - - def test_done_and_reload(self, quotes): - results = indicators.get_sma(quotes, 20) - results.done() - - with pytest.raises(ValueError): - results * 2 + idx = len(results) * i + assert r4[18 + idx].sma is None + assert 214.5250 == round(float(r4[19 + idx].sma), 4) + assert 215.0310 == round(float(r4[24 + idx].sma), 4) + assert 234.9350 == round(float(r4[149 + idx].sma), 4) + assert 255.5500 == round(float(r4[249 + idx].sma), 4) + assert 251.8600 == round(float(r4[501 + idx].sma), 4) - results.reload() - r2 = results * 2 - - assert len(r2) == len(results) * 2 - def test_find(self, quotes): results = indicators.get_sma(quotes, 20) - + # r[19] r = results.find(datetime(2017, 1, 31)) + assert r is not None assert 214.5250 == round(float(r.sma), 4) def test_not_found(self, quotes): results = indicators.get_sma(quotes, 20) - + # returns None r = results.find(datetime(1996, 10, 12)) assert r is None @@ -63,6 +52,6 @@ def test_not_found(self, quotes): def test_remove_with_period(self, quotes): results = indicators.get_sma(quotes, 20) length = len(results) - + results = results.remove_warmup_periods(50) assert len(results) == length - 50 diff --git a/tests/common/test_locale.py b/tests/common/test_locale.py index 1856d7af..4b47fe57 100644 --- a/tests/common/test_locale.py +++ b/tests/common/test_locale.py @@ -1,5 +1,6 @@ -from decimal import Decimal as PyDecimal import locale +from decimal import Decimal as PyDecimal + import pytest from stock_indicators._cslib import CsDecimal @@ -19,9 +20,7 @@ def _uses_comma_decimal_separator() -> bool: import clr # type: ignore # noqa: F401 from System.Globalization import CultureInfo # type: ignore - return ( - CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator == "," - ) + return CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator == "," except Exception: pass @@ -41,7 +40,10 @@ def _uses_comma_decimal_separator() -> bool: @pytest.mark.localization -@pytest.mark.skipif(not uses_comma_decimal, reason="Localization tests require a comma decimal separator culture (e.g., ru-RU)") +@pytest.mark.skipif( + not uses_comma_decimal, + reason="Localization tests require a comma decimal separator culture (e.g., ru-RU)", +) class TestLocale: """ These tests are intended for environments where a comma is used as the decimal separator, diff --git a/tests/common/test_quote.py b/tests/common/test_quote.py index cfd3b818..997ccd47 100644 --- a/tests/common/test_quote.py +++ b/tests/common/test_quote.py @@ -1,63 +1,64 @@ -from stock_indicators.indicators.common.quote import Quote -from decimal import Decimal from datetime import datetime, timezone +from decimal import Decimal + +from stock_indicators.indicators.common.quote import Quote def test_quote_constructor_retains_timezone(): - dt = datetime.fromisoformat('2000-03-26T23:00:00+00:00') + dt = datetime.fromisoformat("2000-03-26T23:00:00+00:00") q = Quote( date=dt, - open=Decimal('23'), - high=Decimal('26'), - low=Decimal('20'), - close=Decimal('25'), - volume=Decimal('323') + open=Decimal("23"), + high=Decimal("26"), + low=Decimal("20"), + close=Decimal("25"), + volume=Decimal("323"), ) - assert str(q.date.tzinfo) == 'UTC' - assert str(q.date.time()) == '23:00:00' + assert str(q.date.tzinfo) == "UTC" + assert str(q.date.time()) == "23:00:00" def test_quote_constructor_handles_various_date_formats(): - dt1 = datetime.fromisoformat('2000-03-26T23:00:00+00:00') - dt2 = datetime.strptime('2000-03-26 23:00:00', '%Y-%m-%d %H:%M:%S') - dt3 = datetime.strptime('2000-03-26', '%Y-%m-%d') + dt1 = datetime.fromisoformat("2000-03-26T23:00:00+00:00") + dt2 = datetime.strptime("2000-03-26 23:00:00", "%Y-%m-%d %H:%M:%S") + dt3 = datetime.strptime("2000-03-26", "%Y-%m-%d") q1 = Quote( date=dt1, - open=Decimal('23'), - high=Decimal('26'), - low=Decimal('20'), - close=Decimal('25'), - volume=Decimal('323') + open=Decimal("23"), + high=Decimal("26"), + low=Decimal("20"), + close=Decimal("25"), + volume=Decimal("323"), ) q2 = Quote( date=dt2, - open=Decimal('23'), - high=Decimal('26'), - low=Decimal('20'), - close=Decimal('25'), - volume=Decimal('323') + open=Decimal("23"), + high=Decimal("26"), + low=Decimal("20"), + close=Decimal("25"), + volume=Decimal("323"), ) q3 = Quote( date=dt3, - open=Decimal('23'), - high=Decimal('26'), - low=Decimal('20'), - close=Decimal('25'), - volume=Decimal('323') + open=Decimal("23"), + high=Decimal("26"), + low=Decimal("20"), + close=Decimal("25"), + volume=Decimal("323"), ) - assert str(q1.date.tzinfo) == 'UTC' - assert str(q1.date.time()) == '23:00:00' + assert str(q1.date.tzinfo) == "UTC" + assert str(q1.date.time()) == "23:00:00" - assert str(q2.date.tzinfo) == 'None' - assert str(q2.date.time()) == '23:00:00' + assert str(q2.date.tzinfo) == "None" + assert str(q2.date.time()) == "23:00:00" - assert str(q3.date.tzinfo) == 'None' - assert str(q3.date.time()) == '00:00:00' + assert str(q3.date.tzinfo) == "None" + assert str(q3.date.time()) == "00:00:00" def test_quote_constructor_handles_system_local_timezone(): @@ -67,14 +68,16 @@ def test_quote_constructor_handles_system_local_timezone(): q = Quote( date=dt, - open=Decimal('23'), - high=Decimal('26'), - low=Decimal('20'), - close=Decimal('25'), - volume=Decimal('323'), + open=Decimal("23"), + high=Decimal("26"), + low=Decimal("20"), + close=Decimal("25"), + volume=Decimal("323"), ) # tz-aware should normalize to UTC but represent the same instant - d = getattr(q, 'date') - assert str(d.tzinfo) == 'UTC' - assert d.replace(microsecond=0) == dt.astimezone(timezone.utc).replace(microsecond=0) + d = q.date + assert str(d.tzinfo) == "UTC" + assert d.replace(microsecond=0) == dt.astimezone(timezone.utc).replace( + microsecond=0 + ) diff --git a/tests/common/test_sma_roundtrip_dates.py b/tests/common/test_sma_roundtrip_dates.py index a0f2d1d8..dbd3514c 100644 --- a/tests/common/test_sma_roundtrip_dates.py +++ b/tests/common/test_sma_roundtrip_dates.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from decimal import Decimal import pytest @@ -39,7 +39,9 @@ def _clone_with_date(quotes, transform): def _mk_utc(d: datetime) -> datetime: # Force tz-aware UTC at same Y-M-D h:m:s - return datetime(d.year, d.month, d.day, d.hour, d.minute, d.second, tzinfo=timezone.utc) + return datetime( + d.year, d.month, d.day, d.hour, d.minute, d.second, tzinfo=timezone.utc + ) def _mk_offset(d: datetime) -> datetime: @@ -59,7 +61,7 @@ def _mk_date_only(d: datetime) -> datetime: @pytest.mark.parametrize( - "variant, maker", + ("variant", "maker"), [ ("utc", _mk_utc), ("offset", _mk_offset), diff --git a/tests/common/test_type_compatibility.py b/tests/common/test_type_compatibility.py index 18da8b19..8f88eb58 100644 --- a/tests/common/test_type_compatibility.py +++ b/tests/common/test_type_compatibility.py @@ -1,15 +1,16 @@ - from enum import Enum, IntEnum -from stock_indicators._cslib import CsQuote, CsCandleProperties + +from stock_indicators._cslib import CsCandleProperties, CsQuote from stock_indicators.indicators.common.candles import CandleProperties from stock_indicators.indicators.common.enums import PivotPointType from stock_indicators.indicators.common.quote import Quote + class TestTypeCompat: def test_quote_based_class(self): # Quote assert issubclass(Quote, CsQuote) - + # CandleProperties assert issubclass(CandleProperties, Quote) assert issubclass(CandleProperties, CsQuote) diff --git a/tests/conftest.py b/tests/conftest.py index c8fbc33d..b7a09bcb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,6 @@ import pytest -from stock_indicators._cslib import clr from stock_indicators.indicators.common import Quote # pre-initialized from stock_indicators.logging_config import configure_logging @@ -38,7 +37,7 @@ def setup_logging(): @pytest.fixture(autouse=True, scope="session") def setup_clr_culture(): """Configure CLR culture settings for all tests.""" - import clr + import clr # noqa: F401 from System.Globalization import CultureInfo from System.Threading import Thread @@ -63,7 +62,8 @@ def get_data_from_csv(filename): quotes_dir = base_dir / "quotes" if not base_dir.exists() or not quotes_dir.exists(): raise FileNotFoundError( - "Test data not found. Please see README.md for test data setup instructions." + "Test data not found. Please see README.md " + "for test data setup instructions." ) data_path = quotes_dir / f"{filename}.csv" @@ -72,7 +72,7 @@ def get_data_from_csv(filename): if not data_path.exists(): raise FileNotFoundError(f"Test data file not found: {filename}") - with open(data_path, "r", newline="", encoding="utf-8") as csvfile: + with open(data_path, newline="", encoding="utf-8") as csvfile: reader = csv.reader(csvfile) data = list(reader) return data[1:] # skips the first row, those are headers @@ -89,11 +89,14 @@ def parse_decimal(value): def parse_date(date_str): """Parse date value across many common formats. + Supported families: - Date-only: YYYY-MM-DD, YYYYMMDD, DD-MM-YYYY, MM/DD/YYYY - - Naive date+time: YYYY-MM-DDTHH:MM, YYYY-MM-DDTHH:MM:SS, YYYY-MM-DDTHH:MM:SS.sss[sss], YYYYMMDDTHHMMSS - - With offset: ISO-8601 extended with offset (e.g., +00:00, -04:00, +05:30, with optional fractions); - ISO basic with offset without colon: YYYYMMDDTHHMMSS+0000 + - Naive date+time: YYYY-MM-DDTHH:MM, YYYY-MM-DDTHH:MM:SS, + YYYY-MM-DDTHH:MM:SS.sss[sss], YYYYMMDDTHHMMSS + - With offset: ISO-8601 extended with offset (e.g., +00:00, -04:00, +05:30, + with optional fractions); ISO basic with offset without colon: + YYYYMMDDTHHMMSS+0000 - Zulu: ...Z with optional fractional seconds - RFC1123/HTTP-date: Fri, 22 Aug 2025 17:45:30 GMT - IANA zone name appended after a space: YYYY-MM-DDTHH:MM:SS America/New_York @@ -123,7 +126,8 @@ def parse_date(date_str): try: return dt.replace(tzinfo=ZoneInfo(zone)) except Exception: - # Fallback if IANA zone isn't available on the system: treat as naive + # Fallback if IANA zone isn't available on the system: + # treat as naive return dt # ZoneInfo not available; treat as naive return dt @@ -144,7 +148,8 @@ def parse_date(date_str): # ISO extended with Zulu or offset, including fractional seconds if "T" in s and (s.endswith("Z") or re.search(r"[+-]\d{2}:?\d{2}$", s)): s_norm = s.replace("Z", "+00:00") - # If offset without colon at end (e.g., +0000), insert colon for fromisoformat + # If offset without colon at end (e.g., +0000), insert colon for + # fromisoformat m_off = re.search(r"([+-])(\d{2})(\d{2})$", s_norm) if m_off and ":" not in s_norm[-6:]: s_norm = ( @@ -182,7 +187,8 @@ def parse_date(date_str): # As a final attempt, try fromisoformat on whatever remains return datetime.fromisoformat(s) except Exception: - # Last-resort fallback to keep tests running; individual tests will assert equality + # Last-resort fallback to keep tests running; individual tests will + # assert equality return datetime.now() diff --git a/tests/test_adl.py b/tests/test_adl.py index 783d4551..cbc7d153 100644 --- a/tests/test_adl.py +++ b/tests/test_adl.py @@ -1,6 +1,8 @@ import pytest + from stock_indicators import indicators + class TestADL: def test_standard(self, quotes): results = indicators.get_adl(quotes) @@ -11,15 +13,15 @@ def test_standard(self, quotes): assert 502 == len(list(filter(lambda x: x.adl_sma is None, results))) r1 = results[249] - assert 0.7778 == round(float(r1.money_flow_multiplier), 4) - assert 36433792.89 == round(float(r1.money_flow_volume), 2) - assert 3266400865.74 == round(float(r1.adl), 2) + assert 0.7778 == round(float(r1.money_flow_multiplier), 4) + assert 36433792.89 == round(float(r1.money_flow_volume), 2) + assert 3266400865.74 == round(float(r1.adl), 2) assert r1.adl_sma is None r2 = results[501] - assert 0.8052 == round(float(r2.money_flow_multiplier), 4) - assert 118396116.25 == round(float(r2.money_flow_volume), 2) - assert 3439986548.42 == round(float(r2.adl), 2) + assert 0.8052 == round(float(r2.money_flow_multiplier), 4) + assert 118396116.25 == round(float(r2.money_flow_volume), 2) + assert 3439986548.42 == round(float(r2.adl), 2) assert r2.adl_sma is None # def test_convert_to_quotes(self, quotes): @@ -47,10 +49,10 @@ def test_with_sma(self, quotes): assert 483 == len(list(filter(lambda x: x.adl_sma is not None, results))) r = results[501] - assert 0.8052 == round(float(r.money_flow_multiplier), 4) - assert 118396116.25 == round(float(r.money_flow_volume), 2) - assert 3439986548.42 == round(float(r.adl), 2) - assert 3595352721.16 == round(float(r.adl_sma), 2) + assert 0.8052 == round(float(r.money_flow_multiplier), 4) + assert 118396116.25 == round(float(r.money_flow_volume), 2) + assert 3439986548.42 == round(float(r.adl), 2) + assert 3595352721.16 == round(float(r.adl_sma), 2) def test_condense(self, quotes): results = indicators.get_adl(quotes).condense() @@ -58,12 +60,13 @@ def test_condense(self, quotes): assert 502 == len(results) r = results[-1] - assert 0.8052 == round(float(r.money_flow_multiplier), 4) - assert 118396116.25 == round(float(r.money_flow_volume), 2) - assert 3439986548.42 == round(float(r.adl), 2) + assert 0.8052 == round(float(r.money_flow_multiplier), 4) + assert 118396116.25 == round(float(r.money_flow_volume), 2) + assert 3439986548.42 == round(float(r.adl), 2) assert r.adl_sma is None def test_exceptions(self, quotes): from System import ArgumentOutOfRangeException + with pytest.raises(ArgumentOutOfRangeException): indicators.get_adl(quotes, 0) diff --git a/tests/test_adx.py b/tests/test_adx.py index e5a24b68..040e1d3a 100644 --- a/tests/test_adx.py +++ b/tests/test_adx.py @@ -10,36 +10,63 @@ def test_standard(self, quotes): # proper quantities # should always be the same number of results as there is quotes assert 502 == len(results) + assert 488 == len(list(filter(lambda x: x.dx is not None, results))) assert 475 == len(list(filter(lambda x: x.adx is not None, results))) - assert 462 == len(list(filter(lambda x: x.adxr is not None, results))) + assert 461 == len(list(filter(lambda x: x.adxr is not None, results))) # sample values + r = results[13] + assert r.pdi is None + assert r.mdi is None + assert r.dx is None + assert r.adx is None + + r = results[14] + assert 21.9669 == round(float(r.pdi), 4) + assert 18.5462 == round(float(r.mdi), 4) + assert 8.4433 == round(float(r.dx), 4) + assert r.adx is None + r = results[19] assert 21.0361 == round(float(r.pdi), 4) assert 25.0124 == round(float(r.mdi), 4) + assert 8.6351 == round(float(r.dx), 4) assert r.adx is None + r = results[26] + assert r.adx is None + + r = results[27] + assert 15.9459 == round(float(r.adx), 4) + r = results[29] assert 37.9719 == round(float(r.pdi), 4) assert 14.1658 == round(float(r.mdi), 4) + assert 45.6600 == round(float(r.dx), 4) assert 19.7949 == round(float(r.adx), 4) r = results[39] assert r.adxr is None r = results[40] - assert 29.1062 == round(float(r.adxr), 4) + assert r.adxr is None + + r = results[41] + assert r.adxr is not None r = results[248] assert 32.3167 == round(float(r.pdi), 4) assert 18.2471 == round(float(r.mdi), 4) + assert 27.8255 == round(float(r.dx), 4) assert 30.5903 == round(float(r.adx), 4) - assert 29.1252 == round(float(r.adxr), 4) + assert r.adxr is not None r = results[501] assert 17.7565 == round(float(r.pdi), 4) assert 31.1510 == round(float(r.mdi), 4) + assert 27.3873 == round(float(r.dx), 4) assert 34.2987 == round(float(r.adx), 4) + assert r.adxr is not None def test_bad_data(self, quotes_bad): results = indicators.get_adx(quotes_bad, 20) diff --git a/tests/test_basic_quote.py b/tests/test_basic_quote.py index 5c4fde01..832ea008 100644 --- a/tests/test_basic_quote.py +++ b/tests/test_basic_quote.py @@ -4,11 +4,12 @@ from stock_indicators.indicators.common.enums import CandlePart from stock_indicators.indicators.common.quote import Quote + class TestBasicQuote: def test_standard(self, quotes): o = indicators.get_basic_quote(quotes, CandlePart.OPEN) h = indicators.get_basic_quote(quotes, CandlePart.HIGH) - l = indicators.get_basic_quote(quotes, CandlePart.LOW) + low = indicators.get_basic_quote(quotes, CandlePart.LOW) c = indicators.get_basic_quote(quotes, CandlePart.CLOSE) v = indicators.get_basic_quote(quotes, CandlePart.VOLUME) hl = indicators.get_basic_quote(quotes, CandlePart.HL2) @@ -16,24 +17,24 @@ def test_standard(self, quotes): oc = indicators.get_basic_quote(quotes, CandlePart.OC2) ohl = indicators.get_basic_quote(quotes, CandlePart.OHL3) ohlc = indicators.get_basic_quote(quotes, CandlePart.OHLC4) - + assert 502 == len(c) - + assert datetime(2018, 12, 31) == c[-1].date - - assert 244.92 == o[-1].value - assert 245.54 == h[-1].value - assert 242.87 == l[-1].value - assert 245.28 == c[-1].value - assert 147031456 == v[-1].value - assert 244.205 == hl[-1].value - assert 244.5633 == round(float(hlc[-1].value), 4) - assert 245.1 == oc[-1].value - assert 244.4433 == round(float(ohl[-1].value), 4) - assert 244.6525 == ohlc[-1].value - + + assert 244.92 == o[-1].value + assert 245.54 == h[-1].value + assert 242.87 == low[-1].value + assert 245.28 == c[-1].value + assert 147031456 == v[-1].value + assert 244.205 == hl[-1].value + assert 244.5633 == round(float(hlc[-1].value), 4) + assert 245.1 == oc[-1].value + assert 244.4433 == round(float(ohl[-1].value), 4) + assert 244.6525 == ohlc[-1].value + def test_use(self, quotes): results = Quote.use(quotes, CandlePart.CLOSE) results = list(results) - + assert 502 == len(results) diff --git a/tests/test_connors_rsi.py b/tests/test_connors_rsi.py index bcb759d7..d7fcb8c1 100644 --- a/tests/test_connors_rsi.py +++ b/tests/test_connors_rsi.py @@ -41,8 +41,6 @@ def test_removed(self, quotes): streak_periods = 2 rank_periods = 100 - removed_periods = max(rsi_periods, max(streak_periods, rank_periods)) + 2 - results = indicators.get_connors_rsi( quotes, rsi_periods, streak_periods, rank_periods ) diff --git a/tests/test_gator.py b/tests/test_gator.py index 4ba3a309..c039af33 100644 --- a/tests/test_gator.py +++ b/tests/test_gator.py @@ -31,47 +31,46 @@ def test_standard(self, quotes): assert r.upper is None assert -0.0406 == round(float(r.lower), 4) assert r.is_upper_expanding is None - assert r.is_lower_expanding == False + assert not r.is_lower_expanding r = results[19] assert r.upper is None assert -1.0018 == round(float(r.lower), 4) assert r.is_upper_expanding is None - assert r.is_lower_expanding == True + assert r.is_lower_expanding r = results[20] assert 0.4004 == round(float(r.upper), 4) assert -1.0130 == round(float(r.lower), 4) assert r.is_upper_expanding is None - assert r.is_lower_expanding == True + assert r.is_lower_expanding r = results[21] assert 0.7298 == round(float(r.upper), 4) assert -0.6072 == round(float(r.lower), 4) - assert r.is_upper_expanding == True - assert r.is_lower_expanding == False + assert r.is_upper_expanding + assert not r.is_lower_expanding r = results[99] assert 0.5159 == round(float(r.upper), 4) assert -0.2320 == round(float(r.lower), 4) - assert r.is_upper_expanding == False - assert r.is_lower_expanding == True + assert not r.is_upper_expanding + assert r.is_lower_expanding r = results[249] assert 3.1317 == round(float(r.upper), 4) assert -1.8058 == round(float(r.lower), 4) - assert r.is_upper_expanding == True - assert r.is_lower_expanding == False + assert r.is_upper_expanding + assert not r.is_lower_expanding r = results[501] assert 7.4538 == round(float(r.upper), 4) assert -9.2399 == round(float(r.lower), 4) - assert r.is_upper_expanding == True - assert r.is_lower_expanding == True + assert r.is_upper_expanding + assert r.is_lower_expanding def test_gator_with_alligator(self, quotes): alligator_results = indicators.get_alligator(quotes) - alligator_results.done() results = indicators.get_gator(alligator_results) assert 502 == len(results) @@ -95,8 +94,8 @@ def test_removed(self, quotes): last = results.pop() assert 7.4538 == round(float(last.upper), 4) assert -9.2399 == round(float(last.lower), 4) - assert last.is_upper_expanding == True - assert last.is_lower_expanding == True + assert last.is_upper_expanding + assert last.is_lower_expanding def test_condense(self, quotes): results = indicators.get_gator(quotes).condense() @@ -106,5 +105,5 @@ def test_condense(self, quotes): last = results.pop() assert 7.4538 == round(float(last.upper), 4) assert -9.2399 == round(float(last.lower), 4) - assert last.is_upper_expanding == True - assert last.is_lower_expanding == True + assert last.is_upper_expanding + assert last.is_lower_expanding diff --git a/tests/test_hurst.py b/tests/test_hurst.py index 8b653f4f..47f601de 100644 --- a/tests/test_hurst.py +++ b/tests/test_hurst.py @@ -14,7 +14,9 @@ def test_standard_long(self, quotes_longest): assert 0.483563 == round(float(r.hurst_exponent), 6) # def test_to_quotes(self, quotes_longest): - # new_quotes = indicators.get_hurst(quotes_longest, len(quotes_longest) - 1).to_quotes() + # new_quotes = indicators.get_hurst( + # quotes_longest, len(quotes_longest) - 1 + # ).to_quotes() # assert 1 == len(new_quotes) diff --git a/tests/test_parabolic_sar.py b/tests/test_parabolic_sar.py index fb248f19..5af9acb2 100644 --- a/tests/test_parabolic_sar.py +++ b/tests/test_parabolic_sar.py @@ -12,19 +12,19 @@ def test_standard(self, quotes): r = results[14] assert 212.8300 == round(float(r.sar), 4) - assert True == r.is_reversal + assert r.is_reversal r = results[16] assert 212.9924 == round(float(r.sar), 4) - assert False == r.is_reversal + assert not r.is_reversal r = results[94] assert 228.3600 == round(float(r.sar), 4) - assert False == r.is_reversal + assert not r.is_reversal r = results[501] assert 229.7662 == round(float(r.sar), 4) - assert False == r.is_reversal + assert not r.is_reversal def test_extended(self, quotes): accleration_step = 0.02 @@ -40,23 +40,23 @@ def test_extended(self, quotes): r = results[14] assert 212.8300 == round(float(r.sar), 4) - assert True == r.is_reversal + assert r.is_reversal r = results[16] assert 212.9518 == round(float(r.sar), 4) - assert False == r.is_reversal + assert not r.is_reversal r = results[94] assert 228.3600 == round(float(r.sar), 4) - assert False == r.is_reversal + assert not r.is_reversal r = results[486] assert 273.4148 == round(float(r.sar), 4) - assert False == r.is_reversal + assert not r.is_reversal r = results[501] assert 246.73 == round(float(r.sar), 4) - assert False == r.is_reversal + assert not r.is_reversal def test_bad_data(self, quotes_bad): r = indicators.get_parabolic_sar(quotes_bad) @@ -71,7 +71,7 @@ def test_removed(self, quotes): last = results.pop() assert 229.7662 == round(float(last.sar), 4) - assert False == last.is_reversal + assert not last.is_reversal def test_condense(self, quotes): results = indicators.get_parabolic_sar(quotes, 0.02, 0.2).condense() @@ -80,7 +80,7 @@ def test_condense(self, quotes): last = results.pop() assert 229.7662 == round(float(last.sar), 4) - assert False == last.is_reversal + assert not last.is_reversal def test_exceptions(self, quotes): from System import ArgumentOutOfRangeException diff --git a/tests/test_renko.py b/tests/test_renko.py index 72c20f81..56e389ba 100644 --- a/tests/test_renko.py +++ b/tests/test_renko.py @@ -18,7 +18,7 @@ def test_standard_close(self, quotes): assert 212.53 == float(round(r.low, 2)) assert 215.5 == float(round(r.close, 1)) assert 1180981564 == float(round(r.volume, 0)) - assert r.is_up == True + assert r.is_up r = results[5] assert 225.5 == float(round(r.open, 1)) @@ -26,7 +26,7 @@ def test_standard_close(self, quotes): assert 219.77 == float(round(r.low, 2)) assert 228 == float(round(r.close, 0)) assert 4192959240 == float(round(r.volume, 0)) - assert r.is_up == True + assert r.is_up r = results.pop() assert 240.5 == float(round(r.open, 1)) @@ -34,7 +34,7 @@ def test_standard_close(self, quotes): assert 234.52 == float(round(r.low, 2)) assert 243 == float(round(r.close, 0)) assert 189794032 == float(round(r.volume, 0)) - assert r.is_up == True + assert r.is_up def test_standard_high_low(self, quotes): results = indicators.get_renko(quotes, 2.5, EndType.HIGH_LOW) @@ -47,7 +47,7 @@ def test_standard_high_low(self, quotes): assert 212.53 == float(round(r.low, 2)) assert 215.5 == float(round(r.close, 1)) assert 1180981564 == float(round(r.volume, 0)) - assert r.is_up == True + assert r.is_up r = results[25] assert 270.5 == float(round(r.open, 1)) @@ -55,7 +55,7 @@ def test_standard_high_low(self, quotes): assert 271.96 == float(round(r.low, 2)) assert 273 == float(round(r.close, 0)) assert 100801672 == float(round(r.volume, 0)) - assert r.is_up == True + assert r.is_up r = results.pop() assert 243 == float(round(r.open, 0)) @@ -63,7 +63,7 @@ def test_standard_high_low(self, quotes): assert 241.87 == float(round(r.low, 2)) assert 245.5 == float(round(r.close, 1)) assert 51999637 == float(round(r.volume, 0)) - assert r.is_up == True + assert r.is_up def test_renko_atr(self, quotes): results = indicators.get_renko_atr(quotes, 14, EndType.CLOSE) @@ -76,7 +76,7 @@ def test_renko_atr(self, quotes): assert 212.53 == float(round(r.low, 2)) assert 218.9497 == float(round(r.close, 4)) assert 2090292272 == float(round(r.volume, 0)) - assert r.is_up == True + assert r.is_up r = results.pop() assert 237.3990 == float(round(r.open, 4)) @@ -84,7 +84,7 @@ def test_renko_atr(self, quotes): assert 229.42 == float(round(r.low, 2)) assert 243.5487 == float(round(r.close, 4)) assert 715446448 == float(round(r.volume, 0)) - assert r.is_up == True + assert r.is_up def test_bad_data(self, quotes_bad): r = indicators.get_renko(quotes_bad, 100) diff --git a/tests/test_sma_analysis.py b/tests/test_sma_analysis.py index b7a49a4f..a5a0c5f3 100644 --- a/tests/test_sma_analysis.py +++ b/tests/test_sma_analysis.py @@ -1,7 +1,6 @@ import pytest from stock_indicators import indicators -from stock_indicators._cslib import CsDecimal class TestSMAExtended: @@ -10,10 +9,10 @@ def test_result_types(self, quotes): # Sample value. r = results[501] - assert float == type(r._csdata.Sma) - assert float == type(r._csdata.Mad) - assert float == type(r._csdata.Mse) - assert float == type(r._csdata.Mape) + assert isinstance(r._csdata.Sma, float) + assert isinstance(r._csdata.Mad, float) + assert isinstance(r._csdata.Mse, float) + assert isinstance(r._csdata.Mape, float) def test_extended(self, quotes): results = indicators.get_sma_analysis(quotes, 20) diff --git a/tests/test_starc_bands.py b/tests/test_starc_bands.py index e4f3bf56..981db6d7 100644 --- a/tests/test_starc_bands.py +++ b/tests/test_starc_bands.py @@ -8,7 +8,6 @@ def test_standard(self, quotes): sma_periods = 20 multiplier = 2 atr_periods = 14 - lookback_periods = max(sma_periods, atr_periods) results = indicators.get_starc_bands( quotes, sma_periods, multiplier, atr_periods diff --git a/tests/test_stdev.py b/tests/test_stdev.py index 4e622236..87836176 100644 --- a/tests/test_stdev.py +++ b/tests/test_stdev.py @@ -1,6 +1,8 @@ import pytest + from stock_indicators import indicators + class TestStdev: def test_standard(self, quotes): results = indicators.get_stdev(quotes, 10) @@ -8,7 +10,7 @@ def test_standard(self, quotes): assert 502 == len(results) assert 493 == len(list(filter(lambda x: x.stdev is not None, results))) assert 493 == len(list(filter(lambda x: x.z_score is not None, results))) - assert 0 == len(list(filter(lambda x: x.stdev_sma is not None, results))) + assert 0 == len(list(filter(lambda x: x.stdev_sma is not None, results))) r = results[8] assert r.stdev is None @@ -17,19 +19,19 @@ def test_standard(self, quotes): assert r.stdev_sma is None r = results[9] - assert 0.5020 == round(float(r.stdev), 4) - assert 214.0140 == round(float(r.mean), 4) + assert 0.5020 == round(float(r.stdev), 4) + assert 214.0140 == round(float(r.mean), 4) assert -0.525917 == round(float(r.z_score), 6) assert r.stdev_sma is None r = results[249] - assert 0.9827 == round(float(r.stdev), 4) + assert 0.9827 == round(float(r.stdev), 4) assert 257.2200 == round(float(r.mean), 4) assert 0.783563 == round(float(r.z_score), 6) assert r.stdev_sma is None r = results[501] - assert 5.4738 == round(float(r.stdev), 4) + assert 5.4738 == round(float(r.stdev), 4) assert 242.4100 == round(float(r.mean), 4) assert 0.524312 == round(float(r.z_score), 6) assert r.stdev_sma is None @@ -43,14 +45,14 @@ def test_stdev_with_sma(self, quotes): assert 489 == len(list(filter(lambda x: x.stdev_sma is not None, results))) r = results[19] - assert 1.1642 == round(float(r.stdev), 4) + assert 1.1642 == round(float(r.stdev), 4) assert -0.065282 == round(float(r.z_score), 6) - assert 1.1422 == round(float(r.stdev_sma), 4) + assert 1.1422 == round(float(r.stdev_sma), 4) r = results[501] - assert 5.4738 == round(float(r.stdev), 4) + assert 5.4738 == round(float(r.stdev), 4) assert 0.524312 == round(float(r.z_score), 6) - assert 7.6886 == round(float(r.stdev_sma), 4) + assert 7.6886 == round(float(r.stdev_sma), 4) def test_bad_data(self, quotes_bad): r = indicators.get_stdev(quotes_bad, 15, 3) @@ -73,7 +75,7 @@ def test_removed(self, quotes): assert 502 - 9 == len(results) last = results.pop() - assert 5.4738 == round(float(last.stdev), 4) + assert 5.4738 == round(float(last.stdev), 4) assert 242.4100 == round(float(last.mean), 4) assert 0.524312 == round(float(last.z_score), 6) assert last.stdev_sma is None @@ -84,15 +86,16 @@ def test_condense(self, quotes): assert 493 == len(results) last = results.pop() - assert 5.4738 == round(float(last.stdev), 4) + assert 5.4738 == round(float(last.stdev), 4) assert 242.4100 == round(float(last.mean), 4) assert 0.524312 == round(float(last.z_score), 6) assert last.stdev_sma is None def test_exceptions(self, quotes): from System import ArgumentOutOfRangeException + with pytest.raises(ArgumentOutOfRangeException): indicators.get_stdev(quotes, 1) with pytest.raises(ArgumentOutOfRangeException): - indicators.get_stdev(quotes, 14,0) + indicators.get_stdev(quotes, 14, 0) diff --git a/tests/test_stoch.py b/tests/test_stoch.py index e53cff51..c665997b 100644 --- a/tests/test_stoch.py +++ b/tests/test_stoch.py @@ -125,9 +125,6 @@ def test_boundary(self, quotes): assert 0 == len( list(filter(lambda x: x.d is not None and (x.d < 0 or x.d > 100), results)) ) - assert 0 == len( - list(filter(lambda x: x.j is not None and (x.d < 0 or x.d > 100), results)) - ) def test_removed(self, quotes): results = indicators.get_stoch(quotes, 14, 3, 3).remove_warmup_periods() diff --git a/tests/test_trix.py b/tests/test_trix.py index 12392e80..1ee5a0dd 100644 --- a/tests/test_trix.py +++ b/tests/test_trix.py @@ -1,6 +1,8 @@ import pytest + from stock_indicators import indicators + class TestTRIX: def test_standard(self, quotes): results = indicators.get_trix(quotes, 20, 5) @@ -21,7 +23,7 @@ def test_standard(self, quotes): assert 0.119769 == round(float(r.signal), 6) r = results[501] - assert 263.3216 == round(float(r.ema3), 4) + assert 263.3216 == round(float(r.ema3), 4) assert -0.230742 == round(float(r.trix), 6) assert -0.204536 == round(float(r.signal), 6) @@ -35,7 +37,7 @@ def test_removed(self, quotes): assert 502 - ((3 * 20) + 100) == len(results) last = results.pop() - assert 263.3216 == round(float(last.ema3), 4) + assert 263.3216 == round(float(last.ema3), 4) assert -0.230742 == round(float(last.trix), 6) assert -0.204536 == round(float(last.signal), 6) @@ -45,11 +47,12 @@ def test_condense(self, quotes): assert 482 == len(results) last = results.pop() - assert 263.3216 == round(float(last.ema3), 4) + assert 263.3216 == round(float(last.ema3), 4) assert -0.230742 == round(float(last.trix), 6) assert -0.204536 == round(float(last.signal), 6) def test_exceptions(self, quotes): from System import ArgumentOutOfRangeException + with pytest.raises(ArgumentOutOfRangeException): indicators.get_trix(quotes, 0) diff --git a/tests/test_tsi.py b/tests/test_tsi.py index 1012e80e..e43ad45d 100644 --- a/tests/test_tsi.py +++ b/tests/test_tsi.py @@ -1,6 +1,8 @@ import pytest + from stock_indicators import indicators + class TestTSI: def test_standard(self, quotes): results = indicators.get_tsi(quotes, 25, 13, 7) @@ -68,6 +70,7 @@ def test_condense(self, quotes): def test_exceptions(self, quotes): from System import ArgumentOutOfRangeException + with pytest.raises(ArgumentOutOfRangeException): indicators.get_tsi(quotes, 0) diff --git a/tests/test_ulcer_index.py b/tests/test_ulcer_index.py index 7747e629..770134ac 100644 --- a/tests/test_ulcer_index.py +++ b/tests/test_ulcer_index.py @@ -1,6 +1,8 @@ import pytest + from stock_indicators import indicators + class TestUlcerIndex: def test_standard(self, quotes): results = indicators.get_ulcer_index(quotes, 14) @@ -40,5 +42,6 @@ def test_condense(self, quotes): def test_exceptions(self, quotes): from System import ArgumentOutOfRangeException + with pytest.raises(ArgumentOutOfRangeException): indicators.get_ulcer_index(quotes, 0) diff --git a/tests/test_vwap.py b/tests/test_vwap.py index 32eeb694..2de05ac0 100644 --- a/tests/test_vwap.py +++ b/tests/test_vwap.py @@ -6,7 +6,6 @@ class TestVWAP: - def test_standard(self, quotes_intraday): quotes_intraday.sort(key=lambda x: x.date) results = indicators.get_vwap(quotes_intraday[:391]) diff --git a/tests/utiltest.py b/tests/utiltest.py index cab72173..41f088c6 100644 --- a/tests/utiltest.py +++ b/tests/utiltest.py @@ -1,20 +1,25 @@ -import os import json +import os from datetime import datetime from stock_indicators.indicators.common.quote import Quote + def load_quotes_from_json(json_path): base_dir = os.path.dirname(__file__) data_path = os.path.join(base_dir, json_path) quotes = [] - with open(data_path, "r", encoding="utf-8") as st_json: + with open(data_path, encoding="utf-8") as st_json: for j in json.load(st_json): - quotes.append(Quote(datetime.fromisoformat(j["Date"]), + quotes.append( + Quote( + datetime.fromisoformat(j["Date"]), j["Open"], j["High"], j["Low"], j["Close"], - j["Volume"])) + j["Volume"], + ) + ) return quotes diff --git a/typings/Skender/Stock/Indicators/__init__.pyi b/typings/Skender/Stock/Indicators/__init__.pyi new file mode 100644 index 00000000..5a21d12b --- /dev/null +++ b/typings/Skender/Stock/Indicators/__init__.pyi @@ -0,0 +1,17 @@ +from typing import Any + +BetaType: Any +CandlePart: Any +CandleProperties: Any +ChandelierType: Any +EndType: Any +Indicator: Any +Match: Any +MaType: Any +PeriodSize: Any +PivotPointType: Any +PivotTrend: Any +Quote: Any +QuoteUtility: Any +ResultBase: Any +ResultUtility: Any diff --git a/typings/System/Collections/Generic/__init__.pyi b/typings/System/Collections/Generic/__init__.pyi new file mode 100644 index 00000000..896f37b9 --- /dev/null +++ b/typings/System/Collections/Generic/__init__.pyi @@ -0,0 +1,4 @@ +from typing import Any + +IEnumerable: Any +List: Any diff --git a/typings/System/Globalization/__init__.pyi b/typings/System/Globalization/__init__.pyi new file mode 100644 index 00000000..03519532 --- /dev/null +++ b/typings/System/Globalization/__init__.pyi @@ -0,0 +1,4 @@ +from typing import Any + +CultureInfo: Any +NumberStyles: Any diff --git a/typings/System/Threading/__init__.pyi b/typings/System/Threading/__init__.pyi new file mode 100644 index 00000000..66e340af --- /dev/null +++ b/typings/System/Threading/__init__.pyi @@ -0,0 +1,3 @@ +from typing import Any + +Thread: Any diff --git a/typings/System/__init__.pyi b/typings/System/__init__.pyi new file mode 100644 index 00000000..baacb27f --- /dev/null +++ b/typings/System/__init__.pyi @@ -0,0 +1,8 @@ +from typing import Any + +ArgumentOutOfRangeException: Any +DateTime: Any +Decimal: Any +Enum: Any +IO: Any +AppDomain: Any diff --git a/typings/pythonnet/__init__.pyi b/typings/pythonnet/__init__.pyi new file mode 100644 index 00000000..4b822079 --- /dev/null +++ b/typings/pythonnet/__init__.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def load(runtime: str | None = ...) -> Any: ... diff --git a/typings/stock_indicators/_cslib/__init__.pyi b/typings/stock_indicators/_cslib/__init__.pyi new file mode 100644 index 00000000..116d292c --- /dev/null +++ b/typings/stock_indicators/_cslib/__init__.pyi @@ -0,0 +1,25 @@ +from typing import Any + +clr: Any +CsIndicator: Any +CsResultUtility: Any +CsQuote: Any +CsQuoteUtility: Any +CsResultBase: Any +CsBetaType: Any +CsCandlePart: Any +CsCandleProperties: Any +CsChandelierType: Any +CsEndType: Any +CsMatch: Any +CsMaType: Any +CsPeriodSize: Any +CsPivotPointType: Any +CsPivotTrend: Any +CsDateTime: Any +CsDecimal: Any +CsEnum: Any +CsIEnumerable: Any +CsList: Any +CsCultureInfo: Any +CsNumberStyles: Any From f96030ee0d7eae429d3414b0aaa15a8bc4abf405 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:59:05 +0000 Subject: [PATCH 07/10] Fix linting issues after merge Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com> --- .../test_decimal_conversion_performance.py | 59 ++++++++------ research_decimal_conversion.py | 68 +++++++++------- tests/common/test_cstype_conversion.py | 8 +- .../test_decimal_conversion_comparison.py | 79 ++++++++++--------- 4 files changed, 124 insertions(+), 90 deletions(-) diff --git a/benchmarks/test_decimal_conversion_performance.py b/benchmarks/test_decimal_conversion_performance.py index a4d3f28b..8a2aad6f 100644 --- a/benchmarks/test_decimal_conversion_performance.py +++ b/benchmarks/test_decimal_conversion_performance.py @@ -1,6 +1,7 @@ """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 @@ -8,63 +9,75 @@ @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.""" from stock_indicators._cstypes.decimal import to_pydecimal - + 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.""" from stock_indicators._cstypes.decimal import to_pydecimal_via_double - + 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 + 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 + 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) \ No newline at end of file + + benchmark(convert_via_double, cs_decimals) diff --git a/research_decimal_conversion.py b/research_decimal_conversion.py index 98b158d1..a2ffda1f 100644 --- a/research_decimal_conversion.py +++ b/research_decimal_conversion.py @@ -2,7 +2,7 @@ Decimal Conversion Performance Research ====================================== -This script demonstrates the performance vs precision trade-offs between different +This script demonstrates the performance vs precision trade-offs between different decimal conversion methods in the stock-indicators-python library. The library provides two methods for converting C# decimals to Python decimals: @@ -15,8 +15,8 @@ - Precision trade-off: Small but measurable precision loss with floating-point arithmetic """ -from decimal import Decimal as PyDecimal import time + from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes.decimal import to_pydecimal, to_pydecimal_via_double @@ -24,7 +24,7 @@ def demonstrate_precision_differences(): """Demonstrate precision differences between conversion methods.""" print("=== Precision Comparison ===\n") - + test_values = [ 1996.1012, 123.456789, @@ -33,43 +33,51 @@ def demonstrate_precision_differences(): 1.8e-05, 12345678901234567890.123456789, ] - - print(f"{'Value':<30} {'String Method':<35} {'Double Method':<35} {'Difference':<15}") + + print( + f"{'Value':<30} {'String Method':<35} {'Double Method':<35} {'Difference':<15}" + ) print("-" * 115) - + for value in test_values: try: cs_decimal = CsDecimal(value) string_result = to_pydecimal(cs_decimal) double_result = to_pydecimal_via_double(cs_decimal) - - difference = abs(string_result - double_result) if string_result != double_result else 0 - - print(f"{str(value):<30} {str(string_result):<35} {str(double_result):<35} {str(difference):<15}") + + difference = ( + abs(string_result - double_result) + if string_result != double_result + else 0 + ) + + print( + f"{value!s:<30} {string_result!s:<35} {double_result!s:<35} {difference!s:<15}" + ) except Exception as e: - print(f"{str(value):<30} Error: {e}") - + print(f"{value!s:<30} Error: {e}") + print() def demonstrate_performance_differences(): """Demonstrate performance differences between conversion methods.""" print("=== Performance Comparison ===\n") - + # Create test data test_values = [1996.1012, 123.456789, 0.123456789, 999999.999999] * 10000 cs_decimals = [CsDecimal(val) for val in test_values] - + # Benchmark string conversion start_time = time.perf_counter() - string_results = [to_pydecimal(cs_decimal) for cs_decimal in cs_decimals] + _ = [to_pydecimal(cs_decimal) for cs_decimal in cs_decimals] string_time = time.perf_counter() - start_time - - # Benchmark double conversion + + # Benchmark double conversion start_time = time.perf_counter() - double_results = [to_pydecimal_via_double(cs_decimal) for cs_decimal in cs_decimals] + _ = [to_pydecimal_via_double(cs_decimal) for cs_decimal in cs_decimals] double_time = time.perf_counter() - start_time - + print(f"String-based conversion: {string_time:.4f} seconds") print(f"Double-based conversion: {double_time:.4f} seconds") print(f"Performance improvement: {string_time / double_time:.2f}x faster") @@ -79,21 +87,23 @@ def demonstrate_performance_differences(): def recommend_usage(): """Provide recommendations for when to use each method.""" print("=== Usage Recommendations ===\n") - + print("Use String-based conversion (to_pydecimal) when:") print(" • Precision is critical (financial calculations, scientific computing)") print(" • Working with very large numbers or high-precision decimals") print(" • Backward compatibility is required") print(" • Small performance overhead is acceptable") print() - + print("Consider Double-based conversion (to_pydecimal_via_double) when:") print(" • Performance is critical and you're processing large datasets") print(" • Small precision loss is acceptable for your use case") - print(" • Working with typical stock price data where floating-point precision is sufficient") + print( + " • Working with typical stock price data where floating-point precision is sufficient" + ) print(" • You need ~4x performance improvement in decimal conversions") print() - + print("Precision Loss Characteristics:") print(" • Typical loss: 10^-14 to 10^-16 for normal values") print(" • More significant loss with very large numbers (>10^15)") @@ -105,10 +115,14 @@ def recommend_usage(): print("Stock Indicators Python - Decimal Conversion Research") print("=" * 56) print() - + demonstrate_precision_differences() demonstrate_performance_differences() recommend_usage() - - print("Note: This research demonstrates the trade-offs identified in GitHub issue #392") - print("The default string-based method remains unchanged for backward compatibility.") \ No newline at end of file + + print( + "Note: This research demonstrates the trade-offs identified in GitHub issue #392" + ) + print( + "The default string-based method remains unchanged for backward compatibility." + ) diff --git a/tests/common/test_cstype_conversion.py b/tests/common/test_cstype_conversion.py index a6094a21..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, to_pydecimal_via_double +from stock_indicators._cstypes import ( + to_pydatetime, + to_pydecimal, + to_pydecimal_via_double, +) class TestCsTypeConversion: @@ -99,7 +103,7 @@ def test_alternative_decimal_conversion_via_double(self): 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 diff --git a/tests/common/test_decimal_conversion_comparison.py b/tests/common/test_decimal_conversion_comparison.py index 97408570..30938c1e 100644 --- a/tests/common/test_decimal_conversion_comparison.py +++ b/tests/common/test_decimal_conversion_comparison.py @@ -1,7 +1,6 @@ """Tests comparing precision and performance of different decimal conversion methods.""" from decimal import Decimal as PyDecimal -import pytest from stock_indicators._cstypes import Decimal as CsDecimal from stock_indicators._cstypes.decimal import to_pydecimal, to_pydecimal_via_double @@ -9,7 +8,7 @@ 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 = [ @@ -20,13 +19,13 @@ def test_basic_decimal_conversion_comparison(self): 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) - + # Check if they're the same if string_result == double_result: # No precision loss @@ -37,7 +36,7 @@ def test_basic_decimal_conversion_comparison(self): 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 = [ @@ -46,21 +45,21 @@ def test_exponential_notation_conversion_comparison(self): 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}") - + # 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 = [ @@ -68,82 +67,86 @@ def test_large_decimal_conversion_comparison(self): 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}") - + # 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 + 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'), + 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}") - + # 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 + 1e28, # Very large + 0.0, # Zero -123.456, # Negative - float('inf') if hasattr(float, '__dict__') and 'inf' in str(float('inf')) else 1e308, # Large number as alternative + float("inf") + if hasattr(float, "__dict__") and "inf" in str(float("inf")) + else 1e308, # Large number as alternative ] - + 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)}") - + 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 \ No newline at end of file + assert double_result is None From f2683896d8aa7872da5d786f9a7fe8ec183b030e Mon Sep 17 00:00:00 2001 From: Dave Skender <8432125+DaveSkender@users.noreply.github.com> Date: Sat, 27 Dec 2025 02:54:34 -0500 Subject: [PATCH 08/10] test: Update precision expectations for double-based decimal conversion --- tests/test_pivot_points.py | 4 ++-- tests/test_rolling_pivots.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_pivot_points.py b/tests/test_pivot_points.py index 47339b2e..8c49bf14 100644 --- a/tests/test_pivot_points.py +++ b/tests/test_pivot_points.py @@ -135,9 +135,9 @@ def test_camarilla(self, quotes): assert 243.1500 == round(float(r.pp), 4) assert 241.56325 == round(float(r.s1), 5) assert 239.9765 == round(float(r.s2), 4) - assert 238.3898 == float(round(r.s3, 4)) + assert 238.3897 == 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) diff --git a/tests/test_rolling_pivots.py b/tests/test_rolling_pivots.py index 2fc4f2be..b0a01595 100644 --- a/tests/test_rolling_pivots.py +++ b/tests/test_rolling_pivots.py @@ -285,12 +285,12 @@ def test_woodie(self, quotes_intraday): assert r.r4 is None r = results[391] - assert 368.7850 == float(round(r.pp, 4)) + assert 368.7849 == float(round(r.pp, 4)) assert 367.9901 == float(round(r.s1, 4)) assert 365.1252 == float(round(r.s2, 4)) assert 364.3303 == float(round(r.s3, 4)) assert 371.6499 == float(round(r.r1, 4)) - assert 372.4448 == float(round(r.r2, 4)) + assert 372.4447 == float(round(r.r2, 4)) assert 375.3097 == float(round(r.r3, 4)) r = results[1172] From c1b6c8db5a527350500a2ddbe26a1fa4e7dd84fd Mon Sep 17 00:00:00 2001 From: Dave Skender <8432125+DaveSkender@users.noreply.github.com> Date: Sat, 27 Dec 2025 03:11:20 -0500 Subject: [PATCH 09/10] refactor: update function overloads to use pass statement --- stock_indicators/indicators/fractal.py | 6 ++++-- stock_indicators/indicators/ichimoku.py | 9 ++++++--- stock_indicators/indicators/parabolic_sar.py | 10 ++++++++-- stock_indicators/indicators/vwap.py | 6 ++++-- tests/common/test_decimal_conversion_comparison.py | 8 ++------ 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/stock_indicators/indicators/fractal.py b/stock_indicators/indicators/fractal.py index c479096e..43d14ee5 100644 --- a/stock_indicators/indicators/fractal.py +++ b/stock_indicators/indicators/fractal.py @@ -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( diff --git a/stock_indicators/indicators/ichimoku.py b/stock_indicators/indicators/ichimoku.py index af1e369b..8451aaef 100644 --- a/stock_indicators/indicators/ichimoku.py +++ b/stock_indicators/indicators/ichimoku.py @@ -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( 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/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/tests/common/test_decimal_conversion_comparison.py b/tests/common/test_decimal_conversion_comparison.py index 30938c1e..ece8ca8c 100644 --- a/tests/common/test_decimal_conversion_comparison.py +++ b/tests/common/test_decimal_conversion_comparison.py @@ -26,12 +26,8 @@ def test_basic_decimal_conversion_comparison(self): string_result = to_pydecimal(cs_decimal) double_result = to_pydecimal_via_double(cs_decimal) - # Check if they're the same - if string_result == double_result: - # No precision loss - assert string_result == double_result - else: - # Document precision loss + # 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}") From ba7f71edab9f82d42ad65c7c9bd99afec41ad308 Mon Sep 17 00:00:00 2001 From: Dave Skender <8432125+DaveSkender@users.noreply.github.com> Date: Sat, 27 Dec 2025 03:17:04 -0500 Subject: [PATCH 10/10] docs: update performance benchmarks for v1.3.0 --- docs/pages/performance.md | 107 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) 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.