1. 项目概述与核心价值
最近在逛GitHub的时候,发现了一个挺有意思的项目,叫agent-next/polymarket-paper-trader。光看这个名字,可能很多朋友会有点懵,这到底是个啥?简单来说,这是一个基于agent-next框架,专门为Polymarket这个预测市场平台开发的模拟交易机器人。你可以把它理解为一个“纸上谈兵”的量化交易系统,让你在不投入真金白银的情况下,去测试你的交易策略、熟悉Polymarket的API、甚至学习如何构建一个自动化的交易Agent。
我自己是做量化策略开发和智能体(Agent)系统设计的,对这个组合特别敏感。Polymarket是一个基于区块链的预测市场,你可以买卖关于未来事件(比如“某候选人能否赢得选举”、“某球队能否夺冠”)的“股票”。agent-next则是一个新兴的、旨在简化AI Agent开发的框架。把这两者结合起来,做一个模拟交易器,这个想法本身就很有实践价值。它解决的痛点非常明确:在真实资金入场前,提供一个安全、可控、可复现的策略验证沙盒。无论是想学习DeFi预测市场玩法的新手,还是希望为自己的Polymarket策略进行回测和优化的开发者,这个项目都是一个极佳的起点。
这个项目的核心价值在于“模拟”和“学习”。它剥离了真实交易中的资金风险和心理压力,让你可以纯粹地关注策略逻辑、市场数据分析和系统稳定性。通过构建和迭代这个模拟交易机器人,你能深入理解预测市场的定价机制、订单簿动态,以及如何用程序化的方式捕捉市场信号。接下来,我就结合自己的经验,把这个项目从设计思路到实操细节,完整地拆解一遍。
2. 核心架构与设计思路拆解
2.1 为什么是“Agent” + “Paper Trader”?
看到agent-next这个前缀,就决定了这个项目的技术底色不是简单的脚本,而是一个具备一定自主决策能力的智能体系统。在交易场景下,“Agent”通常意味着它能够感知环境(市场数据)、根据预设策略或模型进行推理、然后执行动作(下单、撤单)。agent-next框架可能提供了诸如工具调用、记忆管理、任务规划等基础能力,让开发者能更专注于交易逻辑本身,而不是去从头搭建一个Agent运行时。
而“Paper Trader”(模拟交易)则是量化领域的必备环节。任何策略在实盘前,都必须经过严格的模拟测试。一个合格的Paper Trader需要精准地模拟真实交易的各个环节:资金计算、订单撮合、仓位管理、手续费扣除、滑点模拟等。在Polymarket的语境下,还需要模拟其特有的“二元期权”式结算(要么赢取全部份额,要么损失全部投入)以及AMM(自动化做市商)模式的流动性提供与提取。
这个项目的设计思路,我推测是构建一个模块化的系统:
- 数据感知层:持续从Polymarket的API或子图(The Graph)获取市场行情、订单簿、个人仓位等数据。
- 策略逻辑层:这是核心,可能是一个简单的规则引擎(如“价格低于0.3时买入”),也可能集成更复杂的模型。它根据感知到的数据,生成交易信号。
- 模拟执行层:接收策略层的信号,但并不向区块链发送真实交易,而是在本地维护一个虚拟账户,模拟下单、成交、清算的全过程,并记录详细的交易日志和绩效报告。
- Agent协调层:利用
agent-next的能力,可能将上述过程组织成可规划、可回溯的任务流。例如,定期执行“数据收集->分析->决策->模拟执行”的循环,或者在特定事件(如市场波动率激增)触发时执行应急策略。
2.2 关键技术栈选型考量
虽然项目描述可能没有明说,但基于Polymarket(部署在Polygon链上)和agent-next(通常与JavaScript/TypeScript生态关联)的特性,我们可以合理推断其技术栈:
- 语言与框架:TypeScript是首选。类型安全对金融相关代码至关重要。
agent-next框架本身很可能就是TS/JS系的。运行环境可能是Node.js。 - 区块链交互:需要用到ethers.js或viem这类库来与Polygon链交互,读取Polymarket的智能合约数据。但注意,在Paper Trader模式下,大部分“写操作”(交易)是被拦截并模拟的,只有“读操作”是真实发生的。
- 数据获取:Polymarket的数据可能来自:
- 官方API:获取市场列表、基本信息。
- The Graph 子图:这是获取历史交易、订单簿深度、用户持仓等链上数据最高效的方式,避免了直接扫描区块链的低效。
- WebSocket 流:用于实时监听市场订单和交易事件,实现低延迟的模拟撮合。
- 状态管理与存储:模拟交易需要持久化虚拟账户状态、订单历史和绩效数据。简单的可以用SQLite或JSON文件,复杂的可能会用到PostgreSQL。
agent-next框架可能自带了某种状态管理或记忆模块。 - 策略开发:可能会提供插件化的接口,允许用户用JavaScript/TypeScript编写自己的策略函数,并注入到系统中。
注意:在模拟交易中,时间管理是一个关键且容易出错的地方。你需要决定是使用“事件驱动”(一有市场事件就触发)还是“轮询模式”(定期检查),同时要处理好历史回测时的“时间旅行”问题——即让系统能够以历史数据为基础,按照时间顺序推进模拟,而不是直接拿到所有未来数据。
3. 核心模块解析与实操要点
3.1 模拟账户与账本系统
这是Paper Trader的基石,必须设计得严谨。一个虚拟账户至少需要跟踪以下信息:
interface PaperAccount { initialBalance: number; // 初始模拟资金(以USDC计价) currentBalance: number; // 当前可用资金 totalPnl: number; // 总盈亏 positions: Map<string, PaperPosition>; // 各市场持仓,key为市场ID orderHistory: PaperOrder[]; // 所有订单历史 tradeHistory: PaperTrade[]; // 所有成交历史 }实操要点与坑点:
- 计价单位:Polymarket中通常使用USDC稳定币作为抵押和计价单位。你的模拟账户也应以USDC为本位币。所有盈亏计算都需换算成USDC。
- 仓位表示:Polymarket的每个市场有多个结果(例如“是”和“否”)。持仓不再是简单的“多/空”,而是持有某个结果的一定数量的“份额”。你的
PaperPosition需要记录marketId,outcome(结果索引),shares(份额数量),avgCost(平均成本价)等。 - 订单撮合逻辑:这是最复杂的部分。Polymarket采用AMM机制,价格由公式决定。你不能简单地用“最新价”成交。模拟时,你需要:
- 根据订单方向(买/卖)和数量,依据当前池子流动性,计算出预估成交价和滑点。
- 或者,更精确但更复杂的方法是,在本地维护一个简化版的AMM池状态,模拟添加/移除流动性对价格的影响。
- 常见坑:忽略了交易手续费(Polymarket有手续费)。你的模拟必须扣除手续费,否则绩效会虚高。
- 账本更新:任何成交发生后,必须原子化地更新账户余额和仓位。顺序是:计算成交额 -> 扣除手续费 -> 更新现金余额 -> 更新对应仓位份额和成本 -> 记录交易。这里一定要做好异常处理,避免状态不一致。
3.2 市场数据馈送与事件处理
模拟交易要逼真,数据源必须可靠且实时。
历史数据回放(回测模式):
- 数据源:从The Graph子图批量获取历史市场创建、交易、流动性变化等事件。
- 核心实现:你需要一个“事件循环”或“时间控制器”,按时间戳顺序将这些历史事件喂给策略和模拟引擎。策略只能基于“当前”及之前的数据做出决策。
- 技巧:对于大规模回测,可以考虑将历史数据预处理并存储在本地的时序数据库(如InfluxDB)或压缩文件中,以加速读取。
实时数据流(实盘模拟模式):
- 数据源:订阅Polymarket的WebSocket事件流,或者以较高频率(如每秒)轮询The Graph获取最新状态。
- 实现:使用一个事件监听器,当收到新的
Trade、OrderBookChange等事件时,触发策略的onMarketData回调函数。 - 注意事项:网络延迟和API速率限制是现实问题。你的模拟系统需要处理数据延迟、丢失和重复的情况。可以引入一个“模拟延迟”参数,让成交比信号晚几秒发生,更贴近现实。
3.3 策略引擎集成
项目很可能提供了一个策略接口。一个最基本的口子可能长这样:
interface TradingStrategy { name: string; // 初始化,传入配置 initialize(config: StrategyConfig): Promise<void>; // 核心:接收市场数据,返回交易信号 onMarketEvent(event: MarketEvent, context: TradingContext): Promise<TradingSignal[]>; // 生命周期钩子 onStart?(): Promise<void>; onStop?(): Promise<void>; }实操心得:
- 策略上下文:
TradingContext非常重要,它应该提供当前模拟账户状态、历史绩效、以及访问其他市场数据的工具函数。策略不应该直接操作账户,而是通过返回清晰的信号(如{ marketId: ‘xxx’, outcome: 0, action: ‘BUY’, amountUsd: 100 }),由执行层统一处理。 - 避免未来函数:在回测中,策略函数
onMarketEvent被调用时,只能接触到该事件时间点及之前的数据。确保你的策略逻辑没有不小心“窥探”到未来的数据,这是回测失真的主要来源。可以通过严格的事件时间戳管理来防范。 - 参数化:好的策略应该将所有可调参数(如阈值、仓位比例)外部化,方便进行参数优化扫描。
4. 从零搭建与核心环节实现
假设我们现在要参考agent-next/polymarket-paper-trader的思路,自己动手实现一个简化版。以下是关键步骤。
4.1 环境准备与项目初始化
首先,创建一个TypeScript项目并安装核心依赖。
# 初始化项目 mkdir my-polymarket-paper-trader cd my-polymarket-paper-trader npm init -y # 安装TypeScript和类型定义 npm install typescript ts-node @types/node --save-dev npx tsc --init # 安装区块链交互和数据获取库 npm install viem @polymarket/clob-client axios # 安装agent-next框架(假设其npm包名为agent-next) npm install agent-next # 安装存储和工具库 npm install sqlite3 dotenv npm install --save-dev @types/sqlite3配置tsconfig.json,确保target为ES2020或更高,module为commonjs或NodeNext。
4.2 构建模拟引擎核心
我们创建一个PaperTradingEngine.ts文件。这是系统的心脏。
import { Database } from 'sqlite3'; import { Market, OrderBook, Trade } from './types'; // 自定义类型 export class PaperTradingEngine { private db: Database; private account: PaperAccount; private orderBookSim: OrderBookSimulator; constructor(initialBalance: number, dbPath: string) { this.account = this.initializeAccount(initialBalance); this.db = new Database(dbPath); this.orderBookSim = new OrderBookSimulator(); this.setupDatabase(); } private initializeAccount(balance: number): PaperAccount { return { initialBalance: balance, currentBalance: balance, totalPnl: 0, positions: new Map(), orderHistory: [], tradeHistory: [] }; } // 接收策略信号,生成模拟订单 public async placeOrder(signal: TradingSignal): Promise<string> { const orderId = this.generateOrderId(); const paperOrder: PaperOrder = { id: orderId, ...signal, status: 'PENDING', createdAt: Date.now() }; // 1. 检查账户余额是否足够(对于买单) if (signal.action === 'BUY' && this.account.currentBalance < signal.amountUsd) { paperOrder.status = 'REJECTED'; this.account.orderHistory.push(paperOrder); await this.saveOrder(paperOrder); throw new Error(`Insufficient balance. Needed: ${signal.amountUsd}, Available: ${this.account.currentBalance}`); } // 2. 将订单加入历史 this.account.orderHistory.push(paperOrder); await this.saveOrder(paperOrder); // 3. 尝试立即撮合(简化版,实际应根据市场深度模拟) await this.attemptOrderMatching(paperOrder); return orderId; } private async attemptOrderMatching(order: PaperOrder): Promise<void> { // 这里是核心撮合逻辑 // 需要根据order.marketId获取当前市场AMM状态或订单簿快照 const marketState = await this.fetchMarketState(order.marketId); // 调用订单簿模拟器,计算成交价和成交量 const fillResult = this.orderBookSim.simulateFill(order, marketState); if (fillResult.filledAmountUsd > 0) { // 更新订单状态 order.status = 'FILLED'; order.filledPrice = fillResult.avgPrice; order.filledAmountUsd = fillResult.filledAmountUsd; // 创建成交记录 const trade: PaperTrade = { tradeId: this.generateTradeId(), orderId: order.id, ...fillResult, timestamp: Date.now() }; this.account.tradeHistory.push(trade); // **关键:更新账户余额和仓位** await this.updateAccountOnTrade(trade, order); await this.saveTrade(trade); await this.updateOrder(order); } else { order.status = 'OPEN'; // 未完全成交,留在订单簿(如果模拟的话) } } private async updateAccountOnTrade(trade: PaperTrade, order: PaperOrder): Promise<void> { const { marketId, outcome } = order; const { filledAmountUsd, avgPrice, fee } = trade; // 计算获得的份额。Polymarket中,买入1 USDC的“是”份额,在当前价格为P时,获得 1/P 个份额。 const sharesAcquired = filledAmountUsd / avgPrice; // 扣除手续费后的净资金变动 const netCashFlow = -filledAmountUsd - fee; // 买入为负支出 // 更新现金余额 this.account.currentBalance += netCashFlow; // 更新或创建仓位 const positionKey = `${marketId}-${outcome}`; let position = this.account.positions.get(positionKey); if (!position) { position = { marketId, outcome, shares: 0, totalCost: 0, avgCost: 0 }; } // 计算新的平均成本:(原总成本 + 新投入资金) / (原份额 + 新份额) const newTotalCost = position.totalCost + filledAmountUsd; const newTotalShares = position.shares + sharesAcquired; position.avgCost = newTotalCost / newTotalShares; position.shares = newTotalShares; position.totalCost = newTotalCost; this.account.positions.set(positionKey, position); // 更新总盈亏(这里是实现盈亏,未实现盈亏需根据市场价格计算) // 简化处理,这里只记录资金变动。更完整的需要定期根据市价重估仓位。 this.account.totalPnl += netCashFlow; } // ... 省略数据库操作和其他辅助方法 }这个引擎类处理了订单接收、撮合模拟和账户更新的核心循环。OrderBookSimulator是一个需要你根据Polymarket的AMM公式(通常是基于对数市场评分规则LMSR或常数乘积)实现的复杂模块。
4.3 集成Agent-Next框架
agent-next框架的作用是让这个交易机器人更“智能”和“自主”。我们创建一个TradingAgent.ts。
import { Agent, Tool, State } from 'agent-next'; // 假设的导入 export class TradingAgent { private agent: Agent; private engine: PaperTradingEngine; private strategy: TradingStrategy; constructor(engine: PaperTradingEngine, strategy: TradingStrategy) { this.engine = engine; this.strategy = strategy; this.initializeAgent(); } private initializeAgent() { // 1. 定义Agent可以使用的工具(Tools) const tools: Tool[] = [ { name: 'get_market_data', description: '获取指定市场的最新数据,包括价格、流动性、交易量。', execute: async (args: { marketId: string }) => { return await this.fetchMarketDataFromGraph(args.marketId); } }, { name: 'place_paper_order', description: '在模拟引擎中下一个订单。', execute: async (args: { signal: TradingSignal }) => { const orderId = await this.engine.placeOrder(args.signal); return `Order placed with ID: ${orderId}`; } }, { name: 'get_account_summary', description: '获取当前模拟账户的概况,包括余额、持仓、盈亏。', execute: async () => { return this.engine.getAccountSummary(); } } ]; // 2. 定义Agent的初始状态和系统提示词 const initialState: State = { memory: [], currentGoal: 'Monitor Polymarket and execute trading strategy.' }; const systemPrompt = `You are a Polymarket paper trading bot. Your goal is to execute the predefined strategy (“${this.strategy.name}”) by analyzing market data and placing simulated orders. You have access to tools to get data and place orders. Always think step by step.`; // 3. 实例化Agent this.agent = new Agent({ tools, initialState, systemPrompt, model: 'gpt-4' // 或本地模型,这里假设框架支持LLM集成 }); } // 启动Agent的主循环 public async run() { console.log('Trading Agent started.'); // 这里可以设计成事件驱动或定时轮询 // 例如,每30秒让Agent“思考”一次 setInterval(async () => { await this.agentStep(); }, 30000); } private async agentStep() { // 1. Agent自主获取市场数据(通过工具) const marketList = await this.fetchActiveMarkets(); for (const market of marketList.slice(0, 3)) { // 示例:只监控前3个市场 // 2. 触发策略逻辑(这里策略可以作为一个“子工具”或直接计算) // 更复杂的做法是让LLM分析数据后决定是否调用策略,这里简化,直接调用策略函数。 const signals = await this.strategy.onMarketEvent( { type: 'TIMER', marketId: market.id, timestamp: Date.now() }, this.engine.getTradingContext() ); // 3. 如果有信号,让Agent使用工具下单 for (const signal of signals) { const response = await this.agent.run(`The strategy suggests a ${signal.action} order on market ${signal.marketId} for $${signal.amountUsd}. Please execute this.`); console.log('Agent response:', response); } } } }这个TradingAgent将策略引擎、模拟执行和LLM的推理规划能力结合了起来。agent-next框架负责管理工具调用、维护对话状态(记忆),并根据系统提示和当前目标驱动整个流程。策略本身可以作为Agent的一个“内部模块”或一个特殊的“工具”来触发。
5. 常见问题、排查技巧与性能优化
在实际开发和运行这样一个系统时,你会遇到不少坑。下面是一些典型问题及解决思路。
5.1 数据一致性与同步问题
- 问题:模拟账户的仓位和现金余额与根据交易历史重新计算的结果对不上。
- 排查:
- 启用详细日志:记录每一笔成交前后的账户快照。
- 实现对账函数:定期(如每100笔交易)运行一个函数,根据完整的
tradeHistory重新计算理论上的余额和仓位,与引擎当前状态对比。不一致时抛出错误并保存上下文。 - 检查浮点数精度:金融计算中,使用
decimal.js或big.js库来处理小数,避免JavaScript浮点数精度误差累积。
- 技巧:将核心的账本更新函数(如
updateAccountOnTrade)设计为纯函数或具有原子性,并进行单元测试,用大量随机生成的交易信号进行压力测试。
5.2 回测与实盘模拟的差异
- 问题:策略在回测中表现优异,但切换到实时模拟时绩效大幅下滑。
- 原因:
- 未来数据泄露:回测时不小心使用了未来数据(如用到了当根K线的收盘价)。
- 滑点与手续费模拟不充分:回测假设以中间价成交且无滑点,实盘模拟加入了更真实的撮合模型。
- 网络延迟与数据粒度:回测数据可能是分钟级或小时级,而实盘是Tick级,噪音更大。
- 解决:
- 严格的事件时间戳:在回测引擎中,确保策略函数被调用时,只能访问时间戳小于等于“当前模拟时间”的数据。
- 更精细的撮合模型:在回测中也引入基于订单簿的撮合模拟和手续费。
- 在回测中加入“未来函数”检测器。
5.3 策略逻辑与Agent决策的耦合度
- 问题:策略逻辑写死在Agent的思考循环里,难以替换和测试。
- 建议:采用松耦合设计。策略模块应独立,只负责接收数据、返回信号。Agent负责协调工作流(何时获取数据、调用哪个策略、如何处理信号)。可以使用配置文件或依赖注入来动态加载策略。
5.4 性能优化
- 数据缓存:对从The Graph或API获取的市场基础信息进行缓存,避免重复请求。
- 批量处理:在回测模式下,批量读取和处理历史事件,减少I/O操作。
- 异步并发:同时监控多个市场时,使用异步并发(如Promise.all)获取数据,但注意API的速率限制。
- 选择性回放:如果只测试特定策略,可以只加载相关市场的历史数据,而不是全量数据。
构建一个像agent-next/polymarket-paper-trader这样的项目,远不止是写一个下单脚本。它涉及对预测市场机制的理解、对量化回测系统的构建、以及对AI Agent架构的应用。这个过程本身,就是一次对DeFi、量化交易和智能体编程的深度实践。从模拟账户的一分一厘核算,到市场数据的毫秒级处理,再到策略与Agent的有机融合,每一个环节都能挖出不少学问。我最深的体会是,模拟系统的可靠性是策略信心的唯一来源。一个连虚拟账都算不平的系统,给出的任何回测结果都毫无意义。所以,多花时间在引擎的单元测试和对账逻辑上,绝对是值得的。当你看到自己的策略在模拟环境中历经市场波动,产生一条平滑的权益曲线时,那种感觉,比直接实盘梭哈要踏实得多。