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,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