simmer-automaton 0.6.1 → 0.6.2
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 +32 -0
- package/dist/api.js +3 -0
- package/dist/index.js +85 -8
- package/package.json +1 -1
- package/src/api.ts +37 -0
- package/src/index.ts +88 -10
package/dist/api.d.ts
CHANGED
|
@@ -35,6 +35,37 @@ export interface Skill {
|
|
|
35
35
|
config?: Record<string, number | string | boolean>;
|
|
36
36
|
pinned?: string[];
|
|
37
37
|
}
|
|
38
|
+
export interface BriefingPosition {
|
|
39
|
+
market_id: string;
|
|
40
|
+
question: string;
|
|
41
|
+
side: string;
|
|
42
|
+
shares: number;
|
|
43
|
+
avg_entry: number;
|
|
44
|
+
current_price: number;
|
|
45
|
+
pnl: number;
|
|
46
|
+
resolves_at: string | null;
|
|
47
|
+
source: string | null;
|
|
48
|
+
}
|
|
49
|
+
export interface BriefingResponse {
|
|
50
|
+
portfolio: {
|
|
51
|
+
total_pnl: number;
|
|
52
|
+
pnl_percent: number;
|
|
53
|
+
win_rate: number;
|
|
54
|
+
rank: number | null;
|
|
55
|
+
total_agents: number;
|
|
56
|
+
settled_pnl: number;
|
|
57
|
+
};
|
|
58
|
+
positions: BriefingPosition[];
|
|
59
|
+
recent_trades: Array<{
|
|
60
|
+
market_question: string;
|
|
61
|
+
action: string;
|
|
62
|
+
side: string;
|
|
63
|
+
shares: number;
|
|
64
|
+
cost: number;
|
|
65
|
+
source: string | null;
|
|
66
|
+
created_at: string;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
38
69
|
export interface SkillOutcome {
|
|
39
70
|
skill_slug: string;
|
|
40
71
|
trades: number;
|
|
@@ -90,6 +121,7 @@ export declare class SimmerApi {
|
|
|
90
121
|
outcomes: SkillOutcome[];
|
|
91
122
|
since: string;
|
|
92
123
|
}>;
|
|
124
|
+
getBriefing(): Promise<BriefingResponse>;
|
|
93
125
|
postCycle(data: {
|
|
94
126
|
active_skills: string[];
|
|
95
127
|
cycle_number: number;
|
package/dist/api.js
CHANGED
|
@@ -70,6 +70,9 @@ export class SimmerApi {
|
|
|
70
70
|
const params = new URLSearchParams({ since });
|
|
71
71
|
return this.request(`/api/sdk/automaton/outcomes?${params}`);
|
|
72
72
|
}
|
|
73
|
+
async getBriefing() {
|
|
74
|
+
return this.request("/api/sdk/briefing");
|
|
75
|
+
}
|
|
73
76
|
async postCycle(data) {
|
|
74
77
|
return this.request("/api/sdk/automaton/cycle", {
|
|
75
78
|
method: "POST",
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,8 @@ let banditState = [];
|
|
|
19
19
|
let currentTier = "normal";
|
|
20
20
|
let cycleCount = 0;
|
|
21
21
|
let currentSelectedMeta = [];
|
|
22
|
+
let lastExecutionResults = [];
|
|
23
|
+
let cachedPortfolio = null;
|
|
22
24
|
let serviceRunning = false;
|
|
23
25
|
let cycleTimer = null;
|
|
24
26
|
let lastCycleTimestamp = new Date().toISOString();
|
|
@@ -163,6 +165,31 @@ function buildPromptContext() {
|
|
|
163
165
|
lines.push(`- [${h.skill}] ${h.issue}: ${h.suggestion}`);
|
|
164
166
|
}
|
|
165
167
|
}
|
|
168
|
+
// --- Last execution results ---
|
|
169
|
+
if (lastExecutionResults.length > 0) {
|
|
170
|
+
lines.push("");
|
|
171
|
+
lines.push("**Last execution:**");
|
|
172
|
+
for (const r of lastExecutionResults) {
|
|
173
|
+
lines.push(`- ${r.ok ? "✓" : "✗"} ${r.slug}: ${r.detail}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// --- Portfolio snapshot ---
|
|
177
|
+
if (cachedPortfolio) {
|
|
178
|
+
lines.push("");
|
|
179
|
+
lines.push(`**Portfolio:** ${cachedPortfolio.positionCount} positions | P&L: ${fmtCurrency(cachedPortfolio.totalPnl)} | Recent trades: ${cachedPortfolio.recentTradeCount}`);
|
|
180
|
+
if (cachedPortfolio.positions.length > 0) {
|
|
181
|
+
// Show top 3 by absolute PnL
|
|
182
|
+
const sorted = [...cachedPortfolio.positions].sort((a, b) => Math.abs(b.pnl) - Math.abs(a.pnl));
|
|
183
|
+
const top = sorted.slice(0, 3);
|
|
184
|
+
for (const p of top) {
|
|
185
|
+
const pnlStr = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
|
|
186
|
+
lines.push(` - ${p.question.slice(0, 60)} | ${p.side} ${p.shares} shares @ ${p.avg_entry.toFixed(2)} → ${p.current_price.toFixed(2)} | ${pnlStr}`);
|
|
187
|
+
}
|
|
188
|
+
if (sorted.length > 3) {
|
|
189
|
+
lines.push(` - ...and ${sorted.length - 3} more (use /simmer portfolio for full list)`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
166
193
|
// --- Tier warnings ---
|
|
167
194
|
if (currentTier === "critical") {
|
|
168
195
|
lines.push("");
|
|
@@ -170,7 +197,7 @@ function buildPromptContext() {
|
|
|
170
197
|
}
|
|
171
198
|
// --- Instructions for human-facing queries ---
|
|
172
199
|
lines.push("");
|
|
173
|
-
lines.push("**When your human asks about the automaton:** Report tier, budget,
|
|
200
|
+
lines.push("**When your human asks about the automaton:** Report tier, budget, positions, P&L, and which skills ran last cycle. Use `/simmer portfolio` for full position details. Don't dump raw data — summarize.");
|
|
174
201
|
lines.push("");
|
|
175
202
|
lines.push("**Currency formatting:** $SIM amounts must be written as `XXX $SIM` (e.g. `25.00 $SIM`, `100.00 $SIM`). NEVER write `$SIM25` or `$SIMxx` — the `$SIM` suffix goes AFTER the number. Real USDC uses `$` prefix (e.g. `$25.00`).");
|
|
176
203
|
return lines.join("\n");
|
|
@@ -197,10 +224,10 @@ function formatStatus() {
|
|
|
197
224
|
// =============================================================================
|
|
198
225
|
async function executeSkills(selectedSlugs, workspaceDir, logger) {
|
|
199
226
|
if (selectedSlugs.length === 0)
|
|
200
|
-
return;
|
|
227
|
+
return [];
|
|
201
228
|
if (!runtime) {
|
|
202
229
|
logger.error(`[simmer] runtime not initialized — skipping skill execution`);
|
|
203
|
-
return;
|
|
230
|
+
return [];
|
|
204
231
|
}
|
|
205
232
|
// Build execution plan from cached skills with entrypoints
|
|
206
233
|
const tasks = [];
|
|
@@ -217,7 +244,7 @@ async function executeSkills(selectedSlugs, workspaceDir, logger) {
|
|
|
217
244
|
tasks.push({ slug, entrypoint: skill.entrypoint });
|
|
218
245
|
}
|
|
219
246
|
if (tasks.length === 0)
|
|
220
|
-
return;
|
|
247
|
+
return [];
|
|
221
248
|
const cwd = workspaceDir || process.cwd();
|
|
222
249
|
const env = {
|
|
223
250
|
...process.env,
|
|
@@ -248,11 +275,23 @@ async function executeSkills(selectedSlugs, workspaceDir, logger) {
|
|
|
248
275
|
throw e;
|
|
249
276
|
}
|
|
250
277
|
}));
|
|
251
|
-
const
|
|
252
|
-
|
|
278
|
+
const execResults = [];
|
|
279
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
280
|
+
const r = results[i];
|
|
281
|
+
if (r.status === "fulfilled") {
|
|
282
|
+
const v = r.value;
|
|
283
|
+
execResults.push({ slug: v.slug, ok: v.code === 0, detail: v.code === 0 ? "ok" : `exit ${v.code}: ${v.stderr.slice(0, 100)}` });
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
execResults.push({ slug: tasks[i].slug, ok: false, detail: `spawn error: ${r.reason}` });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const succeeded = execResults.filter((r) => r.ok).length;
|
|
290
|
+
const failed = execResults.filter((r) => !r.ok).length;
|
|
253
291
|
if (failed > 0) {
|
|
254
292
|
logger.warn(`[simmer] Skill execution: ${succeeded} succeeded, ${failed} failed`);
|
|
255
293
|
}
|
|
294
|
+
return execResults;
|
|
256
295
|
}
|
|
257
296
|
// =============================================================================
|
|
258
297
|
// Plugin registration
|
|
@@ -370,12 +409,30 @@ export default function register(pluginApi) {
|
|
|
370
409
|
// Execute selected skills deterministically
|
|
371
410
|
if (!cachedState?.halted && currentTier !== "dead" && selected.length > 0) {
|
|
372
411
|
try {
|
|
373
|
-
await executeSkills(selected, ctx.workspaceDir, ctx.logger);
|
|
412
|
+
lastExecutionResults = await executeSkills(selected, ctx.workspaceDir, ctx.logger);
|
|
374
413
|
}
|
|
375
414
|
catch (e) {
|
|
376
415
|
ctx.logger.error(`[simmer] Skill execution error: ${e}`);
|
|
416
|
+
lastExecutionResults = [];
|
|
377
417
|
}
|
|
378
418
|
}
|
|
419
|
+
else {
|
|
420
|
+
lastExecutionResults = [];
|
|
421
|
+
}
|
|
422
|
+
// Fetch portfolio snapshot for prompt context
|
|
423
|
+
try {
|
|
424
|
+
const briefing = await api.getBriefing();
|
|
425
|
+
const positions = briefing.positions || [];
|
|
426
|
+
cachedPortfolio = {
|
|
427
|
+
totalPnl: briefing.portfolio?.total_pnl ?? 0,
|
|
428
|
+
positionCount: positions.length,
|
|
429
|
+
positions,
|
|
430
|
+
recentTradeCount: briefing.recent_trades?.length ?? 0,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
ctx.logger.warn(`[simmer] Failed to fetch briefing: ${e}`);
|
|
435
|
+
}
|
|
379
436
|
// Also record cycle history (fire-and-forget)
|
|
380
437
|
const cycleData = {
|
|
381
438
|
cycle_num: cycleCount,
|
|
@@ -561,8 +618,28 @@ export default function register(pluginApi) {
|
|
|
561
618
|
return { text: `Failed to reset config: ${e}` };
|
|
562
619
|
}
|
|
563
620
|
}
|
|
621
|
+
if (subcommand === "portfolio") {
|
|
622
|
+
try {
|
|
623
|
+
const briefing = await api.getBriefing();
|
|
624
|
+
const positions = briefing.positions || [];
|
|
625
|
+
if (positions.length === 0) {
|
|
626
|
+
return { text: "No open positions." };
|
|
627
|
+
}
|
|
628
|
+
const lines = positions.map((p) => {
|
|
629
|
+
const pnlStr = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
|
|
630
|
+
return `${p.question.slice(0, 55)} | ${p.side} ${p.shares}sh @ ${p.avg_entry.toFixed(2)} → ${p.current_price.toFixed(2)} | ${pnlStr}${p.source ? ` [${p.source}]` : ""}`;
|
|
631
|
+
});
|
|
632
|
+
const totalPnl = positions.reduce((sum, p) => sum + p.pnl, 0);
|
|
633
|
+
return {
|
|
634
|
+
text: `Positions (${positions.length}) | Total P&L: ${fmtCurrency(totalPnl)}\n\n${lines.join("\n")}`,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
catch (e) {
|
|
638
|
+
return { text: `Failed to fetch portfolio: ${e}` };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
564
641
|
return {
|
|
565
|
-
text: "Usage: /simmer [status|halt|resume|skills|history [N]|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
|
|
642
|
+
text: "Usage: /simmer [status|halt|resume|skills|history [N]|portfolio|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
|
|
566
643
|
};
|
|
567
644
|
},
|
|
568
645
|
});
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -39,6 +39,39 @@ export interface Skill {
|
|
|
39
39
|
pinned?: string[];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface BriefingPosition {
|
|
43
|
+
market_id: string;
|
|
44
|
+
question: string;
|
|
45
|
+
side: string;
|
|
46
|
+
shares: number;
|
|
47
|
+
avg_entry: number;
|
|
48
|
+
current_price: number;
|
|
49
|
+
pnl: number;
|
|
50
|
+
resolves_at: string | null;
|
|
51
|
+
source: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface BriefingResponse {
|
|
55
|
+
portfolio: {
|
|
56
|
+
total_pnl: number;
|
|
57
|
+
pnl_percent: number;
|
|
58
|
+
win_rate: number;
|
|
59
|
+
rank: number | null;
|
|
60
|
+
total_agents: number;
|
|
61
|
+
settled_pnl: number;
|
|
62
|
+
};
|
|
63
|
+
positions: BriefingPosition[];
|
|
64
|
+
recent_trades: Array<{
|
|
65
|
+
market_question: string;
|
|
66
|
+
action: string;
|
|
67
|
+
side: string;
|
|
68
|
+
shares: number;
|
|
69
|
+
cost: number;
|
|
70
|
+
source: string | null;
|
|
71
|
+
created_at: string;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
|
|
42
75
|
export interface SkillOutcome {
|
|
43
76
|
skill_slug: string;
|
|
44
77
|
trades: number;
|
|
@@ -139,6 +172,10 @@ export class SimmerApi {
|
|
|
139
172
|
return this.request(`/api/sdk/automaton/outcomes?${params}`);
|
|
140
173
|
}
|
|
141
174
|
|
|
175
|
+
async getBriefing(): Promise<BriefingResponse> {
|
|
176
|
+
return this.request("/api/sdk/briefing");
|
|
177
|
+
}
|
|
178
|
+
|
|
142
179
|
async postCycle(data: {
|
|
143
180
|
active_skills: string[];
|
|
144
181
|
cycle_number: number;
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { SimmerApi } from "./api.js";
|
|
11
|
-
import type { AutomatonState, Skill, SkillOutcome } from "./api.js";
|
|
11
|
+
import type { AutomatonState, Skill, SkillOutcome, BriefingPosition } from "./api.js";
|
|
12
12
|
import { selectSkills, tierMaxSkills, type SkillState } from "./bandit.js";
|
|
13
13
|
import { computeTier, type Tier } from "./tiers.js";
|
|
14
14
|
import { generateTuningHints, computeTuningChanges, type ConfigChange } from "./tuning.js";
|
|
@@ -57,6 +57,8 @@ let banditState: SkillState[] = [];
|
|
|
57
57
|
let currentTier: Tier = "normal";
|
|
58
58
|
let cycleCount = 0;
|
|
59
59
|
let currentSelectedMeta: Array<{ slug: string; reason: string; score: number | null }> = [];
|
|
60
|
+
let lastExecutionResults: Array<{ slug: string; ok: boolean; detail: string }> = [];
|
|
61
|
+
let cachedPortfolio: { totalPnl: number; positionCount: number; positions: BriefingPosition[]; recentTradeCount: number } | null = null;
|
|
60
62
|
let serviceRunning = false;
|
|
61
63
|
let cycleTimer: ReturnType<typeof setInterval> | null = null;
|
|
62
64
|
let lastCycleTimestamp: string = new Date().toISOString();
|
|
@@ -203,6 +205,33 @@ function buildPromptContext(): string {
|
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
|
|
208
|
+
// --- Last execution results ---
|
|
209
|
+
if (lastExecutionResults.length > 0) {
|
|
210
|
+
lines.push("");
|
|
211
|
+
lines.push("**Last execution:**");
|
|
212
|
+
for (const r of lastExecutionResults) {
|
|
213
|
+
lines.push(`- ${r.ok ? "✓" : "✗"} ${r.slug}: ${r.detail}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Portfolio snapshot ---
|
|
218
|
+
if (cachedPortfolio) {
|
|
219
|
+
lines.push("");
|
|
220
|
+
lines.push(`**Portfolio:** ${cachedPortfolio.positionCount} positions | P&L: ${fmtCurrency(cachedPortfolio.totalPnl)} | Recent trades: ${cachedPortfolio.recentTradeCount}`);
|
|
221
|
+
if (cachedPortfolio.positions.length > 0) {
|
|
222
|
+
// Show top 3 by absolute PnL
|
|
223
|
+
const sorted = [...cachedPortfolio.positions].sort((a, b) => Math.abs(b.pnl) - Math.abs(a.pnl));
|
|
224
|
+
const top = sorted.slice(0, 3);
|
|
225
|
+
for (const p of top) {
|
|
226
|
+
const pnlStr = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
|
|
227
|
+
lines.push(` - ${p.question.slice(0, 60)} | ${p.side} ${p.shares} shares @ ${p.avg_entry.toFixed(2)} → ${p.current_price.toFixed(2)} | ${pnlStr}`);
|
|
228
|
+
}
|
|
229
|
+
if (sorted.length > 3) {
|
|
230
|
+
lines.push(` - ...and ${sorted.length - 3} more (use /simmer portfolio for full list)`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
206
235
|
// --- Tier warnings ---
|
|
207
236
|
if (currentTier === "critical") {
|
|
208
237
|
lines.push("");
|
|
@@ -211,7 +240,7 @@ function buildPromptContext(): string {
|
|
|
211
240
|
|
|
212
241
|
// --- Instructions for human-facing queries ---
|
|
213
242
|
lines.push("");
|
|
214
|
-
lines.push("**When your human asks about the automaton:** Report tier, budget,
|
|
243
|
+
lines.push("**When your human asks about the automaton:** Report tier, budget, positions, P&L, and which skills ran last cycle. Use `/simmer portfolio` for full position details. Don't dump raw data — summarize.");
|
|
215
244
|
lines.push("");
|
|
216
245
|
lines.push("**Currency formatting:** $SIM amounts must be written as `XXX $SIM` (e.g. `25.00 $SIM`, `100.00 $SIM`). NEVER write `$SIM25` or `$SIMxx` — the `$SIM` suffix goes AFTER the number. Real USDC uses `$` prefix (e.g. `$25.00`).");
|
|
217
246
|
|
|
@@ -245,11 +274,11 @@ async function executeSkills(
|
|
|
245
274
|
selectedSlugs: string[],
|
|
246
275
|
workspaceDir: string | undefined,
|
|
247
276
|
logger: { info: (m: string) => void; warn: (m: string) => void; error: (m: string) => void },
|
|
248
|
-
): Promise<
|
|
249
|
-
if (selectedSlugs.length === 0) return;
|
|
277
|
+
): Promise<Array<{ slug: string; ok: boolean; detail: string }>> {
|
|
278
|
+
if (selectedSlugs.length === 0) return [];
|
|
250
279
|
if (!runtime) {
|
|
251
280
|
logger.error(`[simmer] runtime not initialized — skipping skill execution`);
|
|
252
|
-
return;
|
|
281
|
+
return [];
|
|
253
282
|
}
|
|
254
283
|
|
|
255
284
|
// Build execution plan from cached skills with entrypoints
|
|
@@ -267,7 +296,7 @@ async function executeSkills(
|
|
|
267
296
|
tasks.push({ slug, entrypoint: skill.entrypoint });
|
|
268
297
|
}
|
|
269
298
|
|
|
270
|
-
if (tasks.length === 0) return;
|
|
299
|
+
if (tasks.length === 0) return [];
|
|
271
300
|
|
|
272
301
|
const cwd = workspaceDir || process.cwd();
|
|
273
302
|
const env: NodeJS.ProcessEnv = {
|
|
@@ -303,11 +332,23 @@ async function executeSkills(
|
|
|
303
332
|
}),
|
|
304
333
|
);
|
|
305
334
|
|
|
306
|
-
const
|
|
307
|
-
|
|
335
|
+
const execResults: Array<{ slug: string; ok: boolean; detail: string }> = [];
|
|
336
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
337
|
+
const r = results[i];
|
|
338
|
+
if (r.status === "fulfilled") {
|
|
339
|
+
const v = r.value;
|
|
340
|
+
execResults.push({ slug: v.slug, ok: v.code === 0, detail: v.code === 0 ? "ok" : `exit ${v.code}: ${v.stderr.slice(0, 100)}` });
|
|
341
|
+
} else {
|
|
342
|
+
execResults.push({ slug: tasks[i].slug, ok: false, detail: `spawn error: ${r.reason}` });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const succeeded = execResults.filter((r) => r.ok).length;
|
|
347
|
+
const failed = execResults.filter((r) => !r.ok).length;
|
|
308
348
|
if (failed > 0) {
|
|
309
349
|
logger.warn(`[simmer] Skill execution: ${succeeded} succeeded, ${failed} failed`);
|
|
310
350
|
}
|
|
351
|
+
return execResults;
|
|
311
352
|
}
|
|
312
353
|
|
|
313
354
|
// =============================================================================
|
|
@@ -441,10 +482,27 @@ export default function register(pluginApi: PluginApi) {
|
|
|
441
482
|
// Execute selected skills deterministically
|
|
442
483
|
if (!cachedState?.halted && currentTier !== "dead" && selected.length > 0) {
|
|
443
484
|
try {
|
|
444
|
-
await executeSkills(selected, ctx.workspaceDir, ctx.logger);
|
|
485
|
+
lastExecutionResults = await executeSkills(selected, ctx.workspaceDir, ctx.logger);
|
|
445
486
|
} catch (e) {
|
|
446
487
|
ctx.logger.error(`[simmer] Skill execution error: ${e}`);
|
|
488
|
+
lastExecutionResults = [];
|
|
447
489
|
}
|
|
490
|
+
} else {
|
|
491
|
+
lastExecutionResults = [];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Fetch portfolio snapshot for prompt context
|
|
495
|
+
try {
|
|
496
|
+
const briefing = await api.getBriefing();
|
|
497
|
+
const positions = briefing.positions || [];
|
|
498
|
+
cachedPortfolio = {
|
|
499
|
+
totalPnl: briefing.portfolio?.total_pnl ?? 0,
|
|
500
|
+
positionCount: positions.length,
|
|
501
|
+
positions,
|
|
502
|
+
recentTradeCount: briefing.recent_trades?.length ?? 0,
|
|
503
|
+
};
|
|
504
|
+
} catch (e) {
|
|
505
|
+
ctx.logger.warn(`[simmer] Failed to fetch briefing: ${e}`);
|
|
448
506
|
}
|
|
449
507
|
|
|
450
508
|
// Also record cycle history (fire-and-forget)
|
|
@@ -640,8 +698,28 @@ export default function register(pluginApi: PluginApi) {
|
|
|
640
698
|
}
|
|
641
699
|
}
|
|
642
700
|
|
|
701
|
+
if (subcommand === "portfolio") {
|
|
702
|
+
try {
|
|
703
|
+
const briefing = await api.getBriefing();
|
|
704
|
+
const positions = briefing.positions || [];
|
|
705
|
+
if (positions.length === 0) {
|
|
706
|
+
return { text: "No open positions." };
|
|
707
|
+
}
|
|
708
|
+
const lines = positions.map((p: BriefingPosition) => {
|
|
709
|
+
const pnlStr = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
|
|
710
|
+
return `${p.question.slice(0, 55)} | ${p.side} ${p.shares}sh @ ${p.avg_entry.toFixed(2)} → ${p.current_price.toFixed(2)} | ${pnlStr}${p.source ? ` [${p.source}]` : ""}`;
|
|
711
|
+
});
|
|
712
|
+
const totalPnl = positions.reduce((sum: number, p: BriefingPosition) => sum + p.pnl, 0);
|
|
713
|
+
return {
|
|
714
|
+
text: `Positions (${positions.length}) | Total P&L: ${fmtCurrency(totalPnl)}\n\n${lines.join("\n")}`,
|
|
715
|
+
};
|
|
716
|
+
} catch (e) {
|
|
717
|
+
return { text: `Failed to fetch portfolio: ${e}` };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
643
721
|
return {
|
|
644
|
-
text: "Usage: /simmer [status|halt|resume|skills|history [N]|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
|
|
722
|
+
text: "Usage: /simmer [status|halt|resume|skills|history [N]|portfolio|disable <slug>|enable <slug>|config <slug>|tune <slug> <ENV> <val>|reset <slug>]",
|
|
645
723
|
};
|
|
646
724
|
},
|
|
647
725
|
});
|