@coinrithm/mcp-trading 0.7.12 → 0.7.14
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 +613 -546
- package/README.md +366 -344
- package/dist/agent/act.d.ts +7 -2
- package/dist/agent/act.js +47 -4
- package/dist/agent/cli.d.ts +1 -0
- package/dist/agent/cli.js +25 -4
- package/dist/agent/client.d.ts +4 -0
- package/dist/agent/decision.d.ts +12 -12
- package/dist/agent/decisionReceipt.js +43 -0
- package/dist/agent/decisionValidator.js +74 -0
- package/dist/agent/definitionSnapshot.d.ts +22 -0
- package/dist/agent/definitionSnapshot.js +28 -0
- package/dist/agent/engine.d.ts +2 -1
- package/dist/agent/engine.js +2 -1
- package/dist/agent/mergeRules.d.ts +1 -1
- package/dist/agent/mergeRules.js +17 -2
- package/dist/agent/observe.js +98 -7
- package/dist/agent/prompt.js +11 -1
- package/dist/agent/providers.d.ts +4 -0
- package/dist/agent/providers.js +16 -10
- package/dist/agent/resolve.js +8 -1
- package/dist/agent/runner.js +42 -8
- package/dist/agent/skill.js +9 -0
- package/dist/agent/skillValidator.js +11 -2
- package/dist/agent/strictLint.js +1 -0
- package/dist/agent/templates.js +30 -30
- package/dist/agent/types.d.ts +35 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +43 -0
- package/dist/httpCompletion.d.ts +1 -1
- package/dist/httpCompletion.js +2 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +208 -17
- package/package.json +97 -97
package/dist/agent/observe.js
CHANGED
|
@@ -6,10 +6,10 @@ import { computeIndicators } from "./indicators.js";
|
|
|
6
6
|
import { scanSetups } from "./setups.js";
|
|
7
7
|
import { freshnessOf, pmQualityOf, pmDecisionSupportOf } from "./pmContext.js";
|
|
8
8
|
import { deriveCapitalBook, usesCapitalSizing } from "./capitalSizing.js";
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// short cadence the hosted house agents run on. Probe-verified 2026-06-17.
|
|
9
|
+
// The 1D endpoint nominally returns 288 five-minute bars. Timestamps and gaps
|
|
10
|
+
// are checked separately; requesting this range does not establish freshness.
|
|
12
11
|
const INDICATOR_RANGE = "1D";
|
|
12
|
+
const INDICATOR_INTERVAL_SECONDS = 300;
|
|
13
13
|
// `universe_scan` bounds: how many top movers to pull, and how many of those
|
|
14
14
|
// to fully resolve into tradable watch entries (each resolved row costs a
|
|
15
15
|
// resolve + market [+ candles] call).
|
|
@@ -73,6 +73,49 @@ const PM_CALIBRATION_CHURN_RE = /(updown|up-or-down|-5-?min|-5m-|-15m|15m(?:-|$)
|
|
|
73
73
|
export function isCalibrationChurnMarket(market) {
|
|
74
74
|
return PM_CALIBRATION_CHURN_RE.test(`${market.slug ?? ""} ${market.title ?? ""}`);
|
|
75
75
|
}
|
|
76
|
+
// The live endpoint's t is Unix seconds. Missing/malformed values (including
|
|
77
|
+
// millisecond epochs) stay unknown; never substitute retrieval time.
|
|
78
|
+
function candleTimestamp(value) {
|
|
79
|
+
return typeof value === "number" &&
|
|
80
|
+
Number.isInteger(value) &&
|
|
81
|
+
value > 0 &&
|
|
82
|
+
value <= 253_402_300_799
|
|
83
|
+
? value
|
|
84
|
+
: undefined;
|
|
85
|
+
}
|
|
86
|
+
function candleIntervals(times) {
|
|
87
|
+
let timestampedBarCount = 0;
|
|
88
|
+
let checkedIntervalCount = 0;
|
|
89
|
+
let irregularIntervalCount = 0;
|
|
90
|
+
let maxGapSeconds;
|
|
91
|
+
for (let i = 0; i < times.length; i++) {
|
|
92
|
+
const current = times[i];
|
|
93
|
+
if (current === undefined)
|
|
94
|
+
continue;
|
|
95
|
+
timestampedBarCount++;
|
|
96
|
+
const previous = times[i - 1];
|
|
97
|
+
if (previous === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
const gap = current - previous;
|
|
100
|
+
checkedIntervalCount++;
|
|
101
|
+
if (gap !== INDICATOR_INTERVAL_SECONDS)
|
|
102
|
+
irregularIntervalCount++;
|
|
103
|
+
maxGapSeconds = Math.max(maxGapSeconds ?? 0, gap);
|
|
104
|
+
}
|
|
105
|
+
const intervalStatus = irregularIntervalCount > 0
|
|
106
|
+
? "irregular"
|
|
107
|
+
: times.length < 2 || timestampedBarCount !== times.length
|
|
108
|
+
? "unknown"
|
|
109
|
+
: "regular";
|
|
110
|
+
return {
|
|
111
|
+
barCount: times.length,
|
|
112
|
+
timestampedBarCount,
|
|
113
|
+
checkedIntervalCount,
|
|
114
|
+
irregularIntervalCount,
|
|
115
|
+
intervalStatus,
|
|
116
|
+
...(maxGapSeconds === undefined ? {} : { maxGapSeconds }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
76
119
|
// Fetch candles for one coin and reduce them to a compact indicator bundle plus
|
|
77
120
|
// the 24h volume. Tolerant by design: any failure (HTTP error, malformed/sparse
|
|
78
121
|
// candles) yields null indicators so the cycle proceeds with price-only context
|
|
@@ -90,8 +133,11 @@ async function fetchCandleContext(client, coinId, trace) {
|
|
|
90
133
|
}
|
|
91
134
|
if (!cr.ok)
|
|
92
135
|
return { indicators: null };
|
|
93
|
-
// Endpoint shape: { candles: [{ t, o, h, l, c, v }] } ascending (oldest first).
|
|
136
|
+
// Endpoint shape: { candles: [{ t, o, h, l, c, v, vm }] } ascending (oldest first).
|
|
94
137
|
const candles = [];
|
|
138
|
+
const times = [];
|
|
139
|
+
let latestVolume;
|
|
140
|
+
let latestVolumeMissingVenues;
|
|
95
141
|
for (const raw of asArr(asObj(cr.data).candles)) {
|
|
96
142
|
const c = asObj(raw);
|
|
97
143
|
const open = asNum(c.o);
|
|
@@ -100,12 +146,37 @@ async function fetchCandleContext(client, coinId, trace) {
|
|
|
100
146
|
const close = asNum(c.c);
|
|
101
147
|
if (open == null || high == null || low == null || close == null)
|
|
102
148
|
continue;
|
|
103
|
-
|
|
149
|
+
const volume = asNum(c.v);
|
|
150
|
+
candles.push({ open, high, low, close, volume: volume ?? undefined });
|
|
151
|
+
times.push(candleTimestamp(c.t));
|
|
152
|
+
latestVolume = volume != null && volume >= 0 ? volume : undefined;
|
|
153
|
+
const coverage = asNum(c.vm);
|
|
154
|
+
latestVolumeMissingVenues =
|
|
155
|
+
latestVolume != null &&
|
|
156
|
+
coverage != null &&
|
|
157
|
+
Number.isInteger(coverage) &&
|
|
158
|
+
coverage >= 0
|
|
159
|
+
? coverage
|
|
160
|
+
: undefined;
|
|
104
161
|
}
|
|
105
|
-
const
|
|
162
|
+
const latestTime = times.at(-1);
|
|
163
|
+
const recent = candleIntervals(times.slice(-15));
|
|
106
164
|
return {
|
|
107
165
|
indicators: computeIndicators(candles),
|
|
108
|
-
|
|
166
|
+
indicatorContext: {
|
|
167
|
+
range: INDICATOR_RANGE,
|
|
168
|
+
nominalIntervalSeconds: INDICATOR_INTERVAL_SECONDS,
|
|
169
|
+
...candleIntervals(times),
|
|
170
|
+
...(latestTime === undefined
|
|
171
|
+
? {}
|
|
172
|
+
: { asOf: new Date(latestTime * 1000).toISOString() }),
|
|
173
|
+
recent15: {
|
|
174
|
+
barCount: recent.barCount,
|
|
175
|
+
intervalStatus: recent.intervalStatus,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
volume24hUsd: latestVolume,
|
|
179
|
+
volumeMissingVenues: latestVolumeMissingVenues,
|
|
109
180
|
};
|
|
110
181
|
}
|
|
111
182
|
// The fundamentals leg of a watch entry, read from the /market context the
|
|
@@ -135,12 +206,20 @@ async function enrichFromCandles(client, entry, coinId, trace) {
|
|
|
135
206
|
const cc = await fetchCandleContext(client, coinId, trace);
|
|
136
207
|
if (cc.indicators)
|
|
137
208
|
entry.indicators = cc.indicators;
|
|
209
|
+
if (cc.indicatorContext)
|
|
210
|
+
entry.indicatorContext = cc.indicatorContext;
|
|
138
211
|
if (cc.volume24hUsd != null) {
|
|
139
212
|
entry.fundamentals = {
|
|
140
213
|
...(entry.fundamentals ?? {}),
|
|
141
214
|
volume24hUsd: cc.volume24hUsd,
|
|
142
215
|
};
|
|
143
216
|
}
|
|
217
|
+
if (cc.volumeMissingVenues != null) {
|
|
218
|
+
entry.fundamentals = {
|
|
219
|
+
...(entry.fundamentals ?? {}),
|
|
220
|
+
volumeMissingVenues: cc.volumeMissingVenues,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
144
223
|
}
|
|
145
224
|
const HEADLINES_PER_COIN = 3;
|
|
146
225
|
const HEADLINE_TITLE_CHARS = 110;
|
|
@@ -316,6 +395,14 @@ export async function observe(client, spec, state, trace) {
|
|
|
316
395
|
// and could not tell a winner from a small loser before a manual close.
|
|
317
396
|
// Tolerant fallbacks keep older/mocked shapes working.
|
|
318
397
|
const coin = asObj(p.coin);
|
|
398
|
+
const fundingPaidMusd = Object.prototype.hasOwnProperty.call(p, "fundingPaidMusd")
|
|
399
|
+
? (asNum(p.fundingPaidMusd) ??
|
|
400
|
+
(p.fundingPaidMusd === null ? null : undefined))
|
|
401
|
+
: undefined;
|
|
402
|
+
const fundingAppliedThrough = Object.prototype.hasOwnProperty.call(p, "fundingAppliedThrough")
|
|
403
|
+
? (asStr(p.fundingAppliedThrough) ??
|
|
404
|
+
(p.fundingAppliedThrough === null ? null : undefined))
|
|
405
|
+
: undefined;
|
|
319
406
|
return {
|
|
320
407
|
venue: "futures",
|
|
321
408
|
id: Number(asNum(p.id) ?? p.id),
|
|
@@ -327,6 +414,10 @@ export async function observe(client, spec, state, trace) {
|
|
|
327
414
|
leverage: asNum(p.leverage),
|
|
328
415
|
marginMusd: asNum(p.marginMusd),
|
|
329
416
|
unrealizedPnlMusd: asNum(p.unrealizedPnlMusd),
|
|
417
|
+
...(fundingPaidMusd !== undefined ? { fundingPaidMusd } : {}),
|
|
418
|
+
...(fundingAppliedThrough !== undefined
|
|
419
|
+
? { fundingAppliedThrough }
|
|
420
|
+
: {}),
|
|
330
421
|
entryPrice: asNum(p.entryPrice),
|
|
331
422
|
markPrice: asNum(p.markPrice),
|
|
332
423
|
liquidationPrice: asNum(p.liquidationPrice),
|
package/dist/agent/prompt.js
CHANGED
|
@@ -147,6 +147,11 @@ opts = {}) {
|
|
|
147
147
|
"- YOUR SHARPEST PM EDGE is the crypto price view you JUST formed: crypto PM markets resolve on the very prices you analyse, so you have a genuine information edge there that you do NOT have on coin futures alone. EVERY cycle you reach a price conviction, it is REQUIRED that you scan observation.pmMarkets for a LISTED crypto market that same view prices wrong and, if one is materially mispriced, open it with pm_open by its `ref` — treat that mispricing exactly like a flagged coin setup (an ACT, not a skip). If you are bearish BTC, a 'BTC above $X by <date>' priced high is a NO; if bullish ETH, an 'ETH above $Y' priced low is a YES. ESCAPE HATCH — only the markets actually listed in observation.pmMarkets THIS cycle (pm1..pmN) are bettable: if NONE of them matches the coin or view you formed, that is a legitimate SKIP for PM (say so in one clause and move on) — do NOT invent, guess, or increment a ref for a market you wish existed, because a made-up ref is rejected (pm_ref_unknown) and wastes the whole cycle exactly like a rejected open. The mistake to avoid is leaving a LISTED, clearly mispriced crypto market untraded — a mispricing that is NOT on this cycle's board is simply not actionable now, not a miss. (For non-crypto events you have no special edge; skip unless the odds are obviously off.)",
|
|
148
148
|
]
|
|
149
149
|
: []),
|
|
150
|
+
...(hasPm && typeof r.pmMinEntryProbabilityPct === "number"
|
|
151
|
+
? [
|
|
152
|
+
`- PM ENTRY FLOOR: pm_open on an outcome whose market probability is below ${r.pmMinEntryProbabilityPct} points is REJECTED by the runner (the chosen outcome's own price; fees are not counted). Do not propose cheaper longshots; look for edge on outcomes priced at or above the floor.`,
|
|
153
|
+
]
|
|
154
|
+
: []),
|
|
150
155
|
...(includeForecast
|
|
151
156
|
? [
|
|
152
157
|
"- FORECAST RULE (pm_open forecastProbability): before you look at what the market is pricing, decide YOUR OWN probability the outcome you are backing actually WINS — reason ONLY from the question, its resolution criteria, and the deadline. Put that number (1-99, whole or one decimal) in `forecastProbability`. This is graded against reality as your PUBLIC calibration record, so it must be YOUR judgement, NOT the market's: do NOT copy, round, or anchor it to the observation.pmMarkets `prob`. It is FINE if your honest forecast happens to land on the market's number — but reaching that by echoing the price defeats the point. If you genuinely cannot form an independent view, OMIT the field rather than parroting the market (an absent forecast is better than a fake one, and it never blocks the bet). A forecast you DO give is enforced: if it is not above what the outcome currently costs, the open is rejected, because buying something you price below the market is a losing trade by your own numbers.",
|
|
@@ -156,7 +161,8 @@ opts = {}) {
|
|
|
156
161
|
...(spec.capabilities.includes("indicators")
|
|
157
162
|
? [
|
|
158
163
|
"",
|
|
159
|
-
"## Signals — each watch entry may carry `indicators` (
|
|
164
|
+
"## Signals — each watch entry may carry `indicators` (nominal five-minute candles)",
|
|
165
|
+
"- `indicatorContext` reports accepted candle counts, source `asOf` and intervalStatus (regular/irregular/unknown); compare its asOf with observation.asOf for age. /market freshness is separate. nominalIntervalSeconds=300 does not prove fresh, continuous candles. Missing timestamps are unknown; stale, future-dated or irregular candles do not establish a current five-minute signal. `recent15` describes only recent spacing; Wilder atr14 also retains earlier history, so recent regularity does not erase older gaps.",
|
|
160
166
|
"- rsi14: momentum (>70 overbought, <30 oversold); ema20 & ema50: trend; atr14: volatility (size stops off it); bollinger {upper,mid,lower}; recent20 {high,low}: breakout levels.",
|
|
161
167
|
"- boolean reads: aboveEma20, ema20AboveEma50 (uptrend when both true), brokeRecentHigh (breakout), brokeRecentLow (breakdown).",
|
|
162
168
|
"- a null field = not enough data; ignore it. These INFORM your decision; they never widen a cap.",
|
|
@@ -338,6 +344,10 @@ export function buildUserPrompt(obs, journal, opts = {}) {
|
|
|
338
344
|
if (brokenTheses.length > 0) {
|
|
339
345
|
lines.push("", "## Positions whose thesis is INVALIDATED this cycle", ...brokenTheses.map((b) => `- ${b}`));
|
|
340
346
|
}
|
|
347
|
+
if (obs.openPositions.some((p) => p.fundingPaidMusd !== undefined ||
|
|
348
|
+
p.fundingAppliedThrough !== undefined)) {
|
|
349
|
+
lines.push("Futures funding shown in the position is already reflected in margin and balances; do not deduct it again when judging available capital or PnL.");
|
|
350
|
+
}
|
|
341
351
|
// Settlement-feedback loop: surface the agent's recently-RESOLVED PM bets so the
|
|
342
352
|
// model can reflect and adapt. Reflective context only — never a new action.
|
|
343
353
|
if (hasPm)
|
|
@@ -52,6 +52,10 @@ export interface ProviderEnv {
|
|
|
52
52
|
GEMINI_API_KEY?: string;
|
|
53
53
|
MODEL_API_KEY?: string;
|
|
54
54
|
}
|
|
55
|
+
/** Keep direct and routed cycle evidence consistent about NVIDIA backpressure. */
|
|
56
|
+
export declare function classifyProviderFailure(result: Extract<DecideResult, {
|
|
57
|
+
ok: false;
|
|
58
|
+
}>): "capacity" | "permanent" | "transient";
|
|
55
59
|
export declare function selectProvider(spec: AgentSpec, env: ProviderEnv, fetchFn?: typeof fetch): Provider;
|
|
56
60
|
export declare function providerForRoute(route: {
|
|
57
61
|
provider: ProviderName;
|
package/dist/agent/providers.js
CHANGED
|
@@ -29,6 +29,16 @@ const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai
|
|
|
29
29
|
// MUST stay below the scheduler's RUN_LOCK_SECONDS and HEARTBEAT_STALE_MS.
|
|
30
30
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
31
31
|
const RETRYABLE_SERVER_STATUSES = new Set([500, 502, 503, 504]);
|
|
32
|
+
/** Keep direct and routed cycle evidence consistent about NVIDIA backpressure. */
|
|
33
|
+
export function classifyProviderFailure(result) {
|
|
34
|
+
if (result.status === 429 ||
|
|
35
|
+
(result.status === 503 &&
|
|
36
|
+
/resourceexhausted|worker local total request limit/i.test(result.error ?? "")))
|
|
37
|
+
return "capacity";
|
|
38
|
+
if (result.status === 404 || result.status === 410)
|
|
39
|
+
return "permanent";
|
|
40
|
+
return "transient";
|
|
41
|
+
}
|
|
32
42
|
// Per-route request quirks (reasoning toggles, token param, temperature) live
|
|
33
43
|
// in the capability table — providerCapabilities.ts is the single source; this
|
|
34
44
|
// module only assembles and sends.
|
|
@@ -332,15 +342,11 @@ class SameModelRetryProvider {
|
|
|
332
342
|
error: res.error,
|
|
333
343
|
status: res.status,
|
|
334
344
|
retryAfterMs: res.retryAfterMs,
|
|
335
|
-
|
|
336
|
-
?
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
? "transient"
|
|
341
|
-
: "permanent",
|
|
342
|
-
}
|
|
343
|
-
: {}),
|
|
345
|
+
failureClass: classifyProviderFailure(res) === "capacity"
|
|
346
|
+
? "capacity"
|
|
347
|
+
: res.status !== undefined && res.status < 500
|
|
348
|
+
? "permanent"
|
|
349
|
+
: "transient",
|
|
344
350
|
}
|
|
345
351
|
: {}),
|
|
346
352
|
});
|
|
@@ -350,7 +356,7 @@ class SameModelRetryProvider {
|
|
|
350
356
|
// or a certificate that returned text passed the trading-decision parser.
|
|
351
357
|
...result,
|
|
352
358
|
route: {
|
|
353
|
-
policyVersion: "coinrithm.configured-same-model-retry.
|
|
359
|
+
policyVersion: "coinrithm.configured-same-model-retry.v2",
|
|
354
360
|
profile: "configured",
|
|
355
361
|
effectiveProvider: this.provider,
|
|
356
362
|
effectiveModel: this.model,
|
package/dist/agent/resolve.js
CHANGED
|
@@ -47,7 +47,14 @@ const CONFIG_BLOCKS = [
|
|
|
47
47
|
const JOURNAL_MAX_LINES = 200;
|
|
48
48
|
const JOURNAL_MAX_BYTES = 8_000;
|
|
49
49
|
// Optional prose files (markdown the LLM reads), in assembly order.
|
|
50
|
-
const PROSE_FILES = [
|
|
50
|
+
const PROSE_FILES = [
|
|
51
|
+
"character/thesis.md",
|
|
52
|
+
"character/persona.md",
|
|
53
|
+
"character/entries.md",
|
|
54
|
+
"character/exits.md",
|
|
55
|
+
"character/sizing.md",
|
|
56
|
+
"character/research.md",
|
|
57
|
+
];
|
|
51
58
|
// Prose files carry an OPTIONAL YAML frontmatter block (type/title/description/
|
|
52
59
|
// tags) that is authoring metadata, not doctrine — the model gains nothing from
|
|
53
60
|
// `tags: [agent, persona, mean-reversion]`. It was being merged verbatim into
|
package/dist/agent/runner.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// spot, futures, and prediction markets. Dry-run never writes. Live uses
|
|
3
3
|
// idempotency keys + agentTrace and exports run evidence. The client + provider
|
|
4
4
|
// are injected so the loop is fully unit-testable with no network/model calls.
|
|
5
|
+
import { classifyProviderFailure } from "./providers.js";
|
|
5
6
|
import { COINRITHM_API } from "./version.js";
|
|
6
7
|
import { spotBuyCost, DEFAULT_TRIGGER_POLICY, } from "./types.js";
|
|
7
8
|
import { usesCapitalSizing, prepareCapitalAction, validateCapitalAction, capitalCashCost, } from "./capitalSizing.js";
|
|
@@ -424,6 +425,12 @@ async function runCycleCore(deps, capture) {
|
|
|
424
425
|
const provenance = buildRunnerProvenance(spec);
|
|
425
426
|
state.cyclesRun += 1;
|
|
426
427
|
rollDay(state);
|
|
428
|
+
// Old state did not identify which route failed. Carrying that count into
|
|
429
|
+
// a replacement model can request a hold on its very first error.
|
|
430
|
+
if (state.consecutivePermanentModelErrors &&
|
|
431
|
+
!state.permanentModelErrorRoute) {
|
|
432
|
+
state.consecutivePermanentModelErrors = 0;
|
|
433
|
+
}
|
|
427
434
|
const runId = state.runId;
|
|
428
435
|
const decisionId = makeDecisionId(state.cyclesRun);
|
|
429
436
|
const captureBase = { runId, decisionId, spec, mergedProse, state };
|
|
@@ -580,7 +587,7 @@ async function runCycleCore(deps, capture) {
|
|
|
580
587
|
...makeTrace(runId, decisionId, spec, undefined, `Thesis exit: ${why}`),
|
|
581
588
|
...observationReceipt,
|
|
582
589
|
};
|
|
583
|
-
const r = await executeAction(client, action, observation, trace, idem, provenance);
|
|
590
|
+
const r = await executeAction(client, action, observation, trace, idem, provenance, { pmMinEntryProbabilityPct: spec.risk.pmMinEntryProbabilityPct });
|
|
584
591
|
exitPlanned.push({
|
|
585
592
|
action,
|
|
586
593
|
accepted: true,
|
|
@@ -727,7 +734,7 @@ async function runCycleCore(deps, capture) {
|
|
|
727
734
|
// only capacity failures. Treat that exactly like a local capacity defer:
|
|
728
735
|
// no action, no fallback invented here, and no model-failure streak that
|
|
729
736
|
// could stop an otherwise healthy agent after repeated quota pressure.
|
|
730
|
-
const capacityOnlyFailure = (!route && res
|
|
737
|
+
const capacityOnlyFailure = (!route && classifyProviderFailure(res) === "capacity") ||
|
|
731
738
|
(!!route?.attempts?.length &&
|
|
732
739
|
route.attempts.every((attempt) => attempt.failureClass === "capacity"));
|
|
733
740
|
if (res.deferred || !actualCallMade || capacityOnlyFailure) {
|
|
@@ -763,13 +770,34 @@ async function runCycleCore(deps, capture) {
|
|
|
763
770
|
// for what deserves them: revoked credentials, drawdown, kill-switch,
|
|
764
771
|
// user action. Transient errors reset the permanent streak.
|
|
765
772
|
if (isPermanentModelError(res.error)) {
|
|
773
|
+
// The router retains the last non-capacity failure even when a later
|
|
774
|
+
// fallback returns 429. Its effective route still describes the last
|
|
775
|
+
// actual call for metering, not necessarily the error returned here.
|
|
776
|
+
const failureAttempt = route?.attempts
|
|
777
|
+
.slice()
|
|
778
|
+
.reverse()
|
|
779
|
+
.find((attempt) => attempt.outcome === "failed" &&
|
|
780
|
+
attempt.failureClass !== "capacity" &&
|
|
781
|
+
attempt.error === res.error);
|
|
782
|
+
const failureRoute = {
|
|
783
|
+
provider: failureAttempt?.provider ??
|
|
784
|
+
route?.effectiveProvider ??
|
|
785
|
+
providerName,
|
|
786
|
+
model: failureAttempt?.model ??
|
|
787
|
+
route?.effectiveModel ??
|
|
788
|
+
spec.model?.name ??
|
|
789
|
+
"unknown",
|
|
790
|
+
};
|
|
791
|
+
const previousRoute = state.permanentModelErrorRoute;
|
|
792
|
+
const sameRoute = previousRoute?.provider === failureRoute.provider &&
|
|
793
|
+
previousRoute?.model === failureRoute.model;
|
|
766
794
|
state.consecutivePermanentModelErrors =
|
|
767
|
-
(state.consecutivePermanentModelErrors ?? 0) + 1;
|
|
795
|
+
(sameRoute ? (state.consecutivePermanentModelErrors ?? 0) : 0) + 1;
|
|
796
|
+
state.permanentModelErrorRoute = failureRoute;
|
|
768
797
|
if (state.consecutivePermanentModelErrors >=
|
|
769
798
|
PERMANENT_MODEL_ERROR_THRESHOLD) {
|
|
770
799
|
const hold = {
|
|
771
|
-
|
|
772
|
-
model: route?.effectiveModel ?? spec.model?.name ?? "unknown",
|
|
800
|
+
...failureRoute,
|
|
773
801
|
error: res.error.slice(0, 200),
|
|
774
802
|
};
|
|
775
803
|
saveState(stateFile, state);
|
|
@@ -791,6 +819,7 @@ async function runCycleCore(deps, capture) {
|
|
|
791
819
|
}
|
|
792
820
|
else {
|
|
793
821
|
state.consecutivePermanentModelErrors = 0;
|
|
822
|
+
delete state.permanentModelErrorRoute;
|
|
794
823
|
}
|
|
795
824
|
saveState(stateFile, state);
|
|
796
825
|
log(`model error: ${res.error}`);
|
|
@@ -807,6 +836,10 @@ async function runCycleCore(deps, capture) {
|
|
|
807
836
|
...observationReceipt,
|
|
808
837
|
};
|
|
809
838
|
}
|
|
839
|
+
// A returned model response proves availability even if its decision JSON
|
|
840
|
+
// is invalid. Output validation keeps its own failure counter below.
|
|
841
|
+
state.consecutivePermanentModelErrors = 0;
|
|
842
|
+
delete state.permanentModelErrorRoute;
|
|
810
843
|
const parsed = parseDecision(res.text);
|
|
811
844
|
if (!parsed.ok) {
|
|
812
845
|
state.consecutiveModelFailures += 1;
|
|
@@ -829,7 +862,6 @@ async function runCycleCore(deps, capture) {
|
|
|
829
862
|
};
|
|
830
863
|
}
|
|
831
864
|
state.consecutiveModelFailures = 0;
|
|
832
|
-
state.consecutivePermanentModelErrors = 0;
|
|
833
865
|
decision = parsed.decision;
|
|
834
866
|
}
|
|
835
867
|
// Reasoning captured for the Arena terminal (keystone transparency): the
|
|
@@ -1026,7 +1058,9 @@ async function runCycleCore(deps, capture) {
|
|
|
1026
1058
|
}
|
|
1027
1059
|
}
|
|
1028
1060
|
}
|
|
1029
|
-
const quote = await fetchQuote(client, action, observation, baseTrace
|
|
1061
|
+
const quote = await fetchQuote(client, action, observation, baseTrace, {
|
|
1062
|
+
pmMinEntryProbabilityPct: spec.risk.pmMinEntryProbabilityPct,
|
|
1063
|
+
});
|
|
1030
1064
|
// Early PM skip: the quote's openBlocked preview tells us a pm/open right now
|
|
1031
1065
|
// would be rejected 422 by the open-time quality gate (distinct from the
|
|
1032
1066
|
// eligible/blockReasons SHAPE gate the validator checks). Bail here with a clear
|
|
@@ -1149,7 +1183,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1149
1183
|
rationaleForAction(action, decision.rationale, meta.rationaleSummary, decision.actions.length)),
|
|
1150
1184
|
...observationReceipt,
|
|
1151
1185
|
};
|
|
1152
|
-
const r = await executeAction(client, action, observation, trace, idem, provenance);
|
|
1186
|
+
const r = await executeAction(client, action, observation, trace, idem, provenance, { pmMinEntryProbabilityPct: spec.risk.pmMinEntryProbabilityPct });
|
|
1153
1187
|
planned.push({
|
|
1154
1188
|
action,
|
|
1155
1189
|
accepted: true,
|
package/dist/agent/skill.js
CHANGED
|
@@ -109,6 +109,15 @@ export function buildSpec(raw) {
|
|
|
109
109
|
entryPredicates: risk.entryPredicates,
|
|
110
110
|
}
|
|
111
111
|
: {}),
|
|
112
|
+
// Optional PM entry floor: an absent key stays absent (no floor); an
|
|
113
|
+
// explicit value is carried AS WRITTEN, malformed or not, so hosted
|
|
114
|
+
// validation fails closed instead of a typo silently meaning "no floor"
|
|
115
|
+
// (the same rule entryPredicates follow).
|
|
116
|
+
...(risk.pmMinEntryProbabilityPct !== undefined
|
|
117
|
+
? {
|
|
118
|
+
pmMinEntryProbabilityPct: risk.pmMinEntryProbabilityPct,
|
|
119
|
+
}
|
|
120
|
+
: {}),
|
|
112
121
|
maxLeverage: num(risk.maxLeverage, 1),
|
|
113
122
|
perTradeMarginMusd: num(risk.perTradeMarginMusd, 0),
|
|
114
123
|
maxConcurrentPositions: num(risk.maxConcurrentPositions, 0),
|
|
@@ -108,6 +108,13 @@ export function validateSkill(parsed, mode = "self-host") {
|
|
|
108
108
|
add("skill_risk_sl", "risk.requireStopLoss must be true or false");
|
|
109
109
|
if (!Array.isArray(r.watchlist) || r.watchlist.length === 0)
|
|
110
110
|
add("skill_risk_watchlist", "risk.watchlist must be a non-empty list of symbols");
|
|
111
|
+
// Optional PM entry floor: absent means no floor; present must be a finite
|
|
112
|
+
// number of probability points in [0, 100].
|
|
113
|
+
if (r.pmMinEntryProbabilityPct !== undefined) {
|
|
114
|
+
const f = r.pmMinEntryProbabilityPct;
|
|
115
|
+
if (typeof f !== "number" || !Number.isFinite(f) || f < 0 || f > 100)
|
|
116
|
+
add("skill_risk_pm_entry_floor", "risk.pmMinEntryProbabilityPct must be a number between 0 and 100 (points; omit for no floor)");
|
|
117
|
+
}
|
|
111
118
|
// Fail-closed on the side restriction: a typo ("shorts_only") must never
|
|
112
119
|
// silently mean "unrestricted" — that is exactly how a prose-only
|
|
113
120
|
// constraint failed live on 2026-08-24.
|
|
@@ -171,8 +178,10 @@ export function validateSkill(parsed, mode = "self-host") {
|
|
|
171
178
|
add("skill_limits_trades", "limits.maxTradesPerDay must be a number >= 0 (0 = unlimited daily trades)");
|
|
172
179
|
if (!isPosNum(l.maxWritesPerCycle))
|
|
173
180
|
add("skill_limits_writes", "limits.maxWritesPerCycle must be a positive number");
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
// 0 = no daily loss cap: the runner only enforces a positive value and
|
|
182
|
+
// the hosted API already accepts 0..50,000, so hosted validation must too.
|
|
183
|
+
if (!isNonNegNum(l.maxDailyLossMusd))
|
|
184
|
+
add("skill_limits_loss", "limits.maxDailyLossMusd must be a number >= 0 (0 = no daily loss cap)");
|
|
176
185
|
if (!isPosNum(l.maxOpenMarginMusd))
|
|
177
186
|
add("skill_limits_open", "limits.maxOpenMarginMusd must be a positive number");
|
|
178
187
|
}
|
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
|
@@ -34,6 +34,7 @@ export interface RiskConfig {
|
|
|
34
34
|
watchlist: string[];
|
|
35
35
|
blocklist?: string[];
|
|
36
36
|
direction?: "long_only" | "short_only";
|
|
37
|
+
pmMinEntryProbabilityPct?: number;
|
|
37
38
|
}
|
|
38
39
|
export interface LimitsConfig {
|
|
39
40
|
maxTradesPerDay: number;
|
|
@@ -133,6 +134,21 @@ export interface Freshness {
|
|
|
133
134
|
asOf?: string;
|
|
134
135
|
basis?: string;
|
|
135
136
|
}
|
|
137
|
+
export interface IndicatorContext {
|
|
138
|
+
range: "1D";
|
|
139
|
+
nominalIntervalSeconds: 300;
|
|
140
|
+
barCount: number;
|
|
141
|
+
timestampedBarCount: number;
|
|
142
|
+
checkedIntervalCount: number;
|
|
143
|
+
irregularIntervalCount: number;
|
|
144
|
+
intervalStatus: "regular" | "irregular" | "unknown";
|
|
145
|
+
maxGapSeconds?: number;
|
|
146
|
+
asOf?: string;
|
|
147
|
+
recent15: {
|
|
148
|
+
barCount: number;
|
|
149
|
+
intervalStatus: "regular" | "irregular" | "unknown";
|
|
150
|
+
};
|
|
151
|
+
}
|
|
136
152
|
export interface WatchEntry {
|
|
137
153
|
symbol: string;
|
|
138
154
|
coinId: string | null;
|
|
@@ -144,6 +160,7 @@ export interface WatchEntry {
|
|
|
144
160
|
sentimentBullishPct?: number;
|
|
145
161
|
freshness?: Freshness;
|
|
146
162
|
indicators?: IndicatorSet;
|
|
163
|
+
indicatorContext?: IndicatorContext;
|
|
147
164
|
discovered?: boolean;
|
|
148
165
|
slug?: string;
|
|
149
166
|
fundamentals?: CoinFundamentals;
|
|
@@ -153,6 +170,7 @@ export interface CoinFundamentals {
|
|
|
153
170
|
marketCapRank?: number;
|
|
154
171
|
marketCapUsd?: number;
|
|
155
172
|
volume24hUsd?: number;
|
|
173
|
+
volumeMissingVenues?: number;
|
|
156
174
|
headlines?: Array<{
|
|
157
175
|
title: string;
|
|
158
176
|
at?: string;
|
|
@@ -171,6 +189,8 @@ export interface OpenPosition {
|
|
|
171
189
|
leverage?: number;
|
|
172
190
|
marginMusd?: number;
|
|
173
191
|
unrealizedPnlMusd?: number;
|
|
192
|
+
fundingPaidMusd?: number | null;
|
|
193
|
+
fundingAppliedThrough?: string | null;
|
|
174
194
|
entryPrice?: number;
|
|
175
195
|
markPrice?: number;
|
|
176
196
|
liquidationPrice?: number;
|
|
@@ -397,10 +417,21 @@ export interface QuoteEvidence {
|
|
|
397
417
|
futuresFeeBps?: number;
|
|
398
418
|
estimatedEntryFeeMusd?: number;
|
|
399
419
|
cashRequiredMusd?: number;
|
|
420
|
+
funding?: FuturesFundingQuoteEvidence | null;
|
|
400
421
|
freshness?: Freshness;
|
|
401
422
|
openBlocked?: boolean;
|
|
402
423
|
openBlockReasons?: unknown;
|
|
403
424
|
}
|
|
425
|
+
export interface FuturesFundingQuoteEvidence {
|
|
426
|
+
venue?: string;
|
|
427
|
+
symbol?: string;
|
|
428
|
+
rate?: number;
|
|
429
|
+
intervalHours?: number;
|
|
430
|
+
nextFundingTime?: string;
|
|
431
|
+
asOf?: string;
|
|
432
|
+
estimatedPerIntervalMusd?: number | null;
|
|
433
|
+
annualizedRate?: number;
|
|
434
|
+
}
|
|
404
435
|
export interface RunState {
|
|
405
436
|
runId: string;
|
|
406
437
|
cyclesRun: number;
|
|
@@ -423,6 +454,10 @@ export interface RunState {
|
|
|
423
454
|
lastLlmCallAt?: number;
|
|
424
455
|
lastTriggerFingerprint?: string;
|
|
425
456
|
consecutivePermanentModelErrors?: number;
|
|
457
|
+
permanentModelErrorRoute?: {
|
|
458
|
+
provider: string;
|
|
459
|
+
model: string;
|
|
460
|
+
};
|
|
426
461
|
consecutiveAuthFailures?: number;
|
|
427
462
|
journal?: Array<{
|
|
428
463
|
at: string;
|
package/dist/client.d.ts
CHANGED
|
@@ -64,7 +64,11 @@ export declare class CoinRithmClient {
|
|
|
64
64
|
getPublicPmEvent(source: string, slug: string, query?: {
|
|
65
65
|
fiat?: string;
|
|
66
66
|
}): Promise<ApiResult>;
|
|
67
|
+
/** Best-effort anonymous interest marker for an explicitly viewed open event. */
|
|
68
|
+
markPublicPmEventViewed(source: string, slug: string): Promise<ApiResult>;
|
|
67
69
|
getPublicPmWhales(): Promise<ApiResult>;
|
|
70
|
+
getPublicPmWhaleWallets(window?: "7d" | "30d", source?: "polymarket" | "limitless" | "myriad"): Promise<ApiResult>;
|
|
71
|
+
getPublicPmWhaleWallet(source: string, wallet: string): Promise<ApiResult>;
|
|
68
72
|
getPublicPmMatches(query?: {
|
|
69
73
|
limit?: number;
|
|
70
74
|
offset?: number;
|
|
@@ -168,6 +172,9 @@ export declare class CoinRithmClient {
|
|
|
168
172
|
outcomeExternalMarketId: string;
|
|
169
173
|
side?: "yes" | "no";
|
|
170
174
|
stakeMusd: number;
|
|
175
|
+
forecastProbability?: number;
|
|
176
|
+
bankrollMusd?: number;
|
|
177
|
+
minEntryProbabilityPct?: number;
|
|
171
178
|
} & {
|
|
172
179
|
agentTrace?: AgentTrace;
|
|
173
180
|
}, apiKey?: string): Promise<ApiResult>;
|
|
@@ -215,6 +222,8 @@ export declare class CoinRithmClient {
|
|
|
215
222
|
stakeMusd: number;
|
|
216
223
|
idempotencyKey: string;
|
|
217
224
|
forecastProbability?: number;
|
|
225
|
+
minEntryProbabilityPct?: number;
|
|
226
|
+
thesis?: string;
|
|
218
227
|
provenance?: ProvenanceReport;
|
|
219
228
|
}>, apiKey?: string): Promise<ApiResult>;
|
|
220
229
|
reportPmOpportunity(body: TraceableBody<{
|