polygonio-ruby 0.2.8

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.
@@ -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