How to Replace a WebSocket EA with TradeMux REST in MetaTrader 5
Introduction
If you have read the previous article about AI Hedge Fund v4, you know where we left off. A 15-member council: 10 analysts, 4 risk managers, and a Chair; eight currency pairs; a Sharpe ratio of 6.31 and a Profit Factor of 2.09 based on an eight-week backtest. The system ran on a single MetaTrader 5 terminal via a WebSocket Expert Advisor (EA).
After that article was published, most of the questions boiled down to one thing: Why go to the trouble of setting up a 15-member council when you can just use a ready-made trading signal service? The answer became clearer during those eight weeks of operation. It is not about the number of council members, but about the fact that a system with a reputation engine learns from its own mistakes as it trades. Victor's rise from a reputation score of 50 on Monday to 68 by Friday is more than just a number in memory — it represents accumulated information about whom to trust more and on which trading instrument. It was precisely this information that turned out to be a vulnerability in the v4 architecture.
Algorithmic trading isn't about "writing the right code once"; it is about the system's resilience to restarts, broker changes, and new requirements. In v4, the “intelligence” problem had been solved: 15 agents make decisions based on weighted reputation scores, but the infrastructure remained fragile: one process, one EA, one terminal, and one broker. In production, this fragility inevitably makes itself felt sooner or later. The problem is not the quality of the code. Every server reboots, brokers have maintenance periods, and then a second user comes along who needs access to the same signals.
At the end of that article, three specific limitations were candidly acknowledged. This article explains how to remove these limitations not by rewriting the system from scratch, but by adding two components to what already works. If you have not read the previous parts of this series, that is okay; I will explain as we go. But if you have read the previous articles, you will immediately see what has changed and why.
Where to get an API key and documentation: All the necessary resources (registration, pricing, developer console) are available at www.trademux.io. You will also find the latest SDK documentation there.
This article systematically examines the following:
- three issues with the v4 architecture and their causes;
- a new execution layer — a cloud gateway instead of a WebSocket-based EA;
- a persistent reputation engine based on SQLite — a complete implementation;
- an isolated per-symbol context and a motivation engine;
- prompts for the 15-member council and the voting logic;
- the main system loop — putting it all together;
- deployment to a VPS, monitoring via Telegram, SQL analytics;
- comparison of v4 and v5, practical recommendations.
Three Issues with Version v4
The first problem is amnesia upon restart. Victor earned a reputation score of 78 over three weeks of operation. The server went down; Victor's reputation score is back to 50. Three weeks of learning have been lost. The reputation engine in v4 was a Python dictionary stored in the process's memory. This meant that whenever the process terminated — whether as planned or due to a crash — all the accumulated information about the analysts’ reliability was erased. The system was not just losing numbers; it was losing several weeks of adaptation to a specific trading instrument, a specific broker, and a specific market regime. Every Monday after the weekend restart, the council would start from scratch — as if those three weeks had never happened.
The second problem is the EA as an unnecessary link in the chain. In v4, the data flowed through the following chain: MetaTrader 5 → EA → WebSocket → Python → WebSocket → EA → order. Four hops instead of two. Every hop is a potential point of failure, adds latency, and introduces an extra component that needs to be maintained. If you change the entry logic, recompile the MQL5 code and restart the EA. If an error occurs in Python, the EA hangs while waiting for a response over WebSocket. When the Python server is restarted, the WebSocket connection is broken and the EA loses its connection. These are not just theoretical risks; all of this happened during eight weeks of operation.
The third problem is the single-broker limitation. The council made the following decision: BUY EURUSD. This decision was sent to exactly one MetaTrader 5 account. The v4 architecture cannot transmit a signal to multiple terminals simultaneously. If a partner wants to connect their account to the same signals, they need to run a second instance of the entire system: a second Python process, a second EA, and a second WebSocket server. This is not scalable. But it is precisely the ability to manage multiple accounts that is one of the main advantages of an algorithmic trading system over manual trading.
All three issues are resolved in v5. Let's go through them in order.
What Is Changing in the Architecture — and What Is Staying the Same
The good news is: we are not touching any of the system's intelligence. Ten analysts with their prompts, four risk managers, and the Chair with a temperature of 0.15 and a three-step voting procedure — all of this works exactly as it did in v4. The code for the 15-member council is copied without a single change.
Only two components change: how reputation scores are stored (SQLite instead of process memory) and how trading decisions are executed in the market (Python REST via the TradeMux SDK instead of WebSocket via the MQL5 EA).
The v4 architecture consists of a chain of four hops:
[MT5 Terminal]
↕ WebSocket (winhttp.mqh)
[AIHedgeFund_v4.mq5]
→ copy_rates() // prices → Python
→ SendRawCmd("COUNCIL") // signal → Python
← {"signal":"buy",...}
→ Trade.Buy() // order → broker
→ SendRawCmd("RESULT") // result → Python → reputation scores The v5 architecture — a single Python process manages multiple terminals via a single SDK call:
[MT5 Terminal — Broker A] [MT5 Terminal — Broker B]
Bridge EA ←──────────────────── Bridge EA
↑ ↑
└──────── TradeMux Cloud ──────────┘
↑
// Python TradeMux SDK
↑
[Python Server v5]
→ mt5.copy_rates() // data — local MT5 SDK
→ Council of 15 → // council signal
→ client.buy_market() / sell_market() → // both terminals at the same time
← callback (via close_trade) → // result → reputation scores → SQLite The execution layer uses the official TradeMux Python package (available via `pip install trademux`). It communicates with the cloud gateway, while the EA component (installed in each terminal) maintains a persistent connection. Python communicates via the SDK; a single call sends a signal to all connected terminals at the same time.
Installing Bridge EA involves four steps:
- Install TradeMux via the MQL5 Market.
- Enable Allow Algorithmic Trading and add https://mux.skybluefin.tech to the list of allowed URLs for WebRequest.
- Launch the EA on the chart and enter the API key (available in the TradeMux console).
- Repeat for each terminal.
Persistent reputation scores — SQLite
The reputation engine in v4 is a Python dictionary stored in process memory. The dictionary dies along with the process. SQLite is the obvious choice: one dependency, one file, zero configuration, works everywhere. The database schema covers three functions: reputation storage for analysts, a trade log for analytics, and a summary for each symbol.
# persistent_reputation.py import sqlite3 from datetime import datetime from typing import Dict, Optional class PersistentReputationEngine: #--- Constants from v4 — unchanged REWARD_CORRECT = +8.0 REWARD_CORRECT_BIG = +15.0 PENALTY_WRONG = -6.0 PENALTY_WRONG_BIG = -12.0 REWARD_STREAK_BONUS = +3.0 PENALTY_STREAK_MULT = 1.5 REPUTATION_DECAY = 0.995 def __init__(self, db_path: str = "fund_memory.db"): self.db_path = db_path self._init_schema() def _init_schema(self): with sqlite3.connect(self.db_path) as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS analyst_reputation ( pair TEXT NOT NULL, analyst TEXT NOT NULL, reputation REAL DEFAULT 50.0, streak INTEGER DEFAULT 0, total_calls INTEGER DEFAULT 0, correct INTEGER DEFAULT 0, style_override TEXT, updated_at TEXT, PRIMARY KEY (pair, analyst) ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS trade_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, pair TEXT, side TEXT, pnl REAL, broker TEXT, fill_price REAL, latency_ms INTEGER, signal_json TEXT, closed_at TEXT ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS fund_state ( pair TEXT PRIMARY KEY, total_pnl REAL DEFAULT 0.0, day_pnl REAL DEFAULT 0.0, last_day TEXT ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_trade_log_date ON trade_log (closed_at) """) def load_analysts(self, pair: str) -> Dict: with sqlite3.connect(self.db_path) as conn: rows = conn.execute( """SELECT analyst, reputation, streak, total_calls, correct, style_override FROM analyst_reputation WHERE pair = ?""", (pair,) ).fetchall() if not rows: return { name: {"reputation": 50.0, "streak": 0, "total_calls": 0, "correct": 0, "style_override": None, "last_vote": None} for name in [ "victor", "maria", "elena", "dmitri", "chen", "isabella", "marcus", "yuki", "rafael", "sophie" ] } return { analyst: {"reputation": rep, "streak": streak, "total_calls": calls, "correct": correct, "style_override": style, "last_vote": None} for analyst, rep, streak, calls, correct, style in rows } def save_analysts(self, pair: str, analysts: Dict): now = datetime.utcnow().isoformat() with sqlite3.connect(self.db_path) as conn: for name, data in analysts.items(): conn.execute( "INSERT OR REPLACE INTO analyst_reputation VALUES (?,?,?,?,?,?,?,?)", (pair, name, data["reputation"], data["streak"], data.get("total_calls", 0), data.get("correct", 0), data.get("style_override"), now)) def log_trade(self, pair: str, side: str, result: dict, signal_json: str = ""): with sqlite3.connect(self.db_path) as conn: conn.execute( """INSERT INTO trade_log (pair, side, pnl, broker, fill_price, latency_ms, signal_json, closed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", (pair, side, result.get("profit", 0), result.get("broker", "unknown"), result.get("close_price", 0), result.get("ea_processing_ms", 0), signal_json, datetime.utcnow().isoformat())) def update_fund_state(self, pair: str, pnl: float): today = datetime.utcnow().strftime("%Y-%m-%d") with sqlite3.connect(self.db_path) as conn: row = conn.execute( "SELECT total_pnl, day_pnl, last_day FROM fund_state WHERE pair = ?", (pair,)).fetchone() if row: total, day, last_day = row day = day if last_day == today else 0.0 conn.execute( "REPLACE INTO fund_state VALUES (?, ?, ?, ?)", (pair, total + pnl, day + pnl, today)) else: conn.execute( "INSERT INTO fund_state VALUES (?, ?, ?, ?)", (pair, pnl, pnl, today)) def get_top_analysts(self, pair: str, n: int = 3) -> list: with sqlite3.connect(self.db_path) as conn: rows = conn.execute( """SELECT analyst, reputation, ROUND(CAST(correct AS REAL)/total_calls*100,1) as acc FROM analyst_reputation WHERE pair = ? AND total_calls > 5 ORDER BY reputation DESC LIMIT ?""", (pair, n)).fetchall() return rows
What this means in practice. First launch on Monday — all analysts start with a reputation score of 50.0. By Friday, Victor's reputation score on EURUSD has risen to 68, Dmitri's to 71, and Chen's on gold to 74. We restart the server that evening. On Sunday morning, the system starts up — Victor's reputation score is 68, Dmitri's 71, and Chen's 74. Nothing is lost.
The fund_memory.db file is created automatically when the program is first launched. After a month of use with eight pairs, the file size is about 2 MB. Backing up is easy: cp fund_memory.db fund_memory_$(date +%Y%m%d).db
Execution Layer — TradeMux SDK Instead of an MQL5 EA
In v4, execution was handled by the Trade.Buy() method in the MQL5 EA. v5 uses the official Python package `trademux`. The SDK provides a unified interface for MetaTrader 4 and MetaTrader 5, abstracting away the details of REST and WebSocket. TradeMux's latency on European VPS servers averages 20–25 ms. For an H1 system that analyzes the market every six hours, this is insignificant.
Installation: pip install trademux
# execution_layer.py — a simple wrapper around the official SDK from trademux import MTClient import logging log = logging.getLogger("ExecutionLayer") class TradeMuxExecutor: """Proxy class using TradeMux SDK real methods""" def __init__(self, api_key: str): self.client = MTClient(api_key=api_key, server_url="https://mux.skybluefin.tech") # Check the connection if not self.client.is_connected(): log.warning("TradeMux client not connected, check API key") def get_account_info(self) -> dict: """Return account data: balance, equity, floating_pnl, etc.""" info = self.client.get_account_info() return { "balance": info.get("balance", 0), "equity": info.get("equity", 0), "floating_pnl": info.get("floating_pnl", 0), "open_positions": info.get("open_positions", 0), "account_number": info.get("account_number"), "server": info.get("server"), "platform": "MT5" } def get_positions(self, symbol: str = None) -> list: """Return open positions and orders""" resp = self.client.list_positions() positions = resp.get("positions", []) result = [] for p in positions: if p.get("position_type") == "position": result.append({ "id": p["ticket"], "symbol": p["symbol"], "side": p["type"].upper(), "volume": p["volume"], "price": p["open_price"], "profit": p.get("profit", 0), "sl": p.get("sl"), "tp": p.get("tp"), }) if symbol: result = [p for p in result if p["symbol"] == symbol] return result def buy_market(self, symbol: str, volume: float, sl: float = None, tp: float = None, comment: str = "AIHedgeFund_v5") -> dict: """Market buy order""" result = self.client.buy_market(symbol=symbol, lots=volume, sl=sl, tp=tp, comment=comment) return self._normalize_order_result(result) def sell_market(self, symbol: str, volume: float, sl: float = None, tp: float = None, comment: str = "AIHedgeFund_v5") -> dict: result = self.client.sell_market(symbol=symbol, lots=volume, sl=sl, tp=tp, comment=comment) return self._normalize_order_result(result) def close_position(self, position_id: str) -> dict: """Close position by ticket""" result = self.client.close_ticket(int(position_id)) return { "status": result.get("status"), "pnl": result.get("profit", 0), "close_price": result.get("close_price"), "latency_ms": result.get("ea_processing_ms", 0) } def _normalize_order_result(self, raw: dict) -> dict: """Convert SDK response to a unified format used in the article""" return { "status": raw.get("status"), "ticket": raw.get("ticket"), "fill_price": raw.get("price"), "latency_ms": raw.get("ea_processing_ms", 0), "error": raw.get("error"), "error_code": raw.get("error_code") } def shutdown(self): self.client.close()
Pair Context — an Isolated World for Each Symbol
In v4, there was a PortContext class — one for each WebSocket port (30001–30008). In v5, the ports are gone, but the isolation remains. Each symbol exists within its own context, with its own reputation scores, voting history, and position state. The motivational context is a mechanism from v4 that has not changed: the further the daily PnL is from the target, the more aggressive the entry thresholds become.
# port_context_v5.py import threading, logging log = logging.getLogger("PortContext") class PortContext_v5: def __init__(self, symbol: str, rep_engine): self.symbol = symbol self.rep_engine = rep_engine self.analysts = rep_engine.load_analysts(symbol) self.chat_history = [] self.lock = threading.Lock() self.total_pnl = 0.0 self.day_pnl = 0.0 self.day_target = 50.0 self.aggression_mode = False self.cycle_count = 0 top = self._top_analyst() log.info(f"[{symbol}] Context ready | top: {top[0]} rep={top[1]:.1f}") def _top_analyst(self) -> tuple: return max(self.analysts.items(), key=lambda x: x[1]["reputation"]) def get_analyst_weights(self) -> dict: reps = {n: max(d["reputation"], 1.0) for n, d in self.analysts.items()} total = sum(reps.values()) return {n: round(r / total, 4) for n, r in reps.items()} def get_vote_threshold(self) -> int: if self.aggression_mode: return 4 hunger = (1.0 - min(self.day_pnl / self.day_target, 1.0) if self.day_target > 0 else 0.5) return 5 if hunger > 0.4 else 6 def get_motivation_context(self) -> str: hunger = (1.0 - min(self.day_pnl / self.day_target, 1.0) if self.day_target > 0 else 0.5) bar = "█" * int(hunger * 10) + "░" * (10 - int(hunger * 10)) lines = [ f"FUND MOTIVATION [{self.symbol}]", f"Day PnL : {self.day_pnl:+.2f} / {self.day_target:.0f}", f"Total : {self.total_pnl:+.2f}", f"Cycles : {self.cycle_count}", f"Hunger : {bar} {hunger*100:.0f}%", ] if self.aggression_mode: lines.append("[!] AGGRESSIVE — push for signals") elif hunger > 0.4: lines.append("[~] HUNGRY — prefer active signals") else: lines.append("[+] COMFORTABLE — standard discipline") return "\n".join(lines) def update_pnl(self, pnl: float): self.day_pnl += pnl self.total_pnl += pnl self.cycle_count += 1 self.aggression_mode = (self.day_pnl < -abs(self.day_target) * 0.5) def reset_day(self): self.day_pnl = 0.0 self.aggression_mode = False def get_stats(self) -> dict: top = self._top_analyst() bottom = min(self.analysts.items(), key=lambda x: x[1]["reputation"]) return { "symbol": self.symbol, "top": f"{top[0]}={top[1]['reputation']:.1f}", "bottom": f"{bottom[0]}={bottom[1]['reputation']:.1f}", "day_pnl": self.day_pnl, "total_pnl": self.total_pnl, "cycles": self.cycle_count, "aggressive": self.aggression_mode, }
Prompts for the 15-member Council — Decision-Making Architecture
All of the system's intelligence is contained in `council_prompts.py`. This file has not changed since v4. It contains system prompts for ten analysts, four risk managers, and the Chair. Every analyst represents a distinct market hypothesis embedded in the language model's system prompt.
# council_prompts.py (key excerpts—unchanged from v4) BASE_ANALYST_PROMPTS = { "victor": """You are Victor, a trend-following analyst. You focus on momentum, moving averages and ADX. Give a clear BUY, SELL or NO_SIGNAL with brief reasoning. Be decisive. Your reputation depends on correct calls.""", "maria": """You are Maria, a mean-reversion specialist. You focus on RSI extremes, Bollinger bands and price deviation from SMA. Give BUY (oversold bounce), SELL (overbought rejection) or NO_SIGNAL. Look for stretched conditions.""", "chen": """You are Chen, a volatility and regime analyst. You analyze ATR, Hurst exponent and market microstructure. Your edge is identifying when NOT to trade. Give BUY, SELL or NO_SIGNAL with volatility context.""", "dmitri": """You are Dmitri, a macro and sentiment analyst. You weigh time-of-day effects, volume patterns and cross-asset signals. Give BUY, SELL or NO_SIGNAL. Factor in session timing.""", # ... the remaining six analysts } RISK_PROMPTS = { "risk_alpha": """You are Risk Manager Alpha. Focus: drawdown and position sizing. Review analyst votes and current market volatility. Output: APPROVE_BUY, APPROVE_SELL, REDUCE_SIZE, BLOCK_ALL. Block if ATR spike > 2x normal or if analyst consensus < 60%.""", "risk_beta": """You are Risk Manager Beta. Focus: correlation and regime. Check if multiple analysts agree for the wrong reason (herd bias). Output: APPROVE_BUY, APPROVE_SELL, REDUCE_SIZE, BLOCK_ALL.""", # ... the other two risk managers } def _build_chairman_prompt(threshold: int, motivation: str) -> str: return f"""You are the Chair of the AI Hedge Fund Council. You receive votes from 10 analysts and 4 risk managers. Current vote threshold for action: {threshold} out of 10 analysts. {motivation} Your output MUST be valid JSON: {{"signal": "buy"|"sell"|"hold", "weighted_conviction": 0.0-1.0, "reasoning": "brief explanation", "risk_override": true|false}} Apply the weighted reputation scores. High-reputation analysts count more. If risk managers raise BLOCK_ALL — output hold regardless of analyst votes.""" def _build_market_brief(symbol: str, ind: dict) -> str: return f"""=== MARKET BRIEF: {symbol} === Momentum 5/20/50 bar: {ind['mom5']:+.3f} / {ind['mom20']:+.3f} / {ind['mom50']:+.3f} Trend ADX proxy : {ind['adx']:+.3f} (>0.3 = strong trend) Volatility ATR norm : {ind['atr14']:.3f} (>0.5 = high vol) RSI 14-bar : {ind['rsi14']:.1f} Bollinger pos 20-bar : {ind['bb20']:+.3f} (-1=lower, +1=upper band) Hurst 30-bar : {ind['hurst30']:+.3f} (>0=trend, <0=mean-rev) Volume ratio : {ind['vol_ratio']:.2f}x average Autocorr lag-1 : {ind['ac1']:+.3f} Time hour sin/cos: {ind['hour_sin']:+.3f} / {ind['hour_cos']:+.3f} ================================="""
An important detail: the Chair’s prompt includes a dynamic voting threshold calculated from the motivation context. When the system is far from its daily target, the threshold is lowered from 6 to 4 to allow for riskier entries. Once the target is reached, the threshold increases to 7 to protect the accumulated profit. This is precisely what makes the system adaptive not only to the market but also to its own P&L.
Practical Implementation: A Step-by-Step Guide to Using the TradeMux SDK
Below is a minimal example of retrieving market data and submitting an order using the official SDK. This code can be embedded in any Python script.
# demo_trademux_sdk.py from trademux import MTClient import os API_KEY = os.environ["TRADEMUX_API_KEY"] client = MTClient(api_key=API_KEY, server_url="https://mux.skybluefin.tech") # 1. Connection check if client.is_connected(): print("Connected to TradeMux") # 2. Obtain account information account = client.get_account_info() print(f"Balance: {account['balance']}, Equity: {account['equity']}") # 3. Get the current EURUSD price price = client.get_price("EURUSD") print(f"EURUSD: bid={price['bid']}, ask={price['ask']}, mid={price['mid']}") # 4. Retrieve the last 100 hourly candles (DataFrame) candles = client.get_ohlc("EURUSD", timeframe="1h", count=100, as_df=True) print(candles.head()) # 5. Place a market buy order (demo account) result = client.buy_market(symbol="EURUSD", lots=0.01, sl=price['bid'] - 0.0010, tp=price['bid'] + 0.0020, comment="DemoOrder") print(f"Order status: {result['status']}, ticket: {result.get('ticket')}") # 6. Close a position by ticket if result.get("status") == "filled": close_res = client.close_ticket(result["ticket"]) print(f"Closed with PnL: {close_res.get('profit')}") client.close()
Unified Logic for MetaTrader 4 and MetaTrader 5
One of the key advantages of the TradeMux SDK is its unified interface. The same code (buy_market, sell_market, list_positions, get_ohlc) works the same way for both MetaTrader 4 and MetaTrader 5. You do not need to write different wrappers or learn the specifics of each platform. This allows you to create strategies that can be applied across different brokers and account types without any changes.
Performance: sub-millisecond latency
TradeMux provides an average end-to-end latency of less than 30 ms (often 20–25 ms) on European VPS servers. By comparison, the custom WebSocket implementation in v4 had a latency of 50–100 ms due to additional hops. Normalized data schemas (consistent fields across MetaTrader 4 and MetaTrader 5) let you achieve “hedge-fund-grade” accuracy without manually converting fields.
Main file — Full Build
# llm_hedge_fund_v5.py import MetaTrader5 as mt5 import numpy as np import json, logging, os, time from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Optional from anthropic import Anthropic from persistent_reputation import PersistentReputationEngine from execution_layer import TradeMuxExecutor from port_context_v5 import PortContext_v5 from council_prompts import (BASE_ANALYST_PROMPTS, RISK_PROMPTS, _build_chairman_prompt, _build_market_brief, build_indicators) log = logging.getLogger("HedgeFund_v5") SYMBOLS = ["EURUSD", "GBPUSD", "AUDUSD", "NZDUSD", "USDCHF", "USDCAD", "USDJPY", "XAUUSD"] class HedgeFundV5: def __init__(self): if not mt5.initialize(): raise RuntimeError(f"MT5 init: {mt5.last_error()}") log.info(f"MT5 | build={mt5.terminal_info().build}") self.client = Anthropic() self.rep_engine = PersistentReputationEngine("fund_memory.db") self.executor = TradeMuxExecutor(api_key=os.environ["TRADEMUX_API_KEY"]) self.contexts = {sym: PortContext_v5(sym, self.rep_engine) for sym in SYMBOLS} self.tg_token = os.environ.get("TELEGRAM_TOKEN", "") self.tg_chat = os.environ.get("TELEGRAM_CHAT_ID", "") def run_cycle(self, symbol: str): ctx = self.contexts[symbol] rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_H1, 0, 100) if rates is None or len(rates) < 50: log.warning(f"[{symbol}] No data") return prices = rates["close"].tolist() with ctx.lock: decision = self._run_council(prices, symbol, ctx) signal = decision.get("signal", "hold") conviction = decision.get("weighted_conviction", 0) reasoning = decision.get("reasoning", "") log.info(f"[{symbol}] → {signal.upper()} | conviction={conviction:.2f} | {reasoning[:60]}") self._execute(symbol, signal, ctx, decision) def _execute(self, symbol: str, signal: str, ctx, decision: dict): account = self.executor.get_account_info() drawdown = (account["balance"] - account["equity"]) / account["balance"] if drawdown > 0.03: log.warning(f"[{symbol}] Daily loss limit {drawdown:.1%} hit, skipping") return positions = self.executor.get_positions(symbol) has_buy = any(p["side"] == "BUY" for p in positions) has_sell = any(p["side"] == "SELL" for p in positions) lot = self._calc_lot(account["balance"]) tick = mt5.symbol_info_tick(symbol) pt = mt5.symbol_info(symbol).point if signal == "buy": if has_sell: for p in positions: if p["side"] == "SELL": res = self.executor.close_position(p["id"]) self._on_result(symbol, "sell", res, ctx) if not has_buy: ask = tick.ask self.executor.buy_market(symbol=symbol, volume=lot, sl=round(ask - 800 * pt, 5), tp=round(ask + 1600 * pt, 5), comment="Council_v5") elif signal == "sell": if has_buy: for p in positions: if p["side"] == "BUY": res = self.executor.close_position(p["id"]) self._on_result(symbol, "buy", res, ctx) if not has_sell: bid = tick.bid self.executor.sell_market(symbol=symbol, volume=lot, sl=round(bid + 800 * pt, 5), tp=round(bid - 1600 * pt, 5), comment="Council_v5") def _on_result(self, symbol, side, result, ctx): pnl = result.get("pnl", 0) correct = pnl > 0 for name, data in ctx.analysts.items(): last_vote = data.get("last_vote") if last_vote is None: continue voted_right = (last_vote.lower() == side.lower()) == correct if voted_right: delta = (self.rep_engine.REWARD_CORRECT_BIG if abs(pnl) > 50 else self.rep_engine.REWARD_CORRECT) data["streak"] = max(0, data["streak"]) + 1 if data["streak"] >= 3: delta += self.rep_engine.REWARD_STREAK_BONUS else: delta = (self.rep_engine.PENALTY_WRONG_BIG if abs(pnl) > 50 else self.rep_engine.PENALTY_WRONG) if data["streak"] <= -3: delta *= self.rep_engine.PENALTY_STREAK_MULT data["streak"] = min(0, data["streak"]) - 1 data["reputation"] = ( data["reputation"] * self.rep_engine.REPUTATION_DECAY + delta) data["reputation"] = max(10.0, min(100.0, data["reputation"])) data["total_calls"] = data.get("total_calls", 0) + 1 if voted_right: data["correct"] = data.get("correct", 0) + 1 self.rep_engine.save_analysts(symbol, ctx.analysts) self.rep_engine.log_trade(symbol, side, result) self.rep_engine.update_fund_state(symbol, pnl) ctx.update_pnl(pnl) log.info( f"[{symbol}] RESULT {side.upper()} | pnl={pnl:+.2f} | " f"latency={result.get('latency_ms','?')}ms") if self.tg_token: self._send_telegram( f"{'✅' if pnl > 0 else '❌'} {symbol} {side.upper()} | " f"PnL: {pnl:+.2f} | Latency: {result.get('latency_ms','?')}ms") # ... (the remaining methods—_run_council, _analyst_call, _risk_call, and _chairman_call—remain unchanged) # ... they are identical to the version from the previous article def run_all(self, interval_bars: int = 6): log.info(f"Starting main loop | {len(SYMBOLS)} symbols") while True: start = time.time() with ThreadPoolExecutor(max_workers=8) as pool: futures = {pool.submit(self.run_cycle, sym): sym for sym in SYMBOLS} for fut in as_completed(futures): sym = futures[fut] try: fut.result() except Exception as e: log.error(f"[{sym}] Cycle error: {e}") elapsed = time.time() - start log.info(f"Cycle complete in {elapsed:.1f}s. Next in {interval_bars}h.") time.sleep(max(0, interval_bars * 3600 - elapsed)) def shutdown(self): mt5.shutdown() self.executor.shutdown() log.info("Shutdown complete") if __name__ == "__main__": import dotenv dotenv.load_dotenv() logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s", handlers=[logging.StreamHandler(), logging.FileHandler("hedgefund.log")]) fund = HedgeFundV5() try: fund.run_all(interval_bars=6) except KeyboardInterrupt: fund.shutdown()
Technical requirements and installation
The system requires the following:
- Python 3.9+ with the following packages: pip install MetaTrader 5 numpy python-dotenv anthropic trademux
- A MetaTrader 5 (or MT4) terminal installed and running
- An account on TradeMux.io to obtain an API key
- The TradeMux EA installed in the terminal (via the MQL5 Market) with WebRequest enabled for https://mux.skybluefin.tech
Example of a .env file:
TRADEMUX_API_KEY=your_key_from_console_trademux ANTHROPIC_API_KEY=your_key_anthropic TELEGRAM_TOKEN=bot_token # optional TELEGRAM_CHAT_ID=your_chat_id # optional
Starting the program and reading logs
After configuring the terminal and installing the dependencies, run the following:
python llm_hedge_fund_v5.py
First-run logs — all analysts have a reputation score of 50.0:
09:00:01 | INFO | MT5 | build=4710 09:00:01 | INFO | [EURUSD] Context ready | top: victor rep=50.0 09:00:01 | INFO | Starting main loop | 8 symbols
Logs after a week of operation — reputation scores restored from the database:
09:00:01 | INFO | [EURUSD] Context ready | top: dmitri rep=71.4 09:00:02 | INFO | [EURUSD] Phase I: BUY=0.42 SELL=0.15 09:00:03 | INFO | [EURUSD] → BUY | conviction=0.68 09:00:03 | INFO | ORDER BUY EURUSD 0.06 | status=filled | price=1.08621 | 24ms 09:06:14 | INFO | [EURUSD] RESULT SELL | pnl=+87.50 | latency=24msSQL Analytics: What to Look for in the Database
-- Analysts' rating on each pair SELECT pair, analyst, reputation, ROUND(CAST(correct AS REAL) / total_calls * 100, 1) AS accuracy_pct, total_calls FROM analyst_reputation WHERE total_calls > 10 ORDER BY pair, reputation DESC; -- Weekly trade summary SELECT pair, side, COUNT(*) AS trades, ROUND(SUM(pnl), 2) AS total_pnl, ROUND(AVG(latency_ms), 0) AS avg_latency_ms, ROUND(100.0 * SUM(CASE WHEN pnl > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS win_rate FROM trade_log WHERE closed_at > datetime('now', '-7 days') GROUP BY pair, side ORDER BY total_pnl DESC;
Deployment on a VPS for 24/7 autonomous operation
We recommend using a VPS running Ubuntu 20.04/22.04 with Wine preinstalled to run MetaTrader and a systemd service for the Python script.
# /etc/systemd/system/hedgefund.service
[Unit]
Description=AI Hedge Fund v5
After=network.target
[Service]
Type=simple
User=trader
WorkingDirectory=/home/trader/hedgefund
EnvironmentFile=/home/trader/hedgefund/.env
ExecStart=/usr/bin/python3 llm_hedge_fund_v5.py
Restart=always
RestartSec=15
StandardOutput=append:/home/trader/hedgefund/hedgefund.log
StandardError=append:/home/trader/hedgefund/hedgefund.log
[Install]
WantedBy=multi-user.target sudo systemctl enable hedgefund sudo systemctl start hedgefund sudo journalctl -u hedgefund -f
VPS best practices:
- Set up a daily backup of the fund_memory.db database (cron).
- Use `kill_switch()` as an emergency shut-off when the daily loss limit is exceeded.
- Set up a Telegram bot to monitor critical events (every trade, errors, and restarts).
- Update the TradeMux EA periodically via the MQL5 Market.
| Component | v4 | v5 |
|---|---|---|
| Reputation Storage | Python Dictionary in RAM | SQLite — survives restarts |
| Order Execution | Trade.Buy() in an MQL5 EA | TradeMux SDK (buy_market/sell_market) |
| Number of brokers | 1 | Unlimited |
| Protocol | WebSocket (custom-built) | Official SDK built on top of REST/WebSocket |
| Number of data hops | 4 | 2 |
| Trade Audit | No | trade_log with latency data |
| Monitoring | MetaTrader 5 Log Only | Telegram + SQLite + System Log |
| The Council of Fifteen | No changes | No changes |
Conclusion and Next Steps
Reputation scores are preserved between restarts — this is the main change in v5. A single Python process handles everything; the WebSocket servers on eight ports are gone. It is now possible to execute orders on multiple platforms through a single Python interface. A complete audit trail is now available for every trade, including latency and broker details.
The system's entire intelligence remained intact: analysts' prompts, risk managers' logic, the Chair's voting procedure, reputation score decay, and the motivational context. There was no need to change this.
Unresolved issue: All 15 council members are the same language model with different system prompts. When the model misinterprets a particular pattern, all fifteen make the same mistake. Eight weeks of backtesting is a good result, but it is not enough to draw conclusions about how the system will behave in a sideways market or during periods of high news-driven volatility. Test on a demo account before switching to a live account.
The next step is to retain the decisions made in long-term memory. Not just the analysts' reputation scores, but the full history of their reasoning: what each of the fifteen said, what the market context was, and what ultimately happened. The `signal_json` field in the `trade_log` table is already reserved for this purpose. This turns `trade_log` from a trade log into a training dataset. That will be the topic of the next part.
| File name | File Description |
|---|---|
| llm_hedge_fund_v5.py | Main Server — data, council, execution |
| persistent_reputation.py | SQLite persistence for the reputation engine |
| execution_layer.py | A wrapper for the TradeMux SDK with response normalization |
| port_context_v5.py | An isolated per-symbol context |
| council_prompts.py | Prompts for 15 participants (unchanged from v4) |
| fund_memory.db | Reputation score database (created automatically) |
| hedgefund.log | Event log (created automatically) |
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/22444
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Features of Custom Indicators Creation
Crypto Exchange Integration with MQL5 (Part 1): Connecting to MEXC Futures
Features of Experts Advisors
Creating Custom Indicators in MQL5 (Part 12): Mapping Liquidity with a Volume Profile Heatmap
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use