eodhd.rb 0.15.0 → 0.17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75d710273541a726ea8483974f5ac0f0a78ddee69fcb5024fe5a003ecfff3f35
4
- data.tar.gz: d156d5857f9cc1892d23f54c491a7a5cdefcea49135c50e16f1ab70ea402a0c9
3
+ metadata.gz: 32a73dbd43fadc7f1f4f84e0e3cd8490fd08cc8ee75afa9207bf6f341514bd20
4
+ data.tar.gz: d40408203120a0dc787345d3545c118570e58488053705f6c6280bfa0c523971
5
5
  SHA512:
6
- metadata.gz: 6c09445a3ef620a5ee45be611bf41d4d70c9edc2aef5a52ab691849eb4dea1c06381decb9534faa2b6ff680d11a3e44c47133b4a997125697fa84209add6c49a
7
- data.tar.gz: 30f076f3f55ff6c11fbd8551627b01b90791ca467834b6154e3be2f64f66c3edb7695a2dc6ed04be9464f3befd653ea854108833c8313a2939fe0fead7b2c478
6
+ metadata.gz: f77f80889b5a92557731cb2753be73cafc1f663568dfa0d52ae844532cd1bbc95446f9c8951cbf0bf2874a8a74eb90e3baecf0cf7fdcee223d440611077e9ed1
7
+ data.tar.gz: ac326a3aba6438c3911a6184f4c932253c7f6fed4875a9bd11bac19dd411c7623c0c25ec7ff7856c677889b15b1b605938b4961ec638839d34fb8a098b982a5e
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-09-16'
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')
@@ -61,9 +44,7 @@ class Eodhd
61
44
  from: from,
62
45
  to: to
63
46
  )
64
- args = {period: period}
65
- args.merge!(from: from) if from
66
- args.merge!(to: to) if to
47
+ args = {period: period, from: from, to: to}.compact
67
48
  response = get(path: "/eod/#{symbol}.#{exchange_id}", args: args)
68
49
  handle_response(response)
69
50
  end
@@ -75,20 +56,28 @@ class Eodhd
75
56
  handle_response(response)
76
57
  end
77
58
 
59
+ def intraday(exchange_code:, symbol:, interval:, from: nil, to: nil)
60
+ validate_arguments(
61
+ exchange_code: exchange_code,
62
+ symbol: symbol,
63
+ interval: interval,
64
+ from: from,
65
+ to: to
66
+ )
67
+ args = {interval: interval, from: from, to: to}.compact
68
+ response = get(path: "/intraday/#{symbol}.#{exchange_code}", args: args)
69
+ handle_response(response)
70
+ end
71
+
72
+ attr_accessor\
73
+ :api_token,
74
+ :logger
75
+
78
76
  private
79
77
 
80
- def initialize(api_token:)
78
+ def initialize(api_token: nil, logger: nil, use_default_logger: false)
81
79
  @api_token = api_token
82
- end
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
80
+ @logger = use_default_logger ? DefaultLogger.logger : logger
92
81
  end
93
82
 
94
83
  def request_string(path)
@@ -104,24 +93,24 @@ class Eodhd
104
93
  if log_args?(args)
105
94
  log_string << "?#{args.x_www_form_urlencode}"
106
95
  end
107
- self.class.logger.info(log_string)
96
+ @logger.info(log_string)
108
97
  end
109
98
 
110
99
  def log_response(code:, message:, body:)
111
100
  log_string = "#{code}\n#{message}\n#{body}"
112
- self.class.logger.info(log_string)
101
+ @logger.info(log_string)
113
102
  end
114
103
 
115
104
  def log_error(code:, message:, body:)
116
105
  log_string = "#{code}\n#{message}\n#{body}"
117
- self.class.logger.error(log_string)
106
+ @logger.error(log_string)
118
107
  end
119
108
 
120
109
  def do_request(verb:, path:, args: {})
121
110
  api_token = args[:api_token] || @api_token
122
111
  fmt = args[:fmt] || 'json'
123
112
  args.merge!(api_token: api_token, fmt: fmt)
124
- log_request(verb: verb, request_string: request_string(path), args: args)
113
+ log_request(verb: verb, request_string: request_string(path), args: args) if use_logging?
125
114
  HTTP.send(verb.to_s.downcase, request_string(path), args)
126
115
  end
127
116
 
@@ -135,9 +124,13 @@ class Eodhd
135
124
  log_response(code: response.code, message: response.message, body: response.body) if use_logging?
136
125
  parsed_body
137
126
  else
138
- log_error(code: response.code, message: response.message, body: response.body)
127
+ log_error(code: response.code, message: response.message, body: response.body) if use_logging?
139
128
  raise Eodhd::Error.new(code: response.code, message: response.message, body: response.body)
