Skip to content

Runbook opérateur — Diagnostic s43 (economic tradability, CVN-N001-EI-S05)

Manuel d'utilisation du diagnostic s43 : déclencher, lire le verdict, agir. Réfs : plan dossier r2.5 · architecture r1.1 · committee plan_review c6a789fa PASSED. Public : opérateur. Nature : diagnostic read-only — ne déploie rien, ne promeut rien (ADR-2).


1. Quoi & quand le lancer

s43 répond à une question : « le signal ML est-il tradable, net de frais, à un rythme déployable — pour LightGBM, XGBoost et CatBoost ? ». Il généralise le statut C de l'Epic (constat « non tradable » déjà établi sur CatBoost) aux deux autres familles.

Quand : après le merge + deploy de S05 (Block 4), une fois les prérequis §2 satisfaits. Run unique opérateur (schedule=None, ADR-18).

2. Prérequis (à vérifier AVANT le trigger)

Prérequis Vérification Si KO
Clés coût PG créées + valeurs réelles confirmées Console → ftf_config contient CVN_COST_FEES_BPS, CVN_COST_SLIPPAGE_BPS, CVN_COST_FUNDING_BPS_H4 (par leg, bps). ⚠️ Confirmer que ce sont tes chiffres réels mesurés (palier taker courant, slippage observé à ta taille sur fold-3, funding amorti — cf. §8), pas des placeholders. Un coût faux = un verdict faux. les créer/corriger en Console (ADR-59). Sans clé : event=s43_io_cost_resolve_failedINCONCLUSIVE_TOOLING au dry-run
Pin S07 disponible run S07 antérieur sur defi_top5/fold_id=3 use_pin=false → cold-capture (~3h/cellule, parallèle)
Deploy à jour Deploy to Scaleway K8s SUCCESS sur le SHA qui contient s43 attendre le deploy (~10 min post-merge)
DAG synchronisé diagnostic__s43 visible dans l'UI Airflow + bannière build=<sha> (ADR-92) attendre la sync DAG-repo (~7 min)

⚠️ Gate coût — smoke OK, full run GATED tant que les 3 valeurs ne sont pas MESURÉES. Les coûts sont 3 mesures que toi seul peux sourcer : fees (palier taker exact du relevé de compte au volume courant ; sanity ~2-10 bps), slippage per-leg à ta taille de position réelle sur les 5 cryptos (per-crypto si OPUSDC moins liquide), funding amorti H4 = funding_8h × 0.5 × hold_eff/4h mesuré sur fold-3. Piège du signe du funding : en régime de carry positif (longs defi), funding_8h ≤ 0 → il améliore le net-EV. Un funding mis à +1 alors qu'il est réellement négatif biaise le verdict vers NOT_TRADEABLE à tort. Pourquoi le gate : la sensibilité ±50% (§5.1) attrape les petites erreurs (fees 6 au lieu de 5) ; elle n'attrape PAS une erreur systématique (funding au mauvais signe, slippage sous-estimé ×3, palier taker faux) → un full run sur placeholders décide le statut C de l'Epic sur des coûts inventés. smoke dry-run (machinerie : câblage, Hamilton compile, no-crash) peut tourner sur placeholders. full run décisionnel : interdit tant que fees/slippage/funding ne sont pas tes mesures réelles saisies en Console.

2bis. Checklist de validité r3 (b) — la BARRIÈRE avant le run décisionnel

r3 reconstruit s43 sous la construction (b) : le diagnostic rejoue la distribution de sélection de la HPO prod (tirages MLflow par crypto) sur le fold-3 pinné, et demande « l'edge survit-il à l'instabilité de sélection ? » (gate cross-tirage). Cette machinerie passe tous les tests mockés ; sa validité bascule sur des invariants que seul l'in-pod confirme (leçon S03 : le mock vert ≠ le chemin porteur fidèle).

