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.
- {quantflow-0.3.3 → quantflow-0.4.1}/LICENSE +1 -1
- {quantflow-0.3.3 → quantflow-0.4.1}/PKG-INFO +15 -15
- {quantflow-0.3.3 → quantflow-0.4.1}/pyproject.toml +28 -34
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/__init__.py +1 -1
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/deribit.py +36 -14
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/fmp.py +1 -1
- quantflow-0.4.1/quantflow/options/inputs.py +72 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/options/surface.py +178 -91
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/numbers.py +5 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/plot.py +5 -5
- quantflow-0.3.3/quantflow/options/inputs.py +0 -51
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/__init__.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/app.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/__init__.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/base.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/crypto.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/fred.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/stocks.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/commands/vault.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/script.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/cli/settings.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/__init__.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/fed.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/fiscal_data.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/fred.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/data/vault.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/options/__init__.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/options/bs.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/options/calibration.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/options/pricer.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/py.typed +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/__init__.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/base.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/bns.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/cir.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/copula.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/dsp.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/heston.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/jump_diffusion.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/ou.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/poisson.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/sp/weiner.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/ta/__init__.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/ta/base.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/ta/ohlc.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/ta/paths.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/__init__.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/bins.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/dates.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/distributions.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/functions.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/interest_rates.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/marginal.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/transforms.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/quantflow/utils/types.py +0 -0
- {quantflow-0.3.3 → quantflow-0.4.1}/readme.md +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: quantflow
|
|
3
|
-
Version: 0.
|
|
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
|
|
17
|
-
Requires-Dist: asciichartpy (>=1.5.25
|
|
18
|
-
Requires-Dist: async-cache (>=1.1.1
|
|
19
|
-
Requires-Dist: ccy (>=1.7.1
|
|
20
|
-
Requires-Dist: click (>=8.1.7
|
|
21
|
-
Requires-Dist: holidays (>=0.63
|
|
22
|
-
Requires-Dist: polars[pandas,pyarrow] (>=1.11.0
|
|
23
|
-
Requires-Dist: prompt-toolkit (>=3.0.43
|
|
24
|
-
Requires-Dist: pydantic (>=2.0.2
|
|
25
|
-
Requires-Dist: python-dotenv (>=1.0.1
|
|
26
|
-
Requires-Dist: rich (>=13.9.4
|
|
27
|
-
Requires-Dist: scipy (>=1.14.1
|
|
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
|
-
[
|
|
1
|
+
[project]
|
|
2
2
|
name = "quantflow"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.1"
|
|
4
4
|
description = "quantitative analysis"
|
|
5
|
-
authors = ["Luca
|
|
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
|
-
[
|
|
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
|
-
[
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
35
|
-
pytest-asyncio = "^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"]
|
|
@@ -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
|
|
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(
|
|
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
|
|
96
|
-
if (bid_ :=
|
|
97
|
-
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=
|
|
109
|
-
volume=
|
|
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=
|
|
123
|
-
volume=
|
|
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
|
|
129
|
-
if (bid_ :=
|
|
130
|
-
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
|
-
|
|
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("
|
|
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
|
-
|
|
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:
|
|
74
|
-
volume:
|
|
74
|
+
open_interest: Decimal = ZERO
|
|
75
|
+
volume: Decimal = ZERO
|
|
75
76
|
|
|
76
77
|
def inputs(self) -> SpotInput:
|
|
77
|
-
return SpotInput(
|
|
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:
|
|
84
|
-
volume:
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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) ->
|
|
276
|
-
return
|
|
277
|
-
bid=self.bid.
|
|
278
|
-
ask=self.ask.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
686
|
+
option_type: OptionType,
|
|
620
687
|
security: S,
|
|
621
688
|
bid: Decimal = ZERO,
|
|
622
689
|
ask: Decimal = ZERO,
|
|
623
|
-
open_interest:
|
|
624
|
-
volume:
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
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:
|
|
702
|
-
volume:
|
|
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:
|
|
722
|
-
volume:
|
|
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
|
-
|
|
808
|
+
option_type: OptionType,
|
|
742
809
|
bid: Decimal = ZERO,
|
|
743
810
|
ask: Decimal = ZERO,
|
|
744
|
-
open_interest:
|
|
745
|
-
volume:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
879
|
+
elif isinstance(input, OptionInput):
|
|
795
880
|
self.add_option(
|
|
796
881
|
VolSecurityType.option,
|
|
797
|
-
strike=
|
|
798
|
-
|
|
799
|
-
maturity=
|
|
800
|
-
bid=input.bid
|
|
801
|
-
ask=input.ask
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|