chore: remove legacy scripts and update SKILL.md to reference CLI only

This commit is contained in:
2026-04-16 03:03:25 +08:00
parent 7abdb30d6e
commit 863d10b872
11 changed files with 71 additions and 605 deletions

View File

@@ -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 |
---

View File

@@ -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

View File

@@ -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:

View File

@@ -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/`

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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__")

View File

@@ -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__")

View File

@@ -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__")

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
python3 "$HOME/.hermes/skills/coinhunter/scripts/rotate_external_gate_log.py"