@coinrithm/mcp-trading 0.7.16 → 0.7.18
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 +903 -811
- package/README.md +488 -415
- package/dist/agent/capitalSizing.d.ts +10 -1
- package/dist/agent/capitalSizing.js +28 -4
- package/dist/agent/client.d.ts +13 -0
- package/dist/agent/client.js +17 -6
- package/dist/agent/decision.d.ts +21 -16
- package/dist/agent/decision.js +7 -0
- package/dist/agent/decisionValidator.d.ts +1 -0
- package/dist/agent/decisionValidator.js +1 -1
- package/dist/agent/engine.d.ts +3 -1
- package/dist/agent/engine.js +3 -1
- package/dist/agent/futuresCloseOutcome.d.ts +19 -0
- package/dist/agent/futuresCloseOutcome.js +54 -0
- package/dist/agent/futuresEligibility.d.ts +13 -0
- package/dist/agent/futuresEligibility.js +43 -5
- package/dist/agent/observe.d.ts +2 -1
- package/dist/agent/observe.js +1 -1
- package/dist/agent/pmPaperCandidates.d.ts +17 -0
- package/dist/agent/pmPaperCandidates.js +125 -0
- package/dist/agent/pmPaperCycle.d.ts +16 -0
- package/dist/agent/pmPaperCycle.js +372 -0
- package/dist/agent/pmPaperDecision.d.ts +187 -0
- package/dist/agent/pmPaperDecision.js +101 -0
- package/dist/agent/pmPaperPolicy.d.ts +71 -0
- package/dist/agent/pmPaperPolicy.js +46 -0
- package/dist/agent/prompt.d.ts +12 -1
- package/dist/agent/prompt.js +90 -3
- package/dist/agent/providerCapabilities.d.ts +1 -0
- package/dist/agent/providerCapabilities.js +4 -1
- package/dist/agent/providers.d.ts +1 -0
- package/dist/agent/providers.js +9 -4
- package/dist/agent/runner.d.ts +9 -1
- package/dist/agent/runner.js +152 -24
- package/dist/agent/skill.js +9 -0
- package/dist/agent/skillValidator.js +23 -0
- package/dist/agent/strictLint.js +1 -0
- package/dist/agent/templates.js +30 -30
- package/dist/agent/types.d.ts +31 -0
- package/dist/agent/version.d.ts +1 -1
- package/dist/agent/version.js +1 -1
- package/dist/client.d.ts +14 -0
- package/dist/client.js +31 -1
- package/dist/http.js +1 -1
- package/dist/pmPaperV2.d.ts +81 -0
- package/dist/pmPaperV2.js +53 -0
- package/dist/tools.js +106 -14
- package/package.json +100 -98
package/dist/agent/providers.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// single structured-JSON decision (parsed in decision.ts). No free-form tool
|
|
4
4
|
// execution — the model only proposes; the runner disposes.
|
|
5
5
|
import { retryAfterSeconds } from "../retryAfter.js";
|
|
6
|
-
import {
|
|
6
|
+
import { parseDecisionForContract, } from "./decision.js";
|
|
7
7
|
import { chatShapeFor, buildChatBody, withJsonContentTransport, canRetrySuperToolOutput, DECISION_TOOL_NAME, NVIDIA_BASE_URL as CAP_NVIDIA_BASE_URL, } from "./providerCapabilities.js";
|
|
8
8
|
// A token count as Anthropic reports it: a non-negative safe integer. Zero
|
|
9
9
|
// is a real count; anything else (missing, null, negative, NaN, fractional,
|
|
@@ -334,6 +334,7 @@ class OpenAiCompatProvider {
|
|
|
334
334
|
user: input.user,
|
|
335
335
|
maxTokens: input.maxTokens ?? 1024,
|
|
336
336
|
excludeActionTypes: input.excludeActionTypes,
|
|
337
|
+
decisionContract: input.decisionContract,
|
|
337
338
|
})),
|
|
338
339
|
});
|
|
339
340
|
if (!res.ok) {
|
|
@@ -423,7 +424,9 @@ class SameModelRetryProvider {
|
|
|
423
424
|
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
424
425
|
const first = await this.delegate.decide(input);
|
|
425
426
|
const firstFinished = Date.now();
|
|
426
|
-
const parsedFirst = first.ok
|
|
427
|
+
const parsedFirst = first.ok
|
|
428
|
+
? parseDecisionForContract(first.text, input.decisionContract)
|
|
429
|
+
: undefined;
|
|
427
430
|
const retryContent = first.ok &&
|
|
428
431
|
parsedFirst?.ok === false &&
|
|
429
432
|
this.contentRetry !== undefined &&
|
|
@@ -450,7 +453,7 @@ class SameModelRetryProvider {
|
|
|
450
453
|
timeoutMs: remainingMs,
|
|
451
454
|
});
|
|
452
455
|
const parsedRetry = retryResult.ok
|
|
453
|
-
?
|
|
456
|
+
? parseDecisionForContract(retryResult.text, input.decisionContract)
|
|
454
457
|
: undefined;
|
|
455
458
|
const result = retryResult.ok && parsedRetry?.ok === false
|
|
456
459
|
? {
|
|
@@ -461,7 +464,9 @@ class SameModelRetryProvider {
|
|
|
461
464
|
}
|
|
462
465
|
: retryResult;
|
|
463
466
|
const attempt = (res, latencyMs) => {
|
|
464
|
-
const parsed = res.ok
|
|
467
|
+
const parsed = res.ok
|
|
468
|
+
? parseDecisionForContract(res.text, input.decisionContract)
|
|
469
|
+
: undefined;
|
|
465
470
|
return {
|
|
466
471
|
provider: this.provider,
|
|
467
472
|
model: this.model,
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CoinRithmClient, ProvenanceReport } from "./client.js";
|
|
2
2
|
import { Provider } from "./providers.js";
|
|
3
|
-
import { AgentSpec, RunState, CycleResult, Decision, ProposedAction, PmMarket, PostedOpportunity, QuoteEvidence } from "./types.js";
|
|
3
|
+
import { AgentSpec, RunState, CycleResult, Decision, PlannedAction, ProposedAction, PmMarket, PostedOpportunity, QuoteEvidence, LastRejection, TakeProfitRepair } from "./types.js";
|
|
4
4
|
import { type ObserveOptions } from "./observe.js";
|
|
5
5
|
import { type DecisionInputRecord } from "./decisionReceipt.js";
|
|
6
6
|
export interface RunnerDeps {
|
|
@@ -30,7 +30,15 @@ export declare function sanitizeForecastProbability(raw: unknown): number | unde
|
|
|
30
30
|
export declare function repairFuturesTakeProfit(action: ProposedAction, quote?: QuoteEvidence, capitalMinimumRewardRisk?: number): {
|
|
31
31
|
action: ProposedAction;
|
|
32
32
|
repaired: boolean;
|
|
33
|
+
reason?: TakeProfitRepair["reason"];
|
|
33
34
|
};
|
|
35
|
+
export declare const LAST_REJECTIONS_MAX = 5;
|
|
36
|
+
/**
|
|
37
|
+
* Pure: the rejected actions of one validated decision, newest decision only,
|
|
38
|
+
* for the next prompt. Undefined when nothing was rejected, so a fixed
|
|
39
|
+
* mistake drops out at once.
|
|
40
|
+
*/
|
|
41
|
+
export declare function lastRejectionsOf(planned: readonly PlannedAction[], at: string): LastRejection[] | undefined;
|
|
34
42
|
export declare function rationaleForAction(a: ProposedAction, decisionRationale: string | undefined, perActionSummary: string | undefined, totalActions: number): string | undefined;
|
|
35
43
|
export declare function buildSkipOpportunity(decision: Decision, pmMarkets: PmMarket[], forecastEnabled: boolean): PostedOpportunity | null;
|
|
36
44
|
export declare function runCycle(deps: RunnerDeps): Promise<CycleResult>;
|
package/dist/agent/runner.js
CHANGED
|
@@ -19,6 +19,7 @@ import { parseDecision } from "./decision.js";
|
|
|
19
19
|
import { validateAction, isRiskIncreasingAction, pmEventKey, pmCoinHorizonOf, pmReferenceMs, } from "./decisionValidator.js";
|
|
20
20
|
import { resolvePmRef } from "./resolvePm.js";
|
|
21
21
|
import { fetchQuote, executeAction } from "./act.js";
|
|
22
|
+
import { executionOutcome, exitOutcomeMetadata, } from "./futuresCloseOutcome.js";
|
|
22
23
|
import { makeDecisionId, makeTrace, exportRunEvidence } from "./runEvidence.js";
|
|
23
24
|
import { rollDay, checkKillSwitch, saveState, isPermanentModelError, isAuthFailureSkip, PERMANENT_MODEL_ERROR_THRESHOLD, AUTH_FAILURE_THRESHOLD, } from "./state.js";
|
|
24
25
|
import { asObj, asNum, asStr } from "./extract.js";
|
|
@@ -137,8 +138,10 @@ function cashConsumed(action, quote) {
|
|
|
137
138
|
// WHOLE open (take_profit_not_*_mark) — the position never opens, so the model
|
|
138
139
|
// can't "let winners run". Here we substitute a sensible R:R target off the stop so
|
|
139
140
|
// the open succeeds with a valid TP. Only fires when a usable stop is present (so
|
|
140
|
-
// risk is computable) and the TP is
|
|
141
|
-
//
|
|
141
|
+
// risk is computable) and the TP is missing/wrong-side, or (capital policy only,
|
|
142
|
+
// owner GO 2026-10-08) on the right side but inside the agent's own reward:risk
|
|
143
|
+
// floor, where it is widened to the floor and never narrowed. Any other correct
|
|
144
|
+
// TP is left untouched. Systemic fix for the whole free-tier 8B/instruct fleet.
|
|
142
145
|
const DEFAULT_TP_RR = 1.5; // reward:risk of the substituted take-profit
|
|
143
146
|
export function repairFuturesTakeProfit(action, quote, capitalMinimumRewardRisk) {
|
|
144
147
|
if (action.type !== "futures_open")
|
|
@@ -155,7 +158,10 @@ export function repairFuturesTakeProfit(action, quote, capitalMinimumRewardRisk)
|
|
|
155
158
|
Number.isFinite(tp) &&
|
|
156
159
|
tp > 0 &&
|
|
157
160
|
(isLong ? tp > entry : tp < entry);
|
|
158
|
-
|
|
161
|
+
// A correct-side TP is only revisited against the agent's own capital R:R
|
|
162
|
+
// floor (owner GO 2026-10-08: widen a too-close target instead of refusing
|
|
163
|
+
// the trade; 114 capital_quote_reward_risk_too_low refusals in 10 h).
|
|
164
|
+
if (tpValid && capitalMinimumRewardRisk === undefined)
|
|
159
165
|
return { action, repaired: false };
|
|
160
166
|
// Stop must be on the correct side to imply a positive risk distance; if it
|
|
161
167
|
// isn't, leave the action for the validator to reject (don't fabricate).
|
|
@@ -176,15 +182,30 @@ export function repairFuturesTakeProfit(action, quote, capitalMinimumRewardRisk)
|
|
|
176
182
|
return { action, repaired: false };
|
|
177
183
|
const notional = action.marginMusd * action.leverage;
|
|
178
184
|
const fee = bps / 10_000;
|
|
185
|
+
// Fail closed on economics that cannot yield a finite target.
|
|
186
|
+
if (!(Number.isFinite(notional) && notional > 0) || !(fee < 1))
|
|
187
|
+
return { action, repaired: false };
|
|
179
188
|
const stopRisk = (notional * risk) / entry + entryFee + ((notional * sl) / entry) * fee;
|
|
180
189
|
const required = (capitalMinimumRewardRisk + 1e-8) * stopRisk;
|
|
181
190
|
target = isLong
|
|
182
191
|
? (entry * (notional + entryFee + required)) / (notional * (1 - fee))
|
|
183
192
|
: (entry * (notional - entryFee - required)) / (notional * (1 + fee));
|
|
184
193
|
}
|
|
185
|
-
if (!(target > 0))
|
|
194
|
+
if (!Number.isFinite(target) || !(target > 0))
|
|
195
|
+
return { action, repaired: false };
|
|
196
|
+
// Widen only: a valid TP at or beyond the floor is the model's choice.
|
|
197
|
+
if (tpValid && (isLong ? tp >= target : tp <= target))
|
|
186
198
|
return { action, repaired: false };
|
|
187
|
-
|
|
199
|
+
const reason = tpValid
|
|
200
|
+
? "below_reward_risk_floor"
|
|
201
|
+
: typeof tp === "number" && Number.isFinite(tp) && tp > 0
|
|
202
|
+
? "wrong_side"
|
|
203
|
+
: "missing";
|
|
204
|
+
return {
|
|
205
|
+
action: { ...action, takeProfitPrice: target },
|
|
206
|
+
repaired: true,
|
|
207
|
+
reason,
|
|
208
|
+
};
|
|
188
209
|
}
|
|
189
210
|
// One-line, human-readable summary of an executed action for the agent's journal
|
|
190
211
|
// (slice-3 memory). Compact so a few entries cost almost nothing in the prompt.
|
|
@@ -193,6 +214,48 @@ export function repairFuturesTakeProfit(action, quote, capitalMinimumRewardRisk)
|
|
|
193
214
|
function thesisTail(t) {
|
|
194
215
|
return t ? ` on thesis: ${t.summary.slice(0, 70)}` : "";
|
|
195
216
|
}
|
|
217
|
+
/** What the model tried, for its own rejection feedback (not a completed move). */
|
|
218
|
+
function describeAttempt(a) {
|
|
219
|
+
switch (a.type) {
|
|
220
|
+
case "futures_open":
|
|
221
|
+
return `futures_open ${a.side} ${a.symbol}`;
|
|
222
|
+
case "futures_close":
|
|
223
|
+
return `futures_close pos#${a.positionId}`;
|
|
224
|
+
case "futures_set_sltp":
|
|
225
|
+
return `futures_set_sltp pos#${a.positionId} (stopLossPrice ${a.stopLossPrice ?? "unchanged"}, takeProfitPrice ${a.takeProfitPrice ?? "unchanged"})`;
|
|
226
|
+
case "spot_order":
|
|
227
|
+
return `spot_order ${a.side} ${a.symbol}`;
|
|
228
|
+
case "spot_cancel":
|
|
229
|
+
return `spot_cancel order#${a.orderId}`;
|
|
230
|
+
case "pm_open":
|
|
231
|
+
return `pm_open ${a.ref ?? a.slug}`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export const LAST_REJECTIONS_MAX = 5;
|
|
235
|
+
const REJECTION_REASON_MAX = 200;
|
|
236
|
+
/**
|
|
237
|
+
* Pure: the rejected actions of one validated decision, newest decision only,
|
|
238
|
+
* for the next prompt. Undefined when nothing was rejected, so a fixed
|
|
239
|
+
* mistake drops out at once.
|
|
240
|
+
*/
|
|
241
|
+
export function lastRejectionsOf(planned, at) {
|
|
242
|
+
const rejected = planned.filter((p) => !p.accepted && p.code);
|
|
243
|
+
if (rejected.length === 0)
|
|
244
|
+
return undefined;
|
|
245
|
+
return rejected.slice(0, LAST_REJECTIONS_MAX).map((p) => ({
|
|
246
|
+
at,
|
|
247
|
+
action: describeAttempt(p.action),
|
|
248
|
+
code: p.code,
|
|
249
|
+
...(p.reason
|
|
250
|
+
? {
|
|
251
|
+
reason: p.reason
|
|
252
|
+
.replace(/\s+/g, " ")
|
|
253
|
+
.trim()
|
|
254
|
+
.slice(0, REJECTION_REASON_MAX),
|
|
255
|
+
}
|
|
256
|
+
: {}),
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
196
259
|
function summarizeAction(a) {
|
|
197
260
|
switch (a.type) {
|
|
198
261
|
case "futures_open":
|
|
@@ -607,25 +670,33 @@ async function runCycleCore(deps, capture) {
|
|
|
607
670
|
...observationReceipt,
|
|
608
671
|
};
|
|
609
672
|
const r = await executeAction(client, action, observation, trace, idem, provenance, { pmMinEntryProbabilityPct: spec.risk.pmMinEntryProbabilityPct });
|
|
673
|
+
const outcome = executionOutcome(action, r);
|
|
610
674
|
exitPlanned.push({
|
|
611
675
|
action,
|
|
612
676
|
accepted: true,
|
|
613
677
|
code: "thesis_invalidated",
|
|
614
678
|
reason: why,
|
|
615
|
-
executed:
|
|
679
|
+
executed: outcome.executed,
|
|
680
|
+
...exitOutcomeMetadata(outcome),
|
|
616
681
|
result: r.data,
|
|
617
682
|
});
|
|
618
|
-
if (
|
|
683
|
+
if (outcome.newWrite) {
|
|
619
684
|
state.intentSeq[intentKey] = seq + 1;
|
|
620
685
|
state.writesToday += 1;
|
|
686
|
+
}
|
|
687
|
+
if (outcome.executed) {
|
|
621
688
|
forgetThesis(state, thesisKey("futures", pos.id));
|
|
622
689
|
closedByThesis.push(pos.id);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
690
|
+
if (!outcome.replayed)
|
|
691
|
+
state.journal = [
|
|
692
|
+
...(state.journal ?? []),
|
|
693
|
+
{
|
|
694
|
+
at: observation.asOf,
|
|
695
|
+
did: `thesis exit: closed ${label} (${why})`,
|
|
696
|
+
},
|
|
697
|
+
].slice(-12);
|
|
627
698
|
}
|
|
628
|
-
log(`${
|
|
699
|
+
log(`${outcome.executed ? "executed" : outcome.pending ? "pending" : "FAILED"} thesis exit on ${label} (HTTP ${r.status}): ${why}`);
|
|
629
700
|
}
|
|
630
701
|
if (closedByThesis.length > 0) {
|
|
631
702
|
// The model must see the book as it IS now: drop the closed positions,
|
|
@@ -677,7 +748,8 @@ async function runCycleCore(deps, capture) {
|
|
|
677
748
|
tokensOut: 0,
|
|
678
749
|
estimatedCostUsd: 0,
|
|
679
750
|
writeAttempted: live ? exitPlanned.length : 0,
|
|
680
|
-
writeAccepted: exitPlanned.filter((p) => p.executed)
|
|
751
|
+
writeAccepted: exitPlanned.filter((p) => p.writeRecorded ?? p.executed)
|
|
752
|
+
.length,
|
|
681
753
|
...observationReceipt,
|
|
682
754
|
};
|
|
683
755
|
}
|
|
@@ -740,6 +812,7 @@ async function runCycleCore(deps, capture) {
|
|
|
740
812
|
const userOptions = {
|
|
741
813
|
venues: spec.venues,
|
|
742
814
|
dailyRiskBudget: buildDailyRiskBudget(spec, state),
|
|
815
|
+
...(state.lastRejections ? { lastRejections: state.lastRejections } : {}),
|
|
743
816
|
...(usesCapitalSizing(spec) ? { capitalSizing: spec.capitalSizing } : {}),
|
|
744
817
|
...(futuresCapacity ? { futuresCapacity } : {}),
|
|
745
818
|
...actionAvailability,
|
|
@@ -975,7 +1048,9 @@ async function runCycleCore(deps, capture) {
|
|
|
975
1048
|
// undefined (not the raw text) → db.ts stores NULL for raw_model_output.
|
|
976
1049
|
const rawModelOutput = undefined;
|
|
977
1050
|
if (decision.decision === "skip" || decision.actions.length === 0) {
|
|
978
|
-
|
|
1051
|
+
if (!exitPlanned.length ||
|
|
1052
|
+
!exitPlanned.every((p) => p.executionPending === true))
|
|
1053
|
+
state.consecutiveRejectCycles += 1;
|
|
979
1054
|
// Capture the abstention / forecast-only: the model evaluated a non-empty PM
|
|
980
1055
|
// universe and chose NOT to open, so the public evaluation must see it (else an
|
|
981
1056
|
// agent looks skilled by exposure choice alone). One post carries the whole
|
|
@@ -983,6 +1058,8 @@ async function runCycleCore(deps, capture) {
|
|
|
983
1058
|
const skipOpp = buildSkipOpportunity(decision, observation.pmMarkets, forecastEnabled);
|
|
984
1059
|
if (skipOpp)
|
|
985
1060
|
await opportunities.post(skipOpp);
|
|
1061
|
+
// A valid new decision saw the feedback and did not repeat it.
|
|
1062
|
+
state.lastRejections = undefined;
|
|
986
1063
|
saveState(stateFile, state);
|
|
987
1064
|
log(`model chose skip${decision.reason ? `: ${decision.reason}` : ""}`);
|
|
988
1065
|
return {
|
|
@@ -1006,6 +1083,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1006
1083
|
}
|
|
1007
1084
|
// VALIDATE (+ ACT when live). Quote evidence is fetched by the runner.
|
|
1008
1085
|
const planned = [];
|
|
1086
|
+
const protectionRefusals = [];
|
|
1009
1087
|
let riskIncreasesThisCycle = 0;
|
|
1010
1088
|
let openCount = observation.openPositions.length;
|
|
1011
1089
|
// RUNNING totals so multiple opens in one cycle accumulate correctly.
|
|
@@ -1030,6 +1108,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1030
1108
|
let anyAccepted = false;
|
|
1031
1109
|
let anyExecuted = false;
|
|
1032
1110
|
let anyExecFailed = false;
|
|
1111
|
+
let anyPending = false;
|
|
1033
1112
|
for (let action of decision.actions) {
|
|
1034
1113
|
let capitalSizing;
|
|
1035
1114
|
// Resolve a short PM ref (pm1…pmN) to the canonical {source,slug,outcome} BEFORE
|
|
@@ -1201,12 +1280,25 @@ async function runCycleCore(deps, capture) {
|
|
|
1201
1280
|
}
|
|
1202
1281
|
// Auto-clamp a missing/wrong-side futures take-profit to a valid R:R target
|
|
1203
1282
|
// off the stop, so the open isn't silently rejected server-side (the runner
|
|
1204
|
-
// owns trigger orientation; weak models routinely mis-sign it).
|
|
1283
|
+
// owns trigger orientation; weak models routinely mis-sign it). Under a
|
|
1284
|
+
// capital policy a correct-side TP inside the R:R floor is widened to it.
|
|
1285
|
+
let takeProfitRepair;
|
|
1205
1286
|
{
|
|
1206
1287
|
const fixed = repairFuturesTakeProfit(action, quote, capitalSizingEnabled ? spec.capitalSizing?.minRewardRisk : undefined);
|
|
1207
1288
|
if (fixed.repaired) {
|
|
1289
|
+
// Diagnostics keep the model's own request next to the applied target.
|
|
1290
|
+
const requested = action
|
|
1291
|
+
.takeProfitPrice;
|
|
1208
1292
|
action = fixed.action;
|
|
1209
|
-
|
|
1293
|
+
takeProfitRepair = {
|
|
1294
|
+
requestedTakeProfitPrice: typeof requested === "number" && Number.isFinite(requested)
|
|
1295
|
+
? requested
|
|
1296
|
+
: null,
|
|
1297
|
+
appliedTakeProfitPrice: action
|
|
1298
|
+
.takeProfitPrice,
|
|
1299
|
+
reason: fixed.reason ?? "missing",
|
|
1300
|
+
};
|
|
1301
|
+
log(`repaired ${action.type} take-profit ${requested ?? "missing"} -> ${action.takeProfitPrice} (R:R off stop; model TP was missing, wrong-side or inside the reward:risk floor)`);
|
|
1210
1302
|
}
|
|
1211
1303
|
}
|
|
1212
1304
|
const ctx = {
|
|
@@ -1240,6 +1332,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1240
1332
|
reason: v.reason,
|
|
1241
1333
|
quote,
|
|
1242
1334
|
...(capitalSizing ? { capitalSizing } : {}),
|
|
1335
|
+
...(takeProfitRepair ? { takeProfitRepair } : {}),
|
|
1243
1336
|
});
|
|
1244
1337
|
log(`reject ${action.type}: ${v.code} (${v.reason})`);
|
|
1245
1338
|
continue;
|
|
@@ -1255,6 +1348,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1255
1348
|
reason: "paper capital policy rejected quoted economics",
|
|
1256
1349
|
quote,
|
|
1257
1350
|
capitalSizing,
|
|
1351
|
+
...(takeProfitRepair ? { takeProfitRepair } : {}),
|
|
1258
1352
|
});
|
|
1259
1353
|
log(`reject ${action.type}: ${capitalRejection}`);
|
|
1260
1354
|
continue;
|
|
@@ -1284,6 +1378,7 @@ async function runCycleCore(deps, capture) {
|
|
|
1284
1378
|
quote,
|
|
1285
1379
|
executed: false,
|
|
1286
1380
|
...(capitalSizing ? { capitalSizing } : {}),
|
|
1381
|
+
...(takeProfitRepair ? { takeProfitRepair } : {}),
|
|
1287
1382
|
});
|
|
1288
1383
|
// Opt-in simulations reserve the same accepted capital as paper-live.
|
|
1289
1384
|
// No close/sell proceeds are credited until a future observed wallet read.
|
|
@@ -1317,18 +1412,27 @@ async function runCycleCore(deps, capture) {
|
|
|
1317
1412
|
...observationReceipt,
|
|
1318
1413
|
};
|
|
1319
1414
|
const r = await executeAction(client, action, observation, trace, idem, provenance, { pmMinEntryProbabilityPct: spec.risk.pmMinEntryProbabilityPct });
|
|
1415
|
+
const outcome = executionOutcome(action, r);
|
|
1320
1416
|
planned.push({
|
|
1321
1417
|
action,
|
|
1322
1418
|
accepted: true,
|
|
1323
1419
|
quote,
|
|
1324
|
-
executed:
|
|
1420
|
+
executed: outcome.executed,
|
|
1421
|
+
...exitOutcomeMetadata(outcome),
|
|
1325
1422
|
result: r.data,
|
|
1326
1423
|
...(capitalSizing ? { capitalSizing } : {}),
|
|
1424
|
+
...(takeProfitRepair ? { takeProfitRepair } : {}),
|
|
1327
1425
|
});
|
|
1328
|
-
if (
|
|
1329
|
-
anyExecuted = true;
|
|
1426
|
+
if (outcome.newWrite) {
|
|
1330
1427
|
state.intentSeq[intentKey] = seq + 1;
|
|
1331
1428
|
state.writesToday += 1;
|
|
1429
|
+
}
|
|
1430
|
+
if (outcome.pending)
|
|
1431
|
+
anyPending = true;
|
|
1432
|
+
if (outcome.executed) {
|
|
1433
|
+
anyExecuted = true;
|
|
1434
|
+
if (outcome.v2 && action.type === "futures_close")
|
|
1435
|
+
forgetThesis(state, thesisKey("futures", action.positionId));
|
|
1332
1436
|
if (isRiskIncreasingAction(action)) {
|
|
1333
1437
|
riskIncreasesThisCycle += 1;
|
|
1334
1438
|
state.riskIncreasesToday += 1;
|
|
@@ -1364,8 +1468,25 @@ async function runCycleCore(deps, capture) {
|
|
|
1364
1468
|
if (capitalSizingEnabled)
|
|
1365
1469
|
committedCapitalMusd += capitalCashCost(action, quote);
|
|
1366
1470
|
}
|
|
1367
|
-
else {
|
|
1471
|
+
else if (!outcome.pending) {
|
|
1368
1472
|
anyExecFailed = true;
|
|
1473
|
+
// The mark can move after local validation. This explicit API rejection
|
|
1474
|
+
// confirms that the protection update was refused. A timeout/5xx cannot
|
|
1475
|
+
// establish that nothing happened and must never enter this feedback.
|
|
1476
|
+
const response = asObj(r.data);
|
|
1477
|
+
if (action.type === "futures_set_sltp" &&
|
|
1478
|
+
r.status === 422 &&
|
|
1479
|
+
response.error === "sl_tp_invalid") {
|
|
1480
|
+
const codes = Array.isArray(response.blockReasons)
|
|
1481
|
+
? response.blockReasons.filter((code) => typeof code === "string" && /^[a-z][a-z0-9_]{0,79}$/.test(code))
|
|
1482
|
+
: [];
|
|
1483
|
+
protectionRefusals.push({
|
|
1484
|
+
action,
|
|
1485
|
+
accepted: false,
|
|
1486
|
+
code: "sl_tp_invalid",
|
|
1487
|
+
reason: codes.slice(0, 5).join(", ") || "Recheck the current mark.",
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1369
1490
|
// A transport/server failure does not prove an entry stayed unfilled.
|
|
1370
1491
|
// Reserve its possible spend for subsequent actions this cycle, without
|
|
1371
1492
|
// recording a confirmed trade or advancing its idempotency sequence.
|
|
@@ -1402,23 +1523,30 @@ async function runCycleCore(deps, capture) {
|
|
|
1402
1523
|
});
|
|
1403
1524
|
}
|
|
1404
1525
|
}
|
|
1405
|
-
log(`${
|
|
1526
|
+
log(`${outcome.executed ? "executed" : outcome.pending ? "pending" : "FAILED"} ${action.type} (HTTP ${r.status})`);
|
|
1406
1527
|
}
|
|
1407
1528
|
// Reset the reject kill-switch only on real PROGRESS: an accepted-but-FAILED
|
|
1408
1529
|
// live write is not progress, or a persistently failing live agent would
|
|
1409
1530
|
// never trip the kill-switch.
|
|
1410
1531
|
const progressed = live ? anyExecuted : anyAccepted;
|
|
1532
|
+
state.lastRejections = lastRejectionsOf([...planned, ...protectionRefusals], observation.asOf);
|
|
1411
1533
|
state.consecutiveRejectCycles = progressed
|
|
1412
1534
|
? 0
|
|
1413
|
-
:
|
|
1535
|
+
: live && anyPending && planned.every((p) => p.executionPending === true)
|
|
1536
|
+
? state.consecutiveRejectCycles
|
|
1537
|
+
: state.consecutiveRejectCycles + 1;
|
|
1414
1538
|
state.consecutiveExecFailures =
|
|
1415
|
-
anyExecFailed && !anyExecuted
|
|
1539
|
+
anyExecFailed && !anyExecuted
|
|
1540
|
+
? state.consecutiveExecFailures + 1
|
|
1541
|
+
: !anyExecuted && anyPending
|
|
1542
|
+
? state.consecutiveExecFailures
|
|
1543
|
+
: 0;
|
|
1416
1544
|
state.rateLimitHits = client.rateLimitHits ?? state.rateLimitHits;
|
|
1417
1545
|
// Memory describes completed moves, not policy acceptance. A backend rejection,
|
|
1418
1546
|
// lost response, or dry-run plan must not become "opened" / "trailed stop" in
|
|
1419
1547
|
// the next model prompt. Attempts remain in planned[] and the audit ledger.
|
|
1420
1548
|
const moves = planned
|
|
1421
|
-
.filter((p) => p.accepted && p.executed === true)
|
|
1549
|
+
.filter((p) => p.accepted && p.executed === true && p.executionReplayed !== true)
|
|
1422
1550
|
.map((p) => summarizeAction(p.action));
|
|
1423
1551
|
if (moves.length > 0) {
|
|
1424
1552
|
// A cycle-wide rationale may describe a failed/rejected sibling action.
|
package/dist/agent/skill.js
CHANGED
|
@@ -75,6 +75,15 @@ function buildCapitalSizing(raw) {
|
|
|
75
75
|
totalCapitalPct: num(policy.totalCapitalPct, Number.NaN),
|
|
76
76
|
cashReservePct: num(policy.cashReservePct, Number.NaN),
|
|
77
77
|
minRewardRisk: num(policy.minRewardRisk, Number.NaN),
|
|
78
|
+
...(policy.highConviction !== undefined
|
|
79
|
+
? {
|
|
80
|
+
highConviction: {
|
|
81
|
+
minConfidence: num(obj(policy.highConviction).minConfidence, Number.NaN),
|
|
82
|
+
perTicketCapitalPct: num(obj(policy.highConviction).perTicketCapitalPct, Number.NaN),
|
|
83
|
+
futuresRiskPct: num(obj(policy.highConviction).futuresRiskPct, Number.NaN),
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
: {}),
|
|
78
87
|
};
|
|
79
88
|
}
|
|
80
89
|
// Coerce raw frontmatter into a best-effort AgentSpec. This NEVER throws on bad
|
|
@@ -28,6 +28,7 @@ export function validateCapitalSizingPolicy(value) {
|
|
|
28
28
|
...percentKeys,
|
|
29
29
|
"cashReservePct",
|
|
30
30
|
"minRewardRisk",
|
|
31
|
+
"highConviction",
|
|
31
32
|
]);
|
|
32
33
|
for (const key of Object.keys(p)) {
|
|
33
34
|
if (!allowedKeys.has(key))
|
|
@@ -57,6 +58,28 @@ export function validateCapitalSizingPolicy(value) {
|
|
|
57
58
|
p.totalCapitalPct + p.cashReservePct > 100) {
|
|
58
59
|
add("skill_capital_sizing_total_reserve", "capitalSizing.totalCapitalPct + cashReservePct cannot exceed 100");
|
|
59
60
|
}
|
|
61
|
+
if (p.highConviction !== undefined) {
|
|
62
|
+
const hc = p.highConviction;
|
|
63
|
+
if (!isObj(hc)) {
|
|
64
|
+
add("skill_capital_sizing_high_conviction", "capitalSizing.highConviction must be an object");
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
for (const key of Object.keys(hc)) {
|
|
68
|
+
if (!["minConfidence", "perTicketCapitalPct", "futuresRiskPct"].includes(key))
|
|
69
|
+
add("skill_capital_sizing_unknown_key", `unknown capitalSizing.highConviction key "${key}"`);
|
|
70
|
+
}
|
|
71
|
+
if (!isPosNum(hc.minConfidence) || hc.minConfidence > 1)
|
|
72
|
+
add("skill_capital_sizing_high_conviction", "capitalSizing.highConviction.minConfidence must be a finite number in (0, 1]");
|
|
73
|
+
for (const key of ["perTicketCapitalPct", "futuresRiskPct"]) {
|
|
74
|
+
if (!isPosNum(hc[key]) || hc[key] > 100)
|
|
75
|
+
add("skill_capital_sizing_percent", `capitalSizing.highConviction.${key} must be a finite number in (0, 100]`);
|
|
76
|
+
}
|
|
77
|
+
if (isPosNum(hc.perTicketCapitalPct) &&
|
|
78
|
+
isPosNum(p.totalCapitalPct) &&
|
|
79
|
+
hc.perTicketCapitalPct > p.totalCapitalPct)
|
|
80
|
+
add("skill_capital_sizing_ticket_cap", "capitalSizing.highConviction.perTicketCapitalPct cannot exceed totalCapitalPct");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
60
83
|
return issues;
|
|
61
84
|
}
|
|
62
85
|
export function validateSkill(parsed, mode = "self-host") {
|
package/dist/agent/strictLint.js
CHANGED
package/dist/agent/templates.js
CHANGED
|
@@ -50,37 +50,37 @@ const PRESETS = {
|
|
|
50
50
|
modelFail: 5,
|
|
51
51
|
},
|
|
52
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 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.
|
|
69
|
-
4. Quote before you open. Read the liquidation price and confirm it is sane. If
|
|
70
|
-
the quote is not eligible, relay the reason and stop.
|
|
71
|
-
5. Open small and protected: enter in the trend direction and set a stop-loss at
|
|
72
|
-
open. Place the take-profit a touch wider than the stop.
|
|
73
|
-
6. Stay in sync: poll your trades for any stop / take-profit / liquidation that
|
|
74
|
-
fired while you were not looking, and react to what actually happened.
|
|
75
|
-
|
|
76
|
-
The hard caps (leverage, margin, watchlist) live in the config blocks above and
|
|
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 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.
|
|
69
|
+
4. Quote before you open. Read the liquidation price and confirm it is sane. If
|
|
70
|
+
the quote is not eligible, relay the reason and stop.
|
|
71
|
+
5. Open small and protected: enter in the trend direction and set a stop-loss at
|
|
72
|
+
open. Place the take-profit a touch wider than the stop.
|
|
73
|
+
6. Stay in sync: poll your trades for any stop / take-profit / liquidation that
|
|
74
|
+
fired while you were not looking, and react to what actually happened.
|
|
75
|
+
|
|
76
|
+
The hard caps (leverage, margin, watchlist) live in the config blocks above and
|
|
77
77
|
are enforced by the runner — change them there, not in this prose.`;
|
|
78
|
-
const PERSONA_STUB = `# Persona
|
|
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.
|
|
78
|
+
const PERSONA_STUB = `# Persona
|
|
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.
|
|
84
84
|
`;
|
|
85
85
|
export function buildAgentObject(name, preset) {
|
|
86
86
|
const p = PRESETS[preset];
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -58,6 +58,15 @@ export interface CapitalSizingPolicy {
|
|
|
58
58
|
totalCapitalPct: number;
|
|
59
59
|
cashReservePct: number;
|
|
60
60
|
minRewardRisk: number;
|
|
61
|
+
/** Opt-in (owner 2026-10-08): a futures_open the model rates at or above
|
|
62
|
+
* minConfidence uses these larger ticket and stop-risk limits instead of
|
|
63
|
+
* the base ones. Every other cap (total, reserve, open margin, R:R) holds. */
|
|
64
|
+
highConviction?: CapitalHighConviction;
|
|
65
|
+
}
|
|
66
|
+
export interface CapitalHighConviction {
|
|
67
|
+
minConfidence: number;
|
|
68
|
+
perTicketCapitalPct: number;
|
|
69
|
+
futuresRiskPct: number;
|
|
61
70
|
}
|
|
62
71
|
export type CapitalBook = {
|
|
63
72
|
status: "unavailable";
|
|
@@ -687,6 +696,8 @@ export interface FuturesFundingQuoteEvidence {
|
|
|
687
696
|
annualizedRate?: number;
|
|
688
697
|
}
|
|
689
698
|
export interface RunState {
|
|
699
|
+
/** Exact checkpointed v2 request; validated before replay, never model-controlled. */
|
|
700
|
+
pmPaperV2Pending?: unknown;
|
|
690
701
|
runId: string;
|
|
691
702
|
cyclesRun: number;
|
|
692
703
|
writesToday: number;
|
|
@@ -718,6 +729,13 @@ export interface RunState {
|
|
|
718
729
|
did: string;
|
|
719
730
|
}>;
|
|
720
731
|
theses?: Record<string, PositionThesis>;
|
|
732
|
+
lastRejections?: LastRejection[];
|
|
733
|
+
}
|
|
734
|
+
export interface LastRejection {
|
|
735
|
+
at: string;
|
|
736
|
+
action: string;
|
|
737
|
+
code: string;
|
|
738
|
+
reason?: string;
|
|
721
739
|
}
|
|
722
740
|
export interface AgentTrace {
|
|
723
741
|
runId?: string;
|
|
@@ -743,8 +761,21 @@ export interface PlannedAction {
|
|
|
743
761
|
reason?: string;
|
|
744
762
|
quote?: QuoteEvidence;
|
|
745
763
|
executed?: boolean;
|
|
764
|
+
/** V2 close accepted but exposure remains open; accounting is separate. */
|
|
765
|
+
executionPending?: boolean;
|
|
766
|
+
executionReplayed?: boolean;
|
|
767
|
+
/** A newly accepted v2 intent/fill consumes write budget; replay does not. */
|
|
768
|
+
writeRecorded?: boolean;
|
|
746
769
|
result?: unknown;
|
|
747
770
|
capitalSizing?: CapitalSizingAdjustment;
|
|
771
|
+
/** Set when the runner replaced the model's futures take-profit; persisted
|
|
772
|
+
* with the cycle so the requested target is never lost. */
|
|
773
|
+
takeProfitRepair?: TakeProfitRepair;
|
|
774
|
+
}
|
|
775
|
+
export interface TakeProfitRepair {
|
|
776
|
+
requestedTakeProfitPrice: number | null;
|
|
777
|
+
appliedTakeProfitPrice: number;
|
|
778
|
+
reason: "missing" | "wrong_side" | "below_reward_risk_floor";
|
|
748
779
|
}
|
|
749
780
|
export interface CycleResult {
|
|
750
781
|
/** Private non-enumerable evidence. Never forward through AgentTrace/public APIs. */
|
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.9.0";
|
|
9
9
|
readonly mcpPackage: "@coinrithm/mcp-trading";
|
|
10
10
|
readonly mcpVersion: string;
|
|
11
11
|
};
|
package/dist/agent/version.js
CHANGED
|
@@ -20,7 +20,7 @@ export const COINRITHM_API = {
|
|
|
20
20
|
mcpUrl: "https://mcp.coinrithm.com/mcp",
|
|
21
21
|
// The API CONTRACT version (openapi.yaml info.version). Versioned independently
|
|
22
22
|
// from the npm package below — hand-bump this when the OpenAPI contract changes.
|
|
23
|
-
openapiVersion: "1.
|
|
23
|
+
openapiVersion: "1.9.0",
|
|
24
24
|
mcpPackage: "@coinrithm/mcp-trading",
|
|
25
25
|
mcpVersion: PACKAGE_VERSION,
|
|
26
26
|
};
|