Skip to main content

État RUNNING - Gestion des Ordres

📊 Statut des Tests

Tests Frontend (Checkly)

checkly-frontend no-tests

Tests Backend (Vitest)

vitest-backend failing

🔗 Avant cette étape

🧭 Diagramme XState (RUNNING) — ordres et erreurs

Source machine : docs/specifications/swing-positions/xstate/etat-running.machine.json
Registre des codes d’erreur : Observabilité erreurs et error-map.json.

Vue synthétique (Mermaid)

Les errorFlags REPEATED_TP_EXCHANGE_ERROR (≥3) et TP_EXCHANGE_ERROR_STALLED (≥10) sont détaillés dans la page observabilité.

🎯 Vue d’ensemble

L’état RUNNING correspond à une position active où le système gère les ordres BUY2, TP1, TP2 et calcule le PnL en temps réel. C’est l’état le plus complexe avec de nombreuses variantes de workflow possibles.

🔍 Gestion des Ordres en RUNNING

BUY2 (processBuy2)

Conditions de création/remplacement :
  • buy2Order.status !== 'closed'
  • buy2Order n’existe pas OU prix différent OU annulé
Mise à Jour Dynamique de l’Ordre BUY2 :
  • À chaque nouvelle bougie, le système vérifie si RANGE_FILTER_LOW/HIGH a changé
  • Si le nouveau niveau est différent du prix de l’ordre BUY2 en cours :
    • L’ordre BUY2 existant est annulé
    • Un nouvel ordre BUY2 est placé au nouveau niveau RANGE_FILTER_LOW/HIGH
    • L’activité “BUY2_ORDER_UPDATED” est enregistrée avec l’ancien et nouveau prix
  • Cette mise à jour évite que l’ordre BUY2 se retrouve sous le trailing stop
  • Si l’ordre BUY2 ne peut pas être annulé (déjà exécuté), la mise à jour est ignorée
Logique si BUY2 fermé :
  • Mise à jour de buy2Order, buy2Amount, buy2Price
  • Recalcul de relativeEntryPrice (moyenne pondérée de BUY1 et BUY2)
  • Mise à jour de relativeAmount via calculateRelativeAmount()
  • Recalcul de tp1Amount / tp2Amount via les mêmes helpers que pour BUY1_COMPLETED (70 % / 30 % du montant net vendable après réserve), recalcul des prix tp1Price / tp2Price depuis le nouveau prix d’entrée moyen — reserveAmount n’est pas modifié (fixé à l’étape BUY1)
  • L’activité “BUY2_ORDER_EXECUTED” est enregistrée dans le champ activity avec le prix d’exécution et le montant de l’ordre BUY2
  • Si l’ordre TP1 n’est pas clos, on annule l’ordre pour placer nouvel ordre TP1 avec le nouveau montant actif
  • Si l’ordre TP2 n’est pas clos, on annule l’ordre pour placer nouvel ordre TP2 avec le nouveau montant actif

Close partiel (shark / SL BUY1) — closeSwingPositionAtMarket

Lorsque le SL est touché (closedReason = REACHED_STOP_LOSS) et que BUY2 n’est pas encore fermé, le montant de l’ordre Close au marché ne doit pas excéder la partie vendable de la jambe BUY1 hors réserve :
  • amountToClose = min( calculateRelativeAmount , buy1Amount - reserveAmount )
Objectif : vendre au plus buy1Amount - reserveAmount (ex. 888 − 2 = 886 contrats), en conservant la réserve sur le compte, et ne pas saturer à tort sur la seule valeur calculatedRelativeAmount si celle-ci était trop élevée. Aligné avec le comportement attendu après BUY1_SL_TOUCHED (position RUNNING, BUY2 toujours actif pour un rachat ultérieur).

TP1 (processTp1)

Conditions de création/remplacement :
  • tp1Order.status !== 'closed'
  • tp1Order n’existe pas OU montant différent OU annulé
