7 votos

QuantLib: Procesos Black / BSM y fijación de precios a través de la superficie de volatilidad. ¿Diferentes resultados?

Comienzo esta pregunta con un par de funciones C++ que serán útiles para mostrar algunos resultados. Así que empieza tu Visual Studio C++ Express o Ceemple o lo que quieras y copia y pega esto:

#include <ql/quantlib.hpp>
#include <boost/timer.hpp>
#include <iostream>
#include <iomanip>

using namespace QuantLib;

#if defined(QL_ENABLE_SESSIONS)
namespace QuantLib {

    Integer sessionId() { return 0; }

}
#endif

Después de la introducción estándar, la primera función actúa como una pequeña envoltura: toma un shared_ptr de BlackVolTermStructure plantilla y algunos datos, se construye una curva de tipo libre de riesgo cero-plana y una BlackProcess se construye; por lo tanto, se devuelve el VAN de la opción.

double EurVanillaSurfacePricerBlack(boost::shared_ptr<BlackVolTermStructure> forwardVolSurface, Option::Type type, Real underlying, Real strike, Date maturity)
{
  Rate riskFreeRate = 0.00;
  DayCounter dayCounter = Actual365Fixed();
  Calendar calendar = TARGET();
  Natural settlementDays = 3;

  // exercise
  boost::shared_ptr<Exercise> europeanExercise(
      new EuropeanExercise(maturity));

  // underlying
  Handle<Quote> underlyingH(boost::shared_ptr<Quote>(
      new SimpleQuote(underlying)));

  // bootstrap the yield curve
  Handle<YieldTermStructure> flatTermStructure(
      boost::shared_ptr<YieldTermStructure>(
          new FlatForward(settlementDays, calendar, riskFreeRate, dayCounter)));

  // payoff
  boost::shared_ptr<StrikedTypePayoff> payoff(
      new PlainVanillaPayoff(type, strike));

  // process
  boost::shared_ptr<BlackProcess> blackProcess(
      new BlackProcess(underlyingH, flatTermStructure, Handle<BlackVolTermStructure>(forwardVolSurface)));

  // options
  VanillaOption europeanOption(payoff, europeanExercise);
  europeanOption.setPricingEngine(boost::shared_ptr<PricingEngine>(
                                       new AnalyticEuropeanEngine(blackProcess)));
  double optionValue = europeanOption.NPV();

  return(optionValue);
}

La misma función puede escribirse utilizando BlackScholesMertonProcess en lugar de BlackProcess y, por supuesto, especificando una estructura de plazos de rentabilidad de dividendos debido a que ya no estamos utilizando el precio a plazo subyacente... pero aquí lo ponemos todo a cero, tanto el tipo libre de riesgo como la rentabilidad de dividendos, dando así a la función estructuras de plazos planas y sin sentido:

double EurVanillaSurfacePricerBSM(boost::shared_ptr<BlackVolTermStructure> forwardVolSurface, Option::Type type, Real underlying, Real strike, Date maturity)
{
  Spread dividendYield = 0.00;
  Rate riskFreeRate = 0.00;
  DayCounter dayCounter = Actual365Fixed();
  Calendar calendar = TARGET();
  Natural settlementDays = 3;

  // exercise
  boost::shared_ptr<Exercise> europeanExercise(
      new EuropeanExercise(maturity));

  // underlying
  Handle<Quote> underlyingH(boost::shared_ptr<Quote>(
      new SimpleQuote(underlying)));

  // bootstrap the yield curve and the dividend curve
  Handle<YieldTermStructure> flatTermStructure(
      boost::shared_ptr<YieldTermStructure>(
          new FlatForward(settlementDays, calendar, riskFreeRate, dayCounter)));
  Handle<YieldTermStructure> flatDividendTS(
      boost::shared_ptr<YieldTermStructure>(
          new FlatForward(settlementDays, calendar, dividendYield, dayCounter)));

  // payoff
  boost::shared_ptr<StrikedTypePayoff> payoff(
      new PlainVanillaPayoff(type, strike));

  // process
  boost::shared_ptr<BlackScholesMertonProcess> bsmProcess(
      new BlackScholesMertonProcess(underlyingH, flatDividendTS, flatTermStructure, Handle<BlackVolTermStructure>(forwardVolSurface)));

  // options
  VanillaOption europeanOption(payoff, europeanExercise);
  europeanOption.setPricingEngine(boost::shared_ptr<PricingEngine>(
                                       new AnalyticEuropeanEngine(bsmProcess)));
  double optionValue = europeanOption.NPV();

  return(optionValue);
}

