Search code examples
pythonoopabc

Static method-only class and subclasses in Python - is there a better design pattern?


I am pricing financial instruments, and each financial instrument object requires a day counter as a property. There are 4 kinds of day counters which have different implementations for each of their two methods, year_fraction and day_count. This day counter property on financial instruments is used in other classes when pricing, to know how to discount curves appropriately, etc. However, all of the day count methods are static, and doing nothing more than applying some formula.

So despite everything I've read online telling me to not use static methods and just have module-level functions instead, I couldn't see a way to nicely pass around the correct DayCounter without implementing something like this

class DayCounter:
    __metaclass__ = abc.ABCMeta

    @abc.abstractstaticmethod
    def year_fraction(start_date, end_date):
        raise NotImplementedError("DayCounter subclass must define a year_fraction method to be valid.")

    @abc.abstractstaticmethod
    def day_count(start_date, end_date):
        raise NotImplementedError("DayCounter subclass must define a day_count method to be valid.")


class Actual360(DayCounter):

    @staticmethod
    def day_count(start_date, end_date):
        # some unique formula

    @staticmethod
    def year_fraction(start_date, end_date):
        # some unique formula


class Actual365(DayCounter):

    @staticmethod
    def day_count(start_date, end_date):
        # some unique formula

    @staticmethod
    def year_fraction(start_date, end_date):
        # some unique formula


class Thirty360(DayCounter):

    @staticmethod
    def day_count(start_date, end_date):
        # some unique formula

    @staticmethod
    def year_fraction(start_date, end_date):
        # some unique formula

class ActualActual(DayCounter):

    @staticmethod
    def day_count(start_date, end_date):
        # some unique formula

    @staticmethod
    def year_fraction(start_date, end_date):
        # some unique formula

So that in a pricing engine for some particular instrument that is passed an instrument as a parameter, I can use the instrument's day counter property as needed.

Am I missing something more idiomatic / stylistically acceptable in Python or does this seem like appropriate use for static method-only classes?


Example:

I have a class FxPricingEngine, which has an __init__ method passed an FxInstrument and subsequent underlying_instrument property. Then in order to use the Value method of my pricing engine I need to discount a curve with a particular day counter. I have a YieldCurve class with a discount method to which I pass self.underlying_instrument.day_counter.year_fraction such that I can apply the correct formula. Really all that the classes are serving to do is provide some logical organization for the unique implementations.


Solution

  • As it is, object orientation does not make any sense in your scenario: there is no data associated with an instance of your types, so any two objects of some type (e.g. Thirty360) are going to be equal (i.e. you only have singletons).

    It looks like you want to be able to parametrise client code on behaviour - the data which your methods operate on is not given in a constructor but rather via the arguments of the methods. In that case, plain free functions may be a much more straight forward solution.

    For instance, given some imaginary client code which operates on your counters like:

    def f(counter):
      counter.day_count(a, b)
      # ...
      counter.year_fraction(x, y)
    

    ...you could just as well imagine passing two functions as arguments right away instead of an object, e.g. have

    def f(day_count, year_fraction):
        day_count(a, b)
        # ...
        year_fraction(x, y)
    

    On the caller side, you would pass plain functions, e.g.

    f(thirty360_day_count, thirty360_year_fraction)
    

    If you like, you could also have different names for the functions, or you could define them in separate modules to get the namespacing. You could also easily pass special functions like way (for instance if you only need the day_count to be correct but the year_fraction could be a noop).