logo le blog invivoo blanc

Pont – Design Pattern

25 avril 2024 | Python | 0 comments

1. Contexte

De nombreuses applications financières nécessitent une analyse des performances des portefeuilles, suivie de diverses représentations en fonction des besoins : graphiques en ligne, diagrammes en barres, diagrammes circulaires, etc. Ces évaluations de performance sont souvent classées par catégories d’instruments financiers tels que les actions, les obligations, les options, etc… Traditionnellement, une sélection parmi toutes les combinaisons de catégories et de représentations, basée sur une interface commune, est utilisée pour générer un rapport selon les exigences du client. Cependant, l’extension des catégories dans ces deux dimensions indépendantes ne peut pas toujours être prévue exhaustivement dès le début du développement. L’introduction d’un nouvel instrument financier impliquerait la révision de toutes les démonstrations existantes. De même, l’ajout d’une nouvelle représentation graphique devrait être pris en compte pour tous les produits à évaluer. En somme, toute introduction de nouveaux algorithmes dans une dimension donnée pourrait potentiellement entraîner une augmentation exponentielle de la taille des hiérarchies, un problème classique causé par l’héritage simple. Cependant, ce problème ne concerne pas les interfaces obsolètes et diffère de celui résolu par le patron de conception adaptateur [1].

Notre approche logicielle vise à trouver une solution générique pour étendre les deux dimensions de manière indépendante, en utilisant la composition plutôt que l’héritage. Nous introduisons donc un nouveau patron de conception plus souple : le Pont (ou “bridge” en anglais), qui fait partie des conceptions structurelles [2] et vise à établir des relations simples entre les entités. Pour ce faire, une des dimensions est insérée dans une hiérarchie de classes distincte, permettant à la classe d’origine de référencer un objet de cette nouvelle hiérarchie, au lieu de regrouper tous les états et comportements au sein d’une même classe. Cela permet de diviser une grande classe ou un ensemble de classes complexes en deux hiérarchies distinctes : l’abstraction et l’implémentation, qui peuvent évoluer indépendamment l’une de l’autre.

Dans cet article, nous mettons en œuvre un pont pour résoudre le problème de génération de rapports avec des représentations variables pour différentes classes de produits financiers. Nous commençons par calculer les performances des instruments financiers, puis nous utilisons des outils de mise en forme graphique pour illustrer les résultats. Le pont établit le lien entre ces deux étapes indépendantes avant de produire les rapports de performance à évaluer. De plus, nous implémentons cette conception en Python 3 en respectant les principes SOLID [3].

2. Conception du pont

Le diagramme UML illustre le développement du Pont en Python 3. Ce patron de conception décompose l’interface et l’implémentation du composant en hiérarchies de classes orthogonales. Outre la classe “bridge” (pont), la structure comprend trois composants principaux : une interface au-dessus du pont, plusieurs classes concrètes en dessous du pont, et une abstraction d’implémentation comprise dans le pont, qui appelle les instances d’implémentation selon les besoins.

Basée sur la composition, la classe Bridge hérite de l’abstraction Interface et contient une référence vers la classe d’implémentation abstraite. Cette référence est initialisée avec une instance d’une classe d’implémentation concrète. Toute interaction ultérieure de l’Interface avec la classe d’implémentation est limitée à l’abstraction maintenue dans la classe commune Abstraction d’implémentation. Ainsi, le client interagit avec une classe dérivant de l’Interface, qui délègue toutes les requêtes à une classe concrète d’implémentation via le pont. Le pont agit comme un lien entre les deux abstractions qui évoluent indépendamment sans dépendance de schéma.

En d’autres termes, l’objet de la classe pont est le point de contact connu et utilisé par le client, tandis que l’objet délégué d’implémentation, ou “corps”, est encapsulé en toute sécurité pour garantir qu’il puisse continuer à évoluer isolément, être entièrement remplacé ou partagé au moment de l’exécution.

