@coinrithm/mcp-trading 0.7.14 → 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 (72) hide show
  1. package/CHANGELOG.md +811 -613
  2. package/README.md +415 -366
  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 +14 -0
  22. package/dist/agent/client.js +113 -4
  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/decisionReceipt.d.ts +5 -1
  27. package/dist/agent/decisionReceipt.js +52 -3
  28. package/dist/agent/decisionValidator.d.ts +30 -0
  29. package/dist/agent/decisionValidator.js +147 -2
  30. package/dist/agent/engine.d.ts +3 -3
  31. package/dist/agent/engine.js +3 -3
  32. package/dist/agent/futuresEligibility.d.ts +36 -0
  33. package/dist/agent/futuresEligibility.js +90 -0
  34. package/dist/agent/observe.d.ts +35 -2
  35. package/dist/agent/observe.js +969 -128
  36. package/dist/agent/pmContext.d.ts +10 -1
  37. package/dist/agent/pmContext.js +163 -1
  38. package/dist/agent/prompt.d.ts +18 -1
  39. package/dist/agent/prompt.js +229 -33
  40. package/dist/agent/promptTables.d.ts +1 -0
  41. package/dist/agent/promptTables.js +33 -0
  42. package/dist/agent/providerCapabilities.d.ts +9 -0
  43. package/dist/agent/providerCapabilities.js +57 -4
  44. package/dist/agent/providers.d.ts +28 -7
  45. package/dist/agent/providers.js +200 -50
  46. package/dist/agent/resolve.js +3 -0
  47. package/dist/agent/runner.d.ts +8 -0
  48. package/dist/agent/runner.js +159 -26
  49. package/dist/agent/setups.d.ts +2 -1
  50. package/dist/agent/setups.js +17 -16
  51. package/dist/agent/signals.d.ts +33 -0
  52. package/dist/agent/signals.js +89 -0
  53. package/dist/agent/skill.js +37 -0
  54. package/dist/agent/skillValidator.js +33 -0
  55. package/dist/agent/strictLint.js +29 -0
  56. package/dist/agent/templates.js +30 -30
  57. package/dist/agent/types.d.ts +266 -13
  58. package/dist/agent/types.js +4 -0
  59. package/dist/agent/universe.d.ts +78 -0
  60. package/dist/agent/universe.js +215 -0
  61. package/dist/client.d.ts +9 -2
  62. package/dist/client.js +22 -10
  63. package/dist/http.d.ts +1 -0
  64. package/dist/http.js +17 -2
  65. package/dist/httpCompletion.d.ts +1 -1
  66. package/dist/httpCompletion.js +1 -0
  67. package/dist/index.js +2 -1
  68. package/dist/instructions.d.ts +1 -0
  69. package/dist/instructions.js +16 -0
  70. package/dist/tools.d.ts +1 -0
  71. package/dist/tools.js +150 -66
  72. package/package.json +98 -97
@@ -1,5 +1,7 @@
1
1
  import { fail, VENUES, PROVIDERS, SPEC_VERSION, OBJECTIVE_PRIMARIES, ALLOWED_CAPABILITIES, } from "./types.js";
2
+ import { universeIssues } from "./universe.js";
2
3
  import { parseCadenceMs, scanForSecrets } from "./util.js";
4
+ import { signalIssues } from "./signals.js";
3
5
  import { entryPredicateIssues } from "./entryPredicates.js";
4
6
  const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5
7
  const isPosNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
@@ -88,6 +90,12 @@ export function validateSkill(parsed, mode = "self-host") {
88
90
  add("skill_venue_unknown", `unknown venue "${String(v)}" (allowed: ${VENUES.join(", ")})`);
89
91
  }
90
92
  }
93
+ // Signal thresholds and data diet (optional).
94
+ for (const [code, reason] of signalIssues(raw.signals, raw.data))
95
+ add(code, reason);
96
+ // Market boundaries (optional).
97
+ for (const [code, reason] of universeIssues(raw.universe))
98
+ add(code, reason);
91
99
  // Risk — always required
