tastytrade 0.2.0 → 0.3.1
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 +180 -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/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
- metadata +34 -18
- data/lib/tastytrade/keyring_store.rb +0 -72
- 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" => "
|
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("
|
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("
|
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/
|
100
|
+
allow(session).to receive(:get).with("/accounts/5WT0001/").and_return(response)
|
101
101
|
|
102
|
-
account = described_class.get(session, "
|
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("
|
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" => "
|
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/
|
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("
|
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/
|
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" => "
|
162
|
-
"
|
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
|
168
|
-
allow(session).to receive(:get).with("/accounts/
|
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
|
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
|