# Audit & Corrections — Framework crypto_trading_bot
**Date:** 17 Mai 2026  
**Périmètre:** Testnet Binance (USDT) — `market_spy.py` + couche ML + processus démons  
**Rédigé par:** GitHub Copilot (Claude Sonnet 4.6) — **Soumis à validation Claude Opus 4.7**

---

## 1. Contexte du Framework

### Architecture générale
```
market_spy.py (4 403 lignes) — processus principal
├── market_context.py (624 L) — Couche7 : classifieur de régime marché (ML XGBoost)
├── market_behavior.py (448 L) — comportement FTR/IRR/surge_ft → CONVICTION/SPECULATION/BEAR
├── spy_optimizer/
│   ├── signal_classifier.py (694 L) — LightGBM + XGBoost + LSTM optionnel
│   └── models/signal_classifier.pkl — modèle sérialisé (dict)
├── config.py — paramètres globaux
├── api/routes.py — dashboard Flask
└── auto_updater.py — retraining ML quotidien
```

### Paramètres clés (testnet, état actuel)
| Paramètre | Valeur |
|---|---|
| `TESTNET_MODE` | `True` |
| `SURGE_MIN_PRICE_CHANGE` | 1.0% (hausse min scan-to-scan) |
| `SURGE_MIN_PRICE_CHANGE_2` | 1.1% (confirmation sur 2 scans) |
| `SURGE_MIN_VOLUME_RATIO` | 3.0× |
| `HARD_STOP_LOSS_PCT` | 1.2% |
| `MAX_HOLD_MINUTES` | 12 min |
| `SPY_MAX_POSITIONS` | 10 (configurable via `bot_settings.json`) |
| Position size | 900 USDC |
| Trailing | 5 paliers (≥1% → garde 1.0%, ≥2% → 1.4%, ≥3% → 2.0%, ≥5% → 2.8%, ≥8% → 3.5%) |
| Scan interval | 7 secondes |
| Max trades/heure | 6 |
| Patterns actifs | `FLASH_SURGE`, `BREAKOUT_SURGE` |
| Patterns désactivés | `MOMENTUM_SURGE`, `TREND_MOMENTUM_SURGE` (depuis 02/05) |

### Performance historique (testnet)
- **Total** : 1 392 trades, WR 51.1%, +10 459 USDT (18 Mars → 14 Mai 2026)
- **Mai 2026** (exécutions réelles testnet) : 12 trades, WR 83.3%, +136.76 USDT
- **Prod réelle** (16/04→07/05) : 114 trades, WR 32.5%, −6.74 USDT → **arrêt sur slippage**

---

## 2. Problèmes Identifiés

### P1 — CRITIQUE : Fichiers PID pointaient vers des processus morts
**Gravité :** Critique  
**Description :** `spy_testnet.pid` (3 479 391) et `market_spy.pid` (3 961 468) référençaient des PID inexistants. Le vrai processus tournait en PID 654 356.  
**Impact :** Si l'API dashboard déclenche "Apply ML Optimizations" (`api/routes.py` ligne 7923), elle envoie `kill` au PID lu dans `spy_testnet.pid`, puis relance `market_spy.py`. Avec un PID mort stocké, cela aurait lancé un **second processus en parallèle** du processus existant — double trading, double positions, état corrompu.  
**Correction appliquée :** PID files mis à jour vers 654 356 (puis 1 882 986 après restart).  
**Statut :** ✅ Corrigé

---

### P2 — CRITIQUE : Production (argent réel) utilise une ancienne version du code
**Gravité :** Critique  
**Description :** `diff --unified=0 market_spy.py /home/ubuntu/crypto_trading_prod/market_spy.py` révèle **58 hunks de diff, 303 lignes absentes** de la production. La prod utilise un `market_spy.py` vieux de plusieurs semaines, sans :
- Trailing 5 paliers (était 3 paliers)
- `INSTANT_REVERSAL_EARLY` (sortie à 15-30s si max_pnl<0.05% et pnl<-0.35%)
- `execution_logger` (diagnostic slippage)
- Filtres entrée améliorés Mai 2026
- OSMOUSDC dans `FLASH_SURGE_STRICT`
- Toutes les optimisations du 29/04 au 17/05

