@coinrithm/mcp-trading 0.7.14 → 0.7.15
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 +692 -613
- package/README.md +400 -366
- package/dist/agent/client.d.ts +13 -0
- package/dist/agent/client.js +107 -4
- package/dist/agent/decisionReceipt.d.ts +5 -1
- package/dist/agent/decisionReceipt.js +52 -3
- package/dist/agent/futuresEligibility.d.ts +36 -0
- package/dist/agent/futuresEligibility.js +90 -0
- package/dist/agent/observe.d.ts +9 -1
- package/dist/agent/observe.js +443 -42
- package/dist/agent/pmContext.d.ts +10 -1
- package/dist/agent/pmContext.js +163 -1
- package/dist/agent/prompt.d.ts +14 -0
- package/dist/agent/prompt.js +131 -26
- package/dist/agent/providerCapabilities.d.ts +3 -0
- package/dist/agent/providerCapabilities.js +34 -4
- package/dist/agent/providers.d.ts +3 -0
- package/dist/agent/providers.js +12 -2
- package/dist/agent/runner.js +100 -11
- package/dist/agent/templates.js +30 -30
- package/dist/agent/types.d.ts +117 -1
- package/dist/agent/types.js +4 -0
- package/dist/client.d.ts +9 -2
- package/dist/client.js +22 -10
- package/dist/http.d.ts +1 -0
- package/dist/http.js +17 -2
- package/dist/httpCompletion.d.ts +1 -1
- package/dist/httpCompletion.js +1 -0
- package/dist/index.js +2 -1
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +16 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +150 -66
- package/package.json +97 -97
package/dist/agent/client.d.ts
CHANGED
|
@@ -40,13 +40,26 @@ export declare class CoinRithmClient {
|
|
|
40
40
|
rateLimitHits: number;
|
|
41
41
|
constructor(cfg: ClientConfig);
|
|
42
42
|
private request;
|
|
43
|
+
private publicRequest;
|
|
44
|
+
credentialFingerprint(): string;
|
|
43
45
|
me(trace?: AgentTrace): Promise<ApiResult>;
|
|
46
|
+
performance(trace?: AgentTrace, options?: {
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
maxRetries?: number;
|
|
49
|
+
}): Promise<ApiResult>;
|
|
44
50
|
portfolio(trace?: AgentTrace): Promise<ApiResult>;
|
|
45
51
|
wallet(query?: {
|
|
46
52
|
coinId?: string;
|
|
47
53
|
}, trace?: AgentTrace): Promise<ApiResult>;
|
|
48
54
|
resolve(q: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
49
55
|
cryptoMovers(direction: "gainers" | "losers", limit: number, trace?: AgentTrace): Promise<ApiResult>;
|
|
56
|
+
getPublicPmWhales(options?: {
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
limit?: number;
|
|
59
|
+
}): Promise<ApiResult>;
|
|
60
|
+
getPublicPmWhaleWallet(source: string, wallet: string, options?: {
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
}): Promise<ApiResult>;
|
|
50
63
|
market(coinId: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
51
64
|
candles(coinId: string, range: string, trace?: AgentTrace): Promise<ApiResult>;
|
|
52
65
|
trades(query?: {
|
package/dist/agent/client.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// from an agent file). 429 backs off on Retry-After; 401/403/409/422 are
|
|
5
5
|
// FAIL-CLOSED cycle outcomes (returned, not retried). fetch + sleep are
|
|
6
6
|
// injectable so tests run with no network and no real waits.
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
8
9
|
import { retryAfterSeconds } from "../retryAfter.js";
|
|
9
10
|
export const DEFAULT_BASE_URL = "https://api.coinrithm.com";
|
|
@@ -54,6 +55,16 @@ export class CoinRithmClient {
|
|
|
54
55
|
this.extraHeaders = cfg.extraHeaders;
|
|
55
56
|
}
|
|
56
57
|
async request(method, path, opts = {}) {
|
|
58
|
+
const requestTimeoutMs = opts.timeoutMs !== undefined &&
|
|
59
|
+
Number.isSafeInteger(opts.timeoutMs) &&
|
|
60
|
+
opts.timeoutMs >= 1
|
|
61
|
+
? Math.min(opts.timeoutMs, this.requestTimeoutMs)
|
|
62
|
+
: this.requestTimeoutMs;
|
|
63
|
+
const maxRetries = opts.maxRetries !== undefined &&
|
|
64
|
+
Number.isSafeInteger(opts.maxRetries) &&
|
|
65
|
+
opts.maxRetries >= 0
|
|
66
|
+
? Math.min(opts.maxRetries, this.maxRetries)
|
|
67
|
+
: this.maxRetries;
|
|
57
68
|
const url = new URL(this.baseUrl + path);
|
|
58
69
|
if (opts.query) {
|
|
59
70
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -76,7 +87,7 @@ export class CoinRithmClient {
|
|
|
76
87
|
const timer = setTimeout(() => {
|
|
77
88
|
timedOut = true;
|
|
78
89
|
controller.abort(new Error("API request deadline exceeded"));
|
|
79
|
-
},
|
|
90
|
+
}, requestTimeoutMs);
|
|
80
91
|
this.signal?.addEventListener("abort", cancel, { once: true });
|
|
81
92
|
if (this.signal?.aborted)
|
|
82
93
|
cancel();
|
|
@@ -102,12 +113,12 @@ export class CoinRithmClient {
|
|
|
102
113
|
const retryAfter = retryAfterSeconds(res.headers.get("retry-after"));
|
|
103
114
|
if (res.status === 429)
|
|
104
115
|
this.rateLimitHits += 1;
|
|
105
|
-
if (res.status === 429 && attempt <
|
|
116
|
+
if (res.status === 429 && attempt < maxRetries) {
|
|
106
117
|
// Release this response before waiting so retries do not retain sockets.
|
|
107
118
|
void res.body?.cancel().catch(() => { });
|
|
108
119
|
const delayMs = (retryAfter ?? 5) * 1000;
|
|
109
120
|
// Never shorten a provider's Retry-After or overflow a Node timer.
|
|
110
|
-
if (delayMs >=
|
|
121
|
+
if (delayMs >= requestTimeoutMs)
|
|
111
122
|
await aborted;
|
|
112
123
|
else if (this.sleepFn)
|
|
113
124
|
await this.sleepFn(delayMs);
|
|
@@ -151,7 +162,7 @@ export class CoinRithmClient {
|
|
|
151
162
|
? "request_aborted"
|
|
152
163
|
: "network_error",
|
|
153
164
|
message: timedOut
|
|
154
|
-
? `API request exceeded ${
|
|
165
|
+
? `API request exceeded ${requestTimeoutMs}ms deadline`
|
|
155
166
|
: controller.signal.aborted
|
|
156
167
|
? "API request cancelled"
|
|
157
168
|
: err instanceof Error
|
|
@@ -166,10 +177,96 @@ export class CoinRithmClient {
|
|
|
166
177
|
controller.signal.removeEventListener("abort", rejectAborted);
|
|
167
178
|
}
|
|
168
179
|
}
|
|
180
|
+
// Keyless, bounded reads against the public PM surface. These deliberately
|
|
181
|
+
// bypass the authenticated request path so a runner key cannot leak into a
|
|
182
|
+
// public context lookup. No retries are added here; the enrichment is
|
|
183
|
+
// optional and its deadline includes response-body consumption.
|
|
184
|
+
async publicRequest(path, query, timeoutMs = 5_000) {
|
|
185
|
+
const url = new URL(this.baseUrl + path);
|
|
186
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
187
|
+
if (value !== undefined && value !== null && value !== "")
|
|
188
|
+
url.searchParams.set(key, String(value));
|
|
189
|
+
}
|
|
190
|
+
const controller = new AbortController();
|
|
191
|
+
if (this.signal?.aborted) {
|
|
192
|
+
return { ok: false, status: 0, data: { error: "network_error" } };
|
|
193
|
+
}
|
|
194
|
+
const cancel = () => controller.abort();
|
|
195
|
+
this.signal?.addEventListener("abort", cancel, { once: true });
|
|
196
|
+
const deadlineMs = Math.min(timeoutMs, this.requestTimeoutMs);
|
|
197
|
+
let timedOut = false;
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
timedOut = true;
|
|
200
|
+
controller.abort();
|
|
201
|
+
}, deadlineMs);
|
|
202
|
+
let rejectAborted;
|
|
203
|
+
const aborted = new Promise((_, reject) => {
|
|
204
|
+
rejectAborted = () => reject(new Error("public request aborted"));
|
|
205
|
+
controller.signal.addEventListener("abort", rejectAborted, {
|
|
206
|
+
once: true,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
if (this.signal?.aborted)
|
|
210
|
+
cancel();
|
|
211
|
+
try {
|
|
212
|
+
const perform = async () => {
|
|
213
|
+
const res = await this.fetchFn(url.toString(), {
|
|
214
|
+
method: "GET",
|
|
215
|
+
headers: { Accept: "application/json" },
|
|
216
|
+
signal: controller.signal,
|
|
217
|
+
});
|
|
218
|
+
const text = await res.text();
|
|
219
|
+
let data = text;
|
|
220
|
+
if (text) {
|
|
221
|
+
try {
|
|
222
|
+
data = JSON.parse(text);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
/* leave as text */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { ok: res.ok, status: res.status, data };
|
|
229
|
+
};
|
|
230
|
+
return await Promise.race([perform(), aborted]);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
status: 0,
|
|
236
|
+
data: {
|
|
237
|
+
error: timedOut ? "timeout" : "network_error",
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
controller.signal.removeEventListener("abort", rejectAborted);
|
|
244
|
+
this.signal?.removeEventListener("abort", cancel);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// A stable, non-reversible identity for this credential and API base, for
|
|
248
|
+
// per-agent in-memory caches that must outlive one client instance (the
|
|
249
|
+
// hosted scheduler builds a new client every cycle). Never the key itself.
|
|
250
|
+
credentialFingerprint() {
|
|
251
|
+
return createHash("sha256")
|
|
252
|
+
.update(`${this.baseUrl}\n${this.apiKey}`, "utf8")
|
|
253
|
+
.digest("hex")
|
|
254
|
+
.slice(0, 32);
|
|
255
|
+
}
|
|
169
256
|
// ── reads ──────────────────────────────────────────────────────────────────
|
|
170
257
|
me(trace) {
|
|
171
258
|
return this.request("GET", "/api/agent/me", { trace });
|
|
172
259
|
}
|
|
260
|
+
// GET /api/agent/performance: the calling key's own realized performance.
|
|
261
|
+
// Newer backends add an optional `pmCalibration` block (the key's settled PM
|
|
262
|
+
// forecast record), parsed defensively in observe.
|
|
263
|
+
performance(trace, options) {
|
|
264
|
+
return this.request("GET", "/api/agent/performance", {
|
|
265
|
+
trace,
|
|
266
|
+
timeoutMs: options?.timeoutMs,
|
|
267
|
+
maxRetries: options?.maxRetries,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
173
270
|
portfolio(trace) {
|
|
174
271
|
return this.request("GET", "/api/agent/portfolio", { trace });
|
|
175
272
|
}
|
|
@@ -186,6 +283,12 @@ export class CoinRithmClient {
|
|
|
186
283
|
? "/api/coins/top-losers"
|
|
187
284
|
: "/api/coins/top-gainers", { query: { limit }, trace });
|
|
188
285
|
}
|
|
286
|
+
getPublicPmWhales(options) {
|
|
287
|
+
return this.publicRequest("/api/prediction-markets/whales", { limit: options?.limit }, options?.timeoutMs);
|
|
288
|
+
}
|
|
289
|
+
getPublicPmWhaleWallet(source, wallet, options) {
|
|
290
|
+
return this.publicRequest(`/api/prediction-markets/whales/wallets/${encodeURIComponent(source)}/${encodeURIComponent(wallet)}`, undefined, options?.timeoutMs);
|
|
291
|
+
}
|
|
189
292
|
market(coinId, trace) {
|
|
190
293
|
return this.request("GET", `/api/agent/market/${encodeURIComponent(coinId)}`, { trace });
|
|
191
294
|
}
|
|
@@ -8,7 +8,7 @@ interface Row {
|
|
|
8
8
|
}
|
|
9
9
|
export interface DecisionInputRecord {
|
|
10
10
|
version: "coinrithm.decision-input.v1";
|
|
11
|
-
projectionVersion?: "coinrithm.decision-input-projection.v2";
|
|
11
|
+
projectionVersion?: "coinrithm.decision-input-projection.v2" | "coinrithm.decision-input-projection.v3";
|
|
12
12
|
visibility: "private";
|
|
13
13
|
completeness: "partial";
|
|
14
14
|
phase: DecisionInputPhase;
|
|
@@ -21,6 +21,10 @@ export interface DecisionInputRecord {
|
|
|
21
21
|
dailyRiskBudget: DailyRiskBudget | null;
|
|
22
22
|
guardState: Record<string, number | boolean | null>;
|
|
23
23
|
account: Record<string, number | boolean | string | null> | null;
|
|
24
|
+
marketMood?: {
|
|
25
|
+
fearGreed: number | null;
|
|
26
|
+
fetchedAt: string | null;
|
|
27
|
+
} | null;
|
|
24
28
|
lists: Record<string, Row[]>;
|
|
25
29
|
counts: Record<string, {
|
|
26
30
|
source: number;
|
|
@@ -22,6 +22,20 @@ const fingerprint = (value) => typeof value === "string" && /^sha256:[a-f0-9]{64
|
|
|
22
22
|
? value
|
|
23
23
|
: null;
|
|
24
24
|
const numeric = (raw, keys) => Object.fromEntries(keys.map((key) => [key, num(raw[key])]));
|
|
25
|
+
const count = (value) => Number.isSafeInteger(value) && value >= 0
|
|
26
|
+
? value
|
|
27
|
+
: null;
|
|
28
|
+
const fearGreed = (value) => {
|
|
29
|
+
const v = num(value);
|
|
30
|
+
return v !== null && v >= 0 && v <= 100 ? v : null;
|
|
31
|
+
};
|
|
32
|
+
const PROJECTION_V3 = "coinrithm.decision-input-projection.v3";
|
|
33
|
+
// Watch-row fields that exist only from projection v3 on.
|
|
34
|
+
const SENTIMENT_V3_KEYS = [
|
|
35
|
+
"sentimentTotalVotes",
|
|
36
|
+
"sentimentDayUtc",
|
|
37
|
+
"sentimentUpdatedAt",
|
|
38
|
+
];
|
|
25
39
|
const freshness = (value) => {
|
|
26
40
|
const raw = obj(parseFreshness({ freshness: value }));
|
|
27
41
|
return {
|
|
@@ -49,7 +63,7 @@ export function buildDecisionInputRecord(input) {
|
|
|
49
63
|
const budget = buildDailyRiskBudget(input.spec, input.state);
|
|
50
64
|
const record = {
|
|
51
65
|
version: "coinrithm.decision-input.v1",
|
|
52
|
-
projectionVersion:
|
|
66
|
+
projectionVersion: PROJECTION_V3,
|
|
53
67
|
visibility: "private",
|
|
54
68
|
completeness: "partial",
|
|
55
69
|
phase: input.phase,
|
|
@@ -115,6 +129,12 @@ export function buildDecisionInputRecord(input) {
|
|
|
115
129
|
equityMusd: num(obs.equityMusd),
|
|
116
130
|
polledBeforeWrite: bool(obs.polledBeforeWrite),
|
|
117
131
|
};
|
|
132
|
+
record.marketMood = obs.marketMood
|
|
133
|
+
? {
|
|
134
|
+
fearGreed: fearGreed(obs.marketMood.fearGreed),
|
|
135
|
+
fetchedAt: sourceTimestamp(obs.marketMood.fetchedAt) ?? null,
|
|
136
|
+
}
|
|
137
|
+
: null;
|
|
118
138
|
const add = (name, items, project) => {
|
|
119
139
|
const all = arr(items);
|
|
120
140
|
// Bound work/space independently of the upstream list limit. Preserve order,
|
|
@@ -137,6 +157,10 @@ export function buildDecisionInputRecord(input) {
|
|
|
137
157
|
"change7d",
|
|
138
158
|
"sentimentBullishPct",
|
|
139
159
|
]),
|
|
160
|
+
// The sample and its own clocks; null = unknown, never "fresh".
|
|
161
|
+
sentimentTotalVotes: count(r.sentimentTotalVotes),
|
|
162
|
+
sentimentDayUtc: sourceTimestamp(r.sentimentDayUtc) ?? null,
|
|
163
|
+
sentimentUpdatedAt: sourceTimestamp(r.sentimentUpdatedAt) ?? null,
|
|
140
164
|
freshness: freshness(r.freshness),
|
|
141
165
|
...(r.indicatorContext
|
|
142
166
|
? {
|
|
@@ -357,6 +381,7 @@ const LIST_KEYS = {
|
|
|
357
381
|
"change24h",
|
|
358
382
|
"change7d",
|
|
359
383
|
"sentimentBullishPct",
|
|
384
|
+
...SENTIMENT_V3_KEYS,
|
|
360
385
|
"freshness",
|
|
361
386
|
"indicators",
|
|
362
387
|
"indicatorContext",
|
|
@@ -467,8 +492,16 @@ function validRow(value, keys) {
|
|
|
467
492
|
return true;
|
|
468
493
|
if (NESTED_KEYS[key])
|
|
469
494
|
return validRow(v, NESTED_KEYS[key]);
|
|
470
|
-
if (
|
|
495
|
+
if ([
|
|
496
|
+
"asOf",
|
|
497
|
+
"assessedAt",
|
|
498
|
+
"openedAt",
|
|
499
|
+
"sentimentDayUtc",
|
|
500
|
+
"sentimentUpdatedAt",
|
|
501
|
+
].includes(key))
|
|
471
502
|
return sourceTimestamp(v) === v;
|
|
503
|
+
if (key === "sentimentTotalVotes")
|
|
504
|
+
return count(v) !== null;
|
|
472
505
|
if (key === "basis")
|
|
473
506
|
return code(v, FRESHNESS_BASES) !== null;
|
|
474
507
|
if (key === "range")
|
|
@@ -565,13 +598,29 @@ export function sanitizeDecisionInputRecord(value) {
|
|
|
565
598
|
"dailyRiskBudget",
|
|
566
599
|
"guardState",
|
|
567
600
|
"account",
|
|
601
|
+
"marketMood",
|
|
568
602
|
"lists",
|
|
569
603
|
"counts",
|
|
570
604
|
"omissions",
|
|
571
605
|
]))
|
|
572
606
|
return undefined;
|
|
573
607
|
if (r.projectionVersion !== undefined &&
|
|
574
|
-
r.projectionVersion !== "coinrithm.decision-input-projection.v2"
|
|
608
|
+
r.projectionVersion !== "coinrithm.decision-input-projection.v2" &&
|
|
609
|
+
r.projectionVersion !== PROJECTION_V3)
|
|
610
|
+
return undefined;
|
|
611
|
+
// An older record must never carry, or be mistaken for, v3 fields.
|
|
612
|
+
const v3 = r.projectionVersion === PROJECTION_V3;
|
|
613
|
+
if (!v3 &&
|
|
614
|
+
(r.marketMood !== undefined ||
|
|
615
|
+
arr(obj(r.lists).watch).some((row) => SENTIMENT_V3_KEYS.some((k) => k in obj(row)))))
|
|
616
|
+
return undefined;
|
|
617
|
+
if (r.marketMood !== undefined &&
|
|
618
|
+
r.marketMood !== null &&
|
|
619
|
+
(!keysOnly(r.marketMood, ["fearGreed", "fetchedAt"]) ||
|
|
620
|
+
(r.marketMood.fearGreed !== null &&
|
|
621
|
+
fearGreed(r.marketMood.fearGreed) === null) ||
|
|
622
|
+
(r.marketMood.fetchedAt !== null &&
|
|
623
|
+
sourceTimestamp(r.marketMood.fetchedAt) !== r.marketMood.fetchedAt)))
|
|
575
624
|
return undefined;
|
|
576
625
|
if (r.version !== "coinrithm.decision-input.v1" ||
|
|
577
626
|
r.visibility !== "private" ||
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Observation, ProposedAction } from "./types.js";
|
|
2
|
+
export type FuturesEntryEligibilityStatus = "eligible" | "reference_stale" | "reference_unavailable";
|
|
3
|
+
export interface FuturesEntryEligibility {
|
|
4
|
+
status: FuturesEntryEligibilityStatus;
|
|
5
|
+
/** Whether the server gate currently requires a reference for NEW opens. */
|
|
6
|
+
referenceRequired: boolean;
|
|
7
|
+
venue: string | null;
|
|
8
|
+
symbol: string | null;
|
|
9
|
+
referenceFetchedAt: string | null;
|
|
10
|
+
maxReferenceAgeHours: number;
|
|
11
|
+
evaluatedAt: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Strict parse of the market context field against its contract (backend-v2
|
|
15
|
+
* #106). Anything missing or malformed (an older API, a partial response, an
|
|
16
|
+
* unknown status, a non-boolean referenceRequired, a missing or invalid
|
|
17
|
+
* evaluatedAt, a non-positive or non-finite maxReferenceAgeHours, or a present
|
|
18
|
+
* but invalid referenceFetchedAt) is `undefined` = UNKNOWN. Unknown never
|
|
19
|
+
* blocks: the runner keeps today's behaviour and the server gate decides.
|
|
20
|
+
*/
|
|
21
|
+
export declare function futuresEntryEligibilityOf(marketContext: unknown): FuturesEntryEligibility | undefined;
|
|
22
|
+
export interface FuturesEntryPreflightRejection {
|
|
23
|
+
code: "futures_reference_unavailable" | "futures_reference_stale";
|
|
24
|
+
reason: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Pre-quote check for a NEW futures open. Returns a rejection only when the
|
|
28
|
+
* observation EXPLICITLY reports that the server gate requires a reference and
|
|
29
|
+
* has none (or only a stale one) for this coin. Everything else passes:
|
|
30
|
+
* - non-futures_open actions (spot, PM, futures_close / futures_set_sltp);
|
|
31
|
+
* - any open futures position on the same coin (the open is an add or position
|
|
32
|
+
* management, which the server never refuses for the reference);
|
|
33
|
+
* - unknown eligibility (field absent on an older API);
|
|
34
|
+
* - referenceRequired false.
|
|
35
|
+
*/
|
|
36
|
+
export declare function futuresEntryPreflight(action: ProposedAction, observation: Observation): FuturesEntryPreflightRejection | null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Futures entry eligibility (API >= backend-v2 #106): GET /api/agent/market/{coinId}
|
|
2
|
+
// carries `futuresEntryEligibility`, what the server entry gate's own
|
|
3
|
+
// perpetual-reference rule says about a NEW futures open on the coin. Before it,
|
|
4
|
+
// universe_scan surfaced coins with no supported perpetual reference (tokenized
|
|
5
|
+
// ETFs such as SLVON/NAKA) and the runner spent a futures quote only to be
|
|
6
|
+
// refused with perpetual_reference_unavailable (J-74). The server gate stays
|
|
7
|
+
// authoritative: eligibility can change between observation and execution.
|
|
8
|
+
import { asObj, asStr } from "./extract.js";
|
|
9
|
+
import { baseSymbol } from "./setups.js";
|
|
10
|
+
const STATUSES = [
|
|
11
|
+
"eligible",
|
|
12
|
+
"reference_stale",
|
|
13
|
+
"reference_unavailable",
|
|
14
|
+
];
|
|
15
|
+
// A zoned ISO 8601 date-time (the contract's `format: date-time`):
|
|
16
|
+
// YYYY-MM-DDTHH:MM[:SS[.fraction]] with Z or a +/-HH:MM offset. Date-only,
|
|
17
|
+
// zone-less and bare numbers ("0") are rejected even though Date.parse
|
|
18
|
+
// accepts them.
|
|
19
|
+
const ZONED_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
20
|
+
const isIsoDate = (v) => typeof v === "string" &&
|
|
21
|
+
ZONED_DATE_TIME.test(v) &&
|
|
22
|
+
Number.isFinite(Date.parse(v));
|
|
23
|
+
/**
|
|
24
|
+
* Strict parse of the market context field against its contract (backend-v2
|
|
25
|
+
* #106). Anything missing or malformed (an older API, a partial response, an
|
|
26
|
+
* unknown status, a non-boolean referenceRequired, a missing or invalid
|
|
27
|
+
* evaluatedAt, a non-positive or non-finite maxReferenceAgeHours, or a present
|
|
28
|
+
* but invalid referenceFetchedAt) is `undefined` = UNKNOWN. Unknown never
|
|
29
|
+
* blocks: the runner keeps today's behaviour and the server gate decides.
|
|
30
|
+
*/
|
|
31
|
+
export function futuresEntryEligibilityOf(marketContext) {
|
|
32
|
+
const raw = asObj(asObj(marketContext).futuresEntryEligibility);
|
|
33
|
+
const status = asStr(raw.status);
|
|
34
|
+
if (!status || !STATUSES.includes(status))
|
|
35
|
+
return undefined;
|
|
36
|
+
if (typeof raw.referenceRequired !== "boolean")
|
|
37
|
+
return undefined;
|
|
38
|
+
const hours = raw.maxReferenceAgeHours;
|
|
39
|
+
if (typeof hours !== "number" || !Number.isFinite(hours) || hours <= 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (!isIsoDate(raw.evaluatedAt))
|
|
42
|
+
return undefined;
|
|
43
|
+
const fetchedAt = raw.referenceFetchedAt;
|
|
44
|
+
if (fetchedAt != null && !isIsoDate(fetchedAt))
|
|
45
|
+
return undefined;
|
|
46
|
+
return {
|
|
47
|
+
status,
|
|
48
|
+
referenceRequired: raw.referenceRequired,
|
|
49
|
+
venue: asStr(raw.venue) ?? null,
|
|
50
|
+
symbol: asStr(raw.symbol) ?? null,
|
|
51
|
+
referenceFetchedAt: fetchedAt ?? null,
|
|
52
|
+
maxReferenceAgeHours: hours,
|
|
53
|
+
evaluatedAt: raw.evaluatedAt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pre-quote check for a NEW futures open. Returns a rejection only when the
|
|
58
|
+
* observation EXPLICITLY reports that the server gate requires a reference and
|
|
59
|
+
* has none (or only a stale one) for this coin. Everything else passes:
|
|
60
|
+
* - non-futures_open actions (spot, PM, futures_close / futures_set_sltp);
|
|
61
|
+
* - any open futures position on the same coin (the open is an add or position
|
|
62
|
+
* management, which the server never refuses for the reference);
|
|
63
|
+
* - unknown eligibility (field absent on an older API);
|
|
64
|
+
* - referenceRequired false.
|
|
65
|
+
*/
|
|
66
|
+
export function futuresEntryPreflight(action, observation) {
|
|
67
|
+
if (action.type !== "futures_open")
|
|
68
|
+
return null;
|
|
69
|
+
const base = baseSymbol(action.symbol);
|
|
70
|
+
const holdsCoin = (observation.openPositions ?? []).some((p) => p.venue === "futures" &&
|
|
71
|
+
(p.status ?? "open") === "open" &&
|
|
72
|
+
baseSymbol(p.symbol) === base);
|
|
73
|
+
if (holdsCoin)
|
|
74
|
+
return null;
|
|
75
|
+
const entry = observation.watch.find((w) => baseSymbol(w.symbol) === base);
|
|
76
|
+
const e = entry?.futuresEntryEligibility;
|
|
77
|
+
if (!e || !e.referenceRequired || e.status === "eligible")
|
|
78
|
+
return null;
|
|
79
|
+
const when = ` at ${e.evaluatedAt}`;
|
|
80
|
+
if (e.status === "reference_stale") {
|
|
81
|
+
return {
|
|
82
|
+
code: "futures_reference_stale",
|
|
83
|
+
reason: `${action.symbol}: the perpetual reference${e.venue ? ` (${e.venue})` : ""} is stale or unusable for a NEW futures open${when} (too old, or its refresh time is missing, invalid or in the future); the server would refuse it (perpetual_reference_stale). Trade another coin or wait for a fresh reference.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
code: "futures_reference_unavailable",
|
|
88
|
+
reason: `${action.symbol}: CoinRithm has no supported perpetual reference for this coin${when}, so the server refuses a NEW futures open (perpetual_reference_unavailable). Use spot for it, or another coin for futures.`,
|
|
89
|
+
};
|
|
90
|
+
}
|
package/dist/agent/observe.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { CoinRithmClient } from "./client.js";
|
|
2
|
-
import { AgentSpec, RunState, Observation, AgentTrace } from "./types.js";
|
|
2
|
+
import { AgentSpec, RunState, Observation, AgentTrace, PmCalibration, WhaleContext } from "./types.js";
|
|
3
3
|
export interface ObserveOutput {
|
|
4
4
|
observation: Observation;
|
|
5
5
|
skip?: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Add bounded public whale context after the deterministic gate. This is
|
|
9
|
+
* deliberately separate from observe(): a skipped or mechanical cycle never
|
|
10
|
+
* pays for the public reads, and this context cannot widen candidate selection.
|
|
11
|
+
*/
|
|
12
|
+
export declare function enrichWhaleContext(client: CoinRithmClient, observation: Observation): Promise<WhaleContext>;
|
|
13
|
+
export declare function clearPmCalibrationCache(): void;
|
|
14
|
+
export declare function readPmCalibration(client: CoinRithmClient, trace?: AgentTrace, nowMs?: number): Promise<PmCalibration | undefined>;
|
|
7
15
|
export declare function isCalibrationChurnMarket(market: {
|
|
8
16
|
slug?: string;
|
|
9
17
|
title?: string;
|