quantflow 0.3.3__tar.gz → 0.4.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 (56) hide show
  1. {quantflow-0.3.3 → quantflow-0.4.1}/LICENSE +1 -1
  2. {quantflow-0.3.3 → quantflow-0.4.1}/PKG-INFO +15 -15
  3. {quantflow-0.3.3 → quantflow-0.4.1}/pyproject.toml +28 -34
  4. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/__init__.py +1 -1
  5. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/deribit.py +36 -14
  6. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/fmp.py +1 -1
  7. quantflow-0.4.1/quantflow/options/inputs.py +72 -0
  8. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/options/surface.py +178 -91
  9. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/numbers.py +5 -0
  10. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/plot.py +5 -5
  11. quantflow-0.3.3/quantflow/options/inputs.py +0 -51
  12. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/__init__.py +0 -0
  13. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/app.py +0 -0
  14. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/__init__.py +0 -0
  15. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/base.py +0 -0
  16. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/crypto.py +0 -0
  17. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/fred.py +0 -0
  18. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/stocks.py +0 -0
  19. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/vault.py +0 -0
  20. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/script.py +0 -0
  21. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/settings.py +0 -0
  22. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/__init__.py +0 -0
  23. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/fed.py +0 -0
  24. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/fiscal_data.py +0 -0
  25. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/fred.py +0 -0
  26. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/vault.py +0 -0
  27. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/options/__init__.py +0 -0
  28. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/options/bs.py +0 -0
  29. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/options/calibration.py +0 -0
  30. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/options/pricer.py +0 -0
  31. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/py.typed +0 -0
  32. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/__init__.py +0 -0
  33. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/base.py +0 -0
  34. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/bns.py +0 -0
  35. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/cir.py +0 -0
  36. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/copula.py +0 -0
  37. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/dsp.py +0 -0
  38. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/heston.py +0 -0
  39. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/jump_diffusion.py +0 -0
  40. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/ou.py +0 -0
  41. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/poisson.py +0 -0
  42. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/weiner.py +0 -0
  43. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/ta/__init__.py +0 -0
  44. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/ta/base.py +0 -0
  45. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/ta/ohlc.py +0 -0
  46. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/ta/paths.py +0 -0
  47. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/__init__.py +0 -0
  48. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/bins.py +0 -0
  49. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/dates.py +0 -0
  50. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/distributions.py +0 -0
  51. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/functions.py +0 -0
  52. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/interest_rates.py +0 -0
  53. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/marginal.py +0 -0
  54. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/transforms.py +0 -0
  55. {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/types.py +0 -0
  56. {quantflow-0.3.3 → quantflow-0.4.1}/readme.md +0 -0
@@ -1,4 +1,4 @@
1
- Copyright (c) 2024 Quantmind
1
+ Copyright (c) 2023-2025 Quantmind
2
2
 
3
3
  Redistribution and use in source and binary forms, with or without modification,
4
4
  are permitted provided that the following conditions are met:
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: quantflow
3
- Version: 0.3.3
3
+ Version: 0.4.1
4
4
  Summary: quantitative analysis
5
5
  License: BSD-3-Clause
6
- Author: Luca
6
+ Author: Luca Sbardella
7
7
  Author-email: luca@quantmind.com
8
- Requires-Python: >=3.11
8
+ Requires-Python: >=3.11,<4.0
9
9
  Classifier: License :: OSI Approved :: BSD License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
@@ -13,18 +13,18 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Provides-Extra: cli
15
15
  Provides-Extra: data
16
- Requires-Dist: aio-fluid[http] (>=1.2.1,<2.0.0) ; extra == "data"
17
- Requires-Dist: asciichartpy (>=1.5.25,<2.0.0) ; extra == "cli"
18
- Requires-Dist: async-cache (>=1.1.1,<2.0.0) ; extra == "cli"
19
- Requires-Dist: ccy (>=1.7.1,<2.0.0)
20
- Requires-Dist: click (>=8.1.7,<9.0.0) ; extra == "cli"
21
- Requires-Dist: holidays (>=0.63,<0.64) ; extra == "cli"
22
- Requires-Dist: polars[pandas,pyarrow] (>=1.11.0,<2.0.0)
23
- Requires-Dist: prompt-toolkit (>=3.0.43,<4.0.0) ; extra == "cli"
24
- Requires-Dist: pydantic (>=2.0.2,<3.0.0)
25
- Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
26
- Requires-Dist: rich (>=13.9.4,<14.0.0) ; extra == "cli"
27
- Requires-Dist: scipy (>=1.14.1,<2.0.0)
16
+ Requires-Dist: aio-fluid[http] (>=1.2.1) ; extra == "data"
17
+ Requires-Dist: asciichartpy (>=1.5.25) ; extra == "cli"
18
+ Requires-Dist: async-cache (>=1.1.1) ; extra == "cli"
19
+ Requires-Dist: ccy (>=1.7.1)
20
+ Requires-Dist: click (>=8.1.7) ; extra == "cli"
21
+ Requires-Dist: holidays (>=0.63) ; extra == "cli"
22
+ Requires-Dist: polars[pandas,pyarrow] (>=1.11.0)
23
+ Requires-Dist: prompt-toolkit (>=3.0.43) ; extra == "cli"
24
+ Requires-Dist: pydantic (>=2.0.2)
25
+ Requires-Dist: python-dotenv (>=1.0.1)
26
+ Requires-Dist: rich (>=13.9.4) ; extra == "cli"
27
+ Requires-Dist: scipy (>=1.14.1)
28
28
  Project-URL: Documentation, https://quantmind.github.io/quantflow/
29
29
  Project-URL: Homepage, https://github.com/quantmind/quantflow
30
30
  Project-URL: Repository, https://github.com/quantmind/quantflow
@@ -1,52 +1,48 @@
1
- [tool.poetry]
1
+ [project]
2
2
  name = "quantflow"
3
- version = "0.3.3"
3
+ version = "0.4.1"
4
4
  description = "quantitative analysis"
5
- authors = ["Luca <luca@quantmind.com>"]
5
+ authors = [{ name = "Luca Sbardella", email = "luca@quantmind.com" }]
6
6
  license = "BSD-3-Clause"
7
7
  readme = "readme.md"
8
+ requires-python = ">=3.11,<4.0"
9
+ dependencies = [
10
+ "scipy>=1.14.1",
11
+ "pydantic>=2.0.2",
12
+ "ccy>=1.7.1",
13
+ "python-dotenv>=1.0.1",
14
+ "polars[pandas,pyarrow]>=1.11.0",
15
+ ]
8
16
 
9
- [tool.poetry.urls]
17
+ [project.urls]
10
18
  Homepage = "https://github.com/quantmind/quantflow"
11
19
  Repository = "https://github.com/quantmind/quantflow"
12
20
  Documentation = "https://quantmind.github.io/quantflow/"
13
21
 
14
- [tool.poetry.dependencies]
15
- python = ">=3.11"
16
- scipy = "^1.14.1"
17
- pydantic = "^2.0.2"
18
- ccy = { version = "^1.7.1" }
19
- python-dotenv = "^1.0.1"
20
- polars = { version = "^1.11.0", extras = ["pandas", "pyarrow"] }
21
- asciichartpy = { version = "^1.5.25", optional = true }
22
- prompt-toolkit = { version = "^3.0.43", optional = true }
23
- aio-fluid = { version = "^1.2.1", extras = ["http"], optional = true }
24
- rich = { version = "^13.9.4", optional = true }
25
- click = { version = "^8.1.7", optional = true }
26
- holidays = { version = "^0.63", optional = true }
27
- async-cache = { version = "^1.1.1", optional = true }
22
+ [project.optional-dependencies]
23
+ data = ["aio-fluid[http]>=1.2.1"]
24
+ cli = [
25
+ "asciichartpy>=1.5.25",
26
+ "async-cache>=1.1.1",
27
+ "prompt-toolkit>=3.0.43",
28
+ "rich>=13.9.4",
29
+ "click>=8.1.7",
30
+ "holidays>=0.63",
31
+ ]
32
+
33
+ [project.scripts]
34
+ qf = "quantflow.cli.script:main"
35
+
28
36
 
29
37
  [tool.poetry.group.dev.dependencies]
30
38
  black = "^25.1.0"
31
39
  pytest-cov = "^6.0.0"
32
40
  mypy = "^1.14.1"
33
41
  ghp-import = "^2.0.2"
34
- ruff = "^0.11.12"
35
- pytest-asyncio = "^0.26.0"
42
+ ruff = "^0.12.2"
43
+ pytest-asyncio = "^1.0.0"
36
44
  isort = "^6.0.1"
37
45
 
38
-
39
- [tool.poetry.extras]
40
- data = ["aio-fluid"]
41
- cli = [
42
- "asciichartpy",
43
- "async-cache",
44
- "prompt-toolkit",
45
- "rich",
46
- "click",
47
- "holidays",
48
- ]
49
-
50
46
  [tool.poetry.group.book]
51
47
  optional = true
52
48
 
@@ -62,8 +58,6 @@ sphinx-autosummary-accessors = "^2023.4.0"
62
58
  sphinx-copybutton = "^0.5.2"
63
59
  autodocsumm = "^0.2.14"
64
60
 
65
- [tool.poetry.scripts]
66
- qf = "quantflow.cli.script:main"
67
61
 
68
62
  [build-system]
69
63
  requires = ["poetry-core>=1.0.0"]
@@ -1,3 +1,3 @@
1
1
  """Quantitative analysis and pricing"""
2
2
 
3
- __version__ = "0.3.3"
3
+ __version__ = "0.4.1"
@@ -9,8 +9,14 @@ from dateutil.parser import parse
9
9
  from fluid.utils.data import compact_dict
10
10
  from fluid.utils.http_client import AioHttpClient, HttpResponse, HttpResponseError
11
11
 
12
+ from quantflow.options.inputs import OptionType
12
13
  from quantflow.options.surface import VolSecurityType, VolSurfaceLoader
13
- from quantflow.utils.numbers import round_to_step, to_decimal
14
+ from quantflow.utils.numbers import (
15
+ Number,
16
+ round_to_step,
17
+ to_decimal,
18
+ to_decimal_or_none,
19
+ )
14
20
 
15
21
 
16
22
  def parse_maturity(v: str) -> datetime:
@@ -80,9 +86,19 @@ class Deribit(AioHttpClient):
80
86
  kw.update(params=dict(currency=currency), callback=self.to_df)
81
87
  return await self.get_path("public/get_historical_volatility", **kw)
82
88
 
83
- async def volatility_surface_loader(self, currency: str) -> VolSurfaceLoader:
89
+ async def volatility_surface_loader(
90
+ self,
91
+ currency: str,
92
+ *,
93
+ exclude_open_interest: Number | None = None,
94
+ exclude_volume: Number | None = None,
95
+ ) -> VolSurfaceLoader:
84
96
  """Create a :class:`.VolSurfaceLoader` for a given crypto-currency"""
85
- loader = VolSurfaceLoader()
97
+ loader = VolSurfaceLoader(
98
+ asset=currency,
99
+ exclude_open_interest=to_decimal_or_none(exclude_open_interest),
100
+ exclude_volume=to_decimal_or_none(exclude_volume),
101
+ )
86
102
  futures = await self.get_book_summary_by_currency(
87
103
  currency=currency, kind=InstrumentKind.future
88
104
  )
@@ -92,9 +108,9 @@ class Deribit(AioHttpClient):
92
108
  instruments = await self.get_instruments(currency=currency)
93
109
  instrument_map = {i["instrument_name"]: i for i in instruments}
94
110
  min_tick_size = Decimal("inf")
95
- for future in futures:
96
- if (bid_ := future["bid_price"]) and (ask_ := future["ask_price"]):
97
- name = future["instrument_name"]
111
+ for entry in futures:
112
+ if (bid_ := entry["bid_price"]) and (ask_ := entry["ask_price"]):
113
+ name = entry["instrument_name"]
98
114
  meta = instrument_map[name]
99
115
  tick_size = to_decimal(meta["tick_size"])
100
116
  min_tick_size = min(min_tick_size, tick_size)
@@ -105,8 +121,8 @@ class Deribit(AioHttpClient):
105
121
  VolSecurityType.spot,
106
122
  bid=bid,
107
123
  ask=ask,
108
- open_interest=int(future["open_interest"]),
109
- volume=int(future["volume_usd"]),
124
+ open_interest=to_decimal(entry["open_interest"]),
125
+ volume=to_decimal(entry["volume_usd"]),
110
126
  )
