This commit is contained in:
James Maslek
2023-05-10 07:42:05 -07:00
10 changed files with 430 additions and 20 deletions

76
KEYBOARD_HOTKEY_MACROS.md vendored Normal file
View File

@@ -0,0 +1,76 @@
# Keyboard Hotkey Macros
This document will explain how you can set hotkeys macros on your customizable keyboard which allow you to perform investment research in a few seconds.
## Introduction to Routines
The OpenBB Terminal is a powerful open source investment research platform.
The more proefficient you get on the platform, the faster and more efficiently you are able to do investment research.
One of the concepts that OpenBB introduced early on was the concept of script routines. Read more [here](https://docs.openbb.co/terminal/usage/guides/scripts-and-routines).
**TL;DR: You can create MACROS like in Excel to run a sequence of commands.**
E.g. by running `exe script.openbb` the following sequence could be executed
```shell
stocks/load AAPL/candle --ma 20/fa/epsfc/pt/est
```
which would lead to:
![image](https://user-images.githubusercontent.com/25267873/236659876-e119c820-b9ed-40e7-bb8d-5510fe862149.png)
This allows to automate the process of investment research, and can improve user's experience by a significant margin.
## Supported Configurators
1. [VIA](#via)
## VIA
This document will explain how you can set hotkeys on your customizable keyboard using [VIA](https://www.caniusevia.com/).
Here is a list of VIA's supported keyboards: [https://www.caniusevia.com/docs/supported_keyboards](https://www.caniusevia.com/docs/supported_keyboards)
Note: We were in the market looking for a keyboard that could be highliy customizable for the needs of OpenBB power users. This is when we stumbled upon Keychron and the VIA configurator which allows users to intuitively remap any key on the keyboard, and create numerous macro commands, shortcuts, or key combinations.
Here's a post from Keychron on VIA: [Why VIA is one of the most essential features for a custom keyboard?](https://www.keychron.com/blogs/news/why-qmk-via-is-one-of-the-most-essential-features-for-a-custom-keyboard)
For the purpose of this example, the command pipeline we are creating has the following sequence of commands: `dps/psi/../fa/pt/income/..`
1. Try VIA [here](https://usevia.app/). The following screen should popup
<img width="1436" alt="Screenshot 2023-05-06 at 11 10 51 PM" src="https://user-images.githubusercontent.com/25267873/236660856-f92ac602-cde9-48e6-8029-c083fbb75ff9.png">
2. Select the Layer you are interested in altering. As we're not looking forward to remap any of the existing main keys, we need to go to layer 2, 3 or 4. In my case, with a Keychron Q2, I know that Layer 4 can be accessed by pressing on "Fn2". Thus, I select that layer.
<img width="1445" alt="Screenshot 2023-05-06 at 11 10 06 PM" src="https://user-images.githubusercontent.com/25267873/236660841-09203874-8a8e-4393-8674-357aad67a22b.png">
3. Most keys should have a triangle upside down which simbolizes that they don't have any functionality. You want to select one of those keys to contain your hotkey routine. After that selection you want to remap the key functionality, hence in the **KEYMAP settings** below you click on MACROS tab and select "MO". Now the screen should look like this:
<img width="1454" alt="Screenshot 2023-05-06 at 11 13 43 PM" src="https://user-images.githubusercontent.com/25267873/236660948-a148582e-f928-4f12-ae54-9bdd3adfd020.png">
4. Now we want to change what "MO" does and for that we select the **MACROS settings** below. Then we can select "M0" and insert `dps/psi/../fa/pt/income/..{KC_ENT}`. The screen should look like this:
<img width="1453" alt="Screenshot 2023-05-06 at 11 17 52 PM" src="https://user-images.githubusercontent.com/25267873/236661126-eeb5dc7c-2c01-4a43-ab64-12c470e864ce.png">
Note that the `{KC_ENT}` will ensure that the command is run on the terminal.
5. Save. To ensure that everything is correct you can go into the **KEYMAP settings** and click on the hotkey that you just created to see if it contains the sequence of commands, the following sequence should appear:
<img width="1434" alt="Screenshot 2023-05-06 at 11 20 24 PM" src="https://user-images.githubusercontent.com/25267873/236661232-4f9119de-af37-49a2-948d-cfd6d650ed92.png">
6. Finally, we are ready to test it on the [OpenBB Terminal](https://my.openbb.co/app/terminal). After going into `stocks` and doing `load AAPL`, I pressed "Fn2'+Z which lead to:
<img width="1784" alt="Screenshot 2023-05-06 at 10 53 49 PM" src="https://user-images.githubusercontent.com/25267873/236660272-290fe586-7663-4cd6-bfc0-80b7f8f2efd1.png">
PS: If you prefer to see this in video format, you can do so by checking the following video.
<p align="center">
<a href="https://www.youtube.com/watch?v=cgeN3Ep2nEw" rel="Keychron x OpenBB Demo">
<img src="https://user-images.githubusercontent.com/25267873/236660025-581d0e4f-df5e-4461-b2b9-70154c1bdf89.png" alt="Didier demonstrating Keychron x OpenBB" width="100%"/>
</a>
</p>
Hope you enjoy this tutorial, and please let us know what type of MACROS are you setting up.

View File

@@ -22,6 +22,8 @@ class EconometricsRoot(Category):
`dwat_chart`: Show Durbin-Watson autocorrelation tests\n
`fdols`: First differencing is an alternative to using fixed effects when there might be correlation.\n
`fe`: When effects are correlated with the regressors the RE and BE estimators are not consistent.\n
`garch`: Calculates annualized volatility forecasts based on GARCH.\n
`garch_chart`: Plots the annualized volatility forecasts based on GARCH\n
`get_regression_data`: This function creates a DataFrame with the required regression data as\n
`granger`: Calculate granger tests\n
`granger_chart`: Show granger tests\n
@@ -56,6 +58,8 @@ class EconometricsRoot(Category):
self.dwat_chart = lib.econometrics_regression_view.display_dwat
self.fdols = lib.econometrics_regression_model.get_fdols
self.fe = lib.econometrics_regression_model.get_fe
self.garch = lib.econometrics_model.get_garch
self.garch_chart = lib.econometrics_view.display_garch
self.get_regression_data = lib.econometrics_regression_model.get_regression_data
self.granger = lib.econometrics_model.get_granger_causality
self.granger_chart = lib.econometrics_view.display_granger

View File

@@ -183,6 +183,7 @@ econometrics.panel,econometrics_regression_model.get_regressions_results,econome
econometrics.pols,econometrics_regression_model.get_pols,
econometrics.re,econometrics_regression_model.get_re,
econometrics.root,econometrics_model.get_root,econometrics_view.display_root
econometrics.garch,econometrics_model.get_garch,econometrics_view.display_garch
economy.available_indices,economy_yfinance_model.get_available_indices,
economy.balance,economy_oecd_model.get_balance,economy_oecd_view.plot_balance
economy.bigmac,economy_nasdaq_model.get_big_mac_indices,economy_nasdaq_view.display_big_mac_index
1 trail model view
183 econometrics.pols econometrics_regression_model.get_pols
184 econometrics.re econometrics_regression_model.get_re
185 econometrics.root econometrics_model.get_root econometrics_view.display_root
186 econometrics.garch econometrics_model.get_garch econometrics_view.display_garch
187 economy.available_indices economy_yfinance_model.get_available_indices
188 economy.balance economy_oecd_model.get_balance economy_oecd_view.plot_balance
189 economy.bigmac economy_nasdaq_model.get_big_mac_indices economy_nasdaq_view.display_big_mac_index

View File

@@ -65,6 +65,7 @@ class EconometricsController(BaseController):
"combine",
"rename",
"lag",
"ret",
"ols",
"norm",
"root",
@@ -73,6 +74,7 @@ class EconometricsController(BaseController):
"dwat",
"bgod",
"bpag",
"garch",
"granger",
"coint",
]
@@ -181,6 +183,7 @@ class EconometricsController(BaseController):
"plot",
"norm",
"root",
"garch",
"granger",
"coint",
"corr",
@@ -217,6 +220,7 @@ class EconometricsController(BaseController):
"ols",
"panel",
"delete",
"garch",
]:
self.choices[feature] = dataset_columns
for feature in [
@@ -280,17 +284,19 @@ class EconometricsController(BaseController):
mt.add_cmd("combine", self.files)
mt.add_cmd("rename", self.files)
mt.add_cmd("lag", self.files)
mt.add_cmd("ret", self.files)
mt.add_cmd("export", self.files)
mt.add_info("_tests_")
mt.add_info("time_series_")
mt.add_cmd("norm", self.files)
mt.add_cmd("root", self.files)
mt.add_cmd("granger", self.files)
mt.add_cmd("coint", self.files)
mt.add_info("_regression_")
mt.add_cmd("ols", self.files)
mt.add_cmd("granger", self.files)
mt.add_cmd("root", self.files)
mt.add_cmd("coint", self.files)
mt.add_cmd("garch", self.files)
mt.add_info("_panel_")
mt.add_cmd("panel", self.files)
mt.add_cmd("compare", self.files)
mt.add_info("_regression_tests_")
mt.add_info("_residuals_")
mt.add_cmd("dwat", self.files and self.regression["OLS"]["model"])
mt.add_cmd("bgod", self.files and self.regression["OLS"]["model"])
mt.add_cmd("bpag", self.files and self.regression["OLS"]["model"])
@@ -1224,6 +1230,50 @@ class EconometricsController(BaseController):
self.update_runtime_choices()
@log_start_end(log=logger)
def call_ret(self, other_args: List[str]):
"""Process ret command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="ret",
description="Calculate returns for the given column.",
)
parser.add_argument(
"-v",
"--values",
help="Dataset.column values to calculate returns.",
dest="values",
choices={
f"{dataset}.{column}": {column: None, dataset: None}
for dataset, dataframe in self.datasets.items()
for column in dataframe.columns
},
type=str,
required="-h" not in other_args,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-v")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, export_allowed=NO_EXPORT
)
if not ns_parser:
return
try:
dataset, col = ns_parser.values.split(".")
data = self.datasets[dataset]
except ValueError:
console.print("[red]Please enter 'dataset'.'column'.[/red]\n")
return
data[col + "_returns"] = econometrics_model.get_returns(data[col])
self.datasets[dataset] = data
self.update_runtime_choices()
@log_start_end(log=logger)
def call_eval(self, other_args):
"""Process eval command"""
@@ -1890,6 +1940,124 @@ class EconometricsController(BaseController):
self.regression["OLS"]["model"], ns_parser.export
)
@log_start_end(log=logger)
def call_garch(self, other_args: List[str]):
"""Process garch command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="garch",
description=r"""Calculates annualized volatility forecasts based on GARCH.
GARCH (Generalized autoregressive conditional heteroskedasticity) is stochastic model for time series,
which is for instance used to model volatility clusters, stock return and inflation. It is a
generalisation of the ARCH models.
$\text{GARCH}(p, q) = (1 - \alpha - \beta) \sigma_l + \sum_{i=1}^q \alpha u_{t-i}^2 + \sum_{i=1}^p \beta
\sigma_{t-i}^2$ [1]
The GARCH-model assumes that the variance estimate consists of 3 components:
- $\sigma_l$ ; the long term component, which is unrelated to the current market conditions
- $u_t$ ; the innovation/discovery through current market price changes
- $\sigma_t$ ; the last estimate
GARCH can be understood as a model, which allows to optimize these 3 variance components to the time
series. This is done assigning weights to variance components: $(1 - \alpha - \beta)$ for $\sigma_l$ ,
$\alpha$ for $u_t$ and $\beta$ for $\sigma_t$ . [2]
The weights can be estimated by iterating over different values of $(1 - \alpha - \beta) \sigma_l$
which we will call $\omega$ , $\alpha$ and $\beta$ , while maximizing:
$\sum_{i} -ln(v_i) - (u_i ^ 2) / v_i$ . With the constraints:
- $\alpha > 0$
- $\beta > 0$
- $\alpha + \beta < 1$
Note that there is no restriction on $\omega$ .
Another method used for estimation is "variance targeting", where one first sets $\omega$
equal to the variance of the time series. This method nearly as effective as the previously mentioned and
is less computationally effective.
One can measure the fit of the time series to the GARCH method by using the Ljung-Box statistic. [3]
See the sources below for reference and for greater detail.
Sources:
[1] Generalized Autoregressive Conditional Heteroskedasticity, by Tim Bollerslev
[2] Finance Compact Plus Band 1, by Yvonne Seler Zimmerman and Heinz Zimmerman; ISBN: 978-3-907291-31-1
[3] Options, Futures & other Derivates, by John C. Hull; ISBN: 0-13-022444-8""",
)
parser.add_argument(
"-v",
"--value",
type=str,
choices=self.choices["garch"],
dest="column",
help="The column and name of the database you want to estimate volatility for",
required="-h" not in other_args,
)
parser.add_argument(
"-p",
help="The lag order of the symmetric innovation",
dest="p",
type=int,
default=1,
)
parser.add_argument(
"-o",
help="The lag order of the asymmetric innovation",
dest="o",
type=int,
default=0,
)
parser.add_argument(
"-q",
help="The lag order of lagged volatility or equivalent",
dest="q",
type=int,
default=1,
)
parser.add_argument(
"-m",
"--mean",
help="Choose mean model",
choices=["LS", "AR", "ARX", "HAR", "HARX", "constant", "zero"],
default="constant",
type=str,
dest="mean",
)
parser.add_argument(
"-l",
"--length",
help="The length of the estimate",
dest="horizon",
type=int,
default=100,
)
parser.add_argument(
"-d",
"--detailed",
help="Display the details about the parameter fit, for instance the confidence interval",
dest="detailed",
action="store_true",
default=False,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-v")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if ns_parser:
dataset, column = ns_parser.column.split(".")
econometrics_view.display_garch(
self.datasets[dataset],
column,
ns_parser.p,
ns_parser.o,
ns_parser.q,
ns_parser.mean,
ns_parser.horizon,
ns_parser.detailed,
)
@log_start_end(log=logger)
def call_granger(self, other_args: List[str]):
"""Process granger command"""

View File

@@ -8,18 +8,18 @@ import warnings
from itertools import combinations
from typing import Any, Dict, Optional, Tuple, Union
import numpy as np
import pandas as pd
import statsmodels.api as sm
from arch import arch_model
from scipy import stats
from statsmodels.tsa.stattools import adfuller, grangercausalitytests, kpss
from openbb_terminal.decorators import log_start_end
from openbb_terminal.rich_config import console
logger = logging.getLogger(__name__)
@log_start_end(log=logger)
def get_options(
datasets: Dict[str, pd.DataFrame], dataset_name: str = ""
) -> Dict[Union[str, Any], pd.DataFrame]:
@@ -61,7 +61,6 @@ def get_options(
return option_tables
@log_start_end(log=logger)
def get_corr_df(data: pd.DataFrame) -> pd.DataFrame:
"""Returns correlation for a given df
@@ -79,7 +78,6 @@ def get_corr_df(data: pd.DataFrame) -> pd.DataFrame:
return corr
@log_start_end(log=logger)
def clean(
dataset: pd.DataFrame,
fill: str = "",
@@ -127,7 +125,6 @@ def clean(
return dataset
@log_start_end(log=logger)
def get_normality(data: pd.Series) -> pd.DataFrame:
"""
The distribution of returns and generate statistics on the relation to the normal curve.
@@ -182,7 +179,6 @@ def get_normality(data: pd.Series) -> pd.DataFrame:
)
@log_start_end(log=logger)
def get_root(
data: pd.Series, fuller_reg: str = "c", kpss_reg: str = "c"
) -> pd.DataFrame:
@@ -224,10 +220,9 @@ def get_root(
return data
@log_start_end(log=logger)
def get_granger_causality(
dependent_series: pd.Series, independent_series: pd.Series, lags: int = 3
) -> dict:
) -> pd.DataFrame:
"""Calculate granger tests
Parameters
@@ -241,8 +236,8 @@ def get_granger_causality(
Returns
-------
dict
Dictionary containing results of Granger test
pd.DataFrame
Dataframe containing results of Granger test
"""
granger_set = pd.concat([dependent_series, independent_series], axis=1)
@@ -326,6 +321,90 @@ def get_coint_df(
return pd.DataFrame()
def get_returns(data: pd.Series):
"""Calculate returns for the given time series
Parameters
----------
data: pd.Series
The data series to calculate returns for
"""
return 100 * data.pct_change().dropna()
def get_garch(
data: pd.Series,
p: int = 1,
o: int = 0,
q: int = 1,
mean: str = "constant",
horizon: int = 100,
):
r"""Calculates volatility forecasts based on GARCH.
GARCH (Generalized autoregressive conditional heteroskedasticity) is stochastic model for time series,
which is for instance used to model volatility clusters, stock return and inflation. It is a
generalisation of the ARCH models.
$ GARCH(p, q) = (1 - \alpha - \beta) \sigma_l + \sum_{i=1}^q \alpha u_{t-i}^2 + \sum_{i=1}^p \beta \sigma_{t-i}^2
$ [1]
The GARCH-model assumes that the variance estimate consists of 3 components:
- $ \sigma_l $; the long term component, which is unrelated to the current market conditions
- $ u_t $; the innovation/discovery through current market price changes
- $ \sigma_t $; the last estimate
GARCH can be understood as a model, which allows to optimize these 3 variance components to the time series.
This is done assigning weights to variance components: $ (1 - \alpha - \beta) $ for $ \sigma_l $, $ \alpha $ for
$ u_t $ and $ \beta $ for $ \sigma_t $. [2]
The weights can be estimated by iterating over different values of $ (1 - \alpha - \beta) \sigma_l $ which we
will call $ \omega $, $ \alpha $ and $ \beta $, while maximizing: $ \sum_{i} -ln(v_i) - (u_i ^ 2) / v_i $.
With the constraints:
- $ \alpha > 0 $
- $ \beta > 0 $
- $ \alpha + \beta < 1 $
Note that there is no restriction on $ \omega $.
Another method used for estimation is "variance targeting", where one first sets $ \omega $
equal to the variance of the time series. This method nearly as effective as the previously mentioned and
is less computationally effective.
One can measure the fit of the time series to the GARCH method by using the Ljung-Box statistic. [3]
See the sources below for reference and for greater detail.
Sources:
[1] Generalized Autoregressive Conditional Heteroskedasticity, by Tim Bollerslev
[2] Finance Compact Plus Band 1, by Yvonne Seler Zimmerman and Heinz Zimmerman; ISBN: 978-3-907291-31-1
[3] Options, Futures & other Derivates, by John C. Hull; ISBN: 0-13-022444-8
Parameters
----------
data: pd.Series
The time series (often returns) to estimate volatility from
p: int
Lag order of the symmetric innovation
o: int
Lag order of the asymmetric innovation
q: int
Lag order of lagged volatility or equivalent
mean: str
The name of the mean model
horizon: int
The horizon of the forecast
Examples
--------
>>> from openbb_terminal.sdk import openbb
>>> openbb.econometrics.garch(openbb.stocks.load("AAPL").iloc[:, 0].pct_change()*100)
"""
model = arch_model(data.dropna(), vol="GARCH", p=p, o=o, q=q, mean=mean)
model_fit = model.fit(disp="off")
pred = model_fit.forecast(horizon=horizon, reindex=False)
return np.sqrt(pred.variance.values[-1, :]), model_fit
def get_engle_granger_two_step_cointegration_test(
dependent_series: pd.Series, independent_series: pd.Series
) -> Tuple[float, float, float, pd.Series, float, float]:

View File

@@ -1,6 +1,8 @@
"""Econometrics View"""
__docformat__ = "numpy"
# pylint: disable=too-many-arguments
import logging
import os
from typing import Dict, Optional, Union
@@ -43,7 +45,9 @@ def show_options(
"Please load in a dataset by using the 'load' command before using this feature."
)
else:
option_tables = econometrics_model.get_options(datasets, dataset_name)
option_tables = econometrics_model.get_options(
datasets, dataset_name if dataset_name is not None else ""
)
for dataset, data_values in option_tables.items():
print_rich_table(
@@ -386,6 +390,77 @@ def display_root(
)
@log_start_end(log=logger)
def display_garch(
dataset: pd.DataFrame,
column: str,
p: int = 1,
o: int = 0,
q: int = 1,
mean: str = "constant",
horizon: int = 1,
detailed: bool = False,
export: str = "",
external_axes: bool = False,
) -> Union[OpenBBFigure, None]:
"""Plots the volatility forecasts based on GARCH
Parameters
----------
dataset: pd.DataFrame
The dataframe to use
column: str
The column of the dataframe to use
p: int
Lag order of the symmetric innovation
o: int
Lag order of the asymmetric innovation
q: int
Lag order of lagged volatility or equivalent
mean: str
The name of the mean model
horizon: int
The horizon of the forecast
detailed: bool
Whether to display the details about the parameter fit, for instance the confidence interval
export: str
Format to export data
external_axes: bool
Whether to return the figure object or not, by default False
"""
data = dataset[column]
result, garch_fit = econometrics_model.get_garch(data, p, o, q, mean, horizon)
fig = OpenBBFigure()
fig.add_scatter(x=list(range(1, horizon + 1)), y=result)
fig.set_title(
f"{f'GARCH({p}, {o}, {q})' if o != 0 else f'GARCH({p}, {q})'} volatility forecast"
)
if fig.is_image_export(export):
export_data(
export,
os.path.dirname(os.path.abspath(__file__)),
f"{column}_{dataset}_GARCH({p},{q})",
result,
figure=fig,
)
if not detailed:
print_rich_table(
garch_fit.params.to_frame(),
headers=["Values"],
show_index=True,
index_name="Parameters",
title=f"GARCH({p}, {o}, {q})" if o != 0 else f"GARCH({p}, {q})",
export=bool(export),
)
else:
console.print(garch_fit)
return fig.show(external=external_axes)
@log_start_end(log=logger)
def display_granger(
dependent_series: pd.Series,

View File

@@ -966,19 +966,21 @@ en:
econometrics/combine: Combine columns from different datasets
econometrics/rename: Rename column from dataset
econometrics/lag: Add lag to a variable by shifting a column
econometrics/_regression_: Regression
econometrics/_regression_tests_: Regression Tests
econometrics/_panel_: Panel
econometrics/_residuals_: Residuals
econometrics/ols: fit a (multi) linear regression model
econometrics/norm: perform normality tests on a column of a dataset
econometrics/root: perform unitroot tests (ADF & KPSS) on a column of a dataset
econometrics/panel: estimate model based on various regression techniques
econometrics/compare: compare results of all estimated models
econometrics/_tests_: Tests
econometrics/_time_series_: Time Series
econometrics/dwat: Durbin-Watson autocorrelation test on the residuals of the regression
econometrics/bgod: Breusch-Godfrey autocorrelation tests with lags on the residuals of the regression
econometrics/bpag: Breusch-Pagan heteroscedasticity test on the residuals of the regression
econometrics/granger: Granger causality tests on two columns
econometrics/coint: co-integration test on a multitude of columns
econometrics/garch: estimate future volatility with GARCH
econometrics/ret: calculate returns for the given time series
portfolio/bro: brokers holdings supports robinhood, ally, degiro, coinbase
portfolio/po: portfolio optimization optimize your portfolio weights efficiently
portfolio/load: load transactions into the portfolio (use load --example for an example)

View File

@@ -3,6 +3,8 @@ econometrics
## Exploration
load nile
ret -v nile.volume
garch -v nile.volume_returns
desc nile
load nile -a nile_2
eval double_volume = volume * 2

View File

@@ -123,6 +123,8 @@ class OpenBBSDK:
`dwat_chart`: Show Durbin-Watson autocorrelation tests\n
`fdols`: First differencing is an alternative to using fixed effects when there might be correlation.\n
`fe`: When effects are correlated with the regressors the RE and BE estimators are not consistent.\n
`garch`: Calculates annualized volatility forecasts based on GARCH.\n
`garch_chart`: Plots the annualized volatility forecasts based on GARCH\n
`get_regression_data`: This function creates a DataFrame with the required regression data as\n
`granger`: Calculate granger tests\n
`granger_chart`: Show granger tests\n

1
requirements.txt vendored
View File

@@ -6,6 +6,7 @@ altair==4.2.2 ; python_version >= "3.8" and python_full_version != "3.9.7" and p
ansiwrap==0.8.4 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
anyio==3.6.2 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
appdirs==1.4.4 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
arch==5.3.1 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
appnope==0.1.3 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11" and sys_platform == "darwin" or python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11" and platform_system == "Darwin"
argon2-cffi-bindings==21.2.0 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
argon2-cffi==21.3.0 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"