sqa-tai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Monkey patch for ta_lib_ffi 0.3.0 to fix multi-array parameter bug
4
+ #
5
+ # This patch fixes a critical bug in ta_lib_ffi 0.3.0 where functions requiring
6
+ # multiple array parameters (high, low, close) fail with NoMethodError.
7
+ #
8
+ # Affected functions:
9
+ # - All volatility indicators (ATR, NATR, SAR, TRANGE)
10
+ # - All momentum indicators requiring OHLC (CCI, WILLR, ADX, STOCH)
11
+ # - All volume indicators (OBV, AD, ADOSC)
12
+ # - All candlestick pattern recognition functions (60+ patterns)
13
+ #
14
+ # This patch can be removed once ta_lib_ffi fixes the upstream bug.
15
+
16
+ module TALibFFI
17
+ class << self
18
+ # Store original method to call it for non-price parameters
19
+ alias_method :original_setup_input_parameters, :setup_input_parameters
20
+
21
+ # Fixed version that properly handles multi-array Price inputs
22
+ #
23
+ # The original code assumes a 1-to-1 mapping between input_arrays and function
24
+ # parameters, but TA_Input_Price parameters can consume multiple arrays.
25
+ #
26
+ # This fix:
27
+ # 1. Tracks position in input_arrays with array_index
28
+ # 2. For each function parameter, checks its type
29
+ # 3. For TA_Input_Price, determines how many arrays are needed from flags
30
+ # 4. Bundles those arrays together: [[high], [low], [close]]
31
+ # 5. Passes the bundle to setup_price_inputs
32
+ # 6. Advances array_index by the number of arrays consumed
33
+ def setup_input_parameters(params_ptr, input_arrays, func_name)
34
+ func_info = function_info_map[func_name]
35
+ array_index = 0 # Track position in input_arrays
36
+
37
+ func_info[:inputs].each_with_index do |input_info, param_index|
38
+ case input_info["type"]
39
+ when TA_PARAM_TYPE[:TA_Input_Price]
40
+ # Price inputs consume multiple arrays based on required flags
41
+ required_flags = extract_flags(input_info["flags"], :TA_InputFlags)
42
+ num_arrays = required_flags.length
43
+
44
+ # Collect the next num_arrays from input_arrays
45
+ if array_index + num_arrays > input_arrays.length
46
+ raise ArgumentError, "Function #{func_name} requires #{num_arrays} price arrays " \
47
+ "(#{required_flags.join(', ')}), but only #{input_arrays.length - array_index} " \
48
+ "provided at position #{array_index}"
49
+ end
50
+
51
+ price_arrays = input_arrays[array_index, num_arrays]
52
+
53
+ # Pass the bundled arrays to set_input_parameter
54
+ ret_code = set_input_parameter(params_ptr, param_index, price_arrays, input_info)
55
+ check_ta_return_code(ret_code)
56
+
57
+ array_index += num_arrays # Advance by number of arrays consumed
58
+
59
+ when TA_PARAM_TYPE[:TA_Input_Real], TA_PARAM_TYPE[:TA_Input_Integer]
60
+ # Single array inputs
61
+ if array_index >= input_arrays.length
62
+ raise ArgumentError, "Not enough input arrays for function #{func_name} " \
63
+ "(expected array at index #{array_index}, but only #{input_arrays.length} provided)"
64
+ end
65
+
66
+ ret_code = set_input_parameter(params_ptr, param_index, input_arrays[array_index], input_info)
67
+ check_ta_return_code(ret_code)
68
+
69
+ array_index += 1 # Advance by one
70
+
71
+ else
72
+ # Unknown type - should not happen, but handle gracefully
73
+ raise ArgumentError, "Unknown input type #{input_info['type']} for function #{func_name}"
74
+ end
75
+ end
76
+
77
+ # Verify all arrays were consumed
78
+ if array_index != input_arrays.length
79
+ raise ArgumentError, "Function #{func_name} expected #{array_index} input arrays " \
80
+ "but received #{input_arrays.length}"
81
+ end
82
+ end
83
+
84
+ # Store original method
85
+ alias_method :original_setup_price_inputs, :setup_price_inputs
86
+
87
+ # Fixed version that handles bundled price arrays correctly
88
+ #
89
+ # After the setup_input_parameters fix, price_data is now guaranteed to be
90
+ # an array of arrays: [[high_array], [low_array], [close_array]]
91
+ def setup_price_inputs(params_ptr, index, price_data, flags)
92
+ required_flags = extract_flags(flags, :TA_InputFlags)
93
+ data_pointers = Array.new(6) { Fiddle::Pointer.malloc(0) }
94
+
95
+ # price_data is now an array of arrays: [[high], [low], [close]]
96
+ # Each element corresponds to one required flag
97
+ required_flags.each_with_index do |flag, i|
98
+ flag_index = TA_FLAGS[:TA_InputFlags].keys.index(flag)
99
+
100
+ # price_data[i] now correctly returns an array (e.g., high, low, or close)
101
+ if price_data[i].nil?
102
+ raise ArgumentError, "Missing price array for flag #{flag} at index #{i}"
103
+ end
104
+
105
+ # Validate it's an array
106
+ unless price_data[i].is_a?(Array)
107
+ raise ArgumentError, "Expected array for flag #{flag} at index #{i}, " \
108
+ "got #{price_data[i].class}"
109
+ end
110
+
111
+ data_pointers[flag_index] = prepare_double_array(price_data[i]) if required_flags.include?(flag)
112
+ end
113
+
114
+ TA_SetInputParamPricePtr(params_ptr, index, *data_pointers)
115
+ end
116
+
117
+ # Store original method
118
+ alias_method :original_calculate_results, :calculate_results
119
+
120
+ # Fixed version that correctly determines input_size for Price inputs
121
+ #
122
+ # The original code expects input_arrays[0][0].length for Price inputs,
123
+ # which assumes format [[high], [low], [close]]. Our fix passes [high, low, close],
124
+ # so we need to adjust this check.
125
+ def calculate_results(params_ptr, input_arrays, func_name)
126
+ func_info = function_info_map[func_name]
127
+
128
+ # Determine input size based on input type
129
+ input_size = if func_info[:inputs].first["type"] == TA_PARAM_TYPE[:TA_Input_Price]
130
+ # For Price inputs, input_arrays is [high, low, close]
131
+ # Get length from the first array
132
+ input_arrays[0].length
133
+ else
134
+ # For other inputs, get length normally
135
+ input_arrays[0].length
136
+ end
137
+
138
+ out_begin = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
139
+ out_size = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
140
+ output_arrays = setup_output_buffers(params_ptr, input_size, func_name)
141
+
142
+ begin
143
+ ret_code = TA_CallFunc(params_ptr, 0, input_size - 1, out_begin, out_size)
144
+ check_ta_return_code(ret_code)
145
+
146
+ actual_size = out_size[0, Fiddle::SIZEOF_INT].unpack1("l")
147
+ format_output_results(output_arrays, actual_size, func_name)
148
+ ensure
149
+ out_begin.free
150
+ out_size.free
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SQA
4
+ module TAI
5
+ VERSION = "0.1.0"
6
+ end
7
+ end