监控币安「跟单交易」带单员的实时操作,推送到飞书/钉钉,并可选地把他的开平仓动作复制到自己的 HTX USDT 本位永续账户。
币安带单页接口 ──轮询──▶ 去重 ──▶ 飞书 / 钉钉通知
└──▶ HTX 下单(可选,默认关闭)
这个程序会动真钱。开启跟单前请理解以下事实,它们不是代码能解决的:
- 开仓无条件跟随。 带单员判断失误会被原样复制到你的账户,程序不做任何行情判断。
- 平仓依赖进程存活。 进程挂掉或币安接口断供期间发生的平仓信号,重启后会因超过信号有效期被跳过,账户里会留下无人看管的裸仓。
- 平仓是全平,会平掉你手动开的仓。 程序区分不了哪部分持仓是它开的,带单员平仓时会把该方向持仓全部平掉。
- 数据源是币安站内未公开接口,没有 SLA,路径和字段可能随时变动。
- 轮询有延迟。 默认 60 秒一轮,你的成交价必然滞后于带单员,滑点无法避免。
- 多笔叠加会放大敞口。 单笔名义敞口默认等于账户全部权益,开满 3 笔就是 3 倍权益敞口。
强烈建议:真跑实盘前,在 HTX 侧对每个仓位另挂一道独立止损单兜底,别把风控全押在这个进程上。
| 项目 | 要求 |
|---|---|
| JDK | 8 以上(源码按 Java 8 编译,实测 21 可运行) |
| Maven | 3.6+ |
| 网络 | 国内需 HTTP 代理,见下节 |
| HTX 账户 | 仅跟单功能需要;需开通 USDⓈ 本位合约 |
www.binance.com在国内 DNS 被污染,会解析到无关 IP,必须走代理。api.hbdm.com(HTX)实测部分网络直连超时,也建议走代理。- 飞书
open.larksuite.com、钉钉oapi.dingtalk.com一般可直连。
自测代理是否可用:
curl -x http://127.0.0.1:7890 -s -o /dev/null -w '%{http_code}\n' \
https://www.binance.com/bapi/composite/v1/public/promo/campaign/enable
curl -x http://127.0.0.1:7890 -s -o /dev/null -w '%{http_code}\n' \
'https://api.hbdm.com/linear-swap-api/v1/swap_contract_info?contract_code=BTC-USDT'两条都返回 200 才继续。Clash 默认端口 7890,Surge 一般 6152,按自己的工具改。
mvn clean package -DskipTests
# 产物:target/test-1.0-SNAPSHOT.jarcp config/application.yml.example config/application.yml
chmod 600 config/application.yml # 里面有 API 密钥
vim config/application.ymlconfig/application.yml 会被 Spring Boot 自动加载,不需要任何启动参数。该文件已在 .gitignore 中。
最小可用配置(只监控 + 通知,不跟单):
bn:
portfolio-ids:
- "5075281354358777856" # 带单员 ID,见下文
proxy-host: "127.0.0.1"
proxy-port: 7890
notify:
lark:
url: "https://open.larksuite.com/open-apis/bot/v2/hook/xxxx"带单员 ID 从带单页 URL 末尾取:
https://www.binance.com/zh-CN/copy-trading/lead-details/5075281354358777856
→ ID 是 5075281354358777856。名字程序会自动取昵称,不用填。
java -jar target/test-1.0-SNAPSHOT.jar首次运行会把每个带单员近 24 小时的历史记录静默存为基线,不推送通知。从第二轮起才推送新增操作。
| 配置项 | 默认 | 说明 |
|---|---|---|
portfolio-ids |
无 | 带单员 ID 列表,必填。也可用 portfolio-id 填单个,两者取并集 |
portfolio-labels |
{} |
名字覆盖,如 "5075...": 老熬。留空则自动取币安昵称 |
interval-ms |
60000 | 轮询间隔。低于 60000 会告警,容易触发币安 WAF |
lookback-hours |
24 | 每轮回溯多少小时 |
page-size |
50 | 每轮拉取的记录条数上限 |
proxy-host / proxy-port |
空 / 7890 | 代理,国内必填 |
state-file |
./binance-lead-monitor-state.json |
去重状态。删掉即重建基线 |
stats-file |
./binance-lead-monitor-stats.json |
拉取成功率快照,每轮覆盖写 |
stats-interval-ms |
3600000 | 统计汇总日志间隔 |
stats-alert-rate |
0.5 | 窗口内失败率超此值推群告警 |
notify-on-startup |
true | 启动时在日志里输出最新一条操作做自检,只打日志不推群 |
cookie / csrf-token |
空 | 实测公开带单数据无需登录态,留空即可 |
dump-raw |
false | 打印接口原始响应,排查字段变动时用 |
填了 url 即启用,可多渠道并存;全部留空则只打日志。
bn:
notify:
via-proxy: false # 通知一般直连,本地 OneBot 必须直连
lark:
url: "https://open.larksuite.com/open-apis/bot/v2/hook/xxxx"
dingtalk:
url: "https://oapi.dingtalk.com/robot/send?access_token=xxxx"
secret: "" # 「加签」模式填 SEC 开头的密钥;「关键词」模式留空
qq: # 需自行部署 OneBot 11 实现(NapCat 等)
url: "http://127.0.0.1:3000/send_group_msg"
group-id: "123456789"
token: ""
raw: # 通用 {"text":"..."},自建接收端用
url: ""钉钉若用「自定义关键词」安全模式,关键词请设为 带单监控 —— 所有消息都以
【带单监控】开头。
两道开关都放开才会下真单:
htx:
enabled: false # 总开关
dry-run: true # 演练:只推送「将要下的单」,不提交建议放开顺序:先 enabled: true 保持 dry-run: true 跑几天,核对推送里的换算数字,确认无误再关 dry-run。
| 配置项 | 默认 | 说明 |
|---|---|---|
access-key / secret-key |
空 | HTX API Key。只勾「合约交易」,不要勾提币,建议绑 IP 白名单 |
via-proxy |
false | 是否复用 bn.proxy-* 访问 HTX |
margin-mode |
cross | cross 全仓 / isolated 逐仓 |
lever-rate |
5 | 期望杠杆。v5 下单参数里没有杠杆,它是合约维度的独立设置;程序只读交易所实际值用于换算并在不一致时告警,不会自动修改(改杠杆会影响已有持仓强平价) |
order-price-type |
market | v5 的 type,只支持 market / limit / post_only |
price-match |
空 | v5 的 price_match:opponent / optimal_5/10/20,与限价互斥 |
sizing-mode |
equity_percent | 见下节 |
equity-percent |
20 | 单笔保证金占账户权益的百分比 |
max-notional-usdt |
0 | 单笔名义金额硬顶,0 表示不限 |
volume-rounding |
up | 张数取整:up 向上(保证金不够自动下压)/ nearest / down |
margin-buffer-rate |
0.02 | 下压张数时留的余量,给手续费与滑点 |
close-mode |
full | full 市价全平 / sized 按量平 |
follow-portfolio-ids |
空 | 只跟这些带单员,留空表示全跟 |
symbol-whitelist / symbol-blacklist |
空 | 币安标的过滤,黑名单优先 |
symbol-mapping |
{} |
币种映射覆盖,如 1000PEPEUSDT: PEPE-USDT |
max-signal-age-ms |
600000 | 信号有效期,超时只通知不跟单 |
dry-run-equity-usdt |
1000 | 演练且未配 Key 时的假定权益,实盘不参与计算 |
sizing-mode 有三种,默认第一种:
| 模式 | 含义 | 相关配置 |
|---|---|---|
equity_percent |
按账户权益百分比投保证金,完全忽略带单员仓位 | equity-percent |
ratio |
按带单员成交量的固定比例跟 | ratio |
fixed_notional |
每单固定名义金额 | fixed-notional-usdt |
默认的 equity_percent 只借用带单员的方向和时机,仓位大小由你自己的账户决定:
目标名义金额 = 账户权益 × equity-percent% × 实际杠杆
张数 = 目标名义 ÷ (contract_size × 单币价格),按 volume-rounding 取整
举例(权益 173 USDT,equity-percent: 20,杠杆 5x):
目标名义 = 173 × 20% × 5 = 173 USDT
BTC-USDT 1 张 = 64.16 USDT → 3 张 = 192.48 达成 111% 保证金 38.50
SNDK-USDT 1 张 = 1.68 USDT → 104 张 = 174.72 达成 101% 保证金 34.94
20% × 5x意味着单笔名义敞口 = 权益的 100%,即 1 倍账户杠杆。想让敞口只占权益 20%,equity-percent要填 4。- HTX 下单以「张」为单位,不是 USDT,也不是币。 v5 接口没有按金额下单的参数,所以取整误差不可避免。每张价值越接近目标名义,误差越大 —— BTC 一张 64 USDT,向上取整可能超出 11%;某些低杠杆合约甚至能超 46%。用
max-notional-usdt设硬顶可以兜住。 - 杠杆按合约分别设置。 同一账户里 BTC 可能是 5x 而 SOL 是 3x,导致不同标的的实际仓位大小不一致。程序按交易所实际值算并告警,要统一得去 HTX 逐个合约调。
close-mode: full(默认):调 HTX 市价全平接口,不传张数,由交易所按实际持仓全平。好处是没有「查到的张数在下单前已过期」的竞态,也保证带单员离场后本地不留残仓。代价是对方分批止盈时你一次性走完,并且会平掉你手动开的同方向仓位。close-mode: sized:按开仓那套换算算出张数再平,带reduce_only=1,与实际可平量取小。对方分批止盈时更贴近,但对方一次性清仓时你会留下残仓,而且后面不会再有平仓信号了。
cat binance-lead-monitor-stats.json
watch -n5 cat binance-lead-monitor-stats.json{
"uptime": "22 分",
"traders": {
"5075281354358777856": {
"name": "熬鹰资本",
"attempts": 23,
"failures": 0,
"failureRate": "0.0%",
"consecutiveFailures": 0,
"maxConsecutiveFailures": 0,
"errorCounts": {}
}
}
}errorCounts 按币安业务码归类,能直接看出是不是同一个毛病在反复出现。
每 stats-interval-ms 打一条,同时给累计和窗口两个口径:
[STAT] 拉取统计(已运行 3 小时 12 分,本窗口 1 小时)
熬鹰资本:累计 190/192 成功(失败率 1.0%),本窗口 60/60(失败率 0.0%)
错误分布 code=11012005 ×2
窗口内失败率超 stats-alert-rate 会推群(样本不足 3 次不判定)。
| 错误码 | 含义 | 处理 |
|---|---|---|
11012005 |
当前系统正忙 | 偶发抖动,占比高于 10% 时考虑调大 interval-ms |
11012030 |
当前项目已关闭 | 该带单员已停止带单,不是网络问题 |
~/Library/LaunchAgents/com.you.bn-lead-monitor.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.you.bn-lead-monitor</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/java</string>
<string>-jar</string>
<string>/绝对路径/target/test-1.0-SNAPSHOT.jar</string>
</array>
<!-- 必须设为项目根目录,否则读不到 config/application.yml -->
<key>WorkingDirectory</key><string>/绝对路径</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/绝对路径/logs/monitor.log</string>
<key>StandardErrorPath</key><string>/绝对路径/logs/monitor.err</string>
</dict>
</plist>mkdir -p logs
launchctl load ~/Library/LaunchAgents/com.you.bn-lead-monitor.plist
launchctl list | grep bn-lead # 确认已托管
launchctl unload ~/Library/LaunchAgents/com.you.bn-lead-monitor.plistWorkingDirectory 必须是项目根目录 —— 配置文件和状态文件都是相对路径。
- 绝对不要把密钥写进
src/main/resources/application.yml。那份文件会被打进 jar、也会进 git,它只放${ENV_VAR:}占位。真实凭证一律放config/application.yml(600 权限 + 已 gitignore)。 - HTX API Key 只勾「合约交易」,不要勾提币,并绑定 IP 白名单。
- 币安 Cookie 是账号级会话凭证,泄露等同账号被接管。本程序默认不需要它(公开带单数据免登录),别随手往配置里贴。
- 提交前自查:
git diff --cached | grep -iE 'secret|access-key|access_token|cookie|hook/'
unzip -p target/test-1.0-SNAPSHOT.jar BOOT-INF/classes/application.yml | grep -E 'key|token|url'| 现象 | 原因与处理 |
|---|---|
无法连接 www.binance.com / No route to host |
DNS 污染,必须配 bn.proxy-host |
bn.proxy-host 的值填成了配置项名 |
填成了 bn.proxy-host 字面量,应填 127.0.0.1 |
启动即报 至少要配一个带单员 |
portfolio-id 与 portfolio-ids 都是空的 |
htx.order-price-type 不是 v5 接受的委托类型 |
填了 v1 的 opponent/optimal_5,v5 只认 market/limit/post_only |
HTX 返回 err_code 6023 |
调到了 v1 老接口。统一账户下账户/持仓/下单必须用 /v5/* |
HTX 返回 code 1067 |
v5 查询/撤单类接口缺 margin_mode |
| 跟单日志全是「可用保证金不足」 | 已有持仓占满保证金,属正常保护;程序不会盲目丢单 |
| 「换算后不足 1 张」 | 目标名义小于一张的价值,调大 equity-percent 或 lever-rate |
| 「HTX 没有 XXX-USDT 这个合约」 | HTX 未上该标的,或需在 symbol-mapping 里手动映射 |
| 通知没收到但日志有 | 看日志里的 [WARN] [lark] / [dingtalk] 行;钉钉常见原因是关键词不匹配 |
| 新增带单员后刷了一屏历史 | 不应发生。基线按人记录,新人首轮静默入库 |
| 想重置去重 | 删掉 state-file,下次启动重新建基线 |
# 绕开 config/application.yml,只用 jar 内默认值
java -jar target/test-1.0-SNAPSHOT.jar \
--spring.config.location=classpath:/application.yml \
--bn.portfolio-ids=5075281354358777856 \
--bn.proxy-host=127.0.0.1 --bn.proxy-port=7890 \
--bn.state-file=./tmp-state.json \
--bn.notify.lark.url= --bn.notify.dingtalk.url= \
--htx.enabled=true --htx.dry-run=true命令行参数优先级最高。用独立的 state-file 和清空的 webhook,就不会污染线上去重、也不会误发到群里。
src/main/java/org/example/
├── binance/
│ ├── BinanceMonitorApplication 入口,@EnableScheduling
│ ├── LeadOrderMonitorTask @Scheduled 轮询、去重、编排
│ ├── LeadOrderClient 币安接口客户端(订单历史、带单员昵称)
│ ├── LeadOrder 一条带单记录,含指纹去重键
│ ├── SeenStore 去重状态持久化,按带单员记录基线
│ ├── PollStats 成功率统计与快照
│ ├── Notifier 多渠道通知
│ ├── MonitorProperties bn.* 配置
│ └── NotifyProperties bn.notify.* 配置
└── htx/
├── CopyTradeService 信号 → 委托的翻译与风控
├── HtxClient HTX v5 客户端
├── HtxSigner HMAC-SHA256 验签
├── CopyTradeResult 跟单结果
└── HtxProperties htx.* 配置
- 去重键是
portfolioId|fp:时间|币种|方向|持仓方向|价格|数量。币安该接口不返回订单 ID,只能用指纹。带单员前缀是必须的,否则两人同毫秒的相同操作会互相吞掉。昵称刻意不参与去重键 —— 对方改名不该让历史记录重推一遍。 - 落盘在下单之前。进程若在下单过程中被杀,重启后不会把同一批信号再执行一遍。
- 按人隔离拉取。单个带单员失败不影响其他人,只有全部失败才走整体告警。
- 币安倍数标的(
1000PEPEUSDT表示一手 1000 个 PEPE)会自动识别前缀并还原为单币口径,否则数量会差三个数量级。 - HTX v5 的
1000/10000前缀识别用的正则分支按长到短排列 —— 若1000在前,10000SATS会被错拆成1000+0SATS。
本项目仅供技术学习。加密货币合约交易风险极高,可能导致本金全部损失。使用者需自行承担全部后果,作者不对任何资金损失负责。开启自动跟单前请务必先用演练模式验证。