**Impact :** La prod perd de l'argent sur des patterns qui ont été corrigés en testnet (ex: INSTANT_REVERSAL non intercepté précocement).  
**Correction appliquée :** Aucune — **la production ne doit pas être redémarrée** tant que les LIMIT orders ne sont pas implémentés et que le slippage mesuré reste >0.3% (mesure sur 39 paires testnet/prod identiques : slippage moyen +0.535%).  
**Statut :** ⚠️ Non corrigé — en attente des prérequis (LIMIT orders + 100 trades execution_logger + slippage <0.3%)

> **Question pour validation :** Est-il pertinent de déployer sélectivement les corrections non-liées au slippage (ex: OSMOUSDC, trailing amélioré) sans attendre les LIMIT orders ?

---

### P3 — INFO : Zéro trade depuis 72h+
**Gravité :** Informatif (non-bug)  
**Description :** Aucun trade exécuté depuis ~72h. Les filtres d'entrée (`ENTRY_FILTER`) bloqueaient tous les signaux.  
**Analyse :** Régime marché CORRECTION confirmé par `market_behavior.py` (surge_ft_rate élevé, IRR élevé). Les filtres fonctionnent comme prévu — ne pas forcer des trades en marché baissier.  
**Statut :** ✅ Non-bug confirmé — pas d'action requise

---

### P4 — IMPORTANT : OSMOUSDC — 15+ faux pumps en 72h, pattern PUMP_TERMINAL
**Gravité :** Importante  
**Description :** OSMOUSDC a généré 15+ déclenchements de `FLASH_SURGE` à 1.0-1.5% de hausse suivis d'un retrace immédiat. 1 trade exécuté → `INSTANT_REVERSAL`, PnL −2.62 USDT. Couche7 (`market_context.py`) classifie OSMOUSDC comme `PUMP_TERMINAL` avec 85% de confiance.  
**Cause racine :** OSMO avait un seuil `FLASH_SURGE` standard de 1.0%. Les faux pumps restaient systématiquement sous 2.0%, donc qualifiaient pour entrée sans jamais avoir de follow-through.  
**Correction appliquée :** OSMOUSDC ajouté dans `FLASH_SURGE_STRICT` avec seuil 2.0% :
```python
# market_spy.py, ligne 221
FLASH_SURGE_STRICT = {
    'NOMUSDC':         2.0,
    '0GUSDC':          2.0,
    'GIGGLEUSDC':      2.5,
    'BROCCOLI714USDC': 2.0,
    'HIVEUSDC':        2.0,
    'OSMOUSDC':        2.0,  # 🔧 17/05: INSTANT_REVERSAL 1T -2.62U + 15+ faux pumps 72h — PUMP_TERMINAL c7=85%
}
```
**Vérification :** Log de démarrage confirme : `• Flash strict : NOMUSDC, 0GUSDC, GIGGLEUSDC, BROCCOLI714USDC, HIVEUSDC, OSMOUSDC, ...`  
**Statut :** ✅ Corrigé et actif

> **Question pour validation :** Le seuil 2.0% est-il adapté pour OSMO, ou faudrait-il 2.5% (même seuil que GIGGLE) compte tenu de sa volatilité intrinsèque plus faible ?

---

### P5 — MODÉRÉ : `market_behavior_state.json` périmé (sauvegardé le 14 Mai)
**Gravité :** Modérée  
**Description :** Le fichier état de `market_behavior.py` indiquait régime SPECULATION avec `last_update` du 14 Mai, alors que le comportement réel (FTR=100%, IRR=0%) correspond à CONVICTION. Le régime bloqué en SPECULATION rend les filtres FLASH_SURGE_STRICT plus stricts sans raison (buy_ratio requis 75% au lieu de 65%).  
**Cause racine :** Redémarrage du processus avec état stale non recalculé.  
**Mitigation existante :** `_load_state()` appelle `_recalculate()` au démarrage avec `regime_consecutive=REGIME_STABILITY_COUNT` → force la réévaluation. Confirmé dans le log post-restart : `🧠 🟢 Comportement: CONVICTION (FTR=100% IRR=0% surge_FT=25% n=30 depuis 0min)`.  
**Statut :** ✅ Auto-corrigé au restart — mécanisme de protection validé

