Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to this project will be documented in this file.

## [2.43.27] - 2026-05-25

### Fixed

- **Kalshi**: Use enriched series titles to normalize contaminated broad-future event titles. Multi-market futures such as Champions League Winner and conference championship winner events no longer inherit current-matchup labels like `PSG vs Arsenal` or `Cleveland vs New York` as their PMXT event title, while true match events keep their matchup titles.
- **Kalshi**: Add regression coverage for contaminated futures, already-sane futures, and true matchup events.

## [2.43.26] - 2026-05-25

### Fixed

- **SDK streaming**: Remove TypeScript and Python REST fallbacks for `watchOrderBook`, `watchOrderBooks`, `watchTrades`, and `unwatchOrderBook`. Streaming methods now require the hosted `/ws` transport and fail fast if WebSocket transport is unavailable, preventing accidental 30s REST long-poll calls to `/api/{exchange}/watch*`.
- **Python SDK**: Add regression coverage proving streaming methods use WebSocket transport and do not invoke HTTP fallbacks.

## [2.43.25] - 2026-05-24

### Added
Expand Down
44 changes: 22 additions & 22 deletions core/COMPLIANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,28 @@ This document details the feature support and compliance status for each exchang

## Functions Status

| Category | Function | Polymarket | Kalshi | Limitless | Probable | Baozi | Myriad | Opinion | Metaculus |
| :--- | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| **Market Data** | `fetchMarkets` | Y | Y | Y | Y | Y | Y | Y | Y |
| | `fetchEvents` | Y | Y | Y | Y | Y | Y | Y | Y |
| | `fetchMarket` | Y | Y | Y | Y | Y | Y | Y | Y |
| | `fetchEvent` | Y | Y | Y | Y | Y | Y | Y | Y |
| **Public Data** | `fetchOHLCV` | Y | Y | Y | Y | Y | Y | Y | - |
| | `fetchOrderBook` | Y | Y | Y | Y | Y | Y | Y | - |
| | `fetchTrades` | Y | Y | Y | Y | Y | Y | - | - |
| **Private Data** | `fetchBalance` | Y | Y | Y | Y | Y | Y | - | - |
| | `fetchPositions` | Y | Y | Y | Y | Y | Y | Y | - |
| | `fetchMyTrades` | Y | Y | Y | Y | - | Y | Y | - |
| **Trading** | `createOrder` | Y | Y | Y | Y | Y | Y | Y | Y |
| | `cancelOrder` | Y | Y | Y | Y | Y | - | Y | Y |
| | `fetchOrder` | Y | Y | Y | Y | Y | - | Y | - |
| | `fetchOpenOrders` | Y | Y | Y | Y | Y | Y | Y | - |
| | `fetchClosedOrders` | - | Y | Y | - | - | - | Y | - |
| | `fetchAllOrders` | - | Y | Y | - | - | - | Y | - |
| **Calculations** | `getExecutionPrice` | Y | Y | Y | Y | Y | Y | Y | - |
| | `getExecutionPriceDetailed` | Y | Y | Y | Y | Y | Y | Y | - |
| **Real-time** | `watchOrderBook` | Y | Y | Y | Y | Y | Y | Y | - |
| | `watchTrades` | Y | Y | Y | - | - | Y | Y | - |
| Category | Function | Polymarket | Kalshi | Limitless | Probable | Baozi | Myriad | Opinion | Metaculus | SuiBets |
| :--- | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| **Market Data** | `fetchMarkets` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| | `fetchEvents` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| | `fetchMarket` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| | `fetchEvent` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| **Public Data** | `fetchOHLCV` | Y | Y | Y | Y | Y | Y | Y | - | - |
| | `fetchOrderBook` | Y | Y | Y | Y | Y | Y | Y | - | Y (emulated) |
| | `fetchTrades` | Y | Y | Y | Y | Y | Y | - | - | - |
| **Private Data** | `fetchBalance` | Y | Y | Y | Y | Y | Y | - | - | - |
| | `fetchPositions` | Y | Y | Y | Y | Y | Y | Y | - | Y |
| | `fetchMyTrades` | Y | Y | Y | Y | - | Y | Y | - | - |
| **Trading** | `createOrder` | Y | Y | Y | Y | Y | Y | Y | Y | - |
| | `cancelOrder` | Y | Y | Y | Y | Y | - | Y | Y | - |
| | `fetchOrder` | Y | Y | Y | Y | Y | - | Y | - | - |
| | `fetchOpenOrders` | Y | Y | Y | Y | Y | Y | Y | - | - |
| | `fetchClosedOrders` | - | Y | Y | - | - | - | Y | - | - |
| | `fetchAllOrders` | - | Y | Y | - | - | - | Y | - | - |
| **Calculations** | `getExecutionPrice` | Y | Y | Y | Y | Y | Y | Y | - | - |
| | `getExecutionPriceDetailed` | Y | Y | Y | Y | Y | Y | Y | - | - |
| **Real-time** | `watchOrderBook` | Y | Y | Y | Y | Y | Y | Y | - | - |
| | `watchTrades` | Y | Y | Y | - | - | Y | Y | - | - |

