@coinrithm/mcp-trading 0.7.15 → 0.7.17
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/CHANGELOG.md +885 -692
- package/README.md +486 -400
- package/dist/agent/bench/bench.d.ts +59 -0
- package/dist/agent/bench/bench.js +319 -0
- package/dist/agent/bench/cassette.d.ts +97 -0
- package/dist/agent/bench/cassette.js +181 -0
- package/dist/agent/bench/costs.d.ts +28 -0
- package/dist/agent/bench/costs.js +60 -0
- package/dist/agent/bench/labelBuilder.d.ts +123 -0
- package/dist/agent/bench/labelBuilder.js +282 -0
- package/dist/agent/bench/labels.d.ts +75 -0
- package/dist/agent/bench/labels.js +259 -0
- package/dist/agent/bench/recordingClient.d.ts +51 -0
- package/dist/agent/bench/recordingClient.js +193 -0
- package/dist/agent/bench/replayClient.d.ts +41 -0
- package/dist/agent/bench/replayClient.js +262 -0
- package/dist/agent/bench/report.d.ts +115 -0
- package/dist/agent/bench/report.js +435 -0
- package/dist/agent/capitalSizing.d.ts +10 -1
- package/dist/agent/capitalSizing.js +28 -4
- package/dist/agent/cli.d.ts +25 -0
- package/dist/agent/cli.js +300 -2
- package/dist/agent/client.d.ts +14 -0
- package/dist/agent/client.js +23 -6
- package/dist/agent/configurationWarnings.js +11 -0
- package/dist/agent/decision.d.ts +24 -16
- package/dist/agent/decision.js +92 -3
- package/dist/agent/decisionValidator.d.ts +31 -0
- package/dist/agent/decisionValidator.js +148 -3
- package/dist/agent/engine.d.ts +5 -3
- package/dist/agent/engine.js +5 -3
- package/dist/agent/futuresCloseOutcome.d.ts +19 -0
- package/dist/agent/futuresCloseOutcome.js +54 -0
- package/dist/agent/futuresEligibility.d.ts +13 -0
- package/dist/agent/futuresEligibility.js +43 -5
- package/dist/agent/observe.d.ts +28 -2
- package/dist/agent/observe.js +530 -90
- package/dist/agent/pmPaperCandidates.d.ts +17 -0
- package/dist/agent/pmPaperCandidates.js +125 -0
- package/dist/agent/pmPaperCycle.d.ts +16 -0
- package/dist/agent/pmPaperCycle.js +372 -0
- package/dist/agent/pmPaperDecision.d.ts +187 -0
- package/dist/agent/pmPaperDecision.js +101 -0
- package/dist/agent/pmPaperPolicy.d.ts +71 -0
- package/dist/agent/pmPaperPolicy.js +46 -0
- package/dist/agent/prompt.d.ts +15 -1
- package/dist/agent/prompt.js +187 -9
- package/dist/agent/promptTables.d.ts +1 -0
- package/dist/agent/promptTables.js +33 -0
- package/dist/agent/providerCapabilities.d.ts +7 -0
- package/dist/agent/providerCapabilities.js +27 -1
- package/dist/agent/providers.d.ts +26 -7
- package/dist/agent/providers.js +193 -48
- package/dist/agent/resolve.js +3 -0
- package/dist/agent/runner.d.ts +17 -1
- package/dist/agent/runner.js +210 -38
- package/dist/agent/setups.d.ts +2 -1
- package/dist/agent/setups.js +17 -16
- package/dist/agent/signals.d.ts +33 -0
- package/dist/agent/signals.js +89 -0
- package/dist/agent/skill.js +46 -0
- package/dist/agent/skillValidator.js +56 -0
- package/dist/agent/strictLint.js +30 -0
- package/dist/agent/templates.js +30 -30
- package/dist/agent/types.d.ts +180 -12
- package/dist/agent/universe.d.ts +78 -0
- package/dist/agent/universe.js +215 -0
- package/dist/agent/version.d.ts +1 -1
- package/dist/agent/version.js +1 -1
- package/dist/client.d.ts +14 -0
- package/dist/client.js +31 -1
- package/dist/http.js +1 -1
- package/dist/pmPaperV2.d.ts +81 -0
- package/dist/pmPaperV2.js +53 -0
- package/dist/tools.js +84 -6
- package/package.json +100 -97
package/dist/agent/decision.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { parsePmPaperDecision } from "./pmPaperDecision.js";
|
|
1
2
|
// Parse the model's single text response into a strict, structured Decision.
|
|
2
3
|
// Accepts futures + spot + prediction-market actions. Anything else — invalid
|
|
3
4
|
// JSON, an unknown action type, a free-form endpoint/tool name, extra unknown
|
|
4
5
|
// fields, or a missing required field — fails closed (the runner skips).
|
|
5
6
|
import { z } from "zod";
|
|
7
|
+
import JSON5 from "json5";
|
|
6
8
|
import { coerceThesis } from "./thesis.js";
|
|
7
9
|
// Small models (especially Llama 3.1 8B) frequently emit numbers as JSON strings
|
|
8
10
|
// ("12345", "0.8", "62000"). Coerce a *clean* numeric string to a number before
|
|
@@ -363,15 +365,95 @@ function coerceJson(text) {
|
|
|
363
365
|
}
|
|
364
366
|
return JSON.parse(s); // throws on invalid JSON -> caller treats as fail-closed
|
|
365
367
|
}
|
|
368
|
+
// Fixed, non-content diagnostics only. Never retain model text, identifiers,
|
|
369
|
+
// action values or reasoning to investigate a provider's encoding failures.
|
|
370
|
+
export const ACTIONS_STRING_DIAGNOSTICS = [
|
|
371
|
+
"json_array_empty_valid_decision",
|
|
372
|
+
"json_array_nonempty_valid_decision",
|
|
373
|
+
"json_array_invalid_decision",
|
|
374
|
+
"json_nonarray",
|
|
375
|
+
"invalid_json",
|
|
376
|
+
"empty_string_valid_skip",
|
|
377
|
+
"empty_string_other_decision",
|
|
378
|
+
"non_json_array_text",
|
|
379
|
+
"json5_array_valid_decision",
|
|
380
|
+
"json5_array_invalid_decision",
|
|
381
|
+
"over_size_limit",
|
|
382
|
+
];
|
|
383
|
+
function diagnoseActionsString(obj) {
|
|
384
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj))
|
|
385
|
+
return undefined;
|
|
386
|
+
const record = obj;
|
|
387
|
+
if (typeof record.actions !== "string")
|
|
388
|
+
return undefined;
|
|
389
|
+
if (record.actions.length > 65_536)
|
|
390
|
+
return "over_size_limit";
|
|
391
|
+
const text = record.actions.trim();
|
|
392
|
+
if (text === "") {
|
|
393
|
+
return record.decision === "skip" &&
|
|
394
|
+
decisionSchema.safeParse({ ...record, actions: [] }).success
|
|
395
|
+
? "empty_string_valid_skip"
|
|
396
|
+
: "empty_string_other_decision";
|
|
397
|
+
}
|
|
398
|
+
let actions;
|
|
399
|
+
try {
|
|
400
|
+
actions = JSON.parse(record.actions);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
if (!text.startsWith("["))
|
|
404
|
+
return "invalid_json";
|
|
405
|
+
// Diagnostic only: identify alternate literal encodings without accepting
|
|
406
|
+
// them or retaining content. JSON5 never evaluates expressions. Requiring
|
|
407
|
+
// finite JSON values keeps Infinity/NaN from looking like usable actions.
|
|
408
|
+
try {
|
|
409
|
+
const candidate = JSON5.parse(text);
|
|
410
|
+
JSON.stringify(candidate, (key, value) => {
|
|
411
|
+
if (["__proto__", "constructor", "prototype"].includes(key) ||
|
|
412
|
+
(typeof value === "number" && !Number.isFinite(value)))
|
|
413
|
+
throw new Error("Unsafe diagnostic value");
|
|
414
|
+
return value;
|
|
415
|
+
});
|
|
416
|
+
return Array.isArray(candidate) &&
|
|
417
|
+
decisionSchema.safeParse({ ...record, actions: candidate }).success
|
|
418
|
+
? "json5_array_valid_decision"
|
|
419
|
+
: "json5_array_invalid_decision";
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return "non_json_array_text";
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!Array.isArray(actions))
|
|
426
|
+
return "json_nonarray";
|
|
427
|
+
if (!decisionSchema.safeParse({ ...record, actions }).success)
|
|
428
|
+
return "json_array_invalid_decision";
|
|
429
|
+
return actions.length === 0
|
|
430
|
+
? "json_array_empty_valid_decision"
|
|
431
|
+
: "json_array_nonempty_valid_decision";
|
|
432
|
+
}
|
|
433
|
+
function safeIssueMessage(issue) {
|
|
434
|
+
// These Zod messages echo model-supplied values or unknown field names.
|
|
435
|
+
// Other messages in this fixed schema describe types or contract limits.
|
|
436
|
+
switch (issue.code) {
|
|
437
|
+
case "invalid_enum_value":
|
|
438
|
+
return "Invalid enum value";
|
|
439
|
+
case "invalid_literal":
|
|
440
|
+
return "Invalid literal value";
|
|
441
|
+
case "unrecognized_keys":
|
|
442
|
+
return "Unrecognized field(s)";
|
|
443
|
+
default:
|
|
444
|
+
return issue.message;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
366
447
|
export function parseDecision(text) {
|
|
367
448
|
let obj;
|
|
368
449
|
try {
|
|
369
450
|
obj = normalizeDecisionVerb(coerceJson(text));
|
|
370
451
|
}
|
|
371
|
-
catch
|
|
452
|
+
catch {
|
|
372
453
|
return {
|
|
373
454
|
ok: false,
|
|
374
|
-
|
|
455
|
+
// Node's JSON syntax errors can include excerpts of private model text.
|
|
456
|
+
error: "model output is not valid JSON",
|
|
375
457
|
};
|
|
376
458
|
}
|
|
377
459
|
const res = decisionSchema.safeParse(obj);
|
|
@@ -379,8 +461,9 @@ export function parseDecision(text) {
|
|
|
379
461
|
return {
|
|
380
462
|
ok: false,
|
|
381
463
|
error: res.error.issues
|
|
382
|
-
.map((i) => `${i.path.join(".") || "(root)"}: ${i
|
|
464
|
+
.map((i) => `${i.path.join(".") || "(root)"}: ${safeIssueMessage(i)}`)
|
|
383
465
|
.join("; "),
|
|
466
|
+
actionsStringDiagnostic: diagnoseActionsString(obj),
|
|
384
467
|
};
|
|
385
468
|
}
|
|
386
469
|
const d = res.data;
|
|
@@ -397,3 +480,9 @@ export function parseDecision(text) {
|
|
|
397
480
|
},
|
|
398
481
|
};
|
|
399
482
|
}
|
|
483
|
+
/** Only the trusted host selects a versioned contract; default stays legacy. */
|
|
484
|
+
export function parseDecisionForContract(text, contract) {
|
|
485
|
+
return contract === "pm_paper_v2"
|
|
486
|
+
? parsePmPaperDecision(text)
|
|
487
|
+
: parseDecision(text);
|
|
488
|
+
}
|
|
@@ -20,7 +20,38 @@ export interface DecisionContext {
|
|
|
20
20
|
realizedLossTodayMusd: number;
|
|
21
21
|
targetedPositionIds: number[];
|
|
22
22
|
targetedOrderIds: number[];
|
|
23
|
+
pmEventsOpenedThisCycle?: string[];
|
|
24
|
+
pmCoinHorizonsOpenedThisCycle?: PmCoinHorizon[];
|
|
25
|
+
nowMs?: number;
|
|
23
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Edge (points) risk.pmMinEdgeGapPct requires at a fee-inclusive entry cost:
|
|
29
|
+
* pct% of the room left to 100. Cost at or above 100 leaves no room, so any
|
|
30
|
+
* positive gap rule makes the requirement unreachable rather than zero.
|
|
31
|
+
*/
|
|
32
|
+
export declare function requiredGapEdgePoints(entryPct: number, gapPct: number): number;
|
|
33
|
+
/** Ends within this of each other share a coin horizon (venue date splits). */
|
|
34
|
+
export declare const PM_COIN_HORIZON_WINDOW_MS: number;
|
|
35
|
+
/** An end beyond this is a venue sentinel, never a real horizon. */
|
|
36
|
+
export declare const PM_PLAUSIBLE_END_MAX_MS: number;
|
|
37
|
+
export interface PmCoinHorizon {
|
|
38
|
+
coins: string[];
|
|
39
|
+
endMs: number;
|
|
40
|
+
}
|
|
41
|
+
/** The later of observation.asOf and the validation clock, when known. */
|
|
42
|
+
export declare function pmReferenceMs(asOf: string, nowMs?: number): number;
|
|
43
|
+
/**
|
|
44
|
+
* The coin horizon of a market or held position, or null when it cannot be
|
|
45
|
+
* grouped: no linked coin, or an end that is missing, unparseable, not after
|
|
46
|
+
* the reference time, or more than 5 years after it (venue sentinels).
|
|
47
|
+
*/
|
|
48
|
+
export declare function pmCoinHorizonOf(item: {
|
|
49
|
+
coins?: string[];
|
|
50
|
+
endDate?: string;
|
|
51
|
+
}, refMs: number): PmCoinHorizon | null;
|
|
52
|
+
/** Event key shared by pm_open actions, held positions and markets. */
|
|
53
|
+
export declare function pmEventKey(source: string, slug: string): string;
|
|
54
|
+
export declare const TRIGGER_PRICE_EPS = 1e-8;
|
|
24
55
|
/**
|
|
25
56
|
* True when a thesis says, in so many words, that it is betting AGAINST the
|
|
26
57
|
* outcome the action is buying. Deliberately narrow: it fires only when the
|
|
@@ -4,6 +4,46 @@
|
|
|
4
4
|
// market text cannot widen a limit or force a trade. Covers futures + spot + PM.
|
|
5
5
|
import { ok, fail, actionVenue, spotBuyCost, } from "./types.js";
|
|
6
6
|
import { checkEntryPredicates } from "./entryPredicates.js";
|
|
7
|
+
import { universeEntryBlock } from "./universe.js";
|
|
8
|
+
/**
|
|
9
|
+
* Edge (points) risk.pmMinEdgeGapPct requires at a fee-inclusive entry cost:
|
|
10
|
+
* pct% of the room left to 100. Cost at or above 100 leaves no room, so any
|
|
11
|
+
* positive gap rule makes the requirement unreachable rather than zero.
|
|
12
|
+
*/
|
|
13
|
+
export function requiredGapEdgePoints(entryPct, gapPct) {
|
|
14
|
+
const room = 100 - entryPct;
|
|
15
|
+
if (room <= 0)
|
|
16
|
+
return gapPct > 0 ? Number.POSITIVE_INFINITY : 0;
|
|
17
|
+
return (gapPct / 100) * room;
|
|
18
|
+
}
|
|
19
|
+
/** Ends within this of each other share a coin horizon (venue date splits). */
|
|
20
|
+
export const PM_COIN_HORIZON_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
21
|
+
/** An end beyond this is a venue sentinel, never a real horizon. */
|
|
22
|
+
export const PM_PLAUSIBLE_END_MAX_MS = 5 * 365 * 24 * 60 * 60 * 1000;
|
|
23
|
+
/** The later of observation.asOf and the validation clock, when known. */
|
|
24
|
+
export function pmReferenceMs(asOf, nowMs) {
|
|
25
|
+
return Math.max(...[Date.parse(asOf), nowMs ?? NaN].filter((t) => Number.isFinite(t)));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The coin horizon of a market or held position, or null when it cannot be
|
|
29
|
+
* grouped: no linked coin, or an end that is missing, unparseable, not after
|
|
30
|
+
* the reference time, or more than 5 years after it (venue sentinels).
|
|
31
|
+
*/
|
|
32
|
+
export function pmCoinHorizonOf(item, refMs) {
|
|
33
|
+
const coins = item.coins ?? [];
|
|
34
|
+
if (coins.length === 0 || !item.endDate || !Number.isFinite(refMs))
|
|
35
|
+
return null;
|
|
36
|
+
const endMs = Date.parse(item.endDate);
|
|
37
|
+
if (!Number.isFinite(endMs) ||
|
|
38
|
+
endMs <= refMs ||
|
|
39
|
+
endMs - refMs > PM_PLAUSIBLE_END_MAX_MS)
|
|
40
|
+
return null;
|
|
41
|
+
return { coins, endMs };
|
|
42
|
+
}
|
|
43
|
+
/** Event key shared by pm_open actions, held positions and markets. */
|
|
44
|
+
export function pmEventKey(source, slug) {
|
|
45
|
+
return `${source.toLowerCase()}/${slug.toLowerCase()}`;
|
|
46
|
+
}
|
|
7
47
|
const SERVER_MAX_LEVERAGE = 20;
|
|
8
48
|
// Minimum edge, in probability POINTS, between the model's own forecast for
|
|
9
49
|
// the outcome it is backing and what that outcome currently costs. Live
|
|
@@ -20,7 +60,7 @@ const PM_MIN_STAKE_MUSD = 10; // server minimum prediction-market stake
|
|
|
20
60
|
// Match backend-v2 validateSlTpPrices, including its boundary tolerance. This
|
|
21
61
|
// preflight uses observed evidence only; the API still checks a fresh mark and
|
|
22
62
|
// locked position at execution. Older observations can omit these prices.
|
|
23
|
-
const TRIGGER_PRICE_EPS = 1e-8;
|
|
63
|
+
export const TRIGGER_PRICE_EPS = 1e-8;
|
|
24
64
|
function validateTriggerUpdate(action, pos) {
|
|
25
65
|
// executeAction omits null as well as undefined; neither clears a trigger in
|
|
26
66
|
// this runner. Check the resulting state, including retained protection.
|
|
@@ -151,6 +191,9 @@ export function validateAction(action, ctx) {
|
|
|
151
191
|
return fail("unknown_symbol", `${action.symbol} is not on the watchlist`);
|
|
152
192
|
if (!entry.coinId)
|
|
153
193
|
return fail("unresolved_symbol", `${action.symbol} did not resolve to a coin`);
|
|
194
|
+
const outsideUniverse = universeEntryBlock(spec, entry);
|
|
195
|
+
if (outsideUniverse)
|
|
196
|
+
return fail("outside_universe", `${action.symbol} is ${outsideUniverse}`);
|
|
154
197
|
// A futures_open on a symbol you ALREADY hold is treated as an ADD by the
|
|
155
198
|
// server, which REJECTS any SL/TP on an add (sl_tp_not_supported_on_add) and
|
|
156
199
|
// fails the whole open — the single biggest rejection class (e.g. Sam 61/61).
|
|
@@ -267,6 +310,11 @@ export function validateAction(action, ctx) {
|
|
|
267
310
|
return fail("unknown_symbol", `${action.symbol} is not on the watchlist`);
|
|
268
311
|
if (!entry.coinId)
|
|
269
312
|
return fail("unresolved_symbol", `${action.symbol} did not resolve to a coin`);
|
|
313
|
+
if (action.side === "buy") {
|
|
314
|
+
const outside = universeEntryBlock(spec, entry);
|
|
315
|
+
if (outside)
|
|
316
|
+
return fail("outside_universe", `${action.symbol} is ${outside}`);
|
|
317
|
+
}
|
|
270
318
|
if (action.orderType === "limit" &&
|
|
271
319
|
!(typeof action.limitPrice === "number" && action.limitPrice > 0)) {
|
|
272
320
|
return fail("missing_limit_price", "a limit order needs a positive limitPrice");
|
|
@@ -333,6 +381,68 @@ export function validateAction(action, ctx) {
|
|
|
333
381
|
if (!mkt) {
|
|
334
382
|
return fail("pm_market_not_discovered", `${action.source}/${action.slug} is not in the discovered PM markets`);
|
|
335
383
|
}
|
|
384
|
+
const perEventCap = spec.risk.pmMaxOpenPerEvent;
|
|
385
|
+
if (perEventCap !== undefined) {
|
|
386
|
+
if (typeof perEventCap !== "number" ||
|
|
387
|
+
!Number.isInteger(perEventCap) ||
|
|
388
|
+
perEventCap < 1) {
|
|
389
|
+
return fail("pm_event_cap_invalid", `risk.pmMaxOpenPerEvent ${JSON.stringify(perEventCap)} is not a whole number of at least 1`);
|
|
390
|
+
}
|
|
391
|
+
const key = pmEventKey(mkt.source, mkt.slug);
|
|
392
|
+
const held = observation.pmPositions.filter((p) => (p.status ?? "open") === "open" &&
|
|
393
|
+
p.source !== undefined &&
|
|
394
|
+
p.slug !== undefined &&
|
|
395
|
+
pmEventKey(p.source, p.slug) === key).length;
|
|
396
|
+
const thisCycle = (ctx.pmEventsOpenedThisCycle ?? []).filter((k) => k === key).length;
|
|
397
|
+
if (held + thisCycle >= perEventCap) {
|
|
398
|
+
return fail("pm_event_cap", `${held + thisCycle} open bet(s) on ${key} >= per-event cap ${perEventCap}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const coinHorizonCap = spec.risk.pmMaxOpenPerCoinHorizon;
|
|
402
|
+
if (coinHorizonCap !== undefined) {
|
|
403
|
+
if (typeof coinHorizonCap !== "number" ||
|
|
404
|
+
!Number.isInteger(coinHorizonCap) ||
|
|
405
|
+
coinHorizonCap < 1 ||
|
|
406
|
+
coinHorizonCap > 50) {
|
|
407
|
+
return fail("pm_coin_horizon_cap_invalid", `risk.pmMaxOpenPerCoinHorizon ${JSON.stringify(coinHorizonCap)} is not a whole number from 1 to 50`);
|
|
408
|
+
}
|
|
409
|
+
const refMs = pmReferenceMs(observation.asOf, ctx.nowMs);
|
|
410
|
+
const candidate = pmCoinHorizonOf(mkt, refMs);
|
|
411
|
+
// Unknown coin or end: never grouped, never blocked.
|
|
412
|
+
if (candidate) {
|
|
413
|
+
const near = (endMs) => Math.abs(endMs - candidate.endMs) <= PM_COIN_HORIZON_WINDOW_MS;
|
|
414
|
+
for (const coin of candidate.coins) {
|
|
415
|
+
const held = observation.pmPositions.filter((p) => {
|
|
416
|
+
if ((p.status ?? "open") !== "open")
|
|
417
|
+
return false;
|
|
418
|
+
const h = pmCoinHorizonOf(p, refMs);
|
|
419
|
+
return !!h && h.coins.includes(coin) && near(h.endMs);
|
|
420
|
+
}).length;
|
|
421
|
+
const thisCycle = (ctx.pmCoinHorizonsOpenedThisCycle ?? []).filter((h) => h.coins.includes(coin) && near(h.endMs)).length;
|
|
422
|
+
if (held + thisCycle >= coinHorizonCap) {
|
|
423
|
+
return fail("pm_coin_horizon_cap", `${held + thisCycle} open bet(s) on ${coin} ending within 24 h of ${new Date(candidate.endMs).toISOString()} >= per-coin-horizon cap ${coinHorizonCap}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const closeCutoff = spec.risk.pmMinMinutesToClose;
|
|
429
|
+
if (closeCutoff !== undefined) {
|
|
430
|
+
if (typeof closeCutoff !== "number" ||
|
|
431
|
+
!Number.isFinite(closeCutoff) ||
|
|
432
|
+
closeCutoff < 0) {
|
|
433
|
+
return fail("pm_close_cutoff_invalid", `risk.pmMinMinutesToClose ${JSON.stringify(closeCutoff)} is not a non-negative number`);
|
|
434
|
+
}
|
|
435
|
+
// Only a KNOWN close can trip the cutoff: venues publish null/sentinel
|
|
436
|
+
// end dates, and an unparseable one is unknown, never "closing now".
|
|
437
|
+
const endMs = mkt.endDate ? Date.parse(mkt.endDate) : NaN;
|
|
438
|
+
const refMs = Math.max(...[Date.parse(observation.asOf), ctx.nowMs ?? NaN].filter((t) => Number.isFinite(t)));
|
|
439
|
+
if (Number.isFinite(endMs) && Number.isFinite(refMs)) {
|
|
440
|
+
const minutesLeft = (endMs - refMs) / 60_000;
|
|
441
|
+
if (minutesLeft < closeCutoff) {
|
|
442
|
+
return fail("pm_closes_too_soon", `market closes in ${minutesLeft.toFixed(1)} min, inside the ${closeCutoff} min cutoff`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
336
446
|
if (action.stakeMusd < PM_MIN_STAKE_MUSD) {
|
|
337
447
|
return fail("pm_stake_below_min", `stake ${action.stakeMusd} < $${PM_MIN_STAKE_MUSD} minimum`);
|
|
338
448
|
}
|
|
@@ -393,11 +503,37 @@ export function validateAction(action, ctx) {
|
|
|
393
503
|
// Forecast consistency. By prompt contract forecastProbability is the
|
|
394
504
|
// model's own probability (1-99) that the outcome IT IS BACKING wins, so
|
|
395
505
|
// buying that outcome only makes sense when the forecast clears what the
|
|
396
|
-
// market charges for it.
|
|
397
|
-
//
|
|
506
|
+
// market charges for it. A forecast is optional unless a configured edge
|
|
507
|
+
// rule requires one; a present forecast must satisfy the enforced rules.
|
|
398
508
|
// The API's entryProbability is the RAW mid, and executionModel's effective
|
|
399
509
|
// probability excludes fee. Total stake / net shares is the fee-inclusive
|
|
400
510
|
// break-even cost. Do not guess units or fall back to a discovery mid.
|
|
511
|
+
const maxEdge = spec.risk.pmMaxEdgePoints;
|
|
512
|
+
if (!ctx.mechanical && maxEdge !== undefined) {
|
|
513
|
+
if (typeof maxEdge !== "number" ||
|
|
514
|
+
!Number.isFinite(maxEdge) ||
|
|
515
|
+
maxEdge < 0 ||
|
|
516
|
+
maxEdge > 100) {
|
|
517
|
+
return fail("pm_max_edge_invalid", `risk.pmMaxEdgePoints ${JSON.stringify(maxEdge)} is not a number between 0 and 100`);
|
|
518
|
+
}
|
|
519
|
+
// An overconfidence cap cannot be checked against a missing number.
|
|
520
|
+
if (action.forecastProbability == null) {
|
|
521
|
+
return fail("pm_forecast_required", `risk.pmMaxEdgePoints ${maxEdge} needs forecastProbability on every pm_open`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const edgeGapPct = spec.risk.pmMinEdgeGapPct;
|
|
525
|
+
if (!ctx.mechanical && edgeGapPct !== undefined) {
|
|
526
|
+
if (typeof edgeGapPct !== "number" ||
|
|
527
|
+
!Number.isFinite(edgeGapPct) ||
|
|
528
|
+
edgeGapPct < 0 ||
|
|
529
|
+
edgeGapPct > 100) {
|
|
530
|
+
return fail("pm_edge_gap_invalid", `risk.pmMinEdgeGapPct ${JSON.stringify(edgeGapPct)} is not a number between 0 and 100`);
|
|
531
|
+
}
|
|
532
|
+
// An edge rule cannot be checked against a missing number.
|
|
533
|
+
if (action.forecastProbability == null) {
|
|
534
|
+
return fail("pm_forecast_required", `risk.pmMinEdgeGapPct ${edgeGapPct} needs forecastProbability on every pm_open`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
401
537
|
if (!ctx.mechanical && action.forecastProbability != null) {
|
|
402
538
|
const stake = ctx.quote.stakeMusd;
|
|
403
539
|
const shares = ctx.quote.sharesEstimate;
|
|
@@ -418,6 +554,15 @@ export function validateAction(action, ctx) {
|
|
|
418
554
|
if (edge + 1e-9 < PM_MIN_FORECAST_EDGE_POINTS) {
|
|
419
555
|
return fail("forecast_no_positive_edge", `forecast ${action.forecastProbability} vs entry ${entryPct.toFixed(1)} = ${edge.toFixed(1)}pt edge, under the ${PM_MIN_FORECAST_EDGE_POINTS}pt minimum`);
|
|
420
556
|
}
|
|
557
|
+
if (maxEdge !== undefined && edge > maxEdge + 1e-9) {
|
|
558
|
+
return fail("pm_edge_overconfident", `forecast ${action.forecastProbability} vs entry ${entryPct.toFixed(1)} = ${edge.toFixed(1)}pt edge, over the ${maxEdge}pt cap`);
|
|
559
|
+
}
|
|
560
|
+
if (edgeGapPct !== undefined) {
|
|
561
|
+
const required = requiredGapEdgePoints(entryPct, edgeGapPct);
|
|
562
|
+
if (edge + 1e-9 < required) {
|
|
563
|
+
return fail("pm_edge_below_gap_rule", `forecast ${action.forecastProbability} vs entry ${entryPct.toFixed(1)} = ${edge.toFixed(1)}pt edge, under the ${required.toFixed(1)}pt required by ${edgeGapPct}% of the gap to 100`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
421
566
|
}
|
|
422
567
|
if (thesisContradictsOutcome(action.thesis?.summary, mkt.outcomeName)) {
|
|
423
568
|
return fail("thesis_action_conflict", `thesis bets against ${mkt.outcomeName}, which this action buys`);
|
package/dist/agent/engine.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { runCycle, type RunnerDeps } from "./runner.js";
|
|
2
2
|
export { buildAgentDefinitionSnapshot, type AgentDefinitionSnapshot, } from "./definitionSnapshot.js";
|
|
3
3
|
export { DECISION_INPUT_MAX_BYTES, sanitizeDecisionInputRecord, type DecisionInputRecord, } from "./decisionReceipt.js";
|
|
4
|
-
export { selectProvider, providerForRoute, classifyProviderFailure, type ProviderEnv, type Provider, type DecideInput, type DecideResult, type DecideRouteAttempt, type DecideRouteMeta, } from "./providers.js";
|
|
5
|
-
export { parseDecision } from "./decision.js";
|
|
6
|
-
export { chatShapeFor, buildChatBody, type ChatShape, } from "./providerCapabilities.js";
|
|
4
|
+
export { selectProvider, providerForRoute, classifyProviderFailure, anthropicUsage, type ProviderUsage, type ProviderEnv, type ProviderRouteOptions, type Provider, type DecideInput, type DecideResult, type DecideRouteAttempt, type DecideRouteMeta, } from "./providers.js";
|
|
5
|
+
export { parseDecision, parseDecisionForContract, ACTIONS_STRING_DIAGNOSTICS, type ActionsStringDiagnostic, } from "./decision.js";
|
|
6
|
+
export { chatShapeFor, buildChatBody, canRetrySuperToolOutput, type ChatShape, } from "./providerCapabilities.js";
|
|
7
7
|
export { probeDecisionContract, type ProbeRoute, type ProbeDecisionResult, } from "./decisionProbe.js";
|
|
8
8
|
export { CoinRithmClient } from "./client.js";
|
|
9
9
|
export { loadAgent, buildSpec, type LoadedAgent } from "./skill.js";
|
|
@@ -14,3 +14,5 @@ export { makeRunId } from "./runEvidence.js";
|
|
|
14
14
|
export { parseCadenceMs } from "./util.js";
|
|
15
15
|
export { BENCHMARK_AGENTS, BENCHMARK_STRATEGIES, decideMechanical, isBenchmarkStrategy, type BenchmarkStrategy, type BenchmarkAgentDefinition, } from "./mechanical.js";
|
|
16
16
|
export type { AgentSpec, RunState, CycleResult, OpportunityReport, PostedOpportunity, PlannedAction, Venue, ProviderName, ModelConfig, } from "./types.js";
|
|
17
|
+
export { readPmHousePolicies, selectPmHousePolicy, type PmHousePolicy, } from "./pmPaperPolicy.js";
|
|
18
|
+
export { runPmPaperCycle, type PmPaperCycleResult } from "./pmPaperCycle.js";
|
package/dist/agent/engine.js
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
export { runCycle } from "./runner.js";
|
|
9
9
|
export { buildAgentDefinitionSnapshot, } from "./definitionSnapshot.js";
|
|
10
10
|
export { DECISION_INPUT_MAX_BYTES, sanitizeDecisionInputRecord, } from "./decisionReceipt.js";
|
|
11
|
-
export { selectProvider, providerForRoute, classifyProviderFailure, } from "./providers.js";
|
|
12
|
-
export { parseDecision } from "./decision.js";
|
|
11
|
+
export { selectProvider, providerForRoute, classifyProviderFailure, anthropicUsage, } from "./providers.js";
|
|
12
|
+
export { parseDecision, parseDecisionForContract, ACTIONS_STRING_DIAGNOSTICS, } from "./decision.js";
|
|
13
13
|
// Reliability slice A: the declarative request-capability table and the
|
|
14
14
|
// representative decision probe (route eligibility = a REAL parsed decision,
|
|
15
15
|
// never a bare HTTP 200 — the 62f3a12 lesson).
|
|
16
|
-
export { chatShapeFor, buildChatBody, } from "./providerCapabilities.js";
|
|
16
|
+
export { chatShapeFor, buildChatBody, canRetrySuperToolOutput, } from "./providerCapabilities.js";
|
|
17
17
|
export { probeDecisionContract, } from "./decisionProbe.js";
|
|
18
18
|
export { CoinRithmClient } from "./client.js";
|
|
19
19
|
export { loadAgent, buildSpec } from "./skill.js";
|
|
@@ -25,3 +25,5 @@ export { parseCadenceMs } from "./util.js";
|
|
|
25
25
|
// Mechanical BENCHMARK baseline agents (sol #7): the deterministic, non-LLM
|
|
26
26
|
// reference line the scheduler seeds into agent_runtime and the runner executes.
|
|
27
27
|
export { BENCHMARK_AGENTS, BENCHMARK_STRATEGIES, decideMechanical, isBenchmarkStrategy, } from "./mechanical.js";
|
|
28
|
+
export { readPmHousePolicies, selectPmHousePolicy, } from "./pmPaperPolicy.js";
|
|
29
|
+
export { runPmPaperCycle } from "./pmPaperCycle.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ApiResult, ProposedAction } from "./types.js";
|
|
2
|
+
/** Future v2 producer contract; legacy responses retain their existing res.ok
|
|
3
|
+
* meaning. A filled exit closes exposure, even while accounting remains pending. */
|
|
4
|
+
export declare function executionOutcome(action: ProposedAction, result: ApiResult): {
|
|
5
|
+
executed: boolean;
|
|
6
|
+
pending: boolean;
|
|
7
|
+
newWrite: boolean;
|
|
8
|
+
v2: boolean;
|
|
9
|
+
replayed: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function exitOutcomeMetadata(outcome: ReturnType<typeof executionOutcome>): {
|
|
12
|
+
executionPending: boolean;
|
|
13
|
+
executionReplayed: boolean;
|
|
14
|
+
writeRecorded: boolean;
|
|
15
|
+
} | {
|
|
16
|
+
executionPending?: undefined;
|
|
17
|
+
executionReplayed?: undefined;
|
|
18
|
+
writeRecorded?: undefined;
|
|
19
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { asObj } from "./extract.js";
|
|
2
|
+
/** Future v2 producer contract; legacy responses retain their existing res.ok
|
|
3
|
+
* meaning. A filled exit closes exposure, even while accounting remains pending. */
|
|
4
|
+
export function executionOutcome(action, result) {
|
|
5
|
+
const body = asObj(result.data);
|
|
6
|
+
if (action.type !== "futures_close" ||
|
|
7
|
+
body.executionModel !== "hl_paper_v2") {
|
|
8
|
+
return {
|
|
9
|
+
executed: result.ok,
|
|
10
|
+
pending: false,
|
|
11
|
+
newWrite: result.ok,
|
|
12
|
+
v2: false,
|
|
13
|
+
replayed: false,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const intent = asObj(body.intent);
|
|
17
|
+
const common = result.ok &&
|
|
18
|
+
Number.isSafeInteger(body.positionId) &&
|
|
19
|
+
body.positionId === action.positionId &&
|
|
20
|
+
(action.fraction === undefined || action.fraction === 1) &&
|
|
21
|
+
body.accepted === true &&
|
|
22
|
+
typeof body.replayed === "boolean" &&
|
|
23
|
+
typeof intent.id === "string" &&
|
|
24
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(intent.id);
|
|
25
|
+
const pending = !!common &&
|
|
26
|
+
result.status === 202 &&
|
|
27
|
+
body.executed === false &&
|
|
28
|
+
typeof intent.status === "string" &&
|
|
29
|
+
typeof body.accountingStatus === "string" &&
|
|
30
|
+
["pending", "leased", "retry", "manual_attention"].includes(intent.status) &&
|
|
31
|
+
["open", "pending", "manual_attention"].includes(body.accountingStatus);
|
|
32
|
+
const executed = !!common &&
|
|
33
|
+
result.status === 200 &&
|
|
34
|
+
body.executed === true &&
|
|
35
|
+
intent.status === "filled" &&
|
|
36
|
+
typeof body.accountingStatus === "string" &&
|
|
37
|
+
["pending", "settled", "manual_attention"].includes(body.accountingStatus);
|
|
38
|
+
return {
|
|
39
|
+
executed,
|
|
40
|
+
pending,
|
|
41
|
+
newWrite: (executed || pending) && body.replayed === false,
|
|
42
|
+
v2: true,
|
|
43
|
+
replayed: body.replayed === true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function exitOutcomeMetadata(outcome) {
|
|
47
|
+
return outcome.v2
|
|
48
|
+
? {
|
|
49
|
+
executionPending: outcome.pending,
|
|
50
|
+
executionReplayed: outcome.replayed,
|
|
51
|
+
writeRecorded: outcome.newWrite,
|
|
52
|
+
}
|
|
53
|
+
: {};
|
|
54
|
+
}
|
|
@@ -34,3 +34,16 @@ export interface FuturesEntryPreflightRejection {
|
|
|
34
34
|
* - referenceRequired false.
|
|
35
35
|
*/
|
|
36
36
|
export declare function futuresEntryPreflight(action: ProposedAction, observation: Observation): FuturesEntryPreflightRejection | null;
|
|
37
|
+
/** Prompt-only choices from the SAME reference preflight, not trade approval.
|
|
38
|
+
* Keep unknown evidence and held-position exceptions distinct from eligibility.
|
|
39
|
+
* Watch rows, setup receipts and all execution/risk checks remain unchanged. */
|
|
40
|
+
export declare function futuresEntryChoices(observation: Observation): {
|
|
41
|
+
candidates: Array<{
|
|
42
|
+
symbol: string;
|
|
43
|
+
reference: "eligible" | "unknown" | "not_required" | "held_position";
|
|
44
|
+
}>;
|
|
45
|
+
blocked: Array<{
|
|
46
|
+
symbol: string;
|
|
47
|
+
code: FuturesEntryPreflightRejection["code"];
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
@@ -66,11 +66,17 @@ export function futuresEntryEligibilityOf(marketContext) {
|
|
|
66
66
|
export function futuresEntryPreflight(action, observation) {
|
|
67
67
|
if (action.type !== "futures_open")
|
|
68
68
|
return null;
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
return futuresReferenceBlock(action.symbol, observation);
|
|
70
|
+
}
|
|
71
|
+
function holdsFutures(symbol, observation) {
|
|
72
|
+
const base = baseSymbol(symbol);
|
|
73
|
+
return (observation.openPositions ?? []).some((p) => p.venue === "futures" &&
|
|
71
74
|
(p.status ?? "open") === "open" &&
|
|
72
75
|
baseSymbol(p.symbol) === base);
|
|
73
|
-
|
|
76
|
+
}
|
|
77
|
+
function futuresReferenceBlock(symbol, observation) {
|
|
78
|
+
const base = baseSymbol(symbol);
|
|
79
|
+
if (holdsFutures(symbol, observation))
|
|
74
80
|
return null;
|
|
75
81
|
const entry = observation.watch.find((w) => baseSymbol(w.symbol) === base);
|
|
76
82
|
const e = entry?.futuresEntryEligibility;
|
|
@@ -80,11 +86,43 @@ export function futuresEntryPreflight(action, observation) {
|
|
|
80
86
|
if (e.status === "reference_stale") {
|
|
81
87
|
return {
|
|
82
88
|
code: "futures_reference_stale",
|
|
83
|
-
reason: `${
|
|
89
|
+
reason: `${symbol}: the perpetual reference${e.venue ? ` (${e.venue})` : ""} is stale or unusable for a NEW futures open${when} (too old, or its refresh time is missing, invalid or in the future); the server would refuse it (perpetual_reference_stale). Trade another coin or wait for a fresh reference.`,
|
|
84
90
|
};
|
|
85
91
|
}
|
|
86
92
|
return {
|
|
87
93
|
code: "futures_reference_unavailable",
|
|
88
|
-
reason: `${
|
|
94
|
+
reason: `${symbol}: CoinRithm has no supported perpetual reference for this coin${when}, so the server refuses a NEW futures open (perpetual_reference_unavailable). Use spot for it, or another coin for futures.`,
|
|
89
95
|
};
|
|
90
96
|
}
|
|
97
|
+
/** Prompt-only choices from the SAME reference preflight, not trade approval.
|
|
98
|
+
* Keep unknown evidence and held-position exceptions distinct from eligibility.
|
|
99
|
+
* Watch rows, setup receipts and all execution/risk checks remain unchanged. */
|
|
100
|
+
export function futuresEntryChoices(observation) {
|
|
101
|
+
const candidates = [];
|
|
102
|
+
const blocked = [];
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
for (const entry of observation.watch) {
|
|
105
|
+
const base = baseSymbol(entry.symbol);
|
|
106
|
+
if (seen.has(base))
|
|
107
|
+
continue;
|
|
108
|
+
seen.add(base);
|
|
109
|
+
const rejection = futuresReferenceBlock(entry.symbol, observation);
|
|
110
|
+
if (rejection) {
|
|
111
|
+
blocked.push({ symbol: entry.symbol, code: rejection.code });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const e = entry.futuresEntryEligibility;
|
|
115
|
+
candidates.push({
|
|
116
|
+
symbol: entry.symbol,
|
|
117
|
+
reference: holdsFutures(entry.symbol, observation)
|
|
118
|
+
? "held_position"
|
|
119
|
+
: !e
|
|
120
|
+
? "unknown"
|
|
121
|
+
: !e.referenceRequired
|
|
122
|
+
? "not_required"
|
|
123
|
+
: "eligible",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { candidates, blocked };
|
|
128
|
+
}
|
package/dist/agent/observe.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CoinRithmClient } from "./client.js";
|
|
2
|
-
import { AgentSpec, RunState, Observation, AgentTrace, PmCalibration, WhaleContext } from "./types.js";
|
|
2
|
+
import { AgentSpec, RunState, Observation, PmMarket, OpenInterestContext, PositioningContext, LiquidationContext, MacroContext, ChainTvlContext, StablecoinSupplyContext, DepthContext, FundingByVenueContext, AgentTrace, PmCalibration, WhaleContext } from "./types.js";
|
|
3
3
|
export interface ObserveOutput {
|
|
4
4
|
observation: Observation;
|
|
5
5
|
skip?: string;
|
|
@@ -16,4 +16,30 @@ export declare function isCalibrationChurnMarket(market: {
|
|
|
16
16
|
slug?: string;
|
|
17
17
|
title?: string;
|
|
18
18
|
}): boolean;
|
|
19
|
-
export declare function
|
|
19
|
+
export declare function openInterestOf(m: Record<string, unknown>, nowMs?: number): OpenInterestContext | undefined;
|
|
20
|
+
export declare function positioningOf(m: Record<string, unknown>, nowMs?: number): PositioningContext | undefined;
|
|
21
|
+
export declare function liquidationsOf(m: Record<string, unknown>, nowMs?: number): LiquidationContext | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* `{ coins }` from an additive `relatedCoins` array (direct coin slugs), or
|
|
24
|
+
* nothing: non-arrays and non-string/blank entries are dropped, duplicates
|
|
25
|
+
* merged. Every valid slug is kept: the API never truncates the list, and
|
|
26
|
+
* dropping a linked coin could let a bet bypass that coin's cap. An empty
|
|
27
|
+
* result adds no field.
|
|
28
|
+
*/
|
|
29
|
+
export declare function coinsField(value: unknown): {
|
|
30
|
+
coins?: string[];
|
|
31
|
+
};
|
|
32
|
+
export declare function fundingByVenueOf(m: Record<string, unknown>, nowMs?: number): FundingByVenueContext | undefined;
|
|
33
|
+
export declare function depthOf(m: Record<string, unknown>, nowMs?: number): DepthContext | undefined;
|
|
34
|
+
export declare function chainTvlOf(m: Record<string, unknown>, nowMs?: number): ChainTvlContext | undefined;
|
|
35
|
+
export declare function stablecoinSupplyOf(m: Record<string, unknown>, nowMs?: number): StablecoinSupplyContext | undefined;
|
|
36
|
+
export declare function macroOf(m: Record<string, unknown>, nowMs?: number): MacroContext | undefined;
|
|
37
|
+
export declare function expandPmMarkets(discData: unknown, heldPmKeys: Set<string>): Omit<PmMarket, "ref">[];
|
|
38
|
+
/** Bench-only observation overrides; live callers pass none. */
|
|
39
|
+
export interface ObserveOptions {
|
|
40
|
+
/** Read the curated PM board (depth 30, churn removed, calibration read)
|
|
41
|
+
* even for a mechanical spec, so a bench baseline sees the recorded
|
|
42
|
+
* agent opportunity set. Default: non-mechanical only. */
|
|
43
|
+
curatedPmBoard?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function observe(client: CoinRithmClient, spec: AgentSpec, state: RunState, trace?: AgentTrace, options?: ObserveOptions): Promise<ObserveOutput>;
|