Según lo poco que sé de teoría de opciones, no debería haber ninguna diferencia entre ambas funciones: $$F(t)=S(0)e^{[r(t)-q(t)]t},$$ donde $q$ y $r$ son cero para cada vencimiento, por lo que $F=S(0)$ para cada madurez.

La tercera función es una pequeña variación sobre el mismo tema: en lugar de utilizar constructores estándar que requieren estructuras de términos, introducimos un valor de volatilidad constante en una superficie plana:

double EurVanillaPricer(Volatility volatility, Option::Type type, Real underlying, Real strike, Date maturity)
{
  Spread dividendYield = 0.00;
  Rate riskFreeRate = 0.00;
  DayCounter dayCounter = Actual365Fixed();
  Calendar calendar = TARGET();
  // This was "Natural settlementDays = 3;" before Luigi Ballabio's correction
  Natural settlementDays = 0;

  // exercise
  boost::shared_ptr<Exercise> europeanExercise(
      new EuropeanExercise(maturity));

  // underlying
  Handle<Quote> underlyingH(boost::shared_ptr<Quote>(
      new SimpleQuote(underlying)));

  // bootstrap the yield/dividend/vol curves
  Handle<YieldTermStructure> flatTermStructure(
      boost::shared_ptr<YieldTermStructure>(
          new FlatForward(settlementDays, calendar, riskFreeRate, dayCounter)));
  Handle<YieldTermStructure> flatDividendTS(
      boost::shared_ptr<YieldTermStructure>(
          new FlatForward(settlementDays, calendar, dividendYield, dayCounter)));
  Handle<BlackVolTermStructure> flatVolTS(
      boost::shared_ptr<BlackVolTermStructure>(
          new BlackConstantVol(settlementDays, calendar, volatility, dayCounter)));

  // payoff
  boost::shared_ptr<StrikedTypePayoff> payoff(
      new PlainVanillaPayoff(type, strike));

  // process
  boost::shared_ptr<BlackScholesMertonProcess> bsmProcess(
      new BlackScholesMertonProcess(underlyingH, flatDividendTS, flatTermStructure, flatVolTS));

  // options
  VanillaOption europeanOption(payoff, europeanExercise);
  europeanOption.setPricingEngine(boost::shared_ptr<PricingEngine>(
                                       new AnalyticEuropeanEngine(bsmProcess)));
  double optionValue = europeanOption.NPV();

  return(optionValue);
}

Por último, pero no menos importante, permítanme presentar una envoltura de superficie de volatilidad hacia adelante:

boost::shared_ptr<BlackVolTermStructure> ForwardImpliedVolSurface(Date todaysDate, Date forwardDate, Calendar calendar, std::vector<Date> maturityArray, std::vector<Real> strikeArray, Matrix volatilityMatrix)
{
  // Handle to boost::shared_ptr
  DayCounter dayCounter = Actual365Fixed();
  boost::shared_ptr<BlackVarianceSurface> volatilitySurface(new BlackVarianceSurface(todaysDate, calendar, maturityArray, strikeArray, volatilityMatrix, dayCounter));
  Handle<BlackVolTermStructure> volatilitySurfaceH(volatilitySurface);

  // Volatility surface interpolation
  volatilitySurface->enableExtrapolation(true);

  // Change interpolator to bicubic splines
  volatilitySurface->setInterpolation<Bicubic>(Bicubic());

  // Forward implied volatility surface
  boost::shared_ptr<BlackVolTermStructure> forwardVolSurface(new ImpliedVolTermStructure(volatilitySurfaceH, forwardDate));

  return(forwardVolSurface);
}

