@coinrithm/mcp-trading 0.7.16 → 0.7.18

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 (48) hide show
  1. package/CHANGELOG.md +903 -811
  2. package/README.md +488 -415
  3. package/dist/agent/capitalSizing.d.ts +10 -1
  4. package/dist/agent/capitalSizing.js +28 -4
  5. package/dist/agent/client.d.ts +13 -0
  6. package/dist/agent/client.js +17 -6
  7. package/dist/agent/decision.d.ts +21 -16
  8. package/dist/agent/decision.js +7 -0
  9. package/dist/agent/decisionValidator.d.ts +1 -0
  10. package/dist/agent/decisionValidator.js +1 -1
  11. package/dist/agent/engine.d.ts +3 -1
  12. package/dist/agent/engine.js +3 -1
  13. package/dist/agent/futuresCloseOutcome.d.ts +19 -0
  14. package/dist/agent/futuresCloseOutcome.js +54 -0
  15. package/dist/agent/futuresEligibility.d.ts +13 -0
  16. package/dist/agent/futuresEligibility.js +43 -5
  17. package/dist/agent/observe.d.ts +2 -1
  18. package/dist/agent/observe.js +1 -1
  19. package/dist/agent/pmPaperCandidates.d.ts +17 -0
  20. package/dist/agent/pmPaperCandidates.js +125 -0
  21. package/dist/agent/pmPaperCycle.d.ts +16 -0
  22. package/dist/agent/pmPaperCycle.js +372 -0
  23. package/dist/agent/pmPaperDecision.d.ts +187 -0
  24. package/dist/agent/pmPaperDecision.js +101 -0
  25. package/dist/agent/pmPaperPolicy.d.ts +71 -0
  26. package/dist/agent/pmPaperPolicy.js +46 -0
  27. package/dist/agent/prompt.d.ts +12 -1
  28. package/dist/agent/prompt.js +90 -3
  29. package/dist/agent/providerCapabilities.d.ts +1 -0
  30. package/dist/agent/providerCapabilities.js +4 -1
  31. package/dist/agent/providers.d.ts +1 -0
  32. package/dist/agent/providers.js +9 -4
  33. package/dist/agent/runner.d.ts +9 -1
  34. package/dist/agent/runner.js +152 -24
  35. package/dist/agent/skill.js +9 -0
  36. package/dist/agent/skillValidator.js +23 -0
  37. package/dist/agent/strictLint.js +1 -0
  38. package/dist/agent/templates.js +30 -30
  39. package/dist/agent/types.d.ts +31 -0
  40. package/dist/agent/version.d.ts +1 -1
  41. package/dist/agent/version.js +1 -1
  42. package/dist/client.d.ts +14 -0
  43. package/dist/client.js +31 -1
  44. package/dist/http.js +1 -1
  45. package/dist/pmPaperV2.d.ts +81 -0
  46. package/dist/pmPaperV2.js +53 -0
  47. package/dist/tools.js +106 -14
  48. package/package.json +100 -98
