| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| @app.api_route("/health", methods=["GET", "HEAD"]) |
| def health(): |
| return {"status": "ok"} |
|
|
|
|
| |
| @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": |
| |
| 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}) |
|
|
|
|
| |
| @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 = 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_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), |
| } |
| ) |
|
|
| |
| 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 = 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_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_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() |
|
|
| |
| 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 = [] |
| 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_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, |
| } |
|
|
|
|
| |
| @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) |