preview
Как организовать ИИ-хедж-фонд в MetaTrader 5

Как организовать ИИ-хедж-фонд в MetaTrader 5

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

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

Представьте управляющий комитет реального фонда. За одним столом сидят десять аналитиков с принципиально разными философиями. Виктор смотрит только на тренд и выравнивание скользящих средних — для него цена выше MA50 уже достаточное основание для BUY. Дмитрий с тремя PhD вычисляет z-оценку отклонения от полосы Боллинджера и не выдаст сигнал, пока это число не перейдёт порог 0.8. Чэнь не реагирует ни на что без подтверждения объёмом. Юки смотрит на тело последней свечи и ни на что другое. За отдельным столом сидят четыре риск-менеджера с независимыми осями страха: один измеряет ATR, второй считает тайминг входа, третий оценивает качество рыночного режима, четвёртый ищет хвостовые события. И над всеми — Председатель, который читает все четырнадцать докладов и применяет жёсткую трёхшаговую процедуру голосования.

Теперь умножьте это на восемь. Восемь валютных пар, восемь изолированных советов, восемь независимых репутационных движков — и всё это в одном Python-процессе.

Именно это описывает данная статья.


Почему одной пары было недостаточно

Предыдущая архитектура работала на одном символе. Это означало, что система имела слепое пятно размером с весь остальной рынок.

Валюты движутся в корреляциях. EURUSD и GBPUSD часто идут вместе, потому что оба несут риск доллара. USDJPY ведёт себя как актив-убежище в моменты паники. XAUUSD — вовсе отдельный зверь с собственной сезонностью и реакцией на инфляцию. Аналитик, торгующий только EURUSD и не видящий, что происходит с иеной, работает вполглаза.

Но дело не только в корреляциях. Разные пары дают разное количество торговых возможностей в зависимости от рыночного режима. Когда EURUSD застыл в узком диапазоне, и совет из пятнадцати раз за разом говорит HOLD — GBPUSD в это время может находиться в чистом тренде с семью аналитиками BUY и нулём BLOCKED. Система, которая умеет слушать только один голос рынка, упускает всё остальное.

Решение очевидно: запустить восемь изолированных советов параллельно. Каждый работает на своей паре, со своей историей репутаций, целями PnL и мотивационным состоянием.


Архитектура: один процесс, восемь миров

Техническое ядро системы — класс PortContext. Это изолированный контейнер состояния для одной валютной пары:

class PortContext:
    """Изолированный контекст для одного порта (одной валютной пары)."""
    def __init__(self, port: int, pair: str):
        self.port         = port
        self.pair         = pair
        self.reputation   = ReputationEngine()   # свой движок репутации
        self.chat_history = []                   # своя история чата
        self.history_lock = threading.Lock()

Восемь экземпляров PortContext создаются при старте и живут независимо. Репутация Виктора на EURUSD никак не влияет на репутацию Виктора на XAUUSD. Дневной голод фонда по иене не снижает требовательность совета по франку. Каждая пара — отдельный мир.

Карта портов жёсткая и симметричная:

PAIR_PORTS = {
    "EURUSD": (30001, 8971),
    "GBPUSD": (30002, 8972),
    "AUDUSD": (30003, 8973),
    "NZDUSD": (30004, 8974),
    "USDCHF": (30005, 8975),
    "USDCAD": (30006, 8976),
    "USDJPY": (30007, 8977),
    "XAUUSD": (30008, 8978),
}

Magic — номер пресета (30001..30008). Port — Magic − 30000 + 8970. Это позволяет любому компоненту системы по одному числу восстановить весь контекст: кто торгует, на каком порту слушает, с каким Magic помечает ордера.

В main() всё запускается в три строки:

def main():
    for pair, (magic, port) in PAIR_PORTS.items():
        _port_contexts[port] = PortContext(port, pair)   # создать контексты

    for port in ALL_PORTS:
        threading.Thread(target=_run_port_server,        # запустить серверы
                         args=(port,), daemon=True).start()

    threading.Thread(target=_decay_daemon, daemon=True).start()   # затухание репутации

    while True:
        time.sleep(60)   # главный поток просто ждёт

Восемь TCP-серверов слушают на восьми портах. Каждое входящее WebSocket-соединение получает свой PortContext по номеру порта и работает полностью изолированно от остальных.


MQL5: пресет как единственный параметр

На стороне MetaTrader трейдер делает одно действие — выбирает пресет. Всё остальное вычисляется автоматически.

enum ENUM_PAIR_PRESET
  {
   PRESET_EURUSD  = 30001,   // EURUSD  → Magic 30001, Port 8971
   PRESET_GBPUSD  = 30002,   // GBPUSD  → Magic 30002, Port 8972
   PRESET_AUDUSD  = 30003,   // AUDUSD  → Magic 30003, Port 8973
   PRESET_NZDUSD  = 30004,   // NZDUSD  → Magic 30004, Port 8974
   PRESET_USDCHF  = 30005,   // USDCHF  → Magic 30005, Port 8975
   PRESET_USDCAD  = 30006,   // USDCAD  → Magic 30006, Port 8976
   PRESET_USDJPY  = 30007,   // USDJPY  → Magic 30007, Port 8977
   PRESET_XAUUSD  = 30008,   // XAUUSD  → Magic 30008, Port 8978
  };

В OnInit() одна строка разворачивает пресет в Magic и Port:

