schwab_rb 0.2.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 +7 -0
- data/.copilotignore +4 -0
- data/.rspec +2 -0
- data/.rspec_status +292 -0
- data/.rubocop.yml +41 -0
- data/.rubocop_todo.yml +105 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +23 -0
- data/README.md +271 -0
- data/Rakefile +12 -0
- data/doc/notes/data_objects_analysis.md +223 -0
- data/doc/notes/data_objects_refactoring_plan.md +82 -0
- data/examples/fetch_account_numbers.rb +49 -0
- data/examples/fetch_user_preferences.rb +49 -0
- data/lib/schwab_rb/account.rb +9 -0
- data/lib/schwab_rb/auth/auth_context.rb +23 -0
- data/lib/schwab_rb/auth/init_client_easy.rb +45 -0
- data/lib/schwab_rb/auth/init_client_login.rb +201 -0
- data/lib/schwab_rb/auth/init_client_token_file.rb +30 -0
- data/lib/schwab_rb/auth/login_flow_server.rb +55 -0
- data/lib/schwab_rb/auth/token.rb +24 -0
- data/lib/schwab_rb/auth/token_manager.rb +105 -0
- data/lib/schwab_rb/clients/async_client.rb +122 -0
- data/lib/schwab_rb/clients/base_client.rb +887 -0
- data/lib/schwab_rb/clients/client.rb +97 -0
- data/lib/schwab_rb/configuration.rb +39 -0
- data/lib/schwab_rb/constants.rb +7 -0
- data/lib/schwab_rb/data_objects/account.rb +281 -0
- data/lib/schwab_rb/data_objects/account_numbers.rb +68 -0
- data/lib/schwab_rb/data_objects/instrument.rb +156 -0
- data/lib/schwab_rb/data_objects/market_hours.rb +275 -0
- data/lib/schwab_rb/data_objects/option.rb +147 -0
- data/lib/schwab_rb/data_objects/option_chain.rb +95 -0
- data/lib/schwab_rb/data_objects/option_expiration_chain.rb +134 -0
- data/lib/schwab_rb/data_objects/order.rb +186 -0
- data/lib/schwab_rb/data_objects/order_leg.rb +68 -0
- data/lib/schwab_rb/data_objects/order_preview.rb +237 -0
- data/lib/schwab_rb/data_objects/position.rb +100 -0
- data/lib/schwab_rb/data_objects/price_history.rb +187 -0
- data/lib/schwab_rb/data_objects/quote.rb +276 -0
- data/lib/schwab_rb/data_objects/transaction.rb +132 -0
- data/lib/schwab_rb/data_objects/user_preferences.rb +129 -0
- data/lib/schwab_rb/market_hours.rb +13 -0
- data/lib/schwab_rb/movers.rb +35 -0
- data/lib/schwab_rb/option.rb +64 -0
- data/lib/schwab_rb/orders/builder.rb +202 -0
- data/lib/schwab_rb/orders/destination.rb +19 -0
- data/lib/schwab_rb/orders/duration.rb +9 -0
- data/lib/schwab_rb/orders/equity_instructions.rb +10 -0
- data/lib/schwab_rb/orders/errors.rb +5 -0
- data/lib/schwab_rb/orders/instruments.rb +35 -0
- data/lib/schwab_rb/orders/option_instructions.rb +10 -0
- data/lib/schwab_rb/orders/order.rb +77 -0
- data/lib/schwab_rb/orders/price_link_basis.rb +15 -0
- data/lib/schwab_rb/orders/price_link_type.rb +9 -0
- data/lib/schwab_rb/orders/session.rb +14 -0
- data/lib/schwab_rb/orders/special_instruction.rb +10 -0
- data/lib/schwab_rb/orders/stop_price_link_basis.rb +15 -0
- data/lib/schwab_rb/orders/stop_price_link_type.rb +9 -0
- data/lib/schwab_rb/orders/stop_type.rb +11 -0
- data/lib/schwab_rb/orders/tax_lot_method.rb +13 -0
- data/lib/schwab_rb/price_history.rb +55 -0
- data/lib/schwab_rb/quote.rb +13 -0
- data/lib/schwab_rb/transaction.rb +23 -0
- data/lib/schwab_rb/utils/enum_enforcer.rb +73 -0
- data/lib/schwab_rb/utils/logger.rb +70 -0
- data/lib/schwab_rb/utils/redactor.rb +104 -0
- data/lib/schwab_rb/version.rb +5 -0
- data/lib/schwab_rb.rb +48 -0
- data/sig/schwab_rb.rbs +4 -0
- metadata +289 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
module SchwabRb
|
2
|
+
module Orders
|
3
|
+
module TaxLotMethod
|
4
|
+
FIFO = 'FIFO'
|
5
|
+
LIFO = 'LIFO'
|
6
|
+
HIGH_COST = 'HIGH_COST'
|
7
|
+
LOW_COST = 'LOW_COST'
|
8
|
+
AVERAGE_COST = 'AVERAGE_COST'
|
9
|
+
SPECIFIC_LOT = 'SPECIFIC_LOT'
|
10
|
+
LOSS_HARVESTER = 'LOSS_HARVESTER'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SchwabRb
|
4
|
+
class PriceHistory
|
5
|
+
module PeriodTypes
|
6
|
+
DAY = 'day'
|
7
|
+
MONTH = 'month'
|
8
|
+
YEAR = 'year'
|
9
|
+
YEAR_TO_DATE = 'ytd'
|
10
|
+
end
|
11
|
+
|
12
|
+
module Periods
|
13
|
+
ONE_DAY = 1
|
14
|
+
TWO_DAYS = 2
|
15
|
+
THREE_DAYS = 3
|
16
|
+
FOUR_DAYS = 4
|
17
|
+
FIVE_DAYS = 5
|
18
|
+
TEN_DAYS = 10
|
19
|
+
|
20
|
+
ONE_MONTH = 1
|
21
|
+
TWO_MONTHS = 2
|
22
|
+
THREE_MONTHS = 3
|
23
|
+
SIX_MONTHS = 6
|
24
|
+
|
25
|
+
ONE_YEAR = 1
|
26
|
+
TWO_YEARS = 2
|
27
|
+
THREE_YEARS = 3
|
28
|
+
FIVE_YEARS = 5
|
29
|
+
TEN_YEARS = 10
|
30
|
+
FIFTEEN_YEARS = 15
|
31
|
+
TWENTY_YEARS = 20
|
32
|
+
|
33
|
+
YEAR_TO_DATE = 1
|
34
|
+
end
|
35
|
+
|
36
|
+
module FrequencyTypes
|
37
|
+
MINUTE = 'minute'
|
38
|
+
DAILY = 'daily'
|
39
|
+
WEEKLY = 'weekly'
|
40
|
+
MONTHLY = 'monthly'
|
41
|
+
end
|
42
|
+
|
43
|
+
module Frequencies
|
44
|
+
EVERY_MINUTE = 1
|
45
|
+
EVERY_FIVE_MINUTES = 5
|
46
|
+
EVERY_TEN_MINUTES = 10
|
47
|
+
EVERY_FIFTEEN_MINUTES = 15
|
48
|
+
EVERY_THIRTY_MINUTES = 30
|
49
|
+
|
50
|
+
DAILY = 1
|
51
|
+
WEEKLY = 1
|
52
|
+
MONTHLY = 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SchwabRb
|
4
|
+
class Transaction
|
5
|
+
module Types
|
6
|
+
TRADE = 'TRADE'
|
7
|
+
RECEIVE_AND_DELIVER = 'RECEIVE_AND_DELIVER'
|
8
|
+
DIVIDEND_OR_INTEREST = 'DIVIDEND_OR_INTEREST'
|
9
|
+
ACH_RECEIPT = 'ACH_RECEIPT'
|
10
|
+
ACH_DISBURSEMENT = 'ACH_DISBURSEMENT'
|
11
|
+
CASH_RECEIPT = 'CASH_RECEIPT'
|
12
|
+
CASH_DISBURSEMENT = 'CASH_DISBURSEMENT'
|
13
|
+
ELECTRONIC_FUND = 'ELECTRONIC_FUND'
|
14
|
+
WIRE_OUT = 'WIRE_OUT'
|
15
|
+
WIRE_IN = 'WIRE_IN'
|
16
|
+
JOURNAL = 'JOURNAL'
|
17
|
+
MEMORANDUM = 'MEMORANDUM'
|
18
|
+
MARGIN_CALL = 'MARGIN_CALL'
|
19
|
+
MONEY_MARKET = 'MONEY_MARKET'
|
20
|
+
SMA_ADJUSTMENT = 'SMA_ADJUSTMENT'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module EnumEnforcer
|
2
|
+
def enforce_enums?
|
3
|
+
@enforce_enums ||= false
|
4
|
+
end
|
5
|
+
|
6
|
+
def enforce_enums=(value)
|
7
|
+
@enforce_enums = value
|
8
|
+
end
|
9
|
+
|
10
|
+
def type_error(value, required_enum_type)
|
11
|
+
possible_members_message = ""
|
12
|
+
|
13
|
+
if value.is_a? String
|
14
|
+
possible_members = required_enum_type.constants.filter_map do |member|
|
15
|
+
fullname = "#{required_enum_type}::#{member}"
|
16
|
+
fullname if fullname.include?(value)
|
17
|
+
end
|
18
|
+
|
19
|
+
if possible_members.any?
|
20
|
+
possible_members_message = "Did you mean " +
|
21
|
+
possible_members[0..-2].join(", ") +
|
22
|
+
(possible_members.size > 1 ? " or " : "") +
|
23
|
+
possible_members[-1].to_s + "? "
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
raise ArgumentError,
|
28
|
+
"expected type \"#{required_enum_type}\", got type \"#{value.class}\". #{possible_members_message}(initialize with enforce_enums: false to disable this checking)"
|
29
|
+
end
|
30
|
+
|
31
|
+
def convert_enum(value, enum_type)
|
32
|
+
return nil if value.nil?
|
33
|
+
|
34
|
+
valid_values = enum_type.constants.map { |const| enum_type.const_get(const) }
|
35
|
+
|
36
|
+
if valid_values.include? value
|
37
|
+
value
|
38
|
+
elsif enforce_enums
|
39
|
+
type_error(value, enum_type)
|
40
|
+
else
|
41
|
+
value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def convert_enum_iterable(iterable, enum_type)
|
46
|
+
return [] if iterable.nil?
|
47
|
+
|
48
|
+
valid_values = get_valid_enum_values(enum_type)
|
49
|
+
|
50
|
+
return [iterable] if valid_values.include? iterable
|
51
|
+
|
52
|
+
values = []
|
53
|
+
iterable.each do |value|
|
54
|
+
if valid_values.include? value
|
55
|
+
values << value
|
56
|
+
elsif enforce_enums
|
57
|
+
type_error(value, enum_type)
|
58
|
+
else
|
59
|
+
values << value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
values
|
64
|
+
end
|
65
|
+
|
66
|
+
def set_enforce_enums(enforce_enums)
|
67
|
+
@enforce_enums = enforce_enums
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_valid_enum_values(enum_type)
|
71
|
+
enum_type.constants.map { |const| enum_type.const_get(const) }
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module SchwabRb
|
5
|
+
class Logger
|
6
|
+
class << self
|
7
|
+
def logger
|
8
|
+
@logger ||= create_logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def reset!
|
12
|
+
@logger = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def configure
|
16
|
+
yield(self) if block_given?
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def create_logger
|
22
|
+
config = SchwabRb.configuration
|
23
|
+
|
24
|
+
return config.logger if config.has_external_logger?
|
25
|
+
return null_logger if config.silence_output
|
26
|
+
return null_logger unless config.should_create_logger?
|
27
|
+
|
28
|
+
log_destination = config.effective_log_file || STDOUT
|
29
|
+
|
30
|
+
if log_destination == :null || log_destination == '/dev/null'
|
31
|
+
return null_logger
|
32
|
+
end
|
33
|
+
|
34
|
+
if log_destination.is_a?(String) && log_destination != 'STDOUT'
|
35
|
+
setup_log_file(log_destination)
|
36
|
+
end
|
37
|
+
|
38
|
+
::Logger.new(log_destination, 'weekly').tap do |log|
|
39
|
+
log.level = parse_log_level(config.log_level)
|
40
|
+
log.formatter = proc do |severity, datetime, progname, msg|
|
41
|
+
"[#{datetime.strftime('%H:%M:%S')}] SCHWAB_RB #{severity}: #{msg}\n"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def setup_log_file(log_file)
|
47
|
+
dir = File.dirname(log_file)
|
48
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
49
|
+
FileUtils.touch(log_file) unless File.exist?(log_file)
|
50
|
+
end
|
51
|
+
|
52
|
+
def null_logger
|
53
|
+
::Logger.new(IO::NULL).tap do |log|
|
54
|
+
log.level = ::Logger::FATAL
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_log_level(level)
|
59
|
+
case level.to_s.upcase
|
60
|
+
when 'DEBUG' then ::Logger::DEBUG
|
61
|
+
when 'INFO' then ::Logger::INFO
|
62
|
+
when 'WARN' then ::Logger::WARN
|
63
|
+
when 'ERROR' then ::Logger::ERROR
|
64
|
+
when 'FATAL' then ::Logger::FATAL
|
65
|
+
else ::Logger::WARN
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module SchwabRb
|
6
|
+
class Redactor
|
7
|
+
# Patterns for account numbers and hashes that should be redacted
|
8
|
+
ACCOUNT_NUMBER_PATTERN = /\b\d{8,12}\b/
|
9
|
+
ACCOUNT_HASH_PATTERN = /\b[A-Z0-9]{32}\b/
|
10
|
+
|
11
|
+
# JSON keys that commonly contain sensitive account information
|
12
|
+
SENSITIVE_KEYS = %w[
|
13
|
+
accountNumber
|
14
|
+
accountId
|
15
|
+
accountHash
|
16
|
+
hashValue
|
17
|
+
encryptedId
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
def self.redact_url(url_string)
|
21
|
+
return url_string unless url_string
|
22
|
+
|
23
|
+
redacted = url_string.to_s.dup
|
24
|
+
redacted.gsub!(ACCOUNT_NUMBER_PATTERN, '[REDACTED_ACCOUNT_NUMBER]')
|
25
|
+
redacted.gsub!(ACCOUNT_HASH_PATTERN, '[REDACTED_ACCOUNT_HASH]')
|
26
|
+
redacted
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.redact_data(data)
|
30
|
+
return data unless data
|
31
|
+
|
32
|
+
case data
|
33
|
+
when Hash
|
34
|
+
redact_hash(data)
|
35
|
+
when String
|
36
|
+
begin
|
37
|
+
parsed = JSON.parse(data)
|
38
|
+
redact_data(parsed).to_json
|
39
|
+
rescue JSON::ParserError
|
40
|
+
redact_string(data)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
data
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.redact_response_body(response)
|
48
|
+
return unless response&.respond_to?(:body)
|
49
|
+
|
50
|
+
body = response.body
|
51
|
+
return unless body
|
52
|
+
|
53
|
+
begin
|
54
|
+
if body.is_a?(String)
|
55
|
+
parsed = JSON.parse(body)
|
56
|
+
redact_data(parsed)
|
57
|
+
elsif body.respond_to?(:read)
|
58
|
+
# Handle IO-like objects
|
59
|
+
content = body.read
|
60
|
+
body.rewind if body.respond_to?(:rewind)
|
61
|
+
parsed = JSON.parse(content)
|
62
|
+
redact_data(parsed)
|
63
|
+
else
|
64
|
+
redact_data(body)
|
65
|
+
end
|
66
|
+
rescue JSON::ParserError
|
67
|
+
# If it's not JSON, just redact as a string
|
68
|
+
body_str = body.respond_to?(:read) ? body.read : body.to_s
|
69
|
+
body.rewind if body.respond_to?(:rewind)
|
70
|
+
redact_string(body_str)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def self.redact_hash(hash)
|
77
|
+
hash.each_with_object({}) do |(key, value), redacted|
|
78
|
+
if SENSITIVE_KEYS.include?(key.to_s)
|
79
|
+
redacted[key] = '[REDACTED]'
|
80
|
+
else
|
81
|
+
case value
|
82
|
+
when Hash
|
83
|
+
redacted[key] = redact_hash(value)
|
84
|
+
when Array
|
85
|
+
redacted[key] = value.map { |item| redact_data(item) }
|
86
|
+
when String
|
87
|
+
redacted[key] = redact_string(value)
|
88
|
+
else
|
89
|
+
redacted[key] = value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.redact_string(str)
|
96
|
+
return str unless str.is_a?(String)
|
97
|
+
|
98
|
+
redacted = str.dup
|
99
|
+
redacted.gsub!(ACCOUNT_NUMBER_PATTERN, '[REDACTED_ACCOUNT_NUMBER]')
|
100
|
+
redacted.gsub!(ACCOUNT_HASH_PATTERN, '[REDACTED_ACCOUNT_HASH]')
|
101
|
+
redacted
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/schwab_rb.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "schwab_rb/version"
|
4
|
+
require_relative "schwab_rb/configuration"
|
5
|
+
require_relative "schwab_rb/auth/token_manager"
|
6
|
+
require_relative "schwab_rb/auth/token"
|
7
|
+
require_relative "schwab_rb/auth/init_client_login"
|
8
|
+
require_relative "schwab_rb/auth/init_client_token_file"
|
9
|
+
require_relative "schwab_rb/auth/init_client_easy"
|
10
|
+
require_relative "schwab_rb/auth/auth_context"
|
11
|
+
require_relative "schwab_rb/clients/client"
|
12
|
+
require_relative "schwab_rb/clients/async_client"
|
13
|
+
require_relative "schwab_rb/auth/login_flow_server"
|
14
|
+
require_relative "schwab_rb/constants"
|
15
|
+
require_relative "schwab_rb/orders/order"
|
16
|
+
require_relative "schwab_rb/account"
|
17
|
+
require_relative "schwab_rb/transaction"
|
18
|
+
require_relative "schwab_rb/quote"
|
19
|
+
require_relative "schwab_rb/option"
|
20
|
+
require_relative "schwab_rb/orders/instruments"
|
21
|
+
require_relative "schwab_rb/market_hours"
|
22
|
+
require_relative "schwab_rb/price_history"
|
23
|
+
require_relative "schwab_rb/movers"
|
24
|
+
require_relative "schwab_rb/orders/builder"
|
25
|
+
require_relative "schwab_rb/orders/session"
|
26
|
+
require_relative "schwab_rb/orders/duration"
|
27
|
+
require_relative "schwab_rb/orders/equity_instructions"
|
28
|
+
require_relative "schwab_rb/orders/option_instructions"
|
29
|
+
require_relative "schwab_rb/utils/logger"
|
30
|
+
require_relative "schwab_rb/data_objects/account"
|
31
|
+
require_relative "schwab_rb/data_objects/account_numbers"
|
32
|
+
require_relative "schwab_rb/data_objects/instrument"
|
33
|
+
require_relative "schwab_rb/data_objects/position"
|
34
|
+
require_relative "schwab_rb/data_objects/quote"
|
35
|
+
require_relative "schwab_rb/data_objects/transaction"
|
36
|
+
require_relative "schwab_rb/data_objects/user_preferences"
|
37
|
+
require_relative "schwab_rb/data_objects/order"
|
38
|
+
require_relative "schwab_rb/data_objects/order_leg"
|
39
|
+
require_relative "schwab_rb/data_objects/order_preview"
|
40
|
+
require_relative "schwab_rb/data_objects/option"
|
41
|
+
require_relative "schwab_rb/data_objects/option_chain"
|
42
|
+
require_relative "schwab_rb/data_objects/option_expiration_chain"
|
43
|
+
require_relative "schwab_rb/data_objects/price_history"
|
44
|
+
require_relative "schwab_rb/data_objects/market_hours"
|
45
|
+
|
46
|
+
module SchwabRb
|
47
|
+
class Error < StandardError; end
|
48
|
+
end
|
data/sig/schwab_rb.rbs
ADDED