Last active
September 30, 2022 12:50
-
-
Save omarfsosa/d5fff5628c59b6449006824f2a192c5c to your computer and use it in GitHub Desktop.
Binomial Asset Pricing (Shreve's chapter 1)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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