Règle de stabilité des montants TP :
  • tp1Amount et tp2Amount sont des montants de planification, mis à jour uniquement pendant les étapes BUY (BUY1_COMPLETED / BUY2_ORDER_EXECUTED) et les cas SL documentés.
  • processTp1 / processTp2 ne doivent pas recalculer ni ajuster automatiquement tp1Amount / tp2Amount.
  • En RUNNING, ces process gèrent le cycle d’ordres TP (fetch/cancel/place) en utilisant les montants déjà établis.
Logique si TP1 fermé :
  • Archive de TP1 dans archivedTp1Orders (si nécessaire)
  • Calcul de relativeAmount via calculateRelativeAmount()
  • Si BUY2 pas encore fermé : ajustement de buy2Amount pour inclure le montant TP1 vendu

TP2 (processTp2)

Conditions de création/remplacement :
  • tp2Order.status !== 'closed'
  • tp2Order n’existe pas OU montant différent OU annulé
Logique si TP2 fermé :
  • Archive de TP2 dans archivedTp2Orders (si nécessaire)
  • Calcul de relativeAmount via calculateRelativeAmount()
  • Si BUY2 pas encore fermé : ajustement de buy2Amount pour inclure le montant TP2 vendu

Gestion des Take Profits

  • Le montant actif ÉGALE la somme des quantités achetées avec BUY1 et BUY2 moins une réserve correspondant au montant minimum d’un ordre (+ marge). Ce montant actif détermine les quantités vendues pour TP1 et TP2
  • Quantités TP1 / TP2 : sur le net vendable (relativeAmount), on calcule TP1 (70 % + précision exchange) ; TP2 = net − TP1. Somme toujours égale au net ; pas d’arrondi séparé sur les 30 %.
  • Le prix TP1 correspond à 50% de la moyenne des GAP max des 10 dernières positions et l’activité “TP1_ORDER_PLACED” sont enregistrées dans le champ activity
  • Le prix TP2 correspond à 95% de la moyenne des GAP max des 10 dernières positions et l’activité “TP2_ORDER_PLACED” sont enregistrées dans le champ activity
  • Les ordres TP sont annulés si la position se ferme et les activités “TP1_ORDER_CANCELED” et “TP2_ORDER_CANCELED” sont enregistrées dans le champ activity

Historique candles

  • Champ JSON swingPosition.candles : objet { [timeframe]: bougie[] }, clés typiques "5m" (timeframe du webhook / position) et "1m" (shark mode).
  • Webhook TradingView : à chaque traitement d’une position active, append de la bougie correspondant au timeframe (source: tradingViewWebhook).
  • Shark mode (sharkModeCron) : append de la dernière bougie 1m exchange, avec snapshot Heikin Ashi (heikinAshi, source: sharkModeCron).
  • Dédoublonnage par openTime sur une même clé (remplace l’entrée si replay), 500 bougies max par timeframe (FIFO).

💰 ÉVOLUTION DES PRIX ET CALCULS FINANCIERS

Calcul des Prix TP1/TP2

Les prix TP1 et TP2 sont recalculés à chaque fois que relativeEntryPrice change :
// Formule de calcul des prix TP
const avgMaxPnl = position.avgMaxPnlForPair || 2.0; // PnL maximum historique

// Pour LONG (achat)
tp1Price = relativeEntryPrice * (1 + (0.5 * avgMaxPnl / 100))   // 50% du PnL max
tp2Price = relativeEntryPrice * (1 + (0.95 * avgMaxPnl / 100))  // 95% du PnL max

// Pour SHORT (vente)
tp1Price = relativeEntryPrice * (1 - (0.5 * avgMaxPnl / 100))   // 50% du PnL max
tp2Price = relativeEntryPrice * (1 - (0.95 * avgMaxPnl / 100))  // 95% du PnL max

Évolution de relativeEntryPrice

