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_reviewc6a789faPASSED). 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 statsdecide_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ée —
hamilton/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_verdictest déjà group-level (par famille, sur les 5 cryptos). Il n'y a PAS de_aggregate_cellni 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
_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.