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.
- checksums.yaml +4 -4
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +12 -0
- data/CLAUDE.md +124 -0
- data/README.md +1 -7
- 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 +15 -4
- data/exe/schwab_token_refresh +12 -11
- data/exe/schwab_token_reset +11 -10
- 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 -81
- data/lib/schwab_mcp/tools/get_account_names_tool.rb +58 -0
- data/lib/schwab_mcp/tools/get_market_hours_tool.rb +27 -28
- data/lib/schwab_mcp/tools/get_order_tool.rb +50 -137
- data/lib/schwab_mcp/tools/get_price_history_tool.rb +23 -35
- data/lib/schwab_mcp/tools/help_tool.rb +12 -33
- data/lib/schwab_mcp/tools/list_account_orders_tool.rb +36 -90
- data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +43 -98
- data/lib/schwab_mcp/tools/list_movers_tool.rb +21 -34
- data/lib/schwab_mcp/tools/option_chain_tool.rb +132 -84
- data/lib/schwab_mcp/tools/place_order_tool.rb +111 -141
- data/lib/schwab_mcp/tools/preview_order_tool.rb +71 -81
- data/lib/schwab_mcp/tools/quote_tool.rb +33 -28
- data/lib/schwab_mcp/tools/quotes_tool.rb +97 -45
- data/lib/schwab_mcp/tools/replace_order_tool.rb +110 -140
- data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +56 -98
- data/lib/schwab_mcp/version.rb +1 -1
- data/lib/schwab_mcp.rb +11 -10
- metadata +12 -9
- 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/list_schwab_accounts_tool.rb +0 -162
- data/lib/schwab_mcp/tools/option_strategy_finder_tool.rb +0 -378
- data/start_mcp_server.sh +0 -4
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "loggable"
|
4
|
+
|
5
|
+
module SchwabMCP
|
6
|
+
module SchwabClientFactory
|
7
|
+
extend Loggable
|
8
|
+
|
9
|
+
def self.create_client
|
10
|
+
begin
|
11
|
+
log_debug("Initializing Schwab client")
|
12
|
+
client = SchwabRb::Auth.init_client_easy(
|
13
|
+
ENV['SCHWAB_API_KEY'],
|
14
|
+
ENV['SCHWAB_APP_SECRET'],
|
15
|
+
ENV['SCHWAB_CALLBACK_URI'],
|
16
|
+
ENV['SCHWAB_TOKEN_PATH']
|
17
|
+
)
|
18
|
+
|
19
|
+
unless client
|
20
|
+
log_error("Failed to initialize Schwab client - check credentials")
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
|
24
|
+
log_debug("Schwab client initialized successfully")
|
25
|
+
client
|
26
|
+
rescue => e
|
27
|
+
log_error("Error initializing Schwab client: #{e.message}")
|
28
|
+
log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.cached_client
|
34
|
+
@client ||= create_client
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.client_error_response
|
38
|
+
MCP::Tool::Response.new([{
|
39
|
+
type: "text",
|
40
|
+
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
41
|
+
}])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -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,69 +53,23 @@ 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
|
-
|
71
|
-
unless
|
72
|
-
|
73
|
-
log_error("Account name '#{account_name}' not found in environment variables")
|
59
|
+
available_accounts = client.available_account_names
|
60
|
+
unless available_accounts.include?(account_name)
|
61
|
+
log_error("Account name '#{account_name}' not found in configured accounts")
|
74
62
|
return MCP::Tool::Response.new([{
|
75
63
|
type: "text",
|
76
|
-
text: "**Error**: Account name '#{account_name}' not found in
|
64
|
+
text: "**Error**: Account name '#{account_name}' not found in configured accounts.\n\nAvailable accounts: #{available_accounts.join(', ')}\n\nTo configure: Add the account to your schwab_rb configuration file."
|
77
65
|
}])
|
78
66
|
end
|
79
67
|
|
80
|
-
log_debug("
|
81
|
-
log_debug("Fetching account numbers mapping")
|
82
|
-
|
83
|
-
account_numbers_response = client.get_account_numbers
|
84
|
-
|
85
|
-
unless account_numbers_response&.body
|
86
|
-
log_error("Failed to retrieve account numbers")
|
87
|
-
return MCP::Tool::Response.new([{
|
88
|
-
type: "text",
|
89
|
-
text: "**Error**: Failed to retrieve account numbers from Schwab API"
|
90
|
-
}])
|
91
|
-
end
|
92
|
-
|
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
|
-
account_hash = nil
|
97
|
-
account_mappings.each do |mapping|
|
98
|
-
if mapping[:accountNumber] == account_id
|
99
|
-
account_hash = mapping[:hashValue]
|
100
|
-
break
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
unless account_hash
|
105
|
-
log_error("Account ID not found in available accounts")
|
106
|
-
return MCP::Tool::Response.new([{
|
107
|
-
type: "text",
|
108
|
-
text: "**Error**: Account ID not found in available accounts. #{account_mappings.length} accounts available."
|
109
|
-
}])
|
110
|
-
end
|
111
|
-
|
112
|
-
log_debug("Found account hash for account ID: #{account_name}")
|
68
|
+
log_debug("Using account name: #{account_name}")
|
113
69
|
log_debug("Verifying order exists before attempting cancellation")
|
114
70
|
|
115
|
-
|
116
|
-
|
117
|
-
unless order_response&.body
|
71
|
+
order = client.get_order(order_id, account_name: account_name) # returns SchwabRb::DataObjects::Order
|
72
|
+
unless order
|
118
73
|
log_warn("Order not found or empty response for order ID: #{order_id}")
|
119
74
|
return MCP::Tool::Response.new([{
|
120
75
|
type: "text",
|
@@ -122,9 +77,8 @@ module SchwabMCP
|
|
122
77
|
}])
|
123
78
|
end
|
124
79
|
|
125
|
-
|
126
|
-
|
127
|
-
cancelable = order_data['cancelable']
|
80
|
+
order_status = order.status
|
81
|
+
cancelable = order.respond_to?(:cancelable) ? order.cancelable : true # fallback if attribute not present
|
128
82
|
|
129
83
|
log_debug("Order found - Status: #{order_status}, Cancelable: #{cancelable}")
|
130
84
|
if cancelable == false
|
@@ -136,11 +90,11 @@ module SchwabMCP
|
|
136
90
|
end
|
137
91
|
|
138
92
|
log_info("Attempting to cancel order ID: #{order_id} (Status: #{order_status})")
|
139
|
-
cancel_response = client.cancel_order(order_id,
|
93
|
+
cancel_response = client.cancel_order(order_id, account_name: account_name)
|
140
94
|
|
141
95
|
if cancel_response.respond_to?(:status) && cancel_response.status == 200
|
142
96
|
log_info("Successfully cancelled order ID: #{order_id}")
|
143
|
-
formatted_response = format_cancellation_success(order_id, account_name,
|
97
|
+
formatted_response = format_cancellation_success(order_id, account_name, order)
|
144
98
|
elsif cancel_response.respond_to?(:status) && cancel_response.status == 404
|
145
99
|
log_warn("Order not found during cancellation: #{order_id}")
|
146
100
|
return MCP::Tool::Response.new([{
|
@@ -149,7 +103,7 @@ module SchwabMCP
|
|
149
103
|
}])
|
150
104
|
else
|
151
105
|
log_info("Order cancellation request submitted for order ID: #{order_id}")
|
152
|
-
formatted_response = format_cancellation_success(order_id, account_name,
|
106
|
+
formatted_response = format_cancellation_success(order_id, account_name, order)
|
153
107
|
end
|
154
108
|
|
155
109
|
MCP::Tool::Response.new([{
|
@@ -157,12 +111,7 @@ module SchwabMCP
|
|
157
111
|
text: formatted_response
|
158
112
|
}])
|
159
113
|
|
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
|
-
}])
|
114
|
+
# No JSON::ParserError rescue needed with data objects
|
166
115
|
rescue => e
|
167
116
|
log_error("Error cancelling order ID #{order_id}: #{e.message}")
|
168
117
|
log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
|
@@ -186,27 +135,26 @@ module SchwabMCP
|
|
186
135
|
|
187
136
|
private
|
188
137
|
|
189
|
-
def self.format_cancellation_success(order_id, account_name,
|
138
|
+
def self.format_cancellation_success(order_id, account_name, order)
|
190
139
|
friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
|
191
140
|
|
192
141
|
formatted = "**✅ Order Cancellation Successful**\n\n"
|
193
142
|
formatted += "**Order ID**: #{order_id}\n"
|
194
143
|
formatted += "**Account**: #{friendly_name} (#{account_name})\n\n"
|
195
144
|
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?
|
145
|
+
formatted += "- Original Status: #{order.status}\n" if order.status
|
146
|
+
formatted += "- Order Type: #{order.order_type}\n" if order.order_type
|
147
|
+
formatted += "- Duration: #{order.duration}\n" if order.duration
|
148
|
+
formatted += "- Quantity: #{order.quantity}\n" if order.quantity
|
149
|
+
formatted += "- Price: $#{format_currency(order.price)}\n" if order.price
|
150
|
+
|
151
|
+
if order.order_leg_collection && order.order_leg_collection.any?
|
204
152
|
formatted += "\n**Instruments:**\n"
|
205
|
-
|
206
|
-
if leg
|
207
|
-
symbol = leg
|
208
|
-
instruction = leg
|
209
|
-
quantity = leg
|
153
|
+
order.order_leg_collection.each do |leg|
|
154
|
+
if leg.instrument
|
155
|
+
symbol = leg.instrument.symbol
|
156
|
+
instruction = leg.instruction
|
157
|
+
quantity = leg.quantity
|
210
158
|
formatted += "- #{symbol}: #{instruction} #{quantity}\n"
|
211
159
|
end
|
212
160
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mcp"
|
4
|
+
require_relative "../loggable"
|
5
|
+
require "schwab_rb"
|
6
|
+
|
7
|
+
module SchwabMCP
|
8
|
+
module Tools
|
9
|
+
class GetAccountNamesTool < MCP::Tool
|
10
|
+
extend Loggable
|
11
|
+
description "Get a list of configured Schwab account names"
|
12
|
+
|
13
|
+
input_schema(
|
14
|
+
properties: {
|
15
|
+
topic: {
|
16
|
+
type: "string",
|
17
|
+
description: "Asking about a specific topic related to account names (optional)"
|
18
|
+
}
|
19
|
+
},
|
20
|
+
required: []
|
21
|
+
)
|
22
|
+
|
23
|
+
annotations(
|
24
|
+
title: "Get Account Names",
|
25
|
+
read_only_hint: true,
|
26
|
+
destructive_hint: false,
|
27
|
+
idempotent_hint: true
|
28
|
+
)
|
29
|
+
|
30
|
+
def self.call(topic: nil, server_context:)
|
31
|
+
account_names = SchwabRb::AccountHashManager.new.available_account_names
|
32
|
+
acct_names_content = if account_names && !account_names.empty?
|
33
|
+
formatted_names = account_names.map { |name| "- #{name}" }.join("\n")
|
34
|
+
"Configured Schwab Account Names:\n\n#{formatted_names}"
|
35
|
+
else
|
36
|
+
<<~NO_ACCOUNTS
|
37
|
+
No Schwab Account Names Configured
|
38
|
+
|
39
|
+
You need to configure your Schwab account names in the account_names.json file.
|
40
|
+
|
41
|
+
This file should be located in your schwab_home directory (typically ~/.schwab_rb/).
|
42
|
+
|
43
|
+
For detailed setup instructions, please refer to:
|
44
|
+
https://github.com/jwplatta/schwab_rb/blob/main/doc/ACCOUNT_MANAGEMENT.md
|
45
|
+
|
46
|
+
The account_names.json file should map friendly names to your Schwab account hashes.
|
47
|
+
NO_ACCOUNTS
|
48
|
+
end
|
49
|
+
|
50
|
+
MCP::Tool::Response.new([{
|
51
|
+
type: "text",
|
52
|
+
text: acct_names_content
|
53
|
+
}])
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
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,74 +54,25 @@ 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
|
-
)
|
57
|
+
client = SchwabClientFactory.create_client
|
58
|
+
return SchwabClientFactory.client_error_response unless client
|
62
59
|
|
63
|
-
|
64
|
-
|
60
|
+
available_accounts = client.available_account_names
|
61
|
+
unless available_accounts.include?(account_name)
|
62
|
+
log_error("Account name '#{account_name}' not found in configured accounts")
|
65
63
|
return MCP::Tool::Response.new([{
|
66
64
|
type: "text",
|
67
|
-
text: "**Error**:
|
65
|
+
text: "**Error**: Account name '#{account_name}' not found in configured accounts.\n\nAvailable accounts: #{available_accounts.join(', ')}\n\nTo configure: Add the account to your schwab_rb configuration file."
|
68
66
|
}])
|
69
67
|
end
|
70
68
|
|
71
|
-
|
72
|
-
unless account_id
|
73
|
-
available_accounts = ENV.keys.select { |key| key.end_with?('_ACCOUNT') }
|
74
|
-
log_error("Account name '#{account_name}' not found in environment variables")
|
75
|
-
return MCP::Tool::Response.new([{
|
76
|
-
type: "text",
|
77
|
-
text: "**Error**: Account name '#{account_name}' not found in environment variables.\n\nAvailable accounts: #{available_accounts.join(', ')}\n\nTo configure: Set ENV['#{account_name}'] to your account ID."
|
78
|
-
}])
|
79
|
-
end
|
80
|
-
|
81
|
-
log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
|
82
|
-
log_debug("Fetching account numbers mapping")
|
83
|
-
|
84
|
-
account_numbers_response = client.get_account_numbers
|
85
|
-
|
86
|
-
unless account_numbers_response&.body
|
87
|
-
log_error("Failed to retrieve account numbers")
|
88
|
-
return MCP::Tool::Response.new([{
|
89
|
-
type: "text",
|
90
|
-
text: "**Error**: Failed to retrieve account numbers from Schwab API"
|
91
|
-
}])
|
92
|
-
end
|
93
|
-
|
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
|
-
account_hash = nil
|
98
|
-
account_mappings.each do |mapping|
|
99
|
-
if mapping[:accountNumber] == account_id
|
100
|
-
account_hash = mapping[:hashValue]
|
101
|
-
break
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
unless account_hash
|
106
|
-
log_error("Account ID not found in available accounts")
|
107
|
-
return MCP::Tool::Response.new([{
|
108
|
-
type: "text",
|
109
|
-
text: "**Error**: Account ID not found in available accounts. #{account_mappings.length} accounts available."
|
110
|
-
}])
|
111
|
-
end
|
112
|
-
|
113
|
-
log_debug("Found account hash for account ID: #{account_name}")
|
69
|
+
log_debug("Using account name: #{account_name}")
|
114
70
|
log_debug("Fetching order details for order ID: #{order_id}")
|
115
71
|
|
116
|
-
|
117
|
-
|
118
|
-
if order_response&.body
|
72
|
+
order = client.get_order(order_id, account_name: account_name) # returns SchwabRb::DataObjects::Order
|
73
|
+
if order
|
119
74
|
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
|
-
|
75
|
+
formatted_response = format_order_object(order, order_id, account_name)
|
124
76
|
MCP::Tool::Response.new([{
|
125
77
|
type: "text",
|
126
78
|
text: formatted_response
|
@@ -132,13 +84,6 @@ module SchwabMCP
|
|
132
84
|
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
85
|
}])
|
134
86
|
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
87
|
rescue => e
|
143
88
|
log_error("Error retrieving order details for order ID #{order_id}: #{e.message}")
|
144
89
|
log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
|
@@ -151,104 +96,72 @@ module SchwabMCP
|
|
151
96
|
|
152
97
|
private
|
153
98
|
|
154
|
-
def self.format_order_data(order_data, order_id, account_name)
|
155
|
-
friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
|
156
99
|
|
100
|
+
def self.format_order_object(order, order_id, account_name)
|
101
|
+
friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
|
157
102
|
formatted = "**Order Details for Order ID #{order_id}:**\n\n"
|
158
103
|
formatted += "**Account:** #{friendly_name} (#{account_name})\n\n"
|
159
104
|
|
160
105
|
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')
|
106
|
+
formatted += "- Order ID: #{order.order_id}\n" if order.order_id
|
107
|
+
formatted += "- Status: #{order.status}\n" if order.status
|
108
|
+
formatted += "- Order Type: #{order.order_type}\n" if order.order_type
|
109
|
+
formatted += "- Duration: #{order.duration}\n" if order.duration
|
110
|
+
formatted += "- Complex Order Strategy Type: #{order.complex_order_strategy_type}\n" if order.complex_order_strategy_type
|
169
111
|
|
170
112
|
formatted += "\n**Timing:**\n"
|
171
|
-
formatted += "- Entered Time: #{
|
172
|
-
formatted += "- Close Time: #{
|
113
|
+
formatted += "- Entered Time: #{order.entered_time}\n" if order.entered_time
|
114
|
+
formatted += "- Close Time: #{order.close_time}\n" if order.close_time
|
173
115
|
|
174
116
|
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']
|
117
|
+
formatted += "- Quantity: #{order.quantity}\n" if order.quantity
|
118
|
+
formatted += "- Filled Quantity: #{order.filled_quantity}\n" if order.filled_quantity
|
119
|
+
formatted += "- Remaining Quantity: #{order.remaining_quantity}\n" if order.remaining_quantity
|
120
|
+
formatted += "- Price: $#{format_currency(order.price)}\n" if order.price
|
186
121
|
|
187
|
-
if
|
122
|
+
if order.order_leg_collection && order.order_leg_collection.any?
|
188
123
|
formatted += "\n**Order Legs:**\n"
|
189
|
-
|
124
|
+
order.order_leg_collection.each_with_index do |leg, index|
|
190
125
|
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']
|
126
|
+
formatted += "- Instruction: #{leg.instruction}\n" if leg.instruction
|
127
|
+
formatted += "- Quantity: #{leg.quantity}\n" if leg.quantity
|
128
|
+
formatted += "- Position Effect: #{leg.position_effect}\n" if leg.position_effect
|
195
129
|
|
196
|
-
if leg
|
197
|
-
instrument = leg
|
130
|
+
if leg.instrument
|
131
|
+
instrument = leg.instrument
|
198
132
|
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
|
133
|
+
formatted += " * Asset Type: #{instrument.asset_type}\n" if instrument.asset_type
|
134
|
+
formatted += " * Symbol: #{instrument.symbol}\n" if instrument.symbol
|
135
|
+
formatted += " * Description: #{instrument.description}\n" if instrument.description
|
215
136
|
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']
|
137
|
+
formatted += "\n" unless index == order.order_leg_collection.length - 1
|
225
138
|
end
|
226
139
|
end
|
227
140
|
|
228
|
-
if
|
141
|
+
if order.order_activity_collection && order.order_activity_collection.any?
|
229
142
|
formatted += "\n**Order Activities:**\n"
|
230
|
-
|
143
|
+
order.order_activity_collection.each_with_index do |activity, index|
|
231
144
|
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
|
145
|
+
formatted += "- Activity Type: #{activity.activity_type}\n" if activity.activity_type
|
146
|
+
formatted += "- Execution Type: #{activity.execution_type}\n" if activity.execution_type
|
147
|
+
formatted += "- Quantity: #{activity.quantity}\n" if activity.quantity
|
148
|
+
formatted += "- Order Remaining Quantity: #{activity.order_remaining_quantity}\n" if activity.order_remaining_quantity
|
236
149
|
|
237
|
-
if activity
|
238
|
-
activity
|
150
|
+
if activity.execution_legs && activity.execution_legs.any?
|
151
|
+
activity.execution_legs.each_with_index do |exec_leg, leg_index|
|
239
152
|
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
|
153
|
+
formatted += " * Leg ID: #{exec_leg.leg_id}\n" if exec_leg.leg_id
|
154
|
+
formatted += " * Price: $#{format_currency(exec_leg.price)}\n" if exec_leg.price
|
155
|
+
formatted += " * Quantity: #{exec_leg.quantity}\n" if exec_leg.quantity
|
156
|
+
formatted += " * Mismarked Quantity: #{exec_leg.mismarked_quantity}\n" if exec_leg.mismarked_quantity
|
157
|
+
formatted += " * Time: #{exec_leg.time}\n" if exec_leg.time
|
245
158
|
end
|
246
159
|
end
|
247
|
-
formatted += "\n" unless index ==
|
160
|
+
formatted += "\n" unless index == order.order_activity_collection.length - 1
|
248
161
|
end
|
249
162
|
end
|
250
163
|
|
251
|
-
redacted_data = Redactor.redact(
|
164
|
+
redacted_data = Redactor.redact(order.to_h)
|
252
165
|
formatted += "\n**Full Response (Redacted):**\n"
|
253
166
|
formatted += "```json\n#{JSON.pretty_generate(redacted_data)}\n```"
|
254
167
|
formatted
|