@coinrithm/mcp-trading 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -0
- package/README.md +37 -8
- package/dist/agent/act.js +19 -6
- package/dist/agent/capitalSizing.d.ts +32 -0
- package/dist/agent/capitalSizing.js +257 -0
- package/dist/agent/client.d.ts +2 -0
- package/dist/agent/client.js +4 -0
- package/dist/agent/decision.d.ts +392 -0
- package/dist/agent/decision.js +177 -0
- package/dist/agent/decisionProbe.d.ts +17 -0
- package/dist/agent/decisionProbe.js +70 -0
- package/dist/agent/decisionReceipt.d.ts +45 -0
- package/dist/agent/decisionReceipt.js +595 -0
- package/dist/agent/decisionValidator.d.ts +19 -2
- package/dist/agent/decisionValidator.js +97 -3
- package/dist/agent/engine.d.ts +5 -1
- package/dist/agent/engine.js +8 -1
- package/dist/agent/observe.js +194 -35
- package/dist/agent/pmContext.d.ts +13 -0
- package/dist/agent/pmContext.js +136 -0
- package/dist/agent/prompt.d.ts +14 -2
- package/dist/agent/prompt.js +232 -35
- package/dist/agent/providerCapabilities.d.ts +23 -0
- package/dist/agent/providerCapabilities.js +105 -0
- package/dist/agent/providers.d.ts +29 -1
- package/dist/agent/providers.js +159 -88
- package/dist/agent/resolve.d.ts +1 -1
- package/dist/agent/resolve.js +21 -1
- package/dist/agent/runner.d.ts +4 -1
- package/dist/agent/runner.js +418 -47
- package/dist/agent/scorecard.js +7 -1
- package/dist/agent/skill.js +23 -0
- package/dist/agent/skillValidator.d.ts +1 -0
- package/dist/agent/skillValidator.js +63 -0
- package/dist/agent/state.js +7 -1
- package/dist/agent/strictLint.js +20 -0
- package/dist/agent/thesis.d.ts +40 -0
- package/dist/agent/thesis.js +319 -0
- package/dist/agent/types.d.ts +151 -0
- package/dist/http.js +21 -0
- package/dist/tools.js +10 -10
- package/package.json +1 -1
package/dist/agent/decision.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// JSON, an unknown action type, a free-form endpoint/tool name, extra unknown
|
|
4
4
|
// fields, or a missing required field — fails closed (the runner skips).
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { coerceThesis } from "./thesis.js";
|
|
6
7
|
// Small models (especially Llama 3.1 8B) frequently emit numbers as JSON strings
|
|
7
8
|
// ("12345", "0.8", "62000"). Coerce a *clean* numeric string to a number before
|
|
8
9
|
// validating; leave anything else untouched so genuine garbage ("abc", "pos#5")
|
|
@@ -31,6 +32,15 @@ const forecastProbability = z
|
|
|
31
32
|
return typeof n === "number" && Number.isFinite(n) ? n : undefined;
|
|
32
33
|
})
|
|
33
34
|
.optional();
|
|
35
|
+
// The thesis an OPEN is made on (slice 2, 2026-09-02): {summary, invalidation}.
|
|
36
|
+
// TOLERANT like forecastProbability: a missing / malformed thesis becomes
|
|
37
|
+
// undefined and never fails an otherwise-valid open. The prompt requires it and
|
|
38
|
+
// the structured-output schema below lists it as required; the runner binds it
|
|
39
|
+
// to the position the server returns and evaluates it every cycle.
|
|
40
|
+
const thesis = z
|
|
41
|
+
.any()
|
|
42
|
+
.transform((v) => coerceThesis(v))
|
|
43
|
+
.optional();
|
|
34
44
|
const futuresOpen = z
|
|
35
45
|
.object({
|
|
36
46
|
type: z.literal("futures_open"),
|
|
@@ -42,6 +52,7 @@ const futuresOpen = z
|
|
|
42
52
|
takeProfitPrice: num(z.number()).nullable().optional(),
|
|
43
53
|
confidence,
|
|
44
54
|
rationaleSummary: z.string().optional(),
|
|
55
|
+
thesis,
|
|
45
56
|
})
|
|
46
57
|
.strict();
|
|
47
58
|
const futuresClose = z
|
|
@@ -51,6 +62,11 @@ const futuresClose = z
|
|
|
51
62
|
fraction: num(z.number().positive().max(1)).optional(),
|
|
52
63
|
confidence,
|
|
53
64
|
rationaleSummary: z.string().optional(),
|
|
65
|
+
// Accepted-and-unused: models copy the open-side `thesis` onto every
|
|
66
|
+
// action exactly as they copied `confidence` (the .strict() fail-closed
|
|
67
|
+
// lesson of 2026-08-19). The structured-output schema never asks for it
|
|
68
|
+
// here; a BYO model that echoes it must not zero the whole cycle.
|
|
69
|
+
thesis,
|
|
54
70
|
})
|
|
55
71
|
.strict();
|
|
56
72
|
const futuresSetSltp = z
|
|
@@ -66,6 +82,7 @@ const futuresSetSltp = z
|
|
|
66
82
|
// trade actions already have.
|
|
67
83
|
confidence,
|
|
68
84
|
rationaleSummary: z.string().optional(),
|
|
85
|
+
thesis, // accepted-and-unused, see futures_close
|
|
69
86
|
})
|
|
70
87
|
.strict();
|
|
71
88
|
const spotOrder = z
|
|
@@ -79,6 +96,7 @@ const spotOrder = z
|
|
|
79
96
|
stopPrice: num(z.number().positive()).optional(),
|
|
80
97
|
confidence,
|
|
81
98
|
rationaleSummary: z.string().optional(),
|
|
99
|
+
thesis,
|
|
82
100
|
})
|
|
83
101
|
.strict();
|
|
84
102
|
const spotCancel = z
|
|
@@ -88,6 +106,7 @@ const spotCancel = z
|
|
|
88
106
|
// Same accepted-and-unused tolerance as futures_set_sltp above.
|
|
89
107
|
confidence,
|
|
90
108
|
rationaleSummary: z.string().optional(),
|
|
109
|
+
thesis,
|
|
91
110
|
})
|
|
92
111
|
.strict();
|
|
93
112
|
// pm_open accepts EITHER a short ref (pm1…pmN, what the prompt now asks for) OR
|
|
@@ -107,6 +126,7 @@ const pmOpen = z
|
|
|
107
126
|
confidence,
|
|
108
127
|
rationaleSummary: z.string().optional(),
|
|
109
128
|
forecastProbability,
|
|
129
|
+
thesis,
|
|
110
130
|
})
|
|
111
131
|
.strict();
|
|
112
132
|
export const actionSchema = z.discriminatedUnion("type", [
|
|
@@ -117,6 +137,163 @@ export const actionSchema = z.discriminatedUnion("type", [
|
|
|
117
137
|
spotCancel,
|
|
118
138
|
pmOpen,
|
|
119
139
|
]);
|
|
140
|
+
// Request-side structured-output contract for providers that support JSON
|
|
141
|
+
// Schema. Keep this beside the Zod parser: the provider must not be allowed to
|
|
142
|
+
// emit an `act` with no actions (the Nano incident on 2026-08-27), while a real
|
|
143
|
+
// skip must carry an empty action list. The runner still parses with Zod and
|
|
144
|
+
// re-validates every action against live quotes/caps before any write.
|
|
145
|
+
//
|
|
146
|
+
// `pm_open` deliberately requires the short per-cycle `ref` here. The parser
|
|
147
|
+
// remains backwards-compatible with the full source/slug/id triple, but the
|
|
148
|
+
// hosted prompt asks models for refs and that is the only reliable generated
|
|
149
|
+
// shape.
|
|
150
|
+
const nullableNumber = { anyOf: [{ type: "number" }, { type: "null" }] };
|
|
151
|
+
const optionalConfidence = { type: "number", minimum: 0, maximum: 1 };
|
|
152
|
+
const optionalRationaleSummary = { type: "string" };
|
|
153
|
+
// Thesis contract for the structured-output transports (the NVIDIA endpoint
|
|
154
|
+
// forces the tool call from this schema, so a field absent here can never be
|
|
155
|
+
// emitted). One shared shape; the runner drops the fields that do not apply to
|
|
156
|
+
// the venue and re-signs nothing.
|
|
157
|
+
const thesisSchema = {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: {
|
|
160
|
+
summary: { type: "string", maxLength: 200 },
|
|
161
|
+
invalidation: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
priceBelow: { type: "number", exclusiveMinimum: 0 },
|
|
165
|
+
priceAbove: { type: "number", exclusiveMinimum: 0 },
|
|
166
|
+
probabilityBelow: { type: "number", exclusiveMinimum: 0, maximum: 100 },
|
|
167
|
+
probabilityAbove: { type: "number", exclusiveMinimum: 0, maximum: 100 },
|
|
168
|
+
maxHoldMinutes: { type: "number", exclusiveMinimum: 0 },
|
|
169
|
+
catalyst: { type: "string", maxLength: 160 },
|
|
170
|
+
},
|
|
171
|
+
additionalProperties: false,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
required: ["summary", "invalidation"],
|
|
175
|
+
additionalProperties: false,
|
|
176
|
+
};
|
|
177
|
+
const generatedActionSchemas = [
|
|
178
|
+
{
|
|
179
|
+
type: "object",
|
|
180
|
+
properties: {
|
|
181
|
+
type: { const: "futures_open" },
|
|
182
|
+
symbol: { type: "string", minLength: 1 },
|
|
183
|
+
side: { enum: ["long", "short"] },
|
|
184
|
+
leverage: { type: "number", exclusiveMinimum: 0 },
|
|
185
|
+
marginMusd: { type: "number", exclusiveMinimum: 0 },
|
|
186
|
+
stopLossPrice: nullableNumber,
|
|
187
|
+
takeProfitPrice: nullableNumber,
|
|
188
|
+
confidence: optionalConfidence,
|
|
189
|
+
rationaleSummary: optionalRationaleSummary,
|
|
190
|
+
thesis: thesisSchema,
|
|
191
|
+
},
|
|
192
|
+
required: ["type", "symbol", "side", "leverage", "marginMusd", "thesis"],
|
|
193
|
+
additionalProperties: false,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
type: { const: "futures_close" },
|
|
199
|
+
positionId: { type: "number" },
|
|
200
|
+
fraction: { type: "number", exclusiveMinimum: 0, maximum: 1 },
|
|
201
|
+
confidence: optionalConfidence,
|
|
202
|
+
rationaleSummary: optionalRationaleSummary,
|
|
203
|
+
},
|
|
204
|
+
required: ["type", "positionId"],
|
|
205
|
+
additionalProperties: false,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
type: { const: "futures_set_sltp" },
|
|
211
|
+
positionId: { type: "number" },
|
|
212
|
+
stopLossPrice: nullableNumber,
|
|
213
|
+
takeProfitPrice: nullableNumber,
|
|
214
|
+
confidence: optionalConfidence,
|
|
215
|
+
rationaleSummary: optionalRationaleSummary,
|
|
216
|
+
},
|
|
217
|
+
required: ["type", "positionId"],
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
type: { const: "spot_order" },
|
|
224
|
+
symbol: { type: "string", minLength: 1 },
|
|
225
|
+
side: { enum: ["buy", "sell"] },
|
|
226
|
+
orderType: { enum: ["market", "limit", "stop"] },
|
|
227
|
+
quantity: { type: "number", exclusiveMinimum: 0 },
|
|
228
|
+
limitPrice: { type: "number", exclusiveMinimum: 0 },
|
|
229
|
+
stopPrice: { type: "number", exclusiveMinimum: 0 },
|
|
230
|
+
confidence: optionalConfidence,
|
|
231
|
+
rationaleSummary: optionalRationaleSummary,
|
|
232
|
+
thesis: thesisSchema,
|
|
233
|
+
},
|
|
234
|
+
required: ["type", "symbol", "side", "orderType", "quantity", "thesis"],
|
|
235
|
+
additionalProperties: false,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
type: { const: "spot_cancel" },
|
|
241
|
+
orderId: { type: "number" },
|
|
242
|
+
confidence: optionalConfidence,
|
|
243
|
+
rationaleSummary: optionalRationaleSummary,
|
|
244
|
+
},
|
|
245
|
+
required: ["type", "orderId"],
|
|
246
|
+
additionalProperties: false,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
type: "object",
|
|
250
|
+
properties: {
|
|
251
|
+
type: { const: "pm_open" },
|
|
252
|
+
ref: { type: "string", minLength: 1 },
|
|
253
|
+
stakeMusd: { type: "number", exclusiveMinimum: 0 },
|
|
254
|
+
confidence: optionalConfidence,
|
|
255
|
+
rationaleSummary: optionalRationaleSummary,
|
|
256
|
+
forecastProbability: { type: "number" },
|
|
257
|
+
thesis: thesisSchema,
|
|
258
|
+
},
|
|
259
|
+
required: ["type", "ref", "stakeMusd", "thesis"],
|
|
260
|
+
additionalProperties: false,
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
const generatedDecisionProperties = {
|
|
264
|
+
confidence: optionalConfidence,
|
|
265
|
+
reason: { type: "string" },
|
|
266
|
+
rationale: { type: "string", maxLength: 1200 },
|
|
267
|
+
};
|
|
268
|
+
export const DECISION_JSON_SCHEMA = {
|
|
269
|
+
type: "object",
|
|
270
|
+
properties: {
|
|
271
|
+
decision: { enum: ["skip", "act"] },
|
|
272
|
+
...generatedDecisionProperties,
|
|
273
|
+
actions: {
|
|
274
|
+
type: "array",
|
|
275
|
+
items: { oneOf: generatedActionSchemas },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
required: ["decision", "actions"],
|
|
279
|
+
additionalProperties: false,
|
|
280
|
+
allOf: [
|
|
281
|
+
{
|
|
282
|
+
if: {
|
|
283
|
+
properties: { decision: { const: "act" } },
|
|
284
|
+
required: ["decision"],
|
|
285
|
+
},
|
|
286
|
+
then: { properties: { actions: { minItems: 1 } } },
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
if: {
|
|
290
|
+
properties: { decision: { const: "skip" } },
|
|
291
|
+
required: ["decision"],
|
|
292
|
+
},
|
|
293
|
+
then: { properties: { actions: { maxItems: 0 } } },
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
120
297
|
const decisionSchema = z
|
|
121
298
|
.object({
|
|
122
299
|
decision: z.enum(["skip", "act"]),
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AgentSpec, Observation, RunState } from "./types.js";
|
|
2
|
+
import { type DailyRiskBudget } from "./prompt.js";
|
|
3
|
+
export declare const DECISION_INPUT_MAX_BYTES: number;
|
|
4
|
+
export type DecisionInputPhase = "before_observation" | "observed" | "decision_input";
|
|
5
|
+
type Scalar = string | number | boolean | null;
|
|
6
|
+
type Row = Record<string, Scalar | string[] | Record<string, Scalar | string[]>>;
|
|
7
|
+
export interface DecisionInputRecord {
|
|
8
|
+
version: "coinrithm.decision-input.v1";
|
|
9
|
+
visibility: "private";
|
|
10
|
+
completeness: "partial";
|
|
11
|
+
phase: DecisionInputPhase;
|
|
12
|
+
outcome: "pending" | "returned" | "runtime_error";
|
|
13
|
+
runId: string | null;
|
|
14
|
+
decisionId: string | null;
|
|
15
|
+
configFingerprint: string | null;
|
|
16
|
+
observationFingerprint: string | null;
|
|
17
|
+
preThesisObservationFingerprint: string | null;
|
|
18
|
+
dailyRiskBudget: DailyRiskBudget | null;
|
|
19
|
+
guardState: Record<string, number | boolean | null>;
|
|
20
|
+
account: Record<string, number | boolean | string | null> | null;
|
|
21
|
+
lists: Record<string, Row[]>;
|
|
22
|
+
counts: Record<string, {
|
|
23
|
+
source: number;
|
|
24
|
+
retained: number;
|
|
25
|
+
omitted: number;
|
|
26
|
+
}>;
|
|
27
|
+
omissions: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface DecisionInputCapture {
|
|
30
|
+
phase: DecisionInputPhase;
|
|
31
|
+
runId: string;
|
|
32
|
+
decisionId: string;
|
|
33
|
+
spec: AgentSpec;
|
|
34
|
+
mergedProse: string;
|
|
35
|
+
state: RunState;
|
|
36
|
+
observation?: Observation;
|
|
37
|
+
observationFingerprint?: string;
|
|
38
|
+
preThesisObservationFingerprint?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function buildDecisionInputRecord(input: DecisionInputCapture): DecisionInputRecord;
|
|
41
|
+
export declare function unavailableDecisionInputRecord(): DecisionInputRecord;
|
|
42
|
+
/** Defense at the storage boundary. Reject rather than preserve an arbitrary
|
|
43
|
+
* extension field. Detached JSON copy also prevents post-check mutation. */
|
|
44
|
+
export declare function sanitizeDecisionInputRecord(value: unknown): DecisionInputRecord | undefined;
|
|
45
|
+
export {};
|