@coinrithm/mcp-trading 0.7.8 → 0.7.10
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 +121 -0
- package/README.md +93 -69
- package/dist/agent/capitalSizing.js +17 -4
- package/dist/agent/cli.js +11 -1
- package/dist/agent/client.d.ts +8 -1
- package/dist/agent/client.js +98 -39
- package/dist/agent/configurationWarnings.d.ts +1 -0
- package/dist/agent/configurationWarnings.js +21 -0
- package/dist/agent/decisionReceipt.d.ts +4 -1
- package/dist/agent/decisionReceipt.js +27 -3
- package/dist/agent/decisionValidator.js +4 -0
- package/dist/agent/engine.d.ts +1 -1
- package/dist/agent/engine.js +1 -1
- package/dist/agent/entryPredicates.d.ts +10 -0
- package/dist/agent/entryPredicates.js +67 -0
- package/dist/agent/gate.js +14 -11
- package/dist/agent/opportunityReporter.d.ts +16 -0
- package/dist/agent/opportunityReporter.js +38 -0
- package/dist/agent/prompt.js +5 -0
- package/dist/agent/providers.d.ts +2 -1
- package/dist/agent/providers.js +109 -19
- package/dist/agent/reconcileObservation.d.ts +2 -0
- package/dist/agent/reconcileObservation.js +33 -0
- package/dist/agent/runner.js +32 -74
- package/dist/agent/skill.js +6 -0
- package/dist/agent/skillValidator.js +3 -0
- package/dist/agent/state.js +17 -2
- package/dist/agent/strictLint.js +1 -0
- package/dist/agent/templates.js +34 -30
- package/dist/agent/types.d.ts +1 -0
- package/dist/agent/util.js +3 -3
- package/dist/client.js +3 -2
- package/dist/http.d.ts +7 -1
- package/dist/http.js +36 -16
- package/dist/httpCompletion.d.ts +33 -0
- package/dist/httpCompletion.js +220 -0
- package/dist/retryAfter.d.ts +1 -0
- package/dist/retryAfter.js +16 -0
- package/dist/tools.js +19 -1
- package/package.json +13 -2
package/dist/agent/providers.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// never from an agent file. One call returns one chunk of text that must be a
|
|
3
3
|
// single structured-JSON decision (parsed in decision.ts). No free-form tool
|
|
4
4
|
// execution — the model only proposes; the runner disposes.
|
|
5
|
+
import { retryAfterSeconds } from "../retryAfter.js";
|
|
5
6
|
import { chatShapeFor, buildChatBody, DECISION_TOOL_NAME, NVIDIA_BASE_URL as CAP_NVIDIA_BASE_URL, } from "./providerCapabilities.js";
|
|
6
7
|
// NVIDIA NIM is OpenAI-compatible; the `nvidia` preset hard-wires the hosted
|
|
7
8
|
// endpoint so an agent only needs `{ provider: nvidia, name: "<model id>" }`.
|
|
@@ -27,6 +28,7 @@ const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai
|
|
|
27
28
|
// (the recurring Leo/70B timeout). A real hang still aborts -> retried next cadence.
|
|
28
29
|
// MUST stay below the scheduler's RUN_LOCK_SECONDS and HEARTBEAT_STALE_MS.
|
|
29
30
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
31
|
+
const RETRYABLE_SERVER_STATUSES = new Set([500, 502, 503, 504]);
|
|
30
32
|
// Per-route request quirks (reasoning toggles, token param, temperature) live
|
|
31
33
|
// in the capability table — providerCapabilities.ts is the single source; this
|
|
32
34
|
// module only assembles and sends.
|
|
@@ -58,22 +60,24 @@ function callError(err, timeoutMs) {
|
|
|
58
60
|
}
|
|
59
61
|
return err instanceof Error ? err.message : String(err);
|
|
60
62
|
}
|
|
63
|
+
// Error text reaches retained cycle evidence. Redact before truncating so a
|
|
64
|
+
// credential crossing the size boundary cannot leave a partial key behind.
|
|
65
|
+
function safeProviderError(text, apiKey) {
|
|
66
|
+
return (apiKey ? text.split(apiKey).join("[redacted]") : text)
|
|
67
|
+
.replace(/Bearer\s+[^\s"'<>\\]+/gi, "Bearer [redacted]")
|
|
68
|
+
.replace(/(?:nvapi-|crk_live_|sk-|sk_live_|ghp_|AIza)[A-Za-z0-9_-]+/g, "[redacted]")
|
|
69
|
+
.replace(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[redacted]")
|
|
70
|
+
.replace(/((?:api[_-]?key|access[_-]?token|password|secret)["']?\s*[:=]\s*["']?)[^\s"',}&<>]+/gi, "$1[redacted]")
|
|
71
|
+
.slice(0, 2000);
|
|
72
|
+
}
|
|
61
73
|
// Parse a Retry-After header (delta-seconds or HTTP-date) into ms, capped at
|
|
62
74
|
// one hour — a provider asking for more is treated as "an hour, then re-probe".
|
|
63
75
|
const RETRY_AFTER_CAP_MS = 3_600_000;
|
|
64
76
|
function retryAfterMs(res) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (Number.isFinite(secs) && secs >= 0) {
|
|
70
|
-
return Math.min(Math.round(secs * 1000), RETRY_AFTER_CAP_MS);
|
|
71
|
-
}
|
|
72
|
-
const at = Date.parse(raw);
|
|
73
|
-
if (!Number.isFinite(at))
|
|
74
|
-
return undefined;
|
|
75
|
-
const ms = at - Date.now();
|
|
76
|
-
return ms > 0 ? Math.min(ms, RETRY_AFTER_CAP_MS) : 0;
|
|
77
|
+
const seconds = retryAfterSeconds(res.headers.get("retry-after"));
|
|
78
|
+
return seconds === undefined
|
|
79
|
+
? undefined
|
|
80
|
+
: Math.min(Math.round(seconds * 1000), RETRY_AFTER_CAP_MS);
|
|
77
81
|
}
|
|
78
82
|
function envKey(provider, env) {
|
|
79
83
|
switch (provider) {
|
|
@@ -165,7 +169,7 @@ class AnthropicProvider {
|
|
|
165
169
|
ok: false,
|
|
166
170
|
// Cap the upstream body: it lands in agent_cycles.skip_reason, so an
|
|
167
171
|
// unbounded provider error page must not bloat the ledger row.
|
|
168
|
-
error: `anthropic HTTP ${res.status}: ${(await res.text()
|
|
172
|
+
error: `anthropic HTTP ${res.status}: ${safeProviderError(await res.text(), this.apiKey)}`,
|
|
169
173
|
status: res.status,
|
|
170
174
|
retryAfterMs: retryAfterMs(res),
|
|
171
175
|
};
|
|
@@ -186,9 +190,9 @@ class AnthropicProvider {
|
|
|
186
190
|
catch (err) {
|
|
187
191
|
return {
|
|
188
192
|
ok: false,
|
|
189
|
-
error: failureResponse
|
|
193
|
+
error: safeProviderError(failureResponse
|
|
190
194
|
? `anthropic HTTP ${failureResponse.status}: ${callError(err, timeoutMs)}`
|
|
191
|
-
: callError(err, timeoutMs),
|
|
195
|
+
: callError(err, timeoutMs), this.apiKey),
|
|
192
196
|
...(failureResponse
|
|
193
197
|
? {
|
|
194
198
|
status: failureResponse.status,
|
|
@@ -240,9 +244,12 @@ class OpenAiCompatProvider {
|
|
|
240
244
|
ok: false,
|
|
241
245
|
// Cap the upstream body: it lands in agent_cycles.skip_reason, so an
|
|
242
246
|
// unbounded provider error page must not bloat the ledger row.
|
|
243
|
-
error: `provider HTTP ${res.status}: ${(await res.text()
|
|
247
|
+
error: `provider HTTP ${res.status}: ${safeProviderError(await res.text(), this.apiKey)}`,
|
|
244
248
|
status: res.status,
|
|
245
249
|
retryAfterMs: retryAfterMs(res),
|
|
250
|
+
...(RETRYABLE_SERVER_STATUSES.has(res.status)
|
|
251
|
+
? { retryableHttpFailure: true }
|
|
252
|
+
: {}),
|
|
246
253
|
};
|
|
247
254
|
}
|
|
248
255
|
const json = (await res.json());
|
|
@@ -263,9 +270,9 @@ class OpenAiCompatProvider {
|
|
|
263
270
|
catch (err) {
|
|
264
271
|
return {
|
|
265
272
|
ok: false,
|
|
266
|
-
error: failureResponse
|
|
273
|
+
error: safeProviderError(failureResponse
|
|
267
274
|
? `provider HTTP ${failureResponse.status}: ${callError(err, timeoutMs)}`
|
|
268
|
-
: callError(err, timeoutMs),
|
|
275
|
+
: callError(err, timeoutMs), this.apiKey),
|
|
269
276
|
...(failureResponse
|
|
270
277
|
? {
|
|
271
278
|
status: failureResponse.status,
|
|
@@ -276,6 +283,86 @@ class OpenAiCompatProvider {
|
|
|
276
283
|
}
|
|
277
284
|
}
|
|
278
285
|
}
|
|
286
|
+
// A direct NVIDIA 5xx previously lost the whole cycle until the next cadence.
|
|
287
|
+
// Retry only an explicit server refusal, once, on the identical configured
|
|
288
|
+
// route. The shared router owns its own attempt/capacity budget and MUST NOT
|
|
289
|
+
// receive this wrapper. No auth/404/429, network, timeout or decision repair
|
|
290
|
+
// retries; no model substitution or request-parameter changes.
|
|
291
|
+
class SameModelRetryProvider {
|
|
292
|
+
delegate;
|
|
293
|
+
provider;
|
|
294
|
+
model;
|
|
295
|
+
label;
|
|
296
|
+
constructor(delegate, provider, model) {
|
|
297
|
+
this.delegate = delegate;
|
|
298
|
+
this.provider = provider;
|
|
299
|
+
this.model = model;
|
|
300
|
+
this.label = delegate.label;
|
|
301
|
+
}
|
|
302
|
+
async decide(input) {
|
|
303
|
+
const started = Date.now();
|
|
304
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
305
|
+
const first = await this.delegate.decide(input);
|
|
306
|
+
const firstFinished = Date.now();
|
|
307
|
+
if (first.ok || !first.retryableHttpFailure) {
|
|
308
|
+
return first;
|
|
309
|
+
}
|
|
310
|
+
const delayMs = Math.max(1000, first.retryAfterMs ?? 0);
|
|
311
|
+
// Honor the provider's cooldown; a long one waits for the next cycle. Both
|
|
312
|
+
// attempts and this backoff share the ORIGINAL deadline, never two 5m caps.
|
|
313
|
+
if (delayMs > 5000 || delayMs >= timeoutMs - (firstFinished - started)) {
|
|
314
|
+
return first;
|
|
315
|
+
}
|
|
316
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
317
|
+
const retryStarted = Date.now();
|
|
318
|
+
const remainingMs = timeoutMs - (retryStarted - started);
|
|
319
|
+
if (remainingMs <= 0)
|
|
320
|
+
return first;
|
|
321
|
+
const result = await this.delegate.decide({
|
|
322
|
+
...input,
|
|
323
|
+
timeoutMs: remainingMs,
|
|
324
|
+
});
|
|
325
|
+
const attempt = (res, latencyMs) => ({
|
|
326
|
+
provider: this.provider,
|
|
327
|
+
model: this.model,
|
|
328
|
+
outcome: res.ok ? "success" : "failed",
|
|
329
|
+
latencyMs,
|
|
330
|
+
...(!res.ok
|
|
331
|
+
? {
|
|
332
|
+
error: res.error,
|
|
333
|
+
status: res.status,
|
|
334
|
+
retryAfterMs: res.retryAfterMs,
|
|
335
|
+
...(res.status !== undefined
|
|
336
|
+
? {
|
|
337
|
+
failureClass: res.status === 429
|
|
338
|
+
? "capacity"
|
|
339
|
+
: res.status >= 500
|
|
340
|
+
? "transient"
|
|
341
|
+
: "permanent",
|
|
342
|
+
}
|
|
343
|
+
: {}),
|
|
344
|
+
}
|
|
345
|
+
: {}),
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
// Usage, when present, describes only the final response. The preceding
|
|
349
|
+
// 5xx did not report usage; attempt evidence is not total billed tokens
|
|
350
|
+
// or a certificate that returned text passed the trading-decision parser.
|
|
351
|
+
...result,
|
|
352
|
+
route: {
|
|
353
|
+
policyVersion: "coinrithm.configured-same-model-retry.v1",
|
|
354
|
+
profile: "configured",
|
|
355
|
+
effectiveProvider: this.provider,
|
|
356
|
+
effectiveModel: this.model,
|
|
357
|
+
reason: "configured_direct",
|
|
358
|
+
attempts: [
|
|
359
|
+
attempt(first, firstFinished - started),
|
|
360
|
+
attempt(result, Date.now() - retryStarted),
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
279
366
|
// Build the provider from the spec's model block + env key. Throws a clear
|
|
280
367
|
// error if no model is configured (self-host requires one) or the env key is
|
|
281
368
|
// missing. fetch is injectable for tests.
|
|
@@ -308,7 +395,10 @@ export function selectProvider(spec, env, fetchFn = fetch) {
|
|
|
308
395
|
if (!resolvedBase) {
|
|
309
396
|
throw new Error("openai-compatible provider needs model.baseUrl");
|
|
310
397
|
}
|
|
311
|
-
|
|
398
|
+
const direct = new OpenAiCompatProvider(provider, name, key, resolvedBase, fetchFn);
|
|
399
|
+
return provider === "nvidia"
|
|
400
|
+
? new SameModelRetryProvider(direct, provider, name)
|
|
401
|
+
: direct;
|
|
312
402
|
}
|
|
313
403
|
// Build a provider for an EXPLICIT route + raw key (no spec, no env) — the
|
|
314
404
|
// decision probe's entry point. Same classes as selectProvider, so a probe
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { accrueRealized } from "./state.js";
|
|
2
|
+
import { asObj, asNum, asStr } from "./extract.js";
|
|
3
|
+
export function reconcileObservation(state, observation) {
|
|
4
|
+
accrueRealized(state, observation.newClosedTrades);
|
|
5
|
+
state.cursor = observation.syncCursor;
|
|
6
|
+
for (const t of observation.newClosedTrades) {
|
|
7
|
+
state.seen.push(`${asStr(asObj(t).venue) ?? "futures"}:${asNum(asObj(t).id) ?? String(asObj(t).id)}`);
|
|
8
|
+
}
|
|
9
|
+
state.seen = state.seen.slice(-500);
|
|
10
|
+
// Slice-3 reflection: journal closed-trade OUTCOMES (not just opens) so the agent
|
|
11
|
+
// remembers how its theses RESOLVED — a stop-out it should not revenge-trade, a
|
|
12
|
+
// winner its style works on. Defensive field reads (the /trades shape varies);
|
|
13
|
+
// a partial entry is harmless, a missing one is skipped.
|
|
14
|
+
for (const t of observation.newClosedTrades.slice(-5)) {
|
|
15
|
+
const o = asObj(t);
|
|
16
|
+
const sym = asStr(o.symbol) ?? asStr(o.coinSymbol) ?? asStr(o.coinId);
|
|
17
|
+
const pnl = asNum(o.realizedPnlMusd) ??
|
|
18
|
+
asNum(o.pnlMusd) ??
|
|
19
|
+
asNum(o.realizedPnl) ??
|
|
20
|
+
asNum(o.pnl);
|
|
21
|
+
const side = asStr(o.side);
|
|
22
|
+
if (sym || pnl != null) {
|
|
23
|
+
const did = `closed ${side ?? ""} ${sym ?? "position"}`.trim() +
|
|
24
|
+
(pnl != null
|
|
25
|
+
? `: ${pnl >= 0 ? "+" : ""}${Math.round(pnl)}mUSD ${pnl >= 0 ? "WIN" : "LOSS"}`
|
|
26
|
+
: "");
|
|
27
|
+
state.journal = [
|
|
28
|
+
...(state.journal ?? []),
|
|
29
|
+
{ at: observation.asOf, did },
|
|
30
|
+
].slice(-12);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/agent/runner.js
CHANGED
|
@@ -8,6 +8,8 @@ import { usesCapitalSizing, prepareCapitalAction, validateCapitalAction, capital
|
|
|
8
8
|
import { decideMechanical } from "./mechanical.js";
|
|
9
9
|
import { evaluateGate, noteLlmCall, estimateCostUsd } from "./gate.js";
|
|
10
10
|
import { baseSymbol, scanSetups } from "./setups.js";
|
|
11
|
+
import { reconcileObservation } from "./reconcileObservation.js";
|
|
12
|
+
import { createOpportunityReporter } from "./opportunityReporter.js";
|
|
11
13
|
import { observe } from "./observe.js";
|
|
12
14
|
import { buildDailyRiskBudget, buildSystemPrompt, buildUserPrompt, } from "./prompt.js";
|
|
13
15
|
import { parseDecision } from "./decision.js";
|
|
@@ -15,7 +17,7 @@ import { validateAction, isRiskIncreasingAction, } from "./decisionValidator.js"
|
|
|
15
17
|
import { resolvePmRef } from "./resolvePm.js";
|
|
16
18
|
import { fetchQuote, executeAction } from "./act.js";
|
|
17
19
|
import { makeDecisionId, makeTrace, exportRunEvidence } from "./runEvidence.js";
|
|
18
|
-
import { rollDay, checkKillSwitch,
|
|
20
|
+
import { rollDay, checkKillSwitch, saveState, isPermanentModelError, isAuthFailureSkip, PERMANENT_MODEL_ERROR_THRESHOLD, AUTH_FAILURE_THRESHOLD, } from "./state.js";
|
|
19
21
|
import { asObj, asNum, asStr } from "./extract.js";
|
|
20
22
|
import { parseCadenceMs, sleep } from "./util.js";
|
|
21
23
|
import { buildObservationReceipt } from "./observationReceipt.js";
|
|
@@ -441,43 +443,22 @@ async function runCycleCore(deps, capture) {
|
|
|
441
443
|
live,
|
|
442
444
|
};
|
|
443
445
|
}
|
|
446
|
+
// Establish durable run identity before the first possible write. A process
|
|
447
|
+
// killed before its first response must not restart with a new idempotency key.
|
|
448
|
+
if (live)
|
|
449
|
+
saveState(stateFile, state);
|
|
444
450
|
const baseTrace = makeTrace(runId, decisionId, spec);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
opportunityPosted = true;
|
|
457
|
-
postedOpportunity = o;
|
|
458
|
-
try {
|
|
459
|
-
await client.reportPmOpportunity({
|
|
460
|
-
kind: o.kind,
|
|
461
|
-
source: o.source,
|
|
462
|
-
slug: o.slug,
|
|
463
|
-
outcomeExternalMarketId: o.outcomeExternalMarketId,
|
|
464
|
-
forecastProbability: o.forecastProbability,
|
|
465
|
-
marketProbability: o.marketProbability,
|
|
466
|
-
reasonCode: o.reasonCode,
|
|
467
|
-
cohort: {
|
|
468
|
-
universeSize: o.universeSize,
|
|
469
|
-
horizon: spec.objective?.horizon,
|
|
470
|
-
},
|
|
471
|
-
decisionId,
|
|
472
|
-
runId,
|
|
473
|
-
provenance,
|
|
474
|
-
}, baseTrace);
|
|
475
|
-
log(`reported ${o.kind} opportunity (universe ${o.universeSize ?? "?"})`);
|
|
476
|
-
}
|
|
477
|
-
catch (err) {
|
|
478
|
-
log(`opportunity post failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
479
|
-
}
|
|
480
|
-
};
|
|
451
|
+
const opportunities = createOpportunityReporter({
|
|
452
|
+
client,
|
|
453
|
+
spec,
|
|
454
|
+
live,
|
|
455
|
+
enabled: agentOpportunityCaptureEnabled(),
|
|
456
|
+
runId,
|
|
457
|
+
decisionId,
|
|
458
|
+
provenance,
|
|
459
|
+
baseTrace,
|
|
460
|
+
log,
|
|
461
|
+
});
|
|
481
462
|
// OBSERVE
|
|
482
463
|
const obs = await observe(client, spec, state, baseTrace);
|
|
483
464
|
const observation = obs.observation;
|
|
@@ -496,35 +477,7 @@ async function runCycleCore(deps, capture) {
|
|
|
496
477
|
// Reads build the observation, so its hash cannot exist before they finish.
|
|
497
478
|
// From this point every durable write carries the exact decision-input receipt.
|
|
498
479
|
Object.assign(baseTrace, observationReceipt);
|
|
499
|
-
|
|
500
|
-
state.cursor = observation.syncCursor;
|
|
501
|
-
for (const t of observation.newClosedTrades) {
|
|
502
|
-
state.seen.push(`${asStr(asObj(t).venue) ?? "futures"}:${asNum(asObj(t).id) ?? String(asObj(t).id)}`);
|
|
503
|
-
}
|
|
504
|
-
state.seen = state.seen.slice(-500);
|
|
505
|
-
// Slice-3 reflection: journal closed-trade OUTCOMES (not just opens) so the agent
|
|
506
|
-
// remembers how its theses RESOLVED — a stop-out it should not revenge-trade, a
|
|
507
|
-
// winner its style works on. Defensive field reads (the /trades shape varies);
|
|
508
|
-
// a partial entry is harmless, a missing one is skipped.
|
|
509
|
-
for (const t of observation.newClosedTrades.slice(-5)) {
|
|
510
|
-
const o = asObj(t);
|
|
511
|
-
const sym = asStr(o.symbol) ?? asStr(o.coinSymbol) ?? asStr(o.coinId);
|
|
512
|
-
const pnl = asNum(o.realizedPnlMusd) ??
|
|
513
|
-
asNum(o.pnlMusd) ??
|
|
514
|
-
asNum(o.realizedPnl) ??
|
|
515
|
-
asNum(o.pnl);
|
|
516
|
-
const side = asStr(o.side);
|
|
517
|
-
if (sym || pnl != null) {
|
|
518
|
-
const did = `closed ${side ?? ""} ${sym ?? "position"}`.trim() +
|
|
519
|
-
(pnl != null
|
|
520
|
-
? `: ${pnl >= 0 ? "+" : ""}${Math.round(pnl)}mUSD ${pnl >= 0 ? "WIN" : "LOSS"}`
|
|
521
|
-
: "");
|
|
522
|
-
state.journal = [
|
|
523
|
-
...(state.journal ?? []),
|
|
524
|
-
{ at: observation.asOf, did },
|
|
525
|
-
].slice(-12);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
480
|
+
reconcileObservation(state, observation);
|
|
528
481
|
capture({
|
|
529
482
|
...captureBase,
|
|
530
483
|
phase: "observed",
|
|
@@ -897,7 +850,7 @@ async function runCycleCore(deps, capture) {
|
|
|
897
850
|
// cohort via universeSize.
|
|
898
851
|
const skipOpp = buildSkipOpportunity(decision, observation.pmMarkets, forecastEnabled);
|
|
899
852
|
if (skipOpp)
|
|
900
|
-
await
|
|
853
|
+
await opportunities.post(skipOpp);
|
|
901
854
|
saveState(stateFile, state);
|
|
902
855
|
log(`model chose skip${decision.reason ? `: ${decision.reason}` : ""}`);
|
|
903
856
|
return {
|
|
@@ -912,7 +865,7 @@ async function runCycleCore(deps, capture) {
|
|
|
912
865
|
decisionType: "skip",
|
|
913
866
|
writeAttempted: decision.actions.length,
|
|
914
867
|
writeAccepted: 0,
|
|
915
|
-
...(
|
|
868
|
+
...(opportunities.posted ? { opportunity: opportunities.posted } : {}),
|
|
916
869
|
...observationReceipt,
|
|
917
870
|
};
|
|
918
871
|
}
|
|
@@ -1265,7 +1218,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1265
1218
|
// (once-per-cycle; carries the universe breadth in the cohort field).
|
|
1266
1219
|
if (action.type === "pm_open" && isQuoteExpiredResult(r.status, r.data)) {
|
|
1267
1220
|
const pm = action;
|
|
1268
|
-
await
|
|
1221
|
+
await opportunities.post({
|
|
1269
1222
|
kind: "quote_expired",
|
|
1270
1223
|
source: pm.source,
|
|
1271
1224
|
slug: pm.slug,
|
|
@@ -1291,13 +1244,18 @@ async function runCycleCore(deps, capture) {
|
|
|
1291
1244
|
state.consecutiveExecFailures =
|
|
1292
1245
|
anyExecFailed && !anyExecuted ? state.consecutiveExecFailures + 1 : 0;
|
|
1293
1246
|
state.rateLimitHits = client.rateLimitHits ?? state.rateLimitHits;
|
|
1294
|
-
//
|
|
1295
|
-
//
|
|
1247
|
+
// Memory describes completed moves, not policy acceptance. A backend rejection,
|
|
1248
|
+
// lost response, or dry-run plan must not become "opened" / "trailed stop" in
|
|
1249
|
+
// the next model prompt. Attempts remain in planned[] and the audit ledger.
|
|
1296
1250
|
const moves = planned
|
|
1297
|
-
.filter((p) => p.accepted)
|
|
1251
|
+
.filter((p) => p.accepted && p.executed === true)
|
|
1298
1252
|
.map((p) => summarizeAction(p.action));
|
|
1299
1253
|
if (moves.length > 0) {
|
|
1300
|
-
|
|
1254
|
+
// A cycle-wide rationale may describe a failed/rejected sibling action.
|
|
1255
|
+
// Keep it only when every proposed action executed; per-action theses are
|
|
1256
|
+
// already included by summarizeAction for confirmed opens.
|
|
1257
|
+
const confirmedRationale = moves.length === planned.length ? rationale : undefined;
|
|
1258
|
+
const did = `${moves.join("; ")}${confirmedRationale ? ` — ${confirmedRationale.slice(0, 90)}` : ""}`;
|
|
1301
1259
|
state.journal = [
|
|
1302
1260
|
...(state.journal ?? []),
|
|
1303
1261
|
{ at: observation.asOf, did },
|
|
@@ -1317,7 +1275,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1317
1275
|
decisionType: "act",
|
|
1318
1276
|
writeAttempted: decision.actions.length,
|
|
1319
1277
|
writeAccepted: planned.filter((p) => p.accepted).length,
|
|
1320
|
-
...(
|
|
1278
|
+
...(opportunities.posted ? { opportunity: opportunities.posted } : {}),
|
|
1321
1279
|
...observationReceipt,
|
|
1322
1280
|
};
|
|
1323
1281
|
}
|
package/dist/agent/skill.js
CHANGED
|
@@ -103,6 +103,12 @@ export function buildSpec(raw) {
|
|
|
103
103
|
? { capitalSizing: buildCapitalSizing(raw.capitalSizing) }
|
|
104
104
|
: {}),
|
|
105
105
|
risk: {
|
|
106
|
+
// Preserve malformed explicit policies for fail-closed validation.
|
|
107
|
+
...(risk.entryPredicates !== undefined
|
|
108
|
+
? {
|
|
109
|
+
entryPredicates: risk.entryPredicates,
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
106
112
|
maxLeverage: num(risk.maxLeverage, 1),
|
|
107
113
|
perTradeMarginMusd: num(risk.perTradeMarginMusd, 0),
|
|
108
114
|
maxConcurrentPositions: num(risk.maxConcurrentPositions, 0),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fail, VENUES, PROVIDERS, SPEC_VERSION, OBJECTIVE_PRIMARIES, ALLOWED_CAPABILITIES, } from "./types.js";
|
|
2
2
|
import { parseCadenceMs, scanForSecrets } from "./util.js";
|
|
3
|
+
import { entryPredicateIssues } from "./entryPredicates.js";
|
|
3
4
|
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4
5
|
const isPosNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
5
6
|
// maxTradesPerDay accepts 0 as the explicit "unlimited daily trades" sentinel.
|
|
@@ -93,6 +94,8 @@ export function validateSkill(parsed, mode = "self-host") {
|
|
|
93
94
|
}
|
|
94
95
|
else {
|
|
95
96
|
const r = raw.risk;
|
|
97
|
+
if (r.entryPredicates !== undefined)
|
|
98
|
+
issues.push(...entryPredicateIssues(r.entryPredicates));
|
|
96
99
|
if (!isPosNum(r.maxLeverage))
|
|
97
100
|
add("skill_risk_leverage", "risk.maxLeverage must be a positive number");
|
|
98
101
|
else if (r.maxLeverage > 20)
|
package/dist/agent/state.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Local run state: the cursor, dedupe set, daily counters, and the kill-switch
|
|
2
2
|
// inputs. Persisted to a JSON file so a re-run resumes where it left off.
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, rmSync, } from "node:fs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { dirname } from "node:path";
|
|
5
6
|
import { dayKey } from "./util.js";
|
|
6
7
|
import { asObj, asNum } from "./extract.js";
|
|
@@ -59,8 +60,22 @@ export function loadState(file, runId) {
|
|
|
59
60
|
export function saveState(file, state) {
|
|
60
61
|
if (!file)
|
|
61
62
|
return;
|
|
63
|
+
const serialized = JSON.stringify(state, null, 2);
|
|
62
64
|
mkdirSync(dirname(file), { recursive: true });
|
|
63
|
-
|
|
65
|
+
// A sibling file keeps rename on the same filesystem. Readers see either
|
|
66
|
+
// the previous complete state or the new one, never a truncated JSON write.
|
|
67
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
68
|
+
try {
|
|
69
|
+
writeFileSync(temporary, serialized, {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
flag: "wx",
|
|
72
|
+
mode: 0o600,
|
|
73
|
+
});
|
|
74
|
+
renameSync(temporary, file);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
rmSync(temporary, { force: true });
|
|
78
|
+
}
|
|
64
79
|
}
|
|
65
80
|
export function rollDay(state) {
|
|
66
81
|
const today = dayKey();
|
package/dist/agent/strictLint.js
CHANGED
package/dist/agent/templates.js
CHANGED
|
@@ -50,37 +50,37 @@ const PRESETS = {
|
|
|
50
50
|
modelFail: 5,
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
|
-
const THESIS_BODY = `# Momentum Futures — strategy
|
|
54
|
-
|
|
55
|
-
You operate a CoinRithm **paper-trading** futures account (50,000 virtual mUSD).
|
|
56
|
-
Everything here is simulated; it is not financial advice and never touches real
|
|
57
|
-
money. Edit this prose freely (any language) — it is your agent's borders.
|
|
58
|
-
|
|
59
|
-
## Each cycle
|
|
60
|
-
|
|
61
|
-
1. Ground yourself: read your portfolio and open positions first. Never assume
|
|
62
|
-
balances or what is already open.
|
|
63
|
-
2. Scan the watchlist. A candidate is a coin whose short and medium momentum
|
|
64
|
-
agree (both up, or both down) and is not already an open position.
|
|
65
|
-
3. Pick the strongest candidate and commit when the read is clear — even a
|
|
66
|
-
moderate-confidence one — sized small with a stop. Skip only when the signals
|
|
67
|
-
contradict or the data is stale; a quiet tape where your edge is still real
|
|
68
|
-
is an act, not a skip.
|
|
69
|
-
4. Quote before you open. Read the liquidation price and confirm it is sane. If
|
|
70
|
-
the quote is not eligible, relay the reason and stop.
|
|
71
|
-
5. Open small and protected: enter in the trend direction and set a stop-loss at
|
|
72
|
-
open. Place the take-profit a touch wider than the stop.
|
|
73
|
-
6. Stay in sync: poll your trades for any stop / take-profit / liquidation that
|
|
74
|
-
fired while you were not looking, and react to what actually happened.
|
|
75
|
-
|
|
76
|
-
The hard caps (leverage, margin, watchlist) live in the config blocks above and
|
|
53
|
+
const THESIS_BODY = `# Momentum Futures — strategy
|
|
54
|
+
|
|
55
|
+
You operate a CoinRithm **paper-trading** futures account (50,000 virtual mUSD).
|
|
56
|
+
Everything here is simulated; it is not financial advice and never touches real
|
|
57
|
+
money. Edit this prose freely (any language) — it is your agent's borders.
|
|
58
|
+
|
|
59
|
+
## Each cycle
|
|
60
|
+
|
|
61
|
+
1. Ground yourself: read your portfolio and open positions first. Never assume
|
|
62
|
+
balances or what is already open.
|
|
63
|
+
2. Scan the watchlist. A candidate is a coin whose short and medium momentum
|
|
64
|
+
agree (both up, or both down) and is not already an open position.
|
|
65
|
+
3. Pick the strongest candidate and commit when the read is clear — even a
|
|
66
|
+
moderate-confidence one — sized small with a stop. Skip only when the signals
|
|
67
|
+
contradict or the data is stale; a quiet tape where your edge is still real
|
|
68
|
+
is an act, not a skip.
|
|
69
|
+
4. Quote before you open. Read the liquidation price and confirm it is sane. If
|
|
70
|
+
the quote is not eligible, relay the reason and stop.
|
|
71
|
+
5. Open small and protected: enter in the trend direction and set a stop-loss at
|
|
72
|
+
open. Place the take-profit a touch wider than the stop.
|
|
73
|
+
6. Stay in sync: poll your trades for any stop / take-profit / liquidation that
|
|
74
|
+
fired while you were not looking, and react to what actually happened.
|
|
75
|
+
|
|
76
|
+
The hard caps (leverage, margin, watchlist) live in the config blocks above and
|
|
77
77
|
are enforced by the runner — change them there, not in this prose.`;
|
|
78
|
-
const PERSONA_STUB = `# Persona
|
|
79
|
-
|
|
80
|
-
Decisive and in character. Acts on a clear read — even a moderate-confidence one
|
|
81
|
-
— sized small and protected with a stop, and skips only when the read is
|
|
82
|
-
contradictory or the data is stale. States its reasoning plainly in its own
|
|
83
|
-
voice, and never frames paper results as real-money advice.
|
|
78
|
+
const PERSONA_STUB = `# Persona
|
|
79
|
+
|
|
80
|
+
Decisive and in character. Acts on a clear read — even a moderate-confidence one
|
|
81
|
+
— sized small and protected with a stop, and skips only when the read is
|
|
82
|
+
contradictory or the data is stale. States its reasoning plainly in its own
|
|
83
|
+
voice, and never frames paper results as real-money advice.
|
|
84
84
|
`;
|
|
85
85
|
export function buildAgentObject(name, preset) {
|
|
86
86
|
const p = PRESETS[preset];
|
|
@@ -182,6 +182,10 @@ export function ejectFiles(fm, body) {
|
|
|
182
182
|
};
|
|
183
183
|
if (fm.sizing)
|
|
184
184
|
agentFm.sizing = fm.sizing;
|
|
185
|
+
if (fm.capitalSizing !== undefined)
|
|
186
|
+
agentFm.capitalSizing = fm.capitalSizing;
|
|
187
|
+
if (fm.triggerPolicy !== undefined)
|
|
188
|
+
agentFm.triggerPolicy = fm.triggerPolicy;
|
|
185
189
|
if (fm.objective)
|
|
186
190
|
agentFm.objective = fm.objective;
|
|
187
191
|
if (fm.capabilities)
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface TriggerPolicy {
|
|
|
26
26
|
}
|
|
27
27
|
export declare const DEFAULT_TRIGGER_POLICY: TriggerPolicy;
|
|
28
28
|
export interface RiskConfig {
|
|
29
|
+
entryPredicates?: import("./entryPredicates.js").EntryPredicate[];
|
|
29
30
|
maxLeverage: number;
|
|
30
31
|
perTradeMarginMusd: number;
|
|
31
32
|
maxConcurrentPositions: number;
|
package/dist/agent/util.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Small dependency-free helpers shared across the runner.
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { resolve as resolvePath, relative as relativePath, isAbsolute, } from "node:path";
|
|
4
4
|
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
5
|
// Parse a cadence string ("30s", "15m", "1h", "4h", "1d") to milliseconds.
|
|
@@ -63,8 +63,8 @@ export function scanForSecrets(value, pathPrefix = "", findings = []) {
|
|
|
63
63
|
}
|
|
64
64
|
// Short random id (for runId/decisionId/idempotencyKey suffixes).
|
|
65
65
|
export function shortId() {
|
|
66
|
-
//
|
|
67
|
-
return
|
|
66
|
+
// Import the Node API: globalThis.crypto is not enabled by default in Node 18.
|
|
67
|
+
return randomUUID().slice(0, 8);
|
|
68
68
|
}
|
|
69
69
|
// ── Resolver helpers (cross-platform-deterministic) ──────────────────────────
|
|
70
70
|
// Normalize file content for hashing: CRLF/CR -> LF, strip trailing whitespace,
|
package/dist/client.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
//
|
|
18
18
|
// IMPORTANT: this module must NEVER write to stdout (stdout is the MCP JSON-RPC
|
|
19
19
|
// channel). All diagnostics go to stderr via the logger below.
|
|
20
|
+
import { retryAfterSeconds } from "./retryAfter.js";
|
|
20
21
|
export const DEFAULT_BASE_URL = "https://api.coinrithm.com";
|
|
21
22
|
export function log(...args) {
|
|
22
23
|
// stderr only — stdout is reserved for the MCP protocol.
|
|
@@ -139,12 +140,12 @@ export class CoinRithmClient {
|
|
|
139
140
|
if (res.status === 429) {
|
|
140
141
|
// Surface the back-off contract so an agent can pace itself instead of
|
|
141
142
|
// hammering: 120 req/min per key baseline, 20 trade-writes/min.
|
|
142
|
-
const retryAfter =
|
|
143
|
+
const retryAfter = retryAfterSeconds(res.headers.get("retry-after"));
|
|
143
144
|
data = {
|
|
144
145
|
...(typeof data === "object" && data !== null
|
|
145
146
|
? data
|
|
146
147
|
: { error: String(data) }),
|
|
147
|
-
retryAfterSeconds:
|
|
148
|
+
retryAfterSeconds: retryAfter ?? null,
|
|
148
149
|
hint: "Rate limited. Wait retryAfterSeconds (or the Retry-After header) before retrying; pace future calls using the RateLimit-Remaining response header.",
|
|
149
150
|
};
|
|
150
151
|
}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { CoinRithmClient } from "./client.js";
|
|
4
|
+
import { type CompletionLogger } from "./httpCompletion.js";
|
|
5
|
+
export declare function createHttpApp(client: CoinRithmClient, options?: {
|
|
6
|
+
completionLogger?: CompletionLogger;
|
|
7
|
+
createServer?: () => McpServer;
|
|
8
|
+
}): import("express-serve-static-core").Express;
|