Skill: yyu812707-wq/binance-funding-monitor (version 1.0.3)

--- name: binance-funding-monitor description: 币安资金费率套利监控工具 - 查看账户、持仓、盈亏统计，SkillPay收费版 version: 1.0.3 author: partner --- # Binance Funding Monitor (SkillPay) 币安资金费率套利监控工具 - 按次付费版 ## 价格 - 每次调用: **1 USDT** - 支付方式: Crypto 钱包 (Web3) ## 前置要求 ### 必需环境变量 - `BINANCE_API_KEY` - 币安 API Key - `BINANCE_API_SECRET` - 币安 API Secret ### 可选环境变量 - `SKILLPAY_API_KEY` - SkillPay API Key（平台自动注入） - `SKILLPAY_ENDPOINT` - SkillPay API 端点 ## 功能 - `get_account_summary` - 账户总览（权益、保证金、余额） - `get_positions` - 当前持仓列表 - `get_funding_income` - 近7天资金费收入 - `get_full_report` - 完整监控报告 ## 配置示例 ```bash export BINANCE_API_KEY="your_api_key" export BINANCE_API_SECRET="your_api_secret" ``` ## 免责声明 本工具仅供监控使用，不构成投资建议。

path: README.md
content: # 币安资金费率套利监控 (SkillPay版)

通过 SkillPay 按次付费使用币安账户监控服务。

## 价格

- 每次调用: **1 USDT**
- 支付方式: Crypto 钱包 (Web3)

## 前置要求

使用本工具前，需要配置以下环境变量：

### 必需（用户自己的币安 API）
```bash
export BINANCE_API_KEY="your_binance_api_key"
export BINANCE_API_SECRET="your_binance_api_secret"
```

### 可选（SkillPay 配置，通常由平台自动注入）
```bash
export SKILLPAY_API_KEY="your_skillpay_api_key"
export SKILLPAY_ENDPOINT="https://api.skillpay.me/v1"
```

## 功能

1. **账户总览** - 总权益、已用保证金、可用余额
2. **持仓监控** - 当前持仓、方向、浮动盈亏
3. **资金费统计** - 近7天资金费收入
4. **完整报告** - 一键获取所有信息

## 使用方法

1. 配置币安 API Key
2. 安装 Skill: `clawhub install binance-funding-monitor`
3. 调用功能，按提示支付 1 USDT
4. 获取监控数据

## 免责声明

本工具仅供监控使用，不构成投资建议。交易有风险，投资需谨慎。

path: funding_arbitrage.py
content: #!/usr/bin/env python3
"""
币安资金费率套利策略 (Binance Funding Rate Arbitrage)
策略：做多负资金费率币种 + 做空正资金费率币种，赚取资金费差价
"""

