schwab_rb 0.8.2 → 0.9.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: f843b2f06b701abac043f861a8337225e3f4e625bd894fbaf23b71b014cebe48
4
- data.tar.gz: 60d8af1b0f3f79f3b73797bb21b6fbe05ce6e13ba9cb28ae46d48e7f75ef7a07
3
+ metadata.gz: 4886933bb9c99f66735ff9503b0904ac55a605e653550d413713c8d33e412a79
4
+ data.tar.gz: d4f9476eeb6f97b00d890295e1edb6002c0780f4c34eb1b321d2355d55853e6a
5
5
  SHA512:
6
- metadata.gz: dc656b71d0bc3a10d79d34be8f1a316fe864862e9ef3b1d7d448800938bf2196ecc8c6ba88e7c780431f04ab8d040b4a4a211e8df45b0d34eaafa85bc57a0e63
7
- data.tar.gz: 0a2b5bb6385d9bad29eca36c8684b431f80c4456e99d9a38a994c956331dfcd236d61008d3ea4396fea40c1ad7e4b3675006cefd851484ad4d3d44545f79202d
6
+ metadata.gz: 7998b7bbbfac20de7e62b3dc2ccc6472be8a19b2956f2a2f93c1fbd441c0ff0e5713004f27cc96b6b27286aead9457a16aca4d92a62411ea1a662a6f67d9f49f
7
+ data.tar.gz: e911c3bf6958b4dc0858669e0f38795466a4abf6d202805f6f12fa19ec6a991b1819b8743c682b89e12908595cb856ca8a15bea4b1011a4d6549f9a4a40feb6b
data/.rubocop.yml CHANGED
@@ -41,3 +41,4 @@ Metrics/ClassLength:
41
41
  Metrics/ModuleLength:
42
42
  Exclude:
43
43
  - 'spec/**/*'
44
+ - 'lib/schwab_rb/option_sample/downloader.rb'
@@ -1,12 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "csv"
4
3
  require "date"
5
4
  require "json"
6
5
  require "optparse"
7
6
  require "pathname"
8
- require "fileutils"
9
- require "time"
10
7
 
11
8
  module SchwabRb
12
9
  module CLI
@@ -18,33 +15,6 @@ module SchwabRb
18
15
  DEFAULT_HISTORY_DIR = "~/.schwab_rb/data/history"
19
16
  DEFAULT_OPTIONS_DIR = "~/.schwab_rb/data/options"
20
17
  SUPPORTED_FORMATS = %w[csv json].freeze