Résolus read-only le 2026-06-05 (contre le live mlflow.cvntrade.eu) : S1 schéma (corrigé : exp CVNTrade_HPO, filtres sur params + model_id LIKE), C1 complétude (LGB 9/9, CB 4/4 — zéro substitution), C2 CB l2_leaf_reg (nom natif → fidèle). XGBoost EXCLU (fiche décision — par-trial, pas par-cycle). Restent C3-C6 (config) + S2 (inspection artefact in-pod).

Deux classes — ne pas les traiter au même moment :

Classe 1 — STRUCTURE (read-only, à confirmer EN AMONT du merge)

Si ces deux ratent, la structure est fausse — pas le run mal-paramétré. Read-only, de-risquables avec l'accès pod existant AVANT de bâtir le merge dessus (la leçon S03 : le read-only in-pod attrape ce que la revue ne voit pas).

# Invariant Comment Si KO
S1 Schéma tags MLflow = run_type=hpo / model_id / coin_pair, params best_-préfixés. L'énum (s43_regime.list_hpo_draws) est bâtie dessus. read-only pur : kubectl port-forward svc/mlflow 5001:5000 -n <ns> puis la requête search_runs(filter="tags.run_type='hpo' AND tags.model_id='LGB' AND tags.coin_pair='UNIUSDC'") → vérifier N>0 + best_-params présents énum vide → famille tooling ; schéma différent = re-câbler le filtre AVANT merge
S2 Parquets fold-3 self-describing (classe S03). Chemin code CONFIRMÉ sain : _load_captured_parquet sélectionne par nom ([c for c in df.columns if not c.startswith('_')]) et le DAG lit feature_names du même fichier → ordre identique garanti, features mal-nommées non-atteignables. résidu in-pod = inspection d'artefact : captured-fold-<crypto>-3.parquet × 5 portent de vrais noms (pas positionnels f0..) + sont bien fold-3 (pas un capture parallèle réintroduisant le bug) re-capturer (skip_phase_a=false)

Classe 2 — CONFIG (paramètrent le run — phase checklist, après merge)

