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.
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Schwab
6
+ module Resources
7
+ # Resource wrapper for transaction objects
8
+ # Provides transaction-specific helper methods and type identification
9
+ class Transaction < Base
10
+ # Set up field type coercions for transaction fields
11
+ set_field_type :transaction_date, :datetime
12
+ set_field_type :settlement_date, :date
13
+ set_field_type :net_amount, :float
14
+ set_field_type :fees, :float
15
+ set_field_type :commission, :float
16
+ set_field_type :price, :float
17
+ set_field_type :quantity, :float
18
+ set_field_type :amount, :float
19
+ set_field_type :cost, :float
20
+
21
+ # Get transaction ID
22
+ #
23
+ # @return [String] The transaction ID
24
+ def transaction_id
25
+ self[:transactionId] || self[:transaction_id] || self[:id]
26
+ end
27
+ alias_method :id, :transaction_id
28
+
29
+ # Get transaction type
30
+ #
31
+ # @return [String] The transaction type
32
+ def transaction_type
33
+ self[:transactionType] || self[:transaction_type] || self[:type]
34
+ end
35
+ alias_method :type, :transaction_type
36
+
37
+ # Get transaction subtype
38
+ #
39
+ # @return [String] The transaction subtype
40
+ def transaction_subtype
41
+ self[:transactionSubType] || self[:transaction_sub_type] || self[:subtype]
42
+ end
43
+ alias_method :subtype, :transaction_subtype
44
+
45
+ # Get transaction date
46
+ #
47
+ # @return [Time, Date, String] The transaction date
48
+ def transaction_date
49
+ self[:transactionDate] || self[:transaction_date] || self[:date]
50
+ end
51
+ alias_method :date, :transaction_date
52
+
53
+ # Get settlement date
54
+ #
55
+ # @return [Date, String] The settlement date
56
+ def settlement_date
57
+ self[:settlementDate] || self[:settlement_date]
58
+ end
59
+
60
+ # Get transaction description
61
+ #
62
+ # @return [String] The transaction description
63
+ def description
64
+ self[:description] || self[:transactionDescription] || self[:transaction_description]
65
+ end
66
+
67
+ # Get the symbol associated with the transaction
68
+ #
69
+ # @return [String, nil] The symbol
70
+ def symbol
71
+ if self[:transactionItem]
72
+ begin
73
+ self[:transactionItem][:instrument][:symbol]
74
+ rescue
75
+ nil
76
+ end
77
+ elsif self[:instrument]
78
+ self[:instrument][:symbol]
79
+ else
80
+ self[:symbol]
81
+ end
82
+ end
83
+
84
+ # Get the quantity
85
+ #
86
+ # @return [Float] The quantity
87
+ def quantity
88
+ if self[:transactionItem]
89
+ (self[:transactionItem][:quantity] || self[:transactionItem][:amount] || 0).to_f
90
+ else
91
+ (self[:quantity] || self[:amount] || 0).to_f
92
+ end
93
+ end
94
+
95
+ # Get the price
96
+ #
97
+ # @return [Float, nil] The price
98
+ def price
99
+ if self[:transactionItem]
100
+ self[:transactionItem][:price]
101
+ else
102
+ self[:price]
103
+ end
104
+ end
105
+
106
+ # Get the net amount
107
+ #
108
+ # @return [Float] The net amount
109
+ def net_amount
110
+ (self[:netAmount] || self[:net_amount] || 0).to_f
111
+ end
112
+ alias_method :amount, :net_amount
113
+
114
+ # Get fees
115
+ #
116
+ # @return [Float] The fees
117
+ def fees
118
+ if self[:fees]
119
+ fees_data = self[:fees]
120
+ total = 0.0
121
+
122
+ # Sum up different fee types if fees is a hash
123
+ if fees_data.is_a?(Hash)
124
+ fees_data.each_value { |v| total += v.to_f if v }
125
+ else
126
+ total = fees_data.to_f
127
+ end
128
+
129
+ total
130
+ else
131
+ 0.0
132
+ end
133
+ end
134
+
135
+ # Get commission
136
+ #
137
+ # @return [Float] The commission
138
+ def commission
139
+ begin
140
+ self[:commission] || self[:fees][:commission]
141
+ rescue
142
+ 0
143
+ end.to_f
144
+ end
145
+
146
+ # Check if this is a trade transaction
147
+ #
148
+ # @return [Boolean] True if trade
149
+ def trade?
150
+ ["TRADE", "BUY", "SELL", "BUY_TO_OPEN", "BUY_TO_CLOSE", "SELL_TO_OPEN", "SELL_TO_CLOSE"].include?(transaction_type&.upcase)
151
+ end
152
+
153
+ # Check if this is a buy transaction
154
+ #
155
+ # @return [Boolean] True if buy
156
+ def buy?
157
+ type_upper = transaction_type&.upcase
158
+ ["BUY", "BUY_TO_OPEN", "BUY_TO_CLOSE"].include?(type_upper) ||
159
+ (type_upper == "TRADE" && quantity > 0)
160
+ end
161
+
162
+ # Check if this is a sell transaction
163
+ #
164
+ # @return [Boolean] True if sell
165
+ def sell?
166
+ type_upper = transaction_type&.upcase
167
+ ["SELL", "SELL_TO_OPEN", "SELL_TO_CLOSE"].include?(type_upper) ||
168
+ (type_upper == "TRADE" && quantity < 0)
169
+ end
170
+
171
+ # Check if this is a dividend transaction
172
+ #
173
+ # @return [Boolean] True if dividend
174
+ def dividend?
175
+ type_upper = transaction_type&.upcase
176
+ ["DIVIDEND", "DIVIDEND_REINVEST", "QUALIFIED_DIVIDEND"].include?(type_upper)
177
+ end
178
+
179
+ # Check if this is an interest transaction
180
+ #
181
+ # @return [Boolean] True if interest
182
+ def interest?
183
+ type_upper = transaction_type&.upcase
184
+ ["INTEREST", "INTEREST_INCOME", "MARGIN_INTEREST"].include?(type_upper)
185
+ end
186
+
187
+ # Check if this is a deposit
188
+ #
189
+ # @return [Boolean] True if deposit
190
+ def deposit?
191
+ type_upper = transaction_type&.upcase
192
+ ["DEPOSIT", "ELECTRONIC_FUND", "WIRE_IN", "ACH_DEPOSIT"].include?(type_upper)
193
+ end
194
+
195
+ # Check if this is a withdrawal
196
+ #
197
+ # @return [Boolean] True if withdrawal
198
+ def withdrawal?
199
+ type_upper = transaction_type&.upcase
200
+ ["WITHDRAWAL", "WIRE_OUT", "ACH_WITHDRAWAL"].include?(type_upper)
201
+ end
202
+
203
+ # Check if this is a transfer
204
+ #
205
+ # @return [Boolean] True if transfer
206
+ def transfer?
207
+ type_upper = transaction_type&.upcase
208
+ ["TRANSFER", "INTERNAL_TRANSFER", "JOURNAL"].include?(type_upper)
209
+ end
210
+
211
+ # Check if this is a fee transaction
212
+ #
213
+ # @return [Boolean] True if fee
214
+ def fee?
215
+ type_upper = transaction_type&.upcase
216
+ ["FEE", "COMMISSION", "SERVICE_FEE", "TRANSACTION_FEE"].include?(type_upper)
217
+ end
218
+
219
+ # Check if this is an option transaction
220
+ #
221
+ # @return [Boolean] True if option transaction
222
+ def option?
223
+ if self[:transactionItem]
224
+ asset_type = begin
225
+ self[:transactionItem][:instrument][:assetType]
226
+ rescue
227
+ nil
228
+ end
229
+ asset_type == "OPTION"
230
+ elsif self[:instrument]
231
+ self[:instrument][:assetType] == "OPTION"
232
+ else
233
+ # Check if transaction type indicates options
234
+ type_upper = transaction_type&.upcase || ""
235
+ type_upper.include?("OPTION") ||
236
+ ["BUY_TO_OPEN", "BUY_TO_CLOSE", "SELL_TO_OPEN", "SELL_TO_CLOSE", "ASSIGNMENT", "EXERCISE"].include?(type_upper)
237
+ end
238
+ end
239
+
240
+ # Check if this is an assignment
241
+ #
242
+ # @return [Boolean] True if assignment
243
+ def assignment?
244
+ type_upper = transaction_type&.upcase
245
+ type_upper == "ASSIGNMENT" || type_upper == "OPTION_ASSIGNMENT"
246
+ end
247
+
248
+ # Check if this is an exercise
249
+ #
250
+ # @return [Boolean] True if exercise
251
+ def exercise?
252
+ type_upper = transaction_type&.upcase
253
+ type_upper == "EXERCISE" || type_upper == "OPTION_EXERCISE"
254
+ end
255
+
256
+ # Check if this is an expiration
257
+ #
258
+ # @return [Boolean] True if expiration
259
+ def expiration?
260
+ type_upper = transaction_type&.upcase
261
+ type_upper == "EXPIRATION" || type_upper == "OPTION_EXPIRATION"
262
+ end
263
+
264
+ # Get the cost basis for trade transactions
265
+ #
266
+ # @return [Float, nil] The cost basis
267
+ def cost_basis
268
+ return unless trade?
269
+
270
+ if price && quantity
271
+ (price * quantity.abs).round(2)
272
+ else
273
+ net_amount.abs
274
+ end
275
+ end
276
+
277
+ # Get total cost including fees
278
+ #
279
+ # @return [Float, nil] The total cost
280
+ def total_cost
281
+ return unless trade?
282
+
283
+ cost = cost_basis || 0
284
+ cost + fees + commission
285
+ end
286
+
287
+ # Check if transaction is pending
288
+ #
289
+ # @return [Boolean] True if pending
290
+ def pending?
291
+ status = self[:status] || self[:transactionStatus] || self[:transaction_status]
292
+ status&.upcase == "PENDING"
293
+ end
294
+
295
+ # Check if transaction is completed
296
+ #
297
+ # @return [Boolean] True if completed
298
+ def completed?
299
+ status = self[:status] || self[:transactionStatus] || self[:transaction_status]
300
+ status.nil? || status.upcase == "COMPLETED" || status.upcase == "EXECUTED"
301
+ end
302
+
303
+ # Check if transaction is cancelled
304
+ #
305
+ # @return [Boolean] True if cancelled
306
+ def cancelled?
307
+ status = self[:status] || self[:transactionStatus] || self[:transaction_status]
308
+ status&.upcase == "CANCELLED" || status&.upcase == "CANCELED"
309
+ end
310
+
311
+ # Get account ID associated with transaction
312
+ #
313
+ # @return [String, nil] The account ID
314
+ def account_id
315
+ self[:accountNumber] || self[:account_number] || self[:accountId] || self[:account_id]
316
+ end
317
+
318
+ # Get formatted display string for the transaction
319
+ #
320
+ # @return [String] Formatted transaction string
321
+ def to_display_string
322
+ parts = []
323
+ parts << transaction_date.to_s if transaction_date
324
+ parts << transaction_type
325
+ parts << symbol if symbol
326
+ parts << "#{quantity} @ $#{price}" if quantity && price
327
+ parts << "$#{net_amount}" if net_amount != 0
328
+
329
+ parts.compact.join(" - ")
330
+ end
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schwab
4
+ # The current version of the Schwab SDK
5
+ VERSION = "0.2.0"
6
+ end
data/lib/schwab.rb ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "schwab/version"
4
+ require_relative "schwab/error"
5
+ require_relative "schwab/configuration"
6
+ require_relative "schwab/oauth"
7
+ require_relative "schwab/client"
8
+ require_relative "schwab/market_data"
9
+ require_relative "schwab/accounts"
10
+
11
+ # Main namespace for the Schwab API SDK
12
+ # @see https://developer.schwab.com/
13
+ module Schwab
14
+ class << self
15
+ # Global configuration instance
16
+ attr_writer :configuration
17
+
18
+ # Global client instance (optional, for convenience)
19
+ attr_accessor :client
20
+
21
+ # Access the global configuration
22
+ def configuration
23
+ @configuration ||= Configuration.new
24
+ end
25
+
26
+ # Configure the SDK globally
27
+ #
28
+ # @example
29
+ # Schwab.configure do |config|
30
+ # config.client_id = ENV['SCHWAB_CLIENT_ID']
31
+ # config.client_secret = ENV['SCHWAB_CLIENT_SECRET']
32
+ # config.redirect_uri = ENV['SCHWAB_REDIRECT_URI']
33
+ # config.logger = Rails.logger
34
+ # end
35
+ #
36
+ # @yield [Configuration] The configuration instance
37
+ def configure
38
+ yield(configuration)
39
+ end
40
+
41
+ # Reset the global configuration to defaults
42
+ def reset_configuration!
43
+ @configuration = Configuration.new
44
+ end
45
+ end
46
+ end
data/sig/schwab.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Schwab
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,302 @@
1
+ # PRD: Accounts & Trading API Methods
2
+
3
+ ## Introduction/Overview
4
+
5
+ This feature extends the Schwab Ruby SDK to include comprehensive account management and trading functionality. Building upon the existing OAuth authentication and market data foundation, this enhancement enables developers to retrieve account information, manage positions, and execute trades programmatically through the Schwab API.
6
+
7
+ The implementation will follow Ruby idioms by using a hybrid approach: returning raw hashes by default for simplicity, with optional object wrappers (similar to Octokit's Sawyer::Resource pattern) for enhanced functionality and type safety.
8
+
9
+ ## Goals
10
+
11
+ 1. Provide complete coverage of Schwab's account and trading API endpoints
12
+ 2. Enable developers to retrieve account balances, positions, and transaction history
13
+ 3. Support all standard order types (market, limit, stop, stop-limit, trailing stop)
14
+ 4. Implement robust error handling with detailed messages and automatic retry logic
15
+ 5. Ensure order validation before submission to prevent API errors
16
+ 6. Maintain consistency with existing SDK patterns and Ruby best practices
17
+ 7. Achieve 80%+ test coverage for all new functionality
18
+
19
+ ## User Stories
20
+
21
+ 1. **As a developer**, I want to retrieve all accounts associated with my credentials so that I can manage multiple accounts programmatically.
22
+
23
+ 2. **As a developer**, I want to get real-time account balances and positions so that I can make informed trading decisions.
24
+
25
+ 3. **As a developer**, I want to place various types of orders (market, limit, stop) so that I can execute different trading strategies.
26
+
27
+ 4. **As a developer**, I want to cancel or modify existing orders so that I can react to market changes.
28
+
29
+ 5. **As a developer**, I want to retrieve transaction history so that I can track account activity and performance.
30
+
31
+ 6. **As a developer**, I want clear error messages when orders fail so that I can debug issues quickly.
32
+
33
+ 7. **As a developer**, I want automatic retry logic for transient failures so that my application is more resilient.
34
+
35
+ ## Functional Requirements
36
+
37
+ ### Account Operations
38
+
39
+ 1. **Get Single Account** - Retrieve detailed information for a specific account
40
+ - Input: account_id
41
+ - Output: Account details including type, status, balances
42
+
43
+ 2. **Get All Accounts** - List all accounts accessible with current credentials
44
+ - Input: optional fields parameter for filtering response
45
+ - Output: Array of account summaries
46
+
47
+ 3. **Get Account Positions** - Retrieve current positions for an account
48
+ - Input: account_id
49
+ - Output: Array of position details with current values
50
+
51
+ 4. **Get Account Balances** - Get detailed balance information
52
+ - Input: account_id
53
+ - Output: Cash balances, buying power, margin details
54
+
55
+ 5. **Get Transaction History** - Retrieve historical transactions
56
+ - Input: account_id, date range, transaction type filters
57
+ - Output: Paginated transaction list
58
+
59
+ 6. **Get Account Preferences** - Retrieve account settings and preferences
60
+ - Input: account_id
61
+ - Output: Account preferences and configuration
62
+
63
+ ### Trading Operations
64
+
65
+ 7. **Place Order** - Submit a new order to the market
66
+ - Input: account_id, order object with type, symbol, quantity, etc.
67
+ - Output: Order confirmation with order_id
68
+
69
+ 8. **Cancel Order** - Cancel a pending order
70
+ - Input: account_id, order_id
71
+ - Output: Cancellation confirmation
72
+
73
+ 9. **Replace Order** - Modify an existing order
74
+ - Input: account_id, order_id, new order parameters
75
+ - Output: Updated order details
76
+
77
+ 10. **Get Order Status** - Check status of a specific order
78
+ - Input: account_id, order_id
79
+ - Output: Current order status and details
80
+
81
+ 11. **Get All Orders** - Retrieve all orders for an account
82
+ - Input: account_id, optional filters (status, date range)
83
+ - Output: Array of orders
84
+
85
+ 12. **Get Order History** - Retrieve historical orders
86
+ - Input: account_id, date range
87
+ - Output: Paginated historical order list
88
+
89
+ ### Order Types Support
90
+
91
+ 13. **Market Orders** - Buy/sell at current market price
92
+ 14. **Limit Orders** - Buy/sell at specified price or better
93
+ 15. **Stop Orders** - Trigger market order when price reaches threshold
94
+ 16. **Stop-Limit Orders** - Trigger limit order when price reaches threshold
95
+ 17. **Trailing Stop Orders** - Dynamic stop that follows price movement
96
+ 18. **Complex Orders** - Support for brackets, OCO, and conditional orders
97
+
98
+ ### Response Format
99
+
100
+ 19. **Hash Responses** - Default response format as Ruby hashes for simplicity
101
+ 20. **Object Wrappers** - Optional Schwab::Resource objects (like Sawyer::Resource)
102
+ - Accessible as both hash keys and methods
103
+ - Lazy loading for nested resources
104
+ - Type coercion for dates/times
105
+
106
+ ### Error Handling
107
+
108
+ 21. **Detailed Error Messages** - Include API error codes, messages, and field-level errors
109
+ 22. **Automatic Retry Logic** - Configurable retry for 429 (rate limit) and 503 (service unavailable)
110
+ 23. **Order Validation** - Client-side validation before API submission
111
+ - Symbol validation
112
+ - Quantity and price checks
113
+ - Order type requirements
114
+
115
+ ### Data Management
116
+
117
+ 24. **Optional Caching** - Cache account data with configurable TTL (default: 30 seconds)
118
+ - Cache keys: account_id + data_type
119
+ - Manual cache invalidation available
120
+ - Bypass cache with `fresh: true` parameter
121
+
122
+ ## Non-Goals (Out of Scope)
123
+
124
+ 1. Paper trading/simulation mode - Users should implement their own simulation layer
125
+ 2. Position size limits or daily trading limits - Leave risk management to users
126
+ 3. Order confirmation dialogs - This is a programmatic API, not a UI
127
+ 4. Real-time WebSocket streaming - Will be addressed in separate WebSocket feature
128
+ 5. Advanced analytics or reporting - Focus on core API functionality
129
+ 6. Tax calculation or reporting features
130
+
131
+ ## Design Considerations
132
+
133
+ ### Module Structure
134
+ ```ruby
135
+ # New modules to add
136
+ lib/schwab/
137
+ accounts.rb # Account-related methods
138
+ trading.rb # Trading/order methods
139
+ resources/ # Optional object wrappers
140
+ base.rb # Base resource class
141
+ account.rb # Account resource
142
+ order.rb # Order resource
143
+ position.rb # Position resource
144
+ transaction.rb # Transaction resource
145
+ ```
146
+
147
+ ### Method Signatures
148
+ ```ruby
149
+ # Account methods
150
+ get_accounts(fields: nil, client: nil)
151
+ get_account(account_id, fields: nil, client: nil)
152
+ get_positions(account_id, client: nil)
153
+ get_transactions(account_id, from_date:, to_date:, types: nil, client: nil)
154
+
155
+ # Trading methods
156
+ place_order(account_id, order:, client: nil)
157
+ cancel_order(account_id, order_id, client: nil)
158
+ replace_order(account_id, order_id, order:, client: nil)
159
+ get_orders(account_id, status: nil, from_date: nil, to_date: nil, client: nil)
160
+
161
+ # Response format options
162
+ Schwab.configure do |config|
163
+ config.response_format = :hash # default
164
+ # or
165
+ config.response_format = :resource # for object wrappers
166
+ end
167
+ ```
168
+
169
+ ## Technical Considerations
170
+
171
+ 1. **Authentication** - Use existing OAuth setup with appropriate scopes
172
+ 2. **Rate Limiting** - Leverage existing rate limit middleware (120 req/min)
173
+ 3. **API Versioning** - Target Schwab API v1
174
+ 4. **Dependencies** - No new gems required, use existing Faraday setup
175
+ 5. **Thread Safety** - Ensure all methods are thread-safe for concurrent usage
176
+ 6. **Memory Management** - Stream large result sets to avoid memory issues
177
+
178
+ ## Success Metrics
179
+
180
+ 1. All account and trading endpoints implemented and tested
181
+ 2. Test coverage ≥ 80% for new code
182
+ 3. Average response time < 500ms for account queries
183
+ 4. Successful order placement rate > 99% (excluding validation failures)
184
+ 5. Zero security vulnerabilities in Brakeman scan
185
+ 6. Complete YARD documentation for all public methods
186
+ 7. Successfully handle all documented Schwab API error codes
187
+
188
+ ## Decisions
189
+
190
+ Based on requirements discussion, the following decisions have been made:
191
+
192
+ 1. **Order Queue System**: No queue implementation - direct API calls. Users will handle their own queuing/throttling needs.
193
+
194
+ 2. **Market Support**: US equities and options only in initial release. International/forex can be added later if needed.
195
+
196
+ 3. **Order Strategy Helpers**: Provide a strategy builder pattern for complex orders, allowing flexible composition of multi-leg strategies.
197
+
198
+ 4. **Order Preview**: Include full preview functionality with estimated costs, commissions, and margin impact before order submission.
199
+
200
+ 5. **Session Management**: Use existing OAuth token refresh mechanism (already implemented). No additional session management needed.
201
+
202
+ 6. **Account Types**: Treat all account types uniformly, returning raw API data. Users can implement account-type-specific logic as needed.
203
+
204
+ ## Additional Requirements Based on Decisions
205
+
206
+ ### Strategy Builder Pattern for Options
207
+
208
+ The strategy builder should support common multi-leg options strategies with proper validation:
209
+
210
+ ```ruby
211
+ # Vertical Spread (Bull Call or Bear Put)
212
+ strategy = Schwab::OptionsStrategy.vertical_spread(
213
+ symbol: "AAPL",
214
+ expiration: "2024-03-15",
215
+ buy_strike: 150,
216
+ sell_strike: 155,
217
+ quantity: 10,
218
+ spread_type: :call # or :put
219
+ )
220
+
221
+ # Iron Condor
222
+ strategy = Schwab::OptionsStrategy.iron_condor(
223
+ symbol: "SPY",
224
+ expiration: "2024-03-15",
225
+ put_sell_strike: 420,
226
+ put_buy_strike: 415,
227
+ call_sell_strike: 440,
228
+ call_buy_strike: 445,
229
+ quantity: 5
230
+ )
231
+
232
+ # Butterfly
233
+ strategy = Schwab::OptionsStrategy.butterfly(
234
+ symbol: "AAPL",
235
+ expiration: "2024-03-15",
236
+ lower_strike: 145,
237
+ middle_strike: 150,
238
+ upper_strike: 155,
239
+ quantity: 10,
240
+ option_type: :call # or :put
241
+ )
242
+
243
+ # Calendar Spread
244
+ strategy = Schwab::OptionsStrategy.calendar_spread(
245
+ symbol: "AAPL",
246
+ strike: 150,
247
+ near_expiration: "2024-02-15",
248
+ far_expiration: "2024-03-15",
249
+ quantity: 10,
250
+ option_type: :call # or :put
251
+ )
252
+
253
+ # Straddle
254
+ strategy = Schwab::OptionsStrategy.straddle(
255
+ symbol: "AAPL",
256
+ expiration: "2024-03-15",
257
+ strike: 150,
258
+ quantity: 10,
259
+ direction: :long # or :short
260
+ )
261
+
262
+ # Custom strategy builder for complex combinations
263
+ strategy = Schwab::OptionsStrategy.custom
264
+ .add_leg(:buy_to_open, symbol: "AAPL", expiration: "2024-03-15", strike: 150, option_type: :call, quantity: 10)
265
+ .add_leg(:sell_to_open, symbol: "AAPL", expiration: "2024-03-15", strike: 155, option_type: :call, quantity: 10)
266
+ .add_leg(:sell_to_open, symbol: "AAPL", expiration: "2024-03-15", strike: 145, option_type: :put, quantity: 10)
267
+ .add_leg(:buy_to_open, symbol: "AAPL", expiration: "2024-03-15", strike: 140, option_type: :put, quantity: 10)
268
+ .as_single_order(price_type: :net_credit, limit_price: 2.50)
269
+
270
+ # Submit the strategy
271
+ place_order(account_id, strategy: strategy)
272
+ ```
273
+
274
+ ### Supported Options Strategies
275
+
276
+ 1. **Vertical Spreads** - Bull/Bear Call/Put spreads
277
+ 2. **Iron Condor** - Four-leg neutral strategy
278
+ 3. **Iron Butterfly** - Four-leg neutral strategy with strikes converging
279
+ 4. **Butterfly** - Three-strike strategy (long/short)
280
+ 5. **Calendar/Horizontal Spreads** - Different expirations, same strike
281
+ 6. **Diagonal Spreads** - Different expirations and strikes
282
+ 7. **Straddle** - Same strike calls and puts
283
+ 8. **Strangle** - Different strike calls and puts
284
+ 9. **Collar** - Protective put with covered call
285
+ 10. **Condor** - Four-strike strategy
286
+ 11. **Ratio Spreads** - Unequal quantities
287
+ 12. **Custom Combinations** - Build any multi-leg strategy
288
+
289
+ ### Strategy Validation
290
+
291
+ The builder should validate:
292
+ - Proper strike relationships (e.g., bull call spread buy strike < sell strike)
293
+ - Expiration dates are valid and in correct order for calendar spreads
294
+ - Quantities make sense for the strategy type
295
+ - Option chains exist for the requested symbols/expirations
296
+
297
+ ### Order Preview
298
+ ```ruby
299
+ # Preview before submission
300
+ preview = preview_order(account_id, order: order_params)
301
+ # Returns: estimated_cost, commission, margin_requirement, buying_power_effect
302
+ ```