@coinrithm/mcp-trading 0.7.4 → 0.7.6
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 +259 -171
- package/README.md +290 -277
- package/dist/agent/cli.js +22 -1
- package/dist/agent/client.d.ts +1 -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/indicators.d.ts +1 -0
- package/dist/agent/indicators.js +3 -0
- package/dist/agent/manifest.d.ts +1 -0
- package/dist/agent/manifest.js +3 -0
- package/dist/agent/observationReceipt.d.ts +6 -0
- package/dist/agent/observationReceipt.js +10 -0
- package/dist/agent/observe.js +100 -6
- package/dist/agent/prompt.js +19 -1
- package/dist/agent/resolve.d.ts +12 -0
- package/dist/agent/resolve.js +63 -3
- package/dist/agent/runner.js +89 -9
- package/dist/agent/state.d.ts +5 -0
- package/dist/agent/state.js +20 -0
- package/dist/agent/strictLint.js +13 -0
- package/dist/agent/templates.js +6 -0
- package/dist/agent/types.d.ts +14 -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 +5 -0
- package/dist/client.js +16 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +91 -6
- package/package.json +86 -86
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
|
@@ -37,6 +37,7 @@ export declare class CoinRithmClient {
|
|
|
37
37
|
coinId?: string;
|
|
38
38
|
}, trace?: AgentTrace): Promise<ApiResult>;
|
|
39
39
|
resolve(q: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
40
|
+
cryptoMovers(direction: "gainers" | "losers", limit: number, trace?: AgentTrace): Promise<ApiResult>;
|
|
40
41
|
market(coinId: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
41
42
|
candles(coinId: string, range: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
42
43
|
trades(query?: {
|
package/dist/agent/client.js
CHANGED
|
@@ -18,6 +18,10 @@ function traceHeaders(trace) {
|
|
|
18
18
|
h["X-CoinRithm-Strategy-Label"] = trace.strategyLabel;
|
|
19
19
|
if (typeof trace.confidence === "number")
|
|
20
20
|
h["X-CoinRithm-Confidence"] = String(trace.confidence);
|
|
21
|
+
if (trace.observationHash)
|
|
22
|
+
h["X-CoinRithm-Observation-Hash"] = trace.observationHash;
|
|
23
|
+
if (trace.indicatorVersion)
|
|
24
|
+
h["X-CoinRithm-Indicator-Version"] = trace.indicatorVersion;
|
|
21
25
|
return h;
|
|
22
26
|
}
|
|
23
27
|
export class CoinRithmClient {
|
|
@@ -112,6 +116,13 @@ export class CoinRithmClient {
|
|
|
112
116
|
resolve(q, trace) {
|
|
113
117
|
return this.request("GET", "/api/agent/resolve", { query: { q }, trace });
|
|
114
118
|
}
|
|
119
|
+
// Keyless public universe scan (top 24h movers). The Bearer header rides
|
|
120
|
+
// along harmlessly — the /api/coins routes are public and ignore it.
|
|
121
|
+
cryptoMovers(direction, limit, trace) {
|
|
122
|
+
return this.request("GET", direction === "losers"
|
|
123
|
+
? "/api/coins/top-losers"
|
|
124
|
+
: "/api/coins/top-gainers", { query: { limit }, trace });
|
|
125
|
+
}
|
|
115
126
|
market(coinId, trace) {
|
|
116
127
|
return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { trace });
|
|
117
128
|
}
|
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
|
package/dist/agent/indicators.js
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Every function returns null when there are too few candles, so callers can
|
|
12
12
|
// omit an indicator from the observation rather than emit a misleading number.
|
|
13
|
+
// Bump whenever indicator inputs, defaults, formulas, rounding, or output
|
|
14
|
+
// semantics change. Stored beside each observation hash and in manifest.lock.
|
|
15
|
+
export const INDICATOR_VERSION = "coinrithm.indicators.v1";
|
|
13
16
|
const closesOf = (c) => c.map((x) => x.close);
|
|
14
17
|
const finite = (n) => Number.isFinite(n);
|
|
15
18
|
// Simple moving average of the last `period` values.
|
package/dist/agent/manifest.d.ts
CHANGED
package/dist/agent/manifest.js
CHANGED
|
@@ -6,12 +6,14 @@ import { writeFileSync, mkdirSync } from "node:fs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { sha256, stableStringify, toPosix } from "./util.js";
|
|
8
8
|
import { RESOLVER_VERSION, RUNNER_VERSION, MANIFEST_SCHEMA, } from "./version.js";
|
|
9
|
+
import { INDICATOR_VERSION } from "./indicators.js";
|
|
9
10
|
export function buildManifest(resolved, spec) {
|
|
10
11
|
// configHash binds the RESOLVED spec to the resolver + schema version, so a
|
|
11
12
|
// run reproduces only against the same compile (not just the same files).
|
|
12
13
|
const configHash = sha256(stableStringify({
|
|
13
14
|
resolvedSpec: spec,
|
|
14
15
|
resolverVersion: RESOLVER_VERSION,
|
|
16
|
+
indicatorVersion: INDICATOR_VERSION,
|
|
15
17
|
schema: MANIFEST_SCHEMA,
|
|
16
18
|
}));
|
|
17
19
|
return {
|
|
@@ -19,6 +21,7 @@ export function buildManifest(resolved, spec) {
|
|
|
19
21
|
spec: spec.spec || "coinrithm.agent.v1",
|
|
20
22
|
resolverVersion: RESOLVER_VERSION,
|
|
21
23
|
runnerVersion: RUNNER_VERSION,
|
|
24
|
+
indicatorVersion: INDICATOR_VERSION,
|
|
22
25
|
resolvedConfig: resolved.rawFrontmatter,
|
|
23
26
|
resolvedSpec: spec,
|
|
24
27
|
provenance: resolved.provenance,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// A compact, privacy-safe receipt for the exact structured observation used by
|
|
2
|
+
// one decision. We persist the digest, never the full prompt or model output.
|
|
3
|
+
import { INDICATOR_VERSION } from "./indicators.js";
|
|
4
|
+
import { sha256, stableStringify } from "./util.js";
|
|
5
|
+
export function buildObservationReceipt(observation) {
|
|
6
|
+
return {
|
|
7
|
+
observationHash: sha256(stableStringify(observation)),
|
|
8
|
+
indicatorVersion: INDICATOR_VERSION,
|
|
9
|
+
};
|
|
10
|
+
}
|
package/dist/agent/observe.js
CHANGED
|
@@ -8,6 +8,11 @@ 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
|
+
const UNIVERSE_SCAN_LIMIT = 15;
|
|
15
|
+
const UNIVERSE_RESOLVE_TOP = 3;
|
|
11
16
|
// Watchlist symbols -> the coin NAMES prediction-market titles use, so an agent
|
|
12
17
|
// discovers PM markets about the coins it actually has a price view on.
|
|
13
18
|
const PM_COIN_NAMES = {
|
|
@@ -42,7 +47,16 @@ export function isCalibrationChurnMarket(market) {
|
|
|
42
47
|
// Tolerant by design: any failure (HTTP error, malformed/sparse candles) returns
|
|
43
48
|
// null so the cycle proceeds with price-only context rather than skipping.
|
|
44
49
|
async function fetchIndicators(client, coinId, trace) {
|
|
45
|
-
|
|
50
|
+
// The try honors the documented tolerance for SYNCHRONOUS throws too (an
|
|
51
|
+
// unexpected client error must degrade to price-only context, never kill
|
|
52
|
+
// the cycle).
|
|
53
|
+
let cr;
|
|
54
|
+
try {
|
|
55
|
+
cr = await client.candles(coinId, INDICATOR_RANGE, trace);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
46
60
|
if (!cr.ok)
|
|
47
61
|
return null;
|
|
48
62
|
// Endpoint shape: { candles: [{ t, o, h, l, c, v }] } ascending (oldest first).
|
|
@@ -272,6 +286,78 @@ export async function observe(client, spec, state, trace) {
|
|
|
272
286
|
}
|
|
273
287
|
watch.push(entry);
|
|
274
288
|
}
|
|
289
|
+
// `universe_scan` capability (2026-08-18, direct user request): discover the
|
|
290
|
+
// top 24h movers across the whole tracked universe, resolve the strongest
|
|
291
|
+
// few into FULL watch entries (marked discovered) and pass the remainder as
|
|
292
|
+
// compact context. Bounds: one movers call + up to
|
|
293
|
+
// UNIVERSE_RESOLVE_TOP resolve/market(+candles) calls per cycle — the same
|
|
294
|
+
// per-symbol cost as ~3 extra watchlist rows, all against CoinRithm's own
|
|
295
|
+
// API (never the model quota). Failures degrade to "no universe section",
|
|
296
|
+
// never a skipped cycle. Watchlist + blocklist symbols are excluded up
|
|
297
|
+
// front so a discovered row can never duplicate or bypass the deny-list.
|
|
298
|
+
let universeMovers;
|
|
299
|
+
if (spec.capabilities.includes("universe_scan")) {
|
|
300
|
+
const mv = await client.cryptoMovers("gainers", UNIVERSE_SCAN_LIMIT, trace);
|
|
301
|
+
if (mv.ok && Array.isArray(mv.data)) {
|
|
302
|
+
const excluded = new Set([...spec.risk.watchlist, ...(spec.risk.blocklist ?? [])].map((s) => s.toUpperCase()));
|
|
303
|
+
const rows = mv.data
|
|
304
|
+
.map(asObj)
|
|
305
|
+
.map((r) => ({
|
|
306
|
+
symbol: (asStr(r.symbol) ?? "").toUpperCase(),
|
|
307
|
+
name: asStr(r.name),
|
|
308
|
+
change24hPct: asNum(r.change24h),
|
|
309
|
+
priceUsd: asNum(r.currentPrice),
|
|
310
|
+
// The movers row already carries the ucid, which IS the coinId every
|
|
311
|
+
// downstream call takes. Kept so the resolve round-trip below can be
|
|
312
|
+
// skipped — see the comment there.
|
|
313
|
+
coinId: asStr(r.ucid),
|
|
314
|
+
}))
|
|
315
|
+
.filter((r) => r.symbol && !excluded.has(r.symbol));
|
|
316
|
+
const resolveTop = rows.slice(0, UNIVERSE_RESOLVE_TOP);
|
|
317
|
+
for (const row of resolveTop) {
|
|
318
|
+
// Prefer the ucid the movers feed already gave us. Resolving the
|
|
319
|
+
// SYMBOL instead was both a wasted call per discovered mover and a
|
|
320
|
+
// correctness hazard: symbols collide across listings, so the resolver
|
|
321
|
+
// could hand back a different coin than the one that actually moved,
|
|
322
|
+
// and the agent would analyze (and trade) that other coin.
|
|
323
|
+
let coinId = row.coinId;
|
|
324
|
+
let resolvedName;
|
|
325
|
+
if (!coinId) {
|
|
326
|
+
const rs = await client.resolve(row.symbol, trace);
|
|
327
|
+
const match = asObj(asObj(rs.data).match);
|
|
328
|
+
coinId =
|
|
329
|
+
rs.ok && match.coinId != null ? String(match.coinId) : undefined;
|
|
330
|
+
resolvedName = asStr(match.name);
|
|
331
|
+
}
|
|
332
|
+
if (!coinId)
|
|
333
|
+
continue;
|
|
334
|
+
const mk = await client.market(coinId, trace);
|
|
335
|
+
const m = asObj(mk.data);
|
|
336
|
+
const price = asObj(m.price);
|
|
337
|
+
const entry = {
|
|
338
|
+
symbol: row.symbol,
|
|
339
|
+
coinId,
|
|
340
|
+
name: resolvedName ?? row.name ?? undefined,
|
|
341
|
+
priceUsd: asNum(price.usd) ?? row.priceUsd,
|
|
342
|
+
change1h: asNum(price.change1h),
|
|
343
|
+
change24h: asNum(price.change24h) ?? row.change24hPct,
|
|
344
|
+
change7d: asNum(price.change7d),
|
|
345
|
+
sentimentBullishPct: asNum(asObj(m.sentiment).bullishPct) ?? undefined,
|
|
346
|
+
freshness: freshnessOf(asObj(m.observation)),
|
|
347
|
+
discovered: true,
|
|
348
|
+
};
|
|
349
|
+
if (wantIndicators) {
|
|
350
|
+
const ind = await fetchIndicators(client, coinId, trace);
|
|
351
|
+
if (ind)
|
|
352
|
+
entry.indicators = ind;
|
|
353
|
+
}
|
|
354
|
+
watch.push(entry);
|
|
355
|
+
}
|
|
356
|
+
const context = rows.slice(UNIVERSE_RESOLVE_TOP);
|
|
357
|
+
if (context.length > 0)
|
|
358
|
+
universeMovers = context;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
275
361
|
// Spot resting orders (for cancel + affordability) — only if spot is enabled.
|
|
276
362
|
const wantSpot = spec.venues.includes("spot");
|
|
277
363
|
const wantPm = spec.venues.includes("pm");
|
|
@@ -451,12 +537,19 @@ export async function observe(client, spec, state, trace) {
|
|
|
451
537
|
}
|
|
452
538
|
}
|
|
453
539
|
// News context (only with the `news` capability): recent high-importance news
|
|
454
|
-
// for the
|
|
455
|
-
//
|
|
456
|
-
//
|
|
540
|
+
// for the coins the agent is actually LOOKING AT this cycle — the watch array,
|
|
541
|
+
// which includes any `universe_scan`-discovered movers. Keying this to the
|
|
542
|
+
// static watchlist alone (the old behavior) starved exactly the case news
|
|
543
|
+
// exists for: a discovered pump whose catalyst the agent is supposed to
|
|
544
|
+
// investigate before acting (the pump-fade pattern, 2026-08-19). One cached
|
|
545
|
+
// call; degrades to no news on failure (never blocks a cycle).
|
|
457
546
|
let news;
|
|
458
|
-
|
|
459
|
-
|
|
547
|
+
const newsCoins = Array.from(new Set([
|
|
548
|
+
...spec.risk.watchlist,
|
|
549
|
+
...watch.map((w) => w.symbol.toUpperCase()),
|
|
550
|
+
]));
|
|
551
|
+
if (wantNews && newsCoins.length > 0) {
|
|
552
|
+
const nr = await client.agentNews({ coins: newsCoins.join(","), limit: 8, hours: 48 }, trace);
|
|
460
553
|
if (nr.ok) {
|
|
461
554
|
news = asArr(asObj(nr.data).items)
|
|
462
555
|
.map(asObj)
|
|
@@ -495,6 +588,7 @@ export async function observe(client, spec, state, trace) {
|
|
|
495
588
|
syncCursor,
|
|
496
589
|
newClosedTrades,
|
|
497
590
|
polledBeforeWrite,
|
|
591
|
+
universeMovers,
|
|
498
592
|
};
|
|
499
593
|
// Skip only when there is NOTHING actionable: no coin resolved (futures/spot)
|
|
500
594
|
// AND no PM candidate (pm). A pm-only agent proceeds on its discovered markets.
|
package/dist/agent/prompt.js
CHANGED
|
@@ -63,7 +63,15 @@ opts = {}) {
|
|
|
63
63
|
`- venues you may act in: ${v.join(", ")}`,
|
|
64
64
|
`- perTradeMarginMusd ${r.perTradeMarginMusd} is the per-trade SIZE cap (futures margin / spot buy notional / PM stake)`,
|
|
65
65
|
`- futures: maxLeverage ${r.maxLeverage}, maxConcurrentPositions ${r.maxConcurrentPositions}, requireStopLoss ${r.requireStopLoss} (long stop below entry, short stop above)`,
|
|
66
|
-
|
|
66
|
+
// With universe_scan, the validator's gate is WATCH-membership (manual
|
|
67
|
+
// watchlist ∪ this cycle's discovered entries) — saying "ONLY these" here
|
|
68
|
+
// while the universe-scan section below calls discovered movers tradable
|
|
69
|
+
// made cap-obedient models refuse every discovered candidate (the caps
|
|
70
|
+
// header says proposing outside a cap wastes the cycle). Keep the two
|
|
71
|
+
// sections telling one story.
|
|
72
|
+
spec.capabilities.includes("universe_scan")
|
|
73
|
+
? `- tradable symbols (spot + futures): your watchlist (${r.watchlist.join(", ")}) PLUS this cycle's watch entries marked \`discovered: true\` — nothing outside those`
|
|
74
|
+
: `- watchlist (spot + futures use ONLY these): ${r.watchlist.join(", ")}`,
|
|
67
75
|
...(r.blocklist && r.blocklist.length > 0
|
|
68
76
|
? [
|
|
69
77
|
`- deny-list (NEVER open these, even if on the watchlist): ${r.blocklist.join(", ")}`,
|
|
@@ -87,6 +95,15 @@ opts = {}) {
|
|
|
87
95
|
"- a null field = not enough data; ignore it. These INFORM your decision; they never widen a cap.",
|
|
88
96
|
]
|
|
89
97
|
: []),
|
|
98
|
+
...(spec.capabilities.includes("universe_scan")
|
|
99
|
+
? [
|
|
100
|
+
"",
|
|
101
|
+
"## Universe scan (discovered movers) — candidates beyond your watchlist",
|
|
102
|
+
"Watch entries with `discovered: true` are today's strongest 24h movers across the WHOLE tracked universe, resolved with the same price/sentiment (and indicators) data as your watchlist. observation.universeMovers lists further movers as symbol + 24h change only (context — you cannot trade those directly this cycle).",
|
|
103
|
+
"- Treat a discovered candidate like any other symbol: analyze it for catalysts, exhaustion and reversal BEFORE acting. A big 24h pump is as often a top as a beginning — chasing green candles blind is how discovery loses money.",
|
|
104
|
+
"- All your normal risk rules apply unchanged: caps, stops, blocklist, confidence floor. Discovery widens what you can SEE, never what you may risk.",
|
|
105
|
+
]
|
|
106
|
+
: []),
|
|
90
107
|
...(spec.capabilities.includes("news")
|
|
91
108
|
? [
|
|
92
109
|
"",
|
|
@@ -169,6 +186,7 @@ export function buildUserPrompt(obs, journal) {
|
|
|
169
186
|
watch: obs.watch,
|
|
170
187
|
setups: obs.setups,
|
|
171
188
|
news: obs.news,
|
|
189
|
+
universeMovers: obs.universeMovers,
|
|
172
190
|
marketMood: obs.marketMood,
|
|
173
191
|
newClosedTrades: obs.newClosedTrades.slice(0, 20),
|
|
174
192
|
polledBeforeWrite: obs.polledBeforeWrite,
|
package/dist/agent/resolve.d.ts
CHANGED
|
@@ -3,9 +3,21 @@ export declare class ResolveError extends Error {
|
|
|
3
3
|
issues: ResolveIssue[];
|
|
4
4
|
constructor(issues: ResolveIssue[]);
|
|
5
5
|
}
|
|
6
|
+
export declare const proseBody: (raw: string) => string;
|
|
7
|
+
export declare const GUARDS_FILE = "character/guards.md";
|
|
8
|
+
export declare const GUARDS_HEADER = "## HARD BEHAVIORAL GUARDS \u2014 never violate these";
|
|
9
|
+
export declare const GUARDS_FOOTER = "(These guards override every other instruction in this strategy. When a guard conflicts with an opportunity, the guard wins and the correct output is a skip that names the guard.)";
|
|
10
|
+
export declare const wrapGuardsProse: (body: string) => string;
|
|
6
11
|
export declare function isSkillProseSource(source: string): boolean;
|
|
7
12
|
export declare function mergeProseParts(parts: Array<{
|
|
8
13
|
source: string;
|
|
9
14
|
text: string;
|
|
10
15
|
}>): string;
|
|
11
16
|
export declare function resolveAgent(inputPath: string): ResolvedAgent;
|
|
17
|
+
export declare const HOSTED_PROSE_MAX_CHARS = 8000;
|
|
18
|
+
/** PURE — exported for tests. Mirrors the backend's trim-then-measure. */
|
|
19
|
+
export declare const hostedProseBudget: (mergedProse: string) => {
|
|
20
|
+
used: number;
|
|
21
|
+
over: number;
|
|
22
|
+
fits: boolean;
|
|
23
|
+
};
|
package/dist/agent/resolve.js
CHANGED
|
@@ -47,6 +47,32 @@ const JOURNAL_MAX_LINES = 200;
|
|
|
47
47
|
const JOURNAL_MAX_BYTES = 8_000;
|
|
48
48
|
// Optional prose files (markdown the LLM reads), in assembly order.
|
|
49
49
|
const PROSE_FILES = ["character/thesis.md", "character/persona.md"];
|
|
50
|
+
// Prose files carry an OPTIONAL YAML frontmatter block (type/title/description/
|
|
51
|
+
// tags) that is authoring metadata, not doctrine — the model gains nothing from
|
|
52
|
+
// `tags: [agent, persona, mean-reversion]`. It was being merged verbatim into
|
|
53
|
+
// the system prompt: pure noise, and on the hosted path it also consumed the
|
|
54
|
+
// 8,000-char strategy budget (measured 2026-08-19: ~1.0k chars across a
|
|
55
|
+
// decomposed bundle's thesis/persona/journal). Skill files already strip theirs
|
|
56
|
+
// (their frontmatter is parsed for the cap patch), and guards.md strips too —
|
|
57
|
+
// this brings the remaining prose files in line. Files WITHOUT frontmatter are
|
|
58
|
+
// returned unchanged.
|
|
59
|
+
export const proseBody = (raw) => {
|
|
60
|
+
const src = raw.replace(/\r\n/g, "\n");
|
|
61
|
+
const m = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(src);
|
|
62
|
+
return (m ? m[1] : src).trim();
|
|
63
|
+
};
|
|
64
|
+
// First-class hard-guards file (2026-08-19, audit rank 7). User-authored
|
|
65
|
+
// behavioral borders that machine caps cannot express ("never open a short
|
|
66
|
+
// unless a qualifying pump preceded it") previously lived as an undocumented
|
|
67
|
+
// "Hard borders" paragraph buried mid-persona — present in only 5 of 8
|
|
68
|
+
// bundles and easy for a forker to miss. character/guards.md gets a dedicated
|
|
69
|
+
// slot: loaded LAST so the block lands at the END of the strategy prose,
|
|
70
|
+
// immediately adjacent to the system prompt's hard-caps section, wrapped in a
|
|
71
|
+
// high-salience header plus an explicit guards-win-conflicts rule.
|
|
72
|
+
export const GUARDS_FILE = "character/guards.md";
|
|
73
|
+
export const GUARDS_HEADER = "## HARD BEHAVIORAL GUARDS — never violate these";
|
|
74
|
+
export const GUARDS_FOOTER = "(These guards override every other instruction in this strategy. When a guard conflicts with an opportunity, the guard wins and the correct output is a skip that names the guard.)";
|
|
75
|
+
export const wrapGuardsProse = (body) => `${GUARDS_HEADER}\n\n${body.trim()}\n\n${GUARDS_FOOTER}`;
|
|
50
76
|
const FUNCTIONALITY_PIN = "functionality/coinrithm.yaml";
|
|
51
77
|
// Enforced cap field names. sizing.yaml is SOFT guidance and must NOT contain
|
|
52
78
|
// any of these (or a user could think a limit binds when it does not).
|
|
@@ -427,8 +453,9 @@ function resolveDirectory(dir) {
|
|
|
427
453
|
const p = join(dir, pf);
|
|
428
454
|
if (existsSync(p)) {
|
|
429
455
|
const abs = safePath(ctx, pf, "prose");
|
|
430
|
-
if (abs)
|
|
431
|
-
proseParts.push({ source: pf, text: readHashed(ctx, abs) });
|
|
456
|
+
if (abs) {
|
|
457
|
+
proseParts.push({ source: pf, text: proseBody(readHashed(ctx, abs)) });
|
|
458
|
+
}
|
|
432
459
|
}
|
|
433
460
|
}
|
|
434
461
|
proseParts.push(...skillProse);
|
|
@@ -437,13 +464,28 @@ function resolveDirectory(dir) {
|
|
|
437
464
|
if (existsSync(journalPath)) {
|
|
438
465
|
const abs = safePath(ctx, "journal/notes.md", "journal");
|
|
439
466
|
if (abs) {
|
|
440
|
-
const full = readHashed(ctx, abs);
|
|
467
|
+
const full = proseBody(readHashed(ctx, abs));
|
|
441
468
|
proseParts.push({
|
|
442
469
|
source: "journal/notes.md",
|
|
443
470
|
text: boundTail(full, JOURNAL_MAX_LINES, JOURNAL_MAX_BYTES),
|
|
444
471
|
});
|
|
445
472
|
}
|
|
446
473
|
}
|
|
474
|
+
// Hard behavioral guards — see GUARDS_FILE above. Pushed AFTER the journal
|
|
475
|
+
// so the wrapped block is the final prose the model reads before the caps
|
|
476
|
+
// section. Frontmatter is optional and stripped (only the body is doctrine);
|
|
477
|
+
// an empty body contributes nothing.
|
|
478
|
+
const guardsAbsPath = join(dir, GUARDS_FILE);
|
|
479
|
+
if (existsSync(guardsAbsPath)) {
|
|
480
|
+
const abs = safePath(ctx, GUARDS_FILE, "guards");
|
|
481
|
+
if (abs) {
|
|
482
|
+
const raw = readHashed(ctx, abs);
|
|
483
|
+
const body = (raw.startsWith("---") ? parseFrontmatter(raw).body : raw).trim();
|
|
484
|
+
if (body) {
|
|
485
|
+
proseParts.push({ source: GUARDS_FILE, text: wrapGuardsProse(body) });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
447
489
|
// Optional API/tool contract pin. It is locked for reproducibility and stale
|
|
448
490
|
// warnings, but it is not part of AgentSpec and is never sent to the model.
|
|
449
491
|
const functionalityPath = join(dir, FUNCTIONALITY_PIN);
|
|
@@ -542,3 +584,21 @@ export function resolveAgent(inputPath) {
|
|
|
542
584
|
{ code: "input_invalid", message: "input must be a file or a directory" },
|
|
543
585
|
]);
|
|
544
586
|
}
|
|
587
|
+
// ── Hosted strategy-prose budget ───────────────────────────────────────────
|
|
588
|
+
// The managed (Studio) deploy/edit API caps the merged strategy prose and
|
|
589
|
+
// reverts the save past the cap; the self-host runner has NO such limit. The
|
|
590
|
+
// cap is not arbitrary — the merged prose becomes the system prompt, and an
|
|
591
|
+
// oversized prompt is what previously pushed small free-tier models to ~69k
|
|
592
|
+
// tokens / 413s per cycle. Mirrored here (backend-v2
|
|
593
|
+
// controllers/agentManage.ts sanitizeStrategyProse) so `validate --hosted`
|
|
594
|
+
// can catch it before a user does.
|
|
595
|
+
export const HOSTED_PROSE_MAX_CHARS = 8000;
|
|
596
|
+
/** PURE — exported for tests. Mirrors the backend's trim-then-measure. */
|
|
597
|
+
export const hostedProseBudget = (mergedProse) => {
|
|
598
|
+
const used = mergedProse.replace(/\r\n/g, "\n").trim().length;
|
|
599
|
+
return {
|
|
600
|
+
used,
|
|
601
|
+
over: Math.max(0, used - HOSTED_PROSE_MAX_CHARS),
|
|
602
|
+
fits: used <= HOSTED_PROSE_MAX_CHARS,
|
|
603
|
+
};
|
|
604
|
+
};
|