92
100
  if (!isObj(raw.risk)) {
93
101
  add("skill_risk", "risk block is required (the caps the agent runs under)");
@@ -115,6 +123,31 @@ export function validateSkill(parsed, mode = "self-host") {
115
123
  if (typeof f !== "number" || !Number.isFinite(f) || f < 0 || f > 100)
116
124
  add("skill_risk_pm_entry_floor", "risk.pmMinEntryProbabilityPct must be a number between 0 and 100 (points; omit for no floor)");
117
125
  }
126
+ if (r.pmMinEdgeGapPct !== undefined) {
127
+ const g = r.pmMinEdgeGapPct;
128
+ if (typeof g !== "number" || !Number.isFinite(g) || g < 0 || g > 100)
129
+ add("skill_risk_pm_edge_gap", "risk.pmMinEdgeGapPct must be a number between 0 and 100 (% of the gap to 100; omit for the global minimum edge only)");
130
+ }
131
+ if (r.pmMaxEdgePoints !== undefined) {
132
+ const m = r.pmMaxEdgePoints;
133
+ if (typeof m !== "number" || !Number.isFinite(m) || m < 0 || m > 100)
134
+ add("skill_risk_pm_max_edge", "risk.pmMaxEdgePoints must be a number between 0 and 100 (points over cost; omit for no cap)");
135
+ }
136
+ if (r.pmMaxOpenPerEvent !== undefined) {
137
+ const n = r.pmMaxOpenPerEvent;
138
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 50)
139
+ add("skill_risk_pm_per_event", "risk.pmMaxOpenPerEvent must be a whole number between 1 and 50 (omit for no per-event cap)");
140
+ }
141
+ if (r.pmMaxOpenPerCoinHorizon !== undefined) {
142
+ const n = r.pmMaxOpenPerCoinHorizon;
143
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 50)
144
+ add("skill_risk_pm_coin_horizon", "risk.pmMaxOpenPerCoinHorizon must be a whole number between 1 and 50 (omit for no per-coin-horizon cap)");
145
+ }
146
+ if (r.pmMinMinutesToClose !== undefined) {
147
+ const m = r.pmMinMinutesToClose;
148
+ if (typeof m !== "number" || !Number.isFinite(m) || m < 0 || m > 10_080)
149
+ add("skill_risk_pm_close_cutoff", "risk.pmMinMinutesToClose must be a number of minutes between 0 and 10080 (omit for no cutoff)");
150
+ }
118
151
  // Fail-closed on the side restriction: a typo ("shorts_only") must never
119
152
  // silently mean "unrestricted" — that is exactly how a prose-only
120
153
  // constraint failed live on 2026-08-24.
@@ -30,8 +30,29 @@ const ALLOWED_KEYS = {
30
30
  // it) but was missing here, so any bundle actually SETTING it got an
31
31
  // unknown_key lint — the knob existed and was unreachable (audit rank 10).
32
32
  "triggerPolicy",
33
+ "signals",
34
+ "data",
35
+ "universe",
33
36
  ],
34
37
  trigger: ["cadence", "timezone", "events"],
