polygonio 0.1.0
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.
- 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
|