Calculs & affichages / Le mode geek /
Indicateurs dynamiques

Ajout d'une dimension à l'espace des recherches


Description

L'indicateur dynamique :


Plan du tuto :
  1. Préparation : Modèles dyn_model et dyn_quick_model.
  2. Fonction polynomiale.
  3. Lignes de tendance.

 


1 - Modèles dyn_model et dyn_quick_model :

Retour au plan

Préparation :

Voici 2 modèles conçus pour gagner du temps lors de la création d'une nouvelle stratégie basée sur des indicateurs dynamiques :

☐ /trading/strategies/models/dyn_model.py :

""" Version 2022-04-22 """
# Imports externes
import pandas_ta as ta                # Pour afficher la documentation d'un indicateur -> help(ta.kama)
# import numpy as np
import seaborn as sns

# Imports internes
from functions.utils import Dictionary
from show.show_geek import ShowGeek


# noinspection PyTypeChecker,PyUnresolvedReferences
class Geek(ShowGeek):
    def __init__(self):
        super().__init__()

    def central_args(self, l_keys):
        od_args = Dictionary(dict(
            # ************************************ FENÊTRE ************************************
            ui=dict(
                geometry=(200, 40, 1_600, 700),
                abscissa_size=200,
                window_title="Signaux dynamiques (Modèle)",
                figure_title="Modèle 'dyn_model.py'",
                subplots=dict(
                    Principal=60,
                    Milieu='',
                    # Bas='',
                ),
                # show_volume=True,
                # animation=False,
                interval=10,
            ),

            # ******************************** DATAFRAME PILOTE *******************************
            pilot=dict(
                instrument='EUR/USD',
                table=10,
                test=dict(pc_from=None, pc_to=100, nb_rows=2_000),
            ),

            # ********************************** INDICATEURS **********************************
            indics=dict(
                backtest=False,
                stats=False,
                lignes=dict(
                    function=ta.lines,
                    series=['Trace_H', 'Pente_H', 'X_H', 'Trace_L', 'Pente_L', 'X_L', 'Stats'],
                    params=dict(
                        # statistics=False,
                        delay=10,
                    ),
                ),
                custom=dict(
                    function=self.custom,
                    params=dict(),
                ),
                ma=dict(
                    function=ta.ema,
                    series='MA',
                    params=dict(
                        length=80,
                    ),
                ),
            ),

            # *********************************** AFFICHAGE ***********************************
            display=dict(
                Principal=dict(
                    lines=dict(     # c : 'r', 'g', 'b', 'y', 'm', 'c', 'w', 'k' - Voir matplotlib named_colors.
                        Trace_H=dict(c='b', lw=.6, ls='-'),     # ls = '-', '--', '-.', ':' ('' = invisible)
                        Trace_L=dict(c='darkorange', lw=.6, ls='-'),
                        Mean_trace=dict(c='g', lw=.3, ls='-'),
                        MA=None,
                    ),
                    legend='lm',        # 2 lettres : [l|c|r][h|m,b], ou bien False.
                    color_between=[     # (Ctrl + clic) https://matplotlib.org/stable/gallery/color/named_colors.html
                        dict(y1='Trace_H', y2='Trace_L', color_up='#0a01', color_down='#00a1'),
                    ],
                ),
                Milieu=dict(
                    lines={
                        'Pente_H': dict(c='b', lw=.6, ls='-'),
                        'Pente_L': dict(c='darkorange', lw=.6, ls='-'),
                        # 'Length': dict(zoom=.05),
                    },
                    legend='lm',        # 'rh', 'lh', 'lb', 'rb', 'rm', 'lm', 'rm', 'cb', 'ch', 'cm' ... ou False.
                    lines_H=[
                        dict(y=0, lw=.5, ls='-.', c='r'),
                    ],
                    color_between=[
                        dict(y1='Pente_H', y2='Pente_L', color_up='#00aa0008', color_down='#00a1'),
                    ],
                ),
            )
        ))
        return Dictionary(od_args.read(l_keys, {}))

    def custom(self, **kwargs):
        """ Indicateurs personnalisés.
        - Le nom de cette méthode est libre.
        - Il doit simplement correspondre au nom donné à un indicateur de [central_args, indics].
        - Les 'Series' utilisées ici (les colonnes de df_pilot) doivent exister (préalablement créées).
        """
        indx = self.df_pilot.index.to_series()
        self.df_pilot['Length'] = 2*indx - (self.df_pilot['X_H'] + self.df_pilot['X_L'])
        self.df_pilot['Mean_trace'] = (self.df_pilot['Trace_H'] + self.df_pilot['Trace_L']) / 2

    def post_params(self):
        """ En absence de surcharge, cette méthode peut être supprimée. """
        """ Méthode optionnelle. A supprimer ou à commenter si la surcharge est inutile.
        Surcharge, insertion (avant super()) ou ajout (après super()) au code post_params() de la classe-parent. """

        """ Documentation de ta.lines """
        help(ta.lines)

        """ Affichage des peaks. """
        t_added = self.get_d_added('lignes')
        if t_added is not None:
            pass
            np_top, np_bottom = t_added
            o_ax = self.o_ax('Principal')
            sns.scatterplot(x=np_top[:, 0], y=np_top[:, 1], ax=o_ax)
            sns.scatterplot(x=np_bottom[:, 0], y=np_bottom[:, 1], ax=o_ax)

        ret = super().post_params()  # Code avant -> inséré, code après -> ajouté.
        return ret

    def hook_axis_anim(self, axis_name, x, o_ax, df, y_min, y_max, get_line):
        """ En absence de surcharge, cette méthode peut être supprimée.
        Surcharge, insertion ou ajout au code hook_axis_anim() de la classe-parent. """
        if axis_name == 'Principal':
            kwargs = dict(
                line_h=dict(col_trace='Trace_H', col_pente='Pente_H', col_x='X_H', c='b', lw=2, ls='-.'),
                line_l=dict(col_trace='Trace_L', col_pente='Pente_L', col_x='X_L', c='darkorange', lw=2, ls='-.'),
                # pattern_verbose=False,      # Valeur par défaut : True.
            )
            """ Voir documentation de l'indicateur ta.lines """
            ta.lines.plot(ax=o_ax, df=self.df_pilot, x=x, **kwargs)

        return super().hook_axis_anim(axis_name, x, o_ax, df, y_min, y_max, get_line)

    def hook_line_anim(self, axis_name, line_name, x, y, o_ax, df, o_line):
        """ En absence de surcharge, cette méthode peut être supprimée.
        Surcharge ou ajout au code hook_line_anim() de la classe-parent. """
        hook = super().hook_line_anim(axis_name, line_name, x, y, o_ax, df, o_line)     # hook = Valeur de retour.

        """ - Masque la partie droite des graph. pour permettre de voir certaines prédictions (ex: nuage ichimoku).
            - Masquage conditionnel : ajoutez des conditions avec axis_name, line_name, ... """
        # y.loc[x[-10]:] = np.nan        # La valeur 100 est un exemple, la remplacer par une variable.

        return hook


