Skip to content

Commit fb9d7f1

Browse files
committed
Add sector performance
1 parent e12787e commit fb9d7f1

File tree

9 files changed

+538
-319
lines changed

9 files changed

+538
-319
lines changed

notebooks/_config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ html:
3434
use_edit_page_button: true
3535
use_issues_button: true
3636
use_repository_button: true
37-
google_analytics_id: G-XBNNWQ560T
37+
analytics:
38+
google_analytics_id: G-XBNNWQ560T
3839

3940
parse:
4041
myst_enable_extensions:

notebooks/applications/volatility_surface.md

Lines changed: 11 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ jupytext:
44
extension: .md
55
format_name: myst
66
format_version: 0.13
7-
jupytext_version: 1.14.7
7+
jupytext_version: 1.16.6
88
kernelspec:
99
display_name: Python 3 (ipykernel)
1010
language: python
@@ -18,50 +18,10 @@ In this notebook we illustrate the use of the Volatility Surface tool in the lib
1818
First thing, fetch the data
1919

2020
```{code-cell} ipython3
21-
from quantflow.data.client import HttpClient
21+
from quantflow.data.deribit import Deribit
2222
23-
deribit_url = "https://test.deribit.com/api/v2/public/get_book_summary_by_currency"
24-
async with HttpClient() as cli:
25-
futures = await cli.get(deribit_url, params=dict(currency="BTC", kind="future"))
26-
options = await cli.get(deribit_url, params=dict(currency="BTC", kind="option"))
27-
```
28-
29-
```{code-cell} ipython3
30-
from decimal import Decimal
31-
from quantflow.options.surface import VolSurfaceLoader, VolSecurityType
32-
from datetime import timezone
33-
from dateutil.parser import parse
34-
35-
def parse_maturity(v: str):
36-
return parse(v).replace(tzinfo=timezone.utc, hour=8)
37-
38-
loader = VolSurfaceLoader()
39-
40-
for future in futures["result"]:
41-
if (bid := future["bid_price"]) and (ask := future["ask_price"]):
42-
maturity = future["instrument_name"].split("-")[-1]
43-
if maturity == "PERPETUAL":
44-
loader.add_spot(VolSecurityType.spot, bid=Decimal(bid), ask=Decimal(ask))
45-
else:
46-
loader.add_forward(
47-
VolSecurityType.forward,
48-
maturity=parse_maturity(maturity),
49-
bid=Decimal(str(bid)),
50-
ask=Decimal(str(ask))
51-
)
52-
53-
for option in options["result"]:
54-
if (bid := option["bid_price"]) and (ask := option["ask_price"]):
55-
_, maturity, strike, ot = option["instrument_name"].split("-")
56-
loader.add_option(
57-
VolSecurityType.option,
58-
strike=Decimal(strike),
59-
maturity=parse_maturity(maturity),
60-
call=ot == "C",
61-
bid=Decimal(str(bid)),
62-
ask=Decimal(str(ask))
63-
)
64-
23+
async with Deribit() as cli:
24+
loader = await cli.volatility_surface_loader("eth")
6525
```
6626

6727
Once we have loaded the data, we create the surface and display the term-structure of forwards
@@ -72,6 +32,10 @@ vs.maturities = vs.maturities[1:]
7232
vs.term_structure()
7333
```
7434

35+
```{code-cell} ipython3
36+
vs.spot
37+
```
38+
7539
## bs method
7640

7741
This method calculate the implied Black volatility from option prices. By default it uses the best option in the surface for the calculation.
@@ -135,10 +99,11 @@ pricer.model
13599
```
136100

137101
```{code-cell} ipython3
138-
cal.plot(index=4, max_moneyness_ttm=1)
102+
cal.plot(index=6, max_moneyness_ttm=1)
139103
```
140104

141-
## Serialization
105+
##
106+
Serialization
142107

143108
It is possible to save the vol surface into a json file so it can be recreated for testing or for serialization/deserialization.
144109

poetry.lock

Lines changed: 232 additions & 244 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quantflow/cli/commands/base.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import enum
34
from typing import TYPE_CHECKING, Any, Self, cast
45

56
import click
@@ -11,6 +12,15 @@
1112
from quantflow.cli.app import QfApp
1213

1314

15+
class HistoricalPeriod(enum.StrEnum):
16+
day = "1d"
17+
week = "1w"
18+
month = "1m"
19+
three_months = "3m"
20+
six_months = "6m"
21+
year = "1y"
22+
23+
1424
class QuantContext(click.Context):
1525

