Skip to content

Commit 44eb954

Browse files
committed
fix: add missing Python SDK exchange classes and auto-generate from app.ts
1 parent ee08030 commit 44eb954

11 files changed

Lines changed: 844 additions & 196 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Verify COMPLIANCE.md
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'core/src/exchanges/**'
7+
- 'core/scripts/generate-compliance.js'
8+
- '.github/workflows/compliance-check.yml'
9+
10+
jobs:
11+
verify:
12+
name: Verify COMPLIANCE.md is up-to-date
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: '20'
20+
21+
- name: Regenerate COMPLIANCE.md
22+
run: node core/scripts/generate-compliance.js
23+
24+
- name: Check if COMPLIANCE.md changed
25+
run: |
26+
if git diff --exit-code core/COMPLIANCE.md; then
27+
echo "COMPLIANCE.md is up-to-date"
28+
else
29+
echo "COMPLIANCE.md is out of sync with exchange implementations"
30+
echo "Run: npm run generate:compliance --workspace=pmxt-core"
31+
exit 1
32+
fi
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Verify Python Exchange Classes
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'core/src/server/app.ts'
7+
- 'core/scripts/generate-python-exchanges.js'
8+
- '.github/workflows/python-exchanges-check.yml'
9+
10+
jobs:
11+
verify:
12+
name: Verify _exchanges.py is up-to-date
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: '20'
20+
21+
- name: Regenerate exchange classes
22+
run: node core/scripts/generate-python-exchanges.js
23+
24+
- name: Check if Python SDK files changed
25+
run: |
26+
if git diff --exit-code sdks/python/pmxt/_exchanges.py sdks/python/pmxt/__init__.py; then
27+
echo "Python SDK exchange files are up-to-date"
28+
else
29+
echo "Python SDK is out of sync with app.ts"
30+
echo "Run: npm run generate:python-exchanges --workspace=pmxt-core"
31+
exit 1
32+
fi
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Verify TypeScript Client Methods
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'core/src/BaseExchange.ts'
7+
- 'sdks/typescript/scripts/generate-client-methods.js'
8+
- '.github/workflows/typescript-client-check.yml'
9+
10+
jobs:
11+
verify:
12+
name: Verify client.ts methods are up-to-date
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: '20'
20+
21+
- name: Install dependencies
22+
run: npm install --workspace=pmxt-core
23+
24+
- name: Regenerate client methods
25+
run: node sdks/typescript/scripts/generate-client-methods.js
26+
27+
- name: Check if client.ts changed
28+
run: |
29+
if git diff --exit-code sdks/typescript/pmxt/client.ts; then
30+
echo "client.ts methods are up-to-date"
31+
else
32+
echo "client.ts methods are out of sync with BaseExchange.ts"
33+
echo "Run: node sdks/typescript/scripts/generate-client-methods.js"
34+
exit 1
35+
fi

changelog.md

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

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

5+
## [2.17.7] - 2026-02-25
6+
7+
### Fixed
8+
9+
- **Python SDK Missing Exchange Classes**: `pmxt.Probable`, `pmxt.Baozi`, and `pmxt.Myriad` raised `AttributeError` on import because the Python SDK's exchange subclasses were maintained manually and had drifted from the TypeScript core. All three classes are now available.
10+
11+
### Infrastructure
12+
13+
- **Auto-generated Python SDK exchange classes**: `sdks/python/pmxt/_exchanges.py` is now generated from `core/src/server/app.ts` (the single source of truth for registered exchanges) via `core/scripts/generate-python-exchanges.js`. The generator also keeps `__init__.py` imports and `__all__` in sync. A CI guard (`python-exchanges-check.yml`) fails any PR where the generated file diverges from the committed one.
14+
- **Auto-generated `COMPLIANCE.md`**: The feature support matrix is now generated from exchange implementations via `core/scripts/generate-compliance.js`, replacing the previously manual document. A CI guard (`compliance-check.yml`) keeps it in sync with `core/src/exchanges/*/index.ts`.
15+
- **TypeScript SDK client methods CI guard**: Added `typescript-client-check.yml` to fail PRs where `BaseExchange.ts` changes without regenerating the corresponding methods in the TypeScript SDK `client.ts`.
16+
- All three generators are wired into `generate:sdk:all` and run automatically on every publish.
17+
518
## [2.17.6] - 2026-02-24
619

