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,403 @@
1
+ # VCR Implementation Plan for Tastytrade Gem
2
+
3
+ ## Executive Summary
4
+ Implement VCR cassettes to replace existing mocks, accounting for Tastytrade sandbox's market-hours-only order routing behavior.
5
+
6
+ ## Phase 1: Foundation Setup (Week 1)
7
+
8
+ ### 1.1 Credential Management
9
+ ```bash
10
+ # Create .env.test (add to .gitignore)
11
+ TASTYTRADE_SANDBOX_USERNAME=your_username
12
+ TASTYTRADE_SANDBOX_PASSWORD=your_password
13
+ TASTYTRADE_SANDBOX_ACCOUNT=your_account
14
+ TASTYTRADE_ENVIRONMENT=sandbox
15
+ ```
16
+
17
+ ### 1.2 Enhanced VCR Configuration
18
+ ```ruby
19
+ # spec/spec_helper.rb
20
+ require 'dotenv'
21
+ Dotenv.load('.env.test') if File.exist?('.env.test')
22
+
23
+ VCR.configure do |config|
24
+ config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
25
+ config.hook_into :webmock
26
+ config.configure_rspec_metadata!
27
+
28
+ # Recording mode management
29
+ vcr_mode = ENV['VCR_MODE'] =~ /rec/i ? :all : :once
30
+ vcr_mode = :none if ENV['CI'] # Prevent recording in CI
31
+
32
+ config.default_cassette_options = {
33
+ record: vcr_mode,
34
+ match_requests_on: [:method, :uri, :body],
35
+ allow_playback_repeats: true,
36
+ record_on_error: false
37
+ }
38
+
39
+ # Enhanced sensitive data filtering
40
+ config.filter_sensitive_data('<SANDBOX_USERNAME>') { ENV['TASTYTRADE_SANDBOX_USERNAME'] }
41
+ config.filter_sensitive_data('<SANDBOX_PASSWORD>') { ENV['TASTYTRADE_SANDBOX_PASSWORD'] }
42
+ config.filter_sensitive_data('<SANDBOX_ACCOUNT>') { ENV['TASTYTRADE_SANDBOX_ACCOUNT'] }
43
+
44
+ # Filter dynamic/sensitive data from responses
45
+ config.before_record do |interaction|
46
+ if interaction.response.body
47
+ body = interaction.response.body
48
+
49
+ # Filter account numbers (format: 5WX12345)
50
+ body.gsub!(/5W[A-Z0-9]{6,8}/, '<ACCOUNT_NUMBER>')
51
+
52
+ # Filter session tokens
53
+ body.gsub!(/"session-token":"[^"]+/, '"session-token":"<SESSION_TOKEN>')
54
+ body.gsub!(/"remember-token":"[^"]+/, '"remember-token":"<REMEMBER_TOKEN>')
55
+
56
+ # Filter personal information
57
+ body.gsub!(/"email":"[^"]+/, '"email":"<EMAIL>')
58
+ body.gsub!(/"external-id":"[^"]+/, '"external-id":"<EXTERNAL_ID>')
59
+
60
+ # Add recording metadata
61
+ interaction.response.headers['X-VCR-Recorded-At'] = Time.current.iso8601
62
+ interaction.response.headers['X-VCR-Market-Status'] = market_open? ? 'open' : 'closed'
63
+ end
64
+ end
65
+
66
+ # Ignore certain dynamic parameters
67
+ config.before_playback do |interaction|
68
+ # Normalize timestamps in URLs if needed
69
+ interaction.request.uri.gsub!(/timestamp=\d+/, 'timestamp=NORMALIZED')
70
+ end
71
+ end
72
+ ```
73
+
74
+ ### 1.3 Market Hours Helper
75
+ ```ruby
76
+ # spec/support/market_hours_helper.rb
77
+ module MarketHoursHelper
78
+ def market_open?(time = Time.current)
79
+ # Convert to ET (Eastern Time)
80
+ et_time = time.in_time_zone('America/New_York')
81
+
82
+ # Check if weekend
83
+ return false if et_time.saturday? || et_time.sunday?
84
+
85
+ # Check if US market holiday (simplified - expand as needed)
86
+ holidays = [
87
+ Date.new(et_time.year, 1, 1), # New Year's Day
88
+ Date.new(et_time.year, 7, 4), # Independence Day
89
+ Date.new(et_time.year, 12, 25), # Christmas
90
+ # Add more holidays as needed
91
+ ]
92
+ return false if holidays.include?(et_time.to_date)
93
+
94
+ # Check market hours (9:30 AM - 4:00 PM ET)
95
+ market_open = et_time.change(hour: 9, min: 30)
96
+ market_close = et_time.change(hour: 16, min: 0)
97
+
98
+ et_time >= market_open && et_time <= market_close
99
+ end
100
+
101
+ def skip_outside_market_hours
102
+ unless market_open? || VCR.current_cassette
103
+ skip "Test requires market hours or existing cassette"
104
+ end
105
+ end
106
+
107
+ def next_market_open_time
108
+ time = Time.current.in_time_zone('America/New_York')
109
+
110
+ # If it's before 9:30 AM on a weekday, return today's open
111
+ if !time.saturday? && !time.sunday? && time.hour < 9 || (time.hour == 9 && time.min < 30)
112
+ return time.change(hour: 9, min: 30)
113
+ end
114
+
115
+ # Otherwise, find next weekday
116
+ loop do
117
+ time = time.tomorrow
118
+ break if !time.saturday? && !time.sunday?
119
+ end
120
+
121
+ time.change(hour: 9, min: 30)
122
+ end
123
+ end
124
+
125
+ RSpec.configure do |config|
126
+ config.include MarketHoursHelper
127
+ end
128
+ ```
129
+
130
+ ## Phase 2: Proof of Concept - Session Class (Week 1)
131
+
132
+ ### 2.1 Convert Session Tests
133
+ ```ruby
134
+ # spec/tastytrade/session_vcr_spec.rb
135
+ require 'spec_helper'
136
+
137
+ RSpec.describe Tastytrade::Session, :vcr do
138
+ let(:sandbox_credentials) do
139
+ {
140
+ username: ENV['TASTYTRADE_SANDBOX_USERNAME'],
141
+ password: ENV['TASTYTRADE_SANDBOX_PASSWORD'],
142
+ is_test: true
143
+ }
144
+ end
145
+
146
+ describe "#login" do
147
+ context "with valid credentials" do
148
+ it "authenticates successfully" do
149
+ session = described_class.new(**sandbox_credentials).login
150
+
151
+ expect(session.session_token).not_to be_nil
152
+ expect(session.user).to be_a(Tastytrade::Models::User)
153
+ expect(session.user.email).not_to be_nil
154
+ end
155
+ end
156
+
157
+ context "with invalid credentials" do
158
+ it "raises InvalidCredentialsError" do
159
+ invalid_creds = sandbox_credentials.merge(password: 'wrong_password')
160
+
161
+ expect {
162
+ described_class.new(**invalid_creds).login
163
+ }.to raise_error(Tastytrade::InvalidCredentialsError)
164
+ end
165
+ end
166
+ end
167
+
168
+ describe "#validate" do
169
+ let(:session) { described_class.new(**sandbox_credentials).login }
170
+
171
+ it "validates active session" do
172
+ expect(session.validate).to be true
173
+ end
174
+ end
175
+
176
+ describe "#destroy" do
177
+ let(:session) { described_class.new(**sandbox_credentials).login }
178
+
179
+ it "destroys the session" do
180
+ session.destroy
181
+ expect(session.session_token).to be_nil
182
+ end
183
+ end
184
+ end
185
+ ```
186
+
187
+ ### 2.2 Recording Schedule
188
+ ```markdown
189
+ # Recording Schedule
190
+
191
+ ## Initial Recording Session
192
+ - **Date**: [Schedule a weekday]
193
+ - **Time**: 10:00 AM - 3:00 PM ET (avoiding market open/close volatility)
194
+ - **Checklist**:
195
+ - [ ] Verify sandbox credentials work
196
+ - [ ] Market is open
197
+ - [ ] No US market holidays
198
+ - [ ] VCR_MODE=rec environment variable set
199
+
200
+ ## Recording Commands
201
+ ```bash
202
+ # Record all cassettes
203
+ VCR_MODE=rec bundle exec rspec spec/tastytrade/session_vcr_spec.rb
204
+
205
+ # Verify cassettes work
206
+ bundle exec rspec spec/tastytrade/session_vcr_spec.rb
207
+ ```
208
+
209
+ ## Phase 3: Order-Related Tests (Week 2)
210
+
211
+ ### 3.1 Order Placement Tests with Market Hours
212
+ ```ruby
213
+ # spec/tastytrade/models/account_place_order_vcr_spec.rb
214
+ RSpec.describe "Order Placement", :vcr do
215
+ include MarketHoursHelper
216
+
217
+ let(:session) { sandbox_session } # From helper
218
+ let(:account) { session.accounts.first }
219
+
220
+ describe "placing orders" do
221
+ context "during market hours" do
222
+ before { skip_outside_market_hours }
223
+
224
+ it "places a limit order" do
225
+ order = Tastytrade::Order.new(
226
+ type: Tastytrade::OrderType::LIMIT,
227
+ time_in_force: Tastytrade::OrderTimeInForce::DAY,
228
+ legs: build_spy_leg(100, "Buy to Open"),
229
+ price: 430.00
230
+ )
231
+
232
+ response = account.place_order(session, order)
233
+
234
+ expect(response).to be_a(Tastytrade::Models::OrderResponse)
235
+ expect(response.order_id).not_to be_nil
236
+ end
237
+ end
238
+
239
+ context "order validation (market independent)" do
240
+ it "validates order without placing" do
241
+ order = Tastytrade::Order.new(
242
+ type: Tastytrade::OrderType::LIMIT,
243
+ time_in_force: Tastytrade::OrderTimeInForce::DAY,
244
+ legs: build_spy_leg(100, "Buy to Open"),
245
+ price: 430.00
246
+ )
247
+
248
+ # Dry run doesn't require market hours
249
+ response = account.place_order(session, order, dry_run: true)
250
+
251
+ expect(response.buying_power_effect).not_to be_nil
252
+ end
253
+ end
254
+ end
255
+
256
+ private
257
+
258
+ def build_spy_leg(quantity, action)
259
+ Tastytrade::OrderLeg.new(
260
+ action: action,
261
+ symbol: "SPY",
262
+ quantity: quantity
263
+ )
264
+ end
265
+ end
266
+ ```
267
+
268
+ ## Phase 4: Gradual Migration (Weeks 3-4)
269
+
270
+ ### 4.1 Parallel Testing Strategy
271
+ ```ruby
272
+ # spec/spec_helper.rb
273
+ RSpec.configure do |config|
274
+ # Run VCR tests only when marked
275
+ config.around(:each) do |example|
276
+ if example.metadata[:vcr]
277
+ example.run
278
+ elsif example.metadata[:use_mocks]
279
+ # Keep existing mock behavior
280
+ VCR.turned_off { example.run }
281
+ else
282
+ # Default to mocks for now
283
+ VCR.turned_off { example.run }
284
+ end
285
+ end
286
+ end
287
+ ```
288
+
289
+ ### 4.2 Migration Checklist
290
+ - [ ] Session tests converted
291
+ - [ ] Client HTTP tests converted
292
+ - [ ] Account model tests converted
293
+ - [ ] Balance retrieval tests converted
294
+ - [ ] Order placement tests converted
295
+ - [ ] Order history tests converted
296
+ - [ ] Position tests converted
297
+ - [ ] CLI tests converted (mock the API client layer)
298
+
299
+ ## Phase 5: CI/CD Integration (Week 4)
300
+
301
+ ### 5.1 GitHub Actions Configuration
302
+ ```yaml
303
+ # .github/workflows/test.yml
304
+ name: Tests
305
+ on: [push, pull_request]
306
+
307
+ jobs:
308
+ test:
309
+ runs-on: ubuntu-latest
310
+ env:
311
+ VCR_MODE: none # Never record in CI
312
+ TASTYTRADE_SANDBOX_USERNAME: ${{ secrets.TASTYTRADE_SANDBOX_USERNAME }}
313
+ TASTYTRADE_SANDBOX_PASSWORD: ${{ secrets.TASTYTRADE_SANDBOX_PASSWORD }}
314
+ TASTYTRADE_SANDBOX_ACCOUNT: ${{ secrets.TASTYTRADE_SANDBOX_ACCOUNT }}
315
+
316
+ steps:
317
+ - uses: actions/checkout@v4
318
+ with:
319
+ fetch-depth: 1 # Shallow clone for speed
320
+
321
+ - name: Set up Ruby
322
+ uses: ruby/setup-ruby@v1
323
+ with:
324
+ ruby-version: '3.2'
325
+ bundler-cache: true
326
+
327
+ - name: Run tests
328
+ run: bundle exec rspec
329
+ ```
330
+
331
+ ### 5.2 Cassette Maintenance
332
+ ```ruby
333
+ # lib/tasks/vcr.rake
334
+ namespace :vcr do
335
+ desc "Delete cassettes older than 30 days"
336
+ task :clean_old do
337
+ Dir.glob("spec/fixtures/vcr_cassettes/**/*.yml").each do |cassette|
338
+ if File.mtime(cassette) < 30.days.ago
339
+ puts "Deleting old cassette: #{cassette}"
340
+ File.delete(cassette)
341
+ end
342
+ end
343
+ end
344
+
345
+ desc "Re-record all cassettes (run during market hours)"
346
+ task :refresh_all do
347
+ unless market_open?
348
+ puts "ERROR: Market is closed. Run during market hours (9:30 AM - 4:00 PM ET)"
349
+ puts "Next market open: #{next_market_open_time}"
350
+ exit 1
351
+ end
352
+
353
+ ENV['VCR_MODE'] = 'rec'
354
+ system('bundle exec rspec --tag vcr')
355
+ end
356
+
357
+ desc "Show cassette statistics"
358
+ task :stats do
359
+ cassettes = Dir.glob("spec/fixtures/vcr_cassettes/**/*.yml")
360
+ total_size = cassettes.sum { |f| File.size(f) }
361
+
362
+ puts "Total cassettes: #{cassettes.count}"
363
+ puts "Total size: #{(total_size / 1024.0 / 1024.0).round(2)} MB"
364
+ puts "Oldest cassette: #{cassettes.min_by { |f| File.mtime(f) }}"
365
+ puts "Newest cassette: #{cassettes.max_by { |f| File.mtime(f) }}"
366
+ end
367
+ end
368
+ ```
369
+
370
+ ## Phase 6: Documentation (Week 5)
371
+
372
+ ### 6.1 Developer Guide
373
+ Create `docs/vcr_testing.md`:
374
+ - How to record new cassettes
375
+ - Market hours requirements
376
+ - Credential setup
377
+ - Troubleshooting guide
378
+ - Best practices
379
+
380
+ ### 6.2 Git Configuration
381
+ ```gitignore
382
+ # .gitignore
383
+ .env.test
384
+ .env.local
385
+
386
+ # .gitattributes
387
+ spec/fixtures/vcr_cassettes/**/*.yml -diff
388
+ ```
389
+
390
+ ## Success Metrics
391
+ - [ ] All tests pass with VCR cassettes
392
+ - [ ] CI build time reduced by >50%
393
+ - [ ] Tests work offline
394
+ - [ ] No sensitive data in cassettes
395
+ - [ ] Documentation complete
396
+ - [ ] Team trained on VCR workflow
397
+
398
+ ## Risk Mitigation
399
+ 1. **Market Hours Dependency**: Use strict `:once` recording mode
400
+ 2. **Sensitive Data Leaks**: Multiple layers of filtering
401
+ 3. **Cassette Bloat**: Regular cleanup tasks
402
+ 4. **API Changes**: Monthly cassette refresh schedule
403
+ 5. **Team Adoption**: Comprehensive documentation and helpers