schwab_mcp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/settings.json +14 -0
- data/CLAUDE.md +124 -0
- data/debug_env.rb +46 -0
- data/doc/DATA_OBJECTS_MIGRATION_TODO.md +80 -0
- data/doc/SCHWAB_CLIENT_FACTORY_REFACTOR_PLAN.md +187 -0
- data/exe/schwab_mcp +14 -3
- data/exe/schwab_token_refresh +10 -9
- data/lib/schwab_mcp/redactor.rb +4 -0
- data/lib/schwab_mcp/schwab_client_factory.rb +44 -0
- data/lib/schwab_mcp/tools/cancel_order_tool.rb +29 -50
- data/lib/schwab_mcp/tools/get_market_hours_tool.rb +27 -28
- data/lib/schwab_mcp/tools/get_order_tool.rb +51 -108
- data/lib/schwab_mcp/tools/get_price_history_tool.rb +23 -35
- data/lib/schwab_mcp/tools/help_tool.rb +1 -22
- data/lib/schwab_mcp/tools/list_account_orders_tool.rb +35 -63
- data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +43 -72
- data/lib/schwab_mcp/tools/list_movers_tool.rb +21 -34
- data/lib/schwab_mcp/tools/list_schwab_accounts_tool.rb +18 -31
- data/lib/schwab_mcp/tools/option_chain_tool.rb +130 -82
- data/lib/schwab_mcp/tools/place_order_tool.rb +105 -117
- data/lib/schwab_mcp/tools/preview_order_tool.rb +100 -48
- data/lib/schwab_mcp/tools/quote_tool.rb +33 -26
- data/lib/schwab_mcp/tools/quotes_tool.rb +97 -45
- data/lib/schwab_mcp/tools/replace_order_tool.rb +104 -116
- data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +56 -72
- data/lib/schwab_mcp/version.rb +1 -1
- data/lib/schwab_mcp.rb +1 -2
- data/orders_example.json +7084 -0
- data/spx_option_chain.json +25073 -0
- data/test_mcp.rb +16 -0
- data/test_server.rb +23 -0
- data/trading_brokerage_account_details.json +89 -0
- data/transactions_example.json +488 -0
- metadata +17 -7
- data/lib/schwab_mcp/option_chain_filter.rb +0 -213
- data/lib/schwab_mcp/orders/iron_condor_order.rb +0 -87
- data/lib/schwab_mcp/orders/order_factory.rb +0 -40
- data/lib/schwab_mcp/orders/vertical_order.rb +0 -62
- data/lib/schwab_mcp/tools/option_strategy_finder_tool.rb +0 -378
@@ -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
|
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'schwab_rb'
|
4
|
-
require_relative 'iron_condor_order'
|
5
|
-
require_relative 'vertical_order'
|
6
|
-
|
7
|
-
module SchwabMCP
|
8
|
-
module Orders
|
9
|
-
class OrderFactory
|
10
|
-
class << self
|
11
|
-
def build(**options)
|
12
|
-
case options[:strategy_type] || 'none'
|
13
|
-
when 'ironcondor'
|
14
|
-
IronCondorOrder.build(
|
15
|
-
put_short_symbol: options[:put_short_symbol],
|
16
|
-
put_long_symbol: options[:put_long_symbol],
|
17
|
-
call_short_symbol: options[:call_short_symbol],
|
18
|
-
call_long_symbol: options[:call_long_symbol],
|
19
|
-
price: options[:price],
|
20
|
-
account_number: options[:account_number],
|
21
|
-
order_instruction: options[:order_instruction] || :open,
|
22
|
-
quantity: options[:quantity] || 1
|
23
|
-
)
|
24
|
-
when 'callspread', 'putspread'
|
25
|
-
VerticalOrder.build(
|
26
|
-
short_leg_symbol: options[:short_leg_symbol],
|
27
|
-
long_leg_symbol: options[:long_leg_symbol],
|
28
|
-
price: options[:price],
|
29
|
-
account_number: options[:account_number],
|
30
|
-
order_instruction: options[:order_instruction] || :open,
|
31
|
-
quantity: options[:quantity] || 1
|
32
|
-
)
|
33
|
-
else
|
34
|
-
raise "Unsupported trade strategy: #{options[:strategy_type] || 'none'}"
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'schwab_rb'
|
4
|
-
|
5
|
-
module SchwabMCP
|
6
|
-
module Orders
|
7
|
-
class VerticalOrder
|
8
|
-
class << self
|
9
|
-
def build(short_leg_symbol:, long_leg_symbol:, price:, account_number:, order_instruction: :open, quantity: 1)
|
10
|
-
schwab_order_builder.new.tap do |builder|
|
11
|
-
builder.set_account_number(account_number)
|
12
|
-
builder.set_order_strategy_type('SINGLE')
|
13
|
-
builder.set_session(SchwabRb::Orders::Session::NORMAL)
|
14
|
-
builder.set_duration(SchwabRb::Orders::Duration::DAY)
|
15
|
-
builder.set_order_type(order_type(order_instruction))
|
16
|
-
builder.set_complex_order_strategy_type(SchwabRb::Order::ComplexOrderStrategyTypes::VERTICAL)
|
17
|
-
builder.set_quantity(quantity)
|
18
|
-
builder.set_price(price)
|
19
|
-
builder.add_option_leg(
|
20
|
-
short_leg_instruction(order_instruction),
|
21
|
-
short_leg_symbol,
|
22
|
-
quantity
|
23
|
-
)
|
24
|
-
builder.add_option_leg(
|
25
|
-
long_leg_instruction(order_instruction),
|
26
|
-
long_leg_symbol,
|
27
|
-
quantity
|
28
|
-
)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def order_type(order_instruction)
|
33
|
-
if order_instruction == :open
|
34
|
-
SchwabRb::Order::Types::NET_CREDIT
|
35
|
-
else
|
36
|
-
SchwabRb::Order::Types::NET_DEBIT
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def short_leg_instruction(order_instruction)
|
41
|
-
if order_instruction == :open
|
42
|
-
SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN
|
43
|
-
else
|
44
|
-
SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def long_leg_instruction(order_instruction)
|
49
|
-
if order_instruction == :open
|
50
|
-
SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN
|
51
|
-
else
|
52
|
-
SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def schwab_order_builder
|
57
|
-
SchwabRb::Orders::Builder
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|