feat: add Polymarket and Kalshi prediction market extractors
All checks were successful
CI / vet (pull_request) Successful in 2m15s
CI / build (pull_request) Successful in 2m17s
CI / test (pull_request) Successful in 2m27s

Add site extractors for two prediction market platforms using their
public REST APIs (no browser/Playwright dependency needed).

Polymarket: search via Gamma API, get events/markets by slug/ID/URL.
Handles inconsistent JSON encoding (stringified arrays).

Kalshi: get markets/events by ticker/URL, search via client-side
filtering of open events (no text search API available).

Both include CLI tools and comprehensive test suites using httptest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 00:52:40 +00:00
parent 8c2848246b
commit 29e53bb7c2
8 changed files with 1910 additions and 0 deletions

326
sites/polymarket/api.go Normal file
View File

@@ -0,0 +1,326 @@
package polymarket
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
const (
defaultGammaBaseURL = "https://gamma-api.polymarket.com"
)
// stringOrArray handles Polymarket's inconsistent JSON encoding where fields
// can be either a JSON array or a stringified JSON array (e.g. "[\"Yes\",\"No\"]").
type stringOrArray []string
func (s *stringOrArray) UnmarshalJSON(data []byte) error {
// Try as regular array first
var arr []string
if err := json.Unmarshal(data, &arr); err == nil {
*s = arr
return nil
}
// Try as stringified array
var str string
if err := json.Unmarshal(data, &str); err == nil {
if err := json.Unmarshal([]byte(str), &arr); err == nil {
*s = arr
return nil
}
}
*s = nil
return nil
}
// floatOrArray handles the same pattern for numeric arrays.
type floatOrArray []float64
func (f *floatOrArray) UnmarshalJSON(data []byte) error {
// Try as regular array first
var arr []float64
if err := json.Unmarshal(data, &arr); err == nil {
*f = arr
return nil
}
// Try as array of strings
var strArr []string
if err := json.Unmarshal(data, &strArr); err == nil {
result := make([]float64, 0, len(strArr))
for _, s := range strArr {
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return fmt.Errorf("parsing price %q: %w", s, err)
}
result = append(result, v)
}
*f = result
return nil
}
// Try as stringified array of strings
var str string
if err := json.Unmarshal(data, &str); err == nil {
if err := json.Unmarshal([]byte(str), &strArr); err == nil {
result := make([]float64, 0, len(strArr))
for _, s := range strArr {
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return fmt.Errorf("parsing price %q: %w", s, err)
}
result = append(result, v)
}
*f = result
return nil
}
}
*f = nil
return nil
}
// apiMarket is the JSON shape returned by the Gamma API for a market.
type apiMarket struct {
ID string `json:"id"`
Question string `json:"question"`
Slug string `json:"slug"`
Outcomes stringOrArray `json:"outcomes"`
OutcomePrices floatOrArray `json:"outcomePrices"`
Volume flexFloat `json:"volume"`
Volume24hr float64 `json:"volume24hr"`
LiquidityClob float64 `json:"liquidityClob"`
EndDate string `json:"endDate"`
Active bool `json:"active"`
Closed bool `json:"closed"`
BestBid float64 `json:"bestBid"`
BestAsk float64 `json:"bestAsk"`
LastTradePrice float64 `json:"lastTradePrice"`
Description string `json:"description"`
GroupItemTitle string `json:"groupItemTitle"`
}
// flexFloat handles volume fields that are sometimes strings, sometimes numbers.
type flexFloat float64
func (f *flexFloat) UnmarshalJSON(data []byte) error {
var num float64
if err := json.Unmarshal(data, &num); err == nil {
*f = flexFloat(num)
return nil
}
var str string
if err := json.Unmarshal(data, &str); err == nil {
v, err := strconv.ParseFloat(str, 64)
if err != nil {
*f = 0
return nil
}
*f = flexFloat(v)
return nil
}
*f = 0
return nil
}
// apiEvent is the JSON shape returned by the Gamma API for an event.
type apiEvent struct {
ID string `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Description string `json:"description"`
Markets []apiMarket `json:"markets"`
Volume float64 `json:"volume"`
Volume24hr float64 `json:"volume24hr"`
Liquidity float64 `json:"liquidity"`
Active bool `json:"active"`
Closed bool `json:"closed"`
}
// apiSearchResponse is the JSON shape returned by the public-search endpoint.
type apiSearchResponse struct {
Events []apiEvent `json:"events"`
}
func apiMarketToMarket(m apiMarket) Market {
return Market{
ID: m.ID,
Question: m.Question,
Slug: m.Slug,
Outcomes: []string(m.Outcomes),
OutcomePrices: []float64(m.OutcomePrices),
Volume: float64(m.Volume),
Volume24h: m.Volume24hr,
Liquidity: m.LiquidityClob,
EndDate: m.EndDate,
Active: m.Active,
Closed: m.Closed,
BestBid: m.BestBid,
BestAsk: m.BestAsk,
LastPrice: m.LastTradePrice,
Description: m.Description,
}
}
func apiEventToEvent(e apiEvent) Event {
markets := make([]Market, 0, len(e.Markets))
for _, m := range e.Markets {
markets = append(markets, apiMarketToMarket(m))
}
return Event{
ID: e.ID,
Title: e.Title,
Slug: e.Slug,
Description: e.Description,
Markets: markets,
Volume: e.Volume,
Volume24h: e.Volume24hr,
Liquidity: e.Liquidity,
Active: e.Active,
Closed: e.Closed,
}
}
func (c Config) doGet(ctx context.Context, endpoint string) ([]byte, error) {
base := c.BaseURL
if base == "" {
base = defaultGammaBaseURL
}
reqURL := base + endpoint
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Accept", "application/json")
client := c.HTTPClient
if client == nil {
client = http.DefaultClient
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
return body, nil
}
func (c Config) searchAPI(ctx context.Context, query string) ([]Event, error) {
endpoint := "/public-search?q=" + url.QueryEscape(query)
body, err := c.doGet(ctx, endpoint)
if err != nil {
return nil, fmt.Errorf("searching: %w", err)
}
var resp apiSearchResponse
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("parsing search response: %w", err)
}
events := make([]Event, 0, len(resp.Events))
for _, e := range resp.Events {
events = append(events, apiEventToEvent(e))
}
return events, nil
}
func (c Config) getEventBySlug(ctx context.Context, slug string) (*Event, error) {
body, err := c.doGet(ctx, "/events/slug/"+url.PathEscape(slug))
if err != nil {
return nil, fmt.Errorf("getting event: %w", err)
}
var raw apiEvent
if err := json.Unmarshal(body, &raw); err != nil {
return nil, fmt.Errorf("parsing event: %w", err)
}
ev := apiEventToEvent(raw)
return &ev, nil
}
func (c Config) getEventByID(ctx context.Context, id string) (*Event, error) {
body, err := c.doGet(ctx, "/events/"+url.PathEscape(id))
if err != nil {
return nil, fmt.Errorf("getting event: %w", err)
}
var raw apiEvent
if err := json.Unmarshal(body, &raw); err != nil {
return nil, fmt.Errorf("parsing event: %w", err)
}
ev := apiEventToEvent(raw)
return &ev, nil
}
func (c Config) getMarketBySlug(ctx context.Context, slug string) (*Market, error) {
body, err := c.doGet(ctx, "/markets/slug/"+url.PathEscape(slug))
if err != nil {
return nil, fmt.Errorf("getting market: %w", err)
}
var raw apiMarket
if err := json.Unmarshal(body, &raw); err != nil {
return nil, fmt.Errorf("parsing market: %w", err)
}
m := apiMarketToMarket(raw)
return &m, nil
}
// parsePolymarketURL extracts the type ("event" or "market") and slug from a Polymarket URL.
// Supported formats:
// - https://polymarket.com/event/<slug>
// - https://polymarket.com/event/<event-slug>/<market-slug>
func parsePolymarketURL(rawURL string) (urlType, slug, marketSlug string, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return "", "", "", fmt.Errorf("parsing URL: %w", err)
}
host := strings.TrimPrefix(u.Host, "www.")
if host != "polymarket.com" {
return "", "", "", fmt.Errorf("not a Polymarket URL: %s", rawURL)
}
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(parts) < 2 {
return "", "", "", fmt.Errorf("invalid Polymarket URL path: %s", u.Path)
}
switch parts[0] {
case "event":
if len(parts) >= 3 {
return "event", parts[1], parts[2], nil
}
return "event", parts[1], "", nil
default:
return "", "", "", fmt.Errorf("unsupported Polymarket URL type: %s", parts[0])
}
}

View File

@@ -0,0 +1,131 @@
package main
import (
"context"
"fmt"
"os"
"strings"
"gitea.stevedudenhoeffer.com/steve/go-extractor/sites/polymarket"
"github.com/urfave/cli/v3"
)
func main() {
app := &cli.Command{
Name: "polymarket",
Usage: "Query prediction markets on Polymarket",
Commands: []*cli.Command{
{
Name: "search",
Usage: "Search for markets by keyword",
ArgsUsage: "<query>",
Action: func(ctx context.Context, cmd *cli.Command) error {
query := strings.Join(cmd.Args().Slice(), " ")
if query == "" {
return fmt.Errorf("usage: polymarket search <query>")
}
events, err := polymarket.Search(ctx, query)
if err != nil {
return err
}
if len(events) == 0 {
fmt.Println("No events found.")
return nil
}
for _, e := range events {
printEvent(e)
}
return nil
},
},
{
Name: "event",
Usage: "Get event details by slug, ID, or URL",
ArgsUsage: "<slug-or-url>",
Action: func(ctx context.Context, cmd *cli.Command) error {
arg := cmd.Args().First()
if arg == "" {
return fmt.Errorf("usage: polymarket event <slug-or-url>")
}
var ev *polymarket.Event
var err error
if strings.Contains(arg, "polymarket.com") {
ev, err = polymarket.GetEventFromURL(ctx, arg)
} else {
ev, err = polymarket.GetEvent(ctx, arg)
}
if err != nil {
return err
}
printEvent(*ev)
return nil
},
},
{
Name: "market",
Usage: "Get market details by slug",
ArgsUsage: "<slug>",
Action: func(ctx context.Context, cmd *cli.Command) error {
slug := cmd.Args().First()
if slug == "" {
return fmt.Errorf("usage: polymarket market <slug>")
}
m, err := polymarket.GetMarket(ctx, slug)
if err != nil {
return err
}
printMarket(*m, "")
return nil
},
},
},
}
if err := app.Run(context.Background(), os.Args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func printEvent(e polymarket.Event) {
fmt.Printf("Event: %s\n", e.Title)
fmt.Printf(" URL: %s\n", e.URL())
fmt.Printf(" Volume: $%.0f\n", e.Volume)
fmt.Printf(" Liquidity: $%.0f\n", e.Liquidity)
if e.Active {
fmt.Printf(" Status: Active\n")
} else if e.Closed {
fmt.Printf(" Status: Closed\n")
}
if len(e.Markets) > 0 {
fmt.Printf(" Markets:\n")
for _, m := range e.Markets {
printMarket(m, " ")
}
}
fmt.Println()
}
func printMarket(m polymarket.Market, indent string) {
fmt.Printf("%s- %s\n", indent, m.Question)
for i, outcome := range m.Outcomes {
if i < len(m.OutcomePrices) {
fmt.Printf("%s %s: %.1f%%\n", indent, outcome, m.OutcomePrices[i]*100)
}
}
if m.Volume > 0 {
fmt.Printf("%s Volume: $%.0f\n", indent, m.Volume)
}
if m.LastPrice > 0 {
fmt.Printf("%s Last: %.1f%%\n", indent, m.LastPrice*100)
}
}

View File

@@ -0,0 +1,136 @@
package polymarket
import (
"context"
"fmt"
"net/http"
)
// Market represents a single prediction market on Polymarket.
type Market struct {
ID string
Question string
Slug string
Description string
Outcomes []string
OutcomePrices []float64 // probabilities 0-1
Volume float64
Volume24h float64
Liquidity float64
EndDate string
Active bool
Closed bool
BestBid float64
BestAsk float64
LastPrice float64
}
// URL returns the Polymarket web URL for this market.
func (m Market) URL() string {
if m.Slug == "" {
return ""
}
return "https://polymarket.com/event/" + m.Slug
}
// Event represents a prediction market event on Polymarket, which may contain
// one or more individual markets.
type Event struct {
ID string
Title string
Slug string
Description string
Markets []Market
Volume float64
Volume24h float64
Liquidity float64
Active bool
Closed bool
}
// URL returns the Polymarket web URL for this event.
func (e Event) URL() string {
if e.Slug == "" {
return ""
}
return "https://polymarket.com/event/" + e.Slug
}
// Config holds configuration for the Polymarket extractor.
type Config struct {
// BaseURL overrides the Gamma API base URL. Leave empty for the default.
BaseURL string
// HTTPClient overrides the HTTP client used for API requests. Leave nil for default.
HTTPClient *http.Client
}
// DefaultConfig is the default configuration.
var DefaultConfig = Config{}
func (c Config) validate() Config {
return c
}
// Search finds events matching the given query string.
func (c Config) Search(ctx context.Context, query string) ([]Event, error) {
c = c.validate()
return c.searchAPI(ctx, query)
}
// GetEvent retrieves an event by its slug or numeric ID.
func (c Config) GetEvent(ctx context.Context, slugOrID string) (*Event, error) {
c = c.validate()
// Try slug first (most common from URLs)
ev, err := c.getEventBySlug(ctx, slugOrID)
if err == nil {
return ev, nil
}
// Fall back to numeric ID
ev, idErr := c.getEventByID(ctx, slugOrID)
if idErr == nil {
return ev, nil
}
return nil, fmt.Errorf("getting event %q: slug lookup: %w", slugOrID, err)
}
// GetMarket retrieves a single market by its slug.
func (c Config) GetMarket(ctx context.Context, slug string) (*Market, error) {
c = c.validate()
return c.getMarketBySlug(ctx, slug)
}
// GetEventFromURL extracts the event slug from a Polymarket URL and retrieves
// the event. If the URL points to a specific market within an event, the full
// event is still returned.
func (c Config) GetEventFromURL(ctx context.Context, rawURL string) (*Event, error) {
c = c.validate()
_, slug, _, err := parsePolymarketURL(rawURL)
if err != nil {
return nil, err
}
return c.getEventBySlug(ctx, slug)
}
// Convenience functions using DefaultConfig.
func Search(ctx context.Context, query string) ([]Event, error) {
return DefaultConfig.Search(ctx, query)
}
func GetEvent(ctx context.Context, slugOrID string) (*Event, error) {
return DefaultConfig.GetEvent(ctx, slugOrID)
}
func GetMarket(ctx context.Context, slug string) (*Market, error) {
return DefaultConfig.GetMarket(ctx, slug)
}
func GetEventFromURL(ctx context.Context, rawURL string) (*Event, error) {
return DefaultConfig.GetEventFromURL(ctx, rawURL)
}

View File

@@ -0,0 +1,363 @@
package polymarket
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestStringOrArray_Array(t *testing.T) {
input := `["Yes","No"]`
var s stringOrArray
if err := json.Unmarshal([]byte(input), &s); err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
if len(s) != 2 || s[0] != "Yes" || s[1] != "No" {
t.Errorf("got %v, want [Yes No]", s)
}
}
func TestStringOrArray_StringifiedArray(t *testing.T) {
input := `"[\"Yes\",\"No\"]"`
var s stringOrArray
if err := json.Unmarshal([]byte(input), &s); err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
if len(s) != 2 || s[0] != "Yes" || s[1] != "No" {
t.Errorf("got %v, want [Yes No]", s)
}
}
func TestFloatOrArray_Array(t *testing.T) {
input := `[0.65, 0.35]`
var f floatOrArray
if err := json.Unmarshal([]byte(input), &f); err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
if len(f) != 2 || f[0] != 0.65 || f[1] != 0.35 {
t.Errorf("got %v, want [0.65 0.35]", f)
}
}
func TestFloatOrArray_StringArray(t *testing.T) {
input := `["0.65","0.35"]`
var f floatOrArray
if err := json.Unmarshal([]byte(input), &f); err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
if len(f) != 2 || f[0] != 0.65 || f[1] != 0.35 {
t.Errorf("got %v, want [0.65 0.35]", f)
}
}
func TestFloatOrArray_StringifiedStringArray(t *testing.T) {
input := `"[\"0.0125\",\"0.9875\"]"`
var f floatOrArray
if err := json.Unmarshal([]byte(input), &f); err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
if len(f) != 2 || f[0] != 0.0125 || f[1] != 0.9875 {
t.Errorf("got %v, want [0.0125 0.9875]", f)
}
}
func TestFlexFloat_Number(t *testing.T) {
input := `1234.56`
var f flexFloat
if err := json.Unmarshal([]byte(input), &f); err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
if float64(f) != 1234.56 {
t.Errorf("got %v, want 1234.56", f)
}
}
func TestFlexFloat_String(t *testing.T) {
input := `"1787746.945826"`
var f flexFloat
if err := json.Unmarshal([]byte(input), &f); err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
if float64(f) != 1787746.945826 {
t.Errorf("got %v, want 1787746.945826", f)
}
}
func TestParsePolymarketURL(t *testing.T) {
tests := []struct {
name string
url string
wantType string
wantSlug string
wantMarket string
wantErr bool
}{
{
name: "event URL",
url: "https://polymarket.com/event/microstrategy-sell-any-bitcoin-in-2025",
wantType: "event",
wantSlug: "microstrategy-sell-any-bitcoin-in-2025",
},
{
name: "event with market slug",
url: "https://polymarket.com/event/measles-cases/will-there-be-10000-cases",
wantType: "event",
wantSlug: "measles-cases",
wantMarket: "will-there-be-10000-cases",
},
{
name: "www prefix",
url: "https://www.polymarket.com/event/some-event",
wantType: "event",
wantSlug: "some-event",
},
{
name: "wrong host",
url: "https://example.com/event/foo",
wantErr: true,
},
{
name: "too short path",
url: "https://polymarket.com/event",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
typ, slug, mkt, err := parsePolymarketURL(tt.url)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if typ != tt.wantType {
t.Errorf("type = %q, want %q", typ, tt.wantType)
}
if slug != tt.wantSlug {
t.Errorf("slug = %q, want %q", slug, tt.wantSlug)
}
if mkt != tt.wantMarket {
t.Errorf("market = %q, want %q", mkt, tt.wantMarket)
}
})
}
}
func newTestServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, Config) {
t.Helper()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
return srv, Config{BaseURL: srv.URL}
}
func TestSearch(t *testing.T) {
_, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/public-search" {
http.NotFound(w, r)
return
}
q := r.URL.Query().Get("q")
if q != "olympic hockey" {
t.Errorf("query = %q, want %q", q, "olympic hockey")
}
resp := apiSearchResponse{
Events: []apiEvent{
{
ID: "100",
Title: "USA vs CAN Olympic Hockey (Men)",
Slug: "usa-vs-can-olympic-hockey-men",
Markets: []apiMarket{
{
ID: "200",
Question: "Will USA win?",
Slug: "will-usa-win",
Outcomes: stringOrArray{"Yes", "No"},
OutcomePrices: floatOrArray{0.45, 0.55},
Active: true,
},
},
Active: true,
},
{
ID: "101",
Title: "USA vs CAN Olympic Hockey (Women)",
Slug: "usa-vs-can-olympic-hockey-women",
Active: true,
},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
})
events, err := cfg.Search(context.Background(), "olympic hockey")
if err != nil {
t.Fatalf("Search error: %v", err)
}
if len(events) != 2 {
t.Fatalf("got %d events, want 2", len(events))
}
if events[0].Title != "USA vs CAN Olympic Hockey (Men)" {
t.Errorf("title = %q", events[0].Title)
}
if len(events[0].Markets) != 1 {
t.Fatalf("got %d markets, want 1", len(events[0].Markets))
}
m := events[0].Markets[0]
if m.Question != "Will USA win?" {
t.Errorf("question = %q", m.Question)
}
if len(m.OutcomePrices) != 2 || m.OutcomePrices[0] != 0.45 {
t.Errorf("prices = %v", m.OutcomePrices)
}
}
func TestGetEvent(t *testing.T) {
_, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/events/slug/my-event" {
http.NotFound(w, r)
return
}
resp := apiEvent{
ID: "42",
Title: "My Event",
Slug: "my-event",
Markets: []apiMarket{
{
ID: "100",
Question: "Will it happen?",
Slug: "will-it-happen",
Outcomes: stringOrArray{"Yes", "No"},
OutcomePrices: floatOrArray{0.7, 0.3},
BestBid: 0.69,
BestAsk: 0.71,
LastTradePrice: 0.7,
Active: true,
},
},
Volume: 500000,
Liquidity: 25000,
Active: true,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
})
ev, err := cfg.GetEvent(context.Background(), "my-event")
if err != nil {
t.Fatalf("GetEvent error: %v", err)
}
if ev.Title != "My Event" {
t.Errorf("title = %q", ev.Title)
}
if ev.Volume != 500000 {
t.Errorf("volume = %v", ev.Volume)
}
if len(ev.Markets) != 1 {
t.Fatalf("got %d markets, want 1", len(ev.Markets))
}
if ev.Markets[0].BestBid != 0.69 {
t.Errorf("best bid = %v", ev.Markets[0].BestBid)
}
if ev.URL() != "https://polymarket.com/event/my-event" {
t.Errorf("URL = %q", ev.URL())
}
}
func TestGetMarket(t *testing.T) {
_, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/markets/slug/will-it-happen" {
http.NotFound(w, r)
return
}
resp := apiMarket{
ID: "100",
Question: "Will it happen?",
Slug: "will-it-happen",
Outcomes: stringOrArray{"Yes", "No"},
OutcomePrices: floatOrArray{0.7, 0.3},
Volume: flexFloat(500000),
Volume24hr: 12000,
LiquidityClob: 25000,
BestBid: 0.69,
BestAsk: 0.71,
LastTradePrice: 0.7,
Active: true,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
})
m, err := cfg.GetMarket(context.Background(), "will-it-happen")
if err != nil {
t.Fatalf("GetMarket error: %v", err)
}
if m.Question != "Will it happen?" {
t.Errorf("question = %q", m.Question)
}
if m.Volume != 500000 {
t.Errorf("volume = %v", m.Volume)
}
if m.LastPrice != 0.7 {
t.Errorf("last price = %v", m.LastPrice)
}
}
func TestGetEventFromURL(t *testing.T) {
_, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/events/slug/my-event" {
http.NotFound(w, r)
return
}
resp := apiEvent{
ID: "42",
Title: "My Event",
Slug: "my-event",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
})
// Override the URL to point to our test server via the config
ev, err := cfg.GetEventFromURL(context.Background(), "https://polymarket.com/event/my-event")
if err != nil {
t.Fatalf("GetEventFromURL error: %v", err)
}
if ev.Title != "My Event" {
t.Errorf("title = %q", ev.Title)
}
}
func TestSearch_Empty(t *testing.T) {
_, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(apiSearchResponse{})
})
events, err := cfg.Search(context.Background(), "nonexistent")
if err != nil {
t.Fatalf("Search error: %v", err)
}
if len(events) != 0 {
t.Errorf("got %d events, want 0", len(events))
}
}
func TestEventURL(t *testing.T) {
e := Event{Slug: "test-event"}
if e.URL() != "https://polymarket.com/event/test-event" {
t.Errorf("URL = %q", e.URL())
}
e2 := Event{}
if e2.URL() != "" {
t.Errorf("empty slug URL = %q, want empty", e2.URL())
}
}