schwab 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 +7 -0
- data/.brakeman.yml +75 -0
- data/.claude/commands/release-pr.md +120 -0
- data/.env.example +15 -0
- data/.rspec +3 -0
- data/.rubocop.yml +25 -0
- data/CHANGELOG.md +115 -0
- data/LICENSE.txt +21 -0
- data/README.md +133 -0
- data/Rakefile +12 -0
- data/docs/resource_objects.md +474 -0
- data/lib/schwab/account_number_resolver.rb +123 -0
- data/lib/schwab/accounts.rb +331 -0
- data/lib/schwab/client.rb +266 -0
- data/lib/schwab/configuration.rb +140 -0
- data/lib/schwab/connection.rb +81 -0
- data/lib/schwab/error.rb +51 -0
- data/lib/schwab/market_data.rb +179 -0
- data/lib/schwab/middleware/authentication.rb +100 -0
- data/lib/schwab/middleware/rate_limit.rb +119 -0
- data/lib/schwab/oauth.rb +95 -0
- data/lib/schwab/resources/account.rb +272 -0
- data/lib/schwab/resources/base.rb +300 -0
- data/lib/schwab/resources/order.rb +441 -0
- data/lib/schwab/resources/position.rb +318 -0
- data/lib/schwab/resources/strategy.rb +410 -0
- data/lib/schwab/resources/transaction.rb +333 -0
- data/lib/schwab/version.rb +6 -0
- data/lib/schwab.rb +46 -0
- data/sig/schwab.rbs +4 -0
- data/tasks/prd-accounts-trading-api.md +302 -0
- data/tasks/tasks-prd-accounts-trading-api-reordered.md +140 -0
- data/tasks/tasks-prd-accounts-trading-api.md +106 -0
- metadata +146 -0
@@ -0,0 +1,318 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Schwab
|
6
|
+
module Resources
|
7
|
+
# Resource wrapper for position objects
|
8
|
+
# Provides position-specific calculations and helper methods
|
9
|
+
class Position < Base
|
10
|
+
# Set up field type coercions for position fields
|
11
|
+
set_field_type :average_price, :float
|
12
|
+
set_field_type :current_day_cost, :float
|
13
|
+
set_field_type :current_day_profit_loss, :float
|
14
|
+
set_field_type :current_day_profit_loss_percentage, :float
|
15
|
+
set_field_type :long_quantity, :float
|
16
|
+
set_field_type :short_quantity, :float
|
17
|
+
set_field_type :settled_long_quantity, :float
|
18
|
+
set_field_type :settled_short_quantity, :float
|
19
|
+
set_field_type :market_value, :float
|
20
|
+
set_field_type :maintenance_requirement, :float
|
21
|
+
set_field_type :previous_session_long_quantity, :float
|
22
|
+
set_field_type :previous_session_short_quantity, :float
|
23
|
+
|
24
|
+
# Get the symbol
|
25
|
+
#
|
26
|
+
# @return [String] The position symbol
|
27
|
+
def symbol
|
28
|
+
if self[:instrument]
|
29
|
+
self[:instrument][:symbol] || self[:instrument][:underlying_symbol]
|
30
|
+
else
|
31
|
+
self[:symbol]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get the asset type
|
36
|
+
#
|
37
|
+
# @return [String] The asset type (e.g., "EQUITY", "OPTION")
|
38
|
+
def asset_type
|
39
|
+
if self[:instrument]
|
40
|
+
self[:instrument][:assetType] || self[:instrument][:asset_type]
|
41
|
+
else
|
42
|
+
self[:assetType] || self[:asset_type]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get the CUSIP
|
47
|
+
#
|
48
|
+
# @return [String] The CUSIP identifier
|
49
|
+
def cusip
|
50
|
+
if self[:instrument]
|
51
|
+
self[:instrument][:cusip]
|
52
|
+
else
|
53
|
+
self[:cusip]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get the quantity (net of long and short)
|
58
|
+
#
|
59
|
+
# @return [Float] The net quantity
|
60
|
+
def quantity
|
61
|
+
long_qty = (self[:longQuantity] || self[:long_quantity] || 0).to_f
|
62
|
+
short_qty = (self[:shortQuantity] || self[:short_quantity] || 0).to_f
|
63
|
+
long_qty - short_qty
|
64
|
+
end
|
65
|
+
alias_method :net_quantity, :quantity
|
66
|
+
|
67
|
+
# Get the average price (cost basis per share)
|
68
|
+
#
|
69
|
+
# @return [Float, nil] The average price
|
70
|
+
def average_price
|
71
|
+
self[:averagePrice] || self[:average_price]
|
72
|
+
end
|
73
|
+
alias_method :cost_basis_per_share, :average_price
|
74
|
+
|
75
|
+
# Get the current price
|
76
|
+
#
|
77
|
+
# @return [Float, nil] The current market price
|
78
|
+
def current_price
|
79
|
+
if self[:instrument] && self[:instrument][:lastPrice]
|
80
|
+
self[:instrument][:lastPrice]
|
81
|
+
elsif self[:quote]
|
82
|
+
self[:quote][:last] || self[:quote][:mark]
|
83
|
+
else
|
84
|
+
self[:currentPrice] || self[:current_price] || self[:lastPrice] || self[:last_price]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
alias_method :market_price, :current_price
|
88
|
+
alias_method :last_price, :current_price
|
89
|
+
|
90
|
+
# Get the market value of the position
|
91
|
+
#
|
92
|
+
# @return [Float] The market value
|
93
|
+
def market_value
|
94
|
+
value = self[:marketValue] || self[:market_value]
|
95
|
+
return value.to_f if value
|
96
|
+
|
97
|
+
# Calculate if not provided
|
98
|
+
if current_price && quantity
|
99
|
+
(current_price * quantity.abs).round(2)
|
100
|
+
else
|
101
|
+
0.0
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Calculate the total cost basis
|
106
|
+
#
|
107
|
+
# @return [Float] The total cost basis
|
108
|
+
def cost_basis
|
109
|
+
if average_price && quantity
|
110
|
+
(average_price * quantity.abs).round(2)
|
111
|
+
else
|
112
|
+
0.0
|
113
|
+
end
|
114
|
+
end
|
115
|
+
alias_method :total_cost, :cost_basis
|
116
|
+
|
117
|
+
# Calculate unrealized P&L
|
118
|
+
#
|
119
|
+
# @return [Float] The unrealized profit/loss
|
120
|
+
def unrealized_pnl
|
121
|
+
market_value - cost_basis
|
122
|
+
end
|
123
|
+
alias_method :unrealized_profit_loss, :unrealized_pnl
|
124
|
+
|
125
|
+
# Calculate unrealized P&L percentage
|
126
|
+
#
|
127
|
+
# @return [Float, nil] The unrealized profit/loss percentage
|
128
|
+
def unrealized_pnl_percentage
|
129
|
+
return if cost_basis.zero?
|
130
|
+
|
131
|
+
((unrealized_pnl / cost_basis) * 100).round(2)
|
132
|
+
end
|
133
|
+
alias_method :unrealized_profit_loss_percentage, :unrealized_pnl_percentage
|
134
|
+
|
135
|
+
# Get today's P&L
|
136
|
+
#
|
137
|
+
# @return [Float, nil] Today's profit/loss
|
138
|
+
def day_pnl
|
139
|
+
self[:currentDayProfitLoss] ||
|
140
|
+
self[:current_day_profit_loss] ||
|
141
|
+
self[:dayProfitLoss] ||
|
142
|
+
self[:day_profit_loss]
|
143
|
+
end
|
144
|
+
alias_method :todays_pnl, :day_pnl
|
145
|
+
alias_method :current_day_pnl, :day_pnl
|
146
|
+
|
147
|
+
# Get today's P&L percentage
|
148
|
+
#
|
149
|
+
# @return [Float, nil] Today's profit/loss percentage
|
150
|
+
def day_pnl_percentage
|
151
|
+
self[:currentDayProfitLossPercentage] ||
|
152
|
+
self[:current_day_profit_loss_percentage] ||
|
153
|
+
self[:dayProfitLossPercentage] ||
|
154
|
+
self[:day_profit_loss_percentage]
|
155
|
+
end
|
156
|
+
alias_method :todays_pnl_percentage, :day_pnl_percentage
|
157
|
+
alias_method :current_day_pnl_percentage, :day_pnl_percentage
|
158
|
+
|
159
|
+
# Check if this is a long position
|
160
|
+
#
|
161
|
+
# @return [Boolean] True if long position
|
162
|
+
def long?
|
163
|
+
quantity > 0
|
164
|
+
end
|
165
|
+
|
166
|
+
# Check if this is a short position
|
167
|
+
#
|
168
|
+
# @return [Boolean] True if short position
|
169
|
+
def short?
|
170
|
+
quantity < 0
|
171
|
+
end
|
172
|
+
|
173
|
+
# Check if this is an equity position
|
174
|
+
#
|
175
|
+
# @return [Boolean] True if equity
|
176
|
+
def equity?
|
177
|
+
asset_type == "EQUITY"
|
178
|
+
end
|
179
|
+
|
180
|
+
# Check if this is an option position
|
181
|
+
#
|
182
|
+
# @return [Boolean] True if option
|
183
|
+
def option?
|
184
|
+
asset_type == "OPTION"
|
185
|
+
end
|
186
|
+
|
187
|
+
# Check if this is a profitable position
|
188
|
+
#
|
189
|
+
# @return [Boolean] True if profitable
|
190
|
+
def profitable?
|
191
|
+
unrealized_pnl > 0
|
192
|
+
end
|
193
|
+
|
194
|
+
# Check if this is a losing position
|
195
|
+
#
|
196
|
+
# @return [Boolean] True if losing
|
197
|
+
def losing?
|
198
|
+
unrealized_pnl < 0
|
199
|
+
end
|
200
|
+
|
201
|
+
# Get maintenance requirement for this position
|
202
|
+
#
|
203
|
+
# @return [Float, nil] The maintenance requirement
|
204
|
+
def maintenance_requirement
|
205
|
+
self[:maintenanceRequirement] || self[:maintenance_requirement]
|
206
|
+
end
|
207
|
+
|
208
|
+
# Get the instrument details
|
209
|
+
#
|
210
|
+
# @return [Hash, nil] The instrument details
|
211
|
+
def instrument
|
212
|
+
self[:instrument]
|
213
|
+
end
|
214
|
+
|
215
|
+
# Get option details if this is an option position
|
216
|
+
#
|
217
|
+
# @return [Hash, nil] Option details including strike, expiration, etc.
|
218
|
+
def option_details
|
219
|
+
return unless option? && instrument
|
220
|
+
|
221
|
+
{
|
222
|
+
underlying_symbol: instrument[:underlyingSymbol] || instrument[:underlying_symbol],
|
223
|
+
strike_price: instrument[:strikePrice] || instrument[:strike_price],
|
224
|
+
expiration_date: instrument[:expirationDate] || instrument[:expiration_date],
|
225
|
+
option_type: instrument[:putCall] || instrument[:put_call] || instrument[:optionType] || instrument[:option_type],
|
226
|
+
contract_size: instrument[:contractSize] || instrument[:contract_size] || 100,
|
227
|
+
}
|
228
|
+
end
|
229
|
+
|
230
|
+
# Get the underlying symbol for options
|
231
|
+
#
|
232
|
+
# @return [String, nil] The underlying symbol
|
233
|
+
def underlying_symbol
|
234
|
+
return unless option?
|
235
|
+
|
236
|
+
option_details[:underlying_symbol] if option_details
|
237
|
+
end
|
238
|
+
|
239
|
+
# Get the strike price for options
|
240
|
+
#
|
241
|
+
# @return [Float, nil] The strike price
|
242
|
+
def strike_price
|
243
|
+
return unless option?
|
244
|
+
|
245
|
+
option_details[:strike_price] if option_details
|
246
|
+
end
|
247
|
+
|
248
|
+
# Get the expiration date for options
|
249
|
+
#
|
250
|
+
# @return [String, nil] The expiration date
|
251
|
+
def expiration_date
|
252
|
+
return unless option?
|
253
|
+
|
254
|
+
option_details[:expiration_date] if option_details
|
255
|
+
end
|
256
|
+
|
257
|
+
# Check if this is a call option
|
258
|
+
#
|
259
|
+
# @return [Boolean, nil] True if call option
|
260
|
+
def call?
|
261
|
+
return false unless option?
|
262
|
+
|
263
|
+
details = option_details
|
264
|
+
return false unless details
|
265
|
+
|
266
|
+
details[:option_type] == "CALL" || details[:option_type] == "call"
|
267
|
+
end
|
268
|
+
|
269
|
+
# Check if this is a put option
|
270
|
+
#
|
271
|
+
# @return [Boolean, nil] True if put option
|
272
|
+
def put?
|
273
|
+
return false unless option?
|
274
|
+
|
275
|
+
details = option_details
|
276
|
+
return false unless details
|
277
|
+
|
278
|
+
details[:option_type] == "PUT" || details[:option_type] == "put"
|
279
|
+
end
|
280
|
+
|
281
|
+
# Calculate the value per contract for options
|
282
|
+
#
|
283
|
+
# @return [Float, nil] Value per contract
|
284
|
+
def value_per_contract
|
285
|
+
return unless option? && market_value && quantity != 0
|
286
|
+
|
287
|
+
contracts = quantity.abs
|
288
|
+
market_value / contracts
|
289
|
+
end
|
290
|
+
|
291
|
+
# Calculate cost per contract for options
|
292
|
+
#
|
293
|
+
# @return [Float, nil] Cost per contract
|
294
|
+
def cost_per_contract
|
295
|
+
return unless option? && cost_basis != 0 && quantity != 0
|
296
|
+
|
297
|
+
contracts = quantity.abs
|
298
|
+
cost_basis / contracts
|
299
|
+
end
|
300
|
+
|
301
|
+
# Get formatted display string for the position
|
302
|
+
#
|
303
|
+
# @return [String] Formatted position string
|
304
|
+
def to_display_string
|
305
|
+
if option?
|
306
|
+
details = option_details
|
307
|
+
if details
|
308
|
+
"#{quantity.to_i} #{details[:underlying_symbol]} #{details[:strike_price]} #{details[:option_type]} #{details[:expiration_date]}"
|
309
|
+
else
|
310
|
+
"#{quantity.to_i} #{symbol}"
|
311
|
+
end
|
312
|
+
else
|
313
|
+
"#{quantity} shares of #{symbol}"
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
@@ -0,0 +1,410 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Schwab
|
6
|
+
module Resources
|
7
|
+
# Resource wrapper for strategy objects (complex multi-leg orders)
|
8
|
+
# Provides strategy-specific helper methods for options strategies
|
9
|
+
class Strategy < Base
|
10
|
+
# Set up field type coercions for strategy fields
|
11
|
+
set_field_type :entered_time, :datetime
|
12
|
+
set_field_type :close_time, :datetime
|
13
|
+
set_field_type :filled_quantity, :float
|
14
|
+
set_field_type :remaining_quantity, :float
|
15
|
+
set_field_type :quantity, :float
|
16
|
+
|
17
|
+
# Get strategy type
|
18
|
+
#
|
19
|
+
# @return [String] The strategy type
|
20
|
+
def strategy_type
|
21
|
+
self[:strategyType] || self[:strategy_type] || self[:type]
|
22
|
+
end
|
23
|
+
alias_method :type, :strategy_type
|
24
|
+
|
25
|
+
# Get the strategy name/description
|
26
|
+
#
|
27
|
+
# @return [String] The strategy name
|
28
|
+
def name
|
29
|
+
self[:strategyName] || self[:strategy_name] || self[:name] || strategy_type
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get strategy status
|
33
|
+
#
|
34
|
+
# @return [String] The strategy status
|
35
|
+
def status
|
36
|
+
self[:status] || self[:strategyStatus] || self[:strategy_status]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get the legs of the strategy
|
40
|
+
#
|
41
|
+
# @return [Array] Array of strategy legs
|
42
|
+
def legs
|
43
|
+
self[:legs] || self[:strategyLegs] || self[:strategy_legs] || self[:orderLegCollection] || []
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get number of legs
|
47
|
+
#
|
48
|
+
# @return [Integer] Number of legs in strategy
|
49
|
+
def leg_count
|
50
|
+
legs.size
|
51
|
+
end
|
52
|
+
|
53
|
+
# Check if this is a single-leg strategy
|
54
|
+
#
|
55
|
+
# @return [Boolean] True if single leg
|
56
|
+
def single_leg?
|
57
|
+
leg_count == 1
|
58
|
+
end
|
59
|
+
|
60
|
+
# Check if this is a multi-leg strategy
|
61
|
+
#
|
62
|
+
# @return [Boolean] True if multi-leg
|
63
|
+
def multi_leg?
|
64
|
+
leg_count > 1
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get the underlying symbol
|
68
|
+
#
|
69
|
+
# @return [String, nil] The underlying symbol
|
70
|
+
def underlying_symbol
|
71
|
+
# All legs should have the same underlying for a valid strategy
|
72
|
+
first_leg = legs.first
|
73
|
+
return unless first_leg
|
74
|
+
|
75
|
+
if first_leg[:instrument]
|
76
|
+
first_leg[:instrument][:underlyingSymbol] ||
|
77
|
+
first_leg[:instrument][:underlying_symbol] ||
|
78
|
+
first_leg[:instrument][:symbol]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Get all strike prices in the strategy
|
83
|
+
#
|
84
|
+
# @return [Array<Float>] Array of strike prices
|
85
|
+
def strike_prices
|
86
|
+
legs.map do |leg|
|
87
|
+
if leg[:instrument]
|
88
|
+
leg[:instrument][:strikePrice] || leg[:instrument][:strike_price]
|
89
|
+
end
|
90
|
+
end.compact.uniq.sort
|
91
|
+
end
|
92
|
+
|
93
|
+
# Get all expiration dates in the strategy
|
94
|
+
#
|
95
|
+
# @return [Array<String>] Array of expiration dates
|
96
|
+
def expiration_dates
|
97
|
+
legs.map do |leg|
|
98
|
+
if leg[:instrument]
|
99
|
+
leg[:instrument][:expirationDate] || leg[:instrument][:expiration_date]
|
100
|
+
end
|
101
|
+
end.compact.uniq.sort
|
102
|
+
end
|
103
|
+
|
104
|
+
# Get net credit/debit
|
105
|
+
#
|
106
|
+
# @return [Float, nil] Net credit (positive) or debit (negative)
|
107
|
+
def net_premium
|
108
|
+
total = 0.0
|
109
|
+
legs.each do |leg|
|
110
|
+
price = leg[:price] || 0
|
111
|
+
quantity = leg[:quantity] || 0
|
112
|
+
instruction = leg[:instruction]
|
113
|
+
|
114
|
+
# Selling generates credit, buying generates debit
|
115
|
+
if ["SELL", "SELL_TO_OPEN", "SELL_TO_CLOSE"].include?(instruction&.upcase)
|
116
|
+
total += price * quantity * 100 # Options are in contracts of 100
|
117
|
+
else
|
118
|
+
total -= price * quantity * 100
|
119
|
+
end
|
120
|
+
end
|
121
|
+
total
|
122
|
+
end
|
123
|
+
|
124
|
+
# Check if this is a credit strategy
|
125
|
+
#
|
126
|
+
# @return [Boolean] True if net credit
|
127
|
+
def credit_strategy?
|
128
|
+
net_premium > 0
|
129
|
+
end
|
130
|
+
|
131
|
+
# Check if this is a debit strategy
|
132
|
+
#
|
133
|
+
# @return [Boolean] True if net debit
|
134
|
+
def debit_strategy?
|
135
|
+
net_premium < 0
|
136
|
+
end
|
137
|
+
|
138
|
+
# Strategy type identification methods
|
139
|
+
|
140
|
+
# Check if this is a vertical spread
|
141
|
+
#
|
142
|
+
# @return [Boolean] True if vertical spread
|
143
|
+
def vertical_spread?
|
144
|
+
return false unless leg_count == 2
|
145
|
+
|
146
|
+
expirations = expiration_dates
|
147
|
+
strikes = strike_prices
|
148
|
+
|
149
|
+
# Same expiration, different strikes
|
150
|
+
expirations.size == 1 && strikes.size == 2
|
151
|
+
end
|
152
|
+
alias_method :vertical?, :vertical_spread?
|
153
|
+
|
154
|
+
# Check if this is a calendar spread
|
155
|
+
#
|
156
|
+
# @return [Boolean] True if calendar spread
|
157
|
+
def calendar_spread?
|
158
|
+
return false unless leg_count == 2
|
159
|
+
|
160
|
+
expirations = expiration_dates
|
161
|
+
strikes = strike_prices
|
162
|
+
|
163
|
+
# Different expirations, same strike
|
164
|
+
expirations.size == 2 && strikes.size == 1
|
165
|
+
end
|
166
|
+
alias_method :calendar?, :calendar_spread?
|
167
|
+
alias_method :horizontal?, :calendar_spread?
|
168
|
+
|
169
|
+
# Check if this is a diagonal spread
|
170
|
+
#
|
171
|
+
# @return [Boolean] True if diagonal spread
|
172
|
+
def diagonal_spread?
|
173
|
+
return false unless leg_count == 2
|
174
|
+
|
175
|
+
expirations = expiration_dates
|
176
|
+
strikes = strike_prices
|
177
|
+
|
178
|
+
# Different expirations and strikes
|
179
|
+
expirations.size == 2 && strikes.size == 2
|
180
|
+
end
|
181
|
+
alias_method :diagonal?, :diagonal_spread?
|
182
|
+
|
183
|
+
# Check if this is a butterfly spread
|
184
|
+
#
|
185
|
+
# @return [Boolean] True if butterfly
|
186
|
+
def butterfly?
|
187
|
+
return false unless leg_count == 4
|
188
|
+
|
189
|
+
strikes = strike_prices
|
190
|
+
# Butterfly has 3 unique strikes
|
191
|
+
strikes.size == 3
|
192
|
+
end
|
193
|
+
|
194
|
+
# Check if this is an iron butterfly
|
195
|
+
#
|
196
|
+
# @return [Boolean] True if iron butterfly
|
197
|
+
def iron_butterfly?
|
198
|
+
return false unless butterfly?
|
199
|
+
|
200
|
+
# Iron butterfly uses both puts and calls
|
201
|
+
has_puts = legs.any? { |leg| leg_is_put?(leg) }
|
202
|
+
has_calls = legs.any? { |leg| leg_is_call?(leg) }
|
203
|
+
|
204
|
+
has_puts && has_calls
|
205
|
+
end
|
206
|
+
|
207
|
+
# Check if this is a condor
|
208
|
+
#
|
209
|
+
# @return [Boolean] True if condor
|
210
|
+
def condor?
|
211
|
+
return false unless leg_count == 4
|
212
|
+
|
213
|
+
strikes = strike_prices
|
214
|
+
# Condor has 4 unique strikes
|
215
|
+
strikes.size == 4
|
216
|
+
end
|
217
|
+
|
218
|
+
# Check if this is an iron condor
|
219
|
+
#
|
220
|
+
# @return [Boolean] True if iron condor
|
221
|
+
def iron_condor?
|
222
|
+
return false unless condor?
|
223
|
+
|
224
|
+
# Iron condor uses both puts and calls
|
225
|
+
has_puts = legs.any? { |leg| leg_is_put?(leg) }
|
226
|
+
has_calls = legs.any? { |leg| leg_is_call?(leg) }
|
227
|
+
|
228
|
+
has_puts && has_calls
|
229
|
+
end
|
230
|
+
|
231
|
+
# Check if this is a straddle
|
232
|
+
#
|
233
|
+
# @return [Boolean] True if straddle
|
234
|
+
def straddle?
|
235
|
+
return false unless leg_count == 2
|
236
|
+
|
237
|
+
strikes = strike_prices
|
238
|
+
expirations = expiration_dates
|
239
|
+
|
240
|
+
# Same strike and expiration, one put and one call
|
241
|
+
if strikes.size == 1 && expirations.size == 1
|
242
|
+
has_put = legs.any? { |leg| leg_is_put?(leg) }
|
243
|
+
has_call = legs.any? { |leg| leg_is_call?(leg) }
|
244
|
+
has_put && has_call
|
245
|
+
else
|
246
|
+
false
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Check if this is a strangle
|
251
|
+
#
|
252
|
+
# @return [Boolean] True if strangle
|
253
|
+
def strangle?
|
254
|
+
return false unless leg_count == 2
|
255
|
+
|
256
|
+
strikes = strike_prices
|
257
|
+
expirations = expiration_dates
|
258
|
+
|
259
|
+
# Different strikes, same expiration, one put and one call
|
260
|
+
if strikes.size == 2 && expirations.size == 1
|
261
|
+
has_put = legs.any? { |leg| leg_is_put?(leg) }
|
262
|
+
has_call = legs.any? { |leg| leg_is_call?(leg) }
|
263
|
+
has_put && has_call
|
264
|
+
else
|
265
|
+
false
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Check if this is a collar
|
270
|
+
#
|
271
|
+
# @return [Boolean] True if collar
|
272
|
+
def collar?
|
273
|
+
return false unless leg_count == 3 || leg_count == 2
|
274
|
+
|
275
|
+
# Collar: long stock + long put + short call
|
276
|
+
# Or just long put + short call (if stock is held separately)
|
277
|
+
|
278
|
+
has_long_put = legs.any? { |leg| leg_is_put?(leg) && leg_is_long?(leg) }
|
279
|
+
has_short_call = legs.any? { |leg| leg_is_call?(leg) && leg_is_short?(leg) }
|
280
|
+
|
281
|
+
has_long_put && has_short_call
|
282
|
+
end
|
283
|
+
|
284
|
+
# Check if this is a ratio spread
|
285
|
+
#
|
286
|
+
# @return [Boolean] True if ratio spread
|
287
|
+
def ratio_spread?
|
288
|
+
return false unless multi_leg?
|
289
|
+
|
290
|
+
# Ratio spread has unequal quantities
|
291
|
+
quantities = legs.map { |leg| leg[:quantity].to_i.abs }.uniq
|
292
|
+
quantities.size > 1
|
293
|
+
end
|
294
|
+
|
295
|
+
# Get max profit for the strategy
|
296
|
+
#
|
297
|
+
# @return [Float, nil] Maximum profit potential
|
298
|
+
def max_profit
|
299
|
+
# This would require complex calculations based on strategy type
|
300
|
+
# Placeholder for strategy-specific calculations
|
301
|
+
nil
|
302
|
+
end
|
303
|
+
|
304
|
+
# Get max loss for the strategy
|
305
|
+
#
|
306
|
+
# @return [Float, nil] Maximum loss potential
|
307
|
+
def max_loss
|
308
|
+
# This would require complex calculations based on strategy type
|
309
|
+
# Placeholder for strategy-specific calculations
|
310
|
+
nil
|
311
|
+
end
|
312
|
+
|
313
|
+
# Get breakeven points
|
314
|
+
#
|
315
|
+
# @return [Array<Float>] Breakeven points
|
316
|
+
def breakeven_points
|
317
|
+
# This would require complex calculations based on strategy type
|
318
|
+
# Placeholder for strategy-specific calculations
|
319
|
+
[]
|
320
|
+
end
|
321
|
+
|
322
|
+
# Get strategy description
|
323
|
+
#
|
324
|
+
# @return [String] Human-readable strategy description
|
325
|
+
def description
|
326
|
+
if vertical_spread?
|
327
|
+
"Vertical Spread"
|
328
|
+
elsif calendar_spread?
|
329
|
+
"Calendar Spread"
|
330
|
+
elsif diagonal_spread?
|
331
|
+
"Diagonal Spread"
|
332
|
+
elsif iron_butterfly?
|
333
|
+
"Iron Butterfly"
|
334
|
+
elsif butterfly?
|
335
|
+
"Butterfly Spread"
|
336
|
+
elsif iron_condor?
|
337
|
+
"Iron Condor"
|
338
|
+
elsif condor?
|
339
|
+
"Condor Spread"
|
340
|
+
elsif straddle?
|
341
|
+
"Straddle"
|
342
|
+
elsif strangle?
|
343
|
+
"Strangle"
|
344
|
+
elsif collar?
|
345
|
+
"Collar"
|
346
|
+
elsif ratio_spread?
|
347
|
+
"Ratio Spread"
|
348
|
+
elsif multi_leg?
|
349
|
+
"Multi-leg Strategy"
|
350
|
+
else
|
351
|
+
"Single Option"
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Get formatted display string for the strategy
|
356
|
+
#
|
357
|
+
# @return [String] Formatted strategy string
|
358
|
+
def to_display_string
|
359
|
+
parts = []
|
360
|
+
parts << description
|
361
|
+
parts << underlying_symbol if underlying_symbol
|
362
|
+
|
363
|
+
if strike_prices.any?
|
364
|
+
parts << "Strikes: #{strike_prices.join("/")}"
|
365
|
+
end
|
366
|
+
|
367
|
+
if expiration_dates.any?
|
368
|
+
parts << "Exp: #{expiration_dates.first}"
|
369
|
+
end
|
370
|
+
|
371
|
+
premium = net_premium
|
372
|
+
if premium != 0
|
373
|
+
parts << (premium > 0 ? "Credit: $#{premium.abs}" : "Debit: $#{premium.abs}")
|
374
|
+
end
|
375
|
+
|
376
|
+
parts.compact.join(" - ")
|
377
|
+
end
|
378
|
+
|
379
|
+
private
|
380
|
+
|
381
|
+
# Check if a leg is a put option
|
382
|
+
def leg_is_put?(leg)
|
383
|
+
return false unless leg[:instrument]
|
384
|
+
|
385
|
+
put_call = leg[:instrument][:putCall] || leg[:instrument][:put_call]
|
386
|
+
put_call&.upcase == "PUT"
|
387
|
+
end
|
388
|
+
|
389
|
+
# Check if a leg is a call option
|
390
|
+
def leg_is_call?(leg)
|
391
|
+
return false unless leg[:instrument]
|
392
|
+
|
393
|
+
put_call = leg[:instrument][:putCall] || leg[:instrument][:put_call]
|
394
|
+
put_call&.upcase == "CALL"
|
395
|
+
end
|
396
|
+
|
397
|
+
# Check if a leg is long (buy)
|
398
|
+
def leg_is_long?(leg)
|
399
|
+
instruction = leg[:instruction]
|
400
|
+
["BUY", "BUY_TO_OPEN", "BUY_TO_CLOSE"].include?(instruction&.upcase)
|
401
|
+
end
|
402
|
+
|
403
|
+
# Check if a leg is short (sell)
|
404
|
+
def leg_is_short?(leg)
|
405
|
+
instruction = leg[:instruction]
|
406
|
+
["SELL", "SELL_TO_OPEN", "SELL_TO_CLOSE"].include?(instruction&.upcase)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|