@coinrithm/mcp-trading 0.7.2 → 0.7.4
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 +171 -113
- package/README.md +277 -238
- package/dist/agent/act.d.ts +2 -2
- package/dist/agent/act.js +24 -3
- package/dist/agent/cli.js +68 -23
- package/dist/agent/client.d.ts +33 -0
- package/dist/agent/client.js +34 -7
- package/dist/agent/decision.d.ts +3 -0
- package/dist/agent/decision.js +26 -3
- package/dist/agent/decisionValidator.js +2 -1
- package/dist/agent/deploymentOverlay.js +25 -5
- package/dist/agent/engine.d.ts +2 -1
- package/dist/agent/engine.js +4 -1
- package/dist/agent/extract.js +3 -1
- package/dist/agent/gate.js +25 -5
- package/dist/agent/index.js +0 -1
- package/dist/agent/indicators.js +4 -2
- package/dist/agent/manifest.js +1 -1
- package/dist/agent/mechanical.d.ts +36 -0
- package/dist/agent/mechanical.js +286 -0
- package/dist/agent/observe.d.ts +4 -0
- package/dist/agent/observe.js +140 -53
- package/dist/agent/prompt.d.ts +3 -1
- package/dist/agent/prompt.js +17 -6
- package/dist/agent/providers.js +39 -4
- package/dist/agent/resolve.js +23 -6
- package/dist/agent/resolvePm.js +14 -3
- package/dist/agent/runEvidence.js +6 -2
- package/dist/agent/runner.d.ts +8 -2
- package/dist/agent/runner.js +363 -59
- package/dist/agent/scorecard.js +12 -4
- package/dist/agent/setups.js +57 -9
- package/dist/agent/skill.js +1 -1
- package/dist/agent/state.js +9 -4
- package/dist/agent/types.d.ts +17 -2
- package/dist/agent/types.js +2 -1
- package/dist/agent/util.js +11 -4
- package/dist/agent/version.d.ts +1 -1
- package/dist/agent/version.js +1 -1
- package/dist/client.d.ts +51 -0
- package/dist/client.js +30 -3
- package/dist/executionPolicy.d.ts +2 -0
- package/dist/executionPolicy.js +21 -0
- package/dist/http.js +13 -3
- package/dist/tools.d.ts +23 -0
- package/dist/tools.js +796 -39
- package/package.json +86 -78
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Mechanical BENCHMARK baseline agents — the living reference line the Arena
|
|
2
|
+
// measures skill against (sol-audit #7 baselines).
|
|
3
|
+
//
|
|
4
|
+
// These are NOT LLM agents. There is no model, no prompt, and no inference cost.
|
|
5
|
+
// Each cycle the runner short-circuits the provider and computes a decision
|
|
6
|
+
// deterministically from the observation, so a benchmark's forecast is fully
|
|
7
|
+
// reproducible from (market, date) alone. Three strategies, ALL mechanical:
|
|
8
|
+
//
|
|
9
|
+
// • market-implied — submits a forecast EXACTLY equal to the market's own
|
|
10
|
+
// probability at decision time. This is the definition of the baseline the
|
|
11
|
+
// forecast-skill scorecard measures against, and the ONLY agent for which an
|
|
12
|
+
// echo of the market price is correct BY DESIGN. Its description says
|
|
13
|
+
// BENCHMARK so the runner's anti-echo log is never read as a defect.
|
|
14
|
+
// • base-rate — submits an uninformative 50 for every market. We do NOT
|
|
15
|
+
// invent per-category historical base rates: the observation carries no
|
|
16
|
+
// calibrated category prior, so the honest baseline is the uninformative
|
|
17
|
+
// prior. If a cheap calibrated base rate is ever surfaced in the
|
|
18
|
+
// observation, swap it in here (documented, never fabricated).
|
|
19
|
+
// • random — submits a deterministic pseudo-random forecast in [20,80]
|
|
20
|
+
// seeded from (market key, UTC date), so a run is reproducible and a "null"
|
|
21
|
+
// forecaster's noise floor is a fair, stable comparison.
|
|
22
|
+
//
|
|
23
|
+
// The market PICK rule is identical across all three (highest-volume eligible
|
|
24
|
+
// market that carries a usable probability and is not already held), so the
|
|
25
|
+
// three benchmarks bet the SAME markets and differ ONLY in the forecast — which
|
|
26
|
+
// is exactly what a clean baseline comparison needs.
|
|
27
|
+
import { SPEC_VERSION, } from "./types.js";
|
|
28
|
+
import { dayKey } from "./util.js";
|
|
29
|
+
// ───────────────────────── Strategy vocabulary ──────────────────────────────
|
|
30
|
+
export const BENCHMARK_STRATEGIES = [
|
|
31
|
+
"market-implied",
|
|
32
|
+
"base-rate",
|
|
33
|
+
"random",
|
|
34
|
+
];
|
|
35
|
+
export function isBenchmarkStrategy(s) {
|
|
36
|
+
return BENCHMARK_STRATEGIES.includes(s);
|
|
37
|
+
}
|
|
38
|
+
// The uninformative prior the base-rate benchmark submits. Documented, NOT an
|
|
39
|
+
// invented per-category historical rate: the observation carries no calibrated
|
|
40
|
+
// category prior to read, so 50 (maximum entropy for a binary) is the honest,
|
|
41
|
+
// non-fabricated baseline. See the module header.
|
|
42
|
+
export const BASE_RATE_UNINFORMATIVE = 50;
|
|
43
|
+
// Tiny fixed stake (mUSD). Equal to the server's PM minimum so a benchmark bets
|
|
44
|
+
// the smallest honest ticket — it exists to record forecasts, not to size risk.
|
|
45
|
+
export const BENCHMARK_STAKE_MUSD = 10;
|
|
46
|
+
// The random/null benchmark's forecast range (inclusive), kept away from the
|
|
47
|
+
// [1,99] extremes so a "no-information" forecaster never masquerades as confident.
|
|
48
|
+
export const RANDOM_FORECAST_MIN = 20;
|
|
49
|
+
export const RANDOM_FORECAST_MAX = 80;
|
|
50
|
+
// ───────────────────────── Deterministic helpers ────────────────────────────
|
|
51
|
+
// A market's stable identity: the canonical triple, lower-cased. Doubles as the
|
|
52
|
+
// dedupe key against held positions and the seed component for the random RNG.
|
|
53
|
+
export function marketKey(m) {
|
|
54
|
+
return `${m.source.toLowerCase()}|${m.slug.toLowerCase()}|${m.outcomeExternalMarketId}`;
|
|
55
|
+
}
|
|
56
|
+
// FNV-1a 32-bit string hash — small, dependency-free, and deterministic across
|
|
57
|
+
// platforms. Used only to derive the reproducible random-benchmark forecast.
|
|
58
|
+
function fnv1a32(s) {
|
|
59
|
+
let h = 0x811c9dc5;
|
|
60
|
+
for (let i = 0; i < s.length; i++) {
|
|
61
|
+
h ^= s.charCodeAt(i);
|
|
62
|
+
// h *= 16777619, kept in 32-bit unsigned space via Math.imul.
|
|
63
|
+
h = Math.imul(h, 0x01000193);
|
|
64
|
+
}
|
|
65
|
+
return h >>> 0;
|
|
66
|
+
}
|
|
67
|
+
// Deterministic pseudo-random forecast in [RANDOM_FORECAST_MIN,
|
|
68
|
+
// RANDOM_FORECAST_MAX], one-decimal, seeded from (marketKey, dateKey). Same seed
|
|
69
|
+
// ⇒ same value, so a re-run of the same cycle reproduces exactly.
|
|
70
|
+
export function seededRandomForecast(seed) {
|
|
71
|
+
const span = RANDOM_FORECAST_MAX - RANDOM_FORECAST_MIN;
|
|
72
|
+
// 0..1 from the hash, then map into the span at one-decimal precision.
|
|
73
|
+
const unit = fnv1a32(seed) / 0xffffffff;
|
|
74
|
+
const raw = RANDOM_FORECAST_MIN + unit * span;
|
|
75
|
+
return Math.round(raw * 10) / 10;
|
|
76
|
+
}
|
|
77
|
+
// Clamp any probability-percentage to the backend's exclusive (0,100) rail as a
|
|
78
|
+
// one-decimal value in [1,99] — the same rail the runner enforces on model
|
|
79
|
+
// forecasts. Non-finite input returns undefined.
|
|
80
|
+
function clampForecast(pct) {
|
|
81
|
+
if (!Number.isFinite(pct))
|
|
82
|
+
return undefined;
|
|
83
|
+
const clamped = Math.min(99, Math.max(1, pct));
|
|
84
|
+
return Math.round(clamped * 10) / 10;
|
|
85
|
+
}
|
|
86
|
+
// A market is a usable benchmark candidate iff it carries a real probability
|
|
87
|
+
// (needed for the market-implied echo) AND a canonical triple. Keeping the SAME
|
|
88
|
+
// gate for all three strategies is what makes them bet identical markets.
|
|
89
|
+
function hasUsableProbability(m) {
|
|
90
|
+
return (typeof m.probability === "number" &&
|
|
91
|
+
Number.isFinite(m.probability) &&
|
|
92
|
+
!!m.source &&
|
|
93
|
+
!!m.slug &&
|
|
94
|
+
!!m.outcomeExternalMarketId);
|
|
95
|
+
}
|
|
96
|
+
// Deterministic pick: among eligible (usable-probability), not-already-held
|
|
97
|
+
// candidates, the highest-volume market wins; ties break on the market key
|
|
98
|
+
// ascending so the choice is fully reproducible. `volumeUsd` absent ⇒ treated as
|
|
99
|
+
// 0, so an older backend that omits volume falls back to a pure key-ordered pick
|
|
100
|
+
// (still deterministic). observe() already excludes held markets and eligibility
|
|
101
|
+
// -false outcomes, so this is a belt-and-suspenders re-filter.
|
|
102
|
+
export function pickBenchmarkMarket(markets, heldKeys = new Set()) {
|
|
103
|
+
const candidates = markets
|
|
104
|
+
.filter(hasUsableProbability)
|
|
105
|
+
.filter((m) => !heldKeys.has(marketKey(m)));
|
|
106
|
+
if (candidates.length === 0)
|
|
107
|
+
return undefined;
|
|
108
|
+
return candidates.reduce((best, m) => {
|
|
109
|
+
const bv = best.volumeUsd ?? 0;
|
|
110
|
+
const mv = m.volumeUsd ?? 0;
|
|
111
|
+
if (mv !== bv)
|
|
112
|
+
return mv > bv ? m : best;
|
|
113
|
+
return marketKey(m) < marketKey(best) ? m : best;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// The per-strategy forecast (1..99) for a chosen market on a given UTC date.
|
|
117
|
+
// Returns undefined only if a market-implied echo can't be sized (non-finite
|
|
118
|
+
// probability) — the caller then skips rather than fabricating a value.
|
|
119
|
+
export function benchmarkForecast(strategy, market, dateKey) {
|
|
120
|
+
switch (strategy) {
|
|
121
|
+
case "market-implied":
|
|
122
|
+
// Echo the market's own probability (0..1 ⇒ percentage). BY DESIGN — this
|
|
123
|
+
// agent IS the market-implied baseline definition.
|
|
124
|
+
return clampForecast(Math.round((market.probability ?? NaN) * 100));
|
|
125
|
+
case "base-rate":
|
|
126
|
+
return BASE_RATE_UNINFORMATIVE;
|
|
127
|
+
case "random":
|
|
128
|
+
return clampForecast(seededRandomForecast(`${marketKey(market)}|${dateKey}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const heldKeysOf = (positions) => new Set(positions
|
|
132
|
+
.filter((p) => (p.status ?? "open") === "open")
|
|
133
|
+
.map((p) => marketKey({
|
|
134
|
+
source: p.source ?? "",
|
|
135
|
+
slug: p.slug ?? "",
|
|
136
|
+
outcomeExternalMarketId: p.outcomeExternalMarketId ?? "",
|
|
137
|
+
})));
|
|
138
|
+
// Compute one benchmark cycle's decision directly from the observation — no
|
|
139
|
+
// model call. Skips (never throws) when the strategy is unknown or no eligible
|
|
140
|
+
// market is available; otherwise emits a single pm_open carrying the strategy's
|
|
141
|
+
// forecast. Confidence is fixed at 1 (a benchmark never abstains on confidence).
|
|
142
|
+
export function decideMechanical(input) {
|
|
143
|
+
const log = [];
|
|
144
|
+
const strategy = input.strategy;
|
|
145
|
+
if (!isBenchmarkStrategy(strategy)) {
|
|
146
|
+
log.push(`mechanical: unknown benchmark strategy "${strategy}" — skipping`);
|
|
147
|
+
return {
|
|
148
|
+
decision: { decision: "skip", reason: "unknown_strategy", actions: [] },
|
|
149
|
+
log,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const dateKey = input.dateKey ?? dayKey();
|
|
153
|
+
const stakeMusd = input.stakeMusd ?? BENCHMARK_STAKE_MUSD;
|
|
154
|
+
const held = heldKeysOf(input.observation.pmPositions);
|
|
155
|
+
const market = pickBenchmarkMarket(input.observation.pmMarkets, held);
|
|
156
|
+
if (!market) {
|
|
157
|
+
log.push(`mechanical(${strategy}): no eligible PM market to benchmark this cycle — skipping`);
|
|
158
|
+
return {
|
|
159
|
+
decision: { decision: "skip", reason: "no_eligible_market", actions: [] },
|
|
160
|
+
log,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const forecast = benchmarkForecast(strategy, market, dateKey);
|
|
164
|
+
if (forecast == null) {
|
|
165
|
+
log.push(`mechanical(${strategy}): could not size a forecast for ${market.slug} — skipping`);
|
|
166
|
+
return {
|
|
167
|
+
decision: { decision: "skip", reason: "unsizable_forecast", actions: [] },
|
|
168
|
+
log,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const marketPct = typeof market.probability === "number"
|
|
172
|
+
? Math.round(market.probability * 100)
|
|
173
|
+
: undefined;
|
|
174
|
+
const rationale = strategy === "market-implied"
|
|
175
|
+
? `BENCHMARK market-implied: forecast ${forecast}% = market probability${marketPct != null ? ` ${marketPct}%` : ""} on ${market.source}/${market.slug} (echo is the baseline definition, not a defect).`
|
|
176
|
+
: strategy === "base-rate"
|
|
177
|
+
? `BENCHMARK base-rate: uninformative ${forecast}% prior on ${market.source}/${market.slug} (no invented per-category base rate).`
|
|
178
|
+
: `BENCHMARK random: seeded pseudo-random ${forecast}% on ${market.source}/${market.slug} (reproducible from market+date).`;
|
|
179
|
+
const action = {
|
|
180
|
+
type: "pm_open",
|
|
181
|
+
ref: market.ref,
|
|
182
|
+
source: market.source,
|
|
183
|
+
slug: market.slug,
|
|
184
|
+
outcomeExternalMarketId: market.outcomeExternalMarketId,
|
|
185
|
+
stakeMusd,
|
|
186
|
+
confidence: 1,
|
|
187
|
+
forecastProbability: forecast,
|
|
188
|
+
rationaleSummary: rationale,
|
|
189
|
+
};
|
|
190
|
+
log.push(`mechanical(${strategy}): bet ${stakeMusd}mUSD on ${market.source}/${market.slug} @ forecast ${forecast}% (market ${marketPct ?? "?"}%, vol ${market.volumeUsd ?? 0})`);
|
|
191
|
+
return {
|
|
192
|
+
decision: {
|
|
193
|
+
decision: "act",
|
|
194
|
+
confidence: 1,
|
|
195
|
+
rationale,
|
|
196
|
+
actions: [action],
|
|
197
|
+
},
|
|
198
|
+
log,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// A complete, valid AgentSpec for a mechanical benchmark. The strategy travels
|
|
202
|
+
// in model.name (provider "mechanical"); the runner reads it there. Caps are set
|
|
203
|
+
// so a benchmark NEVER self-disables (all kill-switches off — it is a permanent
|
|
204
|
+
// reference line) and never abstains on confidence (minConfidence 0). PM-only,
|
|
205
|
+
// tiny fixed stake, always-fire trigger policy (mechanical is free, so there is
|
|
206
|
+
// no reason to gate a cycle).
|
|
207
|
+
function benchmarkSpec(strategy, cadenceSeconds) {
|
|
208
|
+
const label = LABELS[strategy];
|
|
209
|
+
return {
|
|
210
|
+
name: `bench-${strategy}`,
|
|
211
|
+
description: `BENCHMARK (${label}) — a mechanical, non-LLM baseline reference agent. ${DESCRIPTIONS[strategy]} It is NOT a skill agent; it exists so the Arena can show real agents beating (or not) a fixed, deterministic baseline. Paper-only, zero inference cost.`,
|
|
212
|
+
spec: SPEC_VERSION,
|
|
213
|
+
trigger: { cadence: `${Math.round(cadenceSeconds / 60)}m` },
|
|
214
|
+
model: { provider: "mechanical", name: strategy },
|
|
215
|
+
venues: ["pm"],
|
|
216
|
+
risk: {
|
|
217
|
+
maxLeverage: 1,
|
|
218
|
+
perTradeMarginMusd: BENCHMARK_STAKE_MUSD, // per-trade stake cap = the tiny fixed stake
|
|
219
|
+
maxConcurrentPositions: 1000, // PM opens aren't capped by this; kept generous
|
|
220
|
+
requireStopLoss: false,
|
|
221
|
+
// Discovery needs a query coin; PM-only agents still seed discover from the
|
|
222
|
+
// watchlist. Majors give the broadest, most-liquid crypto board to benchmark.
|
|
223
|
+
watchlist: ["BTC", "ETH", "SOL"],
|
|
224
|
+
},
|
|
225
|
+
limits: {
|
|
226
|
+
maxTradesPerDay: 0, // 0 = unlimited: a benchmark records as many markets as it sees
|
|
227
|
+
maxWritesPerCycle: 1, // one benchmarked market per cycle
|
|
228
|
+
maxDailyLossMusd: 0, // disabled — a reference line never risk-stops
|
|
229
|
+
maxOpenMarginMusd: 100000,
|
|
230
|
+
},
|
|
231
|
+
abstention: {
|
|
232
|
+
onStaleData: false,
|
|
233
|
+
onWeakSignal: false,
|
|
234
|
+
onMissingQuote: false,
|
|
235
|
+
onInsufficientBalance: false,
|
|
236
|
+
minConfidence: 0, // a benchmark never abstains on confidence
|
|
237
|
+
},
|
|
238
|
+
sync: { requirePollBeforeWrite: false },
|
|
239
|
+
killSwitch: {
|
|
240
|
+
maxDrawdownMusd: 0,
|
|
241
|
+
maxConsecutiveRejects: 0,
|
|
242
|
+
maxConsecutiveModelFailures: 0,
|
|
243
|
+
onRateLimitPressure: false,
|
|
244
|
+
},
|
|
245
|
+
objective: {
|
|
246
|
+
primary: "calibration",
|
|
247
|
+
secondary: ["benchmark", strategy],
|
|
248
|
+
horizon: "all",
|
|
249
|
+
},
|
|
250
|
+
capabilities: [],
|
|
251
|
+
triggerPolicy: {
|
|
252
|
+
mode: "always", // mechanical = free; always evaluate, never gate a cycle
|
|
253
|
+
skipLlmWhenNoTrigger: false,
|
|
254
|
+
alwaysManageOpenPositions: true,
|
|
255
|
+
maxLlmCallsPerHour: 0,
|
|
256
|
+
debounceMinutes: 0,
|
|
257
|
+
pmEvalCooldownMinutes: 0,
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const LABELS = {
|
|
262
|
+
"market-implied": "market-implied",
|
|
263
|
+
"base-rate": "base-rate",
|
|
264
|
+
random: "random/null",
|
|
265
|
+
};
|
|
266
|
+
const DESCRIPTIONS = {
|
|
267
|
+
"market-implied": "Each cycle it picks the highest-volume eligible market and submits a forecast EXACTLY equal to the market's own probability — the market-implied baseline every skill claim is measured against.",
|
|
268
|
+
"base-rate": "It submits an uninformative 50% prior on every market (no fabricated per-category base rate).",
|
|
269
|
+
random: "It submits a deterministic pseudo-random forecast in [20,80] seeded from the market and date, giving a reproducible no-information noise floor.",
|
|
270
|
+
};
|
|
271
|
+
// Human-readable prose stored on the row. NEVER fed to a model (mechanical agents
|
|
272
|
+
// don't reason) — it exists so the Arena/terminal can describe the agent honestly.
|
|
273
|
+
function benchmarkProse(strategy) {
|
|
274
|
+
return `# Benchmark: ${LABELS[strategy]}\n\nThis is a MECHANICAL BENCHMARK baseline, not a skill agent. ${DESCRIPTIONS[strategy]}\n\nIt calls no language model, has zero inference cost, and is fully deterministic and reproducible. It exists purely as a public reference line: the Arena compares real agents' calibration against these baselines. Paper-only.`;
|
|
275
|
+
}
|
|
276
|
+
// Default cadence: hourly. Frequent enough to accumulate a steady benchmark
|
|
277
|
+
// record, slow enough that the three benchmarks don't churn the discovered board.
|
|
278
|
+
const DEFAULT_BENCHMARK_CADENCE_SECONDS = 3600;
|
|
279
|
+
export const BENCHMARK_AGENTS = BENCHMARK_STRATEGIES.map((strategy) => ({
|
|
280
|
+
handle: `bench-${strategy}`,
|
|
281
|
+
displayName: `Benchmark: ${LABELS[strategy]}`,
|
|
282
|
+
strategy,
|
|
283
|
+
cadenceSeconds: DEFAULT_BENCHMARK_CADENCE_SECONDS,
|
|
284
|
+
spec: benchmarkSpec(strategy, DEFAULT_BENCHMARK_CADENCE_SECONDS),
|
|
285
|
+
prose: benchmarkProse(strategy),
|
|
286
|
+
}));
|
package/dist/agent/observe.d.ts
CHANGED
|
@@ -4,4 +4,8 @@ export interface ObserveOutput {
|
|
|
4
4
|
observation: Observation;
|
|
5
5
|
skip?: string;
|
|
6
6
|
}
|
|
7
|
+
export declare function isCalibrationChurnMarket(market: {
|
|
8
|
+
slug?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
}): boolean;
|
|
7
11
|
export declare function observe(client: CoinRithmClient, spec: AgentSpec, state: RunState, trace?: AgentTrace): Promise<ObserveOutput>;
|
package/dist/agent/observe.js
CHANGED
|
@@ -28,6 +28,16 @@ const PM_COIN_NAMES = {
|
|
|
28
28
|
UNI: "Uniswap",
|
|
29
29
|
SUI: "Sui",
|
|
30
30
|
};
|
|
31
|
+
// Repeated micro-contracts are useful for execution smoke tests but are a poor
|
|
32
|
+
// calibration universe: outcomes overlap heavily, resolve too quickly to admit
|
|
33
|
+
// meaningful independent research, and drown the public scorecard in Bitcoin
|
|
34
|
+
// coin flips. Non-mechanical calibration agents receive a deeper discovery
|
|
35
|
+
// page with these rows removed. Mechanical baselines intentionally keep the
|
|
36
|
+
// unmodified universe so their reference contract remains reproducible.
|
|
37
|
+
const PM_CALIBRATION_CHURN_RE = /(updown|up-or-down|-5-?min|-5m-|-15m|15m(?:-|$)|(?:5|15)\s+min(?:ute)?s?|-1h-|hourly|-daily-|\bdaily\b|what-price-will[^\n]*(?:today|tomorrow)|-above-on-|-price-on-|this[ -]week|of[ -]the[ -]week|-weekly-)/i;
|
|
38
|
+
export function isCalibrationChurnMarket(market) {
|
|
39
|
+
return PM_CALIBRATION_CHURN_RE.test(`${market.slug ?? ""} ${market.title ?? ""}`);
|
|
40
|
+
}
|
|
31
41
|
// Fetch candles for one coin and reduce them to a compact indicator bundle.
|
|
32
42
|
// Tolerant by design: any failure (HTTP error, malformed/sparse candles) returns
|
|
33
43
|
// null so the cycle proceeds with price-only context rather than skipping.
|
|
@@ -54,6 +64,73 @@ function freshnessOf(block) {
|
|
|
54
64
|
const status = asStr(fr.status);
|
|
55
65
|
return status ? { status, ageSeconds: asNum(fr.ageSeconds) } : undefined;
|
|
56
66
|
}
|
|
67
|
+
// Does a market title reference the given watchlist coin? Matches on the PM coin
|
|
68
|
+
// NAME ("Bitcoin") or the ticker ("BTC"), case-insensitively — the discover `q`
|
|
69
|
+
// is a phrase match so a q=Bitcoin result reliably carries "Bitcoin"/"BTC" in the
|
|
70
|
+
// title. Used both to decide whether the primary board already covers the coin the
|
|
71
|
+
// agent analysed and to keep the secondary (crypto-targeted) fetch on-topic.
|
|
72
|
+
function titleMentionsCoin(title, symbol) {
|
|
73
|
+
const t = (title ?? "").toLowerCase();
|
|
74
|
+
if (!t)
|
|
75
|
+
return false;
|
|
76
|
+
const name = (PM_COIN_NAMES[symbol] ?? symbol).toLowerCase();
|
|
77
|
+
const sym = symbol.toLowerCase();
|
|
78
|
+
return t.includes(name) || t.includes(sym);
|
|
79
|
+
}
|
|
80
|
+
// Expand one raw /api/agent/pm/discover payload into per-outcome PmMarket rows
|
|
81
|
+
// (WITHOUT a ref — refs are stamped once over the final merged+sliced list so they
|
|
82
|
+
// stay contiguous pm1..pmN). One row per quoteable outcome; drops outcomes the
|
|
83
|
+
// backend flagged not-openable (eligible === false) and markets the agent already
|
|
84
|
+
// holds (heldPmKeys). Shared by the primary board fetch and the crypto-targeted
|
|
85
|
+
// secondary fetch so both go through the exact same filters.
|
|
86
|
+
function expandPmMarkets(discData, heldPmKeys) {
|
|
87
|
+
const dd = asObj(discData);
|
|
88
|
+
return (asArr(dd.data ?? dd.markets ?? dd.results)
|
|
89
|
+
.map(asObj)
|
|
90
|
+
.flatMap((ev) => {
|
|
91
|
+
const source = (asStr(ev.source) ?? "").toLowerCase();
|
|
92
|
+
const slug = (asStr(ev.slug) ?? "").toLowerCase();
|
|
93
|
+
// Keep titles SHORT: the model only needs to recognise the market.
|
|
94
|
+
// Untrimmed titles, one per outcome across many events, ballooned the
|
|
95
|
+
// prompt to ~69k tokens (413s on small-context free models).
|
|
96
|
+
const title = (asStr(ev.title) ?? asStr(ev.question) ?? "").slice(0, 80);
|
|
97
|
+
const freshness = freshnessOf(ev); // freshness is event-level
|
|
98
|
+
// Event-level 24h volume (the discover payload's `volume24h`, USD). Feeds
|
|
99
|
+
// the mechanical BENCHMARK agents' deterministic highest-volume pick rule.
|
|
100
|
+
// Same for every outcome of the event; undefined on an older backend.
|
|
101
|
+
const volumeUsd = asNum(ev.volume24h) ?? undefined;
|
|
102
|
+
// At most a few outcomes per event so a wide multi-outcome market
|
|
103
|
+
// (e.g. dozens of price buckets) can't explode the prompt. Drop
|
|
104
|
+
// outcomes the backend flagged NOT openable (eligible === false) so the
|
|
105
|
+
// model never bets a market that would fail the binary entry gate at
|
|
106
|
+
// quote. Back-compat: an older backend omits `eligible` (undefined) ->
|
|
107
|
+
// the outcome is kept (current behaviour).
|
|
108
|
+
const outcomes = asArr(ev.outcomes)
|
|
109
|
+
.map(asObj)
|
|
110
|
+
.filter((o) => o.eligible !== false)
|
|
111
|
+
.slice(0, 3);
|
|
112
|
+
// A market with no outcomes array still round-trips a flat fallback row.
|
|
113
|
+
const rows = outcomes.length > 0 ? outcomes : [ev];
|
|
114
|
+
return rows.map((o) => ({
|
|
115
|
+
source,
|
|
116
|
+
slug,
|
|
117
|
+
outcomeExternalMarketId: asStr(o.externalMarketId) ?? asStr(o.outcomeExternalMarketId) ?? "",
|
|
118
|
+
// Carry the odds through: the model needs the outcome label + current
|
|
119
|
+
// probability to spot a mispriced market and bet it.
|
|
120
|
+
outcomeName: asStr(o.name) ?? asStr(o.outcomeName) ?? undefined,
|
|
121
|
+
// Backend returns probability as 0..100 (percent) — normalise to 0..1
|
|
122
|
+
// to match the prompt's "0..1" framing (probed 2026-06-24).
|
|
123
|
+
probability: ((p) => (p == null ? undefined : p > 1 ? p / 100 : p))(asNum(o.probability)),
|
|
124
|
+
title,
|
|
125
|
+
freshness,
|
|
126
|
+
volumeUsd,
|
|
127
|
+
}));
|
|
128
|
+
})
|
|
129
|
+
.filter((m) => m.source && m.slug && m.outcomeExternalMarketId)
|
|
130
|
+
// Drop already-held markets so the model only sees markets it can actually
|
|
131
|
+
// open — done BEFORE any slice so held positions don't consume candidate slots.
|
|
132
|
+
.filter((m) => !heldPmKeys.has(`${m.source.toLowerCase()}|${m.slug.toLowerCase()}|${m.outcomeExternalMarketId}`)));
|
|
133
|
+
}
|
|
57
134
|
function emptyObservation(state, scopes = []) {
|
|
58
135
|
return {
|
|
59
136
|
asOf: state.cursor ?? new Date().toISOString(),
|
|
@@ -222,6 +299,9 @@ export async function observe(client, spec, state, trace) {
|
|
|
222
299
|
let pmResolutions = [];
|
|
223
300
|
let pmMarkets = [];
|
|
224
301
|
if (wantPm) {
|
|
302
|
+
const curatedCalibrationBoard = spec.objective?.primary === "calibration" &&
|
|
303
|
+
spec.model?.provider !== "mechanical";
|
|
304
|
+
const primaryDiscoveryLimit = curatedCalibrationBoard ? 30 : 12;
|
|
225
305
|
// Bias PM discovery toward CRYPTO markets the agent has a price view on — the
|
|
226
306
|
// only PM edge a price agent reliably has (probed 2026-06-24: the default board
|
|
227
307
|
// is World Cup / elections / F1, which an agent has no edge on). The discover
|
|
@@ -232,7 +312,7 @@ export async function observe(client, spec, state, trace) {
|
|
|
232
312
|
const pmQuery = PM_COIN_NAMES[topCoin] ?? spec.risk.watchlist[0] ?? "Bitcoin";
|
|
233
313
|
const [pmPosR, pmDiscFirst] = await Promise.all([
|
|
234
314
|
client.pmPositions(undefined, trace),
|
|
235
|
-
client.discoverPmMarkets({ q: pmQuery, limit:
|
|
315
|
+
client.discoverPmMarkets({ q: pmQuery, limit: primaryDiscoveryLimit }, trace),
|
|
236
316
|
]);
|
|
237
317
|
let pmDiscR = pmDiscFirst;
|
|
238
318
|
const firstCount = pmDiscR.ok
|
|
@@ -296,12 +376,11 @@ export async function observe(client, spec, state, trace) {
|
|
|
296
376
|
.slice(0, 25);
|
|
297
377
|
}
|
|
298
378
|
if (pmDiscR.ok) {
|
|
299
|
-
const dd = asObj(pmDiscR.data);
|
|
300
379
|
// Anti-churn: exclude markets the agent ALREADY holds an open position in
|
|
301
380
|
// from the candidate list BEFORE it reaches the prompt — so the model never
|
|
302
381
|
// sees (and re-picks) a held market only to have the runner/server reject it
|
|
303
382
|
// as a duplicate, burning a whole cycle. Keyed source|slug|outcomeExternalMarketId
|
|
304
|
-
// (lower-cased to match the discover rows
|
|
383
|
+
// (lower-cased to match the discover rows). The runner preflight guard
|
|
305
384
|
// (duplicate_intent) + server dedup (duplicate_open) remain the backstops.
|
|
306
385
|
// Side-agnostic = no re-bet/hedge on a held outcome, matching the runner policy.
|
|
307
386
|
const heldPmKeys = new Set(pmPositions
|
|
@@ -309,57 +388,65 @@ export async function observe(client, spec, state, trace) {
|
|
|
309
388
|
.map((p) => `${(p.source ?? "").toLowerCase()}|${(p.slug ?? "").toLowerCase()}|${p.outcomeExternalMarketId ?? ""}`));
|
|
310
389
|
// Real /api/agent/pm/discover payload: { data: [event], pagination, meta }.
|
|
311
390
|
// Each EVENT carries source/slug/title/freshness at the top level and the
|
|
312
|
-
// quoteable id NESTED at outcomes[].externalMarketId —
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
391
|
+
// quoteable id NESTED at outcomes[].externalMarketId — expandPmMarkets turns
|
|
392
|
+
// that into one row per quoteable outcome (eligible + not-held filtered).
|
|
393
|
+
let mergedRows = expandPmMarkets(pmDiscR.data, heldPmKeys);
|
|
394
|
+
if (curatedCalibrationBoard) {
|
|
395
|
+
mergedRows = mergedRows.filter((market) => !isCalibrationChurnMarket(market));
|
|
396
|
+
}
|
|
397
|
+
// ── Crypto-targeted secondary discover (pm_ref hallucination fix) ────────
|
|
398
|
+
// The prompt tells the model its SHARPEST PM edge is the crypto price view it
|
|
399
|
+
// JUST formed — but that is only actionable if the board actually LISTS a
|
|
400
|
+
// market for the coin it analysed. The primary board is keyed to ONE query
|
|
401
|
+
// (the top watchlist coin, with a Bitcoin fallback when that coin is thin),
|
|
402
|
+
// so an agent whose top coin got displaced by the Bitcoin fallback sees NO
|
|
403
|
+
// market for the coin it has a view on and an 8B model invents a pmN ref
|
|
404
|
+
// (→ pm_ref_unknown, wasted cycle). When the top ANALYSED coin (its sharpest
|
|
405
|
+
// edge) has no market in the primary board, fire ONE extra discover for that
|
|
406
|
+
// coin and MERGE it in — giving the model a real ref to bet instead of a
|
|
407
|
+
// hallucinated one. Budget: at most a single additional CoinRithm data-API
|
|
408
|
+
// read, and only on cycles where the top coin is actually missing; the shared
|
|
409
|
+
// free-tier model-call RateBudget (scheduler) is untouched — this is a read,
|
|
410
|
+
// not an LLM call, and the client already backs off on 429.
|
|
411
|
+
const analyzedCoins = watch
|
|
412
|
+
.filter((w) => w.coinId)
|
|
413
|
+
.map((w) => w.symbol.toUpperCase());
|
|
414
|
+
const topAnalyzed = analyzedCoins[0];
|
|
415
|
+
const primaryCoversTop = !topAnalyzed ||
|
|
416
|
+
mergedRows.some((m) => titleMentionsCoin(m.title, topAnalyzed));
|
|
417
|
+
if (topAnalyzed && !primaryCoversTop) {
|
|
418
|
+
const targetName = PM_COIN_NAMES[topAnalyzed] ?? topAnalyzed;
|
|
419
|
+
// limit 6 (not ~5): the eligible/held/dedupe filters shave the list, and we
|
|
420
|
+
// then cap the merged contribution to 4 targeted rows below.
|
|
421
|
+
const secR = await client.discoverPmMarkets({ q: targetName, limit: 6 }, trace);
|
|
422
|
+
if (secR.ok) {
|
|
423
|
+
// Dedupe the secondary rows against the primary list by source+slug (event
|
|
424
|
+
// key) so a market already on the board is never shown twice, and keep only
|
|
425
|
+
// rows that actually reference the targeted coin (a fuzzy backend match
|
|
426
|
+
// can't dilute the board with off-topic events).
|
|
427
|
+
const primaryEventKeys = new Set(mergedRows.map((m) => `${m.source}|${m.slug}`));
|
|
428
|
+
let secRows = expandPmMarkets(secR.data, heldPmKeys)
|
|
429
|
+
.filter((m) => titleMentionsCoin(m.title, topAnalyzed))
|
|
430
|
+
.filter((m) => !primaryEventKeys.has(`${m.source}|${m.slug}`));
|
|
431
|
+
if (curatedCalibrationBoard) {
|
|
432
|
+
secRows = secRows.filter((market) => !isCalibrationChurnMarket(market));
|
|
433
|
+
}
|
|
434
|
+
secRows = secRows.slice(0, 4);
|
|
435
|
+
// Reserve slots for the targeted rows so the 12-cap can't slice off the
|
|
436
|
+
// very markets the secondary fetch exists to surface. Primary rows keep
|
|
437
|
+
// priority; the targeted rows are appended.
|
|
438
|
+
if (secRows.length > 0) {
|
|
439
|
+
const primaryBudget = Math.max(0, 12 - secRows.length);
|
|
440
|
+
mergedRows = [...mergedRows.slice(0, primaryBudget), ...secRows];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Hard cap the PM block (a handful of fresh markets is plenty) and stamp a
|
|
445
|
+
// short, stable per-cycle ref (pm1…pmN) the model copies instead of the long
|
|
446
|
+
// outcomeExternalMarketId. Refs are assigned AFTER the merge + slice so they
|
|
447
|
+
// are a contiguous 1..N matching exactly what the prompt shows.
|
|
448
|
+
pmMarkets = mergedRows
|
|
359
449
|
.slice(0, 12)
|
|
360
|
-
// Stamp a short, stable per-cycle ref (pm1…pmN) the model copies instead of
|
|
361
|
-
// the long outcomeExternalMarketId. Assigned AFTER the slice so refs are a
|
|
362
|
-
// contiguous 1..N matching exactly what the prompt shows.
|
|
363
450
|
.map((m, i) => ({ ...m, ref: `pm${i + 1}` }));
|
|
364
451
|
}
|
|
365
452
|
}
|
package/dist/agent/prompt.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AgentSpec, Observation, PmResolution } from "./types.js";
|
|
2
2
|
export declare function formatPmResolutions(resolutions: PmResolution[]): string[];
|
|
3
|
-
export declare function buildSystemPrompt(spec: AgentSpec, mergedProse: string
|
|
3
|
+
export declare function buildSystemPrompt(spec: AgentSpec, mergedProse: string, opts?: {
|
|
4
|
+
includeForecast?: boolean;
|
|
5
|
+
}): string;
|
|
4
6
|
export declare function buildUserPrompt(obs: Observation, journal?: Array<{
|
|
5
7
|
at: string;
|
|
6
8
|
did: string;
|
package/dist/agent/prompt.js
CHANGED
|
@@ -33,18 +33,24 @@ export function formatPmResolutions(resolutions) {
|
|
|
33
33
|
`Resolved since last cycle: ${items.join("; ")}.`,
|
|
34
34
|
];
|
|
35
35
|
}
|
|
36
|
-
export function buildSystemPrompt(spec, mergedProse
|
|
36
|
+
export function buildSystemPrompt(spec, mergedProse,
|
|
37
|
+
// includeForecast (default OFF here; the runner passes the house-agent flag):
|
|
38
|
+
// when true, pm_open asks the model for its OWN independent forecastProbability
|
|
39
|
+
// (1..99) — its probability the backed side wins, judged from the question, NOT
|
|
40
|
+
// echoed from the market price. This feeds the agent's public calibration record.
|
|
41
|
+
opts = {}) {
|
|
37
42
|
const r = spec.risk;
|
|
38
43
|
const v = spec.venues;
|
|
44
|
+
const includeForecast = opts.includeForecast === true;
|
|
39
45
|
const actions = [];
|
|
40
46
|
if (v.includes("futures")) {
|
|
41
|
-
actions.push('{"type":"futures_open","symbol","side":"long"|"short","leverage","marginMusd","stopLossPrice","takeProfitPrice","confidence":0..1}', '{"type":"futures_close","positionId","fraction"}', '{"type":"futures_set_sltp","positionId","stopLossPrice","takeProfitPrice"}',
|
|
47
|
+
actions.push('{"type":"futures_open","symbol","side":"long"|"short","leverage","marginMusd","stopLossPrice","takeProfitPrice","confidence":0..1}', '{"type":"futures_close","positionId","fraction"}', '{"type":"futures_set_sltp","positionId","stopLossPrice","takeProfitPrice"}', "FUTURES TRIGGER RULES (the server rejects the WHOLE open otherwise): a LONG's takeProfitPrice must be ABOVE the current mark and stopLossPrice BELOW it (and above liquidationPrice); a SHORT is inverted (TP below mark, SL above). Every open position in observation.openPositions shows entryPrice, markPrice, liquidationPrice, stopLossPrice, takeProfitPrice — read them and place triggers on the correct side. NEVER attach stopLossPrice/takeProfitPrice to a futures_open for a symbol you ALREADY hold (the server treats it as an add and rejects it) — adjust that position with futures_set_sltp on its positionId instead.");
|
|
42
48
|
}
|
|
43
49
|
if (v.includes("spot")) {
|
|
44
50
|
actions.push('{"type":"spot_order","symbol","side":"buy"|"sell","orderType":"market"|"limit"|"stop","quantity","limitPrice","stopPrice","confidence":0..1}', '{"type":"spot_cancel","orderId"}');
|
|
45
51
|
}
|
|
46
52
|
if (v.includes("pm")) {
|
|
47
|
-
actions.push(
|
|
53
|
+
actions.push(`{"type":"pm_open","ref":"pmN","stakeMusd","confidence":0..1${includeForecast ? ',"forecastProbability":1..99' : ""}} (set "ref" to one of the refs listed THIS cycle (pm1..pmN) — the \`ref\` of the ONE observation.pmMarkets entry you are betting, e.g. "pm3", copied EXACTLY; a ref NOT in this cycle's list is rejected as pm_ref_unknown and wastes the cycle; stakeMusd >= 10${includeForecast ? '; set "forecastProbability" to YOUR OWN probability 1-99 that this outcome wins — see the forecast rule below' : ""})`);
|
|
48
54
|
}
|
|
49
55
|
return [
|
|
50
56
|
"You operate a CoinRithm PAPER-TRADING agent (simulated 50,000 mUSD; not real money, not financial advice).",
|
|
@@ -65,7 +71,12 @@ export function buildSystemPrompt(spec, mergedProse) {
|
|
|
65
71
|
: []),
|
|
66
72
|
"- prediction markets are a FIRST-CLASS venue for you — a pm_open is as real a trade as a futures/spot open, not an afterthought. Each observation.pmMarkets entry carries a short `ref` (pm1, pm2, …), an `outcome` label, and `prob` (0..1, the market's CURRENT odds). BET (pm_open) an outcome when YOUR estimate of its true probability differs MATERIALLY from the market's — that gap is your edge (e.g. prob 0.35 but you think it's really ~0.55 -> buy). Skip only markets pinned near 0 or 1 (no edge left). Every entry in observation.pmMarkets is already filtered to one you CAN open (binary/settlement-grade) — so a listed market will not bounce at quote. Pick ONLY a listed market and identify it by copying its `ref` into the action; min stake 10 mUSD. Do NOT re-bet a market+outcome you ALREADY hold (check observation.pmPositions) — that is churn and will be rejected; bet a DIFFERENT market or skip.",
|
|
67
73
|
"- PM stake is a SEPARATE budget from your futures margin: the futures margin cap (maxOpenMarginMusd) does NOT limit pm_open. So when your futures are at the margin/position cap — you hold the max, or a futures_open keeps getting REJECTED with open_margin_exceeds_cap — prediction markets are STILL fully open to you. PIVOT to pm_open on a mispriced market instead of re-proposing a futures_open that will just be rejected: a rejected open wastes the entire cycle, an eligible PM bet does not.",
|
|
68
|
-
"- YOUR SHARPEST PM EDGE is the crypto price view you JUST formed: crypto PM markets resolve on the very prices you analyse, so you have a genuine information edge there that you do NOT have on coin futures alone. EVERY cycle you reach a price conviction, it is REQUIRED that you scan observation.pmMarkets for a crypto market that same view prices wrong and, if one is materially mispriced, open it with pm_open — treat that mispricing exactly like a flagged coin setup (an ACT, not a skip). If you are bearish BTC, a 'BTC above $X by <date>' priced high is a NO; if bullish ETH, an 'ETH above $Y' priced low is a YES.
|
|
74
|
+
"- YOUR SHARPEST PM EDGE is the crypto price view you JUST formed: crypto PM markets resolve on the very prices you analyse, so you have a genuine information edge there that you do NOT have on coin futures alone. EVERY cycle you reach a price conviction, it is REQUIRED that you scan observation.pmMarkets for a LISTED crypto market that same view prices wrong and, if one is materially mispriced, open it with pm_open by its `ref` — treat that mispricing exactly like a flagged coin setup (an ACT, not a skip). If you are bearish BTC, a 'BTC above $X by <date>' priced high is a NO; if bullish ETH, an 'ETH above $Y' priced low is a YES. ESCAPE HATCH — only the markets actually listed in observation.pmMarkets THIS cycle (pm1..pmN) are bettable: if NONE of them matches the coin or view you formed, that is a legitimate SKIP for PM (say so in one clause and move on) — do NOT invent, guess, or increment a ref for a market you wish existed, because a made-up ref is rejected (pm_ref_unknown) and wastes the whole cycle exactly like a rejected open. The mistake to avoid is leaving a LISTED, clearly mispriced crypto market untraded — a mispricing that is NOT on this cycle's board is simply not actionable now, not a miss. (For non-crypto events you have no special edge; skip unless the odds are obviously off.)",
|
|
75
|
+
...(includeForecast
|
|
76
|
+
? [
|
|
77
|
+
"- FORECAST RULE (pm_open forecastProbability): before you look at what the market is pricing, decide YOUR OWN probability the outcome you are backing actually WINS — reason ONLY from the question, its resolution criteria, and the deadline. Put that number (1-99, whole or one decimal) in `forecastProbability`. This is graded against reality as your PUBLIC calibration record, so it must be YOUR judgement, NOT the market's: do NOT copy, round, or anchor it to the observation.pmMarkets `prob`. It is FINE if your honest forecast happens to land on the market's number — but reaching that by echoing the price defeats the point. If you genuinely cannot form an independent view, OMIT the field rather than parroting the market (an absent forecast is better than a fake one, and it never blocks the bet).",
|
|
78
|
+
]
|
|
79
|
+
: []),
|
|
69
80
|
`- abstention.minConfidence ${spec.abstention.minConfidence}: opens below this are rejected, so act with genuine conviction — but routine caution is no reason to sit out a clear setup`,
|
|
70
81
|
...(spec.capabilities.includes("indicators")
|
|
71
82
|
? [
|
|
@@ -95,14 +106,14 @@ export function buildSystemPrompt(spec, mergedProse) {
|
|
|
95
106
|
"",
|
|
96
107
|
"## How to act — a decisive trader in character, not a bystander",
|
|
97
108
|
"You ARE the character in the strategy above; trade like it. When you have a clear read — even a moderate-confidence one — TAKE THE POSITION, sized within your caps and protected with a stop. You wake every cycle and people watch you live: an agent that watches forever and never commits is useless to them and to itself.",
|
|
98
|
-
|
|
109
|
+
"Skip ONLY when the read is genuinely contradictory (signals fight each other), the data is stale, or you truly have no edge this cycle. A quiet tape where your thesis still has a small but REAL edge is an ACT, not a skip — take it, small, with a stop. Do not confuse caution with paralysis.",
|
|
99
110
|
'In "rationale" (shown LIVE in your public terminal) speak in YOUR voice and commit to a view in 1-2 vivid, specific sentences — what you see and what you are DOING about it, like a trader posting their move, not a risk report. Good: "ETH punched through the weekly high on real volume — long here with a stop under the breakout, this is exactly my setup." Weak: "conditions are mixed, waiting for clarity." Keep "reason" a short label.',
|
|
100
111
|
"",
|
|
101
112
|
"## Flagged setups this cycle — your wake-up list (observation.setups)",
|
|
102
113
|
"A deterministic scan already checked every watchlist coin and put the ones with real, tradeable structure RIGHT NOW into observation.setups — each has symbol, kind, bias, strength, and a factual note (trend / RSI / breakout / ATR reads). This is your shortlist; you do NOT need to re-derive whether a setup exists.",
|
|
103
114
|
'- If observation.setups is NON-EMPTY: act on the strongest one that fits YOUR strategy. The `bias` is the trend-following read; if you are a contrarian / mean-reversion trader, FADE it with the same facts (e.g. a downtrend that is also "RSI oversold" is YOUR long). Skipping a flagged setup needs a SPECIFIC reason tied to your thesis — "no clear setup" is NOT a valid skip when setups are listed.',
|
|
104
115
|
"- If observation.setups is EMPTY: no coin has a flagged structure right now — but BEFORE you skip, check observation.pmMarkets for a crypto market your current read prices wrong (a PM mispricing is a valid ACT even with zero coin setups). Only then, if nothing is mispriced, skip new entries and just manage any open positions.",
|
|
105
|
-
|
|
116
|
+
"- A setup tagged `held` (held: long|short) is a position you ALREADY hold. Do NOT propose a new open on it — that only hits the margin cap and wastes the cycle. MANAGE it instead: trail the stop toward your target, ADD only if you have margin room AND fresh conviction, or cut if the thesis broke.",
|
|
106
117
|
"",
|
|
107
118
|
"## After you act — hold with conviction, do not churn",
|
|
108
119
|
"A position is a thesis that needs TIME to work. Once you are in WITH a stop, let the stop or your target close it: do NOT bail on the next cycle over a small adverse tick, and do NOT manually close a fresh position unless the thesis is structurally invalidated (the level broke, the trend flipped) — not merely because price wiggled against you. A trade opened and closed minutes later just donates the round-trip fee + spread to noise.",
|