eodhd.rb 0.14.2 → 0.16.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 +4 -4
- data/eodhd.rb.gemspec +13 -2
- data/lib/Eodhd/Client.rb +37 -36
- data/lib/Eodhd/DefaultLogger.rb +30 -0
- data/lib/Eodhd/VERSION.rb +3 -0
- data/lib/Eodhd/Validations.rb +69 -0
- data/lib/Eodhd/WebSocketClient.rb +25 -40
- data/lib/{eodhd.rb → Eodhd.rb} +6 -26
- data/test/client_test.rb +71 -0
- data/test/eodhd_test.rb +50 -0
- data/test/helper.rb +14 -0
- data/test/test_all.rb +5 -0
- data/test/validations_test.rb +118 -0
- data/test/web_socket_client_test.rb +97 -0
- metadata +111 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1400c119e7a404b5100e55d51234c2bde712e9227454799556191bcb96bc2923
|
4
|
+
data.tar.gz: ed91c5bc5a273e50f5462780c55529dd26bb5a98fa318747ccbb390b5d4b2228
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e13b25610267151935b2e57d2367ed3274ba327d3edcee569b16ad5ac2a956671cebbf81e4146ff727a1b36522d53c6005e1c14c4ffbf4ce49342bf11f70878
|
7
|
+
data.tar.gz: 03e6404cc5fc0e7e58758dbb4086deb5039fd9807f3c26e350c4b7f33cbb4cf9e363e123b94f37ea394a5ee973e35616b83c1bbd426403e3e8de1fc9cee64d5f
|
data/eodhd.rb.gemspec
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
require_relative './lib/Eodhd/VERSION'
|
2
|
+
|
1
3
|
Gem::Specification.new do |spec|
|
2
4
|
spec.name = 'eodhd.rb'
|
3
5
|
|
4
|
-
spec.version =
|
5
|
-
spec.date = '2025-
|
6
|
+
spec.version = Eodhd::VERSION
|
7
|
+
spec.date = '2025-10-05'
|
6
8
|
|
7
9
|
spec.summary = "Access the eodhd.com API with Ruby."
|
8
10
|
spec.description = "Access the eodhd.com API with Ruby."
|
@@ -15,6 +17,15 @@ Gem::Specification.new do |spec|
|
|
15
17
|
spec.required_ruby_version = '>= 2.5'
|
16
18
|
|
17
19
|
spec.add_dependency('http.rb')
|
20
|
+
spec.add_dependency('iodine')
|
21
|
+
|
22
|
+
spec.add_development_dependency('rake')
|
23
|
+
spec.add_development_dependency('minitest')
|
24
|
+
spec.add_development_dependency('minitest-spec-context')
|
25
|
+
spec.add_development_dependency('webmock')
|
26
|
+
spec.add_development_dependency('vcr')
|
27
|
+
spec.add_development_dependency('simplecov')
|
28
|
+
|
18
29
|
spec.files = [
|
19
30
|
'eodhd.rb.gemspec',
|
20
31
|
'Gemfile',
|
data/lib/Eodhd/Client.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
# Eodhd/Client.rb
|
2
2
|
# Eodhd::Client
|
3
3
|
|
4
|
-
require 'fileutils'
|
5
4
|
gem 'http.rb'
|
5
|
+
|
6
6
|
require 'http.rb'
|
7
7
|
require 'json'
|
8
|
-
require 'logger'
|
9
8
|
|
9
|
+
require_relative './DefaultLogger'
|
10
10
|
require_relative './Error'
|
11
11
|
require_relative '../Hash/x_www_form_urlencode'
|
12
|
+
require_relative './Validations'
|
12
13
|
|
13
14
|
class Eodhd
|
14
15
|
class Client
|
@@ -16,30 +17,13 @@ class Eodhd
|
|
16
17
|
API_HOST = 'eodhd.com'
|
17
18
|
|
18
19
|
class << self
|
19
|
-
attr_writer :log_file_path
|
20
|
-
|
21
20
|
def path_prefix
|
22
21
|
'/api'
|
23
22
|
end
|
24
|
-
|
25
|
-
def default_log_file_path
|
26
|
-
File.join(%w{~ log eodhd log.txt})
|
27
|
-
end
|
28
|
-
|
29
|
-
def log_file_path
|
30
|
-
File.expand_path(@log_file_path || default_log_file_path)
|
31
|
-
end
|
32
|
-
|
33
|
-
def log_file
|
34
|
-
FileUtils.mkdir_p(File.dirname(log_file_path))
|
35
|
-
File.open(log_file_path, File::WRONLY | File::APPEND | File::CREAT)
|
36
|
-
end
|
37
|
-
|
38
|
-
def logger
|
39
|
-
@logger ||= Logger.new(log_file, 'daily')
|
40
|
-
end
|
41
23
|
end # class << self
|
42
24
|
|
25
|
+
include Validations
|
26
|
+
|
43
27
|
# This endpoint always returns json regardless of what fmt is specified.
|
44
28
|
def exchanges_list
|
45
29
|
response = get(path: '/exchanges-list')
|
@@ -47,11 +31,19 @@ class Eodhd
|
|
47
31
|
end
|
48
32
|
|
49
33
|
def exchange_symbol_list(exchange_code:)
|
34
|
+
validate_arguments(exchange_code: exchange_code)
|
50
35
|
response = get(path: "/exchange-symbol-list/#{exchange_code}")
|
51
36
|
handle_response(response)
|
52
37
|
end
|
53
38
|
|
54
39
|
def eod_data(exchange_id:, symbol:, period:, from: nil, to: nil)
|
40
|
+
validate_arguments(
|
41
|
+
exchange_id: exchange_id,
|
42
|
+
symbol: symbol,
|
43
|
+
period: period,
|
44
|
+
from: from,
|
45
|
+
to: to
|
46
|
+
)
|
55
47
|
args = {period: period}
|
56
48
|
args.merge!(from: from) if from
|
57
49
|
args.merge!(to: to) if to
|
@@ -60,15 +52,21 @@ class Eodhd
|
|
60
52
|
end
|
61
53
|
|
62
54
|
def eod_bulk_last_day(exchange_id:, date:)
|
55
|
+
validate_arguments(exchange_id: exchange_id, date: date)
|
63
56
|
args = {date: date}
|
64
57
|
response = get(path: "/eod-bulk-last-day/#{exchange_id}", args: args)
|
65
58
|
handle_response(response)
|
66
59
|
end
|
67
60
|
|
61
|
+
attr_accessor\
|
62
|
+
:api_token,
|
63
|
+
:logger
|
64
|
+
|
68
65
|
private
|
69
66
|
|
70
|
-
def initialize(api_token:)
|
67
|
+
def initialize(api_token: nil, logger: nil, use_default_logger: false)
|
71
68
|
@api_token = api_token
|
69
|
+
@logger = use_default_logger ? DefaultLogger.logger : logger
|
72
70
|
end
|
73
71
|
|
74
72
|
def request_string(path)
|
@@ -84,19 +82,24 @@ class Eodhd
|
|
84
82
|
if log_args?(args)
|
85
83
|
log_string << "?#{args.x_www_form_urlencode}"
|
86
84
|
end
|
87
|
-
|
85
|
+
@logger.info(log_string)
|
86
|
+
end
|
87
|
+
|
88
|
+
def log_response(code:, message:, body:)
|
89
|
+
log_string = "#{code}\n#{message}\n#{body}"
|
90
|
+
@logger.info(log_string)
|
88
91
|
end
|
89
92
|
|
90
93
|
def log_error(code:, message:, body:)
|
91
94
|
log_string = "#{code}\n#{message}\n#{body}"
|
92
|
-
|
95
|
+
@logger.error(log_string)
|
93
96
|
end
|
94
97
|
|
95
98
|
def do_request(verb:, path:, args: {})
|
96
|
-
log_request(verb: verb, request_string: request_string(path), args: args)
|
97
99
|
api_token = args[:api_token] || @api_token
|
98
100
|
fmt = args[:fmt] || 'json'
|
99
101
|
args.merge!(api_token: api_token, fmt: fmt)
|
102
|
+
log_request(verb: verb, request_string: request_string(path), args: args) if use_logging?
|
100
103
|
HTTP.send(verb.to_s.downcase, request_string(path), args)
|
101
104
|
end
|
102
105
|
|
@@ -106,19 +109,17 @@ class Eodhd
|
|
106
109
|
|
107
110
|
def handle_response(response)
|
108
111
|
if response.success?
|
109
|
-
JSON.parse(response.body)
|
112
|
+
parsed_body = JSON.parse(response.body)
|
113
|
+
log_response(code: response.code, message: response.message, body: response.body) if use_logging?
|
114
|
+
parsed_body
|
110
115
|
else
|
111
|
-
log_error(
|
112
|
-
|
113
|
-
message: response.message,
|
114
|
-
body: response.body
|
115
|
-
)
|
116
|
-
raise Eodhd::Error.new(
|
117
|
-
code: response.code,
|
118
|
-
message: response.message,
|
119
|
-
body: response.body
|
120
|
-
)
|
116
|
+
log_error(code: response.code, message: response.message, body: response.body) if use_logging?
|
117
|
+
raise Eodhd::Error.new(code: response.code, message: response.message, body: response.body)
|
121
118
|
end
|
122
119
|
end
|
120
|
+
|
121
|
+
def use_logging?
|
122
|
+
!@logger.nil?
|
123
|
+
end
|
123
124
|
end
|
124
125
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Eodhd/DefaultLogger.rb
|
2
|
+
# Eodhd::DefaultLogger
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
class Eodhd
|
8
|
+
class DefaultLogger
|
9
|
+
class << self
|
10
|
+
attr_writer :log_filepath
|
11
|
+
|
12
|
+
def default_log_filepath
|
13
|
+
File.join(%w{~ log eodhd log.txt})
|
14
|
+
end
|
15
|
+
|
16
|
+
def log_filepath
|
17
|
+
File.expand_path(@log_filepath || default_log_filepath)
|
18
|
+
end
|
19
|
+
|
20
|
+
def log_file
|
21
|
+
FileUtils.mkdir_p(File.dirname(log_filepath))
|
22
|
+
File.open(log_filepath, File::WRONLY | File::APPEND | File::CREAT)
|
23
|
+
end
|
24
|
+
|
25
|
+
def logger
|
26
|
+
@logger ||= Logger.new(log_file, 'daily')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# Eodhd/Validations.rb
|
2
|
+
# Eodhd::Validations
|
3
|
+
|
4
|
+
class Eodhd
|
5
|
+
module Validations
|
6
|
+
def validate_arguments(exchange_code: nil, exchange_id: nil, symbol: nil, period: nil, from: nil, to: nil, date: nil)
|
7
|
+
exchange_code ||= exchange_id
|
8
|
+
validate_exchange_code(exchange_code)
|
9
|
+
validate_symbol(symbol)
|
10
|
+
validate_period(period)
|
11
|
+
validate_date(from, 'from')
|
12
|
+
validate_date(to, 'to')
|
13
|
+
validate_date_range(from, to)
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate_exchange_code(exchange_code)
|
17
|
+
return unless exchange_code
|
18
|
+
unless exchange_code.match?(/\A[A-Z]{2,6}\z/)
|
19
|
+
raise ArgumentError, "Invalid exchange_code '#{exchange_code}'. Must be 2-6 uppercase letters"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_symbol(symbol)
|
24
|
+
return unless symbol
|
25
|
+
if symbol.strip.empty?
|
26
|
+
raise ArgumentError, "Symbol cannot be empty"
|
27
|
+
end
|
28
|
+
unless symbol.match?(/\A[A-Za-z0-9.-]{1,12}\z/)
|
29
|
+
raise ArgumentError, "Invalid symbol '#{symbol}'. Must be 1-12 characters, letters/numbers/dots/hyphens only"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_period(period)
|
34
|
+
return unless period
|
35
|
+
valid_periods = %w[d w m]
|
36
|
+
unless valid_periods.include?(period)
|
37
|
+
raise ArgumentError, "Invalid period '#{period}'. Must be one of: #{valid_periods.join(', ')}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_date(date, param_name = 'date')
|
42
|
+
return unless date
|
43
|
+
case date
|
44
|
+
when Date
|
45
|
+
return
|
46
|
+
when String
|
47
|
+
unless date.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
48
|
+
raise ArgumentError, "Invalid #{param_name} '#{date}'. Must be in format 'yyyy-mm-dd'"
|
49
|
+
end
|
50
|
+
begin
|
51
|
+
Date.parse(date)
|
52
|
+
rescue Date::Error
|
53
|
+
raise ArgumentError, "Invalid #{param_name} '#{date}'. Not a valid date"
|
54
|
+
end
|
55
|
+
else
|
56
|
+
raise ArgumentError, "Invalid #{param_name}. Must be String in 'yyyy-mm-dd' format or Date object"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_date_range(from, to)
|
61
|
+
return unless from && to
|
62
|
+
from_date = from.is_a?(Date) ? from : Date.parse(from.to_s)
|
63
|
+
to_date = to.is_a?(Date) ? to : Date.parse(to.to_s)
|
64
|
+
if from_date > to_date
|
65
|
+
raise ArgumentError, "from date (#{from}) cannot be after to date (#{to})"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -9,41 +9,23 @@
|
|
9
9
|
# iv. Crypto: wss://ws.eodhistoricaldata.com/ws/crypto?api_token=XXX
|
10
10
|
|
11
11
|
require 'iodine'
|
12
|
-
require 'json'
|
12
|
+
require 'json'
|
13
|
+
|
14
|
+
require_relative './DefaultLogger'
|
15
|
+
require_relative './Error'
|
13
16
|
|
14
17
|
class Eodhd
|
15
18
|
class WebSocketClient
|
16
19
|
API_HOST = 'ws.eodhistoricaldata.com'
|
17
20
|
|
18
21
|
class Handler
|
19
|
-
class << self
|
20
|
-
attr_writer :log_file_path
|
21
|
-
|
22
|
-
def default_log_file_path
|
23
|
-
File.join(%w{~ log eodhd log.txt})
|
24
|
-
end
|
25
|
-
|
26
|
-
def log_file_path
|
27
|
-
File.expand_path(@log_file_path || default_log_file_path)
|
28
|
-
end
|
29
|
-
|
30
|
-
def log_file
|
31
|
-
FileUtils.mkdir_p(File.dirname(log_file_path))
|
32
|
-
File.open(log_file_path, File::WRONLY | File::APPEND | File::CREAT)
|
33
|
-
end
|
34
|
-
|
35
|
-
def logger
|
36
|
-
@logger ||= Logger.new(log_file, 'daily')
|
37
|
-
end
|
38
|
-
end # class << self
|
39
|
-
|
40
22
|
def on_open(connection)
|
41
23
|
connection.write({action: 'subscribe', symbols: @symbols}.to_json)
|
42
24
|
end
|
43
25
|
|
44
26
|
def on_message(connection, message)
|
45
27
|
dataframe = handle_response(message)
|
46
|
-
@consumer.call(dataframe)
|
28
|
+
@consumer.call(dataframe) if @consumer
|
47
29
|
end
|
48
30
|
|
49
31
|
def on_close(connection)
|
@@ -52,29 +34,26 @@ class Eodhd
|
|
52
34
|
|
53
35
|
private
|
54
36
|
|
55
|
-
def initialize(symbols:, consumer:)
|
37
|
+
def initialize(symbols:, consumer:, logger: nil)
|
56
38
|
@symbols = symbols
|
57
39
|
@consumer = consumer
|
40
|
+
@logger = logger
|
58
41
|
end
|
59
42
|
|
60
43
|
def log_error(code:, message:, body:)
|
61
44
|
log_string = "WebSocketError #{code}\n#{message}\n#{body}"
|
62
|
-
|
45
|
+
@logger.error(log_string) if use_logging?
|
63
46
|
end
|
64
47
|
|
65
48
|
def handle_response(message)
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
message: response.message,
|
75
|
-
body: ''
|
76
|
-
)
|
77
|
-
end
|
49
|
+
JSON.parse(message)
|
50
|
+
rescue StandardError => e
|
51
|
+
log_error(code: 'ws_parse_error', message: e.message, body: message)
|
52
|
+
raise Eodhd::Error.new(code: 'ws_parse_error', message: e.message, body: message)
|
53
|
+
end
|
54
|
+
|
55
|
+
def use_logging?
|
56
|
+
!@logger.nil?
|
78
57
|
end
|
79
58
|
end
|
80
59
|
|
@@ -104,7 +83,8 @@ class Eodhd
|
|
104
83
|
attr_accessor\
|
105
84
|
:api_token,
|
106
85
|
:asset_class,
|
107
|
-
:consumer
|
86
|
+
:consumer,
|
87
|
+
:logger
|
108
88
|
|
109
89
|
attr_reader\
|
110
90
|
:symbols
|
@@ -115,7 +95,7 @@ class Eodhd
|
|
115
95
|
|
116
96
|
def run
|
117
97
|
Iodine.threads = 1
|
118
|
-
Iodine.connect(url: url, handler: Handler.new(symbols: @symbols, consumer: @consumer))
|
98
|
+
Iodine.connect(url: url, handler: Handler.new(symbols: @symbols, consumer: @consumer, logger: @logger))
|
119
99
|
Iodine.start
|
120
100
|
rescue SystemExit, Interrupt
|
121
101
|
return
|
@@ -123,11 +103,16 @@ class Eodhd
|
|
123
103
|
|
124
104
|
private
|
125
105
|
|
126
|
-
def initialize(api_token:, asset_class:, symbols:, consumer:)
|
106
|
+
def initialize(api_token:, asset_class:, symbols:, consumer:, logger: nil, use_default_logger: false)
|
127
107
|
@api_token = api_token
|
128
108
|
@asset_class = asset_class # crypto, forex, us-quote, us
|
129
109
|
@symbols = format_symbols(symbols)
|
130
110
|
@consumer = consumer
|
111
|
+
@logger = use_default_logger ? DefaultLogger.logger : logger
|
112
|
+
end
|
113
|
+
|
114
|
+
def use_logging?
|
115
|
+
!@logger.nil?
|
131
116
|
end
|
132
117
|
|
133
118
|
def format_symbols(symbols)
|
data/lib/{eodhd.rb → Eodhd.rb}
RENAMED
@@ -1,32 +1,12 @@
|
|
1
1
|
# Eodhd.rb
|
2
2
|
# Eodhd
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# 2. + Eodhd#web_socket
|
11
|
-
# 3. + Eodhd#stream
|
12
|
-
# 4. + Eodhd#us_quote_stream
|
13
|
-
# 5. + Eodhd#us_trade_stream
|
14
|
-
# 6. + Eodhd#forex_stream
|
15
|
-
# 7. + Eodhd#crypto_stream
|
16
|
-
# 8. Moved Eodhd::Error from Eodhd::Client to a separate file.
|
17
|
-
# 0/1: Some fixes for the WebSocketClient interface.
|
18
|
-
# 9. ~ Eodhd::WebSocketClient: + attr_accessor :consumer
|
19
|
-
# 10. ~ Eodhd::WebSocketClient: /attr_accessor :symbols/attr_reader :symbols/
|
20
|
-
# 11. + Eodhd::WebSocketClient#symbols=
|
21
|
-
# 1/2: Remove reference to currently unused library UnixTime.
|
22
|
-
# 12. ~ Eodhd::WebSocketClient: - require 'UnixTime'
|
23
|
-
|
24
|
-
require_relative 'Eodhd/Client'
|
25
|
-
require_relative 'Eodhd/EodBulkLastDay'
|
26
|
-
require_relative 'Eodhd/EodData'
|
27
|
-
require_relative 'Eodhd/Exchange'
|
28
|
-
require_relative 'Eodhd/ExchangeSymbol'
|
29
|
-
require_relative 'Eodhd/WebSocketClient'
|
4
|
+
require_relative './Eodhd/Client'
|
5
|
+
require_relative './Eodhd/EodBulkLastDay'
|
6
|
+
require_relative './Eodhd/EodData'
|
7
|
+
require_relative './Eodhd/Exchange'
|
8
|
+
require_relative './Eodhd/ExchangeSymbol'
|
9
|
+
require_relative './Eodhd/WebSocketClient'
|
30
10
|
|
31
11
|
class Eodhd
|
32
12
|
def initialize(api_token:, consumer: nil)
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require_relative "helper"
|
2
|
+
|
3
|
+
describe Eodhd::Client do
|
4
|
+
let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
|
5
|
+
|
6
|
+
let(:client){Eodhd::Client.new(api_token: api_token)}
|
7
|
+
|
8
|
+
describe "#exchanges_list" do
|
9
|
+
it "exchanges_list returns parsed JSON array" do
|
10
|
+
VCR.use_cassette("client_exchanges_list") do
|
11
|
+
result = client.exchanges_list
|
12
|
+
_(result).must_be_kind_of(Array)
|
13
|
+
_(result.first["Code"]).wont_be_nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it "raises Eodhd::Error on failure" do
|
18
|
+
VCR.use_cassette("client_exchanges_list_401_error") do
|
19
|
+
_{client.exchanges_list}.must_raise(Eodhd::Error)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#exchange_symbol_list" do
|
25
|
+
it "exchange_symbol_list returns parsed JSON array" do
|
26
|
+
VCR.use_cassette("client_exchange_symbol_list") do
|
27
|
+
result = client.exchange_symbol_list(exchange_code: 'AU')
|
28
|
+
_(result).must_be_kind_of(Array)
|
29
|
+
_(result.first['Code']).wont_be_nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "raises Eodhd::Error on failure" do
|
34
|
+
VCR.use_cassette("client_exchange_symbol_list_401_error") do
|
35
|
+
_{client.exchange_symbol_list(exchange_code: 'AU')}.must_raise(Eodhd::Error)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#eod_data" do
|
41
|
+
it "eod_data returns parsed JSON array" do
|
42
|
+
VCR.use_cassette('client_eod_data') do
|
43
|
+
result = client.eod_data(exchange_id: 'AU', symbol: 'BHP', period: 'd')
|
44
|
+
_(result).must_be_kind_of(Array)
|
45
|
+
_(result.first.keys).must_equal(%w{date open high low close adjusted_close volume})
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "raises Eodhd::Error on failure" do
|
50
|
+
VCR.use_cassette('client_eod_data_401_error') do
|
51
|
+
_{client.eod_data(exchange_id: 'AU', symbol: 'BHP', period: 'd')}.must_raise(Eodhd::Error)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#eod_bulk_last_day" do
|
57
|
+
it "eod_bulk_last_day returns parsed JSON array" do
|
58
|
+
VCR.use_cassette("client_eod_bulk_last_day") do
|
59
|
+
result = client.eod_bulk_last_day(exchange_id: 'AU', date: "2024-09-30")
|
60
|
+
_(result).must_be_kind_of(Array)
|
61
|
+
_(result.first.keys).must_equal(%w{code exchange_short_name date open high low close adjusted_close volume})
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it "raises Eodhd::Error on failure" do
|
66
|
+
VCR.use_cassette('client_eod_bulk_last_day_401_error') do
|
67
|
+
_{client.eod_bulk_last_day(exchange_id: 'AU', date: "2024-09-30")}.must_raise(Eodhd::Error)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/test/eodhd_test.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative "helper"
|
2
|
+
|
3
|
+
describe Eodhd do
|
4
|
+
let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
|
5
|
+
let(:eodhd){Eodhd.new(api_token: api_token)}
|
6
|
+
|
7
|
+
describe "#exchanges" do
|
8
|
+
it "delegates to Eodhd::Exchange.all" do
|
9
|
+
VCR.use_cassette('eodhd_exchanges') do
|
10
|
+
exchanges = eodhd.exchanges
|
11
|
+
_(exchanges).must_be_kind_of(Array)
|
12
|
+
_(exchanges).wont_be_empty
|
13
|
+
_(exchanges.first).must_be_kind_of(Eodhd::Exchange)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#exchange_symbols" do
|
19
|
+
it "delegates to Eodhd::ExchangeSymbol.all" do
|
20
|
+
VCR.use_cassette('eodhd_exchange_symbols') do
|
21
|
+
symbols = eodhd.exchange_symbols(exchange_code: 'AU')
|
22
|
+
_(symbols).must_be_kind_of(Array)
|
23
|
+
_(symbols).wont_be_empty
|
24
|
+
_(symbols.first).must_be_kind_of(Eodhd::ExchangeSymbol)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#eod_data" do
|
30
|
+
it "delegates to Eodhd::EodData.all" do
|
31
|
+
VCR.use_cassette('eodhd_eod_data') do
|
32
|
+
eod_data = eodhd.eod_data(exchange_code: 'AU', symbol: 'BHP', period: 'd')
|
33
|
+
_(eod_data).must_be_kind_of(Array)
|
34
|
+
_(eod_data).wont_be_empty
|
35
|
+
_(eod_data.first).must_be_kind_of(Eodhd::EodData)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#eod_bulk_last_day" do
|
41
|
+
it "delegates to Eodhd::EodBulkLastDay.all" do
|
42
|
+
VCR.use_cassette('eodhd_eod_bulk_last_day') do
|
43
|
+
bulk_last_day = eodhd.eod_bulk_last_day(exchange_code: 'AU', date: '2024-09-30')
|
44
|
+
_(bulk_last_day).must_be_kind_of(Array)
|
45
|
+
_(bulk_last_day).wont_be_empty
|
46
|
+
_(bulk_last_day.first).must_be_kind_of(Eodhd::EodBulkLastDay)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'minitest/spec'
|
3
|
+
require 'minitest-spec-context'
|
4
|
+
require 'vcr'
|
5
|
+
require 'webmock/minitest'
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
8
|
+
require 'Eodhd'
|
9
|
+
|
10
|
+
VCR.configure do |config|
|
11
|
+
config.cassette_library_dir = File.expand_path('./fixtures/vcr_cassettes', __dir__)
|
12
|
+
config.hook_into :webmock
|
13
|
+
config.filter_sensitive_data('<API_TOKEN>'){ENV['EODHD_API_TOKEN']}
|
14
|
+
end
|
data/test/test_all.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class DummyIncludingValidations
|
4
|
+
include Eodhd::Validations
|
5
|
+
end
|
6
|
+
|
7
|
+
describe "Eodhd::Validations" do
|
8
|
+
let(:v){DummyIncludingValidations.new}
|
9
|
+
|
10
|
+
describe "#validate_arguments" do
|
11
|
+
it "accepts all valid arguments" do
|
12
|
+
_{v.validate_arguments(
|
13
|
+
exchange_code: 'AU',
|
14
|
+
symbol: 'BHP',
|
15
|
+
period: 'd',
|
16
|
+
from: '2024-01-01',
|
17
|
+
to: '2024-12-31'
|
18
|
+
)}.must_be_silent
|
19
|
+
end
|
20
|
+
|
21
|
+
it "accepts exchange_id as alias for exchange_code" do
|
22
|
+
_{v.validate_arguments(exchange_id: 'AU')}.must_be_silent
|
23
|
+
end
|
24
|
+
|
25
|
+
it "raises ArgumentError for invalid exchange_code" do
|
26
|
+
_{v.validate_arguments(exchange_code: 'au')}.must_raise(ArgumentError)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "raises ArgumentError for invalid symbol" do
|
30
|
+
_{v.validate_arguments(symbol: 'bad$')}.must_raise(ArgumentError)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "raises ArgumentError for invalid period" do
|
34
|
+
_{v.validate_arguments(period: 'h')}.must_raise(ArgumentError)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "raises ArgumentError for invalid from date" do
|
38
|
+
_{v.validate_arguments(from: '2024/01/01')}.must_raise(ArgumentError)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "raises ArgumentError for invalid to date" do
|
42
|
+
_{v.validate_arguments(to: '2024-02-30')}.must_raise(ArgumentError)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "raises ArgumentError for reversed date range" do
|
46
|
+
_{v.validate_arguments(from: '2024-12-31', to: '2024-01-01')}.must_raise(ArgumentError)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "accepts nil values for all arguments" do
|
50
|
+
_{v.validate_arguments}.must_be_silent
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#validate_exchange_code" do
|
55
|
+
it "accepts valid exchange codes" do
|
56
|
+
_{v.validate_exchange_code('AU')}.must_be_silent
|
57
|
+
_{v.validate_exchange_code('XETRA')}.must_be_silent
|
58
|
+
end
|
59
|
+
|
60
|
+
it "rejects invalid exchange codes" do
|
61
|
+
_{v.validate_exchange_code('us')}.must_raise(ArgumentError)
|
62
|
+
_{v.validate_exchange_code('U')}.must_raise(ArgumentError)
|
63
|
+
_{v.validate_exchange_code('TOOLONG')}.must_raise(ArgumentError)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#validate_symbol" do
|
68
|
+
it "accepts valid symbols" do
|
69
|
+
_{v.validate_symbol('AAPL')}.must_be_silent
|
70
|
+
_{v.validate_symbol('BRK.B')}.must_be_silent
|
71
|
+
_{v.validate_symbol('RDS-A')}.must_be_silent
|
72
|
+
end
|
73
|
+
|
74
|
+
it "rejects invalid symbols" do
|
75
|
+
_{v.validate_symbol('')}.must_raise(ArgumentError)
|
76
|
+
_{v.validate_symbol(' ')}.must_raise(ArgumentError)
|
77
|
+
_{v.validate_symbol('THIS_SYMBOL_IS_TOO_LONG')}.must_raise(ArgumentError)
|
78
|
+
_{v.validate_symbol('bad$')}.must_raise(ArgumentError)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#validate_period" do
|
83
|
+
it "accepts valid periods" do
|
84
|
+
_{v.validate_period('d')}.must_be_silent
|
85
|
+
_{v.validate_period('w')}.must_be_silent
|
86
|
+
_{v.validate_period('m')}.must_be_silent
|
87
|
+
end
|
88
|
+
|
89
|
+
it "rejects invalid periods" do
|
90
|
+
_{v.validate_period('h')}.must_raise(ArgumentError)
|
91
|
+
_{v.validate_period('')}.must_raise(ArgumentError)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "#validate_date" do
|
96
|
+
it "accepts valid date formats and Date objects" do
|
97
|
+
_{v.validate_date('2024-09-30')}.must_be_silent
|
98
|
+
_{v.validate_date(Date.new(2024,9,30))}.must_be_silent
|
99
|
+
end
|
100
|
+
|
101
|
+
it "rejects invalid dates and formats" do
|
102
|
+
_{v.validate_date('2024/09/30')}.must_raise(ArgumentError)
|
103
|
+
_{v.validate_date('2024-02-30')}.must_raise(ArgumentError)
|
104
|
+
_{v.validate_date(12345)}.must_raise(ArgumentError)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "#validate_date_range" do
|
109
|
+
it "accepts valid date ranges" do
|
110
|
+
_{v.validate_date_range('2024-01-01','2024-12-31')}.must_be_silent
|
111
|
+
_{v.validate_date_range(Date.new(2024,1,1),Date.new(2024,12,31))}.must_be_silent
|
112
|
+
end
|
113
|
+
|
114
|
+
it "rejects reversed date ranges" do
|
115
|
+
_{v.validate_date_range('2024-12-31','2024-01-01')}.must_raise(ArgumentError)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
describe Eodhd::WebSocketClient do
|
4
|
+
let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
|
5
|
+
let(:consumer){->(data){}}
|
6
|
+
|
7
|
+
describe "#initialize" do
|
8
|
+
it "accepts array of symbols and formats to comma-separated string" do
|
9
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: ['AAPL', 'MSFT'], consumer: consumer)
|
10
|
+
_(client.symbols).must_equal('AAPL,MSFT')
|
11
|
+
end
|
12
|
+
|
13
|
+
it "accepts string of symbols" do
|
14
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL,MSFT', consumer: consumer)
|
15
|
+
_(client.symbols).must_equal('AAPL,MSFT')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#url" do
|
20
|
+
it "generates correct URL for US trade stream" do
|
21
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL', consumer: consumer)
|
22
|
+
_(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/us?api_token=#{api_token}")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "generates correct URL for US quote stream" do
|
26
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us-quote', symbols: 'AAPL', consumer: consumer)
|
27
|
+
_(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/us-quote?api_token=#{api_token}")
|
28
|
+
end
|
29
|
+
|
30
|
+
it "generates correct URL for forex stream" do
|
31
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'forex', symbols: 'EURUSD', consumer: consumer)
|
32
|
+
_(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/forex?api_token=#{api_token}")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "generates correct URL for crypto stream" do
|
36
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'crypto', symbols: 'BTC-USD', consumer: consumer)
|
37
|
+
_(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/crypto?api_token=#{api_token}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#format_symbols" do
|
42
|
+
it "converts array to comma-separated string" do
|
43
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: ['AAPL', 'MSFT', 'GOOGL'], consumer: consumer)
|
44
|
+
_(client.symbols).must_equal('AAPL,MSFT,GOOGL')
|
45
|
+
end
|
46
|
+
|
47
|
+
it "returns string as-is" do
|
48
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL,MSFT', consumer: consumer)
|
49
|
+
_(client.symbols).must_equal('AAPL,MSFT')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "#run" do
|
54
|
+
it "configures Iodine and starts connection" do
|
55
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL', consumer: consumer)
|
56
|
+
|
57
|
+
iodine_connect_called = false
|
58
|
+
iodine_start_called = false
|
59
|
+
|
60
|
+
Iodine.stub(:threads=, nil) do
|
61
|
+
Iodine.stub(:connect, ->(opts){iodine_connect_called = true}) do
|
62
|
+
Iodine.stub(:start, ->{iodine_start_called = true}) do
|
63
|
+
client.run
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
_(iodine_connect_called).must_equal(true)
|
69
|
+
_(iodine_start_called).must_equal(true)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "Handler" do
|
74
|
+
describe "#on_message" do
|
75
|
+
it "parses JSON and calls consumer" do
|
76
|
+
received_data = nil
|
77
|
+
test_consumer = ->(data){received_data = data}
|
78
|
+
handler = Eodhd::WebSocketClient::Handler.new(symbols: 'AAPL', consumer: test_consumer, logger: nil)
|
79
|
+
|
80
|
+
message = '{"symbol":"AAPL","price":150.0}'
|
81
|
+
handler.on_message(nil, message)
|
82
|
+
|
83
|
+
_(received_data).must_be_kind_of(Hash)
|
84
|
+
_(received_data['symbol']).must_equal('AAPL')
|
85
|
+
_(received_data['price']).must_equal(150.0)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "raises Eodhd::Error on invalid JSON" do
|
89
|
+
test_consumer = ->(data){}
|
90
|
+
handler = Eodhd::WebSocketClient::Handler.new(symbols: 'AAPL', consumer: test_consumer, logger: nil)
|
91
|
+
|
92
|
+
invalid_message = '{invalid json'
|
93
|
+
_{handler.on_message(nil, invalid_message)}.must_raise(Eodhd::Error)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: eodhd.rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.16.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- thoran
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-10-05 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: http.rb
|
@@ -23,6 +23,104 @@ dependencies:
|
|
23
23
|
- - ">="
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: iodine
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rake
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: minitest
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: minitest-spec-context
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: webmock
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: vcr
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: simplecov
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
26
124
|
description: Access the eodhd.com API with Ruby.
|
27
125
|
email: code@thoran.com
|
28
126
|
executables: []
|
@@ -32,17 +130,26 @@ files:
|
|
32
130
|
- Gemfile
|
33
131
|
- README.md
|
34
132
|
- eodhd.rb.gemspec
|
133
|
+
- lib/Eodhd.rb
|
35
134
|
- lib/Eodhd/Client.rb
|
135
|
+
- lib/Eodhd/DefaultLogger.rb
|
36
136
|
- lib/Eodhd/EodBulkLastDay.rb
|
37
137
|
- lib/Eodhd/EodData.rb
|
38
138
|
- lib/Eodhd/Error.rb
|
39
139
|
- lib/Eodhd/Exchange.rb
|
40
140
|
- lib/Eodhd/ExchangeSymbol.rb
|
141
|
+
- lib/Eodhd/VERSION.rb
|
142
|
+
- lib/Eodhd/Validations.rb
|
41
143
|
- lib/Eodhd/WebSocketClient.rb
|
42
144
|
- lib/Hash/x_www_form_urlencode.rb
|
43
145
|
- lib/Thoran/Hash/XWwwFormUrlencode/x_www_form_urlencode.rb
|
44
146
|
- lib/Thoran/String/UrlEncode/url_encode.rb
|
45
|
-
-
|
147
|
+
- test/client_test.rb
|
148
|
+
- test/eodhd_test.rb
|
149
|
+
- test/helper.rb
|
150
|
+
- test/test_all.rb
|
151
|
+
- test/validations_test.rb
|
152
|
+
- test/web_socket_client_test.rb
|
46
153
|
homepage: http://github.com/thoran/eodhd.rb
|
47
154
|
licenses:
|
48
155
|
- Ruby
|
@@ -61,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
168
|
- !ruby/object:Gem::Version
|
62
169
|
version: '0'
|
63
170
|
requirements: []
|
64
|
-
rubygems_version: 3.
|
171
|
+
rubygems_version: 3.7.2
|
65
172
|
specification_version: 4
|
66
173
|
summary: Access the eodhd.com API with Ruby.
|
67
174
|
test_files: []
|