Files
Trade/get_positions.py
T
2026-08-26 15:07:04 +08:00

182 lines
6.8 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
OKX 账户只读查询: 余额 + 当前持仓
- 仅使用 GET 只读接口, 绝不写入/下单
- 密钥从同目录 okx_keys.json 读取
- 支持代理: 命令行 --proxy 或环境变量 OKX_PROXY, 如 http://127.0.0.1:1081 或 socks5://127.0.0.1:1082
- 支持自定义入口: --host 默认 https://www.okx.com
- 支持 DNS 劫持绕过: --dns-ip 指定连接 IP (如 TUN fake-ip 198.18.0.0),
仅影响 TCP 连接地址, Host/SNI/证书校验仍用真实域名
"""
import json, hmac, hashlib, base64, datetime, sys, os, argparse
import subprocess
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)
def sign(secret, 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 build_opener(proxy):
if proxy:
handler = urllib.request.ProxyHandler({"http": proxy, "https": proxy})
return urllib.request.build_opener(handler)
return urllib.request.build_opener()
def patch_dns(dns_ip):
"""把指定域名的 DNS 解析替换为 dns_ip (绕过 DNS 污染, TUN fake-ip 场景)"""
import socket
real_getaddrinfo = socket.getaddrinfo
def patched(host, port, *args, **kwargs):
if host == "www.okx.com":
return real_getaddrinfo(dns_ip, port, *args, **kwargs)
return real_getaddrinfo(host, port, *args, **kwargs)
socket.getaddrinfo = patched
def okx_get_curl(path, keys, host, dns_ip=None, proxy=None):
"""用 curl 发请求 (绕过 Cloudflare 对 Python-urllib 的 TLS/UA 拦截)"""
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
sig = sign(keys["secret"], ts, "GET", path, "")
url = host + path
cmd = ["curl", "-s", "--max-time", "15"]
if dns_ip:
domain = host.split("//")[1].split("/")[0]
cmd += ["--resolve", f"{domain}:443:{dns_ip}"]
if proxy:
cmd += ["-x", proxy]
cmd += [
"-H", f"OK-ACCESS-KEY:{keys['api_key']}",
"-H", f"OK-ACCESS-SIGN:{sig}",
"-H", f"OK-ACCESS-TIMESTAMP:{ts}",
"-H", f"OK-ACCESS-PASSPHRASE:{keys['passphrase']}",
"-H", "Content-Type:application/json",
"-H", "User-Agent:curl/8.5.0",
url,
]
try:
out = subprocess.run(cmd, capture_output=True, text=True, timeout=20).stdout
return json.loads(out)
except Exception as e:
return {"code": "-1", "msg": str(e)}
def okx_get(path, keys, host, opener):
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
sig = sign(keys["secret"], ts, "GET", path, "")
req = urllib.request.Request(host + path)
req.add_header("OK-ACCESS-KEY", keys["api_key"])
req.add_header("OK-ACCESS-SIGN", sig)
req.add_header("OK-ACCESS-TIMESTAMP", ts)
req.add_header("OK-ACCESS-PASSPHRASE", keys["passphrase"])
req.add_header("Content-Type", "application/json")
try:
with opener.open(req, timeout=15) as r:
return json.loads(r.read().decode())
except Exception as e:
return {"code": "-1", "msg": str(e)}
def fmt(n, d=4):
try:
return f"{float(n):,.{d}f}"
except Exception:
return str(n)
def main():
ap = argparse.ArgumentParser(description="OKX 账户只读查询")
ap.add_argument("--proxy", default=os.environ.get("OKX_PROXY"),
help="代理地址, 如 http://127.0.0.1:1081 或 socks5://127.0.0.1:1082")
ap.add_argument("--host", default="https://www.okx.com",
help="API 入口, 默认 https://www.okx.com (可换 aws.okx.com)")
ap.add_argument("--dns-ip", default=os.environ.get("OKX_DNS_IP"),
help="绕过 DNS 污染: 强制用该 IP 连接 www.okx.com (如 TUN fake-ip 198.18.0.0)")
args = ap.parse_args()
keys = load_keys()
bal = okx_get_curl("/api/v5/account/balance", keys, args.host, args.dns_ip, args.proxy)
if bal.get("code") != "0":
print("余额查询失败:", bal.get("msg", bal))
return
pos = okx_get_curl("/api/v5/account/positions", keys, args.host, args.dns_ip, args.proxy)
if pos.get("code") != "0":
print("持仓查询失败:", pos.get("msg", pos))
return
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
d = bal["data"][0]
# 从 details 汇总各币种余额
details_bal = d.get("details", [])
rows = []
total_avail = total_frozen = total_upl_bal = 0.0
for c in details_bal:
ccy = c.get("ccy", "")
avail = float(c.get("availEq") or c.get("availBal") or 0)
frozen = float(c.get("frozenBal") or 0)
ord_froz = float(c.get("ordFrozen") or 0)
upl_c = float(c.get("upl") or 0)
if ccy:
rows.append((ccy, avail, frozen, ord_froz, upl_c))
total_avail += avail
total_frozen += frozen
total_upl_bal += upl_c
print("=" * 78)
print(f"OKX 账户快照 {now}")
print(f"总权益 totalEq : {fmt(d.get('totalEq'), 2)} USDT")
print(f"可用余额合计 : {total_avail:,.2f} USDT")
print(f"冻结资金合计 : {total_frozen:,.2f} USDT (其中挂单冻结 {sum(r[3] for r in rows):,.2f})")
print(f"未实现盈亏 UPL : {total_upl_bal:+,.2f} USDT")
print("-" * 78)
print(f"{'币种':<8}{'可用余额':>16}{'冻结':>16}{'挂单冻结':>16}")
for ccy, avail, frozen, ord_froz, _ in rows:
if avail or frozen:
print(f"{ccy:<8}{avail:>16,.6f}{frozen:>16,.6f}{ord_froz:>16,.6f}")
print("=" * 78)
details = pos.get("data", [])
if not details:
print("当前无持仓")
return
print(f"{'合约':<14}{'方向':<6}{'数量':>12}{'开仓均价':>14}{'标记价':>14}{'保证金':>12}{'杠杆':>6}{'强平价':>14}{'浮动盈亏':>14}")
print("-" * 78)
total_upl = 0.0
for p in details:
inst = p.get("instId", "")
pos_side = p.get("posSide", "")
sz = float(p.get("pos", 0) or 0)
avg_px = float(p.get("avgPx", 0) or 0)
mark_px = float(p.get("markPx", 0) or 0)
margin = float(p.get("margin", 0) or 0)
lev = p.get("lever", "")
liq = float(p.get("liqPx", 0) or 0)
upl = float(p.get("upl", 0) or 0)
total_upl += upl
side_cn = {"long": "多", "short": "空", "net": "净"}.get(pos_side, pos_side)
print(f"{inst:<14}{side_cn:<6}{fmt(sz):>12}{fmt(avg_px):>14}{fmt(mark_px):>14}"
f"{fmt(margin, 2):>12}{lev:>6}{fmt(liq):>14}{fmt(upl, 2):>14}")
print("-" * 78)
print(f"持仓合计浮动盈亏: {total_upl:+,.2f} USDT")
if __name__ == "__main__":
main()