@coinrithm/mcp-trading 0.7.15 → 0.7.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +127 -8
  2. package/README.md +18 -3
  3. package/dist/agent/bench/bench.d.ts +59 -0
  4. package/dist/agent/bench/bench.js +319 -0
  5. package/dist/agent/bench/cassette.d.ts +97 -0
  6. package/dist/agent/bench/cassette.js +181 -0
  7. package/dist/agent/bench/costs.d.ts +28 -0
  8. package/dist/agent/bench/costs.js +60 -0
  9. package/dist/agent/bench/labelBuilder.d.ts +123 -0
  10. package/dist/agent/bench/labelBuilder.js +282 -0
  11. package/dist/agent/bench/labels.d.ts +75 -0
  12. package/dist/agent/bench/labels.js +259 -0
  13. package/dist/agent/bench/recordingClient.d.ts +51 -0
  14. package/dist/agent/bench/recordingClient.js +193 -0
  15. package/dist/agent/bench/replayClient.d.ts +41 -0
  16. package/dist/agent/bench/replayClient.js +262 -0
  17. package/dist/agent/bench/report.d.ts +115 -0
  18. package/dist/agent/bench/report.js +435 -0
  19. package/dist/agent/cli.d.ts +25 -0
  20. package/dist/agent/cli.js +300 -2
  21. package/dist/agent/client.d.ts +1 -0
  22. package/dist/agent/client.js +6 -0
  23. package/dist/agent/configurationWarnings.js +11 -0
  24. package/dist/agent/decision.d.ts +3 -0
  25. package/dist/agent/decision.js +85 -3
  26. package/dist/agent/decisionValidator.d.ts +30 -0
  27. package/dist/agent/decisionValidator.js +147 -2
  28. package/dist/agent/engine.d.ts +3 -3
  29. package/dist/agent/engine.js +3 -3
  30. package/dist/agent/observe.d.ts +27 -2
  31. package/dist/agent/observe.js +529 -89
  32. package/dist/agent/prompt.d.ts +4 -1
  33. package/dist/agent/prompt.js +98 -7
  34. package/dist/agent/promptTables.d.ts +1 -0
  35. package/dist/agent/promptTables.js +33 -0
  36. package/dist/agent/providerCapabilities.d.ts +6 -0
  37. package/dist/agent/providerCapabilities.js +23 -0
  38. package/dist/agent/providers.d.ts +25 -7
  39. package/dist/agent/providers.js +188 -48
  40. package/dist/agent/resolve.js +3 -0
  41. package/dist/agent/runner.d.ts +8 -0
  42. package/dist/agent/runner.js +59 -15
  43. package/dist/agent/setups.d.ts +2 -1
  44. package/dist/agent/setups.js +17 -16
  45. package/dist/agent/signals.d.ts +33 -0
  46. package/dist/agent/signals.js +89 -0
  47. package/dist/agent/skill.js +37 -0
  48. package/dist/agent/skillValidator.js +33 -0
  49. package/dist/agent/strictLint.js +29 -0
  50. package/dist/agent/types.d.ts +149 -12
  51. package/dist/agent/universe.d.ts +78 -0
  52. package/dist/agent/universe.js +215 -0
  53. package/package.json +3 -2
@@ -10,14 +10,15 @@
10
10
  //
11
11
  // Pure + stateless: no I/O, no model. Strategy-neutral — it reports the structure
12
12
  // and the trend-following bias; a contrarian agent fades the same facts.
13
+ import { DEFAULT_SIGNAL_THRESHOLDS } from "./signals.js";
13
14
  // Thresholds tuned to FIRE readily on a normal moving market (the failure mode we
14
15
  // are fixing is under-firing). A genuinely flat tape still yields an empty list,
15
16
  // which is the correct "nothing to do" signal.
