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

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)
}