diff --git a/exotx/_version.py b/exotx/_version.py index 63af887..364e7ba 100644 --- a/exotx/_version.py +++ b/exotx/_version.py @@ -1 +1 @@ -__version__ = "0.6.3" +__version__ = "0.6.4" diff --git a/exotx/helpers/dates.py b/exotx/helpers/dates.py index 1e36e76..3ef0609 100644 --- a/exotx/helpers/dates.py +++ b/exotx/helpers/dates.py @@ -5,6 +5,20 @@ def convert_maturity_to_ql_date(maturity: Union[str, datetime, ql.Date], string_format: str = '%Y-%m-%d') -> ql.Date: + """ + Converts a maturity date in various formats to a QuantLib Date object. + + This function accepts input dates as strings, Python datetime objects, or QuantLib Date objects and + returns the corresponding QuantLib Date object. + + :param maturity: The maturity date to be converted, which can be a string, datetime, or QuantLib Date object. + :type maturity: Union[str, datetime, ql.Date] + :param string_format: The date format for string input, defaults to '%Y-%m-%d'. + :type string_format: str, optional + :return: The converted maturity date as a QuantLib Date object. + :rtype: ql.Date + :raises TypeError: If the input maturity type is not valid. + """ if isinstance(maturity, ql.Date): return maturity elif isinstance(maturity, datetime): diff --git a/exotx/instruments/asian_option.py b/exotx/instruments/asian_option.py index 0c47ae2..43554c2 100644 --- a/exotx/instruments/asian_option.py +++ b/exotx/instruments/asian_option.py @@ -17,6 +17,26 @@ class AsianOption(Instrument): + """ + A class representing an Asian option, which is an option type that derives its value from the average price of + the underlying asset over a specified period. + + The AsianOption class extends the Instrument class and provides functionality for constructing and pricing + Asian options using the QuantLib library. + + Attributes: + strike (float): The strike price of the option. + maturity (Union[str, datetime]): The maturity date of the option as a string or datetime object. + option_type (Union[str, OptionType]): The type of the option (call or put) as a string or OptionType object. + average_type (Union[str, AverageType]): The averaging type of the option as a string or AverageType object. + average_calculation (Union[str, AverageCalculation]): The average calculation method for the option as a string or AverageCalculation object. + average_convention (Union[str, AverageConvention]): The averaging convention of the option as a string or AverageConvention object. + arithmetic_running_accumulator (float, optional): The arithmetic running accumulator for the option. Defaults to 0.0. + geometric_running_accumulator (float, optional): The geometric running accumulator for the option. Defaults to 1.0. + past_fixings (int, optional): The number of past fixings for the option. Defaults to 0. + future_fixing_dates (List[datetime], optional): A list of future fixing dates for the option. Defaults to None. + """ + def __init__(self, strike: float, maturity: Union[str, datetime], option_type: Union[str, OptionType], average_type: Union[str, AverageType], average_calculation: Union[str, AverageCalculation], @@ -28,8 +48,10 @@ def __init__(self, self.maturity = convert_maturity_to_ql_date(maturity) self.option_type = convert_option_type_to_ql(option_type) self.average_type = convert_average_type_to_ql(average_type) - self.average_calculation: AverageCalculation = convert_average_calculation(average_calculation) - self.average_convention: AverageConvention = convert_average_convention(average_convention) + self.average_calculation: AverageCalculation = convert_average_calculation( + average_calculation) + self.average_convention: AverageConvention = convert_average_convention( + average_convention) self.arithmetic_running_accumulator = arithmetic_running_accumulator self.geometric_running_accumulator = geometric_running_accumulator self.past_fixings = past_fixings @@ -37,6 +59,20 @@ def __init__(self, for future_fixing_date in future_fixing_dates] def price(self, market_data, static_data, pricing_config: PricingConfiguration, seed: int = 1) -> dict: + """ + Prices the Asian option using the given market data, static data, and pricing configuration. + + :param market_data: An object containing the market data needed to price the option. + :type market_data: MarketData + :param static_data: An object containing the static data needed to price the option. + :type static_data: StaticData + :param pricing_config: A configuration object for the pricing process. + :type pricing_config: PricingConfiguration + :param seed: An optional integer seed for random number generation. Defaults to 1. + :type seed: int + :return: A dictionary containing the option price and, if applicable, Greeks. + :rtype: dict + """ reference_date: ql.Date = market_data.get_ql_reference_date() ql.Settings.instance().evaluationDate = reference_date @@ -49,7 +85,8 @@ def price(self, market_data, static_data, pricing_config: PricingConfiguration, ql_payoff = ql.PlainVanillaPayoff(self.option_type, self.strike) ql_exercise = ql.EuropeanExercise(self.maturity) if self.average_calculation == AverageCalculation.CONTINUOUS: - ql_option = ql.ContinuousAveragingAsianOption(self.average_type, ql_payoff, ql_exercise) + ql_option = ql.ContinuousAveragingAsianOption( + self.average_type, ql_payoff, ql_exercise) elif self.average_calculation == AverageCalculation.DISCRETE: if self.average_type == ql.Average().Arithmetic: ql_option = ql.DiscreteAveragingAsianOption(self.average_type, @@ -61,12 +98,15 @@ def price(self, market_data, static_data, pricing_config: PricingConfiguration, self.past_fixings, self.future_fixing_dates, ql_payoff, ql_exercise) else: - raise ValueError(f"Invalid average type \"{self.average_type}\"") + raise ValueError( + f"Invalid average type \"{self.average_type}\"") else: - raise ValueError(f"Invalid average calculation \"{self.average_calculation}\"") + raise ValueError( + f"Invalid average calculation \"{self.average_calculation}\"") # set the pricing engine - ql_engine = self._get_ql_pricing_engine(market_data, static_data, pricing_config, seed) + ql_engine = self._get_ql_pricing_engine( + market_data, static_data, pricing_config, seed) ql_option.setPricingEngine(ql_engine) # price @@ -91,12 +131,14 @@ def _get_ql_pricing_engine(self, market_data, static_data, pricing_config: Prici elif self.average_convention == AverageConvention.STRIKE: return ql.AnalyticDiscreteGeometricAverageStrikeAsianEngine(process) else: - raise ValueError(f"Invalid average convention \"{self.average_convention}\"") + raise ValueError( + f"Invalid average convention \"{self.average_convention}\"") elif pricing_config.numerical_method == NumericalMethod.MC: # TODO: filter on pricing_config.pricing_model, here we assume black-scholes only bs_model = BlackScholesModel(market_data, static_data) process = bs_model.setup() - random_number_generator = str(pricing_config.random_number_generator) + random_number_generator = str( + pricing_config.random_number_generator) if self.average_convention == AverageConvention.PRICE: return ql.MCDiscreteGeometricAPEngine(process, random_number_generator) elif self.average_convention == AverageConvention.STRIKE: @@ -104,7 +146,8 @@ def _get_ql_pricing_engine(self, market_data, static_data, pricing_config: Prici f"No corresponding engine for asian option for numerical method {pricing_config.numerical_method}, " f"average type {self.average_type}, average calculation {self.average_calculation}, and average convention {self.average_convention}") else: - raise ValueError(f"Invalid average convention \"{self.average_convention}\"") + raise ValueError( + f"Invalid average convention \"{self.average_convention}\"") else: raise ValueError( f"No engine for asian option with numerical method {pricing_config.numerical_method}" @@ -113,13 +156,15 @@ def _get_ql_pricing_engine(self, market_data, static_data, pricing_config: Prici if pricing_config.numerical_method == NumericalMethod.MC: bs_model = BlackScholesModel(market_data, static_data) process = bs_model.setup() - random_number_generator = str(pricing_config.random_number_generator) + random_number_generator = str( + pricing_config.random_number_generator) if self.average_convention == AverageConvention.PRICE: return ql.MCDiscreteArithmeticAPEngine(process, random_number_generator) elif self.average_convention == AverageConvention.STRIKE: return ql.MCDiscreteArithmeticASEngine(process, random_number_generator) else: - raise ValueError(f"Invalid average convention \"{self.average_convention}\"") + raise ValueError( + f"Invalid average convention \"{self.average_convention}\"") else: raise ValueError(f"Invalid average type {self.average_type}") elif self.average_calculation == AverageCalculation.CONTINUOUS: @@ -136,7 +181,8 @@ def _get_ql_pricing_engine(self, market_data, static_data, pricing_config: Prici else: raise ValueError("No engine for asian option") else: - raise ValueError(f"Invalid average calculation \"{self.average_calculation}\"") + raise ValueError( + f"Invalid average calculation \"{self.average_calculation}\"") # region serialization/deserialization def to_json(self): @@ -153,6 +199,34 @@ def from_json(cls, json_data): # region Schema class AsianOptionSchema(Schema): + """ + AsianOptionSchema is a Marshmallow schema class for deserializing and validating JSON data into an AsianOption object. + + This schema defines the required fields for an AsianOption object and validates their types and values. It also provides + a post_load method to create an AsianOption object after deserialization and validation. + + Fields: + - strike (float): The option's strike price. + - maturity (date): The option's maturity date in the format "YYYY-MM-DD". + - option_type (OptionType): The option type, either "call" or "put". + - average_type (AverageType): The averaging type, either "arithmetic" or "geometric". + - average_calculation (AverageCalculation): The averaging calculation, either "continuous" or "discrete". + - average_convention (AverageConvention): The average convention, either "price" or "strike". + + Example usage: + + >>> asian_option_data = { + ... "strike": 100.0, + ... "maturity": "2023-05-10", + ... "option_type": "call", + ... "average_type": "arithmetic", + ... "average_calculation": "continuous", + ... "average_convention": "price" + ... } + >>> schema = AsianOptionSchema() + >>> result = schema.load(asian_option_data) + >>> asian_option = result.data + """ strike = fields.Float() maturity = fields.Date(format="%Y-%m-%d") option_type = OptionTypeField() @@ -162,5 +236,16 @@ class AsianOptionSchema(Schema): @post_load def make_asian_option(self, data, **kwargs) -> AsianOption: + """ + Creates an AsianOption object after deserialization and validation of JSON data. + + This method is called after the JSON data has been deserialized and validated against the schema. It constructs + an AsianOption object using the deserialized data. + + :param data: The deserialized and validated JSON data. + :type data: dict + :return: The created AsianOption object. + :rtype: AsianOption + """ return AsianOption(**data) # endregion diff --git a/exotx/instruments/autocallable.py b/exotx/instruments/autocallable.py index 07481ad..5338831 100644 --- a/exotx/instruments/autocallable.py +++ b/exotx/instruments/autocallable.py @@ -9,7 +9,21 @@ class Autocallable(Instrument): - """Class for modeling an autocallable and pricing it.""" + """ + A class for modeling an autocallable instrument. + + The Autocallable class provides methods for pricing and analyzing autocallable instruments using various + models and pricing engines from the QuantLib library. + + Attributes: + notional (int): The notional amount of the instrument. + strike (float): The strike price of the instrument. + autocall_barrier_level (float): The autocall barrier level. + annual_coupon_value (float): The annual coupon value. + coupon_barrier_level (float): The coupon barrier level. + protection_barrier_level (float): The protection barrier level. + has_memory (bool): Indicates whether the instrument has memory, affecting the calculation of unpaid coupons. + """ def __init__(self, notional: int, @@ -57,24 +71,57 @@ def _get_underlying_paths(dates: np.ndarray, static_data: StaticData, model: str, seed: int = 1) -> np.ndarray: + """ + Generates the underlying paths for the autocallable instrument using the given market data, static data, + and model. + + :param dates: The array of dates for which the underlying paths are generated. + :type dates: np.ndarray + :param market_data: The market data used for generating the underlying paths. + :type market_data: MarketData + :param static_data: The static data used for generating the underlying paths. + :type static_data: StaticData + :param model: The model used for generating the underlying paths. + :type model: str + :param seed: The seed used for random number generation, defaults to 1. + :type seed: int, optional + :return: The generated underlying paths. + :rtype: np.ndarray + """ # set static data day_counter = static_data.get_ql_day_counter() if model.lower() == 'black-scholes': black_scholes_model = BlackScholesModel(market_data, static_data) process = black_scholes_model.setup() - underlying_paths = black_scholes_model.generate_paths(dates, day_counter, process, seed=seed)[:, 1:] + underlying_paths = black_scholes_model.generate_paths( + dates, day_counter, process, seed=seed)[:, 1:] else: # defaults to Heston model # create and calibrate the heston model based on market data heston_model = HestonModel(market_data, static_data) process, model = heston_model.calibrate(seed=seed) # generate paths for a given set of dates, exclude the current spot rate - underlying_paths = heston_model.generate_paths(dates, day_counter, process, seed=seed)[:, 1:] + underlying_paths = heston_model.generate_paths( + dates, day_counter, process, seed=seed)[:, 1:] return underlying_paths def price(self, market_data: MarketData, static_data: StaticData, model: str, seed: int = 1): + """ + Calculates the price of the autocallable instrument using the given market data, static data, and model. + + :param market_data: The market data used for pricing the instrument. + :type market_data: MarketData + :param static_data: The static data used for pricing the instrument. + :type static_data: StaticData + :param model: The model used for pricing the instrument. + :type model: str + :param seed: The seed used for random number generation, defaults to 1. + :type seed: int, optional + :return: The price of the autocallable instrument. + :rtype: float + """ reference_date: ql.Date = market_data.get_ql_reference_date() ql.Settings.instance().evaluationDate = reference_date @@ -84,7 +131,8 @@ def price(self, market_data: MarketData, static_data: StaticData, model: str, se # coupon schedule start_date = reference_date - first_coupon_date = calendar.advance(start_date, ql.Period(6, ql.Months)) + first_coupon_date = calendar.advance( + start_date, ql.Period(6, ql.Months)) last_coupon_date = calendar.advance(start_date, ql.Period(3, ql.Years)) coupon_dates = np.array(list(ql.Schedule(first_coupon_date, last_coupon_date, ql.Period(ql.Semiannual), calendar, business_day_convention, business_day_convention, @@ -103,18 +151,22 @@ def price(self, market_data: MarketData, static_data: StaticData, model: str, se # create date array for path generator # combine valuation date and all the remaining coupon dates - dates = np.hstack((np.array([reference_date]), coupon_dates[coupon_dates > reference_date])) + dates = np.hstack( + (np.array([reference_date]), coupon_dates[coupon_dates > reference_date])) # get underlying paths - paths = self._get_underlying_paths(dates, market_data, static_data, model, seed) + paths = self._get_underlying_paths( + dates, market_data, static_data, model, seed) # identify the past coupon dates past_coupon_dates = coupon_dates[coupon_dates <= reference_date] # conditionally, merge given past fixings from a given dictionary and generated paths if past_coupon_dates.shape[0] > 0: - past_fixings_array = np.array([past_fixings[past_date] for past_date in past_coupon_dates]) - past_fixings_array = np.tile(past_fixings_array, (paths.shape[0], 1)) + past_fixings_array = np.array( + [past_fixings[past_date] for past_date in past_coupon_dates]) + past_fixings_array = np.tile( + past_fixings_array, (paths.shape[0], 1)) paths = np.hstack((past_fixings_array, paths)) # result accumulator @@ -142,7 +194,7 @@ def price(self, market_data: MarketData, static_data: StaticData, model: str, se # pay 100% redemption, plus coupon, plus conditionally all unpaid coupons if index >= self.coupon_barrier_level: payoff = self.notional * ( - 1 + (self.annual_coupon_value * year_fraction * (1 + unpaid_coupons * has_memory))) + 1 + (self.annual_coupon_value * year_fraction * (1 + unpaid_coupons * has_memory))) # index is greater or equal to protection barrier level and less than coupon barrier level # pay 100% redemption, no coupon if (index >= self.protection_barrier_level) & (index < self.coupon_barrier_level): @@ -158,14 +210,14 @@ def price(self, market_data: MarketData, static_data: StaticData, model: str, se # pay 100% redemption, plus coupon, plus conditionally all unpaid coupons if index >= self.autocall_barrier_level: payoff = self.notional * ( - 1 + (self.annual_coupon_value * year_fraction * (1 + unpaid_coupons * has_memory))) + 1 + (self.annual_coupon_value * year_fraction * (1 + unpaid_coupons * has_memory))) has_auto_called = True # index is greater or equal to coupon barrier level and less than autocall barrier level # autocall will not happen # pay coupon, plus conditionally all unpaid coupons if (index >= self.coupon_barrier_level) & (index < self.autocall_barrier_level): payoff = self.notional * ( - self.annual_coupon_value * year_fraction * (1 + unpaid_coupons * has_memory)) + self.annual_coupon_value * year_fraction * (1 + unpaid_coupons * has_memory)) unpaid_coupons = 0 # index is less than coupon barrier level # autocall will not happen @@ -176,7 +228,8 @@ def price(self, market_data: MarketData, static_data: StaticData, model: str, se # conditionally, calculate PV for period payoff, add PV to local accumulator if date > reference_date: - df = market_data.get_yield_curve(day_counter).discount(date) + df = market_data.get_yield_curve( + day_counter).discount(date) payoff_present_value += payoff * df # add path PV to global accumulator diff --git a/exotx/instruments/average_calculation.py b/exotx/instruments/average_calculation.py index 322f80a..0897961 100644 --- a/exotx/instruments/average_calculation.py +++ b/exotx/instruments/average_calculation.py @@ -10,6 +10,26 @@ class AverageCalculation(Enum): def convert_average_calculation(average_calculation: Union[str, AverageCalculation]) -> AverageCalculation: + """ + Converts an AverageCalculation enum value or a string representing an average calculation to the corresponding AverageCalculation enum value. + + This function takes an input average_calculation, which can be either an AverageCalculation enum value or a string, and returns the + corresponding AverageCalculation enum value. The function raises an error if the input is invalid or not supported. + + :param average_calculation: The input average calculation, either as an AverageCalculation enum value or a string. + :type average_calculation: Union[str, AverageCalculation] + :return: The corresponding AverageCalculation enum value. + :rtype: AverageCalculation + :raises ValueError: If the input average calculation is invalid or not supported. + :raises Exception: If the input type is not a valid AverageCalculation enum value or a string. + + Example usage: + + >>> convert_average_calculation(AverageCalculation.CONTINUOUS) + + >>> convert_average_calculation('discrete') + + """ if isinstance(average_calculation, str): average_calculation = average_calculation.upper() if average_calculation not in AverageCalculation.__members__: @@ -19,7 +39,8 @@ def convert_average_calculation(average_calculation: Union[str, AverageCalculati elif isinstance(average_calculation, AverageCalculation): return average_calculation else: - raise Exception(f"Invalid average calculation type \"{type(average_calculation)}\"") + raise Exception( + f"Invalid average calculation type \"{type(average_calculation)}\"") class AverageCalculationField(fields.Field): diff --git a/exotx/instruments/average_convention.py b/exotx/instruments/average_convention.py index db2a543..a09b59a 100644 --- a/exotx/instruments/average_convention.py +++ b/exotx/instruments/average_convention.py @@ -10,6 +10,26 @@ class AverageConvention(Enum): def convert_average_convention(average_convention: Union[str, AverageConvention]) -> AverageConvention: + """ + Converts an AverageConvention enum value or a string representing an average convention to the corresponding AverageConvention enum value. + + This function takes an input average_convention, which can be either an AverageConvention enum value or a string, and returns the + corresponding AverageConvention enum value. The function raises an error if the input is invalid or not supported. + + :param average_convention: The input average convention, either as an AverageConvention enum value or a string. + :type average_convention: Union[str, AverageConvention] + :return: The corresponding AverageConvention enum value. + :rtype: AverageConvention + :raises ValueError: If the input average convention is invalid or not supported. + :raises Exception: If the input type is not a valid AverageConvention enum value or a string. + + Example usage: + + >>> convert_average_convention(AverageConvention.PRICE) + + >>> convert_average_convention('strike') + + """ if isinstance(average_convention, str): average_convention = average_convention.upper() if average_convention not in AverageConvention.__members__: @@ -19,7 +39,8 @@ def convert_average_convention(average_convention: Union[str, AverageConvention] elif isinstance(average_convention, AverageConvention): return average_convention else: - raise Exception(f"Invalid average convention type \"{type(average_convention)}\"") + raise Exception( + f"Invalid average convention type \"{type(average_convention)}\"") class AverageConventionField(fields.Field): diff --git a/exotx/instruments/average_type.py b/exotx/instruments/average_type.py index c644bb7..3e9ba22 100644 --- a/exotx/instruments/average_type.py +++ b/exotx/instruments/average_type.py @@ -11,6 +11,27 @@ class AverageType(Enum): def convert_average_type_to_ql(average_type: Union[str, AverageType]): + """ + Converts an AverageType enum value or a string representing an average type to the corresponding QuantLib average type. + + This function takes an input average_type, which can be either an AverageType enum value or a string, and returns the + corresponding QuantLib average type (ql.Average().Arithmetic or ql.Average().Geometric). The function raises an error + if the input is invalid or not supported. + + :param average_type: The input average type, either as an AverageType enum value or a string. + :type average_type: Union[str, AverageType] + :return: The corresponding QuantLib average type (ql.Average().Arithmetic or ql.Average().Geometric). + :rtype: ql.Average + :raises ValueError: If the input average type is invalid or not supported. + :raises TypeError: If the input type is not a valid AverageType enum value or a string. + + Example usage: + + >>> convert_average_type_to_ql(AverageType.ARITHMETIC) + + >>> convert_average_type_to_ql('geometric') + + """ if isinstance(average_type, str): average_type = average_type.upper() if average_type not in AverageType.__members__: @@ -25,7 +46,8 @@ def convert_average_type_to_ql(average_type: Union[str, AverageType]): else: raise ValueError(f"Invalid average type \"{average_type}\"") else: - raise TypeError(f"Invalid input type {type(average_type)} for average type") + raise TypeError( + f"Invalid input type {type(average_type)} for average type") class AverageTypeField(fields.Field): diff --git a/exotx/instruments/barrier_option.py b/exotx/instruments/barrier_option.py index c04e8eb..e0d30fe 100644 --- a/exotx/instruments/barrier_option.py +++ b/exotx/instruments/barrier_option.py @@ -35,7 +35,23 @@ class BarrierOptionEngine(Enum): class BarrierOption(Instrument): - """Class for modeling a barrier option.""" + """ + BarrierOption is a class representing a barrier option financial instrument. + + The BarrierOption class provides methods for pricing and analyzing barrier options using various models + and pricing engines from the QuantLib library. + + Attributes: + barrier_type (BarrierType): The barrier type (Up-and-In, Up-and-Out, Down-and-In, or Down-and-Out). + barrier (float): The barrier level. + strike (float): The strike price of the option. + maturity (ql.Date): The maturity date of the option. + exercise (ExerciseType): The exercise style (European or American). + option_type (OptionType): The option type (Call or Put). + rebate (float): The rebate amount. + reference_date (ql.Date): The reference date used for pricing the option. + model (str): The pricing model used for the option. + """ def __init__(self, barrier_type: str, @@ -56,6 +72,18 @@ def __init__(self, self.model = None def price(self, market_data: MarketData, static_data: StaticData, model: str): + """ + Calculates the price of the barrier option using the given market data, static data, and model. + + :param market_data: The market data used for pricing the option. + :type market_data: MarketData + :param static_data: The static data used for pricing the option. + :type static_data: StaticData + :param model: The pricing model used for the option. + :type model: str + :return: The net present value (NPV) of the option. + :rtype: float + """ self.reference_date: ql.Date = market_data.get_ql_reference_date() ql.Settings.instance().evaluationDate = self.reference_date @@ -63,10 +91,12 @@ def price(self, market_data: MarketData, static_data: StaticData, model: str): ql_barrier_type = self._get_ql_barrier_type() ql_payoff = self._get_ql_payoff() ql_exercise = self._get_ql_exercise() - ql_option = ql.BarrierOption(ql_barrier_type, self.barrier, self.rebate, ql_payoff, ql_exercise) + ql_option = ql.BarrierOption( + ql_barrier_type, self.barrier, self.rebate, ql_payoff, ql_exercise) # set pricing engine - ql_pricing_engine = self._get_ql_pricing_engine(market_data, static_data, model) + ql_pricing_engine = self._get_ql_pricing_engine( + market_data, static_data, model) ql_option.setPricingEngine(ql_pricing_engine) return ql_option.NPV() diff --git a/exotx/instruments/basket_option.py b/exotx/instruments/basket_option.py index 099b51e..c158c57 100644 --- a/exotx/instruments/basket_option.py +++ b/exotx/instruments/basket_option.py @@ -13,6 +13,28 @@ class BasketOption(Instrument): + """ + BasketOption is a class representing a basket option financial instrument. + + A basket option is a financial derivative that derives its value from the weighted sum of several underlying assets. + The class inherits from the Instrument abstract base class and implements the price method to calculate the price of + the basket option. + + The class also provides methods for converting basket type to the corresponding QuantLib payoff and creating a + QuantLib pricing engine for the basket option. + + Attributes: + - strike (float): The option's strike price. + - maturity (ql.Date): The option's maturity date in QuantLib format. + - option_type (ql.Option.Type): The option type (call or put) in QuantLib format. + - basket_type (BasketType): The basket type (minbasket, maxbasket, spreadbasket, or averagebasket). + + Example usage: + + >>> basket_option = BasketOption(strike=100.0, maturity='2023-05-10', option_type='call', basket_type='minbasket') + >>> price = basket_option.price(market_data, static_data, pricing_config) + """ + def __init__(self, strike: float, maturity: Union[str, datetime], @@ -25,6 +47,27 @@ def __init__(self, self.basket_type = convert_basket_type(basket_type) def price(self, market_data, static_data, pricing_config: PricingConfiguration, seed: int = 1) -> dict: + """ + Calculates the price and optionally the greeks (delta, gamma, and theta) for the basket option using the + provided market data, static data, and pricing configuration. + + :param market_data: Market data object containing relevant market information such as yield curve, dividend curve, + underlying spots, and volatilities. + :type market_data: MarketData + :param static_data: Static data object containing relevant static information such as calendar and day counter. + :type static_data: StaticData + :param pricing_config: Pricing configuration object containing pricing settings and whether to compute greeks. + :type pricing_config: PricingConfiguration + :param seed: Seed for the random number generator used in Monte Carlo pricing engine, defaults to 1. + :type seed: int, optional + :return: A dictionary containing the price and optionally the greeks (delta, gamma, and theta) of the option. + :rtype: dict + + Example usage: + + >>> basket_option.price(market_data, static_data, pricing_config) + {'price': 10.1234, 'delta': 0.5678, 'gamma': 0.0123, 'theta': -0.0987} + """ # set the reference date reference_date: ql.Date = market_data.get_ql_reference_date() ql.Settings.instance().evaluationDate = reference_date @@ -37,7 +80,8 @@ def price(self, market_data, static_data, pricing_config: PricingConfiguration, ql_option = ql.BasketOption(ql_basket_payoff, ql_exercise) # set the pricing engine - ql_engine = self._get_ql_pricing_engine(market_data, static_data, pricing_config, seed) + ql_engine = self._get_ql_pricing_engine( + market_data, static_data, pricing_config, seed) ql_option.setPricingEngine(ql_engine) # price @@ -108,12 +152,15 @@ def _get_ql_pricing_engine(market_data, static_data, pricing_config: PricingConf day_counter = static_data.get_ql_day_counter() processes = [ql.BlackScholesMertonProcess(ql.QuoteHandle(ql.SimpleQuote(x)), - market_data.get_dividend_curve(day_counter), - market_data.get_yield_curve(day_counter), + market_data.get_dividend_curve( + day_counter), + market_data.get_yield_curve( + day_counter), ql.BlackVolTermStructureHandle( ql.BlackConstantVol(reference_date, calendar, y, day_counter))) for x, y in zip(market_data.underlying_spots, market_data.underlying_black_scholes_volatilities)] - multi_processes = ql.StochasticProcessArray(processes, market_data.get_correlation_matrix()) + multi_processes = ql.StochasticProcessArray( + processes, market_data.get_correlation_matrix()) # TODO: Consider different pricing engines based on self.basket_type and/or pricing_config return ql.MCEuropeanBasketEngine(multi_processes, RandomNumberGenerator.PSEUDORANDOM.value, timeStepsPerYear=1, @@ -122,6 +169,30 @@ def _get_ql_pricing_engine(market_data, static_data, pricing_config: PricingConf # region Schema class BasketOptionSchema(Schema): + """ + BasketOptionSchema is a Marshmallow schema class for deserializing and validating JSON data into a BasketOption object. + + This schema defines the required fields for a BasketOption object and validates their types and values. It also provides + a post_load method to create a BasketOption object after deserialization and validation. + + Fields: + - strike (float): The option's strike price. + - maturity (date): The option's maturity date in the format "YYYY-MM-DD". + - option_type (OptionType): The option type, either "call" or "put". + - basket_type (BasketType): The basket type, one of "minbasket", "maxbasket", "spreadbasket", or "averagebasket". + + Example usage: + + >>> basket_option_data = { + ... "strike": 100.0, + ... "maturity": "2023-05-10", + ... "option_type": "call", + ... "basket_type": "minbasket" + ... } + >>> schema = BasketOptionSchema() + >>> result = schema.load(basket_option_data) + >>> basket_option = result.data + """ strike = fields.Float() maturity = fields.Date(format="%Y-%m-%d") option_type = OptionTypeField() @@ -129,5 +200,17 @@ class BasketOptionSchema(Schema): @post_load def make_basket_option(self, data, **kwargs) -> BasketOption: + """ + Constructs a BasketOption instance from the deserialized data. + + This method is called by the Marshmallow schema after the input data has been deserialized. + It uses the deserialized data to create a new BasketOption instance and returns it. + + :param data: A dictionary containing the deserialized data for the basket option. + :type data: dict + :param kwargs: Additional keyword arguments. + :return: A BasketOption instance created from the deserialized data. + :rtype: BasketOption + """ return BasketOption(**data) # endregion diff --git a/exotx/instruments/basket_type.py b/exotx/instruments/basket_type.py index 6a82c54..070df86 100644 --- a/exotx/instruments/basket_type.py +++ b/exotx/instruments/basket_type.py @@ -12,6 +12,23 @@ class BasketType(Enum): def convert_basket_type(basket_type: Union[str, BasketType]) -> BasketType: + """ + Converts the input basket_type to a BasketType enumeration value. + + :param basket_type: A string or BasketType instance representing the basket type. + :type basket_type: Union[str, BasketType] + :return: The corresponding BasketType enumeration value. + :rtype: BasketType + :raises ValueError: If the input string is not a valid BasketType member. + :raises Exception: If the input is neither a string nor a BasketType instance. + + Example usage: + + >>> convert_basket_type("minbasket") + + >>> convert_basket_type(BasketType.MINBASKET) + + """ if isinstance(basket_type, str): basket_type = basket_type.upper() if basket_type not in BasketType.__members__: diff --git a/exotx/instruments/instrument.py b/exotx/instruments/instrument.py index f33e87b..aa1f11c 100644 --- a/exotx/instruments/instrument.py +++ b/exotx/instruments/instrument.py @@ -5,8 +5,34 @@ class Instrument(ABC): @abstractmethod def price(self, *args, **kwargs) -> Union[float, dict]: + """ + Abstract method to calculate the price of a financial instrument. + + This method should be implemented by subclasses to provide the pricing functionality + specific to the financial instrument they represent. + + :param args: Positional arguments to be passed to the price calculation. + :param kwargs: Keyword arguments to be passed to the price calculation. + :return: The price of the financial instrument, either as a single float or a dictionary + containing additional information such as greeks. + :rtype: Union[float, dict] + """ pass def price(instrument: Instrument, *args, **kwargs) -> Union[float, dict]: + """ + Calculates the price of the given financial instrument. + + This function serves as a wrapper around the price method of the Instrument class. + It calls the price method on the given instrument instance and returns the result. + + :param instrument: An instance of a subclass of the Instrument class. + :type instrument: Instrument + :param args: Positional arguments to be passed to the price method of the instrument. + :param kwargs: Keyword arguments to be passed to the price method of the instrument. + :return: The price of the financial instrument, either as a single float or a dictionary + containing additional information such as greeks. + :rtype: Union[float, dict] + """ return instrument.price(*args, **kwargs) diff --git a/exotx/instruments/option_type.py b/exotx/instruments/option_type.py index 83a485a..74c33a9 100644 --- a/exotx/instruments/option_type.py +++ b/exotx/instruments/option_type.py @@ -15,6 +15,27 @@ def values(): def convert_option_type_to_ql(option_type: Union[str, OptionType]) -> ql.Option: + """ + Converts an OptionType enum value or a string representing an option type to the corresponding QuantLib option type. + + This function takes an input option_type, which can be either an OptionType enum value or a string, and returns the + corresponding QuantLib option type (ql.Option.Call or ql.Option.Put). The function raises an error if the input is + invalid or not supported. + + :param option_type: The input option type, either as an OptionType enum value or a string. + :type option_type: Union[str, OptionType] + :return: The corresponding QuantLib option type (ql.Option.Call or ql.Option.Put). + :rtype: ql.Option + :raises ValueError: If the input option type is invalid or not supported. + :raises TypeError: If the input type is not a valid OptionType enum value or a string. + + Example usage: + + >>> convert_option_type_to_ql(OptionType.CALL) + + >>> convert_option_type_to_ql('put') + + """ if isinstance(option_type, str): option_type = option_type.upper() if option_type not in OptionType.__members__: @@ -29,7 +50,8 @@ def convert_option_type_to_ql(option_type: Union[str, OptionType]) -> ql.Option: else: raise ValueError(f"Invalid option type \"{option_type}\"") else: - raise TypeError(f"Invalid input type {type(option_type)} for option type") + raise TypeError( + f"Invalid input type {type(option_type)} for option type") class OptionTypeField(fields.Field): diff --git a/exotx/instruments/vanilla_option.py b/exotx/instruments/vanilla_option.py index 32e3ea1..9df3fdb 100644 --- a/exotx/instruments/vanilla_option.py +++ b/exotx/instruments/vanilla_option.py @@ -14,6 +14,25 @@ class VanillaOption(Instrument): + """ + VanillaOption is a class representing a vanilla option financial instrument. + + A vanilla option is a financial derivative that gives the holder the right, but not the obligation, to buy or sell + an underlying asset at a specific price on or before a specific date. The class inherits from the Instrument + abstract base class and implements the price method to calculate the price of the vanilla option. + + The class supports multiple pricing models and numerical methods, as specified in the pricing configuration. + + Attributes: + - strike (float): The option's strike price. + - maturity (ql.Date): The option's maturity date in QuantLib format. + - option_type (ql.Option.Type): The option type (call or put) in QuantLib format. + + Example usage: + + >>> vanilla_option = VanillaOption(strike=100.0, maturity='2023-05-10', option_type='call') + >>> price = vanilla_option.price(market_data, static_data, pricing_config) + """ available_engines = { PricingModel.BLACK_SCHOLES: ql.AnalyticEuropeanEngine, PricingModel.HESTON: ql.AnalyticHestonEngine @@ -26,6 +45,27 @@ def __init__(self, strike: float, maturity: Union[str, datetime], option_type: U self.option_type = convert_option_type_to_ql(option_type) def price(self, market_data, static_data, pricing_config: PricingConfiguration, seed: int = 1) -> dict: + """ + Calculates the price and optionally the greeks (delta, gamma, and theta) for the vanilla option using the + provided market data, static data, and pricing configuration. + + :param market_data: Market data object containing relevant market information such as yield curve, dividend curve, + underlying spots, and volatilities. + :type market_data: MarketData + :param static_data: Static data object containing relevant static information such as calendar and day counter. + :type static_data: StaticData + :param pricing_config: Pricing configuration object containing pricing settings and whether to compute greeks. + :type pricing_config: PricingConfiguration + :param seed: Seed for the random number generator used in Monte Carlo pricing engine, defaults to 1. + :type seed: int, optional + :return: A dictionary containing the price and optionally the greeks (delta, gamma, and theta) of the option. + :rtype: dict + + Example usage: + + >>> vanilla_option.price(market_data, static_data, pricing_config) + {'price': 10.1234, 'delta': 0.5678, 'gamma': 0.0123, 'theta': -0.0987} + """ reference_date: ql.Date = market_data.get_ql_reference_date() ql.Settings.instance().evaluationDate = reference_date @@ -35,7 +75,8 @@ def price(self, market_data, static_data, pricing_config: PricingConfiguration, ql_option = ql.VanillaOption(ql_payoff, ql_exercise) # set the pricing engine - ql_engine = self._get_ql_pricing_engine(market_data, static_data, pricing_config, seed) + ql_engine = self._get_ql_pricing_engine( + market_data, static_data, pricing_config, seed) ql_option.setPricingEngine(ql_engine) # price @@ -50,6 +91,27 @@ def price(self, market_data, static_data, pricing_config: PricingConfiguration, @staticmethod def _get_ql_pricing_engine(market_data, static_data, pricing_config: PricingConfiguration, seed: int): + """ + Constructs the appropriate QuantLib pricing engine based on the provided pricing configuration. + + This function creates a Black-Scholes or Heston pricing engine depending on the specified pricing model + and numerical method in the pricing configuration. + + :param market_data: Market data object containing relevant market information such as yield curve, dividend curve, + underlying spots, and volatilities. + :type market_data: MarketData + :param static_data: Static data object containing relevant static information such as calendar and day counter. + :type static_data: StaticData + :param pricing_config: Pricing configuration object containing pricing settings and whether to compute greeks. + :type pricing_config: PricingConfiguration + :param seed: Seed for the random number generator used in Monte Carlo pricing engine, defaults to 1. + :type seed: int, optional + :return: A QuantLib pricing engine for the specified pricing model and numerical method. + :rtype: ql.PricingEngine + + Raises: + ValueError: If an invalid combination of pricing model and numerical method is provided. + """ if pricing_config.model == PricingModel.BLACK_SCHOLES and \ pricing_config.numerical_method == NumericalMethod.ANALYTIC: bs_model = BlackScholesModel(market_data, static_data) @@ -68,11 +130,25 @@ def _get_ql_pricing_engine(market_data, static_data, pricing_config: PricingConf # region serialization/deserialization def to_json(self): + """ + Serializes the VanillaOption instance to a JSON representation using the VanillaOptionSchema. + + :return: A JSON representation of the VanillaOption instance. + :rtype: dict + """ schema = VanillaOptionSchema() return schema.dump(self) @classmethod def from_json(cls, json_data): + """ + Deserializes a JSON representation of a VanillaOption instance using the VanillaOptionSchema. + + :param json_data: A JSON representation of a VanillaOption instance. + :type json_data: dict + :return: A VanillaOption instance created from the JSON data. + :rtype: VanillaOption + """ schema = VanillaOptionSchema() data = schema.load(json_data) return cls(**data) @@ -83,12 +159,35 @@ def from_json(cls, json_data): class VanillaOptionSchema(Schema): + """ + A Marshmallow schema for serializing and deserializing VanillaOption instances. + + The VanillaOptionSchema is used to define the structure of the JSON data for the VanillaOption class. It contains + the fields required for a VanillaOption instance, such as strike, maturity, and option type. + + Attributes: + strike (fields.Float): A Marshmallow field for the option's strike price. + maturity (fields.Date): A Marshmallow field for the option's maturity date. + option_type (OptionTypeField): A Marshmallow field for the option's type (call or put). + """ strike = fields.Float() maturity = fields.Date(format="%Y-%m-%d") option_type = OptionTypeField(allow_none=False) @post_load def make_vanilla_option(self, data, **kwargs) -> VanillaOption: + """ + Constructs a VanillaOption instance from the deserialized data. + + This method is called by the Marshmallow schema after the input data has been deserialized. + It uses the deserialized data to create a new VanillaOption instance and returns it. + + :param data: A dictionary containing the deserialized data for the vanilla option. + :type data: dict + :param kwargs: Additional keyword arguments. + :return: A VanillaOption instance created from the deserialized data. + :rtype: VanillaOption + """ return VanillaOption(**data) # endregion