Русский
preview
How to Replace a WebSocket EA with TradeMux REST in MetaTrader 5

How to Replace a WebSocket EA with TradeMux REST in MetaTrader 5

MetaTrader 5 — Trading systems |
435 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

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:

  1. Install TradeMux via the MQL5 Market.
  2. Enable Allow Algorithmic Trading and add https://mux.skybluefin.tech to the list of allowed URLs for WebRequest.
  3. Launch the EA on the chart and enter the API key (available in the TradeMux console).
  4. 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=24ms
SQL 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.
Comparison of v4 and v5
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

Attached files |
ai_hedge_fund_v6.zip (288.19 KB)
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Crypto Exchange Integration with MQL5 (Part 1): Connecting to MEXC Futures Crypto Exchange Integration with MQL5 (Part 1): Connecting to MEXC Futures
This article introduces the foundations of connecting MetaTrader 5 to a cryptocurrency exchange using MQL5 and the MEXC Futures API. It covers terminal configuration, JSON response processing, WebRequest() execution and validation, and conversion of API data into native MQL5 types. Practical examples demonstrate retrieving and processing exchange data. These components establish the basis for a reusable MEXC Futures client, developed progressively throughout the series. The series focuses exclusively on exchange integration; how readers use the retrieved data remains outside its scope.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Creating Custom Indicators in MQL5 (Part 12): Mapping Liquidity with a Volume Profile Heatmap Creating Custom Indicators in MQL5 (Part 12): Mapping Liquidity with a Volume Profile Heatmap
We build a volume profile heatmap indicator in MQL5 that bins window volume into uniform price levels and paints each bar as a column of colored cells behind the candles. We cover binning, heat coloring, the side profile, the point-of-control and value-area lines, and the liquidity readouts, with every level published to buffers. The result is a map of dense bands and thin gaps, showing how much traded volume sits above and below the current price.