#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ OKX 合约账户 只读监控 + 实时分析提醒 - 仅使用 GET 只读接口(账户/持仓/未成交/行情),绝不写入/下单 - 密钥从同目录 okx_keys.json 读取(不要硬编码) - 每5分钟运行, 触发条件弹 macOS 通知 """ import subprocess, json, hmac, hashlib, base64, datetime, sys, os HERE = os.path.dirname(os.path.abspath(__file__)) KEYS_FILE = os.path.join(HERE, "okx_keys.json") # ===== 读取密钥 ===== def load_keys(): with open(KEYS_FILE, "r", encoding="utf-8") as f: return json.load(f) API_KEY = "" SECRET = "" PASSPHRASE = "" try: k = load_keys() API_KEY = k["api_key"] SECRET = k["secret"] PASSPHRASE = k["passphrase"] except Exception as e: print("读取密钥失败:", e) sys.exit(1) def sign(timestamp, method, path, body=""): pre = timestamp + method.upper() + path + body mac = hmac.new(SECRET.encode("utf-8"), pre.encode("utf-8"), hashlib.sha256) return base64.b64encode(mac.digest()).decode() def okx_get(path): ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" sig = sign(ts, "GET", path, "") curl = [ "curl", "-s", "--max-time", "10", "-H", f"OK-ACCESS-KEY:{API_KEY}", "-H", f"OK-ACCESS-SIGN:{sig}", "-H", f"OK-ACCESS-TIMESTAMP:{ts}", "-H", f"OK-ACCESS-PASSPHRASE:{PASSPHRASE}", "-H", "Content-Type:application/json", f"https://www.okx.com{path}", ] out = subprocess.run(curl, capture_output=True, text=True, timeout=15).stdout try: return json.loads(out) except Exception: return {"code": "-1", "msg": out[:200]} ALERTED = set() def notify(title, msg): subprocess.run(["osascript", "-e", f'display notification "{msg}" with title "{title}" sound name "Glass"']) def mark_price(inst_id): """拉实时标记价(只读公共接口)""" try: out = subprocess.run( ["curl", "-s", "--max-time", "8", f"https://www.okx.com/api/v5/market/ticker?instId={inst_id}"], capture_output=True, text=True, timeout=12).stdout d = json.loads(out) if d.get("code") == "0": return float(d["data"][0]["last"]) except Exception: pass return None # 合约面值缓存(每张对应多少基础币) _CTVAL = {} def ctval(inst_id): if inst_id in _CTVAL: return _CTVAL[inst_id] try: out = subprocess.run( ["curl", "-s", "--max-time", "8", f"https://www.okx.com/api/v5/public/instruments?instType=SWAP"], capture_output=True, text=True, timeout=12).stdout for x in json.loads(out).get("data", []): if x["instId"] == inst_id: v = float(x.get("ctVal", 1)) _CTVAL[inst_id] = v return v except Exception: pass _CTVAL[inst_id] = 1.0 return 1.0 # 手续费档(普通用户Taker); 开平各一次 TAKER_FEE = 0.0005 # 资金费保守估算: 每8h最多按0.01%偏多, 默认持仓1天=3期(无持仓时长则按0算) FUNDING_PER_PERIOD = 0.0001 FUNDING_PERIODS = 3 def be_price(inst, sz, entry, upl): """算覆盖所有费用后的回本价(多/空通用)""" cv = ctval(inst) notional = sz * cv * entry # 名义价值(开仓) fee = notional * TAKER_FEE * 2 # 开+平手续费 funding = notional * FUNDING_PER_PERIOD * FUNDING_PERIODS total_cost = fee + funding # 需覆盖的总费用 per_point = sz * cv # 每点价格变动盈亏 need_move = total_cost / per_point # 需涨/跌点数 # 多单需涨, 空单需跌 side = "long" if upl >= 0 else "long" return entry + need_move, total_cost, need_move def analyze_and_alert(bal, pos): """基于持仓做风险提示: 浮亏预警 + 强平价关注 + 距强平<=3%紧急预警 + 费用回本价""" today = datetime.date.today().isoformat() if ALERTED and not any(x.endswith(today) for x in ALERTED): ALERTED.clear() details = pos.get("data", []) for p in details: inst = p.get("instId", "") pos_side = p.get("posSide", "") sz = float(p.get("pos", "0")) if p.get("pos") else 0.0 lev = p.get("lever", "1") liq = p.get("liqPx", "") upl = float(p.get("upl", "0")) if p.get("upl") else 0.0 ccy = p.get("ccy", "") if sz == 0 or not liq: continue liq = float(liq) tip = f"{inst} {pos_side} 量{sz} 杠杆{lev} 强平@{liq:.2f} 浮动{upl:+.2f}{ccy}" # 费用回本价 be, cost, move = be_price(inst, sz, float(p.get("avgPx", 0)) or 0, upl) if float(p.get("avgPx", 0) or 0) > 0: print(f" 费用回本价: {be:.2f} (需覆盖费用{cost:.2f}USDT, 即再涨{move:.2f}点)") # 浮亏超阈值提醒 if upl < -50: key = f"loss_{inst}_{today}" if key not in ALERTED: ALERTED.add(key) notify(f"[OKX 浮亏预警] {inst}", tip) # 强平价关注(每日一次) key0 = f"liq_{inst}_{today}" if key0 not in ALERTED: ALERTED.add(key0) notify(f"[OKX 持仓监控] {inst}", f"强平价 {liq:.2f} | 回本价{be:.2f} | {tip}") # 距强平 <=3% 紧急预警(每小时一次, 用分钟级key避免刷屏) mp = mark_price(inst) if mp: dist = abs(mp - liq) / mp * 100 if dist <= 3.0: hm = datetime.datetime.now().strftime("%Y%m%d%H") key1 = f"risk_{inst}_{hm}" if key1 not in ALERTED: ALERTED.add(key1) notify(f"[OKX 紧急·近强平] {inst}", f"现价{mp:.2f} 距强平{liq:.2f}仅{dist:.1f}%! 回本价{be:.2f} | 建议降杠杆/减仓 | {tip}") print(f" {inst} 现价{mp:.2f} 强平{liq:.2f} 距强平{dist:.2f}%") def main(): if not API_KEY: sys.exit(0) # 只读接口 bal = okx_get("/api/v5/account/balance") if bal.get("code") != "0": err = bal.get("msg", "未知错误") print("余额查询失败:", err) # 401/签名错也通知, 方便排查 notify("[OKX 监控] 查询失败", err[:80]) return pos = okx_get("/api/v5/account/positions?instType=SWAP") # 打印摘要 try: total = float(bal["data"][0]["totalEq"]) print(f"[{datetime.datetime.now():%H:%M}] 总权益: {total:.2f} USDT") except Exception: pass analyze_and_alert(bal, pos if pos.get("code") == "0" else {"data": []}) if __name__ == "__main__": main()