Le prix d’entrée relatif est recalculé à chaque BUY2 fermé :
// Calcul du prix d'entrée pondéré
const totalBuy1Cost = buy1Amount * buy1Price;
const totalBuy2Cost = buy2Amount * buy2Price;
relativeEntryPrice = (totalBuy1Cost + totalBuy2Cost) / (buy1Amount + buy2Amount);

Calcul des Quantités TP

Les quantités TP sont déterminées lors des étapes BUY puis conservées. Le relativeAmount calculé dynamiquement (calculateRelativeAmount) sert au runtime et aux contrôles d’intégrité.
// Quantités fixes basées sur le total calculé
const calculatedRelativeAmount = calculateRelativeAmount(position)
tp1Amount = calculatedRelativeAmount * 0.7;  // 70% du total
tp2Amount = calculatedRelativeAmount * 0.3;  // 30% du total
La colonne relativeAmount stockée en base est conservée pour info/debug et vérification d’intégrité, mais n’est pas la source de vérité pour les décisions runtime.

🦈 Gestion du Shark Mode

Le Shark Mode est un mode de gestion agressive du Stop Loss activé automatiquement lorsque le prix évolue favorablement d’au moins 0.5% par rapport au prix d’entrée.

Activation du Shark Mode

Conditions :
  • Position en statut RUNNING ou NEW
  • Évolution du prix ≥ 0.5% par rapport au prix d’entrée (avgEntryPrice ou relativeEntryPrice)
  • Calcul basé sur les bougies Heikin Ashi 1m
Actions lors de l’activation :
  • sharkModeEnabled = true
  • sharkModeEnabledAt = Date.now()
  • Ajout d’une activité SHARK_MODE_ACTIVATED avec le pourcentage d’évolution

Mise à Jour du Stop Loss en Shark Mode

Fréquence : Chaque minute (cron job sharkModeCron.ts) Note importante :
  • sharkModeCron.ts s’exécute chaque minute pour surveiller les positions actives et gérer le SL agressif
  • tradingview.ts (webhook) reçoit les bougies toutes les 5 minutes pour traiter les positions et créer de nouvelles positions
  • Cette différence de fréquence peut créer des situations où sharkModeCron.ts détecte un SL touché et crée un ordre Close avec slMode = 'SL_BUY1', mais le webhook TradingView arrive 5 minutes plus tard et doit respecter ce slMode pour ne pas fermer incorrectement la position
Calcul du SL agressif :
  • LONG : SL = haLow (low de la dernière bougie Heikin Ashi)
  • SHORT : SL = haHigh (high de la dernière bougie Heikin Ashi)
  • Le SL ne peut que se rapprocher du prix (monter pour LONG, descendre pour SHORT)

Gestion du SL Touché en Shark Mode

Le système utilise un champ slMode pour distinguer le type de SL touché et déterminer le comportement de la position :
  • SL_BUY1 : SL de BUY1 touché (BUY2 non fermé) - Position reste en RUNNING
  • SL_BUY2 : SL de BUY2 touché (BUY2 fermé) - Position passe en CLOSED après l’exécution de l’ordre Close

Cas 1 : SL de BUY1 touché (BUY2 non fermé)

Comportement :
  • Le status reste RUNNING (ne pas passer en CLOSED)
  • Définir slMode = SL_BUY1 pour marquer que c’est un SL de BUY1
  • Créer un ordre Close pour vendre relativeAmount (si > 0)
  • ✅ Ajouter une activité BUY1_SL_TOUCHED avec indication “SL de BUY1”
  • ✅ Utiliser processClose() pour créer l’ordre Close (position reste en RUNNING) — annule TP1 et TP2 ouverts
  • Tant que slMode === SL_BUY1 et que BUY2 n’est pas exécuté (buy2Order.status !== 'closed') : ne pas replacer TP1 ni TP2 (processTp1 / processTp2 court-circuitent). Reprise du placement TP après exécution de BUY2 (BUY2 closed)
  • Même après l’exécution de l’ordre Close : La position reste en RUNNING si slMode === 'SL_BUY1' et buy2IsClosed === false

