Skip to content

Commit

Permalink
Merge pull request #32 from SebastienEveno/add_basket_option
Browse files Browse the repository at this point in the history
Add basket option pricing
  • Loading branch information
SebastienEveno authored Apr 10, 2023
2 parents c2d093b + a28cb76 commit b8b4cb0
Show file tree
Hide file tree
Showing 19 changed files with 397 additions and 95 deletions.
40 changes: 20 additions & 20 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,25 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10.2"]
python-version: [ "3.10.2" ]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
48 changes: 24 additions & 24 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ name: Upload Python Package

on:
release:
types: [published]
types: [ published ]

permissions:
contents: read
Expand All @@ -21,26 +21,26 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@master
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish distribution to Test PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
- name: Publish distribution to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
- uses: actions/checkout@master
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish distribution to Test PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
- name: Publish distribution to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# exotx

<p align="center">
<img src="https://github.com/SebastienEveno/exotx/actions/workflows/python-package.yml/badge.svg?branch=master" />
<a href="https://pypi.org/project/exotx" alt="Python Versions">
Expand All @@ -9,18 +10,22 @@
<img src="https://pepy.tech/badge/exotx" /></a>
</p>

exotx is a Python wrapper for the [QuantLib library](https://www.quantlib.org/), a powerful open-source library for quantitative finance. exotx provides a simple and user-friendly interface for pricing and analyzing financial derivatives using QuantLib's advanced numerical methods.
exotx is a Python wrapper for the [QuantLib library](https://www.quantlib.org/), a powerful open-source library for
quantitative finance. exotx provides a simple and user-friendly interface for pricing and analyzing financial
derivatives using QuantLib's advanced numerical methods.

## Installation

To install exotx, simply use pip:

```sh
pip install exotx
```

## Usage

### Define the product

```python
import exotx

Expand All @@ -35,14 +40,17 @@ my_autocallable = exotx.Autocallable(notional, strike, autocall_barrier_level, a
```

### Define the static data

The object that represents static data such as the calendar, the day counter or the business day convention used.

#### From the constructor

```python
my_static_data = exotx.StaticData(day_counter='Actual360', business_day_convention='ModifiedFollowing')
```

#### From JSON

```python
my_json = {
'day_counter': 'Actual360',
Expand All @@ -52,7 +60,9 @@ my_static_data = exotx.StaticData.from_json(my_json)
```

### Define the market data

#### From the constructor

```python
reference_date = '2015-11-06'
spot = 100.0
Expand All @@ -62,7 +72,9 @@ black_scholes_volatility = 0.2

my_market_data = exotx.MarketData(reference_date, spot, risk_free_rate, dividend_rate, black_scholes_volatility=black_scholes_volatility)
```

#### From JSON

```python
my_json = {
'reference_date': '2015-11-06',
Expand All @@ -75,16 +87,19 @@ my_market_data = exotx.MarketData.from_json(my_json)
```

### Price the product

```python
exotx.price(my_autocallable, my_market_data, my_static_data, model='black-scholes')
```

```plaintext
96.08517973497098
```

## Contributing

We welcome contributions to exotx! If you find a bug or would like to request a new feature, please open an issue on the [Github repository](https://github.com/sebastieneveno/exotx).
We welcome contributions to exotx! If you find a bug or would like to request a new feature, please open an issue on
the [Github repository](https://github.com/sebastieneveno/exotx).
If you would like to contribute code, please submit a pull request.

## License
Expand Down
2 changes: 1 addition & 1 deletion exotx/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.6.1"
__version__ = "0.6.3"
66 changes: 51 additions & 15 deletions exotx/data/marketdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,40 @@
class MarketData:

def __init__(self,
spot: float,
underlying_spots: List[float],
risk_free_rate: float,
dividend_rate: float,
reference_date: Union[datetime, str, None] = None,
expiration_dates: List[Union[datetime, str]] = None,
strikes: List[float] = None,
data: List[List[float]] = None,
black_scholes_volatility: float = None) -> None:
underlying_black_scholes_volatilities: List[float] = None,
correlation_matrix: List[List[float]] = None) -> None:
# set the reference date
self._set_reference_date(reference_date)

# set the underlying spot value
self._set_underlying_spot(spot)
# set underlying spot values
self._set_underlying_spots(underlying_spots)

# set market rate curves
self._set_rate_curves(dividend_rate, risk_free_rate)

# set the volatility surface
self._set_volatility_surface(black_scholes_volatility, data, expiration_dates, strikes)
self._set_volatility_surface(underlying_black_scholes_volatilities, data, expiration_dates, strikes)

# set the correlation matrix
self._set_correlation_matrix(correlation_matrix)

# region setters

# TODO: Have a proper rate curves stripper service
def _set_rate_curves(self, dividend_rate, risk_free_rate):
def _set_rate_curves(self, dividend_rate, risk_free_rate) -> None:
self.risk_free_rate = risk_free_rate
self.dividend_rate = dividend_rate

# TODO: Allow for multiple volatility surfaces for each underlying
def _set_volatility_surface(self, black_scholes_volatility, data, expiration_dates, strikes):
def _set_volatility_surface(self, underlying_black_scholes_volatilities: List[float], data, expiration_dates,
strikes) -> None:
self.expiration_dates: List[datetime] = []
if not expiration_dates:
self.expiration_dates = None
Expand All @@ -47,14 +54,19 @@ def _set_volatility_surface(self, black_scholes_volatility, data, expiration_dat
self.expiration_dates.append(expiration_date)
self.strikes = strikes
self.data = data
self.black_scholes_volatility = black_scholes_volatility

# TODO: Allow for multiple underlying spots to be defined, may need a proper container class
def _set_underlying_spot(self, spot):
assert spot > 0
self.spot = spot
if underlying_black_scholes_volatilities:
for vol in underlying_black_scholes_volatilities:
assert vol >= 0, f"Invalid volatility: {vol}"
self.underlying_black_scholes_volatilities = underlying_black_scholes_volatilities # set to None if it does not exist, OK

def _set_reference_date(self, reference_date: Union[datetime, str, None]):
# TODO: Allow for multiple underlying spots to be defined, may need a proper container class or a dict
def _set_underlying_spots(self, underlying_spots: List[float]) -> None:
for underlying_spot in underlying_spots:
assert underlying_spot > 0, f"Invalid underlying spot {underlying_spot}"
self.underlying_spots = underlying_spots

def _set_reference_date(self, reference_date: Union[datetime, str, None]) -> None:
if isinstance(reference_date, str):
reference_date = datetime.strptime(reference_date, '%Y-%m-%d')
elif isinstance(reference_date, datetime):
Expand All @@ -64,9 +76,28 @@ def _set_reference_date(self, reference_date: Union[datetime, str, None]):
reference_date = datetime.today()
self.reference_date: datetime = reference_date

def _set_correlation_matrix(self, correlation_matrix: Union[List[List[float]], None]) -> None:
if correlation_matrix:
for rows in correlation_matrix:
for rho in rows:
assert 1 >= rho >= -1, "Invalid correlation matrix"
self.correlation_matrix = correlation_matrix

# endregion

# region getters
def get_ql_reference_date(self) -> ql.Date:
return ql.Date().from_date(self.reference_date)

def get_correlation_matrix(self) -> ql.Matrix:
matrix = ql.Matrix(len(self.correlation_matrix), len(self.correlation_matrix))
for i in range(len(self.correlation_matrix)):
matrix[i][i] = 1.0
for j in range(i + 1, len(self.correlation_matrix)):
matrix[i][j] = self.correlation_matrix[i][j]
matrix[j][i] = self.correlation_matrix[i][j]
return matrix

# TODO: Get these from a proper rate curve stripper service
def get_yield_curve(self, day_counter) -> ql.YieldTermStructureHandle:
flat_forward = ql.FlatForward(self.get_ql_reference_date(), self.risk_free_rate, day_counter)
Expand All @@ -76,6 +107,9 @@ def get_dividend_curve(self, day_counter) -> ql.YieldTermStructureHandle:
flat_forward = ql.FlatForward(self.get_ql_reference_date(), self.dividend_rate, day_counter)
return ql.YieldTermStructureHandle(flat_forward)

# endregion

# region serialization/deserialization
@classmethod
def from_json(cls, data: dict):
schema = MarketDataSchema()
Expand All @@ -90,19 +124,21 @@ def to_json(self, format_type: str = "dict"):
return json.dumps(my_json)
else:
raise NotImplementedError(f"Invalid format type {format_type} when dumping")
# endregion


# region Schema

class MarketDataSchema(Schema):
spot = fields.Float(required=True)
underlying_spots = fields.List(fields.Float(required=True))
risk_free_rate = fields.Float(required=True)
dividend_rate = fields.Float(required=True)
reference_date = fields.DateTime(format='%Y-%m-%d', allow_none=True)
expiration_dates = fields.List(fields.DateTime(format='%Y-%m-%d'), allow_none=True)
strikes = fields.List(fields.Float(), allow_none=True)
data = fields.List(fields.List(fields.Float), allow_none=True)
black_scholes_volatility = fields.Float(allow_none=True)
underlying_black_scholes_volatilities = fields.List(fields.Float(allow_none=True))
correlation_matrix = fields.List(fields.List(fields.Float()))

@post_load
def make_market_data(self, data, **kwargs) -> MarketData:
Expand Down
2 changes: 2 additions & 0 deletions exotx/instruments/asian_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def from_json(cls, json_data):
# endregion


# region Schema
class AsianOptionSchema(Schema):
strike = fields.Float()
maturity = fields.Date(format="%Y-%m-%d")
Expand All @@ -162,3 +163,4 @@ class AsianOptionSchema(Schema):
@post_load
def make_asian_option(self, data, **kwargs) -> AsianOption:
return AsianOption(**data)
# endregion
Loading

0 comments on commit b8b4cb0

Please sign in to comment.