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.
Files changed (58) hide show
  1. {quantflow-0.3.0 → quantflow-0.3.1}/PKG-INFO +8 -3
  2. {quantflow-0.3.0 → quantflow-0.3.1}/pyproject.toml +18 -5
  3. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/__init__.py +1 -1
  4. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/app.py +16 -0
  5. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/commands/base.py +17 -0
  6. quantflow-0.3.1/quantflow/cli/commands/crypto.py +121 -0
  7. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/commands/stocks.py +1 -10
  8. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/script.py +2 -2
  9. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/data/deribit.py +21 -7
  10. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/data/fmp.py +8 -2
  11. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/data/fred.py +12 -2
  12. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/data/vault.py +6 -0
  13. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/bs.py +37 -12
  14. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/calibration.py +7 -1
  15. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/inputs.py +6 -4
  16. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/pricer.py +4 -2
  17. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/surface.py +144 -32
  18. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/base.py +5 -7
  19. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/bns.py +1 -1
  20. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/cir.py +5 -1
  21. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/heston.py +28 -7
  22. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/jump_diffusion.py +10 -7
  23. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/ou.py +1 -1
  24. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/poisson.py +8 -5
  25. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/weiner.py +1 -1
  26. quantflow-0.3.1/quantflow/ta/base.py +14 -0
  27. quantflow-0.3.1/quantflow/ta/ohlc.py +109 -0
  28. {quantflow-0.3.0/quantflow/utils → quantflow-0.3.1/quantflow/ta}/paths.py +77 -8
  29. quantflow-0.3.1/quantflow/utils/__init__.py +0 -0
  30. quantflow-0.3.1/quantflow/utils/dates.py +24 -0
  31. {quantflow-0.3.0 → quantflow-0.3.1}/readme.md +4 -1
  32. quantflow-0.3.0/quantflow/cli/commands/crypto.py +0 -41
  33. quantflow-0.3.0/quantflow/data/client.py +0 -4
  34. quantflow-0.3.0/quantflow/utils/dates.py +0 -11
  35. quantflow-0.3.0/quantflow/utils/df.py +0 -71
  36. quantflow-0.3.0/quantflow/utils/volatility.py +0 -71
  37. {quantflow-0.3.0 → quantflow-0.3.1}/LICENSE +0 -0
  38. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/__init__.py +0 -0
  39. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/commands/__init__.py +0 -0
  40. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/commands/fred.py +0 -0
  41. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/commands/vault.py +0 -0
  42. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/cli/settings.py +0 -0
  43. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/data/__init__.py +0 -0
  44. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/options/__init__.py +0 -0
  45. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/py.typed +0 -0
  46. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/__init__.py +0 -0
  47. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/copula.py +0 -0
  48. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/sp/dsp.py +0 -0
  49. {quantflow-0.3.0/quantflow/utils → quantflow-0.3.1/quantflow/ta}/__init__.py +0 -0
  50. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/bins.py +0 -0
  51. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/distributions.py +0 -0
  52. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/functions.py +0 -0
  53. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/interest_rates.py +0 -0
  54. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/marginal.py +0 -0
  55. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/numbers.py +0 -0
  56. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/plot.py +0 -0
  57. {quantflow-0.3.0 → quantflow-0.3.1}/quantflow/utils/transforms.py +0 -0
  58. {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.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: 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,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) aommand line client (requires `quantflow[cli,data]`)
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.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.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,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.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,19 +38,29 @@ isort = "^5.13.2"
36
38
 
37
39
  [tool.poetry.extras]
38
40
  data = ["aio-fluid"]
39
- cli = ["asciichartpy", "prompt-toolkit", "rich", "click"]
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.*",
@@ -1,3 +1,3 @@
1
1
  """Quantitative analysis and pricing"""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.1"
@@ -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
- @click.option(
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 None
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 the volatility surface loader for a given crypto-currency"""
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, sigma: FloatArrayLike, ttm: FloatArrayLike, s: FloatArrayLike
23
+ k: np.ndarray,
24
+ sigma: FloatArrayLike,
25
+ ttm: FloatArrayLike,
26
+ s: FloatArrayLike,
24
27
  ) -> np.ndarray:
25
- """Calculate the Black call option price from
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
- 1) a vector of log(strikes/forward)
28
- 2) a corresponding vector of implied volatilities (0.2 for 20%)
29
- 3) time to maturity
30
- 4) s as the call/put flag, 1 for calls, -1 for puts
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 log strike,
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 from
82
+ """Calculate the implied block volatility via Newton's method
59
83
 
60
- 1) a vector of log(strikes/forward)
61
- 2) a corresponding vector of call_price/forward
62
- 3) time to maturity and
63
- 4) initial volatility guess
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
- """Calibration of a stochastic volatility model"""
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(str, enum.Enum):
14
- spot = "spot"
15
- forward = "forward"
16
- option = "option"
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 the maturity pricer"""
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 the maturity cache or create a new one and return it"""
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