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 · committeeplan_reviewc6a789faPASSED. 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_failed → INCONCLUSIVE_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/4hmesuré 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 à+1alors qu'il est réellement négatif biaise le verdict versNOT_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 rundé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é : expCVNTrade_HPO, filtres sur params +model_id LIKE), C1 complétude (LGB 9/9, CB 4/4 — zéro substitution), C2 CBl2_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_lambda → fix 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=-1 → sentinelle « 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-diagnostic → dir 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_SELECTIONest 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/5 → INCONCLUSIVE_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_UNDERPOWEREDest 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.