Polymarket预测市场模拟交易沙盒:零风险学习DeFi交易策略开发
1. 项目概述:一个模拟链上预测市场的交易沙盒
如果你对加密货币、DeFi或者预测市场感兴趣,大概率听说过Polymarket。这是一个建立在Polygon链上的去中心化预测市场平台,用户可以就各类事件(从“某国央行是否会降息”到“某部电影的首周票房”)创建市场并进行交易。交易的标的物是“是”或“否”的份额,价格在0到1美元之间波动,代表市场对该事件发生概率的共识。这听起来很酷,但直接拿真金白银去链上交易,对于新手来说门槛不低:需要钱包、需要理解Gas费、需要承担市场波动风险。这时候,一个名为jchimbor/polymarket-paper-trader的开源项目就派上了用场。
简单来说,这是一个“纸上谈兵”的模拟交易工具。它允许你使用一个虚拟的、离线的环境,来模拟与Polymarket智能合约的交互,进行下单、取消、结算等全套操作,而无需连接真实的区块链网络,也无需花费一分钱。项目的核心价值在于,它为学习者、策略开发者甚至是想熟悉Polymarket UI交互的用户,提供了一个零风险、零成本的沙盒环境。你可以在这里反复试错,验证你的交易想法,理解市场机制,而不用担心因为操作失误或策略失败而损失资产。对于想要深入理解预测市场运作机制,或者为开发更复杂的交易机器人做前期准备的开发者来说,这无疑是一个极佳的起点。
2. 核心架构与设计思路拆解
2.1 为什么需要模拟交易器?
在深入代码之前,我们先要理解模拟交易器的必要性。预测市场交易,尤其是链上的,涉及多个复杂层面:
- 合约交互逻辑:你需要理解Polymarket的智能合约ABI,知道如何调用
createOrder、cancelOrder、redeemWinnings等方法,并处理返回数据和事件。 - 市场数据解析:你需要从链上或索引器(如The Graph)获取市场详情、订单簿、交易历史,并正确解析。
- 交易策略逻辑:基于市场数据,你的程序需要做出买卖决策,计算下单价格、数量。
- 资金与风险管理:管理你的资产余额,计算持仓盈亏,设置止损止盈(如果策略需要)。
如果直接在上述所有环节都使用真实环境和资金,开发调试将异常痛苦。一个错误的循环可能导致资金被锁,一个价格计算失误可能导致巨额亏损。模拟交易器将第4点(资金)虚拟化,并通常在第1点(合约交互)和第3点(策略逻辑)之间插入一个模拟层,从而将策略逻辑与真实的链上风险完全隔离。
2.2polymarket-paper-trader的核心设计
jchimbor/polymarket-paper-trader项目的设计巧妙地模拟了真实环境。通过分析其代码结构,我们可以梳理出它的核心组件:
- 模拟客户端 (PaperClient):这是项目的核心类。它模仿了像
ethers.js或web3.py这样的区块链客户端库的接口。当你调用client.createOrder(...)时,它并不会真的向区块链发送交易,而是将订单记录在一个内存或持久化的存储中(如一个JSON文件或本地数据库)。它同样会模拟返回一个“交易哈希”和触发相应的“事件”,以供你的策略代码监听。 - 模拟合约 (Mock Contracts):项目内很可能定义了一系列与Polymarket核心合约(如ConditionalTokens、FixedProductMarketMaker)ABI一致的模拟类。这些类的方法(如
balanceOf,getOutcomeTokenAmount)返回的是基于模拟状态计算出的数据,而不是链上查询结果。 - 状态管理器 (State Manager):负责维护整个模拟环境的状态。这包括:
- 用户虚拟余额:每个模拟账户的USDC(或其它结算代币)和 outcome token(结果代币,代表“是”或“否”的份额)余额。
- 订单簿:记录所有活跃的模拟订单,包括订单ID、创建者、价格、数量、方向(买入/卖出)。
- 市场状态:模拟市场的解析结果、流动性池信息等。这些数据可以静态配置,也可以从一个公开的API端点(如Polymarket的官方API或一个索引器)定期同步真实数据,但交易行为不影响真实市场。
- 数据馈送 (Data Feed):为了让模拟环境更真实,项目需要接入市场数据。这可以是一个简单的模块,从Polymarket的公共API获取市场列表、价格历史和订单簿快照。在模拟环境中,你的“交易”不会影响这个数据馈送,它只是提供一个真实的市场背景板。
- 策略执行引擎:虽然项目本身可能主要提供模拟环境,但一个完整的“paper trader”通常会包含一个运行策略的框架。它按照一定频率(如每分钟)轮询数据馈送,检查当前模拟状态,然后调用用户定义的策略函数,该函数再通过模拟客户端发出交易指令。
注意:开源项目的具体实现可能有所不同,但上述组件是构建一个功能完整的预测市场模拟交易器所必需的逻辑模块。
jchimbor/polymarket-paper-trader可能实现了其中的全部或大部分。
2.3 技术栈选择分析
从项目名称和常见实践推断,该项目很可能基于Node.js(Python也是常见选择)。技术栈可能包括:
- 运行时:Node.js。因其在Web3领域的广泛生态,拥有丰富的库支持。
- 核心模拟库:可能会用到
jest或sinon等测试库中的mock功能来模拟合约调用,也可能完全自行实现。 - 数据获取:
axios或node-fetch用于从Polymarket API获取真实市场数据。 - 状态存储:简单的
JSON文件用于持久化模拟账户状态和交易历史,便于回溯分析。对于更复杂的场景,可能会用到SQLite或LowDB。 - 工具库:
ethers.js的类型定义可能被用来确保模拟客户端与真实客户端接口一致,但实际网络请求被拦截。
这种技术栈选择平衡了开发效率、与现有Web3生态的兼容性以及轻量化的需求。
3. 环境搭建与项目初始化实操
假设我们想要基于或参考jchimbor/polymarket-paper-trader来搭建自己的模拟交易环境。以下是详细的步骤和要点。
3.1 基础环境准备
首先,确保你的开发机已安装 Node.js (版本16或18 LTS为佳) 和 npm/yarn/pnpm 包管理器。
# 检查Node.js和npm版本 node --version npm --version # 克隆项目仓库(如果项目公开) git clone https://github.com/jchimbor/polymarket-paper-trader.git cd polymarket-paper-trader # 安装依赖 npm install # 或使用 yarn yarn install如果原项目没有提供,或者你想从头搭建,可以初始化一个新项目:
mkdir my-poly-paper-trader cd my-poly-paper-trader npm init -y3.2 核心依赖安装
我们需要安装一些核心包来构建模拟器。以下是一个可能的package.json依赖项示例:
{ "dependencies": { "axios": "^1.6.0", // 用于获取真实市场数据 "lodash": "^4.17.21", // 实用工具函数 "decimal.js": "^10.4.3" // 高精度金融计算,避免JavaScript浮点数精度问题 }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.0.0", // 推荐使用TypeScript以获得更好的类型安全和IDE支持 "ts-node": "^10.9.0", "jest": "^29.0.0" // 单元测试,也可用于模拟 } }安装命令:npm install axios lodash decimal.js以及npm install -D typescript ts-node @types/node jest @types/jest。
实操心得:强烈建议使用TypeScript。在模拟金融合约时,订单价格、数量、余额等都具有明确的类型(如字符串表示的wei单位,或Decimal类型)。TypeScript能在编译期捕捉大量类型错误,比如误将字符串当数字计算,这在JavaScript中会导致难以调试的bug。
decimal.js是处理金融计算的必备品,永远不要用原生的number类型进行涉及货币的计算。
3.3 项目结构规划
一个清晰的项目结构有助于管理复杂度。参考如下布局:
my-poly-paper-trader/ ├── src/ │ ├── core/ │ │ ├── PaperClient.ts # 模拟客户端核心类 │ │ ├── StateManager.ts # 状态管理(余额、订单簿) │ │ └── types.ts # 全局类型定义(订单、市场等) │ ├── data/ │ │ ├── Feed.ts # 市场数据馈送接口 │ │ └── PolymarketAPIFeed.ts # 具体的Polymarket API实现 │ ├── strategies/ # 交易策略目录 │ │ └── SimpleMeanReversion.ts # 示例策略 │ ├── simulation/ │ │ └── Engine.ts # 策略执行引擎 │ └── index.ts # 程序入口 ├── tests/ # 单元测试 ├── config/ # 配置文件 ├── data/ # 本地持久化数据(如账户状态JSON) ├── package.json └── tsconfig.json4. 核心模块实现深度解析
4.1 定义数据类型(src/core/types.ts)
这是构建类型安全系统的基石。我们需要定义模拟环境中所有核心实体的形状。
// src/core/types.ts import Decimal from 'decimal.js'; export type AccountId = string; // 模拟账户地址,可以用‘trader-1’这样的字符串 export interface Market { id: string; question: string; outcomes: string[]; // 例如 ['YES', 'NO'] volume?: Decimal; liquidity?: Decimal; resolution?: Date; isResolved: boolean; } export interface OutcomeTokenBalance { outcomeIndex: number; // 对应 outcomes 数组的索引 balance: Decimal; } export interface AccountState { accountId: AccountId; collateralBalance: Decimal; // 模拟的USDC余额 outcomeTokenBalances: Map<string, OutcomeTokenBalance[]>; // marketId -> 该市场的代币余额数组 openOrders: Order[]; } // 订单方向 export enum OrderSide { BUY = 'buy', SELL = 'sell' } // 订单状态 export enum OrderStatus { PENDING = 'pending', FILLED = 'filled', CANCELLED = 'cancelled', PARTIALLY_FILLED = 'partially_filled' } export interface Order { orderId: string; marketId: string; outcomeIndex: number; side: OrderSide; price: Decimal; // 每股价格,0-1之间 amount: Decimal; // 订单数量(股数) remainingAmount: Decimal; status: OrderStatus; createdAt: Date; filledAt?: Date; filledPrice?: Decimal; }4.2 实现状态管理器(src/core/StateManager.ts)
状态管理器是模拟环境的“单一数据源”。它必须保证在多步操作下的状态一致性。
// src/core/StateManager.ts import { AccountId, AccountState, Market, Order, OrderSide, OrderStatus } from './types'; import Decimal from 'decimal.js'; import fs from 'fs/promises'; import path from 'path'; export class StateManager { private accounts: Map<AccountId, AccountState>; private markets: Map<string, Market>; private allOpenOrders: Map<string, Order>; // orderId -> Order private dataFilePath: string; constructor(dataDir: string = './data') { this.accounts = new Map(); this.markets = new Map(); this.allOpenOrders = new Map(); this.dataFilePath = path.join(dataDir, 'simulation_state.json'); this.initializeDefaultAccount(); } private initializeDefaultAccount(): void { const defaultAccountId: AccountId = 'default-trader'; this.accounts.set(defaultAccountId, { accountId: defaultAccountId, collateralBalance: new Decimal(1000), // 初始虚拟资金1000 USDC outcomeTokenBalances: new Map(), openOrders: [] }); } async loadState(): Promise<void> { try { const data = await fs.readFile(this.dataFilePath, 'utf-8'); const savedState = JSON.parse(data); // 注意:JSON.parse 不会恢复 Decimal 类型和 Map,需要手动转换 this.accounts = this._reviveAccounts(savedState.accounts); this.allOpenOrders = this._reviveOrders(savedState.openOrders); console.log('State loaded from file.'); } catch (error) { console.log('No saved state found, starting fresh.'); } } async saveState(): Promise<void> { const stateToSave = { accounts: this._serializeAccounts(this.accounts), openOrders: Array.from(this.allOpenOrders.values()).filter(o => o.status === OrderStatus.PENDING || o.status === OrderStatus.PARTIALLY_FILLED) }; await fs.mkdir(path.dirname(this.dataFilePath), { recursive: true }); await fs.writeFile(this.dataFilePath, JSON.stringify(stateToSave, null, 2), 'utf-8'); } // 关键方法:创建订单 createOrder(accountId: AccountId, order: Omit<Order, 'orderId' | 'status' | 'createdAt' | 'remainingAmount'>): Order { const account = this.getAccount(accountId); const newOrder: Order = { ...order, orderId: `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, status: OrderStatus.PENDING, createdAt: new Date(), remainingAmount: order.amount }; // 检查资金是否充足 (简化逻辑) if (order.side === OrderSide.BUY) { const maxCost = order.price.mul(order.amount); if (account.collateralBalance.lessThan(maxCost)) { throw new Error(`Insufficient collateral. Needed: ${maxCost}, Available: ${account.collateralBalance}`); } // 预冻结资金 (真实链上合约会处理) account.collateralBalance = account.collateralBalance.minus(maxCost); } else if (order.side === OrderSide.SELL) { // 检查是否有足够的 outcome token 可卖 const tokenBalances = account.outcomeTokenBalances.get(order.marketId) || []; const targetTokenBalance = tokenBalances.find(t => t.outcomeIndex === order.outcomeIndex); if (!targetTokenBalance || targetTokenBalance.balance.lessThan(order.amount)) { throw new Error(`Insufficient outcome tokens to sell. Outcome Index: ${order.outcomeIndex}`); } // 预冻结代币 targetTokenBalance.balance = targetTokenBalance.balance.minus(order.amount); } account.openOrders.push(newOrder); this.allOpenOrders.set(newOrder.orderId, newOrder); return newOrder; } // 关键方法:尝试撮合订单 (一个简化的连续双拍卖逻辑) matchOrders(marketId: string, outcomeIndex: number): { fills: Array<{orderId: string, filledAmount: Decimal, price: Decimal}> } { const marketOrders = Array.from(this.allOpenOrders.values()) .filter(o => o.marketId === marketId && o.outcomeIndex === outcomeIndex && o.status === OrderStatus.PENDING) .sort((a, b) => { // 买单按价格降序排列(价高者优先),卖单按价格升序排列(价低者优先) if (a.side === OrderSide.BUY && b.side === OrderSide.BUY) return b.price.comparedTo(a.price); if (a.side === OrderSide.SELL && b.side === OrderSide.SELL) return a.price.comparedTo(b.price); // 通常买单和卖单分开处理,这里简化返回0 return 0; }); const buys = marketOrders.filter(o => o.side === OrderSide.BUY); const sells = marketOrders.filter(o => o.side === OrderSide.SELL); const fills = []; // 简化撮合:遍历最高买单和最低卖单 let i = 0, j = 0; while (i < buys.length && j < sells.length) { const buyOrder = buys[i]; const sellOrder = sells[j]; if (buyOrder.price.greaterThanOrEqualTo(sellOrder.price)) { // 可以成交 const fillAmount = Decimal.min(buyOrder.remainingAmount, sellOrder.remainingAmount); const fillPrice = buyOrder.price.plus(sellOrder.price).div(2); // 取中间价,实际可能更复杂 // 更新订单状态 buyOrder.remainingAmount = buyOrder.remainingAmount.minus(fillAmount); sellOrder.remainingAmount = sellOrder.remainingAmount.minus(fillAmount); if (buyOrder.remainingAmount.equals(0)) buyOrder.status = OrderStatus.FILLED; else buyOrder.status = OrderStatus.PARTIALLY_FILLED; if (sellOrder.remainingAmount.equals(0)) sellOrder.status = OrderStatus.FILLED; else sellOrder.status = OrderStatus.PARTIALLY_FILLED; buyOrder.filledAt = new Date(); sellOrder.filledAt = new Date(); fills.push({ orderId: buyOrder.orderId, filledAmount: fillAmount, price: fillPrice }); fills.push({ orderId: sellOrder.orderId, filledAmount: fillAmount, price: fillPrice }); // 更新账户余额和持仓 (这里需要更复杂的清算逻辑,简化处理) this._settleTrade(buyOrder.accountId, sellOrder.accountId, marketId, outcomeIndex, fillAmount, fillPrice); if (buyOrder.remainingAmount.equals(0)) i++; if (sellOrder.remainingAmount.equals(0)) j++; } else { // 最高买价低于最低卖价,无法继续成交 break; } } // 清理已完全成交的订单 this._cleanupFilledOrders(); return { fills }; } private _settleTrade(buyerId: AccountId, sellerId: AccountId, marketId: string, outcomeIndex: number, amount: Decimal, price: Decimal): void { // 这是一个极度简化的结算。真实情况涉及手续费、滑点、以及买卖双方账户的精确借贷记。 const buyer = this.getAccount(buyerId); const seller = this.getAccount(sellerId); // 买方获得代币,支付资金 const buyerTokenBalances = buyer.outcomeTokenBalances.get(marketId) || []; let buyerToken = buyerTokenBalances.find(t => t.outcomeIndex === outcomeIndex); if (!buyerToken) { buyerToken = { outcomeIndex, balance: new Decimal(0) }; buyerTokenBalances.push(buyerToken); } buyerToken.balance = buyerToken.balance.plus(amount); buyer.outcomeTokenBalances.set(marketId, buyerTokenBalances); // 注意:买方资金在创建订单时已预冻结,这里只需处理实际成交部分的资金转移(或释放未成交部分) // 卖方失去代币,获得资金 const sellerTokenBalances = seller.outcomeTokenBalances.get(marketId) || []; let sellerToken = sellerTokenBalances.find(t => t.outcomeIndex === outcomeIndex); if (!sellerToken) { // 卖方理论上应该有代币才能卖,这里防御性处理 sellerToken = { outcomeIndex, balance: new Decimal(0) }; sellerTokenBalances.push(sellerToken); } sellerToken.balance = sellerToken.balance.minus(amount); // 创建订单时已预扣除,这里确保一致性 seller.outcomeTokenBalances.set(marketId, sellerTokenBalances); const saleValue = price.mul(amount); seller.collateralBalance = seller.collateralBalance.plus(saleValue); // 卖方收到资金 // 买方资金在创建订单时已扣除,这里无需再扣 } getAccount(accountId: AccountId): AccountState { const account = this.accounts.get(accountId); if (!account) throw new Error(`Account ${accountId} not found`); return account; } // ... 其他辅助方法,如 getMarket, addMarket, cancelOrder 等 private _serializeAccounts(accountsMap: Map<AccountId, AccountState>): any { /* 转换Map为可序列化对象 */ } private _reviveAccounts(serialized: any): Map<AccountId, AccountState> { /* 反序列化 */ } private _reviveOrders(serialized: any): Map<string, Order> { /* 反序列化 */ } private _cleanupFilledOrders(): void { /* 从 openOrders 列表中移除已成交订单 */ } }注意事项:撮合引擎是模拟交易的核心,也是复杂度最高的部分。上面的
matchOrders方法是一个极度简化的连续双拍卖模型。真实的Polymarket使用基于自动做市商(AMM)的流动性池模型,价格由公式x * y = k决定。一个更高级的模拟器需要实现这个AMM模型。对于入门学习,简化模型足以理解订单创建、资金检查、状态更新等核心流程。
4.3 构建模拟客户端(src/core/PaperClient.ts)
模拟客户端是对外提供API的门面,它应该模仿真实Web3库的调用方式。
// src/core/PaperClient.ts import { StateManager } from './StateManager'; import { AccountId, Market, Order, OrderSide } from './types'; import Decimal from 'decimal.js'; export class PaperClient { private stateManager: StateManager; private accountId: AccountId; constructor(accountId: AccountId = 'default-trader') { this.stateManager = new StateManager(); this.accountId = accountId; } async initialize(): Promise<void> { await this.stateManager.loadState(); console.log(`Paper client initialized for account: ${this.accountId}`); } async shutdown(): Promise<void> { await this.stateManager.saveState(); console.log('State saved.'); } // 模拟“发送交易” async createOrder( marketId: string, outcomeIndex: number, side: OrderSide, amount: Decimal, price: Decimal ): Promise<{ hash: string; order: Order }> { try { const orderInput = { marketId, outcomeIndex, side, amount, price, accountId: this.accountId }; const order = this.stateManager.createOrder(this.accountId, orderInput); // 模拟链上交易哈希 const mockTxHash = `0x_mock_${order.orderId}`; // 创建订单后,可以尝试进行一次撮合(模拟链上交易被矿工打包后更新状态) // 在实际设计中,撮合可能由独立的引擎定时触发 // this.stateManager.matchOrders(marketId, outcomeIndex); console.log(`[PaperTrade] Order created: ${order.orderId}, Side: ${side}, Amount: ${amount}, Price: ${price.toFixed(4)}`); return { hash: mockTxHash, order }; } catch (error: any) { console.error(`[PaperTrade] Failed to create order: ${error.message}`); throw error; } } async getBalance(): Promise<{ collateral: Decimal; positions: Array<{marketId: string, outcomeIndex: number, balance: Decimal}> }> { const account = this.stateManager.getAccount(this.accountId); const positions = []; for (const [marketId, tokens] of account.outcomeTokenBalances.entries()) { for (const token of tokens) { if (token.balance.greaterThan(0)) { positions.push({ marketId, outcomeIndex: token.outcomeIndex, balance: token.balance }); } } } return { collateral: account.collateralBalance, positions }; } // 模拟“查询合约” async getMarket(marketId: string): Promise<Market | null> { // 这里可以集成真实的数据馈送,返回模拟的市场对象 // 或者从 stateManager 中获取预加载的市场信息 return this.stateManager.getMarket(marketId); } async cancelOrder(orderId: string): Promise<{ hash: string }> { // 实现取消订单逻辑,更新状态,释放预冻结的资金或代币 this.stateManager.cancelOrder(this.accountId, orderId); console.log(`[PaperTrade] Order cancelled: ${orderId}`); return { hash: `0x_mock_cancel_${orderId}` }; } }4.4 集成真实市场数据(src/data/PolymarketAPIFeed.ts)
为了让模拟环境有真实感,我们需要从Polymarket获取真实的市场数据。这通常通过其公共API或子图(The Graph)完成。
// src/data/PolymarketAPIFeed.ts import axios from 'axios'; import { Market } from '../core/types'; import Decimal from 'decimal.js'; export class PolymarketAPIFeed { private baseURL: string; constructor(baseURL: string = 'https://gamma-api.polymarket.com') { this.baseURL = baseURL; } async fetchActiveMarkets(limit: number = 50): Promise<Market[]> { try { // 注意:Polymarket的API端点可能发生变化,此处为示例 const response = await axios.get(`${this.baseURL}/markets`, { params: { limit, active: true } }); const marketsData = response.data; // 假设返回数据格式符合预期 return marketsData.map((m: any) => ({ id: m.id || m.slug, question: m.question, outcomes: m.outcomes || ['YES', 'NO'], volume: new Decimal(m.volume || 0), liquidity: new Decimal(m.liquidity || 0), resolution: m.resolution ? new Date(m.resolution) : undefined, isResolved: m.resolved || false })); } catch (error) { console.error('Failed to fetch markets from Polymarket API:', error); return []; } } async fetchMarketPriceData(marketId: string): Promise<{ yesPrice: Decimal; noPrice: Decimal; lastUpdated: Date }> { try { // 示例:获取某个市场的实时价格数据 const response = await axios.get(`${this.baseURL}/markets/${marketId}/prices`); const data = response.data; // 假设返回 { yesPrice: "0.65", noPrice: "0.35" } return { yesPrice: new Decimal(data.yesPrice || 0.5), noPrice: new Decimal(data.noPrice || 0.5), lastUpdated: new Date() }; } catch (error) { console.error(`Failed to fetch price for market ${marketId}:`, error); // 返回默认值 return { yesPrice: new Decimal(0.5), noPrice: new Decimal(0.5), lastUpdated: new Date() }; } } }5. 策略开发与回测引擎实战
有了模拟环境,我们就可以在上面运行交易策略了。策略引擎负责循环执行“获取数据 -> 运行策略逻辑 -> 下达指令”的流程。
5.1 定义一个简单策略(src/strategies/SimpleMeanReversion.ts)
均值回归是一种常见的策略思想:假设价格会围绕某个均值波动,当价格偏离均值过多时,倾向于它会回归。
// src/strategies/SimpleMeanReversion.ts import { PaperClient } from '../core/PaperClient'; import { PolymarketAPIFeed } from '../data/PolymarketAPIFeed'; import { OrderSide } from '../core/types'; import Decimal from 'decimal.js'; export interface MeanReversionConfig { marketId: string; lookbackPeriod: number; // 观察的历史数据点数 deviationThreshold: number; // 触发交易的偏离阈值,例如 0.05 表示5% tradeAmount: Decimal; // 每次交易的数量 checkIntervalMs: number; // 检查间隔(毫秒) } export class SimpleMeanReversionStrategy { private client: PaperClient; private dataFeed: PolymarketAPIFeed; private config: MeanReversionConfig; private priceHistory: Decimal[] = []; private isRunning: boolean = false; constructor(client: PaperClient, dataFeed: PolymarketAPIFeed, config: MeanReversionConfig) { this.client = client; this.dataFeed = dataFeed; this.config = config; } async start(): Promise<void> { if (this.isRunning) return; this.isRunning = true; console.log(`Starting MeanReversion strategy for market: ${this.config.marketId}`); while (this.isRunning) { try { await this.runIteration(); } catch (error) { console.error('Error in strategy iteration:', error); } await this.delay(this.config.checkIntervalMs); } } stop(): void { this.isRunning = false; console.log('Strategy stopped.'); } private async runIteration(): Promise<void> { // 1. 获取最新市场数据 const priceData = await this.dataFeed.fetchMarketPriceData(this.config.marketId); const currentYesPrice = priceData.yesPrice; // 2. 更新价格历史 this.priceHistory.push(currentYesPrice); if (this.priceHistory.length > this.config.lookbackPeriod) { this.priceHistory.shift(); // 保持固定长度 } // 3. 计算均值回归信号 (需要足够的历史数据) if (this.priceHistory.length < this.config.lookbackPeriod) { console.log(`Collecting data... (${this.priceHistory.length}/${this.config.lookbackPeriod})`); return; } const meanPrice = this.priceHistory.reduce((sum, price) => sum.plus(price), new Decimal(0)) .div(this.priceHistory.length); const deviation = currentYesPrice.minus(meanPrice).div(meanPrice).abs(); // 相对偏离度 // 4. 决策与下单 if (deviation.greaterThan(this.config.deviationThreshold)) { const side = currentYesPrice.lessThan(meanPrice) ? OrderSide.BUY : OrderSide.SELL; // 简单逻辑:价格低于均值买YES,高于均值卖YES(或买NO,这里简化) console.log(`Signal detected! Current: ${currentYesPrice.toFixed(4)}, Mean: ${meanPrice.toFixed(4)}, Dev: ${deviation.toFixed(4)}. Action: ${side} YES`); try { const result = await this.client.createOrder( this.config.marketId, 0, // 假设 outcomeIndex 0 代表 YES side, this.config.tradeAmount, currentYesPrice // 以当前价格下单 ); console.log(`Order placed: ${result.order.orderId}`); } catch (error: any) { console.log(`Order failed: ${error.message}`); } } else { console.log(`No signal. Current: ${currentYesPrice.toFixed(4)}, Mean: ${meanPrice.toFixed(4)}, Dev: ${deviation.toFixed(4)}`); } // 5. 打印当前账户状态 const balance = await this.client.getBalance(); console.log(`Account - Collateral: ${balance.collateral.toFixed(2)}, Positions: ${balance.positions.length}`); } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } }5.2 策略执行引擎(src/simulation/Engine.ts)
引擎负责管理多个策略的生命周期,并可能提供更复杂的调度和监控。
// src/simulation/Engine.ts import { PaperClient } from '../core/PaperClient'; import { PolymarketAPIFeed } from '../data/PolymarketAPIFeed'; import { SimpleMeanReversionStrategy, MeanReversionConfig } from '../strategies/SimpleMeanReversion'; export class SimulationEngine { private clients: Map<string, PaperClient> = new Map(); private strategies: any[] = []; // 策略实例数组 private dataFeed: PolymarketAPIFeed; constructor() { this.dataFeed = new PolymarketAPIFeed(); } async addStrategy(config: MeanReversionConfig, accountId: string = 'default-trader'): Promise<void> { let client = this.clients.get(accountId); if (!client) { client = new PaperClient(accountId); await client.initialize(); this.clients.set(accountId, client); } const strategy = new SimpleMeanReversionStrategy(client, this.dataFeed, config); this.strategies.push(strategy); console.log(`Strategy added for market ${config.marketId} on account ${accountId}`); } async runAll(): Promise<void> { console.log('Starting all strategies...'); const promises = this.strategies.map(s => s.start()); await Promise.all(promises); // 注意:这里start()是无限循环,Promise.all不会resolve // 实际应用中,你可能需要更精细的控制,例如通过信号量停止 } async stopAll(): Promise<void> { console.log('Stopping all strategies...'); this.strategies.forEach(s => s.stop()); // 保存所有客户端状态 for (const client of this.clients.values()) { await client.shutdown(); } } }5.3 主程序入口(src/index.ts)
最后,我们将一切组合起来,形成一个可运行的程序。
// src/index.ts import { SimulationEngine } from './simulation/Engine'; import Decimal from 'decimal.js'; async function main() { const engine = new SimulationEngine(); // 配置一个策略:交易Polymarket上某个特定市场(需要真实的市场ID) const config = { marketId: 'clk8vg1jp0001l308k7sz1q1q', // 示例,需替换为真实有效的市场slug或ID lookbackPeriod: 20, // 观察最近20个价格点 deviationThreshold: 0.03, // 价格偏离均值3%时触发 tradeAmount: new Decimal(10), // 每次买卖10股 checkIntervalMs: 60000 // 每分钟检查一次 }; await engine.addStrategy(config); // 启动引擎 await engine.runAll(); // 设置一个停止条件,例如运行1小时后停止 setTimeout(async () => { console.log('Simulation time elapsed. Stopping...'); await engine.stopAll(); process.exit(0); }, 60 * 60 * 1000); // 1小时 // 优雅关闭处理 process.on('SIGINT', async () => { console.log('\nReceived SIGINT. Shutting down gracefully...'); await engine.stopAll(); process.exit(0); }); } main().catch(console.error);运行程序:npx ts-node src/index.ts。你将看到控制台输出策略的决策过程、订单创建信息以及账户状态变化。
6. 常见问题、调试技巧与进阶方向
6.1 模拟交易中的常见陷阱
- 价格来源与撮合逻辑不一致:这是最大的失真源。如果你的数据馈送来自Polymarket API(反映真实AMM价格),但你的撮合引擎使用的是简单的订单簿匹配,那么你的模拟成交价和仓位变化将与真实情况相差甚远。解决方案:要么实现一个简化但原理一致的AMM模拟器(根据买卖量计算价格变化),要么你的策略只使用“市价单”逻辑,并假设总能以数据馈送提供的价格瞬间成交(忽略滑点)。
- 忽略Gas费和手续费:真实交易需要支付Polygon网络的Gas费和Polymarket可能收取的交易手续费。在模拟中忽略这些,会高估策略收益。解决方案:在
StateManager的_settleTrade方法中,引入一个固定的或按比例计算的费用扣除步骤。 - 浮点数精度问题:JavaScript的
number类型在进行金融计算时会导致精度丢失。解决方案:如前所述,全程使用decimal.js或big.js这类高精度数学库。 - 状态持久化与恢复:如果模拟程序意外崩溃,所有内存中的状态都会丢失。解决方案:像我们示例中那样,定期(如每次订单变化后)将关键状态(账户余额、持仓、未成交订单)序列化到磁盘。更健壮的做法是使用SQLite数据库。
- 时间处理:模拟中时间可以是“事件驱动”(来一笔数据算一次)或“实时驱动”。回测时,你需要按历史数据的时间戳顺序处理。解决方案:明确你的时间模型。对于实时模拟,用
setInterval;对于历史回测,需要按时间顺序遍历数据点。
6.2 调试与日志记录
- 结构化日志:不要只用
console.log。使用winston或pino等日志库,将不同级别的信息(错误、警告、信息、调试)输出到文件和控制台,并包含时间戳、策略名称、市场ID等上下文。 - 快照与复盘:定期(例如每天)保存一份完整的市场状态和账户状态的快照。当策略出现异常行为时,你可以加载某个历史快照,重新运行并添加更详细的调试日志来定位问题。
- 单元测试:为
StateManager、PaperClient的核心方法编写单元测试。特别是订单创建、资金检查、撮合逻辑和结算逻辑,确保它们在各种边界条件下(如余额不足、价格为0、数量极大)行为正确。
6.3 从模拟到实盘的挑战
模拟交易通过后,迈向实盘仍需克服以下障碍:
- 真实的Web3连接:将
PaperClient替换为真正的ethers.jsProvider和Signer,连接到Polygon主网或测试网。 - 钱包与私钥管理:安全地管理私钥,用于签名交易。绝对不要将私钥硬编码在代码中!使用环境变量或专业的密钥管理服务。
- 交易发送与确认:处理交易发送、Gas价格估算、nonce管理、交易确认等待、失败重试等复杂逻辑。需要考虑网络拥堵情况。
- 错误处理与监控:实盘代码必须有极其健壮的错误处理和报警机制。交易失败、网络中断、API限流等情况必须能被捕获并通知到你(如通过Telegram Bot、Discord Webhook)。
- 策略频率与成本:在模拟中你可以每秒检查一次。在实盘中,高频调用RPC节点和发送交易会产生巨大的Gas成本。你需要优化策略频率,权衡信号收益与交易成本。
6.4 项目扩展与进阶方向
jchimbor/polymarket-paper-trader项目可以作为一个基础,向多个方向扩展:
- 图形化回测界面:使用
React+Chart.js或D3.js构建一个Web界面,可视化策略的资产曲线、持仓变化、交易信号点。 - 多策略管理与资金分配:开发一个框架,可以同时运行多个策略,并在不同策略间动态分配虚拟资金。
- 更复杂的AMM模拟:实现一个完整的常数乘积做市商(CPMM)模拟,能够更真实地反映Polymarket上的价格滑点和流动性影响。
- 集成更多数据源:除了价格,还可以集成社交媒体情绪数据、相关新闻事件等,开发多因子策略。
- 机器学习策略:将模拟环境作为强化学习(RL)的“环境”,训练AI智能体在预测市场中交易。
构建一个完整的模拟交易系统是一项复杂的工程,但它带来的价值是巨大的。它让你能够在安全的环境中,将交易想法从模糊的概念,转化为可测试、可度量、可迭代的自动化策略。jchimbor/polymarket-paper-trader这样的项目提供了一个坚实的起点,剩下的,就取决于你的创造力和对市场的理解了。记住,在模拟中亏掉一百万虚拟美元所学到的东西,远比在实盘中亏掉一百真实美元要便宜和深刻得多。
