/** Tests for interest rate future options analytics functions using the black calculator. */
@Test(groups = TestGroup.UNIT)
public class IRFutureOptionFnTest {

  private static final ZonedDateTime VALUATION_TIME = DateUtils.getUTCDate(2014, 1, 22);
  public static final LocalDate TRADE_DATE = LocalDate.of(2000, 1, 1);
  public static final OffsetTime TRADE_TIME = OffsetTime.of(LocalTime.of(0, 0), ZoneOffset.UTC);
  private static final LocalDate MARKET_DATA_DATE = LocalDate.of(2014, 2, 18);
  private static final double STD_TOLERANCE_PV = 1.0E-3;
  private IRFutureOptionFn _blackIRFutureOptionFn;
  private IRFutureOptionFn _normalIRFutureOptionFn;
  private InterestRateFutureSecurity _irFuture = createIRFuture();
  private IRFutureOptionTrade _irFutureOptionTrade = createIRFutureOptionTrade();
  private FunctionRunner _functionRunner;
  private static final CalculationArguments ARGS =
      CalculationArguments.builder()
          .valuationTime(VALUATION_TIME)
          .marketDataSpecification(LiveMarketDataSpecification.LIVE_SPEC)
          .build();
  private static final Interpolator1D LINEAR_FLAT =
      CombinedInterpolatorExtrapolatorFactory.getInterpolator(
          Interpolator1DFactory.LINEAR,
          Interpolator1DFactory.FLAT_EXTRAPOLATOR,
          Interpolator1DFactory.FLAT_EXTRAPOLATOR);
  private static final GridInterpolator2D INTERPOLATOR_2D =
      new GridInterpolator2D(LINEAR_FLAT, LINEAR_FLAT);
  private static final InterpolatedDoublesSurface TEST_SURFACE =
      InterpolatedDoublesSurface.from(
          new double[] {.1, .2, .3},
          new double[] {.1, .2, .3},
          new double[] {.1, .2, .3},
          INTERPOLATOR_2D);

  private static MarketDataEnvironment createSuppliedData() {
    LocalDateDoubleTimeSeries optionPrice =
        ImmutableLocalDateDoubleTimeSeries.of(VALUATION_TIME.toLocalDate(), 0.975);
    RawId<Double> optionRawId =
        RawId.of(ExternalSchemes.syntheticSecurityId("Test future option").toBundle());
    MarketDataEnvironmentBuilder builder = new MarketDataEnvironmentBuilder();
    builder.add(optionRawId, optionPrice);
    builder.add(VolatilitySurfaceId.of("TestExchange"), new VolatilitySurface(TEST_SURFACE));
    builder.valuationTime(VALUATION_TIME);
    return builder.build();
  }

  @BeforeClass
  public void setUpClass() {

    ImmutableMap<Class<?>, Object> components = generateComponents();
    VersionCorrectionProvider vcProvider = new FixedInstantVersionCorrectionProvider(Instant.now());
    ServiceContext serviceContext =
        ServiceContext.of(components).with(VersionCorrectionProvider.class, vcProvider);
    ThreadLocalServiceContext.init(serviceContext);

    ComponentMap componentMap = ComponentMap.of(components);
    MarketDataSource marketDataSource =
        InterestRateMockSources.createMarketDataSource(MARKET_DATA_DATE, true);
    TestMarketDataFactory marketDataFactory = new TestMarketDataFactory(marketDataSource);
    ConfigLink<CurrencyMatrix> currencyMatrixLink =
        ConfigLink.resolved(componentMap.getComponent(CurrencyMatrix.class));
    List<MarketDataBuilder> builders =
        MarketDataBuilders.standard(componentMap, "dataSource", currencyMatrixLink);

    MarketDataEnvironmentFactory environmentFactory =
        new MarketDataEnvironmentFactory(marketDataFactory, builders);

    _functionRunner = new FunctionRunner(environmentFactory);
    _normalIRFutureOptionFn =
        FunctionModel.build(IRFutureOptionFn.class, normalConfig(), componentMap);
    _blackIRFutureOptionFn =
        FunctionModel.build(IRFutureOptionFn.class, blackConfig(), componentMap);
  }

