Skip to content

Plan de mise en œuvre — POC Runtime v1

Réf : ../design/CVN-N003-poc-runtime-design.md, issue #417, ADR 39-42 Date : 2026-03-30


Objectif

UNI ATR1.2_1.3_H3 en paper trading résilient, avec possibilité d'ajouter CRV sans reset, et chemin clair vers le live.

Contrainte

Chaque phase est déployable et testable indépendamment. Pas de big bang.


Phase 0 — Préparation (1 jour)

0.1 Promouvoir le modèle UNI

Le modèle existe déjà (MLflow v6). Il faut le promouvoir en Production.

# Via MLflow API ou UI
mlflow models transition-stage \
  --name CVNTrade_XGBoost_UNIUSDC_ATR1.2_1.3_H3 \
  --version 6 --stage Production

Vérification : mlflow.tracking.MlflowClient().get_latest_versions("CVNTrade_XGBoost_UNIUSDC_ATR1.2_1.3_H3", stages=["Production"])

0.2 Valider le paper trading existant (embedded)

Avant de migrer, s'assurer que le paper trading actuel fonctionne avec UNI :

# Lancer via l'API existante
POST /api/sessions {"name": "UNI-baseline", "symbols": ["UNIUSDC"]}
POST /api/sessions/{id}/start
# Vérifier : signaux générés, positions ouvertes, état cohérent

Objectif : avoir un baseline mesurable avant la migration.

0.3 Créer le DeploymentArtifact UNI manuellement

Insérer dans la future table deployments (ou un fichier JSON de référence) :

{
  "deployment_id": "dep_uni_v6",
  "crypto": "UNIUSDC",
  "strategy": "ATR1.2_1.3_H3",
  "model_uri": "models:/CVNTrade_XGBoost_UNIUSDC_ATR1.2_1.3_H3/6",
  "meta_uri": null,
  "pte_config": {"sl_atr": 1.2, "tp_atr": 1.3, "horizon": "H3"},
  "wfrb_verdict": "GO_PAPER",
  "regime_compat": 0.71,
  "regime_exploit": 0.74
}

Livrable : fichier deployments/dep_uni_v6.json ou INSERT SQL.


Phase 1 — Event Store + Tables (2-3 jours)

1.1 Migration m013 — tables runtime

Fichier : api/stores/migrations/m013_runtime_tables.py

Créer les 5 tables : - deployments - trading_sessions (avec desired_status + observed_status) - trading_events (append-only, JSONB payload) - trading_positions (projection) - deployment_bindings

Index critiques : - trading_events(session_id, sequence) — reconstruction d'état - trading_sessions(observed_status) — recovery au restart - deployment_bindings(runtime_instance, crypto, mode) — lookup rapide

Enregistrer dans pg_session_store._run_migrations().

Test : migration idempotente, make test-unit passe.

1.2 Event store module

Fichier : src/commun/runtime/event_store.py

class EventStore:
    def append(session_id: str, event_type: str, payload: dict) -> str
    def get_events(session_id: str, since_sequence: int = 0) -> List[TradingEvent]
    def get_latest(session_id: str, event_type: str) -> Optional[TradingEvent]
    def rebuild_state(session_id: str) -> SessionState

Implémentation PostgreSQL. Pas d'ORM, psycopg2 direct (pattern existant).

Test : append + read + rebuild sur données synthétiques.

1.3 Session state projection

Fichier : src/commun/runtime/session_state.py

@dataclass
class SessionState:
    session_id: str
    equity: float
    open_positions: List[Position]
    closed_positions_count: int
    total_pnl: float
    last_signal_at: Optional[datetime]
    last_heartbeat_at: Optional[datetime]

    @staticmethod
    def from_events(events: List[TradingEvent]) -> "SessionState"

Test : construire un état depuis une séquence d'events simulés.


Phase 2 — Runtime Service minimal (3-4 jours)

2.1 Service FastAPI standalone

Fichier : services/runtime/app.py

app = FastAPI(title="cvntrade-runtime")

@app.on_event("startup")
async def startup():
    recover_active_sessions()

@app.on_event("shutdown")
async def shutdown():
    graceful_stop_all_sessions()

Endpoints : - POST /sessions/{id}/start — démarre un session actor - POST /sessions/{id}/stop — arrête proprement - POST /sessions/{id}/pause — suspend le polling - POST /sessions/{id}/resume — reprend - GET /sessions/{id}/health — heartbeat + observed_status - GET /sessions/{id}/state — projection courante

