@coinrithm/mcp-trading 0.7.9 → 0.7.10
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 +30 -4
- package/README.md +10 -3
- package/dist/agent/cli.js +11 -1
- package/dist/agent/configurationWarnings.d.ts +1 -0
- package/dist/agent/configurationWarnings.js +21 -0
- package/dist/agent/decisionValidator.js +4 -0
- package/dist/agent/engine.d.ts +1 -1
- package/dist/agent/engine.js +1 -1
- package/dist/agent/entryPredicates.d.ts +10 -0
- package/dist/agent/entryPredicates.js +67 -0
- package/dist/agent/opportunityReporter.d.ts +16 -0
- package/dist/agent/opportunityReporter.js +38 -0
- package/dist/agent/prompt.js +5 -0
- package/dist/agent/reconcileObservation.d.ts +2 -0
- package/dist/agent/reconcileObservation.js +33 -0
- package/dist/agent/runner.js +23 -70
- package/dist/agent/skill.js +6 -0
- package/dist/agent/skillValidator.js +3 -0
- package/dist/agent/strictLint.js +1 -0
- package/dist/agent/templates.js +30 -30
- package/dist/agent/types.d.ts +1 -0
- package/dist/agent/util.js +3 -3
- package/package.json +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,10 +5,36 @@ ships two binaries — `coinrithm-mcp` (the MCP server) and `coinrithm-agent` (t
|
|
|
5
5
|
self-host agent runner) — versioned together. The CoinRithm **API contract** is
|
|
6
6
|
versioned separately (see `openapi.yaml` `info.version`, currently `1.7.0`).
|
|
7
7
|
|
|
8
|
-
## 0.7.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
## 0.7.10 — 2026-09-15
|
|
9
|
+
|
|
10
|
+
Source changes following the published 0.7.9 release. The TypeScript SDK remains
|
|
11
|
+
0.3.1 and Python remains 1.8.1; their runtime source is unchanged.
|
|
12
|
+
|
|
13
|
+
- Persist file-backed run identity before execution so a first-cycle process
|
|
14
|
+
crash cannot discard the idempotency identity. Transport uncertainty is still
|
|
15
|
+
not automatically replayed or recorded as a completed trade.
|
|
16
|
+
- Fix runner startup on Node 18: use the imported Node crypto API instead of
|
|
17
|
+
depending on a global crypto object. The installed-package matrix reproduced
|
|
18
|
+
this failure on Linux, Windows and macOS.
|
|
19
|
+
- Add the supported `@coinrithm/mcp-trading/engine` entry point, preserving
|
|
20
|
+
existing deep imports, and separate observation accounting and opportunity
|
|
21
|
+
reporting from cycle ordering.
|
|
22
|
+
- Add opt-in, machine-checked crypto return predicates and visible warnings for
|
|
23
|
+
inactive/reserved configuration. Existing agents are not automatically opted in.
|
|
24
|
+
- Serialize scheduler migrations in a bounded transaction; add an offline
|
|
25
|
+
credential-rotation helper and interruption/recovery rehearsal.
|
|
26
|
+
- Pin workflow actions and verify release-tool checksums. Add installed-package
|
|
27
|
+
compatibility and restart smoke checks across operating systems and runtimes.
|
|
28
|
+
|
|
29
|
+
Source/CI checks, registry publication and production deployment are separate.
|
|
30
|
+
This entry does not claim that 0.7.10 is published or deployed.
|
|
31
|
+
|
|
32
|
+
## 0.7.9 - 2026-09-15
|
|
33
|
+
|
|
34
|
+
Published on npm and verified on 2026-09-15: the registry archive matches the
|
|
35
|
+
reviewed artifact and passes fresh-install checks. See the
|
|
36
|
+
[combined release notes](https://github.com/CoinRithm/coinrithm-agent-trading/releases/tag/mcp-trading-v0.7.9)
|
|
37
|
+
for source provenance, verification and community acknowledgments.
|
|
12
38
|
|
|
13
39
|
Public market-data fidelity and runner reliability release. No MCP tool was
|
|
14
40
|
renamed or removed, and the API **contract stays 1.7.0**.
|
package/README.md
CHANGED
|
@@ -42,10 +42,12 @@ This package ships two binaries:
|
|
|
42
42
|
|
|
43
43
|
> **Paper trading only** — virtual funds (50,000 mUSD). Not financial advice.
|
|
44
44
|
|
|
45
|
-
## Version 0.7.
|
|
45
|
+
## Version 0.7.10
|
|
46
46
|
|
|
47
|
-
This version
|
|
48
|
-
|
|
47
|
+
This source version adds restart identity persistence, a supported engine
|
|
48
|
+
import, opt-in entry predicates and configuration warnings. It also fixes
|
|
49
|
+
runner startup on Node 18 by importing the Node crypto API explicitly. See
|
|
50
|
+
[CHANGELOG.md](./CHANGELOG.md). Check `npm view @coinrithm/mcp-trading
|
|
49
51
|
version` for the latest published version. Hosted deployments and npm releases
|
|
50
52
|
are separate.
|
|
51
53
|
|
|
@@ -53,6 +55,11 @@ Runner API operations have a 30-second total deadline, including response
|
|
|
53
55
|
bodies and 429 retry waits. Timeout and cancellation results remain unconfirmed;
|
|
54
56
|
the client does not automatically replay an uncertain trading write.
|
|
55
57
|
|
|
58
|
+
Embedding the runner? Import from `@coinrithm/mcp-trading/engine` for the
|
|
59
|
+
supported engine and state helpers. Existing `dist/agent/engine.js` imports
|
|
60
|
+
remain compatible. See the [entry conditions and engine guide](https://github.com/CoinRithm/coinrithm-agent-trading/blob/main/docs/agent-runner.md#binding-entry-conditions-and-strategy-prose)
|
|
61
|
+
for the exact opt-in policy and persistence contract.
|
|
62
|
+
|
|
56
63
|
## Quick start
|
|
57
64
|
|
|
58
65
|
```bash
|
package/dist/agent/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { configurationWarnings } from "./configurationWarnings.js";
|
|
1
2
|
// coinrithm-agent — the public scaffolder/inspector CLI.
|
|
2
3
|
//
|
|
3
4
|
// Authors, validates, ejects, locks, and inspects agent DEFINITIONS. It does
|
|
@@ -101,6 +102,7 @@ export function cmdValidate(path, mode = "self-host") {
|
|
|
101
102
|
const spec = buildSpec(raw);
|
|
102
103
|
const lint = [...strictLint(raw), ...checkCapabilityDrift(resolved, spec)];
|
|
103
104
|
const v = validateSkill({ spec, body: resolved.mergedProse, raw }, mode);
|
|
105
|
+
const warnings = configurationWarnings(raw);
|
|
104
106
|
// Hosted-only: the managed deploy/edit API caps the merged strategy prose at
|
|
105
107
|
// HOSTED_PROSE_MAX_CHARS and REVERTS the save when it is exceeded, so a
|
|
106
108
|
// bundle that resolves and lints perfectly can still be undeployable through
|
|
@@ -129,10 +131,16 @@ export function cmdValidate(path, mode = "self-host") {
|
|
|
129
131
|
}
|
|
130
132
|
for (const i of v.issues)
|
|
131
133
|
lines.push(`✗ ${i.code}: ${i.reason}`);
|
|
134
|
+
lines.push(...warnings.map((warning) => `⚠ ${warning}`));
|
|
132
135
|
lines.push(...pinWarnings(path));
|
|
133
136
|
const ok = v.valid && (!lintFatal || lint.length === 0);
|
|
134
137
|
lines.unshift(ok ? `✓ valid (${mode})` : `✗ invalid (${mode})`);
|
|
135
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
ok,
|
|
140
|
+
code: ok ? 0 : 1,
|
|
141
|
+
lines,
|
|
142
|
+
data: { lint, validation: v, warnings },
|
|
143
|
+
};
|
|
136
144
|
}
|
|
137
145
|
export function cmdLock(path) {
|
|
138
146
|
const v = cmdValidate(path, "self-host");
|
|
@@ -215,6 +223,7 @@ export function cmdInspect(path, json = false) {
|
|
|
215
223
|
];
|
|
216
224
|
const v = validateSkill({ spec, body: resolved.mergedProse, raw: resolved.rawFrontmatter }, "self-host");
|
|
217
225
|
const output = {
|
|
226
|
+
warnings: configurationWarnings(resolved.rawFrontmatter),
|
|
218
227
|
resolvedConfig: resolved.rawFrontmatter,
|
|
219
228
|
provenance: resolved.provenance,
|
|
220
229
|
contentHashes: resolved.contentHashes,
|
|
@@ -236,6 +245,7 @@ export function cmdInspect(path, json = false) {
|
|
|
236
245
|
`risk: maxLeverage=${spec.risk.maxLeverage} perTradeMargin=${spec.risk.perTradeMarginMusd} requireStopLoss=${spec.risk.requireStopLoss}`,
|
|
237
246
|
`sources: ${Object.keys(resolved.contentHashes).length} file(s)`,
|
|
238
247
|
`validation: ${v.valid ? "valid" : "INVALID"}${lint.length ? ` (+${lint.length} lint note(s))` : ""}`,
|
|
248
|
+
...output.warnings.map((warning) => `⚠ ${warning}`),
|
|
239
249
|
];
|
|
240
250
|
return { ok: v.valid, code: 0, lines, data: output };
|
|
241
251
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function configurationWarnings(raw: Record<string, unknown>): string[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Advisory only: these fields remain loadable for backwards compatibility.
|
|
2
|
+
export function configurationWarnings(raw) {
|
|
3
|
+
const warnings = [];
|
|
4
|
+
if (Array.isArray(raw.capabilities) &&
|
|
5
|
+
raw.capabilities.includes("websearch")) {
|
|
6
|
+
warnings.push("capabilities.websearch is reserved: the runner does not perform web searches");
|
|
7
|
+
}
|
|
8
|
+
if (raw.abstention && typeof raw.abstention === "object") {
|
|
9
|
+
for (const key of [
|
|
10
|
+
"onStaleData",
|
|
11
|
+
"onWeakSignal",
|
|
12
|
+
"onMissingQuote",
|
|
13
|
+
"onInsufficientBalance",
|
|
14
|
+
]) {
|
|
15
|
+
if (Object.prototype.hasOwnProperty.call(raw.abstention, key)) {
|
|
16
|
+
warnings.push(`abstention.${key} is inactive: changing it does not change execution; freshness, quote and balance checks remain mandatory`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return warnings;
|
|
21
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// come from the spec (not the observation or the model), a prompt-injection in
|
|
4
4
|
// market text cannot widen a limit or force a trade. Covers futures + spot + PM.
|
|
5
5
|
import { ok, fail, actionVenue, spotBuyCost, } from "./types.js";
|
|
6
|
+
import { checkEntryPredicates } from "./entryPredicates.js";
|
|
6
7
|
const SERVER_MAX_LEVERAGE = 20;
|
|
7
8
|
// Minimum edge, in probability POINTS, between the model's own forecast for
|
|
8
9
|
// the outcome it is backing and what that outcome currently costs. Live
|
|
@@ -51,6 +52,9 @@ export function validateAction(action, ctx) {
|
|
|
51
52
|
return fail("no_poll_before_write", "must successfully poll /trades before writing");
|
|
52
53
|
}
|
|
53
54
|
const increasesRisk = isRiskIncreasingAction(action);
|
|
55
|
+
const entryCondition = checkEntryPredicates(action, spec, observation);
|
|
56
|
+
if (!entryCondition.valid)
|
|
57
|
+
return entryCondition;
|
|
54
58
|
if (increasesRisk &&
|
|
55
59
|
ctx.riskIncreasesThisCycle >= spec.limits.maxWritesPerCycle) {
|
|
56
60
|
return fail("write_budget_exceeded", `maxWritesPerCycle ${spec.limits.maxWritesPerCycle} reached`);
|
package/dist/agent/engine.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { CoinRithmClient } from "./client.js";
|
|
|
8
8
|
export { loadAgent, buildSpec, type LoadedAgent } from "./skill.js";
|
|
9
9
|
export { resolveAgent } from "./resolve.js";
|
|
10
10
|
export { validateSkill, type SkillValidationMode } from "./skillValidator.js";
|
|
11
|
-
export { newState, rollDay } from "./state.js";
|
|
11
|
+
export { newState, rollDay, loadState, saveState } from "./state.js";
|
|
12
12
|
export { makeRunId } from "./runEvidence.js";
|
|
13
13
|
export { parseCadenceMs } from "./util.js";
|
|
14
14
|
export { BENCHMARK_AGENTS, BENCHMARK_STRATEGIES, decideMechanical, isBenchmarkStrategy, type BenchmarkStrategy, type BenchmarkAgentDefinition, } from "./mechanical.js";
|
package/dist/agent/engine.js
CHANGED
|
@@ -18,7 +18,7 @@ export { CoinRithmClient } from "./client.js";
|
|
|
18
18
|
export { loadAgent, buildSpec } from "./skill.js";
|
|
19
19
|
export { resolveAgent } from "./resolve.js";
|
|
20
20
|
export { validateSkill } from "./skillValidator.js";
|
|
21
|
-
export { newState, rollDay } from "./state.js";
|
|
21
|
+
export { newState, rollDay, loadState, saveState } from "./state.js";
|
|
22
22
|
export { makeRunId } from "./runEvidence.js";
|
|
23
23
|
export { parseCadenceMs } from "./util.js";
|
|
24
24
|
// Mechanical BENCHMARK baseline agents (sol #7): the deterministic, non-LLM
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentSpec, Observation, ProposedAction, ValidationResult } from "./types.js";
|
|
2
|
+
export interface EntryPredicate {
|
|
3
|
+
side: "long" | "short";
|
|
4
|
+
metric: "change1h" | "change24h";
|
|
5
|
+
operator: "gte" | "lte";
|
|
6
|
+
threshold: number;
|
|
7
|
+
maxAgeSeconds: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function entryPredicateIssues(value: unknown): ValidationResult[];
|
|
10
|
+
export declare function checkEntryPredicates(action: ProposedAction, spec: AgentSpec, observation: Observation): ValidationResult;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { fail, ok } from "./types.js";
|
|
2
|
+
export function entryPredicateIssues(value) {
|
|
3
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 16)
|
|
4
|
+
return [
|
|
5
|
+
fail("entry_predicate_config", "risk.entryPredicates must contain 1–16 conditions"),
|
|
6
|
+
];
|
|
7
|
+
const keys = ["side", "metric", "operator", "threshold", "maxAgeSeconds"];
|
|
8
|
+
const issues = [];
|
|
9
|
+
for (const [i, item] of value.entries()) {
|
|
10
|
+
const p = item;
|
|
11
|
+
if (!p ||
|
|
12
|
+
typeof p !== "object" ||
|
|
13
|
+
Array.isArray(p) ||
|
|
14
|
+
Object.keys(p).some((key) => !keys.includes(key)) ||
|
|
15
|
+
!["long", "short"].includes(p.side) ||
|
|
16
|
+
!["change1h", "change24h"].includes(p.metric) ||
|
|
17
|
+
!["gte", "lte"].includes(p.operator) ||
|
|
18
|
+
typeof p.threshold !== "number" ||
|
|
19
|
+
!Number.isFinite(p.threshold) ||
|
|
20
|
+
typeof p.maxAgeSeconds !== "number" ||
|
|
21
|
+
!Number.isFinite(p.maxAgeSeconds) ||
|
|
22
|
+
p.maxAgeSeconds <= 0) {
|
|
23
|
+
issues.push(fail("entry_predicate_config", `risk.entryPredicates[${i}] has an invalid condition`));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return issues;
|
|
27
|
+
}
|
|
28
|
+
export function checkEntryPredicates(action, spec, observation) {
|
|
29
|
+
const rules = spec.risk.entryPredicates;
|
|
30
|
+
if (rules === undefined)
|
|
31
|
+
return ok();
|
|
32
|
+
if (action.type !== "futures_open" &&
|
|
33
|
+
!(action.type === "spot_order" && action.side === "buy"))
|
|
34
|
+
return ok();
|
|
35
|
+
const invalid = entryPredicateIssues(rules);
|
|
36
|
+
if (invalid.length)
|
|
37
|
+
return invalid[0];
|
|
38
|
+
const side = action.type === "futures_open" ? action.side : "long";
|
|
39
|
+
const applicable = rules.filter((rule) => rule.side === side);
|
|
40
|
+
if (!applicable.length)
|
|
41
|
+
return ok();
|
|
42
|
+
const entry = observation.watch.find((item) => item.symbol.toUpperCase() === action.symbol.toUpperCase());
|
|
43
|
+
const observedAt = Date.parse(observation.asOf);
|
|
44
|
+
const age = entry?.freshness?.ageSeconds;
|
|
45
|
+
for (const rule of applicable) {
|
|
46
|
+
const value = entry?.[rule.metric];
|
|
47
|
+
// Missing evidence never becomes a passing predicate. Include time since
|
|
48
|
+
// observation so a long model call cannot freeze the freshness clock.
|
|
49
|
+
if (entry?.freshness?.status !== "fresh" ||
|
|
50
|
+
typeof age !== "number" ||
|
|
51
|
+
!Number.isFinite(age) ||
|
|
52
|
+
age < 0 ||
|
|
53
|
+
!Number.isFinite(observedAt) ||
|
|
54
|
+
age + Math.max(0, (Date.now() - observedAt) / 1000) >
|
|
55
|
+
rule.maxAgeSeconds ||
|
|
56
|
+
typeof value !== "number" ||
|
|
57
|
+
!Number.isFinite(value)) {
|
|
58
|
+
return fail("entry_predicate_evidence", `fresh ${rule.metric} evidence required for ${action.symbol}`);
|
|
59
|
+
}
|
|
60
|
+
if (!(rule.operator === "gte"
|
|
61
|
+
? value >= rule.threshold
|
|
62
|
+
: value <= rule.threshold)) {
|
|
63
|
+
return fail("entry_predicate_false", `${action.symbol} ${rule.metric}=${value} does not satisfy ${rule.operator} ${rule.threshold}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return ok();
|
|
67
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CoinRithmClient, ProvenanceReport } from "./client.js";
|
|
2
|
+
import type { AgentSpec, AgentTrace, PostedOpportunity } from "./types.js";
|
|
3
|
+
export declare function createOpportunityReporter({ client, spec, live, enabled, runId, decisionId, provenance, baseTrace, log, }: {
|
|
4
|
+
client: CoinRithmClient;
|
|
5
|
+
spec: AgentSpec;
|
|
6
|
+
live: boolean;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
runId: string;
|
|
9
|
+
decisionId: string;
|
|
10
|
+
provenance: ProvenanceReport;
|
|
11
|
+
baseTrace: AgentTrace;
|
|
12
|
+
log: (line: string) => void;
|
|
13
|
+
}): {
|
|
14
|
+
post: (o: PostedOpportunity) => Promise<void>;
|
|
15
|
+
readonly posted: PostedOpportunity | undefined;
|
|
16
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function createOpportunityReporter({ client, spec, live, enabled, runId, decisionId, provenance, baseTrace, log, }) {
|
|
2
|
+
let opportunityPosted = false;
|
|
3
|
+
let postedOpportunity;
|
|
4
|
+
const post = async (o) => {
|
|
5
|
+
if (!enabled || !live || opportunityPosted)
|
|
6
|
+
return;
|
|
7
|
+
opportunityPosted = true;
|
|
8
|
+
postedOpportunity = o;
|
|
9
|
+
try {
|
|
10
|
+
await client.reportPmOpportunity({
|
|
11
|
+
kind: o.kind,
|
|
12
|
+
source: o.source,
|
|
13
|
+
slug: o.slug,
|
|
14
|
+
outcomeExternalMarketId: o.outcomeExternalMarketId,
|
|
15
|
+
forecastProbability: o.forecastProbability,
|
|
16
|
+
marketProbability: o.marketProbability,
|
|
17
|
+
reasonCode: o.reasonCode,
|
|
18
|
+
cohort: {
|
|
19
|
+
universeSize: o.universeSize,
|
|
20
|
+
horizon: spec.objective?.horizon,
|
|
21
|
+
},
|
|
22
|
+
decisionId,
|
|
23
|
+
runId,
|
|
24
|
+
provenance,
|
|
25
|
+
}, baseTrace);
|
|
26
|
+
log(`reported ${o.kind} opportunity (universe ${o.universeSize ?? "?"})`);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
log(`opportunity post failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
post,
|
|
34
|
+
get posted() {
|
|
35
|
+
return postedOpportunity;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
package/dist/agent/prompt.js
CHANGED
|
@@ -117,6 +117,11 @@ opts = {}) {
|
|
|
117
117
|
: '- DIRECTION: LONG ONLY — every futures_open MUST be side:"long". A short is REJECTED by the runner no matter how strong the setup looks.',
|
|
118
118
|
]
|
|
119
119
|
: []),
|
|
120
|
+
...(r.entryPredicates
|
|
121
|
+
? [
|
|
122
|
+
`- Binding crypto entry conditions (all conditions matching the entry side must pass on fresh observed data): ${JSON.stringify(r.entryPredicates)}. Missing evidence is a rejection. These conditions never authorize exceeding another cap.`,
|
|
123
|
+
]
|
|
124
|
+
: []),
|
|
120
125
|
// With universe_scan, the validator's gate is WATCH-membership (manual
|
|
121
126
|
// watchlist ∪ this cycle's discovered entries) — saying "ONLY these" here
|
|
122
127
|
// while the universe-scan section below calls discovered movers tradable
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { accrueRealized } from "./state.js";
|
|
2
|
+
import { asObj, asNum, asStr } from "./extract.js";
|
|
3
|
+
export function reconcileObservation(state, observation) {
|
|
4
|
+
accrueRealized(state, observation.newClosedTrades);
|
|
5
|
+
state.cursor = observation.syncCursor;
|
|
6
|
+
for (const t of observation.newClosedTrades) {
|
|
7
|
+
state.seen.push(`${asStr(asObj(t).venue) ?? "futures"}:${asNum(asObj(t).id) ?? String(asObj(t).id)}`);
|
|
8
|
+
}
|
|
9
|
+
state.seen = state.seen.slice(-500);
|
|
10
|
+
// Slice-3 reflection: journal closed-trade OUTCOMES (not just opens) so the agent
|
|
11
|
+
// remembers how its theses RESOLVED — a stop-out it should not revenge-trade, a
|
|
12
|
+
// winner its style works on. Defensive field reads (the /trades shape varies);
|
|
13
|
+
// a partial entry is harmless, a missing one is skipped.
|
|
14
|
+
for (const t of observation.newClosedTrades.slice(-5)) {
|
|
15
|
+
const o = asObj(t);
|
|
16
|
+
const sym = asStr(o.symbol) ?? asStr(o.coinSymbol) ?? asStr(o.coinId);
|
|
17
|
+
const pnl = asNum(o.realizedPnlMusd) ??
|
|
18
|
+
asNum(o.pnlMusd) ??
|
|
19
|
+
asNum(o.realizedPnl) ??
|
|
20
|
+
asNum(o.pnl);
|
|
21
|
+
const side = asStr(o.side);
|
|
22
|
+
if (sym || pnl != null) {
|
|
23
|
+
const did = `closed ${side ?? ""} ${sym ?? "position"}`.trim() +
|
|
24
|
+
(pnl != null
|
|
25
|
+
? `: ${pnl >= 0 ? "+" : ""}${Math.round(pnl)}mUSD ${pnl >= 0 ? "WIN" : "LOSS"}`
|
|
26
|
+
: "");
|
|
27
|
+
state.journal = [
|
|
28
|
+
...(state.journal ?? []),
|
|
29
|
+
{ at: observation.asOf, did },
|
|
30
|
+
].slice(-12);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/agent/runner.js
CHANGED
|
@@ -8,6 +8,8 @@ import { usesCapitalSizing, prepareCapitalAction, validateCapitalAction, capital
|
|
|
8
8
|
import { decideMechanical } from "./mechanical.js";
|
|
9
9
|
import { evaluateGate, noteLlmCall, estimateCostUsd } from "./gate.js";
|
|
10
10
|
import { baseSymbol, scanSetups } from "./setups.js";
|
|
11
|
+
import { reconcileObservation } from "./reconcileObservation.js";
|
|
12
|
+
import { createOpportunityReporter } from "./opportunityReporter.js";
|
|
11
13
|
import { observe } from "./observe.js";
|
|
12
14
|
import { buildDailyRiskBudget, buildSystemPrompt, buildUserPrompt, } from "./prompt.js";
|
|
13
15
|
import { parseDecision } from "./decision.js";
|
|
@@ -15,7 +17,7 @@ import { validateAction, isRiskIncreasingAction, } from "./decisionValidator.js"
|
|
|
15
17
|
import { resolvePmRef } from "./resolvePm.js";
|
|
16
18
|
import { fetchQuote, executeAction } from "./act.js";
|
|
17
19
|
import { makeDecisionId, makeTrace, exportRunEvidence } from "./runEvidence.js";
|
|
18
|
-
import { rollDay, checkKillSwitch,
|
|
20
|
+
import { rollDay, checkKillSwitch, saveState, isPermanentModelError, isAuthFailureSkip, PERMANENT_MODEL_ERROR_THRESHOLD, AUTH_FAILURE_THRESHOLD, } from "./state.js";
|
|
19
21
|
import { asObj, asNum, asStr } from "./extract.js";
|
|
20
22
|
import { parseCadenceMs, sleep } from "./util.js";
|
|
21
23
|
import { buildObservationReceipt } from "./observationReceipt.js";
|
|
@@ -441,43 +443,22 @@ async function runCycleCore(deps, capture) {
|
|
|
441
443
|
live,
|
|
442
444
|
};
|
|
443
445
|
}
|
|
446
|
+
// Establish durable run identity before the first possible write. A process
|
|
447
|
+
// killed before its first response must not restart with a new idempotency key.
|
|
448
|
+
if (live)
|
|
449
|
+
saveState(stateFile, state);
|
|
444
450
|
const baseTrace = makeTrace(runId, decisionId, spec);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
opportunityPosted = true;
|
|
457
|
-
postedOpportunity = o;
|
|
458
|
-
try {
|
|
459
|
-
await client.reportPmOpportunity({
|
|
460
|
-
kind: o.kind,
|
|
461
|
-
source: o.source,
|
|
462
|
-
slug: o.slug,
|
|
463
|
-
outcomeExternalMarketId: o.outcomeExternalMarketId,
|
|
464
|
-
forecastProbability: o.forecastProbability,
|
|
465
|
-
marketProbability: o.marketProbability,
|
|
466
|
-
reasonCode: o.reasonCode,
|
|
467
|
-
cohort: {
|
|
468
|
-
universeSize: o.universeSize,
|
|
469
|
-
horizon: spec.objective?.horizon,
|
|
470
|
-
},
|
|
471
|
-
decisionId,
|
|
472
|
-
runId,
|
|
473
|
-
provenance,
|
|
474
|
-
}, baseTrace);
|
|
475
|
-
log(`reported ${o.kind} opportunity (universe ${o.universeSize ?? "?"})`);
|
|
476
|
-
}
|
|
477
|
-
catch (err) {
|
|
478
|
-
log(`opportunity post failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
479
|
-
}
|
|
480
|
-
};
|
|
451
|
+
const opportunities = createOpportunityReporter({
|
|
452
|
+
client,
|
|
453
|
+
spec,
|
|
454
|
+
live,
|
|
455
|
+
enabled: agentOpportunityCaptureEnabled(),
|
|
456
|
+
runId,
|
|
457
|
+
decisionId,
|
|
458
|
+
provenance,
|
|
459
|
+
baseTrace,
|
|
460
|
+
log,
|
|
461
|
+
});
|
|
481
462
|
// OBSERVE
|
|
482
463
|
const obs = await observe(client, spec, state, baseTrace);
|
|
483
464
|
const observation = obs.observation;
|
|
@@ -496,35 +477,7 @@ async function runCycleCore(deps, capture) {
|
|
|
496
477
|
// Reads build the observation, so its hash cannot exist before they finish.
|
|
497
478
|
// From this point every durable write carries the exact decision-input receipt.
|
|
498
479
|
Object.assign(baseTrace, observationReceipt);
|
|
499
|
-
|
|
500
|
-
state.cursor = observation.syncCursor;
|
|
501
|
-
for (const t of observation.newClosedTrades) {
|
|
502
|
-
state.seen.push(`${asStr(asObj(t).venue) ?? "futures"}:${asNum(asObj(t).id) ?? String(asObj(t).id)}`);
|
|
503
|
-
}
|
|
504
|
-
state.seen = state.seen.slice(-500);
|
|
505
|
-
// Slice-3 reflection: journal closed-trade OUTCOMES (not just opens) so the agent
|
|
506
|
-
// remembers how its theses RESOLVED — a stop-out it should not revenge-trade, a
|
|
507
|
-
// winner its style works on. Defensive field reads (the /trades shape varies);
|
|
508
|
-
// a partial entry is harmless, a missing one is skipped.
|
|
509
|
-
for (const t of observation.newClosedTrades.slice(-5)) {
|
|
510
|
-
const o = asObj(t);
|
|
511
|
-
const sym = asStr(o.symbol) ?? asStr(o.coinSymbol) ?? asStr(o.coinId);
|
|
512
|
-
const pnl = asNum(o.realizedPnlMusd) ??
|
|
513
|
-
asNum(o.pnlMusd) ??
|
|
514
|
-
asNum(o.realizedPnl) ??
|
|
515
|
-
asNum(o.pnl);
|
|
516
|
-
const side = asStr(o.side);
|
|
517
|
-
if (sym || pnl != null) {
|
|
518
|
-
const did = `closed ${side ?? ""} ${sym ?? "position"}`.trim() +
|
|
519
|
-
(pnl != null
|
|
520
|
-
? `: ${pnl >= 0 ? "+" : ""}${Math.round(pnl)}mUSD ${pnl >= 0 ? "WIN" : "LOSS"}`
|
|
521
|
-
: "");
|
|
522
|
-
state.journal = [
|
|
523
|
-
...(state.journal ?? []),
|
|
524
|
-
{ at: observation.asOf, did },
|
|
525
|
-
].slice(-12);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
480
|
+
reconcileObservation(state, observation);
|
|
528
481
|
capture({
|
|
529
482
|
...captureBase,
|
|
530
483
|
phase: "observed",
|
|
@@ -897,7 +850,7 @@ async function runCycleCore(deps, capture) {
|
|
|
897
850
|
// cohort via universeSize.
|
|
898
851
|
const skipOpp = buildSkipOpportunity(decision, observation.pmMarkets, forecastEnabled);
|
|
899
852
|
if (skipOpp)
|
|
900
|
-
await
|
|
853
|
+
await opportunities.post(skipOpp);
|
|
901
854
|
saveState(stateFile, state);
|
|
902
855
|
log(`model chose skip${decision.reason ? `: ${decision.reason}` : ""}`);
|
|
903
856
|
return {
|
|
@@ -912,7 +865,7 @@ async function runCycleCore(deps, capture) {
|
|
|
912
865
|
decisionType: "skip",
|
|
913
866
|
writeAttempted: decision.actions.length,
|
|
914
867
|
writeAccepted: 0,
|
|
915
|
-
...(
|
|
868
|
+
...(opportunities.posted ? { opportunity: opportunities.posted } : {}),
|
|
916
869
|
...observationReceipt,
|
|
917
870
|
};
|
|
918
871
|
}
|
|
@@ -1265,7 +1218,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1265
1218
|
// (once-per-cycle; carries the universe breadth in the cohort field).
|
|
1266
1219
|
if (action.type === "pm_open" && isQuoteExpiredResult(r.status, r.data)) {
|
|
1267
1220
|
const pm = action;
|
|
1268
|
-
await
|
|
1221
|
+
await opportunities.post({
|
|
1269
1222
|
kind: "quote_expired",
|
|
1270
1223
|
source: pm.source,
|
|
1271
1224
|
slug: pm.slug,
|
|
@@ -1322,7 +1275,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1322
1275
|
decisionType: "act",
|
|
1323
1276
|
writeAttempted: decision.actions.length,
|
|
1324
1277
|
writeAccepted: planned.filter((p) => p.accepted).length,
|
|
1325
|
-
...(
|
|
1278
|
+
...(opportunities.posted ? { opportunity: opportunities.posted } : {}),
|
|
1326
1279
|
...observationReceipt,
|
|
1327
1280
|
};
|
|
1328
1281
|
}
|
package/dist/agent/skill.js
CHANGED
|
@@ -103,6 +103,12 @@ export function buildSpec(raw) {
|
|
|
103
103
|
? { capitalSizing: buildCapitalSizing(raw.capitalSizing) }
|
|
104
104
|
: {}),
|
|
105
105
|
risk: {
|
|
106
|
+
// Preserve malformed explicit policies for fail-closed validation.
|
|
107
|
+
...(risk.entryPredicates !== undefined
|
|
108
|
+
? {
|
|
109
|
+
entryPredicates: risk.entryPredicates,
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
106
112
|
maxLeverage: num(risk.maxLeverage, 1),
|
|
107
113
|
perTradeMarginMusd: num(risk.perTradeMarginMusd, 0),
|
|
108
114
|
maxConcurrentPositions: num(risk.maxConcurrentPositions, 0),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fail, VENUES, PROVIDERS, SPEC_VERSION, OBJECTIVE_PRIMARIES, ALLOWED_CAPABILITIES, } from "./types.js";
|
|
2
2
|
import { parseCadenceMs, scanForSecrets } from "./util.js";
|
|
3
|
+
import { entryPredicateIssues } from "./entryPredicates.js";
|
|
3
4
|
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4
5
|
const isPosNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
5
6
|
// maxTradesPerDay accepts 0 as the explicit "unlimited daily trades" sentinel.
|
|
@@ -93,6 +94,8 @@ export function validateSkill(parsed, mode = "self-host") {
|
|
|
93
94
|
}
|
|
94
95
|
else {
|
|
95
96
|
const r = raw.risk;
|
|
97
|
+
if (r.entryPredicates !== undefined)
|
|
98
|
+
issues.push(...entryPredicateIssues(r.entryPredicates));
|
|
96
99
|
if (!isPosNum(r.maxLeverage))
|
|
97
100
|
add("skill_risk_leverage", "risk.maxLeverage must be a positive number");
|
|
98
101
|
else if (r.maxLeverage > 20)
|
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];
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface TriggerPolicy {
|
|
|
26
26
|
}
|
|
27
27
|
export declare const DEFAULT_TRIGGER_POLICY: TriggerPolicy;
|
|
28
28
|
export interface RiskConfig {
|
|
29
|
+
entryPredicates?: import("./entryPredicates.js").EntryPredicate[];
|
|
29
30
|
maxLeverage: number;
|
|
30
31
|
perTradeMarginMusd: number;
|
|
31
32
|
maxConcurrentPositions: number;
|
package/dist/agent/util.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Small dependency-free helpers shared across the runner.
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { resolve as resolvePath, relative as relativePath, isAbsolute, } from "node:path";
|
|
4
4
|
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
5
|
// Parse a cadence string ("30s", "15m", "1h", "4h", "1d") to milliseconds.
|
|
@@ -63,8 +63,8 @@ export function scanForSecrets(value, pathPrefix = "", findings = []) {
|
|
|
63
63
|
}
|
|
64
64
|
// Short random id (for runId/decisionId/idempotencyKey suffixes).
|
|
65
65
|
export function shortId() {
|
|
66
|
-
//
|
|
67
|
-
return
|
|
66
|
+
// Import the Node API: globalThis.crypto is not enabled by default in Node 18.
|
|
67
|
+
return randomUUID().slice(0, 8);
|
|
68
68
|
}
|
|
69
69
|
// ── Resolver helpers (cross-platform-deterministic) ──────────────────────────
|
|
70
70
|
// Normalize file content for hashing: CRLF/CR -> LF, strip trailing whitespace,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinrithm/mcp-trading",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
4
4
|
"mcpName": "io.github.CoinRithm/mcp-trading",
|
|
5
5
|
"description": "CoinRithm paper-trading toolkit: an MCP server (coinrithm-mcp) AND a self-host agent runner (coinrithm-agent) for spot, futures, and prediction markets with a user-minted API key.",
|
|
6
6
|
"type": "module",
|
|
@@ -84,5 +84,14 @@
|
|
|
84
84
|
"typescript": "^5.5.0",
|
|
85
85
|
"typescript-eslint": "^8.11.0",
|
|
86
86
|
"vitest": "4.1.11"
|
|
87
|
+
},
|
|
88
|
+
"exports": {
|
|
89
|
+
".": "./dist/index.js",
|
|
90
|
+
"./engine": {
|
|
91
|
+
"types": "./dist/agent/engine.d.ts",
|
|
92
|
+
"import": "./dist/agent/engine.js"
|
|
93
|
+
},
|
|
94
|
+
"./package.json": "./package.json",
|
|
95
|
+
"./dist/*": "./dist/*"
|
|
87
96
|
}
|
|
88
97
|
}
|