  private FunctionModelConfig blackConfig() {
    FunctionModelConfig config =
        config(
            arguments(
                function(
                    MarketExposureSelector.class,
                    argument(
                        "exposureFunctions",
                        ConfigLink.resolved(InterestRateMockSources.mockExposureFunctions()))),
                function(
                    RootFinderConfiguration.class,
                    argument("rootFinderAbsoluteTolerance", 1e-9),
                    argument("rootFinderRelativeTolerance", 1e-9),
                    argument("rootFinderMaxIterations", 1000)),
                function(
                    DefaultCurveNodeConverterFn.class,
                    argument("timeSeriesDuration", RetrievalPeriod.of(Period.ofYears(1))))),
            implementations(
                IRFutureOptionFn.class, DefaultIRFutureOptionFn.class,
                IRFutureOptionCalculatorFactory.class, IRFutureOptionBlackCalculatorFactory.class,
                CurveSpecificationMarketDataFn.class, DefaultCurveSpecificationMarketDataFn.class,
                FXMatrixFn.class, DefaultFXMatrixFn.class,
                BlackSTIRFuturesProviderFn.class, TestBlackSTIRFuturesProviderFn.class,
                DiscountingMulticurveCombinerFn.class,
                    ExposureFunctionsDiscountingMulticurveCombinerFn.class,
                CurveDefinitionFn.class, DefaultCurveDefinitionFn.class,
                CurveLabellingFn.class, CurveDefinitionCurveLabellingFn.class,
                CurveSpecificationFn.class, DefaultCurveSpecificationFn.class,
                CurveConstructionConfigurationSource.class,
                    ConfigDBCurveConstructionConfigurationSource.class,
                CurveNodeConverterFn.class, DefaultCurveNodeConverterFn.class,
                HistoricalMarketDataFn.class, DefaultHistoricalMarketDataFn.class,
                FixingsFn.class, DefaultFixingsFn.class,
                MarketDataFn.class, DefaultMarketDataFn.class,
                CurveSelector.class, MarketExposureSelector.class,
                DiscountingMulticurveCombinerFn.class, CurveSelectorMulticurveBundleFn.class));

    return config;
  }

  private FunctionModelConfig normalConfig() {
    FunctionModelConfig config =
        config(
            arguments(
                function(
                    MarketExposureSelector.class,
                    argument(
                        "exposureFunctions",
                        ConfigLink.resolved(InterestRateMockSources.mockExposureFunctions()))),
                function(
                    RootFinderConfiguration.class,
                    argument("rootFinderAbsoluteTolerance", 1e-9),
                    argument("rootFinderRelativeTolerance", 1e-9),
                    argument("rootFinderMaxIterations", 1000)),
                function(
                    TestIRFutureOptionNormalSurfaceProviderFn.class,
                    argument("moneynessOnPrice", false)),
                function(
                    DefaultCurveNodeConverterFn.class,
                    argument("timeSeriesDuration", RetrievalPeriod.of(Period.ofYears(1))))),
            implementations(
                IRFutureOptionFn.class, DefaultIRFutureOptionFn.class,
                IRFutureOptionCalculatorFactory.class, IRFutureOptionNormalCalculatorFactory.class,
                CurveSpecificationMarketDataFn.class, DefaultCurveSpecificationMarketDataFn.class,
                FXMatrixFn.class, DefaultFXMatrixFn.class,
                CurveDefinitionFn.class, DefaultCurveDefinitionFn.class,
                CurveLabellingFn.class, CurveDefinitionCurveLabellingFn.class,
                CurveSpecificationFn.class, DefaultCurveSpecificationFn.class,
                CurveConstructionConfigurationSource.class,
                    ConfigDBCurveConstructionConfigurationSource.class,
                CurveNodeConverterFn.class, DefaultCurveNodeConverterFn.class,
                HistoricalMarketDataFn.class, DefaultHistoricalMarketDataFn.class,
                FixingsFn.class, DefaultFixingsFn.class,
                MarketDataFn.class, DefaultMarketDataFn.class,
                CurveSelector.class, MarketExposureSelector.class,
                IRFutureOptionNormalSurfaceProviderFn.class,
                    TestIRFutureOptionNormalSurfaceProviderFn.class,
                DiscountingMulticurveCombinerFn.class, CurveSelectorMulticurveBundleFn.class));

    return config;
  }

  private ImmutableMap<Class<?>, Object> generateComponents() {
    ImmutableMap.Builder<Class<?>, Object> builder = ImmutableMap.builder();
    for (Map.Entry<Class<?>, Object> entry :
        InterestRateMockSources.generateBaseComponents().entrySet()) {
      Class<?> key = entry.getKey();
      if (key.equals(SecuritySource.class)) {
        appendSecuritySource((SecuritySource) entry.getValue());
      }
      builder.put(key, entry.getValue());
    }
    return builder.build();
  }

  // TODO - this assumes knowledge of the underlying source, should find a better way to do this
  private void appendSecuritySource(SecuritySource source) {
    SecurityMaster master = ((MasterSecuritySource) source).getMaster();
    master.add(new SecurityDocument(_irFuture));
  }

