This document describes the current architecture of the TraderX trading platform. TraderX is a legacy monolithic application built as a single Python/FastAPI process serving all trading functionality. It was consolidated from a distributed microservices architecture into a single deployable unit and carries significant technical debt by design.
TraderX is a single Python/FastAPI monolith that serves all trading functionality through one process:
- Account management — create, update, and query trading accounts and account users
- Trade submission and processing — submit trade orders, validate, process, and settle trades synchronously
- Position tracking — maintain and query net holdings per account and security
- Reference data — S&P 500 stock ticker lookup from a CSV file loaded into memory
- People directory — user/person lookup from a JSON file loaded into memory
- Real-time updates — Socket.io server embedded in the same ASGI process for pushing trade and position updates to connected frontends
The backend runs on uvicorn (ASGI) at port 8000 by default. The frontend is a separate React application served on port 3000.
┌─────────────────────────────────────────────────────────────────┐
│ Browser │
│ (React UI :3000) │
└──────────┬──────────────────────────────────┬───────────────────┘
│ HTTP (REST API) │ WebSocket
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ FastAPI Monolith (:8000) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ /account/* │ │ /trade/ │ │ /positions/* │ │
│ │ /accountuser/│ │ /trades/* │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └───────────┬───────────┘ │
│ │ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ │ │
│ │ account_ │◄►│ trade_ │──────────────┘ │
│ │ service.py │ │ processor.py │ (cross-domain queries) │
│ └──────────────┘ │ (god service)│ │
│ │ 1047 lines │ │
│ ┌──────────────┐ └──────┬───────┘ ┌───────────────────────┐ │
│ │ /stocks/* │ │ │ Socket.io Server │ │
│ │ /people/* │ ├─────────►│ (embedded in process) │ │
│ │ (ref data + │ │ │ real-time push │ │
│ │ people svc) │ │ └───────────────────────┘ │
│ └──────────────┘ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ SQLAlchemy ORM + SQLite │ │
│ │ ┌──────────┐ ┌──────────────┐ ┌────────┐ ┌───────────┐ │ │
│ │ │ accounts │ │ account_users│ │ trades │ │ positions │ │ │
│ │ └──────────┘ └──────────────┘ └────────┘ └───────────┘ │ │
│ │ (all tenants in same tables via tenant_id) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Global Config (config.py) │ │
│ │ Mutable runtime state, tenant rules, env vars │ │
│ │ Imported via `from app.config import *` everywhere │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
graph TD
Browser["Browser (React UI :3000)"]
Monolith["FastAPI Monolith (:8000)"]
SQLite["SQLite Database (traderx.db)"]
SocketIO["Socket.io (embedded)"]
Browser -->|"REST API (HTTP)"| Monolith
Browser <-->|"WebSocket"| SocketIO
Monolith --> SQLite
Monolith --> SocketIO
SocketIO -->|"trade & position updates"| Browser
The application uses a single SQLite database (traderx.db) accessed through SQLAlchemy ORM. All tenants share the same tables, separated only by a tenant_id column.
| Table | Primary Key | Tenant Isolation |
|---|---|---|
accounts |
id (auto-increment) |
tenant_id column |
account_users |
composite (account_id, tenant_id, username) |
tenant_id column |
trades |
id (auto-increment) |
tenant_id column |
positions |
composite (account_id, tenant_id, security) |
tenant_id column |
Three demo tenants are pre-seeded: acme_corp, globex_inc, initech. The active tenant is determined by the X-Tenant-ID HTTP header, falling back to the DEFAULT_TENANT environment variable (default: acme_corp). There is no row-level security, no schema isolation, and no runtime isolation between tenants.
The trade_processor.py god service directly queries account tables to validate trades:
validate_account_exists()— queries theaccountstable directly instead of calling account_servicevalidate_account_has_users()— queries theaccount_userstable directlyget_account_portfolio_summary()— joins data fromaccounts,trades, andpositionstables in a single cross-domain aggregation
trade_processor.py and account_service.py have a circular dependency:
trade_processorimportsAccountandAccountUsermodels for cross-domain queriesaccount_serviceimportscount_trades_for_accountfromtrade_processor(via lazy import to avoid import-time failure)
Some route handlers use the service layer, others bypass it with inline SQLAlchemy queries:
GET /trades/uses a raw inline query (bypassestrade_processor.get_all_trades())GET /trades/{account_id}uses the service layer (trade_processor.get_trades_for_account())GET /positions/*endpoints use raw inline queries (bypasses trade_processor position functions)GET /account/{account_id}directly queries the positions table for portfolio summary
The trade_processor.py module is 1,047 lines and handles nearly all business logic:
| Responsibility | Functions |
|---|---|
| Trade validation | validate_trade_request(), validate_account_exists(), validate_account_has_users(), validate_security_exists() |
| Trade processing | process_trade() — the main orchestration function |
| Trade state machine | can_transition(), transition_trade_state() |
| Position management | update_position(), get_current_position_quantity(), get_positions_for_account(), get_all_positions() |
| Socket.io publishing | publish_trade_update(), publish_position_update(), publish_trade_and_position() |
| Reporting/aggregation | get_account_portfolio_summary(), get_tenant_trading_summary(), get_trade_volume_by_date(), get_top_traders(), get_position_concentration() |
| Tenant-specific rules | get_max_accounts_for_tenant(), check_tenant_account_limit(), get_tenant_trade_restrictions(), apply_tenant_specific_rules() |
| Maintenance operations | settle_pending_trades(), cancel_stale_trades(), recalculate_positions() |
| Audit/history | get_recent_trades_audit(), get_trade_history() |
| Cross-domain helpers | get_account_details_for_trade(), validate_user_can_trade(), get_account_display_name() |
There are no Dockerfiles, no docker-compose.yml for the monolith, and no container build pipeline. The application runs directly on the host machine using Python and Node.js runtimes.
Deployment is handled via deploy.sh, a shell script that:
- Installs Python dependencies with
pip install - Runs database seeding
- Starts the backend with
python run.py - Installs frontend dependencies and builds the React app
- Serves the frontend with
npx serve
There is no blue-green deployment, no rollback mechanism, and no health check verification in the deployment process.
There are no GitHub Actions workflows, no automated test pipeline, no linting checks, and no automated deployment triggers for the monolith. All testing and deployment is manual.
| Layer | Isolation |
|---|---|
| Runtime | None — single process serves all tenants |
| Database | None — single SQLite file, shared tables, tenant_id column only |
| Configuration | None — shared config.py with global mutable state |
| Network | None — single port, single process |
| Secrets | None — all tenants share the same environment variables |
The X-Tenant-ID header is the only mechanism for identifying the current tenant. There is no authentication, no authorization, and no validation that a request is allowed to access a given tenant's data.
The following anti-patterns are intentional and present in the codebase by design:
- Every module imports config via
from app.config import * CURRENT_TENANTis a mutable global variable modified at runtime viaset_current_tenant()_runtime_statedict is mutated from multiple modules (total_trades_processed,last_trade_timestamp, etc.)KNOWN_TENANTSlist is appended to at runtime when new tenants are encountered
- Some endpoints use
account_serviceandtrade_processorfunctions - Other endpoints issue raw SQLAlchemy queries inline in route handlers
- No consistent pattern for when to use vs. bypass the service layer
trade_processor.pycombines validation logic, state transitions, position calculations, Socket.io publishing, reporting queries, and audit logging in a single module- No separation between domain logic and persistence layer
- Trade state machine, position arithmetic, and database operations are interleaved in
process_trade()
trade_processor.pyimports models used byaccount_service.pyaccount_service.pylazily importscount_trades_for_accountfromtrade_processor.py- Resolved at runtime but creates fragile import ordering requirements
TENANT_MAX_ACCOUNTS,TENANT_ALLOWED_SIDES,TENANT_AUTO_SETTLEare defined as dicts inconfig.py- Business rules are coupled to configuration rather than being encapsulated in domain logic
- Adding a new tenant requires modifying the config module
- Socket.io publish failures are caught but silently logged
- Database errors in seeding propagate as unhandled exceptions
- No circuit breaker, retry logic, or graceful degradation
- SQLite is a single-writer, file-based database not designed for concurrent multi-tenant production workloads
- No connection pooling, no read replicas, no backup strategy