Skip to main content

État RUNNING - Gestion des Ordres

📊 Statut des Tests

Tests Frontend (Checkly)

Tests Backend (Vitest)


🎯 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()
  • 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

TP1 (processTp1)

Conditions de création/remplacement :
  • tp1Order.status !== 'closed'
  • tp1Order n’existe pas OU montant différent OU annulé
Recalcul des montants TP après BUY2 fermé :
  • Si buy2Order.status === 'closed' ET tp1Order.status !== 'closed' :
    • Recalcul de tp1Amount = relativeAmount * 0.7 (arrondi selon précision exchange)
    • Recalcul de tp2Amount = relativeAmount - tp1Amount (après arrondi de TP1)
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
  • 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

💰 É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 toujours basées sur relativeAmount :
// Quantités fixes basées sur le total
tp1Amount = relativeAmount * 0.7;  // 70% du total
tp2Amount = relativeAmount * 0.3;  // 30% du total

🦈 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)
  • 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
  • Nouveaux TP1/TP2 créés avec montants recalculés

Variante 4 : BUY2 fermé avant TP1/TP2

BUY1 → BUY2 fermé → TP1 fermé → TP2 fermé → CLOSE
Vérifications:
  • relativeEntryPrice recalculé (moyenne pondérée)
  • Nouveaux TP1/TP2 avec montants basés sur relativeAmount
  • 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