111
127
  else:
112
128
  maturity = pd.to_datetime(
@@ -119,15 +135,15 @@ class Deribit(AioHttpClient):
119
135
  maturity=maturity,
120
136
  bid=bid,
121
137
  ask=ask,
122
- open_interest=int(future["open_interest"]),
123
- volume=int(future["volume_usd"]),
138
+ open_interest=to_decimal(entry["open_interest"]),
139
+ volume=to_decimal(entry["volume_usd"]),
124
140
  )
125
141
  loader.tick_size_forwards = min_tick_size
126
142
 
127
143
  min_tick_size = Decimal("inf")
128
- for option in options:
129
- if (bid_ := option["bid_price"]) and (ask_ := option["ask_price"]):
130
- name = option["instrument_name"]
144
+ for entry in options:
145
+ if (bid_ := entry["bid_price"]) and (ask_ := entry["ask_price"]):
146
+ name = entry["instrument_name"]
131
147
  meta = instrument_map[name]
132
148
  tick_size = to_decimal(meta["tick_size"])
133
149
  min_tick_size = min(min_tick_size, tick_size)
@@ -139,9 +155,15 @@ class Deribit(AioHttpClient):
139
155
  unit="ms",
140
156
  utc=True,
141
157
  ).to_pydatetime(),
