JsonHandler utility class for JSON serialization/deserialization is shown below. In a nutshell, this class has only two methods: FileToObject, which will re-hydrate JSON file content to a custom object (deserialization) and ObjectToFile, which will hydrate custom object into JSON file content (serialization). However, in this program we only need to deserialize (re-hydrate) transactions.
# class for handling transformations between custom object and JSON file class JsonHandler: # transform json file to custom object def FileToObject(file): # nested function: transform dictionary to custom object def DictionaryToObject(dic): if("__class__" in dic): class_name = dic.pop("__class__") module_name = dic.pop("__module__") module = __import__(module_name) class_ = getattr(module, class_name) obj = class_(**dic) else: obj = dic return obj return DictionaryToObject(json.load(open(file, 'r'))) # transform custom object to json file def ObjectToFile(obj, file): # nested function: check whether an object can be json serialized def IsSerializable(obj): check = True try: # throws, if an object is not serializable json.dumps(obj) except: check = False return check # nested function: transform custom object to dictionary def ObjectToDictionary(obj): dic = { "__class__": obj.__class__.__name__, "__module__": obj.__module__ } dic.update(obj.__dict__) # remove all non-serializable items from dictionary before serialization keysToBeRemoved = [] for k, v in dic.items(): if(IsSerializable(v) == False): keysToBeRemoved.append(k) [dic.pop(k, None) for k in keysToBeRemoved] return dic json.dump(ObjectToDictionary(obj), open(file, 'w'))
Next, let us take a look at VanillaSwap class, which is actually wrapping QuantLib VanillaSwap class. Data members are initialized at constructor. It should be noted, that we will construct the actual QuantLib instrument instance in setPricingEngine method, after receiving correct pricing engine (DiscountingSwapEngine) and constructed index object (including required set of reference index fixings) for floating leg. Class method NPV will just delegate our valuation request for wrapped VanillaSwap instance.
# wrapper class for QuantLib vanilla interest rate swap class VanillaSwap(object): def __init__(self, ID, swapType, nominal, startDate, maturityDate, fixedLegFrequency, fixedLegCalendar, fixedLegConvention, fixedLegDateGenerationRule, fixedLegRate, fixedLegDayCount, fixedLegEndOfMonth, floatingLegFrequency, floatingLegCalendar, floatingLegConvention, floatingLegDateGenerationRule, floatingLegSpread, floatingLegDayCount, floatingLegEndOfMonth): self.ID = ID self.swapType = swapType self.nominal = nominal self.startDate = startDate self.maturityDate = maturityDate self.fixedLegFrequency = fixedLegFrequency self.fixedLegCalendar = fixedLegCalendar self.fixedLegConvention = fixedLegConvention self.fixedLegDateGenerationRule = fixedLegDateGenerationRule self.fixedLegRate = fixedLegRate self.fixedLegDayCount = fixedLegDayCount self.fixedLegEndOfMonth = fixedLegEndOfMonth self.floatingLegFrequency = floatingLegFrequency self.floatingLegCalendar = floatingLegCalendar self.floatingLegConvention = floatingLegConvention self.floatingLegDateGenerationRule = floatingLegDateGenerationRule self.floatingLegSpread = floatingLegSpread self.floatingLegDayCount = floatingLegDayCount self.floatingLegEndOfMonth = floatingLegEndOfMonth def setPricingEngine(self, engine, floatingLegIborIndex): # create fixed leg schedule fixedLegSchedule = ql.Schedule( Convert.to_date(self.startDate), Convert.to_date(self.maturityDate), ql.Period(Convert.to_frequency(self.fixedLegFrequency)), Convert.to_calendar(self.fixedLegCalendar), Convert.to_businessDayConvention(self.fixedLegConvention), Convert.to_businessDayConvention(self.fixedLegConvention), Convert.to_dateGenerationRule(self.fixedLegDateGenerationRule), self.fixedLegEndOfMonth) # create floating leg schedule floatingLegSchedule = ql.Schedule( Convert.to_date(self.startDate), Convert.to_date(self.maturityDate), ql.Period(Convert.to_frequency(self.floatingLegFrequency)), Convert.to_calendar(self.floatingLegCalendar), Convert.to_businessDayConvention(self.floatingLegConvention), Convert.to_businessDayConvention(self.floatingLegConvention), Convert.to_dateGenerationRule(self.floatingLegDateGenerationRule), self.floatingLegEndOfMonth) # create vanilla interest rate swap instance self.instrument = ql.VanillaSwap( Convert.to_swapType(self.swapType), self.nominal, fixedLegSchedule, self.fixedLegRate, Convert.to_dayCounter(self.fixedLegDayCount), floatingLegSchedule, floatingLegIborIndex, # use given index argument self.floatingLegSpread, Convert.to_dayCounter(self.floatingLegDayCount)) # pair instrument with pricing engine self.instrument.setPricingEngine(engine) def NPV(self): return self.instrument.NPV()
Let us then take a look at the actual JSON feed. One may notice quickly, that there is no QuantLib data types in this source file. The idea is, that in the first stage, VanillaSwap class instance will be created and all its data members will be "in-built Python data types", shown in this source file (string, float, boolean, etc.).
{ "__class__": "VanillaSwap", "__module__": "__main__", "ID": "002", "swapType": "PAYER", "nominal": 48000000, "startDate": "2018-03-14", "maturityDate": "2028-03-14", "fixedLegFrequency": "ANNUAL", "fixedLegCalendar": "TARGET", "fixedLegConvention": "MODIFIEDFOLLOWING", "fixedLegDateGenerationRule": "BACKWARD", "fixedLegRate": 0.019, "fixedLegDayCount": "ACTUAL360", "fixedLegEndOfMonth": false, "floatingLegFrequency": "QUARTERLY", "floatingLegCalendar": "TARGET", "floatingLegConvention": "MODIFIEDFOLLOWING", "floatingLegDateGenerationRule": "BACKWARD", "floatingLegSpread": 0.0007, "floatingLegDayCount": "ACTUAL360", "floatingLegEndOfMonth": false }
Now, as we execute setPricingEngine method (which then creates leg schedules and the actual QuantLib VanillaSwap instance), one may also see that we are using Convert utility class for transforming specific in-built data types into specific QuantLib types (Date, Calendar, DayCounter, etc.). This utility class is shown here below. Needless to say, it is pretty far from being complete as such, but good enough for making a point.
# utility class for different QuantLib type conversions class Convert(): # convert date string ('yyyy-mm-dd') to QuantLib Date object def to_date(s): monthDictionary = { '01': ql.January, '02': ql.February, '03': ql.March, '04': ql.April, '05': ql.May, '06': ql.June, '07': ql.July, '08': ql.August, '09': ql.September, '10': ql.October, '11': ql.November, '12': ql.December } arr = re.findall(r"[\w']+", s) return ql.Date(int(arr[2]), monthDictionary[arr[1]], int(arr[0])) # convert string to QuantLib businessdayconvention enumerator def to_businessDayConvention(s): if (s.upper() == 'FOLLOWING'): return ql.Following if (s.upper() == 'MODIFIEDFOLLOWING'): return ql.ModifiedFollowing if (s.upper() == 'PRECEDING'): return ql.Preceding if (s.upper() == 'MODIFIEDPRECEDING'): return ql.ModifiedPreceding if (s.upper() == 'UNADJUSTED'): return ql.Unadjusted # convert string to QuantLib calendar object def to_calendar(s): if (s.upper() == 'TARGET'): return ql.TARGET() if (s.upper() == 'UNITEDSTATES'): return ql.UnitedStates() if (s.upper() == 'UNITEDKINGDOM'): return ql.UnitedKingdom() # TODO: add new calendar here # convert string to QuantLib swap type enumerator def to_swapType(s): if (s.upper() == 'PAYER'): return ql.VanillaSwap.Payer if (s.upper() == 'RECEIVER'): return ql.VanillaSwap.Receiver # convert string to QuantLib frequency enumerator def to_frequency(s): if (s.upper() == 'DAILY'): return ql.Daily if (s.upper() == 'WEEKLY'): return ql.Weekly if (s.upper() == 'MONTHLY'): return ql.Monthly if (s.upper() == 'QUARTERLY'): return ql.Quarterly if (s.upper() == 'SEMIANNUAL'): return ql.Semiannual if (s.upper() == 'ANNUAL'): return ql.Annual # convert string to QuantLib date generation rule enumerator def to_dateGenerationRule(s): if (s.upper() == 'BACKWARD'): return ql.DateGeneration.Backward if (s.upper() == 'FORWARD'): return ql.DateGeneration.Forward # TODO: add new date generation rule here # convert string to QuantLib day counter object def to_dayCounter(s): if (s.upper() == 'ACTUAL360'): return ql.Actual360() if (s.upper() == 'ACTUAL365FIXED'): return ql.Actual365Fixed() if (s.upper() == 'ACTUALACTUAL'): return ql.ActualActual() if (s.upper() == 'ACTUAL365NOLEAP'): return ql.Actual365NoLeap() if (s.upper() == 'BUSINESS252'): return ql.Business252() if (s.upper() == 'ONEDAYCOUNTER'): return ql.OneDayCounter() if (s.upper() == 'SIMPLEDAYCOUNTER'): return ql.SimpleDayCounter() if (s.upper() == 'THIRTY360'): return ql.Thirty360()
At this point, we have presented JSON handler class for performing required deserializations, VanillaSwap class for wrapping corresponding QuantLib class instance and Conversion utility class for performing required QuantLib type conversions. Finally, it is time to take a look how we can easily deserialize a batch of QuantLib VanillaSwap transaction JSON presentations from specific directory and request valuations for all instances.
# read all JSON files from repository, create vanilla swap instances repository = sys.argv[1] files = os.listdir(repository) swaps = [JsonHandler.FileToObject(repository + file) for file in files] # create valuation curve, index fixings and pricing engine curveHandle = ql.YieldTermStructureHandle(ql.FlatForward(ql.Date(5, ql.July, 2019), 0.02, ql.Actual360())) engine = ql.DiscountingSwapEngine(curveHandle) index = ql.USDLibor(ql.Period(ql.Quarterly), curveHandle) index.addFixings([ql.Date(12, ql.June, 2019)], [0.02]) # set pricing engine (and floating index) and request pv for all swaps for swap in swaps: swap.setPricingEngine(engine, index) print(swap.ID, swap.NPV())
So, at this point the question might be "why bother"? In my opinion, by using deserialization scheme (such as the one presented in this post), we
- have been avoiding significant amount of hard-coded stuff in our program.
- can set as many VanillaSwap JSON presentations into a directory as we want and then easily process these transactions without changing anything in our executing program.
- can relatively easily add implementations for new types of QuantLib instruments (add corresponding wrapper class, define constructor for capturing all required arguments, in setPricingEngine method assemble the actual QuantLib instrument instance and use Convert utility class for transforming in-built data types into QuantLib data types.
Program execution in terminal is shown below.
As always, thanks a lot for reading my blog.
-Mike
No comments:
Post a Comment