Skip to content

Commit f564205

Browse files
committed
feat: add unified semantic shortcuts (.yes/.no) for binary markets across all SDKs
1 parent 76bc7fd commit f564205

File tree

19 files changed

+291
-19
lines changed

19 files changed

+291
-19
lines changed

changelog.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

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

5+
## [1.2.0] - 2026-01-29
6+
7+
### Added
8+
- **Unified Semantic Shortcuts**: Introduced convenience properties for binary markets across all SDKs (Python and TypeScript).
9+
- **New Properties**: `market.yes`, `market.no`, `market.up`, and `market.down`.
10+
- **Intelligent Mapping**: Implemented shared core logic to automatically identify "Yes" vs "No" outcomes based on labels and common patterns (e.g., "Not X" pairs).
11+
- **Expressive Aliases**: Added `.up` and `.down` as semantic aliases for `.yes` and `.no` to improve readability for directional markets.
12+
13+
### Improved
14+
- **Core Architecture**: Extracted market normalization logic into a shared utility to ensure absolute parity between exchange implementations.
15+
- **SDK Parity**: Updated both auto-generated and handcrafted portions of the Python and TypeScript SDKs to expose the new fields with full type hinting.
16+
517
## [1.1.4] - 2026-01-27
618

719
### Fixed

core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pmxt-core",
3-
"version": "1.0.0-b4",
3+
"version": "1.2.0",
44
"description": "pmxt is a unified prediction market data API. The ccxt for prediction markets.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

core/src/exchanges/kalshi/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { UnifiedMarket, MarketOutcome, CandleInterval } from '../../types';
2+
import { addBinaryOutcomes } from '../../utils/market-utils';
23

34
export const KALSHI_API_URL = "https://api.elections.kalshi.com/trade-api/v2/events";
45
export const KALSHI_SERIES_URL = "https://api.elections.kalshi.com/trade-api/v2/series";
@@ -60,7 +61,7 @@ export function mapMarketToUnified(event: any, market: any): UnifiedMarket | nul
6061
}
6162
}
6263

63-
return {
64+
const um = {
6465
id: market.ticker,
6566
title: event.title,
6667
description: market.rules_primary || market.rules_secondary || "",
@@ -73,7 +74,10 @@ export function mapMarketToUnified(event: any, market: any): UnifiedMarket | nul
7374
url: `https://kalshi.com/events/${event.event_ticker}`,
7475
category: event.category,
7576
tags: unifiedTags
76-
};
77+
} as UnifiedMarket;
78+
79+
addBinaryOutcomes(um);
80+
return um;
7781
}
7882

7983
export function mapIntervalToKalshi(interval: CandleInterval): number {

core/src/exchanges/polymarket/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { UnifiedMarket, MarketOutcome, CandleInterval } from '../../types';
2+
import { addBinaryOutcomes } from '../../utils/market-utils';
23

34
export const GAMMA_API_URL = 'https://gamma-api.polymarket.com/events';
45
export const CLOB_API_URL = 'https://clob.polymarket.com';
@@ -69,7 +70,7 @@ export function mapMarketToUnified(event: any, market: any, options: { useQuesti
6970
});
7071
}
7172

