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、FixedProductMarketMakerABI一致的模拟类。这些类的方法如balanceOf,getOutcomeTokenAmount返回的是基于模拟状态计算出的数据而不是链上查询结果。状态管理器 (State Manager)负责维护整个模拟环境的状态。这包括用户虚拟余额每个模拟账户的USDC或其它结算代币和 outcome token结果代币代表“是”或“否”的份额余额。订单簿记录所有活跃的模拟订单包括订单ID、创建者、价格、数量、方向买入/卖出。市场状态模拟市场的解析结果、流动性池信息等。这些数据可以静态配置也可以从一个公开的API端点如Polymarket的官方API或一个索引器定期同步真实数据但交易行为不影响真实市场。数据馈送 (Data Feed)为了让模拟环境更真实项目需要接入市场数据。这可以是一个简单的模块从Polymarket的公共API获取市场列表、价格历史和订单簿快照。在模拟环境中你的“交易”不会影响这个数据馈送它只是提供一个真实的市场背景板。策略执行引擎虽然项目本身可能主要提供模拟环境但一个完整的“paper trader”通常会包含一个运行策略的框架。它按照一定频率如每分钟轮询数据馈送检查当前模拟状态然后调用用户定义的策略函数该函数再通过模拟客户端发出交易指令。注意开源项目的具体实现可能有所不同但上述组件是构建一个功能完整的预测市场模拟交易器所必需的逻辑模块。jchimbor/polymarket-paper-trader可能实现了其中的全部或大部分。2.3 技术栈选择分析从项目名称和常见实践推断该项目很可能基于Node.jsPython也是常见选择。技术栈可能包括运行时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: Mapstring, 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: MapAccountId, AccountState; private markets: Mapstring, Market; private allOpenOrders: Mapstring, 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(): Promisevoid { 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(): Promisevoid { 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: OmitOrder, 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: MapAccountId, AccountState): any { /* 转换Map为可序列化对象 */ } private _reviveAccounts(serialized: any): MapAccountId, AccountState { /* 反序列化 */ } private _reviveOrders(serialized: any): Mapstring, 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(): Promisevoid { await this.stateManager.loadState(); console.log(Paper client initialized for account: ${this.accountId}); } async shutdown(): Promisevoid { 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): PromiseMarket | 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): PromiseMarket[] { 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(): Promisevoid { 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(): Promisevoid { // 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): Promisevoid { 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: Mapstring, PaperClient new Map(); private strategies: any[] []; // 策略实例数组 private dataFeed: PolymarketAPIFeed; constructor() { this.dataFeed new PolymarketAPIFeed(); } async addStrategy(config: MeanReversionConfig, accountId: string default-trader): Promisevoid { 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(): Promisevoid { console.log(Starting all strategies...); const promises this.strategies.map(s s.start()); await Promise.all(promises); // 注意这里start()是无限循环Promise.all不会resolve // 实际应用中你可能需要更精细的控制例如通过信号量停止 } async stopAll(): Promisevoid { 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项目可以作为一个基础向多个方向扩展图形化回测界面使用ReactChart.js或D3.js构建一个Web界面可视化策略的资产曲线、持仓变化、交易信号点。多策略管理与资金分配开发一个框架可以同时运行多个策略并在不同策略间动态分配虚拟资金。更复杂的AMM模拟实现一个完整的常数乘积做市商CPMM模拟能够更真实地反映Polymarket上的价格滑点和流动性影响。集成更多数据源除了价格还可以集成社交媒体情绪数据、相关新闻事件等开发多因子策略。机器学习策略将模拟环境作为强化学习RL的“环境”训练AI智能体在预测市场中交易。构建一个完整的模拟交易系统是一项复杂的工程但它带来的价值是巨大的。它让你能够在安全的环境中将交易想法从模糊的概念转化为可测试、可度量、可迭代的自动化策略。jchimbor/polymarket-paper-trader这样的项目提供了一个坚实的起点剩下的就取决于你的创造力和对市场的理解了。记住在模拟中亏掉一百万虚拟美元所学到的东西远比在实盘中亏掉一百真实美元要便宜和深刻得多。