## Legend
- **Y** - Supported
Expand Down
9 changes: 9 additions & 0 deletions core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ export class ValidationError extends BaseError {
// 5xx Server/Network Errors
// ============================================================================

/**
* 501 Not Implemented - The requested operation is not supported
*/
export class NotSupported extends BaseError {
constructor(message: string, exchange?: string) {
super(message, 501, 'NOT_SUPPORTED', false, exchange);
}
}

/**
* 503 Service Unavailable - Network connectivity issues (retryable)
*/
Expand Down
84 changes: 64 additions & 20 deletions core/src/exchanges/kalshi/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,23 @@ export interface KalshiRawMarket {
export interface KalshiRawEvent {
event_ticker: string;
title: string;
sub_title?: string;
image_url?: string;
category?: string;
tags?: string[];
series_ticker?: string;
series_title?: string;
mutually_exclusive?: boolean;
markets?: KalshiRawMarket[];

[key: string]: unknown;
}

interface KalshiSeriesInfo {
title?: string;
tags?: string[];
}

export interface KalshiRawEventPage {
events: KalshiRawEvent[];
cursor?: string | null;
Expand Down Expand Up @@ -149,7 +157,7 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw

// Instance-level cache (moved from module-level)
private cachedEvents: KalshiRawEvent[] | null = null;
private cachedSeriesMap: Map<string, string[]> | null = null;
private cachedSeriesMap: Map<string, KalshiSeriesInfo> | null = null;
private lastCacheTime: number = 0;

constructor(ctx: FetcherContext) {
Expand Down Expand Up @@ -205,16 +213,16 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
this.fetchAllWithStatus('closed'),
this.fetchAllWithStatus('settled'),
]);
return [...openEvents, ...closedEvents, ...settledEvents];
return this.enrichEventsWithSeriesList([...openEvents, ...closedEvents, ...settledEvents]);
} else if (status === 'closed' || status === 'inactive') {
const [closedEvents, settledEvents] = await Promise.all([
this.fetchAllWithStatus('closed'),
this.fetchAllWithStatus('settled'),
]);
return [...closedEvents, ...settledEvents];
return this.enrichEventsWithSeriesList([...closedEvents, ...settledEvents]);
}

return this.fetchAllWithStatus('open');
return this.enrichEventsWithSeriesList(await this.fetchAllWithStatus('open'));
} catch (error: any) {
throw kalshiErrorMapper.mapError(error);
}
Expand All @@ -232,7 +240,9 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
if (status === 'settled') apiStatus = 'settled';

