Как заменить WebSocket EA на TradeMux REST в MetaTrader 5
Введение
Если вы читали предыдущую статью про 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 состоит из четырёх шагов:
- Установить TradeMux через MQL5 Market.
- Включить Allow Algorithmic Trading и добавить https://mux.skybluefin.tech в разрешённые URL для WebRequest.
- Запустить EA на графике и указать API‑ключ (получить в консоли TradeMux).
- Повторить для каждого терминала.
Персистентные репутации — 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=24msSQL-аналитика: что смотреть в базе
-- Рейтинг аналитиков на каждой паре 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 |
|---|---|---|
| Хранение репутаций | Словарь 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 | Журнал событий (создаётся автоматически) |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Тестер стратегий для Python и MetaTrader 5 (Часть 03): Обработка и управление торговыми операциями по образцу MetaTrader 5
Автоматизация торговых стратегий в MQL5 (Часть 27): Выявление и визуализация гармонического паттерна "Краб" на основе Price Action
Сила MetaTrader 5: от пошаговой отладки до защиты EX5 в одной среде
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования