@coinrithm/mcp-trading 0.1.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +43 -19
  3. package/dist/agent/act.d.ts +4 -0
  4. package/dist/agent/act.js +114 -0
  5. package/dist/agent/capabilityGuard.d.ts +2 -0
  6. package/dist/agent/capabilityGuard.js +131 -0
  7. package/dist/agent/cli.d.ts +21 -0
  8. package/dist/agent/cli.js +382 -0
  9. package/dist/agent/client.d.ts +107 -0
  10. package/dist/agent/client.js +173 -0
  11. package/dist/agent/decision.d.ts +137 -0
  12. package/dist/agent/decision.js +118 -0
  13. package/dist/agent/decisionValidator.d.ts +16 -0
  14. package/dist/agent/decisionValidator.js +215 -0
  15. package/dist/agent/engine.d.ts +10 -0
  16. package/dist/agent/engine.js +16 -0
  17. package/dist/agent/extract.d.ts +4 -0
  18. package/dist/agent/extract.js +5 -0
  19. package/dist/agent/frontmatter.d.ts +5 -0
  20. package/dist/agent/frontmatter.js +19 -0
  21. package/dist/agent/index.d.ts +2 -0
  22. package/dist/agent/index.js +10 -0
  23. package/dist/agent/indicators.d.ts +44 -0
  24. package/dist/agent/indicators.js +135 -0
  25. package/dist/agent/manifest.d.ts +15 -0
  26. package/dist/agent/manifest.js +40 -0
  27. package/dist/agent/mergeRules.d.ts +11 -0
  28. package/dist/agent/mergeRules.js +82 -0
  29. package/dist/agent/observe.d.ts +7 -0
  30. package/dist/agent/observe.js +244 -0
  31. package/dist/agent/prompt.d.ts +3 -0
  32. package/dist/agent/prompt.js +76 -0
  33. package/dist/agent/providers.d.ts +25 -0
  34. package/dist/agent/providers.js +143 -0
  35. package/dist/agent/resolve.d.ts +11 -0
  36. package/dist/agent/resolve.js +499 -0
  37. package/dist/agent/runEvidence.d.ts +6 -0
  38. package/dist/agent/runEvidence.js +23 -0
  39. package/dist/agent/runner.d.ts +19 -0
  40. package/dist/agent/runner.js +280 -0
  41. package/dist/agent/skill.d.ts +12 -0
  42. package/dist/agent/skill.js +136 -0
  43. package/dist/agent/skillValidator.d.ts +7 -0
  44. package/dist/agent/skillValidator.js +123 -0
  45. package/dist/agent/state.d.ts +7 -0
  46. package/dist/agent/state.js +96 -0
  47. package/dist/agent/strictLint.d.ts +3 -0
  48. package/dist/agent/strictLint.js +165 -0
  49. package/dist/agent/templates.d.ts +14 -0
  50. package/dist/agent/templates.js +192 -0
  51. package/dist/agent/types.d.ts +286 -0
  52. package/dist/agent/types.js +88 -0
  53. package/dist/agent/util.d.ts +13 -0
  54. package/dist/agent/util.js +116 -0
  55. package/dist/agent/version.d.ts +11 -0
  56. package/dist/agent/version.js +16 -0
  57. package/dist/client.d.ts +162 -0
  58. package/dist/http.d.ts +2 -0
  59. package/dist/index.d.ts +2 -0
  60. package/dist/tools.d.ts +3 -0
  61. package/dist/version.d.ts +1 -0
  62. package/package.json +78 -67