Contrairement au patron de conception de la fabrique abstraite [4], qui propose la création d’objets complexes en séparant leurs caractéristiques intrinsèques via plusieurs interfaces avant de les assembler, notre Pont se charge de déléguer l’implémentation d’une abstraction dont l’évolution est indépendante de l’interface accessible par le client. Notre attention se porte uniquement sur l’appel effectué dans le Pont vers la méthode en délégation, qui ne fait partie d’aucune interface accessible par le client.

Dans notre contexte, le client cherche à appeler l’interface du compte rendu pour générer des rapports sur les valeurs ajoutées des actions, des obligations et des options répertoriées, qui sont représentées par des lignes de segments, des barres et des camemberts. Cependant, le calcul des valeurs ajoutées dépend uniquement de la classe de l’instrument financier et de sa référence à comparer, tandis que la mise en forme graphique nécessite simplement la prise en compte des valeurs calculées pour différentes démonstrations. Le Pont, dérivant de l’interface du compte rendu, déclare de nouveaux contrats pour les opérations qui gèreront le calcul lors de l’appel de la génération du rapport par le client, tout en déléguant l’illustration des résultats à son objet graphique. L’objet graphique du Pont, héritant d’une interface commune appelée à dessiner, définit les représentations variables en fonction de la demande, tandis que les classes raffinées de produits financiers dérivant du Pont mettent en œuvre concrètement les opérations enrichies dans le Pont.

En effet, en contournant l’héritage simple qui regrouperait le calcul et le dessin dans une seule classe, le Pont intervient pour découpler les deux dimensions orthogonales, réduisant ainsi le nombre de combinaisons nécessaires à neuf grâce à la composition pour la délégation.

3. Développement du pont

Dans notre exemple, nous cherchons à générer neuf comptes rendus de performance sur les valeurs ajoutées en combinant trois classes de produits financiers : actions, obligations et options listées, avec trois représentations de résultats : ligne de segments, barres et camemberts. Pour des raisons de confidentialité, nous simulons les prix d’achat et de vente en 2021, qui devraient normalement être récupérés à partir des bases de données de référence par des suites de valeurs fictives, de même que les prix de transaction réels. Pour la mise en forme des schémas des rapports, nous utilisons la bibliothèque “matplotlib” en Python 3.

Afin de simplifier notre démonstration, nous calculons la valeur ajoutée pour les actions et les obligations en soustrayant le prix réel du prix de vente de la référence, tandis que pour les options listées, le calcul de la valeur ajoutée repose sur la différence entre le spread (prix de vente moins prix d’achat) en valeur absolue dans la base de référence et celui des transactions réelles. Chaque rapport présente une figure montrant l’évolution des valeurs ajoutées mensuelles pendant l’année 2021.

Nous définissons l’interface IReport, qui déclare la méthode generate, appelée par le client pour générer un compte rendu. Cette interface est héritée par la classe Bridge ReportBridge, qui implémente la méthode generate en appelant la méthode draw pour dessiner sur son objet graphique. Ce dernier est une instance des classes LineChart, BarChart et PieChart, dérivées de la classe abstraite AbstractGraphic, et prend les arguments nécessaires pour tracer les figures dans les rapports. Les classes EquityReport, FixedIncomeReport et ListedOptionReport sont des classes concrètes de ReportBridge, raffinant les méthodes nécessaires liées aux différents calculs des valeurs ajoutées mensuelles.

Toutes les requêtes du client sont simplement déléguées par la classe d’interface à la classe d’implémentation encapsulée. Nous instancions une classe concrète de ReportBridge, qui prend les arguments sur les prix de transaction, la devise et l’année, ainsi qu’un objet graphique encapsulé dérivant de AbstractGraphic, avant d’appeler la méthode generate. Ainsi, les rapports des valeurs ajoutées en euros pour 2021 seront disponibles en sortie.

# bridge.py

from abc import ABC, abstractmethod
from matplotlib import pylab as plt
from typing import Dict, Tuple
import numpy as np


