@coinrithm/mcp-trading 0.1.8 → 0.3.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 +32 -0
- package/README.md +43 -19
- package/dist/agent/act.d.ts +4 -0
- package/dist/agent/act.js +114 -0
- package/dist/agent/capabilityGuard.d.ts +2 -0
- package/dist/agent/capabilityGuard.js +131 -0
- package/dist/agent/cli.d.ts +21 -0
- package/dist/agent/cli.js +382 -0
- package/dist/agent/client.d.ts +107 -0
- package/dist/agent/client.js +173 -0
- package/dist/agent/decision.d.ts +137 -0
- package/dist/agent/decision.js +118 -0
- package/dist/agent/decisionValidator.d.ts +16 -0
- package/dist/agent/decisionValidator.js +215 -0
- package/dist/agent/engine.d.ts +10 -0
- package/dist/agent/engine.js +16 -0
- package/dist/agent/extract.d.ts +4 -0
- package/dist/agent/extract.js +5 -0
- package/dist/agent/frontmatter.d.ts +5 -0
- package/dist/agent/frontmatter.js +19 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +10 -0
- package/dist/agent/indicators.d.ts +44 -0
- package/dist/agent/indicators.js +135 -0
- package/dist/agent/manifest.d.ts +15 -0
- package/dist/agent/manifest.js +40 -0
- package/dist/agent/mergeRules.d.ts +11 -0
- package/dist/agent/mergeRules.js +82 -0
- package/dist/agent/observe.d.ts +7 -0
- package/dist/agent/observe.js +244 -0
- package/dist/agent/prompt.d.ts +3 -0
- package/dist/agent/prompt.js +76 -0
- package/dist/agent/providers.d.ts +25 -0
- package/dist/agent/providers.js +143 -0
- package/dist/agent/resolve.d.ts +11 -0
- package/dist/agent/resolve.js +499 -0
- package/dist/agent/runEvidence.d.ts +6 -0
- package/dist/agent/runEvidence.js +23 -0
- package/dist/agent/runner.d.ts +19 -0
- package/dist/agent/runner.js +280 -0
- package/dist/agent/skill.d.ts +12 -0
- package/dist/agent/skill.js +136 -0
- package/dist/agent/skillValidator.d.ts +7 -0
- package/dist/agent/skillValidator.js +123 -0
- package/dist/agent/state.d.ts +7 -0
- package/dist/agent/state.js +96 -0
- package/dist/agent/strictLint.d.ts +3 -0
- package/dist/agent/strictLint.js +165 -0
- package/dist/agent/templates.d.ts +14 -0
- package/dist/agent/templates.js +192 -0
- package/dist/agent/types.d.ts +286 -0
- package/dist/agent/types.js +88 -0
- package/dist/agent/util.d.ts +13 -0
- package/dist/agent/util.js +116 -0
- package/dist/agent/version.d.ts +11 -0
- package/dist/agent/version.js +16 -0
- package/dist/client.d.ts +162 -0
- package/dist/http.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/tools.d.ts +3 -0
- package/dist/version.d.ts +1 -0
- package/package.json +78 -67
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Local run state: the cursor, dedupe set, daily counters, and the kill-switch
|
|
2
|
+
// inputs. Persisted to a JSON file so a re-run resumes where it left off.
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { dayKey } from "./util.js";
|
|
6
|
+
import { asObj, asNum } from "./extract.js";
|
|
7
|
+
// Disable after this many 429s in a session when killSwitch.onRateLimitPressure.
|
|
8
|
+
const RATE_LIMIT_PRESSURE_THRESHOLD = 5;
|
|
9
|
+
export function newState(runId) {
|
|
10
|
+
return {
|
|
11
|
+
runId,
|
|
12
|
+
cyclesRun: 0,
|
|
13
|
+
writesToday: 0,
|
|
14
|
+
realizedPnlMusd: 0,
|
|
15
|
+
peakRealizedMusd: 0,
|
|
16
|
+
consecutiveRejectCycles: 0,
|
|
17
|
+
consecutiveModelFailures: 0,
|
|
18
|
+
rateLimitHits: 0,
|
|
19
|
+
disabled: false,
|
|
20
|
+
dayKey: dayKey(),
|
|
21
|
+
cursor: null,
|
|
22
|
+
seen: [],
|
|
23
|
+
realizedPnlTodayMusd: 0,
|
|
24
|
+
consecutiveExecFailures: 0,
|
|
25
|
+
intentSeq: {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// A corrupt EXISTING state file is FAIL-CLOSED: we refuse to run rather than
|
|
29
|
+
// silently reset and (e.g.) re-enable a kill-switched agent or zero the daily
|
|
30
|
+
// counters. A missing file is fine (fresh start).
|
|
31
|
+
export function loadState(file, runId) {
|
|
32
|
+
if (file && existsSync(file)) {
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
throw new Error(`run state file is corrupt (${file}): ${e instanceof Error ? e.message : String(e)} — fix or delete it before running`);
|
|
39
|
+
}
|
|
40
|
+
const base = newState(parsed.runId ?? runId);
|
|
41
|
+
return rollDay({
|
|
42
|
+
...base,
|
|
43
|
+
...parsed,
|
|
44
|
+
seen: Array.isArray(parsed.seen) ? parsed.seen : [],
|
|
45
|
+
intentSeq: parsed.intentSeq && typeof parsed.intentSeq === "object" && !Array.isArray(parsed.intentSeq)
|
|
46
|
+
? parsed.intentSeq
|
|
47
|
+
: {},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return newState(runId);
|
|
51
|
+
}
|
|
52
|
+
export function saveState(file, state) {
|
|
53
|
+
if (!file)
|
|
54
|
+
return;
|
|
55
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
56
|
+
writeFileSync(file, JSON.stringify(state, null, 2), "utf8");
|
|
57
|
+
}
|
|
58
|
+
export function rollDay(state) {
|
|
59
|
+
const today = dayKey();
|
|
60
|
+
if (state.dayKey !== today) {
|
|
61
|
+
state.dayKey = today;
|
|
62
|
+
state.writesToday = 0;
|
|
63
|
+
state.realizedPnlTodayMusd = 0;
|
|
64
|
+
}
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
// Accrue realized PnL from newly-closed trades into both the session total (for
|
|
68
|
+
// drawdown) and today's total (for the daily-loss cap). Dedupe by the caller.
|
|
69
|
+
export function accrueRealized(state, closedTrades) {
|
|
70
|
+
for (const t of closedTrades) {
|
|
71
|
+
const pnl = asNum(asObj(t).realizedPnlMusd);
|
|
72
|
+
if (pnl != null) {
|
|
73
|
+
state.realizedPnlMusd += pnl;
|
|
74
|
+
state.realizedPnlTodayMusd += pnl;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (state.realizedPnlMusd > state.peakRealizedMusd)
|
|
78
|
+
state.peakRealizedMusd = state.realizedPnlMusd;
|
|
79
|
+
}
|
|
80
|
+
// Returns a disable reason if any kill-switch condition is tripped, else null.
|
|
81
|
+
export function checkKillSwitch(spec, state) {
|
|
82
|
+
const ks = spec.killSwitch;
|
|
83
|
+
if (ks.maxConsecutiveModelFailures > 0 && state.consecutiveModelFailures >= ks.maxConsecutiveModelFailures) {
|
|
84
|
+
return `consecutive model failures ${state.consecutiveModelFailures} >= ${ks.maxConsecutiveModelFailures}`;
|
|
85
|
+
}
|
|
86
|
+
if (ks.maxConsecutiveRejects > 0 && state.consecutiveRejectCycles >= ks.maxConsecutiveRejects) {
|
|
87
|
+
return `consecutive reject cycles ${state.consecutiveRejectCycles} >= ${ks.maxConsecutiveRejects}`;
|
|
88
|
+
}
|
|
89
|
+
if (ks.maxDrawdownMusd > 0 && state.peakRealizedMusd - state.realizedPnlMusd >= ks.maxDrawdownMusd) {
|
|
90
|
+
return `drawdown ${(state.peakRealizedMusd - state.realizedPnlMusd).toFixed(2)} >= ${ks.maxDrawdownMusd}`;
|
|
91
|
+
}
|
|
92
|
+
if (ks.onRateLimitPressure && state.rateLimitHits >= RATE_LIMIT_PRESSURE_THRESHOLD) {
|
|
93
|
+
return `rate-limit pressure: ${state.rateLimitHits} 429s this session`;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Strict key/enum lint over the RESOLVED frontmatter — the fix for the
|
|
2
|
+
// "silently coerced to a default" footgun (Codex audit #9). A typo like
|
|
3
|
+
// `maxLevrage: 3` must NOT silently become the default; it must be surfaced
|
|
4
|
+
// (with a "did you mean" suggestion). The runner uses this in hosted mode to
|
|
5
|
+
// fail closed; self-host can treat the issues as warnings.
|
|
6
|
+
import { PROVIDERS, VENUES, ALLOWED_CAPABILITIES, OBJECTIVE_PRIMARIES, } from "./types.js";
|
|
7
|
+
// Allowed keys per block. `null` = free-form (sizing is soft guidance; its
|
|
8
|
+
// enforced-key ban is handled in the resolver, not here).
|
|
9
|
+
const ALLOWED_KEYS = {
|
|
10
|
+
$root: [
|
|
11
|
+
"name",
|
|
12
|
+
"description",
|
|
13
|
+
"spec",
|
|
14
|
+
"mode",
|
|
15
|
+
"trigger",
|
|
16
|
+
"model",
|
|
17
|
+
"venues",
|
|
18
|
+
"risk",
|
|
19
|
+
"sizing",
|
|
20
|
+
"limits",
|
|
21
|
+
"abstention",
|
|
22
|
+
"sync",
|
|
23
|
+
"killSwitch",
|
|
24
|
+
"objective",
|
|
25
|
+
"capabilities",
|
|
26
|
+
"include",
|
|
27
|
+
"watchlist",
|
|
28
|
+
],
|
|
29
|
+
trigger: ["cadence", "timezone", "events"],
|
|
30
|
+
model: ["provider", "name", "baseUrl"],
|
|
31
|
+
risk: [
|
|
32
|
+
"maxLeverage",
|
|
33
|
+
"perTradeMarginMusd",
|
|
34
|
+
"maxConcurrentPositions",
|
|
35
|
+
"requireStopLoss",
|
|
36
|
+
"watchlist",
|
|
37
|
+
"blocklist",
|
|
38
|
+
],
|
|
39
|
+
sizing: null,
|
|
40
|
+
limits: [
|
|
41
|
+
"maxTradesPerDay",
|
|
42
|
+
"maxWritesPerCycle",
|
|
43
|
+
"maxDailyLossMusd",
|
|
44
|
+
"maxOpenMarginMusd",
|
|
45
|
+
],
|
|
46
|
+
abstention: [
|
|
47
|
+
"onStaleData",
|
|
48
|
+
"onWeakSignal",
|
|
49
|
+
"onMissingQuote",
|
|
50
|
+
"onInsufficientBalance",
|
|
51
|
+
"minConfidence",
|
|
52
|
+
],
|
|
53
|
+
sync: ["requirePollBeforeWrite"],
|
|
54
|
+
killSwitch: [
|
|
55
|
+
"maxDrawdownMusd",
|
|
56
|
+
"maxConsecutiveRejects",
|
|
57
|
+
"maxConsecutiveModelFailures",
|
|
58
|
+
"onRateLimitPressure",
|
|
59
|
+
],
|
|
60
|
+
objective: ["primary", "secondary", "horizon"],
|
|
61
|
+
};
|
|
62
|
+
export function levenshtein(a, b) {
|
|
63
|
+
const m = a.length;
|
|
64
|
+
const n = b.length;
|
|
65
|
+
const d = Array.from({ length: n + 1 }, (_, i) => i);
|
|
66
|
+
for (let i = 1; i <= m; i++) {
|
|
67
|
+
let prev = d[0];
|
|
68
|
+
d[0] = i;
|
|
69
|
+
for (let j = 1; j <= n; j++) {
|
|
70
|
+
const tmp = d[j];
|
|
71
|
+
d[j] = Math.min(d[j] + 1, d[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
72
|
+
prev = tmp;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return d[n];
|
|
76
|
+
}
|
|
77
|
+
function suggest(key, allowed) {
|
|
78
|
+
let best = null;
|
|
79
|
+
let bestDist = 3; // only suggest within edit distance 2
|
|
80
|
+
for (const cand of allowed) {
|
|
81
|
+
const dist = levenshtein(key.toLowerCase(), cand.toLowerCase());
|
|
82
|
+
if (dist < bestDist) {
|
|
83
|
+
bestDist = dist;
|
|
84
|
+
best = cand;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return best;
|
|
88
|
+
}
|
|
89
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
90
|
+
function lintKeys(block, obj, issues) {
|
|
91
|
+
const allowed = ALLOWED_KEYS[block];
|
|
92
|
+
if (!allowed)
|
|
93
|
+
return; // free-form (null) or unknown block
|
|
94
|
+
for (const k of Object.keys(obj)) {
|
|
95
|
+
if (!allowed.includes(k)) {
|
|
96
|
+
const s = suggest(k, allowed);
|
|
97
|
+
issues.push({
|
|
98
|
+
code: "unknown_key",
|
|
99
|
+
path: block === "$root" ? k : `${block}.${k}`,
|
|
100
|
+
message: `unknown key "${k}"${s ? ` — did you mean "${s}"?` : ""}`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function strictLint(raw) {
|
|
106
|
+
const issues = [];
|
|
107
|
+
lintKeys("$root", raw, issues);
|
|
108
|
+
for (const block of [
|
|
109
|
+
"trigger",
|
|
110
|
+
"model",
|
|
111
|
+
"risk",
|
|
112
|
+
"limits",
|
|
113
|
+
"abstention",
|
|
114
|
+
"sync",
|
|
115
|
+
"killSwitch",
|
|
116
|
+
"objective",
|
|
117
|
+
]) {
|
|
118
|
+
if (isObj(raw[block]))
|
|
119
|
+
lintKeys(block, raw[block], issues);
|
|
120
|
+
}
|
|
121
|
+
// Enum checks.
|
|
122
|
+
const model = raw.model;
|
|
123
|
+
if (isObj(model) &&
|
|
124
|
+
typeof model.provider === "string" &&
|
|
125
|
+
!PROVIDERS.includes(model.provider)) {
|
|
126
|
+
issues.push({
|
|
127
|
+
code: "bad_enum",
|
|
128
|
+
path: "model.provider",
|
|
129
|
+
message: `model.provider "${model.provider}" is not one of: ${PROVIDERS.join(", ")}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(raw.venues)) {
|
|
133
|
+
for (const v of raw.venues) {
|
|
134
|
+
if (!VENUES.includes(v)) {
|
|
135
|
+
issues.push({
|
|
136
|
+
code: "bad_enum",
|
|
137
|
+
path: "venues",
|
|
138
|
+
message: `unknown venue "${String(v)}" (allowed: ${VENUES.join(", ")})`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(raw.capabilities)) {
|
|
144
|
+
for (const c of raw.capabilities) {
|
|
145
|
+
if (!ALLOWED_CAPABILITIES.includes(c)) {
|
|
146
|
+
issues.push({
|
|
147
|
+
code: "bad_enum",
|
|
148
|
+
path: "capabilities",
|
|
149
|
+
message: `unknown capability "${String(c)}" (allowed: ${ALLOWED_CAPABILITIES.join(", ")})`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const objective = raw.objective;
|
|
155
|
+
if (isObj(objective) &&
|
|
156
|
+
typeof objective.primary === "string" &&
|
|
157
|
+
!OBJECTIVE_PRIMARIES.includes(objective.primary)) {
|
|
158
|
+
issues.push({
|
|
159
|
+
code: "bad_enum",
|
|
160
|
+
path: "objective.primary",
|
|
161
|
+
message: `objective.primary "${objective.primary}" is not one of: ${OBJECTIVE_PRIMARIES.join(", ")}`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return issues;
|
|
165
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type PresetName = "conservative" | "balanced" | "bold";
|
|
2
|
+
export declare const PRESET_NAMES: PresetName[];
|
|
3
|
+
export interface AgentTemplate {
|
|
4
|
+
frontmatter: Record<string, unknown>;
|
|
5
|
+
body: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function buildAgentObject(name: string, preset: PresetName): AgentTemplate;
|
|
8
|
+
export declare function renderFolderOfOne(name: string, preset: PresetName): string;
|
|
9
|
+
export declare function renderCoinrithmPin(): string;
|
|
10
|
+
export declare function renderRuntime(model: unknown, trigger: unknown): string;
|
|
11
|
+
export interface EjectResult {
|
|
12
|
+
files: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export declare function ejectFiles(fm: Record<string, unknown>, body: string): EjectResult;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Agent templates + risk presets for the scaffolder.
|
|
2
|
+
//
|
|
3
|
+
// A `new` agent is a folder-of-one (a single agent.md) that ALWAYS emits the
|
|
4
|
+
// hosted-required blocks (limits / abstention / sync / killSwitch) so it passes
|
|
5
|
+
// `validate --hosted` out of the box. Presets differ only within safe ranges:
|
|
6
|
+
// every preset is paper-only, keeps requireStopLoss=true, the kill-switch on,
|
|
7
|
+
// and stays under the server caps (leverage <= 20). "bold" is faster + larger
|
|
8
|
+
// but still safe.
|
|
9
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
10
|
+
import { COINRITHM_API } from "./version.js";
|
|
11
|
+
export const PRESET_NAMES = ["conservative", "balanced", "bold"];
|
|
12
|
+
const PRESETS = {
|
|
13
|
+
conservative: {
|
|
14
|
+
leverage: 2,
|
|
15
|
+
margin: 50,
|
|
16
|
+
maxPos: 2,
|
|
17
|
+
tradesDay: 6,
|
|
18
|
+
writesCycle: 1,
|
|
19
|
+
dailyLoss: 300,
|
|
20
|
+
openMargin: 600,
|
|
21
|
+
minConf: 0.6,
|
|
22
|
+
cadence: "4h",
|
|
23
|
+
drawdown: 300,
|
|
24
|
+
modelFail: 3,
|
|
25
|
+
},
|
|
26
|
+
balanced: {
|
|
27
|
+
leverage: 3,
|
|
28
|
+
margin: 100,
|
|
29
|
+
maxPos: 3,
|
|
30
|
+
tradesDay: 12,
|
|
31
|
+
writesCycle: 2,
|
|
32
|
+
dailyLoss: 750,
|
|
33
|
+
openMargin: 2000,
|
|
34
|
+
minConf: 0.55,
|
|
35
|
+
cadence: "1h",
|
|
36
|
+
drawdown: 750,
|
|
37
|
+
modelFail: 4,
|
|
38
|
+
},
|
|
39
|
+
bold: {
|
|
40
|
+
leverage: 5,
|
|
41
|
+
margin: 250,
|
|
42
|
+
maxPos: 5,
|
|
43
|
+
tradesDay: 24,
|
|
44
|
+
writesCycle: 3,
|
|
45
|
+
dailyLoss: 2000,
|
|
46
|
+
openMargin: 5000,
|
|
47
|
+
minConf: 0.5,
|
|
48
|
+
cadence: "1h",
|
|
49
|
+
drawdown: 2000,
|
|
50
|
+
modelFail: 5,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const THESIS_BODY = `# Momentum Futures — strategy
|
|
54
|
+
|
|
55
|
+
You operate a CoinRithm **paper-trading** futures account (50,000 virtual mUSD).
|
|
56
|
+
Everything here is simulated; it is not financial advice and never touches real
|
|
57
|
+
money. Edit this prose freely (any language) — it is your agent's borders.
|
|
58
|
+
|
|
59
|
+
## Each cycle
|
|
60
|
+
|
|
61
|
+
1. Ground yourself: read your portfolio and open positions first. Never assume
|
|
62
|
+
balances or what is already open.
|
|
63
|
+
2. Scan the watchlist. A candidate is a coin whose short and medium momentum
|
|
64
|
+
agree (both up, or both down) and is not already an open position.
|
|
65
|
+
3. Pick at most one strongest candidate. If nothing is clean, skip — a skipped
|
|
66
|
+
cycle is cheaper than a forced trade.
|
|
67
|
+
4. Quote before you open. Read the liquidation price and confirm it is sane. If
|
|
68
|
+
the quote is not eligible, relay the reason and stop.
|
|
69
|
+
5. Open small and protected: enter in the trend direction and set a stop-loss at
|
|
70
|
+
open. Place the take-profit a touch wider than the stop.
|
|
71
|
+
6. Stay in sync: poll your trades for any stop / take-profit / liquidation that
|
|
72
|
+
fired while you were not looking, and react to what actually happened.
|
|
73
|
+
|
|
74
|
+
The hard caps (leverage, margin, watchlist) live in the config blocks above and
|
|
75
|
+
are enforced by the runner — change them there, not in this prose.`;
|
|
76
|
+
const PERSONA_STUB = `# Persona
|
|
77
|
+
|
|
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
|
+
`;
|
|
81
|
+
export function buildAgentObject(name, preset) {
|
|
82
|
+
const p = PRESETS[preset];
|
|
83
|
+
const frontmatter = {
|
|
84
|
+
spec: "coinrithm.agent.v1",
|
|
85
|
+
name,
|
|
86
|
+
description: `Trend-following CoinRithm paper-trading agent (momentum-futures, ${preset}).`,
|
|
87
|
+
trigger: { cadence: p.cadence, timezone: "UTC" },
|
|
88
|
+
model: { provider: "anthropic", name: "claude-sonnet-4-6" },
|
|
89
|
+
venues: ["futures"],
|
|
90
|
+
risk: {
|
|
91
|
+
maxLeverage: p.leverage,
|
|
92
|
+
perTradeMarginMusd: p.margin,
|
|
93
|
+
maxConcurrentPositions: p.maxPos,
|
|
94
|
+
requireStopLoss: true,
|
|
95
|
+
watchlist: ["BTC", "ETH", "SOL"],
|
|
96
|
+
},
|
|
97
|
+
sizing: { riskRewardMin: 1.8, riskPerTradePct: 1, kellyFraction: 0.25 },
|
|
98
|
+
limits: {
|
|
99
|
+
maxTradesPerDay: p.tradesDay,
|
|
100
|
+
maxWritesPerCycle: p.writesCycle,
|
|
101
|
+
maxDailyLossMusd: p.dailyLoss,
|
|
102
|
+
maxOpenMarginMusd: p.openMargin,
|
|
103
|
+
},
|
|
104
|
+
abstention: {
|
|
105
|
+
onStaleData: true,
|
|
106
|
+
onWeakSignal: true,
|
|
107
|
+
onMissingQuote: true,
|
|
108
|
+
onInsufficientBalance: true,
|
|
109
|
+
minConfidence: p.minConf,
|
|
110
|
+
},
|
|
111
|
+
sync: { requirePollBeforeWrite: true },
|
|
112
|
+
killSwitch: {
|
|
113
|
+
maxDrawdownMusd: p.drawdown,
|
|
114
|
+
maxConsecutiveRejects: 0,
|
|
115
|
+
maxConsecutiveModelFailures: p.modelFail,
|
|
116
|
+
onRateLimitPressure: true,
|
|
117
|
+
},
|
|
118
|
+
objective: {
|
|
119
|
+
primary: "realized_pnl",
|
|
120
|
+
secondary: ["drawdown_control", "evidence_completeness"],
|
|
121
|
+
horizon: "7d",
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
return { frontmatter, body: THESIS_BODY };
|
|
125
|
+
}
|
|
126
|
+
export function renderFolderOfOne(name, preset) {
|
|
127
|
+
const { frontmatter, body } = buildAgentObject(name, preset);
|
|
128
|
+
return `---\n${stringifyYaml(frontmatter)}---\n\n${body}\n`;
|
|
129
|
+
}
|
|
130
|
+
export function renderCoinrithmPin() {
|
|
131
|
+
return stringifyYaml({
|
|
132
|
+
api: {
|
|
133
|
+
kind: COINRITHM_API.kind,
|
|
134
|
+
baseUrl: COINRITHM_API.baseUrl,
|
|
135
|
+
mcpUrl: COINRITHM_API.mcpUrl,
|
|
136
|
+
openapiVersion: COINRITHM_API.openapiVersion,
|
|
137
|
+
mcpPackage: COINRITHM_API.mcpPackage,
|
|
138
|
+
mcpVersion: COINRITHM_API.mcpVersion,
|
|
139
|
+
},
|
|
140
|
+
venues: ["spot", "futures", "pm"],
|
|
141
|
+
note: "Reference pin. The runner warns if this lags the live API; it does not block self-host.",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
export function renderRuntime(model, trigger) {
|
|
145
|
+
return stringifyYaml({ mode: "self-host", model, trigger });
|
|
146
|
+
}
|
|
147
|
+
// Split a folder-of-one frontmatter+body into the decomposed layout. The
|
|
148
|
+
// resolved AgentSpec must be unchanged (model/trigger via runtime.yaml extends;
|
|
149
|
+
// risk/limits/abstention/killSwitch via $ref; venues/sync/sizing/objective stay
|
|
150
|
+
// inline). Prose moves to character/thesis.md + persona.md.
|
|
151
|
+
export function ejectFiles(fm, body) {
|
|
152
|
+
const files = {};
|
|
153
|
+
files["character/thesis.md"] = (body.trim() || THESIS_BODY) + "\n";
|
|
154
|
+
files["character/persona.md"] = PERSONA_STUB;
|
|
155
|
+
if (fm.risk)
|
|
156
|
+
files["character/risk.yaml"] = stringifyYaml(fm.risk);
|
|
157
|
+
if (fm.limits)
|
|
158
|
+
files["character/limits.yaml"] = stringifyYaml(fm.limits);
|
|
159
|
+
if (fm.abstention)
|
|
160
|
+
files["character/abstention.yaml"] = stringifyYaml(fm.abstention);
|
|
161
|
+
if (fm.killSwitch)
|
|
162
|
+
files["safety/killSwitch.yaml"] = stringifyYaml(fm.killSwitch);
|
|
163
|
+
files["runtime.yaml"] = renderRuntime(fm.model, fm.trigger);
|
|
164
|
+
files["functionality/coinrithm.yaml"] = renderCoinrithmPin();
|
|
165
|
+
const agentFm = {
|
|
166
|
+
spec: fm.spec,
|
|
167
|
+
name: fm.name,
|
|
168
|
+
description: fm.description,
|
|
169
|
+
extends: ["runtime.yaml"],
|
|
170
|
+
venues: fm.venues,
|
|
171
|
+
sync: fm.sync,
|
|
172
|
+
};
|
|
173
|
+
if (fm.sizing)
|
|
174
|
+
agentFm.sizing = fm.sizing;
|
|
175
|
+
if (fm.objective)
|
|
176
|
+
agentFm.objective = fm.objective;
|
|
177
|
+
if (fm.capabilities)
|
|
178
|
+
agentFm.capabilities = fm.capabilities;
|
|
179
|
+
if (fm.risk)
|
|
180
|
+
agentFm.risk = { $ref: "character/risk.yaml" };
|
|
181
|
+
if (fm.limits)
|
|
182
|
+
agentFm.limits = { $ref: "character/limits.yaml" };
|
|
183
|
+
if (fm.abstention)
|
|
184
|
+
agentFm.abstention = { $ref: "character/abstention.yaml" };
|
|
185
|
+
if (fm.killSwitch)
|
|
186
|
+
agentFm.killSwitch = { $ref: "safety/killSwitch.yaml" };
|
|
187
|
+
files["agent.md"] =
|
|
188
|
+
`---\n${stringifyYaml(agentFm)}---\n\n` +
|
|
189
|
+
`Strategy lives in [character/thesis.md](character/thesis.md); ` +
|
|
190
|
+
`persona in [character/persona.md](character/persona.md).\n`;
|
|
191
|
+
return { files };
|
|
192
|
+
}
|