# Invariant Comment Si KO
C1 Complétude best_params : non-loggés = cherchés (→ strict_params = exclure) ou fixes-stables (→ event=s43_param_substituted suffit) ? Échantillon représentatif (pas 2-3) si les search-spaces ont varié grep event=s43_param_substituted au dry-run vs params loggés des runs strict_params incomplet = infidélité loggée-mais-rejouée-sur-défaut-PG
C2 CB l2_leaf_reg — sous quel nom la HPO CB logge-t-elle le L2 ? Le harness le lit sans alt_name : si loggé reg_lambda (≠ l2_leaf_reg) → valeur ignorée, replay sur défaut PG (tracé par l'audit mais pas fidèle) inspecter les best_params CB en MLflow l2_leaf_reg → OK ; reg_lambdafix harness (ticket prod) OU exclure CB du gate (verdict-famille CB sinon non-fiable)
C3 early_stopping_rounds (toujours résolu) a-t-il varié sur la fenêtre des tirages ? historique PG/runs varié → pinned_resolve={"EARLY_STOPPING_ROUNDS": <vérifiée>} (sinon infidélité uniforme assumée), ou source par-tirage
C4 Valeurs env = régime defi : CVN_TIMEFRAME=5m, CVN_BINARY_CLASSIFICATION=1 (posés par code) confirmer = runs defi_top5 régime hors-mappé
C5 predictions_dir = store PARTAGÉ (S3/cache), pas /tmp pod-local param du DAG gate lit cohorte vide → INCONCLUSIVE_TOOLING
C6 Coût réel mesuré (§2 — signe de M en dépend) §2 gate coût full run interdit

Notes de cadrage (en lisant M) : n_estimators = plafond (l'early-stop coupe en-dessous via le val fold-3 → capacité effective en partie fold-3-déterminée) ; le gate est nécessaire-non-suffisant (PASS gate la dépense multi-fold, pas une evidence cross-fold — fold-3 reste in-sample pour l'axe fold) ; complétude sur K_eff ≥ min par crypto, pas présence-de-clé (présent-mais-vide → tooling, jamais pool-4 silencieux).

3. Déclenchement

DAG : diagnostic__s43 (UI Airflow → Trigger DAG w/ config). Param set (mirror s42 + spécifiques s43) :

{
    "crypto_group": "defi_top5",
    "fold_id": 3,
    "use_pin": true,
    "skip_phase_a": false,
    "seed_list": "1337,1338,1339,1340,1341",
    "n_rounds_canonical": 300,
    "expected_f1": -1,
    "epsilon": 0.005,
    "artifact_dir": "/tmp/s18-diagnostic"
}

Sémantique des params : - use_pin=true → warm via le pin S07 (~3-5 min/cellule au lieu de ~3h cold). - skip_phase_a=false + use_pin=true (pas un conflit) : phase_a_should_run(skip_phase_a, use_pin) → la phase A (capture) ne tourne que si NOT skip_phase_a ET pin MISS. Donc skip_phase_a=false = fallback cold-capture sur un MISS ; sur un HIT, le pin est lu et la capture est sautée d'office. - seed_list 5 seeds → variance multi-seed pour CB (+XGB si stochastique) ; LGB déterministe ignore les seeds. - expected_f1=-1sentinelle « gate f1 désactivé » : S05 remplace f1_buy par net_expectancy comme métrique-vérité (≤0 → pas de gate f1, lu depuis PG si besoin). - epsilon=0.005 → tolérance numérique de replay Phase A (cohérence capture↔replay), sans rapport avec la matérialité micro/macro. - artifact_dir=/tmp/s18-diagnosticdir partagé des parquets capturés par s18 (convention commune aux diagnostics, identique à s42) ; le verdict JSON s43 y est écrit préfixé s43 (pas d'écrasement inter-diagnostics). /tmp pod-éphémère by design. - defi_top5 → 5 cellules-pods parallèles (producteurs de prédictions) + 1 pod de synthèse (group bootstrap).

Durée attendue : ~quelques heures (group bootstrap B=2000 × 3 familles, parallélisé). Surveiller via Loki (§6) plutôt que d'attendre la fin.

4. Ce que ça produit — catalogue des verdicts

Verdict GLOBAL (c_verdict, group-level) :

Verdict Sens Action (routing §5)
C_GENERALISED_NOT_TRADEABLE Pas exploitable — les 3 familles perdent net à tout θ → S06 (retravailler features/cibles)
C_REFUTED_TRADEABLE Exploitable — ≥1 famille gagne net → follow-up déploiement de cette famille
C_PER_FAMILY Mixte routing par famille
INCONCLUSIVE_UNDERPOWERED On ne sait pas — IC trop large plus de données / cohorte plus large
INCONCLUSIVE_COST_SENSITIVE Dépend des frais affiner les coûts, re-décider
INCONCLUSIVE_TOOLING Machinerie en échec corriger + relancer (§7)

⚠️ Sémantique « REFUTED » : C_REFUTED_TRADEABLE = le statut C « non tradable » est réfuté (donc : oui tradable). À ne pas confondre avec le « C REFUTED » de l'Epic (= tradabilité réfutée = non tradable).

Verdict du GATE r3 (b) (event=s43_gate_outcome, S43PrefilterVerdict.status + .gate) — ce que le pré-filtre cross-tirage sur fold-3 rend :

