schwab_mcp 0.1.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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.copilotignore +3 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +8 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +157 -0
  9. data/Rakefile +12 -0
  10. data/exe/schwab_mcp +19 -0
  11. data/exe/schwab_token_refresh +38 -0
  12. data/exe/schwab_token_reset +49 -0
  13. data/lib/schwab_mcp/loggable.rb +31 -0
  14. data/lib/schwab_mcp/logger.rb +62 -0
  15. data/lib/schwab_mcp/option_chain_filter.rb +213 -0
  16. data/lib/schwab_mcp/orders/iron_condor_order.rb +87 -0
  17. data/lib/schwab_mcp/orders/order_factory.rb +40 -0
  18. data/lib/schwab_mcp/orders/vertical_order.rb +62 -0
  19. data/lib/schwab_mcp/redactor.rb +210 -0
  20. data/lib/schwab_mcp/resources/.keep +0 -0
  21. data/lib/schwab_mcp/tools/cancel_order_tool.rb +226 -0
  22. data/lib/schwab_mcp/tools/get_market_hours_tool.rb +104 -0
  23. data/lib/schwab_mcp/tools/get_order_tool.rb +263 -0
  24. data/lib/schwab_mcp/tools/get_price_history_tool.rb +203 -0
  25. data/lib/schwab_mcp/tools/help_tool.rb +406 -0
  26. data/lib/schwab_mcp/tools/list_account_orders_tool.rb +295 -0
  27. data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +311 -0
  28. data/lib/schwab_mcp/tools/list_movers_tool.rb +125 -0
  29. data/lib/schwab_mcp/tools/list_schwab_accounts_tool.rb +162 -0
  30. data/lib/schwab_mcp/tools/option_chain_tool.rb +274 -0
  31. data/lib/schwab_mcp/tools/option_strategy_finder_tool.rb +378 -0
  32. data/lib/schwab_mcp/tools/place_order_tool.rb +305 -0
  33. data/lib/schwab_mcp/tools/preview_order_tool.rb +259 -0
  34. data/lib/schwab_mcp/tools/quote_tool.rb +77 -0
  35. data/lib/schwab_mcp/tools/quotes_tool.rb +110 -0
  36. data/lib/schwab_mcp/tools/replace_order_tool.rb +312 -0
  37. data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +208 -0
  38. data/lib/schwab_mcp/version.rb +5 -0
  39. data/lib/schwab_mcp.rb +107 -0
  40. data/sig/schwab_mcp.rbs +4 -0
  41. data/start_mcp_server.sh +4 -0
  42. metadata +115 -0
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+ require_relative "loggable"
3
+
4
+ module SchwabMCP
5
+ class OptionChainFilter
6
+ include Loggable
7
+
8
+ attr_reader :expiration_date, :underlying_price, :expiration_type,
9
+ :settlement_type, :option_root, :max_delta, :max_spread,
10
+ :min_credit, :min_open_interest, :dist_from_strike, :quantity,
11
+ :min_delta, :min_strike, :max_strike
12
+
13
+ def initialize(
14
+ expiration_date:,
15
+ underlying_price: nil,
16
+ expiration_type: nil,
17
+ settlement_type: nil,
18
+ option_root: nil,
19
+ min_delta: 0.0,
20
+ max_delta: 0.15,
21
+ max_spread: 20.0,
22
+ min_credit: 0.0,
23
+ min_open_interest: 0,
24
+ dist_from_strike: 0.0,
25
+ quantity: 1,
26
+ max_strike: nil,
27
+ min_strike: nil
28
+ )
29
+ @expiration_date = expiration_date
30
+ @underlying_price = underlying_price
31
+ @expiration_type = expiration_type
32
+ @settlement_type = settlement_type
33
+ @option_root = option_root
34
+ @max_spread = max_spread
35
+ @min_credit = min_credit
36
+ @min_open_interest = min_open_interest
37
+ @dist_from_strike = dist_from_strike
38
+ @quantity = quantity
39
+ @max_delta = max_delta
40
+ @min_delta = min_delta
41
+ @max_strike = max_strike
42
+ @min_strike = min_strike
43
+ end
44
+
45
+ def select(options_map)
46
+ filtered_options = []
47
+ exp_date_str = expiration_date.strftime('%Y-%m-%d')
48
+
49
+ options_map.each do |date_key, strikes|
50
+ strikes.each do |strike_key, option_array|
51
+ option_array.each do |option|
52
+ next unless passes_delta_filter?(option)
53
+ next unless passes_strike_range_filter?(option)
54
+ filtered_options << option
55
+ end
56
+ end
57
+ end
58
+
59
+ log_debug("Found #{filtered_options.size} filtered options")
60
+ filtered_options
61
+ end
62
+
63
+ def find_spreads(options_map, option_type)
64
+ spreads = []
65
+ exp_date_str = expiration_date.strftime('%Y-%m-%d')
66
+
67
+ short_cnt = 0
68
+
69
+ options_map.each do |date_key, strikes|
70
+ next unless date_matches?(date_key, exp_date_str)
71
+ log_debug("Processing options for date: #{date_key}. Searching #{strikes.size} strikes...")
72
+
73
+ strikes.each do |strike_key, option_array|
74
+ option_array.each do |short_option|
75
+ next unless passes_short_option_filters?(short_option)
76
+
77
+ log_debug("Found short option: #{short_option[:symbol]} at strike #{short_option[:strikePrice]}")
78
+
79
+ short_cnt += 1
80
+
81
+ short_strike = short_option[:strikePrice]
82
+ short_mark = short_option[:mark]
83
+
84
+ long_options = find_long_option_candidates(strikes, short_option, option_type)
85
+
86
+ long_options.each do |long_option|
87
+ spread = build_spread(short_option, long_option)
88
+ spreads << spread if spread
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ log_debug("Found #{spreads.size} #{option_type} spreads for #{short_cnt} short options")
95
+
96
+ spreads
97
+ end
98
+
99
+ def passes_short_option_filters?(option)
100
+ return false unless passes_delta_filter?(option)
101
+ return false unless passes_open_interest_filter?(option)
102
+ return false unless passes_distance_filter?(option)
103
+ return false unless passes_optional_filters?(option)
104
+
105
+ true
106
+ end
107
+
108
+ private
109
+
110
+ def date_matches?(date_key, exp_date_str)
111
+ date_key.to_s.include?(exp_date_str)
112
+ end
113
+
114
+ def passes_delta_filter?(option)
115
+ delta = option[:delta]&.abs || 0.0
116
+ delta <= max_delta && delta >= min_delta
117
+ end
118
+
119
+ def passes_open_interest_filter?(option)
120
+ open_interest = option[:openInterest] || 0
121
+ open_interest >= min_open_interest
122
+ end
123
+
124
+ def passes_distance_filter?(option)
125
+ raise "Underlying price must be set for distance filter" unless underlying_price
126
+
127
+ strike = option[:strikePrice]
128
+ return false unless strike
129
+
130
+ distance = ((underlying_price - strike) / underlying_price).abs
131
+ distance >= dist_from_strike
132
+ end
133
+
134
+ def passes_optional_filters?(option)
135
+ return false if expiration_type && option[:expirationType] != expiration_type
136
+ return false if settlement_type && option[:settlementType] != settlement_type
137
+ return false if option_root && option[:optionRoot] != option_root
138
+
139
+ true
140
+ end
141
+
142
+ def passes_strike_range_filter?(option)
143
+ strike = option[:strikePrice]
144
+ return false unless strike
145
+
146
+ return false if @min_strike && strike < @min_strike
147
+ return false if @max_strike && strike > @max_strike
148
+
149
+ true
150
+ end
151
+
152
+ def find_long_option_candidates(strikes, short_option, option_type)
153
+ short_strike = short_option[:strikePrice]
154
+ candidates = []
155
+
156
+ strikes.each do |long_strike_key, long_option_array|
157
+ long_option_array.each do |long_option|
158
+ long_strike = long_option[:strikePrice]
159
+ long_mark = long_option[:mark]
160
+
161
+ next unless long_mark > 0
162
+ next unless valid_spread_structure?(short_strike, long_strike, option_type)
163
+ next unless passes_min_credit?(short_option, long_option)
164
+ next unless passes_optional_filters?(long_option)
165
+ next unless passes_open_interest_filter?(long_option)
166
+
167
+ candidates << long_option
168
+ end
169
+ end
170
+
171
+ candidates
172
+ end
173
+
174
+ def valid_spread_structure?(short_strike, long_strike, option_type)
175
+ case option_type
176
+ when 'call'
177
+ long_strike > short_strike && (long_strike - short_strike) <= max_spread
178
+
179
+ when 'put'
180
+ long_strike < short_strike && (short_strike - long_strike) <= max_spread
181
+ else
182
+ false
183
+ end
184
+ end
185
+
186
+ def passes_min_credit?(short_option, long_option)
187
+ return true if min_credit <= 0
188
+
189
+ short_mark = short_option[:mark]
190
+ long_mark = long_option[:mark]
191
+ credit = short_mark - long_mark
192
+
193
+ credit * 100 >= min_credit
194
+ end
195
+
196
+ def build_spread(short_option, long_option)
197
+ short_mark = short_option[:mark]
198
+ long_mark = long_option[:mark]
199
+ credit = short_mark - long_mark
200
+ short_strike = short_option[:strikePrice]
201
+ long_strike = long_option[:strikePrice]
202
+
203
+ {
204
+ short_option: short_option,
205
+ long_option: long_option,
206
+ credit: credit,
207
+ delta: short_option[:delta] || 0,
208
+ spread_width: (short_strike - long_strike).abs,
209
+ quantity: quantity
210
+ }
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schwab_rb'
4
+
5
+ module SchwabMCP
6
+ module Orders
7
+ class IronCondorOrder
8
+ class << self
9
+ def build(
10
+ account_number:,
11
+ put_short_symbol:,
12
+ put_long_symbol:,
13
+ call_short_symbol:,
14
+ call_long_symbol:,
15
+ price:,
16
+ order_instruction: :open,
17
+ quantity: 1
18
+ )
19
+ schwab_order_builder.new.tap do |builder|
20
+ builder.set_account_number(account_number)
21
+ builder.set_order_strategy_type('SINGLE')
22
+ builder.set_session(SchwabRb::Orders::Session::NORMAL)
23
+ builder.set_duration(SchwabRb::Orders::Duration::DAY)
24
+ builder.set_order_type(order_type(order_instruction))
25
+ builder.set_complex_order_strategy_type(SchwabRb::Order::ComplexOrderStrategyTypes::IRON_CONDOR)
26
+ builder.set_quantity(quantity)
27
+ builder.set_price(price)
28
+
29
+ instructions = leg_instructions_for_position(order_instruction)
30
+
31
+ builder.add_option_leg(
32
+ instructions[:put_short],
33
+ put_short_symbol,
34
+ quantity
35
+ )
36
+ builder.add_option_leg(
37
+ instructions[:put_long],
38
+ put_long_symbol,
39
+ quantity
40
+ )
41
+ builder.add_option_leg(
42
+ instructions[:call_short],
43
+ call_short_symbol,
44
+ quantity
45
+ )
46
+ builder.add_option_leg(
47
+ instructions[:call_long],
48
+ call_long_symbol,
49
+ quantity
50
+ )
51
+ end
52
+ end
53
+
54
+ def order_type(order_instruction)
55
+ if order_instruction == :open
56
+ SchwabRb::Order::Types::NET_CREDIT
57
+ else
58
+ SchwabRb::Order::Types::NET_DEBIT
59
+ end
60
+ end
61
+
62
+ def leg_instructions_for_position(order_instruction)
63
+ case order_instruction
64
+ when :open
65
+ {
66
+ put_short: SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN,
67
+ put_long: SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN,
68
+ call_short: SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN,
69
+ call_long: SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN
70
+ }
71
+ when :exit
72
+ {
73
+ put_short: SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE,
74
+ put_long: SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE,
75
+ call_short: SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE,
76
+ call_long: SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE
77
+ }
78
+ end
79
+ end
80
+
81
+ def schwab_order_builder
82
+ SchwabRb::Orders::Builder
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schwab_rb'
4
+ require_relative 'iron_condor_order'
5
+ require_relative 'vertical_order'
6
+
7
+ module SchwabMCP
8
+ module Orders
9
+ class OrderFactory
10
+ class << self
11
+ def build(**options)
12
+ case options[:strategy_type] || 'none'
13
+ when 'ironcondor'
14
+ IronCondorOrder.build(
15
+ put_short_symbol: options[:put_short_symbol],
16
+ put_long_symbol: options[:put_long_symbol],
17
+ call_short_symbol: options[:call_short_symbol],
18
+ call_long_symbol: options[:call_long_symbol],
19
+ price: options[:price],
20
+ account_number: options[:account_number],
21
+ order_instruction: options[:order_instruction] || :open,
22
+ quantity: options[:quantity] || 1
23
+ )
24
+ when 'callspread', 'putspread'
25
+ VerticalOrder.build(
26
+ short_leg_symbol: options[:short_leg_symbol],
27
+ long_leg_symbol: options[:long_leg_symbol],
28
+ price: options[:price],
29
+ account_number: options[:account_number],
30
+ order_instruction: options[:order_instruction] || :open,
31
+ quantity: options[:quantity] || 1
32
+ )
33
+ else
34
+ raise "Unsupported trade strategy: #{options[:strategy_type] || 'none'}"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schwab_rb'
4
+
5
+ module SchwabMCP
6
+ module Orders
7
+ class VerticalOrder
8
+ class << self
9
+ def build(short_leg_symbol:, long_leg_symbol:, price:, account_number:, order_instruction: :open, quantity: 1)
10
+ schwab_order_builder.new.tap do |builder|
11
+ builder.set_account_number(account_number)
12
+ builder.set_order_strategy_type('SINGLE')
13
+ builder.set_session(SchwabRb::Orders::Session::NORMAL)
14
+ builder.set_duration(SchwabRb::Orders::Duration::DAY)
15
+ builder.set_order_type(order_type(order_instruction))
16
+ builder.set_complex_order_strategy_type(SchwabRb::Order::ComplexOrderStrategyTypes::VERTICAL)
17
+ builder.set_quantity(quantity)
18
+ builder.set_price(price)
19
+ builder.add_option_leg(
20
+ short_leg_instruction(order_instruction),
21
+ short_leg_symbol,
22
+ quantity
23
+ )
24
+ builder.add_option_leg(
25
+ long_leg_instruction(order_instruction),
26
+ long_leg_symbol,
27
+ quantity
28
+ )
29
+ end
30
+ end
31
+
32
+ def order_type(order_instruction)
33
+ if order_instruction == :open
34
+ SchwabRb::Order::Types::NET_CREDIT
35
+ else
36
+ SchwabRb::Order::Types::NET_DEBIT
37
+ end
38
+ end
39
+
40
+ def short_leg_instruction(order_instruction)
41
+ if order_instruction == :open
42
+ SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN
43
+ else
44
+ SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE
45
+ end
46
+ end
47
+
48
+ def long_leg_instruction(order_instruction)
49
+ if order_instruction == :open
50
+ SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN
51
+ else
52
+ SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE
53
+ end
54
+ end
55
+
56
+ def schwab_order_builder
57
+ SchwabRb::Orders::Builder
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SchwabMCP
6
+ class Redactor
7
+ ACCOUNT_NUMBER_PATTERN = /\b\d{8,9}\b/
8
+ HASH_VALUE_PATTERN = /\b[A-Za-z0-9]{20,}\b/
9
+ BEARER_TOKEN_PATTERN = /Bearer\s+[A-Za-z0-9\.\-_]+/i
10
+ ACCOUNT_FIELDS = %w[
11
+ accountNumber
12
+ accountId
13
+ account_number
14
+ account_id
15
+ hashValue
16
+ hash_value
17
+ ].freeze
18
+ NON_SENSITIVE_FIELDS = %w[
19
+ cusip
20
+ orderId
21
+ order_id
22
+ legId
23
+ leg_id
24
+ strikePrice
25
+ strike_price
26
+ quantity
27
+ daysToExpiration
28
+ days_to_expiration
29
+ expirationDate
30
+ expiration_date
31
+ price
32
+ netChange
33
+ net_change
34
+ mismarkedQuantity
35
+ mismarked_quantity
36
+ ].freeze
37
+ REDACTED_PLACEHOLDER = '[REDACTED]'
38
+ REDACTED_ACCOUNT_PLACEHOLDER = '[REDACTED_ACCOUNT]'
39
+ REDACTED_HASH_PLACEHOLDER = '[REDACTED_HASH]'
40
+ REDACTED_TOKEN_PLACEHOLDER = '[REDACTED_TOKEN]'
41
+
42
+ class << self
43
+ # Main entry point for redacting any data structure
44
+ # @param data [Object] Data to redact (Hash, Array, String, or other)
45
+ # @return [Object] Redacted copy of the data
46
+ def redact(data)
47
+ case data
48
+ when Hash
49
+ redact_hash(data)
50
+ when Array
51
+ redact_array(data)
52
+ when String
53
+ redact_string(data)
54
+ else
55
+ data
56
+ end
57
+ end
58
+
59
+ # Redact a JSON response from Schwab API
60
+ # @param response_body [String] Raw response body from API
61
+ # @return [String] Pretty-formatted redacted JSON
62
+ def redact_api_response(response_body)
63
+ return response_body unless response_body.is_a?(String)
64
+
65
+ begin
66
+ parsed = JSON.parse(response_body)
67
+ redacted = redact(parsed)
68
+ JSON.pretty_generate(redacted)
69
+ rescue JSON::ParserError
70
+ # If parsing fails, redact as string
71
+ redact_string(response_body)
72
+ end
73
+ end
74
+
75
+ # Redact formatted text that might contain sensitive data
76
+ # @param text [String] Formatted text to redact
77
+ # @return [String] Redacted text
78
+ def redact_formatted_text(text)
79
+ return text unless text.is_a?(String)
80
+ redacted = text.dup
81
+ redacted.gsub!(BEARER_TOKEN_PATTERN, "Bearer #{REDACTED_TOKEN_PLACEHOLDER}")
82
+
83
+ # Redact account numbers in specific text patterns
84
+ redacted.gsub!(/Account\s+ID:\s*\d{8,9}/i, "Account ID: #{REDACTED_ACCOUNT_PLACEHOLDER}")
85
+ redacted.gsub!(/Account\s+Number:\s*\d{8,9}/i, "Account Number: #{REDACTED_ACCOUNT_PLACEHOLDER}")
86
+ redacted.gsub!(/Account:\s*\d{8,9}/i, "Account: #{REDACTED_ACCOUNT_PLACEHOLDER}")
87
+
88
+ # Redact account numbers in log patterns like "account_number: 123456789"
89
+ redacted.gsub!(/account[_\s]*number[_\s]*[:\=]\s*\d{8,9}/i, "account_number: #{REDACTED_ACCOUNT_PLACEHOLDER}")
90
+ redacted.gsub!(/account[_\s]*id[_\s]*[:\=]\s*\d{8,9}/i, "account_id: #{REDACTED_ACCOUNT_PLACEHOLDER}")
91
+
92
+ redacted
93
+ end
94
+
95
+ # Redact a log message that might contain sensitive data
96
+ # @param message [String] Log message to redact
97
+ # @return [String] Redacted log message
98
+ def redact_log_message(message)
99
+ return message unless message.is_a?(String)
100
+
101
+ redact_formatted_text(message)
102
+ end
103
+
104
+ # Redact an MCP tool response before sending to client
105
+ # @param response [Hash] MCP tool response
106
+ # @return [Hash] Redacted response
107
+ def redact_mcp_response(response)
108
+ return response unless response.is_a?(Hash)
109
+
110
+ redacted = redact(response)
111
+
112
+ # Also redact any content in the response content field
113
+ if redacted.dig("content")
114
+ case redacted["content"]
115
+ when String
116
+ redacted["content"] = redact_formatted_text(redacted["content"])
117
+ when Array
118
+ redacted["content"] = redacted["content"].map do |item|
119
+ if item.is_a?(Hash) && item["text"]
120
+ item["text"] = redact_formatted_text(item["text"])
121
+ end
122
+ item
123
+ end
124
+ end
125
+ end
126
+
127
+ redacted
128
+ end
129
+
130
+ private
131
+
132
+ # Redact a hash/object recursively
133
+ # @param hash [Hash] Hash to redact
134
+ # @return [Hash] Redacted copy of hash
135
+ def redact_hash(hash)
136
+ return hash unless hash.is_a?(Hash)
137
+
138
+ redacted = {}
139
+
140
+ hash.each do |key, value|
141
+ key_str = key.to_s.downcase
142
+
143
+ # Check if this is a known non-sensitive field
144
+ if NON_SENSITIVE_FIELDS.any? { |field| key_str.include?(field.downcase) }
145
+ redacted[key] = value
146
+ # Check if this is a known account-related field
147
+ elsif ACCOUNT_FIELDS.any? { |field| key_str.include?(field.downcase) }
148
+ redacted[key] = redact_account_field(value)
149
+ else
150
+ redacted[key] = redact(value)
151
+ end
152
+ end
153
+
154
+ redacted
155
+ end
156
+
157
+ # Redact an array recursively
158
+ # @param array [Array] Array to redact
159
+ # @return [Array] Redacted copy of array
160
+ def redact_array(array)
161
+ return array unless array.is_a?(Array)
162
+
163
+ array.map { |item| redact(item) }
164
+ end
165
+
166
+ # Redact a string value
167
+ # @param string [String] String to redact
168
+ # @return [String] Redacted string
169
+ def redact_string(string)
170
+ return string unless string.is_a?(String)
171
+
172
+ redacted = string.dup
173
+
174
+ # Redact bearer tokens
175
+ redacted.gsub!(BEARER_TOKEN_PATTERN, "Bearer #{REDACTED_TOKEN_PLACEHOLDER}")
176
+
177
+ # Redact JSON-embedded account data (specific patterns)
178
+ redacted.gsub!(/"hashValue":\s*"[^"]+"/i, "\"hashValue\": \"#{REDACTED_HASH_PLACEHOLDER}\"")
179
+ redacted.gsub!(/"accountNumber":\s*"?\d{8,9}"?/i, "\"accountNumber\": \"#{REDACTED_ACCOUNT_PLACEHOLDER}\"")
180
+ redacted.gsub!(/"account_id":\s*"?\d{8,9}"?/i, "\"account_id\": \"#{REDACTED_ACCOUNT_PLACEHOLDER}\"")
181
+
182
+ redacted
183
+ end
184
+
185
+ # Redact a field that is known to contain account information
186
+ # @param value [Object] Value to redact
187
+ # @return [Object] Redacted value
188
+ def redact_account_field(value)
189
+ case value
190
+ when String
191
+ if value.match?(ACCOUNT_NUMBER_PATTERN) && value.length.between?(8, 9)
192
+ REDACTED_ACCOUNT_PLACEHOLDER
193
+ elsif value.length > 20 && value.match?(/\A[A-Za-z0-9]+\z/)
194
+ REDACTED_HASH_PLACEHOLDER
195
+ else
196
+ value
197
+ end
198
+ when Integer
199
+ if value.to_s.length.between?(8, 9)
200
+ REDACTED_ACCOUNT_PLACEHOLDER
201
+ else
202
+ value
203
+ end
204
+ else
205
+ redact(value)
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
File without changes