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