@@ -0,0 +1,187 @@
1
+ import { z } from "zod";
2
+ import type { ActionsStringDiagnostic } from "./decision.js";
3
+ export declare const pmV2OpenActionSchema: z.ZodObject<Omit<{
4
+ source: z.ZodEnum<["kalshi", "polymarket"]>;
5
+ slug: z.ZodString;
6
+ outcomeExternalMarketId: z.ZodString;
7
+ side: z.ZodEnum<["yes", "no"]>;
8
+ maxCashBudget: z.ZodEffects<z.ZodString, string, string>;
9
+ idempotencyKey: z.ZodString;
10
+ minEntryProbabilityPct: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
11
+ thesis: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
12
+ } & {
13
+ forecastProbability: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
14
+ }, "idempotencyKey" | "minEntryProbabilityPct"> & {
15
+ type: z.ZodLiteral<"pm_v2_open">;
16
+ }, "strict", z.ZodTypeAny, {
17
+ source: "kalshi" | "polymarket";
18
+ type: "pm_v2_open";
19
+ slug: string;
20
+ outcomeExternalMarketId: string;
21
+ side: "yes" | "no";
22
+ maxCashBudget: string;
23
+ forecastProbability: string;
24
+ thesis?: string | null | undefined;
25
+ }, {
26
+ source: "kalshi" | "polymarket";
27
+ type: "pm_v2_open";
28
+ slug: string;
29
+ outcomeExternalMarketId: string;
30
+ side: "yes" | "no";
31
+ maxCashBudget: string;
32
+ forecastProbability: string;
33
+ thesis?: string | null | undefined;
34
+ }>;
35
+ export declare const pmV2CloseActionSchema: z.ZodObject<Omit<{
36
+ positionId: z.ZodNumber;
37
+ idempotencyKey: z.ZodString;
38
+ detail: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
39
+ }, "idempotencyKey"> & {
40
+ type: z.ZodLiteral<"pm_v2_close">;
41
+ }, "strict", z.ZodTypeAny, {
42
+ type: "pm_v2_close";
43
+ positionId: number;
44
+ detail?: string | null | undefined;
45
+ }, {
46
+ type: "pm_v2_close";
47
+ positionId: number;
48
+ detail?: string | null | undefined;
49
+ }>;
50
+ export declare const pmPaperDecisionSchema: z.ZodEffects<z.ZodObject<{
51
+ decision: z.ZodEnum<["act", "skip"]>;
52
+ reason: z.ZodOptional<z.ZodString>;
53
+ rationale: z.ZodOptional<z.ZodString>;
54
+ confidence: z.ZodOptional<z.ZodNumber>;
55
+ actions: z.ZodArray<z.ZodDiscriminatedUnion<"type", [z.ZodObject<Omit<{
56
+ source: z.ZodEnum<["kalshi", "polymarket"]>;
57
+ slug: z.ZodString;
58
+ outcomeExternalMarketId: z.ZodString;
59
+ side: z.ZodEnum<["yes", "no"]>;
60
+ maxCashBudget: z.ZodEffects<z.ZodString, string, string>;
61
+ idempotencyKey: z.ZodString;
62
+ minEntryProbabilityPct: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
63
+ thesis: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
64
+ } & {
65
+ forecastProbability: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
66
+ }, "idempotencyKey" | "minEntryProbabilityPct"> & {
67
+ type: z.ZodLiteral<"pm_v2_open">;
68
+ }, "strict", z.ZodTypeAny, {
69
+ source: "kalshi" | "polymarket";
70
+ type: "pm_v2_open";
71
+ slug: string;
72
+ outcomeExternalMarketId: string;
73
+ side: "yes" | "no";
74
+ maxCashBudget: string;
75
+ forecastProbability: string;
76
+ thesis?: string | null | undefined;
77
+ }, {
78
+ source: "kalshi" | "polymarket";
79
+ type: "pm_v2_open";
80
+ slug: string;
81
+ outcomeExternalMarketId: string;
82
+ side: "yes" | "no";
83
+ maxCashBudget: string;
84
+ forecastProbability: string;
85
+ thesis?: string | null | undefined;
86
+ }>, z.ZodObject<Omit<{
87
+ positionId: z.ZodNumber;
88
+ idempotencyKey: z.ZodString;
89
+ detail: z.ZodOptional<z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>>;
90
+ }, "idempotencyKey"> & {
91
+ type: z.ZodLiteral<"pm_v2_close">;
92
+ }, "strict", z.ZodTypeAny, {
93
+ type: "pm_v2_close";
94
+ positionId: number;
95
+ detail?: string | null | undefined;
96
+ }, {
97
+ type: "pm_v2_close";
98
+ positionId: number;
99
+ detail?: string | null | undefined;
100
+ }>]>, "many">;
101
+ }, "strict", z.ZodTypeAny, {
102
+ decision: "skip" | "act";
103
+ actions: ({
104
+ source: "kalshi" | "polymarket";
105
+ type: "pm_v2_open";
106
+ slug: string;
107
+ outcomeExternalMarketId: string;
108
+ side: "yes" | "no";
109
+ maxCashBudget: string;
110
+ forecastProbability: string;
111
+ thesis?: string | null | undefined;
112
+ } | {
113
+ type: "pm_v2_close";
114
+ positionId: number;
115
+ detail?: string | null | undefined;
116
+ })[];
117
+ confidence?: number | undefined;
118
+ reason?: string | undefined;
119
+ rationale?: string | undefined;
120
+ }, {
121
+ decision: "skip" | "act";
122
+ actions: ({
123
+ source: "kalshi" | "polymarket";
124
+ type: "pm_v2_open";
125
+ slug: string;
126
+ outcomeExternalMarketId: string;
127
+ side: "yes" | "no";
128
+ maxCashBudget: string;
129
+ forecastProbability: string;
130
+ thesis?: string | null | undefined;
131
+ } | {
132
+ type: "pm_v2_close";
133
+ positionId: number;
134
+ detail?: string | null | undefined;
135
+ })[];
136
+ confidence?: number | undefined;
137
+ reason?: string | undefined;
138
+ rationale?: string | undefined;
139
+ }>, {
140
+ decision: "skip" | "act";
141
+ actions: ({
142
+ source: "kalshi" | "polymarket";
143
+ type: "pm_v2_open";
144
+ slug: string;
145
+ outcomeExternalMarketId: string;
146
+ side: "yes" | "no";
147
+ maxCashBudget: string;
148
+ forecastProbability: string;
149
+ thesis?: string | null | undefined;
150
+ } | {
151
+ type: "pm_v2_close";
152
+ positionId: number;
153
+ detail?: string | null | undefined;
154
+ })[];
155
+ confidence?: number | undefined;
156
+ reason?: string | undefined;
157
+ rationale?: string | undefined;
158
+ }, {
159
+ decision: "skip" | "act";
160
+ actions: ({
161
+ source: "kalshi" | "polymarket";
162
+ type: "pm_v2_open";
163
+ slug: string;
164
+ outcomeExternalMarketId: string;
165
+ side: "yes" | "no";
166
+ maxCashBudget: string;
167
+ forecastProbability: string;
168
+ thesis?: string | null | undefined;
169
+ } | {
170
+ type: "pm_v2_close";
171
+ positionId: number;
172
+ detail?: string | null | undefined;
173
+ })[];
174
+ confidence?: number | undefined;
175
+ reason?: string | undefined;
176
+ rationale?: string | undefined;
177
+ }>;
178
+ export type PmPaperAction = z.infer<typeof pmV2OpenActionSchema> | z.infer<typeof pmV2CloseActionSchema>;
179
+ export declare function parsePmPaperDecision(text: string): {
180
+ ok: true;
181
+ decision: z.infer<typeof pmPaperDecisionSchema>;
182
+ } | {
183
+ ok: false;
184
+ error: string;
185
+ actionsStringDiagnostic?: ActionsStringDiagnostic;
186
+ };
187
+ export declare const PM_PAPER_DECISION_JSON_SCHEMA: Record<string, unknown>;
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+ import { pmPaperAgentOpenSchema, pmPaperCloseSchema } from "../pmPaperV2.js";
3
+ // forecastProbability is required: a house open without it fails locally
4
+ // instead of spending a write on the API's 422 forecast_required.
5
+ export const pmV2OpenActionSchema = pmPaperAgentOpenSchema
6
+ .omit({ idempotencyKey: true, minEntryProbabilityPct: true })
7
+ .extend({ type: z.literal("pm_v2_open") })
8
+ .strict();
9
+ export const pmV2CloseActionSchema = pmPaperCloseSchema
10
+ .omit({ idempotencyKey: true })
11
+ .extend({ type: z.literal("pm_v2_close") })
12
+ .strict();
13
+ export const pmPaperDecisionSchema = z
14
+ .object({
15
+ decision: z.enum(["act", "skip"]),
16
+ reason: z.string().max(280).optional(),
17
+ rationale: z.string().max(1000).optional(),
18
+ confidence: z.number().min(0).max(1).optional(),
19
+ actions: z
20
+ .array(z.discriminatedUnion("type", [
21
+ pmV2OpenActionSchema,
22
+ pmV2CloseActionSchema,
23
+ ]))
24
+ .max(1),
25
+ })
26
+ .strict()
27
+ .refine((value) => value.decision === "act"
28
+ ? value.actions.length === 1
29
+ : value.actions.length === 0);
30
+ export function parsePmPaperDecision(text) {
31
+ try {
32
+ const parsed = pmPaperDecisionSchema.safeParse(JSON.parse(text));
33
+ return parsed.success
34
+ ? { ok: true, decision: parsed.data }
35
+ : { ok: false, error: "invalid PM paper v2 decision" };
36
+ }
37
+ catch {
38
+ return { ok: false, error: "invalid PM paper v2 JSON" };
39
+ }
40
+ }
41
+ const decimal = {
42
+ type: "string",
43
+ pattern: "^(0|[1-9]\\d*)(\\.\\d{1,18})?$",
44
+ maxLength: 40,
45
+ };
46
+ export const PM_PAPER_DECISION_JSON_SCHEMA = {
47
+ type: "object",
48
+ additionalProperties: false,
49
+ required: ["decision", "actions"],
50
+ properties: {
51
+ decision: { type: "string", enum: ["act", "skip"] },
52
+ reason: { type: "string", maxLength: 280 },
53
+ rationale: { type: "string", maxLength: 1000 },
54
+ confidence: { type: "number", minimum: 0, maximum: 1 },
55
+ actions: {
56
+ type: "array",
57
+ maxItems: 1,
58
+ items: {
59
+ oneOf: [
60
+ {
61
+ type: "object",
62
+ additionalProperties: false,
63
+ required: [
64
+ "type",
65
+ "source",
66
+ "slug",
67
+ "outcomeExternalMarketId",
68
+ "side",
69
+ "maxCashBudget",
70
+ "forecastProbability",
71
+ ],
72
+ properties: {
73
+ type: { const: "pm_v2_open" },
74
+ source: { type: "string", enum: ["kalshi", "polymarket"] },
75
+ slug: { type: "string", minLength: 1, maxLength: 256 },
76
+ outcomeExternalMarketId: {
77
+ type: "string",
78
+ minLength: 1,
79
+ maxLength: 256,
80
+ },
81
+ side: { type: "string", enum: ["yes", "no"] },
82
+ maxCashBudget: decimal,
83
+ forecastProbability: decimal,
84
+ thesis: { type: ["string", "null"], maxLength: 280 },
85
+ },
86
+ },
87
+ {
88
+ type: "object",
89
+ additionalProperties: false,
90
+ required: ["type", "positionId"],
91
+ properties: {
92
+ type: { const: "pm_v2_close" },
93
+ positionId: { type: "integer", minimum: 1, maximum: 2147483647 },
94
+ detail: { type: ["string", "null"], maxLength: 1024 },
95
+ },
96
+ },
97
+ ],
98
+ },
99
+ },
100
+ },
101
+ };
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+ export declare const pmHouseIdentitySchema: z.ZodObject<{
3
+ userId: z.ZodNumber;
4
+ apiKeyId: z.ZodNumber;
5
+ walletId: z.ZodNumber;
6
+ houseAgentId: z.ZodNumber;
7
+ }, "strict", z.ZodTypeAny, {
8
+ walletId: number;
9
+ userId: number;
10
+ apiKeyId: number;
11
+ houseAgentId: number;
12
+ }, {
13
+ walletId: number;
14
+ userId: number;
15
+ apiKeyId: number;
16
+ houseAgentId: number;
17
+ }>;
18
+ export declare const pmHousePolicySchema: z.ZodObject<{
19
+ userId: z.ZodNumber;
20
+ apiKeyId: z.ZodNumber;
21
+ walletId: z.ZodNumber;
22
+ houseAgentId: z.ZodNumber;
23
+ } & {
24
+ version: z.ZodLiteral<"pm_paper_house_v2">;
25
+ entryEnabled: z.ZodDefault<z.ZodBoolean>;
26
+ maxCashBudgetPerEntry: z.ZodEffects<z.ZodString, string, string>;
27
+ maxOpenPositions: z.ZodNumber;
28
+ maxEntriesPerDay: z.ZodNumber;
29
+ maxModelCallsPerHour: z.ZodDefault<z.ZodNumber>;
30
+ maxDailyLoss: z.ZodEffects<z.ZodString, string, string>;
31
+ discoveryQuery: z.ZodString;
32
+ maxEndDays: z.ZodOptional<z.ZodNumber>;
33
+ }, "strict", z.ZodTypeAny, {
34
+ version: "pm_paper_house_v2";
35
+ walletId: number;
36
+ userId: number;
37
+ apiKeyId: number;
38
+ houseAgentId: number;
39
+ entryEnabled: boolean;
40
+ maxCashBudgetPerEntry: string;
41
+ maxOpenPositions: number;
42
+ maxEntriesPerDay: number;
43
+ maxModelCallsPerHour: number;
44
+ maxDailyLoss: string;
45
+ discoveryQuery: string;
46
+ maxEndDays?: number | undefined;
47
+ }, {
48
+ version: "pm_paper_house_v2";
49
+ walletId: number;
50
+ userId: number;
51
+ apiKeyId: number;
52
+ houseAgentId: number;
53
+ maxCashBudgetPerEntry: string;
54
+ maxOpenPositions: number;
55
+ maxEntriesPerDay: number;
56
+ maxDailyLoss: string;
57
+ discoveryQuery: string;
58
+ entryEnabled?: boolean | undefined;
59
+ maxModelCallsPerHour?: number | undefined;
60
+ maxEndDays?: number | undefined;
61
+ }>;
62
+ export type PmHouseIdentity = z.infer<typeof pmHouseIdentitySchema>;
63
+ export type PmHousePolicy = z.infer<typeof pmHousePolicySchema>;
64
+ export declare const samePmHouse: (a: PmHouseIdentity, b: PmHouseIdentity) => boolean;
65
+ export declare function readPmHousePolicies(raw: string | undefined): readonly PmHousePolicy[];
66
+ export declare function selectPmHousePolicy(agent: {
67
+ id: number;
68
+ ownerUserId?: number | null;
69
+ isHouse?: boolean;
70
+ }, policies?: readonly PmHousePolicy[]): PmHousePolicy | undefined;
71
+ export declare function pmCashUnits(value: string): bigint;
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ import { pmPaperOpenSchema } from "../pmPaperV2.js";
3
+ const id = z.number().int().positive().max(2147483647);
4
+ export const pmHouseIdentitySchema = z
5
+ .object({ userId: id, apiKeyId: id, walletId: id, houseAgentId: id })
6
+ .strict();
7
+ export const pmHousePolicySchema = pmHouseIdentitySchema
8
+ .extend({
9
+ version: z.literal("pm_paper_house_v2"),
10
+ entryEnabled: z.boolean().default(false),
11
+ maxCashBudgetPerEntry: pmPaperOpenSchema.shape.maxCashBudget,
12
+ maxOpenPositions: z.number().int().min(1).max(20),
13
+ maxEntriesPerDay: z.number().int().min(1).max(100),
14
+ maxModelCallsPerHour: z.number().int().min(1).max(30).default(12),
15
+ maxDailyLoss: pmPaperOpenSchema.shape.maxCashBudget,
16
+ discoveryQuery: z.string().min(1).max(100),
17
+ maxEndDays: z.number().int().min(1).max(30).optional(),
18
+ })
19
+ .strict();
20
+ export const samePmHouse = (a, b) => a.userId === b.userId &&
21
+ a.apiKeyId === b.apiKeyId &&
22
+ a.walletId === b.walletId &&
23
+ a.houseAgentId === b.houseAgentId;
24
+ export function readPmHousePolicies(raw) {
25
+ if (raw === undefined || raw === "")
26
+ return [];
27
+ const policies = z.array(pmHousePolicySchema).max(10).parse(JSON.parse(raw));
28
+ if (new Set(policies.map((p) => p.houseAgentId)).size !== policies.length ||
29
+ new Set(policies.map((p) => p.apiKeyId)).size !== policies.length ||
30
+ new Set(policies.map((p) => p.walletId)).size !== policies.length)
31
+ throw new Error("Duplicate PM house policy identity");
32
+ return policies;
33
+ }
34
+ export function selectPmHousePolicy(agent, policies = []) {
35
+ if (agent.isHouse !== true)
36
+ return undefined;
37
+ return policies.find((p) => p.houseAgentId === agent.id && p.userId === agent.ownerUserId);
38
+ }
39
+ export function pmCashUnits(value) {
40
+ if (!/^-?(0|[1-9]\d*)(\.\d{1,18})?$/.test(value) || value.length > 41)
41
+ throw new Error("Invalid exact paper cash");
42
+ const negative = value.startsWith("-");
43
+ const [whole, fraction = ""] = (negative ? value.slice(1) : value).split(".");
44
+ const units = BigInt(whole) * 10n ** 18n + BigInt(fraction.padEnd(18, "0"));
45
+ return negative ? -units : units;
46
+ }
@@ -1,4 +1,4 @@
1
- import { AgentSpec, Observation, PmResolution, RunState, type ObjectivePrimary } from "./types.js";
1
+ import { AgentSpec, Observation, PmResolution, RunState, LastRejection, type ObjectivePrimary, type OpenPosition } from "./types.js";
2
2
  import type { DecisionActionExclusion } from "./providerCapabilities.js";
