Exemplo n.º 1
0
 def __init__(
     self,
     mu: Union[List[float], List[int], np.ndarray],
     sigma: Union[List[float], List[int], np.ndarray],
     correlation: Union[List[float], np.ndarray],
 ) -> None:
     super().__init__(dim=len(mu))
     self.mu = to_array(mu)
     self.sigma = to_array(sigma)
     self.correlation = to_array(correlation)
Exemplo n.º 2
0
 def step(self,
          x0: Array,
          dt: float,
          random_state: RandomState = None) -> np.ndarray:
     """
     Applies the stochastic process to an array of initial values using the Euler
     Discretization method
     """
     # generate an array of independent draws from the dW distribution (defaults to a
     # normal distribution.) In the general case, we can use matrix multiplication to
     # combine the random draws with the StochasticProcess's standard deviation. This
     # means that we can handle both single-dimensional and multi-dimensional
     # stochastic processes with a single abstract base class. For joint stochastic
     # processes, the standard deviation is a n x n matrix, where n is the dimension
     # of the process, so we effectively convert the independent random draws into
     # correlated random draws.
     x0 = to_array(x0)
     rvs = self.dW.rvs(size=x0.shape,
                       random_state=check_random_state(random_state))
     if self.dim == 1:
         dx = rvs * self.standard_deviation(x0=x0, dt=dt)
     else:
         if x0.ndim == 1:
             # single sample from a joint process
             dx = rvs @ self.standard_deviation(x0=x0, dt=dt).transpose(
                 1, 0)
         else:
             # multiple samples from a joint process
             # we have rvs as a (samples, dimension) array and standard deviation as
             # a (samples, dimension, dimension) array. We want to matrix multiply
             # the rvs (dimension) index with the transposed (dimension, dimension)
             # standard deviation for each sample to get a (samples, dimension) array
             dx = np.einsum("ab,acb->ac", rvs,
                            self.standard_deviation(x0=x0, dt=dt))
     return self.apply(self.expectation(x0=x0, dt=dt), dx)
