Module: SchwabRb::OptionSample::Downloader

Defined in:
lib/schwab_rb/option_sample/downloader.rb

Overview

Public file-oriented downloader for one-expiration option chain samples.

Constant Summary collapse

SUPPORTED_FORMATS =
%w[csv json].freeze
CSV_HEADERS =
%w[
  contract_type
  symbol
  description
  strike
  expiration_date
  mark
  bid
  bid_size
  ask
  ask_size
  last
  last_size
  open_interest
  total_volume
  delta
  gamma
  theta
  vega
  rho
  volatility
  theoretical_volatility
  theoretical_option_value
  intrinsic_value
  extrinsic_value
  underlying_price
].freeze

Class Method Summary collapse

Class Method Details

.blank?(value) ⇒ Boolean

Returns:

  • (Boolean)


205
206
207
# File 'lib/schwab_rb/option_sample/downloader.rb', line 205

def blank?(value)
  value.nil? || value.to_s.strip.empty?
end

.csv_row(response, option, _sample_timestamp) ⇒ Object

rubocop:disable Metrics/AbcSize



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/schwab_rb/option_sample/downloader.rb', line 143

def csv_row(response, option, _sample_timestamp)
  [
    option[:putCall],
    option[:symbol],
    option[:description],
    option[:strikePrice],
    normalize_option_date(option[:expirationDate]),
    option[:mark],
    option[:bid],
    option[:bidSize],
    option[:ask],
    option[:askSize],
    option[:last],
    option[:lastSize],
    option[:openInterest],
    option[:totalVolume],
    option[:delta],
    option[:gamma],
    option[:theta],
    option[:vega],
    option[:rho],
    option[:volatility],
    option[:theoreticalVolatility],
    option[:theoreticalOptionValue],
    option[:intrinsicValue],
    option[:extrinsicValue],
    response[:underlyingPrice]
  ]
end

.fetch(client:, symbol:, expiration_date:, root: nil) ⇒ Object

rubocop:enable Metrics/ParameterLists



68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/schwab_rb/option_sample/downloader.rb', line 68

def fetch(client:, symbol:, expiration_date:, root: nil)
  response = client.get_option_chain(
    SchwabRb::PriceHistory::Downloader.api_symbol(symbol),
    contract_type: SchwabRb::Option::ContractTypes::ALL,
    strike_range: SchwabRb::Option::StrikeRanges::ALL,
    from_date: expiration_date,
    to_date: expiration_date,
    return_data_objects: false
  )

  filter_response_by_root(response, root)
end

.filter_date_map_by_root(date_map, option_root) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/schwab_rb/option_sample/downloader.rb', line 92

def filter_date_map_by_root(date_map, option_root)
  return {} unless date_map

  date_map.each_with_object({}) do |(expiration_key, strikes), filtered_dates|
    filtered_strikes = strikes.each_with_object({}) do |(strike, contracts), filtered_by_strike|
      matching_contracts = contracts.select { |contract| contract[:optionRoot].to_s.upcase == option_root }
      filtered_by_strike[strike] = matching_contracts if matching_contracts.any?
    end

    filtered_dates[expiration_key] = filtered_strikes if filtered_strikes.any?
  end
end

.filter_response_by_root(response, option_root) ⇒ Object



81
82
83
84
85
86
87
88
89
90
# File 'lib/schwab_rb/option_sample/downloader.rb', line 81

def filter_response_by_root(response, option_root)
  return response if blank?(option_root)

  normalized_root = option_root.to_s.strip.upcase

  response.merge(
    callExpDateMap: filter_date_map_by_root(response[:callExpDateMap], normalized_root),
    putExpDateMap: filter_date_map_by_root(response[:putExpDateMap], normalized_root)
  )
end

.normalize_option_date(value) ⇒ Object



199
200
201
202
203
# File 'lib/schwab_rb/option_sample/downloader.rb', line 199

def normalize_option_date(value)
  return if value.nil?

  Date.parse(value.to_s).iso8601
end

.option_root(response, fallback_symbol) ⇒ Object



194
195
196
197
# File 'lib/schwab_rb/option_sample/downloader.rb', line 194

def option_root(response, fallback_symbol)
  first_option = rows(response).find { |option| !blank?(option[:optionRoot]) }
  first_option ? first_option[:optionRoot] : fallback_symbol
end

.output_path(directory:, symbol:, expiration_date:, format:, timestamp:, root: nil, response: nil) ⇒ Object

rubocop:disable Metrics/ParameterLists



106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/schwab_rb/option_sample/downloader.rb', line 106

def output_path(directory:, symbol:, expiration_date:, format:, timestamp:, root: nil, response: nil)
  selected_root = root || option_root(response, symbol)

  File.join(
    directory,
    [
      SchwabRb::PriceHistory::Downloader.sanitize_symbol(selected_root),
      "exp#{expiration_date.iso8601}",
      timestamp.strftime("%Y-%m-%d_%H-%M-%S")
    ].join("_") + ".#{format}"
  )
end

.resolve(client:, symbol:, expiration_date:, directory:, format:, timestamp:, root: nil) ⇒ Object

rubocop:disable Metrics/ParameterLists



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/schwab_rb/option_sample/downloader.rb', line 44

def resolve(client:, symbol:, expiration_date:, directory:, format:, timestamp:, root: nil)
  response = fetch(
    client: client,
    symbol: symbol,
    expiration_date: expiration_date,
    root: root
  )

  FileUtils.mkdir_p(directory)
  path = output_path(
    directory: directory,
    symbol: symbol,
    expiration_date: expiration_date,
    format: format,
    timestamp: timestamp,
    root: root,
    response: response
  )
  File.write(path, serialize(response: response, format: format, timestamp: timestamp))

  [response, path]
end

.rows(response) ⇒ Object

rubocop:enable Metrics/AbcSize



174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/schwab_rb/option_sample/downloader.rb', line 174

def rows(response)
  extracted_rows = [response[:callExpDateMap], response[:putExpDateMap]].compact.flat_map do |date_map|
    rows_from_date_map(date_map)
  end

  extracted_rows.sort_by do |option|
    [
      normalize_option_date(option[:expirationDate]).to_s,
      option[:putCall].to_s,
      option[:strikePrice].to_f
    ]
  end
end

.rows_from_date_map(date_map) ⇒ Object



188
189
190
191
192
# File 'lib/schwab_rb/option_sample/downloader.rb', line 188

def rows_from_date_map(date_map)
  date_map.values.flat_map do |strikes|
    strikes.values.flatten.map { |option| option.transform_keys(&:to_sym) }
  end
end

.serialize(response:, format:, timestamp:) ⇒ Object

rubocop:enable Metrics/ParameterLists



120
121
122
123
124
125
126
127
128
129
# File 'lib/schwab_rb/option_sample/downloader.rb', line 120

def serialize(response:, format:, timestamp:)
  case format
  when "json"
    JSON.pretty_generate(response)
  when "csv"
    serialize_csv(response: response, timestamp: timestamp)
  else
    raise ArgumentError, "Unsupported format `#{format}`."
  end
end

.serialize_csv(response:, timestamp:) ⇒ Object



131
132
133
134
135
136
137
138
139
140
# File 'lib/schwab_rb/option_sample/downloader.rb', line 131

def serialize_csv(response:, timestamp:)
  sample_timestamp = timestamp.utc.iso8601

  CSV.generate do |csv|
    csv << CSV_HEADERS
    rows(response).each do |option|
      csv << csv_row(response, option, sample_timestamp)
    end
  end
end