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,163 @@
|
|
|
1
|
+
# Eodhd/Fundamentals_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../helper'
|
|
4
|
+
|
|
5
|
+
describe Eodhd::Fundamentals do
|
|
6
|
+
let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
|
|
7
|
+
let(:fd){Eodhd::Fundamentals.all(api_token: api_token, exchange_code: 'US', symbol: 'AAPL')}
|
|
8
|
+
|
|
9
|
+
describe ".all" do
|
|
10
|
+
it "returns an Eodhd::Fundamentals object" do
|
|
11
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
12
|
+
_(fd).must_be_kind_of(Eodhd::Fundamentals)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "stores exchange_code and symbol" do
|
|
17
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
18
|
+
_(fd.exchange_code).must_equal('US')
|
|
19
|
+
_(fd.symbol).must_equal('AAPL')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "returns General as an Eodhd::Fundamentals::General" do
|
|
24
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
25
|
+
_(fd.general).must_be_kind_of(Eodhd::Fundamentals::General)
|
|
26
|
+
_(fd.general.code).must_equal('AAPL')
|
|
27
|
+
_(fd.general.name).wont_be_nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "returns Highlights as an Eodhd::Fundamentals::Highlights" do
|
|
32
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
33
|
+
_(fd.highlights).must_be_kind_of(Eodhd::Fundamentals::Highlights)
|
|
34
|
+
_(fd.highlights.market_capitalization).wont_be_nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "returns Valuation as an Eodhd::Fundamentals::Valuation" do
|
|
39
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
40
|
+
_(fd.valuation).must_be_kind_of(Eodhd::Fundamentals::Valuation)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "returns Financials as an Eodhd::Fundamentals::Financials" do
|
|
45
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
46
|
+
_(fd.financials).must_be_kind_of(Eodhd::Fundamentals::Financials)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "returns Financials with typed income_statement, balance_sheet, and cash_flow" do
|
|
51
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
52
|
+
_(fd.financials.income_statement).must_be_kind_of(Eodhd::Fundamentals::Financials::IncomeStatement)
|
|
53
|
+
_(fd.financials.balance_sheet).must_be_kind_of(Eodhd::Fundamentals::Financials::BalanceSheet)
|
|
54
|
+
_(fd.financials.cash_flow).must_be_kind_of(Eodhd::Fundamentals::Financials::CashFlow)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "returns quarterly and yearly as sorted arrays of typed entries" do
|
|
59
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
60
|
+
yearly = fd.financials.balance_sheet.yearly
|
|
61
|
+
_(yearly).must_be_kind_of(Array)
|
|
62
|
+
_(yearly.first).must_be_kind_of(Eodhd::Fundamentals::Financials::BalanceSheet::Entry)
|
|
63
|
+
_(fd.financials.balance_sheet.quarterly).must_be_kind_of(Array)
|
|
64
|
+
dates = yearly.map(&:date)
|
|
65
|
+
_(dates).must_equal(dates.sort)
|
|
66
|
+
_(dates.first).must_equal('1985-09-30')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "resolves interpreted and typo-corrected balance sheet accessors" do
|
|
71
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
72
|
+
latest = fd.financials.balance_sheet.yearly.last
|
|
73
|
+
_(latest.date).must_equal('2025-09-30')
|
|
74
|
+
_(latest.total_liabilities).must_equal('285508000000.00') # totalLiab
|
|
75
|
+
_(latest.capital_surplus).must_equal('93567852270.00') # capitalSurpluse (misspelling corrected)
|
|
76
|
+
_(latest.non_current_assets_other).must_equal('62950000000.00') # nonCurrrentAssetsOther (typo corrected)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "expands abbreviated income statement accessors" do
|
|
81
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
82
|
+
latest = fd.financials.income_statement.yearly.last
|
|
83
|
+
_(latest.net_income_from_continuing_operations).must_equal('112010000000.00') # netIncomeFromContinuingOps
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "returns Earnings as an Eodhd::Fundamentals::Earnings" do
|
|
88
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
89
|
+
_(fd.earnings).must_be_kind_of(Eodhd::Fundamentals::Earnings)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "returns Earnings annual, history, and trend as arrays of typed entries" do
|
|
94
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
95
|
+
_(fd.earnings.annual.first).must_be_kind_of(Eodhd::Fundamentals::Earnings::AnnualEntry)
|
|
96
|
+
_(fd.earnings.history.first).must_be_kind_of(Eodhd::Fundamentals::Earnings::HistoryEntry)
|
|
97
|
+
_(fd.earnings.trend.first).must_be_kind_of(Eodhd::Fundamentals::Earnings::TrendEntry)
|
|
98
|
+
dates = fd.earnings.annual.map(&:date)
|
|
99
|
+
_(dates).must_equal(dates.sort)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "returns Holders institutions and funds as arrays of typed entries" do
|
|
104
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
105
|
+
_(fd.holders.institutions).must_be_kind_of(Array)
|
|
106
|
+
_(fd.holders.funds).must_be_kind_of(Array)
|
|
107
|
+
institution = fd.holders.institutions.first
|
|
108
|
+
_(institution).must_be_kind_of(Eodhd::Fundamentals::Holders::Institution)
|
|
109
|
+
_(institution.name).must_equal('Vanguard Group Inc')
|
|
110
|
+
_(institution.change_percent).must_equal(1.9191) # change_p
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "returns insider_transactions as a sorted array of InsiderTransaction" do
|
|
115
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
116
|
+
_(fd.insider_transactions).must_be_kind_of(Array)
|
|
117
|
+
_(fd.insider_transactions.first).must_be_kind_of(Eodhd::Fundamentals::InsiderTransaction)
|
|
118
|
+
dates = fd.insider_transactions.map(&:date)
|
|
119
|
+
_(dates).must_equal(dates.sort)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "returns outstanding_shares as OutstandingShares with typed annual and quarterly arrays" do
|
|
124
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
125
|
+
_(fd.outstanding_shares).must_be_kind_of(Eodhd::Fundamentals::OutstandingShares)
|
|
126
|
+
_(fd.outstanding_shares.annual.first).must_be_kind_of(Eodhd::Fundamentals::OutstandingShares::Entry)
|
|
127
|
+
_(fd.outstanding_shares.quarterly.first).must_be_kind_of(Eodhd::Fundamentals::OutstandingShares::Entry)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "returns SharesStats as an Eodhd::Fundamentals::SharesStats" do
|
|
132
|
+
VCR.use_cassette('fundamentals_aapl') do
|
|
133
|
+
_(fd.shares_stats).must_be_kind_of(Eodhd::Fundamentals::SharesStats)
|
|
134
|
+
_(fd.shares_stats.shares_outstanding).wont_be_nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe ".all with an ASX symbol" do
|
|
140
|
+
let(:fd){Eodhd::Fundamentals.all(api_token: api_token, exchange_code: 'AU', symbol: 'CBA')}
|
|
141
|
+
|
|
142
|
+
it "wraps a real AU fundamentals payload into the same typed objects" do
|
|
143
|
+
VCR.use_cassette('fundamentals_cba_au') do
|
|
144
|
+
_(fd).must_be_kind_of(Eodhd::Fundamentals)
|
|
145
|
+
_(fd.exchange_code).must_equal('AU')
|
|
146
|
+
_(fd.symbol).must_equal('CBA')
|
|
147
|
+
_(fd.general).must_be_kind_of(Eodhd::Fundamentals::General)
|
|
148
|
+
|
|
149
|
+
_(fd.financials.balance_sheet).must_be_kind_of(Eodhd::Fundamentals::Financials::BalanceSheet)
|
|
150
|
+
yearly = fd.financials.balance_sheet.yearly
|
|
151
|
+
_(yearly).must_be_kind_of(Array)
|
|
152
|
+
_(yearly).wont_be_empty
|
|
153
|
+
_(yearly.first).must_be_kind_of(Eodhd::Fundamentals::Financials::BalanceSheet::Entry)
|
|
154
|
+
dates = yearly.map(&:date)
|
|
155
|
+
_(dates).must_equal(dates.sort)
|
|
156
|
+
|
|
157
|
+
_(fd.holders.institutions).must_be_kind_of(Array)
|
|
158
|
+
_(fd.insider_transactions).must_be_kind_of(Array)
|
|
159
|
+
_(fd.outstanding_shares).must_be_kind_of(Eodhd::Fundamentals::OutstandingShares)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# test/Eodhd/VERSION_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../helper'
|
|
4
|
+
|
|
5
|
+
describe Eodhd do
|
|
6
|
+
describe "VERSION" do
|
|
7
|
+
it "is a string" do
|
|
8
|
+
_(Eodhd::VERSION).must_be_instance_of String
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "is three numbers separated by dots" do
|
|
12
|
+
_(Eodhd::VERSION).must_match(/\A\d+\.\d+\.\d+\z/)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "matches the newest entry in the CHANGELOG" do
|
|
16
|
+
changelog = File.read(File.expand_path('../../CHANGELOG', __dir__))
|
|
17
|
+
_(changelog[/^(\d+\.\d+\.\d+):/, 1]).must_equal Eodhd::VERSION
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Eodhd/Validations_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../helper'
|
|
4
|
+
|
|
5
|
+
class DummyIncludingValidations
|
|
6
|
+
include Eodhd::Validations
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe Eodhd::Validations do
|
|
10
|
+
let(:v){DummyIncludingValidations.new}
|
|
11
|
+
|
|
12
|
+
describe "#validate_arguments" do
|
|
13
|
+
it "accepts all valid arguments" do
|
|
14
|
+
_{v.validate_arguments(
|
|
15
|
+
exchange_code: 'AU',
|
|
16
|
+
symbol: 'BHP',
|
|
17
|
+
period: 'd',
|
|
18
|
+
from: '2024-01-01',
|
|
19
|
+
to: '2024-12-31'
|
|
20
|
+
)}.must_be_silent
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "accepts exchange_id as alias for exchange_code" do
|
|
24
|
+
_{v.validate_arguments(exchange_id: 'AU')}.must_be_silent
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "raises ArgumentError for invalid exchange_code" do
|
|
28
|
+
_{v.validate_arguments(exchange_code: 'au')}.must_raise(ArgumentError)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "raises ArgumentError for invalid symbol" do
|
|
32
|
+
_{v.validate_arguments(symbol: 'bad$')}.must_raise(ArgumentError)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "raises ArgumentError for invalid period" do
|
|
36
|
+
_{v.validate_arguments(period: 'h')}.must_raise(ArgumentError)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "raises ArgumentError for invalid from date" do
|
|
40
|
+
_{v.validate_arguments(from: '2024/01/01')}.must_raise(ArgumentError)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "raises ArgumentError for invalid to date" do
|
|
44
|
+
_{v.validate_arguments(to: '2024-02-30')}.must_raise(ArgumentError)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "raises ArgumentError for reversed date range" do
|
|
48
|
+
_{v.validate_arguments(from: '2024-12-31', to: '2024-01-01')}.must_raise(ArgumentError)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "accepts nil values for all arguments" do
|
|
52
|
+
_{v.validate_arguments}.must_be_silent
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "#validate_exchange_code" do
|
|
57
|
+
it "accepts valid exchange codes" do
|
|
58
|
+
_{v.validate_exchange_code('AU')}.must_be_silent
|
|
59
|
+
_{v.validate_exchange_code('XETRA')}.must_be_silent
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "rejects invalid exchange codes" do
|
|
63
|
+
_{v.validate_exchange_code('us')}.must_raise(ArgumentError)
|
|
64
|
+
_{v.validate_exchange_code('U')}.must_raise(ArgumentError)
|
|
65
|
+
_{v.validate_exchange_code('TOOLONG')}.must_raise(ArgumentError)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe "#validate_symbol" do
|
|
70
|
+
it "accepts valid symbols" do
|
|
71
|
+
_{v.validate_symbol('AAPL')}.must_be_silent
|
|
72
|
+
_{v.validate_symbol('BRK.B')}.must_be_silent
|
|
73
|
+
_{v.validate_symbol('RDS-A')}.must_be_silent
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "rejects invalid symbols" do
|
|
77
|
+
_{v.validate_symbol('')}.must_raise(ArgumentError)
|
|
78
|
+
_{v.validate_symbol(' ')}.must_raise(ArgumentError)
|
|
79
|
+
_{v.validate_symbol('THIS_SYMBOL_IS_TOO_LONG')}.must_raise(ArgumentError)
|
|
80
|
+
_{v.validate_symbol('bad$')}.must_raise(ArgumentError)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe "#validate_period" do
|
|
85
|
+
it "accepts valid periods" do
|
|
86
|
+
_{v.validate_period('d')}.must_be_silent
|
|
87
|
+
_{v.validate_period('w')}.must_be_silent
|
|
88
|
+
_{v.validate_period('m')}.must_be_silent
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "rejects invalid periods" do
|
|
92
|
+
_{v.validate_period('h')}.must_raise(ArgumentError)
|
|
93
|
+
_{v.validate_period('')}.must_raise(ArgumentError)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe "#validate_interval" do
|
|
98
|
+
it "accepts valid intervals" do
|
|
99
|
+
_{v.validate_interval('1m')}.must_be_silent
|
|
100
|
+
_{v.validate_interval('5m')}.must_be_silent
|
|
101
|
+
_{v.validate_interval('1h')}.must_be_silent
|
|
102
|
+
_{v.validate_interval('1d')}.must_be_silent
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "rejects invalid intervals" do
|
|
106
|
+
_{v.validate_interval('2m')}.must_raise(ArgumentError)
|
|
107
|
+
_{v.validate_interval('invalid')}.must_raise(ArgumentError)
|
|
108
|
+
_{v.validate_interval('')}.must_raise(ArgumentError)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe "#validate_date" do
|
|
113
|
+
it "accepts valid date formats and Date objects" do
|
|
114
|
+
_{v.validate_date('2024-09-30')}.must_be_silent
|
|
115
|
+
_{v.validate_date(Date.new(2024,9,30))}.must_be_silent
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "rejects invalid dates and formats" do
|
|
119
|
+
_{v.validate_date('2024/09/30')}.must_raise(ArgumentError)
|
|
120
|
+
_{v.validate_date('2024-02-30')}.must_raise(ArgumentError)
|
|
121
|
+
_{v.validate_date(12345)}.must_raise(ArgumentError)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe "#validate_date_range" do
|
|
126
|
+
it "accepts valid date ranges" do
|
|
127
|
+
_{v.validate_date_range('2024-01-01','2024-12-31')}.must_be_silent
|
|
128
|
+
_{v.validate_date_range(Date.new(2024,1,1),Date.new(2024,12,31))}.must_be_silent
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "rejects reversed date ranges" do
|
|
132
|
+
_{v.validate_date_range('2024-12-31','2024-01-01')}.must_raise(ArgumentError)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Eodhd/WebSocketClient_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../helper'
|
|
4
|
+
|
|
5
|
+
describe Eodhd::WebSocketClient do
|
|
6
|
+
let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
|
|
7
|
+
let(:consumer){->(data){}}
|
|
8
|
+
|
|
9
|
+
describe "#initialize" do
|
|
10
|
+
it "accepts array of symbols and formats to comma-separated string" do
|
|
11
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: ['AAPL', 'MSFT'], consumer: consumer)
|
|
12
|
+
_(client.symbols).must_equal('AAPL,MSFT')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "accepts string of symbols" do
|
|
16
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL,MSFT', consumer: consumer)
|
|
17
|
+
_(client.symbols).must_equal('AAPL,MSFT')
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe "#url" do
|
|
22
|
+
it "generates correct URL for US trade stream" do
|
|
23
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL', consumer: consumer)
|
|
24
|
+
_(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/us?api_token=#{api_token}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "generates correct URL for US quote stream" do
|
|
28
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us-quote', symbols: 'AAPL', consumer: consumer)
|
|
29
|
+
_(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/us-quote?api_token=#{api_token}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "generates correct URL for forex stream" do
|
|
33
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'forex', symbols: 'EURUSD', consumer: consumer)
|
|
34
|
+
_(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/forex?api_token=#{api_token}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "generates correct URL for crypto stream" do
|
|
38
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'crypto', symbols: 'BTC-USD', consumer: consumer)
|
|
39
|
+
_(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/crypto?api_token=#{api_token}")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "#format_symbols" do
|
|
44
|
+
it "converts array to comma-separated string" do
|
|
45
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: ['AAPL', 'MSFT', 'GOOGL'], consumer: consumer)
|
|
46
|
+
_(client.symbols).must_equal('AAPL,MSFT,GOOGL')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "returns string as-is" do
|
|
50
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL,MSFT', consumer: consumer)
|
|
51
|
+
_(client.symbols).must_equal('AAPL,MSFT')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe "#run" do
|
|
56
|
+
it "configures Iodine and starts connection" do
|
|
57
|
+
client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL', consumer: consumer)
|
|
58
|
+
|
|
59
|
+
iodine_connect_called = false
|
|
60
|
+
iodine_start_called = false
|
|
61
|
+
|
|
62
|
+
Iodine.stub(:threads=, nil) do
|
|
63
|
+
Iodine.stub(:connect, ->(opts){iodine_connect_called = true}) do
|
|
64
|
+
Iodine.stub(:start, ->{iodine_start_called = true}) do
|
|
65
|
+
client.run
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
_(iodine_connect_called).must_equal(true)
|
|
71
|
+
_(iodine_start_called).must_equal(true)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "Handler" do
|
|
76
|
+
describe "#on_message" do
|
|
77
|
+
it "parses JSON and calls consumer" do
|
|
78
|
+
received_data = nil
|
|
79
|
+
test_consumer = ->(data){received_data = data}
|
|
80
|
+
handler = Eodhd::WebSocketClient::Handler.new(symbols: 'AAPL', consumer: test_consumer, logger: nil)
|
|
81
|
+
|
|
82
|
+
message = '{"symbol":"AAPL","price":150.0}'
|
|
83
|
+
handler.on_message(nil, message)
|
|
84
|
+
|
|
85
|
+
_(received_data).must_be_kind_of(Hash)
|
|
86
|
+
_(received_data['symbol']).must_equal('AAPL')
|
|
87
|
+
_(received_data['price']).must_equal(150.0)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "raises Eodhd::Error on invalid JSON" do
|
|
91
|
+
test_consumer = ->(data){}
|
|
92
|
+
handler = Eodhd::WebSocketClient::Handler.new(symbols: 'AAPL', consumer: test_consumer, logger: nil)
|
|
93
|
+
|
|
94
|
+
invalid_message = '{invalid json'
|
|
95
|
+
_{handler.on_message(nil, invalid_message)}.must_raise(Eodhd::Error)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/test/Eodhd_test.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Eodhd_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative "helper"
|
|
4
|
+
|
|
5
|
+
describe Eodhd do
|
|
6
|
+
let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
|
|
7
|
+
let(:eodhd){Eodhd.new(api_token: api_token)}
|
|
8
|
+
|
|
9
|
+
describe "#exchanges" do
|
|
10
|
+
it "delegates to Eodhd::Exchange.all" do
|
|
11
|
+
VCR.use_cassette('eodhd_exchanges') do
|
|
12
|
+
exchanges = eodhd.exchanges
|
|
13
|
+
_(exchanges).must_be_kind_of(Array)
|
|
14
|
+
_(exchanges).wont_be_empty
|
|
15
|
+
_(exchanges.first).must_be_kind_of(Eodhd::Exchange)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "#exchange_symbols" do
|
|
21
|
+
it "delegates to Eodhd::ExchangeSymbol.all" do
|
|
22
|
+
VCR.use_cassette('eodhd_exchange_symbols') do
|
|
23
|
+
symbols = eodhd.exchange_symbols(exchange_code: 'AU')
|
|
24
|
+
_(symbols).must_be_kind_of(Array)
|
|
25
|
+
_(symbols).wont_be_empty
|
|
26
|
+
_(symbols.first).must_be_kind_of(Eodhd::ExchangeSymbol)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "#eod_data" do
|
|
32
|
+
it "delegates to Eodhd::EodData.all" do
|
|
33
|
+
VCR.use_cassette('eodhd_eod_data') do
|
|
34
|
+
eod_data = eodhd.eod_data(exchange_code: 'AU', symbol: 'BHP', period: 'd')
|
|
35
|
+
_(eod_data).must_be_kind_of(Array)
|
|
36
|
+
_(eod_data).wont_be_empty
|
|
37
|
+
_(eod_data.first).must_be_kind_of(Eodhd::EodData)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "#eod_bulk_last_day" do
|
|
43
|
+
it "delegates to Eodhd::EodBulkLastDay.all" do
|
|
44
|
+
VCR.use_cassette('eodhd_eod_bulk_last_day') do
|
|
45
|
+
bulk_last_day = eodhd.eod_bulk_last_day(exchange_code: 'AU', date: '2024-09-30')
|
|
46
|
+
_(bulk_last_day).must_be_kind_of(Array)
|
|
47
|
+
_(bulk_last_day).wont_be_empty
|
|
48
|
+
_(bulk_last_day.first).must_be_kind_of(Eodhd::EodBulkLastDay)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "#intraday" do
|
|
54
|
+
it "delegates to Eodhd::Intraday.all" do
|
|
55
|
+
VCR.use_cassette('eodhd_intraday') do
|
|
56
|
+
intraday_data = eodhd.intraday(exchange_code: 'US', symbol: 'AAPL', interval: '5m')
|
|
57
|
+
_(intraday_data).must_be_kind_of(Array)
|
|
58
|
+
_(intraday_data).wont_be_empty
|
|
59
|
+
_(intraday_data.first).must_be_kind_of(Eodhd::Intraday)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe "#fundamentals" do
|
|
65
|
+
it "delegates to Eodhd::Fundamentals.all" do
|
|
66
|
+
VCR.use_cassette('eodhd_fundamentals') do
|
|
67
|
+
fundamental_data = eodhd.fundamentals(exchange_code: 'US', symbol: 'AAPL')
|
|
68
|
+
_(fundamental_data).must_be_kind_of(Eodhd::Fundamentals)
|
|
69
|
+
_(fundamental_data.general).must_be_kind_of(Eodhd::Fundamentals::General)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/test/helper.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# helper.rb
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'minitest/mock'
|
|
5
|
+
require 'minitest/spec'
|
|
6
|
+
require 'minitest-spec-context'
|
|
7
|
+
require 'vcr'
|
|
8
|
+
require 'webmock/minitest'
|
|
9
|
+
|
|
10
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
11
|
+
require 'Eodhd'
|
|
12
|
+
|
|
13
|
+
VCR.configure do |config|
|
|
14
|
+
config.cassette_library_dir = File.expand_path('./fixtures/vcr_cassettes', __dir__)
|
|
15
|
+
config.hook_into :webmock
|
|
16
|
+
config.filter_sensitive_data('<API_TOKEN>'){ENV['EODHD_API_TOKEN']}
|
|
17
|
+
end
|