Centaur_Quant_Architecture/Python/assistant_bridge.py

107 lines
4.5 KiB
Python

#!/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__))))
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()