140
129
  end
141
130
  end
131
+
132
+ def use_logging?
133
+ !@logger.nil?
134
+ end
142
135
  end
143
136
  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,76 @@
1
+ # Eodhd/Intraday.rb
2
+ # Eodhd::Intraday
3
+
4
+ class Eodhd
5
+ class Intraday
6
+ class << self
7
+ def all(client: nil, api_token: nil, exchange_code: 'US', symbol:, interval:, from: nil, to: nil)
8
+ load(
9
+ client: client,
10
+ api_token: api_token,
11
+ exchange_code: exchange_code,
12
+ symbol: symbol,
13
+ interval: interval,
14
+ from: from,
15
+ to: to
16
+ )
17
+ end
18
+
19
+ private
20
+
21
+ def load(client: nil, api_token: nil, exchange_code: 'US', symbol:, interval:, from:, to:)
22
+ client ||= Client.new(api_token: api_token)
23
+ client.intraday(
24
+ exchange_code: exchange_code,
25
+ symbol: symbol,
26
+ interval: interval,
27
+ from: from,
28
+ to: to
29
+ ).collect do |intraday_data|
30
+ self.new(
31
+ exchange_code: exchange_code,
32
+ symbol: symbol,
33
+ interval: interval,
34
+ timestamp: intraday_data['timestamp'],
35
+ gmtoffset: intraday_data['gmtoffset'],
36
+ datetime: intraday_data['datetime'],
37
+ open: intraday_data['open'],
38
+ high: intraday_data['high'],
39
+ low: intraday_data['low'],
40
+ close: intraday_data['close'],
41
+ volume: intraday_data['volume']
42
+ )
43
+ end
44
+ end
45
+ end # class << self
46
+
47
+ attr_reader\
48
+ :exchange_code,
49
+ :symbol,
50
+ :interval,
51
+ :timestamp,
52
+ :gmtoffset,
53
+ :datetime,
54
+ :open,
55
+ :high,
56
+ :low,
57
+ :close,
58
+ :volume
59
+
60
+ private
61
+
62
+ def initialize(exchange_code:, symbol:, interval:, timestamp:, gmtoffset:, datetime:, open:, high:, low:, close:, volume:)
63
+ @exchange_code = exchange_code
64
+ @symbol = symbol
65
+ @interval = interval
66
+ @timestamp = timestamp
67
+ @gmtoffset = gmtoffset
68
+ @datetime = datetime
69
+ @open = open
70
+ @high = high
71
+ @low = low
72
+ @close = close
73
+ @volume = volume
74
+ end
75
+ end
76
+ end
data/lib/Eodhd/VERSION.rb CHANGED
@@ -1,3 +1,3 @@
1
- module Eodhd
2
- VERSION = '0.15.0'
1
+ class Eodhd
2
+ VERSION = '0.17.0'
3
3
  end
@@ -1,16 +1,27 @@
1
1
  # Eodhd/Validations.rb
2
2
  # Eodhd::Validations
3
3
 
4
- module Eodhd
4
+ class Eodhd
5
5
  module Validations
