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
data/ROADMAP.md CHANGED
@@ -24,16 +24,16 @@ This document outlines the development roadmap for the unofficial Tastytrade Rub
24
24
  #### Account Operations
25
25
  - [x] Fetch account info and balances (closes #6)
26
26
  - [x] Get positions
27
- - [ ] Get transaction history
28
- - [ ] Calculate buying power
29
- - [ ] Account status and trading permissions
27
+ - [x] Get transaction history (closes #8)
28
+ - [x] Calculate buying power (closes #9)
29
+ - [x] Account status and trading permissions (closes #10)
30
30
 
31
31
  #### Basic Trading
32
- - [ ] Place equity orders (market, limit)
33
- - [ ] Cancel/replace orders
34
- - [ ] Get order status
35
- - [ ] Order validation
36
- - [ ] Basic order types (day, GTC)
32
+ - [x] Place equity orders (market, limit) (closes #11)
33
+ - [x] Cancel/replace orders (closes #12)
34
+ - [x] Get order status (closes #13)
35
+ - [x] Order validation (closes #14)
36
+ - [x] Basic order types (day, GTC) (closes #15)
37
37
 
38
38
  #### Core Infrastructure
39
39
  - [x] HTTP client setup (Faraday) (closes #16)
@@ -77,11 +77,11 @@ This document outlines the development roadmap for the unofficial Tastytrade Rub
77
77
  **Target: Q1 2026 (January - March)**
78
78
 
79
79
  #### Core CLI Commands
80
- - [ ] Authentication (`tt login`)
81
- - [ ] Account info (`tt account`)
82
- - [ ] Portfolio view (`tt portfolio`)
83
- - [ ] Basic trading (`tt trade`)
84
- - [ ] Order management (`tt orders`)
80
+ - [x] Authentication (`tt login`)
81
+ - [x] Account info (`tt account`)
82
+ - [x] Portfolio view (`tt portfolio`)
83
+ - [x] Basic trading (`tt trade`)
84
+ - [x] Order management (`tt orders`)
85
85
 
86
86
  #### Options CLI
87
87
  - [ ] Option chains (`tt option chain`)
@@ -94,13 +94,13 @@ This document outlines the development roadmap for the unofficial Tastytrade Rub
94
94
  - [ ] Portfolio analysis (`tt analyze`)
95
95
  - [ ] Real-time quotes (`tt quote`)
96
96
  - [ ] Configuration management (`tt config`)
97
- - [ ] Interactive mode
97
+ - [x] Interactive mode
98
98
 
99
99
  #### CLI Enhancements
100
- - [ ] Rich terminal output (TTY gems)
100
+ - [x] Rich terminal output (TTY gems)
101
101
  - [ ] Progress indicators
102
- - [ ] Confirmation prompts
103
- - [ ] Output formatting (JSON, CSV, table)
102
+ - [x] Confirmation prompts
103
+ - [x] Output formatting (JSON, CSV, table)
104
104
  - [ ] Shell completion
105
105
 
106
106
  ### Phase 4: Advanced Features & Polish
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-table"
4
+ require "bigdecimal"
5
+
6
+ module Tastytrade
7
+ # Formatter for displaying transaction history in various formats
8
+ class HistoryFormatter
9
+ def initialize(pastel: nil)
10
+ @pastel = pastel || Pastel.new
11
+ end
12
+
13
+ # Format transactions as a table
14
+ def format_table(transactions, group_by: nil)
15
+ return if transactions.empty?
16
+
17
+ case group_by
18
+ when :symbol
19
+ format_by_symbol(transactions)
20
+ when :type
21
+ format_by_type(transactions)
22
+ when :date
23
+ format_by_date(transactions)
24
+ else
25
+ format_detailed_table(transactions)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def format_detailed_table(transactions)
32
+ headers = ["Date", "Symbol", "Type", "Action", "Qty", "Price", "Value", "Fees", "Net"]
33
+ rows = build_detailed_rows(transactions)
34
+
35
+ table = TTY::Table.new(headers, rows)
36
+
37
+ begin
38
+ puts table.render(:unicode, padding: [0, 1])
39
+ rescue StandardError
40
+ # Fallback for testing or non-TTY environments
41
+ puts headers.join(" | ")
42
+ puts "-" * 120
43
+ rows.each { |row| puts row.join(" | ") }
44
+ end
45
+
46
+ display_detailed_summary(transactions)
47
+ end
48
+
49
+ def format_by_symbol(transactions)
50
+ grouped = transactions.group_by(&:symbol)
51
+
52
+ grouped.each do |symbol, symbol_transactions|
53
+ next unless symbol # Skip transactions without symbols
54
+
55
+ puts @pastel.bold("\n#{symbol}")
56
+ puts "-" * 80
57
+
58
+ headers = ["Date", "Type", "Action", "Qty", "Price", "Value", "Net"]
59
+ rows = build_symbol_rows(symbol_transactions)
60
+
61
+ table = TTY::Table.new(headers, rows)
62
+ begin
63
+ puts table.render(:unicode, padding: [0, 1])
64
+ rescue StandardError
65
+ puts headers.join(" | ")
66
+ puts "-" * 80
67
+ rows.each { |row| puts row.join(" | ") }
68
+ end
69
+
70
+ display_symbol_summary(symbol_transactions)
71
+ end
72
+
73
+ display_detailed_summary(transactions)
74
+ end
75
+
76
+ def format_by_type(transactions)
77
+ grouped = transactions.group_by(&:transaction_type)
78
+
79
+ grouped.each do |type, type_transactions|
80
+ puts @pastel.bold("\n#{type}")
81
+ puts "-" * 80
82
+
83
+ headers = ["Date", "Symbol", "Action", "Qty", "Price", "Value", "Net"]
84
+ rows = build_type_rows(type_transactions)
85
+
86
+ table = TTY::Table.new(headers, rows)
87
+ begin
88
+ puts table.render(:unicode, padding: [0, 1])
89
+ rescue StandardError
90
+ puts headers.join(" | ")
91
+ puts "-" * 80
92
+ rows.each { |row| puts row.join(" | ") }
93
+ end
94
+
95
+ display_type_summary(type_transactions)
96
+ end
97
+
98
+ display_detailed_summary(transactions)
99
+ end
100
+
101
+ def format_by_date(transactions)
102
+ grouped = transactions.group_by { |t| t.transaction_date&.strftime("%Y-%m-%d") }
103
+
104
+ grouped.keys.sort.reverse.each do |date|
105
+ date_transactions = grouped[date]
106
+ next unless date # Skip transactions without dates
107
+
108
+ puts @pastel.bold("\n#{date}")
109
+ puts "-" * 80
110
+
111
+ headers = ["Symbol", "Type", "Action", "Qty", "Price", "Value", "Net"]
112
+ rows = build_date_rows(date_transactions)
113
+
114
+ table = TTY::Table.new(headers, rows)
115
+ begin
116
+ puts table.render(:unicode, padding: [0, 1])
117
+ rescue StandardError
118
+ puts headers.join(" | ")
119
+ puts "-" * 80
120
+ rows.each { |row| puts row.join(" | ") }
121
+ end
122
+
123
+ display_date_summary(date_transactions)
124
+ end
125
+
126
+ display_detailed_summary(transactions)
127
+ end
128
+
129
+ def build_detailed_rows(transactions)
130
+ transactions.map do |transaction|
131
+ [
132
+ format_date(transaction.transaction_date),
133
+ transaction.symbol || "-",
134
+ truncate(transaction.transaction_type, 12),
135
+ truncate(transaction.action || transaction.transaction_sub_type, 12),
136
+ format_quantity(transaction.quantity),
137
+ format_currency(transaction.price),
138
+ format_value(transaction.value, transaction.value_effect),
139
+ format_fees(transaction),
140
+ format_value(transaction.net_value, transaction.net_value_effect)
141
+ ]
142
+ end
143
+ end
144
+
145
+ def build_symbol_rows(transactions)
146
+ transactions.map do |transaction|
147
+ [
148
+ format_date(transaction.transaction_date),
149
+ truncate(transaction.transaction_type, 12),
150
+ truncate(transaction.action || transaction.transaction_sub_type, 12),
151
+ format_quantity(transaction.quantity),
152
+ format_currency(transaction.price),
153
+ format_value(transaction.value, transaction.value_effect),
154
+ format_value(transaction.net_value, transaction.net_value_effect)
155
+ ]
156
+ end
157
+ end
158
+
159
+ def build_type_rows(transactions)
160
+ transactions.map do |transaction|
161
+ [
162
+ format_date(transaction.transaction_date),
163
+ transaction.symbol || "-",
164
+ truncate(transaction.action || transaction.transaction_sub_type, 12),
165
+ format_quantity(transaction.quantity),
166
+ format_currency(transaction.price),
167
+ format_value(transaction.value, transaction.value_effect),
168
+ format_value(transaction.net_value, transaction.net_value_effect)
169
+ ]
170
+ end
171
+ end
172
+
173
+ def build_date_rows(transactions)
174
+ transactions.map do |transaction|
175
+ [
176
+ transaction.symbol || "-",
177
+ truncate(transaction.transaction_type, 12),
178
+ truncate(transaction.action || transaction.transaction_sub_type, 12),
179
+ format_quantity(transaction.quantity),
180
+ format_currency(transaction.price),
181
+ format_value(transaction.value, transaction.value_effect),
182
+ format_value(transaction.net_value, transaction.net_value_effect)
183
+ ]
184
+ end
185
+ end
186
+
187
+ def format_date(date)
188
+ return "-" unless date
189
+ date.strftime("%m/%d/%y")
190
+ end
191
+
192
+ def format_quantity(quantity)
193
+ return "-" unless quantity
194
+ quantity.to_i.to_s
195
+ end
196
+
197
+ def format_currency(amount)
198
+ return "-" unless amount
199
+ "$#{"%.2f" % amount.to_f}"
200
+ end
201
+
202
+ def format_value(amount, effect)
203
+ return "-" unless amount
204
+
205
+ formatted = format_currency(amount.abs)
206
+
207
+ if effect == "Debit" || (amount && amount < 0)
208
+ @pastel.red("-#{formatted}")
209
+ elsif effect == "Credit" || (amount && amount > 0)
210
+ @pastel.green("+#{formatted}")
211
+ else
212
+ formatted
213
+ end
214
+ end
215
+
216
+ def format_fees(transaction)
217
+ total_fees = [
218
+ transaction.commission,
219
+ transaction.clearing_fees,
220
+ transaction.regulatory_fees,
221
+ transaction.proprietary_index_option_fees
222
+ ].compact.reduce(BigDecimal("0"), :+)
223
+
224
+ return "-" if total_fees.zero?
225
+ format_currency(total_fees)
226
+ end
227
+
228
+ def truncate(text, length)
229
+ return "-" unless text
230
+ text.length > length ? "#{text[0...length - 2]}.." : text
231
+ end
232
+
233
+ def display_detailed_summary(transactions)
234
+ total_credits = BigDecimal("0")
235
+ total_debits = BigDecimal("0")
236
+ total_fees = BigDecimal("0")
237
+
238
+ transactions.each do |t|
239
+ if t.value_effect == "Credit" || (t.value && t.value > 0)
240
+ total_credits += t.value.abs if t.value
241
+ elsif t.value_effect == "Debit" || (t.value && t.value < 0)
242
+ total_debits += t.value.abs if t.value
243
+ end
244
+
245
+ total_fees += calculate_total_fees(t)
246
+ end
247
+
248
+ net_flow = total_credits - total_debits - total_fees
249
+
250
+ puts
251
+ puts @pastel.bold("Transaction Summary")
252
+ puts "-" * 40
253
+ puts "Total Transactions: #{transactions.size}"
254
+ puts "Total Credits: #{@pastel.green(format_currency(total_credits))}"
255
+ puts "Total Debits: #{@pastel.red(format_currency(total_debits))}"
256
+ puts "Total Fees: #{@pastel.yellow(format_currency(total_fees))}"
257
+ puts "Net Cash Flow: #{format_net_flow(net_flow)}"
258
+
259
+ # Group by type for summary
260
+ by_type = transactions.group_by(&:transaction_type)
261
+ puts
262
+ puts @pastel.bold("By Transaction Type:")
263
+ by_type.each do |type, type_transactions|
264
+ puts " #{type}: #{type_transactions.size} transactions"
265
+ end
266
+ end
267
+
268
+ def display_symbol_summary(transactions)
269
+ total_value = transactions.sum { |t| t.net_value || BigDecimal("0") }
270
+ puts @pastel.dim("\nSymbol Total: #{format_net_flow(total_value)} (#{transactions.size} transactions)")
271
+ end
272
+
273
+ def display_type_summary(transactions)
274
+ total_value = transactions.sum { |t| t.net_value || BigDecimal("0") }
275
+ puts @pastel.dim("\nType Total: #{format_net_flow(total_value)} (#{transactions.size} transactions)")
276
+ end
277
+
278
+ def display_date_summary(transactions)
279
+ total_value = transactions.sum { |t| t.net_value || BigDecimal("0") }
280
+ puts @pastel.dim("\nDaily Total: #{format_net_flow(total_value)} (#{transactions.size} transactions)")
281
+ end
282
+
283
+ def calculate_total_fees(transaction)
284
+ [
285
+ transaction.commission,
286
+ transaction.clearing_fees,
287
+ transaction.regulatory_fees,
288
+ transaction.proprietary_index_option_fees
289
+ ].compact.reduce(BigDecimal("0"), :+)
290
+ end
291
+
292
+ def format_net_flow(amount)
293
+ formatted = format_currency(amount.abs)
294
+
295
+ if amount > 0
296
+ @pastel.green("+#{formatted}")
297
+ elsif amount < 0
298
+ @pastel.red("-#{formatted}")
299
+ else
300
+ formatted
301
+ end
302
+ end
303
+ end
304
+ end