dexterm-cli 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/dist/packages/agents/src/spectator.js +31 -3
  2. package/dist/packages/agents/src/tiers.js +12 -0
  3. package/dist/packages/agents/src/tools.js +13 -5
  4. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  5. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  6. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  7. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  8. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  9. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  10. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  11. package/dist/packages/autotrade/src/backtest.js +7 -2
  12. package/dist/packages/autotrade/src/chat/config.js +4 -2
  13. package/dist/packages/autotrade/src/chat/control.js +1 -0
  14. package/dist/packages/autotrade/src/chat/notify.js +16 -6
  15. package/dist/packages/autotrade/src/chat/secrets.js +25 -18
  16. package/dist/packages/autotrade/src/chat/service.js +3 -0
  17. package/dist/packages/autotrade/src/engine.js +16 -2
  18. package/dist/packages/autotrade/src/executor-health.js +217 -0
  19. package/dist/packages/autotrade/src/fleet.js +5 -2
  20. package/dist/packages/autotrade/src/forward.js +355 -0
  21. package/dist/packages/autotrade/src/index.js +21 -0
  22. package/dist/packages/autotrade/src/journey.js +477 -0
  23. package/dist/packages/autotrade/src/limits.js +682 -0
  24. package/dist/packages/autotrade/src/outbox.js +394 -0
  25. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  26. package/dist/packages/autotrade/src/real-mode.js +688 -0
  27. package/dist/packages/autotrade/src/receipts.js +330 -0
  28. package/dist/packages/autotrade/src/report.js +168 -0
  29. package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
  30. package/dist/packages/autotrade/src/rugproof.js +231 -0
  31. package/dist/packages/autotrade/src/runner.js +194 -28
  32. package/dist/packages/autotrade/src/service-guard.js +125 -0
  33. package/dist/packages/autotrade/src/service-install.js +215 -0
  34. package/dist/packages/autotrade/src/service-recover.js +232 -0
  35. package/dist/packages/autotrade/src/service.js +289 -0
  36. package/dist/packages/autotrade/src/sessions.js +16 -7
  37. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  38. package/dist/packages/autotrade/src/signal.js +196 -0
  39. package/dist/packages/autotrade/src/strategy-stats.js +344 -0
  40. package/dist/packages/autotrade/src/strategy.js +15 -2
  41. package/dist/packages/autotrade/src/trade-ledger.js +2 -2
  42. package/dist/packages/classifier/src/calibrate.js +138 -0
  43. package/dist/packages/classifier/src/classify.js +107 -0
  44. package/dist/packages/classifier/src/config.js +220 -0
  45. package/dist/packages/classifier/src/dossier.js +95 -0
  46. package/dist/packages/classifier/src/hardware.js +63 -0
  47. package/dist/packages/classifier/src/index.js +18 -0
  48. package/dist/packages/classifier/src/models.js +111 -0
  49. package/dist/packages/classifier/src/providers/heuristic.js +77 -0
  50. package/dist/packages/classifier/src/providers/http.js +63 -0
  51. package/dist/packages/classifier/src/providers/jev-open.js +86 -0
  52. package/dist/packages/classifier/src/providers/llm.js +89 -0
  53. package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
  54. package/dist/packages/classifier/src/questions.js +97 -0
  55. package/dist/packages/classifier/src/registry.js +42 -0
  56. package/dist/packages/classifier/src/snapshot.js +31 -0
  57. package/dist/packages/classifier/src/systemone.js +88 -0
  58. package/dist/packages/classifier/src/types.js +18 -0
  59. package/dist/packages/cli/assets/dash/dash.css +139 -0
  60. package/dist/packages/cli/assets/dash/dash.js +200 -0
  61. package/dist/packages/cli/assets/dash/icon.svg +1 -0
  62. package/dist/packages/cli/assets/dash/index.html +36 -0
  63. package/dist/packages/cli/src/analyst/backend.js +3 -2
  64. package/dist/packages/cli/src/analyst/codex.js +3 -2
  65. package/dist/packages/cli/src/app-help-gen.js +26 -2
  66. package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
  67. package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
  68. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  69. package/dist/packages/cli/src/autotrade-cmd/fleet.js +16 -5
  70. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  71. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  72. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  73. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  74. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  75. package/dist/packages/cli/src/autotrade-cmd/run.js +268 -14
  76. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  77. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  78. package/dist/packages/cli/src/autotrade-cmd.js +50 -4
  79. package/dist/packages/cli/src/classifier-cmd.js +353 -0
  80. package/dist/packages/cli/src/classifier-live.js +81 -0
  81. package/dist/packages/cli/src/dash/data.js +217 -0
  82. package/dist/packages/cli/src/dash/server.js +92 -0
  83. package/dist/packages/cli/src/dash-cmd.js +113 -0
  84. package/dist/packages/cli/src/doctor.js +34 -0
  85. package/dist/packages/cli/src/ledger-cmd.js +4 -2
  86. package/dist/packages/cli/src/live/screens/token-live.js +4 -0
  87. package/dist/packages/cli/src/live/signals.js +8 -0
  88. package/dist/packages/cli/src/main.js +98 -6
  89. package/dist/packages/cli/src/mcp-follow.js +21 -1
  90. package/dist/packages/cli/src/narratives-cmd.js +147 -0
  91. package/dist/packages/cli/src/narratives.js +114 -0
  92. package/dist/packages/cli/src/overlay/model.js +147 -0
  93. package/dist/packages/cli/src/overlay/page.js +146 -0
  94. package/dist/packages/cli/src/overlay/pnl.js +75 -0
  95. package/dist/packages/cli/src/overlay/server.js +170 -0
  96. package/dist/packages/cli/src/overlay-cmd.js +367 -0
  97. package/dist/packages/cli/src/pnl-card.js +4 -0
  98. package/dist/packages/cli/src/record-cmd.js +82 -11
  99. package/dist/packages/cli/src/rules-cmd.js +2 -1
  100. package/dist/packages/cli/src/rules.js +24 -4
  101. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  102. package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
  103. package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
  104. package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
  105. package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
  106. package/dist/packages/cli/src/screens/narrative.js +154 -2
  107. package/dist/packages/cli/src/state.js +1 -0
  108. package/dist/packages/cli/src/telegram-cmd.js +4 -1
  109. package/dist/packages/cli/src/version.js +1 -1
  110. package/dist/packages/custom/src/rules.js +5 -1
  111. package/dist/packages/data/src/trade/ledger/schema.js +1 -1
  112. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  113. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  114. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  115. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  116. package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
  117. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
  118. package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
  119. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
  120. package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
  121. package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
  122. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
  123. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
  124. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
  125. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
  126. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
  127. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
  128. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +59 -0
  129. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
  130. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
  131. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
  132. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
  133. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  134. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
  135. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
  136. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
  137. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
  138. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  139. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
  140. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  141. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  142. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  143. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  144. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  145. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +41 -0
  146. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  147. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  148. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +26 -0
  149. package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
  150. package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
  151. package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
  152. package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
  153. package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
  154. package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
  155. package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
  156. package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
  157. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
  158. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
  159. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  160. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
  161. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  162. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
  163. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
  164. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
  165. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  166. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  167. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  168. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  169. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  170. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  171. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  172. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  173. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
  174. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
  175. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
  176. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
  177. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  178. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
  179. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
  180. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
  181. package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
  182. package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
  183. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
  184. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
  186. package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
  188. package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
  189. package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
  194. package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
  195. package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
  197. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  198. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  200. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  201. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +37 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
  207. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
  209. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
  210. package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
  211. package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
  212. package/dist/packages/mcp/src/autotrade-tools.js +168 -5
  213. package/dist/packages/mcp/src/engine/restrictions.js +3 -3
  214. package/dist/packages/mcp/src/index.js +1 -1
  215. package/dist/packages/mcp/src/ledger-tools.js +155 -7
  216. package/dist/packages/mcp/src/server.js +125 -10
  217. package/dist/packages/mcp/src/spectator-sink.js +27 -0
  218. package/dist/packages/narratives/src/cluster.js +193 -0
  219. package/dist/packages/narratives/src/embed.js +184 -0
  220. package/dist/packages/narratives/src/export.js +60 -0
  221. package/dist/packages/narratives/src/index.js +10 -0
  222. package/dist/packages/narratives/src/label.js +48 -0
  223. package/dist/packages/narratives/src/normalize.js +179 -0
  224. package/dist/packages/narratives/src/recorder.js +93 -0
  225. package/dist/packages/narratives/src/tracker.js +328 -0
  226. package/dist/packages/recorder/src/exit-probes.js +215 -0
  227. package/dist/packages/recorder/src/index.js +1 -0
  228. package/dist/packages/recorder/src/recorder.js +11 -1
  229. package/dist/packages/recorder/src/retention.js +4 -0
  230. package/dist/packages/recorder/src/schema.js +20 -1
  231. package/dist/packages/tui/src/keymap.js +12 -0
  232. package/package.json +3 -1
  233. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