package/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@coinrithm/mcp-trading` are documented here. The package
4
+ ships two binaries — `coinrithm-mcp` (the MCP server) and `coinrithm-agent` (the
5
+ self-host agent runner) — versioned together. The CoinRithm **API contract** is
6
+ versioned separately (see `openapi.yaml` `info.version`, currently `1.4.0`).
7
+
8
+ ## 0.3.0
9
+
10
+ - **Agent risk config: coin deny-list (`blocklist`).** `risk.blocklist` lets an
11
+ agent name symbols it must never open, even if they are on the watchlist —
12
+ deny wins over allow. Enforced in the runner's decision validator (rejects
13
+ `futures_open` / `spot_order` on a denied symbol) and surfaced in the system
14
+ prompt, so the model is told the boundary and the runner re-checks it.
15
+ - **Docs: Open Knowledge Format positioning.** Clarified that a CoinRithm agent
16
+ is an OKF bundle — a portable directory of markdown + frontmatter that is
17
+ model-agnostic (run the same definition on any model). Develop and prove it
18
+ free on paper, then run it anywhere.
19
+ - npm keywords refreshed (`open-knowledge-format`, `okf`, `model-agnostic`,
20
+ `gemini`) for registry discovery.
21
+
22
+ ## 0.2.0
23
+
24
+ - Added the **`coinrithm-agent`** self-host runner binary alongside the MCP
25
+ server: a folder-as-architecture (OKF) agent you bring your own model key to,
26
+ with caps enforced by the runner (not the model), dry-run by default,
27
+ paper-only.
28
+
29
+ ## 0.1.x
30
+
31
+ - Initial `coinrithm-mcp` MCP server: reads, quotes, scoped spot/futures/PM
32
+ writes, ledger export, and Agent Arena integration over a user-minted API key.
package/README.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # @coinrithm/mcp-trading
2
2
 
3
- An MCP server that lets an AI agent paper-trade on CoinRithm (spot, futures,
4
- prediction markets) using a personal API key.
3
+ CoinRithm paper-trading toolkit. Two binaries:
4
+
5
+ - **`coinrithm-mcp`** — an MCP server that lets an AI agent paper-trade on
6
+ CoinRithm (spot, futures, prediction markets) using a personal API key.
7
+ - **`coinrithm-agent`** — a self-host **agent runner**: author an agent as a
8
+ folder and run an observe→decide→validate→act loop with your own model key
9
+ (paper: spot, futures, prediction markets). See [Agent runner](#agent-runner-coinrithm-agent) below.
5
10
 
6
11
  > **Paper trading only** — virtual funds (50,000 mUSD). Not financial advice.
7
12
 
@@ -12,6 +17,25 @@ npm install
12
17
  npm run build
13
18
  ```
14
19
 
20
+ ## Agent runner (`coinrithm-agent`)
21
+
22
+ This package also ships a **self-host agent runner**. You write an agent as a
23
+ folder (strategy + hard caps in markdown/YAML); the runner compiles it and runs
24
+ an `observe → decide → validate → act` loop, asking *your* model (bring-your-own
25
+ key) for structured decisions and executing only the ones that pass your caps —
26
+ **dry-run by default**, paper-only across spot, futures, and prediction markets.
27
+
28
+ ```bash
29
+ coinrithm-agent new my-agent --preset conservative
30
+ coinrithm-agent validate my-agent
31
+ COINRITHM_API_KEY=crk_live_… ANTHROPIC_API_KEY=sk-ant-… \
32
+ coinrithm-agent run my-agent --once --dry-run
33
+ ```
34
+
35
+ Full guide (env vars, fail-closed guarantees, folder layout):
36
+ **[docs/agent-runner.md](../../docs/agent-runner.md)**. The CoinRithm hosted
37
+ scheduler runs this same engine for you (later).
38
+
15
39
  ## Two ways to run
16
40
 
17
41
  | Mode | Entry | Auth | Who it's for |
@@ -59,11 +83,11 @@ key upstream. See [`DEPLOY.md`](./DEPLOY.md).
59
83
  | `get_market_context` (coinId) | read | `GET /api/agent/market/:coinId` |
60
84
  | `get_candles` (coinId, range) | read | `GET /api/agent/market/:coinId/candles` |
61
85
  | `discover_pm_markets` | read | `GET /api/agent/pm/discover` |
62
- | `get_performance` | read | `GET /api/agent/performance` |
63
- | `get_agent_ledger` | read | `GET /api/agent/ledger` |
64
- | `export_agent_ledger` | read | `GET /api/agent/ledger/export` |
65
- | `export_run_evidence` | read | `GET /api/agent/ledger/export?runId=...` |
66
- | `get_arena_leaderboard` | read | `GET /api/arena` |
86
+ | `get_performance` | read | `GET /api/agent/performance` |
87
+ | `get_agent_ledger` | read | `GET /api/agent/ledger` |
88
+ | `export_agent_ledger` | read | `GET /api/agent/ledger/export` |
89
+ | `export_run_evidence` | read | `GET /api/agent/ledger/export?runId=...` |
90
+ | `get_arena_leaderboard` | read | `GET /api/arena` |
67
91
  | `get_arena_agent` (handle) | read | `GET /api/arena/:handle` |
