import requests import pandas as pd import time from datetime import datetime, timezone from zoneinfo import ZoneInfo import os import threading import gradio as gr from dotenv import load_dotenv import hmac import hashlib import json import base64 import math # ============================================================================== # ========== CẤU HÌNH & BIẾN TOÀN CỤC ========== # ============================================================================== if os.path.exists(".env"): load_dotenv(".env") OKX_API_KEY = os.environ.get("OKX_API_KEY") OKX_SECRET_KEY = os.environ.get("OKX_SECRET_KEY") OKX_PASSPHRASE = os.environ.get("OKX_PASSPHRASE") OKX_BASE_URL = "https://www.okx.com" SLACK_WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL") # Danh mục coin (đã gộp gọn) BTC_ETH_GOLD = ["BTC-USDT-SWAP", "ETH-USDT-SWAP", "XAU-USDT"] ALTS_STANDARD = ["SOL-USDT-SWAP", "BNB-USDT-SWAP", "ADA-USDT-SWAP", "XRP-USDT-SWAP", "DOT-USDT-SWAP", "AVAX-USDT-SWAP", "LINK-USDT-SWAP", "NEAR-USDT-SWAP", "SUI-USDT-SWAP"] ALTS_HIGH_VOL = [ "OP-USDT-SWAP", "ARB-USDT-SWAP", "PEPE-USDT-SWAP", "DOGE-USDT-SWAP", "SHIB-USDT-SWAP", "FLOKI-USDT-SWAP", "BONK-USDT-SWAP", "WIF-USDT-SWAP", "ORDI-USDT-SWAP", "TIA-USDT-SWAP", "SEI-USDT-SWAP", "FET-USDT-SWAP", "RNDR-USDT-SWAP", "NOT-USDT-SWAP", "BOME-USDT-SWAP", "MEW-USDT-SWAP", "POPCAT-USDT-SWAP", "BRETT-USDT-SWAP", "PEOPLE-USDT-SWAP", "MEME-USDT-SWAP", "1000SATS-USDT-SWAP", "TRX-USDT-SWAP", "LTC-USDT-SWAP", "APT-USDT-SWAP", "ATOM-USDT-SWAP", "BCH-USDT-SWAP", "INJ-USDT-SWAP", "AAVE-USDT-SWAP", "UNI-USDT-SWAP", "SUSHI-USDT-SWAP", "CRV-USDT-SWAP", "MKR-USDT-SWAP", "RUNE-USDT-SWAP", "GALA-USDT-SWAP", "SAND-USDT-SWAP", "MANA-USDT-SWAP", "CHZ-USDT-SWAP", "FTM-USDT-SWAP", "ICP-USDT-SWAP", "HBAR-USDT-SWAP", "VET-USDT-SWAP", "FIL-USDT-SWAP", "ETC-USDT-SWAP", "PENDLE-USDT-SWAP", "ARKM-USDT-SWAP", "ENA-USDT-SWAP", "TAO-USDT-SWAP", "IO-USDT-SWAP", "ZRO-USDT-SWAP" ] TIMEFRAME = "5m" VIETNAM_TZ = ZoneInfo("Asia/Ho_Chi_Minh") CONFIG = { "RUNNING": False, "AMOUNT": 10.0, "LEVERAGE": 25, "WICK_MAIN": 0.1, "WICK_ALTS": 0.2, "WICK_NEW": 0.45, "WICK_SUB": 0.05, "RR_RATIO": 1.5, "SL_BUFFER_PCT": 0.15, "MAX_POSITIONS": 5, # GIỚI HẠN TỐI ĐA 5 LỆNH "LAST_PROCESSED_MIN": -1 } MARKET_DATA_CACHE = {} # ============================================================================== # ========== HÀM HỖ TRỢ API & UTILS ========== # ============================================================================== def okx_request(method, endpoint, body=None): try: ts = datetime.now(timezone.utc).isoformat(timespec='milliseconds').replace("+00:00", "Z") body_str = json.dumps(body) if body else "" message = ts + method + endpoint + body_str mac = hmac.new(bytes(OKX_SECRET_KEY, 'utf-8'), bytes(message, 'utf-8'), hashlib.sha256) sign = base64.b64encode(mac.digest()).decode() headers = { 'OK-ACCESS-KEY': OKX_API_KEY, 'OK-ACCESS-SIGN': sign, 'OK-ACCESS-TIMESTAMP': ts, 'OK-ACCESS-PASSPHRASE': OKX_PASSPHRASE, 'Content-Type': 'application/json' } res = requests.request(method, OKX_BASE_URL + endpoint, headers=headers, data=body_str, timeout=10) return res.json() except Exception as e: return {"code": "-1", "msg": str(e)} def get_total_open_positions(): """Đếm tổng số vị thế đang mở trên toàn tài khoản""" res = okx_request("GET", "/api/v5/account/positions") if res and res.get('code') == '0': # Lọc những vị thế có số lượng khác 0 open_pos = [p for p in res.get('data', []) if float(p['pos']) != 0] return len(open_pos) return 999 # Trả về số lớn nếu lỗi để chặn mở lệnh mới an toàn def get_position(symbol): res = okx_request("GET", f"/api/v5/account/positions?instId={symbol}") if res and res.get('code') == '0' and res.get('data'): for p in res['data']: if float(p['pos']) != 0: return p return None def close_position(symbol, posSide): body = {"instId": symbol, "mgnMode": "isolated", "posSide": posSide} return okx_request("POST", "/api/v5/trade/close-position", body) def get_market_rules(symbol): if symbol in MARKET_DATA_CACHE: return MARKET_DATA_CACHE[symbol] try: url = f"{OKX_BASE_URL}/api/v5/public/instruments?instType=SWAP&instId={symbol}" res = requests.get(url, timeout=10).json() if res.get('code') == '0' and res['data']: inst = res['data'][0] prec = len(inst['tickSz'].split('.')[-1]) if '.' in inst['tickSz'] else 0 lot_prec = len(inst['lotSz'].split('.')[-1]) if '.' in inst['lotSz'] else 0 data = {"lotSz": float(inst['lotSz']), "tickSz": float(inst['tickSz']), "prec": prec, "lotPrec": lot_prec, "ctVal": float(inst.get('ctVal', 1)), "minSz": float(inst['minSz'])} MARKET_DATA_CACHE[symbol] = data return data except: return None # ============================================================================== # ========== LOGIC QUÉT VÀ THỰC THI ========== # ============================================================================== def calculate_wick_pct(row): max_oc, min_oc = max(row['o'], row['c']), min(row['o'], row['c']) up_wick = ((row['h'] - max_oc) / max_oc) * 100 if max_oc > 0 else 0 lo_wick = ((min_oc - row['l']) / min_oc) * 100 if min_oc > 0 else 0 return up_wick, lo_wick, row['c'] > row['o'], row['c'] < row['o'] def run_market_scan(): X_MAIN, X_ALTS, X_NEW, Y = CONFIG["WICK_MAIN"], CONFIG["WICK_ALTS"], CONFIG["WICK_NEW"], CONFIG["WICK_SUB"] print(f"\n--- SCAN: {datetime.now(VIETNAM_TZ).strftime('%H:%M:%S')} ---") targets = [(s, X_MAIN) for s in BTC_ETH_GOLD] + [(s, X_ALTS) for s in ALTS_STANDARD] + [(s, X_NEW) for s in ALTS_HIGH_VOL] for i, (symbol, X_val) in enumerate(targets): try: if i > 0 and i % 15 == 0: time.sleep(0.4) url = f"{OKX_BASE_URL}/api/v5/market/history-candles?instId={symbol}&bar={TIMEFRAME}&limit=5" resp = requests.get(url, timeout=10).json() if not resp or not resp.get('data'): continue df = pd.DataFrame(resp['data'], columns=['ts','o','h','l','c','v','volCcy','volCcyQuote','confirm']) df[['o','h','l','c']] = df[['o','h','l','c']].astype(float) n0, n1 = df.iloc[1], df.iloc[2] up0, lo0, g0, r0 = calculate_wick_pct(n0) up1, lo1, g1, r1 = calculate_wick_pct(n1) l_pass = (lo0 >= X_val and up0 <= Y) and (lo1 >= X_val and up1 <= Y) and (g0 or g1) s_pass = (up0 >= X_val and lo0 <= Y) and (up1 >= X_val and lo1 <= Y) and (r0 or r1) if l_pass or s_pass: execute_trade(symbol, "long" if l_pass else "short", n0) except: continue def execute_trade(symbol, side, row_data): try: # 1. KIỂM TRA SỐ LƯỢNG LỆNH HIỆN TẠI current_pos_count = get_total_open_positions() my_pos = get_position(symbol) # Nếu đã đủ 5 lệnh VÀ cặp này chưa có vị thế nào -> CHẶN if current_pos_count >= CONFIG["MAX_POSITIONS"] and not my_pos: print(f" ⚠️ BLOCK: Đã đạt giới hạn {current_pos_count}/5 lệnh. Bỏ qua {symbol}") return rules = get_market_rules(symbol) if not rules: return entry = round(row_data['c'], rules['prec']) sl = round(row_data['l']*(1-CONFIG["SL_BUFFER_PCT"]/100), rules['prec']) if side=="long" else round(row_data['h']*(1+CONFIG["SL_BUFFER_PCT"]/100), rules['prec']) risk = abs(entry - sl) if risk == 0: return tp = round((entry + risk*CONFIG["RR_RATIO"]) if side=="long" else (entry - risk*CONFIG["RR_RATIO"]), rules['prec']) size = round(math.floor((CONFIG["AMOUNT"]*CONFIG["LEVERAGE"]/(entry*rules['ctVal']))/rules['lotSz'])*rules['lotSz'], rules['lotPrec']) if size < rules['minSz']: return # Logic đảo vị thế if my_pos and my_pos['posSide'] != side: close_position(symbol, my_pos['posSide']) time.sleep(1.0) body = { "instId": symbol, "tdMode": "isolated", "side": "buy" if side=="long" else "sell", "posSide": side, "ordType": "market", "sz": str(size), "attachAlgoOrds": [ {"attachAlgoOrdType": "sl", "slTriggerPx": str(sl), "slOrdPx": "-1"}, {"attachAlgoOrdType": "tp", "tpTriggerPx": str(tp), "tpOrdPx": "-1"} ] } res = okx_request("POST", "/api/v5/trade/order", body) if res.get('code') == '0': print(f" ✅ KHỚP {side.upper()} {symbol} (Slots: {get_total_open_positions()}/{CONFIG['MAX_POSITIONS']})") except Exception as e: print(f"❌ Error {symbol}: {e}") # ============================================================================== # ========== GRADIO UI ========== # ============================================================================== def update_ui(amt, lev, m_main, m_alt, m_new, sub, rr, buf, max_p, run): CONFIG.update({"AMOUNT": amt, "LEVERAGE": lev, "WICK_MAIN": m_main, "WICK_ALTS": m_alt, "WICK_NEW": m_new, "WICK_SUB": sub, "RR_RATIO": rr, "SL_BUFFER_PCT": buf, "MAX_POSITIONS": max_p, "RUNNING": run}) return f"Status: {'RUNNING' if run else 'STOPPED'} | Max Slots: {max_p} | {datetime.now(VIETNAM_TZ).strftime('%H:%M:%S')}" def main_loop(): while True: if CONFIG["RUNNING"]: now = datetime.now(VIETNAM_TZ) if now.minute % 5 == 0 and now.minute != CONFIG["LAST_PROCESSED_MIN"]: time.sleep(8) run_market_scan() CONFIG["LAST_PROCESSED_MIN"] = now.minute time.sleep(1) threading.Thread(target=main_loop, daemon=True).start() with gr.Blocks() as demo: gr.Markdown("# 🤖 OKX Bot V16.4 - Slot Control") with gr.Row(): n_amt = gr.Number(label="USDT/Lệnh", value=10) n_lev = gr.Number(label="Đòn bẩy", value=25) n_max = gr.Number(label="Tối đa số lệnh (Max Open)", value=5) # Ô nhập giới hạn lệnh with gr.Row(): n_main = gr.Number(label="Râu BTC", value=0.1) n_new = gr.Number(label="Râu Meme", value=0.45) n_rr = gr.Number(label="R:R", value=1.5) c_run = gr.Checkbox(label="KÍCH HOẠT") btn = gr.Button("CẬP NHẬT", variant="primary") out = gr.Textbox(label="Status") btn.click(update_ui, [n_amt, n_lev, n_main, gr.Number(value=0.2, visible=False), n_new, gr.Number(value=0.05, visible=False), n_rr, gr.Number(value=0.15, visible=False), n_max, c_run], out) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)