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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +49 -0
- data/Gemfile +6 -0
- data/README.md +50 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/lib/polygonio/rest/api/crypto.rb +198 -0
- data/lib/polygonio/rest/api/forex.rb +129 -0
- data/lib/polygonio/rest/api/reference/locales.rb +22 -0
- data/lib/polygonio/rest/api/reference/markets.rb +55 -0
- data/lib/polygonio/rest/api/reference/stocks.rb +70 -0
- data/lib/polygonio/rest/api/reference/tickers.rb +124 -0
- data/lib/polygonio/rest/api/stocks.rb +327 -0
- data/lib/polygonio/rest/api.rb +34 -0
- data/lib/polygonio/rest/client.rb +72 -0
- data/lib/polygonio/rest/errors.rb +43 -0
- data/lib/polygonio/rest.rb +5 -0
- data/lib/polygonio/types.rb +7 -0
- data/lib/polygonio/version.rb +5 -0
- data/lib/polygonio/websocket/client.rb +152 -0
- data/lib/polygonio/websocket/errors.rb +20 -0
- data/lib/polygonio/websocket/events.rb +167 -0
- data/lib/polygonio/websocket.rb +5 -0
- data/lib/polygonio.rb +16 -0
- data/polygonio.gemspec +53 -0
- metadata +399 -0
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polygonio
|
4
|
+
module Websocket
|
5
|
+
module Connection
|
6
|
+
attr_accessor :url
|
7
|
+
|
8
|
+
def self.connect(api_key, url, channels, args = {}, &block)
|
9
|
+
raise "no onmessage callback block given" unless block_given?
|
10
|
+
|
11
|
+
uri = URI.parse(url)
|
12
|
+
args[:api_key] = api_key
|
13
|
+
args[:channels] = channels
|
14
|
+
args[:on_message] = block
|
15
|
+
EM.connect(uri.host, 443, self, args) do |conn|
|
16
|
+
conn.url = url
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(args)
|
21
|
+
@api_key = args.fetch(:api_key)
|
22
|
+
@channels = args.fetch(:channels)
|
23
|
+
@on_message = args.fetch(:on_message)
|
24
|
+
@debug = args.fetch(:debug) { false }
|
25
|
+
end
|
26
|
+
|
27
|
+
def connection_completed
|
28
|
+
@driver = WebSocket::Driver.client(self)
|
29
|
+
@driver.add_extension(PermessageDeflate)
|
30
|
+
|
31
|
+
uri = URI.parse(@url)
|
32
|
+
start_tls(sni_hostname: uri.host)
|
33
|
+
|
34
|
+
@driver.on(:open) do |_event|
|
35
|
+
msg = dump({ action: "auth", params: @api_key })
|
36
|
+
@driver.text(msg)
|
37
|
+
end
|
38
|
+
|
39
|
+
@driver.on(:message) do |msg|
|
40
|
+
p [:message, msg.data] if @debug
|
41
|
+
|
42
|
+
events = Oj.load(msg.data, symbol_keys: true).map do |event|
|
43
|
+
to_event(event)
|
44
|
+
end
|
45
|
+
|
46
|
+
status_events = events.select { |e| e.is_a? WebsocketEvent }
|
47
|
+
status_events.each do |event|
|
48
|
+
msg = handle_status_event(event)
|
49
|
+
@driver.text(msg) if msg
|
50
|
+
end
|
51
|
+
|
52
|
+
@on_message.call(events) if status_events.length.zero?
|
53
|
+
end
|
54
|
+
|
55
|
+
@driver.on(:close) { |event| finalize(event) }
|
56
|
+
|
57
|
+
@driver.start
|
58
|
+
end
|
59
|
+
|
60
|
+
def receive_data(data)
|
61
|
+
@driver.parse(data)
|
62
|
+
end
|
63
|
+
|
64
|
+
def write(data)
|
65
|
+
send_data(data)
|
66
|
+
end
|
67
|
+
|
68
|
+
def finalize(event)
|
69
|
+
p [:close, event.code, event.reason] if @debug
|
70
|
+
close_connection
|
71
|
+
end
|
72
|
+
|
73
|
+
def subscribe(channels)
|
74
|
+
dump({ action: "subscribe", params: channels })
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def to_event(event)
|
80
|
+
case event.fetch(:ev)
|
81
|
+
when "status"
|
82
|
+
if event.fetch(:status) == "error" && event.fetch(:message) == "not authorized"
|
83
|
+
raise NotAuthorizedError, event.fetch(:message)
|
84
|
+
end
|
85
|
+
WebsocketEvent.new(event)
|
86
|
+
when "C"
|
87
|
+
ForexQuoteEvent.new(event)
|
88
|
+
when "CA"
|
89
|
+
ForexAggregateEvent.new(event)
|
90
|
+
when "XQ"
|
91
|
+
CryptoQuoteEvent.new(event)
|
92
|
+
when "XT"
|
93
|
+
CryptoTradeEvent.new(event)
|
94
|
+
when "XA"
|
95
|
+
CryptoAggregateEvent.new(event)
|
96
|
+
when "XS"
|
97
|
+
CryptoSipEvent.new(event)
|
98
|
+
when "XL2"
|
99
|
+
CryptoLevel2Event.new(event)
|
100
|
+
when "T"
|
101
|
+
StockTradeEvent.new(event)
|
102
|
+
when "AM"
|
103
|
+
StockAggregatePerMinuteEvent.new(event)
|
104
|
+
when "A"
|
105
|
+
StockAggregatePerSecondEvent.new(event)
|
106
|
+
when "Q"
|
107
|
+
StockQuoteEvent.new(event)
|
108
|
+
else
|
109
|
+
raise UnrecognizedEventError.new(event), "Unrecognized event with type: #{event.ev}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# dump json
|
114
|
+
def dump(json)
|
115
|
+
Oj.dump(json, mode: :compat)
|
116
|
+
end
|
117
|
+
|
118
|
+
def handle_status_event(event)
|
119
|
+
case WebsocketEvent::Statuses[event.status]
|
120
|
+
when "auth_success"
|
121
|
+
subscribe(@channels)
|
122
|
+
when "auth_timeout"
|
123
|
+
raise AuthTimeoutError, event.message
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class Client
|
129
|
+
BASE_URL = "wss://socket.polygon.io/"
|
130
|
+
BASE_URL_DELAYED = "wss://delayed.polygon.io/"
|
131
|
+
|
132
|
+
def initialize(path, api_key, opts = {})
|
133
|
+
path = Types::Coercible::String.enum("stocks", "forex", "crypto")[path]
|
134
|
+
|
135
|
+
@api_key = api_key
|
136
|
+
@ws = nil
|
137
|
+
@opts = opts
|
138
|
+
if opts.fetch(:delayed) == true
|
139
|
+
@url = "#{BASE_URL_DELAYED}#{path}"
|
140
|
+
else
|
141
|
+
@url = "#{BASE_URL}#{path}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def subscribe(channels, &block)
|
146
|
+
EM.run do
|
147
|
+
Connection.connect(@api_key, @url, channels, @opts, &block)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polygonio
|
4
|
+
module Websocket
|
5
|
+
module Errors
|
6
|
+
class PolygonWebsocketClientError < StandardError; end
|
7
|
+
|
8
|
+
class AuthTimeoutError < PolygonWebsocketClientError; end
|
9
|
+
class NotAuthorizedError < PolygonWebsocketClientError; end
|
10
|
+
class UnrecognizedEventError < PolygonWebsocketClientError
|
11
|
+
attr_reader :event
|
12
|
+
|
13
|
+
def initialize(message, event)
|
14
|
+
super(message)
|
15
|
+
@event = event
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polygonio
|
4
|
+
module Websocket
|
5
|
+
class WebsocketEvent < Dry::Struct
|
6
|
+
Statuses = Types::String.enum("connected", "successful", "auth_timeout", "auth_success", "success")
|
7
|
+
|
8
|
+
attribute :ev, Types::String
|
9
|
+
attribute :status, Statuses
|
10
|
+
attribute :message, Types::String
|
11
|
+
end
|
12
|
+
|
13
|
+
class ForexQuoteEvent < Dry::Struct
|
14
|
+
attribute :ev, Types::String.enum("C")
|
15
|
+
attribute :p, Types::String # Currency pair
|
16
|
+
attribute :x, Types::Integer # FX Exchange ID
|
17
|
+
attribute :a, Types::JSON::Decimal # Ask Price
|
18
|
+
attribute :b, Types::JSON::Decimal # Bid Price
|
19
|
+
attribute :t, Types::Integer # Quote Timestamp ( Unix MS )
|
20
|
+
end
|
21
|
+
|
22
|
+
class ForexAggregateEvent < Dry::Struct
|
23
|
+
attribute :ev, Types::String.enum("CA")
|
24
|
+
attribute :pair, Types::String
|
25
|
+
attribute :o, Types::JSON::Decimal # Open Price
|
26
|
+
attribute :c, Types::JSON::Decimal # Close Price
|
27
|
+
attribute :h, Types::JSON::Decimal # High Price
|
28
|
+
attribute :l, Types::JSON::Decimal # Low Price
|
29
|
+
attribute :v, Types::Integer # Volume ( Quotes during this duration )
|
30
|
+
attribute :s, Types::Integer # Tick Start Timestamp
|
31
|
+
end
|
32
|
+
|
33
|
+
class StockAggregatePerSecondEvent < Dry::Struct
|
34
|
+
attribute :ev, Types::String.enum("A") # Event Type
|
35
|
+
attribute? :sym, Types::String # Symbol
|
36
|
+
attribute? :v, Types::Integer # Volume
|
37
|
+
attribute? :av, Types::Integer # Accumulated Volume
|
38
|
+
attribute? :op, Types::JSON::Decimal # Official Opening Price
|
39
|
+
attribute? :vw, Types::JSON::Decimal # Volume Weighted Average Price
|
40
|
+
attribute? :o, Types::JSON::Decimal # Opening Price
|
41
|
+
attribute? :h, Types::JSON::Decimal # High Price
|
42
|
+
attribute? :l, Types::JSON::Decimal # Low Price
|
43
|
+
attribute? :c, Types::JSON::Decimal # Close Price
|
44
|
+
attribute? :a, Types::Integer # Today's Volume weighted average price
|
45
|
+
attribute? :z, Types::Integer # Average trade size
|
46
|
+
attribute? :s, Types::Integer # Starting tick timestamp
|
47
|
+
attribute? :e, Types::Integer # Ending tick timestamp
|
48
|
+
attribute? :otc, Types::Bool # Whether or not this aggregate is for an OTC ticker.
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
class StockAggregatePerMinuteEvent < Dry::Struct
|
53
|
+
attribute :ev, Types::String.enum("AM") # Event Type
|
54
|
+
attribute :sym, Types::String # Symbol
|
55
|
+
attribute? :v, Types::Integer # Volume
|
56
|
+
attribute? :av, Types::Integer # Accumulated Volume
|
57
|
+
attribute? :op, Types::JSON::Decimal # Official Opening Price
|
58
|
+
attribute? :vw, Types::JSON::Decimal # Volume Weighted Average Price
|
59
|
+
attribute? :o, Types::JSON::Decimal # Opening Price
|
60
|
+
attribute? :h, Types::JSON::Decimal # High Price
|
61
|
+
attribute? :l, Types::JSON::Decimal # Low Price
|
62
|
+
attribute? :c, Types::JSON::Decimal # Close Price
|
63
|
+
attribute? :a, Types::Integer # Today's Volume weighted average price
|
64
|
+
attribute? :z, Types::Integer # Average trade size
|
65
|
+
attribute? :s, Types::Integer # Starting tick timestamp
|
66
|
+
attribute? :e, Types::Integer # Ending tick timestamp
|
67
|
+
attribute? :otc, Types::Bool # Whether or not this aggregate is for an OTC ticker.
|
68
|
+
end
|
69
|
+
|
70
|
+
class StockTradeEvent < Dry::Struct
|
71
|
+
attribute :ev, Types::String.enum("T")
|
72
|
+
attribute :sym, Types::String
|
73
|
+
attribute? :i, Types::String # Trade ID
|
74
|
+
attribute :x, Types::Integer # Exchange ID
|
75
|
+
attribute :p, Types::JSON::Decimal # Price
|
76
|
+
attribute? :c, Types::Array
|
77
|
+
attribute :s, Types::Integer # Size
|
78
|
+
attribute :t, Types::Integer # Exchange Timestamp Unix ( ms )
|
79
|
+
attribute :q, Types::Integer # Exchange ID
|
80
|
+
attribute :z, Types::Integer #(1 = NYSE, 2 = AMEX, 3 = Nasdaq)
|
81
|
+
attribute? :trfi, Types::Integer #(1 = NYSE, 2 = AMEX, 3 = Nasdaq)
|
82
|
+
attribute? :trft, Types::Integer #(1 = NYSE, 2 = AMEX, 3 = Nasdaq)
|
83
|
+
end
|
84
|
+
class StockQuoteEvent < Dry::Struct
|
85
|
+
attribute :ev, Types::String.enum("Q")
|
86
|
+
attribute :sym, Types::String
|
87
|
+
attribute? :bx, Types::Integer
|
88
|
+
attribute? :bp, Types::JSON::Decimal # Exchange ID
|
89
|
+
attribute? :bs, Types::Integer
|
90
|
+
attribute? :ax, Types::Integer
|
91
|
+
attribute? :ap, Types::JSON::Decimal
|
92
|
+
attribute? :as, Types::Integer
|
93
|
+
attribute? :i, Types::Array.of(Types::Integer)
|
94
|
+
attribute :t, Types::Integer # Exchange Timestamp Unix ( ms )
|
95
|
+
attribute :q, Types::Integer # Exchange ID
|
96
|
+
attribute :z, Types::Integer # Exchange ID
|
97
|
+
end
|
98
|
+
|
99
|
+
class CryptoQuoteEvent < Dry::Struct
|
100
|
+
attribute :ev, Types::String.enum("XQ")
|
101
|
+
attribute :pair, Types::String
|
102
|
+
attribute :lp, Types::JSON::Decimal # Last Trade Price
|
103
|
+
attribute :ls, Types::JSON::Decimal # Last Trade Size
|
104
|
+
attribute :bp, Types::JSON::Decimal # Bid Price
|
105
|
+
attribute :bs, Types::JSON::Decimal # Bid Size
|
106
|
+
attribute :ap, Types::JSON::Decimal # Ask Price
|
107
|
+
attribute :as, Types::JSON::Decimal # Ask Size
|
108
|
+
attribute :t, Types::Integer # Exchange Timestamp Unix ( ms )
|
109
|
+
attribute :x, Types::Integer # Exchange ID
|
110
|
+
attribute :r, Types::Integer # Received @ Polygon Timestamp
|
111
|
+
end
|
112
|
+
|
113
|
+
class CryptoTradeEvent < Dry::Struct
|
114
|
+
attribute :ev, Types::String.enum("XT")
|
115
|
+
attribute :pair, Types::String
|
116
|
+
attribute :p, Types::JSON::Decimal # Price
|
117
|
+
attribute :t, Types::Integer # Timestamp Unix ( ms )
|
118
|
+
attribute :s, Types::JSON::Decimal # Size
|
119
|
+
attribute :c, Types::Array.of(Types::Integer) # Condition
|
120
|
+
attribute :i, Types::String # Trade ID ( Optional )
|
121
|
+
attribute :x, Types::Integer # Exchange ID
|
122
|
+
attribute :r, Types::Integer # Received @ Polygon Timestamp
|
123
|
+
end
|
124
|
+
|
125
|
+
class CryptoAggregateEvent < Dry::Struct
|
126
|
+
attribute :ev, Types::String.enum("XA")
|
127
|
+
attribute :pair, Types::String
|
128
|
+
attribute :o, Types::JSON::Decimal # Open Price
|
129
|
+
attribute? :ox, Types::Integer # Open Exchange
|
130
|
+
attribute :h, Types::JSON::Decimal # High Price
|
131
|
+
attribute? :hx, Types::Integer # High Exchange
|
132
|
+
attribute :l, Types::JSON::Decimal # Low Price
|
133
|
+
attribute? :lx, Types::Integer # Low Exchange
|
134
|
+
attribute :c, Types::JSON::Decimal # Close Price
|
135
|
+
attribute? :cx, Types::Integer # Close Exchange
|
136
|
+
attribute :v, Types::JSON::Decimal # Volume of Trades in Tick
|
137
|
+
attribute :s, Types::Integer # Tick Start Timestamp
|
138
|
+
attribute :e, Types::Integer # Tick End Timestamp
|
139
|
+
end
|
140
|
+
|
141
|
+
class CryptoSipEvent < Dry::Struct
|
142
|
+
attribute :ev, Types::String.enum("XS")
|
143
|
+
attribute :pair, Types::String
|
144
|
+
attribute :as, Types::JSON::Decimal # Ask Size
|
145
|
+
attribute :ap, Types::JSON::Decimal # Ask Price
|
146
|
+
attribute :ax, Types::Integer # Ask Exchange
|
147
|
+
attribute :bs, Types::JSON::Decimal # Bid Size
|
148
|
+
attribute :bp, Types::JSON::Decimal # Bid Price
|
149
|
+
attribute :bx, Types::Integer # Bid Exchange
|
150
|
+
attribute :t, Types::Integer # Tick Start Timestamp
|
151
|
+
end
|
152
|
+
|
153
|
+
class CryptoLevel2Event < Dry::Struct
|
154
|
+
attribute :ev, Types::String.enum("XL2")
|
155
|
+
attribute :pair, Types::String
|
156
|
+
attribute :b, Types::Array.of(Types::Array.of(Types::JSON::Decimal))
|
157
|
+
# Bid Prices ( 100 depth cap )
|
158
|
+
# [ Price, Size ]
|
159
|
+
attribute :a, Types::Array.of(Types::Array.of(Types::JSON::Decimal))
|
160
|
+
# Ask Prices ( 100 depth cap )
|
161
|
+
# [ Price, Size ]
|
162
|
+
attribute :t, Types::Integer # Timestamp Unix ( ms )
|
163
|
+
attribute :x, Types::Integer # Exchange ID
|
164
|
+
attribute :r, Types::Integer # Tick Received @ Polygon Timestamp
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
data/lib/polygonio.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "polygonio/version"
|
4
|
+
|
5
|
+
require "eventmachine"
|
6
|
+
require "faraday"
|
7
|
+
require "faraday_middleware"
|
8
|
+
require "faraday_middleware/parse_oj"
|
9
|
+
require "dry-struct"
|
10
|
+
require "dry-types"
|
11
|
+
require "permessage_deflate"
|
12
|
+
require "websocket/driver"
|
13
|
+
|
14
|
+
require "polygonio/types"
|
15
|
+
require "polygonio/rest"
|
16
|
+
require "polygonio/websocket"
|
data/polygonio.gemspec
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "polygonio/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "polygonio-ruby"
|
9
|
+
spec.version = Polygonio::VERSION
|
10
|
+
spec.authors = ["Anthony Eufemio"]
|
11
|
+
spec.email = ["ace@135.io"]
|
12
|
+
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.summary = "Client library for polygon.io. Polygon IO is an API for market data. Modernized version"
|
16
|
+
spec.description = "Client library for polygon.io's REST and Websocket API's. Polygon IO is an API for market data. Modernized version"
|
17
|
+
|
18
|
+
spec.metadata = {
|
19
|
+
"homepage_uri" => "https://github.com/tymat/polygonio-ruby",
|
20
|
+
"source_code_uri" => "https://github.com/tymat/polygonio-ruby",
|
21
|
+
"bug_tracker_uri" => "https://github.com/tymat/polygonio-ruby/issues"
|
22
|
+
}
|
23
|
+
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
25
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
26
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
27
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_runtime_dependency "activesupport", ["~> 6.0", ">= 6.0.2.2"]
|
34
|
+
spec.add_runtime_dependency "dry-struct", ["~> 1.2", ">= 1.2.0"]
|
35
|
+
spec.add_runtime_dependency "dry-types", ["~> 1.2", ">= 1.2.2"]
|
36
|
+
spec.add_runtime_dependency "eventmachine", ["~> 1.2", ">= 1.2.7"]
|
37
|
+
spec.add_runtime_dependency "faraday", ["~> 0.17", ">= 0.17.3"]
|
38
|
+
spec.add_runtime_dependency "faraday_middleware", ["~> 0.13", ">= 0.13.1"]
|
39
|
+
spec.add_runtime_dependency "faraday_middleware-parse_oj", ["~> 0.3", ">= 0.3.2"]
|
40
|
+
spec.add_runtime_dependency "oj", ["~> 3.10", ">= 3.10.1"]
|
41
|
+
spec.add_runtime_dependency "permessage_deflate", ["~> 0.1", ">= 0.1.4"]
|
42
|
+
spec.add_runtime_dependency "websocket-driver", ["~> 0.7", ">= 0.7.1"]
|
43
|
+
|
44
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
45
|
+
spec.add_development_dependency "bundler-audit", "~> 0.6"
|
46
|
+
spec.add_development_dependency "dotenv", "~> 2.7"
|
47
|
+
spec.add_development_dependency "faker", "~> 2.11"
|
48
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
49
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
50
|
+
spec.add_development_dependency "rubocop", "~> 0.80"
|
51
|
+
spec.add_development_dependency "rubocop-performance", "~> 1.5"
|
52
|
+
spec.add_development_dependency "vcr", "~> 5.1"
|
53
|
+
end
|