  private InterestRateFutureSecurity createIRFuture() {
    Expiry expiry =
        new Expiry(ZonedDateTime.of(LocalDate.of(2014, 6, 18), LocalTime.of(0, 0), ZoneOffset.UTC));
    String tradingExchange = "";
    String settlementExchange = "";
    Currency currency = Currency.USD;
    double unitAmount = 1000;
    ExternalId underlyingId = InterestRateMockSources.getLiborIndexId();
    String category = "";
    InterestRateFutureSecurity irFuture =
        new InterestRateFutureSecurity(
            expiry,
            tradingExchange,
            settlementExchange,
            currency,
            unitAmount,
            underlyingId,
            category);
    // Need this for time series lookup
    ExternalId irFutureId = ExternalSchemes.syntheticSecurityId("Test future");
    irFuture.setExternalIdBundle(irFutureId.toBundle());
    return irFuture;
  }

  private IRFutureOptionTrade createIRFutureOptionTrade() {

    String exchange = "TestExchange";
    ExerciseType exerciseType = new EuropeanExerciseType();
    double pointValue = Double.NaN;
    boolean margined = true;
    double strike = 0.99;
    OptionType optionType = OptionType.PUT;
    ExternalId irFutureId = Iterables.getOnlyElement(_irFuture.getExternalIdBundle());
    IRFutureOptionSecurity irFutureOption =
        new IRFutureOptionSecurity(
            exchange,
            _irFuture.getExpiry(),
            exerciseType,
            irFutureId,
            pointValue,
            margined,
            _irFuture.getCurrency(),
            strike,
            optionType);
    // Need this for time series lookup
    irFutureOption.setExternalIdBundle(
        ExternalSchemes.syntheticSecurityId("Test future option").toBundle());

    Counterparty counterparty =
        new SimpleCounterparty(ExternalId.of(Counterparty.DEFAULT_SCHEME, "COUNTERPARTY"));
    BigDecimal tradeQuantity = BigDecimal.valueOf(1);
    SimpleTrade trade =
        new SimpleTrade(irFutureOption, tradeQuantity, counterparty, TRADE_DATE, TRADE_TIME);
    trade.setPremium(10.0);
    trade.setPremiumCurrency(Currency.USD);
    return new IRFutureOptionTrade(trade);
  }

  @Test
  public void testBlackPresentValue() {
    Result<MultipleCurrencyAmount> result =
        _functionRunner.runFunction(
            ARGS,
            createSuppliedData(),
            new Function<Environment, Result<MultipleCurrencyAmount>>() {
              @Override
              public Result<MultipleCurrencyAmount> apply(Environment env) {
                return _blackIRFutureOptionFn.calculatePV(env, _irFutureOptionTrade);
              }
            });
    assertSuccess(result);

    MultipleCurrencyAmount mca = result.getValue();
    assertThat(
        mca.getCurrencyAmount(Currency.USD).getAmount(),
        is(closeTo(-972.460677, STD_TOLERANCE_PV)));
  }

  @Test
  public void testBlackBucketedZeroDelta() {
    Result<BucketedCurveSensitivities> result =
        _functionRunner.runFunction(
            ARGS,
            createSuppliedData(),
            new Function<Environment, Result<BucketedCurveSensitivities>>() {
              @Override
              public Result<BucketedCurveSensitivities> apply(Environment env) {
                return _blackIRFutureOptionFn.calculateBucketedZeroIRDelta(
                    env, _irFutureOptionTrade);
              }
            });
    assertSuccess(result);
  }

  @Test
  public void testNormalPresentValue() {
    Result<MultipleCurrencyAmount> result =
        _functionRunner.runFunction(
            ARGS,
            createSuppliedData(),
            new Function<Environment, Result<MultipleCurrencyAmount>>() {
              @Override
              public Result<MultipleCurrencyAmount> apply(Environment env) {
                return _normalIRFutureOptionFn.calculatePV(env, _irFutureOptionTrade);
              }
            });
    assertSuccess(result);
    MultipleCurrencyAmount mca = result.getValue();
    assertThat(
        mca.getCurrencyAmount(Currency.USD).getAmount(),
        is(closeTo(-902.7156551, STD_TOLERANCE_PV)));
  }

  @Test
  public void testNormalPrice() {
    Result<Double> result =
        _functionRunner.runFunction(
            ARGS,
            createSuppliedData(),
            new Function<Environment, Result<Double>>() {
              @Override
              public Result<Double> apply(Environment env) {
                return _normalIRFutureOptionFn.calculateModelPrice(env, _irFutureOptionTrade);
              }
            });
    assertSuccess(result);
    Double price = result.getValue();
    assertThat(price, is(closeTo(0.072284344, STD_TOLERANCE_PV)));
  }

