polygonio 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolygonClient
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
+
86
+ WebsocketEvent.new(event)
87
+ when "C"
88
+ ForexQuoteEvent.new(event)
89
+ when "CA"
90
+ ForexAggregateEvent.new(event)
91
+ when "XQ"
92
+ CryptoQuoteEvent.new(event)
93
+ when "XT"
94
+ CryptoTradeEvent.new(event)
95
+ when "XA"
96
+ CryptoAggregateEvent.new(event)
97
+ when "XS"
98
+ CryptoSipEvent.new(event)
99
+ when "XL2"
100
+ CryptoLevel2Event.new(event)
101
+ else
102
+ raise UnrecognizedEventError.new(event), "Unrecognized event with type: #{event.ev}"
103
+ end
104
+ end
105
+
106
+ # dump json
107
+ def dump(json)
108
+ Oj.dump(json, mode: :compat)
109
+ end
110
+
111
+ def handle_status_event(event)
112
+ case WebsocketEvent::Statuses[event.status]
113
+ when "auth_success"
114
+ subscribe(@channels)
115
+ when "auth_timeout"
116
+ raise AuthTimeoutError, event.message
117
+ end
118
+ end
119
+ end
120
+
121
+ class Client
122
+ BASE_URL = "wss://socket.polygon.io/"
123
+
124
+ def initialize(path, api_key, opts = {})
125
+ path = Types::Coercible::String.enum("stocks", "forex", "crypto")[path]
126
+
127
+ @api_key = api_key
128
+ @ws = nil
129
+ @opts = opts
130
+ @url = "#{BASE_URL}#{path}"
131
+ end
132
+
133
+ def subscribe(channels, &block)
134
+ EM.run do
135
+ Connection.connect(@api_key, @url, channels, @opts, &block)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolygonClient
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,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolygonClient
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 CryptoQuoteEvent < Dry::Struct
34
+ attribute :ev, Types::String.enum("XQ")
35
+ attribute :pair, Types::String
36
+ attribute :lp, Types::JSON::Decimal # Last Trade Price
37
+ attribute :ls, Types::JSON::Decimal # Last Trade Size
38
+ attribute :bp, Types::JSON::Decimal # Bid Price
39
+ attribute :bs, Types::JSON::Decimal # Bid Size
40
+ attribute :ap, Types::JSON::Decimal # Ask Price
41
+ attribute :as, Types::JSON::Decimal # Ask Size
42
+ attribute :t, Types::Integer # Exchange Timestamp Unix ( ms )
43
+ attribute :x, Types::Integer # Exchange ID
44
+ attribute :r, Types::Integer # Received @ Polygon Timestamp
45
+ end
46
+
47
+ class CryptoTradeEvent < Dry::Struct
48
+ attribute :ev, Types::String.enum("XT")
49
+ attribute :pair, Types::String
50
+ attribute :p, Types::JSON::Decimal # Price
51
+ attribute :t, Types::Integer # Timestamp Unix ( ms )
52
+ attribute :s, Types::JSON::Decimal # Size
53
+ attribute :c, Types::Array.of(Types::Integer) # Condition
54
+ attribute :i, Types::String # Trade ID ( Optional )
55
+ attribute :x, Types::Integer # Exchange ID
56
+ attribute :r, Types::Integer # Received @ Polygon Timestamp
57
+ end
58
+
59
+ class CryptoAggregateEvent < Dry::Struct
60
+ attribute :ev, Types::String.enum("XA")
61
+ attribute :pair, Types::String
62
+ attribute :o, Types::JSON::Decimal # Open Price
63
+ attribute? :ox, Types::Integer # Open Exchange
64
+ attribute :h, Types::JSON::Decimal # High Price
65
+ attribute? :hx, Types::Integer # High Exchange
66
+ attribute :l, Types::JSON::Decimal # Low Price
67
+ attribute? :lx, Types::Integer # Low Exchange
68
+ attribute :c, Types::JSON::Decimal # Close Price
69
+ attribute? :cx, Types::Integer # Close Exchange
70
+ attribute :v, Types::JSON::Decimal # Volume of Trades in Tick
71
+ attribute :s, Types::Integer # Tick Start Timestamp
72
+ attribute :e, Types::Integer # Tick End Timestamp
73
+ end
74
+
75
+ class CryptoSipEvent < Dry::Struct
76
+ attribute :ev, Types::String.enum("XS")
77
+ attribute :pair, Types::String
78
+ attribute :as, Types::JSON::Decimal # Ask Size
79
+ attribute :ap, Types::JSON::Decimal # Ask Price
80
+ attribute :ax, Types::Integer # Ask Exchange
81
+ attribute :bs, Types::JSON::Decimal # Bid Size
82
+ attribute :bp, Types::JSON::Decimal # Bid Price
83
+ attribute :bx, Types::Integer # Bid Exchange
84
+ attribute :t, Types::Integer # Tick Start Timestamp
85
+ end
86
+
87
+ class CryptoLevel2Event < Dry::Struct
88
+ attribute :ev, Types::String.enum("XL2")
89
+ attribute :pair, Types::String
90
+ attribute :b, Types::Array.of(Types::Array.of(Types::JSON::Decimal))
91
+ # Bid Prices ( 100 depth cap )
92
+ # [ Price, Size ]
93
+ attribute :a, Types::Array.of(Types::Array.of(Types::JSON::Decimal))
94
+ # Ask Prices ( 100 depth cap )
95
+ # [ Price, Size ]
96
+ attribute :t, Types::Integer # Timestamp Unix ( ms )
97
+ attribute :x, Types::Integer # Exchange ID
98
+ attribute :r, Types::Integer # Tick Received @ Polygon Timestamp
99
+ end
100
+ end
101
+ 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"
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "polygon_client/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 "polygon_client/types"
15
+ require "polygon_client/rest"
16
+ require "polygon_client/websocket"
@@ -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 "polygon_client/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "polygonio"
9
+ spec.version = PolygonClient::VERSION
10
+ spec.authors = ["Lance Carlson"]
11
+ spec.email = ["lancecarlson@gmail.com"]
12
+
13
+ spec.license = "MIT"
14
+
15
+ spec.summary = "Client library for polygon.io"
16
+ spec.description = "Client library for polygon.io's REST and Websocker API's"
17
+
18
+ spec.metadata = {
19
+ "homepage_uri" => "https://github.com/lancecarlson/polygon-client-rb",
20
+ "source_code_uri" => 'https://github.com/lancecarlson/polygon-client-rb',
21
+ "bug_tracker_uri" => 'https://github.com/lancecarlson/polygon-client-rb/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