polygonio-ruby 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polygonio
4
+ module Rest
5
+ module Reference
6
+ class Tickers < PolygonRestHandler
7
+ class Ticker < PolygonResponse
8
+ attribute :ticker, Types::String
9
+ attribute :name, Types::String
10
+ attribute :market, Types::String
11
+ attribute :locale, Types::String
12
+ attribute :currency, Types::String
13
+ attribute :active, Types::Bool
14
+ attribute :primary_exch?, Types::String
15
+ attribute? :type, Types::String
16
+ attribute? :codes do
17
+ attribute? :cik, Types::String
18
+ attribute? :figiuid, Types::String
19
+ attribute? :scfigi, Types::String
20
+ attribute? :cfigi, Types::String
21
+ attribute? :figi, Types::String
22
+ end
23
+ attribute :updated, Types::JSON::Date
24
+ attribute :url, Types::String
25
+ end
26
+
27
+ class TickerResponse < PolygonResponse
28
+ attribute :page, Types::Integer
29
+ attribute :per_page, Types::Integer
30
+ attribute :count, Types::Integer
31
+ attribute :status, Types::String
32
+ attribute :tickers, Types::Array.of(Ticker)
33
+ end
34
+
35
+ class TickersParameters < Dry::Struct
36
+ attribute? :sort, Types::String
37
+ attribute? :type, Types::String
38
+ attribute? :market, Types::String
39
+ attribute? :locale, Types::String
40
+ attribute? :search, Types::String
41
+ attribute? :perpage, Types::String
42
+ attribute? :page, Types::Integer
43
+ attribute? :active, Types::Bool
44
+ end
45
+
46
+ def list(params = {})
47
+ params = TickersParameters[params]
48
+ res = client.request.get("/v2/reference/tickers", params.to_h)
49
+ TickerResponse[res.body]
50
+ end
51
+
52
+ class TickerTypesResponse < PolygonResponse
53
+ attribute :status, Types::String
54
+ attribute :results do
55
+ attribute :types, Types::Hash
56
+ attribute :index_types, Types::Hash
57
+ end
58
+ end
59
+
60
+ def types
61
+ res = client.request.get("/v2/reference/types")
62
+ TickerTypesResponse[res.body]
63
+ end
64
+
65
+ class TickerDetailsResponse < PolygonResponse
66
+ attribute? :logo, Types::String
67
+ attribute :exchange, Types::String
68
+ attribute :exchange_symbol, Types::String
69
+ attribute :name, Types::String
70
+ attribute :symbol, Types::String
71
+ attribute? :listdate, Types::JSON::Date
72
+ attribute? :cik, Types::String
73
+ attribute? :bloomberg, Types::String
74
+ attribute? :figi, Types::String.optional
75
+ attribute? :lie, Types::String
76
+ attribute? :sic, Types::Integer
77
+ attribute? :country, Types::String
78
+ attribute? :industry, Types::String
79
+ attribute? :sector, Types::String
80
+ attribute? :marketcap, Types::Integer
81
+ attribute? :employees, Types::Integer
82
+ attribute? :phone, Types::String
83
+ attribute? :ceo, Types::String
84
+ attribute? :url, Types::String
85
+ attribute? :description, Types::String
86
+ attribute? :similar, Types::Array
87
+ attribute? :tags, Types::Array
88
+ attribute? :hq_address, Types::String
89
+ attribute? :hq_state, Types::String
90
+ attribute? :hq_country, Types::String
91
+ attribute? :active, Types::Bool
92
+ attribute? :updated, Types::String
93
+ end
94
+
95
+ def details(symbol)
96
+ symbol = Types::String[symbol]
97
+ res = client.request.get("/v1/meta/symbols/#{symbol}/company")
98
+ TickerDetailsResponse[res.body]
99
+ end
100
+
101
+ class NewsResponse < PolygonResponse
102
+ attribute :symbols, Types::Array.of(Types::String)
103
+ attribute :title, Types::String
104
+ attribute :url, Types::String
105
+ attribute :source, Types::String
106
+ attribute :summary, Types::String
107
+ attribute? :image, Types::String
108
+ attribute :timestamp, Types::JSON::DateTime
109
+ attribute :keywords, Types::Array.of(Types::String)
110
+ end
111
+
112
+ def news(symbol, page = 1, perpage = 50)
113
+ symbol = Types::String[symbol]
114
+ page = Types::Integer[page]
115
+ perpage = Types::Integer[perpage]
116
+ opts = { page: page, perpage: perpage }
117
+
118
+ res = client.request.get("/v1/meta/symbols/#{symbol}/news", opts)
119
+ Types::Array.of(NewsResponse)[res.body]
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polygonio
4
+ module Rest
5
+ class Stocks < PolygonRestHandler
6
+ class StockExchange < PolygonResponse
7
+ attribute :id, Types::Integer
8
+ attribute :type, Types::String
9
+ attribute :market, Types::String
10
+ attribute? :mic, Types::String
11
+ attribute :name, Types::String
12
+ attribute? :tape, Types::String
13
+ end
14
+
15
+ def list_exchanges
16
+ res = client.request.get("/v1/meta/exchanges")
17
+ Types::Array.of(StockExchange)[res.body]
18
+ end
19
+
20
+ class HistoricTradesResponse < PolygonResponse
21
+ attribute :results_count, Types::Integer
22
+ attribute :db_latency, Types::Integer
23
+ attribute :success, Types::Bool
24
+ attribute :ticker, Types::String
25
+ attribute :map, Types::Hash
26
+ attribute :results, Types::Array do
27
+ attribute? :T, Types::String # Not receiving for some reason
28
+ attribute :t, Types::Integer
29
+ attribute :y, Types::Integer
30
+ attribute? :f, Types::Integer
31
+ attribute :q, Types::Integer
32
+ attribute :i, Types::String
33
+ attribute :x, Types::Integer
34
+ attribute :s, Types::Integer
35
+ attribute :c, Types::Array.of(Types::Integer)
36
+ attribute :p, Types::JSON::Decimal
37
+ attribute :z, Types::Integer
38
+ end
39
+ end
40
+
41
+ class HistoricParams < Dry::Struct
42
+ attribute? :timestamp, Types::Integer
43
+ attribute? :timestampLimit, Types::Integer # TODO: change to underscore?
44
+ attribute? :reverse, Types::Bool
45
+ attribute? :limit, Types::Integer
46
+ end
47
+
48
+ def historic_trades(ticker, date, params = {})
49
+ ticker = Types::String[ticker]
50
+ date = Types::JSON::Date[date]
51
+ params = HistoricParams[params]
52
+
53
+ res = client.request.get("/v2/ticks/stocks/trades/#{ticker}/#{date}", params.to_h)
54
+ HistoricTradesResponse[res.body]
55
+ end
56
+
57
+ class HistoricQuotesResponse < PolygonResponse
58
+ attribute :results_count, Types::Integer
59
+ attribute :db_latency, Types::Integer
60
+ attribute :success, Types::Bool
61
+ attribute :ticker, Types::String
62
+ attribute :map, Types::Hash
63
+ attribute :results, Types::Array do
64
+ attribute? :T, Types::String # Not receiving for some reason
65
+ attribute :t, Types::Integer
66
+ attribute :y, Types::Integer
67
+ attribute? :f, Types::Integer
68
+ attribute :q, Types::Integer
69
+ attribute? :i, Types::Array.of(Types::Integer)
70
+ attribute :p, Types::JSON::Decimal
71
+ attribute :x, Types::Integer
72
+ attribute :s, Types::Integer
73
+ attribute? :P, Types::JSON::Decimal
74
+ attribute? :X, Types::Integer
75
+ attribute? :S, Types::Integer
76
+ attribute :c, Types::Array.of(Types::Integer)
77
+ attribute :z, Types::Integer
78
+ end
79
+ end
80
+
81
+ def historic_quotes(ticker, date, params = {})
82
+ ticker = Types::String[ticker]
83
+ date = Types::JSON::Date[date]
84
+ params = HistoricParams[params]
85
+
86
+ res = client.request.get("/v2/ticks/stocks/nbbo/#{ticker}/#{date}", params.to_h)
87
+ HistoricQuotesResponse[res.body]
88
+ end
89
+
90
+ class LastTradeResponse < PolygonResponse
91
+ attribute :status, Types::String
92
+ attribute :symbol, Types::String
93
+ attribute :last do
94
+ attribute :price, Types::JSON::Decimal
95
+ attribute :size, Types::Integer
96
+ attribute :exchange, Types::Integer
97
+ attribute :cond1, Types::Integer
98
+ attribute :cond2, Types::Integer
99
+ attribute :cond3, Types::Integer
100
+ attribute? :cond4, Types::Integer
101
+ attribute :timestamp, Types::Integer
102
+ end
103
+ end
104
+
105
+ def last_trade(symbol)
106
+ symbol = Types::String[symbol]
107
+
108
+ res = client.request.get("/v1/last/stocks/#{symbol}")
109
+ LastTradeResponse[res.body]
110
+ end
111
+
112
+ class LastQuoteResponse < PolygonResponse
113
+ attribute :status, Types::String
114
+ attribute :symbol, Types::String
115
+ attribute :last do
116
+ attribute :askprice, Types::JSON::Decimal
117
+ attribute :asksize, Types::Integer
118
+ attribute :askexchange, Types::Integer
119
+ attribute :bidprice, Types::JSON::Decimal
120
+ attribute :bidsize, Types::Integer
121
+ attribute :bidexchange, Types::Integer
122
+ attribute :timestamp, Types::Integer
123
+ end
124
+ end
125
+
126
+ def last_quote(symbol)
127
+ symbol = Types::String[symbol]
128
+
129
+ res = client.request.get("/v1/last_quote/stocks/#{symbol}")
130
+ LastQuoteResponse[res.body]
131
+ end
132
+
133
+ class DailyOpenCloseResponse < PolygonResponse
134
+ attribute :status, Types::String
135
+ attribute :symbol, Types::String
136
+ attribute :open, Types::JSON::Decimal
137
+ attribute :high, Types::JSON::Decimal
138
+ attribute :low, Types::JSON::Decimal
139
+ attribute :close, Types::JSON::Decimal
140
+ attribute :volume, Types::Integer
141
+ attribute :after_hours, Types::JSON::Decimal
142
+ attribute :from, Types::JSON::DateTime
143
+ end
144
+
145
+ def daily_open_close(symbol, date)
146
+ symbol = Types::String[symbol]
147
+ date = Types::JSON::Date[date]
148
+
149
+ res = client.request.get("/v1/open-close/#{symbol}/#{date}")
150
+ DailyOpenCloseResponse[res.body]
151
+ end
152
+
153
+ def condition_mappings(tick_type)
154
+ tick_type = Types::String.enum("trades", "quotes")[tick_type]
155
+
156
+ res = client.request.get("/v1/meta/conditions/#{tick_type}")
157
+ Types::Hash[res.body]
158
+ end
159
+
160
+ class SnapshotTicker < PolygonResponse
161
+ attribute :ticker, Types::String
162
+ attribute :day do
163
+ attribute :c, Types::JSON::Decimal
164
+ attribute :h, Types::JSON::Decimal
165
+ attribute :l, Types::JSON::Decimal
166
+ attribute :o, Types::JSON::Decimal
167
+ attribute :v, Types::JSON::Decimal
168
+ end
169
+ attribute :last_trade do
170
+ attribute? :c1, Types::Integer
171
+ attribute? :c2, Types::Integer
172
+ attribute? :c3, Types::Integer
173
+ attribute? :c4, Types::Integer
174
+ attribute? :e, Types::Integer
175
+ attribute :p, Types::JSON::Decimal
176
+ attribute :s, Types::Integer
177
+ attribute :t, Types::Integer
178
+ end
179
+ attribute :last_quote do
180
+ attribute :p, Types::JSON::Decimal
181
+ attribute :s, Types::Integer
182
+ attribute? :P, Types::JSON::Decimal
183
+ attribute? :S, Types::Integer
184
+ attribute :t, Types::Integer
185
+ end
186
+ attribute :min do
187
+ attribute :c, Types::JSON::Decimal
188
+ attribute :h, Types::JSON::Decimal
189
+ attribute :l, Types::JSON::Decimal
190
+ attribute :o, Types::JSON::Decimal
191
+ attribute :v, Types::JSON::Decimal
192
+ end
193
+ attribute :prev_day do
194
+ attribute :c, Types::JSON::Decimal
195
+ attribute :h, Types::JSON::Decimal
196
+ attribute :l, Types::JSON::Decimal
197
+ attribute :o, Types::JSON::Decimal
198
+ attribute :v, Types::JSON::Decimal
199
+ end
200
+ attribute :todays_change, Types::JSON::Decimal
201
+ attribute :todays_change_perc, Types::JSON::Decimal
202
+ attribute :updated, Types::Integer
203
+ end
204
+
205
+ class FullSnapshotResponse < PolygonResponse
206
+ attribute :status, Types::String
207
+ attribute :tickers, Types::Array.of(SnapshotTicker)
208
+ end
209
+
210
+ def full_snapshot
211
+ res = client.request.get("/v2/snapshot/locale/us/markets/stocks/tickers")
212
+ FullSnapshotResponse[res.body]
213
+ end
214
+
215
+ class SnapshotResponse < PolygonResponse
216
+ attribute :status, Types::String
217
+ attribute :ticker, SnapshotTicker
218
+ end
219
+
220
+ def snapshot(ticker)
221
+ ticker = Types::String[ticker]
222
+
223
+ res = client.request.get("/v2/snapshot/locale/us/markets/stocks/tickers/#{ticker}")
224
+ SnapshotResponse[res.body]
225
+ end
226
+
227
+ class SnapshotGainersLosersResponse < PolygonResponse
228
+ attribute :status, Types::String
229
+ attribute :tickers, Types::Array.of(SnapshotTicker)
230
+ end
231
+
232
+ def snapshot_gainers_losers(direction)
233
+ direction = Types::String.enum("gainers", "losers")[direction]
234
+
235
+ res = client.request.get("/v2/snapshot/locale/us/markets/stocks/#{direction}")
236
+ SnapshotGainersLosersResponse[res.body]
237
+ end
238
+
239
+ class PreviousCloseResponse < PolygonResponse
240
+ attribute :ticker, Types::String
241
+ attribute :status, Types::String
242
+ attribute :adjusted, Types::Bool
243
+ attribute :query_count, Types::Integer
244
+ attribute :results_count, Types::Integer
245
+ attribute :results, Types::Array do
246
+ attribute :T, Types::String
247
+ attribute :v, Types::JSON::Decimal
248
+ attribute :vw, Types::JSON::Decimal
249
+ attribute :o, Types::JSON::Decimal
250
+ attribute :c, Types::JSON::Decimal
251
+ attribute :h, Types::JSON::Decimal
252
+ attribute :l, Types::JSON::Decimal
253
+ attribute :t, Types::Integer
254
+ attribute? :n, Types::Integer
255
+ end
256
+ end
257
+
258
+ def previous_close(ticker, unadjusted = false)
259
+ ticker = Types::String[ticker]
260
+ unadjusted = Types::Bool[unadjusted]
261
+
262
+ res = client.request.get("/v2/aggs/ticker/#{ticker}/prev", { unadjusted: unadjusted })
263
+ PreviousCloseResponse[res.body]
264
+ end
265
+
266
+ class AggregatesResponse < PolygonResponse
267
+ attribute :ticker, Types::String
268
+ attribute :status, Types::String
269
+ attribute :adjusted, Types::Bool
270
+ attribute :query_count, Types::Integer
271
+ attribute :results_count, Types::Integer
272
+ attribute :results, Types::Array do
273
+ attribute? :T, Types::String # Not appearing
274
+ attribute :v, Types::JSON::Decimal
275
+ attribute? :vw, Types::JSON::Decimal
276
+ attribute :o, Types::JSON::Decimal
277
+ attribute :c, Types::JSON::Decimal
278
+ attribute :h, Types::JSON::Decimal
279
+ attribute :l, Types::JSON::Decimal
280
+ attribute :t, Types::Integer
281
+ attribute? :n, Types::Integer
282
+ end
283
+ end
284
+
285
+ def aggregates(ticker, multiplier, timespan, from, to, unadjusted = false) # rubocop:disable Metrics/ParameterLists
286
+ ticker = Types::String[ticker]
287
+ multiplier = Types::Integer[multiplier]
288
+ timespan = Types::Coercible::String.enum("minute", "hour", "day", "week", "month", "quarter", "year")[timespan]
289
+ from = Types::JSON::Date[from]
290
+ to = Types::JSON::Date[to]
291
+ unadjusted = Types::Bool[unadjusted]
292
+
293
+ res = client.request.get("/v2/aggs/ticker/#{ticker}/range/#{multiplier}/#{timespan}/#{from}/#{to}", { unadjusted: unadjusted })
294
+ AggregatesResponse[res.body]
295
+ end
296
+
297
+ class GroupedDailyResponse < PolygonResponse
298
+ attribute? :ticker, Types::String
299
+ attribute :status, Types::String
300
+ attribute :adjusted, Types::Bool
301
+ attribute :query_count, Types::Integer
302
+ attribute :results_count, Types::Integer
303
+ attribute :results, Types::Array do
304
+ attribute :T, Types::String # Not appearing
305
+ attribute :v, Types::JSON::Decimal
306
+ attribute? :vw, Types::JSON::Decimal
307
+ attribute :o, Types::JSON::Decimal
308
+ attribute :c, Types::JSON::Decimal
309
+ attribute :h, Types::JSON::Decimal
310
+ attribute :l, Types::JSON::Decimal
311
+ attribute :t, Types::Integer
312
+ attribute? :n, Types::Integer
313
+ end
314
+ end
315
+
316
+ def grouped_daily(locale, market, date, unadjusted = false)
317
+ locale = Types::String[locale]
318
+ market = Types::Coercible::String.enum("stocks", "crypto", "bonds", "mf", "mmf", "indices", "fx")[market]
319
+ date = Types::JSON::Date[date]
320
+ unadjusted = Types::Bool[unadjusted]
321
+
322
+ res = client.request.get("/v2/aggs/grouped/locale/#{locale}/market/#{market.upcase}/#{date}", { unadjusted: unadjusted })
323
+ GroupedDailyResponse[res.body]
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polygonio
4
+ module Rest
5
+ class PolygonResponse < Dry::Struct
6
+ NUMBERS_TO_WORDS = {
7
+ "10" => "ten",
8
+ "9" => "nine",
9
+ "8" => "eight",
10
+ "7" => "seven",
11
+ "6" => "six",
12
+ "5" => "five",
13
+ "4" => "four",
14
+ "3" => "three",
15
+ "2" => "two",
16
+ "1" => "one"
17
+ }.freeze
18
+
19
+ transform_keys do |k|
20
+ k = NUMBERS_TO_WORDS[k] if NUMBERS_TO_WORDS.key?(k)
21
+ k = k.underscore unless k.length == 1
22
+ k.to_sym
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ require_relative "api/crypto"
29
+ require_relative "api/forex"
30
+ require_relative "api/stocks"
31
+ require_relative "api/reference/locales"
32
+ require_relative "api/reference/markets"
33
+ require_relative "api/reference/stocks"
34
+ require_relative "api/reference/tickers"
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polygonio
4
+ module Rest
5
+ class Client
6
+ Struct.new("Reference", :locales, :markets, :stocks, :tickers)
7
+
8
+ BASE_URL = "https://api.polygon.io/"
9
+
10
+ attr_reader :url, :api_key
11
+
12
+ def initialize(api_key, &block)
13
+ @url = BASE_URL
14
+ @api_key = Types::String[api_key]
15
+ @request_builder = block if block_given?
16
+ end
17
+
18
+ RETRY_OPTIONS = {
19
+ max: 2,
20
+ interval: 0.05,
21
+ interval_randomness: 0.5,
22
+ backoff_factor: 2,
23
+ exceptions: [Faraday::ConnectionFailed].concat(Faraday::Request::Retry::DEFAULT_EXCEPTIONS)
24
+ }.freeze
25
+
26
+ def request
27
+ Faraday.new(url: "#{url}?apiKey=#{api_key}") do |builder|
28
+ builder.request :retry, RETRY_OPTIONS
29
+ builder.use ErrorMiddleware
30
+ @request_builder&.call(builder)
31
+ builder.request :json
32
+ builder.response :oj
33
+ builder.adapter Faraday.default_adapter
34
+ end
35
+ end
36
+
37
+ def reference
38
+ Struct::Reference.new(
39
+ Rest::Reference::Locales.new(self),
40
+ Rest::Reference::Markets.new(self),
41
+ Rest::Reference::Stocks.new(self),
42
+ Rest::Reference::Tickers.new(self)
43
+ )
44
+ end
45
+
46
+ def stocks
47
+ Rest::Stocks.new(self)
48
+ end
49
+
50
+ def forex
51
+ Rest::Forex.new(self)
52
+ end
53
+
54
+ def crypto
55
+ Rest::Crypto.new(self)
56
+ end
57
+ end
58
+
59
+ class PolygonRestHandler
60
+ attr_reader :client
61
+
62
+ def initialize(client)
63
+ @client = client
64
+ end
65
+ end
66
+
67
+ class PagingParameters < Dry::Struct
68
+ attribute? :offset, Types::Integer.optional
69
+ attribute? :limit, Types::Integer.default(100)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polygonio
4
+ module Errors
5
+ class PolygonRestClientError < StandardError; end
6
+
7
+ class BadRequestError < PolygonRestClientError; end
8
+ class UnauthorizedError < PolygonRestClientError; end
9
+ class ForbiddenError < PolygonRestClientError; end
10
+ class ResourceNotFoundError < PolygonRestClientError; end
11
+ class ServerError < PolygonRestClientError; end
12
+ class UnknownError < PolygonRestClientError; end
13
+ class UnexpectedResponseError < PolygonRestClientError; end
14
+ end
15
+
16
+ class ErrorMiddleware < Faraday::Middleware
17
+ CLIENT_ERROR_STATUSES = (400...500).freeze
18
+ SERVER_ERROR_STATUSES = (500...600).freeze
19
+
20
+ def on_complete(response_env) # rubocop:disable Metrics/MethodLength
21
+ status = response_env.status
22
+
23
+ case status
24
+ when 400
25
+ raise Errors::BadRequestError
26
+ when 401
27
+ raise Errors::UnauthorizedError
28
+ when 403
29
+ raise Errors::ForbiddenError
30
+ when 404
31
+ raise Errors::ResourceNotFoundError
32
+ when CLIENT_ERROR_STATUSES
33
+ raise Errors::UnknownError
34
+ when SERVER_ERROR_STATUSES
35
+ raise Errors::ServerError
36
+ end
37
+ end
38
+
39
+ def call(request_env)
40
+ @app.call(request_env).on_complete(&method(:on_complete))
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rest/client"
4
+ require_relative "rest/api"
5
+ require_relative "rest/errors"
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polygonio
4
+ module Types
5
+ include Dry::Types(default: :strict)
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polygonio
4
+ VERSION = "0.2.8"
5
+ end