# TradingAgents architecture convergence draft: application boundary Status: draft Audience: backend/dashboard/orchestrator maintainers Scope: define the boundary between HTTP/WebSocket delivery, application service orchestration, and the quant+LLM merge kernel ## Current status snapshot (2026-04) This document is still the **target boundary** document, but several convergence pieces are already landed on the mainline: - `web_dashboard/backend/services/job_service.py` now owns public task/job projection logic; - `web_dashboard/backend/services/result_store.py` persists result contracts under `results//result.v1alpha1.json`; - `web_dashboard/backend/services/analysis_service.py` and `api/portfolio.py` already expose contract-first result payloads by default; - `/ws/analysis/{task_id}` and `/ws/orchestrator` already carry `contract_version = "v1alpha1"` and include result/degradation/data-quality metadata. What is **not** fully finished yet: - `web_dashboard/backend/main.py` still contains too much orchestration glue and transport-local logic; - route handlers are thinner than before, but the application layer has not fully absorbed every lifecycle branch; - migration flags/modes still coexist with legacy compatibility paths. ## 1. Why this document exists The current backend mixes three concerns inside `web_dashboard/backend/main.py`: 1. transport concerns: FastAPI routes, headers, WebSocket sessions, task persistence; 2. application orchestration: task lifecycle, stage progress, subprocess wiring, result projection; 3. domain execution: `TradingOrchestrator`, `LiveMode`, quant+LLM signal merge. For architecture convergence, these concerns should be separated so that: - the application service remains a no-strategy orchestration and contract layer; - `orchestrator/` remains the quant+LLM merge kernel; - transport adapters can migrate without re-embedding business rules. ## 2. Current evidence in repo ### 2.1 Merge kernel already exists - `orchestrator/orchestrator.py` owns quant runner + LLM runner composition. - `orchestrator/signals.py` owns `Signal`, `FinalSignal`, and merge math. - `orchestrator/live_mode.py` owns batch live execution against the orchestrator. This is the correct place for quant/LLM merge semantics. ### 2.2 Backend currently crosses the boundary `web_dashboard/backend/main.py` currently also owns: - analysis subprocess template creation; - stage-to-progress mapping; - task state persistence in `app.state.task_results` and `data/task_status/*.json`; - conversion from `FinalSignal` to UI-oriented fields such as `decision`, `quant_signal`, `llm_signal`, `confidence`; - report materialization into `results///complete_report.md`. This makes the transport layer hard to replace and makes result contracts implicit. At the same time, current mainline no longer matches the oldest “all logic sits in routes” description exactly. The codebase now sits in a **mid-migration** state: - merge semantics remain in `orchestrator/`; - public payload shaping has started moving into backend services; - legacy compatibility fields still exist for UI safety. ## 3. Target boundary ## 3.1 Layer model ### Transport adapters Examples: - FastAPI REST routes - FastAPI WebSocket endpoints - future CLI/Tauri/worker adapters Responsibilities: - request parsing and auth - response serialization - websocket connection management - mapping application errors to HTTP/WebSocket status Non-responsibilities: - no strategy logic - no quant/LLM weighting logic - no task-stage business rules beyond rendering application events ### Application service Suggested responsibility set: - accept typed command/query inputs from transport - orchestrate analysis execution lifecycle - map domain results into stable result contracts - own task ids, progress events, persistence coordination, and rollback-safe migration switches - decide which backend implementation to call during migration Non-responsibilities: - no rating-to-signal research logic - no quant/LLM merge math - no provider-specific data acquisition details ### Domain kernel Examples: - `TradingOrchestrator` - `SignalMerger` - `QuantRunner` - `LLMRunner` - `TradingAgentsGraph` Responsibilities: - produce quant signal, LLM signal, merged signal - expose domain-native dataclasses and metadata - degrade gracefully when one lane fails ## 3.2 Canonical dependency direction ```text transport adapter -> application service -> domain kernel transport adapter -> application service -> persistence adapter application service -> result contract mapper ``` Forbidden direction: ```text transport adapter -> domain kernel + ad hoc mapping + ad hoc persistence ``` ## 4. Proposed application-service interface The application service should expose typed use cases instead of letting routes assemble logic inline. ## 4.1 Commands / queries Suggested surface: - `start_analysis(request) -> AnalysisTaskAccepted` - `get_analysis_status(task_id) -> AnalysisTaskStatus` - `cancel_analysis(task_id) -> AnalysisTaskStatus` - `run_live_signals(request) -> LiveSignalBatch` - `list_analysis_tasks() -> AnalysisTaskList` - `get_report(ticker, date) -> HistoricalReport` ## 4.2 Domain input boundary Inputs from transport should already be normalized into application DTOs: - ticker - trade date - auth context - provider/config selection - execution mode The application service may choose subprocess/backend/orchestrator execution strategy, but it must not redefine domain semantics. ## 5. Boundary rules for convergence work ### Rule A: result mapping happens once Current code maps `FinalSignal` to dashboard fields inside the analysis subprocess template. That mapping should move behind a single application mapper so REST, WebSocket, export, and persisted task status share one contract. ### Rule B: stage model belongs to application layer Stage names such as `analysts`, `research`, `trading`, `risk`, `portfolio` are delivery/progress concepts, not merge-kernel concepts. Keep them outside `orchestrator/`. ### Rule C: orchestrator stays contract-light `orchestrator/` should continue returning `Signal` / `FinalSignal` and domain metadata. It should not learn about HTTP status, WebSocket payloads, pagination, or UI labels beyond domain rating semantics already present. ### Rule D: transport only renders contracts Routes should call the application service and return the already-shaped DTO/contract. They should not reconstruct `decision`, `quant_signal`, `llm_signal`, or progress math themselves. ## 6. Suggested module split One viable split: ```text web_dashboard/backend/ application/ analysis_service.py live_signal_service.py report_service.py contracts.py mappers.py infra/ task_store.py subprocess_runner.py report_store.py api/ fastapi_routes remain thin ``` This keeps convergence local to backend/application without moving merge logic out of `orchestrator/`. ## 7. Non-goals - Do not move signal merge math into the application service. - Do not turn the application service into a strategy engine. - Do not require frontend-specific field naming inside `orchestrator/`. - Do not block migration on a full rewrite of existing routes. ## 8. Review checklist A change respects this boundary if all are true: - route handlers mainly validate/auth/call service/return contract; - application service owns task lifecycle and contract mapping; - `orchestrator/` remains the only owner of merge semantics; - domain dataclasses can still be tested without FastAPI or WebSocket context. ## 9. Current maintainer guidance When touching backend convergence code, treat these files as the current application-facing boundary: - `web_dashboard/backend/services/job_service.py` - `web_dashboard/backend/services/result_store.py` - `web_dashboard/backend/services/analysis_service.py` - `web_dashboard/backend/api/portfolio.py` If a change adds or removes externally visible fields, update `docs/contracts/result-contract-v1alpha1.md` in the same change set.