| 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 |
| from decimal import Decimal, ROUND_DOWN |
| 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") |
|
|
| VIETNAM_TZ = ZoneInfo("Asia/Ho_Chi_Minh") |
| |
| SYMBOLS = ["BTC-USDT-SWAP", "XAU-USDT-SWAP"] |
|
|
| CONFIG = { |
| "RUNNING": False, |
| "BTC": { |
| "amount": 10.0, "lev": 25, "body_pct": 0.03, |
| "long_l_wick": 0.13, "long_u_wick": 0.03, |
| "short_u_wick": 0.13, "short_l_wick": 0.03 |
| }, |
| "XAU": { |
| "amount": 10.0, "lev": 25, "body_pct": 0.03, |
| "long_l_wick": 0.13, "long_u_wick": 0.03, |
| "short_u_wick": 0.13, "short_l_wick": 0.03 |
| }, |
| "SL_BUFFER": 0.13, |
| "RR": 3.0, |
| "LAST_PROCESSED_MIN": -1 |
| } |
|
|
| |
| |
| |
|
|
| def send_slack_msg(msg): |
| """Gửi thông báo đến Slack qua Webhook.""" |
| print(f">>> SLACK LOG: {msg}") |
| if SLACK_WEBHOOK_URL: |
| try: |
| payload = {"text": f"🔔 *TÍN HIỆU CHIẾN THUẬT RÂU*: {msg}"} |
| requests.post(SLACK_WEBHOOK_URL, json=payload, timeout=10) |
| except Exception as e: |
| print(f"Lỗi gửi Slack: {e}") |
|
|
| def print_log_table(data_list): |
| header = f"{'SYMBOL':<12} | {'SIDE':<6} | {'BODY%':<8} | {'U-WICK%':<8} | {'L-WICK%':<8} | {'ACTION':<10}" |
| separator = "-" * len(header) |
| print(f"\n🕒 QUÉT LÚC: {datetime.now(VIETNAM_TZ).strftime('%Y-%m-%d %H:%M:%S')}") |
| print(separator) |
| print(header) |
| print(separator) |
| for d in data_list: |
| print(f"{d['sym']:<12} | {d['side']:<6} | {d['body']:<7.3f}% | {d['u_wick']:<7.3f}% | {d['l_wick']:<7.3f}% | {d['act']:<10}") |
| print(separator + "\n") |
|
|
| 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=15) |
| return res.json() |
| except Exception as e: |
| return {"code": "-1", "msg": str(e)} |
|
|
| def get_market_rules(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 |
| return { |
| "lotSz": float(inst['lotSz']), |
| "tickSz": float(inst['tickSz']), |
| "prec": prec, |
| "ctVal": float(inst.get('ctVal', 1)), |
| "minSz": float(inst['minSz']) |
| } |
| except Exception as e: |
| print(f"Lỗi lấy rules cho {symbol}: {e}") |
| return None |
|
|
| |
| |
| |
|
|
| def run_scanner(): |
| scan_results = [] |
| for symbol in SYMBOLS: |
| try: |
| url = f"{OKX_BASE_URL}/api/v5/market/history-candles?instId={symbol}&bar=5m&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) |
| |
| n_curr, n0 = df.iloc[0], df.iloc[1] |
| |
| cfg_key = "BTC" if "BTC" in symbol else "XAU" |
| CONF = CONFIG[cfg_key] |
|
|
| |
| body_val0 = abs(n0['c'] - n0['o']) |
| u_wick_abs = n0['h'] - max(n0['c'], n0['o']) |
| l_wick_abs = min(n0['c'], n0['o']) - n0['l'] |
| |
| body_pct0 = (body_val0 * 100 / n0['o']) if n0['o'] != 0 else 0 |
| u_wick_pct0 = (u_wick_abs * 100 / n0['c']) if n0['c'] != 0 else 0 |
| l_wick_pct0 = (l_wick_abs * 100 / n0['c']) if n0['c'] != 0 else 0 |
|
|
| action = "WAIT" |
|
|
| |
| |
| if body_pct0 >= CONF['body_pct']: |
| |
| if l_wick_pct0 >= CONF['long_l_wick'] and u_wick_pct0 <= CONF['long_u_wick']: |
| action = "🔵 LONG ALERT" |
| execute_trade(symbol, "long", n0, n_curr, CONF) |
| |
| |
| elif u_wick_pct0 >= CONF['short_u_wick'] and l_wick_pct0 <= CONF['short_l_wick']: |
| action = "🔴 SHORT ALERT" |
| execute_trade(symbol, "short", n0, n_curr, CONF) |
|
|
| scan_results.append({ |
| "sym": symbol.replace("-SWAP",""), |
| "side": "GREEN" if n0['c'] > n0['o'] else "RED", |
| "body": body_pct0, "u_wick": u_wick_pct0, "l_wick": l_wick_pct0, |
| "act": action |
| }) |
|
|
| except Exception as e: |
| print(f"❌ Error scanning {symbol}: {e}") |
| |
| if scan_results: |
| print_log_table(scan_results) |
|
|
| def execute_trade(symbol, side, n0_data, n_curr_data, CONF): |
| """ |
| Hàm này giờ chỉ thực hiện tính toán thông số và gửi Slack Alert. |
| Toàn bộ code API đặt lệnh đã được COMMENT OUT. |
| """ |
| try: |
| rules = get_market_rules(symbol) |
| if not rules: return |
| |
| target_lev = int(CONF['lev']) |
| curr_close = n_curr_data['c'] |
| |
| |
| if side == "long": |
| entry = curr_close |
| sl_price = n0_data['l'] * (1 - CONFIG["SL_BUFFER"] / 100) |
| else: |
| entry = curr_close |
| sl_price = n0_data['h'] * (1 + CONFIG["SL_BUFFER"] / 100) |
| |
| risk = abs(entry - sl_price) |
| tp_price = (entry + risk * CONFIG["RR"]) if side == "long" else (entry - risk * CONFIG["RR"]) |
| |
| fmt_entry = "{:.{}f}".format(entry, rules['prec']) |
| fmt_sl = "{:.{}f}".format(sl_price, rules['prec']) |
| fmt_tp = "{:.{}f}".format(tp_price, rules['prec']) |
|
|
| |
| msg = (f"🚀 *PHÁT HIỆN TÍN HIỆU {side.upper()}*\n" |
| f"- Symbol: {symbol}\n" |
| f"- Giá hiện tại: {fmt_entry}\n" |
| f"- Gợi ý SL: {fmt_sl}\n" |
| f"- Gợi ý TP: {fmt_tp}\n" |
| f"- Râu nến N0 đạt điều kiện thiết lập.") |
| send_slack_msg(msg) |
|
|
| |
| |
| |
| """ |
| # Đặt Leverage |
| okx_request("POST", "/api/v5/account/set-leverage", { |
| "instId": symbol, "lever": str(target_lev), "mgnMode": "isolated", "posSide": side |
| }) |
| |
| size = math.floor((CONF['amount'] * target_lev / (entry * rules['ctVal'])) / rules['lotSz']) * rules['lotSz'] |
| |
| if size < rules['minSz']: |
| print(f"⚠️ {symbol}: Size {size} < Min {rules['minSz']}") |
| return |
| |
| body = { |
| "instId": symbol, "tdMode": "isolated", "side": "buy" if side == "long" else "sell", |
| "posSide": side, "ordType": "limit", "px": fmt_entry, "sz": str(size), |
| "attachAlgoOrds": [ |
| {"attachAlgoOrdType": "sl", "slTriggerPx": fmt_sl, "slOrdPx": "-1"}, |
| {"attachAlgoOrdType": "tp", "tpTriggerPx": fmt_tp, "tpOrdPx": "-1"} |
| ] |
| } |
| res = okx_request("POST", "/api/v5/trade/order", body) |
| """ |
| |
|
|
| except Exception as e: |
| print(f"🚨 LỖI HÀM CẢNH BÁO ({symbol}): {str(e)}") |
|
|
| |
| |
| |
|
|
| def update_ui(btc_amt, btc_lev, btc_body, b_l_l, b_l_u, b_s_u, b_s_l, |
| xau_amt, xau_lev, xau_body, x_l_l, x_l_u, x_s_u, x_s_l, |
| rr, running): |
| |
| CONFIG["BTC"].update({ |
| "amount": btc_amt, "lev": btc_lev, "body_pct": btc_body, |
| "long_l_wick": b_l_l, "long_u_wick": b_l_u, "short_u_wick": b_s_u, "short_l_wick": b_s_l |
| }) |
| CONFIG["XAU"].update({ |
| "amount": xau_amt, "lev": xau_lev, "body_pct": xau_body, |
| "long_l_wick": x_l_l, "long_u_wick": x_l_u, "short_u_wick": x_s_u, "short_l_wick": x_s_l |
| }) |
| CONFIG["RR"] = rr |
| CONFIG["RUNNING"] = running |
| status = "BẬT" if running else "TẮT" |
| return f"Hệ thống ALERT {status} - Đã cập nhật chiến lược râu nến cho BTC & XAU." |
|
|
| 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(5) |
| run_scanner() |
| CONFIG["LAST_PROCESSED_MIN"] = now.minute |
| time.sleep(1) |
|
|
| threading.Thread(target=main_loop, daemon=True).start() |
|
|
| with gr.Blocks(title="OKX Wick Alert V1.0") as demo: |
| gr.Markdown("# 🤖 OKX Wick Strategy Alert (Chỉ cảnh báo Slack)") |
| |
| with gr.Row(): |
| with gr.Column(): |
| gr.Markdown("### 🟠 Cấu hình BTC") |
| b_amt = gr.Number(label="Vốn giả định (USDT)", value=10) |
| b_lev = gr.Slider(1, 100, 25, label="Đòn bẩy") |
| b_body = gr.Number(label="Thân nến tối thiểu (%)", value=0.03) |
| with gr.Row(): |
| b_l_l = gr.Number(label="LONG: Râu Dưới >= (%)", value=0.13) |
| b_l_u = gr.Number(label="LONG: Râu Trên <= (%)", value=0.03) |
| with gr.Row(): |
| b_s_u = gr.Number(label="SHORT: Râu Trên >= (%)", value=0.13) |
| b_s_l = gr.Number(label="SHORT: Râu Dưới <= (%)", value=0.03) |
| |
| with gr.Column(): |
| gr.Markdown("### 🟡 Cấu hình XAU (Vàng)") |
| x_amt = gr.Number(label="Vốn giả định (USDT)", value=10) |
| x_lev = gr.Slider(1, 100, 25, label="Đòn bẩy") |
| x_body = gr.Number(label="Thân nến tối thiểu (%)", value=0.03) |
| with gr.Row(): |
| x_l_l = gr.Number(label="LONG: Râu Dưới >= (%)", value=0.13) |
| x_l_u = gr.Number(label="LONG: Râu Trên <= (%)", value=0.03) |
| with gr.Row(): |
| x_s_u = gr.Number(label="SHORT: Râu Trên >= (%)", value=0.13) |
| x_s_l = gr.Number(label="SHORT: Râu Dưới <= (%)", value=0.03) |
|
|
| with gr.Row(): |
| n_rr = gr.Number(label="Tỉ lệ R:R", value=3.0) |
| c_run = gr.Checkbox(label="KÍCH HOẠT QUÉT TÍN HIỆU") |
| |
| btn = gr.Button("CẬP NHẬT CẤU HÌNH", variant="primary") |
| out = gr.Textbox(label="Trạng thái") |
| |
| btn.click(update_ui, |
| [b_amt, b_lev, b_body, b_l_l, b_l_u, b_s_u, b_s_l, |
| x_amt, x_lev, x_body, x_l_l, x_l_u, x_s_u, x_s_l, |
| n_rr, c_run], out) |
|
|
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860) |