@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +37 -8
  3. package/dist/agent/act.js +19 -6
  4. package/dist/agent/capitalSizing.d.ts +32 -0
  5. package/dist/agent/capitalSizing.js +257 -0
  6. package/dist/agent/client.d.ts +2 -0
  7. package/dist/agent/client.js +4 -0
  8. package/dist/agent/decision.d.ts +392 -0
  9. package/dist/agent/decision.js +177 -0
  10. package/dist/agent/decisionProbe.d.ts +17 -0
  11. package/dist/agent/decisionProbe.js +70 -0
  12. package/dist/agent/decisionReceipt.d.ts +45 -0
  13. package/dist/agent/decisionReceipt.js +595 -0
  14. package/dist/agent/decisionValidator.d.ts +19 -2
  15. package/dist/agent/decisionValidator.js +97 -3
  16. package/dist/agent/engine.d.ts +5 -1
  17. package/dist/agent/engine.js +8 -1
  18. package/dist/agent/observe.js +194 -35
  19. package/dist/agent/pmContext.d.ts +13 -0
  20. package/dist/agent/pmContext.js +136 -0
  21. package/dist/agent/prompt.d.ts +14 -2
  22. package/dist/agent/prompt.js +232 -35
  23. package/dist/agent/providerCapabilities.d.ts +23 -0
  24. package/dist/agent/providerCapabilities.js +105 -0
  25. package/dist/agent/providers.d.ts +29 -1
  26. package/dist/agent/providers.js +159 -88
  27. package/dist/agent/resolve.d.ts +1 -1
  28. package/dist/agent/resolve.js +21 -1
  29. package/dist/agent/runner.d.ts +4 -1
  30. package/dist/agent/runner.js +418 -47
  31. package/dist/agent/scorecard.js +7 -1
  32. package/dist/agent/skill.js +23 -0
  33. package/dist/agent/skillValidator.d.ts +1 -0
  34. package/dist/agent/skillValidator.js +63 -0
  35. package/dist/agent/state.js +7 -1
  36. package/dist/agent/strictLint.js +20 -0
  37. package/dist/agent/thesis.d.ts +40 -0
  38. package/dist/agent/thesis.js +319 -0
  39. package/dist/agent/types.d.ts +151 -0
  40. package/dist/http.js +21 -0
  41. package/dist/tools.js +10 -10
  42. package/package.json +1 -1
@@ -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 {};