mt5-quant 1.32.2

MCP server for MT5 strategy development on macOS/Linux
#!/usr/bin/env python3
"""MCP test harness: run backtest then exercise all granular analysis tools."""

import json
import subprocess
import sys
import time
import threading

BINARY = "/Users/masdevid/.cargo/bin/mt5-quant"
TIMEOUT = 1200  # 20 min for backtest

def send(proc, msg):
    line = json.dumps(msg) + "\n"
    proc.stdin.write(line.encode())
    proc.stdin.flush()

def recv(proc, timeout=30):
    """Read one JSON-RPC response line."""
    proc.stdout.readline()  # skip blank / notification lines
    deadline = time.time() + timeout
    while time.time() < deadline:
        line = proc.stdout.readline().decode("utf-8", errors="replace").strip()
        if not line:
            continue
        try:
            return json.loads(line)
        except json.JSONDecodeError:
            pass  # skip non-JSON lines (logs etc)
    return None

def recv_with_id(proc, expected_id, timeout=1200):
    """Read lines until we get a response with the expected id."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        line = proc.stdout.readline().decode("utf-8", errors="replace").strip()
        if not line:
            continue
        try:
            msg = json.loads(line)
            if msg.get("id") == expected_id:
                return msg
        except json.JSONDecodeError:
            pass
    return None

def tool_call(proc, call_id, name, arguments=None):
    send(proc, {
        "jsonrpc": "2.0",
        "id": call_id,
        "method": "tools/call",
        "params": {"name": name, "arguments": arguments or {}}
    })

def extract_text(resp):
    if not resp:
        return "NO RESPONSE"
    result = resp.get("result", {})
    content = result.get("content", [])
    if content:
        text = content[0].get("text", "")
        try:
            parsed = json.loads(text)
            return json.dumps(parsed, indent=2)[:1500]
        except Exception:
            return text[:1500]
    if "error" in resp:
        return f"ERROR: {resp['error']}"
    return str(resp)[:500]

def ok(resp):
    if not resp:
        return False
    result = resp.get("result", {})
    content = result.get("content", [])
    is_error = result.get("isError", True)
    if is_error:
        return False
    if content:
        text = content[0].get("text", "")
        try:
            parsed = json.loads(text)
            return parsed.get("success", True) is not False
        except Exception:
            return True
    return not is_error

# ── Start server ──────────────────────────────────────────────────────────────

print("Starting mt5-quant MCP server...")
proc = subprocess.Popen(
    [BINARY],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.DEVNULL,
)

# ── Handshake ─────────────────────────────────────────────────────────────────

send(proc, {
    "jsonrpc": "2.0", "id": 0,
    "method": "initialize",
    "params": {
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": {"name": "test-harness", "version": "1.0"}
    }
})
init_resp = recv_with_id(proc, 0, timeout=10)
if not init_resp:
    print("FATAL: no initialize response")
    proc.terminate()
    sys.exit(1)
print("Initialized OK")

send(proc, {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}})

# ── Run backtest ──────────────────────────────────────────────────────────────

print("\n=== run_backtest (DPS21, XAUUSD, M5, OHLC, skip_compile) ===")
tool_call(proc, 1, "run_backtest", {
    "expert": "DPS21",
    "symbol": "XAUUSDc",
    "timeframe": "M5",
    "model": 1,            # OHLC – faster
    "skip_compile": True,
    "timeout": 900,
    "startup_delay_secs": 15,
})
resp = recv_with_id(proc, 1, timeout=TIMEOUT)
print(extract_text(resp))
backtest_ok = ok(resp)
print(f"backtest_ok={backtest_ok}")

if not backtest_ok:
    print("Backtest failed – aborting analysis tests")
    proc.terminate()
    sys.exit(1)

# ── Full analysis ─────────────────────────────────────────────────────────────

print("\n=== analyze_report (latest, all analytics) ===")
tool_call(proc, 2, "analyze_report", {})
resp = recv_with_id(proc, 2, timeout=60)
print(extract_text(resp))
print(f"analyze_report ok={ok(resp)}")

# ── Granular analytics ────────────────────────────────────────────────────────

granular = [
    ("analyze_monthly_pnl",           {}),
    ("analyze_drawdown_events",       {}),
    ("analyze_top_losses",            {"limit": 5}),
    ("analyze_loss_sequences",        {}),
    ("analyze_position_pairs",        {}),
    ("analyze_direction_bias",        {}),
    ("analyze_streaks",               {}),
    ("analyze_concurrent_peak",       {}),
    ("analyze_profit_distribution",   {}),
    ("analyze_time_performance",      {}),
    ("analyze_hold_time_distribution",{}),
    ("analyze_layer_performance",     {}),
    ("analyze_volume_vs_profit",      {}),
    ("analyze_costs",                 {}),
    ("analyze_efficiency",            {}),
    ("list_deals",                    {"limit": 5}),
    ("search_deals_by_comment",       {"query": "tp"}),
    ("search_deals_by_magic",         {"magic": "0"}),
]

results = {}
for i, (tool, args) in enumerate(granular, start=10):
    print(f"\n=== {tool} ===")
    tool_call(proc, i, tool, args)
    resp = recv_with_id(proc, i, timeout=30)
    status = "OK" if ok(resp) else "FAIL"
    results[tool] = status
    print(f"  {status}")
    if status == "FAIL":
        print("  " + extract_text(resp)[:400])

# ── Summary ───────────────────────────────────────────────────────────────────

print("\n\n========== SUMMARY ==========")
passed = [t for t, s in results.items() if s == "OK"]
failed = [t for t, s in results.items() if s != "OK"]
print(f"PASSED ({len(passed)}): {', '.join(passed)}")
if failed:
    print(f"FAILED ({len(failed)}): {', '.join(failed)}")
else:
    print("All granular analytics PASSED")

proc.terminate()