diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e420b3e7..f92045b6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -29,3 +29,8 @@ applyTo: '/**' * Do not use em dashes (—) in documentation files or docstrings. Use colons, parentheses, or restructure the sentence instead. * Math in documentation and docstrings uses `$...$` for inline and `$$...$$` or `\begin{equation}...\end{equation}` for block equations. Do not use `.. math::` or `:math:` (RST syntax). * To rebuild doc examples run `uv run ./dev/build-examples` — runs all scripts in `docs/examples/` and writes their output to `docs/examples_output/` + +## Package structure + +* Strategy runtime markdown descriptions (read by `load_description()` at runtime) live inside the package at `quantflow/options/strategies/docs/` — they must be inside the package to be accessible when the library is installed +* mkdocs documentation pages live in `docs/api/options/` — do not mix these two locations diff --git a/notebooks/reference/glossary.md b/docs/glossary.md similarity index 75% rename from notebooks/reference/glossary.md rename to docs/glossary.md index 604d537c..f676abe6 100644 --- a/notebooks/reference/glossary.md +++ b/docs/glossary.md @@ -1,21 +1,8 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.6 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - # Glossary ## Characteristic Function -The [characteristic function](../theory/characteristic.md) of a random variable $x$ is the Fourier transform of ${\mathbb P}_x$, +The [characteristic function](../theory/characteristic) of a random variable $x$ is the Fourier transform of ${\mathbb P}_x$, where ${\mathbb P}_x$ is the distrubution measure of $x$. \begin{equation} @@ -47,24 +34,24 @@ Check this study on the [Hurst exponent with OHLC data](../applications/hurst). ## Moneyness -Moneyness is used in the context of option pricing and it is defined as +Moneyness, or log strike/forward ratio, is used in the context of option pricing and it is defined as \begin{equation} \ln\frac{K}{F} \end{equation} -where $K$ is the strike and $F$ is the Forward price. A positive value implies strikes above the forward, which means put options are in the money and call options are out of the money. +where $K$ is the strike and $F$ is the Forward price. A positive value implies strikes above the forward, which means put options are in the money (ITM) and call options are out of the money (OTM). -## Moneyness Time Adjusted +## Moneyness Vol Adjusted -The time-adjusted moneyness is used in the context of option pricing in order to compare options with different maturities. It is defined as +The vol-adjusted moneyness is used in the context of option pricing in order to compare options with different maturities. It is defined as \begin{equation} - \frac{1}{\sqrt{T}}\ln\frac{K}{F} + \frac{1}{\sigma\sqrt{T}}\ln\frac{K}{F} \end{equation} -where $K$ is the strike and $F$ is the Forward price and $T$ is the time to maturity. +where $K$ is the strike and $F$ is the Forward price and $T$ is the time to maturity and $\sigma$ is the implied Black volatility. The key reason for dividing by the square root of time-to-maturity is related to how volatility and price movement behave over time. The price of the underlying asset is subject to random fluctuations, if these fluctuations follow a Brownian motion than the diff --git a/notebooks/reference/references.bib b/docs/references.bib similarity index 100% rename from notebooks/reference/references.bib rename to docs/references.bib diff --git a/mkdocs.yml b/mkdocs.yml index faebc70b..e918ceca 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -111,6 +111,7 @@ nav: - Hurst: examples/hurst - Supersmoother: examples/supersmoother - Volatility Surface: examples/volatility-surface + - Glossary: glossary.md - Contributing: contributing.md - Bibliography: bibliography.md markdown_extensions: diff --git a/notebooks/CNAME b/notebooks/CNAME deleted file mode 100644 index ab6dc2fb..00000000 --- a/notebooks/CNAME +++ /dev/null @@ -1 +0,0 @@ -quantflow.quantmind.com diff --git a/notebooks/_config.yml b/notebooks/_config.yml deleted file mode 100644 index 98efec3b..00000000 --- a/notebooks/_config.yml +++ /dev/null @@ -1,65 +0,0 @@ -# Book settings -# Learn more at https://jupyterbook.org/customize/config.html - -title: Quantflow library -author: quantmind -copyright: "2014-2025" -logo: assets/quantflow-light.svg - -# Force re-execution of notebooks on each build. -# See https://jupyterbook.org/content/execute.html -execute: - #execute_notebooks: "off" - execute_notebooks: force - -# Define the name of the latex output file for PDF builds -latex: - latex_documents: - targetname: book.tex - -# Add a bibtex file so that we can create citations -bibtex_bibfiles: - - reference/references.bib - -# Information about where the book exists on the web -repository: - url: https://github.com/quantmind/quantflow # Online location of your book - path_to_book: notebooks # Optional path to your book, relative to the repository root - branch: main # Which branch of the repository should be used when creating links (optional) - -# Add GitHub buttons to your book -# See https://jupyterbook.org/customize/config.html#add-a-link-to-your-repository -html: - favicon: assets/quantflow-logo.png - home_page_in_navbar: false - use_edit_page_button: true - use_issues_button: true - use_repository_button: true - analytics: - google_analytics_id: G-CM0DR45HDR - -parse: - myst_enable_extensions: - # don't forget to list any other extensions you want enabled, - # including those that are enabled by default! - - dollarmath - - amsmath - -sphinx: - recursive_update: true - config: - html_static_path: - - assets - html_js_files: - # required by plotly charts - - https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js - mathjax_options: { - "async": "async", - } - extra_extensions: - - "sphinx.ext.autodoc" - - "sphinx.ext.autosummary" - - "sphinx.ext.intersphinx" - - "sphinx_autosummary_accessors" - - "sphinx_copybutton" - - "autodocsumm" diff --git a/notebooks/_toc.yml b/notebooks/_toc.yml deleted file mode 100644 index 54a2e276..00000000 --- a/notebooks/_toc.yml +++ /dev/null @@ -1,46 +0,0 @@ -# Table of contents -# Learn more at https://jupyterbook.org/customize/toc.html - -format: jb-book -root: index -parts: -- caption: Topic Guides - chapters: - - file: theory/overview - sections: - - file: theory/levy - - file: theory/characteristic - - file: theory/inversion - - file: theory/option_pricing - - - file: models/overview - sections: - - file: models/weiner - - file: models/poisson - - file: models/jump_diffusion - - file: models/cir - - file: models/ou - - file: models/heston - - file: models/heston_jumps - - file: models/bns - - - file: applications/overview - sections: - - file: applications/volatility_surface - - file: applications/hurst - - file: applications/calibration - - - file: examples/overview - sections: - - file: examples/gaussian_sampling - - file: examples/exponential_sampling - - file: examples/poisson_sampling - - file: examples/heston_vol_surface - - - file: api/index.rst - -- caption: Reference - chapters: - - file: reference/contributing - - file: reference/glossary - - file: reference/biblio diff --git a/notebooks/conf.py b/notebooks/conf.py deleted file mode 100644 index d561ead6..00000000 --- a/notebooks/conf.py +++ /dev/null @@ -1,38 +0,0 @@ -############################################################################### -# Auto-generated by `jupyter-book config` -# If you wish to continue using _config.yml, make edits to that file and -# re-generate this one. -############################################################################### -author = 'Quantmind Team' -bibtex_bibfiles = ['reference/references.bib'] -comments_config = {'hypothesis': False, 'utterances': False} -copyright = '2024' -exclude_patterns = ['**.ipynb_checkpoints', '.DS_Store', 'Thumbs.db', '_build'] -extensions = ['sphinx_togglebutton', 'sphinx_copybutton', 'myst_nb', 'jupyter_book', 'sphinx_thebe', 'sphinx_comments', 'sphinx_external_toc', 'sphinx.ext.intersphinx', 'sphinx_design', 'sphinx_book_theme', 'sphinx.ext.autodoc', 'sphinx_autodoc_typehints', 'sphinx.ext.autosummary', 'sphinx.ext.linkcode', 'sphinx_autosummary_accessors', 'autodocsumm', 'sphinxcontrib.bibtex', 'sphinx_jupyterbook_latex', 'sphinx_multitoc_numbering'] -external_toc_exclude_missing = False -external_toc_path = '_toc.yml' -html_baseurl = '' -html_favicon = 'assets/quantflow-logo.png' -html_js_files = ['https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js'] -html_logo = 'assets/quantflow-light.svg' -html_sourcelink_suffix = '' -html_theme = 'sphinx_book_theme' -html_theme_options = {'search_bar_text': 'Search this book...', 'launch_buttons': {'notebook_interface': 'classic', 'binderhub_url': '', 'jupyterhub_url': '', 'thebe': False, 'colab_url': '', 'deepnote_url': ''}, 'path_to_docs': 'notebooks', 'repository_url': 'https://github.com/quantmind/quantflow', 'repository_branch': 'main', 'extra_footer': '', 'home_page_in_toc': False, 'announcement': '', 'analytics': {'google_analytics_id': 'G-XBNNWQ560T', 'plausible_analytics_domain': '', 'plausible_analytics_url': 'https://plausible.io/js/script.js'}, 'use_repository_button': True, 'use_edit_page_button': True, 'use_issues_button': True} -html_title = 'Quantflow library' -latex_engine = 'pdflatex' -linkcode_resolve = 'lambda domain, info: "test"\n' -mathjax_options = {'async': 'async'} -myst_enable_extensions = ['dollarmath', 'amsmath'] -myst_url_schemes = ['mailto', 'http', 'https'] -nb_execution_allow_errors = False -nb_execution_cache_path = '' -nb_execution_excludepatterns = [] -nb_execution_in_temp = False -nb_execution_mode = 'off' -nb_execution_timeout = 30 -nb_output_stderr = 'show' -numfig = True -pygments_style = 'sphinx' -suppress_warnings = ['myst.domains'] -use_jupyterbook_latex = True -use_multitoc_numbering = True diff --git a/notebooks/examples/heston_vol_surface.md b/notebooks/examples/heston_vol_surface.md deleted file mode 100644 index 3001152f..00000000 --- a/notebooks/examples/heston_vol_surface.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.6 -kernelspec: - display_name: .venv - language: python - name: python3 ---- - -# HestonJ Volatility Surface - -Here we study the Implied volatility surface of the Heston model with jumps. -The Heston model is a stochastic volatility model that is widely used in the finance industry to price options. - -```{code-cell} ipython3 -from quantflow.sp.heston import HestonJ -from quantflow.utils.distributions import DoubleExponential -from quantflow.options.pricer import OptionPricer - -pricer = OptionPricer(model=HestonJ.create( - DoubleExponential, - vol=0.5, - kappa=2, - rho=-0.2, - sigma=0.8, - jump_fraction=0.5, - jump_asymmetry=0.2 -)) -pricer -``` - -```{code-cell} ipython3 -fig = None -for ttm in (0.1, 0.5, 1): - fig = pricer.maturity(ttm).plot(fig=fig, name=f"ttm={ttm}") -fig -``` - - - -```{code-cell} ipython3 -pricer.plot3d(max_moneyness_ttm=1.5, support=31).update_layout( - height=800, - title="Heston volatility surface", -) -``` - -```{code-cell} ipython3 - -``` diff --git a/notebooks/index.md b/notebooks/index.md deleted file mode 100644 index f62b2dc1..00000000 --- a/notebooks/index.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.14.7 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Quantflow - -A library for quantitative analysis and pricing. - -```{grid} -```{grid-item} -```{image} _static/heston.gif -:alt: Heston volatility surface -:width: 400px -```{grid-item} -``` - - -This documentation is organized into a few major sections. -* [Theory](./theory/overview.md) cover some important concept used throughout the library, for the curious reader -* [Stochastic models](./models/overview.md) cover all the stochastic models supported and their use -* [Applications](./applications/overview.md) show case the real-world use cases -* [Examples](./examples/overview.md) random examples -* [API Reference](./api/index.rst) python API reference - -## Installation - -To install the library use -``` -pip install quantflow -``` - - -## Optional dependencies - -Quantflow comes with two optional dependencies: - -* `data` for data retrieval, to install it use - ``` - pip install quantflow[data] - ``` -* `cli` for command line interface, to install it use - ``` - pip install quantflow[data,cli] - ``` diff --git a/notebooks/reference/biblio.md b/notebooks/reference/biblio.md deleted file mode 100644 index 3df243ce..00000000 --- a/notebooks/reference/biblio.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.19.1 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Bibliography - -```{bibliography} -``` diff --git a/notebooks/reference/contributing.md b/notebooks/reference/contributing.md deleted file mode 100644 index bf97a59c..00000000 --- a/notebooks/reference/contributing.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -jupytext: - formats: ipynb,md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.6 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Contributing - -Welcome to `quantflow` repository! We are excited you are here and want to contribute. - -## Getting Started - -To get started with quantflow's codebase, take the following steps: - -* Clone the repo -``` -git clone git@github.com:quantmind/quantflow.git -``` -* Install dev dependencies -``` -make install-dev -``` -* Run tests -``` -make tests -``` -* Run the jupyter notebook server during development -``` -make notebook -``` -## Documentation - -The documentation is built using [Jupyter book](https://jupyterbook.org/en/stable/intro.html) which supports an *extended version of Jupyter Markdown* called "MyST Markdown". -For information about the MyST syntax and how to use it, see -[the MyST-Parser documentation](https://myst-parser.readthedocs.io/en/latest/using/syntax.html). - -To build the documentation website -``` -make book -``` -Navigate to the `notebook/_build/html` directory to find the `index.html` file you can open on your browser. - -## Notebooks - -To run the notebooks you can use the provided `make` command. - -``` -make notebook -``` - -This will start a jupyter notebook server and open the browser with the notebook interface. -You will be able to run the notebooks and see the results interactively (the book doesn't have interactive widgets). - -## Developing with VS code - -If you develop with VS code we provide several tooling for easing developing. - -* **Notebooks development**: you can use the provided tasks to synchronize notebooks with markdown `myst` files by `Ctrl+Shift+B`. This allows to interact with the notebooks on VS code rather than jupyter interface. - -+++ diff --git a/quantflow/options/docs/butterfly.md b/quantflow/options/docs/butterfly.md new file mode 100644 index 00000000..a873ef85 --- /dev/null +++ b/quantflow/options/docs/butterfly.md @@ -0,0 +1,44 @@ +# Butterfly + +A butterfly consists of three strikes: a lower wing, a body, and an upper wing with equal +log-spacing. It is constructed by buying the wings and selling twice the body (long butterfly) +or the reverse (short butterfly). + +## Structure + +- quantity option at K_low (lower wing) +- -2 * quantity option at K_mid (body) +- quantity option at K_high (upper wing) + +The three strikes are symmetric in log space: log(K_mid/K_low) = log(K_high/K_mid). + +A positive quantity is a long butterfly. A negative quantity is a short butterfly. + +## Call vs Put construction + +By put-call parity, a butterfly built entirely with calls is equivalent in price to one built +entirely with puts. The choice is purely a liquidity consideration: + +- Body above ATM (moneyness > 0): use calls, which are more liquid OTM on the upside +- Body below ATM (moneyness < 0): use puts, which are more liquid OTM on the downside +- Body at ATM (moneyness = 0): either works + +## Greeks + +- Delta: near zero for log-symmetric strikes around ATM +- Gamma: small and negative when long, small and positive when short. The gamma of the wings + and body largely cancel out, leaving low net exposure. +- Vega: small and negative when long, small and positive when short. The vega of the three + legs nearly offsets, so the butterfly has limited sensitivity to parallel shifts in implied + volatility. + +The low vega and gamma distinguish the butterfly from outright vol strategies such as straddles +and strangles. The butterfly is primarily sensitive to the curvature of the vol smile across +strikes, not to the overall level of volatility. + +## Use case + +A long butterfly profits when the underlying stays close to the body strike at expiry. +It is a relative value trade on the shape of the vol smile: it is cheap when the smile is +steep (wings are expensive relative to the body) and expensive when the smile is flat. +A short butterfly profits from large moves away from the body and from smile flattening. diff --git a/quantflow/options/docs/calendar_spread.md b/quantflow/options/docs/calendar_spread.md new file mode 100644 index 00000000..ccbf80b0 --- /dev/null +++ b/quantflow/options/docs/calendar_spread.md @@ -0,0 +1,47 @@ +# Calendar Spread + +A calendar spread (also known as a time spread or horizontal spread) is the same option +type and strike at two different maturities: long the far maturity, short the near maturity +(long calendar) or the reverse (short calendar). + +## Structure + +- quantity option at strike K, maturity T_far +- -quantity option at strike K, maturity T_near + +A positive quantity is a long calendar spread. A negative quantity is a short calendar spread. + +## Greeks + +### Call calendar + +Long far call, short near call. When in the money (K < F), the far call has less delta than +the near call (more time value means less sensitivity to the underlying) for the same value +of implied volatility. The correct cutoff point depends on the term structure of implied volatility. + +In this case, net delta is negative when long. The opposite is true when out of the money +(K > F): the far call has more delta than the near call, so net delta is positive when long. +There is a crossover point where the net delta is zero, near the strike price, where the +delta of the far call equals the delta of the near call. + +### Put calendar + +Long far put, short near put. The sign mirrors the call calendar with moneyness reversed +(ITM for puts means K > F): + +- ITM put (K > F): far put has more negative delta than near put. Net delta is negative when long. +- OTM put (K < F): far put has less negative delta than near put. Net delta is positive when long. +- ATM: net delta near zero, crossover where far and near deltas are equal. + +### Summary + +- Delta: near zero at ATM, sign depends on moneyness and option type (see above) +- Gamma: negative at the near expiry strike when long (short gamma near term) +- Vega: positive when long — the far leg has more vega than the near leg + +## Use case + +A long calendar spread profits from the near-term option decaying faster than the far-term +option (theta play), and from an increase in implied volatility of the far leg relative to +the near leg (forward vol play). +It is sensitive to the term structure of implied volatility rather than the level. diff --git a/quantflow/options/docs/spread.md b/quantflow/options/docs/spread.md new file mode 100644 index 00000000..901456c6 --- /dev/null +++ b/quantflow/options/docs/spread.md @@ -0,0 +1,36 @@ +# Spread (Vertical Spread) + +A spread (formally a vertical spread) combines two options of the same type and expiry +at different strikes. The term "vertical" refers to the strikes being at different levels +on the same expiry column of an options chain. + +## Call Spread + +- Long call at K_low (lower strike) +- Short call at K_high (higher strike) + +A long call spread profits from a rise in the underlying above K_low, with profit capped +at K_high. The short call at K_high finances the long call at K_low. + +## Put Spread + +- Long put at K_high (higher strike) +- Short put at K_low (lower strike) + +A long put spread profits from a fall in the underlying below K_high, with profit capped +at K_low. The short put at K_low finances the long put at K_high. + +## Quantity + +A positive quantity is long the spread (debit spread). A negative quantity is short (credit spread). + +## Greeks + +- Delta: positive for a long call spread, negative for a long put spread +- Gamma: changes sign across the body of the spread +- Vega: low net vega, the two legs largely offset + +## Use case + +Spreads are directional trades with defined risk and reward, cheaper than outright options +because the sold leg partially finances the bought leg. diff --git a/quantflow/options/docs/straddle.md b/quantflow/options/docs/straddle.md new file mode 100644 index 00000000..665ffe4d --- /dev/null +++ b/quantflow/options/docs/straddle.md @@ -0,0 +1,22 @@ +# Straddle + +A straddle is a call and put at the same strike and expiry, with the same signed quantity. + +## Structure + +- quantity call at strike K +- quantity put at strike K + +A positive quantity is a long straddle (long vol). A negative quantity is a short straddle (short vol). + +## Greeks + +- Delta: near zero at inception (ATM) +- Gamma: positive when long, negative when short +- Vega: positive when long, negative when short + +## Use case + +A long straddle profits when realized volatility exceeds implied volatility, +regardless of direction. A short straddle profits when realized volatility +is below implied volatility. diff --git a/quantflow/options/docs/strangle.md b/quantflow/options/docs/strangle.md new file mode 100644 index 00000000..7a290f3a --- /dev/null +++ b/quantflow/options/docs/strangle.md @@ -0,0 +1,23 @@ +# Strangle + +A strangle is a call and put at different strikes, both OTM, same expiry, with the same signed quantity. + +## Structure + +- quantity put at strike K_low (below forward) +- quantity call at strike K_high (above forward) + +A positive quantity is a long strangle (long vol). A negative quantity is a short strangle (short vol). + +## Greeks + +- Delta: near zero for log-symmetric strikes +- Gamma: positive when long, negative when short +- Vega: positive when long, negative when short; lower magnitude than a straddle for the same notional + +## Use case + +A long strangle is cheaper than a straddle but requires a larger move to profit. +It profits when realized volatility significantly exceeds implied volatility. +A short strangle profits from low realized volatility with a wider breakeven range +than a short straddle. diff --git a/quantflow/options/docs/terminology.md b/quantflow/options/docs/terminology.md new file mode 100644 index 00000000..2a9466ae --- /dev/null +++ b/quantflow/options/docs/terminology.md @@ -0,0 +1,5 @@ +# Terminology + +* OTM - out of the money - an option is out of the money if it has no intrinsic value, i.e. for a call option if the strike is above the forward price and for a put option if the strike is below the forward price. +* ITM - in the money - an option is in the money if it has intrinsic value, i.e. for a call option if the strike is below the forward price and for a put option if the strike is above the forward price. +* ATM - at the money - an option is at the money if it has zero intrinsic value, i.e. for a call option if the strike is equal to the forward price and for a put option if the strike is equal to the forward price. diff --git a/quantflow/options/pricer.py b/quantflow/options/pricer.py index 67be2777..dff69664 100644 --- a/quantflow/options/pricer.py +++ b/quantflow/options/pricer.py @@ -88,7 +88,7 @@ def as_option_type( update=dict( option_type=option_type, price=self.price - self.intrinsic_value, - delta=-self.delta, + delta=self.delta - 1.0, gamma=self.gamma, ) ) diff --git a/quantflow/options/strategies/__init__.py b/quantflow/options/strategies/__init__.py new file mode 100644 index 00000000..3935e216 --- /dev/null +++ b/quantflow/options/strategies/__init__.py @@ -0,0 +1,17 @@ +from .base import Strategy, StrategyLeg, StrategyPrice +from .butterfly import Butterfly +from .calendar_spread import CalendarSpread +from .spread import Spread +from .straddle import Straddle +from .strangle import Strangle + +__all__ = [ + "Butterfly", + "CalendarSpread", + "Spread", + "Strategy", + "StrategyLeg", + "StrategyPrice", + "Straddle", + "Strangle", +] diff --git a/quantflow/options/strategies/base.py b/quantflow/options/strategies/base.py new file mode 100644 index 00000000..1845c4e0 --- /dev/null +++ b/quantflow/options/strategies/base.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import math +from datetime import datetime +from pathlib import Path +from typing import ClassVar + +from ccy.core.daycounter import DayCounter +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Doc + +from quantflow.options.inputs import OptionType +from quantflow.options.pricer import ModelOptionPrice, OptionPricer # noqa: TC001 +from quantflow.options.surface import default_day_counter + +OPTIONS_DOCS_PATH = Path(__file__).parent.parent / "docs" + + +def load_description(filename: str) -> str: + """Load a strategy description from a markdown file in this package.""" + return (OPTIONS_DOCS_PATH / filename).read_text(encoding="utf-8") + + +class StrategyLeg(BaseModel, frozen=True): + """A single leg of an option strategy.""" + + option_type: OptionType = Field(description="Call or put") + quantity: float = Field( + description="Signed quantity: positive for long, negative for short" + ) + strike: float = Field(description="Absolute strike price") + maturity: datetime = Field(description="Expiry date of the option") + + @classmethod + def from_moneyness( + cls, + option_type: OptionType, + moneyness: float, + forward: float, + maturity: datetime, + quantity: float = 1.0, + ) -> StrategyLeg: + """Create a leg from a log-strike moneyness offset and forward price.""" + return cls( + option_type=option_type, + quantity=quantity, + strike=forward * math.exp(moneyness), + maturity=maturity, + ) + + +class StrategyPrice(BaseModel, frozen=True): + """Priced result of an option strategy.""" + + legs: tuple[ModelOptionPrice, ...] = Field( + description="Priced legs of the strategy" + ) + price: float = Field(description="Total price in forward space") + delta: float = Field(description="Total delta") + gamma: float = Field(description="Total gamma") + + +class Strategy(BaseModel, frozen=True): + """Base class for option strategies. + + Subclasses define a `description` class variable loaded from a markdown file + via `load_description`. The description is intended for AI agents. + + Legs are built via classmethods and passed directly to the constructor. + """ + + description: ClassVar[str] = "" + + legs: Annotated[ + tuple[StrategyLeg, ...], Doc("Option legs that make up the strategy") + ] + + def price( + self, + pricer: Annotated[OptionPricer, Doc("Option pricer with a fitted model")], + forward: Annotated[float, Doc("Forward price of the underlying")], + ref_date: Annotated[datetime, Doc("Reference date for ttm calculation")], + day_counter: Annotated[ + DayCounter, Doc("Day count convention") + ] = default_day_counter, + ) -> StrategyPrice: + """Price the strategy and return aggregate price and Greeks.""" + priced: list[ModelOptionPrice] = [] + total_price = 0.0 + total_delta = 0.0 + total_gamma = 0.0 + + for leg in self.legs: + ttm = day_counter.dcf(ref_date, leg.maturity) + leg_price = pricer.price( + option_type=leg.option_type, + strike=leg.strike, + forward=forward, + ttm=ttm, + ) + priced.append(leg_price) + total_price += leg.quantity * leg_price.price + total_delta += leg.quantity * leg_price.delta + total_gamma += leg.quantity * leg_price.gamma + + return StrategyPrice( + legs=tuple(priced), + price=total_price, + delta=total_delta, + gamma=total_gamma, + ) diff --git a/quantflow/options/strategies/butterfly.py b/quantflow/options/strategies/butterfly.py new file mode 100644 index 00000000..0b73582e --- /dev/null +++ b/quantflow/options/strategies/butterfly.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from datetime import datetime +from typing import ClassVar + +from typing_extensions import Self + +from quantflow.options.inputs import OptionType + +from .base import Strategy, StrategyLeg, load_description + + +def _option_type_for_moneyness(mid_moneyness: float) -> OptionType: + """Select option type based on body moneyness for best liquidity. + + Calls for body above ATM, puts for body below ATM, calls at ATM. + """ + return OptionType.put if mid_moneyness < 0 else OptionType.call + + +class Butterfly(Strategy, frozen=True): + """Three-strike strategy: long wings, short body. + + Long butterfly when quantity > 0, short butterfly when quantity < 0. + Can be constructed with calls or puts — both are equivalent by put-call parity. + """ + + description: ClassVar[str] = load_description("butterfly.md") + + @classmethod + def from_moneyness( + cls, + forward: float, + maturity: datetime, + wing_moneyness: float = 0.05, + mid_moneyness: float = 0.0, + quantity: float = 1.0, + option_type: OptionType | None = None, + ) -> Self: + """Create a butterfly from a wing offset and body moneyness. + + If option_type is not specified, it is selected automatically based on + the body moneyness for best liquidity. + """ + ot = option_type or _option_type_for_moneyness(mid_moneyness) + return cls( + legs=( + StrategyLeg.from_moneyness( + ot, mid_moneyness - wing_moneyness, forward, maturity, quantity + ), + StrategyLeg.from_moneyness( + ot, mid_moneyness, forward, maturity, -2.0 * quantity + ), + StrategyLeg.from_moneyness( + ot, mid_moneyness + wing_moneyness, forward, maturity, quantity + ), + ) + ) + + @classmethod + def from_strikes( + cls, + low_strike: float, + mid_strike: float, + high_strike: float, + maturity: datetime, + forward: float, + quantity: float = 1.0, + option_type: OptionType | None = None, + ) -> Self: + """Create a butterfly from absolute strikes. + + If option_type is not specified, it is selected automatically based on + the body moneyness for best liquidity. + """ + import math + + ot = option_type or _option_type_for_moneyness(math.log(mid_strike / forward)) + return cls( + legs=( + StrategyLeg( + option_type=ot, + quantity=quantity, + strike=low_strike, + maturity=maturity, + ), + StrategyLeg( + option_type=ot, + quantity=-2.0 * quantity, + strike=mid_strike, + maturity=maturity, + ), + StrategyLeg( + option_type=ot, + quantity=quantity, + strike=high_strike, + maturity=maturity, + ), + ) + ) diff --git a/quantflow/options/strategies/calendar_spread.py b/quantflow/options/strategies/calendar_spread.py new file mode 100644 index 00000000..123572e2 --- /dev/null +++ b/quantflow/options/strategies/calendar_spread.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from datetime import datetime +from typing import ClassVar + +from typing_extensions import Self + +from quantflow.options.inputs import OptionType + +from .base import Strategy, StrategyLeg, load_description + + +class CalendarSpread(Strategy, frozen=True): + """Same strike, same option type, two maturities. + + Long the far maturity, short the near maturity when quantity > 0. + """ + + description: ClassVar[str] = load_description("calendar_spread.md") + + @property + def option_type(self) -> OptionType: + """Option type of the calendar spread.""" + return self.legs[0].option_type + + @classmethod + def create( + cls, + strike: float, + near_maturity: datetime, + far_maturity: datetime, + option_type: OptionType, + quantity: float = 1.0, + ) -> Self: + """Long far call, short near call at the same strike.""" + if near_maturity >= far_maturity: + raise ValueError("Near maturity must be before far maturity.") + return cls( + legs=( + StrategyLeg( + option_type=option_type, + quantity=quantity, + strike=strike, + maturity=far_maturity, + ), + StrategyLeg( + option_type=option_type, + quantity=-quantity, + strike=strike, + maturity=near_maturity, + ), + ) + ) + + @classmethod + def call( + cls, + strike: float, + near_maturity: datetime, + far_maturity: datetime, + quantity: float = 1.0, + ) -> Self: + return cls.create( + strike, near_maturity, far_maturity, OptionType.call, quantity + ) + + @classmethod + def put( + cls, + strike: float, + near_maturity: datetime, + far_maturity: datetime, + quantity: float = 1.0, + ) -> Self: + return cls.create(strike, near_maturity, far_maturity, OptionType.put, quantity) diff --git a/quantflow/options/strategies/spread.py b/quantflow/options/strategies/spread.py new file mode 100644 index 00000000..8c771724 --- /dev/null +++ b/quantflow/options/strategies/spread.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from datetime import datetime +from typing import ClassVar + +from typing_extensions import Self + +from quantflow.options.inputs import OptionType + +from .base import Strategy, StrategyLeg, load_description + + +class Spread(Strategy, frozen=True): + """Vertical spread: same option type, two strikes, same maturity. + + Long the spread when quantity > 0 (debit), short when quantity < 0 (credit). + Call spread: long low strike, short high strike. + Put spread: long high strike, short low strike. + """ + + description: ClassVar[str] = load_description("spread.md") + + @classmethod + def call( + cls, + low_strike: float, + high_strike: float, + maturity: datetime, + quantity: float = 1.0, + ) -> Self: + """Long call at low_strike, short call at high_strike.""" + return cls( + legs=( + StrategyLeg( + option_type=OptionType.call, + quantity=quantity, + strike=low_strike, + maturity=maturity, + ), + StrategyLeg( + option_type=OptionType.call, + quantity=-quantity, + strike=high_strike, + maturity=maturity, + ), + ) + ) + + @classmethod + def put( + cls, + low_strike: float, + high_strike: float, + maturity: datetime, + quantity: float = 1.0, + ) -> Self: + """Long put at high_strike, short put at low_strike.""" + return cls( + legs=( + StrategyLeg( + option_type=OptionType.put, + quantity=quantity, + strike=high_strike, + maturity=maturity, + ), + StrategyLeg( + option_type=OptionType.put, + quantity=-quantity, + strike=low_strike, + maturity=maturity, + ), + ) + ) diff --git a/quantflow/options/strategies/straddle.py b/quantflow/options/strategies/straddle.py new file mode 100644 index 00000000..e98cf036 --- /dev/null +++ b/quantflow/options/strategies/straddle.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from datetime import datetime +from typing import ClassVar + +from typing_extensions import Self + +from quantflow.options.inputs import OptionType + +from .base import Strategy, StrategyLeg, load_description + + +class Straddle(Strategy, frozen=True): + """Call and put at the same strike. + + Long vol when quantity > 0, short vol when quantity < 0. + """ + + description: ClassVar[str] = load_description("straddle.md") + + @classmethod + def create(cls, strike: float, maturity: datetime, quantity: float = 1.0) -> Self: + """Create a straddle at a given absolute strike.""" + return cls( + legs=( + StrategyLeg( + option_type=OptionType.call, + quantity=quantity, + strike=strike, + maturity=maturity, + ), + StrategyLeg( + option_type=OptionType.put, + quantity=quantity, + strike=strike, + maturity=maturity, + ), + ) + ) diff --git a/quantflow/options/strategies/strangle.py b/quantflow/options/strategies/strangle.py new file mode 100644 index 00000000..8fef1470 --- /dev/null +++ b/quantflow/options/strategies/strangle.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from datetime import datetime +from typing import ClassVar + +from typing_extensions import Self + +from quantflow.options.inputs import OptionType + +from .base import Strategy, StrategyLeg, load_description + + +class Strangle(Strategy, frozen=True): + """Call and put at different OTM strikes. + + Long vol when quantity > 0, short vol when quantity < 0. + """ + + description: ClassVar[str] = load_description("strangle.md") + + @classmethod + def from_moneyness( + cls, + forward: float, + maturity: datetime, + put_moneyness: float = -0.05, + call_moneyness: float = 0.05, + quantity: float = 1.0, + ) -> Self: + """Create a strangle from log-strike offsets from forward.""" + return cls( + legs=( + StrategyLeg.from_moneyness( + OptionType.put, put_moneyness, forward, maturity, quantity + ), + StrategyLeg.from_moneyness( + OptionType.call, call_moneyness, forward, maturity, quantity + ), + ) + ) + + @classmethod + def from_strikes( + cls, + put_strike: float, + call_strike: float, + maturity: datetime, + quantity: float = 1.0, + ) -> Self: + """Create a strangle from absolute strikes.""" + if put_strike >= call_strike: + raise ValueError("Put strike must be less than call strike.") + return cls( + legs=( + StrategyLeg( + option_type=OptionType.put, + quantity=quantity, + strike=put_strike, + maturity=maturity, + ), + StrategyLeg( + option_type=OptionType.call, + quantity=quantity, + strike=call_strike, + maturity=maturity, + ), + ) + ) diff --git a/quantflow_tests/test_strategies.py b/quantflow_tests/test_strategies.py new file mode 100644 index 00000000..d7b07b97 --- /dev/null +++ b/quantflow_tests/test_strategies.py @@ -0,0 +1,109 @@ +from datetime import datetime, timezone + +import pytest + +from quantflow.options.pricer import OptionPricer +from quantflow.options.strategies import ( + Butterfly, + CalendarSpread, + Spread, + Straddle, + Strangle, +) +from quantflow.sp.weiner import WeinerProcess + +REF_DATE = datetime(2024, 1, 1, tzinfo=timezone.utc) +MATURITY = datetime(2025, 1, 1, tzinfo=timezone.utc) +FAR_MATURITY = datetime(2026, 1, 1, tzinfo=timezone.utc) +FORWARD = 100.0 + + +@pytest.fixture +def pricer() -> OptionPricer: + return OptionPricer(model=WeinerProcess(sigma=0.3)) + + +def test_straddle_from_strike(pricer: OptionPricer) -> None: + p = Straddle.create(105.0, MATURITY).price(pricer, FORWARD, REF_DATE) + assert p.price > 0 + assert p.gamma > 0 + + +def test_strangle_from_moneyness(pricer: OptionPricer) -> None: + p = Strangle.from_moneyness(FORWARD, MATURITY).price(pricer, FORWARD, REF_DATE) + assert p.price > 0 + assert p.gamma > 0 + assert ( + p.price + < Straddle.create(FORWARD, MATURITY).price(pricer, FORWARD, REF_DATE).price + ) + assert Strangle.description != "" + + +def test_strangle_from_strikes(pricer: OptionPricer) -> None: + p = Strangle.from_strikes(95.0, 105.0, MATURITY).price(pricer, FORWARD, REF_DATE) + assert p.price > 0 + assert p.gamma > 0 + + +def test_butterfly_from_moneyness(pricer: OptionPricer) -> None: + p = Butterfly.from_moneyness(FORWARD, MATURITY).price(pricer, FORWARD, REF_DATE) + assert p.price > 0 + assert p.gamma < 0 + assert Butterfly.description != "" + + +def test_butterfly_from_strikes(pricer: OptionPricer) -> None: + p = Butterfly.from_strikes(95.0, 100.0, 105.0, MATURITY, FORWARD).price( + pricer, FORWARD, REF_DATE + ) + assert p.price > 0 + assert p.gamma < 0 + + +def test_call_spread(pricer: OptionPricer) -> None: + p = Spread.call(95.0, 105.0, MATURITY).price(pricer, FORWARD, REF_DATE) + assert p.price > 0 + assert p.delta > 0 + + +def test_put_spread(pricer: OptionPricer) -> None: + p = Spread.put(95.0, 105.0, MATURITY).price(pricer, FORWARD, REF_DATE) + assert p.price > 0 + assert p.delta < 0 + + +def test_calendar_spread_call_below_forward(pricer: OptionPricer) -> None: + # K < F: net delta negative when long (both calls and puts) + p = CalendarSpread.call(80.0, MATURITY, FAR_MATURITY).price( + pricer, FORWARD, REF_DATE + ) + assert p.price > 0 + assert p.delta < 0 + + +def test_calendar_spread_call_above_forward(pricer: OptionPricer) -> None: + # K > F: net delta positive when long (both calls and puts) + p = CalendarSpread.call(120.0, MATURITY, FAR_MATURITY).price( + pricer, FORWARD, REF_DATE + ) + assert p.price > 0 + assert p.delta > 0 + + +def test_calendar_spread_put_below_forward(pricer: OptionPricer) -> None: + # K < F: net delta negative when long (same as call) + p = CalendarSpread.put(80.0, MATURITY, FAR_MATURITY).price( + pricer, FORWARD, REF_DATE + ) + assert p.price > 0 + assert p.delta < 0 + + +def test_calendar_spread_put_above_forward(pricer: OptionPricer) -> None: + # K > F: net delta positive when long (same as call) + p = CalendarSpread.put(120.0, MATURITY, FAR_MATURITY).price( + pricer, FORWARD, REF_DATE + ) + assert p.price > 0 + assert p.delta > 0