第 9 章 · 01 做市策略与回测(marketmaking) 本节摘要:本节精读 (约 417 行)—— 目录里最有教学价值的一段代码。它实现了一个完整的做市(Market Making)策略:用 异步循环,反复从 Coinbase/Binance 抓价格 → 在买卖两侧模拟挂单(吃价差 spread)→ 用 管理库存(base/quote 双向余额)→ 每笔成交写 CSV 日志。文件还带一个 函数,能对历史 CSV 数据回测策略,返回初始资金/终值/总收益/总笔数。本节重点讲清做市的三个核心概念——买卖价差盈利、库存风险管理、模拟成交而非真实下单,以及代码里值得学的工程细节( 配置、asyncio.gather 并发、多层降级数据源)。
本节摘要:本节精读
experimental/market_making.py(约 417 行)——experimental/目录里最有教学价值的一段代码。它实现了一个完整的做市(Market Making)策略:用asyncio异步循环,反复从 Coinbase/Binance 抓价格 → 在买卖两侧模拟挂单(吃价差 spread)→ 用current_inventory管理库存(base/quote 双向余额)→ 每笔成交写 CSV 日志。文件还带一个backtest_market_maker函数,能对历史 CSV 数据回测策略,返回初始资金/终值/总收益/总笔数。本节重点讲清做市的三个核心概念——买卖价差盈利、库存风险管理、模拟成交而非真实下单,以及代码里值得学的工程细节(dataclass配置、asyncio.gather 并发、多层降级数据源)。读完本节,你理解做市的逻辑骨架,也看清这段「实验代码」与生产做市系统的巨大鸿沟。
内容来源:原项目源码
experimental/market_making.py,逐行精读并套用体系化模板。
⚠️ 现实澄清:这段代码全部是模拟成交(
simulate_order只改内存数字),不会真去交易所下单。它演示做市逻辑,但离真实做市还差:真实的交易所连接、订单管理、撤单改单、对手风险、延迟竞争。教学价值高,实战价值为零(本节末详述)。
阅读完本节,你应当能够:
MarketMaker 类的 fetch/calculate/simulate/strategy 各方法。asyncio.gather 如何并发跑多个交易对的做市。backtest_market_maker 对历史 CSV 回测的逻辑与局限。做市商(Market Maker)是交易所里同时在买盘和卖盘挂单的角色。盈利原理:
代价是库存风险:如果价格单边下跌,你手里屯的 base 资产(如 BTC)会贬值。所以做市必须管理库存——不能无限吃入,到了上限要降价甩货。本文件的 max_inventory_exposure、rebalance_threshold 就是为此而设。
💡 核心心法:做市不是「预测涨跌」,而是「提供流动性赚价差」。它盈利的前提是市场有来回波动(买卖双方都来),且单边趋势不剧烈。单边行情里做市会被「单边屯货」亏死。
文件用 dataclass 定义配置(第 15-31 行),清晰且可改:
@dataclass class MarketMakingConfig: trading_pair: str = "BTC/USDT" # 交易对 total_capital: float = 10000.0 # 总资金(USDT 计) spread_percentage: float = 0.001 # 0.1% 价差 order_size_percentage: float = 0.01 # 每单占总资金 1% max_inventory_exposure: float = 0.2 # 单一资产最多占总资金 20% rebalance_threshold: float = 0.1 # 10% 偏离触发再平衡 min_profit_threshold: float = 0.002 # 0.2% 最小利润目标
逐字段:
spread_percentage=0.001:挂单时,买/卖各偏离中间价 0.05%(合计价差 0.1%)。order_size_percentage=0.01:每单大小是总资金的 1%——小单高频,控制风险。max_inventory_exposure=0.2:核心风控——单个资产最多占总资金 20%,防止单边屯太多。rebalance_threshold=0.1:库存偏离目标 10% 时再平衡。dataclass 的好处:默认值集中、可读、易改。生产系统会用 YAML/JSON 配置,但教学用 dataclass 完全够。
fetch_market_data(第 89-155 行)演示了一个健壮的数据抓取:主源失败→备用源→兜底模拟:
async def fetch_market_data(self) -> MarketData: async with aiohttp.ClientSession() as session: try: # 主源:Coinbase 公开 ticker(免费,无需 key) async with session.get( f"https://api.coinbase.com/v2/prices/{...}/spot" ) as response: if response.status == 200: data = await response.json() last_price = float(data["data"]["amount"]) spread = last_price * self.config.spread_percentage return MarketData( best_bid=last_price - spread / 2, best_ask=last_price + spread / 2, last_price=last_price, volume=0.0, ) except Exception as e: logger.error(f"Market data fetch error: {e}") try: # 备用源:Binance async with session.get( f"https://api.binance.com/api/v3/ticker/price?symbol={...}" ) as response: ... return MarketData(...) except Exception as inner_e: logger.error(f"Backup market data fetch error: {inner_e}") # 兜底:返回硬编码模拟价 return MarketData( best_bid=50000.0, best_ask=50100.0, last_price=50050.0, volume=100.0, )
观察:
50000/50100。两个公开免费源都挂了,也能继续跑(用假价)。这保证了循环不会因为取价失败而中断。last_price),代码自己 ±spread/2 算出 bid/ask——真实订单簿的价差未必是这个值。这是教学简化。volume=0.0:Coinbase 免费接口不给 volume,直接填 0——做市本应关心 volume(决定挂单深度),这里忽略。aiohttp:异步 HTTP 客户端,配合 asyncio 实现非阻塞抓取。💡 健壮性借鉴:三层降级是生产代码的好习惯——主源不可用时不致命。但「兜底硬编码假价」在生产做市里绝对不行(会用假价成交,亏死);这里只是为了教学循环不停。要分清「健壮」和「正确」的边界。
calculate_order_size(第 157-183 行)不是简单地「按比例算」,而是叠加了库存风控:
def calculate_order_size(self) -> float: total_value = ( self.current_inventory["base"] * self.market_data.last_price + self.current_inventory["quote"] ) order_size = ( self.config.total_capital * self.config.order_size_percentage / self.market_data.last_price ) # Risk management: Ensure order size doesn't exceed max inventory exposure max_order_size = ( self.config.total_capital * self.config.max_inventory_exposure / self.market_data.last_price ) return min(order_size, max_order_size)
逻辑:
order_size:理想下单量 = 总资金 × 1% / 当前价(换算成 base 数量)。max_order_size:最大允许下单量 = 总资金 × 20% / 当前价。min(order_size, max_order_size):取两者小,确保不超库存上限。这是做市风控的缩影:理想量 vs 上限量取小。即使策略想下大单,库存上限强制限流。生产做市的限流远比这复杂(还要考虑订单簿深度、对手盘、撤单率),但思路一致。
simulate_order(第 185-260 行)是「假装成交」的核心——只改内存余额、写 CSV,不发任何网络请求:
def simulate_order(self, order_type, price, amount) -> Dict: order_id = f"sim_{int(time.time() * 1000)}" if order_type == "buy": if self.current_inventory["quote"] >= price * amount: # USDT 够付吗 self.current_inventory["base"] += amount # base + self.current_inventory["quote"] -= price * amount # quote - logger.info(f"Simulated BUY: {amount} @ {price}") else: logger.warning("Insufficient funds for buy order") return {} elif order_type == "sell": if self.current_inventory["base"] >= amount: # base 够卖吗 self.current_inventory["base"] -= amount self.current_inventory["quote"] += price * amount ... # 写 CSV 日志 with open(self.csv_filename, "a", newline="") as csvfile: writer = csv.DictWriter(csvfile, fieldnames=[...]) writer.writerow({...}) return {"order_id": ..., "status": "filled"}
要点:
order_id = sim_...:前缀 sim_ 标明是模拟单,不会和真实订单号混。current_inventory 是 {"base": float, "quote": float},每次成交对应加减。这就是库存状态机。{"status": "filled"}——假回执,假装已成交。⚠️ 现实澄清:
simulate_order从不联网、不下单。它只是个会算账的内存状态机。跑这个脚本不会真买卖任何币。要真做市,得替换成交易所 API 的下单/撤单/查状态调用,那是一个数量级更复杂的工程。
market_making_strategy(第 262-294 行)是主循环:
async def market_making_strategy(self): while True: try: self.market_data = await self.fetch_market_data() # 抓价 order_size = self.calculate_order_size() # 算大小 buy_price = self.market_data.best_bid * (1 - self.config.spread_percentage / 2) self.simulate_order("buy", buy_price, order_size) # 买 sell_price = self.market_data.best_ask * (1 + self.config.spread_percentage / 2) self.simulate_order("sell", sell_price, order_size) # 卖 await asyncio.sleep(5) # 5 秒一轮 except Exception as e: logger.error(f"Market making strategy error: {e}") await asyncio.sleep(10)
逻辑清晰:抓价 → 算量 → 挂买单 → 挂卖单 → 睡 5 秒 → 循环。每轮在买卖各偏离 spread/2 的位置各模拟一单。出错睡 10 秒再试,保证循环不死。
run(第 296-303 行)和 main(第 306-325 行)演示多交易对并发:
async def run_market_makers(): market_makers = [MarketMaker(config) for config in configs] await asyncio.gather(*[mm.run() for mm in market_makers]) asyncio.run(run_market_makers())
asyncio.gather 把多个做市协程并发跑——BTC/USDT 和 ETH/USDT 同时做市,互不阻塞。这是 asyncio 在「IO 密集(等交易所响应)」场景的典型用法。
文件后半段(第 333-406 行)是个回测函数,对历史 CSV 跑策略:
def backtest_market_maker(historical_data_path, config): df = pd.read_csv(historical_data_path) # 读历史 K 线(需 close 列) initial_capital = config.total_capital current_inventory = {"base": 0.0, "quote": initial_capital} trades = [] for _, row in df.iterrows(): current_price = row["close"] spread = current_price * config.spread_percentage buy_price = current_price - spread / 2 sell_price = current_price + spread / 2 order_size = (initial_capital * config.order_size_percentage) / current_price # 模拟买 if current_inventory["quote"] >= buy_price * order_size: current_inventory["base"] += order_size current_inventory["quote"] -= buy_price * order_size trades.append({"type": "BUY", "price": buy_price, "amount": order_size}) # 模拟卖 if current_inventory["base"] >= order_size: current_inventory["base"] -= order_size current_inventory["quote"] += sell_price * order_size trades.append({"type": "SELL", "price": sell_price, "amount": order_size}) final_value = current_inventory["base"] * df.iloc[-1]["close"] + current_inventory["quote"] return {"initial_capital": ..., "final_value": ..., "total_return_percentage": ..., "total_trades": ...}
观察:
close 列(每根 K 线收盘价)。文件没带样例数据,historical_prices.csv 要自己准备。💡 回测的陷阱:回测假设「我挂的单一定能成交」,但真实市场里,你的单只在价格触到你时才成交,且可能被更快的对手抢走。本回测每根 K 都默认买卖全成,会高估收益。生产级回测要建模成交概率、滑点、手续费、延迟,复杂得多。
这段代码的教学价值在于逻辑骨架,但它离真实做市还有巨大鸿沟:
| 维度 | 本代码 | 生产做市 |
|---|---|---|
| 下单 | simulate_order 内存假成交 |
真实交易所 WebSocket 下单/撤单/改单 |
| 成交假设 | 挂了必成 | 挂了不一定成(取决于对手盘+延迟) |
| 价差来源 | 中间价 ± 固定比例 | 真实订单簿深度,动态调整 |
| 手续费 | 无 | 每单扣费(直接影响盈亏平衡) |
| 延迟竞争 | 不考虑 | 毫秒级,慢了被高频抢光 |
| 库存风控 | 简单上限 | 动态对冲、跨品种、风险预算 |
| 数据 | 公开免费 spot | L2 订单簿、私有成交流 |
所以这段代码直接拿来跑实盘必亏——没手续费建模就会高估收益,没真实成交建模就会在快速行情里挂不出单。它的价值是「理解做市的因果链:抓价 → 算价差 → 挂双单 → 管库存 → 记日志」,而不是「能用的做市系统」。
尽管不生产级,这段代码有几个值得学的工程点:
calculate_order_size 里 min(理想, 上限),风控是算量的一部分而非外挂。{"base", "quote"} 双向余额,每笔加减——简洁的账本模型。calculate_order_size 用 min(order_size, max_order_size) 限流,库存上限是做市命门。current_inventory + 写 CSV,不联网不下单;前缀 sim_ 标明模拟。asyncio.gather 并发跑多交易对。下一节,我们看
experimental/里另一段代码——btc_agent.py,它用 WebSocket 实时监听 BTC 地址,每笔交易都喂给 Agent 分析,展示「事件驱动 + LLM」的组合。