Skill: iceonme/okx-trading-exe (version 0.1.1)

--- name: okx-trading-exe description: A standardized adapter gateway skill exclusively for OKX exchange to execute trading actions (buy/sell) and query capabilities across OKX Live and Demo environments. --- # `okx-trading-exe` Skill 说明书 > 本 Skill 是一个无状态的“执行手柄”，只负责对接 OKX 交易所（包含模拟盘与实盘），执行标准化的交易动作与资产查询。它**不包含**其他交易所接口，也**不包含**任何策略分析逻辑。 ## 🔐 授权与鉴权获取 (Prompt-driven Auth) - Agent 必读 本代码需要 OKX 的 `API_KEY`, `API_SECRET` 和 `PASSPHRASE` 才能运行。 **如果**你在执行底层的脚本时，收到了关于 `Missing API Key` 或未授权的报错，你**绝对不要**尝试去猜测密码，也**不要**停止工作。 **你的处理逻辑必须是：**中途中断当前的代码思路，使用自然语言明确向你的“物理人类主人”索要这三个秘钥，并让主人将它们写入当前工作目录的 `.env` 文件中。只有在人类确认写好后，你才能再次尝试执行。 ## 能力范围 (Capabilities) 目前支持以下核心操作动作： - 查询账户总余额/可用资金 (`get_balance`) - 查询当前已持有的真实仓位 (`get_positions`) - 发起市价买单/卖单 (`place_market_order`) - 发起限价买单/卖单 (`place_limit_order`) - 拉取最近的历史成交记录 (`get_recent_trades`) ## 调用说明 (Usage for Agents / PA) 如果你是一个外层调度 Agent (比如 TAS 小队成员) 或策略 (Skill)，请使用随附的入口脚本与本 Skill 进行交互。 **标准切入点**： `python scripts/execute.py --action [ACTION_NAME] --provider [PROVIDER_NAME] [OPTIONS]` ### 支持的 Provider (环境提供者) - `okx_demo`: 连接 OKX 交易所模拟盘 (预设). - `okx_live`: 连接 OKX 交易所的实盘交易. ### 示例用法 (Examples) **1. 查询模拟盘所有资产与持仓** ```bash python scripts/execute.py --action get_positions --provider okx_demo ``` **2. 在模拟盘下一笔市价买单** ```bash python scripts/execute.py --action place_market_order --provider okx_demo --symbol BTC-USDT --side buy --size 50 # 注意: OKX 市价买单时，size 通常代表花费 50 USDT。 ``` **3. 在模拟盘下一笔限价卖单** ```bash python scripts/execute.py --action place_limit_order --provider okx_demo --symbol BTC-USDT --side sell --size 0.1 --price 62000 # 卖出 0.1 枚 BTC，限价标的是 62000 USDT。 ``` ## 注意事项与约束 (Constraints) 1. **统一的交易对格式**：所有请求必须使用 `-` 连接的标准格式，如 `BTC-USDT`，不要使用 `/`。 2. **凭据安全**：本 Skill 在真实请求前会自动读取 `.env` 中的秘钥配置，不要在任何参数中明文传递私钥！ 3. **输出格式**：执行结果将以标准化的 JSON 或清爽的 stdout 文本格式输出，方便被其他脚本或大语言模型正则提取。

path: .env
content: # OKX API Credentials (Required if using okx_demo or okx_live provider)
OKX_API_KEY="your_api_key_here"
OKX_API_SECRET="your_api_secret_here"
OKX_PASSPHRASE="your_passphrase_here"


path: scripts/execute.py
content: import os
import sys
import argparse
import json
from dotenv import load_dotenv

# 添加父目录到 sys.path 以便能够导入 providers
script_dir = os.path.dirname(os.path.abspath(__file__))
skill_dir = os.path.dirname(script_dir)
sys.path.insert(0, skill_dir)

from providers.okx_provider import OKXProvider

