Skip to content

Commit e5f3836

Browse files
committed
plotting for other instruments
1 parent 8a8a462 commit e5f3836

File tree

9 files changed

+450
-349
lines changed

9 files changed

+450
-349
lines changed

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,7 @@ tt pf (portfolio) view and close positions, check margin and analyze BP usa
3131
tt trade buy or sell stocks/ETFs, crypto, and futures
3232
tt order view, replace, and cancel orders
3333
tt plot plot charts directly in the terminal! requires `gnuplot` installed
34-
```
35-
Unavailable commands pending development:
36-
```
37-
tt wl (watchlist) view current prices and other data for symbols in your watchlists
34+
tt wl (watchlist) view prices and metrics for symbols in your watchlists (pending further development!)
3835
```
3936
For more options, run `tt --help` or `tt <subcommand> --help`.
4037

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ authors = [
3939
dependencies = [
4040
"py-gnuplot>=1.3",
4141
"rich>=13.8.1",
42-
"tastytrade>=10.1.1",
42+
"tastytrade>=10.2.0",
4343
"typer>=0.15.3",
4444
]
4545
dynamic = ["version"]

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.5.0"
7+
VERSION = "0.6.0"
88
__version__ = VERSION

ttcli/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ttcli.portfolio import portfolio
1313
from ttcli.trade import trade
1414
from ttcli.utils import config_path
15+
from ttcli.watchlist import watchlist
1516

1617
cli = Typer()
1718

@@ -39,6 +40,7 @@ def main(
3940
cli.add_typer(plot, name="plot")
4041
cli.add_typer(portfolio, name="pf")
4142
cli.add_typer(trade, name="trade")
43+
cli.add_typer(watchlist, name=None)
4244

4345

4446
if __name__ == "__main__":

ttcli/plot.py

Lines changed: 111 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
from pygnuplot.gnuplot import Gnuplot
88
from tastytrade import DXLinkStreamer
99
from tastytrade.dxfeed import Candle
10+
from tastytrade.instruments import Cryptocurrency, Future, FutureProduct
1011
from tastytrade.utils import NYSE, TZ, now_in_new_york
1112
from typer import Option
1213

13-
from ttcli.utils import AsyncTyper, RenewableSession
14+
from ttcli.utils import AsyncTyper, RenewableSession, print_error
1415

1516
plot = AsyncTyper(help="Plot candle charts, portfolio P&L, or net liquidating value.")
1617
fmt = "%Y-%m-%d %H:%M:%S"
@@ -28,13 +29,7 @@ class CandleType(str, Enum):
2829
YEAR = "1y"
2930

3031

31-
@plot.command(help="Plot candle chart for the given symbol.")
32-
async def stock(
33-
symbol: str,
34-
width: Annotated[
35-
CandleType, Option("--width", "-w", help="Interval of time for each candle.")
36-
] = CandleType.HALF_HOUR,
37-
):
32+
def get_start_time(width: CandleType) -> datetime:
3833
now = now_in_new_york()
3934
today = now.date()
4035
end = today if now.time() > time(9, 30) else today - timedelta(days=1)
@@ -50,23 +45,10 @@ async def stock(
5045
else:
5146
valid_days = NYSE.valid_days(today - timedelta(days=5), end).to_pydatetime() # type: ignore
5247
start_day = valid_days[-1].date()
53-
start_time = datetime.combine(start_day, time(9, 30), TZ)
54-
sesh = RenewableSession()
55-
candles: list[str] = []
56-
ts = round(start_time.timestamp() * 1000)
57-
async with DXLinkStreamer(sesh) as streamer:
58-
await streamer.subscribe_candle([symbol], width.value, start_time)
59-
async for candle in streamer.listen(Candle):
60-
if candle.close:
61-
date_str = datetime.strftime(
62-
datetime.fromtimestamp(candle.time / 1000, TZ), fmt
63-
)
64-
candles.append(
65-
f"{date_str},{candle.open},{candle.high},{candle.low},{candle.close}",
66-
)
67-
if candle.time == ts:
68-
break
69-
candles.sort()
48+
return datetime.combine(start_day, time(9, 30), TZ)
49+
50+
51+
def gnuplot(sesh: RenewableSession, symbol: str, candles: list[str]) -> None:
7052
gnu = Gnuplot()
7153
tmp = tempfile.NamedTemporaryFile(delete=False)
7254
with open(tmp.name, "w") as f:
@@ -100,3 +82,107 @@ async def stock(
10082
)
10183
_ = input()
10284
os.system("clear")
85+
86+
87+
@plot.command(help="Plot candle chart for the given symbol.")
88+
async def stock(
89+
symbol: str,
90+
width: Annotated[
91+
CandleType, Option("--width", "-w", help="Interval of time for each candle.")
92+
] = CandleType.HALF_HOUR,
93+
):
94+
sesh = RenewableSession()
95+
candles: list[str] = []
96+
start_time = get_start_time(width)
97+
ts = round(start_time.timestamp() * 1000)
98+
async with DXLinkStreamer(sesh) as streamer:
99+
await streamer.subscribe_candle([symbol], width.value, start_time)
100+
async for candle in streamer.listen(Candle):
101+
if candle.close:
102+
date_str = datetime.strftime(
103+
datetime.fromtimestamp(candle.time / 1000, TZ), fmt
104+
)
105+
candles.append(
106+
f"{date_str},{candle.open},{candle.high},{candle.low},{candle.close}",
107+
)
108+
if candle.time == ts:
109+
break
110+
candles.sort()
111+
gnuplot(sesh, symbol, candles)
112+
113+
114+
@plot.command(help="Plot candle chart for the given symbol.")
115+
async def crypto(
116+
symbol: str,
117+
width: Annotated[
118+
CandleType, Option("--width", "-w", help="Interval of time for each candle.")
119+
] = CandleType.HALF_HOUR,
120+
):
121+
sesh = RenewableSession()
122+
symbol = symbol.upper()
123+
if "USD" not in symbol:
124+
symbol += "/USD"
125+
elif "/" not in symbol:
126+
symbol = symbol.split("USD")[0] + "/USD"
127+
crypto = Cryptocurrency.get(sesh, symbol)
128+
candles: list[str] = []
129+
start_time = get_start_time(width)
130+
ts = round(start_time.timestamp() * 1000)
131+
if not crypto.streamer_symbol:
132+
raise Exception("Missing streamer symbol for instrument!")
133+
async with DXLinkStreamer(sesh) as streamer:
134+
await streamer.subscribe_candle(
135+
[crypto.streamer_symbol], width.value, start_time
136+
)
137+
async for candle in streamer.listen(Candle):
138+
if candle.close:
139+
date_str = datetime.strftime(
140+
datetime.fromtimestamp(candle.time / 1000, TZ), fmt
141+
)
142+
candles.append(
143+
f"{date_str},{candle.open},{candle.high},{candle.low},{candle.close}",
144+
)
145+
if candle.time == ts:
146+
break
147+
candles.sort()
148+
gnuplot(sesh, crypto.symbol, candles)
149+
150+
151+
@plot.command(help="Plot candle chart for the given symbol.")
152+
async def future(
153+
symbol: str,
154+
width: Annotated[
155+
CandleType, Option("--width", "-w", help="Interval of time for each candle.")
156+
] = CandleType.HALF_HOUR,
157+
):
158+
sesh = RenewableSession()
159+
symbol = symbol.upper()
160+
if symbol[0] != "/":
161+
symbol = "/" + symbol
162+
if not any(c.isdigit() for c in symbol):
163+
product = FutureProduct.get(sesh, symbol)
164+
_fmt = ",".join([f" {m.name} ({m.value})" for m in product.active_months])
165+
print_error(
166+
f"Please enter the full futures symbol!\nCurrent active months:{_fmt}"
167+
)
168+
return
169+
future = Future.get(sesh, symbol)
170+
candles: list[str] = []
171+
start_time = get_start_time(width)
172+
ts = round(start_time.timestamp() * 1000)
173+
async with DXLinkStreamer(sesh) as streamer:
174+
await streamer.subscribe_candle(
175+
[future.streamer_symbol], width.value, start_time
176+
)
177+
async for candle in streamer.listen(Candle):
178+
if candle.close:
179+
date_str = datetime.strftime(
180+
datetime.fromtimestamp(candle.time / 1000, TZ), fmt
181+
)
182+
candles.append(
183+
f"{date_str},{candle.open},{candle.high},{candle.low},{candle.close}",
184+
)
185+
if candle.time == ts:
186+
break
187+
candles.sort()
188+
gnuplot(sesh, future.symbol, candles)

ttcli/portfolio.py

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import asyncio
21
from collections import defaultdict
32
from datetime import date, datetime
43
from decimal import Decimal
@@ -8,7 +7,7 @@
87
from rich.table import Table
98
from tastytrade import DXLinkStreamer
109
from tastytrade.account import MarginReportEntry
11-
from tastytrade.dxfeed import Greeks, Summary, Trade
10+
from tastytrade.dxfeed import Greeks
1211
from tastytrade.instruments import (
1312
Cryptocurrency,
1413
Equity,
@@ -17,7 +16,7 @@
1716
Option as TastytradeOption,
1817
TickSize,
1918
)
20-
from tastytrade.market_data import get_market_data
19+
from tastytrade.market_data import a_get_market_data_by_type, get_market_data
2120
from tastytrade.metrics import MarketMetricInfo, get_market_metrics
2221
from tastytrade.order import (
2322
InstrumentType,
@@ -108,9 +107,11 @@ async def positions(
108107
greeks_symbols = [o.streamer_symbol for o in options] + [
109108
fo.streamer_symbol for fo in future_options
110109
]
111-
equity_symbols = [
112-
p.symbol for p in positions if p.instrument_type == InstrumentType.EQUITY
113-
] + [o.underlying_symbol for o in options]
110+
equity_symbols = (
111+
[p.symbol for p in positions if p.instrument_type == InstrumentType.EQUITY]
112+
+ [o.underlying_symbol for o in options]
113+
+ ["SPY"]
114+
)
114115
equities = Equity.get(sesh, equity_symbols)
115116
equity_dict = {e.symbol: e for e in equities}
116117
all_symbols = (
@@ -125,20 +126,22 @@ async def positions(
125126
+ greeks_symbols
126127
)
127128
all_symbols = [s for s in all_symbols if s]
128-
async with DXLinkStreamer(sesh) as streamer:
129-
greeks_task = asyncio.create_task(
130-
listen_events(greeks_symbols, Greeks, streamer)
131-
)
132-
summary_task = asyncio.create_task(
133-
listen_events(all_symbols, Summary, streamer)
134-
)
135-
await asyncio.gather(greeks_task, summary_task)
136-
await streamer.subscribe(Trade, ["SPY"])
137-
spy = await streamer.get_event(Trade)
138-
greeks_dict = greeks_task.result()
139-
summary_dict = {
140-
k: v.prev_day_close_price or ZERO for k, v in summary_task.result().items()
141-
}
129+
if greeks_symbols:
130+
async with DXLinkStreamer(sesh) as streamer:
131+
greeks_dict = await listen_events(greeks_symbols, Greeks, streamer)
132+
else:
133+
greeks_dict = {}
134+
data = await a_get_market_data_by_type(
135+
sesh,
136+
cryptocurrencies=crypto_symbols or None,
137+
equities=equity_symbols,
138+
futures=futures_symbols or None,
139+
options=options_symbols or None,
140+
future_options=future_options_symbols or None,
141+
)
142+
data_dict = {d.symbol: d for d in data}
143+
prev_close = lambda s: data_dict[s].prev_close or ZERO
144+
spy = data_dict["SPY"].last or ZERO
142145
tt_symbols = set(pos.symbol for pos in positions)
143146
tt_symbols.update(set(o.underlying_symbol for o in options))
144147
tt_symbols.update(set(o.underlying_symbol for o in future_options))
@@ -204,12 +207,12 @@ async def positions(
204207
metrics = metrics_dict[o.underlying_symbol]
205208
ticks = equity_dict[o.underlying_symbol].option_tick_sizes or []
206209
beta = metrics.beta or 0
207-
bwd = beta * mark * delta / spy.price
210+
bwd = beta * mark * delta / spy
208211
ivr = (metrics.tos_implied_volatility_index_rank or 0) * 100
209212
indicators = get_indicators(today, metrics)
210213
trade_price = pos.average_open_price
211214
pnl = (mark_price - trade_price) * m * pos.multiplier
212-
day_change = mark_price - summary_dict[o.streamer_symbol]
215+
day_change = mark_price - prev_close(o.symbol)
213216
pnl_day = day_change * pos.quantity * pos.multiplier
214217
elif pos.instrument_type == InstrumentType.FUTURE_OPTION:
215218
o = future_options_dict[pos.symbol]
@@ -223,14 +226,14 @@ async def positions(
223226
metrics = metrics_dict[o.root_symbol]
224227
indicators = get_indicators(today, metrics)
225228
bwd = (
226-
(summary_dict[f.streamer_symbol] * metrics.beta * delta / spy.price)
229+
(prev_close(f.symbol) * metrics.beta * delta / spy)
227230
if metrics.beta
228231
else 0
229232
)
230233
ivr = (metrics.tos_implied_volatility_index_rank or 0) * 100
231234
trade_price = pos.average_open_price
232235
pnl = (mark_price - trade_price) * m * pos.multiplier
233-
day_change = mark_price - summary_dict[o.streamer_symbol]
236+
day_change = mark_price - prev_close(o.symbol)
234237
pnl_day = day_change * pos.quantity * pos.multiplier
235238
elif pos.instrument_type == InstrumentType.EQUITY:
236239
theta = 0
@@ -243,11 +246,11 @@ async def positions(
243246
closing[i + 1] = e
244247
beta = metrics.beta or 0
245248
indicators = get_indicators(today, metrics)
246-
bwd = beta * mark_price * delta / spy.price
249+
bwd = beta * mark_price * delta / spy
247250
ivr = (metrics.tos_implied_volatility_index_rank or 0) * 100
248251
pnl = mark - pos.average_open_price * pos.quantity * m
249252
trade_price = pos.average_open_price
250-
day_change = mark_price - summary_dict[pos.symbol]
253+
day_change = mark_price - prev_close(pos.symbol)
251254
pnl_day = day_change * pos.quantity
252255
elif pos.instrument_type == InstrumentType.FUTURE:
253256
theta = 0
@@ -259,11 +262,11 @@ async def positions(
259262
# BWD = beta * stock price * delta / index price
260263
metrics = metrics_dict[f.future_product.root_symbol] # type: ignore
261264
indicators = get_indicators(today, metrics)
262-
bwd = (metrics.beta * mark_price * delta / spy.price) if metrics.beta else 0
265+
bwd = (metrics.beta * mark_price * delta / spy) if metrics.beta else 0
263266
ivr = (metrics.tw_implied_volatility_index_rank or 0) * 100
264267
trade_price = pos.average_open_price
265268
pnl = (mark_price - trade_price) * pos.quantity * m * f.notional_multiplier
266-
day_change = mark_price - summary_dict[f.streamer_symbol]
269+
day_change = mark_price - prev_close(f.symbol)
267270
pnl_day = day_change * pos.quantity * f.notional_multiplier
268271
net_liq = pnl_day
269272
elif pos.instrument_type == InstrumentType.CRYPTOCURRENCY:
@@ -279,7 +282,7 @@ async def positions(
279282
c = crypto_dict[pos.symbol]
280283
ticks = [TickSize(value=c.tick_size)]
281284
closing[i + 1] = c
282-
day_change = mark_price - summary_dict[c.streamer_symbol] # type: ignore
285+
day_change = mark_price - prev_close(c.symbol)
283286
pnl_day = day_change * pos.quantity * pos.multiplier
284287
else:
285288
print_warning(
@@ -342,7 +345,7 @@ async def positions(
342345
delta_diff = delta_target - sums["bwd"]
343346
if abs(delta_diff) > delta_variation:
344347
print_warning(
345-
f"Portfolio beta-weighting misses target of {delta_target} substantially!"
348+
f"Portfolio beta weight misses target of {delta_target} substantially!"
346349
)
347350
close = get_confirmation("Close out a position? y/N ", default=False)
348351
if not close:

ttcli/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from tastytrade.streamer import U
1919
from typer import Typer
2020

21-
from ttcli import CUSTOM_CONFIG_PATH, TOKEN_PATH, logger
21+
from ttcli import CUSTOM_CONFIG_PATH, TOKEN_PATH, VERSION, logger
2222

2323
ZERO = Decimal(0)
2424

@@ -103,7 +103,7 @@ def command(self, *args: Any, **kwargs: Any) -> Any:
103103

104104
class RenewableSession(Session):
105105
def __init__(self):
106-
token_path = os.path.join(os.path.expanduser("~"), TOKEN_PATH)
106+
token_path = os.path.join(os.path.expanduser("~"), f"{TOKEN_PATH}.v{VERSION}")
107107
logged_in = False
108108
# try to load token
109109
if os.path.exists(token_path):

ttcli/watchlist.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typer import Typer
2+
3+
from ttcli.utils import print_warning
4+
5+
6+
watchlist = Typer()
7+
8+
9+
@watchlist.command(
10+
name="wl", help="Show prices and metrics for symbols in the given watchlist."
11+
)
12+
def filter(name: str):
13+
print_warning("This functionality hasn't been implemented yet!")

0 commit comments

Comments
 (0)