最近我在搞一个好玩的项目,用 Go 写了个程序来监控 Polymarket 上的套利机会。Polymarket 是个基于区块链的预测市场,里面有各种事件的结果可以交易,比如“某件事会不会发生”之类的。这篇文章我就来聊聊这个程序(polymarket_arbitrage.go)的代码和设计,带你看看它是怎么帮我发现潜在赚钱机会的!

核心代码片段

以下是程序的核心功能代码,主要负责抓取 Polymarket 的市场数据,找出总概率成本低于 100% 的市场(也就是有套利空间的),然后给出投资建议。

  1. 定义数据结构
 1type Market struct {
 2    ID string `json:"id"`
 3    Question string `json:"question"`
 4    Type string `json:"type"`
 5    Volume string `json:"volume"` // 字符串形式
 6    Outcomes string `json:"outcomes"` // JSON 字符串,如 "[\"Yes\", \"No\"]"
 7    OutcomePrices string `json:"outcomePrices"` // JSON 字符串,如 "[\"0.0345\", \"0.9655\"]"
 8}
 9
10type ArbitrageOpportunity struct {
11    MarketID string
12    Question string
13    TotalCost float64
14    ArbitragePercent float64
15    SuggestedInvestment float64
16    SharesToBuy map[string]float64
17}
  1. 计算套利机会
 1func calculateArbitrage(market Market) *ArbitrageOpportunity {
 2    var outcomes []string
 3    if err := json.Unmarshal([]byte(market.Outcomes), &outcomes); err != nil {
 4        fmt.Printf("解析 outcomes 失败(市场: %s): %v\n", market.Question, err)
 5        return nil
 6    }
 7
 8    var priceStrs []string
 9    if err := json.Unmarshal([]byte(market.OutcomePrices), &priceStrs); err != nil {
10        fmt.Printf("解析 outcomePrices 失败(市场: %s): %v\n", market.Question, err)
11        return nil
12    }
13
14    prices := make([]float64, len(priceStrs))
15    for i, priceStr := range priceStrs {
16        price, err := strconv.ParseFloat(priceStr, 64)
17        if err != nil {
18            fmt.Printf("转换价格失败(市场: %s, 结果: %s): %v\n", market.Question, outcomes[i], err)
19            return nil
20        }
21        prices[i] = price
22    }
23
24    volume, err := strconv.ParseFloat(market.Volume, 64)
25    if err != nil || len(outcomes) < 2 || market.Type == "BINARY" || volume < 10000 {
26        return nil // 过滤二元市场、低量市场或解析失败
27    }
28
29    totalCost := 0.0
30    for _, price := range prices {
31        totalCost += price
32    }
33    if totalCost >= 1.0 {
34        return nil // 无套利机会
35    }
36
37    arbitragePct := (1.0 - totalCost) * 100
38    if arbitragePct < 2.0 { // 至少 2% 利润
39        return nil
40    }
41
42    investment := 1000.0
43    sharesToBuy := make(map[string]float64)
44    for i, outcome := range outcomes {
45        if prices[i] > 0 {
46            sharesToBuy[outcome] = investment / float64(len(outcomes)) / prices[i]
47        }
48    }
49
50    return &ArbitrageOpportunity{
51        MarketID: market.ID,
52        Question: market.Question,
53        TotalCost: totalCost,
54        ArbitragePercent: arbitragePct,
55        SuggestedInvestment: investment,
56        SharesToBuy: sharesToBuy,
57    }
58}
  1. 抓取市场数据
 1func fetchMarkets() ([]Market, error) {
 2    var allMarkets []Market
 3    offset := 0
 4    limit := 100
 5
 6    client, err := proxyClient()
 7    if err != nil {
 8        return nil, fmt.Errorf("创建代理失败: %v", err)
 9    }
10
11    for {
12        endDateMin := time.Now().AddDate(0, 0, 7).Format("2006-01-02")
13        endDateMax := time.Now().AddDate(0, 0, 49).Format("2006-01-02")
14        fetchUrl := fmt.Sprintf("%s/markets?active=true&order=volume&ascending=false&end_date_min=%s&end_date_max=%s&limit=%d&offset=%d",
15            gammaAPI, endDateMin, endDateMax, limit, offset)
16        resp, err := httpProxy(client, fetchUrl)
17        if err != nil {
18            return nil, fmt.Errorf("API 请求失败 (offset=%d): %v", offset, err)
19        }
20        defer resp.Body.Close()
21
22        var markets []Market
23        if err := json.NewDecoder(resp.Body).Decode(&markets); err != nil {
24            return nil, fmt.Errorf("JSON 解析失败 (offset=%d): %v", offset, err)
25        }
26
27        allMarkets = append(allMarkets, markets...)
28        fmt.Printf("发现 %d 个活跃市场\n", len(markets))
29
30        offset += limit
31        if len(markets) < limit {
32            break
33        }
34    }
35
36    return allMarkets, nil
37}

