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,441 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Schwab
|
6
|
+
module Resources
|
7
|
+
# Resource wrapper for order objects
|
8
|
+
# Provides order-specific helper methods and status checking
|
9
|
+
class Order < Base
|
10
|
+
# Set up field type coercions for order 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
|
+
set_field_type :price, :float
|
17
|
+
set_field_type :stop_price, :float
|
18
|
+
set_field_type :limit_price, :float
|
19
|
+
set_field_type :activation_price, :float
|
20
|
+
set_field_type :commission, :float
|
21
|
+
|
22
|
+
# Get order ID
|
23
|
+
#
|
24
|
+
# @return [String] The order ID
|
25
|
+
def order_id
|
26
|
+
self[:orderId] || self[:order_id] || self[:id]
|
27
|
+
end
|
28
|
+
alias_method :id, :order_id
|
29
|
+
|
30
|
+
# Get account ID
|
31
|
+
#
|
32
|
+
# @return [String] The account ID
|
33
|
+
def account_id
|
34
|
+
self[:accountNumber] || self[:account_number] || self[:accountId] || self[:account_id]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get order status
|
38
|
+
#
|
39
|
+
# @return [String] The order status
|
40
|
+
def status
|
41
|
+
self[:status] || self[:orderStatus] || self[:order_status]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get order type
|
45
|
+
#
|
46
|
+
# @return [String] The order type (MARKET, LIMIT, STOP, etc.)
|
47
|
+
def order_type
|
48
|
+
self[:orderType] || self[:order_type] || self[:type]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get session (regular or extended hours)
|
52
|
+
#
|
53
|
+
# @return [String] The session (NORMAL, AM, PM, SEAMLESS)
|
54
|
+
def session
|
55
|
+
self[:session] || self[:tradingSession] || self[:trading_session]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get duration (time in force)
|
59
|
+
#
|
60
|
+
# @return [String] The duration (DAY, GTC, FOK, IOC, etc.)
|
61
|
+
def duration
|
62
|
+
self[:duration] || self[:timeInForce] || self[:time_in_force]
|
63
|
+
end
|
64
|
+
alias_method :time_in_force, :duration
|
65
|
+
|
66
|
+
# Get instruction (BUY, SELL, etc.)
|
67
|
+
#
|
68
|
+
# @return [String] The instruction
|
69
|
+
def instruction
|
70
|
+
if order_legs&.first
|
71
|
+
order_legs.first[:instruction]
|
72
|
+
else
|
73
|
+
self[:instruction] || self[:orderInstruction] || self[:order_instruction]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get the symbol for single-leg orders
|
78
|
+
#
|
79
|
+
# @return [String, nil] The symbol
|
80
|
+
def symbol
|
81
|
+
if order_legs&.size == 1
|
82
|
+
leg = order_legs.first
|
83
|
+
leg[:instrument][:symbol] if leg[:instrument]
|
84
|
+
elsif self[:symbol]
|
85
|
+
self[:symbol]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Get quantity
|
90
|
+
#
|
91
|
+
# @return [Float] The quantity
|
92
|
+
def quantity
|
93
|
+
if order_legs&.first
|
94
|
+
order_legs.first[:quantity].to_f
|
95
|
+
else
|
96
|
+
(self[:quantity] || self[:totalQuantity] || self[:total_quantity] || 0).to_f
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get filled quantity
|
101
|
+
#
|
102
|
+
# @return [Float] The filled quantity
|
103
|
+
def filled_quantity
|
104
|
+
(self[:filledQuantity] || self[:filled_quantity] || 0).to_f
|
105
|
+
end
|
106
|
+
|
107
|
+
# Get remaining quantity
|
108
|
+
#
|
109
|
+
# @return [Float] The remaining quantity
|
110
|
+
def remaining_quantity
|
111
|
+
(self[:remainingQuantity] || self[:remaining_quantity] || (quantity - filled_quantity)).to_f
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get price (for limit/stop orders)
|
115
|
+
#
|
116
|
+
# @return [Float, nil] The price
|
117
|
+
def price
|
118
|
+
self[:price] || self[:limitPrice] || self[:limit_price] || self[:stopPrice] || self[:stop_price]
|
119
|
+
end
|
120
|
+
|
121
|
+
# Get limit price
|
122
|
+
#
|
123
|
+
# @return [Float, nil] The limit price
|
124
|
+
def limit_price
|
125
|
+
self[:limitPrice] || self[:limit_price]
|
126
|
+
end
|
127
|
+
|
128
|
+
# Get stop price
|
129
|
+
#
|
130
|
+
# @return [Float, nil] The stop price
|
131
|
+
def stop_price
|
132
|
+
self[:stopPrice] || self[:stop_price]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Get activation price (for trailing stops)
|
136
|
+
#
|
137
|
+
# @return [Float, nil] The activation price
|
138
|
+
def activation_price
|
139
|
+
self[:activationPrice] || self[:activation_price]
|
140
|
+
end
|
141
|
+
|
142
|
+
# Get entered time
|
143
|
+
#
|
144
|
+
# @return [Time, String] The time order was entered
|
145
|
+
def entered_time
|
146
|
+
self[:enteredTime] || self[:entered_time] || self[:createdTime] || self[:created_time]
|
147
|
+
end
|
148
|
+
|
149
|
+
# Get close time (when order was filled/cancelled)
|
150
|
+
#
|
151
|
+
# @return [Time, String, nil] The close time
|
152
|
+
def close_time
|
153
|
+
self[:closeTime] || self[:close_time] || self[:filledTime] || self[:filled_time]
|
154
|
+
end
|
155
|
+
|
156
|
+
# Get order legs
|
157
|
+
#
|
158
|
+
# @return [Array] Array of order legs
|
159
|
+
def order_legs
|
160
|
+
self[:orderLegCollection] || self[:order_leg_collection] || self[:legs] || []
|
161
|
+
end
|
162
|
+
|
163
|
+
# Get child orders
|
164
|
+
#
|
165
|
+
# @return [Array<Order>] Array of child orders
|
166
|
+
def child_orders
|
167
|
+
children = self[:childOrderStrategies] || self[:child_order_strategies] || []
|
168
|
+
children.map do |child_data|
|
169
|
+
child_data.is_a?(Order) ? child_data : Order.new(child_data, client)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Check if order has child orders
|
174
|
+
#
|
175
|
+
# @return [Boolean] True if has child orders
|
176
|
+
def has_children?
|
177
|
+
!child_orders.empty?
|
178
|
+
end
|
179
|
+
|
180
|
+
# Get replaced orders
|
181
|
+
#
|
182
|
+
# @return [Array<Order>] Array of replaced orders
|
183
|
+
def replaced_orders
|
184
|
+
replaced = self[:replacingOrderCollection] || self[:replacing_order_collection] || []
|
185
|
+
replaced.map do |order_data|
|
186
|
+
order_data.is_a?(Order) ? order_data : Order.new(order_data, client)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Check if this is a complex order (multi-leg)
|
191
|
+
#
|
192
|
+
# @return [Boolean] True if complex order
|
193
|
+
def complex?
|
194
|
+
order_legs.size > 1
|
195
|
+
end
|
196
|
+
|
197
|
+
# Check if this is a single-leg order
|
198
|
+
#
|
199
|
+
# @return [Boolean] True if single-leg
|
200
|
+
def single_leg?
|
201
|
+
order_legs.size == 1
|
202
|
+
end
|
203
|
+
|
204
|
+
# Status check methods
|
205
|
+
|
206
|
+
# Check if order is pending
|
207
|
+
#
|
208
|
+
# @return [Boolean] True if pending
|
209
|
+
def pending?
|
210
|
+
[
|
211
|
+
"PENDING_ACTIVATION",
|
212
|
+
"PENDING_APPROVAL",
|
213
|
+
"PENDING_SUBMISSION",
|
214
|
+
"AWAITING_PARENT_ORDER",
|
215
|
+
"AWAITING_CONDITION",
|
216
|
+
"AWAITING_MANUAL_REVIEW",
|
217
|
+
"AWAITING_UR_OUT",
|
218
|
+
].include?(status&.upcase)
|
219
|
+
end
|
220
|
+
|
221
|
+
# Check if order is active/working
|
222
|
+
#
|
223
|
+
# @return [Boolean] True if active
|
224
|
+
def active?
|
225
|
+
["ACCEPTED", "WORKING", "QUEUED"].include?(status&.upcase)
|
226
|
+
end
|
227
|
+
alias_method :working?, :active?
|
228
|
+
alias_method :open?, :active?
|
229
|
+
|
230
|
+
# Check if order is filled
|
231
|
+
#
|
232
|
+
# @return [Boolean] True if filled
|
233
|
+
def filled?
|
234
|
+
status&.upcase == "FILLED"
|
235
|
+
end
|
236
|
+
|
237
|
+
# Check if order is partially filled
|
238
|
+
#
|
239
|
+
# @return [Boolean] True if partially filled
|
240
|
+
def partially_filled?
|
241
|
+
filled_quantity > 0 && filled_quantity < quantity
|
242
|
+
end
|
243
|
+
|
244
|
+
# Check if order is cancelled
|
245
|
+
#
|
246
|
+
# @return [Boolean] True if cancelled
|
247
|
+
def cancelled?
|
248
|
+
["CANCELED", "CANCELLED", "PENDING_CANCEL"].include?(status&.upcase)
|
249
|
+
end
|
250
|
+
alias_method :canceled?, :cancelled?
|
251
|
+
|
252
|
+
# Check if order is rejected
|
253
|
+
#
|
254
|
+
# @return [Boolean] True if rejected
|
255
|
+
def rejected?
|
256
|
+
status&.upcase == "REJECTED"
|
257
|
+
end
|
258
|
+
|
259
|
+
# Check if order is expired
|
260
|
+
#
|
261
|
+
# @return [Boolean] True if expired
|
262
|
+
def expired?
|
263
|
+
status&.upcase == "EXPIRED"
|
264
|
+
end
|
265
|
+
|
266
|
+
# Check if order is replaced
|
267
|
+
#
|
268
|
+
# @return [Boolean] True if replaced
|
269
|
+
def replaced?
|
270
|
+
["REPLACED", "PENDING_REPLACE"].include?(status&.upcase)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Check if order is complete (filled, cancelled, rejected, or expired)
|
274
|
+
#
|
275
|
+
# @return [Boolean] True if complete
|
276
|
+
def complete?
|
277
|
+
filled? || cancelled? || rejected? || expired?
|
278
|
+
end
|
279
|
+
|
280
|
+
# Order type checks
|
281
|
+
|
282
|
+
# Check if market order
|
283
|
+
#
|
284
|
+
# @return [Boolean] True if market order
|
285
|
+
def market_order?
|
286
|
+
order_type&.upcase == "MARKET"
|
287
|
+
end
|
288
|
+
|
289
|
+
# Check if limit order
|
290
|
+
#
|
291
|
+
# @return [Boolean] True if limit order
|
292
|
+
def limit_order?
|
293
|
+
order_type&.upcase == "LIMIT"
|
294
|
+
end
|
295
|
+
|
296
|
+
# Check if stop order
|
297
|
+
#
|
298
|
+
# @return [Boolean] True if stop order
|
299
|
+
def stop_order?
|
300
|
+
order_type&.upcase == "STOP"
|
301
|
+
end
|
302
|
+
|
303
|
+
# Check if stop limit order
|
304
|
+
#
|
305
|
+
# @return [Boolean] True if stop limit order
|
306
|
+
def stop_limit_order?
|
307
|
+
order_type&.upcase == "STOP_LIMIT"
|
308
|
+
end
|
309
|
+
|
310
|
+
# Check if trailing stop order
|
311
|
+
#
|
312
|
+
# @return [Boolean] True if trailing stop
|
313
|
+
def trailing_stop?
|
314
|
+
order_type&.upcase&.include?("TRAILING")
|
315
|
+
end
|
316
|
+
|
317
|
+
# Instruction checks
|
318
|
+
|
319
|
+
# Check if buy order
|
320
|
+
#
|
321
|
+
# @return [Boolean] True if buy
|
322
|
+
def buy?
|
323
|
+
inst = instruction&.upcase
|
324
|
+
["BUY", "BUY_TO_COVER", "BUY_TO_OPEN", "BUY_TO_CLOSE"].include?(inst)
|
325
|
+
end
|
326
|
+
|
327
|
+
# Check if sell order
|
328
|
+
#
|
329
|
+
# @return [Boolean] True if sell
|
330
|
+
def sell?
|
331
|
+
inst = instruction&.upcase
|
332
|
+
["SELL", "SELL_SHORT", "SELL_TO_OPEN", "SELL_TO_CLOSE"].include?(inst)
|
333
|
+
end
|
334
|
+
|
335
|
+
# Check if opening order
|
336
|
+
#
|
337
|
+
# @return [Boolean] True if opening
|
338
|
+
def opening?
|
339
|
+
inst = instruction&.upcase
|
340
|
+
["BUY_TO_OPEN", "SELL_TO_OPEN"].include?(inst)
|
341
|
+
end
|
342
|
+
|
343
|
+
# Check if closing order
|
344
|
+
#
|
345
|
+
# @return [Boolean] True if closing
|
346
|
+
def closing?
|
347
|
+
inst = instruction&.upcase
|
348
|
+
["BUY_TO_CLOSE", "SELL_TO_CLOSE", "BUY_TO_COVER"].include?(inst)
|
349
|
+
end
|
350
|
+
|
351
|
+
# Check if option order
|
352
|
+
#
|
353
|
+
# @return [Boolean] True if option order
|
354
|
+
def option_order?
|
355
|
+
order_legs.any? do |leg|
|
356
|
+
leg[:instrument] && leg[:instrument][:assetType] == "OPTION"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# Check if equity order
|
361
|
+
#
|
362
|
+
# @return [Boolean] True if equity order
|
363
|
+
def equity_order?
|
364
|
+
order_legs.all? do |leg|
|
365
|
+
leg[:instrument] && leg[:instrument][:assetType] == "EQUITY"
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Session checks
|
370
|
+
|
371
|
+
# Check if regular hours order
|
372
|
+
#
|
373
|
+
# @return [Boolean] True if regular hours
|
374
|
+
def regular_hours?
|
375
|
+
session&.upcase == "NORMAL"
|
376
|
+
end
|
377
|
+
|
378
|
+
# Check if extended hours order
|
379
|
+
#
|
380
|
+
# @return [Boolean] True if extended hours
|
381
|
+
def extended_hours?
|
382
|
+
["AM", "PM", "SEAMLESS"].include?(session&.upcase)
|
383
|
+
end
|
384
|
+
|
385
|
+
# Duration checks
|
386
|
+
|
387
|
+
# Check if day order
|
388
|
+
#
|
389
|
+
# @return [Boolean] True if day order
|
390
|
+
def day_order?
|
391
|
+
duration&.upcase == "DAY"
|
392
|
+
end
|
393
|
+
|
394
|
+
# Check if GTC order
|
395
|
+
#
|
396
|
+
# @return [Boolean] True if GTC
|
397
|
+
def gtc?
|
398
|
+
duration&.upcase == "GTC" || duration&.upcase == "GOOD_TILL_CANCEL"
|
399
|
+
end
|
400
|
+
|
401
|
+
# Check if FOK order
|
402
|
+
#
|
403
|
+
# @return [Boolean] True if FOK
|
404
|
+
def fok?
|
405
|
+
duration&.upcase == "FOK" || duration&.upcase == "FILL_OR_KILL"
|
406
|
+
end
|
407
|
+
|
408
|
+
# Check if IOC order
|
409
|
+
#
|
410
|
+
# @return [Boolean] True if IOC
|
411
|
+
def ioc?
|
412
|
+
duration&.upcase == "IOC" || duration&.upcase == "IMMEDIATE_OR_CANCEL"
|
413
|
+
end
|
414
|
+
|
415
|
+
# Calculate fill percentage
|
416
|
+
#
|
417
|
+
# @return [Float] The fill percentage (0-100)
|
418
|
+
def fill_percentage
|
419
|
+
return 0.0 if quantity.zero?
|
420
|
+
|
421
|
+
((filled_quantity / quantity) * 100).round(2)
|
422
|
+
end
|
423
|
+
|
424
|
+
# Get formatted display string for the order
|
425
|
+
#
|
426
|
+
# @return [String] Formatted order string
|
427
|
+
def to_display_string
|
428
|
+
parts = []
|
429
|
+
parts << status
|
430
|
+
parts << instruction
|
431
|
+
parts << quantity.to_i.to_s
|
432
|
+
parts << symbol if symbol
|
433
|
+
parts << order_type
|
434
|
+
parts << "@#{price}" if price
|
435
|
+
parts << "(#{fill_percentage}% filled)" if partially_filled?
|
436
|
+
|
437
|
+
parts.compact.join(" ")
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|