aboutsummaryrefslogtreecommitdiffstats
path: root/utils/path.py
blob: 41474ef34ab54cc3cf3e7353797d93eead6c43bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# author : S. Mandalia
#          shivesh.mandalia@outlook.com
#
# date   : March 19, 2020

"""
Path generator for underlying.
"""

import math
import random
from copy import deepcopy
from dataclasses import dataclass
from typing import List, Sequence, Tuple

from utils.misc import Number


__all__ = ['PathGenerator']


@dataclass
class PathGenerator:
    """
    Class for generating underlying prices using MC techniques.

    Attributes
    ----------
    S : Number
        Spot price.
    r : Number
        Risk-free interest rate.
    div : Number
        Dividend yield.
    vol : Number
        Volatility.
    net_r

    Methods
    -------
    generate(T)
        Generate a random path {S_t1, S_t2, ..., S_tn}.
    generate_antithetic(T)
        Generate a random plus antithetic path
        [{S_t1, S_t2, ..., S_tn}, {S'_t1, S'_t2, ..., S'_tn}].

    Examples
    --------
    >>> from utils.path import PathGenerator
    >>> path = PathGenerator(S=100., r=0.1, div=0.01, vol=0.3)
    >>> print(path)
    PathGenerator(S=100.0, r=0.1, div=0.01, vol=0.3)

    """
    S: Number
    r: Number
    div: Number
    vol: Number

    @property
    def net_r(self) -> float:
        """Net risk free rate."""
        return float(self.r - self.div)

    def generate(self, T: Sequence[Number]) -> List[float]:
        """
        Generate a random path {S_t1, S_t2, ..., S_tn}.

        Parameters
        ----------
        T : Sequence of Numbers
            Set of times {t1, t2, ..., tn} in years.

        Returns
        -------
        spot_prices : List of floats
            Set of prices for the underlying {S_t1, S_t2, ..., S_tn}.

        Examples
        --------
        >>> from utils.path import PathGenerator
        >>> path = PathGenerator(S=100., r=0.1, div=0.01, vol=0.3)
        >>> print(path.generate(T=range(4)))
        [100.0, 100.33539853588853, 122.76017088387074, 142.29540684005462]
        >>> print(path.generate(T=range(4)))
        [100.0, 73.03094019139712, 77.37310245438943, 66.54240939439934]

        """
        # Calculate dt time differences
        dts = [T[idx + 1] - T[idx] for idx in range(len(T) - 1)]

        spot_prices: List[float] = [0] * len(T)
        spot_prices[0] = self.S
        for idx, dt in enumerate(dts):
            # Calculate the drift e^{(r - (1/2) σ²) Δt}
            drift = math.exp((self.net_r - (1/2) * self.vol**2) * dt)

            # Calculate the volatility term e^{σ √{Δt} N(0, 1)}
            rdm_gauss = random.gauss(0, 1)
            vol_term = math.exp(self.vol * math.sqrt(dt) * rdm_gauss)

            # Calculate next spot price
            S_t = spot_prices[idx] * drift * vol_term
            spot_prices[idx + 1] = S_t
        return spot_prices

    def generate_antithetic(
            self, T: Sequence[Number]
    ) -> Tuple[List[float], List[float]]:
        """
        Generate a random plus antithetic path
        [{S_t1, S_t2, ..., S_tn}, {S'_t1, S'_t2, ..., S'_tn}].

        Parameters
        ----------
        T : Sequence of Numbers
            Set of times {t1, t2, ..., tn} in years.

        Returns
        -------
        prices_tuple : Tuple of two Lists of floats
            Set of prices for the underlying
            [{S_t1, S_t2, ..., S_tn}, {S'_t1, S'_t2, ..., S'_tn}].

        Examples
        --------
        >>> from utils.path import PathGenerator
        >>> path = PathGenerator(S=100., r=0.1, div=0.01, vol=0.3)
        >>> print(path.generate_antithetic(T=range(4)))
        ([100.0, 106.30304144532359, 122.02501765852367, 108.71120035365013],
         [100.0, 102.92972513566257, 98.11245153613649, 120.49949282794978])
        >>> print(path.generate_antithetic(T=range(4)))
        ([100.0, 130.1595970941697, 103.35241529329348, 93.58800177914543],
         [100.0, 84.06404968460234, 115.83835362960282, 139.97140935058962])

        """
        # Calculate dt time differences
        dts = [T[idx + 1] - T[idx] for idx in range(len(T) - 1)]

        # Create data structures
        spot_prices: List[float] = [0] * len(T)
        spot_prices[0] = self.S

        a_spot_prices = deepcopy(spot_prices)

        for idx, dt in enumerate(dts):
            # Calculate the drift e^{(r - (1/2) σ²) Δt}
            drift = math.exp((self.net_r - (1/2) * self.vol**2) * dt)

            # Calculate the volatility term e^{σ √{Δt} N(0, 1)}
            rdm_gauss = random.gauss(0, 1)
            a_gauss = -rdm_gauss
            vol_term = math.exp(self.vol * math.sqrt(dt) * rdm_gauss)
            a_vol_term = math.exp(self.vol * math.sqrt(dt) * a_gauss)

            # Calculate next spot price
            S_t = spot_prices[idx] * drift * vol_term
            a_S_t = a_spot_prices[idx] * drift * a_vol_term

            # Add to data structure
            spot_prices[idx + 1] = S_t
            a_spot_prices[idx + 1] = a_S_t

        return (spot_prices, a_spot_prices)