这个程序干啥的?

简单来说,这程序是用来从 Polymarket 的 API(https://gamma-api.polymarket.com/markets)抓取市场数据,找出有套利机会的市场。啥叫套利?就是在预测市场里,如果某个事件的所有结果概率加起来不到 100%,你就可以“低买高卖”,稳赚差价!比如,一个市场有三个结果,价格分别是 $0.30、$0.40、$0.25,加起来才 $0.95,投 1000 块就能赚 5% 的利润。 程序会:

  1. 抓取活跃市场(7-49 天内结束,按交易量排序)。
  2. 分析每个市场,找出总成本 < 0.98(利润 ≥ 2%)的多结果市场(排除 Yes/No 的二元市场)。
  3. 给出投资建议,比如投 1000 USDC,买多少股每种结果。

一些注意的点

1. 分页抓取,啥都不漏

Polymarket 的 API 一次最多返回 100 个市场(limit=100),但实际市场可能有好几百。程序用了个循环,通过 offset(0、100、200…)把所有市场都抓下来,直到返回的数据少于 100 条为止。代码里这个逻辑在 fetchMarkets 函数:

1for {
2    fetchUrl := fmt.Sprintf("%s/markets?...&limit=%d&offset=%d", gammaAPI, limit, offset)
3    // 抓数据,存到 allMarkets
4    allMarkets = append(allMarkets, markets...)
5    if len(markets) < limit {
6        break
7    }
8    offset += limit
9}

每次抓完一页,还会打印“已分析 X 个市场,继续获取 offset=Y…”,让我知道进度,挺贴心的。

2. 代理支持,网络无忧

我用的是 ClashX 代理(127.0.0.1:7890),因为国内网络访问 Polymarket API 可能会被墙。程序把代理配置单独抽到 proxyClienthttpProxy 函数里,清晰又好改:

 1func proxyClient() (*http.Client, error) {
 2    proxyURL, err := url.Parse("http://127.0.0.1:7890")
 3    client := &http.Client{
 4        Transport: &http.Transport{
 5            Proxy: http.ProxyURL(proxyURL),
 6        },
 7        Timeout: 10 * time.Second,
 8    }
 9    return client, nil
10}

想换别的代理?改个 proxyURL 就行,方便得很!

3. JSON 解析,适配 API 怪癖

Polymarket API 的 outcomesoutcomePrices 字段是 JSON 字符串(像 "[\"Yes\", \"No\"]"),而不是直接的数组。这让我一开始解析的时候踩了个坑。程序用 json.Unmarshal 专门处理这个怪癖,稳稳地把字符串转成数组:

1var outcomes []string
2if err := json.Unmarshal([]byte(market.Outcomes), &outcomes); err != nil {
3    fmt.Printf("解析 outcomes 失败(市场: %s): %v\n", market.Question, err)
4    return nil
5}

价格也是先转成 []string,再用 strconv.ParseFloatfloat64,保证计算精准。

4. 错误处理,稳如老狗

程序在每个关键步骤都加了错误检查,比如代理配置、API 请求、JSON 解析。如果出问题,会打印详细错误,比如“API 请求失败 (offset=100): …”或“解析 outcomes 失败(市场: XXX)”。这让我调试的时候特别省心:

1if err := json.NewDecoder(resp.Body).Decode(&markets); err != nil {
2    return nil, fmt.Errorf("JSON 解析失败 (offset=%d): %v", offset, err)
3}

5. 输出清晰,交易直接抄

找到套利机会后,程序会打印超级清楚的建议,比如:

发现 1 个套利机会!
机会 1:
  市场: Who will win the 2025 Oscar for Best Picture?
  市场 ID: 123456
  总成本: $0.9500 USDC (预期利润: 5.00%)
  建议投资: $1000 USDC
  买入建议:
    - Film A: 1111.11 股 (价格: $0.3000)
    - Film B: 833.33 股 (价格: $0.4000)
    - Film C: 1333.33 股 (价格: $0.2500)

直接照着这个去 Polymarket 下单就行,省得我自己算!

不足和改进点

当然,这程序也不是完美无缺:

  • 速度慢:分页是串行请求,如果市场有 1000 个,得抓 10 次,挺慢的。可以加 goroutines 并行抓,但得小心 API 限速。
  • 没通知:找到套利机会只能打印到终端,我想加个 Telegram 推送,实时通知我。
  • 过滤太严:只看多结果市场(type != "BINARY")和利润 ≥ 2%,可能错过一些小机会。可以让用户自定义过滤条件。

咋跑这个程序?

  1. 装 Go:确保 Go 1.16+ 装好了(go version)。
  2. 配代理:ClashX 跑在 127.0.0.1:7890(或改成你自己的代理)。
  3. 跑代码
    1go run polymarket_arbitrage.go
    
  4. 看结果:程序会抓数据、分析套利,打印机会。如果没机会,就说“暂无套利机会”。

完整代码

以下是完整的 Go 代码,核心功能是抓取 Polymarket 的市场数据,找出总概率成本低于 100% 的市场(也就是有套利空间的),然后给出投资建议。

  1package main
  2import (
  3    "encoding/json"
  4    "fmt"
  5    "net/http"
  6    "net/url"
  7    "strconv"
  8    "time"
  9)
 10// Gamma API 基础 URL
 11const gammaAPI = "https://gamma-api.polymarket.com"
 12// Market 结构体:解析 API 返回的市场数据
 13type Market struct {
 14    ID string `json:"id"`
 15    Question string `json:"question"`
 16    Type string `json:"type"`
 17    Volume string `json:"volume"` // 字符串形式
 18    Outcomes string `json:"outcomes"` // JSON 字符串,如 "[\"Yes\", \"No\"]"
 19    OutcomePrices string `json:"outcomePrices"` // JSON 字符串,如 "[\"0.0345\", \"0.9655\"]"
 20}
 21// ArbitrageOpportunity 结构体:套利机会
 22type ArbitrageOpportunity struct {
 23    MarketID string
 24    Question string
 25    TotalCost float64
 26    ArbitragePercent float64
 27    SuggestedInvestment float64
 28    SharesToBuy map[string]float64
 29}
 30func proxyClient() (*http.Client, error) {
 31    // 配置 ClashX HTTP 代理
 32    proxyURL, err := url.Parse("http://127.0.0.1:7890")
 33    if err != nil {
 34       return nil, fmt.Errorf("解析代理 URL 失败: %v", err)
 35    }
 36    client := &http.Client{
 37       Transport: &http.Transport{
 38          Proxy: http.ProxyURL(proxyURL),
 39       },
 40       Timeout: 10 * time.Second,
 41    }
 42    return client, nil
 43}
 44func httpProxy(client *http.Client, fetchUrl string) (*http.Response, error) {
 45    req, err := http.NewRequest("GET", fetchUrl, nil)
 46    if err != nil {
 47       return nil, fmt.Errorf("创建请求失败: %v", err)
 48    }
 49    resp, err := client.Do(req)
 50    if err != nil {
 51       return nil, fmt.Errorf("API 请求失败: %v", err)
 52    }
 53    if resp.StatusCode != http.StatusOK {
 54       return nil, fmt.Errorf("API 返回错误: %d", resp.StatusCode)
 55    }
 56    return resp, nil
 57}
 58// fetchMarkets 获取所有活跃市场数据
 59func fetchMarkets() ([]Market, error) {
 60    var allMarkets []Market
 61    offset := 0
 62    limit := 100
 63    // 配置 ClashX HTTP 代理
 64    client, err := proxyClient()
 65    if err != nil {
 66       return nil, fmt.Errorf("创建代理失败: %v", err)
 67    }
 68    for {
 69       endDateMin := time.Now().AddDate(0, 0, 7).Format("2006-01-02")
 70       endDateMax := time.Now().AddDate(0, 0, 49).Format("2006-01-02")
 71       fetchUrl := fmt.Sprintf("%s/markets?active=true&order=volume&ascending=false&end_date_min=%s&end_date_max=%s&limit=%d&offset=%d",
 72          gammaAPI, endDateMin, endDateMax, limit, offset)
 73       resp, err := httpProxy(client, fetchUrl)
 74       if err != nil {
 75          return nil, fmt.Errorf("API 请求失败 (offset=%d): %v", offset, err)
 76       }
 77       defer resp.Body.Close()
 78       var markets []Market
 79       if err := json.NewDecoder(resp.Body).Decode(&markets); err != nil {
 80          return nil, fmt.Errorf("JSON 解析失败 (offset=%d): %v", offset, err)
 81       }
 82       // 累积市场数据
 83       allMarkets = append(allMarkets, markets...)
 84       fmt.Printf("发现 %d 个活跃市场\n", len(markets))
 85       var opportunities []ArbitrageOpportunity
 86       for _, market := range markets {
 87          if opp := calculateArbitrage(market); opp != nil {
 88             opportunities = append(opportunities, *opp)
 89          }
 90       }
 91       if len(opportunities) == 0 {
 92          fmt.Println("暂无套利机会(总概率成本 >= 98%)。")
 93       } else {
 94          fmt.Printf("发现 %d 个套利机会!\n\n", len(opportunities))
 95          for i, opp := range opportunities {
 96             fmt.Printf("机会 %d:\n", i+1)
 97             fmt.Printf(" 市场: %s\n", opp.Question)
 98             fmt.Printf(" 市场 ID: %s\n", opp.MarketID)
 99             fmt.Printf(" 总成本: $%.4f USDC (预期利润: %.2f%%)\n", opp.TotalCost, opp.ArbitragePercent)
100             fmt.Printf(" 建议投资: $%.0f USDC\n", opp.SuggestedInvestment)
101             fmt.Println(" 买入建议:")
102             for outcome, shares := range opp.SharesToBuy {
103                fmt.Printf(" - %s: %.2f 股 (价格: $%.4f)\n", outcome, shares, opp.SuggestedInvestment/float64(len(opp.SharesToBuy))/shares)
104             }
105             fmt.Print("\n" + "==================================================" + "\n")
106          }
107       }
108       // 递增 offset,获取下一页
109       offset += limit
110       fmt.Printf("已分析 %d 个市场,继续获取 offset=%d...\n\n", len(allMarkets), offset)
111       // 如果返回的市场数量 < limit,说明已到最后一页
112       if len(markets) < limit {
113          break
114       }
115    }
116    return allMarkets, nil
117}
118// calculateArbitrage 计算单个市场的套利机会
119func calculateArbitrage(market Market) *ArbitrageOpportunity {
120    // 解析 outcomes JSON 字符串
121    var outcomes []string
122    if err := json.Unmarshal([]byte(market.Outcomes), &outcomes); err != nil {
123       fmt.Printf("解析 outcomes 失败(市场: %s): %v\n", market.Question, err)
124       return nil
125    }
126    // 解析 outcomePrices JSON 字符串
127    var priceStrs []string
128    if err := json.Unmarshal([]byte(market.OutcomePrices), &priceStrs); err != nil {
129       fmt.Printf("解析 outcomePrices 失败(市场: %s): %v\n", market.Question, err)
130       return nil
131    }
132    // 转换价格为 float64
133    prices := make([]float64, len(priceStrs))
134    for i, priceStr := range priceStrs {
135       price, err := strconv.ParseFloat(priceStr, 64)
136       if err != nil {
137          fmt.Printf("转换价格失败(市场: %s, 结果: %s): %v\n", market.Question, outcomes[i], err)
138          return nil
139       }
140       prices[i] = price
141    }
142    // 过滤无效市场
143    volume, err := strconv.ParseFloat(market.Volume, 64)
144    if err != nil || len(outcomes) < 2 || market.Type == "BINARY" || volume < 10000 {
145       return nil // 过滤二元市场、低量市场或解析失败
146    }
147    // 计算总成本
148    totalCost := 0.0
149    for _, price := range prices {
150       totalCost += price
151    }
152    if totalCost >= 1.0 {
153       return nil // 无套利机会
154    }
155    // 计算套利百分比
156    arbitragePct := (1.0 - totalCost) * 100
157    if arbitragePct < 2.0 { // 至少 2% 利润
158       return nil
159    }
160    // 计算建议买入股份(投资 1000 USDC)
161    investment := 1000.0
162    sharesToBuy := make(map[string]float64)
163    for i, outcome := range outcomes {
164       if prices[i] > 0 {
165          sharesToBuy[outcome] = investment / float64(len(outcomes)) / prices[i]
166       }
167    }
168    return &ArbitrageOpportunity{
169       MarketID: market.ID,
170       Question: market.Question,
171       TotalCost: totalCost,
172       ArbitragePercent: arbitragePct,
173       SuggestedInvestment: investment,
174       SharesToBuy: sharesToBuy,
175    }
176}
177func main() {
178    fmt.Println("=== Polymarket 套利监控程序 ===")
179    fmt.Printf("当前时间: %s\n", time.Now().Format("2006-01-02 15:04:05"))
180    fmt.Print("监控活跃市场(结束日期: 7-49 天内,volume 高优先)..." + "\n")
181    fmt.Println()
182    markets, err := fetchMarkets()
183    if err != nil {
184       fmt.Printf("错误: %v\n", err)
185       fmt.Println("检查网络或 API 状态。")
186       return
187    }
188    fmt.Printf("共分析 %d 个活跃市场\n", len(markets))
189}

总结

这程序是我用 Go 写的一个小玩具,专门挖 Polymarket 的套利机会。分页抓数据、代理支持、精准解析、错误处理、清晰输出,功能齐全又好用。接下来我想加点并行请求和 Telegram 通知,让它更牛!如果你也对预测市场感兴趣,试试跑跑看,说不定能发现个赚钱的机会呢!