polymarket-arbitrage-bot

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Polymarket Arbitrage Bot

Polymarket套利机器人

Skill by ara.so — Daily 2026 Skills collection.
TypeScript bot automating the dump-and-hedge strategy on Polymarket's 15-minute Up/Down markets (BTC, ETH, SOL, XRP). Detects sharp price drops, buys the dipped side, then hedges the opposite outcome when combined cost falls below a profit threshold.
ara.so开发的Skill — 2026每日Skill合集。
基于TypeScript开发的机器人,可在Polymarket的15分钟涨跌市场(BTC、ETH、SOL、XRP)中自动执行抛售对冲策略。它会检测价格大幅下跌,买入下跌侧的份额,当组合成本低于盈利阈值时,买入相反结果的份额进行对冲。

Installation

安装

bash
git clone https://github.com/infraform/polymarket-arbitrage-bot.git
cd polymarket-arbitrage-bot
npm install
npm run build
cp .env.example .env
bash
git clone https://github.com/infraform/polymarket-arbitrage-bot.git
cd polymarket-arbitrage-bot
npm install
npm run build
cp .env.example .env

Key Commands

核心命令

CommandDescription
npm start
Run compiled bot (simulation by default)
npm run sim
Explicitly run in simulation (no real orders)
npm run prod
Run with real trades (
PRODUCTION=true
)
npm run dev
Run TypeScript directly via ts-node
npm run build
Compile TypeScript to
dist/
Always test with
npm run sim
before enabling production mode.
命令描述
npm start
运行编译后的机器人(默认模拟模式)
npm run sim
显式在模拟模式下运行(不发起真实订单)
npm run prod
运行真实交易模式(
PRODUCTION=true
npm run dev
通过ts-node直接运行TypeScript代码
npm run build
将TypeScript编译到
dist/
目录
开启生产模式前请务必先使用
npm run sim
测试。

Project Structure

项目结构

src/
├── main.ts            # Entry point, config load, market discovery, wiring
├── config.ts          # Loads/validates .env into typed config
├── api.ts             # Gamma + CLOB API client (markets, orderbook, orders, redemption)
├── monitor.ts         # Orderbook snapshot polling, strategy callback driver
├── dumpHedgeTrader.ts # Dump detection, leg1/leg2, stop-loss, P&L tracking
├── models.ts          # Shared types: Market, OrderBook, TokenPrice, etc.
└── logger.ts          # history.toml append log + stderr output
src/
├── main.ts            # 入口文件,配置加载、市场发现、逻辑组装
├── config.ts          # 加载/校验.env文件并转为带类型的配置
├── api.ts             # Gamma + CLOB API客户端(市场、订单簿、订单、兑换)
├── monitor.ts         # 订单簿快照轮询、策略回调驱动
├── dumpHedgeTrader.ts # 下跌检测、第一/第二笔交易、止损、盈亏跟踪
├── models.ts          # 共享类型:Market、OrderBook、TokenPrice等
└── logger.ts          # history.toml追加日志 + stderr输出

Environment Configuration

环境配置

Create
.env
from
.env.example
:
env
undefined
.env.example
创建
.env
文件:
env
undefined

--- Wallet & Auth (required for production) ---

--- 钱包与身份验证(生产模式必填) ---

PRIVATE_KEY=0x_your_private_key_here PROXY_WALLET_ADDRESS=0x_your_proxy_wallet_address SIGNATURE_TYPE=2 # 0=EOA, 1=Proxy, 2=GnosisSafe
PRIVATE_KEY=0x_your_private_key_here PROXY_WALLET_ADDRESS=0x_your_proxy_wallet_address SIGNATURE_TYPE=2 # 0=EOA, 1=Proxy, 2=GnosisSafe

--- Optional explicit CLOB API credentials ---

--- 可选显式配置CLOB API凭证 ---

If not set, credentials are derived from signer automatically

如不设置,凭证将自动从签名方派生

API_KEY= API_SECRET= API_PASSPHRASE=
API_KEY= API_SECRET= API_PASSPHRASE=

--- API Endpoints (defaults are production Polymarket) ---

--- API端点(默认指向Polymarket生产环境) ---

--- Markets ---

--- 交易市场 ---

MARKETS=btc # comma-separated: btc,eth,sol,xrp
MARKETS=btc # 逗号分隔:btc,eth,sol,xrp

--- Polling ---

--- 轮询配置 ---

CHECK_INTERVAL_MS=1000 MARKET_CLOSURE_CHECK_INTERVAL_SECONDS=20
CHECK_INTERVAL_MS=1000 MARKET_CLOSURE_CHECK_INTERVAL_SECONDS=20

--- Strategy Parameters ---

--- 策略参数 ---

DUMP_HEDGE_SHARES=10 # Shares per leg DUMP_HEDGE_SUM_TARGET=0.95 # Hedge when leg1 + opposite_ask <= this DUMP_HEDGE_MOVE_THRESHOLD=0.15 # 15% drop triggers dump detection DUMP_HEDGE_WINDOW_MINUTES=2 # Watch window at period start DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES=5 DUMP_HEDGE_STOP_LOSS_PERCENTAGE=0.2
DUMP_HEDGE_SHARES=10 # 每笔交易的份额数 DUMP_HEDGE_SUM_TARGET=0.95 # 当第一笔交易成本+反向卖价<=该值时执行对冲 DUMP_HEDGE_MOVE_THRESHOLD=0.15 # 下跌15%触发下跌检测 DUMP_HEDGE_WINDOW_MINUTES=2 # 周期开启后的观察窗口时长 DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES=5 DUMP_HEDGE_STOP_LOSS_PERCENTAGE=0.2

--- Mode ---

--- 运行模式 ---

PRODUCTION=false # true = real trades
undefined
PRODUCTION=false # true = 真实交易
undefined

Core Types (models.ts)

核心类型(models.ts)

typescript
// Key shared types used throughout the bot
interface Market {
  conditionId: string;
  questionId: string;
  tokens: Token[];         // [upToken, downToken]
  startTime: number;
  endTime: number;
  asset: string;           // "BTC", "ETH", etc.
}

interface Token {
  tokenId: string;
  outcome: string;         // "Up" or "Down"
}

interface OrderBook {
  tokenId: string;
  outcome: string;
  bids: PriceLevel[];
  asks: PriceLevel[];
  bestBid: number;
  bestAsk: number;
}

interface TokenPrice {
  tokenId: string;
  outcome: string;
  bestBid: number;
  bestAsk: number;
  timestamp: number;
}

interface MarketSnapshot {
  upPrice: TokenPrice;
  downPrice: TokenPrice;
  timeRemainingSeconds: number;
  periodStart: number;
}
typescript
// 整个机器人使用的核心共享类型
interface Market {
  conditionId: string;
  questionId: string;
  tokens: Token[];         // [upToken, downToken]
  startTime: number;
  endTime: number;
  asset: string;           // "BTC", "ETH", etc.
}

interface Token {
  tokenId: string;
  outcome: string;         // "Up" or "Down"
}

interface OrderBook {
  tokenId: string;
  outcome: string;
  bids: PriceLevel[];
  asks: PriceLevel[];
  bestBid: number;
  bestAsk: number;
}

interface TokenPrice {
  tokenId: string;
  outcome: string;
  bestBid: number;
  bestAsk: number;
  timestamp: number;
}

interface MarketSnapshot {
  upPrice: TokenPrice;
  downPrice: TokenPrice;
  timeRemainingSeconds: number;
  periodStart: number;
}

Strategy Flow

策略流程

1. Discovery  → Gamma API finds current 15m market slug for each asset
2. Monitor    → Poll CLOB orderbooks every CHECK_INTERVAL_MS
3. Watch      → First DUMP_HEDGE_WINDOW_MINUTES: detect if ask drops >= MOVE_THRESHOLD
4. Leg 1      → Buy DUMP_HEDGE_SHARES of dumped side at current ask
5. Wait       → Watch for: leg1_entry + opposite_ask <= DUMP_HEDGE_SUM_TARGET
6. Leg 2      → Buy DUMP_HEDGE_SHARES of opposite outcome (hedge)
7. Stop-loss  → If hedge not triggered within STOP_LOSS_MAX_WAIT_MINUTES, hedge anyway
8. Rollover   → New 15m period → discover new market, reset state
9. Closure    → Redeem winning tokens (production), log P&L
1. 市场发现  → Gamma API查找每个资产当前的15分钟市场标识
2. 监控    → 每CHECK_INTERVAL_MS毫秒轮询CLOB订单簿
3. 观察      → 前DUMP_HEDGE_WINDOW_MINUTES分钟:检测卖价下跌幅度是否>= MOVE_THRESHOLD
4. 第一笔交易      → 以当前卖价买入DUMP_HEDGE_SHARES份下跌侧的份额
5. 等待       → 监听:第一笔交易入场价 + 相反侧卖价 <= DUMP_HEDGE_SUM_TARGET
6. 第二笔对冲交易      → 买入DUMP_HEDGE_SHARES份相反结果的份额(对冲)
7. 止损  → 如果在STOP_LOSS_MAX_WAIT_MINUTES分钟内未触发对冲,则强制进行对冲
8. 周期滚动   → 新的15分钟周期开启 → 发现新市场,重置状态
9. 结算    → 兑换获胜代币(生产模式),记录盈亏

Code Examples

代码示例

Loading and using config (config.ts pattern)

加载和使用配置(config.ts模式)

typescript
import * as dotenv from 'dotenv';
dotenv.config();

interface BotConfig {
  privateKey: string;
  proxyWalletAddress: string | undefined;
  signatureType: number;
  markets: string[];
  production: boolean;
  checkIntervalMs: number;
  dumpHedgeShares: number;
  dumpHedgeSumTarget: number;
  dumpHedgeMoveThreshold: number;
  dumpHedgeWindowMinutes: number;
  stopLossMaxWaitMinutes: number;
  stopLossPercentage: number;
  gammaApiUrl: string;
  clobApiUrl: string;
}

function loadConfig(): BotConfig {
  return {
    privateKey: process.env.PRIVATE_KEY ?? '',
    proxyWalletAddress: process.env.PROXY_WALLET_ADDRESS,
    signatureType: parseInt(process.env.SIGNATURE_TYPE ?? '2'),
    markets: (process.env.MARKETS ?? 'btc').split(',').map(m => m.trim()),
    production: process.env.PRODUCTION === 'true',
    checkIntervalMs: parseInt(process.env.CHECK_INTERVAL_MS ?? '1000'),
    dumpHedgeShares: parseInt(process.env.DUMP_HEDGE_SHARES ?? '10'),
    dumpHedgeSumTarget: parseFloat(process.env.DUMP_HEDGE_SUM_TARGET ?? '0.95'),
    dumpHedgeMoveThreshold: parseFloat(process.env.DUMP_HEDGE_MOVE_THRESHOLD ?? '0.15'),
    dumpHedgeWindowMinutes: parseFloat(process.env.DUMP_HEDGE_WINDOW_MINUTES ?? '2'),
    stopLossMaxWaitMinutes: parseFloat(process.env.DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES ?? '5'),
    stopLossPercentage: parseFloat(process.env.DUMP_HEDGE_STOP_LOSS_PERCENTAGE ?? '0.2'),
    gammaApiUrl: process.env.GAMMA_API_URL ?? 'https://gamma-api.polymarket.com',
    clobApiUrl: process.env.CLOB_API_URL ?? 'https://clob.polymarket.com',
  };
}
typescript
import * as dotenv from 'dotenv';
dotenv.config();

interface BotConfig {
  privateKey: string;
  proxyWalletAddress: string | undefined;
  signatureType: number;
  markets: string[];
  production: boolean;
  checkIntervalMs: number;
  dumpHedgeShares: number;
  dumpHedgeSumTarget: number;
  dumpHedgeMoveThreshold: number;
  dumpHedgeWindowMinutes: number;
  stopLossMaxWaitMinutes: number;
  stopLossPercentage: number;
  gammaApiUrl: string;
  clobApiUrl: string;
}

function loadConfig(): BotConfig {
  return {
    privateKey: process.env.PRIVATE_KEY ?? '',
    proxyWalletAddress: process.env.PROXY_WALLET_ADDRESS,
    signatureType: parseInt(process.env.SIGNATURE_TYPE ?? '2'),
    markets: (process.env.MARKETS ?? 'btc').split(',').map(m => m.trim()),
    production: process.env.PRODUCTION === 'true',
    checkIntervalMs: parseInt(process.env.CHECK_INTERVAL_MS ?? '1000'),
    dumpHedgeShares: parseInt(process.env.DUMP_HEDGE_SHARES ?? '10'),
    dumpHedgeSumTarget: parseFloat(process.env.DUMP_HEDGE_SUM_TARGET ?? '0.95'),
    dumpHedgeMoveThreshold: parseFloat(process.env.DUMP_HEDGE_MOVE_THRESHOLD ?? '0.15'),
    dumpHedgeWindowMinutes: parseFloat(process.env.DUMP_HEDGE_WINDOW_MINUTES ?? '2'),
    stopLossMaxWaitMinutes: parseFloat(process.env.DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES ?? '5'),
    stopLossPercentage: parseFloat(process.env.DUMP_HEDGE_STOP_LOSS_PERCENTAGE ?? '0.2'),
    gammaApiUrl: process.env.GAMMA_API_URL ?? 'https://gamma-api.polymarket.com',
    clobApiUrl: process.env.CLOB_API_URL ?? 'https://clob.polymarket.com',
  };
}

Fetching market via Gamma API (api.ts pattern)

通过Gamma API获取市场(api.ts模式)

typescript
import axios from 'axios';

// Find current 15m market for an asset
async function findCurrentMarket(
  gammaApiUrl: string,
  asset: string  // "btc", "eth", "sol", "xrp"
): Promise<Market | null> {
  // Polymarket 15m slug format: btc-updown-15m-<period_timestamp>
  // Round current time down to nearest 15m period
  const now = Math.floor(Date.now() / 1000);
  const periodStart = now - (now % (15 * 60));
  const slug = `${asset}-updown-15m-${periodStart}`;

  try {
    const response = await axios.get(`${gammaApiUrl}/markets`, {
      params: { slug }
    });
    const markets = response.data;
    if (!markets || markets.length === 0) return null;
    return markets[0] as Market;
  } catch (err) {
    console.error(`[${asset}] Market discovery failed:`, err);
    return null;
  }
}
typescript
import axios from 'axios';

// 查找某个资产当前的15分钟市场
async function findCurrentMarket(
  gammaApiUrl: string,
  asset: string  // "btc", "eth", "sol", "xrp"
): Promise<Market | null> {
  // Polymarket 15分钟市场slug格式:btc-updown-15m-<period_timestamp>
  // 将当前时间向下取整到最近的15分钟周期
  const now = Math.floor(Date.now() / 1000);
  const periodStart = now - (now % (15 * 60));
  const slug = `${asset}-updown-15m-${periodStart}`;

  try {
    const response = await axios.get(`${gammaApiUrl}/markets`, {
      params: { slug }
    });
    const markets = response.data;
    if (!markets || markets.length === 0) return null;
    return markets[0] as Market;
  } catch (err) {
    console.error(`[${asset}] 市场发现失败:`, err);
    return null;
  }
}

Fetching orderbook from CLOB (api.ts pattern)

从CLOB获取订单簿(api.ts模式)

typescript
async function getOrderBook(
  clobApiUrl: string,
  tokenId: string
): Promise<OrderBook | null> {
  try {
    const response = await axios.get(`${clobApiUrl}/book`, {
      params: { token_id: tokenId }
    });
    const data = response.data;
    
    const bestBid = data.bids?.length > 0 
      ? Math.max(...data.bids.map((b: any) => parseFloat(b.price))) 
      : 0;
    const bestAsk = data.asks?.length > 0 
      ? Math.min(...data.asks.map((a: any) => parseFloat(a.price))) 
      : 1;
    
    return {
      tokenId,
      outcome: data.outcome ?? '',
      bids: data.bids ?? [],
      asks: data.asks ?? [],
      bestBid,
      bestAsk,
    };
  } catch (err) {
    console.error(`OrderBook fetch failed for ${tokenId}:`, err);
    return null;
  }
}
typescript
async function getOrderBook(
  clobApiUrl: string,
  tokenId: string
): Promise<OrderBook | null> {
  try {
    const response = await axios.get(`${clobApiUrl}/book`, {
      params: { token_id: tokenId }
    });
    const data = response.data;
    
    const bestBid = data.bids?.length > 0 
      ? Math.max(...data.bids.map((b: any) => parseFloat(b.price))) 
      : 0;
    const bestAsk = data.asks?.length > 0 
      ? Math.min(...data.asks.map((a: any) => parseFloat(a.price))) 
      : 1;
    
    return {
      tokenId,
      outcome: data.outcome ?? '',
      bids: data.bids ?? [],
      asks: data.asks ?? [],
      bestBid,
      bestAsk,
    };
  } catch (err) {
    console.error(`获取${tokenId}的订单簿失败:`, err);
    return null;
  }
}

Dump detection logic (dumpHedgeTrader.ts pattern)

下跌检测逻辑(dumpHedgeTrader.ts模式)

typescript
interface DumpHedgeState {
  phase: 'watching' | 'leg1_placed' | 'hedging' | 'closed';
  leg1Outcome?: 'Up' | 'Down';
  leg1EntryPrice?: number;
  leg1PlacedAt?: number;
  leg1TokenId?: string;
  hedgeTokenId?: string;
  periodStart: number;
}

function detectDump(
  snapshot: MarketSnapshot,
  priceHistory: TokenPrice[],
  config: BotConfig
): 'Up' | 'Down' | null {
  const now = Date.now() / 1000;
  const windowStart = snapshot.periodStart;
  const windowEnd = windowStart + config.dumpHedgeWindowMinutes * 60;

  // Only detect within watch window
  if (now > windowEnd) return null;

  // Get earliest prices in window for comparison
  const windowHistory = priceHistory.filter(p => p.timestamp >= windowStart);
  if (windowHistory.length < 2) return null;

  const earliest = windowHistory[0];
  const current = snapshot;

  // Check Up side dump
  if (earliest.upPrice.bestAsk > 0) {
    const upDrop = (earliest.upPrice.bestAsk - current.upPrice.bestAsk) / earliest.upPrice.bestAsk;
    if (upDrop >= config.dumpHedgeMoveThreshold) {
      console.error(`[DUMP] Up side dropped ${(upDrop * 100).toFixed(1)}%`);
      return 'Up';
    }
  }

  // Check Down side dump
  if (earliest.downPrice.bestAsk > 0) {
    const downDrop = (earliest.downPrice.bestAsk - current.downPrice.bestAsk) / earliest.downPrice.bestAsk;
    if (downDrop >= config.dumpHedgeMoveThreshold) {
      console.error(`[DUMP] Down side dropped ${(downDrop * 100).toFixed(1)}%`);
      return 'Down';
    }
  }

  return null;
}

function shouldHedge(
  state: DumpHedgeState,
  snapshot: MarketSnapshot,
  config: BotConfig
): boolean {
  if (state.phase !== 'leg1_placed' || !state.leg1EntryPrice) return false;

  const oppositeAsk = state.leg1Outcome === 'Up'
    ? snapshot.downPrice.bestAsk
    : snapshot.upPrice.bestAsk;

  const combinedCost = state.leg1EntryPrice + oppositeAsk;
  return combinedCost <= config.dumpHedgeSumTarget;
}

function shouldStopLoss(
  state: DumpHedgeState,
  config: BotConfig
): boolean {
  if (state.phase !== 'leg1_placed' || !state.leg1PlacedAt) return false;
  const waitedMinutes = (Date.now() / 1000 - state.leg1PlacedAt) / 60;
  return waitedMinutes >= config.stopLossMaxWaitMinutes;
}
typescript
interface DumpHedgeState {
  phase: 'watching' | 'leg1_placed' | 'hedging' | 'closed';
  leg1Outcome?: 'Up' | 'Down';
  leg1EntryPrice?: number;
  leg1PlacedAt?: number;
  leg1TokenId?: string;
  hedgeTokenId?: string;
  periodStart: number;
}

function detectDump(
  snapshot: MarketSnapshot,
  priceHistory: TokenPrice[],
  config: BotConfig
): 'Up' | 'Down' | null {
  const now = Date.now() / 1000;
  const windowStart = snapshot.periodStart;
  const windowEnd = windowStart + config.dumpHedgeWindowMinutes * 60;

  // 仅在观察窗口内执行检测
  if (now > windowEnd) return null;

  // 获取窗口内最早的价格用于对比
  const windowHistory = priceHistory.filter(p => p.timestamp >= windowStart);
  if (windowHistory.length < 2) return null;

  const earliest = windowHistory[0];
  const current = snapshot;

  // 检测上涨侧下跌
  if (earliest.upPrice.bestAsk > 0) {
    const upDrop = (earliest.upPrice.bestAsk - current.upPrice.bestAsk) / earliest.upPrice.bestAsk;
    if (upDrop >= config.dumpHedgeMoveThreshold) {
      console.error(`[下跌检测] 上涨侧下跌${(upDrop * 100).toFixed(1)}%`);
      return 'Up';
    }
  }

  // 检测下跌侧下跌
  if (earliest.downPrice.bestAsk > 0) {
    const downDrop = (earliest.downPrice.bestAsk - current.downPrice.bestAsk) / earliest.downPrice.bestAsk;
    if (downDrop >= config.dumpHedgeMoveThreshold) {
      console.error(`[下跌检测] 下跌侧下跌${(downDrop * 100).toFixed(1)}%`);
      return 'Down';
    }
  }

  return null;
}

function shouldHedge(
  state: DumpHedgeState,
  snapshot: MarketSnapshot,
  config: BotConfig
): boolean {
  if (state.phase !== 'leg1_placed' || !state.leg1EntryPrice) return false;

  const oppositeAsk = state.leg1Outcome === 'Up'
    ? snapshot.downPrice.bestAsk
    : snapshot.upPrice.bestAsk;

  const combinedCost = state.leg1EntryPrice + oppositeAsk;
  return combinedCost <= config.dumpHedgeSumTarget;
}

function shouldStopLoss(
  state: DumpHedgeState,
  config: BotConfig
): boolean {
  if (state.phase !== 'leg1_placed' || !state.leg1PlacedAt) return false;
  const waitedMinutes = (Date.now() / 1000 - state.leg1PlacedAt) / 60;
  return waitedMinutes >= config.stopLossMaxWaitMinutes;
}

Placing an order via CLOB (api.ts pattern)

通过CLOB下单(api.ts模式)

typescript
import { ethers } from 'ethers';

interface OrderParams {
  tokenId: string;
  price: number;      // 0.0 to 1.0
  size: number;       // number of shares
  side: 'BUY' | 'SELL';
}

async function placeOrder(
  clobApiUrl: string,
  signer: ethers.Wallet,
  apiKey: string,
  apiSecret: string,
  apiPassphrase: string,
  params: OrderParams,
  production: boolean
): Promise<string | null> {
  if (!production) {
    console.error(`[SIM] Would place ${params.side} ${params.size} shares of ${params.tokenId} @ ${params.price}`);
    return `sim-order-${Date.now()}`;
  }

  // Build and sign order for CLOB
  const order = {
    token_id: params.tokenId,
    price: params.price.toFixed(4),
    size: params.size.toString(),
    side: params.side,
    type: 'GTC',
  };

  // CLOB requires L1/L2 auth headers derived from API credentials
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const signature = await signClobOrder(signer, order, timestamp);

  try {
    const response = await axios.post(`${clobApiUrl}/order`, order, {
      headers: {
        'POLY_ADDRESS': await signer.getAddress(),
        'POLY_SIGNATURE': signature,
        'POLY_TIMESTAMP': timestamp,
        'POLY_API_KEY': apiKey,
      }
    });
    return response.data.orderId ?? null;
  } catch (err) {
    console.error('[ORDER] Placement failed:', err);
    return null;
  }
}
typescript
import { ethers } from 'ethers';

interface OrderParams {
  tokenId: string;
  price: number;      // 0.0 to 1.0
  size: number;       // 份额数量
  side: 'BUY' | 'SELL';
}

async function placeOrder(
  clobApiUrl: string,
  signer: ethers.Wallet,
  apiKey: string,
  apiSecret: string,
  apiPassphrase: string,
  params: OrderParams,
  production: boolean
): Promise<string | null> {
  if (!production) {
    console.error(`[模拟模式] 将执行${params.side}操作,买入${params.size}${params.tokenId},单价${params.price}`);
    return `sim-order-${Date.now()}`;
  }

  // 构建并签名CLOB订单
  const order = {
    token_id: params.tokenId,
    price: params.price.toFixed(4),
    size: params.size.toString(),
    side: params.side,
    type: 'GTC',
  };

  // CLOB要求从API凭证派生L1/L2认证头
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const signature = await signClobOrder(signer, order, timestamp);

  try {
    const response = await axios.post(`${clobApiUrl}/order`, order, {
      headers: {
        'POLY_ADDRESS': await signer.getAddress(),
        'POLY_SIGNATURE': signature,
        'POLY_TIMESTAMP': timestamp,
        'POLY_API_KEY': apiKey,
      }
    });
    return response.data.orderId ?? null;
  } catch (err) {
    console.error('[订单] 下单失败:', err);
    return null;
  }
}

Monitor loop (monitor.ts pattern)

监控循环(monitor.ts模式)

typescript
async function startMonitor(
  market: Market,
  config: BotConfig,
  onSnapshot: (snapshot: MarketSnapshot) => Promise<void>
): Promise<void> {
  const [upToken, downToken] = market.tokens;

  const poll = async () => {
    try {
      const [upBook, downBook] = await Promise.all([
        getOrderBook(config.clobApiUrl, upToken.tokenId),
        getOrderBook(config.clobApiUrl, downToken.tokenId),
      ]);

      if (!upBook || !downBook) return;

      const now = Math.floor(Date.now() / 1000);
      const snapshot: MarketSnapshot = {
        upPrice: {
          tokenId: upToken.tokenId,
          outcome: 'Up',
          bestBid: upBook.bestBid,
          bestAsk: upBook.bestAsk,
          timestamp: now,
        },
        downPrice: {
          tokenId: downToken.tokenId,
          outcome: 'Down',
          bestBid: downBook.bestBid,
          bestAsk: downBook.bestAsk,
          timestamp: now,
        },
        timeRemainingSeconds: market.endTime - now,
        periodStart: market.startTime,
      };

      await onSnapshot(snapshot);
    } catch (err) {
      console.error('[MONITOR] Poll error:', err);
    }
  };

  // Start polling
  const intervalId = setInterval(poll, config.checkIntervalMs);
  await poll(); // immediate first poll

  // Stop when market ends
  const msUntilEnd = (market.endTime * 1000) - Date.now();
  setTimeout(() => clearInterval(intervalId), msUntilEnd + 5000);
}
typescript
async function startMonitor(
  market: Market,
  config: BotConfig,
  onSnapshot: (snapshot: MarketSnapshot) => Promise<void>
): Promise<void> {
  const [upToken, downToken] = market.tokens;

  const poll = async () => {
    try {
      const [upBook, downBook] = await Promise.all([
        getOrderBook(config.clobApiUrl, upToken.tokenId),
        getOrderBook(config.clobApiUrl, downToken.tokenId),
      ]);

      if (!upBook || !downBook) return;

      const now = Math.floor(Date.now() / 1000);
      const snapshot: MarketSnapshot = {
        upPrice: {
          tokenId: upToken.tokenId,
          outcome: 'Up',
          bestBid: upBook.bestBid,
          bestAsk: upBook.bestAsk,
          timestamp: now,
        },
        downPrice: {
          tokenId: downToken.tokenId,
          outcome: 'Down',
          bestBid: downBook.bestBid,
          bestAsk: downBook.bestAsk,
          timestamp: now,
        },
        timeRemainingSeconds: market.endTime - now,
        periodStart: market.startTime,
      };

      await onSnapshot(snapshot);
    } catch (err) {
      console.error('[监控] 轮询错误:', err);
    }
  };

  // 启动轮询
  const intervalId = setInterval(poll, config.checkIntervalMs);
  await poll(); // 立即执行首次轮询

  // 市场结束时停止轮询
  const msUntilEnd = (market.endTime * 1000) - Date.now();
  setTimeout(() => clearInterval(intervalId), msUntilEnd + 5000);
}

History logging (logger.ts pattern)

历史日志(logger.ts模式)

typescript
import * as fs from 'fs';

const HISTORY_FILE = 'history.toml';

interface TradeRecord {
  timestamp: string;
  asset: string;
  action: 'leg1' | 'hedge' | 'stop_loss' | 'redemption';
  outcome: string;
  price: number;
  shares: number;
  simulation: boolean;
  pnl?: number;
}

function logTrade(record: TradeRecord): void {
  const entry = `
[[trade]]
timestamp = "${record.timestamp}"
asset = "${record.asset}"
action = "${record.action}"
outcome = "${record.outcome}"
price = ${record.price}
shares = ${record.shares}
simulation = ${record.simulation}
${record.pnl !== undefined ? `pnl = ${record.pnl}` : ''}
`;
  fs.appendFileSync(HISTORY_FILE, entry, 'utf8');
  console.error(`[LOG] ${record.action} ${record.outcome} @ ${record.price} (sim=${record.simulation})`);
}
typescript
import * as fs from 'fs';

const HISTORY_FILE = 'history.toml';

interface TradeRecord {
  timestamp: string;
  asset: string;
  action: 'leg1' | 'hedge' | 'stop_loss' | 'redemption';
  outcome: string;
  price: number;
  shares: number;
  simulation: boolean;
  pnl?: number;
}

function logTrade(record: TradeRecord): void {
  const entry = `
[[trade]]
timestamp = "${record.timestamp}"
asset = "${record.asset}"
action = "${record.action}"
outcome = "${record.outcome}"
price = ${record.price}
shares = ${record.shares}
simulation = ${record.simulation}
${record.pnl !== undefined ? `pnl = ${record.pnl}` : ''}
`;
  fs.appendFileSync(HISTORY_FILE, entry, 'utf8');
  console.error(`[日志] ${record.action} ${record.outcome} @ ${record.price} (sim=${record.simulation})`);
}

Main entry pattern (main.ts)

主入口模式(main.ts)

typescript
import { loadConfig } from './config';
import { findCurrentMarket } from './api';
import { startMonitor } from './monitor';
import { DumpHedgeTrader } from './dumpHedgeTrader';

async function main() {
  const config = loadConfig();

  console.error(`[BOOT] Mode: ${config.production ? 'PRODUCTION' : 'SIMULATION'}`);
  console.error(`[BOOT] Markets: ${config.markets.join(', ')}`);

  // Start a monitor+trader for each configured asset
  const tasks = config.markets.map(async (asset) => {
    while (true) {
      // Discover current 15m market
      const market = await findCurrentMarket(config.gammaApiUrl, asset);
      if (!market) {
        console.error(`[${asset}] No active market found, retrying in 30s`);
        await sleep(30_000);
        continue;
      }

      console.error(`[${asset}] Found market: ${market.conditionId}, ends ${new Date(market.endTime * 1000).toISOString()}`);

      const trader = new DumpHedgeTrader(asset, market, config);
      await startMonitor(market, config, (snap) => trader.onSnapshot(snap));

      // Market ended — handle closure, then loop to find next period
      await trader.onClose();
      console.error(`[${asset}] Period ended, discovering next market...`);
      await sleep(5_000);
    }
  });

  await Promise.all(tasks);
}

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

main().catch(err => {
  console.error('[FATAL]', err);
  process.exit(1);
});
typescript
import { loadConfig } from './config';
import { findCurrentMarket } from './api';
import { startMonitor } from './monitor';
import { DumpHedgeTrader } from './dumpHedgeTrader';

async function main() {
  const config = loadConfig();

  console.error(`[启动] 模式: ${config.production ? '生产模式' : '模拟模式'}`);
  console.error(`[启动] 交易市场: ${config.markets.join(', ')}`);

  // 为每个配置的资产启动监控+交易器
  const tasks = config.markets.map(async (asset) => {
    while (true) {
      // 发现当前15分钟市场
      const market = await findCurrentMarket(config.gammaApiUrl, asset);
      if (!market) {
        console.error(`[${asset}] 未找到活跃市场,30秒后重试`);
        await sleep(30_000);
        continue;
      }

      console.error(`[${asset}] 找到市场: ${market.conditionId}, 结束时间${new Date(market.endTime * 1000).toISOString()}`);

      const trader = new DumpHedgeTrader(asset, market, config);
      await startMonitor(market, config, (snap) => trader.onSnapshot(snap));

      // 市场结束 — 处理结算,然后循环查找下一个周期
      await trader.onClose();
      console.error(`[${asset}] 周期结束,正在查找下一个市场...`);
      await sleep(5_000);
    }
  });

  await Promise.all(tasks);
}

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

main().catch(err => {
  console.error('[致命错误]', err);
  process.exit(1);
});

Common Patterns

常见配置模式

Multi-asset configuration

多资产配置

env
undefined
env
undefined

Monitor BTC and ETH simultaneously

同时监控BTC和ETH

MARKETS=btc,eth
MARKETS=btc,eth

More aggressive dump detection

更激进的下跌检测

DUMP_HEDGE_MOVE_THRESHOLD=0.10 DUMP_HEDGE_WINDOW_MINUTES=3
DUMP_HEDGE_MOVE_THRESHOLD=0.10 DUMP_HEDGE_WINDOW_MINUTES=3

Tighter profit target

更严格的盈利目标

DUMP_HEDGE_SUM_TARGET=0.93
undefined
DUMP_HEDGE_SUM_TARGET=0.93
undefined

Tuning for volatile markets

高波动市场调优

env
undefined
env
undefined

Larger position per leg

每笔交易更大的仓位

DUMP_HEDGE_SHARES=25
DUMP_HEDGE_SHARES=25

Wider dump threshold catches more opportunities

更宽的下跌阈值捕捉更多机会

DUMP_HEDGE_MOVE_THRESHOLD=0.10
DUMP_HEDGE_MOVE_THRESHOLD=0.10

Longer window to detect slower dumps

更长的检测窗口适配更缓慢的下跌

DUMP_HEDGE_WINDOW_MINUTES=4
DUMP_HEDGE_WINDOW_MINUTES=4

More time before stop-loss kicks in

止损触发前等待更长时间

DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES=8
undefined
DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES=8
undefined

Switching simulation → production

切换模拟模式 → 生产模式

bash
undefined
bash
undefined

1. Verify strategy looks correct in simulation

1. 在模拟模式下验证策略逻辑正确

npm run sim
npm run sim

2. Check history.toml for expected trade pattern

2. 查看history.toml确认交易模式符合预期

cat history.toml
cat history.toml

3. Enable production (ensure wallet funded with USDC + POL for gas)

3. 开启生产模式(确保钱包有足够的USDC + POL支付gas)

PRODUCTION=true npm start
PRODUCTION=true npm start

or

或者

npm run prod
undefined
npm run prod
undefined

Using EOA wallet (no proxy)

使用EOA钱包(无代理)

env
PRIVATE_KEY=0x_your_eoa_private_key
SIGNATURE_TYPE=0
env
PRIVATE_KEY=0x_your_eoa_private_key
SIGNATURE_TYPE=0

Leave PROXY_WALLET_ADDRESS unset

不设置PROXY_WALLET_ADDRESS

undefined
undefined

Using GnosisSafe proxy (default Polymarket setup)

使用GnosisSafe代理(Polymarket默认设置)

env
PRIVATE_KEY=0x_your_signer_private_key
PROXY_WALLET_ADDRESS=0x_your_polymarket_profile_address
SIGNATURE_TYPE=2
env
PRIVATE_KEY=0x_your_signer_private_key
PROXY_WALLET_ADDRESS=0x_your_polymarket_profile_address
SIGNATURE_TYPE=2

Profit Mechanics

盈利机制

Per resolved pair:
  Revenue:          1.00  (winning outcome pays $1/share)
  Cost (leg1):      e.g. 0.45  (bought dumped side)
  Cost (leg2):      e.g. 0.49  (hedge at ask)
  Combined cost:    0.94  (<= SUM_TARGET of 0.95)
  Profit/share:     0.06  (6% per share pair, before fees)

Worst case (stop-loss hedge):
  If hedge triggers at stop-loss, combined cost may exceed 0.95
  Loss is bounded by STOP_LOSS_PERCENTAGE (e.g. 0.2 = 20% of leg1 size)
每对已结算交易:
  收入:          1.00  (获胜结果每股支付1美元)
  成本(第一笔交易):      例如 0.45  (买入下跌侧)
  成本(第二笔对冲交易):      例如 0.49  (以卖价对冲)
  总成本:    0.94  (<= SUM_TARGET 0.95)
  每股利润:     0.06  (每对份额6%利润,未扣除手续费)

最坏情况(止损对冲):
  如果在止损点触发对冲,总成本可能超过0.95
  损失由STOP_LOSS_PERCENTAGE限制(例如0.2 = 第一笔交易仓位的20%)

Troubleshooting

故障排除

ProblemCauseFix
No markets foundWrong slug/timingCheck
GAMMA_API_URL
connectivity; confirm asset name is lowercase
Orders fail in productionBad credentialsVerify
PRIVATE_KEY
,
PROXY_WALLET_ADDRESS
,
SIGNATURE_TYPE
Redemption failsInsufficient POL gasFund wallet with POL/MATIC on Polygon mainnet
No dumps detectedThreshold too highLower
DUMP_HEDGE_MOVE_THRESHOLD
(e.g. 0.10) or extend window
Strategy never hedgesSum target too tightRaise
DUMP_HEDGE_SUM_TARGET
(e.g. 0.97)
Frequent stop-loss triggersMarket low volatilityIncrease
STOP_LOSS_MAX_WAIT_MINUTES
问题原因解决方案
未找到市场slug/时间错误检查
GAMMA_API_URL
连通性;确认资产名称为小写
生产模式下订单失败凭证错误验证
PRIVATE_KEY
PROXY_WALLET_ADDRESS
SIGNATURE_TYPE
兑换失败POL gas不足为Polygon主网的钱包充值POL/MATIC
未检测到下跌阈值过高降低
DUMP_HEDGE_MOVE_THRESHOLD
(例如0.10)或延长检测窗口
策略从未执行对冲盈利目标过严提高
DUMP_HEDGE_SUM_TARGET
(例如0.97)
频繁触发止损市场波动率低提高
STOP_LOSS_MAX_WAIT_MINUTES

Debugging with simulation logs

使用模拟日志调试

bash
undefined
bash
undefined

Run simulation and watch logs in real time

运行模拟并实时查看日志

npm run sim 2>&1 | tee debug.log
npm run sim 2>&1 | tee debug.log

Review all trades

查看所有交易

grep "action" history.toml
grep "action" history.toml

Check P&L entries

查看盈亏记录

grep "pnl" history.toml
undefined
grep "pnl" history.toml
undefined

Security Checklist

安全检查清单

  • .env
    is in
    .gitignore
    — never commit it
  • Use a dedicated wallet with limited USDC (not your main wallet)
  • Always run
    npm run sim
    first and review
    history.toml
    before going live
  • Rotate
    PRIVATE_KEY
    immediately if it may have been exposed
  • API keys derived from signer are preferred over explicit
    API_KEY
    /
    API_SECRET
  • .env
    已加入
    .gitignore
    — 永远不要提交该文件
  • 使用资金有限的专用钱包(不要使用主钱包)
  • 上线前务必先运行
    npm run sim
    并查看
    history.toml
    确认逻辑正确
  • 如果
    PRIVATE_KEY
    可能泄露请立即更换
  • 优先使用从签名方派生的API密钥,而非显式配置
    API_KEY
    /
    API_SECRET