├── thetagang ├── __init__.py ├── entry.py ├── options.py ├── main.py ├── util.py ├── test_util.py ├── thetagang.py ├── config.py └── portfolio_manager.py ├── .github ├── FUNDING.yml └── workflows │ ├── python-publish.yaml │ └── docker-publish.yml ├── entrypoint.bash ├── vscode.py ├── COPYING ├── Dockerfile ├── pyproject.toml ├── .dockerignore ├── .gitignore ├── thetagang.toml ├── README.md ├── ibc-config.ini ├── LICENSE └── poetry.lock /thetagang/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thetagang/entry.py: -------------------------------------------------------------------------------- 1 | # Do not reorder imports 2 | from .main import * # NOQA: E402 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: brndnmtthws 4 | -------------------------------------------------------------------------------- /entrypoint.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | Xvfb :1 -ac -screen 0 1024x768x24 & 7 | export DISPLAY=:1 8 | 9 | exec poetry run thetagang "$@" 10 | -------------------------------------------------------------------------------- /thetagang/options.py: -------------------------------------------------------------------------------- 1 | def contract_date_to_datetime(expiration): 2 | from datetime import datetime 3 | 4 | if len(expiration) == 8: 5 | return datetime.strptime(expiration, "%Y%m%d") 6 | else: 7 | return datetime.strptime(expiration, "%Y%m") 8 | 9 | 10 | def option_dte(expiration): 11 | from datetime import date 12 | 13 | dte = contract_date_to_datetime(expiration).date() - date.today() 14 | return dte.days 15 | -------------------------------------------------------------------------------- /vscode.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | from pathlib import Path 4 | 5 | 6 | def vscode(): 7 | venv_path = subprocess.check_output("poetry env info --path".split()) 8 | venv_path = venv_path.decode("UTF-8").rstrip() 9 | 10 | settings = dict() 11 | 12 | Path(".vscode").mkdir(parents=True, exist_ok=True) 13 | Path(".vscode/settings.json").touch() 14 | 15 | with open(".vscode/settings.json", "r") as f: 16 | try: 17 | settings = json.load(f) 18 | except json.decoder.JSONDecodeError: 19 | pass 20 | settings["python.pythonPath"] = venv_path 21 | 22 | with open(".vscode/settings.json", "w") as f: 23 | json.dump(settings, f, sort_keys=True, indent=4) 24 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Brenden Matthews 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU Affero General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /thetagang/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import click 4 | import click_log 5 | 6 | logger = logging.getLogger(__name__) 7 | click_log.basic_config(logger) 8 | 9 | 10 | CONTEXT_SETTINGS = dict( 11 | help_option_names=["-h", "--help"], auto_envvar_prefix="THETAGANG" 12 | ) 13 | 14 | 15 | @click.command(context_settings=CONTEXT_SETTINGS) 16 | @click_log.simple_verbosity_option(logger) 17 | @click.option( 18 | "-c", 19 | "--config", 20 | help="Path to toml config", 21 | required=True, 22 | default="thetagang.toml", 23 | type=click.Path(exists=True, readable=True), 24 | ) 25 | def cli(config): 26 | """ThetaGang is an IBKR bot for collecting money. 27 | 28 | You can configure this tool by supplying a toml configuration file. 29 | There's a sample config on GitHub, here: 30 | https://github.com/brndnmtthws/thetagang/blob/main/thetagang.toml 31 | """ 32 | 33 | from .thetagang import start 34 | 35 | start(config) 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye 2 | 3 | RUN apt-get update \ 4 | && apt-get install -qy python3-pip xvfb libxrender1 openjfx unzip curl \ 5 | && pip3 install --upgrade pip poetry \ 6 | && echo 'bb901694b94d6332e86a468cffe96d842d6f2fabacb97977903ab808408f7583 tws-installer.sh' | tee tws-installer.sh.sha256 \ 7 | && curl -qL https://download2.interactivebrokers.com/installers/tws/stable-standalone/tws-stable-standalone-linux-x64.sh -o tws-installer.sh \ 8 | && sha256sum -c tws-installer.sh.sha256 \ 9 | && echo 'c079e0ade7e95069e464859197498f0abb4ce277b2f101d7474df4826dcac837 ibc.zip' | tee ibc.zip.sha256 \ 10 | && curl -qL https://github.com/IbcAlpha/IBC/releases/download/3.8.4-beta.2/IBCLinux-3.8.4-beta.2.zip -o ibc.zip \ 11 | && sha256sum -c ibc.zip.sha256 \ 12 | && yes "" | sh tws-installer.sh \ 13 | && unzip ibc.zip -d /opt/ibc \ 14 | && chmod o+x /opt/ibc/*.sh /opt/ibc/*/*.sh \ 15 | && rm tws-installer.sh ibc.zip \ 16 | && apt-get clean \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | ADD . /src 20 | WORKDIR /src 21 | 22 | RUN poetry install 23 | 24 | ENTRYPOINT [ "/src/entrypoint.bash" ] 25 | -------------------------------------------------------------------------------- /thetagang/util.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from ib_insync.contract import Option 4 | 5 | 6 | def to_camel_case(snake_str): 7 | components = snake_str.split("_") 8 | # We capitalize the first letter of each component except the first one 9 | # with the 'title' method and join them together. 10 | return components[0] + "".join(x.title() for x in components[1:]) 11 | 12 | 13 | def account_summary_to_dict(account_summary): 14 | d = dict() 15 | for s in account_summary: 16 | d[s.tag] = s 17 | return d 18 | 19 | 20 | def portfolio_positions_to_dict(portfolio_positions): 21 | d = dict() 22 | for p in portfolio_positions: 23 | symbol = p.contract.symbol 24 | if symbol not in d: 25 | d[symbol] = [] 26 | d[symbol].append(p) 27 | return d 28 | 29 | 30 | def justify(s): 31 | return s.rjust(12) 32 | 33 | 34 | def position_pnl(position): 35 | return position.unrealizedPNL / abs(position.averageCost * position.position) 36 | 37 | 38 | def count_option_positions(symbol, portfolio_positions, right): 39 | if symbol in portfolio_positions: 40 | return math.floor( 41 | -sum( 42 | [ 43 | p.position 44 | for p in portfolio_positions[symbol] 45 | if isinstance(p.contract, Option) 46 | and p.contract.right.startswith(right) 47 | ] 48 | ) 49 | ) 50 | 51 | return 0 52 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yaml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.8", "3.9"] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip poetry 26 | poetry install 27 | - name: Test with pytest 28 | run: | 29 | poetry run py.test 30 | 31 | deploy: 32 | needs: test 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Set up Python 38 | uses: actions/setup-python@v2 39 | with: 40 | python-version: "3.x" 41 | - name: Install dependencies 42 | run: | 43 | python -m pip install --upgrade pip poetry 44 | - name: Build and publish 45 | env: 46 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 47 | run: | 48 | poetry build 49 | poetry publish 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | authors = ["Brenden Matthews "] 3 | description = "ThetaGang is an IBKR bot for getting money" 4 | documentation = "https://github.com/brndnmtthws/thetagang/blob/master/README.md" 5 | homepage = "https://github.com/brndnmtthws/thetagang" 6 | license = "AGPL-3.0-only" 7 | name = "thetagang" 8 | readme = "README.md" 9 | repository = "https://github.com/brndnmtthws/thetagang.git" 10 | version = "0.1.10" 11 | 12 | [tool.poetry.dependencies] 13 | click = "^7.1.2" 14 | click-log = "^0.3.2" 15 | colorama = "^0.4.4" 16 | ib_insync = "^0.9.64" 17 | pandas = "^1.1.4" 18 | python = "^3.8" 19 | python-dateutil = "^2.8.1" 20 | pytimeparse = "^1.1.8" 21 | schema = "^0.7.3" 22 | toml = "^0.10.2" 23 | 24 | [tool.poetry.dev-dependencies] 25 | autoflake = "^1.4" 26 | autohooks = "^2.2.0" 27 | autohooks-plugin-black = "^1.2.0" 28 | autohooks-plugin-isort = "^1.0.0" 29 | black = "^20.8b1" 30 | isort = "^5.6.4" 31 | pylint = "^2.6.0" 32 | pytest = "^6.1.2" 33 | 34 | [tool.poetry.urls] 35 | "Bug Tracker" = "https://github.com/brndnmtthws/thetagang/issues" 36 | "GitHub" = "https://github.com/brndnmtthws/thetagang" 37 | 38 | [tool.poetry.scripts] 39 | thetagang = 'thetagang.entry:cli' 40 | vscode = "vscode:vscode" 41 | 42 | [tool.pylint.messages_control] 43 | disable = "C0330, C0326" 44 | 45 | [tool.pylint.format] 46 | max-line-length = "88" 47 | 48 | [tool.isort] 49 | ensure_newline_before_comments = true 50 | force_grid_wrap = 0 51 | include_trailing_comma = true 52 | line_length = 88 53 | multi_line_output = 3 54 | use_parentheses = true 55 | 56 | [tool.autohooks] 57 | mode = "poetry" 58 | pre-commit = ["autohooks.plugins.isort", "autohooks.plugins.black"] 59 | 60 | [build-system] 61 | build-backend = "poetry.masonry.api" 62 | requires = ["poetry>=0.12"] 63 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Ignore dotenv/direnv files 141 | .env* 142 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Ignore dotenv/direnv files 141 | .env* 142 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | # Publish `main` as Docker `latest` image. 6 | branches: 7 | - main 8 | 9 | # Publish `v1.2.3` tags as releases. 10 | tags: 11 | - v* 12 | 13 | # Run tests for any PRs. 14 | pull_request: 15 | 16 | env: 17 | IMAGE_NAME: thetagang 18 | DOCKERHUB_ACCOUNT: brndnmtthws 19 | 20 | jobs: 21 | test: 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | python-version: ["3.8", "3.9"] 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Set up Python ${{ matrix.python-version }} 30 | uses: actions/setup-python@v2 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip poetry 36 | poetry install 37 | - name: Test with pytest 38 | run: | 39 | poetry run py.test 40 | 41 | # Push image to GitHub Packages. 42 | # See also https://docs.docker.com/docker-hub/builds/ 43 | push: 44 | needs: test 45 | runs-on: ubuntu-latest 46 | if: github.event_name == 'push' 47 | 48 | steps: 49 | - uses: actions/checkout@v2 50 | 51 | - name: Build image 52 | run: docker build . --file Dockerfile --tag $IMAGE_NAME 53 | 54 | - name: Log into GitHub registry 55 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin 56 | - name: Log into Dockerhub registry 57 | run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u $DOCKERHUB_ACCOUNT --password-stdin 58 | 59 | - name: Push image 60 | run: | 61 | GITHUB_IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME 62 | DOCKERHUB_IMAGE_ID=$DOCKERHUB_ACCOUNT/$IMAGE_NAME 63 | 64 | # Change all uppercase to lowercase 65 | GITHUB_IMAGE_ID=$(echo $GITHUB_IMAGE_ID | tr '[A-Z]' '[a-z]') 66 | DOCKERHUB_IMAGE_ID=$(echo $DOCKERHUB_IMAGE_ID | tr '[A-Z]' '[a-z]') 67 | 68 | # Strip git ref prefix from version 69 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 70 | 71 | # Strip "v" prefix from tag name 72 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') 73 | 74 | # Use Docker `latest` tag convention 75 | [ "$VERSION" == "main" ] && VERSION=latest 76 | 77 | echo GITHUB_IMAGE_ID=$GITHUB_IMAGE_ID 78 | echo DOCKERHUB_IMAGE_ID=$DOCKERHUB_IMAGE_ID 79 | echo VERSION=$VERSION 80 | 81 | docker tag $IMAGE_NAME $GITHUB_IMAGE_ID:$VERSION 82 | docker push $GITHUB_IMAGE_ID:$VERSION 83 | 84 | docker tag $IMAGE_NAME $DOCKERHUB_IMAGE_ID:$VERSION 85 | docker push $DOCKERHUB_IMAGE_ID:$VERSION 86 | -------------------------------------------------------------------------------- /thetagang/test_util.py: -------------------------------------------------------------------------------- 1 | from ib_insync import Option, PortfolioItem 2 | from ib_insync.contract import Stock 3 | 4 | from thetagang.util import position_pnl 5 | 6 | 7 | def test_position_pnl(): 8 | qqq_put = PortfolioItem( 9 | contract=Option( 10 | conId=397556522, 11 | symbol="QQQ", 12 | lastTradeDateOrContractMonth="20201218", 13 | strike=300.0, 14 | right="P", 15 | multiplier="100", 16 | primaryExchange="AMEX", 17 | currency="USD", 18 | localSymbol="QQQ 201218P00300000", 19 | tradingClass="QQQ", 20 | ), 21 | position=-1.0, 22 | marketPrice=4.1194396, 23 | marketValue=-411.94, 24 | averageCost=222.4293, 25 | unrealizedPNL=-189.51, 26 | realizedPNL=0.0, 27 | account="DU2962946", 28 | ) 29 | assert round(position_pnl(qqq_put), 2) == -0.85 30 | 31 | spy = PortfolioItem( 32 | contract=Stock( 33 | conId=756733, 34 | symbol="SPY", 35 | right="0", 36 | primaryExchange="ARCA", 37 | currency="USD", 38 | localSymbol="SPY", 39 | tradingClass="SPY", 40 | ), 41 | position=100.0, 42 | marketPrice=365.4960022, 43 | marketValue=36549.6, 44 | averageCost=368.42, 45 | unrealizedPNL=-292.4, 46 | realizedPNL=0.0, 47 | account="DU2962946", 48 | ) 49 | assert round(position_pnl(spy), 4) == -0.0079 50 | 51 | spy_call = PortfolioItem( 52 | contract=Option( 53 | conId=454208258, 54 | symbol="SPY", 55 | lastTradeDateOrContractMonth="20201214", 56 | strike=373.0, 57 | right="C", 58 | multiplier="100", 59 | primaryExchange="AMEX", 60 | currency="USD", 61 | localSymbol="SPY 201214C00373000", 62 | tradingClass="SPY", 63 | ), 64 | position=-1.0, 65 | marketPrice=0.08, 66 | marketValue=-8.0, 67 | averageCost=96.422, 68 | unrealizedPNL=88.42, 69 | realizedPNL=0.0, 70 | account="DU2962946", 71 | ) 72 | assert round(position_pnl(spy_call), 2) == 0.92 73 | 74 | spy_put = PortfolioItem( 75 | contract=Option( 76 | conId=458705534, 77 | symbol="SPY", 78 | lastTradeDateOrContractMonth="20210122", 79 | strike=352.5, 80 | right="P", 81 | multiplier="100", 82 | primaryExchange="AMEX", 83 | currency="USD", 84 | localSymbol="SPY 210122P00352500", 85 | tradingClass="SPY", 86 | ), 87 | position=-1.0, 88 | marketPrice=5.96710015, 89 | marketValue=-596.71, 90 | averageCost=528.9025, 91 | unrealizedPNL=-67.81, 92 | realizedPNL=0.0, 93 | account="DU2962946", 94 | ) 95 | assert round(position_pnl(spy_put), 2) == -0.13 96 | -------------------------------------------------------------------------------- /thetagang/thetagang.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | 5 | import click 6 | from ib_insync import IB, IBC, Index, Watchdog, util 7 | from ib_insync.contract import Contract, Stock 8 | from ib_insync.objects import Position 9 | 10 | from thetagang.config import normalize_config, validate_config 11 | 12 | from .portfolio_manager import PortfolioManager 13 | from .util import ( 14 | account_summary_to_dict, 15 | justify, 16 | portfolio_positions_to_dict, 17 | position_pnl, 18 | to_camel_case, 19 | ) 20 | 21 | util.patchAsyncio() 22 | 23 | 24 | def start(config): 25 | import toml 26 | 27 | with open(config, "r") as f: 28 | config = toml.load(f) 29 | 30 | config = normalize_config(config) 31 | 32 | validate_config(config) 33 | 34 | click.secho(f"Config:", fg="green") 35 | click.echo() 36 | 37 | click.secho(f" Account details:", fg="green") 38 | click.secho( 39 | f" Number = {config['account']['number']}", fg="cyan" 40 | ) 41 | click.secho( 42 | f" Cancel existing orders = {config['account']['cancel_orders']}", 43 | fg="cyan", 44 | ) 45 | click.secho( 46 | f" Margin usage = {config['account']['margin_usage']} ({config['account']['margin_usage'] * 100}%)", 47 | fg="cyan", 48 | ) 49 | click.secho( 50 | f" Market data type = {config['account']['market_data_type']}", 51 | fg="cyan", 52 | ) 53 | click.echo() 54 | 55 | click.secho(f" Roll options when either condition is true:", fg="green") 56 | click.secho( 57 | f" Days to expiry <= {config['roll_when']['dte']}", fg="cyan" 58 | ) 59 | click.secho( 60 | f" P&L >= {config['roll_when']['pnl']} ({config['roll_when']['pnl'] * 100}%)", 61 | fg="cyan", 62 | ) 63 | 64 | click.echo() 65 | click.secho(f" Write options with targets of:", fg="green") 66 | click.secho(f" Days to expiry >= {config['target']['dte']}", fg="cyan") 67 | click.secho( 68 | f" Delta <= {config['target']['delta']}", fg="cyan" 69 | ) 70 | click.secho( 71 | f" Minimum open interest >= {config['target']['minimum_open_interest']}", 72 | fg="cyan", 73 | ) 74 | 75 | click.echo() 76 | click.secho(f" Symbols:", fg="green") 77 | for s in config["symbols"].keys(): 78 | click.secho( 79 | f" {s}, weight = {config['symbols'][s]['weight']} ({config['symbols'][s]['weight'] * 100}%)", 80 | fg="cyan", 81 | ) 82 | assert ( 83 | sum([config["symbols"][s]["weight"] for s in config["symbols"].keys()]) == 1.0 84 | ) 85 | click.echo() 86 | 87 | if config.get("ib_insync", {}).get("logfile"): 88 | util.logToFile(config["ib_insync"]["logfile"]) 89 | 90 | ibc = IBC(978, **config["ibc"]) 91 | 92 | def onConnected(): 93 | portfolio_manager.manage() 94 | 95 | ib = IB() 96 | ib.connectedEvent += onConnected 97 | 98 | completion_future = asyncio.Future() 99 | portfolio_manager = PortfolioManager(config, ib, completion_future) 100 | 101 | probeContractConfig = config["watchdog"]["probeContract"] 102 | watchdogConfig = config.get("watchdog") 103 | del watchdogConfig["probeContract"] 104 | probeContract = Contract( 105 | secType=probeContractConfig["secType"], 106 | symbol=probeContractConfig["symbol"], 107 | currency=probeContractConfig["currency"], 108 | exchange=probeContractConfig["exchange"], 109 | ) 110 | 111 | watchdog = Watchdog(ibc, ib, probeContract=probeContract, **watchdogConfig) 112 | 113 | watchdog.start() 114 | ib.run(completion_future) 115 | watchdog.stop() 116 | ibc.terminate() 117 | -------------------------------------------------------------------------------- /thetagang/config.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import click 4 | from schema import And, Optional, Schema, Use 5 | 6 | 7 | def normalize_config(config): 8 | # Do any pre-processing necessary to the config here, such as handling 9 | # defaults, deprecated values, config changes, etc. 10 | 11 | if "twsVersion" in config["ibc"]: 12 | click.secho( 13 | "WARNING: IBC config param 'twsVersion' is deprecated, please remove it from your config.", 14 | fg="yellow", 15 | err=True, 16 | ) 17 | 18 | # TWS version is pinned to latest stable, delete any existing config if it's present 19 | del config["ibc"]["twsVersion"] 20 | 21 | return config 22 | 23 | 24 | def validate_config(config): 25 | if "minimum_cushion" in config["account"]: 26 | raise "Config error: minimum_cushion is deprecated and replaced with margin_usage. See sample config for details." 27 | 28 | schema = Schema( 29 | { 30 | "account": { 31 | "number": And(str, len), 32 | "cancel_orders": bool, 33 | "margin_usage": And(float, lambda n: 0 <= n), 34 | "market_data_type": And(int, lambda n: 1 <= n <= 4), 35 | }, 36 | "option_chains": { 37 | "expirations": And(int, lambda n: 1 <= n), 38 | "strikes": And(int, lambda n: 1 <= n), 39 | }, 40 | "roll_when": { 41 | "pnl": And(float, lambda n: 0 <= n <= 1), 42 | "dte": And(int, lambda n: 0 <= n), 43 | Optional("calls"): { 44 | "itm": bool, 45 | }, 46 | Optional("puts"): { 47 | "itm": bool, 48 | }, 49 | }, 50 | "target": { 51 | "dte": And(int, lambda n: 0 <= n), 52 | "delta": And(float, lambda n: 0 <= n <= 1), 53 | "minimum_open_interest": And(int, lambda n: 0 <= n), 54 | }, 55 | "symbols": {object: {"weight": And(float, lambda n: 0 <= n <= 1)}}, 56 | Optional("ib_insync"): {Optional("logfile"): And(str, len)}, 57 | "ibc": { 58 | Optional("password"): And(str, len), 59 | Optional("userid"): And(str, len), 60 | Optional("gateway"): bool, 61 | Optional("ibcPath"): And(str, len), 62 | Optional("tradingMode"): And( 63 | str, len, lambda s: s in ("live", "paper") 64 | ), 65 | Optional("ibcIni"): And(str, len), 66 | Optional("twsPath"): And(str, len), 67 | Optional("twsSettingsPath"): And(str, len), 68 | Optional("javaPath"): And(str, len), 69 | Optional("fixuserid"): And(str, len), 70 | Optional("fixpassword"): And(str, len), 71 | }, 72 | "watchdog": { 73 | Optional("appStartupTime"): int, 74 | Optional("appTimeout"): int, 75 | Optional("clientId"): int, 76 | Optional("connectTimeout"): int, 77 | Optional("host"): And(str, len), 78 | Optional("port"): int, 79 | Optional("probeTimeout"): int, 80 | Optional("readonly"): bool, 81 | Optional("retryDelay"): int, 82 | Optional("probeContract"): { 83 | Optional("currency"): And(str, len), 84 | Optional("exchange"): And(str, len), 85 | Optional("secType"): And(str, len), 86 | Optional("symbol"): And(str, len), 87 | }, 88 | }, 89 | } 90 | ) 91 | schema.validate(config) 92 | 93 | assert len(config["symbols"]) > 0 94 | assert math.isclose( 95 | 1, sum([s["weight"] for s in config["symbols"].values()]), rel_tol=1e-5 96 | ) 97 | -------------------------------------------------------------------------------- /thetagang.toml: -------------------------------------------------------------------------------- 1 | # NOTE: It is STRONGLY recommended you read through all notes, config options, 2 | # and documentation before proceeding. Be sure to update the configuration 3 | # values according to your preferences. Additionally, any default values in 4 | # this config do not constitute a recommendation or endorsement, or any provide 5 | # claims abount returns or performance. 6 | # 7 | # Should you decide to use ThetaGang, please experiment with a paper trading 8 | # account before trying on a live account. 9 | 10 | [account] 11 | # The account number to operate on 12 | number = "DU1234567" 13 | 14 | # Cancel any existing orders for the symbols configured at startup 15 | cancel_orders = true 16 | 17 | # Maximum amount of margin to use, as a percentage of net liquidation. IB lets 18 | # you use varying amounts of margin, depending on the assets. To use up to 4x 19 | # margin, set this to 4. It's recommended you always leave some additional 20 | # cushion. IB will start to close positions if you go below certain thresholds 21 | # of available margin in your account. 22 | # 23 | # For details on margin usage, see: 24 | # https://www.interactivebrokers.com/en/index.php?f=24176 25 | # 26 | # The default value uses 50% of your available net liquidation value 27 | # (i.e., half of your funds). Set this to 1.0 to use all your funds, 28 | # or 1.5 to use 150% (which may incur margin interest charges). 29 | margin_usage = 0.5 30 | 31 | # Market data type (see 32 | # https://interactivebrokers.github.io/tws-api/market_data_type.html) 33 | market_data_type = 1 34 | 35 | [option_chains] 36 | # Number of expirations to load from option chains 37 | expirations = 5 38 | # Number of strikes to load from option chains 39 | strikes = 50 40 | 41 | [roll_when] 42 | # Roll when P&L reaches 90% 43 | pnl = 0.9 44 | 45 | # Or when there are <= 15 days to expiry 46 | dte = 15 47 | 48 | [roll_when.calls] 49 | # Roll calls to the next expiration even if they're in the money. Defaults to 50 | # true if not specified. 51 | itm = true 52 | 53 | [roll_when.puts] 54 | # Roll puts if they're in the money. Defaults to false if not specified. 55 | itm = false 56 | 57 | [target] 58 | # Target 45 or more days to expiry 59 | dte = 45 60 | 61 | # Target delta of 0.3 or less 62 | delta = 0.3 63 | 64 | # Minimum amount of open interest for a contract to qualify 65 | minimum_open_interest = 10 66 | 67 | [symbols] 68 | 69 | # NOTE: Please change these symbols and weights according to your preferences. 70 | # These are provided only as an example for the purpose of configuration. These 71 | # values were chosen as sane values should someone decide to run this code 72 | # without changes, however it is in no way a recommendation or endorsement. 73 | # 74 | # You should try to choose ETFs or stocks that: 75 | # 76 | # 1) Have sufficient trading volume for the underlying 77 | # 2) Have standard options contracts (100 shares per contract) 78 | # 3) Have options with sufficient open interest and trading volume 79 | [symbols.SPY] 80 | weight = 0.4 81 | 82 | [symbols.QQQ] 83 | weight = 0.3 84 | 85 | [symbols.TLT] 86 | weight = 0.3 87 | 88 | [ib_insync] 89 | logfile = 'ib_insync.log' 90 | 91 | [ibc] 92 | # IBC configuration parameters. See 93 | # https://ib-insync.readthedocs.io/api.html#ibc for details. 94 | gateway = true 95 | ibcPath = '/opt/ibc' 96 | password = 'demo' 97 | tradingMode = 'paper' 98 | userid = 'demo' 99 | # Change this to point to your config.ini for IBC 100 | ibcIni = '/etc/thetagang/config.ini' 101 | 102 | # twsPath = '' 103 | # twsSettingsPath = '' 104 | # javaPath = '' 105 | # fixuserid = '' 106 | # fixpassword = '' 107 | 108 | [watchdog] 109 | # Watchdog configuration params. See 110 | # https://ib-insync.readthedocs.io/api.html#watchdog for details. 111 | appStartupTime = 30 112 | appTimeout = 20 113 | clientId = 1 114 | connectTimeout = 2 115 | host = '127.0.0.1' 116 | port = 4002 117 | probeTimeout = 4 118 | readonly = false 119 | retryDelay = 2 120 | 121 | [watchdog.probeContract] 122 | currency = 'USD' 123 | exchange = 'SMART' 124 | secType = 'STK' 125 | symbol = 'SPY' 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Θ ThetaGang Θ 2 | 3 | *Beat the capitalists at their own game with ThetaGang 📈* 4 | 5 | ThetaGang is an [IBKR](https://www.interactivebrokers.com/) trading bot for 6 | collecting premium by selling options using "The Wheel" strategy. The Wheel 7 | is a strategy that [surfaced on 8 | Reddit](https://www.reddit.com/r/options/comments/a36k4j/the_wheel_aka_triple_income_strategy_explained/), 9 | but has been used by many in the past. This bot implements a slightly 10 | modified version of The Wheel, with my own personal tweaks. 11 | 12 | I've been streaming most of the work on this project [on Twitch, so follow me 13 | over there](https://www.twitch.tv/letsmakestuff). 14 | 15 | ## How it works 16 | 17 | You should start by reading [the Reddit 18 | post](https://www.reddit.com/r/options/comments/a36k4j/the_wheel_aka_triple_income_strategy_explained/) 19 | to get some background. 20 | 21 | The strategy, as implemented here, does a few things differently from the one 22 | described in the post above. For one, it's intended to be used to augment a 23 | typical index-fund based portfolio with specific asset allocations. For 24 | example, you might want to use a 60/40 portfolio with SPY (S&P500 fund) and 25 | TLT (20 year treasury fund). 26 | 27 | The main difference between ThetaGang and simply buying and holding index 28 | funds is that this script will attempt to harvest volatility by selling 29 | options, rather than buying shares directly. This works because implied 30 | volatility is typically higher than realized volatility on average. Instead 31 | of buying shares, you write puts. This has pros and cons, which are outside 32 | the scope of this README. 33 | 34 | You could use this tool on individual stocks, but I personally don't 35 | recommend it because I am not smart enough to understand which stocks to buy. 36 | That's why I just buy index funds. 37 | 38 | ThetaGang will try to acquire your desired allocation of each stock or ETF 39 | according to the weights you specify in the config. To acquire the positions, 40 | the script will write puts when conditions are met (adequate buying power, 41 | acceptable contracts are available, enough shares needed, etc). 42 | 43 | ThetaGang will continue to roll any open option positions indefinitely, with 44 | the only exception being ITM puts. Once puts are in the money, they will be 45 | ignored until they expire and are execised (after which you will own the 46 | underlying). 47 | 48 | Please note: this strategy is based on the assumption that implied volatility 49 | is, on average, always higher than realized volatility. In cases where this 50 | is not true, this strategy will cause you to lose money. 51 | 52 | In the case of deep ITM calls, the bot will prefer to roll the calls to next 53 | expiration rather than allowing the underlying to get called away. If you 54 | don't have adequate buying power available in your account, it's possible 55 | that the options may get exercised instead of rolling forward and the process 56 | starts back at the beginning. Please keep in mind this may have tax 57 | implications, but that is outside the scope of this README. 58 | 59 | In normal usage, you would run the script as a cronjob on a daily, weekly, or 60 | monthly basis according to your preferences. Running more frequently than 61 | daily is not recommended, but the choice is yours. 62 | 63 | ## Project status 64 | 65 | This project is, in its current state, considered to be complete. I'm open 66 | to contributions, but I am unlikely to accept PRs or feature requests that 67 | involve significant changes to the underlying algorithm. 68 | 69 | If you find something that you think is a bug, or some other issue, please 70 | [create a new issue](https://github.com/brndnmtthws/thetagang/issues/new). 71 | 72 | ## "Show me your gains bro" – i.e., what are the returns? 73 | 74 | As discussed elsewhere in this README, you must conduct your own research, 75 | and I suggest starting with resources such as CBOE's BXM and BXDM indices, 76 | and comparing those to SPX. I've had a lot of people complain because "that 77 | strategy isn't better than buy and hold BRUH"–let me assure you, that is not 78 | my goal with this. 79 | 80 | Generally speaking, the point of selling options is not to exceed the returns 81 | of the underlying, but rather to reduce risk. Reducing risk is an important 82 | feature because it, in turn, allows one to increase risk in other ways 83 | (i.e., allocate higher percentage to stocks or buy riskier assets). 84 | 85 | Whether you use this or not is up to you. I have not one single fuck to give, 86 | whether you use it or not. I am not here to convince you to use it, I merely 87 | want to share knowledge and perhaps help create a little bit of wealth 88 | redistribution. 89 | 90 | 💫 91 | 92 | ## Requirements 93 | 94 | The bot is based on the [ib_insync](https://github.com/erdewit/ib_insync) 95 | library, and uses [IBC](https://github.com/IbcAlpha/IBC) for managing the API 96 | gateway. 97 | 98 | To use the bot, you'll need an Interactive Brokers account with a working 99 | installation of IBC. If you want to modify the bot, you'll need an 100 | installation of Python 3.8 or newer with the 101 | [`poetry`](https://python-poetry.org/) package manager. 102 | 103 | One more thing: to run this on a live account, you'll require enough capital 104 | to purchase at least 100 shares of the stocks or ETFs you choose. For 105 | example, if SPY is trading at $300/share you'd need $30,000 available. You 106 | can search for lower priced alternatives, but these tend to have low volume 107 | on options which may not be appropriate for this strategy. You should 108 | generally avoid low volume ETFs/stocks. If you don't have that kind of 109 | capital, you'll need to keep renting out your time to the capitalists until 110 | you can become a capitalist yourself. That's the way the pyramid scheme we 111 | call capitalism works. 112 | 113 | ## Installation 114 | 115 | *Before running ThetaGang, you should set up an IBKR paper account to test the 116 | code.* 117 | 118 | ```shell 119 | $ pip install thetagang 120 | ``` 121 | 122 | It's recommended you familiarize yourself with 123 | [IBC](https://github.com/IbcAlpha/IBC) so you know how it works. You'll need 124 | to know how to configure the various knows and settings, and make sure things 125 | like API ports are configured correctly. If you don't want to mess around too 126 | much, consider [running ThetaGang with Docker](#running-with-docker). 127 | 128 | ## Usage 129 | 130 | ```shell 131 | $ thetagang -h 132 | ``` 133 | 134 | ## Running with Docker 135 | 136 | My preferred way for running ThetaGang is to use a cronjob to execute Docker 137 | commands. I've built a Docker image as part of this project, which you can 138 | use with your installation. There's a [prebuilt Docker image 139 | here](https://hub.docker.com/repository/docker/brndnmtthws/thetagang). 140 | 141 | To run ThetaGang within Docker, you'll need to pass `config.ini` for [IBC 142 | configuration](https://github.com/IbcAlpha/IBC/blob/master/userguide.md) and 143 | [`thetagang.toml`](/thetagang.toml) for ThetaGang. There's a sample 144 | [`ibc-config.ini`](/ibc-config.ini) included in this repo for your convenience. 145 | 146 | The easiest way to get the config files into the container is by mounting a 147 | volume. For example, you can use the following command: 148 | 149 | ```shell 150 | $ docker run --rm -it \ 151 | -v ~/ibc:/ibc \ 152 | brndnmtthws/thetagang:latest \ 153 | --config /ibc/thetagang.toml 154 | ``` 155 | 156 | ## Development 157 | 158 | Check out the code to your local machine and install the Python dependencies: 159 | 160 | ```shell 161 | $ poetry install 162 | $ poetry run autohooks activate 163 | $ poetry run thetagang -h 164 | ... 165 | ``` 166 | 167 | You are now ready to make a splash! 🐳 168 | 169 | ## Support 170 | 171 | If you like what you see but want something different, I am willing 172 | to work on bespoke or custom trading bots for a fee. Reach out 173 | to me directly through my GitHub profile. 174 | -------------------------------------------------------------------------------- /ibc-config.ini: -------------------------------------------------------------------------------- 1 | # Note that in the comments in this file, TWS refers to either the Trader 2 | # Workstation or the Gateway for the IB API. 3 | # 4 | # When referred to below, the default value for a setting is the value 5 | # assumed if either the setting is included but no value is specified, or 6 | # the setting is not included at all. 7 | # 8 | # IBC may also be used to start the FIX CTCI Gateway. All settings 9 | # relating to this have names prefixed with FIX. 10 | # 11 | # The IB API Gateway and the FIX CTCI Gateway share the same code. Which 12 | # gateway actually runs is governed by an option on the initial gateway 13 | # login screen. The FIX setting described under IBC Startup 14 | # Settings below controls this. 15 | 16 | 17 | # 1. IBC Startup Settings 18 | # ------------------------- 19 | 20 | 21 | # IBC may be used to start the IB Gateway for the FIX CTCI. This 22 | # setting must be set to 'yes' if you want to run the FIX CTCI gateway. The 23 | # default is 'no'. 24 | 25 | FIX=no 26 | 27 | 28 | # 2. Authentication Settings 29 | # ---------------------------- 30 | # 31 | # TWS and the IB API gateway require a single username and password. 32 | # You may specify the username and password using the following settings: 33 | # 34 | # IbLoginId 35 | # IbPassword 36 | # 37 | # Alternatively, you can specify the username and password in the command 38 | # files used to start TWS or the Gateway, but this is not recommended for 39 | # security reasons. 40 | # 41 | # If you don't specify them, you will be prompted for them in the usual 42 | # login dialog when TWS starts (but whatever you have specified will be 43 | # included in the dialog automatically: for example you may specify the 44 | # username but not the password, and then you will be prompted for the 45 | # password via the login dialog). Note that if you specify either 46 | # the username or the password (or both) in the command file, then 47 | # IbLoginId and IbPassword settings defined in this file are ignored. 48 | # 49 | # 50 | # The FIX CTCI gateway requires one username and password for FIX order 51 | # routing, and optionally a separate username and password for market 52 | # data connections. You may specify the usernames and passwords using 53 | # the following settings: 54 | # 55 | # FIXLoginId 56 | # FIXPassword 57 | # IbLoginId (optional - for market data connections) 58 | # IbPassword (optional - for market data connections) 59 | # 60 | # Alternatively you can specify the FIX username and password in the 61 | # command file used to start the FIX CTCI Gateway, but this is not 62 | # recommended for security reasons. 63 | # 64 | # If you don't specify them, you will be prompted for them in the usual 65 | # login dialog when FIX CTCI gateway starts (but whatever you have 66 | # specified will be included in the dialog automatically: for example 67 | # you may specify the usernames but not the passwords, and then you will 68 | # be prompted for the passwords via the login dialog). Note that if you 69 | # specify either the FIX username or the FIX password (or both) on the 70 | # command line, then FIXLoginId and FIXPassword settings defined in this 71 | # file are ignored; he same applies to the market data username and 72 | # password. 73 | 74 | # IB API Authentication Settings 75 | # ------------------------------ 76 | 77 | # Your TWS username: 78 | 79 | IbLoginId='demo' 80 | 81 | 82 | # PLEASE DON'T CHANGE ANYTHING BELOW THIS LINE !! 83 | #============================================================================== 84 | 85 | # Notes: 86 | # 87 | 88 | # TWS_MAJOR_VRSN 89 | # 90 | # Specifies the major version number of TWS to be run. If you are 91 | # unsure of which version number to use, run TWS manually from the 92 | # icon on the desktop, then click Help > About Trader Workstation. In the 93 | # displayed information you'll see a line similar to this: 94 | # 95 | # Build 954.2a, Oct 30, 2015 4:07:54 PM 96 | # 97 | # Here the major version number is 954. Do not include the rest of the 98 | # version number in this setting. 99 | 100 | 101 | # Your TWS password: 102 | 103 | IbPassword='demo' 104 | 105 | 106 | # FIX CTCI Authentication Settings 107 | # -------------------------------- 108 | 109 | # Your FIX CTCI username: 110 | 111 | FIXLoginId= 112 | 113 | 114 | # Your FIX CTCI password: 115 | 116 | FIXPassword= 117 | 118 | 119 | # Second Factor Authentication Timeout Settings 120 | # --------------------------------------------- 121 | 122 | # If you use the IBKR Mobile app for second factor authentication, 123 | # and you fail to complete the process before the time limit imposed 124 | # by IBKR, you can use this setting to tell IBC to exit: arrangements 125 | # can then be made to automatically restart IBC in order to initiate 126 | # the login sequence afresh. Otherwise, manual intervention at TWS's 127 | # Second Factor Authentication dialog is needed to complete the 128 | # login. 129 | # 130 | # Permitted values are 'yes' and 'no'. The default is 'no'. 131 | # 132 | # Note that the scripts provided with the IBC zips for Windows and 133 | # Linux provide options to automatically restart in these 134 | # circumstances, but only if this setting is also set to 'yes'. 135 | 136 | ExitAfterSecondFactorAuthenticationTimeout=no 137 | 138 | 139 | # Ths setting is only relevant if 140 | # ExitAfterSecondFactorAuthenticationTimeout is set to 'yes'. 141 | # 142 | # It controls how long (in seconds) IBC waits for login to complete 143 | # after the user acknowledges the second factor authentication 144 | # alert at the IBKR Mobile app. If login has not completed after 145 | # this time, IBC terminates. 146 | # The default value is 40. 147 | 148 | SecondFactorAuthenticationExitInterval= 149 | 150 | 151 | # Trading Mode 152 | # ------------ 153 | # 154 | # TWS 955 introduced a new Trading Mode combo box on its login 155 | # dialog. This indicates whether the live account or the paper 156 | # trading account corresponding to the supplied credentials is 157 | # to be used. The allowed values are 'live' (the default) and 158 | # 'paper'. For earlier versions of TWS this setting has no 159 | # effect. 160 | 161 | TradingMode=live 162 | 163 | 164 | # 3. TWS Startup Settings 165 | # ------------------------- 166 | # 167 | # Path to the directory where TWS should store its settings. This is 168 | # normally the folder in which TWS is installed. However you may set 169 | # it to some other location if you wish (for example if you want to 170 | # run multiple instances of TWS with different settings). 171 | # 172 | # It is recommended for clarity that you use an absolute path. The 173 | # effect of using a relative path is undefined. 174 | # 175 | # Linux and OS X users should use the appropriate path syntax. 176 | # 177 | # Note that, for Windows users, you MUST use double separator 178 | # characters to separate the elements of the folder path: for 179 | # example, IbDir=C:\\IBLiveSettings is valid, but 180 | # IbDir=C:\IBLiveSettings is NOT valid and will give unexpected 181 | # results. Linux and OS X users need not use double separators, 182 | # but they are acceptable. 183 | # 184 | # The default is the current working directory when IBC is 185 | # started. 186 | 187 | IbDir= 188 | 189 | 190 | # If you wish to store a copy of your TWS settings on IB's 191 | # servers as well as locally on your computer, set this to 192 | # 'yes': this enables you to run TWS on different computers 193 | # with the same configuration, market data lines, etc. If set 194 | # to 'no', running TWS on different computers will not share the 195 | # same settings. If no value is specified, TWS will obtain its 196 | # settings from the same place as the last time this user logged 197 | # in (whether manually or using IBC). 198 | 199 | StoreSettingsOnServer= 200 | 201 | 202 | # Set to 'yes' to minimise TWS when it starts: 203 | 204 | MinimizeMainWindow=yes 205 | 206 | 207 | # When a user logs on to an IBKR account for trading purposes by any means, the 208 | # IBKR account server checks to see whether the account is already logged in 209 | # elsewhere. If so, a dialog is displayed to both the users that enables them 210 | # to determine what happens next. The `ExistingSessionDetectedAction` setting 211 | # instructs TWS how to proceed when it displays one of these dialogs: 212 | # 213 | # * If the existing TWS session is set to 'primary', the existing session 214 | # continues and the new session is not permitted to proceed. 215 | # 216 | # * If the existing TWS session is set to 'primaryoverride', the existing 217 | # session terminates and the new session is permitted to proceed. 218 | # 219 | # * If the new session is via TWS with 220 | # `ExistingSessionDetectedAction=secondary', the new TWS exits so that the 221 | # existing session is unaffected. 222 | # 223 | # * If the existing TWS session is set to 'manual', the user must handle the 224 | # dialog. 225 | # 226 | # The difference between `primary` and `primaryoverride` is that a 227 | # `primaryoverride` session can be taken over by a new `primary` or 228 | # `primaryoverride` session, but a `primary` session cannot be taken over by 229 | # any other session. 230 | # 231 | # When set to 'primary', if another TWS session is started and manually told to 232 | # end the `primary` session, the `primary` session is automatically reconnected. 233 | # 234 | # The default is 'manual'. 235 | 236 | ExistingSessionDetectedAction=primary 237 | 238 | 239 | # If set to 'accept', IBC automatically accepts incoming 240 | # API connection dialogs. If set to 'reject', IBC 241 | # automatically rejects incoming API connection dialogs. If 242 | # set to 'manual', the user must decide whether to accept or reject 243 | # incoming API connection dialogs. The default is 'manual'. 244 | # NB: it is recommended to set this to 'reject', and to explicitly 245 | # configure which IP addresses can connect to the API in TWS's API 246 | # configuration page, as this is much more secure (in this case, no 247 | # incoming API connection dialogs will occur for those IP addresses). 248 | 249 | AcceptIncomingConnectionAction=accept 250 | 251 | 252 | # NB: ShowAllTrades is deprecated starting with TWS 955. This is 253 | # because IB have changed the Trades window in such a way that it 254 | # is no longer possible to programmatically set the desired option. 255 | # However the user's setting is now persisted within and across 256 | # TWS sessions, so ShowAllTrades is no longer really needed. If 257 | # ShowAllTrades is set to 'yes', it will have no observable effect. 258 | # 259 | # The following description applies to TWS versions BEFORE TWS 955: 260 | # 261 | # If ShowAllTrades is set to 'yes', IBC causes TWS to 262 | # display the Trades log at startup, and sets the 'All' checkbox 263 | # to ensure that the API reports all executions that have occurred 264 | # during the past week. Moreover, any attempt by the user to change 265 | # any of the 'Show trades' checkboxes is ignored; similarly if the 266 | # user closes the Trades log, it is immediately re-displayed with 267 | # the 'All' checkbox set. If set to 'no', IBC does not 268 | # interact with the Trades log, and only the current session's 269 | # executions are returned via the API (unless the user changes the 270 | # Trades log checkboxes). The default is 'no'. 271 | 272 | ShowAllTrades=no 273 | 274 | 275 | # If OverrideTwsApiPort is set to an integer, IBC changes the 276 | # 'Socket port' in TWS's API configuration to that number shortly 277 | # after startup. Leaving the setting blank will make no change to 278 | # the current setting. This setting is only intended for use in 279 | # certain specialized situations where the port number needs to 280 | # be set dynamically at run-time: most users will never need it, 281 | # so don't use it unless you know you need it. 282 | 283 | OverrideTwsApiPort=4002 284 | 285 | 286 | # If ReadOnlyLogin is set to 'yes', and the user is enrolled in IB's 287 | # account security programme, the user will not be asked to supply 288 | # the security code, and login to TWS will occur automatically in 289 | # read-only mode: in this mode, placing or managing orders is not 290 | # allowed. If set to 'no', and the user is enrolled in IB's account 291 | # security programme, the user must supply the relevant security 292 | # code to complete the login. If the user is not enrolled in IB's 293 | # account security programme, this setting is ignored. The default 294 | # is 'no'. 295 | 296 | ReadOnlyLogin=no 297 | 298 | 299 | # If ReadOnlyApi is set to 'yes', API programs cannot submit, modify 300 | # or cancel orders. If set to 'no', API programs can do these things. 301 | # If not set, the existing TWS/Gateway configuration is unchanged. 302 | # NB: this setting is really only supplied for the benefit of new TWS 303 | # or Gateway instances that are being automatically installed and 304 | # started without user intervention (eg Docker containers). Where 305 | # a user is involved, they should use the Global Configuration to 306 | # set the relevant checkbox (this only needs to be done once) and 307 | # not provide a value for this setting. 308 | 309 | ReadOnlyApi=no 310 | 311 | # Logging in to a paper-trading account results in TWS displaying 312 | # a dialog asking the user to confirm that they are aware that this 313 | # is not a brokerage account. Until this dialog has been accepted, 314 | # TWS will not allow API connections to succeed. Setting this 315 | # to 'yes' (the default) will cause IBC to automatically 316 | # confirm acceptance. Setting it to 'no' will leave the dialog 317 | # on display, and the user will have to deal with it manually. 318 | 319 | AcceptNonBrokerageAccountWarning=yes 320 | 321 | 322 | # 4. TWS Auto-Closedown 323 | # ----------------------- 324 | # 325 | # Set to yes or no (lower case). Only affects TWS, not the Gateway. 326 | # 327 | # yes means allow TWS to shut down automatically at its 328 | # specified shutdown time, which is set via the TWS 329 | # configuration menu. 330 | # 331 | # no means TWS never shuts down automatically. 332 | # 333 | # NB: IB recommends that you do not keep TWS running 334 | # continuously. If you set this setting to 'no', you may 335 | # experience incorrect TWS operation. 336 | # 337 | # Starting with TWS 974, this setting no longer works properly, 338 | # because IB have changed the way TWS handles its autologoff 339 | # mechanism. You should now configure the TWS autologoff time 340 | # to something convenient for you, and restart IBC each day. 341 | # Alternatively, discontinue use of IBC and use the autorestart 342 | # mechanism within TWS 974 and later versions. 343 | 344 | IbAutoClosedown=yes 345 | 346 | 347 | 348 | # 5. TWS Tidy Closedown Time 349 | # ---------------------------- 350 | # 351 | # NB: starting with TWS 974 this is no longer a useful option 352 | # for TWS, because of changes in TWS 974's autologoff handling. 353 | # However it can still be useful with the Gateway. 354 | # 355 | # To tell IBC to tidily close TWS at a specified time every 356 | # day, set this value to , for example: 357 | # ClosedownAt=22:00 358 | # 359 | # To tell IBC to tidily close TWS at a specified day and time 360 | # each week, set this value to , for example: 361 | # ClosedownAt=Friday 22:00 362 | # 363 | # Note that the day of the week must be specified using your 364 | # default locale. Also note that Java will only accept 365 | # characters encoded to ISO 8859-1 (Latin-1). This means that 366 | # if the day name in your default locale uses any non-Latin-1 367 | # characters you need to encode them using Unicode escapes 368 | # (see http://java.sun.com/docs/books/jls/third_edition/html/lexical.html#3.3 369 | # for details). For example, to tidily close TWS at 12:00 on 370 | # Saturday where the default locale is Simplified Chinese, 371 | # use the following: 372 | # #ClosedownAt=\u661F\u671F\u516D 12:00 373 | 374 | ClosedownAt= 375 | 376 | 377 | 378 | # 6. Other TWS Settings 379 | # ----------------------- 380 | # 381 | # If you attempt to place an order for a contract for which 382 | # you have no market data subscription, TWS displays a dialog 383 | # to warn you against such blind trading. 384 | # 385 | # yes means the dialog is dismissed as though the user had 386 | # clicked the 'Ok' button: this means that you accept 387 | # the risk and want the order to be submitted. 388 | # 389 | # no means the dialog remains on display and must be 390 | # handled by the user. 391 | 392 | AllowBlindTrading=yes 393 | 394 | 395 | # Indian versions of TWS may display a password expiry 396 | # notification dialog and a NSE Compliance dialog. These can be 397 | # dismissed by setting the following to yes. By default the 398 | # password expiry notice is not dismissed, but the NSE Compliance 399 | # notice is dismissed. 400 | 401 | # Warning: setting DismissPasswordExpiryWarning=yes will mean 402 | # you will not be notified when your password is about to expire. 403 | # You must then take other measures to ensure that your password 404 | # is changed within the expiry period, otherwise IBC will 405 | # not be able to login successfully. 406 | 407 | DismissPasswordExpiryWarning=no 408 | DismissNSEComplianceNotice=yes 409 | 410 | 411 | # Since TWS 906, orders placed using the BookTrader have to be confirmed 412 | # via a dialog box (this was due to a legal dispute between IB and Trading 413 | # Technologies who claimed IB were infringing a patent). If 414 | # AutoConfirmOrders=yes, then when orders are placed using the BookTrader, 415 | # the confirmation dialog is automatically handled, thereby effectively 416 | # restoring the one-click trading. The default is 'no', requiring the user 417 | # to manually confirm each trade. 418 | # 419 | # NB: this setting has been removed as the dispute has been resolved and 420 | # TWS users now have the option to request the order confirmation dialog 421 | # to not be displayed. 422 | # 423 | AutoConfirmOrders=yes 424 | 425 | 426 | # You can tell TWS to automatically save its settings on a schedule 427 | # of your choosing. You can specify one or more specific times, 428 | # like this: 429 | # 430 | # SaveTwsSettingsAt=HH:MM [ HH:MM]... 431 | # 432 | # for example: 433 | # SaveTwsSettingsAt=08:00 12:30 17:30 434 | # 435 | # Or you can specify an interval at which settings are to be saved, 436 | # optionally starting at a specific time and continuing until another 437 | # time, like this: 438 | # 439 | #SaveTwsSettingsAt=Every n [{mins | hours}] [hh:mm] [hh:mm] 440 | # 441 | # where the first hh:mm is the start time and the second is the end 442 | # time. If you don't specify the end time, settings are saved regularly 443 | # from the start time till midnight. If you don't specify the start time. 444 | # settings are saved regularly all day, beginning at 00:00. Note that 445 | # settings will always be saved at the end time, even if that is not 446 | # exactly one interval later than the previous time. If neither 'mins' 447 | # nor 'hours' is specified, 'mins' is assumed. Examples: 448 | # 449 | # To save every 30 minutes all day starting at 00:00 450 | #SaveTwsSettingsAt=Every 30 451 | #SaveTwsSettingsAt=Every 30 mins 452 | # 453 | # To save every hour starting at 08:00 and ending at midnight 454 | #SaveTwsSettingsAt=Every 1 hours 08:00 455 | #SaveTwsSettingsAt=Every 1 hours 08:00 00:00 456 | # 457 | # To save every 90 minutes starting at 08:00 up to and including 17:43 458 | #SaveTwsSettingsAt=Every 90 08:00 17:43 459 | 460 | SaveTwsSettingsAt= 461 | 462 | 463 | 464 | # 6. IBC Command Server Settings 465 | # -------------------------------- 466 | # 467 | # Do NOT CHANGE THE FOLLOWING SETTINGS unless you 468 | # intend to issue commands to IBC (for example 469 | # using telnet). Note that these settings have nothing to 470 | # do with running programs that use the TWS API. 471 | 472 | # The port that IBC listens on for commands 473 | # such as "STOP". DO NOT set this to the port number 474 | # used for TWS API connections. There is no good reason 475 | # to change this setting unless the port is used by 476 | # some other application (typically another instance of 477 | # IBC). The default value is 0, which tells IBC not to 478 | # start the command server 479 | 480 | #CommandServerPort=7462 481 | 482 | 483 | # A comma separated list of ip addresses, or host names, 484 | # which are allowed addresses for sending commands to 485 | # IBC. Commands can always be sent from the 486 | # same host as IBC is running on. 487 | 488 | ControlFrom= 489 | 490 | 491 | # Specifies the IP address on which the Command Server 492 | # is to listen. For a multi-homed host, this can be used 493 | # to specify that connection requests are only to be 494 | # accepted on the specified address. The default is to 495 | # accept connection requests on all local addresses. 496 | 497 | BindAddress= 498 | 499 | 500 | # The specified string is output by the server when 501 | # the connection is first opened and after the completion 502 | # of each command. This can be useful if sending commands 503 | # using an interactive program such as telnet. The default 504 | # is that no prompt is output. 505 | # For example: 506 | # 507 | # CommandPrompt=> 508 | 509 | CommandPrompt= 510 | 511 | 512 | # Some commands can return intermediate information about 513 | # their progress. This setting controls whether such 514 | # information is sent. The default is that such information 515 | # is not sent. 516 | 517 | SuppressInfoMessages=yes 518 | 519 | 520 | 521 | # 7. Diagnostic Settings 522 | # ------------------------ 523 | 524 | # If LogComponents is set to 'open' or 'yes' or 'true', 525 | # IBC logs information about the structure of each 526 | # TWS window it detects the first time it is encountered. If 527 | # set to 'activate', the information is logged every time 528 | # a TWS window is made active. If set to 'never' or 'no' or 529 | # 'false', this information is never logged. The default is 530 | # 'never'. 531 | # 532 | # The logged information shows the hierarchical organisation 533 | # of all the components of the window, and includes the 534 | # current values of text boxes and labels. 535 | # 536 | # Note that when set to 'open', 'activate' or 'yes', there is 537 | # a small performance impact due to the extra logging. Also 538 | # logfile size may be significantly increased, especially if 539 | # set to 'activate' and the user is actively using TWS. It 540 | # is therefore recommended that the setting be left at 'no' 541 | # unless there is a specific reason that this information is 542 | # needed. 543 | 544 | LogComponents=never 545 | -------------------------------------------------------------------------------- /thetagang/portfolio_manager.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import click 4 | from ib_insync import util 5 | from ib_insync.contract import ComboLeg, Contract, Option, Stock, TagValue 6 | from ib_insync.order import LimitOrder, Order 7 | 8 | from thetagang.util import ( 9 | account_summary_to_dict, 10 | count_option_positions, 11 | justify, 12 | portfolio_positions_to_dict, 13 | position_pnl, 14 | ) 15 | 16 | from .options import option_dte 17 | 18 | 19 | class PortfolioManager: 20 | def __init__(self, config, ib, completion_future): 21 | self.config = config 22 | self.ib = ib 23 | self.completion_future = completion_future 24 | 25 | def get_calls(self, portfolio_positions): 26 | return self.get_options(portfolio_positions, "C") 27 | 28 | def get_puts(self, portfolio_positions): 29 | return self.get_options(portfolio_positions, "P") 30 | 31 | def get_options(self, portfolio_positions, right): 32 | r = [] 33 | for symbol in portfolio_positions: 34 | r = r + list( 35 | filter( 36 | lambda p: ( 37 | isinstance(p.contract, Option) 38 | and p.contract.right.startswith(right) 39 | ), 40 | portfolio_positions[symbol], 41 | ) 42 | ) 43 | 44 | return r 45 | 46 | def put_is_itm(self, contract): 47 | stock = Stock(contract.symbol, "SMART", currency="USD") 48 | [ticker] = self.ib.reqTickers(stock) 49 | return contract.strike >= ticker.marketPrice() 50 | 51 | def put_can_be_rolled(self, put): 52 | # Check if this put is ITM, and if it's o.k. to roll 53 | if ( 54 | "puts" not in self.config["roll_when"] 55 | or not self.config["roll_when"]["puts"]["itm"] 56 | ) and self.put_is_itm(put.contract): 57 | return False 58 | 59 | dte = option_dte(put.contract.lastTradeDateOrContractMonth) 60 | pnl = position_pnl(put) 61 | 62 | if dte <= self.config["roll_when"]["dte"]: 63 | click.secho( 64 | f" {put.contract.localSymbol} can be rolled because DTE of {dte} is <= {self.config['roll_when']['dte']}", 65 | fg="blue", 66 | ) 67 | return True 68 | 69 | if pnl >= self.config["roll_when"]["pnl"]: 70 | click.secho( 71 | f" {put.contract.localSymbol} can be rolled because P&L of {round(pnl * 100, 1)}% is >= {round(self.config['roll_when']['pnl'] * 100, 1)}", 72 | fg="blue", 73 | ) 74 | return True 75 | 76 | return False 77 | 78 | def call_is_itm(self, contract): 79 | stock = Stock(contract.symbol, "SMART", currency="USD") 80 | [ticker] = self.ib.reqTickers(stock) 81 | return contract.strike <= ticker.marketPrice() 82 | 83 | def call_can_be_rolled(self, call): 84 | # Check if this call is ITM, and it's o.k. to roll 85 | if ( 86 | "calls" in self.config["roll_when"] 87 | and not self.config["roll_when"]["calls"]["itm"] 88 | ) and self.call_is_itm(call.contract): 89 | return False 90 | 91 | dte = option_dte(call.contract.lastTradeDateOrContractMonth) 92 | pnl = position_pnl(call) 93 | 94 | if dte <= self.config["roll_when"]["dte"]: 95 | click.secho( 96 | f"{call.contract.localSymbol} can be rolled because DTE of {dte} is <= {self.config['roll_when']['dte']}", 97 | fg="blue", 98 | ) 99 | return True 100 | 101 | if pnl >= self.config["roll_when"]["pnl"]: 102 | click.secho( 103 | f"{call.contract.localSymbol} can be rolled because P&L of {round(pnl * 100, 1)}% is >= {round(self.config['roll_when']['pnl'] * 100, 1)}", 104 | fg="blue", 105 | ) 106 | return True 107 | 108 | return False 109 | 110 | def filter_positions(self, portfolio_positions): 111 | keys = portfolio_positions.keys() 112 | for k in keys: 113 | if k not in self.config["symbols"]: 114 | del portfolio_positions[k] 115 | return portfolio_positions 116 | 117 | def get_portfolio_positions(self): 118 | portfolio_positions = self.ib.portfolio() 119 | # Filter out any positions we don't care about, i.e., we don't know the 120 | # symbol or it's not in the desired account. 121 | portfolio_positions = [ 122 | item 123 | for item in portfolio_positions 124 | if item.account == self.config["account"]["number"] 125 | and item.contract.symbol in self.config["symbols"] 126 | ] 127 | return portfolio_positions_to_dict(portfolio_positions) 128 | 129 | def initialize_account(self): 130 | self.ib.reqMarketDataType(self.config["account"]["market_data_type"]) 131 | 132 | if self.config["account"]["cancel_orders"]: 133 | # Cancel any existing orders 134 | open_trades = self.ib.openTrades() 135 | for trade in open_trades: 136 | if trade.isActive() and trade.contract.symbol in self.config["symbols"]: 137 | click.secho(f"Canceling order {trade.order}", fg="red") 138 | self.ib.cancelOrder(trade.order) 139 | 140 | def summarize_account(self): 141 | account_summary = self.ib.accountSummary(self.config["account"]["number"]) 142 | click.echo() 143 | click.secho(f"Account summary:", fg="green") 144 | click.echo() 145 | account_summary = account_summary_to_dict(account_summary) 146 | 147 | click.secho( 148 | f" Excess liquidity = {justify(account_summary['ExcessLiquidity'].value)}", 149 | fg="cyan", 150 | ) 151 | click.secho( 152 | f" Net liquidation = {justify(account_summary['NetLiquidation'].value)}", 153 | fg="cyan", 154 | ) 155 | click.secho( 156 | f" Cushion = {account_summary['Cushion'].value} ({round(float(account_summary['Cushion'].value) * 100, 1)}%)", 157 | fg="cyan", 158 | ) 159 | click.secho( 160 | f" Full maint margin = {justify(account_summary['FullMaintMarginReq'].value)}", 161 | fg="cyan", 162 | ) 163 | click.secho( 164 | f" Buying power = {justify(account_summary['BuyingPower'].value)}", 165 | fg="cyan", 166 | ) 167 | click.secho( 168 | f" Total cash value = {justify(account_summary['TotalCashValue'].value)}", 169 | fg="cyan", 170 | ) 171 | 172 | portfolio_positions = self.get_portfolio_positions() 173 | 174 | click.echo() 175 | click.secho("Portfolio positions:", fg="green") 176 | click.echo() 177 | for symbol in portfolio_positions.keys(): 178 | click.secho(f" {symbol}:", fg="cyan") 179 | for p in portfolio_positions[symbol]: 180 | if isinstance(p.contract, Stock): 181 | pnl = round(position_pnl(p) * 100, 2) 182 | click.secho( 183 | f" Stock Qty={int(p.position)} Price={round(p.marketPrice,2)} Value={round(p.marketValue,2)} AvgCost={round(p.averageCost,2)} P&L={pnl}%", 184 | fg="cyan", 185 | ) 186 | elif isinstance(p.contract, Option): 187 | pnl = round(position_pnl(p) * 100, 2) 188 | 189 | def p_or_c(p): 190 | return "Call" if p.contract.right.startswith("C") else "Put " 191 | 192 | click.secho( 193 | f" {p_or_c(p)} Qty={int(p.position)} Price={round(p.marketPrice,2)} Value={round(p.marketValue,2)} AvgCost={round(p.averageCost,2)} P&L={pnl}% Strike={p.contract.strike} Exp={p.contract.lastTradeDateOrContractMonth}", 194 | fg="cyan", 195 | ) 196 | else: 197 | click.secho(f" {p.contract}", fg="cyan") 198 | 199 | return (account_summary, portfolio_positions) 200 | 201 | def manage(self): 202 | try: 203 | self.initialize_account() 204 | (account_summary, portfolio_positions) = self.summarize_account() 205 | 206 | click.echo() 207 | click.secho("Checking positions...", fg="green") 208 | click.echo() 209 | 210 | portfolio_positions = self.filter_positions(portfolio_positions) 211 | 212 | self.check_puts(portfolio_positions) 213 | self.check_calls(portfolio_positions) 214 | 215 | # Look for lots of stock that don't have covered calls 216 | self.check_for_uncovered_positions(portfolio_positions) 217 | 218 | # Refresh positions, in case anything changed from the ordering above 219 | portfolio_positions = self.get_portfolio_positions() 220 | 221 | # Check if we have enough buying power to write some puts 222 | self.check_if_can_write_puts(account_summary, portfolio_positions) 223 | 224 | click.echo() 225 | click.secho("ThetaGang is done, shutting down! Cya next time.", fg="yellow") 226 | click.echo() 227 | 228 | except: 229 | click.secho("An exception was raised, exiting", fg="red") 230 | click.secho("Check log for details", fg="red") 231 | raise 232 | 233 | finally: 234 | # Shut it down 235 | self.completion_future.set_result(True) 236 | 237 | def check_puts(self, portfolio_positions): 238 | # Check for puts which may be rolled to the next expiration or a better price 239 | puts = self.get_puts(portfolio_positions) 240 | 241 | # find puts eligible to be rolled 242 | rollable_puts = list(filter(lambda p: self.put_can_be_rolled(p), puts)) 243 | 244 | total_rollable_puts = math.floor(sum([abs(p.position) for p in rollable_puts])) 245 | 246 | click.echo() 247 | click.secho(f"{total_rollable_puts} puts will be rolled", fg="magenta") 248 | click.echo() 249 | 250 | self.roll_puts(rollable_puts) 251 | 252 | def check_calls(self, portfolio_positions): 253 | # Check for calls which may be rolled to the next expiration or a better price 254 | calls = self.get_calls(portfolio_positions) 255 | 256 | # find calls eligible to be rolled 257 | rollable_calls = list(filter(lambda p: self.call_can_be_rolled(p), calls)) 258 | total_rollable_calls = math.floor( 259 | sum([abs(p.position) for p in rollable_calls]) 260 | ) 261 | 262 | click.echo() 263 | click.secho(f"{total_rollable_calls} calls will be rolled", fg="magenta") 264 | click.echo() 265 | 266 | self.roll_calls(rollable_calls) 267 | 268 | def check_for_uncovered_positions(self, portfolio_positions): 269 | for symbol in portfolio_positions: 270 | call_count = count_option_positions(symbol, portfolio_positions, "C") 271 | stock_count = math.floor( 272 | sum( 273 | [ 274 | p.position 275 | for p in portfolio_positions[symbol] 276 | if isinstance(p.contract, Stock) 277 | ] 278 | ) 279 | ) 280 | 281 | target_calls = stock_count // 100 282 | 283 | calls_to_write = target_calls - call_count 284 | 285 | if calls_to_write > 0: 286 | click.secho(f"Need to write {calls_to_write} for {symbol}", fg="green") 287 | self.write_calls(symbol, calls_to_write) 288 | 289 | def wait_for_trade_submitted(self, trade): 290 | while trade.orderStatus.status not in [ 291 | "Submitted", 292 | "Filled", 293 | "ApiCancelled", 294 | "Cancelled", 295 | ]: 296 | self.ib.waitOnUpdate(timeout=2) 297 | return trade 298 | 299 | def write_calls(self, symbol, quantity): 300 | sell_ticker = self.find_eligible_contracts(symbol, "C") 301 | 302 | # Create order 303 | order = LimitOrder( 304 | "SELL", 305 | quantity, 306 | round(sell_ticker.marketPrice(), 2), 307 | algoStrategy="Adaptive", 308 | algoParams=[TagValue("adaptivePriority", "Patient")], 309 | tif="DAY", 310 | ) 311 | 312 | # Submit order 313 | trade = self.wait_for_trade_submitted( 314 | self.ib.placeOrder(sell_ticker.contract, order) 315 | ) 316 | click.echo() 317 | click.secho("Order submitted", fg="green") 318 | click.secho(f"{trade}", fg="green") 319 | 320 | def write_puts(self, symbol, quantity): 321 | sell_ticker = self.find_eligible_contracts(symbol, "P") 322 | 323 | # Create order 324 | order = LimitOrder( 325 | "SELL", 326 | quantity, 327 | round(sell_ticker.marketPrice(), 2), 328 | algoStrategy="Adaptive", 329 | algoParams=[TagValue("adaptivePriority", "Patient")], 330 | tif="DAY", 331 | ) 332 | 333 | # Submit order 334 | trade = self.wait_for_trade_submitted( 335 | self.ib.placeOrder(sell_ticker.contract, order) 336 | ) 337 | click.echo() 338 | click.secho("Order submitted", fg="green") 339 | click.secho(f"{trade}", fg="green") 340 | 341 | def check_if_can_write_puts(self, account_summary, portfolio_positions): 342 | # Get stock positions 343 | stock_positions = [ 344 | position 345 | for symbol in portfolio_positions 346 | for position in portfolio_positions[symbol] 347 | if isinstance(position.contract, Stock) 348 | ] 349 | 350 | total_buying_power = math.floor( 351 | float(account_summary["NetLiquidation"].value) 352 | * self.config["account"]["margin_usage"] 353 | ) 354 | 355 | click.echo() 356 | click.secho( 357 | f"Total buying power: ${total_buying_power} at {round(self.config['account']['margin_usage'] * 100, 1)}% margin usage", 358 | fg="green", 359 | ) 360 | 361 | # Sum stock values that we care about 362 | total_value = ( 363 | sum([stock.marketValue for stock in stock_positions]) + total_buying_power 364 | ) 365 | click.secho(f"Total value: ${total_value}", fg="green") 366 | click.echo() 367 | 368 | stock_symbols = dict() 369 | for stock in stock_positions: 370 | symbol = stock.contract.symbol 371 | stock_symbols[symbol] = stock 372 | 373 | targets = dict() 374 | target_additional_quantity = dict() 375 | 376 | # Determine target quantity of each stock 377 | for symbol in self.config["symbols"].keys(): 378 | click.secho(f" {symbol}", fg="green") 379 | stock = Stock(symbol, "SMART", currency="USD") 380 | [ticker] = self.ib.reqTickers(stock) 381 | 382 | current_position = math.floor( 383 | stock_symbols[symbol].position if symbol in stock_symbols else 0 384 | ) 385 | click.secho( 386 | f" Current position quantity {current_position}", 387 | fg="cyan", 388 | ) 389 | 390 | targets[symbol] = round( 391 | self.config["symbols"][symbol]["weight"] * total_value, 2 392 | ) 393 | click.secho(f" Target value ${targets[symbol]}", fg="cyan") 394 | target_quantity = math.floor(targets[symbol] / ticker.marketPrice()) 395 | click.secho(f" Target quantity {target_quantity}", fg="cyan") 396 | 397 | target_additional_quantity[symbol] = math.floor( 398 | target_quantity - current_position 399 | ) 400 | 401 | click.secho( 402 | f" Target additional quantity (excl. existing options) {target_additional_quantity[symbol]}", 403 | fg="cyan", 404 | ) 405 | 406 | click.echo() 407 | 408 | # Figure out how many addition puts are needed, if they're needed 409 | for symbol in target_additional_quantity.keys(): 410 | additional_quantity = target_additional_quantity[symbol] 411 | # NOTE: it's possible there are non-standard option contract sizes, 412 | # like with futures, but we don't bother handling those cases. 413 | # Please don't use this code with futures. 414 | if additional_quantity >= 100: 415 | put_count = count_option_positions(symbol, portfolio_positions, "P") 416 | target_put_count = additional_quantity // 100 417 | puts_to_write = target_put_count - put_count 418 | if puts_to_write > 0: 419 | click.secho( 420 | f"Preparing to write additional {puts_to_write} puts to purchase {symbol}", 421 | fg="cyan", 422 | ) 423 | self.write_puts(symbol, puts_to_write) 424 | 425 | return 426 | 427 | def roll_puts(self, puts): 428 | return self.roll_positions(puts, "P") 429 | 430 | def roll_calls(self, calls): 431 | return self.roll_positions(calls, "C") 432 | 433 | def roll_positions(self, positions, right): 434 | for position in positions: 435 | symbol = position.contract.symbol 436 | sell_ticker = self.find_eligible_contracts(symbol, right) 437 | quantity = abs(position.position) 438 | 439 | position.contract.exchange = "SMART" 440 | [buy_ticker] = self.ib.reqTickers(position.contract) 441 | 442 | price = buy_ticker.marketPrice() - sell_ticker.marketPrice() 443 | 444 | # Create combo legs 445 | comboLegs = [ 446 | ComboLeg( 447 | conId=position.contract.conId, 448 | ratio=1, 449 | exchange="SMART", 450 | action="BUY", 451 | ), 452 | ComboLeg( 453 | conId=sell_ticker.contract.conId, 454 | ratio=1, 455 | exchange="SMART", 456 | action="SELL", 457 | ), 458 | ] 459 | 460 | # Create contract 461 | combo = Contract( 462 | secType="BAG", 463 | symbol=symbol, 464 | currency="USD", 465 | exchange="SMART", 466 | comboLegs=comboLegs, 467 | ) 468 | 469 | # Create order 470 | order = LimitOrder( 471 | "BUY", 472 | quantity, 473 | round(price, 2), 474 | algoStrategy="Adaptive", 475 | algoParams=[TagValue("adaptivePriority", "Patient")], 476 | tif="DAY", 477 | ) 478 | 479 | # Submit order 480 | trade = self.wait_for_trade_submitted(self.ib.placeOrder(combo, order)) 481 | click.secho("Order submitted", fg="green") 482 | click.secho(f"{trade}", fg="green") 483 | 484 | def find_eligible_contracts(self, symbol, right): 485 | click.echo() 486 | click.secho( 487 | f"Searching option chain for symbol={symbol} right={right}, this can take a while...", 488 | fg="green", 489 | ) 490 | click.echo() 491 | stock = Stock(symbol, "SMART", currency="USD") 492 | contracts = self.ib.qualifyContracts(stock) 493 | 494 | [ticker] = self.ib.reqTickers(stock) 495 | tickerValue = ticker.marketPrice() 496 | 497 | chains = self.ib.reqSecDefOptParams( 498 | stock.symbol, "", stock.secType, stock.conId 499 | ) 500 | chain = next(c for c in chains if c.exchange == "SMART") 501 | 502 | def valid_strike(strike): 503 | if right.startswith("P"): 504 | return strike <= tickerValue 505 | if right.startswith("C"): 506 | return strike >= tickerValue 507 | return False 508 | 509 | chain_expirations = self.config["option_chains"]["expirations"] 510 | 511 | strikes = sorted(strike for strike in chain.strikes if valid_strike(strike)) 512 | expirations = sorted( 513 | exp 514 | for exp in chain.expirations 515 | if option_dte(exp) >= self.config["target"]["dte"] 516 | )[:chain_expirations] 517 | rights = [right] 518 | 519 | def nearest_strikes(strikes): 520 | chain_strikes = self.config["option_chains"]["strikes"] 521 | if right.startswith("P"): 522 | return strikes[-chain_strikes:] 523 | if right.startswith("C"): 524 | return strikes[:chain_strikes] 525 | 526 | contracts = [ 527 | Option( 528 | symbol, 529 | expiration, 530 | strike, 531 | right, 532 | "SMART", 533 | tradingClass=chain.tradingClass, 534 | ) 535 | for right in rights 536 | for expiration in expirations 537 | for strike in nearest_strikes(strikes) 538 | ] 539 | 540 | contracts = self.ib.qualifyContracts(*contracts) 541 | 542 | tickers = self.ib.reqTickers(*contracts) 543 | 544 | def open_interest_is_valid(ticker): 545 | ticker = self.ib.reqMktData(ticker.contract, genericTickList="101") 546 | 547 | def open_interest_is_not_ready(): 548 | if right.startswith("P"): 549 | return util.isNan(ticker.putOpenInterest) 550 | if right.startswith("C"): 551 | return util.isNan(ticker.callOpenInterest) 552 | 553 | while open_interest_is_not_ready(): 554 | self.ib.waitOnUpdate(timeout=2) 555 | 556 | self.ib.cancelMktData(ticker.contract) 557 | 558 | # The open interest value is never present when using historical 559 | # data, so just ignore it when the value is None 560 | if right.startswith("P"): 561 | return ( 562 | ticker.putOpenInterest 563 | >= self.config["target"]["minimum_open_interest"] 564 | ) 565 | if right.startswith("C"): 566 | return ( 567 | ticker.callOpenInterest 568 | >= self.config["target"]["minimum_open_interest"] 569 | ) 570 | 571 | def delta_is_valid(ticker): 572 | return ( 573 | ticker.modelGreeks 574 | and ticker.modelGreeks.delta 575 | and abs(ticker.modelGreeks.delta) <= self.config["target"]["delta"] 576 | ) 577 | 578 | # Filter by delta and open interest 579 | tickers = [ticker for ticker in tickers if delta_is_valid(ticker)] 580 | tickers = [ticker for ticker in tickers if open_interest_is_valid(ticker)] 581 | tickers = sorted( 582 | reversed(sorted(tickers, key=lambda t: abs(t.modelGreeks.delta))), 583 | key=lambda t: option_dte(t.contract.lastTradeDateOrContractMonth), 584 | ) 585 | 586 | if len(tickers) == 0: 587 | raise RuntimeError(f"No valid contracts found for {symbol}. Aborting.") 588 | 589 | # Return the first result 590 | return tickers[0] 591 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "astroid" 11 | version = "2.4.2" 12 | description = "An abstract syntax tree for Python with inference support." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=3.5" 16 | 17 | [package.dependencies] 18 | lazy-object-proxy = ">=1.4.0,<1.5.0" 19 | six = ">=1.12,<2.0" 20 | wrapt = ">=1.11,<2.0" 21 | 22 | [[package]] 23 | name = "atomicwrites" 24 | version = "1.4.0" 25 | description = "Atomic file writes." 26 | category = "dev" 27 | optional = false 28 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 29 | 30 | [[package]] 31 | name = "attrs" 32 | version = "20.3.0" 33 | description = "Classes Without Boilerplate" 34 | category = "dev" 35 | optional = false 36 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 37 | 38 | [package.extras] 39 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] 40 | docs = ["furo", "sphinx", "zope.interface"] 41 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 42 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 43 | 44 | [[package]] 45 | name = "autoflake" 46 | version = "1.4" 47 | description = "Removes unused imports and unused variables" 48 | category = "dev" 49 | optional = false 50 | python-versions = "*" 51 | 52 | [package.dependencies] 53 | pyflakes = ">=1.1.0" 54 | 55 | [[package]] 56 | name = "autohooks" 57 | version = "2.2.0" 58 | description = "Library for managing git hooks" 59 | category = "dev" 60 | optional = false 61 | python-versions = ">=3.5,<4.0" 62 | 63 | [package.dependencies] 64 | colorful = ">=0.5.4,<0.6.0" 65 | packaging = ">=20.3,<21.0" 66 | tomlkit = ">=0.5.11,<0.6.0" 67 | 68 | [[package]] 69 | name = "autohooks-plugin-black" 70 | version = "1.2.0" 71 | description = "Autohooks plugin for code formatting via black" 72 | category = "dev" 73 | optional = false 74 | python-versions = ">=3.5" 75 | 76 | [package.dependencies] 77 | autohooks = ">=1.1" 78 | black = "*" 79 | 80 | [[package]] 81 | name = "autohooks-plugin-isort" 82 | version = "1.0.0" 83 | description = "Autohooks plugin for include sorting via isort" 84 | category = "dev" 85 | optional = false 86 | python-versions = ">=3.5" 87 | 88 | [package.dependencies] 89 | autohooks = ">=1.1" 90 | isort = "*" 91 | 92 | [[package]] 93 | name = "black" 94 | version = "20.8b1" 95 | description = "The uncompromising code formatter." 96 | category = "dev" 97 | optional = false 98 | python-versions = ">=3.6" 99 | 100 | [package.dependencies] 101 | appdirs = "*" 102 | click = ">=7.1.2" 103 | mypy-extensions = ">=0.4.3" 104 | pathspec = ">=0.6,<1" 105 | regex = ">=2020.1.8" 106 | toml = ">=0.10.1" 107 | typed-ast = ">=1.4.0" 108 | typing-extensions = ">=3.7.4" 109 | 110 | [package.extras] 111 | colorama = ["colorama (>=0.4.3)"] 112 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 113 | 114 | [[package]] 115 | name = "click" 116 | version = "7.1.2" 117 | description = "Composable command line interface toolkit" 118 | category = "main" 119 | optional = false 120 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 121 | 122 | [[package]] 123 | name = "click-log" 124 | version = "0.3.2" 125 | description = "Logging integration for Click" 126 | category = "main" 127 | optional = false 128 | python-versions = "*" 129 | 130 | [package.dependencies] 131 | click = "*" 132 | 133 | [[package]] 134 | name = "colorama" 135 | version = "0.4.4" 136 | description = "Cross-platform colored terminal text." 137 | category = "main" 138 | optional = false 139 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 140 | 141 | [[package]] 142 | name = "colorful" 143 | version = "0.5.4" 144 | description = "Terminal string styling done right, in Python." 145 | category = "dev" 146 | optional = false 147 | python-versions = "*" 148 | 149 | [package.dependencies] 150 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 151 | 152 | [[package]] 153 | name = "contextlib2" 154 | version = "0.6.0.post1" 155 | description = "Backports and enhancements for the contextlib module" 156 | category = "main" 157 | optional = false 158 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 159 | 160 | [[package]] 161 | name = "eventkit" 162 | version = "0.8.6" 163 | description = "Event-driven data pipelines" 164 | category = "main" 165 | optional = false 166 | python-versions = "*" 167 | 168 | [package.dependencies] 169 | numpy = "*" 170 | 171 | [[package]] 172 | name = "ib-insync" 173 | version = "0.9.64" 174 | description = "Python sync/async framework for Interactive Brokers API" 175 | category = "main" 176 | optional = false 177 | python-versions = ">=3.6" 178 | 179 | [package.dependencies] 180 | eventkit = "*" 181 | nest-asyncio = "*" 182 | 183 | [[package]] 184 | name = "iniconfig" 185 | version = "1.1.1" 186 | description = "iniconfig: brain-dead simple config-ini parsing" 187 | category = "dev" 188 | optional = false 189 | python-versions = "*" 190 | 191 | [[package]] 192 | name = "isort" 193 | version = "5.6.4" 194 | description = "A Python utility / library to sort Python imports." 195 | category = "dev" 196 | optional = false 197 | python-versions = ">=3.6,<4.0" 198 | 199 | [package.extras] 200 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 201 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 202 | colors = ["colorama (>=0.4.3,<0.5.0)"] 203 | 204 | [[package]] 205 | name = "lazy-object-proxy" 206 | version = "1.4.3" 207 | description = "A fast and thorough lazy object proxy." 208 | category = "dev" 209 | optional = false 210 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 211 | 212 | [[package]] 213 | name = "mccabe" 214 | version = "0.6.1" 215 | description = "McCabe checker, plugin for flake8" 216 | category = "dev" 217 | optional = false 218 | python-versions = "*" 219 | 220 | [[package]] 221 | name = "mypy-extensions" 222 | version = "0.4.3" 223 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 224 | category = "dev" 225 | optional = false 226 | python-versions = "*" 227 | 228 | [[package]] 229 | name = "nest-asyncio" 230 | version = "1.4.3" 231 | description = "Patch asyncio to allow nested event loops" 232 | category = "main" 233 | optional = false 234 | python-versions = ">=3.5" 235 | 236 | [[package]] 237 | name = "numpy" 238 | version = "1.19.4" 239 | description = "NumPy is the fundamental package for array computing with Python." 240 | category = "main" 241 | optional = false 242 | python-versions = ">=3.6" 243 | 244 | [[package]] 245 | name = "packaging" 246 | version = "20.8" 247 | description = "Core utilities for Python packages" 248 | category = "dev" 249 | optional = false 250 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 251 | 252 | [package.dependencies] 253 | pyparsing = ">=2.0.2" 254 | 255 | [[package]] 256 | name = "pandas" 257 | version = "1.1.5" 258 | description = "Powerful data structures for data analysis, time series, and statistics" 259 | category = "main" 260 | optional = false 261 | python-versions = ">=3.6.1" 262 | 263 | [package.dependencies] 264 | numpy = ">=1.15.4" 265 | python-dateutil = ">=2.7.3" 266 | pytz = ">=2017.2" 267 | 268 | [package.extras] 269 | test = ["pytest (>=4.0.2)", "pytest-xdist", "hypothesis (>=3.58)"] 270 | 271 | [[package]] 272 | name = "pathspec" 273 | version = "0.8.1" 274 | description = "Utility library for gitignore style pattern matching of file paths." 275 | category = "dev" 276 | optional = false 277 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 278 | 279 | [[package]] 280 | name = "pluggy" 281 | version = "0.13.1" 282 | description = "plugin and hook calling mechanisms for python" 283 | category = "dev" 284 | optional = false 285 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 286 | 287 | [package.extras] 288 | dev = ["pre-commit", "tox"] 289 | 290 | [[package]] 291 | name = "py" 292 | version = "1.10.0" 293 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 294 | category = "dev" 295 | optional = false 296 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 297 | 298 | [[package]] 299 | name = "pyflakes" 300 | version = "2.2.0" 301 | description = "passive checker of Python programs" 302 | category = "dev" 303 | optional = false 304 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 305 | 306 | [[package]] 307 | name = "pylint" 308 | version = "2.6.0" 309 | description = "python code static checker" 310 | category = "dev" 311 | optional = false 312 | python-versions = ">=3.5.*" 313 | 314 | [package.dependencies] 315 | astroid = ">=2.4.0,<=2.5" 316 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 317 | isort = ">=4.2.5,<6" 318 | mccabe = ">=0.6,<0.7" 319 | toml = ">=0.7.1" 320 | 321 | [[package]] 322 | name = "pyparsing" 323 | version = "2.4.7" 324 | description = "Python parsing module" 325 | category = "dev" 326 | optional = false 327 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 328 | 329 | [[package]] 330 | name = "pytest" 331 | version = "6.2.1" 332 | description = "pytest: simple powerful testing with Python" 333 | category = "dev" 334 | optional = false 335 | python-versions = ">=3.6" 336 | 337 | [package.dependencies] 338 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 339 | attrs = ">=19.2.0" 340 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 341 | iniconfig = "*" 342 | packaging = "*" 343 | pluggy = ">=0.12,<1.0.0a1" 344 | py = ">=1.8.2" 345 | toml = "*" 346 | 347 | [package.extras] 348 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 349 | 350 | [[package]] 351 | name = "python-dateutil" 352 | version = "2.8.1" 353 | description = "Extensions to the standard Python datetime module" 354 | category = "main" 355 | optional = false 356 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 357 | 358 | [package.dependencies] 359 | six = ">=1.5" 360 | 361 | [[package]] 362 | name = "pytimeparse" 363 | version = "1.1.8" 364 | description = "Time expression parser" 365 | category = "main" 366 | optional = false 367 | python-versions = "*" 368 | 369 | [[package]] 370 | name = "pytz" 371 | version = "2020.4" 372 | description = "World timezone definitions, modern and historical" 373 | category = "main" 374 | optional = false 375 | python-versions = "*" 376 | 377 | [[package]] 378 | name = "regex" 379 | version = "2020.11.13" 380 | description = "Alternative regular expression module, to replace re." 381 | category = "dev" 382 | optional = false 383 | python-versions = "*" 384 | 385 | [[package]] 386 | name = "schema" 387 | version = "0.7.3" 388 | description = "Simple data validation library" 389 | category = "main" 390 | optional = false 391 | python-versions = "*" 392 | 393 | [package.dependencies] 394 | contextlib2 = ">=0.5.5" 395 | 396 | [[package]] 397 | name = "six" 398 | version = "1.15.0" 399 | description = "Python 2 and 3 compatibility utilities" 400 | category = "main" 401 | optional = false 402 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 403 | 404 | [[package]] 405 | name = "toml" 406 | version = "0.10.2" 407 | description = "Python Library for Tom's Obvious, Minimal Language" 408 | category = "main" 409 | optional = false 410 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 411 | 412 | [[package]] 413 | name = "tomlkit" 414 | version = "0.5.11" 415 | description = "Style preserving TOML library" 416 | category = "dev" 417 | optional = false 418 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 419 | 420 | [[package]] 421 | name = "typed-ast" 422 | version = "1.4.1" 423 | description = "a fork of Python 2 and 3 ast modules with type comment support" 424 | category = "dev" 425 | optional = false 426 | python-versions = "*" 427 | 428 | [[package]] 429 | name = "typing-extensions" 430 | version = "3.7.4.3" 431 | description = "Backported and Experimental Type Hints for Python 3.5+" 432 | category = "dev" 433 | optional = false 434 | python-versions = "*" 435 | 436 | [[package]] 437 | name = "wrapt" 438 | version = "1.12.1" 439 | description = "Module for decorators, wrappers and monkey patching." 440 | category = "dev" 441 | optional = false 442 | python-versions = "*" 443 | 444 | [metadata] 445 | lock-version = "1.1" 446 | python-versions = "^3.8" 447 | content-hash = "f949108894ac9434d54454f2b2a3b7485d41c4ce06efd8232995a3b10c64914e" 448 | 449 | [metadata.files] 450 | appdirs = [ 451 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 452 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 453 | ] 454 | astroid = [ 455 | {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, 456 | {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, 457 | ] 458 | atomicwrites = [ 459 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 460 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 461 | ] 462 | attrs = [ 463 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 464 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 465 | ] 466 | autoflake = [ 467 | {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"}, 468 | ] 469 | autohooks = [ 470 | {file = "autohooks-2.2.0-py3-none-any.whl", hash = "sha256:9c7b1117ebace5f7edb3352ea7951dc09cfab8a1f8331ae4157da11cdf66f280"}, 471 | {file = "autohooks-2.2.0.tar.gz", hash = "sha256:44bc4675ae6eae0f0803e1f23f31405317ffc679b6a1873ee192999466e05cdd"}, 472 | ] 473 | autohooks-plugin-black = [ 474 | {file = "autohooks-plugin-black-1.2.0.tar.gz", hash = "sha256:e69a18209f3fd584da903e87faf0e3685caa4d51a9e870ddb3c2b1aef93387b8"}, 475 | {file = "autohooks_plugin_black-1.2.0-py3-none-any.whl", hash = "sha256:5099f620e01d3fcfd70195e63f101d003528b40475e921829a9ebde000f2c5ee"}, 476 | ] 477 | autohooks-plugin-isort = [ 478 | {file = "autohooks-plugin-isort-1.0.0.tar.gz", hash = "sha256:3308b24fa60562c3193c6bc443051e213986216155b37d0550443db337f0f90b"}, 479 | {file = "autohooks_plugin_isort-1.0.0-py3-none-any.whl", hash = "sha256:a43442277aa2c1f4c9d6ac57ccbb87cc2508b1f29ab2e621a2f0b599773c250f"}, 480 | ] 481 | black = [ 482 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 483 | ] 484 | click = [ 485 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 486 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 487 | ] 488 | click-log = [ 489 | {file = "click-log-0.3.2.tar.gz", hash = "sha256:16fd1ca3fc6b16c98cea63acf1ab474ea8e676849dc669d86afafb0ed7003124"}, 490 | {file = "click_log-0.3.2-py2.py3-none-any.whl", hash = "sha256:eee14dc37cdf3072158570f00406572f9e03e414accdccfccd4c538df9ae322c"}, 491 | ] 492 | colorama = [ 493 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 494 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 495 | ] 496 | colorful = [ 497 | {file = "colorful-0.5.4-py2.py3-none-any.whl", hash = "sha256:8d264b52a39aae4c0ba3e2a46afbaec81b0559a99be0d2cfe2aba4cf94531348"}, 498 | {file = "colorful-0.5.4.tar.gz", hash = "sha256:86848ad4e2eda60cd2519d8698945d22f6f6551e23e95f3f14dfbb60997807ea"}, 499 | ] 500 | contextlib2 = [ 501 | {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, 502 | {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, 503 | ] 504 | eventkit = [ 505 | {file = "eventkit-0.8.6-py3-none-any.whl", hash = "sha256:7ff6d05d6dc2991e093eebb3c86cfeadce215e1256a294dca8e30c6c64422e0e"}, 506 | {file = "eventkit-0.8.6.tar.gz", hash = "sha256:ca9ef8eb42e0846381c2f79e5cf6c572224ea25b52c6410f6e17090ce761ae10"}, 507 | ] 508 | ib-insync = [ 509 | {file = "ib_insync-0.9.64-py3-none-any.whl", hash = "sha256:0c967411475e5176660a07c4707dd2db25e56b9aaa03ea066f398fb1fcfb5d19"}, 510 | {file = "ib_insync-0.9.64.tar.gz", hash = "sha256:30d518714d32cbacd8465b02adf625b45d18ba3265471e94d70ca26a6c05c19d"}, 511 | ] 512 | iniconfig = [ 513 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 514 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 515 | ] 516 | isort = [ 517 | {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, 518 | {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, 519 | ] 520 | lazy-object-proxy = [ 521 | {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, 522 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, 523 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, 524 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, 525 | {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, 526 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, 527 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, 528 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, 529 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, 530 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, 531 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, 532 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, 533 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, 534 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, 535 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, 536 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, 537 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, 538 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, 539 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, 540 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, 541 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, 542 | ] 543 | mccabe = [ 544 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 545 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 546 | ] 547 | mypy-extensions = [ 548 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 549 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 550 | ] 551 | nest-asyncio = [ 552 | {file = "nest_asyncio-1.4.3-py3-none-any.whl", hash = "sha256:dbe032f3e9ff7f120e76be22bf6e7958e867aed1743e6894b8a9585fe8495cc9"}, 553 | {file = "nest_asyncio-1.4.3.tar.gz", hash = "sha256:eaa09ef1353ebefae19162ad423eef7a12166bcc63866f8bff8f3635353cd9fa"}, 554 | ] 555 | numpy = [ 556 | {file = "numpy-1.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9b30d4bd69498fc0c3fe9db5f62fffbb06b8eb9321f92cc970f2969be5e3949"}, 557 | {file = "numpy-1.19.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fedbd128668ead37f33917820b704784aff695e0019309ad446a6d0b065b57e4"}, 558 | {file = "numpy-1.19.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8ece138c3a16db8c1ad38f52eb32be6086cc72f403150a79336eb2045723a1ad"}, 559 | {file = "numpy-1.19.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:64324f64f90a9e4ef732be0928be853eee378fd6a01be21a0a8469c4f2682c83"}, 560 | {file = "numpy-1.19.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:ad6f2ff5b1989a4899bf89800a671d71b1612e5ff40866d1f4d8bcf48d4e5764"}, 561 | {file = "numpy-1.19.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d6c7bb82883680e168b55b49c70af29b84b84abb161cbac2800e8fcb6f2109b6"}, 562 | {file = "numpy-1.19.4-cp36-cp36m-win32.whl", hash = "sha256:13d166f77d6dc02c0a73c1101dd87fdf01339febec1030bd810dcd53fff3b0f1"}, 563 | {file = "numpy-1.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:448ebb1b3bf64c0267d6b09a7cba26b5ae61b6d2dbabff7c91b660c7eccf2bdb"}, 564 | {file = "numpy-1.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:27d3f3b9e3406579a8af3a9f262f5339005dd25e0ecf3cf1559ff8a49ed5cbf2"}, 565 | {file = "numpy-1.19.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:16c1b388cc31a9baa06d91a19366fb99ddbe1c7b205293ed072211ee5bac1ed2"}, 566 | {file = "numpy-1.19.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e5b6ed0f0b42317050c88022349d994fe72bfe35f5908617512cd8c8ef9da2a9"}, 567 | {file = "numpy-1.19.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:18bed2bcb39e3f758296584337966e68d2d5ba6aab7e038688ad53c8f889f757"}, 568 | {file = "numpy-1.19.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:fe45becb4c2f72a0907c1d0246ea6449fe7a9e2293bb0e11c4e9a32bb0930a15"}, 569 | {file = "numpy-1.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6d7593a705d662be5bfe24111af14763016765f43cb6923ed86223f965f52387"}, 570 | {file = "numpy-1.19.4-cp37-cp37m-win32.whl", hash = "sha256:6ae6c680f3ebf1cf7ad1d7748868b39d9f900836df774c453c11c5440bc15b36"}, 571 | {file = "numpy-1.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9eeb7d1d04b117ac0d38719915ae169aa6b61fca227b0b7d198d43728f0c879c"}, 572 | {file = "numpy-1.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cb1017eec5257e9ac6209ac172058c430e834d5d2bc21961dceeb79d111e5909"}, 573 | {file = "numpy-1.19.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:edb01671b3caae1ca00881686003d16c2209e07b7ef8b7639f1867852b948f7c"}, 574 | {file = "numpy-1.19.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f29454410db6ef8126c83bd3c968d143304633d45dc57b51252afbd79d700893"}, 575 | {file = "numpy-1.19.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:ec149b90019852266fec2341ce1db513b843e496d5a8e8cdb5ced1923a92faab"}, 576 | {file = "numpy-1.19.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1aeef46a13e51931c0b1cf8ae1168b4a55ecd282e6688fdb0a948cc5a1d5afb9"}, 577 | {file = "numpy-1.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db"}, 578 | {file = "numpy-1.19.4-cp38-cp38-win32.whl", hash = "sha256:5734bdc0342aba9dfc6f04920988140fb41234db42381cf7ccba64169f9fe7ac"}, 579 | {file = "numpy-1.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:09c12096d843b90eafd01ea1b3307e78ddd47a55855ad402b157b6c4862197ce"}, 580 | {file = "numpy-1.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e452dc66e08a4ce642a961f134814258a082832c78c90351b75c41ad16f79f63"}, 581 | {file = "numpy-1.19.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a5d897c14513590a85774180be713f692df6fa8ecf6483e561a6d47309566f37"}, 582 | {file = "numpy-1.19.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a09f98011236a419ee3f49cedc9ef27d7a1651df07810ae430a6b06576e0b414"}, 583 | {file = "numpy-1.19.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:50e86c076611212ca62e5a59f518edafe0c0730f7d9195fec718da1a5c2bb1fc"}, 584 | {file = "numpy-1.19.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f0d3929fe88ee1c155129ecd82f981b8856c5d97bcb0d5f23e9b4242e79d1de3"}, 585 | {file = "numpy-1.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c42c4b73121caf0ed6cd795512c9c09c52a7287b04d105d112068c1736d7c753"}, 586 | {file = "numpy-1.19.4-cp39-cp39-win32.whl", hash = "sha256:8cac8790a6b1ddf88640a9267ee67b1aee7a57dfa2d2dd33999d080bc8ee3a0f"}, 587 | {file = "numpy-1.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:4377e10b874e653fe96985c05feed2225c912e328c8a26541f7fc600fb9c637b"}, 588 | {file = "numpy-1.19.4-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:2a2740aa9733d2e5b2dfb33639d98a64c3b0f24765fed86b0fd2aec07f6a0a08"}, 589 | {file = "numpy-1.19.4.zip", hash = "sha256:141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512"}, 590 | ] 591 | packaging = [ 592 | {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, 593 | {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, 594 | ] 595 | pandas = [ 596 | {file = "pandas-1.1.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bf23a3b54d128b50f4f9d4675b3c1857a688cc6731a32f931837d72effb2698d"}, 597 | {file = "pandas-1.1.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5a780260afc88268a9d3ac3511d8f494fdcf637eece62fb9eb656a63d53eb7ca"}, 598 | {file = "pandas-1.1.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b61080750d19a0122469ab59b087380721d6b72a4e7d962e4d7e63e0c4504814"}, 599 | {file = "pandas-1.1.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:0de3ddb414d30798cbf56e642d82cac30a80223ad6fe484d66c0ce01a84d6f2f"}, 600 | {file = "pandas-1.1.5-cp36-cp36m-win32.whl", hash = "sha256:70865f96bb38fec46f7ebd66d4b5cfd0aa6b842073f298d621385ae3898d28b5"}, 601 | {file = "pandas-1.1.5-cp36-cp36m-win_amd64.whl", hash = "sha256:19a2148a1d02791352e9fa637899a78e371a3516ac6da5c4edc718f60cbae648"}, 602 | {file = "pandas-1.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26fa92d3ac743a149a31b21d6f4337b0594b6302ea5575b37af9ca9611e8981a"}, 603 | {file = "pandas-1.1.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c16d59c15d946111d2716856dd5479221c9e4f2f5c7bc2d617f39d870031e086"}, 604 | {file = "pandas-1.1.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3be7a7a0ca71a2640e81d9276f526bca63505850add10206d0da2e8a0a325dae"}, 605 | {file = "pandas-1.1.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:573fba5b05bf2c69271a32e52399c8de599e4a15ab7cec47d3b9c904125ab788"}, 606 | {file = "pandas-1.1.5-cp37-cp37m-win32.whl", hash = "sha256:21b5a2b033380adbdd36b3116faaf9a4663e375325831dac1b519a44f9e439bb"}, 607 | {file = "pandas-1.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:24c7f8d4aee71bfa6401faeba367dd654f696a77151a8a28bc2013f7ced4af98"}, 608 | {file = "pandas-1.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2860a97cbb25444ffc0088b457da0a79dc79f9c601238a3e0644312fcc14bf11"}, 609 | {file = "pandas-1.1.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5008374ebb990dad9ed48b0f5d0038124c73748f5384cc8c46904dace27082d9"}, 610 | {file = "pandas-1.1.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2c2f7c670ea4e60318e4b7e474d56447cf0c7d83b3c2a5405a0dbb2600b9c48e"}, 611 | {file = "pandas-1.1.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0a643bae4283a37732ddfcecab3f62dd082996021b980f580903f4e8e01b3c5b"}, 612 | {file = "pandas-1.1.5-cp38-cp38-win32.whl", hash = "sha256:5447ea7af4005b0daf695a316a423b96374c9c73ffbd4533209c5ddc369e644b"}, 613 | {file = "pandas-1.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:4c62e94d5d49db116bef1bd5c2486723a292d79409fc9abd51adf9e05329101d"}, 614 | {file = "pandas-1.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:731568be71fba1e13cae212c362f3d2ca8932e83cb1b85e3f1b4dd77d019254a"}, 615 | {file = "pandas-1.1.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c61c043aafb69329d0f961b19faa30b1dab709dd34c9388143fc55680059e55a"}, 616 | {file = "pandas-1.1.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2b1c6cd28a0dfda75c7b5957363333f01d370936e4c6276b7b8e696dd500582a"}, 617 | {file = "pandas-1.1.5-cp39-cp39-win32.whl", hash = "sha256:c94ff2780a1fd89f190390130d6d36173ca59fcfb3fe0ff596f9a56518191ccb"}, 618 | {file = "pandas-1.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:edda9bacc3843dfbeebaf7a701763e68e741b08fccb889c003b0a52f0ee95782"}, 619 | {file = "pandas-1.1.5.tar.gz", hash = "sha256:f10fc41ee3c75a474d3bdf68d396f10782d013d7f67db99c0efbfd0acb99701b"}, 620 | ] 621 | pathspec = [ 622 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 623 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 624 | ] 625 | pluggy = [ 626 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 627 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 628 | ] 629 | py = [ 630 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 631 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 632 | ] 633 | pyflakes = [ 634 | {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, 635 | {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, 636 | ] 637 | pylint = [ 638 | {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, 639 | {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, 640 | ] 641 | pyparsing = [ 642 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 643 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 644 | ] 645 | pytest = [ 646 | {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, 647 | {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, 648 | ] 649 | python-dateutil = [ 650 | {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, 651 | {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, 652 | ] 653 | pytimeparse = [ 654 | {file = "pytimeparse-1.1.8-py2.py3-none-any.whl", hash = "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd"}, 655 | {file = "pytimeparse-1.1.8.tar.gz", hash = "sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a"}, 656 | ] 657 | pytz = [ 658 | {file = "pytz-2020.4-py2.py3-none-any.whl", hash = "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"}, 659 | {file = "pytz-2020.4.tar.gz", hash = "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268"}, 660 | ] 661 | regex = [ 662 | {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, 663 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, 664 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, 665 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, 666 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, 667 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, 668 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, 669 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, 670 | {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, 671 | {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, 672 | {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, 673 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, 674 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, 675 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, 676 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, 677 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, 678 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, 679 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, 680 | {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, 681 | {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, 682 | {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, 683 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, 684 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, 685 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, 686 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, 687 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, 688 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, 689 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, 690 | {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, 691 | {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, 692 | {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, 693 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, 694 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, 695 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, 696 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, 697 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, 698 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, 699 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, 700 | {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, 701 | {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, 702 | {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, 703 | ] 704 | schema = [ 705 | {file = "schema-0.7.3-py2.py3-none-any.whl", hash = "sha256:c331438b60f634cab5664ab720d3083cc444f924d55269530c36b33e3354276f"}, 706 | {file = "schema-0.7.3.tar.gz", hash = "sha256:4cf529318cfd1e844ecbe02f41f7e5aa027463e7403666a52746f31f04f47a5e"}, 707 | ] 708 | six = [ 709 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 710 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 711 | ] 712 | toml = [ 713 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 714 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 715 | ] 716 | tomlkit = [ 717 | {file = "tomlkit-0.5.11-py2.py3-none-any.whl", hash = "sha256:4e1bd6c9197d984528f9ff0cc9db667c317d8881288db50db20eeeb0f6b0380b"}, 718 | {file = "tomlkit-0.5.11.tar.gz", hash = "sha256:f044eda25647882e5ef22b43a1688fb6ab12af2fc50e8456cdfc751c873101cf"}, 719 | ] 720 | typed-ast = [ 721 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 722 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 723 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 724 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 725 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 726 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 727 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 728 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, 729 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 730 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 731 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 732 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 733 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 734 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, 735 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 736 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 737 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 738 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 739 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 740 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, 741 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 742 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 743 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 744 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, 745 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, 746 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, 747 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, 748 | {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, 749 | {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, 750 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 751 | ] 752 | typing-extensions = [ 753 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 754 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 755 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 756 | ] 757 | wrapt = [ 758 | {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, 759 | ] 760 | --------------------------------------------------------------------------------