Overview
This project is a signal-mirroring bridge that turns MetaTrader 4 indicator alerts into live binary options trades on Deriv, without requiring an Expert Advisor, a socket connection, or any modification to the MT4 terminal itself. It's built for a trader running an MT4 indicator that fires native "Alert()" popups on buy/sell conditions — the bridge watches those alert windows directly at the OS level and converts each one into an executed contract on the broker side within milliseconds of the alert appearing.
The standout piece is the ingestion method: instead of listening on a socket for an EA to push signals (the approach used elsewhere in this codebase for MT5), the MT4 gateway enumerates running MetaTrader windows, locates the native Alert dialog for the selected terminal, and reads its SysListView32 rows directly out of the MT4 process's memory using ReadProcessMemory/VirtualAllocEx calls against the Win32 API. This lets it mirror signals from stock or third-party MT4 indicators that were never built with automation in mind.
Architecture
- Alert window poller (
bridge/ingestion/mt4_alert/windows.py,monitor.py) — enumerates MetaTrader windows viawin32gui.EnumWindows, finds the terminal's Alert dialog, and polls its list view every 50ms in a thread-pool executor so the asyncio loop is never blocked by blocking Win32 calls. - Alert receiver (
bridge/ingestion/mt4_alert/receiver.py) — parses raw alert text into a canonicalTradeEventusing configurable buy/sell/ignore keyword lists and a symbol whitelist derived from the user's broker symbol mappings. - Event queue & dispatcher (
bridge/core/queue,bridge/core/events) — an asyncio queue decouples alert detection from execution; a dispatcher routesindicator.signalevents to the execution engine. - Execution engine (
bridge/execution/engine.py) — routes each event through risk validation, stake resolution, martingale state, and out to the registered broker executor(s), with retry and next-candle deferral support. - Deriv executor (
bridge/brokers/deriv/) — WebSocket client, symbol/contract mapper, and a proposal cache that pre-warms price quotes so trade placement can skip Deriv's proposal round-trip. - Persistence — Redis for martingale/pending-trade runtime state and event status, PostgreSQL for execution audit records.
- Web dashboard (
aiohttp, served fromapps/dashboard/static) — terminal selection, gateway start/stop, live config editing, and a rolling activity/log feed, all served from the same process as the gateway.
Implementation
The application entry point (scripts/run_mt4_alert_gateway.py → apps/gateway/mt4_alert_gateway.py) bootstraps a single MT4AlertGatewayApplication dataclass that wires together the queue, dispatcher, risk validator, retry manager, martingale manager, Redis state store, Postgres audit store, and the Deriv executor, then serves an aiohttp app exposing both the dashboard UI and its JSON API (/api/gateway/*, /api/terminals, /api/config, /api/logs).
Domain data flows through immutable Pydantic v2 models (ImmutableModel, frozen + enum-value serialization): a TradeEvent is the canonical signal, a MirrorInstruction is the broker-agnostic normalized execution instruction produced by the OrderRouter, and a BrokerOrder/ExecutionOutcome pair represents the broker-specific payload and its terminal result. The OrderRouter resolves per-account BrokerRoute configuration — symbol mappings, stake mode (fixed stake vs. percentage-of-balance), expiry policy, and martingale config — entirely from config/user-config.json, so behavior can be changed from the dashboard without a code change or restart in most cases (the put_config handler hot-applies keyword lists, routes, and Deriv credentials in place).
Timing is tracked end-to-end: the alert monitor records a t_detect perf-counter timestamp the instant a new alert row is read, the receiver records t_enqueue after the event is parsed, and the worker loop computes queue_wait_ms, broker_ms, and total_ms for every signal, logging them via structlog and surfacing them in the dashboard's activity log (e.g. "Trade placed — EURUSD on deriv (contract 123) [broker: 420ms, total: 610ms]"). This latency instrumentation was clearly built to answer a specific question for a real-money system: how much time elapses between the MT4 alert firing and the Deriv contract being placed.
Signal Detection And Execution Latency
Because MT4's native Alert() dialog was never designed to be polled programmatically, monitor.py implements raw process memory access: it opens the terminal process with PROCESS_ALL_ACCESS, allocates a remote buffer with VirtualAllocEx, sends an LVM_GETITEMTEXTW message to the alert window's list view with a pointer into that remote buffer, then reads the result back with ReadProcessMemory. It handles both 32-bit and 64-bit MT4 processes by detecting WOW64 status and packing the LVITEMW struct differently for each. The alert HWND is cached and only re-resolved via EnumWindows if the dialog closes, keeping steady-state polling cheap at a 50ms interval.
On the execution side, DerivExecutor.warm_proposals() opens persistent proposal subscriptions for every configured symbol × direction (CALL/PUT) combination as soon as the gateway connects, seeding a ProposalCache. When a signal arrives, place_order() checks this cache first and, on a hit, skips Deriv's proposal request entirely and sends only the buy request — cutting the two-step proposal-then-buy round-trip down to one. Proposal warming re-runs automatically whenever the user saves new configuration, so newly added symbols get pre-warmed without a gateway restart.
Risk, Martingale, And Recovery
Every instruction passes through a composable RiskValidator (positive stake, positive expiry, martingale timing filters) before it reaches a broker. Martingale recovery is handled by a dedicated MartingaleManager keyed per routed lane (broker:account:route:symbol), supporting two timing modes — next_signal (arm immediately, apply on the next incoming alert for that lane) and next_candle (wait for a broker candle-boundary event before applying the increased stake). Contract outcomes are tracked via Deriv's contract-outcome subscription; a loss arms the next martingale step, a win clears it. A profit-target watchdog tracks running profit across the session and pauses the trading loop once the configured target is hit, exposing resume/pause state through the dashboard.
Runtime state (armed martingale lanes, active contract subscriptions, pending next-candle trades) is snapshotted to Redis after every state-changing operation and restored on startup via restore_runtime_state(), so an unexpected restart doesn't lose in-flight martingale sequences or pending deferred trades.
Reliability
The repository includes a pytest suite (tests/unit, tests/integration) covering the risk validator, martingale policy, retry policy, order router, Deriv mapper/client/auth, stake resolution, runtime state store, dedupe state, and the execution-outcome contract, run via python -m pytest tests/. Config is Pydantic-validated on load (UserConfig.from_data) with rejection of malformed payloads at the /api/config PUT boundary. Sensitive fields (API tokens, credentials) are redacted before being returned from the config API. A background watchdog reconnects to Deriv and refreshes balance every 30 seconds if the broker connection drops. The project ships PyInstaller specs and a build script (scripts/build_pyinstaller.ps1) producing a standalone Windows console distributable, with a dist/ build present in the repository confirming this has been packaged and run outside a dev environment.


