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.
Files changed (59) hide show
  1. {quantflow-0.2.9 → quantflow-0.3.1}/PKG-INFO +8 -2
  2. {quantflow-0.2.9 → quantflow-0.3.1}/pyproject.toml +19 -5
  3. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/__init__.py +1 -1
  4. quantflow-0.3.1/quantflow/cli/app.py +100 -0
  5. quantflow-0.3.1/quantflow/cli/commands/__init__.py +18 -0
  6. quantflow-0.3.1/quantflow/cli/commands/base.py +134 -0
  7. quantflow-0.3.1/quantflow/cli/commands/crypto.py +121 -0
  8. quantflow-0.3.1/quantflow/cli/commands/fred.py +118 -0
  9. quantflow-0.3.1/quantflow/cli/commands/stocks.py +120 -0
  10. quantflow-0.3.1/quantflow/cli/commands/vault.py +52 -0
  11. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/cli/script.py +2 -2
  12. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/cli/settings.py +1 -0
  13. quantflow-0.3.1/quantflow/data/deribit.py +129 -0
  14. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/data/fmp.py +62 -4
  15. quantflow-0.3.1/quantflow/data/fred.py +62 -0
  16. quantflow-0.3.1/quantflow/data/vault.py +45 -0
  17. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/bs.py +37 -12
  18. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/calibration.py +7 -1
  19. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/inputs.py +6 -4
  20. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/pricer.py +4 -2
  21. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/surface.py +194 -37
  22. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/base.py +5 -7
  23. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/bns.py +1 -1
  24. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/cir.py +5 -1
  25. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/heston.py +28 -7
  26. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/jump_diffusion.py +10 -7
  27. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/ou.py +2 -2
  28. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/poisson.py +8 -5
  29. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/weiner.py +1 -1
  30. quantflow-0.3.1/quantflow/ta/base.py +14 -0
  31. quantflow-0.3.1/quantflow/ta/ohlc.py +109 -0
  32. {quantflow-0.2.9/quantflow/utils → quantflow-0.3.1/quantflow/ta}/paths.py +77 -8
  33. quantflow-0.3.1/quantflow/utils/__init__.py +0 -0
  34. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/bins.py +1 -1
  35. quantflow-0.3.1/quantflow/utils/dates.py +24 -0
  36. quantflow-0.3.1/quantflow/utils/numbers.py +45 -0
  37. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/types.py +2 -2
  38. {quantflow-0.2.9 → quantflow-0.3.1}/readme.md +4 -0
  39. quantflow-0.2.9/quantflow/cli/app.py +0 -72
  40. quantflow-0.2.9/quantflow/cli/commands.py +0 -102
  41. quantflow-0.2.9/quantflow/utils/dates.py +0 -11
  42. quantflow-0.2.9/quantflow/utils/df.py +0 -72
  43. quantflow-0.2.9/quantflow/utils/numbers.py +0 -14
  44. quantflow-0.2.9/quantflow/utils/volatility.py +0 -71
  45. {quantflow-0.2.9 → quantflow-0.3.1}/LICENSE +0 -0
  46. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/cli/__init__.py +0 -0
  47. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/data/__init__.py +0 -0
  48. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/options/__init__.py +0 -0
  49. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/py.typed +0 -0
  50. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/__init__.py +0 -0
  51. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/copula.py +0 -0
  52. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/sp/dsp.py +0 -0
  53. {quantflow-0.2.9/quantflow/utils → quantflow-0.3.1/quantflow/ta}/__init__.py +0 -0
  54. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/distributions.py +0 -0
  55. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/functions.py +0 -0
  56. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/interest_rates.py +0 -0
  57. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/marginal.py +0 -0
  58. {quantflow-0.2.9 → quantflow-0.3.1}/quantflow/utils/plot.py +0 -0
  59. {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.2.9
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: ccy (==1.6.0)
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.2.9"
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.6.0"}
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.13.0"
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 = ["asciichartpy", "prompt-toolkit", "rich", "click"]
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.*",
@@ -1,3 +1,3 @@
1
1
  """Quantitative analysis and pricing"""
2
2
 
3
- __version__ = "0.2.9"
3
+ __version__ = "0.3.1"
@@ -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
+ )