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,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
|
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,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
|
+
```
|