quantflow 0.2.9__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.2.9 → quantflow-0.3.1}/PKG-INFO +8 -2
- {quantflow-0.2.9 → quantflow-0.3.1}/pyproject.toml +19 -5
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/__init__.py +1 -1
- quantflow-0.3.1/quantflow/cli/app.py +100 -0
- quantflow-0.3.1/quantflow/cli/commands/__init__.py +18 -0
- quantflow-0.3.1/quantflow/cli/commands/base.py +134 -0
- quantflow-0.3.1/quantflow/cli/commands/crypto.py +121 -0
- quantflow-0.3.1/quantflow/cli/commands/fred.py +118 -0
- quantflow-0.3.1/quantflow/cli/commands/stocks.py +120 -0
- quantflow-0.3.1/quantflow/cli/commands/vault.py +52 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/cli/script.py +2 -2
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/cli/settings.py +1 -0
- quantflow-0.3.1/quantflow/data/deribit.py +129 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/data/fmp.py +62 -4
- quantflow-0.3.1/quantflow/data/fred.py +62 -0
- quantflow-0.3.1/quantflow/data/vault.py +45 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/bs.py +37 -12
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/calibration.py +7 -1
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/inputs.py +6 -4
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/pricer.py +4 -2
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/surface.py +194 -37
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/base.py +5 -7
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/bns.py +1 -1
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/cir.py +5 -1
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/heston.py +28 -7
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/jump_diffusion.py +10 -7
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/ou.py +2 -2
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/poisson.py +8 -5
- {quantflow-0.2.9 → 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.2.9/quantflow/utils → quantflow-0.3.1/quantflow/ta}/paths.py +77 -8
- quantflow-0.3.1/quantflow/utils/__init__.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/bins.py +1 -1
- quantflow-0.3.1/quantflow/utils/dates.py +24 -0
- quantflow-0.3.1/quantflow/utils/numbers.py +45 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/types.py +2 -2
- {quantflow-0.2.9 → quantflow-0.3.1}/readme.md +4 -0
- quantflow-0.2.9/quantflow/cli/app.py +0 -72
- quantflow-0.2.9/quantflow/cli/commands.py +0 -102
- quantflow-0.2.9/quantflow/utils/dates.py +0 -11
- quantflow-0.2.9/quantflow/utils/df.py +0 -72
- quantflow-0.2.9/quantflow/utils/numbers.py +0 -14
- quantflow-0.2.9/quantflow/utils/volatility.py +0 -71
- {quantflow-0.2.9 → quantflow-0.3.1}/LICENSE +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/cli/__init__.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/data/__init__.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/__init__.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/py.typed +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/__init__.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/copula.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/dsp.py +0 -0
- {quantflow-0.2.9/quantflow/utils → quantflow-0.3.1/quantflow/ta}/__init__.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/distributions.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/functions.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/interest_rates.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/marginal.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/plot.py +0 -0
- {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/transforms.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: quantflow
|
|
3
|
-
Version: 0.
|
|
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,9 +53,13 @@ pip install quantflow
|
|
|
51
53
|
|
|
52
54
|
## Modules
|
|
53
55
|
|
|
56
|
+
* [quantflow.cli](https://github.com/quantmind/quantflow/tree/main/quantflow/cli) command line client (requires `quantflow[cli,data]`)
|
|
54
57
|
* [quantflow.data](https://github.com/quantmind/quantflow/tree/main/quantflow/data) data APIs (requires `quantflow[data]`)
|
|
55
58
|
* [quantflow.options](https://github.com/quantmind/quantflow/tree/main/quantflow/options) option pricing and calibration
|
|
56
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
|
+
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
## Command line tools
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "quantflow"
|
|
3
|
-
version = "0.
|
|
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,31 +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
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
|
+
isort = "^5.13.2"
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
[tool.poetry.extras]
|
|
37
40
|
data = ["aio-fluid"]
|
|
38
|
-
cli = [
|
|
41
|
+
cli = [
|
|
42
|
+
"asciichartpy",
|
|
43
|
+
"async-cache",
|
|
44
|
+
"prompt-toolkit",
|
|
45
|
+
"rich",
|
|
46
|
+
"click",
|
|
47
|
+
"holidays"
|
|
48
|
+
]
|
|
39
49
|
|
|
40
50
|
[tool.poetry.group.book]
|
|
41
51
|
optional = true
|
|
42
52
|
|
|
43
53
|
[tool.poetry.group.book.dependencies]
|
|
44
54
|
jupyter-book = "^1.0.0"
|
|
45
|
-
nbconvert = "^7.16.3"
|
|
46
55
|
jupytext = "^1.13.8"
|
|
47
56
|
plotly = "^5.20.0"
|
|
48
57
|
jupyterlab = "^4.0.2"
|
|
49
58
|
sympy = "^1.12"
|
|
50
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"
|
|
51
64
|
|
|
52
65
|
[tool.poetry.scripts]
|
|
53
66
|
qf = "quantflow.cli.script:main"
|
|
@@ -84,6 +97,7 @@ warn_no_return = true
|
|
|
84
97
|
[[tool.mypy.overrides]]
|
|
85
98
|
module = [
|
|
86
99
|
"asciichartpy.*",
|
|
100
|
+
"cache.*",
|
|
87
101
|
"quantflow_tests.*",
|
|
88
102
|
"IPython.*",
|
|
89
103
|
"pandas.*",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from functools import partial
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from fluid.utils.http_client import HttpResponseError
|
|
8
|
+
from prompt_toolkit import PromptSession
|
|
9
|
+
from prompt_toolkit.completion import NestedCompleter
|
|
10
|
+
from prompt_toolkit.formatted_text import HTML
|
|
11
|
+
from prompt_toolkit.history import FileHistory
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from quantflow.data.vault import Vault
|
|
16
|
+
|
|
17
|
+
from . import settings
|
|
18
|
+
from .commands import quantflow
|
|
19
|
+
from .commands.base import QuantGroup
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class QfApp:
|
|
24
|
+
console: Console = field(default_factory=Console)
|
|
25
|
+
vault: Vault = field(default_factory=partial(Vault, settings.VAULT_FILE_PATH))
|
|
26
|
+
sections: list[QuantGroup] = field(default_factory=lambda: [quantflow])
|
|
27
|
+
|
|
28
|
+
def __call__(self) -> None:
|
|
29
|
+
os.makedirs(settings.SETTINGS_DIRECTORY, exist_ok=True)
|
|
30
|
+
history = FileHistory(str(settings.HIST_FILE_PATH))
|
|
31
|
+
session: PromptSession = PromptSession(history=history)
|
|
32
|
+
|
|
33
|
+
self.print("Welcome to QuantFlow!", style="bold green")
|
|
34
|
+
self.handle_command("help")
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
while True:
|
|
38
|
+
try:
|
|
39
|
+
text = session.prompt(
|
|
40
|
+
self.prompt_message(),
|
|
41
|
+
completer=self.prompt_completer(),
|
|
42
|
+
complete_while_typing=True,
|
|
43
|
+
bottom_toolbar=self.bottom_toolbar,
|
|
44
|
+
)
|
|
45
|
+
except KeyboardInterrupt:
|
|
46
|
+
break
|
|
47
|
+
else:
|
|
48
|
+
self.handle_command(text)
|
|
49
|
+
except click.Abort:
|
|
50
|
+
self.console.print(Text("Bye!", style="bold magenta"))
|
|
51
|
+
|
|
52
|
+
def prompt_message(self) -> str:
|
|
53
|
+
name = ":".join([str(section.name) for section in self.sections])
|
|
54
|
+
return f"{name} > "
|
|
55
|
+
|
|
56
|
+
def prompt_completer(self) -> NestedCompleter:
|
|
57
|
+
return NestedCompleter.from_nested_dict(
|
|
58
|
+
{command: None for command in self.sections[-1].commands}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def set_section(self, section: QuantGroup) -> None:
|
|
62
|
+
self.sections.append(section)
|
|
63
|
+
|
|
64
|
+
def back(self) -> None:
|
|
65
|
+
self.sections.pop()
|
|
66
|
+
|
|
67
|
+
def print(self, text_alike: Any, style: str = "") -> None:
|
|
68
|
+
if isinstance(text_alike, str):
|
|
69
|
+
style = style or "cyan"
|
|
70
|
+
text_alike = Text(f"\n{text_alike}\n", style="cyan")
|
|
71
|
+
self.console.print(text_alike)
|
|
72
|
+
|
|
73
|
+
def error(self, err: str | Exception) -> None:
|
|
74
|
+
self.console.print(Text(f"\n{err}\n", style="bold red"))
|
|
75
|
+
|
|
76
|
+
def handle_command(self, text: str) -> None:
|
|
77
|
+
if not text:
|
|
78
|
+
return
|
|
79
|
+
command = self.sections[-1]
|
|
80
|
+
try:
|
|
81
|
+
command.main(text.split(), standalone_mode=False, obj=self)
|
|
82
|
+
except (
|
|
83
|
+
click.exceptions.MissingParameter,
|
|
84
|
+
click.exceptions.NoSuchOption,
|
|
85
|
+
click.exceptions.UsageError,
|
|
86
|
+
HttpResponseError,
|
|
87
|
+
) as e:
|
|
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
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .base import QuantContext, quant_group
|
|
2
|
+
from .crypto import crypto
|
|
3
|
+
from .fred import fred
|
|
4
|
+
from .stocks import stocks
|
|
5
|
+
from .vault import vault
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@quant_group()
|
|
9
|
+
def quantflow() -> None:
|
|
10
|
+
ctx = QuantContext.current()
|
|
11
|
+
if ctx.invoked_subcommand is None:
|
|
12
|
+
ctx.qf.print(ctx.get_help())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
quantflow.add_command(vault)
|
|
16
|
+
quantflow.add_command(crypto)
|
|
17
|
+
quantflow.add_command(stocks)
|
|
18
|
+
quantflow.add_command(fred)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Self, cast
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from quantflow.data.fmp import FMP
|
|
9
|
+
from quantflow.data.fred import Fred
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from quantflow.cli.app import QfApp
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
FREQUENCIES = tuple(FMP().historical_frequencies())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HistoricalPeriod(enum.StrEnum):
|
|
19
|
+
day = "1d"
|
|
20
|
+
week = "1w"
|
|
21
|
+
month = "1m"
|
|
22
|
+
three_months = "3m"
|
|
23
|
+
six_months = "6m"
|
|
24
|
+
year = "1y"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class QuantContext(click.Context):
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def current(cls) -> Self:
|
|
31
|
+
return cast(Self, click.get_current_context())
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def qf(self) -> QfApp:
|
|
35
|
+
return self.obj # type: ignore
|
|
36
|
+
|
|
37
|
+
def set_as_section(self) -> None:
|
|
38
|
+
group = cast(QuantGroup, self.command)
|
|
39
|
+
group.add_command(back)
|
|
40
|
+
self.qf.set_section(group)
|
|
41
|
+
self.qf.print(self.get_help())
|
|
42
|
+
|
|
43
|
+
def fmp(self) -> FMP:
|
|
44
|
+
if key := self.qf.vault.get("fmp"):
|
|
45
|
+
return FMP(key=key)
|
|
46
|
+
else:
|
|
47
|
+
raise click.UsageError("No FMP API key found")
|
|
48
|
+
|
|
49
|
+
def fred(self) -> Fred:
|
|
50
|
+
if key := self.qf.vault.get("fred"):
|
|
51
|
+
return Fred(key=key)
|
|
52
|
+
else:
|
|
53
|
+
raise click.UsageError("No FRED API key found")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class QuantCommand(click.Command):
|
|
57
|
+
context_class = QuantContext
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class QuantGroup(click.Group):
|
|
61
|
+
context_class = QuantContext
|
|
62
|
+
command_class = QuantCommand
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@click.command(cls=QuantCommand)
|
|
66
|
+
def exit() -> None:
|
|
67
|
+
"""Exit the program"""
|
|
68
|
+
raise click.Abort()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@click.command(cls=QuantCommand)
|
|
72
|
+
def help() -> None:
|
|
73
|
+
"""display the commands"""
|
|
74
|
+
if ctx := QuantContext.current().parent:
|
|
75
|
+
cast(QuantContext, ctx).qf.print(ctx.get_help())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@click.command(cls=QuantCommand)
|
|
79
|
+
def back() -> None:
|
|
80
|
+
"""Exit the current section"""
|
|
81
|
+
ctx = QuantContext.current()
|
|
82
|
+
ctx.qf.back()
|
|
83
|
+
ctx.qf.handle_command("help")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def quant_group() -> Any:
|
|
87
|
+
return click.group(
|
|
88
|
+
cls=QuantGroup,
|
|
89
|
+
commands=[exit, help],
|
|
90
|
+
invoke_without_command=True,
|
|
91
|
+
add_help_option=False,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class options:
|
|
96
|
+
length = click.option(
|
|
97
|
+
"-l",
|
|
98
|
+
"--length",
|
|
99
|
+
type=int,
|
|
100
|
+
default=100,
|
|
101
|
+
show_default=True,
|
|
102
|
+
help="Number of data points",
|
|
103
|
+
)
|
|
104
|
+
height = click.option(
|
|
105
|
+
"-h",
|
|
106
|
+
"--height",
|
|
107
|
+
type=int,
|
|
108
|
+
default=20,
|
|
109
|
+
show_default=True,
|
|
110
|
+
help="Chart height",
|
|
111
|
+
)
|
|
112
|
+
chart = click.option("-c", "--chart", is_flag=True, help="Display chart")
|
|
113
|
+
period = click.option(
|
|
114
|
+
"-p",
|
|
115
|
+
"--period",
|
|
116
|
+
type=click.Choice(tuple(p.value for p in HistoricalPeriod)),
|
|
117
|
+
default="1d",
|
|
118
|
+
show_default=True,
|
|
119
|
+
help="Historical period",
|
|
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()
|
|
@@ -0,0 +1,118 @@
|
|
|
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 ccy.cli.console import df_to_rich
|
|
9
|
+
from fluid.utils.data import compact_dict
|
|
10
|
+
from fluid.utils.http_client import HttpResponseError
|
|
11
|
+
|
|
12
|
+
from quantflow.data.fred import Fred
|
|
13
|
+
|
|
14
|
+
from .base import QuantContext, options, quant_group
|
|
15
|
+
|
|
16
|
+
FREQUENCIES = tuple(Fred.freq)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@quant_group()
|
|
20
|
+
def fred() -> None:
|
|
21
|
+
"""Federal Reserve of St. Louis data commands"""
|
|
22
|
+
ctx = QuantContext.current()
|
|
23
|
+
if ctx.invoked_subcommand is None:
|
|
24
|
+
ctx.set_as_section()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@fred.command()
|
|
28
|
+
@click.argument("category-id", required=False)
|
|
29
|
+
def subcategories(category_id: str | None = None) -> None:
|
|
30
|
+
"""List subcategories for a Fred category"""
|
|
31
|
+
ctx = QuantContext.current()
|
|
32
|
+
try:
|
|
33
|
+
data = asyncio.run(get_subcategories(ctx, category_id))
|
|
34
|
+
except HttpResponseError as e:
|
|
35
|
+
ctx.qf.error(e)
|
|
36
|
+
else:
|
|
37
|
+
df = pd.DataFrame(data["categories"], columns=["id", "name"])
|
|
38
|
+
ctx.qf.print(df_to_rich(df))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@fred.command()
|
|
42
|
+
@click.argument("category-id")
|
|
43
|
+
@click.option("-j", "--json", is_flag=True, help="Output as JSON")
|
|
44
|
+
def series(category_id: str, json: bool = False) -> None:
|
|
45
|
+
"""List series for a Fred category"""
|
|
46
|
+
ctx = QuantContext.current()
|
|
47
|
+
try:
|
|
48
|
+
data = asyncio.run(get_series(ctx, category_id))
|
|
49
|
+
except HttpResponseError as e:
|
|
50
|
+
ctx.qf.error(e)
|
|
51
|
+
else:
|
|
52
|
+
if json:
|
|
53
|
+
ctx.qf.print(data)
|
|
54
|
+
else:
|
|
55
|
+
df = pd.DataFrame(
|
|
56
|
+
data["seriess"],
|
|
57
|
+
columns=[
|
|
58
|
+
"id",
|
|
59
|
+
"popularity",
|
|
60
|
+
"title",
|
|
61
|
+
"frequency",
|
|
62
|
+
"observation_start",
|
|
63
|
+
"observation_end",
|
|
64
|
+
],
|
|
65
|
+
).sort_values("popularity", ascending=False)
|
|
66
|
+
ctx.qf.print(df_to_rich(df))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@fred.command()
|
|
70
|
+
@click.argument("series-id")
|
|
71
|
+
@options.length
|
|
72
|
+
@options.height
|
|
73
|
+
@options.chart
|
|
74
|
+
@click.option(
|
|
75
|
+
"-f",
|
|
76
|
+
"--frequency",
|
|
77
|
+
type=click.Choice(FREQUENCIES),
|
|
78
|
+
default="d",
|
|
79
|
+
show_default=True,
|
|
80
|
+
help="Frequency of data",
|
|
81
|
+
)
|
|
82
|
+
def data(series_id: str, length: int, height: int, chart: bool, frequency: str) -> None:
|
|
83
|
+
"""Display a series data"""
|
|
84
|
+
ctx = QuantContext.current()
|
|
85
|
+
try:
|
|
86
|
+
df = asyncio.run(get_serie_data(ctx, series_id, length, frequency))
|
|
87
|
+
except HttpResponseError as e:
|
|
88
|
+
ctx.qf.error(e)
|
|
89
|
+
else:
|
|
90
|
+
if chart:
|
|
91
|
+
data = list(reversed(df["value"].tolist()[:length]))
|
|
92
|
+
ctx.qf.print(plot(data, {"height": height}))
|
|
93
|
+
else:
|
|
94
|
+
ctx.qf.print(df_to_rich(df))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def get_subcategories(ctx: QuantContext, category_id: str | None) -> dict:
|
|
98
|
+
async with ctx.fred() as cli:
|
|
99
|
+
return await cli.subcategories(params=compact_dict(category_id=category_id))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def get_series(ctx: QuantContext, category_id: str) -> dict:
|
|
103
|
+
async with ctx.fred() as cli:
|
|
104
|
+
return await cli.series(params=compact_dict(category_id=category_id))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def get_serie_data(
|
|
108
|
+
ctx: QuantContext, series_id: str, length: int, frequency: str
|
|
109
|
+
) -> dict:
|
|
110
|
+
async with ctx.fred() as cli:
|
|
111
|
+
return await cli.serie_data(
|
|
112
|
+
params=dict(
|
|
113
|
+
series_id=series_id,
|
|
114
|
+
limit=length,
|
|
115
|
+
frequency=frequency,
|
|
116
|
+
sort_order="desc",
|
|
117
|
+
)
|
|
118
|
+
)
|