@coinrithm/mcp-trading 0.3.0 → 0.5.0

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 (41) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/README.md +36 -9
  3. package/dist/agent/act.js +8 -1
  4. package/dist/agent/cli.d.ts +1 -0
  5. package/dist/agent/cli.js +58 -4
  6. package/dist/agent/client.d.ts +8 -1
  7. package/dist/agent/client.js +14 -2
  8. package/dist/agent/decision.d.ts +66 -63
  9. package/dist/agent/decision.js +95 -24
  10. package/dist/agent/decisionValidator.js +41 -1
  11. package/dist/agent/deploymentOverlay.d.ts +22 -0
  12. package/dist/agent/deploymentOverlay.js +55 -0
  13. package/dist/agent/gate.d.ts +9 -0
  14. package/dist/agent/gate.js +114 -0
  15. package/dist/agent/indicators.js +22 -7
  16. package/dist/agent/observe.js +201 -18
  17. package/dist/agent/prompt.d.ts +6 -2
  18. package/dist/agent/prompt.js +116 -26
  19. package/dist/agent/providers.d.ts +6 -0
  20. package/dist/agent/providers.js +89 -12
  21. package/dist/agent/resolve.js +28 -0
  22. package/dist/agent/resolvePm.d.ts +14 -0
  23. package/dist/agent/resolvePm.js +69 -0
  24. package/dist/agent/runner.d.ts +6 -1
  25. package/dist/agent/runner.js +312 -10
  26. package/dist/agent/scorecard.d.ts +24 -0
  27. package/dist/agent/scorecard.js +177 -0
  28. package/dist/agent/setups.d.ts +3 -0
  29. package/dist/agent/setups.js +133 -0
  30. package/dist/agent/skill.d.ts +1 -0
  31. package/dist/agent/skill.js +21 -3
  32. package/dist/agent/skillValidator.js +4 -2
  33. package/dist/agent/state.js +10 -2
  34. package/dist/agent/templates.js +8 -4
  35. package/dist/agent/types.d.ts +75 -2
  36. package/dist/agent/types.js +14 -1
  37. package/dist/agent/version.d.ts +2 -2
  38. package/dist/agent/version.js +12 -2
  39. package/dist/client.d.ts +2 -0
  40. package/dist/tools.js +28 -5
  41. package/package.json +1 -1