class IReport(ABC):
    @abstractmethod
    def generate(self):
        """
        generate report
        :return:
        """
        raise NotImplementedError



class AbstractGraphic(ABC):
    chart_cls = ''

    def __init__(self, x_label: str,
                 y_label: str,
                 title: str,
                 to_save: bool = True):
        """
        init graphic instance
        :param x_label: x label of the graphic demo
        :param y_label: y label of the graphic demo
        :param title: the title of the graphic demo
        :param to_save: whether to save the graphic demo as png
        """
        self._x_label = x_label
        self._y_label = y_label
        self._title = title
        self._to_save = to_save

    @abstractmethod
    def draw(self, added_value: Dict[str, float], currency: str):
        """
        draw graphic demo
        :param added_value: added value
        :param currency: currency of the price
        :return:
        """
        raise NotImplementedError

    def display(self):
        if self._to_save:
            plt.savefig(f'{self._title} {self.chart_cls}.png',
                        bbox_inches='tight')
        else:
            plt.show()
        plt.clf()


class LineChart(AbstractGraphic):
    chart_cls = 'Line Chart'

    def draw(self, added_value: Dict[str, float], currency: str):
        """
        draw graphic demo
        :param added_value: added value
        :param currency: currency of the price
        :return:
        """
        fig, ax = plt.subplots(figsize=(8, 8))
        # draw segments
        ax.plot(list(added_value.keys()), added_value.values(), color='b')
        # label for x-axis
        ax.set_xlabel(self._x_label)
        # label for y-axis
        ax.set_ylabel(f"{self._y_label} / {currency}")
        # add grid
        ax.grid()
        # title of the demo
        ax.set_title(self._title,
                     fontdict={'fontsize': 20, 'fontweight': 'bold'})
        # display
        self.display()


class BarChart(AbstractGraphic):
    chart_cls = 'Bar Chart'

    def draw(self, added_value: Dict[str, float], currency: str):
        """
        draw graphic demo
        :param added_value: added value
        :param currency: currency of the price
        :return:
        """
        fig, ax = plt.subplots(figsize=(8, 8))
        width = 0.75  # the width of the bars
        ind = np.arange(len(added_value))  # the x locations for the groups
        # draw bars
        bars = plt.bar(
            list(added_value.keys()),
            list(added_value.values()),
            width=width,
            color='b'
        )
        # set x ticks
        ax.set_xticks(ind)
        ax.set_xticklabels(list(added_value.keys()))
        # show y value over the bar
        for bar in bars:
            y_height = bar.get_height()
            plt.text(bar.get_x(), y_height + .005, f'{y_height:.2f}')
        # label for x-axis
        plt.xlabel(self._x_label)
        # label for y-axis
        plt.ylabel(f"{self._y_label} / {currency}")
        # title of the demo
        plt.title(self._title,
                  fontdict={'fontsize': 20, 'fontweight': 'bold'})
        # display
        self.display()


class PieChart(AbstractGraphic):
    chart_cls = 'Pie Chart'

    def draw(self, added_value: Dict[str, float], currency: str):
        """
        draw graphic demo
        :param added_value: added value
        :param currency: currency of the price
        :return: 
        """
        # explode first part
        explode_list = [0.2] + [0] * (len(added_value) - 1)

        fig, ax = plt.subplots(figsize=(8, 8))

        # capture each of the return elements
        patches, texts, percents = ax.pie(
            added_value.values(),
            labels=added_value.keys(),
            explode=explode_list,
            autopct='%.1f%%',
            wedgeprops={'linewidth': 3.0, 'edgecolor': 'white'},
            textprops={'size': 'x-large'})
        # style just the percent values.
        plt.setp(percents, color='white', fontweight='bold')
        # title of the demo
        ax.set_title(f"{self._title} {self._y_label} / {currency}",
                     fontdict={'fontsize': 20, 'fontweight': 'bold'})
        plt.tight_layout()
        # add legend
        plt.legend(title=self._x_label,
                   loc='lower left',
                   title_fontsize=10,
                   bbox_to_anchor=(-0.15, 0))
        # display
        self.display()