6
- def validate_exchange_code!(exchange_code)
6
+ def validate_arguments(exchange_code: nil, exchange_id: nil, symbol: nil, period: nil, interval: 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_interval(interval)
12
+ validate_date(from, 'from')
13
+ validate_date(to, 'to')
14
+ validate_date_range(from, to)
15
+ end
16
+
17
+ def validate_exchange_code(exchange_code)
7
18
  return unless exchange_code
8
19
  unless exchange_code.match?(/\A[A-Z]{2,6}\z/)
9
20
  raise ArgumentError, "Invalid exchange_code '#{exchange_code}'. Must be 2-6 uppercase letters"
10
21
  end
11
22
  end
12
23
 
13
- def validate_symbol!(symbol)
24
+ def validate_symbol(symbol)
14
25
  return unless symbol
15
26
  if symbol.strip.empty?
16
27
  raise ArgumentError, "Symbol cannot be empty"
@@ -20,7 +31,7 @@ module Eodhd
20
31
  end
21
32
  end
22
33
 
23
- def validate_period!(period)
34
+ def validate_period(period)
24
35
  return unless period
25
36
  valid_periods = %w[d w m]
26
37
  unless valid_periods.include?(period)
@@ -28,7 +39,15 @@ module Eodhd
28
39
  end
29
40
  end
30
41
 
31
- def validate_date!(date, param_name = 'date')
42
+ def validate_interval(interval)
43
+ return unless interval
44
+ valid_intervals = %w[1m 5m 15m 30m 1h 4h 1d 1w 1mo]
45
+ unless valid_intervals.include?(interval)
46
+ raise ArgumentError, "Invalid interval: #{interval}. Must be one of: #{valid_intervals.join(', ')}"
47
+ end
48
+ end
49
+
50
+ def validate_date(date, param_name = 'date')
32
51
  return unless date
33
52
  case date
34
53
  when Date
@@ -47,7 +66,7 @@ module Eodhd
47
66
  end
48
67
  end
49
68
 
50
- def validate_date_range!(from, to)
69
+ def validate_date_range(from, to)
51
70
  return unless from && to
52
71
  from_date = from.is_a?(Date) ? from : Date.parse(from.to_s)
53
72
  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' # This line doesn't seem to be necessary in Ruby 3.
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
- self.class.logger.error(log_string)
45
+ @logger.error(log_string) if use_logging?
63
46
  end
64
47
 
65
48
  def handle_response(message)
66
- if parsed_json = JSON.parse(message)
67
- parsed_json
68
- else
69
- log_error(
70
- message: response.message,
71
- )
72
- raise Eodhd::Error.new(
73
- code: 'ws',
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)
@@ -1,12 +1,13 @@
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/Intraday'
10
+ require_relative './Eodhd/WebSocketClient'
10
11
 
11
12
  class Eodhd
12
13
  def initialize(api_token:, consumer: nil)
@@ -34,13 +35,13 @@ class Eodhd
34
35
  Eodhd::EodBulkLastDay.all(api_token: @api_token, exchange_code: exchange_code, date: date)
35
36
  end
36
37
 
38
+ def intraday(exchange: nil, exchange_code: nil, symbol:, interval:, from: nil, to: nil)
39
+ exchange_code ||= exchange&.code || 'US'
40
+ Eodhd::Intraday.all(api_token: @api_token, exchange_code: exchange_code, symbol: symbol, interval: interval, from: from, to: to)
41
+ end
42
+
37
43
  def web_socket(asset_class:, symbols:)
38
- Eodhd::WebSocketClient.new(
39
- api_token: @api_token,
40
- asset_class: asset_class,
41
- symbols: symbols,
42
- consumer: @consumer,
43
- )
44
+ Eodhd::WebSocketClient.new(api_token: @api_token, asset_class: asset_class, symbols: symbols, consumer: @consumer)
44
45
  end
45
46
 
46
47
  def stream(asset_class:, symbols:)
@@ -0,0 +1,87 @@
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 "#intraday" do
57
+ it "intraday returns parsed JSON array" do
58
+ VCR.use_cassette('client_intraday') do
59
+ result = client.intraday(exchange_code: 'US', symbol: 'AAPL', interval: '5m')
60
+ _(result).must_be_kind_of(Array)
61
+ _(result.first.keys).must_equal(%w{timestamp gmtoffset datetime open high low close volume})
62
+ end
63
+ end
64
+
65
+ it "raises Eodhd::Error on failure" do
66
+ VCR.use_cassette('client_intraday_401_error') do
67
+ _{client.intraday(exchange_code: 'US', symbol: 'AAPL', interval: '5m')}.must_raise(Eodhd::Error)
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "#eod_bulk_last_day" do
73
+ it "eod_bulk_last_day returns parsed JSON array" do
74
+ VCR.use_cassette("client_eod_bulk_last_day") do
75
+ result = client.eod_bulk_last_day(exchange_id: 'AU', date: "2024-09-30")
76
+ _(result).must_be_kind_of(Array)
77
+ _(result.first.keys).must_equal(%w{code exchange_short_name date open high low close adjusted_close volume})
78
+ end
79
+ end
80
+
81
+ it "raises Eodhd::Error on failure" do
82
+ VCR.use_cassette('client_eod_bulk_last_day_401_error') do
83
+ _{client.eod_bulk_last_day(exchange_id: 'AU', date: "2024-09-30")}.must_raise(Eodhd::Error)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,62 @@
1
+ require_relative "helper"
2
+
3
+ describe Eodhd do
4
+ let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
5
+
6
+ let(:eodhd){Eodhd.new(api_token: api_token)}
7
+
8
+ describe "#exchanges" do
9
+ it "delegates to Eodhd::Exchange.all" do
10
+ VCR.use_cassette('eodhd_exchanges') do
11
+ exchanges = eodhd.exchanges
12
+ _(exchanges).must_be_kind_of(Array)
13
+ _(exchanges).wont_be_empty
14
+ _(exchanges.first).must_be_kind_of(Eodhd::Exchange)
15
+ end
16
+ end
17
+ end
18
+
19
+ describe "#exchange_symbols" do
20
+ it "delegates to Eodhd::ExchangeSymbol.all" do
21
+ VCR.use_cassette('eodhd_exchange_symbols') do
22
+ symbols = eodhd.exchange_symbols(exchange_code: 'AU')
23
+ _(symbols).must_be_kind_of(Array)
24
+ _(symbols).wont_be_empty
25
+ _(symbols.first).must_be_kind_of(Eodhd::ExchangeSymbol)
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "#eod_data" do
31
+ it "delegates to Eodhd::EodData.all" do
32
+ VCR.use_cassette('eodhd_eod_data') do
33
+ eod_data = eodhd.eod_data(exchange_code: 'AU', symbol: 'BHP', period: 'd')
34
+ _(eod_data).must_be_kind_of(Array)
35
+ _(eod_data).wont_be_empty
36
+ _(eod_data.first).must_be_kind_of(Eodhd::EodData)
37
+ end
38
+ end
39
+ end
40
+
41
+ describe "#eod_bulk_last_day" do
42
+ it "delegates to Eodhd::EodBulkLastDay.all" do
43
+ VCR.use_cassette('eodhd_eod_bulk_last_day') do
44
+ bulk_last_day = eodhd.eod_bulk_last_day(exchange_code: 'AU', date: '2024-09-30')
45
+ _(bulk_last_day).must_be_kind_of(Array)
46
+ _(bulk_last_day).wont_be_empty
47
+ _(bulk_last_day.first).must_be_kind_of(Eodhd::EodBulkLastDay)
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#intraday" do
53
+ it "delegates to Eodhd::Intraday.all" do
54
+ VCR.use_cassette('eodhd_intraday') do
55
+ intraday_data = eodhd.intraday(exchange_code: 'US', symbol: 'AAPL', interval: '5m')
56
+ _(intraday_data).must_be_kind_of(Array)
57
+ _(intraday_data).wont_be_empty
58
+ _(intraday_data.first).must_be_kind_of(Eodhd::Intraday)
59
+ end
60
+ end
61
+ end
62
+ 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,5 @@
1
+ require_relative "helper"
2
+
3
+ Dir[File.expand_path("./**/*_test.rb", __dir__)].each do |file|
4
+ require file
5
+ end
@@ -0,0 +1,133 @@
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_interval" do
96
+ it "accepts valid intervals" do
97
+ _{v.validate_interval('1m')}.must_be_silent
98
+ _{v.validate_interval('5m')}.must_be_silent
99
+ _{v.validate_interval('1h')}.must_be_silent
100
+ _{v.validate_interval('1d')}.must_be_silent
101
+ end
102
+
103
+ it "rejects invalid intervals" do
104
+ _{v.validate_interval('2m')}.must_raise(ArgumentError)
105
+ _{v.validate_interval('invalid')}.must_raise(ArgumentError)
106
+ _{v.validate_interval('')}.must_raise(ArgumentError)
107
+ end
108
+ end
109
+
110
+ describe "#validate_date" do
111
+ it "accepts valid date formats and Date objects" do
112
+ _{v.validate_date('2024-09-30')}.must_be_silent
113
+ _{v.validate_date(Date.new(2024,9,30))}.must_be_silent
114
+ end
115
+
116
+ it "rejects invalid dates and formats" do
117
+ _{v.validate_date('2024/09/30')}.must_raise(ArgumentError)
118
+ _{v.validate_date('2024-02-30')}.must_raise(ArgumentError)
119
+ _{v.validate_date(12345)}.must_raise(ArgumentError)
120
+ end
121
+ end
122
+
123
+ describe "#validate_date_range" do
124
+ it "accepts valid date ranges" do
125
+ _{v.validate_date_range('2024-01-01','2024-12-31')}.must_be_silent
126
+ _{v.validate_date_range(Date.new(2024,1,1),Date.new(2024,12,31))}.must_be_silent
127
+ end
128
+
129
+ it "rejects reversed date ranges" do
130
+ _{v.validate_date_range('2024-12-31','2024-01-01')}.must_raise(ArgumentError)
131
+ end
132
+ end
133
+ 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.15.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-09-16 00:00:00.000000000 Z
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,19 +130,27 @@ 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/Intraday.rb
41
142
  - lib/Eodhd/VERSION.rb
42
143
  - lib/Eodhd/Validations.rb
43
144
  - lib/Eodhd/WebSocketClient.rb
44
145
  - lib/Hash/x_www_form_urlencode.rb
45
146
  - lib/Thoran/Hash/XWwwFormUrlencode/x_www_form_urlencode.rb
46
147
  - lib/Thoran/String/UrlEncode/url_encode.rb
47
- - lib/eodhd.rb
148
+ - test/client_test.rb
149
+ - test/eodhd_test.rb
150
+ - test/helper.rb
151
+ - test/test_all.rb
152
+ - test/validations_test.rb
153
+ - test/web_socket_client_test.rb
48
154
  homepage: http://github.com/thoran/eodhd.rb
49
155
  licenses:
50
156
  - Ruby