16
- const STRONG_MOVE_PCT = 2.0; // |24h %| that counts as a real directional push
17
- const LEAN_MOVE_PCT = 0.8; // smaller move that still confirms an EMA-stack trend
18
- const RSI_OVERSOLD = 35;
19
- const RSI_OVERBOUGHT = 68;
20
- const MIN_STRENGTH = 0.5; // below this we do not flag (avoid noise)
17
+ // Defaults live in signals.ts (DEFAULT_SIGNAL_THRESHOLDS): |24h %| 2.0 for a
18
+ // strong push, 0.8 for a move that still confirms an EMA-stack trend, RSI 35 /
19
+ // 68, and 0.5 as the strength under which nothing is flagged. An agent may
20
+ // declare its own (spec.signals), which changes both this shortlist and the
21
+ // gate that decides whether the model is called.
21
22
  function pct(n) {
22
23
  return `${n >= 0 ? "+" : ""}${n.toFixed(1)}%`;
23
24
  }
@@ -34,7 +35,7 @@ export function baseSymbol(s) {
34
35
  // signal — the same structure is a momentum trade to a trend-follower and a
35
36
  // mean-reversion trade to a contrarian, so we surface both and let each agent pick
36
37
  // the one matching its style (fixes contrarians skipping "no setup fits me").
37
- function classify(w, openPositions) {
38
+ function classify(w, openPositions, t) {
38
39
  const ind = w.indicators;
39
40
  if (!ind)
40
41
  return [];
@@ -42,8 +43,8 @@ function classify(w, openPositions) {
42
43
  const rsi = ind.rsi14;
43
44
  const up = ind.ema20AboveEma50 === true && ind.aboveEma20 === true;
44
45
  const down = ind.ema20AboveEma50 === false && ind.aboveEma20 === false;
45
- const oversold = rsi != null && rsi <= RSI_OVERSOLD;
46
- const overbought = rsi != null && rsi >= RSI_OVERBOUGHT;
46
+ const oversold = rsi != null && rsi <= t.rsiOversold;
47
+ const overbought = rsi != null && rsi >= t.rsiOverbought;
47
48
  // Compact, factual note the model reads (no interpretation — just the structure).
48
49
  const facts = [`${pct(ch)} 24h`];
49
50
  if (up)
@@ -79,21 +80,21 @@ function classify(w, openPositions) {
79
80
  note,
80
81
  });
81
82
  }
82
- else if (up && ch >= LEAN_MOVE_PCT) {
83
+ else if (up && ch >= t.leanMovePct) {
83
84
  out.push({
84
85
  symbol: w.symbol,
85
86
  kind: "uptrend",
86
87
  bias: "long",
87
- strength: ch >= STRONG_MOVE_PCT ? 0.75 : 0.6,
88
+ strength: ch >= t.strongMovePct ? 0.75 : 0.6,
88
89
  note,
89
90
  });
90
91
  }
91
- else if (down && ch <= -LEAN_MOVE_PCT) {
92
+ else if (down && ch <= -t.leanMovePct) {
92
93
  out.push({
93
94
  symbol: w.symbol,
94
95
  kind: "downtrend",
95
96
  bias: "short",
96
- strength: ch <= -STRONG_MOVE_PCT ? 0.75 : 0.6,
97
+ strength: ch <= -t.strongMovePct ? 0.75 : 0.6,
97
98
  note,
98
99
  });
99
100
  }
@@ -115,7 +116,7 @@ function classify(w, openPositions) {
115
116
  note,
116
117
  });
117
118
  }
118
- else if (Math.abs(ch) >= STRONG_MOVE_PCT) {
119
+ else if (Math.abs(ch) >= t.strongMovePct) {
119
120
  // A strong move with no clean EMA stack — still tradeable momentum.
120
121
  out.push({
121
122
  symbol: w.symbol,
@@ -170,11 +171,11 @@ function classify(w, openPositions) {
170
171
  // Scan the whole watchlist, return the flagged setups strongest-first. An empty
171
172
  // list = a flat tape = a legitimate reason to skip new entries this cycle. Setups
172
173
  // on a symbol we already hold are tagged `held` (manage, don't re-open).
173
- export function scanSetups(watch, openPositions = []) {
174
+ export function scanSetups(watch, openPositions = [], thresholds = DEFAULT_SIGNAL_THRESHOLDS) {
174
175
  const out = [];
175
176
  for (const w of watch) {
176
- for (const s of classify(w, openPositions))
177
- if (s.strength >= MIN_STRENGTH)
177
+ for (const s of classify(w, openPositions, thresholds))
178
+ if (s.strength >= thresholds.minStrength)
178
179
  out.push(s);
179
180
  }
180
181
  return out.sort((a, b) => b.strength - a.strength);
@@ -0,0 +1,33 @@
1
+ import type { AgentSpec } from "./types.js";
2
+ export interface SignalThresholds {
3
+ rsiOversold: number;
4
+ rsiOverbought: number;
5
+ strongMovePct: number;
6
+ leanMovePct: number;
7
+ minStrength: number;
8
+ }
9
+ export declare const DEFAULT_SIGNAL_THRESHOLDS: SignalThresholds;
10
+ /** Candle range -> bar interval, as GET /api/agent/market/:id/candles serves. */
11
+ export declare const INDICATOR_RANGES: {
12
+ readonly "1D": 300;
13
+ readonly "1W": 900;
14
+ readonly "1M": 3600;
15
+ readonly "3M": 14400;
16
+ };
17
+ export type IndicatorRange = keyof typeof INDICATOR_RANGES;
18
+ export declare const DEFAULT_INDICATOR_RANGE: IndicatorRange;
19
+ /** Effective thresholds: declared values over the defaults. */
20
+ export declare function signalThresholdsOf(spec: {
21
+ signals?: Partial<SignalThresholds>;
22
+ }): SignalThresholds;
23
+ export declare function indicatorRangeOf(spec: {
24
+ data?: {
25
+ indicatorRange?: IndicatorRange;
26
+ };
27
+ }): IndicatorRange;
28
+ /** "5-minute", "15-minute", "1-hour", "4-hour". */
29
+ export declare function barLabel(range: IndicatorRange): string;
30
+ /** Validation issues for raw `signals` / `data` blocks ([] when valid). */
31
+ export declare function signalIssues(rawSignals: unknown, rawData: unknown): Array<[string, string]>;
32
+ /** Type guard used where only the spec shape is known. */
33
+ export type SignalSpec = Pick<AgentSpec, "signals" | "data">;
@@ -0,0 +1,89 @@
1
+ // What counts as a signal, per agent (owner 2026-10-07, Telegram 56624: show
2
+ // that personality changes trade handling; audit: setup thresholds and the
3
+ // indicator window were hard-coded for every agent).
4
+ //
5
+ // signals: # optional; absent = the platform defaults below
6
+ // rsiOversold: 30 # RSI at or under this flags an oversold fade
7
+ // rsiOverbought: 72 # RSI at or over this flags an overbought fade
8
+ // strongMovePct: 3 # |24h %| that counts as a strong push
9
+ // leanMovePct: 1 # smaller move that still confirms a trend stack
10
+ // minStrength: 0.6 # flags under this are not raised at all
11
+ // data:
12
+ // indicatorRange: 1W # 1D = 5m bars, 1W = 15m, 1M = 1h, 3M = 4h
13
+ //
14
+ // The flags feed both the prompt's setup shortlist AND the deterministic gate
15
+ // that decides whether a cycle calls the model at all, so these numbers change
16
+ // what the agent notices and when it thinks, not just what it is told.
17
+ export const DEFAULT_SIGNAL_THRESHOLDS = {
18
+ rsiOversold: 35,
19
+ rsiOverbought: 68,
20
+ strongMovePct: 2.0,
21
+ leanMovePct: 0.8,
22
+ minStrength: 0.5,
23
+ };
24
+ /** Candle range -> bar interval, as GET /api/agent/market/:id/candles serves. */
25
+ export const INDICATOR_RANGES = {
26
+ "1D": 300,
27
+ "1W": 900,
28
+ "1M": 3_600,
29
+ "3M": 14_400,
30
+ };
31
+ export const DEFAULT_INDICATOR_RANGE = "1D";
32
+ /** Effective thresholds: declared values over the defaults. */
33
+ export function signalThresholdsOf(spec) {
34
+ return { ...DEFAULT_SIGNAL_THRESHOLDS, ...(spec.signals ?? {}) };
35
+ }
36
+ export function indicatorRangeOf(spec) {
37
+ return spec.data?.indicatorRange ?? DEFAULT_INDICATOR_RANGE;
38
+ }
39
+ /** "5-minute", "15-minute", "1-hour", "4-hour". */
40
+ export function barLabel(range) {
41
+ const s = INDICATOR_RANGES[range];
42
+ return s < 3_600 ? `${s / 60}-minute` : `${s / 3_600}-hour`;
43
+ }
44
+ const BOUNDS = {
45
+ rsiOversold: [5, 50],
46
+ rsiOverbought: [50, 95],
47
+ strongMovePct: [0.1, 50],
48
+ leanMovePct: [0.05, 20],
49
+ minStrength: [0, 1],
50
+ };
51
+ /** Validation issues for raw `signals` / `data` blocks ([] when valid). */
52
+ export function signalIssues(rawSignals, rawData) {
53
+ const issues = [];
54
+ const add = (code, reason) => issues.push([code, reason]);
55
+ const isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
56
+ if (rawSignals !== undefined) {
57
+ if (!isMap(rawSignals)) {
58
+ add("skill_signals", "signals must be a mapping");
59
+ }
60
+ else {
61
+ for (const [key, [lo, hi]] of Object.entries(BOUNDS)) {
62
+ const v = rawSignals[key];
63
+ if (v === undefined)
64
+ continue;
65
+ if (typeof v !== "number" || !Number.isFinite(v) || v < lo || v > hi)
66
+ add("skill_signals_range", `signals.${key} must be a number between ${lo} and ${hi}`);
67
+ }
68
+ const t = signalThresholdsOf({
69
+ signals: rawSignals,
70
+ });
71
+ if (t.rsiOversold >= t.rsiOverbought)
72
+ add("skill_signals_rsi", "signals.rsiOversold must be below signals.rsiOverbought");
73
+ if (t.leanMovePct > t.strongMovePct)
74
+ add("skill_signals_moves", "signals.leanMovePct must not exceed signals.strongMovePct");
75
+ }
76
+ }
77
+ if (rawData !== undefined) {
78
+ if (!isMap(rawData)) {
79
+ add("skill_data", "data must be a mapping");
80
+ }
81
+ else if (rawData.indicatorRange !== undefined &&
82
+ // A real string only: String(['1W']) is "1W" and must not pass.
83
+ (typeof rawData.indicatorRange !== "string" ||
84
+ !Object.hasOwn(INDICATOR_RANGES, rawData.indicatorRange))) {
85
+ add("skill_data_range", `data.indicatorRange must be one of ${Object.keys(INDICATOR_RANGES).join(", ")}`);
86
+ }
87
+ }
88
+ return issues;
89
+ }
@@ -118,6 +118,32 @@ export function buildSpec(raw) {
118
118
  pmMinEntryProbabilityPct: risk.pmMinEntryProbabilityPct,
119
119
  }
120
120
  : {}),
121
+ // The three PM policy dials follow the same as-written rule.
122
+ ...(risk.pmMinEdgeGapPct !== undefined
123
+ ? {
124
+ pmMinEdgeGapPct: risk.pmMinEdgeGapPct,
125
+ }
126
+ : {}),
127
+ ...(risk.pmMaxEdgePoints !== undefined
128
+ ? {
129
+ pmMaxEdgePoints: risk.pmMaxEdgePoints,
130
+ }
131
+ : {}),
132
+ ...(risk.pmMaxOpenPerEvent !== undefined
133
+ ? {
134
+ pmMaxOpenPerEvent: risk.pmMaxOpenPerEvent,
135
+ }
136
+ : {}),
137
+ ...(risk.pmMaxOpenPerCoinHorizon !== undefined
138
+ ? {
139
+ pmMaxOpenPerCoinHorizon: risk.pmMaxOpenPerCoinHorizon,
140
+ }
141
+ : {}),
142
+ ...(risk.pmMinMinutesToClose !== undefined
143
+ ? {
144
+ pmMinMinutesToClose: risk.pmMinMinutesToClose,
145
+ }
146
+ : {}),
121
147
  maxLeverage: num(risk.maxLeverage, 1),
122
148
  perTradeMarginMusd: num(risk.perTradeMarginMusd, 0),
123
149
  maxConcurrentPositions: num(risk.maxConcurrentPositions, 0),
@@ -155,6 +181,17 @@ export function buildSpec(raw) {
155
181
  },
156
182
  objective: buildObjective(raw.objective),
157
183
  capabilities: strArr(raw.capabilities).filter((c) => ALLOWED_CAPABILITIES.includes(c)),
184
+ // Signal thresholds and data diet: carried AS WRITTEN so skillValidator
185
+ // fails closed on a malformed block instead of a typo meaning "defaults".
186
+ ...(raw.signals !== undefined
187
+ ? { signals: raw.signals }
188
+ : {}),
189
+ ...(raw.data !== undefined ? { data: raw.data } : {}),
190
+ // Market boundaries: carried AS WRITTEN (an object) so skillValidator can
191
+ // fail closed on a malformed block instead of a typo meaning "no limits".
192
+ ...(raw.universe !== undefined
193
+ ? { universe: raw.universe }
194
+ : {}),
158
195
  // OKF v2 (load-bearing): the gate reads this; omitted -> DEFAULT_TRIGGER_POLICY.
159
196
  // This is the agent's INTENT — the platform deployment overlay may tighten it
160
197
  // server-side, and it can never widen a hard cap (caps live in the runner).
@@ -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);
@@ -1,6 +1,7 @@
1
1
  import type { FuturesEntryEligibility } from "./futuresEligibility.js";
2
2
  import { IndicatorSet } from "./indicators.js";
3
3
  import type { DecisionInputRecord } from "./decisionReceipt.js";
4
+ import type { DecideRouteAttempt } from "./providers.js";
4
5
  export declare const SPEC_VERSION = "coinrithm.agent.v1";
5
6
  export type Venue = "spot" | "futures" | "pm";
6
7
  export declare const VENUES: readonly Venue[];
@@ -36,6 +37,11 @@ export interface RiskConfig {
36
37
  blocklist?: string[];
37
38
  direction?: "long_only" | "short_only";
38
39
  pmMinEntryProbabilityPct?: number;
40
+ pmMinEdgeGapPct?: number;
41
+ pmMaxEdgePoints?: number;
42
+ pmMaxOpenPerEvent?: number;
43
+ pmMaxOpenPerCoinHorizon?: number;
44
+ pmMinMinutesToClose?: number;
39
45
  }
40
46
  export interface LimitsConfig {
41
47
  maxTradesPerDay: number;
@@ -116,6 +122,25 @@ export interface AgentSpec {
116
122
  objective?: ObjectiveConfig;
117
123
  capabilities: Capability[];
118
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;
119
144
  }
120
145
  export interface ParsedSkill {
121
146
  spec: AgentSpec;
@@ -136,8 +161,8 @@ export interface Freshness {
136
161
  basis?: string;
137
162
  }
138
163
  export interface IndicatorContext {
139
- range: "1D";
140
- nominalIntervalSeconds: 300;
164
+ range: import("./signals.js").IndicatorRange;
165
+ nominalIntervalSeconds: number;
141
166
  barCount: number;
142
167
  timestampedBarCount: number;
143
168
  checkedIntervalCount: number;
@@ -150,6 +175,105 @@ export interface IndicatorContext {
150
175
  intervalStatus: "regular" | "irregular" | "unknown";
151
176
  };
152
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
+ }
153
277
  export interface WatchEntry {
154
278
  symbol: string;
155
279
  coinId: string | null;
@@ -166,8 +290,15 @@ export interface WatchEntry {
166
290
  indicators?: IndicatorSet;
167
291
  indicatorContext?: IndicatorContext;
168
292
  discovered?: boolean;
293
+ withinBoundaries?: boolean;
169
294
  slug?: string;
170
295
  fundamentals?: CoinFundamentals;
296
+ openInterest?: OpenInterestContext;
297
+ positioning?: PositioningContext;
298
+ liquidations?: LiquidationContext;
299
+ chainTvl?: ChainTvlContext;
300
+ depth?: DepthContext;
301
+ fundingByVenue?: FundingByVenueContext;
171
302
  futuresEntryEligibility?: FuturesEntryEligibility;
172
303
  }
173
304
  export interface CoinFundamentals {
@@ -227,6 +358,8 @@ export interface PmPosition {
227
358
  entryProbability?: number;
228
359
  currentProbability?: number;
229
360
  openedAt?: string;
361
+ coins?: string[];
362
+ endDate?: string;
230
363
  thesis?: ThesisView;
231
364
  }
232
365
  export interface PmResolution {
@@ -250,6 +383,7 @@ export interface PmMarket {
250
383
  volumeUsd?: number;
251
384
  endDate?: string;
252
385
  liquidityUsd?: number;
386
+ coins?: string[];
253
387
  quality?: PmQuality;
254
388
  decisionSupport?: PmDecisionSupport;
255
389
  consensus?: PmConsensus | null;
@@ -322,6 +456,8 @@ export interface NewsItem {
322
456
  ageHours?: number;
323
457
  publishedAt?: string;
324
458
  coins?: string[];
459
+ url?: string;
460
+ category?: string;
325
461
  }
326
462
  export type WhaleContextStatus = "available" | "partial" | "unavailable";
327
463
  export type WhaleContextFailure = "timeout" | "network_error" | "http_error" | "invalid_payload";
@@ -414,6 +550,8 @@ export interface Observation {
414
550
  label: string;
415
551
  fetchedAt?: string;
416
552
  };
553
+ macro?: MacroContext;
554
+ stablecoinSupply?: StablecoinSupplyContext;
417
555
  syncCursor: string | null;
418
556
  newClosedTrades: Array<Record<string, unknown>>;
419
557
  polledBeforeWrite: boolean;
@@ -638,19 +776,18 @@ export interface CycleResult {
638
776
  tokensIn?: number;
639
777
  tokensOut?: number;
640
778
  estimatedCostUsd?: number;
779
+ providerUsage?: {
780
+ promptTokens: number;
781
+ completionTokens: number;
782
+ cacheReadTokens?: number;
783
+ cacheWriteTokens?: number;
784
+ cacheWrite5mTokens?: number;
785
+ cacheWrite1hTokens?: number;
786
+ };
641
787
  effectiveProvider?: string;
642
788
  effectiveModel?: string;
643
789
  routeReason?: string;
644
- routeAttempts?: Array<{
645
- provider: string;
646
- model: string;
647
- outcome: "success" | "failed" | "deferred";
648
- failureClass?: "capacity" | "permanent" | "transient" | "malformed";
649
- status?: number;
650
- retryAfterMs?: number;
651
- latencyMs: number;
652
- error?: string;
653
- }>;
790
+ routeAttempts?: DecideRouteAttempt[];
654
791
  decisionType?: "act" | "skip" | "gate_skip" | "model_error";
655
792
  writeAttempted?: number;
656
793
  writeAccepted?: number;