eodhd 0.19.5
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 +7 -0
- data/CHANGELOG +195 -0
- data/Gemfile +3 -0
- data/README.md +160 -0
- data/Rakefile +28 -0
- data/eodhd.gemspec +52 -0
- data/lib/Eodhd/Client.rb +143 -0
- data/lib/Eodhd/DefaultLogger.rb +30 -0
- data/lib/Eodhd/EodBulkLastDay.rb +56 -0
- data/lib/Eodhd/EodData.rb +56 -0
- data/lib/Eodhd/Error.rb +16 -0
- data/lib/Eodhd/Exchange.rb +50 -0
- data/lib/Eodhd/ExchangeSymbol.rb +50 -0
- data/lib/Eodhd/Fundamentals/AnalystRatings.rb +29 -0
- data/lib/Eodhd/Fundamentals/ESGScores.rb +37 -0
- data/lib/Eodhd/Fundamentals/Earnings/AnnualEntry.rb +21 -0
- data/lib/Eodhd/Fundamentals/Earnings/HistoryEntry.rb +33 -0
- data/lib/Eodhd/Fundamentals/Earnings/TrendEntry.rb +65 -0
- data/lib/Eodhd/Fundamentals/Earnings.rb +30 -0
- data/lib/Eodhd/Fundamentals/Financials/BalanceSheet/Entry.rb +147 -0
- data/lib/Eodhd/Fundamentals/Financials/BalanceSheet.rb +28 -0
- data/lib/Eodhd/Fundamentals/Financials/CashFlow/Entry.rb +83 -0
- data/lib/Eodhd/Fundamentals/Financials/CashFlow.rb +28 -0
- data/lib/Eodhd/Fundamentals/Financials/IncomeStatement/Entry.rb +87 -0
- data/lib/Eodhd/Fundamentals/Financials/IncomeStatement.rb +28 -0
- data/lib/Eodhd/Fundamentals/Financials.rb +25 -0
- data/lib/Eodhd/Fundamentals/General.rb +85 -0
- data/lib/Eodhd/Fundamentals/Highlights.rb +63 -0
- data/lib/Eodhd/Fundamentals/Holders/Fund.rb +31 -0
- data/lib/Eodhd/Fundamentals/Holders/Institution.rb +31 -0
- data/lib/Eodhd/Fundamentals/Holders.rb +27 -0
- data/lib/Eodhd/Fundamentals/InsiderTransaction.rb +35 -0
- data/lib/Eodhd/Fundamentals/OutstandingShares/Entry.rb +25 -0
- data/lib/Eodhd/Fundamentals/OutstandingShares.rb +26 -0
- data/lib/Eodhd/Fundamentals/SharesStats.rb +33 -0
- data/lib/Eodhd/Fundamentals/SplitsDividends.rb +31 -0
- data/lib/Eodhd/Fundamentals/Technicals.rb +33 -0
- data/lib/Eodhd/Fundamentals/Valuation.rb +29 -0
- data/lib/Eodhd/Fundamentals.rb +92 -0
- data/lib/Eodhd/Intraday.rb +76 -0
- data/lib/Eodhd/VERSION.rb +3 -0
- data/lib/Eodhd/Validations.rb +78 -0
- data/lib/Eodhd/WebSocketClient.rb +130 -0
- data/lib/Hash/x_www_form_urlencode.rb +7 -0
- data/lib/Thoran/Hash/XWwwFormUrlencode/x_www_form_urlencode.rb +27 -0
- data/lib/Thoran/String/Urlencode/urlencode.rb +29 -0
- data/lib/eodhd.rb +78 -0
- data/test/Eodhd/Client_test.rb +111 -0
- data/test/Eodhd/Fundamentals_test.rb +163 -0
- data/test/Eodhd/VERSION_test.rb +20 -0
- data/test/Eodhd/Validations_test.rb +135 -0
- data/test/Eodhd/WebSocketClient_test.rb +99 -0
- data/test/Eodhd_test.rb +73 -0
- data/test/helper.rb +17 -0
- metadata +218 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Eodhd/Fundamentals.rb
|
|
2
|
+
# Eodhd::Fundamentals
|
|
3
|
+
|
|
4
|
+
require_relative './Fundamentals/AnalystRatings'
|
|
5
|
+
require_relative './Fundamentals/Earnings'
|
|
6
|
+
require_relative './Fundamentals/ESGScores'
|
|
7
|
+
require_relative './Fundamentals/Financials'
|
|
8
|
+
require_relative './Fundamentals/General'
|
|
9
|
+
require_relative './Fundamentals/Highlights'
|
|
10
|
+
require_relative './Fundamentals/Holders'
|
|
11
|
+
require_relative './Fundamentals/InsiderTransaction'
|
|
12
|
+
require_relative './Fundamentals/OutstandingShares'
|
|
13
|
+
require_relative './Fundamentals/SharesStats'
|
|
14
|
+
require_relative './Fundamentals/SplitsDividends'
|
|
15
|
+
require_relative './Fundamentals/Technicals'
|
|
16
|
+
require_relative './Fundamentals/Valuation'
|
|
17
|
+
|
|
18
|
+
class Eodhd
|
|
19
|
+
class Fundamentals
|
|
20
|
+
class << self
|
|
21
|
+
def all(client: nil, api_token: nil, exchange_code:, symbol:, filter: nil)
|
|
22
|
+
load(client: client, api_token: api_token, exchange_code: exchange_code, symbol: symbol, filter: filter)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def load(client: nil, api_token: nil, exchange_code:, symbol:, filter:)
|
|
28
|
+
client ||= Client.new(api_token: api_token)
|
|
29
|
+
response = client.fundamentals(exchange_code: exchange_code, symbol: symbol, filter: filter)
|
|
30
|
+
self.new(
|
|
31
|
+
exchange_code: exchange_code,
|
|
32
|
+
symbol: symbol,
|
|
33
|
+
general: response['General'],
|
|
34
|
+
highlights: response['Highlights'],
|
|
35
|
+
valuation: response['Valuation'],
|
|
36
|
+
shares_stats: response['SharesStats'],
|
|
37
|
+
technicals: response['Technicals'],
|
|
38
|
+
splits_dividends: response['SplitsDividends'],
|
|
39
|
+
analyst_ratings: response['AnalystRatings'],
|
|
40
|
+
holders: response['Holders'],
|
|
41
|
+
insider_transactions: response['InsiderTransactions'],
|
|
42
|
+
esg_scores: response['ESGScores'],
|
|
43
|
+
outstanding_shares: response['outstandingShares'],
|
|
44
|
+
earnings: response['Earnings'],
|
|
45
|
+
financials: response['Financials']
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end # class << self
|
|
49
|
+
|
|
50
|
+
attr_reader\
|
|
51
|
+
:exchange_code,
|
|
52
|
+
:symbol,
|
|
53
|
+
:general,
|
|
54
|
+
:highlights,
|
|
55
|
+
:valuation,
|
|
56
|
+
:shares_stats,
|
|
57
|
+
:technicals,
|
|
58
|
+
:splits_dividends,
|
|
59
|
+
:analyst_ratings,
|
|
60
|
+
:holders,
|
|
61
|
+
:insider_transactions,
|
|
62
|
+
:esg_scores,
|
|
63
|
+
:outstanding_shares,
|
|
64
|
+
:earnings,
|
|
65
|
+
:financials
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def initialize(exchange_code:, symbol:, general:, highlights:, valuation:, shares_stats:, technicals:, splits_dividends:, analyst_ratings:, holders:, insider_transactions:, esg_scores:, outstanding_shares:, earnings:, financials:)
|
|
70
|
+
@exchange_code = exchange_code
|
|
71
|
+
@symbol = symbol
|
|
72
|
+
@general = General.new(general) if general
|
|
73
|
+
@highlights = Highlights.new(highlights) if highlights
|
|
74
|
+
@valuation = Valuation.new(valuation) if valuation
|
|
75
|
+
@shares_stats = SharesStats.new(shares_stats) if shares_stats
|
|
76
|
+
@technicals = Technicals.new(technicals) if technicals
|
|
77
|
+
@splits_dividends = SplitsDividends.new(splits_dividends) if splits_dividends
|
|
78
|
+
@analyst_ratings = AnalystRatings.new(analyst_ratings) if analyst_ratings
|
|
79
|
+
@holders = Holders.new(holders) if holders
|
|
80
|
+
@insider_transactions = wrap_insider_transactions(insider_transactions)
|
|
81
|
+
@esg_scores = ESGScores.new(esg_scores) if esg_scores
|
|
82
|
+
@outstanding_shares = OutstandingShares.new(outstanding_shares) if outstanding_shares
|
|
83
|
+
@earnings = Earnings.new(earnings) if earnings
|
|
84
|
+
@financials = Financials.new(financials) if financials
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def wrap_insider_transactions(entries)
|
|
88
|
+
return [] unless entries
|
|
89
|
+
entries.values.map{|entry| InsiderTransaction.new(entry)}.sort_by(&:date)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
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
|
|
@@ -0,0 +1,78 @@
|
|
|
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, 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)
|
|
18
|
+
return unless exchange_code
|
|
19
|
+
unless exchange_code.match?(/\A[A-Z]{2,6}\z/)
|
|
20
|
+
raise ArgumentError, "Invalid exchange_code '#{exchange_code}'. Must be 2-6 uppercase letters"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate_symbol(symbol)
|
|
25
|
+
return unless symbol
|
|
26
|
+
if symbol.strip.empty?
|
|
27
|
+
raise ArgumentError, "Symbol cannot be empty"
|
|
28
|
+
end
|
|
29
|
+
unless symbol.match?(/\A[A-Za-z0-9.-]{1,12}\z/)
|
|
30
|
+
raise ArgumentError, "Invalid symbol '#{symbol}'. Must be 1-12 characters, letters/numbers/dots/hyphens only"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate_period(period)
|
|
35
|
+
return unless period
|
|
36
|
+
valid_periods = %w[d w m]
|
|
37
|
+
unless valid_periods.include?(period)
|
|
38
|
+
raise ArgumentError, "Invalid period '#{period}'. Must be one of: #{valid_periods.join(', ')}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
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')
|
|
51
|
+
return unless date
|
|
52
|
+
case date
|
|
53
|
+
when Date
|
|
54
|
+
return
|
|
55
|
+
when String
|
|
56
|
+
unless date.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
57
|
+
raise ArgumentError, "Invalid #{param_name} '#{date}'. Must be in format 'yyyy-mm-dd'"
|
|
58
|
+
end
|
|
59
|
+
begin
|
|
60
|
+
Date.parse(date)
|
|
61
|
+
rescue Date::Error
|
|
62
|
+
raise ArgumentError, "Invalid #{param_name} '#{date}'. Not a valid date"
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
raise ArgumentError, "Invalid #{param_name}. Must be String in 'yyyy-mm-dd' format or Date object"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_date_range(from, to)
|
|
70
|
+
return unless from && to
|
|
71
|
+
from_date = from.is_a?(Date) ? from : Date.parse(from.to_s)
|
|
72
|
+
to_date = to.is_a?(Date) ? to : Date.parse(to.to_s)
|
|
73
|
+
if from_date > to_date
|
|
74
|
+
raise ArgumentError, "from date (#{from}) cannot be after to date (#{to})"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Eodhd/WebSocketClient.rb
|
|
2
|
+
# Eodhd::WebSocketClient
|
|
3
|
+
|
|
4
|
+
# Notes:
|
|
5
|
+
# 1. All 4 of 4 WebSocket endpoints which Eodhd provides (See https://eodhd.com/financial-apis/new-real-time-data-api-websockets.) are as folllows...
|
|
6
|
+
# i. US market trade data: wss://ws.eodhistoricaldata.com/ws/us?api_token=XXX
|
|
7
|
+
# ii. US market quote data: wss://ws.eodhistoricaldata.com/ws/us-quote?api_token=XXX
|
|
8
|
+
# iii. Forex: wss://ws.eodhistoricaldata.com/ws/forex?api_token=XXX
|
|
9
|
+
# iv. Crypto: wss://ws.eodhistoricaldata.com/ws/crypto?api_token=XXX
|
|
10
|
+
|
|
11
|
+
require 'iodine'
|
|
12
|
+
require 'json'
|
|
13
|
+
|
|
14
|
+
require_relative './DefaultLogger'
|
|
15
|
+
require_relative './Error'
|
|
16
|
+
|
|
17
|
+
class Eodhd
|
|
18
|
+
class WebSocketClient
|
|
19
|
+
API_HOST = 'ws.eodhistoricaldata.com'
|
|
20
|
+
|
|
21
|
+
class Handler
|
|
22
|
+
def on_open(connection)
|
|
23
|
+
connection.write({action: 'subscribe', symbols: @symbols}.to_json)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def on_message(connection, message)
|
|
27
|
+
dataframe = handle_response(message)
|
|
28
|
+
@consumer.call(dataframe) if @consumer
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def on_close(connection)
|
|
32
|
+
Iodine.stop
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def initialize(symbols:, consumer:, logger: nil)
|
|
38
|
+
@symbols = symbols
|
|
39
|
+
@consumer = consumer
|
|
40
|
+
@logger = logger
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def log_error(code:, message:, body:)
|
|
44
|
+
log_string = "WebSocketError #{code}\n#{message}\n#{body}"
|
|
45
|
+
@logger.error(log_string) if use_logging?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def handle_response(message)
|
|
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?
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class << self
|
|
61
|
+
def path_prefix
|
|
62
|
+
'/ws'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def us_trade(api_token:, symbols:, consumer:)
|
|
66
|
+
self.class.new(api_token: api_token, asset_class: 'us', symbols: symbols, consumer: consumer)
|
|
67
|
+
end
|
|
68
|
+
alias_method :us, :us_trade
|
|
69
|
+
|
|
70
|
+
def us_quote(api_token:, symbols:, consumer:)
|
|
71
|
+
self.class.new(api_token: api_token, asset_class: 'us-quote', symbols: symbols, consumer: consumer)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def forex(api_token:, symbols:, consumer:)
|
|
75
|
+
self.class.new(api_token: api_token, asset_class: 'forex', symbols: symbols, consumer: consumer)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def crypto(api_token:, symbols:, consumer:)
|
|
79
|
+
self.class.new(api_token: api_token, asset_class: 'crypto', symbols: symbols, consumer: consumer)
|
|
80
|
+
end
|
|
81
|
+
end # class << self
|
|
82
|
+
|
|
83
|
+
attr_accessor\
|
|
84
|
+
:api_token,
|
|
85
|
+
:asset_class,
|
|
86
|
+
:consumer,
|
|
87
|
+
:logger
|
|
88
|
+
|
|
89
|
+
attr_reader\
|
|
90
|
+
:symbols
|
|
91
|
+
|
|
92
|
+
def symbols=(symbols)
|
|
93
|
+
@symbols = format_symbols(symbols)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def run
|
|
97
|
+
Iodine.threads = 1
|
|
98
|
+
Iodine.connect(url: url, handler: Handler.new(symbols: @symbols, consumer: @consumer, logger: @logger))
|
|
99
|
+
Iodine.start
|
|
100
|
+
rescue SystemExit, Interrupt
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def initialize(api_token:, asset_class:, symbols:, consumer:, logger: nil, use_default_logger: false)
|
|
107
|
+
@api_token = api_token
|
|
108
|
+
@asset_class = asset_class # crypto, forex, us-quote, us
|
|
109
|
+
@symbols = format_symbols(symbols)
|
|
110
|
+
@consumer = consumer
|
|
111
|
+
@logger = use_default_logger ? DefaultLogger.logger : logger
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def use_logging?
|
|
115
|
+
!@logger.nil?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def format_symbols(symbols)
|
|
119
|
+
if symbols.is_a?(Array)
|
|
120
|
+
symbols.join(',')
|
|
121
|
+
elsif symbols.is_a?(String)
|
|
122
|
+
symbols
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def url
|
|
127
|
+
"wss://#{API_HOST}#{self.class.path_prefix}/#{@asset_class}?api_token=#{@api_token}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Thoran/Hash/XWwwFormUrlencode/x_www_form_urlencode.rb
|
|
2
|
+
# Thoran::Hash::XWwwFormUrlencode#x_www_form_urlencode
|
|
3
|
+
|
|
4
|
+
# 20260713
|
|
5
|
+
# 0.3.0
|
|
6
|
+
|
|
7
|
+
# Changes since 0.2:
|
|
8
|
+
# -/0: (The class name and the snake case name really are consistent now.)
|
|
9
|
+
# 1. /XWwwFormUrlEncode/XWwwFormUrlencode/
|
|
10
|
+
# 2. /url_encode/urlencode/
|
|
11
|
+
|
|
12
|
+
require 'Thoran/String/Urlencode/urlencode'
|
|
13
|
+
|
|
14
|
+
module Thoran
|
|
15
|
+
module Hash
|
|
16
|
+
module XWwwFormUrlencode
|
|
17
|
+
|
|
18
|
+
def x_www_form_urlencode(joiner = '&')
|
|
19
|
+
inject([]){|a,e| a << "#{e.first.to_s.urlencode}=#{e.last.to_s.urlencode}" unless e.last.nil?; a}.join(joiner)
|
|
20
|
+
end
|
|
21
|
+
alias_method :x_www_form_url_encode, :x_www_form_urlencode
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Hash.send(:include, Thoran::Hash::XWwwFormUrlencode)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Thoran/String/Urlencode/urlencode.rb
|
|
2
|
+
# Thoran::String::Urlencode#urlencode
|
|
3
|
+
|
|
4
|
+
# 20260713
|
|
5
|
+
# 0.4.0
|
|
6
|
+
|
|
7
|
+
# Acknowledgements: I've simply ripped off and refashioned the code from the CGI module!...
|
|
8
|
+
|
|
9
|
+
# Changes since 0.3:
|
|
10
|
+
# -/0: More consistent naming.
|
|
11
|
+
# 1. /UrlEncode/Urlencode/
|
|
12
|
+
# 2. /url_encode/urlencode/
|
|
13
|
+
|
|
14
|
+
module Thoran
|
|
15
|
+
module String
|
|
16
|
+
module Urlencode
|
|
17
|
+
|
|
18
|
+
def urlencode
|
|
19
|
+
self.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
|
|
20
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
|
21
|
+
end.tr(' ', '+')
|
|
22
|
+
end
|
|
23
|
+
alias_method :url_encode, :urlencode
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
String.send(:include, Thoran::String::Urlencode)
|
data/lib/eodhd.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Eodhd.rb
|
|
2
|
+
# Eodhd
|
|
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/Intraday'
|
|
10
|
+
require_relative './Eodhd/Fundamentals'
|
|
11
|
+
require_relative './Eodhd/WebSocketClient'
|
|
12
|
+
require_relative './Eodhd/VERSION'
|
|
13
|
+
|
|
14
|
+
class Eodhd
|
|
15
|
+
def exchanges
|
|
16
|
+
Eodhd::Exchange.all(api_token: @api_token)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def exchange_symbols(exchange: nil, exchange_code: nil)
|
|
20
|
+
exchange_code ||= exchange&.code
|
|
21
|
+
Eodhd::ExchangeSymbol.all(api_token: @api_token, exchange_code: exchange_code)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def eod_data(exchange: nil, exchange_code: nil, exchange_symbol: nil, symbol: nil, period: nil, from: nil, to: nil)
|
|
25
|
+
exchange_code ||= exchange&.code
|
|
26
|
+
symbol ||= exchange_symbol&.code
|
|
27
|
+
Eodhd::EodData.all(api_token: @api_token, exchange_code: exchange_code, symbol: symbol, period: period, from: from, to: to)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def eod_bulk_last_day(exchange: nil, exchange_code: nil, date:)
|
|
31
|
+
exchange_code ||= exchange&.code
|
|
32
|
+
Eodhd::EodBulkLastDay.all(api_token: @api_token, exchange_code: exchange_code, date: date)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def intraday(exchange: nil, exchange_code: nil, exchange_symbol: nil, symbol: nil, interval:, from: nil, to: nil)
|
|
36
|
+
exchange_code ||= exchange&.code
|
|
37
|
+
symbol ||= exchange_symbol&.code
|
|
38
|
+
Eodhd::Intraday.all(api_token: @api_token, exchange_code: exchange_code, symbol: symbol, interval: interval, from: from, to: to)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def fundamentals(exchange: nil, exchange_code: nil, exchange_symbol: nil, symbol: nil, filter: nil)
|
|
42
|
+
exchange_code ||= exchange&.code
|
|
43
|
+
symbol ||= exchange_symbol&.code
|
|
44
|
+
Eodhd::Fundamentals.all(api_token: @api_token, exchange_code: exchange_code, symbol: symbol, filter: filter)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def us_trade_stream(symbols)
|
|
48
|
+
stream(asset_class: 'us', symbols: symbols)
|
|
49
|
+
end
|
|
50
|
+
alias_method :us_stream, :us_trade_stream
|
|
51
|
+
|
|
52
|
+
def us_quote_stream(symbols)
|
|
53
|
+
stream(asset_class: 'us-quote', symbols: symbols)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def forex_stream(symbols)
|
|
57
|
+
stream(asset_class: 'forex', symbols: symbols)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def crypto_stream(symbols)
|
|
61
|
+
stream(asset_class: 'crypto', symbols: symbols)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def initialize(api_token:, consumer: nil)
|
|
67
|
+
@api_token = api_token
|
|
68
|
+
@consumer = consumer
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def web_socket(asset_class:, symbols:)
|
|
72
|
+
Eodhd::WebSocketClient.new(api_token: @api_token, asset_class: asset_class, symbols: symbols, consumer: @consumer)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def stream(asset_class:, symbols:)
|
|
76
|
+
web_socket(asset_class: asset_class, symbols: symbols).run
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Eodhd/Client_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../helper'
|
|
4
|
+
|
|
5
|
+
describe Eodhd::Client do
|
|
6
|
+
let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
|
|
7
|
+
let(:client){Eodhd::Client.new(api_token: api_token)}
|
|
8
|
+
|
|
9
|
+
describe "#exchanges_list" do
|
|
10
|
+
it "returns parsed JSON array" do
|
|
11
|
+
VCR.use_cassette("client_exchanges_list") do
|
|
12
|
+
result = client.exchanges_list
|
|
13
|
+
_(result).must_be_kind_of(Array)
|
|
14
|
+
_(result.first["Code"]).wont_be_nil
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "raises Eodhd::Error on failure" do
|
|
19
|
+
VCR.use_cassette("client_exchanges_list_401_error") do
|
|
20
|
+
_{client.exchanges_list}.must_raise(Eodhd::Error)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe "#exchange_symbol_list" do
|
|
26
|
+
it "returns parsed JSON array" do
|
|
27
|
+
VCR.use_cassette("client_exchange_symbol_list") do
|
|
28
|
+
result = client.exchange_symbol_list(exchange_code: 'AU')
|
|
29
|
+
_(result).must_be_kind_of(Array)
|
|
30
|
+
_(result.first['Code']).wont_be_nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "raises Eodhd::Error on failure" do
|
|
35
|
+
VCR.use_cassette("client_exchange_symbol_list_401_error") do
|
|
36
|
+
_{client.exchange_symbol_list(exchange_code: 'AU')}.must_raise(Eodhd::Error)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe "#eod_data" do
|
|
42
|
+
it "returns parsed JSON array" do
|
|
43
|
+
VCR.use_cassette('client_eod_data') do
|
|
44
|
+
result = client.eod_data(exchange_id: 'AU', symbol: 'BHP', period: 'd')
|
|
45
|
+
_(result).must_be_kind_of(Array)
|
|
46
|
+
_(result.first.keys).must_equal(%w{date open high low close adjusted_close volume})
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "raises Eodhd::Error on failure" do
|
|
51
|
+
VCR.use_cassette('client_eod_data_401_error') do
|
|
52
|
+
_{client.eod_data(exchange_id: 'AU', symbol: 'BHP', period: 'd')}.must_raise(Eodhd::Error)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "#eod_bulk_last_day" do
|
|
58
|
+
it "returns parsed JSON array" do
|
|
59
|
+
VCR.use_cassette("client_eod_bulk_last_day") do
|
|
60
|
+
result = client.eod_bulk_last_day(exchange_id: 'AU', date: "2024-09-30")
|
|
61
|
+
_(result).must_be_kind_of(Array)
|
|
62
|
+
_(result.first.keys).must_equal(%w{code exchange_short_name date open high low close adjusted_close volume})
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "raises Eodhd::Error on failure" do
|
|
67
|
+
VCR.use_cassette('client_eod_bulk_last_day_401_error') do
|
|
68
|
+
_{client.eod_bulk_last_day(exchange_id: 'AU', date: "2024-09-30")}.must_raise(Eodhd::Error)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "#intraday" do
|
|
74
|
+
it "returns parsed JSON array" do
|
|
75
|
+
VCR.use_cassette('client_intraday') do
|
|
76
|
+
result = client.intraday(exchange_code: 'US', symbol: 'AAPL', interval: '5m')
|
|
77
|
+
_(result).must_be_kind_of(Array)
|
|
78
|
+
_(result.first.keys).must_equal(%w{timestamp gmtoffset datetime open high low close volume})
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "raises Eodhd::Error on failure" do
|
|
83
|
+
VCR.use_cassette('client_intraday_401_error') do
|
|
84
|
+
_{client.intraday(exchange_code: 'US', symbol: 'AAPL', interval: '5m')}.must_raise(Eodhd::Error)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe "#fundamentals" do
|
|
90
|
+
it "returns parsed JSON hash" do
|
|
91
|
+
VCR.use_cassette('client_fundamentals') do
|
|
92
|
+
result = client.fundamentals(exchange_code: 'US', symbol: 'AAPL')
|
|
93
|
+
_(result).must_be_kind_of(Hash)
|
|
94
|
+
_(result['General']).wont_be_nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "supports filter parameter" do
|
|
99
|
+
VCR.use_cassette('client_fundamentals_filtered') do
|
|
100
|
+
result = client.fundamentals(exchange_code: 'US', symbol: 'AAPL', filter: 'General')
|
|
101
|
+
_(result).must_be_kind_of(Hash)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "raises Eodhd::Error on failure" do
|
|
106
|
+
VCR.use_cassette('client_fundamentals_401_error') do
|
|
107
|
+
_{client.fundamentals(exchange_code: 'US', symbol: 'AAPL')}.must_raise(Eodhd::Error)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|