dsh-hyperliquid 0.2.0 → 0.3.0
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/README.md +14 -8
- package/client.js +1969 -543
- package/docs/research-review-2026-10-16.md +46 -0
- package/index.js +2 -1
- package/lib/chart-drawings.d.ts +6 -2
- package/lib/chart-drawings.js +58 -0
- package/lib/remote.js +105 -34
- package/lib/review.d.ts +3 -0
- package/lib/review.js +79 -0
- package/lib/store.js +38 -8
- package/lib/studies.d.ts +13 -0
- package/lib/studies.js +157 -0
- package/lib/tools.js +64 -19
- package/lib/workspace-settings.d.ts +3 -0
- package/lib/workspace-settings.js +12 -0
- package/package.json +7 -4
- package/skills/hyperliquid-trading/SKILL.md +8 -3
package/client.js
CHANGED
|
@@ -24,9 +24,9 @@ var __export = (target, all) => {
|
|
|
24
24
|
};
|
|
25
25
|
var __copyProps = (to, from, except, desc) => {
|
|
26
26
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
27
|
-
for (let
|
|
28
|
-
if (!__hasOwnProp.call(to,
|
|
29
|
-
__defProp(to,
|
|
27
|
+
for (let key3 of __getOwnPropNames(from))
|
|
28
|
+
if (!__hasOwnProp.call(to, key3) && key3 !== except)
|
|
29
|
+
__defProp(to, key3, { get: () => from[key3], enumerable: !(desc = __getOwnPropDesc(from, key3)) || desc.enumerable });
|
|
30
30
|
}
|
|
31
31
|
return to;
|
|
32
32
|
};
|
|
@@ -48,7 +48,7 @@ __export(index_exports, {
|
|
|
48
48
|
inject: () => inject
|
|
49
49
|
});
|
|
50
50
|
module.exports = __toCommonJS(index_exports);
|
|
51
|
-
var
|
|
51
|
+
var React34 = __toESM(require("react"), 1);
|
|
52
52
|
|
|
53
53
|
// src/client/styles.css
|
|
54
54
|
var styles_default = `/**
|
|
@@ -319,6 +319,41 @@ body:has(.dhl-divider.dragging) * { cursor: col-resize !important; }
|
|
|
319
319
|
.dhl-chart-tools button:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
320
320
|
.dhl-chart-tools button.on { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); }
|
|
321
321
|
.dhl-chart-actions { display: flex; align-items: center; gap: 3px; }
|
|
322
|
+
.dhl-favorite-tools { display: flex; align-items: center; gap: 3px; padding: 4px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1); flex-wrap: wrap; }
|
|
323
|
+
.dhl-favorite-tools button { display: grid; place-items: center; width: 28px; height: 28px; padding: 4px; border: 0; border-radius: 5px; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; }
|
|
324
|
+
.dhl-favorite-tools button:hover, .dhl-favorite-tools button.on { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-brand-primary); }
|
|
325
|
+
.dhl-favorite-tools small { color: var(--dsw-alias-label-tertiary); font-size: 10px; margin-left: 6px; }
|
|
326
|
+
.dhl-chart-navigation { position: relative; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; padding: 5px 10px; border-top: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
327
|
+
.dhl-chart-navigation button, .dhl-chart-navigation summary { display: inline-flex; gap: 5px; align-items: center; justify-content: center; padding: 5px 7px; border: 0; border-radius: 5px; background: transparent; color: inherit; font: inherit; cursor: pointer; list-style: none; }
|
|
328
|
+
.dhl-chart-navigation summary::-webkit-details-marker { display: none; }
|
|
329
|
+
.dhl-chart-navigation button:hover, .dhl-chart-navigation summary:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
330
|
+
.dhl-chart-navigation button:disabled { opacity: .4; cursor: default; }
|
|
331
|
+
.dhl-chart-ranges { display: flex; gap: 2px; }
|
|
332
|
+
.dhl-chart-footer-spacer { flex: 1; }
|
|
333
|
+
.dhl-chart-footer-menu { position: relative; }
|
|
334
|
+
.dhl-chart-footer-menu > div, .dhl-chart-footer-menu > form { position: absolute; z-index: 15; bottom: calc(100% + 8px); right: 0; min-width: 225px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); box-shadow: 0 6px 22px rgba(0,0,0,.15); }
|
|
335
|
+
.dhl-chart-footer-menu > .dhl-chart-date-form { left: 0; right: auto; width: 270px; display: grid; gap: 8px; }
|
|
336
|
+
.dhl-chart-date-form label { font-weight: 600; color: var(--dsw-alias-label-primary); }
|
|
337
|
+
.dhl-chart-date-form small { line-height: 1.5; }
|
|
338
|
+
.dhl-chart-date-form input { width: 100%; min-width: 0; border-radius: 7px; color-scheme: light dark; }
|
|
339
|
+
.dhl-chart-date-form button { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-brand-primary); }
|
|
340
|
+
.dhl-chart-export-menu { display: grid; gap: 4px; }
|
|
341
|
+
.dhl-chart-export-menu button { justify-content: flex-start; white-space: nowrap; }
|
|
342
|
+
.dhl-chart-shortcuts { width: min(340px, 85vw); }
|
|
343
|
+
.dhl-chart-shortcuts strong { display: block; margin-bottom: 8px; color: var(--dsw-alias-label-primary); }
|
|
344
|
+
.dhl-chart-shortcuts > div { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 5px 0; }
|
|
345
|
+
.dhl-chart-shortcuts kbd { font: inherit; color: var(--dsw-alias-label-primary); }
|
|
346
|
+
.dhl-draw-tool-row { display: flex; align-items: center; }
|
|
347
|
+
.dhl-chart-tools .dhl-draw-popover .dhl-draw-tool-row > button:first-child { flex: 1; min-width: 0; }
|
|
348
|
+
.dhl-chart-tools .dhl-draw-popover .dhl-draw-tool-row > .dhl-draw-favorite { flex: 0 0 30px; width: 30px; justify-content: center; padding: 7px 5px; }
|
|
349
|
+
.dhl-draw-favorite[aria-pressed="true"] { color: var(--dsw-alias-brand-primary); }
|
|
350
|
+
.dhl-coordinate-editor { display: grid; gap: 10px; }
|
|
351
|
+
.dhl-coordinate-editor label { display: grid; gap: 5px; font-size: 12px; }
|
|
352
|
+
.dhl-coordinate-editor input { min-width: 0; width: 100%; border-radius: 7px; }
|
|
353
|
+
.dhl-coordinate-editor small { font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
354
|
+
.dhl-coordinate-anchors { max-height: 320px; overflow: auto; display: grid; gap: 8px; }
|
|
355
|
+
.dhl-coordinate-anchors fieldset { display: grid; gap: 8px; margin: 0; padding: 10px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 7px; }
|
|
356
|
+
.dhl-coordinate-anchors legend { color: var(--dsw-alias-label-secondary); font-size: 11px; }
|
|
322
357
|
.dhl-chart-actions button:disabled { opacity: .4; cursor: default; }
|
|
323
358
|
.dhl-layer-menu { position: relative; font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
324
359
|
.dhl-layer-menu summary { cursor: pointer; padding: 4px 6px; white-space: nowrap; }
|
|
@@ -445,10 +480,6 @@ body:has(.dhl-divider.dragging) * { cursor: col-resize !important; }
|
|
|
445
480
|
.dhl-chart-drawing-preview { stroke: currentColor; stroke-width: 1.5; stroke-dasharray: 5 4; opacity: .9; }
|
|
446
481
|
.dhl-chart-ohlc { position: absolute; z-index: 1; top: 6px; left: 10px; display: flex; flex-wrap: wrap; gap: 4px 10px; pointer-events: none; color: var(--dsw-alias-label-secondary); font-size: 10px; font-variant-numeric: tabular-nums; }
|
|
447
482
|
.dhl-chart-ohlc b { font-weight: 550; }
|
|
448
|
-
.dhl-chart-annotations { position: absolute; z-index: 1; right: 56px; top: 25px; display: flex; flex-direction: column; align-items: flex-end; gap: 3px; max-width: 45%; pointer-events: none; font-size: 10px; }
|
|
449
|
-
.dhl-chart-annotations span { padding: 2px 5px; border-radius: 3px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
|
|
450
|
-
.dhl-chart-annotations .role-support { color: var(--dsw-alias-state-success-primary); }
|
|
451
|
-
.dhl-chart-annotations .role-resistance { color: var(--dsw-alias-state-error-primary); }
|
|
452
483
|
.dhl-chart-draw-hint { padding: 6px 12px; color: var(--dsw-alias-brand-primary); font-size: 11px; border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
453
484
|
.dhl-chart-click-hint { padding: 5px 8px 0; color: var(--dsw-alias-label-secondary); font-size: 10.5px; line-height: 1.4; }
|
|
454
485
|
.dhl-chart-mark-list { display: flex; flex-wrap: wrap; gap: 5px; padding: 5px 10px; border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
@@ -821,6 +852,73 @@ body:has(.dhl-divider.dragging) * { cursor: col-resize !important; }
|
|
|
821
852
|
.dhl-sticker-custom button { height: 32px; padding: 0 14px; border: 0; border-radius: 8px; background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary)); color: var(--dsw-alias-label-primary-foreground, #fff); font: inherit; font-size: 13px; cursor: pointer; }
|
|
822
853
|
.dhl-sticker-custom button:disabled { opacity: .4; cursor: default; }
|
|
823
854
|
.dhl-icon-grid { grid-template-columns: repeat(6, 1fr); }
|
|
855
|
+
|
|
856
|
+
/* Expanded charts reserve room for the quick tools, navigation and drawing list. */
|
|
857
|
+
.dhl-chart-panel-expanded .dhl-chart { display: flex; flex-direction: column; overflow: auto; }
|
|
858
|
+
.dhl-chart-panel-expanded .dhl-chartbox { height: auto; flex: 1 0 240px; }
|
|
859
|
+
.dhl-chart-panel-expanded .dhl-chart-tools { flex-wrap: wrap; flex-shrink: 0; }
|
|
860
|
+
.dhl-chart-panel-expanded .dhl-intervals, .dhl-chart-panel-expanded .dhl-chart-actions { flex-wrap: wrap; }
|
|
861
|
+
.dhl-chart-panel-expanded .dhl-favorite-tools, .dhl-chart-panel-expanded .dhl-chart-click-hint, .dhl-chart-panel-expanded .dhl-chart-navigation { flex-shrink: 0; }
|
|
862
|
+
.dhl-chart-panel-expanded .dhl-drawing-list { max-height: 130px; flex-shrink: 0; overflow: auto; }
|
|
863
|
+
.dhl-draw-popover .dhl-draw-favorite[aria-pressed="true"] svg { color: var(--dsw-alias-brand-primary); }
|
|
864
|
+
|
|
865
|
+
/* Research and evidence share the existing desk tokens. */
|
|
866
|
+
.dhl-research-controls { display:flex; align-items:center; flex-wrap:wrap; gap:12px; padding:8px 12px; border-bottom:1px solid var(--dsw-alias-border-l1); font-size:11px; }
|
|
867
|
+
.dhl-research-controls label { display:flex; gap:5px; align-items:center; cursor:pointer; }
|
|
868
|
+
.dhl-research-grid { display:grid; grid-template-columns:minmax(0,1fr); }
|
|
869
|
+
.dhl-research-grid.dual, .dhl-research-grid.quad { grid-template-columns:repeat(2,minmax(0,1fr)); gap:1px; background:var(--dsw-alias-border-l1); }
|
|
870
|
+
.dhl-research-pane { min-width:0; background:var(--dsw-alias-bg-layer-1); }
|
|
871
|
+
.dhl-research-pane > header { display:flex; gap:8px; align-items:center; min-height:36px; padding:4px 10px; font-size:11px; }
|
|
872
|
+
.dhl-research-pane > header small { margin-left:auto; color:var(--dsw-alias-label-secondary); }
|
|
873
|
+
.dhl-chart-panel-expanded .dhl-research-grid.single .dhl-primary-pane { display:flex; flex-direction:column; min-height:0; flex:1; }
|
|
874
|
+
.dhl-data-window { padding:10px 12px; border-top:1px solid var(--dsw-alias-border-l1); font-size:11px; }
|
|
875
|
+
.dhl-data-window header { display:flex; flex-wrap:wrap; gap:10px; align-items:center; color:var(--dsw-alias-label-secondary); }
|
|
876
|
+
.dhl-data-window header strong { color:var(--dsw-alias-label-primary); }
|
|
877
|
+
.dhl-data-window dl { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); margin:8px 0; gap:5px 20px; }
|
|
878
|
+
.dhl-data-window dl > div { display:flex; justify-content:space-between; gap:8px; }
|
|
879
|
+
.dhl-data-window dt { color:var(--dsw-alias-label-secondary); }
|
|
880
|
+
.dhl-data-window dd { margin:0; font-variant-numeric:tabular-nums; }
|
|
881
|
+
.dhl-data-window small { color:var(--dsw-alias-label-secondary); }
|
|
882
|
+
.dhl-study-settings { display:flex; flex-direction:column; gap:16px; max-height:65vh; overflow:auto; padding:2px; }
|
|
883
|
+
.dhl-study-template { display:flex; flex-direction:column; gap:8px; }
|
|
884
|
+
.dhl-study-template small { color:var(--dsw-alias-label-secondary); }
|
|
885
|
+
.dhl-study-template-actions { display:flex; flex-wrap:wrap; gap:8px; }
|
|
886
|
+
.dhl-study-parameter-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; }
|
|
887
|
+
.dhl-study-parameter-grid fieldset { border:1px solid var(--dsw-alias-border-l1); border-radius:8px; padding:12px; min-width:0; }
|
|
888
|
+
.dhl-study-parameter-grid legend { font-size:12px; font-weight:600; }
|
|
889
|
+
.dhl-study-parameter-grid label { display:flex; align-items:center; justify-content:space-between; gap:10px; margin:6px 0; font-size:12px; }
|
|
890
|
+
.dhl-study-parameter-grid input { width:90px; }
|
|
891
|
+
.dhl-indicator-settings-trigger { gap:8px; border-top:1px solid var(--dsw-alias-border-l1)!important; }
|
|
892
|
+
.dhl-review { font-size:12px; }
|
|
893
|
+
.dhl-review .dhl-history-controls { flex-wrap:wrap; }
|
|
894
|
+
.dhl-review-summary { display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px; padding:10px 0; }
|
|
895
|
+
.dhl-review-summary > div { display:flex; flex-direction:column; gap:5px; }
|
|
896
|
+
.dhl-review-summary span, .dhl-evidence-note { color:var(--dsw-alias-label-secondary); font-size:11px; line-height:1.65; }
|
|
897
|
+
.dhl-review-summary strong { font-size:15px; font-variant-numeric:tabular-nums; }
|
|
898
|
+
.dhl-review details { border-top:1px solid var(--dsw-alias-border-l1); padding:9px 0; }
|
|
899
|
+
.dhl-review summary { cursor:pointer; line-height:1.8; }
|
|
900
|
+
.dhl-review-fill summary { word-spacing:3px; }
|
|
901
|
+
.dhl-review-fill summary time { color:var(--dsw-alias-label-secondary); }
|
|
902
|
+
.dhl-evidence-detail { display:flex; flex-direction:column; align-items:flex-start; gap:8px; padding:10px 14px; overflow-wrap:anywhere; }
|
|
903
|
+
.dhl-review-order { display:grid; grid-template-columns:1fr 1fr 1.3fr; gap:10px; padding:6px 0; }
|
|
904
|
+
@media(max-width:1000px) { .dhl-research-grid.dual,.dhl-research-grid.quad { grid-template-columns:minmax(0,1fr); } }
|
|
905
|
+
@media(max-width:520px) { .dhl-study-parameter-grid { grid-template-columns:minmax(0,1fr); } }
|
|
906
|
+
|
|
907
|
+
.dhl-chart-panel-expanded .dhl-research-grid.single { flex:1; min-height:0; }
|
|
908
|
+
|
|
909
|
+
/* Fixed range volume profile: up/down volume per price row, value area brighter, POC in the alert red. */
|
|
910
|
+
.dhl-vprofile-range { fill: color-mix(in srgb, currentColor 5%, transparent); stroke: none; }
|
|
911
|
+
.dhl-vprofile rect.dhl-vprofile-up, .dhl-vprofile rect.dhl-vprofile-down { stroke: none; }
|
|
912
|
+
.dhl-vprofile-up { fill: color-mix(in srgb, #4c82d8 62%, transparent); }
|
|
913
|
+
.dhl-vprofile-down { fill: color-mix(in srgb, #e2a943 62%, transparent); }
|
|
914
|
+
.dhl-vprofile-out .dhl-vprofile-up { fill: color-mix(in srgb, #4c82d8 26%, transparent); }
|
|
915
|
+
.dhl-vprofile-out .dhl-vprofile-down { fill: color-mix(in srgb, #e2a943 26%, transparent); }
|
|
916
|
+
.dhl-vprofile-poc { stroke: #ef5f6c; stroke-width: 1.5; }
|
|
917
|
+
.dhl-drawing .dhl-vprofile-warn { fill: var(--dsw-alias-state-warning-primary, #e2a943); }
|
|
918
|
+
|
|
919
|
+
/* Magnet feedback: a ring on the O/H/L/C value the anchor will snap to. */
|
|
920
|
+
.dhl-snap-ring { fill: none; stroke: var(--dsw-alias-brand-primary, #4c82d8); stroke-width: 1.5; pointer-events: none; }
|
|
921
|
+
.dhl-draw-toolbar .dhl-select > select { height: 26px; font-size: 11px; }
|
|
824
922
|
`;
|
|
825
923
|
|
|
826
924
|
// src/client/locale.ts
|
|
@@ -1195,8 +1293,8 @@ function getContext() {
|
|
|
1195
1293
|
function setLocaleBinder(binder) {
|
|
1196
1294
|
localeT = binder;
|
|
1197
1295
|
}
|
|
1198
|
-
function t(
|
|
1199
|
-
return localeT(
|
|
1296
|
+
function t(key3, fallback) {
|
|
1297
|
+
return localeT(key3, fallback);
|
|
1200
1298
|
}
|
|
1201
1299
|
function tr(zh, en) {
|
|
1202
1300
|
return ctxRef?.get?.("locale")?.getLocale?.()?.active === "en" ? en : zh;
|
|
@@ -1211,12 +1309,87 @@ async function hyperCall(method, request = null) {
|
|
|
1211
1309
|
if (envelope && typeof envelope === "object" && envelope.ok === true) return envelope.value;
|
|
1212
1310
|
throw new Error(t("error.badResponse", "\u8FDC\u7AEF\u54CD\u5E94\u683C\u5F0F\u9519\u8BEF"));
|
|
1213
1311
|
}
|
|
1312
|
+
try {
|
|
1313
|
+
window.__dhlCall = hyperCall;
|
|
1314
|
+
} catch {
|
|
1315
|
+
}
|
|
1214
1316
|
|
|
1215
1317
|
// src/client/app.tsx
|
|
1216
|
-
var
|
|
1217
|
-
var
|
|
1318
|
+
var React32 = __toESM(require("react"), 1);
|
|
1319
|
+
var import_dsh_client_ui_primitives10 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
1218
1320
|
|
|
1219
|
-
//
|
|
1321
|
+
// lib/studies.js
|
|
1322
|
+
var DEFAULT_INDICATOR_SETTINGS = {
|
|
1323
|
+
rsi: { period: 14, lower: 30, upper: 70 },
|
|
1324
|
+
macd: { fast: 12, slow: 26, signal: 9 },
|
|
1325
|
+
bollinger: { period: 20, multiplier: 2 },
|
|
1326
|
+
stochastic: { period: 14, smoothK: 3, smoothD: 3, lower: 20, upper: 80 },
|
|
1327
|
+
adx: { period: 14, smoothing: 14 }
|
|
1328
|
+
};
|
|
1329
|
+
function normalizeIndicatorSettings(input = {}) {
|
|
1330
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !Object.hasOwn(DEFAULT_INDICATOR_SETTINGS, key3))) throw Error("\u6307\u6807\u53C2\u6570\u65E0\u6548");
|
|
1331
|
+
const result = {};
|
|
1332
|
+
for (const [section, defaults] of Object.entries(DEFAULT_INDICATOR_SETTINGS)) {
|
|
1333
|
+
const supplied = input[section] ?? {};
|
|
1334
|
+
if (typeof supplied !== "object" || Array.isArray(supplied) || Object.keys(supplied).some((key3) => !Object.hasOwn(defaults, key3))) throw Error("\u6307\u6807\u53C2\u6570\u5B57\u6BB5\u65E0\u6548");
|
|
1335
|
+
const values = { ...defaults, ...supplied };
|
|
1336
|
+
for (const [key3, value] of Object.entries(values)) {
|
|
1337
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw Error(`${section}.${key3} \u5FC5\u987B\u4E3A\u6570\u503C`);
|
|
1338
|
+
if (key3 === "multiplier") {
|
|
1339
|
+
if (value <= 0 || value > 10) throw Error("\u5E03\u6797\u5E26\u500D\u6570\u5FC5\u987B\u5927\u4E8E 0 \u4E14\u4E0D\u8D85\u8FC7 10");
|
|
1340
|
+
} else if (key3 === "lower" || key3 === "upper") {
|
|
1341
|
+
if (value < 0 || value > 100) throw Error("\u6307\u6807\u9608\u503C\u5FC5\u987B\u4E3A 0\u2013100");
|
|
1342
|
+
} else if (!Number.isInteger(value) || value < 1 || value > 500) throw Error("\u6307\u6807\u5468\u671F\u5FC5\u987B\u4E3A 1\u2013500 \u7684\u6574\u6570");
|
|
1343
|
+
}
|
|
1344
|
+
if ("lower" in values && values.lower >= values.upper) throw Error("\u6307\u6807\u4E0B\u9608\u503C\u5FC5\u987B\u5C0F\u4E8E\u4E0A\u9608\u503C");
|
|
1345
|
+
result[section] = values;
|
|
1346
|
+
}
|
|
1347
|
+
if (result.macd.fast >= result.macd.slow) throw Error("MACD \u5FEB\u7EBF\u5468\u671F\u5FC5\u987B\u5C0F\u4E8E\u6162\u7EBF\u5468\u671F");
|
|
1348
|
+
return result;
|
|
1349
|
+
}
|
|
1350
|
+
function nullableSma(values, period) {
|
|
1351
|
+
return values.map((_, i) => {
|
|
1352
|
+
if (i < period - 1) return null;
|
|
1353
|
+
const window2 = values.slice(i - period + 1, i + 1);
|
|
1354
|
+
return window2.some((value) => value == null) ? null : window2.reduce((sum, value) => sum + value, 0) / period;
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
function rma(values, period) {
|
|
1358
|
+
let prior = null, seed = [];
|
|
1359
|
+
return values.map((value) => {
|
|
1360
|
+
if (value == null) return null;
|
|
1361
|
+
if (prior == null) {
|
|
1362
|
+
seed.push(value);
|
|
1363
|
+
if (seed.length < period) return null;
|
|
1364
|
+
prior = seed.reduce((sum, item) => sum + item, 0) / period;
|
|
1365
|
+
} else prior = (prior * (period - 1) + value) / period;
|
|
1366
|
+
return prior;
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
function studyDefinitions(input) {
|
|
1370
|
+
const p = normalizeIndicatorSettings(input);
|
|
1371
|
+
return [
|
|
1372
|
+
["sma20", "sma20", "SMA20"],
|
|
1373
|
+
["sma60", "sma60", "MA60"],
|
|
1374
|
+
["ema20", "ema20", "EMA20"],
|
|
1375
|
+
["ema50", "ema50", "EMA50"],
|
|
1376
|
+
["bollinger20", "basis", `BOLL ${p.bollinger.period} \xD7 ${p.bollinger.multiplier}`],
|
|
1377
|
+
["bollinger20", "upper", "BOLL upper"],
|
|
1378
|
+
["bollinger20", "lower", "BOLL lower"],
|
|
1379
|
+
["rsi14", "rsi14", `RSI${p.rsi.period}`],
|
|
1380
|
+
["macd", "macd", p.macd.fast === 12 && p.macd.slow === 26 && p.macd.signal === 9 ? "MACD" : `MACD ${p.macd.fast}/${p.macd.slow}/${p.macd.signal}`],
|
|
1381
|
+
["macd", "signal", "Signal"],
|
|
1382
|
+
["macd", "histogram", "Hist"],
|
|
1383
|
+
["atr14", "atr14", "ATR14"],
|
|
1384
|
+
["vwap", "vwap", "VWAP (UTC HLC3)"],
|
|
1385
|
+
["stochastic", "stochK", `Stoch %K ${p.stochastic.period}/${p.stochastic.smoothK}`],
|
|
1386
|
+
["stochastic", "stochD", `Stoch %D ${p.stochastic.smoothD}`],
|
|
1387
|
+
["adx", "adx", `ADX ${p.adx.period}/${p.adx.smoothing}`],
|
|
1388
|
+
["adx", "plusDI", "+DI"],
|
|
1389
|
+
["adx", "minusDI", "-DI"],
|
|
1390
|
+
["obv", "obv", "OBV"]
|
|
1391
|
+
];
|
|
1392
|
+
}
|
|
1220
1393
|
function points(rows, values) {
|
|
1221
1394
|
return values.flatMap((value, index) => value == null || !Number.isFinite(value) ? [] : [{ time: Math.floor(rows[index].t / 1e3), value }]);
|
|
1222
1395
|
}
|
|
@@ -1245,16 +1418,17 @@ function ema(values, period) {
|
|
|
1245
1418
|
}
|
|
1246
1419
|
return result;
|
|
1247
1420
|
}
|
|
1248
|
-
function
|
|
1421
|
+
function computeStudies(candles, input) {
|
|
1422
|
+
const settings = normalizeIndicatorSettings(input);
|
|
1249
1423
|
const rows = candles.filter((row) => Number.isFinite(row.c) && row.c > 0 && Number.isFinite(row.t));
|
|
1250
1424
|
const closes = rows.map((row) => row.c);
|
|
1251
|
-
const mean = sma(closes,
|
|
1252
|
-
const ema12 = ema(closes,
|
|
1253
|
-
const ema26 = ema(closes,
|
|
1425
|
+
const mean = sma(closes, settings.bollinger.period);
|
|
1426
|
+
const ema12 = ema(closes, settings.macd.fast);
|
|
1427
|
+
const ema26 = ema(closes, settings.macd.slow);
|
|
1254
1428
|
const macd = ema12.map((value, index) => value == null || ema26[index] == null ? null : value - ema26[index]);
|
|
1255
|
-
const signal = ema(macd,
|
|
1429
|
+
const signal = ema(macd, settings.macd.signal);
|
|
1256
1430
|
const deviation = mean.map((value, index) => value == null ? null : Math.sqrt(
|
|
1257
|
-
closes.slice(index -
|
|
1431
|
+
closes.slice(index - settings.bollinger.period + 1, index + 1).reduce((total, close) => total + (close - value) ** 2, 0) / settings.bollinger.period
|
|
1258
1432
|
));
|
|
1259
1433
|
const rsi = Array(rows.length).fill(null);
|
|
1260
1434
|
let gain = 0;
|
|
@@ -1263,34 +1437,98 @@ function chartIndicators(candles) {
|
|
|
1263
1437
|
const delta = closes[i] - closes[i - 1];
|
|
1264
1438
|
const up = Math.max(delta, 0);
|
|
1265
1439
|
const down = Math.max(-delta, 0);
|
|
1266
|
-
if (i <=
|
|
1440
|
+
if (i <= settings.rsi.period) {
|
|
1267
1441
|
gain += up;
|
|
1268
1442
|
loss += down;
|
|
1269
1443
|
} else {
|
|
1270
|
-
gain = gain *
|
|
1271
|
-
loss = loss *
|
|
1444
|
+
gain = (gain * (settings.rsi.period - 1) + up) / settings.rsi.period;
|
|
1445
|
+
loss = (loss * (settings.rsi.period - 1) + down) / settings.rsi.period;
|
|
1272
1446
|
}
|
|
1273
|
-
if (i ===
|
|
1274
|
-
gain /=
|
|
1275
|
-
loss /=
|
|
1447
|
+
if (i === settings.rsi.period) {
|
|
1448
|
+
gain /= settings.rsi.period;
|
|
1449
|
+
loss /= settings.rsi.period;
|
|
1276
1450
|
}
|
|
1277
|
-
if (i >=
|
|
1451
|
+
if (i >= settings.rsi.period) rsi[i] = loss === 0 ? gain === 0 ? 50 : 100 : 100 - 100 / (1 + gain / loss);
|
|
1278
1452
|
}
|
|
1279
1453
|
const point = (values) => points(rows, values);
|
|
1454
|
+
const atr = Array(rows.length).fill(null);
|
|
1455
|
+
let rangeSum = 0;
|
|
1456
|
+
for (let i = 1; i < rows.length; i += 1) {
|
|
1457
|
+
const range = Math.max(rows[i].h - rows[i].l, Math.abs(rows[i].h - rows[i - 1].c), Math.abs(rows[i].l - rows[i - 1].c));
|
|
1458
|
+
if (i <= 14) rangeSum += range;
|
|
1459
|
+
if (i === 14) atr[i] = rangeSum / 14;
|
|
1460
|
+
if (i > 14) atr[i] = (atr[i - 1] * 13 + range) / 14;
|
|
1461
|
+
}
|
|
1462
|
+
let day = -1, weighted = 0, volume = 0, complete = false;
|
|
1463
|
+
const vwap = rows.map((row) => {
|
|
1464
|
+
const nextDay = Math.floor(row.t / 864e5);
|
|
1465
|
+
if (nextDay !== day) {
|
|
1466
|
+
complete = day >= 0 || row.t % 864e5 === 0;
|
|
1467
|
+
day = nextDay;
|
|
1468
|
+
weighted = 0;
|
|
1469
|
+
volume = 0;
|
|
1470
|
+
}
|
|
1471
|
+
if (!Number.isFinite(row.v) || row.v < 0) return null;
|
|
1472
|
+
weighted += (row.h + row.l + row.c) / 3 * row.v;
|
|
1473
|
+
volume += row.v;
|
|
1474
|
+
return complete && volume > 0 ? weighted / volume : null;
|
|
1475
|
+
});
|
|
1476
|
+
const rawK = rows.map((row, index) => {
|
|
1477
|
+
if (index < settings.stochastic.period - 1) return null;
|
|
1478
|
+
const window2 = rows.slice(index - settings.stochastic.period + 1, index + 1);
|
|
1479
|
+
const low = Math.min(...window2.map((bar) => bar.l)), high = Math.max(...window2.map((bar) => bar.h));
|
|
1480
|
+
return high === low ? null : 100 * (row.c - low) / (high - low);
|
|
1481
|
+
});
|
|
1482
|
+
const stochK = nullableSma(rawK, settings.stochastic.smoothK);
|
|
1483
|
+
const stochD = nullableSma(stochK, settings.stochastic.smoothD);
|
|
1484
|
+
const tr2 = rows.map((row, i) => i === 0 ? null : Math.max(row.h - row.l, Math.abs(row.h - rows[i - 1].c), Math.abs(row.l - rows[i - 1].c)));
|
|
1485
|
+
const plus = rows.map((row, i) => {
|
|
1486
|
+
if (!i) return null;
|
|
1487
|
+
const up = row.h - rows[i - 1].h, down = rows[i - 1].l - row.l;
|
|
1488
|
+
return up > down && up > 0 ? up : 0;
|
|
1489
|
+
});
|
|
1490
|
+
const minus = rows.map((row, i) => {
|
|
1491
|
+
if (!i) return null;
|
|
1492
|
+
const up = row.h - rows[i - 1].h, down = rows[i - 1].l - row.l;
|
|
1493
|
+
return down > up && down > 0 ? down : 0;
|
|
1494
|
+
});
|
|
1495
|
+
const atrDm = rma(tr2, settings.adx.period), plusRma = rma(plus, settings.adx.period), minusRma = rma(minus, settings.adx.period);
|
|
1496
|
+
const di = (values) => values.map((value, i) => value == null || atrDm[i] == null ? null : atrDm[i] === 0 ? 0 : 100 * value / atrDm[i]);
|
|
1497
|
+
const plusDI = di(plusRma), minusDI = di(minusRma);
|
|
1498
|
+
const dx = plusDI.map((value, i) => value == null || minusDI[i] == null ? null : value + minusDI[i] === 0 ? 0 : 100 * Math.abs(value - minusDI[i]) / (value + minusDI[i]));
|
|
1499
|
+
let balance = 0;
|
|
1500
|
+
const obv = rows.map((row, i) => {
|
|
1501
|
+
if (i) balance += Math.sign(row.c - rows[i - 1].c) * row.v;
|
|
1502
|
+
return balance;
|
|
1503
|
+
});
|
|
1280
1504
|
return {
|
|
1281
|
-
|
|
1505
|
+
stochK: point(stochK),
|
|
1506
|
+
stochD: point(stochD),
|
|
1507
|
+
adx: point(rma(dx, settings.adx.smoothing)),
|
|
1508
|
+
plusDI: point(plusDI),
|
|
1509
|
+
minusDI: point(minusDI),
|
|
1510
|
+
obv: point(obv),
|
|
1511
|
+
sma20: point(sma(closes, 20)),
|
|
1512
|
+
basis: point(mean),
|
|
1282
1513
|
sma60: point(sma(closes, 60)),
|
|
1283
1514
|
ema20: point(ema(closes, 20)),
|
|
1284
1515
|
ema50: point(ema(closes, 50)),
|
|
1285
|
-
upper: point(mean.map((value, index) => value == null ? null : value +
|
|
1286
|
-
lower: point(mean.map((value, index) => value == null ? null : value -
|
|
1516
|
+
upper: point(mean.map((value, index) => value == null ? null : value + settings.bollinger.multiplier * deviation[index])),
|
|
1517
|
+
lower: point(mean.map((value, index) => value == null ? null : value - settings.bollinger.multiplier * deviation[index])),
|
|
1287
1518
|
rsi14: point(rsi),
|
|
1288
1519
|
macd: point(macd),
|
|
1289
1520
|
signal: point(signal),
|
|
1290
|
-
histogram: point(macd.map((value, index) => value == null || signal[index] == null ? null : value - signal[index]))
|
|
1521
|
+
histogram: point(macd.map((value, index) => value == null || signal[index] == null ? null : value - signal[index])),
|
|
1522
|
+
atr14: point(atr),
|
|
1523
|
+
vwap: point(vwap)
|
|
1291
1524
|
};
|
|
1292
1525
|
}
|
|
1293
1526
|
|
|
1527
|
+
// src/client/chart-indicators.ts
|
|
1528
|
+
function chartIndicators(candles, settings) {
|
|
1529
|
+
return computeStudies(candles, settings);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1294
1532
|
// lib/moving-average.js
|
|
1295
1533
|
var MA_KINDS = ["sma", "ema", "smma", "wma"];
|
|
1296
1534
|
var MA_SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4"];
|
|
@@ -1388,28 +1626,16 @@ function maSeries(candles, line) {
|
|
|
1388
1626
|
}
|
|
1389
1627
|
|
|
1390
1628
|
// src/client/chart-reference.ts
|
|
1391
|
-
|
|
1392
|
-
["sma20", "SMA20", "sma20"],
|
|
1393
|
-
["sma60", "MA60", "sma60"],
|
|
1394
|
-
["ema20", "EMA20", "ema20"],
|
|
1395
|
-
["ema50", "EMA50", "ema50"],
|
|
1396
|
-
["bollinger20", "BOLL\u4E0A\u8F68", "upper"],
|
|
1397
|
-
["bollinger20", "BOLL\u4E0B\u8F68", "lower"],
|
|
1398
|
-
["rsi14", "RSI14", "rsi14"],
|
|
1399
|
-
["macd", "MACD", "macd"],
|
|
1400
|
-
["macd", "\u4FE1\u53F7\u7EBF", "signal"],
|
|
1401
|
-
["macd", "MACD\u67F1", "histogram"]
|
|
1402
|
-
];
|
|
1403
|
-
function selectChartCandle(candles, time, active, hit, maLines = []) {
|
|
1629
|
+
function selectChartCandle(candles, time, active, hit, maLines = [], settings) {
|
|
1404
1630
|
const rows = candles;
|
|
1405
1631
|
const index = rows.findIndex((candle2) => Math.floor(candle2.t / 1e3) === Math.floor(time / 1e3));
|
|
1406
1632
|
if (index < 0) return null;
|
|
1407
1633
|
const candle = rows[index];
|
|
1408
|
-
const studies = chartIndicators(rows);
|
|
1634
|
+
const studies = chartIndicators(rows, settings);
|
|
1409
1635
|
const epoch = Math.floor(candle.t / 1e3);
|
|
1410
|
-
const values =
|
|
1636
|
+
const values = studyDefinitions(settings).flatMap(([id, key3, label]) => {
|
|
1411
1637
|
if (!active.has(id)) return [];
|
|
1412
|
-
const point = studies[
|
|
1638
|
+
const point = studies[key3].find((item) => item.time === epoch);
|
|
1413
1639
|
return point ? [{ label, value: point.value }] : [];
|
|
1414
1640
|
});
|
|
1415
1641
|
for (const line of maLines) {
|
|
@@ -1419,7 +1645,7 @@ function selectChartCandle(candles, time, active, hit, maLines = []) {
|
|
|
1419
1645
|
}
|
|
1420
1646
|
const previous = rows.slice(Math.max(0, index - 20), index).map((row) => row.v).filter((value) => Number.isFinite(value) && value >= 0);
|
|
1421
1647
|
const priorVolumeMean20 = previous.length === 20 ? previous.reduce((sum, value) => sum + value, 0) / 20 : null;
|
|
1422
|
-
return { candle, hit, values, priorVolumeMean20 };
|
|
1648
|
+
return { candle, hit, values, priorVolumeMean20, settings };
|
|
1423
1649
|
}
|
|
1424
1650
|
function formatChartReference(selection, coin, interval, english = false) {
|
|
1425
1651
|
const { candle, hit, values, priorVolumeMean20 } = selection;
|
|
@@ -1434,10 +1660,15 @@ function formatChartReference(selection, coin, interval, english = false) {
|
|
|
1434
1660
|
ema50: "EMA50",
|
|
1435
1661
|
bollingerUpper: english ? "BOLL upper" : "BOLL\u4E0A\u8F68",
|
|
1436
1662
|
bollingerLower: english ? "BOLL lower" : "BOLL\u4E0B\u8F68",
|
|
1437
|
-
rsi14:
|
|
1663
|
+
rsi14: `RSI${selection.settings?.rsi.period ?? 14}`,
|
|
1438
1664
|
macd: "MACD",
|
|
1439
1665
|
macdSignal: english ? "MACD signal" : "\u4FE1\u53F7\u7EBF",
|
|
1440
|
-
macdHistogram: english ? "MACD histogram" : "MACD\u67F1"
|
|
1666
|
+
macdHistogram: english ? "MACD histogram" : "MACD\u67F1",
|
|
1667
|
+
atr14: "ATR14",
|
|
1668
|
+
vwap: "VWAP (UTC HLC3)",
|
|
1669
|
+
stochastic: "Stochastic",
|
|
1670
|
+
adx: "ADX / DMI",
|
|
1671
|
+
obv: "OBV"
|
|
1441
1672
|
};
|
|
1442
1673
|
const lines = [
|
|
1443
1674
|
english ? `[Chart point] ${coin} ${interval} candle opened ${time}` : `\u3010\u56FE\u8868\u70B9\u4F4D\u3011${coin} ${interval} K \u7EBF\u5F00\u76D8 ${time}`,
|
|
@@ -1447,6 +1678,8 @@ function formatChartReference(selection, coin, interval, english = false) {
|
|
|
1447
1678
|
if (priorVolumeMean20 != null && priorVolumeMean20 > 0) {
|
|
1448
1679
|
lines.push(english ? `Previous 20-candle mean volume: ${computed(priorVolumeMean20)}; current/mean: ${(candle.v / priorVolumeMean20).toFixed(2)}x` : `\u524D 20 \u6839\u5747\u91CF\uFF1A${computed(priorVolumeMean20)}\uFF1B\u5F53\u524D/\u5747\u91CF\uFF1A${(candle.v / priorVolumeMean20).toFixed(2)} \u500D`);
|
|
1449
1680
|
}
|
|
1681
|
+
if (selection.settings) lines.push(`${english ? "Study parameters" : "\u6307\u6807\u53C2\u6570"}\uFF1A${JSON.stringify(selection.settings)}`);
|
|
1682
|
+
if (values.some((row) => row.label === "OBV")) lines.push(english ? "OBV starts at zero on the first loaded candle; its absolute value depends on the window." : "OBV \u4EE5\u5DF2\u52A0\u8F7D\u9996\u6839\u4E3A\u96F6\u70B9\uFF0C\u7EDD\u5BF9\u503C\u53D6\u51B3\u4E8E\u5386\u53F2\u7A97\u53E3\u3002");
|
|
1450
1683
|
if (values.length) lines.push(`${english ? "Displayed studies" : "\u5DF2\u663E\u793A\u6307\u6807"}\uFF1A${values.map(({ label, value }) => `${label}=${computed(value)}`).join("\uFF0C")}`);
|
|
1451
1684
|
if (hit && hit !== "volume") lines.push(`${english ? "Selected study" : "\u70B9\u51FB\u6307\u6807"}\uFF1A${hitLabel[hit] ?? hit}`);
|
|
1452
1685
|
return lines.join("\n");
|
|
@@ -1629,6 +1862,7 @@ function applyLocalCoin(coin) {
|
|
|
1629
1862
|
void loadCandles();
|
|
1630
1863
|
void loadSnapshot();
|
|
1631
1864
|
void loadMarketDetail(coin);
|
|
1865
|
+
void loadChartMarks();
|
|
1632
1866
|
}
|
|
1633
1867
|
function liveCandle(value) {
|
|
1634
1868
|
if (!value || typeof value !== "object") return null;
|
|
@@ -1715,9 +1949,9 @@ async function loadMarketDetail(coin) {
|
|
|
1715
1949
|
async function loadSnapshot() {
|
|
1716
1950
|
const { activeCoin: coin, interval, bookGrouping } = store.get();
|
|
1717
1951
|
const epoch = networkEpoch;
|
|
1718
|
-
const
|
|
1719
|
-
if (pendingSnapshots.has(
|
|
1720
|
-
pendingSnapshots.add(
|
|
1952
|
+
const key3 = `${epoch}:${coin}:${interval}:${bookGrouping}`;
|
|
1953
|
+
if (pendingSnapshots.has(key3)) return;
|
|
1954
|
+
pendingSnapshots.add(key3);
|
|
1721
1955
|
try {
|
|
1722
1956
|
const snapshot = await hyperCall("snapshot", { coin, interval, bookGrouping });
|
|
1723
1957
|
if (epoch !== networkEpoch || store.get().activeCoin !== coin || store.get().interval !== interval || store.get().bookGrouping !== bookGrouping) return;
|
|
@@ -1731,22 +1965,22 @@ async function loadSnapshot() {
|
|
|
1731
1965
|
} catch (e) {
|
|
1732
1966
|
if (epoch === networkEpoch && store.get().activeCoin === coin && store.get().interval === interval && store.get().bookGrouping === bookGrouping) store.patch({ snapshotError: String(e?.message ?? e) });
|
|
1733
1967
|
} finally {
|
|
1734
|
-
pendingSnapshots.delete(
|
|
1968
|
+
pendingSnapshots.delete(key3);
|
|
1735
1969
|
}
|
|
1736
1970
|
}
|
|
1737
1971
|
async function loadOlderCandles() {
|
|
1738
1972
|
const { activeCoin: coin, interval, candles, candlesState, candlesOlder } = store.get();
|
|
1739
1973
|
const first = candles[0];
|
|
1740
1974
|
if (olderPending || !first || candlesState !== "ready" || candlesOlder === "done" || candles.length >= MAX_CANDLES) return;
|
|
1741
|
-
const
|
|
1742
|
-
if (candlesFor !==
|
|
1975
|
+
const key3 = `${coin}:${interval}`;
|
|
1976
|
+
if (candlesFor !== key3) return;
|
|
1743
1977
|
olderPending = true;
|
|
1744
1978
|
const epoch = networkEpoch;
|
|
1745
1979
|
store.patch({ candlesOlder: "loading" });
|
|
1746
1980
|
try {
|
|
1747
1981
|
const endMs = first.t - 1;
|
|
1748
1982
|
const res = await hyperCall("candles", { coin, interval, startMs: endMs - INTERVAL_MS[interval] * OLDER_PAGE_BARS, endMs });
|
|
1749
|
-
if (epoch !== networkEpoch || candlesFor !==
|
|
1983
|
+
if (epoch !== networkEpoch || candlesFor !== key3) return;
|
|
1750
1984
|
const current2 = store.get().candles;
|
|
1751
1985
|
const head = current2[0]?.t ?? Infinity;
|
|
1752
1986
|
const older = (res.candles ?? []).filter((row) => row.t < head);
|
|
@@ -1754,7 +1988,7 @@ async function loadOlderCandles() {
|
|
|
1754
1988
|
const next = older.slice(-room);
|
|
1755
1989
|
store.patch({ candles: [...next, ...current2], candlesOlder: next.length === 0 || next.length === room ? "done" : "idle" });
|
|
1756
1990
|
} catch {
|
|
1757
|
-
if (epoch === networkEpoch && candlesFor ===
|
|
1991
|
+
if (epoch === networkEpoch && candlesFor === key3) store.patch({ candlesOlder: "error" });
|
|
1758
1992
|
} finally {
|
|
1759
1993
|
olderPending = false;
|
|
1760
1994
|
}
|
|
@@ -1767,13 +2001,13 @@ async function loadCandles() {
|
|
|
1767
2001
|
const res = await hyperCall("candles", { coin, interval });
|
|
1768
2002
|
if (epoch === networkEpoch && store.get().activeCoin === coin && store.get().interval === interval) {
|
|
1769
2003
|
const fetched = res.candles ?? [];
|
|
1770
|
-
const
|
|
2004
|
+
const key3 = `${coin}:${interval}`;
|
|
1771
2005
|
const prev = store.get().candles;
|
|
1772
|
-
const same = candlesFor ===
|
|
2006
|
+
const same = candlesFor === key3;
|
|
1773
2007
|
const older = same && fetched.length ? prev.filter((row) => row.t < fetched[0].t) : [];
|
|
1774
2008
|
const rows = [...older, ...fetched];
|
|
1775
2009
|
const lastLive = same ? prev.at(-1) : void 0;
|
|
1776
|
-
candlesFor =
|
|
2010
|
+
candlesFor = key3;
|
|
1777
2011
|
store.patch({
|
|
1778
2012
|
candles: lastLive && lastLive.t >= (rows.at(-1)?.t ?? 0) ? mergeCandle(rows.filter((row) => row.t < lastLive.t), lastLive) : rows,
|
|
1779
2013
|
candlesState: "ready",
|
|
@@ -1786,17 +2020,17 @@ async function loadCandles() {
|
|
|
1786
2020
|
}
|
|
1787
2021
|
async function loadChartMarks() {
|
|
1788
2022
|
const { activeSessionId: sessionId, activeCoin: coin } = store.get();
|
|
1789
|
-
const
|
|
2023
|
+
const key3 = `${sessionId ?? ""}:${coin}`;
|
|
1790
2024
|
const version = ++chartMarksLoadVersion;
|
|
1791
2025
|
if (!sessionId) {
|
|
1792
|
-
store.patch({ chartMarks: [], chartMarksKey:
|
|
2026
|
+
store.patch({ chartMarks: [], chartMarksKey: key3 });
|
|
1793
2027
|
return;
|
|
1794
2028
|
}
|
|
1795
2029
|
try {
|
|
1796
2030
|
const res = await hyperCall("chartMarks", { sessionId, coin, allIntervals: true });
|
|
1797
2031
|
const current2 = store.get();
|
|
1798
|
-
if (version === chartMarksLoadVersion && `${current2.activeSessionId ?? ""}:${current2.activeCoin}` ===
|
|
1799
|
-
store.patch({ chartMarks: res.marks ?? [], chartMarksKey:
|
|
2032
|
+
if (version === chartMarksLoadVersion && `${current2.activeSessionId ?? ""}:${current2.activeCoin}` === key3) {
|
|
2033
|
+
store.patch({ chartMarks: res.marks ?? [], chartMarksKey: key3 });
|
|
1800
2034
|
}
|
|
1801
2035
|
} catch {
|
|
1802
2036
|
}
|
|
@@ -1851,14 +2085,30 @@ async function loadSessions() {
|
|
|
1851
2085
|
} catch {
|
|
1852
2086
|
}
|
|
1853
2087
|
}
|
|
2088
|
+
var maReloadVersion = 0;
|
|
1854
2089
|
async function reloadMaLines(sessionId) {
|
|
2090
|
+
const version = ++maReloadVersion;
|
|
1855
2091
|
try {
|
|
1856
2092
|
const workspace = await hyperCall("sessionWorkspace", { sessionId });
|
|
1857
2093
|
const state = store.get();
|
|
2094
|
+
if (version !== maReloadVersion) return;
|
|
1858
2095
|
if (state.workspaceSessionId === sessionId && state.workspace) store.patch({ workspace: { ...state.workspace, maLines: workspace.maLines ?? [] } });
|
|
1859
2096
|
} catch {
|
|
1860
2097
|
}
|
|
1861
2098
|
}
|
|
2099
|
+
var viewReloadVersion = 0;
|
|
2100
|
+
async function reloadChartView(sessionId) {
|
|
2101
|
+
const version = ++viewReloadVersion;
|
|
2102
|
+
try {
|
|
2103
|
+
const workspace = await hyperCall("sessionWorkspace", { sessionId });
|
|
2104
|
+
const state = store.get();
|
|
2105
|
+
if (version !== viewReloadVersion || state.workspaceSessionId !== sessionId || !state.workspace) return;
|
|
2106
|
+
const intervalChanged = workspace.interval !== state.interval;
|
|
2107
|
+
store.patch({ workspace: { ...state.workspace, interval: workspace.interval, indicators: workspace.indicators }, interval: workspace.interval });
|
|
2108
|
+
if (intervalChanged) void loadCandles();
|
|
2109
|
+
} catch {
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
1862
2112
|
async function pollEvents() {
|
|
1863
2113
|
if (eventsPending) return;
|
|
1864
2114
|
eventsPending = true;
|
|
@@ -1879,6 +2129,7 @@ async function pollEvents() {
|
|
|
1879
2129
|
if (ev.data.coin === store.get().activeCoin) void loadChartMarks();
|
|
1880
2130
|
}
|
|
1881
2131
|
if (ev.kind === "chart.ma" && ev.data?.sessionId === store.get().activeSessionId) void reloadMaLines(ev.data.sessionId);
|
|
2132
|
+
if (ev.kind === "chart.view" && ev.data?.sessionId === store.get().activeSessionId) void reloadChartView(ev.data.sessionId);
|
|
1882
2133
|
if (ev.data?.clientId === focusClientId) continue;
|
|
1883
2134
|
if (ev.kind === "desk.state" && ev.data?.coin && (!ev.data.sessionId || ev.data.sessionId === store.get().activeSessionId) && ev.data.coin !== store.get().activeCoin) {
|
|
1884
2135
|
focusVersion += 1;
|
|
@@ -2212,6 +2463,12 @@ function SearchIcon() {
|
|
|
2212
2463
|
}
|
|
2213
2464
|
function UiIcon({ name, size: size3 = 16, filled = false }) {
|
|
2214
2465
|
const paths2 = {
|
|
2466
|
+
cursor: /* @__PURE__ */ React4.createElement("path", { d: "m5 3 14 10-7 1-3 7z" }),
|
|
2467
|
+
calendar: /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement("rect", { x: "3", y: "5", width: "18", height: "16", rx: "2" }), /* @__PURE__ */ React4.createElement("path", { d: "M7 3v4m10-4v4M3 11h18M7 15h3m4 0h3M7 18h3" })),
|
|
2468
|
+
download: /* @__PURE__ */ React4.createElement("path", { d: "M12 3v12m-5-5 5 5 5-5M4 16v5h16v-5" }),
|
|
2469
|
+
camera: /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement("path", { d: "M3 7h4l2-3h6l2 3h4v14H3z" }), /* @__PURE__ */ React4.createElement("circle", { cx: "12", cy: "13", r: "4" })),
|
|
2470
|
+
help: /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement("circle", { cx: "12", cy: "12", r: "9" }), /* @__PURE__ */ React4.createElement("path", { d: "M9 9a3 3 0 0 1 6 0c0 2-3 2-3 5m0 3h.01" })),
|
|
2471
|
+
live: /* @__PURE__ */ React4.createElement("path", { d: "m4 4 10 8L4 20zm14 0v16" }),
|
|
2215
2472
|
chevronDown: /* @__PURE__ */ React4.createElement("path", { d: "m6 9 6 6 6-6" }),
|
|
2216
2473
|
chevronRight: /* @__PURE__ */ React4.createElement("path", { d: "m9 6 6 6-6 6" }),
|
|
2217
2474
|
bell: /* @__PURE__ */ React4.createElement("path", { d: "M6 16V11a6 6 0 0 1 12 0v5l2 2H4zM10 20a2 2 0 0 0 4 0" }),
|
|
@@ -2342,7 +2599,7 @@ function SessionsColumn({ width, fill = false, onCollapse }) {
|
|
|
2342
2599
|
}
|
|
2343
2600
|
|
|
2344
2601
|
// src/client/components/desk-column.tsx
|
|
2345
|
-
var
|
|
2602
|
+
var React26 = __toESM(require("react"), 1);
|
|
2346
2603
|
|
|
2347
2604
|
// src/client/components/select.tsx
|
|
2348
2605
|
var React6 = __toESM(require("react"), 1);
|
|
@@ -2351,7 +2608,7 @@ function Select({ className, children, ...props }) {
|
|
|
2351
2608
|
}
|
|
2352
2609
|
|
|
2353
2610
|
// src/client/components/candle-chart.tsx
|
|
2354
|
-
var
|
|
2611
|
+
var React10 = __toESM(require("react"), 1);
|
|
2355
2612
|
|
|
2356
2613
|
// node_modules/fancy-canvas/size.mjs
|
|
2357
2614
|
function size(_a) {
|
|
@@ -3459,9 +3716,9 @@ var PriceAxisViewRenderer = class {
|
|
|
3459
3716
|
const text = this._private__data._internal_text;
|
|
3460
3717
|
const actualTextHeight = rendererOptions._internal_fontSize;
|
|
3461
3718
|
const textMidCorrection = textWidthCache._internal_yMidCorrection(ctx, text);
|
|
3462
|
-
const
|
|
3719
|
+
const textWidth2 = Math.ceil(textWidthCache._internal_measureText(ctx, text));
|
|
3463
3720
|
const totalHeight = actualTextHeight + paddingTop + paddingBottom;
|
|
3464
|
-
const totalWidth = rendererOptions._internal_borderSize + paddingInner + paddingOuter +
|
|
3721
|
+
const totalWidth = rendererOptions._internal_borderSize + paddingInner + paddingOuter + textWidth2 + tickSize;
|
|
3465
3722
|
const tickHeightBitmap = Math.max(1, Math.floor(verticalPixelRatio));
|
|
3466
3723
|
let totalHeightBitmap = Math.round(totalHeight * verticalPixelRatio);
|
|
3467
3724
|
if (totalHeightBitmap % 2 !== tickHeightBitmap % 2) {
|
|
@@ -3645,15 +3902,15 @@ var TimeAxisViewRenderer = class {
|
|
|
3645
3902
|
if (this._private__data === null || this._private__data._internal_visible === false || this._private__data._internal_text.length === 0) {
|
|
3646
3903
|
return;
|
|
3647
3904
|
}
|
|
3648
|
-
const
|
|
3905
|
+
const textWidth2 = target.useMediaCoordinateSpace(({ context: ctx }) => {
|
|
3649
3906
|
ctx.font = rendererOptions._internal_font;
|
|
3650
3907
|
return Math.round(rendererOptions._internal_widthCache._internal_measureText(ctx, ensureNotNull(this._private__data)._internal_text, optimizationReplacementRe));
|
|
3651
3908
|
});
|
|
3652
|
-
if (
|
|
3909
|
+
if (textWidth2 <= 0) {
|
|
3653
3910
|
return;
|
|
3654
3911
|
}
|
|
3655
3912
|
const horzMargin = rendererOptions._internal_paddingHorizontal;
|
|
3656
|
-
const labelWidth =
|
|
3913
|
+
const labelWidth = textWidth2 + 2 * horzMargin;
|
|
3657
3914
|
const labelWidthHalf = labelWidth / 2;
|
|
3658
3915
|
const timeScaleWidth = this._private__data._internal_width;
|
|
3659
3916
|
let coordinate = this._private__data._internal_coordinate;
|
|
@@ -6577,7 +6834,7 @@ var Magnet = class {
|
|
|
6577
6834
|
}
|
|
6578
6835
|
const firstPrice = ensure(series._internal_firstValue());
|
|
6579
6836
|
const plotRowKeys = this._private__options.mode === 3 ? magnetOHLCPlotRowKeys : magnetPlotRowKeys;
|
|
6580
|
-
return acc.concat(plotRowKeys.map((
|
|
6837
|
+
return acc.concat(plotRowKeys.map((key3) => ps._internal_priceToCoordinate(bar._internal_value[key3], firstPrice._internal_value)));
|
|
6581
6838
|
}, []);
|
|
6582
6839
|
if (candidates.length === 0) {
|
|
6583
6840
|
return res;
|
|
@@ -14639,9 +14896,9 @@ function checkBarItem(type, barItem) {
|
|
|
14639
14896
|
if (!isFulfilledData(barItem)) {
|
|
14640
14897
|
return;
|
|
14641
14898
|
}
|
|
14642
|
-
["open", "high", "low", "close"].forEach((
|
|
14643
|
-
assert(typeof barItem[
|
|
14644
|
-
assert(isSafeValue(barItem[
|
|
14899
|
+
["open", "high", "low", "close"].forEach((key3) => {
|
|
14900
|
+
assert(typeof barItem[key3] === "number", `${type} series item data value of ${key3} must be a number, got=${typeof barItem[key3]}, value=${barItem[key3]}`);
|
|
14901
|
+
assert(isSafeValue(barItem[key3]), `${type} series item data value of ${key3} must be between ${MIN_SAFE_VALUE.toPrecision(16)} and ${MAX_SAFE_VALUE.toPrecision(16)}, got=${typeof barItem[key3]}, value=${barItem[key3]}`);
|
|
14645
14902
|
});
|
|
14646
14903
|
}
|
|
14647
14904
|
function checkLineItem(type, lineItem) {
|
|
@@ -16786,9 +17043,9 @@ function drawText(ctx, text, x, y, horizontalPixelRatio, verticalPixelRatio) {
|
|
|
16786
17043
|
ctx.fillText(text, x, y);
|
|
16787
17044
|
ctx.restore();
|
|
16788
17045
|
}
|
|
16789
|
-
function hitTestText(textX, textY,
|
|
17046
|
+
function hitTestText(textX, textY, textWidth2, textHeight, x, y) {
|
|
16790
17047
|
const halfHeight = textHeight / 2;
|
|
16791
|
-
return x >= textX && x <= textX +
|
|
17048
|
+
return x >= textX && x <= textX + textWidth2 && y >= textY - halfHeight && y <= textY + halfHeight;
|
|
16792
17049
|
}
|
|
16793
17050
|
var SeriesMarkersRenderer = class {
|
|
16794
17051
|
constructor() {
|
|
@@ -17356,6 +17613,8 @@ var DRAWING_POINTS = {
|
|
|
17356
17613
|
gannfan: 2,
|
|
17357
17614
|
pitchfork: 3,
|
|
17358
17615
|
regression: 2,
|
|
17616
|
+
// Fixed range volume profile: the two points only set the time range.
|
|
17617
|
+
vprofile: 2,
|
|
17359
17618
|
// Annotation and shapes. brush is freehand: 0 = variable (BRUSH_MIN..BRUSH_MAX points).
|
|
17360
17619
|
arrow: 2,
|
|
17361
17620
|
callout: 2,
|
|
@@ -17392,8 +17651,49 @@ function isSingleEmoji(value) {
|
|
|
17392
17651
|
if (typeof value !== "string" || !value || value.length > 32 || !/\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(value)) return false;
|
|
17393
17652
|
return [...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(value)].length === 1;
|
|
17394
17653
|
}
|
|
17654
|
+
function volumeProfile(bars, { rows = 48, valueArea = 0.7 } = {}) {
|
|
17655
|
+
const valid = bars.filter((bar) => [bar.open, bar.high, bar.low, bar.close, bar.volume].every(Number.isFinite) && bar.high >= bar.low && bar.volume >= 0);
|
|
17656
|
+
if (!valid.length) return null;
|
|
17657
|
+
const low = Math.min(...valid.map((bar) => bar.low)), high = Math.max(...valid.map((bar) => bar.high));
|
|
17658
|
+
const count = high > low ? rows : 1;
|
|
17659
|
+
const step = high > low ? (high - low) / count : 0;
|
|
17660
|
+
const out = Array.from({ length: count }, (_, index) => ({ low: low + index * step, high: count === 1 ? high : low + (index + 1) * step, up: 0, down: 0 }));
|
|
17661
|
+
for (const bar of valid) {
|
|
17662
|
+
const side2 = bar.close >= bar.open ? "up" : "down";
|
|
17663
|
+
if (count === 1) {
|
|
17664
|
+
out[0][side2] += bar.volume;
|
|
17665
|
+
continue;
|
|
17666
|
+
}
|
|
17667
|
+
if (bar.high === bar.low) {
|
|
17668
|
+
out[Math.min(count - 1, Math.floor((bar.low - low) / step))][side2] += bar.volume;
|
|
17669
|
+
continue;
|
|
17670
|
+
}
|
|
17671
|
+
const first = Math.max(0, Math.floor((bar.low - low) / step)), last = Math.min(count - 1, Math.floor((bar.high - low) / step));
|
|
17672
|
+
for (let index = first; index <= last; index += 1) {
|
|
17673
|
+
const overlap = Math.min(bar.high, out[index].high) - Math.max(bar.low, out[index].low);
|
|
17674
|
+
if (overlap > 0) out[index][side2] += bar.volume * overlap / (bar.high - bar.low);
|
|
17675
|
+
}
|
|
17676
|
+
}
|
|
17677
|
+
const total = out.reduce((sum, row) => sum + row.up + row.down, 0);
|
|
17678
|
+
if (!(total > 0)) return null;
|
|
17679
|
+
const size3 = (index) => index >= 0 && index < count ? out[index].up + out[index].down : -1;
|
|
17680
|
+
let poc = 0;
|
|
17681
|
+
for (let index = 1; index < count; index += 1) if (size3(index) > size3(poc)) poc = index;
|
|
17682
|
+
let lo = poc, hi = poc, held = size3(poc);
|
|
17683
|
+
while (held < total * valueArea && (lo > 0 || hi < count - 1)) {
|
|
17684
|
+
if (size3(hi + 1) >= size3(lo - 1)) {
|
|
17685
|
+
hi += 1;
|
|
17686
|
+
held += size3(hi);
|
|
17687
|
+
} else {
|
|
17688
|
+
lo -= 1;
|
|
17689
|
+
held += size3(lo);
|
|
17690
|
+
}
|
|
17691
|
+
}
|
|
17692
|
+
return { rows: out, total, poc: (out[poc].low + out[poc].high) / 2, pocIndex: poc, vah: out[hi].high, val: out[lo].low, valueLow: lo, valueHigh: hi };
|
|
17693
|
+
}
|
|
17395
17694
|
var GANN_BOX_LEVELS = [0, 0.25, 0.382, 0.5, 0.618, 0.75, 1];
|
|
17396
17695
|
var ALERT_KINDS = ["level", "hray", "trend", "ray", "extended"];
|
|
17696
|
+
var BRUSH_MIN = 2;
|
|
17397
17697
|
var BRUSH_MAX = 500;
|
|
17398
17698
|
var TEXT_KINDS = ["text", "callout"];
|
|
17399
17699
|
var DRAWING_CLICKS = { ...Object.fromEntries(Object.entries(DRAWING_POINTS)), long: 2, short: 2 };
|
|
@@ -17403,25 +17703,66 @@ var FIB_EXT_LEVELS = [0, 0.382, 0.618, 1, 1.272, 1.618, 2, 2.618];
|
|
|
17403
17703
|
var FIB_TIME_SERIES = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89];
|
|
17404
17704
|
var GANN_RATIOS = [[1, 8], [1, 4], [1, 3], [1, 2], [1, 1], [2, 1], [3, 1], [4, 1], [8, 1]];
|
|
17405
17705
|
var OLDEST = Date.UTC(2015, 0, 1);
|
|
17706
|
+
function drawingGeometry(kind, request, now = Date.now()) {
|
|
17707
|
+
if (!DRAWING_KINDS.includes(kind)) throw new Error(`\u753B\u7EBF\u7C7B\u578B\u5FC5\u987B\u4E3A ${DRAWING_KINDS.join("/")}`);
|
|
17708
|
+
const latest = now + 365 * 864e5;
|
|
17709
|
+
const point = (time, price, label) => {
|
|
17710
|
+
const t2 = Number(time), p = Number(price);
|
|
17711
|
+
if (!Number.isFinite(t2) || t2 < OLDEST || t2 > latest) throw new Error(`${label}\u65F6\u95F4\u5FC5\u987B\u4E3A 2015 \u5E74\u81F3\u4ECA\u540E\u4E00\u5E74\u5185\u7684\u6BEB\u79D2\u65F6\u95F4\u6233`);
|
|
17712
|
+
if (!Number.isFinite(p) || p <= 0) throw new Error(`${label}\u4EF7\u683C\u5FC5\u987B\u4E3A\u6B63\u6570`);
|
|
17713
|
+
return { time: t2, price: p };
|
|
17714
|
+
};
|
|
17715
|
+
if (kind === "level") {
|
|
17716
|
+
const price = Number(request?.price);
|
|
17717
|
+
if (!Number.isFinite(price) || price <= 0) throw new Error("\u6C34\u5E73\u7EBF price \u5FC5\u987B\u4E3A\u6B63\u6570");
|
|
17718
|
+
return { price };
|
|
17719
|
+
}
|
|
17720
|
+
if (kind === "trend") {
|
|
17721
|
+
const from = point(request?.fromTime, request?.fromPrice, "\u8D8B\u52BF\u7EBF\u8D77\u70B9");
|
|
17722
|
+
const to = point(request?.toTime, request?.toPrice, "\u8D8B\u52BF\u7EBF\u7EC8\u70B9");
|
|
17723
|
+
if (to.time <= from.time) throw new Error("\u8D8B\u52BF\u7EBF\u7EC8\u70B9\u65F6\u95F4\u5FC5\u987B\u665A\u4E8E\u8D77\u70B9");
|
|
17724
|
+
return { fromTime: from.time, fromPrice: from.price, toTime: to.time, toPrice: to.price };
|
|
17725
|
+
}
|
|
17726
|
+
const need = DRAWING_POINTS[kind];
|
|
17727
|
+
const raw = request?.points;
|
|
17728
|
+
if (kind === "brush") {
|
|
17729
|
+
if (!Array.isArray(raw) || raw.length < BRUSH_MIN || raw.length > BRUSH_MAX) throw new Error(`brush \u9700\u8981 ${BRUSH_MIN}-${BRUSH_MAX} \u4E2A points`);
|
|
17730
|
+
} else if (!Array.isArray(raw) || raw.length !== need) throw new Error(`${kind} \u9700\u8981 ${need} \u4E2A points\uFF08{ time, price }\uFF09`);
|
|
17731
|
+
const points2 = raw.map((item, index) => point(item?.time, item?.price, `\u7B2C ${index + 1} \u4E2A\u70B9`));
|
|
17732
|
+
if (["trend", "ray", "extended", "channel", "fibchannel", "gannfan", "regression", "fibtime", "infoline", "trendangle"].includes(kind) && points2[0].time === points2[1].time) throw new Error("\u524D\u4E24\u4E2A\u70B9\u7684\u65F6\u95F4\u4E0D\u80FD\u76F8\u540C");
|
|
17733
|
+
if (kind === "vprofile") {
|
|
17734
|
+
if (points2[0].time === points2[1].time) throw new Error("\u6210\u4EA4\u91CF\u5206\u5E03\u9700\u8981\u4E00\u4E2A\u65F6\u95F4\u533A\u95F4");
|
|
17735
|
+
if (points2[1].time < points2[0].time) points2.reverse();
|
|
17736
|
+
}
|
|
17737
|
+
if ((kind === "regression" || kind === "fibtime" || kind === "gannfan") && points2[1].time < points2[0].time) throw new Error("\u7B2C\u4E8C\u4E2A\u70B9\u5FC5\u987B\u665A\u4E8E\u7B2C\u4E00\u4E2A\u70B9");
|
|
17738
|
+
if (kind === "long" || kind === "short") {
|
|
17739
|
+
const [entry, stop, target] = points2.map((item) => item.price);
|
|
17740
|
+
const ok = kind === "long" ? stop < entry && entry < target : target < entry && entry < stop;
|
|
17741
|
+
if (!ok) throw new Error(kind === "long" ? "\u591A\u5934\u5934\u5BF8\u9700\u8981 \u6B62\u635F < \u5165\u573A < \u6B62\u76C8" : "\u7A7A\u5934\u5934\u5BF8\u9700\u8981 \u6B62\u76C8 < \u5165\u573A < \u6B62\u635F");
|
|
17742
|
+
if (points2[1].time <= points2[0].time) throw new Error("\u5934\u5BF8\u53F3\u8FB9\u754C\u65F6\u95F4\u5FC5\u987B\u665A\u4E8E\u5165\u573A\u65F6\u95F4");
|
|
17743
|
+
}
|
|
17744
|
+
return { points: points2 };
|
|
17745
|
+
}
|
|
17406
17746
|
|
|
17407
17747
|
// src/client/drawing-tools.tsx
|
|
17408
17748
|
var isTextKind = (kind) => TEXT_KINDS.includes(kind);
|
|
17409
17749
|
var DRAWING_TOOLS = [
|
|
17410
|
-
{ group: "lines", kind: "trend", zh: "\u8D8B\u52BF\u7EBF", en: "Trend line", hintZh: "\u4F9D\u6B21\u70B9\u51FB\u8D77\u70B9\u548C\u7EC8\u70B9", hintEn: "Click start and end points", icon: "M4 19 20 5" },
|
|
17750
|
+
{ group: "lines", kind: "trend", zh: "\u8D8B\u52BF\u7EBF", en: "Trend line", hintZh: "\u4F9D\u6B21\u70B9\u51FB\u8D77\u70B9\u548C\u7EC8\u70B9\uFF08Alt+T\uFF09", hintEn: "Click start and end points (Alt+T)", icon: "M4 19 20 5" },
|
|
17411
17751
|
{ group: "lines", kind: "ray", zh: "\u5C04\u7EBF", en: "Ray", hintZh: "\u70B9\u51FB\u8D77\u70B9\uFF0C\u518D\u70B9\u51FB\u65B9\u5411\u70B9\uFF0C\u5411\u524D\u65E0\u9650\u5EF6\u4F38", hintEn: "Click origin, then direction; extends forward", icon: "M4 19 20 5M4 19h.01" },
|
|
17412
17752
|
{ group: "lines", kind: "extended", zh: "\u5EF6\u957F\u7EBF", en: "Extended line", hintZh: "\u70B9\u51FB\u4E24\u70B9\uFF0C\u4E24\u7AEF\u65E0\u9650\u5EF6\u4F38", hintEn: "Click two points; extends both ways", icon: "M2 21 22 3" },
|
|
17413
|
-
{ group: "lines", kind: "level", zh: "\u6C34\u5E73\u7EBF", en: "Horizontal line", hintZh: "\u5355\u51FB\u8BBE\u7F6E\u4EF7\u683C", hintEn: "Click to set the price", icon: "M3 12h18" },
|
|
17414
|
-
{ group: "lines", kind: "hray", zh: "\u6C34\u5E73\u5C04\u7EBF", en: "Horizontal ray", hintZh: "\u5355\u51FB\u8D77\u70B9\uFF0C\u5411\u53F3\u5EF6\u4F38", hintEn: "Click origin; extends right", icon: "M6 12h15M6 12h.01" },
|
|
17753
|
+
{ group: "lines", kind: "level", zh: "\u6C34\u5E73\u7EBF", en: "Horizontal line", hintZh: "\u5355\u51FB\u8BBE\u7F6E\u4EF7\u683C\uFF08Alt+H\uFF09", hintEn: "Click to set the price (Alt+H)", icon: "M3 12h18" },
|
|
17754
|
+
{ group: "lines", kind: "hray", zh: "\u6C34\u5E73\u5C04\u7EBF", en: "Horizontal ray", hintZh: "\u5355\u51FB\u8D77\u70B9\uFF0C\u5411\u53F3\u5EF6\u4F38\uFF08Alt+J\uFF09", hintEn: "Click origin; extends right (Alt+J)", icon: "M6 12h15M6 12h.01" },
|
|
17415
17755
|
{ group: "lines", kind: "infoline", zh: "\u4FE1\u606F\u7EBF", en: "Info line", hintZh: "\u4E24\u70B9\u753B\u7EBF\uFF0C\u663E\u793A\u6DA8\u8DCC\u3001\u6839\u6570\u3001\u65F6\u957F\u548C\u89D2\u5EA6", hintEn: "Two points; shows change, bars, time and angle", icon: "M4 18 18 6M14 4h6v5" },
|
|
17416
17756
|
{ group: "lines", kind: "trendangle", zh: "\u8D8B\u52BF\u89D2\u5EA6", en: "Trend angle", hintZh: "\u4E24\u70B9\u753B\u7EBF\uFF0C\u663E\u793A\u4E0E\u6C34\u5E73\u7EBF\u7684\u5939\u89D2", hintEn: "Two points; shows the angle to horizontal", icon: "M4 19 19 6M4 19h15M10 19a6 6 0 0 0-1.6-4" },
|
|
17417
|
-
{ group: "lines", kind: "crossline", zh: "\u5341\u5B57\u7EBF", en: "Cross line", hintZh: "\u5355\u51FB\u653E\u7F6E\u6C34\u5E73\u4E0E\u5782\u76F4\u4EA4\u53C9\u7EBF", hintEn: "Click to place a horizontal and vertical cross", icon: "M12 3v18M3 12h18" },
|
|
17418
|
-
{ group: "lines", kind: "vline", zh: "\u5782\u76F4\u7EBF", en: "Vertical line", hintZh: "\u5355\u51FB\u6807\u8BB0\u65F6\u95F4", hintEn: "Click to mark a time", icon: "M12 3v18" },
|
|
17757
|
+
{ group: "lines", kind: "crossline", zh: "\u5341\u5B57\u7EBF", en: "Cross line", hintZh: "\u5355\u51FB\u653E\u7F6E\u6C34\u5E73\u4E0E\u5782\u76F4\u4EA4\u53C9\u7EBF\uFF08Alt+C\uFF09", hintEn: "Click to place a horizontal and vertical cross (Alt+C)", icon: "M12 3v18M3 12h18" },
|
|
17758
|
+
{ group: "lines", kind: "vline", zh: "\u5782\u76F4\u7EBF", en: "Vertical line", hintZh: "\u5355\u51FB\u6807\u8BB0\u65F6\u95F4\uFF08Alt+V\uFF09", hintEn: "Click to mark a time (Alt+V)", icon: "M12 3v18" },
|
|
17419
17759
|
{ group: "channels", kind: "channel", zh: "\u5E73\u884C\u901A\u9053", en: "Parallel channel", hintZh: "\u70B9\u51FB\u4E24\u70B9\u753B\u57FA\u7EBF\uFF0C\u7B2C\u4E09\u70B9\u8BBE\u7F6E\u901A\u9053\u5BBD\u5EA6", hintEn: "Two points for the base, a third for width", icon: "M3 15 15 5M9 19 21 9" },
|
|
17420
17760
|
{ group: "channels", kind: "pitchfork", zh: "\u5B89\u5FB7\u9C81\u53C9\u5B50\u7EBF", en: "Pitchfork", hintZh: "\u70B9\u51FB\u67A2\u8F74\u70B9\uFF0C\u518D\u70B9\u51FB\u6CE2\u6BB5\u9AD8\u70B9\u4E0E\u4F4E\u70B9", hintEn: "Click the pivot, then the swing high and low", icon: "M4 20 12 4M12 4l8 8M12 4 4 12M8 16l12-4" },
|
|
17421
17761
|
{ group: "channels", kind: "schiff", zh: "Schiff \u53C9\u5B50\u7EBF", en: "Schiff pitchfork", hintZh: "\u540C\u53C9\u5B50\u7EBF\uFF0C\u8D77\u70B9\u79FB\u5230 A\u3001B \u4EF7\u683C\u4E2D\u70B9", hintEn: "Pitchfork with the handle at A\u2013B price midpoint", icon: "M4 14 12 4M12 4l8 8M12 4 4 12M8 16l12-4" },
|
|
17422
17762
|
{ group: "channels", kind: "modschiff", zh: "\u6539\u826F Schiff \u53C9\u5B50\u7EBF", en: "Modified Schiff pitchfork", hintZh: "\u540C\u53C9\u5B50\u7EBF\uFF0C\u8D77\u70B9\u79FB\u5230 A\u3001B \u65F6\u95F4\u4E0E\u4EF7\u683C\u4E2D\u70B9", hintEn: "Pitchfork with the handle at the A\u2013B midpoint", icon: "M6 12 12 4M12 4l8 8M12 4 4 12M8 16l12-4" },
|
|
17423
17763
|
{ group: "channels", kind: "regression", zh: "\u56DE\u5F52\u8D8B\u52BF", en: "Regression trend", hintZh: "\u70B9\u51FB\u533A\u95F4\u8D77\u70B9\u548C\u7EC8\u70B9\uFF0C\u6309\u6536\u76D8\u4EF7\u62DF\u5408 \xB12\u03C3 \u901A\u9053", hintEn: "Click range start and end; fits closes with \xB12\u03C3", icon: "M3 17 21 7M3 13 21 3M3 21 21 11" },
|
|
17424
|
-
{ group: "
|
|
17764
|
+
{ group: "channels", kind: "vprofile", zh: "\u56FA\u5B9A\u8303\u56F4\u6210\u4EA4\u91CF\u5206\u5E03", en: "Fixed range volume profile", hintZh: "\u70B9\u51FB\u533A\u95F4\u8D77\u70B9\u548C\u7EC8\u70B9\uFF0C\u6309\u4EF7\u683C\u7EDF\u8BA1\u6210\u4EA4\u91CF\uFF0C\u6807\u51FA POC \u4E0E 70% \u4EF7\u503C\u533A", hintEn: "Click range start and end; volume by price with POC and 70% value area", icon: "M4 3v18M4 6h7M4 9h12M4 12h16M4 15h9M4 18h5" },
|
|
17765
|
+
{ group: "fib", kind: "fib", zh: "\u6590\u6CE2\u90A3\u5951\u56DE\u64A4", en: "Fib retracement", hintZh: "\u4ECE\u6CE2\u6BB5\u8D77\u70B9\u70B9\u5230\u7EC8\u70B9\uFF08\u7EC8\u70B9\u4E3A 0\uFF09\uFF08Alt+F\uFF09", hintEn: "Click swing start, then end (end = 0) (Alt+F)", icon: "M3 5h18M3 9h18M3 13h18M3 19h18" },
|
|
17425
17766
|
{ group: "fib", kind: "fibext", zh: "\u6590\u6CE2\u90A3\u5951\u6269\u5C55", en: "Trend-based fib extension", hintZh: "\u70B9\u51FB\u6CE2\u6BB5\u8D77\u70B9\u3001\u7EC8\u70B9\uFF0C\u518D\u70B9\u51FB\u56DE\u64A4\u70B9\uFF0C\u81EA\u56DE\u64A4\u70B9\u6295\u5C04", hintEn: "Swing start, swing end, then the retracement point", icon: "M3 20 9 6l6 9M15 4h7M15 8h7M15 12h7" },
|
|
17426
17767
|
{ group: "fib", kind: "fibchannel", zh: "\u6590\u6CE2\u90A3\u5951\u901A\u9053", en: "Fib channel", hintZh: "\u4E24\u70B9\u753B\u57FA\u7EBF\uFF0C\u7B2C\u4E09\u70B9\u8BBE\u7F6E\u901A\u9053\u5BBD\u5EA6", hintEn: "Two points for the base, a third for width", icon: "M3 19 21 9M3 15 21 5M3 11 21 1" },
|
|
17427
17768
|
{ group: "fib", kind: "fibtime", zh: "\u6590\u6CE2\u90A3\u5951\u65F6\u95F4\u5468\u671F", en: "Fib time zone", hintZh: "\u70B9\u51FB\u4E24\u70B9\u786E\u5B9A\u5355\u4F4D\u95F4\u9694", hintEn: "Click two points to set the unit spacing", icon: "M4 3v18M7 3v18M10 3v18M15 3v18M22 3v18" },
|
|
@@ -17433,7 +17774,7 @@ var DRAWING_TOOLS = [
|
|
|
17433
17774
|
{ group: "patterns", kind: "xabcd", zh: "XABCD \u8C10\u6CE2", en: "XABCD pattern", hintZh: "\u4F9D\u6B21\u70B9\u51FB X\u3001A\u3001B\u3001C\u3001D\uFF0C\u663E\u793A\u6590\u6CE2\u90A3\u5951\u6BD4\u503C", hintEn: "Click X, A, B, C, D; shows Fibonacci ratios", icon: "M2 18 7 5l4 9 5-7 6 12M2 18l9-4M11 14l11 5" },
|
|
17434
17775
|
{ group: "patterns", kind: "abcd", zh: "ABCD \u5F62\u6001", en: "ABCD pattern", hintZh: "\u4F9D\u6B21\u70B9\u51FB A\u3001B\u3001C\u3001D", hintEn: "Click A, B, C, D", icon: "M3 19 9 6l5 8 7-10" },
|
|
17435
17776
|
{ group: "patterns", kind: "headshoulders", zh: "\u5934\u80A9\u5F62\u6001", en: "Head and shoulders", hintZh: "\u4F9D\u6B21\u70B9\u51FB\u5DE6\u5E95\u3001\u5DE6\u80A9\u3001\u9888\u7EBF\u70B9\u3001\u5934\u3001\u9888\u7EBF\u70B9\u3001\u53F3\u80A9\u3001\u53F3\u5E95", hintEn: "Click 7 points from left base to right base", icon: "M2 18l3-6 3 3 4-10 4 10 3-3 3 6M5 15h14" },
|
|
17436
|
-
{ group: "shapes", kind: "rect", zh: "\u77E9\u5F62", en: "Rectangle", hintZh: "\u70B9\u51FB\u4E24\u4E2A\u5BF9\u89D2", hintEn: "Click two opposite corners", icon: "M4 6h16v12H4z" },
|
|
17777
|
+
{ group: "shapes", kind: "rect", zh: "\u77E9\u5F62", en: "Rectangle", hintZh: "\u70B9\u51FB\u4E24\u4E2A\u5BF9\u89D2\uFF08Alt+Shift+R\uFF09", hintEn: "Click two opposite corners (Alt+Shift+R)", icon: "M4 6h16v12H4z" },
|
|
17437
17778
|
{ group: "shapes", kind: "circle", zh: "\u5706\u5F62", en: "Circle", hintZh: "\u70B9\u51FB\u5706\u5FC3\uFF0C\u518D\u70B9\u51FB\u8FB9\u7F18", hintEn: "Click the center, then the edge", icon: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z" },
|
|
17438
17779
|
{ group: "shapes", kind: "triangle", zh: "\u4E09\u89D2\u5F62", en: "Triangle", hintZh: "\u4F9D\u6B21\u70B9\u51FB\u4E09\u4E2A\u9876\u70B9", hintEn: "Click three corners", icon: "M12 4 21 20H3z" },
|
|
17439
17780
|
{ group: "shapes", kind: "brush", zh: "\u753B\u7B14", en: "Brush", hintZh: "\u6309\u4F4F\u9F20\u6807\u62D6\u52A8\u81EA\u7531\u7ED8\u5236", hintEn: "Press and drag to draw freehand", icon: "M3 17c3-6 5 2 8-3s4-6 7-4 3 4 3 4" },
|
|
@@ -17546,6 +17887,27 @@ function duration(ms) {
|
|
|
17546
17887
|
if (hours2 < 48) return `${Math.round(hours2 * 10) / 10}h`;
|
|
17547
17888
|
return `${Math.round(hours2 / 24 * 10) / 10}d`;
|
|
17548
17889
|
}
|
|
17890
|
+
var textWidth = (value) => [...value].reduce((sum, ch) => sum + (ch.charCodeAt(0) > 255 ? 12 : 6.6), 0);
|
|
17891
|
+
function wrapText(value, max) {
|
|
17892
|
+
const out = [];
|
|
17893
|
+
for (const paragraph of value.split("\n")) {
|
|
17894
|
+
let line = "";
|
|
17895
|
+
for (const ch of paragraph) {
|
|
17896
|
+
if (line && textWidth(line + ch) > max) {
|
|
17897
|
+
const space = line.lastIndexOf(" ");
|
|
17898
|
+
if (ch !== " " && space > 0 && /^[\x20-\x7e]+$/.test(line.slice(space + 1))) {
|
|
17899
|
+
out.push(line.slice(0, space));
|
|
17900
|
+
line = line.slice(space + 1) + ch;
|
|
17901
|
+
} else {
|
|
17902
|
+
out.push(line);
|
|
17903
|
+
line = ch === " " ? "" : ch;
|
|
17904
|
+
}
|
|
17905
|
+
} else line += ch;
|
|
17906
|
+
}
|
|
17907
|
+
out.push(line);
|
|
17908
|
+
}
|
|
17909
|
+
return out.slice(0, 12);
|
|
17910
|
+
}
|
|
17549
17911
|
function regressionFit(bars) {
|
|
17550
17912
|
const n2 = bars.length;
|
|
17551
17913
|
if (n2 < 2) return null;
|
|
@@ -17568,12 +17930,13 @@ function wave(p, labels, text) {
|
|
|
17568
17930
|
}));
|
|
17569
17931
|
}
|
|
17570
17932
|
var ratio = (a, b) => b === 0 ? "\u2014" : (Math.abs(a) / Math.abs(b)).toFixed(3);
|
|
17571
|
-
function guide(a, b, value,
|
|
17572
|
-
return /* @__PURE__ */ React7.createElement("g", { key:
|
|
17933
|
+
function guide(a, b, value, key3, text) {
|
|
17934
|
+
return /* @__PURE__ */ React7.createElement("g", { key: key3 }, /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: a.x, y1: a.y, x2: b.x, y2: b.y }), text((a.x + b.x) / 2, (a.y + b.y) / 2 - 4, value, "middle", "dhl-drawing-text dhl-ratio-label"));
|
|
17573
17935
|
}
|
|
17574
17936
|
var BADGE_PATHS = { alert: "M6 16V11a6 6 0 0 1 12 0v5l2 2H4zM10 20a2 2 0 0 0 4 0", shared: "M4 9h13l-3-3M20 15H7l3 3" };
|
|
17575
17937
|
var DASH = { solid: void 0, dashed: "7 5", dotted: "1.5 4" };
|
|
17576
|
-
var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, label, text: textValue, className, view, anchors = false, interactive = false, color, lineWidth, lineStyle, id,
|
|
17938
|
+
var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, label, text: textValue, className, view, anchors = false, interactive = false, color, lineWidth, lineStyle, id, alert, shared, rows, valueArea }) {
|
|
17939
|
+
const badges = [...alert ? ["alert"] : [], ...shared ? ["shared"] : []];
|
|
17577
17940
|
const { width: w, height: h } = view;
|
|
17578
17941
|
const px = points2.map((point) => view.project(point.time, point.price));
|
|
17579
17942
|
if (!px.length || px.some((point) => point == null)) return null;
|
|
@@ -17625,8 +17988,8 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
|
|
|
17625
17988
|
if (!p[1]) break;
|
|
17626
17989
|
const [start, end] = points2;
|
|
17627
17990
|
const x0 = Math.min(p[0].x, p[1].x), x1 = Math.max(p[0].x, p[1].x, x0 + 40);
|
|
17628
|
-
const
|
|
17629
|
-
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null,
|
|
17991
|
+
const rows2 = FIB_LEVELS.map((level) => ({ level, price: end.price + (start.price - end.price) * level })).map((row) => ({ ...row, y: view.project(end.time, row.price)?.y })).filter((row) => row.y != null);
|
|
17992
|
+
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, rows2.slice(1).map((row, index) => /* @__PURE__ */ React7.createElement("rect", { key: `b${row.level}`, className: `dhl-fib-band band-${index % 3}`, x: x0, width: x1 - x0, y: Math.min(row.y, rows2[index].y), height: Math.abs(row.y - rows2[index].y) })), rows2.map((row) => /* @__PURE__ */ React7.createElement("g", { key: row.level }, /* @__PURE__ */ React7.createElement("line", { className: row.level === 0.5 || row.level === 0.618 ? "dhl-fib-key" : void 0, x1: x0, x2: x1, y1: row.y, y2: row.y }), text(x0 + 4, row.y - 3, `${row.level} (${fmtPrice(row.price)})`))), /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y }));
|
|
17630
17993
|
break;
|
|
17631
17994
|
}
|
|
17632
17995
|
case "rect": {
|
|
@@ -17707,8 +18070,8 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
|
|
|
17707
18070
|
}
|
|
17708
18071
|
const [a, b, c] = points2;
|
|
17709
18072
|
const x0 = p[2].x, x1 = Math.max(x0 + Math.abs(p[1].x - p[0].x), x0 + 60);
|
|
17710
|
-
const
|
|
17711
|
-
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-drawing-mid", points: `${p[0].x},${p[0].y} ${p[1].x},${p[1].y} ${p[2].x},${p[2].y}` }),
|
|
18073
|
+
const rows2 = FIB_EXT_LEVELS.map((level) => ({ level, price: c.price + (b.price - a.price) * level })).map((row) => ({ ...row, y: view.project(c.time, row.price)?.y })).filter((row) => row.y != null);
|
|
18074
|
+
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-drawing-mid", points: `${p[0].x},${p[0].y} ${p[1].x},${p[1].y} ${p[2].x},${p[2].y}` }), rows2.map((row) => /* @__PURE__ */ React7.createElement("g", { key: row.level }, /* @__PURE__ */ React7.createElement("line", { className: row.level === 1 || row.level === 1.618 ? "dhl-fib-key" : void 0, x1: x0, x2: x1, y1: row.y, y2: row.y }), text(x0 + 4, row.y - 3, `${row.level} (${fmtPrice(row.price)})`))));
|
|
17712
18075
|
break;
|
|
17713
18076
|
}
|
|
17714
18077
|
case "fibchannel": {
|
|
@@ -17823,10 +18186,37 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
|
|
|
17823
18186
|
handles = [c0, c1];
|
|
17824
18187
|
break;
|
|
17825
18188
|
}
|
|
18189
|
+
case "vprofile": {
|
|
18190
|
+
if (!p[1]) break;
|
|
18191
|
+
const [from, to] = points2[0].time <= points2[1].time ? points2 : [points2[1], points2[0]];
|
|
18192
|
+
const x0 = Math.min(p[0].x, p[1].x), x1 = Math.max(p[0].x, p[1].x);
|
|
18193
|
+
const settings = { rows: rows ?? 48, valueArea: valueArea ?? 0.7 };
|
|
18194
|
+
const profile = view.profile ? view.profile(from.time, to.time, settings) : volumeProfile(view.candles?.(from.time, to.time) ?? [], settings);
|
|
18195
|
+
const partial = view.firstTime != null && view.barMs != null && view.firstTime - from.time > view.barMs;
|
|
18196
|
+
if (!profile) {
|
|
18197
|
+
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-drawing-fill", x: x0, y: 0, width: Math.max(1, x1 - x0), height: h }), text(x0 + 4, 16, tr("\u533A\u95F4\u5185\u6682\u65E0\u6210\u4EA4\u91CF\u6570\u636E", "No volume in this range"), "start"));
|
|
18198
|
+
handles = [p[0], p[1]];
|
|
18199
|
+
break;
|
|
18200
|
+
}
|
|
18201
|
+
const y = (price) => view.project(from.time, price)?.y;
|
|
18202
|
+
const maxRow = Math.max(...profile.rows.map((row) => row.up + row.down)) || 1;
|
|
18203
|
+
const span = Math.max(24, (x1 - x0) * 0.3);
|
|
18204
|
+
const yTop = y(profile.rows.at(-1).high), yBottom = y(profile.rows[0].low), yPoc = y(profile.poc), yVah = y(profile.vah), yVal = y(profile.val);
|
|
18205
|
+
body = /* @__PURE__ */ React7.createElement("g", { className: "dhl-vprofile" }, yTop != null && yBottom != null ? /* @__PURE__ */ React7.createElement("rect", { className: "dhl-vprofile-range", x: x0, y: yTop, width: Math.max(1, x1 - x0), height: Math.max(1, yBottom - yTop) }) : null, profile.rows.map((row, index) => {
|
|
18206
|
+
const top = y(row.high), bottom = y(row.low);
|
|
18207
|
+
if (top == null || bottom == null) return null;
|
|
18208
|
+
const height = Math.max(1, bottom - top - 0.5), inValue = index >= profile.valueLow && index <= profile.valueHigh;
|
|
18209
|
+
const upWidth = span * row.up / maxRow, downWidth = span * row.down / maxRow;
|
|
18210
|
+
return /* @__PURE__ */ React7.createElement("g", { key: index, className: inValue ? "dhl-vprofile-va" : "dhl-vprofile-out" }, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-vprofile-up", x: x0, y: top, width: upWidth, height }), /* @__PURE__ */ React7.createElement("rect", { className: "dhl-vprofile-down", x: x0 + upWidth, y: top, width: downWidth, height }));
|
|
18211
|
+
}), yPoc != null ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { className: "dhl-vprofile-poc", x1: x0, y1: yPoc, x2: x1, y2: yPoc }), text(x1 - 4, yPoc - 4, `POC ${fmtPrice(profile.poc)}`, "end")) : null, yVah != null ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: x0, y1: yVah, x2: x1, y2: yVah }), text(x1 - 4, yVah - 4, `VAH ${fmtPrice(profile.vah)}`, "end")) : null, yVal != null ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: x0, y1: yVal, x2: x1, y2: yVal }), text(x1 - 4, yVal + 12, `VAL ${fmtPrice(profile.val)}`, "end")) : null, yBottom != null ? text(x0 + 4, Math.min(h - 4, yBottom + 14), partial ? tr("K \u7EBF\u4F30\u7B97 \xB7 \u533A\u95F4\u524D\u6BB5\u5386\u53F2\u672A\u52A0\u8F7D", "Estimated from bars \xB7 early range not loaded") : tr("K \u7EBF\u4F30\u7B97", "Estimated from bars"), "start", partial ? "dhl-drawing-text dhl-vprofile-warn" : "dhl-drawing-text") : null);
|
|
18212
|
+
handles = [p[0], p[1]];
|
|
18213
|
+
break;
|
|
18214
|
+
}
|
|
17826
18215
|
case "text": {
|
|
17827
18216
|
const value = textValue || label || tr("\u6587\u672C", "Text");
|
|
17828
|
-
const
|
|
17829
|
-
|
|
18217
|
+
const lines = wrapText(value, 300);
|
|
18218
|
+
const width = 14 + Math.max(...lines.map(textWidth));
|
|
18219
|
+
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-text-box", x: p[0].x, y: p[0].y - 20, width, height: 8 + lines.length * 16, rx: "4" }), lines.map((line, index) => /* @__PURE__ */ React7.createElement(React7.Fragment, { key: index }, text(p[0].x + 7, p[0].y - 4 + index * 16, line, "start", "dhl-drawing-text dhl-text-value"))));
|
|
17830
18220
|
break;
|
|
17831
18221
|
}
|
|
17832
18222
|
case "measure": {
|
|
@@ -17850,7 +18240,7 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
|
|
|
17850
18240
|
const reward = target ? Math.abs(target.price - entry.price) : 0;
|
|
17851
18241
|
const pct = (value) => entry.price > 0 ? (value / entry.price * 100).toFixed(2) : "0.00";
|
|
17852
18242
|
const x0 = Math.min(left, right), width = Math.max(2, Math.abs(right - left));
|
|
17853
|
-
body = /* @__PURE__ */ React7.createElement("g", { className: "dhl-position" }, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-position-stop", x: x0, width, y: Math.min(yEntry, yStop), height: Math.abs(yStop - yEntry) }), yTarget != null ? /* @__PURE__ */ React7.createElement("rect", { className: "dhl-position-target", x: x0, width, y: Math.min(yEntry, yTarget), height: Math.abs(yTarget - yEntry) }) : null, /* @__PURE__ */ React7.createElement("line", { className: "dhl-position-entry", x1: x0, x2: x0 + width, y1: yEntry, y2: yEntry }), yTarget != null ? text(x0 + width / 2, yTarget + (kind === "long" ?
|
|
18243
|
+
body = /* @__PURE__ */ React7.createElement("g", { className: "dhl-position" }, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-position-stop", x: x0, width, y: Math.min(yEntry, yStop), height: Math.abs(yStop - yEntry) }), yTarget != null ? /* @__PURE__ */ React7.createElement("rect", { className: "dhl-position-target", x: x0, width, y: Math.min(yEntry, yTarget), height: Math.abs(yTarget - yEntry) }) : null, /* @__PURE__ */ React7.createElement("line", { className: "dhl-position-entry", x1: x0, x2: x0 + width, y1: yEntry, y2: yEntry }), yTarget != null ? text(x0 + width / 2, yTarget + (kind === "long" ? -6 : 14), `${tr("\u6B62\u76C8", "Target")} ${fmtPrice(target.price)} (+${pct(reward)}%)`, "middle", "dhl-drawing-text dhl-position-text") : null, text(x0 + width / 2, yStop + (kind === "long" ? 14 : -6), `${tr("\u6B62\u635F", "Stop")} ${fmtPrice(stop.price)} (-${pct(risk)}%)`, "middle", "dhl-drawing-text dhl-position-text"), text(x0 + width / 2, yEntry + (kind === "long" ? -6 : 14), `${kind === "long" ? tr("\u591A", "Long") : tr("\u7A7A", "Short")} ${fmtPrice(entry.price)} \xB7 R:R ${risk > 0 ? (reward / risk).toFixed(2) : "\u2014"}`, "middle", "dhl-drawing-text dhl-position-text"));
|
|
17854
18244
|
handles = [p[0], p[1], ...p[2] ? [p[2]] : []];
|
|
17855
18245
|
break;
|
|
17856
18246
|
}
|
|
@@ -17887,9 +18277,9 @@ function saveStyleTemplate(kind, style) {
|
|
|
17887
18277
|
}
|
|
17888
18278
|
|
|
17889
18279
|
// src/client/components/draw-toolbar.tsx
|
|
17890
|
-
var
|
|
18280
|
+
var DRAW_COLORS = ["#4c82d8", "#2fd3a0", "#ef5f6c", "#e2a943", "#9b86db", "#26b5c8", "#8a8f98"];
|
|
17891
18281
|
var STYLES = [["solid", "\u5B9E\u7EBF", "Solid"], ["dashed", "\u865A\u7EBF", "Dashed"], ["dotted", "\u70B9\u7EBF", "Dotted"]];
|
|
17892
|
-
function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone }) {
|
|
18282
|
+
function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone, onCoordinates }) {
|
|
17893
18283
|
const [savedDefault, setSavedDefault] = React8.useState(false);
|
|
17894
18284
|
React8.useEffect(() => {
|
|
17895
18285
|
setSavedDefault(false);
|
|
@@ -17920,7 +18310,7 @@ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone
|
|
|
17920
18310
|
if (text.trim() && text !== mark.label) onEdit({ label: text.trim() });
|
|
17921
18311
|
}
|
|
17922
18312
|
}
|
|
17923
|
-
) : null, /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group", role: "group", "aria-label": tr("\u989C\u8272", "Color") },
|
|
18313
|
+
) : null, /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group", role: "group", "aria-label": tr("\u989C\u8272", "Color") }, DRAW_COLORS.map((color) => /* @__PURE__ */ React8.createElement(
|
|
17924
18314
|
"button",
|
|
17925
18315
|
{
|
|
17926
18316
|
type: "button",
|
|
@@ -17958,7 +18348,7 @@ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone
|
|
|
17958
18348
|
onClick: () => onEdit({ lineStyle: value })
|
|
17959
18349
|
},
|
|
17960
18350
|
/* @__PURE__ */ React8.createElement("svg", { viewBox: "0 0 18 18", width: "16", height: "16", "aria-hidden": "true" }, /* @__PURE__ */ React8.createElement("line", { x1: "2", x2: "16", y1: "9", y2: "9", stroke: "currentColor", strokeWidth: "2", strokeDasharray: value === "dashed" ? "4 3" : value === "dotted" ? "1 3" : void 0, strokeLinecap: "round" }))
|
|
17961
|
-
))) : null, /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group" }, /* @__PURE__ */ React8.createElement(
|
|
18351
|
+
))) : null, mark.kind === "vprofile" ? /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group", role: "group", "aria-label": tr("\u6210\u4EA4\u91CF\u5206\u5E03\u8BBE\u7F6E", "Volume profile settings") }, /* @__PURE__ */ React8.createElement(Select, { "aria-label": tr("\u4EF7\u683C\u884C\u6570", "Rows"), title: tr("\u4EF7\u683C\u884C\u6570", "Rows"), value: String(mark.rows ?? 48), disabled: locked, onChange: (event) => onEdit({ rows: Number(event.target.value) }) }, [24, 48, 96, 150].map((value) => /* @__PURE__ */ React8.createElement("option", { key: value, value }, tr(`${value} \u884C`, `${value} rows`)))), /* @__PURE__ */ React8.createElement(Select, { "aria-label": tr("\u4EF7\u503C\u533A\u6BD4\u4F8B", "Value area"), title: tr("\u4EF7\u503C\u533A\u6210\u4EA4\u91CF\u5360\u6BD4", "Value area volume"), value: String(mark.valueArea ?? 0.7), disabled: locked, onChange: (event) => onEdit({ valueArea: Number(event.target.value) }) }, [0.68, 0.7, 0.8].map((value) => /* @__PURE__ */ React8.createElement("option", { key: value, value }, `VA ${Math.round(value * 100)}%`)))) : null, /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group" }, /* @__PURE__ */ React8.createElement("button", { type: "button", disabled: locked, title: tr("\u7CBE\u786E\u7F16\u8F91\u65F6\u95F4\u3001\u4EF7\u683C\u4E0E\u540D\u79F0", "Edit exact time, price and name"), "aria-label": tr("\u7F16\u8F91\u753B\u7EBF\u5750\u6807", "Edit drawing coordinates"), onClick: onCoordinates }, /* @__PURE__ */ React8.createElement(UiIcon, { name: "settings", size: 15 })), /* @__PURE__ */ React8.createElement(
|
|
17962
18352
|
"button",
|
|
17963
18353
|
{
|
|
17964
18354
|
type: "button",
|
|
@@ -18010,6 +18400,46 @@ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone
|
|
|
18010
18400
|
), /* @__PURE__ */ React8.createElement("button", { type: "button", disabled: locked, title: tr("\u5220\u9664\uFF08Delete\uFF09", "Delete (Del)"), "aria-label": tr("\u5220\u9664\u753B\u7EBF", "Delete drawing"), onClick: onRemove }, /* @__PURE__ */ React8.createElement("svg", { viewBox: "0 0 24 24", width: "15", height: "15", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React8.createElement("path", { d: "M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" }))), /* @__PURE__ */ React8.createElement("button", { type: "button", title: tr("\u53D6\u6D88\u9009\u62E9\uFF08Esc\uFF09", "Deselect (Esc)"), "aria-label": tr("\u53D6\u6D88\u9009\u62E9", "Deselect"), onClick: onClose }, /* @__PURE__ */ React8.createElement(UiIcon, { name: "close", size: 14 }))));
|
|
18011
18401
|
}
|
|
18012
18402
|
|
|
18403
|
+
// src/client/components/drawing-editor.tsx
|
|
18404
|
+
var React9 = __toESM(require("react"), 1);
|
|
18405
|
+
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
18406
|
+
function localDate(time) {
|
|
18407
|
+
const date = new Date(time);
|
|
18408
|
+
return new Date(time - date.getTimezoneOffset() * 6e4).toISOString().slice(0, 23);
|
|
18409
|
+
}
|
|
18410
|
+
function DrawingEditor({ mark, onApply, onClose }) {
|
|
18411
|
+
const [label, setLabel] = React9.useState(mark.label);
|
|
18412
|
+
const [anchors, setAnchors] = React9.useState(() => markPoints(mark).map((point) => ({ time: localDate(point.time), price: String(point.price) })));
|
|
18413
|
+
const [error, setError] = React9.useState("");
|
|
18414
|
+
const update = (index, key3, value) => setAnchors((rows) => rows.map((row, at) => at === index ? { ...row, [key3]: value } : row));
|
|
18415
|
+
const apply2 = () => {
|
|
18416
|
+
try {
|
|
18417
|
+
if (!label.trim()) throw new Error(tr("\u8BF7\u8F93\u5165\u540D\u79F0\u6216\u6587\u672C", "Enter a name or text"));
|
|
18418
|
+
const points2 = anchors.map((row) => ({ time: new Date(row.time).getTime(), price: Number(row.price) }));
|
|
18419
|
+
const geometry = drawingGeometry(mark.kind, geometryPatch(mark.kind, points2));
|
|
18420
|
+
onApply({ ...geometry, label: label.trim() });
|
|
18421
|
+
onClose();
|
|
18422
|
+
} catch (problem) {
|
|
18423
|
+
setError(String(problem.message ?? problem));
|
|
18424
|
+
}
|
|
18425
|
+
};
|
|
18426
|
+
return /* @__PURE__ */ React9.createElement(
|
|
18427
|
+
import_dsh_client_ui_primitives2.Modal,
|
|
18428
|
+
{
|
|
18429
|
+
open: true,
|
|
18430
|
+
onClose,
|
|
18431
|
+
title: `${tr(toolOf(mark.kind).zh, toolOf(mark.kind).en)} \xB7 ${tr("\u5750\u6807\u4E0E\u540D\u79F0", "Coordinates & name")}`,
|
|
18432
|
+
closeLabel: tr("\u5173\u95ED", "Close"),
|
|
18433
|
+
className: "dhl-coordinate-modal",
|
|
18434
|
+
footer: /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(import_dsh_client_ui_primitives2.Button, { variant: "outline", onClick: onClose }, tr("\u53D6\u6D88", "Cancel")), /* @__PURE__ */ React9.createElement(import_dsh_client_ui_primitives2.Button, { variant: "primary", onClick: apply2 }, tr("\u5E94\u7528\u5750\u6807", "Apply coordinates")))
|
|
18435
|
+
},
|
|
18436
|
+
/* @__PURE__ */ React9.createElement("form", { className: "dhl-coordinate-editor", onSubmit: (event) => {
|
|
18437
|
+
event.preventDefault();
|
|
18438
|
+
apply2();
|
|
18439
|
+
} }, /* @__PURE__ */ React9.createElement("label", null, tr("\u540D\u79F0 / \u6587\u672C", "Name / text"), /* @__PURE__ */ React9.createElement(import_dsh_client_ui_primitives2.Input, { "aria-label": tr("\u753B\u7EBF\u540D\u79F0", "Drawing name"), value: label, readOnly: mark.kind === "icon", maxLength: isTextKind(mark.kind) ? 200 : 60, onChange: (event) => setLabel(event.target.value) })), /* @__PURE__ */ React9.createElement("small", null, tr("\u65F6\u95F4\u91C7\u7528\u672C\u5730\u65F6\u533A", "Times use your local timezone"), " \xB7 ", Intl.DateTimeFormat().resolvedOptions().timeZone), /* @__PURE__ */ React9.createElement("div", { className: "dhl-coordinate-anchors" }, anchors.map((row, index) => /* @__PURE__ */ React9.createElement("fieldset", { key: index }, /* @__PURE__ */ React9.createElement("legend", null, tr(`\u951A\u70B9 ${index + 1}`, `Anchor ${index + 1}`)), mark.kind !== "level" ? /* @__PURE__ */ React9.createElement("label", null, tr("\u65F6\u95F4", "Time"), /* @__PURE__ */ React9.createElement(import_dsh_client_ui_primitives2.Input, { type: "datetime-local", step: "0.001", value: row.time, "aria-label": tr(`\u951A\u70B9 ${index + 1} \u65F6\u95F4`, `Anchor ${index + 1} time`), onChange: (event) => update(index, "time", event.target.value), required: true })) : null, /* @__PURE__ */ React9.createElement("label", null, tr("\u4EF7\u683C", "Price"), /* @__PURE__ */ React9.createElement(import_dsh_client_ui_primitives2.Input, { type: "number", step: "any", min: "0", value: row.price, "aria-label": tr(`\u951A\u70B9 ${index + 1} \u4EF7\u683C`, `Anchor ${index + 1} price`), onChange: (event) => update(index, "price", event.target.value), required: true }))))), error ? /* @__PURE__ */ React9.createElement("p", { role: "alert", className: "dhl-inline-error" }, error) : null)
|
|
18440
|
+
);
|
|
18441
|
+
}
|
|
18442
|
+
|
|
18013
18443
|
// src/client/click-guard.ts
|
|
18014
18444
|
var LIVE_SHIFT_BARS = 2;
|
|
18015
18445
|
function legacyPressStillAClick(atPress, atRelease) {
|
|
@@ -18024,6 +18454,100 @@ function pressStillAClick(atPress, atRelease) {
|
|
|
18024
18454
|
return !zoomed && !panned;
|
|
18025
18455
|
}
|
|
18026
18456
|
|
|
18457
|
+
// src/client/chart-basics.ts
|
|
18458
|
+
function chartBars(candles, type) {
|
|
18459
|
+
if (type !== "heikinAshi") return [...candles];
|
|
18460
|
+
const result = [];
|
|
18461
|
+
for (const candle of candles) {
|
|
18462
|
+
const prior = result.at(-1);
|
|
18463
|
+
const o = prior ? (prior.o + prior.c) / 2 : (candle.o + candle.c) / 2;
|
|
18464
|
+
const c = (candle.o + candle.h + candle.l + candle.c) / 4;
|
|
18465
|
+
result.push({ ...candle, o, c, h: Math.max(candle.h, o, c), l: Math.min(candle.l, o, c) });
|
|
18466
|
+
}
|
|
18467
|
+
return result;
|
|
18468
|
+
}
|
|
18469
|
+
function candleIndex(candles, time) {
|
|
18470
|
+
let low = 0, high = candles.length;
|
|
18471
|
+
while (low < high) {
|
|
18472
|
+
const mid = low + high >>> 1;
|
|
18473
|
+
if (candles[mid].t < time) low = mid + 1;
|
|
18474
|
+
else high = mid;
|
|
18475
|
+
}
|
|
18476
|
+
return Math.min(low, candles.length - 1);
|
|
18477
|
+
}
|
|
18478
|
+
function candlesCsv(candles, coin, interval) {
|
|
18479
|
+
const cell = (value) => '"' + value.replaceAll('"', '""') + '"';
|
|
18480
|
+
return "\uFEFF" + ["symbol,interval,time_utc,open,high,low,close,volume", ...candles.map((bar) => [cell(coin), cell(interval), new Date(bar.t).toISOString(), bar.o, bar.h, bar.l, bar.c, bar.v].join(","))].join("\r\n") + "\r\n";
|
|
18481
|
+
}
|
|
18482
|
+
function downloadBlob(blob, filename) {
|
|
18483
|
+
const url = URL.createObjectURL(blob);
|
|
18484
|
+
const link = document.createElement("a");
|
|
18485
|
+
link.href = url;
|
|
18486
|
+
link.download = filename;
|
|
18487
|
+
document.body.append(link);
|
|
18488
|
+
link.click();
|
|
18489
|
+
link.remove();
|
|
18490
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e4);
|
|
18491
|
+
}
|
|
18492
|
+
|
|
18493
|
+
// src/client/chart-export.ts
|
|
18494
|
+
async function chartImage(chart, box, title) {
|
|
18495
|
+
const source = chart.takeScreenshot();
|
|
18496
|
+
const scale = source.width / box.clientWidth;
|
|
18497
|
+
const canvas = document.createElement("canvas");
|
|
18498
|
+
canvas.width = source.width;
|
|
18499
|
+
canvas.height = source.height + Math.round(30 * scale);
|
|
18500
|
+
const ctx = canvas.getContext("2d");
|
|
18501
|
+
const theme = getComputedStyle(box);
|
|
18502
|
+
let background = "#151517";
|
|
18503
|
+
for (let node = box; node; node = node.parentElement) {
|
|
18504
|
+
const color = getComputedStyle(node).backgroundColor;
|
|
18505
|
+
if (color !== "transparent" && color !== "rgba(0, 0, 0, 0)") {
|
|
18506
|
+
background = color;
|
|
18507
|
+
break;
|
|
18508
|
+
}
|
|
18509
|
+
}
|
|
18510
|
+
ctx.fillStyle = background;
|
|
18511
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
18512
|
+
ctx.drawImage(source, 0, 30 * scale);
|
|
18513
|
+
const overlay = box.querySelector(".dhl-chart-drawing-overlay");
|
|
18514
|
+
if (overlay) {
|
|
18515
|
+
const clone2 = overlay.cloneNode(true);
|
|
18516
|
+
const originals = [overlay, ...overlay.querySelectorAll("*")];
|
|
18517
|
+
const copies = [clone2, ...clone2.querySelectorAll("*")];
|
|
18518
|
+
originals.forEach((original, index) => {
|
|
18519
|
+
const css2 = getComputedStyle(original);
|
|
18520
|
+
for (const key3 of ["color", "fill", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "opacity", "font-family", "font-size", "font-weight", "text-anchor", "dominant-baseline", "paint-order"]) {
|
|
18521
|
+
copies[index].style.setProperty(key3, css2.getPropertyValue(key3));
|
|
18522
|
+
}
|
|
18523
|
+
});
|
|
18524
|
+
clone2.querySelectorAll(".dhl-drawing-hit, .dhl-chart-anchor, .dhl-drawing-preview").forEach((node) => node.remove());
|
|
18525
|
+
clone2.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
18526
|
+
clone2.setAttribute("width", String(box.clientWidth));
|
|
18527
|
+
clone2.setAttribute("height", String(box.clientHeight));
|
|
18528
|
+
const url = URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(clone2)], { type: "image/svg+xml" }));
|
|
18529
|
+
try {
|
|
18530
|
+
const image = new Image();
|
|
18531
|
+
image.src = url;
|
|
18532
|
+
await image.decode();
|
|
18533
|
+
ctx.drawImage(image, 0, 30 * scale, source.width, source.height);
|
|
18534
|
+
} finally {
|
|
18535
|
+
URL.revokeObjectURL(url);
|
|
18536
|
+
}
|
|
18537
|
+
}
|
|
18538
|
+
ctx.fillStyle = theme.color;
|
|
18539
|
+
ctx.font = `${11 * scale}px sans-serif`;
|
|
18540
|
+
ctx.fillText(title, 10 * scale, 19 * scale);
|
|
18541
|
+
for (const selector of [".dhl-chart-ohlc", ".dhl-chart-ma-legend", ".dhl-chart-study-legend"]) {
|
|
18542
|
+
const element = box.querySelector(selector);
|
|
18543
|
+
if (!element) continue;
|
|
18544
|
+
const rect = element.getBoundingClientRect(), base = box.getBoundingClientRect();
|
|
18545
|
+
ctx.font = `${10 * scale}px sans-serif`;
|
|
18546
|
+
ctx.fillText(element.innerText.replaceAll("\n", " "), (rect.left - base.left) * scale, (rect.top - base.top + 11 + 30) * scale, source.width - 24 * scale);
|
|
18547
|
+
}
|
|
18548
|
+
return new Promise((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("PNG export failed")), "image/png"));
|
|
18549
|
+
}
|
|
18550
|
+
|
|
18027
18551
|
// src/client/components/candle-chart.tsx
|
|
18028
18552
|
var MAGNET_PX = 14;
|
|
18029
18553
|
var MA_PALETTE = ["#29b6f6", "#ff7eb6", "#c6e377", "#ffb74d", "#b39ddb", "#4dd0e1", "#f48fb1", "#aed581"];
|
|
@@ -18058,53 +18582,81 @@ function cssVar(el, name, fallback) {
|
|
|
18058
18582
|
function chartData(points2) {
|
|
18059
18583
|
return points2.map((point) => ({ time: point.time, value: point.value }));
|
|
18060
18584
|
}
|
|
18061
|
-
|
|
18062
|
-
|
|
18063
|
-
|
|
18064
|
-
|
|
18065
|
-
|
|
18066
|
-
|
|
18067
|
-
|
|
18068
|
-
const
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
|
|
18072
|
-
|
|
18073
|
-
const
|
|
18074
|
-
const
|
|
18075
|
-
const
|
|
18076
|
-
|
|
18077
|
-
const
|
|
18585
|
+
var profileCache = /* @__PURE__ */ new WeakMap();
|
|
18586
|
+
function cachedProfile(candles, from, to, options = {}) {
|
|
18587
|
+
let byRange = profileCache.get(candles);
|
|
18588
|
+
if (!byRange) {
|
|
18589
|
+
byRange = /* @__PURE__ */ new Map();
|
|
18590
|
+
profileCache.set(candles, byRange);
|
|
18591
|
+
}
|
|
18592
|
+
const key3 = `${from}:${to}:${options.rows ?? ""}:${options.valueArea ?? ""}`;
|
|
18593
|
+
if (!byRange.has(key3)) byRange.set(key3, volumeProfile(candles.filter((c) => c.t >= from && c.t <= to).map((c) => ({ open: c.o, high: c.h, low: c.l, close: c.c, volume: c.v })), options));
|
|
18594
|
+
return byRange.get(key3);
|
|
18595
|
+
}
|
|
18596
|
+
function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMode, onDraw, onSelect, priceScale, visibleRange, onVisibleRange, tradeEvents, onNeedOlder, olderState, focus, maLines, magnet, drawingsLocked, onEditMark, onRemoveMark, onRemoveMarks, onEditMarks, onEditEach, showReasons, onAnalyzeFill, onCloneMark, selectRequest, chartType = "candles", command = null, caption = "", indicatorSettings, showDataWindow = false, link, compact = false, readOnly = false }) {
|
|
18597
|
+
const boxRef = React10.useRef(null);
|
|
18598
|
+
const clipId = React10.useRef("dhl-plot-" + Math.random().toString(36).slice(2));
|
|
18599
|
+
const linkRef = React10.useRef(link);
|
|
18600
|
+
linkRef.current = link;
|
|
18601
|
+
const syncingCursor = React10.useRef(false);
|
|
18602
|
+
const externalRangeUntil = React10.useRef(0);
|
|
18603
|
+
const settings = React10.useMemo(() => normalizeIndicatorSettings(indicatorSettings), [indicatorSettings]);
|
|
18604
|
+
const refs = React10.useRef(null);
|
|
18605
|
+
const priceLines = React10.useRef(/* @__PURE__ */ new Map());
|
|
18606
|
+
const tradeMarkers = React10.useRef(null);
|
|
18607
|
+
const fitted = React10.useRef(false);
|
|
18608
|
+
const firstBarTime = React10.useRef(null);
|
|
18609
|
+
const handledFocus = React10.useRef(0);
|
|
18610
|
+
const handledCommand = React10.useRef(0);
|
|
18611
|
+
const maRefs = React10.useRef(/* @__PURE__ */ new Map());
|
|
18612
|
+
const restoring = React10.useRef(false);
|
|
18613
|
+
const initialRange = React10.useRef(visibleRange);
|
|
18614
|
+
const drawModeRef = React10.useRef(drawMode);
|
|
18615
|
+
const onDrawRef = React10.useRef(onDraw);
|
|
18616
|
+
const onSelectRef = React10.useRef(onSelect);
|
|
18617
|
+
const selectionData = React10.useRef({ candles, indicators, maLines, settings });
|
|
18618
|
+
const onRangeRef = React10.useRef(onVisibleRange);
|
|
18619
|
+
const onNeedOlderRef = React10.useRef(onNeedOlder);
|
|
18078
18620
|
onNeedOlderRef.current = onNeedOlder;
|
|
18079
|
-
const pending =
|
|
18080
|
-
const magnetRef =
|
|
18621
|
+
const pending = React10.useRef([]);
|
|
18622
|
+
const magnetRef = React10.useRef(magnet);
|
|
18081
18623
|
magnetRef.current = magnet;
|
|
18082
|
-
const candlesRef =
|
|
18624
|
+
const candlesRef = React10.useRef(candles);
|
|
18083
18625
|
candlesRef.current = candles;
|
|
18084
|
-
const [selectedId, setSelectedId] =
|
|
18085
|
-
const
|
|
18626
|
+
const [selectedId, setSelectedId] = React10.useState(null);
|
|
18627
|
+
const [editingId, setEditingId] = React10.useState(null);
|
|
18628
|
+
const selectedRef = React10.useRef(null);
|
|
18086
18629
|
selectedRef.current = selectedId;
|
|
18087
|
-
const [
|
|
18088
|
-
const
|
|
18089
|
-
|
|
18630
|
+
const [group, setGroup] = React10.useState([]);
|
|
18631
|
+
const groupRef = React10.useRef([]);
|
|
18632
|
+
groupRef.current = group;
|
|
18633
|
+
const [hoverSnapped, setHoverSnapped] = React10.useState(false);
|
|
18634
|
+
const lastSnapped = React10.useRef(false);
|
|
18635
|
+
const [draft, setDraft] = React10.useState(null);
|
|
18636
|
+
const [groupDraft, setGroupDraft] = React10.useState(null);
|
|
18637
|
+
const [selectedFill, setSelectedFill] = React10.useState(null);
|
|
18638
|
+
const selectedFillRef = React10.useRef(null);
|
|
18090
18639
|
selectedFillRef.current = selectedFill;
|
|
18091
|
-
const snapRef =
|
|
18092
|
-
const hoveredHit =
|
|
18640
|
+
const snapRef = React10.useRef(null);
|
|
18641
|
+
const hoveredHit = React10.useRef(null);
|
|
18093
18642
|
drawModeRef.current = drawMode;
|
|
18094
18643
|
onDrawRef.current = onDraw;
|
|
18095
18644
|
onSelectRef.current = onSelect;
|
|
18096
|
-
selectionData.current = { candles, indicators, maLines };
|
|
18097
|
-
onRangeRef.current =
|
|
18098
|
-
|
|
18099
|
-
|
|
18100
|
-
|
|
18101
|
-
const [
|
|
18102
|
-
const [
|
|
18103
|
-
const [
|
|
18645
|
+
selectionData.current = { candles, indicators, maLines, settings };
|
|
18646
|
+
onRangeRef.current = (range) => {
|
|
18647
|
+
onVisibleRange(range);
|
|
18648
|
+
if (performance.now() > externalRangeUntil.current) linkRef.current?.onRange(range);
|
|
18649
|
+
};
|
|
18650
|
+
const [crosshair, setCrosshair] = React10.useState(null);
|
|
18651
|
+
const [selectedTime, setSelectedTime] = React10.useState(null);
|
|
18652
|
+
const [themeVersion, setThemeVersion] = React10.useState(0);
|
|
18653
|
+
const [viewportVersion, setViewportVersion] = React10.useState(0);
|
|
18654
|
+
const [pendingPoints, setPendingPoints] = React10.useState([]);
|
|
18655
|
+
const [drawingHover, setDrawingHover] = React10.useState(null);
|
|
18104
18656
|
const showVolume = indicators.has("volume");
|
|
18105
|
-
const studies =
|
|
18106
|
-
const maComputed =
|
|
18107
|
-
const timeline =
|
|
18657
|
+
const studies = React10.useMemo(() => chartIndicators(candles, settings), [candles, settings]);
|
|
18658
|
+
const maComputed = React10.useMemo(() => maLines.map((line, index) => ({ line, color: maColor(line, index), ...maSeries(candles, line) })), [candles, maLines]);
|
|
18659
|
+
const timeline = React10.useMemo(() => {
|
|
18108
18660
|
const times = [...new Set(candles.map((candle) => Math.floor(candle.t / 1e3)))].sort((a, b) => a - b);
|
|
18109
18661
|
const step = times.length > 1 ? times[times.length - 1] - times[times.length - 2] : 60;
|
|
18110
18662
|
const toLogical = (ms) => {
|
|
@@ -18129,9 +18681,9 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18129
18681
|
};
|
|
18130
18682
|
return { toLogical, toTime };
|
|
18131
18683
|
}, [candles]);
|
|
18132
|
-
const timelineRef =
|
|
18684
|
+
const timelineRef = React10.useRef(timeline);
|
|
18133
18685
|
timelineRef.current = timeline;
|
|
18134
|
-
|
|
18686
|
+
React10.useEffect(() => {
|
|
18135
18687
|
const box = boxRef.current;
|
|
18136
18688
|
if (!box) return void 0;
|
|
18137
18689
|
const readTheme = () => ({
|
|
@@ -18150,7 +18702,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18150
18702
|
rightPriceScale: { borderColor: theme.border },
|
|
18151
18703
|
timeScale: { borderColor: theme.border, timeVisible: true, secondsVisible: false }
|
|
18152
18704
|
});
|
|
18153
|
-
const candleSeries = chart.addSeries(candlestickSeries, {
|
|
18705
|
+
const candleSeries = chartType === "line" ? chart.addSeries(lineSeries, { color: "#4c82d8", lineWidth: 2 }) : chartType === "area" ? chart.addSeries(areaSeries, { lineColor: "#4c82d8", topColor: "rgba(76,130,216,.28)", bottomColor: "rgba(76,130,216,0)" }) : chartType === "bars" ? chart.addSeries(barSeries, { upColor: theme.up, downColor: theme.down }) : chart.addSeries(candlestickSeries, {
|
|
18154
18706
|
upColor: theme.up,
|
|
18155
18707
|
downColor: theme.down,
|
|
18156
18708
|
borderUpColor: theme.up,
|
|
@@ -18170,13 +18722,14 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18170
18722
|
lastValueVisible: false,
|
|
18171
18723
|
priceLineVisible: false
|
|
18172
18724
|
});
|
|
18725
|
+
const basis = chart.addSeries(lineSeries, { color: "#7aa4d4", lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
|
18173
18726
|
const sma60 = chart.addSeries(lineSeries, { color: "#d78c78", lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
|
|
18174
18727
|
const ema20 = chart.addSeries(lineSeries, { color: "#e2a943", lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
|
|
18175
18728
|
const ema50 = chart.addSeries(lineSeries, { color: "#9b86db", lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
|
|
18176
18729
|
const upper = chart.addSeries(lineSeries, { color: "#7aa4d4", lineWidth: 1, lineStyle: LineStyle.Dashed, lastValueVisible: false, priceLineVisible: false });
|
|
18177
18730
|
const lower = chart.addSeries(lineSeries, { color: "#7aa4d4", lineWidth: 1, lineStyle: LineStyle.Dashed, lastValueVisible: false, priceLineVisible: false });
|
|
18178
18731
|
chart.priceScale("dhl-vol").applyOptions({ scaleMargins: { top: 0.82, bottom: 0 } });
|
|
18179
|
-
refs.current = { chart, candles: candleSeries, volume: volumeSeries, sma: smaSeries, sma60, ema20, ema50, upper, lower };
|
|
18732
|
+
refs.current = { chart, candles: candleSeries, volume: volumeSeries, sma: smaSeries, basis, extra: {}, sma60, ema20, ema50, upper, lower };
|
|
18180
18733
|
tradeMarkers.current = createSeriesMarkers(candleSeries, []);
|
|
18181
18734
|
fitted.current = false;
|
|
18182
18735
|
const identifyHit = (hovered) => {
|
|
@@ -18189,7 +18742,13 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18189
18742
|
if (hovered === r.ema50) return "ema50";
|
|
18190
18743
|
if (hovered === r.upper) return "bollingerUpper";
|
|
18191
18744
|
if (hovered === r.lower) return "bollingerLower";
|
|
18745
|
+
if (hovered === r.basis) return "bollinger20";
|
|
18746
|
+
if (hovered === r.extra.stochK || hovered === r.extra.stochD) return "stochastic";
|
|
18747
|
+
if (hovered === r.extra.adx || hovered === r.extra.plusDI || hovered === r.extra.minusDI) return "adx";
|
|
18748
|
+
if (hovered === r.extra.obv) return "obv";
|
|
18192
18749
|
if (hovered === r.rsi) return "rsi14";
|
|
18750
|
+
if (hovered === r.atr) return "atr14";
|
|
18751
|
+
if (hovered === r.vwap) return "vwap";
|
|
18193
18752
|
if (hovered === r.macd) return "macd";
|
|
18194
18753
|
if (hovered === r.signal) return "macdSignal";
|
|
18195
18754
|
if (hovered === r.histogram) return "macdHistogram";
|
|
@@ -18197,10 +18756,9 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18197
18756
|
};
|
|
18198
18757
|
chart.subscribeCrosshairMove((param) => {
|
|
18199
18758
|
hoveredHit.current = identifyHit(param.hoveredSeries);
|
|
18200
|
-
const
|
|
18201
|
-
|
|
18202
|
-
|
|
18203
|
-
} else setCrosshair(null);
|
|
18759
|
+
const time = typeof param.time === "number" ? param.time * 1e3 : null;
|
|
18760
|
+
setCrosshair(time != null ? barAt(candlesRef.current, time) : null);
|
|
18761
|
+
if (!syncingCursor.current) linkRef.current?.onCursor(time);
|
|
18204
18762
|
});
|
|
18205
18763
|
let press = null;
|
|
18206
18764
|
const down = (event) => {
|
|
@@ -18217,7 +18775,10 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18217
18775
|
if (press?.id === event.pointerId && Math.hypot(event.clientX - press.x, event.clientY - press.y) > 5) press.dragged = true;
|
|
18218
18776
|
if (drawModeRef.current && event.target instanceof HTMLCanvasElement) {
|
|
18219
18777
|
const point = pointAt2(event, pending.current.at(-1));
|
|
18220
|
-
if (point)
|
|
18778
|
+
if (point) {
|
|
18779
|
+
setDrawingHover(point);
|
|
18780
|
+
setHoverSnapped(lastSnapped.current);
|
|
18781
|
+
}
|
|
18221
18782
|
}
|
|
18222
18783
|
};
|
|
18223
18784
|
const cancel = () => {
|
|
@@ -18230,7 +18791,8 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18230
18791
|
let price = Number(candleSeries.coordinateToPrice(y));
|
|
18231
18792
|
if (logical == null || !Number.isFinite(price) || price <= 0) return null;
|
|
18232
18793
|
const time = timelineRef.current.toTime(Number(logical));
|
|
18233
|
-
const mode = magnetRef.current;
|
|
18794
|
+
const mode = event.ctrlKey || event.metaKey ? magnetRef.current === "off" ? "strong" : "off" : magnetRef.current;
|
|
18795
|
+
lastSnapped.current = false;
|
|
18234
18796
|
if (useMagnet && mode !== "off") {
|
|
18235
18797
|
const bar = barAt(candlesRef.current, time);
|
|
18236
18798
|
if (bar) {
|
|
@@ -18239,7 +18801,10 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18239
18801
|
const coordinate = candleSeries.priceToCoordinate(value);
|
|
18240
18802
|
if (coordinate != null && Math.abs(coordinate - y) < best.distance) best = { price: value, distance: Math.abs(coordinate - y) };
|
|
18241
18803
|
}
|
|
18242
|
-
if (mode === "strong" || best.distance <= MAGNET_PX)
|
|
18804
|
+
if (mode === "strong" || best.distance <= MAGNET_PX) {
|
|
18805
|
+
price = best.price;
|
|
18806
|
+
lastSnapped.current = true;
|
|
18807
|
+
}
|
|
18243
18808
|
}
|
|
18244
18809
|
}
|
|
18245
18810
|
if (event.shiftKey && previous) price = previous.price;
|
|
@@ -18288,6 +18853,10 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18288
18853
|
} else onDrawRef.current({ kind: mode, points: points2 });
|
|
18289
18854
|
return;
|
|
18290
18855
|
}
|
|
18856
|
+
if (groupRef.current.length) {
|
|
18857
|
+
setGroup([]);
|
|
18858
|
+
return;
|
|
18859
|
+
}
|
|
18291
18860
|
if (selectedRef.current) {
|
|
18292
18861
|
setSelectedId(null);
|
|
18293
18862
|
return;
|
|
@@ -18297,7 +18866,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18297
18866
|
return;
|
|
18298
18867
|
}
|
|
18299
18868
|
if (typeof barTime !== "number") return;
|
|
18300
|
-
const selection = selectChartCandle(selectionData.current.candles, barTime * 1e3, selectionData.current.indicators, hoveredHit.current, selectionData.current.maLines);
|
|
18869
|
+
const selection = selectChartCandle(selectionData.current.candles, barTime * 1e3, selectionData.current.indicators, hoveredHit.current, selectionData.current.maLines, selectionData.current.settings);
|
|
18301
18870
|
if (selection) {
|
|
18302
18871
|
setSelectedTime(selection.candle.t);
|
|
18303
18872
|
onSelectRef.current(selection);
|
|
@@ -18311,12 +18880,21 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18311
18880
|
viewportFrame = requestAnimationFrame(() => setViewportVersion((value) => value + 1));
|
|
18312
18881
|
};
|
|
18313
18882
|
chart.timeScale().subscribeVisibleLogicalRangeChange(refreshOverlay);
|
|
18883
|
+
const scaleDrag = (event) => {
|
|
18884
|
+
if (event.buttons) refreshOverlay();
|
|
18885
|
+
};
|
|
18886
|
+
box.addEventListener("pointermove", scaleDrag, { passive: true });
|
|
18887
|
+
box.addEventListener("wheel", refreshOverlay, { passive: true });
|
|
18888
|
+
box.addEventListener("dblclick", refreshOverlay);
|
|
18314
18889
|
const sizeObserver = new ResizeObserver(refreshOverlay);
|
|
18315
18890
|
sizeObserver.observe(box);
|
|
18316
18891
|
chart.timeScale().subscribeVisibleTimeRangeChange((range) => {
|
|
18317
18892
|
if (!fitted.current || restoring.current || !range || typeof range.from !== "number" || typeof range.to !== "number") return;
|
|
18318
18893
|
if (rangeTimer) clearTimeout(rangeTimer);
|
|
18319
|
-
rangeTimer = setTimeout(() =>
|
|
18894
|
+
rangeTimer = setTimeout(() => {
|
|
18895
|
+
const current2 = chart.timeScale().getVisibleRange();
|
|
18896
|
+
if (current2 && typeof current2.from === "number" && typeof current2.to === "number") onRangeRef.current({ from: current2.from, to: current2.to });
|
|
18897
|
+
}, 650);
|
|
18320
18898
|
});
|
|
18321
18899
|
chart.timeScale().subscribeVisibleLogicalRangeChange((range) => {
|
|
18322
18900
|
if (!fitted.current || restoring.current || !range) return;
|
|
@@ -18351,17 +18929,22 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18351
18929
|
box.removeEventListener("pointercancel", cancel, true);
|
|
18352
18930
|
box.removeEventListener("click", chartClick);
|
|
18353
18931
|
chart.timeScale().unsubscribeVisibleLogicalRangeChange(refreshOverlay);
|
|
18932
|
+
box.removeEventListener("pointermove", scaleDrag);
|
|
18933
|
+
box.removeEventListener("wheel", refreshOverlay);
|
|
18934
|
+
box.removeEventListener("dblclick", refreshOverlay);
|
|
18354
18935
|
sizeObserver.disconnect();
|
|
18355
18936
|
if (viewportFrame) cancelAnimationFrame(viewportFrame);
|
|
18356
18937
|
if (rangeTimer) clearTimeout(rangeTimer);
|
|
18938
|
+
const range = chart.timeScale().getVisibleRange();
|
|
18939
|
+
if (range && typeof range.from === "number" && typeof range.to === "number") initialRange.current = { from: range.from, to: range.to };
|
|
18357
18940
|
refs.current = null;
|
|
18358
18941
|
maRefs.current.clear();
|
|
18359
18942
|
priceLines.current.clear();
|
|
18360
18943
|
chart.remove();
|
|
18361
18944
|
tradeMarkers.current = null;
|
|
18362
18945
|
};
|
|
18363
|
-
}, []);
|
|
18364
|
-
|
|
18946
|
+
}, [chartType]);
|
|
18947
|
+
React10.useEffect(() => {
|
|
18365
18948
|
const markers = tradeEvents.filter((event) => event.kind === "order").map((event) => ({
|
|
18366
18949
|
time: Math.floor(event.candleTime / 1e3),
|
|
18367
18950
|
position: event.isBuy ? "belowBar" : "aboveBar",
|
|
@@ -18378,11 +18961,11 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18378
18961
|
});
|
|
18379
18962
|
markers.sort((a, b) => Number(a.time) - Number(b.time));
|
|
18380
18963
|
tradeMarkers.current?.setMarkers(markers);
|
|
18381
|
-
}, [tradeEvents, selectedTime, candles]);
|
|
18382
|
-
|
|
18964
|
+
}, [tradeEvents, selectedTime, candles, chartType]);
|
|
18965
|
+
React10.useEffect(() => {
|
|
18383
18966
|
const r = refs.current;
|
|
18384
18967
|
if (!r) return;
|
|
18385
|
-
const rows = candles;
|
|
18968
|
+
const rows = chartBars(candles, chartType);
|
|
18386
18969
|
if (rows.length < 2) return;
|
|
18387
18970
|
const seen = /* @__PURE__ */ new Set();
|
|
18388
18971
|
const bars = [];
|
|
@@ -18406,9 +18989,11 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18406
18989
|
const prevFirst = firstBarTime.current;
|
|
18407
18990
|
const prepended = prevFirst != null && bars[0].time < prevFirst ? Math.max(0, bars.findIndex((bar) => bar.time >= prevFirst)) : 0;
|
|
18408
18991
|
firstBarTime.current = bars[0].time;
|
|
18409
|
-
r.candles.setData(bars);
|
|
18992
|
+
r.candles.setData(chartType === "line" || chartType === "area" ? bars.map((bar) => ({ time: bar.time, value: bar.close })) : bars);
|
|
18410
18993
|
r.volume.setData(vols.map((v) => ({ ...v, color: v.color === FALLBACK_UP ? th.up : th.down })));
|
|
18411
18994
|
r.sma.setData(chartData(studies.sma20));
|
|
18995
|
+
r.basis.setData(chartData(studies.basis));
|
|
18996
|
+
r.basis.applyOptions({ visible: indicators.has("bollinger20") });
|
|
18412
18997
|
r.sma60.setData(chartData(studies.sma60));
|
|
18413
18998
|
r.ema20.setData(chartData(studies.ema20));
|
|
18414
18999
|
r.ema50.setData(chartData(studies.ema50));
|
|
@@ -18421,13 +19006,32 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18421
19006
|
r.ema50.applyOptions({ visible: indicators.has("ema50") });
|
|
18422
19007
|
r.upper.applyOptions({ visible: indicators.has("bollinger20") });
|
|
18423
19008
|
r.lower.applyOptions({ visible: indicators.has("bollinger20") });
|
|
19009
|
+
if (indicators.has("vwap")) {
|
|
19010
|
+
r.vwap ??= r.chart.addSeries(lineSeries, { color: "#e89a56", lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
|
|
19011
|
+
r.vwap.setData(chartData(studies.vwap));
|
|
19012
|
+
} else if (r.vwap) {
|
|
19013
|
+
r.chart.removeSeries(r.vwap);
|
|
19014
|
+
r.vwap = void 0;
|
|
19015
|
+
}
|
|
19016
|
+
if (indicators.has("atr14")) {
|
|
19017
|
+
if (!r.atr) {
|
|
19018
|
+
const pane = r.chart.addPane();
|
|
19019
|
+
pane.setStretchFactor(0.28);
|
|
19020
|
+
r.atr = pane.addSeries(lineSeries, { color: "#56bca7", lineWidth: 2, priceLineVisible: false });
|
|
19021
|
+
}
|
|
19022
|
+
r.atr.setData(chartData(studies.atr14));
|
|
19023
|
+
} else if (r.atr) {
|
|
19024
|
+
r.chart.removeSeries(r.atr);
|
|
19025
|
+
r.atr = void 0;
|
|
19026
|
+
}
|
|
18424
19027
|
if (indicators.has("rsi14")) {
|
|
18425
19028
|
if (!r.rsi) {
|
|
18426
19029
|
const pane = r.chart.addPane();
|
|
18427
19030
|
pane.setStretchFactor(0.28);
|
|
18428
19031
|
r.rsi = pane.addSeries(lineSeries, { color: "#a788df", lineWidth: 2, priceLineVisible: false, lastValueVisible: true });
|
|
18429
|
-
|
|
19032
|
+
r.rsiBounds = [settings.rsi.lower, settings.rsi.upper].map((price) => r.rsi.createPriceLine({ price, color: "#8d8d8d", lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: false }));
|
|
18430
19033
|
}
|
|
19034
|
+
r.rsiBounds?.forEach((line, index) => line.applyOptions({ price: index ? settings.rsi.upper : settings.rsi.lower }));
|
|
18431
19035
|
r.rsi.setData(chartData(studies.rsi14));
|
|
18432
19036
|
} else if (r.rsi) {
|
|
18433
19037
|
r.chart.removeSeries(r.rsi);
|
|
@@ -18452,6 +19056,30 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18452
19056
|
r.signal = void 0;
|
|
18453
19057
|
r.histogram = void 0;
|
|
18454
19058
|
}
|
|
19059
|
+
for (const group2 of [
|
|
19060
|
+
{ id: "stochastic", keys: ["stochK", "stochD"], colors: ["#55a6ce", "#e2a943"] },
|
|
19061
|
+
{ id: "adx", keys: ["adx", "plusDI", "minusDI"], colors: ["#a788df", "#2fd3a0", "#ef5f6c"] },
|
|
19062
|
+
{ id: "obv", keys: ["obv"], colors: ["#56bca7"] }
|
|
19063
|
+
]) {
|
|
19064
|
+
if (indicators.has(group2.id)) {
|
|
19065
|
+
if (!r.extra[group2.keys[0]]) {
|
|
19066
|
+
const pane = r.chart.addPane();
|
|
19067
|
+
pane.setStretchFactor(0.28);
|
|
19068
|
+
group2.keys.forEach((key3, index) => {
|
|
19069
|
+
r.extra[key3] = pane.addSeries(lineSeries, { color: group2.colors[index], lineWidth: 2, priceLineVisible: false, lastValueVisible: false });
|
|
19070
|
+
});
|
|
19071
|
+
if (group2.id === "stochastic") r.stochBounds = [settings.stochastic.lower, settings.stochastic.upper].map((price) => r.extra.stochK.createPriceLine({ price, color: "#8d8d8d", lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: false }));
|
|
19072
|
+
if (group2.id === "adx") r.extra.adx.createPriceLine({ price: 25, color: "#8d8d8d", lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: false });
|
|
19073
|
+
}
|
|
19074
|
+
for (const key3 of group2.keys) r.extra[key3].setData(chartData(studies[key3]));
|
|
19075
|
+
if (group2.id === "stochastic") r.stochBounds?.forEach((line, index) => line.applyOptions({ price: index ? settings.stochastic.upper : settings.stochastic.lower }));
|
|
19076
|
+
} else for (const key3 of group2.keys) {
|
|
19077
|
+
if (r.extra[key3]) {
|
|
19078
|
+
r.chart.removeSeries(r.extra[key3]);
|
|
19079
|
+
delete r.extra[key3];
|
|
19080
|
+
}
|
|
19081
|
+
}
|
|
19082
|
+
}
|
|
18455
19083
|
if (viewBefore && prepended > 0) {
|
|
18456
19084
|
restoring.current = true;
|
|
18457
19085
|
r.chart.timeScale().setVisibleLogicalRange({ from: viewBefore.from + prepended, to: viewBefore.to + prepended });
|
|
@@ -18509,8 +19137,8 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18509
19137
|
restoring.current = false;
|
|
18510
19138
|
}, 100);
|
|
18511
19139
|
}
|
|
18512
|
-
}, [candles, studies, indicators, showVolume, maComputed]);
|
|
18513
|
-
|
|
19140
|
+
}, [candles, studies, indicators, showVolume, maComputed, chartType, settings]);
|
|
19141
|
+
React10.useEffect(() => {
|
|
18514
19142
|
const r = refs.current;
|
|
18515
19143
|
if (!r || !focus || focus.at === handledFocus.current || candles.length < 2) return;
|
|
18516
19144
|
if (focus.t < candles[0].t) {
|
|
@@ -18518,7 +19146,8 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18518
19146
|
else if (olderState === "idle") onNeedOlderRef.current();
|
|
18519
19147
|
return;
|
|
18520
19148
|
}
|
|
18521
|
-
|
|
19149
|
+
let index = candleIndex(candles, focus.t);
|
|
19150
|
+
if (index > 0 && candles[index].t > focus.t) index--;
|
|
18522
19151
|
if (index < 0) return;
|
|
18523
19152
|
handledFocus.current = focus.at;
|
|
18524
19153
|
const view2 = r.chart.timeScale().getVisibleLogicalRange();
|
|
@@ -18530,19 +19159,105 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18530
19159
|
}, 100);
|
|
18531
19160
|
setSelectedTime(candles[index].t);
|
|
18532
19161
|
}, [focus, candles, olderState]);
|
|
18533
|
-
|
|
19162
|
+
React10.useEffect(() => {
|
|
18534
19163
|
refs.current?.chart.priceScale("right").applyOptions({ mode: priceScale === "log" ? PriceScaleMode.Logarithmic : priceScale === "percentage" ? PriceScaleMode.Percentage : PriceScaleMode.Normal });
|
|
18535
19164
|
setViewportVersion((value) => value + 1);
|
|
18536
|
-
}, [priceScale]);
|
|
18537
|
-
|
|
19165
|
+
}, [priceScale, chartType]);
|
|
19166
|
+
React10.useEffect(() => {
|
|
19167
|
+
const r = refs.current, box = boxRef.current;
|
|
19168
|
+
if (!command || command.at === handledCommand.current || !r || !box || candles.length < 2) return;
|
|
19169
|
+
const last = candles.at(-1);
|
|
19170
|
+
if (command.kind === "snapshot") {
|
|
19171
|
+
handledCommand.current = command.at;
|
|
19172
|
+
void chartImage(r.chart, box, caption).then((blob) => downloadBlob(blob, `${caption.replace(/[^a-zA-Z0-9_-]/g, "_")}.png`)).catch(() => notify(tr("\u56FE\u8868\u56FE\u7247\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5", "Chart image export failed; please retry")));
|
|
19173
|
+
return;
|
|
19174
|
+
}
|
|
19175
|
+
if (command.kind === "live") {
|
|
19176
|
+
r.chart.priceScale("right").applyOptions({ autoScale: true });
|
|
19177
|
+
r.chart.timeScale().scrollToPosition(0, false);
|
|
19178
|
+
} else if (command.kind === "fit") r.chart.timeScale().fitContent();
|
|
19179
|
+
else {
|
|
19180
|
+
const target = command.kind === "date" ? command.time : last.t - command.duration;
|
|
19181
|
+
if (target > Date.now()) {
|
|
19182
|
+
handledCommand.current = command.at;
|
|
19183
|
+
notify(tr("\u8BF7\u9009\u62E9\u5DF2\u53D1\u751F\u7684\u65E5\u671F", "Choose a past date"));
|
|
19184
|
+
return;
|
|
19185
|
+
}
|
|
19186
|
+
if (target < candles[0].t && olderState !== "done" && olderState !== "error") {
|
|
19187
|
+
if (olderState === "idle") onNeedOlderRef.current();
|
|
19188
|
+
return;
|
|
19189
|
+
}
|
|
19190
|
+
if (target < candles[0].t) notify(olderState === "error" ? tr("\u5386\u53F2\u52A0\u8F7D\u5931\u8D25\uFF0C\u4EC5\u663E\u793A\u5DF2\u52A0\u8F7D\u7684\u6570\u636E\uFF1B\u53EF\u91CD\u65B0\u5B9A\u4F4D\u91CD\u8BD5", "History load failed; showing loaded data. Select the date again to retry.") : tr("\u5DF2\u5230\u53EF\u7528\u5386\u53F2\u8FB9\u754C\uFF0C\u663E\u793A\u6700\u65E9\u53EF\u7528\u7684\u6570\u636E", "History limit reached; showing the earliest available data"));
|
|
19191
|
+
const index = candleIndex(candles, target);
|
|
19192
|
+
if (command.kind === "date") {
|
|
19193
|
+
const view2 = r.chart.timeScale().getVisibleLogicalRange();
|
|
19194
|
+
const half = Math.max(15, view2 ? (view2.to - view2.from) / 2 : 40);
|
|
19195
|
+
r.chart.timeScale().setVisibleLogicalRange({ from: index - half, to: index + half });
|
|
19196
|
+
} else r.chart.timeScale().setVisibleLogicalRange({ from: index, to: candles.length - 1 + 3 });
|
|
19197
|
+
}
|
|
19198
|
+
handledCommand.current = command.at;
|
|
19199
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
19200
|
+
if (refs.current !== r || handledCommand.current !== command.at) return;
|
|
19201
|
+
const range = r.chart.timeScale().getVisibleRange();
|
|
19202
|
+
if (range && typeof range.from === "number" && typeof range.to === "number") onRangeRef.current({ from: range.from, to: range.to });
|
|
19203
|
+
}));
|
|
19204
|
+
setViewportVersion((value) => value + 1);
|
|
19205
|
+
}, [command, candles, olderState, caption]);
|
|
19206
|
+
React10.useEffect(() => {
|
|
19207
|
+
const r = refs.current, cursor = link?.cursor;
|
|
19208
|
+
if (!r || !cursor || cursor.source === link?.id) return;
|
|
19209
|
+
syncingCursor.current = true;
|
|
19210
|
+
try {
|
|
19211
|
+
if (cursor.time == null || !candles.length || cursor.time < candles[0].t || cursor.time >= candles[candles.length - 1].t + (candles.length > 1 ? candles[1].t - candles[0].t : 6e4)) {
|
|
19212
|
+
r.chart.clearCrosshairPosition();
|
|
19213
|
+
setCrosshair(null);
|
|
19214
|
+
return;
|
|
19215
|
+
}
|
|
19216
|
+
let lo = 0, hi = candles.length - 1;
|
|
19217
|
+
while (lo < hi) {
|
|
19218
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
19219
|
+
if (candles[mid].t <= cursor.time) lo = mid;
|
|
19220
|
+
else hi = mid - 1;
|
|
19221
|
+
}
|
|
19222
|
+
const candle = candles[lo];
|
|
19223
|
+
r.chart.setCrosshairPosition(candle.c, Math.floor(candle.t / 1e3), r.candles);
|
|
19224
|
+
setCrosshair(candle);
|
|
19225
|
+
} finally {
|
|
19226
|
+
syncingCursor.current = false;
|
|
19227
|
+
}
|
|
19228
|
+
}, [link?.cursor, candles, chartType]);
|
|
19229
|
+
const handledLinkRange = React10.useRef("");
|
|
19230
|
+
React10.useEffect(() => {
|
|
19231
|
+
const r = refs.current, range = link?.range;
|
|
19232
|
+
if (!r || !range || range.source === link?.id || candles.length < 2 || handledLinkRange.current === `${range.at}:${chartType}`) return;
|
|
19233
|
+
if (range.from * 1e3 < candles[0].t && olderState === "idle") {
|
|
19234
|
+
onNeedOlderRef.current();
|
|
19235
|
+
return;
|
|
19236
|
+
}
|
|
19237
|
+
handledLinkRange.current = `${range.at}:${chartType}`;
|
|
19238
|
+
externalRangeUntil.current = performance.now() + 900;
|
|
19239
|
+
restoring.current = true;
|
|
19240
|
+
const line = timelineRef.current;
|
|
19241
|
+
r.chart.timeScale().setVisibleLogicalRange({ from: line.toLogical(range.from * 1e3), to: line.toLogical(range.to * 1e3) });
|
|
19242
|
+
const actual = r.chart.timeScale().getVisibleRange();
|
|
19243
|
+
if (actual && typeof actual.from === "number" && typeof actual.to === "number") onRangeRef.current({ from: actual.from, to: actual.to });
|
|
19244
|
+
const timer = setTimeout(() => {
|
|
19245
|
+
restoring.current = false;
|
|
19246
|
+
}, 120);
|
|
19247
|
+
return () => {
|
|
19248
|
+
clearTimeout(timer);
|
|
19249
|
+
restoring.current = false;
|
|
19250
|
+
};
|
|
19251
|
+
}, [link?.range, candles, olderState, chartType]);
|
|
19252
|
+
React10.useEffect(() => {
|
|
18538
19253
|
pending.current = [];
|
|
18539
19254
|
setPendingPoints([]);
|
|
18540
19255
|
setDrawingHover(null);
|
|
18541
19256
|
}, [drawMode]);
|
|
18542
|
-
|
|
19257
|
+
React10.useEffect(() => {
|
|
18543
19258
|
if (resetToken > 0) refs.current?.chart.timeScale().fitContent();
|
|
18544
19259
|
}, [resetToken]);
|
|
18545
|
-
|
|
19260
|
+
React10.useEffect(() => {
|
|
18546
19261
|
const r = refs.current;
|
|
18547
19262
|
const box = boxRef.current;
|
|
18548
19263
|
if (!r || !box) return;
|
|
@@ -18561,20 +19276,24 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18561
19276
|
title: mark.label
|
|
18562
19277
|
}));
|
|
18563
19278
|
}
|
|
18564
|
-
}, [marks, themeVersion]);
|
|
19279
|
+
}, [marks, themeVersion, chartType]);
|
|
18565
19280
|
const warming = candles.length < 2;
|
|
18566
|
-
const markById =
|
|
18567
|
-
const markByIdRef =
|
|
19281
|
+
const markById = React10.useMemo(() => new Map(marks.map((mark) => [mark.id, mark])), [marks]);
|
|
19282
|
+
const markByIdRef = React10.useRef(markById);
|
|
18568
19283
|
markByIdRef.current = markById;
|
|
18569
|
-
const lockedRef =
|
|
19284
|
+
const lockedRef = React10.useRef(drawingsLocked);
|
|
18570
19285
|
lockedRef.current = drawingsLocked;
|
|
18571
|
-
const onEditRef =
|
|
19286
|
+
const onEditRef = React10.useRef(onEditMark);
|
|
18572
19287
|
onEditRef.current = onEditMark;
|
|
18573
|
-
const onRemoveRef =
|
|
19288
|
+
const onRemoveRef = React10.useRef(onRemoveMark);
|
|
18574
19289
|
onRemoveRef.current = onRemoveMark;
|
|
18575
|
-
const
|
|
19290
|
+
const onRemoveMarksRef = React10.useRef(onRemoveMarks);
|
|
19291
|
+
onRemoveMarksRef.current = onRemoveMarks;
|
|
19292
|
+
const onEditEachRef = React10.useRef(onEditEach);
|
|
19293
|
+
onEditEachRef.current = onEditEach;
|
|
19294
|
+
const shapes = React10.useMemo(() => marks.filter((mark) => !mark.hidden).map((mark) => ({ mark, points: markPoints(mark) })), [marks]);
|
|
18576
19295
|
const lastTime = candles.at(-1)?.t ?? Date.now();
|
|
18577
|
-
const view =
|
|
19296
|
+
const view = React10.useMemo(() => {
|
|
18578
19297
|
const chartApi = refs.current, box = boxRef.current;
|
|
18579
19298
|
if (!chartApi || !box) return null;
|
|
18580
19299
|
return {
|
|
@@ -18586,26 +19305,60 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18586
19305
|
width: Math.max(0, box.clientWidth - chartApi.chart.priceScale("right").width()),
|
|
18587
19306
|
height: chartApi.chart.panes()[0]?.getHeight() ?? box.clientHeight,
|
|
18588
19307
|
bars: (from, to) => timeline.toLogical(to) - timeline.toLogical(from),
|
|
18589
|
-
closes: (from, to) => candles.filter((candle) => candle.t >= from && candle.t <= to).map((candle) => ({ time: candle.t, price: candle.c }))
|
|
19308
|
+
closes: (from, to) => candles.filter((candle) => candle.t >= from && candle.t <= to).map((candle) => ({ time: candle.t, price: candle.c })),
|
|
19309
|
+
candles: (from, to) => candles.filter((candle) => candle.t >= from && candle.t <= to).map((candle) => ({ time: candle.t, open: candle.o, high: candle.h, low: candle.l, close: candle.c, volume: candle.v })),
|
|
19310
|
+
profile: (from, to, options) => cachedProfile(candles, from, to, options),
|
|
19311
|
+
firstTime: candles[0]?.t,
|
|
19312
|
+
barMs: candles.length > 1 ? candles[1].t - candles[0].t : void 0
|
|
18590
19313
|
};
|
|
18591
19314
|
}, [viewportVersion, timeline, themeVersion, expanded, indicators]);
|
|
18592
|
-
|
|
19315
|
+
React10.useEffect(() => {
|
|
18593
19316
|
if (selectedId && !markById.has(selectedId)) setSelectedId(null);
|
|
18594
19317
|
}, [markById, selectedId]);
|
|
18595
|
-
|
|
19318
|
+
React10.useEffect(() => {
|
|
18596
19319
|
if (drawMode) setSelectedId(null);
|
|
18597
19320
|
}, [drawMode]);
|
|
18598
|
-
|
|
19321
|
+
React10.useEffect(() => {
|
|
18599
19322
|
if (!draft) return;
|
|
18600
19323
|
const mark = markById.get(draft.id);
|
|
18601
19324
|
if (mark?.kind === "level") priceLines.current.get(draft.id)?.applyOptions({ price: draft.points[0].price });
|
|
18602
19325
|
}, [draft, markById]);
|
|
18603
|
-
|
|
19326
|
+
React10.useEffect(() => {
|
|
19327
|
+
setGroup((current2) => {
|
|
19328
|
+
const next = current2.filter((id) => markById.has(id));
|
|
19329
|
+
return next.length === current2.length ? current2 : next;
|
|
19330
|
+
});
|
|
19331
|
+
}, [markById]);
|
|
19332
|
+
React10.useEffect(() => {
|
|
19333
|
+
if (drawMode) setGroup([]);
|
|
19334
|
+
}, [drawMode]);
|
|
19335
|
+
React10.useEffect(() => {
|
|
19336
|
+
if (group.length < 2) return void 0;
|
|
19337
|
+
const onKey = (event) => {
|
|
19338
|
+
const element = event.target;
|
|
19339
|
+
if (element?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(element?.tagName ?? "")) return;
|
|
19340
|
+
if (event.key === "Escape") {
|
|
19341
|
+
event.preventDefault();
|
|
19342
|
+
setGroup([]);
|
|
19343
|
+
return;
|
|
19344
|
+
}
|
|
19345
|
+
if (event.key !== "Delete" && event.key !== "Backspace") return;
|
|
19346
|
+
if (lockedRef.current) return;
|
|
19347
|
+
event.preventDefault();
|
|
19348
|
+
const ids = groupRef.current.filter((id) => !markByIdRef.current.get(id)?.locked);
|
|
19349
|
+
if (ids.length) onRemoveMarksRef.current?.(ids);
|
|
19350
|
+
setGroup([]);
|
|
19351
|
+
};
|
|
19352
|
+
window.addEventListener("keydown", onKey, true);
|
|
19353
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
19354
|
+
}, [group.length]);
|
|
19355
|
+
React10.useEffect(() => {
|
|
18604
19356
|
if (!selectedId) return void 0;
|
|
18605
19357
|
const onKey = (event) => {
|
|
18606
19358
|
const element = event.target;
|
|
18607
19359
|
if (element?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(element?.tagName ?? "")) return;
|
|
18608
19360
|
if (event.key === "Escape") {
|
|
19361
|
+
event.preventDefault();
|
|
18609
19362
|
setSelectedId(null);
|
|
18610
19363
|
return;
|
|
18611
19364
|
}
|
|
@@ -18616,10 +19369,12 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18616
19369
|
onRemoveRef.current(selectedId);
|
|
18617
19370
|
setSelectedId(null);
|
|
18618
19371
|
};
|
|
18619
|
-
window.addEventListener("keydown", onKey);
|
|
18620
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
19372
|
+
window.addEventListener("keydown", onKey, true);
|
|
19373
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
18621
19374
|
}, [selectedId]);
|
|
18622
|
-
const
|
|
19375
|
+
const abortGesture = React10.useRef(null);
|
|
19376
|
+
React10.useEffect(() => () => abortGesture.current?.(), []);
|
|
19377
|
+
const [brushPoints, setBrushPoints] = React10.useState([]);
|
|
18623
19378
|
const beginBrush = (event) => {
|
|
18624
19379
|
const snap = snapRef.current;
|
|
18625
19380
|
const first = snap?.(event.nativeEvent, void 0, false);
|
|
@@ -18638,41 +19393,56 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18638
19393
|
cancelAnimationFrame(frame);
|
|
18639
19394
|
frame = requestAnimationFrame(() => setBrushPoints([...collected]));
|
|
18640
19395
|
};
|
|
18641
|
-
const
|
|
19396
|
+
const detach = () => {
|
|
18642
19397
|
window.removeEventListener("pointermove", move);
|
|
18643
19398
|
window.removeEventListener("pointerup", up);
|
|
18644
|
-
window.removeEventListener("pointercancel",
|
|
19399
|
+
window.removeEventListener("pointercancel", cancel);
|
|
19400
|
+
window.removeEventListener("keydown", onKey, true);
|
|
18645
19401
|
cancelAnimationFrame(frame);
|
|
18646
19402
|
setBrushPoints([]);
|
|
19403
|
+
abortGesture.current = null;
|
|
19404
|
+
};
|
|
19405
|
+
const cancel = () => detach();
|
|
19406
|
+
const onKey = (ev) => {
|
|
19407
|
+
if (ev.key === "Escape") {
|
|
19408
|
+
ev.stopPropagation();
|
|
19409
|
+
detach();
|
|
19410
|
+
}
|
|
19411
|
+
};
|
|
19412
|
+
const up = () => {
|
|
19413
|
+
detach();
|
|
18647
19414
|
if (collected.length < 2) return;
|
|
18648
19415
|
const stride = Math.ceil(collected.length / BRUSH_MAX);
|
|
18649
19416
|
const points2 = collected.filter((_, index) => index % stride === 0 || index === collected.length - 1).slice(-BRUSH_MAX);
|
|
18650
19417
|
onDrawRef.current({ kind: "brush", points: points2 });
|
|
18651
19418
|
};
|
|
19419
|
+
abortGesture.current?.();
|
|
19420
|
+
abortGesture.current = cancel;
|
|
18652
19421
|
window.addEventListener("pointermove", move);
|
|
18653
19422
|
window.addEventListener("pointerup", up);
|
|
18654
|
-
window.addEventListener("pointercancel",
|
|
19423
|
+
window.addEventListener("pointercancel", cancel);
|
|
19424
|
+
window.addEventListener("keydown", onKey, true);
|
|
18655
19425
|
};
|
|
18656
|
-
const cloneOf =
|
|
19426
|
+
const cloneOf = React10.useCallback((mark) => {
|
|
18657
19427
|
const line = timelineRef.current;
|
|
18658
19428
|
const shifted = markPoints(mark).map((point) => ({ time: line.toTime(line.toLogical(point.time) + 5), price: point.price }));
|
|
18659
19429
|
const style = Object.fromEntries(Object.entries({ color: mark.color, lineWidth: mark.lineWidth, lineStyle: mark.lineStyle }).filter(([, value]) => value != null));
|
|
18660
19430
|
return { kind: mark.kind, geometry: geometryPatch(mark.kind, shifted), style, label: mark.label };
|
|
18661
19431
|
}, []);
|
|
18662
|
-
const clipboard =
|
|
18663
|
-
const onCloneRef =
|
|
19432
|
+
const clipboard = React10.useRef(null);
|
|
19433
|
+
const onCloneRef = React10.useRef(onCloneMark);
|
|
18664
19434
|
onCloneRef.current = onCloneMark;
|
|
18665
|
-
|
|
19435
|
+
React10.useEffect(() => {
|
|
18666
19436
|
const onKey = (event) => {
|
|
18667
19437
|
if (!(event.metaKey || event.ctrlKey) || event.altKey) return;
|
|
18668
19438
|
const element = event.target;
|
|
18669
19439
|
if (element?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(element?.tagName ?? "")) return;
|
|
18670
|
-
const
|
|
18671
|
-
if (
|
|
19440
|
+
const key3 = event.key.toLowerCase();
|
|
19441
|
+
if (key3 === "c" && selectedRef.current) {
|
|
18672
19442
|
clipboard.current = markByIdRef.current.get(selectedRef.current) ?? null;
|
|
18673
19443
|
if (clipboard.current) event.preventDefault();
|
|
18674
19444
|
}
|
|
18675
|
-
if (
|
|
19445
|
+
if (key3 === "v" && clipboard.current) {
|
|
18676
19446
|
event.preventDefault();
|
|
18677
19447
|
const copy = cloneOf(clipboard.current);
|
|
18678
19448
|
onCloneRef.current(copy);
|
|
@@ -18682,8 +19452,8 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18682
19452
|
window.addEventListener("keydown", onKey);
|
|
18683
19453
|
return () => window.removeEventListener("keydown", onKey);
|
|
18684
19454
|
}, [cloneOf]);
|
|
18685
|
-
const handledSelect =
|
|
18686
|
-
|
|
19455
|
+
const handledSelect = React10.useRef(0);
|
|
19456
|
+
React10.useEffect(() => {
|
|
18687
19457
|
const r = refs.current;
|
|
18688
19458
|
if (!selectRequest || selectRequest.at === handledSelect.current || !r) return;
|
|
18689
19459
|
const mark = markById.get(selectRequest.id);
|
|
@@ -18702,7 +19472,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18702
19472
|
}, 100);
|
|
18703
19473
|
}
|
|
18704
19474
|
}, [selectRequest, markById, timeline]);
|
|
18705
|
-
const beginDrag =
|
|
19475
|
+
const beginDrag = React10.useCallback((event) => {
|
|
18706
19476
|
if (drawModeRef.current === "brush" && event.button === 0) {
|
|
18707
19477
|
beginBrush(event);
|
|
18708
19478
|
return;
|
|
@@ -18722,6 +19492,81 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18722
19492
|
if (!id || !mark) return;
|
|
18723
19493
|
event.preventDefault();
|
|
18724
19494
|
event.stopPropagation();
|
|
19495
|
+
if (event.shiftKey || event.ctrlKey || event.metaKey) {
|
|
19496
|
+
const base = groupRef.current.length ? groupRef.current : selectedRef.current ? [selectedRef.current] : [];
|
|
19497
|
+
const next = base.includes(id) ? base.filter((item) => item !== id) : [...base, id];
|
|
19498
|
+
if (next.length >= 2) {
|
|
19499
|
+
setGroup(next);
|
|
19500
|
+
setSelectedId(null);
|
|
19501
|
+
} else {
|
|
19502
|
+
setGroup([]);
|
|
19503
|
+
setSelectedId(next[0] ?? null);
|
|
19504
|
+
}
|
|
19505
|
+
return;
|
|
19506
|
+
}
|
|
19507
|
+
const anchorHit = target.getAttribute("data-anchor") != null;
|
|
19508
|
+
if (groupRef.current.includes(id) && groupRef.current.length >= 2 && !anchorHit) {
|
|
19509
|
+
const snapGroup = snapRef.current;
|
|
19510
|
+
if (lockedRef.current || !snapGroup) return;
|
|
19511
|
+
const startGroup = snapGroup(event.nativeEvent, void 0, false);
|
|
19512
|
+
if (!startGroup) return;
|
|
19513
|
+
const line2 = timelineRef.current, startLogical2 = line2.toLogical(startGroup.time);
|
|
19514
|
+
const originals = new Map(groupRef.current.map((member) => markByIdRef.current.get(member)).filter((item) => Boolean(item) && !item.locked).map((item) => [item.id, item]));
|
|
19515
|
+
const origin2 = { x: event.clientX, y: event.clientY };
|
|
19516
|
+
let moved2 = false, frame2 = 0, latest2 = null;
|
|
19517
|
+
const compute2 = (ev) => {
|
|
19518
|
+
const point = snapGroup(ev, void 0, false);
|
|
19519
|
+
if (!point) return;
|
|
19520
|
+
const shift = Math.round(line2.toLogical(point.time) - startLogical2), delta = point.price - startGroup.price;
|
|
19521
|
+
const next = /* @__PURE__ */ new Map();
|
|
19522
|
+
for (const [memberId, member] of originals) {
|
|
19523
|
+
const moved3 = markPoints(member).map((item) => ({ time: line2.toTime(line2.toLogical(item.time) + shift), price: item.price + delta }));
|
|
19524
|
+
if (moved3.some((item) => item.price <= 0)) return;
|
|
19525
|
+
next.set(memberId, moved3);
|
|
19526
|
+
}
|
|
19527
|
+
latest2 = next;
|
|
19528
|
+
setGroupDraft(next);
|
|
19529
|
+
};
|
|
19530
|
+
const move2 = (ev) => {
|
|
19531
|
+
if (!moved2 && Math.hypot(ev.clientX - origin2.x, ev.clientY - origin2.y) < 3) return;
|
|
19532
|
+
moved2 = true;
|
|
19533
|
+
cancelAnimationFrame(frame2);
|
|
19534
|
+
frame2 = requestAnimationFrame(() => compute2(ev));
|
|
19535
|
+
};
|
|
19536
|
+
const detach2 = () => {
|
|
19537
|
+
window.removeEventListener("pointermove", move2);
|
|
19538
|
+
window.removeEventListener("pointerup", up2);
|
|
19539
|
+
window.removeEventListener("pointercancel", cancel2);
|
|
19540
|
+
window.removeEventListener("keydown", onKey2, true);
|
|
19541
|
+
cancelAnimationFrame(frame2);
|
|
19542
|
+
abortGesture.current = null;
|
|
19543
|
+
};
|
|
19544
|
+
const cancel2 = () => {
|
|
19545
|
+
detach2();
|
|
19546
|
+
setGroupDraft(null);
|
|
19547
|
+
};
|
|
19548
|
+
const onKey2 = (ev) => {
|
|
19549
|
+
if (ev.key === "Escape" && moved2) {
|
|
19550
|
+
ev.stopPropagation();
|
|
19551
|
+
cancel2();
|
|
19552
|
+
}
|
|
19553
|
+
};
|
|
19554
|
+
const up2 = (ev) => {
|
|
19555
|
+
detach2();
|
|
19556
|
+
if (moved2) compute2(ev);
|
|
19557
|
+
const final = latest2;
|
|
19558
|
+
if (moved2 && final) onEditEachRef.current?.([...final].map(([memberId, points2]) => [memberId, geometryPatch(originals.get(memberId).kind, points2)]));
|
|
19559
|
+
setGroupDraft(null);
|
|
19560
|
+
};
|
|
19561
|
+
abortGesture.current?.();
|
|
19562
|
+
abortGesture.current = cancel2;
|
|
19563
|
+
window.addEventListener("pointermove", move2);
|
|
19564
|
+
window.addEventListener("pointerup", up2);
|
|
19565
|
+
window.addEventListener("pointercancel", cancel2);
|
|
19566
|
+
window.addEventListener("keydown", onKey2, true);
|
|
19567
|
+
return;
|
|
19568
|
+
}
|
|
19569
|
+
setGroup([]);
|
|
18725
19570
|
setSelectedId(id);
|
|
18726
19571
|
const snap = snapRef.current;
|
|
18727
19572
|
if (mark.locked || lockedRef.current || !snap) return;
|
|
@@ -18767,25 +19612,44 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18767
19612
|
cancelAnimationFrame(frame);
|
|
18768
19613
|
frame = requestAnimationFrame(() => compute(ev));
|
|
18769
19614
|
};
|
|
18770
|
-
const
|
|
19615
|
+
const detach = () => {
|
|
18771
19616
|
window.removeEventListener("pointermove", move);
|
|
18772
19617
|
window.removeEventListener("pointerup", up);
|
|
18773
|
-
window.removeEventListener("pointercancel",
|
|
19618
|
+
window.removeEventListener("pointercancel", cancel);
|
|
19619
|
+
window.removeEventListener("keydown", onKey, true);
|
|
18774
19620
|
cancelAnimationFrame(frame);
|
|
19621
|
+
abortGesture.current = null;
|
|
19622
|
+
};
|
|
19623
|
+
const cancel = () => {
|
|
19624
|
+
detach();
|
|
19625
|
+
if (mark.kind === "level") priceLines.current.get(id)?.applyOptions({ price: original[0].price });
|
|
19626
|
+
setDraft(null);
|
|
19627
|
+
};
|
|
19628
|
+
const onKey = (ev) => {
|
|
19629
|
+
if (ev.key === "Escape" && moved) {
|
|
19630
|
+
ev.stopPropagation();
|
|
19631
|
+
cancel();
|
|
19632
|
+
}
|
|
19633
|
+
};
|
|
19634
|
+
const up = () => {
|
|
19635
|
+
detach();
|
|
18775
19636
|
if (moved && last) compute(last);
|
|
18776
19637
|
if (moved && latest) onEditRef.current(id, geometryPatch(mark.kind, latest));
|
|
18777
19638
|
setDraft(null);
|
|
18778
19639
|
};
|
|
19640
|
+
abortGesture.current?.();
|
|
19641
|
+
abortGesture.current = cancel;
|
|
18779
19642
|
window.addEventListener("pointermove", move);
|
|
18780
19643
|
window.addEventListener("pointerup", up);
|
|
18781
|
-
window.addEventListener("pointercancel",
|
|
19644
|
+
window.addEventListener("pointercancel", cancel);
|
|
19645
|
+
window.addEventListener("keydown", onKey, true);
|
|
18782
19646
|
}, []);
|
|
18783
|
-
const fills =
|
|
19647
|
+
const fills = React10.useMemo(() => tradeEvents.filter((event) => event.kind === "fill"), [tradeEvents]);
|
|
18784
19648
|
const fillCard = selectedFill ? fills.find((event) => event.id === selectedFill) ?? null : null;
|
|
18785
|
-
|
|
19649
|
+
React10.useEffect(() => {
|
|
18786
19650
|
if (selectedFill && !fills.some((event) => event.id === selectedFill)) setSelectedFill(null);
|
|
18787
19651
|
}, [fills, selectedFill]);
|
|
18788
|
-
|
|
19652
|
+
React10.useEffect(() => {
|
|
18789
19653
|
if (!selectedFill) return void 0;
|
|
18790
19654
|
const onKey = (event) => {
|
|
18791
19655
|
if (event.key === "Escape") setSelectedFill(null);
|
|
@@ -18797,11 +19661,17 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18797
19661
|
const previewPoints = drawMode && drawingHover ? [...pendingPoints, drawingHover] : pendingPoints;
|
|
18798
19662
|
const roleClass = (mark) => mark.source === "user" ? "role-user" : `role-${mark.role}`;
|
|
18799
19663
|
const selectedMark = selectedId ? markById.get(selectedId) ?? null : null;
|
|
18800
|
-
|
|
18801
|
-
|
|
19664
|
+
const editingMark = editingId ? markById.get(editingId) ?? null : null;
|
|
19665
|
+
const inspected = crosshair ?? candles.at(-1);
|
|
19666
|
+
const inspectedTime = inspected ? Math.floor(inspected.t / 1e3) : null;
|
|
19667
|
+
const studyAt = (key3) => studies[key3].find((point) => point.time === inspectedTime)?.value;
|
|
19668
|
+
const inspectedStudies = studyDefinitions(settings).filter(([id]) => indicators.has(id)).map(([, key3, label]) => [label, studyAt(key3)]);
|
|
19669
|
+
const paneCount = ["rsi14", "macd", "atr14", "stochastic", "adx", "obv"].filter((id) => indicators.has(id)).length;
|
|
19670
|
+
return /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement("div", { className: "dhl-chartbox", ref: boxRef, style: { height: expanded && !compact ? void 0 : (compact ? 310 : CHART_HEIGHT) + paneCount * (compact ? 65 : 125) } }, view ? /* @__PURE__ */ React10.createElement("svg", { className: "dhl-chart-drawing-overlay" + (drawMode ? " drawing" : "") + (drawMode === "brush" ? " brushing" : ""), "aria-hidden": "true", onPointerDown: beginDrag }, /* @__PURE__ */ React10.createElement("defs", null, /* @__PURE__ */ React10.createElement("clipPath", { id: clipId.current }, /* @__PURE__ */ React10.createElement("rect", { x: "0", y: "0", width: view.width, height: view.height })), /* @__PURE__ */ React10.createElement("marker", { id: `${clipId.current}-arrow`, viewBox: "0 0 10 10", refX: "8", refY: "5", markerWidth: "7", markerHeight: "7", orient: "auto-start-reverse" }, /* @__PURE__ */ React10.createElement("path", { d: "M0 0 10 5 0 10z", fill: "currentColor" }))), /* @__PURE__ */ React10.createElement("g", { clipPath: `url(#${clipId.current})` }, shapes.map(({ mark, points: points2 }) => {
|
|
19671
|
+
const live = draft?.id === mark.id ? draft.points : groupDraft?.get(mark.id) ?? points2;
|
|
18802
19672
|
const shown = mark.kind === "level" ? [{ time: lastTime, price: live[0].price }] : live;
|
|
18803
|
-
const selected = selectedId === mark.id;
|
|
18804
|
-
return /* @__PURE__ */
|
|
19673
|
+
const selected = selectedId === mark.id || group.includes(mark.id);
|
|
19674
|
+
return /* @__PURE__ */ React10.createElement(
|
|
18805
19675
|
DrawingShape,
|
|
18806
19676
|
{
|
|
18807
19677
|
key: mark.id,
|
|
@@ -18817,15 +19687,21 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18817
19687
|
color: mark.color,
|
|
18818
19688
|
lineWidth: mark.lineWidth,
|
|
18819
19689
|
lineStyle: mark.lineStyle,
|
|
18820
|
-
|
|
19690
|
+
alert: mark.alert,
|
|
19691
|
+
shared: mark.shared,
|
|
19692
|
+
rows: mark.rows,
|
|
19693
|
+
valueArea: mark.valueArea
|
|
18821
19694
|
}
|
|
18822
19695
|
);
|
|
18823
|
-
}), /* @__PURE__ */
|
|
19696
|
+
}), /* @__PURE__ */ React10.createElement("g", { className: "dhl-fills" }, fills.map((event) => {
|
|
18824
19697
|
const point = view.project(event.time, event.px);
|
|
18825
19698
|
if (!point) return null;
|
|
18826
19699
|
const short = event.reason.length > 18 ? event.reason.slice(0, 17) + "\u2026" : event.reason;
|
|
18827
|
-
return /* @__PURE__ */
|
|
18828
|
-
})), brushPoints.length > 1 ? /* @__PURE__ */
|
|
19700
|
+
return /* @__PURE__ */ React10.createElement("g", { key: event.id, className: `dhl-fill ${event.isBuy ? "buy" : "sell"}${event.reason ? " has-reason" : ""}${selectedFill === event.id ? " selected" : ""}`, "data-fill-id": event.id }, event.reason ? /* @__PURE__ */ React10.createElement("circle", { className: "dhl-fill-ring", cx: point.x, cy: point.y, r: "9" }) : null, /* @__PURE__ */ React10.createElement("circle", { className: "dhl-fill-dot", cx: point.x, cy: point.y, r: "5" }), showReasons && short ? /* @__PURE__ */ React10.createElement("text", { className: "dhl-fill-reason", x: point.x + 12, y: point.y + 4 }, short) : null);
|
|
19701
|
+
})), brushPoints.length > 1 ? /* @__PURE__ */ React10.createElement(DrawingShape, { kind: "brush", points: brushPoints, view, className: "dhl-drawing dhl-drawing-preview role-user" }) : null, drawMode && drawingHover && hoverSnapped ? (() => {
|
|
19702
|
+
const at = view.project(drawingHover.time, drawingHover.price);
|
|
19703
|
+
return at ? /* @__PURE__ */ React10.createElement("circle", { className: "dhl-snap-ring", cx: at.x, cy: at.y, r: "6" }) : null;
|
|
19704
|
+
})() : null, drawMode && drawMode !== "brush" && previewPoints.length ? /* @__PURE__ */ React10.createElement(DrawingShape, { kind: drawMode, points: completePreview(drawMode, previewPoints), view, className: "dhl-drawing dhl-drawing-preview role-user", anchors: true }) : null)) : null, fillCard && fillAt && view ? /* @__PURE__ */ React10.createElement(
|
|
18829
19705
|
"div",
|
|
18830
19706
|
{
|
|
18831
19707
|
className: "dhl-fill-card",
|
|
@@ -18835,19 +19711,29 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18835
19711
|
onPointerDown: (event) => event.stopPropagation(),
|
|
18836
19712
|
onClick: (event) => event.stopPropagation()
|
|
18837
19713
|
},
|
|
18838
|
-
/* @__PURE__ */
|
|
18839
|
-
/* @__PURE__ */
|
|
18840
|
-
/* @__PURE__ */
|
|
18841
|
-
fillCard.reason ? /* @__PURE__ */
|
|
18842
|
-
/* @__PURE__ */
|
|
19714
|
+
/* @__PURE__ */ React10.createElement("header", null, /* @__PURE__ */ React10.createElement("b", { className: fillCard.isBuy ? "dhl-up" : "dhl-down" }, fillCard.isBuy ? tr("\u4E70\u5165", "Buy") : tr("\u5356\u51FA", "Sell"), " ", Number.isFinite(fillCard.sz) ? fillCard.sz : "\u2014", " ", fillCard.coin, " @ ", fmtPrice(fillCard.px)), /* @__PURE__ */ React10.createElement("button", { type: "button", "aria-label": tr("\u5173\u95ED", "Close"), onClick: () => setSelectedFill(null) }, /* @__PURE__ */ React10.createElement(UiIcon, { name: "close", size: 14 }))),
|
|
19715
|
+
/* @__PURE__ */ React10.createElement("time", null, new Date(fillCard.time).toLocaleString(tr("zh-CN", "en-US"), { hour12: false })),
|
|
19716
|
+
/* @__PURE__ */ React10.createElement("strong", null, tr("AI \u4EA4\u6613\u51B3\u7B56\u539F\u56E0", "AI decision rationale")),
|
|
19717
|
+
fillCard.reason ? /* @__PURE__ */ React10.createElement("p", null, fillCard.reason) : /* @__PURE__ */ React10.createElement("p", { className: "muted" }, tr("\u8FD9\u7B14\u6210\u4EA4\u6CA1\u6709\u8BB0\u5F55\u539F\u56E0\uFF08\u65E7\u8BA2\u5355\uFF0C\u6216\u5728\u5176\u4ED6\u5BA2\u6237\u7AEF\u4E0B\u5355\uFF09\u3002", "No rationale recorded (older order or placed elsewhere).")),
|
|
19718
|
+
/* @__PURE__ */ React10.createElement("footer", null, /* @__PURE__ */ React10.createElement("button", { type: "button", className: "dhl-fill-analyze", onClick: () => {
|
|
18843
19719
|
onAnalyzeFill(fillCard);
|
|
18844
19720
|
setSelectedFill(null);
|
|
18845
19721
|
} }, fillCard.sessionId ? tr("\u56DE\u5230\u51B3\u7B56\u4F1A\u8BDD\u5206\u6790", "Analyze in its session") : tr("\u5728\u5F53\u524D\u4F1A\u8BDD\u5206\u6790", "Analyze in this session")))
|
|
18846
|
-
) : null,
|
|
19722
|
+
) : null, group.length >= 2 && !drawMode && !readOnly ? (() => {
|
|
19723
|
+
const members = group.map((id) => markById.get(id)).filter((mark) => Boolean(mark));
|
|
19724
|
+
const editable = members.filter((mark) => !mark.locked).map((mark) => mark.id);
|
|
19725
|
+
const allHidden = members.every((mark) => mark.hidden), allLocked = members.every((mark) => mark.locked);
|
|
19726
|
+
const stop = (event) => event.stopPropagation();
|
|
19727
|
+
return /* @__PURE__ */ React10.createElement("div", { className: "dhl-draw-toolbar", role: "toolbar", "aria-label": tr("\u6279\u91CF\u7F16\u8F91\u753B\u7EBF", "Edit selected drawings"), onPointerDown: stop, onClick: stop }, /* @__PURE__ */ React10.createElement("span", { className: "dhl-draw-toolbar-name" }, tr(`\u5DF2\u9009 ${members.length} \u6761\u753B\u7EBF`, `${members.length} drawings selected`)), /* @__PURE__ */ React10.createElement("span", { className: "dhl-draw-toolbar-group", role: "group", "aria-label": tr("\u989C\u8272", "Color") }, DRAW_COLORS.map((color) => /* @__PURE__ */ React10.createElement("button", { type: "button", key: color, className: "dhl-swatch", style: { background: color }, disabled: drawingsLocked || editable.length === 0, "aria-label": `${tr("\u5168\u90E8\u6539\u4E3A", "Recolor all")} ${color}`, onClick: () => onEditMarks?.(editable, { color }) }))), /* @__PURE__ */ React10.createElement("span", { className: "dhl-draw-toolbar-group" }, /* @__PURE__ */ React10.createElement("button", { type: "button", "aria-label": allHidden ? tr("\u5168\u90E8\u663E\u793A", "Show all") : tr("\u5168\u90E8\u9690\u85CF", "Hide all"), title: allHidden ? tr("\u5168\u90E8\u663E\u793A", "Show all") : tr("\u5168\u90E8\u9690\u85CF", "Hide all"), disabled: drawingsLocked, onClick: () => onEditMarks?.(group, { hidden: !allHidden }) }, /* @__PURE__ */ React10.createElement(UiIcon, { name: allHidden ? "eye" : "eyeOff", size: 15 })), /* @__PURE__ */ React10.createElement("button", { type: "button", className: allLocked ? "on" : "", "aria-pressed": allLocked, "aria-label": allLocked ? tr("\u5168\u90E8\u89E3\u9501", "Unlock all") : tr("\u5168\u90E8\u9501\u5B9A", "Lock all"), title: allLocked ? tr("\u5168\u90E8\u89E3\u9501", "Unlock all") : tr("\u5168\u90E8\u9501\u5B9A", "Lock all"), disabled: drawingsLocked, onClick: () => onEditMarks?.(group, { locked: !allLocked }) }, /* @__PURE__ */ React10.createElement(UiIcon, { name: allLocked ? "lock" : "unlock", size: 15 })), /* @__PURE__ */ React10.createElement("button", { type: "button", "aria-label": tr("\u5220\u9664\u6240\u9009\u753B\u7EBF", "Delete selected drawings"), title: tr("\u5220\u9664\uFF08Delete\uFF09", "Delete (Del)"), disabled: drawingsLocked || editable.length === 0, onClick: () => {
|
|
19728
|
+
onRemoveMarks?.(editable);
|
|
19729
|
+
setGroup([]);
|
|
19730
|
+
} }, /* @__PURE__ */ React10.createElement(UiIcon, { name: "trash", size: 15 })), /* @__PURE__ */ React10.createElement("button", { type: "button", "aria-label": tr("\u53D6\u6D88\u9009\u62E9", "Deselect"), title: tr("\u53D6\u6D88\u9009\u62E9\uFF08Esc\uFF09", "Deselect (Esc)"), onClick: () => setGroup([]) }, /* @__PURE__ */ React10.createElement(UiIcon, { name: "close", size: 14 }))));
|
|
19731
|
+
})() : null, selectedMark && !drawMode && !readOnly ? /* @__PURE__ */ React10.createElement(
|
|
18847
19732
|
DrawToolbar,
|
|
18848
19733
|
{
|
|
18849
19734
|
mark: selectedMark,
|
|
18850
19735
|
globallyLocked: drawingsLocked,
|
|
19736
|
+
onCoordinates: () => setEditingId(selectedMark.id),
|
|
18851
19737
|
onEdit: (patch) => onEditMark(selectedMark.id, patch),
|
|
18852
19738
|
onClone: () => onCloneMark(cloneOf(selectedMark)),
|
|
18853
19739
|
onRemove: () => {
|
|
@@ -18856,21 +19742,36 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
18856
19742
|
},
|
|
18857
19743
|
onClose: () => setSelectedId(null)
|
|
18858
19744
|
}
|
|
18859
|
-
) : null,
|
|
19745
|
+
) : null, editingMark ? /* @__PURE__ */ React10.createElement(DrawingEditor, { key: editingMark.id, mark: editingMark, onApply: (patch) => {
|
|
19746
|
+
if (!drawingsLocked && !editingMark.locked) onEditMark(editingMark.id, patch);
|
|
19747
|
+
}, onClose: () => setEditingId(null) }) : null, !warming && /* @__PURE__ */ React10.createElement("div", { className: "dhl-chart-ohlc", "data-time": inspected?.t, "aria-live": "off" }, /* @__PURE__ */ React10.createElement("span", null, crosshair ? new Date(crosshair.t).toLocaleString(tr("zh-CN", "en-US"), { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false }) : tr("\u6700\u65B0 K \u7EBF", "Latest candle")), ["o", "h", "l", "c"].map((key3) => /* @__PURE__ */ React10.createElement("span", { key: key3 }, key3.toUpperCase(), " ", /* @__PURE__ */ React10.createElement("b", { className: inspected && inspected.c < inspected.o ? "dhl-down" : "dhl-up" }, fmtPrice(inspected?.[key3])))), /* @__PURE__ */ React10.createElement("span", null, tr("\u91CF", "Vol"), " ", /* @__PURE__ */ React10.createElement("b", null, inspected?.v.toLocaleString(void 0, { maximumFractionDigits: 4 }) ?? "\u2014")), inspected ? /* @__PURE__ */ React10.createElement("span", { className: inspected.c >= inspected.o ? "dhl-up" : "dhl-down" }, ((inspected.c / inspected.o - 1) * 100).toFixed(2), "%") : null, chartType === "heikinAshi" ? /* @__PURE__ */ React10.createElement("span", null, tr("\u5E73\u5747K\u7EBF \xB7 \u6570\u503C\u4E3A\u539F\u59CB\u884C\u60C5", "Heikin Ashi \xB7 values show exchange OHLC")) : null), !warming && olderState === "loading" ? /* @__PURE__ */ React10.createElement("div", { className: "dhl-chart-older", role: "status" }, tr("\u6B63\u5728\u52A0\u8F7D\u66F4\u65E9\u7684 K \u7EBF\u2026", "Loading earlier candles\u2026")) : null, !warming && olderState === "error" ? /* @__PURE__ */ React10.createElement("div", { className: "dhl-chart-older", role: "status" }, tr("\u66F4\u65E9\u7684 K \u7EBF\u52A0\u8F7D\u5931\u8D25\uFF0C\u7EE7\u7EED\u5411\u5DE6\u62D6\u52A8\u53EF\u91CD\u8BD5", "Could not load earlier candles; drag left again to retry")) : null, !warming && maComputed.length > 0 ? /* @__PURE__ */ React10.createElement("div", { className: "dhl-chart-ma-legend", "aria-label": tr("\u5747\u7EBF\u6570\u503C", "Moving average values") }, maComputed.map((item) => {
|
|
18860
19748
|
const index = crosshair ? candles.findIndex((candle) => Math.floor(candle.t / 1e3) === Math.floor(crosshair.t / 1e3)) : candles.length - 1;
|
|
18861
19749
|
const at = (series) => series && index >= 0 ? series[index] : null;
|
|
18862
|
-
return /* @__PURE__ */
|
|
18863
|
-
})) : null,
|
|
19750
|
+
return /* @__PURE__ */ React10.createElement("span", { key: item.line.id, style: { color: item.color } }, maLabel(item.line), " ", /* @__PURE__ */ React10.createElement("b", null, fmtPrice(at(item.values))), item.smoothed ? /* @__PURE__ */ React10.createElement(React10.Fragment, null, " / ", /* @__PURE__ */ React10.createElement("b", null, fmtPrice(at(item.smoothed)))) : null);
|
|
19751
|
+
})) : null, crosshair && tradeEvents.some((event) => event.candleTime === crosshair.t) ? /* @__PURE__ */ React10.createElement("div", { className: "dhl-chart-trade-detail", "aria-live": "off" }, tradeEvents.filter((event) => event.candleTime === crosshair.t).slice(0, 4).map((event) => /* @__PURE__ */ React10.createElement("div", { key: event.id }, /* @__PURE__ */ React10.createElement("strong", null, event.kind === "order" ? tr("\u59D4\u6258", "Order") : tr("\u6210\u4EA4", "Fill"), " \xB7 ", event.isBuy ? tr("\u4E70\u5165", "Buy") : tr("\u5356\u51FA", "Sell")), /* @__PURE__ */ React10.createElement("span", null, Number.isFinite(event.sz) ? event.sz : "\u2014", " @ ", fmtPrice(event.px), " \xB7 ", new Date(event.time).toLocaleString()), event.reason ? /* @__PURE__ */ React10.createElement("span", { title: event.reason }, tr("\u539F\u56E0", "Reason"), "\uFF1A", event.reason) : null))) : null, inspectedStudies.length ? /* @__PURE__ */ React10.createElement("div", { className: "dhl-chart-study-legend", "aria-label": tr("\u6307\u6807\u70B9\u4F4D\u6570\u503C", "Study values at crosshair") }, inspectedStudies.map(([label, value]) => /* @__PURE__ */ React10.createElement("span", { key: label, "data-study": label }, label, " ", /* @__PURE__ */ React10.createElement("b", null, value == null ? "\u2014" : Number(value.toPrecision(7)).toLocaleString(void 0, { maximumFractionDigits: 7 }))))) : null, warming ? /* @__PURE__ */ React10.createElement("div", { className: "dhl-empty", style: { height: CHART_HEIGHT } }, t("chart.loading", "K \u7EBF\u52A0\u8F7D\u4E2D\u2026")) : null), showDataWindow && inspected ? /* @__PURE__ */ React10.createElement("section", { className: "dhl-data-window", "aria-label": tr("\u56FE\u8868\u6570\u636E\u7A97\u53E3", "Chart data window"), "data-time": inspected.t }, /* @__PURE__ */ React10.createElement("header", null, /* @__PURE__ */ React10.createElement("strong", null, tr("\u6570\u636E\u7A97\u53E3", "Data window")), /* @__PURE__ */ React10.createElement("span", null, new Date(inspected.t).toLocaleString(), " \xB7 ", crosshair ? tr("\u5149\u6807 K \u7EBF", "Cursor bar") : tr("\u6700\u65B0 K \u7EBF\uFF08\u53EF\u80FD\u672A\u6536\u76D8\uFF09", "Latest bar (may be forming)"))), /* @__PURE__ */ React10.createElement("dl", null, [
|
|
19752
|
+
["Open", inspected.o],
|
|
19753
|
+
["High", inspected.h],
|
|
19754
|
+
["Low", inspected.l],
|
|
19755
|
+
["Close", inspected.c],
|
|
19756
|
+
["Volume", inspected.v],
|
|
19757
|
+
...inspectedStudies,
|
|
19758
|
+
...maComputed.flatMap((item) => {
|
|
19759
|
+
const index = candles.findIndex((bar) => bar.t === inspected.t);
|
|
19760
|
+
return [[maLabel(item.line), item.values[index]], ...item.smoothed ? [[maLabel(item.line) + " smooth", item.smoothed[index]]] : []];
|
|
19761
|
+
})
|
|
19762
|
+
].map(([label, value]) => /* @__PURE__ */ React10.createElement("div", { key: String(label) }, /* @__PURE__ */ React10.createElement("dt", null, label), /* @__PURE__ */ React10.createElement("dd", null, value == null ? "\u2014" : Number(value).toLocaleString(void 0, { maximumSignificantDigits: 10 }))))), indicators.has("obv") ? /* @__PURE__ */ React10.createElement("small", null, tr("OBV \u4EE5\u5DF2\u52A0\u8F7D\u9996\u6839\u4E3A\u96F6\u70B9\uFF1B\u8865\u53D6\u5386\u53F2\u540E\u57FA\u51C6\u4F1A\u53D8\u5316\u3002", "OBV starts at zero on the first loaded bar; loading history changes its baseline.")) : null) : null);
|
|
18864
19763
|
}
|
|
18865
19764
|
|
|
18866
19765
|
// src/client/drawing-history.ts
|
|
18867
|
-
var
|
|
19766
|
+
var React11 = __toESM(require("react"), 1);
|
|
18868
19767
|
var HISTORY_LIMIT = 100;
|
|
18869
19768
|
var GEOMETRY = ["price", "fromTime", "fromPrice", "toTime", "toPrice", "points"];
|
|
18870
|
-
var STYLE = ["color", "lineWidth", "lineStyle", "locked", "hidden"];
|
|
19769
|
+
var STYLE = ["color", "lineWidth", "lineStyle", "locked", "hidden", "shared", "alert", "rows", "valueArea"];
|
|
19770
|
+
var FLAGS = /* @__PURE__ */ new Set(["locked", "hidden", "shared", "alert"]);
|
|
18871
19771
|
function recreateRequest(mark) {
|
|
18872
19772
|
const out = { kind: mark.kind, label: mark.label, role: mark.role, coin: mark.coin, interval: mark.interval };
|
|
18873
|
-
for (const
|
|
19773
|
+
for (const key3 of [...GEOMETRY, ...STYLE]) if (mark[key3] != null) out[key3] = mark[key3];
|
|
19774
|
+
if (mark.source === "agent") out.restoreSource = "agent";
|
|
18874
19775
|
return out;
|
|
18875
19776
|
}
|
|
18876
19777
|
function remap(ops, from, to) {
|
|
@@ -18881,138 +19782,176 @@ function remap(ops, from, to) {
|
|
|
18881
19782
|
} else if (op.mark.id === from) op.mark = { ...op.mark, id: to };
|
|
18882
19783
|
}
|
|
18883
19784
|
}
|
|
19785
|
+
var beforeOf = (mark, patch) => Object.fromEntries(Object.keys(patch).map((key3) => [key3, mark[key3] ?? (FLAGS.has(key3) ? false : null)]));
|
|
18884
19786
|
function useDrawingHistory(scope) {
|
|
18885
|
-
const undoStack =
|
|
18886
|
-
const redoStack =
|
|
18887
|
-
const
|
|
19787
|
+
const undoStack = React11.useRef([]);
|
|
19788
|
+
const redoStack = React11.useRef([]);
|
|
19789
|
+
const scopeRef = React11.useRef(scope);
|
|
19790
|
+
const queue = React11.useRef(Promise.resolve());
|
|
19791
|
+
const [, setVersion] = React11.useState(0);
|
|
18888
19792
|
const bump = () => setVersion((value) => value + 1);
|
|
18889
|
-
|
|
19793
|
+
React11.useEffect(() => {
|
|
19794
|
+
scopeRef.current = scope;
|
|
18890
19795
|
undoStack.current = [];
|
|
18891
19796
|
redoStack.current = [];
|
|
18892
19797
|
bump();
|
|
18893
19798
|
}, [scope]);
|
|
18894
|
-
const
|
|
19799
|
+
const ctxNow = () => {
|
|
19800
|
+
const { activeSessionId, activeCoin, interval } = store.get();
|
|
19801
|
+
return activeSessionId ? { sessionId: activeSessionId, coin: activeCoin, interval, scope: scopeRef.current } : null;
|
|
19802
|
+
};
|
|
19803
|
+
const serial = (task, fallback) => {
|
|
19804
|
+
const ctx = ctxNow();
|
|
19805
|
+
if (!ctx) {
|
|
19806
|
+
notify(tr("\u8BF7\u5148\u9009\u62E9\u4EA4\u6613\u4F1A\u8BDD", "Choose a trading session first"));
|
|
19807
|
+
return Promise.resolve(fallback);
|
|
19808
|
+
}
|
|
19809
|
+
const run = queue.current.catch(() => {
|
|
19810
|
+
}).then(() => ctx.scope === scopeRef.current ? task(ctx) : fallback);
|
|
19811
|
+
queue.current = run;
|
|
19812
|
+
return run;
|
|
19813
|
+
};
|
|
19814
|
+
const onScreen = (ctx) => store.get().activeSessionId === ctx.sessionId && store.get().activeCoin === ctx.coin;
|
|
18895
19815
|
const fail = (what, error) => {
|
|
18896
19816
|
notify(`${what}\uFF1A${String(error?.message ?? error)}`);
|
|
18897
19817
|
void loadChartMarks();
|
|
18898
19818
|
};
|
|
18899
|
-
const apiCreate = async (request) => {
|
|
18900
|
-
const sessionId = session();
|
|
18901
|
-
if (!sessionId) {
|
|
18902
|
-
notify(tr("\u8BF7\u5148\u9009\u62E9\u4EA4\u6613\u4F1A\u8BDD", "Choose a trading session first"));
|
|
18903
|
-
return null;
|
|
18904
|
-
}
|
|
18905
|
-
const { activeCoin, interval } = store.get();
|
|
19819
|
+
const apiCreate = async (ctx, request) => {
|
|
18906
19820
|
try {
|
|
18907
|
-
const saved = await hyperCall("drawChartMark", {
|
|
18908
|
-
store.patch((current3) => ({ chartMarks: [...current3.chartMarks.filter((item) => item.id !== saved.id), saved] }));
|
|
19821
|
+
const saved = await hyperCall("drawChartMark", { interval: ctx.interval, role: "neutral", ...request, coin: request.coin ?? ctx.coin, sessionId: ctx.sessionId });
|
|
19822
|
+
if (onScreen(ctx) && saved.coin === ctx.coin) store.patch((current3) => ({ chartMarks: [...current3.chartMarks.filter((item) => item.id !== saved.id), saved] }));
|
|
18909
19823
|
return saved;
|
|
18910
19824
|
} catch (error) {
|
|
18911
19825
|
fail(tr("\u753B\u7EBF\u5931\u8D25", "Could not draw"), error);
|
|
18912
19826
|
return null;
|
|
18913
19827
|
}
|
|
18914
19828
|
};
|
|
18915
|
-
const apiDelete = async (id) => {
|
|
18916
|
-
|
|
18917
|
-
if (!sessionId) return;
|
|
18918
|
-
store.patch((current3) => ({ chartMarks: current3.chartMarks.filter((item) => item.id !== id) }));
|
|
19829
|
+
const apiDelete = async (ctx, id) => {
|
|
19830
|
+
if (onScreen(ctx)) store.patch((current3) => ({ chartMarks: current3.chartMarks.filter((item) => item.id !== id) }));
|
|
18919
19831
|
try {
|
|
18920
|
-
await hyperCall("eraseChartMark", { sessionId, id });
|
|
19832
|
+
await hyperCall("eraseChartMark", { sessionId: ctx.sessionId, id });
|
|
19833
|
+
return true;
|
|
18921
19834
|
} catch (error) {
|
|
18922
19835
|
fail(tr("\u5220\u9664\u753B\u7EBF\u5931\u8D25", "Could not remove drawing"), error);
|
|
19836
|
+
return false;
|
|
18923
19837
|
}
|
|
18924
19838
|
};
|
|
18925
|
-
const apiEdit = async (id, patch) => {
|
|
18926
|
-
|
|
18927
|
-
if (!sessionId) return;
|
|
18928
|
-
store.patch((current3) => ({ chartMarks: current3.chartMarks.map((item) => {
|
|
19839
|
+
const apiEdit = async (ctx, id, patch) => {
|
|
19840
|
+
if (onScreen(ctx)) store.patch((current3) => ({ chartMarks: current3.chartMarks.map((item) => {
|
|
18929
19841
|
if (item.id !== id) return item;
|
|
18930
19842
|
const next = { ...item, ...patch };
|
|
18931
|
-
for (const [
|
|
19843
|
+
for (const [key3, value] of Object.entries(patch)) if (value === null) delete next[key3];
|
|
18932
19844
|
return next;
|
|
18933
19845
|
}) }));
|
|
18934
19846
|
try {
|
|
18935
|
-
await hyperCall("updateChartMark", { sessionId, id, patch });
|
|
19847
|
+
await hyperCall("updateChartMark", { sessionId: ctx.sessionId, id, patch });
|
|
19848
|
+
return true;
|
|
18936
19849
|
} catch (error) {
|
|
18937
19850
|
fail(tr("\u4FEE\u6539\u753B\u7EBF\u5931\u8D25", "Could not edit drawing"), error);
|
|
19851
|
+
return false;
|
|
18938
19852
|
}
|
|
18939
19853
|
};
|
|
18940
|
-
const record = (op) => {
|
|
19854
|
+
const record = (op, ctx) => {
|
|
19855
|
+
if (ctx.scope !== scopeRef.current) return;
|
|
18941
19856
|
undoStack.current.push(op);
|
|
18942
19857
|
if (undoStack.current.length > HISTORY_LIMIT) undoStack.current.shift();
|
|
18943
19858
|
redoStack.current = [];
|
|
18944
19859
|
bump();
|
|
18945
19860
|
};
|
|
18946
19861
|
const current2 = (id) => store.get().chartMarks.find((item) => item.id === id);
|
|
18947
|
-
const apply2 = async (op, forward) => {
|
|
19862
|
+
const apply2 = async (ctx, op, forward) => {
|
|
18948
19863
|
if (op.type === "batch") {
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
18952
|
-
if (op.type === "edit") {
|
|
18953
|
-
await apiEdit(op.id, forward ? op.after : op.before);
|
|
18954
|
-
return;
|
|
19864
|
+
let ok = true;
|
|
19865
|
+
for (const inner of forward ? op.ops : [...op.ops].reverse()) ok = await apply2(ctx, inner, forward) && ok;
|
|
19866
|
+
return ok;
|
|
18955
19867
|
}
|
|
19868
|
+
if (op.type === "edit") return apiEdit(ctx, op.id, forward ? op.after : op.before);
|
|
18956
19869
|
const shouldExist = op.type === "create" === forward;
|
|
18957
19870
|
if (shouldExist) {
|
|
18958
|
-
const saved = await apiCreate(recreateRequest(op.mark));
|
|
18959
|
-
if (saved)
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
|
|
18965
|
-
}
|
|
19871
|
+
const saved = await apiCreate(ctx, recreateRequest(op.mark));
|
|
19872
|
+
if (!saved) return false;
|
|
19873
|
+
const old = op.mark.id;
|
|
19874
|
+
remap(undoStack.current, old, saved.id);
|
|
19875
|
+
remap(redoStack.current, old, saved.id);
|
|
19876
|
+
op.mark = { ...op.mark, id: saved.id };
|
|
19877
|
+
return true;
|
|
19878
|
+
}
|
|
19879
|
+
return apiDelete(ctx, op.mark.id);
|
|
18966
19880
|
};
|
|
18967
19881
|
return {
|
|
18968
|
-
|
|
18969
|
-
const saved = await apiCreate(request);
|
|
18970
|
-
if (saved) record({ type: "create", mark: saved });
|
|
19882
|
+
create: (request) => serial(async (ctx) => {
|
|
19883
|
+
const saved = await apiCreate(ctx, request);
|
|
19884
|
+
if (saved) record({ type: "create", mark: saved }, ctx);
|
|
18971
19885
|
return saved;
|
|
18972
|
-
},
|
|
18973
|
-
|
|
19886
|
+
}, null),
|
|
19887
|
+
edit: (id, patch) => serial(async (ctx) => {
|
|
18974
19888
|
const mark = current2(id);
|
|
18975
19889
|
if (!mark) return;
|
|
18976
|
-
const before =
|
|
18977
|
-
record({ type: "edit", id, before, after: patch });
|
|
18978
|
-
|
|
18979
|
-
|
|
18980
|
-
async remove(id) {
|
|
19890
|
+
const before = beforeOf(mark, patch);
|
|
19891
|
+
if (await apiEdit(ctx, id, patch)) record({ type: "edit", id, before, after: patch }, ctx);
|
|
19892
|
+
}, void 0),
|
|
19893
|
+
remove: (id) => serial(async (ctx) => {
|
|
18981
19894
|
const mark = current2(id);
|
|
18982
19895
|
if (!mark) return;
|
|
18983
|
-
record({ type: "delete", mark });
|
|
18984
|
-
|
|
18985
|
-
|
|
18986
|
-
|
|
18987
|
-
const
|
|
18988
|
-
if (
|
|
18989
|
-
record({ type: "batch", ops:
|
|
18990
|
-
|
|
18991
|
-
|
|
18992
|
-
|
|
19896
|
+
if (await apiDelete(ctx, id)) record({ type: "delete", mark }, ctx);
|
|
19897
|
+
}, void 0),
|
|
19898
|
+
// Batches record only the members the host accepted, so an undo never re-creates something that still exists.
|
|
19899
|
+
removeMany: (ids) => serial(async (ctx) => {
|
|
19900
|
+
const done = [];
|
|
19901
|
+
for (const mark of ids.map(current2).filter((item) => Boolean(item))) if (await apiDelete(ctx, mark.id)) done.push({ type: "delete", mark });
|
|
19902
|
+
if (done.length) record({ type: "batch", ops: done }, ctx);
|
|
19903
|
+
}, void 0),
|
|
19904
|
+
editMany: (ids, patch) => serial(async (ctx) => {
|
|
19905
|
+
const done = [];
|
|
19906
|
+
for (const mark of ids.map(current2).filter((item) => Boolean(item))) {
|
|
19907
|
+
const before = beforeOf(mark, patch);
|
|
19908
|
+
if (await apiEdit(ctx, mark.id, patch)) done.push({ type: "edit", id: mark.id, before, after: patch });
|
|
19909
|
+
}
|
|
19910
|
+
if (done.length) record({ type: "batch", ops: done }, ctx);
|
|
19911
|
+
}, void 0),
|
|
19912
|
+
editEach: (patches) => serial(async (ctx) => {
|
|
19913
|
+
const done = [];
|
|
19914
|
+
for (const [id, patch] of patches) {
|
|
19915
|
+
const mark = current2(id);
|
|
19916
|
+
if (!mark) continue;
|
|
19917
|
+
const before = beforeOf(mark, patch);
|
|
19918
|
+
if (await apiEdit(ctx, id, patch)) done.push({ type: "edit", id, before, after: patch });
|
|
19919
|
+
}
|
|
19920
|
+
if (done.length) record({ type: "batch", ops: done }, ctx);
|
|
19921
|
+
}, void 0),
|
|
19922
|
+
// A failed undo/redo goes back where it was, so the stacks keep describing the host.
|
|
19923
|
+
undo: () => serial(async (ctx) => {
|
|
18993
19924
|
const op = undoStack.current.pop();
|
|
18994
19925
|
if (!op) return;
|
|
18995
19926
|
redoStack.current.push(op);
|
|
18996
19927
|
bump();
|
|
18997
|
-
await apply2(op, false)
|
|
18998
|
-
|
|
18999
|
-
|
|
19928
|
+
if (!await apply2(ctx, op, false)) {
|
|
19929
|
+
redoStack.current.pop();
|
|
19930
|
+
undoStack.current.push(op);
|
|
19931
|
+
bump();
|
|
19932
|
+
}
|
|
19933
|
+
}, void 0),
|
|
19934
|
+
redo: () => serial(async (ctx) => {
|
|
19000
19935
|
const op = redoStack.current.pop();
|
|
19001
19936
|
if (!op) return;
|
|
19002
19937
|
undoStack.current.push(op);
|
|
19003
19938
|
bump();
|
|
19004
|
-
await apply2(op, true)
|
|
19005
|
-
|
|
19939
|
+
if (!await apply2(ctx, op, true)) {
|
|
19940
|
+
undoStack.current.pop();
|
|
19941
|
+
redoStack.current.push(op);
|
|
19942
|
+
bump();
|
|
19943
|
+
}
|
|
19944
|
+
}, void 0),
|
|
19006
19945
|
canUndo: undoStack.current.length > 0,
|
|
19007
19946
|
canRedo: redoStack.current.length > 0
|
|
19008
19947
|
};
|
|
19009
19948
|
}
|
|
19010
19949
|
|
|
19011
19950
|
// src/client/components/drawing-list.tsx
|
|
19012
|
-
var
|
|
19951
|
+
var React12 = __toESM(require("react"), 1);
|
|
19013
19952
|
var OPEN_KEY = "dsh-hyperliquid.draw.listOpen";
|
|
19014
19953
|
function DrawingList({ marks, locked, onSelect, onEdit, onRemove }) {
|
|
19015
|
-
const [open, setOpen] =
|
|
19954
|
+
const [open, setOpen] = React12.useState(() => {
|
|
19016
19955
|
try {
|
|
19017
19956
|
return localStorage.getItem(OPEN_KEY) !== "0";
|
|
19018
19957
|
} catch {
|
|
@@ -19027,31 +19966,31 @@ function DrawingList({ marks, locked, onSelect, onEdit, onRemove }) {
|
|
|
19027
19966
|
return !value;
|
|
19028
19967
|
});
|
|
19029
19968
|
const hidden = marks.filter((mark) => mark.hidden).length;
|
|
19030
|
-
return /* @__PURE__ */
|
|
19969
|
+
return /* @__PURE__ */ React12.createElement("section", { className: "dhl-drawing-list", "aria-label": tr("\u753B\u7EBF\u5217\u8868", "Drawings") }, /* @__PURE__ */ React12.createElement("button", { type: "button", className: "dhl-drawing-list-head", "aria-expanded": open, onClick: toggle }, /* @__PURE__ */ React12.createElement("span", null, tr("\u753B\u7EBF", "Drawings"), " ", marks.length, hidden ? tr(` \xB7 \u9690\u85CF ${hidden}`, ` \xB7 ${hidden} hidden`) : ""), /* @__PURE__ */ React12.createElement(UiIcon, { name: open ? "chevronDown" : "chevronRight", size: 14 })), open ? /* @__PURE__ */ React12.createElement("ul", null, marks.map((mark) => {
|
|
19031
19970
|
const tool = toolOf(mark.kind);
|
|
19032
|
-
return /* @__PURE__ */
|
|
19971
|
+
return /* @__PURE__ */ React12.createElement("li", { key: mark.id, className: mark.hidden ? "is-hidden" : "" }, /* @__PURE__ */ React12.createElement("button", { type: "button", className: "dhl-drawing-list-name", title: tr("\u5728\u56FE\u4E0A\u5B9A\u4F4D\u5E76\u9009\u4E2D", "Find and select on the chart"), onClick: () => onSelect(mark.id), disabled: mark.hidden }, /* @__PURE__ */ React12.createElement("span", { style: { color: mark.color } }, /* @__PURE__ */ React12.createElement(ToolIcon, { kind: mark.kind, size: 14 })), /* @__PURE__ */ React12.createElement("b", null, mark.label), /* @__PURE__ */ React12.createElement("small", null, mark.alert ? /* @__PURE__ */ React12.createElement("span", { className: "dhl-list-flag", title: tr("\u4EF7\u683C\u63D0\u9192\u5DF2\u5F00\u542F", "Price alert armed") }, /* @__PURE__ */ React12.createElement(UiIcon, { name: "bell", size: 12 })) : null, mark.shared ? /* @__PURE__ */ React12.createElement("span", { className: "dhl-list-flag", title: tr("\u5DF2\u540C\u6B65\u5230\u6240\u6709\u4F1A\u8BDD", "Synced to all sessions") }, /* @__PURE__ */ React12.createElement(UiIcon, { name: "sync", size: 12 })) : null, tr(tool.zh, tool.en), " \xB7 ", mark.interval, mark.source === "agent" ? " \xB7 agent" : "")), /* @__PURE__ */ React12.createElement("button", { type: "button", "aria-pressed": !mark.hidden, title: mark.hidden ? tr("\u663E\u793A", "Show") : tr("\u9690\u85CF", "Hide"), "aria-label": `${mark.hidden ? tr("\u663E\u793A", "Show") : tr("\u9690\u85CF", "Hide")} ${mark.label}`, onClick: () => onEdit(mark.id, { hidden: !mark.hidden }) }, /* @__PURE__ */ React12.createElement(UiIcon, { name: mark.hidden ? "eyeOff" : "eye", size: 14 })), /* @__PURE__ */ React12.createElement("button", { type: "button", className: mark.locked ? "on" : "", "aria-pressed": Boolean(mark.locked), disabled: locked, title: mark.locked ? tr("\u89E3\u9501", "Unlock") : tr("\u9501\u5B9A", "Lock"), "aria-label": `${mark.locked ? tr("\u89E3\u9501", "Unlock") : tr("\u9501\u5B9A", "Lock")} ${mark.label}`, onClick: () => onEdit(mark.id, { locked: !mark.locked }) }, /* @__PURE__ */ React12.createElement(UiIcon, { name: mark.locked ? "lock" : "unlock", size: 14 })), /* @__PURE__ */ React12.createElement("button", { type: "button", disabled: locked || mark.locked, title: tr("\u5220\u9664", "Delete"), "aria-label": `${tr("\u5220\u9664\u753B\u7EBF", "Remove line")} ${mark.label}`, onClick: () => onRemove(mark.id) }, /* @__PURE__ */ React12.createElement(UiIcon, { name: "trash", size: 14 })));
|
|
19033
19972
|
})) : null);
|
|
19034
19973
|
}
|
|
19035
19974
|
|
|
19036
19975
|
// src/client/components/sticker-picker.tsx
|
|
19037
|
-
var
|
|
19976
|
+
var React13 = __toESM(require("react"), 1);
|
|
19038
19977
|
var TABS = [["icons", "\u56FE\u6807", "Icons"], ["trading", "\u4EA4\u6613", "Trading"], ["faces", "\u8868\u60C5", "Faces"], ["symbols", "\u7B26\u53F7", "Symbols"]];
|
|
19039
19978
|
function StickerPicker({ onPick }) {
|
|
19040
|
-
const [tab, setTab] =
|
|
19041
|
-
const [custom, setCustom] =
|
|
19979
|
+
const [tab, setTab] = React13.useState("icons");
|
|
19980
|
+
const [custom, setCustom] = React13.useState("");
|
|
19042
19981
|
const value = custom.trim();
|
|
19043
19982
|
const valid = isSingleEmoji(value);
|
|
19044
|
-
return /* @__PURE__ */
|
|
19983
|
+
return /* @__PURE__ */ React13.createElement("div", { className: "dhl-sticker-picker" }, /* @__PURE__ */ React13.createElement("form", { className: "dhl-sticker-custom", onSubmit: (event) => {
|
|
19045
19984
|
event.preventDefault();
|
|
19046
19985
|
if (valid) onPick(`emoji:${value}`);
|
|
19047
|
-
} }, /* @__PURE__ */
|
|
19986
|
+
} }, /* @__PURE__ */ React13.createElement("input", { value: custom, onChange: (event) => setCustom(event.target.value), placeholder: tr("\u8F93\u5165\u6216\u7C98\u8D34\u4EFB\u610F emoji\uFF0C\u4F8B\u5982 \u{1F984}", "Type or paste any emoji, e.g. \u{1F984}"), "aria-label": tr("\u81EA\u5B9A\u4E49 emoji \u8D34\u7EB8", "Custom emoji sticker"), maxLength: 16 }), /* @__PURE__ */ React13.createElement("button", { type: "submit", disabled: !valid }, tr("\u653E\u7F6E", "Place"))), /* @__PURE__ */ React13.createElement("div", { className: "dhl-sticker-tabs", role: "tablist" }, TABS.map(([id, zh, en]) => /* @__PURE__ */ React13.createElement("button", { type: "button", role: "tab", key: id, "aria-selected": tab === id, className: tab === id ? "on" : "", onClick: () => setTab(id) }, tr(zh, en)))), tab === "icons" ? /* @__PURE__ */ React13.createElement("div", { className: "dhl-icon-grid", role: "group", "aria-label": tr("\u56FE\u6807", "Icons") }, ICON_LIST.map((name) => /* @__PURE__ */ React13.createElement("button", { type: "button", key: name, "aria-label": tr(ICONS[name][0], ICONS[name][1]), title: tr(ICONS[name][0], ICONS[name][1]), onClick: () => onPick(name) }, /* @__PURE__ */ React13.createElement("svg", { viewBox: "0 0 24 24", width: "22", height: "22", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true" }, /* @__PURE__ */ React13.createElement("path", { d: ICONS[name][2] })), /* @__PURE__ */ React13.createElement("span", null, tr(ICONS[name][0], ICONS[name][1]))))) : /* @__PURE__ */ React13.createElement("div", { className: "dhl-sticker-grid", role: "group", "aria-label": tr("\u8D34\u7EB8", "Stickers") }, STICKERS[tab].map((emoji) => /* @__PURE__ */ React13.createElement("button", { type: "button", key: emoji, "aria-label": `${tr("\u8D34\u7EB8", "Sticker")} ${emoji}`, onClick: () => onPick(`emoji:${emoji}`) }, emoji))));
|
|
19048
19987
|
}
|
|
19049
19988
|
|
|
19050
19989
|
// src/client/components/desk-column.tsx
|
|
19051
|
-
var
|
|
19990
|
+
var import_dsh_client_ui_primitives6 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
19052
19991
|
|
|
19053
19992
|
// src/client/components/ma-editor.tsx
|
|
19054
|
-
var
|
|
19993
|
+
var React14 = __toESM(require("react"), 1);
|
|
19055
19994
|
var KIND_NAMES = {
|
|
19056
19995
|
sma: ["SMA", "SMA"],
|
|
19057
19996
|
ema: ["EMA", "EMA"],
|
|
@@ -19065,11 +20004,11 @@ var PRESETS = [
|
|
|
19065
20004
|
{ kind: "ema", period: 200 }
|
|
19066
20005
|
];
|
|
19067
20006
|
function MaEditor({ lines, onChange }) {
|
|
19068
|
-
const [kind, setKind] =
|
|
19069
|
-
const [period, setPeriod] =
|
|
19070
|
-
const [source, setSource] =
|
|
19071
|
-
const [smoothKind, setSmoothKind] =
|
|
19072
|
-
const [smoothPeriod, setSmoothPeriod] =
|
|
20007
|
+
const [kind, setKind] = React14.useState("smma");
|
|
20008
|
+
const [period, setPeriod] = React14.useState("21");
|
|
20009
|
+
const [source, setSource] = React14.useState("close");
|
|
20010
|
+
const [smoothKind, setSmoothKind] = React14.useState("");
|
|
20011
|
+
const [smoothPeriod, setSmoothPeriod] = React14.useState("9");
|
|
19073
20012
|
const add2 = (input) => {
|
|
19074
20013
|
let line;
|
|
19075
20014
|
try {
|
|
@@ -19085,17 +20024,17 @@ function MaEditor({ lines, onChange }) {
|
|
|
19085
20024
|
}
|
|
19086
20025
|
onChange([...rest, line]);
|
|
19087
20026
|
};
|
|
19088
|
-
return /* @__PURE__ */
|
|
20027
|
+
return /* @__PURE__ */ React14.createElement("div", { className: "dhl-ma-editor" }, /* @__PURE__ */ React14.createElement("strong", null, tr("\u5747\u7EBF", "Moving averages")), lines.length ? /* @__PURE__ */ React14.createElement("ul", { className: "dhl-ma-list" }, lines.map((line, index) => /* @__PURE__ */ React14.createElement("li", { key: line.id }, /* @__PURE__ */ React14.createElement("i", { style: { background: maColor(line, index) }, "aria-hidden": "true" }), /* @__PURE__ */ React14.createElement("span", null, maLabel(line)), /* @__PURE__ */ React14.createElement("button", { type: "button", "aria-label": `${tr("\u79FB\u9664", "Remove")} ${maLabel(line)}`, onClick: () => onChange(lines.filter((item) => item.id !== line.id)) }, /* @__PURE__ */ React14.createElement(UiIcon, { name: "close", size: 12 }))))) : null, /* @__PURE__ */ React14.createElement("div", { className: "dhl-ma-presets" }, PRESETS.map((preset) => {
|
|
19089
20028
|
const id = `${preset.kind}${preset.period}`;
|
|
19090
|
-
return /* @__PURE__ */
|
|
19091
|
-
})), /* @__PURE__ */
|
|
20029
|
+
return /* @__PURE__ */ React14.createElement("button", { type: "button", key: id, disabled: lines.some((line) => line.id === id), onClick: () => add2(preset) }, `${preset.kind.toUpperCase()} ${preset.period}`);
|
|
20030
|
+
})), /* @__PURE__ */ React14.createElement("div", { className: "dhl-ma-form" }, /* @__PURE__ */ React14.createElement(Select, { "aria-label": tr("\u5747\u7EBF\u7C7B\u578B", "MA type"), value: kind, onChange: (event) => setKind(event.target.value) }, MA_KINDS.map((item) => /* @__PURE__ */ React14.createElement("option", { key: item, value: item }, tr(KIND_NAMES[item][0], KIND_NAMES[item][1])))), /* @__PURE__ */ React14.createElement("input", { "aria-label": tr("\u5468\u671F", "Length"), type: "number", min: 1, max: 500, value: period, onChange: (event) => setPeriod(event.target.value) }), /* @__PURE__ */ React14.createElement(Select, { "aria-label": tr("\u4EF7\u683C\u6E90", "Source"), value: source, onChange: (event) => setSource(event.target.value) }, MA_SOURCES.map((item) => /* @__PURE__ */ React14.createElement("option", { key: item, value: item }, item)))), /* @__PURE__ */ React14.createElement("div", { className: "dhl-ma-form" }, /* @__PURE__ */ React14.createElement(Select, { "aria-label": tr("\u5E73\u6ED1", "Smoothing"), value: smoothKind, onChange: (event) => setSmoothKind(event.target.value) }, /* @__PURE__ */ React14.createElement("option", { value: "" }, tr("\u4E0D\u5E73\u6ED1", "No smoothing")), MA_KINDS.map((item) => /* @__PURE__ */ React14.createElement("option", { key: item, value: item }, tr("\u5E73\u6ED1\uFF1A", "Smooth: ") + item.toUpperCase()))), /* @__PURE__ */ React14.createElement("input", { "aria-label": tr("\u5E73\u6ED1\u5468\u671F", "Smoothing length"), type: "number", min: 1, max: 500, value: smoothPeriod, disabled: !smoothKind, onChange: (event) => setSmoothPeriod(event.target.value) }), /* @__PURE__ */ React14.createElement("button", { type: "button", className: "dhl-ma-add", onClick: () => add2({ kind, period: Number(period), source, smoothing: smoothKind ? { kind: smoothKind, period: Number(smoothPeriod) } : null }) }, tr("\u6DFB\u52A0", "Add"))));
|
|
19092
20031
|
}
|
|
19093
20032
|
|
|
19094
20033
|
// src/client/components/market-tape.tsx
|
|
19095
|
-
var
|
|
20034
|
+
var React17 = __toESM(require("react"), 1);
|
|
19096
20035
|
|
|
19097
20036
|
// src/client/components/book-card.tsx
|
|
19098
|
-
var
|
|
20037
|
+
var React15 = __toESM(require("react"), 1);
|
|
19099
20038
|
var GROUPINGS = [
|
|
19100
20039
|
["full", null],
|
|
19101
20040
|
["5", 1],
|
|
@@ -19146,7 +20085,7 @@ function BookCard({ snapshot }) {
|
|
|
19146
20085
|
saveSessionWorkspace({ bookGrouping: next });
|
|
19147
20086
|
void loadSnapshot();
|
|
19148
20087
|
};
|
|
19149
|
-
const row = (level, side2) => /* @__PURE__ */
|
|
20088
|
+
const row = (level, side2) => /* @__PURE__ */ React15.createElement("div", { className: "dhl-lvl", key: `${side2}:${level.px}` }, /* @__PURE__ */ React15.createElement("div", { className: "bar " + side2, style: { width: `${level.total / maxTotal * 100}%` } }), /* @__PURE__ */ React15.createElement(
|
|
19150
20089
|
"button",
|
|
19151
20090
|
{
|
|
19152
20091
|
type: "button",
|
|
@@ -19156,35 +20095,181 @@ function BookCard({ snapshot }) {
|
|
|
19156
20095
|
onClick: () => store.patch({ ticketDraft: { coin, isBuy: side2 === "bid", px: level.px, at: Date.now() } })
|
|
19157
20096
|
},
|
|
19158
20097
|
exchangePrice(level.px)
|
|
19159
|
-
), /* @__PURE__ */
|
|
19160
|
-
return /* @__PURE__ */
|
|
20098
|
+
), /* @__PURE__ */ React15.createElement("span", null, fmt(level.sz, 4)), /* @__PURE__ */ React15.createElement("span", { style: { color: "var(--dsw-alias-label-secondary)" } }, fmt(level.total, 3)));
|
|
20099
|
+
return /* @__PURE__ */ React15.createElement("div", { className: "dhl-card" }, /* @__PURE__ */ React15.createElement("h4", null, t("book.title", "\u76D8\u53E3")), /* @__PURE__ */ React15.createElement("div", { className: "dhl-book-options" }, /* @__PURE__ */ React15.createElement("label", null, tr("\u4EF7\u683C\u6863\u4F4D", "Price grouping"), " ", /* @__PURE__ */ React15.createElement(Select, { "aria-label": tr("\u76D8\u53E3\u4EF7\u683C\u6863\u4F4D", "Order book price grouping"), value: grouping, onChange: (event) => chooseGrouping(event.target.value) }, GROUPINGS.map(([key3, factor]) => /* @__PURE__ */ React15.createElement("option", { key: key3, value: key3 }, factor == null ? tr("\u539F\u59CB", "Full") : stepLabel(baseStep * factor))))), /* @__PURE__ */ React15.createElement("span", null, coin)), asks.length === 0 && bids.length === 0 ? /* @__PURE__ */ React15.createElement("div", { className: "dhl-book-wait" }, grouping === "full" ? t("book.waiting", "\u7B49\u5F85\u4E0A\u6E38\u884C\u60C5\u2026") : tr("\u6B63\u5728\u8BFB\u53D6\u4EA4\u6613\u6240\u805A\u5408\u76D8\u53E3\u2026", "Loading grouped order book\u2026")) : /* @__PURE__ */ React15.createElement(React15.Fragment, null, /* @__PURE__ */ React15.createElement("div", { className: "dhl-lvl dhl-bookhead" }, /* @__PURE__ */ React15.createElement("span", null, t("book.price", "\u4EF7\u683C")), /* @__PURE__ */ React15.createElement("span", null, t("book.size", "\u6570\u91CF")), /* @__PURE__ */ React15.createElement("span", null, t("book.total", "\u7D2F\u8BA1"))), asks.slice().reverse().map((level) => row(level, "ask")), /* @__PURE__ */ React15.createElement("div", { className: "dhl-spread-row" }, /* @__PURE__ */ React15.createElement("span", null, t("book.spread", "\u4EF7\u5DEE")), /* @__PURE__ */ React15.createElement("strong", null, spread != null ? fmtPrice(spread) : "\u2014"), /* @__PURE__ */ React15.createElement("span", null, spread != null && mid ? `${(spread / mid * 100).toFixed(3)}%` : "\u2014")), bids.map((level) => row(level, "bid"))));
|
|
19161
20100
|
}
|
|
19162
20101
|
|
|
19163
20102
|
// src/client/components/trades-card.tsx
|
|
19164
|
-
var
|
|
20103
|
+
var React16 = __toESM(require("react"), 1);
|
|
19165
20104
|
function TradesCard({ snapshot }) {
|
|
19166
20105
|
const trades = (Array.isArray(snapshot?.trades) ? snapshot.trades : []).slice(0, 24);
|
|
19167
|
-
return /* @__PURE__ */
|
|
20106
|
+
return /* @__PURE__ */ React16.createElement("div", { className: "dhl-card" }, /* @__PURE__ */ React16.createElement("h4", null, t("trades.title", "\u6700\u8FD1\u6210\u4EA4")), /* @__PURE__ */ React16.createElement("div", { className: "dhl-lvl dhl-bookhead" }, /* @__PURE__ */ React16.createElement("span", null, t("book.price", "\u4EF7\u683C")), /* @__PURE__ */ React16.createElement("span", null, t("book.size", "\u6570\u91CF")), /* @__PURE__ */ React16.createElement("span", null, tr("\u65F6\u95F4", "Time"))), trades.length === 0 ? /* @__PURE__ */ React16.createElement("div", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: 11 } }, "\u2014") : trades.map((tr2, i) => /* @__PURE__ */ React16.createElement("div", { className: "dhl-lvl", key: i, style: { position: "relative" } }, /* @__PURE__ */ React16.createElement("span", { style: { color: tr2.side === "B" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)" } }, fmtPrice(tr2.px)), /* @__PURE__ */ React16.createElement("span", { style: { color: "var(--dsw-alias-label-secondary)" } }, fmt(tr2.sz, 4)), /* @__PURE__ */ React16.createElement("span", { style: { color: "var(--dsw-alias-label-secondary)" } }, Number.isFinite(Number(tr2.time)) ? new Date(Number(tr2.time)).toLocaleTimeString(tr("zh-CN", "en-US"), { hour12: false }) : "\u2014"))));
|
|
19168
20107
|
}
|
|
19169
20108
|
|
|
19170
20109
|
// src/client/components/market-tape.tsx
|
|
19171
20110
|
function MarketTape({ snapshot }) {
|
|
19172
20111
|
const s = useStore();
|
|
19173
20112
|
const tab = s.workspaceSessionId === s.activeSessionId ? s.workspace?.tapeTab ?? "book" : "book";
|
|
19174
|
-
return /* @__PURE__ */
|
|
20113
|
+
return /* @__PURE__ */ React17.createElement("aside", { className: "dhl-tape", "aria-label": tr("\u76D8\u53E3\u4E0E\u6210\u4EA4", "Order book and trades") }, /* @__PURE__ */ React17.createElement("div", { className: "dhl-tape-tabs", role: "tablist", "aria-label": tr("\u5E02\u573A\u6570\u636E", "Market data") }, /* @__PURE__ */ React17.createElement("button", { type: "button", role: "tab", "aria-selected": tab === "book", className: tab === "book" ? "on" : "", onClick: () => saveSessionWorkspace({ tapeTab: "book" }) }, tr("\u76D8\u53E3", "Order book")), /* @__PURE__ */ React17.createElement("button", { type: "button", role: "tab", "aria-selected": tab === "trades", className: tab === "trades" ? "on" : "", onClick: () => saveSessionWorkspace({ tapeTab: "trades" }) }, tr("\u6210\u4EA4", "Trades"))), /* @__PURE__ */ React17.createElement("div", { className: "dhl-tape-content", role: "tabpanel" }, tab === "book" ? /* @__PURE__ */ React17.createElement(BookCard, { snapshot }) : /* @__PURE__ */ React17.createElement(TradesCard, { snapshot })));
|
|
19175
20114
|
}
|
|
19176
20115
|
|
|
19177
20116
|
// src/client/components/bottom-table.tsx
|
|
19178
|
-
var
|
|
20117
|
+
var React19 = __toESM(require("react"), 1);
|
|
20118
|
+
|
|
20119
|
+
// src/client/components/trade-review.tsx
|
|
20120
|
+
var React18 = __toESM(require("react"), 1);
|
|
20121
|
+
|
|
20122
|
+
// lib/review.js
|
|
20123
|
+
var numeric = (value) => value == null || value === "" || !Number.isFinite(Number(value)) ? null : Number(value);
|
|
20124
|
+
var key2 = (row) => row?.oid == null || !row.coin ? null : `${row.coin}:${String(row.oid)}`;
|
|
20125
|
+
var core = (coin) => typeof coin === "string" && coin.trim().length > 0 && !coin.startsWith("#") && !coin.includes(":") && !coin.startsWith("@") && !coin.includes("/");
|
|
20126
|
+
function reviewLedger(payload, coin = "") {
|
|
20127
|
+
const orders = (payload.orders ?? []).filter((row) => !coin || row.coin === coin);
|
|
20128
|
+
const allFills = (payload.fills ?? []).filter((row) => !coin || row.coin === coin);
|
|
20129
|
+
const byOrder = new Map(orders.filter((row) => key2(row)).map((row) => [key2(row), row]));
|
|
20130
|
+
const seen = /* @__PURE__ */ new Set(), records = [];
|
|
20131
|
+
let duplicates = 0;
|
|
20132
|
+
for (const [index, fill] of allFills.entries()) {
|
|
20133
|
+
const id = fill.tid != null ? `${fill.coin}:${fill.tid}` : payload.mode === "paper" && fill.oid ? `${fill.coin}:${fill.oid}` : `unidentified:${index}`;
|
|
20134
|
+
if (seen.has(id)) {
|
|
20135
|
+
duplicates++;
|
|
20136
|
+
continue;
|
|
20137
|
+
}
|
|
20138
|
+
seen.add(id);
|
|
20139
|
+
const order = byOrder.get(key2(fill)), pnl = numeric(fill.closedPnl ?? fill.realized), fee = numeric(fill.fee);
|
|
20140
|
+
const feeToken = String(fill.feeToken ?? "").trim();
|
|
20141
|
+
const px = numeric(fill.px), size3 = numeric(fill.sz), buy = typeof fill.isBuy === "boolean" ? fill.isBuy : fill.side === "B" ? true : fill.side === "A" ? false : null;
|
|
20142
|
+
const reference = numeric(fill.referencePx ?? order?.referencePx);
|
|
20143
|
+
const reason = fill.reason || order?.reason || "";
|
|
20144
|
+
const closed = numeric(fill.closedSize) ?? (buy != null && numeric(fill.startPosition) != null && Math.sign(Number(fill.startPosition)) !== (buy ? 1 : -1) ? Math.min(Math.abs(Number(fill.startPosition)), size3 ?? 0) : /^Close/.test(fill.dir ?? "") ? size3 : null);
|
|
20145
|
+
const eligible = core(fill.coin) && feeToken === "USDC";
|
|
20146
|
+
const net = eligible && pnl != null && fee != null ? pnl - fee : null;
|
|
20147
|
+
const flags2 = [];
|
|
20148
|
+
if (!order) flags2.push("unmatched-order");
|
|
20149
|
+
if (px == null || px <= 0) flags2.push("missing-price");
|
|
20150
|
+
if (size3 == null || size3 <= 0) flags2.push("missing-size");
|
|
20151
|
+
if (numeric(fill.time) == null) flags2.push("missing-time");
|
|
20152
|
+
if (buy == null) flags2.push("missing-side");
|
|
20153
|
+
if (!reason || /^(session:|tool$|chat$|order$)/.test(reason)) flags2.push("missing-reason");
|
|
20154
|
+
if (fee == null || !feeToken) flags2.push("missing-fee");
|
|
20155
|
+
if (pnl == null) flags2.push("missing-pnl");
|
|
20156
|
+
if (fill.tid == null && payload.mode !== "paper") flags2.push("missing-trade-id");
|
|
20157
|
+
records.push({
|
|
20158
|
+
id,
|
|
20159
|
+
coin: fill.coin,
|
|
20160
|
+
oid: fill.oid ?? null,
|
|
20161
|
+
time: numeric(fill.time),
|
|
20162
|
+
px,
|
|
20163
|
+
size: size3,
|
|
20164
|
+
isBuy: buy,
|
|
20165
|
+
gross: pnl,
|
|
20166
|
+
fee,
|
|
20167
|
+
feeToken,
|
|
20168
|
+
net,
|
|
20169
|
+
reference,
|
|
20170
|
+
slippageBps: reference && reference > 0 && px && buy != null ? (buy ? px - reference : reference - px) / reference * 1e4 : null,
|
|
20171
|
+
closedSize: closed,
|
|
20172
|
+
reason: flags2.includes("missing-reason") ? "" : reason,
|
|
20173
|
+
sessionId: fill.sessionId ?? order?.sessionId ?? null,
|
|
20174
|
+
flags: flags2,
|
|
20175
|
+
source: payload.mode
|
|
20176
|
+
});
|
|
20177
|
+
}
|
|
20178
|
+
const reconciled = orders.map((order) => {
|
|
20179
|
+
const fills = records.filter((row) => key2(row) && key2(row) === key2(order));
|
|
20180
|
+
const filled = fills.reduce((sum, row) => sum + (row.size ?? 0), 0);
|
|
20181
|
+
const expected = numeric(order.origSz ?? order.sz), tolerance = Math.max(1e-8, (expected ?? 0) * 1e-6);
|
|
20182
|
+
const status = fills.some((row) => row.flags.includes("missing-size")) ? "missing-size" : /filled/i.test(order.status ?? "") && expected != null && Math.abs(filled - expected) > tolerance ? "size-gap" : expected != null && filled > expected + tolerance ? "excess-fill" : fills.length ? "matched" : "no-fill-in-window";
|
|
20183
|
+
return { coin: order.coin, oid: order.oid ?? null, status, filled, expected, exchangeStatus: order.status ?? "unknown", reason: order.reason ?? "" };
|
|
20184
|
+
});
|
|
20185
|
+
const supported = records.filter((row) => core(row.coin));
|
|
20186
|
+
const fees = {};
|
|
20187
|
+
for (const row of records) if (row.fee != null && row.feeToken) fees[row.feeToken] = (fees[row.feeToken] ?? 0) + row.fee;
|
|
20188
|
+
const supportedScope = !coin || core(coin);
|
|
20189
|
+
const gross = supportedScope ? supported.reduce((sum, row) => sum + (row.gross ?? 0), 0) : null;
|
|
20190
|
+
const tradingNet = !supportedScope || supported.some((row) => row.net == null) ? null : supported.reduce((sum, row) => sum + row.net, 0);
|
|
20191
|
+
const fundingRows = (payload.funding ?? []).filter((row) => core(row.delta?.coin) && (!coin || row.delta.coin === coin));
|
|
20192
|
+
const funding = supportedScope && payload.coverage?.fundingStatus === "complete" && fundingRows.every((row) => numeric(row.delta.usdc) != null) ? fundingRows.reduce((sum, row) => sum + Number(row.delta.usdc), 0) : null;
|
|
20193
|
+
const closes = supported.filter((row) => row.closedSize > 0);
|
|
20194
|
+
return {
|
|
20195
|
+
records,
|
|
20196
|
+
orders: reconciled,
|
|
20197
|
+
duplicates,
|
|
20198
|
+
fees,
|
|
20199
|
+
gross,
|
|
20200
|
+
tradingNet,
|
|
20201
|
+
funding,
|
|
20202
|
+
net: tradingNet != null && funding != null ? tradingNet + funding : null,
|
|
20203
|
+
closeCount: closes.length,
|
|
20204
|
+
missingReasons: records.filter((row) => row.flags.includes("missing-reason")).length,
|
|
20205
|
+
unmatched: records.filter((row) => row.flags.includes("unmatched-order")).length,
|
|
20206
|
+
gaps: reconciled.filter((row) => ["size-gap", "excess-fill"].includes(row.status)).length,
|
|
20207
|
+
unsupported: records.length - supported.length
|
|
20208
|
+
};
|
|
20209
|
+
}
|
|
20210
|
+
|
|
20211
|
+
// src/client/components/trade-review.tsx
|
|
20212
|
+
var flags = {
|
|
20213
|
+
"unmatched-order": ["\u672A\u5339\u914D\u59D4\u6258", "Unmatched order"],
|
|
20214
|
+
"missing-reason": ["\u672A\u8BB0\u5F55\u539F\u56E0", "Missing rationale"],
|
|
20215
|
+
"missing-fee": ["\u8D39\u7528\u672A\u77E5", "Unknown fee"],
|
|
20216
|
+
"missing-pnl": ["\u76C8\u4E8F\u672A\u77E5", "Unknown P&L"],
|
|
20217
|
+
"missing-trade-id": ["\u7F3A\u5C11\u6210\u4EA4 ID", "Missing trade ID"],
|
|
20218
|
+
"size-gap": ["\u6570\u91CF\u6709\u5DEE\u5F02", "Size gap"],
|
|
20219
|
+
"excess-fill": ["\u6210\u4EA4\u8D85\u51FA\u59D4\u6258\u91CF", "Excess fill"],
|
|
20220
|
+
"missing-price": ["\u4EF7\u683C\u672A\u77E5", "Unknown price"],
|
|
20221
|
+
"missing-size": ["\u6570\u91CF\u672A\u77E5", "Unknown size"],
|
|
20222
|
+
"missing-time": ["\u65F6\u95F4\u672A\u77E5", "Unknown time"],
|
|
20223
|
+
"missing-side": ["\u65B9\u5411\u672A\u77E5", "Unknown side"],
|
|
20224
|
+
"matched": ["\u6570\u91CF\u5339\u914D", "Quantity matched"],
|
|
20225
|
+
"no-fill-in-window": ["\u7A97\u53E3\u5185\u65E0\u6210\u4EA4", "No fill in window"]
|
|
20226
|
+
};
|
|
20227
|
+
function TradeReview() {
|
|
20228
|
+
const s = useStore(), [days, setDays] = React18.useState(30), [scope, setScope] = React18.useState("coin");
|
|
20229
|
+
const [reload, setReload] = React18.useState(0), [data, setData] = React18.useState(null), [error, setError] = React18.useState(""), [limit, setLimit] = React18.useState(50);
|
|
20230
|
+
const identity = `${s.status?.network}:${s.status?.tradingMode}:${s.account?.address ?? ""}:${days}`;
|
|
20231
|
+
const [loadedFor, setLoadedFor] = React18.useState("");
|
|
20232
|
+
React18.useEffect(() => {
|
|
20233
|
+
let live = true;
|
|
20234
|
+
setData(null);
|
|
20235
|
+
setError("");
|
|
20236
|
+
setLimit(50);
|
|
20237
|
+
setLoadedFor("");
|
|
20238
|
+
void hyperCall("tradeHistory", { includeFunding: true, days }).then((value) => {
|
|
20239
|
+
if (live) {
|
|
20240
|
+
setData(value);
|
|
20241
|
+
setLoadedFor(identity);
|
|
20242
|
+
}
|
|
20243
|
+
}).catch((cause) => {
|
|
20244
|
+
if (live) setError(String(cause?.message ?? cause));
|
|
20245
|
+
});
|
|
20246
|
+
return () => {
|
|
20247
|
+
live = false;
|
|
20248
|
+
};
|
|
20249
|
+
}, [identity, reload]);
|
|
20250
|
+
const payload = loadedFor === identity ? data : null;
|
|
20251
|
+
const coin = scope === "coin" ? s.activeCoin : "";
|
|
20252
|
+
const ledger = React18.useMemo(() => payload ? reviewLedger(payload, coin) : null, [payload, coin]);
|
|
20253
|
+
const showFlag = (flag) => flags[flag] ? tr(...flags[flag]) : flag;
|
|
20254
|
+
const money = (value) => value == null ? tr("\u672A\u77E5", "Unknown") : `${fmt(value)} USDC`;
|
|
20255
|
+
const coverage = payload?.coverage;
|
|
20256
|
+
return /* @__PURE__ */ React18.createElement("section", { className: "dhl-review", "aria-label": tr("\u4EA4\u6613\u590D\u76D8", "Trade review") }, /* @__PURE__ */ React18.createElement("div", { className: "dhl-history-controls" }, /* @__PURE__ */ React18.createElement(Select, { "aria-label": tr("\u590D\u76D8\u65F6\u95F4\u8303\u56F4", "Review window"), value: days, onChange: (e) => setDays(Number(e.target.value)) }, [7, 30, 90].map((d) => /* @__PURE__ */ React18.createElement("option", { value: d, key: d }, tr(`\u6700\u8FD1 ${d} \u5929`, `Last ${d} days`)))), /* @__PURE__ */ React18.createElement(Select, { "aria-label": tr("\u590D\u76D8\u6807\u7684\u8303\u56F4", "Review markets"), value: scope, onChange: (e) => setScope(e.target.value) }, /* @__PURE__ */ React18.createElement("option", { value: "coin" }, s.activeCoin), /* @__PURE__ */ React18.createElement("option", { value: "all" }, tr("\u5168\u90E8\u6807\u7684", "All markets"))), /* @__PURE__ */ React18.createElement("button", { type: "button", onClick: () => setReload((v) => v + 1) }, tr("\u5237\u65B0\u8BC1\u636E", "Refresh evidence")), /* @__PURE__ */ React18.createElement("button", { type: "button", disabled: !ledger, onClick: () => downloadBlob(new Blob([JSON.stringify({ exportedAt: (/* @__PURE__ */ new Date()).toISOString(), scope: coin || "all", evidence: payload, reconciliation: ledger }, null, 2)], { type: "application/json" }), `trade-review-${Date.now()}.json`) }, tr("\u5BFC\u51FA\u8BC1\u636E JSON", "Export evidence JSON")), /* @__PURE__ */ React18.createElement("button", { type: "button", disabled: !ledger, onClick: () => queueToAgent(tr(`\u8BF7\u7528 hyper_account action=review days=${days}${coin ? ` coin=${coin}` : ""} \u83B7\u53D6\u590D\u76D8\u8BC1\u636E\uFF0C\u6838\u5BF9\u8BA2\u5355\u3001\u6210\u4EA4\u3001\u6210\u672C\u548C\u539F\u56E0\u3002\u5148\u8BF4\u660E\u8986\u76D6\u7F3A\u53E3\uFF0C\u518D\u590D\u76D8\u51B3\u7B56\u4E0E\u6267\u884C\uFF1B\u4E0D\u8981\u628A\u7A97\u53E3\u7EDF\u8BA1\u5F53\u6210\u8D26\u6237\u6536\u76CA\u7387\u3002`, `Use hyper_account action=review days=${days}${coin ? ` coin=${coin}` : ""} to review orders, fills, costs and rationale. Explain coverage gaps before reviewing decisions and execution. Window totals are not account returns.`)) }, tr("\u4EA4\u7ED9 AI \u590D\u76D8", "Review with AI"))), error ? /* @__PURE__ */ React18.createElement("p", { className: "dhl-inline-error", role: "alert" }, error) : !ledger ? /* @__PURE__ */ React18.createElement("p", { role: "status" }, tr("\u6B63\u5728\u6838\u5BF9\u4EA4\u6613\u8BC1\u636E\u2026", "Checking trading evidence\u2026")) : /* @__PURE__ */ React18.createElement(React18.Fragment, null, /* @__PURE__ */ React18.createElement("p", { className: "dhl-evidence-note" }, payload.mode === "paper" ? tr("\u6A21\u62DF\u8D26\u672C", "Paper ledger") : tr("\u4EA4\u6613\u6240\u5386\u53F2", "Exchange history"), " \xB7 ", payload.network, " \xB7 ", new Date(coverage.startTime).toLocaleString(), " \u2014 ", new Date(coverage.endTime).toLocaleString(), /* @__PURE__ */ React18.createElement("br", null), tr("\u7A97\u53E3\u7EDF\u8BA1\uFF0C\u4E0D\u4EE3\u8868\u8D26\u6237\u6536\u76CA\u6216\u5B8C\u6574\u5F80\u8FD4\u4EA4\u6613\u3002\u4EC5\u6838\u5FC3 USDC \u6C38\u7EED\u5408\u5E76\u76C8\u4E8F\uFF1B\u5176\u4ED6\u62B5\u62BC\u5E01\u79CD\u5355\u72EC\u770B\u539F\u59CB\u8BB0\u5F55\u3002\u624B\u7EED\u8D39\u5DF2\u542B builder fee\uFF0C\u4E0D\u91CD\u590D\u6263\u9664\u3002", "Window statistics, not account returns or completed round trips. Combined P&L covers core USDC perpetuals only. Fees include builder fees once.")), coverage.fillsLimited || coverage.ordersLimited ? /* @__PURE__ */ React18.createElement("p", { className: "dhl-inline-error" }, tr("\u5386\u53F2\u6570\u91CF\u8FBE\u5230\u4FDD\u7559\u4E0A\u9650\uFF0C\u65F6\u95F4\u8303\u56F4\u53EF\u80FD\u4E0D\u5B8C\u6574\uFF1B\u4E0D\u80FD\u636E\u6B64\u786E\u8BA4\u5168\u90E8\u4EA4\u6613\u5DF2\u6838\u5BF9\u3002", "History reaches its retention limit; this window may be incomplete.")) : null, coverage.fundingStatus !== "complete" ? /* @__PURE__ */ React18.createElement("p", { className: "dhl-evidence-note" }, tr("\u8D44\u91D1\u8D39", "Funding"), "\uFF1A", coverage.fundingStatus === "not-simulated" ? tr("\u672A\u6A21\u62DF", "Not simulated") : tr("\u672A\u5B8C\u6574\u53D6\u5F97", "Incomplete"), " ", coverage.fundingError ?? "", " \xB7 ", tr("\u4E0D\u663E\u793A\u542B\u8D44\u91D1\u8D39\u51C0\u989D", "Net including funding is unknown")) : null, /* @__PURE__ */ React18.createElement("div", { className: "dhl-review-summary" }, [[tr("\u5DF2\u77E5\u6BDB\u76C8\u4E8F", "Known gross P&L"), money(ledger.gross)], [tr("\u6263\u624B\u7EED\u8D39\u51C0\u989D", "Net after fees"), money(ledger.tradingNet)], [tr("\u8D44\u91D1\u8D39\u6536\u5165 / \u652F\u51FA", "Funding credits / debits"), money(ledger.funding)], [tr("\u542B\u8D44\u91D1\u8D39\u51C0\u989D", "Net including funding"), money(ledger.net)], [tr("\u6210\u4EA4 / \u5E73\u4ED3\u6210\u4EA4\u7B14\u6570", "Fills / closing fills"), `${ledger.records.length} / ${ledger.closeCount}`], [tr("\u7F3A\u5C11\u539F\u56E0 / \u672A\u5339\u914D / \u6570\u91CF\u5DEE\u5F02", "Missing rationale / unmatched / size gaps"), `${ledger.missingReasons} / ${ledger.unmatched} / ${ledger.gaps}`]].map(([label, value]) => /* @__PURE__ */ React18.createElement("div", { key: label }, /* @__PURE__ */ React18.createElement("span", null, label), /* @__PURE__ */ React18.createElement("strong", null, value)))), /* @__PURE__ */ React18.createElement("p", { className: "dhl-evidence-note" }, tr("\u5DF2\u77E5\u624B\u7EED\u8D39", "Known fees"), "\uFF1A", Object.entries(ledger.fees).map(([token, value]) => `${fmt(value, 6)} ${token}`).join(" \xB7 ") || "\u2014", " \xB7 ", tr("\u53BB\u91CD\u6210\u4EA4", "Duplicate fills removed"), " ", ledger.duplicates, " \xB7 ", tr("\u672A\u5408\u5E76\u76C8\u4E8F\u7684\u6210\u4EA4", "Fills excluded from combined P&L"), " ", ledger.unsupported), payload.paperCosts ? /* @__PURE__ */ React18.createElement("p", { className: "dhl-evidence-note" }, tr("\u5F53\u524D\u6A21\u62DF\u5047\u8BBE", "Current simulation assumptions"), "\uFF1A", payload.paperCosts.feeBps, " bps ", tr("\u624B\u7EED\u8D39", "fee"), " / ", payload.paperCosts.slippageBps, " bps ", tr("\u6ED1\u70B9", "slippage"), " \xB7 ", tr("\u5386\u53F2\u9010\u7B14\u4FDD\u7559\u5F53\u65F6\u914D\u7F6E\uFF1B\u65E7\u8BB0\u5F55\u8D39\u7528\u672A\u77E5\u3002\u672A\u6A21\u62DF\u76D8\u53E3\u6DF1\u5EA6\u3001\u8D44\u91D1\u8D39\u3001\u5F3A\u5E73\u3002", "Each fill preserves its assumptions; legacy fees may be unknown. No depth, funding or liquidation simulation.")) : null, payload.localOrders?.some((row) => ["unknown", "submitting", "protection-unknown"].includes(row.status)) ? /* @__PURE__ */ React18.createElement("details", null, /* @__PURE__ */ React18.createElement("summary", { className: "dhl-inline-error" }, tr("\u672C\u5730\u4E0B\u5355\u72B6\u6001\u5F85\u6838\u5B9E", "Local order status unconfirmed")), payload.localOrders.filter((row) => ["unknown", "submitting", "protection-unknown"].includes(row.status)).map((row) => /* @__PURE__ */ React18.createElement("div", { className: "dhl-evidence-detail", key: row.cloid }, /* @__PURE__ */ React18.createElement("span", null, row.coin, " \xB7 ", row.status, " \xB7 ", row.cloid), /* @__PURE__ */ React18.createElement("button", { type: "button", className: "dhl-ghost", onClick: () => queueToAgent(tr(`\u8BF7\u7528 hyper_account action=orderStatus oid=${row.cloid} \u6838\u5B9E\u8BA2\u5355\u4E0E\u4FDD\u62A4\u5355\u72B6\u6001\uFF0C\u4E0D\u8981\u91CD\u590D\u4E0B\u5355\u3002`, `Use hyper_account action=orderStatus oid=${row.cloid} to check the order and protection state. Do not submit another order.`)) }, tr("\u6838\u5B9E\u8BA2\u5355\u72B6\u6001", "Check order status"))))) : null, /* @__PURE__ */ React18.createElement("details", null, /* @__PURE__ */ React18.createElement("summary", null, tr("\u59D4\u6258\u6570\u91CF\u6838\u5BF9", "Order quantity reconciliation"), " \xB7 ", ledger.orders.length), /* @__PURE__ */ React18.createElement("p", { className: "dhl-evidence-note" }, tr("\u6309\u6807\u7684 + oid \u5339\u914D\uFF1B\u90E8\u5206\u6210\u4EA4\u3001\u7A97\u53E3\u5916\u6210\u4EA4\u6216\u4FDD\u7559\u622A\u65AD\u90FD\u4F1A\u4EA7\u751F\u5DEE\u5F02\uFF0C\u65E0\u6210\u4EA4\u4E0D\u7B49\u4E8E\u672A\u6267\u884C\u3002", "Matched by market and oid. Partial fills, window boundaries and retention can cause gaps. No fill is not proof of no execution.")), ledger.orders.slice(0, limit).map((row, i) => /* @__PURE__ */ React18.createElement("div", { className: "dhl-review-order", key: `${row.coin}:${row.oid}:${i}` }, /* @__PURE__ */ React18.createElement("span", null, row.coin, " \xB7 oid ", row.oid ?? "\u2014"), /* @__PURE__ */ React18.createElement("span", null, fmt(row.filled, 6), " / ", row.expected == null ? "\u2014" : fmt(row.expected, 6)), /* @__PURE__ */ React18.createElement("span", null, row.exchangeStatus, " \xB7 ", showFlag(row.status))))), !ledger.records.length ? /* @__PURE__ */ React18.createElement("p", { className: "dhl-empty" }, tr("\u7A97\u53E3\u5185\u65E0\u6210\u4EA4\u8BB0\u5F55", "No fills in this window")) : null, [...ledger.records].sort((a, b) => (b.time ?? 0) - (a.time ?? 0)).slice(0, limit).map((row) => /* @__PURE__ */ React18.createElement("details", { key: row.id, className: "dhl-review-fill" }, /* @__PURE__ */ React18.createElement("summary", null, /* @__PURE__ */ React18.createElement("time", null, row.time ? new Date(row.time).toLocaleString() : "\u2014"), " ", /* @__PURE__ */ React18.createElement("b", null, row.coin), " ", row.isBuy == null ? tr("\u65B9\u5411\u672A\u77E5", "Side unknown") : row.isBuy ? tr("\u4E70", "Buy") : tr("\u5356", "Sell"), " ", fmt(row.size, 6), " @ ", fmtPrice(row.px), " ", /* @__PURE__ */ React18.createElement("span", null, tr("\u6BDB\u76C8\u4E8F", "Gross"), " ", row.gross == null ? "\u2014" : fmt(row.gross))), /* @__PURE__ */ React18.createElement("div", { className: "dhl-evidence-detail" }, /* @__PURE__ */ React18.createElement("span", null, "oid ", row.oid ?? "\u2014", " \xB7 ", row.id), /* @__PURE__ */ React18.createElement("span", null, tr("\u624B\u7EED\u8D39", "Fee"), " ", row.fee == null ? tr("\u672A\u77E5", "Unknown") : `${fmt(row.fee, 6)} ${row.feeToken}`, " \xB7 ", tr("USDC \u51C0\u989D", "USDC net"), " ", money(row.net)), /* @__PURE__ */ React18.createElement("span", null, tr("\u76F8\u5BF9\u51B3\u7B56\u4E2D\u95F4\u4EF7\u7684\u6ED1\u70B9\uFF08\u6B63\u6570\u4E3A\u4E0D\u5229\uFF09", "Slippage vs decision mid (positive is adverse)"), " ", row.slippageBps == null ? tr("\u672A\u77E5", "Unknown") : `${fmt(row.slippageBps, 2)} bps`), /* @__PURE__ */ React18.createElement("span", null, tr("\u5F53\u65F6\u539F\u56E0", "Rationale"), "\uFF1A", row.reason || tr("\u672A\u8BB0\u5F55\uFF0C\u4E0D\u80FD\u4E8B\u540E\u8865\u5199\u6210\u539F\u59CB\u4F9D\u636E", "Not recorded; cannot reconstruct original rationale")), /* @__PURE__ */ React18.createElement("span", null, row.flags.map(showFlag).join(" \xB7 ")), row.coin === s.activeCoin && row.time ? /* @__PURE__ */ React18.createElement("button", { type: "button", className: "dhl-ghost", onClick: () => {
|
|
20257
|
+
store.patch({ chartFocus: { coin: row.coin, interval: s.interval, t: row.time, at: Date.now() } });
|
|
20258
|
+
saveChartTab();
|
|
20259
|
+
} }, tr("\u5B9A\u4F4D\u56FE\u8868", "Locate on chart")) : null))), Math.max(ledger.records.length, ledger.orders.length) > limit ? /* @__PURE__ */ React18.createElement("button", { type: "button", className: "dhl-ghost", onClick: () => setLimit((v) => v + 100) }, tr("\u663E\u793A\u66F4\u591A", "Show more")) : null));
|
|
20260
|
+
}
|
|
20261
|
+
var saveChartTab = () => saveSessionWorkspace({ chartTab: "chart" });
|
|
20262
|
+
|
|
20263
|
+
// src/client/components/bottom-table.tsx
|
|
19179
20264
|
function BottomTable({ account, accountError }) {
|
|
19180
20265
|
const s = useStore();
|
|
19181
20266
|
const tab = s.workspaceSessionId === s.activeSessionId ? s.workspace?.bottomTab ?? "positions" : "positions";
|
|
19182
|
-
const [orderView, setOrderView] =
|
|
19183
|
-
const [historyLimit, setHistoryLimit] =
|
|
20267
|
+
const [orderView, setOrderView] = React19.useState("open");
|
|
20268
|
+
const [historyLimit, setHistoryLimit] = React19.useState(100);
|
|
19184
20269
|
const positions = account?.positions ?? [];
|
|
19185
20270
|
const orders = account?.openOrders ?? [];
|
|
19186
20271
|
const history = s.tradeHistory;
|
|
19187
|
-
|
|
20272
|
+
React19.useEffect(() => {
|
|
19188
20273
|
if (tab === "orders") void loadTradeHistory();
|
|
19189
20274
|
}, [tab, account?.address, s.status?.network]);
|
|
19190
20275
|
const requestCancel = (coin, oid) => {
|
|
@@ -19196,58 +20281,61 @@ function BottomTable({ account, accountError }) {
|
|
|
19196
20281
|
const tabs = [
|
|
19197
20282
|
["positions", t("tab.positions", "\u6301\u4ED3")],
|
|
19198
20283
|
["orders", t("tab.orders", "\u59D4\u6258\u4E0E\u6210\u4EA4")],
|
|
19199
|
-
["account", t("tab.account", "\u8D26\u6237")]
|
|
20284
|
+
["account", t("tab.account", "\u8D26\u6237")],
|
|
20285
|
+
["review", tr("\u4EA4\u6613\u590D\u76D8", "Trade review")]
|
|
19200
20286
|
];
|
|
19201
|
-
const head = (...cells) => /* @__PURE__ */
|
|
20287
|
+
const head = (...cells) => /* @__PURE__ */ React19.createElement("div", { className: "dhl-posrow dhl-poshead" }, cells.map((c) => /* @__PURE__ */ React19.createElement("span", { key: c }, c)));
|
|
19202
20288
|
let body = null;
|
|
19203
20289
|
if (tab === "positions") {
|
|
19204
|
-
body = /* @__PURE__ */
|
|
20290
|
+
body = /* @__PURE__ */ React19.createElement("div", { className: "dhl-positions-table" }, /* @__PURE__ */ React19.createElement("div", { className: "dhl-posrow dhl-posrow-position dhl-poshead" }, [t("positions.coin", "\u5408\u7EA6"), t("positions.size", "\u6570\u91CF"), t("positions.entry", "\u5F00\u4ED3\u4EF7"), t("positions.mark", "\u6807\u8BB0"), t("positions.upnl", "\u672A\u5B9E\u73B0"), "ROE", tr("\u6E05\u7B97\u4EF7", "Liquidation")].map((label) => /* @__PURE__ */ React19.createElement("span", { key: label }, label))), positions.length === 0 ? /* @__PURE__ */ React19.createElement("div", { className: "dhl-posrow" }, /* @__PURE__ */ React19.createElement("span", { style: { color: "var(--dsw-alias-label-secondary)" } }, t("positions.none", "\uFF08\u65E0\u6301\u4ED3\uFF09"))) : positions.map((p, i) => {
|
|
19205
20291
|
const roe = p.returnOnEquity == null ? null : Number(p.returnOnEquity) * 100;
|
|
19206
20292
|
const leverage = typeof p.leverage === "number" ? p.leverage : Number(p.leverage?.value);
|
|
19207
|
-
return /* @__PURE__ */
|
|
20293
|
+
return /* @__PURE__ */ React19.createElement("div", { key: `${p.coin}:${i}` }, /* @__PURE__ */ React19.createElement("div", { className: "dhl-posrow dhl-posrow-position" }, /* @__PURE__ */ React19.createElement("span", null, p.coin), /* @__PURE__ */ React19.createElement("span", null, `${p.size > 0 ? "+" : ""}${fmt(p.size, 4)}`), /* @__PURE__ */ React19.createElement("span", null, fmtPrice(p.entryPx)), /* @__PURE__ */ React19.createElement("span", null, p.markPx == null ? "\u2014" : fmtPrice(p.markPx)), /* @__PURE__ */ React19.createElement("span", { className: p.uPnl >= 0 ? "dhl-up" : "dhl-down" }, fmt(p.uPnl)), /* @__PURE__ */ React19.createElement("span", { className: roe == null ? "" : roe >= 0 ? "dhl-up" : "dhl-down" }, roe == null ? "\u2014" : `${fmt(roe)}%`), /* @__PURE__ */ React19.createElement("span", null, p.liquidationPx == null ? "\u2014" : fmtPrice(p.liquidationPx))), account?.mode === "live" ? /* @__PURE__ */ React19.createElement("div", { className: "dhl-position-extra" }, /* @__PURE__ */ React19.createElement("span", null, tr("\u4FDD\u8BC1\u91D1", "Margin"), " ", p.marginUsed == null ? "\u2014" : `${fmt(p.marginUsed)} USDC`), /* @__PURE__ */ React19.createElement("span", null, tr("\u6760\u6746", "Leverage"), " ", Number.isFinite(leverage) && leverage > 0 ? `${leverage}x` : "\u2014"), /* @__PURE__ */ React19.createElement("span", null, tr("\u5F00\u4ED3\u4EE5\u6765\u8D44\u91D1\u8D39", "Funding since open"), " ", p.cumFunding?.sinceOpen == null ? "\u2014" : `${fmt(p.cumFunding.sinceOpen)} USDC`)) : null);
|
|
19208
20294
|
}));
|
|
19209
20295
|
} else if (tab === "orders") {
|
|
19210
20296
|
const rows = orderView === "open" ? orders : orderView === "history" ? history?.orders ?? [] : history?.fills ?? [];
|
|
19211
|
-
body = /* @__PURE__ */
|
|
20297
|
+
body = /* @__PURE__ */ React19.createElement("div", null, /* @__PURE__ */ React19.createElement("div", { className: "dhl-history-controls" }, [["open", tr("\u5F53\u524D\u6302\u5355", "Open")], ["history", tr("\u5386\u53F2\u59D4\u6258", "Order history")], ["fills", tr("\u6210\u4EA4\u8BB0\u5F55", "Fills")]].map(([id, label]) => /* @__PURE__ */ React19.createElement("button", { type: "button", key: id, className: orderView === id ? "on" : "", onClick: () => {
|
|
19212
20298
|
setOrderView(id);
|
|
19213
20299
|
setHistoryLimit(100);
|
|
19214
|
-
} }, label)), /* @__PURE__ */
|
|
20300
|
+
} }, label)), /* @__PURE__ */ React19.createElement("button", { type: "button", className: "dhl-history-refresh", onClick: () => void loadTradeHistory(true) }, tr("\u5237\u65B0", "Refresh"))), orderView !== "open" && s.tradeHistoryError ? /* @__PURE__ */ React19.createElement("div", { className: "dhl-inline-error", role: "status" }, s.tradeHistoryError) : null, head(
|
|
19215
20301
|
t("orders.coin", "\u5408\u7EA6"),
|
|
19216
20302
|
t("orders.side", "\u65B9\u5411"),
|
|
19217
20303
|
t("orders.px", "\u4EF7\u683C"),
|
|
19218
20304
|
t("orders.sz", "\u6570\u91CF"),
|
|
19219
20305
|
t("orders.status", "\u72B6\u6001")
|
|
19220
|
-
), rows.length === 0 ? /* @__PURE__ */
|
|
20306
|
+
), rows.length === 0 ? /* @__PURE__ */ React19.createElement("div", { className: "dhl-posrow" }, /* @__PURE__ */ React19.createElement("span", { style: { color: "var(--dsw-alias-label-secondary)" } }, orderView !== "open" && !s.tradeHistoryAt && !s.tradeHistoryError ? tr("\u6B63\u5728\u8BFB\u53D6\u5386\u53F2\u2026", "Loading history\u2026") : accountError && orderView === "open" ? tr("\u6302\u5355\u72B6\u6001\u672A\u786E\u8BA4", "Order status unconfirmed") : tr("\u6682\u65E0\u8BB0\u5F55", "No records"))) : null, rows.slice(0, orderView === "open" ? 30 : historyLimit).map((row, index) => {
|
|
19221
20307
|
const buy = row.isBuy ?? row.side === "B";
|
|
19222
20308
|
const time = Number(row.timestamp ?? row.time ?? row.statusTimestamp);
|
|
19223
20309
|
const reason = orderView === "fills" ? row.reason || history?.orders.find((order) => order.oid != null && order.oid === row.oid)?.reason : row.reason;
|
|
19224
20310
|
const displayReason = typeof reason === "string" && !reason.startsWith("session:") && !["tool", "chat", "order"].includes(reason) ? reason : "";
|
|
19225
|
-
return /* @__PURE__ */
|
|
19226
|
-
}), orderView !== "open" && rows.length > historyLimit ? /* @__PURE__ */
|
|
20311
|
+
return /* @__PURE__ */ React19.createElement("div", { key: `${orderView}:${row.oid ?? row.tid ?? index}:${time}` }, /* @__PURE__ */ React19.createElement("div", { className: "dhl-posrow" }, /* @__PURE__ */ React19.createElement("span", null, row.coin ?? "\u2014"), /* @__PURE__ */ React19.createElement("span", { className: buy ? "dhl-up" : "dhl-down" }, buy ? t("side.buy", "\u4E70") : t("side.sell", "\u5356")), /* @__PURE__ */ React19.createElement("span", null, fmtPrice(row.limitPx ?? row.px)), /* @__PURE__ */ React19.createElement("span", null, fmt(row.origSz ?? row.sz, 4)), /* @__PURE__ */ React19.createElement("span", { className: "dhl-order-action" }, /* @__PURE__ */ React19.createElement("span", null, orderView === "open" ? t("orders.open", "\u6302\u5355\u4E2D") : orderView === "fills" ? tr("\u5DF2\u6210\u4EA4", "Filled") : String(row.status ?? "\u2014")), orderView === "open" && Number.isInteger(Number(row.oid)) && Number(row.oid) > 0 ? /* @__PURE__ */ React19.createElement("button", { type: "button", className: "dhl-ghost", onClick: () => requestCancel(String(row.coin), Number(row.oid)) }, tr("\u64A4\u5355", "Cancel")) : null)), orderView !== "open" ? /* @__PURE__ */ React19.createElement("div", { className: "dhl-history-detail" }, /* @__PURE__ */ React19.createElement("span", null, Number.isFinite(time) && time > 0 ? new Date(time).toLocaleString() : "\u2014"), orderView === "fills" && row.closedPnl != null ? /* @__PURE__ */ React19.createElement("span", null, "P&L ", fmt(row.closedPnl), " \xB7 ", tr("\u624B\u7EED\u8D39", "Fee"), " ", fmt(row.fee)) : null, displayReason ? /* @__PURE__ */ React19.createElement("span", { title: displayReason }, tr("\u4E0B\u5355\u539F\u56E0", "Order reason"), "\uFF1A", displayReason) : null) : null);
|
|
20312
|
+
}), orderView !== "open" && rows.length > historyLimit ? /* @__PURE__ */ React19.createElement("button", { type: "button", className: "dhl-history-more", onClick: () => setHistoryLimit((limit) => limit + 100) }, tr("\u663E\u793A\u66F4\u591A", "Show more"), " \xB7 ", Math.min(historyLimit, rows.length), "/", rows.length) : null);
|
|
20313
|
+
} else if (tab === "review") {
|
|
20314
|
+
body = /* @__PURE__ */ React19.createElement(TradeReview, null);
|
|
19227
20315
|
} else {
|
|
19228
|
-
body = /* @__PURE__ */
|
|
19229
|
-
/* @__PURE__ */
|
|
19230
|
-
/* @__PURE__ */
|
|
19231
|
-
/* @__PURE__ */
|
|
19232
|
-
/* @__PURE__ */
|
|
19233
|
-
/* @__PURE__ */
|
|
20316
|
+
body = /* @__PURE__ */ React19.createElement("div", { style: { display: "flex", gap: 14, flexWrap: "wrap", fontSize: 11.5, fontVariantNumeric: "tabular-nums", padding: "4px 0" } }, account?.mode === "paper" ? [
|
|
20317
|
+
/* @__PURE__ */ React19.createElement("span", { key: "c" }, `${t("account.cash", "\u73B0\u91D1")}: ${fmt(account.usd)}`),
|
|
20318
|
+
/* @__PURE__ */ React19.createElement("span", { key: "e" }, `${t("account.equity", "\u6743\u76CA")}: ${fmt(account.equity)}`),
|
|
20319
|
+
/* @__PURE__ */ React19.createElement("span", { key: "m" }, `${tr("\u5DF2\u7528\u4FDD\u8BC1\u91D1", "Margin used")}: ${fmt(account.marginUsed)}`),
|
|
20320
|
+
/* @__PURE__ */ React19.createElement("span", { key: "a" }, `${tr("\u53EF\u7528\u4FDD\u8BC1\u91D1", "Available margin")}: ${fmt(account.availableMargin)}`),
|
|
20321
|
+
/* @__PURE__ */ React19.createElement("span", { key: "u", style: { color: account.uPnl >= 0 ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)" } }, `uPnL: ${fmt(account.uPnl)}`)
|
|
19234
20322
|
] : [
|
|
19235
|
-
account?.accountMode ? /* @__PURE__ */
|
|
19236
|
-
account?.accountDex ? /* @__PURE__ */
|
|
19237
|
-
account?.marginSummary ? /* @__PURE__ */
|
|
19238
|
-
account?.balanceSource === "spot" ? /* @__PURE__ */
|
|
19239
|
-
account?.address ? /* @__PURE__ */
|
|
20323
|
+
account?.accountMode ? /* @__PURE__ */ React19.createElement("span", { key: "mode" }, `${tr("\u8D26\u6237\u6A21\u5F0F", "Account mode")}: ${{ unifiedAccount: "Unified", portfolioMargin: "Portfolio", disabled: "Standard", default: "Standard", dexAbstraction: "DEX abstraction" }[account.accountMode] ?? account.accountMode}`) : null,
|
|
20324
|
+
account?.accountDex ? /* @__PURE__ */ React19.createElement("span", { key: "dex" }, `${tr("\u6C38\u7EED DEX", "Perp DEX")}: ${account.accountDex}`) : null,
|
|
20325
|
+
account?.marginSummary ? /* @__PURE__ */ React19.createElement("span", { key: "v" }, `${account.accountDex ? tr("DEX \u62B5\u62BC\u8D26\u6237\u4EF7\u503C", "DEX collateral value") : t("account.value", "\u8D26\u6237\u4EF7\u503C")}: ${fmt(account.marginSummary.accountValue)}`) : null,
|
|
20326
|
+
account?.balanceSource === "spot" ? /* @__PURE__ */ React19.createElement("span", { key: "usdc" }, `${tr("USDC \u4F59\u989D", "USDC balance")}: ${fmt(account.usdcBalance)} \xB7 ${tr("\u672A\u5360\u7528", "Unheld")}: ${fmt(account.unheldUsdc)}`) : account ? /* @__PURE__ */ React19.createElement("span", { key: "a" }, `${account.accountDex ? tr("DEX \u53EF\u7528", "DEX available") : t("account.available", "\u53EF\u7528")}: ${fmt(account.withdrawable)}`) : null,
|
|
20327
|
+
account?.address ? /* @__PURE__ */ React19.createElement("span", { key: "addr", style: { color: "var(--dsw-alias-label-secondary)" } }, account.address.slice(0, 6) + "\u2026" + account.address.slice(-4)) : null
|
|
19240
20328
|
]);
|
|
19241
20329
|
}
|
|
19242
|
-
return /* @__PURE__ */
|
|
20330
|
+
return /* @__PURE__ */ React19.createElement("div", { className: "dhl-card", style: { margin: "0 14px 12px", flex: "none" } }, accountError ? /* @__PURE__ */ React19.createElement("div", { className: "dhl-account-warning", role: "status" }, tr("\u8D26\u6237\u4E0E\u6302\u5355\u72B6\u6001\u672A\u786E\u8BA4", "Account and order status unconfirmed"), "\uFF1A", accountError) : null, /* @__PURE__ */ React19.createElement("div", { style: { display: "flex", gap: 2, marginBottom: 6 } }, tabs.map(([id, label]) => /* @__PURE__ */ React19.createElement("button", { key: id, className: "dhl-tab" + (tab === id ? " on" : ""), onClick: () => saveSessionWorkspace({ bottomTab: id }) }, label))), body, account?.mode === "paper" ? /* @__PURE__ */ React19.createElement("p", { className: "dhl-ticket-note" }, tr(account.simulation, "Configured slippage and fees; depth, funding and liquidation are not simulated.")) : null);
|
|
19243
20331
|
}
|
|
19244
20332
|
|
|
19245
20333
|
// src/client/components/market-picker.tsx
|
|
19246
|
-
var
|
|
19247
|
-
var
|
|
20334
|
+
var React21 = __toESM(require("react"), 1);
|
|
20335
|
+
var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
19248
20336
|
|
|
19249
20337
|
// src/client/components/market-icon.tsx
|
|
19250
|
-
var
|
|
20338
|
+
var React20 = __toESM(require("react"), 1);
|
|
19251
20339
|
var ICON_ROOT = "https://app.hyperliquid.xyz/coins/";
|
|
19252
20340
|
function iconNames(market) {
|
|
19253
20341
|
if (market.kind === "outcome") return [];
|
|
@@ -19259,10 +20347,10 @@ function iconNames(market) {
|
|
|
19259
20347
|
function MarketIcon({ market, small = false }) {
|
|
19260
20348
|
const names = iconNames(market);
|
|
19261
20349
|
const identity = `${market.kind}:${market.name}:${names.join(",")}`;
|
|
19262
|
-
const [state, setState] =
|
|
20350
|
+
const [state, setState] = React20.useState({ identity, index: 0, loaded: false });
|
|
19263
20351
|
const current2 = state.identity === identity ? state : { identity, index: 0, loaded: false };
|
|
19264
20352
|
const name = names[current2.index];
|
|
19265
|
-
return /* @__PURE__ */
|
|
20353
|
+
return /* @__PURE__ */ React20.createElement("span", { className: "dhl-market-icon" + (small ? " dhl-market-icon-small" : ""), "aria-hidden": "true" }, name ? /* @__PURE__ */ React20.createElement(
|
|
19266
20354
|
"img",
|
|
19267
20355
|
{
|
|
19268
20356
|
key: `${identity}:${current2.index}`,
|
|
@@ -19313,19 +20401,19 @@ function interestOf(row) {
|
|
|
19313
20401
|
}
|
|
19314
20402
|
function MarketPicker({ activeCoin, onClose }) {
|
|
19315
20403
|
const s = useStore();
|
|
19316
|
-
const [query, setQuery] =
|
|
19317
|
-
const [category, setCategory] =
|
|
19318
|
-
const [strict, setStrict] =
|
|
19319
|
-
const [sort, setSort] =
|
|
19320
|
-
const [descending, setDescending] =
|
|
19321
|
-
const [favorites, setFavorites] =
|
|
19322
|
-
const [cursor, setCursor] =
|
|
19323
|
-
const [loading, setLoading] =
|
|
19324
|
-
const [renderCount, setRenderCount] =
|
|
19325
|
-
const searchRef =
|
|
19326
|
-
const listRef =
|
|
19327
|
-
const keyboardScroll =
|
|
19328
|
-
|
|
20404
|
+
const [query, setQuery] = React21.useState("");
|
|
20405
|
+
const [category, setCategory] = React21.useState("all");
|
|
20406
|
+
const [strict, setStrict] = React21.useState(true);
|
|
20407
|
+
const [sort, setSort] = React21.useState("volume");
|
|
20408
|
+
const [descending, setDescending] = React21.useState(true);
|
|
20409
|
+
const [favorites, setFavorites] = React21.useState(readFavorites);
|
|
20410
|
+
const [cursor, setCursor] = React21.useState(0);
|
|
20411
|
+
const [loading, setLoading] = React21.useState(false);
|
|
20412
|
+
const [renderCount, setRenderCount] = React21.useState(150);
|
|
20413
|
+
const searchRef = React21.useRef(null);
|
|
20414
|
+
const listRef = React21.useRef(null);
|
|
20415
|
+
const keyboardScroll = React21.useRef(false);
|
|
20416
|
+
React21.useEffect(() => {
|
|
19329
20417
|
searchRef.current?.focus();
|
|
19330
20418
|
setLoading(true);
|
|
19331
20419
|
void loadMarketCatalog().catch((error) => notify(tr("\u5E02\u573A\u76EE\u5F55\u52A0\u8F7D\u5931\u8D25", "Market catalog load failed") + "\uFF1A" + String(error))).finally(() => setLoading(false));
|
|
@@ -19335,7 +20423,7 @@ function MarketPicker({ activeCoin, onClose }) {
|
|
|
19335
20423
|
}, 5e3);
|
|
19336
20424
|
return () => window.clearInterval(timer);
|
|
19337
20425
|
}, []);
|
|
19338
|
-
const marketRows =
|
|
20426
|
+
const marketRows = React21.useMemo(() => {
|
|
19339
20427
|
const livePerps = new Map(s.markets.map((market) => [market.name, market]));
|
|
19340
20428
|
const catalog = s.marketCatalog.length ? s.marketCatalog.map((row) => {
|
|
19341
20429
|
const latest = row.tradable ? livePerps.get(row.name) : null;
|
|
@@ -19390,20 +20478,20 @@ function MarketPicker({ activeCoin, onClose }) {
|
|
|
19390
20478
|
if (category === "trending" && !needle) rows.sort((a, b) => n(b.ctx?.dayNtlVlm) - n(a.ctx?.dayNtlVlm));
|
|
19391
20479
|
return rows;
|
|
19392
20480
|
}, [s.marketCatalog, s.markets, query, category, strict, sort, descending, favorites]);
|
|
19393
|
-
|
|
20481
|
+
React21.useEffect(() => {
|
|
19394
20482
|
setCursor(0);
|
|
19395
20483
|
setRenderCount(150);
|
|
19396
20484
|
listRef.current?.scrollTo({ top: 0 });
|
|
19397
20485
|
}, [query, category, strict, sort, descending]);
|
|
19398
|
-
|
|
20486
|
+
React21.useEffect(() => {
|
|
19399
20487
|
if (cursor >= renderCount) setRenderCount(Math.ceil((cursor + 1) / 150) * 150);
|
|
19400
20488
|
}, [cursor, renderCount]);
|
|
19401
|
-
|
|
20489
|
+
React21.useLayoutEffect(() => {
|
|
19402
20490
|
if (!keyboardScroll.current || cursor >= renderCount) return;
|
|
19403
20491
|
listRef.current?.querySelector(`[data-market-index="${cursor}"]`)?.scrollIntoView({ block: "nearest" });
|
|
19404
20492
|
keyboardScroll.current = false;
|
|
19405
20493
|
}, [cursor, renderCount]);
|
|
19406
|
-
const choose =
|
|
20494
|
+
const choose = React21.useCallback((row) => {
|
|
19407
20495
|
onClose();
|
|
19408
20496
|
void setDeskCoin(row.name).catch(() => {
|
|
19409
20497
|
});
|
|
@@ -19418,11 +20506,11 @@ function MarketPicker({ activeCoin, onClose }) {
|
|
|
19418
20506
|
return next;
|
|
19419
20507
|
});
|
|
19420
20508
|
};
|
|
19421
|
-
const sortBy = (
|
|
19422
|
-
if (
|
|
20509
|
+
const sortBy = (key3) => {
|
|
20510
|
+
if (key3 === sort) setDescending((value) => !value);
|
|
19423
20511
|
else {
|
|
19424
|
-
setSort(
|
|
19425
|
-
setDescending(
|
|
20512
|
+
setSort(key3);
|
|
20513
|
+
setDescending(key3 !== "name");
|
|
19426
20514
|
}
|
|
19427
20515
|
};
|
|
19428
20516
|
const onKeyDown = (event) => {
|
|
@@ -19451,21 +20539,21 @@ function MarketPicker({ activeCoin, onClose }) {
|
|
|
19451
20539
|
choose(marketRows[cursor]);
|
|
19452
20540
|
}
|
|
19453
20541
|
};
|
|
19454
|
-
const header = (label,
|
|
19455
|
-
return /* @__PURE__ */
|
|
19456
|
-
setCategory(
|
|
19457
|
-
if (["spot", "hip3", "outcome", "crypto", "tradfi", "preipo"].includes(
|
|
19458
|
-
} }, t(`market.category.${
|
|
20542
|
+
const header = (label, key3) => /* @__PURE__ */ React21.createElement("button", { type: "button", onClick: () => sortBy(key3), "aria-label": tr(`\u6309${label}\u6392\u5E8F`, `Sort by ${label}`) }, label, sort === key3 ? /* @__PURE__ */ React21.createElement(UiIcon, { name: descending ? "arrowDown" : "arrowUp", size: 12 }) : null);
|
|
20543
|
+
return /* @__PURE__ */ React21.createElement(import_dsh_client_ui_primitives3.Modal, { open: true, onClose, title: t("market.choose", "\u9009\u62E9\u5E02\u573A"), headless: true, className: "dhl-picker-modal", backdropBlur: true, onKeyDownCapture: onKeyDown }, /* @__PURE__ */ React21.createElement("section", { className: "dhl-picker" }, /* @__PURE__ */ React21.createElement("div", { className: "dhl-picker-top" }, /* @__PURE__ */ React21.createElement("div", { className: "dhl-picker-search" }, /* @__PURE__ */ React21.createElement(SearchIcon, null), /* @__PURE__ */ React21.createElement("input", { ref: searchRef, "data-modal-autofocus": true, value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("market.search", "\u641C\u7D22\u5E02\u573A"), "aria-label": t("market.search", "\u641C\u7D22\u5E02\u573A") }), /* @__PURE__ */ React21.createElement("kbd", null, "/")), /* @__PURE__ */ React21.createElement("div", { className: "dhl-picker-mode", role: "group", "aria-label": tr("\u5E02\u573A\u8303\u56F4", "Market scope") }, /* @__PURE__ */ React21.createElement("button", { type: "button", className: strict ? "on" : "", "aria-pressed": strict, onClick: () => setStrict(true) }, t("market.tradable", "\u53EF\u4EA4\u6613")), /* @__PURE__ */ React21.createElement("button", { type: "button", className: !strict ? "on" : "", "aria-pressed": !strict, onClick: () => setStrict(false) }, t("market.all", "\u5168\u90E8"))), /* @__PURE__ */ React21.createElement("button", { type: "button", className: "dhl-picker-close", onClick: onClose, "aria-label": t("modal.close", "\u5173\u95ED") }, /* @__PURE__ */ React21.createElement(UiIcon, { name: "close", size: 18 }))), !strict ? /* @__PURE__ */ React21.createElement("p", { className: "dhl-picker-readonly-note" }, tr("\u201C\u53EA\u8BFB\u201D\u8868\u793A\u672C\u63D2\u4EF6\u4EC5\u63D0\u4F9B\u884C\u60C5\uFF0C\u5C1A\u672A\u63A5\u5165\u8BE5\u5E02\u573A\u7684\u4E0B\u5355\u4E0E\u98CE\u63A7\uFF1BHIP-3 \u548C\u73B0\u8D27\u5728 Hyperliquid \u672C\u8EAB\u53EF\u4EA4\u6613\u3002", "\u201CView only\u201D means this plugin has market data but no order and risk support for that market. Hyperliquid itself supports HIP-3 and spot trading.")) : null, /* @__PURE__ */ React21.createElement("nav", { className: "dhl-picker-tabs", "aria-label": tr("\u5E02\u573A\u5206\u7C7B", "Market categories") }, CATEGORIES.map(([key3, label]) => /* @__PURE__ */ React21.createElement("button", { type: "button", key: key3, className: category === key3 ? "on" : "", "aria-current": category === key3 ? "page" : void 0, onClick: () => {
|
|
20544
|
+
setCategory(key3);
|
|
20545
|
+
if (["spot", "hip3", "outcome", "crypto", "tradfi", "preipo"].includes(key3)) setStrict(false);
|
|
20546
|
+
} }, t(`market.category.${key3}`, label)))), /* @__PURE__ */ React21.createElement("div", { className: "dhl-picker-tablehead" }, /* @__PURE__ */ React21.createElement(UiIcon, { name: "star", size: 15 }), header(t("market.name", "\u5E02\u573A"), "name"), header(t("market.price", "\u6700\u65B0\u4EF7"), "price"), header(t("market.change", "24h \u6DA8\u8DCC"), "change"), header(t("market.funding", "\u8D44\u91D1\u8D39"), "funding"), header(t("market.volume", "24h \u6210\u4EA4\u989D"), "volume"), header(t("market.openInterest", "\u672A\u5E73\u4ED3\u91CF"), "openInterest")), /* @__PURE__ */ React21.createElement("div", { className: "dhl-picker-list", role: "listbox", "aria-label": tr("\u5E02\u573A\u5217\u8868", "Market list"), ref: listRef, onScroll: (event) => {
|
|
19459
20547
|
const element = event.currentTarget;
|
|
19460
20548
|
if (element.scrollTop + element.clientHeight >= element.scrollHeight - 200) setRenderCount((count) => Math.min(marketRows.length, count + 150));
|
|
19461
20549
|
} }, marketRows.slice(0, renderCount).map((row, index) => {
|
|
19462
20550
|
const change = changeOf(row);
|
|
19463
|
-
return /* @__PURE__ */
|
|
19464
|
-
}), !loading && marketRows.length === 0 ? /* @__PURE__ */
|
|
20551
|
+
return /* @__PURE__ */ React21.createElement("div", { className: "dhl-picker-row" + (row.name === activeCoin ? " active" : "") + (cursor === index ? " cursor" : ""), role: "option", "aria-selected": row.name === activeCoin, key: `${row.kind}:${row.name}`, "data-market-index": index, onMouseEnter: () => setCursor(index) }, /* @__PURE__ */ React21.createElement("button", { type: "button", className: "dhl-picker-star" + (favorites.includes(row.name) ? " on" : ""), "aria-label": `${favorites.includes(row.name) ? tr("\u53D6\u6D88\u81EA\u9009", "Remove favorite") : tr("\u52A0\u5165\u81EA\u9009", "Add favorite")} ${row.label}`, onClick: () => toggleFavorite(row.name) }, /* @__PURE__ */ React21.createElement(UiIcon, { name: "star", size: 17, filled: favorites.includes(row.name) })), /* @__PURE__ */ React21.createElement("button", { type: "button", className: "dhl-picker-market", onClick: () => choose(row), title: `${row.label} \xB7 ${row.name} \xB7 ${row.venue}${row.category ? ` \xB7 ${row.category}` : ""}` }, /* @__PURE__ */ React21.createElement(MarketIcon, { market: row, small: true }), /* @__PURE__ */ React21.createElement("strong", null, row.label), /* @__PURE__ */ React21.createElement("small", null, row.kind === "perp" ? "PERP" : row.kind === "hip3" ? "HIP-3" : row.kind === "outcome" ? "OUTCOME" : "SPOT", row.name.startsWith("@") ? ` ${row.name}` : "", row.category ? ` \xB7 ${row.category}` : "", row.tradable ? "" : ` \xB7 ${tr("\u53EA\u8BFB", "View only")}`)), /* @__PURE__ */ React21.createElement("span", null, n(row.ctx?.markPx) > 0 ? fmtPrice(row.ctx.markPx) : "\u2014"), /* @__PURE__ */ React21.createElement("span", { className: change > 0 ? "dhl-up" : change < 0 ? "dhl-down" : "" }, n(row.ctx?.prevDayPx) > 0 ? `${change >= 0 ? "+" : ""}${change.toFixed(2)}%` : "\u2014"), /* @__PURE__ */ React21.createElement("span", null, row.ctx?.funding != null ? `${(n(row.ctx.funding) * 100).toFixed(4)}%` : "\u2014"), /* @__PURE__ */ React21.createElement("span", null, row.ctx?.dayNtlVlm != null ? "$" + fmt(row.ctx.dayNtlVlm, 0) : "\u2014"), /* @__PURE__ */ React21.createElement("span", null, row.ctx?.openInterest != null ? "$" + fmt(interestOf(row), 0) : "\u2014"));
|
|
20552
|
+
}), !loading && marketRows.length === 0 ? /* @__PURE__ */ React21.createElement("div", { className: "dhl-picker-empty" }, category === "outcome" && s.status?.network === "mainnet" ? t("market.outcomeTestnet", "Outcome \u76EE\u5F55\u76EE\u524D\u4EC5\u5728 testnet \u63D0\u4F9B\u3002") : t("market.empty", "\u6CA1\u6709\u5339\u914D\u7684\u5E02\u573A")) : null, loading && !s.marketCatalog.length ? /* @__PURE__ */ React21.createElement("div", { className: "dhl-picker-empty" }, t("market.loading", "\u6B63\u5728\u8BFB\u53D6\u5E02\u573A\u76EE\u5F55\u2026")) : null), /* @__PURE__ */ React21.createElement("footer", { className: "dhl-picker-foot" }, /* @__PURE__ */ React21.createElement("span", null, t("market.shortcuts", "\u2191\u2193 \u9009\u62E9\u3000Enter \u6253\u5F00\u3000\u2318/Ctrl+S \u81EA\u9009\u3000Esc \u5173\u95ED")), /* @__PURE__ */ React21.createElement("span", null, marketRows.length, " ", t("market.count", "\u4E2A\u5E02\u573A"), " \xB7 Hyperliquid \xB7 ", s.status?.network ?? "Hyperliquid"))));
|
|
19465
20553
|
}
|
|
19466
20554
|
|
|
19467
20555
|
// src/client/components/strategy-menu.tsx
|
|
19468
|
-
var
|
|
20556
|
+
var React22 = __toESM(require("react"), 1);
|
|
19469
20557
|
|
|
19470
20558
|
// src/client/strategy-theories.ts
|
|
19471
20559
|
var MINUTES = {
|
|
@@ -19623,13 +20711,13 @@ var STATE_LABELS = {
|
|
|
19623
20711
|
insufficient: ["\u6570\u636E\u4E0D\u8DB3", "Insufficient data"]
|
|
19624
20712
|
};
|
|
19625
20713
|
function StrategyMenu({ candles, coin, interval }) {
|
|
19626
|
-
const menu =
|
|
20714
|
+
const menu = React22.useRef(null);
|
|
19627
20715
|
const english = tr("zh", "en") === "en";
|
|
19628
|
-
const studies =
|
|
19629
|
-
return /* @__PURE__ */
|
|
20716
|
+
const studies = React22.useMemo(() => evaluateStrategyStudies(candles, coin, interval, Date.now(), english), [candles, coin, interval, english]);
|
|
20717
|
+
return /* @__PURE__ */ React22.createElement("details", { className: "dhl-strategy-menu", ref: menu }, /* @__PURE__ */ React22.createElement("summary", null, tr("\u7B56\u7565\u6846\u67B6", "Strategies"), /* @__PURE__ */ React22.createElement(UiIcon, { name: "chevronDown", size: 12 })), /* @__PURE__ */ React22.createElement("div", { className: "dhl-strategy-popover", role: "group", "aria-label": tr("\u4EA4\u6613\u7B56\u7565\u7814\u7A76", "Trading strategy studies") }, /* @__PURE__ */ React22.createElement("header", null, /* @__PURE__ */ React22.createElement("strong", null, tr("\u7B56\u7565\u7814\u7A76 \xB7 \u5DF2\u6536\u76D8 K \u7EBF", "Strategy studies \xB7 closed candles")), /* @__PURE__ */ React22.createElement("small", null, coin, " \xB7 ", interval)), /* @__PURE__ */ React22.createElement("p", { className: "dhl-strategy-caveat" }, tr(
|
|
19630
20718
|
"\u8FD9\u4E9B\u6761\u4EF6\u7528\u4E8E\u7EC4\u7EC7\u5206\u6790\uFF0C\u672A\u5728\u672C\u5E02\u573A\u548C\u5468\u671F\u9A8C\u8BC1\u6536\u76CA\uFF1B\u9700\u8981\u6838\u5BF9\u66F4\u9AD8\u5468\u671F\u3001\u76D8\u53E3\u3001\u8D44\u91D1\u8D39\u548C\u4EA4\u6613\u6210\u672C\u3002",
|
|
19631
20719
|
"These conditions structure analysis; returns are unverified for this market and interval. Check higher timeframe, book, funding and costs."
|
|
19632
|
-
)), studies.map((study) => /* @__PURE__ */
|
|
20720
|
+
)), studies.map((study) => /* @__PURE__ */ React22.createElement("section", { className: "dhl-strategy-card", key: study.id }, /* @__PURE__ */ React22.createElement("div", { className: "dhl-strategy-card-head" }, /* @__PURE__ */ React22.createElement("b", null, study.title), /* @__PURE__ */ React22.createElement("span", { className: `state-${study.state}` }, tr(...STATE_LABELS[study.state]))), /* @__PURE__ */ React22.createElement("p", null, study.summary), study.evidence.map((item) => /* @__PURE__ */ React22.createElement("small", { key: item }, item)), study.evidence.length ? /* @__PURE__ */ React22.createElement("small", null, tr("\u5931\u6548\uFF1A", "Invalidation: "), study.invalidation) : null, /* @__PURE__ */ React22.createElement("div", { className: "dhl-strategy-card-actions" }, /* @__PURE__ */ React22.createElement("a", { href: study.source, target: "_blank", rel: "noopener noreferrer" }, tr("\u7406\u8BBA\u6765\u6E90", "Source")), /* @__PURE__ */ React22.createElement("button", { type: "button", disabled: study.state === "stale" || study.state === "insufficient", onClick: () => {
|
|
19633
20721
|
if (queueToAgent(study.prompt)) menu.current?.removeAttribute("open");
|
|
19634
20722
|
} }, tr("\u8BF7 Agent \u6838\u5BF9", "Ask agent to verify")))))));
|
|
19635
20723
|
}
|
|
@@ -19695,6 +20783,226 @@ function chartTradeEvents(candles, coin, orders, fills) {
|
|
|
19695
20783
|
return out.sort((a, b) => a.candleTime - b.candleTime || a.time - b.time).slice(-120);
|
|
19696
20784
|
}
|
|
19697
20785
|
|
|
20786
|
+
// src/client/components/indicator-editor.tsx
|
|
20787
|
+
var React23 = __toESM(require("react"), 1);
|
|
20788
|
+
var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
20789
|
+
var SECTIONS = [
|
|
20790
|
+
["rsi", "RSI", [["period", "\u5468\u671F", "Length"], ["lower", "\u4E0B\u9608\u503C", "Lower"], ["upper", "\u4E0A\u9608\u503C", "Upper"]]],
|
|
20791
|
+
["macd", "MACD", [["fast", "\u5FEB\u7EBF", "Fast"], ["slow", "\u6162\u7EBF", "Slow"], ["signal", "\u4FE1\u53F7\u7EBF", "Signal"]]],
|
|
20792
|
+
["bollinger", "BOLL", [["period", "\u5468\u671F", "Length"], ["multiplier", "\u6807\u51C6\u5DEE\u500D\u6570", "Std. deviations"]]],
|
|
20793
|
+
["stochastic", "Stochastic", [["period", "%K \u5468\u671F", "%K length"], ["smoothK", "%K \u5E73\u6ED1", "%K smoothing"], ["smoothD", "%D \u5468\u671F", "%D length"], ["lower", "\u4E0B\u9608\u503C", "Lower"], ["upper", "\u4E0A\u9608\u503C", "Upper"]]],
|
|
20794
|
+
["adx", "ADX / DMI", [["period", "DI \u5468\u671F", "DI length"], ["smoothing", "ADX \u5E73\u6ED1", "ADX smoothing"]]]
|
|
20795
|
+
];
|
|
20796
|
+
function IndicatorEditor({ settings, indicators, maLines, onApply }) {
|
|
20797
|
+
const [open, setOpen] = React23.useState(false);
|
|
20798
|
+
const [draft, setDraft] = React23.useState({});
|
|
20799
|
+
const [active, setActive] = React23.useState(indicators);
|
|
20800
|
+
const [lines, setLines] = React23.useState(maLines);
|
|
20801
|
+
const [templates, setTemplates] = React23.useState([]);
|
|
20802
|
+
const [selected, setSelected] = React23.useState("");
|
|
20803
|
+
const [name, setName] = React23.useState("");
|
|
20804
|
+
const [error, setError] = React23.useState("");
|
|
20805
|
+
const [busy, setBusy] = React23.useState(false);
|
|
20806
|
+
const populate = (value) => setDraft(Object.fromEntries(Object.entries(normalizeIndicatorSettings(value)).map(([section, fields]) => [section, Object.fromEntries(Object.entries(fields).map(([key3, n2]) => [key3, String(n2)]))])));
|
|
20807
|
+
React23.useEffect(() => {
|
|
20808
|
+
if (!open) return;
|
|
20809
|
+
populate(settings);
|
|
20810
|
+
setActive(indicators);
|
|
20811
|
+
setLines(maLines);
|
|
20812
|
+
setSelected("");
|
|
20813
|
+
setName("");
|
|
20814
|
+
setError("");
|
|
20815
|
+
let cancelled = false;
|
|
20816
|
+
void hyperCall("indicatorTemplates", { action: "list" }).then((result) => {
|
|
20817
|
+
if (!cancelled) setTemplates(result.templates);
|
|
20818
|
+
}).catch((cause) => {
|
|
20819
|
+
if (!cancelled) setError(String(cause.message ?? cause));
|
|
20820
|
+
});
|
|
20821
|
+
return () => {
|
|
20822
|
+
cancelled = true;
|
|
20823
|
+
};
|
|
20824
|
+
}, [open]);
|
|
20825
|
+
const values = () => normalizeIndicatorSettings(Object.fromEntries(Object.entries(draft).map(([section, fields]) => [section, Object.fromEntries(Object.entries(fields).map(([key3, n2]) => [key3, n2.trim() ? Number(n2) : NaN]))])));
|
|
20826
|
+
const apply2 = () => {
|
|
20827
|
+
try {
|
|
20828
|
+
onApply({ indicatorSettings: values(), indicators: active, maLines: lines });
|
|
20829
|
+
setOpen(false);
|
|
20830
|
+
} catch (cause) {
|
|
20831
|
+
setError(String(cause.message));
|
|
20832
|
+
}
|
|
20833
|
+
};
|
|
20834
|
+
const save = async (overwrite = false) => {
|
|
20835
|
+
setError("");
|
|
20836
|
+
setBusy(true);
|
|
20837
|
+
try {
|
|
20838
|
+
const result = await hyperCall("indicatorTemplates", { action: "save", name, ...overwrite ? { id: selected } : {}, indicators: active, maLines: lines, indicatorSettings: values() });
|
|
20839
|
+
setTemplates(result.templates);
|
|
20840
|
+
setSelected(result.template.id);
|
|
20841
|
+
} catch (cause) {
|
|
20842
|
+
setError(String(cause.message));
|
|
20843
|
+
} finally {
|
|
20844
|
+
setBusy(false);
|
|
20845
|
+
}
|
|
20846
|
+
};
|
|
20847
|
+
return /* @__PURE__ */ React23.createElement(React23.Fragment, null, /* @__PURE__ */ React23.createElement("button", { className: "dhl-indicator-settings-trigger", type: "button", onClick: () => setOpen(true) }, /* @__PURE__ */ React23.createElement(UiIcon, { name: "settings", size: 14 }), tr("\u53C2\u6570\u4E0E\u6A21\u677F", "Settings & templates")), /* @__PURE__ */ React23.createElement(
|
|
20848
|
+
import_dsh_client_ui_primitives4.Modal,
|
|
20849
|
+
{
|
|
20850
|
+
open,
|
|
20851
|
+
onClose: () => setOpen(false),
|
|
20852
|
+
title: tr("\u6307\u6807\u53C2\u6570\u4E0E\u6A21\u677F", "Indicator settings & templates"),
|
|
20853
|
+
closeLabel: tr("\u5173\u95ED", "Close"),
|
|
20854
|
+
className: "dhl-indicator-settings-modal",
|
|
20855
|
+
footer: /* @__PURE__ */ React23.createElement(React23.Fragment, null, /* @__PURE__ */ React23.createElement(import_dsh_client_ui_primitives4.Button, { variant: "outline", onClick: () => populate() }, tr("\u9ED8\u8BA4\u53C2\u6570", "Default parameters")), /* @__PURE__ */ React23.createElement(import_dsh_client_ui_primitives4.Button, { variant: "primary", onClick: apply2 }, tr("\u5E94\u7528\u6307\u6807\u8BBE\u7F6E", "Apply indicator settings")))
|
|
20856
|
+
},
|
|
20857
|
+
/* @__PURE__ */ React23.createElement("div", { className: "dhl-study-settings" }, /* @__PURE__ */ React23.createElement("section", { className: "dhl-study-template" }, /* @__PURE__ */ React23.createElement("strong", null, tr("\u6307\u6807\u6A21\u677F", "Indicator templates")), /* @__PURE__ */ React23.createElement("small", null, tr("\u6A21\u677F\u5305\u542B\u5DF2\u542F\u7528\u6307\u6807\u3001\u5168\u90E8\u53C2\u6570\u4E0E\u81EA\u5B9A\u4E49\u5747\u7EBF\uFF1B\u4FDD\u5B58\u5230\u5BBF\u4E3B\uFF0C\u53EF\u8DE8\u4F1A\u8BDD\u4F7F\u7528\u3002", "Templates include enabled studies, parameters and custom MAs, saved on the host for reuse across sessions.")), /* @__PURE__ */ React23.createElement(Select, { "aria-label": tr("\u5DF2\u4FDD\u5B58\u6307\u6807\u6A21\u677F", "Saved indicator templates"), value: selected, onChange: (event) => {
|
|
20858
|
+
const id = event.target.value;
|
|
20859
|
+
setSelected(id);
|
|
20860
|
+
setError("");
|
|
20861
|
+
const template = templates.find((row) => row.id === id);
|
|
20862
|
+
if (template) {
|
|
20863
|
+
populate(template.indicatorSettings);
|
|
20864
|
+
setActive(template.indicators);
|
|
20865
|
+
setLines(template.maLines);
|
|
20866
|
+
setName(template.name);
|
|
20867
|
+
}
|
|
20868
|
+
} }, /* @__PURE__ */ React23.createElement("option", { value: "" }, tr("\u9009\u62E9\u6A21\u677F", "Select template")), templates.map((row) => /* @__PURE__ */ React23.createElement("option", { key: row.id, value: row.id }, row.name))), /* @__PURE__ */ React23.createElement(import_dsh_client_ui_primitives4.Input, { "aria-label": tr("\u6307\u6807\u6A21\u677F\u540D\u79F0", "Indicator template name"), value: name, maxLength: 60, placeholder: tr("\u6A21\u677F\u540D\u79F0", "Template name"), onChange: (event) => setName(event.target.value) }), /* @__PURE__ */ React23.createElement("div", { className: "dhl-study-template-actions" }, /* @__PURE__ */ React23.createElement(import_dsh_client_ui_primitives4.Button, { variant: "outline", disabled: busy || !name.trim(), onClick: () => void save() }, tr("\u53E6\u5B58\u6A21\u677F", "Save new")), /* @__PURE__ */ React23.createElement(import_dsh_client_ui_primitives4.Button, { variant: "outline", disabled: busy || !selected || !name.trim(), onClick: () => void save(true) }, tr("\u66F4\u65B0\u6A21\u677F", "Update")), /* @__PURE__ */ React23.createElement(import_dsh_client_ui_primitives4.Button, { variant: "outline", disabled: busy || !selected, onClick: () => {
|
|
20869
|
+
setBusy(true);
|
|
20870
|
+
void hyperCall("indicatorTemplates", { action: "remove", id: selected }).then((result) => {
|
|
20871
|
+
setTemplates(result.templates);
|
|
20872
|
+
setSelected("");
|
|
20873
|
+
}).catch((cause) => setError(String(cause.message))).finally(() => setBusy(false));
|
|
20874
|
+
} }, tr("\u5220\u9664\u6A21\u677F", "Delete")))), /* @__PURE__ */ React23.createElement("div", { className: "dhl-study-parameter-grid" }, SECTIONS.map(([section, title, fields]) => /* @__PURE__ */ React23.createElement("fieldset", { key: section }, /* @__PURE__ */ React23.createElement("legend", null, title), fields.map(([key3, zh, en]) => /* @__PURE__ */ React23.createElement("label", { key: key3 }, tr(zh, en), /* @__PURE__ */ React23.createElement(import_dsh_client_ui_primitives4.Input, { type: "number", step: key3 === "multiplier" ? "any" : 1, "aria-label": `${title} ${tr(zh, en)}`, value: draft[section]?.[key3] ?? "", onChange: (event) => setDraft((current2) => ({ ...current2, [section]: { ...current2[section], [key3]: event.target.value } })) })))))), error ? /* @__PURE__ */ React23.createElement("p", { className: "dhl-inline-error", role: "alert" }, error) : null)
|
|
20875
|
+
));
|
|
20876
|
+
}
|
|
20877
|
+
|
|
20878
|
+
// src/client/components/research-pane.tsx
|
|
20879
|
+
var React24 = __toESM(require("react"), 1);
|
|
20880
|
+
var RESEARCH_INTERVALS = ["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "8h", "12h", "1d", "3d", "1w", "1M"];
|
|
20881
|
+
var durations = [6e4, 18e4, 3e5, 9e5, 18e5, 36e5, 72e5, 144e5, 288e5, 432e5, 864e5, 2592e5, 6048e5, 26784e5];
|
|
20882
|
+
var merge2 = (a, b) => [...new Map([...a, ...b].filter((c) => Number.isFinite(c.t) && [c.o, c.h, c.l, c.c, c.v].every(Number.isFinite)).map((c) => [c.t, c])).values()].sort((a2, b2) => a2.t - b2.t).slice(-5e3);
|
|
20883
|
+
function ResearchPane({ coin, interval, onInterval, indicators, marks, link, chartType, onAnalyzeFill }) {
|
|
20884
|
+
const s = useStore(), scope = `${s.activeSessionId}:${s.status?.network}:${coin}:${interval}`;
|
|
20885
|
+
const [state, setState] = React24.useState({ scope: "", candles: [], error: "", older: "idle", at: 0 });
|
|
20886
|
+
const currentScope = React24.useRef(scope);
|
|
20887
|
+
currentScope.current = scope;
|
|
20888
|
+
const feed = state.scope === scope ? state : { scope, candles: [], error: "", older: "idle", at: 0 };
|
|
20889
|
+
React24.useEffect(() => {
|
|
20890
|
+
let alive = true, busy = false;
|
|
20891
|
+
setState({ scope, candles: [], error: "", older: "idle", at: 0 });
|
|
20892
|
+
const load = async () => {
|
|
20893
|
+
if (busy || document.hidden) return;
|
|
20894
|
+
busy = true;
|
|
20895
|
+
try {
|
|
20896
|
+
const result = await hyperCall("candles", { coin, interval });
|
|
20897
|
+
if (alive) setState((old) => ({ ...old, scope, candles: merge2(old.scope === scope ? old.candles : [], result.candles), error: "", at: Date.now() }));
|
|
20898
|
+
} catch (cause) {
|
|
20899
|
+
if (alive) setState((old) => ({ ...old, scope, error: String(cause?.message ?? cause) }));
|
|
20900
|
+
} finally {
|
|
20901
|
+
busy = false;
|
|
20902
|
+
}
|
|
20903
|
+
};
|
|
20904
|
+
void load();
|
|
20905
|
+
const timer = setInterval(() => void load(), 12e3);
|
|
20906
|
+
return () => {
|
|
20907
|
+
alive = false;
|
|
20908
|
+
clearInterval(timer);
|
|
20909
|
+
};
|
|
20910
|
+
}, [scope]);
|
|
20911
|
+
const olderBusy = React24.useRef(false);
|
|
20912
|
+
const needOlder = async () => {
|
|
20913
|
+
if (olderBusy.current || feed.older !== "idle" || !feed.candles.length) return;
|
|
20914
|
+
olderBusy.current = true;
|
|
20915
|
+
setState((old) => ({ ...old, older: "loading" }));
|
|
20916
|
+
try {
|
|
20917
|
+
const first = feed.candles[0].t;
|
|
20918
|
+
const result = await hyperCall("candles", { coin, interval, endMs: first - 1, startMs: first - durations[RESEARCH_INTERVALS.indexOf(interval)] * 500 });
|
|
20919
|
+
if (currentScope.current === scope) setState((old) => {
|
|
20920
|
+
const rows = merge2(result.candles, old.candles);
|
|
20921
|
+
return { ...old, candles: rows, older: !rows.length || rows[0].t >= first || rows.length >= 5e3 ? "done" : "idle" };
|
|
20922
|
+
});
|
|
20923
|
+
} catch (cause) {
|
|
20924
|
+
if (currentScope.current === scope) setState((old) => ({ ...old, older: "error", error: String(cause?.message ?? cause) }));
|
|
20925
|
+
} finally {
|
|
20926
|
+
olderBusy.current = false;
|
|
20927
|
+
}
|
|
20928
|
+
};
|
|
20929
|
+
const layers = { orders: true, fills: true, reasons: false, ...s.workspace?.chartLayers };
|
|
20930
|
+
const events = React24.useMemo(() => chartTradeEvents(feed.candles, coin, layers.orders ? s.tradeHistory?.orders ?? [] : [], layers.fills ? s.tradeHistory?.fills ?? [] : []), [feed.candles, s.tradeHistory, layers.orders, layers.fills, coin]);
|
|
20931
|
+
return /* @__PURE__ */ React24.createElement("section", { className: "dhl-research-pane", "aria-label": `${coin} ${interval} ${tr("\u7814\u7A76\u56FE\u8868", "research chart")}` }, /* @__PURE__ */ React24.createElement("header", null, /* @__PURE__ */ React24.createElement("b", null, coin), /* @__PURE__ */ React24.createElement(Select, { "aria-label": tr("\u7814\u7A76\u5468\u671F", "Research interval"), value: interval, onChange: (e) => onInterval(e.target.value) }, RESEARCH_INTERVALS.map((iv) => /* @__PURE__ */ React24.createElement("option", { key: iv, value: iv }, iv))), /* @__PURE__ */ React24.createElement("small", null, feed.at ? `${tr("\u66F4\u65B0", "Updated")} ${new Date(feed.at).toLocaleTimeString()}` : tr("\u52A0\u8F7D\u4E2D\u2026", "Loading\u2026"))), /* @__PURE__ */ React24.createElement(
|
|
20932
|
+
CandleChart,
|
|
20933
|
+
{
|
|
20934
|
+
key: scope,
|
|
20935
|
+
candles: feed.candles,
|
|
20936
|
+
marks,
|
|
20937
|
+
indicators,
|
|
20938
|
+
expanded: false,
|
|
20939
|
+
compact: true,
|
|
20940
|
+
readOnly: true,
|
|
20941
|
+
resetToken: 0,
|
|
20942
|
+
chartType,
|
|
20943
|
+
caption: `${coin} ${interval}`,
|
|
20944
|
+
link,
|
|
20945
|
+
indicatorSettings: s.workspace?.indicatorSettings,
|
|
20946
|
+
showDataWindow: s.workspace?.showDataWindow,
|
|
20947
|
+
maLines: s.workspace?.maLines ?? [],
|
|
20948
|
+
priceScale: s.workspace?.priceScale ?? "normal",
|
|
20949
|
+
visibleRange: s.workspace?.chartRanges?.[`${coin}:${interval}`] ?? null,
|
|
20950
|
+
onVisibleRange: (range) => saveSessionWorkspace({ chartRanges: { [`${coin}:${interval}`]: range } }),
|
|
20951
|
+
drawMode: null,
|
|
20952
|
+
onDraw: () => {
|
|
20953
|
+
},
|
|
20954
|
+
onSelect: (selection) => insertChartReference(selection, coin, interval),
|
|
20955
|
+
onNeedOlder: () => void needOlder(),
|
|
20956
|
+
olderState: feed.older,
|
|
20957
|
+
tradeEvents: events,
|
|
20958
|
+
showReasons: layers.fills && layers.reasons,
|
|
20959
|
+
onAnalyzeFill,
|
|
20960
|
+
magnet: "off",
|
|
20961
|
+
drawingsLocked: true,
|
|
20962
|
+
onEditMark: () => {
|
|
20963
|
+
},
|
|
20964
|
+
onRemoveMark: () => {
|
|
20965
|
+
},
|
|
20966
|
+
onCloneMark: () => {
|
|
20967
|
+
},
|
|
20968
|
+
selectRequest: null,
|
|
20969
|
+
focus: null
|
|
20970
|
+
}
|
|
20971
|
+
), feed.error ? /* @__PURE__ */ React24.createElement("button", { type: "button", className: "dhl-inline-error", onClick: () => setState((old) => ({ ...old, older: "idle" })) }, feed.error, " \xB7 ", tr("\u81EA\u52A8\u91CD\u8BD5\u4E2D\uFF1B\u70B9\u51FB\u91CD\u8BD5\u5386\u53F2", "Retrying; click to retry history")) : null);
|
|
20972
|
+
}
|
|
20973
|
+
|
|
20974
|
+
// lib/workspace-settings.js
|
|
20975
|
+
var INTERVALS2 = ["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "8h", "12h", "1d", "3d", "1w", "1M"];
|
|
20976
|
+
function normalizeResearch(input = {}) {
|
|
20977
|
+
const next = { layout: "single", intervals: ["1h", "4h", "1d"], syncTime: true, syncCursor: true, ...input };
|
|
20978
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !["layout", "intervals", "syncTime", "syncCursor"].includes(key3)) || !["single", "dual", "quad"].includes(next.layout) || !Array.isArray(next.intervals) || next.intervals.length !== 3 || next.intervals.some((iv) => !INTERVALS2.includes(iv)) || typeof next.syncTime !== "boolean" || typeof next.syncCursor !== "boolean") throw Error("\u591A\u5468\u671F\u7814\u7A76\u914D\u7F6E\u65E0\u6548");
|
|
20979
|
+
return next;
|
|
20980
|
+
}
|
|
20981
|
+
|
|
20982
|
+
// src/client/components/chart-navigation.tsx
|
|
20983
|
+
var React25 = __toESM(require("react"), 1);
|
|
20984
|
+
var import_dsh_client_ui_primitives5 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
20985
|
+
function ChartNavigation({ onNavigate, onCsv, disabled }) {
|
|
20986
|
+
const [date, setDate] = React25.useState("");
|
|
20987
|
+
const dateMenu = React25.useRef(null);
|
|
20988
|
+
const exportMenu = React25.useRef(null);
|
|
20989
|
+
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
20990
|
+
const go = (event) => {
|
|
20991
|
+
event.preventDefault();
|
|
20992
|
+
const time = new Date(date).getTime();
|
|
20993
|
+
if (!Number.isFinite(time)) return;
|
|
20994
|
+
onNavigate({ kind: "date", time });
|
|
20995
|
+
dateMenu.current?.removeAttribute("open");
|
|
20996
|
+
};
|
|
20997
|
+
return /* @__PURE__ */ React25.createElement("div", { className: "dhl-chart-navigation", "aria-label": tr("\u56FE\u8868\u5BFC\u822A", "Chart navigation") }, /* @__PURE__ */ React25.createElement("div", { className: "dhl-chart-ranges", role: "group", "aria-label": tr("\u663E\u793A\u65F6\u95F4\u8303\u56F4", "Visible time range") }, [["1D", 1], ["5D", 5], ["1M", 30]].map(([name, days]) => /* @__PURE__ */ React25.createElement("button", { key: name, type: "button", disabled, title: tr(`\u67E5\u770B\u6700\u8FD1 ${days} \u5929`, `Show the last ${days} days`), onClick: () => onNavigate({ kind: "range", duration: days * 864e5 }) }, name)), /* @__PURE__ */ React25.createElement("button", { type: "button", disabled, title: tr("\u9002\u5E94\u5DF2\u52A0\u8F7D\u7684\u5168\u90E8 K \u7EBF", "Fit all loaded candles"), onClick: () => onNavigate({ kind: "fit" }) }, tr("\u5168\u90E8", "All"))), /* @__PURE__ */ React25.createElement("details", { ref: dateMenu, className: "dhl-chart-footer-menu" }, /* @__PURE__ */ React25.createElement("summary", { "aria-label": tr("\u8DF3\u8F6C\u5230\u65E5\u671F", "Go to date"), title: tr("\u8DF3\u8F6C\u5230\u65E5\u671F", "Go to date") }, /* @__PURE__ */ React25.createElement(UiIcon, { name: "calendar", size: 15 })), /* @__PURE__ */ React25.createElement("form", { className: "dhl-chart-date-form", onSubmit: go }, /* @__PURE__ */ React25.createElement("label", { htmlFor: "dhl-chart-date" }, tr("\u8DF3\u8F6C\u5230\u65E5\u671F", "Go to date")), /* @__PURE__ */ React25.createElement("small", null, tr("\u672C\u5730\u65F6\u95F4", "Local time"), " \xB7 ", zone), /* @__PURE__ */ React25.createElement(import_dsh_client_ui_primitives5.Input, { id: "dhl-chart-date", type: "datetime-local", step: "60", value: date, onChange: (event) => setDate(event.target.value), required: true }), /* @__PURE__ */ React25.createElement("button", { type: "submit", disabled: disabled || !date }, tr("\u5B9A\u4F4D K \u7EBF", "Find candle")), /* @__PURE__ */ React25.createElement("small", null, tr("\u8D85\u51FA\u5DF2\u52A0\u8F7D\u8303\u56F4\u65F6\u4F1A\u8865\u53D6\u5386\u53F2\u6570\u636E\uFF0C\u53D7\u4EA4\u6613\u6240\u5386\u53F2\u4E0A\u9650\u9650\u5236\u3002", "Earlier data is loaded as needed, up to the exchange history limit.")))), /* @__PURE__ */ React25.createElement("button", { type: "button", className: "dhl-chart-live", disabled, onClick: () => onNavigate({ kind: "live" }) }, /* @__PURE__ */ React25.createElement(UiIcon, { name: "live", size: 14 }), tr("\u56DE\u5230\u6700\u65B0", "Latest")), /* @__PURE__ */ React25.createElement("span", { className: "dhl-chart-footer-spacer" }), /* @__PURE__ */ React25.createElement("details", { ref: exportMenu, className: "dhl-chart-footer-menu" }, /* @__PURE__ */ React25.createElement("summary", { "aria-label": tr("\u5BFC\u51FA\u56FE\u8868", "Export chart"), title: tr("\u5BFC\u51FA\u56FE\u8868", "Export chart") }, /* @__PURE__ */ React25.createElement(UiIcon, { name: "download", size: 15 }), tr("\u5BFC\u51FA", "Export")), /* @__PURE__ */ React25.createElement("div", { className: "dhl-chart-export-menu" }, /* @__PURE__ */ React25.createElement("button", { type: "button", disabled, onClick: () => {
|
|
20998
|
+
onNavigate({ kind: "snapshot" });
|
|
20999
|
+
exportMenu.current?.removeAttribute("open");
|
|
21000
|
+
} }, /* @__PURE__ */ React25.createElement(UiIcon, { name: "camera", size: 15 }), tr("\u56FE\u7247 PNG\uFF08\u542B\u753B\u7EBF\uFF09", "PNG image with drawings")), /* @__PURE__ */ React25.createElement("button", { type: "button", disabled, onClick: () => {
|
|
21001
|
+
onCsv();
|
|
21002
|
+
exportMenu.current?.removeAttribute("open");
|
|
21003
|
+
} }, /* @__PURE__ */ React25.createElement(UiIcon, { name: "download", size: 15 }), tr("\u5DF2\u52A0\u8F7D K \u7EBF CSV", "Loaded candles as CSV")))), /* @__PURE__ */ React25.createElement("details", { className: "dhl-chart-footer-menu" }, /* @__PURE__ */ React25.createElement("summary", { "aria-label": tr("\u56FE\u8868\u64CD\u4F5C\u5E2E\u52A9", "Chart shortcuts"), title: tr("\u56FE\u8868\u64CD\u4F5C\u5E2E\u52A9", "Chart shortcuts") }, /* @__PURE__ */ React25.createElement(UiIcon, { name: "help", size: 15 })), /* @__PURE__ */ React25.createElement("div", { className: "dhl-chart-shortcuts" }, /* @__PURE__ */ React25.createElement("strong", null, tr("\u56FE\u8868\u64CD\u4F5C", "Chart controls")), [[tr("\u79FB\u52A8 / \u7F29\u653E", "Pan / Zoom"), tr("\u62D6\u52A8 / \u6EDA\u8F6E", "Drag / Wheel")], [tr("\u64A4\u9500 / \u91CD\u505A", "Undo / Redo"), "Ctrl/\u2318 Z \xB7 Shift Ctrl/\u2318 Z"], [tr("\u590D\u5236 / \u7C98\u8D34\u753B\u7EBF", "Copy / Paste drawing"), "Ctrl/\u2318 C \xB7 Ctrl/\u2318 V"], [tr("\u5220\u9664\u9009\u4E2D\u753B\u7EBF", "Delete drawing"), "Delete / Backspace"], [tr("\u6C34\u5E73\u7EA6\u675F", "Horizontal constraint"), "Shift"], [tr("\u53D6\u6D88\u7ED8\u5236 / \u9009\u62E9", "Cancel drawing / selection"), "Esc"], [tr("\u641C\u7D22\u6807\u7684", "Search symbols"), "/"]].map(([label, key3]) => /* @__PURE__ */ React25.createElement("div", { key: label }, /* @__PURE__ */ React25.createElement("span", null, label), /* @__PURE__ */ React25.createElement("kbd", null, key3))))));
|
|
21004
|
+
}
|
|
21005
|
+
|
|
19698
21006
|
// src/client/components/desk-column.tsx
|
|
19699
21007
|
var INDICATOR_CHOICES = [
|
|
19700
21008
|
["volume", "\u6210\u4EA4\u91CF", "K \u7EBF\u4E0B\u65B9\u7684\u6210\u4EA4\u91CF\u67F1"],
|
|
@@ -19704,7 +21012,12 @@ var INDICATOR_CHOICES = [
|
|
|
19704
21012
|
["ema50", "EMA 50", "50 \u6839\u6536\u76D8\u4EF7\u7684\u6307\u6570\u5747\u7EBF"],
|
|
19705
21013
|
["bollinger20", "\u5E03\u6797\u5E26 20", "\u5747\u7EBF\u53CA\u4E0A\u4E0B\u4E24\u500D\u6807\u51C6\u5DEE"],
|
|
19706
21014
|
["rsi14", "RSI 14", "\u76F8\u5BF9\u5F3A\u5F31\u6307\u6570\u526F\u56FE"],
|
|
19707
|
-
["macd", "MACD", "12 / 26 / 9 \u52A8\u91CF\u526F\u56FE"]
|
|
21015
|
+
["macd", "MACD", "12 / 26 / 9 \u52A8\u91CF\u526F\u56FE"],
|
|
21016
|
+
["stochastic", "Stochastic", "%K \u4E0E %D \u968F\u673A\u6307\u6807"],
|
|
21017
|
+
["adx", "ADX / DMI", "\u8D8B\u52BF\u5F3A\u5EA6\u4E0E\u6B63\u8D1F\u65B9\u5411\u6307\u6807"],
|
|
21018
|
+
["obv", "OBV", "\u4EE5\u52A0\u8F7D\u7A97\u53E3\u9996\u6839\u4E3A\u96F6\u70B9\u7684\u7D2F\u79EF\u80FD\u91CF\u6F6E"],
|
|
21019
|
+
["atr14", "ATR 14", "Wilder \u5E73\u6ED1\u7684\u771F\u5B9E\u6CE2\u5E45\uFF0C\u8861\u91CF\u4EF7\u683C\u6CE2\u52A8"],
|
|
21020
|
+
["vwap", "VWAP \xB7 UTC", "UTC \u6BCF\u65E5\u91CD\u7F6E\uFF0C\u4EE5 HLC3 \u548C\u6210\u4EA4\u91CF\u8BA1\u7B97\uFF1B\u81EA\u52A8\u8865\u53D6\u5F53\u65E5 K \u7EBF"]
|
|
19708
21021
|
];
|
|
19709
21022
|
var INDICATOR_EN = {
|
|
19710
21023
|
volume: ["Volume", "Volume bars below the candles"],
|
|
@@ -19714,25 +21027,30 @@ var INDICATOR_EN = {
|
|
|
19714
21027
|
ema50: ["EMA 50", "50-close exponential moving average"],
|
|
19715
21028
|
bollinger20: ["Bollinger 20", "Mean and two standard deviation bands"],
|
|
19716
21029
|
rsi14: ["RSI 14", "Relative strength index"],
|
|
19717
|
-
macd: ["MACD", "12 / 26 / 9 momentum panel"]
|
|
21030
|
+
macd: ["MACD", "12 / 26 / 9 momentum panel"],
|
|
21031
|
+
stochastic: ["Stochastic", "%K and %D oscillator"],
|
|
21032
|
+
adx: ["ADX / DMI", "Trend strength and directional movement"],
|
|
21033
|
+
obv: ["OBV", "Cumulative signed volume; zero starts at the first loaded candle"],
|
|
21034
|
+
atr14: ["ATR 14", "Wilder average true range, a measure of volatility"],
|
|
21035
|
+
vwap: ["VWAP \xB7 UTC", "Daily UTC session, HLC3 weighted by volume; loads today\u2019s bars as needed"]
|
|
19718
21036
|
};
|
|
19719
21037
|
function usePreference(name, fallback, parse) {
|
|
19720
|
-
const
|
|
19721
|
-
const [value, setValue] =
|
|
21038
|
+
const key3 = `dsh-hyperliquid.draw.${name}`;
|
|
21039
|
+
const [value, setValue] = React26.useState(() => {
|
|
19722
21040
|
try {
|
|
19723
|
-
const raw = localStorage.getItem(
|
|
21041
|
+
const raw = localStorage.getItem(key3);
|
|
19724
21042
|
return raw == null ? fallback : parse(JSON.parse(raw));
|
|
19725
21043
|
} catch {
|
|
19726
21044
|
return fallback;
|
|
19727
21045
|
}
|
|
19728
21046
|
});
|
|
19729
|
-
const update =
|
|
21047
|
+
const update = React26.useCallback((next) => {
|
|
19730
21048
|
setValue(next);
|
|
19731
21049
|
try {
|
|
19732
|
-
localStorage.setItem(
|
|
21050
|
+
localStorage.setItem(key3, JSON.stringify(next));
|
|
19733
21051
|
} catch {
|
|
19734
21052
|
}
|
|
19735
|
-
}, [
|
|
21053
|
+
}, [key3]);
|
|
19736
21054
|
return [value, update];
|
|
19737
21055
|
}
|
|
19738
21056
|
function finite(value) {
|
|
@@ -19757,34 +21075,55 @@ function DeskColumn({ onCollapse }) {
|
|
|
19757
21075
|
const funding = finite(ctx?.funding);
|
|
19758
21076
|
const change = mark != null && mark > 0 && prev != null && prev > 0 ? (mark / prev - 1) * 100 : null;
|
|
19759
21077
|
const chartTab = s.workspaceSessionId === s.activeSessionId ? s.workspace?.chartTab ?? "chart" : "chart";
|
|
19760
|
-
const indicators =
|
|
19761
|
-
const [drawMode, setDrawMode] =
|
|
19762
|
-
const
|
|
21078
|
+
const indicators = React26.useMemo(() => new Set(s.workspaceSessionId === s.activeSessionId ? s.workspace?.indicators ?? ["volume"] : ["volume"]), [s.workspace, s.workspaceSessionId, s.activeSessionId]);
|
|
21079
|
+
const [drawMode, setDrawMode] = React26.useState(null);
|
|
21080
|
+
const [favorites, setFavorites] = usePreference("favorites", ["trend", "level", "fib", "rect", "measure", "long", "short"], (value) => Array.isArray(value) ? [...new Set(value.filter((kind) => DRAWING_TOOLS.some((tool) => tool.kind === kind)))].slice(0, 12) : []);
|
|
21081
|
+
const chartScope = `${s.activeSessionId}:${coin}:${s.interval}`;
|
|
21082
|
+
const [chartCommand, setChartCommand] = React26.useState(null);
|
|
21083
|
+
const chartType = s.workspace?.chartType ?? "candles";
|
|
21084
|
+
const studySettings = React26.useMemo(() => normalizeIndicatorSettings(s.workspace?.indicatorSettings), [s.workspace?.indicatorSettings]);
|
|
21085
|
+
const research = React26.useMemo(() => normalizeResearch(s.workspace?.research), [s.workspace?.research]);
|
|
21086
|
+
const researchScope = `${s.activeSessionId}:${s.workspaceSessionId}:${s.status?.network}:${coin}`;
|
|
21087
|
+
const [cursorEvent, setCursorEvent] = React26.useState(null);
|
|
21088
|
+
const [rangeEvent, setRangeEvent] = React26.useState(null);
|
|
21089
|
+
const makeLink = (id) => ({
|
|
21090
|
+
id,
|
|
21091
|
+
cursor: research.syncCursor && cursorEvent?.scope === researchScope ? cursorEvent.value : null,
|
|
21092
|
+
range: research.syncTime && rangeEvent?.scope === researchScope ? rangeEvent.value : null,
|
|
21093
|
+
onCursor: (time) => {
|
|
21094
|
+
if (research.syncCursor) setCursorEvent((old) => old?.scope === researchScope && old.value.source === id && old.value.time === time ? old : { scope: researchScope, value: { source: id, time, at: performance.now() } });
|
|
21095
|
+
},
|
|
21096
|
+
onRange: (range) => {
|
|
21097
|
+
if (research.syncTime) setRangeEvent({ scope: researchScope, value: { source: id, at: performance.now(), ...range } });
|
|
21098
|
+
}
|
|
21099
|
+
});
|
|
21100
|
+
const researchCount = research.layout === "quad" ? 3 : research.layout === "dual" ? 1 : 0;
|
|
21101
|
+
const drawMenu = React26.useRef(null);
|
|
19763
21102
|
const [magnet, setMagnet] = usePreference("magnet", "off", (value) => value === "weak" || value === "strong" ? value : "off");
|
|
19764
21103
|
const [stayDrawing, setStayDrawing] = usePreference("stayDrawing", false, (value) => value === true);
|
|
19765
21104
|
const [drawingsLocked, setDrawingsLocked] = usePreference("drawingsLocked", false, (value) => value === true);
|
|
19766
|
-
const [textAt, setTextAt] =
|
|
19767
|
-
const [selectRequest, setSelectRequest] =
|
|
19768
|
-
const [textValue, setTextValue] =
|
|
19769
|
-
const [confirmClear, setConfirmClear] =
|
|
19770
|
-
const [pickerOpen, setPickerOpen] =
|
|
19771
|
-
const [chartExpanded, setChartExpanded] =
|
|
19772
|
-
const [chartResetToken, setChartResetToken] =
|
|
21105
|
+
const [textAt, setTextAt] = React26.useState(null);
|
|
21106
|
+
const [selectRequest, setSelectRequest] = React26.useState(null);
|
|
21107
|
+
const [textValue, setTextValue] = React26.useState("");
|
|
21108
|
+
const [confirmClear, setConfirmClear] = React26.useState(false);
|
|
21109
|
+
const [pickerOpen, setPickerOpen] = React26.useState(false);
|
|
21110
|
+
const [chartExpanded, setChartExpanded] = React26.useState(false);
|
|
21111
|
+
const [chartResetToken, setChartResetToken] = React26.useState(0);
|
|
19773
21112
|
const sessionMaLines = s.workspaceSessionId === s.activeSessionId ? s.workspace?.maLines : void 0;
|
|
19774
|
-
const maLines =
|
|
21113
|
+
const maLines = React26.useMemo(() => sessionMaLines ?? [], [sessionMaLines]);
|
|
19775
21114
|
const toggleIndicator = (id) => {
|
|
19776
21115
|
const next = new Set(indicators);
|
|
19777
21116
|
if (next.has(id)) next.delete(id);
|
|
19778
21117
|
else next.add(id);
|
|
19779
21118
|
saveSessionWorkspace({ indicators: [...next] });
|
|
19780
21119
|
};
|
|
19781
|
-
|
|
21120
|
+
React26.useEffect(() => {
|
|
19782
21121
|
setDrawMode(null);
|
|
19783
21122
|
setChartExpanded(false);
|
|
19784
21123
|
}, [s.activeSessionId]);
|
|
19785
21124
|
const marks = s.chartMarksKey === `${s.activeSessionId ?? ""}:${coin}` ? s.chartMarks : [];
|
|
19786
21125
|
const layers = { orders: true, fills: true, drawings: true, reasons: false, ...s.workspace?.chartLayers };
|
|
19787
|
-
const tradeEvents =
|
|
21126
|
+
const tradeEvents = React26.useMemo(
|
|
19788
21127
|
() => chartTradeEvents(
|
|
19789
21128
|
s.candles,
|
|
19790
21129
|
coin,
|
|
@@ -19793,15 +21132,15 @@ function DeskColumn({ onCollapse }) {
|
|
|
19793
21132
|
),
|
|
19794
21133
|
[s.candles, coin, s.tradeHistory, layers.orders, layers.fills]
|
|
19795
21134
|
);
|
|
19796
|
-
const stayRef =
|
|
21135
|
+
const stayRef = React26.useRef(stayDrawing);
|
|
19797
21136
|
stayRef.current = stayDrawing;
|
|
19798
21137
|
const history = useDrawingHistory(`${s.activeSessionId ?? ""}:${coin}`);
|
|
19799
|
-
const historyRef =
|
|
21138
|
+
const historyRef = React26.useRef(history);
|
|
19800
21139
|
historyRef.current = history;
|
|
19801
|
-
const save =
|
|
21140
|
+
const save = React26.useCallback((mark2, label) => {
|
|
19802
21141
|
void historyRef.current.create({ ...loadStyleTemplate(mark2.kind), ...mark2, label });
|
|
19803
21142
|
}, []);
|
|
19804
|
-
const draw =
|
|
21143
|
+
const draw = React26.useCallback((mark2) => {
|
|
19805
21144
|
if (!stayRef.current) setDrawMode(null);
|
|
19806
21145
|
if ((isTextKind(mark2.kind) || mark2.kind === "icon") && "points" in mark2) {
|
|
19807
21146
|
setTextValue("");
|
|
@@ -19810,7 +21149,7 @@ function DeskColumn({ onCollapse }) {
|
|
|
19810
21149
|
}
|
|
19811
21150
|
void save(mark2, mark2.kind === "level" ? drawingLabel("level", [], mark2.price) : mark2.kind === "trend" ? drawingLabel("trend", [{ time: mark2.fromTime, price: mark2.fromPrice }]) : drawingLabel(mark2.kind, mark2.points));
|
|
19812
21151
|
}, [save]);
|
|
19813
|
-
const analyzeFill =
|
|
21152
|
+
const analyzeFill = React26.useCallback((event) => {
|
|
19814
21153
|
const state = store.get();
|
|
19815
21154
|
const known = (id) => Boolean(id && state.sessions.some((row2) => row2.sessionId === id) && !state.archivedSessionIds.includes(id));
|
|
19816
21155
|
const target = known(event.sessionId) ? event.sessionId : state.activeSessionId;
|
|
@@ -19839,21 +21178,21 @@ function DeskColumn({ onCollapse }) {
|
|
|
19839
21178
|
notify(tr("\u5DF2\u5207\u6362\u5230\u505A\u51FA\u8FD9\u7B14\u51B3\u7B56\u7684\u4F1A\u8BDD\uFF0C\u590D\u76D8\u5F15\u7528\u5DF2\u653E\u5165\u8F93\u5165\u6846", "Switched to the session that made this decision; the review is in the composer"));
|
|
19840
21179
|
}
|
|
19841
21180
|
}, []);
|
|
19842
|
-
const editMark =
|
|
19843
|
-
const erase =
|
|
19844
|
-
const cloneMark =
|
|
21181
|
+
const editMark = React26.useCallback((id, patch) => historyRef.current.edit(id, patch), []);
|
|
21182
|
+
const erase = React26.useCallback((id) => historyRef.current.remove(id), []);
|
|
21183
|
+
const cloneMark = React26.useCallback((copy) => {
|
|
19845
21184
|
void historyRef.current.create({ kind: copy.kind, label: copy.label, ...copy.geometry, ...copy.style });
|
|
19846
21185
|
}, []);
|
|
19847
|
-
|
|
21186
|
+
React26.useEffect(() => {
|
|
19848
21187
|
const onKey = (event) => {
|
|
19849
21188
|
if (!(event.metaKey || event.ctrlKey) || event.altKey) return;
|
|
19850
21189
|
const element = event.target;
|
|
19851
21190
|
if (element?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(element?.tagName ?? "")) return;
|
|
19852
|
-
const
|
|
19853
|
-
if (
|
|
21191
|
+
const key3 = event.key.toLowerCase();
|
|
21192
|
+
if (key3 === "z" && !event.shiftKey) {
|
|
19854
21193
|
event.preventDefault();
|
|
19855
21194
|
void historyRef.current.undo();
|
|
19856
|
-
} else if (
|
|
21195
|
+
} else if (key3 === "z" && event.shiftKey || key3 === "y") {
|
|
19857
21196
|
event.preventDefault();
|
|
19858
21197
|
void historyRef.current.redo();
|
|
19859
21198
|
}
|
|
@@ -19861,7 +21200,7 @@ function DeskColumn({ onCollapse }) {
|
|
|
19861
21200
|
window.addEventListener("keydown", onKey);
|
|
19862
21201
|
return () => window.removeEventListener("keydown", onKey);
|
|
19863
21202
|
}, []);
|
|
19864
|
-
|
|
21203
|
+
React26.useEffect(() => {
|
|
19865
21204
|
const onKey = (event) => {
|
|
19866
21205
|
if (event.key !== "/" || event.metaKey || event.ctrlKey || event.altKey || pickerOpen) return;
|
|
19867
21206
|
const element = event.target;
|
|
@@ -19872,10 +21211,10 @@ function DeskColumn({ onCollapse }) {
|
|
|
19872
21211
|
window.addEventListener("keydown", onKey);
|
|
19873
21212
|
return () => window.removeEventListener("keydown", onKey);
|
|
19874
21213
|
}, [pickerOpen]);
|
|
19875
|
-
|
|
21214
|
+
React26.useEffect(() => {
|
|
19876
21215
|
if (!chartExpanded) return void 0;
|
|
19877
21216
|
const onEscape = (event) => {
|
|
19878
|
-
if (event.key === "Escape" && !pickerOpen) {
|
|
21217
|
+
if (event.key === "Escape" && !pickerOpen && !event.defaultPrevented) {
|
|
19879
21218
|
event.preventDefault();
|
|
19880
21219
|
setChartExpanded(false);
|
|
19881
21220
|
}
|
|
@@ -19883,25 +21222,52 @@ function DeskColumn({ onCollapse }) {
|
|
|
19883
21222
|
window.addEventListener("keydown", onEscape);
|
|
19884
21223
|
return () => window.removeEventListener("keydown", onEscape);
|
|
19885
21224
|
}, [chartExpanded, pickerOpen]);
|
|
19886
|
-
|
|
21225
|
+
React26.useEffect(() => {
|
|
21226
|
+
const keys = { KeyT: "trend", KeyH: "level", KeyJ: "hray", KeyV: "vline", KeyC: "crossline", KeyF: "fib" };
|
|
21227
|
+
const onKey = (event) => {
|
|
21228
|
+
if (!event.altKey || event.ctrlKey || event.metaKey || chartTab !== "chart") return;
|
|
21229
|
+
const element = event.target;
|
|
21230
|
+
if (element?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(element?.tagName ?? "")) return;
|
|
21231
|
+
const kind = event.shiftKey ? event.code === "KeyR" ? "rect" : void 0 : keys[event.code];
|
|
21232
|
+
if (!kind) return;
|
|
21233
|
+
event.preventDefault();
|
|
21234
|
+
setDrawMode(kind);
|
|
21235
|
+
};
|
|
21236
|
+
window.addEventListener("keydown", onKey);
|
|
21237
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
21238
|
+
}, [chartTab]);
|
|
21239
|
+
React26.useEffect(() => {
|
|
19887
21240
|
if (!drawMode) return void 0;
|
|
19888
21241
|
const cancel = (event) => {
|
|
19889
|
-
if (event.key === "Escape")
|
|
21242
|
+
if (event.key === "Escape") {
|
|
21243
|
+
event.preventDefault();
|
|
21244
|
+
setDrawMode(null);
|
|
21245
|
+
}
|
|
19890
21246
|
};
|
|
19891
|
-
window.addEventListener("keydown", cancel);
|
|
19892
|
-
return () => window.removeEventListener("keydown", cancel);
|
|
21247
|
+
window.addEventListener("keydown", cancel, true);
|
|
21248
|
+
return () => window.removeEventListener("keydown", cancel, true);
|
|
19893
21249
|
}, [drawMode]);
|
|
19894
|
-
|
|
21250
|
+
React26.useEffect(() => {
|
|
19895
21251
|
if (chartTab === "funding" && row?.kind !== "spot" && row?.kind !== "outcome" && store.get().fundingCoin !== coin) void loadFunding();
|
|
19896
21252
|
if (chartTab === "funding" && (row?.kind === "spot" || row?.kind === "outcome")) saveSessionWorkspace({ chartTab: "chart" });
|
|
19897
21253
|
}, [chartTab, coin, row?.kind]);
|
|
19898
|
-
|
|
21254
|
+
React26.useEffect(() => {
|
|
21255
|
+
if (!indicators.has("vwap") || s.candlesOlder !== "idle" || s.candles.length < 2) return;
|
|
21256
|
+
const start = Math.floor(s.candles.at(-1).t / 864e5) * 864e5;
|
|
21257
|
+
if (s.candles[0].t > start) void loadOlderCandles();
|
|
21258
|
+
}, [indicators, s.candles, s.candlesOlder]);
|
|
21259
|
+
React26.useEffect(() => {
|
|
21260
|
+
if (s.candlesOlder !== "idle" || s.candles.length < 2) return;
|
|
21261
|
+
const earliest = Math.min(...s.chartMarks.filter((mark2) => !mark2.hidden && (mark2.kind === "vprofile" || mark2.kind === "regression")).map((mark2) => Math.min(...(mark2.points ?? []).map((point) => point.time))));
|
|
21262
|
+
if (Number.isFinite(earliest) && s.candles[0].t > earliest) void loadOlderCandles();
|
|
21263
|
+
}, [s.chartMarks, s.candles, s.candlesOlder]);
|
|
21264
|
+
React26.useEffect(() => {
|
|
19899
21265
|
void loadChartMarks();
|
|
19900
21266
|
}, [s.activeSessionId, coin, s.interval]);
|
|
19901
|
-
|
|
21267
|
+
React26.useEffect(() => {
|
|
19902
21268
|
void loadTradeHistory();
|
|
19903
21269
|
}, [s.status?.network, s.status?.tradingMode]);
|
|
19904
|
-
const changeInterval =
|
|
21270
|
+
const changeInterval = React26.useCallback((next) => {
|
|
19905
21271
|
store.patch({ interval: next });
|
|
19906
21272
|
pushDeskInterval(next);
|
|
19907
21273
|
void loadCandles();
|
|
@@ -19914,24 +21280,24 @@ function DeskColumn({ onCollapse }) {
|
|
|
19914
21280
|
[tr("\u672A\u5E73\u4ED3\u91CF", "Open interest"), openInterest == null || mark == null ? "\u2014" : `$${fmt(openInterest * mark, 0)}`],
|
|
19915
21281
|
[tr("\u8D44\u91D1\u8D39 / h", "Funding / h"), funding == null ? "\u2014" : `${(funding * 100).toFixed(4)}%`]
|
|
19916
21282
|
];
|
|
19917
|
-
return /* @__PURE__ */
|
|
19918
|
-
|
|
21283
|
+
return /* @__PURE__ */ React26.createElement("main", { id: "dhl-panel-desk", className: "dhl-col-desk" }, /* @__PURE__ */ React26.createElement("header", { className: "dhl-market-strip" }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-market-name" }, /* @__PURE__ */ React26.createElement("button", { type: "button", className: "dhl-market-trigger", "aria-haspopup": "dialog", "aria-expanded": pickerOpen, onClick: () => setPickerOpen(true) }, /* @__PURE__ */ React26.createElement(MarketIcon, { market: row ?? { name: coin, label: `${coin}-USDC`, kind: "perp" } }), /* @__PURE__ */ React26.createElement("strong", null, row?.label ?? `${coin}-USDC`), /* @__PURE__ */ React26.createElement("span", { className: "dhl-market-chevron" }, /* @__PURE__ */ React26.createElement(UiIcon, { name: "chevronDown", size: 16 }))), /* @__PURE__ */ React26.createElement("span", { className: "dhl-market-kind" }, row?.kind === "spot" ? "SPOT" : row?.kind === "hip3" ? "HIP-3" : row?.kind === "outcome" ? "OUTCOME" : "PERP"), row?.maxLeverage ? /* @__PURE__ */ React26.createElement("span", { className: "dhl-market-tag" }, row.maxLeverage, "x") : null, row?.tradable === false ? /* @__PURE__ */ React26.createElement("span", { className: "dhl-market-readonly" }, tr("\u53EA\u8BFB", "View only")) : null), /* @__PURE__ */ React26.createElement("div", { className: "dhl-market-stats" }, stats.map(([label, value, tone]) => /* @__PURE__ */ React26.createElement("div", { className: "dhl-market-stat", key: label }, /* @__PURE__ */ React26.createElement("span", null, label), /* @__PURE__ */ React26.createElement("strong", { className: tone ?? "" }, value)))), /* @__PURE__ */ React26.createElement("button", { className: "dhl-panel-iconbtn", type: "button", onClick: onCollapse, "aria-label": tr("\u6536\u8D77\u884C\u60C5\u533A", "Collapse market panel"), title: tr("\u6536\u8D77\u884C\u60C5\u533A", "Collapse market panel") }, /* @__PURE__ */ React26.createElement(FoldIcon, { direction: "right" }))), /* @__PURE__ */ React26.createElement(
|
|
21284
|
+
import_dsh_client_ui_primitives6.Modal,
|
|
19919
21285
|
{
|
|
19920
21286
|
open: Boolean(textAt),
|
|
19921
21287
|
onClose: () => setTextAt(null),
|
|
19922
21288
|
title: textAt?.kind === "icon" ? tr("\u9009\u62E9\u56FE\u6807", "Pick an icon") : textAt?.kind === "callout" ? tr("\u6DFB\u52A0\u6807\u6CE8", "Add callout") : tr("\u6DFB\u52A0\u6587\u672C", "Add text"),
|
|
19923
21289
|
closeLabel: tr("\u5173\u95ED", "Close"),
|
|
19924
21290
|
className: "dhl-rename-modal",
|
|
19925
|
-
footer: /* @__PURE__ */
|
|
21291
|
+
footer: /* @__PURE__ */ React26.createElement(React26.Fragment, null, /* @__PURE__ */ React26.createElement(import_dsh_client_ui_primitives6.Button, { variant: "outline", onClick: () => setTextAt(null) }, tr("\u53D6\u6D88", "Cancel")), /* @__PURE__ */ React26.createElement(import_dsh_client_ui_primitives6.Button, { variant: "primary", disabled: !textValue.trim(), onClick: () => {
|
|
19926
21292
|
if (textAt && textValue.trim()) void save({ kind: textAt.kind, points: textAt.points }, textValue.trim());
|
|
19927
21293
|
setTextAt(null);
|
|
19928
21294
|
} }, tr("\u6DFB\u52A0", "Add")))
|
|
19929
21295
|
},
|
|
19930
|
-
textAt?.kind === "icon" ? /* @__PURE__ */
|
|
21296
|
+
textAt?.kind === "icon" ? /* @__PURE__ */ React26.createElement(StickerPicker, { onPick: (name) => {
|
|
19931
21297
|
if (textAt) void save({ kind: "icon", points: textAt.points }, name);
|
|
19932
21298
|
setTextAt(null);
|
|
19933
|
-
} }) : /* @__PURE__ */
|
|
19934
|
-
|
|
21299
|
+
} }) : /* @__PURE__ */ React26.createElement(
|
|
21300
|
+
import_dsh_client_ui_primitives6.Input,
|
|
19935
21301
|
{
|
|
19936
21302
|
"data-modal-autofocus": true,
|
|
19937
21303
|
className: "dhl-rename-input",
|
|
@@ -19949,12 +21315,11 @@ function DeskColumn({ onCollapse }) {
|
|
|
19949
21315
|
}
|
|
19950
21316
|
}
|
|
19951
21317
|
)
|
|
19952
|
-
), pickerOpen ? /* @__PURE__ */
|
|
21318
|
+
), pickerOpen ? /* @__PURE__ */ React26.createElement(MarketPicker, { activeCoin: coin, onClose: () => setPickerOpen(false) }) : null, /* @__PURE__ */ React26.createElement("div", { className: "dhl-market-workspace" + (s.tapePlacement === "below" ? " dhl-tape-below" : "") }, /* @__PURE__ */ React26.createElement("section", { className: "dhl-chart-panel" + (chartExpanded ? " dhl-chart-panel-expanded" : ""), "aria-label": tr("\u884C\u60C5\u56FE\u8868", "Market chart") }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart-tabs", role: "tablist", "aria-label": tr("\u56FE\u8868\u5185\u5BB9", "Chart content") }, /* @__PURE__ */ React26.createElement("button", { type: "button", role: "tab", "aria-selected": chartTab === "chart", className: chartTab === "chart" ? "on" : "", onClick: () => saveSessionWorkspace({ chartTab: "chart" }) }, tr("K \u7EBF", "Chart")), row?.kind !== "spot" && row?.kind !== "outcome" ? /* @__PURE__ */ React26.createElement("button", { type: "button", role: "tab", "aria-selected": chartTab === "funding", className: chartTab === "funding" ? "on" : "", onClick: () => saveSessionWorkspace({ chartTab: "funding" }) }, tr("\u8D44\u91D1\u8D39\u5386\u53F2", "Funding history")) : null), chartTab === "chart" ? /* @__PURE__ */ React26.createElement("div", { role: "tabpanel" }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart-tools" }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-intervals", "aria-label": tr("K \u7EBF\u5468\u671F", "Chart interval") }, INTERVALS.map((iv) => /* @__PURE__ */ React26.createElement("button", { key: iv, type: "button", className: s.interval === iv ? "on" : "", onClick: () => changeInterval(iv) }, iv)), /* @__PURE__ */ React26.createElement(Select, { "aria-label": tr("\u66F4\u591A K \u7EBF\u5468\u671F", "More chart intervals"), value: MORE_INTERVALS.includes(s.interval) ? s.interval : "", onChange: (event) => changeInterval(event.target.value) }, /* @__PURE__ */ React26.createElement("option", { value: "", disabled: true }, tr("\u66F4\u591A", "More")), MORE_INTERVALS.map((iv) => /* @__PURE__ */ React26.createElement("option", { key: iv, value: iv }, iv)))), /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart-actions" }, /* @__PURE__ */ React26.createElement(Select, { "aria-label": tr("\u56FE\u8868\u7C7B\u578B", "Chart type"), value: chartType, onChange: (event) => saveSessionWorkspace({ chartType: event.target.value }) }, [["candles", "\u8721\u70DB\u56FE", "Candles"], ["bars", "\u7F8E\u56FD\u7EBF", "OHLC bars"], ["line", "\u6298\u7EBF\u56FE", "Line"], ["area", "\u9762\u79EF\u56FE", "Area"], ["heikinAshi", "\u5E73\u5747K\u7EBF", "Heikin Ashi"]].map(([value, zh, en]) => /* @__PURE__ */ React26.createElement("option", { key: value, value }, tr(zh, en)))), /* @__PURE__ */ React26.createElement("details", { className: "dhl-layer-menu" }, /* @__PURE__ */ React26.createElement("summary", null, tr("\u663E\u793A", "Show"), /* @__PURE__ */ React26.createElement(UiIcon, { name: "chevronDown", size: 12 })), /* @__PURE__ */ React26.createElement("div", { className: "dhl-layer-popover" }, [["orders", tr("\u59D4\u6258", "Orders")], ["fills", tr("\u6210\u4EA4", "Fills")], ["reasons", tr("\u6210\u4EA4\u539F\u56E0", "Fill reasons")], ["drawings", tr("\u753B\u7EBF", "Drawings")]].map(([id, label]) => /* @__PURE__ */ React26.createElement("label", { key: id }, /* @__PURE__ */ React26.createElement("input", { type: "checkbox", checked: layers[id], onChange: (event) => saveSessionWorkspace({ chartLayers: { ...layers, [id]: event.target.checked } }) }), label)))), /* @__PURE__ */ React26.createElement("details", { className: "dhl-indicator-menu" }, /* @__PURE__ */ React26.createElement("summary", { "aria-label": tr("\u9009\u62E9\u56FE\u8868\u6307\u6807", "Choose chart indicators") }, tr("\u6307\u6807", "Indicators"), " ", /* @__PURE__ */ React26.createElement("span", { className: "dhl-indicator-count" }, indicators.size + maLines.length)), /* @__PURE__ */ React26.createElement("div", { className: "dhl-indicator-popover", role: "group", "aria-label": tr("\u56FE\u8868\u6307\u6807", "Chart indicators") }, /* @__PURE__ */ React26.createElement("strong", null, tr("\u56FE\u8868\u6307\u6807", "Chart indicators")), INDICATOR_CHOICES.map(([id, label, description]) => /* @__PURE__ */ React26.createElement("button", { type: "button", key: id, className: indicators.has(id) ? "on" : "", "aria-pressed": indicators.has(id), onClick: () => toggleIndicator(id) }, /* @__PURE__ */ React26.createElement("span", { className: "dhl-indicator-check", "aria-hidden": "true" }, indicators.has(id) ? /* @__PURE__ */ React26.createElement(UiIcon, { name: "check", size: 13 }) : null), /* @__PURE__ */ React26.createElement("span", null, /* @__PURE__ */ React26.createElement("b", null, id === "rsi14" ? `RSI ${studySettings.rsi.period}` : id === "bollinger20" ? `BOLL ${studySettings.bollinger.period} \xD7 ${studySettings.bollinger.multiplier}` : tr(label, INDICATOR_EN[id]?.[0] ?? label)), /* @__PURE__ */ React26.createElement("small", null, id === "macd" ? `${studySettings.macd.fast} / ${studySettings.macd.slow} / ${studySettings.macd.signal}` : id === "bollinger20" ? tr("\u5747\u7EBF\u4E0E\u6807\u51C6\u5DEE\u901A\u9053", "Mean and standard deviation bands") : tr(description, INDICATOR_EN[id]?.[1] ?? description))))), /* @__PURE__ */ React26.createElement(IndicatorEditor, { key: s.activeSessionId, settings: s.workspace?.indicatorSettings, indicators: [...indicators], maLines, onApply: saveSessionWorkspace }), /* @__PURE__ */ React26.createElement(MaEditor, { lines: maLines, onChange: (next) => saveSessionWorkspace({ maLines: next }) }))), /* @__PURE__ */ React26.createElement("button", { type: "button", className: s.workspace?.showDataWindow ? "on" : "", "aria-pressed": s.workspace?.showDataWindow ?? false, onClick: () => saveSessionWorkspace({ showDataWindow: !s.workspace?.showDataWindow }) }, tr("\u6570\u636E\u7A97\u53E3", "Data window")), /* @__PURE__ */ React26.createElement(StrategyMenu, { candles: s.candles, coin, interval: s.interval }), /* @__PURE__ */ React26.createElement("details", { className: "dhl-draw-menu" + (drawMode ? " active" : ""), ref: drawMenu }, /* @__PURE__ */ React26.createElement("summary", { "aria-label": tr("\u753B\u7EBF\u5DE5\u5177", "Drawing tools") }, drawMode ? /* @__PURE__ */ React26.createElement(React26.Fragment, null, /* @__PURE__ */ React26.createElement(ToolIcon, { kind: drawMode, size: 14 }), tr(toolOf(drawMode).zh, toolOf(drawMode).en)) : tr("\u753B\u7EBF", "Draw"), /* @__PURE__ */ React26.createElement(UiIcon, { name: "chevronDown", size: 12 })), /* @__PURE__ */ React26.createElement("div", { className: "dhl-draw-popover", role: "menu", "aria-label": tr("\u753B\u7EBF\u5DE5\u5177", "Drawing tools") }, DRAWING_TOOLS.map((tool, index) => /* @__PURE__ */ React26.createElement(React26.Fragment, { key: tool.kind }, index > 0 && DRAWING_TOOLS[index - 1].group !== tool.group ? /* @__PURE__ */ React26.createElement("hr", null) : null, /* @__PURE__ */ React26.createElement("div", { className: "dhl-draw-tool-row" }, /* @__PURE__ */ React26.createElement(
|
|
19953
21319
|
"button",
|
|
19954
21320
|
{
|
|
19955
21321
|
type: "button",
|
|
19956
21322
|
role: "menuitemradio",
|
|
19957
|
-
key: tool.kind,
|
|
19958
21323
|
"aria-checked": drawMode === tool.kind,
|
|
19959
21324
|
className: drawMode === tool.kind ? "on" : "",
|
|
19960
21325
|
onClick: () => {
|
|
@@ -19962,9 +21327,20 @@ function DeskColumn({ onCollapse }) {
|
|
|
19962
21327
|
drawMenu.current?.removeAttribute("open");
|
|
19963
21328
|
}
|
|
19964
21329
|
},
|
|
19965
|
-
/* @__PURE__ */
|
|
19966
|
-
/* @__PURE__ */
|
|
19967
|
-
)
|
|
21330
|
+
/* @__PURE__ */ React26.createElement(ToolIcon, { kind: tool.kind }),
|
|
21331
|
+
/* @__PURE__ */ React26.createElement("span", null, /* @__PURE__ */ React26.createElement("b", null, tr(tool.zh, tool.en)), /* @__PURE__ */ React26.createElement("small", null, tr(tool.hintZh, tool.hintEn)))
|
|
21332
|
+
), /* @__PURE__ */ React26.createElement(
|
|
21333
|
+
"button",
|
|
21334
|
+
{
|
|
21335
|
+
type: "button",
|
|
21336
|
+
className: "dhl-draw-favorite",
|
|
21337
|
+
"aria-label": `${favorites.includes(tool.kind) ? tr("\u53D6\u6D88\u6536\u85CF", "Unfavorite") : tr("\u6536\u85CF", "Favorite")} ${tr(tool.zh, tool.en)}`,
|
|
21338
|
+
"aria-pressed": favorites.includes(tool.kind),
|
|
21339
|
+
disabled: !favorites.includes(tool.kind) && favorites.length >= 12,
|
|
21340
|
+
onClick: () => setFavorites(favorites.includes(tool.kind) ? favorites.filter((kind) => kind !== tool.kind) : [...favorites, tool.kind])
|
|
21341
|
+
},
|
|
21342
|
+
/* @__PURE__ */ React26.createElement(UiIcon, { name: "star", filled: favorites.includes(tool.kind), size: 14 })
|
|
21343
|
+
)))), /* @__PURE__ */ React26.createElement("div", { className: "dhl-draw-settings" }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-draw-setting" }, /* @__PURE__ */ React26.createElement("span", null, tr("\u78C1\u5438", "Magnet")), /* @__PURE__ */ React26.createElement("span", { className: "dhl-draw-seg", role: "radiogroup", "aria-label": tr("\u78C1\u5438", "Magnet") }, [["off", "\u5173", "Off"], ["weak", "\u5F31", "Weak"], ["strong", "\u5F3A", "Strong"]].map(([value, zh, en]) => /* @__PURE__ */ React26.createElement("button", { type: "button", role: "radio", key: value, "aria-checked": magnet === value, className: magnet === value ? "on" : "", onClick: () => setMagnet(value) }, tr(zh, en))))), /* @__PURE__ */ React26.createElement("label", { className: "dhl-draw-setting" }, /* @__PURE__ */ React26.createElement("span", null, tr("\u8FDE\u7EED\u7ED8\u5236", "Stay in drawing mode")), /* @__PURE__ */ React26.createElement("input", { type: "checkbox", checked: stayDrawing, onChange: (event) => setStayDrawing(event.target.checked) })), /* @__PURE__ */ React26.createElement("label", { className: "dhl-draw-setting" }, /* @__PURE__ */ React26.createElement("span", null, tr("\u9501\u5B9A\u6240\u6709\u753B\u7EBF", "Lock all drawings")), /* @__PURE__ */ React26.createElement("input", { type: "checkbox", checked: drawingsLocked, onChange: (event) => setDrawingsLocked(event.target.checked) })), /* @__PURE__ */ React26.createElement(
|
|
19968
21344
|
"button",
|
|
19969
21345
|
{
|
|
19970
21346
|
type: "button",
|
|
@@ -19982,9 +21358,22 @@ function DeskColumn({ onCollapse }) {
|
|
|
19982
21358
|
onBlur: () => setConfirmClear(false)
|
|
19983
21359
|
},
|
|
19984
21360
|
confirmClear ? tr(`\u786E\u8BA4\u5220\u9664 ${marks.filter((item) => !item.locked).length} \u6761\u753B\u7EBF`, `Delete ${marks.filter((item) => !item.locked).length} drawings`) : tr("\u5220\u9664\u6240\u6709\u753B\u7EBF", "Remove all drawings")
|
|
19985
|
-
)))), /* @__PURE__ */
|
|
21361
|
+
)))), /* @__PURE__ */ React26.createElement("button", { type: "button", className: "dhl-chart-iconbtn", disabled: !history.canUndo, "aria-label": tr("\u64A4\u9500", "Undo"), title: tr("\u64A4\u9500\u753B\u7EBF\u64CD\u4F5C\uFF08Ctrl/Cmd+Z\uFF09", "Undo drawing change (Ctrl/Cmd+Z)"), onClick: () => void history.undo() }, /* @__PURE__ */ React26.createElement("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React26.createElement("path", { d: "M9 14 4 9l5-5" }), /* @__PURE__ */ React26.createElement("path", { d: "M4 9h10a6 6 0 0 1 0 12h-3" }))), /* @__PURE__ */ React26.createElement("button", { type: "button", className: "dhl-chart-iconbtn", disabled: !history.canRedo, "aria-label": tr("\u91CD\u505A", "Redo"), title: tr("\u91CD\u505A\uFF08Shift+Ctrl/Cmd+Z\uFF09", "Redo (Shift+Ctrl/Cmd+Z)"), onClick: () => void history.redo() }, /* @__PURE__ */ React26.createElement("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React26.createElement("path", { d: "m15 14 5-5-5-5" }), /* @__PURE__ */ React26.createElement("path", { d: "M20 9H10a6 6 0 0 0 0 12h3" }))), /* @__PURE__ */ React26.createElement(Select, { "aria-label": tr("\u4EF7\u683C\u523B\u5EA6", "Price scale"), value: s.workspace?.priceScale ?? "normal", onChange: (event) => saveSessionWorkspace({ priceScale: event.target.value }) }, /* @__PURE__ */ React26.createElement("option", { value: "normal" }, tr("\u7EBF\u6027", "Linear")), /* @__PURE__ */ React26.createElement("option", { value: "log" }, tr("\u5BF9\u6570", "Log")), /* @__PURE__ */ React26.createElement("option", { value: "percentage" }, "%")), /* @__PURE__ */ React26.createElement("span", { className: "dhl-chart-action-separator", "aria-hidden": "true" }), /* @__PURE__ */ React26.createElement("button", { type: "button", className: "dhl-chart-iconbtn", "aria-label": tr("\u590D\u4F4D\u56FE\u8868\u89C6\u56FE", "Reset chart view"), title: tr("\u590D\u4F4D\u56FE\u8868\u89C6\u56FE", "Reset chart view"), onClick: () => setChartResetToken((value) => value + 1) }, /* @__PURE__ */ React26.createElement("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React26.createElement("path", { d: "M3 11a9 9 0 1 0 2.6-6.4M3 4v6h6" }), /* @__PURE__ */ React26.createElement("path", { d: "M12 7v5l3 2" }))), /* @__PURE__ */ React26.createElement("button", { type: "button", className: "dhl-chart-iconbtn", "aria-label": chartExpanded ? tr("\u9000\u51FA\u653E\u5927\u56FE\u8868", "Exit expanded chart") : tr("\u653E\u5927\u56FE\u8868", "Expand chart"), "aria-pressed": chartExpanded, title: chartExpanded ? tr("\u9000\u51FA\u653E\u5927\u56FE\u8868 (Esc)", "Exit expanded chart (Esc)") : tr("\u653E\u5927\u56FE\u8868", "Expand chart"), onClick: () => setChartExpanded((value) => !value) }, chartExpanded ? /* @__PURE__ */ React26.createElement("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React26.createElement("path", { d: "M9 3v6H3m18 6h-6v6M3 9h6V3m12 12h-6v6" })) : /* @__PURE__ */ React26.createElement("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React26.createElement("path", { d: "M9 3H3v6m12-6h6v6M3 15v6h6m12-6v6h-6" }))))), /* @__PURE__ */ React26.createElement("div", { className: "dhl-favorite-tools", role: "toolbar", "aria-label": tr("\u5E38\u7528\u753B\u7EBF", "Favorite drawing tools") }, /* @__PURE__ */ React26.createElement("button", { type: "button", "aria-label": tr("\u5149\u6807\uFF08\u53D6\u6D88\u7ED8\u5236\uFF09", "Cursor (cancel drawing)"), title: tr("\u5149\u6807 \xB7 Esc \u53D6\u6D88\u7ED8\u5236", "Cursor \xB7 Esc cancels drawing"), "aria-pressed": !drawMode, className: !drawMode ? "on" : "", onClick: () => setDrawMode(null) }, /* @__PURE__ */ React26.createElement(UiIcon, { name: "cursor", size: 16 })), /* @__PURE__ */ React26.createElement("span", { className: "dhl-chart-action-separator", "aria-hidden": "true" }), favorites.map((kind) => /* @__PURE__ */ React26.createElement("button", { key: kind, type: "button", title: tr(toolOf(kind).zh, toolOf(kind).en), "aria-label": `${tr("\u5E38\u7528", "Favorite")} ${tr(toolOf(kind).zh, toolOf(kind).en)}`, "aria-pressed": drawMode === kind, className: drawMode === kind ? "on" : "", onClick: () => setDrawMode(drawMode === kind ? null : kind) }, /* @__PURE__ */ React26.createElement(ToolIcon, { kind, size: 16 }))), /* @__PURE__ */ React26.createElement("small", null, tr("\u5728\u753B\u7EBF\u83DC\u5355\u4E2D\u6536\u85CF\u5E38\u7528\u5DE5\u5177", "Star tools in the Draw menu"))), /* @__PURE__ */ React26.createElement("div", { className: "dhl-research-controls" }, /* @__PURE__ */ React26.createElement(Select, { "aria-label": tr("\u591A\u5468\u671F\u5E03\u5C40", "Research layout"), value: research.layout, onChange: (e) => saveSessionWorkspace({ research: { ...research, layout: e.target.value } }) }, /* @__PURE__ */ React26.createElement("option", { value: "single" }, tr("\u5355\u56FE", "Single chart")), /* @__PURE__ */ React26.createElement("option", { value: "dual" }, tr("\u53CC\u56FE\u7814\u7A76", "Two charts")), /* @__PURE__ */ React26.createElement("option", { value: "quad" }, tr("\u56DB\u56FE\u7814\u7A76", "Four charts"))), /* @__PURE__ */ React26.createElement("label", null, /* @__PURE__ */ React26.createElement("input", { type: "checkbox", checked: research.syncTime, onChange: (e) => {
|
|
21362
|
+
setRangeEvent(null);
|
|
21363
|
+
saveSessionWorkspace({ research: { ...research, syncTime: e.target.checked } });
|
|
21364
|
+
} }), tr("\u65F6\u95F4\u8054\u52A8", "Sync time")), /* @__PURE__ */ React26.createElement("label", null, /* @__PURE__ */ React26.createElement("input", { type: "checkbox", checked: research.syncCursor, onChange: (e) => {
|
|
21365
|
+
setCursorEvent(null);
|
|
21366
|
+
saveSessionWorkspace({ research: { ...research, syncCursor: e.target.checked } });
|
|
21367
|
+
} }), tr("\u5149\u6807\u8054\u52A8", "Sync cursor")), researchCount > 0 ? /* @__PURE__ */ React26.createElement("button", { type: "button", className: "dhl-ghost", onClick: () => queueToAgent(tr(`\u8BF7\u7814\u7A76 ${coin} \u7684 ${[s.interval, ...research.intervals.slice(0, researchCount)].join("\u3001")} \u591A\u5468\u671F\u7ED3\u6784\u3002\u5148\u8BFB\u53D6 hyper_context \u7684\u6307\u6807\u53C2\u6570\uFF0C\u518D\u9010\u5468\u671F\u8C03\u7528 hyper_market action=indicators\uFF1B\u53EA\u5BF9\u5DF2\u6536\u76D8 K \u7EBF\u4F5C\u6BD4\u8F83\uFF0C\u8BF4\u660E\u5927\u5468\u671F\u8D8B\u52BF\u3001\u5C0F\u5468\u671F\u52A8\u91CF\u4E0E\u51B2\u7A81\uFF0C\u4EE5\u53CA\u6570\u636E\u65F6\u6548\u3002`, `Research ${coin} across ${[s.interval, ...research.intervals.slice(0, researchCount)].join(", ")}. Read hyper_context for configured indicators, then query hyper_market action=indicators for each interval. Compare closed bars, trend, momentum, conflicts and freshness.`)) }, tr("\u8BA8\u8BBA\u591A\u5468\u671F", "Discuss timeframes")) : null), /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart" }, /* @__PURE__ */ React26.createElement("div", { className: `dhl-research-grid ${research.layout}` }, /* @__PURE__ */ React26.createElement("section", { className: "dhl-research-pane dhl-primary-pane" }, researchCount > 0 ? /* @__PURE__ */ React26.createElement("header", null, /* @__PURE__ */ React26.createElement("b", null, coin, " \xB7 ", s.interval), /* @__PURE__ */ React26.createElement("small", null, tr("\u4E3B\u56FE \xB7 \u7F16\u8F91\u753B\u7EBF", "Main chart \xB7 edit drawings"))) : null, /* @__PURE__ */ React26.createElement(
|
|
19986
21368
|
CandleChart,
|
|
19987
21369
|
{
|
|
21370
|
+
link: researchCount > 0 ? makeLink("primary") : void 0,
|
|
21371
|
+
compact: researchCount > 0,
|
|
21372
|
+
indicatorSettings: s.workspace?.indicatorSettings,
|
|
21373
|
+
showDataWindow: s.workspace?.showDataWindow ?? false,
|
|
21374
|
+
chartType,
|
|
21375
|
+
command: chartCommand?.scope === chartScope ? chartCommand.command : null,
|
|
21376
|
+
caption: `${coin} ${s.interval} \xB7 ${chartType}`,
|
|
19988
21377
|
candles: s.candles,
|
|
19989
21378
|
marks: layers.drawings ? marks : [],
|
|
19990
21379
|
tradeEvents,
|
|
@@ -20004,6 +21393,9 @@ function DeskColumn({ onCollapse }) {
|
|
|
20004
21393
|
drawingsLocked,
|
|
20005
21394
|
onEditMark: (id, patch) => void editMark(id, patch),
|
|
20006
21395
|
onRemoveMark: (id) => void erase(id),
|
|
21396
|
+
onRemoveMarks: (ids) => void history.removeMany(ids),
|
|
21397
|
+
onEditMarks: (ids, patch) => void history.editMany(ids, patch),
|
|
21398
|
+
onEditEach: (patches) => void history.editEach(patches),
|
|
20007
21399
|
focus: s.chartFocus && s.chartFocus.coin === coin && s.chartFocus.interval === s.interval ? s.chartFocus : null,
|
|
20008
21400
|
maLines,
|
|
20009
21401
|
priceScale: s.workspace?.priceScale ?? "normal",
|
|
@@ -20011,7 +21403,20 @@ function DeskColumn({ onCollapse }) {
|
|
|
20011
21403
|
onVisibleRange: (range) => saveSessionWorkspace({ chartRanges: { [`${coin}:${s.interval}`]: range } }),
|
|
20012
21404
|
key: `${s.activeSessionId ?? ""}:${s.workspaceSessionId === s.activeSessionId ? "ready" : "pending"}:${coin}:${s.interval}`
|
|
20013
21405
|
}
|
|
20014
|
-
),
|
|
21406
|
+
)), research.intervals.slice(0, researchCount).map((iv, index) => /* @__PURE__ */ React26.createElement(
|
|
21407
|
+
ResearchPane,
|
|
21408
|
+
{
|
|
21409
|
+
key: `${researchScope}:${index}`,
|
|
21410
|
+
coin,
|
|
21411
|
+
interval: iv,
|
|
21412
|
+
indicators,
|
|
21413
|
+
marks: layers.drawings ? marks : [],
|
|
21414
|
+
chartType,
|
|
21415
|
+
link: makeLink(`secondary-${index}`),
|
|
21416
|
+
onAnalyzeFill: analyzeFill,
|
|
21417
|
+
onInterval: (next) => saveSessionWorkspace({ research: { ...research, intervals: research.intervals.map((value, i) => i === index ? next : value) } })
|
|
21418
|
+
}
|
|
21419
|
+
))), /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart-click-hint" }, drawMode ? `${tr(toolOf(drawMode).zh, toolOf(drawMode).en)}\uFF1A${tr(toolOf(drawMode).hintZh, toolOf(drawMode).hintEn)} \xB7 ${tr("Esc \u53D6\u6D88", "Esc to cancel")}` : tr("\u70B9\u51FB K \u7EBF\u3001\u6210\u4EA4\u91CF\u67F1\u6216\u6307\u6807\u7EBF\uFF0C\u5C06\u8BE5\u70B9\u6570\u636E\u52A0\u5165\u804A\u5929\u8F93\u5165\u6846", "Click a candle, volume bar or study line to add its data to the chat draft")), marks.length > 0 ? /* @__PURE__ */ React26.createElement(
|
|
20015
21420
|
DrawingList,
|
|
20016
21421
|
{
|
|
20017
21422
|
marks,
|
|
@@ -20020,18 +21425,25 @@ function DeskColumn({ onCollapse }) {
|
|
|
20020
21425
|
onEdit: (id, patch) => void editMark(id, patch),
|
|
20021
21426
|
onRemove: (id) => void erase(id)
|
|
20022
21427
|
}
|
|
20023
|
-
) : null,
|
|
21428
|
+
) : null, /* @__PURE__ */ React26.createElement(
|
|
21429
|
+
ChartNavigation,
|
|
21430
|
+
{
|
|
21431
|
+
disabled: s.candles.length < 2,
|
|
21432
|
+
onNavigate: (action) => setChartCommand({ scope: chartScope, command: { ...action, at: performance.now() } }),
|
|
21433
|
+
onCsv: () => downloadBlob(new Blob([candlesCsv(s.candles, coin, s.interval)], { type: "text/csv;charset=utf-8" }), `${coin.replace(/[^a-zA-Z0-9_-]/g, "_")}_${s.interval}.csv`)
|
|
21434
|
+
}
|
|
21435
|
+
), s.candlesState === "error" ? /* @__PURE__ */ React26.createElement("div", { className: "dhl-inline-error" }, tr("K \u7EBF\u6682\u65F6\u4E0D\u53EF\u7528\u3002\u5207\u6362\u5468\u671F\u53EF\u91CD\u8BD5\u3002", "Chart data is unavailable. Change interval to retry.")) : null)) : /* @__PURE__ */ React26.createElement("div", { className: "dhl-funding-panel", role: "tabpanel" }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-funding-summary" }, /* @__PURE__ */ React26.createElement("span", null, tr("\u5F53\u524D\u5C0F\u65F6\u8D44\u91D1\u8D39", "Current hourly funding")), /* @__PURE__ */ React26.createElement("strong", null, funding == null ? "\u2014" : `${(funding * 100).toFixed(4)}%`), /* @__PURE__ */ React26.createElement("span", null, tr("\u6700\u8FD1 7 \u5929 \xB7 \u6309\u5C0F\u65F6\u7ED3\u7B97", "Last 7 days \xB7 hourly settlement"))), s.fundingState === "loading" ? /* @__PURE__ */ React26.createElement("div", { className: "dhl-empty" }, tr("\u6B63\u5728\u8BFB\u53D6\u8D44\u91D1\u8D39\u5386\u53F2\u2026", "Loading funding history\u2026")) : null, s.fundingState === "error" ? /* @__PURE__ */ React26.createElement("button", { type: "button", className: "dhl-ghost", onClick: () => void loadFunding() }, tr("\u52A0\u8F7D\u5931\u8D25\uFF0C\u91CD\u8BD5", "Load failed, retry")) : null, s.fundingState === "ready" && s.fundingRows.length === 0 ? /* @__PURE__ */ React26.createElement("div", { className: "dhl-empty" }, tr("\u6682\u65E0\u8D44\u91D1\u8D39\u8BB0\u5F55", "No funding records")) : null, /* @__PURE__ */ React26.createElement("div", { className: "dhl-funding-list" }, s.fundingRows.slice(-30).reverse().map((item) => {
|
|
20024
21436
|
const rate = Number(item.fundingRate) * 100;
|
|
20025
|
-
return /* @__PURE__ */
|
|
20026
|
-
})))), /* @__PURE__ */
|
|
21437
|
+
return /* @__PURE__ */ React26.createElement("div", { className: "dhl-funding-row", key: item.time }, /* @__PURE__ */ React26.createElement("time", null, new Date(item.time).toLocaleString(tr("zh-CN", "en-US"), { hour12: false })), /* @__PURE__ */ React26.createElement("span", { className: rate >= 0 ? "dhl-up" : "dhl-down" }, `${rate >= 0 ? "+" : ""}${rate.toFixed(5)}%`));
|
|
21438
|
+
})))), /* @__PURE__ */ React26.createElement(MarketTape, { snapshot: s.snapshot })), /* @__PURE__ */ React26.createElement("div", { className: "dhl-desk-foot" }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-banner" }, /* @__PURE__ */ React26.createElement("span", { className: "dhl-banner-signal", "aria-hidden": "true" }), /* @__PURE__ */ React26.createElement("span", null, /* @__PURE__ */ React26.createElement("b", null, t("banner.title", "\u4F1A\u8BDD\u9A71\u52A8\u4EA4\u6613")), " \xB7 ", t("banner.body", "\u5728\u53F3\u4FA7\u4F1A\u8BDD\u4E2D\u8BA8\u8BBA\u884C\u60C5\u3001\u98CE\u9669\u4E0E\u4EA4\u6613\u51B3\u7B56\uFF1B\u76D8\u53E3\u4EF7\u683C\u53EF\u7528\u4E8E\u8F85\u52A9\u586B\u5199\u7968\u636E\u3002"))), /* @__PURE__ */ React26.createElement(BottomTable, { account: s.account, accountError: s.accountError })));
|
|
20027
21439
|
}
|
|
20028
21440
|
|
|
20029
21441
|
// src/client/components/chat-column.tsx
|
|
20030
|
-
var
|
|
21442
|
+
var React29 = __toESM(require("react"), 1);
|
|
20031
21443
|
|
|
20032
21444
|
// src/client/dsh-extensions.tsx
|
|
20033
|
-
var
|
|
20034
|
-
var
|
|
21445
|
+
var React27 = __toESM(require("react"), 1);
|
|
21446
|
+
var import_dsh_client_ui_primitives7 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
20035
21447
|
var HYPER_TOOLS = ["hyper_context", "hyper_market", "hyper_chart", "hyper_account", "hyper_decide", "hyper_order", "hyper_session"];
|
|
20036
21448
|
var chartSourceReady = false;
|
|
20037
21449
|
function chartReferenceChipsReady() {
|
|
@@ -20074,11 +21486,11 @@ function registerDshExtensions(ctx, slots) {
|
|
|
20074
21486
|
});
|
|
20075
21487
|
}
|
|
20076
21488
|
if (!slots) return;
|
|
20077
|
-
for (const
|
|
21489
|
+
for (const key3 of HYPER_TOOLS) {
|
|
20078
21490
|
ctx.effect(() => slots.inject("tool.call.toolview", () => slots.register(
|
|
20079
|
-
{ name: "tool.call.toolview", key:
|
|
21491
|
+
{ name: "tool.call.toolview", key: key3, registrant: "dsh-hyperliquid" },
|
|
20080
21492
|
HyperToolRow
|
|
20081
|
-
)), `dsh-hyperliquid: ${
|
|
21493
|
+
)), `dsh-hyperliquid: ${key3} row`);
|
|
20082
21494
|
}
|
|
20083
21495
|
ctx.effect(() => slots.inject("conversation.approval.detail", () => slots.register(
|
|
20084
21496
|
{ name: "conversation.approval.detail", priority: -1, registrant: "dsh-hyperliquid" },
|
|
@@ -20144,7 +21556,7 @@ function verdictOf(text) {
|
|
|
20144
21556
|
return line.length > 72 ? line.slice(0, 71) + "\u2026" : line;
|
|
20145
21557
|
}
|
|
20146
21558
|
function Mark() {
|
|
20147
|
-
return /* @__PURE__ */
|
|
21559
|
+
return /* @__PURE__ */ React27.createElement("svg", { viewBox: "0 0 200 200", width: "14", height: "14", "aria-hidden": "true" }, /* @__PURE__ */ React27.createElement("path", { d: HL_MARK_PATH, fill: "currentColor" }));
|
|
20148
21560
|
}
|
|
20149
21561
|
function HyperToolRow(props) {
|
|
20150
21562
|
const { phase, block, toolName } = props;
|
|
@@ -20155,19 +21567,19 @@ function HyperToolRow(props) {
|
|
|
20155
21567
|
const failed = phase === "result" && block?.isError === true;
|
|
20156
21568
|
const verdict = phase === "result" ? verdictOf(text) : "";
|
|
20157
21569
|
const heading = [title, summary].filter(Boolean).join(" \xB7 ");
|
|
20158
|
-
return /* @__PURE__ */
|
|
20159
|
-
|
|
21570
|
+
return /* @__PURE__ */ React27.createElement(
|
|
21571
|
+
import_dsh_client_ui_primitives7.DisclosureRow,
|
|
20160
21572
|
{
|
|
20161
|
-
icon: /* @__PURE__ */
|
|
21573
|
+
icon: /* @__PURE__ */ React27.createElement(Mark, null),
|
|
20162
21574
|
title: heading,
|
|
20163
21575
|
open: disclosure.expanded,
|
|
20164
21576
|
expandable: phase === "result" && text.length > 0,
|
|
20165
21577
|
onToggle: disclosure.toggle,
|
|
20166
21578
|
running: phase !== "result",
|
|
20167
21579
|
expandOnRowClick: true,
|
|
20168
|
-
collapsedContent: verdict ? /* @__PURE__ */
|
|
21580
|
+
collapsedContent: verdict ? /* @__PURE__ */ React27.createElement("span", { className: "dhl-tool-verdict" + (failed ? " err" : "") }, verdict) : null
|
|
20169
21581
|
},
|
|
20170
|
-
phase === "result" ? /* @__PURE__ */
|
|
21582
|
+
phase === "result" ? /* @__PURE__ */ React27.createElement("pre", { className: "dhl-tool-output" }, text) : null
|
|
20171
21583
|
);
|
|
20172
21584
|
}
|
|
20173
21585
|
function ApprovalDetail({ callId, useChat }) {
|
|
@@ -20180,12 +21592,12 @@ function ApprovalDetail({ callId, useChat }) {
|
|
|
20180
21592
|
});
|
|
20181
21593
|
if (!call) return null;
|
|
20182
21594
|
const args = parseArgs(call.argsRaw);
|
|
20183
|
-
if (typeof call.name === "string" && call.name.startsWith("hyper_")) return /* @__PURE__ */
|
|
21595
|
+
if (typeof call.name === "string" && call.name.startsWith("hyper_")) return /* @__PURE__ */ React27.createElement(HyperApprovalCard, { name: call.name, args });
|
|
20184
21596
|
return typeof args.command === "string" ? args.command : null;
|
|
20185
21597
|
}
|
|
20186
21598
|
function HyperApprovalCard({ name, args }) {
|
|
20187
|
-
const [status, setStatus] =
|
|
20188
|
-
|
|
21599
|
+
const [status, setStatus] = React27.useState(null);
|
|
21600
|
+
React27.useEffect(() => {
|
|
20189
21601
|
let live = true;
|
|
20190
21602
|
void hyperCall("status").then((value) => {
|
|
20191
21603
|
if (live) setStatus(value);
|
|
@@ -20218,11 +21630,11 @@ function HyperApprovalCard({ name, args }) {
|
|
|
20218
21630
|
if (args.reduceOnly) rows.push([tr("\u53EA\u51CF\u4ED3", "Reduce-only"), tr("\u662F", "Yes")]);
|
|
20219
21631
|
}
|
|
20220
21632
|
const limits = status?.riskLimits;
|
|
20221
|
-
return /* @__PURE__ */
|
|
21633
|
+
return /* @__PURE__ */ React27.createElement("div", { className: "dhl-approval-card" }, /* @__PURE__ */ React27.createElement("div", { className: "dhl-approval-head" }, /* @__PURE__ */ React27.createElement(Mark, null), /* @__PURE__ */ React27.createElement("strong", null, title), status ? /* @__PURE__ */ React27.createElement("span", { className: "dhl-approval-env" }, status.network === "mainnet" ? tr("\u4E3B\u7F51", "Mainnet") : tr("\u6D4B\u8BD5\u7F51", "Testnet"), " \xB7 ", status.tradingMode === "paper" ? tr("\u6A21\u62DF\u76D8", "Paper") : tr("\u5B9E\u76D8", "Live")) : null), /* @__PURE__ */ React27.createElement("dl", { className: "dhl-approval-rows" }, rows.map(([label, value, tone]) => /* @__PURE__ */ React27.createElement("div", { key: label }, /* @__PURE__ */ React27.createElement("dt", null, label), /* @__PURE__ */ React27.createElement("dd", { className: tone === "up" ? "dhl-up" : tone === "down" ? "dhl-down" : tone === "warn" ? "dhl-warn" : "" }, value)))), limits ? /* @__PURE__ */ React27.createElement("div", { className: "dhl-approval-limits" }, tr("\u4EA4\u6613\u9650\u989D", "Limits"), "\uFF1A", tr("\u5355\u7B14", "per order"), " \u2264 ", fmt(limits.maxOrderUsd), " USD \xB7 ", tr("\u4ECA\u65E5", "today"), " ", fmt(limits.usedTodayUsd ?? 0), " / ", fmt(limits.maxDailyUsd), " USD \xB7 ", tr("\u6760\u6746", "leverage"), " \u2264 ", fmt(limits.maxLeverage, 0), "x") : null, typeof args.reason === "string" && args.reason ? /* @__PURE__ */ React27.createElement("p", { className: "dhl-approval-reason" }, args.reason) : null);
|
|
20222
21634
|
}
|
|
20223
21635
|
|
|
20224
21636
|
// src/client/components/trade-ticket.tsx
|
|
20225
|
-
var
|
|
21637
|
+
var React28 = __toESM(require("react"), 1);
|
|
20226
21638
|
function TradeTicket({ onSubmitted }) {
|
|
20227
21639
|
const s = useStore();
|
|
20228
21640
|
const coin = s.activeCoin;
|
|
@@ -20234,19 +21646,19 @@ function TradeTicket({ onSubmitted }) {
|
|
|
20234
21646
|
const sharedBalance = s.account?.balanceSource === "spot";
|
|
20235
21647
|
const capacity = s.account?.tradeCapacity?.coin === coin ? s.account.tradeCapacity : null;
|
|
20236
21648
|
const available = Number(s.account?.mode === "paper" ? s.account?.availableMargin : sharedBalance ? capacity?.availableToTrade?.length === 2 ? Math.min(...capacity.availableToTrade.map(Number)) : NaN : s.account?.withdrawable);
|
|
20237
|
-
const [isBuy, setIsBuy] =
|
|
20238
|
-
const [kind, setKind] =
|
|
20239
|
-
const [margin, setMargin] =
|
|
20240
|
-
const [leverage, setLeverage] =
|
|
20241
|
-
const [size3, setSize] =
|
|
20242
|
-
const [price, setPrice] =
|
|
20243
|
-
const [pct, setPct] =
|
|
20244
|
-
const [reduceOnly, setReduceOnly] =
|
|
20245
|
-
const [withTargets, setWithTargets] =
|
|
20246
|
-
const [tp, setTp] =
|
|
20247
|
-
const [sl, setSl] =
|
|
20248
|
-
const [reason, setReason] =
|
|
20249
|
-
const [enabled, setEnabled] =
|
|
21649
|
+
const [isBuy, setIsBuy] = React28.useState(true);
|
|
21650
|
+
const [kind, setKind] = React28.useState("market");
|
|
21651
|
+
const [margin, setMargin] = React28.useState("cross");
|
|
21652
|
+
const [leverage, setLeverage] = React28.useState(2);
|
|
21653
|
+
const [size3, setSize] = React28.useState("");
|
|
21654
|
+
const [price, setPrice] = React28.useState("");
|
|
21655
|
+
const [pct, setPct] = React28.useState(0);
|
|
21656
|
+
const [reduceOnly, setReduceOnly] = React28.useState(false);
|
|
21657
|
+
const [withTargets, setWithTargets] = React28.useState(false);
|
|
21658
|
+
const [tp, setTp] = React28.useState("");
|
|
21659
|
+
const [sl, setSl] = React28.useState("");
|
|
21660
|
+
const [reason, setReason] = React28.useState("");
|
|
21661
|
+
const [enabled, setEnabled] = React28.useState(false);
|
|
20250
21662
|
const decimals = Number(market?.szDecimals ?? 5);
|
|
20251
21663
|
const maxLeverage = Math.max(1, Math.min(50, Number(market?.maxLeverage ?? 2), Number(s.status?.riskLimits?.maxLeverage ?? 2)));
|
|
20252
21664
|
const priceNum = kind === "limit" ? Number(price) : mark;
|
|
@@ -20258,20 +21670,20 @@ function TradeTicket({ onSubmitted }) {
|
|
|
20258
21670
|
const bid = Number(s.snapshot?.book?.levels?.[0]?.[0]?.px);
|
|
20259
21671
|
const ask = Number(s.snapshot?.book?.levels?.[1]?.[0]?.px);
|
|
20260
21672
|
const spreadPct = bid > 0 && ask > bid ? (ask / bid - 1) * 100 : null;
|
|
20261
|
-
|
|
21673
|
+
React28.useEffect(() => {
|
|
20262
21674
|
setSize("");
|
|
20263
21675
|
setPrice("");
|
|
20264
21676
|
setPct(0);
|
|
20265
21677
|
setEnabled(false);
|
|
20266
21678
|
}, [coin]);
|
|
20267
|
-
|
|
21679
|
+
React28.useEffect(() => {
|
|
20268
21680
|
const draft = s.ticketDraft;
|
|
20269
21681
|
if (!draft || draft.coin !== coin) return;
|
|
20270
21682
|
setIsBuy(draft.isBuy);
|
|
20271
21683
|
setKind("limit");
|
|
20272
21684
|
setPrice(draft.px);
|
|
20273
21685
|
}, [s.ticketDraft?.at, coin]);
|
|
20274
|
-
|
|
21686
|
+
React28.useEffect(() => {
|
|
20275
21687
|
if (leverage > maxLeverage) setLeverage(maxLeverage);
|
|
20276
21688
|
}, [maxLeverage, leverage]);
|
|
20277
21689
|
const setPercent = (next) => {
|
|
@@ -20344,31 +21756,36 @@ function TradeTicket({ onSubmitted }) {
|
|
|
20344
21756
|
];
|
|
20345
21757
|
if (queueToAgent(parts.filter(Boolean).join("\n"))) onSubmitted?.();
|
|
20346
21758
|
};
|
|
20347
|
-
return /* @__PURE__ */
|
|
21759
|
+
return /* @__PURE__ */ React28.createElement("section", { className: "dhl-ticket", "aria-label": tr("\u4EA4\u6613\u7968\u636E", "Trade ticket") }, /* @__PURE__ */ React28.createElement("div", { className: "dhl-ticket-mode" }, /* @__PURE__ */ React28.createElement("label", null, tr("\u4FDD\u8BC1\u91D1\u6A21\u5F0F", "Margin mode"), /* @__PURE__ */ React28.createElement(Select, { value: margin, onChange: (e) => setMargin(e.target.value) }, /* @__PURE__ */ React28.createElement("option", { value: "cross" }, tr("\u5168\u4ED3", "Cross")), /* @__PURE__ */ React28.createElement("option", { value: "isolated" }, tr("\u9010\u4ED3", "Isolated")))), /* @__PURE__ */ React28.createElement("label", null, tr("\u6760\u6746", "Leverage"), /* @__PURE__ */ React28.createElement(Select, { value: leverage, onChange: (e) => setLeverage(Number(e.target.value)) }, Array.from({ length: maxLeverage }, (_, i) => i + 1).map((v) => /* @__PURE__ */ React28.createElement("option", { key: v, value: v }, v, "x"))))), /* @__PURE__ */ React28.createElement("div", { className: "dhl-ticket-tabs", role: "tablist", "aria-label": tr("\u8BA2\u5355\u7C7B\u578B", "Order type") }, /* @__PURE__ */ React28.createElement("button", { type: "button", role: "tab", "aria-selected": kind === "market", className: kind === "market" ? "on" : "", onClick: () => setKind("market") }, tr("\u5E02\u4EF7", "Market")), /* @__PURE__ */ React28.createElement("button", { type: "button", role: "tab", "aria-selected": kind === "limit", className: kind === "limit" ? "on" : "", onClick: () => setKind("limit") }, tr("\u9650\u4EF7", "Limit"))), /* @__PURE__ */ React28.createElement("div", { className: "dhl-ticket-sides", role: "group", "aria-label": tr("\u4EA4\u6613\u65B9\u5411", "Trade side") }, /* @__PURE__ */ React28.createElement("button", { type: "button", className: isBuy ? "buy on" : "buy", "aria-pressed": isBuy, onClick: () => setIsBuy(true) }, tr("\u4E70\u5165 / \u505A\u591A", "Buy / Long")), /* @__PURE__ */ React28.createElement("button", { type: "button", className: !isBuy ? "sell on" : "sell", "aria-pressed": !isBuy, onClick: () => setIsBuy(false) }, tr("\u5356\u51FA / \u505A\u7A7A", "Sell / Short"))), /* @__PURE__ */ React28.createElement("div", { className: "dhl-ticket-balance" }, /* @__PURE__ */ React28.createElement("span", null, tr("\u53EF\u7528\u8D44\u91D1", "Available")), /* @__PURE__ */ React28.createElement("strong", null, Number.isFinite(available) ? fmt(available) : "\u2014", " USDC")), /* @__PURE__ */ React28.createElement("div", { className: "dhl-ticket-balance" }, /* @__PURE__ */ React28.createElement("span", null, tr("\u5F53\u524D\u4ED3\u4F4D", "Current position")), /* @__PURE__ */ React28.createElement("strong", null, position ? fmt(position.size, decimals) : fmt(0, decimals), " ", coin)), kind === "limit" ? /* @__PURE__ */ React28.createElement("label", { className: "dhl-ticket-field" }, /* @__PURE__ */ React28.createElement("span", null, tr("\u9650\u4EF7", "Limit price")), /* @__PURE__ */ React28.createElement("div", null, /* @__PURE__ */ React28.createElement("input", { type: "number", min: "0", value: price, onChange: (e) => setPrice(e.target.value), placeholder: fmtPrice(mark) }), /* @__PURE__ */ React28.createElement("small", null, "USDC"))) : null, /* @__PURE__ */ React28.createElement("label", { className: "dhl-ticket-field" }, /* @__PURE__ */ React28.createElement("span", null, tr("\u6570\u91CF", "Size")), /* @__PURE__ */ React28.createElement("div", null, /* @__PURE__ */ React28.createElement("input", { type: "number", min: "0", step: 10 ** -decimals, value: size3, onChange: (e) => {
|
|
20348
21760
|
setSize(e.target.value);
|
|
20349
21761
|
setPct(0);
|
|
20350
|
-
}, placeholder: "0" }), /* @__PURE__ */
|
|
21762
|
+
}, placeholder: "0" }), /* @__PURE__ */ React28.createElement("small", null, coin))), /* @__PURE__ */ React28.createElement("label", { className: "dhl-ticket-reason" }, /* @__PURE__ */ React28.createElement("span", null, tr("\u4E0B\u5355\u539F\u56E0", "Order reason")), /* @__PURE__ */ React28.createElement("textarea", { value: reason, maxLength: 1e3, onChange: (e) => setReason(e.target.value), placeholder: tr("\u5199\u4E0B\u4EA4\u6613\u4F9D\u636E\u3001\u5931\u6548\u6761\u4EF6\u4E0E\u98CE\u9669", "Record the thesis, invalidation and risk"), rows: 3 })), /* @__PURE__ */ React28.createElement("div", { className: "dhl-ticket-range" }, /* @__PURE__ */ React28.createElement("input", { type: "range", min: "0", max: "100", step: "25", value: pct, onChange: (e) => setPercent(Number(e.target.value)), "aria-label": tr("\u4F7F\u7528\u53EF\u7528\u989D\u5EA6\u767E\u5206\u6BD4", "Percent of available capacity") }), /* @__PURE__ */ React28.createElement("output", null, pct, "%")), /* @__PURE__ */ React28.createElement("label", { className: "dhl-ticket-check" }, /* @__PURE__ */ React28.createElement("input", { type: "checkbox", checked: reduceOnly, onChange: (e) => setReduceOnly(e.target.checked) }), tr("\u53EA\u51CF\u4ED3", "Reduce only")), /* @__PURE__ */ React28.createElement("label", { className: "dhl-ticket-check" }, /* @__PURE__ */ React28.createElement("input", { type: "checkbox", checked: withTargets, onChange: (e) => setWithTargets(e.target.checked) }), tr("\u6B62\u76C8 / \u6B62\u635F", "Take profit / Stop loss")), withTargets ? /* @__PURE__ */ React28.createElement("div", { className: "dhl-ticket-targets" }, /* @__PURE__ */ React28.createElement("label", null, tr("\u6B62\u76C8\u4EF7", "Take profit"), /* @__PURE__ */ React28.createElement("input", { type: "number", min: "0", value: tp, onChange: (e) => setTp(e.target.value), placeholder: tr("\u53EF\u9009", "Optional") })), /* @__PURE__ */ React28.createElement("label", null, tr("\u6B62\u635F\u4EF7", "Stop loss"), /* @__PURE__ */ React28.createElement("input", { type: "number", min: "0", value: sl, onChange: (e) => setSl(e.target.value), placeholder: tr("\u53EF\u9009", "Optional") })), /* @__PURE__ */ React28.createElement("p", { className: "dhl-ticket-validation" }, tr("\u7236\u5355\u672A\u5B8C\u5168\u6210\u4EA4\u65F6\uFF0C\u6B62\u76C8\u6B62\u635F\u5C1A\u672A\u751F\u6548\uFF1B\u90E8\u5206\u6210\u4EA4\u540E\u64A4\u9500\u7236\u5355\u4F1A\u64A4\u9500\u5B50\u5355\u3002\u63D0\u4EA4\u540E\u8BF7\u6838\u5BF9\u4ED3\u4F4D\u4E0E\u5B9E\u9645\u4FDD\u62A4\u6302\u5355\u3002\u6A21\u62DF\u76D8\u6682\u4E0D\u652F\u6301\u89E6\u53D1\u5355\u3002", "Targets are inactive until the parent fully fills. Cancelling a partially filled parent cancels its children. Verify the final position and protection orders. Paper mode does not simulate triggers."))) : null, !enabled ? /* @__PURE__ */ React28.createElement("button", { type: "button", className: "dhl-ticket-enable", onClick: () => setEnabled(true) }, tr("\u542F\u7528\u4EA4\u6613\u7968\u636E", "Enable trade ticket")) : /* @__PURE__ */ React28.createElement("button", { type: "button", className: "dhl-ticket-submit", onClick: submit, disabled: errors.length > 0 }, tr("\u63D0\u4EA4\u5230\u4EA4\u6613\u4F1A\u8BDD", "Send to trading chat")), enabled && errors.length > 0 ? /* @__PURE__ */ React28.createElement("div", { className: "dhl-ticket-validation", role: "status" }, errors[0]) : null, s.accountError ? /* @__PURE__ */ React28.createElement("div", { className: "dhl-ticket-validation", role: "status" }, tr("\u8D26\u6237\u8BFB\u53D6\u5931\u8D25", "Account read failed"), "\uFF1A", s.accountError) : null, /* @__PURE__ */ React28.createElement("div", { className: "dhl-ticket-summary" }, /* @__PURE__ */ React28.createElement("div", null, /* @__PURE__ */ React28.createElement("span", null, tr("\u8BA2\u5355\u4EF7\u503C", "Order value")), /* @__PURE__ */ React28.createElement("strong", null, orderValue > 0 ? `${fmt(orderValue)} USDC` : "\u2014")), /* @__PURE__ */ React28.createElement("div", null, /* @__PURE__ */ React28.createElement("span", null, tr("\u9884\u4F30\u4FDD\u8BC1\u91D1", "Estimated margin")), /* @__PURE__ */ React28.createElement("strong", null, orderValue > 0 ? `${fmt(orderValue / leverage)} USDC` : "\u2014")), /* @__PURE__ */ React28.createElement("div", null, /* @__PURE__ */ React28.createElement("span", null, tr("\u53C2\u8003\u4EF7\u5DEE", "Reference spread")), /* @__PURE__ */ React28.createElement("strong", null, spreadPct == null ? "\u2014" : `${spreadPct.toFixed(3)}%`)), /* @__PURE__ */ React28.createElement("div", null, /* @__PURE__ */ React28.createElement("span", null, tr("\u624B\u7EED\u8D39", "Fees")), /* @__PURE__ */ React28.createElement("strong", null, tr("\u4EA4\u6613\u6240\u7ED3\u7B97", "Charged by exchange")))), /* @__PURE__ */ React28.createElement("p", { className: "dhl-ticket-note" }, tr("\u7968\u636E\u53EA\u63D0\u4EA4\u4EA4\u6613\u610F\u56FE\u3002\u5B9E\u9645\u4EF7\u683C\u3001\u6760\u6746\u4E0E\u8D26\u6237\u98CE\u9669\u7531\u4F1A\u8BDD\u5728\u6267\u884C\u524D\u91CD\u65B0\u6838\u5BF9\u3002", "The ticket submits a trade intent. The chat rechecks price, leverage, and account risk before execution.")), s.account?.mode === "paper" ? /* @__PURE__ */ React28.createElement("p", { className: "dhl-ticket-note" }, tr(s.account.simulation, "Configured slippage and fees; depth, funding and liquidation are not simulated.")) : null);
|
|
21763
|
+
}
|
|
21764
|
+
|
|
21765
|
+
// src/client/native-session-list.ts
|
|
21766
|
+
function readNativeSessionList(list) {
|
|
21767
|
+
return list?.getSnapshot?.() ?? list?.get?.() ?? null;
|
|
20351
21768
|
}
|
|
20352
21769
|
|
|
20353
21770
|
// src/client/components/chat-column.tsx
|
|
20354
21771
|
function FixedChatView({ renderSlot }) {
|
|
20355
|
-
return /* @__PURE__ */
|
|
21772
|
+
return /* @__PURE__ */ React29.createElement(React29.Fragment, null, renderSlot("conversation.session", { view: "chat" }));
|
|
20356
21773
|
}
|
|
20357
21774
|
function ChatColumn({ width, fill = false, onCollapse, SessionProvider, renderFactorySlot }) {
|
|
20358
21775
|
const s = useStore();
|
|
20359
21776
|
const activeId = s.activeSessionId;
|
|
20360
21777
|
const nativeKnown = Boolean(activeId && s.nativeSessionIds.includes(activeId));
|
|
20361
|
-
const [bindError, setBindError] =
|
|
20362
|
-
const [ref, setRef] =
|
|
20363
|
-
const [legacy, setLegacy] =
|
|
20364
|
-
const [showTicket, setShowTicket] =
|
|
20365
|
-
|
|
21778
|
+
const [bindError, setBindError] = React29.useState(null);
|
|
21779
|
+
const [ref, setRef] = React29.useState(null);
|
|
21780
|
+
const [legacy, setLegacy] = React29.useState(false);
|
|
21781
|
+
const [showTicket, setShowTicket] = React29.useState(false);
|
|
21782
|
+
React29.useEffect(() => {
|
|
20366
21783
|
if (s.ticketDraft) setShowTicket(true);
|
|
20367
21784
|
}, [s.ticketDraft?.at]);
|
|
20368
|
-
|
|
21785
|
+
React29.useEffect(() => {
|
|
20369
21786
|
setShowTicket(false);
|
|
20370
21787
|
}, [s.activeSessionId]);
|
|
20371
|
-
|
|
21788
|
+
React29.useEffect(() => {
|
|
20372
21789
|
setActiveRef(null);
|
|
20373
21790
|
setRef(null);
|
|
20374
21791
|
setLegacy(false);
|
|
@@ -20380,7 +21797,7 @@ function ChatColumn({ width, fill = false, onCollapse, SessionProvider, renderFa
|
|
|
20380
21797
|
try {
|
|
20381
21798
|
const sessionsSvc = getContext()?.get("sessions");
|
|
20382
21799
|
if (!sessionsSvc) throw new Error(tr("sessions \u670D\u52A1\u4E0D\u53EF\u7528", "DSH sessions service unavailable"));
|
|
20383
|
-
const nativeList = sessionsSvc.list
|
|
21800
|
+
const nativeList = readNativeSessionList(sessionsSvc.list);
|
|
20384
21801
|
if (nativeList && !nativeList.byId?.[activeId]) {
|
|
20385
21802
|
if (nativeList.phase === "ready") throw new Error(tr("\u8BE5\u4EA4\u6613\u4F1A\u8BDD\u5DF2\u4E0D\u5728 DSH \u4F1A\u8BDD\u5217\u8868\u4E2D", "This trading session no longer exists in DSH"));
|
|
20386
21803
|
return;
|
|
@@ -20417,17 +21834,17 @@ function ChatColumn({ width, fill = false, onCollapse, SessionProvider, renderFa
|
|
|
20417
21834
|
}
|
|
20418
21835
|
};
|
|
20419
21836
|
}, [activeId, nativeKnown, s.nativeSessionsReady]);
|
|
20420
|
-
const title =
|
|
21837
|
+
const title = React29.useMemo(() => {
|
|
20421
21838
|
if (!activeId) return "";
|
|
20422
|
-
const row = getContext()?.get("sessions")?.list
|
|
21839
|
+
const row = readNativeSessionList(getContext()?.get("sessions")?.list)?.byId?.[activeId];
|
|
20423
21840
|
return s.sessionTitles?.[activeId] ?? row?.displayTitle ?? s.sessions.find((r) => r.sessionId === activeId)?.title ?? t("session.defaultTitle", "Hyperliquid \u4EA4\u6613\u4F1A\u8BDD");
|
|
20424
21841
|
}, [activeId, s.sessions, s.sessionTitles, s.localeRevision]);
|
|
20425
21842
|
const readyRef = ref?.sessionId === activeId ? ref : null;
|
|
20426
21843
|
const pendingQuotes = s.chartReferences.filter((item) => item.sessionId === activeId);
|
|
20427
|
-
|
|
21844
|
+
React29.useEffect(() => {
|
|
20428
21845
|
if (pendingQuotes.length && showTicket) setShowTicket(false);
|
|
20429
21846
|
}, [pendingQuotes.length, showTicket]);
|
|
20430
|
-
|
|
21847
|
+
React29.useEffect(() => {
|
|
20431
21848
|
if (!pendingQuotes.length || !readyRef || showTicket) return;
|
|
20432
21849
|
const sessionCtx = readyRef.binding?.ctx;
|
|
20433
21850
|
const input = sessionCtx ? getContext()?.get("conversation")?.input?.for?.(sessionCtx) : null;
|
|
@@ -20457,25 +21874,25 @@ function ChatColumn({ width, fill = false, onCollapse, SessionProvider, renderFa
|
|
|
20457
21874
|
requestAnimationFrame(() => input.focus?.());
|
|
20458
21875
|
}
|
|
20459
21876
|
}, [activeId, readyRef, showTicket, s.chartReferences]);
|
|
20460
|
-
const askMarket =
|
|
21877
|
+
const askMarket = React29.useCallback(() => {
|
|
20461
21878
|
queueToAgent(tr(
|
|
20462
21879
|
`\u8BF7\u8C03\u7528 hyper_context \u8BFB\u53D6\u5F53\u524D\u4EA4\u6613\u53F0\u7684 ${s.activeCoin} \u5B9E\u65F6\u884C\u60C5\u3001\u76D8\u53E3\u548C\u8D26\u6237\u4FE1\u606F\uFF0C\u6982\u62EC\u5E02\u573A\u72B6\u6001\u3001\u5173\u952E\u98CE\u9669\u4E0E\u53EF\u80FD\u7684\u4EA4\u6613\u6761\u4EF6\u3002\u4EC5\u5206\u6790\uFF0C\u4E0D\u8981\u4E0B\u5355\u3002`,
|
|
20463
21880
|
`Use hyper_context to read live ${s.activeCoin} market data, order book and account state. Summarize conditions, key risks and possible trade criteria. Analysis only; do not place an order.`
|
|
20464
21881
|
));
|
|
20465
21882
|
}, [s.activeCoin]);
|
|
20466
|
-
const askRisk =
|
|
21883
|
+
const askRisk = React29.useCallback(() => {
|
|
20467
21884
|
queueToAgent(tr(
|
|
20468
21885
|
`\u8BF7\u8C03\u7528 hyper_context \u68C0\u67E5\u5F53\u524D\u4EA4\u6613\u4F1A\u8BDD\u7684\u8D26\u6237\u3001${s.activeCoin} \u4ED3\u4F4D\u3001\u6760\u6746\u3001\u6302\u5355\u548C\u98CE\u9669\u9650\u989D\uFF0C\u6307\u51FA\u9700\u8981\u6CE8\u610F\u7684\u98CE\u9669\u4E0E\u53EF\u91C7\u53D6\u7684\u7BA1\u7406\u52A8\u4F5C\u3002\u4EC5\u5206\u6790\uFF0C\u4E0D\u8981\u4E0B\u5355\u6216\u64A4\u5355\u3002`,
|
|
20469
21886
|
`Use hyper_context to review the account, ${s.activeCoin} position, leverage, open orders and risk limits. Identify risks and possible management actions. Analysis only; do not place or cancel orders.`
|
|
20470
21887
|
));
|
|
20471
21888
|
}, [s.activeCoin]);
|
|
20472
|
-
const content = !activeId ? /* @__PURE__ */
|
|
20473
|
-
return /* @__PURE__ */
|
|
21889
|
+
const content = !activeId ? /* @__PURE__ */ React29.createElement("div", { className: "dhl-empty" }, t("chat.noSession", "\u9009\u62E9\u5DE6\u4FA7\u4EA4\u6613\u4F1A\u8BDD\uFF0C\u5728\u8FD9\u91CC\u76F4\u63A5\u4E0E\u4EA4\u6613 agent \u5BF9\u8BDD\u3002")) : bindError ? /* @__PURE__ */ React29.createElement("div", { className: "dhl-inline-error", role: "alert" }, t("chat.bindError", "\u4F1A\u8BDD\u7ED1\u5B9A\u5931\u8D25\uFF1A") + bindError) : legacy ? /* @__PURE__ */ React29.createElement("div", { className: "dhl-chat-unavailable", role: "alert" }, t("chat.upgrade", "\u5F53\u524D DSH \u7248\u672C\u4E0D\u652F\u6301\u5D4C\u5165\u539F\u751F\u4F1A\u8BDD\uFF1B\u8BF7\u4F7F\u7528 DSH 0.2 \u6216\u66F4\u65B0\u7248\u672C\u3002")) : !readyRef ? /* @__PURE__ */ React29.createElement("div", { className: "dhl-empty" }, t("chat.linking", "\u6B63\u5728\u63A5\u5165\u4F1A\u8BDD\u2026")) : !SessionProvider || !renderFactorySlot ? /* @__PURE__ */ React29.createElement("div", { className: "dhl-inline-error", role: "alert" }, t("chat.noFactory", "DSH \u539F\u751F\u4F1A\u8BDD\u7EC4\u4EF6\u4E0D\u53EF\u7528\u3002")) : /* @__PURE__ */ React29.createElement("div", { className: "dhl-chatmount", "data-trading-session": activeId }, /* @__PURE__ */ React29.createElement(SessionProvider, { session: readyRef }, renderFactorySlot("conversation.content", { variant: "embedded", phase: "active", hero: false }, { slots: { views: FixedChatView } })));
|
|
21890
|
+
return /* @__PURE__ */ React29.createElement("div", { id: "dhl-panel-chat", className: "dhl-col-chat", style: fill ? { flex: "1 1 0", width: "auto" } : width ? { width } : void 0 }, /* @__PURE__ */ React29.createElement("div", { className: "dhl-chat-header" }, /* @__PURE__ */ React29.createElement("div", { className: "dhl-chat-heading" }, /* @__PURE__ */ React29.createElement("strong", null, t("chat.title", "\u4EA4\u6613\u4F1A\u8BDD")), activeId ? /* @__PURE__ */ React29.createElement("span", { title }, title) : null), /* @__PURE__ */ React29.createElement("button", { type: "button", className: "dhl-chat-ticket-toggle", "aria-expanded": showTicket, onClick: () => setShowTicket((value) => !value) }, showTicket ? t("chat.back", "\u8FD4\u56DE\u4F1A\u8BDD") : t("chat.ticket", "\u4EA4\u6613\u7968\u636E")), /* @__PURE__ */ React29.createElement("button", { className: "dhl-panel-iconbtn", type: "button", onClick: onCollapse, "aria-label": tr("\u6536\u8D77\u5BF9\u8BDD\u680F", "Collapse chat"), title: tr("\u6536\u8D77\u5BF9\u8BDD\u680F", "Collapse chat") }, /* @__PURE__ */ React29.createElement(FoldIcon, { direction: "right" }))), activeId && !showTicket && !legacy ? /* @__PURE__ */ React29.createElement("div", { className: "dhl-chat-prompts", role: "group", "aria-label": t("chat.prompts", "\u5FEB\u6377\u63D0\u95EE") }, /* @__PURE__ */ React29.createElement("button", { type: "button", onClick: askMarket }, t("chat.analyze", "\u5206\u6790\u884C\u60C5")), /* @__PURE__ */ React29.createElement("button", { type: "button", onClick: askRisk }, t("chat.risk", "\u68C0\u67E5\u98CE\u9669"))) : null, showTicket ? /* @__PURE__ */ React29.createElement(TradeTicket, { key: activeId ?? "anonymous", onSubmitted: () => setShowTicket(false) }) : content);
|
|
20474
21891
|
}
|
|
20475
21892
|
|
|
20476
21893
|
// src/client/components/wallet-details.tsx
|
|
20477
|
-
var
|
|
20478
|
-
var
|
|
21894
|
+
var React30 = __toESM(require("react"), 1);
|
|
21895
|
+
var import_dsh_client_ui_primitives8 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
20479
21896
|
var PERIODS = [
|
|
20480
21897
|
["day", "24 \u5C0F\u65F6", "24h"],
|
|
20481
21898
|
["week", "7 \u5929", "7d"],
|
|
@@ -20488,15 +21905,15 @@ function ProfitLine({ points: points2 }) {
|
|
|
20488
21905
|
const min2 = Math.min(...values);
|
|
20489
21906
|
const span = Math.max(...values) - min2 || 1;
|
|
20490
21907
|
const path = values.map((value, index) => `${index ? "L" : "M"}${(index / (values.length - 1) * 100).toFixed(2)},${(40 - (value - min2) / span * 36).toFixed(2)}`).join(" ");
|
|
20491
|
-
return /* @__PURE__ */
|
|
21908
|
+
return /* @__PURE__ */ React30.createElement("svg", { className: "dhl-wallet-graph", viewBox: "0 0 100 44", preserveAspectRatio: "none", role: "img", "aria-label": tr("\u6536\u76CA\u8D70\u52BF", "P&L trend") }, /* @__PURE__ */ React30.createElement("path", { d: path, fill: "none", stroke: "currentColor", strokeWidth: "1.5", vectorEffect: "non-scaling-stroke" }));
|
|
20492
21909
|
}
|
|
20493
21910
|
function WalletDetails({ open, onClose }) {
|
|
20494
21911
|
const s = useStore();
|
|
20495
|
-
const [period, setPeriod] =
|
|
20496
|
-
const [portfolio, setPortfolio] =
|
|
20497
|
-
const [error, setError] =
|
|
20498
|
-
const [loading, setLoading] =
|
|
20499
|
-
|
|
21912
|
+
const [period, setPeriod] = React30.useState("day");
|
|
21913
|
+
const [portfolio, setPortfolio] = React30.useState(null);
|
|
21914
|
+
const [error, setError] = React30.useState(null);
|
|
21915
|
+
const [loading, setLoading] = React30.useState(false);
|
|
21916
|
+
React30.useEffect(() => {
|
|
20500
21917
|
if (!open) return void 0;
|
|
20501
21918
|
let active = true;
|
|
20502
21919
|
setPortfolio(null);
|
|
@@ -20521,7 +21938,8 @@ function WalletDetails({ open, onClose }) {
|
|
|
20521
21938
|
const stats = account?.mode === "paper" ? [
|
|
20522
21939
|
[tr("\u8D26\u6237\u6743\u76CA", "Equity"), `${fmt(account.equity)} USDC`],
|
|
20523
21940
|
[tr("\u53EF\u7528\u4FDD\u8BC1\u91D1", "Available margin"), `${fmt(account.availableMargin)} USDC`],
|
|
20524
|
-
[tr("\
|
|
21941
|
+
[tr("\u4FDD\u7559\u6210\u4EA4\u6BDB\u76C8\u4E8F", "Retained fills gross P&L"), `${fmt(portfolio?.realizedPnl)} USDC`],
|
|
21942
|
+
[tr("\u4FDD\u7559\u6210\u4EA4\u6263\u8D39\u51C0\u989D", "Retained fills net after fees"), portfolio?.netRealizedPnl == null ? "\u2014" : `${fmt(portfolio.netRealizedPnl)} USDC`],
|
|
20525
21943
|
[tr("\u672A\u5B9E\u73B0\u6536\u76CA", "Unrealized P&L"), `${fmt(account.uPnl)} USDC`]
|
|
20526
21944
|
] : [
|
|
20527
21945
|
[shared ? tr("USDC \u4F59\u989D", "USDC balance") : tr("\u6C38\u7EED\u8D26\u6237\u4EF7\u503C", "Perp account value"), `${fmt(shared ? account?.usdcBalance : account?.marginSummary?.accountValue)} USDC`],
|
|
@@ -20529,12 +21947,12 @@ function WalletDetails({ open, onClose }) {
|
|
|
20529
21947
|
[tr("\u5F53\u524D\u4ED3\u4F4D\u672A\u5B9E\u73B0\u6536\u76CA", "Open position P&L"), `${fmt((account?.positions ?? []).reduce((sum, p) => sum + Number(p.uPnl ?? 0), 0))} USDC`],
|
|
20530
21948
|
[tr("\u4EA4\u6613\u6240\u7EC4\u5408\u8D26\u6237\u4EF7\u503C", "Exchange portfolio value"), Number.isFinite(value) ? `${fmt(value)} USDC` : "\u2014"]
|
|
20531
21949
|
];
|
|
20532
|
-
return /* @__PURE__ */
|
|
21950
|
+
return /* @__PURE__ */ React30.createElement(import_dsh_client_ui_primitives8.Modal, { open, onClose, title: tr("\u94B1\u5305\u8BE6\u60C5\u4E0E\u6536\u76CA", "Wallet and P&L"), closeLabel: tr("\u5173\u95ED", "Close"), className: "dhl-wallet-modal" }, /* @__PURE__ */ React30.createElement("div", { className: "dhl-wallet-address" }, s.status?.address ?? tr("\u6A21\u62DF\u8D26\u6237 \xB7 \u672C\u5730\u8D26\u672C", "Paper account \xB7 local ledger")), /* @__PURE__ */ React30.createElement("div", { className: "dhl-wallet-stats" }, stats.map(([label, amount]) => /* @__PURE__ */ React30.createElement("div", { key: label }, /* @__PURE__ */ React30.createElement("span", null, label), /* @__PURE__ */ React30.createElement("strong", null, amount)))), account?.mode === "live" ? /* @__PURE__ */ React30.createElement(React30.Fragment, null, /* @__PURE__ */ React30.createElement("div", { className: "dhl-wallet-periods" }, PERIODS.map(([id, zh, en]) => /* @__PURE__ */ React30.createElement("button", { type: "button", key: id, className: period === id ? "on" : "", onClick: () => setPeriod(id) }, tr(zh, en)))), /* @__PURE__ */ React30.createElement("div", { className: "dhl-wallet-pnl" }, /* @__PURE__ */ React30.createElement("span", null, tr("\u671F\u95F4\u6536\u76CA", "Period P&L")), /* @__PURE__ */ React30.createElement("strong", { className: pnl >= 0 ? "dhl-up" : "dhl-down" }, Number.isFinite(pnl) ? `${pnl >= 0 ? "+" : ""}${fmt(pnl)} USDC` : "\u2014")), /* @__PURE__ */ React30.createElement(ProfitLine, { points: sample?.pnlHistory ?? [] }), /* @__PURE__ */ React30.createElement("p", { className: "dhl-ticket-note" }, tr("\u6536\u76CA\u66F2\u7EBF\u4F7F\u7528 Hyperliquid \u7EC4\u5408\u8D26\u6237\u6837\u672C\uFF08\u542B\u5165\u51FA\u91D1\u8C03\u6574\uFF09\uFF1B\u4E0E\u5F53\u524D\u53EF\u4EA4\u6613\u4F59\u989D\u53E3\u5F84\u4E0D\u540C\u3002", "P&L uses Hyperliquid portfolio samples adjusted for deposits and withdrawals; it differs from tradable balance."))) : /* @__PURE__ */ React30.createElement("p", { className: "dhl-ticket-note" }, tr("\u6A21\u62DF\u6743\u76CA\u5DF2\u6263\u9664\u65B0\u6210\u4EA4\u7684\u914D\u7F6E\u624B\u7EED\u8D39\uFF1B\u5386\u53F2\u7EDF\u8BA1\u4EC5\u8986\u76D6\u4FDD\u7559\u6210\u4EA4\uFF0C\u65E7\u8BB0\u5F55\u8D39\u7528\u53EF\u80FD\u672A\u77E5\u3002\u672A\u6A21\u62DF\u8D44\u91D1\u8D39\u53CA\u5F3A\u5E73\u3002", "Paper equity deducts configured fees on new fills. History totals cover retained fills only; legacy fees may be unknown. No funding or liquidation simulation.")), loading ? /* @__PURE__ */ React30.createElement("div", { className: "dhl-empty" }, tr("\u6B63\u5728\u8BFB\u53D6\u6536\u76CA\u2026", "Loading P&L\u2026")) : null, error ? /* @__PURE__ */ React30.createElement("div", { className: "dhl-inline-error", role: "status" }, error) : null);
|
|
20533
21951
|
}
|
|
20534
21952
|
|
|
20535
21953
|
// src/client/components/trading-limits.tsx
|
|
20536
|
-
var
|
|
20537
|
-
var
|
|
21954
|
+
var React31 = __toESM(require("react"), 1);
|
|
21955
|
+
var import_dsh_client_ui_primitives9 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
20538
21956
|
var EMPTY = { allowedCoins: "", maxOrderUsd: "", maxDailyUsd: "", maxPositionUsd: "", maxLeverage: "" };
|
|
20539
21957
|
function fromLimits(limits) {
|
|
20540
21958
|
return {
|
|
@@ -20549,16 +21967,20 @@ function TradingLimits({ open, onClose }) {
|
|
|
20549
21967
|
const st = useStore().status;
|
|
20550
21968
|
const limits = st?.riskLimits;
|
|
20551
21969
|
const paper = st?.tradingMode === "paper";
|
|
20552
|
-
const [draft, setDraft] =
|
|
20553
|
-
const [busy, setBusy] =
|
|
20554
|
-
const [error, setError] =
|
|
20555
|
-
|
|
21970
|
+
const [draft, setDraft] = React31.useState(EMPTY);
|
|
21971
|
+
const [busy, setBusy] = React31.useState(false);
|
|
21972
|
+
const [error, setError] = React31.useState("");
|
|
21973
|
+
const [costs, setCosts] = React31.useState({ feeBps: "4.5", slippageBps: "5" });
|
|
21974
|
+
React31.useEffect(() => {
|
|
21975
|
+
if (open) setCosts({ feeBps: String(st?.paperCosts?.feeBps ?? 4.5), slippageBps: String(st?.paperCosts?.slippageBps ?? 5) });
|
|
21976
|
+
}, [open]);
|
|
21977
|
+
React31.useEffect(() => {
|
|
20556
21978
|
if (open && limits) {
|
|
20557
21979
|
setDraft(fromLimits(limits));
|
|
20558
21980
|
setError("");
|
|
20559
21981
|
}
|
|
20560
21982
|
}, [open, Boolean(limits)]);
|
|
20561
|
-
const change = (
|
|
21983
|
+
const change = (key3, value) => setDraft((current2) => ({ ...current2, [key3]: value }));
|
|
20562
21984
|
const save = async (event) => {
|
|
20563
21985
|
event?.preventDefault();
|
|
20564
21986
|
setBusy(true);
|
|
@@ -20593,18 +22015,22 @@ function TradingLimits({ open, onClose }) {
|
|
|
20593
22015
|
setBusy(false);
|
|
20594
22016
|
}
|
|
20595
22017
|
};
|
|
20596
|
-
const field = (
|
|
20597
|
-
const footer = limits ? /* @__PURE__ */
|
|
20598
|
-
return /* @__PURE__ */
|
|
22018
|
+
const field = (key3, label, props, wide = false) => /* @__PURE__ */ React31.createElement("label", { className: wide ? "dhl-risk-wide" : void 0 }, /* @__PURE__ */ React31.createElement("span", null, label), /* @__PURE__ */ React31.createElement(import_dsh_client_ui_primitives9.Input, { className: "dhl-risk-input", value: draft[key3], onChange: (event) => change(key3, event.target.value), required: true, ...props }));
|
|
22019
|
+
const footer = limits ? /* @__PURE__ */ React31.createElement(React31.Fragment, null, st?.riskSettingsSource === "desk" ? /* @__PURE__ */ React31.createElement(import_dsh_client_ui_primitives9.Button, { variant: "outline", disabled: busy, onClick: () => void reset() }, tr("\u6062\u590D\u63D2\u4EF6\u914D\u7F6E", "Restore plugin config")) : null, /* @__PURE__ */ React31.createElement(import_dsh_client_ui_primitives9.Button, { variant: "primary", type: "submit", form: "dhl-risk-form", disabled: busy }, busy ? tr("\u4FDD\u5B58\u4E2D\u2026", "Saving\u2026") : tr("\u4FDD\u5B58", "Save"))) : null;
|
|
22020
|
+
return /* @__PURE__ */ React31.createElement(import_dsh_client_ui_primitives9.Modal, { open, onClose, title: tr("\u4EA4\u6613\u9650\u989D\u4E0E\u7B7E\u540D\u89C4\u5219", "Trading limits & signing rules"), closeLabel: tr("\u5173\u95ED", "Close"), className: "dhl-limits-modal", footer }, /* @__PURE__ */ React31.createElement("section", { className: "dhl-limits-section" }, /* @__PURE__ */ React31.createElement("h3", null, tr("\u4EA4\u6613\u9650\u989D", "Trading limits"), /* @__PURE__ */ React31.createElement("span", { className: "dhl-limits-ledger" }, paper ? tr("\u6A21\u62DF\u76D8\u8D26\u672C", "Paper ledger") : tr("\u5B9E\u76D8\u8D26\u672C", "Live ledger"))), limits ? /* @__PURE__ */ React31.createElement("form", { id: "dhl-risk-form", className: "dhl-risk-form", onSubmit: (event) => void save(event) }, field("allowedCoins", tr("\u5141\u8BB8\u5F00\u4ED3\u5E01\u5BF9", "Coins allowed to open"), { placeholder: "BTC, ETH", maxLength: 1e3 }, true), field("maxOrderUsd", tr("\u5355\u7B14\u540D\u4E49\u4EF7\u503C\u4E0A\u9650 \xB7 USD", "Max per order \xB7 USD"), { type: "number", min: "0.01", step: "any" }), field("maxDailyUsd", tr("\u6BCF\u65E5\u5F00\u4ED3\u989D\u5EA6 \xB7 USD", "Daily opening budget \xB7 USD"), { type: "number", min: "0.01", step: "any" }), field("maxPositionUsd", tr("\u5355\u5E01\u4ED3\u4F4D\u4E0A\u9650 \xB7 USD", "Max position per coin \xB7 USD"), { type: "number", min: "0.01", step: "any" }), field("maxLeverage", tr("\u6700\u5927\u6760\u6746 \xB7 \u500D", "Max leverage \xB7 x"), { type: "number", min: "1", max: "50", step: "1" }), /* @__PURE__ */ React31.createElement("p", { className: "dhl-risk-budget" }, tr("\u4ECA\u65E5\u5DF2\u7528\u989D\u5EA6\uFF08UTC\uFF09", "Used today (UTC)"), "\uFF1A", fmt(limits.usedTodayUsd ?? 0), " / ", fmt(limits.maxDailyUsd), " USD"), error ? /* @__PURE__ */ React31.createElement("p", { className: "dhl-risk-error", role: "alert" }, error) : null) : /* @__PURE__ */ React31.createElement("div", { className: "dhl-empty" }, tr("\u6B63\u5728\u8BFB\u53D6\u2026", "Loading\u2026")), /* @__PURE__ */ React31.createElement("p", { className: "dhl-limits-note" }, tr(
|
|
20599
22021
|
"\u4FDD\u5B58\u540E\u7ACB\u5373\u4F5C\u7528\u4E8E\u6A21\u62DF\u76D8\u548C\u5B9E\u76D8\uFF0C\u91CD\u542F\u540E\u7EE7\u7EED\u751F\u6548\u3002\u4E24\u79CD\u6A21\u5F0F\u7684\u6BCF\u65E5\u989D\u5EA6\u5206\u5F00\u8BB0\u8D26\uFF1B\u51CF\u4ED3\u548C\u5E73\u4ED3\u4E0D\u53D7\u5F00\u4ED3\u9650\u989D\u963B\u6B62\u3002",
|
|
20600
22022
|
"Changes apply immediately to paper and live trading and survive restart. Each mode has its own daily budget; reducing and closing remain available."
|
|
20601
|
-
))), /* @__PURE__ */
|
|
22023
|
+
))), /* @__PURE__ */ React31.createElement("section", { className: "dhl-limits-section" }, /* @__PURE__ */ React31.createElement("h3", null, tr("\u6A21\u62DF\u6210\u4EA4\u6210\u672C", "Paper execution costs")), /* @__PURE__ */ React31.createElement("div", { className: "dhl-risk-form" }, ["feeBps", "slippageBps"].map((key3) => /* @__PURE__ */ React31.createElement("label", { key: key3 }, /* @__PURE__ */ React31.createElement("span", null, key3 === "feeBps" ? tr("\u624B\u7EED\u8D39 \xB7 bps", "Fee \xB7 bps") : tr("\u4E0D\u5229\u6ED1\u70B9 \xB7 bps", "Adverse slippage \xB7 bps")), /* @__PURE__ */ React31.createElement(import_dsh_client_ui_primitives9.Input, { type: "number", "aria-label": key3 === "feeBps" ? tr("\u6A21\u62DF\u624B\u7EED\u8D39 bps", "Paper fee bps") : tr("\u6A21\u62DF\u6ED1\u70B9 bps", "Paper slippage bps"), min: "0", max: "100", step: "any", value: costs[key3], onChange: (e) => setCosts((old) => ({ ...old, [key3]: e.target.value })) })))), /* @__PURE__ */ React31.createElement("p", { className: "dhl-limits-note" }, tr("1 bps = 0.01%\u3002\u8FD9\u662F\u6A21\u62DF\u5047\u8BBE\uFF0C\u4E0D\u662F\u5B9E\u76D8\u8D39\u7387\u3002\u4EC5\u4F5C\u7528\u4E8E\u4E4B\u540E\u7684\u6A21\u62DF\u6210\u4EA4\uFF0C\u65E7\u8BB0\u5F55\u4FDD\u7559\u539F\u503C\uFF1B\u4E0D\u6A21\u62DF\u76D8\u53E3\u6DF1\u5EA6\u3001\u8D44\u91D1\u8D39\u548C\u5F3A\u5E73\u3002", "1 bps = 0.01%. Simulation assumptions, not exchange fee tiers. Future paper fills only; historical assumptions are preserved. No depth, funding or liquidation simulation.")), /* @__PURE__ */ React31.createElement(import_dsh_client_ui_primitives9.Button, { variant: "outline", disabled: busy, onClick: () => {
|
|
22024
|
+
setBusy(true);
|
|
22025
|
+
setError("");
|
|
22026
|
+
void hyperCall("updatePaperCosts", { costs: { feeBps: costs.feeBps.trim() ? Number(costs.feeBps) : NaN, slippageBps: costs.slippageBps.trim() ? Number(costs.slippageBps) : NaN } }).then(() => loadStatus()).then(() => notify(tr("\u6A21\u62DF\u6210\u672C\u5DF2\u4FDD\u5B58", "Paper costs saved"))).catch((cause) => setError(String(cause.message))).finally(() => setBusy(false));
|
|
22027
|
+
} }, tr("\u4FDD\u5B58\u6A21\u62DF\u6210\u672C", "Save paper costs"))), /* @__PURE__ */ React31.createElement("section", { className: "dhl-limits-section" }, /* @__PURE__ */ React31.createElement("h3", null, tr("\u94B1\u5305\u7B7E\u540D", "Wallet signing")), /* @__PURE__ */ React31.createElement("p", { className: "dhl-limits-note" }, paper ? tr("\u6A21\u62DF\u76D8\u5728\u672C\u5730\u64AE\u5408\uFF0C\u4E0D\u7B7E\u540D\uFF0C\u4E5F\u4E0D\u7ECF\u8FC7\u94B1\u5305\u89C4\u5219\u3002", "Paper orders fill locally and do not use wallet signing.") : st?.signer === "web3" ? tr(
|
|
20602
22028
|
"\u5B9E\u76D8\u8BA2\u5355\u901A\u8FC7\u9650\u989D\u68C0\u67E5\u540E\uFF0C\u7531 dsh-web3 \u94B1\u5305\u6309\u5176\u7B7E\u540D\u7B56\u7565\u5904\u7406\u3002\u7B7E\u540D\u7B56\u7565\u5728 DSH \u8BBE\u7F6E \u2192 Web3 \u94B1\u5305\u4E2D\u7BA1\u7406\u3002",
|
|
20603
22029
|
"After the limits check, live orders use the dsh-web3 wallet signing policy. Manage it in DSH Settings \u2192 Web3 wallet."
|
|
20604
22030
|
) : tr(
|
|
20605
22031
|
"\u5B9E\u76D8\u4F7F\u7528\u63D2\u4EF6\u914D\u7F6E\u4E2D\u7684\u79C1\u94A5\u7B7E\u540D\u3002\u79C1\u94A5\u4E0E\u7B7E\u540D\u65B9\u5F0F\u4ECD\u5728 DSH \u63D2\u4EF6\u914D\u7F6E\u4E2D\u7BA1\u7406\u3002",
|
|
20606
22032
|
"Live orders use the private key from plugin configuration. Manage the key and signing method in DSH plugin settings."
|
|
20607
|
-
)), !paper && !st?.autonomousLive ? /* @__PURE__ */
|
|
22033
|
+
)), !paper && !st?.autonomousLive ? /* @__PURE__ */ React31.createElement("p", { className: "dhl-limits-note" }, tr("agent \u6BCF\u7B14\u5B9E\u76D8\u4E0B\u5355\u6216\u6539\u6760\u6746\u524D\u4ECD\u9700 DSH \u5BA1\u6279\u3002", "Each agent live order or leverage change still requires DSH approval.")) : null));
|
|
20608
22034
|
}
|
|
20609
22035
|
|
|
20610
22036
|
// src/client/app.tsx
|
|
@@ -20612,15 +22038,15 @@ function App(props) {
|
|
|
20612
22038
|
const s = useStore();
|
|
20613
22039
|
const st = s.status;
|
|
20614
22040
|
const marketStale = isMarketStale(s);
|
|
20615
|
-
const [confirmLive, setConfirmLive] =
|
|
20616
|
-
const [networkBusy, setNetworkBusy] =
|
|
20617
|
-
const [walletOpen, setWalletOpen] =
|
|
20618
|
-
const [moreOpen, setMoreOpen] =
|
|
20619
|
-
const [limitsOpen, setLimitsOpen] =
|
|
20620
|
-
const moreRef =
|
|
22041
|
+
const [confirmLive, setConfirmLive] = React32.useState(null);
|
|
22042
|
+
const [networkBusy, setNetworkBusy] = React32.useState(false);
|
|
22043
|
+
const [walletOpen, setWalletOpen] = React32.useState(false);
|
|
22044
|
+
const [moreOpen, setMoreOpen] = React32.useState(false);
|
|
22045
|
+
const [limitsOpen, setLimitsOpen] = React32.useState(false);
|
|
22046
|
+
const moreRef = React32.useRef(null);
|
|
20621
22047
|
const network = st?.network === "testnet" ? "testnet" : "mainnet";
|
|
20622
22048
|
const mode = st?.tradingMode === "paper" ? "paper" : "live";
|
|
20623
|
-
const applyEnv =
|
|
22049
|
+
const applyEnv = React32.useCallback(async (next) => {
|
|
20624
22050
|
setNetworkBusy(true);
|
|
20625
22051
|
try {
|
|
20626
22052
|
const current2 = store.get().status;
|
|
@@ -20633,12 +22059,12 @@ function App(props) {
|
|
|
20633
22059
|
setConfirmLive(null);
|
|
20634
22060
|
}
|
|
20635
22061
|
}, []);
|
|
20636
|
-
const requestEnv =
|
|
22062
|
+
const requestEnv = React32.useCallback((next) => {
|
|
20637
22063
|
setMoreOpen(false);
|
|
20638
22064
|
if (next.network === "mainnet" && next.mode === "live") setConfirmLive(next);
|
|
20639
22065
|
else void applyEnv(next);
|
|
20640
22066
|
}, [applyEnv]);
|
|
20641
|
-
|
|
22067
|
+
React32.useEffect(() => {
|
|
20642
22068
|
if (!moreOpen) return void 0;
|
|
20643
22069
|
const onDown = (event) => {
|
|
20644
22070
|
if (!moreRef.current?.contains(event.target)) setMoreOpen(false);
|
|
@@ -20653,12 +22079,12 @@ function App(props) {
|
|
|
20653
22079
|
window.removeEventListener("keydown", onKey);
|
|
20654
22080
|
};
|
|
20655
22081
|
}, [moreOpen]);
|
|
20656
|
-
|
|
20657
|
-
|
|
22082
|
+
React32.useEffect(() => startPolling(), []);
|
|
22083
|
+
React32.useEffect(() => {
|
|
20658
22084
|
const list = getContext()?.get("sessions")?.list;
|
|
20659
22085
|
if (!list?.subscribe) return void 0;
|
|
20660
22086
|
const read = () => {
|
|
20661
|
-
const snapshot = list
|
|
22087
|
+
const snapshot = readNativeSessionList(list) ?? {};
|
|
20662
22088
|
const byId = snapshot.byId ?? {};
|
|
20663
22089
|
const titles = {};
|
|
20664
22090
|
for (const [id, row] of Object.entries(byId)) titles[id] = row?.displayTitle ?? null;
|
|
@@ -20667,19 +22093,19 @@ function App(props) {
|
|
|
20667
22093
|
read();
|
|
20668
22094
|
return list.subscribe(read);
|
|
20669
22095
|
}, []);
|
|
20670
|
-
|
|
22096
|
+
React32.useEffect(() => {
|
|
20671
22097
|
const list = getContext()?.get("workspaces")?.list;
|
|
20672
22098
|
if (!list?.subscribe) return void 0;
|
|
20673
22099
|
const read = () => store.patch({ archivedSessionIds: [...list.getSnapshot()?.archivedSessionIds ?? []] });
|
|
20674
22100
|
read();
|
|
20675
22101
|
return list.subscribe(read);
|
|
20676
22102
|
}, []);
|
|
20677
|
-
|
|
22103
|
+
React32.useEffect(() => {
|
|
20678
22104
|
const locale = getContext()?.get("locale");
|
|
20679
22105
|
if (!locale?.subscribe) return void 0;
|
|
20680
22106
|
return locale.subscribe(() => store.patch((prev) => ({ localeRevision: prev.localeRevision + 1 })));
|
|
20681
22107
|
}, []);
|
|
20682
|
-
|
|
22108
|
+
React32.useEffect(() => {
|
|
20683
22109
|
if (!s.sessionsLoaded) return;
|
|
20684
22110
|
const visible = s.sessions.filter((row) => !s.archivedSessionIds.includes(row.sessionId) && (!s.nativeSessionsReady || s.nativeSessionIds.includes(row.sessionId)));
|
|
20685
22111
|
if (visible.some((row) => row.sessionId === s.activeSessionId)) return;
|
|
@@ -20689,53 +22115,53 @@ function App(props) {
|
|
|
20689
22115
|
}, [s.sessions, s.sessionsLoaded, s.archivedSessionIds, s.nativeSessionIds, s.nativeSessionsReady, s.activeSessionId]);
|
|
20690
22116
|
const wsState = s.snapshot?.ws?.state ?? st?.ws?.state;
|
|
20691
22117
|
const resyncing = marketStale && !s.statusError && Date.now() - s.resumedAt < 1e4;
|
|
20692
|
-
const wsBadge = wsState === "open" && !marketStale ? /* @__PURE__ */
|
|
22118
|
+
const wsBadge = wsState === "open" && !marketStale ? /* @__PURE__ */ React32.createElement("span", { className: "dhl-badge ok", title: `WebSocket: ${wsState}` }, t("status.online", "\u5728\u7EBF")) : resyncing ? /* @__PURE__ */ React32.createElement("span", { className: "dhl-badge", title: `WebSocket: ${wsState ?? "off"}` }, tr("\u540C\u6B65\u4E2D", "Syncing")) : /* @__PURE__ */ React32.createElement("span", { className: "dhl-badge err", title: `WebSocket: ${wsState ?? "off"}` }, t("status.offline", "\u79BB\u7EBF"));
|
|
20693
22119
|
const [layout, setLayout, commitLayout] = useLayout(
|
|
20694
22120
|
s.activeSessionId,
|
|
20695
22121
|
s.activeSessionId && s.workspaceSessionId === s.activeSessionId ? s.workspace?.layout : null,
|
|
20696
22122
|
(next) => saveSessionWorkspace({ layout: next })
|
|
20697
22123
|
);
|
|
20698
|
-
const resizeSessions =
|
|
22124
|
+
const resizeSessions = React32.useCallback((delta) => {
|
|
20699
22125
|
setLayout((prev) => ({ sessionsWidth: clampWidth(prev.sessionsWidth + delta, LIMITS.sessions) }));
|
|
20700
22126
|
}, [setLayout]);
|
|
20701
|
-
const resizeChat =
|
|
22127
|
+
const resizeChat = React32.useCallback((delta) => {
|
|
20702
22128
|
setLayout((prev) => ({ chatWidth: clampWidth(prev.chatWidth + delta, LIMITS.chat) }));
|
|
20703
22129
|
}, [setLayout]);
|
|
20704
|
-
const resetSessions =
|
|
22130
|
+
const resetSessions = React32.useCallback(() => {
|
|
20705
22131
|
setLayout({ sessionsWidth: DEFAULTS.sessionsWidth });
|
|
20706
22132
|
commitLayout();
|
|
20707
22133
|
}, [setLayout, commitLayout]);
|
|
20708
|
-
const resetChat =
|
|
22134
|
+
const resetChat = React32.useCallback(() => {
|
|
20709
22135
|
setLayout({ chatWidth: DEFAULTS.chatWidth });
|
|
20710
22136
|
commitLayout();
|
|
20711
22137
|
}, [setLayout, commitLayout]);
|
|
20712
|
-
const toggleSessions =
|
|
22138
|
+
const toggleSessions = React32.useCallback(() => {
|
|
20713
22139
|
setLayout((prev) => ({ sessionsCollapsed: !prev.sessionsCollapsed }));
|
|
20714
22140
|
commitLayout();
|
|
20715
22141
|
}, [setLayout, commitLayout]);
|
|
20716
|
-
const toggleDesk =
|
|
22142
|
+
const toggleDesk = React32.useCallback(() => {
|
|
20717
22143
|
setLayout((prev) => ({ deskCollapsed: !prev.deskCollapsed }));
|
|
20718
22144
|
commitLayout();
|
|
20719
22145
|
}, [setLayout, commitLayout]);
|
|
20720
|
-
const toggleChat =
|
|
22146
|
+
const toggleChat = React32.useCallback(() => {
|
|
20721
22147
|
setLayout((prev) => ({ chatCollapsed: !prev.chatCollapsed }));
|
|
20722
22148
|
commitLayout();
|
|
20723
22149
|
}, [setLayout, commitLayout]);
|
|
20724
22150
|
const showSessions = !layout.sessionsCollapsed;
|
|
20725
22151
|
const showDesk = !layout.deskCollapsed;
|
|
20726
22152
|
const showChat = !layout.chatCollapsed;
|
|
20727
|
-
return /* @__PURE__ */
|
|
22153
|
+
return /* @__PURE__ */ React32.createElement("div", { className: "dhl-root" }, /* @__PURE__ */ React32.createElement("div", { className: "dhl-topbar" }, /* @__PURE__ */ React32.createElement("span", { className: "dhl-brand" }, /* @__PURE__ */ React32.createElement(BrandMark, null), "Hyperliquid ", /* @__PURE__ */ React32.createElement("span", { style: { fontWeight: 400, color: "var(--dsw-alias-label-secondary)" } }, t("app.suffix", "\u4EA4\u6613\u53F0"))), /* @__PURE__ */ React32.createElement("div", { className: "dhl-spacer" }), s.statusError ? /* @__PURE__ */ React32.createElement("span", { className: "dhl-badge err" }, t("status.unavailable", "\u8FDC\u7AEF\u4E0D\u53EF\u7528")) : null, st && network === "testnet" ? /* @__PURE__ */ React32.createElement("span", { className: "dhl-badge paper" }, t("network.testnet", "\u6D4B\u8BD5\u7F51")) : null, st && mode === "paper" ? /* @__PURE__ */ React32.createElement("span", { className: "dhl-badge paper" }, t("mode.paper", "\u6A21\u62DF\u76D8")) : null, wsBadge, /* @__PURE__ */ React32.createElement("button", { type: "button", className: "dhl-badge dhl-wallet-trigger", onClick: () => setWalletOpen(true), "aria-haspopup": "dialog", "aria-label": tr("\u67E5\u770B\u94B1\u5305\u8BE6\u60C5\u548C\u6536\u76CA", "View wallet and P&L") }, mode === "paper" ? tr("\u6A21\u62DF\u8D26\u6237", "Paper account") : st?.address ? st.address.slice(0, 6) + "\u2026" + st.address.slice(-4) : t("account.none", "\u672A\u8FDE\u63A5\u94B1\u5305"), /* @__PURE__ */ React32.createElement(UiIcon, { name: "chevronDown", size: 14 })), /* @__PURE__ */ React32.createElement("div", { className: "dhl-more", ref: moreRef }, /* @__PURE__ */ React32.createElement("button", { type: "button", className: "dhl-top-control dhl-more-trigger", "aria-haspopup": "menu", "aria-expanded": moreOpen, "aria-label": tr("\u4EA4\u6613\u8BBE\u7F6E", "Trading settings"), title: tr("\u4EA4\u6613\u8BBE\u7F6E", "Trading settings"), onClick: () => setMoreOpen((value) => !value) }, /* @__PURE__ */ React32.createElement(UiIcon, { name: "settings", size: 17 })), moreOpen ? /* @__PURE__ */ React32.createElement("div", { className: "dhl-more-popover", role: "menu" }, /* @__PURE__ */ React32.createElement("div", { className: "dhl-more-group" }, /* @__PURE__ */ React32.createElement("span", null, t("network.label", "\u7F51\u7EDC")), /* @__PURE__ */ React32.createElement("div", { className: "dhl-segment" }, /* @__PURE__ */ React32.createElement("button", { type: "button", className: network === "mainnet" ? "on" : "", disabled: networkBusy || !st, onClick: () => network !== "mainnet" && requestEnv({ network: "mainnet", mode }) }, t("network.mainnet", "\u4E3B\u7F51")), /* @__PURE__ */ React32.createElement("button", { type: "button", className: network === "testnet" ? "on" : "", disabled: networkBusy || !st, onClick: () => network !== "testnet" && requestEnv({ network: "testnet", mode }) }, t("network.testnet", "\u6D4B\u8BD5\u7F51")))), /* @__PURE__ */ React32.createElement("div", { className: "dhl-more-group" }, /* @__PURE__ */ React32.createElement("span", null, tr("\u8D26\u6237", "Account")), /* @__PURE__ */ React32.createElement("div", { className: "dhl-segment" }, /* @__PURE__ */ React32.createElement("button", { type: "button", className: mode === "live" ? "on" : "", disabled: networkBusy || !st, onClick: () => mode !== "live" && requestEnv({ network, mode: "live" }) }, t("mode.live", "\u5B9E\u76D8")), /* @__PURE__ */ React32.createElement("button", { type: "button", className: mode === "paper" ? "on" : "", disabled: networkBusy || !st, onClick: () => mode !== "paper" && requestEnv({ network, mode: "paper" }) }, t("mode.paper", "\u6A21\u62DF\u76D8")))), /* @__PURE__ */ React32.createElement("div", { className: "dhl-more-group" }, /* @__PURE__ */ React32.createElement("span", null, tr("\u76D8\u53E3 / \u6210\u4EA4\u4F4D\u7F6E", "Book / trades position")), /* @__PURE__ */ React32.createElement("div", { className: "dhl-segment" }, /* @__PURE__ */ React32.createElement("button", { type: "button", className: s.tapePlacement === "auto" ? "on" : "", onClick: () => setTapePlacement("auto") }, tr("\u81EA\u9002\u5E94", "Auto")), /* @__PURE__ */ React32.createElement("button", { type: "button", className: s.tapePlacement === "below" ? "on" : "", onClick: () => setTapePlacement("below") }, tr("\u56FE\u8868\u4E0B\u65B9", "Below chart")))), /* @__PURE__ */ React32.createElement("div", { className: "dhl-more-group" }, /* @__PURE__ */ React32.createElement("span", null, tr("\u4EA4\u6613\u8BBE\u7F6E", "Trading settings")), /* @__PURE__ */ React32.createElement("div", { className: "dhl-segment" }, /* @__PURE__ */ React32.createElement("button", { type: "button", onClick: () => {
|
|
20728
22154
|
setMoreOpen(false);
|
|
20729
22155
|
setLimitsOpen(true);
|
|
20730
|
-
} }, tr("\u4EA4\u6613\u9650\u989D\u4E0E\u7B7E\u540D\u89C4\u5219", "Limits & signing rules"))))) : null)), /* @__PURE__ */
|
|
22156
|
+
} }, tr("\u4EA4\u6613\u9650\u989D\u4E0E\u7B7E\u540D\u89C4\u5219", "Limits & signing rules"))))) : null)), /* @__PURE__ */ React32.createElement(import_dsh_client_ui_primitives10.Modal, { open: Boolean(confirmLive), onClose: () => setConfirmLive(null), title: tr("\u5207\u6362\u5230\u4E3B\u7F51\u5B9E\u76D8", "Switch to mainnet live"), closeLabel: t("modal.close", "\u5173\u95ED"), className: "dhl-network-modal", footer: /* @__PURE__ */ React32.createElement(React32.Fragment, null, /* @__PURE__ */ React32.createElement(import_dsh_client_ui_primitives10.Button, { variant: "outline", onClick: () => setConfirmLive(null) }, t("modal.cancel", "\u53D6\u6D88")), /* @__PURE__ */ React32.createElement(import_dsh_client_ui_primitives10.Button, { variant: "primary", disabled: networkBusy, onClick: () => {
|
|
20731
22157
|
if (confirmLive) void applyEnv(confirmLive);
|
|
20732
|
-
} }, tr("\u786E\u8BA4\u5207\u6362", "Switch"))) }, /* @__PURE__ */
|
|
22158
|
+
} }, tr("\u786E\u8BA4\u5207\u6362", "Switch"))) }, /* @__PURE__ */ React32.createElement("p", null, tr("\u4E3B\u7F51\u5B9E\u76D8\u4F1A\u663E\u793A\u771F\u5B9E\u884C\u60C5\u548C\u771F\u5B9E\u8D26\u6237\uFF0C\u540E\u7EED\u4EA4\u6613\u5C06\u4F7F\u7528\u771F\u5B9E\u8D44\u91D1\u3002", "Mainnet live shows real markets and your real account; trades use real funds."))), /* @__PURE__ */ React32.createElement(WalletDetails, { open: walletOpen, onClose: () => setWalletOpen(false) }), /* @__PURE__ */ React32.createElement(TradingLimits, { open: limitsOpen, onClose: () => setLimitsOpen(false) }), /* @__PURE__ */ React32.createElement("div", { className: "dhl-toasts" }, marketStale && !resyncing ? /* @__PURE__ */ React32.createElement("div", { className: "dhl-data-warning", role: "status" }, tr("\u884C\u60C5\u6216\u5BBF\u4E3B\u72B6\u6001\u672A\u53CA\u65F6\u66F4\u65B0\uFF1B\u5F53\u524D\u4EF7\u683C\u53EF\u80FD\u8FC7\u671F\u3002\u4EA4\u6613\u7968\u636E\u5DF2\u6682\u505C\u63D0\u4EA4\uFF0C\u7B49\u5F85\u6570\u636E\u6062\u590D\u3002", "Market or host data is stale. Ticket submission is paused until data recovers.")) : null, s.notice ? /* @__PURE__ */ React32.createElement("div", { className: "dhl-notice", onClick: () => notify(null) }, /* @__PURE__ */ React32.createElement(UiIcon, { name: "info", size: 16 }), /* @__PURE__ */ React32.createElement("span", { style: { flex: 1 } }, s.notice.text), /* @__PURE__ */ React32.createElement(UiIcon, { name: "close", size: 14 })) : null), /* @__PURE__ */ React32.createElement("div", { className: "dhl-body" }, showSessions ? /* @__PURE__ */ React32.createElement(SessionsColumn, { width: layout.sessionsWidth, fill: !showDesk && !showChat, onCollapse: toggleSessions }) : /* @__PURE__ */ React32.createElement("button", { className: "dhl-panel-rail", type: "button", onClick: toggleSessions, "aria-label": tr("\u5C55\u5F00\u4F1A\u8BDD\u680F", "Expand sessions"), title: tr("\u5C55\u5F00\u4F1A\u8BDD\u680F", "Expand sessions") }, /* @__PURE__ */ React32.createElement(PanelIcon, { kind: "sessions" })), showSessions && (showDesk || showChat) ? /* @__PURE__ */ React32.createElement(Divider, { side: "left", onResize: resizeSessions, onCommit: commitLayout, onReset: resetSessions }) : null, showDesk ? /* @__PURE__ */ React32.createElement(DeskColumn, { onCollapse: toggleDesk }) : /* @__PURE__ */ React32.createElement("button", { className: "dhl-panel-rail", type: "button", onClick: toggleDesk, "aria-label": tr("\u5C55\u5F00\u884C\u60C5\u533A", "Expand market panel"), title: tr("\u5C55\u5F00\u884C\u60C5\u533A", "Expand market panel") }, /* @__PURE__ */ React32.createElement(PanelIcon, { kind: "desk" })), showDesk && showChat ? /* @__PURE__ */ React32.createElement(Divider, { side: "right", onResize: resizeChat, onCommit: commitLayout, onReset: resetChat }) : null, showChat ? /* @__PURE__ */ React32.createElement(ChatColumn, { width: layout.chatWidth, fill: !showDesk, onCollapse: toggleChat, SessionProvider: props.SessionProvider ?? props.sessionProvider, renderFactorySlot: props.renderFactorySlot }) : /* @__PURE__ */ React32.createElement("button", { className: "dhl-panel-rail", type: "button", onClick: toggleChat, "aria-label": tr("\u5C55\u5F00\u5BF9\u8BDD\u680F", "Expand chat"), title: tr("\u5C55\u5F00\u5BF9\u8BDD\u680F", "Expand chat") }, /* @__PURE__ */ React32.createElement(PanelIcon, { kind: "chat" }))));
|
|
20733
22159
|
}
|
|
20734
22160
|
|
|
20735
22161
|
// src/client/components/panel-entry.tsx
|
|
20736
|
-
var
|
|
22162
|
+
var React33 = __toESM(require("react"), 1);
|
|
20737
22163
|
function PanelEntry(props) {
|
|
20738
|
-
return /* @__PURE__ */
|
|
22164
|
+
return /* @__PURE__ */ React33.createElement("span", { className: "dhl-entrymark" }, /* @__PURE__ */ React33.createElement(BrandMark, { size: props?.size ?? 18 }));
|
|
20739
22165
|
}
|
|
20740
22166
|
|
|
20741
22167
|
// src/client/index.tsx
|
|
@@ -20767,30 +22193,30 @@ function apply(ctx) {
|
|
|
20767
22193
|
locale: NS,
|
|
20768
22194
|
children: { "hyper.panel.seat": { kind: "single", scope: "session" } }
|
|
20769
22195
|
},
|
|
20770
|
-
(props) =>
|
|
22196
|
+
(props) => React34.createElement(App, {
|
|
20771
22197
|
SessionProvider: props?.SessionProvider,
|
|
20772
22198
|
renderFactorySlot: props?.renderFactorySlot
|
|
20773
22199
|
})
|
|
20774
22200
|
)), NS + ": main panel");
|
|
20775
22201
|
ctx.effect(() => slots.inject("sidebar.panellist", () => slots.register(
|
|
20776
22202
|
{ name: "sidebar.panellist", id: NS, order: 20, label: () => t("app.title", "Hyperliquid \u4EA4\u6613\u53F0"), registrant: NS, locale: NS },
|
|
20777
|
-
(props) =>
|
|
22203
|
+
(props) => React34.createElement(PanelEntry, props)
|
|
20778
22204
|
)), NS + ": panel entry");
|
|
20779
22205
|
}
|
|
20780
22206
|
registerDshExtensions(ctx, slots);
|
|
20781
22207
|
const layout = ctx.get("layout");
|
|
20782
22208
|
if (layout?.panelInfo?.subscribe && slots) ctx.effect(() => {
|
|
20783
|
-
const
|
|
22209
|
+
const key3 = `${NS}.panel-selected.v1`;
|
|
20784
22210
|
let restore = false;
|
|
20785
22211
|
try {
|
|
20786
|
-
restore = sessionStorage.getItem(
|
|
22212
|
+
restore = sessionStorage.getItem(key3) === "1";
|
|
20787
22213
|
} catch {
|
|
20788
22214
|
}
|
|
20789
22215
|
let ready = false;
|
|
20790
22216
|
const remember = () => {
|
|
20791
22217
|
if (!ready) return;
|
|
20792
22218
|
try {
|
|
20793
|
-
sessionStorage.setItem(
|
|
22219
|
+
sessionStorage.setItem(key3, layout.panelInfo.getSnapshot()?.activePanelId === NS ? "1" : "0");
|
|
20794
22220
|
} catch {
|
|
20795
22221
|
}
|
|
20796
22222
|
};
|