38
+ signals: [
39
+ "rsiOversold",
40
+ "rsiOverbought",
41
+ "strongMovePct",
42
+ "leanMovePct",
43
+ "minStrength",
44
+ ],
45
+ data: ["indicatorRange"],
46
+ universe: [
47
+ "rank",
48
+ "minVolume24hUsd",
49
+ "excludeStablecoins",
50
+ "includeSectors",
51
+ "excludeSectors",
52
+ "sort",
53
+ "resolveTop",
54
+ "scanLimit",
55
+ ],
35
56
  triggerPolicy: [
36
57
  "mode",
37
58
  "skipLlmWhenNoTrigger",
@@ -51,6 +72,11 @@ const ALLOWED_KEYS = {
51
72
  "direction",
52
73
  "entryPredicates",
53
74
  "pmMinEntryProbabilityPct",
75
+ "pmMinEdgeGapPct",
76
+ "pmMaxEdgePoints",
77
+ "pmMaxOpenPerEvent",
78
+ "pmMaxOpenPerCoinHorizon",
79
+ "pmMinMinutesToClose",
54
80
  ],
55
81
  sizing: null,
56
82
  capitalSizing: [
@@ -141,6 +167,9 @@ export function strictLint(raw) {
141
167
  "sync",
142
168
  "killSwitch",
143
169
  "objective",
170
+ "signals",
171
+ "data",
172
+ "universe",
144
173
  ]) {
145
174
  if (isObj(raw[block]))
146
175
  lintKeys(block, raw[block], issues);
@@ -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];
@@ -1,5 +1,7 @@
1
+ import type { FuturesEntryEligibility } from "./futuresEligibility.js";
1
2
  import { IndicatorSet } from "./indicators.js";
2
3
  import type { DecisionInputRecord } from "./decisionReceipt.js";
4
+ import type { DecideRouteAttempt } from "./providers.js";
3
5
  export declare const SPEC_VERSION = "coinrithm.agent.v1";
4
6
  export type Venue = "spot" | "futures" | "pm";
5
7
  export declare const VENUES: readonly Venue[];
@@ -35,6 +37,11 @@ export interface RiskConfig {
35
37
  blocklist?: string[];
36
38
  direction?: "long_only" | "short_only";
37
39
  pmMinEntryProbabilityPct?: number;
40
+ pmMinEdgeGapPct?: number;
41
+ pmMaxEdgePoints?: number;
42
+ pmMaxOpenPerEvent?: number;
43
+ pmMaxOpenPerCoinHorizon?: number;
44
+ pmMinMinutesToClose?: number;
38
45
  }
39
46
  export interface LimitsConfig {
40
47
  maxTradesPerDay: number;
@@ -97,7 +104,7 @@ export interface ObjectiveConfig {
97
104
  secondary: string[];
98
105
  horizon?: string;
99
106
  }
100
- export declare const ALLOWED_CAPABILITIES: readonly ["websearch", "indicators", "news", "universe_scan"];
107
+ export declare const ALLOWED_CAPABILITIES: readonly ["websearch", "indicators", "news", "universe_scan", "whale_context"];
101
108
  export type Capability = (typeof ALLOWED_CAPABILITIES)[number];
102
109
  export interface AgentSpec {
103
110
  name: string;
@@ -115,6 +122,25 @@ export interface AgentSpec {
115
122
  objective?: ObjectiveConfig;
116
123
  capabilities: Capability[];
117
124
  triggerPolicy?: TriggerPolicy;
125
+ signals?: Partial<import("./signals.js").SignalThresholds>;
126
+ data?: {
127
+ indicatorRange?: import("./signals.js").IndicatorRange;
128
+ };
129
+ universe?: UniverseConfig;
130
+ }
131
+ /** Declared market boundaries; see universe.ts for semantics and defaults. */
132
+ export interface UniverseConfig {
133
+ rank?: {
134
+ min?: number;
135
+ max?: number;
136
+ };
137
+ minVolume24hUsd?: number;
138
+ excludeStablecoins?: boolean;
139
+ includeSectors?: string[];
140
+ excludeSectors?: string[];
141
+ sort?: "gainers_24h" | "losers_24h" | "abs_change_24h" | "abs_change_1h" | "volume_24h" | "rank";
142
+ resolveTop?: number;
143
+ scanLimit?: number;
118
144
  }
119
145
  export interface ParsedSkill {
120
146
  spec: AgentSpec;
@@ -135,8 +161,8 @@ export interface Freshness {
135
161
  basis?: string;
136
162
  }
137
163
  export interface IndicatorContext {
138
- range: "1D";
139
- nominalIntervalSeconds: 300;
164
+ range: import("./signals.js").IndicatorRange;
165
+ nominalIntervalSeconds: number;
140
166
  barCount: number;
141
167
  timestampedBarCount: number;
142
168
  checkedIntervalCount: number;
@@ -149,6 +175,105 @@ export interface IndicatorContext {
149
175
  intervalStatus: "regular" | "irregular" | "unknown";
150
176
  };
151
177
  }
178
+ export interface OpenInterestContext {
179
+ totalUsd: number;
180
+ venues: string[];
181
+ change1hPct: number | null;
182
+ change1hVenues: string[];
183
+ change24hPct: number | null;
184
+ change24hVenues: string[];
185
+ asOf: string;
186
+ stale: boolean;
187
+ }
188
+ export interface DatedValue {
189
+ value: number;
190
+ asOf: string;
191
+ stale: boolean;
192
+ }
193
+ export interface PositioningContext {
194
+ venue: string;
195
+ symbol: string;
196
+ longShortAccountRatio?: DatedValue;
197
+ longAccountPct?: DatedValue;
198
+ topTraderPositionRatio?: DatedValue;
199
+ takerBuySellRatio?: DatedValue;
200
+ }
201
+ export interface LiquidationWindow {
202
+ longLiquidatedUsdt: number;
203
+ shortLiquidatedUsdt: number;
204
+ events: number;
205
+ capturedPct: number;
206
+ }
207
+ export interface LiquidationContext {
208
+ venue: string;
209
+ instId: string;
210
+ last1h: LiquidationWindow;
211
+ last24h: LiquidationWindow;
212
+ lastEventAt: string | null;
213
+ }
214
+ export interface MacroQuote {
215
+ symbol: string;
216
+ label: string;
217
+ kind: string;
218
+ price: number;
219
+ change24hPct: number | null;
220
+ asOf: string;
221
+ stale: boolean;
222
+ }
223
+ export interface MacroContext {
224
+ note: string;
225
+ quotes: MacroQuote[];
226
+ }
227
+ export interface ChainTvlContext {
228
+ chain: string;
229
+ tvlUsd: number;
230
+ publishedAt: string | null;
231
+ fetchedAt: string | null;
232
+ sourceObservedAt: null;
233
+ stale: boolean;
234
+ dayAt: string | null;
235
+ change1dPct: number | null;
236
+ change7dPct: number | null;
237
+ }
238
+ export interface StablecoinSupplyContext {
239
+ totalUsd: number;
240
+ dayAt: string;
241
+ change1dPct: number | null;
242
+ change7dPct: number | null;
243
+ stale: boolean;
244
+ }
245
+ export interface DepthSideContext {
246
+ usd: number;
247
+ reachPct: number;
248
+ levels: number;
249
+ complete: boolean;
250
+ }
251
+ export interface DepthContext {
252
+ venue: string;
253
+ bid: DepthSideContext | null;
254
+ ask: DepthSideContext | null;
255
+ asOf: string;
256
+ stale: boolean;
257
+ }
258
+ export interface FundingVenueRate {
259
+ role: "settlement_reference" | "context_only";
260
+ venue: string;
261
+ symbol: string;
262
+ source: "paper_futures_reference" | "hyperliquid_predicted_fundings";
263
+ rateFraction: number;
264
+ intervalHours: number | null;
265
+ hourlyEquivalentFraction: number | null;
266
+ nextFundingTime: string | null;
267
+ fetchedAt: string;
268
+ sourceAt: null;
269
+ ageSeconds: number;
270
+ stale: boolean;
271
+ freshnessBasis: "collection_time";
272
+ }
273
+ export interface FundingByVenueContext {
274
+ rates: FundingVenueRate[];
275
+ sameTime: false;
276
+ }
152
277
  export interface WatchEntry {
153
278
  symbol: string;
154
279
  coinId: string | null;
@@ -158,12 +283,23 @@ export interface WatchEntry {
158
283
  change24h?: number;
159
284
  change7d?: number;
160
285
  sentimentBullishPct?: number;
286
+ sentimentTotalVotes?: number;
287
+ sentimentDayUtc?: string;
288
+ sentimentUpdatedAt?: string;
161
289
  freshness?: Freshness;
162
290
  indicators?: IndicatorSet;
163
291
  indicatorContext?: IndicatorContext;
164
292
  discovered?: boolean;
293
+ withinBoundaries?: boolean;
165
294
  slug?: string;
166
295
  fundamentals?: CoinFundamentals;
296
+ openInterest?: OpenInterestContext;
297
+ positioning?: PositioningContext;
298
+ liquidations?: LiquidationContext;
299
+ chainTvl?: ChainTvlContext;
300
+ depth?: DepthContext;
301
+ fundingByVenue?: FundingByVenueContext;
302
+ futuresEntryEligibility?: FuturesEntryEligibility;
167
303
  }
168
304
  export interface CoinFundamentals {
169
305
  categories?: string[];
@@ -222,6 +358,8 @@ export interface PmPosition {
222
358
  entryProbability?: number;
223
359
  currentProbability?: number;
224
360
  openedAt?: string;
361
+ coins?: string[];
362
+ endDate?: string;
225
363
  thesis?: ThesisView;
226
364
  }
227
365
  export interface PmResolution {
@@ -245,8 +383,45 @@ export interface PmMarket {
245
383
  volumeUsd?: number;
246
384
  endDate?: string;
247
385
  liquidityUsd?: number;
386
+ coins?: string[];
248
387
  quality?: PmQuality;
249
388
  decisionSupport?: PmDecisionSupport;
389
+ consensus?: PmConsensus | null;
390
+ rules?: PmSettlementRule | null;
391
+ outcomeRule?: PmOutcomeRule;
392
+ }
393
+ export interface PmCalibration {
394
+ settled: number;
395
+ brierAgent: number | null;
396
+ brierMarket: number | null;
397
+ meanForecastPct: number | null;
398
+ winRatePct: number | null;
399
+ bands: Array<{
400
+ fromPct: number;
401
+ toPct: number;
402
+ n: number;
403
+ meanForecastPct: number;
404
+ winRatePct: number;
405
+ }>;
406
+ }
407
+ export interface PmSettlementRule {
408
+ published: boolean;
409
+ text?: string;
410
+ sources?: string[];
411
+ scope?: "per_outcome";
412
+ }
413
+ export type PmOutcomeRule = {
414
+ primary: string;
415
+ secondary?: string;
416
+ } | {
417
+ unknown: "source_rules_unavailable" | "market_not_found" | "rule_missing" | "conflicting_duplicates" | "unreadable";
418
+ };
419
+ export interface PmConsensus {
420
+ prob: number;
421
+ venues: number;
422
+ spreadPts: number;
423
+ kind: "binary" | "leader";
424
+ outcome: string | null;
250
425
  }
251
426
  export interface PmQuality {
252
427
  decisionEligible?: boolean;
@@ -281,6 +456,80 @@ export interface NewsItem {
281
456
  ageHours?: number;
282
457
  publishedAt?: string;
283
458
  coins?: string[];
459
+ url?: string;
460
+ category?: string;
461
+ }
462
+ export type WhaleContextStatus = "available" | "partial" | "unavailable";
463
+ export type WhaleContextFailure = "timeout" | "network_error" | "http_error" | "invalid_payload";
464
+ export interface WhaleTradeContext {
465
+ source: string;
466
+ eventSlug: string;
467
+ eventTitle?: string;
468
+ marketQuestion?: string;
469
+ side: string;
470
+ outcome?: string;
471
+ usdValue?: number;
472
+ price?: number;
473
+ sourceMarketRef?: string;
474
+ valueBasis?: string;
475
+ evidenceType?: string;
476
+ evidenceRef?: string;
477
+ nativeValue?: number;
478
+ nativeCurrency?: string;
479
+ availability?: "live" | "delayed" | "unavailable";
480
+ tradedAt?: string;
481
+ observedAt?: string;
482
+ latencySeconds?: number;
483
+ walletAddress?: string;
484
+ }
485
+ export interface WhaleWalletFillContext {
486
+ eventSlug?: string;
487
+ eventTitle?: string;
488
+ marketQuestion?: string;
489
+ side?: string;
490
+ outcome?: string;
491
+ usdValue?: number;
492
+ price?: number;
493
+ evidenceType?: string;
494
+ tradedAt?: string;
495
+ sourceMarketRef?: string;
496
+ }
497
+ export interface WhaleWalletContext {
498
+ source: string;
499
+ address: string;
500
+ asOf?: string;
501
+ summary30d?: {
502
+ basis?: string;
503
+ windowStart?: string;
504
+ tradeCount?: number;
505
+ notionalUsd?: number;
506
+ buyNotionalUsd?: number;
507
+ sellNotionalUsd?: number;
508
+ };
509
+ rollup?: {
510
+ available?: boolean;
511
+ coveredDays?: number;
512
+ from?: string;
513
+ to?: string;
514
+ computedAt?: string;
515
+ };
516
+ daily: Array<{
517
+ day?: string;
518
+ tradeCount?: number;
519
+ buyCount?: number;
520
+ sellCount?: number;
521
+ notionalUsd?: number;
522
+ }>;
523
+ recentFills: WhaleWalletFillContext[];
524
+ }
525
+ export interface WhaleContext {
526
+ status: WhaleContextStatus;
527
+ fetchedAt: string;
528
+ coverage: "relevant_events" | "no_relevant_events";
529
+ trades: WhaleTradeContext[];
530
+ wallets: WhaleWalletContext[];
531
+ omitted: number;
532
+ reason?: WhaleContextFailure;
284
533
  }
285
534
  export interface Observation {
286
535
  asOf: string;
@@ -293,16 +542,21 @@ export interface Observation {
293
542
  pmPositions: PmPosition[];
294
543
  pmResolutions: PmResolution[];
295
544
  pmMarkets: PmMarket[];
545
+ pmCalibration?: PmCalibration;
296
546
  watch: WatchEntry[];
297
547
  setups: SetupSignal[];
298
548
  marketMood?: {
299
549
  fearGreed: number;
300
550
  label: string;
551
+ fetchedAt?: string;
301
552
  };
553
+ macro?: MacroContext;
554
+ stablecoinSupply?: StablecoinSupplyContext;
302
555
  syncCursor: string | null;
303
556
  newClosedTrades: Array<Record<string, unknown>>;
304
557
  polledBeforeWrite: boolean;
305
558
  news?: NewsItem[];
559
+ whaleContext?: WhaleContext;
306
560
  universeMovers?: Array<{
307
561
  symbol: string;
308
562
  name?: string;
@@ -522,19 +776,18 @@ export interface CycleResult {
522
776
  tokensIn?: number;
523
777
  tokensOut?: number;
524
778
  estimatedCostUsd?: number;
779
+ providerUsage?: {
780
+ promptTokens: number;
781
+ completionTokens: number;
782
+ cacheReadTokens?: number;
783
+ cacheWriteTokens?: number;
784
+ cacheWrite5mTokens?: number;
785
+ cacheWrite1hTokens?: number;
786
+ };
525
787
  effectiveProvider?: string;
526
788
  effectiveModel?: string;
527
789
  routeReason?: string;
528
- routeAttempts?: Array<{
529
- provider: string;
530
- model: string;
531
- outcome: "success" | "failed" | "deferred";
532
- failureClass?: "capacity" | "permanent" | "transient" | "malformed";
533
- status?: number;
534
- retryAfterMs?: number;
535
- latencyMs: number;
536
- error?: string;
537
- }>;
790
+ routeAttempts?: DecideRouteAttempt[];
538
791
  decisionType?: "act" | "skip" | "gate_skip" | "model_error";
539
792
  writeAttempted?: number;
540
793
  writeAccepted?: number;
@@ -50,6 +50,9 @@ export const OBJECTIVE_PRIMARIES = [
50
50
  // slice. `websearch` = external lookups (an injection surface + a cost — it can
51
51
  // inform reasoning but NEVER widen a cap, since caps live in the runner);
52
52
  // `indicators` = runner-computed RSI/MACD/etc. fed into the observation.
53
+ // `whale_context` reads bounded public prediction-market whale context only
54
+ // after the deterministic gate fires, and only when PM is in the venue scope.
55
+ // It never widens the candidate set or acts as a trading signal by itself.
53
56
  // `universe_scan` (2026-08-18, direct user request): each cycle the runner
54
57
  // pulls the top 24h movers across CoinRithm's tracked coin universe, resolves
55
58
  // the top few into FULL watch entries (price, sentiment, indicators when that
@@ -64,6 +67,7 @@ export const ALLOWED_CAPABILITIES = [
64
67
  "indicators",
65
68
  "news",
66
69
  "universe_scan",
70
+ "whale_context",
67
71
  ];
68
72
  export const ok = () => ({ valid: true });
69
73
  export const fail = (code, reason) => ({
@@ -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;