Full-stack algorithmic trading engine — see Technical Design Document for architecture and details.
Phase 1 is complete. MariaAlpha ships as a working, end-to-end algorithmic trading engine with the following capabilities:
- Live market data ingestion — simulated CSV replay (default) or Alpaca IEX WebSocket, with auto-reconnect and per-symbol in-memory order book
- VWAP execution strategy enhanced by an ML signal — LightGBM classifier (15 technical indicators) called over gRPC with a Resilience4j circuit breaker; strategy hot-swap at runtime via REST
- Risk check chain — five composable checks (max notional, max position, max portfolio exposure, max open orders, daily loss limit) with trading halt and resume
- Dual exchange routing — simulated exchange (configurable fill latency + slippage) or Alpaca paper trading (REST +
trade_updatesWebSocket), switchable viaEXECUTION_PROFILE - Real-time position and P&L tracking — mark-to-market unrealized P&L, portfolio aggregates, fill history, all persisted in PostgreSQL
- Transaction Cost Analysis — slippage vs. arrival mid, implementation shortfall, VWAP benchmark deviation, and spread cost per completed order
- React UI — portfolio dashboard, order entry with WebSocket live updates, daily P&L chart, served from nginx in Docker Compose
- Full observability — Grafana LGTM stack (Alloy, Prometheus, Loki, Tempo, Grafana 11) with a provisioned Trading Pipeline dashboard covering all 7 services
- Production-grade CI — lint, unit, integration, and end-to-end tests gating every pull request; CodeQL and Snyk security scans
The Phase 1 acceptance test — SimulatedHappyPathE2ETest — boots the full Docker Compose stack (15 long-running services + a one-shot Kafka topics initializer) and traverses the complete Tick-to-Trade pipeline on every CI run. See docs/phase-1-completion.md for the full record of what was built.
Phase 2 work has begun landing on main: the Smart Order Router (issue 2.1.1), the advanced order-type handlers IOC / FOK / GTC / Iceberg (2.1.3, 2.1.4), and the umbrella Helm chart for Kubernetes deployment (2.7.1) are all shipped. Remaining Phase 2 items — TWAP / Momentum / Implementation Shortfall strategies, Redis distributed position cache, regime classifier, Playwright UI e2e tests, and the docker image publish workflow — are tracked in §11 of the Technical Design Document.
-
just — command runner used for all project tasks
brew install just # macOS -
Docker — required for infrastructure services
A clean-checkout walkthrough. Tested on macOS 14 (Apple Silicon) and Ubuntu 22.04 with Docker Desktop ≥ 25.0 and 8 GB RAM allocated.
git clone https://github.com/drag0sd0g/MariaAlpha.git
cd MariaAlpha
cp .env.example .env
# .env defaults are fine for local dev. For docker-compose, changing
# MARIAALPHA_API_KEY requires rebuilding the UI image
# (`docker compose build ui && docker compose up -d ui`) because Vite bakes the
# value into the bundle at build time. The Helm chart avoids this — the UI's
# init container renders the key into /config.js at pod start, so secret
# rotation is a `helm upgrade` + pod restart with no image rebuild.just buildThis compiles every Java module, builds the React UI bundle, runs static checks
(Spotless, Checkstyle, SpotBugs, ESLint, Prettier, ruff, mypy), and runs every unit
and Testcontainers integration test except e2e. Expected runtime: ~5 minutes on a
warm Gradle cache.
just runRuns docker compose up -d and starts 15 long-running containers plus a
one-shot kafka-init container that creates topics and exits. First-time runs
build all service images (~3 minutes); subsequent runs reuse the cache. Wait
~90 seconds after just run returns for every service to become healthy.
just verifyExpected output:
Polling /actuator/health on every service...
✓ market-data-gateway
✓ strategy-engine
✓ execution-engine
✓ order-manager
✓ post-trade
✓ api-gateway
✓ ml-signal-service
✓ ui
✓ grafana
| URL | What |
|---|---|
| http://localhost:5173/ | MariaAlpha UI — Dashboard + Order Entry |
| http://localhost:3001/ | Grafana (anonymous admin, no login) |
| http://localhost:9090/ | Prometheus |
| http://localhost:8080/actuator/health | API Gateway aggregate health |
An alternative to docker-compose: deploy the same stack to a local Kubernetes cluster via the umbrella Helm chart in charts/mariaalpha/.
brew install helm # one-time
orb start k8s # OrbStack ships a single-node cluster
just k8s-up # build images, helm dep update, helm install (≈3 min cold)
just k8s-test # helm test: actuator-health + iceberg-parent → FILLED
open http://mariaalpha.orb.local # UI
open http://grafana.mariaalpha.orb.local # Grafana dashboards
just k8s-down # uninstall + drop PVCs (dev-cluster only)Detailed install/upgrade/rollback recipes live in docs/runbooks/helm-install.md. Secret rotation is documented in docs/runbooks/helm-rotate-secrets.md.
Every API call requires the X-API-Key header. The default key is local-dev-key.
# List all available strategies.
curl -fsS -H "X-API-Key: local-dev-key" \
http://localhost:8080/api/strategies
# Bind VWAP to AAPL.
curl -X PUT -H "X-API-Key: local-dev-key" \
-H "Content-Type: application/json" \
-d '{"strategyName":"VWAP"}' \
http://localhost:8080/api/strategies/AAPL
# Configure VWAP — 100 shares spread evenly between 14:30 and 16:00 NY time.
curl -X PUT -H "X-API-Key: local-dev-key" \
-H "Content-Type: application/json" \
-d '{
"targetQuantity": 100,
"side": "BUY",
"startTime": "14:30:00",
"endTime": "16:00:00",
"volumeProfile": [
{"startTime":"14:30:00","endTime":"16:00:00","volumeFraction":1.0}
]
}' \
http://localhost:8080/api/strategies/VWAP/parameters
# Place a manual LIMIT order.
curl -X POST -H "X-API-Key: local-dev-key" \
-H "Content-Type: application/json" \
-d '{"symbol":"AAPL","side":"BUY","orderType":"LIMIT","quantity":10,"limitPrice":180.00}' \
http://localhost:8080/api/execution/orders
# Read positions and portfolio summary.
curl -fsS -H "X-API-Key: local-dev-key" http://localhost:8080/api/positions
curl -fsS -H "X-API-Key: local-dev-key" http://localhost:8080/api/portfolio/summaryjust stopTo wipe all state (Postgres, Kafka logs, Grafana dashboards):
docker compose down -v| Symptom | Likely cause | Fix |
|---|---|---|
MARIAALPHA_API_KEY must be set on just run |
.env not copied |
cp .env.example .env |
port 5432 already in use |
local Postgres already running | stop it or change POSTGRES_PORT_HOST in .env |
ui container restarting |
stale build args (e.g. API key changed) | docker compose build --no-cache ui |
| UI loads but API calls return 401 | UI bundle built with a different key than the gateway expects | rebuild: docker compose build ui && just run |
tradingHalted: true immediately |
daily-loss limit tripped from a prior run | curl -X POST -H "X-API-Key: $KEY" http://localhost:8080/api/execution/resume |
| No data on UI dashboard | strategy-engine has no symbols routed yet | PUT /api/strategies/AAPL (see step 6) |
| Service | Port | Notes |
|---|---|---|
| PostgreSQL 16 | 5432 | Credentials via .env |
| Kafka (KRaft) | 9092 | Single-node, no ZooKeeper |
| Prometheus | 9090 | Metrics storage, remote-write enabled |
| Grafana | 3001 | Dashboards — anonymous admin access |
| Alloy | 12345 | Telemetry collector UI; OTLP on 4317/4318 |
| UI (nginx) | 5173 | Static SPA + reverse-proxy to api-gateway |
Loki (logs) and Tempo (traces) run within the Docker network, reachable by Alloy and Grafana.
Single front door for the React UI and external clients. Implements:
- REST routing to all backend services (
/api/...). - API key authentication via
X-API-Keyheader (or?apiKey=query parameter for browser WebSocket clients). - Real-time WebSocket fan-out from Kafka to UI clients.
| Env var | Required | Description |
|---|---|---|
MARIAALPHA_API_KEY |
yes | Shared secret. Without it the gateway rejects every request with HTTP 401. |
KAFKA_BOOTSTRAP_SERVERS |
yes | Kafka cluster (default localhost:9092). |
STRATEGY_ENGINE_URL |
optional | Default http://localhost:8082. |
ORDER_MANAGER_URL |
optional | Default http://localhost:8086. |
EXECUTION_ENGINE_URL |
optional | Default http://localhost:8084. |
POST_TRADE_URL |
optional | Default http://localhost:8088. |
ANALYTICS_SERVICE_URL |
optional | Default http://localhost:8095. |
MARKET_DATA_GATEWAY_URL |
optional | Default http://localhost:8079. |
export MARIAALPHA_API_KEY=local-dev-key
just run
./gradlew :api-gateway:bootRunThe web UI is a Vite + React 18 + TypeScript single-page app. The Phase-1 MVP exposes a Dashboard (live positions, P&L, exposure) and an Order Entry page (manual order submission, active orders, fills). Five additional pages (Market Data, RFQ, Strategies, Analytics, Reconciliation) are scaffolded as placeholders for Phase 2.
just run # bring up infra + backend services
cp ui/.env.example ui/.env.local
echo "VITE_MARIAALPHA_API_KEY=local-dev-key" >> ui/.env.local
just ui-install # one-time: npm install
just ui-dev # opens http://localhost:5173The dev server proxies /api/* and /ws/* to http://localhost:8080 (the API Gateway), so no CORS configuration is needed.
Env var (in ui/.env.local) |
Required | Description |
|---|---|---|
VITE_MARIAALPHA_API_KEY |
yes | Must match the key in repo-root .env. |
VITE_API_BASE_URL |
no | Leave blank for dev (uses Vite proxy). Set to http://localhost:8080 only when running vite preview against a built bundle. |
just ui-build # static assets in ui/dist/
cd ui && npm run preview # serves dist/ on http://localhost:4173For docker-compose deployments the UI runs as an nginx container exposed on port 5173 (see docker-compose.yml). The image is built from ui/Dockerfile.
Manual orders submitted via the Order Entry page hit POST /api/execution/orders (not /api/orders). The distinction matters for external clients:
| Method | Path | Backend | Notes |
|---|---|---|---|
POST |
/api/execution/orders |
execution-engine | Submit a manual order; returns {orderId, status, acceptedAt} |
DELETE |
/api/execution/orders/{orderId} |
execution-engine | Cancel a manual order; 204 on success, 404 if unknown/terminal |
Liquibase migrations run automatically on Spring Boot service startup. Verify schema:
docker compose exec postgres psql -U mariaalpha -c '\dt'The Grafana LGTM stack (Loki, Grafana, Tempo, Mimir/Prometheus) starts with just run. Grafana is pre-configured with all datasources and available at http://localhost:3001.
graph LR
subgraph Application Services
J[Java Services<br/>Micrometer + OTLP]
P[Python Services<br/>prometheus_client + OTLP]
end
subgraph Observability Stack
AL[Grafana Alloy<br/>:12345]
PROM[Prometheus<br/>:9090]
LOKI[Loki]
TEMPO[Tempo]
GF[Grafana<br/>:3001]
end
subgraph Infrastructure
PG[(PostgreSQL<br/>:5432)]
KF[Kafka<br/>:9092]
end
J -->|/metrics scrape| AL
P -->|/metrics scrape| AL
J -->|OTLP traces :4317| AL
P -->|OTLP traces :4318| AL
AL -->|remote write| PROM
AL -->|push| LOKI
AL -->|OTLP forward| TEMPO
PROM --> GF
LOKI --> GF
TEMPO --> GF
Alloy is the unified telemetry collector — it scrapes Prometheus-format metrics endpoints and forwards them to Prometheus via remote write, receives OTLP traces (gRPC :4317, HTTP :4318) and forwards to Tempo, and pushes logs to Loki. Grafana queries all three backends and supports cross-linking between traces, logs, and metrics.
GitHub Actions workflows run on every push and PR to main:
| Workflow | File | What it checks |
|---|---|---|
| CI | ci.yml |
Java: Spotless, Checkstyle, SpotBugs, tests + JaCoCo. Python: ruff, mypy, pytest. UI: ESLint, Prettier, tsc. |
| CodeQL | codeql.yml |
Security analysis for Java, Python, TypeScript (also runs weekly). |
| Snyk | snyk.yml |
Dependency vulnerability scanning (requires SNYK_TOKEN secret). |
| PR Metadata | pr-metadata.yml |
Auto-populates labels, milestone, assignee, and project from linked issues. |
Python and UI jobs skip automatically when no source files exist yet. JaCoCo and test reports are uploaded as build artifacts. Snyk requires a SNYK_TOKEN repository secret — obtain one from snyk.io.
Direct pushes to main are not allowed — all changes go through pull requests. The Java (lint + test) and CodeQL (java-kotlin) checks must pass before merging. Branches are auto-deleted after merge.