if __name__ == '__main__':
    Geek()

☐ /trading/strategies/models/dyn_quick_model.py :

""" Version 2022-04-22 """
# Imports externes
# import pandas_ta as ta                # Pour afficher la documentation d'un indicateur -> help(ta.kama)
# import seaborn as sns

# Imports internes
from functions.utils import Dictionary
from show.show_geek import ShowGeek


# noinspection PyTypeChecker,PyUnresolvedReferences
class Geek(ShowGeek):
    def __init__(self):
        super().__init__()

    def central_args(self, l_keys):
        od_args = Dictionary(dict(
            # ************************************ FENÊTRE ************************************
            ui=dict(
                geometry=(200, 40, 1_600, 700),
                abscissa_size=200,
                window_title="Signaux dynamiques (Quick model)",
                figure_title="Modèle 'dyn_quick_model.py'",
                subplots=dict(
                    Principal=60,
                    Milieu='',
                ),
                # animation=False,
                interval=10,
            ),

            # ******************************** DATAFRAME PILOTE *******************************
            pilot=dict(
                instrument='EUR/USD',
                table=10,
                test=dict(pc_from=None, pc_to=100, nb_rows=2_000),
            ),

            # ********************************** INDICATEURS **********************************
            indics=dict(
                backtest=False,
                stats=False,
                # xxxxx=dict(     # ... indicateurs
                #     function=ta.xxxxx,
                #     series=['xxxx', 'xxxx', 'xxxx'],
                #     params=dict(
                #         # ...
                #     ),
                # ),
                custom=dict(
                    function=self.custom,
                    params=dict(),
                ),
            ),

            # *********************************** AFFICHAGE ***********************************
            # color : 'r', 'g', 'b', 'y', 'm', 'c', 'w', 'k' - Voir aussi matplotlib named_colors.
            # ls = '-', '--', '-.', ':' ('' = invisible) - Voir matplotlib linestyles.
            # legend, 2 lettres : [l|c|r][h|m,b], ou bien False.
            display=dict(
                Principal=dict(
                    lines=dict(
                        # ...
                    ),
                    # legend='lm',
                    # color_between=[  # (Ctrl + clic) https://matplotlib.org/stable/gallery/color/named_colors.html
                    #     dict(y1='...', y2='...', color_up='...', color_down='...'),
                    # ],
                ),
                Milieu=dict(
                    lines=dict(
                        # ...
                    ),
                ),
            )
        ))
        return Dictionary(od_args.read(l_keys, {}))

    def custom(self, **kwargs):
        pass


if __name__ == '__main__':
    Geek()

 

☐ Renommer quick_model en ga_quick_model.

Pas d'héritage multiple, ces modèles héritent seulement de show_geek, qui a du être adapté pour assurer la compatibilité ascendante avec les modèles étudiés précédemment.

☐ /show/show_geek.py

""" Version 2022-04-22 """
# Imports externes.
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
from matplotlib.ticker import FormatStrFormatter
import matplotlib.animation
import copy

# Imports internes.
from functions.utils import Dictionary, Utils
from trading.historiques.ctrl_histos import CtrlHistos


