@coinrithm/mcp-trading 0.7.13 → 0.7.15
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 +692 -573
- package/README.md +400 -364
- package/dist/agent/act.d.ts +7 -2
- package/dist/agent/act.js +10 -3
- package/dist/agent/cli.d.ts +1 -0
- package/dist/agent/cli.js +25 -4
- package/dist/agent/client.d.ts +13 -0
- package/dist/agent/client.js +107 -4
- package/dist/agent/decisionReceipt.d.ts +5 -1
- package/dist/agent/decisionReceipt.js +95 -3
- package/dist/agent/decisionValidator.js +74 -0
- package/dist/agent/definitionSnapshot.d.ts +22 -0
- package/dist/agent/definitionSnapshot.js +28 -0
- package/dist/agent/engine.d.ts +1 -0
- package/dist/agent/engine.js +1 -0
- package/dist/agent/futuresEligibility.d.ts +36 -0
- package/dist/agent/futuresEligibility.js +90 -0
- package/dist/agent/mergeRules.d.ts +1 -1
- package/dist/agent/mergeRules.js +17 -2
- package/dist/agent/observe.d.ts +9 -1
- package/dist/agent/observe.js +507 -45
- package/dist/agent/pmContext.d.ts +10 -1
- package/dist/agent/pmContext.js +163 -1
- package/dist/agent/prompt.d.ts +14 -0
- package/dist/agent/prompt.js +138 -27
- package/dist/agent/providerCapabilities.d.ts +3 -0
- package/dist/agent/providerCapabilities.js +34 -4
- package/dist/agent/providers.d.ts +3 -0
- package/dist/agent/providers.js +12 -2
- package/dist/agent/resolve.js +8 -1
- package/dist/agent/runner.js +140 -18
- package/dist/agent/skill.js +9 -0
- package/dist/agent/skillValidator.js +11 -2
- package/dist/agent/strictLint.js +1 -0
- package/dist/agent/templates.js +30 -30
- package/dist/agent/types.d.ts +138 -1
- package/dist/agent/types.js +4 -0
- package/dist/client.d.ts +11 -2
- package/dist/client.js +22 -10
- package/dist/http.d.ts +1 -0
- package/dist/http.js +17 -2
- package/dist/httpCompletion.d.ts +1 -1
- package/dist/httpCompletion.js +1 -0
- package/dist/index.js +2 -1
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +16 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +150 -66
- package/package.json +97 -97
package/dist/agent/act.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { CoinRithmClient, ProvenanceReport } from "./client.js";
|
|
2
2
|
import { ProposedAction, AgentTrace, ApiResult, Observation, QuoteEvidence } from "./types.js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/** Configured policy values the server enforces independently of the model's
|
|
4
|
+
* action fields (2026-09-24: the PM entry floor). */
|
|
5
|
+
export interface ExecutionPolicy {
|
|
6
|
+
pmMinEntryProbabilityPct?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function fetchQuote(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace?: AgentTrace, policy?: ExecutionPolicy): Promise<QuoteEvidence | undefined>;
|
|
9
|
+
export declare function executeAction(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace: AgentTrace, idempotencyKey: string, provenance?: ProvenanceReport, policy?: ExecutionPolicy): Promise<ApiResult>;
|
package/dist/agent/act.js
CHANGED
|
@@ -35,8 +35,11 @@ function fundingQuoteEvidence(value) {
|
|
|
35
35
|
function coinIdFor(observation, symbol) {
|
|
36
36
|
return (observation.watch.find((w) => w.symbol.toUpperCase() === symbol.toUpperCase())?.coinId ?? undefined);
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const pmFloorBody = (policy) => typeof policy?.pmMinEntryProbabilityPct === "number" &&
|
|
39
|
+
Number.isFinite(policy.pmMinEntryProbabilityPct)
|
|
40
|
+
? { minEntryProbabilityPct: policy.pmMinEntryProbabilityPct }
|
|
41
|
+
: {};
|
|
42
|
+
export async function fetchQuote(client, action, observation, trace, policy) {
|
|
40
43
|
let r;
|
|
41
44
|
if (action.type === "futures_open") {
|
|
42
45
|
const coinId = coinIdFor(observation, action.symbol);
|
|
@@ -64,6 +67,7 @@ export async function fetchQuote(client, action, observation, trace) {
|
|
|
64
67
|
...(action.forecastProbability != null
|
|
65
68
|
? { forecastProbability: action.forecastProbability }
|
|
66
69
|
: {}),
|
|
70
|
+
...pmFloorBody(policy),
|
|
67
71
|
}, trace);
|
|
68
72
|
}
|
|
69
73
|
else {
|
|
@@ -111,7 +115,7 @@ export async function executeAction(client, action, observation, trace, idempote
|
|
|
111
115
|
// Optional SELF-REPORTED runner provenance, attached to a pm_open only (the one
|
|
112
116
|
// durable-artifact write path here). Absent => the request is byte-identical to
|
|
113
117
|
// pre-provenance behavior.
|
|
114
|
-
provenance) {
|
|
118
|
+
provenance, policy) {
|
|
115
119
|
if (action.type === "futures_open") {
|
|
116
120
|
const coinId = coinIdFor(observation, action.symbol);
|
|
117
121
|
if (!coinId)
|
|
@@ -182,6 +186,9 @@ provenance) {
|
|
|
182
186
|
? { forecastProbability: action.forecastProbability }
|
|
183
187
|
: {}),
|
|
184
188
|
...(action.thesis?.summary ? { thesis: action.thesis.summary } : {}),
|
|
189
|
+
// The CONFIGURED entry floor rides along so the server re-checks it inside
|
|
190
|
+
// its locked open transaction (absent => byte-identical request).
|
|
191
|
+
...pmFloorBody(policy),
|
|
185
192
|
// Attach runner provenance only when present (byte-identical to before when absent).
|
|
186
193
|
...(provenance ? { provenance } : {}),
|
|
187
194
|
agentTrace: trace,
|
package/dist/agent/cli.d.ts
CHANGED
package/dist/agent/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { resolve as resolvePath, dirname, join, basename } from "node:path";
|
|
|
10
10
|
import { parse as parseYaml } from "yaml";
|
|
11
11
|
import { resolveAgent, ResolveError, mergeProseParts, isSkillProseSource, hostedProseBudget, HOSTED_PROSE_MAX_CHARS, } from "./resolve.js";
|
|
12
12
|
import { buildSpec, loadAgent } from "./skill.js";
|
|
13
|
+
import { buildAgentDefinitionSnapshot } from "./definitionSnapshot.js";
|
|
13
14
|
import { validateSkill } from "./skillValidator.js";
|
|
14
15
|
import { strictLint } from "./strictLint.js";
|
|
15
16
|
import { checkCapabilityDrift } from "./capabilityGuard.js";
|
|
@@ -227,6 +228,9 @@ export function cmdInspect(path, json = false) {
|
|
|
227
228
|
resolvedConfig: resolved.rawFrontmatter,
|
|
228
229
|
provenance: resolved.provenance,
|
|
229
230
|
contentHashes: resolved.contentHashes,
|
|
231
|
+
// The local runtime consumes these exact compiled inputs. In particular,
|
|
232
|
+
// skill ablation changes prose, not the source manifest or hard caps.
|
|
233
|
+
compiledDefinition: buildAgentDefinitionSnapshot(spec, runtimeProse(resolved)),
|
|
230
234
|
validation: { valid: v.valid, issues: v.issues, lint },
|
|
231
235
|
};
|
|
232
236
|
if (json) {
|
|
@@ -244,11 +248,17 @@ export function cmdInspect(path, json = false) {
|
|
|
244
248
|
`model: ${spec.model ? `${spec.model.provider}/${spec.model.name}` : "(host free-tier)"}`,
|
|
245
249
|
`risk: maxLeverage=${spec.risk.maxLeverage} perTradeMargin=${spec.risk.perTradeMarginMusd} requireStopLoss=${spec.risk.requireStopLoss}`,
|
|
246
250
|
`sources: ${Object.keys(resolved.contentHashes).length} file(s)`,
|
|
251
|
+
`definition: ${output.compiledDefinition.definitionHash}`,
|
|
247
252
|
`validation: ${v.valid ? "valid" : "INVALID"}${lint.length ? ` (+${lint.length} lint note(s))` : ""}`,
|
|
248
253
|
...output.warnings.map((warning) => `⚠ ${warning}`),
|
|
249
254
|
];
|
|
250
255
|
return { ok: v.valid, code: 0, lines, data: output };
|
|
251
256
|
}
|
|
257
|
+
function runtimeProse(resolved) {
|
|
258
|
+
return envFlag(process.env.COINRITHM_AGENT_DISABLE_SKILLS)
|
|
259
|
+
? mergeProseParts(resolved.proseParts.filter((part) => !isSkillProseSource(part.source)))
|
|
260
|
+
: resolved.mergedProse;
|
|
261
|
+
}
|
|
252
262
|
// Is a process still alive? signal 0 probes without sending — ESRCH means it's
|
|
253
263
|
// gone, EPERM means it exists but we can't signal it (still alive). Unknown PIDs
|
|
254
264
|
// (NaN / non-positive) are treated as alive so we never reclaim a malformed lock.
|
|
@@ -333,6 +343,16 @@ export async function cmdRun(path, opts = {}) {
|
|
|
333
343
|
return issuesResult(e.issues, "resolve failed");
|
|
334
344
|
throw e;
|
|
335
345
|
}
|
|
346
|
+
const mergedProse = runtimeProse(loaded.resolved);
|
|
347
|
+
const definition = buildAgentDefinitionSnapshot(loaded.spec, mergedProse);
|
|
348
|
+
if (opts.expectDefinition !== undefined &&
|
|
349
|
+
opts.expectDefinition !== definition.definitionHash) {
|
|
350
|
+
return fail([
|
|
351
|
+
"compiled agent definition does not match --expect-definition; no model or account call was made",
|
|
352
|
+
`current definition: ${definition.definitionHash}`,
|
|
353
|
+
"inspect the change before choosing a new baseline",
|
|
354
|
+
]);
|
|
355
|
+
}
|
|
336
356
|
const apiKey = process.env.COINRITHM_API_KEY;
|
|
337
357
|
if (!apiKey)
|
|
338
358
|
return fail([
|
|
@@ -386,14 +406,12 @@ export async function cmdRun(path, opts = {}) {
|
|
|
386
406
|
const live = !!opts.live;
|
|
387
407
|
const lines = [
|
|
388
408
|
`run ${live ? "LIVE (paper trades WILL be placed)" : "DRY-RUN (no writes; set --live or LIVE=1)"} — ${loaded.spec.name}`,
|
|
409
|
+
`definition: ${definition.definitionHash}`,
|
|
389
410
|
];
|
|
390
411
|
// Skills ablation kill-switch: drop tactic-skill prose from the prompt for
|
|
391
412
|
// token-cost control or A/B testing. Affects ONLY the run-time prompt — the
|
|
392
413
|
// resolver, manifest, and caps are untouched (the spec is still enforced).
|
|
393
414
|
const disableSkills = envFlag(process.env.COINRITHM_AGENT_DISABLE_SKILLS);
|
|
394
|
-
const mergedProse = disableSkills
|
|
395
|
-
? mergeProseParts(loaded.resolved.proseParts.filter((p) => !isSkillProseSource(p.source)))
|
|
396
|
-
: loaded.body;
|
|
397
415
|
if (disableSkills) {
|
|
398
416
|
const dropped = loaded.resolved.proseParts.filter((p) => isSkillProseSource(p.source)).length;
|
|
399
417
|
lines.push(`skills DISABLED via COINRITHM_AGENT_DISABLE_SKILLS — ${dropped} tactic skill(s) dropped from the prompt (caps unchanged)`);
|
|
@@ -442,6 +460,8 @@ function parseFlags(args) {
|
|
|
442
460
|
out.preset = args[++i];
|
|
443
461
|
else if (a === "--state")
|
|
444
462
|
out.state = args[++i];
|
|
463
|
+
else if (a === "--expect-definition")
|
|
464
|
+
out.expectDefinition = args[++i] ?? "";
|
|
445
465
|
else
|
|
446
466
|
out._.push(a);
|
|
447
467
|
}
|
|
@@ -455,7 +475,7 @@ function usageLines() {
|
|
|
455
475
|
" inspect <path> [--json]",
|
|
456
476
|
" eject <agent.md | dir>",
|
|
457
477
|
" lock <path>",
|
|
458
|
-
" run <path> [--once] [--live] [--dry-run] [--state <file>] (dry-run by default)",
|
|
478
|
+
" run <path> [--once] [--live] [--dry-run] [--state <file>] [--expect-definition sha256:...] (dry-run by default)",
|
|
459
479
|
];
|
|
460
480
|
}
|
|
461
481
|
export async function main(argv) {
|
|
@@ -489,6 +509,7 @@ export async function main(argv) {
|
|
|
489
509
|
once: flags.once,
|
|
490
510
|
live,
|
|
491
511
|
stateFile: flags.state,
|
|
512
|
+
expectDefinition: flags.expectDefinition,
|
|
492
513
|
});
|
|
493
514
|
break;
|
|
494
515
|
}
|
package/dist/agent/client.d.ts
CHANGED
|
@@ -40,13 +40,26 @@ export declare class CoinRithmClient {
|
|
|
40
40
|
rateLimitHits: number;
|
|
41
41
|
constructor(cfg: ClientConfig);
|
|
42
42
|
private request;
|
|
43
|
+
private publicRequest;
|
|
44
|
+
credentialFingerprint(): string;
|
|
43
45
|
me(trace?: AgentTrace): Promise<ApiResult>;
|
|
46
|
+
performance(trace?: AgentTrace, options?: {
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
maxRetries?: number;
|
|
49
|
+
}): Promise<ApiResult>;
|
|
44
50
|
portfolio(trace?: AgentTrace): Promise<ApiResult>;
|
|
45
51
|
wallet(query?: {
|
|
46
52
|
coinId?: string;
|
|
47
53
|
}, trace?: AgentTrace): Promise<ApiResult>;
|
|
48
54
|
resolve(q: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
49
55
|
cryptoMovers(direction: "gainers" | "losers", limit: number, trace?: AgentTrace): Promise<ApiResult>;
|
|
56
|
+
getPublicPmWhales(options?: {
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
limit?: number;
|
|
59
|
+
}): Promise<ApiResult>;
|
|
60
|
+
getPublicPmWhaleWallet(source: string, wallet: string, options?: {
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
}): Promise<ApiResult>;
|
|
50
63
|
market(coinId: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
51
64
|
candles(coinId: string, range: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
52
65
|
trades(query?: {
|
package/dist/agent/client.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// from an agent file). 429 backs off on Retry-After; 401/403/409/422 are
|
|
5
5
|
// FAIL-CLOSED cycle outcomes (returned, not retried). fetch + sleep are
|
|
6
6
|
// injectable so tests run with no network and no real waits.
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
8
9
|
import { retryAfterSeconds } from "../retryAfter.js";
|
|
9
10
|
export const DEFAULT_BASE_URL = "https://api.coinrithm.com";
|
|
@@ -54,6 +55,16 @@ export class CoinRithmClient {
|
|
|
54
55
|
this.extraHeaders = cfg.extraHeaders;
|
|
55
56
|
}
|
|
56
57
|
async request(method, path, opts = {}) {
|
|
58
|
+
const requestTimeoutMs = opts.timeoutMs !== undefined &&
|
|
59
|
+
Number.isSafeInteger(opts.timeoutMs) &&
|
|
60
|
+
opts.timeoutMs >= 1
|
|
61
|
+
? Math.min(opts.timeoutMs, this.requestTimeoutMs)
|
|
62
|
+
: this.requestTimeoutMs;
|
|
63
|
+
const maxRetries = opts.maxRetries !== undefined &&
|
|
64
|
+
Number.isSafeInteger(opts.maxRetries) &&
|
|
65
|
+
opts.maxRetries >= 0
|
|
66
|
+
? Math.min(opts.maxRetries, this.maxRetries)
|
|
67
|
+
: this.maxRetries;
|
|
57
68
|
const url = new URL(this.baseUrl + path);
|
|
58
69
|
if (opts.query) {
|
|
59
70
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -76,7 +87,7 @@ export class CoinRithmClient {
|
|
|
76
87
|
const timer = setTimeout(() => {
|
|
77
88
|
timedOut = true;
|
|
78
89
|
controller.abort(new Error("API request deadline exceeded"));
|
|
79
|
-
},
|
|
90
|
+
}, requestTimeoutMs);
|
|
80
91
|
this.signal?.addEventListener("abort", cancel, { once: true });
|
|
81
92
|
if (this.signal?.aborted)
|
|
82
93
|
cancel();
|
|
@@ -102,12 +113,12 @@ export class CoinRithmClient {
|
|
|
102
113
|
const retryAfter = retryAfterSeconds(res.headers.get("retry-after"));
|
|
103
114
|
if (res.status === 429)
|
|
104
115
|
this.rateLimitHits += 1;
|
|
105
|
-
if (res.status === 429 && attempt <
|
|
116
|
+
if (res.status === 429 && attempt < maxRetries) {
|
|
106
117
|
// Release this response before waiting so retries do not retain sockets.
|
|
107
118
|
void res.body?.cancel().catch(() => { });
|
|
108
119
|
const delayMs = (retryAfter ?? 5) * 1000;
|
|
109
120
|
// Never shorten a provider's Retry-After or overflow a Node timer.
|
|
110
|
-
if (delayMs >=
|
|
121
|
+
if (delayMs >= requestTimeoutMs)
|
|
111
122
|
await aborted;
|
|
112
123
|
else if (this.sleepFn)
|
|
113
124
|
await this.sleepFn(delayMs);
|
|
@@ -151,7 +162,7 @@ export class CoinRithmClient {
|
|
|
151
162
|
? "request_aborted"
|
|
152
163
|
: "network_error",
|
|
153
164
|
message: timedOut
|
|
154
|
-
? `API request exceeded ${
|
|
165
|
+
? `API request exceeded ${requestTimeoutMs}ms deadline`
|
|
155
166
|
: controller.signal.aborted
|
|
156
167
|
? "API request cancelled"
|
|
157
168
|
: err instanceof Error
|
|
@@ -166,10 +177,96 @@ export class CoinRithmClient {
|
|
|
166
177
|
controller.signal.removeEventListener("abort", rejectAborted);
|
|
167
178
|
}
|
|
168
179
|
}
|
|
180
|
+
// Keyless, bounded reads against the public PM surface. These deliberately
|
|
181
|
+
// bypass the authenticated request path so a runner key cannot leak into a
|
|
182
|
+
// public context lookup. No retries are added here; the enrichment is
|
|
183
|
+
// optional and its deadline includes response-body consumption.
|
|
184
|
+
async publicRequest(path, query, timeoutMs = 5_000) {
|
|
185
|
+
const url = new URL(this.baseUrl + path);
|
|
186
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
187
|
+
if (value !== undefined && value !== null && value !== "")
|
|
188
|
+
url.searchParams.set(key, String(value));
|
|
189
|
+
}
|
|
190
|
+
const controller = new AbortController();
|
|
191
|
+
if (this.signal?.aborted) {
|
|
192
|
+
return { ok: false, status: 0, data: { error: "network_error" } };
|
|
193
|
+
}
|
|
194
|
+
const cancel = () => controller.abort();
|
|
195
|
+
this.signal?.addEventListener("abort", cancel, { once: true });
|
|
196
|
+
const deadlineMs = Math.min(timeoutMs, this.requestTimeoutMs);
|
|
197
|
+
let timedOut = false;
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
timedOut = true;
|
|
200
|
+
controller.abort();
|
|
201
|
+
}, deadlineMs);
|
|
202
|
+
let rejectAborted;
|
|
203
|
+
const aborted = new Promise((_, reject) => {
|
|
204
|
+
rejectAborted = () => reject(new Error("public request aborted"));
|
|
205
|
+
controller.signal.addEventListener("abort", rejectAborted, {
|
|
206
|
+
once: true,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
if (this.signal?.aborted)
|
|
210
|
+
cancel();
|
|
211
|
+
try {
|
|
212
|
+
const perform = async () => {
|
|
213
|
+
const res = await this.fetchFn(url.toString(), {
|
|
214
|
+
method: "GET",
|
|
215
|
+
headers: { Accept: "application/json" },
|
|
216
|
+
signal: controller.signal,
|
|
217
|
+
});
|
|
218
|
+
const text = await res.text();
|
|
219
|
+
let data = text;
|
|
220
|
+
if (text) {
|
|
221
|
+
try {
|
|
222
|
+
data = JSON.parse(text);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
/* leave as text */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { ok: res.ok, status: res.status, data };
|
|
229
|
+
};
|
|
230
|
+
return await Promise.race([perform(), aborted]);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
status: 0,
|
|
236
|
+
data: {
|
|
237
|
+
error: timedOut ? "timeout" : "network_error",
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
controller.signal.removeEventListener("abort", rejectAborted);
|
|
244
|
+
this.signal?.removeEventListener("abort", cancel);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// A stable, non-reversible identity for this credential and API base, for
|
|
248
|
+
// per-agent in-memory caches that must outlive one client instance (the
|
|
249
|
+
// hosted scheduler builds a new client every cycle). Never the key itself.
|
|
250
|
+
credentialFingerprint() {
|
|
251
|
+
return createHash("sha256")
|
|
252
|
+
.update(`${this.baseUrl}\n${this.apiKey}`, "utf8")
|
|
253
|
+
.digest("hex")
|
|
254
|
+
.slice(0, 32);
|
|
255
|
+
}
|
|
169
256
|
// ── reads ──────────────────────────────────────────────────────────────────
|
|
170
257
|
me(trace) {
|
|
171
258
|
return this.request("GET", "/api/agent/me", { trace });
|
|
172
259
|
}
|
|
260
|
+
// GET /api/agent/performance: the calling key's own realized performance.
|
|
261
|
+
// Newer backends add an optional `pmCalibration` block (the key's settled PM
|
|
262
|
+
// forecast record), parsed defensively in observe.
|
|
263
|
+
performance(trace, options) {
|
|
264
|
+
return this.request("GET", "/api/agent/performance", {
|
|
265
|
+
trace,
|
|
266
|
+
timeoutMs: options?.timeoutMs,
|
|
267
|
+
maxRetries: options?.maxRetries,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
173
270
|
portfolio(trace) {
|
|
174
271
|
return this.request("GET", "/api/agent/portfolio", { trace });
|
|
175
272
|
}
|
|
@@ -186,6 +283,12 @@ export class CoinRithmClient {
|
|
|
186
283
|
? "/api/coins/top-losers"
|
|
187
284
|
: "/api/coins/top-gainers", { query: { limit }, trace });
|
|
188
285
|
}
|
|
286
|
+
getPublicPmWhales(options) {
|
|
287
|
+
return this.publicRequest("/api/prediction-markets/whales", { limit: options?.limit }, options?.timeoutMs);
|
|
288
|
+
}
|
|
289
|
+
getPublicPmWhaleWallet(source, wallet, options) {
|
|
290
|
+
return this.publicRequest(`/api/prediction-markets/whales/wallets/${encodeURIComponent(source)}/${encodeURIComponent(wallet)}`, undefined, options?.timeoutMs);
|
|
291
|
+
}
|
|
189
292
|
market(coinId, trace) {
|
|
190
293
|
return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { trace });
|
|
191
294
|
}
|
|
@@ -8,7 +8,7 @@ interface Row {
|
|
|
8
8
|
}
|
|
9
9
|
export interface DecisionInputRecord {
|
|
10
10
|
version: "coinrithm.decision-input.v1";
|
|
11
|
-
projectionVersion?: "coinrithm.decision-input-projection.v2";
|
|
11
|
+
projectionVersion?: "coinrithm.decision-input-projection.v2" | "coinrithm.decision-input-projection.v3";
|
|
12
12
|
visibility: "private";
|
|
13
13
|
completeness: "partial";
|
|
14
14
|
phase: DecisionInputPhase;
|
|
@@ -21,6 +21,10 @@ export interface DecisionInputRecord {
|
|
|
21
21
|
dailyRiskBudget: DailyRiskBudget | null;
|
|
22
22
|
guardState: Record<string, number | boolean | null>;
|
|
23
23
|
account: Record<string, number | boolean | string | null> | null;
|
|
24
|
+
marketMood?: {
|
|
25
|
+
fearGreed: number | null;
|
|
26
|
+
fetchedAt: string | null;
|
|
27
|
+
} | null;
|
|
24
28
|
lists: Record<string, Row[]>;
|
|
25
29
|
counts: Record<string, {
|
|
26
30
|
source: number;
|
|
@@ -22,6 +22,20 @@ const fingerprint = (value) => typeof value === "string" && /^sha256:[a-f0-9]{64
|
|
|
22
22
|
? value
|
|
23
23
|
: null;
|
|
24
24
|
const numeric = (raw, keys) => Object.fromEntries(keys.map((key) => [key, num(raw[key])]));
|
|
25
|
+
const count = (value) => Number.isSafeInteger(value) && value >= 0
|
|
26
|
+
? value
|
|
27
|
+
: null;
|
|
28
|
+
const fearGreed = (value) => {
|
|
29
|
+
const v = num(value);
|
|
30
|
+
return v !== null && v >= 0 && v <= 100 ? v : null;
|
|
31
|
+
};
|
|
32
|
+
const PROJECTION_V3 = "coinrithm.decision-input-projection.v3";
|
|
33
|
+
// Watch-row fields that exist only from projection v3 on.
|
|
34
|
+
const SENTIMENT_V3_KEYS = [
|
|
35
|
+
"sentimentTotalVotes",
|
|
36
|
+
"sentimentDayUtc",
|
|
37
|
+
"sentimentUpdatedAt",
|
|
38
|
+
];
|
|
25
39
|
const freshness = (value) => {
|
|
26
40
|
const raw = obj(parseFreshness({ freshness: value }));
|
|
27
41
|
return {
|
|
@@ -49,7 +63,7 @@ export function buildDecisionInputRecord(input) {
|
|
|
49
63
|
const budget = buildDailyRiskBudget(input.spec, input.state);
|
|
50
64
|
const record = {
|
|
51
65
|
version: "coinrithm.decision-input.v1",
|
|
52
|
-
projectionVersion:
|
|
66
|
+
projectionVersion: PROJECTION_V3,
|
|
53
67
|
visibility: "private",
|
|
54
68
|
completeness: "partial",
|
|
55
69
|
phase: input.phase,
|
|
@@ -115,6 +129,12 @@ export function buildDecisionInputRecord(input) {
|
|
|
115
129
|
equityMusd: num(obs.equityMusd),
|
|
116
130
|
polledBeforeWrite: bool(obs.polledBeforeWrite),
|
|
117
131
|
};
|
|
132
|
+
record.marketMood = obs.marketMood
|
|
133
|
+
? {
|
|
134
|
+
fearGreed: fearGreed(obs.marketMood.fearGreed),
|
|
135
|
+
fetchedAt: sourceTimestamp(obs.marketMood.fetchedAt) ?? null,
|
|
136
|
+
}
|
|
137
|
+
: null;
|
|
118
138
|
const add = (name, items, project) => {
|
|
119
139
|
const all = arr(items);
|
|
120
140
|
// Bound work/space independently of the upstream list limit. Preserve order,
|
|
@@ -137,7 +157,36 @@ export function buildDecisionInputRecord(input) {
|
|
|
137
157
|
"change7d",
|
|
138
158
|
"sentimentBullishPct",
|
|
139
159
|
]),
|
|
160
|
+
// The sample and its own clocks; null = unknown, never "fresh".
|
|
161
|
+
sentimentTotalVotes: count(r.sentimentTotalVotes),
|
|
162
|
+
sentimentDayUtc: sourceTimestamp(r.sentimentDayUtc) ?? null,
|
|
163
|
+
sentimentUpdatedAt: sourceTimestamp(r.sentimentUpdatedAt) ?? null,
|
|
140
164
|
freshness: freshness(r.freshness),
|
|
165
|
+
...(r.indicatorContext
|
|
166
|
+
? {
|
|
167
|
+
indicatorContext: {
|
|
168
|
+
range: code(obj(r.indicatorContext).range, ["1D"]),
|
|
169
|
+
asOf: sourceTimestamp(obj(r.indicatorContext).asOf) ?? null,
|
|
170
|
+
intervalStatus: code(obj(r.indicatorContext).intervalStatus, [
|
|
171
|
+
"regular",
|
|
172
|
+
"irregular",
|
|
173
|
+
"unknown",
|
|
174
|
+
]),
|
|
175
|
+
...numeric(obj(r.indicatorContext), [
|
|
176
|
+
"nominalIntervalSeconds",
|
|
177
|
+
"barCount",
|
|
178
|
+
"timestampedBarCount",
|
|
179
|
+
"checkedIntervalCount",
|
|
180
|
+
"irregularIntervalCount",
|
|
181
|
+
"maxGapSeconds",
|
|
182
|
+
]),
|
|
183
|
+
recent15: {
|
|
184
|
+
...numeric(obj(obj(r.indicatorContext).recent15), ["barCount"]),
|
|
185
|
+
intervalStatus: code(obj(obj(r.indicatorContext).recent15).intervalStatus, ["regular", "irregular", "unknown"]),
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
141
190
|
indicators: {
|
|
142
191
|
...numeric(obj(r.indicators), [
|
|
143
192
|
"asOfClose",
|
|
@@ -332,8 +381,10 @@ const LIST_KEYS = {
|
|
|
332
381
|
"change24h",
|
|
333
382
|
"change7d",
|
|
334
383
|
"sentimentBullishPct",
|
|
384
|
+
...SENTIMENT_V3_KEYS,
|
|
335
385
|
"freshness",
|
|
336
386
|
"indicators",
|
|
387
|
+
"indicatorContext",
|
|
337
388
|
"fundamentals",
|
|
338
389
|
],
|
|
339
390
|
futuresPositions: [
|
|
@@ -379,6 +430,19 @@ const LIST_KEYS = {
|
|
|
379
430
|
universeMovers: ["symbol", "change24hPct", "priceUsd"],
|
|
380
431
|
};
|
|
381
432
|
const NESTED_KEYS = {
|
|
433
|
+
indicatorContext: [
|
|
434
|
+
"range",
|
|
435
|
+
"asOf",
|
|
436
|
+
"nominalIntervalSeconds",
|
|
437
|
+
"barCount",
|
|
438
|
+
"timestampedBarCount",
|
|
439
|
+
"checkedIntervalCount",
|
|
440
|
+
"irregularIntervalCount",
|
|
441
|
+
"maxGapSeconds",
|
|
442
|
+
"intervalStatus",
|
|
443
|
+
"recent15",
|
|
444
|
+
],
|
|
445
|
+
recent15: ["barCount", "intervalStatus"],
|
|
382
446
|
freshness: ["status", "ageSeconds", "asOf", "basis"],
|
|
383
447
|
quality: [
|
|
384
448
|
"decisionEligible",
|
|
@@ -428,10 +492,22 @@ function validRow(value, keys) {
|
|
|
428
492
|
return true;
|
|
429
493
|
if (NESTED_KEYS[key])
|
|
430
494
|
return validRow(v, NESTED_KEYS[key]);
|
|
431
|
-
if (
|
|
495
|
+
if ([
|
|
496
|
+
"asOf",
|
|
497
|
+
"assessedAt",
|
|
498
|
+
"openedAt",
|
|
499
|
+
"sentimentDayUtc",
|
|
500
|
+
"sentimentUpdatedAt",
|
|
501
|
+
].includes(key))
|
|
432
502
|
return sourceTimestamp(v) === v;
|
|
503
|
+
if (key === "sentimentTotalVotes")
|
|
504
|
+
return count(v) !== null;
|
|
433
505
|
if (key === "basis")
|
|
434
506
|
return code(v, FRESHNESS_BASES) !== null;
|
|
507
|
+
if (key === "range")
|
|
508
|
+
return code(v, ["1D"]) !== null;
|
|
509
|
+
if (key === "intervalStatus")
|
|
510
|
+
return code(v, ["regular", "irregular", "unknown"]) !== null;
|
|
435
511
|
if (key === "policyVersion")
|
|
436
512
|
return typeof v === "string" && /^pm-quality-\d{1,3}$/.test(v);
|
|
437
513
|
if (key === "warningReasons" || key === "blockReasons") {
|
|
@@ -522,13 +598,29 @@ export function sanitizeDecisionInputRecord(value) {
|
|
|
522
598
|
"dailyRiskBudget",
|
|
523
599
|
"guardState",
|
|
524
600
|
"account",
|
|
601
|
+
"marketMood",
|
|
525
602
|
"lists",
|
|
526
603
|
"counts",
|
|
527
604
|
"omissions",
|
|
528
605
|
]))
|
|
529
606
|
return undefined;
|
|
530
607
|
if (r.projectionVersion !== undefined &&
|
|
531
|
-
r.projectionVersion !== "coinrithm.decision-input-projection.v2"
|
|
608
|
+
r.projectionVersion !== "coinrithm.decision-input-projection.v2" &&
|
|
609
|
+
r.projectionVersion !== PROJECTION_V3)
|
|
610
|
+
return undefined;
|
|
611
|
+
// An older record must never carry, or be mistaken for, v3 fields.
|
|
612
|
+
const v3 = r.projectionVersion === PROJECTION_V3;
|
|
613
|
+
if (!v3 &&
|
|
614
|
+
(r.marketMood !== undefined ||
|
|
615
|
+
arr(obj(r.lists).watch).some((row) => SENTIMENT_V3_KEYS.some((k) => k in obj(row)))))
|
|
616
|
+
return undefined;
|
|
617
|
+
if (r.marketMood !== undefined &&
|
|
618
|
+
r.marketMood !== null &&
|
|
619
|
+
(!keysOnly(r.marketMood, ["fearGreed", "fetchedAt"]) ||
|
|
620
|
+
(r.marketMood.fearGreed !== null &&
|
|
621
|
+
fearGreed(r.marketMood.fearGreed) === null) ||
|
|
622
|
+
(r.marketMood.fetchedAt !== null &&
|
|
623
|
+
sourceTimestamp(r.marketMood.fetchedAt) !== r.marketMood.fetchedAt)))
|
|
532
624
|
return undefined;
|
|
533
625
|
if (r.version !== "coinrithm.decision-input.v1" ||
|
|
534
626
|
r.visibility !== "private" ||
|
|
@@ -17,6 +17,44 @@ const PM_MIN_FORECAST_EDGE_POINTS = (() => {
|
|
|
17
17
|
return Number.isFinite(raw) && raw >= 0 ? raw : 2;
|
|
18
18
|
})();
|
|
19
19
|
const PM_MIN_STAKE_MUSD = 10; // server minimum prediction-market stake
|
|
20
|
+
// Match backend-v2 validateSlTpPrices, including its boundary tolerance. This
|
|
21
|
+
// preflight uses observed evidence only; the API still checks a fresh mark and
|
|
22
|
+
// locked position at execution. Older observations can omit these prices.
|
|
23
|
+
const TRIGGER_PRICE_EPS = 1e-8;
|
|
24
|
+
function validateTriggerUpdate(action, pos) {
|
|
25
|
+
// executeAction omits null as well as undefined; neither clears a trigger in
|
|
26
|
+
// this runner. Check the resulting state, including retained protection.
|
|
27
|
+
const sl = action.stopLossPrice ?? pos.stopLossPrice;
|
|
28
|
+
const tp = action.takeProfitPrice ?? pos.takeProfitPrice;
|
|
29
|
+
const positive = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
30
|
+
if (sl != null && !positive(sl))
|
|
31
|
+
return fail("invalid_stop_loss_price", `position ${pos.id}: stop loss must be finite and positive`);
|
|
32
|
+
if (tp != null && !positive(tp))
|
|
33
|
+
return fail("invalid_take_profit_price", `position ${pos.id}: take profit must be finite and positive`);
|
|
34
|
+
if (pos.side !== "long" && pos.side !== "short")
|
|
35
|
+
return ok();
|
|
36
|
+
const long = pos.side === "long";
|
|
37
|
+
const mark = pos.markPrice;
|
|
38
|
+
const liq = pos.liquidationPrice;
|
|
39
|
+
if (sl != null) {
|
|
40
|
+
if (positive(mark) &&
|
|
41
|
+
(long ? sl >= mark - TRIGGER_PRICE_EPS : sl <= mark + TRIGGER_PRICE_EPS)) {
|
|
42
|
+
return fail(long ? "stop_loss_not_below_mark" : "stop_loss_not_above_mark", `position ${pos.id}: ${pos.side} effective stop ${sl} must be ${long ? "below" : "above"} observed mark ${mark}; omitted triggers keep their existing values`);
|
|
43
|
+
}
|
|
44
|
+
if (positive(liq) &&
|
|
45
|
+
(long ? sl <= liq + TRIGGER_PRICE_EPS : sl >= liq - TRIGGER_PRICE_EPS)) {
|
|
46
|
+
return fail(long
|
|
47
|
+
? "stop_loss_not_above_liquidation"
|
|
48
|
+
: "stop_loss_not_below_liquidation", `position ${pos.id}: ${pos.side} effective stop ${sl} must be ${long ? "above" : "below"} observed liquidation ${liq}; omitted triggers keep their existing values`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (tp != null &&
|
|
52
|
+
positive(mark) &&
|
|
53
|
+
(long ? tp <= mark + TRIGGER_PRICE_EPS : tp >= mark - TRIGGER_PRICE_EPS)) {
|
|
54
|
+
return fail(long ? "take_profit_not_above_mark" : "take_profit_not_below_mark", `position ${pos.id}: ${pos.side} effective take profit ${tp} must be ${long ? "above" : "below"} observed mark ${mark}; omitted triggers keep their existing values`);
|
|
55
|
+
}
|
|
56
|
+
return ok();
|
|
57
|
+
}
|
|
20
58
|
// Entry budgets are exposure budgets, not emergency-action budgets. Closing a
|
|
21
59
|
// futures position, updating its protection, cancelling an order, or selling
|
|
22
60
|
// spot reduces/contains risk and must remain available after an entry cap.
|
|
@@ -219,6 +257,7 @@ export function validateAction(action, ctx) {
|
|
|
219
257
|
if (!hasTrigger) {
|
|
220
258
|
return fail("sltp_no_op", "futures_set_sltp must set at least one positive stopLossPrice or takeProfitPrice");
|
|
221
259
|
}
|
|
260
|
+
return validateTriggerUpdate(action, pos);
|
|
222
261
|
}
|
|
223
262
|
return ok();
|
|
224
263
|
}
|
|
@@ -316,6 +355,41 @@ export function validateAction(action, ctx) {
|
|
|
316
355
|
if (!ctx.quote.freshness || ctx.quote.freshness.status !== "fresh") {
|
|
317
356
|
return fail("stale_quote", `quote freshness ${ctx.quote.freshness?.status ?? "missing"} (need fresh)`);
|
|
318
357
|
}
|
|
358
|
+
// Hard entry-price floor (opt-in, 2026-09-24): compares the CHOSEN
|
|
359
|
+
// outcome's raw market probability at entry (the quote's entryProbability,
|
|
360
|
+
// points) with risk.pmMinEntryProbabilityPct. Fees are deliberately left
|
|
361
|
+
// out: a 19-point outcome fails a 20-point floor even when fees push its
|
|
362
|
+
// all-in cost above 20 (fees belong to the edge check below). No
|
|
363
|
+
// discovery-price fallback: with a floor set and no finite quoted
|
|
364
|
+
// probability the open is rejected, never waved through.
|
|
365
|
+
const entryFloor = spec.risk.pmMinEntryProbabilityPct;
|
|
366
|
+
if (entryFloor !== undefined) {
|
|
367
|
+
// A present-but-invalid floor never silently means "no floor".
|
|
368
|
+
if (typeof entryFloor !== "number" ||
|
|
369
|
+
!Number.isFinite(entryFloor) ||
|
|
370
|
+
entryFloor < 0 ||
|
|
371
|
+
entryFloor > 100) {
|
|
372
|
+
return fail("pm_entry_floor_invalid", `risk.pmMinEntryProbabilityPct ${JSON.stringify(entryFloor)} is not a number between 0 and 100`);
|
|
373
|
+
}
|
|
374
|
+
const market = ctx.quote.entryProbability;
|
|
375
|
+
if (typeof market !== "number" ||
|
|
376
|
+
!Number.isFinite(market) ||
|
|
377
|
+
market < 0 ||
|
|
378
|
+
market > 100) {
|
|
379
|
+
return fail("pm_entry_price_unavailable", `entry floor ${entryFloor}pt is set but the quote carries no usable market probability for the chosen outcome (${JSON.stringify(market ?? null)})`);
|
|
380
|
+
}
|
|
381
|
+
if (market + 1e-9 < entryFloor) {
|
|
382
|
+
const stake = ctx.quote.stakeMusd;
|
|
383
|
+
const shares = ctx.quote.sharesEstimate;
|
|
384
|
+
const cost = typeof stake === "number" &&
|
|
385
|
+
typeof shares === "number" &&
|
|
386
|
+
stake > 0 &&
|
|
387
|
+
shares > 0
|
|
388
|
+
? ` (fee-inclusive cost ${((stake / shares) * 100).toFixed(1)}pt)`
|
|
389
|
+
: "";
|
|
390
|
+
return fail("pm_entry_below_floor", `market ${market.toFixed(1)}pt is below the ${entryFloor}pt entry floor for the chosen outcome${cost}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
319
393
|
// Forecast consistency. By prompt contract forecastProbability is the
|
|
320
394
|
// model's own probability (1-99) that the outcome IT IS BACKING wins, so
|
|
321
395
|
// buying that outcome only makes sense when the forecast clears what the
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentSpec } from "./types.js";
|
|
2
|
+
/** Private, portable definition; deliberately excludes mutable account state. */
|
|
3
|
+
export interface AgentDefinitionSnapshot {
|
|
4
|
+
schema: "coinrithm.agent-definition.v1";
|
|
5
|
+
engine: {
|
|
6
|
+
packageVersion: string;
|
|
7
|
+
resolverVersion: string;
|
|
8
|
+
indicatorVersion: string;
|
|
9
|
+
apiContractVersion: string;
|
|
10
|
+
};
|
|
11
|
+
/** Pass the actual post-overlay spec, without rebuilding it from frontmatter. */
|
|
12
|
+
spec: AgentSpec;
|
|
13
|
+
/** Exact strategy prose supplied to runCycle, after any skills ablation. */
|
|
14
|
+
mergedProse: string;
|
|
15
|
+
definitionHash: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Binds the compiled definition and declared engine versions, not the original
|
|
19
|
+
* source folder alone. This is an integrity/comparison hash, not a signature,
|
|
20
|
+
* complete replay record, or attestation of a particular deployed binary.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildAgentDefinitionSnapshot(spec: AgentSpec, mergedProse: string): AgentDefinitionSnapshot;
|