polygonio-ruby 0.2.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|