class ReportBridge(IReport, ABC):
    instrument_cls = ''

    def __init__(self, rfq_price: dict,
                 graphic: AbstractGraphic,
                 year: int,
                 currency: str = 'EUR'):
        """
        init report bridge
        :param rfq_price: purchase price
        :param graphic: delegated Graphic Demo instance
        :param year: report year
        :param currency: currency of the price
        """
        self._rfq_price = rfq_price
        self._added_value = self.get_added_value()
        self._graphic = graphic
        self._year = year
        self._ccy = currency

    @abstractmethod
    def get_ref_price(self) -> Dict[str, Tuple[float, float]]:
        """
        get referential prices
        :return: referential bid ask prices
        """
        raise NotImplementedError

    @abstractmethod
    def get_added_value(self) -> Dict[str, float]:
        """
        calculate added value
        :return: calculated added value
        """
        raise NotImplementedError

    def generate(self):
        """
        generate report
        :return:
        """
        print(f'The Added Valued Report of {self.instrument_cls} '
              f'for {self._year} by {self._graphic.chart_cls}.')
        self._graphic.draw(self._added_value, self._ccy)


class EquityReport(ReportBridge):
    instrument_cls = 'Equity'

    def get_ref_price(self) -> Dict[str, Tuple[float, float]]:
        """
        get referential prices
        :return: referential bid ask prices
        """
        # mock the referential data fetched from the database
        ref_bid_ask_price_from_cac40_db = {
            'JAN': (118.0, 125.4),
            'FEB': (115.0, 120.8),
            'MAR': (113.3, 118.9),
            'APR': (116.1, 120.6),
            'MAY': (116.0, 120.4),
            'JUNE': (111.0, 116.7),
            'JULY': (116.0, 120.6),
            'AUG': (119.0, 120.9),
            'SEPT': (112.0, 120.4),
            'OCT': (111.0, 120.2),
            'NOV': (110.0, 119.3),
            'DEC': (115.0, 121.5),
        }
        return ref_bid_ask_price_from_cac40_db

    def get_added_value(self) -> Dict[str, float]:
        """
        calculate added value
        :return: calculated added value
        """
        added_value = {}
        ref_price = self.get_ref_price()
        for month, price in self._rfq_price.items():
            added_value[month] = ref_price[month][1] - price
        return added_value


class FixedIncomeReport(ReportBridge):
    instrument_cls = 'Fixed Income'

    def get_ref_price(self) -> Dict[str, Tuple[float, float]]:
        """
        get referential prices
        :return: referential bid ask prices
        """
        # mock the referential data fetched from the database
        ref_bid_ask_price_from_reuters_db = {
            'JAN': (98.0, 100.4),
            'FEB': (99.0, 100.8),
            'MAR': (99.3, 100.9),
            'APR': (99.1, 100.6),
            'MAY': (97.0, 100.4),
            'JUNE': (98.0, 101.7),
            'JULY': (99.0, 103.6),
            'AUG': (100.0, 104.9),
            'SEPT': (102.0, 106.4),
            'OCT': (101.0, 105.2),
            'NOV': (100.0, 103.3),
            'DEC': (101.0, 104.5),
        }
        return ref_bid_ask_price_from_reuters_db

    def get_added_value(self) -> Dict[str, float]:
        """
        calculate added value
        :return: calculated added value
        """
        added_value = {}
        ref_price = self.get_ref_price()
        for month, price in self._rfq_price.items():
            added_value[month] = ref_price[month][1] - price
        return added_value


