[Automated changes] feat: Prediction on ccxt (#28752)
* restore eslintrc
* fix liniting
* add missing tpyes
* missing config file
* rm js folder
* rm file
* polymarket static tests
* fix request tests
* fix(pro): make ArrayCacheBySymbolById keyField non-enumerable
keyField was set via a plain assignment, which on an Array subclass creates
an enumerable own property. It then leaked into array equality/iteration,
breaking the base WS cache test (assert equals(cache, [object2, object3]))
once the JS was freshly transpiled. Define it via Object.defineProperty like
the other internal cache fields so it stays invisible; ArrayCacheByOutcomeById
still overrides the value via its writable reassignment.
* fix(examples): use comma args in prediction-common-fed-event console.log
The example transpiler rejects console.log with + concatenation (breaks the
Python/PHP/C#/Go example transpile step); switch to comma-separated args.
* remove symbol from prediction types
* add more filtering options to prediction
* use sort and other params
* add example
* update example
* update types.py
* remove loadMarkets calls and marketSymbol refs
* rm loadMarkets and others
* remove symbol > outcome
* rm more symbol
* rename symbol to event
* update example
* fix precision
* some kalshi fixes (still not working signing)
* some hL fixes
* some fixes and HL example
* fix limitless order placement using eoa
* kalshi signature fix
* fix markets kalshi
* add example
* feat(prediction): outcome convention, native PredictionOrderBook + types, cross-lang fixes
Source + transpiler contribution on top of the prediction-on-ccxt base (generated
per-exchange files intentionally omitted — they regenerate from this source).
- prediction methods use the `outcomes`/`outcome` handle, not `symbols` (industry
convention): fetchTickers/fetchPositions take `outcomes` across all 5 exchanges
- safePredictionOrderBook base helper; fetchOrderBook returns PredictionOrderBook
(outcome/outcomeId/market, no symbol) consistently in all 5 exchanges
- native dedicated structs PredictionOrderBook / PredictionTradingFee /
PredictionOpenInterest in C#/Go/Java (+ transpiler type mappings) — they were
referenced by generated wrappers but never defined, so prediction didn't compile
- fetchEvents typed with fetchEventsParams; mapped per language
- base filterByValueSinceLimit uses safeValue(entry,field) so a missing field is a
non-match, not a python/php KeyError on outcome-keyed structures
- Go transpiler: prediction methods that rename a base option param emit a
prediction-local options struct (FetchTickersOptionsStruct{ Outcomes }); generate
go/v4/prediction/exchange_dynamic.go; map fetchEventsParams
- myriad populateOutcomes normalizes legacy symbol/id/marketSymbol keys (Go/C#/Java
don't dispatch the prediction setMarkets override)
- Java EIP-712 signing (ethEncodeStructuredData root type, encode, ethAbiEncode)
- test framework + cli: prediction is async-only (ccxt.prediction.<id>); java cli
loads optional credentials and respects real args
- regenerate prediction kalshi abstract (PortfolioEventsOrders)
Static response tests pass for polymarket/kalshi/limitless/myriad in all six
languages when regenerated from this source. (myriad watchTickers stays `symbols`:
WS options live in the pro package — outcome rename there is a follow-up.)
* refactor(prediction): WS methods use outcome(s); drop symbol aliases
- all prediction watch* methods take outcome/outcomes (matching REST + the base
PredictionExchange declarations): myriad watchTickers/watchPositions/watchTicker/
watchOrderBook/watchTrades/watchMyTrades/watchOHLCV/watchOrders, polymarket
watch* — no more `symbols`
- use outcome/outcomes directly in the method bodies instead of a
`const symbol = outcome` alias (cleaner; same for the REST fetchTickers/
fetchPositions)
- Go transpiler: copy the cached extractTypeAndFuncNames Set before deleting the
prediction-local option-struct names — mutating the shared cache de-qualified
WatchTickersOptions/WatchPositionsOptions in other exchanges' base delegations
WS list methods that rename a base option param (watchTickers `outcomes` vs base
`symbols`) now emit a prediction-local options struct with Outcomes, like the REST
fetchTickers fix.
* fix(kalshi): parseOrder reads V2 order fields (live-verified on demo)
The V2 order objects (GET /portfolio/orders, fetchOpenOrders) return
yes_price_dollars/no_price_dollars (already dollars), initial_count_fp,
fill_count_fp and remaining_count_fp — parseOrder only read the legacy
yes_price/no_price (cents), count and filled_count, so fetched orders came back
with price/amount/remaining undefined. Now read the V2 fields (price by the
outcome's own YES/NO leg) with legacy fallbacks.
Verified live on the demo environment (demo-api.kalshi.co): RSA-PSS signing,
fetchBalance, createOrder (resting limit), fetchOpenOrders, fetchOrder and
cancelOrder all work; fetched orders now report price/amount/remaining correctly.
* feat(prediction): RSA-PSS signing in all 6 langs + outcome convention cleanup
- RSA-PSS support in PHP (manual EMSA-PSS via openssl NO_PADDING since
ext-openssl has no native PSS), C#, Go and Java base crypto, so kalshi's
RSASSA-PSS request signing works in every language
- kalshi fetchMarkets: hoist .length to locals + use arraySlice so the PHP
regex transpiler emits count()/array_slice() not strlen()/mb_substr()
- base parseOrders / parseTrades / filterByValueSinceLimit: use
safeValue/safeString so prediction structures keyed on outcome (no symbol)
don't KeyError in python/php
- goTranspiler: prediction-local option structs for outcome-renamed params
- drop doubled "outcome outcome" docstring artifacts across all 5 exchanges
Live-tested kalshi create/fetch/cancel on the demo env in TS, JS, Python,
PHP, C#, Go and Java.
* fix(kalshi): cancelAllOrders uses the real batched-cancel endpoint
cancelAllOrders hit DELETE /portfolio/orders which 404s — kalshi V2 has no
"cancel all" endpoint. Now it fetches the resting orders and batch-cancels
them by id via DELETE /portfolio/orders/batched, chunked to 20 ids/call with
arraySlice (transpiler-safe — no variable-step for loop). Dropped the phantom
portfolio/orders DELETE from the api block.
Live-verified on the kalshi demo: swept 32 leftover resting orders -> 0 (JS),
and create->cancelAllOrders->0 (Python). Builds clean in all 6 langs.
* fix(base): Go ethEncodeStructuredData infers nested EIP-712 root type
toTypedDataTypes hardcoded the primary (root) type as OrderWithBuilderFee→
Order→first-key, so nested typed data like the ERC-7739
TypedDataSign(Order contents,…) wrapper picked "Order" and hashed the wrong
struct (the go-ethereum apitypes "doesn't match type 'address'" panic). Now
the root is inferred as the struct no other struct references as a field type
— matching ethers' TypedDataEncoder. Unblocks polymarket deposit-wallet
(signatureType 3) createOrder signing in Go.
Live-verified on the real polymarket CLOB: deposit-wallet createOrder →
open → cancel in Go; signClobOrder byte-identical to JS/Python/Java/C#/PHP.
hyperliquid/derive/paradex Go static request tests still pass (no regression).
* fix(polymarket): make createOrder signing transpile correctly to PHP
Five php-regex-transpiler issues that blocked deposit-wallet createOrder in
PHP (the other 5 languages were already correct):
- sign(): 'auth/derive-api-key' built by concatenation so the `api` param
name does not leak into the literal as '$api' (broke the L1-auth routing)
- signClobOrder: param renamed signatureType→sigType and local chainId→
chainIdValue so neither leaks into the 'Order(...)' / 'EIP712Domain(...)'
type-string literals (which feed the EIP-712 type hashes)
- signClobOrder: compare parseToInt(sigType) !== 3 — php types the number
param as float and 3.0 !== 3 (strict) always took the wrong (EOA) branch
- signClobOrder: orderTypeString.length used inline so php emits strlen(),
not count() (the `const n = str.length;` form wrongly assumes an array)
- L2 HMAC: unchained the .replaceAll calls (php converts only the outermost)
- buildClobOrderBody: made synchronous (no I/O) — a no-await async method
transpiles to a php promise-typed wrapper returning a plain array, throwing
Live-verified deposit-wallet createOrder → open → cancel on the real
polymarket CLOB in ALL 6 languages; signClobOrder byte-identical across all.
* fix(base): Go auto-loads prediction outcomes after loadMarkets
Go has no virtual dispatch, so base loadMarketsHelper's this.SetMarkets()
bypassed the PredictionExchange.SetMarkets override that builds the outcome
lookup — leaving this.Outcomes nil after LoadMarkets, so every
outcome-resolving method (fetchTicker/fetchOrderBook/createOrder/…) panicked
"outcomes not loaded" in Go for ALL prediction exchanges. loadMarketsHelper
now invokes setOutcomesFromMarkets() on the concrete instance via a type
assertion when it implements it (non-prediction exchanges do not, so they
are unaffected). Mirrors the TS override that runs inside setMarkets.
Live-verified: limitless createOrder → open → cancel works in Go with no
explicit SetOutcomesFromMarkets() call (Outcomes auto-populates, count 820).
* fix(base): Java omits Content-Type on body-less requests
The Java HTTP client forced Content-Type: application/json on every non-GET
request even when the body was empty, diverging from the other languages.
Some APIs reject that — limitless DELETE /orders/{id} returns 400 "Body
cannot be empty when content-type is set to 'application/json'". Now
Content-Type is sent only when the exchange set one explicitly, or there is
an actual request body.
Live-verified: limitless createOrder -> open -> cancelOrder now works in Java.
* chore(prediction): regenerate generated files across all 6 langs after the master merge
The upstream merge auto-merged committed generated files, leaving the prediction
base classes, abstracts and per-exchange files stale/inconsistent — and it
corrupted go/v4/exchange_generated.go (duplicated content → syntax error, Go
package wouldn't build). Regenerated from the merged source:
- prediction base classes: PredictionExchange.{cs,java}, exchange_prediction.go,
prediction_exchange.py, PredictionExchange.php
- kalshi abstract (the new portfolio/events/orders endpoint) across langs
- per-exchange prediction generated files (Go/C#/Java/Python/PHP)
- go/v4/exchange_generated.go rebuilt (removes the merge's duplicated funcs)
All six languages now build/transpile clean: TS (tsBuild), JS, Python, PHP, C#
(0 errors), Go, Java (compileJava). Non-prediction *_api.go whitespace churn from
the abstract regen was reverted to keep the diff scoped; C# Exchange.BaseMethods.cs
left as merged (builds fine).
* docs(prediction): build per-exchange docs + prediction sections in wiki, website, skills
- jsdoc2md.js: render the prediction exchanges (js/src/prediction) to
wiki/exchanges/prediction/<id>.md and index them in the sidebar, so the
jsdoc→md docs build now covers prediction exchanges (own subdir avoids the
hyperliquid name collision with the regular exchange)
- build/wiki-to-fumadocs.ts: emit a "Prediction Markets" sub-group under
Exchanges on the website (website/content/docs/exchanges/prediction/ with its
own meta.json, referenced from the parent meta)
- wiki/Manual.md: fix the Prediction Markets section — async-only Python/PHP
namespaces (no async_support / async\ sub-namespace) and the outcome
convention (outcome/outcomeId, not symbol/id); add a createOrder example
- .claude/skills/ccxt-{typescript,python,php,csharp,go,java}: add a Prediction
Markets section (namespace access, outcome convention, create/cancel example)
* docs(prediction): list prediction exchanges in Supported Exchanges, by-country & examples
- build/export-exchanges.js: inject the prediction-exchange table into
wiki/Exchange-Markets.md (+ README) via the prediction-list markers, and
include prediction exchanges in the Exchange-Markets-By-Country listing
(polymarket/kalshi declare US; the country-less DEX-style ones don't group,
same as other country-less exchanges)
- wiki/Exchange-Markets.md + README.md: add the "Prediction Market Exchanges"
section + markers the generator fills
- wiki/examples: prediction example docs (via examples2md.js) now generated for
ts/py/php/cs/go, so they show in the website Examples section
Verified on the rebuilt fumadocs site: /docs/exchange-markets shows the 5
prediction exchanges, /docs/exchange-markets-by-country lists polymarket+kalshi
under United States, and the prediction example pages serve under /docs/examples.
* docs(prediction): animated data-model diagram, sortable exchanges table, unified-methods catalogue
- new Prediction Markets guide (wiki/Prediction-Markets.md) as the /docs/prediction
tab index, with a bespoke animated Event -> Market -> Outcome -> methods diagram
(PredictionDataModel) and a grouped catalogue of every unified PredictionExchange
method, replacing the thin fetchEvents/fetchEvent-only overview
- add a "Prediction" top-nav link (-> /docs/prediction/polymarket, mirroring Exchanges
-> binance) since the sidebar root-switcher is disabled
- merge the 5 prediction exchanges into the main Supported Exchanges table via a
sortable/filterable ExchangesTable component (search, type/certified/pro filters,
column sort, logo fallback); wiki-to-fumadocs parses the markdown tables into its data
- wire ```prediction-diagram / ```exchanges-table fences to components via a rehype step
- Manual.md points to the new guide; home page card -> /docs/prediction
* docs(prediction): fail the build if the Supported Exchanges table parses to empty
parseExchangeMarkets is coupled to export-exchanges.js's markdown (column order +
the <!--- init list -->/<!--- init prediction list --> markers). Throw with an
actionable message when crypto or prediction rows parse to 0, instead of silently
shipping a blank <ExchangesTable/>.
* test(prediction): live-test harness for prediction exchanges across all languages
Make run-tests.js exercise every prediction-market method (public, private,
and order placement) live in all six languages, and capture the gaps that
geo-blocking and TLS fingerprinting leave for static fixtures.
Harness (transpiles to JS/Py/PHP/C#/Go/Java):
- tests.ts: runPredictionTests resolves the outcome handle from typed markets
and drives prediction-specific + trading methods via dynamic dispatch
(each wrapped in try/catch for Java's checked exceptions); --fundedTests
gates a non-marketable place-then-cancel order (0.10 USD); createOrder
honors a getSkips() skip so geo-blocked CI does not attempt placement
- tests.helpers.ts: resolve ids present in both namespaces to ccxt.prediction
only under --prediction (hyperliquid collision)
- base validators guard prediction shapes (market/ticker/trade/orderBook/
position/tradingFee), afterConstruct reads options via safeDict, and
fetchTickers tests by outcome handle instead of the no-arg "all" path
Runner + per-language infra:
- run-tests.js concatenates ids+prediction and adds --prediction/--fundedTests
- utils/check_modified_files.sh detects ts/src/prediction/*.ts changes
- python argparse learns --prediction/--fundedTests; php memory_limit 2048M
for kalshi's ~1000 markets
Exchange fixes surfaced by live testing:
- kalshi: migrate cancelOrder/cancelAllOrders to the V2 endpoint
(DELETE /portfolio/events/orders/{order_id}; v1 is 410 Gone) and sanitize
negative bid/ask sizes in parseTicker
- PredictionExchange: fix strlen-on-array transpile of queries.length
Static + skips:
- skip-tests.json: route kalshi + polymarket through the CI proxy and skip
polymarket createOrder live (geo-blocked); signing stays covered by the
request fixtures
- add polymarket createOrder/cancelOrder response fixtures (pass JS/C#/Go;
Python/PHP prediction static tests are a pre-existing async-harness gap)
* ignore transpiling for now
* require query search inside fetch events
* pretty print events in cli.ts
* -p support in CLI
* fix CLi and HL
* fix(base): apply credentials in Go SetProperty and accept any slice in ArrayConcat
Two Go base bugs surfaced while live-testing prediction exchanges, both affecting
all exchanges (not just prediction):
- SetProperty only set a field when the value was already assignable to it, but it
did not guard the reflect.Set, so a plain map passed for a typed field (e.g.
Options *sync.Map) would panic. Now it skips when the value is not AssignableTo
the field type.
- ArrayConcat did a hard aa.([]any) assertion and panicked on []string (which
ObjectKeys returns in Go — the balance validator feeds it exactly that). It now
copies elements through reflection so any slice type works, and drops a dead
duplicate branch.
* test(prediction): force the prediction namespace under --prediction in every language
Live-verify prediction trading (create→cancel + private reads) across all six
languages, which required the test harness to actually build the prediction class.
- For ids present in both namespaces (only hyperliquid), every non-JS init helper
fell back to prediction only when the id was ABSENT from crypto, so --prediction
was ignored and it built the crypto class — Go's "pass" was a false positive
(no prediction outcome line). Now python/php/cs/go/java honor --prediction;
C#/Java add a forcePrediction param to the lib DynamicallyCreateInstance and the
test BaseTest passes getCliArgValue("--prediction").
- Go SetExchangeProp passed the value as the property NAME (so creds were never
set) and FieldByName is case-sensitive — fixed to capitalize and pass the name.
- Harness reads skip-tests.json preferredPredictionOutcome (some venues list many
resolved/halted markets whose first outcome can't be traded) and fundedAmount/
fundedPrice (hyperliquid testnet has a 10 USD min vs the default 0.10 USD).
- Validator prediction guards: test.order skips symbol; test.position skips the
derivatives-only fields a share-holding lacks; test.sharedMethods assertType
accepts a dict against an empty-array format marker (PHP's is_dictionary([]) is
false, so an empty {} format failed — no-op in JS, binance response tests pass).
* make search easier and more flexible
* fix(hyperliquid): transpile prediction createOrder/fetchEvents to PHP and typed langs
Surfaced when the cross-language harness finally ran the prediction hyperliquid:
- fetchEvents and initializeClient were async with no await, so the PHP/typed
transpilers emit a non-promise the caller's await then chokes on. Both now
await loadMarkets() at the top, which is also correct — fetchEvents builds
events from the loaded markets client-side and createOrder needs them to
resolve the outcome handle.
- signHash did signature['r'].padStart(...) on a subscript, which leaks an
undefined padStart() call in PHP — assigned to a bare local first.
* feat(polymarket): send the PING heartbeat required by the CLOB websocket
Polymarket's market and user ws channels have no protocol-level ping-pong and
require a plain-text "PING" every 10s (the server replies "PONG", already skipped
as a string frame). The implementation handled incoming PONG but never sent PING,
so idle connections would drop. Added a streaming block (ping + keepAlive 10000)
and a ping() returning "PING". Verified live in JS: watchOrderBook streams and
watchOrders receives the open/canceled events for a placed-then-cancelled order.
* test(prediction): scope fetchEvents with a query where the venue requires one
The prediction fetchEvents implementations now call requireEventQuery (added on
this branch), which throws unless the call is scoped by query/queries/tags/eventId/
slug — so the harness's unscoped fetchEvents([]) broke for every prediction
exchange. runPredictionTests now passes a skip-tests.json preferredEventQuery as
{query} when set. Added matching queries (verified to return events): polymarket
trump, myriad/limitless bitcoin, hyperliquid WORLD, kalshi trump.
* fix trades/mytrades
* fix(base): send the first ws ping after one keepAlive interval, not on connect
The Python aiohttp ws client's ping_loop sent the first ping immediately on
connect, before the subscribe frame. Some servers (e.g. Polymarket's CLOB ws)
close the connection (1006) if a ping arrives before the subscribe. The JS client
already waits one interval (setInterval fires after keepAlive), so this aligns
Python with it by sleeping before the first ping.
* fix(polymarket): use safe dict access in the ws order-book and trade handlers
handleOrderBookSnapshot/Delta and handleTrade read this.orderbooks[outcome] and
this.trades[outcome] directly, which raises KeyError in Python (JS returns
undefined). Use `outcome in this.orderbooks` and safeValue for the existence
checks, matching the standard pro exchanges (okx/binance). Fixes the Python ws
KeyError once the connection stays open.
* test(prediction): route prediction WS to the prediction class in every language
The harness instantiated ccxt.pro.<id> for any --ws run, but prediction exchanges
have no ccxt.pro variant — their watch* methods live on the main prediction class
(no separate pro namespace), so this errored with "ccxt.pro[id] is not a
constructor". The init helpers now resolve the prediction namespace before the
ws/pro branch in all six languages (JS/Python/PHP route to the prediction class;
C#/Go/Java keep the bare id and let forcePrediction pick the non-pro prediction
package). The watch* tests now run for prediction exchanges in every language.
* fix fetchPositions
* fix(polymarket): orderBook({}) for empty book + ws keepalive on text PONG
this.orderBook([]) builds an empty book from an empty *list*, which the typed
languages reject: C#/Go cast the snapshot to a dict, an empty list becomes null,
and the dict constructor dereferences nil (this was the C# watchTicker NPE). Use
{} (the standard pro pattern) so the empty book is a dict in every language.
Polymarket keeps the ws alive with text PING/PONG (not protocol frames), so the
client's onPong never fires; refresh client.lastPong on the "PONG" reply so the
Go keepalive does not drop the connection after maxPingPongMisses.
Add wsProxy for polymarket so C#/Go can reach the ws endpoint (their TLS
fingerprint is rejected directly by Cloudflare; mirrors the existing httpsProxy).
* fix(go): unblock prediction test harness build
The prediction Go test harness failed to compile (blocking every Go test,
including ws):
- ICoreExchange was missing FetchTransfers, which the generated
test.fetchTransfers.go references on the exchange interface.
- test.fetchTransfers declared an unused `now`; Go rejects unused variables.
* fix(tests): null-safe C# dump() on null args
C# dump() called value.ToString() with no null guard and crashed on a null
argument, unlike the JS/Python/PHP dumps which stringify null. The prediction
ws tests dump exchange.json(eventId), and eventId is null in C# (ws tests carry
no eventId).
* fix(polymarket): avoid KeyError in tokenIdToSymbol on the ws trade path
The markets_by_id fallback used bare marketsById[tokenId] and market['symbol'];
both are undefined in JS but raise KeyError in Python when the token is not a
market id, which crashed watch_trades when a trade arrived. Use safeDict/
safeString so the lookup degrades to undefined in every language.
* fix(pro): ArrayCache.append tolerates items without a 'symbol' (python)
The Python ArrayCache.append read item['symbol'] directly, which raises KeyError
for items that carry 'outcome' instead of 'symbol' (prediction ws trades, whose
'symbol' is stripped by safePredictionTrade). JS yields undefined for the same
access, so this is a Python-only port gap. Use item.get('symbol').
Blast radius: every Python async watch* using ArrayCache. No behavior change for
items that have 'symbol' (regression-checked); symbol-less items no longer crash.
Verified live: polymarket watch_trades now returns trades with zero KeyError.
* feat(prediction): prediction types for all unified methods, WS book identity, PredictionOrderRequest
- promote the unified methods onto PredictionExchange with Prediction* return
types (fetchOrderTrades, fetchMyTrades, fetchPosition, createOrders,
cancelOrders, watchOrders, watchMyTrades, watchPositions, watchTickers,
fetchOpenInterest, fetchTradingFee, createMarket*OrderWithCost, ...) so venues
that don't override a method still expose prediction-typed signatures instead
of the crypto Exchange fallback; Go/C# wrappers source from the prediction base
for inherited methods
- watchOrderBook returns PredictionOrderBook in the Go/C# typed bindings, and the
live WS OrderBook cache carries outcome/outcomeId/market in all 6 languages
(added conditionally so crypto order books are unchanged)
- add PredictionOrderRequest (outcome instead of symbol) for createOrders
- fix Go implicit-api casing (callEndpointAsync -> CallEndpointAsync)
- fix Python prediction base type import
* test(polymarket): createOrders static fixture uses outcome (PredictionOrderRequest)
The createOrders request fixture still passed each order as { symbol: ... };
since createOrders now reads `outcome` (PredictionOrderRequest), update the
fixture key so the static request test passes in JS/C#/Go.
* docs(prediction): JSDoc for the PredictionExchange base unified methods
Add JSDoc blocks to the unified methods promoted onto PredictionExchange
(fetchOrderTrades, fetchMyTrades, fetchPosition, createOrders, cancelOrders,
watch*, fetchOpenInterest, fetchTradingFee, createMarket*OrderWithCost, the
retyped fetch*/create*/cancel*/watch* single-outcome methods, ...). These
drive the generated Python/PHP/C#/Go docstrings.
* fix(prediction): resolve cross-language user-test bugs + regenerate stale outputs
Source fixes:
- limitless: hoist allRaw.length to a named local (was mis-transpiled to
strlen() on an array, fataling fetchMarkets in PHP/Python) and route the
six "pad hex to even" sites through a padHexToEven helper (inline
`.length % 2` became a broken 1-arg fmod() in PHP, breaking EVM signing)
- polymarket: seed the WS order book via a hoisted local so `orderBook({})`
transpiles to a map/Dictionary, not a list/slice (was a deterministic
WatchTicker NullReferenceException in C# and a process panic in Go); add the
missing `market` field to fetchOpenInterest and fetchTradingFee for shape
parity with every other prediction structure
- PredictionExchange: an unknown outcome now throws BadSymbol (was
ArgumentsRequired)
- php/pro/ArrayCache: fall back to the `outcome` handle when an item has no
`symbol` (prediction trades flooded watchTrades with undefined-key warnings)
- transpile.ts: detect PredictionOrderRequest in Python param annotations so
venue files import it (createOrders) instead of NameError-ing at import
Regenerate the prediction package across all six languages (the committed
generated code was stale: kalshi/limitless called a renamed checkEvents,
exchange_generated.go was missing CleanWsData/CleanRestData so a fresh
checkout didn't compile Go, and the watchOrderBook/typed-base fixes hadn't
propagated). Builds/transpiles + static request/response + WS base tests pass
in all six languages; kalshi/limitless verified live in Python/PHP and the
WatchTicker crash verified fixed in C#.
* fix(prediction): drop unused `symbol` key from WS order books (REST/WS parity)
The live WS OrderBook cache always seeded a `symbol` key, so watchOrderBook
returned `{ symbol: undefined, ... }` while REST fetchOrderBook (via
safePredictionOrderBook) omits it — `'symbol' in book` diverged between WS and
REST for the dict-based languages (JS/Python/PHP) and Java's toMap. Prediction
books are keyed by `outcome`; drop `symbol` in reset()/toMap when an outcome is
present (crypto books keep `symbol` unchanged). Verified live: REST and WS now
agree (no `symbol`, has `outcome`); WS base tests still pass.
* fix(kalshi): resolve market search via events endpoint; tidy fetch-limit options
fetchMarkets({query}) sent only limit/cursor to the API and filtered every
open market client-side, paging ~25 x 2.6MB and hanging. It now resolves the
query against the events endpoint (bounded, multi-query, server-scoped) and
returns the matched events' markets; the events scan drops nested markets so
matched events fetch their own (4x faster, ~100s -> ~23s).
Also: wire the previously-dead defaultFetchEventsLimit option, drop the
phantom fetchMarketsLimit (read but never defined) in favour of
maxFetchMarketsLimit, fix the maxPages JSDoc (5 -> 50), and regenerate the
stale TS abstract (kalshiPrivateDeletePortfolioEventsOrdersOrderId).
* fix(examples): correct prediction example imports, fetchEvents scoping, and outcome fields
The prediction examples under examples/ts/prediction/ couldn't run:
- import path was one level too shallow (../../ts -> ../../../ts)
- fetchEvents(['x']) used an array shorthand the API never supported; use
fetchEvents({ queries: ['x'] })
- read .outcomeId for the unified outcome id (.id/.symbol are undefined on
polymarket/limitless)
- unscoped fetchEvents() now throws (requireEventQuery); use loadMarkets() to
prime outcomes, or a scoped query
- hyperliquid fetchOHLCV needs loadMarkets() first
- prediction-fetch-events-options scopes each case with a query
All public read examples verified live; the private templates now reach the
auth step (still need real credentials).
* feat(prediction): auto-load & cache outcomes like loadMarkets
Outcome-addressed methods now auto-load on first use and serve from cache,
mirroring loadMarkets()+market() — no more "fetch events first" throws.
Base PredictionExchange:
- loadOutcomes(reload, params): bulk loader (loadMarkets + populateOutcomes),
idempotent, mirrors loadMarkets reload/params
- loadOutcome(id): per-call resolver — cache hit, else bulk-warm
(options.loadAllOutcomes, default true) or single-fetch via fetchOutcome
- fetchOutcome(id): "fetch one" — base bulk-fallback; exchanges override
- populateOutcomes(): rebuild caches from markets (renamed from
setOutcomesFromMarkets; myriad's duplicate removed); checkEvents() removed
Single outcome → loadOutcome(id), full set → loadOutcomes(); fetchOrder
resolves cache-only (1 request). kalshi has too many markets to bulk-load,
so it sets loadAllOutcomes=false and overrides fetchOutcome to fetch one
market by ticker and cache it. Both unified handle and native outcomeId
resolve once loaded (dual index). Pattern documented in
.claude/rules/prediction-outcomes.md.
Verified live on all 5 exchanges; builds across all 6 languages.
* fix(kalshi): classify not-found 404 as BadSymbol; clearer cold fetchOutcome error
kalshi's missing-market 404 ({"error":{"code":"not_found"}}) was mapped by the
base to ExchangeNotAvailable — indistinguishable from an outage. Add handleErrors
+ an exceptions block mapping not_found -> BadSymbol, and have fetchOutcome
re-throw it with a hint ("pass an outcomeId, or call fetchEvents()/loadOutcomes()
first") for a cold unified handle, while letting genuine network errors propagate.
* fix(prediction): export prediction namespace from the built js/ccxt.js bundle
The tracked js/ccxt.js was stale — it never re-exported the prediction
namespace after the prediction exchanges were added, so `ccxt.prediction`
was undefined from the built bundle (breaking the canonical JS/TS examples
and normal `import ccxt from 'ccxt'; ccxt.prediction.*` usage). Regenerate
the bundle + its .d.ts so `prediction` is exported.
* Revert "fix(prediction): export prediction namespace from the built js/ccxt.js bundle"
This reverts commit 669a983e8c59d7da8c1413a8326841fba9ecb512.
* fix(examples): make python prediction example self-contained
examples/py/prediction_markets.py did a bare `import ccxt.prediction`, which
picks up a pip-installed ccxt (no prediction) instead of this repo. Prepend
the repo's python/ to sys.path so it runs directly with `python3 examples/py/
prediction_markets.py` (matching the PHP example, which self-includes ccxt.php).
* fix(examples): close(True) to tear down REST session in python prediction example
* fix(prediction): outcome-aware parsing fixes + parsePrediction* base aggregators
- PredictionExchange: add parsePredictionTrades/Orders/Positions - the base
parseTrades/parseOrders/parsePositions post-filter by the market 'symbol' key
(or resolve symbols), silently dropping prediction structures; all call sites
in the five exchanges converted
- limitless: fetchTrades always returned []; optional-outcome methods
(fetchMyTrades/fetchOrder/fetchOrdersByIds/cancelOrder/cancelOrders/
cancelAllOrders) threw BadSymbol when outcome was omitted
- kalshi: fetchPositions outcomes filter never matched; fetchOpenOrders dropped
every row in python; parseOrder now derives the leg from the raw side
('no' -> ticker + '-NO') instead of always resolving the YES leg
- polymarket: parsePosition raw ['event'] access crashed python; empty params
serialized to a "[]" body in php (empty array is also an empty dict there)
- hyperliquid: restore userFills/userFillsByTime in fetchMyTrades (it delegated
to the public recentTrades tape)
- all: import error classes from base/errors.js, not the ccxt.js bundle
(circular import crashed deep imports of prediction modules)
- cs: rsa() PEM parsing drops header/footer by content, tolerating the trailing
newline openssl writes (kalshi RSA-PSS keys)
- fixtures: first kalshi private fixtures (committed throwaway PKCS#1 RSA key;
C#/Go rsa are PKCS#1-only), fetchPositions/fetchOpenOrders/fetchMyTrades
coverage, refreshed stale fetchTrades captures that had enshrined the bugs
* fix(tests): prediction CI paths — async-only sync skip + namespace-aware scoped tooling
- static fixtures declare asyncOnly; the python/php sync harnesses skip them
(prediction is async-only there by design) — full python sync sweep passes
4448 tests instead of crashing on the first prediction id
- transpile.ts/csharpTranspiler/goTranspiler/javaTranspiler auto-route bare
prediction-only ids (e.g. 'transpile.ts kalshi') to ts/src/prediction/, so
the scoped CI transpile steps work unchanged
- goTranspiler always emits the full prediction set (a single-exchange run
truncated the shared exchange_wrapper_structs.go)
- js.yml scoped lint resolves ts/src/prediction/ paths
- regenerated test harnesses (cs generated tests were stale vs ts/src/test)
* fix(prediction): export the prediction namespace from the built js bundle
js/src/prediction modules were committed but js/ccxt.js never regenerated, so
ccxt.prediction was undefined for every ESM/CJS/browser consumer; also brings
the committed prediction js modules and the js test harness current with ts
* fix(prediction): remove broken python async_support/pro stub subpackages
ccxt.prediction is flattened async-only in python; the stubs imported modules
that don't exist and shipped in the wheel as instant ModuleNotFoundError;
align the prediction __init__ version and drop the stubs from vss
* chore: remove committed scratch/debug debris
probe scripts (lmtsprobe/, cs/lmtsprobe/), design-doc drafts
(prediction-types-proposal.*, prediction-types.ts) and an unreferenced 145 KB
sample payload (ts/src/base/polyMarketEvent.json)
* docs(examples): polymarket spain-world-cup prediction example
* fix(prediction): unify the market field + outcome auto-load stragglers
- myriad (9 sites) and hyperliquid (6 sites) reported the outcome handle in the
'market' field instead of the parent market symbol; myriad parseOrder read
outcomeId from the legacy 'id' key; hyperliquid parseTicker day-volume now
resolves the parent market so quoteVolume is populated
- polymarket watch* auto-load the outcome cache instead of throwing on a cold
instance (watchOrderBook/watchTrades/watchTicker/watchOrders/watchMyTrades)
- hyperliquid fetchTickers()/fetchPositions() warm the outcome set on a cold
instance instead of silently returning nothing
- kalshi fetchBalance/fetchOrder/cancelOrder/cancelAllOrders no longer force a
full market-listing scan for label-only lookups (1 request instead of 30+)
* docs(prediction): JSDoc for the parsePrediction* base helpers
* fix(build): make the full CI transpile/test matrix pass end-to-end
- csharpTranspiler/goTranspiler: the prediction pass inside transpileEverything
reused the CLI's regular-exchange ids and tried to transpile them from
ts/src/prediction/ — every --multi worker crashed (this broke CI's transpileCS);
scoped runs with no prediction work now skip the pass
- goTranspiler: safeOptionsStructFile's non-prediction tail referenced phantom
locals (isPrediction/isWs/needsCcxtImport/structsContent) — restored the
upstream loop; csharpTranspiler: createCSharpClass passed an undefined
restNamespace arg in the ws pass
- transpile.ts: the prediction WS-import block also fired for regular pro files,
duplicating the ArrayCache import in every python/ccxt/pro/*.py (ruff F811)
- cs base: convertToBigInt passes hex strings through (extended's stark chain
parses hex itself; decimal strings still become BigInteger for eip-712),
numberToString/intToBase16 handle BigInteger (not IConvertible)
- prediction sources: ruff-clean comments (E265) and truthiness instead of
'!== null' (E711); drop the unused asyncio import and kalshi's comment-induced
unused error import
- export the prediction namespace from dist/cjs and the browser bundles
- spain-world-cup example: PredictionMarket has no 'symbol' (tsBuildExamples)
* docs(examples): regenerate prediction example docs
* fix(merge): post-merge reconciliation with upstream master
- go/v4/exchange_interface.go: both sides added FetchTransfers (branch harness
fix + upstream #29012); dedupe the auto-merged duplicate
- wiki/Exchange-Markets.md: restore the prediction-table markers upstream's side
didn't carry, regenerate the table
- regenerate the js/dist bundles on top of the merged sources
* chore: mark dist/js and prediction generated outputs linguist-generated
* fix(prediction): review-round bug fixes, live event cache, prediction CLIs everywhere
- kalshi: fetchOHLCV(since) without limit sets end_ts; parseMarket reads the
current volume_fp/liquidity_dollars/open_interest_fp keys; cancelOrder
backfills id/status (the delete response carries neither)
- polymarket: parseEvent maps the real gamma keys (createdAt/endDate/image/
updatedAt/closed) and carries 'active' so the client-side status filter works
- limitless: missing privateKey throws ArgumentsRequired instead of a raw
TypeError; feeRateBps defaults to 0; search honors params.limit (capped at
the API max of 50); fetchAccounts/approve has-flags; myriad declares pro:true
- PredictionExchange: event cache is live - applyEventFetchParams merges every
fetched event into this.events/events_by_slug, setEvents accumulates,
getEvent() resolves cache-only ('event' is a C# keyword, hence the name);
dropped the never-usable reloadingEvents/eventsLoading members
- CLIs: python/php/go/c# route prediction ids to the prediction namespace
(regular ids win for duals, --prediction forces); go gets a generated
prediction exchange_metadata.go wired into export-exchanges/vss
live-verified: kalshi DEMO full trading lifecycle (create->fetch->open->cancel
->refetch) in js and python, demo fetchBalance through all four CLIs, plus
polymarket events/cache, limitless limits and the privateKey guard
* fix(prediction): full-matrix live-test fixes across all six languages
- limitless: fetchOHLCV buckets raw price points into timeframe-aligned candles
(single points carried unaligned timestamps; inline bucketing - buildOHLCVC
transpiles to a mangled name; php needs the value-copy write-back)
- hyperliquid: fetchEvents reuses the cached market load so advertised handles
always match the outcome cache; venues can rotate outcome ids, so the base
loadOutcome retries once with a forced reload when a warm cache misses
- kalshi: 5 req/s throttle (the demo host 429s at 10 req/s)
- polymarket: handleErrors + exceptions map (dead CLOB book 404 -> BadSymbol
instead of retryable ExchangeNotAvailable; balance/allowance/geo mappings)
- go: fetchEvents/fetchOutcome registered for virtual dispatch (kalshi's
on-demand fetchOutcome and loadEvents overrides were unreachable from the
base in go); IDerivedExchange + base stub extended
- test harness: preferredPredictionOutcome pins are validated against the live
listing and fall back to the market scan when stale; pin polymarket to a
liquid 2028 market (C#/Go/Java dict ordering scans onto placeholder markets
with no CLOB book); refresh the stale hyperliquid pin path
live-verified: all 5 prediction exchanges x all 6 languages, public+private
(kalshi on the demo host), full run-tests --prediction --private green
* refactor(prediction): architecture/perf/DX improvements from review
Architecture (PredictionExchange base):
- extract indexMarketOutcomes(market) — kalshi on-demand fetchOutcome indexes one
market instead of a full O(markets x outcomes) rebuild per outcome
- shortenSlug collision handling: same handle + different outcomeId now
disambiguates (append id suffix) instead of silently overwriting
- setEvents keys events by the unified event handle too; eventsList() helper;
deleted the 3 inline populateOutcomes copies + limitless rebuildOutcomes + the
per-exchange manual event-cache writes
- cold-instance guards: populateOutcomes no-ops when markets are unloaded; the
no-query fetchEvents branch serves from the cache without crashing
- loadOutcome indexes already-loaded markets for free before any network call, so
cold-cache resolution is consistent across languages (removed the over-eager
forced-reload that clobbered offline markets and masked live typos)
- filterEventsByTags implemented (tags was accepted but filtered nothing);
actionable outcome/getEvent errors; safeOutcome stub carries an event key
Performance:
- polymarket pagination was silently broken (page size 500 vs gamma's 100 cap, so
the >=pageSize check never fired) — fixed to 100, default fetchMarketsLimit 200
to bound the ~90 KB/event cold start
- kalshi fetchMarkets requests min(maxMarkets, pageLimit), not always 1000
- myriad ethRpc returns safeValue not safeString (a receipt object was coerced to
"[object Object]")
Types/docs:
- fetchEventsParams gains queries + tags
- wiki + language skills: fetchEvents({ query }) not fetchEvents([...]) (the old
form throws), cold caches auto-load (not "throws, fetch first"), outcome-cache
+ CLI sections, event structure symbol->event, prediction checklist in the
new-exchange skill
Verified: all 6 languages regen + build + offline static tests green; live
public+private smoke green for all 5 exchanges (kalshi on the demo host).
* refactor(prediction): extract EVM toolkit, fix event types, add fetchEvent coverage
Architecture:
- move the shared EVM helpers (padHexToEven/padHexAddress/rlpEncode*/intToRlpHex/
hexToRlpBytes/ethRpc/sendEvmTransaction/waitForTransactionReceipt) from
limitless+myriad onto PredictionExchange — they're used by zero crypto
exchanges, so the base Exchange (transpiled into ~100 exchanges) is the wrong
home. signEvmTransaction stays per-exchange (needs the noble crypto imports the
prediction base skeletons don't carry); the base sendEvmTransaction dispatches
to it. Net -65 source lines and the ethRpc receipt-object drift can't recur.
go: signEvmTransaction registered for (sync) virtual dispatch + IDerivedExchange
method + base stub
- PredictionEvent.markets: PredictionMarket[] -> Market[] (venues put ccxt market
rows in it; the wrong type gave typed-language users empty structs)
- polymarket fetchEvents now delegates to the shared applyEventFetchParams
(injecting its fuzzy-search defaults status=active/searchIn=title as explicit
params), so all five venues filter identically and sort/eventId/slug/tags now
work on polymarket (sort was silently ignored)
- remove dead slugToMarketId; createMarketBuy/SellOrderWithCost use safeBool, not
a raw options[...] access that KeyErrors in Python/PHP
Tests:
- strengthen the prediction-event validator (id + event handle + each market's
outcomes list + typed active/tags/info), wired into the live fetchEvents and
fetchEvent paths so it runs in all six languages
- add a myriad fetchEvent static fixture (limitless's dropped — its 6-decimal
1e-6 precision serialises as 1.0E-6 in PHP vs 1.0e-6 elsewhere, an unavoidable
float-format divergence; covered live instead)
Verified: all 6 languages regen + build + offline static green; live prediction
tests green for limitless (py/php/cs) and myriad (js/go/java).
* fix(prediction): search-based kalshi fetchEvents + fix cold-cache event scopes
kalshi:
- point trade-api at external-api.kalshi.com; add the elections search host
- fetchEvents resolves each scope server-side then fetches canonically, no client scan:
query -> ranked /v1/search/series; tags/category -> /series; series_ticker verbatim;
eventId is the event_ticker (direct). limit now bounds fetching, not just output
- createOrder requires a price (kalshi has only limit orders)
limitless / myriad:
- no-query scopes (eventId/slug/tags) fetch from the API instead of serving an empty
cold cache, which silently returned []
hyperliquid:
- cache events through setEvents so getEvent resolves by id/slug/handle
tests:
- runPredictionTests exercises each fetchEvents scope (eventId round-trip, limit bound,
and configured preferredEventScopes) so a broken parameter path fails loudly
* static tests
* hl tests
* move static tests
* adapt cli
* run static tests
* add hl events
* fix import
* feat(prediction): settlement + resolution reads across all venues
Adds a read-side settlement/resolution surface to the prediction exchanges:
- PredictionSettlement type (ts/src/base/types.ts) + Go/C# native structs
- base PredictionExchange.fetchSettlements (NotSupported stub)
- kalshi: fetchSettlements + parseSettlement (yes/no leg the user held, won,
amount, price, cost, payout, pnl); has['fetchSettlements'] = true
- resolution fields on every parseMarket/outcome: market.resolved +
market.resolvedOutcome, and per-outcome winner + settleFraction
- kalshi: status==settled / result
- polymarket outcomePrices (0/1) + umaResolutionStatus/closed
- myriad: resolvedOutcomeId (-1 until resolved) + voided
- limitless: winningOutcomeIndex
Transpiler:
- regex transpiler (build/transpile.ts) skips comment-only method chunks and
strips leading //-comment/blank lines before signature detection. Fixes a
pre-existing crash on the prediction base's EVM section-divider comment that
silently truncated the Python/PHP prediction base (dropping the EVM helpers).
Provably a no-op for the main Exchange base (it has no such chunks).
- kalshi parseSettlement: hoisted a compound ternary condition (broke the Py
ternary rewrite) and reworded a comment whose " in " triggered the PHP
in->array_key_exists rewrite.
Verified: TS build + lint clean; Py/PHP syntax; C#/Go build; offline
request/response green in JS/C#/Go across kalshi/polymarket/myriad/limitless;
resolution + settlement parsing verified offline with mocks.
* fix(polymarket): return a fully-populated order from createOrder
The CLOB create response only echoes {orderID, status, success}, so
createOrder/createOrders returned orders with undefined side/price/amount.
buildClobOrderBody now also returns a `request` echo (keyed as the fetchOrder
response fields parseOrder reads); createOrder/createOrders merge it before
parsing and reset `info` to the raw response so it stays clean.
Live-verified against the polymarket CLOB: a limit buy round-trip now returns
side=buy price=0.03 amount=5 timeInForce=GTC (previously all undefined),
created status=open then canceled (0 open orders after). Signed order body
unchanged (request tests 12/12); createOrder response fixture regenerated;
offline response green in JS/C#/Go.
* fix(polymarket): map a killed FAK/FOK order to OrderNotFillable
A FAK/FOK order that finds no match is killed by the CLOB (HTTP 400) — a
normal order outcome, not a transport failure. It fell through handleErrors to
the base default and surfaced as ExchangeNotAvailable, so a caller would retry
as if the exchange were down. Map "no orders found to match" and "could not be
fully filled" to OrderNotFillable.
Live-verified: a non-marketable FAK buy now throws OrderNotFillable (was
ExchangeNotAvailable). Offline request/response green in JS/C#/Go.
* feat(kalshi): fetchMyTrades + fix PredictionSettlement Python import
fetchMyTrades: fetch the authenticated user's fills via GET /portfolio/fills,
parsed per-leg (yes -> <ticker>, no -> <ticker>-NO) into unified trades with
side (from action), price (yes/no_price_dollars), amount (count_fp), cost, fee
(fee_cost), takerOrMaker (is_taker), order (order_id) + outcome filter. has
flag set. Live-verified on kalshi demo in JS and Python (identical output:
KXBTCMAX100-26-DEC buy 0.25 x10 cost 2.5 fee $0.13 taker).
Also fixes a latent Python-only NameError in the settlement feature: the
`PredictionSettlement` return type was never defined in python/ccxt/base/types.py
nor registered in the transpiler's import-detection list, so importing any
prediction module raised NameError at load (Python static tests don't run and
py_compile only checks syntax, so it slipped through). Add the TypedDict, the
transpiler import regex, and the PHP array-return regex entry. Verified all 5
prediction modules now import in Python. Offline request/response green JS/C#/Go.
* feat(kalshi): fetchOrders + fetchClosedOrders
fetchOrders returns every order (resting/executed/canceled) via
GET /portfolio/orders with no status filter; fetchClosedOrders fetches all and
keeps the executed+canceled ones client-side (kalshi's status filter takes a
single value, so "closed" — which spans executed and canceled — can't be one
query). Both reuse the existing parsePredictionOrders path + outcome filter.
has flags set.
Live-verified on kalshi demo in JS and Python (identical): fetchOrders=6
(1 executed + 5 canceled), fetchOpenOrders=0, fetchClosedOrders=6, every closed
row status in {closed,canceled}, every open row status open. Offline
request/response green JS/C#/Go.
* feat(kalshi): editOrder via cancel + recreate
kalshi's live amend endpoint is deprecated (V1 /portfolio/orders/{id}/amend
returns 410 Gone, no V2 replacement), so editOrder cancels the resting order
via the V2 DELETE path then places a fresh order with the updated terms — the
same pattern myriad.editOrder uses. has flag set.
Live-verified on kalshi demo in JS and Python: create (0.02 x1) -> edit
(0.03 x2) returns a new order id, old order no longer open, cleanup canceled.
Offline request/response green JS/C#/Go.
* fix(polymarket): honor the unified postOnly param
buildClobOrderBody omitted postOnly from the passthrough params but hardcoded
postOnly:false in the signed order body, so a unified postOnly:true was silently
dropped. Read params.postOnly and set it on the order body + carry it in the
request echo; parseOrder now reflects it. Live-verified in JS and Python: a
far-from-market postOnly buy rests as maker and the returned order shows
postOnly=true, then cancels. Offline request/response green JS/C#/Go.
* feat(limitless): redeem a resolved position by conditionId
redeem(outcome, params) POSTs the market's CTF conditionId to /portfolio/redeem
(gasless — the operator settles on-chain). The conditionId resolves from the
outcome's market (now carried in the per-outcome info) or params.conditionId
directly; ArgumentsRequired if neither is available. has flag set.
Live-verified in JS and Python: the request is accepted by the real endpoint
(returns "market not resolved" for an unresolved market — I have no winning
position to fully redeem, but the request path + conditionId resolution + guard
are confirmed). Static request fixture locks the body across JS/C#/Go; offline
request/response green.
* fix(limitless): map unmatched 400 to BadRequest not ExchangeNotAvailable
An unmatched 400 (bad params, or a business rule like "market not resolved")
returned undefined from handleErrors and fell through to the base default,
surfacing as ExchangeNotAvailable — so a caller would retry as if the exchange
were down. Throw BadRequest for a bare 400 after the specific exact/broad
matches (which still win). Live-verified: the redeem "market not resolved"
response now throws BadRequest. Offline request/response green JS/C#/Go.
* fix(kalshi): map unmatched 400 to BadRequest not ExchangeNotAvailable
An unmapped kalshi error code on a 400 (e.g. invalid_order) fell through
handleErrors to the base default and surfaced as ExchangeNotAvailable, so a
caller would retry as if the exchange were down. Throw BadRequest for a bare
400 after the specific code matches (which still win). Live-verified on demo:
an invalid-price createOrder now throws BadRequest (was ExchangeNotAvailable).
Offline request/response green JS/C#/Go.
* docs(examples): kalshi end-to-end example + fix spain-world-cup type error
Adds prediction-kalshi-end-to-end.ts — the only prediction venue without an
end-to-end example. It demonstrates the full surface completed this session:
fetchEvents scoped by tag + resolution winners, an outcome's ticker/order book,
a create -> editOrder -> cancel round-trip, and fetchOrders / fetchMyTrades /
fetchSettlements. Defaults to the kalshi demo (KALSHI_SANDBOX=false for prod);
run-verified live end-to-end on demo.
Also fixes a pre-existing type error in prediction-polymarket-spain-world-cup.ts
(title/market not on MarketInterface) that failed tsBuildExamples — cast to any.
* chore(go): gofmt the regenerated prediction + base Go files
The goTranspiler runs this session emitted un-gofmt'd Go (spaces, stray
semicolons) that was committed as-is — CI runs `go fmt` and would flag it.
gofmt the 14 Go files in this branch's diff. No content change (the FilterByKey
/ SafeValue-for-prediction deltas in the main base are correct syncs from
Exchange.ts, which the committed Go base had gone stale against). Full Go build
(v4 + pro + prediction) passes.
* fix(prediction): address review findings (Go winner determinism + 3 more)
From a cross-language review of this session's changes:
- limitless: the resolved-market winner was `i === winningOutcomeIndex` where i
iterates Object.keys(tokens) — Go/Java randomize map iteration, so ~50% of the
time the wrong outcome was flagged winner (JS/Python are insertion-ordered so
it passed live). Map the leg to its canonical index by label (yes=0, no=1)
instead of loop position. Now deterministic across all languages.
- polymarket: a market that is closed-for-trading but not yet UMA-resolved has
fractional outcomePrices; the code reported settleFraction = the mid (e.g. 0.5)
as if final. Only set winner/settleFraction when a decisive price exists
(>=0.99 or <=0.01); leave undefined otherwise.
- kalshi editOrder: validate price + amount BEFORE cancelling the old order, so a
bad edit (kalshi is limit-only) doesn't leave the user with the order cancelled
and an exception instead of a replacement.
- C# PredictionSettlement.timestamp: Int64? not double?, matching every sibling
struct + Go (*int64) / Python (Int) / TS (Int).
Verified: limitless (winningOutcomeIndex=1 -> NO winner) + polymarket (closed-
unsettled -> no winner/settleFraction) via mocks; live re-check confirms Trump-
2024 (prices 1/0) still winner=true/settleFraction=1 and myriad categorical
intact; kalshi editOrder throws before cancel on missing price. Full offline
matrix green JS/C#/Go; C#/Go build; gofmt clean.
* fix(kalshi): map FOK to fill_or_kill + killed-FOK to OrderNotFillable
Review follow-up. kalshi has a native fill_or_kill primitive (distinct from
immediate_or_cancel), confirmed live — so collapsing unified FOK into IOC was
wrong (FOK is all-or-nothing; IOC allows a partial fill). Map timeInForce 'FOK'
-> 'fill_or_kill' and 'IOC' -> 'immediate_or_cancel' separately. A killed FOK
returns 409 fill_or_kill_insufficient_resting_volume (a normal order outcome)
which fell through to ExchangeNotAvailable; map it to OrderNotFillable, matching
the polymarket FAK/FOK fix.
Live-verified on demo: unified timeInForce:'FOK' now submits fill_or_kill and a
non-matchable FOK throws OrderNotFillable (was ExchangeNotAvailable). Offline
request/response green JS/C#/Go.
* fix(kalshi): map the invalid_order error code to InvalidOrder
Review follow-up: the invalid_order code (bad price/size, verified live) fell to
the generic 400->BadRequest fallback; map it explicitly to InvalidOrder so
callers get the precise class. The bare-400->BadRequest fallback now only
catches genuinely-unclassified client errors. Offline green JS/C#/Go.
* fix(prediction): make the resolution fields compile in Java + wire Java support
Running the full Java build (a CI target) caught a real cross-language bug the
JS/C#/Go offline tests could not: the resolution-field locals (winner /
settleFraction / resolvedOutcome) are reassigned `let`s captured in the outcome
and market object literals, which Java emits as anonymous inner classes that
cannot capture a reassigned local ("must be final or effectively final") —
PolymarketCore.java failed to compile. Copy each to an effectively-final const
before the literal, across kalshi/polymarket/myriad/limitless. Behaviour-
preserving (verified via mocks + the offline matrix); transpiles harmlessly to
every language.
Java support: add PredictionSettlement.java (the native type, like Go/C#) +
register it in the Java wrapper generator's known-types. The full Java build
(lib + tests module) now compiles. Note: fetchSettlements/redeem are prediction-
specific methods that don't fit Java's shared-wrapper model (built from base
Exchange.ts), so their typed wrappers aren't generated — the impls live in
*Core.java and CI compile is green.
Re-transpiled + verified all 6 languages: offline request/response matrix green
JS/C#/Go, C#/Go build, Py/PHP syntax, Java assemble (BUILD SUCCESSFUL), gofmt
clean.
* fix(java): generate typed wrappers for prediction-only methods
Close the Java wrapper gap flagged in review. generateJavaWrappers built the
prediction method list only from base Exchange.ts, so prediction-only methods
had no typed wrapper (impl was in *Core.java, callable only dynamically):
- fetchSettlements lives on PredictionExchange.ts — parse the prediction base
and add its methods that aren't already on Exchange.ts to the shared list.
Every prediction Core extends PredictionExchange so super.fetchSettlements()
resolves; all 5 venues now get a typed List<PredictionSettlement> wrapper.
- redeem is limitless-specific and returns a plain dict — add a per-exchange
method map so only Limitless.java gets it (Object return); genReturnExpr /
genAsyncReturnExpr now handle Object (identity, not `new Object(res)`).
Verified: Kalshi.fetchSettlements() -> List<PredictionSettlement>,
Limitless.redeem() -> Object present only on limitless; full Java build
(assemble) SUCCESSFUL; zero change to the ~100 non-prediction wrappers.
* move static data to events/
* chore: regenerate base + prediction after merging carlos + upstream
tsBuild + base (--baseClass) + prediction transpile across all langs on the
merged source. Small delta (js/go/cs base+prediction) — Py/PHP/Java auto-merged
identically. Go builds; JS prediction static tests green (polymarket 12/7,
myriad 8/6, limitless 5/4, kalshi 5/7).
* fix(kalshi): initialize positionSide so Python fetchPositions doesn't UnboundLocalError
Live testing found kalshi fetchPositions threw UnboundLocalError in Python when
a position has no 'position' field (yesContracts undefined): positionSide was
declared 'let positionSide: Str;' and only assigned inside the if, so Python
referenced an unassigned local. Initialize to undefined. Also fixes the
malformed .gitignore line that failed to ignore the go/main build binary.
* fix(prediction): cross-language transpiler correctness for kalshi/myriad
Bugs surfaced by live-testing the prediction exchanges in all six languages
(public + private endpoints):
- kalshi.parseTrade: initialize `side` to undefined — a trade whose taker_side
is neither "yes" nor "no" left it unassigned, raising UnboundLocalError in
Python (same class as the earlier positionSide fix).
- kalshi.fetchEvents: hoist rawEvents.length into a named local at the three
`>= limit` comparison sites so the PHP regex transpiler emits count() instead
of strlen() — fetchEvents was a fatal TypeError in PHP. Same hoist applied to
queries/tags in base requireEventQuery.
- base slugToMarketSymbol/slugToOutcomeSymbol/shortenSlug: accept a nullable
(Str) eventSlug. The bodies already collapse an absent event to just the
market part, but the strict `string` parameter made PHP throw on null,
breaking every myriad market load (fetchEvents/fetchBalance/fetchPositions).
- myriad.fetchPositions: derive the owner from the configured privateKey via
walletAddressOrUndefined() (matching fetchBalance) so a privateKey-only
config resolves the wallet for both methods.
Verified public + private live in JS, Python, PHP, C#, Go and Java; all five
compiled targets build clean.
* fix(kalshi): skip unresolvable events in fetchEvents({query}) loop
The series search can rank an event_ticker whose /events/{ticker} endpoint
404s (a series-only ticker, or one absent on the demo host). A single 404
threw BadSymbol and failed the entire fetchEvents call; catch it and skip that
event instead. Repro on demo: fetchEvents({query:'BTC'}) hit KXBTCHALF ->
not_found -> whole call died. Now returns the resolvable events (45).
* refactor(base): BaseExchange tier — PredictionExchange independent of Exchange
Extract all shared infrastructure into a new BaseExchange base class. Exchange
becomes a thin concrete subclass (regular venues extend it, unchanged), and
PredictionExchange extends BaseExchange as an independent sibling — a prediction
instance is no longer an Exchange (instanceof/embed check is false in every
language) while still reusing every base helper via BaseExchange.
Across all 6 languages:
- TS: class BaseExchange + thin `Exchange extends BaseExchange`; PredictionExchange extends BaseExchange
- Python: async base -> BaseExchange + thin Exchange; prediction extends BaseExchange
- PHP: async base -> BaseExchange + thin Exchange; prediction extends \ccxt\async\BaseExchange
- Go: `type BaseExchange struct` (receivers retargeted) + thin `Exchange struct { BaseExchange }`; PredictionExchange embeds BaseExchange
- C#: Exchange partials -> BaseExchange + thin `Exchange : BaseExchange`; PredictionExchange : BaseExchange
- Java: Exchange.java -> BaseExchange + thin `Exchange extends BaseExchange`; PredictionExchange extends BaseExchange
Transpilers updated to emit the 3-tier hierarchy and recognize BaseExchange as a
base class (fixes a TS-type leak from name-keyed type stripping).
Verified: all 6 langs build clean; prediction is not-an-Exchange while regular
exchanges and prediction both work live (public + private).
Follow-up: generated static-test harness is still Exchange-typed; prediction
static tests need the harness handle widened to BaseExchange (live/CLI work).
* refactor(base): fine split + standalone Prediction* types (TS)
Move the 62 symbol-based trading methods (fetch/create/cancel/edit/watch* + the
convenience wrappers) out of BaseExchange into the thin Exchange tier, so
PredictionExchange (extends BaseExchange) no longer inherits them and defines
its own outcome-based versions as fresh methods (not overrides).
Make the Prediction* structure types standalone (PredictionOrder/Trade/Ticker/
Position/OrderBook/TradingFee/OpenInterest no longer `extends` the base types;
outcome/outcomeId identity, no symbol). This is what required the fine split:
a standalone PredictionOrder[] can't covariantly override a base Order[] return.
Prediction venues' parsers renamed parse{Order,Ticker,Trade,Position,OpenInterest}
-> parsePrediction* so they no longer override the base parsers (which stay in
BaseExchange returning base types, used by safeOrder's fill parsing).
TS: tsBuild + lint clean; runtime verified — prediction independent, returns
standalone types (no symbol), private + regular exchanges work.
* refactor(base): propagate fine split + standalone types to Python & PHP
Python/PHP are dynamically typed (no return-covariance constraint), so recombine
the 62 trading methods (now in the TS `Exchange extends BaseExchange` class) back
into the transpiled base: transpileBaseMethods now also extracts the Exchange
class's methods and appends them to the base method set. Prediction still shadows
them with its own outcome-typed versions.
- exclude loadOrderBook from the recombine (hand-written above the marker in the WS
async bases; it uses WS cache primitives the REST path can't transpile)
- add a general `new X (` -> `X (` strip for Python (no `new` keyword)
- import Market into the Python prediction base skeleton (…