#!/usr/bin/env python3
"""
格蕾丝 一键下单脚本 — OKX BTC-USDT-SWAP 逐仓20x
用法:
  python3 grace_execute.py long   0.06  <entry约价>   # 市价开多
  python3 grace_execute.py short  0.06  <entry约价>   # 市价开空
  python3 grace_execute.py oco    0.06  <entry> <tp> <sl>   # 为已有持仓挂OCO
  python3 grace_execute.py close                        # 市价平仓
"""
import sys, os, time, hmac, hashlib, base64, json
import requests
from datetime import datetime, timezone, timedelta

BASE = "https://www.okx.com"
INST = "BTC-USDT-SWAP"

def load_env(path='/root/.hermes/.env'):
    env = {}
    with open(path) as f:
        for line in f:
            line = line.strip()
            if line and '=' in line and not line.startswith('#'):
                k, v = line.split('=', 1)
                env[k.strip()] = v.strip().strip('"').strip("'")
    return env

env = load_env()
API_KEY = env['OKX_API_KEY']
SECRET = env['OKX_SECRET_KEY']
PASS = env['OKX_PASSPHRASE']

def okx(method, path, params=None, body=''):
    ts = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
    qs = ''
    if params:
        qs = '?' + '&'.join(f"{k}={v}" for k, v in params.items())
    full = path + qs
    msg = ts + method + full + body
    sig = base64.b64encode(hmac.new(SECRET.encode(), msg.encode(), hashlib.sha256).digest()).decode()
    headers = {'OK-ACCESS-KEY': API_KEY, 'OK-ACCESS-SIGN': sig,
               'OK-ACCESS-TIMESTAMP': ts, 'OK-ACCESS-PASSPHRASE': PASS,
               'Content-Type': 'application/json'}
    r = requests.request(method, BASE + full, headers=headers, data=body if body else None, timeout=15)
    return r.json()

def get_ticker():
    r = requests.get(f"{BASE}/api/v5/market/ticker?instId={INST}", timeout=15)
    return float(r.json()['data'][0]['last'])

def set_leverage(lever=20):
    r = okx('POST', '/api/v5/account/set-leverage',
            body=json.dumps({"instId": INST, "lever": str(lever), "mgnMode": "isolated"}))
    print("设置杠杆:", r.get('code'), r.get('msg', ''))

def open_pos(side, sz):
    """side: buy=开多 sell=开空 (net模式)"""
    set_leverage(20)
    body = {"instId": INST, "tdMode": "isolated", "side": side,
            "ordType": "market", "sz": str(sz)}
    r = okx('POST', '/api/v5/trade/order', body=json.dumps(body))
    print("下单:", json.dumps(r, ensure_ascii=False))
    if r.get('code') == '0':
        print(f"✅ 开仓成功 ordId={r['data'][0]['ordId']}")
        return r['data'][0]['ordId']
    print(f"❌ 开仓失败: {r.get('msg')}")
    return None

def attach_oco(sz, tp, sl, side):
    """挂OCO: 止盈+止损 (One-cancels-the-other) — net模式
    side: 持仓方向 'long'/'short'
    tp/sl 为价格
    net模式下: 平多 side=sell, 平空 side=buy, 不带posSide
    ordType 必须用 oco (conditional 只支持单向, 会丢 tp/sl)
    """
    tp_side = "sell" if side == 'long' else "buy"
    sl_side = "sell" if side == 'long' else "buy"
    body = {
        "instId": INST,
        "tdMode": "isolated",
        "sz": str(sz),
        "tpTriggerPx": str(tp),
        "tpOrdPx": "-1",
        "slTriggerPx": str(sl),
        "slOrdPx": "-1",
        "ordType": "oco",
        "side": tp_side,
    }
    r = okx('POST', '/api/v5/trade/order-algo', body=json.dumps(body))
    print("OCO:", json.dumps(r, ensure_ascii=False))
    if r.get('code') == '0':
        print(f"✅ OCO挂载成功 algoId={r['data'][0].get('algoId')}")
        return r['data'][0].get('algoId')
    print(f"❌ OCO失败: {r.get('msg')}")
    return None

def get_position():
    r = okx('GET', '/api/v5/account/positions', params={'instId': INST})
    if r.get('code') != '0':
        return None
    for p in r.get('data', []):
        if p.get('instId') == INST and float(p.get('pos', 0)) != 0:
            return p
    return None

def verify_oco(algo_id, sz, tp, sl, side):
    """验证OCO已真实挂载(查API pending algo单), 防止日志伪造"""
    r = okx('GET', '/api/v5/trade/orders-algo-pending', params={'instId': INST, 'ordType': 'oco'})
    if r.get('code') != '0':
        print(f"❌ OCO验证失败(API错误): {r.get('msg')}")
        return False
    for a in r.get('data', []):
        if a.get('algoId') == algo_id and a.get('tpTriggerPx') == str(tp) and a.get('slTriggerPx') == str(sl):
            print(f"✅ OCO已确认挂载 algoId={algo_id} tp={tp} sl={sl}")
            return True
    print(f"❌ OCO未找到! algoId={algo_id} 期望tp={tp} sl={sl} — 风控裸奔!!")
    return False

def close_pos():
    p = get_position()
    if not p:
        print("NO_POSITION")
        return
    side = "sell" if float(p['pos']) > 0 else "buy"
    body = {"instId": INST, "tdMode": "isolated", "posSide": p.get('posSide', 'net'),
            "side": side, "ordType": "market", "sz": p.get('pos')}
    r = okx('POST', '/api/v5/trade/close-position',
            body=json.dumps({"instId": INST, "mgnMode": "isolated", "posSide": p.get('posSide', 'net')}))
    print("平仓:", json.dumps(r, ensure_ascii=False))

if __name__ == '__main__':
    args = sys.argv[1:]
    if not args:
        print(__doc__)
        sys.exit(0)
    cmd = args[0]
    if cmd == 'close':
        close_pos()
    elif cmd == 'long' and len(args) >= 2:
        open_pos('buy', args[1])
    elif cmd == 'short' and len(args) >= 2:
        open_pos('sell', args[1])
    elif cmd == 'oco' and len(args) >= 5:
        side, sz, tp, sl = args[1], args[2], args[3], args[4]
        algo = attach_oco(sz, float(tp), float(sl), side)
        if algo and verify_oco(algo, sz, float(tp), float(sl), side):
            print("OCO_OK")
            sys.exit(0)
        print("OCO_FAIL")
        sys.exit(1)
    else:
        print(__doc__)
