聊聊我的 Polymarket 套利机器人:用 Go 找预测市场的赚钱机会!
最近我在搞一个好玩的项目,用 Go 写了个程序来监控 Polymarket 上的套利机会。Polymarket 是个基于区块链的预测市场,里面有各种事件的结果可以交易,比如“某件事会不会发生”之类的。这篇文章我就来聊聊这个程序(polymarket_arbitrage.go)的代码和设计,带你看看它是怎么帮我发现潜在赚钱机会的!
核心代码片段
以下是程序的核心功能代码,主要负责抓取 Polymarket 的市场数据,找出总概率成本低于 100% 的市场(也就是有套利空间的),然后给出投资建议。
- 定义数据结构
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}
- 计算套利机会
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}
- 抓取市场数据
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% 的利润。
程序会:
- 抓取活跃市场(7-49 天内结束,按交易量排序)。
- 分析每个市场,找出总成本 < 0.98(利润 ≥ 2%)的多结果市场(排除 Yes/No 的二元市场)。
- 给出投资建议,比如投 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 可能会被墙。程序把代理配置单独抽到 proxyClient 和 httpProxy 函数里,清晰又好改:
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 的 outcomes 和 outcomePrices 字段是 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.ParseFloat 变 float64,保证计算精准。
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%,可能错过一些小机会。可以让用户自定义过滤条件。
咋跑这个程序?
- 装 Go:确保 Go 1.16+ 装好了(
go version)。 - 配代理:ClashX 跑在
127.0.0.1:7890(或改成你自己的代理)。 - 跑代码:
1go run polymarket_arbitrage.go - 看结果:程序会抓数据、分析套利,打印机会。如果没机会,就说“暂无套利机会”。
完整代码
以下是完整的 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 通知,让它更牛!如果你也对预测市场感兴趣,试试跑跑看,说不定能发现个赚钱的机会呢!
