Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 95 additions & 12 deletions financepy/products/rates/ibor_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
# Copyright (C) 2018, 2019, 2020 Dominic O'Kane
##############################################################################

# TODO: Add functionality around settlement
# TODO: Write test function
# TODO: Handle 1 month futures contracts

import numpy as np

from ...utils.error import FinError
from ...utils.day_count import DayCountTypes
from ...utils.day_count import DayCount, DayCountTypes
from ...utils.global_vars import G_DAYS_IN_YEAR
from ...utils.math import ONE_MILLION
from ...utils.date import Date
Expand All @@ -30,7 +26,7 @@ def __init__(
self,
today_dt: Date,
future_number: int, # The number of the future after today_dt
future_tenor: str = "3M", # '1M', '2M', '3M'
future_tenor: str = "3M", # '1M', '3M'
accrual_dc_type: DayCountTypes = DayCountTypes.ACT_360,
contract_size: float = ONE_MILLION,
):
Expand All @@ -44,22 +40,68 @@ def __init__(
if future_number < 1:
raise FinError("Future number must be 1 or more")

if future_tenor not in ["3M", "3m"]:
raise FinError("Only 3M IMM futures handled currently.")
future_tenor = future_tenor.upper()

self.delivery_dt = today_dt.next_imm_date()
if future_tenor not in ["1M", "3M"]:
raise FinError("Only 1M and 3M IMM futures handled currently.")

for _ in range(0, future_number - 1):
self.delivery_dt = self.delivery_dt.next_imm_date()
self.future_tenor = future_tenor
self.delivery_dt = self._first_delivery_dt(today_dt, future_tenor)

self.end_of_interest_period = self.delivery_dt.next_imm_date()
for _ in range(0, future_number - 1):
self.delivery_dt = self._next_delivery_dt(
self.delivery_dt, future_tenor
)

self.end_of_interest_period = self._next_delivery_dt(
self.delivery_dt, future_tenor
)
self.last_trading_dt = self.delivery_dt.add_days(-2)
self.accrual_dc_type = accrual_dc_type
self.contract_size = contract_size

###########################################################################

@staticmethod
def _next_monthly_imm_date(dt: Date):
"""Return the next third Wednesday after dt, for any month."""

m_imm = dt.m
y_imm = dt.y
d_imm = dt.third_wednesday_of_month(m_imm, y_imm)

if dt.d >= d_imm:
next_month_dt = dt.add_months(1)
m_imm = next_month_dt.m
y_imm = next_month_dt.y
d_imm = next_month_dt.third_wednesday_of_month(m_imm, y_imm)

return Date(d_imm, m_imm, y_imm)

###########################################################################

@staticmethod
def _first_delivery_dt(today_dt: Date, future_tenor: str):
"""Return the first delivery date after today for the tenor."""

if future_tenor == "1M":
return IborFuture._next_monthly_imm_date(today_dt)

return today_dt.next_imm_date()

###########################################################################

@staticmethod
def _next_delivery_dt(delivery_dt: Date, future_tenor: str):
"""Return the next delivery date after an existing delivery date."""

if future_tenor == "1M":
return IborFuture._next_monthly_imm_date(delivery_dt)

return delivery_dt.next_imm_date()

###########################################################################

def to_fra(self, futures_price, convexity):
"""Convert the futures contract to a IborFRA object so it can be
used to boostrap a Ibor curve. For this we need to adjust the futures
Expand Down Expand Up @@ -87,6 +129,47 @@ def futures_rate(self, futures_price):

###########################################################################

def accrual_factor(self):
"""Return the accrual factor for the futures interest period."""

dc = DayCount(self.accrual_dc_type)
return dc.year_frac(
self.delivery_dt, self.end_of_interest_period
)[0]

###########################################################################

def basis_point_value(self):
"""Return the cash value of one basis point move in futures price."""

return self.contract_size * self.accrual_factor() / 10000.0

###########################################################################

def settlement_amount(
self,
futures_price: float,
settlement_price: float,
num_contracts: float = 1.0,
):
"""Return the cash settlement amount for a futures price move.

The amount is for a long position. Use a negative number of contracts
for a short position. Prices are quoted in futures price points, for
example 97.50.
"""

price_change = settlement_price - futures_price
return (
num_contracts
* self.contract_size
* self.accrual_factor()
* price_change
/ 100.0
)

###########################################################################

def fra_rate(self, futures_price, convexity):
"""Convert futures price and convexity to a FRA rate using the BBG
negative convexity (in percent). This is then divided by 100 before
Expand Down
95 changes: 72 additions & 23 deletions unit_tests/test_FinIborFuture.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Copyright (C) 2018, 2019, 2020 Dominic O'Kane

import pytest

from financepy.utils.date_format import set_date_format, DateFormatTypes
from financepy.utils.date import Date
from financepy.utils.error import FinError
from financepy.products.rates.ibor_future import IborFuture

set_date_format(DateFormatTypes.UK_LONG)
Expand All @@ -13,26 +16,72 @@ def test_fin_ibor_future():

today_date = Date(5, 5, 2020)

i = 1
fut = IborFuture(today_date, i, "3M")
fra = fut.to_fra(0.020, 0.0)
assert fut.delivery_dt == Date(17, 6, 2020)
assert fra.start_dt == Date(17, 6, 2020)

i = 4
fut = IborFuture(today_date, i, "3M")
fra = fut.to_fra(0.020, 0.0)
assert fut.delivery_dt == Date(17, 3, 2021)
assert fra.start_dt == Date(17, 3, 2021)

i = 7
fut = IborFuture(today_date, i, "3M")
fra = fut.to_fra(0.020, 0.0)
assert fut.delivery_dt == Date(15, 12, 2021)
assert fra.start_dt == Date(15, 12, 2021)

i = 10
fut = IborFuture(today_date, i, "3M")
fra = fut.to_fra(0.020, 0.0)
assert fut.delivery_dt == Date(21, 9, 2022)
assert fra.start_dt == Date(21, 9, 2022)
expected_dates = [
(1, Date(17, 6, 2020), Date(16, 9, 2020)),
(4, Date(17, 3, 2021), Date(16, 6, 2021)),
(7, Date(15, 12, 2021), Date(16, 3, 2022)),
(10, Date(21, 9, 2022), Date(21, 12, 2022)),
]

for future_number, delivery_dt, end_dt in expected_dates:
fut = IborFuture(today_date, future_number, "3M")
fra = fut.to_fra(97.50, 0.0)

assert fut.future_tenor == "3M"
assert fut.delivery_dt == delivery_dt
assert fut.end_of_interest_period == end_dt
assert fra.start_dt == delivery_dt
assert fra.maturity_dt == end_dt
assert fra.fra_rate == pytest.approx(0.0250)


def test_fin_ibor_future_one_month_contracts():

today_date = Date(5, 5, 2020)

expected_dates = [
(1, Date(20, 5, 2020), Date(17, 6, 2020)),
(2, Date(17, 6, 2020), Date(15, 7, 2020)),
(3, Date(15, 7, 2020), Date(19, 8, 2020)),
(4, Date(19, 8, 2020), Date(16, 9, 2020)),
]

for future_number, delivery_dt, end_dt in expected_dates:
fut = IborFuture(today_date, future_number, "1m")
fra = fut.to_fra(97.50, 0.0)

assert fut.future_tenor == "1M"
assert fut.delivery_dt == delivery_dt
assert fut.end_of_interest_period == end_dt
assert fra.start_dt == delivery_dt
assert fra.maturity_dt == end_dt


def test_fin_ibor_future_settlement_amount():

today_date = Date(5, 5, 2020)
fut = IborFuture(today_date, 1, "3M")

assert fut.last_trading_dt == Date(15, 6, 2020)
assert fut.accrual_factor() == pytest.approx(91.0 / 360.0)
assert fut.basis_point_value() == pytest.approx(25.2777777778)
assert fut.settlement_amount(97.50, 97.51) == pytest.approx(
fut.basis_point_value()
)
assert fut.settlement_amount(97.51, 97.50) == pytest.approx(
-fut.basis_point_value()
)
assert fut.settlement_amount(97.50, 97.51, 3.0) == pytest.approx(
3.0 * fut.basis_point_value()
)


def test_fin_ibor_future_invalid_inputs():

today_date = Date(5, 5, 2020)

with pytest.raises(FinError):
IborFuture(today_date, 0, "3M")

with pytest.raises(FinError):
IborFuture(today_date, 1, "2M")