> **Question pour validation :** Faudrait-il invalider `market_behavior_state.json` au démarrage si `last_update` est > 6h, plutôt que de recalculer à partir de l'état stale ?

---

### P6 — INFO : Couche7 en shadow mode (ne peut pas bloquer)
**Gravité :** Informatif  
**Description :** `market_context.py` : `COUCHE7_ENABLED=False`, `COUCHE7_SHADOW_MODE=True`. Couche7 classe les patterns (ex: PUMP_TERMINAL OSMOUSDC à 85%) mais ne peut rien bloquer.  
**Impact :** La classification Couche7 sert uniquement de diagnostic. En cas de faux pump sur un coin non encore dans `FLASH_SURGE_STRICT`, Couche7 voit le problème mais ne peut pas l'empêcher automatiquement.  
**Décision :** Laissé en shadow mode intentionnellement — nécessite A/B testing avant activation complète.  
**Statut :** ⚠️ Connu et intentionnel

> **Question pour validation :** À quel seuil de confiance Couche7 devrait-il être activé ? Proposition : activer avec `COUCHE7_ENABLED=True` uniquement pour les patterns `PUMP_TERMINAL` (confiance ≥80%), tout en gardant `SHADOW_MODE=True` pour les autres régimes.

---

### P7 — IMPORTANT : Seuil ML sub-random autorisé (0.43 < 0.50)
**Gravité :** Importante  
**Description :** Le modèle ML (`signal_classifier.pkl`) avait `optimal_threshold=0.43`. Un seuil < 0.50 signifie que le modèle considère un trade comme "BUY" même quand il prédit plus de probabilité de perte que de gain — comportement sub-random par construction.

**Contexte du modèle :**
```
Entraîné le : 2026-04-30T17:01:52
Échantillons : 32 883 (train) + 10 961 (val)
Architecture : LightGBM + XGBoost ensemble, LSTM optionnel
AUC-ROC : 0.6104
Métriques à seuil 0.43 :
  - Accuracy : 55.5%
  - Precision : 49.1% (WR des trades passés = 49.1% — sous les 50%)
  - Recall : 87.8%
  - PnL filtré : +446.5 USDT vs −494.5 USDT sans filtre
  - Taux de passage : 77% des trades (trop permissif)
  - Mauvais trades filtrés : 31.1%
  - Bons trades gardés : 87.8%
```

**Cause racine :** `_optimize_threshold()` dans `signal_classifier.py` cherchait le seuil maximisant le profit sur validation, avec plage `np.arange(0.30, 0.75, 0.01)`. Le minimum de 0.30 permettait la sélection de seuils statistiquement absurdes (le modèle dit "60% chance d'échouer" → BUY quand même).

**Corrections appliquées :**
1. **`spy_optimizer/signal_classifier.py` ligne 395** — plancher d'optimisation relevé :
   ```python
   # Avant :
   for threshold in np.arange(0.30, 0.75, 0.01):
   # Après :
   for threshold in np.arange(0.45, 0.75, 0.01):  # 🔧 17/05: plancher 0.30→0.45 (sub-random interdit)
   ```
2. **`spy_optimizer/models/signal_classifier.pkl`** — seuil actif mis à jour :
   ```
   Avant : optimal_threshold = 0.4300
   Après : optimal_threshold = 0.5000
   Backup : signal_classifier.pkl.bak_20260517
   ```
3. **Vérification log :** `🤖 ML Classifier chargé (seuil=0.50, AUC=0.6104)`