720
### Fixed

core/COMPLIANCE.md

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
1+
<!-- This file is auto-generated by core/scripts/generate-compliance.js -->
2+
<!-- Do not edit manually. To regenerate: npm run generate:compliance --workspace=pmxt-core -->
3+
<!-- Source of truth: core/src/exchanges/*/index.ts -->
4+
15
# Feature Support & Compliance
26

37
This document details the feature support and compliance status for each exchange. PMXT enforces a strict compliance standard to ensure protocol consistency across all implementations.
48

59
## Functions Status
610

7-
| Category | Function | Polymarket | Kalshi | Limitless | Baozi | Myriad | Notes |
8-
| :--- | :--- | :---: | :---: | :---: | :---: | :---: | :--- |
9-
| **Identity** | `name` | | | | | | |
10-
| **Market Data** | `fetchMarkets` | | | | | | Myriad: Multi-chain (Abstract, Linea, BNB) |
11-
| | `searchMarkets` | | | | | | Myriad: keyword param on /markets |
12-
| | `getMarketsBySlug` | | | | | | |
13-
| | `searchEvents` | | | | | | Myriad: keyword param on /questions |
14-
| **Public Data** | `fetchOHLCV` | | | | | | Baozi: No historical data. Myriad: Fixed timeframe charts only |
15-
| | `fetchOrderBook` | | | | | | Baozi: Synthetic from pool ratios. Myriad: Synthetic from AMM price |
16-
| | `fetchTrades` | | | | | | Myriad: via /markets/:id/events (buy/sell actions) |
17-
| **Private Data** | `fetchBalance` | | | | | | Myriad: Approximated from portfolio (no balance endpoint) |
18-
| | `fetchPositions` | | | | | | Myriad: via /users/:address/portfolio |
19-
| **Trading** | `createOrder` | | | | | | Myriad: Returns quote + calldata (AMM, no on-chain execution) |
20-
| | `cancelOrder` | | | | | | Baozi/Myriad: Not supported (pari-mutuel/AMM) |
21-
| | `fetchOrder` | | | | | | Myriad: Not supported (AMM) |
22-
| | `fetchOpenOrders` | | | | | | Myriad: Always empty (AMM, instant execution) |
23-
| **Calculations** | `getExecutionPrice` | | | | | | |
24-
| | `getExecutionPriceDetailed` | | | | | | |
25-
| **Real-time** | `watchOrderBook` | | | | | | Myriad: Poll-based fallback (no WebSocket API) |
26-
| | `watchTrades` | | | | | | Myriad: Poll-based fallback (no WebSocket API) |
11+
| Category | Function | Polymarket | Kalshi | Limitless | Probable | Baozi | Myriad |
12+
| :--- | :--- | :---: | :---: | :---: | :---: | :---: | :---: |
13+
| **Market Data** | `fetchMarkets` | Y | Y | Y | Y | Y | Y |
14+
| | `fetchEvents` | Y | Y | Y | Y | Y | Y |
15+
| | `fetchMarket` | Y | Y | Y | Y | Y | Y |
16+
| | `fetchEvent` | Y | Y | Y | Y | Y | Y |
17+
| **Public Data** | `fetchOHLCV` | Y | Y | Y | Y | Y | Y |
18+
| | `fetchOrderBook` | Y | Y | Y | Y | Y | Y |
19+
| | `fetchTrades` | Y | Y | Y | Y | Y | Y |
20+
| **Private Data** | `fetchBalance` | Y | Y | Y | Y | Y | Y |
21+
| | `fetchPositions` | Y | Y | Y | Y | Y | Y |
22+
| | `fetchMyTrades` | Y | Y | Y | Y | - | Y |
23+
| **Trading** | `createOrder` | Y | Y | Y | Y | Y | Y |
24+
| | `cancelOrder` | Y | Y | Y | Y | Y | - |
25+
| | `fetchOrder` | Y | Y | Y | Y | Y | - |
26+
| | `fetchOpenOrders` | Y | Y | Y | Y | Y | Y |
27+
| | `fetchClosedOrders` | - | Y | Y | - | - | - |
28+
| | `fetchAllOrders` | - | Y | Y | - | - | - |
29+
| **Calculations** | `getExecutionPrice` | Y | Y | Y | Y | Y | Y |
30+
| | `getExecutionPriceDetailed` | Y | Y | Y | Y | Y | Y |
31+
| **Real-time** | `watchOrderBook` | Y | Y | Y | Y | Y | Y |
32+
| | `watchTrades` | Y | Y | Y | - | - | Y |
2733

2834
## Legend
29-
- Compliance Verified (Strict Test Passed)
30-
- Compliance Failure (Test Failed or Feature Broken)
31-
- Partial Support / Skipped (e.g., Missing API/Websocket)
35+
- **Y** - Supported
36+
- **-** - Not supported
3237

3338
## Compliance Policy
3439
- **Failure over Warning**: Tests must fail if no relevant data (markets, events, candles) is found. This ensures that we catch API breakages or unexpected empty responses.

core/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
"extract:jsdoc": "node ../scripts/extract-jsdoc.js",
3636
"generate:docs": "npm run extract:jsdoc && node ../scripts/generate-api-docs.js",
3737
"generate:openapi": "node scripts/generate-openapi.js",
38-
"generate:sdk:all": "npm run generate:openapi && npm run generate:sdk:python && npm run generate:sdk:typescript && npm run generate:docs"
38+
"generate:python-exchanges": "node scripts/generate-python-exchanges.js",
39+
"generate:compliance": "node scripts/generate-compliance.js",
40+
"generate:sdk:all": "npm run generate:openapi && npm run generate:sdk:python && npm run generate:python-exchanges && npm run generate:sdk:typescript && npm run generate:docs && npm run generate:compliance"
3941
},
4042
"keywords": [],
4143
"author": "",
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Generates core/COMPLIANCE.md from exchange implementations.
2+
// Run via: npm run generate:compliance --workspace=pmxt-core
3+
//
4+
// Scans each exchange's index.ts to check which BaseExchange methods are
5+
// overridden, and whether they throw "not supported" / "not available".
6+
// Source of truth: core/src/exchanges/*/index.ts
7+
8+
const fs = require('fs');
9+
const path = require('path');
10+
11+
const EXCHANGES_DIR = path.join(__dirname, '../src/exchanges');
12+
const OUTPUT_PATH = path.join(__dirname, '../COMPLIANCE.md');
13+
14+
// Methods to check, grouped by category for the table.
15+
// Only includes methods that are exchange-specific (implemented per-exchange).
16+
const METHOD_CATEGORIES = [
17+
{ category: 'Market Data', methods: ['fetchMarkets', 'fetchEvents', 'fetchMarket', 'fetchEvent'] },
18+
{ category: 'Public Data', methods: ['fetchOHLCV', 'fetchOrderBook', 'fetchTrades'] },
19+
{ category: 'Private Data', methods: ['fetchBalance', 'fetchPositions', 'fetchMyTrades'] },
20+
{ category: 'Trading', methods: ['createOrder', 'cancelOrder', 'fetchOrder', 'fetchOpenOrders', 'fetchClosedOrders', 'fetchAllOrders'] },
21+
{ category: 'Calculations', methods: ['getExecutionPrice', 'getExecutionPriceDetailed'] },
22+
{ category: 'Real-time', methods: ['watchOrderBook', 'watchTrades'] },
23+
];
24+
25+
// Exchange display order (skip kalshi-demo since it inherits Kalshi fully)
26+
const EXCHANGE_ORDER = ['polymarket', 'kalshi', 'limitless', 'probable', 'baozi', 'myriad'];
27+
28+
function toDisplayName(slug) {
29+
return slug.split('-').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
30+
}
31+
32+
// Check if the exchange directory has a file that overrides the given method.
33+
// We scan index.ts (and websocket.ts for watch* methods) for async method declarations.
34+
function analyzeExchange(exchangeDir) {
35+
const results = {};
36+
37+
const indexPath = path.join(exchangeDir, 'index.ts');
38+
const wsPath = path.join(exchangeDir, 'websocket.ts');
39+
40+
const indexContent = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '';
41+
const wsContent = fs.existsSync(wsPath) ? fs.readFileSync(wsPath, 'utf8') : '';
42+
43+
const allMethods = METHOD_CATEGORIES.flatMap(c => c.methods);
44+
45+
for (const method of allMethods) {
46+
// Check index.ts for override
47+
const methodRegex = new RegExp(`async\\s+${method}\\s*\\(`);
48+
49+
if (methodRegex.test(indexContent)) {
50+
// Method is overridden in index.ts — check if it throws "not supported"
51+
const block = extractMethodBlock(indexContent, method);
52+
if (isNotSupported(block)) {
53+
results[method] = 'no';
54+
} else {
55+
results[method] = 'yes';
56+
}
57+
} else {
58+
// Not overridden in index.ts
59+
// For getExecutionPrice/getExecutionPriceDetailed, these are in BaseExchange
60+
// and work generically for all exchanges that have fetchOrderBook
61+
if (method === 'getExecutionPrice' || method === 'getExecutionPriceDetailed') {
62+
// Available if fetchOrderBook is available
63+
results[method] = results['fetchOrderBook'] || 'no';
64+
} else if (method === 'fetchMarkets' || method === 'fetchEvents' ||
65+
method === 'fetchMarket' || method === 'fetchEvent') {
66+
// These are implemented in BaseExchange via fetchMarketsImpl/fetchEventsImpl
67+
// Check if fetchMarketsImpl or fetchEventsImpl is overridden
68+
const implMethod = method.startsWith('fetchMarket') ? 'fetchMarketsImpl' : 'fetchEventsImpl';
69+
const implRegex = new RegExp(`async\\s+${implMethod}\\s*\\(`);
70+
if (implRegex.test(indexContent)) {
71+
results[method] = 'yes';
72+
} else if (method === 'fetchMarket' || method === 'fetchEvent') {
73+
// fetchMarket/fetchEvent delegates to fetchMarkets/fetchEvents
74+
const parentMethod = method === 'fetchMarket' ? 'fetchMarkets' : 'fetchEvents';
75+
results[method] = results[parentMethod] || 'no';
76+
} else {
77+
results[method] = 'no';
78+
}
79+
} else {
80+
results[method] = 'no';
81+
}
82+
}
83+
}
84+
85+
// Special case: watchOrderBook/watchTrades may be delegated to websocket module
86+
// If index.ts has the method and calls websocket, it's supported
87+
// The index.ts override check above already catches this
88+
89+
return results;
90+
}
91+
92+
function extractMethodBlock(content, methodName) {
93+
const regex = new RegExp(`async\\s+${methodName}\\s*\\(`);
94+
const match = regex.exec(content);
95+
if (!match) return '';
96+
97+
// Find the opening brace of the method body
98+
let i = match.index;
99+
let depth = 0;
100+
let foundOpen = false;
101+
while (i < content.length) {
102+
if (content[i] === '{') {
103+
depth++;
104+
foundOpen = true;
105+
} else if (content[i] === '}') {
106+
depth--;
107+
if (foundOpen && depth === 0) {
108+
return content.slice(match.index, i + 1);
109+
}
110+
}
111+
i++;
112+
}
113+
return content.slice(match.index, Math.min(match.index + 500, content.length));
114+
}
115+
116+
function isNotSupported(block) {
117+
// Check if the method body immediately throws "not supported" or "not available"
118+
return /throw\s+new\s+\w+\(.*not (supported|available)/i.test(block);
119+
}
120+
121+
function statusSymbol(status) {
122+
switch (status) {
123+
case 'yes': return 'Y';
124+
case 'no': return '-';
125+
default: return '?';
126+
}
127+
}
128+
129+
// ---------------------------------------------------------------------------
130+
// Main
131+
// ---------------------------------------------------------------------------
132+
133+
const exchangeResults = {};
134+
for (const slug of EXCHANGE_ORDER) {
135+
const dir = path.join(EXCHANGES_DIR, slug);
136+
if (!fs.existsSync(dir)) {
137+
console.warn(`WARNING: exchange directory not found: ${slug}`);
138+
continue;
139+
}
140+
exchangeResults[slug] = analyzeExchange(dir);
141+
}
142+
143+
// Build the table
144+
const exchangeHeaders = EXCHANGE_ORDER.map(toDisplayName);
145+
const headerRow = `| Category | Function | ${exchangeHeaders.join(' | ')} |`;
146+
const alignRow = `| :--- | :--- | ${EXCHANGE_ORDER.map(() => ':---:').join(' | ')} |`;
147+
148+
const rows = [headerRow, alignRow];
149+
150+
for (const { category, methods } of METHOD_CATEGORIES) {
151+
for (let i = 0; i < methods.length; i++) {
152+
const method = methods[i];
153+
const catCol = i === 0 ? `**${category}**` : '';
154+
const cells = EXCHANGE_ORDER.map(slug => statusSymbol(exchangeResults[slug][method]));
155+
rows.push(`| ${catCol} | \`${method}\` | ${cells.join(' | ')} |`);
156+
}
157+
}
158+
159+
const output = `<!-- This file is auto-generated by core/scripts/generate-compliance.js -->
160+
<!-- Do not edit manually. To regenerate: npm run generate:compliance --workspace=pmxt-core -->
161+
<!-- Source of truth: core/src/exchanges/*/index.ts -->
162+
163+
# Feature Support & Compliance
164+
165+
This document details the feature support and compliance status for each exchange. PMXT enforces a strict compliance standard to ensure protocol consistency across all implementations.
166+
167+
## Functions Status
168+
169+
${rows.join('\n')}
170+
171+
## Legend
172+
- **Y** - Supported
173+
- **-** - Not supported
174+
175+
## Compliance Policy
176+
- **Failure over Warning**: Tests must fail if no relevant data (markets, events, candles) is found. This ensures that we catch API breakages or unexpected empty responses.
177+
178+
## Tests with authentication
179+
requires a dotenv in the root dir with
180+
\`\`\`
181+
POLYMARKET_PRIVATE_KEY=0x...
182+
# Kalshi
183+
KALSHI_API_KEY=...
184+
KALSHI_PRIVATE_KEY=... (RSA Private Key)
185+
# Limitless
186+
LIMITLESS_PRIVATE_KEY=0x...
187+
# Myriad
188+
MYRIAD_API_KEY=...
189+
MYRIAD_WALLET_ADDRESS=0x...
190+
\`\`\`
191+
`;
192+
193+
fs.writeFileSync(OUTPUT_PATH, output);
194+
console.log(`Generated COMPLIANCE.md with ${EXCHANGE_ORDER.length} exchanges`);
195+
for (const slug of EXCHANGE_ORDER) {
196+
const r = exchangeResults[slug];
197+
const supported = Object.values(r).filter(v => v === 'yes').length;
198+
const total = Object.values(r).length;
199+
console.log(` ${toDisplayName(slug)}: ${supported}/${total} methods supported`);
200+
}

0 commit comments

Comments
 (0)