@@ -0,0 +1,133 @@
1
+ // Deterministic setup scan — the first slice of the preflight gate.
2
+ //
3
+ // The problem it solves: cautious free-tier brains were skipping 100% of cycles
4
+ // with "no clear setup" even while the tape moved 3-4%. The fix (drawn straight
5
+ // from content-engine's gate design): do NOT make the model decide whether a setup
6
+ // exists. Compute it deterministically from the indicators we already have, then
7
+ // hand the flagged setups to the model so it decides HOW to act, not WHETHER
8
+ // anything is happening. This flips the default from "no setup -> skip" to "here
9
+ // is the structure -> trade it (in your style) or give a real reason not to".
10
+ //
11
+ // Pure + stateless: no I/O, no model. Strategy-neutral — it reports the structure
12
+ // and the trend-following bias; a contrarian agent fades the same facts.
13
+ // Thresholds tuned to FIRE readily on a normal moving market (the failure mode we
14
+ // are fixing is under-firing). A genuinely flat tape still yields an empty list,
15
+ // 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)
21
+ function pct(n) {
22
+ return `${n >= 0 ? "+" : ""}${n.toFixed(1)}%`;
23
+ }
24
+ // Normalize a symbol to its base asset so a watch "BTC" matches an open position
25
+ // "BTC-PERP" / "BTCUSDT" when checking whether we already hold it.
26
+ export function baseSymbol(s) {
27
+ return (s ?? "")
28
+ .toUpperCase()
29
+ .replace(/[-/]?(PERP|USDT|USDC|USD)$/i, "")
30
+ .replace(/[^A-Z0-9]/g, "");
31
+ }
32
+ // Classify ONE coin into its setups. Usually one (the trend/breakout read), but a
33
+ // TRENDING coin that is also RSI-extreme emits a SECOND, counter-trend "fade"
34
+ // signal — the same structure is a momentum trade to a trend-follower and a
35
+ // mean-reversion trade to a contrarian, so we surface both and let each agent pick
36
+ // the one matching its style (fixes contrarians skipping "no setup fits me").
37
+ function classify(w, openPositions) {
38
+ const ind = w.indicators;
39
+ if (!ind)
40
+ return [];
41
+ const ch = w.change24h ?? 0;
42
+ const rsi = ind.rsi14;
43
+ const up = ind.ema20AboveEma50 === true && ind.aboveEma20 === true;
44
+ const down = ind.ema20AboveEma50 === false && ind.aboveEma20 === false;
45
+ const oversold = rsi != null && rsi <= RSI_OVERSOLD;
46
+ const overbought = rsi != null && rsi >= RSI_OVERBOUGHT;
47
+ // Compact, factual note the model reads (no interpretation — just the structure).
48
+ const facts = [`${pct(ch)} 24h`];
49
+ if (up)
50
+ facts.push("price>EMA20>EMA50 (uptrend)");
51
+ else if (down)
52
+ facts.push("price<EMA20<EMA50 (downtrend)");
53
+ if (rsi != null)
54
+ facts.push(`RSI ${Math.round(rsi)}${oversold ? " oversold" : overbought ? " overbought" : ""}`);
55
+ if (ind.brokeRecentHigh === true)
56
+ facts.push("broke 20-bar high");
57
+ if (ind.brokeRecentLow === true)
58
+ facts.push("broke 20-bar low");
59
+ if (ind.atr14 != null && ind.asOfClose)
60
+ facts.push(`ATR ${((100 * ind.atr14) / ind.asOfClose).toFixed(1)}% (stop ~1.5xATR)`);
61
+ const note = facts.join(" · ");
62
+ const out = [];
63
+ // Primary trend-following / breakout read.
64
+ if (ind.brokeRecentHigh === true) {
65
+ out.push({ symbol: w.symbol, kind: "breakout", bias: "long", strength: 0.8, note });
66
+ }
67
+ else if (ind.brokeRecentLow === true) {
68
+ out.push({ symbol: w.symbol, kind: "breakdown", bias: "short", strength: 0.8, note });
69
+ }
70
+ else if (up && ch >= LEAN_MOVE_PCT) {
71
+ out.push({ symbol: w.symbol, kind: "uptrend", bias: "long", strength: ch >= STRONG_MOVE_PCT ? 0.75 : 0.6, note });
72
+ }
73
+ else if (down && ch <= -LEAN_MOVE_PCT) {
74
+ out.push({ symbol: w.symbol, kind: "downtrend", bias: "short", strength: ch <= -STRONG_MOVE_PCT ? 0.75 : 0.6, note });
75
+ }
76
+ else if (overbought) {
77
+ out.push({ symbol: w.symbol, kind: "stretched", bias: "fade-short", strength: 0.55, note });
78
+ }
79
+ else if (oversold) {
80
+ out.push({ symbol: w.symbol, kind: "stretched", bias: "fade-long", strength: 0.55, note });
81
+ }
82
+ else if (Math.abs(ch) >= STRONG_MOVE_PCT) {
83
+ // A strong move with no clean EMA stack — still tradeable momentum.
84
+ out.push({ symbol: w.symbol, kind: ch > 0 ? "uptrend" : "downtrend", bias: ch > 0 ? "long" : "short", strength: 0.55, note });
85
+ }
86
+ // Secondary COUNTER-TREND fade: a standing trend that is ALSO RSI-extreme is a
87
+ // mean-reversion candidate. Only add it when the primary was the trend itself
88
+ // (so we don't double-list a pure stretched read).
89
+ const primaryIsTrend = out[0] && (out[0].kind === "uptrend" || out[0].kind === "downtrend" || out[0].kind === "breakout" || out[0].kind === "breakdown");
90
+ if (primaryIsTrend && (oversold || overbought)) {
91
+ out.push({
92
+ symbol: w.symbol,
93
+ kind: "stretched",
94
+ bias: oversold ? "fade-long" : "fade-short",
95
+ strength: 0.6,
96
+ note: `${note} — counter-trend fade (mean-reversion: ${oversold ? "oversold within downtrend" : "overbought within uptrend"})`,
97
+ });
98
+ }
99
+ // Position awareness: if we already hold this symbol, tag every signal with the
100
+ // side held AND the position's win/loss state right in the note — so the model
101
+ // ADDS to a winner (only with free margin), trails, or cuts a loser, instead of
102
+ // pointlessly re-opening the same size into the margin cap (the open_margin_
103
+ // exceeds_cap churn). A winner with room is the one case a same-side "open" is OK
104
+ // (scaling in); otherwise it's manage-only.
105
+ const wb = baseSymbol(w.symbol);
106
+ const pos = wb ? openPositions.find((p) => baseSymbol(p.symbol) === wb) : undefined;
107
+ const held = pos && (pos.side === "long" || pos.side === "short") ? pos.side : undefined;
108
+ if (held) {
109
+ const u = pos?.unrealizedPnlMusd;
110
+ const tag = u == null
111
+ ? ` [HELD ${held} — manage, do NOT re-open]`
112
+ : u >= 0
113
+ ? ` [HELD ${held}, +${Math.round(u)}mUSD WINNER — ADD only if you have free margin (scale into strength), else trail the stop; never re-open the same size]`
114
+ : ` [HELD ${held}, ${Math.round(u)}mUSD loser — trail or cut; do NOT average down or re-open]`;
115
+ for (const s of out) {
116
+ s.held = held;
117
+ s.note = s.note + tag;
118
+ }
119
+ }
120
+ return out;
121
+ }
122
+ // Scan the whole watchlist, return the flagged setups strongest-first. An empty
123
+ // list = a flat tape = a legitimate reason to skip new entries this cycle. Setups
124
+ // on a symbol we already hold are tagged `held` (manage, don't re-open).
125
+ export function scanSetups(watch, openPositions = []) {
126
+ const out = [];
127
+ for (const w of watch) {
128
+ for (const s of classify(w, openPositions))
129
+ if (s.strength >= MIN_STRENGTH)
130
+ out.push(s);
131
+ }
132
+ return out.sort((a, b) => b.strength - a.strength);
133
+ }
@@ -1,4 +1,5 @@
1
1
  import { AgentSpec, ParsedSkill, ResolvedAgent, ResolveIssue } from "./types.js";