Port : 8030 (déjà réservé par l'ancien standalone).

2.2 Session Actor

Fichier : services/runtime/session_actor.py

Refactoring de CVNTrade_PaperTradingEngine en actor isolé :

class SessionActor:
    def __init__(self, session_id, deployment, event_store, adapter):
        self.session_id = session_id
        self.deployment = deployment
        self.event_store = event_store
        self.adapter = adapter
        self._thread = None

    def setup(self) -> bool:
        # Charger modèle depuis deployment.model_uri
        # Initialiser Signal/Decision/Risk engines
        # Event: SessionStarted

    def start(self):
        # Démarrer la boucle dans un thread daemon
        self._thread = Thread(target=self._run_loop, daemon=True)

    def _run_loop(self):
        while self._running:
            snapshot = self._ingest_market_data()
            signal = self._signal_engine.compute(snapshot)
            decision = self._decision_engine.evaluate(signal)
            if decision.should_trade:
                risk_ok = self._risk_engine.check(decision)
                if risk_ok:
                    result = self.adapter.submit_order(decision.intent)
                    self.event_store.append(self.session_id, "OrderSubmitted", ...)
            self._publish_heartbeat()
            sleep(self.interval)

    def stop(self):
        self._running = False
        self._thread.join(timeout=10)
        self.event_store.append(self.session_id, "SessionStopped", {})

Réutilise : toute la logique LdP de CVNTrade_PaperTradingEngine._generate_signal_ldp().

2.3 Execution Adapters

Fichier : services/runtime/adapters/paper_adapter.py

class PaperExecutionAdapter:
    def submit_order(self, intent: TradeIntent) -> OrderResult:
        # Simulated fill at current price + slippage
    def check_exits(self, positions, market) -> List[ExitSignal]:
        # Check TP/SL/Timeout for each position

Fichier : services/runtime/adapters/base.py

class ExecutionAdapter(Protocol):
    def submit_order(self, intent: TradeIntent) -> OrderResult: ...
    def check_exits(self, positions, market) -> List[ExitSignal]: ...

Le LiveExecutionAdapter sera ajouté en Phase 6.

2.4 Recovery au restart

Fichier : dans services/runtime/app.py

def recover_active_sessions():
    sessions = db.query("SELECT * FROM trading_sessions WHERE desired_status IN ('RUNNING', 'PAUSED')")
    for session in sessions:
        state = event_store.rebuild_state(session.session_id)
        actor = SessionActor(session.session_id, ...)
        actor.restore_from_state(state)
        if session.desired_status == "RUNNING":
            actor.start()
        # observed_status mis à jour par le heartbeat

Test : simuler un crash (kill process), redémarrer, vérifier que les positions sont reconstruites.

2.5 Dockerfile + Helm

Fichier : Dockerfile.runtime (ou réutiliser Dockerfile.k8s avec entrypoint différent)

FROM rg.fr-par.scw.cloud/cvntrade/airflow:${TAG}
CMD ["python", "-m", "uvicorn", "services.runtime.app:app", "--host", "0.0.0.0", "--port", "8030"]

Helm : ajouter un deployment cvntrade-runtime dans les values.


Phase 3 — API façade + Control Plane (2-3 jours)

3.1 Adapter l'API existante

Fichier : api/services/pt_bridge.pydéprécier

Remplacer par un client HTTP vers le runtime :

Fichier : api/services/runtime_client.py

class RuntimeClient:
    def __init__(self, base_url="http://cvntrade-runtime:8030"):
        self.base_url = base_url

    def start_session(self, session_id, config) -> bool
    def stop_session(self, session_id) -> bool
    def get_health(self, session_id) -> dict
    def get_state(self, session_id) -> SessionState

3.2 Endpoints lifecycle

Fichier : api/routers/sessions.py — adapter les routes existantes

  • POST /sessions/{id}/startRuntimeClient.start_session()
  • POST /sessions/{id}/stopRuntimeClient.stop_session()
  • GET /sessions/{id}/signals → lire trading_events PostgreSQL
  • GET /sessions/{id}/positions → lire trading_positions PostgreSQL

3.3 Endpoints Deployment Binding

Fichier : api/routers/bindings.py (nouveau)

  • POST /bindings/attach — créer un binding (BOUND)
  • POST /bindings/{id}/activate — créer session + démarrer actor
  • POST /bindings/{id}/pause — pause session
  • POST /bindings/{id}/promote — créer binding live
  • POST /bindings/{id}/unbind — stop + detach
  • GET /bindings — liste des bindings actifs

3.4 Desired vs Observed sync

Fichier : api/services/session_sync.py

Background task (toutes les 10s) : - Lire heartbeat:{session_id} Redis - Mettre à jour observed_status dans trading_sessions - Si desired ≠ observed depuis > 60s → alerte "stuck" - Si heartbeat TTL expiré → observed_status = DEAD


Phase 4 — Intégration Front + Grafana (2 jours)

4.1 Front : Fleet View

Adapter la page sessions existante pour afficher :

Crypto Mode Desired Observed Equity PnL Trades Heartbeat
UNIUSDC paper RUNNING RUNNING 10,234 +2.3% 12 3s ago

Source : trading_sessions + trading_positions + Redis heartbeat.

4.2 Grafana : Runtime dashboard

Fichier : infra/grafana/dashboards/runtime_sessions.json

Panels : - Sessions actives (table) - Equity par session (time series depuis trading_events type=MetricsSnapshot) - Trades par jour (bar chart) - Heartbeat lag (gauge) - Positions ouvertes (table)

4.3 WebSocket via Redis

Fichier : api/ws/session_feed.py — adapter

Au lieu de consommer InMemoryEventPublisher, consommer Redis stream :

async def consume_redis_stream(session_id):
    stream_key = f"stream:events:{session_id}"
    last_id = "0"
    while True:
        events = await redis.xread({stream_key: last_id}, block=1000)
        for event in events:
            yield event
            last_id = event.id

Phase 5 — Multi-crypto + Résilience (2-3 jours)

5.1 Ajouter CRV sans toucher UNI

  1. Créer dep_crv_v3.json
  2. POST /bindings/attach (crypto=CRVUSDC)
  3. POST /bindings/{id}/activate
  4. Vérifier UNI non impacté (heartbeat, equity, positions inchangés)

5.2 Circuit breaker

Dans le Risk Engine du session actor : - Max drawdown session → pause auto - 3 rejets exchange consécutifs → pause + alerte - Heartbeat manquant > 60s → observed_status = DEAD + auto-restart (max 3)

5.3 Meta-model optionnel

Dans le Decision Engine : - Si deployment.meta_uri est rempli → charger le meta - Appliquer comme filtre post-signal - Event MetaFiltered avec meta_score

Activable par deployment (pas global).


Phase 6 — Live adapter (2 jours)

6.1 LiveExecutionAdapter

Fichier : services/runtime/adapters/live_adapter.py

class LiveExecutionAdapter:
    def __init__(self, api_key, api_secret, testnet=True):
        self.client = BinanceClient(api_key, api_secret, testnet)

    def submit_order(self, intent):
        order = self.client.create_order(
            symbol=intent.crypto,
            side="BUY",
            type="LIMIT",
            quantity=intent.quantity,
            price=intent.price,
        )
        return OrderResult(order_id=order["orderId"], ...)

    def check_exits(self, positions, market):
        # Check TP/SL against current market price
        # Return ExitSignal for positions to close

6.2 Promotion paper → live

POST /bindings/{paper_binding_id}/promote
→ Crée nouveau binding mode=live avec même deployment
→ Démarre session actor avec LiveExecutionAdapter
→ Paper binding reste ACTIVE (comparaison A/B) ou PAUSED

Ordre des PR

PR Phase Contenu Dépend de
#418 0 ADR + Design doc
#419 0 Promouvoir UNI v6, baseline paper existant #418
#420 1 Migration m013 + event store + session state #418
#421 2 Runtime service + session actor + paper adapter #420
#422 2 Recovery + heartbeat #421
#423 3 API façade + runtime client + bindings #421
#424 4 Front Fleet View + Grafana + WebSocket Redis #423
#425 5 Multi-crypto + circuit breaker + meta #422, #423
#426 6 Live adapter + promotion #425

Critères de succès POC

  • UNI paper tourne en continu > 24h sans crash
  • Recovery fonctionne après kill du runtime
  • CRV ajouté sans impacter UNI
  • Equity + trades visibles dans Grafana
  • Front affiche Fleet View avec health
  • Promotion paper → live fonctionne (testnet)

Stories (retro-registered in OP — 2026-06-09)

Enregistré a posteriori : Epic wp#268 (GH #417, l'umbrella réutilisée), parent Need CVN-N003. Statut Epic : On hold — la POC est partielle : le core est livré, mais les critères de succès POC ci-dessus sont tous décochés (POC non validée).

Story Titre GH · OP Artefact Statut
CVN-N003-EA-S01 POC Runtime core — architecture + event store + runtime service #1159 · wp#269 PR #418 (Phases 0-2) Closed

Phases restantes non complétées (POC parkée — non storifiées) : Phase 3 (API façade + Control Plane) · Phase 4 (Front Fleet View + Grafana) · Phase 5 (multi-crypto + circuit breaker) · Phase 6 (live adapter + promotion) · validation 24h. La table « Ordre des PR » ci-dessus était un plan ; les PRs réels #420+ sont des fixes sans rapport (seul #418 a livré le core). Réouverture via nouvelles Stories si la POC Runtime reprend.