142
- call=meta["option_type"] == "call",
158
+ option_type=(
159
+ OptionType.call
160
+ if meta["option_type"] == "call"
161
+ else OptionType.put
162
+ ),
143
163
  bid=round_to_step(bid_, tick_size),
144
164
  ask=round_to_step(ask_, tick_size),
165
+ open_interest=to_decimal(entry["open_interest"]),
166
+ volume=to_decimal(entry["volume_usd"]),
145
167
  )
146
168
  loader.tick_size_options = min_tick_size
147
169
  return loader
@@ -74,7 +74,7 @@ class FMP(AioHttpClient):
74
74
  if not to_date:
75
75
  to_date = date.today() + timedelta(days=7)
76
76
  params = {"from": isoformat(from_date), "to": isoformat(to_date)}
77
- return await self.get_path("stock_dividend_calendar", params=params, **kw)
77
+ return await self.get_path("dividends-calendar", params=params, **kw)
78
78
 
79
79
  # Executives
80
80
 
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ from datetime import datetime
5
+ from decimal import Decimal
6
+ from typing import TypeVar
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from quantflow.utils.numbers import ZERO
11
+
12
+ P = TypeVar("P")
13
+
14
+
15
+ class Side(enum.StrEnum):
16
+ """Side of the market"""
17
+
18
+ bid = enum.auto()
19
+ ask = enum.auto()
20
+
21
+
22
+ class OptionType(enum.StrEnum):
23
+ """Type of option"""
24
+
25
+ call = enum.auto()
26
+ put = enum.auto()
27
+
28
+ def is_call(self) -> bool:
29
+ return self is OptionType.call
30
+
31
+ def is_put(self) -> bool:
32
+ return self is OptionType.put
33
+
34
+
35
+ class VolSecurityType(enum.StrEnum):
36
+ """Type of security for the volatility surface"""
37
+
38
+ spot = enum.auto()
39
+ forward = enum.auto()
40
+ option = enum.auto()
41
+
42
+ def vol_surface_type(self) -> VolSecurityType:
43
+ return self
44
+
45
+
46
+ class VolSurfaceInput(BaseModel):
47
+ bid: Decimal
48
+ ask: Decimal
49
+ open_interest: Decimal = ZERO
50
+ volume: Decimal = ZERO
51
+
52
+
53
+ class SpotInput(VolSurfaceInput):
54
+ security_type: VolSecurityType = VolSecurityType.spot
55
+
56
+
57
+ class ForwardInput(VolSurfaceInput):
58
+ maturity: datetime
59
+ security_type: VolSecurityType = VolSecurityType.forward
60
+
61
+
62
+ class OptionInput(VolSurfaceInput):
63
+ strike: Decimal
64
+ maturity: datetime
65
+ option_type: OptionType
66
+ security_type: VolSecurityType = VolSecurityType.option
67
+
68
+
69
+ class VolSurfaceInputs(BaseModel):
70
+ asset: str
71
+ ref_date: datetime
72
+ inputs: list[ForwardInput | SpotInput | OptionInput]
@@ -10,17 +10,19 @@ from typing import Any, Generic, Iterator, NamedTuple, Protocol, Self, TypeVar
10
10
  import numpy as np
