SendSlack / app.py
toanvv7's picture
Update app.py
6b6793e verified
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)