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.
Files changed (57) 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 +180 -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/tastytrade/cli/positions_spec.rb +267 -0
  32. data/spec/tastytrade/cli_auth_spec.rb +5 -0
  33. data/spec/tastytrade/cli_env_login_spec.rb +199 -0
  34. data/spec/tastytrade/cli_helpers_spec.rb +3 -26
  35. data/spec/tastytrade/cli_orders_spec.rb +168 -0
  36. data/spec/tastytrade/cli_status_spec.rb +153 -164
  37. data/spec/tastytrade/file_store_spec.rb +126 -0
  38. data/spec/tastytrade/models/account_balance_spec.rb +103 -0
  39. data/spec/tastytrade/models/account_order_history_spec.rb +229 -0
  40. data/spec/tastytrade/models/account_order_management_spec.rb +271 -0
  41. data/spec/tastytrade/models/account_place_order_spec.rb +125 -0
  42. data/spec/tastytrade/models/account_spec.rb +86 -15
  43. data/spec/tastytrade/models/buying_power_effect_spec.rb +250 -0
  44. data/spec/tastytrade/models/live_order_json_spec.rb +144 -0
  45. data/spec/tastytrade/models/live_order_spec.rb +295 -0
  46. data/spec/tastytrade/models/order_response_spec.rb +96 -0
  47. data/spec/tastytrade/models/order_status_spec.rb +113 -0
  48. data/spec/tastytrade/models/trading_status_spec.rb +260 -0
  49. data/spec/tastytrade/models/transaction_spec.rb +236 -0
  50. data/spec/tastytrade/order_edge_cases_spec.rb +163 -0
  51. data/spec/tastytrade/order_spec.rb +201 -0
  52. data/spec/tastytrade/order_validator_spec.rb +347 -0
  53. data/spec/tastytrade/session_env_spec.rb +169 -0
  54. data/spec/tastytrade/session_manager_spec.rb +43 -33
  55. metadata +34 -18
  56. data/lib/tastytrade/keyring_store.rb +0 -72
  57. data/spec/tastytrade/keyring_store_spec.rb +0 -168
@@ -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