2026-08-12 21:18:02 +07:00 | | | #!/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 <MQL5\\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__))))
|
2026-08-13 00:29:52 +07:00 | | | import logging
|
2026-08-12 21:18:02 +07:00 | | | 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():
|
2026-08-13 00:29:52 +07:00 | | | logging.basicConfig(level=logging.INFO, format="%(message)s") # tampilkan "provider ready"
|
2026-08-12 21:18:02 +07:00 | | | 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)")
|
2026-08-13 06:51:47 +07:00 | | | 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)")
|
2026-08-12 23:42:34 +07:00 | | | ap.add_argument("--min-gap", type=float, default=2.0,
|
| | | help="jarak minimum antar panggilan LLM (detik; cegah rate limit)")
|
2026-08-12 21:18:02 +07:00 | | | args = ap.parse_args()
|
| | |
|
2026-08-12 23:39:12 +07:00 | | | ai = MarketAnalyzer(timeout=args.timeout)
|
2026-08-12 23:42:34 +07:00 | | | last_call = 0.0
|
2026-08-12 21:18:02 +07:00 | | | last_id = None
|
2026-08-13 06:51:47 +07:00 | | | warned_malformed = False
|
2026-08-12 21:18:02 +07:00 | | | 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)
|
2026-08-13 06:51:47 +07:00 | | | 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
|
2026-08-12 23:42:34 +07:00 | | | 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)):
|
2026-08-13 06:51:47 +07:00 | | | # (last_id di-set dalam try di bawah)
|
| | | # (last_call di-set dalam try di bawah)
|
| | | # (t0 dihapus — latency_ms dari analyzer)
|
2026-08-12 21:18:02 +07:00 | | | 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())
|
2026-08-13 06:51:47 +07:00 | | | # P4: latency_ms diisi analyzer (satu sumber)
|
2026-08-12 21:18:02 +07:00 | | | tmp = res_path + ".tmp"
|
| | | with open(tmp, "w", encoding="utf-8") as f:
|
| | | json.dump(result, f)
|
2026-08-13 06:51:47 +07:00 | | | 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
|
2026-08-12 21:18:02 +07:00 | | | 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()
|