Cas 2 : SL de BUY2 touché (BUY2 fermé)

Comportement :
  • Définir slMode = SL_BUY2 pour marquer que c’est un SL de BUY2
  • Créer un ordre Close pour vendre relativeAmount + reserveAmount (si relativeAmount > 0)
  • ✅ Ajouter une activité BUY2_SL_TOUCHED avec indication “SL de BUY2”
  • ✅ Utiliser processClose() pour gérer la fermeture (annulation TP1/TP2, création ordre market)
  • Après l’exécution de l’ordre Close : La position passe directement en CLOSED (fermeture complète)
Logique de fermeture :
if (closeOrder.status === 'closed') {
  if (slMode === 'SL_BUY2') {
    // SL_BUY2 : Fermeture complète (relativeAmount + reserveAmount vendus)
    status = CLOSED
    closedReason = REACHED_STOP_LOSS
  } else if (slMode === 'SL_BUY1' && !buy2IsClosed) {
    // SL_BUY1 : Position reste en RUNNING (seulement relativeAmount vendu)
    status = RUNNING
  } else {
    // Fermeture normale (changement de tendance) ou SL_BUY2
    status = CLOSED
    closedReason = TREND_CHANGED | REACHED_STOP_LOSS
  }
}

📋 Variantes de Workflow

Variante 1 : Workflow Standard (Sans BUY2)

BUY1 (market) → TP1 (70%) → TP2 (30%) → CLOSE (reserveAmount)
État final: Position fermée avec profit complet

Variante 2 : Workflow Standard avec BUY2

BUY1 (market) → BUY2 (limit) → TP1 (70% total) → TP2 (30% total) → CLOSE (reserveAmount)
État final: Position fermée avec profit complet

Variante 3 : TP1 fermé avant BUY2

BUY1 → TP1 fermé → BUY2 amount += TP1 amount → BUY2 fermé → Nouveaux TP1/TP2
Vérifications:
  • TP1 archivé dans archivedTp1Orders
  • buy2Amount ajusté avec tp1Amount
  • BUY2 ordre remplacé avec nouveau montant
  • Les montants TP existants restent la référence (pas de recalcul automatique dans processTp1/processTp2)

Variante 4 : BUY2 fermé avant TP1/TP2

BUY1 → BUY2 fermé → TP1 fermé → TP2 fermé → CLOSE
Vérifications:
  • relativeEntryPrice recalculé (moyenne pondérée)
  • Les montants TP restent ceux planifiés en amont
  • TP1/TP2 archivés quand fermés

🔧 Fonctions de Vérification

calculateRelativeAmount()

Calcule le montant relatif en prenant en compte tous les ordres :
relativeAmount = 0

// Ajouter les achats fermés
+ (buy1Amount si buy1Order.status === 'closed')
+ (buy2Amount si buy2Order.status === 'closed')

// Soustraire les réserves et ventes
- reserveAmount
- (tp1Amount si tp1Order.status === 'closed')
- (tp2Amount si tp2Order.status === 'closed')
- (tp1Amount des archivedTp1Orders fermés)
- (tp2Amount des archivedTp2Orders fermés)
- (slAmount si slOrder.status === 'closed')
- (closeAmount si closeOrder.status === 'closed')

return Math.max(0, relativeAmount) // Ne jamais retourner négatif

🧪 Tests

Tests Frontend

Les tests frontend vérifient l’affichage et l’interaction avec les positions en RUNNING. Fichiers de tests : features/positions/running-position.spec.ts Configuration Checkly :
// @checkly frequency: 10min
// @checkly locations: paris, new-york
// @checkly alertChannels: linear
// @checkly tags: production, positions

Tests Backend

Les tests backend valident la logique de gestion des ordres en RUNNING et les variantes de workflow. Fichiers de tests : packages/functions/src/tests/positions/running-state.test.ts Exécution :
npm run test -- running-state

🔗 Navigation