¿Cuáles son los resultados de estas funciones? Veamos:

int main() {

try {

    boost::timer timer;
    std::cout << std::endl;

    /* +---------------------------------------------------------------------------------------------------
     * | Date and calendars parameters
     * +---------------------------------------------------------------------------------------------------
     * */

    // set up dates
    Calendar calendar = TARGET();
    Date todaysDate(03, Jul, 2014);
    Date settlementDate = calendar.advance(todaysDate, 3, Days);
    Settings::instance().evaluationDate() = todaysDate;

    // Maturity dates array
    Date expiry1(15, Aug, 2014);
    Date expiry2(19, Sep, 2014);
    Date expiry3(19, Dec, 2014);
    Date expiry4(20, Mar, 2015);
    Date expiry5(19, Jun, 2015);

    std::vector<Date> maturityArray;
    maturityArray.push_back(expiry1);
    maturityArray.push_back(expiry2);
    maturityArray.push_back(expiry3);
    maturityArray.push_back(expiry4);
    maturityArray.push_back(expiry5);

    // Strikes array
    std::vector<Real> strikeArray;
    for(int i = 2975; i < 2975 + (26 * 25); i = i + 25)
    {
      strikeArray.push_back(i);
    }

    // Implied volatility matrix
        Matrix volatilityMatrix(26, 5);

        volatilityMatrix[0][0]  = 0.198989  ; volatilityMatrix[0][1]  = 0.182889 ; volatilityMatrix[0][2]  = 0.182256 ; volatilityMatrix[0][3]  = 0.183319 ; volatilityMatrix[0][4]  = 0.202197 ;
        volatilityMatrix[1][0]  = 0.192338  ; volatilityMatrix[1][1]  = 0.178463 ; volatilityMatrix[1][2]  = 0.17982  ; volatilityMatrix[1][3]  = 0.181494 ; volatilityMatrix[1][4]  = 0.201261 ;
        volatilityMatrix[2][0]  = 0.185184  ; volatilityMatrix[2][1]  = 0.174239 ; volatilityMatrix[2][2]  = 0.177315 ; volatilityMatrix[2][3]  = 0.179669 ; volatilityMatrix[2][4]  = 0.200291 ;
        volatilityMatrix[3][0]  = 0.178718  ; volatilityMatrix[3][1]  = 0.170046 ; volatilityMatrix[3][2]  = 0.175143 ; volatilityMatrix[3][3]  = 0.177845 ; volatilityMatrix[3][4]  = 0.19928  ;
        volatilityMatrix[4][0]  = 0.172647  ; volatilityMatrix[4][1]  = 0.166123 ; volatilityMatrix[4][2]  = 0.172826 ; volatilityMatrix[4][3]  = 0.176046 ; volatilityMatrix[4][4]  = 0.198271 ;
        volatilityMatrix[5][0]  = 0.166556  ; volatilityMatrix[5][1]  = 0.162275 ; volatilityMatrix[5][2]  = 0.170328 ; volatilityMatrix[5][3]  = 0.174391 ; volatilityMatrix[5][4]  = 0.19764  ;
        volatilityMatrix[6][0]  = 0.160933  ; volatilityMatrix[6][1]  = 0.158344 ; volatilityMatrix[6][2]  = 0.16825  ; volatilityMatrix[6][3]  = 0.172892 ; volatilityMatrix[6][4]  = 0.197454 ;
        volatilityMatrix[7][0]  = 0.155747  ; volatilityMatrix[7][1]  = 0.154688 ; volatilityMatrix[7][2]  = 0.166199 ; volatilityMatrix[7][3]  = 0.17105  ; volatilityMatrix[7][4]  = 0.196211 ;
        volatilityMatrix[8][0]  = 0.150464  ; volatilityMatrix[8][1]  = 0.151097 ; volatilityMatrix[8][2]  = 0.164325 ; volatilityMatrix[8][3]  = 0.16875  ; volatilityMatrix[8][4]  = 0.193533 ;
        volatilityMatrix[9][0]  = 0.145234  ; volatilityMatrix[9][1]  = 0.147602 ; volatilityMatrix[9][2]  = 0.16217  ; volatilityMatrix[9][3]  = 0.16793  ; volatilityMatrix[9][4]  = 0.195104 ;
        volatilityMatrix[10][0] = 0.140751  ; volatilityMatrix[10][1] = 0.144357 ; volatilityMatrix[10][2] = 0.160261 ; volatilityMatrix[10][3] = 0.169107 ; volatilityMatrix[10][4] = 0.202441 ;
        volatilityMatrix[11][0] = 0.136502  ; volatilityMatrix[11][1] = 0.141208 ; volatilityMatrix[11][2] = 0.158546 ; volatilityMatrix[11][3] = 0.165058 ; volatilityMatrix[11][4] = 0.194346 ;
        volatilityMatrix[12][0] = 0.13342   ; volatilityMatrix[12][1] = 0.138357 ; volatilityMatrix[12][2] = 0.156949 ; volatilityMatrix[12][3] = 0.15057  ; volatilityMatrix[12][4] = 0.155503 ;
        volatilityMatrix[13][0] = 0.104896  ; volatilityMatrix[13][1] = 0.119273 ; volatilityMatrix[13][2] = 0.128517 ; volatilityMatrix[13][3] = 0.136208 ; volatilityMatrix[13][4] = 0.116855 ;
        volatilityMatrix[14][0] = 0.10099   ; volatilityMatrix[14][1] = 0.115047 ; volatilityMatrix[14][2] = 0.125638 ; volatilityMatrix[14][3] = 0.132476 ; volatilityMatrix[14][4] = 0.109273 ;
        volatilityMatrix[15][0] = 0.100313  ; volatilityMatrix[15][1] = 0.114395 ; volatilityMatrix[15][2] = 0.125642 ; volatilityMatrix[15][3] = 0.133834 ; volatilityMatrix[15][4] = 0.117099 ;
        volatilityMatrix[16][0] = 0.0981065 ; volatilityMatrix[16][1] = 0.112273 ; volatilityMatrix[16][2] = 0.124137 ; volatilityMatrix[16][3] = 0.132863 ; volatilityMatrix[16][4] = 0.118885 ;
        volatilityMatrix[17][0] = 0.0962976 ; volatilityMatrix[17][1] = 0.109955 ; volatilityMatrix[17][2] = 0.122498 ; volatilityMatrix[17][3] = 0.130647 ; volatilityMatrix[17][4] = 0.116549 ;
        volatilityMatrix[18][0] = 0.0950343 ; volatilityMatrix[18][1] = 0.107924 ; volatilityMatrix[18][2] = 0.121311 ; volatilityMatrix[18][3] = 0.129627 ; volatilityMatrix[18][4] = 0.116142 ;
        volatilityMatrix[19][0] = 0.094729  ; volatilityMatrix[19][1] = 0.106211 ; volatilityMatrix[19][2] = 0.119952 ; volatilityMatrix[19][3] = 0.12918  ; volatilityMatrix[19][4] = 0.116873 ;
        volatilityMatrix[20][0] = 0.0952533 ; volatilityMatrix[20][1] = 0.104712 ; volatilityMatrix[20][2] = 0.118585 ; volatilityMatrix[20][3] = 0.128231 ; volatilityMatrix[20][4] = 0.116804 ;
        volatilityMatrix[21][0] = 0.0977423 ; volatilityMatrix[21][1] = 0.103553 ; volatilityMatrix[21][2] = 0.117229 ; volatilityMatrix[21][3] = 0.126978 ; volatilityMatrix[21][4] = 0.116249 ;
        volatilityMatrix[22][0] = 0.0992171 ; volatilityMatrix[22][1] = 0.102743 ; volatilityMatrix[22][2] = 0.115987 ; volatilityMatrix[22][3] = 0.125834 ; volatilityMatrix[22][4] = 0.115905 ;
        volatilityMatrix[23][0] = 0.102137  ; volatilityMatrix[23][1] = 0.1025   ; volatilityMatrix[23][2] = 0.114716 ; volatilityMatrix[23][3] = 0.124794 ; volatilityMatrix[23][4] = 0.115759 ;
        volatilityMatrix[24][0] = 0.108426  ; volatilityMatrix[24][1] = 0.102351 ; volatilityMatrix[24][2] = 0.113496 ; volatilityMatrix[24][3] = 0.123768 ; volatilityMatrix[24][4] = 0.115648 ;
        volatilityMatrix[25][0] = 0.111779  ; volatilityMatrix[25][1] = 0.102869 ; volatilityMatrix[25][2] = 0.112514 ; volatilityMatrix[25][3] = 0.12274  ; volatilityMatrix[25][4] = 0.11554  ;

/* +---------------------------------------------------------------------------------------------------
 * | Forward volatility (ref. pag. 154-157 of "Dynamic Hedging - Managing Vanilla and Exotic Options")
 * +---------------------------------------------------------------------------------------------------
 * */

    // As instance, go 15 days forward
    Date forwardDate = calendar.advance(todaysDate, 15, Days);

    boost::shared_ptr<BlackVolTermStructure> forwardVolSurface = ForwardImpliedVolSurface(todaysDate, forwardDate, calendar, maturityArray, strikeArray, volatilityMatrix);
    Option::Type typeCall(Option::Call);
    Option::Type typePut(Option::Put);

    Real underlying = 3289.75;

    double myOption4;
    myOption4 = EurVanillaSurfacePricerBlack(forwardVolSurface, typeCall, underlying, 3300, expiry3);
    //disp(myOption4);
    double myOption5;
    myOption5 = EurVanillaSurfacePricerBSM(forwardVolSurface, typeCall, underlying, 3300, expiry3);
    //disp(myOption5);
    double myOption6;
    myOption6 = EurVanillaPricer(forwardVolSurface->blackVol(expiry3, 3300), typeCall, underlying, 3300, expiry3);
    //disp(myOption6);

    // Amend evaluation date...
    Settings::instance().evaluationDate() = forwardDate;

    double myOption1;
    myOption1 = EurVanillaSurfacePricerBlack(forwardVolSurface, typeCall, underlying, 3300, expiry3);
    //disp(myOption1);
    double myOption2;
    myOption2 = EurVanillaSurfacePricerBSM(forwardVolSurface, typeCall, underlying, 3300, expiry3);
    //disp(myOption2);
    double myOption3;
    myOption3 = EurVanillaPricer(forwardVolSurface->blackVol(expiry3, 3300), typeCall, underlying, 3300, expiry3);
    //disp(myOption3);

        return 0;

    } catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
        return 1;
    } catch (...) {
        std::cerr << "unknown error" << std::endl;
        return 1;
    }
}