68
92
  | `list_open_orders` | read | `GET /api/agent/orders/open` |
69
93
  | `get_positions` (venue) | read | `GET /api/agent/positions/{futures,pm}` |
@@ -125,18 +149,18 @@ account only, latest stored market/probability snapshots, no explicit
125
149
  commission/slippage in v1, no futures funding/fees, and worker-driven resting
126
150
  order / SL / TP / settlement timing. It is a reproducibility artifact for your
127
151
  run; it is not a full point-in-time market archive and does not expose hidden
128
- reasoning. Aggregate audit stats include trace coverage for `runId` and
129
- `decisionId`. Run exports also include `retentionPolicy`: private ledger rows
130
- use a rolling retention window and exports are capped. They include
131
- `evidenceChecklist`, a derived pass/warn/fail checklist for trace completeness,
132
- decision ids, quote-before-trade coverage, rejected calls, export truncation,
133
- execution assumptions, and outcome attribution; it does not create additional
134
- retained data. `outcomeSummary` derives best-effort realized PnL from existing
135
- related trade/position ids, and spot orders can also match through their
136
- idempotency keys once a terminal `ClosedOrder` exists. It reports whether
137
- coverage is `none`, `partial`, or `complete`; it does not store new data. Public
138
- Arena surfaces only aggregate audit stats; raw request logs and rationale
139
- summaries stay private.
152
+ reasoning. Aggregate audit stats include trace coverage for `runId` and
153
+ `decisionId`. Run exports also include `retentionPolicy`: private ledger rows
154
+ use a rolling retention window and exports are capped. They include
155
+ `evidenceChecklist`, a derived pass/warn/fail checklist for trace completeness,
156
+ decision ids, quote-before-trade coverage, rejected calls, export truncation,
157
+ execution assumptions, and outcome attribution; it does not create additional
158
+ retained data. `outcomeSummary` derives best-effort realized PnL from existing
159
+ related trade/position ids, and spot orders can also match through their
160
+ idempotency keys once a terminal `ClosedOrder` exists. It reports whether
161
+ coverage is `none`, `partial`, or `complete`; it does not store new data. Public
162
+ Arena surfaces only aggregate audit stats; raw request logs and rationale
163
+ summaries stay private.
140
164
 
141
165
  `get_my_trades`, `list_open_orders`, and `get_positions` accept an optional
142
166
  `updatedSince` cursor and their responses carry `asOf` — pass it back to poll
