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
@@ -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
|