quantflow 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quantflow-0.3.0 → quantflow-0.3.2}/PKG-INFO +10 -5
- {quantflow-0.3.0 → quantflow-0.3.2}/pyproject.toml +22 -9
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/__init__.py +1 -1
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/app.py +16 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/commands/base.py +17 -0
- quantflow-0.3.2/quantflow/cli/commands/crypto.py +121 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/commands/stocks.py +1 -10
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/script.py +2 -2
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/data/deribit.py +21 -7
- quantflow-0.3.2/quantflow/data/fed.py +91 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/data/fmp.py +8 -2
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/data/fred.py +12 -2
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/data/vault.py +6 -0
- quantflow-0.3.2/quantflow/options/bs.py +122 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/options/calibration.py +116 -16
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/options/inputs.py +6 -4
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/options/pricer.py +24 -11
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/options/surface.py +150 -34
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/base.py +7 -7
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/bns.py +1 -1
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/cir.py +5 -1
- quantflow-0.3.2/quantflow/sp/heston.py +205 -0
- quantflow-0.3.2/quantflow/sp/jump_diffusion.py +93 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/ou.py +15 -3
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/poisson.py +57 -11
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/weiner.py +1 -1
- quantflow-0.3.2/quantflow/ta/base.py +14 -0
- quantflow-0.3.2/quantflow/ta/ohlc.py +109 -0
- {quantflow-0.3.0/quantflow/utils → quantflow-0.3.2/quantflow/ta}/paths.py +77 -8
- quantflow-0.3.2/quantflow/utils/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/bins.py +14 -0
- quantflow-0.3.2/quantflow/utils/dates.py +24 -0
- quantflow-0.3.2/quantflow/utils/distributions.py +254 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/marginal.py +3 -5
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/plot.py +26 -12
- {quantflow-0.3.0 → quantflow-0.3.2}/readme.md +5 -2
- quantflow-0.3.0/quantflow/cli/commands/crypto.py +0 -41
- quantflow-0.3.0/quantflow/data/client.py +0 -4
- quantflow-0.3.0/quantflow/options/bs.py +0 -70
- quantflow-0.3.0/quantflow/sp/heston.py +0 -133
- quantflow-0.3.0/quantflow/sp/jump_diffusion.py +0 -83
- quantflow-0.3.0/quantflow/utils/dates.py +0 -11
- quantflow-0.3.0/quantflow/utils/df.py +0 -71
- quantflow-0.3.0/quantflow/utils/distributions.py +0 -125
- quantflow-0.3.0/quantflow/utils/volatility.py +0 -71
- {quantflow-0.3.0 → quantflow-0.3.2}/LICENSE +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/commands/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/commands/fred.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/commands/vault.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/settings.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/data/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/options/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/py.typed +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/copula.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/dsp.py +0 -0
- {quantflow-0.3.0/quantflow/utils → quantflow-0.3.2/quantflow/ta}/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/functions.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/interest_rates.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/numbers.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/transforms.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: quantflow
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: quantitative analysis
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
Author: Luca
|
|
@@ -15,8 +15,10 @@ Provides-Extra: cli
|
|
|
15
15
|
Provides-Extra: data
|
|
16
16
|
Requires-Dist: aio-fluid[http] (>=1.2.1,<2.0.0) ; extra == "data"
|
|
17
17
|
Requires-Dist: asciichartpy (>=1.5.25,<2.0.0) ; extra == "cli"
|
|
18
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: async-cache (>=1.1.1,<2.0.0) ; extra == "cli"
|
|
19
|
+
Requires-Dist: ccy (>=1.7.1,<2.0.0)
|
|
19
20
|
Requires-Dist: click (>=8.1.7,<9.0.0) ; extra == "cli"
|
|
21
|
+
Requires-Dist: holidays (>=0.63,<0.64) ; extra == "cli"
|
|
20
22
|
Requires-Dist: polars[pandas,pyarrow] (>=1.11.0,<2.0.0)
|
|
21
23
|
Requires-Dist: prompt-toolkit (>=3.0.43,<4.0.0) ; extra == "cli"
|
|
22
24
|
Requires-Dist: pydantic (>=2.0.2,<3.0.0)
|
|
@@ -38,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
38
40
|
|
|
39
41
|
Quantitative analysis and pricing tools.
|
|
40
42
|
|
|
41
|
-
Documentation is available as [quantflow jupyter book](https://quantmind.
|
|
43
|
+
Documentation is available as [quantflow jupyter book](https://quantflow.quantmind.com).
|
|
42
44
|
|
|
43
45
|
## Installation
|
|
44
46
|
|
|
@@ -51,10 +53,13 @@ pip install quantflow
|
|
|
51
53
|
|
|
52
54
|
## Modules
|
|
53
55
|
|
|
54
|
-
* [quantflow.cli](https://github.com/quantmind/quantflow/tree/main/quantflow/cli)
|
|
56
|
+
* [quantflow.cli](https://github.com/quantmind/quantflow/tree/main/quantflow/cli) command line client (requires `quantflow[cli,data]`)
|
|
55
57
|
* [quantflow.data](https://github.com/quantmind/quantflow/tree/main/quantflow/data) data APIs (requires `quantflow[data]`)
|
|
56
58
|
* [quantflow.options](https://github.com/quantmind/quantflow/tree/main/quantflow/options) option pricing and calibration
|
|
57
59
|
* [quantflow.sp](https://github.com/quantmind/quantflow/tree/main/quantflow/sp) stochastic process primitives
|
|
60
|
+
* [quantflow.ta](https://github.com/quantmind/quantflow/tree/main/quantflow/ta) timeseries analysis tools
|
|
61
|
+
* [quantflow.utils](https://github.com/quantmind/quantflow/tree/main/quantflow/utils) utilities and helpers
|
|
62
|
+
|
|
58
63
|
|
|
59
64
|
|
|
60
65
|
## Command line tools
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "quantflow"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "quantitative analysis"
|
|
5
5
|
authors = ["Luca <luca@quantmind.com>"]
|
|
6
6
|
license = "BSD-3-Clause"
|
|
@@ -15,7 +15,7 @@ Documentation = "https://quantmind.github.io/quantflow/"
|
|
|
15
15
|
python = ">=3.11"
|
|
16
16
|
scipy = "^1.14.1"
|
|
17
17
|
pydantic = "^2.0.2"
|
|
18
|
-
ccy = {version="1.
|
|
18
|
+
ccy = { version = "^1.7.1" }
|
|
19
19
|
python-dotenv = "^1.0.1"
|
|
20
20
|
polars = {version = "^1.11.0", extras=["pandas", "pyarrow"]}
|
|
21
21
|
asciichartpy = { version = "^1.5.25", optional = true }
|
|
@@ -23,32 +23,44 @@ prompt-toolkit = { version = "^3.0.43", optional = true }
|
|
|
23
23
|
aio-fluid = {version = "^1.2.1", extras=["http"], optional = true}
|
|
24
24
|
rich = {version = "^13.9.4", optional = true}
|
|
25
25
|
click = {version = "^8.1.7", optional = true}
|
|
26
|
+
holidays = {version = "^0.63", optional = true}
|
|
27
|
+
async-cache = {version = "^1.1.1", optional = true}
|
|
26
28
|
|
|
27
29
|
[tool.poetry.group.dev.dependencies]
|
|
28
|
-
black = "^
|
|
30
|
+
black = "^25.1.0"
|
|
29
31
|
pytest-cov = "^6.0.0"
|
|
30
|
-
mypy = "^1.
|
|
32
|
+
mypy = "^1.14.1"
|
|
31
33
|
ghp-import = "^2.0.2"
|
|
32
|
-
ruff = "^0.
|
|
33
|
-
pytest-asyncio = "^0.
|
|
34
|
-
isort = "^
|
|
34
|
+
ruff = "^0.11.2"
|
|
35
|
+
pytest-asyncio = "^0.26.0"
|
|
36
|
+
isort = "^6.0.1"
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
[tool.poetry.extras]
|
|
38
40
|
data = ["aio-fluid"]
|
|
39
|
-
cli = [
|
|
41
|
+
cli = [
|
|
42
|
+
"asciichartpy",
|
|
43
|
+
"async-cache",
|
|
44
|
+
"prompt-toolkit",
|
|
45
|
+
"rich",
|
|
46
|
+
"click",
|
|
47
|
+
"holidays"
|
|
48
|
+
]
|
|
40
49
|
|
|
41
50
|
[tool.poetry.group.book]
|
|
42
51
|
optional = true
|
|
43
52
|
|
|
44
53
|
[tool.poetry.group.book.dependencies]
|
|
45
54
|
jupyter-book = "^1.0.0"
|
|
46
|
-
nbconvert = "^7.16.3"
|
|
47
55
|
jupytext = "^1.13.8"
|
|
48
56
|
plotly = "^5.20.0"
|
|
49
57
|
jupyterlab = "^4.0.2"
|
|
50
58
|
sympy = "^1.12"
|
|
51
59
|
ipywidgets = "^8.0.7"
|
|
60
|
+
sphinx-autodoc-typehints = "2.3.0"
|
|
61
|
+
sphinx-autosummary-accessors = "^2023.4.0"
|
|
62
|
+
sphinx-copybutton = "^0.5.2"
|
|
63
|
+
autodocsumm = "^0.2.14"
|
|
52
64
|
|
|
53
65
|
[tool.poetry.scripts]
|
|
54
66
|
qf = "quantflow.cli.script:main"
|
|
@@ -85,6 +97,7 @@ warn_no_return = true
|
|
|
85
97
|
[[tool.mypy.overrides]]
|
|
86
98
|
module = [
|
|
87
99
|
"asciichartpy.*",
|
|
100
|
+
"cache.*",
|
|
88
101
|
"quantflow_tests.*",
|
|
89
102
|
"IPython.*",
|
|
90
103
|
"pandas.*",
|
|
@@ -4,8 +4,10 @@ from functools import partial
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
|
+
from fluid.utils.http_client import HttpResponseError
|
|
7
8
|
from prompt_toolkit import PromptSession
|
|
8
9
|
from prompt_toolkit.completion import NestedCompleter
|
|
10
|
+
from prompt_toolkit.formatted_text import HTML
|
|
9
11
|
from prompt_toolkit.history import FileHistory
|
|
10
12
|
from rich.console import Console
|
|
11
13
|
from rich.text import Text
|
|
@@ -38,6 +40,7 @@ class QfApp:
|
|
|
38
40
|
self.prompt_message(),
|
|
39
41
|
completer=self.prompt_completer(),
|
|
40
42
|
complete_while_typing=True,
|
|
43
|
+
bottom_toolbar=self.bottom_toolbar,
|
|
41
44
|
)
|
|
42
45
|
except KeyboardInterrupt:
|
|
43
46
|
break
|
|
@@ -80,5 +83,18 @@ class QfApp:
|
|
|
80
83
|
click.exceptions.MissingParameter,
|
|
81
84
|
click.exceptions.NoSuchOption,
|
|
82
85
|
click.exceptions.UsageError,
|
|
86
|
+
HttpResponseError,
|
|
83
87
|
) as e:
|
|
84
88
|
self.error(e)
|
|
89
|
+
|
|
90
|
+
def bottom_toolbar(self) -> HTML:
|
|
91
|
+
sections = "/".join([str(section.name) for section in self.sections])
|
|
92
|
+
back = (
|
|
93
|
+
(' <b><style bg="ansired">back</style></b> ' "to exit the current section,")
|
|
94
|
+
if len(self.sections) > 1
|
|
95
|
+
else ""
|
|
96
|
+
)
|
|
97
|
+
return HTML(
|
|
98
|
+
f"Your are in <strong>{sections}</strong>, type{back} "
|
|
99
|
+
'<b><style bg="ansired">exit</style></b> to exit'
|
|
100
|
+
)
|
|
@@ -12,6 +12,9 @@ if TYPE_CHECKING:
|
|
|
12
12
|
from quantflow.cli.app import QfApp
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
FREQUENCIES = tuple(FMP().historical_frequencies())
|
|
16
|
+
|
|
17
|
+
|
|
15
18
|
class HistoricalPeriod(enum.StrEnum):
|
|
16
19
|
day = "1d"
|
|
17
20
|
week = "1w"
|
|
@@ -115,3 +118,17 @@ class options:
|
|
|
115
118
|
show_default=True,
|
|
116
119
|
help="Historical period",
|
|
117
120
|
)
|
|
121
|
+
index = click.option(
|
|
122
|
+
"-i",
|
|
123
|
+
"--index",
|
|
124
|
+
type=int,
|
|
125
|
+
default=-1,
|
|
126
|
+
help="maturity index",
|
|
127
|
+
)
|
|
128
|
+
frequency = click.option(
|
|
129
|
+
"-f",
|
|
130
|
+
"--frequency",
|
|
131
|
+
type=click.Choice(FREQUENCIES),
|
|
132
|
+
default="",
|
|
133
|
+
help="Frequency of data - if not provided it is daily",
|
|
134
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from asciichartpy import plot
|
|
8
|
+
from cache import AsyncTTL
|
|
9
|
+
from ccy.cli.console import df_to_rich
|
|
10
|
+
|
|
11
|
+
from quantflow.data.deribit import Deribit
|
|
12
|
+
from quantflow.options.surface import VolSurface
|
|
13
|
+
from quantflow.utils.numbers import round_to_step
|
|
14
|
+
|
|
15
|
+
from .base import QuantContext, options, quant_group
|
|
16
|
+
from .stocks import get_prices
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@quant_group()
|
|
20
|
+
def crypto() -> None:
|
|
21
|
+
"""Crypto currencies commands"""
|
|
22
|
+
ctx = QuantContext.current()
|
|
23
|
+
if ctx.invoked_subcommand is None:
|
|
24
|
+
ctx.set_as_section()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@crypto.command()
|
|
28
|
+
@click.argument("currency")
|
|
29
|
+
@options.length
|
|
30
|
+
@options.height
|
|
31
|
+
@options.chart
|
|
32
|
+
def volatility(currency: str, length: int, height: int, chart: bool) -> None:
|
|
33
|
+
"""Provides information about historical volatility for given cryptocurrency"""
|
|
34
|
+
ctx = QuantContext.current()
|
|
35
|
+
df = asyncio.run(get_volatility(ctx, currency))
|
|
36
|
+
df["volatility"] = df["volatility"].map(lambda p: round_to_step(p, "0.01"))
|
|
37
|
+
if chart:
|
|
38
|
+
data = df["volatility"].tolist()[:length]
|
|
39
|
+
ctx.qf.print(plot(data, {"height": height}))
|
|
40
|
+
else:
|
|
41
|
+
ctx.qf.print(df_to_rich(df))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@crypto.command()
|
|
45
|
+
@click.argument("currency")
|
|
46
|
+
def term_structure(currency: str) -> None:
|
|
47
|
+
"""Provides information about the term structure for given cryptocurrency"""
|
|
48
|
+
ctx = QuantContext.current()
|
|
49
|
+
vs = asyncio.run(get_vol_surface(currency))
|
|
50
|
+
ts = vs.term_structure().round({"ttm": 4})
|
|
51
|
+
ts["open_interest"] = ts["open_interest"].map("{:,d}".format)
|
|
52
|
+
ts["volume"] = ts["volume"].map("{:,d}".format)
|
|
53
|
+
ctx.qf.print(df_to_rich(ts))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@crypto.command()
|
|
57
|
+
@click.argument("currency")
|
|
58
|
+
@options.index
|
|
59
|
+
@options.height
|
|
60
|
+
@options.chart
|
|
61
|
+
def implied_vol(currency: str, index: int, height: int, chart: bool) -> None:
|
|
62
|
+
"""Display the Volatility Surface for given cryptocurrency
|
|
63
|
+
at a given maturity index
|
|
64
|
+
"""
|
|
65
|
+
ctx = QuantContext.current()
|
|
66
|
+
vs = asyncio.run(get_vol_surface(currency))
|
|
67
|
+
index_or_none = None if index < 0 else index
|
|
68
|
+
vs.bs(index=index_or_none)
|
|
69
|
+
df = vs.options_df(index=index_or_none)
|
|
70
|
+
if chart:
|
|
71
|
+
data = (df["implied_vol"] * 100).tolist()
|
|
72
|
+
ctx.qf.print(plot(data, {"height": height}))
|
|
73
|
+
else:
|
|
74
|
+
df[["ttm", "moneyness", "moneyness_ttm"]] = df[
|
|
75
|
+
["ttm", "moneyness", "moneyness_ttm"]
|
|
76
|
+
].map("{:.4f}".format)
|
|
77
|
+
df["implied_vol"] = df["implied_vol"].map("{:.2%}".format)
|
|
78
|
+
df["price"] = df["price"].map(lambda p: round_to_step(p, vs.tick_size_options))
|
|
79
|
+
df["forward_price"] = df["forward_price"].map(
|
|
80
|
+
lambda p: round_to_step(p, vs.tick_size_forwards)
|
|
81
|
+
)
|
|
82
|
+
ctx.qf.print(df_to_rich(df))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@crypto.command()
|
|
86
|
+
@click.argument("symbol")
|
|
87
|
+
@options.height
|
|
88
|
+
@options.length
|
|
89
|
+
@options.chart
|
|
90
|
+
@options.frequency
|
|
91
|
+
def prices(symbol: str, height: int, length: int, chart: bool, frequency: str) -> None:
|
|
92
|
+
"""Fetch OHLC prices for given cryptocurrency"""
|
|
93
|
+
ctx = QuantContext.current()
|
|
94
|
+
df = asyncio.run(get_prices(ctx, symbol, frequency))
|
|
95
|
+
if df.empty:
|
|
96
|
+
raise click.UsageError(
|
|
97
|
+
f"No data for {symbol} - are you sure the symbol exists?"
|
|
98
|
+
)
|
|
99
|
+
if chart:
|
|
100
|
+
data = list(reversed(df["close"].tolist()[:length]))
|
|
101
|
+
ctx.qf.print(plot(data, {"height": height}))
|
|
102
|
+
else:
|
|
103
|
+
ctx.qf.print(
|
|
104
|
+
df_to_rich(
|
|
105
|
+
df[["date", "open", "high", "low", "close", "volume"]].sort_values(
|
|
106
|
+
"date"
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def get_volatility(ctx: QuantContext, currency: str) -> pd.DataFrame:
|
|
113
|
+
async with Deribit() as client:
|
|
114
|
+
return await client.get_volatility(params=dict(currency=currency))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@AsyncTTL(time_to_live=10)
|
|
118
|
+
async def get_vol_surface(currency: str) -> VolSurface:
|
|
119
|
+
async with Deribit() as client:
|
|
120
|
+
loader = await client.volatility_surface_loader(currency)
|
|
121
|
+
return loader.surface()
|
|
@@ -11,13 +11,10 @@ from ccy import period as to_period
|
|
|
11
11
|
from ccy.cli.console import df_to_rich
|
|
12
12
|
from ccy.tradingcentres import prevbizday
|
|
13
13
|
|
|
14
|
-
from quantflow.data.fmp import FMP
|
|
15
14
|
from quantflow.utils.dates import utcnow
|
|
16
15
|
|
|
17
16
|
from .base import HistoricalPeriod, QuantContext, options, quant_group
|
|
18
17
|
|
|
19
|
-
FREQUENCIES = tuple(FMP().historical_frequencies())
|
|
20
|
-
|
|
21
18
|
|
|
22
19
|
@quant_group()
|
|
23
20
|
def stocks() -> None:
|
|
@@ -56,13 +53,7 @@ def search(text: str) -> None:
|
|
|
56
53
|
@click.argument("symbol")
|
|
57
54
|
@options.height
|
|
58
55
|
@options.length
|
|
59
|
-
@
|
|
60
|
-
"-f",
|
|
61
|
-
"--frequency",
|
|
62
|
-
type=click.Choice(FREQUENCIES),
|
|
63
|
-
default="",
|
|
64
|
-
help="Frequency of data - if not provided it is daily",
|
|
65
|
-
)
|
|
56
|
+
@options.frequency
|
|
66
57
|
def chart(symbol: str, height: int, length: int, frequency: str) -> None:
|
|
67
58
|
"""Symbol chart"""
|
|
68
59
|
ctx = QuantContext.current()
|
|
@@ -4,11 +4,11 @@ dotenv.load_dotenv()
|
|
|
4
4
|
|
|
5
5
|
try:
|
|
6
6
|
from .app import QfApp
|
|
7
|
-
except ImportError:
|
|
7
|
+
except ImportError as ex:
|
|
8
8
|
raise ImportError(
|
|
9
9
|
"Cannot run qf command line, "
|
|
10
10
|
"quantflow needs to be installed with cli & data extras, "
|
|
11
11
|
"pip install quantflow[cli, data]"
|
|
12
|
-
) from
|
|
12
|
+
) from ex
|
|
13
13
|
|
|
14
14
|
main = QfApp()
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from datetime import datetime, timezone
|
|
2
|
+
from decimal import Decimal
|
|
2
3
|
from typing import Any, cast
|
|
3
4
|
|
|
4
5
|
import pandas as pd
|
|
5
6
|
from dateutil.parser import parse
|
|
6
|
-
from fluid.utils.http_client import AioHttpClient, HttpResponse
|
|
7
|
+
from fluid.utils.http_client import AioHttpClient, HttpResponse, HttpResponseError
|
|
7
8
|
|
|
8
9
|
from quantflow.options.surface import VolSecurityType, VolSurfaceLoader
|
|
9
|
-
from quantflow.utils.numbers import round_to_step
|
|
10
|
+
from quantflow.utils.numbers import round_to_step, to_decimal
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def parse_maturity(v: str) -> datetime:
|
|
@@ -14,6 +15,13 @@ def parse_maturity(v: str) -> datetime:
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class Deribit(AioHttpClient):
|
|
18
|
+
"""Deribit API client
|
|
19
|
+
|
|
20
|
+
Fetch market and static data from `Deribit`_.
|
|
21
|
+
|
|
22
|
+
.. _Deribit: https://docs.deribit.com/
|
|
23
|
+
"""
|
|
24
|
+
|
|
17
25
|
url = "https://www.deribit.com/api/v2"
|
|
18
26
|
|
|
19
27
|
async def get_book_summary_by_instrument(self, **kw: Any) -> list[dict]:
|
|
@@ -38,7 +46,7 @@ class Deribit(AioHttpClient):
|
|
|
38
46
|
return await self.get_path("public/get_historical_volatility", **kw)
|
|
39
47
|
|
|
40
48
|
async def volatility_surface_loader(self, currency: str) -> VolSurfaceLoader:
|
|
41
|
-
"""Create
|
|
49
|
+
"""Create a :class:`.VolSurfaceLoader` for a given crypto-currency"""
|
|
42
50
|
loader = VolSurfaceLoader()
|
|
43
51
|
futures = await self.get_book_summary_by_currency(
|
|
44
52
|
params=dict(currency=currency, kind="future")
|
|
@@ -48,12 +56,13 @@ class Deribit(AioHttpClient):
|
|
|
48
56
|
)
|
|
49
57
|
instruments = await self.get_instruments(params=dict(currency=currency))
|
|
50
58
|
instrument_map = {i["instrument_name"]: i for i in instruments}
|
|
51
|
-
|
|
59
|
+
min_tick_size = Decimal("inf")
|
|
52
60
|
for future in futures:
|
|
53
61
|
if (bid_ := future["bid_price"]) and (ask_ := future["ask_price"]):
|
|
54
62
|
name = future["instrument_name"]
|
|
55
63
|
meta = instrument_map[name]
|
|
56
|
-
tick_size = meta["tick_size"]
|
|
64
|
+
tick_size = to_decimal(meta["tick_size"])
|
|
65
|
+
min_tick_size = min(min_tick_size, tick_size)
|
|
57
66
|
bid = round_to_step(bid_, tick_size)
|
|
58
67
|
ask = round_to_step(ask_, tick_size)
|
|
59
68
|
if meta["settlement_period"] == "perpetual":
|
|
@@ -78,12 +87,15 @@ class Deribit(AioHttpClient):
|
|
|
78
87
|
open_interest=int(future["open_interest"]),
|
|
79
88
|
volume=int(future["volume_usd"]),
|
|
80
89
|
)
|
|
90
|
+
loader.tick_size_forwards = min_tick_size
|
|
81
91
|
|
|
92
|
+
min_tick_size = Decimal("inf")
|
|
82
93
|
for option in options:
|
|
83
94
|
if (bid_ := option["bid_price"]) and (ask_ := option["ask_price"]):
|
|
84
95
|
name = option["instrument_name"]
|
|
85
96
|
meta = instrument_map[name]
|
|
86
|
-
tick_size = meta["tick_size"]
|
|
97
|
+
tick_size = to_decimal(meta["tick_size"])
|
|
98
|
+
min_tick_size = min(min_tick_size, tick_size)
|
|
87
99
|
loader.add_option(
|
|
88
100
|
VolSecurityType.option,
|
|
89
101
|
strike=round_to_step(meta["strike"], tick_size),
|
|
@@ -96,7 +108,7 @@ class Deribit(AioHttpClient):
|
|
|
96
108
|
bid=round_to_step(bid_, tick_size),
|
|
97
109
|
ask=round_to_step(ask_, tick_size),
|
|
98
110
|
)
|
|
99
|
-
|
|
111
|
+
loader.tick_size_options = min_tick_size
|
|
100
112
|
return loader
|
|
101
113
|
|
|
102
114
|
# Internal methods
|
|
@@ -106,6 +118,8 @@ class Deribit(AioHttpClient):
|
|
|
106
118
|
|
|
107
119
|
async def to_result(self, response: HttpResponse) -> list[dict]:
|
|
108
120
|
data = await response.json()
|
|
121
|
+
if "error" in data:
|
|
122
|
+
raise HttpResponseError(response, data["error"])
|
|
109
123
|
return cast(list[dict], data["result"])
|
|
110
124
|
|
|
111
125
|
async def to_df(self, response: HttpResponse) -> pd.DataFrame:
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from fluid.utils.http_client import AioHttpClient
|
|
8
|
+
|
|
9
|
+
URL = (
|
|
10
|
+
"https://www.federalreserve.gov/datadownload/Output.aspx?"
|
|
11
|
+
"rel=H15&series=bf17364827e38702b42a58cf8eaa3f78&lastobs=&"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
maturities = [
|
|
15
|
+
"month_1",
|
|
16
|
+
"month_3",
|
|
17
|
+
"month_6",
|
|
18
|
+
"year_1",
|
|
19
|
+
"year_2",
|
|
20
|
+
"year_3",
|
|
21
|
+
"year_5",
|
|
22
|
+
"year_7",
|
|
23
|
+
"year_10",
|
|
24
|
+
"year_20",
|
|
25
|
+
"year_30",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class FederalReserve(AioHttpClient):
|
|
31
|
+
"""Federal Reserve API client.
|
|
32
|
+
|
|
33
|
+
This class is used to fetch yield curves from the Federal Reserve at
|
|
34
|
+
https://www.federalreserve.gov/datadownload/
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
url: str = "https://www.federalreserve.gov/datadownload/Output.aspx"
|
|
38
|
+
default_params: dict[str, Any] = field(
|
|
39
|
+
default_factory=lambda: {
|
|
40
|
+
"from": "",
|
|
41
|
+
"to": "",
|
|
42
|
+
"lastobs": "",
|
|
43
|
+
"filetype": "csv",
|
|
44
|
+
"label": "include",
|
|
45
|
+
"layout": "seriescolumn",
|
|
46
|
+
"type": "package",
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def yield_curves(self, **params: Any) -> pd.DataFrame:
|
|
51
|
+
"""Get treasury constant maturities rates"""
|
|
52
|
+
params.update(series="bf17364827e38702b42a58cf8eaa3f78", rel="H15")
|
|
53
|
+
data = await self._get_text(params)
|
|
54
|
+
df = pd.read_csv(data, header=5, index_col=None, parse_dates=True)
|
|
55
|
+
df.columns = ["date"] + maturities # type: ignore
|
|
56
|
+
df = df.set_index("date").replace("ND", np.nan)
|
|
57
|
+
return df.dropna(axis=0, how="all").reset_index()
|
|
58
|
+
|
|
59
|
+
async def ref_rates(self, **params: Any) -> pd.DataFrame:
|
|
60
|
+
"""Get policy rates
|
|
61
|
+
|
|
62
|
+
Prior to 2021-07-08 it is the rate on excess reserves (IOER rate)
|
|
63
|
+
After 2021-07-08 it is the rate on reserve balances (IORB rate)
|
|
64
|
+
|
|
65
|
+
The IOER rate was the primary tool used by the Federal Reserve to set
|
|
66
|
+
a floor on the federal funds rate.
|
|
67
|
+
While the Interest rate on required reserves (IORR rate) existed,
|
|
68
|
+
the IOER rate had a more direct impact on market rates,
|
|
69
|
+
as banks typically held far more excess reserves than required reserves.
|
|
70
|
+
Therefore, the IOER rate was more influential
|
|
71
|
+
in the Fed's monetary policy implementation.
|
|
72
|
+
"""
|
|
73
|
+
params.update(series="c27939ee810cb2e929a920a6bd77d9f6", rel="PRATES")
|
|
74
|
+
data = await self._get_text(params)
|
|
75
|
+
df = pd.read_csv(data, header=5, index_col=None, parse_dates=True)
|
|
76
|
+
ioer = df["RESBME_N.D"]
|
|
77
|
+
iorb = df["RESBM_N.D"]
|
|
78
|
+
rate = iorb.combine_first(ioer)
|
|
79
|
+
return pd.DataFrame(
|
|
80
|
+
{
|
|
81
|
+
"date": df["Time Period"],
|
|
82
|
+
"rate": rate,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def _get_text(self, params: dict[str, Any]) -> io.StringIO:
|
|
87
|
+
"""Get parameters for the request."""
|
|
88
|
+
params = {**self.default_params, **params}
|
|
89
|
+
response = await self.get(self.url, params=params, callback=True)
|
|
90
|
+
data = await response.text()
|
|
91
|
+
return io.StringIO(data)
|
|
@@ -8,15 +8,21 @@ from typing import Any, Iterator, cast
|
|
|
8
8
|
import inflection
|
|
9
9
|
import pandas as pd
|
|
10
10
|
from fluid.utils.data import compact_dict
|
|
11
|
+
from fluid.utils.http_client import AioHttpClient
|
|
11
12
|
|
|
12
13
|
from quantflow.utils.dates import isoformat
|
|
13
14
|
from quantflow.utils.numbers import to_decimal
|
|
14
15
|
|
|
15
|
-
from .client import AioHttpClient
|
|
16
|
-
|
|
17
16
|
|
|
18
17
|
@dataclass
|
|
19
18
|
class FMP(AioHttpClient):
|
|
19
|
+
"""Financial Modeling Prep API client
|
|
20
|
+
|
|
21
|
+
Fetch market and financial data from `Financial Modeling Prep`_.
|
|
22
|
+
|
|
23
|
+
.. _Financial Modeling Prep: https://financialmodelingprep.com/developer/docs/
|
|
24
|
+
"""
|
|
25
|
+
|
|
20
26
|
url: str = "https://financialmodelingprep.com/api"
|
|
21
27
|
key: str = field(default_factory=lambda: os.environ.get("FMP_API_KEY", ""))
|
|
22
28
|
|
|
@@ -4,12 +4,18 @@ from enum import StrEnum
|
|
|
4
4
|
from typing import Any, cast
|
|
5
5
|
|
|
6
6
|
import pandas as pd
|
|
7
|
-
|
|
8
|
-
from .client import AioHttpClient
|
|
7
|
+
from fluid.utils.http_client import AioHttpClient
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
@dataclass
|
|
12
11
|
class Fred(AioHttpClient):
|
|
12
|
+
"""Federal Reserve Economic Data API client
|
|
13
|
+
|
|
14
|
+
Fetch economic data from `FRED`_.
|
|
15
|
+
|
|
16
|
+
.. _FRED: https://fred.stlouisfed.org/
|
|
17
|
+
"""
|
|
18
|
+
|
|
13
19
|
url: str = "https://api.stlouisfed.org/fred"
|
|
14
20
|
key: str = field(default_factory=lambda: os.environ.get("FRED_API_KEY", ""))
|
|
15
21
|
|
|
@@ -25,15 +31,19 @@ class Fred(AioHttpClient):
|
|
|
25
31
|
a = "a"
|
|
26
32
|
|
|
27
33
|
async def categiories(self, **kw: Any) -> dict:
|
|
34
|
+
"""Get categories"""
|
|
28
35
|
return await self.get_path("category", **kw)
|
|
29
36
|
|
|
30
37
|
async def subcategories(self, **kw: Any) -> dict:
|
|
38
|
+
"""Get subcategories of a given category"""
|
|
31
39
|
return await self.get_path("category/children", **kw)
|
|
32
40
|
|
|
33
41
|
async def series(self, **kw: Any) -> dict:
|
|
42
|
+
"""Get series of a given category"""
|
|
34
43
|
return await self.get_path("category/series", **kw)
|
|
35
44
|
|
|
36
45
|
async def serie_data(self, *, to_date: bool = False, **kw: Any) -> pd.DataFrame:
|
|
46
|
+
"""Get series data frame"""
|
|
37
47
|
data = await self.get_path("series/observations", **kw)
|
|
38
48
|
df = pd.DataFrame(data["observations"])
|
|
39
49
|
df["value"] = pd.to_numeric(df["value"])
|
|
@@ -2,6 +2,7 @@ from pathlib import Path
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class Vault:
|
|
5
|
+
"""Keeps key-value pairs in a file."""
|
|
5
6
|
|
|
6
7
|
def __init__(self, path: str | Path) -> None:
|
|
7
8
|
self.path = Path(path)
|
|
@@ -17,22 +18,27 @@ class Vault:
|
|
|
17
18
|
return data
|
|
18
19
|
|
|
19
20
|
def add(self, key: str, value: str) -> None:
|
|
21
|
+
"""Add a key-value pair to the vault."""
|
|
20
22
|
self.data[key] = value
|
|
21
23
|
self.save()
|
|
22
24
|
|
|
23
25
|
def delete(self, key: str) -> bool:
|
|
26
|
+
"""Delete a key-value pair from the vault."""
|
|
24
27
|
if self.data.pop(key, None) is not None:
|
|
25
28
|
self.save()
|
|
26
29
|
return True
|
|
27
30
|
return False
|
|
28
31
|
|
|
29
32
|
def get(self, key: str) -> str | None:
|
|
33
|
+
"""Get the value of a key if available otherwise None."""
|
|
30
34
|
return self.data.get(key)
|
|
31
35
|
|
|
32
36
|
def keys(self) -> list[str]:
|
|
37
|
+
"""Get the keys in the vault."""
|
|
33
38
|
return sorted(self.data)
|
|
34
39
|
|
|
35
40
|
def save(self) -> None:
|
|
41
|
+
"""Save the data to the file."""
|
|
36
42
|
with open(self.path, "w") as file:
|
|
37
43
|
for key in sorted(self.data):
|
|
38
44
|
value = self.data[key]
|