tastytrade 0.2.0 → 0.3.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 +4 -4
- data/.claude/commands/plan.md +13 -0
- data/.claude/commands/release-pr.md +12 -0
- data/CHANGELOG.md +170 -0
- data/README.md +424 -3
- data/ROADMAP.md +17 -17
- data/lib/tastytrade/cli/history_formatter.rb +304 -0
- data/lib/tastytrade/cli/orders.rb +749 -0
- data/lib/tastytrade/cli/positions_formatter.rb +114 -0
- data/lib/tastytrade/cli.rb +701 -12
- data/lib/tastytrade/cli_helpers.rb +111 -14
- data/lib/tastytrade/client.rb +7 -0
- data/lib/tastytrade/file_store.rb +83 -0
- data/lib/tastytrade/instruments/equity.rb +42 -0
- data/lib/tastytrade/models/account.rb +160 -2
- data/lib/tastytrade/models/account_balance.rb +46 -0
- data/lib/tastytrade/models/buying_power_effect.rb +61 -0
- data/lib/tastytrade/models/live_order.rb +272 -0
- data/lib/tastytrade/models/order_response.rb +106 -0
- data/lib/tastytrade/models/order_status.rb +84 -0
- data/lib/tastytrade/models/trading_status.rb +200 -0
- data/lib/tastytrade/models/transaction.rb +151 -0
- data/lib/tastytrade/models.rb +6 -0
- data/lib/tastytrade/order.rb +191 -0
- data/lib/tastytrade/order_validator.rb +355 -0
- data/lib/tastytrade/session.rb +26 -1
- data/lib/tastytrade/session_manager.rb +43 -14
- data/lib/tastytrade/version.rb +1 -1
- data/lib/tastytrade.rb +43 -0
- data/spec/exe/tastytrade_spec.rb +1 -1
- data/spec/spec_helper.rb +72 -0
- data/spec/tastytrade/cli/positions_spec.rb +267 -0
- data/spec/tastytrade/cli_auth_spec.rb +5 -0
- data/spec/tastytrade/cli_env_login_spec.rb +199 -0
- data/spec/tastytrade/cli_helpers_spec.rb +3 -26
- data/spec/tastytrade/cli_orders_spec.rb +168 -0
- data/spec/tastytrade/cli_status_spec.rb +153 -164
- data/spec/tastytrade/file_store_spec.rb +126 -0
- data/spec/tastytrade/models/account_balance_spec.rb +103 -0
- data/spec/tastytrade/models/account_order_history_spec.rb +229 -0
- data/spec/tastytrade/models/account_order_management_spec.rb +271 -0
- data/spec/tastytrade/models/account_place_order_spec.rb +125 -0
- data/spec/tastytrade/models/account_spec.rb +86 -15
- data/spec/tastytrade/models/buying_power_effect_spec.rb +250 -0
- data/spec/tastytrade/models/live_order_json_spec.rb +144 -0
- data/spec/tastytrade/models/live_order_spec.rb +295 -0
- data/spec/tastytrade/models/order_response_spec.rb +96 -0
- data/spec/tastytrade/models/order_status_spec.rb +113 -0
- data/spec/tastytrade/models/trading_status_spec.rb +260 -0
- data/spec/tastytrade/models/transaction_spec.rb +236 -0
- data/spec/tastytrade/order_edge_cases_spec.rb +163 -0
- data/spec/tastytrade/order_spec.rb +201 -0
- data/spec/tastytrade/order_validator_spec.rb +347 -0
- data/spec/tastytrade/session_env_spec.rb +169 -0
- data/spec/tastytrade/session_manager_spec.rb +43 -33
- data/vcr_implementation_plan.md +403 -0
- data/vcr_implementation_research.md +330 -0
- metadata +50 -18
- data/lib/tastytrade/keyring_store.rb +0 -72
- data/spec/tastytrade/keyring_store_spec.rb +0 -168
@@ -0,0 +1,272 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bigdecimal"
|
4
|
+
require "time"
|
5
|
+
|
6
|
+
module Tastytrade
|
7
|
+
module Models
|
8
|
+
# Represents a live order (open or recently closed) from the API
|
9
|
+
class LiveOrder < Base
|
10
|
+
attr_reader :id, :account_number, :status, :cancellable, :editable,
|
11
|
+
:edited, :time_in_force, :order_type, :size, :price,
|
12
|
+
:price_effect, :underlying_symbol, :underlying_instrument_type,
|
13
|
+
:stop_trigger, :legs, :gtc_date, :created_at, :updated_at,
|
14
|
+
:received_at, :routed_at, :filled_at, :cancelled_at,
|
15
|
+
:expired_at, :rejected_at, :live_at, :terminal_at,
|
16
|
+
:contingent_status, :confirmation_status, :reject_reason,
|
17
|
+
:user_tag, :preflight_check_result, :order_rule
|
18
|
+
|
19
|
+
# Check if order can be cancelled
|
20
|
+
def cancellable?
|
21
|
+
@cancellable && status == "Live"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Check if order can be replaced/edited
|
25
|
+
def editable?
|
26
|
+
@editable && status == "Live"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check if order is in a terminal state
|
30
|
+
def terminal?
|
31
|
+
%w[Filled Cancelled Rejected Expired].include?(status)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Check if order is working (live in market)
|
35
|
+
def working?
|
36
|
+
status == "Live"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if order has been filled
|
40
|
+
def filled?
|
41
|
+
status == "Filled"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Check if order has been cancelled
|
45
|
+
def cancelled?
|
46
|
+
status == "Cancelled"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get remaining quantity across all legs
|
50
|
+
def remaining_quantity
|
51
|
+
return 0 unless @legs
|
52
|
+
@legs.sum { |leg| leg.remaining_quantity || 0 }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get filled quantity across all legs
|
56
|
+
def filled_quantity
|
57
|
+
return 0 unless @legs
|
58
|
+
@legs.sum { |leg| leg.filled_quantity || 0 }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Convert to hash for JSON serialization
|
62
|
+
def to_h
|
63
|
+
{
|
64
|
+
id: @id,
|
65
|
+
account_number: @account_number,
|
66
|
+
status: @status,
|
67
|
+
cancellable: @cancellable,
|
68
|
+
editable: @editable,
|
69
|
+
edited: @edited,
|
70
|
+
time_in_force: @time_in_force,
|
71
|
+
order_type: @order_type,
|
72
|
+
size: @size,
|
73
|
+
price: @price&.to_s("F"),
|
74
|
+
price_effect: @price_effect,
|
75
|
+
underlying_symbol: @underlying_symbol,
|
76
|
+
underlying_instrument_type: @underlying_instrument_type,
|
77
|
+
stop_trigger: @stop_trigger&.to_s("F"),
|
78
|
+
gtc_date: @gtc_date&.to_s,
|
79
|
+
created_at: @created_at&.iso8601,
|
80
|
+
updated_at: @updated_at&.iso8601,
|
81
|
+
received_at: @received_at&.iso8601,
|
82
|
+
routed_at: @routed_at&.iso8601,
|
83
|
+
filled_at: @filled_at&.iso8601,
|
84
|
+
cancelled_at: @cancelled_at&.iso8601,
|
85
|
+
expired_at: @expired_at&.iso8601,
|
86
|
+
rejected_at: @rejected_at&.iso8601,
|
87
|
+
live_at: @live_at&.iso8601,
|
88
|
+
terminal_at: @terminal_at&.iso8601,
|
89
|
+
contingent_status: @contingent_status,
|
90
|
+
confirmation_status: @confirmation_status,
|
91
|
+
reject_reason: @reject_reason,
|
92
|
+
user_tag: @user_tag,
|
93
|
+
preflight_check_result: @preflight_check_result,
|
94
|
+
order_rule: @order_rule,
|
95
|
+
legs: @legs&.map(&:to_h),
|
96
|
+
remaining_quantity: remaining_quantity,
|
97
|
+
filled_quantity: filled_quantity
|
98
|
+
}.compact
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def parse_attributes
|
104
|
+
parse_basic_attributes
|
105
|
+
parse_order_details
|
106
|
+
parse_timestamps
|
107
|
+
parse_status_details
|
108
|
+
parse_legs
|
109
|
+
end
|
110
|
+
|
111
|
+
def parse_basic_attributes
|
112
|
+
@id = @data["id"]
|
113
|
+
@account_number = @data["account-number"]
|
114
|
+
@status = @data["status"]
|
115
|
+
@cancellable = @data["cancellable"]
|
116
|
+
@editable = @data["editable"]
|
117
|
+
@edited = @data["edited"]
|
118
|
+
end
|
119
|
+
|
120
|
+
def parse_order_details
|
121
|
+
@time_in_force = @data["time-in-force"]
|
122
|
+
@order_type = @data["order-type"]
|
123
|
+
@size = @data["size"]&.to_i
|
124
|
+
@price = parse_financial_value(@data["price"])
|
125
|
+
@price_effect = @data["price-effect"]
|
126
|
+
@underlying_symbol = @data["underlying-symbol"]
|
127
|
+
@underlying_instrument_type = @data["underlying-instrument-type"]
|
128
|
+
@stop_trigger = parse_financial_value(@data["stop-trigger"])
|
129
|
+
@gtc_date = parse_date(@data["gtc-date"])
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse_timestamps
|
133
|
+
@created_at = parse_time(@data["created-at"])
|
134
|
+
@updated_at = parse_time(@data["updated-at"])
|
135
|
+
@received_at = parse_time(@data["received-at"])
|
136
|
+
@routed_at = parse_time(@data["routed-at"])
|
137
|
+
@filled_at = parse_time(@data["filled-at"])
|
138
|
+
@cancelled_at = parse_time(@data["cancelled-at"])
|
139
|
+
@expired_at = parse_time(@data["expired-at"])
|
140
|
+
@rejected_at = parse_time(@data["rejected-at"])
|
141
|
+
@live_at = parse_time(@data["live-at"])
|
142
|
+
@terminal_at = parse_time(@data["terminal-at"])
|
143
|
+
end
|
144
|
+
|
145
|
+
def parse_status_details
|
146
|
+
@contingent_status = @data["contingent-status"]
|
147
|
+
@confirmation_status = @data["confirmation-status"]
|
148
|
+
@reject_reason = @data["reject-reason"]
|
149
|
+
@user_tag = @data["user-tag"]
|
150
|
+
@preflight_check_result = @data["preflight-check-result"]
|
151
|
+
@order_rule = @data["order-rule"]
|
152
|
+
end
|
153
|
+
|
154
|
+
def parse_legs
|
155
|
+
legs_data = @data["legs"] || []
|
156
|
+
@legs = legs_data.map { |leg| LiveOrderLeg.new(leg) }
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse_financial_value(value)
|
160
|
+
return nil if value.nil? || value.to_s.empty?
|
161
|
+
BigDecimal(value.to_s)
|
162
|
+
end
|
163
|
+
|
164
|
+
def parse_date(value)
|
165
|
+
return nil if value.nil? || value.to_s.empty?
|
166
|
+
Date.parse(value)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Represents a leg in a live order
|
171
|
+
class LiveOrderLeg < Base
|
172
|
+
attr_reader :symbol, :instrument_type, :action, :quantity,
|
173
|
+
:remaining_quantity, :fills, :fill_quantity, :fill_price,
|
174
|
+
:execution_price, :position_effect, :ratio_quantity
|
175
|
+
|
176
|
+
# Calculate filled quantity
|
177
|
+
def filled_quantity
|
178
|
+
return 0 if @quantity.nil? || @remaining_quantity.nil?
|
179
|
+
@quantity - @remaining_quantity
|
180
|
+
end
|
181
|
+
|
182
|
+
# Check if leg is completely filled
|
183
|
+
def filled?
|
184
|
+
@remaining_quantity.to_i == 0
|
185
|
+
end
|
186
|
+
|
187
|
+
# Check if leg is partially filled
|
188
|
+
def partially_filled?
|
189
|
+
!filled? && filled_quantity > 0
|
190
|
+
end
|
191
|
+
|
192
|
+
# Convert to hash for JSON serialization
|
193
|
+
def to_h
|
194
|
+
{
|
195
|
+
symbol: @symbol,
|
196
|
+
instrument_type: @instrument_type,
|
197
|
+
action: @action,
|
198
|
+
quantity: @quantity,
|
199
|
+
remaining_quantity: @remaining_quantity,
|
200
|
+
filled_quantity: filled_quantity,
|
201
|
+
fill_quantity: @fill_quantity,
|
202
|
+
fill_price: @fill_price&.to_s("F"),
|
203
|
+
execution_price: @execution_price&.to_s("F"),
|
204
|
+
position_effect: @position_effect,
|
205
|
+
ratio_quantity: @ratio_quantity,
|
206
|
+
fills: @fills&.map(&:to_h)
|
207
|
+
}.compact
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def parse_attributes
|
213
|
+
@symbol = @data["symbol"]
|
214
|
+
@instrument_type = @data["instrument-type"]
|
215
|
+
@action = @data["action"]
|
216
|
+
@quantity = @data["quantity"]&.to_i
|
217
|
+
@remaining_quantity = @data["remaining-quantity"]&.to_i
|
218
|
+
@fills = parse_fills(@data["fills"] || [])
|
219
|
+
@fill_quantity = @data["fill-quantity"]&.to_i
|
220
|
+
@fill_price = parse_financial_value(@data["fill-price"])
|
221
|
+
@execution_price = parse_financial_value(@data["execution-price"])
|
222
|
+
@position_effect = @data["position-effect"]
|
223
|
+
@ratio_quantity = @data["ratio-quantity"]&.to_i
|
224
|
+
end
|
225
|
+
|
226
|
+
def parse_fills(fills_data)
|
227
|
+
fills_data.map { |fill| Fill.new(fill) }
|
228
|
+
end
|
229
|
+
|
230
|
+
def parse_financial_value(value)
|
231
|
+
return nil if value.nil? || value.to_s.empty?
|
232
|
+
BigDecimal(value.to_s)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Represents a fill execution
|
237
|
+
class Fill < Base
|
238
|
+
attr_reader :ext_exec_id, :ext_group_fill_id, :fill_id, :quantity,
|
239
|
+
:fill_price, :filled_at, :destination_venue
|
240
|
+
|
241
|
+
# Convert to hash for JSON serialization
|
242
|
+
def to_h
|
243
|
+
{
|
244
|
+
ext_exec_id: @ext_exec_id,
|
245
|
+
ext_group_fill_id: @ext_group_fill_id,
|
246
|
+
fill_id: @fill_id,
|
247
|
+
quantity: @quantity,
|
248
|
+
fill_price: @fill_price&.to_s("F"),
|
249
|
+
filled_at: @filled_at&.iso8601,
|
250
|
+
destination_venue: @destination_venue
|
251
|
+
}.compact
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
def parse_attributes
|
257
|
+
@ext_exec_id = @data["ext-exec-id"]
|
258
|
+
@ext_group_fill_id = @data["ext-group-fill-id"]
|
259
|
+
@fill_id = @data["fill-id"]
|
260
|
+
@quantity = @data["quantity"]&.to_i
|
261
|
+
@fill_price = parse_financial_value(@data["fill-price"])
|
262
|
+
@filled_at = parse_time(@data["filled-at"])
|
263
|
+
@destination_venue = @data["destination-venue"]
|
264
|
+
end
|
265
|
+
|
266
|
+
def parse_financial_value(value)
|
267
|
+
return nil if value.nil? || value.to_s.empty?
|
268
|
+
BigDecimal(value.to_s)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bigdecimal"
|
4
|
+
|
5
|
+
module Tastytrade
|
6
|
+
module Models
|
7
|
+
# Represents the response from placing an order
|
8
|
+
class OrderResponse < Base
|
9
|
+
attr_reader :order_id, :buying_power_effect, :fee_calculations,
|
10
|
+
:warnings, :errors, :complex_order_id, :complex_order_tag,
|
11
|
+
:status, :account_number, :time_in_force, :order_type,
|
12
|
+
:price, :price_effect, :value, :value_effect,
|
13
|
+
:stop_trigger, :legs, :cancellable, :editable,
|
14
|
+
:edited, :updated_at, :created_at
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def parse_attributes
|
19
|
+
parse_basic_attributes
|
20
|
+
parse_financial_attributes
|
21
|
+
parse_order_details
|
22
|
+
parse_metadata
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_basic_attributes
|
26
|
+
@order_id = @data["id"]
|
27
|
+
@account_number = @data["account-number"]
|
28
|
+
@status = @data["status"]
|
29
|
+
@cancellable = @data["cancellable"]
|
30
|
+
@editable = @data["editable"]
|
31
|
+
@edited = @data["edited"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse_financial_attributes
|
35
|
+
# Handle both simple values and dry-run nested objects
|
36
|
+
@buying_power_effect = parse_buying_power_effect(@data["buying-power-effect"])
|
37
|
+
@fee_calculations = @data["fee-calculation"] || @data["fee-calculation-details"]
|
38
|
+
@price = parse_financial_value(@data["price"])
|
39
|
+
@price_effect = @data["price-effect"]
|
40
|
+
@value = parse_financial_value(@data["value"])
|
41
|
+
@value_effect = @data["value-effect"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_order_details
|
45
|
+
@time_in_force = @data["time-in-force"]
|
46
|
+
@order_type = @data["order-type"]
|
47
|
+
@stop_trigger = parse_financial_value(@data["stop-trigger"])
|
48
|
+
@complex_order_id = @data["complex-order-id"]
|
49
|
+
@complex_order_tag = @data["complex-order-tag"]
|
50
|
+
@legs = parse_legs(@data["legs"] || [])
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_metadata
|
54
|
+
@warnings = @data["warnings"] || []
|
55
|
+
@errors = @data["errors"] || []
|
56
|
+
@updated_at = parse_time(@data["updated-at"])
|
57
|
+
@created_at = parse_time(@data["created-at"])
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_financial_value(value)
|
61
|
+
return nil if value.nil? || value.to_s.empty?
|
62
|
+
BigDecimal(value.to_s)
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_legs(legs_data)
|
66
|
+
legs_data.map { |leg| OrderLegResponse.new(leg) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_buying_power_effect(value)
|
70
|
+
return nil if value.nil?
|
71
|
+
|
72
|
+
# Handle dry-run response format with nested object
|
73
|
+
if value.is_a?(Hash) && (value["change-in-buying-power"] || value["impact"])
|
74
|
+
# Create a full BuyingPowerEffect object for dry-run responses
|
75
|
+
BuyingPowerEffect.new(value)
|
76
|
+
else
|
77
|
+
# Handle regular response format (simple numeric value)
|
78
|
+
parse_financial_value(value)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Represents a leg in an order response
|
84
|
+
class OrderLegResponse < Base
|
85
|
+
attr_reader :action, :symbol, :quantity, :instrument_type,
|
86
|
+
:remaining_quantity, :fills, :execution_price
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def parse_attributes
|
91
|
+
@action = @data["action"]
|
92
|
+
@symbol = @data["symbol"]
|
93
|
+
@quantity = @data["quantity"]&.to_i
|
94
|
+
@instrument_type = @data["instrument-type"]
|
95
|
+
@remaining_quantity = @data["remaining-quantity"]&.to_i
|
96
|
+
@fills = @data["fills"] || []
|
97
|
+
@execution_price = parse_financial_value(@data["execution-price"])
|
98
|
+
end
|
99
|
+
|
100
|
+
def parse_financial_value(value)
|
101
|
+
return nil if value.nil? || value.to_s.empty?
|
102
|
+
BigDecimal(value.to_s)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tastytrade
|
4
|
+
module Models
|
5
|
+
# Order status constants and helpers
|
6
|
+
module OrderStatus
|
7
|
+
# Submission phase statuses
|
8
|
+
RECEIVED = "Received"
|
9
|
+
ROUTED = "Routed"
|
10
|
+
IN_FLIGHT = "In Flight"
|
11
|
+
CONTINGENT = "Contingent"
|
12
|
+
|
13
|
+
# Working phase statuses
|
14
|
+
LIVE = "Live"
|
15
|
+
CANCEL_REQUESTED = "Cancel Requested"
|
16
|
+
REPLACE_REQUESTED = "Replace Requested"
|
17
|
+
|
18
|
+
# Terminal phase statuses
|
19
|
+
FILLED = "Filled"
|
20
|
+
CANCELLED = "Cancelled"
|
21
|
+
REJECTED = "Rejected"
|
22
|
+
EXPIRED = "Expired"
|
23
|
+
REMOVED = "Removed"
|
24
|
+
|
25
|
+
# Status groupings
|
26
|
+
SUBMISSION_STATUSES = [
|
27
|
+
RECEIVED,
|
28
|
+
ROUTED,
|
29
|
+
IN_FLIGHT,
|
30
|
+
CONTINGENT
|
31
|
+
].freeze
|
32
|
+
|
33
|
+
WORKING_STATUSES = [
|
34
|
+
LIVE,
|
35
|
+
CANCEL_REQUESTED,
|
36
|
+
REPLACE_REQUESTED
|
37
|
+
].freeze
|
38
|
+
|
39
|
+
TERMINAL_STATUSES = [
|
40
|
+
FILLED,
|
41
|
+
CANCELLED,
|
42
|
+
REJECTED,
|
43
|
+
EXPIRED,
|
44
|
+
REMOVED
|
45
|
+
].freeze
|
46
|
+
|
47
|
+
ALL_STATUSES = (
|
48
|
+
SUBMISSION_STATUSES +
|
49
|
+
WORKING_STATUSES +
|
50
|
+
TERMINAL_STATUSES
|
51
|
+
).freeze
|
52
|
+
|
53
|
+
# Check if status is in submission phase
|
54
|
+
def self.submission?(status)
|
55
|
+
SUBMISSION_STATUSES.include?(status)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if status is in working phase
|
59
|
+
def self.working?(status)
|
60
|
+
WORKING_STATUSES.include?(status)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Check if status is terminal
|
64
|
+
def self.terminal?(status)
|
65
|
+
TERMINAL_STATUSES.include?(status)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Check if status allows cancellation
|
69
|
+
def self.cancellable?(status)
|
70
|
+
status == LIVE
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check if status allows replacement
|
74
|
+
def self.editable?(status)
|
75
|
+
status == LIVE
|
76
|
+
end
|
77
|
+
|
78
|
+
# Validate status value
|
79
|
+
def self.valid?(status)
|
80
|
+
ALL_STATUSES.include?(status)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bigdecimal"
|
4
|
+
require "date"
|
5
|
+
|
6
|
+
module Tastytrade
|
7
|
+
module Models
|
8
|
+
# Represents the trading status and permissions for an account
|
9
|
+
#
|
10
|
+
# @attr_reader [String] account_number The account number
|
11
|
+
# @attr_reader [String] equities_margin_calculation_type Type of margin calculation for equities
|
12
|
+
# @attr_reader [String] fee_schedule_name Fee schedule applied to the account
|
13
|
+
# @attr_reader [BigDecimal] futures_margin_rate_multiplier Margin rate multiplier for futures
|
14
|
+
# @attr_reader [Boolean] has_intraday_equities_margin Whether intraday equities margin is enabled
|
15
|
+
# @attr_reader [Integer] id Trading status record ID
|
16
|
+
# @attr_reader [Boolean] is_aggregated_at_clearing Whether account is aggregated at clearing
|
17
|
+
# @attr_reader [Boolean] is_closed Whether the account is closed
|
18
|
+
# @attr_reader [Boolean] is_closing_only Whether account is restricted to closing trades only
|
19
|
+
# @attr_reader [Boolean] is_cryptocurrency_enabled Whether cryptocurrency trading is enabled
|
20
|
+
# @attr_reader [Boolean] is_frozen Whether the account is frozen
|
21
|
+
# @attr_reader [Boolean] is_full_equity_margin_required Whether full equity margin is required
|
22
|
+
# @attr_reader [Boolean] is_futures_closing_only Whether futures are restricted to closing only
|
23
|
+
# @attr_reader [Boolean] is_futures_intra_day_enabled Whether intraday futures trading is enabled
|
24
|
+
# @attr_reader [Boolean] is_futures_enabled Whether futures trading is enabled
|
25
|
+
# @attr_reader [Boolean] is_in_day_trade_equity_maintenance_call Whether account is in day trade equity
|
26
|
+
# maintenance call
|
27
|
+
# @attr_reader [Boolean] is_in_margin_call Whether account is in margin call
|
28
|
+
# @attr_reader [Boolean] is_pattern_day_trader Whether account is flagged as pattern day trader
|
29
|
+
# @attr_reader [Boolean] is_small_notional_futures_intra_day_enabled Whether small notional futures
|
30
|
+
# intraday is enabled
|
31
|
+
# @attr_reader [Boolean] is_roll_the_day_forward_enabled Whether roll the day forward is enabled
|
32
|
+
# @attr_reader [Boolean] are_far_otm_net_options_restricted Whether far OTM net options are restricted
|
33
|
+
# @attr_reader [String] options_level Options trading permission level
|
34
|
+
# @attr_reader [Boolean] short_calls_enabled Whether short calls are enabled
|
35
|
+
# @attr_reader [BigDecimal] small_notional_futures_margin_rate_multiplier Margin rate multiplier for
|
36
|
+
# small notional futures
|
37
|
+
# @attr_reader [Boolean] is_equity_offering_enabled Whether equity offerings are enabled
|
38
|
+
# @attr_reader [Boolean] is_equity_offering_closing_only Whether equity offerings are closing only
|
39
|
+
# @attr_reader [Time] updated_at When the trading status was last updated
|
40
|
+
# @attr_reader [Boolean, nil] is_portfolio_margin_enabled Whether portfolio margin is enabled (optional)
|
41
|
+
# @attr_reader [Boolean, nil] is_risk_reducing_only Whether only risk-reducing trades are allowed (optional)
|
42
|
+
# @attr_reader [Integer, nil] day_trade_count Current day trade count (optional)
|
43
|
+
# @attr_reader [String, nil] autotrade_account_type Type of autotrade account (optional)
|
44
|
+
# @attr_reader [String, nil] clearing_account_number Clearing account number (optional)
|
45
|
+
# @attr_reader [String, nil] clearing_aggregation_identifier Clearing aggregation identifier (optional)
|
46
|
+
# @attr_reader [Boolean, nil] is_cryptocurrency_closing_only Whether crypto is closing only (optional)
|
47
|
+
# @attr_reader [Date, nil] pdt_reset_on Date when PDT flag will reset (optional)
|
48
|
+
# @attr_reader [Integer, nil] cmta_override CMTA override value (optional)
|
49
|
+
# @attr_reader [Time, nil] enhanced_fraud_safeguards_enabled_at When enhanced fraud safeguards were
|
50
|
+
# enabled (optional)
|
51
|
+
class TradingStatus < Base
|
52
|
+
attr_reader :account_number, :equities_margin_calculation_type, :fee_schedule_name,
|
53
|
+
:futures_margin_rate_multiplier, :has_intraday_equities_margin, :id,
|
54
|
+
:is_aggregated_at_clearing, :is_closed, :is_closing_only,
|
55
|
+
:is_cryptocurrency_enabled, :is_frozen, :is_full_equity_margin_required,
|
56
|
+
:is_futures_closing_only, :is_futures_intra_day_enabled, :is_futures_enabled,
|
57
|
+
:is_in_day_trade_equity_maintenance_call, :is_in_margin_call,
|
58
|
+
:is_pattern_day_trader, :is_small_notional_futures_intra_day_enabled,
|
59
|
+
:is_roll_the_day_forward_enabled, :are_far_otm_net_options_restricted,
|
60
|
+
:options_level, :short_calls_enabled,
|
61
|
+
:small_notional_futures_margin_rate_multiplier,
|
62
|
+
:is_equity_offering_enabled, :is_equity_offering_closing_only,
|
63
|
+
:updated_at, :is_portfolio_margin_enabled, :is_risk_reducing_only,
|
64
|
+
:day_trade_count, :autotrade_account_type, :clearing_account_number,
|
65
|
+
:clearing_aggregation_identifier, :is_cryptocurrency_closing_only,
|
66
|
+
:pdt_reset_on, :cmta_override, :enhanced_fraud_safeguards_enabled_at
|
67
|
+
|
68
|
+
# Check if account can trade options at any level
|
69
|
+
#
|
70
|
+
# @return [Boolean] true if options trading is enabled
|
71
|
+
def can_trade_options?
|
72
|
+
!options_level.nil? && options_level != "No Permissions"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check if account can trade futures
|
76
|
+
#
|
77
|
+
# @return [Boolean] true if futures trading is enabled and not closing only
|
78
|
+
def can_trade_futures?
|
79
|
+
is_futures_enabled && !is_futures_closing_only
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check if account can trade cryptocurrency
|
83
|
+
#
|
84
|
+
# @return [Boolean] true if crypto trading is enabled and not closing only
|
85
|
+
def can_trade_cryptocurrency?
|
86
|
+
is_cryptocurrency_enabled && !is_cryptocurrency_closing_only
|
87
|
+
end
|
88
|
+
|
89
|
+
# Check if account has any trading restrictions
|
90
|
+
#
|
91
|
+
# @return [Boolean] true if account has restrictions
|
92
|
+
def restricted?
|
93
|
+
is_closed || is_frozen || is_closing_only || is_in_margin_call ||
|
94
|
+
is_in_day_trade_equity_maintenance_call || is_risk_reducing_only == true
|
95
|
+
end
|
96
|
+
|
97
|
+
# Get a list of active restrictions
|
98
|
+
#
|
99
|
+
# @return [Array<String>] list of active restrictions
|
100
|
+
def active_restrictions
|
101
|
+
restrictions = []
|
102
|
+
restrictions << "Account Closed" if is_closed
|
103
|
+
restrictions << "Account Frozen" if is_frozen
|
104
|
+
restrictions << "Closing Only" if is_closing_only
|
105
|
+
restrictions << "Margin Call" if is_in_margin_call
|
106
|
+
restrictions << "Day Trade Equity Maintenance Call" if is_in_day_trade_equity_maintenance_call
|
107
|
+
restrictions << "Risk Reducing Only" if is_risk_reducing_only
|
108
|
+
restrictions << "Pattern Day Trader" if is_pattern_day_trader
|
109
|
+
restrictions << "Futures Closing Only" if is_futures_closing_only
|
110
|
+
restrictions << "Cryptocurrency Closing Only" if is_cryptocurrency_closing_only
|
111
|
+
restrictions << "Equity Offering Closing Only" if is_equity_offering_closing_only
|
112
|
+
restrictions << "Far OTM Net Options Restricted" if are_far_otm_net_options_restricted
|
113
|
+
restrictions
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get trading permissions summary
|
117
|
+
#
|
118
|
+
# @return [Hash] summary of trading permissions
|
119
|
+
def permissions_summary
|
120
|
+
{
|
121
|
+
options: can_trade_options? ? options_level : "Disabled",
|
122
|
+
futures: can_trade_futures? ? "Enabled" : (is_futures_enabled ? "Closing Only" : "Disabled"),
|
123
|
+
cryptocurrency: crypto_status,
|
124
|
+
short_calls: short_calls_enabled ? "Enabled" : "Disabled",
|
125
|
+
pattern_day_trader: is_pattern_day_trader ? "Yes" : "No",
|
126
|
+
portfolio_margin: is_portfolio_margin_enabled ? "Enabled" : "Disabled"
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def crypto_status
|
131
|
+
if can_trade_cryptocurrency?
|
132
|
+
"Enabled"
|
133
|
+
elsif is_cryptocurrency_enabled
|
134
|
+
"Closing Only"
|
135
|
+
else
|
136
|
+
"Disabled"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def parse_attributes
|
143
|
+
@account_number = @data["account-number"]
|
144
|
+
@equities_margin_calculation_type = @data["equities-margin-calculation-type"]
|
145
|
+
@fee_schedule_name = @data["fee-schedule-name"]
|
146
|
+
@futures_margin_rate_multiplier = parse_decimal(@data["futures-margin-rate-multiplier"])
|
147
|
+
@has_intraday_equities_margin = @data["has-intraday-equities-margin"]
|
148
|
+
@id = @data["id"]
|
149
|
+
@is_aggregated_at_clearing = @data["is-aggregated-at-clearing"]
|
150
|
+
@is_closed = @data["is-closed"]
|
151
|
+
@is_closing_only = @data["is-closing-only"]
|
152
|
+
@is_cryptocurrency_enabled = @data["is-cryptocurrency-enabled"]
|
153
|
+
@is_frozen = @data["is-frozen"]
|
154
|
+
@is_full_equity_margin_required = @data["is-full-equity-margin-required"]
|
155
|
+
@is_futures_closing_only = @data["is-futures-closing-only"]
|
156
|
+
@is_futures_intra_day_enabled = @data["is-futures-intra-day-enabled"]
|
157
|
+
@is_futures_enabled = @data["is-futures-enabled"]
|
158
|
+
@is_in_day_trade_equity_maintenance_call = @data["is-in-day-trade-equity-maintenance-call"]
|
159
|
+
@is_in_margin_call = @data["is-in-margin-call"]
|
160
|
+
@is_pattern_day_trader = @data["is-pattern-day-trader"]
|
161
|
+
@is_small_notional_futures_intra_day_enabled = @data["is-small-notional-futures-intra-day-enabled"]
|
162
|
+
@is_roll_the_day_forward_enabled = @data["is-roll-the-day-forward-enabled"]
|
163
|
+
@are_far_otm_net_options_restricted = @data["are-far-otm-net-options-restricted"]
|
164
|
+
@options_level = @data["options-level"]
|
165
|
+
@short_calls_enabled = @data["short-calls-enabled"]
|
166
|
+
@small_notional_futures_margin_rate_multiplier =
|
167
|
+
parse_decimal(@data["small-notional-futures-margin-rate-multiplier"])
|
168
|
+
@is_equity_offering_enabled = @data["is-equity-offering-enabled"]
|
169
|
+
@is_equity_offering_closing_only = @data["is-equity-offering-closing-only"]
|
170
|
+
@updated_at = parse_time(@data["updated-at"])
|
171
|
+
|
172
|
+
# Optional fields
|
173
|
+
@is_portfolio_margin_enabled = @data["is-portfolio-margin-enabled"]
|
174
|
+
@is_risk_reducing_only = @data["is-risk-reducing-only"]
|
175
|
+
@day_trade_count = @data["day-trade-count"]
|
176
|
+
@autotrade_account_type = @data["autotrade-account-type"]
|
177
|
+
@clearing_account_number = @data["clearing-account-number"]
|
178
|
+
@clearing_aggregation_identifier = @data["clearing-aggregation-identifier"]
|
179
|
+
@is_cryptocurrency_closing_only = @data["is-cryptocurrency-closing-only"]
|
180
|
+
@pdt_reset_on = parse_date(@data["pdt-reset-on"]) if @data["pdt-reset-on"]
|
181
|
+
@cmta_override = @data["cmta-override"]
|
182
|
+
@enhanced_fraud_safeguards_enabled_at = parse_time(@data["enhanced-fraud-safeguards-enabled-at"])
|
183
|
+
end
|
184
|
+
|
185
|
+
def parse_date(date_string)
|
186
|
+
return nil if date_string.nil?
|
187
|
+
|
188
|
+
Date.parse(date_string)
|
189
|
+
rescue ArgumentError
|
190
|
+
nil
|
191
|
+
end
|
192
|
+
|
193
|
+
def parse_decimal(value)
|
194
|
+
return nil if value.nil? || value.to_s.empty?
|
195
|
+
|
196
|
+
BigDecimal(value.to_s)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|