const limit = Math.max(1, Math.floor(params.limit || BATCH_SIZE));
return this.fetchPageWithStatus(apiStatus, limit, params.cursor);
const page = await this.fetchPageWithStatus(apiStatus, limit, params.cursor);
await this.enrichEventsWithSeriesList(page.events);
return page;
} catch (error: any) {
throw kalshiErrorMapper.mapError(error);
}
Expand Down Expand Up @@ -366,14 +376,17 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
return data.orders || [];
}

async fetchRawSeriesMap(): Promise<Map<string, string[]>> {
async fetchRawSeriesMap(): Promise<Map<string, KalshiSeriesInfo>> {
try {
const data = await this.ctx.callApi('GetSeriesList');
const seriesList = data.series || [];
const map = new Map<string, string[]>();
const map = new Map<string, KalshiSeriesInfo>();
for (const series of seriesList) {
if (series.tags && series.tags.length > 0) {
map.set(series.ticker, series.tags);
if (series.ticker) {
map.set(series.ticker, {
title: typeof series.title === 'string' ? series.title : undefined,
tags: Array.isArray(series.tags) ? series.tags : undefined,
});
}
}
return map;
Expand All @@ -394,19 +407,23 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
const event = data.event;
if (!event) return [];

// Enrich with series tags
// Enrich with series metadata. The series title helps the normalizer
// avoid polluted event titles on multi-market futures.
if (event.series_ticker) {
try {
const seriesData = await this.ctx.callApi('GetSeries', {
series_ticker: event.series_ticker,
});
const series = seriesData.series;
if (typeof series?.title === 'string' && series.title.trim()) {
event.series_title = series.title.trim();
}
if (series?.tags?.length > 0 && (!event.tags || event.tags.length === 0)) {
event.tags = series.tags;
}
} catch (err: unknown) {
// Non-critical — tags are enrichment only.
logger.warn('kalshi: series tag fetch failed', {
// Non-critical — series metadata is enrichment only.
logger.warn('kalshi: series metadata fetch failed', {
series_ticker: event.series_ticker,
error: err instanceof Error ? err.message : String(err),
});
Expand Down Expand Up @@ -438,14 +455,7 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
this.fetchRawSeriesMap(),
]);

// Enrich events with series tags
for (const event of allEvents) {
if (event.series_ticker && fetchedSeriesMap.has(event.series_ticker)) {
if (!event.tags || event.tags.length === 0) {
event.tags = fetchedSeriesMap.get(event.series_ticker);
}
}
}
this.enrichEventsWithSeriesMap(allEvents, fetchedSeriesMap);

if (fetchLimit >= 1000 && useCache) {
this.cachedEvents = allEvents;
Expand All @@ -456,6 +466,40 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
return allEvents;
}

private async enrichEventsWithSeriesList(events: KalshiRawEvent[]): Promise<KalshiRawEvent[]> {
if (events.length === 0) return events;

try {
const seriesMap = await this.fetchRawSeriesMap();
this.enrichEventsWithSeriesMap(events, seriesMap);
} catch (err: unknown) {
// Non-critical — callers can still normalize the venue-native title.
logger.warn('kalshi: series list enrichment failed', {
error: err instanceof Error ? err.message : String(err),
});
}

return events;
}

private enrichEventsWithSeriesMap(
events: KalshiRawEvent[],
seriesMap: Map<string, KalshiSeriesInfo>,
): void {
for (const event of events) {
if (!event.series_ticker) continue;
const seriesInfo = seriesMap.get(event.series_ticker);
if (!seriesInfo) continue;

if (seriesInfo.title) {
event.series_title = seriesInfo.title;
}
if (seriesInfo.tags?.length && (!event.tags || event.tags.length === 0)) {
event.tags = seriesInfo.tags;
}
}
}

private async fetchActiveEvents(targetMarketCount?: number, status: string = 'open'): Promise<KalshiRawEvent[]> {
let allEvents: KalshiRawEvent[] = [];
let totalMarketCount = 0;
Expand Down
Loading