Тема
Polymarket Data Model — Ground Truth Reference
Status: living reference. Last fact-gathering: 2026-05-13 (§1–§7); 2026-07-29 (§8 — контракт отправки заявки). Sources: see §11 "Provenance". Audience: LLM agents and humans extending pm-engine / pm-sdk. No prescriptive recommendations — facts only.
This document is the synthesis of four parallel audit agents (docs+on-chain, code mapping, production probe over 40 500 markets, strategy consumption). Every claim is anchored to a file:line reference inside this repository, a permanent GitHub source link, a docs.polymarket.com URL, or a reproducible probe command. Statements without provenance are listed in §10 "UNRESOLVED".
Table of contents
- Entity hierarchy
- Neg-risk semantics
- Production cardinality (proven facts, 2026-05-13)
- Code mapping
- Assumptions matrix — verified vs unchecked
- Known gaps
- Edge cases & invariants
- Контракт отправки заявки (CLOB) — ответы, отказы, резерв баланса, задержка, идентификатор
- Glossary
- UNRESOLVED items & inter-report contradictions
- Provenance
- Change log
1. Entity hierarchy
Polymarket exposes three logical layers off-chain (Gamma API) that anchor onto two on-chain contracts (Conditional Tokens Framework + NegRiskAdapter).
1.1 Event
An Event is the Gamma-level container that groups one or more Markets. Off-chain only — there is no on-chain Event contract. Distinguishing fields observed on the wire (event JSON, top-level keys from .tmp/gamma-samples/event-neg-risk-augmented.json):
id, slug, title, ticker, description, startDate, endDate, createdAt, updatedAt,
active, closed, archived, restricted, automaticallyActive, deploying, pendingDeployment,
new, featured, competitive, cyom, showAllOutcomes, showMarketImages, requiresTranslation,
commentCount, openInterest, liquidity, liquidityClob, cumulativeMarkets,
volume, volume24hr, volume1wk, volume1mo, volume1yr,
icon, image, eventMetadata, resolutionSource, series, seriesSlug,
tags: [{id, label, slug, ...}],
enableNegRisk, negRisk, negRiskAugmented,
markets: [ ... ] // embedded array of child marketsThe event carries the authoritative neg-risk flags: enableNegRisk: bool, negRisk: bool|null, negRiskAugmented: bool. See §2.2 for the off-chain ↔ on-chain mapping of those flags and §3.4 for distribution numbers.
Sampled production singleton event:
{
"slug": "cap-on-gambling-loss-deductions-repealed-before-2027",
"negRisk": null, "negRiskAugmented": false, "enableNegRisk": false, "markets_count": 1
}Source: .tmp/gamma-samples/event-singleton-binary.json.
Sampled production neg-risk event (139 child markets):
{
"slug": "nhl-2025-26-hart-memorial-trophy",
"negRisk": true, "negRiskAugmented": true, "enableNegRisk": true, "markets_count": 139
}Source: .tmp/gamma-samples/event-neg-risk-large.json.
1.2 Market
A Market is the fundamental tradable unit. Per Polymarket docs:
"A market is the fundamental tradable unit on Polymarket. Each market represents a single binary question with Yes/No outcomes." — https://docs.polymarket.com/concepts/markets-events.md
Every Polymarket market is binary at the contract level: outcomeSlotCount = 2. See https://docs.polymarket.com/trading/ctf/overview.md ("outcomeSlotCount: 2 for all binary markets"). Confirmed by the production probe: 40 500/40 500 active markets sampled have exactly 2 tokens and 2 outcomes (§3.1).
Key wire-level fields (from Gamma /markets response, observed via probe):
| Field | Type | Role |
|---|---|---|
conditionId | 0x-prefixed 32-byte hex | Primary on-chain link to CTF bytes32 conditionId. |
questionID | hex string | On-chain question identifier; input to prepareCondition. |
question | string | Human-readable question text. |
slug | string | URL-safe identifier. |
clobTokenIds | JSON array of decimal strings (2 entries) | ERC1155 token IDs on CTF (or wcol on NegRiskAdapter). [0] = YES, [1] = NO (see §1.3, §2). |
outcomes | JSON array of strings (2 entries) | Display labels — see §3.2 for production distribution. |
outcomePrices | JSON array of decimal strings (2 entries) | Last-known prices, indexed parallel to outcomes. |
negRisk | bool | True if this market is under NegRiskAdapter. |
negRiskAugmented | bool or null | Always null on per-market endpoint; meaningful only on the event. See §2.2 + §3.5. |
enableOrderBook | bool | Order book trading enabled. |
acceptingOrders | bool | Order acceptance toggle. |
active, closed, archived, resolved | bool | Lifecycle flags. |
orderPriceMinTickSize | 0.01 or 0.001 | Production-relevant tick variability — see §3.6. |
minimumOrderSize, volume, volume24hr, liquidity | numbers | Activity metrics. |
endDate | RFC3339 timestamp string | Market expiry. |
gameStartTime | %Y-%m-%d %H:%M:%S%z string | Не только у погоды — заполнено широко у спорта/киберспорта; живой обход Gamma /events/keyset 15.09.2026 (полный курсорный обход, теги 1/64/105907) дал 121 237 из 141 510 активных рынков спорта/киберспорта с непустым значением. Значение не всегда согласовано с endDate: наблюдён случай gameStartTime позже endDate примерно на 2 месяца (isp-ddy-ddy-2026-03-13) — см. §6.8. |
updatedAt | RFC3339 | Server-side last-update timestamp. |
Sampled production neg-risk market (from the 139-market NHL event):
{
"conditionId": "0x7976b8dbacf9077eb1453a62bcefd6ab2df199acd28aad276ff0d920d6992892",
"negRisk": true, "negRiskAugmented": null,
"clobTokenIds": "[\"4394372887385518214471608448209527405727552777602031099972143344338178308080\",\"112680630004798425069810935278212000865453267506345451433803052322987302357330\"]",
"outcomes": "[\"Yes\", \"No\"]",
"orderPriceMinTickSize": 0.001
}Source: .tmp/gamma-samples/event-neg-risk-augmented.json (first child market).
1.3 Token
A Token is an on-chain ERC1155 position. Two tokens per market: index 0 (YES) and index 1 (NO). The clobTokenIds[i] decimal string is the ERC1155 tokenId on either:
- the
ConditionalTokenscontract (standard markets —negRisk = false), or - the
NegRiskAdaptercontract (neg-risk markets —negRisk = true).
From https://docs.polymarket.com/quickstart.md:
"Save a token ID from
clobTokenIds— you'll need it to place an order. The first ID is the Yes token, the second is the No token."
The outcomes array shares the same indexing — outcomes[0] and clobTokenIds[0] describe the same outcome. See §5 for the verification status of this convention (no machine-readable invariant; quickstart prose only).
1.4 On-chain ground truth for resolution
A market settles when the resolution oracle calls reportPayouts on the Conditional Tokens contract. Holders then redeem winning tokens via:
CTF.redeemPositions(IERC20 collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint[] calldata indexSets)— standard markets.NegRiskAdapter.redeemPositions(bytes32 _conditionId, uint256[] calldata _amounts)— neg-risk markets;_amountsmust have length 2:[yesAmount, noAmount].
Sources:
- CTF: https://github.com/Polymarket/conditional-tokens-contracts/blob/a927b5a52cf9ace712bf1b5fe1d92bf76399e692/contracts/ConditionalTokens.sol#L218-L255
- NegRiskAdapter: https://github.com/Polymarket/neg-risk-ctf-adapter/blob/f78b35b0863b4308a431ca307d06f49b2ea65e78/src/NegRiskAdapter.sol#L218-L241
Payout vector for Polymarket binary markets: denominator = 1, vector ∈ {[1,0], [0,1]}. Source: Agent A §3 "CTF.redeemPositions" / crates/pm-sdk/src/onchain/redeem.rs:44.
1.5 Off-chain (Gamma) ↔ on-chain (CTF / NegRiskAdapter) mapping
| Off-chain field | On-chain artifact | Notes |
|---|---|---|
Market.conditionId | bytes32 conditionId parameter to all CTF / NegRiskAdapter ops | Identical 32-byte value. |
Market.clobTokenIds[i] | ERC1155 tokenId (uint256) | Decimal-encoded uint256; YES = i=0, NO = i=1. |
Market.negRisk == true | NegRiskAdapter (NEG_RISK_ADAPTER_ADDRESS) | All split/merge/redeem routed through adapter rather than CTF directly. |
Market.negRisk == false | ConditionalTokens (0x4D97DCd97eC945f40cF65F87097ACe5EA0476045) | Standard CTF path. |
Event.negRisk == true ∧ event.negRiskAugmented == true | Same NegRiskAdapter.sol contract | Augmentation is off-chain only — no separate on-chain contract found (§2.5). |
CTF mainnet address from https://docs.polymarket.com/resources/contracts.md and crates/pm-sdk/src/onchain/ctf.rs:40-76. NegRiskAdapter address referenced as NEG_RISK_ADAPTER_ADDRESS constant in crates/pm-sdk/src/onchain/ctf.rs:173-218.
2. Neg-risk semantics
2.1 Mandatory ≥2 markets per neg-risk event
Single binary events cannot be neg-risk at the contract level. The NegRiskAdapter.convertPositions function reverts when questionCount ≤ 1:
solidity
uint256 questionCount = md.questionCount();
if (md.oracle() == address(0)) revert MarketNotPrepared();
if (questionCount <= 1) revert NoConvertiblePositions();Docs reinforce this: "Negative risk is a mechanism for multi-outcome events where only one outcome can win." — https://docs.polymarket.com/advanced/neg-risk.md.
Production confirmation: among 2 500 sampled active events (§3.7), neg-risk events have an average markets_count of 16.6 vs non-neg-risk events ~2.4. Top-10 neg-risk events have 100+ child markets.
2.2 neg_risk vs neg_risk_augmented — field level placement
The flags carry different semantics and different placement:
| Flag | Placement (Gamma response) | Meaning |
|---|---|---|
enableNegRisk | event-level | The event uses NegRiskAdapter for its children. |
negRisk | event-level (canonical) + mirrored on every child market | Per-market: true if this market lives under NegRiskAdapter. |
negRiskAugmented | event-level only | Augmented mechanism (placeholder outcomes + "Other" catch-all). |
The negRiskAugmented value on the market record is always null in the /markets Gamma endpoint. Verified by probe: head -100 .tmp/gamma-samples/markets-bulk-2026-05-13.jsonl | jq -r '.negRiskAugmented // "null"' | sort | uniq -c returns 100 null. The same value appears as null even on child markets of events whose negRiskAugmented=true (e.g. the first child of 2026-fifa-world-cup-winner-595 returns "negRiskAugmented": null). To learn whether a market is augmented you MUST query the parent event.
Code consequence: pm_common::models::market::Market.neg_risk_augmented: bool (crates/pm-common/src/models/market.rs:13-83) — serde default false — is therefore filled from the event payload when present, or false when constructed from /markets. See §6.3 for the dead-field implication.
2.3 On-chain operations — CTF vs NegRiskAdapter
| Operation | CTF | NegRiskAdapter |
|---|---|---|
splitPosition | splitPosition(collateral, parentCollectionId, conditionId, partition, amount) | Long form (CTF-compatible) + short form splitPosition(bytes32 _conditionId, uint256 _amount). Wraps collateral into wcol internally. |
mergePositions | mergePositions(collateral, parentCollectionId, conditionId, partition, amount) | Long form + short form mergePositions(bytes32 _conditionId, uint256 _amount). Unwraps wcol back to collateral. |
redeemPositions | redeemPositions(collateral, parentCollectionId, conditionId, indexSets) — redeems full balance | redeemPositions(bytes32 _conditionId, uint256[] calldata _amounts) — explicit [yesAmount, noAmount]. |
convertPositions | Not available. | convertPositions(bytes32 _marketId, uint256 _indexSet, uint256 _amount) — converts NO tokens into sibling YES tokens + partial collateral. Only on NegRiskAdapter. |
prepareCondition | Required first step | Called internally via NegRiskAdapter.prepareQuestion(). |
Source: Agent A §3; CTF source https://github.com/Polymarket/conditional-tokens-contracts/blob/a927b5a52cf9ace712bf1b5fe1d92bf76399e692/contracts/ConditionalTokens.sol; NegRiskAdapter source https://github.com/Polymarket/neg-risk-ctf-adapter/blob/f78b35b0863b4308a431ca307d06f49b2ea65e78/src/NegRiskAdapter.sol#L110-L170.
Local repository implementation:
pm-sdk/src/onchain/ctf.rs:40-76—split(CTF, withparentCollectionId = B256::ZERO,BINARY_PARTITION = [1, 2]).pm-sdk/src/onchain/ctf.rs:96-133—merge_positions(CTF, standard markets).pm-sdk/src/onchain/ctf.rs:173-218—merge_positions_neg_risk(NegRiskAdapter, with pre-flightisApprovedForAllcheck + auto-approve).pm-sdk/src/onchain/redeem.rs:44-64—redeem(CTF).pm-sdk/src/onchain/redeem.rs:94-151—redeem_neg_risk(NegRiskAdapter, takes explicityes_token_id+no_token_id).
2.4 convertPositions — verbatim semantics
Function: convertPositions(bytes32 _marketId, uint256 _indexSet, uint256 _amount). Source: https://github.com/Polymarket/neg-risk-ctf-adapter/blob/f78b35b0863b4308a431ca307d06f49b2ea65e78/src/NegRiskAdapter.sol#L244-L365
Step-by-step semantics (verbatim from Agent A §2 Q-A2):
- Validate: marketId must be prepared,
questionCount > 1,_indexSetnonempty and within bounds. - Count
noPositionCount(bits set in_indexSet) andyesPositionCount = questionCount - noPositionCount. - Mint
yesPositionCount * _amountwcol internally (collateral for new YES splits). - For each question NOT in
_indexSet(sibling questions): split wcol intoYES[j]+NO[j]; accumulateYES[j]for output; accumulateNO[j]for burn. - Transfer caller's NO tokens (from
_indexSet) toNO_TOKEN_BURN_ADDRESS. - Transfer all accumulated sibling
NO[j]toNO_TOKEN_BURN_ADDRESS. - Compute fee:
feeAmount = _amount * feeBips / 10_000. - If
noPositionCount > 1: release(noPositionCount - 1) * (_amount - feeAmount)wcol (unwrapped) to caller; release fee portion to vault. - If
yesPositionCount > 0: transferyesPositionCount * (_amount - feeAmount)YES tokens to caller; transfer fee portion of YES tokens to vault.
Key invariant: the classic single-NO-to-all-YES conversion is the special case k=1, N-1 siblings → caller gets (N-1) YES tokens + 0 collateral.
Gas estimate: not in NatSpec. The function loops over questionCount siblings with an internal CTF.splitPosition call per sibling. See §10 item 3.
2.5 Augmented neg-risk — same contract, off-chain bulletin board
From https://docs.polymarket.com/advanced/neg-risk.md:
"Standard negative risk requires the complete set of outcomes to be known at market creation. But sometimes new outcomes emerge after trading begins (e.g., a new candidate enters a race). Augmented negative risk solves this with: Named outcomes (known, e.g. 'Trump', 'Harris'), Placeholder outcomes (reserved slots that can be clarified later, e.g. 'Person A'), Explicit Other (catches any outcome not explicitly named)."
"When a new outcome emerges, a placeholder is clarified via the bulletin board. The 'Other' definition narrows as placeholders are assigned."
"Only trade on named outcomes. Placeholder outcomes should be ignored until they are named or until resolution occurs."
Identification in Gamma response (event level):
json
{ "enableNegRisk": true, "negRiskAugmented": true }On-chain mechanics: no separate contract found in any Polymarket GitHub repository. The augmented logic is managed off-chain via the bulletin board mechanism; the on-chain adapter is the same NegRiskAdapter.sol. The negRiskAugmented flag is a Gamma API/UI metadata flag. The bulletin-board API itself is undocumented at a concrete endpoint — see §10 item 2.
3. Production cardinality (proven facts, 2026-05-13)
All numbers in this section come from the live probe sample recorded as .tmp/gamma-samples/markets-bulk-2026-05-13.jsonl (40 500 active markets) and .tmp/gamma-samples/events-bulk-2026-05-13.jsonl (2 500 active events). Reproduction commands in §3.8.
3.1 Token / outcome cardinality
Among the 40 500 active markets sampled:
tokens_len | count | % |
|---|---|---|
| 2 | 40 500 | 100% |
| 1 | 0 | 0% |
| 0 | 0 | 0% |
outcomes_len: 40 500 / 40 500 == 2 (100%).
Conclusion: every currently-active Polymarket market is binary (2 tokens, 2 outcomes). Multi-token markets do not exist in production.
3.2 Outcome labels — production distribution
The outcomes array always has length 2, but the label pair varies. From the 40 500-market sample:
| Labels | count | % |
|---|---|---|
["Yes", "No"] | 33 196 | 82% |
["Over", "Under"] | 3 202 | 8% |
| Team-vs-team (sports / esports pairings) | 3 116 | 8% |
["Odd", "Even"] | 925 | 2% |
["Up", "Down"] | 61 | 0.2% |
Total active markets using non-["Yes","No"] labels: 7 304 (18% of active sample). All are still binary (2-token); the variation is purely in display labels.
Slug patterns observed: eth-updown-5m-<timestamp>, nba-*-over-*, lol-<team1>-vs-<team2>.
3.3 Lifecycle flags
accepting_orders: 40 446 true, 54 null (0.1%). All sampled markets had active=true, closed=false (the Gamma /markets endpoint without a closed filter returns only active markets in offset 0–40 000).
3.4 neg_risk distribution on active markets
neg_risk | count | % |
|---|---|---|
true | 25 737 | 63.5% |
false | 14 763 | 36.5% |
3.5 neg_risk_augmented field at market level
Field is null for all 40 500 sampled active markets. Confirmed for first 100 records via jq -r '.negRiskAugmented // "null"' | sort | uniq -c → 100 null. negRiskAugmented is an event-level field only (§2.2).
Event-level breakdown over 2 500 sampled events:
negRiskAugmented (event) | count | % |
|---|---|---|
true | 1 026 | 41.0% |
false | 1 474 | 59.0% |
3.6 Tick size variability
tick_size | count | % |
|---|---|---|
0.01 | 30 257 | 74.7% |
0.001 | 10 243 | 25.3% |
Both tick sizes are first-class production values. Strategy tick rounding must handle both.
3.7 Events distribution
Among 2 500 sampled active events:
markets_count bucket | count |
|---|---|
| 1 (singleton) | 624 |
| 2 | 64 |
| 3 | 232 |
| 4 | 75 |
| 5 | 97 |
| 6–9 | 255 |
| 8 (NBA per-game week bundle spike) | 483 |
| 10–20 | 340 |
| 21–50 | 304 |
| >50 | 29 |
neg_risk (event) | count | % |
|---|---|---|
true | 1 254 | 50.2% |
false | 701 | 28.0% |
null | 545 | 21.8% |
neg_risk=null events are all singleton, non-sports (e.g. GTA VI released before June 2026). neg-risk events average markets_count = 16.6 vs ~2.4 for non-neg-risk.
Top-10 events by markets_count (all neg-risk):
| slug | markets_count |
|---|---|
| nhl-2025-26-hart-memorial-trophy | 139 |
| democratic-presidential-nominee-2028 | 128 |
| presidential-election-winner-2028 | 128 |
| republican-presidential-nominee-2028 | 128 |
| next-french-presidential-election | 128 |
| iran-leader-end-of-2026 | 123 |
| mls-2026-defender-of-the-year | 111 |
| 2026-womens-singles-roland-garros-winner | 110 |
| ncaa-football-2026-national-champion | 109 |
| 2026-mens-singles-roland-garros-winner | 103 |
3.8 Weather correction (tag 103040)
Weather events tagged 103040 are neg_risk=true. Sample highest-temperature-in-nyc-on-december-30:
{
"negRisk": true, "negRiskAugmented": false, "enableNegRisk": true,
"tag_ids": ["84","101757","102169","100091","103040","103037","104596"],
"markets_count": 7, "first_market_outcomes": "[\"Yes\", \"No\"]"
}Source: .tmp/gamma-samples/event-weather-multimarket.json. Each temperature bracket is a separate binary Yes/No market under the neg-risk event.
This corrects an earlier project assumption (referenced in Agent C §7) that weather markets were non-neg-risk; they ARE neg-risk and use bracket Yes/No children, not multi-outcome single markets.
3.9 Historical multi-token markets (2021–2022 only)
Among 5 000 sampled closed=true markets, 39 had tokens_len > 2:
| year | count |
|---|---|
| 2021 | 37 |
| 2022 | 2 |
tokens_len | count |
|---|---|
| 3 | 14 |
| 4 | 6 |
| 5 | 9 |
| 6 | 8 |
| 7 | 2 |
Examples: how-many-senators-will-vote-to-convict-donald-trump (5 buckets), which-film-will-win-best-picture-at-the-2021-oscars (5 buckets), who-will-be-the-mvp-of-super-bowl-55 (3 buckets), who-will-the-worlds-richest-person-be-on-february-27-2021 (3 buckets — outcomes: ["Jeff Bezos", "Elon Musk", "Other"]).
Conclusion: multi-token markets are a discontinued 2021–2022 artifact. No active market in 2026 has tokens_len > 2.
3.10 Sample files reference
| File | Description |
|---|---|
.tmp/gamma-samples/event-singleton-binary.json | Singleton binary event (markets=1, negRisk=null). |
.tmp/gamma-samples/event-weather-multimarket.json | Weather neg-risk event (tag 103040, markets=7). |
.tmp/gamma-samples/event-neg-risk-large.json | Large neg-risk event (nhl-2025-26-hart-memorial-trophy, markets=139). |
.tmp/gamma-samples/event-neg-risk-augmented.json | Augmented neg-risk event (2026-fifa-world-cup-winner-595, markets=60). |
.tmp/gamma-samples/market-multi-outcome-historical.json | 2021-era 3-outcome closed market (Bezos/Musk/Other). |
.tmp/gamma-samples/markets-bulk-2026-05-13.jsonl | 40 500 active markets, slim records, 11 MB. |
.tmp/gamma-samples/events-bulk-2026-05-13.jsonl | 2 500 active events, slim records, 421 KB. |
.tmp/gamma-samples/closed-multi-outcome.jsonl | 39 historical multi-outcome closed markets, 7.3 KB. |
3.11 Reproducibility
- Markets bulk:
GET https://gamma-api.polymarket.com/markets?limit=500&offset=<N>forN ∈ {0, 500, …, 40 000}. - Events bulk:
GET https://gamma-api.polymarket.com/events?limit=500&offset=<N>&active=true&closed=falseforN ∈ {0, 500, …, 2 000}. - Closed multi-outcome: add
closed=true, post-filter withjq 'select(.tokens_len > 2)'. - Rate: 0.3–0.5 s sleep between requests.
GET /markets/{id}→ 404; use?slug=or?id=query params.GET /events/{id}→ 404; use?slug=or?id=query params.GET /markets-in-events→ 404 (no such endpoint).GET /events/keyset?closed=false&limit=<N>(текущий метод полного обхода — курсорная пагинация, введена карточкойgamma-keyset-pagination2026-09-04, заменяетoffset-обход выше): размер страницы (events[]) фиксирован API на 100 независимо от запрошенногоlimit— провереноlimit=100/200/300, во всех трёх случаях вернулось ровно 100 записей и непустойnext_cursor(замер 15.09.2026). Полноту обхода даёт курсор, не увеличениеlimit; запрошенное сверх 100 источник молча урезает (то же ограничение уже учтено в рантайме —KEYSET_EFFECTIVE_LIMIT_MAX = 100вcrates/pm-engine/src/adapters/market_data/gamma.rs:67).
4. Code mapping
Историко-архивные якоря pm-bot-эры. Пути и
:NNN-строки в §4 ниже ссылаются на УДАЛЁННУЮ раскладку крейтаpm-bot(cutover 2026-07-22) и НЕ воспроизводимы на текущем дереве. Не переиндексируются построчно в pm-engine (иначе — фабрикация якорей). Структурные факты (какие поля есть, порядок parse, семантика) — crate-agnostic и остаются в силе; чтобы найти текущий код, сопоставьте на уровне каталога:
io/gamma_client.rs→crates/pm-engine/src/adapters/market_data/gamma.rsdomain/market_cache.rs→crates/pm-engine/src/adapters/market_data/application/executor.rs(merge/redeem) →crates/pm-engine/src/adapters/redemption/
crates/pm-engine/src/engine/redemption_service.rsio/onchain/mod.rs→pm-sdkon-chain ops +crates/pm-engine/src/adapters/redemption/domain/strategies/directional.rs→crates/pm-engine/src/strategies/directional/config/mod.rs/persistence/strategy_config.rs→crates/pm-engine/src/config/
- config.db (ADR-015) +
crates/pm-engine/src/strategies/directional/config.rspersistence/capture/schema.rs→crates/pm-engine/src/capture/(плейн-JSONL пара, НЕ Parquet)
4.1 pm_common::models::market::Market (28 fields)
Path: crates/pm-common/src/models/market.rs:13-83.
| Field | Type | Required | Source (Gamma → field) |
|---|---|---|---|
condition_id | String | required | Gamma conditionId. |
question_id | String (default "") | required | Gamma questionID. |
question | String (default "") | required | Gamma question. |
slug | String (default "") | required | Gamma slug. |
category | Option<String> | optional | Always None — parse_market does not populate it. |
tokens | Vec<Token> | required | Built from Gamma clobTokenIds + outcomes + outcomePrices. |
neg_risk | bool | required (default false) | Gamma negRisk. |
neg_risk_augmented | bool | serde default false | Gamma negRiskAugmented. Always false when constructed from /markets — see §3.5, §6.3. |
active | bool (default false) | required | Gamma active. |
closed | bool (default false) | required | Gamma closed. |
archived | bool (default false) | required | Gamma archived. |
resolved | bool (default false) | required | Gamma resolved. |
enable_order_book | bool (default false) | required | Gamma enableOrderBook. |
accepting_orders | bool (default false) | required | Gamma acceptingOrders. |
volume | Decimal (default 0) | required | Gamma volume. |
volume_24h | Decimal (default 0) | required | Gamma volume24hr. |
liquidity | Decimal (default 0) | required | Gamma liquidity. |
min_order_size | Decimal (default 0) | required | Gamma minimumOrderSize. |
tick_size | Decimal (default 0.01) | required | Gamma orderPriceMinTickSize else minimumTickSize else 0.01. |
end_date | Option<DateTime<Utc>> | optional | Gamma endDate (RFC3339 only). |
game_start_time | Option<DateTime<Utc>> | serde default, skip_serializing | Gamma gameStartTime (%Y-%m-%d %H:%M:%S%z); передаётся как есть для любого рынка, где Gamma его прислала — заполнено широко у спорта/киберспорта, не только у погоды (замер 15.09.2026, §1.2). Действующий парсер (crates/pm-engine/src/adapters/market_data/gamma_market_data.rs::parse_game_start_time_ms) тега не проверяет; см. §6.8 про аномалию значения. |
description | Option<String> | optional | Gamma description. |
event_id | Option<String> | optional | Event-level id. |
event_title | Option<String> | optional | Event-level title. |
event_slug | Option<String> | optional | Event-level slug. |
resolution_source | Option<String> | optional | Gamma resolutionSource. |
updated_at | DateTime<Utc> | required | Ingest wall-clock (Utc::now() inside parse_market) — NOT from Gamma. |
source_updated_at | Option<DateTime<Utc>> | serde default, optional | Gamma updatedAt. |
content_hash | u64 (default 0) | computed | XXH3-64 over CONTENT_HASH_FIELDS after construction. |
Rustdoc invariants at market.rs:85-119:
content_hashis XXH3-64 over 21 semantic fields listed inCONTENT_HASH_FIELDS.- Excluded from hash:
updated_at,description,category,event_title,resolution_source, WS runtime state (token.price,best_bid,best_ask,last_trade_*),source_updated_at,content_hashitself. game_start_timeisskip_serializing— not persisted to wire/journal.
4.2 pm_common::models::market::Token
Path: crates/pm-common/src/models/market.rs:187-231.
| Field | Type | Source |
|---|---|---|
token_id | String | Gamma clobTokenIds[i]. |
outcome | String | Gamma outcomes[i]. |
price | Option<Decimal> | Gamma outcomePrices[i]. |
best_bid | Option<Decimal> | Gamma bestBid (YES); 1 - YES_ask (NO). |
best_ask | Option<Decimal> | Gamma bestAsk (YES); 1 - YES_bid (NO). |
last_trade_price | Option<Decimal> | WS Market / RTDS / User WS (serde skip). |
last_trade_ts | Option<DateTime<Utc>> | WS Market (serde skip). |
last_trade_side | Option<String> | WS Market last_trade_price arm only (serde skip). |
last_trade_size | Option<Decimal> | WS Market last_trade_price arm only (serde skip). |
4.3 parse_market flow
Path: crates/pm-bot/src/io/gamma_client.rs:459-679 (private fn). Step-by-step from Agent B §2:
gamma_client.rs:460-471— extractconditionId.None→ early returnNone, emitstracing::warn!(event="gamma_market_rejected").gamma_client.rs:473-487— extractquestionID,question,slug. All default to""when absent.gamma_client.rs:490-491—extract_token_ids(market)→clobTokenIds(array or JSON-encoded string).gamma_client.rs:492—extract_string_array(market, "outcomes").gamma_client.rs:493—extract_string_array(market, "outcomePrices").gamma_client.rs:501-502—gamma_best_bid/gamma_best_askviaparse_decimal_field(handles both string and number JSON types — regression fix 2026-04-18).gamma_client.rs:504-534— buildVec<Token>: indexi=0→ YES (direct bestBid/bestAsk),i=1→ NO (inverted:bid = 1-ask,ask = 1-bid).gamma_client.rs:536-567— extract boolean flags with.unwrap_or(false)defaults.gamma_client.rs:569-581— decimal fields withparse_decimal_field;tick_sizetriesorderPriceMinTickSizethenminimumTickSizethen default 0.01.gamma_client.rs:586—parse_end_date(market)— RFC3339 only; naive strings →None.gamma_client.rs:591-611(крейтpm-bot, снят 2026-07-22 — путь описывает историческое поведение, не текущий код) — здесьgame_start_timeзаполнялся только при тегеid="103040"илиslug="daily-temperature". Действующий путь —crates/pm-engine/src/adapters/market_data/gamma_market_data.rs::parse_game_start_time_ms— такого ограничения не содержит и пропускает значение GammagameStartTimeдля любого рынка, где оно прислано; замер 15.09.2026 подтверждает широкую заполненность у спорта/киберспорта (§1.2).gamma_client.rs:622-634— event-level fields (event_id,event_title,event_slug).gamma_client.rs:637-642—source_updated_atfrom GammaupdatedAt(tolerant to absent/null).gamma_client.rs:646-677— constructMarket, thencompute_content_hashoverwrites thecontent_hash=0placeholder.
Embedded assumptions (all enumerated in §5).
4.4 parse_gamma_events_response (public helper)
Path: crates/pm-bot/src/io/gamma_client.rs:384-424. Heuristic at gamma_client.rs:398-403: if root[0].markets is an array → /events envelope; else → flat /markets list. Returns empty Vec on malformed JSON. Used by replay engine and MarketCache::hydrate_from_gamma_response.
4.5 filter_events
Path: crates/pm-bot/src/io/gamma_client.rs:275-302. Order:
- Event-level
negRiskfilter. - Per-market
resolutionSourceprefix filter. parse_market.
rejected_count increments only for step (3) failures — source-filter drops are not counted.
4.6 compute_content_hash
Path: crates/pm-common/src/models/market.rs:142-185 (public fn). Algorithm: XXH3-64 over 21 fields from CONTENT_HASH_FIELDS (market.rs:97-119). Token projection uses (token_id, outcome) pairs only — skips WS runtime state. Separator: \x1f.
4.7 pm_common::models::strategy::MarketSnapshot
Path: crates/pm-common/src/models/strategy.rs:50-90. Fields: condition_id, question, slug, active, closed, resolved, accepting_orders, resolution_source, neg_risk, neg_risk_augmented, min_tick_size, outcomes: Vec<OutcomeSnapshot>.
No end_date field — by design; deadline computation lives in TimezoneCache. Rustdoc at strategy.rs:5-18. This is repeated in the project root CLAUDE.md ("IMPORTANT: end_date").
OutcomeSnapshot (strategy.rs:103-117): token_id, outcome, probability, clob_midpoint, best_bid, best_ask, last_trade_price.
4.8 pm_common::models::position::PositionInfo
Path: crates/pm-common/src/models/position.rs:11-24. Fields: token_id, condition_id, outcome, size, avg_price, current_price, unrealized_pnl, simulated.
4.9 On-chain implementation in pm-sdk
| Operation | Path | Contract | Signature |
|---|---|---|---|
split | crates/pm-sdk/src/onchain/ctf.rs:40-76 | CTF | splitPosition(collateral, B256::ZERO, conditionId, [1,2], amount) |
merge (standard) | crates/pm-sdk/src/onchain/ctf.rs:96-133 | CTF | mergePositions(collateral, B256::ZERO, conditionId, [1,2], amount) |
merge (neg-risk) | crates/pm-sdk/src/onchain/ctf.rs:173-218 | NegRiskAdapter | mergePositions(conditionId, amount) (2-arg). Pre-flight isApprovedForAll. |
redeem (standard) | crates/pm-sdk/src/onchain/redeem.rs:44-64 | CTF | redeemPositions(USDC_E, B256::ZERO, conditionId, [1,2]) |
redeem (neg-risk) | crates/pm-sdk/src/onchain/redeem.rs:94-151 | NegRiskAdapter | redeemPositions(conditionId, [yes_bal, no_bal]) (2-arg). |
convertPositions | MISSING | — | Not implemented anywhere in pm-sdk or pm-bot. Verified via search for convertPositions, INegRiskAdapter.*convert, convert_position. See §6.1. |
Call sites (selected):
crates/pm-bot/src/application/executor.rs:640-646—merge_positions_neg_riskwhenneg_risk == true.crates/pm-bot/src/application/executor.rs:650—pm_sdk::onchain::ctf::merge_positions(&provider, condition_id, amount)for standard markets.crates/pm-bot/src/io/onchain/mod.rs:90—redeem::redeem_neg_risk(...)wheninfo.neg_risk.crates/pm-bot/src/application/executor.rs:560—SdkRedeemClient::redeem_neg_risk.
4.10 MarketCache storage
Path: crates/pm-bot/src/domain/market_cache.rs:43-633. Internal indexes:
markets: HashMap<String, Market>— keyed bycondition_id.token_to_market: HashMap<String, String>—token_id→condition_id.market_to_tokens: HashMap<String, Vec<String>>—condition_id→[token_ids].markets_by_event: HashMap<String, Vec<String>>—event_slug→[condition_ids].
Selected Market fields read internally:
condition_id(market_cache.rs:247, 261, 448, 597) — primary key.tokens(market_cache.rs:130, 250, 426-437, 456-465) — index iteration.neg_risk(market_cache.rs:182, 300) — viaMarketTypeFilter.passes(m).event_slug(market_cache.rs:138-145, 261-267) — populatesmarkets_by_event.content_hash(market_cache.rs:273-285) — BUG-04 P2 change detection.source_updated_at(market_cache.rs:230-233) —metadata_change_detectedlog.tick_size(market_cache.rs:374) — updated byupdate_tick_size.best_bid,best_ask(market_cache.rs:429-435) — updated byupdate_best_prices.last_trade_price,last_trade_ts,last_trade_side,last_trade_size(market_cache.rs:457-463).
4.11 Capture frames
Path: crates/pm-bot/src/persistence/capture/schema.rs:1-280. Schema version CAPTURE_SCHEMA_VERSION = 1 at schema.rs:30.
| Frame | Fields | Market-data content |
|---|---|---|
WsBookSnapshot | condition_id, asset_id, bids, asks, seq, ts_ms | Token-level book; BookLevel = {price: String, size: String}. |
WsBookDelta | condition_id, asset_id, side, price, new_size, seq, ts_ms | Incremental level; new_size="0" = remove. |
WsMarketResolved | condition_id, winning_asset_id, losing_asset_id, ts_ms | Resolution event. |
WsLastTrade | asset_id, condition_id, price, size, side, ts_ms | Last trade. condition_id may be "" when unknown at capture time (schema.rs:169 comment). |
WsTickSizeChange | condition_id, new_tick_size, ts_ms | Tick update. |
WsUserMeta | kind, ts_ms | Lifecycle only — no market data. |
RtdsTrade | condition_id, asset_id, side, size, price, taker_addr, ts_ms | Public trade flow. |
RtdsOrdersMatched | condition_id, notional, match_count, ts_ms | Aggregated match summary. |
RestGammaResponse | request_url, body_json, ts_ms | Full wire Gamma response. |
RestClobResponse | endpoint, request_hash, body_json, ts_ms | CLOB REST. |
RestDataResponse | endpoint, body_json, ts_ms | Data API. |
SourceTag priority (schema.rs:60-88): WsUser(0) > WsMarket(1) > Rtds(2) > RestGamma(3) > RestClob(4) > RestData(5) > WebhookSignal(6). CaptureFrame envelope (schema.rs:252-259): schema_version, source, ts_ns, monotonic_seq, payload.
4.12 Strategy consumption matrix
Per ADR-024, only directional/Harvester is active. Fields consumed by directional (Market. prefix elided): condition_id, event_slug, neg_risk, active/closed/resolved/accepting_orders, tokens[], tick_size, end_date (deadline), game_start_time (Daily Temperature anchor), resolution_source (ICAO parse), question (OrderMeta label), asks (WS sweep).
Fields unused by directional: question_id, category, event_id, event_title, description, archived, enable_order_book, min_order_size, volume, volume_24h, liquidity, updated_at, source_updated_at, content_hash, neg_risk_augmented.
Strategy file index (active only):
| Strategy | Path | StrategyType variant |
|---|---|---|
| Directional | crates/pm-bot/src/domain/strategies/directional.rs | Directional |
Archived strategies (bucket_arb, maker, maker_rebate, merge_arb, single-token-mm) and their per-field consumption patterns live in docs/restoration-backlog/README.md; the per-strategy field matrix from the pre-pivot snapshot is preserved in docs/history/liveness-audit-2026-05-06.md.
4.13 Strategy-config schema
DDL: crates/pm-bot/src/persistence/strategy_config.rs:206-228.
sql
CREATE TABLE IF NOT EXISTS strategies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT UNIQUE NOT NULL,
strategy_type TEXT NOT NULL, -- whitelisted string
wallet_label TEXT,
budget_limit_usd REAL NOT NULL,
enabled INTEGER NOT NULL DEFAULT 0,
definition TEXT NOT NULL, -- JSON blob, strategy-specific knobs
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);STRATEGY_TYPES whitelist in strategy_config.rs still carries legacy archived labels alongside "directional" so existing config.db rows remain readable; per ADR-024 only directional is admissible at runtime. Archived labels and their env-prefix history live in docs/restoration-backlog/README.md.
BotConfig::hydrate_strategy_block_from_json (crates/pm-bot/src/config/mod.rs:1023) reads flat {KEY: "value"} pairs from definition and applies them using the same env-var key names. No directional.* nested-key parsing exists yet.
5. Assumptions matrix — verified vs unchecked
Captured in code at parse_market, on-chain wrappers, and strategy admission sites. Format: assumption × file:line × verified-by × fail-mode.
| Assumption | file:line | Verified by | Fail mode |
|---|---|---|---|
conditionId is a non-null string. | gamma_client.rs:460-471 | Runtime guard (reject + warn!). | None returned; market dropped. |
clobTokenIds[0] = YES, clobTokenIds[1] = NO. | gamma_client.rs:513-521 | No explicit length check; documented as convention in quickstart (no spec invariant — §10 item 1). Unit test at gamma_client.rs:1067. | Silent wrong-side mapping if order ever differs. |
clobTokenIds.len() == 2 (binary). | gamma_client.rs:504 | MISSING — no assertion; enumerate() stops at actual length. Production sample (40 500/40 500) confirms it holds today. | Silent single-token or 3+-token market passes through. |
NO best_bid = 1 - YES_best_ask, NO best_ask = 1 - YES_best_bid. | gamma_client.rs:517-519 | Unit test test_parse_market_best_bid_ask_numeric (gamma_client.rs:1140). | Silent wrong NO prices if YES bid/ask absent. |
bestBid / bestAsk are market-level (YES token only). | gamma_client.rs:494-499 | Regression comment 2026-04-18. | Prior bug: dropped weather multi-outcome prices. |
outcomes[0] = "Yes", outcomes[1] = "No". | gamma_client.rs:508 via outcomes.get(i) | MISSING explicit check. Unit test at gamma_client.rs:1069. Production probe shows 18% of active markets violate the label assumption (Over/Under, team names, Odd/Even, Up/Down — §3.2). The strategies use positional index (i=0/i=1), not the label string, so the assumption is semantically violated but not physically used. | Code that filters or routes on the literal "Yes"/"No" label would mis-classify 7 304 active markets. |
gameStartTime format = "%Y-%m-%d %H:%M:%S%z". | gamma_client.rs:607 | MISSING — and_then returns None silently on mismatch. | game_start_time = None → deadline falls back to end_date. |
endDate is RFC3339 (not naive). | gamma_client.rs:715-720 | Regression test gamma_end_date_parsing.rs (cited line 585). | Naive strings → None (intentional fail-closed). |
Gamma ?neg_risk=… URL param works. | gamma_client.rs:46-50 | Verified broken — comment "silently ignored (verified 2026-04-20)". | Gamma returns unfiltered response; client-side filter required. |
neg_risk on market matches parent event. | gamma_client.rs:279-286 | Runtime filter checks event-level negRisk. | Mismatch possible if Gamma desyncs. |
parentCollectionId = B256::ZERO for all splits/merges. | crates/pm-sdk/src/onchain/ctf.rs:64, 117 | Unit test split_uses_zero_parent_collection (ctf.rs:232). | Revert on-chain if nested positions ever appear. |
conditionId is exactly 32 bytes hex. | crates/pm-sdk/src/onchain/redeem.rs:230-236 | Unit tests (redeem.rs:243-287). | anyhow::Error returned. |
MarketCache::update resets WS-derived prices. | crates/pm-bot/src/domain/market_cache.rs:109-115 | Rustdoc comment. | Strategy sees stale-then-None prices until WS rebuilds. |
WsLastTrade.condition_id may be "". | crates/pm-bot/src/persistence/capture/schema.rs:169 | Code comment "empty string if condition_id unknown". | Replay engine must handle empty condition_id. |
| Single binary event cannot be neg-risk. | NegRiskAdapter.sol L246-249 | Hard on-chain revert NoConvertiblePositions() when questionCount ≤ 1. | Contract revert. Off-chain negRisk can still be true on a 1-market event, but conversions are disabled. |
| Augmented neg-risk has same on-chain contract as standard neg-risk. | https://docs.polymarket.com/advanced/neg-risk.md | No separate AugmentedNegRiskAdapter.sol in any Polymarket GitHub repo. | n/a — augmentation is off-chain bulletin-board only. |
6. Known gaps
6.1 pm-sdk lacks convertPositions
Verified absent. Search for convertPositions, INegRiskAdapter.*convert, convert_position across all crates returns no results. Consequence: any future restored strategy that wants to convert a NO leg into the sibling-YES set on a neg-risk event has no implementation primitive available. See restoration-backlog for the historical multi-arb usage pattern.
6.2 No runtime length-assertion on clobTokenIds
parse_market does not enforce clobTokenIds.len() == 2. The 40 500-market production probe confirms 100% binary today, but any future drift would pass through silently as a single-token or N-token Market with degenerate fields.
6.3 Market.neg_risk_augmented field is effectively dead
Per §2.2 + §3.5, negRiskAugmented on the /markets Gamma endpoint is always null, so Market.neg_risk_augmented: bool is always false when constructed from /markets. The field carries information only when the Market is constructed inside /events parsing where the event-level value can be propagated. No active strategy consumes the field (the historical bucket_arb consumer is archived per ADR-024; see restoration-backlog). 41% of active events have negRiskAugmented=true — that information is not exposed at the market record returned by /markets and is therefore inaccessible to strategies reading the cache through that path.
6.4 Tick size 0.001 affects 25% of markets
tick_size = 0.001 covers 10 243 of 40 500 active markets (25.3%) — §3.6. Any price-rounding logic that assumes 0.01 would mis-quote a quarter of production.
6.5 /markets/{id} returns 404 — no REST path-by-ID
Per §3.11 (Agent C §6). All Gamma lookups must use ?slug= or ?id= query parameters. There is no /markets/{id} REST path.
6.6 Historical 2021 multi-outcome markets not filtered
parse_market tolerates tokens_len > 2 silently — the enumerate() loop iterates the full array. These markets are all closed (§3.9) and outside active scope, but a replay over 2021 capture data would parse them into degenerate Market records (binary code paths applied to ≥3 tokens).
6.7 4-algorithm taxonomy is documentation-only
Per §4.13 above: STRATEGY_TYPES whitelist still contains legacy labels for backward-compatible config.db reads. Per ADR-024 only directional is admissible at runtime. Archived strategies + restoration framework: restoration-backlog/README.md.
6.8 gameStartTime не валидируется перед использованием в якоре GstOffset
Поле широко заполнено у спорта/киберспорта (§1.2, §4.1 — замер 15.09.2026), но значение, присланное Gamma, не всегда согласовано с endDate: наблюдён случай gameStartTime позже endDate примерно на 2 месяца (слаг isp-ddy-ddy-2026-03-13). Действующий парсер (crates/pm-engine/src/adapters/market_data/gamma_market_data.rs::parse_game_start_time_ms) разбирает строку формата, но не проверяет её правдоподобность относительно endDate или текущего времени. compute_deadline для AnchorSource::GstOffset (crates/pm-engine/src/ports/deadline.rs:75-78) берёт metadata.game_start_time_ms напрямую (gst + offset_seconds) без сверки с end_date — недостоверное значение пройдёт в якорь как есть. Требуется проверка перед использованием (например, game_start_time не позже endDate и не в прошлом на момент обнаружения рынка) — на момент замера (15.09.2026, чтение deadline.rs) такой проверки в коде нет.
7. Edge cases & invariants
7.1 Outcome-label aliases — positional index is the only stable contract
outcomes labels in the production sample (§3.2):
- 82%
["Yes", "No"] - 8%
["Over", "Under"] - 8% team-vs-team pairings
- 2%
["Odd", "Even"] - 0.2%
["Up", "Down"]
All strategy code uses outcomes[i] / tokens[i] positionally (i=0 is the canonical "first" outcome — typically YES). The label is informational. Any code path that does if outcome == "Yes" would fail on 18% of active markets.
7.2 Tick size variability
Both 0.01 and 0.001 are first-class. See §3.6 and §6.4.
7.3 Weather events are neg-risk
Tag 103040 events are neg_risk=true (§3.8). Each temperature bracket is its own binary Yes/No market under a neg-risk event with 7 children (sample: highest-temperature-in-nyc-on-december-30).
7.4 Empty clobTokenIds
Zero markets with tokens_len == 0 in 40 500 active sample (§7 of Agent C). Empty arrays appear in closed markets only.
7.5 Token order ground-truth
Documentation prose at https://docs.polymarket.com/quickstart.md says "The first ID is the Yes token, the second is the No token." No formal OpenAPI invariant; no machine-readable contract. See §10 item 1.
7.6 Token degeneracy
In a 100-market probe, 0 markets had t0 == t1. All sampled markets have distinct YES / NO token IDs.
7.7 WsLastTrade.condition_id may be empty string
Comment at crates/pm-bot/src/persistence/capture/schema.rs:169 notes the field can be "" if condition_id is unknown at capture time. Replay code must tolerate it.
7.8 negRisk=null events
545 of 2 500 sampled events (21.8%) have negRisk=null at the event level — older singleton events (e.g. GTA VI released before June 2026). Parsing falls back to false via the serde default on Market.neg_risk.
8. Контракт отправки заявки (CLOB) — ответы, отказы, резерв баланса, задержка, идентификатор
Раздел о ПОВЕДЕНИИ контрагента на пути отправки заявки: какой формой приходит отказ, чему в ответе верить нельзя, как биржа держит баланс под открытыми заявками, что означает состояние задержки и чем на самом деле является идентификатор заявки. Знание дорогое в добыче: собрано 2026-07-29 из документации биржи, её OpenAPI-описания, исходников биржевого контракта и боевых данных нашего кошелька.
Класс источника назван у КАЖДОГО утверждения — иначе следующий читатель примет опубликованную формулу за измерение:
| Метка | Значение |
|---|---|
[ДОК] | Опубликовано контрагентом: docs.polymarket.com, OpenAPI-описание CLOB, исходник контракта. |
[БОЙ] | Наблюдалось на нашем боевом кошельке 0xba561b30…d0b7f4, в боевой базе или в боевом коде. |
[ВЫВОД] | Следует из [ДОК]/[БОЙ] логически; отдельным измерением не подтверждалось. |
[НЕ ПРОВЕРЕНО] | Ни документации, ни измерения — перечислено в §10. |
8.1 Одиночный и пакетный эндпойнты — РАЗНЫЕ контракты ответа
[ДОК] POST /order (одиночный; ровно им ходит движок — V2_POST_ORDER_PATH = "/order", crates/pm-sdk/src/clob/wire.rs:54). Под кодом 200 опубликованы ТОЛЬКО успехи: три примера live / matched / delayed, у всех errorMsg: ''. Отказ документирован кодами 400 / 401 / 500 / 503 с телом ErrorResponse { error, code?, retry_after_seconds? } — поле называется error, не errorMsg.
[ДОК] POST /orders (пакетный; движок им НЕ пользуется). Возвращает массив SendOrderResponse, и там форма ответа другая: в примере post_only_mode элементы несут success: true при ПУСТОМ orderID, пустом status и непустом errorMsg; в примере mixed_results третий элемент — success: false, orderID: '', status: delayed, errorMsg: 'Rate limit exceeded for tokenId: …'. То есть в пакетном ответе ни success, ни status признаком приёма не являются.
[ВЫВОД] Правило «success ненадёжен, смотри errorMsg» относится к ПАКЕТНОМУ эндпойнту и на одиночный без измерения не переносится. Обратное тоже неверно: из того, что отказы одиночного эндпойнта задокументированы не-2xx кодами, НЕ следует, что 200 с пустым orderID невозможен, — это не проверено (§10).
8.2 Что наш код читает из ответа и где он слеп
[БОЙ] На пути ОТВЕТА НА POST ClobExecutor::send_prepared_order (crates/pm-sdk/src/clob/mod.rs:840) берёт из успешного тела ТОЛЬКО orderID (на пути чтения списка открытых заявок словарь статусов шире — см. 8.5). status, errorMsg, makingAmount, takingAmount, transactionsHashes, tradeIDs отбрасываются. Ответ 2xx без orderID → ClobError::Sdk → в движке OrderError::SubmitOutcomeUnknown, то есть НЕОПРЕДЕЛЁННОСТЬ (деньги удержаны, намерение заблокировано), а не окончательный отказ. Прежняя запись «→ OrderError::Upstream» устарела с расщеплением источника (ambiguous-submit-outcome, волны 6–7) и здесь исправлена.
[БОЙ] Отображение кодов отказа (classify_post_error, crates/pm-sdk/src/clob/wire.rs:594): 425 или тело с post_only_mode → MatchingEngineRestart{retry_after}; 429 → RateLimited; 401|403 → AuthExpired; 400|422 → OrderRejected{reason = тело целиком}; 404 → NotFound; всё прочее → Sdk. Тело отказа сохраняется дословно — это единственное, что потом можно разобрать.
[ВЫВОД] Слепые зоны названы прямо: статусы delayed и unmatched в ответе на POST движок не различает вовсе (см. 8.5), а различение видов отказа держится на разборе текста (см. 8.3).
8.3 Машиночитаемого кода отказа НЕТ
[ДОК] ErrorResponse.code описан как «Machine-readable error code, when provided». Во всём OpenAPI-описании поле code встречается ровно в двух примерах, и оба несут ОДНО значение — post_only_mode (HTTP 503, вместе с retry_after_seconds; один пример у одиночного эндпойнта, один у пакетного). Для отказа по нехватке баланса кода не публикуется вовсе.
[БОЙ] Дословный боевой отказ по балансу: HTTP 400, тело {"error":"not enough balance / allowance: the balance is not enough -> balance: 0, order amount: …"} (зафиксировано в crates/pm-engine/src/adapters/clob/live/client.rs:1317).
[ВЫВОД] Отличить «отказ по нехватке долей» от прочих отказов можно ТОЛЬКО разбором текста. Три классификатора по подстрокам (is_balance_settling_reject, is_post_only_crossing_reject, is_matching_engine_restart_reject в том же файле) — не небрежность, а единственный доступный различитель. Следствие для любого будущего проекта: перефразировка сообщения на стороне биржи молча переводит отказ в неразобранный класс, и защита, построенная на тексте, обязана иметь консервативное поведение по умолчанию.
8.4 Резерв баланса под открытыми заявками — главный факт раздела
[ДОК] Дословно (concepts/order-lifecycle.md, раздел «Requirements»):
Order size is limited by your available balance minus any amounts reserved by existing open orders.
maxOrderSize = balance − Σ(openOrderSize − filledAmount)
[ДОК] Там же: баланс для покупки — pUSD, для продажи — сами доли («Sufficient pUSD (for buys) or tokens (for sells)»). Валидация баланса и allowance выполняется оператором при приёме заявки («The operator validates: … You have sufficient balance»).
[ВЫВОД] Прогон формулы по трём состояниям исходной продажи 100 долей — то, ради чего факт и нужен:
| Состояние первой продажи | Доли на кошельке | Зарезервировано | Доступно к продаже | Ответ на повтор |
|---|---|---|---|---|
| Покоится, не исполнена | 100 | 100 − 0 = 100 | 0 | отказ по балансу |
| Исполнена частично (40 из 100) | 60 | 100 − 40 = 60 | 0 | отказ по балансу |
| Исполнена полностью | 0 | 0 | 0 | отказ по балансу |
Три состояния, требующие РАЗНЫХ действий, дают ОДИН ответ. Отказ по балансу доказывает лишь, что инвентарь связан либо уже продан, и НЕ отвечает, жива ли первая заявка.
Таблица посчитана в предположении, что биржевой взгляд на баланс АКТУАЛЕН на момент повтора. Если между сопоставлением и расчётом биржа ещё считает нашими уже проданные доли, строка «исполнена частично» даёт ненулевой доступный остаток и повтор может быть принят — это не опровергнуто и вынесено в §10 пункт 6.
[ВЫВОД] Обратное направление информативно: успех повторной продажи означает, что доли не связаны и не проданы, то есть первой заявки на бирже нет — с оговоркой о свежести биржевого взгляда на баланс (последний пункт этого подраздела).
[ВЫВОД] Асимметрия сторон, из которой растёт всё поведение при потерянном ответе: продажа резервирует ровно тот инвентарь, который продаёт, поэтому потолок связывает; покупка резервирует деньги, и заявка на 2.50 USD против залога в сотни долларов потолка не достигает — повтор покупки будет ПРИНЯТ, и позиция удвоится.
[ДОК] Крайний рубеж против продажи несуществующих долей — расчёт: «Settlement is atomic: either the entire trade succeeds or nothing happens», а трейд имеет терминальный статус FAILED. Продать доли, которых нет, расчёт не даст; ценой станет неисполнившаяся сделка, а не отрицательный инвентарь.
[БОЙ] Резерв ИЗМЕРЕН прямой пробой 2026-07-29, и текст отказа получен именно от собственной покоящейся продажи (прежде наш единственный образец был получен из-за лага расчёта после исполнения — то есть из другого состояния, и совпадение текстов оставалось неизвестным). При живой первой продаже повтор той же продажи отвергается не-2xx с телом:
json
{"error":"not enough balance / allowance: the balance is not enough -> balance: 5000000, sum of active orders: 5000000, sum of matched orders: 0, order amount (inc. fees): 5000000"}Отказ несёт РАЗБИВКУ из трёх чисел: сколько долей есть, сколько занято АКТИВНЫМИ заявками, сколько ИСПОЛНЕНО. Машиночитаемого кода у него нет (§8.3), поэтому читается только разбором строки.
[БОЙ] GET /balance-allowance резерва НЕ показывает. Баланс долей не меняется ни сразу после выставления продажи, ни через три секунды, ни после её снятия. Значит текст отказа выше — ЕДИНСТВЕННЫЙ канал этой информации.
[ВЫВОД] Разбивка НЕ отменяет вывод таблицы выше. Три числа — агрегаты по кошельку и исходу, а не по конкретной заявке: «занято активными» может держать посторонняя покоящаяся заявка того же исхода, а заявка FAK не связывает доли по построению. Судьбу конкретной отправки разбивка не устанавливает.
[НЕ ПРОВЕРЕНО] Свежесть биржевого взгляда на баланс в момент повторной заявки. Наблюдавшийся у нас лаг направлен в консервативную сторону (balance: 0 сразу после налитой ПОКУПКИ, то есть биржа недооценивает наш инвентарь и отказывает лишний раз). Лаг в противоположную сторону (биржа ещё считает нашими уже проданные доли) не наблюдался и не опровергнут — см. §10.
8.5 Состояние задержки: delayed, unmatched и асинхронный отказ
[ДОК] Задержек две (concepts/order-lifecycle.md, шаг «Match or Rest»):
- taker delay — «used on selected crypto and finance up/down markets. The order is held for 250 ms, then validation runs again». Признак рынка:
itode: trueв ответеGET https://clob.polymarket.com/clob-markets/{condition_id}. - sports/game delay — «enabled on configured sports markets around live game conditions», окно задаёт поле рынка
seconds_delay.
[ДОК] Дословно про обе: «During either delay, the order is pending and cannot be canceled. If the market, balance, allowance, or risk checks fail when the delay expires, the order is rejected instead of matching.» Это закрывает два вопроса разом: внутри окна заявку отменить НЕЛЬЗЯ, а ответ delayed НЕ является обещанием исполнения или покоя — отказ может прийти позже и уже не в ответе на POST.
[ДОК] Статусы ответа на размещение: live, matched, delayed, unmatched («Marketable order placed on the book after the delay expired without a match»). Расхождение внутри документации контрагента: OpenAPI-перечисление SendOrderResponse.status содержит только live|matched|delayed, без unmatched.
[БОЙ] Боевой набор движка задержки не имеет: у трёх активных погодных рынков области поиска (тег 104596) на 2026-07-29 seconds_delay = 0, признака itode в компактном описании GET /clob-markets/{condition_id} нет (проверены 0x09e90a76…33f53, 0x0e791fd5…6b8c, 0xa7aa01d5…b43c3). Путь задержки станет достижимым при расширении области поиска на спорт или крипту.
[БОЙ] В коде строки DELAYED / UNMATCHED известны только читателю списка открытых заявок (crates/pm-engine/src/lifecycle/bootstrap.rs:354); путь ответа на POST их не различает.
8.6 Отмена и частичное исполнение
[ДОК] «You can cancel orders at any time before they're matched via the CLOB API, except while a marketable order is in a pending delay window.»
[ДОК] «Partial fills cannot be cancelled: only the unfilled portion of an order can be cancelled.»
[ДОК] Отмена адресуется хешем: CancelOrderPayload.orderID — «Order ID (order hash) to cancel». Ответ CancelOrdersResponse { canceled: [...], not_canceled: { id: причина } } — причина отказа отмены приходит ТЕКСТОМ на идентификатор, кода там тоже нет.
8.7 Идентификатор заявки = подписной EIP-712-хеш (доказано, не предположено)
[ДОК] SendOrderResponse.orderID — «Unique identifier for the order (order hash)».
[ДОК] Исходник биржевого контракта (репозиторий Polymarket/ctf-exchange): hashOrder(order) = _hashTypedDataV4(keccak256(abi.encode(ORDER_TYPEHASH, …))) (mixins/Hashing.sol:19-20); ОДИН и тот же bytes32 идёт в проверку подписи (mixins/Trading.sol:230-233 → Signatures.sol:21-22) и в событие OrderFilled (mixins/Trading.sol:108).
[БОЙ] Сверка на боевых данных, без единой новой заявки: строка боевой базы polymarket_order_id = 0x31b1d4ff…18c0c (SELL, налито 25 долей, first_fill_at 1785130961517) → сделка data-api (SELL 25 по 0.40, transactionHash = 0xf8cdb352…3a15) → в квитанции транзакции лог контракта 0xe2222d279d744050d28e00520010520000310F59 с topics[1] = 0x31b1d4ff…18c0c и maker = наш кошелёк. Адрес эмитента — ровно тот verifyingContract, с которым pm-sdk строит EIP-712-домен V2 (crates/pm-sdk/src/contracts/mod.rs:151).
[ВЫВОД] Значит идентификатор, который биржа вернула в ответе, равен хешу, вычисленному контрактом, а тот равен хешу, который мы подписали (иначе восстановление подписи дало бы другого подписанта и исполнение бы не состоялось).
[БОЙ] Прямое ПОБАЙТОВОЕ измерение 2026-07-29 — вывод выше больше не единственная опора. Локально вычисленный подписной хеш 0xba953f0e…bebfaf6 совпал с orderID, который вернула биржа на ту же отправку: 0xba953f0e…bebfaf6. Цепочка «строка базы → сделка → лог контракта» остаётся верной, но равенство теперь измерено на ответе самой биржи, а не выведено через квитанцию транзакции.
Следствие практическое: локальный хеш, вычисленный ДО отправки, — законный ключ сверки. По нему можно спрашивать состояние заявки, отменять её и УЗНАВАТЬ СВОЮ заявку в перечне открытых ордеров, вовсе не дождавшись ответа на POST.
[НЕ ПРОВЕРЕНО] Исходник, прочитанный по ссылке, — контракт V1; исходник V2 (0xe2222d…) не читался. Структура «посчитали хеш → проверили им подпись → его же отдали в событие» взята из V1, а на V2 подтверждена совпадением боевых значений выше.
8.8 Ограничения ЧТЕНИЯ, важные для сверки
[ДОК] Get user orders / Get Open Orders — «Returns paginated results». Пустая страница выдачи НЕ является доказательством отсутствия открытых заявок.
[ДОК] Get Orders (история состояний заявок) и Get Fills — «Maximum of 100 entries returned per request». Любая сверка по истории обязана листать выдачу, иначе молча смотрит в хвост.
8.9 Поле deferExec
[ДОК] В теле SendOrder есть deferExec: boolean, default: false («Whether to defer execution»).
[БОЙ] Наш V2OrderPostBody (crates/pm-sdk/src/clob/wire.rs:150-160) содержит ровно четыре поля: order, owner, orderType, postOnly. Поле deferExec не отправляется НИКОГДА, то есть действует документированное умолчание false — исполнение не откладывается. На конструкцию защиты от потерянного ответа поле влияния не оказывает.
9. Glossary
| Term | Meaning |
|---|---|
conditionId / condition_id | 32-byte hex identifier of the on-chain CTF condition. Primary key everywhere. |
questionId / question_id | On-chain question identifier (input to prepareCondition). |
clobTokenIds | ERC1155 token IDs (uint256 decimal strings), 2 per binary market. Index 0 = YES, 1 = NO. |
position_id | Synonym for ERC1155 tokenId of a YES or NO position. |
collection_id | Internal CTF identifier used in splitPosition / mergePositions — Polymarket binary markets always use parentCollectionId = B256::ZERO. |
wcol | Wrapped collateral token used inside NegRiskAdapter to bridge collateral to CTF splits. |
outcomeSlotCount | CTF parameter; always 2 for all Polymarket markets. |
partition | CTF parameter; always [1, 2] for Polymarket binary YES/NO splits. |
payout vector | Resolution payouts indexed by outcome slot; Polymarket binary uses [1,0] or [0,1]. |
tag id | Gamma event categorization. 103040 = weather (Daily Temperature). |
neg-risk | Mechanism for multi-outcome events where only one outcome can win, managed by NegRiskAdapter. Requires ≥2 questions. |
augmented neg-risk | Extension of neg-risk with placeholder outcomes + an explicit "Other" market; managed off-chain via bulletin board; same on-chain contract. |
negRiskMarketID | Event-level identifier under NegRiskAdapter (mentioned in NegRiskAdapter source; not exposed through the Market struct in pm-common). |
event_slug | URL slug of the parent Gamma event. Used by directional and historically by archived multi-leg strategies. |
BINARY_PARTITION | Local constant [1, 2] in crates/pm-sdk/src/onchain/ctf.rs:20. |
MarketInput | DTO synthesized from Market for directional strategy at crates/pm-bot/src/domain/strategies/directional.rs:3195. |
MarketSnapshot | View of Market exposed to strategy interface (without end_date; deadline via TimezoneCache). |
SourceTag | Priority enum for capture-frame de-duplication (WsUser > WsMarket > Rtds > RestGamma > RestClob > RestData > WebhookSignal). |
BotEvent / EventEnvelope / BotSnapshot | Wire contract between pm-bot and pm-dashboard (crates/pm-bot-events/). |
10. UNRESOLVED items & inter-report contradictions
UNRESOLVED facts (need external confirmation)
clobTokenIdsorder — formal spec guarantee. The quickstart prose states the first ID is the Yes token; the OpenAPI YAML schema describesclobTokenIdsonly asarray of stringwith no ordering invariant. Risk: a future API refactor could silently break this. Requires Polymarket dev inquiry for a machine-readable invariant.- Augmented neg-risk bulletin-board API. Docs reference "the bulletin board" for placeholder clarification but provide no API endpoint or contract address. Requires Polymarket dev inquiry for a concrete API surface.
NegRiskAdapter.convertPositionsgas cost. No NatSpec gas estimate; empirical benchmarks not available without on-chain testing.- Separate augmented-neg-risk on-chain contract. No
AugmentedNegRiskAdapter.solfound in any Polymarket GitHub repo. Augmentation appears to be entirely off-chain; could not definitively confirm from public sources alone. - Резервирует ли баланс заявка, находящаяся в окне задержки (
delayed, §8.5). Документация говорит только, что заявка «pending» и не отменяема; о резерве под неё не сказано ничего. Попытка измерить 2026-07-29 НЕ УДАЛАСЬ: на выбранном рынке сseconds_delay = 1заявка исполнилась раньше, чем окно удалось наблюдать; отрицательный результат неинформативен. Остаётся вопросом денег при расширении области поиска на спорт или крипту. Закрывается пробой на рынке с БОЛЬШОЙ задержкой либо ответом разработчиков биржи. - Свежесть биржевого взгляда на баланс в момент повторной заявки (§8.4). Наблюдался лаг в консервативную сторону (
balance: 0сразу после налитой покупки). Лаг в противоположную сторону — биржа ещё считает нашими уже проданные доли — не наблюдался и не опровергнут. Если он существует, повторная продажа может быть ПРИНЯТА и упереться уже в расчёт (терминальный статус сделкиFAILED), а не в отказ приёма. Дословная форма отказа, вызванного ИМЕННО собственной покоящейся заявкой— ЗАКРЫТО ИЗМЕРЕНИЕМ 2026-07-29 (§8.4). Отказ получен прямой пробой при живой первой продаже, дословный текст записан, и он несёт РАЗБИВКУ из трёх чисел (balance,sum of active orders,sum of matched orders). Побочно измерено:GET /balance-allowanceрезерва под открытыми заявками не показывает вовсе, то есть текст отказа — единственный канал этой информации.- Единицы
minimum_order_size. Биржа отдаётminimum_order_size: 5и в описании рынка, и в стакане, но единицы не названы ни в OpenAPI-описании, ни в документации. Проект читает величину как ДОЛИ (min_size_sharesв настройках движка). Прочтение «5 долларов» не опровергнуто. - Возможен ли у одиночного
POST /orderответ 200 с пустымorderID(§8.1). Такая форма документирована ТОЛЬКО у пакетного эндпойнта. Проба 2026-07-29 её НЕ ВОСПРОИЗВЕЛА: заявка «только в стакан», пересекающая рынок, — самая правдоподобная кандидатура на этот ответ — получила не-2xx с телом{"error":"invalid post-only order: order crosses book"}, то есть отказ пришёл обычным путём. Вопрос остаётся открытым (одна проба не исчерпывает класс), но улик в пользу существования формы у одиночного эндпойнта нет. Наш код на неё отвечаетClobError::Sdk→OrderError::SubmitOutcomeUnknown, то есть НЕОПРЕДЕЛЁННОСТЬЮ (прежняя запись «→OrderError::Upstream, окончательный отказ» устарела с волнами 6–7 и здесь исправлена).
Inter-report contradictions observed during synthesis
- NHL Hart Trophy event classification. Agent C §5 lists
nhl-2025-26-hart-memorial-trophyunder "Large neg-risk event" withnegRiskAugmentednot noted; the on-disk sample.tmp/gamma-samples/event-neg-risk-large.jsonactually returns"negRisk": true, "negRiskAugmented": true, "enableNegRisk": true. The sample is therefore both neg-risk and augmented, not "plain" neg-risk as Agent C's tabular row suggests. This document classifies it as augmented per the live sample (§1.1, §3.7). - Archived per-strategy neg_risk handling. Pre-pivot per-strategy neg-risk admission rules (merge_arb / maker_rebate / bucket_arb) live in restoration-backlog/README.md and
docs/history/liveness-audit-2026-05-06.md. - Weather neg-risk status. Agent C §7 frames the
neg_risk=trueweather classification as a "correction to earlier assumption", with the implication being that an older project belief held weather to be non-neg-risk. Project memoryfeedback_max_hours_expiry.mdreferences weather neg-risk as established. The live probe (§3.8) settles the fact: weather IS neg-risk; bracket Yes/No children. - (Removed) Pre-pivot taxonomy notes about
maker_rebate.rsunification — see restoration-backlog/README.md#maker-rebate and #paired-maker-rebate.
11. Provenance
Audit date: 2026-05-13 (§1–§7); 2026-07-29 (§8 — контракт отправки заявки, источники ниже).
Synthesis input — reports (under /home/me/RustroverProjects/polymarket-engine/.tmp/audit-reports/):
A-polymarket-docs-onchain.md— Polymarket docs sweep + on-chain Solidity (CTF, NegRiskAdapter) source links.B-code-mapping.md—pm-common/pm-bot/pm-sdkmapping + assumptions matrix.C-production-data.md— 40 500-market probe with distributions and edge cases.D-strategy-consumption.md— strategy data consumption + 4-algorithm taxonomy migration impact.
Synthesis input — samples (under /home/me/RustroverProjects/polymarket-engine/.tmp/gamma-samples/):
event-singleton-binary.json— singleton binary event.event-weather-multimarket.json— weather neg-risk event (tag 103040).event-neg-risk-large.json— 139-market neg-risk event.event-neg-risk-augmented.json— 60-market augmented neg-risk event.event-neg-risk-large-meta.json— slim metadata sidecar.market-multi-outcome-historical.json— 2021-era 3-outcome closed market.markets-bulk-2026-05-13.jsonl— 40 500 active markets (slim records, 11 MB).events-bulk-2026-05-13.jsonl— 2 500 active events (slim records, 421 KB).closed-multi-outcome.jsonl— 39 historical multi-outcome closed markets.
On-chain source references:
- ConditionalTokens: https://github.com/Polymarket/conditional-tokens-contracts/blob/a927b5a52cf9ace712bf1b5fe1d92bf76399e692/contracts/ConditionalTokens.sol
- NegRiskAdapter: https://github.com/Polymarket/neg-risk-ctf-adapter/blob/f78b35b0863b4308a431ca307d06f49b2ea65e78/src/NegRiskAdapter.sol
- CTF Exchange repo: https://github.com/Polymarket/ctf-exchange
Documentation references:
- Doc index: https://docs.polymarket.com/llms.txt
- Neg-risk: https://docs.polymarket.com/advanced/neg-risk.md
- Markets & events: https://docs.polymarket.com/concepts/markets-events.md
- Positions/tokens: https://docs.polymarket.com/concepts/positions-tokens.md
- CTF overview: https://docs.polymarket.com/trading/ctf/overview.md
- CTF split: https://docs.polymarket.com/trading/ctf/split.md
- CTF merge: https://docs.polymarket.com/trading/ctf/merge.md
- CTF redeem: https://docs.polymarket.com/trading/ctf/redeem.md
- Contracts list: https://docs.polymarket.com/resources/contracts.md
- Quickstart (clobTokenIds order): https://docs.polymarket.com/quickstart.md
- Fetching markets: https://docs.polymarket.com/market-data/fetching-markets.md
- CLOB market schema: https://docs.polymarket.com/api-reference/markets/get-clob-market-info.md
Источники §8 (сбор 2026-07-29):
- Жизненный цикл заявки (резерв баланса, обе задержки, статусы, отмена): https://docs.polymarket.com/concepts/order-lifecycle.md
- Справка по одиночному размещению (коды отказа,
ErrorResponse,deferExec): https://docs.polymarket.com/api-reference/trade/post-a-new-order.md - OpenAPI-описание CLOB (
SendOrderResponse,ErrorResponse, пакетныйPOST /orders,CancelOrderPayload):docs.polymarket.com/api-spec/clob-openapi.yaml, локальная копия.tmp/clob-openapi.yaml. - Исходник биржевого контракта (
hashOrder→ проверка подписи →OrderFilled): https://github.com/Polymarket/ctf-exchange —src/exchange/mixins/{Hashing,Trading,Signatures}.sol. - Боевая сверка идентификатора: строка
ordersбоевой базыharvester-engine, сделкаdata-apiкошелька0xba561b30…d0b7f4, квитанция транзакции0xf8cdb352…3a15в сети Polygon (лог контракта0xe2222d27…310F59). - Живое чтение рынков области поиска:
GET https://clob.polymarket.com/markets/{condition_id}иGET https://clob.polymarket.com/clob-markets/{condition_id}по трём активным погодным рынкам тега 104596.
12. Change log
- 2026-09-15 — Исправлено ложное утверждение «
gameStartTimeприсутствует только у погоды (тег103040)» (§1.2, §4.1, §4.3 п.11 — унаследовано из первичного синтеза 2026-05-13, отражало код удалённого крейтаpm-bot): живой обход Gamma/events/keysetпоказал поле заполненным у 121 237 из 141 510 активных рынков спорта/киберспорта (теги 1, 64, 105907); действующий парсер (gamma_market_data.rs::parse_game_start_time_ms) тега не проверяет. Добавлен §6.8 — значение не проверяется на согласованность сendDateперед использованием в якореGstOffset(наблюдена аномалия:gameStartTimeпозжеendDateна ~2 месяца,isp-ddy-ddy-2026-03-13). Добавлен факт в §3.11: размер страницы/events/keysetфиксирован API на 100 независимо отlimit(провереноlimit=100/200/300). - 2026-07-29 (живые пробы контракта) — §8 приведён к измерениям, полученным пробами на боевом кошельке (
crates/pm-engine/tests/live_order_contract_probes.rs, стоимость $0.22): равенство биржевогоorderIDподписному хешу измерено ПОБАЙТОВО, а не выведено через квитанцию (§8.7); отказ по нехватке долей получен ИМЕННО от собственной покоящейся продажи и несёт разбивку из трёх чисел, аGET /balance-allowanceрезерва не показывает вовсе (§8.4, §10 пункт 7 закрыт); одиночныйPOST /orderотказывает не-2xx с полемerror— форма «200 плюсerrorMsg» пробой не воспроизведена (§10 пункт 9 уточнён); на успехеerrorMsgприсутствует и пуст; попытка измерить резерв внутри окна задержки не удалась (§10 пункт 5). Исправлены две устаревшие записи о нашем коде: 2xx безorderIDдаётOrderError::SubmitOutcomeUnknown, а неUpstream(§8.2, §10 пункт 9). - 2026-07-29 — Добавлен §8 «Контракт отправки заявки»: резерв баланса под открытыми заявками (
maxOrderSize = balance − Σ(openOrderSize − filledAmount)) и его следствие — отказ по балансу не различает «покоится / частично исполнена / исполнена»; отсутствие машиночитаемого кода отказа у одиночногоPOST /order; разная форма ответа у одиночного и пакетного эндпойнтов; семантика окна задержки (pending, отмена запрещена, отказ возможен по истечении окна) и статусunmatched, отсутствующий в OpenAPI-перечислении; доказанное равенство биржевого идентификатора заявки и подписного EIP-712-хеша (боевая сверка с событиемOrderFilled);deferExecдвижком не отправляется. Разделы 8–11 сдвинуты на один номер вниз (Glossary → §9, UNRESOLVED → §10, Provenance → §11, Change log → §12). - 2026-05-13 — Initial synthesis from four parallel audit agents (A docs+on-chain, B code mapping, C production probe 40 500 markets, D strategy consumption). Established: 100% active binary markets, 63.5% neg-risk active, 18% non-
Yes/Nolabels, 25% tick=0.001, weather neg-risk confirmed,Market.neg_risk_augmentedfield dead at/marketspath,convertPositionsabsent from pm-sdk.