Skip to content

Commit 866a6a9

Browse files
committed
update SDK version, add watchlist module
1 parent 44a0035 commit 866a6a9

File tree

13 files changed

+581
-195
lines changed

13 files changed

+581
-195
lines changed

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ You can also install in a virtual environment:
1919
$ pip install tastytrade-cli
2020
```
2121

22-
> [!WARNING]
23-
> The CLI is still under active development. Please report any bugs, and contributions are always welcome!
24-
2522
## Usage
2623

2724
Available commands:
@@ -31,7 +28,7 @@ tt pf (portfolio) view and close positions, check margin and analyze BP usa
3128
tt trade buy or sell stocks/ETFs, crypto, and futures
3229
tt order view, replace, and cancel orders
3330
tt plot plot charts directly in the terminal! requires `gnuplot` installed
34-
tt wl (watchlist) view prices and metrics for symbols in your watchlists (pending further development!)
31+
tt wl (watchlist) view prices and metrics for symbols in your watchlists
3532
```
3633
For more options, run `tt --help` or `tt <subcommand> --help`.
3734

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ name = "tastytrade-cli"
1010
description = "An easy-to-use command line interface for Tastytrade!"
1111
readme = "README.md"
1212
classifiers = [
13-
"Development Status :: 4 - Beta",
13+
"Development Status :: 5 - Production/Stable",
1414
"Environment :: Console",
1515
"Intended Audience :: Developers",
1616
"Intended Audience :: Financial and Insurance Industry",
@@ -34,12 +34,13 @@ classifiers = [
3434
requires-python = ">=3.10"
3535
license = {file = "LICENSE"}
3636
authors = [
37-
{name = "Graeme Holliday", email = "graeme.holliday@pm.me"},
37+
{name = "Graeme Holliday", email = "graeme@tastyware.dev"},
3838
]
3939
dependencies = [
40+
"anyio>=4.6.3",
4041
"py-gnuplot>=1.3",
4142
"rich>=13.8.1",
42-
"tastytrade>=10.2.0",
43+
"tastytrade>=11.0.1",
4344
"typer>=0.15.3",
4445
"yaspin>=3.1.0",
4546
]

ttcli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44

55
CUSTOM_CONFIG_PATH = ".config/ttcli/ttcli.cfg"
66
TOKEN_PATH = ".config/ttcli/.session"
7-
VERSION = "0.6.3"
7+
VERSION = "1.0.0"
88
__version__ = VERSION

ttcli/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ttcli.utils import config_path
1515
from ttcli.watchlist import watchlist
1616

17-
cli = Typer(no_args_is_help=True)
17+
cli = Typer(no_args_is_help=True, pretty_exceptions_show_locals=False)
1818

1919

2020
@cli.callback(invoke_without_command=True, no_args_is_help=True)
@@ -40,7 +40,7 @@ def main(
4040
cli.add_typer(plot, name="plot")
4141
cli.add_typer(portfolio, name="pf")
4242
cli.add_typer(trade, name="trade")
43-
cli.add_typer(watchlist, name=None)
43+
cli.add_typer(watchlist, name="wl")
4444

4545

4646
if __name__ == "__main__":

ttcli/data/ttcli.cfg

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[general]
22
# the username & password can be passed to the CLI in 3 ways: here,
3-
# through the $TT_USERNAME/$TT_PASSWORD environment variables, or,
4-
# if neither is present, entered manually.
5-
# username = foo
6-
# password = bar
3+
# through the $TT_REFRESH/$TT_SECRET environment variables, or, if
4+
# neither is present, entered manually.
5+
# refresh_token = foo
6+
# client_secret = bar
77

88
# the account number to use by default for trades/portfolio commands.
99
# this bypasses the account choice menu.
@@ -50,3 +50,9 @@ show-theta = false
5050
# font for the plot title and labels
5151
font = Courier New
5252
font-size = 11
53+
54+
[watchlist]
55+
# these control whether the columns show up when running `tt wl` commands
56+
show-beta = false
57+
show-dividend-yield = false
58+
show-indicators = false

ttcli/option.py

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import asyncio
2-
from collections import defaultdict
32
from datetime import datetime
43
from decimal import Decimal
54
from typing import Annotated
65

6+
from anyio import move_on_after
77
from rich.console import Console
88
from rich.table import Table
99
from tastytrade import DXLinkStreamer
10-
from tastytrade.dxfeed import Greeks, Summary, Quote, Trade
10+
from tastytrade.dxfeed import Greeks, Quote, Summary, Trade
1111
from tastytrade.instruments import (
1212
Future,
1313
FutureOption,
1414
NestedFutureOptionChain,
1515
NestedFutureOptionChainExpiration,
1616
NestedOptionChain,
1717
NestedOptionChainExpiration,
18+
)
19+
from tastytrade.instruments import (
1820
Option as TastytradeOption,
1921
)
20-
from tastytrade.market_data import get_market_data, get_market_data_by_type
22+
from tastytrade.market_data import get_market_data_by_type
2123
from tastytrade.order import (
22-
InstrumentType,
2324
NewOrder,
2425
OrderAction,
2526
OrderTimeInForce,
@@ -217,11 +218,11 @@ async def call(
217218
bid = data_dict[strike_symbol].bid - data_dict[spread_strike.call].ask # type: ignore
218219
ask = data_dict[strike_symbol].ask - data_dict[spread_strike.call].bid # type: ignore
219220
else:
220-
data = get_market_data(
221+
data = get_market_data_by_type(
221222
sesh,
222-
strike_symbol,
223-
InstrumentType.FUTURE_OPTION if is_future else InstrumentType.EQUITY_OPTION,
224-
)
223+
future_options=[strike_symbol] if is_future else None,
224+
options=[strike_symbol] if not is_future else None,
225+
)[0]
225226
bid = data.bid or 0
226227
ask = data.ask or 0
227228
mid = fmt((bid + ask) / Decimal(2))
@@ -453,11 +454,11 @@ async def put(
453454
bid = data_dict[strike_symbol].bid - data_dict[spread_strike.call].ask # type: ignore
454455
ask = data_dict[strike_symbol].ask - data_dict[spread_strike.call].bid # type: ignore
455456
else:
456-
data = get_market_data(
457+
data = get_market_data_by_type(
457458
sesh,
458-
strike_symbol,
459-
InstrumentType.FUTURE_OPTION if is_future else InstrumentType.EQUITY_OPTION,
460-
)
459+
future_options=[strike_symbol] if is_future else None,
460+
options=[strike_symbol] if not is_future else None,
461+
)[0]
461462
bid = data.bid or 0
462463
ask = data.ask or 0
463464
mid = fmt((bid + ask) / Decimal(2))
@@ -640,7 +641,7 @@ async def strangle(
640641
if (call is not None or put is not None) and delta is not None:
641642
print_error("Must specify either delta or strike, but not both.")
642643
return
643-
elif delta is not None and (call is not None or put is not None):
644+
elif delta is None and (call is None or put is None):
644645
print_error("Please specify either delta, or strikes for both options.")
645646
return
646647
elif delta is not None and abs(delta) > 99:
@@ -901,7 +902,7 @@ async def strangle(
901902
if data.warnings:
902903
for warning in data.warnings:
903904
print_warning(warning.message)
904-
warn_percent = sesh.config.getint(
905+
warn_percent = sesh.config.getfloat(
905906
"portfolio", "bp-max-percent-per-position", fallback=None
906907
)
907908
if warn_percent and percent > warn_percent:
@@ -986,7 +987,10 @@ async def chain(
986987
await streamer.subscribe(Trade, [future.streamer_symbol])
987988
else:
988989
await streamer.subscribe(Trade, [symbol])
989-
trade = await streamer.get_event(Trade)
990+
with move_on_after(3) as scope:
991+
trade = await streamer.get_event(Trade)
992+
if scope.cancelled_caught:
993+
raise Exception("Timed out listening for quote, is symbol active?")
990994

991995
subchain.strikes.sort(key=lambda s: s.strike_price)
992996
mid_index = 0
@@ -1007,8 +1011,8 @@ async def chain(
10071011

10081012
# take into account the symbol we subscribed to
10091013
streamer_symbol = symbol if symbol[0] != "/" else future.streamer_symbol # type: ignore
1010-
trade_dict = defaultdict(lambda: 0)
1011-
trade_dict[streamer_symbol] = trade.day_volume or 0
1014+
trade_dict: dict[str, Trade | None] = {}
1015+
trade_dict[streamer_symbol] = trade
10121016

10131017
greeks_task = asyncio.create_task(listen_events(dxfeeds, Greeks, streamer))
10141018
quote_task = asyncio.create_task(listen_events(dxfeeds, Quote, streamer))
@@ -1029,38 +1033,37 @@ async def chain(
10291033
if show_oi:
10301034
summary_dict = summary_task.result() # type: ignore
10311035
if show_volume:
1032-
trade_dict = trade_task.result() # type: ignore
1036+
trade_dict.update(trade_task.result()) # type: ignore
10331037

10341038
for i, strike in enumerate(all_strikes):
1035-
put_bid = quote_dict[strike.put_streamer_symbol].bid_price
1036-
put_ask = quote_dict[strike.put_streamer_symbol].ask_price
1037-
call_bid = quote_dict[strike.call_streamer_symbol].bid_price
1038-
call_ask = quote_dict[strike.call_streamer_symbol].ask_price
1039+
put = quote_dict[strike.put_streamer_symbol]
1040+
call = quote_dict[strike.call_streamer_symbol]
10391041
row = [
1040-
f"{fmt(call_bid)}",
1041-
f"{fmt(call_ask)}",
1042+
f"{fmt(call.bid_price)}" if call else "",
1043+
f"{fmt(call.ask_price)}" if call else "",
10421044
f"{fmt(strike.strike_price)}",
1043-
f"{fmt(put_bid)}",
1044-
f"{fmt(put_ask)}",
1045+
f"{fmt(put.bid_price)}" if put else "",
1046+
f"{fmt(put.ask_price)}" if put else "",
10451047
]
10461048
prepend = []
1049+
put_greek = greeks_dict[strike.put_streamer_symbol]
1050+
call_greek = greeks_dict[strike.call_streamer_symbol]
10471051
if show_delta:
1048-
put_delta = int(greeks_dict[strike.put_streamer_symbol].delta * 100)
1049-
call_delta = int(greeks_dict[strike.call_streamer_symbol].delta * 100)
1050-
prepend.append(f"{call_delta:g}")
1051-
row.append(f"{put_delta:g}")
1052-
1052+
prepend.append(f"{int(call_greek.delta * 100):g}" if call_greek else "")
1053+
row.append(f"{int(put_greek.delta * 100):g}" if put_greek else "")
10531054
if show_theta:
1054-
prepend.append(f"{abs(greeks_dict[strike.call_streamer_symbol].theta):.2f}")
1055-
row.append(f"{abs(greeks_dict[strike.put_streamer_symbol].theta):.2f}")
1055+
prepend.append(f"{abs(call_greek.theta):.2f}" if call_greek else "")
1056+
row.append(f"{abs(put_greek.theta):.2f}" if put_greek else "")
10561057
if show_oi:
1057-
prepend.append(
1058-
f"{summary_dict[strike.call_streamer_symbol].open_interest}" # type: ignore
1059-
)
1060-
row.append(f"{summary_dict[strike.put_streamer_symbol].open_interest}") # type: ignore
1058+
call_summary = summary_dict[strike.call_streamer_symbol] # type: ignore
1059+
put_summary = summary_dict[strike.put_streamer_symbol] # type: ignore
1060+
prepend.append(f"{call_summary.open_interest}" if call_summary else "")
1061+
row.append(f"{put_summary.open_interest}" if put_summary else "")
10611062
if show_volume:
1062-
prepend.append(f"{trade_dict[strike.call_streamer_symbol].day_volume}") # type: ignore
1063-
row.append(f"{trade_dict[strike.put_streamer_symbol].day_volume}") # type: ignore
1063+
call_trade = trade_dict[strike.call_streamer_symbol]
1064+
put_trade = trade_dict[strike.put_streamer_symbol]
1065+
prepend.append(f"{call_trade.day_volume or 0}" if call_trade else "")
1066+
row.append(f"{put_trade.day_volume or 0}" if put_trade else "")
10641067

10651068
prepend.reverse()
10661069
table.add_row(*(prepend + row), end_section=(i == mid_index - 1))

ttcli/order.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ def live(
4343
for o in orders
4444
if o.status == OrderStatus.LIVE or o.status == OrderStatus.RECEIVED
4545
]
46+
if not orders:
47+
return
4648
instrument_dict: dict[InstrumentType, set[str]] = defaultdict(set)
4749
for o in orders:
4850
for leg in o.legs:

ttcli/plot.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,30 @@ def gnuplot(sesh: RenewableSession, symbol: str, candles: list[str]) -> None:
6363
f.write("\n".join(candles))
6464
first = candles[0].split(",")[0]
6565
last = candles[-1].split(",")[0]
66-
total_time = datetime.strptime(last, fmt) - datetime.strptime(first, fmt)
66+
first_dt = datetime.strptime(first, fmt)
67+
last_dt = datetime.strptime(last, fmt)
68+
total_time = last_dt - first_dt
6769
boxwidth = int(total_time.total_seconds() / len(candles) * 0.5)
70+
padding = timedelta(seconds=boxwidth)
71+
first_padded = (first_dt - padding).strftime(fmt)
72+
last_padded = (last_dt + padding).strftime(fmt)
6873
font = sesh.config.get("plot", "font", fallback="Courier New")
6974
font_size = sesh.config.getint("plot", "font-size", fallback=11)
7075
gnu.set(
7176
terminal=f"kittycairo transparent font '{font},{font_size}'",
7277
xdata="time",
7378
timefmt=f'"{fmt}"',
74-
xrange=f'["{first}":"{last}"]',
79+
xrange=f'["{first_padded}":"{last_padded}"]',
7580
yrange="[*:*]",
7681
datafile='separator ","',
7782
palette="defined (-1 '#D32F2F', 1 '#26BE81')",
7883
cbrange="[-1:1]",
7984
style="fill solid noborder",
8085
boxwidth=f"{boxwidth} absolute",
8186
title=f'"{symbol}" textcolor rgb "white"',
82-
border="31 lc rgb 'white'",
83-
xtics="textcolor rgb 'white'",
84-
ytics="textcolor rgb 'white'",
87+
border="3 lc rgb 'white'",
88+
xtics="nomirror rotate by -45 textcolor rgb 'white' scale 0",
89+
ytics="nomirror textcolor rgb 'white' scale 0",
8590
)
8691
gnu.unset("colorbox")
8792
os.system("clear")

0 commit comments

Comments
 (0)