Skip to content

CVN-N001-EI-S05 — Architecture du diagnostic s43 (economic tradability)

Type : document d'architecture (le comment c'est construit). Compagnon du plan dossier r2.4 (le quoi & pourquoi, committee plan_review c6a789fa PASSED). Story : CVN-N001-EI-S05 (wp#228, GH #1060) · Epic CVN-N001-EI Block 4 · Statut Story : In specification (retour au plan r3). Révision : r2 (b) — le §0bis de cadrage (dossier r3) a superseded la forme per-cell de r1.1. Les sections détaillées ci-dessous décrivent encore r1.1 (per-cell + group-decision r2.7) — voir le §0 r2 (b) pour ce qui a changé ; le reste (couche stats decide_group_s43, transport cross-pod, events) reste valable.


§0 — r2 (b) : ce qui change vs r1.1 (cadrage §0bis, dossier r3)

r1.1 était per-cell (1 crypto/pod → verdict local → synthèse strict-majority). Le §0bis a trouvé que ça ne mappe pas le déploiement : la HPO prod re-sélectionne par-crypto à cadence non-synchronisée (config = processus stochastique, ADR-0098 Inv5). r2 (b) reconstruit s43 comme un pré-filtre cross-tirage sur fold-3 (gate avant le multi-fold) :

Aspect r1.1 (superseded) r2 (b)
Question tradabilité group-level d'un modèle l'edge survit-il à l'instabilité de sélection HPO ?
Régime modèle indécidé (probes stubs) un tirage par crypto depuis son propre set MLflow (sélection par-crypto, pas d'appariement rang cross-crypto) ; train-sur-best_params fidèle (s43_regime.py)
Stat décisionnelle decide_group_s43 (bootstrap (crypto,sub-block)) selection_bootstrap imbriqué (externe=sélection / interne=(crypto,sub-block)) → cross_draw_prefilter (règle robuste-cross-tirage)
Axes de robustesse cross-crypto (within-fold) cross-crypto + cross-tirage (gate) ; cross-fold {2,3,4} déféré à l'item 2
DAG resolve_cells → discriminate_cell (per-cell verdict) → synthesize resolve_cohort → acquire_cell (train+persist p_buy, isolé) → gate (complétude K_eff + bootstrap + verdict)
M re-labellisé « tradabilité » edge modèle+seuil, quantité distincte (le funnel peut la relever — couture aval due) ; teste le rung f1_buy→M de la chaîne A0
Verdicts C_*/INCONCLUSIVE_* PASS_PROCEED_MULTIFOLD / C_FRAGILE_TO_SELECTION / C_GENERALISED_NOT_TRADEABLE / INCONCLUSIVE_CONFIG_UNSTABLE / _TOOLING
Scaffold probe _probe_*/s43_nodes (X_tr/X_va) retiré (vestigial) ; la couche decide_group_s43 est gardée pour le multi-fold (item 2)

Inchangé & valable : le transport cross-pod (archi r1.1 #4 — npz keyé (crypto,fold,family,run_id), XCom=keys), la couche stats decide_group_s43/envelope_M/group_bootstrap_ci, les events, le gate coût. La barrière de validité est la checklist §2bis du runbook (in-pod).

1. Contexte & portée

s43 est un diagnostic read-only : il rejoue le fold S07-pinné (defi_top5 / fold_id=3) contre une grille θ pour les 3 familles (LightGBM, XGBoost θ-swept, CatBoost), calcule l'espérance nette après coûts E(θ), et rend un verdict de tradabilité (généralisation du statut C). N'entraîne aucun artefact de production, ne promeut rien (ADR-2). Méthode statistique : plan §1 (envelope M=maxθ E(θ), group bootstrap, Bonferroni). Niveau d'inférence = group-level (plan §1 Option 3) : le verdict famille est calculé sur les 5 cryptos ensemble ; le per-asset est reporting only (pas de verdict cellule).

2. Style architectural — Hamilton-native, deux couches

Mirror de s41 (S03) / s42 (S04), mais avec une différence structurelle clé due au niveau group (cf. §4) : le calcul statistique lourd est au pod de synthèse, pas par-cellule.

  • Couche Airflow (orchestration, impure)dag_diagnostic__s43.py : trigger, params, fan-out 5 cellules (1 pod/crypto, producteurs de prédictions), pod de synthèse (group bootstrap + verdict), provenance ADR-92.
  • Couche Hamilton (compute pur)hamilton/s43_nodes.py : le graphe statistique (partition, bootstrap, M, décision) exécuté au pod de synthèse. La décision est dans cette couche (fonctions pures, dans le graphe).
  • I/O isoléehamilton/s43_io.py : pin load/store, coût PG, parquet, persistance prédictions per-crypto.
  • Décision (fonctions pures Hamilton)s43_economic_tradability.py : _decide_family, _combine_families, per_asset_report, _route (cf. §4, splittées pour testabilité indépendante).

3. Vue composants

flowchart TB
    OP([Opérateur]) -->|trigger params| DAG

    subgraph Airflow["Couche Airflow — orchestration"]
        DAG[dag_diagnostic__s43.py]
        CELLS[fan-out 5 cell-pods
producteurs de prédictions] SYNTH[pod de synthèse
group bootstrap + verdict] DAG --> CELLS --> SYNTH end subgraph Hamilton["Couche Hamilton — compute pur (au pod synthèse)"] NODES[s43_nodes.py
graphe statistique nommé §5] DECIDE[s43_economic_tradability.py
_decide_family · _combine_families
per_asset_report · _route] TC[[theta_curve.py — RÉUTILISÉ inchangé]] NODES --> TC NODES --> DECIDE end subgraph IO["s43_io.py — effets de bord"] PIN[pin load/store] COST[résolution coût PG] PRED[persist/load prédictions per-crypto] end CELLS -.->|produce + cache| PRED SYNTH --> NODES SYNTH -.->|load 5 cryptos by key| PRED DAG --> IO PIN -.->|S07 warm pin| S07[(S07 pinned fold)] COST -.->|ADR-59| PG[(ftf_config CVN_COST_*)] PRED -.-> CACHE[(cache / S3 / MLflow)] DECIDE -->|verdict JSON| OUT[(artifact_dir + Loki)]

4. Modules & responsabilités

Module Rôle Entrées Sorties
dags/dag_diagnostic__s43.py Orchestration : params, fan-out cell-pods, pod synthèse, provenance ADR-92 params Airflow verdict JSON + Loki
hamilton/s43_io.py I/O : _pin_load/_pin_store, coût PG + ATR/price, persist/load prédictions per-crypto (transport cross-pod §7) pin key, ftf_config, parquet (y_true,p_buy) cachés, cost_atr, snapshot
hamilton/s43_nodes.py Graphe statistique (au synthèse) : partition, bootstrap group M, IC, θ*, per-asset prédictions 5 cryptos, cost_atr M, IC, θ*, n_buy
s43_economic_tradability.py Décision, 4 fonctions pures (§4bis) stats FamilyDecision, c_verdict, routing
nodes/theta_curve.py Réutilisé tel quel : E(θ), precision_buy, cost_atr_from_bps (y_true,p_buy), thetas, cost_atr list[ThetaPoint]
dags/models/xgboost_dag.py Modifié : theta_curve + θ-sweep + calibration LGB-like training XGB event=theta_curve

4bis. Décision splittée (review #3 — fonctions pures testables indépendamment)

⚠️ Correction de niveau : sous Option 3 (group-level), family_verdict est déjà group-level (par famille, sur les 5 cryptos). Il n'y a PAS de _aggregate_cell ni de verdict cellule (le per-asset est reporting). Le split est donc :

def _decide_family(stats: FamilyStats) -> FamilyDecision:
    # (M_obs, ci_low, ci_high, rate_at_star, n_buy, cost_sensitive) → verdict famille
    # pseudo-code pré-enregistré plan §1
def _combine_families(decisions: dict[str, FamilyDecision]) -> CVerdict:
    # 3 family decisions → c_verdict global (table n_refuted/n_not/n_inc + priorité inconclusif)
def per_asset_report(curves_by_crypto: dict[str, Curve]) -> Heterogeneity:
    # signe du M ponctuel par crypto vs groupe → per_asset_divergence (reporting, ne décide pas)
def _route(c_verdict: CVerdict) -> Routing:
    # Chapter 5.B : NOT_TRADEABLE→S06 ; REFUTED→follow-up déploiement ; INCONCLUSIVE_*→pas de routing
Chaque fonction pure, testable seule (ex. _combine_families({LGB:REFUTED, CB:REFUTED, XGB:INCONCLUSIVE}) sans mocker les stats family-level). Même retour que S03 Q9.a r1 (anti-monolithe).

5. Graphe Hamilton nommé (review #2 — l'architecture réelle)

Le graphe exécuté au pod de synthèse par driver.Builder().with_modules(s43_nodes, s43_economic_tradability).build() :

flowchart LR
    PRED[predictions_by_crypto_family] --> SUB[subblock_partition]
    PRED --> OBS[observed_curve]
    COST[resolve_cost_atr] --> OBS
    COST --> CURVE
    SUB --> RS[resample_units
B=2000, +seed si stochastique] PRED --> RS RS --> CURVE[group_curve_per_resample
micro-avg via theta_curve] CURVE --> MB[M_per_resample] MB --> CI[m_bootstrap_ci] OBS --> TSTAR[theta_star + rate_at_star + n_buy_at_star] COST --> CSENS[cost_sensitivity ±50%] PRED --> CSENS CI --> FD[decide_family] TSTAR --> FD CSENS --> FD FD --> CV[combine_families] PRED --> PA[per_asset_report] COST --> PA CV --> RT[route] CV --> OUT[verdict JSON] PA --> OUT RT --> OUT

Nodes (un par boîte) : subblock_partition, resample_units, group_curve_per_resample, M_per_resample, m_bootstrap_ci, observed_curve, theta_star, cost_sensitivity, decide_family, combine_families, per_asset_report, route. Pas de node monolithique ([[feedback_hamilton_native_no_wrappers]]). Deux nodes complémentaires (plan r2.5) : opportunity_balance (gate n_scored_rows max/min) → macro_sensitivity (conditionnel, alimente decide_family via size_domination_sensitive).

6. Contrats d'interface

Output schema (par famille) — plan §1 — avec définitions (review refinement) : - M_obs, theta_star, rate_buy_at_star, n_buy_at_star (nb absolu de BUY à θ), ci_low/ci_high (IC95 percentile de M, B=2000). - n_eff = nb de clusters distincts effectifs du bootstrap = 25 (LGB) | 25×K (CB/XGB stochastiques). - per_asset_divergence = liste des cryptos dont le signe du M ponctuel (sur sa courbe per-crypto) diffère du signe du M groupe (reporting). - c_verdict = verdict group-level* (combinaison cross-famille) — il n'y a pas de niveau cellule. - family_verdict, reason_codes[], xgb_calibration_source, cost_snapshot_id.

Contrat bootstrap (review #1 — explicite per-famille) : - Variance primaire = sub-blocks : 5 cryptos × 5 sub-blocks contigus non-chevauchants = 25 unités de cluster fixes ; le bootstrap tire ces unités avec remise (resampling de clusters, pas réassemblage de série → pas moving-block). Raison architecturale : avec 5 cryptos seuls (n=5) l'IC est trop large, et LGB est déterministe (multi-seed = 0 variance) → les sub-blocks remontent n à 25 indépendamment de la stochasticité modèle. - Variance additive = multi-seed, par famille : LGB aucun (n_eff=25) ; CB K=5 (n_eff=125) ; XGB K=5 ssi PG subsample/colsample<1.0 (n_eff=25|125). Couche complémentaire au sub-block, jamais seule. - B_canonical=2000, seed figé, recompute M (micro-average) par resample → bit-identical.

Contrat coût : cost_atr = legs·(cost_bps/1e4)/(ATR/price), legs=2, cost_bps=FEES+SLIPPAGE+FUNDING_H4 (PG fail-loud), ATR/price trailing (anti look-ahead, test ≤t).

Agrégation groupe = micro décisionnelle + macro sensibilité conditionnelle (plan r2.5) : - DÉCISIONNELLE = micro-average (poids 1/opportunité) → precision_buy/rate_buy = quantités opérationnelles portefeuille (« le portefeuille déployé gagne-t-il ? »). Règle primaire, committee-PASSED. - Check de matérialité : n_scored_rows par crypto (lignes scorées éligibles θ-sweep, pas n_candles — pipeline filtre/labels manquants) loggué event=s43_opportunity_balance. max/min ≤ 1.2 → micro/macro immatériel. - Sensibilité MACRO non-décisionnelle (ssi max/min > 1.2) : M_macro = maxθ mean_c E_c(θ) (poids 1/crypto). Accord micro/macro → micro tient (robustesse) ; désaccord → micro enregistré + reason=size_domination_sensitive + escalade Epic. Pas de swap micro→macro (même statut que la sensibilité coût §5.1).

7. Points d'intégration

Intégration Mécanisme Contrainte
S07 warm pin s43_io._pin_load/_pin_store (mirror s41_io) use_pin=true ; phase_a_should_run ; spy from_training_cache==0 (ADR-0094 Inv 7)
Transport cross-pod (review #4) cell-pod → cache/S3 (prédictions per-crypto, keyed (crypto,fold,family,hp_id)) ; XCom = KEYS + résumé compact, PAS les arrays ; synthèse lit les 5 par key XCom Postgres < 48 KB → jamais de payload courbe/array en XCom (sinon truncate silencieux)
Harness theta_curve import pur, inchangé back-compat ; p=precision_buy correct
XGB θ-curve xgboost_dag.py émet theta_curve + calibration LGB-like parité LGB/CB ; télémétrie prod (US-8)
Coût PG ftf_config CVN_COST_* (ADR-59) Console UI only ; fail-loud (ADR-90)
Provenance dag_doc_md + make_tags(build=sha) + event=dag_loaded ADR-92 (G6)
Capture si MISS chaque cell-pod capture SA crypto in-pod (single-pod chain Phase A+B, [[feedback_diagnostic_dag_must_capture_in_pod]]) ; les 5 cellules en parallèle 5 MISS = ~3h wall-clock (parallèle), pas 15h sériel

8. Architecture d'observabilité & d'échec

Catalogue d'événements s43_* (ADR-30/32/33, event=key=value) :

Event Sévérité Sens
s43_subblock_partition info n_candles, block_len
s43_opportunity_balance info n_scored_rows par crypto + max/min ratio → gate macro-sensibilité (plan r2.5)
s43_atr_price_ratio_observed info crypto, atr_price, window — vérif trailing post-hoc (review)
s43_calibration_fitted info family=xgb, method, n_fit — audit du LGB-like fallback (review)
s43_cost_snapshot info clés/valeurs + slippage_source par crypto
s43_cost_negative info cost_bps<0 (carry positif valide)
s43_bootstrap_complete info family, B_done=2000, walltime_s, ci_width — perf + sanity (review)
s43_io_cost_resolve_failed error clé PG manquante → INCONCLUSIVE_TOOLING
s43_io_parquet_load_failed error capture/pin absent → INCONCLUSIVE_TOOLING
s43_verdict info/error family_verdict + reason codes
s43_group_verdict info/error c_verdict + decision_routing + per_asset_divergence

Principe (ADR-25/31) : aucun chemin d'erreur ne raise vers l'UI ; tout produit un INCONCLUSIVE_* structuré. Pas de print. Sévérité via event=… severity=… ([[feedback_no_python_crash_visible]]). Barrière synthèse trigger_rule="all_done".

9. Conformité ADR

ADR Application
ADR-2 Aucune promotion ; read-only
ADR-25 Fail-loud structuré ; jamais de NaN propagé (rate_buy=0→−∞)
ADR-56/58 Gated, guardrail + tests (smoke + micro-smoke)
ADR-59 Coût en ftf_config, Console only
ADR-60/61 Compute via Hamilton (graphe nommé §5)
ADR-89 Harness inchangé ; réutilise theta_curve
ADR-90 Coût/HP résolus PG, fail-loud
ADR-92 Provenance 3 surfaces
ADR-0093/0094 Smoke/full split ; capture-spy Inv 7

Fichiers à créer / modifier

Voir plan §4. Créer s43_economic_tradability.py (4 fonctions pures §4bis) + hamilton/s43_{nodes,io}.py + dag_diagnostic__s43.py + tests/unit/test_s43_*.py ; modifier xgboost_dag.py.

Open items archi

Aucun. Tous tranchés (r1 + r1.1) : bootstrap per-famille, DAG Hamilton nommé, décision splittée (4 fonctions pures), transport cross-pod (cache + XCom keys), capture parallèle, events, schemas. Le micro vs macro est résolu (plan r2.5) : micro décisionnel + macro = sensibilité conditionnelle à la matérialité (n_scored_rows max/min > 1.2). Archi r1.1-stable → scaffolding s43 peut démarrer.