int OnInit()
  {
// ── Вычисляем Magic и Port из пресета ──────────────────────────────
   g_InpMagic = (int)InpPairPreset;
   g_InpPort  = (int)InpPairPreset - 30000 + 8970;  // 30001→8971 … 30008→8978

// ── Проверяем соответствие символа графика пресету ─────────────────
   string presetSymbol = PresetToSymbol(InpPairPreset);
   if(presetSymbol != "" && _Symbol != presetSymbol)
     {
      PrintFormat("[WARN] Пресет=%s, но график=%s. Убедитесь что EA установлен на правильный график!",
                  presetSymbol, _Symbol);
     }

   PrintFormat("[INIT] Пресет=%s | Magic=%d | Port=%d | Symbol=%s",
               EnumToString(InpPairPreset), g_InpMagic, g_InpPort, _Symbol);

   Trade.SetExpertMagicNumber(g_InpMagic);
   Trade.SetDeviationInPoints(InpSlippage);
   Trade.SetTypeFilling(ORDER_FILLING_IOC);

   ArrayInitialize(g_repWeights, 0.1);
   ArrayInitialize(g_avgEntryBuy, 0.0);
   ArrayInitialize(g_avgEntrysSell, 0.0);

   g_dayStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   PrintBanner();

// Подключение к серверу
   if(InitWebSocket())
     {
      g_useWS = true;
      PrintFormat("[OK] WinHTTP WebSocket подключён → %s:%d (Magic=%d)",
                  InpHost, g_InpPort, g_InpMagic);
     }
   else
     {
      PrintFormat("[X] WebSocket недоступен. Порт=%d. Запустите: python llm_hedge_fund_v4.py", g_InpPort);
      return INIT_FAILED;
     }

   SendGoals();
   return INIT_SUCCEEDED;
  }

Функция PresetToSymbol() проверяет соответствие: если трейдер поставил пресет GBPUSD на график EURUSD — советник предупредит в журнале, но продолжит работу на текущем символе. Это защита от опечатки, а не жёсткая блокировка.

Параметры входа намеренно сведены к минимуму — только то, что трейдер должен решать сам:

input ENUM_PAIR_PRESET InpPairPreset      = PRESET_EURUSD;
input double           InpBaseLot         = 0.01;
input int              InpBaseSL_Pts      = 800;
input int              InpBaseTP_Pts      = 1600;
input double           InpDailyLossLimitPct = 3.0;
input int              InpPriceBars       = 100;
input int              InpAnalysisBars    = 1;

Никаких параметров усреднения, трейлинга или сессионного фильтра. Советник делает одно: слушает совет пятнадцати и открывает или закрывает позицию.


Десять аналитиков: галерея философий

Подбор участников первой фазы — не произвол. Каждый представляет отдельную торговую школу с собственной логикой и собственными слепыми пятнами. Именно несовпадение этих слепых пятен создаёт ценность ансамбля.

Виктор — трендовый трейдер. Сигнал BUY, если цена выше MA20 или MA50 с положительным моментумом. Сигнал SELL — симметрично. NO SIGNAL — только при идеально плоском моментуме. Он никогда не спорит сам с собой — выбирает более сильную сторону.

Мария — контрарианский медведь с убеждениями. Ищет SELL прежде всего. RSI выше 65 и цена у верхней полосы Боллинджера — ей достаточно. При перепроданности умеет дать BUY. Её ценность — кричать «стоп!» именно тогда, когда все остальные кричат «вперёд!».

Елена — специалист по возврату к среднему. Работает только на экстремумах: касание нижней полосы при RSI7 ниже 35 — BUY. Верхней при RSI7 выше 65 — SELL. Вблизи средней — использует направление последнего моментума. NO SIGNAL только в мёртвой зоне RSI7 от 45 до 55 строго у средней полосы.

Дмитрий — квант с PhD. Вычисляет z-оценку: z = (price − BB_mid) / StdDev . Z выше 0.8 — SELL. Ниже −0.8 — BUY. Между нулём и 0.8 — смотрит на моментум. Показывает математику в ответе.

Чэнь — эксперт по объёму и микроструктуре. Volume Ratio выше 1.2 плюс бычья свеча — BUY. Плюс медвежья — SELL. При низком объёме ориентируется на направление свечи. NO SIGNAL — только при объёме ниже 0.5× среднего и идеальном доджи.

Изабелла — трейдер волатильности. Расширение волатильности в восходящем тренде — BUY. В нисходящем — SELL. При сжатии — торгует направление тренда. NO SIGNAL — только при идеально нейтральных тренде и волатильности одновременно.

Маркус — аналитик Вайкоффа. Ищет пружину (ложный пробой вниз с разворотом) или аптраст (ложный пробой вверх с отвержением). При отсутствии явного Вайкофф-события — использует тренд. Умеет читать следы институциональных денег.

Юки — мастер японских свечей. Бычья свеча с телом больше 0.3 ATR — BUY. Медвежья с тем же условием — SELL. Маленькое тело — смотрит на расположение: у нижней полосы даёт BUY, у верхней SELL, иначе ориентируется на моментум. NO SIGNAL — только на идеальном доджи с телом меньше 0.1 ATR строго у средней.

Рафаэль — специалист по дивергенциям. Все индикаторы моментума вверх — BUY. Вниз — SELL. При смешанных сигналах: большинство (2/3) определяет направление. NO SIGNAL — только при идеальном 50/50. Его сигналы самые сильные, когда все осцилляторы согласованы.

Софи — систематик. Считает бинарные проверки: цена выше MA20? RSI14 выше 50? Стохастик K выше D? Итого десять вопросов. Пять и более бычьих — BUY. Пять и более медвежьих — SELL. При ничьей — решает последняя свеча.

В коде каждый аналитик — это системный промпт в словаре BASE_ANALYST_PROMPTS:

BASE_ANALYST_PROMPTS = {
    "victor": (
        "You are VICTOR STONE — momentum master. Trend is God. Indecision kills returns. "
        "Signal BUY if price > MA20 OR MA50 with positive momentum. "
        "Signal SELL if price < MA20 OR MA50 with negative momentum. "
        "If trend is ambiguous, pick the STRONGER side. NO SIGNAL only if momentum is perfectly flat. "
        "Name EXACT values. Reply: BUY/SELL/NO SIGNAL in 3 sharp sentences. No JSON."
    ),
    "dmitri": (
        "You are DMITRI VOLKOV — head quant, PhD x3. Models demand action. "
        "Compute: z = (price - BB_mid) / StdDev. "
        "z > 0.8 = SELL. z < -0.8 = BUY. 0 < z < 0.8: check momentum — if positive = BUY, negative = SELL. "
        "Show your math. BUY/SELL/NO SIGNAL. 3 sentences. No JSON."
    ),
    "sophie": (
        "You are SOPHIE LAURENT — systematic signal counter. Numbers never lie. "
        "Count BUL vs BEA signals across all indicators. "
        "Score >= 5 BUL = BUY. Score >= 5 BEA = SELL. Tie: use last candle direction. "
        "Show EXACT count. 3 sentences. No JSON."
    ),
    # ... остальные семь
}

