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/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_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 |
|
| `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 |
|
| `scripts/coinhunter_cli.py` | Unified CLI entrypoint for CoinHunter operations |
|
||||||
| `logger.py` *(user runtime)* | Structured JSONL logging of every decision & trade |
|
| `scripts/smart_executor.py` | Order execution layer with idempotency & precision validation |
|
||||||
| `review_engine.py` *(user runtime)* | Hourly quality review & parameter optimization |
|
| `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).
|
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.
|
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.
|
4. **Review** — generate an hourly report on decision quality, PnL, and recommended parameter adjustments.
|
||||||
|
|
||||||
## Scientific analysis checklist (mandatory before every trade decision)
|
## 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
|
## Workflow
|
||||||
|
|
||||||
### Discovery & Scanning
|
### 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.
|
- Look for: breakouts, volume spikes, S/R flips, trend alignment.
|
||||||
2. **Meme scan** — Use `web_search` + `dex-search` / `gecko-search` for narrative heat.
|
2. **Meme scan** — Use `web_search` + `dex-search` / `gecko-search` for narrative heat.
|
||||||
- Look for: accelerating attention, DEX flow, CEX listing rumors, social spread.
|
- 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.
|
2. Pull market data for holdings and candidates.
|
||||||
3. Run the 6-question scientific checklist.
|
3. Run the 6-question scientific checklist.
|
||||||
4. Decide: **HOLD** / **SELL_ALL** / **REBALANCE** / **BUY**.
|
4. Decide: **HOLD** / **SELL_ALL** / **REBALANCE** / **BUY**.
|
||||||
5. Execute via `smart_executor.py`.
|
5. Execute via the CLI (`coinhunter exec ...`).
|
||||||
6. Log the full decision context with `logger.py`.
|
6. Log the full decision context via the CLI execution.
|
||||||
|
|
||||||
### Review (every hour)
|
### 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.
|
2. Compare decision prices to current prices.
|
||||||
3. Flag patterns: missed runs, bad entries, over-trading, hesitation.
|
3. Flag patterns: missed runs, bad entries, over-trading, hesitation.
|
||||||
4. Output recommendations for parameter or blacklist adjustments.
|
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
|
## Auto-trading architecture
|
||||||
|
|
||||||
| Component | Path | Purpose |
|
| CLI Command | Purpose |
|
||||||
|-----------|------|---------|
|
|-------------|---------|
|
||||||
| `smart_executor.py` | `~/.coinhunter/smart_executor.py` | Order execution layer (market buy/sell/rebalance) |
|
| `coinhunter exec` | Order execution layer (buy / flat / rotate / hold) |
|
||||||
| `logger.py` | `~/.coinhunter/logger.py` | Records decisions, trades, and market snapshots |
|
| `coinhunter pre` | Lightweight threshold evaluator and trigger gate |
|
||||||
| `review_engine.py` | `~/.coinhunter/review_engine.py` | Hourly quality review and optimization suggestions |
|
| `coinhunter review` | Generate compact review context for the agent |
|
||||||
| `market_probe.py` | `~/.hermes/skills/coinhunter/scripts/market_probe.py` | Market data fetcher |
|
| `coinhunter recap` | Hourly quality review and optimization suggestions |
|
||||||
|
| `coinhunter probe` | Market data fetcher |
|
||||||
|
|
||||||
### Execution schedule
|
### Execution schedule
|
||||||
- **Trade bot** — runs every 15-30 minutes via `cronjob`.
|
- **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.
|
If you want to replicate this low-cost trigger architecture, here is a complete blueprint.
|
||||||
|
|
||||||
#### 1. File layout
|
#### 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/
|
~/.coinhunter/state/
|
||||||
precheck_state.json # last snapshot + trigger flags
|
precheck_state.json # last snapshot + trigger flags
|
||||||
external_gate.lock # flock file for external gate
|
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",
|
"id": "coinhunter-trade",
|
||||||
"schedule": "*/15 * * * *",
|
"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.",
|
"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": "~/.hermes/scripts/coinhunter_precheck.py",
|
"script": "coinhunter_precheck.py",
|
||||||
"deliver": "telegram",
|
"deliver": "telegram",
|
||||||
"model": "kimi-for-coding"
|
"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)
|
#### 5. External gate (optional, for even lower cost)
|
||||||
If you want to run the precheck every 5 minutes without waking Hermes at all:
|
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
|
```python
|
||||||
import fcntl, os, subprocess, json, sys
|
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.
|
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
|
## Scope
|
||||||
|
|
||||||
This guide covers:
|
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