@coinrithm/mcp-trading 0.7.15 → 0.7.16

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 (53) hide show
  1. package/CHANGELOG.md +127 -8
  2. package/README.md +18 -3
  3. package/dist/agent/bench/bench.d.ts +59 -0
  4. package/dist/agent/bench/bench.js +319 -0
  5. package/dist/agent/bench/cassette.d.ts +97 -0
  6. package/dist/agent/bench/cassette.js +181 -0
  7. package/dist/agent/bench/costs.d.ts +28 -0
  8. package/dist/agent/bench/costs.js +60 -0
  9. package/dist/agent/bench/labelBuilder.d.ts +123 -0
  10. package/dist/agent/bench/labelBuilder.js +282 -0
  11. package/dist/agent/bench/labels.d.ts +75 -0
  12. package/dist/agent/bench/labels.js +259 -0
  13. package/dist/agent/bench/recordingClient.d.ts +51 -0
  14. package/dist/agent/bench/recordingClient.js +193 -0
  15. package/dist/agent/bench/replayClient.d.ts +41 -0
  16. package/dist/agent/bench/replayClient.js +262 -0
  17. package/dist/agent/bench/report.d.ts +115 -0
  18. package/dist/agent/bench/report.js +435 -0
  19. package/dist/agent/cli.d.ts +25 -0
  20. package/dist/agent/cli.js +300 -2
  21. package/dist/agent/client.d.ts +1 -0
  22. package/dist/agent/client.js +6 -0
  23. package/dist/agent/configurationWarnings.js +11 -0
  24. package/dist/agent/decision.d.ts +3 -0
  25. package/dist/agent/decision.js +85 -3
  26. package/dist/agent/decisionValidator.d.ts +30 -0
  27. package/dist/agent/decisionValidator.js +147 -2
  28. package/dist/agent/engine.d.ts +3 -3
  29. package/dist/agent/engine.js +3 -3
  30. package/dist/agent/observe.d.ts +27 -2
  31. package/dist/agent/observe.js +529 -89
  32. package/dist/agent/prompt.d.ts +4 -1
  33. package/dist/agent/prompt.js +98 -7
  34. package/dist/agent/promptTables.d.ts +1 -0
  35. package/dist/agent/promptTables.js +33 -0
  36. package/dist/agent/providerCapabilities.d.ts +6 -0
  37. package/dist/agent/providerCapabilities.js +23 -0
  38. package/dist/agent/providers.d.ts +25 -7
  39. package/dist/agent/providers.js +188 -48
  40. package/dist/agent/resolve.js +3 -0
  41. package/dist/agent/runner.d.ts +8 -0
  42. package/dist/agent/runner.js +59 -15
  43. package/dist/agent/setups.d.ts +2 -1
  44. package/dist/agent/setups.js +17 -16
  45. package/dist/agent/signals.d.ts +33 -0
  46. package/dist/agent/signals.js +89 -0
  47. package/dist/agent/skill.js +37 -0
  48. package/dist/agent/skillValidator.js +33 -0
  49. package/dist/agent/strictLint.js +29 -0
  50. package/dist/agent/types.d.ts +149 -12
  51. package/dist/agent/universe.d.ts +78 -0
  52. package/dist/agent/universe.js +215 -0
  53. package/package.json +3 -2
