跳到主要内容

策略详解

每个账号在 rocky-bot 内对应一个策略实例 × 每个 symbol。三种策略类继承相似的循环骨架,只在"算 target / 判断要不要 place"这一步分化。

源码:rocky_bot/strategies/{ladder,anchor,taker}.py。如果不熟悉账号分布,请先看 做市机器人概览


一、共同循环骨架

三种策略都遵循:

async def run(self):
await asyncio.sleep(random.uniform(0, self.interval_s)) # 启动错峰
while True:
try:
await self.iterate_once()
except Exception as e:
log.warning("... iteration failed: %s", e)
self.circuit.record_api_error()
delay = self.interval_s + (random.random() * 2 - 1) * self.jitter_s
await asyncio.sleep(max(0.5, delay))

关键点:

  • 启动时随机延迟:30 账号同时启动,错峰避免对 backend 的瞬时压力 spike
  • 每轮 try/except:单次 iter 失败不杀死循环,记一次 API error 给 CircuitBreaker
  • 间隔抖动:cadence 加 ±jitter 防止 30 个账号刚好同步抢同一个时间片

每个策略的 iterate_once() 是核心逻辑,下面分别讲。


二、LadderMakerLoop(24 账号)

源码:rocky_bot/strategies/ladder.py

2.1 核心思想

每个 ladder 实例只做一件事:在一个固定的 bps offset 上、一个固定的 side(BUY 或 SELL)上、维护一个未成交的限价单。

例如 mm-l1-buy-05bps 账号:

  • BTC-PERP:始终保持有一个 BUY LIMIT 在 mid - 5 bps 附近
  • ETH-PERP:同上

2.2 关键逻辑

async def iterate_once(self):
if self.circuit.is_open():
return # CircuitBreaker 跳过
mid = self.feed.mid(self.symbol) # Binance bookTicker 的中价
target = mid * (1 + sign * offset_bps/10000)

balance, positions = await self.client.balance(), \
await self.client.position_risk(symbol=binance_sym)
# ↑ 同时拿来更新 CircuitBreaker.wallet 与下一步的 position-cap check

# POSITION-CAP GATE
would_be = pos_amt + sign(side) * qty
if abs(would_be * mark) > caps.max_notional_usdc:
cancel_existing_same_side_order(); return # 至 cap,不再加仓

# CANCEL-REPLACE-OR-PLACE
live = find_open_order(side == self.side)
if live:
drift = |live.price - target| / target
if drift <= 2 bps:
return # 现有挂单还在容差内,啥也不做
cancel(live) # 漂太远,撤掉

place_order(side, qty, price=target) # 挂新单

2.3 配置参数