def main():
    parser = argparse.ArgumentParser(description="Trading Execution Interface (TAS Standard)")
    parser.add_argument("--action", required=True, choices=[
        "get_balance", "get_positions", "place_market_order", 
        "place_limit_order", "get_recent_trades"
    ], help="The action to perform")
    
    parser.add_argument("--provider", required=True, choices=["okx_demo", "okx_live"], 
                        help="The execution provider environment")
    
    # action 对应的额外参数
    parser.add_argument("--symbol", type=str, help="Trading pair, e.g. BTC-USDT")
    parser.add_argument("--side", type=str, choices=["buy", "sell"], help="Order side")
    parser.add_argument("--size", type=float, help="Order size (USDT amount for market buy, crypto amount for others)")
    parser.add_argument("--price", type=float, help="Order price (for limit orders)")
    parser.add_argument("--limit", type=int, default=20, help="Limit for history query")
    
    args = parser.parse_args()
    
    # 尝试加载环境所在目录的 .env 文件 (优先向上层找)
    env_path = os.path.join(skill_dir, '.env')
    if not os.path.exists(env_path):
        env_path = os.path.join(os.path.dirname(os.path.dirname(skill_dir)), '.env')
    load_dotenv(env_path)

    [REDACTED_SECRET]"OKX_API_KEY", "")
    api_secret = os.getenv("OKX_API_SECRET", "")
    passphrase = os.getenv("OKX_PASSPHRASE", "")
    
    if args.provider in ["okx_demo", "okx_live"]:
        if not all([api_key, api_secret, passphrase]):
            print(json.dumps({"error": "Missing OKX API credentials in environment variables."}))
            sys.exit(1)
            
        is_demo = (args.provider == "okx_demo")
        provider = OKXProvider(api_key, api_secret, passphrase, is_demo)
    else:
        print(json.dumps({"error": f"Unsupported provider: {args.provider}"}))
        sys.exit(1)
        
    # 执行对应的操作
    try:
        if args.action == "get_balance":
            cash = provider.get_cash()
            print(json.dumps({"balance_usdt": cash}))
            
        elif args.action == "get_positions":
            positions = provider.get_positions()
            print(json.dumps({"positions": positions}))
            
        elif args.action == "place_market_order":
            if not all([args.symbol, args.side, args.size]):
                print(json.dumps({"error": "Missing required arguments: --symbol, --side, --size"}))
                sys.exit(1)
            result = provider.place_market_order(args.symbol, args.side, args.size)
            print(json.dumps(result))
            
        elif args.action == "place_limit_order":
            if not all([args.symbol, args.side, args.size, args.price]):
                print(json.dumps({"error": "Missing required arguments: --symbol, --side, --size, --price"}))
                sys.exit(1)
            result = provider.place_limit_order(args.symbol, args.side, args.size, args.price)
            print(json.dumps(result))
            
        elif args.action == "get_recent_trades":
            if not args.symbol:
                print(json.dumps({"error": "Missing required argument: --symbol"}))
                sys.exit(1)
            trades = provider.get_recent_trades(args.symbol, args.limit)
            print(json.dumps({"trades": trades}))
            
    except Exception as e:
        print(json.dumps({"error": str(e)}))
        sys.exit(1)

if __name__ == "__main__":
    main()


path: providers/base_provider.py
content: from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any