Verdict gate Sens Action
PASS_PROCEED_MULTIFOLD PROCEED ≥1 famille a un edge robuste à travers les tirages (positif dans ≥ frac des rééchantillons-sélection) dépenser le multi-fold {2,3,4} (item 2) — le seul cas qui justifie le compute cher
C_FRAGILE_TO_SELECTION STOP edge présent dans certains tirages mais pas robuste → artefact de la variance de sélection HPO, pas une propriété du signal (winner's-curse sur l'axe config) clôt S05 cheap sur fold-3, sans payer le cold-capture
C_GENERALISED_NOT_TRADEABLE STOP toutes familles robustement négatives (M ≤ 0 cross-tirage) → S06
INCONCLUSIVE_CONFIG_UNSTABLE STOP cohorte/sélection non-assessable (K_eff insuffisant, IC trop larges) plus de tirages / données
INCONCLUSIVE_TOOLING STOP machinerie / cohorte incomplète (un pod tombé, un parquet manquant) corriger (checklist §2bis) + relancer

Le C_FRAGILE_TO_SELECTION est le sortie cheap la plus informative-par-euro : si l'edge ne tient pas à travers les tirages HPO même sur le fold pinné, S05 se clôt observationnellement — le miroir de « MLflow a clos S04 sans le run B ». Annotation récence (recency_most_recent) hors-critère : relire un FRAGILE par récence (fragile-aux-configs-récentes = live-pertinent, vs seuls-vieux-tirages = moins).

Borne du verdict — payload.scope (anti-sur-attribution, deux bornes) : un verdict qui clôt (C_FRAGILE/C_GENERALISED_NOT_TRADEABLE) ne porte que sur : 1. evaluable_families = LightGBM + CatBoost (les deux avec archive de sélection par-cycle fidèle) ; XGBoost = non_evaluable_families (archivage HPO par-trial, pas par-cycle — fiche décision), jamais compté dans le verdict. 2. model_class = GBDT — les deux familles évaluées sont des variantes gradient-boosted trees. Un négatif ne s'over-read PAS en « le signal est mort pour tout modèle » ; il dit « pas d'edge net tradable pour les modèles GBDT » — il ne dit rien d'une famille non-GBDT (régression linéaire, réseau de neurones).

5. Lire la sortie + decision routing

Output (par famille) dans le verdict JSON (artifact_dir) + Loki event=s43_verdict : family, M_obs (max espérance nette), theta_star, rate_buy_at_star, n_buy_at_star (nb absolu de trades), ci_low/ci_high, family_verdict, reason_codes[], per_asset_divergence[].

Reason codes — la nuance derrière le verdict :

reason Signification
non_degenerate_rate_failed EV>0 existe mais seulement à <10% de trades (piège over-trade) → compté NOT_TRADEABLE
cost_sensitive le verdict bascule dans la band coût ±50% (13-39 bps round-trip, legs=2 ; nominal 26 bps)
no_trade_points aucun θ ne déclenche de trade
size_domination_sensitive micro et macro divergent (une crypto domine) → escalade Epic
xgb_calibration_source=lgb_like_fallback XGB calibré comme LGB (assumption, pas prod-grade)

Decision routing (pré-engagé, plan Chapter 5.B) :

C_GENERALISED_NOT_TRADEABLE → route S06 (features/target), PAS de déploiement (ADR-2)
C_REFUTED_TRADEABLE         → follow-up Story déploiement (lead, pas un ship)
                              si XGB : valider la calibration prod-grade AVANT ship
C_PER_FAMILY                → routing par famille
INCONCLUSIVE_*              → pas de routing scientifique (re-data / re-cost / re-run)

6. Observabilité (Loki)

Via le skill loki-query (line-filter, jamais --label-mode). Events s43_* clés :

Event Quand le regarder
s43_opportunity_balance début — n_scored_rows/crypto + ratio max/min (gate macro-sensibilité)
s43_cost_snapshot début — coûts résolus + slippage_source par crypto (ancre réplication)
s43_cost_negative si cost_bps<0 (carry positif net) — valide, pas une erreur
s43_atr_price_ratio_observed par crypto — atr_price, window (vérif trailing / anti look-ahead post-hoc)
s43_subblock_partition n_candles, block_len du bootstrap
s43_calibration_fitted family=xgb, method, n_fit — ancre le caveat xgb_calibration_source=lgb_like_fallback
s43_bootstrap_complete par famille — B_done=2000, walltime_s, ci_width (sanity)
s43_verdict par famille — family_verdict + reason codes
s43_group_verdict fin — c_verdict + decision_routing + per_asset_divergence
s43_io_*_failed (severity=error) tout échec I/O / coût → INCONCLUSIVE_TOOLING

