107 lines
4.5 KiB
Python
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()
|