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