01 — System Architecture¶
1. Shape of the system¶
HalalTrade AI is a modular monolith with detached workers, not a microservice mesh. One deployable Python application exposes the API and hosts the domain modules; long-running and scheduled work runs as separate processes against the same codebase; the browser client talks only to the API.
This is a deliberate choice for a single-user, self-hosted system. Microservices would buy independent scaling we do not need and cost us distributed transactions, N deployment units, and cross-service debugging on a machine that may be a laptop. Module boundaries are enforced in-process by import rules and explicit interfaces, so the seams for later extraction exist without the operational tax now. See ADR-0001.
graph TB
subgraph Client["Client (browser)"]
WEB["Web App<br/>React + TypeScript SPA"]
end
subgraph Edge
API["API Layer<br/>FastAPI · REST + SSE + WS"]
AUTH["Auth & Session<br/>Passkey / device-bound"]
end
subgraph Domain["Domain Modules (in-process)"]
REC["Recommendation<br/>Service"]
SHR["Shariah<br/>Engine"]
PORT["Portfolio &<br/>Paper Broker"]
RISK["Risk<br/>Engine"]
BT["Backtest<br/>Service"]
CHAT["Assistant<br/>Service"]
end
subgraph Agents["Agent Runtime"]
ORCH["Orchestrator<br/>LangGraph"]
AG["12 Specialist Agents"]
AGG["Deterministic<br/>Aggregator"]
LLM["Model Gateway<br/>local ⇄ cloud routing"]
end
subgraph Data["Data Platform"]
ING["Ingestion Workers"]
FS["Feature Store<br/>point-in-time"]
VEC["Vector Store<br/>Qdrant"]
TS[("TimescaleDB<br/>prices, macro")]
PG[("PostgreSQL<br/>domain state")]
OBJ[("Object Store<br/>raw docs, parquet")]
CACHE[("Redis<br/>cache, locks")]
end
subgraph Ext["External (all optional)"]
MKT["Market data<br/>providers"]
FIL["EDGAR / filings"]
MAC["FRED / macro"]
NEWS["News & social"]
CLOUD["Cloud LLM APIs"]
end
WEB --> API
API --> AUTH
API --> REC & PORT & RISK & BT & CHAT
REC --> SHR
REC --> ORCH
ORCH --> AG --> LLM
ORCH --> AGG
AGG --> REC
REC --> RISK
PORT --> RISK
CHAT --> VEC & LLM
BT --> FS
AG --> FS & VEC
SHR --> FS
ING --> TS & PG & OBJ & VEC
FS --> TS & PG
ING -.pull.-> MKT & FIL & MAC & NEWS
LLM -.optional.-> CLOUD
Domain --> PG
Domain --> CACHE
2. Component responsibilities¶
2.1 API Layer¶
FastAPI. Serves REST for CRUD and queries, Server-Sent Events for streaming assistant responses and live recommendation progress, and WebSocket for price/portfolio push. It also serves the web client's built static bundle, so the whole system is one process on one origin — no CORS, no second server. Generates an OpenAPI schema from which the client's TypeScript types are code-generated, so contract drift is a build failure rather than a runtime surprise.
It is a thin adapter: validation, authz, serialisation, rate limiting. No business logic. Full surface in 13 — API Contracts.
2.2 Recommendation Service¶
Owns the lifecycle of a recommendation:
candidate → compliance gate → agent fan-out → aggregation
→ risk sizing → explanation → persistence → notify
It is the only component permitted to construct a Recommendation. It refuses to emit one when the compliance gate is not COMPLIANT, when required features are stale beyond their configured tolerance, or when the risk engine returns a zero position size.
2.3 Shariah Engine¶
Deterministic rule evaluation over point-in-time fundamentals and a business-activity classification. Returns a verdict, the full ratio working, the inputs with as-of dates, and the rule pack version. Called before any agent work is spent on a candidate — compliance failure is the cheapest possible early exit. Detailed in 05.
2.4 Agent Runtime¶
A LangGraph state graph. Analyst nodes fan out in parallel, each producing a schema-validated Signal. A deterministic aggregator combines signals into a score. The Supervisor node writes the explanation and holds a veto. Nodes that consume untrusted external text run in the quarantine sub-graph with no tool access. Detailed in 04.
2.5 Model Gateway¶
The only component that talks to a language model. Responsibilities:
- Routing by task tier — cheap local model for classification and extraction, strong model for synthesis and explanation.
- Provider abstraction — one interface over local (Ollama / vLLM / llama.cpp) and cloud (Anthropic, and any OpenAI-compatible endpoint), so the local path is a config change rather than a rewrite.
- Data-minimisation enforcement — strips or refuses to transmit portfolio-identifying fields to external providers unless the corresponding setting is on. This is enforced here, at one chokepoint, rather than trusted to every caller.
- Caching, retry, budget accounting, and prompt/version hashing for reproducibility.
2.6 Portfolio & Paper Broker¶
The simulated exchange and account ledger. Double-entry positions, order state machine, fill simulation, corporate action processing, performance attribution. It exposes the same interface a live broker adapter would implement in Phase 5 — but the live implementation does not exist yet. Detailed in 06.
2.7 Risk Engine¶
Called twice: pre-trade (sizing and limit checks, with veto authority) and continuously (portfolio-level monitoring, breach detection, circuit breakers). Pure functions over portfolio state and market data, which makes it exhaustively testable. Detailed in 07.
2.8 Backtest Service¶
Replays the point-in-time feature store through the same strategy and risk code the live path uses. Sharing that code is the point: a backtest that runs a parallel implementation tests the wrong thing. Detailed in 08.
2.9 Data Platform¶
Ingestion workers (scheduled and streaming), point-in-time storage, feature computation, and the document/vector corpus that grounds every citation. Detailed in 03.
3. Data flow: how a recommendation is produced¶
sequenceDiagram
autonumber
participant S as Scheduler
participant R as Recommendation Svc
participant SH as Shariah Engine
participant O as Orchestrator
participant A as Analyst Agents
participant G as Aggregator
participant SV as Supervisor
participant RK as Risk Engine
participant DB as Store
S->>R: screen universe (daily, pre-open)
R->>SH: batch compliance check
SH-->>R: compliant subset + verdicts
Note over R,SH: non-compliant candidates exit here<br/>with a stored, explainable rejection
R->>R: rank by liquidity + feature freshness
R->>O: analyse top-N candidates
par fan-out
O->>A: Technical
O->>A: Fundamental
O->>A: News (quarantined)
O->>A: Social (quarantined)
O->>A: Macro
O->>A: Market Analyst
end
A-->>O: Signal[] (schema-validated, evidence-cited)
O->>G: signals + regime context
G-->>O: score, confidence, contribution breakdown
O->>SV: signals + score + evidence
SV-->>O: explanation, or VETO + reason
O-->>R: draft recommendation
R->>RK: size and check limits
RK-->>R: qty, stop, target — or reject
R->>DB: persist with full audit manifest
R-->>S: notify user
The ordering is load-bearing. Compliance runs first because it is deterministic, cheap, and terminal. Risk runs last because it needs the final conviction to size against, and because a signal the portfolio cannot safely hold should still be recorded as a signal.
4. Deployment topologies¶
One codebase, three profiles, selected by configuration.
| Solo (laptop) | Home server (reference) | Private cloud | |
|---|---|---|---|
| Compute | 8–16 GB RAM, integrated GPU or none | 32–64 GB, consumer GPU optional | 8–16 vCPU VPS |
| Datastore | Postgres (single container) | Postgres + TimescaleDB, Redis, Qdrant, MinIO | Same, with managed volumes |
| Models | Local 7–14B quantised, via Ollama | Local for tier-1, cloud for tier-3 synthesis | Cloud-primary, local fallback |
| Universe | ~300 large-cap symbols | 2,000–5,000 symbols | Full covered universe |
| Cadence | Daily, on demand | Daily + intraday news polling | Continuous |
| External AI | Optional, off by default | Optional | Optional |
Orchestration is Docker Compose everywhere. Kubernetes is not recommended and not planned; it solves multi-tenant scheduling problems this system does not have.
Degradation is designed, not accidental: if the cloud model is unavailable, the gateway routes to local and the recommendation is tagged with the model actually used. If a data provider is down, affected features are marked stale and any agent depending on them abstains — which lowers aggregate confidence rather than silently using yesterday's number.
5. Cross-cutting concerns¶
Time. Every stored fact carries three timestamps: event_time (when it happened in the world), available_at (when we could first have known it), and ingested_at (when we stored it). Backtests and features query on available_at. This single convention is what makes look-ahead bias structurally hard rather than a matter of discipline. See 03 §4.
Idempotency. Ingestion and pipeline steps are keyed by (source, entity, event_time, version) and are safe to replay. Recovery is "run it again."
Auditability. Every recommendation persists a manifest: input feature IDs and values, source document IDs, rule pack version, model IDs, prompt template hashes, aggregator weights version, and code commit. Reproducibility is a test, run in CI on a golden set.
Observability. Structured logs, OpenTelemetry traces across the agent graph, and Prometheus metrics — all local, all off-network. The agent graph trace is also a user-facing feature: the "show your work" view in the app is a rendering of the same trace.
Configuration. Layered: defaults in code → profile file → environment → user settings in DB. Secrets never enter the DB layer; they come from the OS keychain or an age-encrypted file. See 10.
6. Proposed repository layout¶
halaltrade/
├── apps/
│ ├── api/ # FastAPI app: routers, schemas, deps
│ ├── worker/ # scheduled + streaming ingestion, pipeline runners
│ └── web/ # React SPA; built bundle served by the API
├── packages/
│ ├── domain/ # entities, value objects, ports. Zero infra imports.
│ │ ├── recommendation/
│ │ ├── portfolio/
│ │ ├── risk/
│ │ └── shariah/
│ ├── agents/ # graph, nodes, prompts, output schemas
│ ├── modelgw/ # provider abstraction, routing, redaction, budgets
│ ├── data/ # sources, normalisation, feature store, PIT queries
│ ├── backtest/ # engine, metrics, walk-forward harness
│ └── shared/ # config, logging, tracing, time, ids
├── infra/
│ ├── compose/ # per-profile docker-compose
│ └── migrations/ # database migrations
├── evals/ # agent eval suites, golden sets, red-team corpus
├── docs/
└── tests/
The rule that keeps this honest: packages/domain may not import from packages/data, packages/agents, or apps/. Dependencies point inward. This is checked in CI with an import linter, because architectural rules that are not enforced are aspirations.
7. Failure modes and responses¶
| Failure | Response |
|---|---|
| Market data provider down | Serve last-known with staleness flag; agents depending on it abstain; universe screening pauses rather than running on stale prices |
| Cloud LLM unavailable / over budget | Route to local model; tag recommendation with actual model; never fail the request |
| Local model OOM | Fall back to smaller tier model; if unavailable, return UNCERTAIN with cause |
| Fundamentals stale beyond tolerance | Compliance verdict becomes UNCERTAIN → candidate excluded, with reason shown |
| Agent returns unparseable output | One reprompt with schema violation detail; on second failure the agent abstains and is logged. Never coerce or guess a value |
| Aggregator confidence below floor | No recommendation emitted; candidate recorded as "watched, inconclusive" |
| Risk limit breach detected | Circuit breaker per 07 §6; new entries blocked, user notified |
| Database unavailable | Read-only degraded mode; no orders accepted, no recommendations persisted |