preview
Как заменить WebSocket EA на TradeMux REST в MetaTrader 5

Как заменить WebSocket EA на TradeMux REST в MetaTrader 5

MetaTrader 5Торговые системы |
62 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Введение

Если вы читали предыдущую статью про AI Hedge Fund v4, вы знаете, где мы остановились. Совет из 15 участников: 10 аналитиков, 4 риск-менеджера и Председатель, восемь валютных пар, Sharpe 6.31, Profit Factor 2.09 на восьминедельном бэктесте. Система работала на одном MetaTrader 5-терминале через WebSocket-советник.

После публикации той статьи большинство вопросов сводилось к одному: зачем городить совет из 15 участников, если можно просто взять готовый сигнальный сервис? Ответ стал яснее именно за те восемь недель работы. Дело не в количестве участников совета, а в том, что система с репутационным движком обучается на собственных ошибках прямо в процессе торговли. Victor, который в понедельник начинает с репутацией 50 и к пятнице добирается до 68, — это не просто число в памяти, это накопленная информация о том, кому и на каком инструменте стоит доверять больше. Именно эта информация оказалась уязвимым местом архитектуры v4.

Алгоритмическая торговля — это не про “один раз написать правильный код”, а про устойчивость системы при перезапусках, смене брокера и появлении новых требований. В v4 “интеллект” был решён: 15 агентов принимают решения с учётом взвешенных репутаций, но инфраструктура оставалась хрупкой: один процесс, один советник, один терминал и один брокер. В продакшене такая хрупкость рано или поздно даёт о себе знать. Причина не в качестве кода. Любой сервер перезагружается, у брокеров бывают техработы, а затем появляется второй пользователь, которому нужен доступ к тем же сигналам.

В конце той статьи были честно названы три конкретных ограничения. Эта статья — о том, как убрать эти ограничения, не переписывая систему с нуля, а добавив два компонента к тому, что уже работает. Если вы не читали предыдущие части серии — ничего страшного, я буду объяснять по ходу. Но если читали — сразу увидите, где что изменилось и почему.

Где взять API‑ключ и документацию: все необходимые материалы (регистрация, тарифы, консоль разработчика) доступны на www.trademux.io. Там же вы найдёте актуальную документацию SDK.

В статье последовательно рассматриваются:

  • три проблемы архитектуры v4 и их причины;
  • новый слой исполнения — облачный шлюз вместо WebSocket EA;
  • персистентный репутационный движок на SQLite — полная реализация;
  • изолированный контекст символа и мотивационный движок;
  • промпты совета из пятнадцати участников и логика голосования;
  • главный цикл системы — полная сборка;
  • деплой на VPS, мониторинг через Telegram, SQL-аналитика;
  • сравнение v4 и v5, практические рекомендации.


Три проблемы версии v4

Первая проблема — амнезия при перезапуске. Victor набрал репутацию 78 за три недели работы. Сервер упал, Victor снова 50. Три недели обучения потеряны. Репутационный движок в v4 был словарём Python в памяти процесса. Это означало: любое завершение процесса — плановое или аварийное — сбрасывало всю накопленную информацию о надёжности аналитиков. Система теряла не просто числа, она теряла несколько недель адаптации к конкретному инструменту, конкретному брокеру, конкретному рыночному режиму. Каждый понедельник после выходных рестарта совет начинал с нуля — как будто эти три недели не существовали.

Вторая проблема — EA как лишнее звено. В v4 данные шли по цепочке: MetaTrader 5 → EA → WebSocket → Python → WebSocket → EA → ордер. Четыре перехода вместо двух. Каждый переход — это потенциальная точка отказа, дополнительная латентность и лишний компонент, который нужно поддерживать. При изменении логики входа — перекомпилируй MQL5, перезапусти советник. При ошибке в Python — советник висит в ожидании ответа по WebSocket. При перезапуске Python-сервера — WebSocket-соединение рвётся и советник теряет связь. Это не теоретические риски, всё это случилось за восемь недель работы.

Третья проблема — один брокер как потолок. Совет принял решение: BUY EURUSD. Это решение ушло ровно на один MetaTrader 5-аккаунт. Архитектура v4 не умеет транслировать сигнал на несколько терминалов одновременно. Если партнёр хочет подключить свой аккаунт к тем же сигналам — нужно запускать вторую копию всей системы: второй Python-процесс, второй советник, второй WebSocket-сервер. Это не масштабируется. А ведь именно мультиаккаунтность — одно из главных преимуществ алгосистемы перед ручной торговлей.

В v5 решаются все три проблемы. Разберём по порядку.


Что меняется в архитектуре — и что остаётся

Хорошая новость: весь интеллект системы мы не трогаем. Десять аналитиков с их промптами, четыре риск-менеджера, Председатель с температурой 0.15 и трёхшаговой процедурой голосования — всё это работает ровно так же, как в v4. Код совета из 15 участников копируется без единого изменения.

