polygonio 0.1.0
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/Gemfile.lock +121 -0
- data/README.md +50 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/lib/polygon_client/rest/api/crypto.rb +198 -0
- data/lib/polygon_client/rest/api/forex.rb +129 -0
- data/lib/polygon_client/rest/api/reference/locales.rb +22 -0
- data/lib/polygon_client/rest/api/reference/markets.rb +55 -0
- data/lib/polygon_client/rest/api/reference/stocks.rb +70 -0
- data/lib/polygon_client/rest/api/reference/tickers.rb +124 -0
- data/lib/polygon_client/rest/api/stocks.rb +327 -0
- data/lib/polygon_client/rest/api.rb +34 -0
- data/lib/polygon_client/rest/client.rb +70 -0
- data/lib/polygon_client/rest/errors.rb +43 -0
- data/lib/polygon_client/rest.rb +5 -0
- data/lib/polygon_client/types.rb +7 -0
- data/lib/polygon_client/version.rb +5 -0
- data/lib/polygon_client/websocket/client.rb +140 -0
- data/lib/polygon_client/websocket/errors.rb +20 -0
- data/lib/polygon_client/websocket/events.rb +101 -0
- data/lib/polygon_client/websocket.rb +5 -0
- data/lib/polygon_client.rb +16 -0
- data/polygon_client.gemspec +53 -0
- metadata +398 -0
@@ -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,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
|