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
@@ -6,7 +6,7 @@ require "bigdecimal"
6
6
  RSpec.describe Tastytrade::Models::Account do
7
7
  let(:account_data) do
8
8
  {
9
- "account-number" => "123456",
9
+ "account-number" => "5WT0001",
10
10
  "nickname" => "My Account",
11
11
  "account-type-name" => "Individual",
12
12
  "opened-at" => "2025-01-01T10:00:00Z",
@@ -25,7 +25,7 @@ RSpec.describe Tastytrade::Models::Account do
25
25
 
26
26
  describe "attributes" do
27
27
  it "parses account number" do
28
- expect(account.account_number).to eq("123456")
28
+ expect(account.account_number).to eq("5WT0001")
29
29
  end
30
30
 
31
31
  it "parses nickname" do
@@ -79,7 +79,7 @@ RSpec.describe Tastytrade::Models::Account do
79
79
  expect(accounts).to be_an(Array)
80
80
  expect(accounts.size).to eq(2)
81
81
  expect(accounts.first).to be_a(described_class)
82
- expect(accounts.first.account_number).to eq("123456")
82
+ expect(accounts.first.account_number).to eq("5WT0001")
83
83
  expect(accounts.last.account_number).to eq("789012")
84
84
  end
85
85
 
@@ -97,12 +97,12 @@ RSpec.describe Tastytrade::Models::Account do
97
97
  end
98
98
 
99
99
  it "returns single Account object" do
100
- allow(session).to receive(:get).with("/accounts/123456/").and_return(response)
100
+ allow(session).to receive(:get).with("/accounts/5WT0001/").and_return(response)
101
101
 
102
- account = described_class.get(session, "123456")
102
+ account = described_class.get(session, "5WT0001")
103
103
 
104
104
  expect(account).to be_a(described_class)
105
- expect(account.account_number).to eq("123456")
105
+ expect(account.account_number).to eq("5WT0001")
106
106
  end
107
107
  end
108
108
 
@@ -110,7 +110,7 @@ RSpec.describe Tastytrade::Models::Account do
110
110
  let(:balance_data) do
111
111
  {
112
112
  "data" => {
113
- "account-number" => "123456",
113
+ "account-number" => "5WT0001",
114
114
  "cash-balance" => "10000.00",
115
115
  "net-liquidating-value" => "15000.00"
116
116
  }
@@ -118,12 +118,12 @@ RSpec.describe Tastytrade::Models::Account do
118
118
  end
119
119
 
120
120
  it "returns balance data" do
121
- allow(session).to receive(:get).with("/accounts/123456/balances/").and_return(balance_data)
121
+ allow(session).to receive(:get).with("/accounts/5WT0001/balances/").and_return(balance_data)
122
122
 
123
123
  balance = account.get_balances(session)
124
124
 
125
125
  expect(balance).to be_a(Tastytrade::Models::AccountBalance)
126
- expect(balance.account_number).to eq("123456")
126
+ expect(balance.account_number).to eq("5WT0001")
127
127
  expect(balance.cash_balance).to eq(BigDecimal("10000.00"))
128
128
  expect(balance.net_liquidating_value).to eq(BigDecimal("15000.00"))
129
129
  end
@@ -142,7 +142,7 @@ RSpec.describe Tastytrade::Models::Account do
142
142
  end
143
143
 
144
144
  it "returns array of positions" do
145
- allow(session).to receive(:get).with("/accounts/123456/positions/", {}).and_return(positions_data)
145
+ allow(session).to receive(:get).with("/accounts/5WT0001/positions/", {}).and_return(positions_data)
146
146
 
147
147
  positions = account.get_positions(session)
148
148
 
@@ -158,18 +158,46 @@ RSpec.describe Tastytrade::Models::Account do
158
158
  let(:status_data) do
159
159
  {
160
160
  "data" => {
161
- "account-number" => "123456",
162
- "is-pattern-day-trader" => false
161
+ "account-number" => "5WT0001",
162
+ "equities-margin-calculation-type" => "REG_T",
163
+ "fee-schedule-name" => "standard",
164
+ "futures-margin-rate-multiplier" => "1.0",
165
+ "has-intraday-equities-margin" => false,
166
+ "id" => 123456,
167
+ "is-aggregated-at-clearing" => false,
168
+ "is-closed" => false,
169
+ "is-closing-only" => false,
170
+ "is-cryptocurrency-enabled" => true,
171
+ "is-frozen" => false,
172
+ "is-full-equity-margin-required" => false,
173
+ "is-futures-closing-only" => false,
174
+ "is-futures-intra-day-enabled" => true,
175
+ "is-futures-enabled" => true,
176
+ "is-in-day-trade-equity-maintenance-call" => false,
177
+ "is-in-margin-call" => false,
178
+ "is-pattern-day-trader" => false,
179
+ "is-small-notional-futures-intra-day-enabled" => false,
180
+ "is-roll-the-day-forward-enabled" => true,
181
+ "are-far-otm-net-options-restricted" => false,
182
+ "options-level" => "Level 2",
183
+ "short-calls-enabled" => false,
184
+ "small-notional-futures-margin-rate-multiplier" => "1.0",
185
+ "is-equity-offering-enabled" => true,
186
+ "is-equity-offering-closing-only" => false,
187
+ "updated-at" => "2024-01-15T10:30:00Z"
163
188
  }
164
189
  }
165
190
  end
166
191
 
167
- it "returns trading status data" do
168
- allow(session).to receive(:get).with("/accounts/123456/trading-status/").and_return(status_data)
192
+ it "returns a TradingStatus object" do
193
+ allow(session).to receive(:get).with("/accounts/5WT0001/trading-status/").and_return(status_data)
169
194
 
170
195
  status = account.get_trading_status(session)
171
196
 
172
- expect(status).to eq(status_data["data"])
197
+ expect(status).to be_a(Tastytrade::Models::TradingStatus)
198
+ expect(status.account_number).to eq("5WT0001")
199
+ expect(status.is_pattern_day_trader).to eq(false)
200
+ expect(status.options_level).to eq("Level 2")
173
201
  end
174
202
  end
175
203
 
@@ -202,5 +230,48 @@ RSpec.describe Tastytrade::Models::Account do
202
230
  expect(account.foreign?).to be false
203
231
  end
204
232
  end
233
+
234
+ describe "#get_transactions" do
235
+ let(:transaction_data) do
236
+ {
237
+ "data" => {
238
+ "items" => [
239
+ {
240
+ "id" => 12345,
241
+ "account-number" => "5WT0001",
242
+ "symbol" => "AAPL",
243
+ "transaction-type" => "Trade",
244
+ "value" => "-1500.00"
245
+ }
246
+ ]
247
+ }
248
+ }
249
+ end
250
+
251
+ it "fetches transactions for the account" do
252
+ allow(Tastytrade::Models::Transaction).to receive(:get_all)
253
+ .with(session, "5WT0001")
254
+ .and_return([])
255
+
256
+ account.get_transactions(session)
257
+ expect(Tastytrade::Models::Transaction).to have_received(:get_all).with(session, "5WT0001")
258
+ end
259
+
260
+ it "passes through filter options" do
261
+ options = {
262
+ start_date: Date.new(2023, 1, 1),
263
+ end_date: Date.new(2023, 12, 31),
264
+ symbol: "AAPL"
265
+ }
266
+
267
+ allow(Tastytrade::Models::Transaction).to receive(:get_all)
268
+ .with(session, "5WT0001", **options)
269
+ .and_return([])
270
+
271
+ account.get_transactions(session, **options)
272
+ expect(Tastytrade::Models::Transaction).to have_received(:get_all)
273
+ .with(session, "5WT0001", **options)
274
+ end
275
+ end
205
276
  end
206
277
  end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "bigdecimal"
5
+
6
+ RSpec.describe Tastytrade::Models::BuyingPowerEffect do
7
+ let(:buying_power_effect_data) do
8
+ {
9
+ "change-in-margin-requirement" => "-125.0",
10
+ "change-in-buying-power" => "-125.004",
11
+ "current-buying-power" => "1000.0",
12
+ "new-buying-power" => "874.996",
13
+ "isolated-order-margin-requirement" => "-125.0",
14
+ "is-spread" => false,
15
+ "impact" => "125.004",
16
+ "effect" => "Debit"
17
+ }
18
+ end
19
+
20
+ subject { described_class.new(buying_power_effect_data) }
21
+
22
+ describe "#initialize" do
23
+ it "parses all attributes correctly" do
24
+ expect(subject.change_in_margin_requirement).to eq(BigDecimal("-125.0"))
25
+ expect(subject.change_in_buying_power).to eq(BigDecimal("-125.004"))
26
+ expect(subject.current_buying_power).to eq(BigDecimal("1000.0"))
27
+ expect(subject.new_buying_power).to eq(BigDecimal("874.996"))
28
+ expect(subject.isolated_order_margin_requirement).to eq(BigDecimal("-125.0"))
29
+ expect(subject.is_spread).to eq(false)
30
+ expect(subject.impact).to eq(BigDecimal("125.004"))
31
+ expect(subject.effect).to eq("Debit")
32
+ end
33
+
34
+ context "with nil values" do
35
+ let(:buying_power_effect_data) do
36
+ {
37
+ "change-in-margin-requirement" => nil,
38
+ "change-in-buying-power" => "",
39
+ "current-buying-power" => "1000.0",
40
+ "effect" => "Debit"
41
+ }
42
+ end
43
+
44
+ it "handles nil and empty values gracefully" do
45
+ expect(subject.change_in_margin_requirement).to be_nil
46
+ expect(subject.change_in_buying_power).to be_nil
47
+ expect(subject.current_buying_power).to eq(BigDecimal("1000.0"))
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#buying_power_usage_percentage" do
53
+ it "calculates the percentage correctly" do
54
+ # 125.004 / 1000.0 * 100 = 12.5004
55
+ expect(subject.buying_power_usage_percentage).to eq(BigDecimal("12.50"))
56
+ end
57
+
58
+ context "with zero current buying power" do
59
+ let(:buying_power_effect_data) do
60
+ {
61
+ "current-buying-power" => "0",
62
+ "impact" => "100.0"
63
+ }
64
+ end
65
+
66
+ it "returns zero" do
67
+ expect(subject.buying_power_usage_percentage).to eq(BigDecimal("0"))
68
+ end
69
+ end
70
+
71
+ context "with nil current buying power" do
72
+ let(:buying_power_effect_data) do
73
+ {
74
+ "current-buying-power" => nil,
75
+ "impact" => "100.0"
76
+ }
77
+ end
78
+
79
+ it "returns zero" do
80
+ expect(subject.buying_power_usage_percentage).to eq(BigDecimal("0"))
81
+ end
82
+ end
83
+
84
+ context "using change-in-buying-power when impact is nil" do
85
+ let(:buying_power_effect_data) do
86
+ {
87
+ "change-in-buying-power" => "-200.50",
88
+ "current-buying-power" => "1000.0",
89
+ "impact" => nil
90
+ }
91
+ end
92
+
93
+ it "uses the absolute value of change-in-buying-power" do
94
+ # 200.50 / 1000.0 * 100 = 20.05
95
+ expect(subject.buying_power_usage_percentage).to eq(BigDecimal("20.05"))
96
+ end
97
+ end
98
+ end
99
+
100
+ describe "#exceeds_threshold?" do
101
+ it "returns true when usage exceeds threshold" do
102
+ expect(subject.exceeds_threshold?(10)).to be true
103
+ expect(subject.exceeds_threshold?("10.0")).to be true
104
+ end
105
+
106
+ it "returns false when usage is below threshold" do
107
+ expect(subject.exceeds_threshold?(15)).to be false
108
+ expect(subject.exceeds_threshold?(20)).to be false
109
+ end
110
+
111
+ it "returns false when usage equals threshold" do
112
+ expect(subject.exceeds_threshold?(12.50)).to be false
113
+ end
114
+ end
115
+
116
+ describe "#buying_power_change_amount" do
117
+ it "returns the absolute value of the change" do
118
+ expect(subject.buying_power_change_amount).to eq(BigDecimal("125.004"))
119
+ end
120
+
121
+ context "with positive change" do
122
+ let(:buying_power_effect_data) do
123
+ {
124
+ "change-in-buying-power" => "150.00",
125
+ "current-buying-power" => "1000.0"
126
+ }
127
+ end
128
+
129
+ it "returns the absolute value" do
130
+ expect(subject.buying_power_change_amount).to eq(BigDecimal("150.00"))
131
+ end
132
+ end
133
+
134
+ context "using impact when change-in-buying-power is nil" do
135
+ let(:buying_power_effect_data) do
136
+ {
137
+ "change-in-buying-power" => nil,
138
+ "impact" => "75.50"
139
+ }
140
+ end
141
+
142
+ it "returns the impact absolute value" do
143
+ expect(subject.buying_power_change_amount).to eq(BigDecimal("75.50"))
144
+ end
145
+ end
146
+
147
+ context "with all nil values" do
148
+ let(:buying_power_effect_data) do
149
+ {
150
+ "change-in-buying-power" => nil,
151
+ "impact" => nil
152
+ }
153
+ end
154
+
155
+ it "returns zero" do
156
+ expect(subject.buying_power_change_amount).to eq(BigDecimal("0"))
157
+ end
158
+ end
159
+ end
160
+
161
+ describe "#debit?" do
162
+ it "returns true for debit effects" do
163
+ expect(subject.debit?).to be true
164
+ end
165
+
166
+ context "with credit effect" do
167
+ let(:buying_power_effect_data) do
168
+ {
169
+ "effect" => "Credit",
170
+ "change-in-buying-power" => "100.0"
171
+ }
172
+ end
173
+
174
+ it "returns false" do
175
+ expect(subject.debit?).to be false
176
+ end
177
+ end
178
+
179
+ context "with negative change and no effect field" do
180
+ let(:buying_power_effect_data) do
181
+ {
182
+ "change-in-buying-power" => "-50.0",
183
+ "effect" => nil
184
+ }
185
+ end
186
+
187
+ it "returns true based on negative change" do
188
+ expect(subject.debit?).to be true
189
+ end
190
+ end
191
+
192
+ context "with positive change and no effect field" do
193
+ let(:buying_power_effect_data) do
194
+ {
195
+ "change-in-buying-power" => "50.0",
196
+ "effect" => nil
197
+ }
198
+ end
199
+
200
+ it "returns false based on positive change" do
201
+ expect(subject.debit?).to be false
202
+ end
203
+ end
204
+ end
205
+
206
+ describe "#credit?" do
207
+ context "with credit effect" do
208
+ let(:buying_power_effect_data) do
209
+ {
210
+ "effect" => "Credit",
211
+ "change-in-buying-power" => "100.0"
212
+ }
213
+ end
214
+
215
+ it "returns true" do
216
+ expect(subject.credit?).to be true
217
+ end
218
+ end
219
+
220
+ it "returns false for debit effects" do
221
+ expect(subject.credit?).to be false
222
+ end
223
+
224
+ context "with positive change and no effect field" do
225
+ let(:buying_power_effect_data) do
226
+ {
227
+ "change-in-buying-power" => "75.0",
228
+ "effect" => nil
229
+ }
230
+ end
231
+
232
+ it "returns true based on positive change" do
233
+ expect(subject.credit?).to be true
234
+ end
235
+ end
236
+ end
237
+
238
+ describe "attribute readers" do
239
+ it "provides access to all attributes" do
240
+ expect(subject).to respond_to(:change_in_margin_requirement)
241
+ expect(subject).to respond_to(:change_in_buying_power)
242
+ expect(subject).to respond_to(:current_buying_power)
243
+ expect(subject).to respond_to(:new_buying_power)
244
+ expect(subject).to respond_to(:isolated_order_margin_requirement)
245
+ expect(subject).to respond_to(:is_spread)
246
+ expect(subject).to respond_to(:impact)
247
+ expect(subject).to respond_to(:effect)
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Tastytrade::Models::LiveOrder#to_h" do
6
+ let(:order_data) do
7
+ {
8
+ "id" => "12345",
9
+ "account-number" => "5WZ38925",
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
+ "stop-trigger" => "148.00",
22
+ "gtc-date" => "2024-12-31",
23
+ "created-at" => "2024-01-01T10:00:00Z",
24
+ "updated-at" => "2024-01-01T10:05:00Z",
25
+ "received-at" => "2024-01-01T10:00:01Z",
26
+ "routed-at" => "2024-01-01T10:00:02Z",
27
+ "live-at" => "2024-01-01T10:00:03Z",
28
+ "legs" => [
29
+ {
30
+ "symbol" => "AAPL",
31
+ "instrument-type" => "Equity",
32
+ "action" => "Buy to Open",
33
+ "quantity" => 100,
34
+ "remaining-quantity" => 75,
35
+ "fill-price" => "150.25",
36
+ "fills" => [
37
+ {
38
+ "ext-exec-id" => "EXT123",
39
+ "fill-id" => "FILL123",
40
+ "quantity" => 25,
41
+ "fill-price" => "150.25",
42
+ "filled-at" => "2024-01-01T10:03:00Z",
43
+ "destination-venue" => "NASDAQ"
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ }
49
+ end
50
+
51
+ let(:order) { Tastytrade::Models::LiveOrder.new(order_data) }
52
+
53
+ it "converts order to hash format" do
54
+ hash = order.to_h
55
+
56
+ expect(hash).to be_a(Hash)
57
+ expect(hash[:id]).to eq("12345")
58
+ expect(hash[:account_number]).to eq("5WZ38925")
59
+ expect(hash[:status]).to eq("Live")
60
+ expect(hash[:cancellable]).to be true
61
+ expect(hash[:editable]).to be true
62
+ expect(hash[:edited]).to be false
63
+ expect(hash[:time_in_force]).to eq("Day")
64
+ expect(hash[:order_type]).to eq("Limit")
65
+ expect(hash[:size]).to eq(100)
66
+ expect(hash[:price]).to eq("150.5")
67
+ expect(hash[:price_effect]).to eq("Debit")
68
+ expect(hash[:underlying_symbol]).to eq("AAPL")
69
+ expect(hash[:underlying_instrument_type]).to eq("Equity")
70
+ expect(hash[:stop_trigger]).to eq("148.0")
71
+ expect(hash[:gtc_date]).to eq("2024-12-31")
72
+ expect(hash[:remaining_quantity]).to eq(75)
73
+ expect(hash[:filled_quantity]).to eq(25)
74
+ end
75
+
76
+ it "includes timestamp fields in ISO8601 format" do
77
+ hash = order.to_h
78
+
79
+ expect(hash[:created_at]).to eq("2024-01-01T10:00:00Z")
80
+ expect(hash[:updated_at]).to eq("2024-01-01T10:05:00Z")
81
+ expect(hash[:received_at]).to eq("2024-01-01T10:00:01Z")
82
+ expect(hash[:routed_at]).to eq("2024-01-01T10:00:02Z")
83
+ expect(hash[:live_at]).to eq("2024-01-01T10:00:03Z")
84
+ end
85
+
86
+ it "includes leg information" do
87
+ hash = order.to_h
88
+
89
+ expect(hash[:legs]).to be_an(Array)
90
+ expect(hash[:legs].size).to eq(1)
91
+
92
+ leg = hash[:legs].first
93
+ expect(leg[:symbol]).to eq("AAPL")
94
+ expect(leg[:instrument_type]).to eq("Equity")
95
+ expect(leg[:action]).to eq("Buy to Open")
96
+ expect(leg[:quantity]).to eq(100)
97
+ expect(leg[:remaining_quantity]).to eq(75)
98
+ expect(leg[:filled_quantity]).to eq(25)
99
+ expect(leg[:fill_price]).to eq("150.25")
100
+ end
101
+
102
+ it "includes fill information" do
103
+ hash = order.to_h
104
+ leg = hash[:legs].first
105
+ fills = leg[:fills]
106
+
107
+ expect(fills).to be_an(Array)
108
+ expect(fills.size).to eq(1)
109
+
110
+ fill = fills.first
111
+ expect(fill[:ext_exec_id]).to eq("EXT123")
112
+ expect(fill[:fill_id]).to eq("FILL123")
113
+ expect(fill[:quantity]).to eq(25)
114
+ expect(fill[:fill_price]).to eq("150.25")
115
+ expect(fill[:filled_at]).to eq("2024-01-01T10:03:00Z")
116
+ expect(fill[:destination_venue]).to eq("NASDAQ")
117
+ end
118
+
119
+ it "excludes nil values from hash" do
120
+ minimal_order_data = {
121
+ "id" => "12345",
122
+ "status" => "Live",
123
+ "legs" => []
124
+ }
125
+ minimal_order = Tastytrade::Models::LiveOrder.new(minimal_order_data)
126
+ hash = minimal_order.to_h
127
+
128
+ expect(hash).not_to have_key(:price)
129
+ expect(hash).not_to have_key(:stop_trigger)
130
+ expect(hash).not_to have_key(:gtc_date)
131
+ expect(hash).not_to have_key(:filled_at)
132
+ expect(hash).not_to have_key(:cancelled_at)
133
+ end
134
+
135
+ it "can be serialized to JSON" do
136
+ hash = order.to_h
137
+ json = JSON.generate(hash)
138
+ parsed = JSON.parse(json)
139
+
140
+ expect(parsed["id"]).to eq("12345")
141
+ expect(parsed["status"]).to eq("Live")
142
+ expect(parsed["legs"]).to be_an(Array)
143
+ end
144
+ end