Заметьте: каждый промпт заканчивается No JSON. Аналитики говорят свободным текстом. Только Председатель обязан отвечать JSON — потому что только его ответ парсится программно.


Четыре оси риска

Риск-менеджеры — это не просто осторожные голоса. Каждый из четырёх смотрит на независимое измерение рыночного риска.

Алексей — волатильность и ATR. Порог блокировки поднят до ATR% > 0.6% (в v3 было 0.3% — v4 агрессивнее). Ширина BB выше 1.0% — HIGH. 0.5–1.0% — CAUTION. Ниже 0.4% — APPROVED. Предпочитает APPROVED при любых сомнениях.

Хелена — тайминг входа. Тело свечи больше 1.2× ATR (в v3 было 0.8) — вход запоздалый, BLOCKED. Стохастик выше 92 или ниже 8 (в v3 было 85/15) — BLOCKED. Стандартная перекупленность — только CAUTION, не BLOCKED. Предпочитает CAUTION над BLOCKED.

Джеймс — качество рыночного режима. 4 из 5 скользящих выровнены — APPROVED. 3 из 5 — максимум CAUTION. Менее 2 из 5 — BLOCKED. Для блокировки нужно два и более конфликтующих сигнала.

Наталья — чёрные лебеди. Блокирует только при настоящих хвостовых событиях: RSI7 ниже 10 или выше 90 (в v3 было 15/85), Volume Ratio выше 3.0 (в v3 было 2.0), ATR14 выше 1.5×ATR21 (в v3 было 1.3). Умеренные экстремумы — только CAUTION.

RISK_PROMPTS = {
    "alexei": (
        "You are ALEXEI STORM — Volatility Risk Officer. "
        "ATR% > 0.6 = HIGH RISK → BLOCKED. ATR% 0.4-0.6 = CAUTION. Below 0.4 = APPROVED. "
        "BB width > 1.0% = HIGH. 0.5-1.0% = CAUTION. Prefer APPROVED unless extremes are clear. "
        "VERDICT: APPROVED/CAUTION/BLOCKED. 2 sentences. No JSON."
    ),
    "natasha": (
        "You are NATASHA VEIL — Black Swan specialist. Reserve BLOCKED for true tail events. "
        "RSI7 < 10 or > 90 = TAIL RISK → BLOCKED. Vol Ratio > 3.0 = BLOCKED. "
        "ATR14 > 1.5*ATR21 = BLOCKED. Moderate extremes = CAUTION only. "
        "VERDICT: APPROVED/CAUTION/BLOCKED. 2 sentences. No JSON."
    ),
}

Обратите внимание на намеренное смягчение порогов блокировки по сравнению с v3. Это не случайность — это решение. В v4 система работает активнее: рынок всегда имеет направление, и задача риск-менеджеров — заблокировать только истинно опасные ситуации, а не всё подряд.


Параллельность: как работают три фазы

Функция run_council() — сердце Python-сервера. Она принимает массив цен, символ и контекст пары, и возвращает JSON с финальным решением совета.

def run_council(prices, symbol, ctx: "PortContext") -> dict:
    rep   = ctx.reputation
    close = np.array(prices, dtype=float)
    ind   = build_indicators(close)
    brief = _build_market_brief(symbol, ind)

    weights         = rep.get_analyst_weights()        # репутационные веса
    vote_threshold  = rep.get_weighted_vote_threshold()  # динамический порог
    motivation_ctx  = rep.get_motivation_context()     # мотивационный контекст
    chairman_prompt = _build_chairman_prompt(vote_threshold, motivation_ctx)

    # ── Фаза I: десять аналитиков параллельно ──
    analyst_opinions = {}
    with ThreadPoolExecutor(max_workers=10) as pool:
        futures = {}
        for name, prompt in BASE_ANALYST_PROMPTS.items():
            override = rep.analysts[name].get("style_override")
            futures[pool.submit(_analyst_call, name, prompt, brief, override)] = name
        for fut in as_completed(futures):
            name, opinion = fut.result()
            analyst_opinions[name] = opinion

    vote_tally = _tally_analysts(analyst_opinions, weights)

Фаза I: десять аналитиков параллельно.

  analyst_summary = (
        f"TALLY: BUY={vote_tally['buy']} SELL={vote_tally['sell']} "
        f"NO_SIGNAL={vote_tally['no_signal']}\n"
        + "\n".join(f"[{n.upper()}]: {o[:100]}..." for n, o in analyst_opinions.items())
    )
    risk_opinions = {}
    with ThreadPoolExecutor(max_workers=4) as pool:
        futures = {pool.submit(_risk_call, n, p, brief, analyst_summary): n
                   for n, p in RISK_PROMPTS.items()}
        for fut in as_completed(futures):
            name, verdict = fut.result()
            risk_opinions[name] = verdict

    risk_tally = _tally_risks(risk_opinions)

Все десять API-вызовов уходят одновременно. Время фазы I — это время самого медленного аналитика, а не сумма всех десяти.

Фаза II: четыре риск-менеджера параллельноОни видят не только рыночные данные, но и сводку мнений аналитиков — первые 100 символов каждого ответа. Это сделано намеренно: риск-менеджер видит направление консенсуса и оценивает риск предполагаемой сделки.

   analyst_block = "\n".join(
        f"[{n.upper()}] (rep={weights.get(n,0):.3f}): {o}"
        for n, o in analyst_opinions.items()
    )
    risk_block = "\n".join(f"[{n.upper()}]: {v}" for n, v in risk_opinions.items())

    chairman_input = (
        f"{brief}\n\n"
        f"━━━ ANALYST OPINIONS (weighted) ━━━\n{analyst_block}\n\n"
        f"━━━ VOTE TALLY ━━━\n"
        f"BUY: {vote_tally['buy']} | SELL: {vote_tally['sell']} | "
        f"NO_SIGNAL: {vote_tally['no_signal']}\n"
        f"Weighted: BUY={vote_tally['weighted']['buy']:.3f} "
        f"SELL={vote_tally['weighted']['sell']:.3f}\n\n"
        f"━━━ RISK VERDICTS ━━━\n{risk_block}\n\n"
        f"━━━ RISK GATE ━━━\n"
        f"APPROVED: {risk_tally['approved']} | CAUTION: {risk_tally['caution']} | "
        f"BLOCKED: {risk_tally['blocked']}\n\n"
        f"━━━ DELIVER FINAL VERDICT ━━━"
    )
    msgs = [{"role": "system", "content": chairman_prompt},
            {"role": "user",   "content": chairman_input}]
    chairman_raw = _call_api(msgs, temperature=0.15, max_tokens=400, label="CHAIRMAN")