class ListedOptionReport(ReportBridge):
    instrument_cls = 'Listed Option'

    @classmethod
    def get_bid_ask_spread(cls, price: Dict[str, Tuple[float, float]]):
        return {
            m: abs(a - b) for m, (b, a) in price.items()
        }

    def get_ref_price(self) -> Dict[str, Tuple[float, float]]:
        """
        get referential prices
        :return: referential bid ask prices
        """
        # mock the referential data fetched from the database
        ref_bid_ask_price_from_bloomberg_db = {
            'JAN': (87.0, 90.4),
            'FEB': (85.0, 93.8),
            'MAR': (89.3, 96.9),
            'APR': (95.1, 101.6),
            'MAY': (94.0, 101.4),
            'JUNE': (88.0, 98.7),
            'JULY': (99.0, 107.6),
            'AUG': (108.0, 118.9),
            'SEPT': (103.0, 115.4),
            'OCT': (104.0, 115.2),
            'NOV': (89.0, 103.5),
            'DEC': (98.0, 114.5),
        }
        return ref_bid_ask_price_from_bloomberg_db

    def get_added_value(self) -> Dict[str, float]:
        """
        calculate added value
        :return: calculated added value
        """
        added_value = {}
        ref_spread_price = self.get_bid_ask_spread(
            self.get_ref_price()
        )
        bid_ask_spread = self.get_bid_ask_spread(
            self._rfq_price
        )
        for month, price in bid_ask_spread.items():
            added_value[month] = ref_spread_price[month] - price
        return added_value


def get_equity_reports(year: int, currency: str):
    """
    call equity reports
    :param year: report year
    :param currency: report currency
    :return:
    """
    # mock real equity performance
    equity_purchase_price_from_db = {
        'JAN': 122.0,
        'FEB': 120.2,
        'MAR': 116.4,
        'APR': 119.9,
        'MAY': 119.0,
        'JUNE': 116.1,
        'JULY': 118.0,
        'AUG': 119.3,
        'SEPT': 119.0,
        'OCT': 117.9,
        'NOV': 118.9,
        'DEC': 120.0,
    }
    # equity report with line chart
    EquityReport(equity_purchase_price_from_db,
                 LineChart(f'Months / {year}',
                           'Added Value',
                           'Equity'),
                 year,
                 currency).generate()
    # equity report with bar chart
    EquityReport(equity_purchase_price_from_db,
                 BarChart(f'Months / {year}',
                          'Added Value',
                          'Equity'),
                 year,
                 currency).generate()
    # equity report with pie chart
    EquityReport(equity_purchase_price_from_db,
                 PieChart(f'Months / {year}',
                          'Added Value',
                          'Equity'),
                 year,
                 currency).generate()


def get_fixed_income_reports(year: int, currency: str):
    """
    call fixed income reports
    :param year: report year
    :param currency: report currency
    :return:
    """
    # mock real fixed income performance
    fixed_income_purchase_price_from_db = {
        'JAN': 99.0,
        'FEB': 100.2,
        'MAR': 100.4,
        'APR': 99.9,
        'MAY': 99.0,
        'JUNE': 100.1,
        'JULY': 101.0,
        'AUG': 102.3,
        'SEPT': 103.0,
        'OCT': 103.9,
        'NOV': 100.9,
        'DEC': 102.0,
    }
    # fixed income report with line chart
    FixedIncomeReport(fixed_income_purchase_price_from_db,
                      LineChart(f'Months / {year}',
                                'Added Value',
                                'Fixed Income'),
                      year,
                      currency).generate()
    # fixed income report with bar chart
    FixedIncomeReport(fixed_income_purchase_price_from_db,
                      BarChart(f'Months / {year}',
                               'Added Value',
                               'Fixed Income'),
                      year,
                      currency).generate()
    # fixed income report with pie chart
    FixedIncomeReport(fixed_income_purchase_price_from_db,
                      PieChart(f'Months / {year}',
                               'Added Value',
                               'Fixed Income'),
                      year,
                      currency).generate()


