Skill: wall-nut417/polymarket-telegram-picks (version 1.0.0)

--- name: polymarket-telegram-picks description: 每日拉取 Polymarket 体育/赛事赔率，经 AI 分析后推送值得下注的推荐到 Telegram user-invocable: true --- # Polymarket 每日赔率分析与 Telegram 推送 ## 功能说明 当用户或定时任务触发「今日 Polymarket 分析」「Polymarket 赔率推送」「执行每日推荐」等请求时，按以下流程执行： 1. **拉取当日未开赛 NBA 赔率**：运行本技能目录下的 `scripts/fetch_polymarket.py`，获取当日（中国时间）、且尚未开赛的 Polymarket NBA 比赛及赔率数据。 2. **AI 分析**：根据脚本输出的结构化数据，结合你自己的判断（赔率价值、热度、流动性等），筛选出你认为值得下注的球队/选项，并简要说明理由。 3. **推送到 Telegram**：将你的分析结论（推荐列表 + 理由）通过 `scripts/send_telegram.py` 发送到用户配置的 Telegram 对话。 ## 执行步骤 1. 在技能目录下执行： ```bash python3 scripts/fetch_polymarket.py ``` 或使用工作区中本技能路径下的 `scripts/fetch_polymarket.py`。脚本会输出当日、未开赛的 NBA 比赛摘要（事件标题、开赛时间、选项与隐含概率等）。 2. 阅读脚本输出，从中挑选你认为有下注价值的项目（例如某方赔率被低估、或与你的判断存在价值差）。用简洁中文写出推荐列表与理由。 3. 将上述分析结论作为内容，调用推送脚本： ```bash python3 scripts/send_telegram.py "你的分析内容（推荐赛事 + 理由）" ``` 或通过 stdin 传入： ```bash echo "你的分析内容" | python3 scripts/send_telegram.py ``` ## 环境与配置 - **Telegram 推送**依赖环境变量（或在 `config/config.json` 中配置）： - `TELEGRAM_BOT_TOKEN`：Bot 的 token（由 @BotFather 获取） - `TELEGRAM_CHAT_ID`：接收消息的聊天 ID（私聊或群组） - 若未配置，`send_telegram.py` 会报错并提示配置方式；分析步骤仍可执行，仅推送会失败。 ## 定时任务（每日 8 点中国时间） 在 OpenClaw 中可使用 cron 在每天北京时间 8:00 触发一次本流程，例如： ```bash openclaw cron add --name "Polymarket每日推送" \ --cron "0 8 * * *" \ --session main \ --message "请执行今日 Polymarket 赔率分析并推送到 Telegram" ``` 若使用系统 crontab（不经过 OpenClaw 会话），可配置为 8 点运行 `scripts/run_daily.py`（该脚本会拉取数据、可选调用本地/远程 LLM 分析、再推送 Telegram），详见 README。 ## 输出格式建议 推送至 Telegram 的内容建议包含： - 日期 - 今日推荐项（赛事/问题 + 推荐选项 + 简要理由） - 可选：风险提示（预测市场有风险等） 分析结论完全由你（AI）基于当前赔率与上下文自行判断，无需与任何外部投注建议绑定。

path: README.md
content: # Polymarket 每日 NBA 赔率 → AI 分析 → Telegram 推送（OpenClaw Skill）

本仓库是一个 **OpenClaw Skill**，实现：每天中国时间 8 点拉取 Polymarket **当日、未开赛的 NBA 比赛**赔率，经 AI 分析后，将「值得下注」的推荐推送到你的 Telegram。

## 项目结构

```
polymarket-telegram-picks/
├── SKILL.md                    # 技能说明与调用方式（OpenClaw 必读）
├── README.md                   # 本说明与部署流程
├── config/
│   ├── config.example.json     # 配置示例（复制为 config.json 并填写）
│   └── config.json             # 实际配置（勿提交到版本库）
└── scripts/
    ├── fetch_polymarket.py     # 拉取当日、未开赛的 NBA 比赛与赔率
    ├── send_telegram.py        # 将文本推送到 Telegram
    └── run_daily.py            # 一键执行：拉取 + 推送（可选用于 crontab）
```

## 依赖

- Python 3.8+
- 无需额外 pip 包（仅用标准库 `urllib.request`、`json` 等）

## 配置

### 1. Telegram

- 在 Telegram 中找 **@BotFather** 创建 Bot，获得 `TELEGRAM_BOT_TOKEN`。
- 与你的 Bot 先发一条消息（例如 `/start`），然后访问（把 `BOT_TOKEN` 换成你的 token）：
  `https://api.telegram.org/bot<BOT_TOKEN>/getUpdates`
  在返回的 JSON 里找到 `message.chat.id`，即为 `TELEGRAM_CHAT_ID`。

### 2. 配置方式二选一

- **环境变量**（推荐，便于 OpenClaw/cron 使用）：
  - `TELEGRAM_BOT_TOKEN`
  - `TELEGRAM_CHAT_ID`
- **配置文件**：复制 `config/config.example.json` 为 `config/config.json`，填入上述两个字段（键名可用 `telegram_bot_token` / `telegram_chat_id`）。

## 在 OpenClaw 上部署与使用

### 1. 安装 Skill

将本目录放到 OpenClaw 的 Skills 目录下，任选一种：

- **工作区 Skills**（项目内使用）  
  复制到当前工作区的 skills 目录，例如：
  ```bash
  cp -r polymarket-telegram-picks /path/to/your/openclaw/workspace/skills/
  ```
  若使用 `openclaw.json` 的 `skills.load.extraDirs`，则把「本目录的父路径」加入，例如：
  ```json
  "skills": {
    "load": {
      "extraDirs": ["/path/to/polymarket-telegram-picks的父目录"]
    }
  }
  ```
  这样 OpenClaw 会扫描到 `polymarket-telegram-picks` 下的 `SKILL.md`。

- **本地/全局 Skills**  
  复制到 `~/.openclaw/skills/`：
  ```bash
  cp -r polymarket-telegram-picks ~/.openclaw/skills/
  ```

### 2. 注入 Telegram 配置（可选）

若用配置文件，确保 OpenClaw 运行时可访问到该目录下的 `config/config.json`。  
若用环境变量，可在 `~/.openclaw/openclaw.json` 的 `skills.entries` 里为本 skill 单独加 `env`，例如：

```json
"skills": {
  "entries": {
    "polymarket-telegram-picks": {
      "enabled": true,
      "env": {
        "TELEGRAM_BOT_TOKEN": "你的 token",
        "TELEGRAM_CHAT_ID": "你的 chat_id"
      }
    }
  }
}
```

（若 skill 名称与目录名不一致，以 `SKILL.md` 里 `name` 为准。）

### 3. 刷新 Skills

- 若开启了 `skills.load.watch`，保存/复制好文件后等待几秒即可。
- 或重启 OpenClaw Gateway，或按文档执行「刷新 skills」操作。

### 4. 手动触发一次

在 OpenClaw 对话里对助手说，例如：

- 「执行今日 Polymarket 赔率分析并推送到 Telegram」
- 「Polymarket 每日推荐」
- 「请执行今日 Polymarket 分析并推送到 Telegram」

助手会按 `SKILL.md` 的说明：运行 `scripts/fetch_polymarket.py` → 根据输出做 AI 分析 → 再运行 `scripts/send_telegram.py` 把推荐发到你的 Telegram。

### 5. 每天中国时间 8 点自动执行（OpenClaw Cron）

在装有 OpenClaw 的机器上执行（时区按你当前环境，下面用 `Asia/Shanghai` 表示中国时间）：

```bash
openclaw cron add --name "Polymarket每日推送" \
  --cron "0 8 * * *" \
  --session main \
  --message "请执行今日 Polymarket 赔率分析并推送到 Telegram"
```

若你的 OpenClaw 支持为 cron 指定时区，可加上，例如：

```bash
openclaw cron add --name "Polymarket每日推送" \
  --cron "0 8 * * *" \
  --tz "Asia/Shanghai" \
  --session main \
  --message "请执行今日 Polymarket 赔率分析并推送到 Telegram"
```

这样每天 8 点会向主会话发一条上述消息，助手会执行本 skill 的流程并推送到 Telegram。

### 6. 不用 OpenClaw、仅用系统 Crontab（仅推送原始摘要）

若你只想「每天 8 点拉取 Polymarket 并直接把原始摘要推到 Telegram」（不做 AI 分析），可用本仓库自带的脚本 + 系统 cron：

```bash
# 中国时间 8:00 = UTC 0:00（冬令时）或 UTC 1:00（夏令时），请按你服务器时区调整
0 0 * * * TELEGRAM_BOT_TOKEN=xxx TELEGRAM_CHAT_ID=xxx POLYMARKET_DAILY_RAW_ONLY=1 /usr/bin/python3 /path/to/polymarket-telegram-picks/scripts/run_daily.py
```

此时不会经过 AI，推送的是 `fetch_polymarket.py` 的原始摘要。若要 AI 分析，请使用 OpenClaw 的定时任务（上面第 5 步）。

## 小结

| 步骤 | 说明 |
|------|------|
| 1 | 复制本目录到 OpenClaw 的 skills 目录或配置 extraDirs |
| 2 | 配置 `TELEGRAM_BOT_TOKEN`、`TELEGRAM_CHAT_ID`（环境变量或 config.json） |
| 3 | 刷新/重启 OpenClaw，使 skill 被加载 |
| 4 | 对话中触发「今日 Polymarket 分析并推送到 Telegram」做一次验证 |
| 5 | 用 `openclaw cron add` 添加每天 8 点（中国时间）的定时任务 |

分析结论完全由 OpenClaw 助手根据当日赔率与上下文自行判断，本 skill 只负责拉取数据与推送，不保证任何投注建议。


path: scripts/run_daily.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
每日一键执行：拉取 Polymarket 赔率 → 可选 AI 分析 → 推送到 Telegram。
适用于系统 crontab 或单独执行，不经过 OpenClaw 会话。
若需「AI 分析」，请使用 OpenClaw 定时任务触发技能，由助手执行 fetch → 分析 → send_telegram。
"""
import os
import subprocess
import sys
from pathlib import Path

SKILL_ROOT = Path(__file__).resolve().parent.parent
FETCH_SCRIPT = SKILL_ROOT / "scripts" / "fetch_polymarket.py"
SEND_SCRIPT = SKILL_ROOT / "scripts" / "send_telegram.py"


def main():
    if not FETCH_SCRIPT.is_file():
        print(f"未找到脚本: {FETCH_SCRIPT}", file=sys.stderr)
        sys.exit(1)
    result = subprocess.run(
        [sys.executable, str(FETCH_SCRIPT)],
        capture_output=True,
        text=True,
        cwd=str(SKILL_ROOT),
        timeout=60,
    )
    summary = result.stdout or "(无输出)"
    if result.returncode != 0:
        summary = (result.stderr or result.stdout or "拉取失败") + "\n\n" + summary

    # 若不配置 LLM，则只把原始摘要推送到 Telegram；用户可改为在 OpenClaw 中由 AI 分析后再推送
    if os.environ.get("POLYMARKET_DAILY_RAW_ONLY") == "1" or not os.environ.get("OPENAI_API_KEY"):
        message = f"【Polymarket 每日摘要】\n\n{summary}"
    else:
        # 可选：在此调用本地/远程 LLM 对 summary 做简短分析，再赋值给 message
        message = f"【Polymarket 每日摘要】\n\n{summary}"

    if not SEND_SCRIPT.is_file():
        print("未找到 send_telegram.py，仅输出摘要：\n", summary)
        return
    send = subprocess.run(
        [sys.executable, str(SEND_SCRIPT), message],
        cwd=str(SKILL_ROOT),
        timeout=15,
    )
    sys.exit(send.returncode)


if __name__ == "__main__":
    main()


path: scripts/fetch_polymarket.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
从 Polymarket Gamma API 拉取「当日、未开赛」的 NBA 比赛及赔率，输出为便于 AI 分析的文本摘要。
无需 API Key，可直接运行。
"""
import json
import sys
from datetime import datetime, timezone, timedelta
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError

GAMMA_BASE = "https://gamma-api.polymarket.com"
CHINA_TZ_OFFSET = timedelta(hours=8)

# Polymarket 体育元数据中 NBA 的 tag_id（/sports 返回的 "nba" 项里 tags 含 745）
NBA_TAG_ID = 745


def parse_utc(ts):
    """将 API 返回的时间转为 UTC datetime。"""
    if ts is None:
        return None
    try:
        if isinstance(ts, (int, float)):
            return datetime.fromtimestamp(ts, tz=timezone.utc)
        s = str(ts).replace("Z", "+00:00")
        return datetime.fromisoformat(s)
    except Exception:
        return None


def fetch_nba_events_today(limit=80):
    """
    拉取 NBA 活动事件（tag_id=745），返回列表供后续按「当日、未开赛」过滤。
    若 API 支持 start_date_min/max 则传入今日中国时间对应的 UTC 范围以缩小数据量。
    """
    now_utc = datetime.now(timezone.utc)
    today_china = (now_utc + CHINA_TZ_OFFSET).date()
    start_utc = datetime(today_china.year, today_china.month, today_china.day, 0, 0, 0, tzinfo=timezone.utc) - CHINA_TZ_OFFSET
    end_utc = start_utc + timedelta(days=1) - timedelta(seconds=1)
    start_iso = start_utc.isoformat().replace("+00:00", "Z")
    end_iso = end_utc.isoformat().replace("+00:00", "Z")

    params = [
        f"limit={limit}",
        "active=true",
        "closed=false",
        f"tag_id={NBA_TAG_ID}",
        "order=startDate",
        "ascending=true",
    ]
    # 部分 Gamma API 支持事件按日期范围筛选；若不支持则用无日期参数的请求再过滤
    url_with_dates = f"{GAMMA_BASE}/events?{'&'.join(params + [f'start_date_min={start_iso}', f'start_date_max={end_iso}'])}"
    url_fallback = f"{GAMMA_BASE}/events?{'&'.join(params)}"
    req_headers = {"Accept": "application/json"}
    raw = None
    for url in (url_with_dates, url_fallback):
        try:
            req = Request(url, headers=req_headers)
            with urlopen(req, timeout=30) as r:
                raw = json.loads(r.read().decode())
            break
        except (URLError, HTTPError):
            continue
    if raw is None:
        raise URLError("无法拉取 Polymarket NBA 事件列表")
    # 本地二次过滤：只保留「中国时间当日」且「未开赛」
    result = []
    for ev in raw:
        start = ev.get("startDate") or ev.get("start_date")
        start_dt = parse_utc(start)
        if start_dt is None:
            continue
        local_dt = start_dt + CHINA_TZ_OFFSET
        if local_dt.date() != today_china:
            continue
        if start_dt <= now_utc:
            continue
        result.append(ev)
    return result


def format_outcome_prices(outcomes_str, prices_str):
    """解析 outcomes 和 outcomePrices（均为 JSON 字符串）。"""
    try:
        outcomes = json.loads(outcomes_str) if isinstance(outcomes_str, str) else outcomes_str
        prices = json.loads(prices_str) if isinstance(prices_str, str) else prices_str
    except (json.JSONDecodeError, TypeError):
        return []
    if not outcomes or not prices:
        return []
    return list(zip(outcomes, prices))


def main():
    try:
        events = fetch_nba_events_today()
    except (URLError, HTTPError) as e:
        print(f"[错误] 拉取 Polymarket NBA 数据失败: {e}", file=sys.stderr)
        sys.exit(1)

    now_utc = datetime.now(timezone.utc)
    today_str = (now_utc + CHINA_TZ_OFFSET).strftime("%Y-%m-%d")

    lines = [
        f"# Polymarket 当日未开赛 NBA 比赛（UTC+8 日期: {today_str}）",
        "",
        "以下为今日、且尚未开赛的 NBA 相关市场及赔率（隐含概率），可作为下注参考。",
        "",
    ]

    count = 0
    for ev in events:
        start = ev.get("startDate") or ev.get("start_date")
        start_dt = parse_utc(start)
        title = ev.get("title") or "无标题"
        markets = ev.get("markets") or ev.get("groupItemMarkets") or []
        if not markets:
            continue

        count += 1
        start_desc = start_dt.strftime("%Y-%m-%d %H:%M UTC") if start_dt else "—"
        lines.append(f"## {title}")
        lines.append(f"开赛时间: {start_desc}")
        lines.append("")
        for m in markets[:20]:
            q = m.get("question") or m.get("groupItemTitle") or "无题目"
            outcomes_str = m.get("outcomes")
            prices_str = m.get("outcomePrices")
            if not outcomes_str or not prices_str:
                lines.append(f"- {q}: (无赔率)")
                continue
            pairs = format_outcome_prices(outcomes_str, prices_str)
            parts = [f"{name} {float(p)*100:.1f}%" for name, p in pairs if p]
            lines.append(f"- {q}")
            lines.append(f"  选项与隐含概率: {', '.join(parts)}")
        lines.append("")

    if count == 0:
        lines.append("今日暂无符合条件的未开赛 NBA 比赛。")
    else:
        lines.append(f"--- 共 {count} 场当日未开赛 NBA 相关事件。")

    print("\n".join(lines))


