market_data 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +6 -1
- data/Gemfile.lock +1 -1
- data/README.md +15 -3
- data/lib/market_data/conn.rb +4 -0
- data/lib/market_data/constants.rb +59 -2
- data/lib/market_data/indexes.rb +1 -0
- data/lib/market_data/mappers.rb +45 -8
- data/lib/market_data/models.rb +8 -0
- data/lib/market_data/options.rb +64 -0
- data/lib/market_data/quotes.rb +4 -2
- data/lib/market_data/validations.rb +301 -0
- data/lib/market_data/version.rb +1 -1
- data/lib/market_data.rb +2 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbc923ec4b12764e6cda0c8ca9fa7e26a0c2b91c01c00d2f43b05bd2bb46f4e3
|
4
|
+
data.tar.gz: 5174eef19c848f47683c6066d53550a780c0ffe22148a54835fa66027f8b541b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 484a07bfbaefd2333e1db7989a297d36389be02cd482bfdd40990eae0ec2183367bb3854ff97a4c58a056d3b36bec224dd3dbc62567f8c929c078993df8d92fb
|
7
|
+
data.tar.gz: 80b6d551634d4b9136b346b98972ecfd1e357e3b5ecf9860cb47d31e5075c57521c62494e3a3fec6b0b436c38c3fb847eb2f083fa64922989c6d0e3814c45358
|
data/CHANGELOG.md
CHANGED
@@ -37,4 +37,9 @@
|
|
37
37
|
|
38
38
|
## [0.5.0] - 2024-10-15
|
39
39
|
|
40
|
-
- Add support for Indexes endpoints under `index_quote` and `ìndex_candles`
|
40
|
+
- Add support for Indexes endpoints under `index_quote` and `ìndex_candles`
|
41
|
+
|
42
|
+
## [0.6.0] - 2024-11-08
|
43
|
+
|
44
|
+
- Add support for all Options endpoints: `expirations`, `lookup`, `strikes`, `chain`, `option_quote`
|
45
|
+
- Improve mappers coverage
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
A Ruby wrapper for the [MarketData API](https://www.marketdata.app/docs/api).
|
4
4
|
|
5
|
-

|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -145,9 +145,21 @@ From Indices endpoints:
|
|
145
145
|
- [X] Quotes
|
146
146
|
- [X] Candles
|
147
147
|
|
148
|
+
From Options endpoints:
|
149
|
+
- [X] Expirations
|
150
|
+
- [X] Lookup
|
151
|
+
- [X] Strikes
|
152
|
+
- [X] Chain
|
153
|
+
- [X] Quotes
|
154
|
+
|
155
|
+
From Mutual Funds endpoints:
|
156
|
+
- [ ] Candles
|
157
|
+
|
148
158
|
From Stocks endpoints:
|
149
|
-
- [ ]
|
150
|
-
|
159
|
+
- [ ] Rewrite Stocks endpoints methods to new validation scheme
|
160
|
+
|
161
|
+
Other:
|
162
|
+
- [ ] [Cache feed](https://www.marketdata.app/docs/api/universal-parameters/feed#cached-feed) support
|
151
163
|
|
152
164
|
|
153
165
|
## Tests
|
data/lib/market_data/conn.rb
CHANGED
@@ -9,6 +9,14 @@ module MarketData
|
|
9
9
|
MONTH_31 = DAY * 31
|
10
10
|
YEAR = DAY * 365
|
11
11
|
|
12
|
+
SIDE_CALL = "Call"
|
13
|
+
SIDE_PUT = "Put"
|
14
|
+
|
15
|
+
RANGE_ALL = "all"
|
16
|
+
RANGE_OTM = "otm"
|
17
|
+
RANGE_ITM = "itm"
|
18
|
+
RANGE_ALLOWED = [RANGE_ALL, RANGE_OTM, RANGE_ITM]
|
19
|
+
|
12
20
|
EARNING_FIELD_MAPPING = {
|
13
21
|
symbol: "symbol",
|
14
22
|
fiscal_year: "fiscalYear",
|
@@ -44,8 +52,8 @@ module MarketData
|
|
44
52
|
change_pct: "changepct",
|
45
53
|
volume: "volume",
|
46
54
|
updated: "updated",
|
47
|
-
high52: "
|
48
|
-
low52: "
|
55
|
+
high52: "52weekHigh",
|
56
|
+
low52: "52weekLow",
|
49
57
|
}
|
50
58
|
MARKET_STATUS_FIELD_MAPPING = {
|
51
59
|
date: "date",
|
@@ -68,5 +76,54 @@ module MarketData
|
|
68
76
|
high: "h",
|
69
77
|
time: "t",
|
70
78
|
}
|
79
|
+
OPTION_CHAIN_FIELD_MAPPING = {
|
80
|
+
option_symbol: "optionSymbol",
|
81
|
+
underlying: "underlying",
|
82
|
+
expiration: "expiration",
|
83
|
+
side: "side",
|
84
|
+
strike: "strike",
|
85
|
+
first_traded: "firstTraded",
|
86
|
+
dte: "dte",
|
87
|
+
ask: "ask",
|
88
|
+
ask_size: "askSize",
|
89
|
+
bid: "bidSize",
|
90
|
+
mid: "mid",
|
91
|
+
last: "last",
|
92
|
+
volume: "volume",
|
93
|
+
open_interest: "openInterest",
|
94
|
+
underlying_price: "underlyingPrice",
|
95
|
+
in_the_money: "inTheMoney",
|
96
|
+
intrinsic_value: "intrinsicValue",
|
97
|
+
extrinsic_value: "extrinsicValue",
|
98
|
+
updated: "updated",
|
99
|
+
iv: "iv",
|
100
|
+
delta: "delta",
|
101
|
+
gamma: "gamma",
|
102
|
+
theta: "theta",
|
103
|
+
vega: "vega",
|
104
|
+
rho: "rho"
|
105
|
+
}
|
106
|
+
OPTION_QUOTE_FIELD_MAPPING = {
|
107
|
+
option_symbol: "optionSymbol",
|
108
|
+
ask: "ask",
|
109
|
+
ask_size: "askSize",
|
110
|
+
bid: "bid",
|
111
|
+
bid_size: "bidSize",
|
112
|
+
mid: "mid",
|
113
|
+
last: "last",
|
114
|
+
volume: "volume",
|
115
|
+
open_interest: "openInterest",
|
116
|
+
underlying_price: "underlyingPrice",
|
117
|
+
in_the_money: "inTheMoney",
|
118
|
+
updated: "updated",
|
119
|
+
iv: "iv",
|
120
|
+
delta: "delta",
|
121
|
+
gamma: "gamma",
|
122
|
+
theta: "theta",
|
123
|
+
vega: "vega",
|
124
|
+
rho: "rho",
|
125
|
+
intrinsic_value: "intrinsicValue",
|
126
|
+
extrinsic_value: "extrinsicValue"
|
127
|
+
}
|
71
128
|
end
|
72
129
|
end
|
data/lib/market_data/indexes.rb
CHANGED
data/lib/market_data/mappers.rb
CHANGED
@@ -6,6 +6,7 @@ module MarketData
|
|
6
6
|
SYMBOL_RESPONSE_KEY = "symbol"
|
7
7
|
STATUS_RESPONSE_KEY = "s"
|
8
8
|
OPEN_RESPONSE_KEY = "o"
|
9
|
+
OPTION_SYMBOL_RESPONSE_KEY = "optionSymbol"
|
9
10
|
|
10
11
|
def map_quote response, i=0
|
11
12
|
Quote.new(**map_fields_for(response, :quote, i))
|
@@ -40,7 +41,7 @@ module MarketData
|
|
40
41
|
h
|
41
42
|
end
|
42
43
|
|
43
|
-
def
|
44
|
+
def map_earnings response
|
44
45
|
ar = []
|
45
46
|
(0..(response[SYMBOL_RESPONSE_KEY].size - 1)).each do |i|
|
46
47
|
ar << Earning.new(**map_fields_for(response, :earning, i))
|
@@ -69,19 +70,55 @@ module MarketData
|
|
69
70
|
ar
|
70
71
|
end
|
71
72
|
|
73
|
+
def map_expirations response
|
74
|
+
Models::OptExpirations.new(
|
75
|
+
expirations: response["expirations"],
|
76
|
+
updated: response["updated"]
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def map_lookup response
|
81
|
+
response["optionSymbol"]
|
82
|
+
end
|
83
|
+
|
84
|
+
def map_strike response
|
85
|
+
date_map = response.reject { |el| !el.match(/\d{4}-\d{2}-\d{2}/) }
|
86
|
+
Models::OptStrike.new(
|
87
|
+
updated: response["updated"],
|
88
|
+
strikes: date_map,
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def map_option_chain response
|
93
|
+
ar = []
|
94
|
+
(0..(response[OPTION_SYMBOL_RESPONSE_KEY].size - 1)).each do |i|
|
95
|
+
args = map_fields_for(response, :option_chain, i)
|
96
|
+
ar << Models::OptChain.new(**args)
|
97
|
+
end
|
98
|
+
ar
|
99
|
+
end
|
100
|
+
|
101
|
+
def map_option_quote response
|
102
|
+
args = map_fields_for(response, :option_quote)
|
103
|
+
Models::OptQuote.new(**args)
|
104
|
+
end
|
105
|
+
|
72
106
|
def map_fields_for(response, kind, i=0)
|
73
|
-
mapping =
|
74
|
-
case kind
|
107
|
+
mapping = case kind
|
75
108
|
when :candle
|
76
|
-
|
109
|
+
Constants::CANDLE_FIELD_MAPPING
|
77
110
|
when :earning
|
78
|
-
|
111
|
+
Constants::EARNING_FIELD_MAPPING
|
79
112
|
when :index_candle
|
80
|
-
|
113
|
+
Constants::INDEX_CANDLE_FIELD_MAPPING
|
81
114
|
when :index_quote
|
82
|
-
|
115
|
+
Constants::INDEX_QUOTE_FIELD_MAPPING
|
116
|
+
when :option_chain
|
117
|
+
Constants::OPTION_CHAIN_FIELD_MAPPING
|
118
|
+
when :option_quote
|
119
|
+
Constants::OPTION_QUOTE_FIELD_MAPPING
|
83
120
|
when :quote
|
84
|
-
|
121
|
+
Constants::QUOTE_FIELD_MAPPING
|
85
122
|
else
|
86
123
|
raise BadParameterError.new("unrecognized model for mapping: #{kind}")
|
87
124
|
end
|
data/lib/market_data/models.rb
CHANGED
@@ -21,5 +21,13 @@ module MarketData
|
|
21
21
|
IndexQuote = Struct.new(*Constants::INDEX_QUOTE_FIELD_MAPPING.keys)
|
22
22
|
|
23
23
|
IndexCandle = Struct.new(*Constants::INDEX_CANDLE_FIELD_MAPPING.keys)
|
24
|
+
|
25
|
+
OptExpirations = Struct.new(:expirations, :updated)
|
26
|
+
|
27
|
+
OptStrike = Struct.new(:updated, :strikes)
|
28
|
+
|
29
|
+
OptChain = Struct.new(*Constants::OPTION_CHAIN_FIELD_MAPPING.keys)
|
30
|
+
|
31
|
+
OptQuote = Struct.new(*Constants::OPTION_QUOTE_FIELD_MAPPING.keys)
|
24
32
|
end
|
25
33
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'market_data/conn'
|
2
|
+
require 'market_data/mappers'
|
3
|
+
require 'market_data/validations'
|
4
|
+
|
5
|
+
module MarketData
|
6
|
+
module Options
|
7
|
+
include MarketData::Conn
|
8
|
+
include MarketData::Mappers
|
9
|
+
include MarketData::Validations
|
10
|
+
|
11
|
+
@@expirations = "/v1/options/expirations/%{symbol}/"
|
12
|
+
@@lookup = "/v1/options/lookup/"
|
13
|
+
@@strike = "/v1/options/strikes/%{symbol}/"
|
14
|
+
@@chain = "/v1/options/chain/%{symbol}/"
|
15
|
+
@@quotes = "/v1/options/quotes/%{symbol}/"
|
16
|
+
|
17
|
+
def expirations(symbol, opts = options_for_expirations)
|
18
|
+
query = validate_expirations_input!(symbol: symbol, **opts)
|
19
|
+
|
20
|
+
map_expirations(
|
21
|
+
do_request(
|
22
|
+
@@expirations % {symbol: symbol},
|
23
|
+
query
|
24
|
+
)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def lookup(required = required_for_lookup)
|
29
|
+
query = validate_lookup_input!(**required)
|
30
|
+
s_query = "#{query[:symbol]} #{query[:expiration]} #{query[:strike]} #{query[:side]}"
|
31
|
+
|
32
|
+
map_lookup(
|
33
|
+
do_request @@lookup + encode_uri_component(s_query), {}
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def strikes(symbol, opts = options_for_strikes)
|
38
|
+
query = validate_strikes_input!(symbol: symbol, **opts)
|
39
|
+
query = query.except(:symbol)
|
40
|
+
|
41
|
+
map_strike(
|
42
|
+
do_request @@strike % {symbol: symbol}, query
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def chain(symbol, opts = options_for_option_chains)
|
47
|
+
query = validate_option_chain_input!(symbol: symbol, **opts)
|
48
|
+
query = query.except(:symbol)
|
49
|
+
|
50
|
+
map_option_chain(
|
51
|
+
do_request @@chain % {symbol: symbol}, query
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def option_quote(symbol, opts = options_for_option_quote)
|
56
|
+
query = validate_option_quote_input!(symbol: symbol, **opts)
|
57
|
+
query = query.except(:symbol)
|
58
|
+
|
59
|
+
map_option_quote(
|
60
|
+
do_request @@quotes % {symbol: symbol}, query
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/market_data/quotes.rb
CHANGED
@@ -6,10 +6,11 @@ require 'market_data/validations'
|
|
6
6
|
module MarketData
|
7
7
|
module Quotes
|
8
8
|
include MarketData::Mappers
|
9
|
-
include MarketData::Errors
|
9
|
+
include MarketData::Errors # <- TODO: remove this
|
10
10
|
include MarketData::Conn
|
11
11
|
include MarketData::Validations
|
12
12
|
|
13
|
+
# TODO use delayed interpolation when building path
|
13
14
|
@@single = "/v1/stocks/quotes/"
|
14
15
|
@@bulk = "/v1/stocks/bulkquotes/"
|
15
16
|
@@candles = "/v1/stocks/candles/"
|
@@ -52,10 +53,11 @@ module MarketData
|
|
52
53
|
)
|
53
54
|
end
|
54
55
|
|
56
|
+
|
55
57
|
def earnings(symbol, opts = {from: nil, to: nil, countback: nil, date: nil, report: nil})
|
56
58
|
query = validate_earnings_input!(**opts)
|
57
59
|
|
58
|
-
|
60
|
+
map_earnings(
|
59
61
|
do_request @@earnings + symbol, query
|
60
62
|
)
|
61
63
|
end
|
@@ -12,6 +12,36 @@ module MarketData
|
|
12
12
|
'yearly', 'Y', '1Y', '2Y', '3Y', '4Y', '5Y',
|
13
13
|
]
|
14
14
|
|
15
|
+
def options_for_expirations
|
16
|
+
{strike: nil, date: nil}
|
17
|
+
end
|
18
|
+
|
19
|
+
def required_for_lookup
|
20
|
+
{symbol: nil, expiration: nil, strike: nil, side: nil}
|
21
|
+
end
|
22
|
+
|
23
|
+
def options_for_strikes
|
24
|
+
{
|
25
|
+
date: nil, expiration: nil
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def options_for_option_chains
|
30
|
+
{
|
31
|
+
date: nil,
|
32
|
+
expiration: nil, dte: nil, from: nil, to: nil, month: nil, year: nil, weekly: nil, monthly: nil, quarterly: nil,
|
33
|
+
strike: nil, delta: nil, strike_limit: nil, range: nil,
|
34
|
+
min_bid: nil, max_bid: nil, min_ask: nil, max_ask: nil, max_bid_ask_spread: nil, max_bid_ask_spread_pct: nil, min_open_interest: nil, min_volume: nil,
|
35
|
+
non_standard: nil, side: nil, am: nil, pm: nil
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def options_for_option_quote
|
40
|
+
{
|
41
|
+
date: nil, to: nil, from: nil
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
15
45
|
def validate_quotes_input!(symbol: nil, w52: nil, extended: nil)
|
16
46
|
result = {}
|
17
47
|
|
@@ -125,6 +155,98 @@ module MarketData
|
|
125
155
|
validate_candles_input!(resolution: resolution, from: from, to: to, countback: countback)
|
126
156
|
end
|
127
157
|
|
158
|
+
def validate_expirations_input!(symbol: nil, strike: nil, date: nil)
|
159
|
+
result = {}
|
160
|
+
if !symbol.kind_of? String
|
161
|
+
raise BadParameterError.new("symbol must be a string: found #{symbol}")
|
162
|
+
end
|
163
|
+
if symbol.empty?
|
164
|
+
raise BadParameterError.new("symbol must be present: found empty string")
|
165
|
+
end
|
166
|
+
|
167
|
+
if !date.nil? && !time_valid?(date)
|
168
|
+
raise BadParameterError.new("date is not valid")
|
169
|
+
end
|
170
|
+
result.merge!({date: date}) unless date.nil?
|
171
|
+
|
172
|
+
if !strike.nil? && !strike.kind_of?(Numeric)
|
173
|
+
raise BadParameterError.new("strike must be number, found: #{strike}")
|
174
|
+
end
|
175
|
+
result.merge!({strike: strike}) unless strike.nil?
|
176
|
+
|
177
|
+
return result
|
178
|
+
end
|
179
|
+
|
180
|
+
def validate_lookup_input!(symbol: nil, expiration: nil, strike: nil, side: nil)
|
181
|
+
raise BadParameterError.new("symbol must be present. Found :#{symbol}") if symbol.nil? || symbol.empty?
|
182
|
+
raise BadParameterError.new("expiration must be present. Found :#{expiration}") if expiration.nil? || expiration.empty?
|
183
|
+
raise BadParameterError.new("strike must be present. Found :#{symbol}") if strike.nil?
|
184
|
+
if side.nil? || ![Constants::SIDE_CALL, Constants::SIDE_PUT].include?(side)
|
185
|
+
raise BadParameterError.new("side must be either '#{Constants::SIDE_PUT}' or '#{Constants::SIDE_CALL}'. Found: '#{symbol}'")
|
186
|
+
end
|
187
|
+
return {symbol: symbol, expiration: expiration, strike: strike, side: side}
|
188
|
+
end
|
189
|
+
|
190
|
+
def validate_strikes_input!(symbol: nil, date: nil, expiration: nil)
|
191
|
+
if symbol.nil? || symbol.empty?
|
192
|
+
raise BadParameterError.new("symbol must be present. Found: #{symbol}")
|
193
|
+
end
|
194
|
+
result = {symbol: symbol}
|
195
|
+
|
196
|
+
if !date.nil?
|
197
|
+
raise BadParameterError.new("invalid date: #{date}") if !time_valid?(date)
|
198
|
+
return result.merge({date: date})
|
199
|
+
end
|
200
|
+
|
201
|
+
if !expiration.nil?
|
202
|
+
raise BadParameterError.new("invalid date: #{expiration}") if !time_valid?(expiration)
|
203
|
+
result.merge!({expiration: expiration})
|
204
|
+
end
|
205
|
+
|
206
|
+
result
|
207
|
+
end
|
208
|
+
|
209
|
+
def validate_option_chain_input!(
|
210
|
+
symbol: nil, date: nil,
|
211
|
+
expiration: nil, dte: nil, from: nil, to: nil, month: nil, year: nil, weekly: nil, monthly: nil, quarterly: nil,
|
212
|
+
strike: nil, delta: nil, strike_limit: nil, range: nil,
|
213
|
+
min_bid: nil, max_bid: nil, min_ask: nil, max_ask: nil, max_bid_ask_spread: nil, max_bid_ask_spread_pct: nil, min_open_interest: nil, min_volume: nil,
|
214
|
+
non_standard: nil, side: nil, am: nil, pm: nil
|
215
|
+
)
|
216
|
+
result = {}
|
217
|
+
|
218
|
+
if symbol.nil? || !symbol.kind_of?(String) || symbol.empty?
|
219
|
+
raise BadParameterError.new("symbol must be present. Found: #{symbol}")
|
220
|
+
end
|
221
|
+
|
222
|
+
if !date.nil?
|
223
|
+
raise BadParameterError.new("invalid date for `date`: #{date}") if !time_valid?(date)
|
224
|
+
result.merge!({date: date})
|
225
|
+
end
|
226
|
+
|
227
|
+
# handle expiration filters
|
228
|
+
e_filters = {expiration: expiration, dte: dte, from: from, to: to, month: month, year: year, weekly: weekly, monthly: monthly, quarterly: quarterly}
|
229
|
+
expiration_filters_validated_query = validate_expiration_filters!(**e_filters)
|
230
|
+
result.merge!(expiration_filters_validated_query)
|
231
|
+
|
232
|
+
# handle strike filters
|
233
|
+
s_filters = {strike: strike, delta: delta, strike_limit: strike_limit, range: range}
|
234
|
+
strike_filters_validated_query = validate_option_chain_strike_filters!(**s_filters)
|
235
|
+
result.merge!(strike_filters_validated_query)
|
236
|
+
|
237
|
+
# handle liquidity filters
|
238
|
+
l_filters = {min_bid: min_bid, max_bid: max_bid, min_ask: min_ask, max_ask: max_ask, max_bid_ask_spread: max_bid_ask_spread,
|
239
|
+
max_bid_ask_spread_pct: max_bid_ask_spread_pct, min_open_interest: min_open_interest, min_volume: min_volume}
|
240
|
+
liquidity_filters_validated_query = validate_option_chain_liquidity_filters!(**l_filters)
|
241
|
+
result.merge!(liquidity_filters_validated_query)
|
242
|
+
|
243
|
+
# handle other filters
|
244
|
+
o_filters = {non_standard: non_standard, side: side, am: am, pm: pm}
|
245
|
+
other_filters_validated_query = validate_option_chain_other_filters!(**o_filters)
|
246
|
+
result.merge!(other_filters_validated_query)
|
247
|
+
end
|
248
|
+
|
249
|
+
# private methods
|
128
250
|
def validate_from_to_countback_strategy(
|
129
251
|
from: nil, to: nil, countback: nil
|
130
252
|
)
|
@@ -157,6 +279,185 @@ module MarketData
|
|
157
279
|
if t.kind_of?(Integer)
|
158
280
|
return t > 0
|
159
281
|
end
|
282
|
+
false
|
283
|
+
end
|
284
|
+
|
285
|
+
def validate_expiration_filters!(
|
286
|
+
expiration: nil, dte: nil, from: nil, to: nil, month: nil, year: nil, weekly: nil, monthly: nil, quarterly: nil
|
287
|
+
)
|
288
|
+
result = {}
|
289
|
+
|
290
|
+
if !expiration.nil?
|
291
|
+
raise BadParameterError.new("invalid date for `expiration`: #{expiration}") if expiration != "all" && !time_valid?(expiration)
|
292
|
+
result.merge!({expiration: expiration})
|
293
|
+
end
|
294
|
+
|
295
|
+
# dte is exclusive with from & to
|
296
|
+
if [dte, to, from].count(nil) == 0
|
297
|
+
raise BadParameterError.new("either `dte` or (`from` and `to`) must be present")
|
298
|
+
end
|
299
|
+
|
300
|
+
if !dte.nil?
|
301
|
+
raise BadParameterError.new("invalid value for `dte`: should be an integer") if !dte.kind_of? Integer
|
302
|
+
result.merge!({dte: dte})
|
303
|
+
end
|
304
|
+
|
305
|
+
to_from_present = [from, to].count(nil)
|
306
|
+
if to_from_present == 1
|
307
|
+
raise BadParameterError.new("either none or both `from` and `to` must be included")
|
308
|
+
end
|
309
|
+
if to_from_present == 0
|
310
|
+
raise BadParameterError.new("invalid date `from`: #{from}") if !time_valid?(from)
|
311
|
+
raise BadParameterError.new("invalid date `to`: #{to}") if !time_valid?(to)
|
312
|
+
result.merge!({from: from, to: to})
|
313
|
+
end
|
314
|
+
|
315
|
+
if !month.nil?
|
316
|
+
if !month.kind_of?(Integer) || month > 12 || month < 1
|
317
|
+
raise BadParameterError.new("`month` must be a number between 1 and 12 both inclusive")
|
318
|
+
end
|
319
|
+
result.merge!({month: month})
|
320
|
+
end
|
321
|
+
|
322
|
+
if !year.nil?
|
323
|
+
if !year.kind_of?(Integer) || year < 1920
|
324
|
+
raise BadParameterError.new("`year` must be a number greater or equal than 1920")
|
325
|
+
end
|
326
|
+
result.merge!({year: year})
|
327
|
+
end
|
328
|
+
|
329
|
+
timely_present = [weekly, monthly, quarterly].reject { |x| x.nil? }
|
330
|
+
|
331
|
+
# check boolean-ness
|
332
|
+
all_boolean = timely_present.all? { |x| x.kind_of?(FalseClass) || x.kind_of?(TrueClass)}
|
333
|
+
raise BadParameterError.new("`weekly`, `monthly` and `quarterly` must be either true or false or nil") if !all_boolean
|
334
|
+
|
335
|
+
# check all are the same
|
336
|
+
all_true = timely_present.all? { |x| x.kind_of?(TrueClass)}
|
337
|
+
all_false = timely_present.all? { |x| x.kind_of?(FalseClass)}
|
338
|
+
raise BadParameterError.new("if two or more of `weekly`, `monthly` and `quarterly` are supplied, they must all have the same value") if !all_true && !all_false
|
339
|
+
result.merge!({weekly: weekly}) if !weekly.nil?
|
340
|
+
result.merge!({monthly: monthly}) if !monthly.nil?
|
341
|
+
result.merge!({quarterly: quarterly}) if !quarterly.nil?
|
342
|
+
|
343
|
+
result
|
344
|
+
end
|
345
|
+
|
346
|
+
def validate_option_chain_strike_filters!(
|
347
|
+
strike: nil, delta: nil, strike_limit: nil, range: nil
|
348
|
+
)
|
349
|
+
result = {}
|
350
|
+
if !strike.nil?
|
351
|
+
raise BadParameterError.new("`strike` must be string") if !strike.kind_of?(String)
|
352
|
+
result.merge!({strike: strike})
|
353
|
+
end
|
354
|
+
|
355
|
+
if !delta.nil?
|
356
|
+
raise BadParameterError.new("`delta` must be string") if !delta.kind_of?(String)
|
357
|
+
result.merge!({delta: delta})
|
358
|
+
end
|
359
|
+
|
360
|
+
if !strike_limit.nil?
|
361
|
+
raise BadParameterError.new("`strike_limit` must be a number") if !strike_limit.kind_of?(Numeric)
|
362
|
+
result.merge!({strikeLimit: strike_limit})
|
363
|
+
end
|
364
|
+
|
365
|
+
if !range.nil?
|
366
|
+
if !Constants::RANGE_ALLOWED.include?(range)
|
367
|
+
raise BadParameterError.new("`range` must be either nil or one of: `itm`,`otm`,`all`")
|
368
|
+
else
|
369
|
+
result.merge!({range: range})
|
370
|
+
end
|
371
|
+
end
|
372
|
+
result
|
373
|
+
end
|
374
|
+
|
375
|
+
def validate_option_chain_liquidity_filters!(
|
376
|
+
min_bid: nil, max_bid: nil, min_ask: nil, max_ask: nil, max_bid_ask_spread: nil,
|
377
|
+
max_bid_ask_spread_pct: nil, min_open_interest: nil, min_volume: nil
|
378
|
+
)
|
379
|
+
result = {}
|
380
|
+
if !min_bid.nil?
|
381
|
+
raise BadParameterError.new("`min_bid` must be a number") if !min_bid.kind_of?(Numeric)
|
382
|
+
result.merge!({minBid: min_bid})
|
383
|
+
end
|
384
|
+
if !max_bid.nil?
|
385
|
+
raise BadParameterError.new("`max_bid` must be a number") if !max_bid.kind_of?(Numeric)
|
386
|
+
result.merge!({maxBid: max_bid})
|
387
|
+
end
|
388
|
+
if !min_ask.nil?
|
389
|
+
raise BadParameterError.new("`min_ask` must be a number") if !min_ask.kind_of?(Numeric)
|
390
|
+
result.merge!({minAsk: min_ask})
|
391
|
+
end
|
392
|
+
if !max_ask.nil?
|
393
|
+
raise BadParameterError.new("`max_ask` must be a number") if !max_ask.kind_of?(Numeric)
|
394
|
+
result.merge!({maxAsk: max_ask})
|
395
|
+
end
|
396
|
+
if !max_bid_ask_spread.nil?
|
397
|
+
raise BadParameterError.new("`max_bid_ask_spread` must be a number") if !max_bid_ask_spread.kind_of?(Numeric)
|
398
|
+
result.merge!({maxBidAskSpread: max_bid_ask_spread})
|
399
|
+
end
|
400
|
+
if !max_bid_ask_spread_pct.nil?
|
401
|
+
raise BadParameterError.new("`max_bid_ask_spread_pct` must be a number") if !max_bid_ask_spread_pct.kind_of?(Numeric)
|
402
|
+
result.merge!({maxBidAskSpreadPct: max_bid_ask_spread_pct})
|
403
|
+
end
|
404
|
+
if !min_open_interest.nil?
|
405
|
+
raise BadParameterError.new("`min_open_interest` must be a number") if !min_open_interest.kind_of?(Numeric)
|
406
|
+
result.merge!({minOpenInterest: min_open_interest})
|
407
|
+
end
|
408
|
+
if !min_volume.nil?
|
409
|
+
raise BadParameterError.new("`min_volume` must be a number") if !min_volume.kind_of?(Numeric)
|
410
|
+
result.merge!({minVolume: min_volume})
|
411
|
+
end
|
412
|
+
result
|
413
|
+
end
|
414
|
+
|
415
|
+
def validate_option_chain_other_filters!(non_standard: nil, side: nil, am: nil, pm: nil)
|
416
|
+
result = {}
|
417
|
+
if !non_standard.nil?
|
418
|
+
raise BadParameterError.new("`non_standard` can be either nil or a boolean") if ![FalseClass, TrueClass].include?(non_standard.class)
|
419
|
+
result.merge!({nonstandard: non_standard}) if non_standard
|
420
|
+
|
421
|
+
end
|
422
|
+
if !side.nil?
|
423
|
+
if ![Constants::SIDE_CALL.downcase, Constants::SIDE_PUT.downcase].include?(side)
|
424
|
+
raise BadParameterError.new("`side` must be either '#{Constants::SIDE_PUT.downcase}' or '#{Constants::SIDE_CALL.downcase}'. Found: '#{side}'")
|
425
|
+
end
|
426
|
+
result.merge!({side: side})
|
427
|
+
end
|
428
|
+
if !am.nil?
|
429
|
+
raise BadParameterError.new("`non_standard` can be either nil or a boolean") if ![FalseClass, TrueClass].include?(am.class)
|
430
|
+
result.merge!({am: am})
|
431
|
+
end
|
432
|
+
if !pm.nil?
|
433
|
+
raise BadParameterError.new("`non_standard` can be either nil or a boolean") if ![FalseClass, TrueClass].include?(pm.class)
|
434
|
+
result.merge!({pm: pm})
|
435
|
+
end
|
436
|
+
result
|
437
|
+
end
|
438
|
+
|
439
|
+
def validate_option_quote_input!(symbol: nil, date: nil, from: nil, to: nil)
|
440
|
+
result = {}
|
441
|
+
if symbol.nil? || !symbol.kind_of?(String) || symbol.empty?
|
442
|
+
raise BadParameterError.new("symbol must be present. Found: #{symbol}")
|
443
|
+
end
|
444
|
+
|
445
|
+
if !date.nil?
|
446
|
+
raise BadParameterError.new("invalid date: #{date}") if !time_valid?(date)
|
447
|
+
return result.merge!({date: date})
|
448
|
+
end
|
449
|
+
|
450
|
+
if [to, from].count(nil) == 1
|
451
|
+
raise BadParameterError.new("either none or both `to` and `from` must be present")
|
452
|
+
end
|
453
|
+
|
454
|
+
if [to, from].count(nil) == 0
|
455
|
+
raise BadParameterError.new("invalid `to`: #{to}") if !time_valid?(to)
|
456
|
+
raise BadParameterError.new("invalid `from`: #{from}") if !time_valid?(from)
|
457
|
+
result.merge!({from: from, to: to})
|
458
|
+
end
|
459
|
+
|
460
|
+
result
|
160
461
|
end
|
161
462
|
end
|
162
463
|
end
|
data/lib/market_data/version.rb
CHANGED
data/lib/market_data.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative "market_data/version"
|
|
4
4
|
require "market_data/quotes"
|
5
5
|
require "market_data/markets"
|
6
6
|
require "market_data/indexes"
|
7
|
+
require "market_data/options"
|
7
8
|
|
8
9
|
module MarketData
|
9
10
|
@@base_host = "api.marketdata.app"
|
@@ -12,6 +13,7 @@ module MarketData
|
|
12
13
|
include MarketData::Quotes
|
13
14
|
include MarketData::Markets
|
14
15
|
include MarketData::Indexes
|
16
|
+
include MarketData::Options
|
15
17
|
|
16
18
|
def initialize token
|
17
19
|
@access_token = token
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: market_data
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastián González
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-11-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A Ruby client for the MarketData API.
|
14
14
|
email:
|
@@ -32,6 +32,7 @@ files:
|
|
32
32
|
- lib/market_data/mappers.rb
|
33
33
|
- lib/market_data/markets.rb
|
34
34
|
- lib/market_data/models.rb
|
35
|
+
- lib/market_data/options.rb
|
35
36
|
- lib/market_data/quotes.rb
|
36
37
|
- lib/market_data/validations.rb
|
37
38
|
- lib/market_data/version.rb
|