quantflow 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {quantflow-0.3.0 → quantflow-0.3.2}/PKG-INFO +10 -5
  2. {quantflow-0.3.0 → quantflow-0.3.2}/pyproject.toml +22 -9
  3. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/__init__.py +1 -1
  4. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/app.py +16 -0
  5. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/commands/base.py +17 -0
  6. quantflow-0.3.2/quantflow/cli/commands/crypto.py +121 -0
  7. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/commands/stocks.py +1 -10
  8. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/script.py +2 -2
  9. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/data/deribit.py +21 -7
  10. quantflow-0.3.2/quantflow/data/fed.py +91 -0
  11. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/data/fmp.py +8 -2
  12. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/data/fred.py +12 -2
  13. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/data/vault.py +6 -0
  14. quantflow-0.3.2/quantflow/options/bs.py +122 -0
  15. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/options/calibration.py +116 -16
  16. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/options/inputs.py +6 -4
  17. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/options/pricer.py +24 -11
  18. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/options/surface.py +150 -34
  19. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/base.py +7 -7
  20. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/bns.py +1 -1
  21. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/cir.py +5 -1
  22. quantflow-0.3.2/quantflow/sp/heston.py +205 -0
  23. quantflow-0.3.2/quantflow/sp/jump_diffusion.py +93 -0
  24. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/ou.py +15 -3
  25. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/poisson.py +57 -11
  26. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/weiner.py +1 -1
  27. quantflow-0.3.2/quantflow/ta/base.py +14 -0
  28. quantflow-0.3.2/quantflow/ta/ohlc.py +109 -0
  29. {quantflow-0.3.0/quantflow/utils → quantflow-0.3.2/quantflow/ta}/paths.py +77 -8
  30. quantflow-0.3.2/quantflow/utils/__init__.py +0 -0
  31. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/bins.py +14 -0
  32. quantflow-0.3.2/quantflow/utils/dates.py +24 -0
  33. quantflow-0.3.2/quantflow/utils/distributions.py +254 -0
  34. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/marginal.py +3 -5
  35. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/plot.py +26 -12
  36. {quantflow-0.3.0 → quantflow-0.3.2}/readme.md +5 -2
  37. quantflow-0.3.0/quantflow/cli/commands/crypto.py +0 -41
  38. quantflow-0.3.0/quantflow/data/client.py +0 -4
  39. quantflow-0.3.0/quantflow/options/bs.py +0 -70
  40. quantflow-0.3.0/quantflow/sp/heston.py +0 -133
  41. quantflow-0.3.0/quantflow/sp/jump_diffusion.py +0 -83
  42. quantflow-0.3.0/quantflow/utils/dates.py +0 -11
  43. quantflow-0.3.0/quantflow/utils/df.py +0 -71
  44. quantflow-0.3.0/quantflow/utils/distributions.py +0 -125
  45. quantflow-0.3.0/quantflow/utils/volatility.py +0 -71
  46. {quantflow-0.3.0 → quantflow-0.3.2}/LICENSE +0 -0
  47. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/__init__.py +0 -0
  48. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/commands/__init__.py +0 -0
  49. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/commands/fred.py +0 -0
  50. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/commands/vault.py +0 -0
  51. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/cli/settings.py +0 -0
  52. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/data/__init__.py +0 -0
  53. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/options/__init__.py +0 -0
  54. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/py.typed +0 -0
  55. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/__init__.py +0 -0
  56. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/copula.py +0 -0
  57. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/sp/dsp.py +0 -0
  58. {quantflow-0.3.0/quantflow/utils → quantflow-0.3.2/quantflow/ta}/__init__.py +0 -0
  59. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/functions.py +0 -0
  60. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/interest_rates.py +0 -0
  61. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/numbers.py +0 -0
  62. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/transforms.py +0 -0
  63. {quantflow-0.3.0 → quantflow-0.3.2}/quantflow/utils/types.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: quantflow
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: quantitative analysis
5
5
  License: BSD-3-Clause
6
6
  Author: Luca
@@ -15,8 +15,10 @@ Provides-Extra: cli
15
15
  Provides-Extra: data
16
16
  Requires-Dist: aio-fluid[http] (>=1.2.1,<2.0.0) ; extra == "data"
17
17
  Requires-Dist: asciichartpy (>=1.5.25,<2.0.0) ; extra == "cli"
18
- Requires-Dist: 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)
@@ -38,7 +40,7 @@ Description-Content-Type: text/markdown
38
40
 
39
41
  Quantitative analysis and pricing tools.
40
42
 
