Détection de Wedge — Algorithme, Dédup, Confidence

Comment Python détecte les wedges, pourquoi la dédup se fait par start_time, et ce que mesure vraiment la confidence.

wedge patterns market-structure dedup tnt-v2 Updated 2026-10-04 · content/wiki/tnt-stack/wedge-detection.md

Détection de Wedge — Algorithme, Dédup, Confidence

Le wedge est un pattern de convergence de deux trendlines (highs et lows) qui annonce souvent un breakout imminent. Ce document explique comment TNT le détecte côté Python, comment la couche PHP maintient un seul enregistrement par wedge malgré l'évolution continue des trendlines, et ce que représente vraiment le score confidence.

Algorithme de détection

Le détecteur vit dans python/src/tnt_python/indicators/market_structure/market_structure_v2.py et s'appuie sur les pivots (swing highs/lows) déjà extraits par le SwingDetector. Il travaille en deux étapes séquentielles.

Étape 1 — _detect_wedge() : identifier un wedge en formation

  • Prend les 4 derniers pivots highs et les 4 derniers pivots lows.
  • Fitte deux droites par régression linéaire (np.polyfit) : une sur les highs, une sur les lows.
  • Teste la convergence des pentes :
    • Descending wedge (bullish breakout) : h_slope < 0 && l_slope < 0 && h_slope < l_slope — les highs descendent plus vite que les lows, les deux lignes convergent vers le haut.
    • Ascending wedge (bearish breakout) : h_slope > 0 && l_slope > 0 && l_slope > h_slope — les lows montent plus vite que les highs, les deux lignes convergent vers le bas.
  • Si aucune convergence valide, retourne None.
if h_slope < 0 and l_slope < 0 and h_slope < l_slope:
    strength = min(abs(h_slope - l_slope) / (abs(h_slope) + 1e-9), 1.0)
    return WedgePattern(
        wedge_type="descending_wedge",
        bias="bullish_breakout",
        strength=round(float(strength), 3),
        ...
    )

Étape 2 — _detect_wedge_breakout() : chercher la bougie de breakout

  • Part du bar wedge.end_bar + 1 (premier bar possible pour un breakout).
  • Scanne jusqu'à la bougie courante, cappé à max_bars_since_breakout = 15 bars pour éviter de surfacer des breakouts anciens.
  • Première bougie dont la close dépasse la trendline projetée (slope × bar_idx + intercept) avec une tolérance de ±0.3% (0.997 pour bull, 1.003 pour bear) → c'est la bougie de breakout.

Important : la détection utilise la trendline projetée dans le futur, pas la confirmation d'un nouveau pivot. Résultat : le breakout fire dès la close de la bougie qui casse, sans attendre.

Payload envoyé à Laravel

python/src/tnt_python/cli/compute_context.py sérialise le wedge en dict :

patterns.append({
    "type":              "wedge",
    "direction":         direction,          # "bull" | "bear"
    "status":            "confirmed" if breakout_type else "active",
    "confidence":        round(w.strength, 4),
    "breakout_price":    brk_price,
    "invalidation_price": inv_price,
    "start_time":        start_time,          # 1er pivot — clé de dédup
    "end_time":          end_time,
    "payload_json": {
        "wedge_type":        w.wedge_type,
        "bias":              w.bias,
        "breakout_type":     breakout_type,
        "breakout_time":     brk_time,        # timestamp exact de la bougie breakout
        "upper_line":        [...],
        "lower_line":        [...],
    },
})

Mécanisme de dédup

Un wedge évolue en continu entre chaque run du cron context : les trendlines s'ajustent à chaque nouveau pivot, le end_bar avance, les prix bougent légèrement. Il faut donc une clé stable pour reconnaître « c'est le même wedge que la dernière fois ».

Le code utilise une stratégie dédiée aux wedges, différente de celle des autres patterns. Elle vit dans app/Repositories/MarketContextRepository.php:490 :

private function findWedgeByStartTime(
    int $coinId, string $timeframe, string $direction, string $startTime
): ?MarketPattern {
    return MarketPattern::where('coin_id', $coinId)
        ->where('timeframe', $timeframe)
        ->where('pattern_type', 'wedge')
        ->where('direction', $direction)
        ->where('start_time', $startTime)
        ->whereIn('status', ['active', 'confirmed'])
        ->first();
}

Clé de dédup = (coin_id, timeframe, pattern_type='wedge', direction, start_time)

