Vous connaissez déjà le calcul de l’amplitude d’une bougie : high - low. Si plusieurs calculs en ont besoin, vous pouvez le placer dans une fonction. Son appel pourra indiquer l’intention du calcul, tout en regroupant la formule à un seul endroit.

Les entrées et le résultat d’une fonction

def price_range(top: float, bottom: float) -> float:
    return top - bottom

def définit une fonction nommée price_range. Elle attend deux entrées, top et bottom. Ce sont des nombres qui peuvent avoir une partie décimale.

-> float indique le type du résultat. return calcule et renvoie ce résultat, puis termine l’appel de la fonction.

price_range(105, 100) donne 5. La fonction ne sait pas si ces valeurs sont des prix et ne choisit pas de bougie. C’est le code appelant qui lui fournit les deux valeurs.

Le corps est indenté de quatre espaces. Ici, la définition se trouve à côté de main(), au niveau extérieur du fichier. Nous lui transmettons donc les deux prix nécessaires comme paramètres. Vous pouvez aussi définir une fonction dans main() ; nous l’essaierons plus bas.

La portée d’un nom

top et bottom sont des paramètres utilisables seulement dans price_range. main() ne peut pas les consulter directement : elle utilise le résultat renvoyé.

La portée d’un nom désigne la partie du code où il est accessible. Deux fonctions peuvent utiliser les mêmes noms de paramètres sans partager une même variable.

Définie au niveau extérieur du fichier, price_range n’accède pas automatiquement aux variables locales de main(). Elle peut cependant utiliser les noms importés au début du fichier, comme close. L’emplacement de la définition influence donc aussi la visibilité des noms.

Définir une fonction et l’appeler sont deux opérations différentes. La définition donne la règle ; l’appel l’exécute avec des valeurs concrètes.

Le programme complet

Créez price_functions.py. La même fonction y calcule l’amplitude totale de la bougie et la variation signée entre ouverture et clôture :

"""
@pyne edge
"""
from pynecore.lib import close, color, high, low, open, plot, script


def price_range(top: float, bottom: float) -> float:
    return top - bottom


@script.indicator("Price Functions", overlay=False)
def main():
    full_range = price_range(high, low)
    body_change = price_range(close, open)
    plot(full_range, "Full range", color=color.blue)
    plot(body_change, "Body change", color=color.orange)

Exécutez-le sur les données de démonstration. L’amplitude totale est positive ou nulle, puisque le plus haut est au moins égal au plus bas. La variation entre ouverture et clôture peut être négative. La fonction ne modifie pas les prix reçus ; elle renvoie leur différence.

Les deux appels de price_functions.py tracent l’amplitude totale de la bougie et la variation signée de son corps.
La même fonction personnalisée calcule deux résultats à partir d’entrées différentes.

Cette fonction est courte et facile à vérifier à la main. Dans un programme plus grand, vous pouvez nommer des sous-tâches plus complexes, sans pour autant devoir créer une fonction pour chaque addition.

L’ordre des paramètres

L’appel price_range(high, low) transmet high à top et low à bottom. Les inverser inverse aussi le signe de la soustraction.

Vous pouvez aussi indiquer les noms des paramètres :

full_range = price_range(top=high, bottom=low)

C’est plus long, mais parfois plus lisible dans un appel complexe. Le nom doit correspondre exactement au paramètre de la fonction.

Une fonction dans main()

Une fonction définie à l’intérieur d’une autre peut accéder aux variables qui l’entourent. C’est utile, par exemple, lorsque plusieurs calculs utilisent le même réglage fourni par un input.

Lissons les deux lignes du programme précédent avec une moyenne mobile. Un seul input définira sa longueur pour les deux calculs. Nous transmettrons uniquement les deux prix à la fonction interne ; elle trouvera la longueur parmi les paramètres de main().

Dans price_functions.py, conservez la fonction extérieure price_range et le décorateur de l’indicateur. Ajoutez input et ta à l’import de pynecore.lib, puis remplacez la définition de main() par :

def main(
    length: int = input.int(5, title="Average length", minval=1, maxval=200),
):
    def average_difference(top: float, bottom: float) -> float:
        return ta.sma(price_range(top, bottom), length)

    average_range = average_difference(high, low)
    average_body_change = average_difference(close, open)
    plot(average_range, "Average range", color=color.blue)
    plot(average_body_change, "Average body change", color=color.orange)

length est l’input entier déjà rencontré. La fonction interne average_difference reçoit deux paramètres : l’appel fournit top et bottom. Elle lit length dans la fonction main() qui l’entoure ; vous n’avez donc pas à le répéter dans les appels.

La définition interne est indentée de quatre espaces car elle appartient à main(). Son propre corps, la ligne return, est indenté de huit espaces. Le nom average_difference s’utilise à l’intérieur de main() ; il n’est pas accessible au niveau extérieur du fichier.

