Beispiel #1
0
    def test_european_derivative_price_delta_mc_close_to_black_scholes(self):
        current_price = tf.constant(100.0, dtype=tf.float32)
        r = 0.05
        vol = tf.constant([[0.2]], dtype=tf.float32)
        strike = 100.0
        maturity = 0.1
        dt = 0.01

        bs_call_price = util.black_scholes_call_price(current_price, r, vol,
                                                      strike, maturity)
        bs_delta = monte_carlo_manager.sensitivity_autodiff(
            bs_call_price, current_price)

        num_samples = int(1e3)
        initial_states = tf.ones([num_samples, 1]) * current_price

        def _dynamics_op(log_s, t, dt):
            return dynamics.gbm_log_euler_step_nd(log_s, r, vol, t, dt, key=1)

        def _payoff_fn(log_s):
            return tf.exp(-r * maturity) * payoffs.call_payoff(
                tf.exp(log_s), strike)

        (_, _, outcomes) = monte_carlo_manager.non_callable_price_mc(
            tf.log(initial_states), _dynamics_op, _payoff_fn, maturity,
            num_samples, dt)

        mc_deltas = monte_carlo_manager.sensitivity_autodiff(
            outcomes, initial_states)

        mean_mc_deltas = tf.reduce_mean(mc_deltas)
        mc_deltas_std = tf.sqrt(
            tf.reduce_mean(mc_deltas**2) - (mean_mc_deltas**2))

        with self.test_session() as session:
            bs_delta_eval = session.run(bs_delta)
            mean_mc_deltas_eval, mc_deltas_std_eval = session.run(
                (mean_mc_deltas, mc_deltas_std))

        self.assertLessEqual(
            mean_mc_deltas_eval,
            bs_delta_eval + 3.0 * mc_deltas_std_eval / np.sqrt(num_samples))
        self.assertGreaterEqual(
            mean_mc_deltas_eval,
            bs_delta_eval - 3.0 * mc_deltas_std_eval / np.sqrt(num_samples))
Beispiel #2
0
    def test_var_has_increasing_estimates_with_increasing_levels(self):
        np.random.seed(0)

        var_levels = [0.95, 0.98, 0.999]

        drift = np.asarray([0.1, 0.3, -0.05], dtype=np.float32)
        vol_matrix = 0.2 * np.asarray(
            [[1.5, 0.2, 0.3], [0.2, 1.1, -0.1], [0.3, -0.1, 0.8]],
            dtype=np.float32)
        maturity = 1.0
        dt = 0.01
        num_samples = 8
        num_dims = drift.shape[0]

        key_placeholder = tf.placeholder(shape=(), dtype=tf.int32)

        def _dynamics_op(s, t, dt):
            return dynamics.gbm_euler_step_nd(s, drift, vol_matrix, t, dt)

        initial_state = tf.ones([num_dims]) * 100.0
        initial_pf_value = tf.reduce_sum(initial_state)

        payoff_fn = lambda s: tf.reduce_sum(s, axis=-1) - initial_pf_value

        _, _, portfolio_returns = monte_carlo_manager.non_callable_price_mc(
            initial_state=initial_state,
            dynamics_op=_dynamics_op,
            payoff_fn=payoff_fn,
            maturity=maturity,
            num_samples=num_samples,
            dt=dt)

        var_ests, cvar_ests = risk_metrics.var_and_cvar(
            portfolio_returns, var_levels, key_placeholder, {})

        self.assertLen(var_ests, len(var_levels))
        self.assertLen(cvar_ests, len(var_levels))

        for i in range(len(var_ests) - 1):
            self.assertLessEqual(var_ests[i], var_ests[i + 1])
            self.assertLessEqual(cvar_ests[i], cvar_ests[i + 1])
Beispiel #3
0
    def test_european_call_estimator_converges_close_to_black_scholes(self):
        current_price = 100.0
        r = interest_rate = 0.05
        vol = 0.2
        strike = 120.0
        maturity = 0.5
        dt = 0.001
        discount = tf.exp(-r * maturity)

        tol = 5e-2
        conf_level = 0.95
        batch_size = int(1e4)
        k = key_placeholder = tf.placeholder(shape=(), dtype=tf.int32)
        max_num_steps = 1e5

        bs_call_price = util.black_scholes_call_price(current_price,
                                                      interest_rate, vol,
                                                      strike, maturity)

        initial_state = tf.constant(current_price)

        dynamics_op = lambda s, t, dt: dynamics.gbm_euler_step(
            s, r, vol, t, dt, k)
        payoff_fn = lambda s: discount * payoffs.call_payoff(s, strike)

        (mean_est, mean_sq_est, _) = monte_carlo_manager.non_callable_price_mc(
            initial_state, dynamics_op, payoff_fn, maturity, batch_size, dt)

        with self.test_session() as session:
            (mean_est_eval, _, converged) = monte_carlo_manager.mc_estimator(
                mean_est, mean_sq_est, batch_size, key_placeholder, {}, tol,
                conf_level, max_num_steps, session)

            bs_call_price_eval = session.run(bs_call_price)

        self.assertTrue(converged)
        # Here the discretization bias would make these asserts fail with larger dt.
        self.assertLessEqual(mean_est_eval, bs_call_price_eval * (1.0 + tol))
        self.assertGreaterEqual(mean_est_eval,
                                bs_call_price_eval * (1.0 - tol))