@@ -0,0 +1,4 @@
1
+ import { CoinRithmClient } from "./client.js";
2
+ import { ProposedAction, AgentTrace, ApiResult, Observation, QuoteEvidence } from "./types.js";
3
+ export declare function fetchQuote(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace?: AgentTrace): Promise<QuoteEvidence | undefined>;
4
+ export declare function executeAction(client: CoinRithmClient, action: ProposedAction, observation: Observation, trace: AgentTrace, idempotencyKey: string): Promise<ApiResult>;
@@ -0,0 +1,114 @@
1
+ // Act phase: fetch the quote evidence for an open (the runner does this, never
2
+ // the model) and execute a validated action (futures / spot / PM) with an
3
+ // idempotency key.
4
+ import { asObj, asNum, asStr } from "./extract.js";
5
+ function coinIdFor(observation, symbol) {
6
+ return observation.watch.find((w) => w.symbol.toUpperCase() === symbol.toUpperCase())?.coinId ?? undefined;
7
+ }
8
+ function freshnessOf(block) {
9
+ const fr = asObj(block.freshness);
10
+ const status = asStr(fr.status);
11
+ return status ? { status, ageSeconds: asNum(fr.ageSeconds) } : undefined;
12
+ }
13
+ // Read-only quote BEFORE any open. Returns ineligible (never throws) on error.
14
+ export async function fetchQuote(client, action, observation, trace) {
15
+ let r;
16
+ if (action.type === "futures_open") {
17
+ const coinId = coinIdFor(observation, action.symbol);
18
+ if (!coinId)
19
+ return { eligible: false, blockReasons: ["unresolved_symbol"] };
20
+ r = await client.futuresQuote({ coinId, side: action.side, leverage: action.leverage, marginMusd: action.marginMusd }, trace);
21
+ }
22
+ else if (action.type === "spot_order") {
23
+ const coinId = coinIdFor(observation, action.symbol);
24
+ if (!coinId)
25
+ return { eligible: false, blockReasons: ["unresolved_symbol"] };
26
+ r = await client.spotQuote({ coinId, side: action.side, quantity: action.quantity }, trace);
27
+ }
28
+ else if (action.type === "pm_open") {
29
+ r = await client.pmQuote({
30
+ source: action.source,
31
+ slug: action.slug,
32
+ outcomeExternalMarketId: action.outcomeExternalMarketId,
33
+ stakeMusd: action.stakeMusd,
34
+ }, trace);
35
+ }
36
+ else {
37
+ return undefined; // close / set-sltp / cancel need no quote
38
+ }
39
+ if (!r.ok)
40
+ return { eligible: false, blockReasons: [`quote_http_${r.status}`] };
41
+ const d = asObj(r.data);
42
+ return {
43
+ eligible: d.eligible === true,
44
+ blockReasons: d.blockReasons,
45
+ entryPrice: asNum(d.entryPrice), // futures
46
+ liquidationPrice: asNum(d.liquidationPrice), // futures
47
+ executionPrice: asNum(d.executionPrice), // spot live fill price
48
+ estimatedCostMusd: asNum(d.estimatedCostMusd), // spot gross notional
49
+ // Freshness lives in the response's `observation` block (anti-look-ahead).
50
+ freshness: freshnessOf(asObj(d.observation)),
51
+ };
52
+ }
53
+ export async function executeAction(client, action, observation, trace, idempotencyKey) {
54
+ if (action.type === "futures_open") {
55
+ const coinId = coinIdFor(observation, action.symbol);
56
+ if (!coinId)
57
+ return { ok: false, status: 0, data: { error: "unresolved_symbol" } };
58
+ return client.openFutures({
59
+ coinId,
60
+ side: action.side,
61
+ leverage: action.leverage,
62
+ marginMusd: action.marginMusd,
63
+ idempotencyKey,
64
+ stopLossPrice: action.stopLossPrice ?? null,
65
+ takeProfitPrice: action.takeProfitPrice ?? null,
66
+ agentTrace: trace,
67
+ });
68
+ }
69
+ if (action.type === "futures_close") {
70
+ return client.closeFutures({
71
+ positionId: action.positionId,
72
+ fraction: action.fraction,
73
+ idempotencyKey,
74
+ agentTrace: trace,
75
+ });
76
+ }
77
+ if (action.type === "futures_set_sltp") {
78
+ return client.setFuturesSlTp({
79
+ positionId: action.positionId,
80
+ stopLossPrice: action.stopLossPrice ?? undefined,
81
+ takeProfitPrice: action.takeProfitPrice ?? undefined,
82
+ agentTrace: trace,
83
+ });
84
+ }
85
+ if (action.type === "spot_order") {
86
+ const coinId = coinIdFor(observation, action.symbol);
87
+ if (!coinId)
88
+ return { ok: false, status: 0, data: { error: "unresolved_symbol" } };
89
+ return client.placeSpotOrder({
90
+ coinId,
91
+ side: action.side,
92
+ orderType: action.orderType,
93
+ quantity: action.quantity,
94
+ limitPrice: action.limitPrice,
95
+ stopPrice: action.stopPrice,
96
+ idempotencyKey,
97
+ agentTrace: trace,
98
+ });
99
+ }
100
+ if (action.type === "spot_cancel") {
101
+ return client.cancelSpotOrder(action.orderId, trace);
102
+ }
103
+ if (action.type === "pm_open") {
104
+ return client.openPmPosition({
105
+ source: action.source,
106
+ slug: action.slug,
107
+ outcomeExternalMarketId: action.outcomeExternalMarketId,
108
+ stakeMusd: action.stakeMusd,
109
+ idempotencyKey,
110
+ agentTrace: trace,
111
+ });
112
+ }
113
+ return { ok: false, status: 0, data: { error: "unsupported_action" } };
114
+ }
@@ -0,0 +1,2 @@
1
+ import { AgentSpec, ResolvedAgent, ResolveIssue } from "./types.js";
2
+ export declare function checkCapabilityDrift(resolved: ResolvedAgent, spec: AgentSpec): ResolveIssue[];
@@ -0,0 +1,131 @@
1
+ // Capability drift guard.
2
+ //
3
+ // An agent's PROSE (thesis, persona, tactic skills) is authored separately from
4
+ // the runner's code, so a skill can name a venue, an action, or a cap that the
5
+ // runner does NOT support — and rot silently, exactly the way a stale doc drifts
6
+ // from a live enum. (We watched this happen in a sibling system: a knowledge file
7
+ // kept naming post types the live enum had dropped, and a test pinned the stale
8
+ // names so the suite stayed green while the model was fed a wrong taxonomy.)
9
+ //
10
+ // This guard asserts that every capability IDENTIFIER referenced in authored
11
+ // prose is in the runner's LIVE capability set AND — for actions/venues — in THIS
12
+ // agent's enabled venues. It is deliberately deterministic and low-false-positive:
13
+ // it inspects only identifier-shaped tokens (snake_case `<venue>_<verb>` actions
14
+ // scanned anywhere; cap/venue identifiers scanned only inside markdown code
15
+ // spans), never free English. Runtime memory (journal/notes.md) is excluded — it
16
+ // records history, and may legitimately recall a venue later disabled.
17
+ import { VENUES, ACTION_TYPES, } from "./types.js";
18
+ import { levenshtein } from "./strictLint.js";
19
+ import { RISK_CAPS, LIMIT_CAPS } from "./mergeRules.js";
20
+ const KNOWN_ACTIONS = new Set(ACTION_TYPES);
21
+ // Every enforced cap the runner knows: risk + limits + killSwitch + abstention.
22
+ const LIVE_CAPS = [
23
+ ...Object.keys(RISK_CAPS),
24
+ ...Object.keys(LIMIT_CAPS),
25
+ "maxDrawdownMusd",
26
+ "maxConsecutiveRejects",
27
+ "maxConsecutiveModelFailures",
28
+ "onRateLimitPressure",
29
+ "onStaleData",
30
+ "onWeakSignal",
31
+ "onMissingQuote",
32
+ "onInsufficientBalance",
33
+ "minConfidence",
34
+ ];
35
+ const LIVE_CAP_SET = new Set(LIVE_CAPS);
36
+ // `<venue>_<verb>` action references. The verb is restricted to the runner's
37
+ // real verbs, so descriptive tokens (spot_price, futures_curve) never match —
38
+ // only action-shaped references (real OR plausibly-fake) do.
39
+ const ACTION_RE = /\b(spot|futures|pm)_(open|close|order|cancel|set_sltp)\b/g;
40
+ // camelCase cap-shaped identifiers (maxLeverage, minConfidence, requireStopLoss…).
41
+ const CAP_RE = /\b(?:max|min|require|on|per)[A-Z][A-Za-z0-9]+\b/g;
42
+ // markdown inline code spans — where authors put identifiers.
43
+ const CODESPAN_RE = /`([^`\n]+)`/g;
44
+ // Levenshtein threshold for "this is a typo/rename of a real cap" — matches the
45
+ // strictLint "did you mean" threshold (< 3, i.e. edit distance <= 2).
46
+ const CAP_NEARMISS_MAX = 3;
47
+ function venueOf(action) {
48
+ return action.split("_")[0];
49
+ }
50
+ // Authored knowledge only: keystone + character/* + skills/*. journal/notes.md is
51
+ // runtime memory and is intentionally NOT checked.
52
+ function authoredProse(resolved) {
53
+ return resolved.proseParts.filter((p) => p.source !== "journal/notes.md");
54
+ }
55
+ // Returns one issue per (token, source) drift found. Empty = no drift.
56
+ export function checkCapabilityDrift(resolved, spec) {
57
+ const issues = [];
58
+ const enabled = new Set(spec.venues);
59
+ const seen = new Set(); // dedupe "kind|token|source"
60
+ const once = (key) => {
61
+ if (seen.has(key))
62
+ return false;
63
+ seen.add(key);
64
+ return true;
65
+ };
66
+ for (const part of authoredProse(resolved)) {
67
+ const where = part.source;
68
+ // ── action references (anywhere in the body) ──
69
+ for (const m of part.text.matchAll(ACTION_RE)) {
70
+ const action = m[0];
71
+ if (!once(`a|${action}|${where}`))
72
+ continue;
73
+ if (!KNOWN_ACTIONS.has(action)) {
74
+ issues.push({
75
+ code: "drift_unknown_action",
76
+ path: where,
77
+ message: `prose references action \`${action}\`, which the runner does not support (allowed: ${ACTION_TYPES.join(", ")})`,
78
+ });
79
+ }
80
+ else {
81
+ const venue = venueOf(action);
82
+ if (!enabled.has(venue)) {
83
+ issues.push({
84
+ code: "drift_venue_not_enabled",
85
+ path: where,
86
+ message: `prose references \`${action}\` but venue \`${venue}\` is not in this agent's venues [${spec.venues.join(", ")}]`,
87
+ });
88
+ }
89
+ }
90
+ }
91
+ // ── venue + cap references inside code spans only ──
92
+ for (const span of part.text.matchAll(CODESPAN_RE)) {
93
+ const inner = span[1];
94
+ // An exact venue identifier in a code span (e.g. `pm`) that the agent has
95
+ // not enabled — a prose promise the runner cannot keep.
96
+ if (VENUES.includes(inner) &&
97
+ !enabled.has(inner) &&
98
+ once(`v|${inner}|${where}`)) {
99
+ issues.push({
100
+ code: "drift_venue_not_enabled",
101
+ path: where,
102
+ message: `prose references venue \`${inner}\` but it is not in this agent's venues [${spec.venues.join(", ")}]`,
103
+ });
104
+ }
105
+ // cap-shaped identifiers — flag only a near-miss of a REAL cap (a typo or
106
+ // rename); unrelated identifiers are left alone to avoid false positives.
107
+ for (const cm of inner.matchAll(CAP_RE)) {
108
+ const token = cm[0];
109
+ if (LIVE_CAP_SET.has(token))
110
+ continue;
111
+ let nearest = null;
112
+ let best = CAP_NEARMISS_MAX;
113
+ for (const cap of LIVE_CAPS) {
114
+ const d = levenshtein(token.toLowerCase(), cap.toLowerCase());
115
+ if (d < best) {
116
+ best = d;
117
+ nearest = cap;
118
+ }
119
+ }
120
+ if (nearest && once(`c|${token}|${where}`)) {
121
+ issues.push({
122
+ code: "drift_unknown_cap",
123
+ path: where,
124
+ message: `prose references cap \`${token}\`, which is not a runner cap — did you mean \`${nearest}\`?`,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return issues;
131
+ }
@@ -0,0 +1,21 @@
1
+ import { SkillValidationMode } from "./skillValidator.js";
2
+ export interface CmdResult {
3
+ ok: boolean;
4
+ code: number;
5
+ lines: string[];
6
+ data?: unknown;
7
+ }
8
+ export declare function cmdNew(targetPath: string, opts?: {
9
+ template?: string;
10
+ preset?: string;
11
+ }): CmdResult;
12
+ export declare function cmdValidate(path: string, mode?: SkillValidationMode): CmdResult;
13
+ export declare function cmdLock(path: string): CmdResult;
14
+ export declare function cmdEject(path: string): CmdResult;
15
+ export declare function cmdInspect(path: string, json?: boolean): CmdResult;
16
+ export declare function cmdRun(path: string, opts?: {
17
+ once?: boolean;
18
+ live?: boolean;
19
+ stateFile?: string;
20
+ }): Promise<CmdResult>;
21
+ export declare function main(argv: string[]): Promise<number>;