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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.copilotignore +4 -0
  3. data/.rspec +2 -0
  4. data/.rspec_status +292 -0
  5. data/.rubocop.yml +41 -0
  6. data/.rubocop_todo.yml +105 -0
  7. data/CHANGELOG.md +28 -0
  8. data/LICENSE.txt +23 -0
  9. data/README.md +271 -0
  10. data/Rakefile +12 -0
  11. data/doc/notes/data_objects_analysis.md +223 -0
  12. data/doc/notes/data_objects_refactoring_plan.md +82 -0
  13. data/examples/fetch_account_numbers.rb +49 -0
  14. data/examples/fetch_user_preferences.rb +49 -0
  15. data/lib/schwab_rb/account.rb +9 -0
  16. data/lib/schwab_rb/auth/auth_context.rb +23 -0
  17. data/lib/schwab_rb/auth/init_client_easy.rb +45 -0
  18. data/lib/schwab_rb/auth/init_client_login.rb +201 -0
  19. data/lib/schwab_rb/auth/init_client_token_file.rb +30 -0
  20. data/lib/schwab_rb/auth/login_flow_server.rb +55 -0
  21. data/lib/schwab_rb/auth/token.rb +24 -0
  22. data/lib/schwab_rb/auth/token_manager.rb +105 -0
  23. data/lib/schwab_rb/clients/async_client.rb +122 -0
  24. data/lib/schwab_rb/clients/base_client.rb +887 -0
  25. data/lib/schwab_rb/clients/client.rb +97 -0
  26. data/lib/schwab_rb/configuration.rb +39 -0
  27. data/lib/schwab_rb/constants.rb +7 -0
  28. data/lib/schwab_rb/data_objects/account.rb +281 -0
  29. data/lib/schwab_rb/data_objects/account_numbers.rb +68 -0
  30. data/lib/schwab_rb/data_objects/instrument.rb +156 -0
  31. data/lib/schwab_rb/data_objects/market_hours.rb +275 -0
  32. data/lib/schwab_rb/data_objects/option.rb +147 -0
  33. data/lib/schwab_rb/data_objects/option_chain.rb +95 -0
  34. data/lib/schwab_rb/data_objects/option_expiration_chain.rb +134 -0
  35. data/lib/schwab_rb/data_objects/order.rb +186 -0
  36. data/lib/schwab_rb/data_objects/order_leg.rb +68 -0
  37. data/lib/schwab_rb/data_objects/order_preview.rb +237 -0
  38. data/lib/schwab_rb/data_objects/position.rb +100 -0
  39. data/lib/schwab_rb/data_objects/price_history.rb +187 -0
  40. data/lib/schwab_rb/data_objects/quote.rb +276 -0
  41. data/lib/schwab_rb/data_objects/transaction.rb +132 -0
  42. data/lib/schwab_rb/data_objects/user_preferences.rb +129 -0
  43. data/lib/schwab_rb/market_hours.rb +13 -0
  44. data/lib/schwab_rb/movers.rb +35 -0
  45. data/lib/schwab_rb/option.rb +64 -0
  46. data/lib/schwab_rb/orders/builder.rb +202 -0
  47. data/lib/schwab_rb/orders/destination.rb +19 -0
  48. data/lib/schwab_rb/orders/duration.rb +9 -0
  49. data/lib/schwab_rb/orders/equity_instructions.rb +10 -0
  50. data/lib/schwab_rb/orders/errors.rb +5 -0
  51. data/lib/schwab_rb/orders/instruments.rb +35 -0
  52. data/lib/schwab_rb/orders/option_instructions.rb +10 -0
  53. data/lib/schwab_rb/orders/order.rb +77 -0
  54. data/lib/schwab_rb/orders/price_link_basis.rb +15 -0
  55. data/lib/schwab_rb/orders/price_link_type.rb +9 -0
  56. data/lib/schwab_rb/orders/session.rb +14 -0
  57. data/lib/schwab_rb/orders/special_instruction.rb +10 -0
  58. data/lib/schwab_rb/orders/stop_price_link_basis.rb +15 -0
  59. data/lib/schwab_rb/orders/stop_price_link_type.rb +9 -0
  60. data/lib/schwab_rb/orders/stop_type.rb +11 -0
  61. data/lib/schwab_rb/orders/tax_lot_method.rb +13 -0
  62. data/lib/schwab_rb/price_history.rb +55 -0
  63. data/lib/schwab_rb/quote.rb +13 -0
  64. data/lib/schwab_rb/transaction.rb +23 -0
  65. data/lib/schwab_rb/utils/enum_enforcer.rb +73 -0
  66. data/lib/schwab_rb/utils/logger.rb +70 -0
  67. data/lib/schwab_rb/utils/redactor.rb +104 -0
  68. data/lib/schwab_rb/version.rb +5 -0
  69. data/lib/schwab_rb.rb +48 -0
  70. data/sig/schwab_rb.rbs +4 -0
  71. metadata +289 -0
@@ -0,0 +1,11 @@
1
+ module SchwabRb
2
+ module Orders
3
+ module StopType
4
+ STANDARD = 'STANDARD'
5
+ BID = 'BID'
6
+ ASK = 'ASK'
7
+ LAST = 'LAST'
8
+ MARK = 'MARK'
9
+ end
10
+ end
11
+ end
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb
4
+ class Quote
5
+ module Types
6
+ QUOTE = 'quote'
7
+ FUNDAMENTAL = 'fundamental'
8
+ EXTENDED = 'extended'
9
+ REFERENCE = 'reference'
10
+ REGULAR = 'regular'
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb
4
+ VERSION = "0.2.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module SchwabRb
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end