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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/plan.md +13 -0
  3. data/.claude/commands/release-pr.md +12 -0
  4. data/CHANGELOG.md +170 -0
  5. data/README.md +424 -3
  6. data/ROADMAP.md +17 -17
  7. data/lib/tastytrade/cli/history_formatter.rb +304 -0
  8. data/lib/tastytrade/cli/orders.rb +749 -0
  9. data/lib/tastytrade/cli/positions_formatter.rb +114 -0
  10. data/lib/tastytrade/cli.rb +701 -12
  11. data/lib/tastytrade/cli_helpers.rb +111 -14
  12. data/lib/tastytrade/client.rb +7 -0
  13. data/lib/tastytrade/file_store.rb +83 -0
  14. data/lib/tastytrade/instruments/equity.rb +42 -0
  15. data/lib/tastytrade/models/account.rb +160 -2
  16. data/lib/tastytrade/models/account_balance.rb +46 -0
  17. data/lib/tastytrade/models/buying_power_effect.rb +61 -0
  18. data/lib/tastytrade/models/live_order.rb +272 -0
  19. data/lib/tastytrade/models/order_response.rb +106 -0
  20. data/lib/tastytrade/models/order_status.rb +84 -0
  21. data/lib/tastytrade/models/trading_status.rb +200 -0
  22. data/lib/tastytrade/models/transaction.rb +151 -0
  23. data/lib/tastytrade/models.rb +6 -0
  24. data/lib/tastytrade/order.rb +191 -0
  25. data/lib/tastytrade/order_validator.rb +355 -0
  26. data/lib/tastytrade/session.rb +26 -1
  27. data/lib/tastytrade/session_manager.rb +43 -14
  28. data/lib/tastytrade/version.rb +1 -1
  29. data/lib/tastytrade.rb +43 -0
  30. data/spec/exe/tastytrade_spec.rb +1 -1
  31. data/spec/spec_helper.rb +72 -0
  32. data/spec/tastytrade/cli/positions_spec.rb +267 -0
  33. data/spec/tastytrade/cli_auth_spec.rb +5 -0
  34. data/spec/tastytrade/cli_env_login_spec.rb +199 -0
  35. data/spec/tastytrade/cli_helpers_spec.rb +3 -26
  36. data/spec/tastytrade/cli_orders_spec.rb +168 -0
  37. data/spec/tastytrade/cli_status_spec.rb +153 -164
  38. data/spec/tastytrade/file_store_spec.rb +126 -0
  39. data/spec/tastytrade/models/account_balance_spec.rb +103 -0
  40. data/spec/tastytrade/models/account_order_history_spec.rb +229 -0
  41. data/spec/tastytrade/models/account_order_management_spec.rb +271 -0
  42. data/spec/tastytrade/models/account_place_order_spec.rb +125 -0
  43. data/spec/tastytrade/models/account_spec.rb +86 -15
  44. data/spec/tastytrade/models/buying_power_effect_spec.rb +250 -0
  45. data/spec/tastytrade/models/live_order_json_spec.rb +144 -0
  46. data/spec/tastytrade/models/live_order_spec.rb +295 -0
  47. data/spec/tastytrade/models/order_response_spec.rb +96 -0
  48. data/spec/tastytrade/models/order_status_spec.rb +113 -0
  49. data/spec/tastytrade/models/trading_status_spec.rb +260 -0
  50. data/spec/tastytrade/models/transaction_spec.rb +236 -0
  51. data/spec/tastytrade/order_edge_cases_spec.rb +163 -0
  52. data/spec/tastytrade/order_spec.rb +201 -0
  53. data/spec/tastytrade/order_validator_spec.rb +347 -0
  54. data/spec/tastytrade/session_env_spec.rb +169 -0
  55. data/spec/tastytrade/session_manager_spec.rb +43 -33
  56. data/vcr_implementation_plan.md +403 -0
  57. data/vcr_implementation_research.md +330 -0
  58. metadata +50 -18
  59. data/lib/tastytrade/keyring_store.rb +0 -72
  60. data/spec/tastytrade/keyring_store_spec.rb +0 -168
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Tastytrade::Models::LiveOrder do
6
+ let(:live_order_data) do
7
+ {
8
+ "id" => "12345",
9
+ "account-number" => "5WV12345",
10
+ "status" => "Live",
11
+ "cancellable" => true,
12
+ "editable" => true,
13
+ "edited" => false,
14
+ "time-in-force" => "Day",
15
+ "order-type" => "Limit",
16
+ "size" => 100,
17
+ "price" => "150.50",
18
+ "price-effect" => "Debit",
19
+ "underlying-symbol" => "AAPL",
20
+ "underlying-instrument-type" => "Equity",
21
+ "created-at" => "2024-01-15T09:30:00.000Z",
22
+ "updated-at" => "2024-01-15T09:30:05.000Z",
23
+ "received-at" => "2024-01-15T09:30:00.000Z",
24
+ "live-at" => "2024-01-15T09:30:05.000Z",
25
+ "legs" => [
26
+ {
27
+ "symbol" => "AAPL",
28
+ "instrument-type" => "Equity",
29
+ "action" => "Buy",
30
+ "quantity" => 100,
31
+ "remaining-quantity" => 100,
32
+ "fills" => []
33
+ }
34
+ ]
35
+ }
36
+ end
37
+
38
+ let(:filled_order_data) do
39
+ live_order_data.merge(
40
+ "status" => "Filled",
41
+ "cancellable" => false,
42
+ "editable" => false,
43
+ "filled-at" => "2024-01-15T09:35:00.000Z",
44
+ "terminal-at" => "2024-01-15T09:35:00.000Z",
45
+ "legs" => [
46
+ {
47
+ "symbol" => "AAPL",
48
+ "instrument-type" => "Equity",
49
+ "action" => "Buy",
50
+ "quantity" => 100,
51
+ "remaining-quantity" => 0,
52
+ "fill-quantity" => 100,
53
+ "fill-price" => "150.45",
54
+ "fills" => [
55
+ {
56
+ "ext-exec-id" => "exec123",
57
+ "fill-id" => "fill123",
58
+ "quantity" => 100,
59
+ "fill-price" => "150.45",
60
+ "filled-at" => "2024-01-15T09:35:00.000Z"
61
+ }
62
+ ]
63
+ }
64
+ ]
65
+ )
66
+ end
67
+
68
+ let(:partially_filled_order_data) do
69
+ live_order_data.merge(
70
+ "legs" => [
71
+ {
72
+ "symbol" => "AAPL",
73
+ "instrument-type" => "Equity",
74
+ "action" => "Buy",
75
+ "quantity" => 100,
76
+ "remaining-quantity" => 60,
77
+ "fill-quantity" => 40,
78
+ "fill-price" => "150.48",
79
+ "fills" => [
80
+ {
81
+ "ext-exec-id" => "exec124",
82
+ "fill-id" => "fill124",
83
+ "quantity" => 40,
84
+ "fill-price" => "150.48",
85
+ "filled-at" => "2024-01-15T09:32:00.000Z"
86
+ }
87
+ ]
88
+ }
89
+ ]
90
+ )
91
+ end
92
+
93
+ describe "#initialize" do
94
+ subject(:live_order) { described_class.new(live_order_data) }
95
+
96
+ it "parses basic attributes correctly" do
97
+ expect(live_order.id).to eq("12345")
98
+ expect(live_order.account_number).to eq("5WV12345")
99
+ expect(live_order.status).to eq("Live")
100
+ expect(live_order.cancellable).to be true
101
+ expect(live_order.editable).to be true
102
+ expect(live_order.edited).to be false
103
+ end
104
+
105
+ it "parses order details correctly" do
106
+ expect(live_order.time_in_force).to eq("Day")
107
+ expect(live_order.order_type).to eq("Limit")
108
+ expect(live_order.size).to eq(100)
109
+ expect(live_order.price).to eq(BigDecimal("150.50"))
110
+ expect(live_order.price_effect).to eq("Debit")
111
+ expect(live_order.underlying_symbol).to eq("AAPL")
112
+ expect(live_order.underlying_instrument_type).to eq("Equity")
113
+ end
114
+
115
+ it "parses timestamps correctly" do
116
+ expect(live_order.created_at).to be_a(Time)
117
+ expect(live_order.updated_at).to be_a(Time)
118
+ expect(live_order.received_at).to be_a(Time)
119
+ expect(live_order.live_at).to be_a(Time)
120
+ end
121
+
122
+ it "parses legs correctly" do
123
+ expect(live_order.legs).to be_an(Array)
124
+ expect(live_order.legs.size).to eq(1)
125
+ leg = live_order.legs.first
126
+ expect(leg).to be_a(Tastytrade::Models::LiveOrderLeg)
127
+ expect(leg.symbol).to eq("AAPL")
128
+ expect(leg.action).to eq("Buy")
129
+ expect(leg.quantity).to eq(100)
130
+ expect(leg.remaining_quantity).to eq(100)
131
+ end
132
+ end
133
+
134
+ describe "status check methods" do
135
+ context "with a live order" do
136
+ subject(:live_order) { described_class.new(live_order_data) }
137
+
138
+ it "#cancellable? returns true" do
139
+ expect(live_order.cancellable?).to be true
140
+ end
141
+
142
+ it "#editable? returns true" do
143
+ expect(live_order.editable?).to be true
144
+ end
145
+
146
+ it "#terminal? returns false" do
147
+ expect(live_order.terminal?).to be false
148
+ end
149
+
150
+ it "#working? returns true" do
151
+ expect(live_order.working?).to be true
152
+ end
153
+
154
+ it "#filled? returns false" do
155
+ expect(live_order.filled?).to be false
156
+ end
157
+
158
+ it "#cancelled? returns false" do
159
+ expect(live_order.cancelled?).to be false
160
+ end
161
+ end
162
+
163
+ context "with a filled order" do
164
+ subject(:filled_order) { described_class.new(filled_order_data) }
165
+
166
+ it "#cancellable? returns false" do
167
+ expect(filled_order.cancellable?).to be false
168
+ end
169
+
170
+ it "#editable? returns false" do
171
+ expect(filled_order.editable?).to be false
172
+ end
173
+
174
+ it "#terminal? returns true" do
175
+ expect(filled_order.terminal?).to be true
176
+ end
177
+
178
+ it "#working? returns false" do
179
+ expect(filled_order.working?).to be false
180
+ end
181
+
182
+ it "#filled? returns true" do
183
+ expect(filled_order.filled?).to be true
184
+ end
185
+ end
186
+ end
187
+
188
+ describe "quantity methods" do
189
+ context "with an unfilled order" do
190
+ subject(:order) { described_class.new(live_order_data) }
191
+
192
+ it "#remaining_quantity returns total remaining" do
193
+ expect(order.remaining_quantity).to eq(100)
194
+ end
195
+
196
+ it "#filled_quantity returns 0" do
197
+ expect(order.filled_quantity).to eq(0)
198
+ end
199
+ end
200
+
201
+ context "with a partially filled order" do
202
+ subject(:order) { described_class.new(partially_filled_order_data) }
203
+
204
+ it "#remaining_quantity returns correct value" do
205
+ expect(order.remaining_quantity).to eq(60)
206
+ end
207
+
208
+ it "#filled_quantity returns correct value" do
209
+ expect(order.filled_quantity).to eq(40)
210
+ end
211
+ end
212
+
213
+ context "with a filled order" do
214
+ subject(:order) { described_class.new(filled_order_data) }
215
+
216
+ it "#remaining_quantity returns 0" do
217
+ expect(order.remaining_quantity).to eq(0)
218
+ end
219
+
220
+ it "#filled_quantity returns total quantity" do
221
+ expect(order.filled_quantity).to eq(100)
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ RSpec.describe Tastytrade::Models::LiveOrderLeg do
228
+ let(:unfilled_leg_data) do
229
+ {
230
+ "symbol" => "AAPL",
231
+ "instrument-type" => "Equity",
232
+ "action" => "Buy",
233
+ "quantity" => 100,
234
+ "remaining-quantity" => 100,
235
+ "fills" => []
236
+ }
237
+ end
238
+
239
+ let(:partially_filled_leg_data) do
240
+ {
241
+ "symbol" => "AAPL",
242
+ "instrument-type" => "Equity",
243
+ "action" => "Buy",
244
+ "quantity" => 100,
245
+ "remaining-quantity" => 60,
246
+ "fill-quantity" => 40,
247
+ "fill-price" => "150.48",
248
+ "fills" => [
249
+ {
250
+ "ext-exec-id" => "exec124",
251
+ "fill-id" => "fill124",
252
+ "quantity" => 40,
253
+ "fill-price" => "150.48",
254
+ "filled-at" => "2024-01-15T09:32:00.000Z"
255
+ }
256
+ ]
257
+ }
258
+ end
259
+
260
+ describe "#filled_quantity" do
261
+ it "calculates correctly for unfilled leg" do
262
+ leg = described_class.new(unfilled_leg_data)
263
+ expect(leg.filled_quantity).to eq(0)
264
+ end
265
+
266
+ it "calculates correctly for partially filled leg" do
267
+ leg = described_class.new(partially_filled_leg_data)
268
+ expect(leg.filled_quantity).to eq(40)
269
+ end
270
+ end
271
+
272
+ describe "#filled?" do
273
+ it "returns false for unfilled leg" do
274
+ leg = described_class.new(unfilled_leg_data)
275
+ expect(leg.filled?).to be false
276
+ end
277
+
278
+ it "returns false for partially filled leg" do
279
+ leg = described_class.new(partially_filled_leg_data)
280
+ expect(leg.filled?).to be false
281
+ end
282
+ end
283
+
284
+ describe "#partially_filled?" do
285
+ it "returns false for unfilled leg" do
286
+ leg = described_class.new(unfilled_leg_data)
287
+ expect(leg.partially_filled?).to be false
288
+ end
289
+
290
+ it "returns true for partially filled leg" do
291
+ leg = described_class.new(partially_filled_leg_data)
292
+ expect(leg.partially_filled?).to be true
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Tastytrade::Models::OrderResponse do
4
+ let(:order_response_data) do
5
+ {
6
+ "id" => "123456",
7
+ "account-number" => "5WX12345",
8
+ "status" => "Filled",
9
+ "buying-power-effect" => "-15050.00",
10
+ "fee-calculation" => {
11
+ "total-fees" => "0.65",
12
+ "regulatory-fees" => "0.01"
13
+ },
14
+ "price" => "150.50",
15
+ "price-effect" => "Debit",
16
+ "value" => "-15050.00",
17
+ "value-effect" => "Debit",
18
+ "time-in-force" => "Day",
19
+ "order-type" => "Limit",
20
+ "cancellable" => false,
21
+ "editable" => false,
22
+ "edited" => false,
23
+ "warnings" => [],
24
+ "errors" => [],
25
+ "legs" => [
26
+ {
27
+ "action" => "Buy to Open",
28
+ "symbol" => "AAPL",
29
+ "quantity" => 100,
30
+ "instrument-type" => "Equity",
31
+ "remaining-quantity" => 0,
32
+ "fills" => [],
33
+ "execution-price" => "150.50"
34
+ }
35
+ ],
36
+ "updated-at" => "2023-01-01T10:00:00Z",
37
+ "created-at" => "2023-01-01T09:59:00Z"
38
+ }
39
+ end
40
+
41
+ describe "#initialize" do
42
+ it "parses all order response attributes" do
43
+ response = described_class.new(order_response_data)
44
+
45
+ expect(response.order_id).to eq("123456")
46
+ expect(response.account_number).to eq("5WX12345")
47
+ expect(response.status).to eq("Filled")
48
+ expect(response.buying_power_effect).to eq(BigDecimal("-15050.00"))
49
+ expect(response.fee_calculations).to be_a(Hash)
50
+ expect(response.price).to eq(BigDecimal("150.50"))
51
+ expect(response.price_effect).to eq("Debit")
52
+ expect(response.value).to eq(BigDecimal("-15050.00"))
53
+ expect(response.value_effect).to eq("Debit")
54
+ expect(response.time_in_force).to eq("Day")
55
+ expect(response.order_type).to eq("Limit")
56
+ expect(response.cancellable).to be false
57
+ expect(response.editable).to be false
58
+ expect(response.edited).to be false
59
+ expect(response.warnings).to be_empty
60
+ expect(response.errors).to be_empty
61
+ expect(response.legs).to be_an(Array)
62
+ expect(response.updated_at).to be_a(Time)
63
+ expect(response.created_at).to be_a(Time)
64
+ end
65
+
66
+ it "handles missing optional fields" do
67
+ minimal_data = {
68
+ "id" => "123456",
69
+ "account-number" => "5WX12345",
70
+ "status" => "Pending"
71
+ }
72
+
73
+ response = described_class.new(minimal_data)
74
+ expect(response.order_id).to eq("123456")
75
+ expect(response.buying_power_effect).to be_nil
76
+ expect(response.price).to be_nil
77
+ expect(response.warnings).to be_empty
78
+ expect(response.legs).to be_empty
79
+ end
80
+ end
81
+
82
+ describe "leg parsing" do
83
+ it "parses order legs correctly" do
84
+ response = described_class.new(order_response_data)
85
+ leg = response.legs.first
86
+
87
+ expect(leg).to be_a(Tastytrade::Models::OrderLegResponse)
88
+ expect(leg.action).to eq("Buy to Open")
89
+ expect(leg.symbol).to eq("AAPL")
90
+ expect(leg.quantity).to eq(100)
91
+ expect(leg.instrument_type).to eq("Equity")
92
+ expect(leg.remaining_quantity).to eq(0)
93
+ expect(leg.execution_price).to eq(BigDecimal("150.50"))
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Tastytrade::Models::OrderStatus do
6
+ describe "constants" do
7
+ it "defines submission phase statuses" do
8
+ expect(described_class::RECEIVED).to eq("Received")
9
+ expect(described_class::ROUTED).to eq("Routed")
10
+ expect(described_class::IN_FLIGHT).to eq("In Flight")
11
+ expect(described_class::CONTINGENT).to eq("Contingent")
12
+ end
13
+
14
+ it "defines working phase statuses" do
15
+ expect(described_class::LIVE).to eq("Live")
16
+ expect(described_class::CANCEL_REQUESTED).to eq("Cancel Requested")
17
+ expect(described_class::REPLACE_REQUESTED).to eq("Replace Requested")
18
+ end
19
+
20
+ it "defines terminal phase statuses" do
21
+ expect(described_class::FILLED).to eq("Filled")
22
+ expect(described_class::CANCELLED).to eq("Cancelled")
23
+ expect(described_class::REJECTED).to eq("Rejected")
24
+ expect(described_class::EXPIRED).to eq("Expired")
25
+ expect(described_class::REMOVED).to eq("Removed")
26
+ end
27
+ end
28
+
29
+ describe ".submission?" do
30
+ it "returns true for submission statuses" do
31
+ expect(described_class.submission?("Received")).to be true
32
+ expect(described_class.submission?("Routed")).to be true
33
+ expect(described_class.submission?("In Flight")).to be true
34
+ expect(described_class.submission?("Contingent")).to be true
35
+ end
36
+
37
+ it "returns false for non-submission statuses" do
38
+ expect(described_class.submission?("Live")).to be false
39
+ expect(described_class.submission?("Filled")).to be false
40
+ expect(described_class.submission?("Cancelled")).to be false
41
+ end
42
+ end
43
+
44
+ describe ".working?" do
45
+ it "returns true for working statuses" do
46
+ expect(described_class.working?("Live")).to be true
47
+ expect(described_class.working?("Cancel Requested")).to be true
48
+ expect(described_class.working?("Replace Requested")).to be true
49
+ end
50
+
51
+ it "returns false for non-working statuses" do
52
+ expect(described_class.working?("Received")).to be false
53
+ expect(described_class.working?("Filled")).to be false
54
+ expect(described_class.working?("Cancelled")).to be false
55
+ end
56
+ end
57
+
58
+ describe ".terminal?" do
59
+ it "returns true for terminal statuses" do
60
+ expect(described_class.terminal?("Filled")).to be true
61
+ expect(described_class.terminal?("Cancelled")).to be true
62
+ expect(described_class.terminal?("Rejected")).to be true
63
+ expect(described_class.terminal?("Expired")).to be true
64
+ expect(described_class.terminal?("Removed")).to be true
65
+ end
66
+
67
+ it "returns false for non-terminal statuses" do
68
+ expect(described_class.terminal?("Live")).to be false
69
+ expect(described_class.terminal?("Received")).to be false
70
+ expect(described_class.terminal?("Routed")).to be false
71
+ end
72
+ end
73
+
74
+ describe ".cancellable?" do
75
+ it "returns true only for Live status" do
76
+ expect(described_class.cancellable?("Live")).to be true
77
+ end
78
+
79
+ it "returns false for other statuses" do
80
+ expect(described_class.cancellable?("Received")).to be false
81
+ expect(described_class.cancellable?("Filled")).to be false
82
+ expect(described_class.cancellable?("Cancel Requested")).to be false
83
+ end
84
+ end
85
+
86
+ describe ".editable?" do
87
+ it "returns true only for Live status" do
88
+ expect(described_class.editable?("Live")).to be true
89
+ end
90
+
91
+ it "returns false for other statuses" do
92
+ expect(described_class.editable?("Received")).to be false
93
+ expect(described_class.editable?("Filled")).to be false
94
+ expect(described_class.editable?("Replace Requested")).to be false
95
+ end
96
+ end
97
+
98
+ describe ".valid?" do
99
+ it "returns true for all valid statuses" do
100
+ all_statuses = described_class::ALL_STATUSES
101
+ all_statuses.each do |status|
102
+ expect(described_class.valid?(status)).to be true
103
+ end
104
+ end
105
+
106
+ it "returns false for invalid statuses" do
107
+ expect(described_class.valid?("Invalid")).to be false
108
+ expect(described_class.valid?("Unknown")).to be false
109
+ expect(described_class.valid?("")).to be false
110
+ expect(described_class.valid?(nil)).to be false
111
+ end
112
+ end
113
+ end