@@ -0,0 +1,78 @@
1
+ import type { UniverseConfig } from "./types.js";
2
+ export declare const UNIVERSE_SORTS: readonly ["gainers_24h", "losers_24h", "abs_change_24h", "abs_change_1h", "volume_24h", "rank"];
3
+ export type UniverseSort = (typeof UNIVERSE_SORTS)[number];
4
+ export declare const UNIVERSE_DEFAULTS: {
5
+ rankMin: number;
6
+ rankMax: number;
7
+ minVolume24hUsd: number;
8
+ sort: UniverseSort;
9
+ resolveTop: number;
10
+ scanLimit: number;
11
+ };
12
+ export declare const UNIVERSE_MAX_RESOLVE_TOP = 10;
13
+ export declare const UNIVERSE_MAX_SCAN_LIMIT = 50;
14
+ /** True when the agent discovers candidates beyond its watchlist. */
15
+ export declare function scansUniverse(spec: {
16
+ capabilities: readonly string[];
17
+ universe?: UniverseConfig;
18
+ }): boolean;
19
+ export interface UniverseQuery {
20
+ rankMin: number;
21
+ rankMax: number;
22
+ minVolume24hUsd: number;
23
+ excludeStablecoins: boolean;
24
+ includeSectors: string[];
25
+ excludeSectors: string[];
26
+ sort: UniverseSort;
27
+ limit: number;
28
+ }
29
+ /** The screener query a validated universe block asks for. */
30
+ export declare function universeQuery(u: UniverseConfig): UniverseQuery;
31
+ /** How many screener rows become full watch entries this cycle. */
32
+ export declare function universeResolveTop(u: UniverseConfig): number;
33
+ /** Wire form: csv sectors, string numbers; empty filters are omitted. With
34
+ * `ucids`, the same boundaries are applied to just those coins (membership). */
35
+ export declare function universeQueryParams(q: UniverseQuery, ucids?: readonly string[]): Record<string, string | number>;
36
+ export interface UniverseRow {
37
+ symbol: string;
38
+ coinId?: string;
39
+ name?: string;
40
+ slug?: string;
41
+ marketCapRank?: number;
42
+ priceUsd?: number;
43
+ change1h?: number;
44
+ change24h?: number;
45
+ change7d?: number;
46
+ volume24hUsd?: number;
47
+ sectors?: string[];
48
+ }
49
+ /** Keep malformed evidence unknown; dropping bad elements can widen a screen. */
50
+ export declare function universeSectorsOf(raw: unknown): string[] | undefined;
51
+ /**
52
+ * Defence in depth on server rows (never widen): a row is kept only when its
53
+ * own fields PROVE it is inside every enabled boundary. The rank band is
54
+ * always enabled, so a row without a rank is dropped; a volume floor needs a
55
+ * volume; any sector rule (stablecoins out, sectors out, sectors in) needs the
56
+ * row's sector list. Missing or malformed evidence drops the row (root review
57
+ * 2026-10-07: unverifiable is not inside).
58
+ */
59
+ export declare function filterUniverseRows(rows: UniverseRow[], q: UniverseQuery): UniverseRow[];
60
+ /**
61
+ * Entry eligibility under declared boundaries (root review 2026-10-07): with a
62
+ * `universe` block, a NEW position may only be opened on a coin proven inside
63
+ * the boundaries this cycle: a discovered row, or a watchlist coin the
64
+ * screener confirmed (`withinBoundaries: true`). Anything else (outside, or
65
+ * unverifiable because the check failed) stays visible as context and can
66
+ * still be closed or sold, but not entered. Returns the rejection reason, or
67
+ * null when the entry is allowed. No universe block = no extra rule.
68
+ */
69
+ export declare function universeEntryBlock(spec: {
70
+ universe?: UniverseConfig;
71
+ }, entry: {
72
+ discovered?: boolean;
73
+ withinBoundaries?: boolean;
74
+ }): string | null;
75
+ /** Validation issues for a raw `universe` block ([] when valid or absent). */
76
+ export declare function universeIssues(raw: unknown): Array<[string, string]>;
77
+ /** One line for the prompt: the boundaries in plain words. */
78
+ export declare function describeUniverse(u: UniverseConfig): string;
@@ -0,0 +1,215 @@
1
+ // Agent market BOUNDARIES (owner 2026-10-07, Telegram 56641/56644). Instead of
2
+ // a fixed coin list, a bundle declares where its agent may look:
3
+ //
4
+ // universe:
5
+ // rank: { min: 1, max: 100 } # market-cap rank band
6
+ // minVolume24hUsd: 1000000 # liquidity floor
7
+ // excludeStablecoins: true
8
+ // includeSectors: [defi, ai] # curated ids, GET /api/agent/universe/sectors
9
+ // excludeSectors: [meme]
10
+ // sort: abs_change_24h # what "an opportunity" means first
11
+ // resolveTop: 6 # rows resolved into full watch entries
12
+ //
13
+ // Each cycle observe() asks GET /api/agent/universe for the rows inside those
14
+ // boundaries, resolves the top few into full watch entries (marked
15
+ // `discovered: true`) and passes the rest as compact context. The validator's
16
+ // tradable set is watch membership, so a discovered row is tradable for that
17
+ // cycle and every cap still applies; the blocklist still wins. The server
18
+ // filters, and filterUniverseRows re-checks what the row says about itself,
19
+ // so a wrong or older server can narrow the set but never widen it.
20
+ export const UNIVERSE_SORTS = [
21
+ "gainers_24h",
22
+ "losers_24h",
23
+ "abs_change_24h",
24
+ "abs_change_1h",
25
+ "volume_24h",
26
+ "rank",
27
+ ];
28
+ export const UNIVERSE_DEFAULTS = {
29
+ rankMin: 1,
30
+ rankMax: 100,
31
+ minVolume24hUsd: 0,
32
+ sort: "abs_change_24h",
33
+ resolveTop: 6,
34
+ scanLimit: 15,
35
+ };
36
+ export const UNIVERSE_MAX_RESOLVE_TOP = 10;
37
+ export const UNIVERSE_MAX_SCAN_LIMIT = 50;
38
+ const SECTOR_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
39
+ /** True when the agent discovers candidates beyond its watchlist. */
40
+ export function scansUniverse(spec) {
41
+ return (spec.universe !== undefined || spec.capabilities.includes("universe_scan"));
42
+ }
43
+ /** The screener query a validated universe block asks for. */
44
+ export function universeQuery(u) {
45
+ return {
46
+ rankMin: u.rank?.min ?? UNIVERSE_DEFAULTS.rankMin,
47
+ rankMax: u.rank?.max ?? UNIVERSE_DEFAULTS.rankMax,
48
+ minVolume24hUsd: u.minVolume24hUsd ?? UNIVERSE_DEFAULTS.minVolume24hUsd,
49
+ excludeStablecoins: u.excludeStablecoins ?? false,
50
+ includeSectors: [...(u.includeSectors ?? [])],
51
+ excludeSectors: [...(u.excludeSectors ?? [])],
52
+ sort: u.sort ?? UNIVERSE_DEFAULTS.sort,
53
+ limit: u.scanLimit ?? UNIVERSE_DEFAULTS.scanLimit,
54
+ };
55
+ }
56
+ /** How many screener rows become full watch entries this cycle. */
57
+ export function universeResolveTop(u) {
58
+ return Math.min(u.resolveTop ?? UNIVERSE_DEFAULTS.resolveTop, UNIVERSE_MAX_RESOLVE_TOP);
59
+ }
60
+ /** Wire form: csv sectors, string numbers; empty filters are omitted. With
61
+ * `ucids`, the same boundaries are applied to just those coins (membership). */
62
+ export function universeQueryParams(q, ucids) {
63
+ return {
64
+ ...(ucids && ucids.length > 0 ? { ucids: ucids.join(",") } : {}),
65
+ rankMin: q.rankMin,
66
+ rankMax: q.rankMax,
67
+ minVolume24hUsd: q.minVolume24hUsd,
68
+ excludeStablecoins: q.excludeStablecoins ? "true" : "false",
69
+ ...(q.includeSectors.length
70
+ ? { includeSectors: q.includeSectors.join(",") }
71
+ : {}),
72
+ ...(q.excludeSectors.length
73
+ ? { excludeSectors: q.excludeSectors.join(",") }
74
+ : {}),
75
+ sort: q.sort,
76
+ limit: q.limit,
77
+ };
78
+ }
79
+ /** Keep malformed evidence unknown; dropping bad elements can widen a screen. */
80
+ export function universeSectorsOf(raw) {
81
+ return Array.isArray(raw) &&
82
+ raw.every((s) => typeof s === "string" && SECTOR_ID_RE.test(s))
83
+ ? raw
84
+ : undefined;
85
+ }
86
+ /**
87
+ * Defence in depth on server rows (never widen): a row is kept only when its
88
+ * own fields PROVE it is inside every enabled boundary. The rank band is
89
+ * always enabled, so a row without a rank is dropped; a volume floor needs a
90
+ * volume; any sector rule (stablecoins out, sectors out, sectors in) needs the
91
+ * row's sector list. Missing or malformed evidence drops the row (root review
92
+ * 2026-10-07: unverifiable is not inside).
93
+ */
94
+ export function filterUniverseRows(rows, q) {
95
+ const excluded = new Set([
96
+ ...q.excludeSectors,
97
+ ...(q.excludeStablecoins ? ["stablecoins"] : []),
98
+ ]);
99
+ const included = q.includeSectors.filter((s) => !excluded.has(s));
100
+ const sectorRules = excluded.size > 0 || q.includeSectors.length > 0;
101
+ return rows.filter((r) => {
102
+ const rank = r.marketCapRank;
103
+ if (typeof rank !== "number" || !Number.isFinite(rank))
104
+ return false;
105
+ if (rank < q.rankMin || rank > q.rankMax)
106
+ return false;
107
+ if (q.minVolume24hUsd > 0) {
108
+ const vol = r.volume24hUsd;
109
+ if (typeof vol !== "number" || !Number.isFinite(vol))
110
+ return false;
111
+ if (vol < q.minVolume24hUsd)
112
+ return false;
113
+ }
114
+ if (sectorRules) {
115
+ const sectors = universeSectorsOf(r.sectors);
116
+ if (sectors === undefined)
117
+ return false;
118
+ if (sectors.some((s) => excluded.has(s)))
119
+ return false;
120
+ if (q.includeSectors.length > 0 &&
121
+ !sectors.some((s) => included.includes(s)))
122
+ return false;
123
+ }
124
+ return true;
125
+ });
126
+ }
127
+ /**
128
+ * Entry eligibility under declared boundaries (root review 2026-10-07): with a
129
+ * `universe` block, a NEW position may only be opened on a coin proven inside
130
+ * the boundaries this cycle: a discovered row, or a watchlist coin the
131
+ * screener confirmed (`withinBoundaries: true`). Anything else (outside, or
132
+ * unverifiable because the check failed) stays visible as context and can
133
+ * still be closed or sold, but not entered. Returns the rejection reason, or
134
+ * null when the entry is allowed. No universe block = no extra rule.
135
+ */
136
+ export function universeEntryBlock(spec, entry) {
137
+ if (!spec.universe)
138
+ return null;
139
+ if (entry.discovered === true || entry.withinBoundaries === true)
140
+ return null;
141
+ return entry.withinBoundaries === false
142
+ ? "outside the declared market boundaries"
143
+ : "not verified inside the declared market boundaries this cycle";
144
+ }
145
+ /** Validation issues for a raw `universe` block ([] when valid or absent). */
146
+ export function universeIssues(raw) {
147
+ if (raw === undefined)
148
+ return [];
149
+ const issues = [];
150
+ const add = (code, reason) => issues.push([code, reason]);
151
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
152
+ add("skill_universe", "universe must be a mapping");
153
+ return issues;
154
+ }
155
+ const u = raw;
156
+ const int = (v, lo, hi) => typeof v === "number" && Number.isInteger(v) && v >= lo && v <= hi;
157
+ if (u.rank !== undefined) {
158
+ const rank = u.rank;
159
+ if (rank === null || typeof rank !== "object" || Array.isArray(rank)) {
160
+ add("skill_universe_rank", "universe.rank must be { min, max }");
161
+ }
162
+ else {
163
+ if (rank.min !== undefined && !int(rank.min, 1, 10_000))
164
+ add("skill_universe_rank", "universe.rank.min must be a whole number between 1 and 10000");
165
+ if (rank.max !== undefined && !int(rank.max, 1, 10_000))
166
+ add("skill_universe_rank", "universe.rank.max must be a whole number between 1 and 10000");
167
+ const min = rank.min ?? UNIVERSE_DEFAULTS.rankMin;
168
+ const max = rank.max ?? UNIVERSE_DEFAULTS.rankMax;
169
+ if (int(min, 1, 10_000) && int(max, 1, 10_000) && min > max)
170
+ add("skill_universe_rank", "universe.rank.min must be <= rank.max");
171
+ }
172
+ }
173
+ if (u.minVolume24hUsd !== undefined &&
174
+ !(typeof u.minVolume24hUsd === "number" &&
175
+ Number.isFinite(u.minVolume24hUsd) &&
176
+ u.minVolume24hUsd >= 0))
177
+ add("skill_universe_volume", "universe.minVolume24hUsd must be a number >= 0");
178
+ if (u.excludeStablecoins !== undefined &&
179
+ typeof u.excludeStablecoins !== "boolean")
180
+ add("skill_universe_stablecoins", "universe.excludeStablecoins must be true or false");
181
+ for (const key of ["includeSectors", "excludeSectors"]) {
182
+ const v = u[key];
183
+ if (v === undefined)
184
+ continue;
185
+ if (!Array.isArray(v) ||
186
+ v.length > 50 ||
187
+ !v.every((s) => typeof s === "string" && SECTOR_ID_RE.test(s)))
188
+ add("skill_universe_sectors", `universe.${key} must be a list of sector ids like "defi" or "eco-solana" (GET /api/agent/universe/sectors)`);
189
+ }
190
+ if (u.sort !== undefined &&
191
+ !UNIVERSE_SORTS.includes(u.sort))
192
+ add("skill_universe_sort", `universe.sort must be one of ${UNIVERSE_SORTS.join(", ")}`);
193
+ if (u.resolveTop !== undefined &&
194
+ !int(u.resolveTop, 1, UNIVERSE_MAX_RESOLVE_TOP))
195
+ add("skill_universe_resolve_top", `universe.resolveTop must be a whole number between 1 and ${UNIVERSE_MAX_RESOLVE_TOP}`);
196
+ if (u.scanLimit !== undefined &&
197
+ !int(u.scanLimit, 1, UNIVERSE_MAX_SCAN_LIMIT))
198
+ add("skill_universe_scan_limit", `universe.scanLimit must be a whole number between 1 and ${UNIVERSE_MAX_SCAN_LIMIT}`);
199
+ return issues;
200
+ }
201
+ /** One line for the prompt: the boundaries in plain words. */
202
+ export function describeUniverse(u) {
203
+ const q = universeQuery(u);
204
+ const parts = [`market-cap rank ${q.rankMin}-${q.rankMax}`];
205
+ if (q.minVolume24hUsd > 0)
206
+ parts.push(`24h volume >= $${Math.round(q.minVolume24hUsd).toLocaleString("en-US")}`);
207
+ if (q.excludeStablecoins)
208
+ parts.push("no stablecoins");
209
+ if (q.includeSectors.length)
210
+ parts.push(`only sectors: ${q.includeSectors.join(", ")}`);
211
+ if (q.excludeSectors.length)
212
+ parts.push(`excluding sectors: ${q.excludeSectors.join(", ")}`);
213
+ parts.push(`ranked by ${q.sort.replace(/_/g, " ")}`);
214
+ return parts.join("; ");
215
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinrithm/mcp-trading",
3
- "version": "0.7.15",
3
+ "version": "0.7.16",
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",
@@ -69,8 +69,9 @@
69
69
  ],
70
70
  "license": "MIT",
71
71
  "dependencies": {
72
- "@modelcontextprotocol/sdk": "^1.0.0",
72
+ "@modelcontextprotocol/sdk": "^1.31.0",
73
73
  "express": "^4.21.0",
74
+ "json5": "2.2.3",
74
75
  "yaml": "^2.5.0",
75
76
  "zod": "^3.23.8"
76
77
  },