eodhd.rb 0.15.0 → 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 +10 -1
- data/lib/Eodhd/Client.rb +19 -37
- data/lib/Eodhd/DefaultLogger.rb +30 -0
- data/lib/Eodhd/VERSION.rb +2 -2
- data/lib/Eodhd/Validations.rb +16 -6
- data/lib/Eodhd/WebSocketClient.rb +25 -40
- data/lib/{eodhd.rb → Eodhd.rb} +6 -6
- 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 +108 -3
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
@@ -4,7 +4,7 @@ Gem::Specification.new do |spec|
|
|
4
4
|
spec.name = 'eodhd.rb'
|
5
5
|
|
6
6
|
spec.version = Eodhd::VERSION
|
7
|
-
spec.date = '2025-
|
7
|
+
spec.date = '2025-10-05'
|
8
8
|
|
9
9
|
spec.summary = "Access the eodhd.com API with Ruby."
|
10
10
|
spec.description = "Access the eodhd.com API with Ruby."
|
@@ -17,6 +17,15 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.required_ruby_version = '>= 2.5'
|
18
18
|
|
19
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
|
+
|
20
29
|
spec.files = [
|
21
30
|
'eodhd.rb.gemspec',
|
22
31
|
'Gemfile',
|
data/lib/Eodhd/Client.rb
CHANGED
@@ -3,13 +3,13 @@
|
|
3
3
|
|
4
4
|
gem 'http.rb'
|
5
5
|
|
6
|
-
require 'fileutils'
|
7
6
|
require 'http.rb'
|
8
7
|
require 'json'
|
9
|
-
require 'logger'
|
10
8
|
|
9
|
+
require_relative './DefaultLogger'
|
11
10
|
require_relative './Error'
|
12
11
|
require_relative '../Hash/x_www_form_urlencode'
|
12
|
+
require_relative './Validations'
|
13
13
|
|
14
14
|
class Eodhd
|
15
15
|
class Client
|
@@ -17,30 +17,13 @@ class Eodhd
|
|
17
17
|
API_HOST = 'eodhd.com'
|
18
18
|
|
19
19
|
class << self
|
20
|
-
attr_writer :log_file_path
|
21
|
-
|
22
20
|
def path_prefix
|
23
21
|
'/api'
|
24
22
|
end
|
25
|
-
|
26
|
-
def default_log_file_path
|
27
|
-
File.join(%w{~ log eodhd log.txt})
|
28
|
-
end
|
29
|
-
|
30
|
-
def log_file_path
|
31
|
-
File.expand_path(@log_file_path || default_log_file_path)
|
32
|
-
end
|
33
|
-
|
34
|
-
def log_file
|
35
|
-
FileUtils.mkdir_p(File.dirname(log_file_path))
|
36
|
-
File.open(log_file_path, File::WRONLY | File::APPEND | File::CREAT)
|
37
|
-
end
|
38
|
-
|
39
|
-
def logger
|
40
|
-
@logger ||= Logger.new(log_file, 'daily')
|
41
|
-
end
|
42
23
|
end # class << self
|
43
24
|
|
25
|
+
include Validations
|
26
|
+
|
44
27
|
# This endpoint always returns json regardless of what fmt is specified.
|
45
28
|
def exchanges_list
|
46
29
|
response = get(path: '/exchanges-list')
|
@@ -75,20 +58,15 @@ class Eodhd
|
|
75
58
|
handle_response(response)
|
76
59
|
end
|
77
60
|
|
61
|
+
attr_accessor\
|
62
|
+
:api_token,
|
63
|
+
:logger
|
64
|
+
|
78
65
|
private
|
79
66
|
|
80
|
-
def initialize(api_token:)
|
67
|
+
def initialize(api_token: nil, logger: nil, use_default_logger: false)
|
81
68
|
@api_token = api_token
|
82
|
-
|
83
|
-
|
84
|
-
def validate_arguments(exchange_code: nil, exchange_id: nil, symbol: nil, period: nil, from: nil, to: nil, date: nil)
|
85
|
-
exchange_code ||= exchange_id
|
86
|
-
validate_exchange_code!(exchange_code) if exchange_code
|
87
|
-
validate_symbol!(symbol) if symbol
|
88
|
-
validate_period!(period) if period
|
89
|
-
validate_date!(from, 'from') if from
|
90
|
-
validate_date!(to, 'to') if to
|
91
|
-
validate_date_range!(from, to) if from && to
|
69
|
+
@logger = use_default_logger ? DefaultLogger.logger : logger
|
92
70
|
end
|
93
71
|
|
94
72
|
def request_string(path)
|
@@ -104,24 +82,24 @@ class Eodhd
|
|
104
82
|
if log_args?(args)
|
105
83
|
log_string << "?#{args.x_www_form_urlencode}"
|
106
84
|
end
|
107
|
-
|
85
|
+
@logger.info(log_string)
|
108
86
|
end
|
109
87
|
|
110
88
|
def log_response(code:, message:, body:)
|
111
89
|
log_string = "#{code}\n#{message}\n#{body}"
|
112
|
-
|
90
|
+
@logger.info(log_string)
|
113
91
|
end
|
114
92
|
|
115
93
|
def log_error(code:, message:, body:)
|
116
94
|
log_string = "#{code}\n#{message}\n#{body}"
|
117
|
-
|
95
|
+
@logger.error(log_string)
|
118
96
|
end
|
119
97
|
|
120
98
|
def do_request(verb:, path:, args: {})
|
121
99
|
api_token = args[:api_token] || @api_token
|
122
100
|
fmt = args[:fmt] || 'json'
|
123
101
|
args.merge!(api_token: api_token, fmt: fmt)
|
124
|
-
log_request(verb: verb, request_string: request_string(path), args: args)
|
102
|
+
log_request(verb: verb, request_string: request_string(path), args: args) if use_logging?
|
125
103
|
HTTP.send(verb.to_s.downcase, request_string(path), args)
|
126
104
|
end
|
127
105
|
|
@@ -135,9 +113,13 @@ class Eodhd
|
|
135
113
|
log_response(code: response.code, message: response.message, body: response.body) if use_logging?
|
136
114
|
parsed_body
|
137
115
|
else
|
138
|
-
log_error(code: response.code, message: response.message, body: response.body)
|
116
|
+
log_error(code: response.code, message: response.message, body: response.body) if use_logging?
|
139
117
|
raise Eodhd::Error.new(code: response.code, message: response.message, body: response.body)
|
140
118
|
end
|
141
119
|
end
|
120
|
+
|
121
|
+
def use_logging?
|
122
|
+
!@logger.nil?
|
123
|
+
end
|
142
124
|
end
|
143
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
|
data/lib/Eodhd/VERSION.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = '0.
|
1
|
+
class Eodhd
|
2
|
+
VERSION = '0.16.0'
|
3
3
|
end
|
data/lib/Eodhd/Validations.rb
CHANGED
@@ -1,16 +1,26 @@
|
|
1
1
|
# Eodhd/Validations.rb
|
2
2
|
# Eodhd::Validations
|
3
3
|
|
4
|
-
|
4
|
+
class Eodhd
|
5
5
|
module Validations
|
6
|
-
def
|
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)
|
7
17
|
return unless exchange_code
|
8
18
|
unless exchange_code.match?(/\A[A-Z]{2,6}\z/)
|
9
19
|
raise ArgumentError, "Invalid exchange_code '#{exchange_code}'. Must be 2-6 uppercase letters"
|
10
20
|
end
|
11
21
|
end
|
12
22
|
|
13
|
-
def validate_symbol
|
23
|
+
def validate_symbol(symbol)
|
14
24
|
return unless symbol
|
15
25
|
if symbol.strip.empty?
|
16
26
|
raise ArgumentError, "Symbol cannot be empty"
|
@@ -20,7 +30,7 @@ module Eodhd
|
|
20
30
|
end
|
21
31
|
end
|
22
32
|
|
23
|
-
def validate_period
|
33
|
+
def validate_period(period)
|
24
34
|
return unless period
|
25
35
|
valid_periods = %w[d w m]
|
26
36
|
unless valid_periods.include?(period)
|
@@ -28,7 +38,7 @@ module Eodhd
|
|
28
38
|
end
|
29
39
|
end
|
30
40
|
|
31
|
-
def validate_date
|
41
|
+
def validate_date(date, param_name = 'date')
|
32
42
|
return unless date
|
33
43
|
case date
|
34
44
|
when Date
|
@@ -47,7 +57,7 @@ module Eodhd
|
|
47
57
|
end
|
48
58
|
end
|
49
59
|
|
50
|
-
def validate_date_range
|
60
|
+
def validate_date_range(from, to)
|
51
61
|
return unless from && to
|
52
62
|
from_date = from.is_a?(Date) ? from : Date.parse(from.to_s)
|
53
63
|
to_date = to.is_a?(Date) ? to : Date.parse(to.to_s)
|
@@ -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,12 +1,12 @@
|
|
1
1
|
# Eodhd.rb
|
2
2
|
# Eodhd
|
3
3
|
|
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'
|
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'
|
10
10
|
|
11
11
|
class Eodhd
|
12
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,7 +130,9 @@ 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
|
@@ -44,7 +144,12 @@ files:
|
|
44
144
|
- lib/Hash/x_www_form_urlencode.rb
|
45
145
|
- lib/Thoran/Hash/XWwwFormUrlencode/x_www_form_urlencode.rb
|
46
146
|
- lib/Thoran/String/UrlEncode/url_encode.rb
|
47
|
-
-
|
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
|
48
153
|
homepage: http://github.com/thoran/eodhd.rb
|
49
154
|
licenses:
|
50
155
|
- Ruby
|