72-
return {
73+
const um = {
7374
id: market.id,
7475
title: market.question ? `${event.title} - ${market.question}` : event.title,
7576
description: market.description || event.description,
@@ -83,7 +84,10 @@ export function mapMarketToUnified(event: any, market: any, options: { useQuesti
8384
image: event.image || market.image || `https://polymarket.com/api/og?slug=${event.slug}`,
8485
category: event.category || event.tags?.[0]?.label,
8586
tags: event.tags?.map((t: any) => t.label) || []
86-
};
87+
} as UnifiedMarket;
88+
89+
addBinaryOutcomes(um);
90+
return um;
8791
}
8892

8993
export function mapIntervalToFidelity(interval: CandleInterval): number {

core/src/server/openapi.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,14 @@ components:
647647
type: array
648648
items:
649649
type: string
650+
"yes":
651+
$ref: '#/components/schemas/MarketOutcome'
652+
"no":
653+
$ref: '#/components/schemas/MarketOutcome'
654+
up:
655+
$ref: '#/components/schemas/MarketOutcome'
656+
down:
657+
$ref: '#/components/schemas/MarketOutcome'
650658

651659
MarketOutcome:
652660
type: object

core/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export interface UnifiedMarket {
3030

3131
category?: string;
3232
tags?: string[];
33+
34+
// Convenience getters for binary markets
35+
yes?: MarketOutcome;
36+
no?: MarketOutcome;
37+
up?: MarketOutcome;
38+
down?: MarketOutcome;
3339
}
3440

3541
export type CandleInterval = '1m' | '5m' | '15m' | '1h' | '6h' | '1d';

core/src/utils/market-utils.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { MarketOutcome, UnifiedMarket } from '../types';
2+
3+
/**
4+
* Standardizes binary market outcomes into yes/no/up/down properties.
5+
*/
6+
export function addBinaryOutcomes(market: UnifiedMarket): void {
7+
const outcomes = market.outcomes;
8+
if (outcomes.length !== 2) return;
9+
10+
const o1 = outcomes[0];
11+
const o2 = outcomes[1];
12+
const l1 = o1.label.toLowerCase();
13+
const l2 = o2.label.toLowerCase();
14+
15+
// 1. Check for explicit opposites
16+
const isYes = (l: string) => l === 'yes' || l === 'up' || l === 'over';
17+
const isNo = (l: string) => l === 'no' || l === 'down' || l === 'under';
18+
19+
if (isYes(l1) || isNo(l2)) {
20+
market.yes = o1;
21+
market.no = o2;
22+
} else if (isYes(l2) || isNo(l1)) {
23+
market.yes = o2;
24+
market.no = o1;
25+
}
26+
// 2. Check for "Not" pattern
27+
else if (l2.startsWith('not ')) {
28+
market.yes = o1;
29+
market.no = o2;
30+
} else if (l1.startsWith('not ')) {
31+
market.yes = o2;
32+
market.no = o1;
33+
}
34+
// 3. Fallback to indexing
35+
else {
36+
market.yes = o1;
37+
market.no = o2;
38+
}
39+
40+
market.up = market.yes;
41+
market.down = market.no;
42+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { addBinaryOutcomes } from '../../src/utils/market-utils';
2+
import { UnifiedMarket, MarketOutcome } from '../../src/types';
3+
4+
describe('addBinaryOutcomes', () => {
5+
const createMockMarket = (outcomeLabels: string[]): UnifiedMarket => {
6+
const outcomes: MarketOutcome[] = outcomeLabels.map((label, index) => ({
7+
id: `id-${index}`,
8+
label,
9+
price: 0.5
10+
}));
11+
12+
return {
13+
id: 'market-1',
14+
title: 'Test Market',
15+
description: 'Test Description',
16+
outcomes,
17+
resolutionDate: new Date(),
18+
volume24h: 1000,
19+
liquidity: 5000,
20+
url: 'https://example.com'
21+
} as UnifiedMarket;
22+
};
23+
24+
test('should map explicit Yes/No outcomes', () => {
25+
const market = createMockMarket(['Yes', 'No']);
26+
addBinaryOutcomes(market);
27+
28+
expect(market.yes?.label).toBe('Yes');
29+
expect(market.no?.label).toBe('No');
30+
});
31+
32+
test('should map explicit Up/Down outcomes', () => {
33+
const market = createMockMarket(['Up', 'Down']);
34+
addBinaryOutcomes(market);
35+
36+
expect(market.up?.label).toBe('Up');
37+
expect(market.down?.label).toBe('Down');
38+
expect(market.yes?.label).toBe('Up');
39+
expect(market.no?.label).toBe('Down');
40+
});
41+
42+
test('should map "Not" patterns correctly', () => {
43+
const market = createMockMarket(['Kevin Warsh', 'Not Kevin Warsh']);
44+
addBinaryOutcomes(market);
45+
46+
expect(market.yes?.label).toBe('Kevin Warsh');
47+
expect(market.no?.label).toBe('Not Kevin Warsh');
48+
});
49+
50+
test('should handle reversed order correctly', () => {
51+
const market = createMockMarket(['No', 'Yes']);
52+
addBinaryOutcomes(market);
53+
54+
expect(market.yes?.label).toBe('Yes');
55+
expect(market.no?.label).toBe('No');
56+
});
57+
58+
test('should handle reversed "Not" order correctly', () => {
59+
const market = createMockMarket(['Not Trump', 'Trump']);
60+
addBinaryOutcomes(market);
61+
62+
expect(market.yes?.label).toBe('Trump');
63+
expect(market.no?.label).toBe('Not Trump');
64+
});
65+
66+
test('should map Over/Under correctly', () => {
67+
const market = createMockMarket(['Over 2.5', 'Under 2.5']);
68+
addBinaryOutcomes(market);
69+
70+
expect(market.yes?.label).toBe('Over 2.5');
71+
expect(market.no?.label).toBe('Under 2.5');
72+
});
73+
74+
test('should do nothing for non-binary markets', () => {
75+
const market = createMockMarket(['Home', 'Away', 'Draw']);
76+
addBinaryOutcomes(market);
77+
78+
expect(market.yes).toBeUndefined();
79+
expect(market.no).toBeUndefined();
80+
});
81+
82+
test('should fallback to first index if "Not" cannot be determined', () => {
83+
// Obscure labels that don't match any patterns
84+
const market = createMockMarket(['Alpha', 'Beta']);
85+
addBinaryOutcomes(market);
86+
87+
expect(market.yes?.label).toBe('Alpha');
88+
expect(market.no?.label).toBe('Beta');
89+
});
90+
});

readme.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ import pmxt
7373
api = pmxt.Polymarket()
7474
markets = api.get_markets_by_slug('who-will-trump-nominate-as-fed-chair')
7575

76-
warsh = next((m for m in markets if m.outcomes[0].label == 'Kevin Warsh'), None)
76+
# Find the specific candidate in the election event
77+
warsh = next((m for m in markets if m.yes.label == 'Kevin Warsh'), None)
7778

78-
print(warsh.outcomes[0].price) # price of 'Yes' outcome
79+
print(f"Price: {warsh.yes.price}")
7980
```
8081

8182
> **Note**: For TypeScript usage, see [pmxtjs documentation](https://pmxt.dev/docs).

sdks/python/API_REFERENCE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,8 +535,19 @@ url: str #
535535
image: str #
536536
category: str #
537537
tags: List[string] #
538+
yes: MarketOutcome # Convenience access to Yes outcome
539+
no: MarketOutcome # Convenience access to No outcome
540+
up: MarketOutcome # Alias for .yes (used for Up/Down markets)
541+
down: MarketOutcome # Alias for .no (used for Up/Down markets)
538542
```
539543

544+
**Binary Outcome Mapping Logic:**
545+
546+
PMXT automatically identifies binary outcomes for markets with exactly two outcomes using the following priority:
547+
1. **Explicit Labels**: Matches "Yes", "Up", or "Over".
548+
2. **Opposite Pattern**: Matches pairs like "Kevin Warsh" and "Not Kevin Warsh", identifying the non-"Not" side as `yes`.
549+
3. **Fallback**: If no pattern is matched, index 0 is mapped to `yes/up` and index 1 to `no/down`.
550+
540551
---
541552
### `MarketOutcome`
542553

0 commit comments

Comments
 (0)