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
@@ -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
@@ -19,7 +20,7 @@ module SchwabMCP
19
20
  },
20
21
  fields: {
21
22
  type: "array",
22
- description: "Optional account fields to retrieve (balances, positions, orders)",
23
+ description: "Optional account fields to retrieve (positions)",
23
24
  items: {
24
25
  type: "string"
25
26
  }
@@ -47,74 +48,27 @@ module SchwabMCP
47
48
  end
48
49
 
49
50
  begin
50
- client = SchwabRb::Auth.init_client_easy(
51
- ENV['SCHWAB_API_KEY'],
52
- ENV['SCHWAB_APP_SECRET'],
53
- ENV['SCHWAB_CALLBACK_URI'],
54
- ENV['TOKEN_PATH']
55
- )
56
-
57
- unless client
58
- log_error("Failed to initialize Schwab client")
59
- return MCP::Tool::Response.new([{
60
- type: "text",
61
- text: "**Error**: Failed to initialize Schwab client. Check your credentials."
62
- }])
63
- end
64
-
65
- account_id = ENV[account_name]
66
- unless account_id
67
- available_accounts = ENV.keys.select { |key| key.end_with?('_ACCOUNT') }
68
- log_error("Account name '#{account_name}' not found in environment variables")
69
- return MCP::Tool::Response.new([{
70
- type: "text",
71
- 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."
72
- }])
73
- end
74
-
75
- log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
76
- log_debug("Fetching account numbers mapping")
77
-
78
- account_numbers_response = client.get_account_numbers
79
-
80
- unless account_numbers_response&.body
81
- log_error("Failed to retrieve account numbers")
82
- return MCP::Tool::Response.new([{
83
- type: "text",
84
- text: "**Error**: Failed to retrieve account numbers from Schwab API"
85
- }])
86
- end
87
-
88
- account_mappings = JSON.parse(account_numbers_response.body, symbolize_names: true)
89
- log_debug("Account mappings retrieved (#{account_mappings.length} accounts found)")
90
-
91
- account_hash = nil
92
- account_mappings.each do |mapping|
93
- if mapping[:accountNumber] == account_id
94
- account_hash = mapping[:hashValue]
95
- break
96
- end
97
- end
51
+ client = SchwabClientFactory.create_client
52
+ return SchwabClientFactory.client_error_response unless client
98
53
 
99
- unless account_hash
100
- log_error("Account ID not found in available accounts")
101
- available_accounts = account_mappings.map { |m| "[REDACTED]" }.join(", ")
54
+ available_accounts = client.available_account_names
55
+ unless available_accounts.include?(account_name)
56
+ log_error("Account name '#{account_name}' not found in configured accounts")
102
57
  return MCP::Tool::Response.new([{
103
58
  type: "text",
104
- text: "**Error**: Account ID not found in available accounts. #{account_mappings.length} accounts available."
59
+ 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."
105
60
  }])
106
61
  end
107
62
 
108
- log_debug("Found account hash for account ID: #{account_name}")
109
-
63
+ log_debug("Using account name: #{account_name}")
110
64
  log_debug("Fetching account information with fields: #{fields}")
111
- account_response = client.get_account(account_hash, fields: fields)
112
65
 
113
- if account_response&.body
66
+ account = client.get_account(account_name: account_name, fields: fields)
67
+
68
+ if account
114
69
  log_info("Successfully retrieved account information for #{account_name}")
115
- account_data = JSON.parse(account_response.body)
116
70
 
117
- formatted_response = format_account_data(account_data, account_name, account_id)
71
+ formatted_response = format_account_data(account, account_name)
118
72
 
119
73
  MCP::Tool::Response.new([{
120
74
  type: "text",
@@ -146,54 +100,58 @@ module SchwabMCP
146
100
 
147
101
  private
148
102
 
149
- def self.format_account_data(account_data, account_name, account_id)
150
- account = account_data["securitiesAccount"]
103
+ def self.format_account_data(account, account_name)
151
104
  friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
152
105
 
153
106
  formatted = "**Account Information for #{friendly_name} (#{account_name}):**\n\n"
154
107
 
155
- if account
156
- formatted += "**Account Number:** #{Redactor::REDACTED_ACCOUNT_PLACEHOLDER}\n"
157
- formatted += "**Account Type:** #{account['type']}\n"
158
-
159
- if current_balances = account['currentBalances']
160
- formatted += "\n**Current Balances:**\n"
161
- formatted += "- Cash Balance: $#{format_currency(current_balances['cashBalance'])}\n"
162
- formatted += "- Buying Power: $#{format_currency(current_balances['buyingPower'])}\n"
163
- formatted += "- Total Cash: $#{format_currency(current_balances['totalCash'])}\n"
164
- formatted += "- Liquidation Value: $#{format_currency(current_balances['liquidationValue'])}\n"
165
- formatted += "- Long Market Value: $#{format_currency(current_balances['longMarketValue'])}\n"
166
- formatted += "- Short Market Value: $#{format_currency(current_balances['shortMarketValue'])}\n"
167
- end
108
+ formatted += "**Account Number:** #{Redactor::REDACTED_ACCOUNT_PLACEHOLDER}\n"
109
+ formatted += "**Account Type:** #{account.type}\n"
168
110
 
169
- # Positions summary
170
- if positions = account['positions']
171
- formatted += "\n**Positions Summary:**\n"
172
- formatted += "- Total Positions: #{positions.length}\n"
173
-
174
- if positions.length > 0
175
- formatted += "\n**Position Details:**\n"
176
- positions.each do |position|
177
- symbol = position.dig('instrument', 'symbol')
178
- qty = position['longQuantity'].to_f - position['shortQuantity'].to_f
179
- market_value = position['marketValue']
180
- formatted += "- #{symbol}: #{qty} shares, Market Value: $#{format_currency(market_value)}\n"
181
- end
111
+ if current_balances = account.current_balances
112
+ formatted += "\n**Current Balances:**\n"
113
+ formatted += "- Cash Balance: $#{format_currency(current_balances.cash_balance)}\n"
114
+ formatted += "- Buying Power: $#{format_currency(current_balances.buying_power)}\n"
115
+ formatted += "- Liquidation Value: $#{format_currency(current_balances.liquidation_value)}\n"
116
+ formatted += "- Long Market Value: $#{format_currency(current_balances.long_market_value)}\n"
117
+ formatted += "- Short Market Value: $#{format_currency(current_balances.short_market_value)}\n"
118
+ end
119
+
120
+ # Positions summary
121
+ if positions = account.positions
122
+ formatted += "\n**Positions Summary:**\n"
123
+ formatted += "- Total Positions: #{positions.length}\n"
124
+
125
+ if positions.length > 0
126
+ formatted += "\n**Position Details:**\n"
127
+ positions.each do |position|
128
+ symbol = position.instrument.symbol
129
+ qty = position.long_quantity - position.short_quantity
130
+ market_value = position.market_value
131
+ formatted += "- #{symbol}: #{qty} shares, Market Value: $#{format_currency(market_value)}\n"
182
132
  end
183
133
  end
134
+ end
184
135
 
185
- if orders = account['orderStrategies']
186
- formatted += "\n**Active Orders:**\n"
187
- formatted += "- Total Orders: #{orders.length}\n"
188
-
189
- orders.each do |order|
190
- status = order['status']
191
- symbol = order.dig('orderLegCollection', 0, 'instrument', 'symbol')
192
- formatted += "- #{symbol}: #{status}\n"
193
- end
194
- end end
136
+ # Note: Orders would need to be fetched separately as they're not part of the Account data object
137
+
138
+ # Convert account back to hash for JSON display (redacted)
139
+ account_hash = {
140
+ securitiesAccount: {
141
+ type: account.type,
142
+ accountNumber: account.account_number,
143
+ positions: account.positions.map(&:to_h),
144
+ currentBalances: {
145
+ cashBalance: account.current_balances&.cash_balance,
146
+ buyingPower: account.current_balances&.buying_power,
147
+ liquidationValue: account.current_balances&.liquidation_value,
148
+ longMarketValue: account.current_balances&.long_market_value,
149
+ shortMarketValue: account.current_balances&.short_market_value
150
+ }
151
+ }
152
+ }
195
153
 
196
- redacted_data = Redactor.redact(account_data)
154
+ redacted_data = Redactor.redact(account_hash)
197
155
  formatted += "\n```json\n#{JSON.pretty_generate(redacted_data)}\n```"
198
156
  formatted
199
157
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwabMCP
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/schwab_mcp.rb CHANGED
@@ -9,10 +9,8 @@ require_relative "schwab_mcp/redactor"
9
9
  require_relative "schwab_mcp/tools/quote_tool"
10
10
  require_relative "schwab_mcp/tools/quotes_tool"
11
11
  require_relative "schwab_mcp/tools/option_chain_tool"
12
- require_relative "schwab_mcp/tools/option_strategy_finder_tool"
13
12
  require_relative "schwab_mcp/tools/help_tool"
14
13
  require_relative "schwab_mcp/tools/schwab_account_details_tool"
15
- require_relative "schwab_mcp/tools/list_schwab_accounts_tool"
16
14
  require_relative "schwab_mcp/tools/list_account_orders_tool"
17
15
  require_relative "schwab_mcp/tools/list_account_transactions_tool"
18
16
  require_relative "schwab_mcp/tools/get_order_tool"
@@ -23,7 +21,9 @@ require_relative "schwab_mcp/tools/replace_order_tool"
23
21
  require_relative "schwab_mcp/tools/list_movers_tool"
24
22
  require_relative "schwab_mcp/tools/get_market_hours_tool"
25
23
  require_relative "schwab_mcp/tools/get_price_history_tool"
24
+ require_relative "schwab_mcp/tools/get_account_names_tool"
26
25
  require_relative "schwab_mcp/loggable"
26
+ require_relative "schwab_mcp/schwab_client_factory"
27
27
 
28
28
 
29
29
  module SchwabMCP
@@ -33,10 +33,8 @@ module SchwabMCP
33
33
  Tools::QuoteTool,
34
34
  Tools::QuotesTool,
35
35
  Tools::OptionChainTool,
36
- Tools::OptionStrategyFinderTool,
37
36
  Tools::HelpTool,
38
37
  Tools::SchwabAccountDetailsTool,
39
- Tools::ListSchwabAccountsTool,
40
38
  Tools::ListAccountOrdersTool,
41
39
  Tools::ListAccountTransactionsTool,
42
40
  Tools::GetOrderTool,
@@ -46,35 +44,38 @@ module SchwabMCP
46
44
  Tools::ReplaceOrderTool,
47
45
  Tools::ListMoversTool,
48
46
  Tools::GetMarketHoursTool,
49
- Tools::GetPriceHistoryTool
47
+ Tools::GetPriceHistoryTool,
48
+ Tools::GetAccountNamesTool
50
49
  ].freeze
51
50
 
52
51
  class Server
53
52
  include Loggable
54
53
 
55
54
  def initialize
56
- configure_schwab_rb_logging
55
+ configure_schwab_rb
57
56
 
58
57
  @server = MCP::Server.new(
59
58
  name: "schwab_mcp_server",
60
59
  version: SchwabMCP::VERSION,
61
60
  tools: TOOLS,
61
+ server_context: { user: "foobar" }
62
62
  )
63
63
  end
64
64
 
65
65
  def start
66
66
  configure_mcp
67
- log_info("🚀 Starting Schwab MCP Server #{SchwabMCP::VERSION}")
68
- log_info("📊 Available tools: #{TOOLS.map { |tool| tool.name.split('::').last }.join(', ')}")
69
- log_info("📝 Logs will be written to: #{log_file_path}")
67
+ log_info("Starting Schwab MCP Server #{SchwabMCP::VERSION}")
68
+ log_info("Available tools: #{TOOLS.map { |tool| tool.name.split('::').last }.join(', ')}")
69
+ log_info("Logs will be written to: #{log_file_path}")
70
70
  transport = MCP::Transports::StdioTransport.new(@server)
71
71
  transport.open
72
72
  end
73
73
 
74
74
  private
75
75
 
76
- def configure_schwab_rb_logging
76
+ def configure_schwab_rb
77
77
  SchwabRb.configure do |config|
78
+ config.schwab_home = ENV['SCHWAB_HOME']
78
79
  config.logger = SchwabMCP::Logger.instance
79
80
  config.log_level = ENV.fetch('LOG_LEVEL', 'INFO').upcase
80
81
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schwab_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joseph Platta
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-07-17 00:00:00.000000000 Z
10
+ date: 2025-10-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: mcp
@@ -48,27 +48,30 @@ executables:
48
48
  extensions: []
49
49
  extra_rdoc_files: []
50
50
  files:
51
+ - ".claude/settings.json"
51
52
  - ".copilotignore"
52
53
  - ".rspec"
53
54
  - ".rubocop.yml"
54
55
  - CHANGELOG.md
56
+ - CLAUDE.md
55
57
  - CODE_OF_CONDUCT.md
56
58
  - LICENSE.txt
57
59
  - README.md
58
60
  - Rakefile
61
+ - debug_env.rb
62
+ - doc/DATA_OBJECTS_MIGRATION_TODO.md
63
+ - doc/SCHWAB_CLIENT_FACTORY_REFACTOR_PLAN.md
59
64
  - exe/schwab_mcp
60
65
  - exe/schwab_token_refresh
61
66
  - exe/schwab_token_reset
62
67
  - lib/schwab_mcp.rb
63
68
  - lib/schwab_mcp/loggable.rb
64
69
  - lib/schwab_mcp/logger.rb
65
- - lib/schwab_mcp/option_chain_filter.rb
66
- - lib/schwab_mcp/orders/iron_condor_order.rb
67
- - lib/schwab_mcp/orders/order_factory.rb
68
- - lib/schwab_mcp/orders/vertical_order.rb
69
70
  - lib/schwab_mcp/redactor.rb
70
71
  - lib/schwab_mcp/resources/.keep
72
+ - lib/schwab_mcp/schwab_client_factory.rb
71
73
  - lib/schwab_mcp/tools/cancel_order_tool.rb
74
+ - lib/schwab_mcp/tools/get_account_names_tool.rb
72
75
  - lib/schwab_mcp/tools/get_market_hours_tool.rb
73
76
  - lib/schwab_mcp/tools/get_order_tool.rb
74
77
  - lib/schwab_mcp/tools/get_price_history_tool.rb
@@ -76,9 +79,7 @@ files:
76
79
  - lib/schwab_mcp/tools/list_account_orders_tool.rb
77
80
  - lib/schwab_mcp/tools/list_account_transactions_tool.rb
78
81
  - lib/schwab_mcp/tools/list_movers_tool.rb
79
- - lib/schwab_mcp/tools/list_schwab_accounts_tool.rb
80
82
  - lib/schwab_mcp/tools/option_chain_tool.rb
81
- - lib/schwab_mcp/tools/option_strategy_finder_tool.rb
82
83
  - lib/schwab_mcp/tools/place_order_tool.rb
83
84
  - lib/schwab_mcp/tools/preview_order_tool.rb
84
85
  - lib/schwab_mcp/tools/quote_tool.rb
@@ -87,7 +88,6 @@ files:
87
88
  - lib/schwab_mcp/tools/schwab_account_details_tool.rb
88
89
  - lib/schwab_mcp/version.rb
89
90
  - sig/schwab_mcp.rbs
90
- - start_mcp_server.sh
91
91
  homepage: https://github.com/jwplatta/schwab_mcp
92
92
  licenses:
93
93
  - MIT
@@ -95,6 +95,9 @@ metadata:
95
95
  homepage_uri: https://github.com/jwplatta/schwab_mcp
96
96
  source_code_uri: https://github.com/jwplatta/schwab_mcp
97
97
  changelog_uri: https://github.com/jwplatta/schwab_mcp/blob/main/CHANGELOG.md
98
+ rubygems_uri: https://rubygems.org/gems/schwab_mcp
99
+ documentation_uri: https://rubydoc.info/gems/schwab_mcp
100
+ bug_tracker_uri: https://github.com/jwplatta/schwab_mcp/issues
98
101
  rdoc_options: []
99
102
  require_paths:
100
103
  - lib
@@ -1,213 +0,0 @@
1
- # frozen_string_literal: true
2
- require_relative "loggable"
3
-
4
- module SchwabMCP
5
- class OptionChainFilter
6
- include Loggable
7
-
8
- attr_reader :expiration_date, :underlying_price, :expiration_type,
9
- :settlement_type, :option_root, :max_delta, :max_spread,
10
- :min_credit, :min_open_interest, :dist_from_strike, :quantity,
11
- :min_delta, :min_strike, :max_strike
12
-
13
- def initialize(
14
- expiration_date:,
15
- underlying_price: nil,
16
- expiration_type: nil,
17
- settlement_type: nil,
18
- option_root: nil,
19
- min_delta: 0.0,
20
- max_delta: 0.15,
21
- max_spread: 20.0,
22
- min_credit: 0.0,
23
- min_open_interest: 0,
24
- dist_from_strike: 0.0,
25
- quantity: 1,
26
- max_strike: nil,
27
- min_strike: nil
28
- )
29
- @expiration_date = expiration_date
30
- @underlying_price = underlying_price
31
- @expiration_type = expiration_type
32
- @settlement_type = settlement_type
33
- @option_root = option_root
34
- @max_spread = max_spread
35
- @min_credit = min_credit
36
- @min_open_interest = min_open_interest
37
- @dist_from_strike = dist_from_strike
38
- @quantity = quantity
39
- @max_delta = max_delta
40
- @min_delta = min_delta
41
- @max_strike = max_strike
42
- @min_strike = min_strike
43
- end
44
-
45
- def select(options_map)
46
- filtered_options = []
47
- exp_date_str = expiration_date.strftime('%Y-%m-%d')
48
-
49
- options_map.each do |date_key, strikes|
50
- strikes.each do |strike_key, option_array|
51
- option_array.each do |option|
52
- next unless passes_delta_filter?(option)
53
- next unless passes_strike_range_filter?(option)
54
- filtered_options << option
55
- end
56
- end
57
- end
58
-
59
- log_debug("Found #{filtered_options.size} filtered options")
60
- filtered_options
61
- end
62
-
63
- def find_spreads(options_map, option_type)
64
- spreads = []
65
- exp_date_str = expiration_date.strftime('%Y-%m-%d')
66
-
67
- short_cnt = 0
68
-
69
- options_map.each do |date_key, strikes|
70
- next unless date_matches?(date_key, exp_date_str)
71
- log_debug("Processing options for date: #{date_key}. Searching #{strikes.size} strikes...")
72
-
73
- strikes.each do |strike_key, option_array|
74
- option_array.each do |short_option|
75
- next unless passes_short_option_filters?(short_option)
76
-
77
- log_debug("Found short option: #{short_option[:symbol]} at strike #{short_option[:strikePrice]}")
78
-
79
- short_cnt += 1
80
-
81
- short_strike = short_option[:strikePrice]
82
- short_mark = short_option[:mark]
83
-
84
- long_options = find_long_option_candidates(strikes, short_option, option_type)
85
-
86
- long_options.each do |long_option|
87
- spread = build_spread(short_option, long_option)
88
- spreads << spread if spread
89
- end
90
- end
91
- end
92
- end
93
-
94
- log_debug("Found #{spreads.size} #{option_type} spreads for #{short_cnt} short options")
95
-
96
- spreads
97
- end
98
-
99
- def passes_short_option_filters?(option)
100
- return false unless passes_delta_filter?(option)
101
- return false unless passes_open_interest_filter?(option)
102
- return false unless passes_distance_filter?(option)
103
- return false unless passes_optional_filters?(option)
104
-
105
- true
106
- end
107
-
108
- private
109
-
110
- def date_matches?(date_key, exp_date_str)
111
- date_key.to_s.include?(exp_date_str)
112
- end
113
-
114
- def passes_delta_filter?(option)
115
- delta = option[:delta]&.abs || 0.0
116
- delta <= max_delta && delta >= min_delta
117
- end
118
-
119
- def passes_open_interest_filter?(option)
120
- open_interest = option[:openInterest] || 0
121
- open_interest >= min_open_interest
122
- end
123
-
124
- def passes_distance_filter?(option)
125
- raise "Underlying price must be set for distance filter" unless underlying_price
126
-
127
- strike = option[:strikePrice]
128
- return false unless strike
129
-
130
- distance = ((underlying_price - strike) / underlying_price).abs
131
- distance >= dist_from_strike
132
- end
133
-
134
- def passes_optional_filters?(option)
135
- return false if expiration_type && option[:expirationType] != expiration_type
136
- return false if settlement_type && option[:settlementType] != settlement_type
137
- return false if option_root && option[:optionRoot] != option_root
138
-
139
- true
140
- end
141
-
142
- def passes_strike_range_filter?(option)
143
- strike = option[:strikePrice]
144
- return false unless strike
145
-
146
- return false if @min_strike && strike < @min_strike
147
- return false if @max_strike && strike > @max_strike
148
-
149
- true
150
- end
151
-
152
- def find_long_option_candidates(strikes, short_option, option_type)
153
- short_strike = short_option[:strikePrice]
154
- candidates = []
155
-
156
- strikes.each do |long_strike_key, long_option_array|
157
- long_option_array.each do |long_option|
158
- long_strike = long_option[:strikePrice]
159
- long_mark = long_option[:mark]
160
-
161
- next unless long_mark > 0
162
- next unless valid_spread_structure?(short_strike, long_strike, option_type)
163
- next unless passes_min_credit?(short_option, long_option)
164
- next unless passes_optional_filters?(long_option)
165
- next unless passes_open_interest_filter?(long_option)
166
-
167
- candidates << long_option
168
- end
169
- end
170
-
171
- candidates
172
- end
173
-
174
- def valid_spread_structure?(short_strike, long_strike, option_type)
175
- case option_type
176
- when 'call'
177
- long_strike > short_strike && (long_strike - short_strike) <= max_spread
178
-
179
- when 'put'
180
- long_strike < short_strike && (short_strike - long_strike) <= max_spread
181
- else
182
- false
183
- end
184
- end
185
-
186
- def passes_min_credit?(short_option, long_option)
187
- return true if min_credit <= 0
188
-
189
- short_mark = short_option[:mark]
190
- long_mark = long_option[:mark]
191
- credit = short_mark - long_mark
192
-
193
- credit * 100 >= min_credit
194
- end
195
-
196
- def build_spread(short_option, long_option)
197
- short_mark = short_option[:mark]
198
- long_mark = long_option[:mark]
199
- credit = short_mark - long_mark
200
- short_strike = short_option[:strikePrice]
201
- long_strike = long_option[:strikePrice]
202
-
203
- {
204
- short_option: short_option,
205
- long_option: long_option,
206
- credit: credit,
207
- delta: short_option[:delta] || 0,
208
- spread_width: (short_strike - long_strike).abs,
209
- quantity: quantity
210
- }
211
- end
212
- end
213
- end
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'schwab_rb'
4
-
5
- module SchwabMCP
6
- module Orders
7
- class IronCondorOrder
8
- class << self
9
- def build(
10
- account_number:,
11
- put_short_symbol:,
12
- put_long_symbol:,
13
- call_short_symbol:,
14
- call_long_symbol:,
15
- price:,
16
- order_instruction: :open,
17
- quantity: 1
18
- )
19
- schwab_order_builder.new.tap do |builder|
20
- builder.set_account_number(account_number)
21
- builder.set_order_strategy_type('SINGLE')
22
- builder.set_session(SchwabRb::Orders::Session::NORMAL)
23
- builder.set_duration(SchwabRb::Orders::Duration::DAY)
24
- builder.set_order_type(order_type(order_instruction))
25
- builder.set_complex_order_strategy_type(SchwabRb::Order::ComplexOrderStrategyTypes::IRON_CONDOR)
26
- builder.set_quantity(quantity)
27
- builder.set_price(price)
28
-
29
- instructions = leg_instructions_for_position(order_instruction)
30
-
31
- builder.add_option_leg(
32
- instructions[:put_short],
33
- put_short_symbol,
34
- quantity
35
- )
36
- builder.add_option_leg(
37
- instructions[:put_long],
38
- put_long_symbol,
39
- quantity
40
- )
41
- builder.add_option_leg(
42
- instructions[:call_short],
43
- call_short_symbol,
44
- quantity
45
- )
46
- builder.add_option_leg(
47
- instructions[:call_long],
48
- call_long_symbol,
49
- quantity
50
- )
51
- end
52
- end
53
-
54
- def order_type(order_instruction)
55
- if order_instruction == :open
56
- SchwabRb::Order::Types::NET_CREDIT
57
- else
58
- SchwabRb::Order::Types::NET_DEBIT
59
- end
60
- end
61
-
62
- def leg_instructions_for_position(order_instruction)
63
- case order_instruction
64
- when :open
65
- {
66
- put_short: SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN,
67
- put_long: SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN,
68
- call_short: SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN,
69
- call_long: SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN
70
- }
71
- when :exit
72
- {
73
- put_short: SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE,
74
- put_long: SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE,
75
- call_short: SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE,
76
- call_long: SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE
77
- }
78
- end
79
- end
80
-
81
- def schwab_order_builder
82
- SchwabRb::Orders::Builder
83
- end
84
- end
85
- end
86
- end
87
- end