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,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Tastytrade::Session do
|
6
|
+
describe ".from_environment" do
|
7
|
+
around do |example|
|
8
|
+
# Save original environment
|
9
|
+
original_env = ENV.to_hash
|
10
|
+
|
11
|
+
# Clear relevant environment variables
|
12
|
+
%w[TASTYTRADE_USERNAME TT_USERNAME TASTYTRADE_PASSWORD TT_PASSWORD
|
13
|
+
TASTYTRADE_REMEMBER TT_REMEMBER TASTYTRADE_ENVIRONMENT TT_ENVIRONMENT].each do |key|
|
14
|
+
ENV.delete(key)
|
15
|
+
end
|
16
|
+
|
17
|
+
example.run
|
18
|
+
|
19
|
+
# Restore original environment
|
20
|
+
ENV.clear
|
21
|
+
ENV.update(original_env)
|
22
|
+
end
|
23
|
+
|
24
|
+
context "with TASTYTRADE_ prefixed variables" do
|
25
|
+
before do
|
26
|
+
ENV["TASTYTRADE_USERNAME"] = "test@example.com"
|
27
|
+
ENV["TASTYTRADE_PASSWORD"] = "test_password"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "creates a session with username and password" do
|
31
|
+
session = described_class.from_environment
|
32
|
+
expect(session).not_to be_nil
|
33
|
+
expect(session.instance_variable_get(:@username)).to eq("test@example.com")
|
34
|
+
expect(session.instance_variable_get(:@password)).to eq("test_password")
|
35
|
+
end
|
36
|
+
|
37
|
+
it "defaults to production environment" do
|
38
|
+
session = described_class.from_environment
|
39
|
+
expect(session.is_test).to be false
|
40
|
+
end
|
41
|
+
|
42
|
+
it "defaults to remember_me false" do
|
43
|
+
session = described_class.from_environment
|
44
|
+
expect(session.instance_variable_get(:@remember_me)).to be false
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with TASTYTRADE_REMEMBER set to true" do
|
48
|
+
before do
|
49
|
+
ENV["TASTYTRADE_REMEMBER"] = "true"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "enables remember_me" do
|
53
|
+
session = described_class.from_environment
|
54
|
+
expect(session.instance_variable_get(:@remember_me)).to be true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "with TASTYTRADE_REMEMBER set to TRUE" do
|
59
|
+
before do
|
60
|
+
ENV["TASTYTRADE_REMEMBER"] = "TRUE"
|
61
|
+
end
|
62
|
+
|
63
|
+
it "enables remember_me (case insensitive)" do
|
64
|
+
session = described_class.from_environment
|
65
|
+
expect(session.instance_variable_get(:@remember_me)).to be true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "with TASTYTRADE_ENVIRONMENT set to sandbox" do
|
70
|
+
before do
|
71
|
+
ENV["TASTYTRADE_ENVIRONMENT"] = "sandbox"
|
72
|
+
end
|
73
|
+
|
74
|
+
it "sets is_test to true" do
|
75
|
+
session = described_class.from_environment
|
76
|
+
expect(session.is_test).to be true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "with TASTYTRADE_ENVIRONMENT set to SANDBOX" do
|
81
|
+
before do
|
82
|
+
ENV["TASTYTRADE_ENVIRONMENT"] = "SANDBOX"
|
83
|
+
end
|
84
|
+
|
85
|
+
it "sets is_test to true (case insensitive)" do
|
86
|
+
session = described_class.from_environment
|
87
|
+
expect(session.is_test).to be true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context "with TT_ prefixed variables" do
|
93
|
+
before do
|
94
|
+
ENV["TT_USERNAME"] = "tt@example.com"
|
95
|
+
ENV["TT_PASSWORD"] = "tt_password"
|
96
|
+
end
|
97
|
+
|
98
|
+
it "creates a session with username and password" do
|
99
|
+
session = described_class.from_environment
|
100
|
+
expect(session).not_to be_nil
|
101
|
+
expect(session.instance_variable_get(:@username)).to eq("tt@example.com")
|
102
|
+
expect(session.instance_variable_get(:@password)).to eq("tt_password")
|
103
|
+
end
|
104
|
+
|
105
|
+
context "with TT_REMEMBER set" do
|
106
|
+
before do
|
107
|
+
ENV["TT_REMEMBER"] = "true"
|
108
|
+
end
|
109
|
+
|
110
|
+
it "enables remember_me" do
|
111
|
+
session = described_class.from_environment
|
112
|
+
expect(session.instance_variable_get(:@remember_me)).to be true
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "with TT_ENVIRONMENT set to sandbox" do
|
117
|
+
before do
|
118
|
+
ENV["TT_ENVIRONMENT"] = "sandbox"
|
119
|
+
end
|
120
|
+
|
121
|
+
it "sets is_test to true" do
|
122
|
+
session = described_class.from_environment
|
123
|
+
expect(session.is_test).to be true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context "with both TASTYTRADE_ and TT_ variables" do
|
129
|
+
before do
|
130
|
+
ENV["TASTYTRADE_USERNAME"] = "tastytrade@example.com"
|
131
|
+
ENV["TT_USERNAME"] = "tt@example.com"
|
132
|
+
ENV["TASTYTRADE_PASSWORD"] = "tastytrade_password"
|
133
|
+
ENV["TT_PASSWORD"] = "tt_password"
|
134
|
+
end
|
135
|
+
|
136
|
+
it "prefers TASTYTRADE_ prefixed variables" do
|
137
|
+
session = described_class.from_environment
|
138
|
+
expect(session.instance_variable_get(:@username)).to eq("tastytrade@example.com")
|
139
|
+
expect(session.instance_variable_get(:@password)).to eq("tastytrade_password")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context "with missing username" do
|
144
|
+
before do
|
145
|
+
ENV["TASTYTRADE_PASSWORD"] = "test_password"
|
146
|
+
end
|
147
|
+
|
148
|
+
it "returns nil" do
|
149
|
+
expect(described_class.from_environment).to be_nil
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "with missing password" do
|
154
|
+
before do
|
155
|
+
ENV["TASTYTRADE_USERNAME"] = "test@example.com"
|
156
|
+
end
|
157
|
+
|
158
|
+
it "returns nil" do
|
159
|
+
expect(described_class.from_environment).to be_nil
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context "with no environment variables set" do
|
164
|
+
it "returns nil" do
|
165
|
+
expect(described_class.from_environment).to be_nil
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "spec_helper"
|
4
4
|
require "tastytrade/session_manager"
|
5
|
-
require "tastytrade/
|
5
|
+
require "tastytrade/file_store"
|
6
6
|
require "tastytrade/cli_config"
|
7
7
|
|
8
8
|
RSpec.describe Tastytrade::SessionManager do
|
@@ -16,7 +16,7 @@ RSpec.describe Tastytrade::SessionManager do
|
|
16
16
|
allow(Tastytrade::CLIConfig).to receive(:new).and_return(config)
|
17
17
|
allow(config).to receive(:set)
|
18
18
|
allow(config).to receive(:delete)
|
19
|
-
allow(Tastytrade::
|
19
|
+
allow(Tastytrade::FileStore).to receive(:available?).and_return(true)
|
20
20
|
end
|
21
21
|
|
22
22
|
describe "#initialize" do
|
@@ -34,16 +34,21 @@ RSpec.describe Tastytrade::SessionManager do
|
|
34
34
|
describe "#save_session" do
|
35
35
|
let(:session_token) { "session_token_123" }
|
36
36
|
let(:remember_token) { "remember_token_456" }
|
37
|
+
let(:user) { instance_double(Tastytrade::Models::User,
|
38
|
+
email: "test@example.com",
|
39
|
+
username: "testuser",
|
40
|
+
external_id: "test-external-id") }
|
37
41
|
|
38
42
|
before do
|
39
43
|
allow(session).to receive(:session_token).and_return(session_token)
|
40
44
|
allow(session).to receive(:remember_token).and_return(remember_token)
|
41
45
|
allow(session).to receive(:session_expiration).and_return(nil)
|
42
|
-
allow(
|
46
|
+
allow(session).to receive(:user).and_return(user)
|
47
|
+
allow(Tastytrade::FileStore).to receive(:set).and_return(true)
|
43
48
|
end
|
44
49
|
|
45
50
|
it "saves session token" do
|
46
|
-
expect(Tastytrade::
|
51
|
+
expect(Tastytrade::FileStore).to receive(:set)
|
47
52
|
.with("token_test@example.com_production", session_token)
|
48
53
|
|
49
54
|
manager.save_session(session)
|
@@ -57,7 +62,7 @@ RSpec.describe Tastytrade::SessionManager do
|
|
57
62
|
end
|
58
63
|
|
59
64
|
it "saves session expiration" do
|
60
|
-
expect(Tastytrade::
|
65
|
+
expect(Tastytrade::FileStore).to receive(:set)
|
61
66
|
.with("session_expiration_test@example.com_production", expiration_time.iso8601)
|
62
67
|
|
63
68
|
manager.save_session(session)
|
@@ -74,18 +79,18 @@ RSpec.describe Tastytrade::SessionManager do
|
|
74
79
|
|
75
80
|
context "with remember option" do
|
76
81
|
it "saves remember token and password" do
|
77
|
-
expect(Tastytrade::
|
82
|
+
expect(Tastytrade::FileStore).to receive(:set)
|
78
83
|
.with("remember_test@example.com_production", remember_token)
|
79
|
-
expect(Tastytrade::
|
84
|
+
expect(Tastytrade::FileStore).to receive(:set)
|
80
85
|
.with("password_test@example.com_production", "secret123")
|
81
86
|
|
82
87
|
manager.save_session(session, password: "secret123", remember: true)
|
83
88
|
end
|
84
89
|
|
85
90
|
it "doesn't save password if keyring unavailable" do
|
86
|
-
allow(Tastytrade::
|
91
|
+
allow(Tastytrade::FileStore).to receive(:available?).and_return(false)
|
87
92
|
|
88
|
-
expect(Tastytrade::
|
93
|
+
expect(Tastytrade::FileStore).not_to receive(:set)
|
89
94
|
.with("password_test@example.com_production", anything)
|
90
95
|
|
91
96
|
manager.save_session(session, password: "secret123", remember: true)
|
@@ -94,9 +99,9 @@ RSpec.describe Tastytrade::SessionManager do
|
|
94
99
|
|
95
100
|
context "without remember option" do
|
96
101
|
it "doesn't save remember token or password" do
|
97
|
-
expect(Tastytrade::
|
102
|
+
expect(Tastytrade::FileStore).not_to receive(:set)
|
98
103
|
.with("remember_test@example.com_production", anything)
|
99
|
-
expect(Tastytrade::
|
104
|
+
expect(Tastytrade::FileStore).not_to receive(:set)
|
100
105
|
.with("password_test@example.com_production", anything)
|
101
106
|
|
102
107
|
manager.save_session(session, password: "secret123", remember: false)
|
@@ -104,7 +109,7 @@ RSpec.describe Tastytrade::SessionManager do
|
|
104
109
|
end
|
105
110
|
|
106
111
|
it "handles errors gracefully" do
|
107
|
-
allow(Tastytrade::
|
112
|
+
allow(Tastytrade::FileStore).to receive(:set).and_raise(StandardError, "Save error")
|
108
113
|
|
109
114
|
expect { manager.save_session(session) }.to output(/Failed to save session/).to_stderr
|
110
115
|
expect(manager.save_session(session)).to be false
|
@@ -114,11 +119,14 @@ RSpec.describe Tastytrade::SessionManager do
|
|
114
119
|
describe "#load_session" do
|
115
120
|
context "with saved token" do
|
116
121
|
before do
|
117
|
-
allow(Tastytrade::
|
122
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
118
123
|
.with("token_test@example.com_production").and_return("saved_token")
|
119
|
-
allow(Tastytrade::
|
124
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
120
125
|
.with("remember_test@example.com_production").and_return("saved_remember")
|
121
|
-
allow(Tastytrade::
|
126
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
127
|
+
.with("user_data_test@example.com_production")
|
128
|
+
.and_return('{"email":"test@example.com","username":"testuser","external_id":"test-external-id"}')
|
129
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
122
130
|
.with("session_expiration_test@example.com_production").and_return(nil)
|
123
131
|
end
|
124
132
|
|
@@ -128,6 +136,8 @@ RSpec.describe Tastytrade::SessionManager do
|
|
128
136
|
expect(result).to eq({
|
129
137
|
session_token: "saved_token",
|
130
138
|
remember_token: "saved_remember",
|
139
|
+
user_data: { "email" => "test@example.com", "username" => "testuser",
|
140
|
+
"external_id" => "test-external-id" },
|
131
141
|
session_expiration: nil,
|
132
142
|
username: username,
|
133
143
|
environment: environment
|
@@ -137,7 +147,7 @@ RSpec.describe Tastytrade::SessionManager do
|
|
137
147
|
|
138
148
|
context "without saved token" do
|
139
149
|
before do
|
140
|
-
allow(Tastytrade::
|
150
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
141
151
|
.with("token_test@example.com_production").and_return(nil)
|
142
152
|
end
|
143
153
|
|
@@ -157,9 +167,9 @@ RSpec.describe Tastytrade::SessionManager do
|
|
157
167
|
|
158
168
|
context "with saved remember token" do
|
159
169
|
before do
|
160
|
-
allow(Tastytrade::
|
170
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
161
171
|
.with("password_test@example.com_production").and_return(nil)
|
162
|
-
allow(Tastytrade::
|
172
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
163
173
|
.with("remember_test@example.com_production").and_return("saved_remember")
|
164
174
|
end
|
165
175
|
|
@@ -177,9 +187,9 @@ RSpec.describe Tastytrade::SessionManager do
|
|
177
187
|
|
178
188
|
context "with saved password" do
|
179
189
|
before do
|
180
|
-
allow(Tastytrade::
|
190
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
181
191
|
.with("remember_test@example.com_production").and_return(nil)
|
182
|
-
allow(Tastytrade::
|
192
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
183
193
|
.with("password_test@example.com_production").and_return("saved_password")
|
184
194
|
end
|
185
195
|
|
@@ -199,9 +209,9 @@ RSpec.describe Tastytrade::SessionManager do
|
|
199
209
|
let(:environment) { "sandbox" }
|
200
210
|
|
201
211
|
before do
|
202
|
-
allow(Tastytrade::
|
212
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
203
213
|
.with("password_test@example.com_sandbox").and_return("saved_password")
|
204
|
-
allow(Tastytrade::
|
214
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
205
215
|
.with("remember_test@example.com_sandbox").and_return(nil)
|
206
216
|
end
|
207
217
|
|
@@ -216,7 +226,7 @@ RSpec.describe Tastytrade::SessionManager do
|
|
216
226
|
|
217
227
|
context "without saved credentials" do
|
218
228
|
before do
|
219
|
-
allow(Tastytrade::
|
229
|
+
allow(Tastytrade::FileStore).to receive(:get).and_return(nil)
|
220
230
|
end
|
221
231
|
|
222
232
|
it "returns nil" do
|
@@ -225,9 +235,9 @@ RSpec.describe Tastytrade::SessionManager do
|
|
225
235
|
end
|
226
236
|
|
227
237
|
it "handles errors gracefully" do
|
228
|
-
allow(Tastytrade::
|
238
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
229
239
|
.with("password_test@example.com_production").and_return("password")
|
230
|
-
allow(Tastytrade::
|
240
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
231
241
|
.with("remember_test@example.com_production").and_return(nil)
|
232
242
|
allow(new_session).to receive(:login).and_raise(StandardError, "Login error")
|
233
243
|
|
@@ -238,13 +248,13 @@ RSpec.describe Tastytrade::SessionManager do
|
|
238
248
|
|
239
249
|
describe "#clear_session!" do
|
240
250
|
it "deletes all stored credentials" do
|
241
|
-
expect(Tastytrade::
|
251
|
+
expect(Tastytrade::FileStore).to receive(:delete)
|
242
252
|
.with("token_test@example.com_production")
|
243
|
-
expect(Tastytrade::
|
253
|
+
expect(Tastytrade::FileStore).to receive(:delete)
|
244
254
|
.with("remember_test@example.com_production")
|
245
|
-
expect(Tastytrade::
|
255
|
+
expect(Tastytrade::FileStore).to receive(:delete)
|
246
256
|
.with("password_test@example.com_production")
|
247
|
-
expect(Tastytrade::
|
257
|
+
expect(Tastytrade::FileStore).to receive(:delete)
|
248
258
|
.with("session_expiration_test@example.com_production")
|
249
259
|
|
250
260
|
manager.clear_session!
|
@@ -261,7 +271,7 @@ RSpec.describe Tastytrade::SessionManager do
|
|
261
271
|
describe "#saved_credentials?" do
|
262
272
|
context "with saved password" do
|
263
273
|
before do
|
264
|
-
allow(Tastytrade::
|
274
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
265
275
|
.with("password_test@example.com_production").and_return("password")
|
266
276
|
end
|
267
277
|
|
@@ -272,9 +282,9 @@ RSpec.describe Tastytrade::SessionManager do
|
|
272
282
|
|
273
283
|
context "with saved remember token" do
|
274
284
|
before do
|
275
|
-
allow(Tastytrade::
|
285
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
276
286
|
.with("password_test@example.com_production").and_return(nil)
|
277
|
-
allow(Tastytrade::
|
287
|
+
allow(Tastytrade::FileStore).to receive(:get)
|
278
288
|
.with("remember_test@example.com_production").and_return("token")
|
279
289
|
end
|
280
290
|
|
@@ -285,7 +295,7 @@ RSpec.describe Tastytrade::SessionManager do
|
|
285
295
|
|
286
296
|
context "without saved credentials" do
|
287
297
|
before do
|
288
|
-
allow(Tastytrade::
|
298
|
+
allow(Tastytrade::FileStore).to receive(:get).and_return(nil)
|
289
299
|
end
|
290
300
|
|
291
301
|
it "returns false" do
|