**Statut :** ✅ Corrigé — seuil 0.50 actif, plancher de retraining à 0.45

> **Question pour validation :** Le seuil 0.45 comme plancher minimum est-il suffisant ? Argument pour 0.50 dur : toute prédiction <0.50 est par définition "le modèle pense que ce trade va échouer". Argument pour 0.45 : le modèle est calibré sur données déséquilibrées (WR historique ~51%), un seuil de 0.45 laisse une marge d'imprécision de calibration tout en restant en zone "plus probable réussite que échec" après correction de biais.

---

### P8 — MINEUR : Code mort dans `config.py`
**Gravité :** Mineure  
**Description :** `config.py` lignes 127-134 définit `TRAILING_STOP_DISTANCE = 1.5` et `TRAILING_STOP_ACTIVATION = 2.5` avec un commentaire `# non utilisé — voir TRAILING_TIERS`. Ces variables sont référencées dans la validation config (`config.py` ligne 437) mais jamais lues par `market_spy.py`.  
**Impact :** Confusion de maintenance — un développeur peut croire que `TRAILING_STOP_DISTANCE` contrôle quelque chose.  
**Correction appliquée :** Aucune — trop mineur, risque de casser la validation config.  
**Statut :** ⚠️ Connu — à nettoyer lors d'une refactorisation config

---

### P9 — INFO : `signal_aggregator` mort
**Gravité :** Informatif  
**Description :** Le module `signal_aggregator` (utilisé par `trading_bot.py`) ne tourne pas. `trading_bot.py` est désactivé depuis plusieurs semaines.  
**Impact :** Aucun — `market_spy.py` n'utilise pas `trading_bot.py`.  
**Statut :** ✅ Non-bug — architecture intentionnelle

---

### P10 — INFO : `espion_trades.json` vide
**Gravité :** Informatif  
**Description :** `espion_trades.json` contient `{}` (dict vide). Ce fichier devrait contenir l'historique des trades détectés par le spy pour l'analyse post-session.  
**Impact :** Perte de données pour analyse rétrospective, mais n'affecte pas le trading en temps réel.  
**Statut :** ⚠️ À investiguer — possible reset accidentel ou logique d'écriture défaillante

---

## 3. Résumé des Corrections Appliquées

| # | Fichier | Changement | Impact |
|---|---|---|---|
| C1 | `spy_testnet.pid` | PID mort → PID réel 654 356 → 1 882 986 | Empêche double-processus sur "Apply ML" API |
| C2 | `market_spy.pid` | PID mort → PID réel 654 356 → 1 882 986 | Cohérence PID management |
| C3 | `market_spy.py` L221 | OSMOUSDC → FLASH_SURGE_STRICT à 2.0% | Élimine 15+ faux pumps/72h, bloque PUMP_TERMINAL |
| C4 | `spy_optimizer/signal_classifier.py` L395 | Plancher optimisation threshold : 0.30 → 0.45 | Interdit sub-random sur retrainings futurs |
| C5 | `spy_optimizer/models/signal_classifier.pkl` | optimal_threshold : 0.43 → 0.50 | Seuil ML cohérent statistiquement (actif immédiatement) |

---

## 4. Points de Validation Demandés à Claude Opus 4.7

### 4.1 Correction C3 — OSMOUSDC dans FLASH_SURGE_STRICT

**Code modifié (`market_spy.py`, début du dict) :**
```python
FLASH_SURGE_STRICT = {
    'NOMUSDC':         2.0,  # 29/04: 5T 100%WR +43USDT testnet
    '0GUSDC':          2.0,  # 29/04: 1T 100%WR +86USDT testnet
    'GIGGLEUSDC':      2.5,  # 15/04: 13T 54%WR +12USDT — spike très court
    'BROCCOLI714USDC': 2.0,  # 15/04: 7T 43%WR +22USDT
    'HIVEUSDC':        2.0,  # 05/05: fast-spiker récurrent
    'OSMOUSDC':        2.0,  # 17/05: INSTANT_REVERSAL 1T -2.62U + 15+ faux pumps 72h
}
```

