@coinrithm/mcp-trading 0.7.5 → 0.7.7
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 +310 -197
- package/README.md +319 -279
- package/dist/agent/cli.js +22 -1
- package/dist/agent/client.d.ts +3 -0
- package/dist/agent/client.js +11 -0
- package/dist/agent/decision.d.ts +12 -0
- package/dist/agent/decision.js +10 -0
- package/dist/agent/decisionProbe.d.ts +17 -0
- package/dist/agent/decisionProbe.js +70 -0
- package/dist/agent/decisionValidator.js +23 -0
- package/dist/agent/engine.d.ts +4 -1
- package/dist/agent/engine.js +7 -1
- package/dist/agent/observe.js +118 -6
- package/dist/agent/prompt.d.ts +3 -1
- package/dist/agent/prompt.js +114 -27
- package/dist/agent/providerCapabilities.d.ts +20 -0
- package/dist/agent/providerCapabilities.js +67 -0
- package/dist/agent/providers.d.ts +29 -1
- package/dist/agent/providers.js +50 -25
- package/dist/agent/resolve.d.ts +12 -0
- package/dist/agent/resolve.js +82 -3
- package/dist/agent/runner.js +113 -8
- package/dist/agent/skill.js +6 -0
- package/dist/agent/skillValidator.js +7 -0
- package/dist/agent/state.d.ts +5 -0
- package/dist/agent/state.js +20 -0
- package/dist/agent/strictLint.js +14 -0
- package/dist/agent/templates.js +6 -0
- package/dist/agent/types.d.ts +34 -1
- package/dist/agent/types.js +10 -0
- package/dist/agent/version.d.ts +1 -1
- package/dist/agent/version.js +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.js +10 -0
- package/dist/http.js +21 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +66 -10
- package/package.json +1 -1
package/dist/agent/cli.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { mkdirSync, writeFileSync, existsSync, statSync, readFileSync, openSync, closeSync, unlinkSync, } from "node:fs";
|
|
8
8
|
import { resolve as resolvePath, dirname, join, basename } from "node:path";
|
|
9
9
|
import { parse as parseYaml } from "yaml";
|
|
10
|
-
import { resolveAgent, ResolveError, mergeProseParts, isSkillProseSource, } from "./resolve.js";
|
|
10
|
+
import { resolveAgent, ResolveError, mergeProseParts, isSkillProseSource, hostedProseBudget, HOSTED_PROSE_MAX_CHARS, } from "./resolve.js";
|
|
11
11
|
import { buildSpec, loadAgent } from "./skill.js";
|
|
12
12
|
import { validateSkill } from "./skillValidator.js";
|
|
13
13
|
import { strictLint } from "./strictLint.js";
|
|
@@ -101,6 +101,27 @@ export function cmdValidate(path, mode = "self-host") {
|
|
|
101
101
|
const spec = buildSpec(raw);
|
|
102
102
|
const lint = [...strictLint(raw), ...checkCapabilityDrift(resolved, spec)];
|
|
103
103
|
const v = validateSkill({ spec, body: resolved.mergedProse, raw }, mode);
|
|
104
|
+
// Hosted-only: the managed deploy/edit API caps the merged strategy prose at
|
|
105
|
+
// HOSTED_PROSE_MAX_CHARS and REVERTS the save when it is exceeded, so a
|
|
106
|
+
// bundle that resolves and lints perfectly can still be undeployable through
|
|
107
|
+
// the Studio. Measured 2026-08-19 after a user hit the wall: 4 of 9 example
|
|
108
|
+
// bundles were over (contrarian-carl 8,159, mia 8,175, olivia 8,587,
|
|
109
|
+
// pia-pump-fader 11,787) while the corpus README claimed they all pass
|
|
110
|
+
// `validate --hosted`. Checking it here is what makes that claim true and
|
|
111
|
+
// stops the corpus drifting back over the wall.
|
|
112
|
+
if (mode === "hosted") {
|
|
113
|
+
const budget = hostedProseBudget(resolved.mergedProse);
|
|
114
|
+
if (!budget.fits) {
|
|
115
|
+
lint.push({
|
|
116
|
+
code: "hosted_prose_too_long",
|
|
117
|
+
path: "character/*.md",
|
|
118
|
+
message: `merged strategy prose is ${budget.used} chars, ${budget.over} over the hosted ` +
|
|
119
|
+
`limit of ${HOSTED_PROSE_MAX_CHARS} — the managed deploy would reject this and ` +
|
|
120
|
+
`revert to the template. Self-host has no such cap. Note the count includes a ` +
|
|
121
|
+
`"<!-- path -->" header per prose file, not just the bodies.`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
104
125
|
const lintFatal = mode === "hosted";
|
|
105
126
|
const lines = [];
|
|
106
127
|
for (const i of lint) {
|
package/dist/agent/client.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface ClientConfig {
|
|
|
21
21
|
fetchFn?: typeof fetch;
|
|
22
22
|
sleepFn?: (ms: number) => Promise<void>;
|
|
23
23
|
maxRetries?: number;
|
|
24
|
+
extraHeaders?: Record<string, string>;
|
|
24
25
|
}
|
|
25
26
|
export declare class CoinRithmClient {
|
|
26
27
|
private readonly apiKey;
|
|
@@ -28,6 +29,7 @@ export declare class CoinRithmClient {
|
|
|
28
29
|
private readonly fetchFn;
|
|
29
30
|
private readonly sleepFn;
|
|
30
31
|
private readonly maxRetries;
|
|
32
|
+
private readonly extraHeaders?;
|
|
31
33
|
rateLimitHits: number;
|
|
32
34
|
constructor(cfg: ClientConfig);
|
|
33
35
|
private request;
|
|
@@ -37,6 +39,7 @@ export declare class CoinRithmClient {
|
|
|
37
39
|
coinId?: string;
|
|
38
40
|
}, trace?: AgentTrace): Promise<ApiResult>;
|
|
39
41
|
resolve(q: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
42
|
+
cryptoMovers(direction: "gainers" | "losers", limit: number, trace?: AgentTrace): Promise<ApiResult>;
|
|
40
43
|
market(coinId: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
41
44
|
candles(coinId: string, range: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
42
45
|
trades(query?: {
|
package/dist/agent/client.js
CHANGED
|
@@ -30,6 +30,7 @@ export class CoinRithmClient {
|
|
|
30
30
|
fetchFn;
|
|
31
31
|
sleepFn;
|
|
32
32
|
maxRetries;
|
|
33
|
+
extraHeaders;
|
|
33
34
|
// Every 429 seen this session (read or write, retried or not) — feeds the
|
|
34
35
|
// rate-limit-pressure kill-switch, which a write-only counter would miss.
|
|
35
36
|
rateLimitHits = 0;
|
|
@@ -39,6 +40,7 @@ export class CoinRithmClient {
|
|
|
39
40
|
this.fetchFn = cfg.fetchFn ?? fetch;
|
|
40
41
|
this.sleepFn = cfg.sleepFn ?? realSleep;
|
|
41
42
|
this.maxRetries = cfg.maxRetries ?? 3;
|
|
43
|
+
this.extraHeaders = cfg.extraHeaders;
|
|
42
44
|
}
|
|
43
45
|
async request(method, path, opts = {}) {
|
|
44
46
|
const url = new URL(this.baseUrl + path);
|
|
@@ -48,7 +50,9 @@ export class CoinRithmClient {
|
|
|
48
50
|
url.searchParams.set(k, String(v));
|
|
49
51
|
}
|
|
50
52
|
}
|
|
53
|
+
// extraHeaders first: auth, accept and trace can never be clobbered by it.
|
|
51
54
|
const headers = {
|
|
55
|
+
...this.extraHeaders,
|
|
52
56
|
Authorization: `Bearer ${this.apiKey}`,
|
|
53
57
|
Accept: "application/json",
|
|
54
58
|
...traceHeaders(opts.trace),
|
|
@@ -116,6 +120,13 @@ export class CoinRithmClient {
|
|
|
116
120
|
resolve(q, trace) {
|
|
117
121
|
return this.request("GET", "/api/agent/resolve", { query: { q }, trace });
|
|
118
122
|
}
|
|
123
|
+
// Keyless public universe scan (top 24h movers). The Bearer header rides
|
|
124
|
+
// along harmlessly — the /api/coins routes are public and ignore it.
|
|
125
|
+
cryptoMovers(direction, limit, trace) {
|
|
126
|
+
return this.request("GET", direction === "losers"
|
|
127
|
+
? "/api/coins/top-losers"
|
|
128
|
+
: "/api/coins/top-gainers", { query: { limit }, trace });
|
|
129
|
+
}
|
|
119
130
|
market(coinId, trace) {
|
|
120
131
|
return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { trace });
|
|
121
132
|
}
|
package/dist/agent/decision.d.ts
CHANGED
|
@@ -53,16 +53,22 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
|
|
|
53
53
|
positionId: z.ZodEffects<z.ZodTypeAny, any, unknown>;
|
|
54
54
|
stopLossPrice: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodTypeAny, any, unknown>>>;
|
|
55
55
|
takeProfitPrice: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodTypeAny, any, unknown>>>;
|
|
56
|
+
confidence: z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodTypeAny, any, unknown>>>, any, unknown>;
|
|
57
|
+
rationaleSummary: z.ZodOptional<z.ZodString>;
|
|
56
58
|
}, "strict", z.ZodTypeAny, {
|
|
57
59
|
type: "futures_set_sltp";
|
|
58
60
|
stopLossPrice?: any;
|
|
59
61
|
takeProfitPrice?: any;
|
|
60
62
|
positionId?: any;
|
|
63
|
+
confidence?: any;
|
|
64
|
+
rationaleSummary?: string | undefined;
|
|
61
65
|
}, {
|
|
62
66
|
type: "futures_set_sltp";
|
|
63
67
|
stopLossPrice?: unknown;
|
|
64
68
|
takeProfitPrice?: unknown;
|
|
65
69
|
positionId?: unknown;
|
|
70
|
+
confidence?: unknown;
|
|
71
|
+
rationaleSummary?: string | undefined;
|
|
66
72
|
}>, z.ZodObject<{
|
|
67
73
|
type: z.ZodLiteral<"spot_order">;
|
|
68
74
|
symbol: z.ZodString;
|
|
@@ -96,11 +102,17 @@ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
|
|
|
96
102
|
}>, z.ZodObject<{
|
|
97
103
|
type: z.ZodLiteral<"spot_cancel">;
|
|
98
104
|
orderId: z.ZodEffects<z.ZodTypeAny, any, unknown>;
|
|
105
|
+
confidence: z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodTypeAny, any, unknown>>>, any, unknown>;
|
|
106
|
+
rationaleSummary: z.ZodOptional<z.ZodString>;
|
|
99
107
|
}, "strict", z.ZodTypeAny, {
|
|
100
108
|
type: "spot_cancel";
|
|
109
|
+
confidence?: any;
|
|
110
|
+
rationaleSummary?: string | undefined;
|
|
101
111
|
orderId?: any;
|
|
102
112
|
}, {
|
|
103
113
|
type: "spot_cancel";
|
|
114
|
+
confidence?: unknown;
|
|
115
|
+
rationaleSummary?: string | undefined;
|
|
104
116
|
orderId?: unknown;
|
|
105
117
|
}>, z.ZodObject<{
|
|
106
118
|
type: z.ZodLiteral<"pm_open">;
|
package/dist/agent/decision.js
CHANGED
|
@@ -59,6 +59,13 @@ const futuresSetSltp = z
|
|
|
59
59
|
positionId: num(z.number()),
|
|
60
60
|
stopLossPrice: num(z.number()).nullable().optional(),
|
|
61
61
|
takeProfitPrice: num(z.number()).nullable().optional(),
|
|
62
|
+
// Accepted-and-unused: the output contract asks for per-action confidence,
|
|
63
|
+
// so models copy it onto EVERY action. The two schemas that lacked it were
|
|
64
|
+
// fail-closing whole decisions under .strict() (~180-200 discarded
|
|
65
|
+
// fleet-wide per day, live-measured 2026-08-19). Same tolerance the four
|
|
66
|
+
// trade actions already have.
|
|
67
|
+
confidence,
|
|
68
|
+
rationaleSummary: z.string().optional(),
|
|
62
69
|
})
|
|
63
70
|
.strict();
|
|
64
71
|
const spotOrder = z
|
|
@@ -78,6 +85,9 @@ const spotCancel = z
|
|
|
78
85
|
.object({
|
|
79
86
|
type: z.literal("spot_cancel"),
|
|
80
87
|
orderId: num(z.number()),
|
|
88
|
+
// Same accepted-and-unused tolerance as futures_set_sltp above.
|
|
89
|
+
confidence,
|
|
90
|
+
rationaleSummary: z.string().optional(),
|
|
81
91
|
})
|
|
82
92
|
.strict();
|
|
83
93
|
// pm_open accepts EITHER a short ref (pm1…pmN, what the prompt now asks for) OR
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ProviderName } from "./types.js";
|
|
2
|
+
export interface ProbeRoute {
|
|
3
|
+
provider: ProviderName;
|
|
4
|
+
model: string;
|
|
5
|
+
baseUrl?: string | null;
|
|
6
|
+
key: string;
|
|
7
|
+
}
|
|
8
|
+
export type ProbeDecisionResult = {
|
|
9
|
+
ok: true;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
stage: "http" | "empty" | "parse";
|
|
13
|
+
error: string;
|
|
14
|
+
status?: number;
|
|
15
|
+
retryAfterMs?: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function probeDecisionContract(route: ProbeRoute, fetchFn?: typeof fetch): Promise<ProbeDecisionResult>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Representative decision probe (reliability slice A, contract frozen on
|
|
2
|
+
// Telegram 2026-08-26). An HTTP-200 chat ping is NOT proof a route can run an
|
|
3
|
+
// agent: the 62f3a12 incident had 200s all round while every cycle failed with
|
|
4
|
+
// "Unexpected token W" (think-chain in the JSON slot), and Codex's gpt-5-nano
|
|
5
|
+
// probe at 256 completion tokens returned EMPTY content with a length finish
|
|
6
|
+
// because reasoning consumed the budget. A route is eligible only when a real
|
|
7
|
+
// call comes back parseable through the REAL decision parser with a non-empty
|
|
8
|
+
// decision — using the exact request shape a cycle would send
|
|
9
|
+
// (providerForRoute -> the same provider classes as the runner).
|
|
10
|
+
//
|
|
11
|
+
// Uses: boot eligibility of fallback-chain targets (slice B), circuit
|
|
12
|
+
// half-open reopens, any future model migration (D18: probe before adopt).
|
|
13
|
+
// The key is used for the one call and never logged; provider error text is
|
|
14
|
+
// sanitized before it can reach any log or ledger row.
|
|
15
|
+
import { providerForRoute } from "./providers.js";
|
|
16
|
+
import { chatShapeFor } from "./providerCapabilities.js";
|
|
17
|
+
import { parseDecision } from "./decision.js";
|
|
18
|
+
// A canned mini-observation whose ONLY correct answer is a tiny skip decision.
|
|
19
|
+
// Small enough to cost nothing, real enough to exercise the full JSON contract.
|
|
20
|
+
const PROBE_SYSTEM = [
|
|
21
|
+
"You are a trading agent contract probe.",
|
|
22
|
+
'Reply with EXACTLY one JSON object: {"decision":"skip","reason":"contract probe"}.',
|
|
23
|
+
"No prose, no code fences, no additional keys.",
|
|
24
|
+
].join(" ");
|
|
25
|
+
const PROBE_USER = "Observation: BTC 24h change 0.0%. Confirm the decision contract.";
|
|
26
|
+
const PROBE_TIMEOUT_MS = 30_000;
|
|
27
|
+
/** Strip the key (and bearer echoes) out of any text a probe might surface. */
|
|
28
|
+
function sanitize(text, key) {
|
|
29
|
+
let out = (text ?? "").slice(0, 400);
|
|
30
|
+
if (key)
|
|
31
|
+
out = out.split(key).join("***");
|
|
32
|
+
out = out.replace(/Bearer\s+[A-Za-z0-9._-]{8,}/g, "Bearer ***");
|
|
33
|
+
return out.slice(0, 200);
|
|
34
|
+
}
|
|
35
|
+
export async function probeDecisionContract(route, fetchFn = fetch) {
|
|
36
|
+
const shape = chatShapeFor(route.provider, route.model, route.baseUrl ?? undefined);
|
|
37
|
+
const provider = providerForRoute(route, route.key, fetchFn);
|
|
38
|
+
const res = await provider.decide({
|
|
39
|
+
system: PROBE_SYSTEM,
|
|
40
|
+
user: PROBE_USER,
|
|
41
|
+
// Reasoning models spend hidden tokens first — grant at least the family
|
|
42
|
+
// floor (1024) or the empty-with-length-finish false negative comes back.
|
|
43
|
+
maxTokens: Math.max(1024, shape.minProbeCompletionTokens),
|
|
44
|
+
timeoutMs: PROBE_TIMEOUT_MS,
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const error = sanitize(res.error, route.key);
|
|
48
|
+
// Provider classes report empty 2xx content as "... returned empty content".
|
|
49
|
+
const stage = /returned empty content/i.test(res.error) ? "empty" : "http";
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
stage,
|
|
53
|
+
error,
|
|
54
|
+
status: res.status,
|
|
55
|
+
retryAfterMs: res.retryAfterMs,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!res.text.trim()) {
|
|
59
|
+
return { ok: false, stage: "empty", error: "empty completion" };
|
|
60
|
+
}
|
|
61
|
+
const parsed = parseDecision(res.text);
|
|
62
|
+
if (!parsed.ok) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
stage: "parse",
|
|
66
|
+
error: sanitize(parsed.error, route.key),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { ok: true };
|
|
70
|
+
}
|
|
@@ -36,6 +36,29 @@ export function validateAction(action, ctx) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
// Direction constraint (2026-08-24): a strategy's side restriction is a HARD
|
|
40
|
+
// cap, not prose-obedience. Live incident: a short-only fade agent opened
|
|
41
|
+
// two momentum LONGS when the flagged-setups act-pressure outweighed its
|
|
42
|
+
// prose. Closes/SL-TP/cancels are never direction-gated — reducing or
|
|
43
|
+
// protecting an existing position is not a directional bet.
|
|
44
|
+
const direction = spec.risk.direction;
|
|
45
|
+
if (direction) {
|
|
46
|
+
if (action.type === "futures_open") {
|
|
47
|
+
if (direction === "short_only" && action.side !== "short") {
|
|
48
|
+
return fail("direction_constraint", `direction ${direction}: futures_open side must be "short", got "${action.side}"`);
|
|
49
|
+
}
|
|
50
|
+
if (direction === "long_only" && action.side !== "long") {
|
|
51
|
+
return fail("direction_constraint", `direction ${direction}: futures_open side must be "long", got "${action.side}"`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Spot buys are long exposure; a short_only agent must not accumulate
|
|
55
|
+
// them. Spot sells reduce a holding and stay allowed.
|
|
56
|
+
if (action.type === "spot_order" &&
|
|
57
|
+
direction === "short_only" &&
|
|
58
|
+
action.side === "buy") {
|
|
59
|
+
return fail("direction_constraint", `direction ${direction}: spot buys are long exposure`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
39
62
|
if (action.type === "futures_open") {
|
|
40
63
|
// Daily realized-loss stop: once today's loss hits the cap, open no new risk.
|
|
41
64
|
if (spec.limits.maxDailyLossMusd > 0 &&
|
package/dist/agent/engine.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { runCycle, type RunnerDeps } from "./runner.js";
|
|
2
|
-
export { selectProvider, type ProviderEnv, type Provider, } from "./providers.js";
|
|
2
|
+
export { selectProvider, providerForRoute, type ProviderEnv, type Provider, type DecideInput, type DecideResult, type DecideRouteAttempt, type DecideRouteMeta, } from "./providers.js";
|
|
3
|
+
export { parseDecision } from "./decision.js";
|
|
4
|
+
export { chatShapeFor, buildChatBody, type ChatShape, } from "./providerCapabilities.js";
|
|
5
|
+
export { probeDecisionContract, type ProbeRoute, type ProbeDecisionResult, } from "./decisionProbe.js";
|
|
3
6
|
export { CoinRithmClient } from "./client.js";
|
|
4
7
|
export { loadAgent, buildSpec, type LoadedAgent } from "./skill.js";
|
|
5
8
|
export { resolveAgent } from "./resolve.js";
|
package/dist/agent/engine.js
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
// This barrel is the ONE import a host scheduler needs; it re-exports only the
|
|
7
7
|
// stable engine pieces, never the CLI.
|
|
8
8
|
export { runCycle } from "./runner.js";
|
|
9
|
-
export { selectProvider, } from "./providers.js";
|
|
9
|
+
export { selectProvider, providerForRoute, } from "./providers.js";
|
|
10
|
+
export { parseDecision } from "./decision.js";
|
|
11
|
+
// Reliability slice A: the declarative request-capability table and the
|
|
12
|
+
// representative decision probe (route eligibility = a REAL parsed decision,
|
|
13
|
+
// never a bare HTTP 200 — the 62f3a12 lesson).
|
|
14
|
+
export { chatShapeFor, buildChatBody, } from "./providerCapabilities.js";
|
|
15
|
+
export { probeDecisionContract, } from "./decisionProbe.js";
|
|
10
16
|
export { CoinRithmClient } from "./client.js";
|
|
11
17
|
export { loadAgent, buildSpec } from "./skill.js";
|
|
12
18
|
export { resolveAgent } from "./resolve.js";
|
package/dist/agent/observe.js
CHANGED
|
@@ -8,6 +8,29 @@ import { scanSetups } from "./setups.js";
|
|
|
8
8
|
// (~5-min fresh, ~288 bars — ample for EMA50/RSI14/Bollinger20), which suits the
|
|
9
9
|
// short cadence the hosted house agents run on. Probe-verified 2026-06-17.
|
|
10
10
|
const INDICATOR_RANGE = "1D";
|
|
11
|
+
// `universe_scan` bounds: how many top movers to pull, and how many of those
|
|
12
|
+
// to fully resolve into tradable watch entries (each resolved row costs a
|
|
13
|
+
// resolve + market [+ candles] call).
|
|
14
|
+
//
|
|
15
|
+
// RESOLVE_TOP 3 -> 6 on 2026-08-21. Only a resolved row carries indicators, and
|
|
16
|
+
// therefore a `setups` flag; the unresolved remainder is bare symbol + 24h
|
|
17
|
+
// change + price. At 3, a discovery-driven strategy could reason properly about
|
|
18
|
+
// exactly three coins per cycle out of fifteen surfaced.
|
|
19
|
+
//
|
|
20
|
+
// That bit a real user case. A pump-fade agent identifies a candidate from a
|
|
21
|
+
// `stretched`/`fade-short` setup (RSI14 >= 68) and then waits for exhaustion,
|
|
22
|
+
// which by definition means RSI is NO LONGER extreme. Nothing persists between
|
|
23
|
+
// cycles, so the candidate has to still be a resolved row at the moment the
|
|
24
|
+
// exhaustion evidence appears. Discovery is gainers-ranked, so a retracing coin
|
|
25
|
+
// slides down the list — at 3 it fell out almost immediately and went blind
|
|
26
|
+
// exactly when the strategy needed to look at it.
|
|
27
|
+
//
|
|
28
|
+
// 6 roughly doubles how far a coin can slide before losing its indicators. Cost
|
|
29
|
+
// is 3 extra market+candles calls per cycle against CoinRithm's own API (never
|
|
30
|
+
// the model quota) and ~3 more watch entries in the prompt, and only for agents
|
|
31
|
+
// that declare universe_scan.
|
|
32
|
+
const UNIVERSE_SCAN_LIMIT = 15;
|
|
33
|
+
const UNIVERSE_RESOLVE_TOP = 6;
|
|
11
34
|
// Watchlist symbols -> the coin NAMES prediction-market titles use, so an agent
|
|
12
35
|
// discovers PM markets about the coins it actually has a price view on.
|
|
13
36
|
const PM_COIN_NAMES = {
|
|
@@ -42,7 +65,16 @@ export function isCalibrationChurnMarket(market) {
|
|
|
42
65
|
// Tolerant by design: any failure (HTTP error, malformed/sparse candles) returns
|
|
43
66
|
// null so the cycle proceeds with price-only context rather than skipping.
|
|
44
67
|
async function fetchIndicators(client, coinId, trace) {
|
|
45
|
-
|
|
68
|
+
// The try honors the documented tolerance for SYNCHRONOUS throws too (an
|
|
69
|
+
// unexpected client error must degrade to price-only context, never kill
|
|
70
|
+
// the cycle).
|
|
71
|
+
let cr;
|
|
72
|
+
try {
|
|
73
|
+
cr = await client.candles(coinId, INDICATOR_RANGE, trace);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
46
78
|
if (!cr.ok)
|
|
47
79
|
return null;
|
|
48
80
|
// Endpoint shape: { candles: [{ t, o, h, l, c, v }] } ascending (oldest first).
|
|
@@ -272,6 +304,78 @@ export async function observe(client, spec, state, trace) {
|
|
|
272
304
|
}
|
|
273
305
|
watch.push(entry);
|
|
274
306
|
}
|
|
307
|
+
// `universe_scan` capability (2026-08-18, direct user request): discover the
|
|
308
|
+
// top 24h movers across the whole tracked universe, resolve the strongest
|
|
309
|
+
// few into FULL watch entries (marked discovered) and pass the remainder as
|
|
310
|
+
// compact context. Bounds: one movers call + up to
|
|
311
|
+
// UNIVERSE_RESOLVE_TOP resolve/market(+candles) calls per cycle — the same
|
|
312
|
+
// per-symbol cost as ~3 extra watchlist rows, all against CoinRithm's own
|
|
313
|
+
// API (never the model quota). Failures degrade to "no universe section",
|
|
314
|
+
// never a skipped cycle. Watchlist + blocklist symbols are excluded up
|
|
315
|
+
// front so a discovered row can never duplicate or bypass the deny-list.
|
|
316
|
+
let universeMovers;
|
|
317
|
+
if (spec.capabilities.includes("universe_scan")) {
|
|
318
|
+
const mv = await client.cryptoMovers("gainers", UNIVERSE_SCAN_LIMIT, trace);
|
|
319
|
+
if (mv.ok && Array.isArray(mv.data)) {
|
|
320
|
+
const excluded = new Set([...spec.risk.watchlist, ...(spec.risk.blocklist ?? [])].map((s) => s.toUpperCase()));
|
|
321
|
+
const rows = mv.data
|
|
322
|
+
.map(asObj)
|
|
323
|
+
.map((r) => ({
|
|
324
|
+
symbol: (asStr(r.symbol) ?? "").toUpperCase(),
|
|
325
|
+
name: asStr(r.name),
|
|
326
|
+
change24hPct: asNum(r.change24h),
|
|
327
|
+
priceUsd: asNum(r.currentPrice),
|
|
328
|
+
// The movers row already carries the ucid, which IS the coinId every
|
|
329
|
+
// downstream call takes. Kept so the resolve round-trip below can be
|
|
330
|
+
// skipped — see the comment there.
|
|
331
|
+
coinId: asStr(r.ucid),
|
|
332
|
+
}))
|
|
333
|
+
.filter((r) => r.symbol && !excluded.has(r.symbol));
|
|
334
|
+
const resolveTop = rows.slice(0, UNIVERSE_RESOLVE_TOP);
|
|
335
|
+
for (const row of resolveTop) {
|
|
336
|
+
// Prefer the ucid the movers feed already gave us. Resolving the
|
|
337
|
+
// SYMBOL instead was both a wasted call per discovered mover and a
|
|
338
|
+
// correctness hazard: symbols collide across listings, so the resolver
|
|
339
|
+
// could hand back a different coin than the one that actually moved,
|
|
340
|
+
// and the agent would analyze (and trade) that other coin.
|
|
341
|
+
let coinId = row.coinId;
|
|
342
|
+
let resolvedName;
|
|
343
|
+
if (!coinId) {
|
|
344
|
+
const rs = await client.resolve(row.symbol, trace);
|
|
345
|
+
const match = asObj(asObj(rs.data).match);
|
|
346
|
+
coinId =
|
|
347
|
+
rs.ok && match.coinId != null ? String(match.coinId) : undefined;
|
|
348
|
+
resolvedName = asStr(match.name);
|
|
349
|
+
}
|
|
350
|
+
if (!coinId)
|
|
351
|
+
continue;
|
|
352
|
+
const mk = await client.market(coinId, trace);
|
|
353
|
+
const m = asObj(mk.data);
|
|
354
|
+
const price = asObj(m.price);
|
|
355
|
+
const entry = {
|
|
356
|
+
symbol: row.symbol,
|
|
357
|
+
coinId,
|
|
358
|
+
name: resolvedName ?? row.name ?? undefined,
|
|
359
|
+
priceUsd: asNum(price.usd) ?? row.priceUsd,
|
|
360
|
+
change1h: asNum(price.change1h),
|
|
361
|
+
change24h: asNum(price.change24h) ?? row.change24hPct,
|
|
362
|
+
change7d: asNum(price.change7d),
|
|
363
|
+
sentimentBullishPct: asNum(asObj(m.sentiment).bullishPct) ?? undefined,
|
|
364
|
+
freshness: freshnessOf(asObj(m.observation)),
|
|
365
|
+
discovered: true,
|
|
366
|
+
};
|
|
367
|
+
if (wantIndicators) {
|
|
368
|
+
const ind = await fetchIndicators(client, coinId, trace);
|
|
369
|
+
if (ind)
|
|
370
|
+
entry.indicators = ind;
|
|
371
|
+
}
|
|
372
|
+
watch.push(entry);
|
|
373
|
+
}
|
|
374
|
+
const context = rows.slice(UNIVERSE_RESOLVE_TOP);
|
|
375
|
+
if (context.length > 0)
|
|
376
|
+
universeMovers = context;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
275
379
|
// Spot resting orders (for cancel + affordability) — only if spot is enabled.
|
|
276
380
|
const wantSpot = spec.venues.includes("spot");
|
|
277
381
|
const wantPm = spec.venues.includes("pm");
|
|
@@ -451,12 +555,19 @@ export async function observe(client, spec, state, trace) {
|
|
|
451
555
|
}
|
|
452
556
|
}
|
|
453
557
|
// News context (only with the `news` capability): recent high-importance news
|
|
454
|
-
// for the
|
|
455
|
-
//
|
|
456
|
-
//
|
|
558
|
+
// for the coins the agent is actually LOOKING AT this cycle — the watch array,
|
|
559
|
+
// which includes any `universe_scan`-discovered movers. Keying this to the
|
|
560
|
+
// static watchlist alone (the old behavior) starved exactly the case news
|
|
561
|
+
// exists for: a discovered pump whose catalyst the agent is supposed to
|
|
562
|
+
// investigate before acting (the pump-fade pattern, 2026-08-19). One cached
|
|
563
|
+
// call; degrades to no news on failure (never blocks a cycle).
|
|
457
564
|
let news;
|
|
458
|
-
|
|
459
|
-
|
|
565
|
+
const newsCoins = Array.from(new Set([
|
|
566
|
+
...spec.risk.watchlist,
|
|
567
|
+
...watch.map((w) => w.symbol.toUpperCase()),
|
|
568
|
+
]));
|
|
569
|
+
if (wantNews && newsCoins.length > 0) {
|
|
570
|
+
const nr = await client.agentNews({ coins: newsCoins.join(","), limit: 8, hours: 48 }, trace);
|
|
460
571
|
if (nr.ok) {
|
|
461
572
|
news = asArr(asObj(nr.data).items)
|
|
462
573
|
.map(asObj)
|
|
@@ -495,6 +606,7 @@ export async function observe(client, spec, state, trace) {
|
|
|
495
606
|
syncCursor,
|
|
496
607
|
newClosedTrades,
|
|
497
608
|
polledBeforeWrite,
|
|
609
|
+
universeMovers,
|
|
498
610
|
};
|
|
499
611
|
// Skip only when there is NOTHING actionable: no coin resolved (futures/spot)
|
|
500
612
|
// AND no PM candidate (pm). A pm-only agent proceeds on its discovered markets.
|
package/dist/agent/prompt.d.ts
CHANGED