@spfunctions/cli 1.1.6 → 1.1.8
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/client.d.ts +2 -0
- package/dist/client.js +6 -0
- package/dist/commands/agent.js +347 -62
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +73 -0
- package/dist/commands/whatif.d.ts +17 -0
- package/dist/commands/whatif.js +209 -0
- package/dist/index.js +23 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare class SFClient {
|
|
|
17
17
|
createThesis(rawThesis: string, sync?: boolean): Promise<any>;
|
|
18
18
|
injectSignal(id: string, type: string, content: string, source?: string): Promise<any>;
|
|
19
19
|
evaluate(id: string): Promise<any>;
|
|
20
|
+
getFeed(hours?: number, limit?: number): Promise<any>;
|
|
21
|
+
getChanges(id: string, since: string): Promise<any>;
|
|
20
22
|
updateThesis(id: string, data: Record<string, unknown>): Promise<any>;
|
|
21
23
|
publish(id: string, slug: string, description?: string): Promise<any>;
|
|
22
24
|
unpublish(id: string): Promise<any>;
|
package/dist/client.js
CHANGED
|
@@ -62,6 +62,12 @@ class SFClient {
|
|
|
62
62
|
async evaluate(id) {
|
|
63
63
|
return this.request('POST', `/api/thesis/${id}/evaluate`);
|
|
64
64
|
}
|
|
65
|
+
async getFeed(hours = 24, limit = 200) {
|
|
66
|
+
return this.request('GET', `/api/feed?hours=${hours}&limit=${limit}`);
|
|
67
|
+
}
|
|
68
|
+
async getChanges(id, since) {
|
|
69
|
+
return this.request('GET', `/api/thesis/${id}/changes?since=${encodeURIComponent(since)}`);
|
|
70
|
+
}
|
|
65
71
|
async updateThesis(id, data) {
|
|
66
72
|
return this.request('PATCH', `/api/thesis/${id}`, data);
|
|
67
73
|
}
|
package/dist/commands/agent.js
CHANGED
|
@@ -100,27 +100,53 @@ function createMutableLine(piTui) {
|
|
|
100
100
|
}
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Header bar — trading terminal style.
|
|
105
|
+
* Shows: thesis ID, confidence+delta, positions P&L, edge count, top edge
|
|
106
|
+
*/
|
|
104
107
|
function createHeaderBar(piTui) {
|
|
105
108
|
const { truncateToWidth, visibleWidth } = piTui;
|
|
106
109
|
return class HeaderBar {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
thesisId = '';
|
|
111
|
+
confidence = 0;
|
|
112
|
+
confidenceDelta = 0;
|
|
113
|
+
pnlDollars = 0;
|
|
114
|
+
positionCount = 0;
|
|
115
|
+
edgeCount = 0;
|
|
116
|
+
topEdge = ''; // e.g. "RECESSION +21¢"
|
|
110
117
|
cachedWidth;
|
|
111
118
|
cachedLines;
|
|
112
|
-
|
|
113
|
-
this.
|
|
114
|
-
this.
|
|
115
|
-
|
|
119
|
+
setFromContext(ctx, positions) {
|
|
120
|
+
this.thesisId = (ctx.thesisId || '').slice(0, 8);
|
|
121
|
+
this.confidence = typeof ctx.confidence === 'number'
|
|
122
|
+
? Math.round(ctx.confidence * 100)
|
|
123
|
+
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
124
|
+
this.confidenceDelta = ctx.lastEvaluation?.confidenceDelta
|
|
125
|
+
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
126
|
+
: 0;
|
|
127
|
+
this.edgeCount = (ctx.edges || []).length;
|
|
128
|
+
// Top edge by absolute size
|
|
129
|
+
const edges = ctx.edges || [];
|
|
130
|
+
if (edges.length > 0) {
|
|
131
|
+
const top = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0))[0];
|
|
132
|
+
const name = (top.market || top.marketTitle || top.marketId || '').slice(0, 20);
|
|
133
|
+
const edge = top.edge || top.edgeSize || 0;
|
|
134
|
+
this.topEdge = `${name} ${edge > 0 ? '+' : ''}${Math.round(edge)}\u00A2`;
|
|
135
|
+
}
|
|
136
|
+
// P&L from positions
|
|
137
|
+
if (positions && positions.length > 0) {
|
|
138
|
+
this.positionCount = positions.length;
|
|
139
|
+
this.pnlDollars = positions.reduce((sum, p) => {
|
|
140
|
+
const pnl = p.unrealized_pnl || 0;
|
|
141
|
+
return sum + pnl;
|
|
142
|
+
}, 0) / 100; // cents → dollars
|
|
143
|
+
}
|
|
144
|
+
this.cachedWidth = undefined;
|
|
145
|
+
this.cachedLines = undefined;
|
|
116
146
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (center !== undefined)
|
|
121
|
-
this.center = center;
|
|
122
|
-
if (right !== undefined)
|
|
123
|
-
this.right = right;
|
|
147
|
+
updateConfidence(newConf, delta) {
|
|
148
|
+
this.confidence = Math.round(newConf * 100);
|
|
149
|
+
this.confidenceDelta = Math.round(delta * 100);
|
|
124
150
|
this.cachedWidth = undefined;
|
|
125
151
|
this.cachedLines = undefined;
|
|
126
152
|
}
|
|
@@ -128,23 +154,39 @@ function createHeaderBar(piTui) {
|
|
|
128
154
|
this.cachedWidth = undefined;
|
|
129
155
|
this.cachedLines = undefined;
|
|
130
156
|
}
|
|
157
|
+
// Keep legacy update() for compatibility with /switch etc.
|
|
158
|
+
update(left, center, right) {
|
|
159
|
+
this.cachedWidth = undefined;
|
|
160
|
+
this.cachedLines = undefined;
|
|
161
|
+
}
|
|
131
162
|
render(width) {
|
|
132
163
|
if (this.cachedLines && this.cachedWidth === width)
|
|
133
164
|
return this.cachedLines;
|
|
134
165
|
this.cachedWidth = width;
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
166
|
+
// Build segments
|
|
167
|
+
const id = C.emerald(bold(this.thesisId));
|
|
168
|
+
// Confidence with arrow
|
|
169
|
+
const arrow = this.confidenceDelta > 0 ? '\u25B2' : this.confidenceDelta < 0 ? '\u25BC' : '\u2500';
|
|
170
|
+
const arrowColor = this.confidenceDelta > 0 ? C.emerald : this.confidenceDelta < 0 ? C.red : C.zinc600;
|
|
171
|
+
const deltaStr = this.confidenceDelta !== 0 ? ` (${this.confidenceDelta > 0 ? '+' : ''}${this.confidenceDelta})` : '';
|
|
172
|
+
const conf = arrowColor(`${arrow} ${this.confidence}%${deltaStr}`);
|
|
173
|
+
// P&L
|
|
174
|
+
let pnl = '';
|
|
175
|
+
if (this.positionCount > 0) {
|
|
176
|
+
const pnlStr = this.pnlDollars >= 0
|
|
177
|
+
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
178
|
+
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
179
|
+
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
180
|
+
}
|
|
181
|
+
// Edges
|
|
182
|
+
const edges = C.zinc600(`${this.edgeCount} edges`);
|
|
183
|
+
// Top edge
|
|
184
|
+
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
185
|
+
// Assemble with separators
|
|
186
|
+
const sep = C.zinc600(' \u2502 ');
|
|
187
|
+
const parts = [id, conf, pnl, edges, top].filter(Boolean);
|
|
188
|
+
const content = parts.join(sep);
|
|
189
|
+
let line = C.bgZinc900(' ' + truncateToWidth(content, width - 2, '') + ' ');
|
|
148
190
|
const lineVw = visibleWidth(line);
|
|
149
191
|
if (lineVw < width) {
|
|
150
192
|
line = line + C.bgZinc900(' '.repeat(width - lineVw));
|
|
@@ -154,13 +196,16 @@ function createHeaderBar(piTui) {
|
|
|
154
196
|
}
|
|
155
197
|
};
|
|
156
198
|
}
|
|
157
|
-
/** Footer bar:
|
|
199
|
+
/** Footer bar: model | tokens | exchange status | trading status | /help */
|
|
158
200
|
function createFooterBar(piTui) {
|
|
159
201
|
const { truncateToWidth, visibleWidth } = piTui;
|
|
160
202
|
return class FooterBar {
|
|
161
203
|
tokens = 0;
|
|
162
204
|
cost = 0;
|
|
163
205
|
toolCount = 0;
|
|
206
|
+
modelName = '';
|
|
207
|
+
tradingEnabled = false;
|
|
208
|
+
exchangeOpen = null; // null = unknown
|
|
164
209
|
cachedWidth;
|
|
165
210
|
cachedLines;
|
|
166
211
|
invalidate() {
|
|
@@ -178,13 +223,23 @@ function createFooterBar(piTui) {
|
|
|
178
223
|
const tokStr = this.tokens >= 1000
|
|
179
224
|
? `${(this.tokens / 1000).toFixed(1)}k`
|
|
180
225
|
: `${this.tokens}`;
|
|
181
|
-
const
|
|
182
|
-
const
|
|
226
|
+
const model = C.zinc600(this.modelName.split('/').pop() || this.modelName);
|
|
227
|
+
const tokens = C.zinc600(`${tokStr} tok`);
|
|
228
|
+
const exchange = this.exchangeOpen === true
|
|
229
|
+
? C.emerald('OPEN')
|
|
230
|
+
: this.exchangeOpen === false
|
|
231
|
+
? C.red('CLOSED')
|
|
232
|
+
: C.zinc600('...');
|
|
233
|
+
const trading = this.tradingEnabled
|
|
234
|
+
? C.amber('\u26A1 trading')
|
|
235
|
+
: C.zinc600('\u26A1 read-only');
|
|
236
|
+
const help = C.zinc600('/help');
|
|
237
|
+
const sep = C.zinc600(' \u2502 ');
|
|
238
|
+
const leftText = [model, tokens, exchange, trading].join(sep);
|
|
183
239
|
const lw = visibleWidth(leftText);
|
|
184
|
-
const rw = visibleWidth(
|
|
185
|
-
const gap = Math.max(1, width - lw - rw);
|
|
186
|
-
let line = leftText + ' '.repeat(gap) +
|
|
187
|
-
line = C.bgZinc900(truncateToWidth(line, width, ''));
|
|
240
|
+
const rw = visibleWidth(help);
|
|
241
|
+
const gap = Math.max(1, width - lw - rw - 2);
|
|
242
|
+
let line = C.bgZinc900(' ' + leftText + ' '.repeat(gap) + help + ' ');
|
|
188
243
|
const lineVw = visibleWidth(line);
|
|
189
244
|
if (lineVw < width) {
|
|
190
245
|
line = line + C.bgZinc900(' '.repeat(width - lineVw));
|
|
@@ -378,6 +433,13 @@ async function agentCommand(thesisId, opts) {
|
|
|
378
433
|
let isProcessing = false;
|
|
379
434
|
// Cache for positions (fetched by /pos or get_positions tool)
|
|
380
435
|
let cachedPositions = null;
|
|
436
|
+
// ── Heartbeat polling state ───────────────────────────────────────────────
|
|
437
|
+
// Background poll delta endpoint every 60s.
|
|
438
|
+
// If confidence changed ≥ 3%, auto-trigger agent analysis.
|
|
439
|
+
// If agent is busy (isProcessing), queue and deliver after agent finishes.
|
|
440
|
+
let lastPollTimestamp = new Date().toISOString();
|
|
441
|
+
let pendingHeartbeatDelta = null; // queued delta when agent is busy
|
|
442
|
+
let heartbeatPollTimer = null;
|
|
381
443
|
// ── Inline confirmation mechanism ─────────────────────────────────────────
|
|
382
444
|
// Tools can call promptUser() during execution to ask the user a question.
|
|
383
445
|
// This temporarily unlocks the editor, waits for input, then resumes.
|
|
@@ -405,9 +467,9 @@ async function agentCommand(thesisId, opts) {
|
|
|
405
467
|
const mdDefaultStyle = {
|
|
406
468
|
color: (s) => C.zinc400(s),
|
|
407
469
|
};
|
|
408
|
-
// Editor theme
|
|
470
|
+
// Editor theme — use dim zinc borders instead of default green
|
|
409
471
|
const editorTheme = {
|
|
410
|
-
borderColor: (s) =>
|
|
472
|
+
borderColor: (s) => `\x1b[38;2;50;50;55m${s}\x1b[39m`,
|
|
411
473
|
selectList: {
|
|
412
474
|
selectedPrefix: (s) => C.emerald(s),
|
|
413
475
|
selectedText: (s) => C.zinc200(s),
|
|
@@ -417,12 +479,31 @@ async function agentCommand(thesisId, opts) {
|
|
|
417
479
|
},
|
|
418
480
|
};
|
|
419
481
|
// ── Build components ───────────────────────────────────────────────────────
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
482
|
+
const headerBar = new HeaderBar();
|
|
483
|
+
// Fetch positions for header P&L (non-blocking, best-effort)
|
|
484
|
+
let initialPositions = null;
|
|
485
|
+
try {
|
|
486
|
+
initialPositions = await (0, kalshi_js_1.getPositions)();
|
|
487
|
+
if (initialPositions) {
|
|
488
|
+
for (const pos of initialPositions) {
|
|
489
|
+
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
490
|
+
if (livePrice !== null) {
|
|
491
|
+
pos.current_value = livePrice;
|
|
492
|
+
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch { /* positions not available, fine */ }
|
|
498
|
+
headerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
425
499
|
const footerBar = new FooterBar();
|
|
500
|
+
footerBar.modelName = currentModelName;
|
|
501
|
+
footerBar.tradingEnabled = (0, config_js_1.loadConfig)().tradingEnabled || false;
|
|
502
|
+
// Fetch exchange status for footer (non-blocking)
|
|
503
|
+
fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } })
|
|
504
|
+
.then(r => r.json())
|
|
505
|
+
.then(d => { footerBar.exchangeOpen = !!d.exchange_active; footerBar.update(); tui.requestRender(); })
|
|
506
|
+
.catch(() => { });
|
|
426
507
|
const topSpacer = new Spacer(1);
|
|
427
508
|
const bottomSpacer = new Spacer(1);
|
|
428
509
|
const chatContainer = new Container();
|
|
@@ -517,11 +598,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
517
598
|
execute: async (_toolCallId, params) => {
|
|
518
599
|
const ctx = await sfClient.getContext(params.thesisId);
|
|
519
600
|
latestContext = ctx;
|
|
520
|
-
|
|
521
|
-
const conf = typeof ctx.confidence === 'number'
|
|
522
|
-
? Math.round(ctx.confidence * 100)
|
|
523
|
-
: 0;
|
|
524
|
-
headerBar.update(undefined, conf > 0 ? C.zinc200(`${conf}%`) : '', undefined);
|
|
601
|
+
headerBar.setFromContext(ctx, initialPositions || undefined);
|
|
525
602
|
tui.requestRender();
|
|
526
603
|
return {
|
|
527
604
|
content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
|
|
@@ -549,6 +626,26 @@ async function agentCommand(thesisId, opts) {
|
|
|
549
626
|
parameters: thesisIdParam,
|
|
550
627
|
execute: async (_toolCallId, params) => {
|
|
551
628
|
const result = await sfClient.evaluate(params.thesisId);
|
|
629
|
+
// Show confidence change prominently
|
|
630
|
+
if (result.evaluation?.confidenceDelta && Math.abs(result.evaluation.confidenceDelta) >= 0.01) {
|
|
631
|
+
const delta = result.evaluation.confidenceDelta;
|
|
632
|
+
const prev = Math.round((result.evaluation.previousConfidence || 0) * 100);
|
|
633
|
+
const now = Math.round((result.evaluation.newConfidence || 0) * 100);
|
|
634
|
+
const arrow = delta > 0 ? '\u25B2' : '\u25BC';
|
|
635
|
+
const color = delta > 0 ? C.emerald : C.red;
|
|
636
|
+
addSystemText(color(` ${arrow} Confidence ${prev}% \u2192 ${now}% (${delta > 0 ? '+' : ''}${Math.round(delta * 100)})`));
|
|
637
|
+
addSpacer();
|
|
638
|
+
// Update header
|
|
639
|
+
headerBar.updateConfidence(result.evaluation.newConfidence, delta);
|
|
640
|
+
tui.requestRender();
|
|
641
|
+
}
|
|
642
|
+
// Refresh context after eval
|
|
643
|
+
try {
|
|
644
|
+
latestContext = await sfClient.getContext(params.thesisId);
|
|
645
|
+
headerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
646
|
+
tui.requestRender();
|
|
647
|
+
}
|
|
648
|
+
catch { }
|
|
552
649
|
return {
|
|
553
650
|
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
554
651
|
details: {},
|
|
@@ -901,6 +998,81 @@ async function agentCommand(thesisId, opts) {
|
|
|
901
998
|
},
|
|
902
999
|
},
|
|
903
1000
|
];
|
|
1001
|
+
// ── What-if tool (always available) ────────────────────────────────────────
|
|
1002
|
+
tools.push({
|
|
1003
|
+
name: 'what_if',
|
|
1004
|
+
label: 'What-If',
|
|
1005
|
+
description: 'Run a what-if scenario: override causal tree node probabilities and see how edges and confidence change. Zero LLM cost — pure computation. Use when user asks "what if X happens?" or "what if this node drops to Y%?".',
|
|
1006
|
+
parameters: Type.Object({
|
|
1007
|
+
overrides: Type.Array(Type.Object({
|
|
1008
|
+
nodeId: Type.String({ description: 'Causal tree node ID (e.g. n1, n3.1)' }),
|
|
1009
|
+
newProbability: Type.Number({ description: 'New probability 0-1' }),
|
|
1010
|
+
}), { description: 'Node probability overrides' }),
|
|
1011
|
+
}),
|
|
1012
|
+
execute: async (_toolCallId, params) => {
|
|
1013
|
+
// Inline what-if simulation
|
|
1014
|
+
const ctx = latestContext;
|
|
1015
|
+
const allNodes = [];
|
|
1016
|
+
function flatten(nodes) {
|
|
1017
|
+
for (const n of nodes) {
|
|
1018
|
+
allNodes.push(n);
|
|
1019
|
+
if (n.children?.length)
|
|
1020
|
+
flatten(n.children);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const rawNodes = ctx.causalTree?.nodes || [];
|
|
1024
|
+
flatten(rawNodes);
|
|
1025
|
+
const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
|
|
1026
|
+
const overrideMap = new Map(params.overrides.map((o) => [o.nodeId, o.newProbability]));
|
|
1027
|
+
const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
|
|
1028
|
+
const newConf = treeNodes.reduce((s, n) => {
|
|
1029
|
+
const p = overrideMap.get(n.id) ?? n.probability ?? 0;
|
|
1030
|
+
return s + p * (n.importance || 0);
|
|
1031
|
+
}, 0);
|
|
1032
|
+
const nodeScales = new Map();
|
|
1033
|
+
for (const [nid, np] of overrideMap.entries()) {
|
|
1034
|
+
const nd = allNodes.find((n) => n.id === nid);
|
|
1035
|
+
if (nd && nd.probability > 0)
|
|
1036
|
+
nodeScales.set(nid, Math.max(0, Math.min(2, np / nd.probability)));
|
|
1037
|
+
}
|
|
1038
|
+
const edges = (ctx.edges || []).map((edge) => {
|
|
1039
|
+
const relNode = edge.relatedNodeId;
|
|
1040
|
+
let scaleFactor = 1;
|
|
1041
|
+
if (relNode) {
|
|
1042
|
+
const candidates = [relNode, relNode.split('.').slice(0, -1).join('.'), relNode.split('.')[0]].filter(Boolean);
|
|
1043
|
+
for (const cid of candidates) {
|
|
1044
|
+
if (nodeScales.has(cid)) {
|
|
1045
|
+
scaleFactor = nodeScales.get(cid);
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const mkt = edge.marketPrice || 0;
|
|
1051
|
+
const oldTP = edge.thesisPrice || edge.thesisImpliedPrice || mkt;
|
|
1052
|
+
const oldEdge = edge.edge || edge.edgeSize || 0;
|
|
1053
|
+
const newTP = Math.round((mkt + (oldTP - mkt) * scaleFactor) * 100) / 100;
|
|
1054
|
+
const dir = edge.direction || 'yes';
|
|
1055
|
+
const newEdge = Math.round((dir === 'yes' ? newTP - mkt : mkt - newTP) * 100) / 100;
|
|
1056
|
+
return {
|
|
1057
|
+
market: edge.market || edge.marketTitle || edge.marketId,
|
|
1058
|
+
marketPrice: mkt,
|
|
1059
|
+
oldEdge,
|
|
1060
|
+
newEdge,
|
|
1061
|
+
delta: newEdge - oldEdge,
|
|
1062
|
+
signal: Math.abs(newEdge - oldEdge) < 1 ? 'unchanged' : (oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0) ? 'REVERSED' : Math.abs(newEdge) < 2 ? 'GONE' : 'reduced',
|
|
1063
|
+
};
|
|
1064
|
+
}).filter((e) => e.signal !== 'unchanged');
|
|
1065
|
+
const result = {
|
|
1066
|
+
overrides: params.overrides.map((o) => {
|
|
1067
|
+
const node = allNodes.find((n) => n.id === o.nodeId);
|
|
1068
|
+
return { nodeId: o.nodeId, label: node?.label || o.nodeId, oldProb: node?.probability, newProb: o.newProbability };
|
|
1069
|
+
}),
|
|
1070
|
+
confidence: { old: Math.round(oldConf * 100), new: Math.round(newConf * 100), delta: Math.round((newConf - oldConf) * 100) },
|
|
1071
|
+
affectedEdges: edges,
|
|
1072
|
+
};
|
|
1073
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
904
1076
|
// ── Trading tools (conditional on tradingEnabled) ──────────────────────────
|
|
905
1077
|
const config = (0, config_js_1.loadConfig)();
|
|
906
1078
|
if (config.tradingEnabled) {
|
|
@@ -1163,6 +1335,12 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1163
1335
|
isProcessing = false;
|
|
1164
1336
|
persistSession();
|
|
1165
1337
|
flushRender();
|
|
1338
|
+
// Deliver queued heartbeat notification if any
|
|
1339
|
+
if (pendingHeartbeatDelta) {
|
|
1340
|
+
const delta = pendingHeartbeatDelta;
|
|
1341
|
+
pendingHeartbeatDelta = null;
|
|
1342
|
+
handleHeartbeatDelta(delta);
|
|
1343
|
+
}
|
|
1166
1344
|
}
|
|
1167
1345
|
if (event.type === 'tool_execution_start') {
|
|
1168
1346
|
const toolLine = new MutableLine(C.zinc600(` \u26A1 ${event.toolName}...`));
|
|
@@ -1298,7 +1476,8 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1298
1476
|
model = resolveModel(currentModelName);
|
|
1299
1477
|
// Update agent model
|
|
1300
1478
|
agent.setModel(model);
|
|
1301
|
-
|
|
1479
|
+
footerBar.modelName = currentModelName;
|
|
1480
|
+
footerBar.update();
|
|
1302
1481
|
addSystemText(C.emerald(`Model switched to ${currentModelName}`));
|
|
1303
1482
|
addSpacer();
|
|
1304
1483
|
tui.requestRender();
|
|
@@ -1338,13 +1517,9 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1338
1517
|
addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
|
|
1339
1518
|
}
|
|
1340
1519
|
// Update header
|
|
1341
|
-
headerBar.
|
|
1520
|
+
headerBar.setFromContext(newContext, initialPositions || undefined);
|
|
1342
1521
|
chatContainer.clear();
|
|
1343
|
-
|
|
1344
|
-
addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
|
|
1345
|
-
C.zinc200(bold(thText)) + '\n' +
|
|
1346
|
-
C.zinc600(`${newContext.status || 'active'} ${newConf > 0 ? newConf + '%' : ''} ${(newContext.edges || []).length} edges`) + '\n' +
|
|
1347
|
-
C.zinc600('\u2500'.repeat(50)));
|
|
1522
|
+
addSystemText(buildWelcomeDashboard(newContext, initialPositions));
|
|
1348
1523
|
}
|
|
1349
1524
|
catch (err) {
|
|
1350
1525
|
addSystemText(C.red(`Switch failed: ${err.message}`));
|
|
@@ -1703,6 +1878,8 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
1703
1878
|
};
|
|
1704
1879
|
// ── Ctrl+C handler ─────────────────────────────────────────────────────────
|
|
1705
1880
|
function cleanup() {
|
|
1881
|
+
if (heartbeatPollTimer)
|
|
1882
|
+
clearInterval(heartbeatPollTimer);
|
|
1706
1883
|
if (currentLoader)
|
|
1707
1884
|
currentLoader.stop();
|
|
1708
1885
|
persistSession();
|
|
@@ -1721,17 +1898,125 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
1721
1898
|
// Also handle SIGINT
|
|
1722
1899
|
process.on('SIGINT', cleanup);
|
|
1723
1900
|
process.on('SIGTERM', cleanup);
|
|
1901
|
+
// ── Welcome dashboard builder ────────────────────────────────────────────
|
|
1902
|
+
function buildWelcomeDashboard(ctx, positions) {
|
|
1903
|
+
const lines = [];
|
|
1904
|
+
const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
|
|
1905
|
+
const truncated = thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText;
|
|
1906
|
+
const conf = typeof ctx.confidence === 'number'
|
|
1907
|
+
? Math.round(ctx.confidence * 100)
|
|
1908
|
+
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
1909
|
+
const delta = ctx.lastEvaluation?.confidenceDelta
|
|
1910
|
+
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
1911
|
+
: 0;
|
|
1912
|
+
const deltaStr = delta !== 0 ? ` (${delta > 0 ? '+' : ''}${delta})` : '';
|
|
1913
|
+
const evalAge = ctx.lastEvaluation?.evaluatedAt
|
|
1914
|
+
? Math.round((Date.now() - new Date(ctx.lastEvaluation.evaluatedAt).getTime()) / 3600000)
|
|
1915
|
+
: null;
|
|
1916
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
1917
|
+
lines.push(' ' + C.zinc200(bold(truncated)));
|
|
1918
|
+
lines.push(' ' + C.zinc600(`${ctx.status || 'active'} ${conf}%${deltaStr}`) +
|
|
1919
|
+
(evalAge !== null ? C.zinc600(` \u2502 last eval: ${evalAge < 1 ? '<1' : evalAge}h ago`) : ''));
|
|
1920
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
1921
|
+
// Positions section
|
|
1922
|
+
if (positions && positions.length > 0) {
|
|
1923
|
+
lines.push(' ' + C.zinc400(bold('POSITIONS')));
|
|
1924
|
+
let totalPnl = 0;
|
|
1925
|
+
for (const p of positions) {
|
|
1926
|
+
const pnlCents = p.unrealized_pnl || 0;
|
|
1927
|
+
totalPnl += pnlCents;
|
|
1928
|
+
const pnlStr = pnlCents >= 0
|
|
1929
|
+
? C.emerald(`+$${(pnlCents / 100).toFixed(2)}`)
|
|
1930
|
+
: C.red(`-$${(Math.abs(pnlCents) / 100).toFixed(2)}`);
|
|
1931
|
+
const ticker = (p.ticker || '').slice(0, 28).padEnd(28);
|
|
1932
|
+
const qty = String(p.quantity || 0).padStart(5);
|
|
1933
|
+
const side = p.side === 'yes' ? C.emerald('Y') : C.red('N');
|
|
1934
|
+
lines.push(` ${C.zinc400(ticker)} ${qty} ${side} ${pnlStr}`);
|
|
1935
|
+
}
|
|
1936
|
+
const totalStr = totalPnl >= 0
|
|
1937
|
+
? C.emerald(bold(`+$${(totalPnl / 100).toFixed(2)}`))
|
|
1938
|
+
: C.red(bold(`-$${(Math.abs(totalPnl) / 100).toFixed(2)}`));
|
|
1939
|
+
lines.push(` ${''.padEnd(28)} ${C.zinc600('Total')} ${totalStr}`);
|
|
1940
|
+
}
|
|
1941
|
+
// Top edges section
|
|
1942
|
+
const edges = ctx.edges || [];
|
|
1943
|
+
if (edges.length > 0) {
|
|
1944
|
+
const sorted = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0)).slice(0, 5);
|
|
1945
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
1946
|
+
lines.push(' ' + C.zinc400(bold('TOP EDGES')) + C.zinc600(' mkt edge liq'));
|
|
1947
|
+
for (const e of sorted) {
|
|
1948
|
+
const name = (e.market || e.marketTitle || e.marketId || '').slice(0, 30).padEnd(30);
|
|
1949
|
+
const mkt = String(Math.round(e.marketPrice || 0)).padStart(3) + '\u00A2';
|
|
1950
|
+
const edge = e.edge || e.edgeSize || 0;
|
|
1951
|
+
const edgeStr = (edge > 0 ? '+' : '') + Math.round(edge);
|
|
1952
|
+
const liq = e.orderbook?.liquidityScore || (e.venue === 'polymarket' ? '-' : '?');
|
|
1953
|
+
const edgeColor = Math.abs(edge) >= 15 ? C.emerald : Math.abs(edge) >= 8 ? C.amber : C.zinc400;
|
|
1954
|
+
lines.push(` ${C.zinc400(name)} ${C.zinc400(mkt)} ${edgeColor(edgeStr.padStart(4))} ${C.zinc600(liq)}`);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
1958
|
+
return lines.join('\n');
|
|
1959
|
+
}
|
|
1724
1960
|
// ── Show initial welcome ───────────────────────────────────────────────────
|
|
1725
|
-
const thesisText = latestContext.thesis || latestContext.rawThesis || 'N/A';
|
|
1726
|
-
const truncatedThesis = thesisText.length > 120 ? thesisText.slice(0, 120) + '...' : thesisText;
|
|
1727
1961
|
const sessionStatus = sessionRestored
|
|
1728
|
-
? C.zinc600(`
|
|
1729
|
-
: C.zinc600('
|
|
1730
|
-
addSystemText(
|
|
1731
|
-
|
|
1732
|
-
C.zinc600(`${latestContext.status || 'active'} ${confidencePct > 0 ? confidencePct + '%' : ''} ${(latestContext.edges || []).length} edges`) + sessionStatus + '\n' +
|
|
1733
|
-
C.zinc600('\u2500'.repeat(50)));
|
|
1962
|
+
? C.zinc600(`resumed (${agent.state.messages.length} messages)`)
|
|
1963
|
+
: C.zinc600('new session');
|
|
1964
|
+
addSystemText(buildWelcomeDashboard(latestContext, initialPositions));
|
|
1965
|
+
addSystemText(' ' + sessionStatus);
|
|
1734
1966
|
addSpacer();
|
|
1967
|
+
// ── Heartbeat delta handler ───────────────────────────────────────────────
|
|
1968
|
+
const HEARTBEAT_CONFIDENCE_THRESHOLD = 0.03; // 3%
|
|
1969
|
+
function handleHeartbeatDelta(delta) {
|
|
1970
|
+
const absDelta = Math.abs(delta.confidenceDelta || 0);
|
|
1971
|
+
const confPct = Math.round((delta.confidence || 0) * 100);
|
|
1972
|
+
const deltaPct = Math.round((delta.confidenceDelta || 0) * 100);
|
|
1973
|
+
const sign = deltaPct > 0 ? '+' : '';
|
|
1974
|
+
if (absDelta >= HEARTBEAT_CONFIDENCE_THRESHOLD) {
|
|
1975
|
+
// Big change → auto-trigger agent analysis
|
|
1976
|
+
const arrow = deltaPct > 0 ? '\u25B2' : '\u25BC';
|
|
1977
|
+
const color = deltaPct > 0 ? C.emerald : C.red;
|
|
1978
|
+
addSystemText(color(` ${arrow} Heartbeat: confidence ${sign}${deltaPct}% → ${confPct}%`));
|
|
1979
|
+
if (delta.latestSummary) {
|
|
1980
|
+
addSystemText(C.zinc400(` ${delta.latestSummary.slice(0, 100)}`));
|
|
1981
|
+
}
|
|
1982
|
+
addSpacer();
|
|
1983
|
+
// Update header
|
|
1984
|
+
headerBar.setFromContext({ ...latestContext, confidence: delta.confidence, lastEvaluation: { confidenceDelta: delta.confidenceDelta } }, initialPositions || undefined);
|
|
1985
|
+
tui.requestRender();
|
|
1986
|
+
// Auto-trigger agent
|
|
1987
|
+
isProcessing = true;
|
|
1988
|
+
const prompt = `[HEARTBEAT ALERT] Confidence just changed ${sign}${deltaPct}% to ${confPct}%. ${delta.evaluationCount} evaluation(s) since last check. Latest: "${(delta.latestSummary || '').slice(0, 150)}". Briefly analyze what happened and whether any action is needed. Be concise.`;
|
|
1989
|
+
agent.prompt(prompt).catch((err) => {
|
|
1990
|
+
addSystemText(C.red(`Error: ${err.message}`));
|
|
1991
|
+
isProcessing = false;
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
else if (absDelta > 0) {
|
|
1995
|
+
// Small change → silent notification line only
|
|
1996
|
+
addSystemText(C.zinc600(` \u2500 heartbeat: ${confPct}% (${sign}${deltaPct}%) \u2014 ${delta.evaluationCount || 0} eval(s)`));
|
|
1997
|
+
tui.requestRender();
|
|
1998
|
+
}
|
|
1999
|
+
// absDelta === 0: truly nothing changed, stay silent
|
|
2000
|
+
}
|
|
2001
|
+
// ── Start heartbeat polling ───────────────────────────────────────────────
|
|
2002
|
+
heartbeatPollTimer = setInterval(async () => {
|
|
2003
|
+
try {
|
|
2004
|
+
const delta = await sfClient.getChanges(resolvedThesisId, lastPollTimestamp);
|
|
2005
|
+
lastPollTimestamp = new Date().toISOString();
|
|
2006
|
+
if (!delta.changed)
|
|
2007
|
+
return;
|
|
2008
|
+
if (isProcessing || pendingPrompt) {
|
|
2009
|
+
// Agent is busy — queue for delivery after agent_end
|
|
2010
|
+
pendingHeartbeatDelta = delta;
|
|
2011
|
+
}
|
|
2012
|
+
else {
|
|
2013
|
+
handleHeartbeatDelta(delta);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
catch {
|
|
2017
|
+
// Silent — don't spam errors from background polling
|
|
2018
|
+
}
|
|
2019
|
+
}, 60_000); // every 60 seconds
|
|
1735
2020
|
// ── Start TUI ──────────────────────────────────────────────────────────────
|
|
1736
2021
|
tui.start();
|
|
1737
2022
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sf feed — Evaluation history stream.
|
|
3
|
+
*
|
|
4
|
+
* Shows what the heartbeat engine has been thinking.
|
|
5
|
+
* One line per evaluation cycle, newest first.
|
|
6
|
+
*/
|
|
7
|
+
export declare function feedCommand(opts: {
|
|
8
|
+
hours?: string;
|
|
9
|
+
thesis?: string;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
apiUrl?: string;
|
|
13
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* sf feed — Evaluation history stream.
|
|
4
|
+
*
|
|
5
|
+
* Shows what the heartbeat engine has been thinking.
|
|
6
|
+
* One line per evaluation cycle, newest first.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.feedCommand = feedCommand;
|
|
10
|
+
const client_js_1 = require("../client.js");
|
|
11
|
+
const utils_js_1 = require("../utils.js");
|
|
12
|
+
async function feedCommand(opts) {
|
|
13
|
+
const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
|
|
14
|
+
const hours = parseInt(opts.hours || '24');
|
|
15
|
+
const data = await client.getFeed(hours, 200);
|
|
16
|
+
let feed = data.feed || [];
|
|
17
|
+
if (feed.length === 0) {
|
|
18
|
+
console.log(`${utils_js_1.c.dim}No evaluations in the last ${hours} hours.${utils_js_1.c.reset}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Filter by thesis if specified
|
|
22
|
+
if (opts.thesis) {
|
|
23
|
+
feed = feed.filter((e) => e.thesisId.startsWith(opts.thesis) || e.thesisShortId === opts.thesis);
|
|
24
|
+
if (feed.length === 0) {
|
|
25
|
+
console.log(`${utils_js_1.c.dim}No evaluations for ${opts.thesis} in the last ${hours} hours.${utils_js_1.c.reset}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
console.log(JSON.stringify(feed, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Render feed
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(`${utils_js_1.c.bold}${utils_js_1.c.cyan}Evaluation Feed${utils_js_1.c.reset}${utils_js_1.c.dim} — last ${hours}h, ${feed.length} cycles${utils_js_1.c.reset}`);
|
|
36
|
+
console.log(`${utils_js_1.c.dim}${'─'.repeat(75)}${utils_js_1.c.reset}`);
|
|
37
|
+
for (const entry of feed) {
|
|
38
|
+
const time = new Date(entry.evaluatedAt);
|
|
39
|
+
const timeStr = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
40
|
+
// Confidence + delta
|
|
41
|
+
const conf = Math.round(entry.confidence * 100);
|
|
42
|
+
const delta = Math.round(entry.delta * 100);
|
|
43
|
+
let deltaStr;
|
|
44
|
+
if (delta > 0) {
|
|
45
|
+
deltaStr = `${utils_js_1.c.green}+${delta}%${utils_js_1.c.reset}`;
|
|
46
|
+
}
|
|
47
|
+
else if (delta < 0) {
|
|
48
|
+
deltaStr = `${utils_js_1.c.red}${delta}%${utils_js_1.c.reset}`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
deltaStr = `${utils_js_1.c.dim}0%${utils_js_1.c.reset}`;
|
|
52
|
+
}
|
|
53
|
+
// Thesis ID (short)
|
|
54
|
+
const id = entry.thesisShortId || entry.thesisId?.slice(0, 8) || '?';
|
|
55
|
+
// Summary — truncate to fit one line
|
|
56
|
+
const summary = (entry.summary || 'No summary').replace(/\n/g, ' ').slice(0, 80);
|
|
57
|
+
// Node changes
|
|
58
|
+
const nodes = entry.updatedNodes || [];
|
|
59
|
+
const nodeStr = nodes.length > 0
|
|
60
|
+
? nodes.slice(0, 3).map((n) => `${n.nodeId}→${Math.round((n.newProb || 0) * 100)}%`).join(', ')
|
|
61
|
+
: '';
|
|
62
|
+
// Format line
|
|
63
|
+
console.log(`${utils_js_1.c.dim}[${timeStr}]${utils_js_1.c.reset} ` +
|
|
64
|
+
`${utils_js_1.c.cyan}${id}${utils_js_1.c.reset} ` +
|
|
65
|
+
`${conf}% (${deltaStr}) ` +
|
|
66
|
+
`${utils_js_1.c.dim}${summary}${utils_js_1.c.reset}`);
|
|
67
|
+
if (nodeStr) {
|
|
68
|
+
console.log(`${' '.repeat(9)} ${utils_js_1.c.dim}nodes: ${nodeStr}${utils_js_1.c.reset}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
console.log(`${utils_js_1.c.dim}${'─'.repeat(75)}${utils_js_1.c.reset}`);
|
|
72
|
+
console.log();
|
|
73
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sf whatif — What-if scenario analysis.
|
|
3
|
+
*
|
|
4
|
+
* Pure computation, zero LLM cost. Answers:
|
|
5
|
+
* "If node X drops to 10%, what happens to my edges and positions?"
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* sf whatif f582bf76 --set "n1=0.1"
|
|
9
|
+
* sf whatif f582bf76 --set "n1=0.1" --set "n3.1=0.2"
|
|
10
|
+
* sf whatif f582bf76 --set "n1=0.1" --json
|
|
11
|
+
*/
|
|
12
|
+
export declare function whatifCommand(thesisId: string, opts: {
|
|
13
|
+
set?: string[];
|
|
14
|
+
json?: boolean;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
apiUrl?: string;
|
|
17
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* sf whatif — What-if scenario analysis.
|
|
4
|
+
*
|
|
5
|
+
* Pure computation, zero LLM cost. Answers:
|
|
6
|
+
* "If node X drops to 10%, what happens to my edges and positions?"
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* sf whatif f582bf76 --set "n1=0.1"
|
|
10
|
+
* sf whatif f582bf76 --set "n1=0.1" --set "n3.1=0.2"
|
|
11
|
+
* sf whatif f582bf76 --set "n1=0.1" --json
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.whatifCommand = whatifCommand;
|
|
15
|
+
const client_js_1 = require("../client.js");
|
|
16
|
+
const kalshi_js_1 = require("../kalshi.js");
|
|
17
|
+
const utils_js_1 = require("../utils.js");
|
|
18
|
+
// Inline what-if simulation (mirrors server-side logic, zero dependency)
|
|
19
|
+
function simulateWhatIf(ctx, overrides) {
|
|
20
|
+
const allNodes = [];
|
|
21
|
+
function flatten(nodes) {
|
|
22
|
+
for (const n of nodes) {
|
|
23
|
+
allNodes.push(n);
|
|
24
|
+
if (n.children?.length)
|
|
25
|
+
flatten(n.children);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const rawNodes = ctx.causalTree?.nodes || [];
|
|
29
|
+
flatten(rawNodes);
|
|
30
|
+
// Top-level nodes only (depth=0 or no depth field + no dot in id)
|
|
31
|
+
const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
|
|
32
|
+
const overrideMap = new Map(overrides.map(o => [o.nodeId, o.newProbability]));
|
|
33
|
+
const overrideDetails = overrides.map(o => {
|
|
34
|
+
const node = allNodes.find((n) => n.id === o.nodeId);
|
|
35
|
+
return {
|
|
36
|
+
nodeId: o.nodeId,
|
|
37
|
+
oldProb: node?.probability ?? 0,
|
|
38
|
+
newProb: o.newProbability,
|
|
39
|
+
label: node?.label || o.nodeId,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
// Confidence (only top-level nodes)
|
|
43
|
+
const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
|
|
44
|
+
const newConf = treeNodes.reduce((s, n) => {
|
|
45
|
+
const p = overrideMap.get(n.id) ?? n.probability ?? 0;
|
|
46
|
+
return s + p * (n.importance || 0);
|
|
47
|
+
}, 0);
|
|
48
|
+
// Per-node override ratios — only scale edges directly related to overridden nodes.
|
|
49
|
+
// No global scale: edges unrelated to any override stay unchanged.
|
|
50
|
+
// User must explicitly override each node they think is affected.
|
|
51
|
+
const nodeScales = new Map();
|
|
52
|
+
for (const [nodeId, newProb] of overrideMap.entries()) {
|
|
53
|
+
const node = allNodes.find((n) => n.id === nodeId);
|
|
54
|
+
if (node && node.probability > 0) {
|
|
55
|
+
nodeScales.set(nodeId, Math.max(0, Math.min(2, newProb / node.probability)));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Edges
|
|
59
|
+
const edges = (ctx.edges || []).map((edge) => {
|
|
60
|
+
const relNode = edge.relatedNodeId;
|
|
61
|
+
let scaleFactor = 1; // default: no change
|
|
62
|
+
// Only scale if edge's related node (or its ancestor) was overridden
|
|
63
|
+
if (relNode) {
|
|
64
|
+
const candidates = [relNode, relNode.split('.').slice(0, -1).join('.'), relNode.split('.')[0]].filter(Boolean);
|
|
65
|
+
for (const cid of candidates) {
|
|
66
|
+
if (nodeScales.has(cid)) {
|
|
67
|
+
scaleFactor = nodeScales.get(cid);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const mkt = edge.marketPrice || 0;
|
|
73
|
+
const oldTP = edge.thesisPrice || edge.thesisImpliedPrice || mkt;
|
|
74
|
+
const oldEdge = edge.edge || edge.edgeSize || 0;
|
|
75
|
+
const premium = oldTP - mkt;
|
|
76
|
+
const newTP = Math.round((mkt + premium * scaleFactor) * 100) / 100;
|
|
77
|
+
const dir = edge.direction || 'yes';
|
|
78
|
+
const newEdge = Math.round((dir === 'yes' ? newTP - mkt : mkt - newTP) * 100) / 100;
|
|
79
|
+
const delta = Math.round((newEdge - oldEdge) * 100) / 100;
|
|
80
|
+
let signal = 'unchanged';
|
|
81
|
+
if (Math.abs(delta) < 1)
|
|
82
|
+
signal = 'unchanged';
|
|
83
|
+
else if ((oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0))
|
|
84
|
+
signal = 'reversed';
|
|
85
|
+
else if (Math.abs(newEdge) < 2)
|
|
86
|
+
signal = 'gone';
|
|
87
|
+
else if (Math.abs(newEdge) < Math.abs(oldEdge))
|
|
88
|
+
signal = 'reduced';
|
|
89
|
+
return {
|
|
90
|
+
marketId: edge.marketId,
|
|
91
|
+
market: edge.market || edge.marketTitle || edge.marketId,
|
|
92
|
+
venue: edge.venue,
|
|
93
|
+
direction: dir,
|
|
94
|
+
marketPrice: mkt,
|
|
95
|
+
oldThesisPrice: oldTP,
|
|
96
|
+
newThesisPrice: newTP,
|
|
97
|
+
oldEdge,
|
|
98
|
+
newEdge,
|
|
99
|
+
delta,
|
|
100
|
+
signal,
|
|
101
|
+
relatedNodeId: relNode,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
edges.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
|
|
105
|
+
return {
|
|
106
|
+
overrides: overrideDetails,
|
|
107
|
+
oldConfidence: oldConf,
|
|
108
|
+
newConfidence: newConf,
|
|
109
|
+
confidenceDelta: Math.round((newConf - oldConf) * 100) / 100,
|
|
110
|
+
edges,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function whatifCommand(thesisId, opts) {
|
|
114
|
+
if (!opts.set || opts.set.length === 0) {
|
|
115
|
+
throw new Error('Usage: sf whatif <thesisId> --set "n1=0.1" [--set "n3=0.5"]');
|
|
116
|
+
}
|
|
117
|
+
// Parse overrides
|
|
118
|
+
const overrides = opts.set.map(s => {
|
|
119
|
+
const [nodeId, valStr] = s.split('=');
|
|
120
|
+
if (!nodeId || !valStr)
|
|
121
|
+
throw new Error(`Invalid override: "${s}". Format: nodeId=probability`);
|
|
122
|
+
const prob = parseFloat(valStr);
|
|
123
|
+
if (isNaN(prob) || prob < 0 || prob > 1)
|
|
124
|
+
throw new Error(`Invalid probability: "${valStr}". Must be 0-1.`);
|
|
125
|
+
return { nodeId: nodeId.trim(), newProbability: prob };
|
|
126
|
+
});
|
|
127
|
+
const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
|
|
128
|
+
const ctx = await client.getContext(thesisId);
|
|
129
|
+
const result = simulateWhatIf(ctx, overrides);
|
|
130
|
+
if (opts.json) {
|
|
131
|
+
console.log(JSON.stringify(result, null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Render
|
|
135
|
+
console.log();
|
|
136
|
+
console.log(`${utils_js_1.c.bold}${utils_js_1.c.cyan}WHAT-IF Scenario${utils_js_1.c.reset}`);
|
|
137
|
+
console.log(`${utils_js_1.c.dim}${'─'.repeat(65)}${utils_js_1.c.reset}`);
|
|
138
|
+
// Overrides
|
|
139
|
+
for (const o of result.overrides) {
|
|
140
|
+
const oldPct = Math.round(o.oldProb * 100);
|
|
141
|
+
const newPct = Math.round(o.newProb * 100);
|
|
142
|
+
const arrow = newPct > oldPct ? utils_js_1.c.green + '↑' + utils_js_1.c.reset : utils_js_1.c.red + '↓' + utils_js_1.c.reset;
|
|
143
|
+
console.log(` ${utils_js_1.c.cyan}${o.nodeId}${utils_js_1.c.reset} ${o.label.slice(0, 40)}`);
|
|
144
|
+
console.log(` ${oldPct}% ${arrow} ${utils_js_1.c.bold}${newPct}%${utils_js_1.c.reset}`);
|
|
145
|
+
}
|
|
146
|
+
// Confidence
|
|
147
|
+
const oldPct = Math.round(result.oldConfidence * 100);
|
|
148
|
+
const newPct = Math.round(result.newConfidence * 100);
|
|
149
|
+
const deltaSign = result.confidenceDelta > 0 ? '+' : '';
|
|
150
|
+
const confColor = result.confidenceDelta >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
|
|
151
|
+
console.log();
|
|
152
|
+
console.log(` Confidence: ${oldPct}% → ${confColor}${utils_js_1.c.bold}${newPct}%${utils_js_1.c.reset} (${confColor}${deltaSign}${Math.round(result.confidenceDelta * 100)}${utils_js_1.c.reset})`);
|
|
153
|
+
console.log();
|
|
154
|
+
// Edges
|
|
155
|
+
const affected = result.edges.filter((e) => e.signal !== 'unchanged');
|
|
156
|
+
if (affected.length === 0) {
|
|
157
|
+
console.log(` ${utils_js_1.c.dim}No edges affected.${utils_js_1.c.reset}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.log(` ${utils_js_1.c.bold}Edges Affected${utils_js_1.c.reset}`);
|
|
161
|
+
console.log(` ${'Market'.padEnd(35)} ${'Now'.padEnd(6)} ${'Edge'.padEnd(8)} ${'→'.padEnd(3)} ${'New Edge'.padEnd(8)} Signal`);
|
|
162
|
+
console.log(` ${utils_js_1.c.dim}${'─'.repeat(65)}${utils_js_1.c.reset}`);
|
|
163
|
+
for (const e of affected) {
|
|
164
|
+
const name = (e.market || e.marketId).slice(0, 33).padEnd(35);
|
|
165
|
+
const mkt = `${Math.round(e.marketPrice)}¢`.padEnd(6);
|
|
166
|
+
const oldE = `${e.oldEdge > 0 ? '+' : ''}${Math.round(e.oldEdge)}`.padEnd(8);
|
|
167
|
+
const newE = `${e.newEdge > 0 ? '+' : ''}${Math.round(e.newEdge)}`.padEnd(8);
|
|
168
|
+
let signalStr;
|
|
169
|
+
switch (e.signal) {
|
|
170
|
+
case 'reversed':
|
|
171
|
+
signalStr = `${utils_js_1.c.red}${utils_js_1.c.bold}REVERSED${utils_js_1.c.reset}`;
|
|
172
|
+
break;
|
|
173
|
+
case 'gone':
|
|
174
|
+
signalStr = `${utils_js_1.c.red}GONE${utils_js_1.c.reset}`;
|
|
175
|
+
break;
|
|
176
|
+
case 'reduced':
|
|
177
|
+
signalStr = `${utils_js_1.c.dim}reduced${utils_js_1.c.reset}`;
|
|
178
|
+
break;
|
|
179
|
+
default: signalStr = `${utils_js_1.c.dim}-${utils_js_1.c.reset}`;
|
|
180
|
+
}
|
|
181
|
+
console.log(` ${utils_js_1.c.dim}${name}${utils_js_1.c.reset} ${mkt} ${oldE} → ${newE} ${signalStr}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Position risk (if positions available)
|
|
185
|
+
try {
|
|
186
|
+
const positions = await (0, kalshi_js_1.getPositions)();
|
|
187
|
+
if (positions && positions.length > 0) {
|
|
188
|
+
const edgeMap = new Map(result.edges.map((e) => [e.marketId, e]));
|
|
189
|
+
const atRisk = positions.filter((p) => {
|
|
190
|
+
const e = edgeMap.get(p.ticker);
|
|
191
|
+
return e && (e.signal === 'reversed' || e.signal === 'gone');
|
|
192
|
+
});
|
|
193
|
+
if (atRisk.length > 0) {
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(` ${utils_js_1.c.red}${utils_js_1.c.bold}⚠ Positions at Risk${utils_js_1.c.reset}`);
|
|
196
|
+
for (const p of atRisk) {
|
|
197
|
+
const e = edgeMap.get(p.ticker);
|
|
198
|
+
const livePrice = await (0, kalshi_js_1.getMarketPrice)(p.ticker);
|
|
199
|
+
const currentPnl = livePrice !== null
|
|
200
|
+
? ((livePrice - p.average_price_paid) * p.quantity / 100).toFixed(2)
|
|
201
|
+
: '?';
|
|
202
|
+
console.log(` ${utils_js_1.c.red}${p.ticker}${utils_js_1.c.reset} ${p.quantity} ${p.side} P&L $${currentPnl} edge ${e.oldEdge > 0 ? '+' : ''}${Math.round(e.oldEdge)} → ${utils_js_1.c.red}${e.newEdge > 0 ? '+' : ''}${Math.round(e.newEdge)}${utils_js_1.c.reset}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch { /* positions not available, skip */ }
|
|
208
|
+
console.log();
|
|
209
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,8 @@ const settlements_js_1 = require("./commands/settlements.js");
|
|
|
42
42
|
const balance_js_1 = require("./commands/balance.js");
|
|
43
43
|
const orders_js_1 = require("./commands/orders.js");
|
|
44
44
|
const fills_js_1 = require("./commands/fills.js");
|
|
45
|
+
const feed_js_1 = require("./commands/feed.js");
|
|
46
|
+
const whatif_js_1 = require("./commands/whatif.js");
|
|
45
47
|
const trade_js_1 = require("./commands/trade.js");
|
|
46
48
|
const cancel_js_1 = require("./commands/cancel.js");
|
|
47
49
|
const schedule_js_1 = require("./commands/schedule.js");
|
|
@@ -296,6 +298,27 @@ program
|
|
|
296
298
|
.action(async (opts) => {
|
|
297
299
|
await run(() => (0, fills_js_1.fillsCommand)(opts));
|
|
298
300
|
});
|
|
301
|
+
// ── sf feed ──────────────────────────────────────────────────────────────────
|
|
302
|
+
program
|
|
303
|
+
.command('feed')
|
|
304
|
+
.description('Evaluation history stream — what the heartbeat engine has been thinking')
|
|
305
|
+
.option('--hours <n>', 'Hours to look back (default 24)', '24')
|
|
306
|
+
.option('--thesis <id>', 'Filter by thesis')
|
|
307
|
+
.option('--json', 'JSON output')
|
|
308
|
+
.action(async (opts, cmd) => {
|
|
309
|
+
const g = cmd.optsWithGlobals();
|
|
310
|
+
await run(() => (0, feed_js_1.feedCommand)({ ...opts, apiKey: g.apiKey, apiUrl: g.apiUrl }));
|
|
311
|
+
});
|
|
312
|
+
// ── sf whatif <thesisId> ──────────────────────────────────────────────────────
|
|
313
|
+
program
|
|
314
|
+
.command('whatif <thesisId>')
|
|
315
|
+
.description('What-if scenario — "if node X drops to 10%, what happens to my edges?"')
|
|
316
|
+
.option('--set <override>', 'Node override: nodeId=probability (0-1). Repeatable.', (val, prev) => [...prev, val], [])
|
|
317
|
+
.option('--json', 'JSON output')
|
|
318
|
+
.action(async (thesisId, opts, cmd) => {
|
|
319
|
+
const g = cmd.optsWithGlobals();
|
|
320
|
+
await run(() => (0, whatif_js_1.whatifCommand)(thesisId, { set: opts.set, json: opts.json, apiKey: g.apiKey, apiUrl: g.apiUrl }));
|
|
321
|
+
});
|
|
299
322
|
// ── sf schedule ──────────────────────────────────────────────────────────────
|
|
300
323
|
program
|
|
301
324
|
.command('schedule')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfunctions/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "Prediction market intelligence CLI. Causal thesis model, 24/7 Kalshi/Polymarket scan, live orderbook, edge detection. Interactive agent mode with tool calling.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sf": "./dist/index.js"
|