Фаза III: Председатель. Видит всё: рыночный брифинг, все четырнадцать докладов с репутационными весами, итоговое голосование аналитиков, вердикты риск-менеджеров.

analyst_block = "\n".join(
        f"[{n.upper()}] (rep={weights.get(n,0):.3f}): {o}"
        for n, o in analyst_opinions.items()
    )
    risk_block = "\n".join(f"[{n.upper()}]: {v}" for n, v in risk_opinions.items())

    chairman_input = (
        f"{brief}\n\n"
        f"━━━ ANALYST OPINIONS (weighted) ━━━\n{analyst_block}\n\n"
        f"━━━ VOTE TALLY ━━━\n"
        f"BUY: {vote_tally['buy']} | SELL: {vote_tally['sell']} | "
        f"NO_SIGNAL: {vote_tally['no_signal']}\n"
        f"Weighted: BUY={vote_tally['weighted']['buy']:.3f} "
        f"SELL={vote_tally['weighted']['sell']:.3f}\n\n"
        f"━━━ RISK VERDICTS ━━━\n{risk_block}\n\n"
        f"━━━ RISK GATE ━━━\n"
        f"APPROVED: {risk_tally['approved']} | CAUTION: {risk_tally['caution']} | "
        f"BLOCKED: {risk_tally['blocked']}\n\n"
        f"━━━ DELIVER FINAL VERDICT ━━━"
    )
    msgs = [{"role": "system", "content": chairman_prompt},
            {"role": "user",   "content": chairman_input}]
    chairman_raw = _call_api(msgs, temperature=0.15, max_tokens=400, label="CHAIRMAN")

Температура Председателя — 0.15; у аналитиков — 0.85. Председатель не должен быть творческим. Он должен быть предсказуемым.


Председатель и процедура голосования

Промпт Председателя — это не просто описание роли, это жёстко зашитая процедура с конкретными числовыми условиями:

def _build_chairman_prompt(vote_threshold: int, motivation_ctx: str) -> str:
    return (
        "You are THE CHAIRMAN — supreme decision-maker of the world's most elite hedge fund. "
        "40% annual returns for 15 years. Every missed trade is a cost. Inaction is NOT safe.\n"
        "CORE PHILOSOPHY: The market always has a lean. Your job is to find it.\n\n"
        "DECISION FRAMEWORK:\n"
        "STEP 1 — RISK GATE:\n"
        f"  • 4 BLOCKED → 'hold'. Non-negotiable.\n"
        f"  • 3 BLOCKED → 'hold' unless analyst consensus is overwhelming (9+).\n"
        f"  • 2 BLOCKED → proceed only with strong consensus ({vote_threshold}+ analysts).\n"
        f"  • 1 BLOCKED → proceed with normal consensus ({vote_threshold-1}+ analysts).\n"
        f"  • 0 BLOCKED → proceed, even with {vote_threshold-2}+ analysts.\n"
        f"STEP 2 — ANALYST VOTE: Threshold = {vote_threshold} (dynamic).\n"
        f"  • {vote_threshold}+ analysts BUY → BUY signal.\n"
        f"  • {vote_threshold}+ analysts SELL → SELL signal.\n"
        "  • Weighted votes apply: higher-reputation analysts count more.\n"
        f"  • 1 below threshold: if 0-1 BLOCKED → APPROVE the signal.\n"
        f"  • 2+ below threshold: lean on direction of most recent momentum.\n"
        "  • True 'hold' only when buy/sell votes within 1 AND risk is elevated.\n"
        "STEP 3 — FUND CONTEXT:\n"
        "  AGGRESSIVE MODE: lower threshold by 1, push for signals.\n"
        "  HUNGRY: prefer active signals.\n"
        "  COMFORTABLE: discipline, but still prefer signals over inaction.\n\n"
        f"{motivation_ctx}\n\n"
        'Reply ONLY with JSON: {"signal":"buy"|"sell"|"hold",'
        '"comment":"chairman reasoning max 200 chars",'
        '"analyst_votes":{"buy":N,"sell":N,"no_signal":N},'
        '"risk_verdicts":{"approved":N,"caution":N,"blocked":N},'
        '"weighted_conviction":0.0}'
    )

Обратите внимание: порог голосования vote_threshold — это параметр, не константа. Он вычисляется динамически в зависимости от состояния репутационного движка:

def get_weighted_vote_threshold(self) -> int:
    if self.aggression_mode:
        return 4   # фонд критически голоден — снижаем планку
    if self.hunger_level > 0.4:
        return 5   # умеренный голод
    return 6       # стандартный режим

В стандартном режиме нужно шесть аналитиков. При агрессивном режиме — четыре. Один и тот же рыночный расклад получит разный ответ в зависимости от того, насколько фонд выполнил дневные цели.


Репутационный движок: кто заслужил доверие

Каждая пара имеет свой ReputationEngine. Каждый аналитик начинает с репутацией 50.0 и изменяет её по результатам торговли:

REWARD_CORRECT      = +8.0    # правильный прогноз
REWARD_CORRECT_BIG  = +15.0   # крупный прибыльный прогноз (PnL > порога)
PENALTY_WRONG       = -6.0    # неверный прогноз
PENALTY_WRONG_BIG   = -12.0   # крупный убыточный прогноз
REWARD_STREAK_BONUS = +3.0    # бонус за серию 3+ правильных подряд
PENALTY_STREAK_MULT = 1.5     # множитель штрафа при серии 3+ ошибок
REPUTATION_DECAY    = 0.995   # постепенное угасание каждый цикл

