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
@@ -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 = SchwabRb::Auth.init_client_easy(
56
- ENV['SCHWAB_API_KEY'],
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
- account_id = ENV[account_name]
71
- unless account_id
72
- available_accounts = ENV.keys.select { |key| key.end_with?('_ACCOUNT') }
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 environment variables.\n\nAvailable accounts: #{available_accounts.join(', ')}\n\nTo configure: Set ENV['#{account_name}'] to your account ID."
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("Found account ID: [REDACTED] for account name: #{account_name}")
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
- order_response = client.get_order(order_id, account_hash)
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
- order_data = JSON.parse(order_response.body)
126
- order_status = order_data['status']
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, account_hash)
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, order_data)
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, order_data)
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
- rescue JSON::ParserError => e
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, order_data)
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: #{order_data['status']}\n" if order_data['status']
197
- formatted += "- Order Type: #{order_data['orderType']}\n" if order_data['orderType']
198
- formatted += "- Session: #{order_data['session']}\n" if order_data['session']
199
- formatted += "- Duration: #{order_data['duration']}\n" if order_data['duration']
200
- formatted += "- Quantity: #{order_data['quantity']}\n" if order_data['quantity']
201
- formatted += "- Price: $#{format_currency(order_data['price'])}\n" if order_data['price']
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
- order_data['orderLegCollection'].each do |leg|
206
- if leg['instrument']
207
- symbol = leg['instrument']['symbol']
208
- instruction = leg['instruction']
209
- quantity = leg['quantity']
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 = SchwabRb::Auth.init_client_easy(
46
- ENV['SCHWAB_API_KEY'],
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
- response = client.get_market_hours(markets, date: parsed_date)
65
+ market_hours_obj = client.get_market_hours(markets, date: parsed_date, return_data_objects: true)
77
66
 
78
- if response&.body
79
- log_info("Successfully retrieved market hours for #{markets.join(', ')}")
80
- date_info = date ? " for #{date}" : " for today"
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**: Empty response from Schwab API for markets: #{markets.join(', ')}"
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 = SchwabRb::Auth.init_client_easy(
57
- ENV['SCHWAB_API_KEY'],
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
- unless client
64
- log_error("Failed to initialize Schwab client")
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**: Failed to initialize Schwab client. Check your credentials."
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
- account_id = ENV[account_name]
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
- order_response = client.get_order(order_id, account_hash)
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
- order_data = JSON.parse(order_response.body)
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: #{order_data['orderId']}\n" if order_data['orderId']
162
- formatted += "- Status: #{order_data['status']}\n" if order_data['status']
163
- formatted += "- Order Type: #{order_data['orderType']}\n" if order_data['orderType']
164
- formatted += "- Session: #{order_data['session']}\n" if order_data['session']
165
- formatted += "- Duration: #{order_data['duration']}\n" if order_data['duration']
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: #{order_data['enteredTime']}\n" if order_data['enteredTime']
172
- formatted += "- Close Time: #{order_data['closeTime']}\n" if order_data['closeTime']
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: #{order_data['quantity']}\n" if order_data['quantity']
176
- formatted += "- Filled Quantity: #{order_data['filledQuantity']}\n" if order_data['filledQuantity']
177
- formatted += "- Remaining Quantity: #{order_data['remainingQuantity']}\n" if order_data['remainingQuantity']
178
- formatted += "- Requested Destination: #{order_data['requestedDestination']}\n" if order_data['requestedDestination']
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 order_data['orderLegCollection'] && order_data['orderLegCollection'].any?
122
+ if order.order_leg_collection && order.order_leg_collection.any?
188
123
  formatted += "\n**Order Legs:**\n"
189
- order_data['orderLegCollection'].each_with_index do |leg, index|
124
+ order.order_leg_collection.each_with_index do |leg, index|
190
125
  formatted += "**Leg #{index + 1}:**\n"
191
- formatted += "- Instruction: #{leg['instruction']}\n" if leg['instruction']
192
- formatted += "- Quantity: #{leg['quantity']}\n" if leg['quantity']
193
- formatted += "- Position Effect: #{leg['positionEffect']}\n" if leg['positionEffect']
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['instrument']
197
- instrument = leg['instrument']
130
+ if leg.instrument
131
+ instrument = leg.instrument
198
132
  formatted += "- **Instrument:**\n"
199
- formatted += " * Asset Type: #{instrument['assetType']}\n" if instrument['assetType']
200
- formatted += " * Symbol: #{instrument['symbol']}\n" if instrument['symbol']
201
- formatted += " * Description: #{instrument['description']}\n" if instrument['description']
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 == order_data['orderLegCollection'].length - 1
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 order_data['orderActivityCollection'] && order_data['orderActivityCollection'].any?
141
+ if order.order_activity_collection && order.order_activity_collection.any?
229
142
  formatted += "\n**Order Activities:**\n"
230
- order_data['orderActivityCollection'].each_with_index do |activity, index|
143
+ order.order_activity_collection.each_with_index do |activity, index|
231
144
  formatted += "**Activity #{index + 1}:**\n"
232
- formatted += "- Activity Type: #{activity['activityType']}\n" if activity['activityType']
233
- formatted += "- Execution Type: #{activity['executionType']}\n" if activity['executionType']
234
- formatted += "- Quantity: #{activity['quantity']}\n" if activity['quantity']
235
- formatted += "- Order Remaining Quantity: #{activity['orderRemainingQuantity']}\n" if activity['orderRemainingQuantity']
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['executionLegs'] && activity['executionLegs'].any?
238
- activity['executionLegs'].each_with_index do |exec_leg, leg_index|
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['legId']}\n" if exec_leg['legId']
241
- formatted += " * Price: $#{format_currency(exec_leg['price'])}\n" if exec_leg['price']
242
- formatted += " * Quantity: #{exec_leg['quantity']}\n" if exec_leg['quantity']
243
- formatted += " * Mismarked Quantity: #{exec_leg['mismarkedQuantity']}\n" if exec_leg['mismarkedQuantity']
244
- formatted += " * Time: #{exec_leg['time']}\n" if exec_leg['time']
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 == order_data['orderActivityCollection'].length - 1
160
+ formatted += "\n" unless index == order.order_activity_collection.length - 1
248
161
  end
249
162
  end
250
163
 
251
- redacted_data = Redactor.redact(order_data)
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