11
11
  import pandas as pd
12
12
  from ccy.core.daycounter import ActAct, DayCounter
13
+ from pydantic import BaseModel
13
14
 
14
15
  from quantflow.utils import plot
15
16
  from quantflow.utils.dates import utcnow
16
17
  from quantflow.utils.interest_rates import rate_from_spot_and_forward
17
- from quantflow.utils.numbers import Number, sigfig, to_decimal
18
+ from quantflow.utils.numbers import ZERO, Number, sigfig, to_decimal
18
19
 
19
20
  from .bs import black_price, implied_black_volatility
20
21
  from .inputs import (
21
22
  ForwardInput,
22
23
  OptionInput,
23
- OptionSidesInput,
24
+ OptionType,
25
+ Side,
24
26
  SpotInput,
25
27
  VolSecurityType,
26
28
  VolSurfaceInput,
@@ -28,7 +30,6 @@ from .inputs import (
28
30
  )
29
31
 
30
32
  INITIAL_VOL = 0.5
31
- ZERO = Decimal("0")
32
33
  default_day_counter = ActAct()
33
34
 
34
35
 
@@ -70,44 +71,59 @@ class Price(Generic[S]):
70
71
 
71
72
  @dataclass
72
73
  class SpotPrice(Price[S]):
73
- open_interest: int = 0
74
- volume: int = 0
74
+ open_interest: Decimal = ZERO
75
+ volume: Decimal = ZERO
75
76
 
76
77
  def inputs(self) -> SpotInput:
77
- return SpotInput(bid=self.bid, ask=self.ask)
78
+ return SpotInput(
79
+ bid=self.bid,
80
+ ask=self.ask,
81
+ open_interest=self.open_interest,
82
+ volume=self.volume,
83
+ )
78
84
 
79
85
 
80
86
  @dataclass
81
87
  class FwdPrice(Price[S]):
82
88
  maturity: datetime
83
- open_interest: int = 0
84
- volume: int = 0
89
+ open_interest: Decimal = ZERO
90
+ volume: Decimal = ZERO
85
91
 
86
92
  def inputs(self) -> ForwardInput:
87
93
  return ForwardInput(
88
94
  bid=self.bid,
89
95
  ask=self.ask,
90
96
  maturity=self.maturity,
97
+ open_interest=self.open_interest,
98
+ volume=self.volume,
91
99
  )
92
100
 
93
101
 
94
- @dataclass
95
- class OptionPrice:
96
- price: Decimal
97
- """Price of the option divided by the forward price"""
102
+ class OptionMetadata(BaseModel):
98
103
  strike: Decimal
99
104
  """Strike price"""
100
- call: bool
101
- """True if call, False if put"""
105
+ option_type: OptionType
106
+ """Type of the option"""
102
107
  maturity: datetime
103
108
  """Maturity date"""
104
109
  forward: Decimal = ZERO
105
110
  """Forward price of the underlying"""
106
- implied_vol: float = 0
107
- """Implied Black volatility"""
108
111
  ttm: float = 0
109
112
  """Time to maturity in years"""
110
- side: str = "bid"
113
+ open_interest: Decimal = ZERO
114
+ """Open interest of the option"""
115
+ volume: Decimal = ZERO
116
+ """Volume of the option in USD"""
117
+
118
+
119
+ class OptionPrice(BaseModel):
120
+ price: Decimal
121
+ """Price of the option divided by the forward price"""
122
+ meta: OptionMetadata
123
+ """Metadata of the option price"""
124
+ implied_vol: float = 0
125
+ """Implied Black volatility"""
126
+ side: Side = Side.bid
111
127
  """Side of the market"""
112
128
  converged: bool = True
113
129
  """Flag indicating if implied vol calculation converged"""
@@ -123,7 +139,9 @@ class OptionPrice:
123
139
  ref_date: datetime | None = None,
124
140
  maturity: datetime | None = None,
125
141
  day_counter: DayCounter | None = None,
126
- call: bool = True,
142
+ option_type: OptionType = OptionType.call,
143
+ open_interest: Number = ZERO,
144
+ volume: Number = ZERO,
127
145
  ) -> OptionPrice:
128
146
  """Create an option price
129
147
 
@@ -134,14 +152,46 @@ class OptionPrice:
134
152
  day_counter = day_counter or default_day_counter
135
153
  return cls(
136
154
  price=to_decimal(price),
137
- strike=to_decimal(strike),
138
- forward=to_decimal(forward or strike),
139
155
  implied_vol=implied_vol,
140
- call=call,
141
- maturity=maturity,
142
- ttm=day_counter.dcf(ref_date, maturity),
156
+ meta=OptionMetadata(
157
+ strike=to_decimal(strike),
158
+ forward=to_decimal(forward or strike),
159
+ option_type=option_type,
160
+ maturity=maturity,
161
+ ttm=day_counter.dcf(ref_date, maturity),
162
+ open_interest=to_decimal(open_interest),
163
+ volume=to_decimal(volume),
164
+ ),
143
165
  )
144
166
 
167
+ @property
168
+ def strike(self) -> Decimal:
169
+ return self.meta.strike
170
+
171
+ @property
172
+ def forward(self) -> Decimal:
173
+ return self.meta.forward
174
+
175
+ @property
176
+ def maturity(self) -> datetime:
177
+ return self.meta.maturity
178
+
179
+ @property
180
+ def ttm(self) -> float:
181
+ return self.meta.ttm
182
+
183
+ @property
184
+ def option_type(self) -> OptionType:
185
+ return self.meta.option_type
186
+
187
+ @property
188
+ def open_interest(self) -> Decimal:
189
+ return self.meta.open_interest
190
+
191
+ @property
192
+ def volume(self) -> Decimal:
193
+ return self.meta.volume
194
+
145
195
  @property
146
196
  def moneyness(self) -> float:
147
197
  return float(np.log(float(self.strike / self.forward)))
@@ -156,7 +206,7 @@ class OptionPrice:
156
206
 
157
207
  @property
158
208
  def price_intrinsic(self) -> Decimal:
159
- if self.call:
209
+ if self.option_type.is_call():
160
210
  return max(self.forward - self.strike, ZERO) / self.forward
161
211
  else:
162
212
  return max(self.strike - self.forward, ZERO) / self.forward
@@ -171,7 +221,7 @@ class OptionPrice:
171
221
 
172
222
  use put-call parity to calculate the call price if a put
173
223
  """
174
- if self.call:
224
+ if self.option_type.is_call():
175
225
  return self.price
176
226
  else:
177
227
  return self.price + 1 - self.strike / self.forward
@@ -182,15 +232,11 @@ class OptionPrice:
182
232
 
183
233
  use put-call parity to calculate the put price if a call