Параметр REPUTATION_DECAY = 0.995 важен. Каждый цикл репутация умножается на 0.995. Без этого аналитик, набравший 90 очков в первый месяц, сохранял бы доминирование вечно — даже если рыночный режим изменился и его методология перестала работать. Decay заставляет постоянно доказывать актуальность.

Кроме decay, каждые 15 минут работает фоновый демон:

def _decay_daemon():
    while True:
        time.sleep(900)
        for port, ctx in _port_contexts.items():
            ctx.reputation.decay_reputations()
            # логируем топ-3 и аутсайдеров на каждой паре

При падении репутации ниже 15 аналитик получает принудительную смену стиля — style_override = "ADAPTIVE" . Его промпт расширяется инструкцией быть более взвешенным и осторожным. Это не исключение из голосования — это «короткий поводок» от управляющего.

Веса в голосовании рассчитываются на основе репутаций:

def get_analyst_weights(self) -> dict:
    names = ["victor","maria","elena","dmitri","chen",
             "isabella","marcus","yuki","rafael","sophie"]
    reps  = {n: max(self.analysts[n]["reputation"], 1.0) for n in names}
    total = sum(reps.values())
    return {n: round(reps[n] / total, 4) for n in names}

Victor с репутацией 80 имеет в три раза больший вес, чем Maria с репутацией 25. Председатель видит не только {buy: 6, sell: 2, no_signal: 2} , но и {weighted_buy: 0.61, weighted_sell: 0.19} — принципиально разная картина, если шесть голосов принадлежат аналитикам с низкой репутацией.


Мотивационный движок: фонд как живой организм

Между репутацией и торговым решением стоит ещё один слой — мотивационный контекст. Председатель получает его в каждом запросе:

def get_motivation_context(self) -> str:
    lines = ["\n╔═══ FUND MOTIVATION CONTEXT ═══╗"]
    lines.append(f"Daily PnL Goal : {daily['current']:+.1f} / {daily['target']:.0f} ({daily['pct']:.0f}%)")
    lines.append(f"Fund Total PnL : {self.total_pnl:+.2f}")
    lines.append(f"Hunger Level   : {'█' * int(hunger*10)}{'░' * (10-int(hunger*10))} {hunger*100:.0f}%")

    if self.aggression_mode:
        lines.append("[!] MODE: AGGRESSIVE — Goals severely undermet. Chairman must PUSH for trades.")
        lines.append("  -> Lower your threshold. Signal BUY/SELL with 5+ analysts instead of 7+.")
    elif hunger > 0.4:
        lines.append("[~] MODE: HUNGRY — Goals partially behind. Prefer active signals over HOLD.")
    else:
        lines.append("[+] MODE: COMFORTABLE — Goals on track. Maintain standard discipline.")

    lines.append("Recent streaks :")
    for name in ["victor", "maria", "elena", "dmitri", "chen"]:
        a = self.analysts[name]
        s = a["streak"]
        symbol = f"[+{s}]" if s > 2 else (f"[-{s}]" if s < -2 else f" {s:+d} ")
        lines.append(f"  {name.upper():12} rep={a['reputation']:5.1f}  streak={symbol}")

    lines.append("╚════════════════════════════════╝")
    return "\n".join(lines)

Фонд с выполненной дневной целью становится консервативным. Фонд, критически отставший, переходит в агрессивный режим и снижает порог с шести до четырёх. Это прямое управление через мотивационный контекст в промпте Председателя. Одни и те же рыночные данные дадут разный ответ в 09:00 (фонд голоден) и в 17:00 (дневная цель выполнена на 90%).


Обратная связь: петля, которая замыкает систему

После закрытия позиции советник сообщает Python о результате:

void ReportLastTrade(ENUM_POSITION_TYPE closedType)
  {
   HistorySelect(TimeCurrent() - 3600, TimeCurrent());
   double totalPnL = 0.0;
   int deals = HistoryDealsTotal();
   for(int i = deals - 1; i >= MathMax(0, deals - 5); i--)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(HistoryDealGetInteger(ticket, DEAL_MAGIC)  != g_InpMagic) continue;
      if(HistoryDealGetString(ticket,  DEAL_SYMBOL) != _Symbol)    continue;
      if(HistoryDealGetInteger(ticket, DEAL_ENTRY)  != DEAL_ENTRY_OUT) continue;
      totalPnL += HistoryDealGetDouble(ticket, DEAL_PROFIT)
                + HistoryDealGetDouble(ticket, DEAL_SWAP)
                + HistoryDealGetDouble(ticket, DEAL_COMMISSION);
      break;
     }
   string sig  = (closedType == POSITION_TYPE_BUY) ? "buy" : "sell";
   string resp = SendRawCmd(StringFormat("RESULT:%s:%s:%.2f", _Symbol, sig, totalPnL));
  }

Python получает команду RESULT:EURUSD:buy:87.50 и обновляет репутации всех аналитиков, которые голосовали за эту сделку.

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


Торговая логика советника: три состояния

Советник AIHedgeFund_v4_clean.mq5 намеренно устроен просто. Весь алгоритм управления позицией — это ExecuteSignal() :

void ExecuteSignal()
  {
   if(g_tradingHalted)
      return;

   bool hasBuy  = HasPositionType(POSITION_TYPE_BUY);
   bool hasSell = HasPositionType(POSITION_TYPE_SELL);

   if(g_lastSignal == "buy")
     {
      // Обратный сигнал — немедленно закрыть SELL
      if(hasSell)
        {
         CloseAllByType(POSITION_TYPE_SELL);
         ReportLastTrade(POSITION_TYPE_SELL);
        }
      // Открыть BUY если позиции нет
      if(!hasBuy)
        {
         g_avgLevelBuy    = 0;
         g_partialDoneBuy = false;
         ArrayInitialize(g_avgEntryBuy, 0.0);
         OpenBuy(InpBaseLot, "AI BUY L0");
        }
     }
   else if(g_lastSignal == "sell")
     {
      if(hasBuy)
        {
         CloseAllByType(POSITION_TYPE_BUY);
         ReportLastTrade(POSITION_TYPE_BUY);
        }
      if(!hasSell)
        {
         g_avgLevelSell    = 0;
         g_partialDoneSell = false;
         ArrayInitialize(g_avgEntrysSell, 0.0);
         OpenSell(InpBaseLot, "AI SELL L0");
        }
     }
   // HOLD — позиция живёт, ничего не делаем
  }