参数默认值说明
interval_s3.0主循环周期
jitter_s1.0± 抖动
DRIFT_BPS0.0002 (2 bps)现有单价与目标价差超过这个就 re-quote
qtyaccounts.py::qty_for 按层级返回L1=0.0005 BTC, L2=0.001, L3=0.002
offset_bps5..100来自账号 ID 后缀(如 05bps

2.4 position-cap gate 的关键点

历史教训:早期版本没有这个 gate,导致每笔成交后 ladder 继续挂单 → 持仓单边累积 → wallet margin 被吃光 → -2010 insufficient balance 爆刷。

加上 gate 后:

  • 如果新挂单成交会让 |position| > cap直接不挂
  • 同时把当前已存在的同侧挂单 cancel 掉(避免别人 take 它继续加仓)
  • 对手方向不受限——总能 reduce position

详细历史 + 决策见 风控与杠杆


三、AnchorMakerLoop(1 账号)

源码:rocky_bot/strategies/anchor.py

3.1 核心思想

只有一个 anchor 账号(mm-anchor),承担收紧 inside spread 的责任。每轮同时报双边:

BUY at mid - 1 bps
SELL at mid + 1 bps

让其他 ladder/taker 看到一个"窄到 2 bps"的最佳买卖差。

3.2 关键逻辑

async def iterate_once(self):
mid = self.feed.mid(self.symbol)
target_bid = mid * (1 - 1bps)
target_ask = mid * (1 + 1bps)

positions = await self.client.position_risk(symbol=binance_sym)
pos_amt, mark, cap = ...

# 同时检查两边的 open orders
live_bid, live_ask = find_open_orders()

async def ensure(target_price, side, live):
# PER-SIDE POSITION-CAP GATE
would_be = pos_amt + sign(side) * qty
if abs(would_be * mark) > cap:
cancel(live); return # 这一侧到 cap,不挂

if live and |live.price - target| / target > 1bps:
cancel(live)
if not live:
place(side, qty, target)

await ensure(target_bid, "BUY", live_bid)
await ensure(target_ask, "SELL", live_ask)

3.3 与 Ladder 的关键差异

维度LadderMakerLoopAnchorMakerLoop
单 iteration 报价数1(仅自己的 side)2(同时报 BUY + SELL)
Position-cap gate一侧每侧独立判断
interval_s3.02.0(更快,因为要紧贴 mid)
DRIFT_BPS2 bps1 bps(更敏感)
qty按层级 0.0005~0.002 BTC固定 0.0003 BTC(更小,因为承担双向风险)

3.4 为什么只 1 个 anchor

  • 多个 anchor 互相 cancel 抢同一个价位,浪费 API quota
  • 单 anchor 容易调参(quota 占用、风险敞口)
  • L1 ladder 在 5 bps 提供"次紧"深度,1 个 anchor 在 1 bps 提供"最紧"已经足够

四、TakerLoop(5 账号)

源码:rocky_bot/strategies/taker.py

4.1 核心思想

不挂单,主动 cross 50 bps 过 mid 强行触发成交。每 30s ± 10s 一次。

每次决定 side 的逻辑:

  • 如果当前持仓的名义值超过 max_notional_usdc cap → 取与持仓反向的 side(强制减仓)
  • 否则随机一边
async def iterate_once(self):
mid = self.feed.mid(self.symbol)
positions = await self.client.position_risk(symbol=binance_sym)
pos_amt, mark = ...
notional = |pos_amt * mark|

if notional > caps.max_notional_usdc:
side = "SELL" if pos_amt > 0 else "BUY" # 强制减仓
else:
side = random.choice(["BUY", "SELL"]) # 随机两侧

qty = TAKE_QTY_BY_SYMBOL[self.symbol]
price = mid * (1 ± 50 bps) # 激进价

# 撤掉之前所有未成交 taker 单(防止积压锁仓)
for o in await self.client.open_orders(symbol):
await self.client.cancel_order(order_id=o["orderId"])

await self.client.place_order(
symbol=symbol, side=side, order_type="LIMIT",
quantity=qty, price=price,
)

4.2 参数

参数默认值说明
base_interval_s30.0主循环周期
jitter_s10.0± 抖动
TAKER_AGGRESSION0.005 (50 bps)跨过 mid 多少 bps 算"激进"
qty0.0005 BTC / 0.01 ETH与 L1 ladder 持平,确保能成交一份 ladder

4.3 为什么 5 个 taker

  • 5 × 2 symbols = 10 个 taker 任务,每个 ~30s 一次 = 平均 3s 一笔成交
  • 太少会出现"长时间无成交"的盘面
  • 太多会过快吃光 ladder 流动性

4.4 与 ladder 的对手关系

ladder 挂在 5-100 bps,taker 报在 ±50 bps:

  • taker BUY → 吃 L1+L2 的 SELL(5-30 bps 范围)
  • taker SELL → 吃 L1+L2 的 BUY
  • L3 的 50-100 bps 偶尔被吃到(如果 taker 路上 mid 移动)

这种结构让 inner ladder(L1)成交最频繁,outer ladder(L3)几乎不成交,与"漏斗"形态匹配。


五、main.py 的协调

源码:rocky_bot/main.py

async def _main_async(settings):
base_url, accounts = load_accounts(settings.keys_path) # 30 行的 .keys.json
feed = BinanceFeed(SYMBOLS)
circuits = {acc.id: CircuitBreaker(RiskCaps(max_notional_usdc=150.0)) for acc in accounts}
clients = {acc.id: RockyClient(httpc_for(acc), acc.key, acc.secret) for acc in accounts}

tasks = [asyncio.create_task(feed.run(), name="binance_feed")]
for acc in accounts:
for sym in SYMBOLS:
loop_cls = {ladder: LadderMakerLoop, anchor: AnchorMakerLoop, taker: TakerLoop}[acc.role]
tasks.append(asyncio.create_task(
loop_cls(client=clients[acc.id], feed=feed, symbol=sym, ..., circuit=circuits[acc.id]).run(),
name=f"{acc.role}[{acc.id},{sym}]",
))

# 等 SIGINT/SIGTERM
await stop_event.wait()
for t in tasks:
t.cancel()
feed.close()
await asyncio.gather(*tasks, return_exceptions=True)

要点:

  • 每个账号一个 RockyClient + CircuitBreaker,彼此隔离
  • 主进程靠 signal.SIGINT/SIGTERM 触发 stop_event,systemd systemctl --user stop rocky-bot 实现优雅退出
  • task name 包含 role + account_id + symbol,方便 journalctl 排查

六、下一步

  • 风控与杠杆 — CircuitBreaker / RiskCaps / position cap / LEVERAGE_V1 完整规则
  • 部署与运维 — 怎么把 30 账号一次性 mint 出来 + 上线