How to Set Up an AI Hedge Fund in MetaTrader 5
The previous article ended with an honest admission: a system consisting of several analysts is a good start, but it is not the end of the conversation. The council demonstrated that several voices are better than one, but even fifteen voices working together on a single pair still amount to a small room.
Imagine the management committee of a real fund. Ten analysts with fundamentally different philosophies are sitting at the same table. Victor focuses solely on the trend and the alignment of moving averages — for him, a price above the MA50 is already sufficient grounds for a BUY. Dmitry, who holds three PhDs, calculates the z-score for the deviation from the Bollinger midline and will not generate a signal until that number crosses the 0.8 threshold. Chen does not react to anything without volume confirmation. Yuki’s primary focus is the body of the last candle, almost to the exclusion of everything else. Seated at a separate table are four risk managers, each with their own independent “axes of fear”: one measures the ATR, the second evaluates entry timing, the third assesses market regime quality, and the fourth looks for tail events. And presiding over them all is the Chairman, who reads all fourteen reports and enforces a strict three-step voting procedure.
Now multiply that by eight. Eight currency pairs, eight isolated councils, eight independent reputation engines — all within a single Python process.
That is exactly what this article describes.
Why One Pair Was Not Enough
The previous architecture operated on a single trading symbol. This meant that the system had a blind spot the size of the entire rest of the market.
Currencies move in correlation with one another. EURUSD and GBPUSD often move in tandem because both are exposed to dollar risk. USDJPY behaves like a safe-haven asset during times of panic. XAUUSD is a completely different beast with its own seasonal patterns and reaction to inflation. An analyst who trades only EURUSD and doesn't see what is happening with the yen is working with one eye closed.
But it's not just about correlations. Different currency pairs offer different numbers of trading opportunities depending on the market regime. When EURUSD is stuck in a narrow range and the council of fifteen keeps saying HOLD over and over, GBPUSD may at that same time be in a clean trend, with seven analysts voting BUY and zero BLOCKED. A system that can listen to only one voice of the market misses out on everything else.
The solution is obvious: run eight isolated councils in parallel. Each one works on its own pair, with its own reputation history, PnL targets, and motivational state.
Architecture: One Process, Eight Worlds
The technical core of the system is the PortContext class. This is an isolated state container for a single currency pair:
class PortContext: """Isolated context for a single port (a single currency pair).""" def __init__(self, port: int, pair: str): self.port = port self.pair = pair self.reputation = ReputationEngine() # its own reputation engine self.chat_history = [] # its own chat history self.history_lock = threading.Lock()
Eight PortContext instances are created at startup and exist independently. Victor's reputation on EURUSD has no effect on his reputation on XAUUSD. The fund’s daily hunger for the yen does not make the franc council any less demanding. Each pair is a world of its own.
The port map is fixed and symmetric:
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 — preset number (30001–30008). Port — Magic − 30000 + 8970. This allows any component in the system to reconstruct the entire context from a single number: who is trading, which port it listens on, and which Magic value it uses to tag orders.
In main(), everything is launched in three lines:
def main(): for pair, (magic, port) in PAIR_PORTS.items(): _port_contexts[port] = PortContext(port, pair) # create contexts for port in ALL_PORTS: threading.Thread(target=_run_port_server, # start the servers args=(port,), daemon=True).start() threading.Thread(target=_decay_daemon, daemon=True).start() # reputation decay while True: time.sleep(60) # the main thread simply waits
Eight TCP servers listen on eight ports. Each incoming WebSocket connection receives its own PortContext based on the port number and operates completely independently of the others.
MQL5: Preset as the Only Parameter
On the MetaTrader side, the trader performs a single action — selects a preset. Everything else is calculated automatically.
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 };
In OnInit(), a single line maps the preset to Magic and Port:
int OnInit() { // ── Calculate Magic and Port from the preset ────────────────────────────── g_InpMagic = (int)InpPairPreset; g_InpPort = (int)InpPairPreset - 30000 + 8970; // 30001→8971 … 30008→8978 // ── Checking whether the chart symbol matches the preset ───────────────── string presetSymbol = PresetToSymbol(InpPairPreset); if(presetSymbol != "" && _Symbol != presetSymbol) { PrintFormat("[WARN] Preset=%s, but chart=%s. Make sure EA is set on the correct chart!", presetSymbol, _Symbol); } PrintFormat("[INIT] Preset=%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(); // Connecting to the server if(InitWebSocket()) { g_useWS = true; PrintFormat("[OK] WinHTTP WebSocket connected → %s:%d (Magic=%d)", InpHost, g_InpPort, g_InpMagic); } else { PrintFormat("[X] WebSocket unavailable. Port=%d. Launch: python llm_hedge_fund_v4.py", g_InpPort); return INIT_FAILED; } SendGoals(); return INIT_SUCCEEDED; }
The PresetToSymbol() function checks for consistency: if a trader has applied a GBPUSD preset to a EURUSD chart, the Expert Advisor will issue a warning in the log but will continue to operate on the current symbol. This is a safeguard against typos, not a strict block.
The input parameters have been intentionally kept to a minimum — only what the trader must decide for themselves:
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;
No averaging, trailing, or session filter parameters. The Expert Advisor does one thing: it listens to the council of fifteen and opens or closes a position.
Ten Analysts: A Gallery of Philosophies
The selection of participants for the first phase is not arbitrary. Each one represents a distinct trading school with its own logic and its own blind spots. It is precisely the mismatch between these blind spots that creates the ensemble's value.
Victor is a trend trader. A BUY signal if the price is above the MA20 or MA50 with positive momentum. A SELL signal is symmetric. NO SIGNAL — only when the momentum is perfectly flat. He never argues with himself — he always sides with the stronger argument.
Maria is a contrarian bear with strong convictions. She looks for SELL first and foremost. An RSI above 65 and the price at the upper Bollinger Band — that is enough for her. When the market is oversold, she can issue a BUY signal. Her value lies in shouting “Stop!” precisely when everyone else is shouting “Go!”
Elena is a specialist in mean reversion. She operates only at extremes: a touch of the lower band with RSI7 below 35 means BUY. A touch of the upper band with RSI7 above 65 means SELL. Near the middle band, she uses the direction of the latest momentum. NO SIGNAL only in the RSI7 dead zone from 45 to 55, strictly at the middle band.
Dmitry is a quant with a PhD. Calculates the z-score: z = (price − BB_mid) / StdDev. Z above 0.8 means SELL. Below −0.8 means BUY. Between zero and 0.8, he looks at momentum. Shows the math in the answer.
Chen is an expert on volume and microstructure. A Volume Ratio above 1.2 plus a bullish candle means BUY. Plus a bearish candle means SELL. When volume is low, he focuses on the direction of the candle. NO SIGNAL — only when volume is below 0.5× the average and there is a perfect doji.
Isabella is a volatility trader. Volatility expansion within an uptrend — BUY. In a downtrend — SELL. When volatility contracts, she trades in the direction of the trend. NO SIGNAL — only when both the trend and volatility are perfectly neutral at the same time.
Marcus is a Wyckoff analyst. Looks for a spring (a false breakout to the downside followed by a reversal) or an upthrust (a false breakout to the upside followed by a rejection). In the absence of a clear Wyckoff event, he follows the trend. He can read the footprints of institutional money.
Yuki is a Japanese candlestick expert. A bullish candle with a body larger than 0.3 ATR — BUY. A bearish candle under the same condition — SELL. With a small body, she checks its position: near the lower band it gives a BUY, near the upper band she gives a SELL; otherwise, she follows momentum. NO SIGNAL — only on a perfect doji with a body less than 0.1 ATR, strictly at the middle band.
Raphael is an expert on divergences. All momentum indicators are pointing upward—BUY. Down — SELL. When signals are mixed: the majority (2/3) determines the direction. NO SIGNAL — only on a perfect 50/50 split. Its signals are strongest when all the oscillators are in sync.
Sophie is a systematic trader. Counts binary checks: is the price above MA20? Is RSI14 above 50? Is Stochastic %K above %D? That makes ten questions in all. Five or more bullish answers — BUY. Five or more bearish answers — SELL. In the event of a tie, the last candle decides.
In the code, each analyst is a system prompt in the BASE_ANALYST_PROMPTS dictionary:
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."
),
# ... the remaining seven
} Note: each prompt ends with "No JSON." Analysts speak in free-form text. Only the Chairman is required to respond in JSON, because only the Chairman's response is parsed programmatically.
Four Risk Axes
Risk managers aren't just voices of caution. Each of the four looks at an independent dimension of market risk.
Alexey — volatility and ATR. The blocking threshold has been raised to ATR% > 0.6% (in v3 it was 0.3% — v4 is more aggressive). BB width above 1.0% — HIGH. 0.5–1.0% — CAUTION. Below 0.4% — APPROVED. Prefers APPROVED whenever there is any doubt.
Helena — entry timing. Candle body greater than 1.2× ATR (it was 0.8 in v3) — the entry is late, BLOCKED. Stochastic above 92 or below 8 (in v3 it was 85/15) — BLOCKED. Standard overbought conditions are CAUTION only, not BLOCKED. Prefers CAUTION over BLOCKED.
James — market regime quality. 4 out of 5 moving averages are aligned — APPROVED. 3 out of 5 — at most CAUTION. Fewer than 2 out of 5 — BLOCKED. Blocking requires two or more conflicting signals.
Natalia — black swans. Blocks only on true tail events: RSI7 below 10 or above 90 (in v3 it was 15/85), Volume Ratio above 3.0 (in v3 it was 2.0), ATR14 above 1.5×ATR21 (in v3 it was 1.3). Moderate extremes are CAUTION only.
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."
),
} Note the intentional relaxation of blocking thresholds compared to v3. This is not accidental — it is a decision. In v4, the system operates more actively: the market always has a direction, and the risk managers' job is to block only truly dangerous situations, not everything indiscriminately.
Parallelism: How the Three Phases Work
The `run_council()` function is the heart of the Python server. It takes an array of prices, a trading symbol, and a pair context, and returns JSON containing the council's final decision.
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() # reputation weights vote_threshold = rep.get_weighted_vote_threshold() # dynamic threshold motivation_ctx = rep.get_motivation_context() # motivational context chairman_prompt = _build_chairman_prompt(vote_threshold, motivation_ctx) # ── Phase I: Ten Analysts in Parallel ── 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)
Phase I: Ten Analysts in Parallel.
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)
All ten API calls are sent simultaneously. The duration of Phase I is the time taken by the slowest analyst, not the sum of all ten.
Phase II: Four Risk Managers in Parallel. They see not only market data but also a summary of the analysts’ opinions — the first 100 characters of each response. This is intentional: the risk manager sees the direction of the consensus and assesses the risk of the proposed trade.
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")
Phase III: Chairman. The Chairman sees everything: the market briefing, all fourteen reports with reputation weights, the analysts’ final vote, and the risk managers’ verdicts.
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")
The Chairman’s temperature is 0.15; the analysts’ temperature is 0.85. The Chairman should not be creative. The Chairman must be predictable.
Chairman and Voting Procedure
The Chairman’s prompt is not just a description of the role; it is a hard-coded procedure with specific numerical conditions:
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}' )
Note: the voting threshold `vote_threshold` is a parameter, not a constant. It is calculated dynamically depending on the state of the reputation engine:
def get_weighted_vote_threshold(self) -> int: if self.aggression_mode: return 4 # the fund is critically hungry — we lower the bar if self.hunger_level > 0.4: return 5 # moderate hunger return 6 # standard mode
In standard mode, six analysts are needed. In aggressive mode, four are needed. The same market setup will produce a different response depending on how far the fund has met its daily targets.
Reputation Engine: Who Has Earned Trust
Each pair has its own ReputationEngine. Each analyst starts with a reputation of 50.0, which changes based on trading results:
REWARD_CORRECT = +8.0 # correct prediction REWARD_CORRECT_BIG = +15.0 # large profitable prediction (PnL > threshold) PENALTY_WRONG = -6.0 # incorrect prediction PENALTY_WRONG_BIG = -12.0 # large losing prediction REWARD_STREAK_BONUS = +3.0 # bonus for a streak of 3+ correct predictions in a row PENALTY_STREAK_MULT = 1.5 # penalty multiplier for a streak of 3+ errors REPUTATION_DECAY = 0.995 # gradual reputation decay each cycle
The REPUTATION_DECAY parameter, set to 0.995, is important. Each cycle, reputation is multiplied by 0.995. Without this, an analyst who scored 90 points in the first month would remain dominant forever — even if the market regime changed and their methodology stopped working. Reputation decay forces an analyst to constantly prove their relevance.
In addition to decay, a background daemon runs every 15 minutes:
def _decay_daemon(): while True: time.sleep(900) for port, ctx in _port_contexts.items(): ctx.reputation.decay_reputations() # We log the top 3 and the outsiders for each pair
If an analyst’s reputation drops below 15, they are assigned a forced style change — style_override = "ADAPTIVE". Their prompt is expanded with an instruction to be more balanced and cautious. This is not an exclusion from voting — it is a “short leash” from the manager.
Voting weights are calculated based on reputations:
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, with a reputation of 80, has three times the weight of Maria, who has a reputation of 25. The Chairman sees not only {buy: 6, sell: 2, no_signal: 2}, but also {weighted_buy: 0.61, weighted_sell: 0.19} — a fundamentally different picture if the six votes belong to analysts with low reputations.
The Motivational Engine: The Fund as a Living Organism
Between reputation and a trading decision lies another layer — the motivational context. The Chairman receives it in every request:
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)
A fund that has met its daily target becomes conservative. A fund that has fallen critically behind switches to an aggressive mode and lowers the vote threshold from six to four. This is direct control through the motivational context in the Chairman’s prompt. The same market data will yield different answers at 09:00 (the fund is hungry) and at 17:00 (the daily target is 90% met).
Feedback: the Loop that Closes the System
After closing the position, the Expert Advisor reports the result to 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 receives the command RESULT:EURUSD:buy:87.50 and updates the reputations of all analysts who voted for this trade.
This creates a closed feedback loop: trade results affect reputations, reputations affect voting weights, and voting weights affect the next decision. The system does not just make decisions — it learns from their consequences, albeit in a limited way: it knows whose predictions turned out to be correct, but it does not know why.
Trading Logic of the Expert Advisor: Three States
The AIHedgeFund_v4_clean.mq5 Expert Advisor is intentionally kept simple. The entire position management algorithm is the ExecuteSignal() function:
void ExecuteSignal() { if(g_tradingHalted) return; bool hasBuy = HasPositionType(POSITION_TYPE_BUY); bool hasSell = HasPositionType(POSITION_TYPE_SELL); if(g_lastSignal == "buy") { // Reversal signal—close the SELL position immediately if(hasSell) { CloseAllByType(POSITION_TYPE_SELL); ReportLastTrade(POSITION_TYPE_SELL); } // Open a BUY position if there is no open position 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—the position stays open; no action is taken }
BUY when a BUY position is already open — the Expert Advisor does not open a second position. A repeated signal is confirmation, not a new entry. The position remains open with its original stop loss (SL) and take profit (TP) levels.
HOLD with any open position — nothing happens. The position waits for either a signal change or the broker-side SL/TP.
Position reversal (BUY when a SELL position is open) — close the SELL position immediately with a report sent to Python, then open a BUY position. Two actions, a clean reversal.
Opening a position:
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 and TP are placed immediately in a single order. If the connection to the server is lost, the position will remain protected by stop orders on the broker side.
Positions are closed starting from the end of the array (i = PositionsTotal() - 1). This is standard practice in MQL5: when a position is closed, the indices are recalculated, and iterating from the beginning may skip the next position.
What the Expert Advisor Sees in the Log
When the system is running, the MetaTrader log reads like the transcript of a professional committee meeting:
[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
Six out of ten analysts voted BUY. Two against. Two abstained. All four risk managers either approved it or expressed caution — not a single BLOCKED. Vote threshold: 5 (moderate hunger). Six is greater than five. The Chairman issued BUY.
What is particularly important is that "NO SIGNAL" conveys information. Every silence in the system is data, not the absence of data. If Chen is silent, that is a concrete fact: volume did not confirm the move.
Backward Compatibility
Migrating from the previous version means changing just one word in the Expert Advisor code:
// Single-Analyst Mode (older versions): string cmd = "PRICES:EURUSD:" + csv; // Four-Debate Mode (previous article): string cmd = "DEBATE:EURUSD:" + csv; // Council of Fifteen mode (this article): string cmd = "COUNCIL:EURUSD:" + csv;
The server recognizes all three prefixes. An Expert Advisor that can read only the `signal` and `comment` fields will continue to work unchanged — it will simply ignore the extended `council` block with all fifteen votes during parsing.
The dependencies remain unchanged: Python 3.8+, `requests`, `numpy`. Everything else is part of the standard library.
System Startup
Python server:
python llm_hedge_fund_v4.pyThe server will display a port map and start eight WebSocket servers:
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: open eight charts, attach AIHedgeFund_v4_clean.mq5 to each one, and select the appropriate preset. Magic and Port are calculated automatically.
Recommended settings to get started:
| Parameter | Value | Comment |
|---|---|---|
| InpBaseLot | 0.01 | Minimum lot size during testing |
| InpBaseSL_Pts | 800 | Standard for H1 |
| InpBaseTP_Pts | 1,600 | A 1:2 ratio |
| InpDailyLossLimitPct | 3.0 | A hard stop for the day |
| InpPriceBars | 100 | 100 bars are enough for all indicators |
| InpAnalysisBars | 1 | Consult the council on each new bar |
| InpReceiveMs | 60,000 | A 60-second timeout for 15 agents |
Typical response times for grok-3-fast: Phase I — 5–8 seconds, Phase II — 3–5 seconds, Chairman — 2–4 seconds. A total of 10–17 seconds for a full cycle. For H1, this is hardly noticeable. For M5, it is already on the edge. For M1, this is unacceptable.
System Backtest Results
Up to that point, the Council of 15 architecture had looked convincing as a concept: ten analysts with decorrelated philosophies, four risk managers with independent axes of assessment, a Chair manwith a strict voting procedure, and a reputation engine that remembers who made mistakes. But in trading, architecture is worthless on its own if it cannot be run through the ruthless machine of facts — the Strategy Tester. That is where the beautiful diagrams end and the only conversation that matters begins.

The backtest was conducted on AIHedgeFund_v4_clean.mq5 — the clean version of the Expert Advisor without averaging or trailing stops. This is crucial: the results reflect the quality of the AI decisions themselves, not that of any additional trading mechanics. Important: results can differ across backtests, sometimes quite significantly; the dispersion of results is large, because this is, after all, an AI trading system.
| Parameter | Value |
|---|---|
| Instrument | EURUSD |
| Timeframe | H1 |
| Period | October 1, 2025 – November 25, 2025 (~8 weeks) |
| Initial deposit | USD 10,000 |
| Lot | 0.5 |
| Stop Loss | 400 points (40 pips) |
| Take Profit | 300 points (30 pips) |
| Analysis | every 6 bars (once every 6 hours) |
| History quality | 100% (tick data) |
| Broker | RoboForex-ECN |
Explanation of the parameters: SL 400 and TP 300 represent a risk-to-reward ratio of 1:0.75 in favor of the stop loss. This is an atypical configuration for manual trading systems, but the logic here is different: the system trades frequently, closes positions not only at TP/SL but also on an opposite signal from the council, and is designed with the expectation that a high win rate will compensate for the wider stop. Let's see if this assumption holds true.
| Metric | Value |
|---|---|
| Net Profit | USD 2,471.17 (+24.7%) |
| Profit Factor | 2.09 |
| Sharpe Ratio | 6.31 |
| Recovery Factor | 6.16 |
| LR Correlation | 0.97 |
| Maximum drawdown (equity) | 3.57% (USD 401.07) |
| Maximum drawdown (balance) | 2.78% (USD 303.15) |
| Total trades | 59 |
| Profitable trades | 38 (64.41%) |
| Losing trades | 21 (35.59%) |
| Average profitable trade | USD 124.91 |
| Average loss | USD 106.72 |
| Longest losing streak | 2 in a row |
| Average time in a trade | 14 hours 1 minute |
Over eight weeks, the system executed 59 trades and earned USD 2,471 on a USD 10,000 deposit. A Profit Factor of 2.09 means that for every dollar lost, the system generated two dollars. That is a strong metric.

A Sharpe Ratio of 6.31 is not a typo. For comparison, professional hedge funds consider a Sharpe Ratio above 1.0 a good result, and above 2.0 an excellent one. The value of 6.31 is explained by a combination of two factors: a high win rate (64%) together with very low volatility of results — the longest losing streak was only 2 trades in a row. The system did not experience any prolonged drawdowns.

LR Correlation 0.97 — the correlation coefficient between the equity curve and a straight line. A value close to 1.0 indicates that the deposit grew steadily, without any sharp drops or spikes. It was not a case of “making all the money in three days and then sitting on it,” but rather steady growth week after week.
The Recovery Factor of 6.16 indicates that the system earned 6.16 times more than the maximum drawdown. With a drawdown of USD 401, the profit was USD 2471. That is a good ratio.
The clean architecture of the Expert Advisor makes it possible to clearly separate the sources of exits:
- By Take Profit — 29 trades (49%). Nearly half of the trades reached their target level. This means that the council quite often issued signals in the right direction with sufficient confidence — the position had time to move 300 points before an opposite signal arrived.
- On an opposite signal from the council — 24 trades (41%). Four out of ten trades were closed not by TP/SL, but because the Chairman changed direction. This is a key feature of the system: it does not mechanically wait for a stop order to trigger, but reacts to changes in the market picture. Some of these exits are early profit-taking, while others are a way to avoid a loss that would otherwise have reached the stop loss (SL).
- By Stop Loss — 6 trades (10%). Only one in ten trades reached the hard stop. This suggests that, in most cases, the council’s opposite signal came before the price reached the stop loss (SL). The risk managers and the Chairman managed to notice the change in the market picture before the loss became maximum.
Breakdown by direction: shorts — 36 trades with a win rate of 66.67%; longs — 23 trades with a win rate of 60.87%. The bearish bias during that period (October–November 2025 on EUR/USD) coincided with the system performing more accurately on sell trades.

The average position holding time is 14 hours and 1 minute. With an H1 timeframe and analysis every 6 hours, that is roughly 2–3 new bars before the next request to the council. The system does not scalp — it holds positions across several trading sessions.
The minimum time in a trade is 1 hour and 6 minutes. These are instances where an opposite signal arrived on the very next bar. The maximum is 69 hours and 16 minutes — almost three days. Positions held for such a long time are typical of cases where the council consistently maintained the same direction over several analysis cycles.

The maximum losing streak is 2 in a row. Over the course of eight weeks and 59 trades, the system never recorded three consecutive losses. The average losing streak is 1. This is a direct consequence of the fact that when the stop loss is triggered, the system immediately readjusts: a loss most often means that the market context has changed, and the next request to the council already produces a different signal.
The test was conducted using a 0.5 lot with a deposit of USD 10,000. That is 5% of the deposit per trade with a 400-point SL — a fairly aggressive setup. With a more conservative lot size of 0.01, all metrics scale linearly: profit is USD 4.94, while the Sharpe ratio and Profit Factor remain the same. The numbers in the table above reflect a specific test configuration, not “recommended parameters.”
Second: the test period is October–November 2025. These are two specific months with a specific market character. Results may differ significantly in other periods, especially in sideways markets with low ATR, where even with good council voting there may not be enough movement to reach the TP.
Third: an SL/TP ratio of 400/300 seems illogical (the stop loss is wider than the take profit), but practice has shown that it works when the win rate is above 57%. At 64%, the expected value is positive: 0.64 × USD 124 − 0.36 × USD 107 ≈ +USD 41 per trade. This is precisely the value shown by the Expected Payoff metric: USD 41.88.
Confirmed: a council of fifteen analysts with reputation weights and a motivational context can generate signals with a positive expected value on real data. A high win rate with minimal losing streaks suggests that filtering through four independent risk managers is working: the system really does admit fewer bad entries than a single algorithm would.
Not confirmed: resilience to changes in the market regime. Eight weeks is too short a period to draw conclusions about the system's behavior in different regimes. There is no data on performance in a sideways market with a narrow ATR, during periods of heightened news-driven volatility, or on other currency pairs with different correlation characteristics.
The system's behavior under reduced aggressiveness has also not been tested: in the test, the voting threshold dynamically dropped to 4 when the fund was hungry. How often this occurred and how it affected signal quality is a separate issue that warrants analysis.
Backtest results are not proof that the system will work in the future. This is evidence that, over the selected period, the collective decision-making architecture demonstrated a measurable advantage over random selection. This is a sufficient basis for the next step: forward testing on a demo account with real API latency.
An Honest Conversation About Limitations
A fifteen-vote system is impressive. This impression needs to be deliberately tempered.
All fifteen participants are the same grok-3-fast model with different system prompts. Their independence refers to the independence of their perspectives, not the independence of the neural network's weights. If the base model is systematically wrong about a specific market pattern, all fifteen “analysts” will be wrong there at the same time. The consensus of fifteen copies of a single model is not the same as the consensus of fifteen independently trained models.
The second limitation is parallelism across eight currency pairs. When processing eight symbols simultaneously, the system makes up to 14 parallel API calls per pair — that is, up to 112 requests in a single analysis cycle. You need to understand which pricing plan can handle that kind of load.
Third: reputation memory is limited to the current process session. When the Python server is restarted, all reputation scores are reset to 50.0. The solution is persistent storage (SQLite) — that is the next step.
Fourth: the Expert Advisor does not know what happened after the previous signal. Every decision is made in a vacuum, based only on the last 100 price bars. The system does not see that a week ago, with a similar vote configuration, the Chairman turned out to be wrong. This is precisely the main argument in favor of the next step: equipping the system with long-term memory and a mechanism for evaluating the quality of its own decisions based on historical data.
Conclusion
We started with just one voice. Previous articles have shown that four voices with different roles produce a qualitatively different result. This article took the next step: fifteen voices with fundamentally different philosophies, operating in parallel across eight currency pairs with isolated reputation engines and a motivational system.
Technically, this is a direct extension of the previous version. The same Python architecture, the same WebSocket protocol, and the same technical indicators built entirely on NumPy. New features include multi-port support, eight isolated contexts, a reputation engine with feedback, and a motivational context that makes voting thresholds dynamic rather than fixed.
The AIHedgeFund_v4_clean.mq5 Expert Advisor has been intentionally stripped of any unnecessary mechanics: no averaging, no trailing, and no partial closing. Only the council signal, SL, TP, and exit on an opposite signal. This makes it possible to evaluate the quality of the AI decisions themselves, without the added complexity of additional logic.
The result is not just a BUY, SELL, or HOLD recommendation; it is the transcript of a professional investment committee meeting featuring ten specialists, four risk managers, and one Chairman. Every vote is identified by name, every argument is recorded in the log, and every risk-manager veto is backed up by specific numbers.
The complete code for llm_hedge_fund_v4.py and AIHedgeFund_v4_clean.mq5 is available in the appendix to the article.
| File name | Purpose |
|---|---|
| llm_hedge_fund_v4.py | Server for our AI hedge fund |
| AIHedgeFund_v4.mq5 | Expert Advisor (the fund’s executing trader) |
| winhttp.mqh | Socket communication library |
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/22164
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Features of Custom Indicators Creation
Chart Synchronization in MQL5 (Part 1): From a Simple Script to an Interactive Indicator
Features of Experts Advisors
Where your backtest stops being real: Measuring the depth of your broker's tick history
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
But what a grand title for the article!
So many encouraging claims in a completely empty strategy!
That’s an interesting question. In that case, why not use `ollama`, as you do in some of your other Python code? Wouldn’t that work well? Thank you for your attention.
That’s an interesting question. In that case, why not use `ollama`, as you do in some of your other Python code? Wouldn’t that work well? Thank you for your attention.
Thank you, a very interesting article. I’m studying this material myself, as well as this trading approach based on discussions involving AI agents and decision-making by senior traders....
Profitable trades are possible with this trading approach… and, again, you can incorporate the news… and trade on the news… Ollama + Cursor + AI agents and that’s it!!! )