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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76061b4644a7a3092a819581faa9b7141dfb88e064b3a78a3615aa4c76ac1b0a
4
- data.tar.gz: '09d87650d7235f8419524a0bb4cba5a43dd1d8d1a4e0eae4cb1b2cc24fc27dbf'
3
+ metadata.gz: dbc923ec4b12764e6cda0c8ca9fa7e26a0c2b91c01c00d2f43b05bd2bb46f4e3
4
+ data.tar.gz: 5174eef19c848f47683c6066d53550a780c0ffe22148a54835fa66027f8b541b
5
5
  SHA512:
6
- metadata.gz: d369b7b3d1e391decd1679262e2c0f9ef8fe20463aac5ef3ebc05ce00a46c86ea94d13441e8d8d2f76ed78220a3f45b99f3cb4caf80da52d115b8e97d54fdb7f
7
- data.tar.gz: 50a5fc1e57d70f12722d84ecac720a4649a1e71ea6f4fa12f7d521f15b2ca8940c19f95e33f93d78b2b998e299f015091812a033b61011f6a1d80ea07a98c84e
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- market_data (0.4.0)
4
+ market_data (0.5.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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
- ![coverage](https://img.shields.io/badge/coverage%3A-87.68%25-yellow.svg)
5
+ ![coverage](https://img.shields.io/badge/coverage%3A-96.72%25-green.svg)
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
- - [ ] Support for optional parameters for Candles
150
- - [ ] Support for optional parameters for Bulk Candles
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
@@ -31,6 +31,10 @@ module MarketData
31
31
  URI::HTTPS.build(path_hash).to_s
32
32
  end
33
33
 
34
+ def encode_uri_component str
35
+ URI.encode_uri_component str
36
+ end
37
+
34
38
  def get_auth_headers
35
39
  { "authorization" => "Bearer #{get_token}"}
36
40
  end
@@ -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: "high52",
48
- low52: "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
@@ -4,6 +4,7 @@ module MarketData
4
4
  include MarketData::Mappers
5
5
  include MarketData::Validations
6
6
 
7
+ # TODO use delayed interpolation when building path
7
8
  @@quotes = "/v1/indices/quotes/"
8
9
  @@candles = "/v1/indices/candles/"
9
10
 
@@ -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 map_earning response
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
- mapping = Constants::CANDLE_FIELD_MAPPING
109
+ Constants::CANDLE_FIELD_MAPPING
77
110
  when :earning
78
- mapping = Constants::EARNING_FIELD_MAPPING
111
+ Constants::EARNING_FIELD_MAPPING
79
112
  when :index_candle
80
- mapping = Constants::INDEX_CANDLE_FIELD_MAPPING
113
+ Constants::INDEX_CANDLE_FIELD_MAPPING
81
114
  when :index_quote
82
- mapping = Constants::INDEX_QUOTE_FIELD_MAPPING
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
- mapping = Constants::QUOTE_FIELD_MAPPING
121
+ Constants::QUOTE_FIELD_MAPPING
85
122
  else
86
123
  raise BadParameterError.new("unrecognized model for mapping: #{kind}")
87
124
  end
@@ -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
@@ -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
- map_earning(
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MarketData
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
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.5.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-10-16 00:00:00.000000000 Z
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