Меняются только два компонента: как репутации хранятся (SQLite вместо памяти процесса) и как решения выходят в рынок (Python REST через TradeMux SDK вместо WebSocket через MQL5 EA).

Архитектура v4 — цепочка из четырёх переходов:

[MT5 Terminal]
    ↕ WebSocket (winhttp.mqh)
[AIHedgeFund_v4.mq5]
    → copy_rates()           // цены → Python
    → SendRawCmd("COUNCIL")  // сигнал → Python
    ← {"signal":"buy",...}
    → Trade.Buy()            // ордер → брокер
    → SendRawCmd("RESULT")   // результат → Python → репутации

Архитектура v5 — один Python-процесс управляет несколькими терминалами через единый вызов SDK:

[MT5 Terminal — Broker A]        [MT5 Terminal — Broker B]
    Bridge EA ←──────────────────── Bridge EA
         ↑                                  ↑
         └──────── TradeMux Cloud ──────────┘
                          ↑
                // Python TradeMux SDK
                          ↑
                [Python Server v5]
                → mt5.copy_rates()        // данные — локальный MT5 SDK
                → Council of 15  →        // сигнал совета
                → client.buy_market() / sell_market() → // оба терминала одновременно
                ← callback (через close_trade) → // результат → репутации → SQLite

Для слоя исполнения используется официальный Python‑пакет TradeMux (доступен через pip install trademux ). Он взаимодействует с облачным шлюзом, а EA‑компонент (устанавливается в каждый терминал) держит постоянное соединение. Python общается через SDK, один вызов — все подключённые терминалы получают сигнал одновременно.

Установка Bridge EA состоит из четырёх шагов:

  1. Установить TradeMux через MQL5 Market.
  2. Включить Allow Algorithmic Trading и добавить https://mux.skybluefin.tech в разрешённые URL для WebRequest.
  3. Запустить EA на графике и указать API‑ключ (получить в консоли TradeMux).
  4. Повторить для каждого терминала.


Персистентные репутации — SQLite

Репутационный движок в v4 — словарь Python в памяти процесса. Словарь умирает вместе с процессом. SQLite — очевидный выбор: одна зависимость, один файл, zero configuration, работает везде. Схема базы данных покрывает три задачи: хранение репутаций аналитиков, журнал сделок для аналитики и сводка по каждому символу.

# persistent_reputation.py
import sqlite3
from datetime import datetime
from typing import Dict, Optional

class PersistentReputationEngine:

    #--- Константы из v4 — не изменились
    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

Что это даёт на практике. Первый запуск в понедельник — все аналитики по 50.0. К пятнице Victor на EURUSD вырос до 68, Дмитрий до 71, Чэнь на золоте до 74. Вечером перезапускаем сервер. Воскресенье утром система поднимается — Victor 68, Дмитрий 71, Чэнь 74. Ничего не потеряно.

Файл fund_memory.db создаётся автоматически при первом запуске. Размер после месяца работы на восьми парах — около 2 МБ. Бэкапить просто: cp fund_memory.db fund_memory_$(date +%Y%m%d).db


Слой исполнения — TradeMux SDK вместо MQL5 EA

В v4 исполнением занимался метод Trade.Buy() в MQL5-советнике. В v5 используется официальный Python‑пакет trademux . SDK предоставляет единый интерфейс для MetaTrader 4 и MetaTrader 5, скрывая детали REST и WebSocket. Латентность TradeMux на европейских VPS — 20–25 мс в среднем. Для H1-системы, которая анализирует рынок раз в шесть часов, это незначимо.

Установка: pip install trademux

# execution_layer.py — простая обёртка над официальным SDK
from trademux import MTClient
import logging

log = logging.getLogger("ExecutionLayer")

class TradeMuxExecutor:
    """Прокси-класс, использующий реальные методы TradeMux SDK"""
    def __init__(self, api_key: str):
        self.client = MTClient(api_key=api_key,
                               server_url="https://mux.skybluefin.tech")
        # Проверяем соединение
        if not self.client.is_connected():
            log.warning("TradeMux client not connected, check API key")

    def get_account_info(self) -> dict:
        """Возвращает информацию о счёте: balance, equity, floating_pnl и др."""
        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:
        """Возвращает открытые позиции и ордера"""
        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:
        """Рыночный ордер на покупку"""
        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:
        """Закрывает позицию по тикету"""
        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:
        """Приводит ответ SDK к единому формату, используемому в статье"""
        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()


Контекст пары — изолированный мир для каждого символа

