ta_lib_ffi 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e3fd4ceb928f6fc7a08af545db5e373e3243e308a6340a4b4b323988f01d8bf3
4
+ data.tar.gz: 05bde454be698fa4d1fbdaac36fb13378651219f78ede4b7fc3b7433cf8fae3f
5
+ SHA512:
6
+ metadata.gz: 5f78a80b04c9d0c8fe70047337a8b8a5674adb70a6148e418b6548afcad1025929cd63fdf6d904e5d26fbe501c596003680615154ac6e954512e19f887e84eca
7
+ data.tar.gz: 265fd54ed815b7750091151e4c0256076a122734169a1c7923b9810ea739dc691bf36cb5d1c6bc39f7cfc048efef874ef9fc1718aa98bb20b42720d2162e8c1e
data/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2024-01-21
9
+
10
+ ### Added
11
+ - Initial release of ta_lib_ffi
12
+ - Basic FFI wrapper for TA-Lib
13
+ - Cross-platform support:
14
+ - Windows (64-bit)
15
+ - macOS (via Homebrew)
16
+ - Linux (Debian/Ubuntu packages)
17
+ - Automated tests with GitHub Actions
18
+ - Basic documentation and usage examples
19
+
20
+ ### Changed
21
+ - N/A
22
+
23
+ ### Deprecated
24
+ - N/A
25
+
26
+ ### Removed
27
+ - N/A
28
+
29
+ ### Fixed
30
+ - N/A
31
+
32
+ ### Security
33
+ - N/A
34
+
35
+ [0.1.0]: https://github.com/Youngv/ta_lib_ffi/releases/tag/v0.1.0
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Victor Yang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # TALib
2
+
3
+ ![Tests](https://github.com/Youngv/ta_lib_ffi/actions/workflows/main.yml/badge.svg)
4
+
5
+ ## Introduction
6
+
7
+ TALib is a Ruby binding for [TA-Lib](https://ta-lib.org/) (Technical Analysis Library) using FFI (Foreign Function Interface). It provides a comprehensive set of functions for technical analysis of financial market data.
8
+
9
+ ## Requirements
10
+
11
+ - Ruby >= 3.0.0
12
+ - TA-Lib >= 0.6.4
13
+
14
+ ## Installation
15
+
16
+ ### [Install TA-Lib](https://ta-lib.org/install/)
17
+
18
+ #### Windows
19
+ Download and run the installer: [ta-lib-0.6.4-windows-x86_64.msi](https://github.com/ta-lib/ta-lib/releases/download/v0.6.4/ta-lib-0.6.4-windows-x86_64.msi)
20
+
21
+ #### macOS
22
+ ```bash
23
+ brew install ta-lib
24
+ ```
25
+
26
+ #### Linux (Debian/Ubuntu)
27
+ ```bash
28
+ # For Intel/AMD 64-bit
29
+ wget https://github.com/ta-lib/ta-lib/releases/download/v0.6.4/ta-lib_0.6.4_amd64.deb
30
+ sudo dpkg -i ta-lib_0.6.4_amd64.deb
31
+
32
+ # For ARM64
33
+ wget https://github.com/ta-lib/ta-lib/releases/download/v0.6.4/ta-lib_0.6.4_arm64.deb
34
+ sudo dpkg -i ta-lib_0.6.4_arm64.deb
35
+
36
+ # For Intel/AMD 32-bits
37
+ wget https://github.com/ta-lib/ta-lib/releases/download/v0.6.4/ta-lib_0.6.4_i386.deb
38
+ sudo dpkg -i ta-lib_0.6.4_i386.deb
39
+ ```
40
+
41
+ ### Installing the Ruby Gem
42
+
43
+ Add this to your application's Gemfile:
44
+
45
+ ```ruby
46
+ gem 'ta_lib_ffi'
47
+ ```
48
+
49
+ Then execute:
50
+
51
+ $ bundle install
52
+
53
+ Or install it directly:
54
+
55
+ $ gem install ta_lib_ffi
56
+
57
+ ## Usage
58
+
59
+ ```ruby
60
+ require 'ta_lib'
61
+
62
+ # Initialize data
63
+ prices = [10.0, 11.0, 12.0, 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0]
64
+
65
+ # Calculate SMA
66
+ puts TALib.sma(prices, time_period: 3)
67
+ # => [11.0, 11.333333333333334, 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0]
68
+ ```
69
+
70
+ ## TODO
71
+ - [ ] Add RDoc documentation for Ruby methods
72
+ - [ ] Create detailed function examples with input/output samples
73
+ - [ ] Add more tests for each function
74
+ - [ ] Support custom TA-Lib installation location
75
+
76
+ ## Development
77
+
78
+ 1. Fork the repository
79
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
80
+ 3. Commit your changes (`git commit -am 'Add some amazing feature'`)
81
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
82
+ 5. Create a Pull Request
83
+
84
+ ## Contributing
85
+
86
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Youngv/ta_lib_ffi
87
+
88
+ ## License
89
+
90
+ This gem is available as open source under the terms of the [MIT License](LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/lib/ta_lib.rb ADDED
@@ -0,0 +1,647 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+ require "fiddle/import"
5
+
6
+ # Ruby FFI wrapper for TA-Lib (Technical Analysis Library)
7
+ module TALib
8
+ VERSION = "0.1.0"
9
+
10
+ extend Fiddle::Importer
11
+
12
+ lib_path = case RUBY_PLATFORM
13
+ when /darwin/
14
+ brew_prefix = `brew --prefix`.chomp
15
+ "#{brew_prefix}/lib/libta-lib.dylib"
16
+ when /linux/
17
+ "libta-lib.so"
18
+ when /cygwin|mswin|mingw|bccwin|wince|emx/
19
+ "C:/Program Files/TA-Lib/bin/ta-lib.dll"
20
+ else
21
+ raise "Unsupported platform"
22
+ end
23
+
24
+ dlload lib_path
25
+
26
+ class TALibError < StandardError; end
27
+
28
+ TA_SUCCESS = 0
29
+ TA_LIB_NOT_INITIALIZE = 1
30
+ TA_BAD_PARAM = 2
31
+ TA_ALLOC_ERR = 3
32
+ TA_GROUP_NOT_FOUND = 4
33
+ TA_FUNC_NOT_FOUND = 5
34
+ TA_INVALID_HANDLE = 6
35
+ TA_INVALID_PARAM_HOLDER = 7
36
+ TA_INVALID_PARAM_HOLDER_TYPE = 8
37
+ TA_INVALID_PARAM_FUNCTION = 9
38
+ TA_INPUT_NOT_ALL_INITIALIZE = 10
39
+ TA_OUTPUT_NOT_ALL_INITIALIZE = 11
40
+ TA_OUT_OF_RANGE_START_INDEX = 12
41
+ TA_OUT_OF_RANGE_END_INDEX = 13
42
+ TA_INVALID_LIST_TYPE = 14
43
+ TA_BAD_OBJECT = 15
44
+ TA_NOT_SUPPORTED = 16
45
+ TA_INTERNAL_ERROR = 5000
46
+ TA_UNKNOWN_ERR = 0xFFFF
47
+
48
+ # {0,"SMA"},
49
+ # {1,"EMA"},
50
+ # {2,"WMA"},
51
+ # {3,"DEMA" },
52
+ # {4,"TEMA" },
53
+ # {5,"TRIMA"},
54
+ # {6,"KAMA" },
55
+ # {7,"MAMA" },
56
+ # {8,"T3"}
57
+
58
+ typealias "TA_Real", "double"
59
+ typealias "TA_Integer", "int"
60
+ typealias "TA_RetCode", "int"
61
+ typealias "TA_FuncHandle", "unsigned int"
62
+ typealias "TA_FuncFlags", "int"
63
+ typealias "TA_CallForEachFunc", "void *"
64
+ typealias "TA_InputParameterType", "int"
65
+ typealias "TA_InputFlags", "int"
66
+ typealias "TA_OptInputParameterType", "int"
67
+ typealias "TA_OptInputFlags", "int"
68
+ typealias "TA_OutputParameterType", "int"
69
+ typealias "TA_OutputFlags", "int"
70
+
71
+ TA_StringTable = struct [
72
+ "unsigned int size",
73
+ "const char **string",
74
+ "void *hiddenData"
75
+ ]
76
+
77
+ TA_FuncInfo = struct [
78
+ "const char *name",
79
+ "const char *group",
80
+ "const char *hint",
81
+ "const char *camelCaseName",
82
+ "TA_FuncFlags flags",
83
+ "unsigned int nbInput",
84
+ "unsigned int nbOptInput",
85
+ "unsigned int nbOutput",
86
+ "const TA_FuncHandle *handle"
87
+ ]
88
+
89
+ TA_ParamHolder = struct [
90
+ "void *hiddenData"
91
+ ]
92
+
93
+ TA_InputParameterInfo = struct [
94
+ "TA_InputParameterType type",
95
+ "const char *paramName",
96
+ "TA_InputFlags flags"
97
+ ]
98
+
99
+ TA_OptInputParameterInfo = struct [
100
+ "TA_OptInputParameterType type",
101
+ "const char *paramName",
102
+ "TA_OptInputFlags flags",
103
+ "const char *displayName",
104
+ "const void *dataSet",
105
+ "TA_Real defaultValue",
106
+ "const char *hint",
107
+ "const char *helpFile"
108
+ ]
109
+
110
+ TA_OutputParameterInfo = struct [
111
+ "TA_OutputParameterType type",
112
+ "const char *paramName",
113
+ "TA_OutputFlags flags"
114
+ ]
115
+
116
+ TA_PARAM_TYPE = {
117
+ TA_Input_Price: 0,
118
+ TA_Input_Real: 1,
119
+ TA_Input_Integer: 2,
120
+ TA_OptInput_RealRange: 0,
121
+ TA_OptInput_RealList: 1,
122
+ TA_OptInput_IntegerRange: 2,
123
+ TA_OptInput_IntegerList: 3,
124
+ TA_Output_Real: 0,
125
+ TA_Output_Integer: 1
126
+ }.freeze
127
+
128
+ TA_FLAGS = {
129
+ TA_InputFlags: {
130
+ TA_IN_PRICE_OPEN: 0x00000001,
131
+ TA_IN_PRICE_HIGH: 0x00000002,
132
+ TA_IN_PRICE_LOW: 0x00000004,
133
+ TA_IN_PRICE_CLOSE: 0x00000008,
134
+ TA_IN_PRICE_VOLUME: 0x00000010,
135
+ TA_IN_PRICE_OPENINTEREST: 0x00000020,
136
+ TA_IN_PRICE_TIMESTAMP: 0x00000040
137
+ },
138
+ TA_OptInputFlags: {
139
+ TA_OPTIN_IS_PERCENT: 0x00100000,
140
+ TA_OPTIN_IS_DEGREE: 0x00200000,
141
+ TA_OPTIN_IS_CURRENCY: 0x00400000,
142
+ TA_OPTIN_ADVANCED: 0x01000000
143
+ },
144
+ TA_OutputFlags: {
145
+ TA_OUT_LINE: 0x00000001,
146
+ TA_OUT_DOT_LINE: 0x00000002,
147
+ TA_OUT_DASH_LINE: 0x00000004,
148
+ TA_OUT_DOT: 0x00000008,
149
+ TA_OUT_HISTO: 0x00000010,
150
+ TA_OUT_PATTERN_BOOL: 0x00000020,
151
+ TA_OUT_PATTERN_BULL_BEAR: 0x00000040,
152
+ TA_OUT_PATTERN_STRENGTH: 0x00000080,
153
+ TA_OUT_POSITIVE: 0x00000100,
154
+ TA_OUT_NEGATIVE: 0x00000200,
155
+ TA_OUT_ZERO: 0x00000400,
156
+ TA_OUT_UPPER_LIMIT: 0x00000800,
157
+ TA_OUT_LOWER_LIMIT: 0x00001000
158
+ }
159
+ }.freeze
160
+
161
+ extern "int TA_Initialize()"
162
+ extern "int TA_Shutdown()"
163
+ extern "int TA_GroupTableAlloc(TA_StringTable**)"
164
+ extern "int TA_GroupTableFree(TA_StringTable*)"
165
+ extern "TA_RetCode TA_FuncTableAlloc(const char *group, TA_StringTable **table)"
166
+ extern "TA_RetCode TA_FuncTableFree(TA_StringTable *table)"
167
+ extern "TA_FuncHandle TA_GetFuncHandle(const char *name, const TA_FuncHandle **handle)"
168
+ extern "TA_RetCode TA_GetFuncInfo(const TA_FuncHandle *handle, const TA_FuncInfo **funcInfo)"
169
+ extern "TA_RetCode TA_ForEachFunc(TA_CallForEachFunc functionToCall, void *opaqueData)"
170
+ extern "TA_RetCode TA_GetInputParameterInfo(const TA_FuncHandle *handle, unsigned int paramIndex, const TA_InputParameterInfo **info)"
171
+ extern "TA_RetCode TA_GetOptInputParameterInfo(const TA_FuncHandle *handle, unsigned int paramIndex, const TA_OptInputParameterInfo **info)"
172
+ extern "TA_RetCode TA_GetOutputParameterInfo(const TA_FuncHandle *handle, unsigned int paramIndex, const TA_OutputParameterInfo **info)"
173
+ extern "TA_RetCode TA_ParamHolderAlloc(const TA_FuncHandle *handle, TA_ParamHolder **allocatedParams)"
174
+ extern "TA_RetCode TA_ParamHolderFree(TA_ParamHolder *params)"
175
+ extern "TA_RetCode TA_SetInputParamIntegerPtr(TA_ParamHolder *params, unsigned int paramIndex, const TA_Integer *value)"
176
+ extern "TA_RetCode TA_SetInputParamRealPtr(TA_ParamHolder *params, unsigned int paramIndex, const TA_Real *value)"
177
+ extern "TA_RetCode TA_SetInputParamPricePtr(TA_ParamHolder *params,
178
+ unsigned int paramIndex,
179
+ const TA_Real *open,
180
+ const TA_Real *high,
181
+ const TA_Real *low,
182
+ const TA_Real *close,
183
+ const TA_Real *volume,
184
+ const TA_Real *openInterest)"
185
+ extern "TA_RetCode TA_SetOptInputParamInteger(TA_ParamHolder *params, unsigned int paramIndex, TA_Integer optInValue)"
186
+ extern "TA_RetCode TA_SetOptInputParamReal(TA_ParamHolder *params, unsigned int paramIndex, TA_Real optInValue)"
187
+ extern "TA_RetCode TA_SetOutputParamIntegerPtr(TA_ParamHolder *params, unsigned int paramIndex, TA_Integer *out)"
188
+ extern "TA_RetCode TA_SetOutputParamRealPtr(TA_ParamHolder *params, unsigned int paramIndex, TA_Real *out)"
189
+ extern "TA_RetCode TA_GetLookback(const TA_ParamHolder *params, TA_Integer *lookback)"
190
+ extern "TA_RetCode TA_CallFunc(const TA_ParamHolder *params,
191
+ TA_Integer startIdx,
192
+ TA_Integer endIdx,
193
+ TA_Integer *outBegIdx,
194
+ TA_Integer *outNbElement)"
195
+ extern "const char *TA_FunctionDescriptionXML(void)"
196
+
197
+ module_function
198
+
199
+ def extract_flags(value, type)
200
+ flags_set = []
201
+ TA_FLAGS[type].each do |k, v|
202
+ flags_set << k if (value & v) != 0
203
+ end
204
+ flags_set
205
+ end
206
+
207
+ def group_table
208
+ string_table_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
209
+ ret_code = TA_GroupTableAlloc(string_table_ptr.ref)
210
+ check_ta_return_code(ret_code)
211
+
212
+ string_table = TA_StringTable.new(string_table_ptr)
213
+ group_names = Fiddle::Pointer.new(string_table["string"])[0, Fiddle::SIZEOF_VOIDP * string_table["size"]].unpack("Q*").collect { |ptr| Fiddle::Pointer.new(ptr).to_s }
214
+ TA_GroupTableFree(string_table_ptr)
215
+
216
+ group_names
217
+ end
218
+
219
+ def function_table(group)
220
+ string_table_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
221
+ ret_code = TA_FuncTableAlloc(group, string_table_ptr.ref)
222
+ check_ta_return_code(ret_code)
223
+
224
+ string_table = TA_StringTable.new(string_table_ptr)
225
+ func_names = Fiddle::Pointer.new(string_table["string"])[0, Fiddle::SIZEOF_VOIDP * string_table["size"]].unpack("Q*").collect { |ptr| Fiddle::Pointer.new(ptr).to_s }
226
+
227
+ TA_FuncTableFree(string_table)
228
+
229
+ func_names
230
+ end
231
+
232
+ def function_info(name)
233
+ handle_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
234
+ ret_code = TA_GetFuncHandle(name, handle_ptr.ref)
235
+ check_ta_return_code(ret_code)
236
+
237
+ info_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
238
+ ret_code = TA_GetFuncInfo(handle_ptr, info_ptr.ref)
239
+ check_ta_return_code(ret_code)
240
+
241
+ TA_FuncInfo.new(info_ptr)
242
+ end
243
+
244
+ def each_function(&block)
245
+ callback = Fiddle::Closure::BlockCaller.new(
246
+ Fiddle::TYPE_VOID,
247
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP],
248
+ Fiddle::Function::DEFAULT
249
+ ) do |func_info_ptr, _|
250
+ block.call TA_FuncInfo.new(func_info_ptr)
251
+ end
252
+
253
+ ret_code = TA_ForEachFunc(callback, nil)
254
+ check_ta_return_code(ret_code)
255
+ end
256
+
257
+ # rubocop:disable Metrics/MethodLength
258
+ # rubocop:disable Metrics/AbcSize
259
+ def print_function_info(func_info)
260
+ puts "Function Name: #{func_info["name"]}"
261
+ puts "Function Group: #{func_info["group"]}"
262
+ puts "Function Hint: #{func_info["hint"]}"
263
+ puts "Camel Case Name: #{func_info["camelCaseName"]}"
264
+ puts "Flags: #{func_info["flags"]}"
265
+ puts "Number of Inputs: #{func_info["nbInput"]}"
266
+ puts "Number of Optional Inputs: #{func_info["nbOptInput"]}"
267
+ puts "Number of Outputs: #{func_info["nbOutput"]}"
268
+ puts "Function Handle: #{func_info["handle"].to_i}"
269
+
270
+ puts "\nInput Parameter Info:"
271
+ func_info["nbInput"].times do |i|
272
+ param_info_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
273
+ ret_code = TA_GetInputParameterInfo(func_info["handle"], i, param_info_ptr.ref)
274
+ check_ta_return_code(ret_code)
275
+ param_info = TA_InputParameterInfo.new(param_info_ptr)
276
+ puts " Parameter #{i + 1}:"
277
+ puts " Name: #{param_info["paramName"]}"
278
+ puts " Type: #{param_info["type"]}"
279
+ puts " Flags: #{extract_flags(param_info["flags"], :TA_InputFlags)}"
280
+ end
281
+
282
+ puts "\nOptional Input Parameter Info:"
283
+ func_info["nbOptInput"].times do |i|
284
+ param_info_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
285
+ ret_code = TA_GetOptInputParameterInfo(func_info["handle"], i, param_info_ptr.ref)
286
+ check_ta_return_code(ret_code)
287
+ param_info = TA_OptInputParameterInfo.new(param_info_ptr)
288
+ puts " Parameter #{i + 1}:"
289
+ puts " Name: #{param_info["paramName"]}"
290
+ puts " Type: #{param_info["type"]}"
291
+ puts " Flags: #{extract_flags(param_info["flags"], :TA_OptInputFlags)}"
292
+ puts " Display Name: #{param_info["displayName"]}"
293
+ puts " Default Value: #{param_info["defaultValue"]}"
294
+ puts " Hint: #{param_info["hint"]}"
295
+ end
296
+
297
+ puts "\nOutput Parameter Info:"
298
+ func_info["nbOutput"].times do |i|
299
+ param_info_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
300
+ ret_code = TA_GetOutputParameterInfo(func_info["handle"], i, param_info_ptr.ref)
301
+ check_ta_return_code(ret_code)
302
+ param_info = TA_OutputParameterInfo.new(param_info_ptr)
303
+ puts " Parameter #{i + 1}:"
304
+ puts " Name: #{param_info["paramName"]}"
305
+ puts " Type: #{param_info["type"]}"
306
+ puts " Flags: #{extract_flags(param_info["flags"], :TA_OutputFlags)}"
307
+ end
308
+ end
309
+ # rubocop:enable Metrics/MethodLength
310
+ # rubocop:enable Metrics/AbcSize
311
+
312
+ # rubocop:disable Metrics/MethodLength
313
+ def call_func(func_name, args)
314
+ options = args.last.is_a?(Hash) ? args.pop : {}
315
+ input_arrays = args
316
+
317
+ validate_inputs!(input_arrays)
318
+
319
+ handle_ptr = get_function_handle(func_name)
320
+ params_ptr = create_parameter_holder(handle_ptr)
321
+
322
+ begin
323
+ setup_input_parameters(params_ptr, input_arrays, func_name)
324
+ setup_optional_parameters(params_ptr, options, func_name)
325
+ _lookback = calculate_lookback(params_ptr)
326
+ calculate_results(params_ptr, input_arrays.first.length, func_name)
327
+ ensure
328
+ TA_ParamHolderFree(params_ptr)
329
+ end
330
+ end
331
+ # rubocop:enable Metrics/MethodLength
332
+
333
+ def calculate_lookback(params_ptr)
334
+ lookback_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
335
+ ret_code = TA_GetLookback(params_ptr, lookback_ptr)
336
+ check_ta_return_code(ret_code)
337
+ lookback_ptr[0, Fiddle::SIZEOF_INT].unpack1("l")
338
+ end
339
+
340
+ # rubocop:disable Metrics/CyclomaticComplexity
341
+ # rubocop:disable Metrics/PerceivedComplexity
342
+ def validate_inputs!(arrays)
343
+ raise TALibError, "Input arrays cannot be empty" if arrays.empty?
344
+
345
+ arrays.each do |arr|
346
+ raise TALibError, "Input must be arrays" unless arr.is_a?(Array)
347
+ end
348
+
349
+ sizes = arrays.map(&:length)
350
+ raise TALibError, "Input arrays cannot be empty" if sizes.any?(&:zero?)
351
+
352
+ arrays.each do |arr|
353
+ raise TALibError, "Input arrays must contain only numbers" unless arr.flatten.all? { |x| x.is_a?(Numeric) }
354
+ end
355
+ end
356
+ # rubocop:enable Metrics/CyclomaticComplexity
357
+ # rubocop:enable Metrics/PerceivedComplexity
358
+
359
+ def get_function_handle(func_name)
360
+ handle_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
361
+ ret_code = TA_GetFuncHandle(func_name, handle_ptr.ref)
362
+ check_ta_return_code(ret_code)
363
+ handle_ptr
364
+ end
365
+
366
+ def create_parameter_holder(handle_ptr)
367
+ params_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
368
+ ret_code = TA_ParamHolderAlloc(handle_ptr, params_ptr.ref)
369
+ check_ta_return_code(ret_code)
370
+ params_ptr
371
+ end
372
+
373
+ def setup_input_parameters(params_ptr, input_arrays, func_name)
374
+ func_info = function_info_map[func_name]
375
+ input_arrays.each_with_index do |array, index|
376
+ input_info = func_info[:inputs][index]
377
+ ret_code = set_input_parameter(params_ptr, index, array, input_info)
378
+ check_ta_return_code(ret_code)
379
+ end
380
+ end
381
+
382
+ def set_input_parameter(params_ptr, index, array, input_info)
383
+ case input_info["type"]
384
+ when TA_PARAM_TYPE[:TA_Input_Real]
385
+ input_ptr = prepare_double_array(array)
386
+ TA_SetInputParamRealPtr(params_ptr, index, input_ptr)
387
+ when TA_PARAM_TYPE[:TA_Input_Integer]
388
+ input_ptr = prepare_integer_array(array)
389
+ TA_SetInputParamIntegerPtr(params_ptr, index, input_ptr)
390
+ when TA_PARAM_TYPE[:TA_Input_Price]
391
+ setup_price_inputs(params_ptr, index, array, input_info["flags"])
392
+ end
393
+ end
394
+
395
+ def prepare_double_array(array)
396
+ array_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE * array.length)
397
+ array.each_with_index do |value, i|
398
+ array_ptr[i * Fiddle::SIZEOF_DOUBLE, Fiddle::SIZEOF_DOUBLE] = [value.to_f].pack("d")
399
+ end
400
+ array_ptr
401
+ end
402
+
403
+ def prepare_integer_array(array)
404
+ array_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT * array.length)
405
+ array.each_with_index do |value, i|
406
+ array_ptr[i * Fiddle::SIZEOF_INT, Fiddle::SIZEOF_INT] = [value.to_i].pack("l")
407
+ end
408
+ array_ptr
409
+ end
410
+
411
+ def setup_optional_parameters(params_ptr, options, func_name)
412
+ func_info = function_info_map[func_name]
413
+ func_info[:opt_inputs]&.each_with_index do |opt_input, index|
414
+ param_name = normalize_parameter_name(opt_input["paramName"].to_s)
415
+ set_optional_parameter(params_ptr, index, options[param_name.to_sym], opt_input["type"]) if options.key?(param_name.to_sym)
416
+ end
417
+ end
418
+
419
+ def set_optional_parameter(params_ptr, index, value, type)
420
+ case type
421
+ when TA_PARAM_TYPE[:TA_OptInput_RealRange], TA_PARAM_TYPE[:TA_OptInput_RealList]
422
+ ret_code = TA_SetOptInputParamReal(params_ptr, index, value)
423
+ when TA_PARAM_TYPE[:TA_OptInput_IntegerRange], TA_PARAM_TYPE[:TA_OptInput_IntegerList]
424
+ ret_code = TA_SetOptInputParamInteger(params_ptr, index, value)
425
+ end
426
+ check_ta_return_code(ret_code)
427
+ end
428
+
429
+ # rubocop:disable Metrics/MethodLength
430
+ def calculate_results(params_ptr, input_size, func_name)
431
+ out_begin = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
432
+ out_size = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
433
+ output_arrays = setup_output_buffers(params_ptr, input_size, func_name)
434
+
435
+ begin
436
+ ret_code = TA_CallFunc(params_ptr, 0, input_size - 1, out_begin, out_size)
437
+ check_ta_return_code(ret_code)
438
+
439
+ actual_size = out_size[0, Fiddle::SIZEOF_INT].unpack1("l")
440
+ format_output_results(output_arrays, actual_size, func_name)
441
+ ensure
442
+ out_begin.free
443
+ out_size.free
444
+ output_arrays.each(&:free)
445
+ end
446
+ end
447
+ # rubocop:enable Metrics/MethodLength
448
+
449
+ # rubocop:disable Metrics/MethodLength
450
+ # rubocop:disable Metrics/AbcSize
451
+ def setup_output_buffers(params_ptr, size, func_name)
452
+ func_info = function_info_map[func_name]
453
+ output_ptrs = []
454
+
455
+ func_info[:outputs].each_with_index do |output, index|
456
+ ptr = case output["type"]
457
+ when TA_PARAM_TYPE[:TA_Output_Real]
458
+ Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE * size)
459
+ when TA_PARAM_TYPE[:TA_Output_Integer]
460
+ Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT * size)
461
+ end
462
+
463
+ output_ptrs << ptr
464
+
465
+ ret_code = case output["type"]
466
+ when TA_PARAM_TYPE[:TA_Output_Real]
467
+ TA_SetOutputParamRealPtr(params_ptr, index, ptr)
468
+ when TA_PARAM_TYPE[:TA_Output_Integer]
469
+ TA_SetOutputParamIntegerPtr(params_ptr, index, ptr)
470
+ end
471
+
472
+ check_ta_return_code(ret_code)
473
+ end
474
+
475
+ output_ptrs
476
+ end
477
+ # rubocop:enable Metrics/MethodLength
478
+ # rubocop:enable Metrics/AbcSize
479
+
480
+ # rubocop:disable Metrics/MethodLength
481
+ # rubocop:disable Metrics/AbcSize
482
+ def format_output_results(output_ptrs, size, func_name)
483
+ func_info = function_info_map[func_name]
484
+ results = output_ptrs.zip(func_info[:outputs]).map do |ptr, output|
485
+ case output["type"]
486
+ when TA_PARAM_TYPE[:TA_Output_Real]
487
+ ptr[0, Fiddle::SIZEOF_DOUBLE * size].unpack("d#{size}")
488
+ when TA_PARAM_TYPE[:TA_Output_Integer]
489
+ ptr[0, Fiddle::SIZEOF_INT * size].unpack("l#{size}")
490
+ end
491
+ end
492
+
493
+ return results.first if results.length == 1
494
+
495
+ output_names = func_info[:outputs].map do |output|
496
+ normalize_parameter_name(output["paramName"].to_s).to_sym
497
+ end
498
+ output_names.zip(results).to_h
499
+ end
500
+ # rubocop:enable Metrics/AbcSize
501
+ # rubocop:enable Metrics/MethodLength
502
+
503
+ def function_description_xml
504
+ TA_FunctionDescriptionXML().to_s
505
+ end
506
+
507
+ def function_info_map
508
+ @function_info_map ||= build_function_info_map
509
+ end
510
+
511
+ def build_function_info_map
512
+ info_map = {}
513
+ each_function do |func_info|
514
+ info_map[func_info["name"].to_s] = {
515
+ info: func_info,
516
+ inputs: collect_input_info(func_info),
517
+ outputs: collect_output_info(func_info),
518
+ opt_inputs: collect_opt_input_info(func_info)
519
+ }
520
+ end
521
+ info_map
522
+ end
523
+
524
+ def collect_input_info(func_info)
525
+ func_info["nbInput"].times.map do |i|
526
+ param_info_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
527
+ TA_GetInputParameterInfo(func_info["handle"], i, param_info_ptr.ref)
528
+ TA_InputParameterInfo.new(param_info_ptr)
529
+ end
530
+ end
531
+
532
+ def collect_opt_input_info(func_info)
533
+ func_info["nbOptInput"].times.map do |i|
534
+ param_info_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
535
+ TA_GetOptInputParameterInfo(func_info["handle"], i, param_info_ptr.ref)
536
+ TA_OptInputParameterInfo.new(param_info_ptr)
537
+ end
538
+ end
539
+
540
+ def collect_output_info(func_info)
541
+ func_info["nbOutput"].times.map do |i|
542
+ param_info_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
543
+ TA_GetOutputParameterInfo(func_info["handle"], i, param_info_ptr.ref)
544
+ TA_OutputParameterInfo.new(param_info_ptr)
545
+ end
546
+ end
547
+
548
+ def generate_ta_functions
549
+ each_function do |func_info|
550
+ define_ta_function(func_info["name"].to_s.downcase, func_info["name"].to_s)
551
+ end
552
+ end
553
+
554
+ def normalize_parameter_name(name)
555
+ name.sub(/^(optIn|outReal|outInteger|out)/, "")
556
+ .gsub(/::/, "/")
557
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
558
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
559
+ .tr("-", "_")
560
+ .downcase
561
+ end
562
+
563
+ # rubocop:disable Metrics/MethodLength
564
+ # rubocop:disable Metrics/AbcSize
565
+ # rubocop:disable Metrics/CyclomaticComplexity
566
+ def check_ta_return_code(code)
567
+ return if code == TA_SUCCESS
568
+
569
+ error_message = case code
570
+ when TA_LIB_NOT_INITIALIZE
571
+ "TA-Lib not initialized, please call TA_Initialize first"
572
+ when TA_BAD_PARAM
573
+ "Bad parameter, please check input parameters"
574
+ when TA_ALLOC_ERR
575
+ "Memory allocation error, possibly insufficient memory"
576
+ when TA_GROUP_NOT_FOUND
577
+ "Function group not found"
578
+ when TA_FUNC_NOT_FOUND
579
+ "Function not found"
580
+ when TA_INVALID_HANDLE
581
+ "Invalid handle"
582
+ when TA_INVALID_PARAM_HOLDER
583
+ "Invalid parameter holder"
584
+ when TA_INVALID_PARAM_HOLDER_TYPE
585
+ "Invalid parameter holder type"
586
+ when TA_INVALID_PARAM_FUNCTION
587
+ "Invalid parameter function"
588
+ when TA_INPUT_NOT_ALL_INITIALIZE
589
+ "Input parameters not fully initialized"
590
+ when TA_OUTPUT_NOT_ALL_INITIALIZE
591
+ "Output parameters not fully initialized"
592
+ when TA_OUT_OF_RANGE_START_INDEX
593
+ "Start index out of range"
594
+ when TA_OUT_OF_RANGE_END_INDEX
595
+ "End index out of range"
596
+ when TA_INVALID_LIST_TYPE
597
+ "Invalid list type"
598
+ when TA_BAD_OBJECT
599
+ "Invalid object"
600
+ when TA_NOT_SUPPORTED
601
+ "Operation not supported"
602
+ when TA_INTERNAL_ERROR
603
+ "TA-Lib internal error"
604
+ when TA_UNKNOWN_ERR
605
+ "Unknown error"
606
+ else
607
+ "Undefined TA-Lib error (Error code: #{code})"
608
+ end
609
+
610
+ raise TALibError, error_message
611
+ end
612
+ # rubocop:enable Metrics/CyclomaticComplexity
613
+ # rubocop:enable Metrics/MethodLength
614
+ # rubocop:enable Metrics/AbcSize
615
+
616
+ def initialize_ta_lib
617
+ return if @initialized
618
+
619
+ ret_code = TA_Initialize()
620
+ check_ta_return_code(ret_code)
621
+ at_exit { TA_Shutdown() }
622
+ @initialized = true
623
+ end
624
+
625
+ def define_ta_function(method_name, func_name)
626
+ define_singleton_method(method_name) do |*args|
627
+ call_func(func_name, args)
628
+ end
629
+ end
630
+
631
+ def setup_price_inputs(params_ptr, index, price_data, flags)
632
+ required_flags = extract_flags(flags, :TA_InputFlags)
633
+ data_pointers = Array.new(6) { nil }
634
+ TA_FLAGS[:TA_InputFlags].keys[0..5].each_with_index do |flag, i|
635
+ data_pointers[i] = if required_flags.include?(flag)
636
+ prepare_double_array(price_data[required_flags.index(flag)])
637
+ else
638
+ Fiddle::Pointer.malloc(0)
639
+ end
640
+ end
641
+
642
+ TA_SetInputParamPricePtr(params_ptr, index, *data_pointers)
643
+ end
644
+
645
+ initialize_ta_lib
646
+ generate_ta_functions
647
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ta_lib"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ta_lib_ffi"
7
+ spec.version = TALib::VERSION
8
+ spec.authors = ["Victor Yang"]
9
+ spec.email = ["victor@rt4u.bid"]
10
+ spec.summary = "Ruby FFI bindings for TA-Lib (Technical Analysis Library)"
11
+ spec.description = "A Ruby wrapper for TA-Lib using FFI, providing technical analysis functions for financial market data"
12
+ spec.homepage = "https://github.com/Youngv/ta_lib_ffi"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.0.0"
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/Youngv/ta_lib_ffi"
18
+ spec.metadata["changelog_uri"] = "https://github.com/Youngv/ta_lib_ffi/blob/main/CHANGELOG.md"
19
+ spec.files = Dir[
20
+ "lib/**/*",
21
+ "LICENSE.txt",
22
+ "README.md",
23
+ "CHANGELOG.md",
24
+ "Gemfile",
25
+ "Rakefile",
26
+ "ta_lib_ffi.gemspec",
27
+ ]
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_dependency "fiddle", "~> 1.1"
33
+ spec.add_development_dependency "rake", "~> 13.2"
34
+ spec.add_development_dependency "rspec", "~> 3.13"
35
+ spec.add_development_dependency "rubocop-rspec", "~> 3.3"
36
+ spec.add_development_dependency "ruby-lsp-rspec", "~> 0.1.20"
37
+
38
+ # For more information and examples about making a new gem, check out our
39
+ # guide at: https://bundler.io/guides/creating_gem.html
40
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ta_lib_ffi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Victor Yang
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-01-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fiddle
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: ruby-lsp-rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.1.20
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.1.20
83
+ description: A Ruby wrapper for TA-Lib using FFI, providing technical analysis functions
84
+ for financial market data
85
+ email:
86
+ - victor@rt4u.bid
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - CHANGELOG.md
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - lib/ta_lib.rb
97
+ - ta_lib_ffi.gemspec
98
+ homepage: https://github.com/Youngv/ta_lib_ffi
99
+ licenses:
100
+ - MIT
101
+ metadata:
102
+ allowed_push_host: https://rubygems.org
103
+ homepage_uri: https://github.com/Youngv/ta_lib_ffi
104
+ source_code_uri: https://github.com/Youngv/ta_lib_ffi
105
+ changelog_uri: https://github.com/Youngv/ta_lib_ffi/blob/main/CHANGELOG.md
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: 3.0.0
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 3.4.19
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Ruby FFI bindings for TA-Lib (Technical Analysis Library)
125
+ test_files: []