184
234
  """
185
- if self.call:
235
+ if self.option_type.is_call():
186
236
  return self.price - 1 + self.strike / self.forward
187
237
  else:
188
238
  return self.price
189
239
 
190
- @property
191
- def option_type(self) -> str:
192
- return "call" if self.call else "put"
193
-
194
240
  def can_price(self, converged: bool, select: OptionSelection) -> bool:
195
241
  if self.price_time > ZERO and not np.isnan(self.implied_vol):
196
242
  if not self.converged and converged is True:
@@ -200,14 +246,6 @@ class OptionPrice:
200
246
  return True
201
247
  return False
202
248
 
203
- def inputs(self) -> OptionInput:
204
- return OptionInput(
205
- strike=self.strike,
206
- price=self.price,
207
- maturity=self.maturity,
208
- call=self.call,
209
- )
210
-
211
249
  def calculate_price(self) -> OptionPrice:
212
250
  self.price = Decimal(
213
251
  sigfig(
@@ -215,7 +253,7 @@ class OptionPrice:
215
253
  np.asarray(self.moneyness),
216
254
  self.implied_vol,
217
255
  self.ttm,
218
- 1 if self.call else -1,
256
+ 1 if self.option_type.is_call() else -1,
219
257
  ).sum(),
220
258
  8,
221
259
  )
@@ -233,8 +271,10 @@ class OptionPrice:
233
271
  price=float(self.price),
234
272
  price_bp=float(self.price_bp),
235
273
  forward_price=float(self.forward_price),
236
- type=self.option_type,
237
- side=self.side,
274
+ type=str(self.option_type),
275
+ side=str(self.side),
276
+ open_interest=float(self.open_interest),
277
+ volume=float(self.volume),
238
278
  )
239
279
 
240
280
 
@@ -250,10 +290,9 @@ class OptionArrays(NamedTuple):
250
290
  @dataclass
251
291
  class OptionPrices(Generic[S]):
252
292
  security: S
293
+ meta: OptionMetadata
253
294
  bid: OptionPrice
254
295
  ask: OptionPrice
255
- open_interest: int = 0
256
- volume: int = 0
257
296
 
258
297
  def prices(
259
298
  self,
@@ -264,18 +303,32 @@ class OptionPrices(Generic[S]):
264
303
  initial_vol: float = INITIAL_VOL,
265
304
  converged: bool = True,
266
305
  ) -> Iterator[OptionPrice]:
306
+ """Iterator over bid/ask option prices
307
+
308
+ :param forward: Forward price of the underlying asset
309
+ :param ttm: Time to maturity in years
310
+ :param select: the :class:`.OptionSelection` method
311
+ :param initial_vol: Initial volatility for the root finding algorithm
312
+ """
313
+ self.meta.forward = forward
314
+ self.meta.ttm = ttm
267
315
  for o in (self.bid, self.ask):
268
- o.forward = forward
269
- o.ttm = ttm
316
+ o.meta.forward = forward
317
+ o.meta.ttm = ttm
270
318
  if not o.implied_vol:
271
319
  o.implied_vol = initial_vol
272
320
  if o.can_price(converged, select):
273
321
  yield o
274
322
 
275
- def inputs(self) -> OptionSidesInput:
276
- return OptionSidesInput(
277
- bid=self.bid.inputs(),
278
- ask=self.ask.inputs(),
323
+ def inputs(self) -> OptionInput:
324
+ return OptionInput(
325
+ bid=self.bid.price,
326
+ ask=self.ask.price,
327
+ open_interest=self.meta.open_interest,
328
+ volume=self.meta.volume,
329
+ strike=self.meta.strike,
330
+ maturity=self.meta.maturity,
331
+ option_type=self.meta.option_type,
279
332
  )
280
333
 
281
334
 
@@ -402,6 +455,8 @@ class VolSurface(Generic[S]):
402
455
 
403
456
  ref_date: datetime
404
457
  """Reference date for the volatility surface"""
458
+ asset: str
459
+ """Name of the underlying asset"""
405
460
  spot: SpotPrice[S]
406
461
  """Spot price of the underlying asset"""
407
462
  maturities: tuple[VolCrossSection[S], ...]
@@ -421,7 +476,9 @@ class VolSurface(Generic[S]):
421
476
 
422
477
  def inputs(self) -> VolSurfaceInputs:
423
478
  return VolSurfaceInputs(
424
- ref_date=self.ref_date, inputs=list(s.inputs() for s in self.securities())
479
+ asset=self.asset,
480
+ ref_date=self.ref_date,
481
+ inputs=list(s.inputs() for s in self.securities()),
425
482
  )
426
483
 
427
484
  def term_structure(self, frequency: float = 0) -> pd.DataFrame:
@@ -442,7 +499,12 @@ class VolSurface(Generic[S]):
442
499
  initial_vol: float = INITIAL_VOL,
443
500
  converged: bool = True,
444
501
  ) -> Iterator[OptionPrice]:
445
- "Iterator over selected option prices in the surface"
502
+ """Iterator over selected option prices in the surface
503
+
504
+ :param select: the :class:`.OptionSelection` method
505
+ :param index: Index of the cross section to use, if None use all
506
+ :param initial_vol: Initial volatility for the root finding algorithm
507
+ """
446
508
  if index is not None:
447
509
  yield from self.maturities[index].option_prices(
448
510
  self.ref_date,
@@ -543,7 +605,12 @@ class VolSurface(Generic[S]):
543
605
  initial_vol: float = INITIAL_VOL,
544
606
  converged: bool = True,
545
607
  ) -> OptionArrays:
546
- """Organize option prices in a numpy arrays for black volatility calculation"""
608
+ """Organize option prices in a numpy arrays for black volatility calculation
609
+
610
+ :param select: the :class:`.OptionSelection` method
611
+ :param index: Index of the cross section to use, if None use all
612
+ :param initial_vol: Initial volatility for the root finding algorithm
613
+ """
547
614
  options = list(
548
615
  self.option_prices(
549
616
  select=select,
@@ -562,7 +629,7 @@ class VolSurface(Generic[S]):
562
629
  price.append(float(option.price))
563
630
  ttm.append(float(option.ttm))
564
631
  vol.append(float(option.implied_vol))
565
- call_put.append(1 if option.call else -1)
632
+ call_put.append(1 if option.option_type.is_call() else -1)
566
633
  return OptionArrays(
567
634
  options=options,
568
635
  moneyness=np.array(moneyness),
@@ -616,35 +683,29 @@ class VolCrossSectionLoader(Generic[S]):
616
683
  def add_option(
617
684
  self,
618
685
  strike: Decimal,
619
- call: bool,
686
+ option_type: OptionType,
620
687
  security: S,
621
688
  bid: Decimal = ZERO,
622
689
  ask: Decimal = ZERO,
623
- open_interest: int = 0,
624
- volume: int = 0,
690
+ open_interest: Decimal = ZERO,
691
+ volume: Decimal = ZERO,
625
692
  ) -> None:
626
693
  if strike not in self.strikes:
627
694
  self.strikes[strike] = Strike(strike=strike)
628
- option = OptionPrices(
629
- security,
630
- bid=OptionPrice(
631
- price=bid,
632
- strike=strike,
633
- call=call,
634
- maturity=self.maturity,
635
- side="bid",
636
- ),
637
- ask=OptionPrice(
638
- price=ask,
639
- strike=strike,
640
- call=call,
641
- maturity=self.maturity,
642
- side="ask",
643
- ),
695
+ meta = OptionMetadata(
696
+ strike=strike,
697
+ option_type=option_type,
698
+ maturity=self.maturity,
644
699
  open_interest=open_interest,
645
700
  volume=volume,
646
701
  )
647
- if call:
702
+ option = OptionPrices(
703
+ security=security,
704
+ meta=meta,
705
+ bid=OptionPrice(price=bid, meta=meta, side=Side.bid),
706
+ ask=OptionPrice(price=ask, meta=meta, side=Side.ask),
707
+ )
708
+ if option_type.is_call():
648
709
  self.strikes[strike].call = option
649
710
  else:
650
711
  self.strikes[strike].put = option
@@ -674,6 +735,8 @@ class VolCrossSectionLoader(Generic[S]):
674
735
  class GenericVolSurfaceLoader(Generic[S]):
675
736
  """Helper class to build a volatility surface from a list of securities"""
676
737
 
738
+ asset: str = ""
739
+ """Name of the underlying asset"""
677
740
  spot: SpotPrice[S] | None = None
678
741
  """Spot price of the underlying asset"""
679
742
  maturities: dict[datetime, VolCrossSectionLoader[S]] = field(default_factory=dict)
@@ -684,6 +747,10 @@ class GenericVolSurfaceLoader(Generic[S]):
684
747
  """Tick size for rounding forward and spot prices - optional"""
685
748
  tick_size_options: Decimal | None = None
686
749
  """Tick size for rounding option prices - optional"""
750
+ exclude_open_interest: Decimal | None = None
751
+ """Exclude options with open interest at or below this value"""
752
+ exclude_volume: Decimal | None = None
753
+ """Exclude options with volume at or below this value"""
687
754
 
688
755
  def get_or_create_maturity(self, maturity: datetime) -> VolCrossSectionLoader[S]:
689
756
  if maturity not in self.maturities:
@@ -698,8 +765,8 @@ class GenericVolSurfaceLoader(Generic[S]):
698
765
  security: S,
699
766
  bid: Decimal = ZERO,
700
767
  ask: Decimal = ZERO,
701
- open_interest: int = 0,
702
- volume: int = 0,
768
+ open_interest: Decimal = ZERO,
769
+ volume: Decimal = ZERO,
703
770
  ) -> None:
704
771
  """Add a spot to the volatility surface loader"""
705
772
  if security.vol_surface_type() != VolSecurityType.spot:
@@ -718,8 +785,8 @@ class GenericVolSurfaceLoader(Generic[S]):
718
785
  maturity: datetime,
719
786
  bid: Decimal = ZERO,
720
787
  ask: Decimal = ZERO,
721
- open_interest: int = 0,
722
- volume: int = 0,
788
+ open_interest: Decimal = ZERO,
789
+ volume: Decimal = ZERO,
723
790
  ) -> None:
724
791
  """Add a forward to the volatility surface loader"""
725
792
  if security.vol_surface_type() != VolSecurityType.forward:
@@ -738,19 +805,26 @@ class GenericVolSurfaceLoader(Generic[S]):
738
805
  security: S,
739
806
  strike: Decimal,
740
807
  maturity: datetime,
741
- call: bool,
808
+ option_type: OptionType,
742
809
  bid: Decimal = ZERO,
743
810
  ask: Decimal = ZERO,
744
- open_interest: int = 0,
745
- volume: int = 0,
811
+ open_interest: Decimal = ZERO,
812
+ volume: Decimal = ZERO,
746
813
  ) -> None:
747
814
  """Add an option to the volatility surface loader"""
748
815
  if security.vol_surface_type() != VolSecurityType.option:
749
816
  raise ValueError("Security is not an option")
817
+ if self.exclude_volume is not None and volume <= self.exclude_volume:
818
+ return
819
+ if (
820
+ self.exclude_open_interest is not None
821
+ and open_interest <= self.exclude_open_interest
822
+ ):
823
+ return
750
824
  self.get_or_create_maturity(maturity=maturity).add_option(
751
- strike,
752
- call,
753
- security,
825
+ strike=strike,
826
+ option_type=option_type,
827
+ security=security,
754
828
  bid=bid,
755
829
  ask=ask,
756
830
  open_interest=open_interest,
@@ -766,6 +840,7 @@ class GenericVolSurfaceLoader(Generic[S]):
766
840
  if section := self.maturities[maturity].cross_section():
767
841
  maturities.append(section)
768
842
  return VolSurface(
843
+ asset=self.asset,
769
844
  ref_date=ref_date or utcnow(),
770
845
  spot=self.spot,
771
846
  maturities=tuple(maturities),
@@ -776,29 +851,41 @@ class GenericVolSurfaceLoader(Generic[S]):
776
851
 
777
852
 
778
853
  class VolSurfaceLoader(GenericVolSurfaceLoader[VolSecurityType]):
779
- def add(self, input: VolSurfaceInput[Any]) -> None:
854
+ """A volatility surface loader"""
855
+
856
+ def add(self, input: VolSurfaceInput) -> None:
780
857
  """Add a volatility security input to the loader