**Logique de déclenchement FLASH_SURGE_STRICT dans `market_spy.py` (extrait) :**
```python
if surge_type == 'FLASH_SURGE' and symbol in FLASH_SURGE_STRICT:
    _strict_min = FLASH_SURGE_STRICT[symbol]
    if price_change_pct < _strict_min:
        # Surge trop faible pour ce coin → skip
        return False, f"FLASH_STRICT seuil={_strict_min}% (actual={price_change_pct:.2f}%)"
```

**Question :** Le seuil 2.0% pour OSMOUSDC est-il approprié, ou devrait-il être calibré sur la volatilité ATR du coin ?

---

### 4.2 Correction C5 — Seuil ML : validité statistique

**Métriques du modèle entraîné (seuil d'entraînement original : 0.43) :**
```
AUC-ROC          : 0.6104
Precision@0.43   : 49.1% (WR prédit → PIRE que random sur ce critère)
Precision@0.50   : non recalculé (pkl modifié directement)
PnL avec filtre  : +446.5 USDT
PnL sans filtre  : −494.5 USDT
Mauvais filtrés  : 31.1%
Taux de passage  : 77.0% (très permissif)
```

**Problème :** Avec `optimal_threshold=0.43`, le filtre ML laisse passer des trades sur lesquels le modèle prédit une probabilité de perte > 57% (P(loss)=0.57, P(win)=0.43). Cela signifie que le ML **aggrave** activement la sélection par rapport à un filtre aléatoire sur ces trades.

**Question :** En fixant le seuil à 0.50, on va filtrer davantage de trades (taux de passage ~77% → estimation <60%). Avec AUC=0.61, est-ce que le modèle a suffisamment de discrimination pour justifier un seuil strict, ou risque-t-on de filtrer des bons trades sur du bruit ?

**Question complémentaire :** Faudrait-il recalibrer le modèle avec un label différent (ex: target = pnl > 0.3% au lieu de pnl > 0) pour avoir une precision@0.50 > 55% ?

---

### 4.3 Architecture ML — Cohérence de la boucle d'optimisation

**Flux actuel :**
```
auto_updater.py (daily)
    → signal_classifier.py:train()
    → _optimize_threshold(np.arange(0.45, 0.75, 0.01))  # ← plancher fixé 17/05
    → sauvegarde pkl avec optimal_threshold
    → market_spy.py recharge pkl au prochain restart ou via API
```

**Problème identifié :** Le retraining quotidien utilise `pnl_with_filter` comme métrique d'optimisation. Or `pnl_with_filter` est calculé sur données de validation (holdout set). Si le marché change de régime entre l'entraînement et le déploiement (ex: de CONVICTION à CORRECTION), le threshold optimal historique peut devenir sous-optimal.

**Question :** Faudrait-il ajouter une contrainte supplémentaire : `precision >= 0.52` (WR minimum acceptable) en plus de la maximisation du PnL, pour éviter de sélectionner un seuil qui maximise le PnL absolu en volume mais avec un mauvais WR ?

---

### 4.4 Couche7 — Activation partielle

**État actuel :**
```python
# market_context.py
COUCHE7_ENABLED = False
COUCHE7_SHADOW_MODE = True
```

**Observation :** Couche7 a correctement identifié OSMOUSDC comme `PUMP_TERMINAL` à 85% de confiance **avant** que nous ajoutions le coin à `FLASH_SURGE_STRICT` manuellement. Cela suggère que Couche7 a une valeur prédictive réelle sur les patterns terminaux.

**Proposition :** Activer Couche7 uniquement pour la classe `PUMP_TERMINAL` avec confiance ≥80% :
```python
# Proposition (non implémentée)
if COUCHE7_SHADOW_MODE and couche7_result.get('pattern') == 'PUMP_TERMINAL':
    if couche7_result.get('confidence', 0) >= 0.80:
        return False, f"C7 PUMP_TERMINAL (conf={couche7_result['confidence']:.0%})"
```

**Question :** Cette activation partielle est-elle logiquement saine ? Y a-t-il un risque de faux positifs sur `PUMP_TERMINAL` (bloquer des vrais breakouts classifiés à tort comme terminaux) ?

---

### 4.5 Production vs Testnet — Stratégie de synchronisation

**État prod :** `market_spy.py` version ancienne, 58 hunks de diff, 303 lignes manquantes.

**Prérequis établis pour mise à jour prod :**
1. LIMIT orders implémentés (achat à cours limité, pas market)
2. `execution_logger` avec 100+ trades réels mesurant le slippage
3. Slippage moyen mesuré < 0.3% (actuellement : +0.535% sur 39 paires)

**Question :** Certaines corrections (OSMOUSDC FLASH_SURGE_STRICT, trailing 5 paliers) ne dépendent pas du slippage. Est-il risqué de les déployer en prod sans les prérequis LIMIT orders, ou le slippage reste-t-il le facteur bloquant principal quelle que soit la correction ?

**Contexte prod :** 17/17 INSTANT_REVERSAL prod avaient `max_pnl=0%` → achats systématiquement au peak. Sans LIMIT orders, améliorer les filtres d'entrée n'empêche pas le slippage d'acheter au sommet.

---

## 5. Optimisations Non Implémentées Identifiées

### OPT-1 : Invalidation `market_behavior_state.json` si stale > 6h
**Motivation :** Si le spy redémarre après 6h+ d'arrêt, l'état stale peut faire démarrer en SPECULATION alors que les métriques actuelles qualifient CONVICTION. Le mécanisme `_recalculate()` corrige cela, mais avec les données trade en mémoire depuis le dernier run — pas les 6h d'inactivité.  
**Proposition :** Ajouter dans `_load_state()` :
```python
if time.time() - state.get('last_update', 0) > 6 * 3600:
    logger.info("[BEHAVIOR] État stale > 6h → reset vers CONVICTION par défaut")
    return self._default_state()
```

### OPT-2 : Activation conditionnelle Couche7 pour PUMP_TERMINAL
Voir §4.4 ci-dessus.

### OPT-3 : Recalibration label ML (target = pnl > 0.3% au lieu de pnl > 0)
**Motivation :** Avec un label `pnl > 0`, le modèle apprend à distinguer +0.001% de −0.001%, ce qui inclut du bruit de frais et spread. Un label `pnl > 0.3%` (= trade réellement rentable après frais ~0.1%) donnerait un signal plus propre.

### OPT-4 : Ajout contrainte `precision >= 0.52` dans `_optimize_threshold()`
Voir §4.3 ci-dessus.

### OPT-5 : Nettoyage code mort `config.py`
Supprimer `TRAILING_STOP_DISTANCE` et `TRAILING_STOP_ACTIVATION` et leurs références dans la validation config.

### OPT-6 : Investiguer `espion_trades.json` vide
Identifier si le fichier est réinitialisé à chaque démarrage (bug) ou si la logique d'écriture est cassée.

---

## 6. État du Système Post-Corrections (17/05/2026 12:05)

```
Processus testnet : PID 1882986 (redémarré 12:04:49)
Processus prod    : PID 654382 (non touché — /home/ubuntu/crypto_trading_prod/)
Positions actives : 0
Régime marché     : CONVICTION (FTR=100%, IRR=0%, surge_FT=25%, n=30)
ML Classifier     : seuil=0.50, AUC=0.6104 (actif)
Flash strict      : NOMUSDC, 0GUSDC, GIGGLEUSDC, BROCCOLI714USDC, HIVEUSDC, OSMOUSDC, +dynamiques
LSTM training     : accuracy=56.7% train, 66.0% val (CPU, 15 000 échantillons, 17/05 10:02)
```

---

*Document généré pour audit externe — toutes les corrections C1-C5 sont en production testnet depuis 12:04:49 UTC+2 le 17/05/2026.*