if __name__ == "__main__":
    main()


path: scripts/send_telegram.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
将文本消息发送到配置的 Telegram 聊天。
支持从命令行参数或 stdin 读取内容。
需要环境变量 TELEGRAM_BOT_TOKEN、TELEGRAM_CHAT_ID，或 config/config.json。
"""
import json
import os
import sys
from pathlib import Path

# 技能根目录（脚本在 scripts/ 下）
SKILL_ROOT = Path(__file__).resolve().parent.parent
CONFIG_PATH = SKILL_ROOT / "config" / "config.json"


def get_config():
    """从环境变量或 config.json 读取配置。"""
    [REDACTED_SECRET]"TELEGRAM_BOT_TOKEN")
    chat_id = os.environ.get("TELEGRAM_CHAT_ID")
    if token and chat_id:
        return token.strip(), chat_id.strip()
    if CONFIG_PATH.is_file():
        try:
            with open(CONFIG_PATH, "r", encoding="utf-8") as f:
                data = json.load(f)
            [REDACTED_SECRET]"telegram_bot_token") or data.get("TELEGRAM_BOT_TOKEN") or "").strip()
            chat_id = (data.get("telegram_chat_id") or data.get("TELEGRAM_CHAT_ID") or "").strip()
            if token and chat_id:
                return token, chat_id
        except Exception:
            pass
    return None, None


def send_message(text: str, token: str, chat_id: str) -> bool:
    """通过 Telegram Bot API 发送文本。"""
    url = f"https://api.telegram.org/bot{token}/sendMessage"
    try:
        from urllib.request import urlopen, Request
        from urllib.parse import urlencode
        body = urlencode({"chat_id": chat_id, "text": text, "disable_web_page_preview": "true"}).encode()
        req = Request(url, data=body, method="POST", headers={"Content-Type": "application/x-www-form-urlencoded"})
        with urlopen(req, timeout=15) as r:
            resp = json.loads(r.read().decode())
            return resp.get("ok") is True
    except Exception as e:
        print(f"[错误] 发送 Telegram 失败: {e}", file=sys.stderr)
        return False


def main():
    token, chat_id = get_config()
    if not token or not chat_id:
        print(
            "未配置 Telegram。请设置环境变量 TELEGRAM_BOT_TOKEN 与 TELEGRAM_CHAT_ID，"
            "或在 config/config.json 中配置 telegram_bot_token、telegram_chat_id。",
            file=sys.stderr,
        )
        sys.exit(1)

    if len(sys.argv) > 1:
        text = " ".join(sys.argv[1:])
    else:
        text = sys.stdin.read().strip()

    if not text:
        print("无内容可发送。请通过参数或 stdin 传入消息。", file=sys.stderr)
        sys.exit(1)

    # Telegram 单条消息长度限制约 4096
    if len(text) > 4000:
        text = text[:3997] + "..."

    if send_message(text, token, chat_id):
        print("已推送到 Telegram。", file=sys.stderr)
    else:
        sys.exit(1)


if __name__ == "__main__":
    main()


path: config/config.example.json
content: telegram_bot_token: 你的 Bot Token（从 @BotFather 获取）
telegram_chat_id: 你的 Chat ID（与 Bot 对话后从 getUpdates 或机器人获取）

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