Skip to content

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