# -*- coding: utf-8 -*- """ HUGGING FACE SPACE: BINANCE SPOT TRADING SIMULATOR ✅ Fixed: Gradio Timer Compatible | No API Key | Rule-Based """ import os, time, json, threading, logging, requests, pandas as pd from datetime import datetime, timezone from pathlib import Path import gradio as gr # ========================================== # KONFIGURASI # ========================================== CONFIG = { "WATCHLIST": ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "LINKUSDT"], "INTERVAL": "1h", "RISK_PCT": 0.01, "MAX_DAILY_TRADES": 2, "DAILY_LOSS_LIMIT": -0.03, "DAILY_PROFIT_LOCK": 0.05, "AI_THRESHOLD": 70, "TP1": 0.03, "TP1_SHARE": 0.3, "TP2": 0.05, "TP2_SHARE": 0.3, "TP3_SHARE": 0.4, "TRAIL_PCT": 0.02, "TELEGRAM_TOKEN": os.getenv("TELEGRAM_TOKEN", ""), "TELEGRAM_CHAT_ID": os.getenv("TELEGRAM_CHAT_ID", "") } STATE_FILE = "sim_state.json" logging.basicConfig(level=logging.INFO, format="%(message)s") # ========================================== # STATE MANAGEMENT # ========================================== def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE) as f: return json.load(f) return { "date": datetime.now(timezone.utc).strftime("%Y-%m-%d"), "balance": 1000.0, "trades": 0, "daily_pnl": 0.0, "positions": [], "logs": [] } def save_state(s): with open(STATE_FILE, "w") as f: json.dump(s, f, indent=2) # ========================================== # PUBLIC BINANCE FETCHER (NO API KEY) # ========================================== def fetch_klines(sym, interval="1h", limit=100): try: r = requests.get("https://api.binance.com/api/v3/klines", params={"symbol": sym, "interval": interval, "limit": limit}, timeout=10) r.raise_for_status() d = r.json() df = pd.DataFrame(d, columns=["ts","o","h","l","c","v"] + ["_"]*6) df["ts"] = pd.to_datetime(df["ts"], unit="ms") for c in "o h l c v".split(): df[c] = df[c].astype(float) return df[["ts","o","h","l","c","v"]] except Exception as e: return pd.DataFrame() # ========================================== # INDIKATOR & AI SCORE # ========================================== def add_ema(df, periods=[20, 50]): for p in periods: df[f"ema_{p}"] = df["c"].ewm(span=p, adjust=False).mean() return df def calc_ai_score(df): if len(df) < 50: return 0 trend = 30 if df["ema_20"].iloc[-1] > df["ema_50"].iloc[-1] else 0 vol_ma = df["v"].rolling(20).mean().iloc[-1] vol_ratio = df["v"].iloc[-1] / max(vol_ma, 1e-9) vol = min(vol_ratio * 10, 40) if vol_ratio > 1 else 0 mom = min((df["c"].iloc[-1] / df["c"].iloc[-5] - 1) * 1000, 30) return max(0, min(100, trend + vol + mom)) # ========================================== # TELEGRAM ALERT # ========================================== def tg_send(txt): if not CONFIG["TELEGRAM_TOKEN"] or not CONFIG["TELEGRAM_CHAT_ID"]: return try: requests.post(f"https://api.telegram.org/bot{CONFIG['TELEGRAM_TOKEN']}/sendMessage", json={"chat_id": CONFIG["TELEGRAM_CHAT_ID"], "text": txt, "parse_mode": "HTML"}, timeout=5) except: pass def tg_fmt(emoji, title, msg): return f"{emoji} {title}\n{msg}\n⏰ {datetime.now(timezone.utc).strftime('%H:%M UTC')}" # ========================================== # SIMULATION ENGINE # ========================================== class SimEngine: def __init__(self): self.state = load_state() self._lock = threading.Lock() self._running = False self._stop = threading.Event() self._thread = None def start(self): if self._running: return "⚠️ Sudah berjalan" self._running = True self._stop.clear() self._thread = threading.Thread(target=self._loop, daemon=True) self._thread.start() return "✅ Simulator Started" def stop(self): self._stop.set() self._running = False return "⏹ Simulator Stopped" def _reset_daily(self): today = datetime.now(timezone.utc).strftime("%Y-%m-%d") if self.state["date"] != today: with self._lock: self.state.update({"date": today, "trades": 0, "daily_pnl": 0.0, "positions": []}) save_state(self.state) def _can_trade(self): with self._lock: if self.state["trades"] >= CONFIG["MAX_DAILY_TRADES"]: return False, "Max 2 trade/hari" if self.state["daily_pnl"] <= CONFIG["DAILY_LOSS_LIMIT"]: return False, "Daily Loss Limit -3%" if self.state["daily_pnl"] >= CONFIG["DAILY_PROFIT_LOCK"]: return False, "Profit Lock +5%" return True, "OK" def _check_positions(self): with self._lock: for p in self.state["positions"][:]: sym = p["symbol"] df = fetch_klines(sym, CONFIG["INTERVAL"], limit=1) if df.empty: continue price = df["c"].iloc[-1] pnl = (price - p["entry"]) / p["entry"] if price > p["trail_h"]: p["trail_h"] = price if pnl >= CONFIG["TP1"] and not p["tp1"]: p["tp1"] = True self._add_log(f"💰 TP1 {sym} +3% (30% closed)") tg_send(tg_fmt("💰", f"TP1 {sym}", f"+3% | 30% closed | Virtual")) if pnl >= CONFIG["TP2"] and not p["tp2"]: p["tp2"] = True self._add_log(f"💰 TP2 {sym} +5% (30% closed)") tg_send(tg_fmt("💰", f"TP2 {sym}", f"+5% | 30% closed | Virtual")) trail_sl = p["trail_h"] * (1 - CONFIG["TRAIL_PCT"]) if price <= max(trail_sl, p["sl"]): close_share = CONFIG["TP3_SHARE"] if p["tp2"] else 1.0 pnl_real = pnl * close_share self.state["daily_pnl"] += pnl_real self.state["balance"] *= (1 + pnl_real) self._add_log(f"📉 EXIT {sym} | PnL: {pnl:.2%} | Balance: ${self.state['balance']:.2f}") tg_send(tg_fmt("📉", f"EXIT {sym}", f"PnL: {pnl:.2%} | Virtual")) self.state["positions"].remove(p) save_state(self.state) elif price <= p["sl"]: pnl_real = pnl self.state["daily_pnl"] += pnl_real self.state["balance"] *= (1 + pnl_real) self._add_log(f"⛔ SL {sym} -2% | Balance: ${self.state['balance']:.2f}") tg_send(tg_fmt("⛔", f"SL {sym}", f"Loss: -2% | Virtual")) self.state["positions"].remove(p) save_state(self.state) def _loop(self): tg_send(tg_fmt("🚀", "System Started", "Simulator Mode (No API Key)")) while not self._stop.is_set(): try: self._reset_daily() ok, reason = self._can_trade() if not ok: time.sleep(60) continue # BTC Filter btc = add_ema(fetch_klines("BTCUSDT", CONFIG["INTERVAL"])) if btc.empty or btc["ema_20"].iloc[-1] <= btc["ema_50"].iloc[-1]: time.sleep(60) continue # Scan Watchlist for sym in CONFIG["WATCHLIST"]: if sym == "BTCUSDT" or self._stop.is_set(): continue df = add_ema(fetch_klines(sym, CONFIG["INTERVAL"])) if df.empty: continue score = calc_ai_score(df) vol_ma = df["v"].rolling(20).mean().iloc[-1] vol_r = df["v"].iloc[-1] / max(vol_ma, 1e-9) if not (df["ema_20"].iloc[-1] > df["ema_50"].iloc[-1] and vol_r > 1.2 and score > CONFIG["AI_THRESHOLD"]): continue price = df["c"].iloc[-1] sl = price * 0.98 risk_usd = self.state["balance"] * CONFIG["RISK_PCT"] qty = (risk_usd / (price - sl)) / price if price > sl else 0 with self._lock: self.state["positions"].append({ "symbol": sym, "entry": price, "qty": qty, "sl": sl, "tp1": False, "tp2": False, "trail_h": price }) self.state["trades"] += 1 save_state(self.state) self._add_log(f"📥 ENTRY {sym} @ {price:.2f} | Risk: 1% | SL: {sl:.2f}") tg_send(tg_fmt("📥", f"ENTRY {sym}", f"Price: {price:.2f} | Qty: {qty:.4f} | Virtual")) break # Anti overtrading self._check_positions() time.sleep(300) except Exception as e: self._add_log(f"⚠️ {str(e)}") time.sleep(60) self._add_log("🛑 System Stopped") def _add_log(self, msg): ts = datetime.now().strftime("%H:%M:%S") with self._lock: self.state["logs"].append(f"[{ts}] {msg}") if len(self.state["logs"]) > 150: self.state["logs"] = self.state["logs"][-50:] def get_state(self): with self._lock: return json.loads(json.dumps(self.state)) engine = SimEngine() # ========================================== # GRADIO DASHBOARD (FIXED) # ========================================== def ui_update(): s = engine.get_state() logs = "\n".join(s["logs"][-20:]) pos = [[p["symbol"], f"${p['entry']:.2f}", f"{p['qty']:.4f}", f"${p['sl']:.2f}", "WAIT" if not p["tp1"] else "TP1" if not p["tp2"] else "TP2"] for p in s["positions"]] return (f"Balance: ${s['balance']:.2f}", f"Trades: {s['trades']}/{CONFIG['MAX_DAILY_TRADES']}", f"Daily PnL: {s['daily_pnl']:.2%}", pos, logs) def refresh_logs(): """Fungsi khusus untuk timer refresh log saja""" s = engine.get_state() return "\n".join(s["logs"][-20:]) with gr.Blocks(title="Binance Spot Simulator", css=".footer {text-align: center; margin-top: 20px;}") as demo: gr.Markdown("# 📊 System Trading + Profit Management (SIMULATOR)") gr.Markdown("*✅ Tanpa API Key | ✅ Data Publik Binance | ✅ Semua Rule Aktif | ✅ Telegram Ready*") with gr.Row(): btn_start = gr.Button("▶ START SIMULATOR", variant="primary", scale=1) btn_stop = gr.Button("⏹ STOP", variant="stop", scale=1) btn_reset = gr.Button("🔄 RESET STATE", variant="secondary", scale=1) with gr.Row(): bal = gr.Textbox(label="💰 Balance", value="Balance: $1000.00", interactive=False) trades = gr.Textbox(label="📈 Trades Hari Ini", value="Trades: 0/2", interactive=False) pnl = gr.Textbox(label="📊 Daily PnL", value="Daily PnL: 0.00%", interactive=False) pos_table = gr.Dataframe( headers=["Pair", "Entry", "Qty", "SL", "Status"], value=[], interactive=False, label="📍 Open Positions", wrap=True ) log_box = gr.Textbox(label="📝 System Log", value="", lines=10, interactive=False, max_lines=20) gr.Markdown('', elem_classes="footer") # Event handlers btn_start.click(lambda: engine.start(), inputs=None, outputs=None) btn_stop.click(lambda: engine.stop(), inputs=None, outputs=None) def do_reset(): engine.state = { "date": datetime.now(timezone.utc).strftime("%Y-%m-%d"), "balance": 1000.0, "trades": 0, "daily_pnl": 0.0, "positions": [], "logs": ["🔄 State di-reset"] } save_state(engine.state) return "Balance: $1000.00", "Trades: 0/2", "Daily PnL: 0.00%", [], "🔄 State di-reset" btn_reset.click(do_reset, inputs=None, outputs=[bal, trades, pnl, pos_table, log_box]) # ✅ FIXED: Gunakan gr.Timer untuk auto-refresh (Gradio 4.x compatible) timer = gr.Timer(value=5) # Refresh setiap 5 detik timer.tick(ui_update, inputs=None, outputs=[bal, trades, pnl, pos_table, log_box]) # Refresh log lebih sering agar real-time log_timer = gr.Timer(value=2) log_timer.tick(refresh_logs, inputs=None, outputs=log_box) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))