polymarket-bot-testing / hermes_agent.py
frank0957's picture
Update hermes_agent.py
246c0d5 verified
Raw
History Blame Contribute Delete
28.1 kB
# hermes_agent.py
# Decision engine with hybrid model routing (free models only),
# consensus analysis, health monitoring, performance‑based threshold adjustment,
# and daily self‑reflection.
# L3 uses a dual‑collaborator + arbiter pattern.
# L2 fallback model updated; safety wrapper added for malformed responses.
# All comments in English.
import os, sys, time, json, hashlib, traceback, glob, re, requests
from datetime import datetime, timezone, timedelta
from llm_wiki import save_entry, query_context, semantic_search
from openai import OpenAI
# ── File paths ──────────────────────────────────────────────
SIGNAL_FILE = "/app/latest_signals.json"
STATUS_FILE = "/app/hermes_status.txt"
ERROR_COUNT_FILE = "/app/llm_error_count.txt"
LAST_PROCESSED_FILE = "/app/last_processed_signal.json"
TASK_QUEUE_FILE = "/app/task_queue.json"
FOLLOW_THRESHOLD_FILE = "/app/follow_threshold.txt"
HALT_FILE = "/app/halt_trading.txt"
UNHALT_FILE = "/app/unhalt_trading.txt"
PENDING_ALERTS_FILE = "/app/pending_alerts.json"
HEALTH_LAST_ALERT_FILE = "/app/last_health_alert_level.txt"
# ── OpenRouter configuration ────────────────────────────────
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
if not OPENROUTER_API_KEY:
raise EnvironmentError("OPENROUTER_API_KEY must be set in Secrets.")
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=OPENROUTER_API_KEY
)
# ── Hybrid model routing (free models only) ─────────────────
FAST_SCAN_PRIMARY = "google/gemma-4-26b-a4b:free"
FAST_SCAN_FALLBACK = "google/gemma-4-31b-it-20260402:free"
L2_PRIMARY = "nvidia/nemotron-3-super-120b-a12b:free"
L2_FALLBACK = "mistralai/mistral-nemo:free" # replaced dead model
# ── L3 dual‑collaborator + arbiter (free tier) ─────────────
L3_COLLABORATOR_A = "google/gemini-2.5-flash-lite:free" # 1M context, reliable
L3_COLLABORATOR_B = "openai/gpt-4o-mini:free" # strong reasoning
L3_ARBITER = "meta-llama/llama-4-maverick:free" # third opinion
FAST_SCAN_ROUTING = [FAST_SCAN_PRIMARY, FAST_SCAN_FALLBACK]
REALTIME_ROUTING = [L2_PRIMARY, L2_FALLBACK]
DEEP_REVIEW_COLLABORATORS = [L3_COLLABORATOR_A, L3_COLLABORATOR_B]
DEEP_REVIEW_ARBITER = L3_ARBITER
# ── Helper functions ─────────────────────────────────────────
def read_file(path, default=""):
try:
if os.path.exists(path):
with open(path, "r") as f:
return f.read().strip()
except:
pass
return default
def write_file(path, content):
try:
with open(path, "w") as f:
f.write(str(content))
except:
pass
def write_status(msg):
try:
with open(STATUS_FILE, "w") as f:
f.write(msg + "\n")
except:
pass
def read_error_count():
try:
if os.path.exists(ERROR_COUNT_FILE):
with open(ERROR_COUNT_FILE, "r") as f:
return int(f.read().strip())
except:
pass
return 0
def increment_error_count(error_type="unknown", detail=""):
count = read_error_count() + 1
try:
with open(ERROR_COUNT_FILE, "w") as f:
f.write(str(count))
except:
pass
try:
with open("/app/llm_error_types.txt", "a") as f:
f.write(f"[{datetime.now(timezone.utc).isoformat()}] {error_type}\n")
except:
pass
if detail:
try:
with open("/app/llm_error_details.txt", "a") as f:
f.write(f"[{datetime.now(timezone.utc).isoformat()}] {error_type}: {detail[:100]}\n")
except:
pass
def reset_error_count():
try:
with open(ERROR_COUNT_FILE, "w") as f:
f.write("0")
with open("/app/llm_error_types.txt", "w") as f:
f.write("")
with open("/app/llm_error_details.txt", "w") as f:
f.write("")
except:
pass
def read_signals():
try:
if os.path.exists(SIGNAL_FILE):
with open(SIGNAL_FILE, "r") as f:
return json.load(f)
except:
pass
return []
def clear_signals():
try:
with open(SIGNAL_FILE, "w") as f:
json.dump([], f)
except:
pass
def get_follow_threshold():
try:
if os.path.exists(FOLLOW_THRESHOLD_FILE):
with open(FOLLOW_THRESHOLD_FILE, "r") as f:
return float(f.read().strip())
except:
pass
return 0.6
def is_halted():
try:
if os.path.exists(HALT_FILE):
return True
if os.path.exists("/app/capital_state.json"):
with open("/app/capital_state.json", "r") as f:
state = json.load(f)
if state.get("halted", False):
return True
except:
pass
return False
# ── Telegram alert helpers ───────────────────────────────────
def send_telegram_alert(text: str):
token = os.getenv("TELEGRAM_BOT_TOKEN")
chat_id = os.getenv("TELEGRAM_CHAT_ID")
if not token or not chat_id:
return False
url = f"https://api.telegram.org/bot{token}/sendMessage"
try:
resp = requests.post(url, json={"chat_id": chat_id, "text": text}, timeout=10)
return resp.status_code == 200
except:
return False
def add_pending_alert(alert_text):
alerts = []
try:
if os.path.exists(PENDING_ALERTS_FILE):
with open(PENDING_ALERTS_FILE, "r") as f:
alerts = json.load(f)
except:
pass
alerts.append({"text": alert_text, "timestamp": time.time()})
try:
with open(PENDING_ALERTS_FILE, "w") as f:
json.dump(alerts, f)
except:
pass
def flush_pending_alerts():
if not os.path.exists(PENDING_ALERTS_FILE):
return
try:
with open(PENDING_ALERTS_FILE, "r") as f:
alerts = json.load(f)
except:
return
remaining = []
for alert in alerts:
if time.time() - alert["timestamp"] > 86400:
continue
if not send_telegram_alert(alert["text"]):
remaining.append(alert)
try:
if remaining:
with open(PENDING_ALERTS_FILE, "w") as f:
json.dump(remaining, f)
else:
if os.path.exists(PENDING_ALERTS_FILE):
os.remove(PENDING_ALERTS_FILE)
except:
pass
# ── Market sentiment overlay ────────────────────────────────
def get_market_sentiment():
try:
resp = requests.get("https://data-api.polymarket.com/markets?limit=50", timeout=10)
if resp.status_code == 200:
data = resp.json()
if isinstance(data, list) and data:
total = len(data)
bullish = sum(1 for m in data if float(m.get("outcomePrices", [0, 0])[0]) > 0.7)
sentiment = (bullish / total - 0.5) * 2
return round(sentiment, 2)
except:
pass
return 0.0
# ── LLM call with sequential model routing ──────────────────
def call_llm_with_routing(prompt: str, routing: list, task_label: str = "", max_tokens: int = 200) -> str:
for model in routing:
try:
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=max_tokens
)
# Safety wrapper: catch malformed or None responses
if response and response.choices and len(response.choices) > 0:
result = response.choices[0].message.content
if result:
result = result.strip()
print(f"[Hermes] {task_label} – Model {model} succeeded.", flush=True)
return result
# Empty or malformed response
print(f"[Hermes] {task_label} – Model {model} returned empty/None, trying next.", flush=True)
except Exception as e:
error_msg = str(e)
if "429" in error_msg:
increment_error_count("rate_limit", error_msg)
elif "502" in error_msg:
increment_error_count("model_down", error_msg)
elif "401" in error_msg:
increment_error_count("auth_error", error_msg)
elif "timeout" in error_msg.lower():
increment_error_count("network", error_msg)
else:
increment_error_count("other", error_msg)
print(f"[Hermes] {task_label} – Model {model} failed: {e}", flush=True)
continue
print(f"[Hermes] {task_label} – All models failed.", flush=True)
return ""
# ── Health score monitoring & Telegram alerts ───────────────
def compute_health_score():
polyhub_status = read_file("/app/polyhub_status.txt", "unknown")
hermes_status = read_file("/app/hermes_status.txt", "unknown")
balance_status = read_file("/app/balance_status.txt", "unknown")
backup_status = read_file("/app/backup_status.txt", "disabled")
halted = False
try:
if os.path.exists("/app/capital_state.json"):
with open("/app/capital_state.json") as f:
state = json.load(f)
halted = state.get("halted", False)
except:
pass
score = 100
if "running" not in polyhub_status: score -= 30
if "running" not in hermes_status: score -= 30
if "running" not in balance_status: score -= 20
if halted: score -= 20
if backup_status == "failed": score -= 10
return score, {
"polyhub": polyhub_status, "hermes": hermes_status,
"balance": balance_status, "backup": backup_status, "halted": halted
}
def health_monitor():
score, details = compute_health_score()
last_level = read_file(HEALTH_LAST_ALERT_FILE, "")
if score >= 70: current_level = "normal"
elif score >= 40: current_level = "warning"
else: current_level = "critical"
if current_level != last_level:
if current_level == "warning":
msg = f"⚠️ Health Score dropped to {score}/100\n\n{json.dumps(details, indent=2)}"
elif current_level == "critical":
msg = f"🚨 CRITICAL Health Score: {score}/100\n\n{json.dumps(details, indent=2)}"
else:
msg = f"βœ… Health Score restored to {score}/100"
if send_telegram_alert(msg):
write_file(HEALTH_LAST_ALERT_FILE, current_level)
else:
add_pending_alert(msg)
write_file(HEALTH_LAST_ALERT_FILE, current_level)
return score, details
# ── Consensus analysis for high‑confidence signals ─────────
def consensus_analyze(signal, context):
prompt = f"""You are a Polymarket trading assistant. A signal was detected:
- Market: {signal.get('market_slug')}
- Action: {signal.get('action')}
- Confidence score: {signal.get('confidence')}
- Amount: ${signal.get('amount_usd')}
Relevant past knowledge: {context}
Market sentiment: {get_market_sentiment()}
Current follow threshold: {get_follow_threshold()}
Should we follow this signal? Reply with "FOLLOW" or "IGNORE" and briefly explain."""
return _dual_collaborator_decision(prompt, "Consensus")
def _dual_collaborator_decision(prompt: str, task_label: str) -> str:
"""Ask both L3 collaborators; if they disagree, ask the arbiter."""
decision_a = call_llm_with_routing(prompt, [L3_COLLABORATOR_A], f"{task_label}-A", max_tokens=150)
decision_b = call_llm_with_routing(prompt, [L3_COLLABORATOR_B], f"{task_label}-B", max_tokens=150)
if not decision_a or not decision_b:
fallback = call_llm_with_routing(prompt, [L3_ARBITER], f"{task_label}-Arbiter", max_tokens=150)
return fallback if fallback else "IGNORE (model error)"
a_follow = "FOLLOW" in decision_a.upper()
b_follow = "FOLLOW" in decision_b.upper()
if a_follow == b_follow:
return decision_a if a_follow else decision_b
arbiter_prompt = f"""Two models disagreed on a trading signal.
Model A says: {decision_a}
Model B says: {decision_b}
Original prompt: {prompt}
As the arbiter, which decision (FOLLOW or IGNORE) is more appropriate? Reply with "FOLLOW" or "IGNORE" and briefly explain."""
arbiter_decision = call_llm_with_routing(arbiter_prompt, [L3_ARBITER], f"{task_label}-Arbiter", max_tokens=150)
if not arbiter_decision:
return "IGNORE (arbiter failed)"
return arbiter_decision
# ── Signal analysis ─────────────────────────────────────────
def analyze_signal(signal: dict, context: str) -> str:
confidence = signal.get("confidence", 0)
threshold = get_follow_threshold()
high_confidence = False
if confidence >= 0.8:
try:
if os.path.exists("/app/cluster_results.json"):
with open("/app/cluster_results.json", "r") as f:
cluster_data = json.load(f)
followed = cluster_data.get("followed_wallets", [])
if len(followed) >= 2:
high_confidence = True
except:
pass
if high_confidence:
return consensus_analyze(signal, context)
prompt = f"""You are a Polymarket trading assistant. A signal was detected:
- Wallet: {signal.get('wallet_address')}
- Market: {signal.get('market_slug')}
- Action: {signal.get('action')}
- Confidence score: {confidence}
- Amount: ${signal.get('amount_usd')}
- Analysis mode: standard
- Market sentiment: {get_market_sentiment()}
- Current follow threshold: {threshold}
Relevant past knowledge from our wiki:
{context if context else 'No historical data found.'}
Given the above, should we follow this signal? Reply with "FOLLOW" or "IGNORE" and briefly explain.
"""
decision = call_llm_with_routing(prompt, REALTIME_ROUTING, f"Signal-{signal.get('market_slug', 'unknown')[:30]}")
if not decision:
return "IGNORE (LLM error)"
if "FOLLOW" not in decision and "IGNORE" not in decision:
return f"IGNORE (ambiguous: {decision[:80]})"
return decision
# ── Task queue processing ───────────────────────────────────
def process_task_queue():
tasks = []
try:
if os.path.exists(TASK_QUEUE_FILE):
with open(TASK_QUEUE_FILE, "r") as f:
tasks = json.load(f)
except:
return
if not tasks:
return
for task in tasks[:5]:
cmd = task.get("command", "")
print(f"[Hermes] Processing task: {cmd}", flush=True)
if cmd.startswith("analyze"):
market = cmd.split(" ", 1)[1] if " " in cmd else "btc-updown-5m-1779771300"
prompt = f"Analyze the following Polymarket market briefly (max 150 words): {market}. Include current pricing, liquidity, and any relevant historical patterns from the wiki."
result = call_llm_with_routing(prompt, DEEP_REVIEW_COLLABORATORS, "Task-Analyze", max_tokens=250)
if result:
save_entry(topic=f"Task-Analysis-{market[:30]}", content=result, tags=["task", "analysis"])
elif cmd.startswith("report"):
with open("/app/force_report.txt", "w") as f:
f.write("1")
elif cmd.startswith("threshold"):
try:
val = float(cmd.split(" ")[1])
with open(FOLLOW_THRESHOLD_FILE, "w") as f:
f.write(str(val))
print(f"[Hermes] Threshold updated to {val}", flush=True)
except:
pass
task["status"] = "done"
task["completed_at"] = datetime.now(timezone.utc).isoformat()
tasks = [t for t in tasks if t.get("status") != "done"]
try:
with open(TASK_QUEUE_FILE, "w") as f:
json.dump(tasks, f)
except:
pass
# ── Auto threshold adjustment (weekly) ──────────────────────
def auto_adjust_threshold():
wiki_dir = "/app/wiki"
signals = []
try:
for fp in sorted(glob.glob(os.path.join(wiki_dir, "*.md")), reverse=True)[:200]:
with open(fp, "r") as f:
content = f.read()
if "FOLLOW" in content or "IGNORE" in content:
signals.append(content)
except:
return
if len(signals) < 20:
return
prompt = f"""You are analyzing historical trading signals. The last {len(signals)} signal analyses are summarized below (excerpts):
{chr(10).join(signals[:10])}
Based on these patterns, what confidence threshold (between 0.5 and 0.9) would have maximized profit while minimizing losing trades?
Reply with just a number between 0.5 and 0.9 and a 1‑sentence explanation.
"""
result = call_llm_with_routing(prompt, DEEP_REVIEW_COLLABORATORS, "AutoThreshold", max_tokens=100)
if result:
try:
nums = re.findall(r"0\.\d+", result)
if nums:
new_threshold = float(nums[0])
new_threshold = max(0.5, min(0.9, new_threshold))
with open(FOLLOW_THRESHOLD_FILE, "w") as f:
f.write(str(new_threshold))
print(f"[Hermes] Auto‑adjusted threshold to {new_threshold}", flush=True)
save_entry(topic="AutoThreshold-Adjustment", content=f"New threshold: {new_threshold}\nReason: {result}", tags=["auto", "threshold"])
except:
pass
# ── Performance‑based threshold adjustment (daily) ──────────
def auto_adjust_threshold_from_performance():
try:
if not os.path.exists("/app/virtual_positions.json"):
return
with open("/app/virtual_positions.json", "r") as f:
positions = json.load(f)
closed = [p for p in positions if p.get("status") == "closed"]
if len(closed) < 5:
return
recent = sorted(closed, key=lambda p: p.get("exit_time", ""), reverse=True)[:10]
wins = [p for p in recent if p.get("result") == "win"]
win_rate = len(wins) / len(recent)
threshold = get_follow_threshold()
new_threshold = threshold
if win_rate >= 0.6 and threshold > 0.5:
new_threshold = round(threshold - 0.02, 2)
new_threshold = max(0.5, new_threshold)
elif win_rate <= 0.4 and threshold < 0.8:
new_threshold = round(threshold + 0.02, 2)
new_threshold = min(0.8, new_threshold)
if new_threshold != threshold:
with open(FOLLOW_THRESHOLD_FILE, "w") as f:
f.write(str(new_threshold))
print(f"[Hermes] Performance‑based threshold: {threshold} β†’ {new_threshold} (win rate {win_rate:.0%})", flush=True)
save_entry(
topic="AutoThreshold-Performance",
content=f"Threshold changed from {threshold} to {new_threshold} (last 10 win rate {win_rate:.0%})",
tags=["auto", "threshold", "performance"]
)
except Exception as e:
print(f"[Hermes] Failed performance threshold adjustment: {e}", flush=True)
# ── Daily Reflection ────────────────────────────────────────
def daily_reflection():
print("[Hermes] Starting daily reflection...", flush=True)
try:
trades = []
if os.path.exists("/app/virtual_positions.json"):
with open("/app/virtual_positions.json", "r") as f:
positions = json.load(f)
cutoff = datetime.now(timezone.utc) - timedelta(days=7)
for p in positions:
if p.get("status") == "closed":
try:
exit_time = datetime.fromisoformat(p.get("exit_time", ""))
if exit_time > cutoff:
trades.append({
"market": p.get("market",""),
"action": p.get("action",""),
"result": p.get("result",""),
"pnl": p.get("pnl",0)
})
except:
pass
flags_closed = []
if os.path.exists("/app/flags.json"):
with open("/app/flags.json", "r") as f:
flags = json.load(f)
for fl in flags:
if fl.get("status") == "closed":
try:
closed_at = datetime.fromisoformat(fl.get("closed_at", ""))
if closed_at > cutoff:
flags_closed.append({
"description": fl.get("description",""),
"result": fl.get("result","")
})
except:
pass
notes = []
try:
with open("/app/notes.txt", "r") as f:
lines = f.readlines()
recent_lines = [l.strip() for l in lines if l.strip()]
notes = recent_lines[-10:]
except:
pass
prompt = f"""You are a self‑reflection module for an automated trading system.
Below is a summary of the last 7 days:
**Closed Virtual Trades:** {json.dumps(trades, indent=2)}
**Closed Flags:** {json.dumps(flags_closed, indent=2)}
**Recent Macro Notes:** {json.dumps(notes, indent=2)}
Please analyze:
1. Which types of signals performed well? Which performed poorly?
2. Are there patterns in the flags accuracy vs. market type?
3. How well do the macro notes align with actual outcomes?
4. What concrete adjustments should the trading agent make (e.g., focus on certain sports/markets, adjust confidence threshold)?
Reply in concise bullet points. Maximum 200 words.
"""
reflection = call_llm_with_routing(prompt, DEEP_REVIEW_COLLABORATORS, "DailyReflection", max_tokens=300)
if reflection:
save_entry(topic="Daily-Reflection", content=reflection, tags=["reflection", "daily"])
with open("/app/latest_reflection.txt", "w") as f:
f.write(reflection)
msg = f"🧠 Daily Reflection\n\n{reflection[:1500]}"
send_telegram_alert(msg)
print("[Hermes] Daily reflection completed.", flush=True)
else:
print("[Hermes] Daily reflection failed (no LLM response).", flush=True)
except Exception as e:
print(f"[Hermes] Daily reflection error: {e}", flush=True)
# ── Signal processing ───────────────────────────────────────
decision_cache = {}
def process_signals():
signals = read_signals()
print(f"[Hermes] Woke up. Signals in queue: {len(signals)}", flush=True)
if not signals:
return
if is_halted():
print("[Hermes] Trading halted – skipping signal processing.", flush=True)
clear_signals()
return
reflection_text = read_file("/app/latest_reflection.txt", "")
recent_notes_text = ""
try:
with open("/app/notes.txt", "r") as f:
lines = [line.strip() for line in f.readlines() if line.strip()]
if lines:
recent_notes_text = "Recent macro notes:\n" + "\n".join(lines[-5:])
except:
pass
for signal in signals:
keywords = signal["market_slug"].split("-")
try:
context = semantic_search(signal.get("market_slug", ""), top_k=3)
except:
context = query_context(keywords)
enriched_context = context
if reflection_text:
enriched_context = f"[Latest Reflection]\n{reflection_text}\n\n{enriched_context}"
if recent_notes_text:
enriched_context = f"{recent_notes_text}\n\n{enriched_context}"
sig_str = json.dumps(signal, sort_keys=True)
sig_hash = hashlib.md5(sig_str.encode()).hexdigest()
now = time.time()
if sig_hash in decision_cache and (now - decision_cache[sig_hash][1]) < 3600:
print(f"[Hermes] Using cached decision for {signal.get('market_slug')}", flush=True)
decision = decision_cache[sig_hash][0]
else:
print(f"[Hermes] Processing signal for {signal.get('market_slug')}...", flush=True)
decision = analyze_signal(signal, enriched_context)
decision_cache[sig_hash] = (decision, now)
print(f"[Hermes] Decision for {signal['market_slug']}: {decision}", flush=True)
summary = f"Signal: {json.dumps(signal)}\nDecision: {decision}"
save_entry(topic=f"Signal-{signal['market_slug']}", content=summary, tags=keywords)
if signals:
try:
signal_with_time = signals[-1].copy()
signal_with_time["processed_at"] = datetime.now(timezone.utc).isoformat()
signal_with_time["decision"] = decision_cache.get(hashlib.md5(json.dumps(signals[-1], sort_keys=True).encode()).hexdigest(), ("", 0))[0]
with open(LAST_PROCESSED_FILE, "w") as f:
json.dump(signal_with_time, f)
except:
pass
clear_signals()
# ── Cron jobs & health check ────────────────────────────────
def check_cron_jobs():
now = datetime.now(timezone.utc)
if now.hour in (0, 6, 12, 18) and now.minute == 0:
print(f"[Hermes] Cron: Auto market analysis at {now.hour}:00 UTC", flush=True)
prompt = "Analyze the 3 most active Polymarket markets right now. For each, provide current probability, liquidity, and a brief assessment."
result = call_llm_with_routing(prompt, DEEP_REVIEW_COLLABORATORS, "Cron-Analyze", max_tokens=300)
if result:
save_entry(topic=f"AutoAnalysis-{now.strftime('%Y-%m-%d-%H')}", content=result, tags=["auto", "analysis"])
if now.weekday() == 0 and now.hour == 0 and now.minute == 0:
print("[Hermes] Cron: Weekly full review", flush=True)
auto_adjust_threshold()
if now.hour == 0 and now.minute == 0 and now.second < 5:
auto_adjust_threshold_from_performance()
if now.hour == 0 and now.minute == 5 and now.second < 5:
daily_reflection()
if now.minute % 5 == 0 and now.second < 5:
health_monitor()
# ── Main loop ───────────────────────────────────────────────
def agent_loop():
try:
print("[Hermes] Multi-tier hybrid agent loop starting...", flush=True)
write_status("running")
last_reset_day = None
last_cron_check = 0
while True:
now_utc = datetime.now(timezone.utc)
if now_utc.hour == 0 and now_utc.minute == 0 and now_utc.day != last_reset_day:
reset_error_count()
last_reset_day = now_utc.day
print("[Hermes] Daily LLM error counter reset.", flush=True)
current_time = time.time()
if current_time - last_cron_check >= 60:
check_cron_jobs()
last_cron_check = current_time
process_task_queue()
flush_pending_alerts()
process_signals()
time.sleep(60)
except Exception as e:
error_msg = f"[Hermes] CRASH: {e}\n{traceback.format_exc()}"
print(error_msg, flush=True)
write_status(f"crashed: {e}")
if __name__ == "__main__":
agent_loop()