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