class BaseProvider(ABC):
    """
    TAS-20 标准: 执行手柄抽象层 (Executor Interface)
    所有具体的交易所/回测引擎插件都必须继承并实现此接口。
    这保证了外层在使用 trading_exe 时，无论底层连向哪，API 始终一致。
    """
    
    @abstractmethod
    def get_cash(self) -> float:
        """获取当前账户可用资金 (如可用 USDT 余额)"""
        pass

    @abstractmethod
    def get_positions(self) -> List[Dict[str, Any]]:
        """
        获取当前账户所有的持有仓位。
        返回的数据结构示例:
        [
            {"symbol": "BTC-USDT", "size": 1.5, "avg_price": 50000, "unrealized_pnl": 1500},
            ...
        ]
        """
        pass

    @abstractmethod
    def place_market_order(self, symbol: str, side: str, size: float) -> Dict[str, Any]:
        """
        下达市价单。
        对于 BUY 操作，size 可能代表花费的 USDT。
        对于 SELL 操作，size 通常代表卖出的基础币种数量。
        
        Args:
            symbol: 交易对 (e.g., 'BTC-USDT')
            side: 方向 ('buy' 或 'sell')
            size: 下单数量或金额
            
        Returns:
            Dict: 必须包含 'order_id' 以及 'status' ('submitted', 'rejected' 等)
        """
        pass

    @abstractmethod
    def place_limit_order(self, symbol: str, side: str, size: float, price: float) -> Dict[str, Any]:
        """
        下达限价单。
        """
        pass

    @abstractmethod
    def get_recent_trades(self, symbol: str, limit: int = 20) -> List[Dict[str, Any]]:
        """
        获取最近在该交易对的历史成交记录。
        """
        pass


path: providers/okx_provider.py
content: import hmac
import hashlib
import base64
import json
import requests
from datetime import datetime, timezone
from typing import List, Dict, Any, Optional
from .base_provider import BaseProvider

class OKXClient:
    """内部轻量级的 OKX API 客户端 (无行情与复杂依赖)"""
    def __init__(self, api_key: str, api_secret: str, passphrase: str, is_demo: bool = True):
        self.[REDACTED_SECRET]
        self.api_secret = api_secret
        self.passphrase = passphrase
        self.is_demo = is_demo
        self.base_url = "https://www.okx.com"
        self.session = requests.Session()

    def _get_timestamp(self) -> str:
        return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
    
    def _sign(self, timestamp: str, method: str, request_path: str, body: str = '') -> str:
        message = timestamp + method.upper() + request_path + body
        mac = hmac.new(
            self.api_secret.encode('utf-8'),
            message.encode('utf-8'),
            hashlib.sha256
        )
        return base64.b64encode(mac.digest()).decode('utf-8')
    
    def request(self, method: str, path: str, params: Optional[Dict] = None, body: Optional[Dict] = None) -> Optional[Dict]:
        url = self.base_url + path
        request_path = path
        
        if method == 'GET' and params:
            from urllib.parse import urlencode
            query = urlencode(params)
            request_path += f"?{query}"
            
        body_str = json.dumps(body) if body else ""
        timestamp = self._get_timestamp()
        
        headers = {
            'OK-ACCESS-KEY': self.api_key,
            'OK-ACCESS-SIGN': self._sign(timestamp, method, request_path, body_str),
            'OK-ACCESS-TIMESTAMP': timestamp,
            'OK-ACCESS-PASSPHRASE': self.passphrase,
            'Content-Type': 'application/json'
        }
        
        if self.is_demo:
            headers['x-simulated-trading'] = '1'
            
        try:
            if method == 'GET':
                resp = self.session.get(url, headers=headers, params=params, timeout=10)
            else:
                resp = self.session.post(url, headers=headers, data=body_str, timeout=10)
            resp.raise_for_status()
            return resp.json()
        except Exception as e:
            print(f"[OKX API Error] {e}")
            return None