Exemplo n.º 3
0
    def scenarios(  # pylint: disable=too-many-arguments
        self,
        x0: Array,
        dt: float,
        n_scenarios: int,
        n_steps: int,
        random_state: RandomState = None,
    ) -> np.ndarray:
        """
        Returns a recursively-generated scenario, starting with initial values/array, x0
        and continuing by steps with length dt for a given number of steps

        Parameters
        ----------
        x0 : Array, either a single start value or array of start values if applicable
        dt : float, the length between steps
        n_scenarios : int, the number of scenarios to generate, e.g. 1000
        n_steps : int, the number of steps in the scenario, e.g. 52. In combination with
            dt, this determines the scope of the scenario, e.g. dt=1/12 and n_step=360
            will produce 360 monthly time steps, i.e. a 30-year monthly projection
        random_state : Union[int, np.random.RandomState, None], either an integer seed
            or a numpy RandomState object directly, if reproducibility is desired

        Returns
        -------
        samples : np.ndarray with shape (n_scenarios, n_steps + 1) for a one-dimensional
            stochastic process, or (n_scenarios, n_steps + 1, dim) for a two-dimensional
            stochastic process, where the first timestep of each scenario is x0
        """
        # set a function-level pseudo random number generator, either by creating a new
        # RandomState object with the integer argument, or using the RandomState object
        # directly passed in the arguments.
        prng = check_random_state(random_state)

        x0 = to_array(
            x0)  # ensure we're working with a numpy array before proceeding

        # create a shell array that we will populate with values once they are available
        # this is generally faster than appending subsequent steps to an array each time
        # we'll generate a 2d array if this process has dim == 1; otherwise it will be 3
        shape: Tuple[int, ...] = (n_scenarios, n_steps + 1)
        if self.dim > 1:
            shape = (shape[0], shape[1], self.dim)
        samples = np.empty(shape=shape, dtype=np.float64)

        try:
            # can we broadcast the x0 array into the number of scenarios we want?
            samples[:, 0] = x0
        except ValueError:
            raise ValueError(
                f"Could not broadcast the input array, with shape {x0.shape}, into "
                f"the scenario output array, with shape {samples.shape}")

        # then we iterate through scenarios along the timesteps dimension
        for i in range(n_steps):
            samples[:, i + 1] = self.step(x0=samples[:, i],
                                          dt=dt,
                                          random_state=prng)

        return samples
 def step(self,
          x0: Array,
          dt: float,
          random_state: RandomState = None) -> np.ndarray:
     """
     Applies the stochastic process to an array of initial values using the Euler
     Discretization method
     """
     # generate an array of independent draws from the dW distribution (defaults to a
     # normal distribution.) In the general case, we can use matrix multiplication to
     # combine the random draws with the StochasticProcess's standard deviation. This
     # means that we can handle both single-dimensional and multi-dimensional
     # stochastic processes with a single abstract base class. For joint stochastic
     # processes, the standard deviation is a n x n matrix, where n is the dimension
     # of the process, so we effectively convert the independent random draws into
     # correlated random draws.
     x0 = to_array(x0)
     rvs = self.dW.rvs(size=x0.shape,
                       random_state=check_random_state(random_state))
     dx = rvs @ self.standard_deviation(x0=x0, dt=dt).T
     return self.apply(self.expectation(x0=x0, dt=dt), dx)
    def scenario(self,
                 x0: Array,
                 dt: float,
                 n_step: int,
                 random_state: RandomState = None) -> np.ndarray:
        """
        Returns a recursively-generated scenario, starting with initial values/array, x0
        and continuing by steps with length dt for a given number of steps

        Parameters
        ----------
        x0 : Array, either a single start value or array of start values if applicable
        dt : float, the length between steps
        n_step : int, the number of steps in the scenario, e.g. 360. In combination with
            dt, this determines the scope of the scenario, e.g. dt=1/12 and n_step=360
            will produce 360 monthly time steps, i.e. a 30-year monthly projection.
        random_state : Union[int, np.random.RandomState, None], either an integer seed
            or a numpy RandomState object directly, if reproducibility is desired

        Returns
        -------
        samples : np.ndarray with shape (n_step + 1, dim), where samples[0] is the input
            array, x0, and the subsequent indices are the steps of the scenario
        """
        # set a function-level pseudo random number generator, either by creating a new
        # RandomState object with the integer argument, or using the RandomState object
        # directly passed in the arguments.
        prng = check_random_state(random_state)

        # create a shell array that we will populate with values once they are available
        # this is generally faster than appending subsequent steps to an array each time
        samples = np.empty(shape=(n_step + 1, self.dim), dtype=np.float64)
        samples[0] = to_array(x0)
        for i in range(n_step):
            samples[i + 1] = self.step(x0=samples[i], dt=dt, random_state=prng)

        # squeeze the final dimension of the array if dim == 1
        return samples.squeeze()
 def _diffusion(self, x0: np.ndarray) -> np.ndarray:
     return to_array(self.sigma)
 def _drift(self, x0: np.ndarray) -> np.ndarray:
     return to_array(self.mu - self.dividend -
                     0.5 * self.sigma * self.sigma)
 def logpdf(self, x0: Array, xt: Array, dt: float) -> np.ndarray:
     """
     Returns the log-probability of moving from x0 to x1 starting at time t and
     moving to time t + dt
     """
     return self.transition_distribution(x0=to_array(x0), dt=dt).logpdf(xt)
 def expectation(self, x0: Array, dt: float) -> np.ndarray:
     """
     Returns the expected value of the stochastic process using the Euler
     Discretization method
     """
     return self.apply(to_array(x0), self.drift(x0=x0) * dt)
 def diffusion(self, x0: Array) -> np.ndarray:
     """Returns the diffusion component of the stochastic process"""
     return self._diffusion(x0=to_array(x0))
 def drift(self, x0: Array) -> np.ndarray:
     """Returns the drift component of the stochastic process"""
     return self._drift(x0=to_array(x0))
 def apply(self, x0: Array, dx: Array) -> np.ndarray:
     """Returns a new array of x-values, given a starting array and change vector"""
     return self._apply(x0=to_array(x0), dx=to_array(dx))
 def nnlf(self, x0: Array, xt: Array, dt: float) -> np.ndarray:
     """
     Returns the negative log-likelihood function of moving from x0 to x1 starting at
     time t and moving to time t + dt
     """
     return -np.sum(self.logpdf(x0=to_array(x0), xt=to_array(xt), dt=dt))
 def _diffusion(self, x0: np.ndarray) -> np.ndarray:
     # diffusion of an Ornstein-Uhlenbeck process does not depend on x0
     return to_array(self.sigma)
Exemplo n.º 15
0
 def _diffusion(self, x0: np.ndarray) -> np.ndarray:
     # diffusion of a Wiener process does not depend on x0
     return to_array(self.sigma)
Exemplo n.º 16
0
 def _drift(self, x0: np.ndarray) -> np.ndarray:
     # drift of a Wiener process does not depend on x0
     return to_array(self.mu)