Outils de trading
Cette étude retrace une série d’outils personnels écrits autour de MetaTrader 5, depuis un relais de 124 lignes jusqu’à un pipeline qui collecte les chandeliers d’un terminal, les envoie à une API écrite en Go et les conserve dans TimescaleDB.
- Rôle
- Auteur unique
- Période
- De mars 2025 à août 2026
- État
- En développement. Outillage personnel, sans utilisateur tiers
- Technologies
- Go, TimescaleDB, Python, MetaTrader 5, MQL5, Flask
- Dépôts
- Trois, ainsi que quelques fichiers isolés
Contexte
Il s’agit d’outillage, et non d’une méthode : aucun résultat de trading n’est rapporté ici. La série commence en avril 2025 par un relais Flask de 124 lignes, qui conserve le dernier message reçu pour chaque symbole et le restitue à une petite application Flutter, laquelle l’interroge toutes les quinze secondes. Ce relais ne calcule rien et ne passe aucun ordre, de sorte que le nom de « bot » que portait son dépôt lui convenait mal.
En décembre 2025 vient un second dépôt, qui réunit un programme Python pilotant le terminal et son équivalent écrit en MQL5, tenus alignés par simple convention, ainsi qu’un outil de balayage des marchés. Le pipeline de données date de juin 2026 : il est né de cet outil de balayage, qu’il sert depuis une base.
Problème
Les chandeliers se trouvent dans un terminal qui tourne à domicile, alors que les outils qui les lisent sont sur un serveur. Le chandelier en cours change jusqu’à sa clôture, et doit donc pouvoir être renvoyé sans créer de doublon. Le terminal horodate en outre ses données à l’heure du serveur du courtier, qui n’est ni l’heure universelle ni l’heure locale et qui change avec l’heure d’été. Il fallait enfin transmettre des commandes à un programme situé à domicile sans ouvrir de port sur ce réseau.
Réponse apportée
Une clé naturelle et une écriture idempotente
Un chandelier est identifié par son symbole, son unité de temps et son heure d’ouverture en temps universel. L’écriture met à jour le plus haut, le plus bas, la clôture et le volume d’une ligne existante, jamais son ouverture, si bien que le collecteur renvoie le chandelier en cours à chaque cycle sans précaution particulière. La conservation ne repose pas sur une politique de rétention : l’écriture et la purge s’exécutent dans la même transaction et gardent un nombre fixe de lignes par série.
const upsertSQL = `
INSERT INTO bars (symbol, timeframe, ts, open, high, low, close, volume)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (symbol, timeframe, ts) DO UPDATE
SET high = EXCLUDED.high,
low = EXCLUDED.low,
close = EXCLUDED.close,
volume = EXCLUDED.volume;`
// pruneSQL deletes rows older than the window-th newest bar for a series, keeping
// exactly `window` rows. $3 is the zero-based offset of that row (window - 1).
// If fewer than `window` rows exist the subquery returns NULL and nothing is deleted.
const pruneSQL = `
DELETE FROM bars
WHERE symbol = $1 AND timeframe = $2
AND ts < (
SELECT ts FROM bars
WHERE symbol = $1 AND timeframe = $2
ORDER BY ts DESC
OFFSET $3 LIMIT 1
);`Un tampon supprimé au bout de 84 minutes
La première version du collecteur conservait dans une base SQLite les envois qui n’avaient pas abouti. Elle a été retirée 84 minutes après avoir été écrite, le terminal gardant déjà l’historique : le collecteur demande désormais au serveur ce qu’il détient et n’envoie que ce qui manque, ou la fenêtre entière au premier démarrage. Un envoi qui échoue remet simplement la série en état de vérification.
L’heure du courtier tenue pour suspecte
Un décalage horaire fixe, inscrit dans la configuration, a horodaté pendant un temps chaque chandelier avec une heure d’avance, après un passage à l’heure d’été resté inaperçu. Le décalage est depuis mesuré à chaque cycle, à partir d’une cotation dont le collecteur vérifie qu’elle est récente, puis arrondi à la demi-heure. Un chandelier daté dans le futur est refusé par le collecteur, puis de nouveau par le serveur, de sorte qu’une erreur de décalage se manifeste par un refus et non par des données fausses. Le stockage reste en temps universel et l’heure locale n’intervient qu’à l’affichage.
Des commandes au plus une fois
Le programme qui passe les ordres et la page qui le commande se connectent tous deux, en sortie, à un point de rencontre situé sur le serveur, ce qui dispense d’ouvrir un port à domicile. Une commande est une ligne en base, qui passe de l’attente à l’envoi puis au succès ou à l’échec. Une commande en attente depuis plus de cinq secondes est déclarée en échec, et une commande envoyée sans accusé de réception n’est jamais rejouée, le risque d’un ordre passé deux fois l’emportant sur celui d’un ordre perdu.
Limites et perspectives
Les tests ne portent que sur trois fonctions de calcul d’indicateurs et sur l’affichage des pages ; rien ne vérifie l’écriture, la purge, les alertes ni le canal de commandes, et aucun pipeline n’exécute ces tests. Les migrations sont par ailleurs rejouées en entier à chaque démarrage, sans table de suivi, ce qui a déjà placé l’API dans une boucle de redémarrage. La base n’emploie de TimescaleDB que la table partitionnée, sans agrégat continu ni compression, et la détection des trous dans les séries reste hors du périmètre.
La documentation a pris du retard sur le code : elle décrit encore le tampon SQLite et un décalage horaire réglé à la main. Dans le dépôt précédent, les deux mises en œuvre tenues alignées par convention ont fini par diverger sur plusieurs règles. La série comporte enfin ses impasses, à savoir le relais et son application, qui n’ont jamais été réemployés, et une maquette de panneau qui n’a pas été reliée au terminal.
