From 863d10b8722e67e6f5bec4453966e06705583348 Mon Sep 17 00:00:00 2001 From: Tacit Lab Date: Thu, 16 Apr 2026 03:03:25 +0800 Subject: [PATCH] chore: remove legacy scripts and update SKILL.md to reference CLI only --- README.md | 7 +- SKILL.md | 35 ++- references/auto-trading-guide.md | 2 + references/shim-templates.md | 16 ++ scripts/auto_trader.py | 277 -------------------- scripts/init_user_state.py | 63 ----- scripts/market_probe.py | 243 ----------------- templates/coinhunter_external_gate_shim.py | 10 + templates/coinhunter_precheck_shim.py | 10 + templates/coinhunter_review_context_shim.py | 10 + templates/rotate_external_gate_log_shim.sh | 3 + 11 files changed, 71 insertions(+), 605 deletions(-) create mode 100644 references/shim-templates.md delete mode 100644 scripts/auto_trader.py delete mode 100644 scripts/init_user_state.py delete mode 100644 scripts/market_probe.py create mode 100644 templates/coinhunter_external_gate_shim.py create mode 100644 templates/coinhunter_precheck_shim.py create mode 100644 templates/coinhunter_review_context_shim.py create mode 100644 templates/rotate_external_gate_log_shim.sh diff --git a/README.md b/README.md index 8bce9df..8a90e1e 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,10 @@ flowchart TD | `scripts/market_probe.py` | Market data fetcher (ccxt + web search) | | `scripts/coinhunter_precheck.py` | **Lightweight gate** — computes adaptive thresholds and decides if analysis is needed | | `scripts/coinhunter_external_gate.py` | Optional **system-crontab wrapper** that runs the gate entirely outside Hermes | -| `smart_executor.py` *(user runtime)* | Order execution layer with idempotency & precision validation | -| `logger.py` *(user runtime)* | Structured JSONL logging of every decision & trade | -| `review_engine.py` *(user runtime)* | Hourly quality review & parameter optimization | +| `scripts/coinhunter_cli.py` | Unified CLI entrypoint for CoinHunter operations | +| `scripts/smart_executor.py` | Order execution layer with idempotency & precision validation | +| `scripts/logger.py` | Structured JSONL logging of every decision & trade | +| `scripts/review_engine.py` | Hourly quality review & parameter optimization | --- diff --git a/SKILL.md b/SKILL.md index 1ec0139..2d84b36 100644 --- a/SKILL.md +++ b/SKILL.md @@ -35,7 +35,7 @@ Anchor all advice to the user's real balances, average costs, exchange, and curr 1. **Single-coin triage** — analyze a specific holding (mainstream or meme). 2. **Active discovery** — scan for the best short-term opportunity across both mainstream and meme sectors. -3. **Execution** — run the auto-trader, evaluate whether to hold, sell, or rebalance. +3. **Execution** — run trades via the CLI (`coinhunter exec`), evaluating whether to hold, sell, or rebalance. 4. **Review** — generate an hourly report on decision quality, PnL, and recommended parameter adjustments. ## Scientific analysis checklist (mandatory before every trade decision) @@ -53,7 +53,7 @@ Read `references/short-term-trading-framework.md` before every active decision p ## Workflow ### Discovery & Scanning -1. **Mainstream scan** — Use `market_probe.py bybit-ticker` or ccxt for liquid coins. +1. **Mainstream scan** — Use `coinhunter probe bybit-ticker` or ccxt for liquid coins. - Look for: breakouts, volume spikes, S/R flips, trend alignment. 2. **Meme scan** — Use `web_search` + `dex-search` / `gecko-search` for narrative heat. - Look for: accelerating attention, DEX flow, CEX listing rumors, social spread. @@ -64,11 +64,11 @@ Read `references/short-term-trading-framework.md` before every active decision p 2. Pull market data for holdings and candidates. 3. Run the 6-question scientific checklist. 4. Decide: **HOLD** / **SELL_ALL** / **REBALANCE** / **BUY**. -5. Execute via `smart_executor.py`. -6. Log the full decision context with `logger.py`. +5. Execute via the CLI (`coinhunter exec ...`). +6. Log the full decision context via the CLI execution. ### Review (every hour) -1. Run `review_engine.py` to analyze all decisions from the past hour. +1. Run `coinhunter recap` to analyze all decisions from the past hour. 2. Compare decision prices to current prices. 3. Flag patterns: missed runs, bad entries, over-trading, hesitation. 4. Output recommendations for parameter or blacklist adjustments. @@ -76,12 +76,13 @@ Read `references/short-term-trading-framework.md` before every active decision p ## Auto-trading architecture -| Component | Path | Purpose | -|-----------|------|---------| -| `smart_executor.py` | `~/.coinhunter/smart_executor.py` | Order execution layer (market buy/sell/rebalance) | -| `logger.py` | `~/.coinhunter/logger.py` | Records decisions, trades, and market snapshots | -| `review_engine.py` | `~/.coinhunter/review_engine.py` | Hourly quality review and optimization suggestions | -| `market_probe.py` | `~/.hermes/skills/coinhunter/scripts/market_probe.py` | Market data fetcher | +| CLI Command | Purpose | +|-------------|---------| +| `coinhunter exec` | Order execution layer (buy / flat / rotate / hold) | +| `coinhunter pre` | Lightweight threshold evaluator and trigger gate | +| `coinhunter review` | Generate compact review context for the agent | +| `coinhunter recap` | Hourly quality review and optimization suggestions | +| `coinhunter probe` | Market data fetcher | ### Execution schedule - **Trade bot** — runs every 15-30 minutes via `cronjob`. @@ -119,12 +120,8 @@ This pattern preserves Telegram auto-delivery from Hermes cron while reducing mo If you want to replicate this low-cost trigger architecture, here is a complete blueprint. #### 1. File layout -Create these files under `~/.hermes/scripts/` and state under `~/.coinhunter/state/`: +User runtime state lives under `~/.coinhunter/state/`: ``` -~/.hermes/scripts/ - coinhunter_precheck.py # lightweight threshold evaluator - coinhunter_external_gate.py # optional system-crontab wrapper - ~/.coinhunter/state/ precheck_state.json # last snapshot + trigger flags external_gate.lock # flock file for external gate @@ -223,8 +220,8 @@ Attach the precheck script as the `script` field of the cron job so its JSON out { "id": "coinhunter-trade", "schedule": "*/15 * * * *", - "prompt": "You are Coin Hunter. If the injected context says should_analyze=false, respond with exactly [SILENT] and do nothing. Otherwise, read ~/.coinhunter/positions.json, run the scientific checklist, decide HOLD/SELL/REBALANCE/BUY, and execute via smart_executor.py. After finishing, run ~/.hermes/scripts/coinhunter_precheck.py --ack to clear the trigger.", - "script": "~/.hermes/scripts/coinhunter_precheck.py", + "prompt": "You are Coin Hunter. If the injected context says should_analyze=false, respond with exactly [SILENT] and do nothing. Otherwise, read ~/.coinhunter/positions.json, run the scientific checklist, decide HOLD/SELL/REBALANCE/BUY, and execute via the `coinhunter` CLI. After finishing, run `coinhunter pre --ack` to clear the trigger.", + "script": "coinhunter_precheck.py", "deliver": "telegram", "model": "kimi-for-coding" } @@ -235,7 +232,7 @@ Add an `--ack` handler to the precheck script (or a separate ack script) that se #### 5. External gate (optional, for even lower cost) If you want to run the precheck every 5 minutes without waking Hermes at all: -`coinhunter_external_gate.py` pseudocode: +External gate pseudocode (run from `~/.hermes/scripts/`): ```python import fcntl, os, subprocess, json, sys diff --git a/references/auto-trading-guide.md b/references/auto-trading-guide.md index bd0dc5a..8e4bb41 100644 --- a/references/auto-trading-guide.md +++ b/references/auto-trading-guide.md @@ -2,6 +2,8 @@ Complete guide for building and running a hands-off meme-coin trading bot using the Binance Spot API. +> Note: CoinHunter code now lives in `~/.hermes/skills/coinhunter/scripts/` and is preferably invoked via `coinhunter_cli.py`. Treat any references in this guide to runtime copies under `~/.coinhunter/` or shell wrappers like `run_trader.sh` as legacy structure notes unless explicitly updated below. + ## Scope This guide covers: diff --git a/references/shim-templates.md b/references/shim-templates.md new file mode 100644 index 0000000..783d728 --- /dev/null +++ b/references/shim-templates.md @@ -0,0 +1,16 @@ +# CoinHunter shim templates + +These files are tiny compatibility shims for Hermes platform features that currently expect scripts under `~/.hermes/scripts/`. + +When needed, copy them like this: + +- `templates/coinhunter_precheck_shim.py` -> `~/.hermes/scripts/coinhunter_precheck.py` +- `templates/coinhunter_external_gate_shim.py` -> `~/.hermes/scripts/coinhunter_external_gate.py` +- `templates/coinhunter_review_context_shim.py` -> `~/.hermes/scripts/coinhunter_review_context.py` +- `templates/rotate_external_gate_log_shim.sh` -> `~/.hermes/scripts/rotate_external_gate_log.sh` + +The real business logic stays inside the skill under: +- `~/.hermes/skills/coinhunter/scripts/` + +The user runtime data stays under: +- `~/.coinhunter/` diff --git a/scripts/auto_trader.py b/scripts/auto_trader.py deleted file mode 100644 index 707ddff..0000000 --- a/scripts/auto_trader.py +++ /dev/null @@ -1,277 +0,0 @@ -#!/usr/bin/env python3 -""" -Coin Hunter Auto Trader Template -全自动妖币猎人 + 币安执行器 - -运行前请在 ~/.hermes/.env 配置: - BINANCE_API_KEY=你的API_KEY - BINANCE_API_SECRET=你的API_SECRET - -首次运行必须先用 DRY_RUN=true 测试逻辑! -""" -import json -import os -import sys -from datetime import datetime, timezone -from pathlib import Path - -import ccxt - -# ============== 配置 ============== -COINS_DIR = Path.home() / ".coinhunter" -POSITIONS_FILE = COINS_DIR / "positions.json" -ENV_FILE = Path.home() / ".hermes" / ".env" - -# 风控参数 -DRY_RUN = os.getenv("DRY_RUN", "true").lower() == "true" # 默认测试模式 -MAX_POSITIONS = 2 # 最大同时持仓数 - -# 资金配置(根据总资产动态计算) -CAPITAL_ALLOCATION_PCT = 0.95 # 用总资产95%玩这个策略(留95%缓冲给手续费和滑点) -MIN_POSITION_USDT = 50 # 单次最小下单金额(避免过小) - -MIN_VOLUME_24H = 1_000_000 # 最小24h成交额 ($) -MIN_PRICE_CHANGE_24H = 0.05 # 最小涨幅 5% -MAX_PRICE = 1.0 # 只玩低价币(meme特征) -STOP_LOSS_PCT = -0.07 # 止损 -7% -TAKE_PROFIT_1_PCT = 0.15 # 止盈1 +15% -TAKE_PROFIT_2_PCT = 0.30 # 止盈2 +30% -BLACKLIST = {"USDC", "BUSD", "TUSD", "FDUSD", "USTC", "PAXG", "XRP", "ETH", "BTC"} - -# ============== 工具函数 ============== -def log(msg: str): - print(f"[{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC] {msg}") - - -def load_positions() -> list: - if POSITIONS_FILE.exists(): - return json.loads(POSITIONS_FILE.read_text(encoding="utf-8")).get("positions", []) - return [] - - -def save_positions(positions: list): - COINS_DIR.mkdir(parents=True, exist_ok=True) - POSITIONS_FILE.write_text(json.dumps({"positions": positions}, indent=2, ensure_ascii=False), encoding="utf-8") - - -def load_env(): - if ENV_FILE.exists(): - for line in ENV_FILE.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if line and not line.startswith("#") and "=" in line: - key, val = line.split("=", 1) - os.environ.setdefault(key.strip(), val.strip()) - - -def calculate_position_size(total_usdt: float, available_usdt: float, open_slots: int) -> float: - """ - 根据总资产动态计算每次下单金额。 - 逻辑:先确定策略总上限,再按剩余开仓位均分。 - """ - strategy_cap = total_usdt * CAPITAL_ALLOCATION_PCT - used_in_strategy = max(0, strategy_cap - available_usdt) - remaining_strategy_cap = max(0, strategy_cap - used_in_strategy) - - if open_slots <= 0 or remaining_strategy_cap < MIN_POSITION_USDT: - return 0 - - size = remaining_strategy_cap / open_slots - size = min(size, available_usdt) - size = max(0, round(size, 2)) - return size if size >= MIN_POSITION_USDT else 0 - - -# ============== 币安客户端 ============== -class BinanceTrader: - def __init__(self): - api_key = os.getenv("BINANCE_API_KEY") - secret = os.getenv("BINANCE_API_SECRET") - if not api_key or not secret: - raise RuntimeError("缺少 BINANCE_API_KEY 或 BINANCE_API_SECRET,请配置 ~/.hermes/.env") - self.exchange = ccxt.binance({ - "apiKey": api_key, - "secret": secret, - "options": {"defaultType": "spot"}, - "enableRateLimit": True, - }) - self.exchange.load_markets() - - def get_balance(self, asset: str = "USDT") -> float: - bal = self.exchange.fetch_balance()["free"].get(asset, 0) - return float(bal) - - def fetch_tickers(self) -> dict: - return self.exchange.fetch_tickers() - - def create_market_buy_order(self, symbol: str, amount_usdt: float): - if DRY_RUN: - log(f"[DRY RUN] 模拟买入 {symbol},金额 ${amount_usdt}") - return {"id": "dry-run-buy", "price": None, "amount": amount_usdt} - ticker = self.exchange.fetch_ticker(symbol) - price = float(ticker["last"]) - qty = amount_usdt / price - order = self.exchange.create_market_buy_order(symbol, qty) - log(f"✅ 买入 {symbol} | 数量 {qty:.4f} | 价格 ~${price}") - return order - - def create_market_sell_order(self, symbol: str, qty: float): - if DRY_RUN: - log(f"[DRY RUN] 模拟卖出 {symbol},数量 {qty}") - return {"id": "dry-run-sell"} - order = self.exchange.create_market_sell_order(symbol, qty) - log(f"✅ 卖出 {symbol} | 数量 {qty:.4f}") - return order - - -# ============== 选币引擎 ============== -class CoinPicker: - def __init__(self, exchange: ccxt.binance): - self.exchange = exchange - - def scan(self) -> list: - tickers = self.exchange.fetch_tickers() - candidates = [] - for symbol, t in tickers.items(): - if not symbol.endswith("/USDT"): - continue - base = symbol.replace("/USDT", "") - if base in BLACKLIST: - continue - - price = float(t["last"] or 0) - change = float(t.get("percentage", 0)) / 100 - volume = float(t.get("quoteVolume", 0)) - - if price <= 0 or price > MAX_PRICE: - continue - if volume < MIN_VOLUME_24H: - continue - if change < MIN_PRICE_CHANGE_24H: - continue - - score = change * (volume / MIN_VOLUME_24H) - candidates.append({ - "symbol": symbol, - "base": base, - "price": price, - "change_24h": change, - "volume_24h": volume, - "score": score, - }) - - candidates.sort(key=lambda x: x["score"], reverse=True) - return candidates[:5] - - -# ============== 主控制器 ============== -def run_cycle(): - load_env() - trader = BinanceTrader() - picker = CoinPicker(trader.exchange) - positions = load_positions() - - log(f"当前持仓数: {len(positions)} | 最大允许: {MAX_POSITIONS} | DRY_RUN={DRY_RUN}") - - # 1. 检查现有持仓(止盈止损) - tickers = trader.fetch_tickers() - new_positions = [] - for pos in positions: - sym = pos["symbol"] - qty = float(pos["quantity"]) - cost = float(pos["avg_cost"]) - # ccxt tickers 使用 slash 格式,如 PENGU/USDT - sym_ccxt = sym.replace("USDT", "/USDT") if "/" not in sym else sym - ticker = tickers.get(sym_ccxt) - if not ticker: - new_positions.append(pos) - continue - - price = float(ticker["last"]) - pnl_pct = (price - cost) / cost - log(f"监控 {sym} | 现价 ${price:.8f} | 成本 ${cost:.8f} | 盈亏 {pnl_pct:+.2%}") - - action = None - if pnl_pct <= STOP_LOSS_PCT: - action = "STOP_LOSS" - elif pnl_pct >= TAKE_PROFIT_2_PCT: - action = "TAKE_PROFIT_2" - elif pnl_pct >= TAKE_PROFIT_1_PCT: - sold_pct = float(pos.get("take_profit_1_sold_pct", 0)) - if sold_pct == 0: - action = "TAKE_PROFIT_1" - - if action == "STOP_LOSS": - trader.create_market_sell_order(sym, qty) - log(f"🛑 {sym} 触发止损,全部清仓") - continue - - if action == "TAKE_PROFIT_1": - sell_qty = qty * 0.5 - trader.create_market_sell_order(sym, sell_qty) - pos["quantity"] = qty - sell_qty - pos["take_profit_1_sold_pct"] = 50 - pos["updated_at"] = datetime.now(timezone.utc).isoformat() - log(f"🎯 {sym} 触发止盈1,卖出50%,剩余 {pos['quantity']:.4f}") - new_positions.append(pos) - continue - - if action == "TAKE_PROFIT_2": - trader.create_market_sell_order(sym, float(pos["quantity"])) - log(f"🚀 {sym} 触发止盈2,全部清仓") - continue - - new_positions.append(pos) - - # 2. 开新仓 - if len(new_positions) < MAX_POSITIONS: - candidates = picker.scan() - held_bases = {p["base_asset"] for p in new_positions} - total_usdt = trader.get_balance("USDT") - available_usdt = total_usdt - open_slots = MAX_POSITIONS - len(new_positions) - position_size = calculate_position_size(total_usdt, available_usdt, open_slots) - - log(f"总资产 USDT: ${total_usdt:.2f} | 策略上限({CAPITAL_ALLOCATION_PCT:.0%}): ${total_usdt*CAPITAL_ALLOCATION_PCT:.2f} | 每仓建议金额: ${position_size:.2f}") - - for cand in candidates: - if len(new_positions) >= MAX_POSITIONS: - break - base = cand["base"] - if base in held_bases: - continue - if position_size <= 0: - log("策略资金已用完或余额不足,停止开新仓") - break - - symbol = cand["symbol"] - order = trader.create_market_buy_order(symbol, position_size) - avg_price = float(order.get("price") or cand["price"]) - qty = position_size / avg_price if avg_price else 0 - - new_positions.append({ - "account_id": "binance-main", - "symbol": symbol.replace("/", ""), - "base_asset": base, - "quote_asset": "USDT", - "market_type": "spot", - "quantity": qty, - "avg_cost": avg_price, - "opened_at": datetime.now(timezone.utc).isoformat(), - "updated_at": datetime.now(timezone.utc).isoformat(), - "note": "Auto-trader entry", - }) - held_bases.add(base) - available_usdt -= position_size - position_size = calculate_position_size(total_usdt, available_usdt, MAX_POSITIONS - len(new_positions)) - log(f"📈 新开仓 {symbol} | 买入价 ${avg_price:.8f} | 数量 {qty:.2f}") - - save_positions(new_positions) - log("周期结束,持仓已保存") - - -if __name__ == "__main__": - try: - run_cycle() - except Exception as e: - log(f"❌ 错误: {e}") - sys.exit(1) diff --git a/scripts/init_user_state.py b/scripts/init_user_state.py deleted file mode 100644 index 1465136..0000000 --- a/scripts/init_user_state.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -import json -from datetime import datetime, timezone -from pathlib import Path - -ROOT = Path.home() / ".coinhunter" -CACHE_DIR = ROOT / "cache" - - -def now_iso(): - return datetime.now(timezone.utc).replace(microsecond=0).isoformat() - - -def ensure_file(path: Path, payload: dict): - if path.exists(): - return False - path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - return True - - -def main(): - ROOT.mkdir(parents=True, exist_ok=True) - CACHE_DIR.mkdir(parents=True, exist_ok=True) - - created = [] - ts = now_iso() - - templates = { - ROOT / "config.json": { - "default_exchange": "bybit", - "default_quote_currency": "USDT", - "timezone": "Asia/Shanghai", - "preferred_chains": ["solana", "base"], - "created_at": ts, - "updated_at": ts, - }, - ROOT / "accounts.json": { - "accounts": [] - }, - ROOT / "positions.json": { - "positions": [] - }, - ROOT / "watchlist.json": { - "watchlist": [] - }, - ROOT / "notes.json": { - "notes": [] - }, - } - - for path, payload in templates.items(): - if ensure_file(path, payload): - created.append(str(path)) - - print(json.dumps({ - "root": str(ROOT), - "created": created, - "cache_dir": str(CACHE_DIR), - }, ensure_ascii=False, indent=2)) - - -if __name__ == "__main__": - main() diff --git a/scripts/market_probe.py b/scripts/market_probe.py deleted file mode 100644 index 2e41344..0000000 --- a/scripts/market_probe.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import json -import os -import sys -import urllib.parse -import urllib.request - -DEFAULT_TIMEOUT = 20 - - -def fetch_json(url, headers=None, timeout=DEFAULT_TIMEOUT): - merged_headers = { - "Accept": "application/json", - "User-Agent": "Mozilla/5.0 (compatible; OpenClaw Coin Hunter/1.0)", - } - if headers: - merged_headers.update(headers) - req = urllib.request.Request(url, headers=merged_headers) - with urllib.request.urlopen(req, timeout=timeout) as resp: - data = resp.read() - return json.loads(data.decode("utf-8")) - - -def print_json(data): - print(json.dumps(data, ensure_ascii=False, indent=2)) - - -def bybit_ticker(symbol: str): - url = ( - "https://api.bybit.com/v5/market/tickers?category=spot&symbol=" - + urllib.parse.quote(symbol.upper()) - ) - payload = fetch_json(url) - items = payload.get("result", {}).get("list", []) - if not items: - raise SystemExit(f"No Bybit spot ticker found for {symbol}") - item = items[0] - out = { - "provider": "bybit", - "symbol": symbol.upper(), - "lastPrice": item.get("lastPrice"), - "price24hPcnt": item.get("price24hPcnt"), - "highPrice24h": item.get("highPrice24h"), - "lowPrice24h": item.get("lowPrice24h"), - "turnover24h": item.get("turnover24h"), - "volume24h": item.get("volume24h"), - "bid1Price": item.get("bid1Price"), - "ask1Price": item.get("ask1Price"), - } - print_json(out) - - -def bybit_klines(symbol: str, interval: str, limit: int): - params = urllib.parse.urlencode({ - "category": "spot", - "symbol": symbol.upper(), - "interval": interval, - "limit": str(limit), - }) - url = f"https://api.bybit.com/v5/market/kline?{params}" - payload = fetch_json(url) - rows = payload.get("result", {}).get("list", []) - out = { - "provider": "bybit", - "symbol": symbol.upper(), - "interval": interval, - "candles": [ - { - "startTime": r[0], - "open": r[1], - "high": r[2], - "low": r[3], - "close": r[4], - "volume": r[5], - "turnover": r[6], - } - for r in rows - ], - } - print_json(out) - - -def dexscreener_search(query: str): - url = "https://api.dexscreener.com/latest/dex/search/?q=" + urllib.parse.quote(query) - payload = fetch_json(url) - pairs = payload.get("pairs") or [] - out = [] - for p in pairs[:10]: - out.append({ - "chainId": p.get("chainId"), - "dexId": p.get("dexId"), - "pairAddress": p.get("pairAddress"), - "url": p.get("url"), - "baseToken": p.get("baseToken"), - "quoteToken": p.get("quoteToken"), - "priceUsd": p.get("priceUsd"), - "liquidityUsd": (p.get("liquidity") or {}).get("usd"), - "fdv": p.get("fdv"), - "marketCap": p.get("marketCap"), - "volume24h": (p.get("volume") or {}).get("h24"), - "buys24h": ((p.get("txns") or {}).get("h24") or {}).get("buys"), - "sells24h": ((p.get("txns") or {}).get("h24") or {}).get("sells"), - }) - print_json({"provider": "dexscreener", "query": query, "pairs": out}) - - -def dexscreener_token(chain: str, address: str): - url = f"https://api.dexscreener.com/tokens/v1/{urllib.parse.quote(chain)}/{urllib.parse.quote(address)}" - payload = fetch_json(url) - pairs = payload if isinstance(payload, list) else payload.get("pairs") or [] - out = [] - for p in pairs[:10]: - out.append({ - "chainId": p.get("chainId"), - "dexId": p.get("dexId"), - "pairAddress": p.get("pairAddress"), - "baseToken": p.get("baseToken"), - "quoteToken": p.get("quoteToken"), - "priceUsd": p.get("priceUsd"), - "liquidityUsd": (p.get("liquidity") or {}).get("usd"), - "fdv": p.get("fdv"), - "marketCap": p.get("marketCap"), - "volume24h": (p.get("volume") or {}).get("h24"), - }) - print_json({"provider": "dexscreener", "chain": chain, "address": address, "pairs": out}) - - -def coingecko_search(query: str): - url = "https://api.coingecko.com/api/v3/search?query=" + urllib.parse.quote(query) - payload = fetch_json(url) - coins = payload.get("coins") or [] - out = [] - for c in coins[:10]: - out.append({ - "id": c.get("id"), - "name": c.get("name"), - "symbol": c.get("symbol"), - "marketCapRank": c.get("market_cap_rank"), - "thumb": c.get("thumb"), - }) - print_json({"provider": "coingecko", "query": query, "coins": out}) - - -def coingecko_coin(coin_id: str): - params = urllib.parse.urlencode({ - "localization": "false", - "tickers": "false", - "market_data": "true", - "community_data": "false", - "developer_data": "false", - "sparkline": "false", - }) - url = f"https://api.coingecko.com/api/v3/coins/{urllib.parse.quote(coin_id)}?{params}" - payload = fetch_json(url) - md = payload.get("market_data") or {} - out = { - "provider": "coingecko", - "id": payload.get("id"), - "symbol": payload.get("symbol"), - "name": payload.get("name"), - "marketCapRank": payload.get("market_cap_rank"), - "currentPriceUsd": (md.get("current_price") or {}).get("usd"), - "marketCapUsd": (md.get("market_cap") or {}).get("usd"), - "fullyDilutedValuationUsd": (md.get("fully_diluted_valuation") or {}).get("usd"), - "totalVolumeUsd": (md.get("total_volume") or {}).get("usd"), - "priceChangePercentage24h": md.get("price_change_percentage_24h"), - "priceChangePercentage7d": md.get("price_change_percentage_7d"), - "priceChangePercentage30d": md.get("price_change_percentage_30d"), - "circulatingSupply": md.get("circulating_supply"), - "totalSupply": md.get("total_supply"), - "maxSupply": md.get("max_supply"), - "homepage": (payload.get("links") or {}).get("homepage", [None])[0], - } - print_json(out) - - -def birdeye_token(address: str): - api_key = os.getenv("BIRDEYE_API_KEY") or os.getenv("BIRDEYE_APIKEY") - if not api_key: - raise SystemExit("Birdeye requires BIRDEYE_API_KEY in the environment") - url = "https://public-api.birdeye.so/defi/token_overview?address=" + urllib.parse.quote(address) - payload = fetch_json(url, headers={ - "x-api-key": api_key, - "x-chain": "solana", - }) - print_json({"provider": "birdeye", "address": address, "data": payload.get("data")}) - - -def build_parser(): - parser = argparse.ArgumentParser(description="Coin Hunter market data probe") - sub = parser.add_subparsers(dest="command", required=True) - - p = sub.add_parser("bybit-ticker", help="Fetch Bybit spot ticker") - p.add_argument("symbol") - - p = sub.add_parser("bybit-klines", help="Fetch Bybit spot klines") - p.add_argument("symbol") - p.add_argument("--interval", default="60", help="Bybit interval, e.g. 1, 5, 15, 60, 240, D") - p.add_argument("--limit", type=int, default=10) - - p = sub.add_parser("dex-search", help="Search DexScreener by query") - p.add_argument("query") - - p = sub.add_parser("dex-token", help="Fetch DexScreener token pairs by chain/address") - p.add_argument("chain") - p.add_argument("address") - - p = sub.add_parser("gecko-search", help="Search CoinGecko") - p.add_argument("query") - - p = sub.add_parser("gecko-coin", help="Fetch CoinGecko coin by id") - p.add_argument("coin_id") - - p = sub.add_parser("birdeye-token", help="Fetch Birdeye token overview (Solana)") - p.add_argument("address") - - return parser - - -def main(): - parser = build_parser() - args = parser.parse_args() - if args.command == "bybit-ticker": - bybit_ticker(args.symbol) - elif args.command == "bybit-klines": - bybit_klines(args.symbol, args.interval, args.limit) - elif args.command == "dex-search": - dexscreener_search(args.query) - elif args.command == "dex-token": - dexscreener_token(args.chain, args.address) - elif args.command == "gecko-search": - coingecko_search(args.query) - elif args.command == "gecko-coin": - coingecko_coin(args.coin_id) - elif args.command == "birdeye-token": - birdeye_token(args.address) - else: - parser.error("Unknown command") - - -if __name__ == "__main__": - main() diff --git a/templates/coinhunter_external_gate_shim.py b/templates/coinhunter_external_gate_shim.py new file mode 100644 index 0000000..07bd97f --- /dev/null +++ b/templates/coinhunter_external_gate_shim.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +"""Compatibility shim for external gate execution. +Real logic lives in the CoinHunter skill. +Copy this file to ~/.hermes/scripts/coinhunter_external_gate.py if needed. +""" +import runpy +from pathlib import Path + +TARGET = Path.home() / ".hermes" / "skills" / "coinhunter" / "scripts" / "coinhunter_external_gate.py" +runpy.run_path(str(TARGET), run_name="__main__") diff --git a/templates/coinhunter_precheck_shim.py b/templates/coinhunter_precheck_shim.py new file mode 100644 index 0000000..75bc6f4 --- /dev/null +++ b/templates/coinhunter_precheck_shim.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +"""Compatibility shim for Hermes cron script hook. +Real logic lives in the CoinHunter skill. +Copy this file to ~/.hermes/scripts/coinhunter_precheck.py if needed. +""" +import runpy +from pathlib import Path + +TARGET = Path.home() / ".hermes" / "skills" / "coinhunter" / "scripts" / "coinhunter_precheck.py" +runpy.run_path(str(TARGET), run_name="__main__") diff --git a/templates/coinhunter_review_context_shim.py b/templates/coinhunter_review_context_shim.py new file mode 100644 index 0000000..0db6680 --- /dev/null +++ b/templates/coinhunter_review_context_shim.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +"""Compatibility shim for review-context cron script hook. +Real logic lives in the CoinHunter skill. +Copy this file to ~/.hermes/scripts/coinhunter_review_context.py if needed. +""" +import runpy +from pathlib import Path + +TARGET = Path.home() / ".hermes" / "skills" / "coinhunter" / "scripts" / "coinhunter_review_context.py" +runpy.run_path(str(TARGET), run_name="__main__") diff --git a/templates/rotate_external_gate_log_shim.sh b/templates/rotate_external_gate_log_shim.sh new file mode 100644 index 0000000..e61dcc1 --- /dev/null +++ b/templates/rotate_external_gate_log_shim.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +python3 "$HOME/.hermes/skills/coinhunter/scripts/rotate_external_gate_log.py"