Beispiel #4
0
    def test_var_in_normal_case(self):
        np.random.seed(0)

        var_levels = [0.95, 0.98, 0.999]

        key_placeholder = tf.placeholder(shape=(), dtype=tf.int32)

        num_samples = int(1e6)
        maturity = 1.0
        dt = 1.0

        def _dynamics_op(unused_s, t, dt):
            return dynamics.random_normal([num_samples], t, dt)

        payoff_fn = tf.identity

        initial_state = tf.constant(0.0)

        _, _, portfolio_returns = monte_carlo_manager.non_callable_price_mc(
            initial_state=initial_state,
            dynamics_op=_dynamics_op,
            payoff_fn=payoff_fn,
            maturity=maturity,
            num_samples=num_samples,
            dt=dt)

        var_ests, cvar_ests = risk_metrics.var_and_cvar(
            portfolio_returns, var_levels, key_placeholder, {})

        self.assertLen(var_ests, len(var_levels))
        self.assertLen(cvar_ests, len(var_levels))

        for i in range(len(var_ests) - 1):
            self.assertLessEqual(var_ests[i], var_ests[i + 1])
            self.assertLessEqual(cvar_ests[i], cvar_ests[i + 1])

        for i, var_level in enumerate(var_levels):
            self.assertAlmostEqual(var_ests[i],
                                   -scipy.stats.norm.isf(var_level),
                                   delta=1e-2)
Beispiel #5
0
    def test_european_call_log_euler_mc_close_to_black_scholes(self):
        current_price = 100.0
        r = interest_rate = 0.05
        vol = 0.2
        strike = 120.0
        maturity = 0.5
        dt = 0.01
        discount = tf.exp(-r * maturity)

        bs_call_price = util.black_scholes_call_price(current_price,
                                                      interest_rate, vol,
                                                      strike, maturity)

        num_samples = int(1e4)
        initial_state = tf.constant(current_price)

        dynamics_op = lambda s, t, dt: dynamics.gbm_log_euler_step(
            s, r, vol, t, dt)
        payoff_fn = lambda s: discount * payoffs.call_payoff(tf.exp(s), strike)

        (mean_outcome, mean_sq_outcome,
         _) = monte_carlo_manager.non_callable_price_mc(
             tf.log(initial_state), dynamics_op, payoff_fn, maturity,
             num_samples, dt)

        std_outcomes = util.stddev_est(mean_outcome, mean_sq_outcome)

        with self.test_session() as session:
            bs_call_price_eval = session.run(bs_call_price)
            mean_outcome_eval, std_outcomes_eval = session.run(
                (mean_outcome, std_outcomes))

        self.assertLessEqual(
            mean_outcome_eval, bs_call_price_eval +
            3.0 * std_outcomes_eval / np.sqrt(num_samples))
        self.assertGreaterEqual(
            mean_outcome_eval, bs_call_price_eval -
            3.0 * std_outcomes_eval / np.sqrt(num_samples))