3
3
  export interface DailyRiskBudget {
4
4
  version: "coinrithm.daily-risk-budget.v1";
@@ -25,6 +25,16 @@ export declare function buildSystemPrompt(spec: AgentSpec, mergedProse: string,
25
25
  includeForecast?: boolean;
26
26
  excludeActionTypes?: readonly DecisionActionExclusion[];
27
27
  }): string;
28
+ /**
29
+ * Per-position stop-loss side of the mark for futures_set_sltp. Live
30
+ * 2026-10-08: 473 stop_loss_not_above/below_mark refusals in 24 h, nearly all
31
+ * agents moving a short's stop to its entry price or trailing it when the
32
+ * mark was already at or past that level. States only the observed-mark
33
+ * constraint the validator applies (pos.markPrice, TRIGGER_PRICE_EPS); the
34
+ * take-profit side, liquidation and every other check stay authoritative.
35
+ * Prices print unrounded so the text never contradicts the validator.
36
+ */
37
+ export declare function stopBoundLines(positions: readonly OpenPosition[]): string[];
28
38
  export declare function buildUserPrompt(obs: Observation, journal?: Array<{
29
39
  at: string;
30
40
  did: string;
@@ -35,4 +45,5 @@ export declare function buildUserPrompt(obs: Observation, journal?: Array<{
35
45
  futuresCapacity?: FuturesCapacity;
36
46
  compactTables?: boolean;
37
47
  excludeActionTypes?: readonly DecisionActionExclusion[];
48
+ lastRejections?: readonly LastRejection[];
38
49
  }): string;
@@ -9,6 +9,9 @@ import { describeUniverse, scansUniverse } from "./universe.js";
9
9
  import { pmQualityOf, pmDecisionSupportOf } from "./pmContext.js";
10
10
  import { usesCapitalSizing } from "./capitalSizing.js";
11
11
  import { serializePromptObservation } from "./promptTables.js";
12
+ import { TRIGGER_PRICE_EPS } from "./decisionValidator.js";
13
+ import { futuresEntryChoices } from "./futuresEligibility.js";
14
+ import { baseSymbol } from "./setups.js";
12
15
  // The runner has already called rollDay. Use the same counter as validation,
13
16
  // including conservative legacy-state migration; never infer it from writes,
14
17
  // model calls, or the number of positions that remain open.
@@ -158,6 +161,11 @@ opts = {}) {
158
161
  ...(usesCapitalSizing(spec)
159
162
  ? [
160
163
  `- Opt-in paper capital policy ${spec.capitalSizing?.version ?? "invalid"}: the runner REPLACES proposed futures margins and PM stakes using current owned-book evidence, stops and fixed policy limits; it does not treat your confidence or the nominal starting grant as a sizing instruction. Choose the market, direction and meaningful protection; invalid policy, quoted costs, shared allocation and cash reserve can still reject an entry.`,
164
+ ...(spec.capitalSizing?.highConviction
165
+ ? [
166
+ `- HIGH-CONVICTION exception: a futures_open with confidence >= ${spec.capitalSizing.highConviction.minConfidence} is sized with the larger limits (ticket up to ${spec.capitalSizing.highConviction.perTicketCapitalPct}% of equity, stop risk up to ${spec.capitalSizing.highConviction.futuresRiskPct}%). Rate a setup that high ONLY when it is your best opportunity in days: its stop-out costs far more. Everything else stays at the base size.`,
167
+ ]
168
+ : []),
161
169
  ]
162
170
  : []),
163
171
  ...(hasFutures
@@ -404,6 +412,39 @@ opts = {}) {
404
412
  `Each cycle, look at your OPEN positions FIRST, not just new entries. A position that is working is your best opportunity: once it moves your way, move the stop to breakeven and then TRAIL it behind the move with futures_set_sltp so a winner keeps running instead of being cut early${futuresOpenWithheld ? "" : " — and you may ADD to a confirming winner (scale in, never beyond your caps)"}. A position that is clearly wrong (its \`thesis.status\` reads invalidated, the level broke, the catalyst resolved against you) is cut cleanly instead of nursed; a position whose thesis is intact is held. Riding one good trade beats opening ten fresh ones.`,
405
413
  ].join("\n");
406
414
  }
415
+ /**
416
+ * Per-position stop-loss side of the mark for futures_set_sltp. Live
417
+ * 2026-10-08: 473 stop_loss_not_above/below_mark refusals in 24 h, nearly all
418
+ * agents moving a short's stop to its entry price or trailing it when the
419
+ * mark was already at or past that level. States only the observed-mark
420
+ * constraint the validator applies (pos.markPrice, TRIGGER_PRICE_EPS); the
421
+ * take-profit side, liquidation and every other check stay authoritative.
422
+ * Prices print unrounded so the text never contradicts the validator.
423
+ */
424
+ export function stopBoundLines(positions) {
425
+ const out = [];
426
+ for (const p of positions) {
427
+ const mark = p.markPrice;
428
+ const side = (p.side ?? "").toLowerCase();
429
+ if (typeof mark !== "number" || !Number.isFinite(mark) || mark <= 0)
430
+ continue;
431
+ if (side !== "long" && side !== "short")
432
+ continue;
433
+ const isShort = side === "short";
434
+ let line = `- pos#${p.id} ${side} ${p.symbol ?? ""}: observed mark ${String(mark)}; a new stopLossPrice must be ${isShort ? "ABOVE" : "BELOW"} it.`;
435
+ const entry = p.entryPrice;
436
+ if (typeof entry === "number" && Number.isFinite(entry) && entry > 0) {
437
+ const entrySideOk = isShort
438
+ ? entry > mark + TRIGGER_PRICE_EPS
439
+ : entry < mark - TRIGGER_PRICE_EPS;
440
+ line += entrySideOk
441
+ ? ` A stop at the entry price ${String(entry)} is on the allowed side of the mark.`
442
+ : ` A stop at the entry price ${String(entry)} is on the wrong side of the mark and would be refused; keep the current stop, or close the position if the thesis broke.`;
443
+ }
444
+ out.push(line);
445
+ }
446
+ return out;
447
+ }
407
448
  export function buildUserPrompt(obs, journal, opts = {}) {
408
449
  // Default to every venue for backwards-compatible direct callers and probes.
409
450
  // The runner always supplies the real spec, so disabled venue instructions and
@@ -413,9 +454,36 @@ export function buildUserPrompt(obs, journal, opts = {}) {
413
454
  const futuresOpenWithheld = hasFutures && opts.excludeActionTypes?.includes("futures_open") === true;
414
455
  const hasSpot = venues.includes("spot");
415
456
  const hasPm = venues.includes("pm");
457
+ const entryChoices = hasFutures ? futuresEntryChoices(obs) : undefined;
458
+ const blockedFutures = new Set(entryChoices?.blocked.map((entry) => baseSymbol(entry.symbol)));
459
+ // Structural signals remain in the observation/receipt. Only the model's
460
+ // action shortlist changes: an unsupported NEW futures entry is not an
461
+ // actionable futures setup. Spot and held-position context remain visible.
462
+ const promptSetups = (obs.setups ?? []).flatMap((setup) => {
463
+ if (!blockedFutures.has(baseSymbol(setup.symbol)))
464
+ return [setup];
465
+ return hasSpot
466
+ ? [
467
+ {
468
+ ...setup,
469
+ note: `${setup.note} [NEW FUTURES ENTRY BLOCKED: consider spot only if independently allowed, or skip]`,
470
+ },
471
+ ]
472
+ : [];
473
+ });
474
+ const noFuturesEntryCandidates = entryChoices !== undefined &&
475
+ entryChoices.blocked.length > 0 &&
476
+ entryChoices.candidates.length === 0;
416
477
  const lines = [
417
478
  "Decide for THIS cycle using only the observation below (data available now — no look-ahead).",
418
479
  ];
480
+ if (entryChoices && entryChoices.blocked.length > 0) {
481
+ lines.push(`NEW FUTURES ENTRY AVAILABILITY: ${entryChoices.blocked.map((entry) => `${entry.symbol} (${entry.code})`).join(", ")} cannot be used for a NEW futures_open this cycle. A strong move or flagged structure does not override this block.`, "If futures_open is otherwise available, choose a new futures entry only from futuresEntryChoices.candidates below; this list checks perpetual-reference availability only, not other caps, universe boundaries, direction or quote/execution rules. reference=unknown is NOT eligibility: it still needs validation at quote time. held_position means the existing add/manage exception, under the unchanged add rules; it is not a fresh entry recommendation.", `Blocked coins remain in watch for market context${hasSpot ? " and otherwise-valid spot decisions" : ""}. Existing futures_close, futures_set_sltp and valid adds remain governed by their existing rules. ${hasSpot ? "Blocked setup notes apply only to NEW futures entries; do not force a spot trade to avoid skipping." : "Their unheld entry setups are omitted from this prompt's shortlist; the underlying market evidence remains visible."}`, ...(noFuturesEntryCandidates
482
+ ? [
483
+ "There are NO new futures entry candidates this cycle. Manage/protect existing positions where valid, or skip. Unavailable futures entries are a valid reason to skip; do not invent an alternative trade.",
484
+ ]
485
+ : []));
486
+ }
419
487
  if (opts.capitalSizing) {
420
488
  lines.push("capitalSizingPolicy is the opt-in paper sizing policy (percent fields use percentage points). capitalBook is captured owned-book collateral plus marked spot, reduced only by negative futures/PM marks on its walletId; positive open-position gains are excluded, so this is NOT complete marked equity. Positions on other walletIds remain visible for management but their collateral, marks and close proceeds do not fund this book. Missing/unavailable capitalBook means no new entries; otherwise-valid closes, protection, cancellations and spot sells remain available.");
421
489
  }
@@ -472,7 +540,7 @@ export function buildUserPrompt(obs, journal, opts = {}) {
472
540
  if (typeof minRewardRisk === "number" &&
473
541
  Number.isFinite(minRewardRisk) &&
474
542
  minRewardRisk >= 1) {
475
- lines.push(`REWARD:RISK FLOOR ${minRewardRisk} (your capitalSizing.minRewardRisk, enforced with fees; an open below it is rejected as capital_quote_reward_risk_too_low): the take-profit's distance from entry, minus the entry and exit fees, must be at least ${minRewardRisk}x the stop's distance from entry plus those fees. A target exactly ${minRewardRisk}x the stop distance FAILS once fees are counted, so leave room. If the setup cannot offer such a target, do not open it.`);
543
+ lines.push(`REWARD:RISK FLOOR ${minRewardRisk} (your capitalSizing.minRewardRisk, enforced with fees; a correct-side take-profit inside it is widened by the runner to the floor, never narrowed, and an open that still cannot meet it is rejected as capital_quote_reward_risk_too_low): the take-profit's distance from entry, minus the entry and exit fees, must be at least ${minRewardRisk}x the stop's distance from entry plus those fees. A target exactly ${minRewardRisk}x the stop distance FAILS once fees are counted, so leave room. If the setup cannot offer such a target, do not open it.`);
476
544
  }
477
545
  if (held.length > 0) {
478
546
  const list = held.join(", ");
@@ -489,7 +557,9 @@ export function buildUserPrompt(obs, journal, opts = {}) {
489
557
  if ((obs.openPositions?.length ?? 0) === 0 &&
490
558
  (!hasPm || (obs.pmPositions?.length ?? 0) === 0)) {
491
559
  const openingActions = [
492
- ...(hasFutures && !futuresOpenWithheld ? ["futures_open"] : []),
560
+ ...(hasFutures && !futuresOpenWithheld && !noFuturesEntryCandidates
561
+ ? ["futures_open"]
562
+ : []),
493
563
  ...(hasSpot ? ["spot_order"] : []),
494
564
  ...(hasPm ? ["pm_open"] : []),
495
565
  ];
@@ -505,6 +575,16 @@ export function buildUserPrompt(obs, journal, opts = {}) {
505
575
  if (journal && journal.length > 0) {
506
576
  lines.push("", "## Your recent moves (memory, newest last) — manage these with continuity; do NOT churn by re-opening an idea you just acted on:", ...journal.slice(-6).map((j) => `- ${j.did}`));
507
577
  }
578
+ // Persisted state is read back defensively: only well-formed entries render.
579
+ const rejections = (Array.isArray(opts.lastRejections) ? opts.lastRejections : [])
580
+ .filter((r) => r &&
581
+ typeof r.action === "string" &&
582
+ typeof r.code === "string" &&
583
+ r.code.length > 0)
584
+ .slice(0, 5);
585
+ if (rejections.length > 0) {
586
+ lines.push("", "## Your last decision's REJECTED actions (refused, nothing happened) - do NOT resubmit them unchanged; fix the cause shown or choose differently:", ...rejections.map((r) => `- ${r.action.replace(/\s+/g, " ").slice(0, 200)}: ${r.code.replace(/\s+/g, " ").slice(0, 80)}${typeof r.reason === "string" && r.reason ? ` - ${r.reason.replace(/\s+/g, " ").slice(0, 200)}` : ""}`));
587
+ }
508
588
  // Slice 2: name the positions whose stated thesis broke this cycle. On a live
509
589
  // run the runner has already closed the futures ones (they are no longer in
510
590
  // openPositions); whatever is listed here is for the MODEL to act on.
@@ -521,6 +601,10 @@ export function buildUserPrompt(obs, journal, opts = {}) {
521
601
  if (brokenTheses.length > 0) {
522
602
  lines.push("", "## Positions whose thesis is INVALIDATED this cycle", ...brokenTheses.map((b) => `- ${b}`));
523
603
  }
604
+ const stopBounds = stopBoundLines(obs.openPositions);
605
+ if (stopBounds.length > 0) {
606
+ lines.push("", "## Stop-loss side of the mark for futures_set_sltp (a stop on the wrong side is refused; take-profit side, liquidation and the other trigger checks still apply)", ...stopBounds);
607
+ }
524
608
  if (obs.openPositions.some((p) => p.fundingPaidMusd !== undefined ||
525
609
  p.fundingAppliedThrough !== undefined)) {
526
610
  lines.push("Futures funding shown in the position is already reflected in margin and balances; do not deduct it again when judging available capital or PnL.");
@@ -590,7 +674,10 @@ export function buildUserPrompt(obs, journal, opts = {}) {
590
674
  ? { pmCalibration: obs.pmCalibration }
591
675
  : {}),
592
676
  watch: obs.watch,
593
- setups: obs.setups,
677
+ ...(entryChoices && entryChoices.blocked.length > 0
678
+ ? { futuresEntryChoices: entryChoices }
679
+ : {}),
680
+ setups: promptSetups,
594
681
  news: obs.news,
595
682
  universeMovers: obs.universeMovers,
596
683
  whaleContext: obs.whaleContext,
@@ -29,4 +29,5 @@ export declare function buildChatBody(shape: ChatShape, args: {
29
29
  maxTokens: number;
30
30
  temperature?: number;
31
31
  excludeActionTypes?: readonly DecisionActionExclusion[];
32
+ decisionContract?: "pm_paper_v2";
32
33
  }): Record<string, unknown>;
@@ -1,3 +1,4 @@
1
+ import { PM_PAPER_DECISION_JSON_SCHEMA } from "./pmPaperDecision.js";
1
2
  import { DECISION_JSON_SCHEMA } from "./decision.js";
2
3
  export const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
3
4
  export const DECISION_TOOL_NAME = "submit_trading_decision";
@@ -110,7 +111,9 @@ export function buildChatBody(shape, args) {
110
111
  ? `${shape.systemHint}\n\n${args.system}`
111
112
  : args.system;
112
113
  const jsonSchema = shape.jsonSchema
113
- ? restrictDecisionSchema(shape.jsonSchema, args.excludeActionTypes)
114
+ ? args.decisionContract === "pm_paper_v2"
115
+ ? PM_PAPER_DECISION_JSON_SCHEMA
116
+ : restrictDecisionSchema(shape.jsonSchema, args.excludeActionTypes)
114
117
  : undefined;
115
118
  return {
116
119
  model: args.model,
@@ -18,6 +18,7 @@ export interface DecideInput {
18
18
  maxTokens?: number;
19
19
  timeoutMs?: number;
20
20
  excludeActionTypes?: readonly DecisionActionExclusion[];
21
+ decisionContract?: "pm_paper_v2";
21
22
  }
22
23
  export interface DecideRouteAttempt {
23
24
  provider: string;