tastytrade-cli 0.1__tar.gz
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.
- tastytrade_cli-0.1/LICENSE +21 -0
- tastytrade_cli-0.1/PKG-INFO +66 -0
- tastytrade_cli-0.1/README.md +52 -0
- tastytrade_cli-0.1/etc/ttcli.cfg +20 -0
- tastytrade_cli-0.1/setup.cfg +4 -0
- tastytrade_cli-0.1/setup.py +29 -0
- tastytrade_cli-0.1/tastytrade_cli.egg-info/PKG-INFO +66 -0
- tastytrade_cli-0.1/tastytrade_cli.egg-info/SOURCES.txt +15 -0
- tastytrade_cli-0.1/tastytrade_cli.egg-info/dependency_links.txt +1 -0
- tastytrade_cli-0.1/tastytrade_cli.egg-info/entry_points.txt +2 -0
- tastytrade_cli-0.1/tastytrade_cli.egg-info/requires.txt +3 -0
- tastytrade_cli-0.1/tastytrade_cli.egg-info/top_level.txt +1 -0
- tastytrade_cli-0.1/ttcli/__init__.py +0 -0
- tastytrade_cli-0.1/ttcli/app.py +25 -0
- tastytrade_cli-0.1/ttcli/option.py +706 -0
- tastytrade_cli-0.1/ttcli/portfolio.py +6 -0
- tastytrade_cli-0.1/ttcli/utils.py +152 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Graeme Holliday
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tastytrade-cli
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: An easy-to-use command line interface for Tastytrade!
|
|
5
|
+
Home-page: https://github.com/tastyware/tastytrade-cli
|
|
6
|
+
Author: Graeme Holliday
|
|
7
|
+
Author-email: graeme.holliday@pm.me
|
|
8
|
+
License: MIT
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: asyncclick>=8.1.7.2
|
|
12
|
+
Requires-Dist: rich>=13.7.1
|
|
13
|
+
Requires-Dist: tastytrade>=7.7
|
|
14
|
+
|
|
15
|
+
# tastytrade-cli
|
|
16
|
+
|
|
17
|
+
An easy-to-use command line interface for Tastytrade!
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
$ pip install tastytrade-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> [!WARNING]
|
|
28
|
+
> The CLI is still under active development. Please report any bugs, and contributions are always welcome!
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Available commands:
|
|
33
|
+
```
|
|
34
|
+
tt option buy, sell, and analyze options
|
|
35
|
+
```
|
|
36
|
+
Unavailable commands pending development:
|
|
37
|
+
```
|
|
38
|
+
tt crypto buy, sell, and analyze cryptocurrencies
|
|
39
|
+
tt future buy, sell, and analyze futures
|
|
40
|
+
tt stock buy, sell, and analyze stock
|
|
41
|
+
tt order view, replace, and cancel orders
|
|
42
|
+
tt pf (portfolio) view statistics and risk metrics for your portfolio
|
|
43
|
+
tt wl (watchlist) view current prices and other data for symbols in your watchlists
|
|
44
|
+
```
|
|
45
|
+
For more options, run `tt --help` or `tt <subcommand> --help`.
|
|
46
|
+
|
|
47
|
+
## Development/Contributing
|
|
48
|
+
|
|
49
|
+
This project includes a number of helpers in the `Makefile` to streamline common development tasks.
|
|
50
|
+
|
|
51
|
+
Creating a virtualenv for development:
|
|
52
|
+
```
|
|
53
|
+
$ make venv
|
|
54
|
+
$ source .venv/bin/activate
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
It's usually a good idea to make sure you're passing tests locally before submitting a PR:
|
|
58
|
+
```
|
|
59
|
+
$ make lint
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
If you have a feature suggestion, find a bug, or would like to contribute, feel free to open an issue or create a pull request.
|
|
63
|
+
|
|
64
|
+
## Disclaimer
|
|
65
|
+
|
|
66
|
+
tastyworks and tastytrade are not affiliated with the makers of this program and do not endorse this product. This program does not provide investment, tax, or legal advice. Stock trading involves risk and is not suitable for all investors. Options involve risk and are not suitable for all investors as the special risks inherent to options trading may expose investors to potentially significant losses. Futures and futures options trading is speculative and is not suitable for all investors. Cryptocurrency trading is speculative and is not suitable for all investors.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# tastytrade-cli
|
|
2
|
+
|
|
3
|
+
An easy-to-use command line interface for Tastytrade!
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
$ pip install tastytrade-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> [!WARNING]
|
|
14
|
+
> The CLI is still under active development. Please report any bugs, and contributions are always welcome!
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Available commands:
|
|
19
|
+
```
|
|
20
|
+
tt option buy, sell, and analyze options
|
|
21
|
+
```
|
|
22
|
+
Unavailable commands pending development:
|
|
23
|
+
```
|
|
24
|
+
tt crypto buy, sell, and analyze cryptocurrencies
|
|
25
|
+
tt future buy, sell, and analyze futures
|
|
26
|
+
tt stock buy, sell, and analyze stock
|
|
27
|
+
tt order view, replace, and cancel orders
|
|
28
|
+
tt pf (portfolio) view statistics and risk metrics for your portfolio
|
|
29
|
+
tt wl (watchlist) view current prices and other data for symbols in your watchlists
|
|
30
|
+
```
|
|
31
|
+
For more options, run `tt --help` or `tt <subcommand> --help`.
|
|
32
|
+
|
|
33
|
+
## Development/Contributing
|
|
34
|
+
|
|
35
|
+
This project includes a number of helpers in the `Makefile` to streamline common development tasks.
|
|
36
|
+
|
|
37
|
+
Creating a virtualenv for development:
|
|
38
|
+
```
|
|
39
|
+
$ make venv
|
|
40
|
+
$ source .venv/bin/activate
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
It's usually a good idea to make sure you're passing tests locally before submitting a PR:
|
|
44
|
+
```
|
|
45
|
+
$ make lint
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If you have a feature suggestion, find a bug, or would like to contribute, feel free to open an issue or create a pull request.
|
|
49
|
+
|
|
50
|
+
## Disclaimer
|
|
51
|
+
|
|
52
|
+
tastyworks and tastytrade are not affiliated with the makers of this program and do not endorse this product. This program does not provide investment, tax, or legal advice. Stock trading involves risk and is not suitable for all investors. Options involve risk and are not suitable for all investors as the special risks inherent to options trading may expose investors to potentially significant losses. Futures and futures options trading is speculative and is not suitable for all investors. Cryptocurrency trading is speculative and is not suitable for all investors.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[general]
|
|
2
|
+
# username = foo
|
|
3
|
+
# password = bar
|
|
4
|
+
# default-account = example
|
|
5
|
+
|
|
6
|
+
[portfolio]
|
|
7
|
+
bp-target-percent-vix-low = 15
|
|
8
|
+
bp-target-percent-vix-high = 55
|
|
9
|
+
bp-target-percent-variation = 5
|
|
10
|
+
portfolio-delta-target = 0
|
|
11
|
+
portfolio-delta-variation = 5
|
|
12
|
+
|
|
13
|
+
[order]
|
|
14
|
+
bp-warn-above-percent = 5
|
|
15
|
+
|
|
16
|
+
[option]
|
|
17
|
+
chain-show-delta = true
|
|
18
|
+
chain-show-volume = false
|
|
19
|
+
chain-show-open-interest = false
|
|
20
|
+
chain-show-theta = false
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
f = open('README.md', 'r')
|
|
4
|
+
LONG_DESCRIPTION = f.read()
|
|
5
|
+
f.close()
|
|
6
|
+
|
|
7
|
+
setup(
|
|
8
|
+
name='tastytrade-cli',
|
|
9
|
+
version='0.1',
|
|
10
|
+
description='An easy-to-use command line interface for Tastytrade!',
|
|
11
|
+
long_description=LONG_DESCRIPTION,
|
|
12
|
+
long_description_content_type='text/markdown',
|
|
13
|
+
author='Graeme Holliday',
|
|
14
|
+
author_email='graeme.holliday@pm.me',
|
|
15
|
+
url='https://github.com/tastyware/tastytrade-cli',
|
|
16
|
+
license='MIT',
|
|
17
|
+
install_requires=[
|
|
18
|
+
'asyncclick>=8.1.7.2',
|
|
19
|
+
'rich>=13.7.1',
|
|
20
|
+
'tastytrade>=7.7',
|
|
21
|
+
],
|
|
22
|
+
data_files = [('etc', ['etc/ttcli.cfg'])],
|
|
23
|
+
packages=find_packages(exclude=['ez_setup']),
|
|
24
|
+
include_package_data=True,
|
|
25
|
+
entry_points="""
|
|
26
|
+
[console_scripts]
|
|
27
|
+
tt = ttcli.app:main
|
|
28
|
+
"""
|
|
29
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tastytrade-cli
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: An easy-to-use command line interface for Tastytrade!
|
|
5
|
+
Home-page: https://github.com/tastyware/tastytrade-cli
|
|
6
|
+
Author: Graeme Holliday
|
|
7
|
+
Author-email: graeme.holliday@pm.me
|
|
8
|
+
License: MIT
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: asyncclick>=8.1.7.2
|
|
12
|
+
Requires-Dist: rich>=13.7.1
|
|
13
|
+
Requires-Dist: tastytrade>=7.7
|
|
14
|
+
|
|
15
|
+
# tastytrade-cli
|
|
16
|
+
|
|
17
|
+
An easy-to-use command line interface for Tastytrade!
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
$ pip install tastytrade-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> [!WARNING]
|
|
28
|
+
> The CLI is still under active development. Please report any bugs, and contributions are always welcome!
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Available commands:
|
|
33
|
+
```
|
|
34
|
+
tt option buy, sell, and analyze options
|
|
35
|
+
```
|
|
36
|
+
Unavailable commands pending development:
|
|
37
|
+
```
|
|
38
|
+
tt crypto buy, sell, and analyze cryptocurrencies
|
|
39
|
+
tt future buy, sell, and analyze futures
|
|
40
|
+
tt stock buy, sell, and analyze stock
|
|
41
|
+
tt order view, replace, and cancel orders
|
|
42
|
+
tt pf (portfolio) view statistics and risk metrics for your portfolio
|
|
43
|
+
tt wl (watchlist) view current prices and other data for symbols in your watchlists
|
|
44
|
+
```
|
|
45
|
+
For more options, run `tt --help` or `tt <subcommand> --help`.
|
|
46
|
+
|
|
47
|
+
## Development/Contributing
|
|
48
|
+
|
|
49
|
+
This project includes a number of helpers in the `Makefile` to streamline common development tasks.
|
|
50
|
+
|
|
51
|
+
Creating a virtualenv for development:
|
|
52
|
+
```
|
|
53
|
+
$ make venv
|
|
54
|
+
$ source .venv/bin/activate
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
It's usually a good idea to make sure you're passing tests locally before submitting a PR:
|
|
58
|
+
```
|
|
59
|
+
$ make lint
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
If you have a feature suggestion, find a bug, or would like to contribute, feel free to open an issue or create a pull request.
|
|
63
|
+
|
|
64
|
+
## Disclaimer
|
|
65
|
+
|
|
66
|
+
tastyworks and tastytrade are not affiliated with the makers of this program and do not endorse this product. This program does not provide investment, tax, or legal advice. Stock trading involves risk and is not suitable for all investors. Options involve risk and are not suitable for all investors as the special risks inherent to options trading may expose investors to potentially significant losses. Futures and futures options trading is speculative and is not suitable for all investors. Cryptocurrency trading is speculative and is not suitable for all investors.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
etc/ttcli.cfg
|
|
5
|
+
tastytrade_cli.egg-info/PKG-INFO
|
|
6
|
+
tastytrade_cli.egg-info/SOURCES.txt
|
|
7
|
+
tastytrade_cli.egg-info/dependency_links.txt
|
|
8
|
+
tastytrade_cli.egg-info/entry_points.txt
|
|
9
|
+
tastytrade_cli.egg-info/requires.txt
|
|
10
|
+
tastytrade_cli.egg-info/top_level.txt
|
|
11
|
+
ttcli/__init__.py
|
|
12
|
+
ttcli/app.py
|
|
13
|
+
ttcli/option.py
|
|
14
|
+
ttcli/portfolio.py
|
|
15
|
+
ttcli/utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ttcli
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import asyncclick as click
|
|
5
|
+
|
|
6
|
+
from ttcli.option import option
|
|
7
|
+
from ttcli.portfolio import portfolio
|
|
8
|
+
from ttcli.utils import CONTEXT_SETTINGS, VERSION, logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
12
|
+
@click.version_option(VERSION)
|
|
13
|
+
async def app():
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
|
|
19
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
20
|
+
logger.debug('Using Windows-specific event loop policy')
|
|
21
|
+
|
|
22
|
+
app.add_command(option)
|
|
23
|
+
app.add_command(portfolio, name='pf')
|
|
24
|
+
|
|
25
|
+
app(_anyio_backend='asyncio')
|
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import asyncclick as click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from tastytrade import DXLinkStreamer
|
|
9
|
+
from tastytrade.dxfeed import EventType, Greeks, Quote
|
|
10
|
+
from tastytrade.instruments import Future, NestedFutureOptionChain, NestedFutureOptionChainExpiration, NestedOptionChain, NestedOptionChainExpiration, Option
|
|
11
|
+
from tastytrade.order import (NewOrder, OrderAction, OrderTimeInForce,
|
|
12
|
+
OrderType, PriceEffect)
|
|
13
|
+
from tastytrade.utils import get_tasty_monthly
|
|
14
|
+
|
|
15
|
+
from ttcli.utils import (ZERO, RenewableSession, get_confirmation, is_monthly,
|
|
16
|
+
print_error, print_warning, test_order_handle_errors)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def choose_expiration(
|
|
20
|
+
chain: NestedOptionChain,
|
|
21
|
+
include_weeklies: bool = False
|
|
22
|
+
) -> NestedOptionChainExpiration:
|
|
23
|
+
exps = [e for e in chain.expirations]
|
|
24
|
+
if not include_weeklies:
|
|
25
|
+
exps = [e for e in exps if is_monthly(e.expiration_date)]
|
|
26
|
+
default = get_tasty_monthly()
|
|
27
|
+
default_option = None
|
|
28
|
+
for i, exp in enumerate(exps):
|
|
29
|
+
if exp.expiration_date == default:
|
|
30
|
+
default_option = exp
|
|
31
|
+
print(f'{i + 1}) {exp.expiration_date} (default)')
|
|
32
|
+
else:
|
|
33
|
+
print(f'{i + 1}) {exp.expiration_date}')
|
|
34
|
+
choice = 0
|
|
35
|
+
while choice not in range(1, len(exps) + 1):
|
|
36
|
+
try:
|
|
37
|
+
raw = input('Please choose an expiration: ')
|
|
38
|
+
choice = int(raw)
|
|
39
|
+
except ValueError:
|
|
40
|
+
if not raw:
|
|
41
|
+
return default_option
|
|
42
|
+
|
|
43
|
+
return exps[choice - 1]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def choose_futures_expiration(
|
|
47
|
+
chain: NestedFutureOptionChain,
|
|
48
|
+
include_weeklies: bool = False
|
|
49
|
+
) -> NestedFutureOptionChainExpiration:
|
|
50
|
+
chain = chain.option_chains[0]
|
|
51
|
+
if include_weeklies:
|
|
52
|
+
exps = [e for e in chain.expirations]
|
|
53
|
+
else:
|
|
54
|
+
exps = [e for e in chain.expirations if e.expiration_type != 'Weekly']
|
|
55
|
+
for i, exp in enumerate(exps):
|
|
56
|
+
if i == 0:
|
|
57
|
+
print(f'{i + 1}) {exp.expiration_date} [{exp.underlying_symbol}] (default)')
|
|
58
|
+
else:
|
|
59
|
+
print(f'{i + 1}) {exp.expiration_date} [{exp.underlying_symbol}]')
|
|
60
|
+
choice = 0
|
|
61
|
+
while choice not in range(1, len(exps) + 1):
|
|
62
|
+
try:
|
|
63
|
+
raw = input('Please choose an expiration: ')
|
|
64
|
+
choice = int(raw)
|
|
65
|
+
except ValueError:
|
|
66
|
+
if not raw:
|
|
67
|
+
return exps[0]
|
|
68
|
+
|
|
69
|
+
return exps[choice - 1]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def listen_quotes(
|
|
73
|
+
n_quotes: int,
|
|
74
|
+
streamer: DXLinkStreamer,
|
|
75
|
+
skip: str | None = None
|
|
76
|
+
) -> dict[str, Quote]:
|
|
77
|
+
quote_dict = {}
|
|
78
|
+
async for quote in streamer.listen(EventType.QUOTE):
|
|
79
|
+
if quote.eventSymbol != skip:
|
|
80
|
+
quote_dict[quote.eventSymbol] = quote
|
|
81
|
+
if len(quote_dict) == n_quotes:
|
|
82
|
+
return quote_dict
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def listen_greeks(
|
|
86
|
+
n_greeks: int,
|
|
87
|
+
streamer: DXLinkStreamer
|
|
88
|
+
) -> dict[str, Greeks]:
|
|
89
|
+
greeks_dict = {}
|
|
90
|
+
async for greeks in streamer.listen(EventType.GREEKS):
|
|
91
|
+
greeks_dict[greeks.eventSymbol] = greeks
|
|
92
|
+
if len(greeks_dict) == n_greeks:
|
|
93
|
+
return greeks_dict
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def listen_summaries(
|
|
97
|
+
n_summaries: int,
|
|
98
|
+
streamer: DXLinkStreamer
|
|
99
|
+
) -> dict[str, Quote]:
|
|
100
|
+
summary_dict = {}
|
|
101
|
+
async for summary in streamer.listen(EventType.SUMMARY):
|
|
102
|
+
summary_dict[summary.eventSymbol] = summary
|
|
103
|
+
if len(summary_dict) == n_summaries:
|
|
104
|
+
return summary_dict
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def listen_trades(
|
|
108
|
+
n_trades: int,
|
|
109
|
+
streamer: DXLinkStreamer
|
|
110
|
+
) -> dict[str, Quote]:
|
|
111
|
+
trade_dict = {}
|
|
112
|
+
async for trade in streamer.listen(EventType.TRADE):
|
|
113
|
+
trade_dict[trade.eventSymbol] = trade
|
|
114
|
+
if len(trade_dict) == n_trades:
|
|
115
|
+
return trade_dict
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@click.group(chain=True, help='Buy, sell, and analyze options.')
|
|
119
|
+
async def option():
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@option.command(help='Buy or sell calls with the given parameters.')
|
|
124
|
+
@click.option('-s', '--strike', type=Decimal, help='The chosen strike for the option.')
|
|
125
|
+
@click.option('-d', '--delta', type=int, help='The chosen delta for the option.')
|
|
126
|
+
@click.option('-w', '--width', type=int, help='Turns the order into a spread with the given width.')
|
|
127
|
+
@click.option('--gtc', is_flag=True, help='Place a GTC order instead of a day order.')
|
|
128
|
+
@click.option('--weeklies', is_flag=True, help='Show all expirations, not just monthlies.')
|
|
129
|
+
@click.argument('symbol', type=str)
|
|
130
|
+
@click.argument('quantity', type=int)
|
|
131
|
+
async def call(symbol: str, quantity: int, strike: Optional[Decimal] = None, width: Optional[int] = None,
|
|
132
|
+
gtc: bool = False, weeklies: bool = False, delta: Optional[int] = None):
|
|
133
|
+
if strike is not None and delta is not None:
|
|
134
|
+
print_error('Must specify either delta or strike, but not both.')
|
|
135
|
+
return
|
|
136
|
+
elif not strike and not delta:
|
|
137
|
+
print_error('Please specify either delta or strike for the option.')
|
|
138
|
+
return
|
|
139
|
+
elif delta is not None and abs(delta) > 99:
|
|
140
|
+
print_error('Delta value is too high, -99 <= delta <= 99')
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
sesh = RenewableSession()
|
|
144
|
+
chain = NestedOptionChain.get_chain(sesh, symbol)
|
|
145
|
+
subchain = choose_expiration(chain, weeklies)
|
|
146
|
+
|
|
147
|
+
async with DXLinkStreamer(sesh) as streamer:
|
|
148
|
+
if not strike:
|
|
149
|
+
dxfeeds = [s.call_streamer_symbol for s in subchain.strikes]
|
|
150
|
+
await streamer.subscribe(EventType.GREEKS, dxfeeds)
|
|
151
|
+
greeks_dict = await listen_greeks(len(dxfeeds), streamer)
|
|
152
|
+
greeks = list(greeks_dict.values())
|
|
153
|
+
|
|
154
|
+
lowest = 100
|
|
155
|
+
selected = None
|
|
156
|
+
for g in greeks:
|
|
157
|
+
diff = abs(g.delta * Decimal(100) - delta)
|
|
158
|
+
if diff < lowest:
|
|
159
|
+
selected = g
|
|
160
|
+
lowest = diff
|
|
161
|
+
# set strike with the closest delta
|
|
162
|
+
strike = next(s.strike_price for s in subchain.strikes
|
|
163
|
+
if s.call_streamer_symbol == selected.eventSymbol)
|
|
164
|
+
|
|
165
|
+
if width:
|
|
166
|
+
spread_strike = next(s for s in subchain.strikes if s.strike_price == strike + width)
|
|
167
|
+
await streamer.subscribe(EventType.QUOTE, [selected.eventSymbol, spread_strike.call_streamer_symbol])
|
|
168
|
+
quote_dict = await listen_quotes(2, streamer)
|
|
169
|
+
bid = quote_dict[selected.eventSymbol].bidPrice - quote_dict[spread_strike.call_streamer_symbol].askPrice
|
|
170
|
+
ask = quote_dict[selected.eventSymbol].askPrice - quote_dict[spread_strike.call_streamer_symbol].bidPrice
|
|
171
|
+
mid = (bid + ask) / Decimal(2)
|
|
172
|
+
else:
|
|
173
|
+
await streamer.subscribe(EventType.QUOTE, [selected.eventSymbol])
|
|
174
|
+
quote = await streamer.get_event(EventType.QUOTE)
|
|
175
|
+
bid = quote.bidPrice
|
|
176
|
+
ask = quote.askPrice
|
|
177
|
+
mid = (bid + ask) / Decimal(2)
|
|
178
|
+
|
|
179
|
+
console = Console()
|
|
180
|
+
if width:
|
|
181
|
+
table = Table(show_header=True, header_style='bold', title_style='bold',
|
|
182
|
+
title=f'Quote for {symbol} call spread {subchain.expiration_date}')
|
|
183
|
+
else:
|
|
184
|
+
table = Table(show_header=True, header_style='bold', title_style='bold',
|
|
185
|
+
title=f'Quote for {symbol} {strike}C {subchain.expiration_date}')
|
|
186
|
+
table.add_column('Bid', style='green', width=8, justify='center')
|
|
187
|
+
table.add_column('Mid', width=8, justify='center')
|
|
188
|
+
table.add_column('Ask', style='red', width=8, justify='center')
|
|
189
|
+
table.add_row(f'{bid:.2f}', f'{mid:.2f}', f'{ask:.2f}')
|
|
190
|
+
console.print(table)
|
|
191
|
+
|
|
192
|
+
price = input('Please enter a limit price per quantity (default mid): ')
|
|
193
|
+
if not price:
|
|
194
|
+
price = round(mid, 2)
|
|
195
|
+
price = Decimal(price)
|
|
196
|
+
|
|
197
|
+
short_symbol = next(s.call for s in subchain.strikes if s.strike_price == strike)
|
|
198
|
+
if width:
|
|
199
|
+
res = Option.get_options(sesh, [short_symbol, spread_strike.call])
|
|
200
|
+
res.sort(key=lambda x: x.strike_price)
|
|
201
|
+
legs = [
|
|
202
|
+
res[0].build_leg(abs(quantity), OrderAction.SELL_TO_OPEN if quantity < 0 else OrderAction.BUY_TO_OPEN),
|
|
203
|
+
res[1].build_leg(abs(quantity), OrderAction.BUY_TO_OPEN if quantity < 0 else OrderAction.SELL_TO_OPEN)
|
|
204
|
+
]
|
|
205
|
+
else:
|
|
206
|
+
call = Option.get_option(sesh, short_symbol)
|
|
207
|
+
legs = [call.build_leg(abs(quantity), OrderAction.SELL_TO_OPEN if quantity < 0 else OrderAction.BUY_TO_OPEN)]
|
|
208
|
+
order = NewOrder(
|
|
209
|
+
time_in_force=OrderTimeInForce.GTC if gtc else OrderTimeInForce.DAY,
|
|
210
|
+
order_type=OrderType.LIMIT,
|
|
211
|
+
legs=legs,
|
|
212
|
+
price=price,
|
|
213
|
+
price_effect=PriceEffect.CREDIT if quantity < 0 else PriceEffect.DEBIT
|
|
214
|
+
)
|
|
215
|
+
acc = sesh.get_account()
|
|
216
|
+
|
|
217
|
+
data = test_order_handle_errors(acc, sesh, order)
|
|
218
|
+
if data is None:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
nl = acc.get_balances(sesh).net_liquidating_value
|
|
222
|
+
bp = data.buying_power_effect.change_in_buying_power
|
|
223
|
+
percent = bp / nl * Decimal(100)
|
|
224
|
+
fees = data.fee_calculation.total_fees
|
|
225
|
+
|
|
226
|
+
table = Table(show_header=True, header_style='bold', title_style='bold', title='Order Review')
|
|
227
|
+
table.add_column('Quantity', width=8, justify='center')
|
|
228
|
+
table.add_column('Symbol', width=8, justify='center')
|
|
229
|
+
table.add_column('Strike', width=8, justify='center')
|
|
230
|
+
table.add_column('Type', width=8, justify='center')
|
|
231
|
+
table.add_column('Expiration', width=10, justify='center')
|
|
232
|
+
table.add_column('Price', width=8, justify='center')
|
|
233
|
+
table.add_column('BP', width=8, justify='center')
|
|
234
|
+
table.add_column('BP %', width=8, justify='center')
|
|
235
|
+
table.add_column('Fees', width=8, justify='center')
|
|
236
|
+
table.add_row(f'{quantity:+}', symbol, f'${strike:.2f}', 'CALL', f'{subchain.expiration_date}', f'${price:.2f}',
|
|
237
|
+
f'${bp:.2f}', f'{percent:.2f}%', f'${fees:.2f}')
|
|
238
|
+
if width:
|
|
239
|
+
table.add_row(f'{-quantity:+}', symbol, f'${spread_strike.strike_price:.2f}',
|
|
240
|
+
'CALL', f'{subchain.expiration_date}', '-', '-', '-', '-')
|
|
241
|
+
console.print(table)
|
|
242
|
+
|
|
243
|
+
if data.warnings:
|
|
244
|
+
for warning in data.warnings:
|
|
245
|
+
print_warning(warning.message)
|
|
246
|
+
warn_percent = sesh.config.getint('order', 'bp-warn-above-percent', fallback=None)
|
|
247
|
+
if warn_percent and percent > warn_percent:
|
|
248
|
+
print_warning(f'Buying power usage is above target of {warn_percent}%!')
|
|
249
|
+
if get_confirmation('Send order? Y/n '):
|
|
250
|
+
acc.place_order(sesh, order, dry_run=False)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@option.command(help='Buy or sell puts with the given parameters.')
|
|
254
|
+
@click.option('-s', '--strike', type=Decimal, help='The chosen strike for the option.')
|
|
255
|
+
@click.option('-d', '--delta', type=int, help='The chosen delta for the option.')
|
|
256
|
+
@click.option('-w', '--width', type=int, help='Turns the order into a spread with the given width.')
|
|
257
|
+
@click.option('--gtc', is_flag=True, help='Place a GTC order instead of a day order.')
|
|
258
|
+
@click.option('--weeklies', is_flag=True, help='Show all expirations, not just monthlies.')
|
|
259
|
+
@click.argument('symbol', type=str)
|
|
260
|
+
@click.argument('quantity', type=int)
|
|
261
|
+
async def put(symbol: str, quantity: int, strike: Optional[int] = None, width: Optional[int] = None,
|
|
262
|
+
gtc: bool = False, weeklies: bool = False, delta: Optional[int] = None):
|
|
263
|
+
if strike is not None and delta is not None:
|
|
264
|
+
print_error('Must specify either delta or strike, but not both.')
|
|
265
|
+
return
|
|
266
|
+
elif not strike and not delta:
|
|
267
|
+
print_error('Please specify either delta or strike for the option.')
|
|
268
|
+
return
|
|
269
|
+
elif delta is not None and abs(delta) > 99:
|
|
270
|
+
print_error('Delta value is too high, -99 <= delta <= 99')
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
sesh = RenewableSession()
|
|
274
|
+
chain = NestedOptionChain.get_chain(sesh, symbol)
|
|
275
|
+
subchain = choose_expiration(chain, weeklies)
|
|
276
|
+
|
|
277
|
+
async with DXLinkStreamer(sesh) as streamer:
|
|
278
|
+
if not strike:
|
|
279
|
+
dxfeeds = [s.put_streamer_symbol for s in subchain.strikes]
|
|
280
|
+
await streamer.subscribe(EventType.GREEKS, dxfeeds)
|
|
281
|
+
greeks_dict = await listen_greeks(len(dxfeeds), streamer)
|
|
282
|
+
greeks = list(greeks_dict.values())
|
|
283
|
+
|
|
284
|
+
lowest = 100
|
|
285
|
+
selected = None
|
|
286
|
+
for g in greeks:
|
|
287
|
+
diff = abs(g.delta * Decimal(100) + delta)
|
|
288
|
+
if diff < lowest:
|
|
289
|
+
selected = g
|
|
290
|
+
lowest = diff
|
|
291
|
+
# set strike with the closest delta
|
|
292
|
+
strike = next(s.strike_price for s in subchain.strikes
|
|
293
|
+
if s.put_streamer_symbol == selected.eventSymbol)
|
|
294
|
+
|
|
295
|
+
if width:
|
|
296
|
+
spread_strike = next(s for s in subchain.strikes if s.strike_price == strike - width)
|
|
297
|
+
await streamer.subscribe(EventType.QUOTE, [selected.eventSymbol, spread_strike.put_streamer_symbol])
|
|
298
|
+
quote_dict = await listen_quotes(2, streamer)
|
|
299
|
+
bid = quote_dict[selected.eventSymbol].bidPrice - quote_dict[spread_strike.put_streamer_symbol].askPrice
|
|
300
|
+
ask = quote_dict[selected.eventSymbol].askPrice - quote_dict[spread_strike.put_streamer_symbol].bidPrice
|
|
301
|
+
mid = (bid + ask) / Decimal(2)
|
|
302
|
+
else:
|
|
303
|
+
await streamer.subscribe(EventType.QUOTE, [selected.eventSymbol])
|
|
304
|
+
quote = await streamer.get_event(EventType.QUOTE)
|
|
305
|
+
bid = quote.bidPrice
|
|
306
|
+
ask = quote.askPrice
|
|
307
|
+
mid = (bid + ask) / Decimal(2)
|
|
308
|
+
|
|
309
|
+
console = Console()
|
|
310
|
+
if width:
|
|
311
|
+
table = Table(show_header=True, header_style='bold', title_style='bold',
|
|
312
|
+
title=f'Quote for {symbol} put spread {subchain.expiration_date}')
|
|
313
|
+
else:
|
|
314
|
+
table = Table(show_header=True, header_style='bold', title_style='bold',
|
|
315
|
+
title=f'Quote for {symbol} {strike}P {subchain.expiration_date}')
|
|
316
|
+
table.add_column('Bid', style='green', width=8, justify='center')
|
|
317
|
+
table.add_column('Mid', width=8, justify='center')
|
|
318
|
+
table.add_column('Ask', style='red', width=8, justify='center')
|
|
319
|
+
table.add_row(f'{bid:.2f}', f'{mid:.2f}', f'{ask:.2f}')
|
|
320
|
+
console.print(table)
|
|
321
|
+
|
|
322
|
+
price = input('Please enter a limit price per quantity (default mid): ')
|
|
323
|
+
if not price:
|
|
324
|
+
price = round(mid, 2)
|
|
325
|
+
price = Decimal(price)
|
|
326
|
+
|
|
327
|
+
short_symbol = next(s.put for s in subchain.strikes if s.strike_price == strike)
|
|
328
|
+
if width:
|
|
329
|
+
res = Option.get_options(sesh, [short_symbol, spread_strike.put])
|
|
330
|
+
res.sort(key=lambda x: x.strike_price, reverse=True)
|
|
331
|
+
legs = [
|
|
332
|
+
res[0].build_leg(abs(quantity), OrderAction.SELL_TO_OPEN if quantity < 0 else OrderAction.BUY_TO_OPEN),
|
|
333
|
+
res[1].build_leg(abs(quantity), OrderAction.BUY_TO_OPEN if quantity < 0 else OrderAction.SELL_TO_OPEN)
|
|
334
|
+
]
|
|
335
|
+
else:
|
|
336
|
+
put = Option.get_option(sesh, short_symbol)
|
|
337
|
+
legs = [put.build_leg(abs(quantity), OrderAction.SELL_TO_OPEN if quantity < 0 else OrderAction.BUY_TO_OPEN)]
|
|
338
|
+
order = NewOrder(
|
|
339
|
+
time_in_force=OrderTimeInForce.GTC if gtc else OrderTimeInForce.DAY,
|
|
340
|
+
order_type=OrderType.LIMIT,
|
|
341
|
+
legs=legs,
|
|
342
|
+
price=price,
|
|
343
|
+
price_effect=PriceEffect.CREDIT if quantity < 0 else PriceEffect.DEBIT
|
|
344
|
+
)
|
|
345
|
+
acc = sesh.get_account()
|
|
346
|
+
|
|
347
|
+
data = test_order_handle_errors(acc, sesh, order)
|
|
348
|
+
if data is None:
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
nl = acc.get_balances(sesh).net_liquidating_value
|
|
352
|
+
bp = data.buying_power_effect.change_in_buying_power
|
|
353
|
+
percent = bp / nl * Decimal(100)
|
|
354
|
+
fees = data.fee_calculation.total_fees
|
|
355
|
+
|
|
356
|
+
table = Table(show_header=True, header_style='bold', title_style='bold', title='Order Review')
|
|
357
|
+
table.add_column('Quantity', width=8, justify='center')
|
|
358
|
+
table.add_column('Symbol', width=8, justify='center')
|
|
359
|
+
table.add_column('Strike', width=8, justify='center')
|
|
360
|
+
table.add_column('Type', width=8, justify='center')
|
|
361
|
+
table.add_column('Expiration', width=10, justify='center')
|
|
362
|
+
table.add_column('Price', width=8, justify='center')
|
|
363
|
+
table.add_column('BP', width=8, justify='center')
|
|
364
|
+
table.add_column('BP %', width=8, justify='center')
|
|
365
|
+
table.add_column('Fees', width=8, justify='center')
|
|
366
|
+
table.add_row(f'{quantity:+}', symbol, f'${strike:.2f}', 'PUT', f'{subchain.expiration_date}', f'${price:.2f}',
|
|
367
|
+
f'${bp:.2f}', f'{percent:.2f}%', f'${fees:.2f}')
|
|
368
|
+
if width:
|
|
369
|
+
table.add_row(f'{-quantity:+}', symbol, f'${spread_strike.strike_price:.2f}',
|
|
370
|
+
'PUT', f'{subchain.expiration_date}', '-', '-', '-', '-')
|
|
371
|
+
console.print(table)
|
|
372
|
+
|
|
373
|
+
if data.warnings:
|
|
374
|
+
for warning in data.warnings:
|
|
375
|
+
print_warning(warning.message)
|
|
376
|
+
warn_percent = sesh.config.getint('order', 'bp-warn-above-percent', fallback=None)
|
|
377
|
+
if warn_percent and percent > warn_percent:
|
|
378
|
+
print_warning(f'Buying power usage is above target of {warn_percent}%!')
|
|
379
|
+
if get_confirmation('Send order? Y/n '):
|
|
380
|
+
acc.place_order(sesh, order, dry_run=False)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@option.command(help='Buy or sell strangles with the given parameters.')
|
|
384
|
+
@click.option('-c', '--call', type=Decimal, help='The chosen strike for the call option.')
|
|
385
|
+
@click.option('-p', '--put', type=Decimal, help='The chosen strike for the put option.')
|
|
386
|
+
@click.option('-d', '--delta', type=int, help='The chosen delta for both options.')
|
|
387
|
+
@click.option('-w', '--width', type=int, help='Turns the order into an iron condor with the given width.')
|
|
388
|
+
@click.option('--gtc', is_flag=True, help='Place a GTC order instead of a day order.')
|
|
389
|
+
@click.option('--weeklies', is_flag=True, help='Show all expirations, not just monthlies.')
|
|
390
|
+
@click.argument('symbol', type=str)
|
|
391
|
+
@click.argument('quantity', type=int)
|
|
392
|
+
async def strangle(symbol: str, quantity: int, call: Optional[Decimal] = None, width: Optional[int] = None,
|
|
393
|
+
gtc: bool = False, weeklies: bool = False, delta: Optional[int] = None, put: Optional[Decimal] = None):
|
|
394
|
+
if (call is not None or put is not None) and delta is not None:
|
|
395
|
+
print_error('Must specify either delta or strike, but not both.')
|
|
396
|
+
return
|
|
397
|
+
elif delta is not None and (call is not None or put is not None):
|
|
398
|
+
print_error('Please specify either delta, or strikes for both options.')
|
|
399
|
+
return
|
|
400
|
+
elif delta is not None and abs(delta) > 99:
|
|
401
|
+
print_error('Delta value is too high, -99 <= delta <= 99')
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
sesh = RenewableSession()
|
|
405
|
+
chain = NestedOptionChain.get_chain(sesh, symbol)
|
|
406
|
+
subchain = choose_expiration(chain, weeklies)
|
|
407
|
+
|
|
408
|
+
async with DXLinkStreamer(sesh) as streamer:
|
|
409
|
+
if delta is not None:
|
|
410
|
+
put_dxf = [s.put_streamer_symbol for s in subchain.strikes]
|
|
411
|
+
call_dxf = [s.call_streamer_symbol for s in subchain.strikes]
|
|
412
|
+
dxfeeds = put_dxf + call_dxf
|
|
413
|
+
await streamer.subscribe(EventType.GREEKS, dxfeeds)
|
|
414
|
+
greeks_dict = await listen_greeks(len(dxfeeds), streamer)
|
|
415
|
+
put_greeks = [v for v in greeks_dict.values() if v.eventSymbol in put_dxf]
|
|
416
|
+
call_greeks = [v for v in greeks_dict.values() if v.eventSymbol in call_dxf]
|
|
417
|
+
|
|
418
|
+
lowest = 100
|
|
419
|
+
selected_put = None
|
|
420
|
+
for g in put_greeks:
|
|
421
|
+
diff = abs(g.delta * Decimal(100) + delta)
|
|
422
|
+
if diff < lowest:
|
|
423
|
+
selected_put = g.eventSymbol
|
|
424
|
+
lowest = diff
|
|
425
|
+
lowest = 100
|
|
426
|
+
selected_call = None
|
|
427
|
+
for g in call_greeks:
|
|
428
|
+
diff = abs(g.delta * Decimal(100) - delta)
|
|
429
|
+
if diff < lowest:
|
|
430
|
+
selected_call = g.eventSymbol
|
|
431
|
+
lowest = diff
|
|
432
|
+
# set strike with the closest delta
|
|
433
|
+
put_strike = next(s for s in subchain.strikes
|
|
434
|
+
if s.put_streamer_symbol == selected_put)
|
|
435
|
+
call_strike = next(s for s in subchain.strikes
|
|
436
|
+
if s.call_streamer_symbol == selected_call)
|
|
437
|
+
else:
|
|
438
|
+
put_strike = next(s for s in subchain.strikes if s.strike_price == put)
|
|
439
|
+
call_strike = next(s for s in subchain.strikes if s.strike_price == call)
|
|
440
|
+
|
|
441
|
+
if width:
|
|
442
|
+
put_spread_strike = next(s for s in subchain.strikes if s.strike_price == put_strike.strike_price - width)
|
|
443
|
+
call_spread_strike = next(s for s in subchain.strikes if s.strike_price == call_strike.strike_price + width)
|
|
444
|
+
await streamer.subscribe(
|
|
445
|
+
EventType.QUOTE,
|
|
446
|
+
[
|
|
447
|
+
call_strike.call_streamer_symbol,
|
|
448
|
+
put_strike.put_streamer_symbol,
|
|
449
|
+
put_spread_strike.put_streamer_symbol,
|
|
450
|
+
call_spread_strike.call_streamer_symbol
|
|
451
|
+
]
|
|
452
|
+
)
|
|
453
|
+
quote_dict = await listen_quotes(4, streamer)
|
|
454
|
+
bid = (quote_dict[call_strike.call_streamer_symbol].bidPrice +
|
|
455
|
+
quote_dict[put_strike.put_streamer_symbol].bidPrice -
|
|
456
|
+
quote_dict[put_spread_strike.put_streamer_symbol].askPrice -
|
|
457
|
+
quote_dict[call_spread_strike.call_streamer_symbol].askPrice)
|
|
458
|
+
ask = (quote_dict[call_strike.call_streamer_symbol].askPrice +
|
|
459
|
+
quote_dict[put_strike.put_streamer_symbol].askPrice -
|
|
460
|
+
quote_dict[put_spread_strike.put_streamer_symbol].bidPrice -
|
|
461
|
+
quote_dict[call_spread_strike.call_streamer_symbol].bidPrice)
|
|
462
|
+
mid = (bid + ask) / Decimal(2)
|
|
463
|
+
else:
|
|
464
|
+
await streamer.subscribe(EventType.QUOTE, [put_strike.put_streamer_symbol, call_strike.call_streamer_symbol])
|
|
465
|
+
quote_dict = await listen_quotes(2, streamer)
|
|
466
|
+
bid = sum([q.bidPrice for q in quote_dict.values()])
|
|
467
|
+
ask = sum([q.askPrice for q in quote_dict.values()])
|
|
468
|
+
mid = (bid + ask) / Decimal(2)
|
|
469
|
+
|
|
470
|
+
console = Console()
|
|
471
|
+
if width:
|
|
472
|
+
table = Table(show_header=True, header_style='bold', title_style='bold',
|
|
473
|
+
title=f'Quote for {symbol} iron condor {subchain.expiration_date}')
|
|
474
|
+
else:
|
|
475
|
+
table = Table(show_header=True, header_style='bold', title_style='bold',
|
|
476
|
+
title=f'Quote for {symbol} {put_strike.strike_price}/{call_strike.strike_price} strangle {subchain.expiration_date}')
|
|
477
|
+
table.add_column('Bid', style='green', width=8, justify='center')
|
|
478
|
+
table.add_column('Mid', width=8, justify='center')
|
|
479
|
+
table.add_column('Ask', style='red', width=8, justify='center')
|
|
480
|
+
table.add_row(f'{bid:.2f}', f'{mid:.2f}', f'{ask:.2f}')
|
|
481
|
+
console.print(table)
|
|
482
|
+
|
|
483
|
+
price = input('Please enter a limit price per quantity (default mid): ')
|
|
484
|
+
if not price:
|
|
485
|
+
price = round(mid, 2)
|
|
486
|
+
price = Decimal(price)
|
|
487
|
+
|
|
488
|
+
tt_symbols = [put_strike.put, call_strike.call]
|
|
489
|
+
if width:
|
|
490
|
+
tt_symbols += [put_spread_strike.put, call_spread_strike.call]
|
|
491
|
+
options = Option.get_options(sesh, tt_symbols)
|
|
492
|
+
options.sort(key=lambda o: o.strike_price)
|
|
493
|
+
if width:
|
|
494
|
+
legs = [
|
|
495
|
+
options[0].build_leg(abs(quantity), OrderAction.BUY_TO_OPEN if quantity < 0 else OrderAction.SELL_TO_OPEN),
|
|
496
|
+
options[1].build_leg(abs(quantity), OrderAction.SELL_TO_OPEN if quantity < 0 else OrderAction.BUY_TO_OPEN),
|
|
497
|
+
options[2].build_leg(abs(quantity), OrderAction.SELL_TO_OPEN if quantity < 0 else OrderAction.BUY_TO_OPEN),
|
|
498
|
+
options[3].build_leg(abs(quantity), OrderAction.BUY_TO_OPEN if quantity < 0 else OrderAction.SELL_TO_OPEN)
|
|
499
|
+
]
|
|
500
|
+
else:
|
|
501
|
+
legs = [
|
|
502
|
+
options[0].build_leg(abs(quantity), OrderAction.SELL_TO_OPEN if quantity < 0 else OrderAction.BUY_TO_OPEN),
|
|
503
|
+
options[1].build_leg(abs(quantity), OrderAction.SELL_TO_OPEN if quantity < 0 else OrderAction.BUY_TO_OPEN)
|
|
504
|
+
]
|
|
505
|
+
order = NewOrder(
|
|
506
|
+
time_in_force=OrderTimeInForce.GTC if gtc else OrderTimeInForce.DAY,
|
|
507
|
+
order_type=OrderType.LIMIT,
|
|
508
|
+
legs=legs,
|
|
509
|
+
price=price,
|
|
510
|
+
price_effect=PriceEffect.CREDIT if quantity < 0 else PriceEffect.DEBIT
|
|
511
|
+
)
|
|
512
|
+
acc = sesh.get_account()
|
|
513
|
+
|
|
514
|
+
data = test_order_handle_errors(acc, sesh, order)
|
|
515
|
+
if data is None:
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
nl = acc.get_balances(sesh).net_liquidating_value
|
|
519
|
+
bp = data.buying_power_effect.change_in_buying_power
|
|
520
|
+
percent = bp / nl * Decimal(100)
|
|
521
|
+
fees = data.fee_calculation.total_fees
|
|
522
|
+
|
|
523
|
+
table = Table(header_style='bold', title_style='bold', title='Order Review')
|
|
524
|
+
table.add_column('Quantity', width=8, justify='center')
|
|
525
|
+
table.add_column('Symbol', width=8, justify='center')
|
|
526
|
+
table.add_column('Strike', width=8, justify='center')
|
|
527
|
+
table.add_column('Type', width=8, justify='center')
|
|
528
|
+
table.add_column('Expiration', width=10, justify='center')
|
|
529
|
+
table.add_column('Price', width=8, justify='center')
|
|
530
|
+
table.add_column('BP', width=8, justify='center')
|
|
531
|
+
table.add_column('BP %', width=8, justify='center')
|
|
532
|
+
table.add_column('Fees', width=8, justify='center')
|
|
533
|
+
table.add_row(
|
|
534
|
+
f'{quantity:+}',
|
|
535
|
+
symbol,
|
|
536
|
+
f'${put_strike.strike_price:.2f}',
|
|
537
|
+
'PUT',
|
|
538
|
+
f'{subchain.expiration_date}',
|
|
539
|
+
f'${price:.2f}',
|
|
540
|
+
f'${bp:.2f}',
|
|
541
|
+
f'{percent:.2f}%',
|
|
542
|
+
f'${fees:.2f}'
|
|
543
|
+
)
|
|
544
|
+
table.add_row(
|
|
545
|
+
f'{quantity:+}',
|
|
546
|
+
symbol,
|
|
547
|
+
f'${call_strike.strike_price:.2f}',
|
|
548
|
+
'CALL',
|
|
549
|
+
f'{subchain.expiration_date}',
|
|
550
|
+
'-',
|
|
551
|
+
'-',
|
|
552
|
+
'-',
|
|
553
|
+
'-'
|
|
554
|
+
)
|
|
555
|
+
if width:
|
|
556
|
+
table.add_row(
|
|
557
|
+
f'{-quantity:+}',
|
|
558
|
+
symbol,
|
|
559
|
+
f'${put_spread_strike.strike_price:.2f}',
|
|
560
|
+
'PUT',
|
|
561
|
+
f'{subchain.expiration_date}',
|
|
562
|
+
'-',
|
|
563
|
+
'-',
|
|
564
|
+
'-',
|
|
565
|
+
'-'
|
|
566
|
+
)
|
|
567
|
+
table.add_row(
|
|
568
|
+
f'{-quantity:+}',
|
|
569
|
+
symbol,
|
|
570
|
+
f'${call_spread_strike.strike_price:.2f}',
|
|
571
|
+
'CALL',
|
|
572
|
+
f'{subchain.expiration_date}',
|
|
573
|
+
'-',
|
|
574
|
+
'-',
|
|
575
|
+
'-',
|
|
576
|
+
'-'
|
|
577
|
+
)
|
|
578
|
+
console.print(table)
|
|
579
|
+
|
|
580
|
+
if data.warnings:
|
|
581
|
+
for warning in data.warnings:
|
|
582
|
+
print_warning(warning.message)
|
|
583
|
+
warn_percent = sesh.config.getint('order', 'bp-warn-above-percent', fallback=None)
|
|
584
|
+
if warn_percent and percent > warn_percent:
|
|
585
|
+
print_warning(f'Buying power usage is above target of {warn_percent}%!')
|
|
586
|
+
if get_confirmation('Send order? Y/n '):
|
|
587
|
+
acc.place_order(sesh, order, dry_run=False)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@option.command(help='Fetch and display an options chain.')
|
|
591
|
+
@click.option('-w', '--weeklies', is_flag=True,
|
|
592
|
+
help='Show all expirations, not just monthlies.')
|
|
593
|
+
@click.option('-s', '--strikes', type=int, default=8,
|
|
594
|
+
help='The number of strikes to fetch above and below the spot price.')
|
|
595
|
+
@click.argument('symbol', type=str)
|
|
596
|
+
async def chain(symbol: str, strikes: int = 8, weeklies: bool = False):
|
|
597
|
+
sesh = RenewableSession()
|
|
598
|
+
async with DXLinkStreamer(sesh) as streamer:
|
|
599
|
+
if symbol[0] == '/': # futures options
|
|
600
|
+
chain = NestedFutureOptionChain.get_chain(sesh, symbol)
|
|
601
|
+
subchain = choose_futures_expiration(chain, weeklies)
|
|
602
|
+
precision = subchain.tick_sizes[0].value.as_tuple().exponent
|
|
603
|
+
else:
|
|
604
|
+
chain = NestedOptionChain.get_chain(sesh, symbol)
|
|
605
|
+
precision = chain.tick_sizes[0].value.as_tuple().exponent
|
|
606
|
+
subchain = choose_expiration(chain, weeklies)
|
|
607
|
+
precision = abs(precision) if precision < 0 else ZERO
|
|
608
|
+
precision = f'.{precision}f'
|
|
609
|
+
|
|
610
|
+
console = Console()
|
|
611
|
+
table = Table(show_header=True, header_style='bold', title_style='bold',
|
|
612
|
+
title=f'Options chain for {symbol} expiring {subchain.expiration_date}')
|
|
613
|
+
|
|
614
|
+
show_delta = sesh.config.getboolean('option', 'chain-show-delta', fallback=True)
|
|
615
|
+
show_theta = sesh.config.getboolean('option', 'chain-show-theta', fallback=False)
|
|
616
|
+
show_oi = sesh.config.getboolean('option', 'chain-show-open-interest', fallback=False)
|
|
617
|
+
show_volume = sesh.config.getboolean('option', 'chain-show-volume', fallback=False)
|
|
618
|
+
if show_volume:
|
|
619
|
+
table.add_column(u'Volume', justify='right')
|
|
620
|
+
if show_oi:
|
|
621
|
+
table.add_column(u'Open Int', justify='right')
|
|
622
|
+
if show_theta:
|
|
623
|
+
table.add_column(u'Call \u03B8', justify='center')
|
|
624
|
+
if show_delta:
|
|
625
|
+
table.add_column(u'Call \u0394', justify='center')
|
|
626
|
+
table.add_column('Bid', style='green', justify='center')
|
|
627
|
+
table.add_column('Ask', style='red', justify='center')
|
|
628
|
+
table.add_column('Strike', justify='center')
|
|
629
|
+
table.add_column('Bid', style='green', justify='center')
|
|
630
|
+
table.add_column('Ask', style='red', justify='center')
|
|
631
|
+
if show_delta:
|
|
632
|
+
table.add_column(u'Put \u0394', justify='center')
|
|
633
|
+
if show_theta:
|
|
634
|
+
table.add_column(u'Put \u03B8', justify='center')
|
|
635
|
+
if show_oi:
|
|
636
|
+
table.add_column(u'Open Int', justify='right')
|
|
637
|
+
if show_volume:
|
|
638
|
+
table.add_column(u'Volume', justify='right')
|
|
639
|
+
|
|
640
|
+
if symbol[0] == '/': # futures options
|
|
641
|
+
future = Future.get_future(sesh, subchain.underlying_symbol)
|
|
642
|
+
await streamer.subscribe(EventType.QUOTE, [future.streamer_symbol])
|
|
643
|
+
else:
|
|
644
|
+
await streamer.subscribe(EventType.QUOTE, [symbol])
|
|
645
|
+
quote = await streamer.get_event(EventType.QUOTE)
|
|
646
|
+
strike_price = (quote.bidPrice + quote.askPrice) / 2
|
|
647
|
+
|
|
648
|
+
subchain.strikes.sort(key=lambda s: s.strike_price)
|
|
649
|
+
if strikes * 2 < len(subchain.strikes):
|
|
650
|
+
mid_index = 0
|
|
651
|
+
while subchain.strikes[mid_index].strike_price < strike_price:
|
|
652
|
+
mid_index += 1
|
|
653
|
+
all_strikes = subchain.strikes[mid_index - strikes:mid_index + strikes]
|
|
654
|
+
else:
|
|
655
|
+
all_strikes = subchain.strikes
|
|
656
|
+
|
|
657
|
+
dxfeeds = ([s.call_streamer_symbol for s in all_strikes] +
|
|
658
|
+
[s.put_streamer_symbol for s in all_strikes])
|
|
659
|
+
await streamer.subscribe(EventType.QUOTE, dxfeeds)
|
|
660
|
+
await streamer.subscribe(EventType.GREEKS, dxfeeds)
|
|
661
|
+
if show_oi:
|
|
662
|
+
await streamer.subscribe(EventType.SUMMARY, dxfeeds)
|
|
663
|
+
if show_volume:
|
|
664
|
+
await streamer.subscribe(EventType.TRADE, dxfeeds)
|
|
665
|
+
|
|
666
|
+
greeks_dict = await listen_greeks(len(dxfeeds), streamer)
|
|
667
|
+
# take into account the symbol we subscribed to
|
|
668
|
+
quote_dict = await listen_quotes(len(dxfeeds), streamer, skip=symbol if symbol[0] != '/' else future.streamer_symbol)
|
|
669
|
+
if show_oi:
|
|
670
|
+
summary_dict = await listen_summaries(len(dxfeeds), streamer)
|
|
671
|
+
if show_volume:
|
|
672
|
+
trade_dict = await listen_trades(len(dxfeeds), streamer)
|
|
673
|
+
|
|
674
|
+
for i, strike in enumerate(all_strikes):
|
|
675
|
+
put_bid = quote_dict[strike.put_streamer_symbol].bidPrice
|
|
676
|
+
put_ask = quote_dict[strike.put_streamer_symbol].askPrice
|
|
677
|
+
call_bid = quote_dict[strike.call_streamer_symbol].bidPrice
|
|
678
|
+
call_ask = quote_dict[strike.call_streamer_symbol].askPrice
|
|
679
|
+
row = [
|
|
680
|
+
f'{call_bid:{precision}}',
|
|
681
|
+
f'{call_ask:{precision}}',
|
|
682
|
+
f'{strike.strike_price:{precision}}',
|
|
683
|
+
f'{put_bid:{precision}}',
|
|
684
|
+
f'{put_ask:{precision}}'
|
|
685
|
+
]
|
|
686
|
+
prepend = []
|
|
687
|
+
if show_delta:
|
|
688
|
+
put_delta = int(greeks_dict[strike.put_streamer_symbol].delta * 100)
|
|
689
|
+
call_delta = int(greeks_dict[strike.call_streamer_symbol].delta * 100)
|
|
690
|
+
prepend.append(f'{call_delta:g}')
|
|
691
|
+
row.append(f'{put_delta:g}')
|
|
692
|
+
|
|
693
|
+
if show_theta:
|
|
694
|
+
prepend.append(f'{abs(greeks_dict[strike.put_streamer_symbol].theta):.2f}')
|
|
695
|
+
row.append(f'{abs(greeks_dict[strike.call_streamer_symbol].theta):.2f}')
|
|
696
|
+
if show_oi:
|
|
697
|
+
prepend.append(f'{summary_dict[strike.put_streamer_symbol].openInterest}')
|
|
698
|
+
row.append(f'{summary_dict[strike.call_streamer_symbol].openInterest}')
|
|
699
|
+
if show_volume:
|
|
700
|
+
prepend.append(f'{trade_dict[strike.put_streamer_symbol].dayVolume}')
|
|
701
|
+
row.append(f'{trade_dict[strike.call_streamer_symbol].dayVolume}')
|
|
702
|
+
|
|
703
|
+
prepend.reverse()
|
|
704
|
+
table.add_row(*(prepend + row), end_section=(i == strikes - 1))
|
|
705
|
+
|
|
706
|
+
console.print(table)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import pickle
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from configparser import ConfigParser
|
|
8
|
+
from datetime import date
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from rich import print as rich_print
|
|
14
|
+
from tastytrade import Account, ProductionSession
|
|
15
|
+
from tastytrade.order import NewOrder, PlacedOrderResponse
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
VERSION = '0.1'
|
|
19
|
+
ZERO = Decimal(0)
|
|
20
|
+
|
|
21
|
+
CONTEXT_SETTINGS = {'help_option_names': ['-h', '--help']}
|
|
22
|
+
|
|
23
|
+
CUSTOM_CONFIG_PATH = '.config/ttcli/ttcli.cfg'
|
|
24
|
+
DEFAULT_CONFIG_PATH = 'etc/ttcli.cfg'
|
|
25
|
+
TOKEN_PATH = '.config/ttcli/.session'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_error(msg: str):
|
|
29
|
+
rich_print(f'[bold red]Error: {msg}[/bold red]')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def print_warning(msg: str):
|
|
33
|
+
rich_print(f'[light_coral]Warning: {msg}[/light_coral]')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_order_handle_errors(
|
|
37
|
+
account: Account,
|
|
38
|
+
session: 'RenewableSession',
|
|
39
|
+
order: NewOrder
|
|
40
|
+
) -> Optional[PlacedOrderResponse]:
|
|
41
|
+
url = f'{session.base_url}/accounts/{account.account_number}/orders/dry-run'
|
|
42
|
+
json = order.model_dump_json(exclude_none=True, by_alias=True)
|
|
43
|
+
|
|
44
|
+
response = requests.post(url, headers=session.headers, data=json)
|
|
45
|
+
# modified to use our error handling
|
|
46
|
+
if response.status_code // 100 != 2:
|
|
47
|
+
content = response.json()['error']
|
|
48
|
+
print_error(f"{content['message']}")
|
|
49
|
+
errors = content.get('errors')
|
|
50
|
+
if errors is not None:
|
|
51
|
+
for error in errors:
|
|
52
|
+
if "code" in error:
|
|
53
|
+
print_error(f"{error['message']}")
|
|
54
|
+
else:
|
|
55
|
+
print_error(f"{error['reason']}")
|
|
56
|
+
return None
|
|
57
|
+
else:
|
|
58
|
+
data = response.json()['data']
|
|
59
|
+
return PlacedOrderResponse(**data)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RenewableSession(ProductionSession):
|
|
63
|
+
def __init__(self):
|
|
64
|
+
custom_path = os.path.join(os.path.expanduser('~'), CUSTOM_CONFIG_PATH)
|
|
65
|
+
default_path = os.path.join(sys.prefix, DEFAULT_CONFIG_PATH)
|
|
66
|
+
token_path = os.path.join(os.path.expanduser('~'), TOKEN_PATH)
|
|
67
|
+
|
|
68
|
+
logged_in = False
|
|
69
|
+
# try to load token
|
|
70
|
+
if os.path.exists(token_path):
|
|
71
|
+
with open(token_path, 'rb') as f:
|
|
72
|
+
self.__dict__ = pickle.load(f)
|
|
73
|
+
|
|
74
|
+
# make sure token hasn't expired
|
|
75
|
+
logged_in = self.validate()
|
|
76
|
+
|
|
77
|
+
# load config
|
|
78
|
+
self.config = ConfigParser()
|
|
79
|
+
if not os.path.exists(custom_path):
|
|
80
|
+
# copy default config to user home dir
|
|
81
|
+
os.makedirs(os.path.dirname(custom_path), exist_ok=True)
|
|
82
|
+
shutil.copyfile(default_path, custom_path)
|
|
83
|
+
self.config.read(default_path)
|
|
84
|
+
self.config.read(custom_path)
|
|
85
|
+
|
|
86
|
+
if not logged_in:
|
|
87
|
+
# either the token expired or doesn't exist
|
|
88
|
+
username, password = self._get_credentials()
|
|
89
|
+
ProductionSession.__init__(self, username, password)
|
|
90
|
+
|
|
91
|
+
accounts = Account.get_accounts(self)
|
|
92
|
+
self.accounts = [acc for acc in accounts if not acc.is_closed]
|
|
93
|
+
# write session token to cache
|
|
94
|
+
os.makedirs(os.path.dirname(token_path), exist_ok=True)
|
|
95
|
+
with open(token_path, 'wb') as f:
|
|
96
|
+
pickle.dump(self.__dict__, f)
|
|
97
|
+
logger.debug('Logged in with new session, cached for next login.')
|
|
98
|
+
else:
|
|
99
|
+
logger.debug('Logged in with cached session.')
|
|
100
|
+
|
|
101
|
+
def _get_credentials(self):
|
|
102
|
+
username = os.getenv('TT_USERNAME')
|
|
103
|
+
password = os.getenv('TT_PASSWORD')
|
|
104
|
+
if self.config.has_section('general'):
|
|
105
|
+
username = username or self.config['general'].get('username')
|
|
106
|
+
password = password or self.config['general'].get('password')
|
|
107
|
+
|
|
108
|
+
if not username:
|
|
109
|
+
username = getpass.getpass('Username: ')
|
|
110
|
+
if not password:
|
|
111
|
+
password = getpass.getpass('Password: ')
|
|
112
|
+
|
|
113
|
+
return username, password
|
|
114
|
+
|
|
115
|
+
def get_account(self) -> Account:
|
|
116
|
+
account = self.config['general'].get('default-account', None)
|
|
117
|
+
if account:
|
|
118
|
+
try:
|
|
119
|
+
return next(a for a in self.accounts if a.account_number == account)
|
|
120
|
+
except StopIteration:
|
|
121
|
+
print_warning('Default account is set, but the account doesn\'t appear to exist!')
|
|
122
|
+
|
|
123
|
+
for i in range(len(self.accounts)):
|
|
124
|
+
if i == 0:
|
|
125
|
+
print(f'{i + 1}) {self.accounts[i].account_number} '
|
|
126
|
+
f'{self.accounts[i].nickname} (default)')
|
|
127
|
+
else:
|
|
128
|
+
print(f'{i + 1}) {self.accounts[i].account_number} {self.accounts[i].nickname}')
|
|
129
|
+
choice = 0
|
|
130
|
+
while choice not in range(1, len(self.accounts) + 1):
|
|
131
|
+
try:
|
|
132
|
+
raw = input('Please choose an account: ')
|
|
133
|
+
choice = int(raw)
|
|
134
|
+
except ValueError:
|
|
135
|
+
if not raw:
|
|
136
|
+
return self.accounts[0]
|
|
137
|
+
return self.accounts[choice - 1]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def is_monthly(day: date) -> bool:
|
|
141
|
+
return day.weekday() == 4 and 15 <= day.day <= 21
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_confirmation(prompt: str) -> bool:
|
|
145
|
+
while True:
|
|
146
|
+
answer = input(prompt).lower()
|
|
147
|
+
if not answer:
|
|
148
|
+
return True
|
|
149
|
+
if answer[0] == 'y':
|
|
150
|
+
return True
|
|
151
|
+
if answer[0] == 'n':
|
|
152
|
+
return False
|