Skip to content

Commit d553107

Browse files
authored
Merge pull request #2 from Youngv/dev
Enhance TA-Lib library and update configuration
2 parents eba7cff + 7a105dc commit d553107

5 files changed

Lines changed: 166 additions & 301 deletions

File tree

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ Layout/LineLength:
1515
RSpec/MultipleExpectations:
1616
Enabled: false
1717

18+
Metrics/ModuleLength:
19+
Enabled: false
20+
1821
require: rubocop-rspec

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# TALib
22

3-
TODO: Delete this and the text below, and describe your gem
3+
![Tests](https://github.com/Youngv/ta_lib_ffi/actions/workflows/main.yml/badge.svg)
44

5-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ta_lib`. To experiment with that code, run `bin/console` for an interactive prompt.
5+
Ruby FFI wrapper for TA-Lib (Technical Analysis Library)
66

77
## Installation
88

lib/ta_lib.rb

Lines changed: 74 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
# frozen_string_literal: true
2+
13
require "fiddle"
24
require "fiddle/import"
35

6+
# Ruby FFI wrapper for TA-Lib (Technical Analysis Library)
47
module TALib
5-
VERSION = "0.1.0".freeze
8+
VERSION = "0.1.0"
69

710
extend Fiddle::Importer
811

@@ -12,7 +15,7 @@ module TALib
1215
"#{brew_prefix}/lib/libta-lib.dylib"
1316
when /linux/
1417
"libta-lib.so"
15-
when /win32|mingw32/
18+
when /cygwin|mswin|mingw|bccwin|wince|emx/
1619
"C:/Program Files/TA-Lib/bin/ta-lib.dll"
1720
else
1821
raise "Unsupported platform"
@@ -42,6 +45,16 @@ class TALibError < StandardError; end
4245
TA_INTERNAL_ERROR = 5000
4346
TA_UNKNOWN_ERR = 0xFFFF
4447

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+
4558
typealias "TA_Real", "double"
4659
typealias "TA_Integer", "int"
4760
typealias "TA_RetCode", "int"
@@ -100,21 +113,21 @@ class TALibError < StandardError; end
100113
"TA_OutputFlags flags"
101114
]
102115

103-
TA_Input_Price = 0
104-
TA_Input_Real = 1
105-
TA_Input_Integer = 2
106-
107-
TA_OptInput_RealRange = 0
108-
TA_OptInput_RealList = 1
109-
TA_OptInput_IntegerRange = 2
110-
TA_OptInput_IntegerList = 3
111-
112-
TA_Output_Real = 0
113-
TA_Output_Integer = 1
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
114127

115128
TA_FLAGS = {
116129
TA_InputFlags: {
117-
TA_InputFlags: 0x00000001,
130+
TA_IN_PRICE_OPEN: 0x00000001,
118131
TA_IN_PRICE_HIGH: 0x00000002,
119132
TA_IN_PRICE_LOW: 0x00000004,
120133
TA_IN_PRICE_CLOSE: 0x00000008,
@@ -143,9 +156,8 @@ class TALibError < StandardError; end
143156
TA_OUT_UPPER_LIMIT: 0x00000800,
144157
TA_OUT_LOWER_LIMIT: 0x00001000
145158
}
146-
}
159+
}.freeze
147160

148-
extern "const char *TA_GetVersionString(void)"
149161
extern "int TA_Initialize()"
150162
extern "int TA_Shutdown()"
151163
extern "int TA_GroupTableAlloc(TA_StringTable**)"
@@ -192,10 +204,6 @@ def extract_flags(value, type)
192204
flags_set
193205
end
194206

195-
def ta_lib_version
196-
TA_GetVersionString().to_s
197-
end
198-
199207
def group_table
200208
string_table_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
201209
ret_code = TA_GroupTableAlloc(string_table_ptr.ref)
@@ -246,6 +254,8 @@ def each_function(&block)
246254
check_ta_return_code(ret_code)
247255
end
248256

257+
# rubocop:disable Metrics/MethodLength
258+
# rubocop:disable Metrics/AbcSize
249259
def print_function_info(func_info)
250260
puts "Function Name: #{func_info["name"]}"
251261
puts "Function Group: #{func_info["group"]}"
@@ -263,7 +273,7 @@ def print_function_info(func_info)
263273
ret_code = TA_GetInputParameterInfo(func_info["handle"], i, param_info_ptr.ref)
264274
check_ta_return_code(ret_code)
265275
param_info = TA_InputParameterInfo.new(param_info_ptr)
266-
puts " Parameter #{i+1}:"
276+
puts " Parameter #{i + 1}:"
267277
puts " Name: #{param_info["paramName"]}"
268278
puts " Type: #{param_info["type"]}"
269279
puts " Flags: #{extract_flags(param_info["flags"], :TA_InputFlags)}"
@@ -296,7 +306,10 @@ def print_function_info(func_info)
296306
puts " Flags: #{extract_flags(param_info["flags"], :TA_OutputFlags)}"
297307
end
298308
end
309+
# rubocop:enable Metrics/MethodLength
310+
# rubocop:enable Metrics/AbcSize
299311

312+
# rubocop:disable Metrics/MethodLength
300313
def call_func(func_name, args)
301314
options = args.last.is_a?(Hash) ? args.pop : {}
302315
input_arrays = args
@@ -316,6 +329,7 @@ def call_func(func_name, args)
316329
TA_ParamHolderFree(params_ptr)
317330
end
318331
end
332+
# rubocop:enable Metrics/MethodLength
319333

320334
def calculate_lookback(params_ptr)
321335
lookback_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
@@ -324,6 +338,8 @@ def calculate_lookback(params_ptr)
324338
lookback_ptr[0, Fiddle::SIZEOF_INT].unpack1("l")
325339
end
326340

341+
# rubocop:disable Metrics/CyclomaticComplexity
342+
# rubocop:disable Metrics/PerceivedComplexity
327343
def validate_inputs!(arrays)
328344
raise TALibError, "Input arrays cannot be empty" if arrays.empty?
329345

@@ -332,13 +348,14 @@ def validate_inputs!(arrays)
332348
end
333349

334350
sizes = arrays.map(&:length)
335-
raise TALibError, "All input arrays must have the same length" unless sizes.uniq.length == 1
336-
raise TALibError, "Input arrays cannot be empty" if sizes.first.zero?
351+
raise TALibError, "Input arrays cannot be empty" if sizes.any?(&:zero?)
337352

338353
arrays.each do |arr|
339354
raise TALibError, "Input arrays must contain only numbers" unless arr.flatten.all? { |x| x.is_a?(Numeric) }
340355
end
341356
end
357+
# rubocop:enable Metrics/CyclomaticComplexity
358+
# rubocop:enable Metrics/PerceivedComplexity
342359

343360
def get_function_handle(func_name)
344361
handle_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
@@ -365,13 +382,13 @@ def setup_input_parameters(params_ptr, input_arrays, func_name)
365382

366383
def set_input_parameter(params_ptr, index, array, input_info)
367384
case input_info["type"]
368-
when TA_Input_Real
385+
when TA_PARAM_TYPE[:TA_Input_Real]
369386
input_ptr = prepare_double_array(array)
370387
TA_SetInputParamRealPtr(params_ptr, index, input_ptr)
371-
when TA_Input_Integer
388+
when TA_PARAM_TYPE[:TA_Input_Integer]
372389
input_ptr = prepare_integer_array(array)
373390
TA_SetInputParamIntegerPtr(params_ptr, index, input_ptr)
374-
when TA_Input_Price
391+
when TA_PARAM_TYPE[:TA_Input_Price]
375392
setup_price_inputs(params_ptr, index, array, input_info["flags"])
376393
end
377394
end
@@ -402,14 +419,15 @@ def setup_optional_parameters(params_ptr, options, func_name)
402419

403420
def set_optional_parameter(params_ptr, index, value, type)
404421
case type
405-
when TA_OptInput_RealRange, TA_OptInput_RealList
422+
when TA_PARAM_TYPE[:TA_OptInput_RealRange], TA_PARAM_TYPE[:TA_OptInput_RealList]
406423
ret_code = TA_SetOptInputParamReal(params_ptr, index, value)
407-
when TA_OptInput_IntegerRange, TA_OptInput_IntegerList
424+
when TA_PARAM_TYPE[:TA_OptInput_IntegerRange], TA_PARAM_TYPE[:TA_OptInput_IntegerList]
408425
ret_code = TA_SetOptInputParamInteger(params_ptr, index, value)
409426
end
410427
check_ta_return_code(ret_code)
411428
end
412429

430+
# rubocop:disable Metrics/MethodLength
413431
def calculate_results(params_ptr, input_size, func_name)
414432
out_begin = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
415433
out_size = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
@@ -420,32 +438,36 @@ def calculate_results(params_ptr, input_size, func_name)
420438
check_ta_return_code(ret_code)
421439

422440
actual_size = out_size[0, Fiddle::SIZEOF_INT].unpack1("l")
441+
puts "actual_size: #{actual_size}"
423442
format_output_results(output_arrays, actual_size, func_name)
424443
ensure
425444
out_begin.free
426445
out_size.free
427446
output_arrays.each(&:free)
428447
end
429448
end
449+
# rubocop:enable Metrics/MethodLength
430450

451+
# rubocop:disable Metrics/MethodLength
452+
# rubocop:disable Metrics/AbcSize
431453
def setup_output_buffers(params_ptr, size, func_name)
432454
func_info = function_info_map[func_name]
433455
output_ptrs = []
434456

435457
func_info[:outputs].each_with_index do |output, index|
436458
ptr = case output["type"]
437-
when TA_Output_Real
459+
when TA_PARAM_TYPE[:TA_Output_Real]
438460
Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE * size)
439-
when TA_Output_Integer
461+
when TA_PARAM_TYPE[:TA_Output_Integer]
440462
Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT * size)
441463
end
442464

443465
output_ptrs << ptr
444466

445467
ret_code = case output["type"]
446-
when TA_Output_Real
468+
when TA_PARAM_TYPE[:TA_Output_Real]
447469
TA_SetOutputParamRealPtr(params_ptr, index, ptr)
448-
when TA_Output_Integer
470+
when TA_PARAM_TYPE[:TA_Output_Integer]
449471
TA_SetOutputParamIntegerPtr(params_ptr, index, ptr)
450472
end
451473

@@ -454,14 +476,18 @@ def setup_output_buffers(params_ptr, size, func_name)
454476

455477
output_ptrs
456478
end
479+
# rubocop:enable Metrics/MethodLength
480+
# rubocop:enable Metrics/AbcSize
457481

482+
# rubocop:disable Metrics/MethodLength
483+
# rubocop:disable Metrics/AbcSize
458484
def format_output_results(output_ptrs, size, func_name)
459485
func_info = function_info_map[func_name]
460486
results = output_ptrs.zip(func_info[:outputs]).map do |ptr, output|
461487
case output["type"]
462-
when TA_Output_Real
488+
when TA_PARAM_TYPE[:TA_Output_Real]
463489
ptr[0, Fiddle::SIZEOF_DOUBLE * size].unpack("d#{size}")
464-
when TA_Output_Integer
490+
when TA_PARAM_TYPE[:TA_Output_Integer]
465491
ptr[0, Fiddle::SIZEOF_INT * size].unpack("l#{size}")
466492
end
467493
end
@@ -473,6 +499,8 @@ def format_output_results(output_ptrs, size, func_name)
473499
end
474500
output_names.zip(results).to_h
475501
end
502+
# rubocop:enable Metrics/AbcSize
503+
# rubocop:enable Metrics/MethodLength
476504

477505
def function_description_xml
478506
TA_FunctionDescriptionXML().to_s
@@ -534,6 +562,9 @@ def normalize_parameter_name(name)
534562
.downcase
535563
end
536564

565+
# rubocop:disable Metrics/MethodLength
566+
# rubocop:disable Metrics/AbcSize
567+
# rubocop:disable Metrics/CyclomaticComplexity
537568
def check_ta_return_code(code)
538569
return if code == TA_SUCCESS
539570

@@ -580,6 +611,9 @@ def check_ta_return_code(code)
580611

581612
raise TALibError, error_message
582613
end
614+
# rubocop:enable Metrics/CyclomaticComplexity
615+
# rubocop:enable Metrics/MethodLength
616+
# rubocop:enable Metrics/AbcSize
583617

584618
def initialize_ta_lib
585619
return if @initialized
@@ -598,23 +632,13 @@ def define_ta_function(method_name, func_name)
598632

599633
def setup_price_inputs(params_ptr, index, price_data, flags)
600634
required_flags = extract_flags(flags, :TA_InputFlags)
601-
flag_to_index = {
602-
TA_IN_PRICE_OPEN: 0,
603-
TA_IN_PRICE_HIGH: 1,
604-
TA_IN_PRICE_LOW: 2,
605-
TA_IN_PRICE_CLOSE: 3,
606-
TA_IN_PRICE_VOLUME: 4,
607-
TA_IN_PRICE_OPENINTEREST: 5
608-
}
609-
610635
data_pointers = Array.new(6) { nil }
611-
612-
flag_to_index.each_key do |flag|
613-
data_pointers[flag_to_index[flag]] = if required_flags.include?(flag)
614-
prepare_double_array(price_data[required_flags.index(flag)])
615-
else
616-
Fiddle::Pointer.malloc(0)
617-
end
636+
TA_FLAGS[:TA_InputFlags].keys[0..5].each_with_index do |flag, i|
637+
data_pointers[i] = if required_flags.include?(flag)
638+
prepare_double_array(price_data[required_flags.index(flag)])
639+
else
640+
Fiddle::Pointer.malloc(0)
641+
end
618642
end
619643

620644
TA_SetInputParamPricePtr(params_ptr, index, *data_pointers)
@@ -623,50 +647,3 @@ def setup_price_inputs(params_ptr, index, price_data, flags)
623647
initialize_ta_lib
624648
generate_ta_functions
625649
end
626-
627-
# puts TALib.group_table
628-
# puts TALib.function_table("Math Operators")
629-
# puts TALib.function_table("Math Transform")
630-
# puts TALib.function_info(TALib.function_info("BBANDS"))
631-
# TALib.each_function do |func_info|
632-
# # puts func_info[]
633-
# end
634-
# prices = [10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0]
635-
# result = TALib.call_func("SMA", [prices, { time_period: 5 }])
636-
# puts "开始索引: #{result[:begin_idx]}"
637-
# puts "SMA结果: #{result[:data].inspect}"
638-
# puts TALib.function_description_xml
639-
# puts TALib.function_info_map
640-
# 计算简单移动平均线
641-
# prices = [10.0, 11.0, 12.0, 13.0, 14.0, 15.0]
642-
# sma = TALib.bbands(prices, time_period: 3)
643-
# puts "SMA: #{sma}"
644-
645-
# # # 计算 MACD
646-
# puts TALib.print_function_info(TALib.function_info("MACD"))
647-
# macd = TALib.macd(prices,
648-
# fast_period: 3,
649-
# slow_period: 2,
650-
# signal_period: 1)
651-
# puts "MACD: #{macd}"
652-
653-
# puts TALib.print_function_info(TALib.function_info("EMA"))
654-
# prices = Array.new(100) { rand(100) }
655-
# # 调用 EMA
656-
657-
# prices = [1, 1, 1, 1, 1, 2, 3, 4, 5, 5, 5, 5, 5, 5]
658-
# puts TALib.ema(prices, time_period: 5)
659-
660-
# require "tulirb"
661-
# puts Tulirb.ema([prices], period: 5)
662-
663-
# puts ema2 == ema1[-ema1.size..-1]
664-
# puts ema2.join(",")
665-
# puts ema1.join(",")
666-
667-
# prices = Array.new(100) { rand(100) }
668-
# puts TALib.print_function_info(TALib.function_info("MA"))
669-
# puts TALib.sma(prices, time_period: 5).join(",")
670-
# puts "XXXXXXXXXXXXXX"
671-
# puts Tulirb.sma([prices], period: 5).join(",")
672-
# puts TALib.sma(prices, time_period: 5).map { |x| x.round(2) } == Tulirb.sma([prices], period: 5).first.map { |x| x.round(2) }

0 commit comments

Comments
 (0)