@coinrithm/mcp-trading 0.2.0 → 0.4.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 +49 -0
- package/README.md +2 -2
- package/dist/agent/act.d.ts +4 -0
- package/dist/agent/act.js +58 -9
- 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 +28 -6
- package/dist/agent/client.d.ts +107 -0
- package/dist/agent/client.js +26 -0
- package/dist/agent/decision.d.ts +137 -0
- package/dist/agent/decision.js +37 -4
- package/dist/agent/decisionValidator.d.ts +16 -0
- package/dist/agent/decisionValidator.js +118 -10
- 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/frontmatter.d.ts +5 -0
- package/dist/agent/index.d.ts +2 -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/mergeRules.d.ts +11 -0
- package/dist/agent/observe.d.ts +7 -0
- package/dist/agent/observe.js +142 -8
- package/dist/agent/prompt.d.ts +3 -0
- package/dist/agent/prompt.js +39 -8
- package/dist/agent/providers.d.ts +25 -0
- package/dist/agent/providers.js +10 -1
- package/dist/agent/resolve.d.ts +11 -0
- package/dist/agent/resolve.js +40 -3
- package/dist/agent/runEvidence.d.ts +6 -0
- package/dist/agent/runner.d.ts +19 -0
- package/dist/agent/runner.js +102 -19
- package/dist/agent/scorecard.d.ts +24 -0
- package/dist/agent/scorecard.js +177 -0
- package/dist/agent/skill.d.ts +12 -0
- package/dist/agent/skill.js +6 -2
- package/dist/agent/skillValidator.d.ts +7 -0
- package/dist/agent/state.d.ts +7 -0
- package/dist/agent/state.js +3 -1
- package/dist/agent/strictLint.d.ts +3 -0
- package/dist/agent/strictLint.js +2 -1
- package/dist/agent/templates.d.ts +14 -0
- package/dist/agent/types.d.ts +286 -0
- package/dist/agent/types.js +39 -1
- package/dist/agent/util.d.ts +13 -0
- package/dist/agent/util.js +4 -0
- package/dist/agent/version.d.ts +11 -0
- package/dist/agent/version.js +1 -1
- 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 +7 -2
package/dist/agent/runner.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
// The execution loop: observe -> decide (BYO model) -> validate -> act
|
|
2
|
-
// futures
|
|
3
|
-
// and exports run evidence. The client + provider
|
|
4
|
-
// fully unit-testable with no network
|
|
1
|
+
// The execution loop: observe -> decide (BYO model) -> validate -> act across
|
|
2
|
+
// spot, futures, and prediction markets. Dry-run never writes. Live uses
|
|
3
|
+
// idempotency keys + agentTrace and exports run evidence. The client + provider
|
|
4
|
+
// are injected so the loop is fully unit-testable with no network/model calls.
|
|
5
|
+
import { spotBuyCost, } from "./types.js";
|
|
5
6
|
import { observe } from "./observe.js";
|
|
6
7
|
import { buildSystemPrompt, buildUserPrompt } from "./prompt.js";
|
|
7
8
|
import { parseDecision } from "./decision.js";
|
|
8
9
|
import { validateAction } from "./decisionValidator.js";
|
|
9
10
|
import { fetchQuote, executeAction } from "./act.js";
|
|
10
11
|
import { makeDecisionId, makeTrace, exportRunEvidence } from "./runEvidence.js";
|
|
11
|
-
import { rollDay, checkKillSwitch, accrueRealized, saveState } from "./state.js";
|
|
12
|
+
import { rollDay, checkKillSwitch, accrueRealized, saveState, } from "./state.js";
|
|
12
13
|
import { asObj, asNum, asStr } from "./extract.js";
|
|
13
14
|
import { parseCadenceMs, sleep } from "./util.js";
|
|
14
15
|
// A stable idempotency-key component per distinct intent (so a lost response
|
|
@@ -23,7 +24,33 @@ function intentKeyOf(action) {
|
|
|
23
24
|
if (action.type === "futures_set_sltp") {
|
|
24
25
|
return `sltp:${action.positionId}`;
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
if (action.type === "spot_order") {
|
|
28
|
+
return `spot:${action.symbol.toUpperCase()}:${action.side}:${action.orderType}:${action.quantity}:${action.limitPrice ?? ""}:${action.stopPrice ?? ""}`;
|
|
29
|
+
}
|
|
30
|
+
if (action.type === "spot_cancel") {
|
|
31
|
+
return `cancel:${action.orderId}`;
|
|
32
|
+
}
|
|
33
|
+
if (action.type === "pm_open") {
|
|
34
|
+
return `pm:${action.source.toLowerCase()}:${action.slug.toLowerCase()}:${action.outcomeExternalMarketId}:${action.stakeMusd}`;
|
|
35
|
+
}
|
|
36
|
+
return "other"; // unreachable: every action type is handled above
|
|
37
|
+
}
|
|
38
|
+
// Estimated cash a successful action consumes (for the running-cash guard):
|
|
39
|
+
// futures margin, spot buy notional, or a PM stake. Closes/cancels/sells free
|
|
40
|
+
// cash or are neutral, so they consume nothing here. Spot buys use the SAME
|
|
41
|
+
// `spotBuyCost` helper the validator gates on, so the gate and this decrement
|
|
42
|
+
// never diverge. The `?? 0` is unreachable for an EXECUTED buy: the validator
|
|
43
|
+
// fails closed (missing_quote_price) on any buy whose cost can't be sized, so
|
|
44
|
+
// nothing with an undefined cost ever reaches execution to be decremented.
|
|
45
|
+
function cashConsumed(action, quote) {
|
|
46
|
+
if (action.type === "futures_open")
|
|
47
|
+
return action.marginMusd;
|
|
48
|
+
if (action.type === "pm_open")
|
|
49
|
+
return action.stakeMusd;
|
|
50
|
+
if (action.type === "spot_order" && action.side === "buy") {
|
|
51
|
+
return spotBuyCost(action, quote) ?? 0;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
27
54
|
}
|
|
28
55
|
export async function runCycle(deps) {
|
|
29
56
|
const { client, provider, spec, mergedProse, state, live, stateFile } = deps;
|
|
@@ -37,7 +64,13 @@ export async function runCycle(deps) {
|
|
|
37
64
|
state.disabledReason = tripped;
|
|
38
65
|
saveState(stateFile, state);
|
|
39
66
|
log(`disabled: ${tripped}`);
|
|
40
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
decision: "skip",
|
|
69
|
+
planned: [],
|
|
70
|
+
disabled: true,
|
|
71
|
+
disabledReason: tripped,
|
|
72
|
+
live,
|
|
73
|
+
};
|
|
41
74
|
}
|
|
42
75
|
const runId = state.runId;
|
|
43
76
|
const decisionId = makeDecisionId(state.cyclesRun);
|
|
@@ -55,12 +88,19 @@ export async function runCycle(deps) {
|
|
|
55
88
|
// not only realized losses.
|
|
56
89
|
const unrealized = observation.openPositions.reduce((s, p) => s + (p.unrealizedPnlMusd ?? 0), 0);
|
|
57
90
|
if (spec.killSwitch.maxDrawdownMusd > 0 &&
|
|
58
|
-
state.peakRealizedMusd - (state.realizedPnlMusd + unrealized) >=
|
|
91
|
+
state.peakRealizedMusd - (state.realizedPnlMusd + unrealized) >=
|
|
92
|
+
spec.killSwitch.maxDrawdownMusd) {
|
|
59
93
|
state.disabled = true;
|
|
60
94
|
state.disabledReason = `equity drawdown >= ${spec.killSwitch.maxDrawdownMusd}`;
|
|
61
95
|
saveState(stateFile, state);
|
|
62
96
|
log(`disabled: ${state.disabledReason}`);
|
|
63
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
decision: "skip",
|
|
99
|
+
planned: [],
|
|
100
|
+
disabled: true,
|
|
101
|
+
disabledReason: state.disabledReason,
|
|
102
|
+
live,
|
|
103
|
+
};
|
|
64
104
|
}
|
|
65
105
|
if (obs.skip) {
|
|
66
106
|
state.consecutiveRejectCycles += 1;
|
|
@@ -77,14 +117,26 @@ export async function runCycle(deps) {
|
|
|
77
117
|
state.consecutiveModelFailures += 1;
|
|
78
118
|
saveState(stateFile, state);
|
|
79
119
|
log(`model error: ${res.error}`);
|
|
80
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
decision: "skip",
|
|
122
|
+
skipReason: `model error: ${res.error}`,
|
|
123
|
+
planned: [],
|
|
124
|
+
modelFailed: true,
|
|
125
|
+
live,
|
|
126
|
+
};
|
|
81
127
|
}
|
|
82
128
|
const parsed = parseDecision(res.text);
|
|
83
129
|
if (!parsed.ok) {
|
|
84
130
|
state.consecutiveModelFailures += 1;
|
|
85
131
|
saveState(stateFile, state);
|
|
86
132
|
log(`model output invalid: ${parsed.error}`);
|
|
87
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
decision: "skip",
|
|
135
|
+
skipReason: `model output invalid: ${parsed.error}`,
|
|
136
|
+
planned: [],
|
|
137
|
+
modelFailed: true,
|
|
138
|
+
live,
|
|
139
|
+
};
|
|
88
140
|
}
|
|
89
141
|
state.consecutiveModelFailures = 0;
|
|
90
142
|
const decision = parsed.decision;
|
|
@@ -92,7 +144,12 @@ export async function runCycle(deps) {
|
|
|
92
144
|
state.consecutiveRejectCycles += 1;
|
|
93
145
|
saveState(stateFile, state);
|
|
94
146
|
log(`model chose skip${decision.reason ? `: ${decision.reason}` : ""}`);
|
|
95
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
decision: "skip",
|
|
149
|
+
skipReason: decision.reason ?? "model chose skip",
|
|
150
|
+
planned: [],
|
|
151
|
+
live,
|
|
152
|
+
};
|
|
96
153
|
}
|
|
97
154
|
// VALIDATE (+ ACT when live). Quote evidence is fetched by the runner.
|
|
98
155
|
const planned = [];
|
|
@@ -105,6 +162,7 @@ export async function runCycle(deps) {
|
|
|
105
162
|
let cashAvailableMusd = observation.cashAvailableMusd;
|
|
106
163
|
const realizedLossTodayMusd = Math.max(0, -state.realizedPnlTodayMusd);
|
|
107
164
|
const targetedPositionIds = [];
|
|
165
|
+
const targetedOrderIds = [];
|
|
108
166
|
let anyAccepted = false;
|
|
109
167
|
let anyExecuted = false;
|
|
110
168
|
let anyExecFailed = false;
|
|
@@ -112,6 +170,10 @@ export async function runCycle(deps) {
|
|
|
112
170
|
const quote = await fetchQuote(client, action, observation, baseTrace);
|
|
113
171
|
const ctx = {
|
|
114
172
|
spec,
|
|
173
|
+
// Inherit the decision-level confidence so the per-action abstention gate
|
|
174
|
+
// doesn't reject a model that reports conviction on the decision (the
|
|
175
|
+
// output contract) rather than on each action.
|
|
176
|
+
decisionConfidence: decision.confidence,
|
|
115
177
|
observation,
|
|
116
178
|
quote,
|
|
117
179
|
writesThisCycle,
|
|
@@ -121,10 +183,17 @@ export async function runCycle(deps) {
|
|
|
121
183
|
openMarginMusd,
|
|
122
184
|
realizedLossTodayMusd,
|
|
123
185
|
targetedPositionIds,
|
|
186
|
+
targetedOrderIds,
|
|
124
187
|
};
|
|
125
188
|
const v = validateAction(action, ctx);
|
|
126
189
|
if (!v.valid) {
|
|
127
|
-
planned.push({
|
|
190
|
+
planned.push({
|
|
191
|
+
action,
|
|
192
|
+
accepted: false,
|
|
193
|
+
code: v.code,
|
|
194
|
+
reason: v.reason,
|
|
195
|
+
quote,
|
|
196
|
+
});
|
|
128
197
|
log(`reject ${action.type}: ${v.code} (${v.reason})`);
|
|
129
198
|
continue;
|
|
130
199
|
}
|
|
@@ -132,6 +201,9 @@ export async function runCycle(deps) {
|
|
|
132
201
|
if (action.type === "futures_close" || action.type === "futures_set_sltp") {
|
|
133
202
|
targetedPositionIds.push(action.positionId);
|
|
134
203
|
}
|
|
204
|
+
if (action.type === "spot_cancel") {
|
|
205
|
+
targetedOrderIds.push(action.orderId);
|
|
206
|
+
}
|
|
135
207
|
if (!live) {
|
|
136
208
|
planned.push({ action, accepted: true, quote, executed: false });
|
|
137
209
|
log(`DRY-RUN: would ${action.type}`);
|
|
@@ -143,9 +215,15 @@ export async function runCycle(deps) {
|
|
|
143
215
|
const seq = state.intentSeq[intentKey] ?? 0;
|
|
144
216
|
const idem = `${runId}:${intentKey}:${seq}`;
|
|
145
217
|
const meta = action;
|
|
146
|
-
const trace = makeTrace(runId, decisionId, spec, meta.confidence, meta.rationaleSummary);
|
|
218
|
+
const trace = makeTrace(runId, decisionId, spec, meta.confidence ?? decision.confidence, meta.rationaleSummary);
|
|
147
219
|
const r = await executeAction(client, action, observation, trace, idem);
|
|
148
|
-
planned.push({
|
|
220
|
+
planned.push({
|
|
221
|
+
action,
|
|
222
|
+
accepted: true,
|
|
223
|
+
quote,
|
|
224
|
+
executed: r.ok,
|
|
225
|
+
result: r.data,
|
|
226
|
+
});
|
|
149
227
|
if (r.ok) {
|
|
150
228
|
anyExecuted = true;
|
|
151
229
|
state.intentSeq[intentKey] = seq + 1;
|
|
@@ -154,9 +232,11 @@ export async function runCycle(deps) {
|
|
|
154
232
|
if (action.type === "futures_open") {
|
|
155
233
|
openCount += 1;
|
|
156
234
|
openMarginMusd += action.marginMusd;
|
|
157
|
-
if (cashAvailableMusd != null)
|
|
158
|
-
cashAvailableMusd -= action.marginMusd;
|
|
159
235
|
}
|
|
236
|
+
// Decrement running cash by what this action consumed (futures margin /
|
|
237
|
+
// spot buy notional / PM stake) so a later action this cycle sees it spent.
|
|
238
|
+
if (cashAvailableMusd != null)
|
|
239
|
+
cashAvailableMusd -= cashConsumed(action, quote);
|
|
160
240
|
}
|
|
161
241
|
else {
|
|
162
242
|
anyExecFailed = true;
|
|
@@ -167,8 +247,11 @@ export async function runCycle(deps) {
|
|
|
167
247
|
// live write is not progress, or a persistently failing live agent would
|
|
168
248
|
// never trip the kill-switch.
|
|
169
249
|
const progressed = live ? anyExecuted : anyAccepted;
|
|
170
|
-
state.consecutiveRejectCycles = progressed
|
|
171
|
-
|
|
250
|
+
state.consecutiveRejectCycles = progressed
|
|
251
|
+
? 0
|
|
252
|
+
: state.consecutiveRejectCycles + 1;
|
|
253
|
+
state.consecutiveExecFailures =
|
|
254
|
+
anyExecFailed && !anyExecuted ? state.consecutiveExecFailures + 1 : 0;
|
|
172
255
|
state.rateLimitHits = client.rateLimitHits ?? state.rateLimitHits;
|
|
173
256
|
saveState(stateFile, state);
|
|
174
257
|
if (live && anyExecuted)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ScorecardInput {
|
|
2
|
+
realizedPnls: number[];
|
|
3
|
+
cumulative?: number[];
|
|
4
|
+
returns?: number[];
|
|
5
|
+
annualizationFactor?: number;
|
|
6
|
+
trials?: number;
|
|
7
|
+
predictions?: Array<{
|
|
8
|
+
p: number;
|
|
9
|
+
outcome: 0 | 1;
|
|
10
|
+
}>;
|
|
11
|
+
gates?: {
|
|
12
|
+
stopCoverage?: number;
|
|
13
|
+
evidenceCoverage?: number;
|
|
14
|
+
leakageClean?: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface Scorecard {
|
|
18
|
+
schema: "coinrithm.agent.scorecard.v1";
|
|
19
|
+
sampleSize: number;
|
|
20
|
+
returnsBasis: "returns" | "realized_pnl";
|
|
21
|
+
metrics: Record<string, number | null>;
|
|
22
|
+
contentHash: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function computeScorecard(input: ScorecardInput): Scorecard;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Deterministic scorecard engine — pure math over an agent's realized track
|
|
2
|
+
// record. The reproducible-evaluation half of coinrithm.agent.scorecard.v1
|
|
3
|
+
// (see examples/agents/_shared/scorecard.metrics.yaml + DECISIONS D17).
|
|
4
|
+
//
|
|
5
|
+
// DETERMINISM CONTRACT: the same inputs always yield the same metrics AND the
|
|
6
|
+
// same contentHash (sha256 of the canonicalized result), mirroring
|
|
7
|
+
// meta/manifest.lock.json. The engine NEVER calls the network or the model — it
|
|
8
|
+
// reads the run-evidence ledger export + realized equity curve (fetched by the
|
|
9
|
+
// caller) and computes, so tuning-to-the-metric is structurally impossible
|
|
10
|
+
// (leakage separation, arXiv 2512.02227). Every function returns null when there
|
|
11
|
+
// is too little data, so a thin record reports "n/a" rather than a fake number.
|
|
12
|
+
//
|
|
13
|
+
// SCIENTIFIC BASIS: risk-adjusted ratios (Sharpe/Sortino), skill-vs-luck
|
|
14
|
+
// deflation (probabilistic + deflated Sharpe, Bailey & Lopez de Prado), and
|
|
15
|
+
// calibration (Brier/ECE) for probabilistic calls — the reproducible-evaluation
|
|
16
|
+
// layer the field lacks (arXiv 2605.19337).
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
const round = (n, d = 6) => {
|
|
19
|
+
const f = 10 ** d;
|
|
20
|
+
return Math.round(n * f) / f;
|
|
21
|
+
};
|
|
22
|
+
const sum = (xs) => xs.reduce((a, b) => a + b, 0);
|
|
23
|
+
const mean = (xs) => (xs.length ? sum(xs) / xs.length : 0);
|
|
24
|
+
// Sample standard deviation (n-1). null for < 2 points (undefined dispersion).
|
|
25
|
+
function sampleStd(xs) {
|
|
26
|
+
if (xs.length < 2)
|
|
27
|
+
return null;
|
|
28
|
+
const m = mean(xs);
|
|
29
|
+
const v = sum(xs.map((x) => (x - m) ** 2)) / (xs.length - 1);
|
|
30
|
+
return Math.sqrt(v);
|
|
31
|
+
}
|
|
32
|
+
// Downside deviation about a 0 minimum-acceptable-return (Sortino denominator).
|
|
33
|
+
function downsideDev(xs) {
|
|
34
|
+
if (xs.length < 2)
|
|
35
|
+
return null;
|
|
36
|
+
const sq = xs.map((x) => (x < 0 ? x * x : 0));
|
|
37
|
+
return Math.sqrt(sum(sq) / xs.length);
|
|
38
|
+
}
|
|
39
|
+
// Population moments used by the (probabilistic) Sharpe formula.
|
|
40
|
+
function moment(xs, k) {
|
|
41
|
+
const m = mean(xs);
|
|
42
|
+
return sum(xs.map((x) => (x - m) ** k)) / xs.length;
|
|
43
|
+
}
|
|
44
|
+
// Standard normal CDF via an Abramowitz & Stegun erf approximation (max err ~1e-7).
|
|
45
|
+
function normalCdf(z) {
|
|
46
|
+
const t = 1 / (1 + 0.2316419 * Math.abs(z));
|
|
47
|
+
const d = 0.3989422804014327 * Math.exp(-(z * z) / 2);
|
|
48
|
+
const p = d *
|
|
49
|
+
t *
|
|
50
|
+
(0.31938153 +
|
|
51
|
+
t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
|
|
52
|
+
return z >= 0 ? 1 - p : p;
|
|
53
|
+
}
|
|
54
|
+
// Max peak-to-trough drawdown (mUSD, >= 0) on a cumulative series.
|
|
55
|
+
function maxDrawdown(cumulative) {
|
|
56
|
+
let peak = -Infinity;
|
|
57
|
+
let maxDd = 0;
|
|
58
|
+
for (const c of cumulative) {
|
|
59
|
+
if (!Number.isFinite(c))
|
|
60
|
+
continue;
|
|
61
|
+
peak = Math.max(peak, c);
|
|
62
|
+
maxDd = Math.max(maxDd, peak - c);
|
|
63
|
+
}
|
|
64
|
+
return Number.isFinite(maxDd) ? maxDd : 0;
|
|
65
|
+
}
|
|
66
|
+
// Per-observation Sharpe (mean/std), the basis for the probabilistic SR test.
|
|
67
|
+
function rawSharpe(rs) {
|
|
68
|
+
const sd = sampleStd(rs);
|
|
69
|
+
if (sd == null || sd === 0)
|
|
70
|
+
return null;
|
|
71
|
+
return mean(rs) / sd;
|
|
72
|
+
}
|
|
73
|
+
// Probabilistic / deflated Sharpe (Bailey & Lopez de Prado, approximated).
|
|
74
|
+
// PSR(SR0) = Phi( (SR - SR0) * sqrt(n-1) / sqrt(1 - skew*SR + ((kurt-1)/4)*SR^2) ).
|
|
75
|
+
// Deflation: SR0 = sqrt(2*ln(trials)) / sqrt(n) — the expected max per-obs Sharpe
|
|
76
|
+
// of `trials` random strategies (extreme-value heuristic). trials=1 -> SR0=0, so
|
|
77
|
+
// it reduces to the probabilistic Sharpe (already penalizing short, skewed tracks).
|
|
78
|
+
function deflatedSharpe(rs, trials) {
|
|
79
|
+
const n = rs.length;
|
|
80
|
+
if (n < 3)
|
|
81
|
+
return null;
|
|
82
|
+
const sr = rawSharpe(rs);
|
|
83
|
+
if (sr == null)
|
|
84
|
+
return null;
|
|
85
|
+
const m2 = moment(rs, 2);
|
|
86
|
+
if (m2 === 0)
|
|
87
|
+
return null;
|
|
88
|
+
const skew = moment(rs, 3) / m2 ** 1.5;
|
|
89
|
+
const kurt = moment(rs, 4) / m2 ** 2; // 3 for a normal distribution
|
|
90
|
+
const denom = Math.sqrt(Math.max(1e-9, 1 - skew * sr + ((kurt - 1) / 4) * sr * sr));
|
|
91
|
+
const sr0 = Math.sqrt(2 * Math.log(Math.max(1, trials))) / Math.sqrt(n);
|
|
92
|
+
const z = ((sr - sr0) * Math.sqrt(n - 1)) / denom;
|
|
93
|
+
return normalCdf(z);
|
|
94
|
+
}
|
|
95
|
+
// Brier score: mean((p - outcome)^2). Lower = better-calibrated.
|
|
96
|
+
function brier(preds) {
|
|
97
|
+
if (preds.length === 0)
|
|
98
|
+
return null;
|
|
99
|
+
return mean(preds.map((x) => (x.p - x.outcome) ** 2));
|
|
100
|
+
}
|
|
101
|
+
// Expected calibration error over 10 equal-width probability buckets.
|
|
102
|
+
function ece(preds) {
|
|
103
|
+
if (preds.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
const buckets = 10;
|
|
106
|
+
let total = 0;
|
|
107
|
+
for (let b = 0; b < buckets; b += 1) {
|
|
108
|
+
const lo = b / buckets;
|
|
109
|
+
const hi = (b + 1) / buckets;
|
|
110
|
+
const inB = preds.filter((x) => (b === buckets - 1 ? x.p >= lo && x.p <= hi : x.p >= lo && x.p < hi));
|
|
111
|
+
if (inB.length === 0)
|
|
112
|
+
continue;
|
|
113
|
+
const avgP = mean(inB.map((x) => x.p));
|
|
114
|
+
const avgO = mean(inB.map((x) => x.outcome));
|
|
115
|
+
total += (inB.length / preds.length) * Math.abs(avgP - avgO);
|
|
116
|
+
}
|
|
117
|
+
return total;
|
|
118
|
+
}
|
|
119
|
+
export function computeScorecard(input) {
|
|
120
|
+
const pnls = input.realizedPnls.filter(Number.isFinite);
|
|
121
|
+
const n = pnls.length;
|
|
122
|
+
const wins = pnls.filter((x) => x > 0);
|
|
123
|
+
const losses = pnls.filter((x) => x < 0);
|
|
124
|
+
const decided = wins.length + losses.length;
|
|
125
|
+
const grossWin = sum(wins);
|
|
126
|
+
const grossLoss = Math.abs(sum(losses));
|
|
127
|
+
const avgWin = wins.length ? grossWin / wins.length : 0;
|
|
128
|
+
const avgLoss = losses.length ? grossLoss / losses.length : 0;
|
|
129
|
+
const pWin = decided ? wins.length / decided : null;
|
|
130
|
+
const rs = input.returns && input.returns.length ? input.returns.filter(Number.isFinite) : pnls;
|
|
131
|
+
const returnsBasis = input.returns && input.returns.length ? "returns" : "realized_pnl";
|
|
132
|
+
const ann = input.annualizationFactor ?? 1;
|
|
133
|
+
const cumulative = input.cumulative && input.cumulative.length
|
|
134
|
+
? input.cumulative
|
|
135
|
+
: pnls.reduce((acc, x) => {
|
|
136
|
+
acc.push((acc.length ? acc[acc.length - 1] : 0) + x);
|
|
137
|
+
return acc;
|
|
138
|
+
}, []);
|
|
139
|
+
const sd = sampleStd(rs);
|
|
140
|
+
const dd = downsideDev(rs);
|
|
141
|
+
const sharpe = sd && sd !== 0 ? (mean(rs) / sd) * ann : null;
|
|
142
|
+
const sortino = dd && dd !== 0 ? (mean(rs) / dd) * ann : null;
|
|
143
|
+
const metrics = {
|
|
144
|
+
realized_pnl_musd: round(sum(pnls)),
|
|
145
|
+
trade_count: n,
|
|
146
|
+
decided_count: decided,
|
|
147
|
+
win_rate: pWin == null ? null : round(pWin),
|
|
148
|
+
expectancy_musd: pWin == null ? null : round(pWin * avgWin - (1 - pWin) * avgLoss),
|
|
149
|
+
profit_factor: grossLoss > 0 ? round(grossWin / grossLoss) : grossWin > 0 ? null : 0, // null = ∞ (no losses)
|
|
150
|
+
reward_to_risk: avgLoss > 0 ? round(avgWin / avgLoss) : null,
|
|
151
|
+
sharpe: sharpe == null ? null : round(sharpe),
|
|
152
|
+
sortino: sortino == null ? null : round(sortino),
|
|
153
|
+
deflated_sharpe: round0(deflatedSharpe(rs, input.trials ?? 1)),
|
|
154
|
+
max_drawdown_musd: round(maxDrawdown(cumulative)),
|
|
155
|
+
brier_score: round0(brier(input.predictions ?? [])),
|
|
156
|
+
calibration_error: round0(ece(input.predictions ?? [])),
|
|
157
|
+
stop_coverage: input.gates?.stopCoverage ?? null,
|
|
158
|
+
evidence_coverage: input.gates?.evidenceCoverage ?? null,
|
|
159
|
+
leakage_clean: input.gates?.leakageClean == null ? null : input.gates.leakageClean ? 1 : 0,
|
|
160
|
+
};
|
|
161
|
+
// Canonicalize (sorted keys) and hash, so the report card carries a stable,
|
|
162
|
+
// verifiable fingerprint — a scorecard whose hash does not reproduce is not trusted.
|
|
163
|
+
const canonical = JSON.stringify(Object.keys(metrics)
|
|
164
|
+
.sort()
|
|
165
|
+
.map((k) => [k, metrics[k]]));
|
|
166
|
+
const contentHash = createHash("sha256").update(canonical).digest("hex");
|
|
167
|
+
return {
|
|
168
|
+
schema: "coinrithm.agent.scorecard.v1",
|
|
169
|
+
sampleSize: n,
|
|
170
|
+
returnsBasis,
|
|
171
|
+
metrics,
|
|
172
|
+
contentHash,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function round0(n) {
|
|
176
|
+
return n == null ? null : round(n);
|
|
177
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AgentSpec, ParsedSkill, ResolvedAgent, ResolveIssue } from "./types.js";
|
|
2
|
+
export declare function buildSpec(raw: Record<string, unknown>): AgentSpec;
|
|
3
|
+
export declare function parseSkill(text: string): ParsedSkill;
|
|
4
|
+
export declare function loadSkill(path: string): ParsedSkill;
|
|
5
|
+
export interface LoadedAgent {
|
|
6
|
+
resolved: ResolvedAgent;
|
|
7
|
+
spec: AgentSpec;
|
|
8
|
+
body: string;
|
|
9
|
+
raw: Record<string, unknown>;
|
|
10
|
+
lint: ResolveIssue[];
|
|
11
|
+
}
|
|
12
|
+
export declare function loadAgent(inputPath: string, mode?: "self-host" | "hosted"): LoadedAgent;
|
package/dist/agent/skill.js
CHANGED
|
@@ -3,6 +3,7 @@ import { parseFrontmatter } from "./frontmatter.js";
|
|
|
3
3
|
import { VENUES, PROVIDERS, ALLOWED_CAPABILITIES, } from "./types.js";
|
|
4
4
|
import { resolveAgent, ResolveError } from "./resolve.js";
|
|
5
5
|
import { strictLint } from "./strictLint.js";
|
|
6
|
+
import { checkCapabilityDrift } from "./capabilityGuard.js";
|
|
6
7
|
// Safe defaults for the OPTIONAL policy blocks. A minimal self-host skill
|
|
7
8
|
// (name/description/spec/trigger/model/venues/risk) runs under these. Hosted
|
|
8
9
|
// mode requires them to be explicit (see skillValidator).
|
|
@@ -83,6 +84,7 @@ export function buildSpec(raw) {
|
|
|
83
84
|
maxConcurrentPositions: num(risk.maxConcurrentPositions, 0),
|
|
84
85
|
requireStopLoss: bool(risk.requireStopLoss, true),
|
|
85
86
|
watchlist: strArr(risk.watchlist),
|
|
87
|
+
blocklist: strArr(risk.blocklist),
|
|
86
88
|
},
|
|
87
89
|
limits: {
|
|
88
90
|
maxTradesPerDay: num(limits.maxTradesPerDay, DEFAULT_LIMITS.maxTradesPerDay),
|
|
@@ -124,9 +126,11 @@ export function loadSkill(path) {
|
|
|
124
126
|
export function loadAgent(inputPath, mode = "self-host") {
|
|
125
127
|
const resolved = resolveAgent(inputPath);
|
|
126
128
|
const raw = resolved.rawFrontmatter;
|
|
127
|
-
const
|
|
129
|
+
const spec = buildSpec(raw);
|
|
130
|
+
// strictLint = frontmatter keys/enums; capability drift = prose references to
|
|
131
|
+
// venues/actions/caps the runner (or this agent's venues) does not support.
|
|
132
|
+
const lint = [...strictLint(raw), ...checkCapabilityDrift(resolved, spec)];
|
|
128
133
|
if (mode === "hosted" && lint.length)
|
|
129
134
|
throw new ResolveError(lint);
|
|
130
|
-
const spec = buildSpec(raw);
|
|
131
135
|
return { resolved, spec, body: resolved.mergedProse, raw, lint };
|
|
132
136
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ParsedSkill, ValidationResult } from "./types.js";
|
|
2
|
+
export type SkillValidationMode = "self-host" | "hosted";
|
|
3
|
+
export interface SkillValidation {
|
|
4
|
+
valid: boolean;
|
|
5
|
+
issues: ValidationResult[];
|
|
6
|
+
}
|
|
7
|
+
export declare function validateSkill(parsed: ParsedSkill, mode?: SkillValidationMode): SkillValidation;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { RunState, AgentSpec } from "./types.js";
|
|
2
|
+
export declare function newState(runId: string): RunState;
|
|
3
|
+
export declare function loadState(file: string | undefined, runId: string): RunState;
|
|
4
|
+
export declare function saveState(file: string | undefined, state: RunState): void;
|
|
5
|
+
export declare function rollDay(state: RunState): RunState;
|
|
6
|
+
export declare function accrueRealized(state: RunState, closedTrades: Record<string, unknown>[]): void;
|
|
7
|
+
export declare function checkKillSwitch(spec: AgentSpec, state: RunState): string | null;
|
package/dist/agent/state.js
CHANGED
|
@@ -42,7 +42,9 @@ export function loadState(file, runId) {
|
|
|
42
42
|
...base,
|
|
43
43
|
...parsed,
|
|
44
44
|
seen: Array.isArray(parsed.seen) ? parsed.seen : [],
|
|
45
|
-
intentSeq: parsed.intentSeq && typeof parsed.intentSeq === "object"
|
|
45
|
+
intentSeq: parsed.intentSeq && typeof parsed.intentSeq === "object" && !Array.isArray(parsed.intentSeq)
|
|
46
|
+
? parsed.intentSeq
|
|
47
|
+
: {},
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
return newState(runId);
|
package/dist/agent/strictLint.js
CHANGED
|
@@ -34,6 +34,7 @@ const ALLOWED_KEYS = {
|
|
|
34
34
|
"maxConcurrentPositions",
|
|
35
35
|
"requireStopLoss",
|
|
36
36
|
"watchlist",
|
|
37
|
+
"blocklist",
|
|
37
38
|
],
|
|
38
39
|
sizing: null,
|
|
39
40
|
limits: [
|
|
@@ -58,7 +59,7 @@ const ALLOWED_KEYS = {
|
|
|
58
59
|
],
|
|
59
60
|
objective: ["primary", "secondary", "horizon"],
|
|
60
61
|
};
|
|
61
|
-
function levenshtein(a, b) {
|
|
62
|
+
export function levenshtein(a, b) {
|
|
62
63
|
const m = a.length;
|
|
63
64
|
const n = b.length;
|
|
64
65
|
const d = Array.from({ length: n + 1 }, (_, i) => i);
|
|
@@ -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;
|