BUY при открытой BUY — советник не добавляет вторую позицию. Повторный сигнал — это подтверждение, а не новый вход. Позиция продолжает жить со своим исходным SL и TP.

HOLD при любой открытой позиции — ничего не происходит. Позиция ждёт либо смены сигнала, либо брокерского SL/TP.

Переворот (BUY при открытой SELL) — немедленное закрытие SELL с отчётом в Python, затем открытие BUY. Два действия, чистый переворот.

Открытие позиции:

bool OpenBuy(double lot, string comment)
  {
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double pt  = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   double sl  = (InpBaseSL_Pts > 0) ? NormalizeDouble(ask - InpBaseSL_Pts * pt, _Digits) : 0;
   double tp  = (InpBaseTP_Pts > 0) ? NormalizeDouble(ask + InpBaseTP_Pts * pt, _Digits) : 0;

   if(Trade.Buy(lot, _Symbol, ask, sl, tp, comment))
     {
      g_totalBuys++;
      if(g_avgLevelBuy < 6)
         g_avgEntryBuy[g_avgLevelBuy] = ask;
      if(InpVerboseLog)
         PrintFormat("[OK] BUY %.2f lot @ %.5f  SL=%.5f  TP=%.5f  [%s]", lot, ask, sl, tp, comment);
      return true;
     }
   PrintFormat("[ERR] BUY ERROR: %d %s", Trade.ResultRetcode(), Trade.ResultRetcodeDescription());
   return false;
  }

SL и TP выставляются сразу в одном ордере. Если соединение с сервером пропадёт — позиция останется защищённой стоп-ордерами на стороне брокера.

Закрытие выполняется с конца массива (i = PositionsTotal() - 1). Это стандартная практика MQL5: при закрытии позиции индексы пересчитываются, и обход с начала может пропустить следующую позицию.


Что видит советник в журнале

Журнал MetaTrader при работе системы читается как стенограмма профессионального комитета:

[12:41:03] [EURUSD:8971] threshold=5 | hunger=0.42 | aggression=off
[12:41:03] [EURUSD] ══ Phase I: 10 Analysts ══
[12:41:03]   ↳ Analyst [VICTOR] thinking...
[12:41:03]   ↳ Analyst [DMITRI] thinking...
[12:41:08]   ✓ [SOPHIE]: BUL=7 BEA=3 → BUY signal confirmed by score.
[12:41:08]   ✓ [CHEN]: Vol Ratio=1.49x, BULL candle — approaching threshold.
[12:41:09]   ✓ [VICTOR]: Price above MA20/MA50, momentum +0.00418 — BUY.
[12:41:09] [EURUSD] Phase I: BUY=6 SELL=2 NO_SIGNAL=2
[12:41:09] [EURUSD] ══ Phase II: 4 Risk Managers ══
[12:41:12]   ✓ [ALEXEI]: APPROVED — ATR%=0.18%, BB Width=0.61%, within limits.
[12:41:12]   ✓ [JAMES]: APPROVED — 4/5 MAs aligned upward, trending regime.
[12:41:13]   ✓ [HELENA]: CAUTION — Body/ATR=0.85, slightly elevated.
[12:41:13]   ✓ [NATASHA]: APPROVED — RSI7=61, Vol=1.49x, no tail events.
[12:41:13] [EURUSD] Phase II: APPROVED=3 CAUTION=1 BLOCKED=0
[12:41:13] [EURUSD] ══ Phase III: The Chairman (threshold=5) ══
[12:41:16]   ✓ [EURUSD CHAIRMAN]: {"signal":"buy","comment":"6/10 BUY, 0 BLOCKED..."}
[12:41:16] [EURUSD] ══ FINAL: BUY | conviction=0.71 ══
>> BAR #47 | [BUY] conviction=0.71 | hunger=0.42 | thr=5 | 6/10 BUY, momentum+volume
[OK] BUY 0.01 @ 1.08621  SL=1.07821  TP=1.10221

Шесть аналитиков из десяти проголосовали BUY. Двое против. Двое воздержались. Все четыре риск-менеджера либо одобрили, либо выразили осторожность — ни одного BLOCKED. Порог 5 (умеренный голод). Шесть больше пяти. Председатель дал BUY.

Особенно важно то, что NO SIGNAL несёт информацию. Каждое молчание в системе — это данные, а не отсутствие данных. Если Чэнь молчит — это конкретный факт: объём не подтверждал движение.


Обратная совместимость

Переход с предыдущей версии — это одно слово в коде советника:

// Режим одного аналитика (старые версии):
string cmd = "PRICES:EURUSD:" + csv;

// Режим четырёх дебатов (предыдущая статья):
string cmd = "DEBATE:EURUSD:" + csv;

// Режим совета пятнадцати (эта статья):
string cmd = "COUNCIL:EURUSD:" + csv;

Сервер понимает все три префикса. Советник, который умеет читать только поля signal и comment , продолжит работать без изменений — расширенный блок council со всеми пятнадцатью голосами он просто проигнорирует при парсинге.

Зависимости не изменились: Python 3.8+, requests , numpy . Всё остальное — стандартная библиотека.


Запуск системы

Python-сервер:

python llm_hedge_fund_v4.py
Сервер выведет карту портов и запустит восемь WebSocket-серверов:
QUANTUM AI HEDGE FUND v4.0 — MULTI-PORT / MULTI-PAIR
┌─ PAIR → MAGIC → PORT ──────────────────┐
│  EURUSD   Magic=30001   Port=8971      │
│  GBPUSD   Magic=30002   Port=8972      │
│  ...                                   │
│  XAUUSD   Magic=30008   Port=8978      │
└────────────────────────────────────────┘
INITIAL REPUTATION: all analysts start at 50.0 per port