41
- Documentation is available as [quantflow jupyter book](https://quantmind.github.io/quantflow/).
43
+ Documentation is available as [quantflow jupyter book](https://quantflow.quantmind.com).
42
44
 
43
45
  ## Installation
44
46
 
@@ -51,10 +53,13 @@ pip install quantflow
51
53
 
52
54
  ## Modules
53
55
 
54
- * [quantflow.cli](https://github.com/quantmind/quantflow/tree/main/quantflow/cli) 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.2"
4
4
  description = "quantitative analysis"
5
5
  authors = ["Luca <luca@quantmind.com>"]
6
6
  license = "BSD-3-Clause"
@@ -15,7 +15,7 @@ Documentation = "https://quantmind.github.io/quantflow/"
15
15
  python = ">=3.11"
16
16
  scipy = "^1.14.1"
17
17
  pydantic = "^2.0.2"
18
- ccy = {version="1.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,32 +23,44 @@ prompt-toolkit = { version = "^3.0.43", optional = true }
23
23
  aio-fluid = {version = "^1.2.1", extras=["http"], optional = true}
24
24
  rich = {version = "^13.9.4", optional = true}
25
25
  click = {version = "^8.1.7", optional = true}
26
+ holidays = {version = "^0.63", optional = true}
27
+ async-cache = {version = "^1.1.1", optional = true}
26
28
 
27
29
  [tool.poetry.group.dev.dependencies]
28
- black = "^24.1.1"
30
+ black = "^25.1.0"
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
- ruff = "^0.8.1"
33
- pytest-asyncio = "^0.25.0"
34
- isort = "^5.13.2"
34
+ ruff = "^0.11.2"
35
+ pytest-asyncio = "^0.26.0"
36
+ isort = "^6.0.1"
35
37
 
36
38
 
37
39
  [tool.poetry.extras]
38
40
  data = ["aio-fluid"]
39
- cli = ["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.2"
@@ -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:
@@ -0,0 +1,91 @@
1
+ import io
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from fluid.utils.http_client import AioHttpClient
8
+
9
+ URL = (
10
+ "https://www.federalreserve.gov/datadownload/Output.aspx?"
11
+ "rel=H15&series=bf17364827e38702b42a58cf8eaa3f78&lastobs=&"
12
+ )
13
+
14
+ maturities = [
15
+ "month_1",
16
+ "month_3",
17
+ "month_6",
18
+ "year_1",
19
+ "year_2",
20
+ "year_3",
21
+ "year_5",
22
+ "year_7",
23
+ "year_10",
24
+ "year_20",
25
+ "year_30",
26
+ ]
27
+
28
+
29
+ @dataclass
30
+ class FederalReserve(AioHttpClient):
31
+ """Federal Reserve API client.
32
+
33
+ This class is used to fetch yield curves from the Federal Reserve at
34
+ https://www.federalreserve.gov/datadownload/
35
+ """
36
+
37
+ url: str = "https://www.federalreserve.gov/datadownload/Output.aspx"
38
+ default_params: dict[str, Any] = field(
39
+ default_factory=lambda: {
40
+ "from": "",
41
+ "to": "",
42
+ "lastobs": "",
43
+ "filetype": "csv",
44
+ "label": "include",
45
+ "layout": "seriescolumn",
46
+ "type": "package",
47
+ }
48
+ )
49
+
50
+ async def yield_curves(self, **params: Any) -> pd.DataFrame:
51
+ """Get treasury constant maturities rates"""
52
+ params.update(series="bf17364827e38702b42a58cf8eaa3f78", rel="H15")
53
+ data = await self._get_text(params)
54
+ df = pd.read_csv(data, header=5, index_col=None, parse_dates=True)
55
+ df.columns = ["date"] + maturities # type: ignore
56
+ df = df.set_index("date").replace("ND", np.nan)
57
+ return df.dropna(axis=0, how="all").reset_index()
58
+
59
+ async def ref_rates(self, **params: Any) -> pd.DataFrame:
60
+ """Get policy rates
61
+
62
+ Prior to 2021-07-08 it is the rate on excess reserves (IOER rate)
63
+ After 2021-07-08 it is the rate on reserve balances (IORB rate)
64
+
65
+ The IOER rate was the primary tool used by the Federal Reserve to set
66
+ a floor on the federal funds rate.
67
+ While the Interest rate on required reserves (IORR rate) existed,
68
+ the IOER rate had a more direct impact on market rates,
69
+ as banks typically held far more excess reserves than required reserves.
70
+ Therefore, the IOER rate was more influential
71
+ in the Fed's monetary policy implementation.
72
+ """
73
+ params.update(series="c27939ee810cb2e929a920a6bd77d9f6", rel="PRATES")
74
+ data = await self._get_text(params)
75
+ df = pd.read_csv(data, header=5, index_col=None, parse_dates=True)
76
+ ioer = df["RESBME_N.D"]
77
+ iorb = df["RESBM_N.D"]
78
+ rate = iorb.combine_first(ioer)
79
+ return pd.DataFrame(
80
+ {
81
+ "date": df["Time Period"],
82
+ "rate": rate,
83
+ }
84
+ )
85
+
86
+ async def _get_text(self, params: dict[str, Any]) -> io.StringIO:
87
+ """Get parameters for the request."""
88
+ params = {**self.default_params, **params}
89
+ response = await self.get(self.url, params=params, callback=True)
90
+ data = await response.text()
91
+ return io.StringIO(data)
@@ -8,15 +8,21 @@ from typing import Any, Iterator, cast
8
8
  import inflection
9
9
  import pandas as pd
10
10
  from fluid.utils.data import compact_dict
11
+ from fluid.utils.http_client import AioHttpClient
11
12
 
12
13
  from quantflow.utils.dates import isoformat
13
14
  from quantflow.utils.numbers import to_decimal
14
15
 
15
- from .client import AioHttpClient
16
-
17
16
 
18
17
  @dataclass
19
18
  class FMP(AioHttpClient):
19
+ """Financial Modeling Prep API client
20
+
21
+ Fetch market and financial data from `Financial Modeling Prep`_.
22
+
23
+ .. _Financial Modeling Prep: https://financialmodelingprep.com/developer/docs/
24
+ """
25
+
20
26
  url: str = "https://financialmodelingprep.com/api"
21
27
  key: str = field(default_factory=lambda: os.environ.get("FMP_API_KEY", ""))
22
28
 
@@ -4,12 +4,18 @@ from enum import StrEnum
4
4
  from typing import Any, cast
5
5
 
6
6
  import pandas as pd
7
-
8
- from .client import AioHttpClient
7
+ from fluid.utils.http_client import AioHttpClient
9
8
 
10
9
 
11
10
  @dataclass
12
11
  class Fred(AioHttpClient):
12
+ """Federal Reserve Economic Data API client
13
+
14
+ Fetch economic data from `FRED`_.
15
+
16
+ .. _FRED: https://fred.stlouisfed.org/
17
+ """
18
+
13
19
  url: str = "https://api.stlouisfed.org/fred"
14
20
  key: str = field(default_factory=lambda: os.environ.get("FRED_API_KEY", ""))
15
21
 
@@ -25,15 +31,19 @@ class Fred(AioHttpClient):
25
31
  a = "a"
26
32
 
27
33
  async def categiories(self, **kw: Any) -> dict:
34
+ """Get categories"""
28
35
  return await self.get_path("category", **kw)
29
36
 
30
37
  async def subcategories(self, **kw: Any) -> dict:
38
+ """Get subcategories of a given category"""
31
39
  return await self.get_path("category/children", **kw)
32
40
 
33
41
  async def series(self, **kw: Any) -> dict:
42
+ """Get series of a given category"""
34
43
  return await self.get_path("category/series", **kw)
35
44
 
36
45
  async def serie_data(self, *, to_date: bool = False, **kw: Any) -> pd.DataFrame:
46
+ """Get series data frame"""
37
47
  data = await self.get_path("series/observations", **kw)
38
48
  df = pd.DataFrame(data["observations"])
39
49
  df["value"] = pd.to_numeric(df["value"])
@@ -2,6 +2,7 @@ from pathlib import Path
2
2
 
3
3
 
4
4
  class Vault:
5
+ """Keeps key-value pairs in a file."""
5
6
 
6
7
  def __init__(self, path: str | Path) -> None:
7
8
  self.path = Path(path)
@@ -17,22 +18,27 @@ class Vault:
17
18
  return data
18
19
 
19
20
  def add(self, key: str, value: str) -> None:
21
+ """Add a key-value pair to the vault."""
20
22
  self.data[key] = value
21
23
  self.save()
22
24
 
23
25
  def delete(self, key: str) -> bool:
26
+ """Delete a key-value pair from the vault."""
24
27
  if self.data.pop(key, None) is not None:
25
28
  self.save()
26
29
  return True
27
30
  return False
28
31
 
29
32
  def get(self, key: str) -> str | None:
33
+ """Get the value of a key if available otherwise None."""
30
34
  return self.data.get(key)
31
35
 
32
36
  def keys(self) -> list[str]:
37
+ """Get the keys in the vault."""
33
38
  return sorted(self.data)
34
39
 
35
40
  def save(self) -> None:
41
+ """Save the data to the file."""
36
42
  with open(self.path, "w") as file:
37
43
  for key in sorted(self.data):
38
44
  value = self.data[key]