Files
2026-07-11 02:51:28 +08:00

404 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
GENESIS — Apollo Telegram Bot (Strategy C Command Handler)
Same pattern as ares_telegram_bot.py — manual trigger via Telegram.
Commands:
/apollo_analyze [SYMBOL] — MA crossover analysis, no trade
/apollo_execute — Execute pending signal
/apollo_scan — Scan all symbols, show best
/apollo_skip — Cancel pending signal
/apollo_status — Account + open Apollo position
/apollo_help — All commands
"""
import os, json, time, logging, threading, sys
from datetime import datetime, timezone
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "core"))
from mt5_bridge import bridge as _bridge
CONFIG_PATH = Path(__file__).parent / "apollo_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "apollo_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
import requests
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(os.getenv("TELEGRAM_CHAT_ID", CFG["telegram"]["chat_id"]))
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "apollo"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
STRATEGY = CFG["strategy"]["name"]
COMMENT = CFG["strategy"]["comment"]
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/apollo/apollo_bot.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "apollo"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "apollo_bot.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
_pending: dict = {}
_lock = threading.Lock()
def tg_send(text: str):
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "Markdown"},
timeout=10
)
except Exception as e:
log.error(f"tg_send: {e}")
def tg_updates(offset=0):
try:
r = requests.get(
f"https://api.telegram.org/bot{TG_TOKEN}/getUpdates",
params={"timeout": 30, "offset": offset}, timeout=40
)
return r.json().get("result", [])
except:
return []
def bridge(path, method="GET", data=None):
return _bridge(path, method, data)
def journal_write(entry: dict):
with open(JOURNAL, "a") as f:
f.write(json.dumps(entry) + "\n")
def journal_stats():
wins = losses = 0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t = json.loads(line)
if t.get("result") == "win": wins += 1
if t.get("result") == "loss": losses += 1
except: pass
return wins, losses
# ── Commands ───────────────────────────────────────────────────────────────────
def cmd_help():
fast = CFG["indicators"]["fast_ma_period"]
slow = CFG["indicators"]["slow_ma_period"]
tg_send(
f"🏹 *{STRATEGY} — Strategy C Commands*\n\n"
f"`/apollo_analyze [SYMBOL]` — MA crossover analysis (no trade)\n"
f"`/apollo_scan` — Scan all symbols for best signal\n"
f"`/apollo_execute` — Execute pending signal\n"
f"`/apollo_skip` — Cancel pending signal\n"
f"`/apollo_status` — Open position + journal stats\n"
f"`/apollo_help` — This message\n\n"
f"📊 Strategy: EMA{fast}/EMA{slow} Golden/Death Cross on M5\n"
f"🔖 Tag: `{COMMENT}` | Session: GMT "
f"{CFG['sessions']['allowed'][0]['start']}:00"
f"{CFG['sessions']['allowed'][0]['end']}:00"
)
def cmd_status():
acc = bridge("/balance")
if "error" in acc:
tg_send(f"🔴 *{STRATEGY}*: Bridge unreachable.")
return
positions = bridge("/positions")
pos_str = "None"
all_str = []
if isinstance(positions, list):
for p in positions:
comment = str(p.get("comment", ""))
all_str.append(f"`{p.get('symbol')}` {p.get('orderType')} "
f"{p.get('lots')}lot P&L:€{p.get('profit',0):.2f} [{comment}]")
if "APOLLO" in comment.upper():
pos_str = (f"{p.get('symbol')} {p.get('orderType')} "
f"{p.get('lots')}lot | P&L: €{p.get('profit',0):.2f}")
wins, losses = journal_stats()
with _lock:
pend = (f"🟡 {_pending.get('symbol')} {_pending.get('direction')} "
f"({_pending.get('signal_type','?')})"
if _pending else "None")
all_display = "\n".join(all_str) if all_str else "None"
tg_send(
f"🏹 *{STRATEGY} — Status*\n\n"
f"💰 Balance: €{acc.get('balance',0):.2f} | "
f"Equity: €{acc.get('equity',0):.2f}\n"
f"📈 Apollo Position: {pos_str}\n"
f"📋 Pending Signal: {pend}\n"
f"📒 Journal: {wins}W / {losses}L\n\n"
f"*All Open Positions:*\n{all_display}"
)
def cmd_skip():
with _lock:
if not _pending:
tg_send(f"🏹 *{STRATEGY}*: No pending signal to cancel.")
return
sym = _pending.get("symbol")
_pending.clear()
tg_send(f"⏭ *{STRATEGY}*: Signal for `{sym}` cancelled.")
def cmd_execute():
with _lock:
if not _pending:
tg_send(f"🏹 *{STRATEGY}*: No pending signal.\n"
f"Run `/apollo_analyze SYMBOL` or `/apollo_scan` first.")
return
signal = dict(_pending)
_pending.clear()
sym = signal.get("symbol")
dire = signal.get("direction")
sl = signal.get("stop_loss")
tp = signal.get("take_profit")
vol = signal.get("volume", 0.01)
if not all([sym, dire, sl, tp]):
tg_send(f"🏹 *{STRATEGY}*: Incomplete signal — cannot execute.")
return
positions = bridge("/positions")
if isinstance(positions, list) and any(
"APOLLO" in str(p.get("comment","")).upper() for p in positions
):
tg_send(f"⚠️ *{STRATEGY}*: Apollo position already open. Close it first.")
return
tg_send(f"🏹 *{STRATEGY}*: Placing order…")
order = bridge("/market", "POST", {
"symbol": sym, "volume": vol, "type": dire,
"stop_loss": sl, "take_profit": tp, "comment": COMMENT
})
ticket = order.get("ticket") or order.get("Ticket")
if ticket:
now = datetime.now(timezone.utc)
journal_write({
"ticket": str(ticket), "symbol": sym, "direction": dire,
"volume": vol, "sl": sl, "tp": tp,
"signal_type": signal.get("signal_type"),
"rr": signal.get("rr_ratio"),
"opened": now.isoformat(), "result": None, "pnl": None,
"strategy": "apollo-ma-crossover"
})
ind = signal.get("indicators", {})
tg_send(
f"✅ *{STRATEGY} TRADE PLACED*\n"
f"📈 `{sym}` {dire} | {signal.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{signal.get('entry')}` | SL: `{sl}` | TP: `{tp}`\n"
f"R:R: `{signal.get('rr_ratio')}` | Vol: `{vol}`\n"
f"ADX: {ind.get('adx','?')} | ATR: {ind.get('atr','?')}\n"
f"🔖 Ticket: `{ticket}`"
)
else:
err = order.get("message", str(order))
tg_send(f"❌ *{STRATEGY}*: Order FAILED — `{err}`")
def cmd_analyze(symbol: str):
sym = symbol.upper().strip()
if not sym.endswith("XX"):
sym = sym + "xx"
tg_send(f"🏹 *{STRATEGY}*: Analysing `{sym}`… (3060s)")
try:
import importlib.util
spec = importlib.util.spec_from_file_location(
"apollo_cycle", Path(__file__).parent / "apollo_cycle.py"
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
result = mod.run_analysis(sym)
except Exception as e:
log.error(f"Analysis error: {e}", exc_info=True)
tg_send(f"❌ *{STRATEGY}*: Analysis failed — `{str(e)[:200]}`")
return
_send_analysis_result(result)
def cmd_scan():
tg_send(f"🏹 *{STRATEGY}*: Scanning all symbols… (may take 6090s)")
try:
import importlib.util
spec = importlib.util.spec_from_file_location(
"apollo_cycle", Path(__file__).parent / "apollo_cycle.py"
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
symbols = CFG["symbols"]
best = None
best_rr = 0
scan_lines = []
for sym in symbols:
r = mod.run_analysis(sym)
action = r.get("action", "wait")
if action == "trade":
rr = r.get("rr_ratio", 0) or 0
icon = "🟢"
scan_lines.append(
f"{icon} `{sym}`: {r.get('direction')} "
f"{r.get('signal_type','').replace('_',' ')} | "
f"R:R {rr} | {r.get('confidence','?')}"
)
if rr > best_rr:
best_rr = rr
best = r
else:
scan_lines.append(f"⚪ `{sym}`: {r.get('reason','wait')[:60]}")
import time; time.sleep(0.5)
summary = "\n".join(scan_lines)
tg_send(f"🏹 *{STRATEGY} SCAN RESULTS*\n\n{summary}")
if best:
with _lock:
_pending.clear()
_pending.update(best)
_send_analysis_result(best, from_scan=True)
else:
tg_send(f"📊 *{STRATEGY}*: No trade signals found across all symbols.")
except Exception as e:
log.error(f"Scan error: {e}", exc_info=True)
tg_send(f"❌ *{STRATEGY}*: Scan failed — `{str(e)[:200]}`")
def _send_analysis_result(result: dict, from_scan: bool = False):
"""Format and send analysis result to Telegram, set pending if trade signal."""
action = result.get("action", "wait")
if action != "trade":
tg_send(
f"🏹 *{STRATEGY}* — `{result.get('symbol','?')}`\n\n"
f"📊 Signal: *WAIT*\n"
f"💡 {result.get('reason','')[:300]}"
)
return
with _lock:
_pending.clear()
_pending.update(result)
conds = result.get("conditions_met", [])
cond_str = "\n".join(f" ✅ {c}" for c in conds) if conds else " (see reason)"
warn_str = ""
if result.get("warnings"):
warn_str = "\n" + "\n".join(f" ⚠️ {w}" for w in result["warnings"])
ind = result.get("indicators", {})
fast = CFG["indicators"]["fast_ma_period"]
slow = CFG["indicators"]["slow_ma_period"]
scan_note = " _(Best from scan)_" if from_scan else ""
tg_send(
f"🏹 *{STRATEGY} ANALYSIS*{scan_note} — `{result.get('symbol')}`\n\n"
f"📊 Signal: *{result.get('direction')}* — "
f"{result.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{result.get('entry')}`\n"
f"SL: `{result.get('stop_loss')}` ({result.get('sl_pips','?')} pips)\n"
f"TP: `{result.get('take_profit')}` ({result.get('tp_pips','?')} pips)\n"
f"R:R `{result.get('rr_ratio')}` | Vol: `{result.get('volume')}`\n"
f"🎯 Confidence: {result.get('confidence','?')}\n"
f"ADX: {ind.get('adx','?')} | ATR: {ind.get('atr','?')}\n"
f"EMA{fast}: {ind.get('fast_ma','?')} | EMA{slow}: {ind.get('slow_ma','?')}\n\n"
f"📌 *Conditions ({len(conds)} met):*\n{cond_str}{warn_str}\n\n"
f"💡 {result.get('reason','')[:250]}\n\n"
f"Reply `/apollo_execute` to trade or `/apollo_skip` to cancel."
)
# ── Dispatcher ─────────────────────────────────────────────────────────────────
def dispatch(text: str, from_id: str):
if str(from_id) != TG_CHAT_ID:
log.warning(f"Unauthorised: {from_id}")
return
text = text.strip()
lower = text.lower()
if lower.startswith("/apollo_analyze"):
parts = text.split(maxsplit=1)
sym = parts[1] if len(parts) > 1 else ""
if not sym:
tg_send("Usage: `/apollo_analyze EURUSD`")
else:
threading.Thread(target=cmd_analyze, args=(sym,), daemon=True).start()
elif lower == "/apollo_scan":
threading.Thread(target=cmd_scan, daemon=True).start()
elif lower == "/apollo_execute":
threading.Thread(target=cmd_execute, daemon=True).start()
elif lower == "/apollo_skip":
cmd_skip()
elif lower == "/apollo_status":
threading.Thread(target=cmd_status, daemon=True).start()
elif lower in ("/apollo_help", "/apollo"):
cmd_help()
# ── Main loop ──────────────────────────────────────────────────────────────────
def main():
log.info(f"=== {STRATEGY} Telegram Bot started ===")
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/setMyCommands",
json={"commands": [
{"command": "apollo_help", "description": "Show all commands"},
{"command": "apollo_analyze", "description": "MA crossover analysis on symbol"},
{"command": "apollo_scan", "description": "Scan all symbols for best signal"},
{"command": "apollo_execute", "description": "Execute pending signal"},
{"command": "apollo_skip", "description": "Cancel pending signal"},
{"command": "apollo_status", "description": "Position + journal stats"},
]},
timeout=10
)
except Exception as e:
log.warning(f"setMyCommands failed: {e}")
fast = CFG["indicators"]["fast_ma_period"]
slow = CFG["indicators"]["slow_ma_period"]
tg_send(
f"🏹 *{STRATEGY} Bot Online*\n"
f"Strategy C: EMA{fast}/EMA{slow} Trend Following (M5)\n"
f"Send `/apollo_help` to see commands.\n\n"
f"🤖 _Hermes controls this bot autonomously._\n"
f"_You can also trigger manually via the commands above._"
)
offset = 0
while True:
try:
updates = tg_updates(offset)
for upd in updates:
offset = upd["update_id"] + 1
msg = upd.get("message", {})
text = msg.get("text", "")
chat_id = str(msg.get("chat", {}).get("id", ""))
if text.startswith("/apollo"):
dispatch(text, chat_id)
except Exception as e:
log.error(f"Polling error: {e}")
time.sleep(5)
time.sleep(1)
if __name__ == "__main__":
main()