@coinrithm/mcp-trading 0.7.13 → 0.7.15
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 +692 -573
- package/README.md +400 -364
- package/dist/agent/act.d.ts +7 -2
- package/dist/agent/act.js +10 -3
- package/dist/agent/cli.d.ts +1 -0
- package/dist/agent/cli.js +25 -4
- package/dist/agent/client.d.ts +13 -0
- package/dist/agent/client.js +107 -4
- package/dist/agent/decisionReceipt.d.ts +5 -1
- package/dist/agent/decisionReceipt.js +95 -3
- package/dist/agent/decisionValidator.js +74 -0
- package/dist/agent/definitionSnapshot.d.ts +22 -0
- package/dist/agent/definitionSnapshot.js +28 -0
- package/dist/agent/engine.d.ts +1 -0
- package/dist/agent/engine.js +1 -0
- package/dist/agent/futuresEligibility.d.ts +36 -0
- package/dist/agent/futuresEligibility.js +90 -0
- package/dist/agent/mergeRules.d.ts +1 -1
- package/dist/agent/mergeRules.js +17 -2
- package/dist/agent/observe.d.ts +9 -1
- package/dist/agent/observe.js +507 -45
- package/dist/agent/pmContext.d.ts +10 -1
- package/dist/agent/pmContext.js +163 -1
- package/dist/agent/prompt.d.ts +14 -0
- package/dist/agent/prompt.js +138 -27
- package/dist/agent/providerCapabilities.d.ts +3 -0
- package/dist/agent/providerCapabilities.js +34 -4
- package/dist/agent/providers.d.ts +3 -0
- package/dist/agent/providers.js +12 -2
- package/dist/agent/resolve.js +8 -1
- package/dist/agent/runner.js +140 -18
- package/dist/agent/skill.js +9 -0
- package/dist/agent/skillValidator.js +11 -2
- package/dist/agent/strictLint.js +1 -0
- package/dist/agent/templates.js +30 -30
- package/dist/agent/types.d.ts +138 -1
- package/dist/agent/types.js +4 -0
- package/dist/client.d.ts +11 -2
- package/dist/client.js +22 -10
- package/dist/http.d.ts +1 -0
- package/dist/http.js +17 -2
- package/dist/httpCompletion.d.ts +1 -1
- package/dist/httpCompletion.js +1 -0
- package/dist/index.js +2 -1
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +16 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +150 -66
- package/package.json +97 -97
|
@@ -54,18 +54,48 @@ export function chatShapeFor(provider, model, baseUrl) {
|
|
|
54
54
|
minProbeCompletionTokens: 1024,
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
// Transport-only restriction: drop the excluded variants from the action oneOf
|
|
58
|
+
// a route decodes against. parseDecision keeps validating the FULL contract, so
|
|
59
|
+
// a route that does not enforce a schema can still propose the action and the
|
|
60
|
+
// validator rejects it. Never returns an empty oneOf.
|
|
61
|
+
export function restrictDecisionSchema(schema, exclude) {
|
|
62
|
+
if (!exclude?.length)
|
|
63
|
+
return schema;
|
|
64
|
+
const properties = schema.properties;
|
|
65
|
+
const actions = properties?.actions;
|
|
66
|
+
const variants = actions?.items?.oneOf;
|
|
67
|
+
if (!properties || !actions || !Array.isArray(variants))
|
|
68
|
+
return schema;
|
|
69
|
+
const kept = variants.filter((variant) => {
|
|
70
|
+
const type = variant
|
|
71
|
+
.properties?.type?.const;
|
|
72
|
+
return !exclude.includes(type);
|
|
73
|
+
});
|
|
74
|
+
if (kept.length === variants.length || kept.length === 0)
|
|
75
|
+
return schema;
|
|
76
|
+
return {
|
|
77
|
+
...schema,
|
|
78
|
+
properties: {
|
|
79
|
+
...properties,
|
|
80
|
+
actions: { ...actions, items: { ...actions.items, oneOf: kept } },
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
57
84
|
/** Build the chat-completions body for a route from its capability shape. */
|
|
58
85
|
export function buildChatBody(shape, args) {
|
|
59
86
|
const system = shape.systemHint
|
|
60
87
|
? `${shape.systemHint}\n\n${args.system}`
|
|
61
88
|
: args.system;
|
|
89
|
+
const jsonSchema = shape.jsonSchema
|
|
90
|
+
? restrictDecisionSchema(shape.jsonSchema, args.excludeActionTypes)
|
|
91
|
+
: undefined;
|
|
62
92
|
return {
|
|
63
93
|
model: args.model,
|
|
64
94
|
...(shape.allowsTemperature
|
|
65
95
|
? { temperature: args.temperature ?? 0.2 }
|
|
66
96
|
: {}),
|
|
67
97
|
[shape.tokenParam]: args.maxTokens,
|
|
68
|
-
...(
|
|
98
|
+
...(jsonSchema && shape.jsonSchemaTransport === "tool_call"
|
|
69
99
|
? {
|
|
70
100
|
tools: [
|
|
71
101
|
{
|
|
@@ -73,7 +103,7 @@ export function buildChatBody(shape, args) {
|
|
|
73
103
|
function: {
|
|
74
104
|
name: DECISION_TOOL_NAME,
|
|
75
105
|
description: "Submit the complete CoinRithm paper-trading decision for this cycle.",
|
|
76
|
-
parameters:
|
|
106
|
+
parameters: jsonSchema,
|
|
77
107
|
},
|
|
78
108
|
},
|
|
79
109
|
],
|
|
@@ -85,12 +115,12 @@ export function buildChatBody(shape, args) {
|
|
|
85
115
|
: {}),
|
|
86
116
|
...(shape.jsonResponseFormat && shape.jsonSchemaTransport !== "tool_call"
|
|
87
117
|
? {
|
|
88
|
-
response_format:
|
|
118
|
+
response_format: jsonSchema
|
|
89
119
|
? {
|
|
90
120
|
type: "json_schema",
|
|
91
121
|
json_schema: {
|
|
92
122
|
name: "coinrithm_trading_decision",
|
|
93
|
-
schema:
|
|
123
|
+
schema: jsonSchema,
|
|
94
124
|
},
|
|
95
125
|
}
|
|
96
126
|
: { type: "json_object" },
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { AgentSpec, ProviderName } from "./types.js";
|
|
2
|
+
import { type DecisionActionExclusion } from "./providerCapabilities.js";
|
|
2
3
|
export interface DecideInput {
|
|
3
4
|
system: string;
|
|
4
5
|
user: string;
|
|
5
6
|
maxTokens?: number;
|
|
6
7
|
timeoutMs?: number;
|
|
8
|
+
excludeActionTypes?: readonly DecisionActionExclusion[];
|
|
7
9
|
}
|
|
8
10
|
export interface DecideRouteAttempt {
|
|
9
11
|
provider: string;
|
|
@@ -26,6 +28,7 @@ export interface DecideRouteMeta {
|
|
|
26
28
|
export type DecideResult = {
|
|
27
29
|
ok: true;
|
|
28
30
|
text: string;
|
|
31
|
+
responseSource?: "tool_call" | "content_fallback" | "content";
|
|
29
32
|
usage?: {
|
|
30
33
|
promptTokens: number;
|
|
31
34
|
completionTokens: number;
|
package/dist/agent/providers.js
CHANGED
|
@@ -193,7 +193,7 @@ class AnthropicProvider {
|
|
|
193
193
|
}
|
|
194
194
|
: undefined;
|
|
195
195
|
return text
|
|
196
|
-
? { ok: true, text, usage }
|
|
196
|
+
? { ok: true, text, usage, responseSource: "content" }
|
|
197
197
|
: { ok: false, error: "anthropic returned empty content" };
|
|
198
198
|
});
|
|
199
199
|
}
|
|
@@ -246,6 +246,7 @@ class OpenAiCompatProvider {
|
|
|
246
246
|
system: input.system,
|
|
247
247
|
user: input.user,
|
|
248
248
|
maxTokens: input.maxTokens ?? 1024,
|
|
249
|
+
excludeActionTypes: input.excludeActionTypes,
|
|
249
250
|
})),
|
|
250
251
|
});
|
|
251
252
|
if (!res.ok) {
|
|
@@ -273,7 +274,16 @@ class OpenAiCompatProvider {
|
|
|
273
274
|
}
|
|
274
275
|
: undefined;
|
|
275
276
|
return text
|
|
276
|
-
? {
|
|
277
|
+
? {
|
|
278
|
+
ok: true,
|
|
279
|
+
text,
|
|
280
|
+
usage,
|
|
281
|
+
responseSource: decisionArguments != null
|
|
282
|
+
? "tool_call"
|
|
283
|
+
: shape.jsonSchemaTransport === "tool_call"
|
|
284
|
+
? "content_fallback"
|
|
285
|
+
: "content",
|
|
286
|
+
}
|
|
277
287
|
: { ok: false, error: "provider returned empty content" };
|
|
278
288
|
});
|
|
279
289
|
}
|
package/dist/agent/resolve.js
CHANGED
|
@@ -47,7 +47,14 @@ const CONFIG_BLOCKS = [
|
|
|
47
47
|
const JOURNAL_MAX_LINES = 200;
|
|
48
48
|
const JOURNAL_MAX_BYTES = 8_000;
|
|
49
49
|
// Optional prose files (markdown the LLM reads), in assembly order.
|
|
50
|
-
const PROSE_FILES = [
|
|
50
|
+
const PROSE_FILES = [
|
|
51
|
+
"character/thesis.md",
|
|
52
|
+
"character/persona.md",
|
|
53
|
+
"character/entries.md",
|
|
54
|
+
"character/exits.md",
|
|
55
|
+
"character/sizing.md",
|
|
56
|
+
"character/research.md",
|
|
57
|
+
];
|
|
51
58
|
// Prose files carry an OPTIONAL YAML frontmatter block (type/title/description/
|
|
52
59
|
// tags) that is authoring metadata, not doctrine — the model gains nothing from
|
|
53
60
|
// `tags: [agent, persona, mean-reversion]`. It was being merged verbatim into
|
package/dist/agent/runner.js
CHANGED
|
@@ -9,10 +9,11 @@ import { usesCapitalSizing, prepareCapitalAction, validateCapitalAction, capital
|
|
|
9
9
|
import { decideMechanical } from "./mechanical.js";
|
|
10
10
|
import { evaluateGate, noteLlmCall, estimateCostUsd } from "./gate.js";
|
|
11
11
|
import { baseSymbol, scanSetups } from "./setups.js";
|
|
12
|
+
import { futuresEntryPreflight } from "./futuresEligibility.js";
|
|
12
13
|
import { reconcileObservation } from "./reconcileObservation.js";
|
|
13
14
|
import { createOpportunityReporter } from "./opportunityReporter.js";
|
|
14
|
-
import { observe } from "./observe.js";
|
|
15
|
-
import { buildDailyRiskBudget, buildSystemPrompt, buildUserPrompt, } from "./prompt.js";
|
|
15
|
+
import { enrichWhaleContext, observe } from "./observe.js";
|
|
16
|
+
import { buildDailyRiskBudget, buildFuturesCapacity, buildSystemPrompt, buildUserPrompt, } from "./prompt.js";
|
|
16
17
|
import { parseDecision } from "./decision.js";
|
|
17
18
|
import { validateAction, isRiskIncreasingAction, } from "./decisionValidator.js";
|
|
18
19
|
import { resolvePmRef } from "./resolvePm.js";
|
|
@@ -425,10 +426,29 @@ async function runCycleCore(deps, capture) {
|
|
|
425
426
|
const provenance = buildRunnerProvenance(spec);
|
|
426
427
|
state.cyclesRun += 1;
|
|
427
428
|
rollDay(state);
|
|
429
|
+
// Old state did not identify which route failed. Carrying that count into
|
|
430
|
+
// a replacement model can request a hold on its very first error.
|
|
431
|
+
if (state.consecutivePermanentModelErrors &&
|
|
432
|
+
!state.permanentModelErrorRoute) {
|
|
433
|
+
state.consecutivePermanentModelErrors = 0;
|
|
434
|
+
}
|
|
428
435
|
const runId = state.runId;
|
|
429
436
|
const decisionId = makeDecisionId(state.cyclesRun);
|
|
430
437
|
const captureBase = { runId, decisionId, spec, mergedProse, state };
|
|
431
438
|
capture({ ...captureBase, phase: "before_observation" });
|
|
439
|
+
// These exits happen before both provider invocation and action execution.
|
|
440
|
+
// Record that known absence explicitly; missing metering means unknown to
|
|
441
|
+
// audit consumers. Do not default later failures to zero: a call/write may
|
|
442
|
+
// already have occurred by then.
|
|
443
|
+
const preDecisionMeter = {
|
|
444
|
+
llmCallMade: false,
|
|
445
|
+
decisionType: "skip",
|
|
446
|
+
tokensIn: 0,
|
|
447
|
+
tokensOut: 0,
|
|
448
|
+
estimatedCostUsd: 0,
|
|
449
|
+
writeAttempted: 0,
|
|
450
|
+
writeAccepted: 0,
|
|
451
|
+
};
|
|
432
452
|
// Kill-switch pre-check: a disabled agent never observes, decides, or acts.
|
|
433
453
|
const tripped = checkKillSwitch(spec, state);
|
|
434
454
|
if (tripped) {
|
|
@@ -442,6 +462,7 @@ async function runCycleCore(deps, capture) {
|
|
|
442
462
|
disabled: true,
|
|
443
463
|
disabledReason: tripped,
|
|
444
464
|
live,
|
|
465
|
+
...preDecisionMeter,
|
|
445
466
|
};
|
|
446
467
|
}
|
|
447
468
|
// Establish durable run identity before the first possible write. A process
|
|
@@ -505,6 +526,7 @@ async function runCycleCore(deps, capture) {
|
|
|
505
526
|
disabled: true,
|
|
506
527
|
disabledReason: state.disabledReason,
|
|
507
528
|
live,
|
|
529
|
+
...preDecisionMeter,
|
|
508
530
|
...observationReceipt,
|
|
509
531
|
};
|
|
510
532
|
}
|
|
@@ -529,6 +551,7 @@ async function runCycleCore(deps, capture) {
|
|
|
529
551
|
disabled: true,
|
|
530
552
|
disabledReason: state.disabledReason,
|
|
531
553
|
live,
|
|
554
|
+
...preDecisionMeter,
|
|
532
555
|
...observationReceipt,
|
|
533
556
|
};
|
|
534
557
|
}
|
|
@@ -540,6 +563,7 @@ async function runCycleCore(deps, capture) {
|
|
|
540
563
|
skipReason: obs.skip,
|
|
541
564
|
planned: [],
|
|
542
565
|
live,
|
|
566
|
+
...preDecisionMeter,
|
|
543
567
|
...observationReceipt,
|
|
544
568
|
};
|
|
545
569
|
}
|
|
@@ -581,7 +605,7 @@ async function runCycleCore(deps, capture) {
|
|
|
581
605
|
...makeTrace(runId, decisionId, spec, undefined, `Thesis exit: ${why}`),
|
|
582
606
|
...observationReceipt,
|
|
583
607
|
};
|
|
584
|
-
const r = await executeAction(client, action, observation, trace, idem, provenance);
|
|
608
|
+
const r = await executeAction(client, action, observation, trace, idem, provenance, { pmMinEntryProbabilityPct: spec.risk.pmMinEntryProbabilityPct });
|
|
585
609
|
exitPlanned.push({
|
|
586
610
|
action,
|
|
587
611
|
accepted: true,
|
|
@@ -618,15 +642,15 @@ async function runCycleCore(deps, capture) {
|
|
|
618
642
|
// so it touches NO kill-switch counter; it just records the cheap cycle.
|
|
619
643
|
const policy = spec.triggerPolicy ?? DEFAULT_TRIGGER_POLICY;
|
|
620
644
|
const gate = evaluateGate(observation, state, policy, nowMs);
|
|
621
|
-
capture({
|
|
622
|
-
...captureBase,
|
|
623
|
-
phase: "decision_input",
|
|
624
|
-
observation,
|
|
625
|
-
observationFingerprint: observationReceipt.observationHash,
|
|
626
|
-
preThesisObservationFingerprint,
|
|
627
|
-
});
|
|
628
645
|
const providerName = spec.model?.provider ?? "nvidia";
|
|
629
646
|
if (!gate.fire) {
|
|
647
|
+
capture({
|
|
648
|
+
...captureBase,
|
|
649
|
+
phase: "decision_input",
|
|
650
|
+
observation,
|
|
651
|
+
observationFingerprint: observationReceipt.observationHash,
|
|
652
|
+
preThesisObservationFingerprint,
|
|
653
|
+
});
|
|
630
654
|
saveState(stateFile, state);
|
|
631
655
|
log(`gate: skip (${gate.reason})`);
|
|
632
656
|
return {
|
|
@@ -645,6 +669,20 @@ async function runCycleCore(deps, capture) {
|
|
|
645
669
|
...observationReceipt,
|
|
646
670
|
};
|
|
647
671
|
}
|
|
672
|
+
if (providerName !== "mechanical" &&
|
|
673
|
+
spec.capabilities.includes("whale_context") &&
|
|
674
|
+
spec.venues.includes("pm")) {
|
|
675
|
+
observation.whaleContext = await enrichWhaleContext(client, observation);
|
|
676
|
+
observationReceipt = buildObservationReceipt(observation);
|
|
677
|
+
Object.assign(baseTrace, observationReceipt);
|
|
678
|
+
}
|
|
679
|
+
capture({
|
|
680
|
+
...captureBase,
|
|
681
|
+
phase: "decision_input",
|
|
682
|
+
observation,
|
|
683
|
+
observationFingerprint: observationReceipt.observationHash,
|
|
684
|
+
preThesisObservationFingerprint,
|
|
685
|
+
});
|
|
648
686
|
// DECIDE. Two paths share the same downstream validate+act loop:
|
|
649
687
|
// • mechanical benchmark agents (provider "mechanical") compute the decision
|
|
650
688
|
// deterministically from the observation — no prompt, no model call, no
|
|
@@ -672,19 +710,43 @@ async function runCycleCore(deps, capture) {
|
|
|
672
710
|
};
|
|
673
711
|
}
|
|
674
712
|
else {
|
|
713
|
+
const futuresCapacity = spec.venues.includes("futures")
|
|
714
|
+
? buildFuturesCapacity(spec, observation)
|
|
715
|
+
: undefined;
|
|
716
|
+
// One exclusion drives both the textual action menu and transport schema.
|
|
717
|
+
// It is a proposal hint only; parsing and validation stay unchanged.
|
|
718
|
+
const capacitySpent = futuresCapacity !== undefined &&
|
|
719
|
+
(futuresCapacity.slotsLeft === 0 ||
|
|
720
|
+
futuresCapacity.marginHeadroomMusd <= 0);
|
|
721
|
+
const actionAvailability = capacitySpent
|
|
722
|
+
? { excludeActionTypes: ["futures_open"] }
|
|
723
|
+
: {};
|
|
675
724
|
const system = buildSystemPrompt(spec, mergedProse, {
|
|
676
725
|
includeForecast: forecastEnabled,
|
|
726
|
+
...actionAvailability,
|
|
677
727
|
});
|
|
678
728
|
const user = buildUserPrompt(observation, state.journal, {
|
|
679
729
|
venues: spec.venues,
|
|
680
730
|
dailyRiskBudget: buildDailyRiskBudget(spec, state),
|
|
681
731
|
...(usesCapitalSizing(spec) ? { capitalSizing: spec.capitalSizing } : {}),
|
|
732
|
+
...(futuresCapacity ? { futuresCapacity } : {}),
|
|
733
|
+
...actionAvailability,
|
|
682
734
|
});
|
|
683
735
|
const tokensInEst = Math.round((system.length + user.length) / 4);
|
|
684
736
|
// Prompt-size + trigger visibility in the live terminal.
|
|
685
737
|
log(`prompt ~${tokensInEst} tok ` +
|
|
686
738
|
`(pm ${observation.pmMarkets.length}, trades ${observation.newClosedTrades.length}, watch ${observation.watch.length}, setups ${observation.setups.length}, triggers ${gate.codes.join("|") || "none"})`);
|
|
687
|
-
|
|
739
|
+
// With either futures cap spent, a schema-enforcing route is not offered
|
|
740
|
+
// futures_open at all. Other routes can still propose it; the validator
|
|
741
|
+
// rejects it there exactly as before.
|
|
742
|
+
if (capacitySpent) {
|
|
743
|
+
log(`futures capacity spent (slotsLeft ${futuresCapacity.slotsLeft}, marginHeadroomMusd ${futuresCapacity.marginHeadroomMusd}): futures_open withheld from schema-enforcing routes`);
|
|
744
|
+
}
|
|
745
|
+
const res = await provider.decide({
|
|
746
|
+
system,
|
|
747
|
+
user,
|
|
748
|
+
...actionAvailability,
|
|
749
|
+
});
|
|
688
750
|
const route = res.route;
|
|
689
751
|
const actualCallMade = route
|
|
690
752
|
? route.attempts.some((attempt) => attempt.outcome !== "deferred")
|
|
@@ -764,13 +826,34 @@ async function runCycleCore(deps, capture) {
|
|
|
764
826
|
// for what deserves them: revoked credentials, drawdown, kill-switch,
|
|
765
827
|
// user action. Transient errors reset the permanent streak.
|
|
766
828
|
if (isPermanentModelError(res.error)) {
|
|
829
|
+
// The router retains the last non-capacity failure even when a later
|
|
830
|
+
// fallback returns 429. Its effective route still describes the last
|
|
831
|
+
// actual call for metering, not necessarily the error returned here.
|
|
832
|
+
const failureAttempt = route?.attempts
|
|
833
|
+
.slice()
|
|
834
|
+
.reverse()
|
|
835
|
+
.find((attempt) => attempt.outcome === "failed" &&
|
|
836
|
+
attempt.failureClass !== "capacity" &&
|
|
837
|
+
attempt.error === res.error);
|
|
838
|
+
const failureRoute = {
|
|
839
|
+
provider: failureAttempt?.provider ??
|
|
840
|
+
route?.effectiveProvider ??
|
|
841
|
+
providerName,
|
|
842
|
+
model: failureAttempt?.model ??
|
|
843
|
+
route?.effectiveModel ??
|
|
844
|
+
spec.model?.name ??
|
|
845
|
+
"unknown",
|
|
846
|
+
};
|
|
847
|
+
const previousRoute = state.permanentModelErrorRoute;
|
|
848
|
+
const sameRoute = previousRoute?.provider === failureRoute.provider &&
|
|
849
|
+
previousRoute?.model === failureRoute.model;
|
|
767
850
|
state.consecutivePermanentModelErrors =
|
|
768
|
-
(state.consecutivePermanentModelErrors ?? 0) + 1;
|
|
851
|
+
(sameRoute ? (state.consecutivePermanentModelErrors ?? 0) : 0) + 1;
|
|
852
|
+
state.permanentModelErrorRoute = failureRoute;
|
|
769
853
|
if (state.consecutivePermanentModelErrors >=
|
|
770
854
|
PERMANENT_MODEL_ERROR_THRESHOLD) {
|
|
771
855
|
const hold = {
|
|
772
|
-
|
|
773
|
-
model: route?.effectiveModel ?? spec.model?.name ?? "unknown",
|
|
856
|
+
...failureRoute,
|
|
774
857
|
error: res.error.slice(0, 200),
|
|
775
858
|
};
|
|
776
859
|
saveState(stateFile, state);
|
|
@@ -792,6 +875,7 @@ async function runCycleCore(deps, capture) {
|
|
|
792
875
|
}
|
|
793
876
|
else {
|
|
794
877
|
state.consecutivePermanentModelErrors = 0;
|
|
878
|
+
delete state.permanentModelErrorRoute;
|
|
795
879
|
}
|
|
796
880
|
saveState(stateFile, state);
|
|
797
881
|
log(`model error: ${res.error}`);
|
|
@@ -808,7 +892,27 @@ async function runCycleCore(deps, capture) {
|
|
|
808
892
|
...observationReceipt,
|
|
809
893
|
};
|
|
810
894
|
}
|
|
895
|
+
// A returned model response proves availability even if its decision JSON
|
|
896
|
+
// is invalid. Output validation keeps its own failure counter below.
|
|
897
|
+
state.consecutivePermanentModelErrors = 0;
|
|
898
|
+
delete state.permanentModelErrorRoute;
|
|
811
899
|
const parsed = parseDecision(res.text);
|
|
900
|
+
if (capacitySpent) {
|
|
901
|
+
// Persist only route provenance and a parsed action-presence bit in the
|
|
902
|
+
// existing cycle log. Unknown adapters and malformed decisions must not
|
|
903
|
+
// be miscounted as tool compliance; raw output stays unretained.
|
|
904
|
+
const responseSource = res.responseSource === "tool_call" ||
|
|
905
|
+
res.responseSource === "content_fallback" ||
|
|
906
|
+
res.responseSource === "content"
|
|
907
|
+
? res.responseSource
|
|
908
|
+
: "unknown";
|
|
909
|
+
log(`capacity-response ${JSON.stringify({
|
|
910
|
+
responseSource,
|
|
911
|
+
withheldActionProposed: parsed.ok
|
|
912
|
+
? parsed.decision.actions.some((action) => action.type === "futures_open")
|
|
913
|
+
: null,
|
|
914
|
+
})}`);
|
|
915
|
+
}
|
|
812
916
|
if (!parsed.ok) {
|
|
813
917
|
state.consecutiveModelFailures += 1;
|
|
814
918
|
saveState(stateFile, state);
|
|
@@ -830,7 +934,6 @@ async function runCycleCore(deps, capture) {
|
|
|
830
934
|
};
|
|
831
935
|
}
|
|
832
936
|
state.consecutiveModelFailures = 0;
|
|
833
|
-
state.consecutivePermanentModelErrors = 0;
|
|
834
937
|
decision = parsed.decision;
|
|
835
938
|
}
|
|
836
939
|
// Reasoning captured for the Arena terminal (keystone transparency): the
|
|
@@ -935,7 +1038,7 @@ async function runCycleCore(deps, capture) {
|
|
|
935
1038
|
log(`reject pm_open: duplicate_intent (hold PM ${pm.slug})`);
|
|
936
1039
|
continue;
|
|
937
1040
|
}
|
|
938
|
-
//
|
|
1041
|
+
// Market-aware forecast submission (HOUSE_AGENT_FORECAST_ENABLED, default ON).
|
|
939
1042
|
// Attach the model's OWN probability the backed side wins — clamped to [1,99],
|
|
940
1043
|
// OMITTED when absent/unparseable (a bad forecast never blocks the trade), and
|
|
941
1044
|
// NEVER defaulted to the market price. Flag OFF strips any forecast so the open
|
|
@@ -1027,7 +1130,26 @@ async function runCycleCore(deps, capture) {
|
|
|
1027
1130
|
}
|
|
1028
1131
|
}
|
|
1029
1132
|
}
|
|
1030
|
-
|
|
1133
|
+
// Supported-reference preflight (backend-v2 #106, J-74): skip a NEW futures
|
|
1134
|
+
// open on a coin the observation EXPLICITLY reports has no supported (or only
|
|
1135
|
+
// a stale) perpetual reference, before spending a quote the server gate would
|
|
1136
|
+
// refuse. Unknown eligibility, adds/management on a held coin, spot and PM
|
|
1137
|
+
// pass; the server gate stays authoritative at quote/open time.
|
|
1138
|
+
const preflight = futuresEntryPreflight(action, observation);
|
|
1139
|
+
if (preflight) {
|
|
1140
|
+
planned.push({
|
|
1141
|
+
action,
|
|
1142
|
+
accepted: false,
|
|
1143
|
+
code: preflight.code,
|
|
1144
|
+
...(capitalSizing ? { capitalSizing } : {}),
|
|
1145
|
+
reason: preflight.reason,
|
|
1146
|
+
});
|
|
1147
|
+
log(`reject ${action.type}: ${preflight.code}`);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const quote = await fetchQuote(client, action, observation, baseTrace, {
|
|
1151
|
+
pmMinEntryProbabilityPct: spec.risk.pmMinEntryProbabilityPct,
|
|
1152
|
+
});
|
|
1031
1153
|
// Early PM skip: the quote's openBlocked preview tells us a pm/open right now
|
|
1032
1154
|
// would be rejected 422 by the open-time quality gate (distinct from the
|
|
1033
1155
|
// eligible/blockReasons SHAPE gate the validator checks). Bail here with a clear
|
|
@@ -1150,7 +1272,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1150
1272
|
rationaleForAction(action, decision.rationale, meta.rationaleSummary, decision.actions.length)),
|
|
1151
1273
|
...observationReceipt,
|
|
1152
1274
|
};
|
|
1153
|
-
const r = await executeAction(client, action, observation, trace, idem, provenance);
|
|
1275
|
+
const r = await executeAction(client, action, observation, trace, idem, provenance, { pmMinEntryProbabilityPct: spec.risk.pmMinEntryProbabilityPct });
|
|
1154
1276
|
planned.push({
|
|
1155
1277
|
action,
|
|
1156
1278
|
accepted: true,
|
package/dist/agent/skill.js
CHANGED
|
@@ -109,6 +109,15 @@ export function buildSpec(raw) {
|
|
|
109
109
|
entryPredicates: risk.entryPredicates,
|
|
110
110
|
}
|
|
111
111
|
: {}),
|
|
112
|
+
// Optional PM entry floor: an absent key stays absent (no floor); an
|
|
113
|
+
// explicit value is carried AS WRITTEN, malformed or not, so hosted
|
|
114
|
+
// validation fails closed instead of a typo silently meaning "no floor"
|
|
115
|
+
// (the same rule entryPredicates follow).
|
|
116
|
+
...(risk.pmMinEntryProbabilityPct !== undefined
|
|
117
|
+
? {
|
|
118
|
+
pmMinEntryProbabilityPct: risk.pmMinEntryProbabilityPct,
|
|
119
|
+
}
|
|
120
|
+
: {}),
|
|
112
121
|
maxLeverage: num(risk.maxLeverage, 1),
|
|
113
122
|
perTradeMarginMusd: num(risk.perTradeMarginMusd, 0),
|
|
114
123
|
maxConcurrentPositions: num(risk.maxConcurrentPositions, 0),
|
|
@@ -108,6 +108,13 @@ export function validateSkill(parsed, mode = "self-host") {
|
|
|
108
108
|
add("skill_risk_sl", "risk.requireStopLoss must be true or false");
|
|
109
109
|
if (!Array.isArray(r.watchlist) || r.watchlist.length === 0)
|
|
110
110
|
add("skill_risk_watchlist", "risk.watchlist must be a non-empty list of symbols");
|
|
111
|
+
// Optional PM entry floor: absent means no floor; present must be a finite
|
|
112
|
+
// number of probability points in [0, 100].
|
|
113
|
+
if (r.pmMinEntryProbabilityPct !== undefined) {
|
|
114
|
+
const f = r.pmMinEntryProbabilityPct;
|
|
115
|
+
if (typeof f !== "number" || !Number.isFinite(f) || f < 0 || f > 100)
|
|
116
|
+
add("skill_risk_pm_entry_floor", "risk.pmMinEntryProbabilityPct must be a number between 0 and 100 (points; omit for no floor)");
|
|
117
|
+
}
|
|
111
118
|
// Fail-closed on the side restriction: a typo ("shorts_only") must never
|
|
112
119
|
// silently mean "unrestricted" — that is exactly how a prose-only
|
|
113
120
|
// constraint failed live on 2026-08-24.
|
|
@@ -171,8 +178,10 @@ export function validateSkill(parsed, mode = "self-host") {
|
|
|
171
178
|
add("skill_limits_trades", "limits.maxTradesPerDay must be a number >= 0 (0 = unlimited daily trades)");
|
|
172
179
|
if (!isPosNum(l.maxWritesPerCycle))
|
|
173
180
|
add("skill_limits_writes", "limits.maxWritesPerCycle must be a positive number");
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
// 0 = no daily loss cap: the runner only enforces a positive value and
|
|
182
|
+
// the hosted API already accepts 0..50,000, so hosted validation must too.
|
|
183
|
+
if (!isNonNegNum(l.maxDailyLossMusd))
|
|
184
|
+
add("skill_limits_loss", "limits.maxDailyLossMusd must be a number >= 0 (0 = no daily loss cap)");
|
|
176
185
|
if (!isPosNum(l.maxOpenMarginMusd))
|
|
177
186
|
add("skill_limits_open", "limits.maxOpenMarginMusd must be a positive number");
|
|
178
187
|
}
|
package/dist/agent/strictLint.js
CHANGED
package/dist/agent/templates.js
CHANGED
|
@@ -50,37 +50,37 @@ const PRESETS = {
|
|
|
50
50
|
modelFail: 5,
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
|
-
const THESIS_BODY = `# Momentum Futures — strategy
|
|
54
|
-
|
|
55
|
-
You operate a CoinRithm **paper-trading** futures account (50,000 virtual mUSD).
|
|
56
|
-
Everything here is simulated; it is not financial advice and never touches real
|
|
57
|
-
money. Edit this prose freely (any language) — it is your agent's borders.
|
|
58
|
-
|
|
59
|
-
## Each cycle
|
|
60
|
-
|
|
61
|
-
1. Ground yourself: read your portfolio and open positions first. Never assume
|
|
62
|
-
balances or what is already open.
|
|
63
|
-
2. Scan the watchlist. A candidate is a coin whose short and medium momentum
|
|
64
|
-
agree (both up, or both down) and is not already an open position.
|
|
65
|
-
3. Pick the strongest candidate and commit when the read is clear — even a
|
|
66
|
-
moderate-confidence one — sized small with a stop. Skip only when the signals
|
|
67
|
-
contradict or the data is stale; a quiet tape where your edge is still real
|
|
68
|
-
is an act, not a skip.
|
|
69
|
-
4. Quote before you open. Read the liquidation price and confirm it is sane. If
|
|
70
|
-
the quote is not eligible, relay the reason and stop.
|
|
71
|
-
5. Open small and protected: enter in the trend direction and set a stop-loss at
|
|
72
|
-
open. Place the take-profit a touch wider than the stop.
|
|
73
|
-
6. Stay in sync: poll your trades for any stop / take-profit / liquidation that
|
|
74
|
-
fired while you were not looking, and react to what actually happened.
|
|
75
|
-
|
|
76
|
-
The hard caps (leverage, margin, watchlist) live in the config blocks above and
|
|
53
|
+
const THESIS_BODY = `# Momentum Futures — strategy
|
|
54
|
+
|
|
55
|
+
You operate a CoinRithm **paper-trading** futures account (50,000 virtual mUSD).
|
|
56
|
+
Everything here is simulated; it is not financial advice and never touches real
|
|
57
|
+
money. Edit this prose freely (any language) — it is your agent's borders.
|
|
58
|
+
|
|
59
|
+
## Each cycle
|
|
60
|
+
|
|
61
|
+
1. Ground yourself: read your portfolio and open positions first. Never assume
|
|
62
|
+
balances or what is already open.
|
|
63
|
+
2. Scan the watchlist. A candidate is a coin whose short and medium momentum
|
|
64
|
+
agree (both up, or both down) and is not already an open position.
|
|
65
|
+
3. Pick the strongest candidate and commit when the read is clear — even a
|
|
66
|
+
moderate-confidence one — sized small with a stop. Skip only when the signals
|
|
67
|
+
contradict or the data is stale; a quiet tape where your edge is still real
|
|
68
|
+
is an act, not a skip.
|
|
69
|
+
4. Quote before you open. Read the liquidation price and confirm it is sane. If
|
|
70
|
+
the quote is not eligible, relay the reason and stop.
|
|
71
|
+
5. Open small and protected: enter in the trend direction and set a stop-loss at
|
|
72
|
+
open. Place the take-profit a touch wider than the stop.
|
|
73
|
+
6. Stay in sync: poll your trades for any stop / take-profit / liquidation that
|
|
74
|
+
fired while you were not looking, and react to what actually happened.
|
|
75
|
+
|
|
76
|
+
The hard caps (leverage, margin, watchlist) live in the config blocks above and
|
|
77
77
|
are enforced by the runner — change them there, not in this prose.`;
|
|
78
|
-
const PERSONA_STUB = `# Persona
|
|
79
|
-
|
|
80
|
-
Decisive and in character. Acts on a clear read — even a moderate-confidence one
|
|
81
|
-
— sized small and protected with a stop, and skips only when the read is
|
|
82
|
-
contradictory or the data is stale. States its reasoning plainly in its own
|
|
83
|
-
voice, and never frames paper results as real-money advice.
|
|
78
|
+
const PERSONA_STUB = `# Persona
|
|
79
|
+
|
|
80
|
+
Decisive and in character. Acts on a clear read — even a moderate-confidence one
|
|
81
|
+
— sized small and protected with a stop, and skips only when the read is
|
|
82
|
+
contradictory or the data is stale. States its reasoning plainly in its own
|
|
83
|
+
voice, and never frames paper results as real-money advice.
|
|
84
84
|
`;
|
|
85
85
|
export function buildAgentObject(name, preset) {
|
|
86
86
|
const p = PRESETS[preset];
|