def get_listed_option_reports(year: int, currency: str):
    """
    call listed option reports
    :param year: report year
    :param currency: report currency
    :return:
    """
    # mock real listed option performance
    listed_option_bid_ask_price_from_db = {
        'JAN': (88.0, 90.2),
        'FEB': (86.0, 92.8),
        'MAR': (89.8, 96.0),
        'APR': (95.8, 100.6),
        'MAY': (94.9, 101.3),
        'JUNE': (89.0, 96.7),
        'JULY': (99.9, 106.6),
        'AUG': (108.9, 116.9),
        'SEPT': (103.1, 113.4),
        'OCT': (104.5, 110.2),
        'NOV': (89.9, 102.5),
        'DEC': (98.8, 111.5),
    }
    # listed option report with line chart
    ListedOptionReport(listed_option_bid_ask_price_from_db,
                       LineChart(f'Months / {year}',
                                 'Added Value',
                                 'Listed Option'),
                       year,
                       currency).generate()
    # listed option report with bar chart
    ListedOptionReport(listed_option_bid_ask_price_from_db,
                       BarChart(f'Months / {year}',
                                'Added Value',
                                'Listed Option'),
                       year,
                       currency).generate()
    # listed option report with pie chart
    ListedOptionReport(listed_option_bid_ask_price_from_db,
                       PieChart(f'Months / {year}',
                                'Added Value',
                                'Listed Option'),
                       year,
                       currency).generate()


if __name__ == '__main__':
    report_year = 2021
    report_currency = 'EUR'
    get_equity_reports(
        report_year, report_currency
    )
    get_fixed_income_reports(
        report_year, report_currency
    )
    get_listed_option_reports(
        report_year, report_currency
    )

Voici les résultats affichés dans la console lorsque nous demandons par défaut à l’objet graphique de sauvegarder les rapports en image au format png dans le répertoire courant.

The Added Valued Report of Equity for 2021 by Line Chart.
The Added Valued Report of Equity for 2021 by Bar Chart.
The Added Valued Report of Equity for 2021 by Pie Chart.
The Added Valued Report of Fixed Income for 2021 by Line Chart.
The Added Valued Report of Fixed Income for 2021 by Bar Chart.
The Added Valued Report of Fixed Income for 2021 by Pie Chart.
The Added Valued Report of Listed Option for 2021 by Line Chart.
The Added Valued Report of Listed Option for 2021 by Bar Chart.
The Added Valued Report of Listed Option for 2021 by Pie Chart.

Les neuf comptes rendus sont bien générés séquentiellement, au fur et à mesure que les méthodes get_equity_reports, get_fixed_income_reports, get_listed_option_reports sont appelées.

En réalité, le Pont incarne l’essence de l’idiome “poignée/corps” en conception. La “poignée” représente notre classe Bridge. Elle est perçue par l’utilisateur comme la classe réelle, tandis que le travail réel s’effectue dans le “corps” à travers nos objets graphiques encapsulés. Cet idiome illustre le partage d’une seule ressource par plusieurs classes qui en contrôlent l’accès. En effet, quel que soit l’enrichissement des classes de produits, les classes graphiques restent inchangées, réutilisables et extensibles, grâce à leur exposition limitée au pont via la méthode draw, déclarée dans leur interface d’implémentation.

4. Conclusion

Dans cet article, nous débutons en mettant en lumière la pertinence du patron de conception Pont pour notre contexte spécifique, en explorant les mécanismes de sa famille de patrons de conception structurelle. À travers son diagramme UML, nous soulignons la nécessité d’encapsuler une référence à l’intérieur du Pont en divisant les fonctions indépendantes. En réalité, le Pont reflète l’idée de l’idiome “poignée/corps”, qui décompose une abstraction complexe en classes plus petites et plus gérables. Par ailleurs, notre exemple est implémenté en Python 3, et les résultats obtenus lors de l’implémentation répondent à nos attentes par rapport à ce patron de conception structurelle.

Références

[1] https://www.invivoo.com/adaptateur-design-pattern/

[2] https://www.invivoo.com/design-patterns-patrons-conception/

[3] https://www.invivoo.com/lart-clean-code-environnement-java/

[4] https://www.invivoo.com/fabrique-abstraite/