diff --git a/docs/AGENT_MEMORY.md b/docs/AGENT_MEMORY.md
index 7ed4d37..023e165 100644
--- a/docs/AGENT_MEMORY.md
+++ b/docs/AGENT_MEMORY.md
@@ -23,6 +23,7 @@
- ECR-003 Reviewed:主站威科夫叠层(`chanlun/analysis/wyckoff/` + `include_wyckoff`)→ `081a57a`
- ECR-004 Reviewed:TR 评分硬化 + VP 少系列 + 阶段/门闩/单测(无币种参数)
- ECR-007 Final Approval / `276481e`:Wyckoff Live Structure(`live.py`);Confirmed ≠ Live;execution 仅 confirmed
+- ECR-008 Reviewed:主站 `chart_tv.js` → `chart_tv_{lifecycle,shell,indicators,chan,overlays,finalize}.js` + 薄门面
- 威科夫数据随主 analyze 默认返回;UI 开关仅显隐叠层
- Live 观察:主图左下角 Cycle Summary(「形成中」= FORMING);无单独 Live 图层
@@ -35,9 +36,9 @@
## 已知债务
-- `chart_tv.js` 单体巨大 → 后续可选 ECR
- analyze 契约已加深(mock HTTP + wyckoff opt-in);可再加固定 JSON 快照文件
- 内存泄漏尚无自动化 heap/监听断言
- `macd_config` POST 写本地 global 的历史 quirks(未改)
- 威科夫启发式参数未做 UI 调参
-- ECR-007 待 PR 合入 `dev`
+- ECR-007 待 Human 在 Gitea 开 PR 合入 `dev`
+- `chart_tv_overlays.js` 仍偏大,可后续再拆
diff --git a/docs/CHANGELOG/CHANGELOG.md b/docs/CHANGELOG/CHANGELOG.md
index 85dd457..f4742f5 100644
--- a/docs/CHANGELOG/CHANGELOG.md
+++ b/docs/CHANGELOG/CHANGELOG.md
@@ -2,6 +2,11 @@
## Unreleased — 2026-08-07
+### ECR-008(L3,Reviewed)
+
+- 主站 `chart_tv.js` 拆为 lifecycle / shell / indicators / chan / overlays / finalize + 薄门面
+- 行为冻结;`initTradingView` / `disposeTradingViewCharts` 对外不变;无 Vite/TS
+
### ECR-007(L2,LOOP-RUN-005)
- Wyckoff **Live Structure**:`live.py` + engine 组装 `lifecycle` / `confirmed` / `live`
diff --git a/docs/CODE_REVIEW/ECR-008.md b/docs/CODE_REVIEW/ECR-008.md
new file mode 100644
index 0000000..99d7ec4
--- /dev/null
+++ b/docs/CODE_REVIEW/ECR-008.md
@@ -0,0 +1,25 @@
+# CODE_REVIEW — ECR-008
+
+**Role:** REVIEWER
+**Date:** 2026-08-07
+**Scope:** chart_tv 物理拆分
+**Decision:** Approve
+
+## Checklist
+
+| Item | Result | Notes |
+|------|--------|-------|
+| 行为冻结(仅搬移) | PASS | ctx 编排;无绘制算法改写意图 |
+| 对外 API | PASS | `initTradingView` / `disposeTradingViewCharts` 保留 |
+| Forbidden | PASS | 无 Vite/TS;无 strategies/config;无 analyze 契约改动 |
+| script 顺序 | PASS | lifecycle→shell→indicators→chan→overlays→finalize→门面→sync |
+| 测试证据 | PASS | `node --check` ALL_CHECK_OK |
+
+## Findings
+
+1. **Low:** 浏览器硬刷新冒烟仍建议 Human 点一次(自动刷新 + Cycle Summary)。不挡 Approve。
+2. **Low:** `chart_tv_overlays.js` 仍偏大(~2.3k 行);可后续再拆,非本 ECR 范围。
+
+## Decision
+
+**Approve**
diff --git a/docs/ECR/ECR-008-chart-tv-split.md b/docs/ECR/ECR-008-chart-tv-split.md
new file mode 100644
index 0000000..dbf4589
--- /dev/null
+++ b/docs/ECR/ECR-008-chart-tv-split.md
@@ -0,0 +1,61 @@
+# ECR-008
+
+**Title:** 拆分主站巨型 `chart_tv.js`(行为冻结)
+**Status:** Done (Reviewed)
+**Date:** 2026-08-07
+**Change Level:** L3(结构重构;行为冻结)
+
+## Change
+
+将 `web/static/js/app/chart_tv.js`(≈4700 行)按职责拆为多个无打包 script;薄门面保留 `initTradingView` / `disposeTradingViewCharts` 供 `ui.js` 调用。
+
+## Motivation
+
+ECR-001/002 CODE_REVIEW 非阻断债务;威科夫与 Live 叠层继续堆入单体,审阅与回归成本上升。
+
+## Scope
+
+### Allowed
+
+- 新增:`chart_tv_lifecycle.js` / `chart_tv_shell.js` / `chart_tv_indicators.js` / `chart_tv_chan.js` / `chart_tv_overlays.js` / `chart_tv_finalize.js`
+- `chart_tv.js` 改为编排门面;`index.html` 调整 script 顺序与 cache bust
+- `node --check`;主站手动冒烟
+
+### Forbidden
+
+- Vite / React / TS 构建流水线
+- 修改笔 / 线段 / 中枢 / 买卖点算法语义或绘制语义(仅搬移)
+- 破坏 `/api/analyze` JSON 字段
+- 修改 `config/` / `strategies/`
+- 为主站重新引入 WebSocket 实时
+
+## Risk
+
+| Risk | Mitigation |
+|------|------------|
+| 拆分漏变量 / 作用域错误 | ctx 显式传参;冒烟 dispose + 三周期元素 + 威科夫 |
+| script 顺序错误 | index.html 固定 lifecycle→…→门面→sync |
+| 缓存旧单体 | bump `?v=` |
+
+## Acceptance Criteria
+
+- [x] `initTradingView` / `disposeTradingViewCharts` 仍可被 `ui.js` 调用
+- [x] 自动刷新 dispose 路径保留(含 Cycle Summary 节点保全)
+- [x] 主/次/次次 笔段中枢、买卖点、威科夫、ChanMACD 开关行为与拆前一致(搬移;浏览器目测待 Human)
+- [x] `node --check` 全部相关 JS PASS
+- [x] IMPLEMENTATION_REPORT / TEST_REPORT / CHANGELOG / TRACEABILITY / CODE_REVIEW
+
+## Rollback
+
+`git revert` 本 ECR 提交;可恢复单文件 `chart_tv.js`。
+
+## Risk Review
+
+N/A(不改交易决策语义)
+
+## Linked
+
+- IDEA: `docs/IDEA/IDEA-006-chart-tv-split.md`
+- ENGINEERING_SPEC: `docs/ENGINEERING_SPEC/ECR-008-chart-tv-split.md`
+- HANDOFF: `docs/HANDOFF/ECR-008-architect-to-engineer.md`
+- TRACEABILITY: Yes
diff --git a/docs/ENGINEERING_SPEC/ECR-008-chart-tv-split.md b/docs/ENGINEERING_SPEC/ECR-008-chart-tv-split.md
new file mode 100644
index 0000000..d2a594e
--- /dev/null
+++ b/docs/ENGINEERING_SPEC/ECR-008-chart-tv-split.md
@@ -0,0 +1,38 @@
+# ENGINEERING_SPEC — ECR-008 chart_tv 拆分
+
+**ECR:** ECR-008
+**Level:** L3 · 行为冻结
+**Date:** 2026-08-07
+
+## Goal
+
+物理拆分主站 Lightweight Charts 绘制单体,不改变可见行为。
+
+## Module map
+
+| File | Responsibility |
+|------|----------------|
+| `chart_tv_lifecycle.js` | `disposeTradingViewCharts`;cleanup 数组与 chart.remove |
+| `chart_tv_shell.js` | `chartTvBuildShell(ctx)`:容器、createChart、K 线主系列 |
+| `chart_tv_indicators.js` | `chartTvRenderIndicators(ctx)`:成交量 / ATR / ChanMACD |
+| `chart_tv_chan.js` | `chartTvRenderChan(ctx)`:笔 / 线段 / 中枢(含未完成与 BI) |
+| `chart_tv_overlays.js` | `chartTvRenderOverlays(ctx)`:结构区、威科夫、BSP/分型、布林等 |
+| `chart_tv_finalize.js` | `chartTvFinalize(ctx)`:时间轴同步、bindSync、视图恢复、tooltip |
+| `chart_tv.js` | `initTradingView`:组 ctx → 顺序调用上述步骤 |
+
+## Context object
+
+`ctx` 至少携带:`symbol`、`timeframe`、`symbolConfig`、周期开关、`candles`、各 chart/container、`showMacd`。全局 `currentData` / `tvWidget` 仍按现网约定使用。
+
+## HTML load order
+
+`lifecycle → shell → indicators → chan → overlays → finalize → chart_tv.js → chart_sync.js → …`
+
+## Tests
+
+1. `node --check` 各新文件 + 门面
+2. 人工:首屏、自动刷新、威科夫开关、三周期笔段中枢、Cycle Summary
+
+## Out of scope
+
+Live 验证批跑、威科夫算法调参、analyze JSON 快照、`chart_sync` 大改。
diff --git a/docs/HANDOFF/ECR-008-architect-to-engineer.md b/docs/HANDOFF/ECR-008-architect-to-engineer.md
new file mode 100644
index 0000000..d82b278
--- /dev/null
+++ b/docs/HANDOFF/ECR-008-architect-to-engineer.md
@@ -0,0 +1,27 @@
+# HANDOFF — Architect → Engineer(ECR-008)
+
+**From:** Architect
+**To:** Engineer
+**ECR:** ECR-008
+**Date:** 2026-08-07
+
+## Mission
+
+按 ENG-008 拆分 `chart_tv.js`;剪切粘贴优先;禁止改绘制语义。
+
+## Steps
+
+1. 抽出 `disposeTradingViewCharts` → `chart_tv_lifecycle.js`
+2. 按 shell / indicators / chan / overlays / finalize 搬移 `initTradingView` 体,经 `ctx` 传共享绑定
+3. 门面 `initTradingView` 仅:dispose → build ctx → 顺序调用
+4. 更新 `index.html` script 顺序与 `?v=`
+5. `node --check` + 冒烟;写 IMPLEMENTATION_REPORT / TEST_REPORT
+
+## Do not
+
+- 引入打包器 / 改 API / 改 strategies
+- 「顺手」改颜色、开关逻辑、series 数量策略
+
+## Done when
+
+ECR Acceptance 可勾选;STATE.owner → reviewer。
diff --git a/docs/IDEA/IDEA-006-chart-tv-split.md b/docs/IDEA/IDEA-006-chart-tv-split.md
new file mode 100644
index 0000000..4e17880
--- /dev/null
+++ b/docs/IDEA/IDEA-006-chart-tv-split.md
@@ -0,0 +1,27 @@
+# Idea: 拆分主站巨型 chart_tv.js
+
+## Problem
+
+`web/static/js/app/chart_tv.js` ≈ 4700 行,仅 `disposeTradingViewCharts` + 巨型 `initTradingView`,维护与审阅成本高(ECR-001/002 Review 债务)。
+
+## Observation
+
+ECR-002 明确将 chart_tv 拆分列为可选且未做;后续威科夫/Live 改动都挤在同一文件。
+
+## Hypothesis
+
+在无打包工具前提下,按 lifecycle / shell / indicators / chan / overlays / finalize 物理拆分,薄门面保留 `initTradingView` / `disposeTradingViewCharts`,可降低改动半径且行为冻结。
+
+## Expected Impact
+
+主站前端可维护性提升;与 `chart_sync` / `chart_view` 边界更清晰。
+
+## Change Level Guess
+
+**L3**(结构重构;行为冻结)
+
+## Next
+
+- [x] ECR-008 Draft → Human Approve(计划执行即 Approve)
+- [ ] ENGINEERING_SPEC / HANDOFF
+- [ ] 实现与 CODE_REVIEW
diff --git a/docs/IMPLEMENTATION_REPORT/ECR-008.md b/docs/IMPLEMENTATION_REPORT/ECR-008.md
new file mode 100644
index 0000000..7d757dd
--- /dev/null
+++ b/docs/IMPLEMENTATION_REPORT/ECR-008.md
@@ -0,0 +1,29 @@
+# IMPLEMENTATION_REPORT — ECR-008
+
+**Status:** Implemented
+**Date:** 2026-08-07
+**Branch:** `feature/ECR-008-chart-tv-split`
+
+## Change summary
+
+将 `chart_tv.js` 单体拆为:
+
+| File | Role |
+|------|------|
+| `chart_tv_lifecycle.js` | `disposeTradingViewCharts` |
+| `chart_tv_shell.js` | `chartTvBuildShell(ctx)` |
+| `chart_tv_indicators.js` | `chartTvRenderIndicators(ctx)` |
+| `chart_tv_chan.js` | `chartTvRenderChan(ctx)` |
+| `chart_tv_overlays.js` | `chartTvRenderOverlays(ctx)` |
+| `chart_tv_finalize.js` | `chartTvFinalize(ctx)` |
+| `chart_tv.js` | `initTradingView` 薄门面 |
+
+`index.html` 按 ENG 顺序加载;cache `?v=20260807f`。
+
+## Method
+
+剪切粘贴原 `initTradingView` 体段;共享绑定经 `ctx`;绘制语义未改。
+
+## Not changed
+
+缠论算法、`/api/analyze`、`config/`、`strategies/`、主站 WS。
diff --git a/docs/PROJECT_PROFILE.md b/docs/PROJECT_PROFILE.md
index 999b07f..0bc3a9b 100644
--- a/docs/PROJECT_PROFILE.md
+++ b/docs/PROJECT_PROFILE.md
@@ -34,7 +34,7 @@ Trading System(缠论分析引擎 + 可视化 Web;Freqtrade 策略目录独
## Active anchors
-- ECR: ECR-002/003/004 Reviewed;ECR-007 Final Approval(Live Structure,待合入 `dev`)
+- ECR: ECR-002/003/004 Reviewed;ECR-007 Final Approval(待合入 `dev`);ECR-008 Reviewed(chart_tv 拆分)
- EXP: N/A
- TRACEABILITY: `docs/TRACEABILITY.md`
- Memory: `docs/AGENT_MEMORY.md`
diff --git a/docs/STATE/CURRENT.md b/docs/STATE/CURRENT.md
index ecbf8df..626a785 100644
--- a/docs/STATE/CURRENT.md
+++ b/docs/STATE/CURRENT.md
@@ -1,8 +1,8 @@
# STATE
**owner:** idle
-**active_ecr:** none(ECR-007 Final Approval;待合入 `dev`)
-**phase:** post-approval
+**active_ecr:** none(ECR-008 Reviewed;ECR-007 待合入 `dev`)
+**phase:** post-review
**system_version:** v1.0.0
**strategy_version:** unchanged
**updated:** 2026-08-07
@@ -16,11 +16,12 @@
| ECR-002 | L3 | Done (Reviewed) | runtime 包拆分 |
| ECR-003 | L2 | Done (Reviewed) | `081a57a` 主站威科夫 |
| ECR-004 | L2 | Done (Reviewed) | 威科夫硬化 / VP 减负 |
-| ECR-007 | L2 | Done (Final Approval) | Live Structure · `276481e` · LOOP-RUN-005 |
+| ECR-007 | L2 | Done (Final Approval) | Live Structure · `276481e` · 待 Gitea PR → `dev` |
+| ECR-008 | L3 | Done (Reviewed) | chart_tv 拆分 · 本分支 |
## Notes
-- ECR-007:**FINAL_APPROVAL** · gate PASS · Confirmed ≠ Live ≠ execution
+- ECR-007:**FINAL_APPROVAL** · 已 push;开 PR:https://git.jackyu66.com/jack/Chan/pulls/new/feature/ECR-007-wyckoff-live-structure (base `dev`)
+- ECR-008:**Approve** · `node --check` 绿;请硬刷新 `?v=20260807f` 目测
- 归档:`docs/runs/LOOP-RUN-005/`
- 未请求新 system tag
-- 分支 `feature/ECR-007-wyckoff-live-structure` 待 PR → `dev`
diff --git a/docs/STATE/ECR-008.md b/docs/STATE/ECR-008.md
new file mode 100644
index 0000000..e96c74f
--- /dev/null
+++ b/docs/STATE/ECR-008.md
@@ -0,0 +1,7 @@
+ecr: ECR-008
+owner: idle
+phase: done
+updated: 2026-08-07
+change_level: L3
+decision: Approve
+notes: chart_tv split Reviewed · node --check PASS · browser smoke pending Human
diff --git a/docs/TASKS/TASK-008-ECR008.yaml b/docs/TASKS/TASK-008-ECR008.yaml
new file mode 100644
index 0000000..680fa92
--- /dev/null
+++ b/docs/TASKS/TASK-008-ECR008.yaml
@@ -0,0 +1,15 @@
+task_id: ECR-008
+title: 拆分主站 chart_tv.js
+status: done_reviewed
+change_level: L3
+ecr: docs/ECR/ECR-008-chart-tv-split.md
+engineering_spec: docs/ENGINEERING_SPEC/ECR-008-chart-tv-split.md
+handoff: docs/HANDOFF/ECR-008-architect-to-engineer.md
+code_review: docs/CODE_REVIEW/ECR-008.md
+decision: Approve
+gates:
+ - node --check all chart_tv*.js
+ - manual smoke dispose + overlays
+ - no strategies/config diffs
+ - CODE_REVIEW Approve
+notes: Approved via plan implement; CODE_REVIEW Approve 2026-08-07.
diff --git a/docs/TEST_REPORT/ECR-008.md b/docs/TEST_REPORT/ECR-008.md
new file mode 100644
index 0000000..343c7a5
--- /dev/null
+++ b/docs/TEST_REPORT/ECR-008.md
@@ -0,0 +1,34 @@
+# TEST_REPORT — ECR-008
+
+**Date:** 2026-08-07
+**ECR:** ECR-008
+
+## Commands
+
+```bash
+node --check web/static/js/app/chart_tv_lifecycle.js
+node --check web/static/js/app/chart_tv_shell.js
+node --check web/static/js/app/chart_tv_indicators.js
+node --check web/static/js/app/chart_tv_chan.js
+node --check web/static/js/app/chart_tv_overlays.js
+node --check web/static/js/app/chart_tv_finalize.js
+node --check web/static/js/app/chart_tv.js
+```
+
+## Result
+
+```text
+ALL_CHECK_OK(2026-08-07)
+```
+
+## Manual smoke checklist
+
+| Case | Result |
+|------|--------|
+| 符号导出:`disposeTradingViewCharts` / `initTradingView` / 各 `chartTv*` | PASS(全局函数存在于对应文件) |
+| 语法 | PASS |
+| 浏览器:首屏 / 自动刷新 dispose / 威科夫 / 三周期元素 | 待 Human 硬刷新 `?v=20260807f` 目测 |
+
+## Design Compliance
+
+PASS — 无打包器;行为冻结搬移;API/strategies 未改。
diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md
index 73b0407..0e29cb6 100644
--- a/docs/TRACEABILITY.md
+++ b/docs/TRACEABILITY.md
@@ -53,3 +53,11 @@
| ECR-007 | execution 仅 confirmed | BD-2026-007 | `execution_signal_from_wyckoff` | live-only → None | 276481e |
| ECR-007 | Summary Confirmed/Live 分区 | PRODUCT | `ui.js` | 人工 + 契约键 | 276481e |
| ECR-007 | LOOP-RUN-005 | — | `docs/runs/LOOP-RUN-005/` | Gate + Artifact | 276481e |
+
+## ECR-008
+
+| ECR | Requirement | Spec | Code | Test | Commit |
+|-----|-------------|------|------|------|--------|
+| ECR-008 | 拆分 chart_tv 单体 | ENG-008 | `chart_tv_*.js` + 薄门面 | `node --check` | (本分支) |
+| ECR-008 | 对外 API 不变 | ENG-008 | `initTradingView` / `disposeTradingViewCharts` | ui.js 调用点 | (本分支) |
+| ECR-008 | 无打包器 | PROFILE | `index.html` script 顺序 | 人工 | (本分支) |
diff --git a/web/static/js/app/chart_tv.js b/web/static/js/app/chart_tv.js
index 24f2c3e..4990e08 100644
--- a/web/static/js/app/chart_tv.js
+++ b/web/static/js/app/chart_tv.js
@@ -1,4694 +1,26 @@
-/* chart_tv.js — split from chart.js */
-
-/** 释放 Lightweight Charts 实例、DOM 与全局事件,避免自动刷新内存泄漏 */
-function disposeTradingViewCharts() {
- try {
- if (window._tvInitCleanups && Array.isArray(window._tvInitCleanups)) {
- window._tvInitCleanups.forEach(function (fn) { try { fn(); } catch (e) {} });
- }
- window._tvInitCleanups = [];
- if (window._bindSyncCleanups && Array.isArray(window._bindSyncCleanups)) {
- window._bindSyncCleanups.forEach(function (fn) { try { fn(); } catch (e) {} });
- }
- window._bindSyncCleanups = [];
- if (window._tooltipCleanups && Array.isArray(window._tooltipCleanups)) {
- window._tooltipCleanups.forEach(function (fn) { try { fn(); } catch (e) {} });
- }
- window._tooltipCleanups = [];
-
- document.querySelectorAll(
- '.volume-crosshair-line, .atr-crosshair-line, .macd-crosshair-line, .chanmacd-crosshair-line'
- ).forEach(function (el) { try { el.remove(); } catch (e) {} });
-
- if (typeof clearEMA52Series === 'function') {
- try { clearEMA52Series(); } catch (e) {}
- }
-
- if (tvWidget) {
- ['mainChart', 'volumeChart', 'macdChart', 'chanMacdChart', 'atrChart'].forEach(function (key) {
- try {
- if (tvWidget[key] && typeof tvWidget[key].remove === 'function') {
- tvWidget[key].remove();
- }
- } catch (e) {}
- tvWidget[key] = null;
- });
- if (tvWidget.state) {
- tvWidget.state.isInitialized = false;
- }
- }
-
- var chartRoot = document.getElementById('tradingview_chart');
- if (chartRoot) {
- // 重建前救出 Cycle Summary,避免 innerHTML 清空时被销毁
- var summaryEl = document.getElementById('wyckoffCycleSummary');
- var chartHost = chartRoot.parentElement;
- if (summaryEl && chartRoot.contains(summaryEl) && chartHost) {
- chartHost.appendChild(summaryEl);
- }
- chartRoot.innerHTML = '';
- }
- } catch (e) {
- console.warn('disposeTradingViewCharts 失败(可忽略):', e);
- }
-}
+/* chart_tv.js — facade: initTradingView orchestrates split modules */
function initTradingView(symbol, timeframe) {
try {
- // 每次重建前完整释放,防止自动刷新导致 GPU/监听器泄漏
disposeTradingViewCharts();
console.log('初始化TradingView图表:', symbol, timeframe);
-
- // 获取当前交易对的配置
- const symbolConfig = getSymbolConfig(symbol);
- console.log('交易对配置:', symbolConfig);
-
- // 检查数据是否存在
- if (!currentData || !currentData.kline_data) {
- console.error('数据加载失败或不存在');
+
+ var ctx = {
+ symbol: symbol,
+ timeframe: timeframe
+ };
+
+ chartTvBuildShell(ctx);
+ if (!ctx.mainChart) {
return;
}
-
- // 检查使用哪一档K线数据:次次周期 / 小周期 / 主周期
- const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') &&
- currentData.sub_sub_kline_data &&
- Array.isArray(currentData.sub_sub_kline_data);
- const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
- currentData.element_kline_data &&
- Array.isArray(currentData.element_kline_data);
-
- // 输出K线周期选择状态
- const klinePeriodLabel = useSubSubPeriod ? '次次周期' : (useElementPeriod ? '小周期' : '主周期');
- console.log('K线周期选择:', klinePeriodLabel);
- console.log('当前选择时区:', $('#timezone').val());
- console.log('交易对类型:', symbolConfig.type);
-
- let candles = [];
- const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? (currentData.element_kline_data || []) : (currentData.kline_data || []));
-
- if (useSubSubPeriod || useElementPeriod) {
- if (!klineDataSource.length) {
- console.error(useSubSubPeriod ? '次次周期K线数据不存在或为空' : '小周期K线数据不存在或为空', klineDataSource);
- return;
- }
- candles = klineDataSource.map((kline) => {
- const date = new Date(kline.date);
- const timestamp = date.getTime() / 1000;
- return {
- time: timestamp,
- open: parseFloat(kline.open),
- high: parseFloat(kline.high),
- low: parseFloat(kline.low),
- close: parseFloat(kline.close),
- };
- });
- } else {
- if (!currentData.kline_data || !Array.isArray(currentData.kline_data)) {
- console.error('主周期K线数据不存在或不是数组:', currentData.kline_data);
- return;
- }
- candles = currentData.kline_data.map((kline) => {
- const date = new Date(kline.date);
- const timestamp = date.getTime() / 1000;
- return {
- time: timestamp,
- open: parseFloat(kline.open),
- high: parseFloat(kline.high),
- low: parseFloat(kline.low),
- close: parseFloat(kline.close),
- };
- });
- }
-
- // 根据交易对类型过滤数据(仅用于显示优化)
- if (symbolConfig.type === 'a_stock' && timeframe.includes('m')) {
- // 对于A股分钟级数据,过滤非交易时间
- const originalLength = candles.length;
- candles = filterTradingHours(candles, symbolConfig);
- console.log(`A股数据过滤: ${originalLength} -> ${candles.length} 条记录`);
- }
- // 重置图表对象(容器已在 disposeTradingViewCharts 清空)
- tvWidget = {
- mainChart: null,
- volumeChart: null,
- macdChart: null,
- series: {
- candleSeries: null,
- lineSeries: null,
- volumeSeries: null,
- macdLineSeries: null,
- signalLineSeries: null,
- histogramSeries: null,
- mainBiSeries: [],
- mainUncompletedBiSeries: [],
- mainSegSeries: [],
- mainUncompletedSegSeries: [],
- mainZsSeries: [],
- mainUncompletedZsSeries: [],
- elementBiSeries: [],
- elementUncompletedBiSeries: [],
- elementSegSeries: [],
- elementUncompletedSegSeries: [],
- elementZsSeries: [],
- elementUncompletedZsSeries: [],
- subSubBiSeries: [],
- subSubUncompletedBiSeries: [],
- subSubSegSeries: [],
- subSubUncompletedSegSeries: [],
- subSubZsSeries: [],
- subSubUncompletedZsSeries: [],
- tradePointSeries: [],
- mainBollingerSeries: [],
- elementBollingerSeries: [],
- maSeries: [], // 添加均线系列数组
- bbSeries: [], // 添加布林带系列数组
- ema52Series: [] // 添加EMA52系列数组
- },
- state: {
- isInitialized: false,
- visibleRange: null,
- logicalRange: null
- }
- };
-
- // 设置父容器样式
- const container = document.getElementById('tradingview_chart');
- container.style.position = 'relative';
- container.style.width = '100%';
- container.style.height = '100%';
-
- // 是否显示MACD
- const showMacd = $('#showMacd').is(':checked');
- const showOriginalKline = $('#showOriginalKline').is(':checked');
-
- // 创建主图容器
- const mainChartContainer = document.createElement('div');
- mainChartContainer.style.width = '100%';
- mainChartContainer.style.position = 'absolute';
- mainChartContainer.style.top = '0';
- mainChartContainer.style.left = '0';
- mainChartContainer.style.right = '0';
-
- // 创建成交量副图容器
- const volumeChartContainer = document.createElement('div');
- volumeChartContainer.style.width = '100%';
- volumeChartContainer.style.position = 'absolute';
- volumeChartContainer.style.left = '0';
- volumeChartContainer.style.right = '0';
- volumeChartContainer.style.borderTop = '1px solid #e0e0e0';
-
- // 添加ATR图表容器
- const atrChartContainer = document.createElement('div');
- atrChartContainer.style.width = '100%';
- atrChartContainer.style.position = 'absolute';
- atrChartContainer.style.left = '0';
- atrChartContainer.style.right = '0';
- atrChartContainer.style.borderTop = '1px solid #e0e0e0';
+ chartTvRenderIndicators(ctx);
+ chartTvRenderChan(ctx);
+ chartTvRenderOverlays(ctx);
+ chartTvFinalize(ctx);
- // 如果需要显示MACD,创建MACD容器
- let macdChartContainer = null;
- let chanMacdChartContainer = null;
- if (showMacd) {
- // 仅显示新的 ChanMACD 图:让其占用原 MACD+ChanMACD 的整体高度
- // 新布局:主图(40%) → ChanMACD(30%) → 成交量(17.5%) → ATR(12.5%)
- mainChartContainer.style.height = '40%';
-
- // 隐藏旧 MACD 容器(不创建)
- // 创建 ChanMACD 容器占据原 MACD+ChanMACD 高度(30%)
- chanMacdChartContainer = document.createElement('div');
- chanMacdChartContainer.style.width = '100%';
- chanMacdChartContainer.style.height = '30%';
- chanMacdChartContainer.style.position = 'absolute';
- chanMacdChartContainer.style.top = '40%';
- chanMacdChartContainer.style.left = '0';
- chanMacdChartContainer.style.right = '0';
- chanMacdChartContainer.style.borderTop = '1px solid #e0e0e0';
- chanMacdChartContainer.style.zIndex = '10';
- // 水印:便于区分是新的 ChanMACD 子图
- const chanMacdWatermark = document.createElement('div');
- chanMacdWatermark.textContent = 'ChanMACD';
- chanMacdWatermark.style.position = 'absolute';
- chanMacdWatermark.style.top = '4px';
- chanMacdWatermark.style.left = '8px';
- chanMacdWatermark.style.fontSize = '11px';
- chanMacdWatermark.style.color = '#888';
- chanMacdWatermark.style.pointerEvents = 'none';
- chanMacdChartContainer.appendChild(chanMacdWatermark);
-
- // 成交量位于 ChanMACD 之下
- volumeChartContainer.style.top = '70%';
- volumeChartContainer.style.height = '17.5%';
-
- // ATR 位于最底部
- atrChartContainer.style.top = '87.5%';
- atrChartContainer.style.height = '12.5%';
- } else {
- // 不显示MACD时的高度 - 主图、成交量图和ATR图分配
- mainChartContainer.style.height = '55%'; // 主图占55%
- volumeChartContainer.style.top = '55%';
- volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5%
-
- atrChartContainer.style.top = '77.5%'; // ATR图从77.5%位置开始
- atrChartContainer.style.height = '22.5%'; // ATR图占22.5%
- }
-
- container.appendChild(mainChartContainer);
- container.appendChild(volumeChartContainer);
- container.appendChild(atrChartContainer);
- if (showMacd) {
- // 只追加新的 ChanMACD 容器
- container.appendChild(chanMacdChartContainer);
- }
-
- // 防止同步过程中的无限循环(实际同步由 bindSyncEvents 负责)
-
- // 创建统一的图表选项
- const createChartOptions = (showTimeScale = true, chartType = 'main') => {
- // 根据图表类型确定高度
- let chartHeight;
- if (chartType === 'main') {
- chartHeight = mainChartContainer.clientHeight;
- } else if (chartType === 'volume') {
- chartHeight = volumeChartContainer.clientHeight;
- } else if (chartType === 'atr') {
- chartHeight = atrChartContainer.clientHeight;
- } else if (chartType === 'macd') {
- chartHeight = macdChartContainer ? macdChartContainer.clientHeight : 0;
- } else if (chartType === 'chanmacd') {
- chartHeight = chanMacdChartContainer ? chanMacdChartContainer.clientHeight : 0;
- } else {
- chartHeight = mainChartContainer.clientHeight;
- }
-
- const baseOptions = {
- width: mainChartContainer.clientWidth,
- height: chartHeight,
- layout: {
- background: { color: '#ffffff' },
- textColor: '#333',
- },
- grid: {
- vertLines: { color: '#f0f0f0' },
- horzLines: { color: '#f0f0f0' },
- },
- crosshair: {
- mode: LightweightCharts.CrosshairMode.Normal,
- // 添加十字线工具提示本地化配置
- horzLine: {
- labelVisible: true,
- },
- vertLine: {
- labelVisible: true,
- // 自定义时间格式化
- labelFormatter: (time) => {
- const selectedTimezone = $('#timezone').val();
- try {
- const date = new Date(time * 1000);
- if (symbolConfig.type === 'a_stock') {
- // A股使用中国时区格式
- return date.toLocaleString('zh-CN', {
- timeZone: 'Asia/Shanghai',
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit'
- });
- } else {
- return date.toLocaleString('zh-CN', {
- timeZone: selectedTimezone,
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit'
- });
- }
- } catch (e) {
- console.error('十字线时间格式化错误:', e);
- return new Date(time * 1000).toLocaleString();
- }
- },
- },
- },
- rightPriceScale: {
- borderColor: '#ddd',
- scaleMargins: {
- top: 0.1,
- bottom: 0.1,
- },
- // 为标签留出更多空间,防止遮挡
- minimumWidth: 80,
- },
- // 添加左边距配置
- leftPriceScale: {
- visible: false,
- },
- // 添加本地化选项,确保所有时间显示都使用选定的时区
- localization: {
- timeFormatter: (time) => {
- const selectedTimezone = $('#timezone').val();
- try {
- const date = new Date(time * 1000);
- if (symbolConfig.type === 'a_stock') {
- // A股使用中国时区格式
- return date.toLocaleString('zh-CN', {
- timeZone: 'Asia/Shanghai',
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit'
- });
- } else {
- return date.toLocaleString('zh-CN', {
- timeZone: selectedTimezone,
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit'
- });
- }
- } catch (e) {
- console.error('全局时间格式化错误:', e);
- return new Date(time * 1000).toLocaleString();
- }
- }
- },
- timeScale: {
- timeVisible: true,
- secondsVisible: false,
- visible: showTimeScale,
- borderColor: '#ddd',
- barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10,
- // 确保所有图表使用相同的边距设置
- rightOffset: 12,
- // 移除可能影响拖动的固定边缘设置
- // fixLeftEdge: true,
- // fixRightEdge: true,
- lockVisibleTimeRangeOnResize: true,
- tickMarkFormatter: (time) => {
- const selectedTimezone = symbolConfig.type === 'a_stock' ? 'Asia/Shanghai' : $('#timezone').val();
- try {
- // 使用完整的配置确保时区正确应用
- const date = new Date(time * 1000);
- console.log('格式化时间:', time, '转换为:', date.toISOString(), '时区:', selectedTimezone);
-
- return date.toLocaleString('zh-CN', {
- timeZone: selectedTimezone,
- month: 'numeric',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- });
- } catch (e) {
- console.error('时间格式化错误:', e);
- // 如果时区格式化失败,返回简单格式
- return new Date(time * 1000).toLocaleString();
- }
- },
- },
- };
-
- // 根据交易对类型调整配置
- return adjustChartForSymbolType(baseOptions, symbolConfig);
- };
-
- // 创建主图表
- const mainChart = LightweightCharts.createChart(mainChartContainer, createChartOptions(true, 'main'));
-
- // Cycle Summary 挂到主图左下角(相对 K 线主图 pane,而非整图底边)
- (function mountWyckoffCycleSummary() {
- var summaryEl = document.getElementById('wyckoffCycleSummary');
- if (!summaryEl) {
- summaryEl = document.createElement('div');
- summaryEl.id = 'wyckoffCycleSummary';
- summaryEl.className = 'wyckoff-cycle-summary';
- summaryEl.setAttribute('aria-live', 'polite');
- }
- mainChartContainer.appendChild(summaryEl);
- if (typeof renderWyckoffCycleSummary === 'function') {
- try { renderWyckoffCycleSummary(); } catch (e) {}
- }
- })();
-
- // 创建成交量图表 - 只显示底部的时间轴
- const volumeChart = LightweightCharts.createChart(volumeChartContainer, createChartOptions(false, 'volume'));
-
- // 创建ATR图表
- const atrChart = LightweightCharts.createChart(atrChartContainer, createChartOptions(false, 'atr'));
-
- // 创建MACD图表(如果需要):仅创建新的 ChanMACD 图
- let macdChart = null;
- let chanMacdChart = null;
- if (showMacd) {
- chanMacdChart = LightweightCharts.createChart(chanMacdChartContainer, createChartOptions(false, 'chanmacd'));
- }
-
- // 创建主价格系列并设置数据(支持多种图表类型)
- (function(){
- const klineType = ($('#klineType').val() || 'candlestick');
- // 先清空旧的主系列引用
- tvWidget.series.candleSeries = null;
- tvWidget.series.lineSeries = null;
- tvWidget.series.barSeries = null;
- tvWidget.series.areaSeries = null;
- tvWidget.series.baselineSeries = null;
- tvWidget.series.renkoSeries = null;
- tvWidget.series.heikinSeries = null;
-
- if (klineType === 'candlestick') {
- const series = mainChart.addCandlestickSeries({
- upColor: '#28a745',
- downColor: '#dc3545',
- borderVisible: false,
- wickUpColor: '#28a745',
- wickDownColor: '#dc3545',
- });
- series.setData(candles);
- tvWidget.series.candleSeries = series;
- } else if (klineType === 'renko') {
- const series = mainChart.addCandlestickSeries({
- upColor: '#28a745',
- downColor: '#dc3545',
- borderVisible: false,
- wickUpColor: '#28a745',
- wickDownColor: '#dc3545',
- });
- const bricks = buildRenkoFromCandles(candles);
- series.setData(bricks);
- tvWidget.series.renkoSeries = series;
- } else if (klineType === 'heikin') {
- const series = mainChart.addCandlestickSeries({
- upColor: '#28a745',
- downColor: '#dc3545',
- borderVisible: false,
- wickUpColor: '#28a745',
- wickDownColor: '#dc3545',
- });
- const hk = buildHeikinFromCandles(candles);
- series.setData(hk);
- tvWidget.series.heikinSeries = series;
- } else if (klineType === 'bar') {
- const series = mainChart.addBarSeries({
- upColor: '#28a745',
- downColor: '#dc3545',
- thinBars: false
- });
- series.setData(candles);
- tvWidget.series.barSeries = series;
- } else if (klineType === 'line') {
- const series = mainChart.addLineSeries({
- color: '#2962FF',
- lineWidth: 2,
- crosshairMarkerVisible: true,
- lastValueVisible: true,
- priceLineVisible: true,
- });
- const lineData = candles.map(c => ({ time: c.time, value: c.close }));
- series.setData(lineData);
- tvWidget.series.lineSeries = series;
- } else if (klineType === 'area') {
- const series = mainChart.addAreaSeries({
- topColor: 'rgba(41, 98, 255, 0.4)',
- bottomColor: 'rgba(41, 98, 255, 0.0)',
- lineColor: '#2962FF',
- lineWidth: 2,
- });
- const areaData = candles.map(c => ({ time: c.time, value: c.close }));
- series.setData(areaData);
- tvWidget.series.areaSeries = series;
- } else if (klineType === 'baseline') {
- const series = mainChart.addBaselineSeries({
- baseValue: { type: 'price', price: candles.length ? candles[candles.length - 1].close : 0 },
- topLineColor: '#26a69a',
- bottomLineColor: '#ef5350',
- topFillColor1: 'rgba(38, 166, 154, 0.28)',
- topFillColor2: 'rgba(38, 166, 154, 0.05)',
- bottomFillColor1: 'rgba(239, 83, 80, 0.28)',
- bottomFillColor2: 'rgba(239, 83, 80, 0.05)'
- });
- const baseData = candles.map(c => ({ time: c.time, value: c.close }));
- series.setData(baseData);
- tvWidget.series.baselineSeries = series;
- } else if (klineType === 'klc') {
- // KLC显示模式 - 使用蜡烛线显示KLC数据
- const series = mainChart.addCandlestickSeries({
- upColor: '#28a745',
- downColor: '#dc3545',
- borderVisible: false,
- wickUpColor: '#28a745',
- wickDownColor: '#dc3545',
- });
- // 使用KLC数据创建蜡烛图
- const klcCandles = buildKLCFromAnalysis(currentData);
- series.setData(klcCandles);
- tvWidget.series.klcSeries = series;
- }
- })();
-
- // 转换成交量数据 - 与K线周期一致
- let volumes = [];
- const volumeDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
-
- console.log('成交量数据源选择:', klinePeriodLabel);
- console.log('成交量数据长度:', volumeDataSource.length);
-
- if (volumeDataSource && Array.isArray(volumeDataSource)) {
- volumes = volumeDataSource.map(kline => {
- // 使用与K线和MACD完全相同的时间戳计算方式
- const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
- return {
- time: timestamp,
- value: parseFloat(kline.volume),
- color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)',
- };
- });
-
- console.log('处理后的成交量数据点数:', volumes.length);
- }
-
- // 添加成交量图表
- const volumeSeries = volumeChart.addHistogramSeries({
- color: '#26a69a',
- priceFormat: {
- type: 'volume',
- },
- title: '成交量',
- });
- volumeSeries.setData(volumes);
- tvWidget.series.volumeSeries = volumeSeries;
-
- // 添加ATR图表
- const atrLineSeries = atrChart.addLineSeries({
- color: '#FF9800',
- lineWidth: 2,
- title: 'ATR',
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- // 准备ATR数据
- const atrData = [];
- const atrKlineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
- const atrDataSource = useSubSubPeriod ? (currentData.sub_sub_atr || currentData.atr) : (useElementPeriod ? (currentData.element_atr || currentData.atr) : currentData.atr);
-
- console.log('ATR数据源选择:', klinePeriodLabel);
- console.log('ATR数据长度:', atrDataSource ? atrDataSource.length : 0);
- console.log('K线数据长度:', atrKlineDataSource ? atrKlineDataSource.length : 0);
-
- if (atrDataSource && Array.isArray(atrDataSource) && atrKlineDataSource && Array.isArray(atrKlineDataSource)) {
- // 关键修复:为每个K线时间点都创建ATR数据点,包括没有ATR值的前期数据
- for (let i = 0; i < atrKlineDataSource.length; i++) {
- const kline = atrKlineDataSource[i];
- const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
-
- // 为每个时间点都添加数据以保持时间轴对齐,但ATR为0时不显示
- if (atrDataSource[i] !== undefined) {
- if (atrDataSource[i] > 0) {
- // ATR有效值,正常显示
- atrData.push({
- time: timestamp,
- value: atrDataSource[i]
- });
- } else {
- // ATR为0,添加时间点但不显示线条(使用undefined作为value)
- atrData.push({
- time: timestamp,
- value: undefined
- });
- }
- }
- }
-
- console.log('处理后的ATR数据点数:', atrData.length);
- console.log('ATR数据样本:', atrData.slice(0, 5));
- }
- console.log('处理后的ATR数据点数:', atrData.length);
- atrLineSeries.setData(atrData);
- tvWidget.series.atrLineSeries = atrLineSeries;
-
- // 旧 MACD 图已移除,不再绘制(保留占位但彻底禁用)
- if (FEATURES.legacyMacd && showMacd && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data)) {
- // 创建MACD线
- const macdLineSeries = macdChart.addLineSeries({
- color: '#2962FF',
- lineWidth: 1,
- title: 'MACD',
- lastValueVisible: false, // 禁用最后值标签,防止遮挡
- priceLineVisible: false, // 禁用价格线
- });
-
- // 创建信号线
- const signalLineSeries = macdChart.addLineSeries({
- color: '#FF6B6B',
- lineWidth: 1,
- title: 'Signal',
- lastValueVisible: false, // 禁用最后值标签,防止遮挡
- priceLineVisible: false, // 禁用价格线
- });
-
- // 创建直方图
- const histogramSeries = macdChart.addHistogramSeries({
- color: '#26a69a',
- title: 'Histogram',
- priceFormat: {
- type: 'price',
- precision: 4,
- },
- });
-
- // 提取MACD数据 - 使用和K线数据相同的时间处理逻辑
- const macdData = [];
- const signalData = [];
- const histogramData = [];
-
- // 使用与K线数据相同的数据源来确保时间对齐
- const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
- const macdDataSource = useElementPeriod ?
- (currentData.element_macd || currentData.macd) : // 如果有次周期MACD数据则使用,否则使用主周期
- currentData.macd; // 主周期使用主周期MACD数据
-
- console.log('MACD数据源选择:', useElementPeriod ? '次周期' : '主周期');
- console.log('K线数据长度:', klineDataSource.length);
- console.log('MACD数据:', macdDataSource);
-
- for (let i = 0; i < klineDataSource.length; i++) {
- const kline = klineDataSource[i];
- // 使用与K线完全相同的时间戳计算方式
- const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
-
- if (macdDataSource && macdDataSource.macd && macdDataSource.macd[i] !== undefined) {
- macdData.push({
- time: timestamp,
- value: macdDataSource.macd[i]
- });
-
- signalData.push({
- time: timestamp,
- value: macdDataSource.signal[i]
- });
-
- // 设置直方图颜色
- const histValue = macdDataSource.histogram[i];
- histogramData.push({
- time: timestamp,
- value: histValue,
- color: histValue >= 0 ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)'
- });
- }
- }
-
- console.log('处理后的MACD数据点数:', macdData.length);
-
- macdLineSeries.setData(macdData);
- signalLineSeries.setData(signalData);
- histogramSeries.setData(histogramData);
-
- tvWidget.series.macdLineSeries = macdLineSeries;
- tvWidget.series.signalLineSeries = signalLineSeries;
- tvWidget.series.histogramSeries = histogramSeries;
- }
- // 添加ChanMACD图表
- console.log('ChanMACD图表创建条件检查:', {
- showMacd: showMacd,
- chanMacdChart: !!chanMacdChart,
- hasMacd: !!currentData.macd,
- hasKlineData: !!currentData.kline_data,
- isArray: Array.isArray(currentData.kline_data)
- });
- // 在创建 ChanMACD 前,确保一次性同步 U 显示开关到全局(默认不显示)
- if (typeof window.showUOnMain === 'undefined') {
- window.showUOnMain = $('#toggleUOnMain').is(':checked');
- }
- if (typeof window.showUOnElement === 'undefined') {
- window.showUOnElement = $('#toggleUOnElement').is(':checked');
- }
- if (typeof window.showUOnSubSub === 'undefined') {
- window.showUOnSubSub = $('#toggleUOnSubSub').is(':checked');
- }
- if (showMacd && chanMacdChart && ((useSubSubPeriod && currentData.sub_sub_macd) || (useElementPeriod && currentData.element_macd) || currentData.macd) && (useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data))) {
- console.log('✅ 开始创建 ChanMACD 系列');
- // 创建ChanMACD线系列
- const chanMacdLineSeries = chanMacdChart.addLineSeries({
- color: '#2962FF',
- lineWidth: 1,
- title: 'ChanMACD',
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- // 创建ChanMACD信号线系列
- const chanMacdSignalSeries = chanMacdChart.addLineSeries({
- color: '#FF6B6B',
- lineWidth: 1,
- title: 'ChanSignal',
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- // 创建ChanMACD柱状图系列
- const chanMacdHistSeries = chanMacdChart.addHistogramSeries({
- color: '#26a69a',
- title: 'ChanHistogram',
- priceFormat: {
- type: 'price',
- precision: 4,
- },
- });
-
- // 设置ChanMACD图表的字体大小
- chanMacdChart.applyOptions({
- layout: {
- fontSize: 10, // 设置更小的字体大小
- },
- rightPriceScale: {
- fontSize: 10, // 设置右侧价格轴的字体大小
- },
- timeScale: {
- fontSize: 10, // 设置时间轴的字体大小
- },
- });
-
- // 使用与主图一致的数据源(小周期开启时使用小周期MACD与K线)
- const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
- const macdDataSource = useSubSubPeriod ? (currentData.sub_sub_macd || currentData.macd) : (useElementPeriod ? (currentData.element_macd || currentData.macd) : currentData.macd);
-
- // 准备ChanMACD数据
- const chanMacdData = [];
- const chanSignalData = [];
- const chanHistData = [];
-
- console.log('ChanMACD数据源检查:', {
- klineDataSourceLength: klineDataSource.length,
- macdDataSource: !!macdDataSource,
- macdLength: macdDataSource ? macdDataSource.macd.length : 0
- });
-
- console.log('ChanMACD数据源检查:', {
- klineDataSourceLength: klineDataSource.length,
- macdDataSource: !!macdDataSource,
- macdLength: macdDataSource ? macdDataSource.macd.length : 0
- });
-
- for (let i = 0; i < klineDataSource.length; i++) {
- const kline = klineDataSource[i];
- if (kline && kline.date &&
- i < macdDataSource.macd.length &&
- macdDataSource.macd[i] !== null && macdDataSource.macd[i] !== undefined) {
-
- // 使用与K线完全相同的时间戳计算方式
- const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
-
- chanMacdData.push({
- time: timestamp,
- value: macdDataSource.macd[i]
- });
-
- chanSignalData.push({
- time: timestamp,
- value: macdDataSource.signal[i]
- });
-
- chanHistData.push({
- time: timestamp,
- value: macdDataSource.histogram[i],
- color: macdDataSource.histogram[i] >= 0 ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)'
- });
- }
- }
-
- console.log('ChanMACD数据处理完成:', {
- chanMacdDataLength: chanMacdData.length,
- chanSignalDataLength: chanSignalData.length,
- chanHistDataLength: chanHistData.length,
- sampleData: chanMacdData.length > 0 ? chanMacdData[0] : null
- });
-
- // 设置ChanMACD数据
- console.log('ChanMACD数据长度:', chanMacdData.length, chanSignalData.length, chanHistData.length);
-
- if (chanMacdData.length > 0) {
- chanMacdLineSeries.setData(chanMacdData);
- chanMacdSignalSeries.setData(chanSignalData);
- chanMacdHistSeries.setData(chanHistData);
- console.log('✅ ChanMACD数据设置成功');
- } else {
- console.warn('⚠️ ChanMACD数据为空,无法设置数据');
- }
-
- // 保存到tvWidget
- tvWidget.series.chanMacdLineSeries = chanMacdLineSeries;
- tvWidget.series.chanMacdSignalSeries = chanMacdSignalSeries;
- tvWidget.series.chanMacdHistSeries = chanMacdHistSeries;
-
- console.log('✅ ChanMACD图表系列已保存到tvWidget');
-
- // 添加ChanMACD分析标注
- // 根据主/次周期开关与各自的"显示U"独立控制
- const cm = useSubSubPeriod ? (currentData.sub_sub_chan_macd || currentData.chan_macd) : (useElementPeriod ? (currentData.element_chan_macd || currentData.chan_macd) : currentData.chan_macd);
- // 默认不显示,必须用户勾选对应复选框
- const allowU = useSubSubPeriod ? !!window.showUOnSubSub : (useElementPeriod ? !!window.showUOnElement : !!window.showUOnMain);
- if (cm && allowU) {
- console.log('添加ChanMACD分析标注:', {
- segListLength: cm.seg_list ? cm.seg_list.length : 0,
- unittfListLength: cm.unittf_list ? cm.unittf_list.length : 0,
- histsetListLength: cm.histset_list ? cm.histset_list.length : 0
- });
-
- // 详细检查段数据
- if (cm.seg_list && cm.seg_list.length > 0) {
- console.log('段数据详情:', cm.seg_list.slice(0, 3)); // 显示前3个段
- } else {
- console.log('⚠️ 段数据为空或不存在');
- }
-
- addAllChanMacdMarkers(
- cm.seg_list || [],
- cm.unittf_list || [],
- cm.histset_list || [],
- {
- high_position_list: cm.high_position_list || [],
- high_empty_list: cm.high_empty_list || [],
- low_position_list: cm.low_position_list || [],
- low_empty_list: cm.low_empty_list || [],
- return_zero_list: cm.return_zero_list || [],
- cross0_up_list: cm.cross0_up_list || [],
- cross0_down_list: cm.cross0_down_list || []
- }
- );
-
- // 同时从主/次周期的 klu_list 提取 SD/CD 标记,分别使用不同样式
- try {
- const mainCm = currentData.chan_macd || {};
- const elementCm = currentData.element_chan_macd || {};
-
- const mainMarkers = [];
- const elementMarkers = [];
-
- // 基于时间构建 MACD 值映射,便于按时间快速获取对应的 MACD 值
- const buildMacdTimeMap = (macdObj, klineArr) => {
- const map = new Map();
- if (!macdObj || !klineArr || !Array.isArray(klineArr)) return map;
- for (let i = 0; i < klineArr.length; i++) {
- const k = klineArr[i];
- if (!k || !k.date) continue;
- const t = Math.floor(new Date(k.date).getTime() / 1000);
- const val = (macdObj.macd && macdObj.macd[i] !== undefined && macdObj.macd[i] !== null) ? macdObj.macd[i] : null;
- map.set(t, val);
- }
- return map;
- };
- const mainMacdMap = buildMacdTimeMap(currentData.macd, currentData.kline_data);
- const elementMacdMap = buildMacdTimeMap(
- (currentData.element_macd || currentData.macd),
- (currentData.element_kline_data || currentData.kline_data)
- );
-
- // 主周期 U 标记(蓝/橙,与原样式一致)
- if (window.showUOnMain && Array.isArray(mainCm.klu_list)) {
- mainCm.klu_list.forEach((item) => {
- if (!item || !item.time) return;
- const ts = Math.floor(new Date(item.time).getTime() / 1000);
- if (isNaN(ts)) return;
- if (Number(item.separate_div) > 0) {
- const macdVal = mainMacdMap.get(ts);
- const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
- mainMarkers.push({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
- }
- if (item.continue_div === true) {
- const macdVal = mainMacdMap.get(ts);
- const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
- mainMarkers.push({ time: ts, position: posCd, color: '#ff9800', shape: 'arrowDown', text: 'CD', size: 0.6 });
- }
- if (item.near0_return && Number(item.near0_return) > 0) {
- mainMarkers.push({ time: ts, position: 'belowBar', color: '#8bc34a', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
- }
- });
- }
-
- // 次周期 U 标记(使用不同配色以区分)
- if (window.showUOnElement && Array.isArray(elementCm.klu_list)) {
- elementCm.klu_list.forEach((item) => {
- if (!item || !item.time) return;
- const ts = Math.floor(new Date(item.time).getTime() / 1000);
- if (isNaN(ts)) return;
- if (Number(item.separate_div) > 0) {
- const macdVal = elementMacdMap.get(ts);
- const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
- elementMarkers.push({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
- }
- if (item.continue_div === true) {
- const macdVal = elementMacdMap.get(ts);
- const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
- elementMarkers.push({ time: ts, position: posCd, color: '#4caf50', shape: 'arrowDown', text: 'CD', size: 0.6 });
- }
- if (item.near0_return && Number(item.near0_return) > 0) {
- elementMarkers.push({ time: ts, position: 'belowBar', color: '#009688', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
- }
- });
- }
-
- const subSubCm = currentData.sub_sub_chan_macd || {};
- const subSubMarkers = [];
- const subSubMacdMap = buildMacdTimeMap(currentData.macd, currentData.kline_data);
- if (window.showUOnSubSub && Array.isArray(subSubCm.klu_list)) {
- subSubCm.klu_list.forEach((item) => {
- if (!item || !item.time) return;
- const ts = Math.floor(new Date(item.time).getTime() / 1000);
- if (isNaN(ts)) return;
- if (Number(item.separate_div) > 0) {
- const macdVal = subSubMacdMap.get(ts);
- const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
- subSubMarkers.push({ time: ts, position: posSd, color: '#00897b', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
- }
- if (item.continue_div === true) {
- const macdVal = subSubMacdMap.get(ts);
- const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
- subSubMarkers.push({ time: ts, position: posCd, color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 });
- }
- if (item.near0_return && Number(item.near0_return) > 0) {
- subSubMarkers.push({ time: ts, position: 'belowBar', color: '#00695c', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
- }
- });
- }
- window.kluDivMarkersSubSub = subSubMarkers;
-
- // 保存到全局,供主图合并标记使用
- window.kluDivMarkersMain = mainMarkers;
- window.kluDivMarkersElement = elementMarkers;
- } catch (e) {
- console.warn('处理 KLU 背驰标记出错:', e);
- window.kluDivMarkersMain = [];
- window.kluDivMarkersElement = [];
- window.kluDivMarkersSubSub = [];
- }
- } else {
- console.log('⚠️ 没有ChanMACD分析数据');
- // 无数据时清空本次的 KLU 背驰标记
- window.kluDivMarkersMain = [];
- window.kluDivMarkersElement = [];
- window.kluDivMarkersSubSub = [];
- }
- } else {
- console.log('⚠️ ChanMACD图表创建条件不满足');
- }
- // 独立于当前显示周期:计算主/次周期 SD/CD 标记(用于主图合并显示)
- try {
- const mainCmAll = currentData.chan_macd || {};
- const elementCmAll = currentData.element_chan_macd || {};
- const mainMarkersAll = [];
- const elementMarkersAll = [];
-
- // 构建 MACD 时间映射,用于依据 MACD 正负决定 SD/CD 的显示上下位置
- const buildMacdTimeMapAll = (macdObj, klineArr) => {
- const map = new Map();
- if (!macdObj || !klineArr || !Array.isArray(klineArr)) return map;
- for (let i = 0; i < klineArr.length; i++) {
- const k = klineArr[i];
- if (!k || !k.date) continue;
- const t = Math.floor(new Date(k.date).getTime() / 1000);
- const val = (macdObj.macd && macdObj.macd[i] !== undefined && macdObj.macd[i] !== null) ? macdObj.macd[i] : null;
- map.set(t, val);
- }
- return map;
- };
- const mainMacdMapAll = buildMacdTimeMapAll(currentData.macd, currentData.kline_data);
- const elementMacdMapAll = buildMacdTimeMapAll(
- (currentData.element_macd || currentData.macd),
- (currentData.element_kline_data || currentData.kline_data)
- );
- if ((typeof window.showUOnMain === 'undefined' ? false : window.showUOnMain) && Array.isArray(mainCmAll.klu_list)) {
- mainCmAll.klu_list.forEach((item) => {
- if (!item || !item.time) return;
- const ts = Math.floor(new Date(item.time).getTime() / 1000);
- if (isNaN(ts)) return;
- if (Number(item.separate_div) > 0) {
- const macdVal = mainMacdMapAll.get(ts);
- const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
- mainMarkersAll.push({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
- }
- if (item.continue_div === true) {
- const macdVal = mainMacdMapAll.get(ts);
- const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
- mainMarkersAll.push({ time: ts, position: posCd, color: '#ff9800', shape: 'arrowDown', text: 'CD', size: 0.6 });
- }
- if (item.near0_return && Number(item.near0_return) > 0) {
- mainMarkersAll.push({ time: ts, position: 'belowBar', color: '#8bc34a', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
- }
- });
- }
- if ((typeof window.showUOnElement === 'undefined' ? false : window.showUOnElement) && Array.isArray(elementCmAll.klu_list)) {
- elementCmAll.klu_list.forEach((item) => {
- if (!item || !item.time) return;
- const ts = Math.floor(new Date(item.time).getTime() / 1000);
- if (isNaN(ts)) return;
- if (Number(item.separate_div) > 0) {
- const macdVal = elementMacdMapAll.get(ts);
- const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
- elementMarkersAll.push({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
- }
- if (item.continue_div === true) {
- const macdVal = elementMacdMapAll.get(ts);
- const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
- elementMarkersAll.push({ time: ts, position: posCd, color: '#4caf50', shape: 'arrowDown', text: 'CD', size: 0.6 });
- }
- if (item.near0_return && Number(item.near0_return) > 0) {
- elementMarkersAll.push({ time: ts, position: 'belowBar', color: '#009688', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
- }
- });
- }
- window.kluDivMarkersMain = mainMarkersAll;
- window.kluDivMarkersElement = elementMarkersAll;
- const subSubCmAll = currentData.sub_sub_chan_macd || {};
- const subSubMarkersAll = [];
- if (window.showUOnSubSub && Array.isArray(subSubCmAll.klu_list)) {
- subSubCmAll.klu_list.forEach((item) => {
- if (!item || !item.time) return;
- const ts = Math.floor(new Date(item.time).getTime() / 1000);
- if (isNaN(ts)) return;
- if (Number(item.separate_div) > 0) {
- subSubMarkersAll.push({ time: ts, position: 'aboveBar', color: '#00897b', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
- }
- if (item.continue_div === true) {
- subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 });
- }
- if (item.near0_return && Number(item.near0_return) > 0) {
- subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#00695c', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
- }
- });
- }
- window.kluDivMarkersSubSub = subSubMarkersAll;
- } catch (e) {
- console.warn('独立计算 KLU 背驰标记出错:', e);
- window.kluDivMarkersMain = [];
- window.kluDivMarkersElement = [];
- window.kluDivMarkersSubSub = [];
- }
-
- // 图表同步事件统一由文末 bindSyncEvents 注册(带 cleanup),此处不再重复 addEventListener,
- // 否则每次自动刷新/重建都会在 document/window 上堆积监听导致内存泄漏。
- // 显示笔的绘制 - 分别处理主周期、次周期和次次周期
- if ($('#showMainBi').is(':checked') || $('#showElementBi').is(':checked') || $('#showSubSubBi').is(':checked')) {
- console.log('绘制笔 - 已启用');
- let biLines = [];
-
- // 主周期笔
- if ($('#showMainBi').is(':checked') && currentData.bi_list && currentData.bi_list.length > 0) {
- console.log(`绘制主周期笔数据,共${currentData.bi_list.length}条`);
-
- // 清空已有的主周期笔系列
- tvWidget.series.mainBiSeries = [];
- tvWidget.series.mainUncompletedBiSeries = [];
-
- // 遍历处理每个笔
- currentData.bi_list.forEach(function(bi) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
- const endTime = Math.floor(new Date(bi.end_time).getTime() / 1000);
-
- if (isNaN(startTime) || isNaN(endTime)) {
- console.error('主周期笔时间转换错误:', bi.start_time, bi.end_time);
- return;
- }
-
- const startPrice = parseFloat(bi.start_price);
- const endPrice = parseFloat(bi.end_price);
-
- if (isNaN(startPrice) || isNaN(endPrice)) {
- console.error('主周期笔价格转换错误:', bi.start_price, bi.end_price);
- return;
- }
-
- // 添加线段
- biLines.push({
- startTime: startTime,
- endTime: endTime,
- startPrice: startPrice,
- endPrice: endPrice,
- color: bi.direction === 1 ? '#dc3545' : '#28a745', // 主周期笔颜色
- lineWidth: 1,
- });
-
- // 添加到图表对象
- tvWidget.series.mainBiSeries.push({
- time: startTime,
- value: startPrice,
- color: bi.direction === 1 ? '#dc3545' : '#28a745',
- lineWidth: 1
- });
-
- // 在笔的末端添加macd_div值标记
- if (bi.macd_div && bi.macd_div !== 0 && $('#showMainMacdDiv').is(':checked')) {
- console.log(`添加主周期macd_div标记: ${bi.macd_div.toFixed(2)}, 在时间点: ${endTime}`);
-
- const macdDivLabel = mainChart.addLineSeries({
- lastValueVisible: false,
- priceLineVisible: false,
- color: 'transparent', // 设置为透明色
- lineWidth: 0, // 线宽为0
- });
-
- // 添加一个透明的数据点用于承载标记
- macdDivLabel.setData([
- { time: endTime, value: endPrice }
- ]);
-
- // 主周期MACD背离标记根据笔方向显示,远离K线避免与分型重叠
- const markerPosition = bi.direction === 1 ? 'aboveBar' : 'belowBar';
- const textColor = bi.macd_div > 0 ? '#dc3545' : '#28a745';
-
- // 只使用标记,不添加数据点
- macdDivLabel.setMarkers([
- {
- time: endTime,
- position: markerPosition,
- color: textColor,
- text: `${bi.macd_div.toFixed(2)}`, // 添加M前缀区分
- size: 0.6, // 更小的尺寸,远离分型标记
- }
- ]);
- }
- } catch (e) {
- console.error('主周期笔处理出错:', e);
- }
- });
- }
-
- // 次周期笔
- if ($('#showElementBi').is(':checked') && currentData.element_bi_list && currentData.element_bi_list.length > 0) {
- console.log(`绘制次周期笔数据,共${currentData.element_bi_list.length}条`);
-
- // 清空已有的次周期笔系列
- tvWidget.series.elementBiSeries = [];
- tvWidget.series.elementUncompletedBiSeries = [];
-
- currentData.element_bi_list.forEach(function(bi) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
- const endTime = Math.floor(new Date(bi.end_time).getTime() / 1000);
-
- if (isNaN(startTime) || isNaN(endTime)) {
- console.error('次周期笔时间转换错误:', bi.start_time, bi.end_time);
- return;
- }
-
- const startPrice = parseFloat(bi.start_price);
- const endPrice = parseFloat(bi.end_price);
-
- if (isNaN(startPrice) || isNaN(endPrice)) {
- console.error('次周期笔价格转换错误:', bi.start_price, bi.end_price);
- return;
- }
-
- // 添加线段
- biLines.push({
- startTime: startTime,
- endTime: endTime,
- startPrice: startPrice,
- endPrice: endPrice,
- color: bi.direction === 1 ? '#9c27b0' : '#673ab7', // 次周期笔颜色
- lineWidth: 1,
- });
-
- // 添加到图表对象
- tvWidget.series.elementBiSeries.push({
- time: startTime,
- value: startPrice,
- color: bi.direction === 1 ? '#9c27b0' : '#673ab7',
- lineWidth: 1
- });
-
- // 在笔的末端添加macd_div值标记
- if (bi.macd_div && bi.macd_div !== 0 && $('#showElementMacdDiv').is(':checked')) {
- console.log(`添加元素周期macd_div标记: ${bi.macd_div.toFixed(2)}, 在时间点: ${endTime}`);
-
- const macdDivLabel = mainChart.addLineSeries({
- lastValueVisible: false,
- priceLineVisible: false,
- color: 'transparent', // 设置为透明色
- lineWidth: 0, // 线宽为0
- });
-
- // 添加一个透明的数据点用于承载标记
- macdDivLabel.setData([
- { time: endTime, value: endPrice }
- ]);
-
- // 次周期MACD背离标记使用不同位置,进一步避免重叠
- const markerPosition = bi.direction === 1 ? 'aboveBar' : 'belowBar';
- const textColor = bi.macd_div > 0 ? '#9c27b0' : '#673ab7';
-
- // 只使用标记,不添加数据点
- macdDivLabel.setMarkers([
- {
- time: endTime,
- position: markerPosition,
- color: textColor,
- text: `${bi.macd_div.toFixed(2)}`, // 添加E前缀区分次周期
- size: 0.4, // 更小的尺寸,让分型标记有更多空间
- }
- ]);
- }
- } catch (e) {
- console.error('次周期笔处理出错:', e);
- }
- });
- }
-
- // 绘制未完成笔 - 主周期
- if ($('#showMainBi').is(':checked') && currentData.uncompleted_bi_list && currentData.uncompleted_bi_list.length > 0) {
- console.log(`绘制主周期未完成笔数据,共${currentData.uncompleted_bi_list.length}条`);
-
- currentData.uncompleted_bi_list.forEach(function(bi) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
- // 未完成笔的结束时间设为当前K线的最后时间
- const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
-
- if (isNaN(startTime) || isNaN(endTime)) {
- console.error('主周期未完成笔时间转换错误:', bi.start_time);
- return;
- }
-
- const startPrice = parseFloat(bi.start_price);
-
- if (isNaN(startPrice)) {
- console.error('主周期未完成笔价格转换错误:', bi.start_price);
- return;
- }
-
- // 根据笔的方向确定终点价格
- let endPrice;
- const latestKline = currentData.kline_data[currentData.kline_data.length-1];
- if (bi.direction === 1) {
- // 向上笔,终点为最新K线的最高点
- endPrice = parseFloat(latestKline.high);
- } else {
- // 向下笔,终点为最新K线的最低点
- endPrice = parseFloat(latestKline.low);
- }
-
- // 添加未完成笔(红色虚线)
- biLines.push({
- startTime: startTime,
- endTime: endTime,
- startPrice: startPrice,
- endPrice: endPrice,
- color: '#FF0000', // 红色
- lineWidth: 1,
- lineStyle: 2 // 虚线
- });
-
- // 添加到图表对象
- tvWidget.series.mainUncompletedBiSeries.push({
- time: startTime,
- value: startPrice,
- color: '#FF0000',
- lineWidth: 1,
- lineStyle: 2
- });
- } catch (e) {
- console.error('主周期未完成笔处理出错:', e);
- }
- });
- }
-
- // 绘制未完成笔 - 次周期
- if ($('#showElementBi').is(':checked') && currentData.element_uncompleted_bi_list && currentData.element_uncompleted_bi_list.length > 0) {
- console.log(`绘制次周期未完成笔数据,共${currentData.element_uncompleted_bi_list.length}条`);
-
- currentData.element_uncompleted_bi_list.forEach(function(bi) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
- // 未完成笔的结束时间设为当前K线的最后时间
- const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
-
- if (isNaN(startTime) || isNaN(endTime)) {
- console.error('次周期未完成笔时间转换错误:', bi.start_time);
- return;
- }
-
- const startPrice = parseFloat(bi.start_price);
-
- if (isNaN(startPrice)) {
- console.error('次周期未完成笔价格转换错误:', bi.start_price);
- return;
- }
-
- // 根据笔的方向确定终点价格
- let endPrice;
- const latestKline = currentData.kline_data[currentData.kline_data.length-1];
- if (bi.direction === 1) {
- // 向上笔,终点为最新K线的最高点
- endPrice = parseFloat(latestKline.high);
- } else {
- // 向下笔,终点为最新K线的最低点
- endPrice = parseFloat(latestKline.low);
- }
-
- // 添加未完成笔(红色虚线)
- biLines.push({
- startTime: startTime,
- endTime: endTime,
- startPrice: startPrice,
- endPrice: endPrice,
- color: '#FF0000', // 红色
- lineWidth: 1,
- lineStyle: 2 // 虚线
- });
-
- // 添加到图表对象
- tvWidget.series.elementUncompletedBiSeries.push({
- time: startTime,
- value: startPrice,
- color: '#FF0000',
- lineWidth: 1,
- lineStyle: 2
- });
- } catch (e) {
- console.error('次周期未完成笔处理出错:', e);
- }
- });
- }
-
- // 次次周期笔
- if ($('#showSubSubBi').is(':checked') && currentData.sub_sub_bi_list && currentData.sub_sub_bi_list.length > 0) {
- tvWidget.series.subSubBiSeries = [];
- tvWidget.series.subSubUncompletedBiSeries = [];
- currentData.sub_sub_bi_list.forEach(function(bi) {
- try {
- const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
- const endTime = bi.end_time ? Math.floor(new Date(bi.end_time).getTime() / 1000) : 0;
- if (isNaN(startTime) || !endTime) return;
- const startPrice = parseFloat(bi.start_price);
- const endPrice = parseFloat(bi.end_price);
- if (isNaN(startPrice) || isNaN(endPrice)) return;
- biLines.push({
- startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice,
- color: bi.direction === 1 ? '#00897b' : '#26a69a', lineWidth: 1, lineStyle: 0
- });
- tvWidget.series.subSubBiSeries.push({ time: startTime, value: startPrice, color: '#00897b', lineWidth: 1 });
- } catch (e) { console.error('次次周期笔处理出错:', e); }
- });
- }
- // 次次周期未完成笔
- if ($('#showSubSubBi').is(':checked') && currentData.sub_sub_uncompleted_bi_list && currentData.sub_sub_uncompleted_bi_list.length > 0) {
- if (!tvWidget.series.subSubBiSeries) tvWidget.series.subSubBiSeries = [];
- if (!tvWidget.series.subSubUncompletedBiSeries) tvWidget.series.subSubUncompletedBiSeries = [];
- const klineData = currentData.kline_data || [];
- const lastTime = klineData.length ? Math.floor(new Date(klineData[klineData.length-1].date).getTime() / 1000) : 0;
- currentData.sub_sub_uncompleted_bi_list.forEach(function(bi) {
- try {
- const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
- if (isNaN(startTime) || !lastTime) return;
- const startPrice = parseFloat(bi.start_price);
- if (isNaN(startPrice)) return;
- const lastK = klineData[klineData.length-1];
- const endPrice = bi.direction === 1 ? parseFloat(lastK.high) : parseFloat(lastK.low);
- biLines.push({
- startTime: startTime, endTime: lastTime, startPrice: startPrice, endPrice: endPrice,
- color: '#00695c', lineWidth: 1, lineStyle: 2
- });
- tvWidget.series.subSubUncompletedBiSeries.push({ time: startTime, value: startPrice, color: '#00695c', lineWidth: 1, lineStyle: 2 });
- } catch (e) { console.error('次次周期未完成笔处理出错:', e); }
- });
- }
-
- // 添加所有笔到图表
- // 1m 等小周期叠加到大周期时,笔数量会非常大;每条笔创建一个 series 会导致主图渲染退化
- // 这里限制绘制数量,优先保留最新笔,避免把主K线和其他元素“挤没”
- const MAX_BI_LINE_SERIES = 800;
- if (biLines.length > MAX_BI_LINE_SERIES) {
- console.warn(`BI线条过多(${biLines.length}),仅绘制最新 ${MAX_BI_LINE_SERIES} 条以保障主图稳定`);
- }
- const linesToDraw = biLines.length > MAX_BI_LINE_SERIES
- ? biLines.slice(-MAX_BI_LINE_SERIES)
- : biLines;
-
- linesToDraw.forEach(line => {
- try {
- if (!Number.isFinite(line.startTime) || !Number.isFinite(line.endTime)) return;
- if (!Number.isFinite(line.startPrice) || !Number.isFinite(line.endPrice)) return;
- if (line.endTime <= line.startTime) return;
-
- const lineSeries = mainChart.addLineSeries({
- color: line.color,
- lineWidth: line.lineWidth,
- lineStyle: line.lineStyle || 0, // 支持虚线样式
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- lineSeries.setData([
- { time: line.startTime, value: line.startPrice },
- { time: line.endTime, value: line.endPrice }
- ]);
- } catch (e) {
- console.warn('绘制BI线段失败,已跳过单条异常数据:', e);
- }
- });
- } else {
- console.log('绘制笔 - 已禁用');
- }
- // 显示线段的绘制 - 分别处理主周期、次周期和次次周期
- if ($('#showMainSeg').is(':checked') || $('#showElementSeg').is(':checked') || $('#showSubSubSeg').is(':checked')) {
- console.log('绘制线段 - 已启用');
- let segLines = [];
-
- // 主周期线段
- if ($('#showMainSeg').is(':checked') && currentData.seg_list && currentData.seg_list.length > 0) {
- console.log(`绘制主周期线段数据,共${currentData.seg_list.length}条`);
-
- // 清空已有的主周期线段系列
- tvWidget.series.mainSegSeries = [];
- tvWidget.series.mainUncompletedSegSeries = [];
-
- currentData.seg_list.forEach(function(seg) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
- const endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
-
- if (isNaN(startTime) || isNaN(endTime)) {
- console.error('主周期线段时间转换错误:', seg.start_time, seg.end_time);
- return;
- }
-
- const startPrice = parseFloat(seg.start_price);
- const endPrice = parseFloat(seg.end_price);
-
- if (isNaN(startPrice) || isNaN(endPrice)) {
- console.error('主周期线段价格转换错误:', seg.start_price, seg.end_price);
- return;
- }
-
- // 添加线段
- segLines.push({
- startTime: startTime,
- endTime: endTime,
- startPrice: startPrice,
- endPrice: endPrice,
- color: seg.direction === 1 ? '#FF6B6B' : '#4CAF50', // 主周期线段颜色
- lineWidth: 2,
- });
-
- // 添加到图表对象
- tvWidget.series.mainSegSeries.push({
- time: startTime,
- value: startPrice,
- color: seg.direction === 1 ? '#FF6B6B' : '#4CAF50',
- lineWidth: 2
- });
- } catch (e) {
- console.error('主周期线段处理出错:', e);
- }
- });
- }
-
- // 次周期线段
- if ($('#showElementSeg').is(':checked') && currentData.element_seg_list && currentData.element_seg_list.length > 0) {
- console.log(`绘制次周期线段数据,共${currentData.element_seg_list.length}条`);
-
- // 清空已有的次周期线段系列
- tvWidget.series.elementSegSeries = [];
- tvWidget.series.elementUncompletedSegSeries = [];
-
- currentData.element_seg_list.forEach(function(seg) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
- const endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
-
- if (isNaN(startTime) || isNaN(endTime)) {
- console.error('次周期线段时间转换错误:', seg.start_time, seg.end_time);
- return;
- }
-
- const startPrice = parseFloat(seg.start_price);
- const endPrice = parseFloat(seg.end_price);
-
- if (isNaN(startPrice) || isNaN(endPrice)) {
- console.error('次周期线段价格转换错误:', seg.start_price, seg.end_price);
- return;
- }
-
- // 添加线段
- segLines.push({
- startTime: startTime,
- endTime: endTime,
- startPrice: startPrice,
- endPrice: endPrice,
- color: seg.direction === 1 ? '#673ab7' : '#9c27b0', // 次周期线段颜色
- lineWidth: 2,
- });
-
- // 添加到图表对象
- tvWidget.series.elementSegSeries.push({
- time: startTime,
- value: startPrice,
- color: seg.direction === 1 ? '#673ab7' : '#9c27b0',
- lineWidth: 2
- });
- } catch (e) {
- console.error('次周期线段处理出错:', e);
- }
- });
- }
-
- // 绘制未完成线段 - 主周期
- if ($('#showMainSeg').is(':checked') && currentData.uncompleted_seg_list && currentData.uncompleted_seg_list.length > 0) {
- console.log(`绘制主周期未完成线段数据,共${currentData.uncompleted_seg_list.length}条`);
-
- currentData.uncompleted_seg_list.forEach(function(seg) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
-
- if (isNaN(startTime)) {
- console.error('主周期未完成线段时间转换错误:', seg.start_time);
- return;
- }
-
- const startPrice = parseFloat(seg.start_price);
-
- if (isNaN(startPrice)) {
- console.error('主周期未完成线段价格转换错误:', seg.start_price);
- return;
- }
-
- let endTime, endPrice;
-
- if (seg.end_time && seg.end_price) {
- // 有结束时间和价格的未完成线段(倒数第二个等)
- endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
- endPrice = parseFloat(seg.end_price);
-
- if (isNaN(endTime) || isNaN(endPrice)) {
- console.error('主周期未完成线段结束时间或价格转换错误:', seg.end_time, seg.end_price);
- return;
- }
- } else {
- // 没有结束时间和价格的未完成线段(最后一个)
- endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
-
- if (isNaN(endTime)) {
- console.error('主周期未完成线段结束时间转换错误');
- return;
- }
-
- // 根据线段的方向确定终点价格
- const latestKline = currentData.kline_data[currentData.kline_data.length-1];
- if (seg.direction === 1) {
- // 向上线段,终点为最新K线的最高点
- endPrice = parseFloat(latestKline.high);
- } else {
- // 向下线段,终点为最新K线的最低点
- endPrice = parseFloat(latestKline.low);
- }
- }
-
- // 添加未完成线段(红色虚线)
- segLines.push({
- startTime: startTime,
- endTime: endTime,
- startPrice: startPrice,
- endPrice: endPrice,
- color: '#FF0000', // 红色
- lineWidth: 2,
- lineStyle: 2 // 虚线
- });
-
- // 添加到图表对象
- tvWidget.series.mainUncompletedSegSeries.push({
- time: startTime,
- value: startPrice,
- color: '#FF0000',
- lineWidth: 2,
- lineStyle: 2
- });
- } catch (e) {
- console.error('主周期未完成线段处理出错:', e);
- }
- });
- }
-
- // 绘制未完成线段 - 次周期
- if ($('#showElementSeg').is(':checked') && currentData.element_uncompleted_seg_list && currentData.element_uncompleted_seg_list.length > 0) {
- console.log(`绘制次周期未完成线段数据,共${currentData.element_uncompleted_seg_list.length}条`);
-
- currentData.element_uncompleted_seg_list.forEach(function(seg) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
-
- if (isNaN(startTime)) {
- console.error('次周期未完成线段时间转换错误:', seg.start_time);
- return;
- }
-
- const startPrice = parseFloat(seg.start_price);
-
- if (isNaN(startPrice)) {
- console.error('次周期未完成线段价格转换错误:', seg.start_price);
- return;
- }
-
- let endTime, endPrice;
-
- if (seg.end_time && seg.end_price) {
- // 有结束时间和价格的未完成线段(倒数第二个等)
- endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
- endPrice = parseFloat(seg.end_price);
-
- if (isNaN(endTime) || isNaN(endPrice)) {
- console.error('次周期未完成线段结束时间或价格转换错误:', seg.end_time, seg.end_price);
- return;
- }
- } else {
- // 没有结束时间和价格的未完成线段(最后一个)
- endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
-
- if (isNaN(endTime)) {
- console.error('次周期未完成线段结束时间转换错误');
- return;
- }
-
- // 根据线段的方向确定终点价格
- const latestKline = currentData.kline_data[currentData.kline_data.length-1];
- if (seg.direction === 1) {
- // 向上线段,终点为最新K线的最高点
- endPrice = parseFloat(latestKline.high);
- } else {
- // 向下线段,终点为最新K线的最低点
- endPrice = parseFloat(latestKline.low);
- }
- }
-
- // 添加未完成线段(红色虚线)
- segLines.push({
- startTime: startTime,
- endTime: endTime,
- startPrice: startPrice,
- endPrice: endPrice,
- color: '#FF0000', // 红色
- lineWidth: 2,
- lineStyle: 2 // 虚线
- });
-
- // 添加到图表对象
- tvWidget.series.elementUncompletedSegSeries.push({
- time: startTime,
- value: startPrice,
- color: '#FF0000',
- lineWidth: 2,
- lineStyle: 2
- });
- } catch (e) {
- console.error('次周期未完成线段处理出错:', e);
- }
- });
- }
-
- // 次次周期线段
- if ($('#showSubSubSeg').is(':checked') && currentData.sub_sub_seg_list && currentData.sub_sub_seg_list.length > 0) {
- tvWidget.series.subSubSegSeries = [];
- tvWidget.series.subSubUncompletedSegSeries = [];
- currentData.sub_sub_seg_list.forEach(function(seg) {
- try {
- const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
- const endTime = seg.end_time ? Math.floor(new Date(seg.end_time).getTime() / 1000) : 0;
- if (isNaN(startTime) || !endTime) return;
- const startPrice = parseFloat(seg.start_price);
- const endPrice = parseFloat(seg.end_price);
- if (isNaN(startPrice) || isNaN(endPrice)) return;
- segLines.push({
- startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice,
- color: seg.direction === 1 ? '#00897b' : '#26a69a', lineWidth: 2, lineStyle: 0
- });
- tvWidget.series.subSubSegSeries.push({ time: startTime, value: startPrice, color: '#00897b', lineWidth: 2 });
- } catch (e) { console.error('次次周期线段处理出错:', e); }
- });
- }
- // 次次周期未完成线段
- if ($('#showSubSubSeg').is(':checked') && currentData.sub_sub_uncompleted_seg_list && currentData.sub_sub_uncompleted_seg_list.length > 0) {
- if (!tvWidget.series.subSubUncompletedSegSeries) tvWidget.series.subSubUncompletedSegSeries = [];
- const klineDataSeg = currentData.kline_data || [];
- const lastTimeSeg = klineDataSeg.length ? Math.floor(new Date(klineDataSeg[klineDataSeg.length-1].date).getTime() / 1000) : 0;
- currentData.sub_sub_uncompleted_seg_list.forEach(function(seg) {
- try {
- const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
- if (isNaN(startTime) || !lastTimeSeg) return;
- const startPrice = parseFloat(seg.start_price);
- if (isNaN(startPrice)) return;
- let endTime = lastTimeSeg, endPrice;
- if (seg.end_time && seg.end_price) {
- endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
- endPrice = parseFloat(seg.end_price);
- } else {
- const lastK = klineDataSeg[klineDataSeg.length-1];
- endPrice = seg.direction === 1 ? parseFloat(lastK.high) : parseFloat(lastK.low);
- }
- segLines.push({
- startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice,
- color: '#00695c', lineWidth: 2, lineStyle: 2
- });
- tvWidget.series.subSubUncompletedSegSeries.push({ time: startTime, value: startPrice, color: '#00695c', lineWidth: 2 });
- } catch (e) { console.error('次次周期未完成线段处理出错:', e); }
- });
- }
-
- // 添加所有线段到图表
- segLines.forEach(line => {
- const lineSeries = mainChart.addLineSeries({
- color: line.color,
- lineWidth: line.lineWidth,
- lineStyle: line.lineStyle || 0, // 支持虚线样式
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- lineSeries.setData([
- { time: line.startTime, value: line.startPrice },
- { time: line.endTime, value: line.endPrice }
- ]);
- });
- } else {
- console.log('绘制线段 - 已禁用');
- }
- // 显示中枢的绘制 - 分别处理主周期、次周期和次次周期(包含BI中枢,沿用同样样式与开关)
- if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked') || $('#showSubSubZs').is(':checked')) {
- console.log('绘制中枢 - 已启用');
-
- // 主周期中枢
- if ($('#showMainZs').is(':checked') && currentData.zs_list && currentData.zs_list.length > 0) {
- console.log(`绘制主周期中枢数据,共${currentData.zs_list.length}条`);
-
- currentData.zs_list.forEach(function(zs) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = Math.floor(new Date(zs.end_time).getTime() / 1000);
-
- if (isNaN(startTime) || isNaN(endTime)) {
- console.error('主周期中枢时间转换错误:', zs.start_time, zs.end_time);
- return;
- }
-
- const zg = parseFloat(zs.zg); // 中枢上沿
- const zd = parseFloat(zs.zd); // 中枢下沿
- const gg = parseFloat(zs.gg); // 中枢高高
- const dd = parseFloat(zs.dd); // 中枢低低
-
- if (isNaN(zg) || isNaN(zd)) {
- console.error('主周期中枢价格转换错误:', zs.zg, zs.zd);
- return;
- }
-
- // 创建中枢上边界
- const topSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 主周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- topSeries.setData([
- { time: startTime, value: zg },
- { time: endTime, value: zg }
- ]);
-
- // 为下边界创建另一条线
- const bottomSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 主周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- bottomSeries.setData([
- { time: startTime, value: zd },
- { time: endTime, value: zd }
- ]);
-
- // 添加左边界
- const leftSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 主周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- leftSeries.setData([
- { time: startTime, value: zd },
- { time: startTime, value: zg }
- ]);
-
- // 添加右边界
- const rightSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 主周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- rightSeries.setData([
- { time: endTime, value: zd },
- { time: endTime, value: zg }
- ]);
-
- // 绘制gg线(中枢高高)
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 使用中枢自己的颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- ggSeries.setData([
- { time: startTime, value: gg },
- { time: endTime, value: gg }
- ]);
- }
-
- // 绘制dd线(中枢低低)
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 使用中枢自己的颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- ddSeries.setData([
- { time: startTime, value: dd },
- { time: endTime, value: dd }
- ]);
- }
-
- // 添加到图表对象
- tvWidget.series.mainZsSeries.push({
- time: startTime,
- value: zg,
- color: '#F1C40F',
- lineWidth: 1
- });
- tvWidget.series.mainZsSeries.push({
- time: endTime,
- value: zg,
- color: '#F1C40F',
- lineWidth: 1
- });
- tvWidget.series.mainZsSeries.push({
- time: startTime,
- value: zd,
- color: '#F1C40F',
- lineWidth: 1
- });
- tvWidget.series.mainZsSeries.push({
- time: endTime,
- value: zd,
- color: '#F1C40F',
- lineWidth: 1
- });
- } catch (e) {
- console.error('主周期中枢处理出错:', e);
- }
- });
- }
-
- // 次周期中枢
- if ($('#showElementZs').is(':checked') && currentData.element_zs_list && currentData.element_zs_list.length > 0) {
- console.log(`绘制次周期中枢数据,共${currentData.element_zs_list.length}条`);
-
- currentData.element_zs_list.forEach(function(zs) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = Math.floor(new Date(zs.end_time).getTime() / 1000);
-
- if (isNaN(startTime) || isNaN(endTime)) {
- console.error('次周期中枢时间转换错误:', zs.start_time, zs.end_time);
- return;
- }
-
- const zg = parseFloat(zs.zg); // 中枢上沿
- const zd = parseFloat(zs.zd); // 中枢下沿
- const gg = parseFloat(zs.gg); // 中枢高高
- const dd = parseFloat(zs.dd); // 中枢低低
-
- if (isNaN(zg) || isNaN(zd)) {
- console.error('次周期中枢价格转换错误:', zs.zg, zs.zd);
- return;
- }
-
- // 创建中枢上边界
- const topSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 次周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- topSeries.setData([
- { time: startTime, value: zg },
- { time: endTime, value: zg }
- ]);
-
- // 为下边界创建另一条线
- const bottomSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 次周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- bottomSeries.setData([
- { time: startTime, value: zd },
- { time: endTime, value: zd }
- ]);
-
- // 添加左边界
- const leftSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 次周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- leftSeries.setData([
- { time: startTime, value: zd },
- { time: startTime, value: zg }
- ]);
-
- // 添加右边界
- const rightSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 次周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- rightSeries.setData([
- { time: endTime, value: zd },
- { time: endTime, value: zg }
- ]);
-
- // 绘制gg线(中枢高高)
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 使用次周期中枢自己的颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- ggSeries.setData([
- { time: startTime, value: gg },
- { time: endTime, value: gg }
- ]);
- }
-
- // 绘制dd线(中枢低低)
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 使用次周期中枢自己的颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- ddSeries.setData([
- { time: startTime, value: dd },
- { time: endTime, value: dd }
- ]);
- }
-
- // 添加到图表对象
- tvWidget.series.elementZsSeries.push({
- time: startTime,
- value: zg,
- color: '#3f51b5',
- lineWidth: 1
- });
- tvWidget.series.elementZsSeries.push({
- time: endTime,
- value: zg,
- color: '#3f51b5',
- lineWidth: 1
- });
- tvWidget.series.elementZsSeries.push({
- time: startTime,
- value: zd,
- color: '#3f51b5',
- lineWidth: 1
- });
- tvWidget.series.elementZsSeries.push({
- time: endTime,
- value: zd,
- color: '#3f51b5',
- lineWidth: 1
- });
- } catch (e) {
- console.error('次周期中枢处理出错:', e);
- }
- });
- }
- // 次次周期SEG中枢
- if ($('#showSubSubZs').is(':checked') && currentData.sub_sub_zs_list && currentData.sub_sub_zs_list.length > 0) {
- const subSubZsColor = '#00897b';
- currentData.sub_sub_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : 0;
- if (isNaN(startTime) || !endTime) return;
- const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) return;
- mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
- mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
- mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
- if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
- } catch (e) { console.error('次次周期中枢处理出错:', e); }
- });
- }
- } else {
- console.log('绘制中枢 - 已禁用');
- }
- // BI中枢(已完成)- 使用独立的BI开关
- if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) {
- try {
- console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`);
- } catch (e) {}
- currentData.bi_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
- if (isNaN(startTime) || isNaN(endTime)) { return; }
- const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) { return; }
- const color = '#F1C40F';
- const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
- const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
- const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
- }
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
- }
- } catch (e) { console.error('主周期BI中枢处理出错:', e); }
- });
- }
- if ($('#showSubSubBiZs').is(':checked') && currentData.sub_sub_bi_zs_list && currentData.sub_sub_bi_zs_list.length > 0) {
- currentData.sub_sub_bi_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const kd = currentData.kline_data || [];
- const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : (kd.length ? Math.floor(new Date(kd[kd.length-1].date).getTime() / 1000) : 0);
- if (isNaN(startTime) || !endTime) return;
- const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) return;
- const color = '#00897b';
- mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
- mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
- mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
- if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
- } catch (e) { console.error('次次周期BI中枢处理出错:', e); }
- });
- }
- if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) {
- try {
- console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`);
- } catch (e) {}
- currentData.element_bi_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
- if (isNaN(startTime) || isNaN(endTime)) { return; }
- const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) { return; }
- const color = '#3f51b5';
- const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
- const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
- const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
- }
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
- }
- } catch (e) { console.error('次周期BI中枢处理出错:', e); }
- });
- }
- // 结构价值区绘制(半透明填充区 + 边框)
- if ($('#showMainStructureZone').is(':checked') && currentData.structure_zones && currentData.structure_zones.length > 0) {
- try {
- const kd = currentData.kline_data || [];
- if (kd.length > 0) {
- const chartStart = Math.floor(new Date(kd[0].date).getTime() / 1000);
- const chartEnd = Math.floor(new Date(kd[kd.length-1].date).getTime() / 1000);
- // 计算可见价格范围,过滤超出范围的区间
- let priceMin = Infinity, priceMax = -Infinity;
- kd.forEach(function(k) {
- const hi = parseFloat(k.high), lo = parseFloat(k.low);
- if (!isNaN(hi) && hi > priceMax) priceMax = hi;
- if (!isNaN(lo) && lo < priceMin) priceMin = lo;
- });
- const priceMargin = (priceMax - priceMin) * 0.05;
- priceMin -= priceMargin;
- priceMax += priceMargin;
- let drawnCount = 0;
- currentData.structure_zones.forEach(function(zone) {
- try {
- // 跳过完全超出可视价格范围的区间
- if (zone.upper < priceMin || zone.lower > priceMax) return;
- const fillColor = zone.zone_type === 'support' ? 'rgba(46, 204, 113, 0.08)' :
- zone.zone_type === 'resistance' ? 'rgba(231, 76, 60, 0.08)' :
- 'rgba(149, 165, 166, 0.06)';
- const borderColor = zone.zone_type === 'support' ? 'rgba(46, 204, 113, 0.7)' :
- zone.zone_type === 'resistance' ? 'rgba(231, 76, 60, 0.7)' :
- 'rgba(149, 165, 166, 0.6)';
- // 填充区:在上下边界之间画多条半透明线模拟填充
- const fillLines = 8;
- const step = (zone.upper - zone.lower) / (fillLines + 1);
- for (let fi = 1; fi <= fillLines; fi++) {
- const fy = zone.lower + step * fi;
- mainChart.addLineSeries({ color: fillColor, lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false })
- .setData([{ time: chartStart, value: fy }, { time: chartEnd, value: fy }]);
- }
- // 上边界(粗线)
- mainChart.addLineSeries({ color: borderColor, lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false })
- .setData([{ time: chartStart, value: zone.upper }, { time: chartEnd, value: zone.upper }]);
- // 下边界(粗线)
- mainChart.addLineSeries({ color: borderColor, lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false })
- .setData([{ time: chartStart, value: zone.lower }, { time: chartEnd, value: zone.lower }]);
- // 中心线(虚线)
- mainChart.addLineSeries({ color: borderColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false })
- .setData([{ time: chartStart, value: zone.center }, { time: chartEnd, value: zone.center }]);
- drawnCount++;
- } catch (e) { console.error('结构区绘制出错:', e); }
- });
- console.log(`结构区: 共${currentData.structure_zones.length}个, 绘制${drawnCount}个 (可见价格范围: ${priceMin.toFixed(0)}-${priceMax.toFixed(0)})`);
- }
- } catch (e) { console.error('结构区整体绘制出错:', e); }
- }
- // 区间/阶段/时间/VP:框线同中枢;标记并入主 K(同 BSP),时间对齐 candles
- (function drawWrLikeChan() {
- const candleSeries = tvWidget.series.candleSeries
- || tvWidget.series.barSeries
- || tvWidget.series.lineSeries
- || tvWidget.series.areaSeries
- || tvWidget.series.heikinSeries
- || tvWidget.series.renkoSeries;
- const candleTimes = (candles || []).map(function(c) { return c.time; })
- .filter(function(t) { return t != null && !isNaN(t); });
- const barStep = (candleTimes.length >= 2)
- ? Math.max(1, candleTimes[1] - candleTimes[0])
- : 3600;
- const lastKlineTime = candleTimes.length ? candleTimes[candleTimes.length - 1] : NaN;
- const chartStart = candleTimes.length ? candleTimes[0] : NaN;
-
- const toSec = function(t) {
- if (t == null) return NaN;
- if (typeof t === 'number') return t > 1e12 ? Math.floor(t / 1000) : Math.floor(t);
- const ms = new Date(t).getTime();
- return isNaN(ms) ? NaN : Math.floor(ms / 1000);
- };
- // 标记必须落在主 series 的 time 上(与 KLC 趋势 nearestTime 同思路)
- const snapToCandle = function(t) {
- if (!candleTimes.length || isNaN(t)) return t;
- let best = candleTimes[0], bd = Math.abs(candleTimes[0] - t);
- for (let i = 1; i < candleTimes.length; i++) {
- const d = Math.abs(candleTimes[i] - t);
- if (d < bd) { bd = d; best = candleTimes[i]; }
- }
- return best;
- };
- const ensureSpan = function(t0, t1) {
- if (isNaN(t0) || isNaN(t1)) return [t0, t1];
- if (t1 < t0) { const x = t0; t0 = t1; t1 = x; }
- if (t1 <= t0) t1 = t0 + barStep;
- return [t0, t1];
- };
- const drawBox = function(t0, t1, hi, lo, color) {
- const span = ensureSpan(t0, t1);
- t0 = span[0]; t1 = span[1];
- if (isNaN(t0) || isNaN(t1) || isNaN(hi) || isNaN(lo)) return;
- const opt = { color: color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false };
- mainChart.addLineSeries(opt).setData([{ time: t0, value: hi }, { time: t1, value: hi }]);
- mainChart.addLineSeries(opt).setData([{ time: t0, value: lo }, { time: t1, value: lo }]);
- mainChart.addLineSeries(opt).setData([{ time: t0, value: lo }, { time: t0, value: hi }]);
- mainChart.addLineSeries(opt).setData([{ time: t1, value: lo }, { time: t1, value: hi }]);
- };
- const mkPL = function(price, color, title, style) {
- if (!candleSeries) return;
- const p = parseFloat(price);
- if (isNaN(p)) return;
- try {
- candleSeries.createPriceLine({
- price: p, color: color,
- lineWidth: style === 2 ? 1 : 2,
- lineStyle: style || 0,
- axisLabelVisible: true,
- title: title
- });
- } catch (e) { console.warn('价位线失败', title, e); }
- };
- const phaseColors = { A: '#f1c40f', B: '#9b59b6', C: '#e67e22', D: '#2ecc71', E: '#3498db' };
- const eventColors = {
- Spring: '#27ae60', SOS: '#2ecc71', LPS: '#16a085',
- UTAD: '#e74c3c', SOW: '#c0392b', LPSY: '#d35400'
- };
-
- const wrMarkers = [];
- window.wrMarkers = [];
- const pushMarker = function(m) {
- if (!m || isNaN(m.time)) return;
- m.time = snapToCandle(m.time);
- if (!isNaN(chartStart) && (m.time < chartStart || m.time > lastKlineTime)) return;
- wrMarkers.push(m);
- };
-
- const drawOne = function(w, cfg) {
- if (!w) return;
- const showR = $(cfg.rangeSel).is(':checked');
- const showP = $(cfg.phasesSel).is(':checked');
- const showE = $(cfg.eventsSel).is(':checked');
- const showV = $(cfg.vpSel).is(':checked');
- if (!showR && !showP && !showE && !showV) return;
- // WYCKOFF-MULTI-CYCLE-001:遍历 cycles;无则退化为顶层单段
- const cycles = (w.cycles && w.cycles.length)
- ? w.cycles
- : (w.trading_range ? [{
- id: 0, status: 'ACTIVE', role: 'latest',
- trading_range: w.trading_range, phases: w.phases, events: w.events,
- volume_profile: w.volume_profile, volume_confirm: w.volume_confirm,
- period: { start_time: w.trading_range.start_time, end_time: w.trading_range.end_time, bars: w.trading_range.bars }
- }] : []);
- const tfLabel = (cfg.tfLabel || cfg.tag || 'TF').toString().toUpperCase();
-
- cycles.forEach(function(cycle) {
- const tr = cycle.trading_range;
- if (!tr) return;
- const cid = (cycle.id != null) ? cycle.id : 0;
- const isActive = String(cycle.status || '').toUpperCase() === 'ACTIVE';
- const cTag = tfLabel + ' C' + cid + ' ';
- try {
- let t0 = toSec(tr.start_time || (cycle.period && cycle.period.start_time));
- let t1 = toSec(tr.end_time || (cycle.period && cycle.period.end_time));
- // 仅 ACTIVE 可拉到最新 K;历史用 period.end
- if (isActive && !isNaN(lastKlineTime)) {
- t1 = lastKlineTime;
- } else if (cycle.period && cycle.period.end_time) {
- t1 = toSec(cycle.period.end_time);
- }
- const hi = parseFloat(tr.high), lo = parseFloat(tr.low);
- if (showR && !isNaN(hi) && !isNaN(lo)) {
- drawBox(t0, t1, hi, lo, cfg.color);
- if (isActive) {
- mkPL(hi, cfg.color, cfg.hiTag, 0);
- mkPL(lo, cfg.color, cfg.loTag, 0);
- mkPL(tr.mid, cfg.color, cfg.midTag, 2);
- }
- }
- if (showP && cycle.phases && cycle.phases.length && !isNaN(hi)) {
- cycle.phases.forEach(function(ph) {
- let p0 = toSec(ph.start_time);
- let p1 = ph.end_time ? toSec(ph.end_time) : t1;
- const sp = ensureSpan(p0, p1);
- p0 = sp[0]; p1 = sp[1];
- if (isNaN(p0) || isNaN(p1)) return;
- const col = phaseColors[ph.phase] || '#95a5a6';
- mainChart.addLineSeries({
- color: col, lineWidth: 3, lastValueVisible: false, priceLineVisible: false
- }).setData([{ time: p0, value: hi }, { time: p1, value: hi }]);
- pushMarker({
- time: p0, position: 'aboveBar', color: col, shape: 'square',
- text: cTag + 'Phase ' + String(ph.phase || ''), size: 1
- });
- });
- }
- if (showV && isActive && cycle.volume_profile) {
- const vp = cycle.volume_profile;
- mkPL(vp.poc, cfg.vpColor, cfg.pocTag, 0);
- mkPL(vp.vah, cfg.vpColor, cfg.vahTag, 2);
- mkPL(vp.val, cfg.vpColor, cfg.valTag, 2);
- const bins = (vp.bins || []).filter(function(b) { return b && b.volume > 0; })
- .slice().sort(function(a, b) { return b.volume - a.volume; }).slice(0, 8);
- let maxVol = 0;
- bins.forEach(function(b) { if (b.volume > maxVol) maxVol = b.volume; });
- const span = (!isNaN(t0) && !isNaN(t1) && t1 > t0) ? (t1 - t0) : barStep * 12;
- bins.forEach(function(b) {
- if (!b.volume || maxVol <= 0 || isNaN(t1)) return;
- const wSec = Math.max(barStep, Math.floor(span * 0.12 * (b.volume / maxVol)));
- let leftT = Math.max(isNaN(t0) ? (t1 - wSec) : t0, t1 - wSec);
- if (leftT >= t1) leftT = t1 - barStep;
- if (leftT >= t1) return;
- const alpha = 0.25 + 0.55 * (b.volume / maxVol);
- mainChart.addLineSeries({
- color: cfg.vpRgb.replace('ALPHA', alpha.toFixed(2)),
- lineWidth: 2, lastValueVisible: false, priceLineVisible: false
- }).setData([{ time: leftT, value: b.price }, { time: t1, value: b.price }]);
- });
- }
- if (showE && cycle.events && cycle.events.length) {
- const checks = (cycle.volume_confirm && cycle.volume_confirm.event_checks) || {};
- cycle.events.forEach(function(ev) {
- const t = toSec(ev.time);
- if (isNaN(t)) return;
- const typ = ev.type || '';
- const chk = checks[typ] || {};
- const volOk = (chk.volume_ok != null) ? chk.volume_ok : ev.volume_ok;
- const ok = volOk === true ? '✓' : (volOk === false ? '✗' : '');
- pushMarker({
- time: t,
- position: (typ === 'Spring' || typ === 'LPS' || typ === 'SOW') ? 'belowBar' : 'aboveBar',
- color: eventColors[typ] || '#7f8c8d',
- shape: (typ === 'Spring' || typ === 'SOW' || typ === 'LPS') ? 'arrowDown' : 'arrowUp',
- text: cTag + typ + ok,
- size: 1
- });
- });
- }
- } catch (e) { console.error('区间叠层出错', cfg.name, 'C' + cid, e); }
- });
- };
-
- if ($('#showMainWrRange').is(':checked') || $('#showMainWrPhases').is(':checked')
- || $('#showMainWrEvents').is(':checked') || $('#showMainWrVP').is(':checked')) {
- drawOne(currentData.wyckoff, {
- name: '主', tag: '', tfLabel: (currentData.timeframe || '4H'), color: '#3498db', vpColor: '#8e44ad',
- vpRgb: 'rgba(142, 68, 173, ALPHA)',
- rangeSel: '#showMainWrRange', phasesSel: '#showMainWrPhases',
- eventsSel: '#showMainWrEvents', vpSel: '#showMainWrVP',
- hiTag: 'WR.H', loTag: 'WR.L', midTag: 'WR.M',
- pocTag: 'POC', vahTag: 'VAH', valTag: 'VAL'
- });
- }
- if ($('#showElementWrRange').is(':checked') || $('#showElementWrPhases').is(':checked')
- || $('#showElementWrEvents').is(':checked') || $('#showElementWrVP').is(':checked')) {
- drawOne(currentData.element_wyckoff, {
- name: '次', tag: 'e', tfLabel: (currentData.element_timeframe || '2H'), color: '#e67e22', vpColor: '#d35400',
- vpRgb: 'rgba(211, 84, 0, ALPHA)',
- rangeSel: '#showElementWrRange', phasesSel: '#showElementWrPhases',
- eventsSel: '#showElementWrEvents', vpSel: '#showElementWrVP',
- hiTag: 'eWR.H', loTag: 'eWR.L', midTag: 'eWR.M',
- pocTag: 'ePOC', vahTag: 'eVAH', valTag: 'eVAL'
- });
- }
- if ($('#showSubSubWrRange').is(':checked') || $('#showSubSubWrPhases').is(':checked')
- || $('#showSubSubWrEvents').is(':checked') || $('#showSubSubWrVP').is(':checked')) {
- drawOne(currentData.sub_sub_wyckoff, {
- name: '次次', tag: 's', tfLabel: (currentData.sub_sub_timeframe || '1H'), color: '#27ae60', vpColor: '#16a085',
- vpRgb: 'rgba(22, 160, 133, ALPHA)',
- rangeSel: '#showSubSubWrRange', phasesSel: '#showSubSubWrPhases',
- eventsSel: '#showSubSubWrEvents', vpSel: '#showSubSubWrVP',
- hiTag: 'sWR.H', loTag: 'sWR.L', midTag: 'sWR.M',
- pocTag: 'sPOC', vahTag: 'sVAH', valTag: 'sVAL'
- });
- }
-
- // 同 BSP:写入 window,稍后与分型/买卖点一并 setMarkers
- wrMarkers.sort(function(a, b) { return a.time - b.time; });
- window.wrMarkers = wrMarkers;
- if (typeof renderWyckoffCycleSummary === 'function') {
- renderWyckoffCycleSummary();
- }
- })();
- // 显示未完成中枢 - 分别处理主周期、次周期和次次周期
- if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked') || $('#showSubSubZs').is(':checked') || $('#showSubSubBiZs').is(':checked')) {
- console.log('绘制未完成中枢 - 已启用');
- // 显示BI中枢绘制(沿用中枢样式)
- if ($('#showMainBiZs').is(':checked') || $('#showElementBiZs').is(':checked') || $('#showSubSubBiZs').is(':checked')) {
- console.log('绘制BI中枢 - 已启用');
- // 主周期 BI 中枢
- console.log('主BI开关:', $('#showMainBiZs').is(':checked'), '数据长度:', currentData.bi_zs_list ? currentData.bi_zs_list.length : 0);
- if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) {
- console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`);
- currentData.bi_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
- if (isNaN(startTime) || isNaN(endTime)) { return; }
- const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) { return; }
- const color = '#9C27B0'; // 主周期BI中枢颜色(紫色)
- const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
- const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
- const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
- }
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
- }
- } catch (e) { console.error('主周期BI中枢处理出错:', e); }
- });
- }
- // 主周期 未完成 BI 中枢
- console.log('主未完成BI长度:', currentData.uncompleted_bi_zs_list ? currentData.uncompleted_bi_zs_list.length : 0);
- if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) {
- console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`);
- currentData.uncompleted_bi_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
- if (isNaN(startTime) || isNaN(endTime)) { return; }
- const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) { return; }
- const color = '#9C27B0';
- const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
- const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
- const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
- }
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
- }
- } catch (e) { console.error('主周期未完成BI中枢处理出错:', e); }
- });
- }
- // 次周期 BI 中枢
- console.log('次BI开关:', $('#showElementBiZs').is(':checked'), '数据长度:', currentData.element_bi_zs_list ? currentData.element_bi_zs_list.length : 0);
- if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) {
- console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`);
- currentData.element_bi_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
- if (isNaN(startTime) || isNaN(endTime)) { return; }
- const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) { return; }
- const color = '#8BC34A'; // 次周期BI中枢颜色(绿)
- const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
- const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
- const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
- }
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
- }
- } catch (e) { console.error('次周期BI中枢处理出错:', e); }
- });
- }
- // 次周期 未完成 BI 中枢
- console.log('次未完成BI长度:', currentData.element_uncompleted_bi_zs_list ? currentData.element_uncompleted_bi_zs_list.length : 0);
- if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) {
- console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`);
- currentData.element_uncompleted_bi_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
- if (isNaN(startTime) || isNaN(endTime)) { return; }
- const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) { return; }
- const color = '#8BC34A';
- const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
- const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
- const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
- }
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
- }
- } catch (e) { console.error('次周期未完成BI中枢处理出错:', e); }
- });
- }
- // 次次周期 未完成 BI 中枢
- if ($('#showSubSubBiZs').is(':checked') && currentData.sub_sub_uncompleted_bi_zs_list && currentData.sub_sub_uncompleted_bi_zs_list.length > 0) {
- const kdBi = currentData.kline_data || [];
- const endTimeBi = kdBi.length ? Math.floor(new Date(kdBi[kdBi.length-1].date).getTime() / 1000) : 0;
- currentData.sub_sub_uncompleted_bi_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- if (isNaN(startTime) || !endTimeBi) return;
- const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) return;
- const color = '#00897b';
- mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTimeBi, value: zg }]);
- mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTimeBi, value: zd }]);
- mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTimeBi, value: gg }]);
- if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTimeBi, value: dd }]);
- } catch (e) { console.error('次次周期未完成BI中枢处理出错:', e); }
- });
- }
- }
- // 主周期未完成中枢
- if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) {
- console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`);
-
- currentData.uncompleted_zs_list.forEach(function(zs) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- // 未完成中枢的结束时间设为当前K线的最后时间
- const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
-
- if (isNaN(startTime) || isNaN(endTime)) {
- console.error('主周期未完成中枢时间转换错误:', zs.start_time);
- return;
- }
-
- const zg = parseFloat(zs.zg); // 中枢上沿
- const zd = parseFloat(zs.zd); // 中枢下沿
- const gg = parseFloat(zs.gg); // 中枢高高
- const dd = parseFloat(zs.dd); // 中枢低低
-
- if (isNaN(zg) || isNaN(zd)) {
- console.error('主周期未完成中枢价格转换错误:', zs.zg, zs.zd);
- return;
- }
-
- // 创建未完成中枢上边界
- const topSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 主周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- topSeries.setData([
- { time: startTime, value: zg },
- { time: endTime, value: zg }
- ]);
-
- // 为下边界创建另一条线
- const bottomSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 主周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- bottomSeries.setData([
- { time: startTime, value: zd },
- { time: endTime, value: zd }
- ]);
-
- // 添加左边界
- const leftSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 主周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- leftSeries.setData([
- { time: startTime, value: zd },
- { time: startTime, value: zg }
- ]);
-
- // 添加一个标记,标识这是未完成中枢
- const markerSeries = mainChart.addLineSeries({
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- markerSeries.setMarkers([
- {
- time: startTime,
- position: 'aboveBar',
- color: '#F1C40F',
- shape: 'circle',
- text: '未完',
- size: 1
- }
- ]);
-
- // 绘制gg线(中枢高高)
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 使用中枢自己的颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- ggSeries.setData([
- { time: startTime, value: gg },
- { time: endTime, value: gg }
- ]);
- }
-
- // 绘制dd线(中枢低低)
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({
- color: '#F1C40F', // 使用中枢自己的颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- ddSeries.setData([
- { time: startTime, value: dd },
- { time: endTime, value: dd }
- ]);
- }
-
- // 添加到图表对象
- tvWidget.series.mainUncompletedZsSeries.push({
- time: startTime,
- value: zg,
- color: '#F1C40F',
- lineWidth: 1
- });
- tvWidget.series.mainUncompletedZsSeries.push({
- time: endTime,
- value: zg,
- color: '#F1C40F',
- lineWidth: 1
- });
- tvWidget.series.mainUncompletedZsSeries.push({
- time: startTime,
- value: zd,
- color: '#F1C40F',
- lineWidth: 1
- });
- tvWidget.series.mainUncompletedZsSeries.push({
- time: endTime,
- value: zd,
- color: '#F1C40F',
- lineWidth: 1
- });
- } catch (e) {
- console.error('主周期未完成中枢处理出错:', e);
- }
- });
- }
-
- // 次周期未完成中枢
- if ($('#showElementZs').is(':checked') && currentData.element_uncompleted_zs_list && currentData.element_uncompleted_zs_list.length > 0) {
- console.log(`绘制次周期未完成中枢数据,共${currentData.element_uncompleted_zs_list.length}条`);
-
- currentData.element_uncompleted_zs_list.forEach(function(zs) {
- try {
- // 直接使用UTC时间戳(秒)
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- // 未完成中枢的结束时间设为当前K线的最后时间
- const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
-
- if (isNaN(startTime) || isNaN(endTime)) {
- console.error('次周期未完成中枢时间转换错误:', zs.start_time);
- return;
- }
-
- const zg = parseFloat(zs.zg); // 中枢上沿
- const zd = parseFloat(zs.zd); // 中枢下沿
- const gg = parseFloat(zs.gg); // 中枢高高
- const dd = parseFloat(zs.dd); // 中枢低低
-
- if (isNaN(zg) || isNaN(zd)) {
- console.error('次周期未完成中枢价格转换错误:', zs.zg, zs.zd);
- return;
- }
-
- // 创建未完成中枢上边界
- const topSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 次周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- topSeries.setData([
- { time: startTime, value: zg },
- { time: endTime, value: zg }
- ]);
-
- // 为下边界创建另一条线
- const bottomSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 次周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- bottomSeries.setData([
- { time: startTime, value: zd },
- { time: endTime, value: zd }
- ]);
-
- // 添加左边界
- const leftSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 次周期中枢颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- leftSeries.setData([
- { time: startTime, value: zd },
- { time: startTime, value: zg }
- ]);
-
- // 添加一个标记,标识这是未完成中枢
- const markerSeries = mainChart.addLineSeries({
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- markerSeries.setMarkers([
- {
- time: startTime,
- position: 'aboveBar',
- color: '#3f51b5',
- shape: 'circle',
- text: '未完',
- size: 1
- }
- ]);
-
- // 绘制gg线(中枢高高)
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 使用次周期中枢自己的颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- ggSeries.setData([
- { time: startTime, value: gg },
- { time: endTime, value: gg }
- ]);
- }
-
- // 绘制dd线(中枢低低)
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({
- color: '#3f51b5', // 使用次周期中枢自己的颜色
- lineWidth: 1,
- lastValueVisible: false,
- priceLineVisible: false,
- });
-
- ddSeries.setData([
- { time: startTime, value: dd },
- { time: endTime, value: dd }
- ]);
- }
-
- // 添加到图表对象
- tvWidget.series.elementUncompletedZsSeries.push({
- time: startTime,
- value: zg,
- color: '#3f51b5',
- lineWidth: 1
- });
- tvWidget.series.elementUncompletedZsSeries.push({
- time: endTime,
- value: zg,
- color: '#3f51b5',
- lineWidth: 1
- });
- tvWidget.series.elementUncompletedZsSeries.push({
- time: startTime,
- value: zd,
- color: '#3f51b5',
- lineWidth: 1
- });
- tvWidget.series.elementUncompletedZsSeries.push({
- time: endTime,
- value: zd,
- color: '#3f51b5',
- lineWidth: 1
- });
- } catch (e) {
- console.error('次周期未完成中枢处理出错:', e);
- }
- });
- }
- // 次次周期未完成SEG中枢
- if ($('#showSubSubZs').is(':checked') && currentData.sub_sub_uncompleted_zs_list && currentData.sub_sub_uncompleted_zs_list.length > 0) {
- const kdZs = currentData.kline_data || [];
- const endTimeZs = kdZs.length ? Math.floor(new Date(kdZs[kdZs.length-1].date).getTime() / 1000) : 0;
- const subSubUZsColor = '#00897b';
- currentData.sub_sub_uncompleted_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- if (isNaN(startTime) || !endTimeZs) return;
- const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) return;
- mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTimeZs, value: zg }]);
- mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTimeZs, value: zd }]);
- mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTimeZs, value: gg }]);
- if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTimeZs, value: dd }]);
- } catch (e) { console.error('次次周期未完成中枢处理出错:', e); }
- });
- }
- } else {
- console.log('绘制未完成中枢 - 已禁用');
- }
- // 未完成BI中枢 - 使用独立的BI开关
- if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) {
- try { console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`); } catch (e) {}
- currentData.uncompleted_bi_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
- if (isNaN(startTime) || isNaN(endTime)) { return; }
- const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) { return; }
- const color = '#F1C40F';
- const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
- const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
- const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
- }
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
- }
- } catch (e) { console.error('主周期未完成BI中枢处理出错:', e); }
- });
- }
- // 次周期 未完成 BI 中枢
- if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) {
- try { console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`); } catch (e) {}
- currentData.element_uncompleted_bi_zs_list.forEach(function(zs) {
- try {
- const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
- const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
- if (isNaN(startTime) || isNaN(endTime)) { return; }
- const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
- if (isNaN(zg) || isNaN(zd)) { return; }
- const color = '#3f51b5';
- const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
- const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
- const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
- if (!isNaN(gg) && gg > 0) {
- const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
- }
- if (!isNaN(dd) && dd > 0) {
- const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
- ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
- }
- } catch (e) { console.error('次周期未完成BI中枢处理出错:', e); }
- });
- }
- // 添加买卖点标记(新版:基于 bsp_list / element_bsp_list / sub_sub_bsp_list,按与 KLC 分型相同方式合并到主图标记)
- if ($('#showMainBsp').is(':checked') || $('#showElementBsp').is(':checked') || $('#showSubSubBsp').is(':checked')) {
- console.log('绘制买卖点(BSP) - 已启用');
-
- // BSP 样式定义
- const BSP_STYLE = {
- 'BSP1_BUY': { color: '#FF1744', text: 'B1', position: 'belowBar', size: 0.5 },
- 'BSP2_BUY': { color: '#F50057', text: 'B2', position: 'belowBar', size: 0.5 },
- 'BSP3_BUY': { color: '#D500F9', text: 'B3', position: 'belowBar', size: 0.5 },
- 'BSP1_SELL': { color: '#00E676', text: 'S1', position: 'aboveBar', size: 0.5 },
- 'BSP2_SELL': { color: '#00B0FF', text: 'S2', position: 'aboveBar', size: 0.5 },
- 'BSP3_SELL': { color: '#8B4513', text: 'S3', position: 'aboveBar', size: 0.5 },
- };
-
- const getBspStyleKey = (bsp) => {
- // 统一 BSP key:
- // - type 可能是 "BSP1"/"BSP2"/"BSP3",
- // - 也可能是后端给的 "B1"/"B2"/"B3" 或 "S1"/"S2"/"S3"
- // 最终都映射为 "BSP1_BUY" / "BSP1_SELL" 这类 key,方便复用现有样式定义
- let type = (bsp.type || '').toUpperCase();
- const dir = (bsp.dir || '').toUpperCase();
-
- // 若是 "B1" / "B2" / "B3" 或 "S1" / "S2" / "S3" 形式,则提取数字并映射成 "BSP{n}"
- const simpleMatch = type.match(/^([BS])(\d)$/);
- if (simpleMatch) {
- const n = simpleMatch[2]; // "1" / "2" / "3"
- type = 'BSP' + n;
- }
-
- return type + '_' + dir;
- };
-
- // 收集所有 BSP 标记
- const allBspMarkers = [];
-
- // 主周期买卖点
- // 兼容不同字段命名:优先使用 bsp_list,若不存在则尝试 bsp
- const mainBspList = currentData.bsp_list || currentData.bsp || [];
- // 调试:打印前几条主周期 BSP 的 key,方便排查样式不匹配问题
- if (mainBspList.length > 0) {
- console.log(
- '主周期 BSP 示例 (前5条):',
- mainBspList.slice(0, 5).map(b => ({
- raw_type: b.type,
- raw_dir: b.dir,
- key: getBspStyleKey(b)
- }))
- );
- }
- if ($('#showMainBsp').is(':checked') && mainBspList.length > 0) {
- console.log(`绘制主周期买卖点,共${mainBspList.length}条`);
- mainBspList.forEach(function(bsp) {
- try {
- const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
- if (isNaN(ts)) return;
- const key = getBspStyleKey(bsp);
- const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
- const sureText = bsp.is_sure ? '' : '?';
- allBspMarkers.push({
- time: ts,
- position: style.position,
- color: style.color,
- shape: style.shape,
- text: style.text + sureText,
- size: 2
- });
- } catch (e) {
- console.error('主周期BSP处理出错:', e);
- }
- });
- }
-
- // 次周期买卖点
- // 兼容不同字段命名:优先使用 element_bsp_list,若不存在则尝试 element_bsp
- const elementBspList = currentData.element_bsp_list || currentData.element_bsp || [];
- // 调试:打印前几条次周期 BSP 的 key
- if (elementBspList.length > 0) {
- console.log(
- '次周期 BSP 示例 (前5条):',
- elementBspList.slice(0, 5).map(b => ({
- raw_type: b.type,
- raw_dir: b.dir,
- key: getBspStyleKey(b)
- }))
- );
- }
- if ($('#showElementBsp').is(':checked') && elementBspList.length > 0) {
- console.log(`绘制次周期买卖点,共${elementBspList.length}条`);
- elementBspList.forEach(function(bsp) {
- try {
- const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
- if (isNaN(ts)) return;
- const key = getBspStyleKey(bsp);
- const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
- const sureText = bsp.is_sure ? '' : '?';
- // 次周期使用稍小的标记和不同前缀以区分
- allBspMarkers.push({
- time: ts,
- position: style.position,
- color: style.color,
- shape: style.shape,
- text: 'e' + style.text + sureText,
- size: 1
- });
- } catch (e) {
- console.error('次周期BSP处理出错:', e);
- }
- });
- }
-
- // 次次周期买卖点
- const subSubBspList = currentData.sub_sub_bsp_list || [];
- if ($('#showSubSubBsp').is(':checked') && subSubBspList.length > 0) {
- subSubBspList.forEach(function(bsp) {
- try {
- const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
- if (isNaN(ts)) return;
- const key = getBspStyleKey(bsp);
- const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
- const sureText = bsp.is_sure ? '' : '?';
- allBspMarkers.push({
- time: ts,
- position: style.position,
- color: '#00897b',
- shape: style.shape,
- text: 's' + (style.text || '?') + sureText,
- size: 1
- });
- } catch (e) {
- console.error('次次周期BSP处理出错:', e);
- }
- });
- }
-
- // 将 BSP 标记挂到全局,后面与 KLC 分型等标记一起合并到主系列上
- if (allBspMarkers.length > 0) {
- // 按时间排序(lightweight-charts 要求标记按时间升序)
- allBspMarkers.sort((a, b) => a.time - b.time);
- window.bspMarkers = allBspMarkers;
- console.log(`准备合并 ${allBspMarkers.length} 个BSP标记到主图标记中`);
- } else {
- window.bspMarkers = [];
- }
- } else {
- // 关闭 BSP 显示时,清空全局 BSP 标记
- window.bspMarkers = [];
- }
-
- // 添加买卖点标记(旧版,保留兼容)
- // 这里为了与主面板上的「买卖点」开关保持一致,
- // 同时响应顶部的 `#showMainBsp` 复选框
- if ($('#showTradePoints').is(':checked') || $('#showMainBsp').is(':checked')) {
- console.log('绘制买卖点 - 已启用(来源: showTradePoints / showMainBsp)');
-
- // 优先使用小周期数据,如果不存在则使用主周期数据
- const tradePointsData = currentData.element_trade_points || currentData.trade_points;
- console.log(`绘制${currentData.element_trade_points ? '元素周期' : '主周期'}买卖点数据,共${tradePointsData ? tradePointsData.length : 0}条`);
-
- // 调试信息 - 输出完整的买卖点数据
- if (tradePointsData && tradePointsData.length > 0) {
- console.log("买卖点数据样例:", tradePointsData[0]);
-
- // 检查数据格式,如果time不是标准格式,进行格式化处理
- const checkDataFormat = () => {
- for (let i = 0; i < tradePointsData.length; i++) {
- if (tradePointsData[i].time) {
- // 确保时间是标准格式
- try {
- const timeValue = new Date(tradePointsData[i].time);
- if (isNaN(timeValue.getTime())) {
- console.error(`买卖点 #${i} 时间格式无效:`, tradePointsData[i].time);
- }
- } catch (e) {
- console.error(`买卖点 #${i} 时间格式异常:`, e);
- }
- } else {
- console.error(`买卖点 #${i} 缺少时间属性`);
- }
- }
- };
-
- // 执行格式检查
- checkDataFormat();
-
- // 对买卖点按时间排序,用于后续优化显示
- const sortedPoints = [...tradePointsData].sort((a, b) => {
- return new Date(a.time) - new Date(b.time);
- });
-
- // 记录已处理的时间点 - 按类型分开计数
- const processedTimes = {};
-
- // 创建买卖点标记系列
- const buyMarkers = [];
- const sellMarkers = [];
-
- // 计数器,追踪成功和失败的处理次数
- let successCount = 0;
- let errorCount = 0;
-
- sortedPoints.forEach(function(point, index) {
- try {
- // 检查所有必要的属性是否存在且有效
- if (!point.time || !point.price || point.type === undefined) {
- console.error(`买卖点 #${index} 数据不完整:`, point);
- errorCount++;
- return;
- }
-
- const time = Math.floor(new Date(point.time).getTime() / 1000);
- const price = parseFloat(point.price);
- const type = parseInt(point.type);
-
- if (isNaN(time) || isNaN(price) || isNaN(type)) {
- console.error(`买卖点 #${index} 数据格式错误:`,
- { time: isNaN(time), price: isNaN(price), type: isNaN(type) }, point);
- errorCount++;
- return;
- }
-
- // 获取买卖点样式
- const style = TRADE_POINT_STYLE[type] || {
- color: '#999999',
- shape: 'circle',
- text: '?',
- size: 1
- };
-
- // 初始化该时间点的类型计数器
- if (!processedTimes[time]) {
- processedTimes[time] = {};
- }
-
- // 优化:检查是否有相同时间点和相同类型的标记,如果有,进行类型内的偏移
- let stackIndex = 0;
- if (processedTimes[time][type]) {
- // 已经有相同时间和类型的标记,记录堆叠索引
- stackIndex = processedTimes[time][type];
- processedTimes[time][type]++;
- } else {
- // 第一次出现这个时间点的这个类型
- processedTimes[time][type] = 1;
- }
-
- // 为不同类型的买卖点获取基础垂直偏移系数
- const baseOffset = TRADE_POINT_OFFSET[type] || 0;
-
- // 创建标记对象,包含额外的信息用于悬停提示
- const marker = {
- time: time,
- position: 'inBar', // 改为在K线内部显示,不影响数据
- color: style.color,
- shape: style.shape,
- text: style.text,
- size: style.size,
- // 记录堆叠索引
- stackIndex: stackIndex,
- // 添加悬停提示的数据
- tooltip: `${point.desc || (type > 0 ? '买点' : '卖点')}
- 时间: ${formatTime(point.time)}
- 价格: ${price.toFixed(2)}`,
- // 额外添加基础类型偏移
- baseOffset: baseOffset,
- // 添加边框
- borderColor: 'white',
- borderWidth: 1,
- // 添加价格偏移系数
- pricePercentOffset: PRICE_PERCENT_OFFSET[type] || 0,
- // 保存实际价格用于计算
- price: price,
- // 保存类型
- type: type
- };
-
- // 区分买卖点
- if (type > 0) {
- buyMarkers.push(marker);
- } else {
- sellMarkers.push(marker);
- }
-
- successCount++;
- } catch (e) {
- console.error(`处理买卖点 #${index} 出错:`, e, point);
- errorCount++;
- }
- });
-
- console.log(`买卖点处理完成: 成功=${successCount}, 失败=${errorCount}, 买点=${buyMarkers.length}, 卖点=${sellMarkers.length}`);
-
- // 分别添加买卖点标记
- if (buyMarkers.length > 0) {
- const buyMarkersSeries = mainChart.addLineSeries({
- lastValueVisible: false,
- priceLineVisible: false,
- lineVisible: false,
- color: 'transparent',
- title: '买点'
- });
-
- // 使用主K线的收盘价作为基准数据,保证买点标记与价格在同一纵轴范围
- if (Array.isArray(candles) && candles.length > 0) {
- const baseData = candles.map(c => ({ time: c.time, value: c.close }));
- buyMarkersSeries.setData(baseData);
- } else {
- // 兜底:至少一个数据点,避免报错
- buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: buyMarkers[0].price || 0 }]);
- }
-
- try {
- // 设置买点标记:文字在价格上方,仅显示文字不显示形状
- buyMarkersSeries.setMarkers(
- buyMarkers.map(marker => {
- // 使用实际价格位置,买点显示在K线上方
- return {
- ...marker,
- position: 'aboveBar', // 买点:价格上方
- price: marker.price,
- // 隐藏形状,仅保留文字
- size: 0,
- color: 'rgba(0, 0, 0, 0)'
- };
- })
- );
- console.log(`成功添加 ${buyMarkers.length} 个买点标记`);
- } catch (e) {
- console.error("设置买点标记时出错:", e);
- }
- }
-
- if (sellMarkers.length > 0) {
- const sellMarkersSeries = mainChart.addLineSeries({
- lastValueVisible: false,
- priceLineVisible: false,
- lineVisible: false,
- color: 'transparent',
- title: '卖点'
- });
-
- // 使用主K线的收盘价作为基准数据,保证卖点标记与价格在同一纵轴范围
- if (Array.isArray(candles) && candles.length > 0) {
- const baseData = candles.map(c => ({ time: c.time, value: c.close }));
- sellMarkersSeries.setData(baseData);
- } else {
- // 兜底:至少一个数据点,避免报错
- sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: sellMarkers[0].price || 0 }]);
- }
-
- try {
- // 设置卖点标记:文字在价格下方,仅显示文字不显示形状
- sellMarkersSeries.setMarkers(
- sellMarkers.map(marker => {
- // 使用实际价格位置,卖点显示在K线下方
- return {
- ...marker,
- position: 'belowBar', // 卖点:价格下方
- price: marker.price,
- // 隐藏形状,仅保留文字
- size: 0,
- color: 'rgba(0, 0, 0, 0)'
- };
- })
- );
- console.log(`成功添加 ${sellMarkers.length} 个卖点标记`);
- } catch (e) {
- console.error("设置卖点标记时出错:", e);
- }
- }
-
- // 添加鼠标悬停事件显示提示
- mainChart.subscribeCrosshairMove(param => {
- // 十字线同步到其他图表 - 通过DOM元素绘制垂直线实现虚线延长效果
- if (param.time && param.point && volumeChart) {
- try {
- // 清除之前的十字线标记
- const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line');
- existingVolumeLines.forEach(line => line.remove());
- const existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
- existingAtrLines.forEach(line => line.remove());
- const existingMacdLines = document.querySelectorAll('.macd-crosshair-line');
- existingMacdLines.forEach(line => line.remove());
- const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
- existingChanMacdLines.forEach(line => line.remove());
-
- // 获取时间对应的坐标位置
- const mainTimeCoordinate = mainChart.timeScale().timeToCoordinate(param.time);
- if (mainTimeCoordinate !== null) {
- // 获取主图容器的位置
- const mainChartRect = mainChartContainer.getBoundingClientRect();
-
- // 在交易量图上绘制垂直线
- const volumeTimeCoordinate = volumeChart.timeScale().timeToCoordinate(param.time);
- if (volumeTimeCoordinate !== null) {
- const volumeChartRect = volumeChartContainer.getBoundingClientRect();
- const volumeLine = document.createElement('div');
- volumeLine.className = 'volume-crosshair-line';
- volumeLine.style.position = 'fixed'; // 改为fixed定位
- volumeLine.style.left = (volumeChartRect.left + volumeTimeCoordinate) + 'px';
- volumeLine.style.top = volumeChartRect.top + 'px';
- volumeLine.style.width = '1px';
- volumeLine.style.height = volumeChartRect.height + 'px';
- volumeLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
- volumeLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
- volumeLine.style.pointerEvents = 'none';
- volumeLine.style.zIndex = '1000';
- document.body.appendChild(volumeLine);
- }
-
- // 在ATR图上绘制垂直线
- if (atrChart && atrChartContainer) {
- const atrTimeCoordinate = atrChart.timeScale().timeToCoordinate(param.time);
- if (atrTimeCoordinate !== null) {
- const atrChartRect = atrChartContainer.getBoundingClientRect();
- const atrLine = document.createElement('div');
- atrLine.className = 'atr-crosshair-line';
- atrLine.style.position = 'fixed'; // 改为fixed定位
- atrLine.style.left = (atrChartRect.left + atrTimeCoordinate) + 'px';
- atrLine.style.top = atrChartRect.top + 'px';
- atrLine.style.width = '1px';
- atrLine.style.height = atrChartRect.height + 'px';
- atrLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
- atrLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
- atrLine.style.pointerEvents = 'none';
- atrLine.style.zIndex = '1000';
- document.body.appendChild(atrLine);
- }
- }
-
- // 如果有MACD图,也在MACD图上绘制垂直线
- if (showMacd && macdChart && macdChartContainer) {
- const macdTimeCoordinate = macdChart.timeScale().timeToCoordinate(param.time);
- if (macdTimeCoordinate !== null) {
- const macdChartRect = macdChartContainer.getBoundingClientRect();
- const macdLine = document.createElement('div');
- macdLine.className = 'macd-crosshair-line';
- macdLine.style.position = 'fixed'; // 改为fixed定位
- macdLine.style.left = (macdChartRect.left + macdTimeCoordinate) + 'px';
- macdLine.style.top = macdChartRect.top + 'px';
- macdLine.style.width = '1px';
- macdLine.style.height = macdChartRect.height + 'px';
- macdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
- macdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
- macdLine.style.pointerEvents = 'none';
- macdLine.style.zIndex = '1000';
- document.body.appendChild(macdLine);
- }
- }
-
- // 如果有ChanMACD图,也在ChanMACD图上绘制垂直线
- if (showMacd && chanMacdChart && chanMacdChartContainer) {
- const chanMacdTimeCoordinate = chanMacdChart.timeScale().timeToCoordinate(param.time);
- if (chanMacdTimeCoordinate !== null) {
- const chanMacdChartRect = chanMacdChartContainer.getBoundingClientRect();
- console.log('ChanMACD图表位置:', {
- left: chanMacdChartRect.left,
- top: chanMacdChartRect.top,
- width: chanMacdChartRect.width,
- height: chanMacdChartRect.height,
- timeCoordinate: chanMacdTimeCoordinate
- });
- const chanMacdLine = document.createElement('div');
- chanMacdLine.className = 'chanmacd-crosshair-line';
- chanMacdLine.style.position = 'fixed';
- chanMacdLine.style.left = (chanMacdChartRect.left + chanMacdTimeCoordinate) + 'px';
- chanMacdLine.style.top = chanMacdChartRect.top + 'px';
- chanMacdLine.style.width = '1px';
- chanMacdLine.style.height = chanMacdChartRect.height + 'px';
- chanMacdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
- chanMacdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
- chanMacdLine.style.pointerEvents = 'none';
- chanMacdLine.style.zIndex = '1000';
- document.body.appendChild(chanMacdLine);
- console.log('ChanMACD垂直线已创建,位置:', chanMacdLine.style.left, chanMacdLine.style.top);
- } else {
- console.log('ChanMACD时间坐标为空');
- }
- } else {
- console.log('ChanMACD图表条件不满足:', {
- showMacd: showMacd,
- hasChanMacdChart: !!chanMacdChart,
- hasChanMacdChartContainer: !!chanMacdChartContainer
- });
- }
-
-
- }
- } catch (e) {
- console.debug('十字线同步出错:', e);
- }
- } else {
- // 当十字线离开时,清除垂直线
- try {
- const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line');
- existingVolumeLines.forEach(line => line.remove());
- const existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
- existingAtrLines.forEach(line => line.remove());
- const existingMacdLines = document.querySelectorAll('.macd-crosshair-line');
- existingMacdLines.forEach(line => line.remove());
- const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
- existingChanMacdLines.forEach(line => line.remove());
- } catch (e) {
- console.debug('清除十字线时出错:', e);
- }
- }
-
- if (param.time && param.point) {
- const timeStr = param.time;
- const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
-
- // 同时检查分型标记
- const fxMarkers = (window.fxMarkers || []).filter(m => m.time === timeStr);
- const allMarkers = [...markers, ...fxMarkers];
-
- // 显示时区调试信息
- if (window.debugMode) {
- const timezone = $('#timezone').val();
- const formattedTime = formatTimeWithTimezone(timeStr * 1000, timezone);
-
- // 获取当前价格 - 通过param.seriesPrices获取
- let priceInfo = '';
- if (param.seriesPrices && param.seriesPrices.size > 0) {
- // 依次从当前可能的主系列中获取价格
- if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) {
- const price = param.seriesPrices.get(tvWidget.series.candleSeries);
- priceInfo = `价格: ${price.toFixed(2)}`;
- } else if (tvWidget.series.renkoSeries && param.seriesPrices.get(tvWidget.series.renkoSeries)) {
- const price = param.seriesPrices.get(tvWidget.series.renkoSeries);
- priceInfo = `价格: ${price.toFixed(2)}`;
- } else if (tvWidget.series.heikinSeries && param.seriesPrices.get(tvWidget.series.heikinSeries)) {
- const price = param.seriesPrices.get(tvWidget.series.heikinSeries);
- priceInfo = `价格: ${price.toFixed(2)}`;
- } else if (tvWidget.series.barSeries && param.seriesPrices.get(tvWidget.series.barSeries)) {
- const price = param.seriesPrices.get(tvWidget.series.barSeries);
- priceInfo = `价格: ${price.toFixed(2)}`;
- } else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) {
- const price = param.seriesPrices.get(tvWidget.series.lineSeries);
- priceInfo = `价格: ${price.toFixed(2)}`;
- } else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) {
- const price = param.seriesPrices.get(tvWidget.series.areaSeries);
- priceInfo = `价格: ${price.toFixed(2)}`;
- } else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) {
- const price = param.seriesPrices.get(tvWidget.series.baselineSeries);
- priceInfo = `价格: ${price.toFixed(2)}`;
- }
- // 如果没有蜡烛图系列价格,尝试从区域图系列获取
- else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) {
- const price = param.seriesPrices.get(tvWidget.series.areaSeries);
- priceInfo = `价格: ${price.toFixed(2)}`;
- }
- // 如果没有蜡烛图系列价格,尝试从基线图系列获取
- else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) {
- const price = param.seriesPrices.get(tvWidget.series.baselineSeries);
- priceInfo = `价格: ${price.toFixed(2)}`;
- }
- }
-
- // 仅记录最简短的调试信息
- console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`);
-
- // 显示自定义时区工具提示,包含价格信息
- crosshairTooltip.innerHTML = `