781
858
 
782
859
  :params input: The input data for the security,
783
860
  it can be spot, forward or option
784
861
  """
785
862
  if isinstance(input, SpotInput):
786
- self.add_spot(VolSecurityType.spot, bid=input.bid, ask=input.ask)
863
+ self.add_spot(
864
+ VolSecurityType.spot,
865
+ bid=input.bid,
866
+ ask=input.ask,
867
+ open_interest=input.open_interest,
868
+ volume=input.volume,
869
+ )
787
870
  elif isinstance(input, ForwardInput):
788
871
  self.add_forward(
789
872
  VolSecurityType.forward,
790
873
  maturity=input.maturity,
791
874
  bid=input.bid,
792
875
  ask=input.ask,
876
+ open_interest=input.open_interest,
877
+ volume=input.volume,
793
878
  )
794
- elif isinstance(input, OptionSidesInput):
879
+ elif isinstance(input, OptionInput):
795
880
  self.add_option(
796
881
  VolSecurityType.option,
797
- strike=assert_same(input.bid.strike, input.ask.strike),
798
- call=assert_same(input.bid.call, input.ask.call),
799
- maturity=assert_same(input.bid.maturity, input.ask.maturity),
800
- bid=input.bid.price,
801
- ask=input.ask.price,
882
+ strike=input.strike,
883
+ option_type=input.option_type,
884
+ maturity=input.maturity,
885
+ bid=input.bid,
886
+ ask=input.ask,
887
+ open_interest=input.open_interest,
888
+ volume=input.volume,
802
889
  )
803
890
  else:
804
891
  raise ValueError(f"Unknown input type {type(input)}")
@@ -18,6 +18,11 @@ def to_decimal(value: Number) -> Decimal:
18
18
  return Decimal(str(value)) if not isinstance(value, Decimal) else value
19
19
 
20
20
 
21
+ def to_decimal_or_none(value: Number | None) -> Decimal | None:
22
+ """Convert a value to Decimal, or return None if the value is None."""
23
+ return to_decimal(value) if value is not None else None
24
+
25
+
21
26
  def sigfig(value: Number, sig: int = 5) -> str:
22
27
  """round a number to the given significant digit"""
23
28
  return f"%.{sig}g" % to_decimal(value)
@@ -40,7 +40,7 @@ def plot_marginal_pdf(
40
40
  label: str = "characteristic PDF",
41
41
  log_y: bool = False,
42
42
  fig: Any | None = None,
43
- **kwargs: Any
43
+ **kwargs: Any,
44
44
  ) -> Any:
45
45
  """Plot the marginal pdf on an input support"""
46
46
  check_plotly()
@@ -104,7 +104,7 @@ def plot_vol_surface(
104
104
  color_series: str = "side",
105
105
  fig: Any | None = None,
106
106
  fig_params: dict | None = None,
107
- **kwargs: Any
107
+ **kwargs: Any,
108
108
  ) -> Any:
109
109
  check_plotly()
110
110
  # Define a color map for the categorical values
@@ -144,7 +144,7 @@ def plot_vol_surface_3d(
144
144
  *,
145
145
  marker_size: int = 10,
146
146
  series: str = "implied_vol",
147
- **kwargs: Any
147
+ **kwargs: Any,
148
148
  ) -> Any:
149
149
  check_plotly()
150
150
  return px.scatter_3d(df, x="moneyness_ttm", y="ttm", z=series, color="side")
@@ -158,7 +158,7 @@ def plot_vol_cross(
158
158
  marker_size: int = 10,
159
159
  fig: Any | None = None,
160
160
  name: str = "model",
161
- **kwargs: Any
161
+ **kwargs: Any,
162
162
  ) -> Any:
163
163
  check_plotly()
164
164
  fig = fig or go.Figure()
@@ -188,7 +188,7 @@ def plot3d(
188
188
  z: FloatArray,
189
189
  contours: Any | None,
190
190
  colorscale: str = "viridis",
191
- **kwargs: Any
191
+ **kwargs: Any,
192
192
  ) -> Any:
193
193
  check_plotly()
194
194
  fig = go.Figure(
@@ -1,51 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import enum
4
- from datetime import datetime
5
- from decimal import Decimal
6
- from typing import Generic, TypeVar
7
-
8
- from pydantic import BaseModel
9
-
10
- P = TypeVar("P")
11
-
12
-
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()
19
-
20
- def vol_surface_type(self) -> VolSecurityType:
21
- return self
22
-
23
-
24
- class VolSurfaceInput(BaseModel, Generic[P]):
25
- bid: P
26
- ask: P
27
-
28
-
29
- class OptionInput(BaseModel):
30
- price: Decimal
31
- strike: Decimal
32
- maturity: datetime
33
- call: bool
34
-
35
-
36
- class SpotInput(VolSurfaceInput[Decimal]):
37
- security_type: VolSecurityType = VolSecurityType.spot
38
-
39
-
40
- class ForwardInput(VolSurfaceInput[Decimal]):
41
- maturity: datetime
42
- security_type: VolSecurityType = VolSecurityType.forward
43
-
44
-
45
- class OptionSidesInput(VolSurfaceInput[OptionInput]):
46
- security_type: VolSecurityType = VolSecurityType.option
47
-
48
-
49
- class VolSurfaceInputs(BaseModel):
50
- ref_date: datetime
51
- inputs: list[ForwardInput | SpotInput | OptionSidesInput]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes