FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(polymarket): add Polymarket perps public REST API by rayBastard · Pull Request #30268 · ccxt/ccxt · GitHub

Repository navigation

feat(polymarket): add Polymarket perps public REST API - #30268

Open
rayBastard wants to merge 16 commits into
ccxt:masterfrom
rayBastard:feat/polymarket-perps
Open

rayBastard wants to merge 16 commits into
ccxt:masterfrom
rayBastard:feat/polymarket-perps

Conversation

rayBastard commented Sep 4, 2026 •
edited
Loading

Copy link
Copy Markdown
Member

New top-level exchange for api.perpetuals.polymarket.com (the prediction CLOB stays in ccxt.prediction). Public market data: markets, tickers, orderbook, trades, OHLCV, funding, open interest, fees + static fixtures.

The scoped java lane runs transpileJavaSingle without build/generateJavaWrappers.ts, so a brand-new exchange with a ws class can't compile (pro/PolymarketCore extends exchanges.Polymarket - the typed wrapper only comes from the full transpileJava). First new ws exchange since the java lane landed.

New top-level exchange for api.perpetuals.polymarket.com (the prediction
CLOB stays in ccxt.prediction). Public market data: markets, tickers,
orderbook, trades, OHLCV, funding, open interest, fees + static fixtures.
rayBastard self-assigned this Sep 4, 2026

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Summary

New top-level polymarket REST exchange for api.perpetuals.polymarket.com, public market data only, with static request/response fixtures. Checked out e4fc951 and built it in every transpiled lane — the port itself is in good shape (details at the bottom).

ts/src/abstract/polymarket.ts being in the diff is correct here, not a stray generated file: a brand-new exchange has to carry it or emitAPI cannot bootstrap. Same file set as the merged revolutx PR.

One blocking correctness bug, proven live against the venue.

fetchFundingRate / fetchOpenInterest return another market's data

Both methods send instrument_id to GET /v1/info/tickers and then take response[0]. That endpoint ignores the filter and always returns the full instrument list:

GET /v1/info/tickers?instrument_id=6  ->  67 rows, first row instrument_id=1
GET /v1/info/tickers?symbol=BTC-USD   ->  67 rows, first row instrument_id=1
GET /v1/info/tickers                  ->  67 rows, first row instrument_id=1

So response[0] is always instrument 1 (SP500-USD). Running the built branch against the live API:

requested symbol: BTC/USD:PUSD | market id: 6
fetchFundingRate  -> symbol: SP500/USD:PUSD | fundingRate: 0.00000625
fetchOpenInterest -> symbol: SP500/USD:PUSD | amount: 1101.59344
fetchTicker       -> symbol: BTC/USD:PUSD   | last: 79800       <- correct
fetchFundingRates[BTC/USD:PUSD] -> 0.0000125                    <- correct

fetchFundingRate ('BTC/USD:PUSD') reports the S&P 500 funding rate (0.00000625) instead of BTC's (0.0000125). It fails silently: parseFundingRate receives market, so safeSymbol stamps the requested symbol onto the wrong instrument's numbers — the caller has no way to notice.

/info/bbo, /info/book, /info/trades and /info/funding all do honour instrument_id; only /info/tickers and /info/statistics ignore it. fetchTicker is unaffected because it filters by symbol after the merge, and the plural fetchFundingRates / fetchOpenInterests are correct since they parse the whole list.

Fix shape: select the row whose instrument_id equals market['id'] instead of index 0 (or reuse the plural parse and index by symbol).

Tests

The fetchFundingRate and fetchOpenInterest response fixtures each contain a single-row httpResponse, so index 0 is coincidentally the right row and the static suites stay green while the live path is wrong. A multi-row fixture where the requested instrument is not first would pin this.

Verification at this head

Lane Result
npm run tsBuild clean
eslint ts/src/polymarket.ts clean
Python transpile + ast.parse (sync + async) OK
PHP transpile + php -l (sync + async) OK
go build ./v4 + go vet exit 0
dotnet build cs/ccxt/ccxt.csproj Build succeeded, 0 errors
static request/response (JS) 24 / 17 passed
static request/response (Python sync) 24 / 17 passed

The polymarket id now exists in both the top-level and prediction lists. That resolves cleanly — Go gets ccxt vs ccxtprediction packages, C# gets ccxt vs ccxt.prediction, and both build — but note build/transpile.ts polymarket routes to ts/src/prediction/ until npm run export-exchanges has run, which is worth knowing when transpiling this one by hand.

Merge gate: 🔴
Merge probability: 94%

Comment thread ts/src/polymarket.ts Outdated
Comment on lines +794 to +812
const response = await this.publicGetInfoTickers (this.extend (request, params));
//
// [
// {
// "instrument_id": 1,
// "symbol": "SP500-USD",
// "index_price": "7713.1",
// "mark_price": "7715",
// "last_price": "7714",
// "mid_price": "7715",
// "open_interest": "1101.82729",
// "funding_rate": "0.00000625",
// "next_funding": 1788548400000,
// "timestamp": 1788546972857
// }
// ]
//
const first = this.safeDict (response, 0, {});
return this.parseFundingRate (first, market);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

GET /v1/info/tickers ignores instrument_id — it returns all 67 instruments, so response[0] is always instrument 1 (SP500-USD). Verified live: fetchFundingRate ('BTC/USD:PUSD') returns 0.00000625 (SP500) instead of BTC's 0.0000125, stamped with the requested symbol so it fails silently. Select the row whose instrument_id matches market['id'] instead of index 0.

Comment thread ts/src/polymarket.ts Outdated
Comment on lines +950 to +952
const response = await this.publicGetInfoTickers (this.extend (request, params));
const first = this.safeDict (response, 0, {});
return this.parseOpenInterest (first, market);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Same /v1/info/tickers filter problem as fetchFundingRate: the endpoint returns every instrument, so response[0] is always SP500-USD. Verified live — fetchOpenInterest ('BTC/USD:PUSD') reports 1101.59344, which is SP500's open interest. Match on market['id'] rather than taking the first row.

watchOrderBook/watchTicker(s)/watchBidsAsks/watchTrades/watchOHLCV with
unWatch support over wss://ws.perpetuals.polymarket.com. Book frames are
full snapshots; acks are matched by request id. Static ws fixtures included.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 5fcbac3 — adds the WS layer (ts/src/pro/polymarket.ts, +734), registers it in ts/ccxt.ts, flips 'pro': false -> true, and ships ts/src/test/static/ws/polymarket.json (+1069) with frames for watchOrderBook, watchTicker, watchTickers, watchBidsAsks, watchOHLCV and both trade shapes.

