@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.
- package/CHANGELOG.md +54 -1
- package/README.md +36 -9
- package/dist/agent/act.js +8 -1
- package/dist/agent/cli.d.ts +1 -0
- package/dist/agent/cli.js +58 -4
- package/dist/agent/client.d.ts +8 -1
- package/dist/agent/client.js +14 -2
- package/dist/agent/decision.d.ts +66 -63
- package/dist/agent/decision.js +95 -24
- package/dist/agent/decisionValidator.js +41 -1
- package/dist/agent/deploymentOverlay.d.ts +22 -0
- package/dist/agent/deploymentOverlay.js +55 -0
- package/dist/agent/gate.d.ts +9 -0
- package/dist/agent/gate.js +114 -0
- package/dist/agent/indicators.js +22 -7
- package/dist/agent/observe.js +201 -18
- package/dist/agent/prompt.d.ts +6 -2
- package/dist/agent/prompt.js +116 -26
- package/dist/agent/providers.d.ts +6 -0
- package/dist/agent/providers.js +89 -12
- package/dist/agent/resolve.js +28 -0
- package/dist/agent/resolvePm.d.ts +14 -0
- package/dist/agent/resolvePm.js +69 -0
- package/dist/agent/runner.d.ts +6 -1
- package/dist/agent/runner.js +312 -10
- package/dist/agent/scorecard.d.ts +24 -0
- package/dist/agent/scorecard.js +177 -0
- package/dist/agent/setups.d.ts +3 -0
- package/dist/agent/setups.js +133 -0
- package/dist/agent/skill.d.ts +1 -0
- package/dist/agent/skill.js +21 -3
- package/dist/agent/skillValidator.js +4 -2
- package/dist/agent/state.js +10 -2
- package/dist/agent/templates.js +8 -4
- package/dist/agent/types.d.ts +75 -2
- package/dist/agent/types.js +14 -1
- package/dist/agent/version.d.ts +2 -2
- package/dist/agent/version.js +12 -2
- package/dist/client.d.ts +2 -0
- package/dist/tools.js +28 -5
- 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
|
+
}
|
package/dist/agent/skill.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent/skill.js
CHANGED
|
@@ -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:
|
|
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 (!
|
|
103
|
-
add("skill_limits_trades", "limits.maxTradesPerDay must be a
|
|
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))
|
package/dist/agent/state.js
CHANGED
|
@@ -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
|
|
84
|
-
|
|
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}`;
|
package/dist/agent/templates.js
CHANGED
|
@@ -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
|
|
66
|
-
|
|
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
|
-
|
|
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.
|
|
80
84
|
`;
|
|
81
85
|
export function buildAgentObject(name, preset) {
|
|
82
86
|
const p = PRESETS[preset];
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent/types.js
CHANGED
|
@@ -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 = [
|
|
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,
|
package/dist/agent/version.d.ts
CHANGED
|
@@ -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.
|
|
8
|
+
readonly openapiVersion: "1.5.0";
|
|
9
9
|
readonly mcpPackage: "@coinrithm/mcp-trading";
|
|
10
|
-
readonly mcpVersion:
|
|
10
|
+
readonly mcpVersion: string;
|
|
11
11
|
};
|
package/dist/agent/version.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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.
|
|
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
|
+
"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",
|