1626
@classmethod
@@ -97,3 +107,11 @@ class options:
97107
help="Chart height",
98108
)
99109
chart = click.option("-c", "--chart", is_flag=True, help="Display chart")
110+
period = click.option(
111+
"-p",
112+
"--period",
113+
type=click.Choice(tuple(p.value for p in HistoricalPeriod)),
114+
default="1d",
115+
show_default=True,
116+
help="Historical period",
117+
)

quantflow/cli/commands/stocks.py

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
from __future__ import annotations
22

33
import asyncio
4+
from datetime import timedelta
5+
from typing import cast
46

57
import click
68
import pandas as pd
79
from asciichartpy import plot
10+
from ccy import period as to_period
811
from ccy.cli.console import df_to_rich
12+
from ccy.tradingcentres import prevbizday
913

1014
from quantflow.data.fmp import FMP
15+
from quantflow.utils.dates import utcnow
1116

12-
from .base import QuantContext, quant_group
17+
from .base import HistoricalPeriod, QuantContext, options, quant_group
1318

1419
FREQUENCIES = tuple(FMP().historical_frequencies())
1520

@@ -49,22 +54,8 @@ def search(text: str) -> None:
4954

5055
@stocks.command()
5156
@click.argument("symbol")
52-
@click.option(
53-
"-h",
54-
"--height",
55-
type=int,
56-
default=20,
57-
show_default=True,
58-
help="Chart height",
59-
)
60-
@click.option(
61-
"-l",
62-
"--length",
63-
type=int,
64-
default=100,
65-
show_default=True,
66-
help="Number of data points",
67-
)
57+
@options.height
58+
@options.length
6859
@click.option(
6960
"-f",
7061
"--frequency",
@@ -84,6 +75,18 @@ def chart(symbol: str, height: int, length: int, frequency: str) -> None:
8475
print(plot(data, {"height": height}))
8576

8677

78+
@stocks.command()
79+
@options.period
80+
def sectors(period: str) -> None:
81+
"""Sectors performance and PE ratios"""
82+
ctx = QuantContext.current()
83+
data = asyncio.run(sector_performance(ctx, HistoricalPeriod(period)))
84+
df = pd.DataFrame(data, columns=["sector", "performance", "pe"]).sort_values(
85+
"performance", ascending=False
86+
)
87+
ctx.qf.print(df_to_rich(df))
88+
89+
8790
async def get_prices(ctx: QuantContext, symbol: str, frequency: str) -> pd.DataFrame:
8891
async with ctx.fmp() as cli:
8992
return await cli.prices(symbol, frequency)
@@ -97,3 +100,30 @@ async def get_profile(ctx: QuantContext, symbol: str) -> list[dict]:
97100
async def search_company(ctx: QuantContext, text: str) -> list[dict]:
98101
async with ctx.fmp() as cli:
99102
return await cli.search(text)
103+
104+
105+
async def sector_performance(
106+
ctx: QuantContext, period: HistoricalPeriod
107+
) -> dict | list[dict]:
108+
async with ctx.fmp() as cli:
109+
to_date = utcnow().date()
110+
if period != HistoricalPeriod.day:
111+
from_date = to_date - timedelta(days=to_period(period.value).totaldays)
112+
sp = await cli.sector_performance(
113+
from_date=prevbizday(from_date, 0).isoformat(), # type: ignore
114+
to_date=prevbizday(to_date, 0).isoformat(), # type: ignore
115+
summary=True,
116+
)
117+
else:
118+
sp = await cli.sector_performance()
119+
spd = cast(dict, sp)
120+
pe = await cli.sector_pe(params=dict(date=prevbizday(to_date, 0).isoformat())) # type: ignore
121+
pes = {}
122+
for k in pe:
123+
sector = k["sector"]
124+
if sector in spd:
125+
pes[sector] = round(float(k["pe"]), 3)
126+
return [
127+
dict(sector=k, performance=float(v), pe=pes.get(k, float("nan")))
128+
for k, v in spd.items()
129+
]

quantflow/data/deribit.py

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,115 @@
1-
from typing import Any
1+
from datetime import datetime, timezone
2+
from typing import Any, cast
23

34
import pandas as pd
5+
from dateutil.parser import parse
46
from fluid.utils.http_client import AioHttpClient, HttpResponse
57

8+
from quantflow.options.surface import VolSecurityType, VolSurfaceLoader
9+
from quantflow.utils.numbers import round_to_step
10+
11+
12+
def parse_maturity(v: str) -> datetime:
13+
return parse(v).replace(tzinfo=timezone.utc, hour=8)
14+
615

716
class Deribit(AioHttpClient):
817
url = "https://www.deribit.com/api/v2"
918

10-
async def get_book_summary_by_instrument(self, **kw: Any) -> Any:
11-
return await self.get_path("public/get_book_summary_by_instrument", **kw)
19+
async def get_book_summary_by_instrument(self, **kw: Any) -> list[dict]:
20+
kw.update(callback=self.to_result)
21+
return cast(
22+
list[dict],
23+
await self.get_path("public/get_book_summary_by_instrument", **kw),
24+
)
25+
26+
async def get_book_summary_by_currency(self, **kw: Any) -> list[dict]:
27+
kw.update(callback=self.to_result)
28+
return cast(
29+
list[dict], await self.get_path("public/get_book_summary_by_currency", **kw)
30+
)
31+
32+
async def get_instruments(self, **kw: Any) -> list[dict]:
33+
kw.update(callback=self.to_result)
34+
return cast(list[dict], await self.get_path("public/get_instruments", **kw))
1235

1336
async def get_volatility(self, **kw: Any) -> pd.DataFrame:
1437
kw.update(callback=self.to_df)
1538
return await self.get_path("public/get_historical_volatility", **kw)
1639

40+
async def volatility_surface_loader(self, currency: str) -> VolSurfaceLoader:
41+
"""Create the volatility surface loader for a given crypto-currency"""
42+
loader = VolSurfaceLoader()
43+
futures = await self.get_book_summary_by_currency(
44+
params=dict(currency=currency, kind="future")
45+
)
46+
options = await self.get_book_summary_by_currency(
47+
params=dict(currency=currency, kind="option")
48+
)
49+
instruments = await self.get_instruments(params=dict(currency=currency))
50+
instrument_map = {i["instrument_name"]: i for i in instruments}
51+
52+
for future in futures:
53+
if (bid_ := future["bid_price"]) and (ask_ := future["ask_price"]):
54+
name = future["instrument_name"]
55+
meta = instrument_map[name]
56+
tick_size = meta["tick_size"]
57+
bid = round_to_step(bid_, tick_size)
58+
ask = round_to_step(ask_, tick_size)
59+
if meta["settlement_period"] == "perpetual":
60+
loader.add_spot(
61+
VolSecurityType.spot,
62+
bid=bid,
63+
ask=ask,
64+
open_interest=int(future["open_interest"]),
65+
volume=int(future["volume_usd"]),
66+
)
67+
else:
68+
maturity = pd.to_datetime(
69+
meta["expiration_timestamp"],
70+
unit="ms",
71+
utc=True,
72+
).to_pydatetime()
73+
loader.add_forward(
74+
VolSecurityType.forward,
75+
maturity=maturity,
76+
bid=bid,
77+
ask=ask,
78+
open_interest=int(future["open_interest"]),
79+
volume=int(future["volume_usd"]),
80+
)
81+
82+
for option in options:
83+
if (bid_ := option["bid_price"]) and (ask_ := option["ask_price"]):
84+
name = option["instrument_name"]
85+
meta = instrument_map[name]
86+
tick_size = meta["tick_size"]
87+
loader.add_option(
88+
VolSecurityType.option,
89+
strike=round_to_step(meta["strike"], tick_size),
90+
maturity=pd.to_datetime(
91+
meta["expiration_timestamp"],
92+
unit="ms",
93+
utc=True,
94+
).to_pydatetime(),
95+
call=meta["option_type"] == "call",
96+
bid=round_to_step(bid_, tick_size),
97+
ask=round_to_step(ask_, tick_size),
98+
)
99+
100+
return loader
101+
102+
# Internal methods
103+
17104
async def get_path(self, path: str, **kw: Any) -> dict:
18105
return await self.get(f"{self.url}/{path}", **kw)
19106

20-
async def to_df(self, response: HttpResponse) -> pd.DataFrame:
107+
async def to_result(self, response: HttpResponse) -> list[dict]:
21108
data = await response.json()
22-
df = pd.DataFrame(data["result"], columns=["timestamp", "volatility"])
109+
return cast(list[dict], data["result"])
110+
111+
async def to_df(self, response: HttpResponse) -> pd.DataFrame:
112+
data = await self.to_result(response)
113+
df = pd.DataFrame(data, columns=["timestamp", "volatility"])
23114
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
24115
return df

0 commit comments

Comments
 (0)