194 lines
6.5 KiB
Python
194 lines
6.5 KiB
Python
|
|
#!/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()
|