#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ assistant_bridge.py — Periodic file-based LLM bridge (pengganti mcp_server.py) Alur (tanpa server HTTP — hemat token, EA baca hasil via JSON): 1. EA menulis MQL5\\Files\\ai_request.json {id, symbol, timeframe, setup_type, entry, sl, tp, historical_context, algorithmic_confidence_score, payload} 2. Bridge polling periodik (--interval detik): - hanya memproses REQUEST BARU (dedupe by id) -> 1 panggilan LLM per setup - MarketAnalyzer.analyze() -> LLM (OpenAI/Google), fallback Anti-Veto 40.0 3. Bridge menulis MQL5\\Files\\ai_response.json (atomic replace): {id, score, reason, used_fallback, provider, latency_ms, timestamp} 4. EA membaca response di timer berikutnya (FileRead), lalu eksekusi. Jalankan: python assistant_bridge.py [--files ] [--interval 5] [--once] """ import argparse import json import os import sys import time sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import logging from models.analyzer import MarketAnalyzer # noqa: E402 REQ = "ai_request.json" RES = "ai_response.json" def load_json(path): try: with open(path, "r", encoding="utf-8") as f: return json.load(f) except Exception: return None def main(): logging.basicConfig(level=logging.INFO, format="%(message)s") # tampilkan "provider ready" ap = argparse.ArgumentParser(description="Centaur file-based LLM bridge") ap.add_argument("--files", default=r"D:\TradingTerminal\MetaTrader 5\MQL5\Files", help="folder MQL5\\Files terminal (tempat request/response)") ap.add_argument("--interval", type=float, default=5.0, help="polling interval (detik)") ap.add_argument("--once", action="store_true", help="proses satu siklus lalu keluar (untuk uji)") ap.add_argument("--timeout", type=float, default=6.0, help="timeout LLM per request (detik; default 6; worst-case 6+2+6=14s < EA 15s)") ap.add_argument("--min-gap", type=float, default=2.0, help="jarak minimum antar panggilan LLM (detik; cegah rate limit)") args = ap.parse_args() ai = MarketAnalyzer(timeout=args.timeout) last_call = 0.0 last_id = None warned_malformed = False req_path = os.path.join(args.files, REQ) res_path = os.path.join(args.files, RES) print("[assistant_bridge] dir=%s interval=%.1fs (file-based, dedupe by id)" % (args.files, args.interval)) while True: req = load_json(req_path) if req is None and os.path.exists(req_path): if not warned_malformed: print("[assistant_bridge] WARNING: ai_request.json rusak/tak ter-parse — tunggu EA menulis ulang") warned_malformed = True else: warned_malformed = False if (isinstance(req, dict) and req.get("id") is not None and req["id"] != last_id and (last_call == 0.0 or time.time() - last_call >= args.min_gap)): # (last_id di-set dalam try di bawah) # (last_call di-set dalam try di bawah) # (t0 dihapus — latency_ms dari analyzer) result = ai.analyze(req) # req = envelope SDP lengkap dari EA result["id"] = req["id"] result["timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()) # P4: latency_ms diisi analyzer (satu sumber) tmp = res_path + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(result, f) try: os.replace(tmp, res_path) # atomic; P1: race dengan EA yang sedang membaca last_id = req["id"] # tandai sukses SETELAH replace OK last_call = time.time() except OSError as exc: # PermissionError dll — jangan mati, retry nanti print("[assistant_bridge] WARNING: replace gagal id=%s: %r — retry poll berikutnya" % (req.get("id"), exc)) try: os.remove(tmp) except OSError: pass time.sleep(0.5) continue # gagal — lewati print sukses, retry id sama di poll berikut print("[assistant_bridge] id=%s score=%.2f fallback=%s latency=%d ms" % (req["id"], result["score"], result.get("used_fallback"), result["latency_ms"])) if args.once: break time.sleep(args.interval) if __name__ == "__main__": main()