outcometick 1.6.4 → 1.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/api/lib/backtest-contract.mjs +20 -2
- package/api/lib/data-taxonomy.mjs +32 -2
- package/cli/local-data.mjs +1 -1
- package/index.d.ts +9 -2
- package/package.json +1 -1
- package/runner/engine/portfolio.mjs +18 -2
- package/runner/engine/report.mjs +13 -12
- package/runner/events.mjs +31 -13
- package/runner/harness/node/sdk/index.d.ts +9 -2
- package/runner/harness/protocol.mjs +3 -2
- package/runner/harness/python/otengine.py +8 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
scripts/publish-sdk-repos.mjs and overwritten wholesale on each publish.
|
|
6
6
|
An edit made here survives until the next publish and then disappears.
|
|
7
7
|
|
|
8
|
-
Generated from monorepo revision
|
|
8
|
+
Generated from monorepo revision 13e95ca5b2d1f465cb53799e610a08476f1db274.
|
|
9
9
|
-->
|
|
10
10
|
|
|
11
11
|
# outcometick
|
|
@@ -15,7 +15,7 @@ import { FIRST_COMPLETE_DAY } from './coverage-window.mjs';
|
|
|
15
15
|
export const SCHEMA_VERSION = 1;
|
|
16
16
|
|
|
17
17
|
/** SDK version reported by the docs page and stamped into every report. */
|
|
18
|
-
export const SDK_VERSION = '1.6.
|
|
18
|
+
export const SDK_VERSION = '1.6.5';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* The tag of the sandbox images, and the ONLY place it is written down.
|
|
@@ -60,7 +60,7 @@ export const SDK_VERSION = '1.6.4';
|
|
|
60
60
|
* forwarded a fourth descriptor, so fd 3 was closed inside the container and no
|
|
61
61
|
* containerised run had ever returned anything.
|
|
62
62
|
*/
|
|
63
|
-
export const SANDBOX_IMAGE_TAG = '1.
|
|
63
|
+
export const SANDBOX_IMAGE_TAG = '1.16.0';
|
|
64
64
|
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
66
|
// Languages
|
|
@@ -294,6 +294,24 @@ const BOOK_CAPTURE = Object.freeze({
|
|
|
294
294
|
]),
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
+
/**
|
|
298
|
+
* The days the book cadence changed, oldest first.
|
|
299
|
+
*
|
|
300
|
+
* Exported for the prewarm, which has to warm one shape PER CADENCE TIER: the
|
|
301
|
+
* cadence is chosen from a run's date RANGE, so a range that stops short of a
|
|
302
|
+
* change and one that crosses it are two different cache entries for the same
|
|
303
|
+
* day. Warming a single range therefore covers exactly one tier and silently
|
|
304
|
+
* misses the others — which is what happened between 2026-08-25 and
|
|
305
|
+
* 2026-09-07, when the prewarm warmed the whole sellable span (500ms) while
|
|
306
|
+
* every run over recent days wanted 20ms.
|
|
307
|
+
*
|
|
308
|
+
* Derived from the table above rather than restated, so a new cadence entry
|
|
309
|
+
* grows the warm set without anyone remembering to come here.
|
|
310
|
+
*/
|
|
311
|
+
export function bookCadenceChangeDays(venue) {
|
|
312
|
+
return (BOOK_CAPTURE[venue] ?? []).map((e) => e.from);
|
|
313
|
+
}
|
|
314
|
+
|
|
297
315
|
/**
|
|
298
316
|
* The cadence ONE ASSET replays at within a run: the COARSEST its date range
|
|
299
317
|
* contains. Ask per asset; there is no run-wide answer by design.
|
|
@@ -21,14 +21,44 @@ export const DATASETS = {
|
|
|
21
21
|
book: 'Full-depth order-book snapshots',
|
|
22
22
|
best_bid_ask: 'Top of book, unthrottled — the same best bid/ask price_change carries, at every update rather than the capture cadence; prices only, no sizes, so depth still needs book or price_change',
|
|
23
23
|
price_change: 'Order-book deltas with best bid/ask',
|
|
24
|
-
last_trade_price: '
|
|
24
|
+
last_trade_price: 'Trade prints, unthrottled',
|
|
25
25
|
markets: 'Per-market metadata, strike and settlement outcome',
|
|
26
26
|
tick_size_change: 'Tick-size changes',
|
|
27
27
|
orderbook: 'Order-book snapshots (Predict.fun)',
|
|
28
|
-
klines: '
|
|
28
|
+
klines: 'OHLC candles derived from the settlement feed, with a tick count — no trade volume; the settlement feed is a price feed',
|
|
29
29
|
other: 'Uncategorised',
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The same descriptions in Chinese.
|
|
34
|
+
*
|
|
35
|
+
* HERE, not in the page that renders them. Two of these lines state the
|
|
36
|
+
* SETTLEMENT BASIS — which TWAP stream settles which market — and CLAUDE.md
|
|
37
|
+
* pins that fact to four places that must move together. A Chinese copy living
|
|
38
|
+
* in a component would be a fifth, and it would drift the way every other
|
|
39
|
+
* hand-copy in this repo has: the basis already moved once (5-minute markets
|
|
40
|
+
* settled on the 30s stream until 2026-08-07, on the 60s stream after), and an
|
|
41
|
+
* AI assistant quoted a stale copy of our coverage back to a customer.
|
|
42
|
+
*
|
|
43
|
+
* Sitting beside DATASETS means changing one entry puts the other on screen in
|
|
44
|
+
* the same diff. data-taxonomy.test.mjs asserts the key sets match, so a new
|
|
45
|
+
* dataset cannot land with only one language.
|
|
46
|
+
*/
|
|
47
|
+
export const DATASETS_ZH = {
|
|
48
|
+
prices: '结算价流,逐 tick(Chainlink 瞬时流)',
|
|
49
|
+
twap30s: 'TWAP 30 秒结算流——5 分钟市场改用 60 秒回看之前的结算线,至今仍逐日归档',
|
|
50
|
+
twap60s: 'TWAP 60 秒结算流——5 分钟与 15 分钟市场都判定在它上面',
|
|
51
|
+
book: '全深度盘口快照',
|
|
52
|
+
best_bid_ask: '盘口顶部,未抽稀——与 price_change 携带的是同一个最优买卖价,但每次更新都出一行,而不是按采集节奏;只有价格没有挂单量,深度仍需 book 或 price_change',
|
|
53
|
+
price_change: '盘口增量,带最优买卖价',
|
|
54
|
+
last_trade_price: '成交流,未抽稀',
|
|
55
|
+
markets: '每个市场的元数据、strike 与结算结果',
|
|
56
|
+
tick_size_change: '最小变动价位的变更',
|
|
57
|
+
orderbook: '盘口快照(Predict.fun)',
|
|
58
|
+
klines: '由结算价流推导的 OHLC K 线,带 tick 计数——没有成交量,结算流是价格流不是成交流',
|
|
59
|
+
other: '未归类',
|
|
60
|
+
};
|
|
61
|
+
|
|
32
62
|
const num = (s) => (s == null ? null : s);
|
|
33
63
|
|
|
34
64
|
/**
|
package/cli/local-data.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// R2. Both go through runner/events.mjs for the row -> event mapping, because
|
|
5
5
|
// the docs make a promise about exactly this pair:
|
|
6
6
|
//
|
|
7
|
-
// The identical files,
|
|
7
|
+
// The identical files, byte for byte, with the same checksums. A backtest
|
|
8
8
|
// here and a backtest on your own machine after subscribing read the same
|
|
9
9
|
// bytes — that is the point of offering it.
|
|
10
10
|
//
|
package/index.d.ts
CHANGED
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
export type Side = 'UP' | 'DOWN';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* How a market settled. `'TIE'` is a 50:50 settlement — Predict.fun resolves
|
|
17
|
+
* an end price equal to the start price this way, and every UP and DOWN
|
|
18
|
+
* contract pays $0.50.
|
|
19
|
+
*/
|
|
20
|
+
export type Outcome = Side | 'TIE';
|
|
21
|
+
|
|
15
22
|
export declare const SIDES: readonly ['UP', 'DOWN'];
|
|
16
23
|
|
|
17
24
|
/** One level of resting depth: [price, size]. */
|
|
@@ -129,7 +136,7 @@ export interface Ctx<P = Record<string, unknown>> {
|
|
|
129
136
|
* one. Recorded for the cross-check panel, never enforced — a mismatch is
|
|
130
137
|
* information, not a failed run.
|
|
131
138
|
*/
|
|
132
|
-
assert_outcome(market: unknown, outcome:
|
|
139
|
+
assert_outcome(market: unknown, outcome: Outcome): void;
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
/**
|
|
@@ -207,7 +214,7 @@ export declare class Order {
|
|
|
207
214
|
* onTick(ctx: Ctx, tick: Tick): Order | null
|
|
208
215
|
* onBook(ctx: Ctx, book: BookView): Order | null
|
|
209
216
|
* onTrade(ctx: Ctx, trade: Tick): Order | null
|
|
210
|
-
* onSettle(ctx: Ctx, market: Market, outcome:
|
|
217
|
+
* onSettle(ctx: Ctx, market: Market, outcome: Outcome): void
|
|
211
218
|
*/
|
|
212
219
|
export declare class Strategy<P = Record<string, unknown>> {
|
|
213
220
|
/** Params from the manifest, injected by the runner before the first hook. */
|
package/package.json
CHANGED
|
@@ -12,8 +12,24 @@
|
|
|
12
12
|
|
|
13
13
|
import { matchOrder, isSide } from './book.mjs';
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* A market that settled 50:50. Predict.fun resolves end_price == start_price
|
|
17
|
+
* this way: both outcome tokens pay half a dollar.
|
|
18
|
+
*/
|
|
19
|
+
export const OUTCOME_TIE = 'TIE';
|
|
20
|
+
|
|
21
|
+
/** Every official outcome a market can settle on. */
|
|
22
|
+
export const OUTCOMES = Object.freeze(['UP', 'DOWN', OUTCOME_TIE]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Settlement value of one contract, given the official outcome.
|
|
26
|
+
*
|
|
27
|
+
* MUST MATCH otengine.py `contract_value`, and `scripts/audit-report.py`.
|
|
28
|
+
*/
|
|
29
|
+
export const contractValue = (side, outcome) => {
|
|
30
|
+
if (outcome === OUTCOME_TIE) return 0.5;
|
|
31
|
+
return outcome === side ? 1 : 0;
|
|
32
|
+
};
|
|
17
33
|
|
|
18
34
|
const EPS = 1e-9;
|
|
19
35
|
|
package/runner/engine/report.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// Nothing in this module can see the strategy. It reads the trade and fill logs
|
|
9
9
|
// the engine produced, so a report cannot be tuned by the thing it is judging.
|
|
10
10
|
|
|
11
|
+
import { contractValue } from './portfolio.mjs';
|
|
12
|
+
|
|
11
13
|
/** Entry-price buckets for the calibration panel. */
|
|
12
14
|
export const CALIBRATION_BUCKETS = Object.freeze([
|
|
13
15
|
[0.0, 0.1], [0.1, 0.2], [0.2, 0.3], [0.3, 0.4], [0.4, 0.5],
|
|
@@ -181,10 +183,11 @@ export function metrics(trades, { feesPaid = 0, days = 1 } = {}) {
|
|
|
181
183
|
/**
|
|
182
184
|
* Mean realised edge per contract, in dollars.
|
|
183
185
|
*
|
|
184
|
-
* A binary token bought at p is worth 1 if its side settles and 0 otherwise
|
|
185
|
-
*
|
|
186
|
-
* outcome
|
|
187
|
-
* truth, and is excluded rather than
|
|
186
|
+
* A binary token bought at p is worth 1 if its side settles and 0 otherwise
|
|
187
|
+
* (0.5 on a TIE — `contractValue`), so the edge on one contract is
|
|
188
|
+
* (outcome - p). Only settled trades carry an outcome; a trade closed early is
|
|
189
|
+
* edge against the market, not against the truth, and is excluded rather than
|
|
190
|
+
* scored as if it had settled.
|
|
188
191
|
*/
|
|
189
192
|
export function edgePerContract(trades) {
|
|
190
193
|
const settled = trades.filter((t) => t.how === 'settled' && t.entry_px != null && t.outcome);
|
|
@@ -192,8 +195,7 @@ export function edgePerContract(trades) {
|
|
|
192
195
|
let contracts = 0;
|
|
193
196
|
let edge = 0;
|
|
194
197
|
for (const t of settled) {
|
|
195
|
-
|
|
196
|
-
edge += (won - t.entry_px) * t.size;
|
|
198
|
+
edge += (contractValue(t.side, t.outcome) - t.entry_px) * t.size;
|
|
197
199
|
contracts += t.size;
|
|
198
200
|
}
|
|
199
201
|
return contracts > 0 ? edge / contracts : 0;
|
|
@@ -207,8 +209,7 @@ export function brier(trades) {
|
|
|
207
209
|
const settled = trades.filter((t) => t.how === 'settled' && t.entry_px != null && t.outcome);
|
|
208
210
|
if (!settled.length) return null;
|
|
209
211
|
return mean(settled.map((t) => {
|
|
210
|
-
|
|
211
|
-
return (t.entry_px - won) ** 2;
|
|
212
|
+
return (t.entry_px - contractValue(t.side, t.outcome)) ** 2;
|
|
212
213
|
}));
|
|
213
214
|
}
|
|
214
215
|
|
|
@@ -268,7 +269,7 @@ export function calibration(trades) {
|
|
|
268
269
|
const inBucket = settled.filter((t) => t.entry_px >= lo && t.entry_px < hi);
|
|
269
270
|
if (!inBucket.length) return null;
|
|
270
271
|
const implied = mean(inBucket.map((t) => t.entry_px));
|
|
271
|
-
const realized = mean(inBucket.map((t) => (t.
|
|
272
|
+
const realized = mean(inBucket.map((t) => contractValue(t.side, t.outcome)));
|
|
272
273
|
return {
|
|
273
274
|
bucket: `${lo.toFixed(2)}-${hi.toFixed(2)}`,
|
|
274
275
|
lo,
|
|
@@ -298,8 +299,8 @@ export function baselines(marketSummaries, { size = 1 } = {}) {
|
|
|
298
299
|
const out = { always_up: 0, always_down: 0, always_favourite: 0 };
|
|
299
300
|
for (const m of marketSummaries) {
|
|
300
301
|
if (!m.outcome || m.up_px == null || m.down_px == null) continue;
|
|
301
|
-
out.always_up += ((
|
|
302
|
-
out.always_down += ((
|
|
302
|
+
out.always_up += (contractValue('UP', m.outcome) - m.up_px) * size;
|
|
303
|
+
out.always_down += (contractValue('DOWN', m.outcome) - m.down_px) * size;
|
|
303
304
|
// The favourite is the side the market thinks is MORE likely, and on a
|
|
304
305
|
// binary market the price IS the implied probability — so it is the DEARER
|
|
305
306
|
// side, not the cheaper one. This was inverted: the panel labelled "always
|
|
@@ -307,7 +308,7 @@ export function baselines(marketSummaries, { size = 1 } = {}) {
|
|
|
307
308
|
// handed customers a backwards comparison to judge their strategy against.
|
|
308
309
|
const favSide = m.up_px >= m.down_px ? 'UP' : 'DOWN';
|
|
309
310
|
const favPx = Math.max(m.up_px, m.down_px);
|
|
310
|
-
out.always_favourite += ((m.outcome
|
|
311
|
+
out.always_favourite += (contractValue(favSide, m.outcome) - favPx) * size;
|
|
311
312
|
}
|
|
312
313
|
return {
|
|
313
314
|
always_up: r2(out.always_up),
|
package/runner/events.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// The lesson this is applying is the one runner/conformance already enforces
|
|
8
8
|
// for the two engines: two implementations of the same rules drift, and the
|
|
9
|
-
// drift is silent. "The identical files,
|
|
9
|
+
// drift is silent. "The identical files, byte for byte, with the same checksums"
|
|
10
10
|
// is a published promise about `ot run` versus a queued run — it cannot be true
|
|
11
11
|
// if local and remote decode the archive differently.
|
|
12
12
|
|
|
@@ -14,6 +14,7 @@ import { classifyPath } from '../api/lib/data-taxonomy.mjs';
|
|
|
14
14
|
import { resolveSettlementStream, degradingCoverage, inputKeys } from '../api/lib/backtest-datasets.mjs';
|
|
15
15
|
import { bookThrottleMs } from '../api/lib/backtest-contract.mjs';
|
|
16
16
|
import { Book } from './engine/book.mjs';
|
|
17
|
+
import { OUTCOME_TIE, OUTCOMES } from './engine/portfolio.mjs';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Coerce a field to a number, or null.
|
|
@@ -146,16 +147,33 @@ function predictRecord(row) {
|
|
|
146
147
|
const closeMs = num(row.end_sec) == null ? null : num(row.end_sec) * 1000;
|
|
147
148
|
const start = num(row.start_price);
|
|
148
149
|
const end = num(row.end_price);
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
// `end_price` is the settlement marker, NOT `status`.
|
|
151
|
+
//
|
|
152
|
+
// This used to require status === 'RESOLVED', on the theory that an OPEN
|
|
153
|
+
// market's prices are live quotes rather than a settlement. Both halves were
|
|
154
|
+
// wrong. `end_price` is written only by the settlement backfill, which is
|
|
155
|
+
// driven by `end_price IS NULL` and drops a market from its re-read queue the
|
|
156
|
+
// moment it lands — so `status` is whatever the venue last said while we were
|
|
157
|
+
// still polling, and it freezes there. There is no row in the whole table
|
|
158
|
+
// with status RESOLVED and no end_price, so the implication runs one way
|
|
159
|
+
// only: end_price present ⇒ settled.
|
|
160
|
+
//
|
|
161
|
+
// MEASURED 2026-09-08, all assets: 1245 markets, every one of them with an
|
|
162
|
+
// end_price, but only 1104 said RESOLVED. The check discarded 39/873 of the
|
|
163
|
+
// 5-minute markets (4.5%) and 21/291 of the 15-minute ones (7.2%) — reported
|
|
164
|
+
// to the customer as "outcome could not be read" while the outcome was right
|
|
165
|
+
// there, and billed for, because a market-day charges whole.
|
|
166
|
+
//
|
|
167
|
+
// A TIE IS ITS OWN OUTCOME. Predict.fun settles end_price == start_price
|
|
168
|
+
// 50:50 — every UP and every DOWN contract pays $0.50 (`contractValue`).
|
|
169
|
+
// It used to be dropped because the engines only knew UP/DOWN, which cost the
|
|
170
|
+
// customer 0.53–1.20% of the 5-minute markets they paid for, reported as
|
|
171
|
+
// "outcome could not be read". Never "fix" a tie by picking a side: half of
|
|
172
|
+
// them would be scored backwards.
|
|
173
|
+
let outcome = null;
|
|
174
|
+
if (start != null && end != null) {
|
|
175
|
+
outcome = end > start ? 'UP' : end < start ? 'DOWN' : OUTCOME_TIE;
|
|
176
|
+
}
|
|
159
177
|
return {
|
|
160
178
|
market_id: String(row.market_id ?? row.condition_id ?? ''),
|
|
161
179
|
slug: row.category_slug ?? null,
|
|
@@ -582,7 +600,7 @@ export function buildSlugIndex(markets) {
|
|
|
582
600
|
* The coverage block, in ONE shape.
|
|
583
601
|
*
|
|
584
602
|
* The docs promise a local run and a queued run produce "the identical files,
|
|
585
|
-
*
|
|
603
|
+
* byte for byte, with the same checksums". They already shared the decoder and
|
|
586
604
|
* the feed list; the coverage object was still built twice, so `ot run` emitted
|
|
587
605
|
* five keys where the queue emitted ten, and anyone diffing the two saw a
|
|
588
606
|
* schema difference rather than an answer. A field a local run genuinely cannot
|
|
@@ -834,7 +852,7 @@ export function marketUnusable(market, inWindow) {
|
|
|
834
852
|
// nothing to merge — replayed it out of the cache anyway.
|
|
835
853
|
if (!market.asset) return 'market has no asset';
|
|
836
854
|
if (market.stream == null) return 'settlement stream could not be resolved';
|
|
837
|
-
if (
|
|
855
|
+
if (!OUTCOMES.includes(market.outcome)) {
|
|
838
856
|
return 'outcome could not be read';
|
|
839
857
|
}
|
|
840
858
|
if (!inWindow || inWindow.length === 0) return 'no events inside the market window';
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
export type Side = 'UP' | 'DOWN';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* How a market settled. `'TIE'` is a 50:50 settlement — Predict.fun resolves
|
|
17
|
+
* an end price equal to the start price this way, and every UP and DOWN
|
|
18
|
+
* contract pays $0.50.
|
|
19
|
+
*/
|
|
20
|
+
export type Outcome = Side | 'TIE';
|
|
21
|
+
|
|
15
22
|
export declare const SIDES: readonly ['UP', 'DOWN'];
|
|
16
23
|
|
|
17
24
|
/** One level of resting depth: [price, size]. */
|
|
@@ -129,7 +136,7 @@ export interface Ctx<P = Record<string, unknown>> {
|
|
|
129
136
|
* one. Recorded for the cross-check panel, never enforced — a mismatch is
|
|
130
137
|
* information, not a failed run.
|
|
131
138
|
*/
|
|
132
|
-
assert_outcome(market: unknown, outcome:
|
|
139
|
+
assert_outcome(market: unknown, outcome: Outcome): void;
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
/**
|
|
@@ -207,7 +214,7 @@ export declare class Order {
|
|
|
207
214
|
* onTick(ctx: Ctx, tick: Tick): Order | null
|
|
208
215
|
* onBook(ctx: Ctx, book: BookView): Order | null
|
|
209
216
|
* onTrade(ctx: Ctx, trade: Tick): Order | null
|
|
210
|
-
* onSettle(ctx: Ctx, market: Market, outcome:
|
|
217
|
+
* onSettle(ctx: Ctx, market: Market, outcome: Outcome): void
|
|
211
218
|
*/
|
|
212
219
|
export declare class Strategy<P = Record<string, unknown>> {
|
|
213
220
|
/** Params from the manifest, injected by the runner before the first hook. */
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// untrusted output — the container is the boundary, not the harness.
|
|
7
7
|
//
|
|
8
8
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
9
|
+
import { OUTCOMES } from '../engine/portfolio.mjs';
|
|
9
10
|
|
|
10
11
|
// This is also the reason the REPORT is not computed inside. Metrics,
|
|
11
12
|
// calibration, latency and slippage are all derived outside, in one shared
|
|
@@ -159,7 +160,7 @@ export function parseTrade(raw) {
|
|
|
159
160
|
opened_ms: finite(raw.opened_ms) ? raw.opened_ms : null,
|
|
160
161
|
closed_ms: finite(raw.closed_ms) ? raw.closed_ms : null,
|
|
161
162
|
how: typeof raw.how === 'string' ? raw.how : 'exit',
|
|
162
|
-
outcome:
|
|
163
|
+
outcome: OUTCOMES.includes(raw.outcome) ? raw.outcome : undefined,
|
|
163
164
|
};
|
|
164
165
|
}
|
|
165
166
|
|
|
@@ -232,7 +233,7 @@ export function parseResult(raw) {
|
|
|
232
233
|
market_id: typeof m.market_id === 'string' ? m.market_id : null,
|
|
233
234
|
asset: typeof m.asset === 'string' ? m.asset : null,
|
|
234
235
|
interval: typeof m.interval === 'string' ? m.interval : null,
|
|
235
|
-
outcome:
|
|
236
|
+
outcome: OUTCOMES.includes(m.outcome) ? m.outcome : null,
|
|
236
237
|
up_px: finite(m.up_px) ? m.up_px : null,
|
|
237
238
|
down_px: finite(m.down_px) ? m.down_px : null,
|
|
238
239
|
stream: typeof m.stream === 'string' ? m.stream : null,
|
|
@@ -321,7 +321,14 @@ def match_order(book: Book, order: dict) -> dict:
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
|
|
324
|
-
|
|
324
|
+
OUTCOME_TIE = "TIE"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def contract_value(side: str, outcome: str) -> float:
|
|
328
|
+
# A tie settles 50:50: both outcome tokens pay half a dollar. MUST MATCH
|
|
329
|
+
# portfolio.mjs `contractValue`.
|
|
330
|
+
if outcome == OUTCOME_TIE:
|
|
331
|
+
return 0.5
|
|
325
332
|
return 1 if outcome == side else 0
|
|
326
333
|
|
|
327
334
|
|