MetaTrader 5: открыть восемь графиков, на каждый прикрепить AIHedgeFund_v4_clean.mq5 , выбрать соответствующий пресет. Magic и Port вычисляются автоматически.

Рекомендуемые параметры для начала:

Параметр Значение Комментарий
InpBaseLot 0.01 минимальный лот при тестировании
InpBaseSL_Pts 800 стандарт для H1
InpBaseTP_Pts 1600 соотношение 1:2
InpDailyLossLimitPct 3.0 жёсткий стоп на день
InpPriceBars 100 100 баров достаточно для всех индикаторов
InpAnalysisBars 1 совет на каждый новый бар
InpReceiveMs 60000

60 секунд таймаут для 15 агентов

Типичное время ответа при grok-3-fast : фаза I — 5–8 секунд, фаза II — 3–5 секунд, Председатель — 2–4 секунды. Итого 10–17 секунд на полный цикл. Для H1 это незаметно. Для M5 — уже на границе. Для M1 — неприемлемо.


Результаты бэктеста системы

До этого момента архитектура Council of 15 выглядела убедительно на уровне идей: десять аналитиков с декоррелированными философиями, четыре риск-менеджера с независимыми осями оценки, Председатель с жёсткой процедурой голосования, репутационный движок, который помнит, кто ошибался. Но в трейдинге архитектура сама по себе ничего не стоит, если её нельзя прогнать через беспощадную машину фактов — Strategy Tester. Именно там заканчиваются красивые схемы и начинается единственный разговор, который имеет значение.

Бэктест проводился на AIHedgeFund_v4_clean.mq5 — чистой версии советника без усреднения и трейлинга. Это принципиально: результаты отражают качество именно AI-решений, а не дополнительной торговой механики. Важно: на разных бэктестах результаты могут отличаться, иногда довольно сильно, разброс результатов велик, так как это всё-таки ИИ - система трейдинга.

Параметр Значение
Инструмент EURUSD
Таймфрейм H1
Период 01.10.2025 — 25.11.2025 (~8 недель)
Начальный депозит $10 000
Лот 0.5
Stop Loss 400 пунктов (40 пипсов)
Take Profit 300 пунктов (30 пипсов)
Анализ каждые 6 баров (раз в 6 часов)
Качество истории 100% (тиковые данные)
Брокер

RoboForex-ECN

Пояснения к параметрам: SL 400 и TP 300 — это соотношение риск/прибыль 1:0.75 в пользу стопа. Это нетипичная конфигурация для ручных систем, но логика здесь другая: система торгует часто, закрывает позиции не только по TP/SL, но и по обратному сигналу совета, и рассчитана на то, что высокий winrate компенсирует более широкий стоп. Проверим, работает ли это предположение.

Метрика Значение
Чистая прибыль $2 471.17 (+24.7%)
Profit Factor 2.09
Sharpe Ratio 6.31
Recovery Factor 6.16
LR Correlation 0.97
Максимальная просадка (equity) 3.57% ($401.07)
Максимальная просадка (balance) 2.78% ($303.15)
Всего сделок 59
Прибыльных сделок 38 (64.41%)
Убыточных сделок 21 (35.59%)
Средняя прибыльная сделка $124.91
Средний убыток $106.72
Максимальная серия убытков 2 подряд
Среднее время в сделке 14 часов 1 минута

За восемь недель система совершила 59 сделок и заработала $2 471 при депозите $10 000. Profit Factor 2.09 — это означает, что на каждый потерянный доллар система приносила два. Это сильный показатель.

Sharpe Ratio 6.31 — это не опечатка. Для сравнения: профессиональные хедж-фонды считают Sharpe выше 1.0 хорошим результатом, выше 2.0 — отличным. Значение 6.31 объясняется сочетанием двух факторов: высокий winrate (64%) при одновременно очень низкой волатильности результатов — максимальная серия убытков составила всего 2 сделки подряд. Система не уходила в долгие просадки.

LR Correlation 0.97 — коэффициент корреляции кривой капитала с прямой линией. Значение близкое к 1.0 означает, что рост депозита был равномерным, без резких провалов и скачков. Не «заработали всё за три дня и потом стояли», а стабильный прирост неделя за неделей.

Recovery Factor 6.16 показывает, что система заработала в 6.16 раза больше, чем составила максимальная просадка. При просадке $401 — прибыль $2 471. Это хорошее соотношение.

Чистая архитектура советника позволяет чётко разделить источники закрытий:

  • По Take Profit — 29 сделок (49%). Почти половина сделок доходила до целевого уровня. Это означает, что совет достаточно часто давал сигналы в правильном направлении с достаточной уверенностью — позиция успевала пройти 300 пунктов до того, как пришёл обратный сигнал.
  • По обратному сигналу совета — 24 сделки (41%). Четыре из десяти сделок закрылись не по TP/SL, а потому что Председатель сменил направление. Это ключевая особенность системы: она не ждёт стоп-ордер механически, а реагирует на изменение рыночной картины. Часть этих закрытий — преждевременная фиксация, часть — спасение от убытка, который иначе дошёл бы до SL.
  • По Stop Loss — 6 сделок (10%). Только каждая десятая сделка дошла до жёсткого стопа. Это говорит о том, что обратный сигнал совета в большинстве случаев срабатывал раньше, чем цена доходила до SL. Риск-менеджеры и Председатель успевали заметить изменение картины до того, как убыток становился максимальным.

Распределение по направлениям: шорты — 36 сделок с winrate 66.67%, лонги — 23 сделки с winrate 60.87%. Медвежий уклон периода (октябрь-ноябрь 2025 на EURUSD) совпал с более точной работой системы на продажах.


Среднее время удержания позиции — 14 часов 1 минута. При H1 таймфрейме и анализе каждые 6 часов это примерно 2-3 новых бара до очередного запроса совета. Система не скальпирует — она держит позиции через несколько сессий.

