chore: remove legacy scripts and update SKILL.md to reference CLI only
This commit is contained in:
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
35
SKILL.md
35
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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
16
references/shim-templates.md
Normal file
16
references/shim-templates.md
Normal 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/`
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
10
templates/coinhunter_external_gate_shim.py
Normal file
10
templates/coinhunter_external_gate_shim.py
Normal 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__")
|
||||
10
templates/coinhunter_precheck_shim.py
Normal file
10
templates/coinhunter_precheck_shim.py
Normal 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__")
|
||||
10
templates/coinhunter_review_context_shim.py
Normal file
10
templates/coinhunter_review_context_shim.py
Normal 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__")
|
||||
3
templates/rotate_external_gate_log_shim.sh
Normal file
3
templates/rotate_external_gate_log_shim.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
python3 "$HOME/.hermes/skills/coinhunter/scripts/rotate_external_gate_log.py"
|
||||
Reference in New Issue
Block a user