Skip to content

Instantly share code, notes, and snippets.

@omarfsosa
Last active September 30, 2022 12:50
Show Gist options
  • Save omarfsosa/d5fff5628c59b6449006824f2a192c5c to your computer and use it in GitHub Desktop.
Save omarfsosa/d5fff5628c59b6449006824f2a192c5c to your computer and use it in GitHub Desktop.
Binomial Asset Pricing (Shreve's chapter 1)
"""
Binomial Asset Pricing Model
----------------------------
These are some class implementations I'm using to replicate in code
some of the examples presented in Shreve's book on Stochastic Calculus
part 1.
Example usage (example 1.3.1)
>>> s0 = 4
>>> u, d = 2, 0.5
>>> stock = SimpleBinomialAsset(s0, u, d)
>>> put_option(stock, 3, 5) # 0.864
Example usage (example 1.3.2)
>>> s0 = 4
>>> u, d = 2, 0.5
>>> stock = SimpleBinomialAsset(s0, u, d)
>>> lookback_option(stock, 3) # 1.376
Example usage (problem 1.9)
>>> s0 = 80
>>> step_size = 10
>>> stock = AdditiveBinomialAsset(s0, step_size)
>>> call_option(stock, 5, 80, interest_rate=0) # 9.375
"""
import abc
INTEREST_RATE = 0.25
class BaseBinomialAsset(abc.ABC):
"""
Base class for an asset that is only allowed to take 2 values
in the next time period.
Args:
value: The present value of the stock
u: The multiplying factor when the stock goes up
d: The multiplying factor when the stock goes down
r: The risk-free rate. Only used to validate no-arbitrage conditions
"""
def __init__(self, value: float, u: float, d: float, r: float | None = None):
if r is not None and not (0 < d < 1 + r < u):
msg = "Inconsistent market conditions:\n" f"d={d}, (1 + r)={1 + r}, u={u}"
raise ValueError(msg)
self.value = value
self.u = u
self.d = d
self.r = r
@abc.abstractmethod
def up(self):
"""
Returns a new asset after a step up
"""
@abc.abstractmethod
def down(self):
"""
Returns a new asset after a step down
"""
def __repr__(self):
v = self.value
u = self.u
d = self.d
r = self.r
classname = self.__class__.__name__
return f"{classname}(value={v!r}, u={u!r}, d={d!r}, r={r!r})"
class SimpleBinomialAsset(BaseBinomialAsset):
def up(self):
return SimpleBinomialAsset(self.value * self.u, self.u, self.d, self.r)
def down(self):
return SimpleBinomialAsset(self.value * self.d, self.u, self.d, self.r)
class AdditiveBinomialAsset(BaseBinomialAsset):
"""For excercise 1.9"""
def __init__(self, value, step_size):
u = (value + step_size) / value
d = (value - step_size) / value
super().__init__(value, u, d)
self.step_size = step_size
def up(self):
return AdditiveBinomialAsset(self.value * self.u, step_size=self.step_size)
def down(self):
return AdditiveBinomialAsset(self.value * self.d, step_size=self.step_size)
def put_option(
asset: BaseBinomialAsset,
time_to_expiry: int,
strike: float,
interest_rate: float = INTEREST_RATE,
) -> float:
"""
Return the no-arbitrage value of a call option on the underlying ``asset``,
with ``strike`` price and ``time_to_expiry`` periods left.
"""
return _put_option(asset, time_to_expiry, strike, interest_rate, {})
def call_option(
asset: BaseBinomialAsset,
time_to_expiry: int,
strike: float,
interest_rate: float = INTEREST_RATE,
) -> float:
"""
Return the no-arbitrage value of a put option on the underlying ``asset``,
with ``strike`` price and ``time_to_expiry`` periods left.
"""
return _call_option(asset, time_to_expiry, strike, interest_rate, {})
def lookback_option(
asset: BaseBinomialAsset,
time_to_expiry: int,
interest_rate: float = INTEREST_RATE,
) -> float:
"""
Return the no-arbitrage value of a lookback option on the underlying ``asset``
"""
return _lookback_option(
asset,
time_to_expiry,
max_val=asset.value,
interest_rate=interest_rate,
memo={},
)
def get_risk_free_probas(
asset: BaseBinomialAsset, interest_rate: float
) -> tuple[float]:
p = (1 + interest_rate - asset.d) / (asset.u - asset.d)
q = 1 - p
return p, q
# TODO: Abstract the logic of put and call options into a PathIndependentDerivative
def _put_option(
asset: BaseBinomialAsset,
time_to_expiry: int,
strike: float,
interest_rate: float,
memo: dict,
):
key = (asset.value, time_to_expiry)
if key in memo:
return memo[key]
if time_to_expiry == 0:
result = max(strike - asset.value, 0)
memo[key] = result
return result
if time_to_expiry < 0:
return 0
p, q = get_risk_free_probas(asset, interest_rate)
discount = 1 / (1 + interest_rate)
asset_up = asset.up()
asset_down = asset.down()
value_up = _put_option(
asset_up,
time_to_expiry - 1,
strike,
interest_rate,
memo,
)
value_down = _put_option(
asset_down,
time_to_expiry - 1,
strike,
interest_rate,
memo,
)
result = discount * (p * value_up + q * value_down)
memo[key] = result
return result
def _call_option(
asset: BaseBinomialAsset,
time_to_expiry: int,
strike: float,
interest_rate: float,
memo: dict,
):
key = (asset.value, time_to_expiry)
if key in memo:
return memo[key]
if time_to_expiry == 0:
result = max(asset.value - strike, 0)
memo[key] = result
return result
if time_to_expiry < 0:
return 0
p, q = get_risk_free_probas(asset, interest_rate)
discount = 1 / (1 + interest_rate)
asset_up = asset.up()
asset_down = asset.down()
value_up = _call_option(
asset_up,
time_to_expiry - 1,
strike,
interest_rate,
memo,
)
value_down = _call_option(
asset_down,
time_to_expiry - 1,
strike,
interest_rate,
memo,
)
result = discount * (p * value_up + q * value_down)
memo[key] = result
return result
def _lookback_option(
asset: BaseBinomialAsset,
time_to_expiry: int,
max_val: float,
interest_rate: float,
memo: dict,
):
key = (asset.value, time_to_expiry, max_val)
if key in memo:
return memo[key]
if time_to_expiry == 0:
result = max_val - asset.value
memo[key] = result
return result
if time_to_expiry < 0:
return 0
p, q = get_risk_free_probas(asset, interest_rate)
discount = 1 / (1 + interest_rate)
asset_up = asset.up()
asset_down = asset.down()
value_up = _lookback_option(
asset_up,
time_to_expiry - 1,
max_val=max(max_val, asset_up.value),
interest_rate=interest_rate,
memo=memo,
)
value_down = _lookback_option(
asset_down,
time_to_expiry - 1,
max_val=max(max_val, asset_down.value),
interest_rate=interest_rate,
memo=memo,
)
result = discount * (p * value_up + q * value_down)
memo[key] = result
return result
def asian_option(
asset,
strike,
time_to_expiry,
interest_rate,
):
"""
Value at time 0 for an Asian option.
"""
return _asian_option(
asset,
strike,
time_to_expiry,
cum_sum=asset.value,
num_steps=1,
interest_rate=interest_rate,
memo={},
)
def _asian_option(
asset,
strike,
time_to_expiry,
cum_sum,
num_steps,
interest_rate,
memo,
):
key = (asset.value, cum_sum, num_steps)
# print(key)
if key in memo:
return memo[key]
if time_to_expiry == 0:
mean = cum_sum / num_steps
result = max(mean - strike, 0)
memo[key] = result
return result
if time_to_expiry < 0:
return 0
p, q = get_risk_free_probas(asset, interest_rate)
asset_up = asset.up()
asset_down = asset.down()
value_up = _asian_option(
asset_up,
strike,
time_to_expiry=time_to_expiry-1,
cum_sum=cum_sum + asset_up.value,
num_steps=num_steps + 1,
interest_rate=interest_rate,
memo=memo,
)
value_down = _asian_option(
asset_down,
strike,
time_to_expiry=time_to_expiry - 1,
cum_sum=cum_sum + asset_down.value,
num_steps=num_steps + 1,
interest_rate=interest_rate,
memo=memo,
)
result = (p * value_up + q * value_down) / (1 + interest_rate)
memo[key] = result
return result
if __name__ == "__main__":
# Problem 1.8, ii)
s0 = 4
u, d = 2, 0.5
stock = SimpleBinomialAsset(s0, u, d)
v = asian_option(stock, strike=4, time_to_expiry=3, interest_rate=INTEREST_RATE)
print(v) # 1.216
# Inefficient implementations
s0 = 4
u, d = 2, 0.5
r = 0.25
stock = BinomialAsset(s0, u, d, r)
print(stock)
print(stock.up())
print(stock.down())
print(stock.up().down())
def option_value(sequence, strike, maturity=1,):
# For now building the stock here -- we will change this next
stock = BinomialAsset(s0, u, d, r)
if len(sequence) == maturity: # at maturity
for coin in sequence:
stock = stock.step(coin)
# For a call-option:
# return max(stock.value - strike, 0)
# For a put-option:
return max(strike - stock.value, 0)
value_up = option_value(sequence + [1], strike, maturity)
value_down = option_value(sequence + [0], strike, maturity)
discount = 1 / (1 + r)
p_tilde = (1 + r - d) / (u - d)
q_tilde = 1 - p_tilde
return discount * (p_tilde * value_up + q_tilde * value_down)
# print(option_value([], strike=5, maturity=3))
# Iteration 2:
def option_value(stock, strike, time_to_expiry=1):
if time_to_expiry == 0:
# For a call-option:
# return max(stock.value - strike, 0)
# For a put-option:
return max(strike - stock.value, 0)
value_up = option_value(stock.up(), strike, time_to_expiry - 1)
value_down = option_value(stock.down(), strike, time_to_expiry - 1)
discount = 1 / (1 + r)
p_tilde = (1 + r - d) / (u - d)
q_tilde = 1 - p_tilde
return discount * (p_tilde * value_up + q_tilde * value_down)
# print(option_value(stock, strike=5, time_to_expiry=3))
# print(option_value(stock, strike=5, time_to_expiry=21))
def option_value(stock, strike, time_to_expiry=1):
return _option_value(stock, strike, time_to_expiry, {})
def _option_value(stock, strike, time_to_expiry, memo):
key = (stock.value, time_to_expiry)
if key in memo:
return memo[key]
if time_to_expiry == 0:
# For a call-option:
# return max(stock.value - strike, 0)
# For a put-option:
result = max(strike - stock.value, 0)
memo[key] = result
return result
value_up = _option_value(stock.up(), strike, time_to_expiry - 1, memo)
value_down = _option_value(stock.down(), strike, time_to_expiry - 1, memo)
discount = 1 / (1 + r)
p_tilde = (1 + r - d) / (u - d)
q_tilde = 1 - p_tilde
result = discount * (p_tilde * value_up + q_tilde * value_down)
memo[key] = result
return result
print(option_value(stock, strike=5, time_to_expiry=100))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment