frank0957's picture
Update web_ui.py
917c4f3 verified
Raw
History Blame Contribute Delete
67.3 kB
# web_ui.py (v3.7.0 – added /arbitrage command and Pseudo‑settle by column)
# FastAPI application serving health endpoint, Telegram webhook, JSON API for dashboard,
# and a live dark‑mode dashboard with auto‑refresh (every 60 seconds, single API call).
# Includes:
# - World Clock with waving flag emojis
# - Economic Calendar card (today & tomorrow events)
# - /note saving, /task commands, /flag commands, /arbitrage status
# - Virtual account, dry‑run positions, LLM errors (today only)
# - Mobile‑friendly responsive tables (horizontal scroll)
# - Backup history, last restore, last factory rebuild, last restart
# - Dry‑Run Open Positions table now shows "Pseudo‑settle by" (entry_time + 3 days)
# All comments in English.
import os, glob, json
from datetime import datetime, timezone, timedelta
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, HTMLResponse
os.makedirs("/app/wiki", exist_ok=True)
app = FastAPI()
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
TELEGRAM_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
DEFAULT_FETCH_INTERVAL = 120
FETCH_INTERVAL_FILE = "/app/fetch_interval.txt"
TRADE_MODE_FILE = "/app/trade_mode.txt"
ADAPTIVE_HISTORY_FILE = "/app/adaptive_history.json"
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 read_json(path, default=None):
try:
if os.path.exists(path):
with open(path, "r") as f:
return json.load(f)
except:
pass
return default
def write_file(path, content):
try:
with open(path, "w") as f:
f.write(str(content))
except:
pass
# ── Health endpoint ──────────────────────────────
@app.api_route("/health", methods=["GET", "HEAD"])
def health():
return {"status": "ok"}
# ── Telegram webhook ─────────────────────────────
@app.api_route("/telegram-webhook", methods=["GET", "POST"])
async def telegram_webhook(request: Request):
if request.method == "GET":
return JSONResponse({"status": "Webhook endpoint is active"})
data = await request.json()
if "message" not in data:
return JSONResponse({"ok": True})
chat_id = data["message"]["chat"]["id"]
text = data["message"].get("text", "")
if text == "/status":
reply_text = "System is running. Risk pool: $5"
elif text == "/arbitrage":
# Check the state file written by arbitrage_launcher.py
state = read_file("/app/arbitrage_clob_state.txt", "unknown")
if state == "up":
reply_text = "✅ Arbitrage bot is running (CLOB API reachable)."
elif state == "down":
reply_text = "⚠️ Arbitrage bot is disabled – CLOB API unreachable."
else:
reply_text = "ℹ️ Arbitrage bot status unknown (still initialising)."
elif text == "/balance":
try:
if os.path.exists("/app/balance.txt"):
with open("/app/balance.txt") as f:
bal = float(f.read().strip())
reply_text = f"Current balance: ${bal:.2f}"
else:
reply_text = "Balance not yet available."
except:
reply_text = "Error reading balance."
elif text == "/pnl":
capital_state = read_json("/app/capital_state.json", {})
total_pnl = round(capital_state.get("total_capital", 20) - 20, 2)
reply_text = f"📊 P&L Summary\nTotal: ${total_pnl:.2f}"
elif text == "/wikicount":
wiki_dir = "/app/wiki"
if os.path.exists(wiki_dir):
count = len(glob.glob(os.path.join(wiki_dir, "*.md")))
reply_text = f"Wiki entries: {count}"
else:
reply_text = "Wiki directory not found."
elif text == "/lastsignal":
try:
if os.path.exists("/app/last_processed_signal.json"):
with open("/app/last_processed_signal.json") as f:
s = json.load(f)
ts = s.get("processed_at", "unknown")
reply_text = (
f"Last signal (processed):\n"
f"Time: {ts}\n"
f"Market: {s.get('market_slug')}\n"
f"Action: {s.get('action')}\n"
f"Confidence: {s.get('confidence')}"
)
elif os.path.exists("/app/latest_signals.json"):
with open("/app/latest_signals.json") as f:
signals = json.load(f)
if signals:
s = signals[-1]
reply_text = (
f"Pending signal:\n"
f"Market: {s.get('market_slug')}\n"
f"Action: {s.get('action')}\n"
f"Confidence: {s.get('confidence')}"
)
else:
reply_text = "No qualified signals found."
else:
reply_text = "No qualified signals found."
except:
reply_text = "Error reading signal file."
elif text == "/clusters":
cluster_data = read_json("/app/cluster_results.json")
if not cluster_data or not cluster_data.get("clusters"):
reply_text = "No cluster data yet. First analysis may take a few minutes."
else:
lines = []
for cid, profile in cluster_data["clusters"].items():
decision = profile.get("decision", "pending")
freq_tag = ""
if profile.get("avg_high_freq_score", 0) > 50 or profile.get("avg_small_trade_ratio", 0) > 0.8:
freq_tag = " [HIGH-FREQ]"
lines.append(
f"Cluster {cid}{freq_tag}: {profile['size']} wallets, "
f"win rate {profile['avg_win_rate']:.1%}, "
f"${profile['avg_trade_size']:.0f}/trade, "
f"{profile['avg_daily_trades']:.1f} trades/day → {decision}"
)
first_cluster = list(cluster_data["clusters"].values())[0]
auto_adj = first_cluster.get("auto_adjust")
if auto_adj:
lines.append(
f"Auto K: tried K={auto_adj['search_range']}, "
f"selected K={auto_adj['selected_k']} (silhouette={auto_adj['silhouette_score']})"
)
lines.append(
f"Adaptive: MIN_TRADES={auto_adj.get('min_trades_for_analysis', 'N/A')}, "
f"MIN_SIZE=${auto_adj.get('min_trade_size_usd', 'N/A')}"
)
lines.append(f"Followed wallets: {len(cluster_data.get('followed_wallets', []))}")
reply_text = "📊 Wallet Clusters:\n" + "\n".join(lines)
elif text.startswith("/note"):
note_content = text[5:].strip()
if not note_content:
reply_text = "Usage: /note <your observation>"
else:
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
entry = f"[{timestamp}] {note_content}\n"
try:
with open("/app/notes.txt", "a") as f:
f.write(entry)
safe_topic = note_content[:30].replace('/', '_').replace(' ', '_').replace('%', '_')
wiki_filename = f"{datetime.now(timezone.utc).strftime('%Y-%m-%d_%H-%M-%S')}_Note-{safe_topic}.md"
with open(f"/app/wiki/{wiki_filename}", "w") as wf:
wf.write(f"# Note: {note_content}\n\n**Time:** {timestamp}\n**Content:** {note_content}\n")
reply_text = f"Note saved and added to Wiki: {note_content}"
except Exception as e:
reply_text = f"Failed to save note: {e}"
elif text.startswith("/deletenote"):
search_text = text[11:].strip()
if not search_text:
reply_text = "Usage: /deletenote <text from the note you want to delete>"
else:
notes_path = "/app/notes.txt"
wiki_dir = "/app/wiki"
deleted_notes = 0
deleted_wiki = 0
if os.path.exists(notes_path):
with open(notes_path, "r") as f:
lines = f.readlines()
new_lines = [l for l in lines if search_text.lower() not in l.lower()]
if len(new_lines) < len(lines):
with open(notes_path, "w") as f:
f.writelines(new_lines)
deleted_notes = len(lines) - len(new_lines)
safe_topic = search_text[:30].replace('/', '_').replace(' ', '_').replace('%', '_')
pattern = os.path.join(wiki_dir, f"*_Note-{safe_topic}.md")
for fp in glob.glob(pattern):
try:
os.remove(fp)
deleted_wiki += 1
except:
pass
if deleted_notes > 0 or deleted_wiki > 0:
reply_text = f"Deleted {deleted_notes} line(s) from notes.txt and {deleted_wiki} wiki file(s) matching that text."
else:
reply_text = "No matching note found."
elif text.startswith("/flag"):
parts = text[5:].strip().split(maxsplit=2)
if len(parts) < 3:
reply_text = "Usage: /flag open <probability>% YES/NO <description> OR /flag close <description> [correct|incorrect]"
else:
sub = parts[0].lower()
if sub == "open":
prob_str = parts[1]
desc = parts[2]
flag = {
"probability": prob_str,
"description": desc,
"opened_at": datetime.now(timezone.utc).isoformat(),
"status": "open",
"result": None,
}
flags = read_json("/app/flags.json", [])
flags.append(flag)
with open("/app/flags.json", "w") as f:
json.dump(flags, f)
reply_text = f"🏴 Flag opened: {desc}"
elif sub == "close":
remaining = text[10:].strip()
tokens = remaining.split()
result = None
if tokens and tokens[-1].lower() in ("correct", "incorrect"):
result = tokens[-1].lower()
desc_to_close = " ".join(tokens[:-1])
else:
desc_to_close = remaining
if not desc_to_close:
reply_text = "Please specify a description to close."
else:
flags = read_json("/app/flags.json", [])
updated = False
for flag in flags:
if flag.get("status") == "open" and desc_to_close.lower() in flag.get("description", "").lower():
flag["status"] = "closed"
flag["closed_at"] = datetime.now(timezone.utc).isoformat()
flag["result"] = result
updated = True
if updated:
with open("/app/flags.json", "w") as f:
json.dump(flags, f)
reply_text = f"🏁 Flag closed: {desc_to_close}"
else:
reply_text = "No matching open flag found."
else:
reply_text = "Unknown flag action."
elif text.startswith("/task"):
task_parts = text[5:].strip().split()
if not task_parts:
reply_text = (
"Usage: /task <subcommand>. Options: simulate on|off|status, insane, conservative, "
"report now, threshold <value>, analyze <market>, wallet <addr>, pause, resume, interval [<seconds>], "
"backup_interval [<hours>], wiki_retention [<days>]"
)
else:
sub = task_parts[0].lower()
if sub == "simulate":
if len(task_parts) >= 2:
action = task_parts[1].lower()
if action == "on":
with open("/app/dry_run_active.txt", "w") as f:
f.write("1")
reply_text = "Dry‑run simulation enabled."
elif action == "off":
if os.path.exists("/app/dry_run_active.txt"):
os.remove("/app/dry_run_active.txt")
reply_text = "Dry‑run simulation disabled."
elif action == "status":
stats = {}
try:
if os.path.exists("/app/virtual_positions.json"):
with open("/app/virtual_positions.json") as f:
positions = json.load(f)
open_pos = [p for p in positions if p.get("status") == "open"]
closed_pos = [p for p in positions if p.get("status") == "closed"]
wins = [p for p in closed_pos if p.get("result") == "win"]
losses = [p for p in closed_pos if p.get("result") == "loss"]
total_pnl = sum(p.get("pnl", 0) or 0 for p in closed_pos)
stats = {
"active": os.path.exists("/app/dry_run_active.txt"),
"total": len(positions),
"open": len(open_pos),
"closed": len(closed_pos),
"wins": len(wins),
"losses": len(losses),
"win_rate": f"{(len(wins) / len(closed_pos) * 100):.1f}%" if closed_pos else "N/A",
"total_pnl": round(total_pnl, 2),
}
else:
stats = {"active": os.path.exists("/app/dry_run_active.txt"), "total": 0}
except Exception as e:
stats = {"error": str(e)}
reply_text = (
f"🧪 Dry‑Run Simulation Status\n"
f"Active: {'Enabled' if stats.get('active') else 'Disabled'}\n"
f"Total positions: {stats.get('total', 0)}\n"
f"Open: {stats.get('open', 0)} | Closed: {stats.get('closed', 0)}\n"
f"Wins: {stats.get('wins', 0)} | Losses: {stats.get('losses', 0)}\n"
f"Win rate: {stats.get('win_rate', 'N/A')}\n"
f"Total P&L: ${stats.get('total_pnl', 0)}"
)
else:
reply_text = "Unknown simulate action."
else:
reply_text = "Usage: /task simulate on|off|status"
elif sub == "insane":
write_file(TRADE_MODE_FILE, "insane")
write_file("/app/dry_run_active.txt", "1")
write_file(FETCH_INTERVAL_FILE, "15")
reply_text = "🔥 INSANE MODE activated. Virtual aggressive trading. Real money PROTECTED."
elif sub == "conservative":
write_file(TRADE_MODE_FILE, "conservative")
write_file(FETCH_INTERVAL_FILE, "120")
reply_text = "🛡️ Conservative mode restored."
elif sub == "report" and len(task_parts) >= 2 and task_parts[1] == "now":
with open("/app/force_report.txt", "w") as f:
f.write("1")
reply_text = "Report triggered."
elif sub == "threshold" and len(task_parts) >= 2:
try:
val = float(task_parts[1])
with open("/app/follow_threshold.txt", "w") as f:
f.write(str(val))
reply_text = f"Follow threshold set to {val}"
except:
reply_text = "Invalid threshold value."
elif sub == "interval":
if len(task_parts) >= 2:
try:
new_val = int(task_parts[1])
if new_val < 10:
reply_text = "Interval must be at least 10 seconds."
else:
write_file(FETCH_INTERVAL_FILE, new_val)
reply_text = f"Fetch interval set to {new_val} seconds."
except:
reply_text = "Invalid interval value."
else:
cur = read_file(FETCH_INTERVAL_FILE, str(DEFAULT_FETCH_INTERVAL))
reply_text = f"Current fetch interval: {cur} seconds."
elif sub == "backup_interval":
if len(task_parts) >= 2:
try:
hours = float(task_parts[1])
if hours < 1:
reply_text = "Backup interval must be at least 1 hour."
else:
write_file("/app/backup_interval_hours.txt", str(hours))
reply_text = f"Backup interval set to {hours} hours."
except:
reply_text = "Invalid number of hours."
else:
cur = read_file("/app/backup_interval_hours.txt", str(6))
reply_text = f"Current backup interval: {cur} hours."
elif sub == "wiki_retention":
if len(task_parts) >= 2:
try:
days = int(task_parts[1])
if days < 1:
reply_text = "Retention must be at least 1 day."
else:
write_file("/app/wiki_retention_days.txt", str(days))
reply_text = f"Wiki retention set to {days} days. Old entries will be archived daily."
except ValueError:
reply_text = "Invalid number of days."
else:
cur = read_file("/app/wiki_retention_days.txt", "45")
reply_text = f"Current wiki retention: {cur} days."
else:
reply_text = "Unknown task."
elif text == "/report":
reply_text = "Use /task report now for a fresh summary."
elif text == "/errors":
errors = read_file("/app/llm_error_count.txt", "0")
details = []
try:
with open("/app/llm_error_details.txt") as f:
details = [line.strip() for line in f.readlines() if line.strip()][-5:]
except:
pass
reply_text = f"LLM errors today: {errors}"
if details:
reply_text += "\nRecent details:\n" + "\n".join(details)
elif "hello" in text.lower():
reply_text = "Hello! I'm alive and running on Hugging Face."
else:
reply_text = (
"Unknown command. Try /status, /balance, /pnl, /wikicount, /lastsignal, /clusters, "
"/note, /flag, /task, /report, /errors, /deletenote, /arbitrage"
)
return JSONResponse({"method": "sendMessage", "chat_id": chat_id, "text": reply_text})
# ── Unified API endpoint ────────────────────────
@app.get("/api/all")
def api_all():
balance = read_file("/app/balance.txt", "N/A")
signal = {"market": "None", "action": "", "confidence": "", "processed_at": ""}
try:
if os.path.exists("/app/last_processed_signal.json"):
with open("/app/last_processed_signal.json") as f:
s = json.load(f)
signal = {
"market": s.get("market_slug"),
"action": s.get("action"),
"confidence": s.get("confidence"),
"processed_at": s.get("processed_at", ""),
}
elif os.path.exists("/app/latest_signals.json"):
with open("/app/latest_signals.json") as f:
signals = json.load(f)
if signals:
s = signals[-1]
signal = {
"market": s.get("market_slug"),
"action": s.get("action"),
"confidence": s.get("confidence"),
"processed_at": "",
}
except:
pass
hermes_decision = read_file("/app/hermes_status.txt", "No recent decision")
wiki_entries = []
wiki_dir = "/app/wiki"
if os.path.exists(wiki_dir):
files = sorted(glob.glob(os.path.join(wiki_dir, "*.md")), reverse=True)[:5]
for fp in files:
name = os.path.basename(fp)
parts = name.split("_", 2)
ts = parts[0] + " " + parts[1].replace("-", ":") if len(parts) >= 2 else name
wiki_entries.append({"time": ts, "filename": name})
polyhub = read_file("/app/polyhub_status.txt", "unknown")
hermes = read_file("/app/hermes_status.txt", "unknown")
balance_updater = read_file("/app/balance_status.txt", "unknown")
report_text = "No report yet."
try:
if os.path.exists("/app/dashboard_report.json"):
with open("/app/dashboard_report.json") as f:
report_text = json.load(f).get("report", "No report yet.")
except:
pass
# ── LLM errors – today only ──────────────────────
llm_errors_today = read_file("/app/llm_error_count.txt", "0")
today_prefix = datetime.now(timezone.utc).strftime("%Y-%m-%d")
error_counts_today = {}
try:
with open("/app/llm_error_types.txt") as f:
for line in f:
line = line.strip()
if not line:
continue
if line.startswith(f"[{today_prefix}"):
etype = line.split("] ", 1)[1].strip()
error_counts_today[etype] = error_counts_today.get(etype, 0) + 1
except:
pass
error_details_today = []
try:
with open("/app/llm_error_details.txt") as f:
for line in f:
line = line.strip()
if line.startswith(f"[{today_prefix}"):
error_details_today.append(line)
except:
pass
error_details_today = error_details_today[-5:]
recent_notes = []
total_notes = 0
try:
with open("/app/notes.txt") as f:
lines = [line.strip() for line in f.readlines() if line.strip()]
total_notes = len(lines)
recent_notes = lines[-5:]
except:
pass
cluster_summary = []
auto_adjust_info = None
cluster_data = read_json("/app/cluster_results.json")
if cluster_data and cluster_data.get("clusters"):
for cid, profile in cluster_data["clusters"].items():
is_high_freq = (
profile.get("avg_high_freq_score", 0) > 50 or profile.get("avg_small_trade_ratio", 0) > 0.8
)
cluster_summary.append(
{
"id": cid,
"size": profile["size"],
"avg_win_rate": round(profile["avg_win_rate"], 3),
"avg_trade_size": round(profile["avg_trade_size"], 0),
"avg_daily_trades": round(profile["avg_daily_trades"], 1),
"decision": profile.get("decision", "pending"),
"is_high_freq": is_high_freq,
}
)
if auto_adjust_info is None:
auto_adjust_info = profile.get("auto_adjust")
followed_count = len(cluster_data.get("followed_wallets", []))
else:
followed_count = 0
if auto_adjust_info is None:
adaptive_params = read_json("/app/adaptive_params.json", {})
if adaptive_params:
auto_adjust_info = {
"search_range": "N/A",
"selected_k": "N/A",
"silhouette_score": "N/A",
"min_trades_for_analysis": adaptive_params.get("min_trades_for_analysis", 5),
"min_trade_size_usd": adaptive_params.get("min_trade_size_usd", 1.0),
}
adaptive_history = read_json(ADAPTIVE_HISTORY_FILE, [])[-5:]
capital_state = read_json("/app/capital_state.json", {})
total_capital = capital_state.get("total_capital", 20.0)
comp_summary = {
"tier": capital_state.get("tier", "L1"),
"consecutive_wins": capital_state.get("consecutive_wins", 0),
"consecutive_losses": capital_state.get("consecutive_losses", 0),
"risk_pool": capital_state.get("risk_pool", 5.0),
"max_bet": capital_state.get("max_bet", 1.0),
"halted": capital_state.get("halted", False),
"original_deposit": capital_state.get("original_deposit"),
"deposit_date": capital_state.get("deposit_date"),
"total_capital": total_capital,
}
# ── Phase definitions ───────────────────────────
phase_thresholds = [50, 200, 1000, 5000]
phase_names = [
"Phase 1: Cold Start",
"Phase 2: Acceleration",
"Phase 3: Breakthrough",
"Phase 4: Expansion",
"Phase 5: Stability",
]
phase_details = [
"Build risk pool, upgrade to L2",
"Grow risk pool, upgrade to L3",
"Monthly net profit $500‑1K",
"Platform migration & paid models",
"Monthly net profit $5K+",
]
current_phase = 1
for i, thr in enumerate(phase_thresholds):
if total_capital >= thr:
current_phase = i + 2
phases = []
for i in range(5):
start_cap = 0 if i == 0 else phase_thresholds[i - 1]
end_cap = phase_thresholds[i] if i < 4 else None
if i == current_phase - 1:
progress = min(1.0, (total_capital - start_cap) / (end_cap - start_cap)) if end_cap else 1.0
elif i < current_phase - 1:
progress = 1.0
else:
progress = 0.0
phases.append(
{
"name": phase_names[i],
"detail": phase_details[i],
"start": start_cap,
"end": end_cap,
"progress": round(progress, 3),
"is_current": (i == current_phase - 1),
}
)
# ── KPI tracker ─────────────────────────────────
monthly_profit_target = float(os.getenv("KPI_MONTHLY_TARGET", "500"))
monthly_profit = 0.0
try:
pnl_data = read_json("/app/balance_pivot.json")
if pnl_data and "daily" in pnl_data and balance:
bal = float(balance)
daily_base = pnl_data.get("daily", bal)
daily_pnl = bal - daily_base
monthly_profit = round(daily_pnl * 30, 2)
except:
pass
kpi = {
"monthly_target": monthly_profit_target,
"current_monthly_profit": monthly_profit,
"progress_pct": round(min(1.0, monthly_profit / monthly_profit_target) * 100, 1),
"break_even_days": "N/A",
}
if monthly_profit > 0:
daily_profit = monthly_profit / 30
if daily_profit > 0:
kpi["break_even_days"] = int(monthly_profit_target / daily_profit)
# ── Virtual capital state ───────────────────────
virtual_capital = read_json("/app/virtual_capital_state.json", {})
tier_idx = virtual_capital.get("tier_index", 0)
tier_labels = ["L1", "L2", "L3", "L4"]
virtual_summary = {
"original_deposit": virtual_capital.get("original_deposit", 20.0),
"total_capital": round(virtual_capital.get("total_capital", 20.0), 2),
"risk_pool": round(virtual_capital.get("risk_pool", 5.0), 2),
"tier": tier_labels[tier_idx] if isinstance(tier_idx, int) and 0 <= tier_idx < 4 else "L1",
"max_bet": round(virtual_capital.get("max_bet", 1.0), 2),
"halted": virtual_capital.get("halted", False),
}
# ── Virtual positions detail ────────────────────
virtual_positions_data = []
try:
if os.path.exists("/app/virtual_positions.json"):
with open("/app/virtual_positions.json") as f:
virtual_positions_data = json.load(f)
except:
pass
open_positions = [p for p in virtual_positions_data if p.get("status") == "open"]
closed_positions = [p for p in virtual_positions_data if p.get("status") == "closed"]
wins = [p for p in closed_positions if p.get("result") == "win"]
losses = [p for p in closed_positions if p.get("result") == "loss"]
total_pnl = sum(p.get("pnl", 0) or 0 for p in closed_positions)
dry_run_stats = {
"active": os.path.exists("/app/dry_run_active.txt"),
"total": len(virtual_positions_data),
"open": len(open_positions),
"closed": len(closed_positions),
"wins": len(wins),
"losses": len(losses),
"win_rate": f"{(len(wins) / len(closed_positions) * 100):.1f}%" if closed_positions else "N/A",
"total_pnl": round(total_pnl, 2),
"open_positions": [
{
"market": p["market"],
"action": p["action"],
"confidence": p.get("confidence", 0),
"bet_amount": p.get("bet_amount", 0),
"entry_time": p["entry_time"],
"tier": p.get("tier_at_entry", "L1"),
"strategy": p.get("strategy", "copy-trade"),
# ── Pseudo‑settle deadline = entry_time + 3 days ──
"pseudo_settle_by": (datetime.fromisoformat(p["entry_time"]) + timedelta(days=3)).strftime("%Y-%m-%d %H:%M")
}
for p in open_positions[-5:]
],
"closed_positions": [
{
"market": p["market"],
"action": p["action"],
"result": p.get("result", "unknown"),
"pnl": p.get("pnl", 0),
"entry_time": p["entry_time"],
"exit_time": p.get("exit_time", ""),
"strategy": p.get("strategy", "copy-trade"),
}
for p in closed_positions[-5:]
],
}
flags = read_json("/app/flags.json", [])
open_flags = [f for f in flags if f.get("status") == "open"]
closed_flags = [f for f in flags if f.get("status") != "open"]
correct = len([f for f in closed_flags if f.get("result") == "correct"])
incorrect = len([f for f in closed_flags if f.get("result") == "incorrect"])
flag_summary = {
"total": len(flags),
"open": len(open_flags),
"closed": len(closed_flags),
"correct": correct,
"incorrect": incorrect,
"accuracy": f"{(correct / (correct + incorrect) * 100):.1f}%" if (correct + incorrect) > 0 else "N/A",
}
backup_status = "disabled"
backup_time = "never"
backup_raw = read_file("/app/backup_status.txt", "")
if backup_raw and "|" in backup_raw:
parts = backup_raw.split("|", 1)
backup_status = parts[0].strip()
backup_time = parts[1].strip()
# ── Timestamps ─────────────────────────────
last_restore = read_file("/app/last_restore.txt", "Never")
last_startup = read_file("/app/last_startup.txt", "Unknown")
last_factory_rebuild = read_file("/app/last_factory_rebuild.txt", "Unknown")
# ── Backup history (last 10 entries) ───────
backup_history = []
try:
if os.path.exists("/app/backup_history.txt"):
with open("/app/backup_history.txt", "r") as f:
lines = [line.strip() for line in f.readlines() if line.strip()]
backup_history = lines[-10:]
except:
pass
# ── Economic Calendar (today & tomorrow) ──────────
economic_events = read_json("/app/economic_calendar_data.json", [])
health_score = 100
if "running" not in polyhub:
health_score -= 30
if "running" not in hermes:
health_score -= 30
if "running" not in balance_updater:
health_score -= 20
if comp_summary.get("halted"):
health_score -= 20
if backup_status == "failed":
health_score -= 10
return {
"balance": balance,
"signal": signal,
"hermes_decision": hermes_decision,
"wiki_entries": wiki_entries,
"polyhub": polyhub,
"hermes": hermes,
"balance_updater": balance_updater,
"backup_status": backup_status,
"backup_time": backup_time,
"report": report_text,
"llm_errors_today": llm_errors_today,
"llm_error_counts": error_counts_today,
"llm_error_details": error_details_today,
"recent_notes": recent_notes,
"total_notes": total_notes,
"clusters": cluster_summary,
"followed_wallets": followed_count,
"auto_adjust": auto_adjust_info,
"compounding": comp_summary,
"virtual_capital": virtual_summary,
"dry_run": dry_run_stats,
"flags": flag_summary,
"health_score": health_score,
"phases": phases,
"current_phase": current_phase,
"kpi": kpi,
"adaptive_history": adaptive_history,
"last_restore": last_restore,
"last_startup": last_startup,
"last_factory_rebuild": last_factory_rebuild,
"backup_history": backup_history,
"economic_events": economic_events,
}
# ── Dashboard (HTML) with waving flag animation ──
@app.get("/dashboard", response_class=HTMLResponse)
def dashboard():
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=yes">
<title>Polymarket Bot Dashboard</title>
<style>
:root {
--bg: #1e1e2e;
--surface: #2a2a3c;
--text: #e0e0e0;
--accent: #89b4fa;
--ok: #a6e3a1;
--warn: #fab387;
--err: #f38ba8;
--border: #45475a;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background: var(--bg);
color: var(--text);
}
h1 { color: var(--accent); }
.card {
background: var(--surface);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.card h2 { margin-top: 0; border-bottom: 2px solid var(--accent); padding-bottom: 8px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid var(--border); }
.badge { padding: 4px 10px; border-radius: 6px; font-size: 0.8em; font-weight: bold; }
.ok { background: var(--ok); color: #1e1e2e; }
.warn { background: var(--warn); color: #1e1e2e; }
.err { background: var(--err); color: #1e1e2e; }
.small { font-size: 0.85em; color: #a0a0b0; }
.progress-bar { width: 100%; height: 20px; background: #444; border-radius: 10px; overflow: hidden; margin: 4px 0; }
.progress-fill { height: 100%; transition: width 0.5s; background: var(--accent); }
.current-phase .progress-fill { background: var(--ok); }
#healthBar { height: 100%; width: 0%; transition: width 0.5s; }
.healthy { background: var(--ok); }
.warning { background: var(--warn); }
.critical { background: var(--err); }
pre { white-space: pre-wrap; font-family: inherit; margin: 0; }
.clock-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 8px; }
.clock-city { font-weight: bold; }
.clock-time { font-size: 1.2em; }
/* ── Waving flag animation ────────────────── */
.flag-emoji {
display: inline-block;
font-size: 1.4em;
margin-right: 4px;
animation: wave 2.5s infinite ease-in-out;
transform-origin: bottom center;
}
@keyframes wave {
0% { transform: rotate(0deg); }
25% { transform: rotate(8deg); }
50% { transform: rotate(0deg); }
75% { transform: rotate(-8deg); }
100% { transform: rotate(0deg); }
}
.history-table { margin-top: 12px; }
.history-table th { background: #333; }
/* ── Mobile‑friendly: horizontal scroll for all tables ─── */
@media (max-width: 600px) {
body { margin: 10px; }
.card { padding: 10px; }
.card table {
display: block;
width: 100%;
overflow-x: auto;
white-space: nowrap;
}
.responsive-table td::before {
content: attr(data-label);
font-weight: bold;
margin-right: 10px;
}
.responsive-table td {
display: flex;
justify-content: space-between;
}
}
</style>
</head>
<body>
<h1>🤖 Polymarket Trading Bot Dashboard</h1>
<div id="healthIndicator" class="progress-bar"><div id="healthBar" class="progress-fill"></div></div>
<p class="small" id="healthLabel">Health Score: --</p>
<p class="small" id="lastRefresh">Loading...</p>
<!-- World Clock with waving flags -->
<div class="card">
<h2>🕒 World Clock</h2>
<div class="clock-grid" id="worldClock"></div>
</div>
<!-- Economic Calendar (today & tomorrow) -->
<div class="card">
<h2>📅 Economic Calendar (Today & Tomorrow)</h2>
<div id="econCalendar">Loading...</div>
</div>
<!-- 5‑Phase Roadmap -->
<div class="card">
<h2>🎯 5‑Phase Roadmap</h2>
<div id="phaseList"></div>
</div>
<!-- KPI Tracker -->
<div class="card">
<h2>📈 KPI Tracker (Monthly Net Profit)</h2>
<table>
<tr><td data-label="Target">Target</td><td data-label="Value" id="kpiTarget">-</td></tr>
<tr><td data-label="Current Month Profit">Current Month Profit</td><td data-label="Value" id="kpiCurrent">-</td></tr>
<tr><td data-label="Progress">Progress</td><td data-label="Value"><span id="kpiProgress">-</span></td></tr>
<tr><td data-label="Break‑even (days)">Break‑even (days)</td><td data-label="Value" id="kpiBreakEven">-</td></tr>
</table>
</div>
<!-- System Status -->
<div class="card">
<h2>📊 System Status</h2>
<table id="statusTable" class="responsive-table">
<tr><td data-label="Original Deposit">Original Deposit</td><td data-label="Value"><strong id="originalDepositStatus">-</strong></td></tr>
<tr><td data-label="Current Balance">Current Balance</td><td data-label="Value"><strong id="currentBalanceStatus">-</strong></td></tr>
<tr><td data-label="PolyHub Listener">PolyHub Listener</td><td data-label="Value"><span class="badge" id="polyhubBadge">-</span></td></tr>
<tr><td data-label="Hermes Agent">Hermes Agent</td><td data-label="Value"><span class="badge" id="hermesBadge">-</span></td></tr>
<tr><td data-label="Balance Updater">Balance Updater</td><td data-label="Value"><span class="badge" id="balanceUpdaterBadge">-</span></td></tr>
<tr><td data-label="Git Backup">Git Backup</td><td data-label="Value"><span class="badge" id="backupBadge">-</span> <span class="small" id="backupTime"></span></td></tr>
<tr><td data-label="Last Restore">Last Restore</td><td data-label="Value"><span id="lastRestore">-</span></td></tr>
<tr><td data-label="Last Factory Rebuild">Last Factory Rebuild</td><td data-label="Value"><span id="lastFactoryRebuild">-</span></td></tr>
<tr><td data-label="Last Space Restart">Last Space Restart</td><td data-label="Value"><span id="lastStartup">-</span></td></tr>
</table>
</div>
<div class="card">
<h2>🕒 Backup History (last 10)</h2>
<table id="backupHistoryTable">
<tr><th>Timestamp (UTC)</th><th>Status</th></tr>
<tbody id="backupHistoryBody">
<tr><td colspan="2">No entries yet.</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h2>📡 Latest Signal</h2>
<p id="signalText">-</p>
</div>
<div class="card">
<h2>🧠 Hermes Decision</h2>
<p id="hermesText">-</p>
</div>
<div class="card">
<h2>⚠️ LLM Errors Today</h2>
<p id="llmErrors">-</p>
<div id="llmErrorTypes"><pre>-</pre></div>
<div id="llmErrorDetails"><pre>-</pre></div>
</div>
<div class="card">
<h2>📝 Recent Notes (<span id="notesTotal">0</span>)</h2>
<div id="notesList"><pre>-</pre></div>
</div>
<div class="card">
<h2>💰 Balance & Compounding</h2>
<table>
<tr><td>Original Deposit</td><td id="originalDeposit">-</td></tr>
<tr><td>Current Balance</td><td id="balance2">-</td></tr>
<tr><td>Tier</td><td id="compTier">-</td></tr>
<tr><td>Consecutive Wins/Losses</td><td id="compWinsLosses">-</td></tr>
<tr><td>Risk Pool</td><td id="riskPool">-</td></tr>
<tr><td>MAX BET</td><td id="maxBet">-</td></tr>
<tr><td>Halted</td><td id="halted">-</td></tr>
</table>
</div>
<div class="card">
<h2>💰 Virtual Account</h2>
<table>
<tr><td>Original Deposit</td><td id="vOriginalDeposit">-</td></tr>
<tr><td>Current Balance</td><td id="vBalance">-</td></tr>
<tr><td>Risk Pool</td><td id="vRiskPool">-</td></tr>
<tr><td>Tier</td><td id="vTier">-</td></tr>
<tr><td>MAX BET</td><td id="vMaxBet">-</td></tr>
</table>
</div>
<div class="card">
<h2>🧪 Dry Run Simulation</h2>
<table>
<tr><td>Active</td><td id="dryActive">-</td></tr>
<tr><td>Open Positions</td><td id="dryOpen">-</td></tr>
<tr><td>Closed</td><td id="dryClosed">-</td></tr>
<tr><td>Wins / Losses</td><td id="dryWinsLosses">-</td></tr>
<tr><td>Win Rate</td><td id="dryWinRate">-</td></tr>
<tr><td>Total P&L</td><td id="dryPnl">-</td></tr>
</table>
</div>
<div class="card">
<h2>📋 Dry Run Open Positions</h2>
<div id="openPositionsTable">-</div>
</div>
<div class="card">
<h2>📋 Dry Run Closed Positions</h2>
<div id="closedPositionsTable">-</div>
</div>
<div class="card">
<h2>🏴 Flags</h2>
<table>
<tr><td>Open</td><td id="flagsOpen">-</td></tr>
<tr><td>Closed</td><td id="flagsClosed">-</td></tr>
<tr><td>Accuracy</td><td id="flagsAccuracy">-</td></tr>
</table>
</div>
<div class="card">
<h2>📊 Wallet Clusters (LLM reasoning)</h2>
<div id="clusterTable">-</div>
<div id="autoAdjustRow" class="small" style="margin-top:8px;"></div>
<div id="adaptiveHistoryRow" style="margin-top:12px;"></div>
</div>
<div class="card">
<h2>📚 Recent Wiki Entries (<span id="wikiTotal">0</span>)</h2>
<div id="wikiList">-</div>
</div>
<div class="card">
<h2>📊 Daily Detailed Report</h2>
<div id="detailedReport">Loading...</div>
</div>
<p class="small">Dashboard auto‑refreshes every 60 seconds. Last update: <span id="updateTime"></span></p>
<script>
// ── World Clock with waving flags ──────────
const CITIES = [
{ name: "Hong Kong", tz: "Asia/Hong_Kong", flag: "🇭🇰" },
{ name: "Tokyo", tz: "Asia/Tokyo", flag: "🇯🇵" },
{ name: "London", tz: "Europe/London", flag: "🇬🇧" },
{ name: "Dubai", tz: "Asia/Dubai", flag: "🇦🇪" },
{ name: "Zurich", tz: "Europe/Zurich", flag: "🇨🇭" },
{ name: "Accra", tz: "Africa/Accra", flag: "🇬🇭" },
{ name: "New York", tz: "America/New_York", flag: "🇺🇸" },
{ name: "Los Angeles", tz: "America/Los_Angeles", flag: "🇺🇸" },
{ name: "Buenos Aires", tz: "America/Argentina/Buenos_Aires", flag: "🇦🇷" },
{ name: "Sydney", tz: "Australia/Sydney", flag: "🇦🇺" }
];
function updateWorldClock() {
const container = document.getElementById('worldClock');
let html = '';
CITIES.forEach(city => {
try {
const now = new Date().toLocaleString('en-US', { timeZone: city.tz });
const date = new Date(now);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
html += `<div>
<span class="flag-emoji">${city.flag}</span>
<span class="clock-city">${city.name}</span><br>
<span class="clock-time">${hours}:${minutes}:${seconds}</span>
</div>`;
} catch (e) {
html += `<div>
<span class="flag-emoji">${city.flag}</span>
<span class="clock-city">${city.name}</span><br>
<span class="clock-time">N/A</span>
</div>`;
}
});
container.innerHTML = html;
}
setInterval(updateWorldClock, 1000);
updateWorldClock();
// ── Dashboard refresh with mobile support ──────
let refreshTimer = null;
function startAutoRefresh() {
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = setInterval(refreshData, 60000);
}
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
refreshData();
startAutoRefresh();
} else {
if (refreshTimer) clearInterval(refreshTimer);
}
});
async function refreshData() {
try {
const res = await fetch('/api/all');
const data = await res.json();
// Health
const score = data.health_score || 100;
const bar = document.getElementById('healthBar');
bar.style.width = score + '%';
bar.className = score >= 80 ? 'healthy' : (score >= 50 ? 'warning' : 'critical');
document.getElementById('healthLabel').textContent = 'Health Score: ' + score + '/100';
// Phases
const phases = data.phases || [];
let phaseHtml = '';
phases.forEach(p => {
const pct = (p.progress * 100).toFixed(0);
phaseHtml +=
'<div style="margin-bottom:12px;" class="' + (p.is_current ? 'current-phase' : '') + '">' +
'<strong>' + p.name + '</strong> <span class="small">' + p.detail + '</span>' +
'<div class="progress-bar"><div class="progress-fill" style="width:' + pct + '%"></div></div>' +
'<div class="small">' + (p.is_current ? '← Current' : (p.progress >= 1 ? 'Completed' : 'Pending')) + ' | Target: $' + (p.end || '∞') + '</div></div>';
});
document.getElementById('phaseList').innerHTML = phaseHtml;
// KPI
const kpi = data.kpi || {};
document.getElementById('kpiTarget').textContent = '$' + (kpi.monthly_target || 500);
document.getElementById('kpiCurrent').textContent = '$' + (kpi.current_monthly_profit || 0);
document.getElementById('kpiProgress').textContent = (kpi.progress_pct || 0) + '%';
document.getElementById('kpiBreakEven').textContent = kpi.break_even_days || 'N/A';
// System status
updateBadge('polyhubBadge', data.polyhub);
updateBadge('hermesBadge', data.hermes);
updateBadge('balanceUpdaterBadge', data.balance_updater);
const backupStatus = data.backup_status || 'disabled';
const backupBadge = document.getElementById('backupBadge');
backupBadge.textContent = backupStatus;
backupBadge.className = 'badge';
if (backupStatus === 'success') backupBadge.classList.add('ok');
else if (backupStatus === 'failed') backupBadge.classList.add('err');
else backupBadge.classList.add('warn');
document.getElementById('backupTime').textContent = data.backup_time ? ' at ' + data.backup_time : '';
// Original Deposit & Current Balance
const comp = data.compounding || {};
document.getElementById('originalDepositStatus').textContent = comp.original_deposit
? '$' + comp.original_deposit + (comp.deposit_date ? ' on ' + comp.deposit_date : '')
: 'N/A';
document.getElementById('currentBalanceStatus').textContent = '$' + data.balance;
// Timestamps
document.getElementById('lastRestore').textContent = data.last_restore || 'Never';
document.getElementById('lastFactoryRebuild').textContent = data.last_factory_rebuild || 'Unknown';
document.getElementById('lastStartup').textContent = data.last_startup || 'Unknown';
// Backup history
const historyBody = document.getElementById('backupHistoryBody');
if (data.backup_history && data.backup_history.length > 0) {
let rows = '';
data.backup_history.forEach(line => {
const parts = line.split('|');
const time = parts[0].trim();
const status = parts[1].trim();
rows += `<tr><td>${time}</td><td>${status}</td></tr>`;
});
historyBody.innerHTML = rows;
} else {
historyBody.innerHTML = '<tr><td colspan="2">No entries yet.</td></tr>';
}
// Signal
const sig = data.signal;
const signalEl = document.getElementById('signalText');
if (sig.market !== 'None') {
let html = sig.market + ' | ' + sig.action + ' | conf ' + sig.confidence;
if (sig.processed_at) {
const processed = new Date(sig.processed_at);
if (!isNaN(processed.getTime())) {
const diffMs = Date.now() - processed.getTime();
const mins = Math.floor(diffMs / 60000);
let timeAgo;
if (mins < 1) timeAgo = 'just now';
else if (mins < 60) timeAgo = mins + ' min ago';
else {
const hours = Math.floor(mins / 60);
timeAgo = hours + ' hour' + (hours > 1 ? 's' : '') + ' ago';
}
html += '<br><small>' + sig.processed_at + ' (' + timeAgo + ')</small>';
} else {
html += '<br><small>' + sig.processed_at + '</small>';
}
}
signalEl.innerHTML = html;
} else {
signalEl.textContent = 'No qualified signals found.';
}
// Hermes decision
document.getElementById('hermesText').textContent = data.hermes_decision;
// LLM errors (today only)
document.getElementById('llmErrors').textContent = data.llm_errors_today || '0';
const errCounts = data.llm_error_counts || {};
let errLines = [];
for (const [type, cnt] of Object.entries(errCounts)) {
errLines.push(type + ': ' + cnt);
}
document.getElementById('llmErrorTypes').innerHTML = errLines.length
? '<pre>' + errLines.join('\\n') + '</pre>'
: '<pre>No errors recorded today</pre>';
const errDetails = data.llm_error_details || [];
document.getElementById('llmErrorDetails').innerHTML = errDetails.length
? '<pre>' + errDetails.join('\\n') + '</pre>'
: '<pre></pre>';
// Notes
const notes = data.recent_notes || [];
document.getElementById('notesTotal').textContent = data.total_notes || 0;
document.getElementById('notesList').innerHTML = notes.length
? '<pre>' + notes.join('\\n') + '</pre>'
: '<pre>No notes yet</pre>';
// Balance & Compounding
document.getElementById('originalDeposit').textContent = comp.original_deposit
? '$' + comp.original_deposit + (comp.deposit_date ? ' on ' + comp.deposit_date : '')
: 'N/A';
document.getElementById('balance2').textContent = '$' + data.balance;
document.getElementById('compTier').textContent = comp.tier || 'L1';
document.getElementById('compWinsLosses').textContent =
'Wins: ' + (comp.consecutive_wins || 0) + ' / Losses: ' + (comp.consecutive_losses || 0);
document.getElementById('riskPool').textContent = '$' + (comp.risk_pool || 5).toFixed(2);
document.getElementById('maxBet').textContent = '$' + (comp.max_bet || 1).toFixed(2);
document.getElementById('halted').textContent = comp.halted ? 'YES – HALTED' : 'NO';
// Virtual capital
const vc = data.virtual_capital || {};
document.getElementById('vOriginalDeposit').textContent = '$' + (vc.original_deposit || 20).toFixed(2);
document.getElementById('vBalance').textContent = '$' + (vc.total_capital || 20).toFixed(2);
document.getElementById('vRiskPool').textContent = '$' + (vc.risk_pool || 5).toFixed(2);
document.getElementById('vTier').textContent = vc.tier || 'L1';
document.getElementById('vMaxBet').textContent = '$' + (vc.max_bet || 1).toFixed(2);
// Dry Run – uses "Enabled"/"Disabled"
const dry = data.dry_run || {};
document.getElementById('dryActive').textContent = dry.active ? 'Enabled' : 'Disabled';
document.getElementById('dryOpen').textContent = dry.open || 0;
document.getElementById('dryClosed').textContent = dry.closed || 0;
document.getElementById('dryWinsLosses').textContent =
(dry.wins || 0) + ' W / ' + (dry.losses || 0) + ' L';
document.getElementById('dryWinRate').textContent = dry.win_rate || 'N/A';
document.getElementById('dryPnl').textContent = '$' + (dry.total_pnl || 0);
// ── Open positions table (with Pseudo‑settle by) ──
const openPos = dry.open_positions || [];
let opHtml = '';
if (openPos.length > 0) {
opHtml = '<table><tr><th>Market</th><th>Action</th><th>Conf</th><th>Bet</th><th>Entry Time</th><th>Pseudo‑settle by</th><th>Tier</th><th>Strategy</th></tr>';
openPos.forEach(p => {
opHtml +=
'<tr><td>' + p.market + '</td><td>' + p.action + '</td><td>' + p.confidence + '</td><td>$' + p.bet_amount + '</td><td>' + p.entry_time + '</td><td>' + (p.pseudo_settle_by || '—') + '</td><td>' + p.tier + '</td><td>' + p.strategy + '</td></tr>';
});
opHtml += '</table>';
} else {
opHtml = '<p>No open positions.</p>';
}
document.getElementById('openPositionsTable').innerHTML = opHtml;
// Closed positions table
const closedPos = dry.closed_positions || [];
let cpHtml = '';
if (closedPos.length > 0) {
cpHtml = '<table><tr><th>Market</th><th>Action</th><th>Result</th><th>P&L</th><th>Entry</th><th>Exit</th><th>Strategy</th></tr>';
closedPos.forEach(p => {
cpHtml +=
'<tr><td>' + p.market + '</td><td>' + p.action + '</td><td>' + p.result + '</td><td>$' + p.pnl + '</td><td>' + p.entry_time + '</td><td>' + p.exit_time + '</td><td>' + p.strategy + '</td></tr>';
});
cpHtml += '</table>';
} else {
cpHtml = '<p>No closed positions yet.</p>';
}
document.getElementById('closedPositionsTable').innerHTML = cpHtml;
// Flags
const flags = data.flags || {};
document.getElementById('flagsOpen').textContent = flags.open || 0;
document.getElementById('flagsClosed').textContent = flags.closed || 0;
document.getElementById('flagsAccuracy').textContent = flags.accuracy || 'N/A';
// Wallet clusters
const clusterDiv = document.getElementById('clusterTable');
if (data.clusters && data.clusters.length > 0) {
let tableHtml = '<table><tr><th>ID</th><th>Size</th><th>Win Rate</th><th>Trade Size</th><th>Freq/day</th><th>Decision</th></tr>';
data.clusters.forEach(c => {
const decisionColor = c.decision === 'FOLLOW' ? 'ok' : (c.decision === 'IGNORE' ? 'err' : 'warn');
let extraBadge = '';
if (c.is_high_freq) extraBadge = ' <span class="badge warn">HIGH-FREQ</span>';
tableHtml +=
'<tr><td>' + c.id + '</td><td>' + c.size + '</td><td>' + (c.avg_win_rate * 100).toFixed(1) + '%</td><td>$' + c.avg_trade_size + '</td><td>' + c.avg_daily_trades + '</td><td><span class="badge ' + decisionColor + '">' + c.decision + '</span>' + extraBadge + '</td></tr>';
});
tableHtml += '<tr><td colspan="6" class="small">Followed wallets total: ' + data.followed_wallets + '</td></tr></table>';
clusterDiv.innerHTML = tableHtml;
const autoAdj = data.auto_adjust;
if (autoAdj) {
document.getElementById('autoAdjustRow').innerHTML =
'Auto K selection: tried K=' + autoAdj.search_range + ', selected K=' + autoAdj.selected_k + ' (silhouette=' + autoAdj.silhouette_score + ')<br>' +
'Adaptive: MIN_TRADES=' + autoAdj.min_trades_for_analysis + ', MIN_SIZE=$' + autoAdj.min_trade_size_usd;
} else {
document.getElementById('autoAdjustRow').innerHTML = '';
}
} else {
clusterDiv.innerHTML = '<p>No cluster data yet. First analysis in progress...</p>';
const autoAdj = data.auto_adjust;
if (autoAdj) {
document.getElementById('autoAdjustRow').innerHTML =
'Adaptive params (current): MIN_TRADES=' + autoAdj.min_trades_for_analysis + ', MIN_SIZE=$' + autoAdj.min_trade_size_usd;
} else {
document.getElementById('autoAdjustRow').innerHTML = '';
}
}
// Adaptive history
const history = data.adaptive_history || [];
const histDiv = document.getElementById('adaptiveHistoryRow');
if (history.length > 0) {
let ht = '<h3>Adaptive History (Last ' + history.length + ' changes)</h3>';
ht += '<table class="history-table"><tr><th>Time</th><th>From</th><th>To</th><th>Reason</th></tr>';
history.forEach(h => {
ht += '<tr><td>' + h.time + '</td><td>' + h.from + '</td><td>' + h.to + '</td><td>' + h.reason + '</td></tr>';
});
ht += '</table>';
histDiv.innerHTML = ht;
} else {
histDiv.innerHTML = '<p class="small">No parameter changes yet.</p>';
}
// ── Economic Calendar ──
const econDiv = document.getElementById('econCalendar');
if (data.economic_events && data.economic_events.length > 0) {
let etable = '<table><tr><th>Time</th><th>Event</th><th>Country</th><th>Impact</th></tr>';
data.economic_events.forEach(ev => {
let impactBadge = '';
if (ev.importance === 'High') impactBadge = '<span class="badge err">High</span>';
else if (ev.importance === 'Medium') impactBadge = '<span class="badge warn">Medium</span>';
else impactBadge = '<span class="badge ok">Low</span>';
etable += `<tr><td>${ev.time || '--:--'}</td><td>${ev.title}</td><td>${ev.country || ''}</td><td>${impactBadge}</td></tr>`;
});
etable += '</table>';
econDiv.innerHTML = etable;
} else {
econDiv.innerHTML = '<p>No economic events scheduled for today or tomorrow.</p>';
}
// Wiki entries
const wikiDiv = document.getElementById('wikiList');
document.getElementById('wikiTotal').textContent = data.wiki_entries ? data.wiki_entries.length : 0;
if (data.wiki_entries && data.wiki_entries.length > 0) {
let wt = '<table><tr><th>Time</th><th>Filename</th></tr>';
data.wiki_entries.forEach(e => {
wt += '<tr><td>' + e.time + '</td><td>' + e.filename + '</td></tr>';
});
wt += '</table>';
wikiDiv.innerHTML = wt;
} else {
wikiDiv.innerHTML = '<p>No wiki entries yet.</p>';
}
// Detailed report
const reportDiv = document.getElementById('detailedReport');
if (data.report && data.report !== 'No report yet.') {
const htmlReport = data.report.split('\\n').join('<br>');
reportDiv.innerHTML = htmlReport;
} else {
reportDiv.textContent = 'No report yet.';
}
document.getElementById('updateTime').textContent = new Date().toLocaleString();
document.getElementById('lastRefresh').textContent = '';
} catch (err) {
console.error('Dashboard refresh error:', err);
}
}
function updateBadge(id, statusText) {
const el = document.getElementById(id);
el.textContent = statusText;
el.className = 'badge';
if (statusText.includes('running')) el.classList.add('ok');
else if (statusText.includes('error') || statusText.includes('crash')) el.classList.add('err');
else el.classList.add('warn');
}
refreshData();
startAutoRefresh();
</script>
</body>
</html>
"""
return HTMLResponse(content=html)