simmer-automaton 0.4.0 → 0.5.1
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/dist/api.d.ts +20 -0
- package/dist/api.js +6 -0
- package/dist/bandit.js +11 -1
- package/dist/index.js +52 -41
- package/dist/tuning.js +5 -3
- package/package.json +1 -1
- package/src/api.ts +12 -0
- package/src/bandit.ts +8 -1
- package/src/index.ts +52 -40
- package/src/tuning.ts +5 -3
package/dist/api.d.ts
CHANGED
|
@@ -90,4 +90,24 @@ export declare class SimmerApi {
|
|
|
90
90
|
outcomes: SkillOutcome[];
|
|
91
91
|
since: string;
|
|
92
92
|
}>;
|
|
93
|
+
postCycle(data: {
|
|
94
|
+
active_skills: string[];
|
|
95
|
+
cycle_number: number;
|
|
96
|
+
selection_meta: Array<{
|
|
97
|
+
slug: string;
|
|
98
|
+
reason: string;
|
|
99
|
+
score: number | null;
|
|
100
|
+
}>;
|
|
101
|
+
config_changes: Array<{
|
|
102
|
+
slug: string;
|
|
103
|
+
env: string;
|
|
104
|
+
old: any;
|
|
105
|
+
new: any;
|
|
106
|
+
reason: string;
|
|
107
|
+
}>;
|
|
108
|
+
}): Promise<{
|
|
109
|
+
ok: boolean;
|
|
110
|
+
active_skills: string[];
|
|
111
|
+
cycle_number: number;
|
|
112
|
+
}>;
|
|
93
113
|
}
|
package/dist/api.js
CHANGED
|
@@ -70,4 +70,10 @@ export class SimmerApi {
|
|
|
70
70
|
const params = new URLSearchParams({ since });
|
|
71
71
|
return this.request(`/api/sdk/automaton/outcomes?${params}`);
|
|
72
72
|
}
|
|
73
|
+
async postCycle(data) {
|
|
74
|
+
return this.request("/api/sdk/automaton/cycle", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
body: JSON.stringify(data),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
73
79
|
}
|
package/dist/bandit.js
CHANGED
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
function avgReward(skill) {
|
|
6
6
|
if (skill.timesSelected === 0)
|
|
7
7
|
return Infinity;
|
|
8
|
-
|
|
8
|
+
// Use P&L as primary signal. When P&L is unavailable (0), fall back to
|
|
9
|
+
// trade execution rate as bootstrap — a skill that finds and executes trades
|
|
10
|
+
// is better than one that finds nothing.
|
|
11
|
+
if (skill.totalPnl !== 0)
|
|
12
|
+
return skill.totalPnl / skill.timesSelected;
|
|
13
|
+
return skill.tradesExecutedTotal / skill.timesSelected;
|
|
9
14
|
}
|
|
10
15
|
export function tierMaxSkills(tier, maxConcurrent) {
|
|
11
16
|
if (tier === "thriving" || tier === "normal")
|
|
@@ -49,6 +54,8 @@ export function selectSkills(skills, n, tier, epsilon) {
|
|
|
49
54
|
score: Math.round(avgReward(s) * 10000) / 10000,
|
|
50
55
|
})));
|
|
51
56
|
}
|
|
57
|
+
for (const p of picks)
|
|
58
|
+
p.timesSelected++;
|
|
52
59
|
return {
|
|
53
60
|
selected: picks.map((s) => s.slug),
|
|
54
61
|
meta,
|
|
@@ -58,6 +65,8 @@ export function selectSkills(skills, n, tier, epsilon) {
|
|
|
58
65
|
if (tier === "critical") {
|
|
59
66
|
const ranked = [...enabled].sort((a, b) => avgReward(b) - avgReward(a));
|
|
60
67
|
const picks = ranked.slice(0, count);
|
|
68
|
+
for (const p of picks)
|
|
69
|
+
p.timesSelected++;
|
|
61
70
|
return {
|
|
62
71
|
selected: picks.map((s) => s.slug),
|
|
63
72
|
meta: picks.map((s) => ({
|
|
@@ -85,6 +94,7 @@ export function selectSkills(skills, n, tier, epsilon) {
|
|
|
85
94
|
pick = available.reduce((best, s) => avgReward(s) > avgReward(best) ? s : best);
|
|
86
95
|
reason = "exploit";
|
|
87
96
|
}
|
|
97
|
+
pick.timesSelected++;
|
|
88
98
|
selected.push(pick);
|
|
89
99
|
meta.push({
|
|
90
100
|
slug: pick.slug,
|
package/dist/index.js
CHANGED
|
@@ -17,11 +17,11 @@ let cachedSkills = [];
|
|
|
17
17
|
let banditState = [];
|
|
18
18
|
let currentTier = "normal";
|
|
19
19
|
let cycleCount = 0;
|
|
20
|
-
let lastPromptCycle = -1; // Track which cycle was last issued to prevent re-running
|
|
21
20
|
let currentSelectedMeta = [];
|
|
22
21
|
let serviceRunning = false;
|
|
23
22
|
let cycleTimer = null;
|
|
24
23
|
let lastCycleTimestamp = new Date().toISOString();
|
|
24
|
+
let lastKnownStartedAt = null;
|
|
25
25
|
// Config defaults
|
|
26
26
|
let config = {
|
|
27
27
|
apiKey: "",
|
|
@@ -67,6 +67,17 @@ async function refreshState(logger) {
|
|
|
67
67
|
cachedSkills = res.skills;
|
|
68
68
|
}
|
|
69
69
|
if (cachedState.initialized) {
|
|
70
|
+
// Detect re-init (started_at changed) — reset in-memory state
|
|
71
|
+
if (cachedState.started_at && cachedState.started_at !== lastKnownStartedAt) {
|
|
72
|
+
if (lastKnownStartedAt !== null) {
|
|
73
|
+
logger.info(`[simmer] Detected re-init (started_at changed) — resetting cycle counter and bandit state`);
|
|
74
|
+
cycleCount = 0;
|
|
75
|
+
banditState = [];
|
|
76
|
+
currentSelectedMeta = [];
|
|
77
|
+
lastCycleTimestamp = new Date().toISOString();
|
|
78
|
+
}
|
|
79
|
+
lastKnownStartedAt = cachedState.started_at;
|
|
80
|
+
}
|
|
70
81
|
// Sync venue from server state (source of truth — server enforces it)
|
|
71
82
|
if (cachedState.venue) {
|
|
72
83
|
config.venue = cachedState.venue;
|
|
@@ -128,40 +139,17 @@ function buildPromptContext() {
|
|
|
128
139
|
"Re-initialize with a new budget to restart.",
|
|
129
140
|
].join("\n");
|
|
130
141
|
}
|
|
131
|
-
const isNewCycle = cycleCount !== lastPromptCycle;
|
|
132
142
|
const lines = [];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
lines.push(
|
|
137
|
-
lines.push("");
|
|
138
|
-
lines.push("**ACTION: Run these skills now** (copy-paste each command):");
|
|
139
|
-
lines.push("");
|
|
140
|
-
// Build slug → entrypoint map from cachedSkills
|
|
141
|
-
const entrypointMap = new Map(cachedSkills.map((s) => [s.id, s.entrypoint]));
|
|
142
|
-
for (const m of currentSelectedMeta) {
|
|
143
|
-
const entrypoint = entrypointMap.get(m.slug);
|
|
144
|
-
if (entrypoint) {
|
|
145
|
-
const skillConfig = cachedSkills.find((s) => s.id === m.slug)?.config || {};
|
|
146
|
-
const envVars = Object.entries(skillConfig).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
147
|
-
const envPrefix = envVars ? `${envVars} ` : "";
|
|
148
|
-
lines.push(`- \`TRADING_VENUE=${config.venue} ${envPrefix}python ${entrypoint} --live --quiet\``);
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
// No entrypoint in registry — bot should find the script from SKILL.md
|
|
152
|
-
lines.push(`- ${m.slug} (check SKILL.md for entrypoint)`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
lines.push("");
|
|
156
|
-
lines.push("After running each skill, briefly note: trades executed, signals found, or why it was skipped.");
|
|
143
|
+
lines.push(`## Simmer Automaton — Cycle ${cycleCount}`);
|
|
144
|
+
lines.push("");
|
|
145
|
+
if (currentSelectedMeta.length > 0) {
|
|
146
|
+
lines.push(`**Active skills:** ${currentSelectedMeta.map((m) => m.slug).join(", ")}`);
|
|
157
147
|
}
|
|
158
148
|
else {
|
|
159
|
-
lines.push("
|
|
160
|
-
if (currentSelectedMeta.length > 0) {
|
|
161
|
-
lines.push("");
|
|
162
|
-
lines.push(`Skills already issued this cycle (${cycleCount}): ${currentSelectedMeta.map((m) => m.slug).join(", ")}`);
|
|
163
|
-
}
|
|
149
|
+
lines.push("**Active skills:** none selected yet");
|
|
164
150
|
}
|
|
151
|
+
lines.push("**Status:** Skills run independently. The automaton selects which ones can trade.");
|
|
152
|
+
lines.push(`**IMPORTANT:** All skill cron jobs MUST include \`TRADING_VENUE=${config.venue}\` in their environment. Skills default to polymarket otherwise.`);
|
|
165
153
|
// --- Status ---
|
|
166
154
|
lines.push("");
|
|
167
155
|
lines.push(`**Tier:** ${currentTier} | **Venue:** ${config.venue}`);
|
|
@@ -255,6 +243,17 @@ export default function register(pluginApi) {
|
|
|
255
243
|
skill.consecutiveZeroSignals = o.trades > 0 ? 0 : skill.consecutiveZeroSignals + 1;
|
|
256
244
|
}
|
|
257
245
|
}
|
|
246
|
+
// Skills that were selected last cycle but had zero trades won't appear in outcomes
|
|
247
|
+
// (GROUP BY only returns rows with trades). Increment consecutiveZeroSignals for them.
|
|
248
|
+
const outcomeSkillSlugs = new Set(outcomeRes.outcomes.map((o) => o.skill_slug));
|
|
249
|
+
for (const m of currentSelectedMeta) {
|
|
250
|
+
if (!outcomeSkillSlugs.has(m.slug)) {
|
|
251
|
+
const skill = banditState.find((s) => s.slug === m.slug);
|
|
252
|
+
if (skill) {
|
|
253
|
+
skill.consecutiveZeroSignals++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
258
257
|
if (outcomeRes.outcomes.length > 0) {
|
|
259
258
|
ctx.logger.info(`[simmer] Outcomes: ${outcomeRes.outcomes.map((o) => `${o.skill_slug}:${o.trades}t`).join(", ")}`);
|
|
260
259
|
}
|
|
@@ -274,14 +273,6 @@ export default function register(pluginApi) {
|
|
|
274
273
|
pinned: s.pinned || [],
|
|
275
274
|
}));
|
|
276
275
|
tuningChanges = computeTuningChanges(banditState, tunableSkills, cycleCount, cachedState?.budget_usd ?? 0);
|
|
277
|
-
for (const change of tuningChanges) {
|
|
278
|
-
const skill = tunableSkills.find((s) => s.slug === change.slug);
|
|
279
|
-
if (skill) {
|
|
280
|
-
const newConfig = { ...skill.config, [change.env]: change.newValue };
|
|
281
|
-
api.setSkillConfig(change.slug, newConfig)
|
|
282
|
-
.catch((e) => ctx.logger.error(`[simmer] Failed to apply config: ${e}`));
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
276
|
if (tuningChanges.length > 0) {
|
|
286
277
|
ctx.logger.info(`[simmer] Tuning: ${tuningChanges.map((c) => `${c.slug}.${c.env}: ${c.oldValue} → ${c.newValue}`).join(", ")}`);
|
|
287
278
|
}
|
|
@@ -293,7 +284,27 @@ export default function register(pluginApi) {
|
|
|
293
284
|
const { selected, meta } = selectSkills(banditState, n, currentTier, config.epsilon);
|
|
294
285
|
currentSelectedMeta = meta;
|
|
295
286
|
const hints = generateTuningHints(banditState, cachedState?.budget_usd ?? 0);
|
|
296
|
-
//
|
|
287
|
+
// POST cycle to API — writes active_skills + config_changes atomically
|
|
288
|
+
// This replaces both individual setSkillConfig calls and the old recordCycle
|
|
289
|
+
const configChangesPayload = tuningChanges.map((c) => ({
|
|
290
|
+
slug: c.slug,
|
|
291
|
+
env: c.env,
|
|
292
|
+
old: c.oldValue,
|
|
293
|
+
new: c.newValue,
|
|
294
|
+
reason: c.reason,
|
|
295
|
+
}));
|
|
296
|
+
try {
|
|
297
|
+
await api.postCycle({
|
|
298
|
+
active_skills: selected,
|
|
299
|
+
cycle_number: cycleCount,
|
|
300
|
+
selection_meta: meta.map((m) => ({ slug: m.slug, reason: m.reason, score: m.score })),
|
|
301
|
+
config_changes: configChangesPayload,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
catch (e) {
|
|
305
|
+
ctx.logger.warn(`[simmer] Failed to post cycle (active_skills may be stale): ${e}`);
|
|
306
|
+
}
|
|
307
|
+
// Also record cycle history (fire-and-forget)
|
|
297
308
|
const cycleData = {
|
|
298
309
|
cycle_num: cycleCount,
|
|
299
310
|
tier: currentTier,
|
|
@@ -304,7 +315,7 @@ export default function register(pluginApi) {
|
|
|
304
315
|
spent_usd: cachedState?.spent_usd,
|
|
305
316
|
};
|
|
306
317
|
if (tuningChanges.length > 0) {
|
|
307
|
-
cycleData.config_changes =
|
|
318
|
+
cycleData.config_changes = configChangesPayload;
|
|
308
319
|
}
|
|
309
320
|
api.recordCycle(cycleData).catch((e) => ctx.logger.error(`[simmer] Failed to record cycle: ${e}`));
|
|
310
321
|
ctx.logger.info(`[simmer] Cycle ${cycleCount} | tier=${currentTier} | ε=${config.epsilon.toFixed(3)} | selected=${selected.length} skills`);
|
package/dist/tuning.js
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
// Track last tuned cycle per skill to enforce cooldowns
|
|
8
8
|
const lastTunedCycle = new Map();
|
|
9
|
-
function
|
|
9
|
+
function isEntryThresholdTunable(t) {
|
|
10
10
|
const l = t.label.toLowerCase();
|
|
11
|
-
|
|
11
|
+
// Only widen entry-side thresholds (lower = accept more signals).
|
|
12
|
+
// Exit/confidence thresholds should NOT be lowered — that degrades trade quality.
|
|
13
|
+
return l.includes("entry") || l.includes("edge") || l.includes("min split") || l.includes("momentum");
|
|
12
14
|
}
|
|
13
15
|
function isMaxBetTunable(t) {
|
|
14
16
|
const l = t.label.toLowerCase();
|
|
@@ -103,7 +105,7 @@ export function computeTuningChanges(skills, tunableSkills, cycleCount, budgetUs
|
|
|
103
105
|
// Rule 1: consecutiveZeroSignals >= 5 → widen thresholds by 20% (cooldown 10)
|
|
104
106
|
if (sk.consecutiveZeroSignals >= 5 && cycleCount - lastTuned >= 10) {
|
|
105
107
|
for (const t of ts.tunables) {
|
|
106
|
-
if (t.type !== "number" || pinnedSet.has(t.env) || !
|
|
108
|
+
if (t.type !== "number" || pinnedSet.has(t.env) || !isEntryThresholdTunable(t))
|
|
107
109
|
continue;
|
|
108
110
|
const cur = currentValue(t, ts.config);
|
|
109
111
|
if (typeof cur !== "number")
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -138,4 +138,16 @@ export class SimmerApi {
|
|
|
138
138
|
const params = new URLSearchParams({ since });
|
|
139
139
|
return this.request(`/api/sdk/automaton/outcomes?${params}`);
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
async postCycle(data: {
|
|
143
|
+
active_skills: string[];
|
|
144
|
+
cycle_number: number;
|
|
145
|
+
selection_meta: Array<{ slug: string; reason: string; score: number | null }>;
|
|
146
|
+
config_changes: Array<{ slug: string; env: string; old: any; new: any; reason: string }>;
|
|
147
|
+
}): Promise<{ ok: boolean; active_skills: string[]; cycle_number: number }> {
|
|
148
|
+
return this.request("/api/sdk/automaton/cycle", {
|
|
149
|
+
method: "POST",
|
|
150
|
+
body: JSON.stringify(data),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
141
153
|
}
|
package/src/bandit.ts
CHANGED
|
@@ -25,7 +25,11 @@ export interface SelectionMeta {
|
|
|
25
25
|
|
|
26
26
|
function avgReward(skill: SkillState): number {
|
|
27
27
|
if (skill.timesSelected === 0) return Infinity;
|
|
28
|
-
|
|
28
|
+
// Use P&L as primary signal. When P&L is unavailable (0), fall back to
|
|
29
|
+
// trade execution rate as bootstrap — a skill that finds and executes trades
|
|
30
|
+
// is better than one that finds nothing.
|
|
31
|
+
if (skill.totalPnl !== 0) return skill.totalPnl / skill.timesSelected;
|
|
32
|
+
return skill.tradesExecutedTotal / skill.timesSelected;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export function tierMaxSkills(tier: string, maxConcurrent: number): number {
|
|
@@ -78,6 +82,7 @@ export function selectSkills(
|
|
|
78
82
|
);
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
for (const p of picks) p.timesSelected++;
|
|
81
86
|
return {
|
|
82
87
|
selected: picks.map((s) => s.slug),
|
|
83
88
|
meta,
|
|
@@ -90,6 +95,7 @@ export function selectSkills(
|
|
|
90
95
|
(a, b) => avgReward(b) - avgReward(a),
|
|
91
96
|
);
|
|
92
97
|
const picks = ranked.slice(0, count);
|
|
98
|
+
for (const p of picks) p.timesSelected++;
|
|
93
99
|
return {
|
|
94
100
|
selected: picks.map((s) => s.slug),
|
|
95
101
|
meta: picks.map((s) => ({
|
|
@@ -122,6 +128,7 @@ export function selectSkills(
|
|
|
122
128
|
reason = "exploit";
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
pick.timesSelected++;
|
|
125
132
|
selected.push(pick);
|
|
126
133
|
meta.push({
|
|
127
134
|
slug: pick.slug,
|
package/src/index.ts
CHANGED
|
@@ -38,11 +38,11 @@ let cachedSkills: Skill[] = [];
|
|
|
38
38
|
let banditState: SkillState[] = [];
|
|
39
39
|
let currentTier: Tier = "normal";
|
|
40
40
|
let cycleCount = 0;
|
|
41
|
-
let lastPromptCycle = -1; // Track which cycle was last issued to prevent re-running
|
|
42
41
|
let currentSelectedMeta: Array<{ slug: string; reason: string; score: number | null }> = [];
|
|
43
42
|
let serviceRunning = false;
|
|
44
43
|
let cycleTimer: ReturnType<typeof setInterval> | null = null;
|
|
45
44
|
let lastCycleTimestamp: string = new Date().toISOString();
|
|
45
|
+
let lastKnownStartedAt: string | null = null;
|
|
46
46
|
|
|
47
47
|
// Config defaults
|
|
48
48
|
let config = {
|
|
@@ -82,6 +82,17 @@ async function refreshState(logger: { info: (m: string) => void; error: (m: stri
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
if (cachedState.initialized) {
|
|
85
|
+
// Detect re-init (started_at changed) — reset in-memory state
|
|
86
|
+
if (cachedState.started_at && cachedState.started_at !== lastKnownStartedAt) {
|
|
87
|
+
if (lastKnownStartedAt !== null) {
|
|
88
|
+
logger.info(`[simmer] Detected re-init (started_at changed) — resetting cycle counter and bandit state`);
|
|
89
|
+
cycleCount = 0;
|
|
90
|
+
banditState = [];
|
|
91
|
+
currentSelectedMeta = [];
|
|
92
|
+
lastCycleTimestamp = new Date().toISOString();
|
|
93
|
+
}
|
|
94
|
+
lastKnownStartedAt = cachedState.started_at;
|
|
95
|
+
}
|
|
85
96
|
// Sync venue from server state (source of truth — server enforces it)
|
|
86
97
|
if (cachedState.venue) {
|
|
87
98
|
config.venue = cachedState.venue;
|
|
@@ -148,39 +159,17 @@ function buildPromptContext(): string {
|
|
|
148
159
|
].join("\n");
|
|
149
160
|
}
|
|
150
161
|
|
|
151
|
-
const isNewCycle = cycleCount !== lastPromptCycle;
|
|
152
162
|
const lines: string[] = [];
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
lines.push(
|
|
158
|
-
lines.push("");
|
|
159
|
-
lines.push("**ACTION: Run these skills now** (copy-paste each command):");
|
|
160
|
-
lines.push("");
|
|
161
|
-
// Build slug → entrypoint map from cachedSkills
|
|
162
|
-
const entrypointMap = new Map(cachedSkills.map((s) => [s.id, s.entrypoint]));
|
|
163
|
-
for (const m of currentSelectedMeta) {
|
|
164
|
-
const entrypoint = entrypointMap.get(m.slug);
|
|
165
|
-
if (entrypoint) {
|
|
166
|
-
const skillConfig = cachedSkills.find((s) => s.id === m.slug)?.config || {};
|
|
167
|
-
const envVars = Object.entries(skillConfig).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
168
|
-
const envPrefix = envVars ? `${envVars} ` : "";
|
|
169
|
-
lines.push(`- \`TRADING_VENUE=${config.venue} ${envPrefix}python ${entrypoint} --live --quiet\``);
|
|
170
|
-
} else {
|
|
171
|
-
// No entrypoint in registry — bot should find the script from SKILL.md
|
|
172
|
-
lines.push(`- ${m.slug} (check SKILL.md for entrypoint)`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
lines.push("");
|
|
176
|
-
lines.push("After running each skill, briefly note: trades executed, signals found, or why it was skipped.");
|
|
164
|
+
lines.push(`## Simmer Automaton — Cycle ${cycleCount}`);
|
|
165
|
+
lines.push("");
|
|
166
|
+
if (currentSelectedMeta.length > 0) {
|
|
167
|
+
lines.push(`**Active skills:** ${currentSelectedMeta.map((m) => m.slug).join(", ")}`);
|
|
177
168
|
} else {
|
|
178
|
-
lines.push("
|
|
179
|
-
if (currentSelectedMeta.length > 0) {
|
|
180
|
-
lines.push("");
|
|
181
|
-
lines.push(`Skills already issued this cycle (${cycleCount}): ${currentSelectedMeta.map((m) => m.slug).join(", ")}`);
|
|
182
|
-
}
|
|
169
|
+
lines.push("**Active skills:** none selected yet");
|
|
183
170
|
}
|
|
171
|
+
lines.push("**Status:** Skills run independently. The automaton selects which ones can trade.");
|
|
172
|
+
lines.push(`**IMPORTANT:** All skill cron jobs MUST include \`TRADING_VENUE=${config.venue}\` in their environment. Skills default to polymarket otherwise.`);
|
|
184
173
|
|
|
185
174
|
// --- Status ---
|
|
186
175
|
lines.push("");
|
|
@@ -291,6 +280,17 @@ export default function register(pluginApi: PluginApi) {
|
|
|
291
280
|
skill.consecutiveZeroSignals = o.trades > 0 ? 0 : skill.consecutiveZeroSignals + 1;
|
|
292
281
|
}
|
|
293
282
|
}
|
|
283
|
+
// Skills that were selected last cycle but had zero trades won't appear in outcomes
|
|
284
|
+
// (GROUP BY only returns rows with trades). Increment consecutiveZeroSignals for them.
|
|
285
|
+
const outcomeSkillSlugs = new Set(outcomeRes.outcomes.map((o: SkillOutcome) => o.skill_slug));
|
|
286
|
+
for (const m of currentSelectedMeta) {
|
|
287
|
+
if (!outcomeSkillSlugs.has(m.slug)) {
|
|
288
|
+
const skill = banditState.find((s) => s.slug === m.slug);
|
|
289
|
+
if (skill) {
|
|
290
|
+
skill.consecutiveZeroSignals++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
294
|
if (outcomeRes.outcomes.length > 0) {
|
|
295
295
|
ctx.logger.info(`[simmer] Outcomes: ${outcomeRes.outcomes.map((o: SkillOutcome) => `${o.skill_slug}:${o.trades}t`).join(", ")}`);
|
|
296
296
|
}
|
|
@@ -310,14 +310,6 @@ export default function register(pluginApi: PluginApi) {
|
|
|
310
310
|
pinned: s.pinned || [],
|
|
311
311
|
}));
|
|
312
312
|
tuningChanges = computeTuningChanges(banditState, tunableSkills, cycleCount, cachedState?.budget_usd ?? 0);
|
|
313
|
-
for (const change of tuningChanges) {
|
|
314
|
-
const skill = tunableSkills.find((s) => s.slug === change.slug);
|
|
315
|
-
if (skill) {
|
|
316
|
-
const newConfig = { ...skill.config, [change.env]: change.newValue };
|
|
317
|
-
api.setSkillConfig(change.slug, newConfig)
|
|
318
|
-
.catch((e) => ctx.logger.error(`[simmer] Failed to apply config: ${e}`));
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
313
|
if (tuningChanges.length > 0) {
|
|
322
314
|
ctx.logger.info(`[simmer] Tuning: ${tuningChanges.map((c) => `${c.slug}.${c.env}: ${c.oldValue} → ${c.newValue}`).join(", ")}`);
|
|
323
315
|
}
|
|
@@ -335,7 +327,27 @@ export default function register(pluginApi: PluginApi) {
|
|
|
335
327
|
currentSelectedMeta = meta;
|
|
336
328
|
const hints = generateTuningHints(banditState, cachedState?.budget_usd ?? 0);
|
|
337
329
|
|
|
338
|
-
//
|
|
330
|
+
// POST cycle to API — writes active_skills + config_changes atomically
|
|
331
|
+
// This replaces both individual setSkillConfig calls and the old recordCycle
|
|
332
|
+
const configChangesPayload = tuningChanges.map((c) => ({
|
|
333
|
+
slug: c.slug,
|
|
334
|
+
env: c.env,
|
|
335
|
+
old: c.oldValue,
|
|
336
|
+
new: c.newValue,
|
|
337
|
+
reason: c.reason,
|
|
338
|
+
}));
|
|
339
|
+
try {
|
|
340
|
+
await api.postCycle({
|
|
341
|
+
active_skills: selected,
|
|
342
|
+
cycle_number: cycleCount,
|
|
343
|
+
selection_meta: meta.map((m) => ({ slug: m.slug, reason: m.reason, score: m.score })),
|
|
344
|
+
config_changes: configChangesPayload,
|
|
345
|
+
});
|
|
346
|
+
} catch (e) {
|
|
347
|
+
ctx.logger.warn(`[simmer] Failed to post cycle (active_skills may be stale): ${e}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Also record cycle history (fire-and-forget)
|
|
339
351
|
const cycleData: Record<string, unknown> = {
|
|
340
352
|
cycle_num: cycleCount,
|
|
341
353
|
tier: currentTier,
|
|
@@ -346,7 +358,7 @@ export default function register(pluginApi: PluginApi) {
|
|
|
346
358
|
spent_usd: cachedState?.spent_usd,
|
|
347
359
|
};
|
|
348
360
|
if (tuningChanges.length > 0) {
|
|
349
|
-
cycleData.config_changes =
|
|
361
|
+
cycleData.config_changes = configChangesPayload;
|
|
350
362
|
}
|
|
351
363
|
api.recordCycle(cycleData as any).catch((e) => ctx.logger.error(`[simmer] Failed to record cycle: ${e}`));
|
|
352
364
|
|
package/src/tuning.ts
CHANGED
|
@@ -33,9 +33,11 @@ export interface TunableSkill {
|
|
|
33
33
|
// Track last tuned cycle per skill to enforce cooldowns
|
|
34
34
|
const lastTunedCycle = new Map<string, number>();
|
|
35
35
|
|
|
36
|
-
function
|
|
36
|
+
function isEntryThresholdTunable(t: Tunable): boolean {
|
|
37
37
|
const l = t.label.toLowerCase();
|
|
38
|
-
|
|
38
|
+
// Only widen entry-side thresholds (lower = accept more signals).
|
|
39
|
+
// Exit/confidence thresholds should NOT be lowered — that degrades trade quality.
|
|
40
|
+
return l.includes("entry") || l.includes("edge") || l.includes("min split") || l.includes("momentum");
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
function isMaxBetTunable(t: Tunable): boolean {
|
|
@@ -151,7 +153,7 @@ export function computeTuningChanges(
|
|
|
151
153
|
// Rule 1: consecutiveZeroSignals >= 5 → widen thresholds by 20% (cooldown 10)
|
|
152
154
|
if (sk.consecutiveZeroSignals >= 5 && cycleCount - lastTuned >= 10) {
|
|
153
155
|
for (const t of ts.tunables) {
|
|
154
|
-
if (t.type !== "number" || pinnedSet.has(t.env) || !
|
|
156
|
+
if (t.type !== "number" || pinnedSet.has(t.env) || !isEntryThresholdTunable(t)) continue;
|
|
155
157
|
const cur = currentValue(t, ts.config);
|
|
156
158
|
if (typeof cur !== "number") continue;
|
|
157
159
|
let newVal = cur * 0.8;
|