关于 binance-onchain-copy-trader
用于搭建 Binance 链上跟单交易流水线的脚手架,面向希望自行开发和评估策略的操作者。它提供事件约定、流水线结构和三个可编辑策略钩子,不提供交易策略。
主要功能
- 01地址与信号触发源
- 02交易者排名与地址分析
- 03可配置的入场、仓位和退出钩子
- 04Dry-run 事件与结果记录
- 05Live 模式预算确认门槛
适用场景
- 01监控指定钱包或信号
- 02依据记录的结果测试自定义钩子
- 03实盘前检查流水线事件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ binance/ binance-skills-hub --skill binance-onchain-copy-trader安装后在 Claude Code 中输入/binance-onchain-copy-trader确认已加载。
附带文件scripts/copytrader.py
详细介绍
Binance 链上跟单交易
用于搭建跟单交易流水线的脚手架。**此技能不提供策略。**它提供的是: 能力索引、事件契约、组装结构和三个策略钩子。策略本身由你构建和完善。
请先阅读
此技能中的每项风险控制、仓位管理方案和参数值都只是建议和示例。没有任何一项是强制要求,也不构成安全保证或投资建议。
- 此脚手架只保证运行你配置的检查。它不保证这些检查足以保护你的资金。预算上限、止损距离、流动性下限——都由你决定。你可以将它们设得很宽松、完全省略,或关闭 honeypot 拦截;脚手架都会照做。
- 附带的三个钩子是用于证明流水线可以运行的最小参考实现。它们不是推荐策略,且没有正期望的证据。按原样在实盘中运行,意味着用真金白银为未经验证的策略提供资金。
- 链上交易不可撤销。任何自动化策略都可能因你未曾预料到的市场状况、平台行为变化或此脚手架中的缺陷而蒙受损失。风险由你承担。
- 建议路径:运行
dry-run,直到数据足够有参考价值 → 阅读events.jsonl并建立你自己的基线 → 使用完全亏得起的金额切换到live→ 再从此基础上调整。
依赖项
确认当前环境中可以使用以下技能。此技能只负责编排;所有底层能力都来自这些技能:
| 技能 | 提供内容 | 此处使用 |
|---|---|---|
binance-agentic-wallet | 钱包、市价单、限价单、配额、链信息 | wallet market-order limit-order |
binance-wallet-tracker | 地址监控、实时推送、群组管理 | tracker ws tracker token/tx tracker group/address |
| ↳ | 必须包含 Connection lifecycle 和 Signal field nullability 两个章节——此技能的连接监督分工以及对首次推送字段的假设都直接依赖它们 | — |
binance-leaderboard | 交易员排名、地址评分、持有者查询 | leaderboard query/analyze/alpha-radar |
binance-trading-signal | 聪明钱信号、自定义策略、回测 | signal list signal strategy signal backtest |
query-token-audit | 代币安全审计 | 审计 HTTP 端点 |
query-token-info | *(可选)*实时代币价格和市场数据 | 仅当钩子需要事件中未携带的实时价格时使用 |
验证:
baw --version
baw cli-check --required-version "$(grep -m1 requiredCliVersion SKILL.md | tr -dc '0-9.')" --json
# ↑ the frontmatter `requiredCliVersion` is the single source of truth for the version floor.
# Below it, `tracker ws --duration 0` has no keepalive and no reconnect, and the scaffold
# refuses to start.
缺少其中任何一项都会禁用对应阶段:没有 binance-leaderboard,就没有系统性挑选地址的方法;没有 binance-trading-signal,就无法创建自定义信号策略;没有 query-token-audit,就没有审计数据,hard_gate 将退回到 require_audit,以决定通过还是拒绝。
安装来源:https://github.com/binance/binance-skills-hub
开始使用
以下步骤将带你从零开始,完成一次经过评分的模拟运行。每一步都指向下文中的相应章节。
**请注意步骤顺序。**策略编写在首次模拟运行之后,而不是之前。这是有意为之:此脚手架的核心前提是,阈值应来自你自己的数据,而首次运行前你还没有任何数据。因此,第一次运行会原样使用附带的参考钩子——不是因为它们有多好,而是因为它们能生成你编写更好策略所需的数据集。
1 · 环境检查 → 参阅“预检”
确认钱包已连接,目标链支持 market-order 和 limit-order,并且报价代币和 Gas 余额充足。如果所在链不支持 limit-order,你就无法下达链上保护性订单;请在继续之前接受这一点。
2 · 选择触发源 → 参见“触发源”
选择地址订阅、信号订阅,或两者兼选。这将决定你接下来如何选择来源。
3 · 选择来源
- 地址来源:使用
leaderboard query查找候选地址 → 使用leaderboard analyze查看每个地址的六维评分(follow_friendly是这里最相关的维度)→ 将地址写入addrs.txt,每行一个,使用#添加注释,最多 100 个 - 信号来源:使用
signal explore浏览官方策略 / 使用signal strategy create创建自己的策略 / 使用signal strategy follow订阅策略——或者直接使用--smy,完全不做任何配置,交由平台自行判断
排序维度会实质性改变候选池的特征(--sort-by 0=PnL,20=胜率,30=交易量,50=交易笔数……),而单个维度可能趋于饱和——例如,在短时间内,许多地址的胜率都可能达到 100%。添加自己的次要标准来打破平局。
4 · 编写配置 → 参见“配置”
首次运行时,会在 $COPYTRADER_HOME(默认为 ~/.local/copytrader)写入空模板,并在所有必填字段填写完毕前拒绝启动,同时准确列出缺少的字段。机制参数(预算 / 每笔交易金额 / 并发数 / Gas 下限)与传递给钩子的 policy 分开。将 mode 保持为 dry-run。
这里的值需要你自己决定,不能靠猜测默认值——尤其是 budget_usd,应填写一个即使全部亏损也能接受的金额,因为你将在第 9 步被要求确认这个金额。
5 · 使用原样的内置钩子进行 Dry-run
cd <skill-dir>/scripts && python3 copytrader.py
暂时不要编辑钩子。内置钩子几乎不做筛选(仅筛选方向和新鲜度),因此策略表现不佳,但它是个很好的观察工具:它会放行广泛的样本,让你在开始排除数据之前,先看看数据流中实际包含什么。
6 · 判断数据管道是否正常
tail -f $COPYTRADER_HOME/copytrader.log
检查:
- 启动日志中的
sources和policy是否符合预期 - 是否正在写入
events.jsonl,且decision分布是否合理——拒绝原因是否正是你想要的那些? - 通过的事件是否包含
lag_s、tax、audit_risk、liquidity、price的值——缺少某个值意味着某个上游调用没有返回结果 conn_seq是否没有快速增加(频繁重连意味着数据流不稳定)- 第一个时间跨度结束后,
outcomes.jsonl是否开始填充 - 在 dry-run 模式下,
state.json中的deployed是否保持为 0
这一步关注的是数据管道,而不是策略。在解读数据之前,先修复这里的所有问题。
7 · 现在编写你的策略 → 参见“策略钩子”和“不花钱评估评分决策”
拿到数据集后,编辑 scripts/copytrader.py 顶部由 ══ 分隔的代码块中的三个函数:should_enter / size_position / exit_plan。
使用 key 关联 events.jsonl 和 outcomes.jsonl,并据此确定阈值。每个新增的门槛都应能在数据中找到依据,而且你应该能从 decision 分布中准确看出它拒绝了什么。钩子读取哪些参数由你决定;将它们放入 config.json 的 policy 部分后,就会原样传入。
8 · 迭代 修改钩子 → 重新运行 → 再次评分。这一切都可以在 dry-run 模式下完成,这正是关键所在:采集结果样本不需要成交,因此可以根据记录的数据而非真实资金来评估策略。否则,积累足够可用的样本量才是昂贵的部分。
不要过早下结论。少数几条经过评分的决策说明不了什么,均值若由一两个离群值拉高,也说明不了什么。
9 · 切换到实盘
将 mode 设为 live,并且确认所承担的风险金额——实盘模式会拒绝启动,直到确认文件中包含当前的 budget_usd:
echo <budget_usd> > $COPYTRADER_HOME/I_UNDERSTAND_THE_RISK
金额必须由操作者确认;确认后,Agent 可以写入文件。 设置这道关卡的目的,是确保决策已经作出,而且针对的是这个具体金额——而不是要求由人来输入命令。因此,顺理成章的流程是:Agent 说明预算以及预算全部损失意味着什么,操作者确认,然后 Agent 写入文件并启动运行。
为什么要求确认金额,而不是简单地 touch:这样无法通过复制他人的运行时目录继承确认;金额必须由作出决定的人确定;而且之后提高 budget_usd 会使确认失效——风险金额增加,理应重新决策,而不是沿用过期的确认。
在此之前,你应该已经:进行了足够长时间的 dry-run,以积累有意义的样本;依据 outcomes.jsonl 而非直觉为这些决策评分;并得出结论,能够接受 budget_usd 全部损失。
dry-run 不会记录持仓,因此预算和并发防护措施都不会触发——所以 dry-run 的通过数会高于实盘实际会开出的持仓数。 不要根据 dry-run 的通过率来确定实盘规模。
能力地图
流水线的每个阶段都对应一项现有能力。以下表格是查阅索引;各项技能自身的文档和 --help 才是参数的权威说明。
来源选择——为地址来源选择地址;为信号来源选择或构建策略
| 能力 | 命令 | 返回内容 | 关键参数 |
|---|---|---|---|
| 交易员排名 | baw leaderboard query | address · winRate · realizedPnl · realizedPnlPercent · totalVolume · totalTxCnt · buyTxCnt · tags · addressLabel | -c 链 · -p 7d/30d/90d · -t ALL/KOL/MPC · --sort-by 0=PnL 20=胜率 30=交易量 50=交易次数 60=活跃度 70=费率 80=代币数量 · --page · --size(≤20) |
| 单地址评分 | baw leaderboard analyze | 6 个维度的评分(winrate / stability / drawdown / tags / pnl / follow_friendly),以及 AI 类型 | -c · -a 地址 · -p · --top-n(≤5000) |
| 按持仓反向查找 | baw leaderboard alpha-radar | 持有指定代币的地址 | -c · -t 以逗号分隔的代币地址 · -m 最低命中数 · -p |
| 代币级监控 | baw tracker token | 近期代币活动 | -c · -g 分组 · --tag-type kol/smy · --token-size · --period 1m/5m/1h/4h/24h · --filter-risk |
| 交易级监控 | baw tracker tx | 单笔交易 | 同上 |
| 已关注地址 | baw tracker follow | 当前关注列表(只读) | -c |
| 分组管理 | baw tracker group / address | 创建分组、批量导入、标注、关联 | 参见 wallet-tracker 文档 |
触发来源——两种同级实时订阅
两者都通过 baw tracker ws 工作,都是 WebSocket 推送,延迟处于同一量级。
区别在于由谁决定哪些内容值得关注:使用地址来源时,由你指定钱包;使用信号来源时,由平台(或你构建的策略)决定。可以单独运行其中一种,也可以同时运行两种(sources: ["address","signal"])。
A · 地址订阅 — 由你选择钱包,它会推送这些钱包的每笔交易
| 命令 | 事件语义 |
|---|---|
baw tracker ws --address-list <file> -c <chain> | 列出的地址发起的每笔链上交易,最多 100 个地址 |
baw tracker ws --address <addr> -c <chain> | 单个地址 |
baw tracker ws --following -c <chain> | 当前关注列表;需要登录,且不能与其他标志同时使用 |
- 如何选择:使用
leaderboard query/analyze/alpha-radar,再用tracker group/address创建分组 - 事件粒度:一笔交易对应一个事件;不会重复推送
- 方向:
tradeSideCategory— 买入{11,19},卖出{21,29} - 链:
-c适用;单条链 - 入场价格:事件携带
tokenPrice(交易对手方的实际成交价) - 推送延迟:自行测量。预计中位数在几秒左右,但长尾较重(P90 可能达到几分钟)。请根据事件时间戳记录实际延迟,不要凭假设估算。
B · 信号订阅 — 由平台或你的策略作出判断,并推送结果
| 命令 | 事件语义 |
|---|---|
baw tracker ws --smy | 聪明钱信号(多地址共识)。所有链;-c 无效 |
baw tracker ws --kol -c <chain> | KOL 级事件 |
baw tracker ws --wallet BSC,SOL,BASE,ETH | 链级钱包活动,以逗号分隔 |
- 如何选择:使用
signal explore浏览官方策略,使用signal strategy create/follow创建或订阅自定义策略(meme-rush / fomo-call),使用signal backtest进行回测 - 事件粒度:同一个
signalId会随着状态更新反复推送 — 每个信号会推送数十到数百次。要判断“这是一个新信号”,必须按signalId去重 - 仅信号包含的字段:
smartMoneyCount(意见一致的聪明钱地址数量)·exitRate(已退出的占比)·maxGain·status status语义:valid/active表示进行中 ·timeout表示已过期(触发后 T+120min 推送一次)·outDecline·exitRate- 链:推送所有链的信号;自行按
chainId筛选 - 入场价格:
currentPrice(通常为null)或alertPrice(触发时的历史价格 — 并非当前可交易价格) - 推送延迟:自行测量,并注意测量方法 — 必须取每个
signalId的最小延迟,且只统计真正的首次推送。否则,重复推送会把“信号当前已存在的时间”误算成“延迟”,导致结果高出几个数量级。
如何选择
| 地址来源 | 信号来源 | |
|---|---|---|
| 谁决定关注什么 | 你(选择地址) | 平台判断 / 你的自定义策略 |
| 一个事件的含义 | 某个人的一笔交易 | 多个地址之间达成共识 |
| 覆盖范围 | 受限于 100 个地址上限 | 覆盖整个平台、所有链 |
| 是否需要去重 | 否 | 是(按 signalId) |
| 时间戳字段 | ts | signalTriggerTime |
| 事件密度 | 取决于所选地址的活跃程度及其数量 | 取决于平台判断,在不同链上的分布不均 — 订阅单条链可能会长时间没有事件 |
共享参数:--duration <sec>(0 或省略 = 无限时长)。在脚手架中,每个来源占用一个读取线程;事件经 norm_address() / norm_signal() 规范化后,合并到同一个队列中。
Signal 查询与策略管理(REST,并非推送)
上表中的 智能资金信号实时路径是
tracker ws --smy。本组功能用于 REST 查询和策略管理——适用于回填、筛选、构建自定义策略和回测。它与延迟无关。
| 功能 | 命令 | 关键参数 |
|---|---|---|
| 信号流(回填) | baw signal list | -c 链 · -s all/user/meme/smart-money · --time-range 5m/1h/24h(按 signalTriggerTime 筛选) · --sort-by time/maxGain · -n(≤100) |
| 自定义策略 | baw signal strategy create/update/delete/follow/unfollow/list/list-followed | -c · -t meme-rush/fomo-call · --job-id · --config |
| 回测 | baw signal backtest list/detail/retry/schedule | --interval 4H/6H/12H/24H/OFF |
| 回测额度 | baw signal credits | — |
| 官方策略库 | baw signal explore | — |
signal list 返回的字段与 tracker ws --smy 推送的数据来自同一来源(signalId 值一一对应),但两者都不包含对方的全部内容。
风险控制与执行
以下功能是可用工具。是否组合使用,以及如何组合,由你的钩子和配置决定。本技能不强制要求任何特定检查——只保证你要求的检查会在正确的时机运行。
| 功能 | 命令 / 端点 |
|---|---|
| 代币审计 | POST https://web3.binance.com/bapi/defi/v1/public/wallet-direct/security/token/audit,请求体 {binanceChainId, contractAddress, requestId}。返回 hasResult isSupported riskLevel extraInfo.buyTax/sellTax riskItems[].details[].isHit |
| 市场数据 | GET .../buw/wallet/market/token/dynamic/info/ai?chainId=&contractAddress= → price liquidity holders volume24h |
| 报价 | baw market-order quote --fromTokenQty --fromToken --toToken --binanceChainId --slippage |
| 市价单 | baw market-order swap(相同参数,另加 --mev --gasLevel)→ 返回 orderId |
| 订单查询 | baw market-order list → status toTokenActualQty txHash |
| 限价单 | baw limit-order buy/sell --triggerPrice --fromTokenQty --fromToken --toToken --binanceChainId --slippage --mev --gasLevel |
| 取消 / 列表 | baw limit-order cancel --strategyId · baw limit-order list |
| 风控读数 | baw wallet balance · left-quota · tx-lock · settings · chains |
事件结构
两个来源都会通过 norm_address() / norm_signal() 规范化为同一结构。钩子只会看到以下内容:
{
"src": "address" | "signal",
"key": dedupe key (address stream = txHash; signal stream = "sig:" + signalId),
"ca": token contract address,
"token": token name,
"price": entry reference price (address stream = counterparty fill price `tokenPrice`;
signal stream = `currentPrice` or `alertPrice`),
"ts": event time (epoch seconds) — time.time() - ts is the delivery latency,
"chain": chain ID,
"side": 11/19 = buy, 21/29 = sell (always 11 on the signal stream),
"who": source identity (address stream = wallet label; signal stream = "SMY×N"),
"usd": USD size of the counterparty's trade,
"risk": tokenRiskLevel,
"top10": top-10 holder concentration,
"tags": token tag array,
"extra": source-specific fields
}
extra 的内容因来源而异:
| 来源 | extra 内容 |
|---|---|
| address | addr |
| signal | signalId status smy(共识计数) exitRate liq entry_liq holders is_alpha launch_ms mcap sig_seq |
原始推送字段的完整列表(包括规范化时被丢弃的字段),请参阅 binance-wallet-tracker 中的字段表。如需将更多字段写入 extra,请扩展 norm_* 函数。
Signal 流:首次推送与重新推送时的字段可用性
编写任何策略之前,请先了解这一点。 --smy 是状态流,而不是事件流——同一个 signalId 在其生命周期内会反复推送,且首次推送与重新推送携带的字段不同。策略只能使用首次推送中存在的字段,因为届时就必须作出决策。
权威定义位于 binance-wallet-tracker/references/cli.md 的 Signal 字段可空性 部分。下表展示了这些字段在组装层的表现:
| 字段 | 首次推送 | 可用于入场决策? |
|---|---|---|
signalId ticker chainId contractAddress | 携带 | 是 |
signalTriggerTime | 携带 | 是——据此计算推送时效性 |
alertPrice alertMarketCap | 携带 | 是(但请注意:这是触发时的历史价格,并非可交易价格) |
alertLiquidity entryLiquidity | 携带 | 是 |
top10 holders smartMoneyCount isAlpha launchTime | 携带 | 是 |
status | 通常为 null | 否——见注释 1 |
exitRate maxGain currentPrice | 通常为 null | 否——见注释 2 |
注释:
-
status检查只能拒绝明确的不良值;缺失值必须通过。 首次推送中的status通常为null(生命周期状态timeout/outDecline/exitRate会在后续推送中写入)。编写if st not in ("valid","active")会悄无声息地丢弃几乎所有真实的首次推送,而且这种现象与“此链上没有信号”无法区分,因此很难诊断。正确写法:if st is not None and st not in ("valid", "active"): return False, f"status={st}" -
不要将
exitRate/maxGain/currentPrice用作入场条件。 它们描述信号触发之后的演变情况;服务端在首次推送时尚未计算这些字段。基于这些字段的阈值检查在首次推送时永远不会触发,只会误判你在信号生命周期中途接入的较早信号。请改用它们处理出场逻辑或事后相关性分析——这些字段仍会完整写入events.jsonl。 -
不要把订单挂在
alertPrice上,仿佛它是可交易价格。 它是触发时的历史价格;考虑到推送延迟,使用它作为成交价格会记录一笔无法成交的订单。本框架仅将其视为传递给钩子的参考价格,而保护性订单的触发价始终根据真实入场价格计算,即usd / actual filled quantity。两者应分开处理。如果钩子需要实时价格,请查询市场数据(query-token-info),并在无法获取时跳过。 -
流动性无需调用市场数据接口——首次推送中已包含
alertLiquidity,而新发行的代币通常没有市场数据索引,因此市场数据不如事件自身的字段可靠。另外请注意,信号来源已经按流动性筛选,所以在信号流上设置流动性下限几乎没有作用;不要指望它能承担筛选工作。 -
应通过时效性检查捕获过期的重新推送,而不是依赖
status。status=timeout的过期通知总是在触发后 T+120min 到达;它携带status,但其signalTriggerTime已经过时。这两项检查相互补充:status检查会捕获明确的不良值,时效性检查会捕获所有时间上已过时的信号。
组装结构
ws_reader(source) one thread per source; timestamps and enqueues only, never blocks
↓ queue
worker × N
↓
norm_* ─────────────► normalized event `ev`
↓
should_enter(ev, ctx) ← hook ① ctx.audit / liquidity are still None here
↓ pass
gate_ok() mechanism guardrails: budget / positions / gas / tx-lock / quota / STOP
↓
audit() ∥ token_market() fetch audit and market data concurrently, completing ctx
↓
hard_gate(ctx) mechanism-forced check: audit has a result + honeypot (configurable)
↓
should_enter(ev, ctx) ← hook ① again, now with audit and liquidity
↓ pass
size_position(ev, ctx) ← hook ② returns a USD amount
↓
market-order swap → actual_qty() reads toTokenActualQty from market-order list
↓
exit_plan(ev, ctx, entry, qty) ← hook ③ returns [(trigger_price, qty, slippage), ...]
↓
place_protection() places limit-order sells in the returned order
↓
reconcile() periodic: once any protective order fills, re-place against the
actual remaining balance
为什么要这样组装
管线中有 3 个不明显的选择,每个都依赖一项接口约定。更改前,请确认约定仍然成立:
| 选择 | 依赖的约定 |
|---|---|
已成交数量来自 market-order list,而不是通过轮询钱包余额获取 | market-order swap 会返回 orderId,既不返回 txHash,也不返回已成交数量;数量是 market-order list 中的 toTokenActualQty |
exit_plan 可以表达止损 | limit-order sell 接受低于市场价的 triggerPrice,并保持 status: WORKING,而不是立即成交 |
ws_reader 只在进程级别重启 | 连接级别的处理已内置于 tracker ws --duration 0(PING 保活 + 自动重连 + 退避);所需的 CLI 版本见 frontmatter 中的 requiredCliVersion。binance-wallet-tracker 的 Connection lifecycle 部分明确要求调用方不要再添加一层处理——外层轮换会中断健康的连接,而外层看门狗则会中止 CLI 已在退避等待的重连 |
另有两个选择会影响读数的解读:ws_reader 会记录 conn_uptime_s,以便区分服务端延迟和重连后的重放;读取器线程不会执行阻塞调用,否则处理时间会累积,导致后续事件的到达时间戳被推迟。
策略钩子
这 3 个钩子是编写策略的地方。仓库中提供的内容只是一个让管线运行起来的示例——其中每项判断和每个阈值都应替换为你自己的分析结果。
这 3 个函数位于 scripts/copytrader.py 顶部;机制层不会触及它们。函数签名:
should_enter(ev, ctx) -> (bool, reason)
size_position(ev, ctx) -> float # USD; the mechanism layer then clamps to
# min(value, position_usd, remaining budget)
exit_plan(ev, ctx, entry_price, qty) -> [(trigger_price, sell_qty, slippage_pct), ...]
ctx 内容:
| 键 | 含义 |
|---|---|
audit | 原始审计响应(首次调用钩子时为 None) |
liquidity | 市场数据中的流动性(首次调用时为 None;信号流可以回退使用 extra.liq) |
price | 入场参考价格 |
positions | 当前持仓字典 |
deployed | 迄今已投入的 USD 金额 |
cfg | 完整配置 |
policy | config.json 的 policy 部分,原样传入;机制层不会解读其中的键 |
should_enter 会被调用两次:一次在花费任何资源获取审计和市场数据之前(只能访问事件自身的字段),另一次在两者都准备就绪之后。在第一次调用中进行成本较低的判断;需要审计数据的判断则放在第二次调用中。
辅助函数:audit_tax(a) 从审计响应中提取 buyTax + sellTax。
机制层防护措施
这是两件不同的事。运行防护措施是机制层的职责,钩子无法绕过;具体的数值和开关则由你决定。
| 决定方 | 说明 | |
|---|---|---|
| 何时运行检查以及由谁运行 | 脚手架 | 例如,“每次入场前检查预算”无法关闭 |
| 检查使用什么阈值,以及是否启用 | 你 | budget_usd 设为多大、是否启用 block_honeypot——脚手架会执行配置指定的内容 |
| 检查通过后买什么、买多少以及如何退出 | 你的钩子 | 机制层只会限制上限,不会选择交易标的 |
因此,下面的内容是一组建议的防护措施,而非安全保证。即使配置为 budget_usd: 1000000 并关闭 honeypot 拦截,程序也会照常运行。
防护栏列表
| 项目 | 行为 |
|---|---|
| 预算 | 累计部署金额绝不超过 budget_usd;size_position 的返回值会被限制在上限内 |
| 单笔交易 | 绝不超过 position_usd |
| 并发 | 绝不超过 max_positions |
| Gas | 原生代币余额低于 gas_floor_native 时停止开仓;余额无法读取时也会停止 |
| 序列化 | tx-lock 报告为 LOCKED 时不下单 |
| 配额 | left-quota 不足时不下单 |
| 相同代币 | 不会再次开仓已持有的代币 |
| 单实例 | 使用 pidfile 锁;第二次启动会立即退出 |
| 闸门 | STOP 会阻止新开仓(不影响现有保护性订单);KILL 会退出进程 |
| 模式 | 在 mode: dry-run 下,完全不会发出订单请求 |
| 实盘确认 | 除非 $COPYTRADER_HOME/I_UNDERSTAND_THE_RISK 中包含当前的 budget_usd,否则 mode: live 会拒绝启动。复制运行时目录无法继承该确认,提高预算后该确认也会失效 |
将 mode 从 dry-run 切换为 live,需要操作员明确决定具体金额;脚手架通过上述确认文件强制执行这一要求,而不是仅凭配置修改就予以信任。操作员确认金额后,Agent 可以写入该文件。
连接监控的职责划分
连接级的维护由 CLI 负责;进程级的维护由此脚手架负责。 不要重复处理。
| 层级 | 负责方 | 作用 |
|---|---|---|
| 连接 | CLI | tracker ws --duration 0 内置以下功能:每隔 30s 发送一次应用级 PING、自动重连,以及在 onclose 时重新 SUBSCRIBE;采用指数退避(收到任何推送后重置) |
| 进程 | 此脚手架 | 仅在子进程实际退出时重启它(固定的短延迟 proc_restart_sec),并写入 state["stream_alarm"] |
不能将两者叠加(binance-wallet-tracker 的连接生命周期一节对此有明确说明):
- 外层的“主动轮换”会定期终止 CLI 正常维护的连接
- 外层的“静默监视器”会在 CLI 正处于退避期间时强制终止子进程,中断待处理的重连
- 两层嵌套的退避机制会导致实际重连间隔无法预测
--duration N > 0 的语义不同:它只建立单个连接,断开后会报错,且不会重连。该模式用于一次性采样,绝不能用于长期运行的进程。
版本下限不可妥协。 内置保活是此脚手架的前提条件。低于所需 CLI 版本时,
--duration 0既不会保活,也不会重连,而脚手架也不再负责连接级重连——仍然运行会导致首次断开后永久静默,且无法将其与市场平静区分开来。启动检查会验证版本,并在不符合要求时拒绝运行。所需版本是 frontmatter 中的requiredCliVersion;脚本中的CLI_MIN与其一致。
使用正确的字段检查存活状态
state["last_push_ms"] 是存活状态信号——每次推送都会更新它,包括重复推送。
不要使用 last_event_ms。 该字段在去重后写入,因此只能反映是否收到新的 signalId。信号流静默半小时完全正常;用该字段检查存活状态,会把正常的静默误判为故障。
盈利退出奖励额度(state.bonus_slots)——建议启用,可禁用
这是一种仓位管理思路的示例,不是建议。它假设“盈利退出表明策略在当前市场环境下有效,因此可以增加风险敞口”——这一假设在趋势市场中成立,但在震荡市场中会放大回撤。如果你不想启用,请让
position_usd成为budget_usd的约数,并忽略此字段。
每次以盈利结算的退出,都会增加一个 position_usd 仓位的额度:
position ceiling = max_positions + bonus_slots
capital ceiling = budget_usd + bonus_slots × position_usd
亏损退出不会增加额度。如果无法确定收益(proceeds is None),也不会增加额度——默认拒绝:宁可不提供额度,也不愿凭猜测扩大风险。收益取自成交交易在链上的接收金额。
同一代币冷却时间(policy.same_token_cooldown_h)——建议默认值为 24,可调整、可禁用
在冷却时间窗口内,同一合约地址只会在最早的信号出现时买入。仅仅确认“我当前是否持有它?”是不够的——仓位平仓后,同一代币的后续信号仍会再次通过(单个代币一天内可能产生多个不同的 signalId)。冷却时间的起点记录在 state.json.ca_last[ca] 中,与仓位状态无关。
配置
$COPYTRADER_HOME/config.json(默认为 ~/.local/copytrader)。首次运行会生成一个
完全空白的模板。
资源保护限制和策略参数没有默认值——缺少其中任何一项都会拒绝启动。 止损距离和每笔交易规模等数值必须由你自行决定, 而不是照搬他人的设置。你从未认真考虑过的止损距离,和你从未认真考虑过的仓位规模一样危险, 因此,这个脚手架宁可不启动。 唯一的例外是
mode:缺省时为dry-run,因此省略它绝不会导致实盘交易。
必填(机制层)
| 键 | 含义 | 如何决定 |
|---|---|---|
chain | 链 ID | 使用 baw wallet chains --json 查看当前支持的链 |
quote_token | 报价代币合约地址 | 该链上的稳定币;必须为小写 |
sources | 触发源 | ["address"] / ["signal"] / 两者兼有 |
budget_usd | 累计投入上限 | 不再投资;数值只增不减。请决定是否能接受全部亏损 |
position_usd | 每笔交易规模 | 与止损距离共同决定每笔交易的最坏情况 |
max_positions | 并发仓位上限 | 与 budget_usd 一起限制总风险敞口 |
gas_floor_native | 原生代币余额下限 | 低于此值时停止开仓;余额无法读取时也会停止 |
必填(策略参数,由参考钩子读取)
policy 部分会原样传递给钩子,机制层不会解析其中的键——如果替换钩子,此表也会随之变化。参考实现要求:
| 键 | 含义 |
|---|---|
follow_sides | 地址流中要跟随的 tradeSideCategory 值 |
max_event_age_sec | 事件新鲜度上限,单位为秒 |
sl_mult / tp_mult | 止损 / 止盈触发价 = 入场价 × 此值 |
tp_fraction | 止盈时卖出的仓位比例 |
buy_slippage / sl_slippage / tp_slippage | 每个阶段的滑点容差(%)。止损滑点应大于止盈滑点——暴跌期间无法成交的止损并不能起到保护作用 |
有默认值(不影响资金安全的基础配置)
mode(dry-run) · address_list(addrs.txt)· reconcile_sec · proc_restart_sec ·
workers · outcome_horizons_min([5, 15, 60];设为 [] 可关闭结果采样)
可选
policy.block_honeypot(默认为 true;不建议禁用)· policy.require_audit ·
policy.enrich_srcs · policy.same_token_cooldown_h · policy.min_liquidity_usd ·
policy.block_risk_levels · policy.max_sell_tax_pct · policy.max_commit_ratio
先运行一段时间的 dry-run,并查看 events.jsonl 中的 decision 分布,确认每个关卡实际拒绝的内容,
再决定是否切换到 live。
平台行为说明
cli-check 实际返回什么
在 baw cli-check --required-version X --json 中,success 只表示命令已运行,且始终为 true。版本是否符合要求由 data.needUpdateCli 决定:
{"success": true, "data": {"currentCliVersion": "x.y.z", "needUpdateCli": true}}
如果将 success 视为“版本没问题”,版本关卡就会失去作用:命令始终“成功”,因此关卡始终放行。正确的判断条件是 data.needUpdateCli === false。
不同链的地址大小写规则并不一致
传给 CLI 的所有合约地址都应转换为小写。 各条链对地址格式的验证严格程度不同——有些接受 EIP-55 混合大小写,有些则会直接拒绝:
[20003002] Invalid fromToken address
文档没有说明这种差异,而且错误提示指向“无效地址”,而不是“格式错误”,让人以为配置了错误的代币。
危险之处在于它的表现形式:该交易对上的每笔订单都会失败,而在每项存活指标中,“从未成交”看起来都和“这条链没有信号”一模一样。除非明确统计失败订单,否则这种情况可能会长期被忽视。这就是为什么你构建的任何报告都应包含 state["buy_fails"] 和最近一次错误。
不花钱评估决策
单靠 events.jsonl 无法判断某个决策是否成功——它记录的是决策,而不是结果。因此,每个本该开仓的决策(包括什么也不买的 dry-run)都会安排在 outcome_horizons_min 指定的时间点重新读取价格,并将结果追加到 outcomes.jsonl:
{"key":"sig:…","ca":"0x…","token":"…","src":"signal","horizon_min":15,
"t0_ms":…,"entry":0.0123,"price":0.0141,"ret_pct":14.63,"sampled_ms":…}
通过 key 联接,即可为每个决策生成一行,包含其特征和后续收益率:
python3 - <<'EOF'
import json, collections
ev = {}
for l in open("events.jsonl"):
r = json.loads(l)
if r.get("decision") in ("DRY_RUN", "OPENED"): ev[r["key"]] = r
by = collections.defaultdict(dict)
for l in open("outcomes.jsonl"):
o = json.loads(l)
if o["key"] in ev and o.get("ret_pct") is not None:
by[o["key"]][o["horizon_min"]] = o["ret_pct"]
print(f"{len(by)} scored decisions")
for h in sorted({h for d in by.values() for h in d}):
v = sorted(d[h] for d in by.values() if h in d)
if v:
n = len(v)
print(f" {h:>3}min n={n:<4} median {v[n//2]:+7.1f}% "
f"win rate {sum(1 for x in v if x > 0)/n*100:.0f}%")
EOF
接下来,根据你要测试的任意特征(liquidity、top10、tax、smy、audit_risk、来自 launch_ms 的代币年龄,……)进行分组,并比较分布。
解读这些数字时,请牢记它们的含义:
- 它们是估值,不是成交。 不包含滑点、手续费、代币税或 Gas,也没有检查仓位是否真的能以该价格卖出。请将其视为策略可能获取收益的上限。
- 时间跨度对应一个时点,而非价格路径。 60 分钟时的
+40%不代表期间从未触发止损。请结合多个时间跨度进行解读,并记住,两个时间点之间触发的止损会彻底改变实际结果。 - 样本量始终至关重要。 少数几个经评估的决策说明不了任何问题;如果均值是由一两个离群值拉高的,也同样说明不了任何问题。请关注中位数和分布,而不是平均值。
- 如果样本读取时间很晚(例如发生故障后),会标记为
sampled_late,而不会悄悄并入序列。
构建自己的基线;不要使用别人的数据
下列数值会随平台变更、链、代币类型和一天中的时间而变化,因此任何硬编码的值都会过时。脚手架会将它们全部写入 events.jsonl /
orders.jsonl,正是为了让你测量自己的基线:
| 测量项 | 方法 | 测量本身的陷阱 |
|---|---|---|
| 交付延迟 | lag_s = 到达时间 − 事件时间 | 在信号流中,按每个 signalId 取最小延迟,并且只统计真正的首次推送(测试:lag 应以秒为单位)。把每次重新推送都当成独立观测,会把“信号当前的存续时间”误当成延迟,误差可能达到几个数量级 |
| 延迟原因 | 使用 conn_uptime_s 区分“服务端很慢”和“我们刚重新连接,收到了一条重放消息” | 没有这个字段,就无法归因,只能得到混合数值 |
| 执行时间 | latency_ms 中的逐步耗时 | 如果读取线程执行阻塞调用,处理时间就会累积到后续事件的测量延迟中 |
| 审计/市场覆盖率 | hasResult 和 px_src 的分布 | 新上线的代币通常尚未被市场数据索引;覆盖率与代币年龄高度相关,因此把所有年龄的代币混在一起没有意义 |
| 每笔交易的 Gas 成本 | 交易前后的原生代币余额差 ÷ 交易笔数 | 挂单和取消限价订单也会消耗 Gas;只统计兑换会低估完整持仓周期的成本 |
| 代币质量特征的预测能力 | 将 events.jsonl 中的 top10 / tags / liquidity / tax / audit_risk 与结果进行关联 | 样本量。 只有个位数的成交记录无法让你了解预期收益;如果回报集中在一两笔交易上,均值就毫无意义 |
不要凭直觉设置代币质量阈值。 税务标签和前 10 名持有者集中度等指标,在新上线的代币中的分布与直觉所想的 大不相同——凭感觉设定的阈值会拒绝大多数合规候选对象,而这种过度拒绝看起来 与“没有信号”完全一样,因此很难察觉。先记录,再测试,最后再设门槛。
启动前检查
随着平台演进,链支持、报价代币允许列表和订单配额都会变化——不要依赖任何硬编码表格。 每次启动前都查询当前状态,并根据查询结果 确定可用功能:
baw wallet status --json # must be CONNECTED
baw wallet chains --json # which chains are supported right now
baw wallet settings --json # daily limit / tradeAllTokens / abnormalTxnHandling
baw wallet left-quota --json # remaining quota today
baw wallet balance --binanceChainId <c> --json # quote-token and native (gas) balances
# Does the target chain support what you need? Probe with a quote — it prices a route without
# placing anything on-chain.
baw market-order quote --fromTokenQty 1 --fromToken <quote> --toToken <any token> --binanceChainId <c> --json
baw limit-order list --binanceChainId <c> --json # does the chain answer for limit orders at all
limit-order 是否可用,决定了你能否下达链上保护性订单——如果某条链不支持它,保护就完全取决于进程能否持续运行。上面的两个探测都不能证明下单一定会成功;只有首笔真正的保护性订单才能证明。在看到订单达到 WORKING 状态之前,不要将该链视为已验证;首次操作时,请使用你愿意在缺乏保护的情况下持有的最小仓位。
错误代码:1001001 不支持该链或操作 · 1001002 参数无效 · 100 服务端错误(在尚未接通的链上,该错误也可能表现为“无流动性”,因此要将它与 1001001 区分开)。
各项依赖的命令和参数,以对应技能自身的文档和 --help 为准;本文不重复列出它们的功能表。
运行
cd <skill-dir>/scripts && python3 copytrader.py # foreground
nohup python3 -u copytrader.py >> runner.log 2>&1 & disown # long-running
| 操作 | 命令 |
|---|---|
| 停止开立新仓位 | touch ~/.local/copytrader/STOP |
| 退出进程 | touch ~/.local/copytrader/KILL |
确认风险金额(mode: live 运行前必须完成) | echo <budget_usd> > ~/.local/copytrader/I_UNDERSTAND_THE_RISK |
| 恢复实盘门控 | rm ~/.local/copytrader/I_UNDERSTAND_THE_RISK |
进程退出不会取消已在链上的保护性订单;取消订单需要显式运行 baw limit-order cancel。
数据文件(~/.local/copytrader/):
| 文件 | 内容 |
|---|---|
events.jsonl | 每个事件占一行,包含决策及其所有输入——策略作出了什么决定 |
outcomes.jsonl | 对每个原本会开仓的决策,在各个时间跨度重新读取价格——该决策是否明智。通过 key 与 events.jsonl 关联 |
orders.jsonl | 订单/限价订单/取消日志 |
state.json | 仓位、已部署资金、已处理的键 |
copytrader.log | 按时间顺序记录的日志 |
events.jsonl 中的诊断字段:lag_s(传送延迟) · conn_uptime_s(事件到达时连接已稳定运行的时长——用它来区分服务端延迟和重新连接后的重放) · conn_seq(连接序号) · replay(事件早于当前连接) · latency_ms(各步骤耗时) · decision(通过或拒绝原因) · tax audit_risk liquidity top10 px_src。
该守护进程没有外部监控程序。 请自行配置存活检查:进程是否仍在运行;state.json.last_push_ms 是否在递增(而不是 last_event_ms——它在去重后写入,且只反映新的 signalId,信号流安静半小时是正常的);conn_seq 增长得有多快;baw wallet status 是否仍显示 CONNECTED;baw limit-order list 返回的活跃订单数是否与 state.json 中的仓位数相符。
已知限制
这些是脚手架的固有属性,不是待办事项。上线前请确认你能接受这些限制。
| 限制 | 详情 |
|---|---|
| 完全没有策略优势 | 附带的三个钩子是最小化的参考实现,用于证明流水线能够运行。**它们不是推荐策略,也没有任何预期收益的证据。**策略需要由你制定 |
| 防护措施不等于安全 | 脚手架会运行你配置的检查,但不会判断这些检查是否充分。阈值设得宽松,或关闭可选模块,它都会照常执行——风险控制效果取决于你的设置,而非此技能 |
| 结构性滞后 | 你的成交必然晚于你跟随的对象(推送延迟 + 决策 + 下单)。如果对方是发行方,或拥有私有执行通道,那么他们的收益无法复现,而他们的损失却会被完整复制 |
| 存在未受保护的时间窗口 | 从买入成交到保护性订单下达之间会有一段时间,受结算和 API 往返时间影响。在此窗口内,仓位没有下行保护。无法消除这段时间,只能缩短 |
| 部分成交后数量会不一致 | 任何保护性订单发生部分成交后,剩余订单就不再与剩余仓位匹配。协调线程会根据实际余额重新下单,但如果进程在协调间隔内退出,就会留下过大的订单 |
| 止损无法挽救流动性不足的代币 | 能够下单不等于能够成交。流动性下限是应对这类尾部风险的工具;止损不是 |
| 模拟运行不等同于实盘 | 模拟运行不会记录仓位,因此预算和并发防护措施永远不会触发,通过次数必然高于实盘可开仓的次数。不要根据模拟运行的通过率估算实盘成交频率 |
| 进程退出不会自行恢复 | 脚手架会在子进程退出时重启它;**脚手架自身退出时没有外部监督器。**请自行配置进程级监督(launchd / systemd / 容器重启策略) |
| 延迟衡量的是“平台记录 → 送达” | 事件时间戳与链上区块时间之间的关系尚未明确。如果事件时间本身已经滞后于链上时间,真正的端到端延迟会比你测得的更长 |
| 失败的订单可能会占用每日配额 | 已观察到失败订单既可能占用配额,也可能不占用。每日限额宽裕时这不构成约束,但配额紧张时,不要假设失败订单不占配额 |
免责声明
这是一个脚手架,不是交易产品,也不包含任何自有策略。
- 随附的钩子是一个最小化的参考实现,仅用于演示管道可以运行。它们没有任何正期望收益的证据,也不构成推荐。
- 本技能中的所有风险控制、仓位管理方案和参数值都只是建议,并非保障措施。该脚手架会执行你配置的检查;但它不会判断这些检查是否充分,并且会遵循任意设置的宽松阈值,或在可选保护措施关闭时照常运行。
- 此处任何内容均不构成投资、金融或交易建议。 不对使用此脚手架构建的任何策略能够盈利或避免亏损作出任何保证。
- 链上交易不可逆。自动化交易可能因你未预料到的市场状况、平台或协议行为变化、本软件缺陷或你编写的策略存在缺陷而造成资金损失。跟单交易在机制上还处于不利地位:你的成交总是晚于被跟单方;如果对方是发行方或拥有私有执行通道,那么他们的收益无法复制,而他们的亏损却可以。
- 本软件按**“原样”提供,不附带任何形式的明示或默示保证**。在法律允许的最大范围内,对于因使用本软件而产生的任何损失或损害,不承担任何责任。
- 你须对自己的运行行为、所选参数、遵守适用于你的法律法规以及投入的任何资金承担全部责任。
如果你无法接受损失你在 budget_usd 中配置的全部金额,请勿将 mode 切换为 live。