schwab_mcp 0.1.0 → 0.3.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/settings.json +14 -0
  3. data/CHANGELOG.md +12 -0
  4. data/CLAUDE.md +124 -0
  5. data/README.md +1 -7
  6. data/debug_env.rb +46 -0
  7. data/doc/DATA_OBJECTS_MIGRATION_TODO.md +80 -0
  8. data/doc/SCHWAB_CLIENT_FACTORY_REFACTOR_PLAN.md +187 -0
  9. data/exe/schwab_mcp +15 -4
  10. data/exe/schwab_token_refresh +12 -11
  11. data/exe/schwab_token_reset +11 -10
  12. data/lib/schwab_mcp/redactor.rb +4 -0
  13. data/lib/schwab_mcp/schwab_client_factory.rb +44 -0
  14. data/lib/schwab_mcp/tools/cancel_order_tool.rb +29 -81
  15. data/lib/schwab_mcp/tools/get_account_names_tool.rb +58 -0
  16. data/lib/schwab_mcp/tools/get_market_hours_tool.rb +27 -28
  17. data/lib/schwab_mcp/tools/get_order_tool.rb +50 -137
  18. data/lib/schwab_mcp/tools/get_price_history_tool.rb +23 -35
  19. data/lib/schwab_mcp/tools/help_tool.rb +12 -33
  20. data/lib/schwab_mcp/tools/list_account_orders_tool.rb +36 -90
  21. data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +43 -98
  22. data/lib/schwab_mcp/tools/list_movers_tool.rb +21 -34
  23. data/lib/schwab_mcp/tools/option_chain_tool.rb +132 -84
  24. data/lib/schwab_mcp/tools/place_order_tool.rb +111 -141
  25. data/lib/schwab_mcp/tools/preview_order_tool.rb +71 -81
  26. data/lib/schwab_mcp/tools/quote_tool.rb +33 -28
  27. data/lib/schwab_mcp/tools/quotes_tool.rb +97 -45
  28. data/lib/schwab_mcp/tools/replace_order_tool.rb +110 -140
  29. data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +56 -98
  30. data/lib/schwab_mcp/version.rb +1 -1
  31. data/lib/schwab_mcp.rb +11 -10
  32. metadata +12 -9
  33. data/lib/schwab_mcp/option_chain_filter.rb +0 -213
  34. data/lib/schwab_mcp/orders/iron_condor_order.rb +0 -87
  35. data/lib/schwab_mcp/orders/order_factory.rb +0 -40
  36. data/lib/schwab_mcp/orders/vertical_order.rb +0 -62
  37. data/lib/schwab_mcp/tools/list_schwab_accounts_tool.rb +0 -162
  38. data/lib/schwab_mcp/tools/option_strategy_finder_tool.rb +0 -378
  39. data/start_mcp_server.sh +0 -4
