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
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
|
-
- [
|
28
|
-
- [
|
29
|
-
- [
|
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
|
-
- [
|
33
|
-
- [
|
34
|
-
- [
|
35
|
-
- [
|
36
|
-
- [
|
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
|
-
- [
|
81
|
-
- [
|
82
|
-
- [
|
83
|
-
- [
|
84
|
-
- [
|
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
|
-
- [
|
97
|
+
- [x] Interactive mode
|
98
98
|
|
99
99
|
#### CLI Enhancements
|
100
|
-
- [
|
100
|
+
- [x] Rich terminal output (TTY gems)
|
101
101
|
- [ ] Progress indicators
|
102
|
-
- [
|
103
|
-
- [
|
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
|