21
- OPTION_SAMPLE_CSV_HEADERS = %w[
22
- contract_type
23
- symbol
24
- description
25
- strike
26
- expiration_date
27
- mark
28
- bid
29
- bid_size
30
- ask
31
- ask_size
32
- last
33
- last_size
34
- open_interest
35
- total_volume
36
- delta
37
- gamma
38
- theta
39
- vega
40
- rho
41
- volatility
42
- theoretical_volatility
43
- theoretical_option_value
44
- intrinsic_value
45
- extrinsic_value
46
- underlying_price
47
- ].freeze
48
18
  PERIOD_TYPES = {
49
19
  "day" => SchwabRb::PriceHistory::PeriodTypes::DAY,
50
20
  "month" => SchwabRb::PriceHistory::PeriodTypes::MONTH,
@@ -221,14 +191,13 @@ module SchwabRb
221
191
 
222
192
  validate_option_sample_options!(options)
223
193
 
224
- output_path = resolve_option_sample_response(options)
194
+ _, output_path = resolve_option_sample_response(options)
225
195
 
226
196
  stdout.puts("Saved #{options[:symbol]} option sample to #{output_path}")
227
197
  0
228
198
  end
229
199
  # rubocop:enable Metrics/AbcSize
230
200
 
231
- # rubocop:disable Metrics/AbcSize
232
201
  def resolve_price_history_response(options)
233
202
  directory = SchwabRb::PathSupport.expand_path(options.fetch(:dir))
234
203
  SchwabRb::PriceHistory::Downloader.resolve(
@@ -245,148 +214,20 @@ module SchwabRb
245
214
  period: options[:period]
246
215
  )
247
216
  end
248
- # rubocop:enable Metrics/AbcSize
249
217
 
250
218
  def resolve_option_sample_response(options)
251
219
  directory = SchwabRb::PathSupport.expand_path(options.fetch(:dir))
252
- response = fetch_option_sample(build_non_interactive_client, options)
253
- FileUtils.mkdir_p(directory)
254
- output_path = option_sample_output_path(directory, options, response)
255
- write_option_sample(output_path, response, options)
256
- output_path
257
- end
258
-
259
- def write_option_sample(output_path, response, options)
260
- File.write(output_path, serialized_option_sample(response, options))
261
- end
262
-
263
- def serialized_option_sample(response, options)
264
- case options.fetch(:format)
265
- when "json"
266
- JSON.pretty_generate(response)
267
- when "csv"
268
- serialized_option_sample_csv(response, options)
269
- else
270
- raise Error, "Unsupported format `#{options[:format]}`."
271
- end
272
- end
273
-
274
- def serialized_option_sample_csv(response, options)
275
- sample_timestamp = options.fetch(:timestamp).utc.iso8601
276
-
277
- CSV.generate do |csv|
278
- csv << OPTION_SAMPLE_CSV_HEADERS
279
- option_sample_rows(response).each do |option|
280
- csv << option_sample_csv_row(response, option, sample_timestamp)
281
- end
282
- end
283
- end
284
-
285
- # rubocop:disable Metrics/AbcSize
286
- def option_sample_csv_row(response, option, _sample_timestamp)
287
- [
288
- option[:putCall],
289
- option[:symbol],
290
- option[:description],
291
- option[:strikePrice],
292
- normalize_option_date(option[:expirationDate]),
293
- option[:mark],
294
- option[:bid],
295
- option[:bidSize],
296
- option[:ask],
297
- option[:askSize],
298
- option[:last],
299
- option[:lastSize],
300
- option[:openInterest],
301
- option[:totalVolume],
302
- option[:delta],
303
- option[:gamma],
304
- option[:theta],
305
- option[:vega],
306
- option[:rho],
307
- option[:volatility],
308
- option[:theoreticalVolatility],
309
- option[:theoreticalOptionValue],
310
- option[:intrinsicValue],
311
- option[:extrinsicValue],
312
- response[:underlyingPrice]
313
- ]
314
- end
315
- # rubocop:enable Metrics/AbcSize
316
-
317
- def fetch_option_sample(client, options)
318
- expiration_date = options.fetch(:expiration_date)
319
-
320
- response = client.get_option_chain(
321
- SchwabRb::PriceHistory::Downloader.api_symbol(options.fetch(:symbol)),
322
- contract_type: SchwabRb::Option::ContractTypes::ALL,
323
- strike_range: SchwabRb::Option::StrikeRanges::ALL,
324
- from_date: expiration_date,
325
- to_date: expiration_date,
326
- return_data_objects: false
327
- )
328
-
329
- filter_option_sample_response(response, options[:root])
330
- end
331
-
332
- def option_sample_rows(response)
333
- rows = [response[:callExpDateMap], response[:putExpDateMap]].compact.flat_map do |date_map|
334
- option_rows_from_date_map(date_map)
335
- end
336
-
337
- rows.sort_by do |option|
338
- [
339
- normalize_option_date(option[:expirationDate]).to_s,
340
- option[:putCall].to_s,
341
- option[:strikePrice].to_f
342
- ]
343
- end
344
- end
345
-
346
- def option_rows_from_date_map(date_map)
347
- date_map.values.flat_map do |strikes|
348
- strikes.values.flatten.map { |option| option.transform_keys(&:to_sym) }
349
- end
350
- end
351
-
352
- def filter_option_sample_response(response, option_root)
353
- return response if blank?(option_root)
354
-
355
- normalized_root = option_root.to_s.strip.upcase
356
- filtered_call_map = filter_option_date_map_by_root(response[:callExpDateMap], normalized_root)
357
- filtered_put_map = filter_option_date_map_by_root(response[:putExpDateMap], normalized_root)
358
-
359
- response.merge(
360
- callExpDateMap: filtered_call_map,
361
- putExpDateMap: filtered_put_map
220
+ SchwabRb::OptionSample::Downloader.resolve(
221
+ client: build_non_interactive_client,
222
+ symbol: options.fetch(:symbol),
223
+ root: options[:root],
224
+ expiration_date: options.fetch(:expiration_date),
225
+ directory: directory,
226
+ format: options.fetch(:format),
227
+ timestamp: options.fetch(:timestamp)
362
228
  )
363
229
  end
364
230
 
365
- def filter_option_date_map_by_root(date_map, option_root)
366
- return {} unless date_map
367
-
368
- date_map.each_with_object({}) do |(expiration_key, strikes), filtered_dates|
369
- filtered_strikes = strikes.each_with_object({}) do |(strike, contracts), filtered_by_strike|
370
- matching_contracts = contracts.select { |contract| contract[:optionRoot].to_s.upcase == option_root }
371
- filtered_by_strike[strike] = matching_contracts if matching_contracts.any?
372
- end
373
-
374
- filtered_dates[expiration_key] = filtered_strikes if filtered_strikes.any?
375
- end
376
- end
377
-
378
- def normalize_option_date(value)
379
- return if value.nil?
380
-
381
- Date.parse(value.to_s).iso8601
382
- end
383
-
384
- def normalize_option_timestamp(value)
385
- return if value.nil?
386
-
387
- Time.at(value / 1000.0).utc.iso8601
388
- end
389
-
390
231
  def build_non_interactive_client
391
232
  credentials = load_credentials(require_callback_url: false)
392
233
  token_path = resolved_token_path
@@ -494,24 +335,6 @@ module SchwabRb
494
335
  )
495
336
  end
496
337
 
497
- def option_sample_output_path(directory, options, response)
498
- File.join(
499
- directory,
500
- [
501
- SchwabRb::PriceHistory::Downloader.sanitize_symbol(
502
- options[:root] || option_sample_root(response, options.fetch(:symbol))
503
- ),
504
- "exp#{options.fetch(:expiration_date).iso8601}",
505
- options.fetch(:timestamp).strftime("%Y-%m-%d_%H-%M-%S")
506
- ].join("_") + ".#{options.fetch(:format)}"
507
- )
508
- end
509
-
510
- def option_sample_root(response, fallback_symbol)
511
- first_option = option_sample_rows(response).find { |option| !blank?(option[:optionRoot]) }
512
- first_option ? first_option[:optionRoot] : fallback_symbol
513
- end
514
-
515
338
  def blank?(value)
516
339
  value.nil? || value.to_s.strip.empty?
517
340
  end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "json"
5
+ require "time"
6
+ require "fileutils"
7
+
8
+ module SchwabRb
9
+ class OptionSample
10
+ # Public file-oriented downloader for one-expiration option chain samples.
11
+ module Downloader
12
+ SUPPORTED_FORMATS = %w[csv json].freeze
13
+ CSV_HEADERS = %w[
14
+ contract_type
15
+ symbol
16
+ description
17
+ strike
18
+ expiration_date
19
+ mark
20
+ bid
21
+ bid_size
22
+ ask
23
+ ask_size
24
+ last
25
+ last_size
26
+ open_interest
27
+ total_volume
28
+ delta
29
+ gamma
30
+ theta
31
+ vega
32
+ rho
33
+ volatility
34
+ theoretical_volatility
35
+ theoretical_option_value
36
+ intrinsic_value
37
+ extrinsic_value
38
+ underlying_price
39
+ ].freeze
40
+
41
+ module_function
42
+
43
+ # rubocop:disable Metrics/ParameterLists
44
+ def resolve(client:, symbol:, expiration_date:, directory:, format:, timestamp:, root: nil)
45
+ response = fetch(
46
+ client: client,
47
+ symbol: symbol,
48
+ expiration_date: expiration_date,
49
+ root: root
50
+ )
51
+
52
+ FileUtils.mkdir_p(directory)
53
+ path = output_path(
54
+ directory: directory,
55
+ symbol: symbol,
56
+ expiration_date: expiration_date,
57
+ format: format,
58
+ timestamp: timestamp,
59
+ root: root,
60
+ response: response
61
+ )
62
+ File.write(path, serialize(response: response, format: format, timestamp: timestamp))
63
+
64
+ [response, path]
65
+ end
66
+ # rubocop:enable Metrics/ParameterLists
67
+
68
+ def fetch(client:, symbol:, expiration_date:, root: nil)
69
+ response = client.get_option_chain(
70
+ SchwabRb::PriceHistory::Downloader.api_symbol(symbol),
71
+ contract_type: SchwabRb::Option::ContractTypes::ALL,
72
+ strike_range: SchwabRb::Option::StrikeRanges::ALL,
73
+ from_date: expiration_date,
74
+ to_date: expiration_date,
75
+ return_data_objects: false
76
+ )
77
+
78
+ filter_response_by_root(response, root)
79
+ end
80
+
81
+ def filter_response_by_root(response, option_root)
82
+ return response if blank?(option_root)
83
+
84
+ normalized_root = option_root.to_s.strip.upcase
85
+
86
+ response.merge(
87
+ callExpDateMap: filter_date_map_by_root(response[:callExpDateMap], normalized_root),
88
+ putExpDateMap: filter_date_map_by_root(response[:putExpDateMap], normalized_root)
89
+ )
90
+ end
91
+
92
+ def filter_date_map_by_root(date_map, option_root)
93
+ return {} unless date_map
94
+
95
+ date_map.each_with_object({}) do |(expiration_key, strikes), filtered_dates|
96
+ filtered_strikes = strikes.each_with_object({}) do |(strike, contracts), filtered_by_strike|
97
+ matching_contracts = contracts.select { |contract| contract[:optionRoot].to_s.upcase == option_root }
98
+ filtered_by_strike[strike] = matching_contracts if matching_contracts.any?
99
+ end
100
+
101
+ filtered_dates[expiration_key] = filtered_strikes if filtered_strikes.any?
102
+ end
103
+ end
104
+
105
+ # rubocop:disable Metrics/ParameterLists
106
+ def output_path(directory:, symbol:, expiration_date:, format:, timestamp:, root: nil, response: nil)
107
+ selected_root = root || option_root(response, symbol)
108
+
109
+ File.join(
110
+ directory,
111
+ [
112
+ SchwabRb::PriceHistory::Downloader.sanitize_symbol(selected_root),
113
+ "exp#{expiration_date.iso8601}",
114
+ timestamp.strftime("%Y-%m-%d_%H-%M-%S")
115
+ ].join("_") + ".#{format}"
116
+ )
117
+ end
118
+ # rubocop:enable Metrics/ParameterLists
119
+
120
+ def serialize(response:, format:, timestamp:)
121
+ case format
122
+ when "json"
123
+ JSON.pretty_generate(response)
124
+ when "csv"
125
+ serialize_csv(response: response, timestamp: timestamp)
126
+ else
127
+ raise ArgumentError, "Unsupported format `#{format}`."
128
+ end
129
+ end
130
+
131
+ def serialize_csv(response:, timestamp:)
132
+ sample_timestamp = timestamp.utc.iso8601
133
+
134
+ CSV.generate do |csv|
135
+ csv << CSV_HEADERS
136
+ rows(response).each do |option|
137
+ csv << csv_row(response, option, sample_timestamp)
138
+ end
139
+ end
140
+ end
141
+
142
+ # rubocop:disable Metrics/AbcSize
143
+ def csv_row(response, option, _sample_timestamp)
144
+ [
145
+ option[:putCall],
146
+ option[:symbol],
147
+ option[:description],
148
+ option[:strikePrice],
149
+ normalize_option_date(option[:expirationDate]),
150
+ option[:mark],
151
+ option[:bid],
152
+ option[:bidSize],
153
+ option[:ask],
154
+ option[:askSize],
155
+ option[:last],
156
+ option[:lastSize],
157
+ option[:openInterest],
158
+ option[:totalVolume],
159
+ option[:delta],
160
+ option[:gamma],
161
+ option[:theta],
162
+ option[:vega],
163
+ option[:rho],
164
+ option[:volatility],
165
+ option[:theoreticalVolatility],
166
+ option[:theoreticalOptionValue],
167
+ option[:intrinsicValue],
168
+ option[:extrinsicValue],
169
+ response[:underlyingPrice]
170
+ ]
171
+ end
172
+ # rubocop:enable Metrics/AbcSize
173
+
174
+ def rows(response)
175
+ extracted_rows = [response[:callExpDateMap], response[:putExpDateMap]].compact.flat_map do |date_map|
176
+ rows_from_date_map(date_map)
177
+ end
178
+
179
+ extracted_rows.sort_by do |option|
180
+ [
181
+ normalize_option_date(option[:expirationDate]).to_s,
182
+ option[:putCall].to_s,
183
+ option[:strikePrice].to_f
184
+ ]
185
+ end
186
+ end
187
+
188
+ def rows_from_date_map(date_map)
189
+ date_map.values.flat_map do |strikes|
190
+ strikes.values.flatten.map { |option| option.transform_keys(&:to_sym) }
191
+ end
192
+ end
193
+
194
+ def option_root(response, fallback_symbol)
195
+ first_option = rows(response).find { |option| !blank?(option[:optionRoot]) }
196
+ first_option ? first_option[:optionRoot] : fallback_symbol
197
+ end
198
+
199
+ def normalize_option_date(value)
200
+ return if value.nil?
201
+
202
+ Date.parse(value.to_s).iso8601
203
+ end
204
+
205
+ def blank?(value)
206
+ value.nil? || value.to_s.strip.empty?
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb
4
+ class OptionSample
5
+ end
6
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwabRb
4
- VERSION = "0.8.2"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/schwab_rb.rb CHANGED
@@ -23,6 +23,8 @@ require_relative "schwab_rb/orders/instruments"
23
23
  require_relative "schwab_rb/market_hours"
24
24
  require_relative "schwab_rb/price_history"
25
25
  require_relative "schwab_rb/price_history/downloader"
26
+ require_relative "schwab_rb/option_sample"
27
+ require_relative "schwab_rb/option_sample/downloader"
26
28
  require_relative "schwab_rb/movers"
27
29
  require_relative "schwab_rb/orders/builder"
28
30
  require_relative "schwab_rb/orders/session"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schwab_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joseph Platta
@@ -256,6 +256,8 @@ files:
256
256
  - lib/schwab_rb/market_hours.rb
257
257
  - lib/schwab_rb/movers.rb
258
258
  - lib/schwab_rb/option.rb
259
+ - lib/schwab_rb/option_sample.rb
260
+ - lib/schwab_rb/option_sample/downloader.rb
259
261
  - lib/schwab_rb/orders/builder.rb
260
262
  - lib/schwab_rb/orders/destination.rb
261
263
  - lib/schwab_rb/orders/duration.rb