Le start_time est le timestamp absolu UTC du premier pivot du wedge. C'est stable parce que :

  • Les pivots sont des points historiques déjà confirmés — ils ne bougent pas rétroactivement.
  • Le start_time est un timestamp absolu, pas un bar_index (qui lui se décale à chaque run avec la rolling window).

Donc tant qu'un wedge garde son premier pivot, c'est le même pattern en DB : on UPDATE les trendlines, le end_bar, le confidence_latest, last_seen_at, et on bump seen_count++.

Quand est-ce qu'on crée un nouveau wedge ?

  • Le premier pivot sort du scope (ex : un pivot plus ancien apparaît et devient le nouveau début, ou le pattern se re-forme sur une base différente).
  • Changement de direction (bull → bear ou inverse).

Pourquoi tu peux voir plusieurs wedges actifs sur un même TF

Plusieurs wedges se sont formés successivement, chacun avec un start_time distinct → chacun est une entrée séparée. La dédup ne les fusionne pas (ce ne sont pas « le même wedge » dans le sens où leur point de départ diffère).

Nuance : les autres patterns utilisent d'autres clés

  • Zones S&D (demand_zone, supply_zone) → dédup par overlap de prix ≥ 70%.
  • BOS, swings → dédup par pattern_key = hash de (type, direction, prix arrondis à ±0.5%).
  • Balance → singleton par (coin, timeframe) — une seule balance active à la fois.

Le wedge est le seul à utiliser start_time comme clé.

Confidence : ce que ça mesure vraiment

Pour un wedge, confidence = wedge.strength calculé dans _detect_wedge() :

# Descending (bullish)
strength = min(abs(h_slope - l_slope) / (abs(h_slope) + 1e-9), 1.0)

# Ascending (bearish)
strength = min(abs(l_slope - h_slope) / (abs(l_slope) + 1e-9), 1.0)

C'est le ratio de différence entre les pentes des deux trendlines, normalisé par la pente dominante :

  • Proche de 1.0 → les deux droites convergent fortement (fort différentiel de pente). Wedge « serré », compression forte, breakout imminent plus probable.
  • Proche de 0.0 → les deux droites sont quasi parallèles. C'est plus un channel qu'un wedge : convergence très lente, breakout lointain ou incertain.

Deux colonnes en DB : confidence vs confidence_latest

Le repository (MarketContextRepository.php:310-311) écrit deux valeurs à chaque update :

'confidence_latest'  => $confidence,                                  // valeur du dernier run
'confidence'         => max((float) ($existing->confidence ?? 0), $confidence),  // max historique
  • confidence → le max historique vu sur ce wedge. Reflète la meilleure forme que ce wedge a jamais eue.
  • confidence_latest → la valeur du dernier run. Reflète l'état instantané.

Un wedge peut avoir confidence = 0.85 mais confidence_latest = 0.42 : il était bien formé à un moment, puis s'est aplati. C'est volontaire — la colonne confidence sert au ranking/sélection, confidence_latest au monitoring de dégradation.

Attention au filtre côté chart

Le filtre appliqué sur la page /market-context/{SYMBOL} (MarketContextController@show:165) utilise la colonne confidence :

$chartPatterns = $patterns
    ->filter(fn ($p) => !($p->pattern_type === 'wedge' && ($p->confidence ?? 1.0) < 0.60))

Donc le filtre regarde le max historique. Un wedge qui s'est dégradé reste visible sur le chart tant qu'il a atteint ≥ 0.60 au moins une fois dans sa vie. Si on voulait filtrer sur l'état courant, il faudrait utiliser confidence_latest.

Diagnostic rapide

Pour inspecter un wedge en DB :

ddev artisan tinker
> App\Models\MarketPattern::find($id)
    ->only(['status','confirmed_at','confidence','confidence_latest',
            'breakout_price','payload_json','start_time','last_seen_at','seen_count'])
  • status = 'confirmed' + breakout_price != null → breakout détecté.
  • payload_json->breakout_time → timestamp exact de la bougie de breakout (plus précis que confirmed_at qui est l'instant d'écriture Laravel).
  • seen_count élevé + last_seen_at récent → wedge toujours tracké par Python à chaque run.

Fichiers de référence

  • python/src/tnt_python/indicators/market_structure/market_structure_v2.py — détecteur Python
  • python/src/tnt_python/cli/compute_context.py — sérialisation du payload
  • app/Repositories/MarketContextRepository.php — upsert + dédup
  • app/Http/Controllers/MarketContextController.php — filtres d'affichage
  • database/migrations/2026_05_08_000001_create_market_patterns_table.php — schéma DB