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,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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "websocket/client"
4
+ require_relative "websocket/errors"
5
+ require_relative "websocket/events"
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