The WS code itself reads well: request ids + req:<id> pending map matched in handleAck, ack status: err rejects the awaited hashes and clears them so a later call can resend, market['id'] on every channel and safeMarket on every parse, typed safe* throughout, no || coalescing, no reserved locals. wsTimeframes correctly excludes 1s, and handleOHLCV tolerating an empty data array matches the venue.

Two things on this commit:

  1. watchTicker's description advertises open interest and funding rate, but handleTicker does not carry oi, fr or mid out of data — they survive only inside info. Either map them or reword the JSDoc.
  2. unWatchOrderBook / unWatchTicker / unWatchTrades / unWatchOHLCV and the handleAck unsub cleanup (cache deletion + cleanUnsubscription) have no fixture in ts/src/test/static/ws/polymarket.json — that branch, including the per-topic delete this.ohlcvs[symbol][timeframe], is currently unexercised offline.

The REST fetchFundingRate / fetchOpenInterest response[0] bug reported on e4fc951 is untouched by this commit (ts/src/polymarket.ts changed only the pro flag), so the gate does not move.

Merge gate: 🔴

Comment thread ts/src/pro/polymarket.ts
Comment on lines +294 to +303
const ticker = this.safeTicker ({
'symbol': symbol,
'timestamp': timestamp,
'datetime': this.iso8601 (timestamp),
'close': this.safeString (data, 'last'),
'last': this.safeString (data, 'last'),
'markPrice': this.safeString (data, 'mark'),
'indexPrice': this.safeString (data, 'idx'),
'info': data,
}, market);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

watchTicker is documented as carrying "the open interest and the funding rate", but this safeTicker call only maps last, mark and idx — oi, fr and mid from the frame are dropped and reachable only via info. Either surface them (e.g. 'markPrice'/'indexPrice' plus an explicit funding/OI path) or drop that clause from the @description on line 193.