import os
import sys
import json
import time
import logging
import asyncio
from datetime import datetime, timedelta
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
from decimal import Decimal, ROUND_DOWN

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('/Users/zc/.openclaw/workspace/logs/funding_arbitrage.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger('funding_arbitrage')

@dataclass
class Position:
    symbol: str
    side: str  # 'LONG' or 'SHORT'
    margin: float  # 保证金
    notional: float  # 名义价值
    entry_price: float
    funding_rate: float
    unrealized_pnl: float = 0

class FundingRateArbitrage:
    """资金费率套利策略"""
    
    # 风控参数
    PARAMS = {
        'margin_per_position': 3.0,      # 每币种保证金 3U
        'leverage': 10,                   # 10x 杠杆
        'notional_per_position': 30.0,    # 名义价值 30U
        'max_total_margin': 60.0,         # 最大总保证金 60U
        'cash_buffer_ratio': 0.5,         # 50% 现金缓冲
        'max_positions': 6,               # 最大持仓币种数
        'min_funding_rate_abs': 0.001,    # 最小资金费率绝对值 0.1%
        'stop_loss_threshold': -20.0,     # 单币种止损线 -20U
        'top_n_symbols': 20,              # 监控前20交易对
    }
    
    def __init__(self):
        self.[REDACTED_SECRET]'BINANCE_API_KEY')
        self.api_secret = os.getenv('BINANCE_API_SECRET')
        self.base_url = 'https://fapi.binance.com'  # 合约API
        self.positions: Dict[str, Position] = {}
        self.traded_symbols: set = set()  # 记录已交易币种，避免重复
        
        if not self.api_key or not self.api_secret:
            raise ValueError("缺少 BINANCE_API_KEY 或 BINANCE_API_SECRET 环境变量")
    
    def _generate_signature(self, query_string: str) -> str:
        """生成API签名"""
        import hmac
        import hashlib
        return hmac.new(
            self.api_secret.encode('utf-8'),
            query_string.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()
    
    def _request(self, method: str, endpoint: str, params: dict = None, signed: bool = False) -> dict:
        """发送HTTP请求"""
        import requests
        
        url = f"{self.base_url}{endpoint}"
        headers = {'X-MBX-APIKEY': self.api_key}
        
        if params is None:
            params = {}
        
        if signed:
            params['timestamp'] = int(time.time() * 1000)
            params['recvWindow'] = 5000
            query = '&'.join([f"{k}={v}" for k, v in params.items()])
            params['signature'] = self._generate_signature(query)
        
        try:
            if method == 'GET':
                response = requests.get(url, headers=headers, params=params, timeout=10)
            elif method == 'POST':
                response = requests.post(url, headers=headers, data=params, timeout=10)
            elif method == 'DELETE':
                response = requests.delete(url, headers=headers, data=params, timeout=10)
            else:
                raise ValueError(f"不支持的HTTP方法: {method}")
            
            response.raise_for_status()
            return response.json()
        except Exception as e:
            logger.error(f"API请求失败: {url}, 错误: {e}")
            raise
    
    def get_funding_rates(self) -> List[dict]:
        """获取所有合约的资金费率"""
        try:
            data = self._request('GET', '/fapi/v1/premiumIndex')
            # 过滤有效数据
            rates = []
            for item in data:
                if 'symbol' in item and 'lastFundingRate' in item:
                    try:
                        rate = float(item['lastFundingRate'])
                        rates.append({
                            'symbol': item['symbol'],
                            'rate': rate,
                            'mark_price': float(item.get('markPrice', 0)),
                            'next_funding_time': item.get('nextFundingTime', 0)
                        })
                    except (ValueError, TypeError):
                        continue
            return rates
        except Exception as e:
            logger.error(f"获取资金费率失败: {e}")
            return []
    
    def get_account_balance(self) -> Tuple[float, float]:
        """获取账户余额 (总权益, 可用余额)"""
        try:
            data = self._request('GET', '/fapi/v2/account', signed=True)
            total_balance = float(data.get('totalWalletBalance', 0))
            available = float(data.get('availableBalance', 0))
            return total_balance, available
        except Exception as e:
            logger.error(f"获取账户余额失败: {e}")
            return 0.0, 0.0
    
    def get_positions(self) -> List[dict]:
        """获取当前持仓"""
        try:
            data = self._request('GET', '/fapi/v2/positionRisk', signed=True)
            positions = []
            for pos in data:
                amt = float(pos.get('positionAmt', 0))
                if abs(amt) > 0:
                    positions.append({
                        'symbol': pos['symbol'],
                        'side': 'LONG' if amt > 0 else 'SHORT',
                        'amount': abs(amt),
                        'entry_price': float(pos.get('entryPrice', 0)),
                        'mark_price': float(pos.get('markPrice', 0)),
                        'unrealized_pnl': float(pos.get('unrealizedProfit', 0)),
                        'margin': float(pos.get('isolatedMargin', 0)) or float(pos.get('positionInitialMargin', 0))
                    })
            return positions
        except Exception as e:
            logger.error(f"获取持仓失败: {e}")
            return []
    
    def select_symbols(self, rates: List[dict]) -> Tuple[List[str], List[str]]:
        """
        选币逻辑
        返回: (做空列表-正费率, 做多列表-负费率)
        """
        # 过滤掉已经交易过的币种和费率绝对值不够的
        min_rate = self.PARAMS['min_funding_rate_abs']
        
        eligible = [
            r for r in rates 
            if abs(r['rate']) >= min_rate and r['symbol'] not in self.traded_symbols
        ]
        
        # 按费率绝对值排序
        eligible.sort(key=lambda x: abs(x['rate']), reverse=True)
        
        # 做空：正费率（资金费率为正，做空方收资金费）
        short_candidates = [r['symbol'] for r in eligible if r['rate'] > 0]
        
        # 做多：负费率（资金费率为负，做多方收资金费）
        long_candidates = [r['symbol'] for r in eligible if r['rate'] < 0]
        
        # 限制数量
        max_pos = self.PARAMS['max_positions']
        current_positions = len(self.positions)
        available_slots = max_pos - current_positions
        
        # 平均分配多空仓位
        slots_per_side = available_slots // 2
        
        return short_candidates[:slots_per_side], long_candidates[:slots_per_side]
    
    def calculate_position_size(self, symbol: str, mark_price: float) -> float:
        """计算开仓数量"""
        notional = self.PARAMS['notional_per_position']
        quantity = notional / mark_price if mark_price > 0 else 0
        
        # 获取交易对精度
        try:
            info = self._request('GET', '/fapi/v1/exchangeInfo')
            for s in info.get('symbols', []):
                if s['symbol'] == symbol:
                    qty_precision = int(s.get('quantityPrecision', 3))
                    return round(quantity, qty_precision)
        except:
            pass
        
        return round(quantity, 3)
    
    def open_position(self, symbol: str, side: str, quantity: float) -> bool:
        """开仓"""
        try:
            # 设置杠杆
            self._request('POST', '/fapi/v1/leverage', {
                'symbol': symbol,
                'leverage': self.PARAMS['leverage']
            }, signed=True)
            
            # 下单
            order_side = 'BUY' if side == 'LONG' else 'SELL'
            params = {
                'symbol': symbol,
                'side': order_side,
                'type': 'MARKET',
                'quantity': quantity,
                'positionSide': 'BOTH'  # 单向持仓模式
            }
            
            result = self._request('POST', '/fapi/v1/order', params, signed=True)
            
            if result.get('status') == 'FILLED':
                logger.info(f"开仓成功: {symbol} {side} 数量:{quantity}")
                self.traded_symbols.add(symbol)
                return True
            else:
                logger.warning(f"开仓状态: {result}")
                return False
                
        except Exception as e:
            logger.error(f"开仓失败 {symbol} {side}: {e}")
            return False
    
    def close_position(self, symbol: str, side: str, quantity: float) -> bool:
        """平仓"""
        try:
            order_side = 'SELL' if side == 'LONG' else 'BUY'
            params = {
                'symbol': symbol,
                'side': order_side,
                'type': 'MARKET',
                'quantity': quantity,
                'reduceOnly': 'true'  # 只减仓
            }
            
            result = self._request('POST', '/fapi/v1/order', params, signed=True)
            
            if result.get('status') in ['FILLED', 'PARTIALLY_FILLED']:
                logger.info(f"平仓成功: {symbol} {side}")
                return True
            return False
            
        except Exception as e:
            logger.error(f"平仓失败 {symbol}: {e}")
            return False
    
    def check_stop_loss(self) -> List[dict]:
        """检查止损条件"""
        positions_to_close = []
        
        for pos in self.get_positions():
            # 检查浮亏
            if pos['unrealized_pnl'] <= self.PARAMS['stop_loss_threshold']:
                # 检查是否有资金费收入和盈利
                funding_pnl = self._estimate_funding_pnl(pos['symbol'], pos['side'])
                if funding_pnl + pos['unrealized_pnl'] > 0:
                    positions_to_close.append(pos)
                    logger.info(f"触发平仓: {pos['symbol']} 浮亏:{pos['unrealized_pnl']:.2f} 资金费:{funding_pnl:.2f}")
        
        return positions_to_close
    
    def _estimate_funding_pnl(self, symbol: str, side: str) -> float:
        """估算已收资金费（简化计算）"""
        # 这里需要查询历史资金费记录
        # 简化处理：实际实现需要调用 /fapi/v1/income 接口
        return 0
    
    def rebalance(self):
        """主策略循环"""
        logger.info("=== 开始资金费率套利调仓 ===")
        
        # 1. 获取账户状态
        total_balance, available = self.get_account_balance()
        logger.info(f"账户权益: {total_balance:.2f} USDT, 可用: {available:.2f} USDT")
        
        # 2. 检查风控
        used_margin = total_balance - available
        if used_margin > self.PARAMS['max_total_margin']:
            logger.warning(f"超过最大保证金限制: {used_margin:.2f} > {self.PARAMS['max_total_margin']}")
            return
        
        # 3. 获取持仓并检查止损
        current_positions = self.get_positions()
        self.positions = {p['symbol']: Position(
            symbol=p['symbol'],
            side=p['side'],
            margin=p.get('margin', 3),
            notional=abs(p['amount'] * p['mark_price']),
            entry_price=p['entry_price'],
            funding_rate=0,
            unrealized_pnl=p['unrealized_pnl']
        ) for p in current_positions}
        
        # 检查止损
        for pos in self.check_stop_loss():
            self.close_position(pos['symbol'], pos['side'], pos['amount'])
        
        # 4. 获取资金费率并选币
        rates = self.get_funding_rates()
        if not rates:
            logger.error("无法获取资金费率")
            return
        
        short_list, long_list = self.select_symbols(rates)
        logger.info(f"候选做空: {short_list}")
        logger.info(f"候选做多: {long_list}")
        
        # 5. 开新仓位
        current_count = len(self.positions)
        max_new = self.PARAMS['max_positions'] - current_count
        
        if max_new <= 0:
            logger.info("已达到最大持仓数，等待调仓")
            return
        
        # 多空均衡开仓
        slots = min(max_new // 2, len(short_list), len(long_list))
        
        for symbol in short_list[:slots]:
            rate_info = next((r for r in rates if r['symbol'] == symbol), None)
            if rate_info:
                qty = self.calculate_position_size(symbol, rate_info['mark_price'])
                if qty > 0:
                    self.open_position(symbol, 'SHORT', qty)
        
        for symbol in long_list[:slots]:
            rate_info = next((r for r in rates if r['symbol'] == symbol), None)
            if rate_info:
                qty = self.calculate_position_size(symbol, rate_info['mark_price'])
                if qty > 0:
                    self.open_position(symbol, 'LONG', qty)
        
        logger.info("=== 调仓完成 ===")


def main():
    """主函数"""
    trader = FundingRateArbitrage()
    
    # 每8小时执行一次（资金费结算周期）
    # 可以在 cron 中设置: 0 0,8,16 * * *
    trader.rebalance()


if __name__ == '__main__':
    main()


path: requirements.txt
content: mcp>=1.0.0
requests>=2.28.0
python-dotenv>=1.0.0

path: server.py
content: #!/usr/bin/env python3
"""
币安资金费率套利监控 - SkillPay 收费版 MCP Server
每次调用收费 0.001 USDT
"""

import os
import sys
import json
import asyncio
from typing import Any, Dict, List
from datetime import datetime

# MCP Server 框架
from mcp.server import Server
from mcp.types import TextContent

# SkillPay 支付验证 - 从环境变量读取
SKILLPAY_API_KEY = os.getenv("SKILLPAY_API_KEY", "")
SKILLPAY_ENDPOINT = os.getenv("SKILLPAY_ENDPOINT", "https://api.skillpay.me/v1")

# 币安监控模块 - 使用本地副本
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from funding_arbitrage import FundingRateArbitrage

app = Server("binance-funding-monitor")


async def verify_payment(session_id: str) -> bool:
    """验证用户是否已支付 - 使用 SkillPay API"""
    if not SKILLPAY_API_KEY or SKILLPAY_API_KEY == "YOUR_API_KEY_HERE":
        # 开发模式：跳过支付验证
        return True
    
    try:
        import aiohttp
        async with aiohttp.ClientSession() as session:
            async with session.post(
                f"{SKILLPAY_ENDPOINT}/verify",
                headers={
                    "Authorization": f"Bearer {SKILLPAY_API_KEY}",
                    "Content-Type": "application/json"
                },
                json={
                    "session_id": session_id,
                    "skill_name": "binance-funding-monitor",
                    "amount": "1",
                    "currency": "USDT"
                }
            ) as resp:
                if resp.status == 200:
                    result = await resp.json()
                    return result.get("verified", False)
                else:
                    print(f"SkillPay API 错误: {resp.status}", file=sys.stderr)
                    return False
    except Exception as e:
        print(f"Payment verification error: {e}", file=sys.stderr)
        return False


@app.call_tool()
async def get_account_summary(arguments: dict) -> List[TextContent]:
    """获取币安账户总览"""
    session_id = arguments.get("session_id", "")
    
    if not await verify_payment(session_id):
        return [TextContent(
            type="text",
            text="❌ 支付验证失败。请先在 SkillPay 完成支付 (0.001 USDT)。"
        )]
    
    try:
        trader = FundingRateArbitrage()
        total, available = trader.get_account_balance()
        used = total - available
        used_pct = (used / total * 100) if total > 0 else 0
        
        report = f"""📊 币安账户总览
━━━━━━━━━━━━━━━━━━━━
💰 总权益: {total:.2f} USDT
🔒 已用保证金: {used:.2f} USDT ({used_pct:.1f}%)
💵 可用余额: {available:.2f} USDT
━━━━━━━━━━━━━━━━━━━━"""
        
        return [TextContent(type="text", text=report)]
    except Exception as e:
        return [TextContent(type="text", text=f"❌ 获取账户信息失败: {e}")]


@app.call_tool()
async def get_positions(arguments: dict) -> List[TextContent]:
    """获取当前持仓"""
    session_id = arguments.get("session_id", "")
    
    if not await verify_payment(session_id):
        return [TextContent(
            type="text",
            text="❌ 支付验证失败。请先在 SkillPay 完成支付 (0.001 USDT)。"
        )]
    
    try:
        trader = FundingRateArbitrage()
        positions = trader.get_positions()
        
        if not positions:
            return [TextContent(type="text", text="📭 当前无持仓")]
        
        lines = [f"📈 当前持仓 ({len(positions)} 个)", "━━━━━━━━━━━━━━━━━━━━"]
        for p in positions:
            emoji = "🟢" if p['unrealized_pnl'] >= 0 else "🔴"
            lines.append(f"{p['symbol']}: {p['side']} {emoji}{p['unrealized_pnl']:+.2f} USDT")
        lines.append("━━━━━━━━━━━━━━━━━━━━")
        
        return [TextContent(type="text", text="\n".join(lines))]
    except Exception as e:
        return [TextContent(type="text", text=f"❌ 获取持仓失败: {e}")]


@app.call_tool()
async def get_funding_income(arguments: dict) -> List[TextContent]:
    """获取资金费收入统计"""
    session_id = arguments.get("session_id", "")
    days = arguments.get("days", 7)
    
    if not await verify_payment(session_id):
        return [TextContent(
            type="text",
            text="❌ 支付验证失败。请先在 SkillPay 完成支付 (0.001 USDT)。"
        )]
    
    try:
        import time
        trader = FundingRateArbitrage()
        
        end_time = int(time.time() * 1000)
        start_time = end_time - (days * 24 * 60 * 60 * 1000)
        
        data = trader._request('GET', '/fapi/v1/income', {
            'incomeType': 'FUNDING_FEE',
            'startTime': start_time,
            'endTime': end_time,
            'limit': 1000
        }, signed=True)
        
        total_income = sum(float(item.get('income', 0)) for item in data)
        
        report = f"""💰 资金费收入统计 (近{days}天)
━━━━━━━━━━━━━━━━━━━━
总计收入: {total_income:+.4f} USDT
━━━━━━━━━━━━━━━━━━━━"""
        
        return [TextContent(type="text", text=report)]
    except Exception as e:
        return [TextContent(type="text", text=f"❌ 获取资金费收入失败: {e}")]


@app.call_tool()
async def get_full_report(arguments: dict) -> List[TextContent]:
    """获取完整监控报告"""
    session_id = arguments.get("session_id", "")
    
    if not await verify_payment(session_id):
        return [TextContent(
            type="text",
            text="❌ 支付验证失败。请先在 SkillPay 完成支付 (0.001 USDT)。"
        )]
    
    try:
        import time
        trader = FundingRateArbitrage()
        
        # 账户
        total, available = trader.get_account_balance()
        used = total - available
        used_pct = (used / total * 100) if total > 0 else 0
        
        # 持仓
        positions = trader.get_positions()
        
        # 资金费
        end_time = int(time.time() * 1000)
        start_time = end_time - (7 * 24 * 60 * 60 * 1000)
        data = trader._request('GET', '/fapi/v1/income', {
            'incomeType': 'FUNDING_FEE',
            'startTime': start_time,
            'endTime': end_time,
            'limit': 1000
        }, signed=True)
        total_income = sum(float(item.get('income', 0)) for item in data)
        total_unrealized = sum(p['unrealized_pnl'] for p in positions)
        
        report_lines = [
            "📊 币安资金费率套利监控报告",
            f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
            "",
            "【账户状态】",
            f"总权益: {total:.2f} USDT",
            f"已用保证金: {used:.2f} USDT ({used_pct:.1f}%)",
            f"可用余额: {available:.2f} USDT",
            "",
            "【盈亏统计】(近7天)",
            f"资金费收入: {total_income:+.4f} USDT",
            f"持仓浮动盈亏: {total_unrealized:+.4f} USDT",
            f"合计盈亏: {total_income + total_unrealized:+.4f} USDT",
            "",
            f"【当前持仓】({len(positions)} 个)"
        ]
        
        for p in positions:
            emoji = "🟢" if p['unrealized_pnl'] >= 0 else "🔴"
            report_lines.append(f"{p['symbol']}: {p['side']} 浮动:{emoji}{p['unrealized_pnl']:+.2f}")
        
        if not positions:
            report_lines.append("(无持仓)")
        
        report_lines.append("")
        report_lines.append("✅ 数据获取成功")
        
        return [TextContent(type="text", text="\n".join(report_lines))]
    except Exception as e:
        return [TextContent(type="text", text=f"❌ 获取报告失败: {e}")]


async def main():
    from mcp.server.stdio import stdio_server
    
    async with stdio_server() as (read_stream, write_stream):
        await app.run(
            read_stream,
            write_stream,
            app.create_initialization_options()
        )


if __name__ == "__main__":
    asyncio.run(main())

path: skill.json
content: {
  "name": "binance-funding-monitor",
  "version": "1.0.3",
  "description": "币安资金费率套利监控工具 - 查看账户、持仓、盈亏统计，SkillPay收费版",
  "author": "partner",
  "license": "MIT",
  "keywords": ["binance", "crypto", "funding", "arbitrage", "监控", "skillpay"],
  "price": {
    "amount": "1",
    "currency": "USDT",
    "type": "per_call"
  },
  "entry": "server.py",
  "mcp": {
    "type": "stdio",
    "command": ["python3", "server.py"]
  },
  "env": {
    "required": [
      "BINANCE_API_KEY",
      "BINANCE_API_SECRET"
    ],
    "optional": [
      "SKILLPAY_API_KEY",
      "SKILLPAY_ENDPOINT"
    ]
  },
  "tools": [
    {
      "name": "get_account_summary",
      "description": "获取币安账户总览：权益、保证金、可用余额"
    },
    {
      "name": "get_positions",
      "description": "获取当前持仓列表及浮动盈亏"
    },
    {
      "name": "get_funding_income",
      "description": "获取近7天资金费收入统计"
    },
    {
      "name": "get_full_report",
      "description": "获取完整监控报告（账户+持仓+盈亏）"
    }
  ]
}

path: start.sh
content: #!/bin/bash
# 启动币安资金费率监控 Skill MCP Server

# 加载 SkillPay 配置
source ~/.config/skillpay/env.sh

# 加载币安 API 配置
source ~/.config/binance/env.sh

# 启动 MCP Server
cd ~/.openclaw/workspace/skills/binance-monitor-skillpay
exec python3 server.py

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