CoolFace
Apppublic

lignarr/Trainer-PPO

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py464 linesDownload Raw Back to root
1"""2app.py — Enhanced Telegram Command Center for PPO Training Space.3Full control: train, backtest, validate, model management, logs, system info.4"""5 6import os, subprocess, threading, collections, shutil, json, glob, time7from datetime import datetime8from flask import Flask, request as flask_request, jsonify9 10app = Flask(__name__)11 12SPACE_NAME = os.environ.get("SPACE_NAME", "PPO Trainer")13current_process = None14process_mode = None15process_start_time = None16log_buffer = collections.deque(maxlen=300)17 18# ═══════════════════════════════════════════════════════════19# MENUS — Deep, multi-level Telegram control20# ═══════════════════════════════════════════════════════════21def menu_main():22    return {"inline_keyboard": [23        [{"text": "🚀 Pipelines", "callback_data": "menu_pipes"}],24        [{"text": "📊 Monitoring", "callback_data": "menu_mon"}],25        [{"text": "📦 Models", "callback_data": "menu_models"}],26        [{"text": "⚙️ Controls", "callback_data": "menu_ctrl"}],27        [{"text": "🧹 Maintenance", "callback_data": "menu_maint"}],28    ]}29 30def menu_pipes():31    return {"inline_keyboard": [32        [{"text": "🔄 Full Pipeline (Train→Backtest→Validate)", "callback_data": "run_full"}],33        [{"text": "📥 Extract Data", "callback_data": "run_extract"}],34        [{"text": "🧠 Train Only", "callback_data": "run_train"}],35        [{"text": "📊 Backtest Only", "callback_data": "run_backtest"}],36        [{"text": "✅ Validate Only", "callback_data": "run_validate"}],37        [{"text": "⬅️ Back", "callback_data": "menu_main"}],38    ]}39 40def menu_mon():41    return {"inline_keyboard": [42        [{"text": "📋 Logs (last 30)", "callback_data": "mon_logs_30"}],43        [{"text": "📋 Logs (last 100)", "callback_data": "mon_logs_100"}],44        [{"text": "📈 Training Metrics", "callback_data": "mon_metrics"}],45        [{"text": "💾 Disk & Files", "callback_data": "mon_disk"}],46        [{"text": "🖥️ System Info", "callback_data": "mon_system"}],47        [{"text": "📊 Backtest Results", "callback_data": "mon_backtest"}],48        [{"text": "⬅️ Back", "callback_data": "menu_main"}],49    ]}50 51def menu_models():52    return {"inline_keyboard": [53        [{"text": "📦 List All Models", "callback_data": "mdl_list"}],54        [{"text": "🏆 Best Model Info", "callback_data": "mdl_best"}],55        [{"text": "✅ Deployment-Ready?", "callback_data": "mdl_deploy_status"}],56        [{"text": "🗑️ Clear Checkpoints", "callback_data": "mdl_clear_ckpt"}],57        [{"text": "🗑️ Clear ALL Models", "callback_data": "mdl_clear_all"}],58        [{"text": "⬅️ Back", "callback_data": "menu_main"}],59    ]}60 61def menu_ctrl():62    return {"inline_keyboard": [63        [{"text": "ℹ️ Detailed Status", "callback_data": "ctrl_status"}],64        [{"text": "🛑 Stop Process", "callback_data": "ctrl_stop"}],65        [{"text": "🔁 Restart Space", "callback_data": "ctrl_restart"}],66        [{"text": "⬅️ Back", "callback_data": "menu_main"}],67    ]}68 69def menu_maint():70    return {"inline_keyboard": [71        [{"text": "🗑️ Clear Logs", "callback_data": "maint_clear_logs"}],72        [{"text": "🗑️ Clear Data Cache", "callback_data": "maint_clear_data"}],73        [{"text": "🗑️ Clear Results", "callback_data": "maint_clear_results"}],74        [{"text": "🔧 Re-extract Features", "callback_data": "run_features"}],75        [{"text": "📏 Check Data Integrity", "callback_data": "maint_check_data"}],76        [{"text": "⬅️ Back", "callback_data": "menu_main"}],77    ]}78 79# ═══════════════════════════════════════════════════════════80# PROCESS MANAGEMENT81# ═══════════════════════════════════════════════════════════82def run_pipeline(mode):83    global current_process, process_mode, process_start_time84    if current_process is not None and current_process.poll() is None:85        return False86    log_buffer.clear()87    process_mode = mode88    process_start_time = datetime.now()89 90    # Map mode to the right command91    cmd_map = {92        "full":      ["python", "-u", "train_and_validate.py", "--mode", "full"],93        "train":     ["python", "-u", "train_and_validate.py", "--mode", "train"],94        "backtest":  ["python", "-u", "train_and_validate.py", "--mode", "backtest"],95        "validate":  ["python", "-u", "train_and_validate.py", "--mode", "validate"],96        "extract":   ["python", "-u", "main.py", "--mode", "extract"],97        "features":  ["python", "-u", "main.py", "--mode", "features"],98    }99    cmd = cmd_map.get(mode, ["python", "-u", "main.py", "--mode", mode])100 101    def worker():102        global current_process103        try:104            current_process = subprocess.Popen(105                cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,106                text=True, bufsize=1)107            for line in current_process.stdout:108                log_buffer.append(line.rstrip())109            current_process.wait()110            log_buffer.append(f"--- {mode} exited: {current_process.returncode} ---")111        except Exception as e:112            log_buffer.append(f"ERROR: {e}")113    threading.Thread(target=worker, daemon=True).start()114    return True115 116# ═══════════════════════════════════════════════════════════117# MONITORING HELPERS118# ═══════════════════════════════════════════════════════════119def get_logs(n=30):120    lines = list(log_buffer)[-n:]121    if not lines:122        return "<i>No logs yet.</i>"123    return "\n".join(l.replace("<", "&lt;").replace(">", "&gt;")[:120] for l in lines)124 125def get_training_metrics():126    log_dir = "logs"127    if not os.path.isdir(log_dir):128        return "<i>No log directory.</i>"129    logs = sorted(glob.glob(os.path.join(log_dir, "*.log")), key=os.path.getmtime, reverse=True)130    if not logs:131        return "<i>No log files found.</i>"132    try:133        with open(logs[0], "r", errors="ignore") as f:134            lines = f.readlines()[-50:]135        # Find key metrics from recent lines136        info = {"balance": "?", "win_rate": "?", "profit_factor": "?", "trades": "?", "timesteps": "?"}137        for line in reversed(lines):138            l = line.lower()139            for key in info:140                if key in l and info[key] == "?":141                    parts = line.split("|") if "|" in line else [line]142                    for p in parts:143                        if key in p.lower():144                            val = p.split(":")[-1].strip() if ":" in p else p.strip()145                            info[key] = val[:30]146                            break147        return (f"📈 <b>Training Metrics</b>\n<i>{os.path.basename(logs[0])}</i>\n\n"148                f"💰 Balance: <b>{info['balance']}</b>\n"149                f"🎯 Win Rate: <b>{info['win_rate']}</b>\n"150                f"📊 PF: <b>{info['profit_factor']}</b>\n"151                f"📊 Trades: <b>{info['trades']}</b>\n"152                f"⏱ Steps: <b>{info['timesteps']}</b>")153    except Exception as e:154        return f"<i>Error: {e}</i>"155 156def get_backtest_results():157    path = os.path.join("results", "backtest_results.json")158    if not os.path.exists(path):159        return "<i>No backtest results yet. Run a backtest first.</i>"160    try:161        with open(path) as f:162            data = json.load(f)163        m = data.get("metrics", {})164        return (f"📊 <b>Last Backtest Results</b>\n\n"165                f"💰 Final Balance: <b>${m.get('Final Balance', '?')}</b>\n"166                f"🎯 Win Rate: <b>{m.get('Win Rate (%)', '?')}%</b>\n"167                f"📊 Profit Factor: <b>{m.get('Profit Factor', '?')}</b>\n"168                f"📉 Max Drawdown: <b>{m.get('Max Drawdown (%)', '?')}%</b>\n"169                f"📈 Sharpe: <b>{m.get('Annualized Sharpe', m.get('Sharpe Ratio', '?'))}</b>\n"170                f"📊 Total Trades: <b>{m.get('Total Trades', '?')}</b>\n"171                f"💵 Expectancy: <b>${m.get('Expectancy', '?')}</b>")172    except Exception as e:173        return f"<i>Error reading results: {e}</i>"174 175def get_disk_info():176    total, used, free = shutil.disk_usage("/")177    text = (f"💾 <b>Disk</b>\nTotal: {total//(1024**3)}GB | "178            f"Used: {used//(1024**3)}GB | Free: {free//(1024**3)}GB\n\n📁 <b>Key Files:</b>\n")179    checks = [("data/eurusd_features.csv", "Features"), ("data/eurusd_master_dataset.csv", "Master Data"),180              ("data/norm_params.pkl", "Norm Params"), ("models/best_model.zip", "Best Model"),181              ("models/deployment_ready_model.zip", "Deploy-Ready Model"),182              ("results/backtest_results.json", "Backtest Results")]183    for path, label in checks:184        if os.path.exists(path):185            sz = os.path.getsize(path) / (1024*1024)186            text += f"  ✅ {label}: <b>{sz:.1f}MB</b>\n"187        else:188            text += f"  ❌ {label}: <i>missing</i>\n"189    return text190 191def get_system_info():192    import sys193    lines = [f"🖥️ <b>System Info</b>", f"CPUs: <b>{os.cpu_count()}</b>",194             f"Python: <b>{sys.version.split()[0]}</b>",195             f"Time: <b>{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</b>"]196    try:197        with open("/proc/meminfo") as f:198            for line in f:199                if line.startswith("MemTotal:"):200                    lines.append(f"RAM Total: <b>{int(line.split()[1])//1024}MB</b>")201                elif line.startswith("MemAvailable:"):202                    lines.append(f"RAM Free: <b>{int(line.split()[1])//1024}MB</b>")203    except:204        pass205    try:206        load1, load5, load15 = os.getloadavg()207        lines.append(f"Load: <b>{load1:.2f} / {load5:.2f} / {load15:.2f}</b>")208    except:209        pass210    return "\n".join(lines)211 212def list_models():213    model_dir = "models"214    if not os.path.isdir(model_dir):215        return "<i>No models directory.</i>"216    files = sorted(os.listdir(model_dir))217    if not files:218        return "<i>No models found.</i>"219    text = "📦 <b>Models</b>\n\n"220    for f in files:221        fp = os.path.join(model_dir, f)222        sz = os.path.getsize(fp) / (1024*1024)223        icon = "🏆" if "best" in f else ("✅" if "deploy" in f else "📄")224        text += f"  {icon} {f} ({sz:.1f}MB)\n"225    return text226 227def get_best_model_info():228    path = os.path.join("models", "best_model.zip")229    if not os.path.exists(path):230        return "<i>No best model yet.</i>"231    sz = os.path.getsize(path) / (1024*1024)232    mtime = datetime.fromtimestamp(os.path.getmtime(path))233    return (f"🏆 <b>Best Model</b>\n\n"234            f"Size: <b>{sz:.1f}MB</b>\n"235            f"Modified: <b>{mtime.strftime('%Y-%m-%d %H:%M:%S')}</b>")236 237def check_deploy_ready():238    path = os.path.join("models", "deployment_ready_model.zip")239    if os.path.exists(path):240        sz = os.path.getsize(path) / (1024*1024)241        mtime = datetime.fromtimestamp(os.path.getmtime(path))242        return (f"✅ <b>Deployment-Ready Model EXISTS</b>\n\n"243                f"Size: <b>{sz:.1f}MB</b>\n"244                f"Validated: <b>{mtime.strftime('%Y-%m-%d %H:%M')}</b>\n\n"245                "This model passed all validation gates.")246    return "❌ <b>No deployment-ready model.</b>\n\nRun the full pipeline to generate one."247 248def check_data_integrity():249    features = "data/eurusd_features.csv"250    if not os.path.exists(features):251        return "❌ <b>Features CSV missing!</b> Run data extraction."252    try:253        import pandas as pd254        df = pd.read_csv(features, nrows=5)255        total = sum(1 for _ in open(features)) - 1256        return (f"✅ <b>Data Integrity Check</b>\n\n"257                f"Rows: <b>{total:,}</b>\n"258                f"Columns: <b>{len(df.columns)}</b>\n"259                f"Features: <b>{', '.join(df.columns[:8].tolist())}...</b>")260    except Exception as e:261        return f"⚠️ <b>Data issue:</b> {e}"262 263# ═══════════════════════════════════════════════════════════264# WEBHOOK HANDLER265# ═══════════════════════════════════════════════════════════266@app.route("/webhook", methods=["POST"])267def webhook():268    data = flask_request.get_json(silent=True) or {}269 270    msg = data.get("message", {})271    if msg:272        chat_id = msg.get("chat", {}).get("id")273        text = (msg.get("text") or "").strip()274        if text.startswith("/start"):275            return jsonify({"method": "sendMessage", "chat_id": chat_id,276                "text": (f"🤖 <b>{SPACE_NAME} — Command Center</b>\n\n"277                         f"📡 Online | 🕐 {datetime.now().strftime('%H:%M:%S')}\n\n"278                         "Full control over training, backtesting, models, and system."),279                "parse_mode": "HTML", "reply_markup": menu_main()})280        elif text.startswith("/logs"):281            return jsonify({"method": "sendMessage", "chat_id": chat_id,282                "text": f"📋 <b>Logs</b>\n\n<pre>{get_logs(30)}</pre>",283                "parse_mode": "HTML", "reply_markup": menu_mon()})284        elif text.startswith("/status"):285            running = current_process is not None and current_process.poll() is None286            s = f"🟢 <b>Running:</b> {process_mode}" if running else "🔴 <b>Idle</b>"287            return jsonify({"method": "sendMessage", "chat_id": chat_id,288                "text": s, "parse_mode": "HTML", "reply_markup": menu_ctrl()})289        elif text.startswith("/models"):290            return jsonify({"method": "sendMessage", "chat_id": chat_id,291                "text": list_models(), "parse_mode": "HTML", "reply_markup": menu_models()})292        elif text.startswith("/train"):293            ok = run_pipeline("full")294            s = "🚀 <b>Full Pipeline Started!</b>" if ok else "⚠️ Already running!"295            return jsonify({"method": "sendMessage", "chat_id": chat_id,296                "text": s, "parse_mode": "HTML", "reply_markup": menu_pipes()})297        elif text.startswith("/backtest"):298            return jsonify({"method": "sendMessage", "chat_id": chat_id,299                "text": get_backtest_results(), "parse_mode": "HTML", "reply_markup": menu_mon()})300        return jsonify({"method": "sendMessage", "chat_id": chat_id,301            "text": "Commands: /start /logs /status /models /train /backtest",302            "parse_mode": "HTML"})303 304    cb = data.get("callback_query", {})305    if cb:306        chat_id = cb["message"]["chat"]["id"]307        msg_id = cb["message"]["message_id"]308        choice = cb.get("data", "")309 310        resp_text, resp_menu = "", menu_main()311 312        # Navigation313        if choice == "menu_main":314            resp_text = f"🤖 <b>{SPACE_NAME}</b>\n🕐 {datetime.now().strftime('%H:%M:%S')}"315            resp_menu = menu_main()316        elif choice == "menu_pipes":317            resp_text = "🚀 <b>Pipeline Control</b>\nSelect a pipeline to run:"318            resp_menu = menu_pipes()319        elif choice == "menu_mon":320            resp_text = "📊 <b>Monitoring</b>"321            resp_menu = menu_mon()322        elif choice == "menu_models":323            resp_text = "📦 <b>Model Management</b>"324            resp_menu = menu_models()325        elif choice == "menu_ctrl":326            resp_text = "⚙️ <b>Controls</b>"327            resp_menu = menu_ctrl()328        elif choice == "menu_maint":329            resp_text = "🧹 <b>Maintenance</b>"330            resp_menu = menu_maint()331 332        # Pipelines333        elif choice in ("run_full", "run_train", "run_backtest", "run_validate", "run_extract", "run_features"):334            mode = choice.replace("run_", "")335            ok = run_pipeline(mode)336            labels = {"full": "Full Pipeline", "train": "Training", "backtest": "Backtest",337                      "validate": "Validation", "extract": "Data Extraction", "features": "Feature Engineering"}338            resp_text = f"🚀 <b>{labels.get(mode, mode)} Started!</b>" if ok else "⚠️ <b>Already running!</b>"339            resp_menu = menu_pipes()340 341        # Monitoring342        elif choice == "mon_logs_30":343            resp_text = f"📋 <b>Logs (last 30)</b>\n\n<pre>{get_logs(30)}</pre>"344            resp_menu = menu_mon()345        elif choice == "mon_logs_100":346            resp_text = f"📋 <b>Logs (last 100)</b>\n\n<pre>{get_logs(100)}</pre>"347            resp_menu = menu_mon()348        elif choice == "mon_metrics":349            resp_text = get_training_metrics()350            resp_menu = menu_mon()351        elif choice == "mon_disk":352            resp_text = get_disk_info()353            resp_menu = menu_mon()354        elif choice == "mon_system":355            resp_text = get_system_info()356            resp_menu = menu_mon()357        elif choice == "mon_backtest":358            resp_text = get_backtest_results()359            resp_menu = menu_mon()360 361        # Models362        elif choice == "mdl_list":363            resp_text = list_models()364            resp_menu = menu_models()365        elif choice == "mdl_best":366            resp_text = get_best_model_info()367            resp_menu = menu_models()368        elif choice == "mdl_deploy_status":369            resp_text = check_deploy_ready()370            resp_menu = menu_models()371        elif choice == "mdl_clear_ckpt":372            ckpt_dir = "checkpoints"373            if os.path.isdir(ckpt_dir):374                count = len([f for f in os.listdir(ckpt_dir) if f.endswith(".zip")])375                for f in os.listdir(ckpt_dir):376                    if f.endswith(".zip"):377                        os.remove(os.path.join(ckpt_dir, f))378                resp_text = f"🗑️ <b>Cleared {count} checkpoints.</b>"379            else:380                resp_text = "ℹ️ No checkpoints to clear."381            resp_menu = menu_models()382        elif choice == "mdl_clear_all":383            model_dir = "models"384            if os.path.isdir(model_dir):385                count = len(os.listdir(model_dir))386                shutil.rmtree(model_dir)387                os.makedirs(model_dir, exist_ok=True)388                resp_text = f"🗑️ <b>Cleared {count} model files.</b>"389            else:390                resp_text = "ℹ️ No models to clear."391            resp_menu = menu_models()392 393        # Controls394        elif choice == "ctrl_status":395            running = current_process is not None and current_process.poll() is None396            if running:397                elapsed = datetime.now() - process_start_time398                recent = list(log_buffer)[-5:]399                recent_text = "\n".join(l.replace("<", "&lt;").replace(">", "&gt;")[:100] for l in recent)400                resp_text = (f"🟢 <b>RUNNING: {process_mode}</b>\n"401                             f"Elapsed: <b>{elapsed}</b>\n"402                             f"PID: <b>{current_process.pid}</b>\n"403                             f"Log lines: <b>{len(log_buffer)}</b>\n\n"404                             f"<pre>{recent_text}</pre>")405            else:406                code = current_process.returncode if current_process else "N/A"407                resp_text = f"🔴 <b>Idle</b>\nLast: {process_mode or 'none'} (exit: {code})"408            resp_menu = menu_ctrl()409        elif choice == "ctrl_stop":410            if current_process and current_process.poll() is None:411                current_process.terminate()412                resp_text = "🛑 <b>Process terminated.</b>"413            else:414                resp_text = "ℹ️ Nothing running."415            resp_menu = menu_ctrl()416        elif choice == "ctrl_restart":417            resp_text = "🔁 <b>Use HF Space Settings to restart.</b>\nOr stop+start a pipeline."418            resp_menu = menu_ctrl()419 420        # Maintenance421        elif choice == "maint_clear_logs":422            log_buffer.clear()423            log_dir = "logs"424            if os.path.isdir(log_dir):425                for f in os.listdir(log_dir):426                    os.remove(os.path.join(log_dir, f))427            resp_text = "🗑️ <b>All logs cleared.</b>"428            resp_menu = menu_maint()429        elif choice == "maint_clear_data":430            data_dir = "data"431            if os.path.isdir(data_dir):432                for f in os.listdir(data_dir):433                    if f.endswith((".csv", ".pkl")):434                        os.remove(os.path.join(data_dir, f))435            resp_text = "🗑️ <b>Data cache cleared.</b> Re-extract to rebuild."436            resp_menu = menu_maint()437        elif choice == "maint_clear_results":438            res_dir = "results"439            if os.path.isdir(res_dir):440                shutil.rmtree(res_dir)441                os.makedirs(res_dir, exist_ok=True)442            resp_text = "🗑️ <b>Results cleared.</b>"443            resp_menu = menu_maint()444        elif choice == "maint_check_data":445            resp_text = check_data_integrity()446            resp_menu = menu_maint()447 448        if len(resp_text) > 4000:449            resp_text = resp_text[:3990] + "\n<i>...truncated</i>"450 451        return jsonify({"method": "editMessageText", "chat_id": chat_id,452            "message_id": msg_id, "text": resp_text, "parse_mode": "HTML",453            "reply_markup": resp_menu})454 455    return "ok", 200456 457@app.route("/")458def health():459    return f"{SPACE_NAME} Online", 200460 461if __name__ == "__main__":462    print(f"=== {SPACE_NAME} starting on port 7860 ===")463    app.run(host="0.0.0.0", port=7860)464