price_range calcule la différence entre les deux prix et ta.sma en renvoie la moyenne mobile. La ligne bleue représente donc l’amplitude moyenne des bougies ; l’orange, la variation signée moyenne entre ouverture et clôture. Les deux appels s’exécutent à chaque bougie, si bien que les deux moyennes sont calculées continuellement.

Exécutez le programme modifié, puis ouvrez Inputs avec l’engrenage. Passez Average length de 5 à 20 et cliquez sur Save. Le graphique ouvert se recalcule automatiquement et les deux lignes passent à une moyenne sur vingt bougies. Les deux appels internes n’ont pas changé. Avec une longueur de 1, vous retrouvez les deux lignes initiales.

Cet accès aux variables extérieures s’appelle une fermeture, ou closure en anglais. Lire une valeur ne signifie pas qu’une affectation interne peut automatiquement remplacer la variable extérieure. Cet exemple lit seulement length.

Avec les paramètres, l’appel montre toutes les valeurs transmises et la fonction se réutilise facilement pour une autre paire de prix. Une fonction interne peut être pratique lorsque plusieurs calculs reposent sur les mêmes variables de main().

Vos fonctions peuvent aussi avoir un état

price_range calcule uniquement avec les valeurs reçues. Elle ne conserve aucune variable entre bougies et n’utilise aucun historique. Vous pouvez donc l’appeler à tout moment avec deux valeurs.

En revanche, average_difference utilise un état, car son appel à ta.sma dépend des valeurs passées. Vous pouvez aussi créer directement une variable Persistent ou Series dans votre fonction.

Une fonction de comptage d’événements

Comptons séparément les bougies haussières et baissières. Écrivons une fonction qui reçoit une valeur logique et mémorise combien de fois elle a reçu vrai.

Importez Persistent au début du fichier :

from pynecore import Persistent

Placez cette fonction avant main(), au niveau extérieur du fichier :

def count_events(event: bool) -> int:
    count: Persistent[int] = 0
    if event:
        count += 1
    return count

count commence à zéro. Il augmente d’un si event est vrai, sinon il conserve sa valeur. Dans les deux cas, return renvoie le total courant. Après la fin de l’appel, la variable Persistent conserve sa valeur pour l’appel suivant.

Gardez overlay=False. Remplacez l’ancienne définition et le corps de main() par :

def main():
    bullish_count = count_events(close > open)
    bearish_count = count_events(close < open)
    plot(bullish_count, "Bullish count", color=color.green)
    plot(bearish_count, "Bearish count", color=color.red)

Nous appelons la même fonction depuis deux emplacements distincts du code. Chacun possède son propre état count : le premier compte les bougies haussières, le second les baissières. Le premier appel n’augmente pas le compteur du second.

Si les quatre premières bougies suivent cet ordre, les valeurs sont :

BougieRelation entre ouverture et clôturebullish_countbearish_count
PremièreClôture supérieure10
DeuxièmeClôture inférieure11
TroisièmePrix identiques11
QuatrièmeClôture supérieure21

Exécutez-le sur les données de démonstration. La ligne verte n’augmente que sur les bougies haussières ; la rouge, sur les baissières. Chaque nouvelle exécution reconstruit les deux compteurs à partir de zéro.

Les deux appels s’exécutent à chaque bougie ; la condition d’incrémentation se trouve dans la fonction. Avec une fonction à état, l’ordre et la fréquence des appels font partie du fonctionnement. Si vous ne l’appelez que sur certaines bougies, son historique avance seulement lors de ces appels.

Si vous voulez un seul compteur partagé, appelez la fonction une fois et réutilisez la variable résultat à plusieurs endroits. Un deuxième emplacement d’appel crée un compteur supplémentaire indépendant.

Plusieurs instructions dans une fonction

Vous pouvez calculer un résultat intermédiaire avant return :

def midpoint(top: float, bottom: float) -> float:
    total = top + bottom
    return total / 2

total est une variable locale. À la fin de l’appel, elle ne devient pas une variable de main(). Nous avons seulement renvoyé le résultat de la fonction.

À vous de jouer

Ajoutez une fonction midpoint, puis créez un indicateur superposé aux bougies qui trace leur prix médian. Utilisez ses paramètres top et bottom pour recevoir le plus haut et le plus bas.

Solution de l’exercice

Placez la définition midpoint ci-dessus avant main(). Réglez le décorateur sur overlay=True et utilisez ce corps :

center = midpoint(high, low)
plot(center, "Midpoint", color=color.blue, linewidth=2)

Vous pouvez supprimer les calculs et les tracés des deux lignes précédentes de cette version. Retirez aussi les imports inutilisés.

Expliquer l’appel

Pour price_range(close, open), expliquez quelle valeur arrive dans quel paramètre, ce que fait return et où main() reçoit le résultat. Vous pouvez suivre les mêmes trois étapes pour un appel plus long.