2
+ export declare const UNLIMITED_TRADES_PER_DAY = 1000000;
2
3
  export declare function buildSpec(raw: Record<string, unknown>): AgentSpec;
3
4
  export declare function parseSkill(text: string): ParsedSkill;
4
5
  export declare function loadSkill(path: string): ParsedSkill;
@@ -1,14 +1,20 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { parseFrontmatter } from "./frontmatter.js";
3
- import { VENUES, PROVIDERS, ALLOWED_CAPABILITIES, } from "./types.js";
3
+ import { VENUES, PROVIDERS, ALLOWED_CAPABILITIES, DEFAULT_TRIGGER_POLICY, } from "./types.js";
4
4
  import { resolveAgent, ResolveError } from "./resolve.js";
5
5
  import { strictLint } from "./strictLint.js";
6
6
  import { checkCapabilityDrift } from "./capabilityGuard.js";
7
7
  // Safe defaults for the OPTIONAL policy blocks. A minimal self-host skill
8
8
  // (name/description/spec/trigger/model/venues/risk) runs under these. Hosted
9
9
  // mode requires them to be explicit (see skillValidator).
10
+ // A daily TRADE-COUNT cap of 0 (or absent) means UNLIMITED. We don't throttle how often
11
+ // an agent trades — the risk caps (daily loss, open margin, leverage, stops) are the real
12
+ // guardrails. "Unlimited" is normalised to a large finite value so cap-merge arithmetic
13
+ // (most-restrictive-wins) and JSON serialisation stay simple.
14
+ export const UNLIMITED_TRADES_PER_DAY = 1_000_000;
15
+ const normalizeTradeCap = (v) => (v <= 0 ? UNLIMITED_TRADES_PER_DAY : v);
10
16
  const DEFAULT_LIMITS = {
11
- maxTradesPerDay: 20,
17
+ maxTradesPerDay: UNLIMITED_TRADES_PER_DAY,
12
18
  maxWritesPerCycle: 2,
13
19
  maxDailyLossMusd: 5_000,
14
20
  maxOpenMarginMusd: 5_000,
@@ -67,6 +73,7 @@ export function buildSpec(raw) {
67
73
  const abst = obj(raw.abstention);
68
74
  const sync = obj(raw.sync);
69
75
  const ks = obj(raw.killSwitch);
76
+ const trig = obj(raw.triggerPolicy);
70
77
  const venues = strArr(raw.venues).filter((v) => VENUES.includes(v));
71
78
  return {
72
79
  name: typeof raw.name === "string" ? raw.name : "",
@@ -87,7 +94,7 @@ export function buildSpec(raw) {
87
94
  blocklist: strArr(risk.blocklist),
88
95
  },
89
96
  limits: {
90
- maxTradesPerDay: num(limits.maxTradesPerDay, DEFAULT_LIMITS.maxTradesPerDay),
97
+ maxTradesPerDay: normalizeTradeCap(num(limits.maxTradesPerDay, DEFAULT_LIMITS.maxTradesPerDay)),
91
98
  maxWritesPerCycle: num(limits.maxWritesPerCycle, DEFAULT_LIMITS.maxWritesPerCycle),
92
99
  maxDailyLossMusd: num(limits.maxDailyLossMusd, DEFAULT_LIMITS.maxDailyLossMusd),
93
100
  maxOpenMarginMusd: num(limits.maxOpenMarginMusd, DEFAULT_LIMITS.maxOpenMarginMusd),
@@ -110,6 +117,17 @@ export function buildSpec(raw) {
110
117
  },
111
118
  objective: buildObjective(raw.objective),
112
119
  capabilities: strArr(raw.capabilities).filter((c) => ALLOWED_CAPABILITIES.includes(c)),
120
+ // OKF v2 (load-bearing): the gate reads this; omitted -> DEFAULT_TRIGGER_POLICY.
121
+ // This is the agent's INTENT — the platform deployment overlay may tighten it
122
+ // server-side, and it can never widen a hard cap (caps live in the runner).
123
+ triggerPolicy: {
124
+ mode: trig.mode === "always" ? "always" : DEFAULT_TRIGGER_POLICY.mode,
125
+ skipLlmWhenNoTrigger: bool(trig.skipLlmWhenNoTrigger, DEFAULT_TRIGGER_POLICY.skipLlmWhenNoTrigger),
126
+ alwaysManageOpenPositions: bool(trig.alwaysManageOpenPositions, DEFAULT_TRIGGER_POLICY.alwaysManageOpenPositions),
127
+ maxLlmCallsPerHour: num(trig.maxLlmCallsPerHour, DEFAULT_TRIGGER_POLICY.maxLlmCallsPerHour),
128
+ debounceMinutes: num(trig.debounceMinutes, DEFAULT_TRIGGER_POLICY.debounceMinutes),
129
+ pmEvalCooldownMinutes: num(trig.pmEvalCooldownMinutes, DEFAULT_TRIGGER_POLICY.pmEvalCooldownMinutes),
130
+ },
113
131
  };
114
132
  }
115
133
  export function parseSkill(text) {
@@ -2,6 +2,8 @@ import { fail, VENUES, PROVIDERS, SPEC_VERSION, OBJECTIVE_PRIMARIES, ALLOWED_CAP
2
2
  import { parseCadenceMs, scanForSecrets } from "./util.js";
3
3
  const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4
4
  const isPosNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
5
+ // maxTradesPerDay accepts 0 as the explicit "unlimited daily trades" sentinel.
6
+ const isNonNegNum = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
5
7
  export function validateSkill(parsed, mode = "self-host") {
6
8
  const raw = parsed.raw;
7
9
  const issues = [];
@@ -99,8 +101,8 @@ export function validateSkill(parsed, mode = "self-host") {
99
101
  }
100
102
  else {
101
103
  const l = raw.limits;
102
- if (!isPosNum(l.maxTradesPerDay))
103
- add("skill_limits_trades", "limits.maxTradesPerDay must be a positive number");
104
+ if (!isNonNegNum(l.maxTradesPerDay))
105
+ add("skill_limits_trades", "limits.maxTradesPerDay must be a number >= 0 (0 = unlimited daily trades)");
104
106
  if (!isPosNum(l.maxWritesPerCycle))
105
107
  add("skill_limits_writes", "limits.maxWritesPerCycle must be a positive number");
106
108
  if (!isPosNum(l.maxDailyLossMusd))
@@ -77,11 +77,19 @@ export function accrueRealized(state, closedTrades) {
77
77
  if (state.realizedPnlMusd > state.peakRealizedMusd)
78
78
  state.peakRealizedMusd = state.realizedPnlMusd;
79
79
  }
80
+ // A transient model-failure streak (free models occasionally time out/hang) must
81
+ // never disable an agent on a hair-trigger, so the model-failure kill-switch is
82
+ // floored at this many consecutive failures regardless of an agent's own (lower)
83
+ // setting. The scheduler additionally auto-revives any model-failure disable.
84
+ const MODEL_FAILURE_FLOOR = 10;
80
85
  // Returns a disable reason if any kill-switch condition is tripped, else null.
81
86
  export function checkKillSwitch(spec, state) {
82
87
  const ks = spec.killSwitch;
83
- if (ks.maxConsecutiveModelFailures > 0 && state.consecutiveModelFailures >= ks.maxConsecutiveModelFailures) {
84
- return `consecutive model failures ${state.consecutiveModelFailures} >= ${ks.maxConsecutiveModelFailures}`;
88
+ if (ks.maxConsecutiveModelFailures > 0) {
89
+ const threshold = Math.max(ks.maxConsecutiveModelFailures, MODEL_FAILURE_FLOOR);
90
+ if (state.consecutiveModelFailures >= threshold) {
91
+ return `consecutive model failures ${state.consecutiveModelFailures} >= ${threshold}`;
92
+ }
85
93
  }
86
94
  if (ks.maxConsecutiveRejects > 0 && state.consecutiveRejectCycles >= ks.maxConsecutiveRejects) {
87
95
  return `consecutive reject cycles ${state.consecutiveRejectCycles} >= ${ks.maxConsecutiveRejects}`;
@@ -62,8 +62,10 @@ money. Edit this prose freely (any language) — it is your agent's borders.
62
62
  balances or what is already open.
63
63
  2. Scan the watchlist. A candidate is a coin whose short and medium momentum
64
64
  agree (both up, or both down) and is not already an open position.
65
- 3. Pick at most one strongest candidate. If nothing is clean, skip — a skipped
66
- cycle is cheaper than a forced trade.
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.
67
69
  4. Quote before you open. Read the liquidation price and confirm it is sane. If
68
70
  the quote is not eligible, relay the reason and stop.
69
71
  5. Open small and protected: enter in the trend direction and set a stop-loss at
@@ -75,8 +77,10 @@ The hard caps (leverage, margin, watchlist) live in the config blocks above and
75
77
  are enforced by the runner — change them there, not in this prose.`;
76
78
  const PERSONA_STUB = `# Persona
77
79
 
78
- Patient and selective. Prefers to skip rather than force a marginal trade.
79
- States its reasoning plainly and never frames paper results as real-money advice.
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.
80
84
  `;
81
85
  export function buildAgentObject(name, preset) {
82
86
  const p = PRESETS[preset];
@@ -4,7 +4,7 @@ export type Venue = "spot" | "futures" | "pm";
4
4
  export declare const VENUES: readonly Venue[];
5
5
  export declare const ACTION_TYPES: readonly ["futures_open", "futures_close", "futures_set_sltp", "spot_order", "spot_cancel", "pm_open"];
6
6
  export type ActionType = (typeof ACTION_TYPES)[number];
7
- export type ProviderName = "anthropic" | "openai" | "groq" | "nvidia" | "openai-compatible";
7
+ export type ProviderName = "anthropic" | "openai" | "groq" | "nvidia" | "gemini" | "openai-compatible";
8
8
  export declare const PROVIDERS: readonly ProviderName[];
9
9
  export interface ModelConfig {
10
10
  provider: ProviderName;
@@ -15,6 +15,15 @@ export interface TriggerConfig {
15
15
  cadence: string;
16
16
  timezone?: string;
17
17
  }
18
+ export interface TriggerPolicy {
19
+ mode: "event_driven" | "always";
20
+ skipLlmWhenNoTrigger: boolean;
21
+ alwaysManageOpenPositions: boolean;
22
+ maxLlmCallsPerHour: number;
23
+ debounceMinutes: number;
24
+ pmEvalCooldownMinutes: number;
25
+ }
26
+ export declare const DEFAULT_TRIGGER_POLICY: TriggerPolicy;
18
27
  export interface RiskConfig {
19
28
  maxLeverage: number;
20
29
  perTradeMarginMusd: number;
@@ -52,7 +61,7 @@ export interface ObjectiveConfig {
52
61
  secondary: string[];
53
62
  horizon?: string;
54
63
  }
55
- export declare const ALLOWED_CAPABILITIES: readonly ["websearch", "indicators"];
64
+ export declare const ALLOWED_CAPABILITIES: readonly ["websearch", "indicators", "news"];
56
65
  export type Capability = (typeof ALLOWED_CAPABILITIES)[number];
57
66
  export interface AgentSpec {
58
67
  name: string;
@@ -68,6 +77,7 @@ export interface AgentSpec {
68
77
  killSwitch: KillSwitchConfig;
69
78
  objective?: ObjectiveConfig;
70
79
  capabilities: Capability[];
80
+ triggerPolicy?: TriggerPolicy;
71
81
  }
72
82
  export interface ParsedSkill {
73
83
  spec: AgentSpec;
@@ -93,6 +103,7 @@ export interface WatchEntry {
93
103
  change1h?: number;
94
104
  change24h?: number;
95
105
  change7d?: number;
106
+ sentimentBullishPct?: number;
96
107
  freshness?: Freshness;
97
108
  indicators?: IndicatorSet;
98
109
  }
@@ -103,8 +114,14 @@ export interface OpenPosition {
103
114
  symbol?: string;
104
115
  side?: string;
105
116
  status?: string;
117
+ leverage?: number;
106
118
  marginMusd?: number;
107
119
  unrealizedPnlMusd?: number;
120
+ entryPrice?: number;
121
+ markPrice?: number;
122
+ liquidationPrice?: number;
123
+ stopLossPrice?: number;
124
+ takeProfitPrice?: number;
108
125
  }
109
126
  export interface SpotOrder {
110
127
  id: number;
@@ -121,15 +138,44 @@ export interface PmPosition {
121
138
  slug?: string;
122
139
  outcomeExternalMarketId?: string;
123
140
  stakeMusd?: number;
141
+ unrealizedPnlMusd?: number;
142
+ status?: string;
143
+ }
144
+ export interface PmResolution {
145
+ id: number;
146
+ eventTitle?: string;
147
+ slug?: string;
148
+ side?: string;
124
149
  status?: string;
150
+ pnlMusd?: number;
151
+ stakeMusd?: number;
125
152
  }
126
153
  export interface PmMarket {
154
+ ref?: string;
127
155
  source: string;
128
156
  slug: string;
129
157
  outcomeExternalMarketId: string;
158
+ outcomeName?: string;
159
+ probability?: number;
130
160
  title?: string;
131
161
  freshness?: Freshness;
132
162
  }
163
+ export interface SetupSignal {
164
+ symbol: string;
165
+ kind: "breakout" | "breakdown" | "uptrend" | "downtrend" | "stretched";
166
+ bias: "long" | "short" | "fade-long" | "fade-short";
167
+ strength: number;
168
+ note: string;
169
+ held?: "long" | "short";
170
+ }
171
+ export interface NewsItem {
172
+ title: string;
173
+ source?: string;
174
+ sentiment?: string;
175
+ importance?: number;
176
+ ageHours?: number;
177
+ coins?: string[];
178
+ }
133
179
  export interface Observation {
134
180
  asOf: string;
135
181
  scopes: string[];
@@ -138,11 +184,18 @@ export interface Observation {
138
184
  openPositions: OpenPosition[];
139
185
  openOrders: SpotOrder[];
140
186
  pmPositions: PmPosition[];
187
+ pmResolutions: PmResolution[];
141
188
  pmMarkets: PmMarket[];
142
189
  watch: WatchEntry[];
190
+ setups: SetupSignal[];
191
+ marketMood?: {
192
+ fearGreed: number;
193
+ label: string;
194
+ };
143
195
  syncCursor: string | null;
144
196
  newClosedTrades: Array<Record<string, unknown>>;
145
197
  polledBeforeWrite: boolean;
198
+ news?: NewsItem[];
146
199
  }
147
200
  export type ProposedAction = {
148
201
  type: "futures_open";
@@ -180,6 +233,7 @@ export type ProposedAction = {
180
233
  orderId: number;
181
234
  } | {
182
235
  type: "pm_open";
236
+ ref?: string;
183
237
  source: string;
184
238
  slug: string;
185
239
  outcomeExternalMarketId: string;
@@ -199,6 +253,7 @@ export declare function spotBuyCost(action: Extract<ProposedAction, {
199
253
  export interface Decision {
200
254
  decision: "skip" | "act";
201
255
  reason?: string;
256
+ rationale?: string;
202
257
  confidence?: number;
203
258
  actions: ProposedAction[];
204
259
  }
@@ -228,6 +283,13 @@ export interface RunState {
228
283
  realizedPnlTodayMusd: number;
229
284
  consecutiveExecFailures: number;
230
285
  intentSeq: Record<string, number>;
286
+ llmCallTimestamps?: number[];
287
+ lastLlmCallAt?: number;
288
+ lastTriggerFingerprint?: string;
289
+ journal?: Array<{
290
+ at: string;
291
+ did: string;
292
+ }>;
231
293
  }
232
294
  export interface AgentTrace {
233
295
  runId?: string;
@@ -256,11 +318,22 @@ export interface PlannedAction {
256
318
  export interface CycleResult {
257
319
  decision: "skip" | "act";
258
320
  skipReason?: string;
321
+ rationale?: string;
322
+ confidence?: number;
323
+ rawModelOutput?: string;
259
324
  planned: PlannedAction[];
260
325
  modelFailed?: boolean;
261
326
  disabled?: boolean;
262
327
  disabledReason?: string;
263
328
  live: boolean;
329
+ triggerCodes?: string[];
330
+ llmCallMade?: boolean;
331
+ tokensIn?: number;
332
+ tokensOut?: number;
333
+ estimatedCostUsd?: number;
334
+ decisionType?: "act" | "skip" | "gate_skip" | "model_error";
335
+ writeAttempted?: number;
336
+ writeAccepted?: number;
264
337
  }
265
338
  export interface ResolveIssue {
266
339
  code: string;
@@ -25,8 +25,17 @@ export const PROVIDERS = [
25
25
  "openai",
26
26
  "groq",
27
27
  "nvidia",
28
+ "gemini",
28
29
  "openai-compatible",
29
30
  ];
31
+ export const DEFAULT_TRIGGER_POLICY = {
32
+ mode: "event_driven",
33
+ skipLlmWhenNoTrigger: true,
34
+ alwaysManageOpenPositions: true,
35
+ maxLlmCallsPerHour: 0,
36
+ debounceMinutes: 0,
37
+ pmEvalCooldownMinutes: 10,
38
+ };
30
39
  // What the agent declares it is optimizing for — so two similar-looking agents
31
40
  // are distinguishable and the scorecard/Arena can read intent.
32
41
  export const OBJECTIVE_PRIMARIES = [
@@ -40,7 +49,11 @@ export const OBJECTIVE_PRIMARIES = [
40
49
  // slice. `websearch` = external lookups (an injection surface + a cost — it can
41
50
  // inform reasoning but NEVER widen a cap, since caps live in the runner);
42
51
  // `indicators` = runner-computed RSI/MACD/etc. fed into the observation.
43
- export const ALLOWED_CAPABILITIES = ["websearch", "indicators"];
52
+ export const ALLOWED_CAPABILITIES = [
53
+ "websearch",
54
+ "indicators",
55
+ "news",
56
+ ];
44
57
  export const ok = () => ({ valid: true });
45
58
  export const fail = (code, reason) => ({
46
59
  valid: false,
@@ -5,7 +5,7 @@ export declare const COINRITHM_API: {
5
5
  readonly kind: "coinrithm-agent-api";
6
6
  readonly baseUrl: "https://api.coinrithm.com";
7
7
  readonly mcpUrl: "https://mcp.coinrithm.com/mcp";
8
- readonly openapiVersion: "1.4.0";
8
+ readonly openapiVersion: "1.5.0";
9
9
  readonly mcpPackage: "@coinrithm/mcp-trading";
10
- readonly mcpVersion: "0.3.0";
10
+ readonly mcpVersion: string;
11
11
  };
@@ -1,8 +1,16 @@
1
1
  // Versions stamped into manifest.lock.json so a resolved agent is reproducible
2
2
  // only against the exact compile that produced it.
3
+ import { createRequire } from "node:module";
4
+ const require = createRequire(import.meta.url);
3
5
  export const RUNNER_VERSION = "0.1.0";
4
6
  export const RESOLVER_VERSION = "1";
5
7
  export const MANIFEST_SCHEMA = "coinrithm.manifest.v1";
8
+ // The published package version, read from package.json at runtime (same
9
+ // createRequire pattern as src/version.ts) so the API pin's mcpVersion can never
10
+ // drift from the real published version again. package.json lives outside
11
+ // rootDir=src; at runtime dist/agent/version.js resolves ../../package.json to
12
+ // the package root in both the repo and the published tarball.
13
+ const PACKAGE_VERSION = require("../../package.json").version;
6
14
  // The CoinRithm execution surface a generated agent talks to. Written into
7
15
  // functionality/coinrithm.yaml as a version PIN; the CLI warns when an agent's
8
16
  // pin lags this, but never blocks self-host use.
@@ -10,7 +18,9 @@ export const COINRITHM_API = {
10
18
  kind: "coinrithm-agent-api",
11
19
  baseUrl: "https://api.coinrithm.com",
12
20
  mcpUrl: "https://mcp.coinrithm.com/mcp",
13
- openapiVersion: "1.4.0",
21
+ // The API CONTRACT version (openapi.yaml info.version). Versioned independently
22
+ // from the npm package below — hand-bump this when the OpenAPI contract changes.
23
+ openapiVersion: "1.5.0",
14
24
  mcpPackage: "@coinrithm/mcp-trading",
15
- mcpVersion: "0.3.0",
25
+ mcpVersion: PACKAGE_VERSION,
16
26
  };
package/dist/client.d.ts CHANGED
@@ -111,6 +111,7 @@ export declare class CoinRithmClient {
111
111
  source: string;
112
112
  slug: string;
113
113
  outcomeExternalMarketId: string;
114
+ side?: "yes" | "no";
114
115
  stakeMusd: number;
115
116
  } & {
116
117
  agentTrace?: AgentTrace;
@@ -155,6 +156,7 @@ export declare class CoinRithmClient {
155
156
  source: string;
156
157
  slug: string;
157
158
  outcomeExternalMarketId: string;
159
+ side?: "yes" | "no";
158
160
  stakeMusd: number;
159
161
  idempotencyKey: string;
160
162
  }>, apiKey?: string): Promise<ApiResult>;
package/dist/tools.js CHANGED
@@ -6,7 +6,13 @@
6
6
  // isError results rather than thrown so the model can react.
7
7
  import { z } from "zod";
8
8
  import { bearerFromHeader } from "./client.js";
9
- const PAPER_NOTE = "Paper trading only — virtual funds (50,000 mUSD). Not financial advice.";
9
+ const PAPER_NOTE = "Paper trading only — virtual funds (50,000 mUSD). Not financial advice. " +
10
+ "Paper fills apply a disclosed execution cost folded into realized PnL: " +
11
+ "spot/futures pay a taker fee (spot market orders also pay half-spread + " +
12
+ "slippage); PM fills at the ask with size-based slippage and a Polymarket-" +
13
+ "shaped taker fee, with entryProbability kept at the mid for calibration. " +
14
+ "See the executionModel in quote/trade results — a rehearsal cost, not an " +
15
+ "exchange fill guarantee.";
10
16
  const API_RESULT_OUTPUT_SCHEMA = {
11
17
  httpStatus: z
12
18
  .number()
@@ -569,7 +575,9 @@ export function registerTools(server, client) {
569
575
  "estimate, max payout, eligibility, freshness, and decisionSupport " +
570
576
  "(market quality/liquidity/volume/spread tiers + flags) so you can " +
571
577
  "quote and gauge tradability in one call. Never mutates state. " +
572
- "stakeMusd must be > 0 (min to open is 10). " +
578
+ "stakeMusd must be > 0 (min to open is 10). Pass side: 'no' to quote " +
579
+ "backing the NO side (omitted = yes); a NO entry fills at 100 minus the " +
580
+ "outcome probability and pays out if the outcome resolves false. " +
573
581
  PAPER_NOTE,
574
582
  inputSchema: {
575
583
  source: z.string().describe("Source slug (e.g. kalshi, polymarket)."),
@@ -577,12 +585,18 @@ export function registerTools(server, client) {
577
585
  outcomeExternalMarketId: z
578
586
  .string()
579
587
  .describe("Case-sensitive outcome / market id."),
588
+ side: z
589
+ .enum(["yes", "no"])
590
+ .optional()
591
+ .describe("Which side of the binary outcome to back. NO pays out if it " +
592
+ "resolves false; fills at 100 minus the outcome probability. " +
593
+ "Omitted = yes."),
580
594
  stakeMusd: z.number().positive().describe("mUSD to stake (> 0)."),
581
595
  agentTrace: AGENT_TRACE_SCHEMA,
582
596
  },
583
597
  outputSchema: API_RESULT_OUTPUT_SCHEMA,
584
598
  annotations: readOnlyAnnotations("Prediction-market quote"),
585
- }, async ({ source, slug, outcomeExternalMarketId, stakeMusd, agentTrace }, extra) => present(await client.pmQuote({ source, slug, outcomeExternalMarketId, stakeMusd, agentTrace }, requestKey(extra))));
599
+ }, async ({ source, slug, outcomeExternalMarketId, side, stakeMusd, agentTrace }, extra) => present(await client.pmQuote({ source, slug, outcomeExternalMarketId, side, stakeMusd, agentTrace }, requestKey(extra))));
586
600
  server.registerTool("spot_quote", {
587
601
  title: "Spot quote",
588
602
  description: "Read-only spot MARKET quote: live execution price, estimated cost " +
@@ -800,7 +814,9 @@ export function registerTools(server, client) {
800
814
  description: "Open a mock prediction-market position (binary outcomes only). Requires " +
801
815
  "the trade:pm scope. Enabled now (server-flag gated — returns 403 'not " +
802
816
  "enabled' only if CoinRithm later disables it). idempotencyKey is " +
803
- "REQUIRED. stakeMusd >= 10. Quote first and CONFIRM with the user. " +
817
+ "REQUIRED. stakeMusd >= 10. Pass side: 'no' to back the NO side (omitted " +
818
+ "= yes); a NO entry fills at 100 minus the outcome probability and pays " +
819
+ "out if the outcome resolves false. Quote first and CONFIRM with the user. " +
804
820
  PAPER_NOTE,
805
821
  inputSchema: {
806
822
  source: z
@@ -810,6 +826,12 @@ export function registerTools(server, client) {
810
826
  outcomeExternalMarketId: z
811
827
  .string()
812
828
  .describe("Case-sensitive outcome or market id returned by discovery."),
829
+ side: z
830
+ .enum(["yes", "no"])
831
+ .optional()
832
+ .describe("Which side of the binary outcome to back. NO pays out if it " +
833
+ "resolves false; fills at 100 minus the outcome probability. " +
834
+ "Omitted = yes."),
813
835
  stakeMusd: z.number().min(10).describe("mUSD stake (>= 10)."),
814
836
  idempotencyKey: z
815
837
  .string()
@@ -821,10 +843,11 @@ export function registerTools(server, client) {
821
843
  annotations: mutatingAnnotations("Open prediction-market position", {
822
844
  idempotent: true,
823
845
  }),
824
- }, async ({ source, slug, outcomeExternalMarketId, stakeMusd, idempotencyKey, agentTrace, }, extra) => present(await client.openPmPosition({
846
+ }, async ({ source, slug, outcomeExternalMarketId, side, stakeMusd, idempotencyKey, agentTrace, }, extra) => present(await client.openPmPosition({
825
847
  source,
826
848
  slug,
827
849
  outcomeExternalMarketId,
850
+ side,
828
851
  stakeMusd,
829
852
  idempotencyKey,
830
853
  agentTrace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinrithm/mcp-trading",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "mcpName": "io.github.CoinRithm/mcp-trading",
5
5
  "description": "CoinRithm paper-trading toolkit: an MCP server (coinrithm-mcp) AND a self-host agent runner (coinrithm-agent) for spot, futures, and prediction markets with a user-minted API key.",
6
6
  "type": "module",