@@ -0,0 +1,42 @@
1
+ // Config -> Provider. The CLI and MCP server call `providerFromConfig`; the LLM runners (Claude CLI, local Ollama)
2
+ // are injected by the caller so this package never spawns a model process or opens a socket on its own.
3
+ import fs from 'node:fs';
4
+ import { fileUsage, readTypesafeKey } from "./config.js";
5
+ import { heuristicProvider } from "./providers/heuristic.js";
6
+ import { jevOpenProvider } from "./providers/jev-open.js";
7
+ import { llmProvider } from "./providers/llm.js";
8
+ import { jevHostedProvider, kevLocalProvider } from "./providers/systemone-http.js";
9
+ import { ClassifierError } from "./types.js";
10
+ export const PROVIDERS = [
11
+ { id: 'heuristic', title: 'Built-in scores (H1 p(rug) + LARP link score)', network: 'none', needs: 'nothing' },
12
+ { id: 'kev-local', title: 'Kev decision model via llama.cpp (open, Apache-2.0)', network: 'local', needs: 'a downloaded Kev GGUF + llama-server' },
13
+ { id: 'jev-hosted', title: "TypeSafe's hosted Jev (flagship, your own API key)", network: 'remote', needs: 'your TypeSafe API key' },
14
+ { id: 'jev-open', title: 'JEV open students (autotrust/JEV-*) via vLLM', network: 'local', needs: 'a vLLM server with the JEV decision adapter (>= 24 GB GPU)' },
15
+ { id: 'claude', title: 'Claude zero-shot (comparison baseline)', network: 'remote', needs: 'the Claude CLI, signed in' },
16
+ { id: 'ollama-llm', title: 'Ollama chat model zero-shot (comparison baseline)', network: 'local', needs: 'Ollama with a chat model' },
17
+ ];
18
+ export function providerFromConfig(c, id = c.provider, d = {}) {
19
+ switch (id) {
20
+ case 'heuristic': return heuristicProvider();
21
+ case 'kev-local': return kevLocalProvider({ endpoint: c.kev.endpoint, modelId: c.kev.model, fetch: d.fetch });
22
+ case 'jev-hosted': return jevHostedProvider({
23
+ endpoint: c.jev.endpoint, model: c.jev.model, apiKey: () => readTypesafeKey(d.env), pricePerMTokUsd: c.jev.pricePerMTokUsd,
24
+ maxCallsPerDay: c.jev.maxCallsPerDay, usage: fileUsage(d.env), fetch: d.fetch,
25
+ });
26
+ case 'jev-open': {
27
+ if (!c.jevOpen.headPath || !c.jevOpen.calibrationPath)
28
+ throw new ClassifierError('not_ready', 'jev-open needs jevOpen.headPath (adapter_vllm/decision_head.json) and jevOpen.calibrationPath (calibration.json) from the model repo');
29
+ const head = JSON.parse(fs.readFileSync(c.jevOpen.headPath, 'utf8'));
30
+ const calibration = JSON.parse(fs.readFileSync(c.jevOpen.calibrationPath, 'utf8'));
31
+ return jevOpenProvider({ endpoint: c.jevOpen.endpoint, modelId: c.jevOpen.model, head, calibration, fetch: d.fetch });
32
+ }
33
+ case 'claude':
34
+ if (!d.claudeRunner)
35
+ throw new ClassifierError('not_ready', 'claude baseline: no Claude runner wired');
36
+ return llmProvider({ id: 'claude', model: c.llm.claudeModel, run: d.claudeRunner });
37
+ case 'ollama-llm':
38
+ if (!d.ollamaRunner || !c.llm.ollamaModel)
39
+ throw new ClassifierError('not_ready', 'ollama-llm baseline: set llm.ollamaModel and have Ollama running');
40
+ return llmProvider({ id: 'ollama-llm', model: c.llm.ollamaModel, run: d.ollamaRunner });
41
+ }
42
+ }
@@ -0,0 +1,31 @@
1
+ // The app's live token row (@dexterm/data TokenSnapshot: token + pair + stats + audit) -> CoinInput, so the screener,
2
+ // the MCP tools and watch rules all build the same dossier the benchmark scored.
3
+ import { coinDossier } from "./dossier.js";
4
+ const n = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
5
+ export function coinInputFromSnapshot(s, now = Date.now()) {
6
+ const p = s.pair, st = s.stats, a = s.audit;
7
+ const created = n(s.token.createdAt) ?? n(p?.createdAt);
8
+ const h1 = st?.perWindow?.h1 ?? null;
9
+ return {
10
+ mint: s.mint ?? s.token.mint,
11
+ symbol: s.token.symbol ?? null,
12
+ name: s.token.name ?? null,
13
+ age_min: created === null ? null : Math.max(0, (now - created) / 60_000),
14
+ liquidity_usd: n(p?.liquidityUsd),
15
+ mcap_usd: n(p?.mcap) ?? n(p?.fdv),
16
+ vol5m_usd: n(p?.volume?.m5),
17
+ vol1h_usd: n(p?.volume?.h1),
18
+ buys5m: n(p?.txns?.m5?.buys), sells5m: n(p?.txns?.m5?.sells),
19
+ buys1h: n(p?.txns?.h1?.buys), sells1h: n(p?.txns?.h1?.sells),
20
+ change5m_pct: n(p?.priceChange?.m5), change1h_pct: n(p?.priceChange?.h1),
21
+ holders: n(st?.holderCount), organic_score: n(st?.organicScore),
22
+ traders1h: n(h1?.numTraders), net_buyers1h: n(h1?.numNetBuyers),
23
+ top10_pct: n(a?.top10Pct), dev_pct: n(a?.devPct), dev_other_mints: n(a?.devMints),
24
+ mint_authority: a?.mintAuthority ?? null, freeze_authority: a?.freezeAuthority ?? null,
25
+ launchpad: typeof s.token.launchpad === 'string' ? s.token.launchpad : null,
26
+ dex: p?.dex ?? null,
27
+ };
28
+ }
29
+ export function dossierFromSnapshot(s, now = Date.now()) {
30
+ return coinDossier(coinInputFromSnapshot(s, now));
31
+ }
@@ -0,0 +1,88 @@
1
+ // The System One wire protocol (`POST /v1/systemone`): TypeSafe's public API, which llama.cpp's llama-server
2
+ // (PR #29818, 2026-10-02) and the Kev reference server also implement. One request = one state + named questions.
3
+ //
4
+ // Request : { model?, state, questions: { id: { type, instructions, criteria? } } }
5
+ // Response: { model, answers: { id: { type, choice?|noul?|score?, probabilities?, confidence?, legend? } }, usage }
6
+ //
7
+ // Normalised here into `Answer` (probs per label + ln-p logits + top + confidence). Anything malformed raises
8
+ // ClassifierError('bad_response') rather than guessing.
9
+ import { labelsOf } from "./questions.js";
10
+ import { ClassifierError } from "./types.js";
11
+ export function buildRequest(set, state, model) {
12
+ const questions = {};
13
+ for (const q of set.questions) {
14
+ const entry = { type: q.type, instructions: q.instructions };
15
+ if (q.criteria !== undefined)
16
+ entry.criteria = q.criteria;
17
+ questions[q.id] = entry;
18
+ }
19
+ return model ? { model, state, questions } : { state, questions };
20
+ }
21
+ const EPS = 1e-12;
22
+ /** Entropy-based confidence (1 - H/ln n): 0 for a uniform answer, 1 for a certain one. */
23
+ export function entropyConfidence(probs) {
24
+ const ps = Object.values(probs);
25
+ if (ps.length < 2)
26
+ return 1;
27
+ const h = -ps.reduce((s, p) => s + (p > 0 ? p * Math.log(p) : 0), 0);
28
+ return Math.max(0, Math.min(1, 1 - h / Math.log(ps.length)));
29
+ }
30
+ export function finishAnswer(type, probs, confidence) {
31
+ const sum = Object.values(probs).reduce((a, b) => a + b, 0);
32
+ if (!(sum > 0))
33
+ throw new ClassifierError('bad_response', 'answer has no probability mass');
34
+ const norm = {};
35
+ const logits = {};
36
+ let top = '';
37
+ let best = -1;
38
+ for (const [k, v] of Object.entries(probs)) {
39
+ const p = v / sum;
40
+ norm[k] = p;
41
+ logits[k] = Math.log(Math.max(p, EPS));
42
+ if (p > best) {
43
+ best = p;
44
+ top = k;
45
+ }
46
+ }
47
+ const conf = typeof confidence === 'number' && Number.isFinite(confidence) ? Math.max(0, Math.min(1, confidence)) : entropyConfidence(norm);
48
+ return { type, probs: norm, logits, top, confidence: conf };
49
+ }
50
+ function num(v) {
51
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
52
+ }
53
+ /** Parse a /v1/systemone response body against the question set that was asked. */
54
+ export function parseResponse(set, body) {
55
+ if (!body || typeof body !== 'object')
56
+ throw new ClassifierError('bad_response', 'response is not an object');
57
+ const b = body;
58
+ if (!b.answers || typeof b.answers !== 'object')
59
+ throw new ClassifierError('bad_response', 'response has no answers');
60
+ const raw = b.answers;
61
+ const answers = {};
62
+ for (const q of set.questions) {
63
+ const a = raw[q.id];
64
+ if (!a || typeof a !== 'object')
65
+ throw new ClassifierError('bad_response', `no answer for ${q.id}`);
66
+ if (q.type === 'noul') {
67
+ const p = num(a.noul);
68
+ if (p === null || p > 1)
69
+ throw new ClassifierError('bad_response', `${q.id}: noul is not a probability`);
70
+ answers[q.id] = finishAnswer('noul', { yes: p, no: 1 - p }, null);
71
+ continue;
72
+ }
73
+ const pr = a.probabilities;
74
+ if (!pr || typeof pr !== 'object')
75
+ throw new ClassifierError('bad_response', `${q.id}: no probabilities`);
76
+ const labels = labelsOf(q);
77
+ const probs = {};
78
+ for (const l of labels) {
79
+ const p = num(pr[l]);
80
+ if (p === null)
81
+ throw new ClassifierError('bad_response', `${q.id}: missing probability for ${l}`);
82
+ probs[l] = p;
83
+ }
84
+ answers[q.id] = finishAnswer(q.type, probs, num(a.confidence));
85
+ }
86
+ const tokens = num(b.usage?.input_tokens);
87
+ return { model: typeof b.model === 'string' ? b.model : '', answers, inputTokens: tokens };
88
+ }
@@ -0,0 +1,18 @@
1
+ // Shared types for @dexterm/classifier (task-1822).
2
+ //
3
+ // A classifier answers TYPED QUESTIONS about ONE DOCUMENT (the coin dossier) with a probability per label. The
4
+ // question shapes follow the System One API (`POST /v1/systemone`, TypeSafe's public API, also served by llama.cpp
5
+ // for Kev GGUFs): `choice` (one of named options), `noul` (yes/no, probability of yes), `score` (ordered levels).
6
+ // Every result names the provider, model id/version and how its probabilities were calibrated, and is labelled a
7
+ // model estimate. Nothing here signs, trades, arms anything or raises a cap.
8
+ export const PROVIDER_IDS = ['heuristic', 'kev-local', 'jev-hosted', 'jev-open', 'claude', 'ollama-llm'];
9
+ export class ClassifierError extends Error {
10
+ code;
11
+ retryAfterMs;
12
+ constructor(code, message, retryAfterMs = null) {
13
+ super(message);
14
+ this.name = 'ClassifierError';
15
+ this.code = code;
16
+ this.retryAfterMs = retryAfterMs;
17
+ }
18
+ }
@@ -0,0 +1,139 @@
1
+ /* dexterm dash (task-1749): the strategy performance dashboard. dexterm's terminal look (site/src/css/site.css tokens) in
2
+ dark, a paper-white counterpart in light. Mobile first; no web fonts, no external URL. */
3
+ :root {
4
+ color-scheme: light;
5
+ --bg: #f6f7f4; --panel: #ffffff; --panel-2: #eef1ec; --text: #0b120e; --muted: #47524b; --line: #d5dbd4; --line-2: #b9c3ba;
6
+ --accent: #006b78; --series: #0f7a34; --series-soft: rgba(15, 122, 52, .14); --dd: #b4233f; --dd-soft: rgba(180, 35, 63, .14);
7
+ --up: #0b6e2e; --down: #b0173d; --warn: #7a5a00; --ok-bg: #e2f3e6; --warn-bg: #fbf0cf; --down-bg: #f9e0e5; --idle-bg: #ebedea;
8
+ --focus: #006b78; --badge: #5a4300; --badge-bg: #fdf3c6;
9
+ --mono: ui-monospace, 'SF Mono', Menlo, Monaco, 'Cascadia Code', 'DejaVu Sans Mono', monospace;
10
+ --sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
11
+ --r: 10px; --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px;
12
+ }
13
+ @media (prefers-color-scheme: dark) {
14
+ :root:where(:not([data-theme="light"])) {
15
+ color-scheme: dark;
16
+ --bg: #020507; --panel: #04090c; --panel-2: #071114; --text: #eafff0; --muted: #9fd8b2; --line: #0c3a24; --line-2: #1b5a3a;
17
+ --accent: #00f0ff; --series: #39ff14; --series-soft: rgba(57, 255, 20, .14); --dd: #ff5c82; --dd-soft: rgba(255, 92, 130, .16);
18
+ --up: #5dff6e; --down: #ff6b8b; --warn: #f5ff2d; --ok-bg: #062512; --warn-bg: #2a2a05; --down-bg: #2d0812; --idle-bg: #0b1512;
19
+ --focus: #00f0ff; --badge: #f5ff2d; --badge-bg: #22230a;
20
+ }
21
+ }
22
+ :root[data-theme="dark"] {
23
+ color-scheme: dark;
24
+ --bg: #020507; --panel: #04090c; --panel-2: #071114; --text: #eafff0; --muted: #9fd8b2; --line: #0c3a24; --line-2: #1b5a3a;
25
+ --accent: #00f0ff; --series: #39ff14; --series-soft: rgba(57, 255, 20, .14); --dd: #ff5c82; --dd-soft: rgba(255, 92, 130, .16);
26
+ --up: #5dff6e; --down: #ff6b8b; --warn: #f5ff2d; --ok-bg: #062512; --warn-bg: #2a2a05; --down-bg: #2d0812; --idle-bg: #0b1512;
27
+ --focus: #00f0ff; --badge: #f5ff2d; --badge-bg: #22230a;
28
+ }
29
+ * { box-sizing: border-box; }
30
+ html { -webkit-text-size-adjust: 100%; }
31
+ body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.45 var(--sans); }
32
+ a { color: var(--accent); }
33
+ :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
34
+ main:focus, main:focus-visible { outline: none; } /* the route target (tabindex -1), focused for screen readers only */
35
+ .skip { position: absolute; left: -999px; top: 0; }
36
+ .skip:focus { left: 8px; top: 8px; background: var(--panel); padding: 8px; z-index: 10; }
37
+ .mono, .num, table, .tile .v, .brand { font-family: var(--mono); }
38
+ .num { font-variant-numeric: tabular-nums; white-space: nowrap; }
39
+ .up { color: var(--up); } .down { color: var(--down); } .muted { color: var(--muted); }
40
+
41
+ header.top { position: sticky; top: 0; z-index: 5; background: var(--bg); border-bottom: 1px solid var(--line); }
42
+ .bar { display: flex; align-items: center; gap: var(--s3); padding: var(--s2) var(--s4); max-width: 1440px; margin: 0 auto; }
43
+ .brand { font-weight: 700; letter-spacing: .02em; font-size: 16px; white-space: nowrap; }
44
+ .brand b { color: var(--series); }
45
+ .paper { display: inline-block; border: 1px solid var(--line-2); border-radius: 999px; padding: 2px 10px; font: 600 12px/1.6 var(--mono); color: var(--text); background: var(--panel-2); }
46
+ .updated { margin-left: auto; font-size: 12px; color: var(--muted); text-align: right; }
47
+ .updated .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--series); margin-right: 6px; vertical-align: 1px; }
48
+ .updated.stale .dot { background: var(--down); }
49
+ button.theme { background: var(--panel-2); color: var(--text); border: 1px solid var(--line-2); border-radius: 8px; padding: 6px 10px; font: 13px var(--mono); min-height: 36px; cursor: pointer; }
50
+ nav.tabs { display: flex; gap: 2px; overflow-x: auto; padding: 0 var(--s3); max-width: 1440px; margin: 0 auto; scrollbar-width: none; }
51
+ nav.tabs a { flex: none; padding: 10px 12px; min-height: 44px; display: inline-flex; align-items: center; color: var(--muted); text-decoration: none; border-bottom: 2px solid transparent; font: 600 14px var(--sans); }
52
+ nav.tabs a[aria-current="page"] { color: var(--text); border-bottom-color: var(--series); }
53
+
54
+ main { max-width: 1440px; margin: 0 auto; padding: var(--s4); }
55
+ h1 { font-size: 20px; margin: 4px 0 4px; } h2 { font-size: 16px; margin: var(--s5) 0 var(--s2); } h3 { font-size: 14px; margin: 0 0 6px; }
56
+ p.lead { margin: 0 0 var(--s3); color: var(--muted); font-size: 14px; }
57
+ .disclaimer { border: 1px dashed var(--line-2); border-radius: var(--r); padding: var(--s2) var(--s3); font-size: 13px; color: var(--muted); margin: 0 0 var(--s4); }
58
+ .disclaimer strong { color: var(--text); }
59
+
60
+ .grid { display: grid; gap: var(--s3); grid-template-columns: 1fr; }
61
+ @media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } }
62
+ @media (min-width: 1080px) { .grid { grid-template-columns: repeat(3, 1fr); } }
63
+ @media (min-width: 1360px) { .grid.g4 { grid-template-columns: repeat(4, 1fr); } }
64
+ .card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); padding: var(--s3); min-width: 0; }
65
+ .card.base { border-color: var(--series); box-shadow: inset 0 0 0 1px var(--series); }
66
+ a.card { color: inherit; text-decoration: none; display: block; }
67
+ a.card:hover { border-color: var(--line-2); }
68
+ .card .head { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; }
69
+ .card .id { font: 700 15px var(--mono); overflow-wrap: anywhere; }
70
+ .card .grp { font-size: 12px; color: var(--muted); }
71
+ .card .label { font-size: 13px; color: var(--muted); margin: 2px 0 6px; }
72
+ .kv { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px 10px; margin: 8px 0; }
73
+ .kv div { min-width: 0; } .kv .v.sm { font-size: 12px; } .kv .v.md { font-size: 13px; }
74
+ p.note { font-size: 12px; margin: 8px 0 0; }
75
+ .kv .k { font-size: 11.5px; color: var(--muted); } .kv .v { font: 600 14px var(--mono); font-variant-numeric: tabular-nums; }
76
+ .badge { display: inline-block; font: 600 11.5px/1.5 var(--mono); padding: 0 7px; border-radius: 999px; border: 1px solid currentColor; }
77
+ .badge.expl { color: var(--badge); background: var(--badge-bg); }
78
+ .badge.ok { color: var(--up); }
79
+ .badge.base { color: var(--series); }
80
+ .badge.none { color: var(--muted); }
81
+ .spark { display: block; width: 100%; height: 40px; }
82
+ .spark .ln { fill: none; stroke: var(--series); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
83
+ .spark .ref { stroke: var(--line-2); stroke-width: 1; stroke-dasharray: 3 3; }
84
+
85
+ .tiles { display: grid; gap: var(--s2); grid-template-columns: repeat(2, minmax(0, 1fr)); margin: var(--s3) 0; }
86
+ @media (min-width: 760px) { .tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
87
+ @media (min-width: 1200px) { .tiles { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
88
+ .tile { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); padding: 10px 12px; min-width: 0; }
89
+ .tile .k { font-size: 12px; color: var(--muted); } .tile .v { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; } .tile .s { font-size: 12px; color: var(--muted); font-family: var(--mono); }
90
+
91
+ .chart { width: 100%; height: auto; display: block; touch-action: pan-y; }
92
+ .chart .grid line { stroke: var(--line); stroke-width: 1; }
93
+ .chart .axis text, .chart .tick { fill: var(--muted); font: 11px var(--mono); }
94
+ .chart .ln { fill: none; stroke: var(--series); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
95
+ .chart .area { fill: var(--series-soft); }
96
+ .chart .dd-area { fill: var(--dd-soft); }
97
+ .chart .dd { fill: none; stroke: var(--dd); stroke-width: 2; stroke-linejoin: round; }
98
+ .chart .ref { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 4 4; }
99
+ .chart .zero { stroke: var(--muted); stroke-width: 1.5; }
100
+ .chart .dot { fill: var(--series); stroke: var(--panel); stroke-width: 2; }
101
+ .chart .ci { stroke: var(--text); stroke-width: 2; stroke-linecap: round; }
102
+ .chart .mean { fill: var(--accent); stroke: var(--panel); stroke-width: 2; }
103
+ .chart .bar { fill: var(--series); }
104
+ .chart .bar.neg { fill: var(--dd); }
105
+ .chart .lbl { fill: var(--text); font: 12px var(--mono); }
106
+ .chart .xh { stroke: var(--muted); stroke-width: 1; }
107
+ .chart .hit { fill: transparent; }
108
+ .chartbox { position: relative; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); padding: var(--s2); }
109
+ .tip { position: absolute; pointer-events: none; background: var(--panel-2); color: var(--text); border: 1px solid var(--line-2); border-radius: 6px; padding: 4px 8px; font: 12px var(--mono); white-space: nowrap; transform: translate(-50%, -110%); display: none; z-index: 3; }
110
+
111
+ .tw { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r); background: var(--panel); -webkit-overflow-scrolling: touch; }
112
+ table { border-collapse: collapse; width: 100%; font-size: 13px; }
113
+ th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line); white-space: nowrap; vertical-align: top; }
114
+ td.wrap { white-space: normal; min-width: 220px; }
115
+ th { color: var(--muted); font-weight: 600; position: sticky; top: 0; background: var(--panel); }
116
+ th.r, td.r { text-align: right; }
117
+ tr.base td { background: var(--panel-2); }
118
+ tbody tr:last-child td { border-bottom: 0; }
119
+ caption { text-align: left; padding: 8px 10px; color: var(--muted); font-size: 12px; }
120
+
121
+ select { font: 14px var(--mono); padding: 8px; min-height: 44px; background: var(--panel); color: var(--text); border: 1px solid var(--line-2); border-radius: 8px; max-width: 100%; }
122
+ label.pick { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; font-size: 13px; color: var(--muted); }
123
+ .rule { border-left: 3px solid var(--accent); padding: 6px 10px; background: var(--panel); font-size: 13.5px; margin: var(--s2) 0 var(--s3); }
124
+ .status { border-left: 4px solid var(--line-2); }
125
+ .status.ok { border-left-color: var(--up); } .status.warn { border-left-color: var(--warn); } .status.down { border-left-color: var(--down); }
126
+ .st { font: 700 12px var(--mono); padding: 1px 8px; border-radius: 999px; }
127
+ .st.ok { background: var(--ok-bg); color: var(--up); } .st.warn { background: var(--warn-bg); color: var(--warn); } .st.down { background: var(--down-bg); color: var(--down); } .st.idle { background: var(--idle-bg); color: var(--muted); }
128
+ dl.items { display: grid; grid-template-columns: minmax(110px, 40%) 1fr; gap: 4px 10px; margin: 8px 0 0; font-size: 13px; }
129
+ dl.items dt { color: var(--muted); } dl.items dd { margin: 0; overflow-wrap: anywhere; }
130
+ footer { max-width: 1440px; margin: 0 auto; padding: var(--s4); color: var(--muted); font-size: 12.5px; border-top: 1px solid var(--line); }
131
+ footer ul { padding-left: 18px; margin: 6px 0; }
132
+ .empty { padding: var(--s4); color: var(--muted); }
133
+ @media (max-width: 480px) { .updated .long { display: none; } .bar { gap: 8px; } nav.tabs { padding: 0 4px; } nav.tabs a { padding: 10px 7px; font-size: 13px; } nav.tabs .wide { display: none; } }
134
+ @media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
135
+ @media (forced-colors: active) { .chart .ln, .chart .dd, .spark .ln { stroke: CanvasText; } }
136
+
137
+ .only-wide { display: none; } .only-narrow { display: grid; gap: var(--s2); }
138
+ @media (min-width: 640px) { .only-wide { display: block; } .only-narrow { display: none; } }
139
+ .card.rug .head .r { margin-left: auto; font: 700 15px var(--mono); }
@@ -0,0 +1,200 @@
1
+ // dexterm dash (task-1749): the page. Plain JS, no dependency, no external request. Reads the JSON the local server
2
+ // computes (or data.json in a static snapshot), renders five views by hash route and refreshes every 30 s.
3
+ 'use strict';
4
+ (() => {
5
+ const SRC = document.querySelector('meta[name="dash-data"]').content;
6
+ const LIVE = document.body.dataset.mode === 'live';
7
+ const REFRESH_MS = 30_000;
8
+ const $ = (s) => document.querySelector(s);
9
+ const view = $('#view');
10
+ let D = null; let lastOk = 0; let lastSig = '';
11
+
12
+ // ---------- formatting ----------
13
+ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
14
+ const fin = (x) => typeof x === 'number' && Number.isFinite(x);
15
+ const MINUS = '−';
16
+ const sgn = (x, d = 1) => (fin(x) ? `${x > 0 ? '+' : x < 0 ? MINUS : ''}${Math.abs(x).toFixed(d)}` : '–');
17
+ const pct = (x, d = 1) => (fin(x) ? `<span class="num ${x > 0 ? 'up' : x < 0 ? 'down' : ''}">${sgn(x, d)}%</span>` : '<span class="muted">–</span>');
18
+ const pp = (x) => (fin(x) ? `${sgn(x)} pp` : '–');
19
+ const money = (x, unit = 'USD', d) => (!fin(x) ? '–' : unit === 'SOL' ? `${x < 0 ? MINUS : ''}${Math.abs(x).toFixed(d ?? 4)} SOL` : `${x < 0 ? MINUS : ''}$${Math.abs(x).toFixed(d ?? 2)}`);
20
+ const smoney = (x, unit) => (fin(x) ? `<span class="num ${x > 0 ? 'up' : x < 0 ? 'down' : ''}">${x > 0 ? '+' : ''}${money(x, unit)}</span>` : '–');
21
+ const ci = (lo, hi) => (fin(lo) && fin(hi) ? `<span class="num muted">(${sgn(lo)} … ${sgn(hi)})</span>` : '');
22
+ const dtf = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false });
23
+ const when = (t) => (fin(t) ? dtf.format(new Date(t)) : '–');
24
+ const ago = (t) => { if (!fin(t)) return '–'; const s = Math.max(0, Math.round((Date.now() - t) / 1000)); return s < 90 ? `${s} s ago` : s < 5400 ? `${Math.round(s / 60)} min ago` : s < 172800 ? `${Math.round(s / 3600)} h ago` : `${Math.round(s / 86400)} d ago`; };
25
+ const coin = (c) => `${esc(c.symbol ?? '?')}${c.mint ? ` <span class="muted mono">${esc(c.mint.slice(0, 4))}…${esc(c.mint.slice(-4))}</span>` : ''}`;
26
+ const byId = (id) => D.strategies.find((s) => s.id === id);
27
+ const minN = () => D.definitions.minClosed;
28
+ const badge = (s) => {
29
+ const r = s.row;
30
+ const b = r.sample === 'none' ? '<span class="badge none">no closed trades</span>' : r.sample === 'exploratory' ? `<span class="badge expl" title="Fewer than ${minN()} closed trades: read nothing into the numbers yet">exploratory · ${r.closed} closed &lt; ${minN()}</span>` : `<span class="badge ok">${r.closed} closed ≥ ${minN()}</span>`;
31
+ return b + (s.baseline ? ' <span class="badge base">baseline</span>' : '');
32
+ };
33
+ const eta = (r) => (r.closed >= minN() ? 'reached' : fin(r.etaDays) ? `≈ ${r.etaDays < 1 ? `${Math.round(r.etaDays * 24)} h` : `${r.etaDays.toFixed(1)} d`}` : 'no entries yet');
34
+
35
+ // ---------- charts (inline SVG, one series per chart, one y-axis) ----------
36
+ // the SVG is drawn at its container's width (320-720 user units) so 11 px axis text stays 11 px on a phone
37
+ let W = 720; let H = 240; const P = { l: 58, r: 12, t: 12, b: 26 };
38
+ const narrow = () => W < 520;
39
+ const fitWidth = () => { W = Math.round(Math.min(720, Math.max(320, view.clientWidth - 34))); H = narrow() ? 200 : 240; };
40
+ const ticks = (lo, hi, n = 4) => { if (lo === hi) { lo -= 1; hi += 1; } const step0 = (hi - lo) / n; const mag = 10 ** Math.floor(Math.log10(step0)); const step = [1, 2, 2.5, 5, 10].map((m) => m * mag).find((s) => s >= step0); const out = []; for (let v = Math.ceil(lo / step) * step; v <= hi + 1e-9; v += step) out.push(+v.toFixed(10)); return out; };
41
+ function spark(s) {
42
+ const pts = [{ v: s.equity.start }, ...s.equity.points.map((p) => ({ v: p.usd }))];
43
+ if (pts.length < 2) return '<svg class="spark" viewBox="0 0 120 40" aria-hidden="true"><line class="ref" x1="0" x2="120" y1="20" y2="20"/></svg>';
44
+ const vs = pts.map((p) => p.v); const lo = Math.min(...vs); const hi = Math.max(...vs); const span = hi - lo || 1;
45
+ const x = (i) => (i / (pts.length - 1)) * 116 + 2; const y = (v) => 36 - ((v - lo) / span) * 32;
46
+ return `<svg class="spark" viewBox="0 0 120 40" preserveAspectRatio="none" role="img" aria-label="Realized equity, ${esc(money(s.equity.start, s.unit))} to ${esc(money(s.equity.end, s.unit))}"><line class="ref" x1="0" x2="120" y1="${y(s.equity.start).toFixed(1)}" y2="${y(s.equity.start).toFixed(1)}"/><path class="ln" vector-effect="non-scaling-stroke" d="${pts.map((p, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)},${y(p.v).toFixed(1)}`).join('')}"/></svg>`;
47
+ }
48
+ /** A time-series line (or drawdown area) with a crosshair + tooltip. pts: [{t, v, tip}] */
49
+ function timeChart(id, pts, { ref = null, fmt = (v) => v.toFixed(2), kind = 'line', label }) {
50
+ if (pts.length < 2) return `<p class="empty">Not enough closed trades for a curve yet (${pts.length}).</p>`;
51
+ const t0 = pts[0].t; const t1 = pts[pts.length - 1].t; const tspan = t1 - t0 || 1;
52
+ const vs = pts.map((p) => p.v).concat(ref === null ? [] : [ref]); let lo = Math.min(...vs); let hi = Math.max(...vs);
53
+ if (kind === 'dd') { hi = 0; lo = Math.min(lo, -1); }
54
+ const pad = (hi - lo || 1) * 0.08; lo -= kind === 'dd' ? pad : pad; hi += kind === 'dd' ? 0 : pad;
55
+ const X = (t) => P.l + ((t - t0) / tspan) * (W - P.l - P.r); const Y = (v) => P.t + (1 - (v - lo) / (hi - lo)) * (H - P.t - P.b);
56
+ const yt = ticks(lo, hi); const path = pts.map((p, i) => `${i ? 'L' : 'M'}${X(p.t).toFixed(1)},${Y(p.v).toFixed(1)}`).join('');
57
+ const xt = [t0, t0 + tspan / 2, t1];
58
+ let body = `<g class="grid">${yt.map((v) => `<line x1="${P.l}" x2="${W - P.r}" y1="${Y(v).toFixed(1)}" y2="${Y(v).toFixed(1)}"/>`).join('')}</g>`;
59
+ body += `<g class="axis">${yt.map((v) => `<text x="${P.l - 6}" y="${(Y(v) + 4).toFixed(1)}" text-anchor="end">${esc(fmt(v))}</text>`).join('')}${xt.map((t, i) => `<text x="${X(t).toFixed(1)}" y="${H - 6}" text-anchor="${['start', 'middle', 'end'][i]}">${esc(when(t))}</text>`).join('')}</g>`;
60
+ if (ref !== null) body += `<line class="ref" x1="${P.l}" x2="${W - P.r}" y1="${Y(ref).toFixed(1)}" y2="${Y(ref).toFixed(1)}"/>`;
61
+ if (kind === 'dd') body += `<path class="dd-area" d="${path}L${X(t1).toFixed(1)},${Y(0).toFixed(1)}L${X(t0).toFixed(1)},${Y(0).toFixed(1)}Z"/><path class="dd" d="${path}"/>`;
62
+ else body += `<path class="area" d="${path}L${X(t1).toFixed(1)},${(H - P.b).toFixed(1)}L${X(t0).toFixed(1)},${(H - P.b).toFixed(1)}Z"/><path class="ln" d="${path}"/>`;
63
+ const last = pts[pts.length - 1];
64
+ if (kind !== 'dd') body += `<circle class="dot" r="4.5" cx="${X(last.t).toFixed(1)}" cy="${Y(last.v).toFixed(1)}"/>`;
65
+ body += `<line class="xh" id="${id}-xh" y1="${P.t}" y2="${H - P.b}" x1="-10" x2="-10"/><rect class="hit" x="${P.l}" y="0" width="${W - P.l - P.r}" height="${H}"/>`;
66
+ chartData[id] = { pts: pts.map((p) => ({ x: X(p.t), y: Y(p.v), tip: p.tip })) };
67
+ return `<div class="chartbox"><svg class="chart" id="${id}" viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(label)}">${body}</svg><div class="tip" id="${id}-tip"></div></div>`;
68
+ }
69
+ const chartData = {};
70
+ function wireCharts() {
71
+ for (const [id, cd] of Object.entries(chartData)) {
72
+ const svg = document.getElementById(id); if (!svg) continue;
73
+ const tip = document.getElementById(`${id}-tip`); const xh = document.getElementById(`${id}-xh`);
74
+ const move = (ev) => {
75
+ const r = svg.getBoundingClientRect(); const vx = ((ev.clientX - r.left) / r.width) * W;
76
+ let best = cd.pts[0]; for (const p of cd.pts) if (Math.abs(p.x - vx) < Math.abs(best.x - vx)) best = p;
77
+ xh.setAttribute('x1', best.x); xh.setAttribute('x2', best.x);
78
+ tip.innerHTML = best.tip; tip.style.display = 'block';
79
+ const box = svg.parentElement.getBoundingClientRect();
80
+ tip.style.left = `${Math.min(Math.max((best.x / W) * r.width + (r.left - box.left), 70), box.width - 70)}px`; tip.style.top = `${(best.y / H) * r.height + (r.top - box.top)}px`;
81
+ };
82
+ const out = () => { tip.style.display = 'none'; xh.setAttribute('x1', -10); xh.setAttribute('x2', -10); };
83
+ svg.addEventListener('pointermove', move); svg.addEventListener('pointerdown', move); svg.addEventListener('pointerleave', out);
84
+ }
85
+ }
86
+ /** Exit reasons as horizontal bars (count), the mean % per reason as text. */
87
+ function exitBars(exits) {
88
+ if (!exits.length) return '<p class="empty">No closed trades yet.</p>';
89
+ const max = Math.max(...exits.map((e) => e.n)); const rowH = 30; const h = exits.length * rowH + 8; const L = narrow() ? 92 : 150; const R = narrow() ? 92 : 150;
90
+ const rows = exits.map((e, i) => { const w = Math.max(4, (e.n / max) * (W - L - R)); const y = 4 + i * rowH;
91
+ return `<g><title>${esc(e.reason)}: ${e.n} closed, mean ${sgn(e.meanPct)}%, ${e.wins} wins</title><text class="lbl" x="${L - 8}" y="${y + 18}" text-anchor="end">${esc(e.reason.slice(0, narrow() ? 12 : 20))}</text><rect class="bar${fin(e.meanPct) && e.meanPct < 0 ? ' neg' : ''}" x="${L}" y="${y + 5}" width="${w.toFixed(1)}" height="18" rx="4"/><text class="lbl" x="${L + w + 8}" y="${y + 18}">${e.n} · ${esc(sgn(e.meanPct))}%</text></g>`; }).join('');
92
+ return `<div class="chartbox"><svg class="chart" viewBox="0 0 ${W} ${h}" role="img" aria-label="Closed trades by exit reason">${rows}</svg></div>`;
93
+ }
94
+ /** Compare: the difference in mean % per entry vs the baseline with its 95% interval (a dot and whisker per variant). */
95
+ function forest(cs) {
96
+ const ok = cs.filter((c) => fin(c.diff.mean)); if (!ok.length) return '<p class="empty">Nothing to compare yet.</p>';
97
+ const vals = ok.flatMap((c) => [c.diff.lo, c.diff.hi, c.diff.mean]).filter(fin).concat([0]); let lo = Math.min(...vals); let hi = Math.max(...vals); const pad = (hi - lo || 1) * 0.06; lo -= pad; hi += pad;
98
+ const L = narrow() ? 118 : 150; const R = 16; const rowH = 30; const h = ok.length * rowH + 34; const X = (v) => L + ((v - lo) / (hi - lo)) * (W - L - R);
99
+ const xt = ticks(lo, hi, 5);
100
+ let b = `<g class="grid">${xt.map((v) => `<line x1="${X(v).toFixed(1)}" x2="${X(v).toFixed(1)}" y1="4" y2="${h - 24}"/>`).join('')}</g><g class="axis">${xt.map((v) => `<text x="${X(v).toFixed(1)}" y="${h - 8}" text-anchor="middle">${esc(sgn(v, 0))}</text>`).join('')}</g><line class="zero" x1="${X(0).toFixed(1)}" x2="${X(0).toFixed(1)}" y1="4" y2="${h - 24}"/>`;
101
+ ok.forEach((c, i) => { const y = 4 + i * rowH + 14;
102
+ b += `<g><title>${esc(c.id)} − ${esc(c.base)}: ${pp(c.diff.mean)} (95% ${sgn(c.diff.lo)} … ${sgn(c.diff.hi)}), ${c.n} vs ${c.baseN} closed. ${esc(c.verdict)}</title><text class="lbl" x="${L - 8}" y="${y + 4}" text-anchor="end">${esc(c.id.slice(0, narrow() ? 15 : 18))}</text>${fin(c.diff.lo) && fin(c.diff.hi) ? `<line class="ci" x1="${X(c.diff.lo).toFixed(1)}" x2="${X(c.diff.hi).toFixed(1)}" y1="${y}" y2="${y}"/>` : ''}<circle class="mean" r="5.5" cx="${X(c.diff.mean).toFixed(1)}" cy="${y}"/></g>`; });
103
+ return `<div class="chartbox"><svg class="chart" viewBox="0 0 ${W} ${h}" role="img" aria-label="Difference in mean % per entry against the baseline, with 95% intervals">${b}</svg></div><p class="muted note">Dot = difference in mean % on cost per entry (variant minus baseline, percentage points); line = its 95% interval. A line that crosses 0 means the data cannot tell them apart.</p>`;
104
+ }
105
+
106
+ // ---------- views ----------
107
+ const disclaimer = () => `<p class="disclaimer"><strong>Paper trading, not financial advice.</strong> Simulated fills on paper books. Small samples say almost nothing: every number carries its sample size, and anything under ${minN()} closed trades is exploratory.</p>`;
108
+ function overview() {
109
+ const S = D.strategies;
110
+ if (!S.length) return `<h1>Overview</h1>${disclaimer()}<p class="empty">No strategies found. Run a paper strategy with <code>dexterm autotrade run</code>, then come back.</p>${notes()}`;
111
+ const cards = S.map((s) => { const r = s.row;
112
+ return `<a class="card${s.baseline ? ' base' : ''}" href="#/strategy/${encodeURIComponent(s.id)}"><div class="head"><span class="id">${esc(s.id)}</span><span class="grp">${esc(s.group)}</span></div><div class="label">${esc(s.label)}</div><div>${badge(s)}</div>
113
+ <div class="kv"><div><div class="k">mean / trade</div><div class="v">${pct(r.meanPct)}</div></div><div><div class="k">closed · open</div><div class="v">${r.closed} · ${r.open}</div></div><div><div class="k">wins · rugs</div><div class="v">${r.wins} · ${r.rugs}</div></div>
114
+ <div><div class="k">95% interval</div><div class="v sm">${fin(r.lo) ? `${sgn(r.lo)} … ${sgn(r.hi)}` : '–'}</div></div><div><div class="k">realized</div><div class="v md">${smoney(r.realizedUsd, s.unit)}</div></div><div><div class="k">ETA to ${minN()}</div><div class="v md">${eta(r)}</div></div></div>${spark(s)}</a>`; }).join('');
115
+ const rows = S.map((s) => { const r = s.row; return `<tr${s.baseline ? ' class="base"' : ''}><td><a href="#/strategy/${encodeURIComponent(s.id)}">${esc(s.id)}</a>${s.baseline ? ' <span class="badge base">baseline</span>' : ''}</td><td>${esc(s.group)}</td><td class="r num">${r.entries}</td><td class="r num">${r.closed}</td><td class="r num">${r.open}</td><td class="r num">${r.writtenOff}</td><td class="r num">${r.wins} / ${r.losses}</td><td class="r num">${r.rugs}${r.closed ? ` (${Math.round(r.rugRate * 100)}%)` : ''}</td><td class="r">${pct(r.meanPct)} ${ci(r.lo, r.hi)}</td><td class="r">${smoney(r.realizedUsd, s.unit)}</td><td class="r num">${fin(s.equity.maxDrawdownPct) ? `${s.equity.maxDrawdownPct.toFixed(1)}%` : '–'}</td><td class="r num">${fin(r.entriesPerDay) ? r.entriesPerDay.toFixed(1) : '–'}</td><td>${eta(r)}</td><td>${r.sample === 'exploratory' ? 'exploratory' : r.sample === 'none' ? '–' : 'n ≥ ' + minN()}</td></tr>`; }).join('');
116
+ return `<h1>Overview</h1><p class="lead">${S.length} strategies side by side${D.baseline ? `; the baseline is <strong class="mono">${esc(D.baseline)}</strong>` : ''}. Tap one for its detail.</p>${disclaimer()}<div class="grid g4">${cards}</div>
117
+ <h2>Table view</h2><div class="tw"><table><caption>Mean % on cost per closed trade with its 95% interval; rugs = closed at ${D.definitions.rugPct}% or worse; drawdown of the realized equity curve.</caption><thead><tr><th>strategy</th><th>group</th><th class="r">entries</th><th class="r">closed</th><th class="r">open</th><th class="r">written off</th><th class="r">wins / losses</th><th class="r">rugs</th><th class="r">mean % (95%)</th><th class="r">realized</th><th class="r">max DD</th><th class="r">entries/day</th><th>ETA to ${minN()}</th><th>sample</th></tr></thead><tbody>${rows}</tbody></table></div>${notes()}`;
118
+ }
119
+ function strategy(id) {
120
+ const S = D.strategies; if (!S.length) return overview();
121
+ const s = byId(id) ?? S[0]; const r = s.row; const u = s.unit;
122
+ const pick = `<label class="pick" for="pick">Strategy <select id="pick">${S.map((x) => `<option value="${esc(x.id)}"${x.id === s.id ? ' selected' : ''}>${esc(x.id)}${x.baseline ? ' (baseline)' : ''} · ${esc(x.group)}</option>`).join('')}</select></label>`;
123
+ const tiles = [['mean % on cost', pct(r.meanPct), fin(r.lo) ? `95% ${sgn(r.lo)} … ${sgn(r.hi)}` : 'no interval yet'], ['closed', `<span class="num">${r.closed}</span>`, `${r.entries} entries · ${r.open} open`], ['wins / losses', `<span class="num">${r.wins} / ${r.losses}</span>`, r.closed ? `${Math.round((r.wins / r.closed) * 100)}% won` : '–'],
124
+ ['rugs (≤ ' + D.definitions.rugPct + '%)', `<span class="num">${r.rugs}</span>`, r.closed ? `${Math.round(r.rugRate * 100)}% (95% ${Math.round((r.rugCi[0] ?? 0) * 100)}–${Math.round((r.rugCi[1] ?? 0) * 100)}%)` : '–'], ['realized', smoney(r.realizedUsd, u), `book ${money(s.equity.start, u)} → ${money(s.equity.end, u)}`], ['max drawdown', `<span class="num">${s.equity.maxDrawdownPct.toFixed(1)}%</span>`, `${fin(r.entriesPerDay) ? r.entriesPerDay.toFixed(1) : '–'} entries/day · ETA ${eta(r)}`]]
125
+ .map(([k, v, x]) => `<div class="tile"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${x}</div></div>`).join('');
126
+ const eqPts = [{ t: s.firstEventAt ?? (s.equity.points[0]?.t ?? 0), v: s.equity.start, tip: `start ${money(s.equity.start, u)}` }, ...s.equity.points.map((p, i) => { const c = s.trades[i]; return { t: p.t, v: p.usd, tip: `${esc(when(p.t))} · ${money(p.usd, u)}${c ? ` · ${esc(c.symbol ?? '?')} ${sgn(c.pnlPct)}%` : ''}` }; })];
127
+ const ddPts = [{ t: eqPts[0].t, v: 0, tip: 'peak' }, ...s.equity.points.map((p) => ({ t: p.t, v: -p.drawdownPct, tip: `${esc(when(p.t))} · drawdown ${p.drawdownPct.toFixed(1)}%` }))];
128
+ const open = s.open.length ? `<div class="tw"><table><thead><tr><th>coin</th><th>profile</th><th class="r">cost</th><th class="r">engine mark</th><th class="r">pool liquidity</th><th class="r">realizable</th><th>state</th></tr></thead><tbody>${s.open.map((o) => `<tr><td>${coin(o)}</td><td>${esc(o.profile ?? '–')}</td><td class="r num">${money(o.costUsd, u)}</td><td class="r">${money(o.markUsd, u)} ${pct(o.markPct)}</td><td class="r num">${fin(o.liqUsd) ? `$${Math.round(o.liqUsd).toLocaleString()} <span class="muted">(${ago(o.liqAt)})</span>` : '<span class="muted">not read</span>'}</td><td class="r">${fin(o.realizableUsd) ? `${money(o.realizableUsd, u)} ${pct(o.realizablePct)}` : '<span class="muted">unknown (no pool read)</span>'}</td><td>${o.stuck ? '<strong class="down">stuck: no pool to sell into</strong>' : 'open'}</td></tr>`).join('')}</tbody></table></div>` : '<p class="empty">No open positions.</p>';
129
+ const trades = s.trades.length ? `<div class="tw"><table><thead><tr><th>closed</th><th>coin</th><th>profile</th><th class="r">% on cost</th><th class="r">P/L</th><th class="r">cost</th><th class="r">held</th><th>exit</th></tr></thead><tbody>${[...s.trades].reverse().slice(0, 300).map((c) => `<tr><td class="num">${esc(when(c.t))}</td><td>${coin(c)}</td><td>${esc(c.profile ?? '–')}</td><td class="r">${pct(c.pnlPct)}</td><td class="r">${smoney(c.pnlUsd, u)}</td><td class="r num">${money(c.costUsd, u)}</td><td class="r num">${fin(c.openedAt) ? `${Math.max(0, Math.round((c.t - c.openedAt) / 60000))} min` : '–'}</td><td>${c.writtenOff ? '<strong>written off</strong>' : esc(c.exitReason ?? '–')}</td></tr>`).join('')}</tbody></table></div>` : '<p class="empty">No closed trades yet.</p>';
130
+ return `<h1 class="mono">${esc(s.id)}</h1><p class="lead">${esc(s.label)} · ${esc(s.group)} · ${esc(s.mode ?? 'paper')} · ${badge(s)} · last event ${ago(s.lastEventAt)}</p>${pick}${disclaimer()}<div class="tiles">${tiles}</div>
131
+ <h2>Realized equity</h2>${timeChart('eq', eqPts, { ref: s.equity.start, fmt: (v) => (u === 'SOL' ? v.toFixed(2) : `$${v.toFixed(0)}`), label: `Realized equity of ${s.id} over time` })}
132
+ <h2>Drawdown from the running peak</h2>${timeChart('dd', ddPts, { fmt: (v) => `${v.toFixed(0)}%`, kind: 'dd', label: `Drawdown of ${s.id}` })}
133
+ <h2>Exit reasons</h2>${exitBars(r.exits)}
134
+ <h2>Open positions</h2><p class="lead">Engine mark = what the engine values it at. Realizable = that mark sold into the last pool read from the local cache (price impact on half the liquidity, 2.3% fees and slippage). Stuck = 0.</p>${open}
135
+ <h2>Trades (${s.trades.length})</h2>${trades}`;
136
+ }
137
+ function rugs() {
138
+ const all = D.strategies.flatMap((s) => s.rugs.map((c) => ({ s, c }))).sort((a, b) => b.c.t - a.c.t);
139
+ const stuckOpen = D.strategies.flatMap((s) => s.open.filter((o) => o.stuck).map((o) => ({ s, o })));
140
+ const rows = all.map(({ s, c }) => `<tr><td><a href="#/strategy/${encodeURIComponent(s.id)}">${esc(s.id)}</a></td><td>${coin(c)}</td><td class="num">${esc(when(c.t))}</td><td class="r">${pct(c.pnlPct)}</td><td class="r num">${money(c.costUsd, s.unit)}</td><td class="r num">${c.writtenOff ? money(c.realizedUsd, s.unit) : '–'}</td><td class="r num">${fin(c.entryAgeMin) ? `${c.entryAgeMin < 120 ? `${Math.round(c.entryAgeMin)} min` : `${(c.entryAgeMin / 60).toFixed(1)} h`}` : '–'}</td><td class="r num">${fin(c.entryLiqUsd) ? `$${Math.round(c.entryLiqUsd).toLocaleString()}` : '–'}</td><td class="wrap">${c.writtenOff ? `<strong>written off</strong>: ${esc(c.reason ?? '')}` : `closed ${esc(c.exitReason ?? '')}`}</td></tr>`).join('');
141
+ const age = (c) => (fin(c.entryAgeMin) ? (c.entryAgeMin < 120 ? `${Math.round(c.entryAgeMin)} min` : `${(c.entryAgeMin / 60).toFixed(1)} h`) : '–');
142
+ const cards = all.map(({ s, c }) => `<div class="card rug"><div class="head"><span class="id">${coin(c)}</span><span class="grp">${esc(s.id)}</span><span class="r">${pct(c.pnlPct)}</span></div><dl class="items"><dt>closed</dt><dd>${esc(when(c.t))}</dd><dt>cost → sold</dt><dd>${money(c.costUsd, s.unit)} → ${c.writtenOff ? money(c.realizedUsd, s.unit) : '–'}</dd><dt>pool at entry</dt><dd>${age(c)} old${fin(c.entryLiqUsd) ? ` · $${Math.round(c.entryLiqUsd).toLocaleString()} liq` : ''}</dd><dt>why</dt><dd>${c.writtenOff ? `<strong>written off</strong>: ${esc(c.reason ?? '')}` : `closed ${esc(c.exitReason ?? '')}`}</dd></dl></div>`).join('');
143
+ return `<h1>Rugs &amp; stuck</h1><p class="lead">Closed at ${D.definitions.rugPct}% or worse, plus every stuck position written off: the engine could not sell it (no pool to sell into) for ${D.definitions.writeOffMin} min or more, so it is closed here at what it actually sold; the unsold rest counts 0. The engine's own files are never edited.</p>${disclaimer()}
144
+ ${stuckOpen.length ? `<h2>Stuck now (not yet written off)</h2><div class="tw"><table><thead><tr><th>strategy</th><th>coin</th><th class="r">cost</th><th class="r">engine mark</th></tr></thead><tbody>${stuckOpen.map(({ s, o }) => `<tr><td>${esc(s.id)}</td><td>${coin(o)}</td><td class="r num">${money(o.costUsd, s.unit)}</td><td class="r">${money(o.markUsd, s.unit)}</td></tr>`).join('')}</tbody></table></div>` : ''}
145
+ <h2>Rugs and write-offs (${all.length})</h2>${all.length ? `<div class="only-narrow">${cards}</div><div class="tw only-wide"><table><thead><tr><th>strategy</th><th>coin</th><th>closed</th><th class="r">% on cost</th><th class="r">cost</th><th class="r">sold before stuck</th><th class="r">pool age at entry</th><th class="r">liquidity at entry</th><th>why</th></tr></thead><tbody>${rows}</tbody></table></div>` : '<p class="empty">None so far.</p>'}`;
146
+ }
147
+ function compare() {
148
+ if (!D.baseline) return `<h1>Compare</h1>${disclaimer()}<p class="empty">No baseline set. Start the dashboard with <code>--baseline ID</code> to compare every strategy with it.</p>`;
149
+ const rows = D.comparisons.map((c) => `<tr><td><a href="#/strategy/${encodeURIComponent(c.id)}">${esc(c.id)}</a></td><td class="r num">${c.n} / ${c.baseN}</td><td class="r num">${pp(c.diff.mean)}</td><td class="r">${ci(c.diff.lo, c.diff.hi)}</td><td>${esc(c.verdict)}</td></tr>`).join('');
150
+ return `<h1>Compare with the baseline</h1><p class="lead">Each strategy against <strong class="mono">${esc(D.baseline)}</strong>: the difference in mean % on cost per closed trade, with its 95% bootstrap interval.</p>${disclaimer()}<div class="rule"><strong>Decision rule.</strong> ${esc(D.decisionRule)}</div>${forest(D.comparisons)}
151
+ <h2>Table view</h2><div class="tw"><table><thead><tr><th>strategy</th><th class="r">closed (strategy / baseline)</th><th class="r">difference</th><th class="r">95% interval</th><th>verdict</th></tr></thead><tbody>${rows}</tbody></table></div>`;
152
+ }
153
+ function live() {
154
+ const cards = D.status.map((c) => `<section class="card status ${esc(c.state)}"><div class="head"><h3>${esc(c.title)}</h3><span class="st ${esc(c.state)}">${esc({ ok: 'up', warn: 'attention', down: 'down', idle: 'idle' }[c.state] ?? c.state)}</span></div><dl class="items">${c.items.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${/^\d{4}-\d\d-\d\dT[\d:.]+Z$/.test(v) ? `${esc(when(Date.parse(v)))} <span class="muted">(${ago(Date.parse(v))})</span>` : esc(v)}</dd>`).join('')}</dl>${fin(c.updatedAt) ? `<p class="muted note">updated ${ago(c.updatedAt)}</p>` : ''}</section>`).join('');
155
+ const last = D.strategies.map((s) => `<tr><td>${esc(s.id)}</td><td>${esc(s.group)}</td><td class="num">${ago(s.lastEventAt)}</td><td class="r num">${s.open.length}</td></tr>`).join('');
156
+ return `<h1>Live status</h1><p class="lead">${LIVE ? `What is running now. This page re-reads the ledgers every ${REFRESH_MS / 1000} s.` : 'A snapshot: what was running when it was taken.'} Data computed ${ago(D.generatedAt)}.</p>${disclaimer()}<div class="grid">${cards || '<p class="empty">No status sources.</p>'}</div>
157
+ <h2>Last ledger event per strategy</h2><div class="tw"><table><thead><tr><th>strategy</th><th>group</th><th>last event</th><th class="r">open</th></tr></thead><tbody>${last}</tbody></table></div>${notes()}`;
158
+ }
159
+ const notes = () => (D.notes?.length ? `<h2>Notes</h2><ul class="muted">${D.notes.map((n) => `<li>${esc(n)}</li>`).join('')}</ul>` : '');
160
+ function foot() {
161
+ $('#foot').innerHTML = `<p><strong>Paper trading, not financial advice.</strong> Simulated trades on paper books; no real funds, no wallet, no keys. Simulated results say nothing about future results.</p><details><summary>What the numbers mean</summary><ul>${D.definitions.text.map((t) => `<li>${esc(t)}</li>`).join('')}</ul></details><p>${LIVE ? 'Served by <code>dexterm dash</code> on this machine (read-only).' : `Read-only snapshot taken ${esc(when(D.generatedAt))}.`}</p>`;
162
+ }
163
+
164
+ // ---------- routing, refresh, theme ----------
165
+ const route = () => { const h = location.hash.replace(/^#\/?/, '').split('/'); return { name: h[0] || 'overview', arg: h[1] ? decodeURIComponent(h[1]) : null }; };
166
+ function render() {
167
+ if (!D) return;
168
+ const r = route(); const focusPick = document.activeElement?.id === 'pick'; const sy = window.scrollY;
169
+ for (const k of Object.keys(chartData)) delete chartData[k];
170
+ fitWidth();
171
+ view.innerHTML = r.name === 'strategy' ? strategy(r.arg) : r.name === 'rugs' ? rugs() : r.name === 'compare' ? compare() : r.name === 'live' ? live() : overview();
172
+ for (const a of document.querySelectorAll('nav.tabs a')) { if (a.dataset.view === (['strategy', 'rugs', 'compare', 'live'].includes(r.name) ? r.name : 'overview')) a.setAttribute('aria-current', 'page'); else a.removeAttribute('aria-current'); }
173
+ const pick = $('#pick'); if (pick) { pick.addEventListener('change', () => { location.hash = `#/strategy/${encodeURIComponent(pick.value)}`; }); if (focusPick) pick.focus(); }
174
+ wireCharts(); foot(); window.scrollTo(0, sy);
175
+ }
176
+ function stamp(err) {
177
+ const el = $('#updated'); const t = $('#updated-text');
178
+ if (!D) { t.textContent = err ? `cannot read data (${err})` : 'loading…'; el.classList.toggle('stale', !!err); return; }
179
+ const stale = LIVE ? (Date.now() - lastOk > REFRESH_MS * 3 || !!err) : true;
180
+ el.classList.toggle('stale', LIVE && stale);
181
+ t.innerHTML = LIVE ? `updated ${esc(ago(D.generatedAt))}<span class="long"> · auto-refresh ${REFRESH_MS / 1000} s</span>${err ? ` · <strong>refresh failed</strong>` : ''}` : `snapshot ${esc(when(D.generatedAt))}`;
182
+ }
183
+ async function load() {
184
+ try {
185
+ const res = await fetch(SRC, { cache: 'no-store' }); if (!res.ok) throw new Error(`HTTP ${res.status}`);
186
+ const d = await res.json(); lastOk = Date.now(); D = d;
187
+ const sig = JSON.stringify([d.strategies, d.status, d.comparisons]);
188
+ if (sig !== lastSig) { lastSig = sig; render(); }
189
+ stamp(null);
190
+ } catch (e) { stamp(String(e.message ?? e)); }
191
+ }
192
+ const THEMES = ['auto', 'dark', 'light'];
193
+ function setTheme(t) { if (t === 'auto') delete document.documentElement.dataset.theme; else document.documentElement.dataset.theme = t; const b = $('#theme'); b.textContent = t; b.setAttribute('aria-label', `Theme: ${t}. Switch theme`); try { localStorage.setItem('dexterm-dash-theme', t); } catch { /* private mode */ } }
194
+ $('#theme').addEventListener('click', () => { const cur = document.documentElement.dataset.theme ?? 'auto'; setTheme(THEMES[(THEMES.indexOf(cur) + 1) % 3]); });
195
+ try { const t = new URLSearchParams(location.search).get('theme') ?? localStorage.getItem('dexterm-dash-theme'); if (THEMES.includes(t)) setTheme(t); } catch { /* none */ }
196
+ let lastW = 0; window.addEventListener('resize', () => { const w = view.clientWidth; if (Math.abs(w - lastW) > 40) { lastW = w; clearTimeout(window.__dashRs); window.__dashRs = setTimeout(render, 150); } });
197
+ window.addEventListener('hashchange', () => { render(); view.focus({ preventScroll: true }); window.scrollTo(0, 0); });
198
+ load();
199
+ if (LIVE) { setInterval(load, REFRESH_MS); setInterval(() => stamp(null), 10_000); }
200
+ })();
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#020507"/><path d="M5 22 L11 16 L16 19 L22 10 L27 13" fill="none" stroke="#39ff14" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
@@ -0,0 +1,36 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <meta name="dash-data" content="api/dash.json">
7
+ <meta name="robots" content="noindex, nofollow">
8
+ <meta name="referrer" content="no-referrer">
9
+ <title>dexterm dash · strategy performance (PAPER)</title>
10
+ <link rel="icon" href="icon.svg" type="image/svg+xml">
11
+ <link rel="stylesheet" href="dash.css">
12
+ </head>
13
+ <body data-mode="live">
14
+ <a class="skip" href="#view">Skip to content</a>
15
+ <header class="top">
16
+ <div class="bar">
17
+ <span class="brand" aria-label="dexterm dash">dexterm<b>/</b>dash</span>
18
+ <span class="paper" title="Every number here is a simulation">PAPER</span>
19
+ <div class="updated" id="updated" role="status" aria-live="polite"><span class="dot" aria-hidden="true"></span><span id="updated-text">loading…</span></div>
20
+ <button class="theme" id="theme" type="button" aria-label="Theme: automatic. Switch theme">auto</button>
21
+ </div>
22
+ <nav class="tabs" aria-label="Views">
23
+ <a href="#/overview" data-view="overview">Overview</a>
24
+ <a href="#/strategy" data-view="strategy">Strategy</a>
25
+ <a href="#/rugs" data-view="rugs">Rugs &amp; stuck</a>
26
+ <a href="#/compare" data-view="compare">Compare</a>
27
+ <a href="#/live" data-view="live">Live<span class="wide">&nbsp;status</span></a>
28
+ </nav>
29
+ </header>
30
+ <main id="view" tabindex="-1"><p class="empty">Loading the run ledgers…</p></main>
31
+ <footer id="foot">
32
+ <p><strong>Paper trading, not financial advice.</strong> Simulated trades on a paper book; no real funds, no wallet, no keys. Past simulated results say nothing about future results.</p>
33
+ </footer>
34
+ <script src="dash.js"></script>
35
+ </body>
36
+ </html>