@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.
- package/CHANGELOG.md +127 -8
- package/README.md +18 -3
- package/dist/agent/bench/bench.d.ts +59 -0
- package/dist/agent/bench/bench.js +319 -0
- package/dist/agent/bench/cassette.d.ts +97 -0
- package/dist/agent/bench/cassette.js +181 -0
- package/dist/agent/bench/costs.d.ts +28 -0
- package/dist/agent/bench/costs.js +60 -0
- package/dist/agent/bench/labelBuilder.d.ts +123 -0
- package/dist/agent/bench/labelBuilder.js +282 -0
- package/dist/agent/bench/labels.d.ts +75 -0
- package/dist/agent/bench/labels.js +259 -0
- package/dist/agent/bench/recordingClient.d.ts +51 -0
- package/dist/agent/bench/recordingClient.js +193 -0
- package/dist/agent/bench/replayClient.d.ts +41 -0
- package/dist/agent/bench/replayClient.js +262 -0
- package/dist/agent/bench/report.d.ts +115 -0
- package/dist/agent/bench/report.js +435 -0
- package/dist/agent/cli.d.ts +25 -0
- package/dist/agent/cli.js +300 -2
- package/dist/agent/client.d.ts +1 -0
- package/dist/agent/client.js +6 -0
- package/dist/agent/configurationWarnings.js +11 -0
- package/dist/agent/decision.d.ts +3 -0
- package/dist/agent/decision.js +85 -3
- package/dist/agent/decisionValidator.d.ts +30 -0
- package/dist/agent/decisionValidator.js +147 -2
- package/dist/agent/engine.d.ts +3 -3
- package/dist/agent/engine.js +3 -3
- package/dist/agent/observe.d.ts +27 -2
- package/dist/agent/observe.js +529 -89
- package/dist/agent/prompt.d.ts +4 -1
- package/dist/agent/prompt.js +98 -7
- package/dist/agent/promptTables.d.ts +1 -0
- package/dist/agent/promptTables.js +33 -0
- package/dist/agent/providerCapabilities.d.ts +6 -0
- package/dist/agent/providerCapabilities.js +23 -0
- package/dist/agent/providers.d.ts +25 -7
- package/dist/agent/providers.js +188 -48
- package/dist/agent/resolve.js +3 -0
- package/dist/agent/runner.d.ts +8 -0
- package/dist/agent/runner.js +59 -15
- package/dist/agent/setups.d.ts +2 -1
- package/dist/agent/setups.js +17 -16
- package/dist/agent/signals.d.ts +33 -0
- package/dist/agent/signals.js +89 -0
- package/dist/agent/skill.js +37 -0
- package/dist/agent/skillValidator.js +33 -0
- package/dist/agent/strictLint.js +29 -0
- package/dist/agent/types.d.ts +149 -12
- package/dist/agent/universe.d.ts +78 -0
- package/dist/agent/universe.js +215 -0
- 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.
|
|
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.
|
|
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
|
},
|