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 = `
时间: ${formattedTime}
` + - (priceInfo ? `
${priceInfo}
` : ''); - crosshairTooltip.style.display = 'block'; - crosshairTooltip.style.left = (param.point.x + 15) + 'px'; - crosshairTooltip.style.top = (param.point.y - 30) + 'px'; - } - - if (allMarkers.length > 0) { - // 有买卖点或分型标记,显示自定义提示 - const tooltips = allMarkers.map(m => m.tooltip).join('

'); - tooltipElement.innerHTML = tooltips; - tooltipElement.style.display = 'block'; - tooltipElement.style.left = (param.point.x + 15) + 'px'; - tooltipElement.style.top = (param.point.y + 15) + 'px'; - } else { - // 隐藏提示 - tooltipElement.style.display = 'none'; - } - } else { - // 隐藏提示 - tooltipElement.style.display = 'none'; - crosshairTooltip.style.display = 'none'; - } - }); - - // 处理图表缩放、平移等事件,隐藏提示 - mainChart.timeScale().subscribeVisibleTimeRangeChange(() => { - tooltipElement.style.display = 'none'; - crosshairTooltip.style.display = 'none'; - }); - } - } else { - console.log('绘制买卖点 - 已禁用'); - } - // 绘制布林带 - if ($('#showMainBollinger').is(':checked') || $('#showElementBollinger').is(':checked')) { - console.log('绘制布林带 - 已启用'); - - // 主周期布林带 - if ($('#showMainBollinger').is(':checked') && currentData.bollinger && currentData.bollinger.upper && currentData.bollinger.lower && currentData.bollinger.middle) { - console.log(`绘制主周期布林带数据,共${currentData.bollinger.upper.length}条`); - - // 准备布林带数据 - const upperBandData = []; - const lowerBandData = []; - const middleBandData = []; - - // 主周期布林带始终使用主周期K线数据作为时间源 - const mainKlineData = currentData.kline_data; - - for (let i = 0; i < mainKlineData.length && i < currentData.bollinger.upper.length; i++) { - const kline = mainKlineData[i]; - const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); - - // 只添加非0的有效数据点 - if (currentData.bollinger.upper[i] && currentData.bollinger.upper[i] !== 0) { - upperBandData.push({ - time: timestamp, - value: currentData.bollinger.upper[i] - }); - } - - if (currentData.bollinger.lower[i] && currentData.bollinger.lower[i] !== 0) { - lowerBandData.push({ - time: timestamp, - value: currentData.bollinger.lower[i] - }); - } - - if (currentData.bollinger.middle[i] && currentData.bollinger.middle[i] !== 0) { - middleBandData.push({ - time: timestamp, - value: currentData.bollinger.middle[i] - }); - } - } - - // 创建布林带上轨 - const upperBandSeries = mainChart.addLineSeries({ - color: '#2196F3', - lineWidth: 1, - lineStyle: 2, // 虚线 - lastValueVisible: false, - priceLineVisible: false, - title: '布林上轨' - }); - upperBandSeries.setData(upperBandData); - - // 创建布林带下轨 - const lowerBandSeries = mainChart.addLineSeries({ - color: '#2196F3', - lineWidth: 1, - lineStyle: 2, // 虚线 - lastValueVisible: false, - priceLineVisible: false, - title: '布林下轨' - }); - lowerBandSeries.setData(lowerBandData); - - // 创建布林带中轨(移动平均线) - const middleBandSeries = mainChart.addLineSeries({ - color: '#FF9800', - lineWidth: 1, - lastValueVisible: false, - priceLineVisible: false, - title: '布林中轨' - }); - middleBandSeries.setData(middleBandData); - - // 保存到tvWidget.series对象 - tvWidget.series.mainBollingerSeries.push(upperBandSeries); - tvWidget.series.mainBollingerSeries.push(lowerBandSeries); - tvWidget.series.mainBollingerSeries.push(middleBandSeries); - - console.log('主周期布林带绘制完成'); - } - - // 次周期布林带 - if ($('#showElementBollinger').is(':checked') && currentData.element_bollinger && currentData.element_bollinger.upper && currentData.element_bollinger.lower && currentData.element_bollinger.middle) { - console.log(`绘制次周期布林带数据,共${currentData.element_bollinger.upper.length}条`); - - // 准备次周期布林带数据 - const elementUpperBandData = []; - const elementLowerBandData = []; - const elementMiddleBandData = []; - - // 使用次周期K线数据 - const elementKlineData = currentData.element_kline_data || currentData.kline_data; - - for (let i = 0; i < elementKlineData.length && i < currentData.element_bollinger.upper.length; i++) { - const kline = elementKlineData[i]; - const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); - - // 只添加非0的有效数据点 - if (currentData.element_bollinger.upper[i] && currentData.element_bollinger.upper[i] !== 0) { - elementUpperBandData.push({ - time: timestamp, - value: currentData.element_bollinger.upper[i] - }); - } - - if (currentData.element_bollinger.lower[i] && currentData.element_bollinger.lower[i] !== 0) { - elementLowerBandData.push({ - time: timestamp, - value: currentData.element_bollinger.lower[i] - }); - } - - if (currentData.element_bollinger.middle[i] && currentData.element_bollinger.middle[i] !== 0) { - elementMiddleBandData.push({ - time: timestamp, - value: currentData.element_bollinger.middle[i] - }); - } - } - - // 创建次周期布林带上轨 - const elementUpperBandSeries = mainChart.addLineSeries({ - color: '#9C27B0', - lineWidth: 1, - lineStyle: 2, // 虚线 - lastValueVisible: false, - priceLineVisible: false, - title: '次周期布林上轨' - }); - elementUpperBandSeries.setData(elementUpperBandData); - - // 创建次周期布林带下轨 - const elementLowerBandSeries = mainChart.addLineSeries({ - color: '#9C27B0', - lineWidth: 1, - lineStyle: 2, // 虚线 - lastValueVisible: false, - priceLineVisible: false, - title: '次周期布林下轨' - }); - elementLowerBandSeries.setData(elementLowerBandData); - - // 创建次周期布林带中轨 - const elementMiddleBandSeries = mainChart.addLineSeries({ - color: '#E91E63', - lineWidth: 1, - lastValueVisible: false, - priceLineVisible: false, - title: '次周期布林中轨' - }); - elementMiddleBandSeries.setData(elementMiddleBandData); - - // 保存到tvWidget.series对象 - tvWidget.series.elementBollingerSeries.push(elementUpperBandSeries); - tvWidget.series.elementBollingerSeries.push(elementLowerBandSeries); - tvWidget.series.elementBollingerSeries.push(elementMiddleBandSeries); - - console.log('次周期布林带绘制完成'); - } - } else { - console.log('绘制布林带 - 已禁用'); - } - - // 绘制分型类型标签 - console.log('=== 开始检查分型显示条件 ==='); - console.log('showKlcFxType勾选状态:', $('#showKlcFxType').is(':checked')); - console.log('showKluFxType勾选状态:', $('#showKluFxType').is(':checked')); - console.log('currentData.klc_fx_info存在:', !!currentData.klc_fx_info); - console.log('currentData.klu_fx_info存在:', !!currentData.klu_fx_info); - console.log('currentData.klc_fx_info长度:', currentData.klc_fx_info ? currentData.klc_fx_info.length : 'undefined'); - console.log('currentData.klu_fx_info长度:', currentData.klu_fx_info ? currentData.klu_fx_info.length : 'undefined'); - if (currentData.klc_fx_info && currentData.klc_fx_info.length > 0) { - console.log('前3个klc分型数据样本:', currentData.klc_fx_info.slice(0, 3)); - } - if (currentData.klu_fx_info && currentData.klu_fx_info.length > 0) { - console.log('前3个klu分型数据样本:', currentData.klu_fx_info.slice(0, 3)); - } - // 收集所有主周期分型标记 - const allMainFxMarkers = []; - const mainFxMarkers = []; // 用于tooltip支持 - // 处理主周期KLC分型 - if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) { - console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`); - - currentData.klc_fx_info.forEach(function(fx) { - try { - // 直接使用UTC时间戳(秒) - const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); - const price = parseFloat(fx.price); - - if (isNaN(timestamp) || isNaN(price)) { - console.error('主周期KLC分型时间或价格转换错误:', fx.time, fx.price); - return; - } - // 主周期 KLC - // 确定颜色和位置 - const color = fx.is_bottom ? '#28a745' : '#dc3545'; // 底分型绿色,顶分型红色 - - // 根据强度等级调整颜色强度 - let strengthColor = color; - - // 构建显示文本,包含分型类型和强度信息 - let displayText = `${fx.fx_strength.toFixed(1)}`; - if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 - displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 - } - displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "").replace("41", "").replace("51", "").replace("01", ""); - // 添加标记配置 - const markerConfig = { - time: timestamp, - position: fx.is_bottom ? 'belowBar' : 'aboveBar', - color: strengthColor, - shape: 'triangle', - text: displayText, - size: 2 // 调整尺寸,强分型稍大,普通分型更小 - }; - - allMainFxMarkers.push(markerConfig); - - // 画虚线分型框(根据 start/end + high/low) - if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { - const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); - const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); - const high = parseFloat(fx.high); - const low = parseFloat(fx.low); - - if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { - const boxHigh = Math.max(high, low); - const boxLow = Math.min(high, low); - const boxColor = strengthColor; - - const topSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, // 虚线 - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); - - const bottomSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, // 虚线 - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); - - const leftSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, // 虚线 - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - // 左边竖线:同一 time 上下两个点(和你已有ZS绘制写法保持一致) - leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); - - const rightSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, // 虚线 - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); - - if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = []; - tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); - } - } - - // 创建分型标记对象,包含tooltip信息 - const fxMarker = { - time: timestamp, - tooltip: `
- 主周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}
- 强度分数: ${fx.fx_strength}分
- 强度等级: ${fx.fx_strength_level}
- 是否强分型: ${fx.is_strong_fx ? '是' : '否'}
- 价格: ${price.toFixed(4)}
- 时间: ${fx.time} -
` - }; - - mainFxMarkers.push(fxMarker); - - } catch (e) { - console.error('绘制主周期KLC分型标签出错:', e); - } - }); - } - - // 处理主周期KLU分型 - if ($('#showKluFxType').is(':checked') && currentData.klu_fx_info && currentData.klu_fx_info.length > 0) { - console.log(`绘制主周期K线未合并分型标签,共${currentData.klu_fx_info.length}条`); - - currentData.klu_fx_info.forEach(function(fx) { - try { - // 直接使用UTC时间戳(秒) - const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); - const price = parseFloat(fx.price); - - if (isNaN(timestamp) || isNaN(price)) { - console.error('主周期KLU分型时间或价格转换错误:', fx.time, fx.price); - return; - } - - // 主周期 KLU - const color = fx.is_bottom ? '#17a2b8' : '#fd7e14'; // 底分型用青色,顶分型用橙色 - - // 根据强度等级调整颜色强度 - let strengthColor = color; - if (fx.is_strong_fx) { - // 强分型使用更亮的颜色 - strengthColor = fx.is_bottom ? '#20c997' : '#fd7e14'; - } - - // 构建显示文本,包含分型类型和强度信息 - let displayText = `${fx.fx_strength.toFixed(1)}`; - if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 - displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 - } - displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", ""); - // 添加标记配置 - const markerConfig = { - time: timestamp, - position: fx.is_bottom ? 'belowBar' : 'aboveBar', - color: strengthColor, - // shape: 'triangle', // 使用三角形区分KLU分型 - text: displayText, - size: fx.is_strong_fx ? 0.8 : 0.5 // KLU分型稍小一些 - }; - - allMainFxMarkers.push(markerConfig); - - // 创建分型标记对象,包含tooltip信息 - const fxMarker = { - time: timestamp, - tooltip: `
- 主周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}
- 强度分数: ${fx.fx_strength}分
- 强度等级: ${fx.fx_strength_level}
- 是否强分型: ${fx.is_strong_fx ? '是' : '否'}
- 价格: ${price.toFixed(4)}
- 时间: ${fx.time} -
` - }; - - mainFxMarkers.push(fxMarker); - - } catch (e) { - console.error('绘制主周期KLU分型标签出错:', e); - } - }); - } - - // 暂存主周期分型标记 - window.mainFxMarkers = allMainFxMarkers; - - // 将分型标记添加到全局markers中以支持tooltip功能 - if (window.fxMarkers) { - window.fxMarkers = [...window.fxMarkers, ...mainFxMarkers]; - } else { - window.fxMarkers = mainFxMarkers; - } - - // 检查是否有任何主周期分型数据 - const hasMainFxData = ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) || - ($('#showKluFxType').is(':checked') && currentData.klu_fx_info && currentData.klu_fx_info.length > 0); - - if (!hasMainFxData) { - console.log('绘制主周期分型标记 - 已禁用或无数据'); - // 清空主周期分型标记 - window.mainFxMarkers = []; - window.fxMarkers = []; - } - // 绘制小周期分型标记(含次次周期) - if (($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) || - ($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) || - ($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0)) { - - // 收集所有小周期分型标记 - const allElementFxMarkers = []; - const elementFxMarkers = []; // 用于tooltip支持 - - // 处理小周期KLC分型 - if ($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) { - console.log(`绘制小周期K线合并分型标记,共${currentData.element_klc_fx_info.length}条`); - - currentData.element_klc_fx_info.forEach(function(fx) { - try { - // 直接使用UTC时间戳(秒) - const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); - const price = parseFloat(fx.price); - - if (isNaN(timestamp) || isNaN(price)) { - console.error('小周期KLC分型时间或价格转换错误:', fx.time, fx.price); - return; - } - - // 小周期 KLC - let strengthColor = fx.is_bottom ? '#11116B' : '#222222'; // 底分型用珊瑚红,顶分型用薄荷绿 - let displayText = `${fx.fx_strength.toFixed(1)}`; - // 构建小周期分型显示文本 - if (fx.fx_strength < 1.0){ // 调整小周期阈值 - displayText = fx.fx_strength >= 0.6 ? '' : '' // 0.6以上显示点 - } - displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", ""); - // 小周期分型标记配置 - const markerConfig = { - time: timestamp, - position: fx.is_bottom ? 'belowBar' : 'aboveBar', - color: strengthColor, - shape: 'triangle', - text: displayText, - size: fx.is_strong_fx ? 0.8 : 0.6 // 小周期标记整体更小一些 - }; - - allElementFxMarkers.push(markerConfig); - - // 画虚线分型框(小周期) - if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { - const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); - const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); - const high = parseFloat(fx.high); - const low = parseFloat(fx.low); - - if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { - const boxHigh = Math.max(high, low); - const boxLow = Math.min(high, low); - const boxColor = strengthColor; - - const topSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); - - const bottomSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); - - const leftSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); - - const rightSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); - - if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = []; - tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); - } - } - - // 创建小周期分型标记对象,包含tooltip信息 - const elementFxMarker = { - time: timestamp, - tooltip: `
- 小周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}
- 强度分数: ${fx.fx_strength}分
- 强度等级: ${fx.fx_strength_level}
- 是否强分型: ${fx.is_strong_fx ? '是' : '否'}
- 价格: ${price.toFixed(4)}
- 时间: ${fx.time} -
` - }; - - elementFxMarkers.push(elementFxMarker); - - } catch (e) { - console.error('绘制小周期KLC分型标记出错:', e); - } - }); - } - - // 处理小周期KLU分型 - if ($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) { - console.log(`绘制小周期K线未合并分型标记,共${currentData.element_klu_fx_info.length}条`); - - currentData.element_klu_fx_info.forEach(function(fx) { - try { - // 直接使用UTC时间戳(秒) - const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); - const price = parseFloat(fx.price); - - if (isNaN(timestamp) || isNaN(price)) { - console.error('小周期KLU分型时间或价格转换错误:', fx.time, fx.price); - return; - } - - // 小周期 KLU - let strengthColor = fx.is_bottom ? '#9A8C98' : '#F2CC8F'; // 底分型用灰紫色,顶分型用浅黄色 - let displayText = `${fx.fx_strength.toFixed(1)}`; - // 构建小周期分型显示文本 - if (fx.fx_strength < 2.0){ // 调整小周期阈值 - displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.6以上显示点 - } - displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", ""); - // 小周期KLU分型标记配置 - const markerConfig = { - time: timestamp, - position: fx.is_bottom ? 'belowBar' : 'aboveBar', - color: strengthColor, - // shape: 'triangle', // 使用三角形区分KLU分型 - text: displayText, - size: fx.is_strong_fx ? 0.7 : 0.5 // 小周期KLU标记更小一些 - }; - - allElementFxMarkers.push(markerConfig); - - // 创建小周期分型标记对象,包含tooltip信息 - const elementFxMarker = { - time: timestamp, - tooltip: `
- 小周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}
- 强度分数: ${fx.fx_strength}分
- 强度等级: ${fx.fx_strength_level}
- 是否强分型: ${fx.is_strong_fx ? '是' : '否'}
- 价格: ${price.toFixed(4)}
- 时间: ${fx.time} -
` - }; - - elementFxMarkers.push(elementFxMarker); - - } catch (e) { - console.error('绘制小周期KLU分型标记出错:', e); - } - }); - } - - // 次次周期KLC分型 - if ($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0) { - currentData.sub_sub_klc_fx_info.forEach(function(fx) { - try { - const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); - const price = parseFloat(fx.price); - if (isNaN(timestamp) || isNaN(price)) return; - const strengthColor = '#00897b'; - let displayText = (fx.fx_type || '').replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", ""); - const markerConfig = { - time: timestamp, - position: fx.is_bottom ? 'belowBar' : 'aboveBar', - color: strengthColor, - shape: 'triangle', - text: displayText, - size: (fx.is_strong_fx ? 0.6 : 0.5) - }; - allElementFxMarkers.push(markerConfig); - - // 画虚线分型框(次次周期) - if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { - const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); - const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); - const high = parseFloat(fx.high); - const low = parseFloat(fx.low); - - if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { - const boxHigh = Math.max(high, low); - const boxLow = Math.min(high, low); - const boxColor = strengthColor; - - const topSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); - - const bottomSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); - - const leftSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); - - const rightSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); - - if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = []; - tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); - } - } - } catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); } - }); - } - - // 将小周期分型标记添加到全局markers中以支持tooltip功能 - if (window.fxMarkers) { - window.fxMarkers = [...window.fxMarkers, ...elementFxMarkers]; - } else { - window.fxMarkers = elementFxMarkers; - } - - // 基于后端提供的 KLC 趋势生成标记(不进行任何计算) - let klcTrendMarkers = []; - try { - if (currentData.klc_trend && currentData.klc_trend.length > 0) { - console.log('KLC趋势点数量:', currentData.klc_trend.length, currentData.klc_trend.slice(0, 3)); - // 当前图表的bar时间集合(秒)用于对齐标记到最近的K线 - const seriesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); - const nearestTime = (target) => { - if (!Array.isArray(candles) || candles.length === 0) return target; - // 简单线性查找(数据量通常可接受),必要时可替换为二分 - let best = candles[0].time; - let bestDiff = Math.abs(best - target); - for (let i = 1; i < candles.length; i++) { - const t = candles[i].time; - const d = Math.abs(t - target); - if (d < bestDiff) { best = t; bestDiff = d; } - } - return best; - }; - - klcTrendMarkers = currentData.klc_trend.map(t => { - const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); - let timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts); - let marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'square', size: 0.8 }; - if (trendRaw === 'UP') { - marker = { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; - } else if (trendRaw === 'DOWN') { - marker = { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; - } else if (trendRaw === 'FLAT') { - marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; - } else { - // UNKNOWN 或其他 - marker = { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; - } - return marker; - }); - console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5)); - } - } catch (e) { - klcTrendMarkers = []; - } - // 暴露到全局以便调试或后续合并 - window.klcTrendMarkers = klcTrendMarkers; - - // 无论当前显示主/小周期,只要勾选对应Trend,就叠加出来 - let trendMarkersToUse = []; - if ($('#showMainTrend').is(':checked')) { - trendMarkersToUse = trendMarkersToUse.concat(window.klcTrendMarkers || []); - } - if ($('#showElementTrend').is(':checked') && currentData.element_klc_trend) { - const candlesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); - const nearestTime = (target) => { - if (!Array.isArray(candles) || candles.length === 0) return target; - let best = candles[0].time, bestDiff = Math.abs(best - target); - for (let i = 1; i < candles.length; i++) { - const t = candles[i].time, d = Math.abs(t - target); - if (d < bestDiff) { best = t; bestDiff = d; } - } - return best; - }; - const elementMarkers = currentData.element_klc_trend.map(t => { - const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); - const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts); - if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; - if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; - if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; - return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; - }); - trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); - } - if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) { - const candlesTimesSs = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); - const nearestTimeSs = (target) => { - if (!Array.isArray(candles) || candles.length === 0) return target; - let best = candles[0].time, bestDiff = Math.abs(best - target); - for (let i = 1; i < candles.length; i++) { - const t = candles[i].time, d = Math.abs(t - target); - if (d < bestDiff) { best = t; bestDiff = d; } - } - return best; - }; - const subSubColor = '#00897b'; - const subSubMarkers = currentData.sub_sub_klc_trend.map(t => { - const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); - const timeAligned = candlesTimesSs.has(ts) ? ts : nearestTimeSs(ts); - if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor, shape: 'arrowUp', size: 0.4 }; - if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor, shape: 'arrowDown', size: 0.4 }; - if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'circle', size: 0.5 }; - return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'square', size: 0.5 }; - }); - trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers); - } - - // 合并标记并设置 - const combinedMarkers = [ - ...(window.mainFxMarkers || []), - ...allElementFxMarkers, - ...(window.kluDivMarkersMain || []), - ...(window.kluDivMarkersElement || []), - ...(window.kluDivMarkersSubSub || []), - ...trendMarkersToUse, - ...(window.bspMarkers || []), - ...(window.wrMarkers || []) - ]; - if (combinedMarkers.length > 0) { - console.log( - '合并设置', combinedMarkers.length, '个标记(主周期分型:', - (window.mainFxMarkers || []).length, - '个,小周期分型:', allElementFxMarkers.length, - '个,UnitTF:', (window.unittfMarkers || []).length, - '个,BSP标记:', (window.bspMarkers || []).length, - '个,区间标记:', (window.wrMarkers || []).length, - '个)' - ); - - // 根据当前主系列类型设置标记 - const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); - let targetSeries = null; - if (klineType === 'candlestick') targetSeries = tvWidget.series.candleSeries; - else if (klineType === 'renko') targetSeries = tvWidget.series.renkoSeries; - else if (klineType === 'heikin') targetSeries = tvWidget.series.heikinSeries; - else if (klineType === 'bar') targetSeries = tvWidget.series.barSeries; - else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries; - else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries; - else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries; - else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries; - if (targetSeries) { - try { - targetSeries.setMarkers(combinedMarkers); - } catch (e) { - console.warn('设置主系列标记失败(可能series已释放):', e); - } - } else { - console.log('未找到主数据系列,无法设置标记'); - } - } - - } else { - console.log('绘制小周期分型标记 - 已禁用或无数据'); - - // 计算并缓存KLC趋势标记(即使未启用小周期分型,也应显示趋势) - try { - let klcTrendMarkers = []; - if (currentData.klc_trend && currentData.klc_trend.length > 0) { - console.log('KLC趋势点数量:', currentData.klc_trend.length, currentData.klc_trend.slice(0, 3)); - const seriesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); - const nearestTime = (target) => { - if (!Array.isArray(candles) || candles.length === 0) return target; - let best = candles[0].time; - let bestDiff = Math.abs(best - target); - for (let i = 1; i < candles.length; i++) { - const t = candles[i].time; - const d = Math.abs(t - target); - if (d < bestDiff) { best = t; bestDiff = d; } - } - return best; - }; - klcTrendMarkers = currentData.klc_trend.map(t => { - const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); - const timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts); - if (trendRaw === 'UP') { - return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; - } else if (trendRaw === 'DOWN') { - return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; - } else if (trendRaw === 'FLAT') { - return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; - } else { - return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; - } - }); - console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5)); - } - window.klcTrendMarkers = klcTrendMarkers; - } catch (e) { - window.klcTrendMarkers = []; - } - - // 与上方一致:勾选哪个Trend就显示哪个 - let trendMarkersToUse = []; - if ($('#showMainTrend').is(':checked')) { - trendMarkersToUse = trendMarkersToUse.concat(window.klcTrendMarkers || []); - } - if ($('#showElementTrend').is(':checked') && currentData.element_klc_trend) { - const candlesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); - const nearestTime = (target) => { - if (!Array.isArray(candles) || candles.length === 0) return target; - let best = candles[0].time, bestDiff = Math.abs(best - target); - for (let i = 1; i < candles.length; i++) { - const t = candles[i].time, d = Math.abs(t - target); - if (d < bestDiff) { best = t; bestDiff = d; } - } - return best; - }; - const elementMarkers = currentData.element_klc_trend.map(t => { - const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); - const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts); - if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; - if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; - if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; - return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; - }); - trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); - } - if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) { - const candlesTimesSs2 = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); - const nearestTimeSs2 = (target) => { - if (!Array.isArray(candles) || candles.length === 0) return target; - let best = candles[0].time, bestDiff = Math.abs(best - target); - for (let i = 1; i < candles.length; i++) { - const t = candles[i].time, d = Math.abs(t - target); - if (d < bestDiff) { best = t; bestDiff = d; } - } - return best; - }; - const subSubColor2 = '#00897b'; - const subSubMarkers2 = currentData.sub_sub_klc_trend.map(t => { - const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); - const timeAligned = candlesTimesSs2.has(ts) ? ts : nearestTimeSs2(ts); - if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor2, shape: 'arrowUp', size: 0.4 }; - if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor2, shape: 'arrowDown', size: 0.4 }; - if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'circle', size: 0.5 }; - return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'square', size: 0.5 }; - }); - trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers2); - } - // 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记 - // 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记, - // 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。 - // 修复:把 BSP 标记一并合并进来。 - const onlyMainAndU = [ - ...(window.mainFxMarkers || []), - ...(window.kluDivMarkersMain || []), - ...(window.kluDivMarkersElement || []), - ...(window.kluDivMarkersSubSub || []), - ...trendMarkersToUse, - ...(window.bspMarkers || []), - ...(window.wrMarkers || []) - ]; - if (onlyMainAndU.length > 0) { - console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, ',UnitTF:', (window.unittfMarkers || []).length, ')'); - - // 根据当前主系列类型设置标记 - const klineType2 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); - let targetSeries2 = null; - if (klineType2 === 'candlestick') targetSeries2 = tvWidget.series.candleSeries; - else if (klineType2 === 'renko') targetSeries2 = tvWidget.series.renkoSeries; - else if (klineType2 === 'heikin') targetSeries2 = tvWidget.series.heikinSeries; - else if (klineType2 === 'bar') targetSeries2 = tvWidget.series.barSeries; - else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries; - else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries; - else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries; - else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries; - if (targetSeries2) { - try { - targetSeries2.setMarkers(onlyMainAndU); - } catch (e) { - console.warn('设置主系列标记失败(可能series已释放):', e); - } - } else { - console.log('未找到主数据系列,无法设置标记'); - } - } else { - console.log('没有分型标记需要显示,清空图表标记'); - // 清空图表上的所有主系列标记 - const klineType3 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); - let targetSeries3 = null; - if (klineType3 === 'candlestick') targetSeries3 = tvWidget.series.candleSeries; - else if (klineType3 === 'renko') targetSeries3 = tvWidget.series.renkoSeries; - else if (klineType3 === 'heikin') targetSeries3 = tvWidget.series.heikinSeries; - else if (klineType3 === 'bar') targetSeries3 = tvWidget.series.barSeries; - else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries; - else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries; - else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries; - else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries; - if (targetSeries3) { - try { - targetSeries3.setMarkers([]); - } catch (e) { - console.warn('清空主系列标记失败(可能series已释放):', e); - } - } - } - } - - // 同步所有图表的时间轴配置 - const syncTimeScaleSettings = () => { - // 获取主图表的时间轴设置 - const mainTimeScale = mainChart.timeScale(); - const baseOptions = { - timeVisible: true, - secondsVisible: false, - borderColor: '#ddd', - barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10, - rightOffset: 12, - lockVisibleTimeRangeOnResize: true, - // 关键:确保所有图表边缘行为完全一致 - fixLeftEdge: false, - fixRightEdge: false, - // 确保时间刻度行为一致 - ticksVisible: true, - minimumHeight: 0, - }; - - console.log('🔧 同步时间轴设置:', baseOptions); - - // 应用相同的设置到所有图表 - mainChart.timeScale().applyOptions(baseOptions); - volumeChart.timeScale().applyOptions(baseOptions); - atrChart.timeScale().applyOptions(baseOptions); - if (showMacd && macdChart) { - macdChart.timeScale().applyOptions(baseOptions); - } - }; - - // 首先同步时间轴设置 - syncTimeScaleSettings(); - - // 仅在没有待恢复视图时,设置默认可见范围 - const totalBars = candles ? candles.length : 0; - const visibleBarsCount = 200; - const hasPendingRestoreView = !!window._pendingRestoreView; - if (!hasPendingRestoreView) { - // 显示最近 200 根K线而非全部挤压(避免K线过多时重叠) - if (totalBars > visibleBarsCount) { - const rangeFrom = totalBars - visibleBarsCount; - const rangeTo = totalBars + 12; - mainChart.timeScale().setVisibleLogicalRange({ from: rangeFrom, to: rangeTo }); - } else { - mainChart.timeScale().fitContent(); - } - } - - // 立即同步其他图表到主图表的范围 - setTimeout(() => { - const logRange = mainChart.timeScale().getVisibleLogicalRange(); - if (logRange) { - console.log('🔧 同步可见范围:', logRange); - volumeChart.timeScale().setVisibleLogicalRange(logRange); - atrChart.timeScale().setVisibleLogicalRange(logRange); - if (showMacd && macdChart) { - macdChart.timeScale().setVisibleLogicalRange(logRange); - } - if (showMacd && chanMacdChart) { - chanMacdChart.timeScale().setVisibleLogicalRange(logRange); - } - console.log('🔧 时间轴同步完成'); - } - }, 50); - - // 保存图表对象 - tvWidget.mainChart = mainChart; - tvWidget.volumeChart = volumeChart; - tvWidget.atrChart = atrChart; - tvWidget.macdChart = macdChart; - tvWidget.chanMacdChart = chanMacdChart; - tvWidget.state.isInitialized = true; - // 注册窗口卸载时释放资源,避免GPU内存泄漏 - window.onbeforeunload = function() { - try { - if (tvWidget && tvWidget.state && tvWidget.state.isInitialized) { - if (tvWidget.mainChart && typeof tvWidget.mainChart.remove === 'function') tvWidget.mainChart.remove(); - if (tvWidget.volumeChart && typeof tvWidget.volumeChart.remove === 'function') tvWidget.volumeChart.remove(); - if (tvWidget.macdChart && typeof tvWidget.macdChart.remove === 'function') tvWidget.macdChart.remove(); - if (tvWidget.chanMacdChart && typeof tvWidget.chanMacdChart.remove === 'function') tvWidget.chanMacdChart.remove(); - if (tvWidget.atrChart && typeof tvWidget.atrChart.remove === 'function') tvWidget.atrChart.remove(); - } - } catch (e) {} - }; - - // 初始化默认均线/布林带配置(仅在首次初始化时) - if (!hasInitializedDefaultMAs && movingAverages.length === 0) { - console.log('初始化默认均线与布林带指标'); - - if (typeof maIdCounter !== 'number' || !Number.isFinite(maIdCounter)) { - maIdCounter = 0; - } - if (typeof bbIdCounter !== 'number' || !Number.isFinite(bbIdCounter)) { - bbIdCounter = 0; - } - - const defaultMAs = [ - { type: 'EMA', length: 13, color: '#800080', name: 'EMA13', visible: true }, // 紫色 - { type: 'EMA', length: 26, color: '#FF8C00', name: 'EMA26', visible: true }, // 橙色 - { type: 'EMA', length: 52, color: '#000000', name: 'EMA52', visible: false }, // 黑色 - { type: 'EMA', length: 104, color: '#1E90FF', name: 'EMA104', visible: false }, // 蓝色 - { type: 'EMA', length: 156, color: '#F700FF', name: 'EMA156', visible: false } // 粉色 - ]; - - defaultMAs.forEach(ma => { - const config = { - id: ++maIdCounter, - type: ma.type, - length: ma.length, - source: 'close', - smoothType: 'none', - smoothLength: 3, - lineWidth: 1, // 1px线宽 - lineStyle: 0, // 实线 - color: ma.color, - visible: ma.visible - }; - - movingAverages.push(config); - console.log(`添加默认${ma.name}:`, ma.color); - }); - - if (bollingerBands.length === 0) { - const defaultBB = { - id: ++bbIdCounter, - type: 'Bollinger Bands', - length: 20, - upperMultiplier: 2, - lowerMultiplier: 2, - source: 'close', - lineWidth: 1, - lineStyle: 0, - upperColor: '#ff6b6b', - middleColor: '#ffffff', - lowerColor: '#ff6b6b', - visible: false - }; - bollingerBands.push(defaultBB); - console.log('添加默认布林带: BB(20, 2, 2)'); - } - - console.log('默认指标配置完成,当前均线数量', movingAverages.length, '布林带数量', bollingerBands.length); - hasInitializedDefaultMAs = true; - } - - // 添加均线到图表 - addMovingAveragesToChart(candles); - - // 添加布林带到图表 - addBollingerBandsToChart(candles); - - // 更新技术指标面板显示 - updateIndicatorPanel(); - - // 绑定同步事件 - bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, mainChart, volumeChart, atrChart, macdChart, chanMacdChart, showMacd); - - // 最终确保所有图表时间轴对齐(同时恢复刷新前保存的缩放/位置) - setTimeout(() => { - const allCharts = [mainChart, volumeChart, atrChart]; - if (showMacd && macdChart) allCharts.push(macdChart); - if (showMacd && chanMacdChart) allCharts.push(chanMacdChart); - - // 检查是否有待恢复的视图(缩放 + 位置) - const pending = window._pendingRestoreView; - window._pendingRestoreView = null; - - if (pending) { - // 恢复刷新前的缩放和位置(优先可见范围/逻辑范围,最后回退到滚动位置) - console.log('📌 恢复图表视图:', JSON.stringify(pending)); - restoreChartViewState(allCharts, pending); - } else { - // 无保存视图,正常同步主图到子图 - const visibleRange = mainChart.timeScale().getVisibleRange(); - if (visibleRange) { - console.log('🔧 最终同步可见范围:', visibleRange); - [volumeChart, atrChart].concat( - showMacd && macdChart ? [macdChart] : [], - showMacd && chanMacdChart ? [chanMacdChart] : [] - ).forEach(c => { - try { c.timeScale().setVisibleRange(visibleRange); } catch(e) {} - }); - } - } - console.log('🔧 最终时间轴对齐完成'); - }, 150); - // 只有在时间输入框都为空时才设置图表默认时间范围 - if (!$('#start_time').val() && !$('#end_time').val()) { - setDefaultTimeRange(); - } - - // 添加买卖点提示 - // 初始化 tooltip 与 U 显示状态 - window.showUOnMain = $('#toggleUOnMain').is(':checked'); - window.showUOnElement = $('#toggleUOnElement').is(':checked'); - setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd); - - // 更新EMA52显示 - if (currentData) { - updateEMA52Display(currentData); - } - console.log('图表初始化完成'); } catch (e) { console.error('图表初始化错误:', e); } } -// 增量更新图表数据 diff --git a/web/static/js/app/chart_tv_chan.js b/web/static/js/app/chart_tv_chan.js new file mode 100644 index 0000000..35dc2e0 --- /dev/null +++ b/web/static/js/app/chart_tv_chan.js @@ -0,0 +1,1103 @@ +/* chart_tv_chan.js — BI / SEG / ZS */ + +function chartTvRenderChan(ctx) { + var symbol = ctx.symbol; + var timeframe = ctx.timeframe; + var symbolConfig = ctx.symbolConfig; + var useSubSubPeriod = ctx.useSubSubPeriod; + var useElementPeriod = ctx.useElementPeriod; + var klinePeriodLabel = ctx.klinePeriodLabel; + var candles = ctx.candles; + var klineDataSource = ctx.klineDataSource; + var container = ctx.container; + var showMacd = ctx.showMacd; + var showOriginalKline = ctx.showOriginalKline; + var mainChartContainer = ctx.mainChartContainer; + var volumeChartContainer = ctx.volumeChartContainer; + var atrChartContainer = ctx.atrChartContainer; + var macdChartContainer = ctx.macdChartContainer; + var chanMacdChartContainer = ctx.chanMacdChartContainer; + var mainChart = ctx.mainChart; + var volumeChart = ctx.volumeChart; + var atrChart = ctx.atrChart; + var macdChart = ctx.macdChart; + var chanMacdChart = ctx.chanMacdChart; + var createChartOptions = ctx.createChartOptions; + // 图表同步事件统一由文末 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); } + }); + } +} diff --git a/web/static/js/app/chart_tv_finalize.js b/web/static/js/app/chart_tv_finalize.js new file mode 100644 index 0000000..7186bb1 --- /dev/null +++ b/web/static/js/app/chart_tv_finalize.js @@ -0,0 +1,228 @@ +/* chart_tv_finalize.js — time sync / bindSync / view restore / tooltip */ + +function chartTvFinalize(ctx) { + var symbol = ctx.symbol; + var timeframe = ctx.timeframe; + var symbolConfig = ctx.symbolConfig; + var useSubSubPeriod = ctx.useSubSubPeriod; + var useElementPeriod = ctx.useElementPeriod; + var klinePeriodLabel = ctx.klinePeriodLabel; + var candles = ctx.candles; + var klineDataSource = ctx.klineDataSource; + var container = ctx.container; + var showMacd = ctx.showMacd; + var showOriginalKline = ctx.showOriginalKline; + var mainChartContainer = ctx.mainChartContainer; + var volumeChartContainer = ctx.volumeChartContainer; + var atrChartContainer = ctx.atrChartContainer; + var macdChartContainer = ctx.macdChartContainer; + var chanMacdChartContainer = ctx.chanMacdChartContainer; + var mainChart = ctx.mainChart; + var volumeChart = ctx.volumeChart; + var atrChart = ctx.atrChart; + var macdChart = ctx.macdChart; + var chanMacdChart = ctx.chanMacdChart; + var createChartOptions = ctx.createChartOptions; + // 同步所有图表的时间轴配置 + const syncTimeScaleSettings = () => { + // 获取主图表的时间轴设置 + const mainTimeScale = mainChart.timeScale(); + const baseOptions = { + timeVisible: true, + secondsVisible: false, + borderColor: '#ddd', + barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10, + rightOffset: 12, + lockVisibleTimeRangeOnResize: true, + // 关键:确保所有图表边缘行为完全一致 + fixLeftEdge: false, + fixRightEdge: false, + // 确保时间刻度行为一致 + ticksVisible: true, + minimumHeight: 0, + }; + + console.log('🔧 同步时间轴设置:', baseOptions); + + // 应用相同的设置到所有图表 + mainChart.timeScale().applyOptions(baseOptions); + volumeChart.timeScale().applyOptions(baseOptions); + atrChart.timeScale().applyOptions(baseOptions); + if (showMacd && macdChart) { + macdChart.timeScale().applyOptions(baseOptions); + } + }; + + // 首先同步时间轴设置 + syncTimeScaleSettings(); + + // 仅在没有待恢复视图时,设置默认可见范围 + const totalBars = candles ? candles.length : 0; + const visibleBarsCount = 200; + const hasPendingRestoreView = !!window._pendingRestoreView; + if (!hasPendingRestoreView) { + // 显示最近 200 根K线而非全部挤压(避免K线过多时重叠) + if (totalBars > visibleBarsCount) { + const rangeFrom = totalBars - visibleBarsCount; + const rangeTo = totalBars + 12; + mainChart.timeScale().setVisibleLogicalRange({ from: rangeFrom, to: rangeTo }); + } else { + mainChart.timeScale().fitContent(); + } + } + + // 立即同步其他图表到主图表的范围 + setTimeout(() => { + const logRange = mainChart.timeScale().getVisibleLogicalRange(); + if (logRange) { + console.log('🔧 同步可见范围:', logRange); + volumeChart.timeScale().setVisibleLogicalRange(logRange); + atrChart.timeScale().setVisibleLogicalRange(logRange); + if (showMacd && macdChart) { + macdChart.timeScale().setVisibleLogicalRange(logRange); + } + if (showMacd && chanMacdChart) { + chanMacdChart.timeScale().setVisibleLogicalRange(logRange); + } + console.log('🔧 时间轴同步完成'); + } + }, 50); + + // 保存图表对象 + tvWidget.mainChart = mainChart; + tvWidget.volumeChart = volumeChart; + tvWidget.atrChart = atrChart; + tvWidget.macdChart = macdChart; + tvWidget.chanMacdChart = chanMacdChart; + tvWidget.state.isInitialized = true; + // 注册窗口卸载时释放资源,避免GPU内存泄漏 + window.onbeforeunload = function() { + try { + if (tvWidget && tvWidget.state && tvWidget.state.isInitialized) { + if (tvWidget.mainChart && typeof tvWidget.mainChart.remove === 'function') tvWidget.mainChart.remove(); + if (tvWidget.volumeChart && typeof tvWidget.volumeChart.remove === 'function') tvWidget.volumeChart.remove(); + if (tvWidget.macdChart && typeof tvWidget.macdChart.remove === 'function') tvWidget.macdChart.remove(); + if (tvWidget.chanMacdChart && typeof tvWidget.chanMacdChart.remove === 'function') tvWidget.chanMacdChart.remove(); + if (tvWidget.atrChart && typeof tvWidget.atrChart.remove === 'function') tvWidget.atrChart.remove(); + } + } catch (e) {} + }; + + // 初始化默认均线/布林带配置(仅在首次初始化时) + if (!hasInitializedDefaultMAs && movingAverages.length === 0) { + console.log('初始化默认均线与布林带指标'); + + if (typeof maIdCounter !== 'number' || !Number.isFinite(maIdCounter)) { + maIdCounter = 0; + } + if (typeof bbIdCounter !== 'number' || !Number.isFinite(bbIdCounter)) { + bbIdCounter = 0; + } + + const defaultMAs = [ + { type: 'EMA', length: 13, color: '#800080', name: 'EMA13', visible: true }, // 紫色 + { type: 'EMA', length: 26, color: '#FF8C00', name: 'EMA26', visible: true }, // 橙色 + { type: 'EMA', length: 52, color: '#000000', name: 'EMA52', visible: false }, // 黑色 + { type: 'EMA', length: 104, color: '#1E90FF', name: 'EMA104', visible: false }, // 蓝色 + { type: 'EMA', length: 156, color: '#F700FF', name: 'EMA156', visible: false } // 粉色 + ]; + + defaultMAs.forEach(ma => { + const config = { + id: ++maIdCounter, + type: ma.type, + length: ma.length, + source: 'close', + smoothType: 'none', + smoothLength: 3, + lineWidth: 1, // 1px线宽 + lineStyle: 0, // 实线 + color: ma.color, + visible: ma.visible + }; + + movingAverages.push(config); + console.log(`添加默认${ma.name}:`, ma.color); + }); + + if (bollingerBands.length === 0) { + const defaultBB = { + id: ++bbIdCounter, + type: 'Bollinger Bands', + length: 20, + upperMultiplier: 2, + lowerMultiplier: 2, + source: 'close', + lineWidth: 1, + lineStyle: 0, + upperColor: '#ff6b6b', + middleColor: '#ffffff', + lowerColor: '#ff6b6b', + visible: false + }; + bollingerBands.push(defaultBB); + console.log('添加默认布林带: BB(20, 2, 2)'); + } + + console.log('默认指标配置完成,当前均线数量', movingAverages.length, '布林带数量', bollingerBands.length); + hasInitializedDefaultMAs = true; + } + + // 添加均线到图表 + addMovingAveragesToChart(candles); + + // 添加布林带到图表 + addBollingerBandsToChart(candles); + + // 更新技术指标面板显示 + updateIndicatorPanel(); + + // 绑定同步事件 + bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, mainChart, volumeChart, atrChart, macdChart, chanMacdChart, showMacd); + + // 最终确保所有图表时间轴对齐(同时恢复刷新前保存的缩放/位置) + setTimeout(() => { + const allCharts = [mainChart, volumeChart, atrChart]; + if (showMacd && macdChart) allCharts.push(macdChart); + if (showMacd && chanMacdChart) allCharts.push(chanMacdChart); + + // 检查是否有待恢复的视图(缩放 + 位置) + const pending = window._pendingRestoreView; + window._pendingRestoreView = null; + + if (pending) { + // 恢复刷新前的缩放和位置(优先可见范围/逻辑范围,最后回退到滚动位置) + console.log('📌 恢复图表视图:', JSON.stringify(pending)); + restoreChartViewState(allCharts, pending); + } else { + // 无保存视图,正常同步主图到子图 + const visibleRange = mainChart.timeScale().getVisibleRange(); + if (visibleRange) { + console.log('🔧 最终同步可见范围:', visibleRange); + [volumeChart, atrChart].concat( + showMacd && macdChart ? [macdChart] : [], + showMacd && chanMacdChart ? [chanMacdChart] : [] + ).forEach(c => { + try { c.timeScale().setVisibleRange(visibleRange); } catch(e) {} + }); + } + } + console.log('🔧 最终时间轴对齐完成'); + }, 150); + // 只有在时间输入框都为空时才设置图表默认时间范围 + if (!$('#start_time').val() && !$('#end_time').val()) { + setDefaultTimeRange(); + } + + // 添加买卖点提示 + // 初始化 tooltip 与 U 显示状态 + window.showUOnMain = $('#toggleUOnMain').is(':checked'); + window.showUOnElement = $('#toggleUOnElement').is(':checked'); + setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd); + + // 更新EMA52显示 + if (currentData) { + updateEMA52Display(currentData); + } + +} diff --git a/web/static/js/app/chart_tv_indicators.js b/web/static/js/app/chart_tv_indicators.js new file mode 100644 index 0000000..eb6f5b2 --- /dev/null +++ b/web/static/js/app/chart_tv_indicators.js @@ -0,0 +1,564 @@ +/* chart_tv_indicators.js — volume / ATR / ChanMACD */ + +function chartTvRenderIndicators(ctx) { + var symbol = ctx.symbol; + var timeframe = ctx.timeframe; + var symbolConfig = ctx.symbolConfig; + var useSubSubPeriod = ctx.useSubSubPeriod; + var useElementPeriod = ctx.useElementPeriod; + var klinePeriodLabel = ctx.klinePeriodLabel; + var candles = ctx.candles; + var klineDataSource = ctx.klineDataSource; + var container = ctx.container; + var showMacd = ctx.showMacd; + var showOriginalKline = ctx.showOriginalKline; + var mainChartContainer = ctx.mainChartContainer; + var volumeChartContainer = ctx.volumeChartContainer; + var atrChartContainer = ctx.atrChartContainer; + var macdChartContainer = ctx.macdChartContainer; + var chanMacdChartContainer = ctx.chanMacdChartContainer; + var mainChart = ctx.mainChart; + var volumeChart = ctx.volumeChart; + var atrChart = ctx.atrChart; + var macdChart = ctx.macdChart; + var chanMacdChart = ctx.chanMacdChart; + var createChartOptions = ctx.createChartOptions; + // 转换成交量数据 - 与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 = []; + } +} diff --git a/web/static/js/app/chart_tv_lifecycle.js b/web/static/js/app/chart_tv_lifecycle.js new file mode 100644 index 0000000..a17a7b6 --- /dev/null +++ b/web/static/js/app/chart_tv_lifecycle.js @@ -0,0 +1,54 @@ +/* chart_tv_lifecycle.js — dispose Lightweight Charts / DOM / listeners */ + +/** 释放 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); + } +} diff --git a/web/static/js/app/chart_tv_overlays.js b/web/static/js/app/chart_tv_overlays.js new file mode 100644 index 0000000..beb421d --- /dev/null +++ b/web/static/js/app/chart_tv_overlays.js @@ -0,0 +1,2341 @@ +/* chart_tv_overlays.js — structure zones / wyckoff / BSP / FX / bollinger */ + +function chartTvRenderOverlays(ctx) { + var symbol = ctx.symbol; + var timeframe = ctx.timeframe; + var symbolConfig = ctx.symbolConfig; + var useSubSubPeriod = ctx.useSubSubPeriod; + var useElementPeriod = ctx.useElementPeriod; + var klinePeriodLabel = ctx.klinePeriodLabel; + var candles = ctx.candles; + var klineDataSource = ctx.klineDataSource; + var container = ctx.container; + var showMacd = ctx.showMacd; + var showOriginalKline = ctx.showOriginalKline; + var mainChartContainer = ctx.mainChartContainer; + var volumeChartContainer = ctx.volumeChartContainer; + var atrChartContainer = ctx.atrChartContainer; + var macdChartContainer = ctx.macdChartContainer; + var chanMacdChartContainer = ctx.chanMacdChartContainer; + var mainChart = ctx.mainChart; + var volumeChart = ctx.volumeChart; + var atrChart = ctx.atrChart; + var macdChart = ctx.macdChart; + var chanMacdChart = ctx.chanMacdChart; + var createChartOptions = ctx.createChartOptions; + // 结构价值区绘制(半透明填充区 + 边框) + 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 = `
时间: ${formattedTime}
` + + (priceInfo ? `
${priceInfo}
` : ''); + crosshairTooltip.style.display = 'block'; + crosshairTooltip.style.left = (param.point.x + 15) + 'px'; + crosshairTooltip.style.top = (param.point.y - 30) + 'px'; + } + + if (allMarkers.length > 0) { + // 有买卖点或分型标记,显示自定义提示 + const tooltips = allMarkers.map(m => m.tooltip).join('

'); + tooltipElement.innerHTML = tooltips; + tooltipElement.style.display = 'block'; + tooltipElement.style.left = (param.point.x + 15) + 'px'; + tooltipElement.style.top = (param.point.y + 15) + 'px'; + } else { + // 隐藏提示 + tooltipElement.style.display = 'none'; + } + } else { + // 隐藏提示 + tooltipElement.style.display = 'none'; + crosshairTooltip.style.display = 'none'; + } + }); + + // 处理图表缩放、平移等事件,隐藏提示 + mainChart.timeScale().subscribeVisibleTimeRangeChange(() => { + tooltipElement.style.display = 'none'; + crosshairTooltip.style.display = 'none'; + }); + } + } else { + console.log('绘制买卖点 - 已禁用'); + } + // 绘制布林带 + if ($('#showMainBollinger').is(':checked') || $('#showElementBollinger').is(':checked')) { + console.log('绘制布林带 - 已启用'); + + // 主周期布林带 + if ($('#showMainBollinger').is(':checked') && currentData.bollinger && currentData.bollinger.upper && currentData.bollinger.lower && currentData.bollinger.middle) { + console.log(`绘制主周期布林带数据,共${currentData.bollinger.upper.length}条`); + + // 准备布林带数据 + const upperBandData = []; + const lowerBandData = []; + const middleBandData = []; + + // 主周期布林带始终使用主周期K线数据作为时间源 + const mainKlineData = currentData.kline_data; + + for (let i = 0; i < mainKlineData.length && i < currentData.bollinger.upper.length; i++) { + const kline = mainKlineData[i]; + const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); + + // 只添加非0的有效数据点 + if (currentData.bollinger.upper[i] && currentData.bollinger.upper[i] !== 0) { + upperBandData.push({ + time: timestamp, + value: currentData.bollinger.upper[i] + }); + } + + if (currentData.bollinger.lower[i] && currentData.bollinger.lower[i] !== 0) { + lowerBandData.push({ + time: timestamp, + value: currentData.bollinger.lower[i] + }); + } + + if (currentData.bollinger.middle[i] && currentData.bollinger.middle[i] !== 0) { + middleBandData.push({ + time: timestamp, + value: currentData.bollinger.middle[i] + }); + } + } + + // 创建布林带上轨 + const upperBandSeries = mainChart.addLineSeries({ + color: '#2196F3', + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + title: '布林上轨' + }); + upperBandSeries.setData(upperBandData); + + // 创建布林带下轨 + const lowerBandSeries = mainChart.addLineSeries({ + color: '#2196F3', + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + title: '布林下轨' + }); + lowerBandSeries.setData(lowerBandData); + + // 创建布林带中轨(移动平均线) + const middleBandSeries = mainChart.addLineSeries({ + color: '#FF9800', + lineWidth: 1, + lastValueVisible: false, + priceLineVisible: false, + title: '布林中轨' + }); + middleBandSeries.setData(middleBandData); + + // 保存到tvWidget.series对象 + tvWidget.series.mainBollingerSeries.push(upperBandSeries); + tvWidget.series.mainBollingerSeries.push(lowerBandSeries); + tvWidget.series.mainBollingerSeries.push(middleBandSeries); + + console.log('主周期布林带绘制完成'); + } + + // 次周期布林带 + if ($('#showElementBollinger').is(':checked') && currentData.element_bollinger && currentData.element_bollinger.upper && currentData.element_bollinger.lower && currentData.element_bollinger.middle) { + console.log(`绘制次周期布林带数据,共${currentData.element_bollinger.upper.length}条`); + + // 准备次周期布林带数据 + const elementUpperBandData = []; + const elementLowerBandData = []; + const elementMiddleBandData = []; + + // 使用次周期K线数据 + const elementKlineData = currentData.element_kline_data || currentData.kline_data; + + for (let i = 0; i < elementKlineData.length && i < currentData.element_bollinger.upper.length; i++) { + const kline = elementKlineData[i]; + const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); + + // 只添加非0的有效数据点 + if (currentData.element_bollinger.upper[i] && currentData.element_bollinger.upper[i] !== 0) { + elementUpperBandData.push({ + time: timestamp, + value: currentData.element_bollinger.upper[i] + }); + } + + if (currentData.element_bollinger.lower[i] && currentData.element_bollinger.lower[i] !== 0) { + elementLowerBandData.push({ + time: timestamp, + value: currentData.element_bollinger.lower[i] + }); + } + + if (currentData.element_bollinger.middle[i] && currentData.element_bollinger.middle[i] !== 0) { + elementMiddleBandData.push({ + time: timestamp, + value: currentData.element_bollinger.middle[i] + }); + } + } + + // 创建次周期布林带上轨 + const elementUpperBandSeries = mainChart.addLineSeries({ + color: '#9C27B0', + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + title: '次周期布林上轨' + }); + elementUpperBandSeries.setData(elementUpperBandData); + + // 创建次周期布林带下轨 + const elementLowerBandSeries = mainChart.addLineSeries({ + color: '#9C27B0', + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + title: '次周期布林下轨' + }); + elementLowerBandSeries.setData(elementLowerBandData); + + // 创建次周期布林带中轨 + const elementMiddleBandSeries = mainChart.addLineSeries({ + color: '#E91E63', + lineWidth: 1, + lastValueVisible: false, + priceLineVisible: false, + title: '次周期布林中轨' + }); + elementMiddleBandSeries.setData(elementMiddleBandData); + + // 保存到tvWidget.series对象 + tvWidget.series.elementBollingerSeries.push(elementUpperBandSeries); + tvWidget.series.elementBollingerSeries.push(elementLowerBandSeries); + tvWidget.series.elementBollingerSeries.push(elementMiddleBandSeries); + + console.log('次周期布林带绘制完成'); + } + } else { + console.log('绘制布林带 - 已禁用'); + } + + // 绘制分型类型标签 + console.log('=== 开始检查分型显示条件 ==='); + console.log('showKlcFxType勾选状态:', $('#showKlcFxType').is(':checked')); + console.log('showKluFxType勾选状态:', $('#showKluFxType').is(':checked')); + console.log('currentData.klc_fx_info存在:', !!currentData.klc_fx_info); + console.log('currentData.klu_fx_info存在:', !!currentData.klu_fx_info); + console.log('currentData.klc_fx_info长度:', currentData.klc_fx_info ? currentData.klc_fx_info.length : 'undefined'); + console.log('currentData.klu_fx_info长度:', currentData.klu_fx_info ? currentData.klu_fx_info.length : 'undefined'); + if (currentData.klc_fx_info && currentData.klc_fx_info.length > 0) { + console.log('前3个klc分型数据样本:', currentData.klc_fx_info.slice(0, 3)); + } + if (currentData.klu_fx_info && currentData.klu_fx_info.length > 0) { + console.log('前3个klu分型数据样本:', currentData.klu_fx_info.slice(0, 3)); + } + // 收集所有主周期分型标记 + const allMainFxMarkers = []; + const mainFxMarkers = []; // 用于tooltip支持 + // 处理主周期KLC分型 + if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) { + console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`); + + currentData.klc_fx_info.forEach(function(fx) { + try { + // 直接使用UTC时间戳(秒) + const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); + const price = parseFloat(fx.price); + + if (isNaN(timestamp) || isNaN(price)) { + console.error('主周期KLC分型时间或价格转换错误:', fx.time, fx.price); + return; + } + // 主周期 KLC + // 确定颜色和位置 + const color = fx.is_bottom ? '#28a745' : '#dc3545'; // 底分型绿色,顶分型红色 + + // 根据强度等级调整颜色强度 + let strengthColor = color; + + // 构建显示文本,包含分型类型和强度信息 + let displayText = `${fx.fx_strength.toFixed(1)}`; + if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 + displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 + } + displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "").replace("41", "").replace("51", "").replace("01", ""); + // 添加标记配置 + const markerConfig = { + time: timestamp, + position: fx.is_bottom ? 'belowBar' : 'aboveBar', + color: strengthColor, + shape: 'triangle', + text: displayText, + size: 2 // 调整尺寸,强分型稍大,普通分型更小 + }; + + allMainFxMarkers.push(markerConfig); + + // 画虚线分型框(根据 start/end + high/low) + if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { + const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); + const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); + const high = parseFloat(fx.high); + const low = parseFloat(fx.low); + + if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { + const boxHigh = Math.max(high, low); + const boxLow = Math.min(high, low); + const boxColor = strengthColor; + + const topSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); + + const bottomSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); + + const leftSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + // 左边竖线:同一 time 上下两个点(和你已有ZS绘制写法保持一致) + leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); + + const rightSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + + if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = []; + tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); + } + } + + // 创建分型标记对象,包含tooltip信息 + const fxMarker = { + time: timestamp, + tooltip: `
+ 主周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}
+ 强度分数: ${fx.fx_strength}分
+ 强度等级: ${fx.fx_strength_level}
+ 是否强分型: ${fx.is_strong_fx ? '是' : '否'}
+ 价格: ${price.toFixed(4)}
+ 时间: ${fx.time} +
` + }; + + mainFxMarkers.push(fxMarker); + + } catch (e) { + console.error('绘制主周期KLC分型标签出错:', e); + } + }); + } + + // 处理主周期KLU分型 + if ($('#showKluFxType').is(':checked') && currentData.klu_fx_info && currentData.klu_fx_info.length > 0) { + console.log(`绘制主周期K线未合并分型标签,共${currentData.klu_fx_info.length}条`); + + currentData.klu_fx_info.forEach(function(fx) { + try { + // 直接使用UTC时间戳(秒) + const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); + const price = parseFloat(fx.price); + + if (isNaN(timestamp) || isNaN(price)) { + console.error('主周期KLU分型时间或价格转换错误:', fx.time, fx.price); + return; + } + + // 主周期 KLU + const color = fx.is_bottom ? '#17a2b8' : '#fd7e14'; // 底分型用青色,顶分型用橙色 + + // 根据强度等级调整颜色强度 + let strengthColor = color; + if (fx.is_strong_fx) { + // 强分型使用更亮的颜色 + strengthColor = fx.is_bottom ? '#20c997' : '#fd7e14'; + } + + // 构建显示文本,包含分型类型和强度信息 + let displayText = `${fx.fx_strength.toFixed(1)}`; + if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 + displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 + } + displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", ""); + // 添加标记配置 + const markerConfig = { + time: timestamp, + position: fx.is_bottom ? 'belowBar' : 'aboveBar', + color: strengthColor, + // shape: 'triangle', // 使用三角形区分KLU分型 + text: displayText, + size: fx.is_strong_fx ? 0.8 : 0.5 // KLU分型稍小一些 + }; + + allMainFxMarkers.push(markerConfig); + + // 创建分型标记对象,包含tooltip信息 + const fxMarker = { + time: timestamp, + tooltip: `
+ 主周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}
+ 强度分数: ${fx.fx_strength}分
+ 强度等级: ${fx.fx_strength_level}
+ 是否强分型: ${fx.is_strong_fx ? '是' : '否'}
+ 价格: ${price.toFixed(4)}
+ 时间: ${fx.time} +
` + }; + + mainFxMarkers.push(fxMarker); + + } catch (e) { + console.error('绘制主周期KLU分型标签出错:', e); + } + }); + } + + // 暂存主周期分型标记 + window.mainFxMarkers = allMainFxMarkers; + + // 将分型标记添加到全局markers中以支持tooltip功能 + if (window.fxMarkers) { + window.fxMarkers = [...window.fxMarkers, ...mainFxMarkers]; + } else { + window.fxMarkers = mainFxMarkers; + } + + // 检查是否有任何主周期分型数据 + const hasMainFxData = ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) || + ($('#showKluFxType').is(':checked') && currentData.klu_fx_info && currentData.klu_fx_info.length > 0); + + if (!hasMainFxData) { + console.log('绘制主周期分型标记 - 已禁用或无数据'); + // 清空主周期分型标记 + window.mainFxMarkers = []; + window.fxMarkers = []; + } + // 绘制小周期分型标记(含次次周期) + if (($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) || + ($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) || + ($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0)) { + + // 收集所有小周期分型标记 + const allElementFxMarkers = []; + const elementFxMarkers = []; // 用于tooltip支持 + + // 处理小周期KLC分型 + if ($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) { + console.log(`绘制小周期K线合并分型标记,共${currentData.element_klc_fx_info.length}条`); + + currentData.element_klc_fx_info.forEach(function(fx) { + try { + // 直接使用UTC时间戳(秒) + const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); + const price = parseFloat(fx.price); + + if (isNaN(timestamp) || isNaN(price)) { + console.error('小周期KLC分型时间或价格转换错误:', fx.time, fx.price); + return; + } + + // 小周期 KLC + let strengthColor = fx.is_bottom ? '#11116B' : '#222222'; // 底分型用珊瑚红,顶分型用薄荷绿 + let displayText = `${fx.fx_strength.toFixed(1)}`; + // 构建小周期分型显示文本 + if (fx.fx_strength < 1.0){ // 调整小周期阈值 + displayText = fx.fx_strength >= 0.6 ? '' : '' // 0.6以上显示点 + } + displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", ""); + // 小周期分型标记配置 + const markerConfig = { + time: timestamp, + position: fx.is_bottom ? 'belowBar' : 'aboveBar', + color: strengthColor, + shape: 'triangle', + text: displayText, + size: fx.is_strong_fx ? 0.8 : 0.6 // 小周期标记整体更小一些 + }; + + allElementFxMarkers.push(markerConfig); + + // 画虚线分型框(小周期) + if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { + const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); + const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); + const high = parseFloat(fx.high); + const low = parseFloat(fx.low); + + if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { + const boxHigh = Math.max(high, low); + const boxLow = Math.min(high, low); + const boxColor = strengthColor; + + const topSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); + + const bottomSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); + + const leftSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); + + const rightSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + + if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = []; + tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); + } + } + + // 创建小周期分型标记对象,包含tooltip信息 + const elementFxMarker = { + time: timestamp, + tooltip: `
+ 小周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}
+ 强度分数: ${fx.fx_strength}分
+ 强度等级: ${fx.fx_strength_level}
+ 是否强分型: ${fx.is_strong_fx ? '是' : '否'}
+ 价格: ${price.toFixed(4)}
+ 时间: ${fx.time} +
` + }; + + elementFxMarkers.push(elementFxMarker); + + } catch (e) { + console.error('绘制小周期KLC分型标记出错:', e); + } + }); + } + + // 处理小周期KLU分型 + if ($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) { + console.log(`绘制小周期K线未合并分型标记,共${currentData.element_klu_fx_info.length}条`); + + currentData.element_klu_fx_info.forEach(function(fx) { + try { + // 直接使用UTC时间戳(秒) + const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); + const price = parseFloat(fx.price); + + if (isNaN(timestamp) || isNaN(price)) { + console.error('小周期KLU分型时间或价格转换错误:', fx.time, fx.price); + return; + } + + // 小周期 KLU + let strengthColor = fx.is_bottom ? '#9A8C98' : '#F2CC8F'; // 底分型用灰紫色,顶分型用浅黄色 + let displayText = `${fx.fx_strength.toFixed(1)}`; + // 构建小周期分型显示文本 + if (fx.fx_strength < 2.0){ // 调整小周期阈值 + displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.6以上显示点 + } + displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", ""); + // 小周期KLU分型标记配置 + const markerConfig = { + time: timestamp, + position: fx.is_bottom ? 'belowBar' : 'aboveBar', + color: strengthColor, + // shape: 'triangle', // 使用三角形区分KLU分型 + text: displayText, + size: fx.is_strong_fx ? 0.7 : 0.5 // 小周期KLU标记更小一些 + }; + + allElementFxMarkers.push(markerConfig); + + // 创建小周期分型标记对象,包含tooltip信息 + const elementFxMarker = { + time: timestamp, + tooltip: `
+ 小周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}
+ 强度分数: ${fx.fx_strength}分
+ 强度等级: ${fx.fx_strength_level}
+ 是否强分型: ${fx.is_strong_fx ? '是' : '否'}
+ 价格: ${price.toFixed(4)}
+ 时间: ${fx.time} +
` + }; + + elementFxMarkers.push(elementFxMarker); + + } catch (e) { + console.error('绘制小周期KLU分型标记出错:', e); + } + }); + } + + // 次次周期KLC分型 + if ($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0) { + currentData.sub_sub_klc_fx_info.forEach(function(fx) { + try { + const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); + const price = parseFloat(fx.price); + if (isNaN(timestamp) || isNaN(price)) return; + const strengthColor = '#00897b'; + let displayText = (fx.fx_type || '').replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", ""); + const markerConfig = { + time: timestamp, + position: fx.is_bottom ? 'belowBar' : 'aboveBar', + color: strengthColor, + shape: 'triangle', + text: displayText, + size: (fx.is_strong_fx ? 0.6 : 0.5) + }; + allElementFxMarkers.push(markerConfig); + + // 画虚线分型框(次次周期) + if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { + const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); + const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); + const high = parseFloat(fx.high); + const low = parseFloat(fx.low); + + if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { + const boxHigh = Math.max(high, low); + const boxLow = Math.min(high, low); + const boxColor = strengthColor; + + const topSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); + + const bottomSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); + + const leftSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); + + const rightSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + + if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = []; + tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); + } + } + } catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); } + }); + } + + // 将小周期分型标记添加到全局markers中以支持tooltip功能 + if (window.fxMarkers) { + window.fxMarkers = [...window.fxMarkers, ...elementFxMarkers]; + } else { + window.fxMarkers = elementFxMarkers; + } + + // 基于后端提供的 KLC 趋势生成标记(不进行任何计算) + let klcTrendMarkers = []; + try { + if (currentData.klc_trend && currentData.klc_trend.length > 0) { + console.log('KLC趋势点数量:', currentData.klc_trend.length, currentData.klc_trend.slice(0, 3)); + // 当前图表的bar时间集合(秒)用于对齐标记到最近的K线 + const seriesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); + const nearestTime = (target) => { + if (!Array.isArray(candles) || candles.length === 0) return target; + // 简单线性查找(数据量通常可接受),必要时可替换为二分 + let best = candles[0].time; + let bestDiff = Math.abs(best - target); + for (let i = 1; i < candles.length; i++) { + const t = candles[i].time; + const d = Math.abs(t - target); + if (d < bestDiff) { best = t; bestDiff = d; } + } + return best; + }; + + klcTrendMarkers = currentData.klc_trend.map(t => { + const ts = Math.floor(new Date(t.time).getTime() / 1000); + const trendRaw = (t.trend || '').toString().toUpperCase(); + let timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts); + let marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'square', size: 0.8 }; + if (trendRaw === 'UP') { + marker = { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; + } else if (trendRaw === 'DOWN') { + marker = { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; + } else if (trendRaw === 'FLAT') { + marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; + } else { + // UNKNOWN 或其他 + marker = { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; + } + return marker; + }); + console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5)); + } + } catch (e) { + klcTrendMarkers = []; + } + // 暴露到全局以便调试或后续合并 + window.klcTrendMarkers = klcTrendMarkers; + + // 无论当前显示主/小周期,只要勾选对应Trend,就叠加出来 + let trendMarkersToUse = []; + if ($('#showMainTrend').is(':checked')) { + trendMarkersToUse = trendMarkersToUse.concat(window.klcTrendMarkers || []); + } + if ($('#showElementTrend').is(':checked') && currentData.element_klc_trend) { + const candlesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); + const nearestTime = (target) => { + if (!Array.isArray(candles) || candles.length === 0) return target; + let best = candles[0].time, bestDiff = Math.abs(best - target); + for (let i = 1; i < candles.length; i++) { + const t = candles[i].time, d = Math.abs(t - target); + if (d < bestDiff) { best = t; bestDiff = d; } + } + return best; + }; + const elementMarkers = currentData.element_klc_trend.map(t => { + const ts = Math.floor(new Date(t.time).getTime() / 1000); + const trendRaw = (t.trend || '').toString().toUpperCase(); + const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts); + if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; + if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; + if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; + return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; + }); + trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); + } + if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) { + const candlesTimesSs = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); + const nearestTimeSs = (target) => { + if (!Array.isArray(candles) || candles.length === 0) return target; + let best = candles[0].time, bestDiff = Math.abs(best - target); + for (let i = 1; i < candles.length; i++) { + const t = candles[i].time, d = Math.abs(t - target); + if (d < bestDiff) { best = t; bestDiff = d; } + } + return best; + }; + const subSubColor = '#00897b'; + const subSubMarkers = currentData.sub_sub_klc_trend.map(t => { + const ts = Math.floor(new Date(t.time).getTime() / 1000); + const trendRaw = (t.trend || '').toString().toUpperCase(); + const timeAligned = candlesTimesSs.has(ts) ? ts : nearestTimeSs(ts); + if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor, shape: 'arrowUp', size: 0.4 }; + if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor, shape: 'arrowDown', size: 0.4 }; + if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'circle', size: 0.5 }; + return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'square', size: 0.5 }; + }); + trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers); + } + + // 合并标记并设置 + const combinedMarkers = [ + ...(window.mainFxMarkers || []), + ...allElementFxMarkers, + ...(window.kluDivMarkersMain || []), + ...(window.kluDivMarkersElement || []), + ...(window.kluDivMarkersSubSub || []), + ...trendMarkersToUse, + ...(window.bspMarkers || []), + ...(window.wrMarkers || []) + ]; + if (combinedMarkers.length > 0) { + console.log( + '合并设置', combinedMarkers.length, '个标记(主周期分型:', + (window.mainFxMarkers || []).length, + '个,小周期分型:', allElementFxMarkers.length, + '个,UnitTF:', (window.unittfMarkers || []).length, + '个,BSP标记:', (window.bspMarkers || []).length, + '个,区间标记:', (window.wrMarkers || []).length, + '个)' + ); + + // 根据当前主系列类型设置标记 + const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); + let targetSeries = null; + if (klineType === 'candlestick') targetSeries = tvWidget.series.candleSeries; + else if (klineType === 'renko') targetSeries = tvWidget.series.renkoSeries; + else if (klineType === 'heikin') targetSeries = tvWidget.series.heikinSeries; + else if (klineType === 'bar') targetSeries = tvWidget.series.barSeries; + else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries; + else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries; + else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries; + else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries; + if (targetSeries) { + try { + targetSeries.setMarkers(combinedMarkers); + } catch (e) { + console.warn('设置主系列标记失败(可能series已释放):', e); + } + } else { + console.log('未找到主数据系列,无法设置标记'); + } + } + + } else { + console.log('绘制小周期分型标记 - 已禁用或无数据'); + + // 计算并缓存KLC趋势标记(即使未启用小周期分型,也应显示趋势) + try { + let klcTrendMarkers = []; + if (currentData.klc_trend && currentData.klc_trend.length > 0) { + console.log('KLC趋势点数量:', currentData.klc_trend.length, currentData.klc_trend.slice(0, 3)); + const seriesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); + const nearestTime = (target) => { + if (!Array.isArray(candles) || candles.length === 0) return target; + let best = candles[0].time; + let bestDiff = Math.abs(best - target); + for (let i = 1; i < candles.length; i++) { + const t = candles[i].time; + const d = Math.abs(t - target); + if (d < bestDiff) { best = t; bestDiff = d; } + } + return best; + }; + klcTrendMarkers = currentData.klc_trend.map(t => { + const ts = Math.floor(new Date(t.time).getTime() / 1000); + const trendRaw = (t.trend || '').toString().toUpperCase(); + const timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts); + if (trendRaw === 'UP') { + return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; + } else if (trendRaw === 'DOWN') { + return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; + } else if (trendRaw === 'FLAT') { + return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; + } else { + return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; + } + }); + console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5)); + } + window.klcTrendMarkers = klcTrendMarkers; + } catch (e) { + window.klcTrendMarkers = []; + } + + // 与上方一致:勾选哪个Trend就显示哪个 + let trendMarkersToUse = []; + if ($('#showMainTrend').is(':checked')) { + trendMarkersToUse = trendMarkersToUse.concat(window.klcTrendMarkers || []); + } + if ($('#showElementTrend').is(':checked') && currentData.element_klc_trend) { + const candlesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); + const nearestTime = (target) => { + if (!Array.isArray(candles) || candles.length === 0) return target; + let best = candles[0].time, bestDiff = Math.abs(best - target); + for (let i = 1; i < candles.length; i++) { + const t = candles[i].time, d = Math.abs(t - target); + if (d < bestDiff) { best = t; bestDiff = d; } + } + return best; + }; + const elementMarkers = currentData.element_klc_trend.map(t => { + const ts = Math.floor(new Date(t.time).getTime() / 1000); + const trendRaw = (t.trend || '').toString().toUpperCase(); + const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts); + if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; + if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; + if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; + return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; + }); + trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); + } + if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) { + const candlesTimesSs2 = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); + const nearestTimeSs2 = (target) => { + if (!Array.isArray(candles) || candles.length === 0) return target; + let best = candles[0].time, bestDiff = Math.abs(best - target); + for (let i = 1; i < candles.length; i++) { + const t = candles[i].time, d = Math.abs(t - target); + if (d < bestDiff) { best = t; bestDiff = d; } + } + return best; + }; + const subSubColor2 = '#00897b'; + const subSubMarkers2 = currentData.sub_sub_klc_trend.map(t => { + const ts = Math.floor(new Date(t.time).getTime() / 1000); + const trendRaw = (t.trend || '').toString().toUpperCase(); + const timeAligned = candlesTimesSs2.has(ts) ? ts : nearestTimeSs2(ts); + if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor2, shape: 'arrowUp', size: 0.4 }; + if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor2, shape: 'arrowDown', size: 0.4 }; + if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'circle', size: 0.5 }; + return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'square', size: 0.5 }; + }); + trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers2); + } + // 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记 + // 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记, + // 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。 + // 修复:把 BSP 标记一并合并进来。 + const onlyMainAndU = [ + ...(window.mainFxMarkers || []), + ...(window.kluDivMarkersMain || []), + ...(window.kluDivMarkersElement || []), + ...(window.kluDivMarkersSubSub || []), + ...trendMarkersToUse, + ...(window.bspMarkers || []), + ...(window.wrMarkers || []) + ]; + if (onlyMainAndU.length > 0) { + console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, ',UnitTF:', (window.unittfMarkers || []).length, ')'); + + // 根据当前主系列类型设置标记 + const klineType2 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); + let targetSeries2 = null; + if (klineType2 === 'candlestick') targetSeries2 = tvWidget.series.candleSeries; + else if (klineType2 === 'renko') targetSeries2 = tvWidget.series.renkoSeries; + else if (klineType2 === 'heikin') targetSeries2 = tvWidget.series.heikinSeries; + else if (klineType2 === 'bar') targetSeries2 = tvWidget.series.barSeries; + else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries; + else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries; + else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries; + else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries; + if (targetSeries2) { + try { + targetSeries2.setMarkers(onlyMainAndU); + } catch (e) { + console.warn('设置主系列标记失败(可能series已释放):', e); + } + } else { + console.log('未找到主数据系列,无法设置标记'); + } + } else { + console.log('没有分型标记需要显示,清空图表标记'); + // 清空图表上的所有主系列标记 + const klineType3 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); + let targetSeries3 = null; + if (klineType3 === 'candlestick') targetSeries3 = tvWidget.series.candleSeries; + else if (klineType3 === 'renko') targetSeries3 = tvWidget.series.renkoSeries; + else if (klineType3 === 'heikin') targetSeries3 = tvWidget.series.heikinSeries; + else if (klineType3 === 'bar') targetSeries3 = tvWidget.series.barSeries; + else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries; + else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries; + else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries; + else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries; + if (targetSeries3) { + try { + targetSeries3.setMarkers([]); + } catch (e) { + console.warn('清空主系列标记失败(可能series已释放):', e); + } + } + } + } +} diff --git a/web/static/js/app/chart_tv_shell.js b/web/static/js/app/chart_tv_shell.js new file mode 100644 index 0000000..8038930 --- /dev/null +++ b/web/static/js/app/chart_tv_shell.js @@ -0,0 +1,519 @@ +/* chart_tv_shell.js — containers, charts, main price series */ + +function chartTvBuildShell(ctx) { + var symbol = ctx.symbol; + var timeframe = ctx.timeframe; + + // 获取当前交易对的配置 + const symbolConfig = getSymbolConfig(symbol); + console.log('交易对配置:', symbolConfig); + + // 检查数据是否存在 + if (!currentData || !currentData.kline_data) { + console.error('数据加载失败或不存在'); + 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'; + + // 如果需要显示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; + } + })(); + ctx.symbolConfig = symbolConfig; + ctx.useSubSubPeriod = useSubSubPeriod; + ctx.useElementPeriod = useElementPeriod; + ctx.klinePeriodLabel = klinePeriodLabel; + ctx.candles = candles; + ctx.klineDataSource = klineDataSource; + ctx.container = container; + ctx.showMacd = showMacd; + ctx.showOriginalKline = showOriginalKline; + ctx.mainChartContainer = mainChartContainer; + ctx.volumeChartContainer = volumeChartContainer; + ctx.atrChartContainer = atrChartContainer; + ctx.macdChartContainer = macdChartContainer; + ctx.chanMacdChartContainer = chanMacdChartContainer; + ctx.mainChart = mainChart; + ctx.volumeChart = volumeChart; + ctx.atrChart = atrChart; + ctx.macdChart = macdChart; + ctx.chanMacdChart = chanMacdChart; + ctx.createChartOptions = createChartOptions; +} diff --git a/web/templates/index.html b/web/templates/index.html index 22ce1a8..15972bd 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1386,13 +1386,19 @@ - - - - - - - + + + + + + + + + + + + +