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.
- checksums.yaml +4 -4
- data/.claude/settings.json +14 -0
- data/CLAUDE.md +124 -0
- data/debug_env.rb +46 -0
- data/doc/DATA_OBJECTS_MIGRATION_TODO.md +80 -0
- data/doc/SCHWAB_CLIENT_FACTORY_REFACTOR_PLAN.md +187 -0
- data/exe/schwab_mcp +14 -3
- data/exe/schwab_token_refresh +10 -9
- data/lib/schwab_mcp/redactor.rb +4 -0
- data/lib/schwab_mcp/schwab_client_factory.rb +44 -0
- data/lib/schwab_mcp/tools/cancel_order_tool.rb +29 -50
- data/lib/schwab_mcp/tools/get_market_hours_tool.rb +27 -28
- data/lib/schwab_mcp/tools/get_order_tool.rb +51 -108
- data/lib/schwab_mcp/tools/get_price_history_tool.rb +23 -35
- data/lib/schwab_mcp/tools/help_tool.rb +1 -22
- data/lib/schwab_mcp/tools/list_account_orders_tool.rb +35 -63
- data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +43 -72
- data/lib/schwab_mcp/tools/list_movers_tool.rb +21 -34
- data/lib/schwab_mcp/tools/list_schwab_accounts_tool.rb +18 -31
- data/lib/schwab_mcp/tools/option_chain_tool.rb +130 -82
- data/lib/schwab_mcp/tools/place_order_tool.rb +105 -117
- data/lib/schwab_mcp/tools/preview_order_tool.rb +100 -48
- data/lib/schwab_mcp/tools/quote_tool.rb +33 -26
- data/lib/schwab_mcp/tools/quotes_tool.rb +97 -45
- data/lib/schwab_mcp/tools/replace_order_tool.rb +104 -116
- data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +56 -72
- data/lib/schwab_mcp/version.rb +1 -1
- data/lib/schwab_mcp.rb +1 -2
- data/orders_example.json +7084 -0
- data/spx_option_chain.json +25073 -0
- data/test_mcp.rb +16 -0
- data/test_server.rb +23 -0
- data/trading_brokerage_account_details.json +89 -0
- data/transactions_example.json +488 -0
- metadata +17 -7
- data/lib/schwab_mcp/option_chain_filter.rb +0 -213
- data/lib/schwab_mcp/orders/iron_condor_order.rb +0 -87
- data/lib/schwab_mcp/orders/order_factory.rb +0 -40
- data/lib/schwab_mcp/orders/vertical_order.rb +0 -62
- data/lib/schwab_mcp/tools/option_strategy_finder_tool.rb +0 -378
@@ -2,6 +2,7 @@ require "mcp"
|
|
2
2
|
require "schwab_rb"
|
3
3
|
require "json"
|
4
4
|
require_relative "../loggable"
|
5
|
+
require_relative "../schwab_client_factory"
|
5
6
|
|
6
7
|
module SchwabMCP
|
7
8
|
module Tools
|
@@ -52,20 +53,8 @@ module SchwabMCP
|
|
52
53
|
end
|
53
54
|
|
54
55
|
begin
|
55
|
-
client =
|
56
|
-
|
57
|
-
ENV['SCHWAB_APP_SECRET'],
|
58
|
-
ENV['SCHWAB_CALLBACK_URI'],
|
59
|
-
ENV['TOKEN_PATH']
|
60
|
-
)
|
61
|
-
|
62
|
-
unless client
|
63
|
-
log_error("Failed to initialize Schwab client")
|
64
|
-
return MCP::Tool::Response.new([{
|
65
|
-
type: "text",
|
66
|
-
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
67
|
-
}])
|
68
|
-
end
|
56
|
+
client = SchwabClientFactory.create_client
|
57
|
+
return SchwabClientFactory.client_error_response unless client
|
69
58
|
|
70
59
|
account_id = ENV[account_name]
|
71
60
|
unless account_id
|
@@ -80,9 +69,9 @@ module SchwabMCP
|
|
80
69
|
log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
|
81
70
|
log_debug("Fetching account numbers mapping")
|
82
71
|
|
83
|
-
account_numbers_response = client.get_account_numbers
|
84
72
|
|
85
|
-
|
73
|
+
account_numbers = client.get_account_numbers # returns SchwabRb::DataObjects::AccountNumbers
|
74
|
+
unless account_numbers && account_numbers.respond_to?(:accounts)
|
86
75
|
log_error("Failed to retrieve account numbers")
|
87
76
|
return MCP::Tool::Response.new([{
|
88
77
|
type: "text",
|
@@ -90,13 +79,10 @@ module SchwabMCP
|
|
90
79
|
}])
|
91
80
|
end
|
92
81
|
|
93
|
-
account_mappings = JSON.parse(account_numbers_response.body, symbolize_names: true)
|
94
|
-
log_debug("Account mappings retrieved (#{account_mappings.length} accounts found)")
|
95
|
-
|
96
82
|
account_hash = nil
|
97
|
-
|
98
|
-
if
|
99
|
-
account_hash =
|
83
|
+
account_numbers.accounts.each do |acct|
|
84
|
+
if acct.account_number.to_s == account_id.to_s
|
85
|
+
account_hash = acct.hash_value
|
100
86
|
break
|
101
87
|
end
|
102
88
|
end
|
@@ -105,16 +91,16 @@ module SchwabMCP
|
|
105
91
|
log_error("Account ID not found in available accounts")
|
106
92
|
return MCP::Tool::Response.new([{
|
107
93
|
type: "text",
|
108
|
-
text: "**Error**: Account ID not found in available accounts. #{
|
94
|
+
text: "**Error**: Account ID not found in available accounts. #{account_numbers.accounts.length} accounts available."
|
109
95
|
}])
|
110
96
|
end
|
111
97
|
|
112
98
|
log_debug("Found account hash for account ID: #{account_name}")
|
113
99
|
log_debug("Verifying order exists before attempting cancellation")
|
114
100
|
|
115
|
-
order_response = client.get_order(order_id, account_hash)
|
116
101
|
|
117
|
-
|
102
|
+
order = client.get_order(order_id, account_hash) # returns SchwabRb::DataObjects::Order
|
103
|
+
unless order
|
118
104
|
log_warn("Order not found or empty response for order ID: #{order_id}")
|
119
105
|
return MCP::Tool::Response.new([{
|
120
106
|
type: "text",
|
@@ -122,9 +108,8 @@ module SchwabMCP
|
|
122
108
|
}])
|
123
109
|
end
|
124
110
|
|
125
|
-
|
126
|
-
|
127
|
-
cancelable = order_data['cancelable']
|
111
|
+
order_status = order.status
|
112
|
+
cancelable = order.respond_to?(:cancelable) ? order.cancelable : true # fallback if attribute not present
|
128
113
|
|
129
114
|
log_debug("Order found - Status: #{order_status}, Cancelable: #{cancelable}")
|
130
115
|
if cancelable == false
|
@@ -140,7 +125,7 @@ module SchwabMCP
|
|
140
125
|
|
141
126
|
if cancel_response.respond_to?(:status) && cancel_response.status == 200
|
142
127
|
log_info("Successfully cancelled order ID: #{order_id}")
|
143
|
-
formatted_response = format_cancellation_success(order_id, account_name,
|
128
|
+
formatted_response = format_cancellation_success(order_id, account_name, order)
|
144
129
|
elsif cancel_response.respond_to?(:status) && cancel_response.status == 404
|
145
130
|
log_warn("Order not found during cancellation: #{order_id}")
|
146
131
|
return MCP::Tool::Response.new([{
|
@@ -149,7 +134,7 @@ module SchwabMCP
|
|
149
134
|
}])
|
150
135
|
else
|
151
136
|
log_info("Order cancellation request submitted for order ID: #{order_id}")
|
152
|
-
formatted_response = format_cancellation_success(order_id, account_name,
|
137
|
+
formatted_response = format_cancellation_success(order_id, account_name, order)
|
153
138
|
end
|
154
139
|
|
155
140
|
MCP::Tool::Response.new([{
|
@@ -157,12 +142,7 @@ module SchwabMCP
|
|
157
142
|
text: formatted_response
|
158
143
|
}])
|
159
144
|
|
160
|
-
|
161
|
-
log_error("JSON parsing error: #{e.message}")
|
162
|
-
MCP::Tool::Response.new([{
|
163
|
-
type: "text",
|
164
|
-
text: "**Error**: Failed to parse API response: #{e.message}"
|
165
|
-
}])
|
145
|
+
# No JSON::ParserError rescue needed with data objects
|
166
146
|
rescue => e
|
167
147
|
log_error("Error cancelling order ID #{order_id}: #{e.message}")
|
168
148
|
log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
|
@@ -186,27 +166,26 @@ module SchwabMCP
|
|
186
166
|
|
187
167
|
private
|
188
168
|
|
189
|
-
def self.format_cancellation_success(order_id, account_name,
|
169
|
+
def self.format_cancellation_success(order_id, account_name, order)
|
190
170
|
friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
|
191
171
|
|
192
172
|
formatted = "**✅ Order Cancellation Successful**\n\n"
|
193
173
|
formatted += "**Order ID**: #{order_id}\n"
|
194
174
|
formatted += "**Account**: #{friendly_name} (#{account_name})\n\n"
|
195
175
|
formatted += "**Order Details:**\n"
|
196
|
-
formatted += "- Original Status: #{
|
197
|
-
formatted += "- Order Type: #{
|
198
|
-
formatted += "-
|
199
|
-
formatted += "-
|
200
|
-
formatted += "-
|
201
|
-
|
202
|
-
|
203
|
-
if order_data['orderLegCollection'] && order_data['orderLegCollection'].any?
|
176
|
+
formatted += "- Original Status: #{order.status}\n" if order.status
|
177
|
+
formatted += "- Order Type: #{order.order_type}\n" if order.order_type
|
178
|
+
formatted += "- Duration: #{order.duration}\n" if order.duration
|
179
|
+
formatted += "- Quantity: #{order.quantity}\n" if order.quantity
|
180
|
+
formatted += "- Price: $#{format_currency(order.price)}\n" if order.price
|
181
|
+
|
182
|
+
if order.order_leg_collection && order.order_leg_collection.any?
|
204
183
|
formatted += "\n**Instruments:**\n"
|
205
|
-
|
206
|
-
if leg
|
207
|
-
symbol = leg
|
208
|
-
instruction = leg
|
209
|
-
quantity = leg
|
184
|
+
order.order_leg_collection.each do |leg|
|
185
|
+
if leg.instrument
|
186
|
+
symbol = leg.instrument.symbol
|
187
|
+
instruction = leg.instruction
|
188
|
+
quantity = leg.quantity
|
210
189
|
formatted += "- #{symbol}: #{instruction} #{quantity}\n"
|
211
190
|
end
|
212
191
|
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "mcp"
|
2
4
|
require "schwab_rb"
|
3
|
-
require "json"
|
4
5
|
require "date"
|
5
6
|
require_relative "../loggable"
|
7
|
+
require_relative "../schwab_client_factory"
|
6
8
|
|
7
9
|
module SchwabMCP
|
8
10
|
module Tools
|
@@ -42,22 +44,9 @@ module SchwabMCP
|
|
42
44
|
log_debug("Date parameter: #{date || 'today'}")
|
43
45
|
|
44
46
|
begin
|
45
|
-
client =
|
46
|
-
|
47
|
-
ENV['SCHWAB_APP_SECRET'],
|
48
|
-
ENV['SCHWAB_CALLBACK_URI'],
|
49
|
-
ENV['TOKEN_PATH']
|
50
|
-
)
|
51
|
-
|
52
|
-
unless client
|
53
|
-
log_error("Failed to initialize Schwab client")
|
54
|
-
return MCP::Tool::Response.new([{
|
55
|
-
type: "text",
|
56
|
-
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
57
|
-
}])
|
58
|
-
end
|
47
|
+
client = SchwabClientFactory.create_client
|
48
|
+
return SchwabClientFactory.client_error_response unless client
|
59
49
|
|
60
|
-
# Parse date if provided
|
61
50
|
parsed_date = nil
|
62
51
|
if date
|
63
52
|
begin
|
@@ -73,23 +62,24 @@ module SchwabMCP
|
|
73
62
|
end
|
74
63
|
|
75
64
|
log_debug("Making API request for markets: #{markets.join(', ')}")
|
76
|
-
|
65
|
+
market_hours_obj = client.get_market_hours(markets, date: parsed_date, return_data_objects: true)
|
77
66
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
MCP::Tool::Response.new([{
|
82
|
-
type: "text",
|
83
|
-
text: "**Market Hours#{date_info}:**\n\n```json\n#{response.body}\n```"
|
84
|
-
}])
|
85
|
-
else
|
86
|
-
log_warn("Empty response from Schwab API for markets: #{markets.join(', ')}")
|
87
|
-
MCP::Tool::Response.new([{
|
67
|
+
unless market_hours_obj
|
68
|
+
log_warn("No market hours data object returned for markets: #{markets.join(', ')}")
|
69
|
+
return MCP::Tool::Response.new([{
|
88
70
|
type: "text",
|
89
|
-
text: "**No Data**:
|
71
|
+
text: "**No Data**: No market hours data returned for markets: #{markets.join(', ')}"
|
90
72
|
}])
|
91
73
|
end
|
92
74
|
|
75
|
+
formatted = format_market_hours_object(market_hours_obj)
|
76
|
+
log_info("Successfully retrieved market hours for #{markets.join(', ')}")
|
77
|
+
date_info = date ? " for #{date}" : " for today"
|
78
|
+
MCP::Tool::Response.new([{
|
79
|
+
type: "text",
|
80
|
+
text: "**Market Hours#{date_info}:**\n\n#{formatted}"
|
81
|
+
}])
|
82
|
+
|
93
83
|
rescue => e
|
94
84
|
log_error("Error retrieving market hours for #{markets.join(', ')}: #{e.message}")
|
95
85
|
log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
|
@@ -99,6 +89,15 @@ module SchwabMCP
|
|
99
89
|
}])
|
100
90
|
end
|
101
91
|
end
|
92
|
+
|
93
|
+
# Format the market hours object for display
|
94
|
+
def self.format_market_hours_object(obj)
|
95
|
+
if obj.respond_to?(:to_h)
|
96
|
+
obj.to_h.inspect
|
97
|
+
else
|
98
|
+
obj.inspect
|
99
|
+
end
|
100
|
+
end
|
102
101
|
end
|
103
102
|
end
|
104
103
|
end
|
@@ -3,6 +3,7 @@ require "schwab_rb"
|
|
3
3
|
require "json"
|
4
4
|
require_relative "../loggable"
|
5
5
|
require_relative "../redactor"
|
6
|
+
require_relative "../schwab_client_factory"
|
6
7
|
|
7
8
|
module SchwabMCP
|
8
9
|
module Tools
|
@@ -53,20 +54,8 @@ module SchwabMCP
|
|
53
54
|
end
|
54
55
|
|
55
56
|
begin
|
56
|
-
client =
|
57
|
-
|
58
|
-
ENV['SCHWAB_APP_SECRET'],
|
59
|
-
ENV['SCHWAB_CALLBACK_URI'],
|
60
|
-
ENV['TOKEN_PATH']
|
61
|
-
)
|
62
|
-
|
63
|
-
unless client
|
64
|
-
log_error("Failed to initialize Schwab client")
|
65
|
-
return MCP::Tool::Response.new([{
|
66
|
-
type: "text",
|
67
|
-
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
68
|
-
}])
|
69
|
-
end
|
57
|
+
client = SchwabClientFactory.create_client
|
58
|
+
return SchwabClientFactory.client_error_response unless client
|
70
59
|
|
71
60
|
account_id = ENV[account_name]
|
72
61
|
unless account_id
|
@@ -81,9 +70,9 @@ module SchwabMCP
|
|
81
70
|
log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
|
82
71
|
log_debug("Fetching account numbers mapping")
|
83
72
|
|
84
|
-
account_numbers_response = client.get_account_numbers
|
85
73
|
|
86
|
-
|
74
|
+
account_numbers = client.get_account_numbers # returns SchwabRb::DataObjects::AccountNumbers
|
75
|
+
unless account_numbers && account_numbers.respond_to?(:accounts)
|
87
76
|
log_error("Failed to retrieve account numbers")
|
88
77
|
return MCP::Tool::Response.new([{
|
89
78
|
type: "text",
|
@@ -91,13 +80,10 @@ module SchwabMCP
|
|
91
80
|
}])
|
92
81
|
end
|
93
82
|
|
94
|
-
account_mappings = JSON.parse(account_numbers_response.body, symbolize_names: true)
|
95
|
-
log_debug("Account mappings retrieved (#{account_mappings.length} accounts found)")
|
96
|
-
|
97
83
|
account_hash = nil
|
98
|
-
|
99
|
-
if
|
100
|
-
account_hash =
|
84
|
+
account_numbers.accounts.each do |acct|
|
85
|
+
if acct.account_number.to_s == account_id.to_s
|
86
|
+
account_hash = acct.hash_value
|
101
87
|
break
|
102
88
|
end
|
103
89
|
end
|
@@ -106,21 +92,17 @@ module SchwabMCP
|
|
106
92
|
log_error("Account ID not found in available accounts")
|
107
93
|
return MCP::Tool::Response.new([{
|
108
94
|
type: "text",
|
109
|
-
text: "**Error**: Account ID not found in available accounts. #{
|
95
|
+
text: "**Error**: Account ID not found in available accounts. #{account_numbers.accounts.length} accounts available."
|
110
96
|
}])
|
111
97
|
end
|
112
98
|
|
113
99
|
log_debug("Found account hash for account ID: #{account_name}")
|
114
100
|
log_debug("Fetching order details for order ID: #{order_id}")
|
115
101
|
|
116
|
-
|
117
|
-
|
118
|
-
if order_response&.body
|
102
|
+
order = client.get_order(order_id, account_hash) # returns SchwabRb::DataObjects::Order
|
103
|
+
if order
|
119
104
|
log_info("Successfully retrieved order details for order ID: #{order_id}")
|
120
|
-
|
121
|
-
|
122
|
-
formatted_response = format_order_data(order_data, order_id, account_name)
|
123
|
-
|
105
|
+
formatted_response = format_order_object(order, order_id, account_name)
|
124
106
|
MCP::Tool::Response.new([{
|
125
107
|
type: "text",
|
126
108
|
text: formatted_response
|
@@ -132,13 +114,6 @@ module SchwabMCP
|
|
132
114
|
text: "**No Data**: Empty response from Schwab API for order ID: #{order_id}. Order may not exist or may be in a different account."
|
133
115
|
}])
|
134
116
|
end
|
135
|
-
|
136
|
-
rescue JSON::ParserError => e
|
137
|
-
log_error("JSON parsing error: #{e.message}")
|
138
|
-
MCP::Tool::Response.new([{
|
139
|
-
type: "text",
|
140
|
-
text: "**Error**: Failed to parse API response: #{e.message}"
|
141
|
-
}])
|
142
117
|
rescue => e
|
143
118
|
log_error("Error retrieving order details for order ID #{order_id}: #{e.message}")
|
144
119
|
log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
|
@@ -151,104 +126,72 @@ module SchwabMCP
|
|
151
126
|
|
152
127
|
private
|
153
128
|
|
154
|
-
def self.format_order_data(order_data, order_id, account_name)
|
155
|
-
friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
|
156
129
|
|
130
|
+
def self.format_order_object(order, order_id, account_name)
|
131
|
+
friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
|
157
132
|
formatted = "**Order Details for Order ID #{order_id}:**\n\n"
|
158
133
|
formatted += "**Account:** #{friendly_name} (#{account_name})\n\n"
|
159
134
|
|
160
135
|
formatted += "**Order Information:**\n"
|
161
|
-
formatted += "- Order ID: #{
|
162
|
-
formatted += "- Status: #{
|
163
|
-
formatted += "- Order Type: #{
|
164
|
-
formatted += "-
|
165
|
-
formatted += "-
|
166
|
-
formatted += "- Complex Order Strategy Type: #{order_data['complexOrderStrategyType']}\n" if order_data['complexOrderStrategyType']
|
167
|
-
formatted += "- Cancelable: #{order_data['cancelable']}\n" if order_data.key?('cancelable')
|
168
|
-
formatted += "- Editable: #{order_data['editable']}\n" if order_data.key?('editable')
|
136
|
+
formatted += "- Order ID: #{order.order_id}\n" if order.order_id
|
137
|
+
formatted += "- Status: #{order.status}\n" if order.status
|
138
|
+
formatted += "- Order Type: #{order.order_type}\n" if order.order_type
|
139
|
+
formatted += "- Duration: #{order.duration}\n" if order.duration
|
140
|
+
formatted += "- Complex Order Strategy Type: #{order.complex_order_strategy_type}\n" if order.complex_order_strategy_type
|
169
141
|
|
170
142
|
formatted += "\n**Timing:**\n"
|
171
|
-
formatted += "- Entered Time: #{
|
172
|
-
formatted += "- Close Time: #{
|
143
|
+
formatted += "- Entered Time: #{order.entered_time}\n" if order.entered_time
|
144
|
+
formatted += "- Close Time: #{order.close_time}\n" if order.close_time
|
173
145
|
|
174
146
|
formatted += "\n**Quantity & Pricing:**\n"
|
175
|
-
formatted += "- Quantity: #{
|
176
|
-
formatted += "- Filled Quantity: #{
|
177
|
-
formatted += "- Remaining Quantity: #{
|
178
|
-
formatted += "-
|
179
|
-
formatted += "- Destination Link Name: #{order_data['destinationLinkName']}\n" if order_data['destinationLinkName']
|
180
|
-
formatted += "- Price: $#{format_currency(order_data['price'])}\n" if order_data['price']
|
181
|
-
formatted += "- Stop Price: $#{format_currency(order_data['stopPrice'])}\n" if order_data['stopPrice']
|
182
|
-
formatted += "- Stop Price Link Basis: #{order_data['stopPriceLinkBasis']}\n" if order_data['stopPriceLinkBasis']
|
183
|
-
formatted += "- Stop Price Link Type: #{order_data['stopPriceLinkType']}\n" if order_data['stopPriceLinkType']
|
184
|
-
formatted += "- Stop Price Offset: $#{format_currency(order_data['stopPriceOffset'])}\n" if order_data['stopPriceOffset']
|
185
|
-
formatted += "- Stop Type: #{order_data['stopType']}\n" if order_data['stopType']
|
147
|
+
formatted += "- Quantity: #{order.quantity}\n" if order.quantity
|
148
|
+
formatted += "- Filled Quantity: #{order.filled_quantity}\n" if order.filled_quantity
|
149
|
+
formatted += "- Remaining Quantity: #{order.remaining_quantity}\n" if order.remaining_quantity
|
150
|
+
formatted += "- Price: $#{format_currency(order.price)}\n" if order.price
|
186
151
|
|
187
|
-
if
|
152
|
+
if order.order_leg_collection && order.order_leg_collection.any?
|
188
153
|
formatted += "\n**Order Legs:**\n"
|
189
|
-
|
154
|
+
order.order_leg_collection.each_with_index do |leg, index|
|
190
155
|
formatted += "**Leg #{index + 1}:**\n"
|
191
|
-
formatted += "- Instruction: #{leg
|
192
|
-
formatted += "- Quantity: #{leg
|
193
|
-
formatted += "- Position Effect: #{leg
|
194
|
-
formatted += "- Quantity Type: #{leg['quantityType']}\n" if leg['quantityType']
|
156
|
+
formatted += "- Instruction: #{leg.instruction}\n" if leg.instruction
|
157
|
+
formatted += "- Quantity: #{leg.quantity}\n" if leg.quantity
|
158
|
+
formatted += "- Position Effect: #{leg.position_effect}\n" if leg.position_effect
|
195
159
|
|
196
|
-
if leg
|
197
|
-
instrument = leg
|
160
|
+
if leg.instrument
|
161
|
+
instrument = leg.instrument
|
198
162
|
formatted += "- **Instrument:**\n"
|
199
|
-
formatted += " * Asset Type: #{instrument
|
200
|
-
formatted += " * Symbol: #{instrument
|
201
|
-
formatted += " * Description: #{instrument
|
202
|
-
formatted += " * CUSIP: #{instrument['cusip']}\n" if instrument['cusip']
|
203
|
-
formatted += " * Net Change: #{instrument['netChange']}\n" if instrument['netChange']
|
204
|
-
|
205
|
-
if instrument['putCall']
|
206
|
-
formatted += " * Option Type: #{instrument['putCall']}\n"
|
207
|
-
formatted += " * Strike Price: $#{format_currency(instrument['strikePrice'])}\n" if instrument['strikePrice']
|
208
|
-
formatted += " * Expiration Date: #{instrument['expirationDate']}\n" if instrument['expirationDate']
|
209
|
-
formatted += " * Days to Expiration: #{instrument['daysToExpiration']}\n" if instrument['daysToExpiration']
|
210
|
-
formatted += " * Expiration Type: #{instrument['expirationType']}\n" if instrument['expirationType']
|
211
|
-
formatted += " * Exercise Type: #{instrument['exerciseType']}\n" if instrument['exerciseType']
|
212
|
-
formatted += " * Settlement Type: #{instrument['settlementType']}\n" if instrument['settlementType']
|
213
|
-
formatted += " * Deliverables: #{instrument['deliverables']}\n" if instrument['deliverables']
|
214
|
-
end
|
163
|
+
formatted += " * Asset Type: #{instrument.asset_type}\n" if instrument.asset_type
|
164
|
+
formatted += " * Symbol: #{instrument.symbol}\n" if instrument.symbol
|
165
|
+
formatted += " * Description: #{instrument.description}\n" if instrument.description
|
215
166
|
end
|
216
|
-
formatted += "\n" unless index ==
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
if order_data['childOrderStrategies'] && order_data['childOrderStrategies'].any?
|
221
|
-
formatted += "\n**Child Order Strategies:**\n"
|
222
|
-
formatted += "- Number of Child Orders: #{order_data['childOrderStrategies'].length}\n"
|
223
|
-
order_data['childOrderStrategies'].each_with_index do |child, index|
|
224
|
-
formatted += "- Child Order #{index + 1}: #{child['orderId']} (Status: #{child['status']})\n" if child['orderId'] && child['status']
|
167
|
+
formatted += "\n" unless index == order.order_leg_collection.length - 1
|
225
168
|
end
|
226
169
|
end
|
227
170
|
|
228
|
-
if
|
171
|
+
if order.order_activity_collection && order.order_activity_collection.any?
|
229
172
|
formatted += "\n**Order Activities:**\n"
|
230
|
-
|
173
|
+
order.order_activity_collection.each_with_index do |activity, index|
|
231
174
|
formatted += "**Activity #{index + 1}:**\n"
|
232
|
-
formatted += "- Activity Type: #{activity
|
233
|
-
formatted += "- Execution Type: #{activity
|
234
|
-
formatted += "- Quantity: #{activity
|
235
|
-
formatted += "- Order Remaining Quantity: #{activity
|
175
|
+
formatted += "- Activity Type: #{activity.activity_type}\n" if activity.activity_type
|
176
|
+
formatted += "- Execution Type: #{activity.execution_type}\n" if activity.execution_type
|
177
|
+
formatted += "- Quantity: #{activity.quantity}\n" if activity.quantity
|
178
|
+
formatted += "- Order Remaining Quantity: #{activity.order_remaining_quantity}\n" if activity.order_remaining_quantity
|
236
179
|
|
237
|
-
if activity
|
238
|
-
activity
|
180
|
+
if activity.execution_legs && activity.execution_legs.any?
|
181
|
+
activity.execution_legs.each_with_index do |exec_leg, leg_index|
|
239
182
|
formatted += "- **Execution Leg #{leg_index + 1}:**\n"
|
240
|
-
formatted += " * Leg ID: #{exec_leg
|
241
|
-
formatted += " * Price: $#{format_currency(exec_leg
|
242
|
-
formatted += " * Quantity: #{exec_leg
|
243
|
-
formatted += " * Mismarked Quantity: #{exec_leg
|
244
|
-
formatted += " * Time: #{exec_leg
|
183
|
+
formatted += " * Leg ID: #{exec_leg.leg_id}\n" if exec_leg.leg_id
|
184
|
+
formatted += " * Price: $#{format_currency(exec_leg.price)}\n" if exec_leg.price
|
185
|
+
formatted += " * Quantity: #{exec_leg.quantity}\n" if exec_leg.quantity
|
186
|
+
formatted += " * Mismarked Quantity: #{exec_leg.mismarked_quantity}\n" if exec_leg.mismarked_quantity
|
187
|
+
formatted += " * Time: #{exec_leg.time}\n" if exec_leg.time
|
245
188
|
end
|
246
189
|
end
|
247
|
-
formatted += "\n" unless index ==
|
190
|
+
formatted += "\n" unless index == order.order_activity_collection.length - 1
|
248
191
|
end
|
249
192
|
end
|
250
193
|
|
251
|
-
redacted_data = Redactor.redact(
|
194
|
+
redacted_data = Redactor.redact(order.to_h)
|
252
195
|
formatted += "\n**Full Response (Redacted):**\n"
|
253
196
|
formatted += "```json\n#{JSON.pretty_generate(redacted_data)}\n```"
|
254
197
|
formatted
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require "mcp"
|
2
2
|
require "schwab_rb"
|
3
|
-
require "json"
|
4
3
|
require "date"
|
5
4
|
require_relative "../loggable"
|
5
|
+
require_relative "../schwab_client_factory"
|
6
6
|
|
7
7
|
module SchwabMCP
|
8
8
|
module Tools
|
@@ -68,20 +68,8 @@ module SchwabMCP
|
|
68
68
|
log_info("Getting price history for symbol: #{symbol}")
|
69
69
|
|
70
70
|
begin
|
71
|
-
client =
|
72
|
-
|
73
|
-
ENV['SCHWAB_APP_SECRET'],
|
74
|
-
ENV['SCHWAB_CALLBACK_URI'],
|
75
|
-
ENV['TOKEN_PATH']
|
76
|
-
)
|
77
|
-
|
78
|
-
unless client
|
79
|
-
log_error("Failed to initialize Schwab client")
|
80
|
-
return MCP::Tool::Response.new([{
|
81
|
-
type: "text",
|
82
|
-
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
83
|
-
}])
|
84
|
-
end
|
71
|
+
client = SchwabClientFactory.create_client
|
72
|
+
return SchwabClientFactory.client_error_response unless client
|
85
73
|
|
86
74
|
parsed_start = nil
|
87
75
|
parsed_end = nil
|
@@ -146,7 +134,7 @@ module SchwabMCP
|
|
146
134
|
|
147
135
|
log_debug("Making price history API request for symbol: #{symbol}")
|
148
136
|
|
149
|
-
|
137
|
+
price_history = client.get_price_history(
|
150
138
|
symbol.upcase,
|
151
139
|
period_type: period_type_enum,
|
152
140
|
period: period,
|
@@ -158,29 +146,29 @@ module SchwabMCP
|
|
158
146
|
need_previous_close: need_previous_close
|
159
147
|
)
|
160
148
|
|
161
|
-
if
|
149
|
+
if price_history
|
162
150
|
log_info("Successfully retrieved price history for #{symbol}")
|
163
151
|
|
164
|
-
|
165
|
-
data
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
"No price data available for the specified parameters"
|
172
|
-
end
|
152
|
+
summary = if price_history.empty?
|
153
|
+
"No price data available for the specified parameters"
|
154
|
+
else
|
155
|
+
"Retrieved #{price_history.count} price candles\n" \
|
156
|
+
"First candle: #{price_history.first_candle&.to_h}\n" \
|
157
|
+
"Last candle: #{price_history.last_candle&.to_h}"
|
158
|
+
end
|
173
159
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
rescue
|
179
|
-
|
180
|
-
type: "text",
|
181
|
-
text: "**Price History for #{symbol.upcase}:**\n\n```json\n#{response.body}\n```"
|
182
|
-
}])
|
160
|
+
# Show a compact JSON representation for advanced users
|
161
|
+
json_preview = begin
|
162
|
+
require "json"
|
163
|
+
JSON.pretty_generate(price_history.to_h)
|
164
|
+
rescue
|
165
|
+
price_history.to_h.inspect
|
183
166
|
end
|
167
|
+
|
168
|
+
MCP::Tool::Response.new([{
|
169
|
+
type: "text",
|
170
|
+
text: "**Price History for #{symbol.upcase}:**\n\n#{summary}\n\n```json\n#{json_preview}\n```"
|
171
|
+
}])
|
184
172
|
else
|
185
173
|
log_warn("Empty response from Schwab API for symbol: #{symbol}")
|
186
174
|
MCP::Tool::Response.new([{
|
@@ -56,8 +56,7 @@ module SchwabMCP
|
|
56
56
|
- **get_market_hours_tool**: Get market hours for specified markets
|
57
57
|
- **list_movers_tool**: Get top ten movers for a given index
|
58
58
|
|
59
|
-
### Option
|
60
|
-
- **option_strategy_finder_tool**: Find option strategies (iron condor, call spread, put spread)
|
59
|
+
### Option Order Tools:
|
61
60
|
- **preview_order_tool**: Preview an options order before placing (⚠️ SAFE PREVIEW)
|
62
61
|
- **place_order_tool**: Place an options order for execution (⚠️ DESTRUCTIVE)
|
63
62
|
- **replace_order_tool**: Replace an existing order with a new one (⚠️ DESTRUCTIVE)
|
@@ -84,7 +83,6 @@ module SchwabMCP
|
|
84
83
|
|
85
84
|
# Options
|
86
85
|
option_chain_tool(symbol: "SPX", contract_type: "ALL")
|
87
|
-
option_strategy_finder_tool(strategy_type: "ironcondor", underlying_symbol: "SPX", expiration_date: "2025-01-17")
|
88
86
|
preview_order_tool(account_name: "TRADING_BROKERAGE_ACCOUNT", strategy_type: "ironcondor", price: 1.50, quantity: 1)
|
89
87
|
|
90
88
|
# Account Management
|
@@ -197,25 +195,6 @@ module SchwabMCP
|
|
197
195
|
|
198
196
|
**Example**: `option_chain_tool(symbol: "SPX", contract_type: "ALL")`
|
199
197
|
|
200
|
-
### option_strategy_finder_tool
|
201
|
-
Find option strategies using sophisticated algorithms.
|
202
|
-
**Parameters**:
|
203
|
-
- `strategy_type` (required) - "ironcondor", "callspread", or "putspread"
|
204
|
-
- `underlying_symbol` (required) - e.g., "SPX", "$SPX"
|
205
|
-
- `expiration_date` (required) - Target expiration "YYYY-MM-DD"
|
206
|
-
- `max_delta` (optional) - Maximum delta for short legs (default: 0.15)
|
207
|
-
- `max_spread` (optional) - Maximum spread width (default: 20.0)
|
208
|
-
- `min_credit` (optional) - Minimum credit in dollars (default: 100.0)
|
209
|
-
- `min_open_interest` (optional) - Minimum open interest (default: 0)
|
210
|
-
- `dist_from_strike` (optional) - Min distance from current price (default: 0.07)
|
211
|
-
- `expiration_type`, `settlement_type`, `option_root` (optional) - Filters
|
212
|
-
|
213
|
-
**Examples**:
|
214
|
-
```
|
215
|
-
option_strategy_finder_tool(strategy_type: "ironcondor", underlying_symbol: "SPX", expiration_date: "2025-01-17")
|
216
|
-
option_strategy_finder_tool(strategy_type: "callspread", underlying_symbol: "SPY", expiration_date: "2025-01-10", max_delta: 0.20, min_credit: 50.0)
|
217
|
-
```
|
218
|
-
|
219
198
|
## Order Management Tools
|
220
199
|
|
221
200
|
### preview_order_tool ⚠️ SAFE PREVIEW
|