| |
| """ |
| 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 |
|
|
| |
| |
| |
| 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") |
|
|
| |
| |
| |
| 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) |
|
|
| |
| |
| |
| 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() |
|
|
| |
| |
| |
| 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)) |
|
|
| |
| |
| |
| 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} <b>{title}</b>\n{msg}\nβ° {datetime.now(timezone.utc).strftime('%H:%M UTC')}" |
|
|
| |
| |
| |
| 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 = 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 |
|
|
| |
| 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 |
|
|
| 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() |
|
|
| |
| |
| |
| 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('<div class="footer">π Mode: SIMULATOR (Virtual Trading) | Data: Binance Public API</div>', elem_classes="footer") |
|
|
| |
| 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]) |
| |
| |
| timer = gr.Timer(value=5) |
| timer.tick(ui_update, inputs=None, outputs=[bal, trades, pnl, pos_table, log_box]) |
| |
| |
| 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"))) |