dexterm-cli 0.1.5 → 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-spectator.js +17 -1
- package/dist/packages/cli/src/analyst/backend.js +23 -0
- package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
- package/dist/packages/cli/src/analyst/session.js +12 -1
- package/dist/packages/cli/src/app-help-gen.js +7 -7
- package/dist/packages/cli/src/backtest-cmd.js +13 -2
- package/dist/packages/cli/src/display.js +55 -9
- package/dist/packages/cli/src/live/dev-history.js +31 -15
- package/dist/packages/cli/src/live/index.js +5 -1
- package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
- package/dist/packages/cli/src/live/screens/token-live.js +41 -11
- package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
- package/dist/packages/cli/src/live/trade.js +22 -0
- package/dist/packages/cli/src/main.js +146 -12
- package/dist/packages/cli/src/mcp-follow.js +103 -6
- package/dist/packages/cli/src/plan-cmd.js +15 -5
- package/dist/packages/cli/src/popup.js +17 -1
- 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 +4 -0
- 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/schema.js +8 -2
- package/dist/packages/data/src/index.js +2 -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/trade/agent/paper-desk.js +77 -12
- 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/dev-history.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +5 -1
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +2 -1
- package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +3 -2
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -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-larp-measured-accuracy.md +1 -0
- 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-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-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 +10 -7
- 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/src/paper-agent.js +3 -2
- package/dist/packages/mcp/src/server.js +162 -21
- package/dist/packages/mcp/src/spectator-sink.js +85 -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 +7 -3
- 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
|
@@ -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; }
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<div id="hud" class="hud">
|
|
13
13
|
<div id="drag" class="drag"><span class="mark">✦</span><span id="title" class="title">dexterm · WATCH THE AGENT COOK</span><span id="status" class="status">connecting…</span><button id="close" class="close" type="button" aria-label="close">✕</button></div>
|
|
14
14
|
<div class="stage"><canvas id="rain"></canvas><div class="body">
|
|
15
|
+
<div id="story" class="spec-story"></div>
|
|
15
16
|
<div id="screen" class="spec-screen"></div>
|
|
16
17
|
<div id="agents" class="spec-agents"></div>
|
|
17
18
|
<div class="spec-activity-head">agent activity</div>
|
|
@@ -14,8 +14,77 @@
|
|
|
14
14
|
|
|
15
15
|
let vm = null;
|
|
16
16
|
|
|
17
|
+
const pctS = (p) => (p == null ? '—' : `${p >= 0 ? '+' : '−'}${Math.abs(p * 100).toFixed(1)}%`);
|
|
18
|
+
const px = (n) => (n == null ? '—' : n >= 1 ? `$${n.toFixed(4)}` : `$${Number(n.toPrecision(3))}`);
|
|
19
|
+
const fmtMs = (ms) => (ms == null ? '·' : ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`);
|
|
20
|
+
const CHECKS = ['price', 'tape', 'chart', 'holders', 'rug', 'dev', 'larp', 'verdict'];
|
|
21
|
+
|
|
22
|
+
// task-1466 spectator v2: the agent story above the activity list (textContent only; strings redacted TUI-side).
|
|
23
|
+
function renderStory(st) {
|
|
24
|
+
const host = $('story'); host.textContent = '';
|
|
25
|
+
if (!st) return;
|
|
26
|
+
const rail = el('div', 'spec-rail');
|
|
27
|
+
const heart = el('span', 'spec-heart' + (st.control === 'killed' ? ' dead' : st.control !== 'running' ? ' still' : ''));
|
|
28
|
+
if (st.bpm > 0) heart.style.animationDuration = `${(60 / st.bpm).toFixed(2)}s`;
|
|
29
|
+
rail.appendChild(heart);
|
|
30
|
+
rail.appendChild(el('span', 'spec-bpm', st.control === 'killed' ? '— bpm' : `${st.bpm} bpm`));
|
|
31
|
+
const ai = st.rail.indexOf(st.phase);
|
|
32
|
+
st.rail.forEach((p, i) => {
|
|
33
|
+
if (i) rail.appendChild(el('span', 'spec-sep', '─'));
|
|
34
|
+
const on = i === ai && st.control !== 'killed';
|
|
35
|
+
rail.appendChild(el('span', 'spec-phase p-' + p + (on ? ' on' : i < ai ? ' done' : ''), (on ? '▸' : '') + p.toUpperCase() + (on && st.sub ? ` · ${st.sub}` : '')));
|
|
36
|
+
});
|
|
37
|
+
if (st.control !== 'running') rail.appendChild(el('span', 'spec-ctlstate ' + st.control, st.control === 'killed' ? '■ KILLED' : st.control === 'paused' ? '❚❚ PAUSED' : 'YOU ARE DRIVING'));
|
|
38
|
+
rail.appendChild(el('span', 'spec-paper', 'PAPER'));
|
|
39
|
+
host.appendChild(rail);
|
|
40
|
+
const th = (st.thoughts || []).slice().reverse();
|
|
41
|
+
const agentT = th.find((x) => x.source === 'agent');
|
|
42
|
+
const ours = th.find((x) => x.source === 'dexterm');
|
|
43
|
+
const pick = agentT && (!ours || agentT.t >= ours.t - 1500 || Date.now() - agentT.t < 9000) ? agentT : ours || agentT;
|
|
44
|
+
const rib = el('div', 'spec-ribbon' + (pick && pick.source === 'dexterm' ? ' ours' : ''));
|
|
45
|
+
if (pick) { if (pick.source === 'agent') rib.appendChild(el('span', 'spec-ribbon-who', '❯')); rib.appendChild(el('span', '', pick.text)); }
|
|
46
|
+
else rib.appendChild(el('span', 'spec-dim', 'the agent has not said anything yet'));
|
|
47
|
+
host.appendChild(rib);
|
|
48
|
+
const tl = el('div', 'spec-timeline');
|
|
49
|
+
(st.nodes || []).slice(-14).forEach((n, i) => {
|
|
50
|
+
if (i) tl.appendChild(el('span', 'spec-link', '──'));
|
|
51
|
+
const node = el('div', 'spec-node' + (n.paper ? ' paper' : ''));
|
|
52
|
+
node.appendChild(el('span', 'spec-node-l', `${n.status === 'refused' ? '✗' : n.paper ? '■' : '●'} ${n.label}${n.count > 1 ? ` ×${n.count}` : ''}`));
|
|
53
|
+
node.appendChild(el('span', 'spec-chip ' + n.status, n.status === 'refused' ? 'refused' : fmtMs(n.ms)));
|
|
54
|
+
tl.appendChild(node);
|
|
55
|
+
});
|
|
56
|
+
host.appendChild(tl);
|
|
57
|
+
const cards = el('div', 'spec-cards');
|
|
58
|
+
if (st.focus) {
|
|
59
|
+
const c = el('div', 'spec-card');
|
|
60
|
+
c.appendChild(el('span', 'spec-card-h', `VETTING $${st.focus.symbol}`));
|
|
61
|
+
const row = el('div');
|
|
62
|
+
for (const k of CHECKS) row.appendChild(el('span', 'spec-check' + (st.focus.checks.includes(k) ? ' ok' : ''), `${st.focus.checks.includes(k) ? '✓' : '·'}${k}`));
|
|
63
|
+
c.appendChild(row);
|
|
64
|
+
if (st.focus.larpFlags != null) c.appendChild(el('div', 'spec-kv', `LARP evidence: ${st.focus.larpFlags} flag${st.focus.larpFlags === 1 ? '' : 's'} (not a verdict)`));
|
|
65
|
+
if (st.focus.verdict) c.appendChild(el('div', 'spec-kv', `verdict ${st.focus.verdict}${st.focus.rug != null ? ` · rug heuristic ${Math.round(st.focus.rug * 100)}%` : ''}`));
|
|
66
|
+
cards.appendChild(c);
|
|
67
|
+
}
|
|
68
|
+
if (st.arc) {
|
|
69
|
+
const a = st.arc;
|
|
70
|
+
const c = el('div', 'spec-card trade');
|
|
71
|
+
c.appendChild(el('span', 'spec-card-h', `PAPER · $${a.symbol} · ${a.stage === 'closed' ? 'RESULT' : a.stage === 'open' ? 'HOLDING' : a.stage === 'refused' ? 'REFUSED' : 'TICKET'}`));
|
|
72
|
+
const big = a.stage === 'closed' ? a.resultPct : a.pnlPct;
|
|
73
|
+
if (big != null) c.appendChild(el('div', 'spec-big ' + (big >= 0 ? 'up' : 'down'), pctS(big)));
|
|
74
|
+
const kv = el('div', 'spec-kv');
|
|
75
|
+
kv.textContent = [a.sizeUsd != null ? `size $${a.sizeUsd.toFixed(2)}` : null, a.entryUsd != null ? `entry ${px(a.entryUsd)}` : null, a.markUsd != null && a.stage !== 'closed' ? `mark ${px(a.markUsd)}` : null,
|
|
76
|
+
a.peakPct != null ? `peak ${pctS(a.peakPct)}` : null, a.exits ? `exits ${a.exits}` : null, a.realizedSol != null ? `realized ${a.realizedSol >= 0 ? '+' : ''}${a.realizedSol.toFixed(4)} SOL` : null,
|
|
77
|
+
a.refusal ? `refused: ${a.refusal}` : null].filter(Boolean).join(' · ');
|
|
78
|
+
c.appendChild(kv);
|
|
79
|
+
c.appendChild(el('div', 'spec-dim', 'simulated paper trade · not a result'));
|
|
80
|
+
cards.appendChild(c);
|
|
81
|
+
}
|
|
82
|
+
if (cards.childNodes.length) host.appendChild(cards);
|
|
83
|
+
}
|
|
84
|
+
|
|
17
85
|
function render() {
|
|
18
86
|
if (!vm) return;
|
|
87
|
+
renderStory(vm.story || null);
|
|
19
88
|
|
|
20
89
|
const scr = $('screen'); scr.textContent = '';
|
|
21
90
|
const s = vm.screen || {};
|
|
@@ -28,4 +28,6 @@ export const CHART_INDICATORS = ['ema9', 'ema21', 'vwap', 'volma', 'volume', 'ta
|
|
|
28
28
|
export const CHART_STYLES = ['candles', 'line', 'area'];
|
|
29
29
|
/** Price-scale mode: auto (normal, autoscaled), logarithmic, percent (from the first visible bar). */
|
|
30
30
|
export const CHART_SCALES = ['auto', 'log', 'percent'];
|
|
31
|
+
/** task-1472: what the chart's Y axis shows (the popup's MCAP | PRICE control / `A` ask the terminal to switch). */
|
|
32
|
+
export const CHART_AXES = ['mcap', 'price'];
|
|
31
33
|
export const PROTOCOL_VERSION = 'dexterm-popup/1';
|
|
@@ -26,7 +26,7 @@ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
|
26
26
|
import { readFileSync, existsSync } from 'node:fs';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
28
|
import path from 'node:path';
|
|
29
|
-
import { PROTOCOL_VERSION, CHART_INTERVALS, CHART_INDICATORS, CHART_STYLES, CHART_SCALES, WIDGET_TOPICS, WIDGET_HTML_MAX } from "./protocol.js";
|
|
29
|
+
import { PROTOCOL_VERSION, CHART_INTERVALS, CHART_INDICATORS, CHART_STYLES, CHART_SCALES, CHART_AXES, WIDGET_TOPICS, WIDGET_HTML_MAX } from "./protocol.js";
|
|
30
30
|
import { auditWidgetHtml, inlineHashes, normalizeWidgetHtml } from "./widget-html.js";
|
|
31
31
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
32
32
|
const PAGES_DIR = path.join(here, 'pages');
|
|
@@ -48,6 +48,8 @@ export const POPUP_EVENTS = new Set([
|
|
|
48
48
|
'event:popup.close-request', 'event:popup.focus', 'event:popup.drag',
|
|
49
49
|
// task-1325 chart v2 requests (payloads validated by validateEventPayload below)
|
|
50
50
|
'event:chart.interval', 'event:chart.indicator', 'event:chart.style', 'event:chart.scale', 'event:chart.painted',
|
|
51
|
+
// task-1472: the chart's Y axis (market cap ⇄ price): a display preference the terminal saves, nothing else
|
|
52
|
+
'event:chart.axis',
|
|
51
53
|
// task-1345 agent roster: per-agent pause / kick, by agent name (the only writes this popup can send)
|
|
52
54
|
'event:roster.pause', 'event:roster.resume', 'event:roster.kick',
|
|
53
55
|
// task-1355 order plans: cancel a plan by id (the ONLY write the plans popup can send — never arm/cap/sign)
|
|
@@ -94,6 +96,8 @@ export function validateEventPayload(name, payload) {
|
|
|
94
96
|
return oneOf(CHART_STYLES, p.style) ? { style: p.style } : null;
|
|
95
97
|
case 'chart.scale':
|
|
96
98
|
return oneOf(CHART_SCALES, p.scale) ? { scale: p.scale } : null;
|
|
99
|
+
case 'chart.axis':
|
|
100
|
+
return oneOf(CHART_AXES, p.axis) ? { axis: p.axis } : null;
|
|
97
101
|
case 'chart.painted':
|
|
98
102
|
return (p.kind === 'first' || p.kind === 'data') && finiteIn(p.ms, 0, 600_000) && finiteIn(p.sentAt, 0, 1e15)
|
|
99
103
|
? { kind: p.kind, ms: p.ms, sentAt: p.sentAt } : null;
|
|
@@ -38,7 +38,7 @@ export function queryDevHistory(db, q) {
|
|
|
38
38
|
creator: q.creator, status: 'unknown', asOf, source: DEV_HISTORY_SOURCE,
|
|
39
39
|
launches: { n: 0, newestT: null, ageSec: null },
|
|
40
40
|
labelled: { n: 0, rug: 0, dead: 0, fade: 0, flat: 0, runner: 0, horizon: '72h preferred', tier: 'keyless-proxy', newestT: null, ageSec: null },
|
|
41
|
-
rugRate: null, badRate: null, devExit: { n: 0, medianMin: null, within60m: 0 }, serialRugger: false, recent: [],
|
|
41
|
+
rugRate: null, badRate: null, devExit: { n: 0, medianMin: null, within60m: 0, newestT: null, ageSec: null }, serialRugger: false, recent: [],
|
|
42
42
|
};
|
|
43
43
|
let links;
|
|
44
44
|
try {
|
|
@@ -56,6 +56,7 @@ export function queryDevHistory(db, q) {
|
|
|
56
56
|
const counts = { rug: 0, dead: 0, fade: 0, flat: 0, runner: 0 };
|
|
57
57
|
let labelledN = 0, labelledNewest = null;
|
|
58
58
|
const exits = [];
|
|
59
|
+
let exitNewest = null;
|
|
59
60
|
const launches = [];
|
|
60
61
|
for (const l of links) {
|
|
61
62
|
const firstT = Number(l.first_t);
|
|
@@ -93,6 +94,7 @@ export function queryDevHistory(db, q) {
|
|
|
93
94
|
const from = createdAt ?? Number(series[held].t);
|
|
94
95
|
devExitMin = Math.max(0, (Number(out.t) - from) / 60_000);
|
|
95
96
|
exits.push(devExitMin);
|
|
97
|
+
exitNewest = Math.max(exitNewest ?? 0, Number(out.t));
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
}
|
|
@@ -109,12 +111,14 @@ export function queryDevHistory(db, q) {
|
|
|
109
111
|
labelled: { ...empty.labelled, n: labelledN, ...counts, newestT: labelledNewest, ageSec: labelledNewest === null ? null : Math.max(0, Math.round((asOf - labelledNewest) / 1000)) },
|
|
110
112
|
rugRate: labelledN ? counts.rug / labelledN : null,
|
|
111
113
|
badRate: labelledN ? (counts.rug + counts.dead) / labelledN : null,
|
|
112
|
-
devExit: { n: exits.length, medianMin: median(exits), within60m: exits.filter((m) => m <= 60).length },
|
|
114
|
+
devExit: { n: exits.length, medianMin: median(exits), within60m: exits.filter((m) => m <= 60).length, newestT: exitNewest, ageSec: exitNewest === null ? null : Math.max(0, Math.round((asOf - exitNewest) / 1000)) },
|
|
113
115
|
serialRugger: counts.rug >= SERIAL_RUGGER_MIN,
|
|
114
116
|
recent: launches,
|
|
115
117
|
};
|
|
116
118
|
}
|
|
117
|
-
|
|
119
|
+
/** Compact age for a panel cell: `5m`, `3h`, `40d`; `age ?` when unknown. */
|
|
120
|
+
export const ageShort = (sec) => sec === null ? 'age ?' : sec < 3600 ? `${Math.max(1, Math.round(sec / 60))}m` : sec < 172800 ? `${Math.round(sec / 3600)}h` : `${Math.round(sec / 86400)}d`;
|
|
121
|
+
const ago = (sec) => sec === null ? 'age unknown' : `${ageShort(sec)} ago`;
|
|
118
122
|
/**
|
|
119
123
|
* The one-line summary every surface shows (DEV panel, rug-signal row, MCP note): counts with the sample size and
|
|
120
124
|
* the age, or `unknown` with why. Plain wording: a risk flag from observed history, never "rug confirmed" or "safe".
|
|
@@ -66,6 +66,11 @@ export class Canvas {
|
|
|
66
66
|
*/
|
|
67
67
|
images;
|
|
68
68
|
imageMode;
|
|
69
|
+
/**
|
|
70
|
+
* task-1466: where each keyed list row was drawn (a table with `rowKey`, e.g. a screener row by mint), so an
|
|
71
|
+
* overlay drawn later (the agent HUD's row glow / cursor) can find the row on screen. Not emitted anywhere.
|
|
72
|
+
*/
|
|
73
|
+
rowMarks;
|
|
69
74
|
constructor(w, h, theme) {
|
|
70
75
|
this.w = w;
|
|
71
76
|
this.h = h;
|
|
@@ -82,6 +87,7 @@ export class Canvas {
|
|
|
82
87
|
this.shields = [];
|
|
83
88
|
this.images = [];
|
|
84
89
|
this.imageMode = 'cells';
|
|
90
|
+
this.rowMarks = [];
|
|
85
91
|
}
|
|
86
92
|
idx(x, y) { return y * this.w + x; }
|
|
87
93
|
/** Copy every buffer of `src` (same size) into this canvas. */
|
|
@@ -97,6 +103,7 @@ export class Canvas {
|
|
|
97
103
|
this.shields = src.shields.slice();
|
|
98
104
|
this.images = src.images.slice();
|
|
99
105
|
this.imageMode = src.imageMode;
|
|
106
|
+
this.rowMarks = src.rowMarks.slice();
|
|
100
107
|
}
|
|
101
108
|
put(x, y, ch, st = {}) {
|
|
102
109
|
if (x < 0 || y < 0 || x >= this.w || y >= this.h)
|
|
@@ -119,6 +119,59 @@ export function fmtMcap(n) {
|
|
|
119
119
|
return bare;
|
|
120
120
|
return bare.startsWith('-') ? `-$${bare.slice(1)}` : `$${bare}`;
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* task-1472: THE chart number formatter (Y axis, crosshair/legend, last-value tag, volume): compact with a LOWERCASE k
|
|
124
|
+
* for thousands, then M, B, T. One decimal while the value is below 10 of its unit, none after, trailing `.0` dropped:
|
|
125
|
+
* 4930 → 4.9k, 42100 → 42k, 487000 → 487k, 1.23e6 → 1.2M, 4.8e7 → 48M, 1.3e9 → 1.3B, 999 → 999. The unit is promoted
|
|
126
|
+
* AFTER rounding, so 999 900 reads 1M (never 1000k). Below 1 it is the subscript price form (0.0000123 → 0.0₄123):
|
|
127
|
+
* never scientific notation, never a long run of zeros. `extra` (0..3) adds decimals when an axis needs them to keep
|
|
128
|
+
* two neighbouring ticks apart (see compactDecimalsFor). No currency symbol; fmtCompactUsd adds the `$`.
|
|
129
|
+
*/
|
|
130
|
+
export function fmtCompact(n, extra = 0) {
|
|
131
|
+
if (n == null || !Number.isFinite(n))
|
|
132
|
+
return '—';
|
|
133
|
+
if (n === 0)
|
|
134
|
+
return '0';
|
|
135
|
+
const sign = n < 0 ? '-' : '';
|
|
136
|
+
const a = Math.abs(n);
|
|
137
|
+
if (a < 1)
|
|
138
|
+
return sign + trimZeros(fmtPriceBare(a));
|
|
139
|
+
const units = ['', 'k', 'M', 'B', 'T'];
|
|
140
|
+
const x = Math.max(0, Math.min(3, Math.floor(extra)));
|
|
141
|
+
const round = (q) => Number(q.toFixed((q < 10 ? 1 : 0) + x));
|
|
142
|
+
let u = 0, v = a;
|
|
143
|
+
while (u < units.length - 1 && v >= 1000) {
|
|
144
|
+
v /= 1000;
|
|
145
|
+
u++;
|
|
146
|
+
}
|
|
147
|
+
let r = round(v);
|
|
148
|
+
if (r >= 1000 && u < units.length - 1) {
|
|
149
|
+
u++;
|
|
150
|
+
r = round(r / 1000);
|
|
151
|
+
}
|
|
152
|
+
return `${sign}${trimZeros(r.toFixed((r < 10 ? 1 : 0) + x))}${units[u]}`;
|
|
153
|
+
}
|
|
154
|
+
/** USD form of fmtCompact: `$4.9k`, `$48M`, `-$1.2k`. */
|
|
155
|
+
export function fmtCompactUsd(n, extra = 0) {
|
|
156
|
+
const bare = fmtCompact(n, extra);
|
|
157
|
+
if (bare === '—')
|
|
158
|
+
return bare;
|
|
159
|
+
return bare.startsWith('-') ? `-$${bare.slice(1)}` : `$${bare}`;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* task-1472: the extra decimals (0..3) fmtCompact needs so that axis ticks `step` apart around `around` never print
|
|
163
|
+
* the same label (a 48.1M–48.6M range would otherwise read 48M on every tick). 0 for a normal memecoin range.
|
|
164
|
+
*/
|
|
165
|
+
export function compactDecimalsFor(around, step) {
|
|
166
|
+
if (!Number.isFinite(around) || !Number.isFinite(step) || step <= 0 || Math.abs(around) < 1)
|
|
167
|
+
return 0;
|
|
168
|
+
for (let x = 0; x < 3; x++) {
|
|
169
|
+
const a = fmtCompact(around, x);
|
|
170
|
+
if (a !== fmtCompact(around + step, x) && a !== fmtCompact(around - step, x))
|
|
171
|
+
return x;
|
|
172
|
+
}
|
|
173
|
+
return 3;
|
|
174
|
+
}
|
|
122
175
|
export function fmtPct(n, { sign = true, digits = 1 } = {}) {
|
|
123
176
|
if (n == null || Number.isNaN(n))
|
|
124
177
|
return '—';
|
|
@@ -268,6 +268,10 @@ export const DEFAULT_KEYMAP = new Keymap([
|
|
|
268
268
|
// the open coin. (`P` is already the global positions key, so chart uses `C`.) Collision-free with the
|
|
269
269
|
// token screen's other capitals (B/S trade, W/X/T/L links, V vet).
|
|
270
270
|
{ keys: ['C'], action: 'popup.chart', label: 'chart popup', screen: 'token' },
|
|
271
|
+
// task-1472: `A` (axis) on the token screen flips the chart Y axis MARKET CAP ⇄ PRICE (both charts; saved as
|
|
272
|
+
// display.chartAxis). Free capital on the token screen (A is only auto-mirror on the follows screen); deliberately
|
|
273
|
+
// not Y, which confirms inside the PAPER ticket and the modals. The chart popup uses the same `A`.
|
|
274
|
+
{ keys: ['A'], action: 'display.chartAxis', label: 'chart axis mcap ⇄ price', screen: 'token' },
|
|
271
275
|
// task-1359: `I` on the token screen opens/toggles the TOKEN-DETAIL HUD (identity, market, risk, snipers/bundled
|
|
272
276
|
// from the wallet-intel scan); `M` on the trending screen opens/toggles the FEEDS HUD (the keyless social feed +
|
|
273
277
|
// official news, newest first; ⏎ in the window opens that coin here). Both are read-only windows. `I` is unused
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// background (hard cells); glow is only ever applied to neighbouring blank cells or to bg tints.
|
|
6
6
|
import { Canvas } from "./canvas.js";
|
|
7
7
|
import { mix, luminance } from "./color.js";
|
|
8
|
-
import { fmtUsd, fmtPct, fmtNum, fmtPrice, fmtAgeSec, shortAddr, clamp } from "./fmt.js";
|
|
8
|
+
import { fmtUsd, fmtPct, fmtNum, fmtPrice, fmtPriceBare, fmtCompactUsd, compactDecimalsFor, fmtAgeSec, shortAddr, clamp } from "./fmt.js";
|
|
9
9
|
import { launchpadColor } from "./theme.js";
|
|
10
10
|
export const BLOCKS = '▁▂▃▄▅▆▇█';
|
|
11
11
|
// braille dot bits: column 0 rows 0..3, column 1 rows 0..3 (row 0 = top)
|
|
@@ -107,6 +107,7 @@ export function brailleLine(c, x, y, w, h, values, opts = {}) {
|
|
|
107
107
|
c.put(x + col, y + r, String.fromCharCode(0x2800 + bits), { fg: neon(color, col / (w - 1), bg), bg, soft: true });
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
+
/** task-1472: the price axis never prints scientific notation: subscript zeros below 0.01 (0.0₅493), like fmtPrice. */
|
|
110
111
|
function fmtPriceAxis(p) {
|
|
111
112
|
if (p >= 1000)
|
|
112
113
|
return p.toFixed(0);
|
|
@@ -114,7 +115,7 @@ function fmtPriceAxis(p) {
|
|
|
114
115
|
return p.toFixed(3);
|
|
115
116
|
if (p >= 0.001)
|
|
116
117
|
return p.toFixed(5);
|
|
117
|
-
return p
|
|
118
|
+
return fmtPriceBare(p);
|
|
118
119
|
}
|
|
119
120
|
/**
|
|
120
121
|
* Candlestick chart with right-hand price axis, last-price tag, volume strip and time axis.
|
|
@@ -137,7 +138,6 @@ export function candleChart(c, x, y, w, h, candles, opts = {}) {
|
|
|
137
138
|
// task-1366: the bars are laid out in price units either way (identical glyphs); only the labels are scaled
|
|
138
139
|
const s = opts.scale && Number.isFinite(opts.scale) && opts.scale > 0 ? opts.scale : 1;
|
|
139
140
|
const baseFmt = opts.axisFmt ?? fmtPriceAxis;
|
|
140
|
-
const fmtAxis = s === 1 ? baseFmt : (p) => baseFmt(p * s);
|
|
141
141
|
let lo = Infinity, hi = -Infinity, vmax = 0;
|
|
142
142
|
for (const k of data) {
|
|
143
143
|
lo = Math.min(lo, k.l);
|
|
@@ -152,6 +152,9 @@ export function candleChart(c, x, y, w, h, candles, opts = {}) {
|
|
|
152
152
|
const gridStyle = { fg: mix(bg, t.border, 0.9), bg, soft: true };
|
|
153
153
|
const labelStyle = { fg: t.muted, bg };
|
|
154
154
|
const ticks = Math.max(2, Math.min(6, Math.floor(plotH / 3)));
|
|
155
|
+
// task-1472: one more decimal only when two neighbouring ticks would otherwise print the same compact label
|
|
156
|
+
const extra = opts.axisFmt ? compactDecimalsFor(((hi + lo) / 2) * s, (range / ticks) * s) : 0;
|
|
157
|
+
const fmtAxis = (p) => baseFmt(p * s, extra);
|
|
155
158
|
for (let i = 0; i <= ticks; i++) {
|
|
156
159
|
const row = Math.round((i / ticks) * (plotH - 1));
|
|
157
160
|
const price = hi - (row / (plotH - 1)) * range;
|
|
@@ -245,7 +248,7 @@ export function candleChart(c, x, y, w, h, candles, opts = {}) {
|
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
250
|
c.text(x + plotW + 1, vy, 'vol', labelStyle);
|
|
248
|
-
c.text(x + plotW + 1, vy + 1,
|
|
251
|
+
c.text(x + plotW + 1, vy + 1, fmtCompactUsd(last.v), { fg: t.text, bg }); // task-1472: volume stays USD, compact k/M
|
|
249
252
|
}
|
|
250
253
|
const ty = y + h - 1;
|
|
251
254
|
const labels = Math.max(2, Math.floor(plotW / 18));
|
|
@@ -386,6 +389,8 @@ export function table(c, x, y, w, h, spec) {
|
|
|
386
389
|
const selected = ri === spec.selected;
|
|
387
390
|
const rowBg = selected ? t.selBg : (r % 2 === 1 && spec.zebra !== false ? mix(bg, t.panelAlt, 1) : bg);
|
|
388
391
|
c.fill(x, ry, w, 1, ' ', { bg: rowBg });
|
|
392
|
+
if (spec.rowKey)
|
|
393
|
+
c.rowMarks.push({ key: spec.rowKey(row), x, y: ry, w });
|
|
389
394
|
if (selected)
|
|
390
395
|
c.put(x - 1 >= 0 ? x - 1 : x, ry, '▎', { fg: t.accent, bg: rowBg, soft: true });
|
|
391
396
|
cx = x;
|