Comment thread ts/src/pro/polymarket.ts
Comment on lines +673 to +701
if (action === 'unsub') {
const topic = this.safeString (pending, 'topic');
const symbol = this.safeString (pending, 'symbol');
if (symbol !== undefined) {
if (topic === 'orderbook') {
if (symbol in this.orderbooks) {
delete this.orderbooks[symbol];
}
} else if (topic === 'trades') {
if (symbol in this.trades) {
delete this.trades[symbol];
}
} else if (topic === 'ticker') {
if (symbol in this.tickers) {
delete this.tickers[symbol];
}
} else if (topic === 'ohlcv') {
const timeframe = this.safeString (pending, 'timeframe');
if ((timeframe !== undefined) && (symbol in this.ohlcvs)) {
if (timeframe in this.ohlcvs[symbol]) {
delete this.ohlcvs[symbol][timeframe];
}
}
}
}
for (let i = 0; i < messageHashes.length; i++) {
this.cleanUnsubscription (client, subMessageHashes[i], messageHashes[i]);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This unsub branch — the per-topic cache deletion and cleanUnsubscription — has no entry in ts/src/test/static/ws/polymarket.json; the fixture only covers the four watch* methods. A canned unsub ack frame per topic (especially ohlcv, which indexes this.ohlcvs[symbol][timeframe]) would pin this offline.

…tickers payload

The tickers and statistics endpoints ignore the instrument_id filter, so
fetchFundingRate/fetchOpenInterest returned the first instrument's data.
Single-market methods now reuse the bulk fetch and select by symbol;
fixtures are multi-row with the target not first, plus unWatch ws fixtures.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 902069a — this commit fixes the blocking bug reported on e4fc951.

fetchFundingRate / fetchOpenInterest / fetchTicker no longer send the ignored instrument_id filter and index response[0]; they now delegate to the plural fetchTickers / fetchFundingRates / fetchOpenInterests and select the requested symbol out of the full payload, throwing BadSymbol when the row is absent instead of silently returning another instrument's numbers. That is the right shape — the plural parsers were already correct, so there is one parse path rather than two.

The fixtures now actually pin it: the fetchTicker and fetchFundingRate response entries carry multi-row httpResponse payloads where the requested instrument is deliberately not first (BTC-USD requested with SP500-USD at index 0), so a regression back to response[0] would fail the static suite rather than pass by coincidence. The request fixtures were updated to drop the ?instrument_id= query accordingly.

The two open points from 5fcbac3 are both addressed: watchTicker's JSDoc no longer claims open interest and funding rate are mapped onto the unified ticker (it now says they stay on the raw frame under info), and ts/src/test/static/ws/polymarket.json gains unWatchOrderBook / unWatchTicker / unWatchTrades / unWatchOHLCV entries, the last of which exercises the per-timeframe cache-deletion branch in the unsub ack path.

Nothing new introduced by this commit: no || coalescing, typed safe* throughout, market id still on the wire, 1e-06 / 1e-05 / 6.25e-06 in the response fixtures are just the canonical JSON serialisation of the same numbers.

Merge gate: 🟢

Self-provisioned proxy sessions (EIP-712 CreateProxy), msgpack+EIP-712
signed operations, orders/cancels/modify, leverage and margin management,
account reads and fixture-only withdraw. Also fixes the hand-written base
randomBytes helpers: python was non-CSPRNG with odd-length hex, csharp
returned base64 from a time-seeded RNG - both now CSPRNG lowercase hex.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 8569911 — adds the private trading layer (+1773 on ts/src/polymarket.ts, +1057/+1679 request/response fixtures), self-provisioned EIP-712 proxy sessions, msgpack-hashed signed ops, and two hand-written base randomBytes fixes.

The session design reads well: loadProxyCredentials is awaited before every private/trade call so sign() always sees a live proxy secret, the compact positional signable row is built once and mirrored by the structured op body, market['id'] goes on the wire everywhere, typed safe* throughout, Precise for the position maths, and the handleErrors array-unwrap for batch endpoints plus the unmapped-400 permanent classification are both right.

The base randomBytes fixes are the important part of this commit

Before this commit the Python base built the proxy key from random.getrandbits() — a Mersenne Twister, not a CSPRNG — and additionally dropped leading zero nibbles, so random_bytes(32) returned an odd-length hex string roughly one time in sixteen. The C# base seeded new Random() from the clock and returned base64 rather than hex. Both are now os.urandom(...).hex() / RandomNumberGenerator lowercase hex, which matches TS (crypto.getRandomValues + binaryToBase16), PHP (bin2hex(random_bytes())), Go (crypto/rand + hex.EncodeToString) and Java (secureRandom + bytesToHex). Both edits sit above the transpile markers, so they are hand-written base and will survive the build. Worth noting in the PR body that this also hardens coinbase's randomBytes(16) nonce, which is the only other caller.

params never reaches the signed envelope on the createOrder path

createOrder calls this.createOrders ([ order ]) with no second argument, so createOrders signs with params = {}. Its own params is the only thing forwarded to signPerpsOp, which is where exp is read. Anything a caller passes to createOrder beyond what createOrderRequest consumes is silently discarded — createOrderRequest even returns the leftover params, and createOrders drops that key on the floor.

The fixture named create an order with a command expiry in the envelope demonstrates this rather than guarding it: the input passes { "exp": 1788546570789 } and the expected output contains only op / sig / salt / ts, with no exp field. So the static suite currently pins the dropped-parameter behaviour under a description that claims the opposite. Forwarding the per-order leftovers (or params) into signPerpsOp and adding exp to the expected body would make that fixture assert what its description says.

Minor

parsePosition writes Precise.stringGt (size, '0') ? 'long' : 'short' with an unbracketed condition; house style is (cond) ? a : b everywhere else in this file.

Nothing else new from this commit: no || coalescing on dict access, no reserved locals, no .includes, no arrow .map/.filter, + only concatenates, JSDoc is complete on the new public methods and @ignore is set on createOrderRequest / loadProxyCredentials / signPerpsOp / compactSignable. withdraw being fixture-only and never exercised live is the right call for a first pass.

Merge gate: 🔴

Comment thread ts/src/polymarket.ts Outdated
const orders = await this.createOrders ([ order as OrderRequest ]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

createOrders is called without params, so its signPerpsOp (signable, bodyOp, params) always signs with {} and the exp command expiry a caller passed to createOrder never reaches the envelope. createOrderRequest already returns the unconsumed params, but createOrders ignores that key — forward it (or pass params through here) so the envelope-level fields survive the single-order path.

Comment on lines +613 to +642
"description": "create an order with a command expiry in the envelope",
"method": "createOrder",
"url": "https://api.perpetuals.polymarket.com/v1/trade/orders",
"input": [
"BTC/USD:PUSD",
"limit",
"buy",
0.001,
79000,
{
"exp": 1788546570789
}
],
"output": {
"op": {
"type": "createOrders",
"args": [
{
"iid": 6,
"buy": true,
"po": false,
"qty": "0.001",
"tif": "gtc",
"p": "79000"
}
]
},
"sig": "0xa7c586d4e12d0cf1a74a77bc9bae7cc29ff474db025e0592cb3fd15f06cce1e26dc103ae4b79ced48d64d093c62cca5b114226dacfc563101896304109bc4b4d1c",
"salt": 877282580,
"ts": 1788564987038

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This case is described as create an order with a command expiry in the envelope, but the expected body has only op / sig / salt / ts — no exp. As written it pins the parameter being dropped rather than guarding the feature. Once createOrder forwards params, add "exp": 1788546570789 to the expected output so a regression back to the drop fails here.

Comment thread ts/src/polymarket.ts Outdated
side = Precise.stringGt (size, '0') ? 'long' : 'short';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Bracket the ternary condition: side = (Precise.stringGt (size, '0')) ? 'long' : 'short'; — house style brackets every ternary condition, and the rest of this file already does ((isCross === true) ? 'cross' : 'isolated' right below).

…ature

Reverts the base randomBytes edits out of this PR - the session key now
derives from an ecdsa signature under the wallet key (paradex idiom), so
no base helper changes are needed. Also fixes the cancelAll abstract type.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 93dc093 — reverts the two hand-written base randomBytes edits out of the PR and derives the session key from a wallet signature instead, plus a cancelAll abstract/return type fix.

The new derivation is sound, and it is a better fit for this PR than touching the base helper. proxyPrivateKey = keccak(signMessage('polymarket-perps-proxy:<ms>:<randNumber(9)>', privateKey)) is unpredictable to anyone without the wallet key regardless of how weak the nonce RNG is: signHash is deterministic, so the nonce only has to be unique, not secret, and milliseconds() + 9 digits gives that. The paradex/derive idiom is the right precedent, and it means the PR no longer needs to change python/ccxt/base/exchange.py or cs/ccxt/base/Exchange.cs at all — narrower diff, nothing for the base to regress.

One thing to flag, not a blocker for this PR: the revert restores the pre-existing base weakness (random.getrandbits() in Python — Mersenne Twister, and odd-length hex when the top nibble is zero; new Random() + base64 in C#, which does not even match the TS hex contract). polymarket no longer depends on it, but coinbase's randomBytes (16) nonce still does. That belongs in its own PR now — correct call to unbundle it.

cancelAllOrders returning Promise<Order[]> with the abstract tradeDeleteTradeOrdersAll narrowed to Dict matches the actual { "status": "ok" } payload and the [ this.safeOrder ({ 'info': response }) ] wrap — the previous List / Promise<any> pair was the inconsistent one.

Nothing new introduced by this commit: no || coalescing, typed safe*, string concat only, no reserved locals.

The createOrder finding from 8569911 is untouched here — createOrder still calls this.createOrders ([ order ]) with no params, so exp never reaches signPerpsOp, and the create an order with a command expiry in the envelope fixture still expects a body with no exp field. Gate does not move.

Merge gate: 🔴

Comment thread ts/src/polymarket.ts
Comment on lines +2560 to +2565
// the session key derives from an ecdsa signature under the wallet
// key over a timestamped nonce - unpredictable without the wallet
// secret and unique per provisioning, the paradex stark-key idiom
const nonceMessage = 'polymarket-perps-proxy:' + this.milliseconds ().toString () + ':' + this.randNumber (9).toString ();
const seedSignature = this.signMessage (nonceMessage, this.privateKey);
const proxyPrivateKey = this.hash (this.encode (seedSignature), keccak, 'hex');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Derivation is fine — the signature is deterministic, so the nonce only needs uniqueness, which milliseconds() + randNumber (9) provides, and the result is unguessable without the wallet key. Worth a short note in the PR body that this removes the base randomBytes dependency entirely, since the earlier commit changed the Python/C# base and this one no longer needs to.

…ate-types

createOrder now forwards params so the documented exp key reaches the
signed request, the fixture asserts it lands. setLeverage takes int per
the base signature, the position-side ternary is hoisted for the transpiler.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 8bb6ff1 — this commit closes the createOrder finding from 8569911 and fixes two type/transpile nits.

createOrder now calls this.createOrders ([ order as OrderRequest ], params), so the top-level params reaches signPerpsOp and the optional exp lands on the envelope next to sig / salt / ts instead of being dropped. The request fixture create an order with a command expiry in the envelope now expects "exp": 1788546570789 in the body, so it asserts what its description claims rather than pinning the old dropped-parameter behaviour — that was the exact gap I raised. Passing params twice (once as the per-order params, once as the batch params) is harmless here: createOrderRequest never reads exp, and it is deliberately outside the msgpack-hashed signable row, so the signature is unaffected. Both JSDoc blocks document params.exp.

setLeverage (leverage: int, ...) matches the base signature in ts/src/base/Exchange.ts and every other venue — the previous Int was the outlier and would have failed validate-types. Hoisting Precise.stringGt (size, '0') into isLong before the ternary is the house-style/transpiler-safe shape.

One residual, non-blocking: with a genuine multi-order createOrders call, exp is only honoured at the batch level — a per-order params.exp inside an orders[i] entry is still discarded by createOrderRequest. That is a reasonable contract given the envelope carries a single expiry for the whole signed command, and the JSDoc now says so.

Nothing new introduced by this commit: no || coalescing, typed safe*, market['id'] on the wire, no reserved locals.

The blocker from 8569911 is resolved and nothing outstanding remains from the earlier passes, so the gate flips.

Merge gate: 🟢

The venue rejects orders whose price or quantity exceeds five significant
figures on top of the per-instrument decimals - found live, the static
fixtures cannot pin it. Round price, truncate amount, then apply the tick.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on d9f4a3b — adds priceToPrecision / amountToPrecision overrides that clamp to five significant figures before applying the tick, and swaps this.safeString (market, 'symbol') for this.safeSymbol (undefined, market) in createOrderRequest.

The two-stage shape is right and matches the bitfinex precedent (significant figures first, venue step second), and SIGNIFICANT_DIGITS / ROUND / TRUNCATE are correctly imported from ./base/functions/number.js. safeSymbol (undefined, market) is the better call than reading the key off the market dict.

Both overrides drop the base zero-guard, so a sub-tick order signs "0" on the wire

Exchange.priceToPrecision / amountToPrecision in ts/src/base/Exchange.ts (~7409 / ~7421) each end with:

if (result === '0') {
    throw new InvalidOrder (this.id + ' amount of ' + market['symbol'] + ' must be greater than minimum amount precision of ' + ...);
}

Neither override reproduces it. With the markets in ts/src/test/static/markets/polymarket.json (BTC/USD:PUSD amount tick 1e-5, price tick 0.1; XRP/USD:PUSD amount tick 1, price tick 1e-6), running the built helpers:

amountToPrecision ('BTC/USD:PUSD', 0.000004) -> '0'
amountToPrecision ('XRP/USD:PUSD', 0.5)      -> '0'
priceToPrecision  ('XRP/USD:PUSD', 0.0000004) -> '0'

Those strings go straight into createOrderRequest's qty / p and into the positional signable row, so a too-small order is msgpack-hashed, signed and POSTed as a zero-quantity command instead of raising InvalidOrder locally the way every other exchange does. editOrder has the same path via market['symbol']. Re-adding the result === '0' throw at the end of both overrides restores the base contract.

The overrides also drop the undefined early-return

The base methods start with if (price === undefined) { return undefined; }. decimalToPrecision (undefined, ROUND, 5, SIGNIFICANT_DIGITS) throws a raw TypeError (Cannot read properties of undefined (reading '0')) rather than an ccxt error. Today's callers guard it (createOrderRequest only calls priceToPrecision when price !== undefined, editOrder requires both args), so this is latent rather than live — but these are public unified helpers and anyone calling exchange.priceToPrecision (symbol, undefined) gets a stack trace instead of undefined.

Note on the rounding direction

A hard 5-significant-figure ROUND coarsens prices whose integer part already exceeds five digits: 105432.5 becomes 105430 and 105436 becomes 105440, i.e. an effective step of 10 on a market whose tick is 0.1. hyperliquid avoids exactly that with Math.max (5, integerPart.length). If the venue really caps at five figures regardless of magnitude this is correct as written, but it is worth a comment line saying so since the behaviour is surprising next to market['precision']['price'].

Nothing else new from this commit: no || coalescing, market['id'] still on the wire, no reserved locals, comment lines stay inside the 2-line house limit.

The createOrder / exp fix from 8bb6ff1 is untouched here, but this commit introduces a new regression on the order path, so the gate moves back.

Merge gate: 🔴

Comment thread ts/src/polymarket.ts
Comment on lines +2717 to +2723
override priceToPrecision (symbol: Str, price: any): Str {
// the venue caps prices at five significant figures on top of the
// per-instrument decimal precision
const market = this.market (symbol);
const significant = this.decimalToPrecision (price, ROUND, 5, SIGNIFICANT_DIGITS);
return this.decimalToPrecision (significant, ROUND, market['precision']['price'], TICK_SIZE);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This override drops both guards the base priceToPrecision has: the price === undefined early-return (so decimalToPrecision throws a raw TypeError instead of returning undefined) and the result === '0' -> InvalidOrder throw. With XRP/USD:PUSD (price tick 1e-6), priceToPrecision ('XRP/USD:PUSD', 0.0000004) returns '0', which then gets signed into the msgpack signable row as the order price. Re-add both guards before returning.

Comment thread ts/src/polymarket.ts
Comment on lines +2725 to +2730
override amountToPrecision (symbol: Str, amount: any): Str {
// quantities carry the same five significant figures cap
const market = this.market (symbol);
const significant = this.decimalToPrecision (amount, TRUNCATE, 5, SIGNIFICANT_DIGITS);
return this.decimalToPrecision (significant, TRUNCATE, market['precision']['amount'], TICK_SIZE);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Same missing zero-guard on the amount side: amountToPrecision ('BTC/USD:PUSD', 0.000004) and amountToPrecision ('XRP/USD:PUSD', 0.5) both return '0' against the fixture markets (amount ticks 1e-5 and 1). createOrderRequest puts that straight into qty and the positional signable row, so a sub-tick order is signed and sent as zero quantity rather than raising InvalidOrder locally like the base method does.

The precision overrides re-add the base zero-quantity throw and the
undefined early-return. Instruments parse active - the venue lists
tradable ones only. Hoists two extend literals for the java emitter and
disables the unWatch ws fixtures for csharp, whose base never resolves
the unsubscribe future.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 60c26ea — this commit closes the precision regression reported on d9f4a3b.

priceToPrecision / amountToPrecision now reproduce both halves of the base contract: the if (price === undefined) { return undefined; } early-return and the trailing if (result === '0') { throw new InvalidOrder (...) } guard, with the same message shape and numberToString (market['precision'][...]) as ts/src/base/Exchange.ts (~7409 / ~7421). So the sub-tick cases I ran before — amountToPrecision ('BTC/USD:PUSD', 0.000004), amountToPrecision ('XRP/USD:PUSD', 0.5), priceToPrecision ('XRP/USD:PUSD', 0.0000004) — now raise locally instead of signing a "0" quantity into the msgpack signable row, and editOrder inherits the fix through the same helpers. The five-significant-figures coarsening is now documented in the comment as deliberate for any magnitude, which was the other half of that note.

'active': undefined -> true checks out against the venue: GET /v1/info/instruments returns 67 rows and carries no per-instrument status field at all — no active / status / trading key, and every ui_live_time is already in the past — so there is nothing per-market to derive a tri-state from, and the exchange-wide halt really does only surface through fetchStatus's cancel_only (already mapped to OnMaintenance via cancel_only_mode). The markets/response fixture flips to true accordingly; the 0.00001 -> 1e-05, 0.000001 -> 1e-06 and 0.00000625 -> 6.25e-06 fixture edits are the same numbers re-serialised, not value changes.

The two extend hoists in ts/src/pro/polymarket.ts are the right shape — an object literal as the first extend argument is the java-emitter foot-gun, and naming emptyRequest / basePending first keeps the behaviour identical.

The four disabledCS markers on the unWatch* ws fixtures are accurate rather than a convenience skip: cleanUnsubscription in cs/ccxt/base/Exchange.BaseMethods.cs:7735 ends with // client.resolve(true, unsubHash); commented out, while the TS source resolves at Exchange.ts:9119, so the C# unsubscribe future genuinely never completes. The string form is the documented convention (binance, bitget, coinbase ws fixtures do the same) and tests.ts gates on safeString (result, 'disabledCS') for the ws path, so these entries stay live in JS/PY/PHP/GO/JAVA and only skip C#. Worth re-checking once the base port catches up.

Nothing new introduced by this commit: no || coalescing, typed safe*, market['id'] still on the wire, no reserved locals, comments within the house limit.

Nothing outstanding remains from the earlier passes and this commit removes the only blocker, so the gate returns to green.

Merge gate: 🟢

watchOrders, watchMyTrades, watchBalance and watchPositions over the
authenticated connection. The portfolio channel pushes one snapshot at
subscribe time and then only on changes, so balance and positions hydrate
over rest first, the bybit idiom. Verified live including a real fill.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 802f9d7 — adds the private WS layer (watchOrders, watchMyTrades, watchBalance, watchPositions, +441 on ts/src/pro/polymarket.ts), a post-reply auth handshake matched back through the req:<id> pending map, and +491 of fixtures covering all four new channels.

The shape is right. authenticate awaits loadProxyCredentials before every private subscribe, registers the pending entry under req:<id> exactly like the existing sub/unsub path, and resolves the authenticated future in place so later calls reuse it while rejecting and clearing the subscription on status != ok so a retry can resend. The portfolio snapshot idiom (setPositionsCache / loadPositionsSnapshot / setBalanceCache / loadBalanceSnapshot + fetchPositionsSnapshot / awaitPositionsSnapshot options) matches bybit line for line, which is the correct precedent for a channel that pushes one snapshot at subscribe time and then only deltas. handlePortfolio feeding both positions and balance off the single portfolio frame is right, and both fixtures assert the two-frame auth -> sub sentMessages sequence, so the handshake is pinned offline.

The new checkHashes split makes the ack-failure cleanup delete the wrong key

This is the one thing this commit introduces that I would fix before merge.

Until this commit every caller passed the same array as messageHashes and subMessageHashes, and sendSubscription checked messageHashes against client.subscriptions. The four private methods break that symmetry deliberately — watchOrders awaits orders:<symbol> but subscribes under the shared hash orders, and watchPositions / watchBalance await positions / balance while both subscribe under portfolio. The new checkHashes argument correctly makes the send decision use the shared hash.

The error branch of handleAck was not updated to match:

for (let i = 0; i < messageHashes.length; i++) {
    client.reject (error, messageHashes[i]);
    const messageHash = messageHashes[i];
    if (messageHash in client.subscriptions) {
        delete client.subscriptions[messageHash];
    }
}

Base watch registers client.subscriptions[subscribeHash], i.e. 'orders' / 'fills' / 'portfolio' — not 'orders:BTC/USD:PUSD' / 'positions' / 'balance'. So on a status: err ack the reject correctly unblocks the caller, but the delete targets a key that was never set and the shared subscription hash survives. The next watchPositions call then finds 'portfolio' in hashesToCheck, gets the empty request back from sendSubscription, and base watch also sees subscriptions['portfolio'] already set — so nothing is ever sent again and the caller hangs on a channel that never subscribed. Same for orders and fills. The comment above that loop ("clearing it lets a later call resend") describes the pre-commit invariant, which no longer holds for these four methods.

Deleting the subMessageHashes[i] entries (the keys base watch actually wrote) instead of, or in addition to, messageHashes[i] restores it. The unsub branch below already indexes subMessageHashes[i] for cleanUnsubscription, so the data is there. None of the four new fixtures exercise an err ack, which is why this stays green offline.

this.myTrades is sized from tradesLimit

handleMyTrades reads this.options['tradesLimit'] to size the myTrades cache, so the private fill history and the public trade history share one knob. The convention elsewhere in ts/src/pro is myTradesLimit for this.myTrades (coinbaseexchange, coinbaseinternational, and the rest), which lets a caller keep a short public tape and a long fill history.

Smaller

handleMyTrades normalises data to a list before parsing, but handleOrder assumes a bare object (safeDict (message, 'data', {})). Both channels ride the same connection and the venue already batches on fills; if orders ever coalesces two updates into an array the frame is silently dropped. The array-tolerant shape you already wrote for fills would cover it for free.

Nothing else new from this commit: typed safe* throughout, no || coalescing, market['id'] on the wire and safeMarket on every parse, no reserved locals, handleMessage routing extended rather than restructured, and the has flags flip to true only for the four methods actually implemented.

The exp fix from 8bb6ff1 and the precision guards from 60c26ea are untouched here, but this commit adds a new stuck-subscription path, so the gate moves back.

Merge gate: 🔴

Comment thread ts/src/pro/polymarket.ts
// subscribed yet, so a pending entry is registered under the same
// condition - otherwise no ack would ever arrive to consume it, the
// private channels register a shared hash next to narrowed ones
const hashesToCheck = (checkHashes === undefined) ? messageHashes : checkHashes;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

hashesToCheck correctly decouples the send decision from the awaited hashes, but handleAck's status: err branch still deletes client.subscriptions[messageHashes[i]]. Base watch registers the subscribe hash (orders / fills / portfolio), so after a failed ack the shared key survives and sendSubscription returns the empty request forever — the channel can never resubscribe. Delete the subMessageHashes[i] keys there instead.

Comment thread ts/src/pro/polymarket.ts
Comment on lines +692 to +695
const subscribeHashes = [ 'orders' ];
const request = this.sendSubscription ('sub', [ 'orders' ], [ messageHash ], subscribeHashes, {}, params, subscribeHashes);
const url = this.urls['api']['ws'];
const orders = await this.watch (url, messageHash, request, 'orders');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This is the first caller where the awaited hash (orders:<symbol>) differs from the registered subscription hash (orders). Worth a fixture with a { "status": "err" } ack on one of the private channels — it would catch the cleanup mismatch above, which the four new entries currently cannot.

Comment thread ts/src/pro/polymarket.ts Outdated
return;
}
if (this.myTrades === undefined) {
const limit = this.safeInteger (this.options, 'tradesLimit', 1000);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

this.myTrades is sized from options['tradesLimit'], which is the public tape's knob — the rest of ts/src/pro uses myTradesLimit for the private fill cache so the two can be tuned independently.

The live market validator resolves base, quote and settle through
exchange.currency(). Currencies now build from instruments plus assets -
all bases, the usd quote and the collateral with its metadata, which also
lives under options.collateral for the withdraw and margin paths.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on eb88529 — rebuilds fetchCurrencies off /info/instruments + /info/assets in parallel so every market base, the USD quote and the pUSD collateral all resolve through exchange.currency(), moves the collateral token metadata into options['collateral'], and repoints withdraw / the margin path at it.

The motivation is right: parseMarket builds base + '/' + quote + ':' + settle from base_asset, the symbol split and quote_asset, so the live market validator was previously asking currency() for BTC and USD when only PUSD existed. Covering all three sources fixes that, and safeCurrencyStructure + parsePrecision (quantity_decimals) is the right shape for the instrument-derived rows. Dropping the hand-written limits block is also correct — safeCurrencyStructure fills it.

withdraw no longer validates that code is the collateral

Before this commit the token address came from currency['info']['address'], which only existed on the row parsed from /info/assets — so withdraw ('BTC', ...) produced an undefined token and failed loudly. Now tokenAddress / decimals come from options['collateral'] unconditionally, while currency is still resolved from code but only used for parseTransaction at the end. Since this commit makes BTC, ETH, USD etc. all valid currency codes, withdraw ('BTC', 5, address) now signs and POSTs a pUSD withdraw struct while reporting it as BTC. A code-vs-collateral check (compare against options['collateral']['asset'], throw BadRequest/NotSupported otherwise) restores the guard the old lookup gave for free.

The new fixtures do not exercise the merged result

Both new fetchCurrencies response entries feed a single httpResponse to both parallel requests, and the descriptions say so explicitly ("the collateral loop skips these rows" / "the instrument loop skips these rows"). So each case only ever proves one of the two loops in isolation — the actual production output, where instrument bases and the collateral land in the same dict, is never asserted. Neither is the de-dupe (if (!(code in result))) across 67 instruments sharing a base, nor the ordering fact that the assets loop overwrites PUSD after the instrument loop may have written it. Worth noting the request fixture for fetchCurrencies was deleted rather than updated to two entries, so the request side of the new /info/instruments call is now unpinned too.

Smaller

The USD row is written unconditionally with 'info': undefined and 'type': 'fiat', while the instrument-derived rows get 'type': undefined. Given quote_asset is always pUSD and the USD code only comes from splitting the venue symbol, a short comment on why the synthetic fiat row exists would help — as written it looks like a currency the API returned.

Nothing new introduced by this commit on the transpile side: no || coalescing, typed safe*, Promise.all over an explicit array (the transpiler-safe form), no reserved locals, comments within the house limit.

The checkHashes / subMessageHashes ack-failure finding from 802f9d7 is untouched here — handleAck still deletes messageHashes[i] from client.subscriptions while base watch registered the shared sub hash — so that blocker stands and the gate does not move.

Merge gate: 🔴

Comment thread ts/src/polymarket.ts
Comment on lines +2303 to +2309
const collateral = this.safeDict (this.options, 'collateral', {});
const tokenAddress = this.safeString (collateral, 'address');
const decimals = this.safeString (collateral, 'decimals', '6');
const precision = this.parsePrecision (decimals);
// the signed amount is denominated using raw base units of the token
const precised = this.decimalToPrecision (this.numberToString (amount), TRUNCATE, this.parseNumber (precision), TICK_SIZE);
const baseUnits = Precise.stringDiv (precised, precision);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

tokenAddress / decimals now come from options['collateral'] regardless of code, but code is no longer constrained to the collateral asset — this same commit makes BTC, ETH, USD valid currency codes. withdraw ('BTC', 5, address) will sign a pUSD Withdraw struct and report it as BTC. Add a check that code resolves to options['collateral']['asset'] and throw otherwise.

Comment thread ts/src/polymarket.ts
Comment on lines +408 to +414
const promises = [
this.publicGetInfoInstruments (params),
this.publicGetInfoAssets (params),
];
const responses = await Promise.all (promises);
const instruments = responses[0];
const assets = responses[1];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Both new fetchCurrencies response fixtures feed one httpResponse to both parallel calls, so each only exercises a single loop while the other skips every row. The merged output — instrument bases plus the collateral in one dict — is never asserted, and neither is the base de-dupe across instruments sharing a base. A fixture with distinct instruments/assets payloads would pin the real result.

Comment thread ts/src/polymarket.ts
Comment on lines +439 to +452
// every instrument quotes using usd
result['USD'] = this.safeCurrencyStructure ({
'id': 'USD',
'code': 'USD',
'name': 'USD',
'info': undefined,
'active': undefined,
'deposit': false,
'withdraw': false,
'fee': undefined,
'precision': this.parseNumber ('0.01'),
'type': 'fiat',
'networks': {},
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This USD row is synthetic — quote_asset is always pUSD and USD only appears from splitting the venue symbol. It is written unconditionally with 'info': undefined and 'type': 'fiat', while the instrument rows above get 'type': undefined. Worth a line saying it is derived rather than returned by the API, and consider giving the instrument bases 'type': 'crypto' for consistency.

A rejected subscribe now clears both the awaited hash and the shared hash
the base watch registered, so a retry resends instead of hanging. The
fills cache sizes from myTradesLimit and order frames tolerate batching.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 7b38d3d — this commit closes the ack-failure finding from 802f9d7.

handleAck now reads method before the error branch and, when the pending op was a sub, also deletes subMessageHashes[i] from client.subscriptions — the shared hash base watch actually registered (orders / fills / portfolio). So a rejected private subscribe no longer leaves the shared key behind, and the next watchPositions / watchOrders / watchMyTrades call resends instead of hanging on a channel that was never subscribed. Gating the extra delete on action === 'sub' is the right narrowing: the unsub branch must keep the live subscription intact, which the rewritten comment now states correctly.

The other two points from that pass are also addressed: this.myTrades is sized from options['myTradesLimit'] rather than tradesLimit, matching coinbaseexchange and friends, so the public tape and the fill history no longer share one knob; and handleOrder normalises data to a list, dedupes the touched symbols into a Dict and resolves orders once plus orders:<symbol> per symbol, so a batched orders frame is no longer silently dropped.

Nothing new introduced by this commit on the transpile side: Array.isArray + for loop rather than arrow sugar, Object.keys over a Dict (the transpile-safe shape), typed safe*, no || coalescing, no reserved locals, no blank lines inside the method bodies.

Still no fixture drives an err ack, so this recovery path remains unexercised offline — same note as before, not a blocker.

The only blocker outstanding is removed and nothing from the earlier passes remains open, so the gate returns to green.

Merge gate: 🟢

Comment thread ts/src/pro/polymarket.ts
Comment on lines +725 to +735
const data = this.safeValue (message, 'data');
let rows = [];
if (Array.isArray (data)) {
rows = data;
} else {
rows.push (data);
}
const rowsLength = rows.length;
if (rowsLength === 0) {
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Minor: switching to safeValue means a frame with no data key yields undefined, which then goes through rows.push (data) — so rows.length is 1, the rowsLength === 0 guard never fires, and safeDict (rows, i, {}) hands parseOrder ({}) an empty dict that gets appended to the cache. handleMyTrades avoids this because its list comes from safeList. Either keep safeDict (message, 'data') for the non-array branch and skip when it is empty, or only push when data !== undefined.

The live currency validator requires a defined type and boolean
deposit/withdraw. Base assets are crypto, the usd quote is fiat and the
pUSD collateral is depositable and withdrawable. Verified against
test.currency for all 69 live currencies.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on a84b7e9 — gives the synthesized fetchCurrencies rows a defined type and boolean deposit / withdraw so test.currency stops rejecting them, and re-serialises the affected fixture numbers.

The motivation is right and the shape matches the validator: ts/src/test/Exchange/base/test.currency.ts asserts type against [ 'fiat', 'crypto', 'leveraged', 'other', undefined ] and, when fetchCurrencies is native, requires deposit / withdraw to be non-empty unless depositForNonCrypto / withdrawForNonCrypto are skipped. Making the collateral row deposit: true / withdraw: true is correct — pUSD is the only asset /v1/info/assets returns and the only thing withdraw can move. The 1e-05 -> 0.00001 / 6.25e-06 -> 0.00000625 edits in the response fixture are the same numbers re-serialised, not value changes.

'type': 'crypto' is wrong for 43 of the 67 instrument bases

The instrument loop stamps 'type': 'crypto' on every base_asset, but /v1/info/instruments carries a category field and most of the venue is not crypto:

GET /v1/info/instruments  ->  67 rows
  equity     36   SPCX, MU, SKHYNIX, AAPL, MSFT, GOOGL, AMZN, ...
  crypto     24   BTC, ETH, SOL, HYPE, PUMP, ZEC, XRP, ...
  commodity   4   GOLD, WTIOIL, SILVER, BRENTOIL
  index       3   SP500, NAS100, DRAM

So AAPL, GOLD, SP500 and 40 others are now published as type: 'crypto' through exchange.currencies, and ts/src/test/static/currencies/polymarket.json pins that: 68 of the 69 entries are "type": "crypto". The previous undefined was at least honest; this commit replaces it with a positive false claim, and it is the one field a caller would use to tell a synthetic equity leg from a real token.

The enum has the value for this — 'other' (ts/src/extended.ts:695 uses it for exactly this kind of non-token row). Deriving it from category is a two-line map: crypto -> 'crypto', everything else -> 'other'. Note that also changes the validator path — currencyType !== 'crypto' is what lets deposit / withdraw be skipped for non-crypto rows — so getting it right is not purely cosmetic.

active: true on the instrument rows

Same reasoning as 60c26ea for markets: /v1/info/instruments has no per-instrument status field, so there is nothing to derive a tri-state from and true is the only defensible value. No objection, just noting it is the same assumption as before, now applied to currencies too.

Nothing new introduced by this commit on the transpile side: no || coalescing, typed safe*, no reserved locals, no blank lines inside the method body, parsePrecision on the string decimals rather than float maths.

The exp fix from 8bb6ff1, the precision guards from 60c26ea and the handleAck fix from 7b38d3d are untouched here, but this commit ships wrong currency metadata for the majority of the venue and freezes it into the fixture, so the gate moves back.

Merge gate: 🔴

Comment thread ts/src/polymarket.ts Outdated
Comment on lines +429 to +434
'active': true,
'deposit': false,
'withdraw': false,
'fee': undefined,
'precision': this.parseNumber (this.parsePrecision (this.safeString (instrument, 'quantity_decimals'))),
'type': 'crypto',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

'type': 'crypto' is hardcoded for every instrument base, but only 24 of the 67 rows are crypto — /v1/info/instruments returns category as one of crypto / equity / commodity / index, so AAPL, GOLD and SP500 get published as crypto currencies. Derive it: category === 'crypto' -> 'crypto', otherwise 'other' (the enum value ts/src/extended.ts:695 uses for the same situation). This also feeds the currencyType !== 'crypto' branch in test.currency.ts that governs whether deposit / withdraw may be empty.

"numericId": null,
"code": "SP500",
"precision": 0.00001,
"type": "crypto",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This fixture now pins the mislabel — 68 of the 69 entries are "type": "crypto", including SP500, GOLD and the 36 equity bases. Once the type is derived from the instrument category, these non-crypto rows should regenerate as "other", which is what would keep a regression back to a blanket 'crypto' from passing silently.

BTC and ETH are perpetual-contract underlyings on the perps venue, not
depositable or withdrawable assets - collateral is pUSD only - so their
deposit/withdraw flags are false by design. Skip the fetchCurrencies
major-currency gate, same as aftermath.

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 87943d8 — skip-tests.json only: adds polymarket.skipMethods.fetchCurrencies.activeMajorCurrencies plus whitespace re-serialisation of a few neighbouring entries.

The skip is the right mechanism and the reasoning holds. ts/src/test/Exchange/test.fetchCurrencies.ts:28 gates the requiredActiveCurrencies assert on 'activeMajorCurrencies' in skippedProperties, and the assert it disables is exactly the one that would fire here: BTC / ETH exist as fetchCurrencies rows only because they are base_asset values on perpetual instruments, and /v1/info/assets returns pUSD as the sole movable asset, so deposit / withdraw are legitimately false on those rows. aftermath carries the same key for the same structural reason (BTC not bridgeable on a Sui DEX), so the precedent cited in the commit message is accurate. The narrow per-method form is also correct — it does not touch activeCurrenciesQuota or amountOfCurrencies, so the rest of the fetchCurrencies suite still runs.

Nothing new introduced by this commit: no source change, no fixture value change, and the reformatted okxus / probit / hyperliquid entries are byte-equivalent JSON.

The 'type': 'crypto' finding from a84b7e9 is untouched here. That commit stamps crypto on every instrument base_asset, but /v1/info/instruments reports 36 equity, 4 commodity and 3 index rows against 24 crypto, and ts/src/test/static/currencies/polymarket.json freezes 68 of 69 entries as "type": "crypto". This commit suppresses a different assert (the major-currency deposit/withdraw gate), so it does not address that — and note the two interact: test.currency only relaxes the deposit/withdraw requirement for rows whose type is not crypto, so deriving type from category (crypto -> 'crypto', everything else -> 'other') would additionally make most of these rows stop needing the skip at all. The blocker stands and the gate does not move.

Merge gate: 🔴

Comment thread skip-tests.json
Comment on lines +2040 to +2042
"fetchCurrencies": {
"activeMajorCurrencies": "perps collateral is pUSD only; BTC/ETH are perpetual-contract underlyings, not depositable or withdrawable assets, so their deposit/withdraw flags are false by design"
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This correctly disables the requiredActiveCurrencies assert in test.fetchCurrencies.ts:28, but it is the second-choice fix for most of the affected rows. test.currency only demands non-empty deposit/withdraw when the row's type is crypto; since 43 of the 67 instrument bases are equity / commodity / index per the category field on /v1/info/instruments, mapping those to 'other' in fetchCurrencies would both fix the wrong metadata flagged on a84b7e9 and shrink what this skip has to cover to just genuine crypto underlyings like BTC/ETH.

The venue classifies each underlying as equity, commodity, index or
crypto - only the crypto ones map to the crypto currency type, the rest
map to other. A response fixture pins both branches (an index underlying
maps to other, a crypto one stays crypto).

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 345db96 — this commit closes the currency-type finding from a84b7e9.

fetchCurrencies now reads category off each instrument and maps crypto -> 'crypto', everything else -> 'other', which is exactly the split the venue reports: of the 67 rows on GET /v1/info/instruments, 24 are crypto and 43 are equity (36), commodity (4) or index (3). ts/src/test/static/currencies/polymarket.json flips precisely those 43 entries from "type": "crypto" to "type": "other" and leaves the 24 crypto bases plus PUSD/USD untouched, so the fixture now matches the venue instead of freezing a false claim. 'other' is a valid test.currency value and the two-line derivation sits inside the existing if (!(code in result)) guard, so the de-dupe ordering is unchanged.

The response fixture pins both branches rather than just the happy one: the fetchCurrencies case now feeds an index underlying (SP500) ahead of the crypto one (BTC) and expects "type": "other" / "type": "crypto" respectively, so a regression back to the unconditional 'crypto' would fail the static suite. The description was updated to say so.

Worth noting the knock-on: test.currency only relaxes the deposit/withdraw requirement for rows whose type is not crypto, so those 43 rows no longer need the relaxation from 87943d8 at all — the activeMajorCurrencies skip is now carrying only the 24 genuine crypto bases (BTC, ETH, …), which is the narrow, defensible use of it.

Nothing new introduced by this commit: typed safe*, a hoisted ternary rather than one inline in a call argument, no || coalescing, no reserved locals, comment within the house limit.

The exp fix from 8bb6ff1, the precision guards from 60c26ea and the handleAck fix from 7b38d3d are untouched, and the only blocker outstanding is removed, so the gate returns to green.

Merge gate: 🟢

rayBastard marked this pull request as ready for review September 7, 2026 22:42
carlotestor added the ws WebSocket (aka ccxt.pro) label Sep 12, 2026

carlotestor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Follow-up on 0a7f79d — Merge branch 'ccxt:master' into feat/polymarket-perps. No authored change: the delta is a merge that brings the branch up to f4cf1be3, and the PR's own file list is unchanged (10 files, all polymarket-local except ts/ccxt.ts and skip-tests.json).

Checked the two shared files against master at this head rather than against the previous PR head, since a merge can silently revert work that landed meanwhile:

  • skip-tests.json — diffing the merge base against master is empty, so everything merged into master since 345db96 (including the #30377 batch entries from 2026-09-11) is present. The only content the PR adds is still the single polymarket.skipMethods.fetchCurrencies.activeMajorCurrencies entry; nothing from another exchange was dropped or rolled back.
  • ts/ccxt.ts — additions only (polymarket, polymarketPro imports, the two registry rows and the export), no lines removed, so no other exchange's registration was clobbered by the merge.

Nothing else moved and no earlier finding was reopened: fetchCurrencies still derives type from category, the five-significant-figure clamps and their result === '0' InvalidOrder guards are intact in priceToPrecision / amountToPrecision (ts/src/polymarket.ts:2769-2792), sendSubscription's hashesToCheck decoupling is unchanged, and myTradesLimit is still the knob for the private tape (ts/src/pro/polymarket.ts:828). No prior ask is outstanding.

mergeable: true — the blocked state is the branch-protection/CI gate, not a conflict.

Merge gate: 🟢

This branch has not been deployed

No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new exchange ws WebSocket (aka ccxt.pro)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants


Back | FazBrowse Home | New Git URL