def main(_):
    num_dims = FLAGS.num_dims
    num_batches = FLAGS.num_batches
    hist_drift = FLAGS.drift
    hist_vol = FLAGS.volatility
    hist_cor = FLAGS.correlation
    hist_cor_matrix = (hist_cor * np.ones(
        (num_dims, num_dims)) + (1.0 - hist_cor) * np.eye(num_dims))

    hist_price = FLAGS.initial_price * np.ones(num_dims)
    hist_vol_matrix = hist_vol * np.real(scipy.linalg.sqrtm(hist_cor_matrix))

    hist_dt = FLAGS.delta_t_historical
    sim_dt = FLAGS.delta_t_monte_carlo

    strike = FLAGS.strike
    maturity = FLAGS.maturity

    # Placeholders for tensorflow-based simulator's arguments.
    sim_price = tf.placeholder(shape=[num_dims], dtype=tf.float32)
    sim_drift = tf.placeholder(shape=(), dtype=tf.float32)
    sim_vol_matrix = tf.constant(hist_vol_matrix, dtype=tf.float32)
    sim_maturity = tf.placeholder(shape=(), dtype=tf.float32)

    # Transition operation between t and t + dt with price in log scale.
    def _dynamics_op(log_s, t, dt):
        return gbm_log_euler_step_nd(log_s, sim_drift, sim_vol_matrix, t, dt)

    # Terminal payoff function (with price in log scale).
    def _payoff_fn(log_s):
        return basket_call_payoff(tf.exp(log_s), strike)

    # Call's price and delta estimates (sensitivity to current underlying price).
    # Monte Carlo estimation under the risk neutral probability is used.
    # The reason why we employ the risk neutral probability is that the position
    # is hedged each day depending on the value of the underlying.
    # See http://www.cmap.polytechnique.fr/~touzi/Poly-MAP552.pdf for a complete
    # explanation.
    price_estimate, _, _ = monte_carlo_manager.non_callable_price_mc(
        initial_state=tf.log(sim_price),
        dynamics_op=_dynamics_op,
        payoff_fn=_payoff_fn,
        maturity=sim_maturity,
        num_samples=FLAGS.num_batch_samples,
        dt=sim_dt)
    delta_estimate = monte_carlo_manager.sensitivity_autodiff(
        price_estimate, sim_price)

    # Start the hedging experiment.
    session = tf.Session()

    hist_price_profile = []
    cash_profile = []
    underlying_profile = []
    wall_times = []

    t = 0
    cash_owned = 0.0
    underlying_owned = np.zeros(num_dims)
    while t <= maturity:
        # Each day, a new stock price is observed.

        cash_eval = 0.0
        delta_eval = 0.0
        for _ in range(num_batches):
            if t == 0.0:
                # The first day a derivative price is computed to decide how mush cash
                # is initially needed to replicate the derivative's payoff at maturity.
                cash_eval_batch = controllers.price_derivative(
                    price_estimate,
                    session,
                    params={
                        sim_drift: 0.0,
                        sim_price: hist_price,
                        sim_maturity: maturity - t
                    })

            # Each day the delta of the derivative is computed to decide how many
            # shares of the underlying should be owned to replicate the derivative's
            # payoff at maturity.
            start_time = time.time()
            delta_eval_batch = controllers.hedge_derivative(delta_estimate,
                                                            session,
                                                            params={
                                                                sim_drift:
                                                                0.0,
                                                                sim_price:
                                                                hist_price,
                                                                sim_maturity:
                                                                maturity - t
                                                            })
            wall_times.append(time.time() - start_time)
            delta_eval += delta_eval_batch / num_batches
            cash_eval += cash_eval_batch / num_batches

        if t == 0.0:
            logging.info("Initial price estimate: %.2f", cash_eval)

        # Self-financing portfolio dynamics, held cash is used to buy the underlying
        # or increases when the underlying is sold.
        if t == 0.0:
            cash_owned = cash_eval - np.sum(delta_eval * hist_price)
            underlying_owned = delta_eval
        else:
            cash_owned -= np.sum((delta_eval - underlying_owned) * hist_price)
            underlying_owned = delta_eval
        logging.info("Cash at t=%.2f: %.2f", t, cash_owned)
        logging.info("Mean delta at t=%.2f: %.4f", t, np.mean(delta_eval))
        logging.info("Mean underlying at t=%.2f: %.2f ", t,
                     np.mean(hist_price))

        hist_price_profile.append(hist_price)
        cash_profile.append(cash_owned)
        underlying_profile.append(underlying_owned)

        # Simulation of price movements under the historical probability (i.e. what
        # is actually happening in the stock market).
        hist_price = hist_log_euler_step(hist_price, hist_drift,
                                         hist_vol_matrix, hist_dt)

        t += hist_dt

    session.close()

    # At maturity, the value of the replicating portfolio should be exactly
    # the opposite of the payoff of the option being sold.
    # The reason why the match is not exact here is two-fold: we only hedge once
    # a day and we use noisy Monte Carlo estimates to do so.
    underlying_owned_value = np.sum(underlying_owned * hist_price)
    profit = np.sum(underlying_owned * hist_price) + cash_owned
    loss = (np.mean(hist_price) - strike) * (np.mean(hist_price) > strike)

    logging.info("Cash owned at maturity %.3f.", cash_owned)
    logging.info("Value of underlying owned at maturity %.3f.",
                 underlying_owned_value)
    logging.info("Profit (value held) = %.3f.", profit)
    logging.info("Loss (payoff sold, 0 if price is below strike) = %.3f.",
                 loss)
    logging.info("PnL (should be close to 0) = %.3f.", profit - loss)