@@ -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
@@ -1,162 +0,0 @@
1
- require "mcp"
2
- require "schwab_rb"
3
- require "json"
4
- require_relative "../loggable"
5
- require_relative "../redactor"
6
-
7
- module SchwabMCP
8
- module Tools
9
- class ListSchwabAccountsTool < MCP::Tool
10
- extend Loggable
11
- description "List all configured Schwab accounts with their friendly names and basic info"
12
-
13
- input_schema(
14
- properties: {},
15
- required: []
16
- )
17
-
18
- annotations(
19
- title: "List Schwab Accounts",
20
- read_only_hint: true,
21
- destructive_hint: false,
22
- idempotent_hint: true
23
- )
24
-
25
- def self.call(server_context:)
26
- log_info("Listing all configured Schwab accounts")
27
-
28
- begin
29
- client = SchwabRb::Auth.init_client_easy(
30
- ENV['SCHWAB_API_KEY'],
31
- ENV['SCHWAB_APP_SECRET'],
32
- ENV['SCHWAB_CALLBACK_URI'],
33
- ENV['TOKEN_PATH']
34
- )
35
-
36
- unless client
37
- log_error("Failed to initialize Schwab client")
38
- return MCP::Tool::Response.new([{
39
- type: "text",
40
- text: "**Error**: Failed to initialize Schwab client. Check your credentials."
41
- }])
42
- end
43
-
44
- log_debug("Fetching account numbers from Schwab API")
45
- account_numbers_response = client.get_account_numbers
46
-
47
- unless account_numbers_response&.body
48
- log_error("Failed to retrieve account numbers")
49
- return MCP::Tool::Response.new([{
50
- type: "text",
51
- text: "**Error**: Failed to retrieve account numbers from Schwab API"
52
- }])
53
- end
54
-
55
- account_mappings = JSON.parse(account_numbers_response.body)
56
- log_debug("Retrieved #{account_mappings.length} accounts from Schwab API")
57
-
58
- configured_accounts = find_configured_accounts(account_mappings)
59
-
60
- if configured_accounts.empty?
61
- return MCP::Tool::Response.new([{
62
- type: "text",
63
- text: "**No Configured Accounts Found**\n\nNo environment variables found ending with '_ACCOUNT'.\n\nTo configure accounts, set environment variables like:\n- TRADING_BROKERAGE_ACCOUNT=123456789\n- RETIREMENT_IRA_ACCOUNT=987654321\n- INCOME_BROKERAGE_ACCOUNT=555666777"
64
- }])
65
- end
66
-
67
- formatted_response = format_accounts_list(configured_accounts, account_mappings)
68
-
69
- MCP::Tool::Response.new([{
70
- type: "text",
71
- text: formatted_response
72
- }])
73
-
74
- rescue JSON::ParserError => e
75
- log_error("JSON parsing error: #{e.message}")
76
- MCP::Tool::Response.new([{
77
- type: "text",
78
- text: "**Error**: Failed to parse API response: #{e.message}"
79
- }])
80
- rescue => e
81
- log_error("Error listing accounts: #{e.message}")
82
- log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
83
- MCP::Tool::Response.new([{
84
- type: "text",
85
- text: "**Error** listing accounts: #{e.message}\n\n#{e.backtrace.first(3).join('\n')}"
86
- }])
87
- end
88
- end
89
-
90
- private
91
-
92
- def self.find_configured_accounts(account_mappings)
93
- # Get all account IDs from Schwab API
94
- schwab_account_ids = account_mappings.map { |mapping| mapping["accountNumber"] }
95
-
96
- # Find environment variables ending with "_ACCOUNT"
97
- configured = []
98
- ENV.each do |key, value|
99
- next unless key.end_with?('_ACCOUNT')
100
-
101
- if schwab_account_ids.include?(value)
102
- configured << {
103
- name: key,
104
- friendly_name: friendly_name_from_env_key(key),
105
- account_id: value,
106
- mapping: account_mappings.find { |m| m["accountNumber"] == value }
107
- }
108
- end
109
- end
110
-
111
- configured.sort_by { |account| account[:name] }
112
- end
113
-
114
- def self.friendly_name_from_env_key(env_key)
115
- # Convert "TRADING_BROKERAGE_ACCOUNT" to "Trading Brokerage"
116
- env_key.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
117
- end
118
-
119
- def self.format_accounts_list(configured_accounts, all_mappings)
120
- response = "**Configured Schwab Accounts:**\n\n"
121
-
122
- configured_accounts.each_with_index do |account, index|
123
- response += "#{index + 1}. **#{account[:friendly_name]}** (`#{account[:name]}`)\n"
124
- response += " - Account ID: #{Redactor::REDACTED_ACCOUNT_PLACEHOLDER}\n"
125
- response += " - Status: ✅ Configured\n\n"
126
- end
127
-
128
- # Show unconfigured accounts (if any)
129
- unconfigured = all_mappings.reject do |mapping|
130
- configured_accounts.any? { |config| config[:account_id] == mapping["accountNumber"] }
131
- end
132
-
133
- if unconfigured.any?
134
- response += "**Unconfigured Accounts Available:**\n\n"
135
- unconfigured.each_with_index do |mapping, index|
136
- response += "#{index + 1}. Account ID: #{Redactor::REDACTED_ACCOUNT_PLACEHOLDER}\n"
137
- response += " - To configure: Set `YOUR_NAME_ACCOUNT=#{Redactor::REDACTED_ACCOUNT_PLACEHOLDER}` in your .env file\n\n"
138
- end
139
- end
140
-
141
- response += "**Usage:**\n"
142
- response += "To get account information, use the `schwab_account` tool with one of these account names:\n"
143
- configured_accounts.each do |account|
144
- response += "- `#{account[:name]}`\n"
145
- end
146
-
147
- if configured_accounts.any?
148
- response += "\n**Example:**\n"
149
- first_account = configured_accounts.first
150
- response += "```\n"
151
- response += "Tool: schwab_account\n"
152
- response += "Parameters: {\n"
153
- response += " \"account_name\": \"#{first_account[:name]}\"\n"
154
- response += "}\n"
155
- response += "```"
156
- end
157
-
158
- response
159
- end
160
- end
161
- end
162
- end
@@ -1,378 +0,0 @@
1
- require "mcp"
2
- require "schwab_rb"
3
- require "json"
4
- require "date"
5
- require_relative "../loggable"
6
- require_relative "../option_chain_filter"
7
-
8
- module SchwabMCP
9
- module Tools
10
- class OptionStrategyFinderTool < MCP::Tool
11
- extend Loggable
12
- description "Find option strategies (iron condor, call spread, put spread) using Schwab API"
13
-
14
- input_schema(
15
- properties: {
16
- strategy_type: {
17
- type: "string",
18
- description: "Type of option strategy to find",
19
- enum: ["ironcondor", "callspread", "putspread"]
20
- },
21
- underlying_symbol: {
22
- type: "string",
23
- description: "Underlying symbol for the options (e.g., '$SPX', 'SPY')",
24
- pattern: "^[A-Za-z$]{1,6}$"
25
- },
26
- expiration_date: {
27
- type: "string",
28
- description: "Target expiration date for options (YYYY-MM-DD format)"
29
- },
30
- expiration_type: {
31
- type: "string",
32
- description: "Type of expiration (e.g., 'W' for weekly, 'M' for monthly)",
33
- enum: ["W", "M", "Q"]
34
- },
35
- settlement_type: {
36
- type: "string",
37
- description: "Settlement type (e.g., 'P' for PM settled, 'A' for AM settled)",
38
- enum: ["P", "A"]
39
- },
40
- option_root: {
41
- type: "string",
42
- description: "Option root symbol (e.g., 'SPXW' for weekly SPX options)"
43
- },
44
- max_delta: {
45
- type: "number",
46
- description: "Maximum absolute delta for short legs (default: 0.15)",
47
- minimum: 0.01,
48
- maximum: 1.0
49
- },
50
- max_spread: {
51
- type: "number",
52
- description: "Maximum spread width in dollars (default: 20.0)",
53
- minimum: 1.0
54
- },
55
- min_credit: {
56
- type: "number",
57
- description: "Minimum credit received in dollars (default: 100.0)",
58
- minimum: 0.01
59
- },
60
- min_open_interest: {
61
- type: "integer",
62
- description: "Minimum open interest for options (default: 0)",
63
- minimum: 0
64
- },
65
- dist_from_strike: {
66
- type: "number",
67
- description: "Minimum distance from current price as percentage (default: 0.07)",
68
- minimum: 0.0,
69
- maximum: 1.0
70
- },
71
- quantity: {
72
- type: "integer",
73
- description: "Number of contracts per leg (default: 1)",
74
- minimum: 1
75
- },
76
- from_date: {
77
- type: "string",
78
- description: "Start date for expiration search (YYYY-MM-DD format)"
79
- },
80
- to_date: {
81
- type: "string",
82
- description: "End date for expiration search (YYYY-MM-DD format)"
83
- }
84
- },
85
- required: ["strategy_type", "underlying_symbol", "expiration_date"]
86
- )
87
-
88
- annotations(
89
- title: "Find Option Strategy",
90
- read_only_hint: true,
91
- destructive_hint: false,
92
- idempotent_hint: true
93
- )
94
-
95
- def self.call(strategy_type:, underlying_symbol:, expiration_date:,
96
- expiration_type: nil, settlement_type: nil, option_root: nil,
97
- max_delta: 0.15, max_spread: 20.0, min_credit: 0.0,
98
- min_open_interest: 0, dist_from_strike: 0.0, quantity: 1,
99
- from_date: nil, to_date: nil, server_context:)
100
-
101
- log_info("Finding #{strategy_type} strategy for #{underlying_symbol} expiring #{expiration_date}")
102
-
103
- begin
104
- unless %w[ironcondor callspread putspread].include?(strategy_type.downcase)
105
- return MCP::Tool::Response.new([{
106
- type: "text",
107
- text: "**Error**: Invalid strategy type '#{strategy_type}'. Must be one of: ironcondor, callspread, putspread"
108
- }])
109
- end
110
-
111
- client = SchwabRb::Auth.init_client_easy(
112
- ENV['SCHWAB_API_KEY'],
113
- ENV['SCHWAB_APP_SECRET'],
114
- ENV['SCHWAB_CALLBACK_URI'],
115
- ENV['TOKEN_PATH']
116
- )
117
-
118
- unless client
119
- log_error("Failed to initialize Schwab client")
120
- return MCP::Tool::Response.new([{
121
- type: "text",
122
- text: "**Error**: Failed to initialize Schwab client. Check your credentials."
123
- }])
124
- end
125
-
126
- exp_date = Date.parse(expiration_date)
127
- from_dt = from_date ? Date.parse(from_date) : exp_date
128
- to_dt = to_date ? Date.parse(to_date) : exp_date
129
-
130
- contract_type = strategy_type.downcase == 'callspread' ? 'CALL' :
131
- strategy_type.downcase == 'putspread' ? 'PUT' : 'ALL'
132
-
133
- log_debug("Fetching option chain for #{underlying_symbol} (#{contract_type})")
134
-
135
- response = client.get_option_chain(
136
- underlying_symbol.upcase,
137
- contract_type: contract_type,
138
- from_date: from_dt,
139
- to_date: to_dt,
140
- include_underlying_quote: true
141
- )
142
-
143
- unless response&.body
144
- log_warn("Empty response from Schwab API for #{underlying_symbol}")
145
- return MCP::Tool::Response.new([{
146
- type: "text",
147
- text: "**No Data**: Could not retrieve option chain for #{underlying_symbol}"
148
- }])
149
- end
150
-
151
- option_data = JSON.parse(response.body, symbolize_names: true)
152
-
153
- result = find_strategy(
154
- strategy_type: strategy_type.downcase,
155
- option_data: option_data,
156
- underlying_symbol: underlying_symbol,
157
- expiration_date: exp_date,
158
- expiration_type: expiration_type,
159
- settlement_type: settlement_type,
160
- option_root: option_root,
161
- max_delta: max_delta,
162
- max_spread: max_spread,
163
- min_credit: min_credit,
164
- min_open_interest: min_open_interest,
165
- dist_from_strike: dist_from_strike,
166
- quantity: quantity
167
- )
168
-
169
- if result.nil? || result[:status] == 'not_found'
170
- log_info("No suitable #{strategy_type} found for #{underlying_symbol}")
171
- return MCP::Tool::Response.new([{
172
- type: "text",
173
- text: "**No Strategy Found**: Could not find a suitable #{strategy_type} for #{underlying_symbol} with the specified criteria."
174
- }])
175
- else
176
- log_info("Found #{strategy_type} strategy for #{underlying_symbol}")
177
- return MCP::Tool::Response.new([{
178
- type: "text",
179
- text: format_strategy_result(result, strategy_type)
180
- }])
181
- end
182
- rescue Date::Error => e
183
- log_error("Invalid date format: #{e.message}")
184
- return MCP::Tool::Response.new([{
185
- type: "text",
186
- text: "**Error**: Invalid date format. Use YYYY-MM-DD format."
187
- }])
188
- rescue JSON::ParserError => e
189
- log_error("Failed to parse option chain data: #{e.message}")
190
- return MCP::Tool::Response.new([{
191
- type: "text",
192
- text: "**Error**: Failed to parse option chain data from Schwab API."
193
- }])
194
- rescue => e
195
- log_error("Error finding #{strategy_type} for #{underlying_symbol}: #{e.message}")
196
- log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
197
- return MCP::Tool::Response.new([{
198
- type: "text",
199
- text: "**Error** finding #{strategy_type} for #{underlying_symbol}: #{e.message}"
200
- }])
201
- end
202
- end
203
-
204
- private
205
-
206
- def self.find_strategy(strategy_type:, option_data:, underlying_symbol:, expiration_date:,
207
- expiration_type:, settlement_type:, option_root:, max_delta:,
208
- max_spread:, min_credit:, min_open_interest:, dist_from_strike:, quantity:)
209
-
210
- case strategy_type
211
- when 'ironcondor'
212
- find_iron_condor(option_data, underlying_symbol, expiration_date, expiration_type,
213
- settlement_type, option_root, max_delta, max_spread, min_credit / 2.0,
214
- min_open_interest, dist_from_strike, quantity)
215
- when 'callspread'
216
- find_spread(option_data, 'call', underlying_symbol, expiration_date, expiration_type,
217
- settlement_type, option_root, max_delta, max_spread, min_credit,
218
- min_open_interest, dist_from_strike, quantity)
219
- when 'putspread'
220
- find_spread(option_data, 'put', underlying_symbol, expiration_date, expiration_type,
221
- settlement_type, option_root, max_delta, max_spread, min_credit,
222
- min_open_interest, dist_from_strike, quantity)
223
- end
224
- end
225
-
226
- def self.find_iron_condor(option_data, underlying_symbol, expiration_date, expiration_type,
227
- settlement_type, option_root, max_delta, max_spread, min_credit,
228
- min_open_interest, dist_from_strike, quantity)
229
-
230
- underlying_price = option_data.dig(:underlyingPrice) || 0.0
231
- call_options = option_data.dig(:callExpDateMap) || {}
232
- put_options = option_data.dig(:putExpDateMap) || {}
233
-
234
- filter = SchwabMCP::OptionChainFilter.new(
235
- expiration_date: expiration_date,
236
- underlying_price: underlying_price,
237
- expiration_type: expiration_type,
238
- settlement_type: settlement_type,
239
- option_root: option_root,
240
- max_delta: max_delta,
241
- max_spread: max_spread,
242
- min_credit: min_credit,
243
- min_open_interest: min_open_interest,
244
- dist_from_strike: dist_from_strike,
245
- quantity: quantity
246
- )
247
-
248
- call_spreads = filter.find_spreads(call_options, 'call')
249
- put_spreads = filter.find_spreads(put_options, 'put')
250
-
251
- return { status: 'not_found' } if call_spreads.empty? || put_spreads.empty?
252
-
253
- best_combo = nil
254
- best_ratio = 0
255
-
256
- call_spreads.each do |call_spread|
257
- put_spreads.each do |put_spread|
258
- total_credit = call_spread[:credit] + put_spread[:credit]
259
- next if total_credit < min_credit / 100.0
260
-
261
- total_delta = call_spread[:delta].abs + put_spread[:delta].abs
262
- ratio = total_credit / total_delta if total_delta > 0
263
-
264
- if ratio > best_ratio
265
- best_ratio = ratio
266
- best_combo = {
267
- type: 'iron_condor',
268
- call_spread: call_spread,
269
- put_spread: put_spread,
270
- total_credit: total_credit,
271
- total_delta: total_delta,
272
- underlying_price: underlying_price
273
- }
274
- end
275
- end
276
- end
277
-
278
- best_combo || { status: 'not_found' }
279
- end
280
-
281
- def self.find_spread(option_data, spread_type, underlying_symbol, expiration_date, expiration_type,
282
- settlement_type, option_root, max_delta, max_spread, min_credit,
283
- min_open_interest, dist_from_strike, quantity)
284
-
285
- underlying_price = option_data.dig(:underlyingPrice) || 0.0
286
- options_map = case spread_type
287
- when 'call'
288
- option_data.dig(:callExpDateMap) || {}
289
- when 'put'
290
- option_data.dig(:putExpDateMap) || {}
291
- else
292
- return { status: 'not_found' }
293
- end
294
-
295
- filter = SchwabMCP::OptionChainFilter.new(
296
- expiration_date: expiration_date,
297
- underlying_price: underlying_price,
298
- expiration_type: expiration_type,
299
- settlement_type: settlement_type,
300
- option_root: option_root,
301
- max_delta: max_delta,
302
- max_spread: max_spread,
303
- min_credit: min_credit,
304
- min_open_interest: min_open_interest,
305
- dist_from_strike: dist_from_strike,
306
- quantity: quantity
307
- )
308
-
309
- spreads = filter.find_spreads(options_map, spread_type)
310
-
311
- return { status: 'not_found' } if spreads.empty?
312
-
313
- best_spread = spreads.max_by { |spread| spread[:credit] }
314
- best_spread.merge(type: "#{spread_type}_spread", underlying_price: underlying_price)
315
- end
316
-
317
- def self.format_strategy_result(result, strategy_type)
318
- case result[:type]
319
- when 'iron_condor'
320
- format_iron_condor(result)
321
- when 'call_spread', 'put_spread'
322
- format_spread(result, result[:type])
323
- else
324
- "**Found Strategy**: #{strategy_type.upcase}\n\n#{result.to_json}"
325
- end
326
- end
327
-
328
- def self.format_iron_condor(result)
329
- call_spread = result[:call_spread]
330
- put_spread = result[:put_spread]
331
-
332
- <<~TEXT
333
- **IRON CONDOR FOUND**
334
-
335
- **Underlying Price**: $#{result[:underlying_price].round(2)}
336
- **Total Credit**: $#{(result[:total_credit] * 100).round(2)}
337
-
338
- **Call Spread (Short)**:
339
- - Short: #{call_spread[:short_option][:symbol]} $#{call_spread[:short_option][:strikePrice]} Call @ $#{call_spread[:short_option][:mark].round(2)}
340
- - Long: #{call_spread[:long_option][:symbol]} $#{call_spread[:long_option][:strikePrice]} Call @ $#{call_spread[:long_option][:mark].round(2)}
341
- - Credit: $#{(call_spread[:credit] * 100).round(2)}
342
- - Width: $#{call_spread[:spread_width].round(2)}
343
- - Delta: #{call_spread[:delta].round(2)}
344
-
345
- **Put Spread (Short)**:
346
- - Short: #{put_spread[:short_option][:symbol]} $#{put_spread[:short_option][:strikePrice]} Put @ $#{put_spread[:short_option][:mark].round(2)}
347
- - Long: #{put_spread[:long_option][:symbol]} $#{put_spread[:long_option][:strikePrice]} Put @ $#{put_spread[:long_option][:mark].round(2)}
348
- - Credit: $#{(put_spread[:credit] * 100).round(2)}
349
- - Width: $#{put_spread[:spread_width].round(2)}
350
- - Delta: #{put_spread[:delta].round(2)}
351
- TEXT
352
- end
353
-
354
- def self.format_spread(result, spread_type)
355
- short_opt = result[:short_option]
356
- long_opt = result[:long_option]
357
- option_type = spread_type == 'call_spread' ? 'Call' : 'Put'
358
-
359
- <<~TEXT
360
- **#{option_type.upcase} SPREAD FOUND**
361
-
362
- **Underlying Price**: $#{result[:underlying_price].round(2)}
363
- **Credit**: $#{(result[:credit] * 100).round(2)}
364
- **Spread Width**: $#{result[:spread_width].round(2)}
365
- **Delta**: #{result[:delta].round(4)}
366
-
367
- **Short**: #{short_opt[:symbol]} $#{short_opt[:strikePrice]} #{option_type} @ $#{short_opt[:mark].round(2)}
368
- - Delta: #{short_opt[:delta]&.round(4)}
369
- - Open Interest: #{short_opt[:openInterest]}
370
-
371
- **Long**: #{long_opt[:symbol]} $#{long_opt[:strikePrice]} #{option_type} @ $#{long_opt[:mark].round(2)}
372
- - Delta: #{long_opt[:delta]&.round(4)}
373
- - Open Interest: #{long_opt[:openInterest]}
374
- TEXT
375
- end
376
- end
377
- end
378
- end
data/start_mcp_server.sh DELETED
@@ -1,4 +0,0 @@
1
- #!/bin/bash
2
-
3
- cd /Users/jplatta/repos/schwab_mcp
4
- exec /Users/jplatta/.asdf/shims/bundle exec exe/schwab_mcp