@coinrithm/mcp-trading 0.7.7 → 0.7.8
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 +83 -0
- package/dist/agent/act.js +19 -6
- package/dist/agent/capitalSizing.d.ts +32 -0
- package/dist/agent/capitalSizing.js +257 -0
- package/dist/agent/decision.d.ts +392 -0
- package/dist/agent/decision.js +177 -0
- package/dist/agent/decisionReceipt.d.ts +45 -0
- package/dist/agent/decisionReceipt.js +595 -0
- package/dist/agent/decisionValidator.d.ts +19 -2
- package/dist/agent/decisionValidator.js +74 -3
- package/dist/agent/engine.d.ts +1 -0
- package/dist/agent/engine.js +1 -0
- package/dist/agent/observe.js +175 -34
- package/dist/agent/pmContext.d.ts +13 -0
- package/dist/agent/pmContext.js +136 -0
- package/dist/agent/prompt.d.ts +11 -1
- package/dist/agent/prompt.js +135 -7
- package/dist/agent/providerCapabilities.d.ts +3 -0
- package/dist/agent/providerCapabilities.js +41 -3
- package/dist/agent/providers.js +119 -73
- package/dist/agent/resolve.js +1 -0
- package/dist/agent/runner.d.ts +4 -1
- package/dist/agent/runner.js +360 -34
- package/dist/agent/scorecard.js +7 -1
- package/dist/agent/skill.js +17 -0
- package/dist/agent/skillValidator.d.ts +1 -0
- package/dist/agent/skillValidator.js +56 -0
- package/dist/agent/state.js +6 -0
- package/dist/agent/strictLint.js +19 -0
- package/dist/agent/thesis.d.ts +40 -0
- package/dist/agent/thesis.js +319 -0
- package/dist/agent/types.d.ts +127 -0
- package/package.json +1 -1
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { buildDailyRiskBudget } from "./prompt.js";
|
|
2
|
+
import { sha256, stableStringify } from "./util.js";
|
|
3
|
+
import { sourceTimestamp, FRESHNESS_BASES, PM_BLOCK_REASONS, PM_WARNING_REASONS, PM_FLAGS, PM_TIERS, PM_SPREAD_TIERS, PM_QUALITY_CAPS, pmQualityOf, pmDecisionSupportOf, freshnessOf as parseFreshness, } from "./pmContext.js";
|
|
4
|
+
export const DECISION_INPUT_MAX_BYTES = 16 * 1024;
|
|
5
|
+
const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
6
|
+
const bool = (value) => typeof value === "boolean" ? value : null;
|
|
7
|
+
const obj = (value) => value && typeof value === "object" && !Array.isArray(value)
|
|
8
|
+
? value
|
|
9
|
+
: {};
|
|
10
|
+
const arr = (value) => (Array.isArray(value) ? value : []);
|
|
11
|
+
const credential = /(crk_live_|sk-|sk_live_|ghp_|nvapi-|AIza|bearer|password|secret|token)/i;
|
|
12
|
+
function id(value, max = 96) {
|
|
13
|
+
return typeof value === "string" &&
|
|
14
|
+
value.length <= max &&
|
|
15
|
+
/^[a-zA-Z0-9][a-zA-Z0-9_.:/-]*$/.test(value) &&
|
|
16
|
+
!credential.test(value)
|
|
17
|
+
? value
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
const code = (value, allowed) => typeof value === "string" && allowed.includes(value) ? value : null;
|
|
21
|
+
const fingerprint = (value) => typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value)
|
|
22
|
+
? value
|
|
23
|
+
: null;
|
|
24
|
+
const numeric = (raw, keys) => Object.fromEntries(keys.map((key) => [key, num(raw[key])]));
|
|
25
|
+
const freshness = (value) => {
|
|
26
|
+
const raw = obj(parseFreshness({ freshness: value }));
|
|
27
|
+
return {
|
|
28
|
+
status: code(raw.status, [
|
|
29
|
+
"fresh",
|
|
30
|
+
"stale",
|
|
31
|
+
"lagging",
|
|
32
|
+
"never_ingested",
|
|
33
|
+
"unknown",
|
|
34
|
+
]),
|
|
35
|
+
ageSeconds: num(raw.ageSeconds),
|
|
36
|
+
asOf: sourceTimestamp(raw.asOf) ?? null,
|
|
37
|
+
basis: code(raw.basis, FRESHNESS_BASES),
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export function buildDecisionInputRecord(input) {
|
|
41
|
+
let configFingerprint = null;
|
|
42
|
+
try {
|
|
43
|
+
configFingerprint = sha256(stableStringify({ spec: input.spec, prose: input.mergedProse }));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* evidence must not interrupt trading */
|
|
47
|
+
}
|
|
48
|
+
const state = obj(input.state);
|
|
49
|
+
const budget = buildDailyRiskBudget(input.spec, input.state);
|
|
50
|
+
const record = {
|
|
51
|
+
version: "coinrithm.decision-input.v1",
|
|
52
|
+
visibility: "private",
|
|
53
|
+
completeness: "partial",
|
|
54
|
+
phase: input.phase,
|
|
55
|
+
outcome: "pending",
|
|
56
|
+
runId: id(input.runId, 160),
|
|
57
|
+
decisionId: id(input.decisionId, 160),
|
|
58
|
+
configFingerprint,
|
|
59
|
+
observationFingerprint: fingerprint(input.observationFingerprint),
|
|
60
|
+
preThesisObservationFingerprint: fingerprint(input.preThesisObservationFingerprint),
|
|
61
|
+
dailyRiskBudget: {
|
|
62
|
+
version: "coinrithm.daily-risk-budget.v1",
|
|
63
|
+
utcDay: /^\d{4}-\d{2}-\d{2}$/.test(budget.utcDay)
|
|
64
|
+
? budget.utcDay
|
|
65
|
+
: "unknown",
|
|
66
|
+
limit: num(budget.limit),
|
|
67
|
+
used: num(budget.used) ?? 0,
|
|
68
|
+
remaining: num(budget.remaining),
|
|
69
|
+
},
|
|
70
|
+
guardState: {
|
|
71
|
+
...numeric(state, [
|
|
72
|
+
"riskIncreasesToday",
|
|
73
|
+
"writesToday",
|
|
74
|
+
"realizedPnlTodayMusd",
|
|
75
|
+
"realizedPnlMusd",
|
|
76
|
+
"peakRealizedMusd",
|
|
77
|
+
"consecutiveRejectCycles",
|
|
78
|
+
"consecutiveModelFailures",
|
|
79
|
+
"consecutiveExecFailures",
|
|
80
|
+
"rateLimitHits",
|
|
81
|
+
]),
|
|
82
|
+
disabled: bool(state.disabled),
|
|
83
|
+
},
|
|
84
|
+
account: null,
|
|
85
|
+
lists: {},
|
|
86
|
+
counts: {},
|
|
87
|
+
omissions: [
|
|
88
|
+
"partial_projection_not_full_model_input",
|
|
89
|
+
"fingerprints_not_historical_attestation",
|
|
90
|
+
"no_model_replay_guarantee",
|
|
91
|
+
"prose_prompts_and_model_reasoning_excluded",
|
|
92
|
+
"journal_news_thesis_and_other_free_text_excluded",
|
|
93
|
+
"some_source_timestamps_and_source_counts_not_available",
|
|
94
|
+
"pm_discovery_filtered_candidates_not_recorded",
|
|
95
|
+
"raw_closed_trade_records_excluded",
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
if (!record.runId || !record.decisionId)
|
|
99
|
+
record.omissions.push("unsafe_identifier_omitted");
|
|
100
|
+
if (!configFingerprint)
|
|
101
|
+
record.omissions.push("config_fingerprint_unavailable");
|
|
102
|
+
const obs = input.observation;
|
|
103
|
+
if (!obs) {
|
|
104
|
+
record.omissions.push("observation_not_available");
|
|
105
|
+
return record;
|
|
106
|
+
}
|
|
107
|
+
record.account = {
|
|
108
|
+
asOf: typeof obs.asOf === "string" &&
|
|
109
|
+
obs.asOf.length <= 30 &&
|
|
110
|
+
/^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/.test(obs.asOf)
|
|
111
|
+
? obs.asOf
|
|
112
|
+
: null,
|
|
113
|
+
cashAvailableMusd: num(obs.cashAvailableMusd),
|
|
114
|
+
equityMusd: num(obs.equityMusd),
|
|
115
|
+
polledBeforeWrite: bool(obs.polledBeforeWrite),
|
|
116
|
+
};
|
|
117
|
+
const add = (name, items, project) => {
|
|
118
|
+
const all = arr(items);
|
|
119
|
+
// Bound work/space independently of the upstream list limit. Preserve order,
|
|
120
|
+
// not only traded assets; every excluded row is counted explicitly.
|
|
121
|
+
record.lists[name] = all.slice(0, 40).map((item) => project(obj(item)));
|
|
122
|
+
record.counts[name] = {
|
|
123
|
+
source: all.length,
|
|
124
|
+
retained: record.lists[name].length,
|
|
125
|
+
omitted: Math.max(0, all.length - record.lists[name].length),
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
add("watch", obs.watch, (r) => ({
|
|
129
|
+
symbol: id(r.symbol, 20),
|
|
130
|
+
coinId: id(r.coinId, 32),
|
|
131
|
+
discovered: bool(r.discovered),
|
|
132
|
+
...numeric(r, [
|
|
133
|
+
"priceUsd",
|
|
134
|
+
"change1h",
|
|
135
|
+
"change24h",
|
|
136
|
+
"change7d",
|
|
137
|
+
"sentimentBullishPct",
|
|
138
|
+
]),
|
|
139
|
+
freshness: freshness(r.freshness),
|
|
140
|
+
indicators: {
|
|
141
|
+
...numeric(obj(r.indicators), [
|
|
142
|
+
"asOfClose",
|
|
143
|
+
"rsi14",
|
|
144
|
+
"ema20",
|
|
145
|
+
"ema50",
|
|
146
|
+
"atr14",
|
|
147
|
+
]),
|
|
148
|
+
aboveEma20: bool(obj(r.indicators).aboveEma20),
|
|
149
|
+
ema20AboveEma50: bool(obj(r.indicators).ema20AboveEma50),
|
|
150
|
+
brokeRecentHigh: bool(obj(r.indicators).brokeRecentHigh),
|
|
151
|
+
brokeRecentLow: bool(obj(r.indicators).brokeRecentLow),
|
|
152
|
+
},
|
|
153
|
+
fundamentals: numeric(obj(r.fundamentals), [
|
|
154
|
+
"marketCapRank",
|
|
155
|
+
"marketCapUsd",
|
|
156
|
+
"volume24hUsd",
|
|
157
|
+
]),
|
|
158
|
+
}));
|
|
159
|
+
add("futuresPositions", obs.openPositions, (r) => ({
|
|
160
|
+
id: num(r.id),
|
|
161
|
+
symbol: id(r.symbol, 20),
|
|
162
|
+
coinId: id(r.coinId, 32),
|
|
163
|
+
side: code(r.side, ["long", "short"]),
|
|
164
|
+
...numeric(r, [
|
|
165
|
+
"leverage",
|
|
166
|
+
"marginMusd",
|
|
167
|
+
"unrealizedPnlMusd",
|
|
168
|
+
"entryPrice",
|
|
169
|
+
"markPrice",
|
|
170
|
+
"liquidationPrice",
|
|
171
|
+
"stopLossPrice",
|
|
172
|
+
"takeProfitPrice",
|
|
173
|
+
]),
|
|
174
|
+
}));
|
|
175
|
+
add("spotOrders", obs.openOrders, (r) => ({
|
|
176
|
+
id: num(r.id),
|
|
177
|
+
symbol: id(r.symbol, 20),
|
|
178
|
+
side: code(r.side, ["buy", "sell"]),
|
|
179
|
+
orderType: code(r.orderType, ["market", "limit", "stop"]),
|
|
180
|
+
quantity: num(r.quantity),
|
|
181
|
+
}));
|
|
182
|
+
add("pmPositions", obs.pmPositions, (r) => ({
|
|
183
|
+
id: num(r.id),
|
|
184
|
+
source: id(r.source, 32),
|
|
185
|
+
slug: id(r.slug, 128),
|
|
186
|
+
outcomeExternalMarketId: id(r.outcomeExternalMarketId, 128),
|
|
187
|
+
side: code(r.side, ["yes", "no"]),
|
|
188
|
+
...numeric(r, [
|
|
189
|
+
"stakeMusd",
|
|
190
|
+
"unrealizedPnlMusd",
|
|
191
|
+
"entryProbability",
|
|
192
|
+
"currentProbability",
|
|
193
|
+
]),
|
|
194
|
+
}));
|
|
195
|
+
add("pmMarkets", obs.pmMarkets, (r) => {
|
|
196
|
+
const quality = pmQualityOf(r.quality);
|
|
197
|
+
const support = pmDecisionSupportOf(r.decisionSupport);
|
|
198
|
+
return {
|
|
199
|
+
ref: id(r.ref, 16),
|
|
200
|
+
source: id(r.source, 32),
|
|
201
|
+
slug: id(r.slug, 128),
|
|
202
|
+
outcomeExternalMarketId: id(r.outcomeExternalMarketId, 128),
|
|
203
|
+
...numeric(r, ["probability", "volumeUsd", "liquidityUsd"]),
|
|
204
|
+
freshness: freshness(r.freshness),
|
|
205
|
+
quality: {
|
|
206
|
+
decisionEligible: quality?.decisionEligible ?? null,
|
|
207
|
+
policyVersion: quality?.policyVersion ?? null,
|
|
208
|
+
assessedAt: quality?.assessedAt ?? null,
|
|
209
|
+
warningReasons: quality?.warningReasons ?? [],
|
|
210
|
+
blockReasons: quality?.blockReasons ?? [],
|
|
211
|
+
reasonsOmitted: quality?.reasonsOmitted ?? null,
|
|
212
|
+
},
|
|
213
|
+
decisionSupport: {
|
|
214
|
+
qualityScore: support?.qualityScore ?? null,
|
|
215
|
+
qualityTier: support?.qualityTier ?? null,
|
|
216
|
+
qualityCapReason: support?.qualityCapReason ?? null,
|
|
217
|
+
spreadTier: support?.spreadTier ?? null,
|
|
218
|
+
liquidityTier: support?.liquidityTier ?? null,
|
|
219
|
+
volumeTier: support?.volumeTier ?? null,
|
|
220
|
+
...Object.fromEntries(PM_FLAGS.map((key) => [key, support?.flags?.[key] ?? null])),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
add("signals", obs.setups, (r) => ({
|
|
225
|
+
symbol: id(r.symbol, 20),
|
|
226
|
+
kind: code(r.kind, [
|
|
227
|
+
"breakout",
|
|
228
|
+
"breakdown",
|
|
229
|
+
"uptrend",
|
|
230
|
+
"downtrend",
|
|
231
|
+
"stretched",
|
|
232
|
+
]),
|
|
233
|
+
bias: code(r.bias, ["long", "short", "fade-long", "fade-short"]),
|
|
234
|
+
strength: num(r.strength),
|
|
235
|
+
held: code(r.held, ["long", "short"]),
|
|
236
|
+
}));
|
|
237
|
+
for (const [name, values] of [
|
|
238
|
+
["news", obs.news],
|
|
239
|
+
["pmResolutions", obs.pmResolutions],
|
|
240
|
+
["newClosedTrades", obs.newClosedTrades],
|
|
241
|
+
["universeMovers", obs.universeMovers],
|
|
242
|
+
["journal", input.state.journal],
|
|
243
|
+
]) {
|
|
244
|
+
record.counts[name] = {
|
|
245
|
+
source: arr(values).length,
|
|
246
|
+
retained: 0,
|
|
247
|
+
omitted: arr(values).length,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
record.omissions.push("unlisted_fields_and_nested_indicators_excluded");
|
|
251
|
+
if (Object.values(record.counts).some((v) => v.omitted > 0))
|
|
252
|
+
record.omissions.push("list_rows_omitted");
|
|
253
|
+
// Leave room for fixed outcome/error metadata. Drop the largest remaining
|
|
254
|
+
// list's tail, updating exact counts, until the serialized record is bounded.
|
|
255
|
+
while (Buffer.byteLength(JSON.stringify(record), "utf8") >
|
|
256
|
+
DECISION_INPUT_MAX_BYTES - 256) {
|
|
257
|
+
const largest = Object.keys(record.lists).sort((a, b) => JSON.stringify(record.lists[b]).length -
|
|
258
|
+
JSON.stringify(record.lists[a]).length)[0];
|
|
259
|
+
if (!largest || record.lists[largest].length === 0)
|
|
260
|
+
break;
|
|
261
|
+
record.lists[largest].pop();
|
|
262
|
+
record.counts[largest].retained -= 1;
|
|
263
|
+
record.counts[largest].omitted += 1;
|
|
264
|
+
if (!record.omissions.includes("byte_budget_exceeded"))
|
|
265
|
+
record.omissions.push("byte_budget_exceeded");
|
|
266
|
+
}
|
|
267
|
+
return record;
|
|
268
|
+
}
|
|
269
|
+
export function unavailableDecisionInputRecord() {
|
|
270
|
+
return {
|
|
271
|
+
version: "coinrithm.decision-input.v1",
|
|
272
|
+
visibility: "private",
|
|
273
|
+
completeness: "partial",
|
|
274
|
+
phase: "before_observation",
|
|
275
|
+
outcome: "pending",
|
|
276
|
+
runId: null,
|
|
277
|
+
decisionId: null,
|
|
278
|
+
configFingerprint: null,
|
|
279
|
+
observationFingerprint: null,
|
|
280
|
+
preThesisObservationFingerprint: null,
|
|
281
|
+
dailyRiskBudget: null,
|
|
282
|
+
guardState: {},
|
|
283
|
+
account: null,
|
|
284
|
+
lists: {},
|
|
285
|
+
counts: {},
|
|
286
|
+
omissions: [
|
|
287
|
+
"evidence_capture_failed",
|
|
288
|
+
"partial_projection_not_full_model_input",
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const OMISSIONS = [
|
|
293
|
+
"partial_projection_not_full_model_input",
|
|
294
|
+
"fingerprints_not_historical_attestation",
|
|
295
|
+
"no_model_replay_guarantee",
|
|
296
|
+
"prose_prompts_and_model_reasoning_excluded",
|
|
297
|
+
"journal_news_thesis_and_other_free_text_excluded",
|
|
298
|
+
"source_timestamps_and_source_counts_not_available",
|
|
299
|
+
"some_source_timestamps_and_source_counts_not_available",
|
|
300
|
+
"pm_discovery_filtered_candidates_not_recorded",
|
|
301
|
+
"raw_closed_trade_records_excluded",
|
|
302
|
+
"unsafe_identifier_omitted",
|
|
303
|
+
"config_fingerprint_unavailable",
|
|
304
|
+
"observation_not_available",
|
|
305
|
+
"unlisted_fields_and_nested_indicators_excluded",
|
|
306
|
+
"list_rows_omitted",
|
|
307
|
+
"byte_budget_exceeded",
|
|
308
|
+
"evidence_capture_failed",
|
|
309
|
+
"runtime_exception_after_snapshot",
|
|
310
|
+
];
|
|
311
|
+
const LIST_KEYS = {
|
|
312
|
+
watch: [
|
|
313
|
+
"symbol",
|
|
314
|
+
"coinId",
|
|
315
|
+
"discovered",
|
|
316
|
+
"priceUsd",
|
|
317
|
+
"change1h",
|
|
318
|
+
"change24h",
|
|
319
|
+
"change7d",
|
|
320
|
+
"sentimentBullishPct",
|
|
321
|
+
"freshness",
|
|
322
|
+
"indicators",
|
|
323
|
+
"fundamentals",
|
|
324
|
+
],
|
|
325
|
+
futuresPositions: [
|
|
326
|
+
"id",
|
|
327
|
+
"symbol",
|
|
328
|
+
"coinId",
|
|
329
|
+
"side",
|
|
330
|
+
"leverage",
|
|
331
|
+
"marginMusd",
|
|
332
|
+
"unrealizedPnlMusd",
|
|
333
|
+
"entryPrice",
|
|
334
|
+
"markPrice",
|
|
335
|
+
"liquidationPrice",
|
|
336
|
+
"stopLossPrice",
|
|
337
|
+
"takeProfitPrice",
|
|
338
|
+
],
|
|
339
|
+
spotOrders: ["id", "symbol", "side", "orderType", "quantity"],
|
|
340
|
+
pmPositions: [
|
|
341
|
+
"id",
|
|
342
|
+
"source",
|
|
343
|
+
"slug",
|
|
344
|
+
"outcomeExternalMarketId",
|
|
345
|
+
"side",
|
|
346
|
+
"stakeMusd",
|
|
347
|
+
"unrealizedPnlMusd",
|
|
348
|
+
"entryProbability",
|
|
349
|
+
"currentProbability",
|
|
350
|
+
],
|
|
351
|
+
pmMarkets: [
|
|
352
|
+
"ref",
|
|
353
|
+
"source",
|
|
354
|
+
"slug",
|
|
355
|
+
"outcomeExternalMarketId",
|
|
356
|
+
"probability",
|
|
357
|
+
"volumeUsd",
|
|
358
|
+
"liquidityUsd",
|
|
359
|
+
"freshness",
|
|
360
|
+
"quality",
|
|
361
|
+
"decisionSupport",
|
|
362
|
+
],
|
|
363
|
+
signals: ["symbol", "kind", "bias", "strength", "held"],
|
|
364
|
+
};
|
|
365
|
+
const NESTED_KEYS = {
|
|
366
|
+
freshness: ["status", "ageSeconds", "asOf", "basis"],
|
|
367
|
+
quality: [
|
|
368
|
+
"decisionEligible",
|
|
369
|
+
"policyVersion",
|
|
370
|
+
"assessedAt",
|
|
371
|
+
"warningReasons",
|
|
372
|
+
"blockReasons",
|
|
373
|
+
"reasonsOmitted",
|
|
374
|
+
],
|
|
375
|
+
decisionSupport: [
|
|
376
|
+
"qualityScore",
|
|
377
|
+
"qualityTier",
|
|
378
|
+
"qualityCapReason",
|
|
379
|
+
"spreadTier",
|
|
380
|
+
"liquidityTier",
|
|
381
|
+
"volumeTier",
|
|
382
|
+
...PM_FLAGS,
|
|
383
|
+
],
|
|
384
|
+
indicators: [
|
|
385
|
+
"asOfClose",
|
|
386
|
+
"rsi14",
|
|
387
|
+
"ema20",
|
|
388
|
+
"ema50",
|
|
389
|
+
"atr14",
|
|
390
|
+
"aboveEma20",
|
|
391
|
+
"ema20AboveEma50",
|
|
392
|
+
"brokeRecentHigh",
|
|
393
|
+
"brokeRecentLow",
|
|
394
|
+
],
|
|
395
|
+
fundamentals: ["marketCapRank", "marketCapUsd", "volume24hUsd"],
|
|
396
|
+
};
|
|
397
|
+
function keysOnly(value, keys) {
|
|
398
|
+
return (!!value &&
|
|
399
|
+
typeof value === "object" &&
|
|
400
|
+
!Array.isArray(value) &&
|
|
401
|
+
Object.keys(value).every((k) => keys.includes(k)));
|
|
402
|
+
}
|
|
403
|
+
function validRow(value, keys) {
|
|
404
|
+
if (!keysOnly(value, keys))
|
|
405
|
+
return false;
|
|
406
|
+
return Object.entries(obj(value)).every(([key, v]) => {
|
|
407
|
+
if (v === null)
|
|
408
|
+
return true;
|
|
409
|
+
if (NESTED_KEYS[key])
|
|
410
|
+
return validRow(v, NESTED_KEYS[key]);
|
|
411
|
+
if (key === "asOf" || key === "assessedAt")
|
|
412
|
+
return sourceTimestamp(v) === v;
|
|
413
|
+
if (key === "basis")
|
|
414
|
+
return code(v, FRESHNESS_BASES) !== null;
|
|
415
|
+
if (key === "policyVersion")
|
|
416
|
+
return typeof v === "string" && /^pm-quality-\d{1,3}$/.test(v);
|
|
417
|
+
if (key === "warningReasons" || key === "blockReasons") {
|
|
418
|
+
const allowed = key === "warningReasons" ? PM_WARNING_REASONS : PM_BLOCK_REASONS;
|
|
419
|
+
return (Array.isArray(v) &&
|
|
420
|
+
v.length <= allowed.length &&
|
|
421
|
+
new Set(v).size === v.length &&
|
|
422
|
+
v.every((x) => code(x, allowed) !== null));
|
|
423
|
+
}
|
|
424
|
+
if (["qualityTier", "liquidityTier", "volumeTier"].includes(key))
|
|
425
|
+
return code(v, PM_TIERS) !== null;
|
|
426
|
+
if (key === "qualityCapReason")
|
|
427
|
+
return code(v, PM_QUALITY_CAPS) !== null;
|
|
428
|
+
if (key === "spreadTier")
|
|
429
|
+
return code(v, PM_SPREAD_TIERS) !== null;
|
|
430
|
+
if (key === "qualityScore")
|
|
431
|
+
return num(v) !== null && v >= 0 && v <= 100;
|
|
432
|
+
if (key === "ageSeconds")
|
|
433
|
+
return num(v) !== null && v >= 0;
|
|
434
|
+
if ([
|
|
435
|
+
"symbol",
|
|
436
|
+
"coinId",
|
|
437
|
+
"source",
|
|
438
|
+
"slug",
|
|
439
|
+
"outcomeExternalMarketId",
|
|
440
|
+
"ref",
|
|
441
|
+
].includes(key))
|
|
442
|
+
return id(v, key === "symbol" ? 20 : 128) !== null;
|
|
443
|
+
if (["side", "orderType", "kind", "bias", "held", "status"].includes(key))
|
|
444
|
+
return (code(v, [
|
|
445
|
+
"long",
|
|
446
|
+
"short",
|
|
447
|
+
"yes",
|
|
448
|
+
"no",
|
|
449
|
+
"buy",
|
|
450
|
+
"sell",
|
|
451
|
+
"market",
|
|
452
|
+
"limit",
|
|
453
|
+
"stop",
|
|
454
|
+
"breakout",
|
|
455
|
+
"breakdown",
|
|
456
|
+
"uptrend",
|
|
457
|
+
"downtrend",
|
|
458
|
+
"stretched",
|
|
459
|
+
"fade-long",
|
|
460
|
+
"fade-short",
|
|
461
|
+
"fresh",
|
|
462
|
+
"stale",
|
|
463
|
+
"lagging",
|
|
464
|
+
"never_ingested",
|
|
465
|
+
"unknown",
|
|
466
|
+
]) !== null);
|
|
467
|
+
if ([
|
|
468
|
+
"discovered",
|
|
469
|
+
"aboveEma20",
|
|
470
|
+
"ema20AboveEma50",
|
|
471
|
+
"brokeRecentHigh",
|
|
472
|
+
"brokeRecentLow",
|
|
473
|
+
"decisionEligible",
|
|
474
|
+
"reasonsOmitted",
|
|
475
|
+
...PM_FLAGS,
|
|
476
|
+
].includes(key))
|
|
477
|
+
return typeof v === "boolean";
|
|
478
|
+
return num(v) !== null;
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
/** Defense at the storage boundary. Reject rather than preserve an arbitrary
|
|
482
|
+
* extension field. Detached JSON copy also prevents post-check mutation. */
|
|
483
|
+
export function sanitizeDecisionInputRecord(value) {
|
|
484
|
+
try {
|
|
485
|
+
const encoded = JSON.stringify(value);
|
|
486
|
+
if (!encoded ||
|
|
487
|
+
Buffer.byteLength(encoded, "utf8") > DECISION_INPUT_MAX_BYTES)
|
|
488
|
+
return undefined;
|
|
489
|
+
const r = JSON.parse(encoded);
|
|
490
|
+
if (!keysOnly(r, [
|
|
491
|
+
"version",
|
|
492
|
+
"visibility",
|
|
493
|
+
"completeness",
|
|
494
|
+
"phase",
|
|
495
|
+
"outcome",
|
|
496
|
+
"runId",
|
|
497
|
+
"decisionId",
|
|
498
|
+
"configFingerprint",
|
|
499
|
+
"observationFingerprint",
|
|
500
|
+
"preThesisObservationFingerprint",
|
|
501
|
+
"dailyRiskBudget",
|
|
502
|
+
"guardState",
|
|
503
|
+
"account",
|
|
504
|
+
"lists",
|
|
505
|
+
"counts",
|
|
506
|
+
"omissions",
|
|
507
|
+
]))
|
|
508
|
+
return undefined;
|
|
509
|
+
if (r.version !== "coinrithm.decision-input.v1" ||
|
|
510
|
+
r.visibility !== "private" ||
|
|
511
|
+
r.completeness !== "partial")
|
|
512
|
+
return undefined;
|
|
513
|
+
if (!code(r.phase, ["before_observation", "observed", "decision_input"]) ||
|
|
514
|
+
!code(r.outcome, ["pending", "returned", "runtime_error"]))
|
|
515
|
+
return undefined;
|
|
516
|
+
if (![r.runId, r.decisionId].every((v) => v === null || id(v, 160) !== null))
|
|
517
|
+
return undefined;
|
|
518
|
+
if (![
|
|
519
|
+
r.configFingerprint,
|
|
520
|
+
r.observationFingerprint,
|
|
521
|
+
r.preThesisObservationFingerprint,
|
|
522
|
+
].every((v) => v === null || fingerprint(v) !== null))
|
|
523
|
+
return undefined;
|
|
524
|
+
if (r.dailyRiskBudget !== null) {
|
|
525
|
+
const b = r.dailyRiskBudget;
|
|
526
|
+
if (!keysOnly(b, ["version", "utcDay", "limit", "used", "remaining"]) ||
|
|
527
|
+
b.version !== "coinrithm.daily-risk-budget.v1" ||
|
|
528
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(b.utcDay))
|
|
529
|
+
return undefined;
|
|
530
|
+
if (num(b.used) === null ||
|
|
531
|
+
![b.limit, b.remaining].every((v) => v === null || num(v) !== null))
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
if (!keysOnly(r.guardState, [
|
|
535
|
+
"riskIncreasesToday",
|
|
536
|
+
"writesToday",
|
|
537
|
+
"realizedPnlTodayMusd",
|
|
538
|
+
"realizedPnlMusd",
|
|
539
|
+
"peakRealizedMusd",
|
|
540
|
+
"consecutiveRejectCycles",
|
|
541
|
+
"consecutiveModelFailures",
|
|
542
|
+
"consecutiveExecFailures",
|
|
543
|
+
"rateLimitHits",
|
|
544
|
+
"disabled",
|
|
545
|
+
]))
|
|
546
|
+
return undefined;
|
|
547
|
+
if (!Object.entries(r.guardState).every(([k, v]) => v === null ||
|
|
548
|
+
(k === "disabled" ? typeof v === "boolean" : num(v) !== null)))
|
|
549
|
+
return undefined;
|
|
550
|
+
if (r.account !== null &&
|
|
551
|
+
(!keysOnly(r.account, [
|
|
552
|
+
"asOf",
|
|
553
|
+
"cashAvailableMusd",
|
|
554
|
+
"equityMusd",
|
|
555
|
+
"polledBeforeWrite",
|
|
556
|
+
]) ||
|
|
557
|
+
!Object.entries(r.account).every(([k, v]) => v === null ||
|
|
558
|
+
(k === "asOf"
|
|
559
|
+
? typeof v === "string" && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/.test(v)
|
|
560
|
+
: k === "polledBeforeWrite"
|
|
561
|
+
? typeof v === "boolean"
|
|
562
|
+
: num(v) !== null))))
|
|
563
|
+
return undefined;
|
|
564
|
+
if (!keysOnly(r.lists, Object.keys(LIST_KEYS)))
|
|
565
|
+
return undefined;
|
|
566
|
+
if (!Object.entries(r.lists).every(([name, rows]) => Array.isArray(rows) &&
|
|
567
|
+
rows.length <= 40 &&
|
|
568
|
+
rows.every((v) => validRow(v, LIST_KEYS[name]))))
|
|
569
|
+
return undefined;
|
|
570
|
+
if (!keysOnly(r.counts, [
|
|
571
|
+
...Object.keys(LIST_KEYS),
|
|
572
|
+
"news",
|
|
573
|
+
"pmResolutions",
|
|
574
|
+
"newClosedTrades",
|
|
575
|
+
"universeMovers",
|
|
576
|
+
"journal",
|
|
577
|
+
]))
|
|
578
|
+
return undefined;
|
|
579
|
+
if (!Object.entries(r.counts).every(([name, c]) => keysOnly(c, ["source", "retained", "omitted"]) &&
|
|
580
|
+
[c.source, c.retained, c.omitted].every((v) => Number.isSafeInteger(v) && v >= 0) &&
|
|
581
|
+
c.source === c.retained + c.omitted &&
|
|
582
|
+
c.retained === (r.lists[name]?.length ?? 0)))
|
|
583
|
+
return undefined;
|
|
584
|
+
if (!Object.keys(r.lists).every((k) => k in r.counts))
|
|
585
|
+
return undefined;
|
|
586
|
+
if (!Array.isArray(r.omissions) ||
|
|
587
|
+
r.omissions.length > OMISSIONS.length ||
|
|
588
|
+
!r.omissions.every((v) => OMISSIONS.includes(v)))
|
|
589
|
+
return undefined;
|
|
590
|
+
return r;
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { AgentSpec, Observation, ProposedAction, QuoteEvidence, ValidationResult } from "./types.js";
|
|
2
2
|
export interface DecisionContext {
|
|
3
|
+
/**
|
|
4
|
+
* True for the mechanical BENCHMARK agents (provider "mechanical"), whose
|
|
5
|
+
* forecast is a deliberate calibration baseline rather than an edge claim:
|
|
6
|
+
* market-implied submits exactly the market probability and base-rate
|
|
7
|
+
* submits a flat 50. Their whole purpose is to bet the same markets at a
|
|
8
|
+
* known forecast, so the forecast-edge gate below does not apply to them.
|
|
9
|
+
*/
|
|
10
|
+
mechanical?: boolean;
|
|
3
11
|
spec: AgentSpec;
|
|
4
12
|
decisionConfidence?: number;
|
|
5
13
|
observation: Observation;
|
|
6
14
|
quote?: QuoteEvidence;
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
riskIncreasesThisCycle: number;
|
|
16
|
+
riskIncreasesToday: number;
|
|
9
17
|
openCount: number;
|
|
10
18
|
cashAvailableMusd: number | null;
|
|
11
19
|
openMarginMusd: number;
|
|
@@ -13,4 +21,13 @@ export interface DecisionContext {
|
|
|
13
21
|
targetedPositionIds: number[];
|
|
14
22
|
targetedOrderIds: number[];
|
|
15
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* True when a thesis says, in so many words, that it is betting AGAINST the
|
|
26
|
+
* outcome the action is buying. Deliberately narrow: it fires only when the
|
|
27
|
+
* negation names the backed outcome directly ("betting against the Up
|
|
28
|
+
* outcome" while buying Up), because a false positive here silences a
|
|
29
|
+
* legitimate trade. Live shape 2026-09-02, cycle 863728.
|
|
30
|
+
*/
|
|
31
|
+
export declare function thesisContradictsOutcome(summary: string | undefined, outcomeName: string | undefined): boolean;
|
|
32
|
+
export declare function isRiskIncreasingAction(action: ProposedAction): boolean;
|
|
16
33
|
export declare function validateAction(action: ProposedAction, ctx: DecisionContext): ValidationResult;
|