# noinspection PyUnusedLocal,PyUnresolvedReferences
class ShowGeek:
    def __init__(self, mode=None):
        CtrlHistos.custom_ta()
        self.b_ga = mode is not None
        if self.b_ga:
            super().__init__(mode=mode)      # Appelle Genetic.__init__(), 2ème classe héritée de la classe dérivée.
        else:
            mode = 1
        self.fig = plt.figure(1)
        self.mode = mode
        self.df_pilot = None
        self.df_scats = None
        self.d_added = dict()
        self.nb_datas = 0
        self.l_ax = None
        self.pips = 0

        """ Animation. """
        self.b_anim = True
        self.abscissa_size = 0
        self.pointer = -1
        self.b_paused = False
        self.b_reverse = False
        self.magn = 1  # Appui sur les flèches : Le pas d'avancement est : 1, 10 ou 100.

        """ Signal pilote. """
        self.od = Dictionary(self.set_heights())
        self.get_df_pilot()

        """ Mise au point : Exécuter les modes 1 à 5 dans l'ordre. """
        if self.mode == 1:
            """ Mode 'normal', selon votre imagination : Création, exécution et affichage de la stratégie. """
            self.show_ui()
        elif self.mode == 2:
            """ Affichage des listes de paramètres à tester par l'algorithme génétique. """
            self.control_bounds()
        elif self.mode == 3:
            """ Exécute une seule boucle de l'algorithme génétique pour la mise au point du code. """
            self.ga(b_single_loop=True)
        elif self.mode == 4:
            """ Exécute l'algorithme génétique complet pour obtenir les meilleurs paramètres. """
            self.ga()   # Les indices fournis à la fin du traitement sont utilisées ci après (mode 5).
        else:       # 5 - Optionnel
            """ Affichage des paramètres sélectionnés par l'algorithme génétique. """
            # self.control_bounds(i1, i2, ..., i{n})  # <-- Remplacer les i par les indices fournis au mode 4.

    def get_args(self):
        args = self.central_args('ui')
        return dict(  # Ces valeurs seront à la racine du super-dictionnaire {self.od}.
            geometry=args.read('geometry', (100, 40, 1000, 700)),  # x, y, w, h.
            margins=args.read('margins', (6, 8, 10, 8)),  # Marges : haut, droite, bas, gauche.
            abscissa_size=args.read('abscissa_size', 600),  # Nb de points affichés en abscisse.
            window_title=args.read('window_title', "Modèle simple"),  # Titre de la fenêtre.
            figure_title=args.read('figure_title', "Modèle simple - Ne pas modifier"),  # Titre des graphiques.
            leader_lines=args.read('leader_lines', True),  # Lignes hortogonales de repère, suivi de la souris.
            subplots=args.read('subplots', dict(  # Noms et hauteurs (en %) des graphiques modifiables.
                Principal=65,  # Subplot principal : NE PAS MODIFER SON ORTHOGRAPHE.
                Milieu='',  # '' : Si chaîne vide => Les hauteurs seront automatiquement réparties.
                Bas='',
            )),
            show_pilot=args.read('show_pilot', True),
            show_volume=args.read('show_volume', False),
            show_linked_label=args.read('show_linked_label', True),
            best_zones=dict(
                show=args.read(['best_zones', 'show'], False),
                gap=args.read(['best_zones', 'gap'], 20),  # Nombre de pips take-profit ou stop-loss.
                bandwidth=args.read(['best_zones', 'bandwidth'], 80),  # Pourcentage : de 0 à 100.
                up=args.read(['best_zones', 'up'], True),  # Affichage des palliers haut.
                down=args.read(['best_zones', 'down'], True),  # Affichage des palliers bas.
                scatters=args.read(['best_zones', 'scatters'], True),  # Affichage des optimums sous forme de ronds.
                confirm=args.read(['best_zones', 'confirm'], True),
                zig_zag=args.read(['best_zones', 'zig_zag'], True),  # Affichage de la courbe zig-zag.
                colors=args.read(['best_zones', 'colors'], ('#ffff0030', '#ff00ff10')),  # Coloriage des ouvertures.
            ),
            animation=args.get('animation', True),
            interval=args.get('interval', .1),
            # Paramètres généraux supplémentaires ici ...
            # Paramètres seaborn : https://www.python-simple.com/python-seaborn/seaborn-general.php
        )

    def show_ui(self):
        """ ****************** Algorithme de construction ****************** """
        """ Paramètres avant la création des graphiques (axes). """
        if self.l_ax is not None:
            return

        self.pre_params()

        """ Ajout des indicateurs dans la dataframe {df_pilot} <-- Colonnes ajoutées à {df_pilot}. """
        self.add_indics()

        """ Ajout du signal pilote. """
        self.show_pilot()

        """ Distribution des signaux (des colonnes) : un dataframe par axe dans {self.l_ax}. """
        self.distrib()

        """ Création des graphiques (axes). """
        self.build_axis()

        """ Paramètres après la création des graphiques (axes). """
        self.post_params()

        """ Affichage animé. """
        self.show()

    def animate(self, _=''):
        """ Appelé dans la boucle matplotlib.FuncAnimation depuis self.show(). """
        def get_line(_line_name):
            _from, _to = x[0], x[0] + len(x)
            return self.df_pilot[_line_name][_from: _to]

        if self.b_paused:
            return

        """ Gestion du pointeur. """
        self.set_pointer()

        """ Abscisse commune : liste de {abscissa_size} valeurs depuis {pointer}. """
        x = list(range(self.pointer, self.pointer + self.abscissa_size))

        """ Parcours des graphiques (axes). """
        for axis_name in self.od.keys():
            if axis_name == 'figure':
                continue
            o_ax = self.od.read([axis_name, 'o_ax'])
            if o_ax is None:
                continue
            df = self.od.read([axis_name, 'df'])
            y_min, y_max = 10 ** 9, -10 ** 9
            if not (df is None or df.empty):
                """ Parcours des courbes (lines). """
                for o_line in self.get_axis_lines(axis_name):
                    """ Plusieurs courbes (lines) dans un graphique (o_ax). """
                    line_name = o_line.get_label()
                    serie = df[line_name]
                    y = serie[self.pointer: self.pointer + self.abscissa_size]

                    """ Égalisation des tailles de x et y. """
                    len_min = min(len(x), len(y))
                    x, y = copy.copy(x[:len_min]), copy.copy(y[:len_min])

                    """ Hook pour l'injection de paramètres dans cette courbe. """
                    d_attr = self.hook_line_anim(axis_name, line_name, x, y, o_ax, df, o_line)
                    if d_attr is None:
                        d_attr = {}

                    """ Injection des datas à afficher. """
                    if d_attr.get('visible', True):
                        o_line.set_ydata(y)
                        o_line.set_xdata(x)
                        if line_name == 'Close':
                            o_line.set_color('0.6')

                    if not np.isnan(y).all():
                        y_min = min(y_min, np.nanmin(y))
                        y_max = max(y_max, np.nanmax(y))

            """ Calcul des limites des ordonnées (y). """
            if y_max < y_min:
                y_min, y_max = -10, 10
            y_padding = max(0, (y_max - y_min) * 0.05)  # Marges top et bottom dans les axes (5%)
            y_min, y_max = (round(y_min - y_padding, 6), round(y_max + y_padding, 6)) if y_max >= y_min else (-10, 10)
            y_min = max(y_min, -10 ** 9)
            y_max = min(y_max, 10 ** 9)
            """ Limites x et y. """
            o_ax.set_xlim(x[0], x[-1])
            if y_min == y_max:
                y_min = y_max = None
            o_ax.set_ylim(y_min, y_max)

            """ Effacement d'éléments ajoutés dans le hook : lignes, scatters, zônes colorées, textes, ... """
            # https://matplotlib.org/stable/api/collections_api.html#matplotlib.collections.PathCollection
            [o_child.remove() for o_child in o_ax.l_added]
            l_before = o_ax.get_children()

            """ Injection d'éventuels éléments : lignes, scatters, zônes colorées, textes, ... """
            self.hook_axis_anim(axis_name, x, o_ax, df, y_min, y_max, get_line)

            """ Mémorisation des éventuels éléments. """
            o_ax.l_added = [o_child for o_child in o_ax.get_children() if o_child not in l_before]

        if not self.b_anim:
            plt.draw()

    def set_pointer(self):
        if self.b_reverse:
            self.pointer -= self.magn
            if self.pointer < 0:
                self.pointer = self.nb_datas - self.abscissa_size
        else:
            self.pointer += self.magn
            if self.pointer > self.nb_datas - self.abscissa_size:
                self.pointer = 0

    def show_pilot(self):
        """ Affichage conditionnel du signal-pilote. """
        self.od.write(['Principal', 'signals', 'Close', 'visible'], self.od.read(['figure', 'show_pilot'], False))
        self.add_df('Principal', ['Close'])

    def pre_params(self):
        """ Fenêtre : taille, position, titre. """
        mgr = plt.get_current_fig_manager()
        mgr.set_window_title(self.od.read(['figure', 'window_title'], 'Stratégie'))
        win = mgr.window
        win.setGeometry(*self.od.read(['figure', 'geometry']))

        """ Paramètres seaborn : https://www.python-simple.com/python-seaborn/seaborn-general.php """
        sns.set_context(self.od.read(['figure', 'seaborn_context'], 'paper'))  # paper, notebook, talk, poster
        sns.set_style(self.od.read(['figure', 'seaborn_style'], 'darkgrid'))  # white, dark, whitegrid, darkgrid, ticks

        """ Ini attributs. """
        self.abscissa_size = self.od.read(['figure', 'abscissa_size'], 600)
        self.b_anim = self.od.read(['figure', 'animation'], False)

        """ Affichage des best-zones dans le graphique (subplot) contenant le signal 'Close'. """
        subplot_name = self.od.read(['figure', 'best_zones', 'subplot'])
        if self.od.read(['figure', 'best_zones', 'show'], False) and subplot_name:
            """ Appel de l'indicateur de trend {best_zones}. """
            gap = self.od.read(['figure', 'best_zones', 'gap'], 24)
            bandwidth = self.od.read(['figure', 'best_zones', 'bandwidth'], 80)
            self.df_pilot[['Up', 'Down', 'Peak']], self.df_scats = self.df_pilot.ta.best_zones(gap=gap,
                                                                                               bandwidth=bandwidth)
            """ Affichage des palliers haut et bas. """
            l_columns = list()
            if self.od.read(['figure', 'best_zones', 'up'], False):
                l_columns.append('Up')
            if self.od.read(['figure', 'best_zones', 'down'], False):
                l_columns.append('Down')
            self.add_df(subplot_name, l_columns)

    def post_params(self):
        """ Recherche des paramètres dans le dictionnaire. """

        """ Titre des graphiques. """
        axis_title = self.od.read(['figure', 'figure_title'])
        if isinstance(axis_title, str) and len(axis_title) > 0:
            o_ax = self.get_first_axis()
            if self.is_axis(o_ax):
                o_ax.set_title(axis_title)

        """ y axis : position et visibilité des graduations (ticks). """
        for l_keys in self.od.key_list():
            if l_keys[-1] != 'y_ticks':
                continue
            position = self.od.read(l_keys)
            o_ax = self.od.read([l_keys[0], 'o_ax'])
            if not self.is_axis(o_ax):
                continue
            if position is False:
                o_ax.axes.set_yticks([])  # Suppression (ticks + ticklabels + grille).
                pass
            else:
                o_ax.yaxis.set_ticks_position(position)  # left, right

        """ Suppression de la grille. """
        for l_keys in self.od.key_list():
            if l_keys[-1] != 'grid':
                continue
            if self.od.read(l_keys) is False:
                o_ax = self.od.read([l_keys[0], 'o_ax'])
                o_ax.grid(False)  # Suppression de la grille.

        """ x axis : visibilité des graduations (ticks) """
        for l_keys in self.od.key_list():
            if l_keys[-1] != 'x_ticks':
                continue
            o_ax = self.od.read([l_keys[0], 'o_ax'])
            if not self.is_axis(o_ax):
                continue
            if self.od.read(l_keys) is False:
                o_ax.tick_params(axis='x', colors='#fff0')  # '#fff0' = invisible

        """ Légendes, lignes horizontales et verticales, coloriages. """
        od_display = self.central_args('display')
        for axis_name, params in od_display.items():
            o_ax = self.od.read([axis_name, 'o_ax'])
            if self.get_axis_lines(axis_name):  # Si le graphique n'a aucune ligne, on ne fait rien.
                """ Légendes. """
                legend = params.get('legend')
                pos = ['auto', 'rh', 'lh', 'lb', 'rb', 'rm', 'lm', 'rm', 'cb', 'ch', 'cm']
                if isinstance(legend, bool):
                    o_ax.legend().set_visible(legend)  # Visibilité de la légende.
                elif legend in pos:
                    o_ax.legend(loc=pos.index(legend)).set_title(axis_name)
                else:
                    o_ax.legend().set_title(axis_name)

                """ Lignes horizontales. """
                l_lines_h = params.get('lines_H', [])
                if isinstance(l_lines_h, dict):
                    l_lines_h = [l_lines_h]
                for d_line in l_lines_h:
                    self.trace_hline(axis_name, **d_line)

                """ Lignes verticales. """
                l_lines_v = params.get('lines_V', [])
                if isinstance(l_lines_v, dict):
                    l_lines_v = [l_lines_v]
                for d_line in l_lines_v:
                    self.trace_vline(axis_name, **d_line)

                """ Coloriage inter-zônes : statiques et dynamiques. """
                ld_color_between = params.get('color_between', [])     # Liste de dictionnaires.
                l_args_dyn, l_args_stat = list(), list()
                for d_between in ld_color_between:
                    y1, y2 = d_between.get('y1'), d_between.get('y2')
                    if isinstance(y1, str) or isinstance(y2, str):
                        """ Coloriage dynamique, rafraîssement (à chaque affichage), délégué au hook axis. """
                        color_up, color_down = d_between.get('color_up'), d_between.get('color_down')
                        if color_up is not None:
                            l_args_dyn.append(dict(y1=y1, y2=y2, where='>', fc=color_up, interpolate=True))
                        if color_down is not None:
                            l_args_dyn.append(dict(y1=y1, y2=y2, where='<', fc=color_down, interpolate=True))
                    elif isinstance(y1, (int, float)) and isinstance(y2, (int, float)):
                        """ Coloriage statique, passage unique. """
                        color = d_between.get('color')
                        if color is not None:
                            l_args_stat.append(dict(y1=y1, y2=y2, fc=color, ec='#fff0'))
                if l_args_stat:
                    self.fill_between(o_ax, l_args_stat, b_static=True)
                if l_args_dyn:
                    self.od.write([axis_name, 'color_between'], l_args_dyn)

    def build_axis(self):
        """ Construction de la fenêtre Windows. """
        """ Marges générales et répartition des subplots. """
        l_margins = self.od.read(['figure', 'margins'], (6, 8, 10, 8))  # Marges : haut, droite, bas, gauche.
        x, w = l_margins[3] / 100, 1 - (l_margins[1] + l_margins[3]) / 100
        y_offset, y_height = l_margins[0], 100 - l_margins[0] - l_margins[2]  # Valeurs : 0 à 100.
        ax_top, y = 100, 1 - y_offset / 100
        first_axis, axis_ante = '', ''
        for axis_name, h in self.od.read(['figure', 'subplots']).items():
            o_ax = None
            if isinstance(h, (int, float)):
                if first_axis == '':
                    first_axis = axis_name
                else:
                    self.od.write([axis_ante, 'x_ticks'], False)
                axis_ante = axis_name
                h = h * y_height / 10_000
                y -= h
                o_ax = self.fig.add_axes((x, y, w, h), sharex=self.od.read([first_axis, 'o_ax']))
                self.od.write([axis_name, 'geometry'], (x, y, w, h))
            else:
                """ Graphique jumelé (twined axis). """
                tw = h.split()
                if tw[0] == 'twinned':
                    """ Axes jumelés. """
                    parent_name = tw[-1]
                    parent_axis = self.od.read([parent_name, 'o_ax'])
                    if self.is_axis(parent_axis):
                        o_ax = parent_axis.twinx()
            if self.is_axis(o_ax):
                self.od.write([axis_name, 'o_ax'], o_ax)
                """ Ajout artificiel d'attributs vides. """
                o_ax.note_x = o_ax.annotate('', (0, 0))
                # o_ax.note_y = o_ax.annotate('', (0, 0))
                o_ax.l_added = list()

                """ Repères : lignes hortogonales sous le curseur de la souris. Voir on_mouse_move()  """
                o_ax.hline = o_ax.axhline(y=-1000, color='k', lw=.2, ls='--')  # -1000 = Souris hors figure.
                o_ax.vline = o_ax.axvline(x=-1000, color='k', lw=.2, ls='--')

                """ Format des graduations y. """
                o_ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f'))

                """ Initialisation des courbes (DataFrames) dans les graphiques (axes). """
                df = self.od.read([axis_name, 'df'])
                if axis_name == self.od.read(['figure', 'best_zones', 'subplot']) and self.is_df(self.df_scats):
                    if self.od.read(['figure', 'best_zones', 'scatters'], False):
                        sns.scatterplot(data=self.df_scats, x='Indx', y='Close', hue=axis_name, ax=o_ax)
                    if self.od.read(['figure', 'best_zones', 'confirm'], False):
                        sns.scatterplot(data=self.df_scats, x='confirm_x', y='confirm_y', ax=o_ax, hue='c_type',
                                        alpha=.3)
                    if self.od.read(['figure', 'best_zones', 'zig_zag'], False):
                        sns.lineplot(data=self.df_scats, x='Indx', y='Close', ax=o_ax, lw=.5)
                    o_ax.set_ylabel('')

                """ Lines. """
                if self.is_df(df) and len(df.columns) > 0:
                    sns.lineplot(data=df[:1], ax=o_ax)
                    for o_line in o_ax.lines:
                        line_name = o_line.get_label()
                        if not line_name.startswith('_line'):
                            d_vals = self.od.read([axis_name, 'signals', line_name])
                            if d_vals is None:
                                continue
                            color = d_vals.get('c', d_vals.get('color'))
                            if color is not None:
                                o_line.set_color(color)
                            if line_name.endswith('__trace'):
                                o_line.set(alpha=.2)
                                o_line.set_linewidth(4)
                            else:
                                linewidth = d_vals.get('lw', d_vals.get('linewidth'))
                                if linewidth is not None:
                                    o_line.set_linewidth(linewidth)
                                linestyle = d_vals.get('ls', d_vals.get('linestyle'))
                                if linestyle is not None:
                                    o_line.set_linestyle(linestyle)

        """ Position (left, right) des graduations y. """
        b_odd = False
        l_axes = self.get_axis_names()
        if self.od.read(['Volume', 'twined_axis']) == 'Principal':
            b_odd = bool(l_axes.index('Principal') % 2)  # Position impaire de 'Principal'.
        for indx, axis_name in enumerate(l_axes):
            self.od.write([axis_name, 'y_ticks'], 'left' if b_odd == indx % 2 else 'right')

        """ Coloriage vertical conditionnel des zônes d'entrée en position (ouverture de trade). """
        b_zones = self.od.read(['figure', 'best_zones', 'show'], False)
        colors = self.od.read(['figure', 'best_zones', 'colors'])
        if b_zones and colors is not None:
            peaks = self.df_pilot['Peak']
            fc_b, fc_t = colors
            l_args = [
                dict(x=peaks.index, y1=10 ** 5, y2=-10 ** 5, where=peaks <= -1, fc=fc_b, ec='#fff0', interpolate=True),
                dict(x=peaks.index, y1=10 ** 5, y2=-10 ** 5, where=peaks >= 1, fc=fc_t, ec='#fff0', interpolate=True),
            ]
            for ax_name in l_axes:
                oax = self.o_ax(ax_name)
                if self.is_axis(oax):
                    self.fill_between(oax, l_args)

    def set_heights(self):
        """ Calcul des hauteurs (en %) subplots. La somme fait 100%. """
        args = self.get_args()

        if not isinstance(args['subplots'], dict):
            raise SystemExit("Super-dictionnaire : La clé 'subplots' doit exister et doit contenir un dictionnaire. ")
        l_heights, sum_heights, nb_zeros = list(), 0, 0
        for name, pc_height in args['subplots'].items():
            height = pc_height if (isinstance(pc_height, int) and pc_height > 0) else 0
            nb_zeros += 1 if height == 0 else 0
            sum_heights += height
            l_heights.append((name, height))

        if sum_heights > 100:
            """ La somme des hauteurs dépasse 100% : on effectue une réduction proportionnelle. """
            l_heights = [(name, pc_height) for (name, pc_height) in l_heights if pc_height > 0]  # Suppression des 0.
            l_heights = [(name, pc_height * 100 / sum_heights) for (name, pc_height) in l_heights]  # Normalisation.
        else:
            """ Si la somme == 100%, on ne fait rien. Si elle est < 100%, on ajoute un subplot, nommé 'rest'. """
            if nb_zeros == 0:
                l_heights.append(('rest', 0))
                nb_zeros = 1
            h_rest = (100 - sum_heights) / nb_zeros
            if h_rest == 0:
                l_heights = [(name, pc_height) for (name, pc_height) in l_heights if pc_height > 0]  # Suppression des 0
            for i, (name, pc_height) in enumerate(l_heights):
                if pc_height == 0:
                    l_heights[i] = (name, h_rest)
        args['subplots'] = dict(l_heights)

        """ Ajout d'arguments par défaut. """
        if 'seaborn_context' not in args:  # Valeurs possibles : paper, notebook, talk, poster
            args['seaborn_context'] = 'paper'
        if 'seaborn_style' not in args:  # Valeurs possibles : white, dark, whitegrid, darkgrid, ticks
            args['seaborn_style'] = 'darkgrid'

        return {'figure': args}

    def dyn_indic(self, l_columns, dyn_func, **kwargs):
        """ Code appelant : add_indics(). A ce stade, on ne connaît pas encore les graphiques (axes) affectés. """

        """ Mise en conformité des colonnes du DataFrame : type 'list', même s'il n'y en a qu'une. """
        if isinstance(l_columns, str):
            l_columns = [l_columns]
        kwargs['l_cols'] = l_columns        # type(l_columns) = list.

        """ Nombre de points affichés. """
        showed_length = min(self.abscissa_size, kwargs.get('showed_length', self.abscissa_size // 3))
        kwargs['showed_length'] = showed_length

        self.df_pilot = self.df_pilot.reindex(columns=self.df_pilot.columns.tolist() + o_dyn.l_columns)

    def add_df(self, subplot_name, l_columns, df=None):
        """ Création du dataframe qui sera associé à un graphique.
                - Méthode appelée par la méthode distrib() de la classe dérivée.
                - Le DataFrame {df} a plusieurs colonnes, chacune associée à une courbe. """

        """ Vérification de l'existence du graphique {subplot_name}. """
        if self.od.read(['figure', 'subplots', subplot_name]) is None:
            raise SystemExit(f"Erreur dans self.distrib() : Le subplot {subplot_name} n'existe pas.")

        l_entire_list = list()
        for column in l_columns:
            """ {column} est le nom de la colonne, ou bien un dictionnaire d'attributs (couleur, épaisseur, ...). """
            if isinstance(column, dict):
                """ Ajout du dictionnaire d'attributs au dictionnaire global {self.od}. """
                col_name = column.pop('name')
                d_params = column if column else None
                self.od.write([subplot_name, 'signals', col_name.strip('_')], d_params)
                column = col_name

            """ Exemples : 'RegLine' : RegLine seul, 'RegLine_' : Trace seule, 'RegLine__' : RegLine + Trace """
            if column.endswith('__'):
                l_entire_list.append(column[:-2])
                l_entire_list.append(f'{column[:-1]}trace')
            elif column.endswith('_'):
                l_entire_list.append(f'{column}trace')
            else:
                l_entire_list.append(column)

        l_cols = list()
        for column in l_entire_list:
            """ Suppression des colonnes inexistantes. """
            try:    # Test colonne par colonne.
                self.df_pilot[column] if df is None else df[column]
            except KeyError:
                """ Si la colonne n'existe pas ... """
                Utils.printc(f"distrib()-{subplot_name} : La colonne '{column}' n'existe pas dans le DataFrame.")
                continue
            l_cols.append(column)

        """ Fusion par ajout de colonnes dans le df existant. """
        df = self.df_pilot[l_cols] if df is None else df[l_cols]
        df_exist = self.od.read([subplot_name, 'df'])
        if df_exist is not None:
            df_exist[df.columns] = df
            df = df_exist  # On retrouve les colonnes de df_exist en premières places.

        if not self.is_df(df):
            return

        self.od.write([subplot_name, 'df'], df)

        for column in df.columns:
            if column.lower() == 'close':
                """ Si ce dataframe a une colonne 'Close', on paramètre 3 traitements distincts par défaut :
                        - Une étiquette-suiveuse (linked_label).
                        - Un graphique jumelé (twined_axis) contenant l'affichage du volume en semi-transparence.
                        - Éléments du graphique jumelé non affichés : y_ticks, y_ticks_labels, grille, légende. """
                self.od.write(['figure', 'best_zones', 'subplot'], subplot_name)
                if self.od.read(['figure', 'show_linked_label'], False):
                    self.od.write([subplot_name, 'linked_label'], 'Close')
                if self.od.read(['figure', 'table_name'], None) is None:
                    self.od.write(['figure', 'show_volume'], False)
                if self.od.read(['figure', 'show_volume'], False):
                    self.od.write(['figure', 'subplots', 'Volume'], f'twinned with {subplot_name}')
                    self.od.write(['Volume', 'df'], self.df_pilot[['Volume']])
                    self.od.write(['Volume', 'signals', 'Volume', 'visible'], False)
                    self.od.write(['Volume', 'twined_axis'], subplot_name)
                    self.od.write(['Volume', 'y_ticks'], False),  # Suppression (ticks + ticklabels + grille).
                    self.od.write(['Volume', 'legend'], False)  # Suppression de la légende.

    def get_df_pilot(self):
        od_pilot = self.central_args('pilot')
        if 'test' in od_pilot:
            od_pilot.fusion(od_pilot['test' if self.mode == 1 else 'learn'])
        instrument = od_pilot.read('instrument', 'EUR/USD')
        self.pips = .01 if instrument.endswith('JPY') else .0001
        table_name = od_pilot.read('table', 10)
        self.df_pilot = CtrlHistos(instrument).get_pilot(**od_pilot)  # DataFrame
        self.nb_datas = self.df_pilot.shape[0]
        self.od.write(['figure', 'instrument'], instrument)
        self.od.write(['figure', 'table_name'], table_name)
        self.od.write(['figure', 'nb_points'], self.nb_datas)

    def linked_label(self, axis_name, x, o_ax, df):
        """ https://matplotlib.org/stable/tutorials/text/annotations.html#annotating-with-text-with-box """
        linked_label = self.od.read([axis_name, 'linked_label'])
        if linked_label:
            bbox = {'boxstyle': 'larrow', 'fc': '#ff02', 'ec': 'k', 'lw': .5}  # fc=face color, ec=edge color
            indx = min(x[-1], df.shape[0] - 1)
            last_value = round(df[linked_label][indx], 2)  # En pips.
            """ Affichage de l'étiquette actuelle (m = marge). """
            m = self.abscissa_size / 70
            o_ax.annotate(last_value, (x[-1], last_value), xytext=(x[-1] + m, last_value), bbox=bbox)

    def paint_volume(self, x, o_ax, df):
        y = df['Volume'][x[0]: x[-1] + 1] * .8  # Affichage du volume à 80% d'amplitude.
        l_args = [
            dict(x=x, y1=y, y2=0, fc='#0079a333', ec='#fff0')
        ]
        self.fill_between(o_ax, l_args)

    def fill_between(self, o_ax, l_params, b_static=False):
        """ Colorie entre 2 valeurs, conditionnellement.
        @param o_ax: Axis matplotlib : graphique contenant des courbes.
        @param l_params: Liste de coloriages. Chaque élément est un dictionnaire personnalisé. Voir lien ci-dessous.
        @param b_static: Coloriage statique (un seul passage).
        https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.fill_between.html """

        """ Effacement du coloriage antérieur, nécessaire si les couleurs ont une transparence. """
        if not self.is_axis(o_ax):
            raise SystemExit(f"Erreur dans self.fill_between() : L'objet o_ax ({o_ax}) n'est pas un axis.")

        """ l_fill contient autant de paramètres  que de coloriages à effectuer. """
        for p in l_params:
            """ Égalisation des vecteurs. """
            if p.get('x') is None:
                p['x'] = range(self.df_pilot.shape[0])
            len_min = len(p['x'])
            if self.is_df(p['y1']):
                len_min = min(len_min, len(p['y1']))
            if self.is_df(p['y2']):
                len_min = min(len_min, len(p['y2']))
            p['x'] = p['x'][:len_min]

            """ Coloriage. """
            o_ax.fill_between(**p)

    """ *********************************** Helpers. ************************************ """

    def trace_hline(self, axis_name, y, **kwargs):
        o_ax = self.o_ax(axis_name)
        if self.is_axis(o_ax):
            o_ax.axhline(xmin=0, xmax=1, y=y, **kwargs)

    def trace_vline(self, axis_name, x, **kwargs):
        o_ax = self.o_ax(axis_name)
        if self.is_axis(o_ax):
            o_ax.axvline(ymin=0, ymax=1, x=x, **kwargs)

    @staticmethod
    def is_axis(o_ax):
        return o_ax.__class__.__name__ == 'Axes'

    @staticmethod
    def is_df(df):
        return df.__class__.__name__ in ['DataFrame', 'Series']

    def get_axis_names(self, b_twin=False):
        if b_twin:
            """ Liste de noms de tous les graphiques (subplots) de base et twined_axis. """
            return [ax_name for ax_name in list(self.od.read(['figure', 'subplots']).keys()) if ax_name != 'rest']
        else:
            """ Liste de noms de tous les graphiques (subplots) de base. """
            return [ax_name for (ax_name, val) in list(self.od.read(['figure', 'subplots']).items())
                    if ax_name != 'rest' and not str(val).startswith('twinned')]

    def get_first_axis(self):
        l_axes = self.get_axis_names()
        o_ax = self.o_ax(l_axes[0])
        if self.is_axis(o_ax):
            return o_ax

    def get_last_axis(self):
        l_axes = self.get_axis_names()
        o_ax = self.o_ax(l_axes[-1])
        if self.is_axis(o_ax):
            return o_ax

    def _get_all_ax(self):
        l_ax = list()
        for key, val in self.od.read(['figure', 'subplots']).items():
            l_ax.append(self.od.read([key, 'o_ax']))
        return l_ax

    def get_axis_lines(self, axis_name):
        """ Retourne une liste d'objets o_line. """
        o_ax = self.o_ax(axis_name)
        if o_ax is None:
            return []
        l_olines = list()
        for o_line in o_ax.lines:
            """ Plusieurs courbes (lines) dans un graphique (o_ax). """
            line_name = o_line.get_label()
            if line_name.startswith('_line'):
                continue
            l_olines.append(o_line)
        return l_olines

    def o_ax(self, axis_name):
        return self.od.read([axis_name, 'o_ax'])

    def df(self, axis_name):
        return self.od.read([axis_name, 'df'])

    def ax_df(self, axis_name):
        o_ax = self.od.read([axis_name, 'o_ax'])
        df = self.od.read([axis_name, 'df'])
        return o_ax, df

    def distrib(self):
        od_display = self.central_args('display')
        for axis, params in od_display.items():
            d_lines = params.get('lines', {})
            for line, d_params in d_lines.items():
                if isinstance(d_params, dict):
                    d_params['name'] = line
                else:
                    d_params = dict(name=line)
                self.add_df(axis, [d_params])

    """ ***************************** Méthodes surchargées. ***************************** """
    @staticmethod
    def get_args_ui():
        pass

    @staticmethod
    def central_args(l_keys):
        raise SystemExit("show_geek.py > ShowGeek :\nLa méthode 'central_args()' doit être surchargée.")

    def get_pilot(self):
        raise SystemExit("show_geek.py > ShowGeek :\nLa méthode 'get_pilot()' doit être surchargée.")

    """ ***************************** Méthodes à surcharger. **************************** """
    def get_d_added(self, indic_key):
        return self.d_added.get(self.get_signature(indic_key))

    def get_signature(self, indic_key):
        """ Fournit une signature unique à chaque indicateur, par assemblage des ses paramètres et noms de colonnes. """
        od_indics = self.central_args('indics')
        d_params = od_indics.read([indic_key, 'params'], {})
        d_params['l_cols'] = od_indics.read([indic_key, 'series'], [])
        signature = '_'.join(['', indic_key] + [str(param) for param in d_params.values()])
        return signature.replace("'", '')   # Suppression des apostrophes.

    def add_indics(self, od_indics=None):
        if od_indics is None:
            """ Indicateurs utilisés dans cette stratégie. Voir central_args(), section 'indics'. """
            od_indics = self.central_args('indics')

        """ Signaux pour le calcul des gains, les marqueurs et l'affichage. """
        for l_keys in od_indics.key_list():
            if l_keys[-1] == 'function':
                indic_key = l_keys[0]
                function = od_indics.read(l_keys)
                od_indics.write([indic_key, 'name'], indic_key)

                kwargs = od_indics.read([indic_key, 'params'], {})
                if function.__module__ == '__main__':
                    """ Traitement personnalisé, dans la classe dérivée utilisatrice. """
                    function(**kwargs)  # Exécution de la fonction ou méthode.
                    continue

                if self.b_ga:
                    self.get_indic(function, **od_indics[indic_key])
                else:
                    series = od_indics.read([indic_key, 'series'])
                    if series is None:
                        continue

                    close = kwargs.pop('close', 'Close')
                    df = function(self.df_pilot[close], **kwargs)
                    if df.__class__.__name__ == 'tuple':
                        signature = self.get_signature(indic_key)
                        self.df_pilot[series] = df[0]
                        self.d_added[signature] = df[1:]
                    else:
                        self.df_pilot[series] = df
        if self.b_ga:
            return super().add_indics(od_indics)

    def hook_axis_anim(self, axis_name, x, o_ax, df, y_min, y_max, get_line):
        """ Affichage de 2 éléments distincts :
            - Volumes, semi-transparent, sans bordure.
            - Étiquette-suiveuse """

        """ 1 - Affichage des volumes, semi-transparent, sans bordure. """
        if axis_name == 'Volume':
            self.paint_volume(x, o_ax, df)

        """ 2 - Étiquette-suiveuse à droite du graphique. """
        self.linked_label(axis_name, x, o_ax, df)

        """ 3 - Coloriage inter-zônes dynamique. """
        l_od_args = self.od.read([axis_name, 'color_between'])
        l_args = list()
        if l_od_args:
            for d_args in l_od_args:
                params = dict()
                try:
                    y1, y2 = d_args['y1'], d_args['y2']
                    if isinstance(y1, str):
                        y1 = get_line(y1)
                    if isinstance(y2, str):
                        y2 = get_line(y2)
                    params['x'] = x
                    params['y1'] = y1
                    params['y2'] = y2
                    params['where'] = (y1 > y2) if d_args['where'] == '>' else (y1 < y2)
                    params['fc'] = d_args['fc']
                    params['ec'] = '#fff0'
                    params['interpolate'] = d_args['interpolate']
                    l_args.append(params)
                    self.fill_between(o_ax, l_args)
                except (Exception,) as err:
                    if not hasattr(o_ax, 'hook_error'):     # Pour éviter la répétition.
                        o_ax.hook_error = f"ShowGeek.hook_axis_anim()." \
                                          f" Erreur dans le coloriage inter-zônes du graphique '{axis_name}' :\n{err}"
                        Utils.printc(o_ax.hook_error)

    def hook_line_anim(self, axis_name, line_name, x, y, o_ax, df, o_line):
        d_attr = self.od.read([axis_name, 'signals', o_line.get_label()])
        llab = o_line.get_label()
        if d_attr is not None:
            zoom = d_attr.get('zoom')
            if zoom is not None:
                y *= zoom
        return d_attr

    """ **************************** Événements et affichage. *************************** """

    def key_event(self, ev):
        def arrows(b_forward):
            b_reverse = self.b_reverse
            self.b_paused, self.b_reverse = False, False if b_forward else True
            self.animate()
            self.b_paused = True
            self.b_reverse = b_reverse
            self.magn = 1

        keycode = ev.key
        if keycode == ' ':
            """ Pause on/off. """
            self.b_paused = not self.b_paused
        elif keycode == 'tab':
            """ Inversion de sens. """
            self.b_reverse = not self.b_reverse
        elif keycode == 'right' or keycode == 'left':
            """ Appui sur touche flèche droite ou flèche gauche. """
            arrows(keycode == 'right')
        elif keycode == 'up' or keycode == 'down':
            """ Appui sur touche flèche haut ou flèche bas. """
            self.magn = 10
            arrows(keycode == 'up')
        elif keycode == 'pageup' or keycode == 'pagedown':
            """ Appui sur touche flèche page-haut ou flèche page-bas. """
            self.magn = 100
            arrows(keycode == 'pageup')

    def on_mouse_move(self, ev):
        """ Le style des lignes est définidans self.build_axis(). """
        for o_ax in self.l_ax:
            o_ax.vline.set_data([ev.xdata, ev.xdata], [0, 1])
            o_ax.hline.set_data(([0, 1], [ev.ydata, ev.ydata]) if o_ax == ev.inaxes else ([0, 0], [0, 0]))

            if ev.xdata is not None and ev.ydata is not None:
                ax = self.get_last_axis()
                ax.note_x.remove()  # Effacement de l'étiquette précédente.
                bbox = {'boxstyle': 'round4', 'fc': '#ff02', 'ec': 'k', 'lw': .5}  # fc=face color, ec=edge color
                ax.note_x = ax.annotate(text=round(ev.xdata), xy=(ev.xdata, ax.viewLim.y0), bbox=bbox)

            if self.pointer <= 1 or not self.b_anim:
                plt.draw()  # Nécessaire si pas d'animation (lorsque self.pointer reste à 1).

    def show(self):
        self.l_ax = self._get_all_ax()
        self.fig.canvas.mpl_connect('key_press_event', self.key_event)
        if self.od.read(['figure', 'leader_lines']):
            self.fig.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
        if self.b_anim:
            interval = self.od.read(['figure', 'interval'], 1)
            ani = matplotlib.animation.FuncAnimation(self.fig, self.animate, interval=interval, repeat=True)
            # |_ La variable {ani} doit exister pour empêcher le garbage collector de supprimer l'animation.
        self.animate()
        plt.show()

Important :


2 - Fonction polynomiale :

Retour au plan

Indicateur personnalisé 'Poly' :

 


Vérification pas à pas :


L'explication est exposée pas à pas. Ce point de départ ne représente que le signal pilote, à savoir les 20 000 derniers points d'un Renko de maille 7.
 


 


L'indicateur poly calcule la droite de régression linéaire sur les derniers points du graphique affiché.
 


Les sommets et les creux sont affichés pour la compréhension.

 


Les segments de droite se comportent comme des stylets laissant une trace tout à droite. Avancer manuellement pour mieux observer.
 


Régression polynomiale de degré 3 : y = ax^3 + bx^2 + cx + d
Seuls les coefficients a, b, c et d de chaque courbe ont été mémorisés par l'indicateur.
La fonction plot(), appellée à chaque affichage, synthétise les courbes avec ces coefficients.

 


3 - Lignes de tendance pour le trading :

Retour au plan


Chaque combinaison de droites prétend à des prédictions sur l'évolution du marché.

 


Les pentes (les dérivées) sont positives, nulles ou négatives. Elles sont représentées dans le graphique 'Milieu'.
 


Ce tuto termine le mode geek.

Bonnes explorations et bon courage !


Bonjour les codeurs !