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
@@ -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 (
|
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 =
|
51
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
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("
|
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
|
-
|
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(
|
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(
|
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
|
-
|
156
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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(
|
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
|
data/lib/schwab_mcp/version.rb
CHANGED
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
|
-
|
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("
|
68
|
-
log_info("
|
69
|
-
log_info("
|
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
|
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.
|
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-
|
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
|