schwab_rb 0.8.1 → 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 +4 -4
- data/.codex/skills/schwab-rb-cli/SKILL.md +10 -4
- data/.codex/skills/schwab-rb-cli/references/cli_examples.md +20 -0
- data/.rubocop.yml +1 -0
- data/README.md +2 -2
- data/examples/fetch_account_numbers.rb +16 -19
- data/examples/fetch_user_preferences.rb +16 -19
- data/examples/place_oco_order.rb +13 -6
- data/lib/schwab_rb/cli/app.rb +9 -186
- data/lib/schwab_rb/configuration.rb +7 -1
- data/lib/schwab_rb/option_sample/downloader.rb +210 -0
- data/lib/schwab_rb/option_sample.rb +6 -0
- data/lib/schwab_rb/utils/logger.rb +1 -1
- data/lib/schwab_rb/version.rb +1 -1
- data/lib/schwab_rb.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4886933bb9c99f66735ff9503b0904ac55a605e653550d413713c8d33e412a79
|
|
4
|
+
data.tar.gz: d4f9476eeb6f97b00d890295e1edb6002c0780f4c34eb1b321d2355d55853e6a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7998b7bbbfac20de7e62b3dc2ccc6472be8a19b2956f2a2f93c1fbd441c0ff0e5713004f27cc96b6b27286aead9457a16aca4d92a62411ea1a662a6f67d9f49f
|
|
7
|
+
data.tar.gz: e911c3bf6958b4dc0858669e0f38795466a4abf6d202805f6f12fa19ec6a991b1819b8743c682b89e12908595cb856ca8a15bea4b1011a4d6549f9a4a40feb6b
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: schwab-rb-cli
|
|
3
|
-
description: Use the installed schwab_rb CLI for authentication and price history downloads. Activate when a user wants to run the schwab_rb command, log in to Schwab, fetch market data, troubleshoot CLI installation or environment variables, or save
|
|
3
|
+
description: Use the installed schwab_rb CLI for authentication, option-chain samples, and price history downloads. Activate when a user wants to run the schwab_rb command, log in to Schwab, fetch market data, troubleshoot CLI installation or environment variables, or save CLI output from the command line.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Schwab RB CLI Skill
|
|
@@ -9,8 +9,8 @@ Use this skill when the task should be completed through the installed `schwab_r
|
|
|
9
9
|
|
|
10
10
|
## When to use
|
|
11
11
|
|
|
12
|
-
- The user wants to run `schwab_rb help`, `schwab_rb login`, or `schwab_rb price-history`
|
|
13
|
-
- The user wants to download price history data to JSON or CSV
|
|
12
|
+
- The user wants to run `schwab_rb help`, `schwab_rb login`, `schwab_rb sample`, or `schwab_rb price-history`
|
|
13
|
+
- The user wants to download an option-chain sample or price history data to JSON or CSV
|
|
14
14
|
- The user needs help with CLI installation, `asdf` shims, or missing `SCHWAB_*` environment variables
|
|
15
15
|
- The user wants the agent to verify the CLI works on the local machine
|
|
16
16
|
|
|
@@ -39,7 +39,13 @@ The wrapper only attempts to execute the globally installed `schwab_rb` command
|
|
|
39
39
|
skills/schwab-rb-cli/scripts/run_schwab_rb.sh login
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
5. For
|
|
42
|
+
5. For option-chain samples, run:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
skills/schwab-rb-cli/scripts/run_schwab_rb.sh sample --symbol SPX --expiration-date 2026-04-17
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
6. For price history downloads, run:
|
|
43
49
|
|
|
44
50
|
```bash
|
|
45
51
|
skills/schwab-rb-cli/scripts/run_schwab_rb.sh price-history --symbol AAPL --start-date 2026-03-01
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
```bash
|
|
6
6
|
skills/schwab-rb-cli/scripts/run_schwab_rb.sh help
|
|
7
7
|
skills/schwab-rb-cli/scripts/run_schwab_rb.sh help price-history
|
|
8
|
+
skills/schwab-rb-cli/scripts/run_schwab_rb.sh help sample
|
|
8
9
|
```
|
|
9
10
|
|
|
10
11
|
## Authentication
|
|
@@ -49,6 +50,25 @@ skills/schwab-rb-cli/scripts/run_schwab_rb.sh price-history \
|
|
|
49
50
|
--dir /tmp/schwab_rb_data
|
|
50
51
|
```
|
|
51
52
|
|
|
53
|
+
## Option-chain samples
|
|
54
|
+
|
|
55
|
+
Default CSV output:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
skills/schwab-rb-cli/scripts/run_schwab_rb.sh sample --symbol SPX --expiration-date 2026-04-17
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Filter by root and write JSON to a custom directory:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
skills/schwab-rb-cli/scripts/run_schwab_rb.sh sample \
|
|
65
|
+
--symbol SPX \
|
|
66
|
+
--root SPXW \
|
|
67
|
+
--expiration-date 2026-04-17 \
|
|
68
|
+
--format json \
|
|
69
|
+
--dir /tmp/schwab_rb_options
|
|
70
|
+
```
|
|
71
|
+
|
|
52
72
|
## Installation troubleshooting
|
|
53
73
|
|
|
54
74
|
Install the gem from the repo:
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
|
@@ -83,7 +83,7 @@ Before using the gem, ensure you have the following environment variables set:
|
|
|
83
83
|
- `APP_CALLBACK_URL`: The callback URL for your application.
|
|
84
84
|
- `TOKEN_PATH`: Path to store the authentication token.
|
|
85
85
|
- `SCHWAB_ACCOUNT_NUMBER`: Your Schwab account number.
|
|
86
|
-
- `SCHWAB_LOGFILE`: (Optional) Path to the log file. Defaults to
|
|
86
|
+
- `SCHWAB_LOGFILE`: (Optional) Path to the log file. Defaults to `~/.schwab_rb/schwab_rb.log`.
|
|
87
87
|
- `SCHWAB_LOG_LEVEL`: (Optional) Log level for the logger. Defaults to `WARN`. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`.
|
|
88
88
|
- `SCHWAB_SILENCE_OUTPUT`: (Optional) Set to `true` to disable logging output. Defaults to `false`.
|
|
89
89
|
|
|
@@ -91,7 +91,7 @@ You can also configure logging programmatically:
|
|
|
91
91
|
|
|
92
92
|
```ruby
|
|
93
93
|
SchwabRb.configure do |config|
|
|
94
|
-
config.logger = Logger.new(
|
|
94
|
+
config.logger = Logger.new($stdout)
|
|
95
95
|
config.log_level = 'INFO'
|
|
96
96
|
config.silence_output = false
|
|
97
97
|
end
|
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
# Script to fetch account numbers data and save as fixture
|
|
5
5
|
# Usage: ruby examples/fetch_account_numbers.rb
|
|
6
6
|
|
|
7
|
-
require_relative
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require
|
|
7
|
+
require_relative "../lib/schwab_rb"
|
|
8
|
+
require "dotenv"
|
|
9
|
+
require "json"
|
|
10
|
+
require "fileutils"
|
|
11
11
|
|
|
12
12
|
Dotenv.load
|
|
13
13
|
|
|
14
14
|
def create_client
|
|
15
|
-
token_path = ENV[
|
|
15
|
+
token_path = ENV["TOKEN_PATH"] || "schwab_token.json"
|
|
16
16
|
SchwabRb::Auth.init_client_easy(
|
|
17
|
-
ENV
|
|
18
|
-
ENV
|
|
19
|
-
ENV
|
|
17
|
+
ENV.fetch("SCHWAB_API_KEY", nil),
|
|
18
|
+
ENV.fetch("SCHWAB_APP_SECRET", nil),
|
|
19
|
+
ENV.fetch("APP_CALLBACK_URL", nil),
|
|
20
20
|
token_path
|
|
21
21
|
)
|
|
22
22
|
end
|
|
@@ -24,26 +24,23 @@ end
|
|
|
24
24
|
def fetch_account_numbers
|
|
25
25
|
client = create_client
|
|
26
26
|
puts "Fetching account numbers..."
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
response = client.get_account_numbers
|
|
29
29
|
parsed_data = JSON.parse(response.body, symbolize_names: true)
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
# Create fixtures directory if it doesn't exist
|
|
32
|
-
fixtures_dir = File.join(__dir__,
|
|
32
|
+
fixtures_dir = File.join(__dir__, "..", "spec", "fixtures")
|
|
33
33
|
FileUtils.mkdir_p(fixtures_dir)
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
# Save the raw response
|
|
36
|
-
fixture_file = File.join(fixtures_dir,
|
|
36
|
+
fixture_file = File.join(fixtures_dir, "account_numbers.json")
|
|
37
37
|
File.write(fixture_file, JSON.pretty_generate(parsed_data))
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
puts "Account numbers data saved to: #{fixture_file}"
|
|
40
40
|
puts "Sample data: #{parsed_data.first(2)}"
|
|
41
|
-
|
|
42
|
-
rescue => e
|
|
41
|
+
rescue StandardError => e
|
|
43
42
|
puts "Error fetching account numbers: #{e.message}"
|
|
44
43
|
puts e.backtrace.first(3)
|
|
45
44
|
end
|
|
46
45
|
|
|
47
|
-
if __FILE__ == $
|
|
48
|
-
fetch_account_numbers
|
|
49
|
-
end
|
|
46
|
+
fetch_account_numbers if __FILE__ == $PROGRAM_NAME
|
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
# Script to fetch user preferences data and save as fixture
|
|
5
5
|
# Usage: ruby examples/fetch_user_preferences.rb
|
|
6
6
|
|
|
7
|
-
require_relative
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require
|
|
7
|
+
require_relative "../lib/schwab_rb"
|
|
8
|
+
require "dotenv"
|
|
9
|
+
require "json"
|
|
10
|
+
require "fileutils"
|
|
11
11
|
|
|
12
12
|
Dotenv.load
|
|
13
13
|
|
|
14
14
|
def create_client
|
|
15
|
-
token_path = ENV[
|
|
15
|
+
token_path = ENV["TOKEN_PATH"] || "schwab_token.json"
|
|
16
16
|
SchwabRb::Auth.init_client_easy(
|
|
17
|
-
ENV
|
|
18
|
-
ENV
|
|
19
|
-
ENV
|
|
17
|
+
ENV.fetch("SCHWAB_API_KEY", nil),
|
|
18
|
+
ENV.fetch("SCHWAB_APP_SECRET", nil),
|
|
19
|
+
ENV.fetch("APP_CALLBACK_URL", nil),
|
|
20
20
|
token_path
|
|
21
21
|
)
|
|
22
22
|
end
|
|
@@ -24,26 +24,23 @@ end
|
|
|
24
24
|
def fetch_user_preferences
|
|
25
25
|
client = create_client
|
|
26
26
|
puts "Fetching user preferences..."
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
response = client.get_user_preferences
|
|
29
29
|
parsed_data = JSON.parse(response.body, symbolize_names: true)
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
# Create fixtures directory if it doesn't exist
|
|
32
|
-
fixtures_dir = File.join(__dir__,
|
|
32
|
+
fixtures_dir = File.join(__dir__, "..", "spec", "fixtures")
|
|
33
33
|
FileUtils.mkdir_p(fixtures_dir)
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
# Save the raw response
|
|
36
|
-
fixture_file = File.join(fixtures_dir,
|
|
36
|
+
fixture_file = File.join(fixtures_dir, "user_preferences.json")
|
|
37
37
|
File.write(fixture_file, JSON.pretty_generate(parsed_data))
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
puts "User preferences data saved to: #{fixture_file}"
|
|
40
40
|
puts "Sample data keys: #{parsed_data.keys.first(5)}"
|
|
41
|
-
|
|
42
|
-
rescue => e
|
|
41
|
+
rescue StandardError => e
|
|
43
42
|
puts "Error fetching user preferences: #{e.message}"
|
|
44
43
|
puts e.backtrace.first(3)
|
|
45
44
|
end
|
|
46
45
|
|
|
47
|
-
if __FILE__ == $
|
|
48
|
-
fetch_user_preferences
|
|
49
|
-
end
|
|
46
|
+
fetch_user_preferences if __FILE__ == $PROGRAM_NAME
|
data/examples/place_oco_order.rb
CHANGED
|
@@ -15,6 +15,8 @@ Dotenv.load
|
|
|
15
15
|
#
|
|
16
16
|
|
|
17
17
|
# SchwabRb::Configuration.configure do |config|
|
|
18
|
+
# config.schwab_home = "/path/to/your/schwab_rb_home"
|
|
19
|
+
# config.log_level = "DEBUG"
|
|
18
20
|
# end
|
|
19
21
|
|
|
20
22
|
CURRENT_ACCT = "TRADING_BROKERAGE_ACCOUNT"
|
|
@@ -46,25 +48,30 @@ oco_order = SchwabRb::Orders::OrderFactory.build(
|
|
|
46
48
|
short_leg_symbol: "SPXW 251020P06530000",
|
|
47
49
|
long_leg_symbol: "SPXW 251020P06510000",
|
|
48
50
|
order_type: SchwabRb::Order::Types::STOP_LIMIT,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
price: 2.1,
|
|
52
|
+
stop_price: 2.0,
|
|
51
53
|
order_instruction: :close,
|
|
52
54
|
credit_debit: :debit,
|
|
53
|
-
quantity:
|
|
55
|
+
quantity: 2
|
|
54
56
|
},
|
|
55
57
|
{
|
|
56
58
|
strategy_type: SchwabRb::Order::ComplexOrderStrategyTypes::VERTICAL,
|
|
57
59
|
short_leg_symbol: "SPXW 251020C06770000",
|
|
58
60
|
long_leg_symbol: "SPXW 251020C06790000",
|
|
59
61
|
order_type: SchwabRb::Order::Types::STOP_LIMIT,
|
|
60
|
-
price:
|
|
62
|
+
price: 2.1,
|
|
63
|
+
stop_price: 2.0,
|
|
61
64
|
order_instruction: :close,
|
|
62
65
|
credit_debit: :debit,
|
|
63
|
-
quantity:
|
|
66
|
+
quantity: 2
|
|
64
67
|
}
|
|
65
68
|
]
|
|
66
69
|
)
|
|
67
70
|
|
|
68
71
|
built_order = oco_order.build
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
binding.pry
|
|
74
|
+
|
|
75
|
+
# response = client.place_order(built_order, account_name: CURRENT_ACCT)
|
|
76
|
+
|
|
77
|
+
binding.pry
|
data/lib/schwab_rb/cli/app.rb
CHANGED
|
@@ -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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
@@ -25,8 +25,14 @@ module SchwabRb
|
|
|
25
25
|
!has_external_logger? && !@silence_output
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def default_log_file
|
|
29
|
+
File.join(@schwab_home, "schwab_rb.log")
|
|
30
|
+
end
|
|
31
|
+
|
|
28
32
|
def effective_log_file
|
|
29
|
-
@log_file ||
|
|
33
|
+
@log_file ||
|
|
34
|
+
(ENV["LOGFILE"] if ENV["LOGFILE"] && !ENV["LOGFILE"].empty?) ||
|
|
35
|
+
default_log_file
|
|
30
36
|
end
|
|
31
37
|
end
|
|
32
38
|
|
|
@@ -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
|
|
@@ -27,7 +27,7 @@ module SchwabRb
|
|
|
27
27
|
return null_logger if config.silence_output
|
|
28
28
|
return null_logger unless config.should_create_logger?
|
|
29
29
|
|
|
30
|
-
log_destination = config.effective_log_file
|
|
30
|
+
log_destination = config.effective_log_file
|
|
31
31
|
|
|
32
32
|
return null_logger if [:null, "/dev/null"].include?(log_destination)
|
|
33
33
|
|
data/lib/schwab_rb/version.rb
CHANGED
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,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schwab_rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joseph Platta
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-10 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|
|
@@ -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
|