quantflow 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quantflow-0.3.0 → quantflow-0.3.1}/PKG-INFO +8 -3
- {quantflow-0.3.0 → quantflow-0.3.1}/pyproject.toml +18 -5
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/__init__.py +1 -1
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/app.py +16 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/commands/base.py +17 -0
- quantflow-0.3.1/quantflow/cli/commands/crypto.py +121 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/commands/stocks.py +1 -10
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/script.py +2 -2
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/data/deribit.py +21 -7
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/data/fmp.py +8 -2
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/data/fred.py +12 -2
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/data/vault.py +6 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/bs.py +37 -12
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/calibration.py +7 -1
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/inputs.py +6 -4
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/pricer.py +4 -2
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/surface.py +144 -32
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/base.py +5 -7
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/bns.py +1 -1
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/cir.py +5 -1
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/heston.py +28 -7
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/jump_diffusion.py +10 -7
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/ou.py +1 -1
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/poisson.py +8 -5
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/weiner.py +1 -1
- quantflow-0.3.1/quantflow/ta/base.py +14 -0
- quantflow-0.3.1/quantflow/ta/ohlc.py +109 -0
- {quantflow-0.3.0/quantflow/utils → quantflow-0.3.1/quantflow/ta}/paths.py +77 -8
- quantflow-0.3.1/quantflow/utils/__init__.py +0 -0
- quantflow-0.3.1/quantflow/utils/dates.py +24 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/readme.md +4 -1
- 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/utils/dates.py +0 -11
- quantflow-0.3.0/quantflow/utils/df.py +0 -71
- quantflow-0.3.0/quantflow/utils/volatility.py +0 -71
- {quantflow-0.3.0 → quantflow-0.3.1}/LICENSE +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/commands/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/commands/fred.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/commands/vault.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/settings.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/data/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/py.typed +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/copula.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/dsp.py +0 -0
- {quantflow-0.3.0/quantflow/utils → quantflow-0.3.1/quantflow/ta}/__init__.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/bins.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/distributions.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/functions.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/interest_rates.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/marginal.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/numbers.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/plot.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/transforms.py +0 -0
- {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: quantflow
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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)
|
|
@@ -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.1"
|
|
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,11 +23,13 @@ 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
30
|
black = "^24.1.1"
|
|
29
31
|
pytest-cov = "^6.0.0"
|
|
30
|
-
mypy = "^1.
|
|
32
|
+
mypy = "^1.14.1"
|
|
31
33
|
ghp-import = "^2.0.2"
|
|
32
34
|
ruff = "^0.8.1"
|
|
33
35
|
pytest-asyncio = "^0.25.0"
|
|
@@ -36,19 +38,29 @@ isort = "^5.13.2"
|
|
|
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:
|
|
@@ -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]
|
|
@@ -20,14 +20,29 @@ def black_put(
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def black_price(
|
|
23
|
-
k: np.ndarray,
|
|
23
|
+
k: np.ndarray,
|
|
24
|
+
sigma: FloatArrayLike,
|
|
25
|
+
ttm: FloatArrayLike,
|
|
26
|
+
s: FloatArrayLike,
|
|
24
27
|
) -> np.ndarray:
|
|
25
|
-
"""Calculate the Black call option
|
|
28
|
+
r"""Calculate the Black call/put option prices in forward terms
|
|
29
|
+
from the following params
|
|
30
|
+
|
|
31
|
+
.. math::
|
|
32
|
+
c &= \frac{C}{F} = N(d1) - e^k N(d2)
|
|
33
|
+
|
|
34
|
+
p &= \frac{C}{F} = -N(-d1) + e^k N(-d2)
|
|
35
|
+
|
|
36
|
+
d1 &= \frac{-k + \frac{\sigma^2 t}{2}}{\sigma \sqrt{t}}
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
d2 &= d1 - \sigma \sqrt{t}
|
|
39
|
+
|
|
40
|
+
:param k: a vector of :math:`\log{\frac{K}{F}}` also known as moneyness
|
|
41
|
+
:param sigma: a corresponding vector of implied volatilities (0.2 for 20%)
|
|
42
|
+
:param ttm: time to maturity
|
|
43
|
+
:param s: the call/put flag, 1 for calls, -1 for puts
|
|
44
|
+
|
|
45
|
+
The results are option prices divided by the forward price.
|
|
31
46
|
"""
|
|
32
47
|
sig2 = sigma * sigma * ttm
|
|
33
48
|
sig = np.sqrt(sig2)
|
|
@@ -37,9 +52,18 @@ def black_price(
|
|
|
37
52
|
|
|
38
53
|
|
|
39
54
|
def black_vega(k: np.ndarray, sigma: np.ndarray, ttm: FloatArrayLike) -> np.ndarray:
|
|
40
|
-
"""Calculate the Black option vega from the
|
|
55
|
+
r"""Calculate the Black option vega from the moneyness,
|
|
41
56
|
volatility and time to maturity.
|
|
42
57
|
|
|
58
|
+
.. math::
|
|
59
|
+
|
|
60
|
+
\nu = \frac{\partial c}{\partial \sigma} =
|
|
61
|
+
\frac{\partial p}{\partial \sigma} = N'(d1) \sqrt{t}
|
|
62
|
+
|
|
63
|
+
:param k: a vector of moneyness, see above
|
|
64
|
+
:param sigma: a corresponding vector of implied volatilities (0.2 for 20%)
|
|
65
|
+
:param ttm: time to maturity
|
|
66
|
+
|
|
43
67
|
Same formula for both calls and puts.
|
|
44
68
|
"""
|
|
45
69
|
sig2 = sigma * sigma * ttm
|
|
@@ -55,12 +79,13 @@ def implied_black_volatility(
|
|
|
55
79
|
initial_sigma: FloatArray,
|
|
56
80
|
call_put: FloatArrayLike,
|
|
57
81
|
) -> RootResults:
|
|
58
|
-
"""Calculate the implied block volatility
|
|
82
|
+
"""Calculate the implied block volatility via Newton's method
|
|
59
83
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
:param k: a vector of log(strikes/forward) also known as moneyness
|
|
85
|
+
:param price: a corresponding vector of option_price/forward
|
|
86
|
+
:param ttm: time to maturity
|
|
87
|
+
:param initial_sigma: a vector of initial volatility guesses
|
|
88
|
+
:param call_put: a vector of call/put flags, 1 for calls, -1 for puts
|
|
64
89
|
"""
|
|
65
90
|
return newton(
|
|
66
91
|
lambda x: black_price(k, x, ttm, call_put) - price,
|
|
@@ -59,10 +59,15 @@ class VolModelCalibration(ABC, Generic[M]):
|
|
|
59
59
|
"""Calibration of a stochastic volatility model"""
|
|
60
60
|
|
|
61
61
|
pricer: OptionPricer[M]
|
|
62
|
+
"""The option pricer for the model"""
|
|
62
63
|
vol_surface: VolSurface[Any]
|
|
64
|
+
"""The volatility surface"""
|
|
63
65
|
minimize_method: str | None = None
|
|
66
|
+
"""The optimization method to use"""
|
|
64
67
|
moneyness_weight: float = 0.5
|
|
68
|
+
"""The weight for moneyness"""
|
|
65
69
|
options: dict[ModelCalibrationEntryKey, OptionEntry] = field(default_factory=dict)
|
|
70
|
+
"""The options to calibrate"""
|
|
66
71
|
|
|
67
72
|
def __post_init__(self) -> None:
|
|
68
73
|
if not self.options:
|
|
@@ -177,7 +182,8 @@ class VolModelCalibration(ABC, Generic[M]):
|
|
|
177
182
|
|
|
178
183
|
@dataclass
|
|
179
184
|
class HestonCalibration(VolModelCalibration[Heston]):
|
|
180
|
-
"""
|
|
185
|
+
"""A :class:`.VolModelCalibration` for the :class:`.Heston`
|
|
186
|
+
stochastic volatility model"""
|
|
181
187
|
|
|
182
188
|
feller_penalize: float = 0.0
|
|
183
189
|
|
|
@@ -10,10 +10,12 @@ from pydantic import BaseModel
|
|
|
10
10
|
P = TypeVar("P")
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class VolSecurityType(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
class VolSecurityType(enum.StrEnum):
|
|
14
|
+
"""Type of security for the volatility surface"""
|
|
15
|
+
|
|
16
|
+
spot = enum.auto()
|
|
17
|
+
forward = enum.auto()
|
|
18
|
+
option = enum.auto()
|
|
17
19
|
|
|
18
20
|
def vol_surface_type(self) -> VolSecurityType:
|
|
19
21
|
return self
|
|
@@ -110,16 +110,18 @@ class OptionPricer(Generic[M]):
|
|
|
110
110
|
model: M
|
|
111
111
|
"""The stochastic process"""
|
|
112
112
|
ttm: dict[int, MaturityPricer] = field(default_factory=dict, repr=False)
|
|
113
|
-
"""Cache for
|
|
113
|
+
"""Cache for :class:`.MaturityPricer`"""
|
|
114
114
|
n: int = 128
|
|
115
115
|
max_moneyness_ttm: float = 1.5
|
|
116
|
+
"""Max moneyness"""
|
|
116
117
|
|
|
117
118
|
def reset(self) -> None:
|
|
118
119
|
"""Clear the cache"""
|
|
119
120
|
self.ttm.clear()
|
|
120
121
|
|
|
121
122
|
def maturity(self, ttm: float, **kwargs: Any) -> MaturityPricer:
|
|
122
|
-
"""Get
|
|
123
|
+
"""Get a :class:`.MaturityPricer` from cache or create
|
|
124
|
+
a new one and return it"""
|
|
123
125
|
ttm_int = int(TTM_FACTOR * ttm)
|
|
124
126
|
if ttm_int not in self.ttm:
|
|
125
127
|
ttmr = ttm_int / TTM_FACTOR
|