| 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 |
|
|
| |
| |
| |
| 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") |
|
|
| |
| 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, |
| "LAST_PROCESSED_MIN": -1 |
| } |
|
|
| MARKET_DATA_CACHE = {} |
|
|
| |
| |
| |
|
|
| 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': |
| |
| open_pos = [p for p in res.get('data', []) if float(p['pos']) != 0] |
| return len(open_pos) |
| return 999 |
|
|
| 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 |
|
|
| |
| |
| |
|
|
| 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: |
| |
| current_pos_count = get_total_open_positions() |
| my_pos = get_position(symbol) |
| |
| |
| 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 |
|
|
| |
| 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}") |
|
|
| |
| |
| |
|
|
| 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) |
| 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) |
|
|