package kalshi import ( "context" "fmt" "net/http" "strings" "time" ) // Market represents a single prediction market on Kalshi. type Market struct { Ticker string EventTicker string Title string Subtitle string Status string YesBid float64 // 0-1 (probability) YesAsk float64 NoBid float64 NoAsk float64 LastPrice float64 Volume int Volume24h int OpenInterest int Result string // "yes", "no", or "" if unresolved Rules string CloseTime time.Time } // URL returns the Kalshi web URL for this market. func (m Market) URL() string { if m.Ticker == "" { return "" } return "https://kalshi.com/markets/" + m.Ticker } // Event represents a prediction market event on Kalshi, which may contain // one or more individual markets. type Event struct { EventTicker string SeriesTicker string Title string Subtitle string Category string Markets []Market } // URL returns the Kalshi web URL for this event. func (e Event) URL() string { if e.EventTicker == "" { return "" } return "https://kalshi.com/markets/" + e.EventTicker } // Config holds configuration for the Kalshi extractor. type Config struct { // BaseURL overrides the 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 // MaxSearchResults limits the number of results returned by Search. // Defaults to 20 if zero. MaxSearchResults int } // DefaultConfig is the default configuration. var DefaultConfig = Config{} func (c Config) validate() Config { if c.MaxSearchResults <= 0 { c.MaxSearchResults = 20 } return c } // Search finds events matching the given query string by fetching open events // and filtering by title. Kalshi does not provide a text search API, so this // performs client-side substring matching. func (c Config) Search(ctx context.Context, query string) ([]Event, error) { c = c.validate() return c.searchEvents(ctx, query, c.MaxSearchResults) } // GetMarket retrieves a single market by its ticker. func (c Config) GetMarket(ctx context.Context, ticker string) (*Market, error) { c = c.validate() return c.getMarketByTicker(ctx, strings.ToUpper(ticker)) } // GetEvent retrieves an event by its ticker, including all nested markets. func (c Config) GetEvent(ctx context.Context, eventTicker string) (*Event, error) { c = c.validate() return c.getEventByTicker(ctx, strings.ToUpper(eventTicker)) } // GetMarketFromURL extracts the market ticker from a Kalshi URL and retrieves // the market details. func (c Config) GetMarketFromURL(ctx context.Context, rawURL string) (*Market, error) { c = c.validate() ticker, err := parseKalshiURL(rawURL) if err != nil { return nil, err } return c.getMarketByTicker(ctx, ticker) } // GetEventFromURL extracts the event ticker from a Kalshi URL and retrieves // the event details. func (c Config) GetEventFromURL(ctx context.Context, rawURL string) (*Event, error) { c = c.validate() ticker, err := parseKalshiURL(rawURL) if err != nil { return nil, err } // The ticker from the URL could be either a market ticker or event ticker. // Try as event first, then fall back to getting the market and looking up its event. ev, err := c.getEventByTicker(ctx, ticker) if err == nil { return ev, nil } // Try as market ticker - get the market, then get its event m, mErr := c.getMarketByTicker(ctx, ticker) if mErr != nil { return nil, fmt.Errorf("getting event from URL: %w", err) } return c.getEventByTicker(ctx, m.EventTicker) } // Convenience functions using DefaultConfig. func Search(ctx context.Context, query string) ([]Event, error) { return DefaultConfig.Search(ctx, query) } func GetMarket(ctx context.Context, ticker string) (*Market, error) { return DefaultConfig.GetMarket(ctx, ticker) } func GetEvent(ctx context.Context, eventTicker string) (*Event, error) { return DefaultConfig.GetEvent(ctx, eventTicker) } func GetMarketFromURL(ctx context.Context, rawURL string) (*Market, error) { return DefaultConfig.GetMarketFromURL(ctx, rawURL) } func GetEventFromURL(ctx context.Context, rawURL string) (*Event, error) { return DefaultConfig.GetEventFromURL(ctx, rawURL) }