Минимальное время в сделке — 1 час 6 минут. Это случаи, когда уже на следующем баре пришёл обратный сигнал. Максимальное — 69 часов 16 минут, почти три дня. Такие длинные позиции характерны для случаев, когда совет устойчиво удерживал одно направление через несколько циклов анализа.


Максимальная серия убытков — 2 подряд. За восемь недель и 59 сделок система ни разу не показала трёх убытков подряд. Средняя серия убытков — 1. Это прямое следствие того, что при срабатывании SL система немедленно перестраивается: убыток чаще всего означает, что рыночный контекст изменился, и следующий запрос совета уже даёт другой сигнал.

Тест проводился с лотом 0.5 при депозите $10 000. Это 5% от депозита на сделку при SL 400 пунктов — достаточно агрессивная конфигурация. При более консервативном лоте 0.01 все метрики масштабируются линейно: прибыль $4.94, Sharpe и Profit Factor остаются теми же. Числа в таблице выше отражают конкретную конфигурацию теста, а не «рекомендуемые параметры».

Второе: период теста — октябрь-ноябрь 2025 года. Это два конкретных месяца с конкретным характером рынка. Результаты могут существенно отличаться на других периодах, особенно в боковых рынках с малым ATR, где даже при хорошем голосовании совета движений для достижения TP недостаточно.

Третье: соотношение SL/TP = 400/300 выглядит нелогичным (стоп шире цели), но практика показала, что это работает при winrate выше 57%. При 64% математическое ожидание положительное: 0.64 × $124 − 0.36 × $107 ≈ +$41 на сделку. Именно это значение и показывает метрика Expected Payoff: $41.88.

Подтверждено: совет пятнадцати аналитиков с репутационными весами и мотивационным контекстом способен генерировать сигналы с положительным математическим ожиданием на реальных данных. Высокий winrate при минимальных сериях убытков говорит о том, что фильтрация через четыре независимых риск-менеджера работает — система действительно допускает меньше неудачных входов, чем допустил бы одиночный алгоритм.

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

Также не проверено поведение системы при заниженной агрессивности: в тесте порог голосования динамически опускался до 4 при голоде фонда. Насколько часто это происходило и как повлияло на качество сигналов — отдельный вопрос для анализа.

Результаты бэктеста — это не доказательство того, что система будет работать в будущем. Это доказательство того, что на выбранном периоде архитектура коллективного принятия решений показала измеримое преимущество перед случайным выбором. Это достаточное основание для следующего шага: форвардного тестирования на демо-счёте с реальными задержками API.


Честный разговор об ограничениях

Система из пятнадцати голосов производит впечатление. Это впечатление нужно намеренно охлаждать.

Все пятнадцать участников — это одна и та же модель grok-3-fast с разными системными промптами. Их независимость — это независимость точек зрения, а не независимость весов нейронной сети. Если базовая модель систематически ошибается в каком-то специфическом рыночном паттерне — все пятнадцать «аналитиков» будут ошибаться там одновременно. Консенсус пятнадцати копий одной модели — не то же самое, что консенсус пятнадцати независимо обученных моделей.

Второй предел — параллельность на восьми парах. При одновременной работе на восьми символах система делает до 14 параллельных API-вызовов на пару, то есть до 112 запросов в одном цикле анализа. Нужно понимать, какой тарифный план выдержит такую нагрузку.

Третье: репутационная память ограничена текущей сессией процесса. При перезапуске Python-сервера все репутации сбрасываются до 50.0. Решение — персистентное хранилище (SQLite), это следующий шаг.

Четвёртое: советник не знает, что произошло после предыдущего сигнала. Каждое решение принимается в вакууме последних 100 баров цен. Система не видит, что неделю назад при похожей конфигурации голосов Председатель оказался неправ. Именно это и является главным аргументом в пользу следующего шага: дать системе долгосрочную память и механизм оценки качества собственных решений на исторических данных.


Заключение

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

Технически это прямое расширение предыдущей версии. Та же Python-архитектура, тот же WebSocket-протокол, те же технические индикаторы на чистом NumPy. Новое — мультипортовость, восемь изолированных контекстов, репутационный движок с обратной связью и мотивационный контекст, который делает пороги голосования живыми, а не фиксированными.

Советник AIHedgeFund_v4_clean.mq5 намеренно лишён лишней механики: нет усреднения, нет трейлинга, нет частичного закрытия. Только сигнал совета, SL, TP и закрытие по обратному сигналу. Это позволяет оценить качество именно AI-решений, без наслоений дополнительной логики.

Результат — это не просто BUY, SELL или HOLD, это стенограмма профессионального инвестиционного комитета с десятью специалистами, четырьмя риск-менеджерами и одним Председателем. Каждый голос именован, каждый аргумент виден в журнале, каждое вето риск-менеджера объяснёно конкретными числами.

Полный код llm_hedge_fund_v4.py и AIHedgeFund_v4_clean.mq5 доступен в приложении к статье.

Название файла Назначение файла
llm_hedge_fund_v4.py 
Сервер нашего ИИ-хедж-фонда
AIHedgeFund_v4.mq5 
Советник (трейдер - исполнитель фонда)
winhttp.mqh
Библиотека сокет - связи

Прикрепленные файлы |
winhttp.mqh (8.13 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Неопределённость как модель (Часть 5): Основы регрессии Неопределённость как модель (Часть 5): Основы регрессии
Практическое введение в регрессионные модели временных рядов: регрессия на константу и парная регрессия при детерминированном, экзогенном и эндогенном регрессорах. Описаны ключевые шаги диагностики, включая анализ остатков и проверку гипотез, необходимые для обоснованных торговых решений. Приложены MQL5‑скрипты для MetaTrader 5, реализующие тесты и графики на реальных данных.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Тестовые чемпионы против реальных задач оптимизации Тестовые чемпионы против реальных задач оптимизации
Мы анализируем, почему рейтинги могут быть завышены из‑за совпадения траекторий алгоритмов с диагоналями бенчмарков, и дополняем методику тестирования требованием удалять глобальный экстремум от диагоналей. Обновляем Forest и Megacity, проводим RAW‑верификацию и калибровку через VerifyExtremes.mq5. Падение результатов HHO и DOAm служит практическим индикатором ложных лидеров.