¿Cuáles son los resultados? Bueno, por supuesto que el modelo Black y el modelo BSM con estructuras de términos planos y nulos devuelven los mismos valores, es decir $105.743$ para ambas opciones. Sin embargo, a pesar de esto, la selección "manual" de la volatilidad implícita a utilizar desde la superficie de volatilidad implícita a través de forwardVolSurface->blackVol(expiry3, 3300) devuelve un valor muy diferente, es decir, $103.858$ .

¿Cómo explicaría esta diferencia?

Lo que me temo es... el desplazamiento automático de la superficie de volatilidad cuando se modifica la fecha de evaluación. Intenté escribir código para evitar tal comportamiento reteniendo asas revinculables en las funciones, para que sean destruidas una vez que la función termina.

Pero no estoy seguro de que funcione como se pretende.

8voto

Brad Tutterow Puntos 5628

Es debido a los días de liquidación que pasó cuando inicializó la curva de volatilidad plana. Estás creando las volatilidades spot, forward y flat como:

boost::shared_ptr<BlackVarianceSurface> volatilitySurface(
    new BlackVarianceSurface(todaysDate, calendar,
                             maturityArray, strikeArray,
                             volatilityMatrix, dayCounter));

boost::shared_ptr<BlackVolTermStructure> forwardVolSurface(
    new ImpliedVolTermStructure(volatilitySurfaceH, forwardDate));

boost::shared_ptr<BlackVolTermStructure> flatVolCurve(
      new BlackConstantVol(settlementDays, calendar,
                           forwardVolSurface->blackVol(expiry3, 3300),
                           dayCounter)));

(los dos primeros son textuales de tu código; en el último, he unido tu llamada para recuperar la volatilidad en main a la llamada del constructor en EurVanillaPricer ). En la última, has puesto settlementDays a 3.

Ahora, la curva a plazo y la curva plana tienen la misma volatilidad para el ejercicio y la huelga que usted especificó; si usted ejecuta

std::cout << forwardVolSurface->blackVol(expiry3, 3300) << std::endl;
std::cout << flatVolTS->blackVol(expiry3, 3300) << std::endl;

que te devolverá

0.132405
0.132405

es decir, lo mismo. Pero esa no es toda la historia, por desgracia. El motor de precios de la opción europea no les pide la volatilidad , sino directamente la varianza ( $\sigma^2 T$ ). Si lo haces:

std::cout << forwardVolSurface->blackVariance(expiry3, 3300) << std::endl;
std::cout << flatVolTS->blackVariance(expiry3, 3300) << std::endl;

obtendrá cifras diferentes:

0.00710851
0.00686836

¿Por qué? Porque las dos curvas tienen fechas de referencia diferentes:

std::cout << forwardVolSurface->referenceDate() << std::endl;
std::cout << flatVolTS->referenceDate() << std::endl;

da

July 24th, 2014
July 29th, 2014

En el constructor de forwardVolSurface se está pasando la fecha de referencia directamente: es forwardDate , que también establece como fecha de evaluación. En el constructor de flatVolTS , estás pasando settlementDays y calendar , que es igual a 3 y TARGET() respectivamente. Esto significa que la fecha de referencia es tres días hábiles después de la fecha de evaluación, que resulta ser 5 días después si se tiene en cuenta el fin de semana.

Así, cuando las dos curvas calculan la varianza $\sigma^2 T$ la volatilidad $\sigma$ es el mismo; pero para la primera curva, $T$ es el tiempo entre el 24 de julio y el vencimiento, mientras que para la segunda curva $T$ es el tiempo que transcurre entre el 29 de julio y el vencimiento y es 5 días más corto.

Para resolver su problema, basta con pasar 0 como settlementDays al crear la curva plana. Tendrá la misma fecha de referencia que la curva a plazo, y los precios de las opciones serán los mismos. (No es necesario que sean los mismos que los días de liquidación que se pasan a las curvas libres de riesgo y de dividendos).

Nota 1: En general, podría no ser una buena idea pasar un número no nulo de días de liquidación a una estructura de plazo de volatilidad (espero que no sean demasiados negativos) (maldición, lo hice de nuevo). La posibilidad de tener días de liquidación está ahí porque se heredó de la base TermStructure y no pensamos en restringirlo, pero se añadió sobre todo para las curvas de tipos de interés en las que se quiera trabajar al momento (es decir, a dos días vista, que es la fecha de liquidación de la mayoría de los instrumentos de tipos de interés cotizados). En el caso de la renta variable, lo más probable es que se quiera empezar desde la fecha de evaluación.

Nota 2: las distintas formas de inicializar las estructuras de términos y su funcionamiento se explican con más detalle en http://www.implementingquantlib.com/2013/09/chapter-3-part-1-of-n-term-structures.html .

Finanhelp.com

FinanHelp es una comunidad para personas con conocimientos de economía y finanzas, o quiere aprender. Puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X