#!/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}`… (30–60s)") 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 60–90s)") 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()