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))
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])
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))
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)
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)