В v4 был класс PortContext — один на каждый WebSocket-порт (30001..30008). В v5 порты исчезли, но изоляция осталась. Каждый символ живёт в своём контексте со своими репутациями, своей историей голосований и своим состоянием позиций. Мотивационный контекст — механизм из v4, который не изменился: чем дальше дневной PnL от целевого, тем агрессивнее пороги входа.

# 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,
        }


Промпты совета из пятнадцати участников — архитектура решений

Весь интеллект системы — в council_prompts.py. Этот файл не изменился от v4. Он содержит системные промпты десяти аналитиков, четырёх риск-менеджеров и Председателя. Каждый аналитик — это отдельная рыночная гипотеза, зашитая в системный промпт языковой модели.

# council_prompts.py (ключевые фрагменты — без изменений от 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.""",
    # ... остальные шесть аналитиков
}

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.""",
    # ... остальные два риск-менеджера
}

def _build_chairman_prompt(threshold: int, motivation: str) -> str:
    return f"""You are the Chairman 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}
================================="""

Важная деталь: промпт Председателя содержит динамический порог голосования, который вычисляется из мотивационного контекста. Когда система далеко от дневной цели — порог снижается с 6 до 4, чтобы позволить более рискованные входы. Когда цель достигнута — порог растёт до 7, чтобы защитить накопленную прибыль. Именно это делает систему адаптивной не только к рынку, но и к собственному P&L.


Практическая реализация: пошаговое руководство по использованию TradeMux SDK

Ниже показан минимальный пример получения рыночных данных и отправки ордера через официальный SDK. Этот код можно встроить в любой Python‑скрипт.

# 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. Проверка соединения
if client.is_connected():
    print("Connected to TradeMux")

# 2. Получение информации о счёте
account = client.get_account_info()
print(f"Balance: {account['balance']}, Equity: {account['equity']}")

# 3. Получение текущей цены EURUSD
price = client.get_price("EURUSD")
print(f"EURUSD: bid={price['bid']}, ask={price['ask']}, mid={price['mid']}")

# 4. Получение последних 100 часовых свечей (DataFrame)
candles = client.get_ohlc("EURUSD", timeframe="1h", count=100, as_df=True)
print(candles.head())

# 5. Размещение рыночного ордера на покупку (демо-счёт)
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. Закрытие позиции по тикету
if result.get("status") == "filled":
    close_res = client.close_ticket(result["ticket"])
    print(f"Closed with PnL: {close_res.get('profit')}")

client.close()


Единая логика для MetaTrader 4 и MetaTrader 5

Одно из ключевых преимуществ TradeMux SDK — унификация. Тот же самый код ( buy_market , sell_market , list_positions , get_ohlc ) работает одинаково и для MetaTrader 4, и для MetaTrader 5. Вам не нужно писать разные обёртки или учить специфику каждой платформы. Это позволяет создавать стратегии, которые без изменений переключаются между брокерами и типами счетов.

Производительность: субмиллисекундная латентность

TradeMux обеспечивает среднюю сквозную задержку менее 30 мс (часто 20–25 мс) на европейских VPS. Для сравнения, собственная WebSocket‑реализация в v4 давала 50–100 мс из-за дополнительных переходов. Нормализованные схемы данных (единые поля для MetaTrader 4/MetaTrader 5) позволяют использовать «hedge‑fund‑grade» точность без ручного преобразования полей.


Главный файл — полная сборка

# 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")

    # ... (остальные методы _run_council, _analyst_call, _risk_call, _chairman_call без изменений)
    # ... они идентичны версии из предыдущей статьи

    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()


Технические требования и установка

Для работы системы необходимо:

  • Python 3.9+ с пакетами: pip install MetaTrader 5 numpy python-dotenv anthropic trademux
  • Установленный и запущенный MetaTrader 5 (или MT4) терминал
  • Аккаунт на TradeMux.io для получения API‑ключа
  • Установленный в терминале TradeMux EA (через MQL5 Market) с включённым WebRequest для https://mux.skybluefin.tech

Пример файла .env :

TRADEMUX_API_KEY=ваш_ключ_из_консоли_trademux
ANTHROPIC_API_KEY=ваш_ключ_anthropic
TELEGRAM_TOKEN=токен_бота          # опционально
TELEGRAM_CHAT_ID=ваш_chat_id       # опционально

Запуск и чтение логов

После настройки терминала и установки зависимостей запуск:

python llm_hedge_fund_v5.py

Логи первого запуска — все аналитики по 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

Логи после недели работы — репутации восстановлены из базы:

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-аналитика: что смотреть в базе
-- Рейтинг аналитиков на каждой паре
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;

-- Недельная динамика сделок
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;

Деплой на VPS для 24/7 автономной работы

Рекомендуется использовать VPS с Ubuntu 20.04/22.04, предустановленным Wine для запуска MetaTrader, и systemd сервис для Python‑скрипта.

# /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

Best practices для VPS:

  • Настройте ежедневный бэкап базы fund_memory.db (cron).
  • Используйте kill_switch() как аварийный выключатель при превышении дневного лимита убытка.
  • Настройте Telegram‑бота для мониторинга критических событий (каждая сделка, ошибки, перезапуск).
  • Периодически обновляйте TradeMux EA через MQL5 Market.
Сравнение v4 и v5
Компонент v4 v5
Хранение репутаций Словарь Python в RAM SQLite — переживает перезапуск
Исполнение ордеров Trade.Buy() в MQL5 EA TradeMux SDK (buy_market/sell_market)
Число брокеров 1 Неограниченно
Протокол WebSocket (самодельный) Официальный SDK поверх REST/WebSocket
Переходов данных 4 2
Аудит сделок Нет trade_log с латентностью
Мониторинг Только лог MetaTrader 5 Telegram + SQLite + системный журнал
Совет пятнадцати Без изменений Без изменений


Итог и что дальше

Репутации сохраняются между перезапусками — это главное изменение v5. Один Python-процесс делает всё, WebSocket-серверы на восьми портах исчезли. Появилась возможность исполнять ордера на нескольких платформах через единый Python-интерфейс. Появился полный аудит-трейл каждой сделки с латентностью и брокером.

Весь интеллект системы остался нетронутым: промпты аналитиков, логика риск-менеджеров, процедура голосования Председателя, репутационный decay, мотивационный контекст. Это не нужно было трогать.

Нерешённая проблема: все 15 участников совета — одна и та же языковая модель с разными системными промптами. Когда модель ошибается в каком-то паттерне — все пятнадцать ошибаются вместе. Восемь недель бэктеста — хороший результат, но недостаточный для выводов о поведении системы в боковом рынке или в период высокой новостной волатильности. Тестируйте на демо-счёте, прежде чем переходить на реальный счёт.

Следующий шаг — долгосрочная память принятых решений. Не просто репутации аналитиков, а полная история рассуждений: что говорил каждый из пятнадцати, какой был контекст рынка, что произошло в итоге. Поле signal_json в таблице trade_log уже зарезервировано под это. Это превращает trade_log из журнала сделок в обучающую выборку. Про это будет следующая часть.

Название файла Описание файла
llm_hedge_fund_v5.py Главный сервер — данные, совет, исполнение
persistent_reputation.py SQLite-персистентность репутационного движка
execution_layer.py Обёртка над TradeMux SDK с нормализацией ответов
port_context_v5.py Изолированный контекст для каждого символа
council_prompts.py Промпты 15 участников (без изменений от v4)
fund_memory.db База репутаций (создаётся автоматически)
hedgefund.log Журнал событий (создаётся автоматически)
Прикрепленные файлы |
ai_hedge_fund_v6.zip (290.94 KB)
Тестер стратегий для Python и MetaTrader 5 (Часть 03): Обработка и управление торговыми операциями по образцу MetaTrader 5 Тестер стратегий для Python и MetaTrader 5 (Часть 03): Обработка и управление торговыми операциями по образцу MetaTrader 5
В этой статье мы представляем способы обработки торговых операций в стиле Python–MetaTrader 5, таких как открытие, закрытие и изменение ордеров в симуляторе. Чтобы симуляция вела себя как MetaTrader 5, реализован строгий уровень проверки торговых запросов, учитывающий торговые параметры символа и типичные брокерские ограничения.
Автоматизация торговых стратегий в MQL5 (Часть 27): Выявление и визуализация гармонического паттерна "Краб" на основе Price Action Автоматизация торговых стратегий в MQL5 (Часть 27): Выявление и визуализация гармонического паттерна "Краб" на основе Price Action
В этой статье мы разрабатываем систему распознавания гармонических паттернов "Краб" на языке MQL5, которая определяет бычьи и медвежьи гармонические паттерны "Краб" с использованием точек разворота и уровней Фибоначчи, запуская сделки с точными уровнями входа, стоп-лосса и тейк-профита. Мы добавляем визуальное представление с помощью графических объектов, таких как треугольники и линии тренда, для отображения структуры паттерна XABCD и торговых уровней.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Сила MetaTrader 5: от пошаговой отладки до защиты EX5 в одной среде Сила MetaTrader 5: от пошаговой отладки до защиты EX5 в одной среде
В статье рассматривается комплексный подход к разработке торговых алгоритмов: от настройки проекта и отладки логики до защиты готового продукта. Разбираются встроенные инструменты MetaEditor, включая пошаговый дебаггинг на реальных тиках, профилирование производительности и прямую интеграцию с C++ DLL для ускорения вычислений. Описывается методика защиты интеллектуальной собственности с помощью MQL5 Cloud Protector. Применение описанных техник позволяет превратить разработку эксперта из хаотичного поиска решений в системный процесс, существенно сокращая время разработки стратегии.