  @Test
  public void testNormalBucketedZeroDelta() {
    Result<BucketedCurveSensitivities> result =
        _functionRunner.runFunction(
            ARGS,
            createSuppliedData(),
            new Function<Environment, Result<BucketedCurveSensitivities>>() {
              @Override
              public Result<BucketedCurveSensitivities> apply(Environment env) {
                return _normalIRFutureOptionFn.calculateBucketedZeroIRDelta(
                    env, _irFutureOptionTrade);
              }
            });
    assertSuccess(result);
  }
}
 @Override
 public OffsetTime deserialize(JsonParser parser, DeserializationContext context)
     throws IOException {
   if (parser.hasToken(JsonToken.VALUE_STRING)) {
     String string = parser.getText().trim();
     if (string.length() == 0) {
       return null;
     }
     try {
       return OffsetTime.parse(string, _formatter);
     } catch (DateTimeException e) {
       _rethrowDateTimeException(parser, context, e, string);
     }
   }
   if (!parser.isExpectedStartArrayToken()) {
     if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
       return (OffsetTime) parser.getEmbeddedObject();
     }
     throw context.wrongTokenException(parser, JsonToken.START_ARRAY, "Expected array or string.");
   }
   int hour = parser.nextIntValue(-1);
   if (hour == -1) {
     JsonToken t = parser.getCurrentToken();
     if (t == JsonToken.END_ARRAY) {
       return null;
     }
     if (t != JsonToken.VALUE_NUMBER_INT) {
       _reportWrongToken(parser, context, JsonToken.VALUE_NUMBER_INT, "hours");
     }
     hour = parser.getIntValue();
   }
   int minute = parser.nextIntValue(-1);
   if (minute == -1) {
     JsonToken t = parser.getCurrentToken();
     if (t == JsonToken.END_ARRAY) {
       return null;
     }
     if (t != JsonToken.VALUE_NUMBER_INT) {
       _reportWrongToken(parser, context, JsonToken.VALUE_NUMBER_INT, "minutes");
     }
     minute = parser.getIntValue();
   }
   int partialSecond = 0;
   int second = 0;
   if (parser.nextToken() == JsonToken.VALUE_NUMBER_INT) {
     second = parser.getIntValue();
     if (parser.nextToken() == JsonToken.VALUE_NUMBER_INT) {
       partialSecond = parser.getIntValue();
       if (partialSecond < 1_000
           && !context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)) {
         partialSecond *= 1_000_000; // value is milliseconds, convert it to nanoseconds
       }
       parser.nextToken();
     }
   }
   if (parser.getCurrentToken() == JsonToken.VALUE_STRING) {
     OffsetTime t =
         OffsetTime.of(hour, minute, second, partialSecond, ZoneOffset.of(parser.getText()));
     if (parser.nextToken() != JsonToken.END_ARRAY) {
       _reportWrongToken(parser, context, JsonToken.END_ARRAY, "timezone");
     }
     return t;
   }
   throw context.wrongTokenException(
       parser, JsonToken.VALUE_STRING, "Expected string for TimeZone after numeric values");
 }
public class TestPersistentOffsetTimeAsLongAndStringOffset extends DatabaseCapable {

  private static final OffsetTime[] offsetTimes =
      new OffsetTime[] {
        OffsetTime.of(LocalTime.of(12, 10, 31), ZoneOffset.UTC),
        OffsetTime.of(LocalTime.of(23, 7, 43, 120), ZoneOffset.ofHours(2))
      };

  private static EntityManagerFactory factory;

  @BeforeClass
  public static void setup() {
    factory = Persistence.createEntityManagerFactory("test1");
  }

  @AfterClass
  public static void tearDown() {
    factory.close();
  }

  @Test
  public void testPersist() {

    EntityManager manager = factory.createEntityManager();

    manager.getTransaction().begin();

    for (int i = 0; i < offsetTimes.length; i++) {

      OffsetTimeAsLongAndStringOffsetHolder item = new OffsetTimeAsLongAndStringOffsetHolder();
      item.setId(i);
      item.setName("test_" + i);
      item.setOffsetTime(offsetTimes[i]);

      manager.persist(item);
    }

    manager.flush();

    manager.getTransaction().commit();

    manager.close();

    manager = factory.createEntityManager();

    for (int i = 0; i < offsetTimes.length; i++) {

      OffsetTimeAsLongAndStringOffsetHolder item =
          manager.find(OffsetTimeAsLongAndStringOffsetHolder.class, Long.valueOf(i));

      assertNotNull(item);
      assertEquals(i, item.getId());
      assertEquals("test_" + i, item.getName());
      assertEquals(offsetTimes[i], item.getOffsetTime());
    }

    verifyDatabaseTable(
        manager, OffsetTimeAsLongAndStringOffsetHolder.class.getAnnotation(Table.class).name());

    manager.close();
  }
}