class OKXProvider(BaseProvider):
    """
    OKX 执行提供者。实现了 BaseProvider 接口。
    """
    def __init__(self, api_key: str, api_secret: str, passphrase: str, is_demo: bool = True):
        self.client = OKXClient(api_key, api_secret, passphrase, is_demo)
        
    def _normalize_symbol(self, symbol: str) -> str:
        """统一交易对格式 (输入如 BTC/USDT 或 BTC-USDT，输出 BTC-USDT)"""
        return symbol.replace('/', '-')

    def get_cash(self) -> float:
        result = self.client.request('GET', '/api/v5/account/balance', {'ccy': 'USDT'})
        if result and result.get('code') == '0':
            for asset in result['data'][0].get('details', []):
                if asset['ccy'] == 'USDT':
                    return float(asset['availBal'])
        return 0.0

    def get_positions(self) -> List[Dict[str, Any]]:
        result = self.client.request('GET', '/api/v5/account/positions')
        positions = []
        if result and result.get('code') == '0':
            for pos in result.get('data', []):
                size = float(pos.get('pos', 0) or 0)
                if abs(size) < 1e-12: continue
                positions.append({
                    "symbol": pos.get('instId', ''),
                    "size": size,
                    "avg_price": float(pos.get('avgPx', 0) or 0),
                    "unrealized_pnl": float(pos.get('upl', 0) or 0),
                })
        return positions

    def place_market_order(self, symbol: str, side: str, size: float) -> Dict[str, Any]:
        """
        OKX 市价单：
        - side = 'buy' 时，如果 ccy='USDT'，sz 代表购买用的 USDT 金额
        - side = 'sell' 时，sz 代表卖出的基础货币数量
        """
        inst_id = self._normalize_symbol(symbol)
        side = side.lower()
        
        body = {
            'instId': inst_id,
            'tdMode': 'cash',
            'side': side,
            'ordType': 'market',
        }
        
        if side == 'buy':
            # 默认传入的 size 为报价币 USDT 的金额
            body['ccy'] = 'USDT'
            body['sz'] = f"{size:.2f}"
            if float(body['sz']) < 10:
                body['sz'] = "10.00"
        else:
            body['sz'] = f"{size:.8f}".rstrip('0').rstrip('.')
            if not body['sz'] or float(body['sz']) < 0.00001:
                body['sz'] = "0.00001"
                
        resp = self.client.request('POST', '/api/v5/trade/order', body=body)
        
        if resp and resp.get('code') == '0':
            return {"order_id": resp['data'][0]['ordId'], "status": "submitted"}
        
        reject_reason = resp.get('msg', 'Unknown Error') if resp else "Request Failed"
        return {"order_id": "", "status": "rejected", "reason": reject_reason}

    def place_limit_order(self, symbol: str, side: str, size: float, price: float) -> Dict[str, Any]:
        inst_id = self._normalize_symbol(symbol)
        side = side.lower()
        
        sz = f"{size:.8f}".rstrip('0').rstrip('.')
        if not sz or float(sz) < 0.00001:
            sz = "0.00001"
            
        body = {
            'instId': inst_id,
            'tdMode': 'cash',
            'side': side,
            'ordType': 'limit',
            'sz': sz,
            'px': str(price)
        }
        
        resp = self.client.request('POST', '/api/v5/trade/order', body=body)
        
        if resp and resp.get('code') == '0':
            return {"order_id": resp['data'][0]['ordId'], "status": "submitted"}
            
        reject_reason = resp.get('msg', 'Unknown Error') if resp else "Request Failed"
        return {"order_id": "", "status": "rejected", "reason": reject_reason}

    def get_recent_trades(self, symbol: str, limit: int = 20) -> List[Dict[str, Any]]:
        inst_id = self._normalize_symbol(symbol)
        params = {
            'instType': 'SPOT',
            'instId': inst_id,
            'limit': str(limit)
        }
        resp = self.client.request('GET', '/api/v5/trade/orders-history', params)
        trades = []
        if resp and resp.get('code') == '0':
            for o in resp.get('data', []):
                side_val = (o.get('side') or '').upper()
                avg_px = float(o.get('avgPx') or o.get('px') or 0)
                fill_sz = float(o.get('fillSz', 0) or o.get('sz', 0) or 0)
                
                # 时间格式化
                c_time = int(o.get('cTime', 0))
                dt = datetime.fromtimestamp(c_time / 1000).isoformat() if c_time else datetime.now().isoformat()
                
                trades.append({
                    "order_id": o.get('ordId', ''),
                    "symbol": inst_id,
                    "side": side_val,
                    "price": avg_px,
                    "filled_size": fill_sz,
                    "state": o.get('state', ''),
                    "time": dt
                })
        return trades


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)