dexterm-cli 0.1.4 → 0.1.6
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/packages/agents/src/index.js +2 -0
- package/dist/packages/agents/src/spectator.js +54 -0
- package/dist/packages/agents/src/story.js +523 -0
- package/dist/packages/agents/src/tiers.js +1 -0
- package/dist/packages/agents/src/tools.js +1 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +1 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +15 -8
- package/dist/packages/cli/src/agent-cmd.js +6 -4
- package/dist/packages/cli/src/agent-paper.js +60 -0
- package/dist/packages/cli/src/agent-spectator.js +17 -1
- package/dist/packages/cli/src/analyst/backend.js +206 -0
- package/dist/packages/cli/src/analyst/codex.js +301 -0
- package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
- package/dist/packages/cli/src/analyst/session.js +21 -3
- package/dist/packages/cli/src/app-help-gen.js +21 -12
- package/dist/packages/cli/src/backtest-cmd.js +13 -2
- package/dist/packages/cli/src/display.js +55 -9
- package/dist/packages/cli/src/doctor.js +58 -2
- package/dist/packages/cli/src/live/dev-history.js +115 -0
- package/dist/packages/cli/src/live/index.js +5 -1
- package/dist/packages/cli/src/live/newpairs.js +12 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
- package/dist/packages/cli/src/live/screens/token-live.js +51 -11
- package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
- package/dist/packages/cli/src/live/token.js +1 -0
- package/dist/packages/cli/src/live/trade.js +22 -0
- package/dist/packages/cli/src/main.js +267 -19
- package/dist/packages/cli/src/mcp-follow.js +137 -10
- package/dist/packages/cli/src/plan-cmd.js +15 -5
- package/dist/packages/cli/src/popup.js +17 -1
- package/dist/packages/cli/src/record-cmd.js +2 -0
- package/dist/packages/cli/src/rules.js +25 -2
- package/dist/packages/cli/src/scan/ui.js +7 -1
- package/dist/packages/cli/src/screens/agent-hud.js +741 -0
- package/dist/packages/cli/src/screens/analyst-session.js +9 -3
- package/dist/packages/cli/src/screens/analyst.js +4 -3
- package/dist/packages/cli/src/screens/brain.js +608 -0
- package/dist/packages/cli/src/screens/newpairs.js +1 -1
- package/dist/packages/cli/src/screens/pnl-chip.js +143 -0
- package/dist/packages/cli/src/screens/screener.js +1 -0
- package/dist/packages/cli/src/screens/token.js +16 -7
- package/dist/packages/cli/src/screens/trending.js +1 -1
- package/dist/packages/cli/src/state.js +69 -5
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +2 -2
- package/dist/packages/custom/src/schema.js +13 -3
- package/dist/packages/data/src/fixtures.js +28 -6
- package/dist/packages/data/src/index.js +5 -0
- package/dist/packages/data/src/larp/score.js +220 -0
- package/dist/packages/data/src/larp/x-account.js +316 -0
- package/dist/packages/data/src/sources/jupiter.js +1 -0
- package/dist/packages/data/src/trade/agent/paper-desk.js +401 -0
- package/dist/packages/data/src/trade/agent/rendezvous.js +161 -0
- package/dist/packages/data/src/trade/plans/engine.js +7 -0
- package/dist/packages/data/src/trade/plans/mandate.js +28 -15
- package/dist/packages/data/src/trade/plans/service.js +1 -1
- package/dist/packages/mcp/assets/kb/app-help/agent-paper-trade.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/dev-history.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +5 -1
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +4 -1
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +4 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +2 -1
- package/dist/packages/mcp/assets/kb/app-help/use-codex-analyst.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +51 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-directory-metadata-prepared-not-submitted.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-install-in-one-command.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-codex-instead-of-claude.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-or-linux-npm.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-rules-engine.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-analyst-s-launch.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +13 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +1 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +60 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +0 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +4 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +1 -9
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-how-well-it-predicts-measured-held-out.md +46 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-agent-hud-spectator-v2.md +58 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +46 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +2 -0
- package/dist/packages/mcp/src/index.js +5 -2
- package/dist/packages/mcp/src/paper-agent.js +93 -0
- package/dist/packages/mcp/src/server.js +354 -19
- package/dist/packages/mcp/src/spectator-sink.js +158 -4
- package/dist/packages/mcp/src/user-content.js +1 -0
- package/dist/packages/packs/src/index.js +1 -0
- package/dist/packages/packs/src/larp-score-backtest.js +119 -0
- package/dist/packages/popup/src/pages/chart-lib.js +36 -15
- package/dist/packages/popup/src/pages/chart.css +9 -0
- package/dist/packages/popup/src/pages/chart.html +4 -1
- package/dist/packages/popup/src/pages/chart.js +34 -5
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +1 -0
- package/dist/packages/popup/src/pages/spectator.js +69 -0
- package/dist/packages/popup/src/protocol.js +2 -0
- package/dist/packages/popup/src/server.js +5 -1
- package/dist/packages/recorder/src/dev-history.js +133 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +23 -0
- package/dist/packages/recorder/src/schema.js +11 -0
- package/dist/packages/tui/src/canvas.js +7 -0
- package/dist/packages/tui/src/fmt.js +53 -0
- package/dist/packages/tui/src/keymap.js +4 -0
- package/dist/packages/tui/src/widgets.js +9 -4
- package/package.json +1 -1
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
// The record carries NOTHING an agent could act on — it only describes a read/navigation call that
|
|
8
8
|
// already completed. The file is local and token-marked; raw addresses live only on this loopback channel
|
|
9
9
|
// and are redacted before anything leaves the machine.
|
|
10
|
-
import { appendFileSync, readFileSync, statSync } from 'node:fs';
|
|
10
|
+
import { appendFileSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
/** The UI-action tools that move the screen (so follow mode can visibly perform them). */
|
|
14
|
-
const NAV_TOOLS = new Set(['open_token', 'show_in_screener', 'switch_feed', 'add_to_watchlist', 'open_ticket']);
|
|
14
|
+
const NAV_TOOLS = new Set(['open_token', 'show_in_screener', 'switch_feed', 'switch_screen', 'add_to_watchlist', 'open_ticket']);
|
|
15
|
+
/**
|
|
16
|
+
* task-1443: the view-only UI tools an EXTERNAL agent may use to move a terminal that follows it over MCP. Nothing
|
|
17
|
+
* here changes user data or settings (no watchlist add, no ticket, no theme/workspace): the screen only moves.
|
|
18
|
+
*/
|
|
19
|
+
export const FOLLOW_NAV_ACTIONS = ['open_token', 'show_in_screener', 'switch_feed', 'switch_screen'];
|
|
15
20
|
function navFor(tool, args) {
|
|
16
21
|
const s = (v) => (typeof v === 'string' ? v : undefined);
|
|
17
22
|
if (tool === 'open_token')
|
|
@@ -19,7 +24,10 @@ function navFor(tool, args) {
|
|
|
19
24
|
if (tool === 'switch_feed')
|
|
20
25
|
return { screen: 'screener', feed: s(args.feed) };
|
|
21
26
|
if (tool === 'show_in_screener')
|
|
22
|
-
return { screen: 'screener', filter: s(args.filter), highlight: s(args.highlight) };
|
|
27
|
+
return { screen: 'screener', feed: s(args.preset), filter: s(args.filter), highlight: s(args.highlight) };
|
|
28
|
+
// task-1443: the task-1413 view-only screen switch moves a following terminal too (e.g. to positions).
|
|
29
|
+
if (tool === 'switch_screen')
|
|
30
|
+
return s(args.screen) ? { screen: s(args.screen) } : null;
|
|
23
31
|
if (tool === 'add_to_watchlist')
|
|
24
32
|
return { screen: 'watchlist', mint: s(args.mint) };
|
|
25
33
|
if (tool === 'open_ticket')
|
|
@@ -46,6 +54,29 @@ function shortMint(args) {
|
|
|
46
54
|
const m = typeof args.mint === 'string' ? args.mint : typeof args.symbol === 'string' ? args.symbol : '';
|
|
47
55
|
return m ? ` ${m.length > 10 ? m.slice(0, 6) + '…' : m}` : '';
|
|
48
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* task-1443: a readable, PAPER-labelled summary for an agent paper-trading call, so the activity panel says what
|
|
59
|
+
* the agent did to the simulated ledger (side, size, symbol), not just the tool name. null for any other tool.
|
|
60
|
+
*/
|
|
61
|
+
export function paperSummary(tool, args, result) {
|
|
62
|
+
if (!tool.startsWith('paper_'))
|
|
63
|
+
return null;
|
|
64
|
+
const s = (v) => (typeof v === 'string' ? v.slice(0, 16) : '');
|
|
65
|
+
const t = (result.ticket ?? {});
|
|
66
|
+
const f = (result.fill ?? {});
|
|
67
|
+
switch (tool) {
|
|
68
|
+
case 'paper_open_ticket': return `PAPER open ${s(args.side)} ticket ${s(args.symbol)}`.trim();
|
|
69
|
+
case 'paper_size_ticket': return typeof t.usd === 'number' ? `PAPER size ${s(t.symbol)} $${t.usd.toFixed(2)}` : typeof t.fraction === 'number' ? `PAPER size ${s(t.symbol)} ${Math.round(t.fraction * 100)}%` : 'PAPER size';
|
|
70
|
+
case 'paper_confirm':
|
|
71
|
+
case 'paper_close': {
|
|
72
|
+
const sol = typeof f.solDelta === 'number' ? ` ${f.solDelta > 0 ? '+' : ''}${f.solDelta.toFixed(4)} SOL` : '';
|
|
73
|
+
return `PAPER ${tool === 'paper_close' ? 'close' : s(f.side) || 'fill'} ${s(f.symbol)}${sol}`;
|
|
74
|
+
}
|
|
75
|
+
case 'paper_positions': return `PAPER positions (${Array.isArray(result.positions) ? result.positions.length : 0})`;
|
|
76
|
+
case 'paper_pnl': return 'PAPER pnl';
|
|
77
|
+
default: return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
49
80
|
/** Build one spectator record for a completed MCP tool call. */
|
|
50
81
|
export function recordForToolCall(opts) {
|
|
51
82
|
const isErr = typeof opts.result.error === 'string';
|
|
@@ -55,11 +86,96 @@ export function recordForToolCall(opts) {
|
|
|
55
86
|
// `nav` phase is the enforcement boundary that prevents a denied MCP call from moving the owner's TUI.
|
|
56
87
|
v: 1, seq: opts.seq, t: (opts.now ?? Date.now)(), phase: isErr ? 'refused' : nav ? 'nav' : 'result',
|
|
57
88
|
agent: opts.agent, transport: 'mcp', tool: opts.tool,
|
|
58
|
-
summary: isErr ? `error: ${String(opts.result.error).slice(0, 80)}` : `${opts.tool}${shortMint(opts.args)}`,
|
|
89
|
+
summary: isErr ? `error: ${String(opts.result.error).slice(0, 80)}` : paperSummary(opts.tool, opts.args, opts.result) ?? `${opts.tool}${shortMint(opts.args)}`,
|
|
59
90
|
ms: opts.ms, dataAgeSec: isErr ? null : maxAgeSec(opts.result), nav, ok: !isErr,
|
|
91
|
+
...(() => { const d = detailFor(opts.tool, opts.args, opts.result); return d ? { detail: d } : {}; })(),
|
|
60
92
|
...(opts.token ? { token: opts.token } : {}),
|
|
61
93
|
};
|
|
62
94
|
}
|
|
95
|
+
const num = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : null);
|
|
96
|
+
/** A firewalled field's inner text (`<untrusted …>X</untrusted>` → X; the inner text is already scrubbed upstream). */
|
|
97
|
+
const unwrap = (x) => x.replace(/^<untrusted\b[^>]*>([\s\S]*?)<\/untrusted>$/, '$1');
|
|
98
|
+
const str = (x, max = 16) => (typeof x === 'string' && x ? unwrap(x).slice(0, max) || null : null);
|
|
99
|
+
const MINT_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
100
|
+
const mintOf = (x) => (typeof x === 'string' && MINT_RE.test(x) ? x : null);
|
|
101
|
+
/**
|
|
102
|
+
* task-1466: the app-computed numbers behind one call, for the follow view (SpectatorDetail in @dexterm/agents).
|
|
103
|
+
* Copied from the tool's OWN result (a screener page, a verdict, a PAPER ticket / fill / position, the caps the
|
|
104
|
+
* desk replayed); agent-supplied args only name the coin. null when the call has nothing worth showing.
|
|
105
|
+
*/
|
|
106
|
+
export function detailFor(tool, args, result) {
|
|
107
|
+
const d = {};
|
|
108
|
+
if (typeof result.error === 'string') {
|
|
109
|
+
if (tool.startsWith('paper_') && typeof result.reason === 'string')
|
|
110
|
+
d.refusal = result.reason.slice(0, 40);
|
|
111
|
+
return Object.keys(d).length ? d : null;
|
|
112
|
+
}
|
|
113
|
+
if (tool === 'screener_query' && Array.isArray(result.rows)) {
|
|
114
|
+
d.rows = result.rows.slice(0, 12).flatMap((r) => {
|
|
115
|
+
const mint = mintOf(r.mint);
|
|
116
|
+
if (!mint)
|
|
117
|
+
return [];
|
|
118
|
+
const pc = (r.priceChange ?? {});
|
|
119
|
+
return [{ mint, symbol: str(r.symbol) ?? '?', chg: num(pc.h1) }];
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (tool === 'screen_state' && Array.isArray(result.visible)) {
|
|
123
|
+
d.rows = result.visible.slice(0, 12).flatMap((r) => {
|
|
124
|
+
const mint = mintOf(r.mint);
|
|
125
|
+
return mint ? [{ mint, symbol: str(r.symbol) ?? '?', chg: num(r.chg1h) }] : [];
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (tool === 'get_token') {
|
|
129
|
+
const tk = (result.token ?? {});
|
|
130
|
+
const pair = (result.pair ?? {});
|
|
131
|
+
const mint = mintOf(tk.mint) ?? mintOf(args.mint);
|
|
132
|
+
if (mint)
|
|
133
|
+
d.coin = { mint, symbol: str(tk.symbol), priceUsd: num(pair.priceUsd) };
|
|
134
|
+
}
|
|
135
|
+
else if (tool === 'analyze_token') {
|
|
136
|
+
const mint = mintOf(result.mint) ?? mintOf(args.mint);
|
|
137
|
+
if (mint)
|
|
138
|
+
d.coin = { mint, symbol: str(result.symbol) };
|
|
139
|
+
const label = str(result.deterministic_verdict, 18);
|
|
140
|
+
if (label)
|
|
141
|
+
d.verdict = { label, rug: num(result.rug_probability) };
|
|
142
|
+
}
|
|
143
|
+
else if (tool === 'larp_check') {
|
|
144
|
+
const mint = mintOf(result.mint) ?? mintOf(args.mint);
|
|
145
|
+
if (mint)
|
|
146
|
+
d.coin = { mint, symbol: str(result.symbol) };
|
|
147
|
+
const flags = Array.isArray(result.flags) ? result.flags.filter((f) => typeof f === 'string') : [];
|
|
148
|
+
d.larp = { flags: flags.length, items: flags.slice(0, 6).map((f) => f.slice(0, 60)) };
|
|
149
|
+
}
|
|
150
|
+
else if (['open_token', 'get_trades', 'get_candles', 'get_holders', 'rugcheck', 'get_dev_history', 'dev_history', 'get_dossier'].includes(tool)) {
|
|
151
|
+
const mint = mintOf(args.mint);
|
|
152
|
+
if (mint)
|
|
153
|
+
d.coin = { mint, symbol: str(args.symbol) };
|
|
154
|
+
}
|
|
155
|
+
if (tool === 'paper_open_ticket' || tool === 'paper_size_ticket') {
|
|
156
|
+
const t = (result.ticket ?? {});
|
|
157
|
+
const mint = mintOf(t.mint);
|
|
158
|
+
if (mint && (t.side === 'buy' || t.side === 'sell'))
|
|
159
|
+
d.ticket = { side: t.side, symbol: str(t.symbol) ?? '?', mint, usd: num(t.usd), fraction: num(t.fraction) };
|
|
160
|
+
}
|
|
161
|
+
if (tool === 'paper_confirm' || tool === 'paper_close') {
|
|
162
|
+
const f = (result.fill ?? {});
|
|
163
|
+
const mint = mintOf(f.mint);
|
|
164
|
+
if (mint && (f.side === 'buy' || f.side === 'sell') && num(f.solDelta) !== null && num(f.tokens) !== null) {
|
|
165
|
+
d.fill = { side: f.side, symbol: str(f.symbol) ?? '?', mint, sol: num(f.solDelta), qty: num(f.tokens), priceUsd: num(f.priceUsd), impactPct: num(f.priceImpactPct), realizedSol: num(f.realizedSol) };
|
|
166
|
+
}
|
|
167
|
+
const p = (result.position ?? null);
|
|
168
|
+
const pm = p ? mintOf(p.mint) : null;
|
|
169
|
+
if (p && pm && num(p.tokens) !== null && num(p.costSol) !== null)
|
|
170
|
+
d.position = { mint: pm, symbol: str(p.symbol) ?? '?', qty: num(p.tokens), costSol: num(p.costSol), realizedSol: num(p.realizedSol) ?? 0 };
|
|
171
|
+
}
|
|
172
|
+
if (tool === 'paper_pnl') {
|
|
173
|
+
const m = (result.mandate ?? null);
|
|
174
|
+
if (m && num(m.perTradeUsd) !== null && num(m.totalUsd) !== null)
|
|
175
|
+
d.caps = { perTradeUsd: num(m.perTradeUsd), totalUsd: num(m.totalUsd), maxPriceImpactPct: num(m.maxPriceImpactPct) };
|
|
176
|
+
}
|
|
177
|
+
return Object.keys(d).length ? d : null;
|
|
178
|
+
}
|
|
63
179
|
export function fileSpectatorSinkFromEnv(env) {
|
|
64
180
|
const file = env.DEXTERM_SPECTATOR_FILE;
|
|
65
181
|
if (!file)
|
|
@@ -88,6 +204,44 @@ function spectatorHome(env) {
|
|
|
88
204
|
return env.DEXTERM_HOME && env.DEXTERM_HOME.trim() ? env.DEXTERM_HOME : join(homedir(), '.dexterm');
|
|
89
205
|
}
|
|
90
206
|
export function rendezvousPath(env) { return join(spectatorHome(env), MCP_SPECTATOR_RENDEZVOUS); }
|
|
207
|
+
// task-1443: an EXTERNAL `dexterm mcp` server has no DEXTERM_SCREEN_STATE (the running app did not spawn it), so it
|
|
208
|
+
// could never validate a navigation call against "the tokens currently on screen" and every nav tool failed — the
|
|
209
|
+
// following terminal never moved. While a terminal follows over MCP it publishes a reduced snapshot of its screen
|
|
210
|
+
// (screen, visible rows, selection, the action allow-list; no wallet, positions, tickets or app settings) here,
|
|
211
|
+
// refreshed about once a second; the server validates nav calls against it exactly as it does for the in-app analyst.
|
|
212
|
+
/** The follow-mode screen snapshot file name under ~/.dexterm. */
|
|
213
|
+
export const MCP_FOLLOW_SCREEN = 'mcp-screen.json';
|
|
214
|
+
/** A snapshot older than this is ignored (the terminal quit or froze): nav calls then fail closed. */
|
|
215
|
+
export const FOLLOW_SCREEN_MAX_AGE_MS = 10_000;
|
|
216
|
+
export function followScreenPath(env) { return join(spectatorHome(env), MCP_FOLLOW_SCREEN); }
|
|
217
|
+
/** Write the follow-mode snapshot (0600). Best-effort: a write failure only means nav calls fail closed. */
|
|
218
|
+
export function writeFollowScreen(env, snapshot) {
|
|
219
|
+
try {
|
|
220
|
+
writeFileSync(followScreenPath(env), JSON.stringify(snapshot) + '\n', { mode: 0o600 });
|
|
221
|
+
}
|
|
222
|
+
catch { /* fail closed */ }
|
|
223
|
+
}
|
|
224
|
+
/** Remove the follow-mode snapshot (the terminal quit or stopped following). */
|
|
225
|
+
export function clearFollowScreen(env) {
|
|
226
|
+
try {
|
|
227
|
+
rmSync(followScreenPath(env), { force: true });
|
|
228
|
+
}
|
|
229
|
+
catch { /* ignore */ }
|
|
230
|
+
}
|
|
231
|
+
/** Read the follow-mode screen snapshot, or null when absent, malformed or stale. Never throws. */
|
|
232
|
+
export function readFollowScreen(env, now = Date.now()) {
|
|
233
|
+
const path = followScreenPath(env);
|
|
234
|
+
try {
|
|
235
|
+
const st = statSync(path);
|
|
236
|
+
if (now - st.mtimeMs > FOLLOW_SCREEN_MAX_AGE_MS)
|
|
237
|
+
return null;
|
|
238
|
+
const o = JSON.parse(readFileSync(path, 'utf8'));
|
|
239
|
+
return o && typeof o === 'object' && !Array.isArray(o) ? o : null;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
91
245
|
/**
|
|
92
246
|
* Resolve the spectator sink for a `dexterm mcp` server. DEXTERM_SPECTATOR_FILE (the running app spawned us,
|
|
93
247
|
* e.g. the in-app analyst) wins and is always active; otherwise the sink watches the rendezvous file under
|
|
@@ -84,6 +84,7 @@ export function parseEntryFile(file, origin = 'bundled') {
|
|
|
84
84
|
const MONEY_WORDS = new Set([
|
|
85
85
|
'trade', 'swap', 'sign', 'signer', 'send', 'arm', 'ticket', 'order', 'transfer', 'withdraw', 'buy', 'sell',
|
|
86
86
|
'confirm', 'execute', 'submit', 'panic', 'cap', 'caps', 'mandate', 'mandates', 'approve', 'pay', 'payment', 'fund',
|
|
87
|
+
'close', // task-1443: paper_close books a (simulated) sell — a skill may never name it
|
|
87
88
|
]);
|
|
88
89
|
/** Tools that exist in dexterm and are money-adjacent by name, refused even though the server knows them. */
|
|
89
90
|
export const MONEY_TOOLS = ['open_ticket'];
|
|
@@ -18,5 +18,6 @@ export * from "./backtest.js";
|
|
|
18
18
|
// task-1359: LARP eval (recorded product verdicts vs outcome labels; counts only below n = 50).
|
|
19
19
|
export * from "./larp-eval.js";
|
|
20
20
|
export * from "./skill-backtest.js";
|
|
21
|
+
export * from "./larp-score-backtest.js";
|
|
21
22
|
// task-1414: skill care — rolling split, the proposal gate (new-coins + interval-noise + frozen canary), trend.
|
|
22
23
|
export * from "./skill-care.js";
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// LARP score backtest + calibration (task-1471). Pure: input is the task-1401 eval set (skills-eval.json).
|
|
2
|
+
//
|
|
3
|
+
// The eval set holds, per coin, the URL-free link features the coin advertised at t0 and the recorder's 72 h
|
|
4
|
+
// outcome (bad = rug or dead). That is the only part of the LARP score history can replay: pages, domains, GitHub
|
|
5
|
+
// and X accounts were not captured point-in-time (and looking them up now leaks the outcome, because dead coins'
|
|
6
|
+
// sites and accounts disappear). So this fits and scores the LINK-SHAPE flags only, and says so in every output.
|
|
7
|
+
//
|
|
8
|
+
// Pre-registered rule (written before the test split was scored):
|
|
9
|
+
// - fit on TRAIN only: for each link flag carried by ≥ LARP_SCORE_MIN_FLAG_N train coins,
|
|
10
|
+
// points = clamp(round(8 × ln OR), 0, 12), OR = Haldane-smoothed odds ratio of a bad outcome with vs without it;
|
|
11
|
+
// flags under that count keep their prior points;
|
|
12
|
+
// - bands are the fixed round thresholds 25 / 50 (LARP_SCORE_BANDS), never tuned;
|
|
13
|
+
// - report AUROC (ties count half) and P(bad) per band on train and on the held-out TEST split, with Wilson 95 %.
|
|
14
|
+
import { LARP_SCORE_BANDS, LARP_SCORE_WEIGHTS, bandOf, linkFlagIds, linkScore } from '../../data/src/index.js';
|
|
15
|
+
import { wilson } from "./larp-eval.js";
|
|
16
|
+
export const LARP_SCORE_BACKTEST_SCHEMA = 'dexterm/packs/larp-score-backtest/1';
|
|
17
|
+
export const LARP_SCORE_MIN_FLAG_N = 20;
|
|
18
|
+
const SCALE = 8;
|
|
19
|
+
const CAP = 12;
|
|
20
|
+
/** The link-shape flags the eval set can carry (in a stable order). */
|
|
21
|
+
export const LINK_FLAG_IDS = ['no_website', 'no_x', 'x_not_account', 'no_telegram', 'site_is_social', 'site_is_launchpad', 'site_linktree', 'site_free_host', 'domain_no_name_match', 'cheap_tld'];
|
|
22
|
+
/** Fit the link-flag points on train rows. Throws on a test row (the split is fixed before tuning). */
|
|
23
|
+
export function calibrateLinkPoints(train) {
|
|
24
|
+
if (train.some((r) => r.split !== 'train'))
|
|
25
|
+
throw new Error('calibrateLinkPoints: test rows are held out');
|
|
26
|
+
const N = train.length, B = train.filter((r) => r.bad).length;
|
|
27
|
+
return LINK_FLAG_IDS.map((id) => {
|
|
28
|
+
const has = train.filter((r) => linkFlagIds(r.link).includes(id));
|
|
29
|
+
const n = has.length, a = has.filter((r) => r.bad).length;
|
|
30
|
+
if (n < LARP_SCORE_MIN_FLAG_N)
|
|
31
|
+
return { id, n, bad: a, ln_or: null, points: LARP_SCORE_WEIGHTS[id].points, source: 'prior' };
|
|
32
|
+
const b = B - a, m = N - n;
|
|
33
|
+
const lnOr = Math.log(((a + 0.5) / (n - a + 0.5)) / ((b + 0.5) / (m - b + 0.5)));
|
|
34
|
+
return { id, n, bad: a, ln_or: Math.round(lnOr * 1000) / 1000, points: Math.max(0, Math.min(CAP, Math.round(SCALE * lnOr))), source: 'calibrated' };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** AUROC with ties counted half. Null when a class is empty. */
|
|
38
|
+
export function auroc(rows) {
|
|
39
|
+
const pos = rows.filter((r) => r.bad).map((r) => r.score), neg = rows.filter((r) => !r.bad).map((r) => r.score);
|
|
40
|
+
if (!pos.length || !neg.length)
|
|
41
|
+
return null;
|
|
42
|
+
let s = 0;
|
|
43
|
+
for (const p of pos)
|
|
44
|
+
for (const q of neg)
|
|
45
|
+
s += p > q ? 1 : p === q ? 0.5 : 0;
|
|
46
|
+
return s / (pos.length * neg.length);
|
|
47
|
+
}
|
|
48
|
+
function splitReport(rows, weights) {
|
|
49
|
+
const scored = rows.map((r) => ({ score: linkScore(r.link, weights), bad: r.bad }));
|
|
50
|
+
const by_band = { low: { n: 0, bad: 0, rate: null }, medium: { n: 0, bad: 0, rate: null }, high: { n: 0, bad: 0, rate: null } };
|
|
51
|
+
for (const s of scored) {
|
|
52
|
+
const b = by_band[bandOf(s.score)];
|
|
53
|
+
b.n++;
|
|
54
|
+
if (s.bad)
|
|
55
|
+
b.bad++;
|
|
56
|
+
}
|
|
57
|
+
for (const b of Object.values(by_band))
|
|
58
|
+
b.rate = b.n >= 20 ? wilson(b.bad, b.n) : null;
|
|
59
|
+
const B = scored.filter((s) => s.bad).length;
|
|
60
|
+
const at_threshold = [LARP_SCORE_BANDS.medium, LARP_SCORE_BANDS.high].map((t) => {
|
|
61
|
+
const f = scored.filter((s) => s.score >= t), tp = f.filter((s) => s.bad).length;
|
|
62
|
+
return { threshold: t, flagged: f.length, precision: f.length >= 20 ? wilson(tp, f.length) : null, recall: B ? wilson(tp, B) : null };
|
|
63
|
+
});
|
|
64
|
+
const sorted = scored.map((s) => s.score).sort((a, b) => a - b);
|
|
65
|
+
const q = (p) => sorted.length ? sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))] : 0;
|
|
66
|
+
const a = auroc(scored);
|
|
67
|
+
return { n: rows.length, bad: B, auroc: a === null ? null : Math.round(a * 1000) / 1000, by_band, at_threshold, score_quantiles: { p10: q(0.1), p50: q(0.5), p90: q(0.9), max: sorted.at(-1) ?? 0 } };
|
|
68
|
+
}
|
|
69
|
+
/** Fit on train, score train and test with the fitted points. */
|
|
70
|
+
export function larpScoreBacktest(set) {
|
|
71
|
+
const train = set.rows.filter((r) => r.split === 'train'), test = set.rows.filter((r) => r.split === 'test');
|
|
72
|
+
const fit = calibrateLinkPoints(train);
|
|
73
|
+
const weights = Object.fromEntries(fit.map((f) => [f.id, f.points]));
|
|
74
|
+
return {
|
|
75
|
+
schema: LARP_SCORE_BACKTEST_SCHEMA,
|
|
76
|
+
eval: { built_at: set.built_at, horizon_sec: set.horizon_sec, rows: set.rows.length, label_rule: set.label_rule, split_rule: set.split_rule },
|
|
77
|
+
part: 'link-shape flags only (what the eval set holds point-in-time)',
|
|
78
|
+
method: [
|
|
79
|
+
`fit on the train split only (${train.length} coins): points = clamp(round(${SCALE} × ln odds ratio of a bad outcome), 0, ${CAP}) for a flag on ≥ ${LARP_SCORE_MIN_FLAG_N} train coins; rarer flags keep their prior points`,
|
|
80
|
+
`bands fixed before scoring: low < ${LARP_SCORE_BANDS.medium} ≤ medium < ${LARP_SCORE_BANDS.high} ≤ high (not tuned)`,
|
|
81
|
+
'AUROC with ties counted half; rates with 95% Wilson intervals, printed only for n ≥ 20',
|
|
82
|
+
'meaning: how well the link-shape points rank coins that went bad (a price outcome) above the rest, NOT whether a product is real',
|
|
83
|
+
],
|
|
84
|
+
fit,
|
|
85
|
+
shipped_matches_fit: fit.every((f) => LARP_SCORE_WEIGHTS[f.id].points === f.points && LARP_SCORE_WEIGHTS[f.id].source === f.source),
|
|
86
|
+
train: splitReport(train, weights),
|
|
87
|
+
test: splitReport(test, weights),
|
|
88
|
+
not_measured: [
|
|
89
|
+
'website page flags (default page, wallet connect, drainer markers, never names the token): pages were not captured at t0',
|
|
90
|
+
'domain flags (new domain, registrant privacy) and GitHub flags: not captured at t0',
|
|
91
|
+
"X-account flags (new account, tiny following, no posts, suspended, handle mismatch): not captured at t0, and a lookup today leaks the outcome (dead coins' accounts get deleted or suspended); the X sources' robots.txt also rule out bulk crawling them for a backfill",
|
|
92
|
+
'the high band: link shape alone tops out below it by design, so its precision is unmeasured',
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const pct = (r) => (r === null ? 'counts only (n < 20)' : `${(r.value * 100).toFixed(1)}% [${(r.lo * 100).toFixed(0)}–${(r.hi * 100).toFixed(0)}]`);
|
|
97
|
+
export function renderLarpScoreBacktestText(r) {
|
|
98
|
+
const L = [];
|
|
99
|
+
L.push(`LARP SCORE BACKTEST ${r.part}`);
|
|
100
|
+
L.push(` eval set built ${r.eval.built_at}, ${r.eval.rows} coins, horizon ${r.eval.horizon_sec}s; ${r.eval.label_rule}`);
|
|
101
|
+
L.push(' fitted points (train split):');
|
|
102
|
+
for (const f of r.fit)
|
|
103
|
+
L.push(` ${f.id.padEnd(22)} n=${String(f.n).padStart(4)} bad=${String(f.bad).padStart(4)} ln OR=${f.ln_or === null ? ' — ' : f.ln_or.toFixed(2).padStart(6)} → ${String(f.points).padStart(2)} pts (${f.source})`);
|
|
104
|
+
L.push(` shipped weights match this fit: ${r.shipped_matches_fit ? 'yes' : 'NO (re-tune or update LARP_SCORE_WEIGHTS)'}`);
|
|
105
|
+
for (const [name, s] of [['train', r.train], ['TEST (held out)', r.test]]) {
|
|
106
|
+
L.push(` ${name}: n=${s.n} bad=${s.bad} (${((s.bad / Math.max(1, s.n)) * 100).toFixed(1)}%) AUROC=${s.auroc === null ? '—' : s.auroc.toFixed(3)} score p10/p50/p90/max=${s.score_quantiles.p10}/${s.score_quantiles.p50}/${s.score_quantiles.p90}/${s.score_quantiles.max}`);
|
|
107
|
+
for (const [b, v] of Object.entries(s.by_band))
|
|
108
|
+
L.push(` band ${b.padEnd(6)} n=${String(v.n).padStart(4)} bad=${String(v.bad).padStart(4)} P(bad)=${pct(v.rate)}`);
|
|
109
|
+
for (const t of s.at_threshold)
|
|
110
|
+
L.push(` score ≥ ${t.threshold}: flagged ${t.flagged} precision=${pct(t.precision)} recall=${t.recall ? pct(t.recall) : '—'}`);
|
|
111
|
+
}
|
|
112
|
+
L.push(' method:');
|
|
113
|
+
for (const m of r.method)
|
|
114
|
+
L.push(` - ${m}`);
|
|
115
|
+
L.push(' not measured:');
|
|
116
|
+
for (const m of r.not_measured)
|
|
117
|
+
L.push(` - ${m}`);
|
|
118
|
+
return L.join('\n') + '\n';
|
|
119
|
+
}
|
|
@@ -180,22 +180,43 @@
|
|
|
180
180
|
if (a >= 1e3) return `${s}$${(a / 1e3).toFixed(1)}K`;
|
|
181
181
|
return `${s}$${a.toFixed(0)}`;
|
|
182
182
|
}
|
|
183
|
-
// ---- task-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
// ---- task-1472 the one chart number formatter (mirrors @dexterm/tui fmtCompact, parity-tested): lowercase k, then
|
|
184
|
+
// M/B/T; one decimal while < 10 of the unit, none after; the unit is promoted after rounding (999 900 → 1M); below 1
|
|
185
|
+
// the subscript price form (0.0₅493): never scientific notation. `extra` (0..3) keeps two axis ticks apart.
|
|
186
|
+
const CU = ['', 'k', 'M', 'B', 'T'];
|
|
187
|
+
const trimZeros = (t) => (t.includes('.') ? t.replace(/0+$/, '').replace(/\.$/, '') : t);
|
|
188
|
+
function tinyBare(p) {
|
|
189
|
+
if (p >= 0.01) return p.toFixed(4);
|
|
190
|
+
const m = p.toFixed(12).match(/^0\.(0*)(\d+)/);
|
|
191
|
+
if (!m) return String(p);
|
|
192
|
+
const zeros = m[1].length;
|
|
193
|
+
const digits = m[2].replace(/0+$/, '').slice(0, 4) || '0';
|
|
194
|
+
if (zeros < 3) return `0.${m[1]}${digits}`;
|
|
195
|
+
return `0.0${String(zeros).split('').map((c) => SUB[Number(c)]).join('')}${digits}`;
|
|
196
|
+
}
|
|
197
|
+
function fmtCompact(n, extra = 0) {
|
|
186
198
|
if (!fin(n)) return '—';
|
|
199
|
+
if (n === 0) return '0';
|
|
187
200
|
const sign = n < 0 ? '-' : '';
|
|
188
|
-
|
|
189
|
-
if (a
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
const a = Math.abs(n);
|
|
202
|
+
if (a < 1) return sign + trimZeros(tinyBare(a));
|
|
203
|
+
const x = Math.max(0, Math.min(3, Math.floor(extra) || 0));
|
|
204
|
+
const round = (q) => Number(q.toFixed((q < 10 ? 1 : 0) + x));
|
|
205
|
+
let u = 0, v = a;
|
|
206
|
+
while (u < CU.length - 1 && v >= 1000) { v /= 1000; u++; }
|
|
207
|
+
let r = round(v);
|
|
208
|
+
if (r >= 1000 && u < CU.length - 1) { u++; r = round(r / 1000); }
|
|
209
|
+
return `${sign}${trimZeros(r.toFixed((r < 10 ? 1 : 0) + x))}${CU[u]}`;
|
|
210
|
+
}
|
|
211
|
+
/** `$4.9k`, `-$1.2k` (volume pane, legend volume). */
|
|
212
|
+
function fmtCompactUsd(n, extra = 0) {
|
|
213
|
+
const b = fmtCompact(n, extra);
|
|
214
|
+
return b === '—' ? b : b.startsWith('-') ? `-$${b.slice(1)}` : `$${b}`;
|
|
196
215
|
}
|
|
216
|
+
/** task-1366 name kept: the mcap axis label is now the compact formatter. */
|
|
217
|
+
const fmtMcapAxis = (n) => fmtCompact(n);
|
|
197
218
|
const MCAP_LABEL = 'mcap (current supply)';
|
|
198
|
-
const MCAP_FALLBACK = 'supply unknown · showing price'
|
|
219
|
+
const MCAP_FALLBACK = 'price (mcap unavailable)'; // task-1472: the card's wording (was 'supply unknown · showing price')
|
|
199
220
|
/**
|
|
200
221
|
* How the page shows numbers for a VM: `mcap` (with the supply multiplier) only when the terminal is in mcap mode AND
|
|
201
222
|
* sent a usable supply; mcap mode without one is `price` + a visible note. Never invents a supply.
|
|
@@ -207,7 +228,7 @@
|
|
|
207
228
|
if (supply) return { mode: 'mcap', supply, label: MCAP_LABEL, key: `mcap:${Number(supply.toPrecision(4))}` };
|
|
208
229
|
return { mode: 'price', supply: null, label: wantMcap ? MCAP_FALLBACK : null, key: wantMcap ? 'price:fallback' : 'price' };
|
|
209
230
|
}
|
|
210
|
-
/** The Y-axis / crosshair label formatter for a display: mcap = price × supply in
|
|
231
|
+
/** The Y-axis / crosshair label formatter for a display: mcap = price × supply in compact k/M/B, else the price label. */
|
|
211
232
|
function axisFormatter(d) {
|
|
212
233
|
if (d && d.mode === 'mcap' && fin(d.supply)) { const s = d.supply; return (p) => fmtMcapAxis(p * s); }
|
|
213
234
|
return fmtAxis;
|
|
@@ -225,7 +246,7 @@
|
|
|
225
246
|
const chg = base > 0 ? (bar.close - base) / base : null;
|
|
226
247
|
const f = axisFormatter(display);
|
|
227
248
|
const mc = !!display && display.mode === 'mcap';
|
|
228
|
-
return { o: f(bar.open), h: f(bar.high), l: f(bar.low), c: mc ? `${f(bar.close)} (${fmtPrice(bar.close)})` : f(bar.close), v:
|
|
249
|
+
return { o: f(bar.open), h: f(bar.high), l: f(bar.low), c: mc ? `${f(bar.close)} (${fmtPrice(bar.close)})` : f(bar.close), v: fmtCompactUsd(bar.volume), chg: fmtPct(chg), up: bar.close >= bar.open };
|
|
229
250
|
}
|
|
230
251
|
|
|
231
252
|
/** Index of the bar at `time` (binary search), or -1. */
|
|
@@ -257,6 +278,6 @@
|
|
|
257
278
|
return bars[i].time + f * (bars[i + 1].time - bars[i].time);
|
|
258
279
|
}
|
|
259
280
|
|
|
260
|
-
const api = { setTzOffset, chartTime, INTERVALS, PERIOD_MS, INDICATORS, STYLES, SCALES, toBars, mergeTail, ema, sma, vwap, volumes, barTimeOf, tapeMarkers, fillMarkers, priceFormatFor, fmtPrice, fmtAxis, fmtUsd, fmtPct, fmtSol, legendFor, fmtMcapAxis, displayOf, axisFormatter, MCAP_LABEL, MCAP_FALLBACK, indexOfTime, logicalOf, timeOfLogical };
|
|
281
|
+
const api = { setTzOffset, chartTime, INTERVALS, PERIOD_MS, INDICATORS, STYLES, SCALES, toBars, mergeTail, ema, sma, vwap, volumes, barTimeOf, tapeMarkers, fillMarkers, priceFormatFor, fmtPrice, fmtAxis, fmtUsd, fmtPct, fmtSol, legendFor, fmtMcapAxis, fmtCompact, fmtCompactUsd, displayOf, axisFormatter, MCAP_LABEL, MCAP_FALLBACK, indexOfTime, logicalOf, timeOfLogical };
|
|
261
282
|
root.DTChart = Object.freeze(api);
|
|
262
283
|
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -48,3 +48,12 @@
|
|
|
48
48
|
/* LWC attribution logo: the library injects this exact rule too (allowed by CSP hash); repeated here so the
|
|
49
49
|
logo stays positioned even if a future build changes its inline style text. */
|
|
50
50
|
a#tv-attr-logo { position: absolute; left: 10px; bottom: 10px; height: 19px; width: 35px; margin: 0; padding: 0; border: 0; z-index: 3; }
|
|
51
|
+
/* task-1473 r2: the persistent POSITION PnL chip (the held coin, which need not be the charted coin). Display only. */
|
|
52
|
+
.pnlchip { display: flex; flex-wrap: nowrap; gap: 0; padding: 2px 10px; font-size: 11px; white-space: nowrap; overflow: hidden; border-bottom: 1px solid var(--border); background: rgba(255,45,240,.07); color: var(--muted); }
|
|
53
|
+
.pnlchip .seg + .seg::before { content: ' │ '; color: var(--border); }
|
|
54
|
+
.pnlchip .head { color: var(--pump); font-weight: 700; }
|
|
55
|
+
.pnlchip .pnl { font-weight: 700; }
|
|
56
|
+
.pnlchip .pnl.up, .pnlchip .peak.up { color: var(--up); } .pnlchip .pnl.down, .pnlchip .peak.down { color: var(--down); }
|
|
57
|
+
.pnlchip .dd { color: var(--warn); }
|
|
58
|
+
.pnlchip .pnl.flash { background: var(--up); color: var(--bg, #000); padding: 0 3px; border-radius: 2px; }
|
|
59
|
+
@media (prefers-reduced-motion: reduce) { .pnlchip .pnl.flash { background: none; color: var(--up); padding: 0; } }
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
<div id="toolbar" class="toolbar">
|
|
24
24
|
<span class="grp" id="tf"></span>
|
|
25
25
|
<span class="sep"></span>
|
|
26
|
+
<span class="grp" id="axis"></span>
|
|
27
|
+
<span class="sep"></span>
|
|
26
28
|
<span class="grp" id="styles"></span>
|
|
27
29
|
<span class="sep"></span>
|
|
28
30
|
<span class="grp" id="scales"></span>
|
|
@@ -31,6 +33,7 @@
|
|
|
31
33
|
<span class="sep"></span>
|
|
32
34
|
<span class="grp" id="draws"></span>
|
|
33
35
|
</div>
|
|
36
|
+
<div id="pnlchip" class="pnlchip" hidden title="position PnL (display only)"></div>
|
|
34
37
|
<div id="banner" class="banner" hidden></div>
|
|
35
38
|
<div class="stage">
|
|
36
39
|
<canvas id="rain"></canvas>
|
|
@@ -44,7 +47,7 @@
|
|
|
44
47
|
<div id="pos-cells" class="pos-cells"></div>
|
|
45
48
|
</div>
|
|
46
49
|
<div id="foot" class="foot">
|
|
47
|
-
<span class="keys">1–6 tf · c style · a/l/p scale · e ema · w vwap · m vol·ma · v vol · b tape · h/t draw · f fit · esc close</span>
|
|
50
|
+
<span class="keys">1–6 tf · A/y mcap⇄price · c style · a/l/p scale · e ema · w vwap · m vol·ma · v vol · b tape · h/t draw · f fit · esc close</span>
|
|
48
51
|
<span class="attr" title="TradingView Lightweight Charts v__LWC_VERSION__ (Apache-2.0), self-hosted"><span id="notice">__LWC_NOTICE__</span> · <a id="tv-link" href="https://www.tradingview.com/" target="_blank" rel="noopener noreferrer">https://www.tradingview.com/</a></span>
|
|
49
52
|
</div>
|
|
50
53
|
</div>
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const $ = (id) => document.getElementById(id);
|
|
13
13
|
const titleEl = $('title'), legendEl = $('legend'), liveEl = $('live'), bannerEl = $('banner'), hintEl = $('hint');
|
|
14
14
|
const posEl = $('pos'), posTitleEl = $('pos-title'), posCapEl = $('pos-cap'), posCellsEl = $('pos-cells');
|
|
15
|
+
const chipEl = $('pnlchip');
|
|
15
16
|
const imgSlot = $('coin-img-slot');
|
|
16
17
|
const chartEl = $('chart'), drawCv = $('draw'), dctx = drawCv.getContext('2d');
|
|
17
18
|
const STALE_MS = 20_000;
|
|
@@ -57,7 +58,7 @@
|
|
|
57
58
|
if (type === 2) return String(d.getUTCDate());
|
|
58
59
|
return `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
|
|
59
60
|
}
|
|
60
|
-
const volFmt = (v) => C.
|
|
61
|
+
const volFmt = (v) => C.fmtCompactUsd(v); // task-1472: volume stays USD, compact k/M
|
|
61
62
|
|
|
62
63
|
// ---------------------------------------------------------------- chart + series
|
|
63
64
|
const chart = L.createChart(chartEl, {
|
|
@@ -80,7 +81,7 @@
|
|
|
80
81
|
handleScroll: { mouseWheel: true, pressedMouseMove: true, horzTouchDrag: true, vertTouchDrag: false },
|
|
81
82
|
handleScale: { axisPressedMouseMove: { time: true, price: true }, axisDoubleClickReset: true, mouseWheel: true, pinch: true },
|
|
82
83
|
});
|
|
83
|
-
// task-1366: the axis formatter follows the terminal's
|
|
84
|
+
// task-1366/1472: the axis formatter follows the terminal's chart axis (mcap = price × the current supply, compact k/M/B)
|
|
84
85
|
const pricePF = (p) => ({ type: 'custom', formatter: C.axisFormatter(S.display), minMove: C.priceFormatFor(p).minMove });
|
|
85
86
|
// Keep the position levels (entry / stop / target) and the live price in view: autoscale widens to include them
|
|
86
87
|
// when they are within 3× of the last close (a far-away level would squash the candles; its row cell still shows it).
|
|
@@ -159,13 +160,16 @@
|
|
|
159
160
|
DT.onTheme(applyChartTheme);
|
|
160
161
|
|
|
161
162
|
// ---------------------------------------------------------------- toolbar (buttons built with textContent only)
|
|
162
|
-
const btns = { tf: new Map(), styles: new Map(), scales: new Map(), inds: new Map(), draws: new Map() };
|
|
163
|
+
const btns = { tf: new Map(), axis: new Map(), styles: new Map(), scales: new Map(), inds: new Map(), draws: new Map() };
|
|
163
164
|
function mkBtn(group, id, label, title, onClick) {
|
|
164
165
|
const b = document.createElement('button'); b.type = 'button'; b.className = 'tb'; b.textContent = label; b.title = title;
|
|
165
166
|
b.addEventListener('click', (e) => { e.preventDefault(); onClick(); });
|
|
166
167
|
$(group).appendChild(b); btns[group].set(id, b); return b;
|
|
167
168
|
}
|
|
168
169
|
C.INTERVALS.forEach((iv, i) => mkBtn('tf', iv, iv, `timeframe ${iv} (${i + 1})`, () => requestInterval(iv)));
|
|
170
|
+
// task-1472: MCAP | PRICE asks the terminal to switch (and save) the chart axis; the next VM re-labels the page
|
|
171
|
+
mkBtn('axis', 'mcap', 'MCAP', 'Y axis: market cap (A / y toggles; saved by the terminal)', () => setAxis('mcap'));
|
|
172
|
+
mkBtn('axis', 'price', 'PRICE', 'Y axis: USD price (A / y toggles; saved by the terminal)', () => setAxis('price'));
|
|
169
173
|
mkBtn('styles', 'candles', 'candles', 'candlesticks (c cycles)', () => setStyle('candles', true));
|
|
170
174
|
mkBtn('styles', 'line', 'line', 'line (c cycles)', () => setStyle('line', true));
|
|
171
175
|
mkBtn('styles', 'area', 'area', 'area (c cycles)', () => setStyle('area', true));
|
|
@@ -183,6 +187,8 @@
|
|
|
183
187
|
mkBtn('draws', 'x', '✕ clear', 'clear drawings (x; backspace = undo last)', () => clearDrawings());
|
|
184
188
|
function syncToolbar() {
|
|
185
189
|
for (const [iv, b] of btns.tf) { b.classList.toggle('on', iv === S.interval); b.classList.toggle('pending', iv === S.pendingInterval && iv !== S.interval); }
|
|
190
|
+
const ax = S.vm && S.vm.priceMode === 'mcap' ? 'mcap' : 'price';
|
|
191
|
+
for (const [k, b] of btns.axis) b.classList.toggle('on', k === ax);
|
|
186
192
|
for (const [k, b] of btns.styles) b.classList.toggle('on', k === S.style);
|
|
187
193
|
for (const [k, b] of btns.scales) b.classList.toggle('on', k === S.scale);
|
|
188
194
|
for (const [k, b] of btns.inds) b.classList.toggle('on', S.ind.has(k));
|
|
@@ -196,6 +202,11 @@
|
|
|
196
202
|
S.pendingInterval = iv; S.reqAt = performance.now(); syncToolbar();
|
|
197
203
|
DT.send({ type: 'event', name: 'chart.interval', payload: { interval: iv } });
|
|
198
204
|
}
|
|
205
|
+
/** task-1472: ask the terminal for a Y axis; nothing changes here until its next VM carries the new mode. */
|
|
206
|
+
function setAxis(axis) {
|
|
207
|
+
if (axis !== 'mcap' && axis !== 'price') return;
|
|
208
|
+
DT.send({ type: 'event', name: 'chart.axis', payload: { axis } });
|
|
209
|
+
}
|
|
199
210
|
function setStyle(st, user) {
|
|
200
211
|
if (!C.STYLES.includes(st) || st === S.style) return;
|
|
201
212
|
S.style = st;
|
|
@@ -397,6 +408,23 @@
|
|
|
397
408
|
}
|
|
398
409
|
}
|
|
399
410
|
|
|
411
|
+
// task-1473 r2: the POSITION PnL chip (formatted terminal-side; textContent only, display only)
|
|
412
|
+
function renderChip() {
|
|
413
|
+
const chip = S.vm && S.vm.chip;
|
|
414
|
+
if (!chipEl) return;
|
|
415
|
+
if (!chip || !Array.isArray(chip.segments) || !chip.segments.length) { chipEl.hidden = true; return; }
|
|
416
|
+
chipEl.hidden = false;
|
|
417
|
+
while (chipEl.firstChild) chipEl.removeChild(chipEl.firstChild);
|
|
418
|
+
const tone = ['up', 'down'].includes(chip.tone) ? chip.tone : '';
|
|
419
|
+
for (const sg of chip.segments.slice(0, 8)) {
|
|
420
|
+
const id = ['head', 'pnl', 'peak', 'px', 'held', 'dd', 'tp'].includes(sg.id) ? sg.id : 'x';
|
|
421
|
+
let cls = `seg ${id}`;
|
|
422
|
+
if (id === 'pnl') cls += ` ${tone}${chip.newPeak ? ' flash' : ''}`;
|
|
423
|
+
if (id === 'peak') cls += ` ${/^peak −/.test(String(sg.text)) ? 'down' : 'up'}`;
|
|
424
|
+
chipEl.appendChild(el('span', cls, String(sg.text).slice(0, 48)));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
400
428
|
// ---------------------------------------------------------------- incoming view models
|
|
401
429
|
function onData(vm) {
|
|
402
430
|
if (!vm || typeof vm !== 'object') return;
|
|
@@ -409,7 +437,7 @@
|
|
|
409
437
|
// task-1366: a mode / supply change re-labels the axis in place (the bars are prices either way)
|
|
410
438
|
const disp = C.displayOf(vm);
|
|
411
439
|
if (disp.key !== S.display.key) { S.display = disp; applyPriceFormat(); }
|
|
412
|
-
if (tail && (!vm.seriesKey || vm.seriesKey !== S.key)) { S.vm = { ...vm, candles: [] }; renderHeader(); renderPosition(); renderOverlays(); return; }
|
|
440
|
+
if (tail && (!vm.seriesKey || vm.seriesKey !== S.key)) { S.vm = { ...vm, candles: [] }; renderHeader(); renderPosition(); renderChip(); renderOverlays(); return; }
|
|
413
441
|
const intervalChanged = vm.interval !== S.interval;
|
|
414
442
|
S.interval = vm.interval;
|
|
415
443
|
const answered = S.pendingInterval === vm.interval ? S.reqAt : null;
|
|
@@ -425,7 +453,7 @@
|
|
|
425
453
|
if (intervalChanged || S.key !== prevKey) S.fitNext = true;
|
|
426
454
|
renderFull();
|
|
427
455
|
}
|
|
428
|
-
renderHeader(); renderLegend(S.hover); renderDataBanner(); renderPosition(); syncToolbar(); renderLive();
|
|
456
|
+
renderHeader(); renderLegend(S.hover); renderDataBanner(); renderPosition(); renderChip(); syncToolbar(); renderLive();
|
|
429
457
|
// rain mood follows direction (gold on a pump, red-shift on a dump), as in the v1 HUD; chg is in percent
|
|
430
458
|
const dir = vm.chg ? (vm.chg.h24 ?? vm.chg.h1 ?? 0) : 0;
|
|
431
459
|
if (rain) { rain.setMood(dir > 1 ? 'rainPump' : dir < -1 ? 'rainDump' : 'rain'); rain.setActivity(Math.min(1, 0.4 + Math.abs(dir) / 100)); rain.setSources([String(vm.symbol || ''), String(vm.mint || '').slice(0, 6)]); }
|
|
@@ -514,6 +542,7 @@
|
|
|
514
542
|
const n = parseInt(k, 10);
|
|
515
543
|
if (n >= 1 && n <= 6) { requestInterval(C.INTERVALS[n - 1]); return; }
|
|
516
544
|
switch (k) {
|
|
545
|
+
case 'A': case 'y': setAxis(S.vm && S.vm.priceMode === 'mcap' ? 'price' : 'mcap'); break; // task-1472
|
|
517
546
|
case 'c': setStyle(C.STYLES[(C.STYLES.indexOf(S.style) + 1) % C.STYLES.length], true); break;
|
|
518
547
|
case 'a': setScale('auto', true); break;
|
|
519
548
|
case 'l': setScale(S.scale === 'log' ? 'auto' : 'log', true); break;
|
|
@@ -43,3 +43,48 @@
|
|
|
43
43
|
.spec-act-age { color: var(--warn); }
|
|
44
44
|
|
|
45
45
|
.spec-foot-redact { color: var(--up); font-weight: 600; }
|
|
46
|
+
|
|
47
|
+
/* task-1466 spectator v2: the agent story (phase rail + heartbeat, thought ribbon, call timeline, vetting + PAPER cards) */
|
|
48
|
+
.spec-story { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; font: 12px ui-monospace, Menlo, monospace; }
|
|
49
|
+
.spec-rail { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
|
50
|
+
.spec-heart { width: 10px; height: 10px; border-radius: 50%; background: var(--down); margin-right: 6px; box-shadow: 0 0 8px var(--down); animation: spec-beat 1s infinite ease-out; }
|
|
51
|
+
.spec-heart.still { animation: none; background: var(--warn); box-shadow: none; }
|
|
52
|
+
.spec-heart.dead { animation: none; background: var(--dim); box-shadow: none; }
|
|
53
|
+
@keyframes spec-beat { 0% { transform: scale(1.35); opacity: 1; } 30% { transform: scale(0.9); opacity: 0.7; } 100% { transform: scale(1); opacity: 0.85; } }
|
|
54
|
+
.spec-bpm { color: var(--muted); margin-right: 8px; min-width: 44px; }
|
|
55
|
+
.spec-phase { padding: 2px 8px; border-radius: 4px; color: var(--dim); border: 1px solid transparent; letter-spacing: 0.05em; }
|
|
56
|
+
.spec-phase.done { color: var(--muted); }
|
|
57
|
+
.spec-phase.on { color: #000; background: var(--accent); font-weight: 700; box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 60%, transparent); }
|
|
58
|
+
.spec-phase.on.p-vetting { background: var(--borderFocus); box-shadow: 0 0 12px color-mix(in srgb, var(--borderFocus) 60%, transparent); }
|
|
59
|
+
.spec-phase.on.p-sizing { background: var(--warn); }
|
|
60
|
+
.spec-phase.on.p-executing { background: var(--up); }
|
|
61
|
+
.spec-phase.on.p-holding { background: var(--pump); }
|
|
62
|
+
.spec-phase.on.p-exiting { background: var(--ai, var(--accent)); }
|
|
63
|
+
.spec-sep { color: var(--border); }
|
|
64
|
+
.spec-paper { margin-left: auto; padding: 1px 6px; border-radius: 4px; background: var(--warn); color: #000; font-weight: 700; }
|
|
65
|
+
.spec-ctlstate { padding: 1px 6px; border-radius: 4px; font-weight: 700; color: #000; }
|
|
66
|
+
.spec-ctlstate.paused { background: var(--warn); }
|
|
67
|
+
.spec-ctlstate.takeover { background: var(--accent); }
|
|
68
|
+
.spec-ctlstate.killed { background: var(--down); color: #fff; }
|
|
69
|
+
.spec-ribbon { padding: 6px 8px; border-left: 3px solid var(--ai, var(--accent)); background: color-mix(in srgb, var(--panel) 85%, transparent); color: var(--text); min-height: 1.4em; }
|
|
70
|
+
.spec-ribbon.ours { border-left-color: var(--border); color: var(--muted); font-style: italic; }
|
|
71
|
+
.spec-ribbon-who { color: var(--ai, var(--accent)); margin-right: 6px; font-style: normal; }
|
|
72
|
+
.spec-timeline { display: flex; align-items: flex-start; gap: 0; overflow: hidden; justify-content: flex-end; }
|
|
73
|
+
.spec-node { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 0 2px; }
|
|
74
|
+
.spec-node-l { color: var(--text); white-space: nowrap; }
|
|
75
|
+
.spec-node.paper .spec-node-l { color: var(--warn); font-weight: 700; }
|
|
76
|
+
.spec-chip { font-size: 10px; padding: 0 5px; border-radius: 3px; background: color-mix(in srgb, var(--up) 18%, transparent); color: var(--up); }
|
|
77
|
+
.spec-chip.slow { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
|
|
78
|
+
.spec-chip.fail, .spec-chip.refused { background: color-mix(in srgb, var(--down) 22%, transparent); color: var(--down); }
|
|
79
|
+
.spec-link { color: var(--border); align-self: flex-start; margin-top: 1px; }
|
|
80
|
+
.spec-cards { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
81
|
+
.spec-card { flex: 1 1 220px; border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; background: color-mix(in srgb, var(--panel) 85%, transparent); }
|
|
82
|
+
.spec-card-h { color: #000; background: var(--borderFocus); font-weight: 700; padding: 0 6px; border-radius: 3px; display: inline-block; margin-bottom: 4px; }
|
|
83
|
+
.spec-card.trade .spec-card-h { background: var(--pump); }
|
|
84
|
+
.spec-check { margin-right: 8px; color: var(--dim); }
|
|
85
|
+
.spec-check.ok { color: var(--up); font-weight: 600; }
|
|
86
|
+
.spec-big { font-size: 30px; font-weight: 800; letter-spacing: 0.02em; line-height: 1.1; }
|
|
87
|
+
.spec-big.up { color: var(--up); text-shadow: 0 0 14px color-mix(in srgb, var(--up) 45%, transparent); }
|
|
88
|
+
.spec-big.down { color: var(--down); }
|
|
89
|
+
.spec-kv { color: var(--muted); }
|
|
90
|
+
.spec-kv b { color: var(--text); font-weight: 600; }
|