Exemple : python scripts/loki_query.py --event s43_group_verdict --since 6h.

7. Troubleshooting

Symptôme Cause probable Fix
INCONCLUSIVE_TOOLING + s43_io_cost_resolve_failed clés coût PG manquantes créer CVN_COST_* en Console (§2)
INCONCLUSIVE_TOOLING + s43_io_parquet_load_failed pin S07 absent / capture échouée use_pin=false pour forcer cold-capture, ou re-pin via S07
INCONCLUSIVE_UNDERPOWERED partout IC trop large (sous-puissance + max-bias) propriété connue (plan §5.5) ; vérifier n_buy_at_star faible → besoin plus de données
INCONCLUSIVE_COST_SENSITIVE verdict fragile aux frais revérifier les valeurs coût (Console) ; le signal est borderline
1-2 cellules-pods en échec, 3-4 OK (le cas ops le plus fréquent : OOM/eviction sur une crypto) une crypto n'a pas produit ses prédictions ; la synthèse tourne quand même (all_done) Règle de couverture cohorte : le group bootstrap exige ≥ 4/5 cryptos. Si 4/5 → verdict émis sur la cohorte disponible avec reason=partial_cohort coverage=4/5 (lire dans s43_group_verdict). Si < 4/5INCONCLUSIVE_TOOLING reason=cohort_coverage_below_floor. Action : relancer uniquement la/les cellule(s) manquante(s) (warm pin → rapide), pas tout le run ; vérifier la cause (Loki de la cellule échouée)
reason=size_domination_sensitive une crypto domine le micro-average lire la macro-sensibilité dans les notes ; escalade Epic
Pods Pending au démarrage autoscale PRO2-M from-zero normal, attendre les nœuds ; ne pas relancer dans un pool à 0
Crash Python visible dans l'UI bug (ne devrait jamais arriver, ADR-25) capturer le traceback Loki, ouvrir une issue — un échec doit produire INCONCLUSIVE_TOOLING, pas un raise

8. Réplication

Le verdict est ancré par le cost_snapshot (timestamp + clés/valeurs coût loggués event=s43_cost_snapshot). Pour rejouer à l'identique 6 mois plus tard : même fold_id=3, même pin S07, et lire le cost_snapshot_id du run original. Les coûts du verdict sont les coûts actuels au moment du run (pas ceux de la date du fold) — philosophie « tradabilité observable aujourd'hui ».

Funding — la clé CVN_COST_FUNDING_BPS_H4 stocke le RÉSULTAT amorti (un nombre, additionné direct à fees+slippage), pas le funding 8h brut. L'opérateur le calcule hors-ligne : FUNDING_BPS_H4 = funding_8h × 0.5 × (hold_eff/4h) (archi §3.3) puis stocke le résultat. → Pour la réplication, documenter dans les notes du run le funding_8h et le hold_eff utilisés pour dériver la valeur (le snapshot ne stocke que le résultat). ATR/price est mesuré in-code (trailing, loggué s43_atr_price_ratio_observed), pas en config.


Annexe — rappel statistique (pour interprétation)

  • Le verdict par famille teste l'enveloppe M = maxθ E(θ) (le meilleur réglage net) via un bootstrap group-level (clusters crypto × sub-block, n=25, B=2000), corrigé Bonferroni sur les 3 familles. Ça évite le piège « le meilleur θ a l'air rentable » (biais de sélection).
  • INCONCLUSIVE_UNDERPOWERED est un résultat honnête, pas un échec : avec peu de données, on refuse de fabriquer une fausse certitude.
  • L'agrégation est micro-average (le portefeuille déployé), avec une macro-sensibilité si une crypto domine.