mlearning / app.py
daafa999's picture
Update app.py
e7cbe26 verified
Raw
History Blame Contribute Delete
12.8 kB
# -*- 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} <b>{title}</b>\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('<div class="footer">πŸ” Mode: SIMULATOR (Virtual Trading) | Data: Binance Public API</div>', 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")))