@coinrithm/mcp-trading 0.7.9 → 0.7.11
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 +47 -4
- package/README.md +13 -4
- 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 +2 -2
- 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 +17 -0
- package/dist/agent/opportunityReporter.js +59 -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 +29 -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 +7 -0
- package/dist/agent/util.js +3 -3
- package/package.json +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,10 +5,53 @@ 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.11 — 2026-09-15
|
|
9
|
+
|
|
10
|
+
Fix opportunity reporting that previously treated resolved API failures as
|
|
11
|
+
successful submissions. Only `ok: true` confirms a report. HTTP errors remain
|
|
12
|
+
unconfirmed; transport failures and exceptions have an unknown delivery outcome.
|
|
13
|
+
|
|
14
|
+
`CycleResult.opportunity` now contains confirmed reports only. The additive
|
|
15
|
+
`opportunityReport` field preserves the attempted payload, outcome and status,
|
|
16
|
+
without API error bodies or exception details. Existing consumers should use
|
|
17
|
+
this field when they need attempted rather than confirmed evidence.
|
|
18
|
+
|
|
19
|
+
The reporter retains its one-invocation-per-cycle latch and adds no retries.
|
|
20
|
+
Focused regressions compare successful and failed reporting in skip/act cycles
|
|
21
|
+
and verify unchanged trading results and runner state. SDK versions are unchanged.
|
|
22
|
+
This source entry does not establish registry publication or hosted deployment.
|
|
23
|
+
|
|
24
|
+
## 0.7.10 — 2026-09-15
|
|
25
|
+
|
|
26
|
+
Source changes following the published 0.7.9 release. The TypeScript SDK remains
|
|
27
|
+
0.3.1 and Python remains 1.8.1; their runtime source is unchanged.
|
|
28
|
+
|
|
29
|
+
- Persist file-backed run identity before execution so a first-cycle process
|
|
30
|
+
crash cannot discard the idempotency identity. Transport uncertainty is still
|
|
31
|
+
not automatically replayed or recorded as a completed trade.
|
|
32
|
+
- Fix runner startup on Node 18: use the imported Node crypto API instead of
|
|
33
|
+
depending on a global crypto object. The installed-package matrix reproduced
|
|
34
|
+
this failure on Linux, Windows and macOS.
|
|
35
|
+
- Add the supported `@coinrithm/mcp-trading/engine` entry point, preserving
|
|
36
|
+
existing deep imports, and separate observation accounting and opportunity
|
|
37
|
+
reporting from cycle ordering.
|
|
38
|
+
- Add opt-in, machine-checked crypto return predicates and visible warnings for
|
|
39
|
+
inactive/reserved configuration. Existing agents are not automatically opted in.
|
|
40
|
+
- Serialize scheduler migrations in a bounded transaction; add an offline
|
|
41
|
+
credential-rotation helper and interruption/recovery rehearsal.
|
|
42
|
+
- Pin workflow actions and verify release-tool checksums. Add installed-package
|
|
43
|
+
compatibility and restart smoke checks across operating systems and runtimes.
|
|
44
|
+
|
|
45
|
+
0.7.10 was published and deployed on 2026-09-15. Registry and GitHub downloads
|
|
46
|
+
matched the CI-tested archive; hosted MCP and scheduler deployments finished.
|
|
47
|
+
See the [release record](https://github.com/CoinRithm/coinrithm-agent-trading/releases/tag/mcp-trading-v0.7.10).
|
|
48
|
+
|
|
49
|
+
## 0.7.9 - 2026-09-15
|
|
50
|
+
|
|
51
|
+
Published on npm and verified on 2026-09-15: the registry archive matches the
|
|
52
|
+
reviewed artifact and passes fresh-install checks. See the
|
|
53
|
+
[combined release notes](https://github.com/CoinRithm/coinrithm-agent-trading/releases/tag/mcp-trading-v0.7.9)
|
|
54
|
+
for source provenance, verification and community acknowledgments.
|
|
12
55
|
|
|
13
56
|
Public market-data fidelity and runner reliability release. No MCP tool was
|
|
14
57
|
renamed or removed, and the API **contract stays 1.7.0**.
|
package/README.md
CHANGED
|
@@ -42,10 +42,14 @@ 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.
|
|
46
|
-
|
|
47
|
-
This
|
|
48
|
-
|
|
45
|
+
## Version 0.7.11
|
|
46
|
+
|
|
47
|
+
This patch corrects opportunity-report status: only a successful API result
|
|
48
|
+
confirms submission. HTTP errors remain unconfirmed, and transport failures
|
|
49
|
+
have an unknown outcome. Attempt evidence is available in
|
|
50
|
+
`CycleResult.opportunityReport`; `opportunity` contains confirmed reports only.
|
|
51
|
+
Trading behavior and the one-report-method-call-per-cycle limit are preserved. See
|
|
52
|
+
[CHANGELOG.md](./CHANGELOG.md). Check `npm view @coinrithm/mcp-trading
|
|
49
53
|
version` for the latest published version. Hosted deployments and npm releases
|
|
50
54
|
are separate.
|
|
51
55
|
|
|
@@ -53,6 +57,11 @@ Runner API operations have a 30-second total deadline, including response
|
|
|
53
57
|
bodies and 429 retry waits. Timeout and cancellation results remain unconfirmed;
|
|
54
58
|
the client does not automatically replay an uncertain trading write.
|
|
55
59
|
|
|
60
|
+
Embedding the runner? Import from `@coinrithm/mcp-trading/engine` for the
|
|
61
|
+
supported engine and state helpers. Existing `dist/agent/engine.js` imports
|
|
62
|
+
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)
|
|
63
|
+
for the exact opt-in policy and persistence contract.
|
|
64
|
+
|
|
56
65
|
## Quick start
|
|
57
66
|
|
|
58
67
|
```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,8 +8,8 @@ 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";
|
|
15
|
-
export type { AgentSpec, RunState, CycleResult, PlannedAction, Venue, ProviderName, ModelConfig, } from "./types.js";
|
|
15
|
+
export type { AgentSpec, RunState, CycleResult, OpportunityReport, PostedOpportunity, PlannedAction, Venue, ProviderName, ModelConfig, } from "./types.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,17 @@
|
|
|
1
|
+
import type { CoinRithmClient, ProvenanceReport } from "./client.js";
|
|
2
|
+
import type { AgentSpec, AgentTrace, OpportunityReport, 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
|
+
readonly report: OpportunityReport | undefined;
|
|
17
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function createOpportunityReporter({ client, spec, live, enabled, runId, decisionId, provenance, baseTrace, log, }) {
|
|
2
|
+
let attempted = false;
|
|
3
|
+
let report;
|
|
4
|
+
const post = async (o) => {
|
|
5
|
+
if (!enabled || !live || attempted)
|
|
6
|
+
return;
|
|
7
|
+
attempted = true;
|
|
8
|
+
try {
|
|
9
|
+
const result = await client.reportPmOpportunity({
|
|
10
|
+
kind: o.kind,
|
|
11
|
+
source: o.source,
|
|
12
|
+
slug: o.slug,
|
|
13
|
+
outcomeExternalMarketId: o.outcomeExternalMarketId,
|
|
14
|
+
forecastProbability: o.forecastProbability,
|
|
15
|
+
marketProbability: o.marketProbability,
|
|
16
|
+
reasonCode: o.reasonCode,
|
|
17
|
+
cohort: {
|
|
18
|
+
universeSize: o.universeSize,
|
|
19
|
+
horizon: spec.objective?.horizon,
|
|
20
|
+
},
|
|
21
|
+
decisionId,
|
|
22
|
+
runId,
|
|
23
|
+
provenance,
|
|
24
|
+
}, baseTrace);
|
|
25
|
+
report = {
|
|
26
|
+
opportunity: o,
|
|
27
|
+
outcome: result.ok
|
|
28
|
+
? "confirmed"
|
|
29
|
+
: result.status === 0
|
|
30
|
+
? "unknown"
|
|
31
|
+
: "http_error",
|
|
32
|
+
status: result.status,
|
|
33
|
+
};
|
|
34
|
+
if (result.ok) {
|
|
35
|
+
log(`reported ${o.kind} opportunity (universe ${o.universeSize ?? "?"})`);
|
|
36
|
+
}
|
|
37
|
+
else if (result.status === 0) {
|
|
38
|
+
log("opportunity report outcome unknown (transport failure)");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
log(`opportunity report received HTTP ${result.status}; delivery unconfirmed`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
report = { opportunity: o, outcome: "unknown", status: 0 };
|
|
46
|
+
// Exceptions and API error bodies may contain private request details.
|
|
47
|
+
log("opportunity report outcome unknown (exception)");
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
post,
|
|
52
|
+
get posted() {
|
|
53
|
+
return report?.outcome === "confirmed" ? report.opportunity : undefined;
|
|
54
|
+
},
|
|
55
|
+
get report() {
|
|
56
|
+
return report;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
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,10 @@ 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 } : {}),
|
|
869
|
+
...(opportunities.report
|
|
870
|
+
? { opportunityReport: opportunities.report }
|
|
871
|
+
: {}),
|
|
916
872
|
...observationReceipt,
|
|
917
873
|
};
|
|
918
874
|
}
|
|
@@ -1265,7 +1221,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1265
1221
|
// (once-per-cycle; carries the universe breadth in the cohort field).
|
|
1266
1222
|
if (action.type === "pm_open" && isQuoteExpiredResult(r.status, r.data)) {
|
|
1267
1223
|
const pm = action;
|
|
1268
|
-
await
|
|
1224
|
+
await opportunities.post({
|
|
1269
1225
|
kind: "quote_expired",
|
|
1270
1226
|
source: pm.source,
|
|
1271
1227
|
slug: pm.slug,
|
|
@@ -1322,7 +1278,10 @@ async function runCycleCore(deps, capture) {
|
|
|
1322
1278
|
decisionType: "act",
|
|
1323
1279
|
writeAttempted: decision.actions.length,
|
|
1324
1280
|
writeAccepted: planned.filter((p) => p.accepted).length,
|
|
1325
|
-
...(
|
|
1281
|
+
...(opportunities.posted ? { opportunity: opportunities.posted } : {}),
|
|
1282
|
+
...(opportunities.report
|
|
1283
|
+
? { opportunityReport: opportunities.report }
|
|
1284
|
+
: {}),
|
|
1326
1285
|
...observationReceipt,
|
|
1327
1286
|
};
|
|
1328
1287
|
}
|
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;
|
|
@@ -503,6 +504,12 @@ export interface CycleResult {
|
|
|
503
504
|
writeAttempted?: number;
|
|
504
505
|
writeAccepted?: number;
|
|
505
506
|
opportunity?: PostedOpportunity;
|
|
507
|
+
opportunityReport?: OpportunityReport;
|
|
508
|
+
}
|
|
509
|
+
export interface OpportunityReport {
|
|
510
|
+
opportunity: PostedOpportunity;
|
|
511
|
+
outcome: "confirmed" | "http_error" | "unknown";
|
|
512
|
+
status: number;
|
|
506
513
|
}
|
|
507
514
|
export interface PostedOpportunity {
|
|
508
515
|
kind: "abstained" | "forecast_only" | "quote_expired";
|
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.11",
|
|
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
|
}
|