schwab_mcp 0.1.0 → 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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/settings.json +14 -0
  3. data/CLAUDE.md +124 -0
  4. data/debug_env.rb +46 -0
  5. data/doc/DATA_OBJECTS_MIGRATION_TODO.md +80 -0
  6. data/doc/SCHWAB_CLIENT_FACTORY_REFACTOR_PLAN.md +187 -0
  7. data/exe/schwab_mcp +14 -3
  8. data/exe/schwab_token_refresh +10 -9
  9. data/lib/schwab_mcp/redactor.rb +4 -0
  10. data/lib/schwab_mcp/schwab_client_factory.rb +44 -0
  11. data/lib/schwab_mcp/tools/cancel_order_tool.rb +29 -50
  12. data/lib/schwab_mcp/tools/get_market_hours_tool.rb +27 -28
  13. data/lib/schwab_mcp/tools/get_order_tool.rb +51 -108
  14. data/lib/schwab_mcp/tools/get_price_history_tool.rb +23 -35
  15. data/lib/schwab_mcp/tools/help_tool.rb +1 -22
  16. data/lib/schwab_mcp/tools/list_account_orders_tool.rb +35 -63
  17. data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +43 -72
  18. data/lib/schwab_mcp/tools/list_movers_tool.rb +21 -34
  19. data/lib/schwab_mcp/tools/list_schwab_accounts_tool.rb +18 -31
  20. data/lib/schwab_mcp/tools/option_chain_tool.rb +130 -82
  21. data/lib/schwab_mcp/tools/place_order_tool.rb +105 -117
  22. data/lib/schwab_mcp/tools/preview_order_tool.rb +100 -48
  23. data/lib/schwab_mcp/tools/quote_tool.rb +33 -26
  24. data/lib/schwab_mcp/tools/quotes_tool.rb +97 -45
  25. data/lib/schwab_mcp/tools/replace_order_tool.rb +104 -116
  26. data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +56 -72
  27. data/lib/schwab_mcp/version.rb +1 -1
  28. data/lib/schwab_mcp.rb +1 -2
  29. data/orders_example.json +7084 -0
  30. data/spx_option_chain.json +25073 -0
  31. data/test_mcp.rb +16 -0
  32. data/test_server.rb +23 -0
  33. data/trading_brokerage_account_details.json +89 -0
  34. data/transactions_example.json +488 -0
  35. metadata +17 -7
  36. data/lib/schwab_mcp/option_chain_filter.rb +0 -213
  37. data/lib/schwab_mcp/orders/iron_condor_order.rb +0 -87
  38. data/lib/schwab_mcp/orders/order_factory.rb +0 -40
  39. data/lib/schwab_mcp/orders/vertical_order.rb +0 -62
  40. data/lib/schwab_mcp/tools/option_strategy_finder_tool.rb +0 -378
@@ -1,213 +0,0 @@
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
@@ -1,87 +0,0 @@
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
@@ -1,40 +0,0 @@
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
@@ -1,62 +0,0 @@
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