Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0fa9ac375 | ||
|
|
6c627f009a | ||
|
|
dbb6202325 |
@@ -23,6 +23,7 @@
|
|||||||
- ECR-003 Reviewed:主站威科夫叠层(`chanlun/analysis/wyckoff/` + `include_wyckoff`)→ `081a57a`
|
- ECR-003 Reviewed:主站威科夫叠层(`chanlun/analysis/wyckoff/` + `include_wyckoff`)→ `081a57a`
|
||||||
- ECR-004 Reviewed:TR 评分硬化 + VP 少系列 + 阶段/门闩/单测(无币种参数)
|
- ECR-004 Reviewed:TR 评分硬化 + VP 少系列 + 阶段/门闩/单测(无币种参数)
|
||||||
- ECR-007 Final Approval / `276481e`:Wyckoff Live Structure(`live.py`);Confirmed ≠ Live;execution 仅 confirmed
|
- 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 开关仅显隐叠层
|
- 威科夫数据随主 analyze 默认返回;UI 开关仅显隐叠层
|
||||||
- Live 观察:主图左下角 Cycle Summary(「形成中」= FORMING);无单独 Live 图层
|
- Live 观察:主图左下角 Cycle Summary(「形成中」= FORMING);无单独 Live 图层
|
||||||
|
|
||||||
@@ -35,9 +36,9 @@
|
|||||||
|
|
||||||
## 已知债务
|
## 已知债务
|
||||||
|
|
||||||
- `chart_tv.js` 单体巨大 → 后续可选 ECR
|
|
||||||
- analyze 契约已加深(mock HTTP + wyckoff opt-in);可再加固定 JSON 快照文件
|
- analyze 契约已加深(mock HTTP + wyckoff opt-in);可再加固定 JSON 快照文件
|
||||||
- 内存泄漏尚无自动化 heap/监听断言
|
- 内存泄漏尚无自动化 heap/监听断言
|
||||||
- `macd_config` POST 写本地 global 的历史 quirks(未改)
|
- `macd_config` POST 写本地 global 的历史 quirks(未改)
|
||||||
- 威科夫启发式参数未做 UI 调参
|
- 威科夫启发式参数未做 UI 调参
|
||||||
- ECR-007 待 PR 合入 `dev`
|
- ECR-007 待 Human 在 Gitea 开 PR 合入 `dev`
|
||||||
|
- `chart_tv_overlays.js` 仍偏大,可后续再拆
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
## Unreleased — 2026-08-07
|
## 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)
|
### ECR-007(L2,LOOP-RUN-005)
|
||||||
|
|
||||||
- Wyckoff **Live Structure**:`live.py` + engine 组装 `lifecycle` / `confirmed` / `live`
|
- Wyckoff **Live Structure**:`live.py` + engine 组装 `lifecycle` / `confirmed` / `live`
|
||||||
|
|||||||
@@ -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**
|
||||||
@@ -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
|
||||||
@@ -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` 大改。
|
||||||
@@ -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。
|
||||||
@@ -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
|
||||||
@@ -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。
|
||||||
@@ -34,7 +34,7 @@ Trading System(缠论分析引擎 + 可视化 Web;Freqtrade 策略目录独
|
|||||||
|
|
||||||
## Active anchors
|
## 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
|
- EXP: N/A
|
||||||
- TRACEABILITY: `docs/TRACEABILITY.md`
|
- TRACEABILITY: `docs/TRACEABILITY.md`
|
||||||
- Memory: `docs/AGENT_MEMORY.md`
|
- Memory: `docs/AGENT_MEMORY.md`
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# STATE
|
# STATE
|
||||||
|
|
||||||
**owner:** idle
|
**owner:** idle
|
||||||
**active_ecr:** none(ECR-007 Final Approval;待合入 `dev`)
|
**active_ecr:** none(ECR-008 Reviewed;ECR-007 待合入 `dev`)
|
||||||
**phase:** post-approval
|
**phase:** post-review
|
||||||
**system_version:** v1.0.0
|
**system_version:** v1.0.0
|
||||||
**strategy_version:** unchanged
|
**strategy_version:** unchanged
|
||||||
**updated:** 2026-08-07
|
**updated:** 2026-08-07
|
||||||
@@ -16,11 +16,12 @@
|
|||||||
| ECR-002 | L3 | Done (Reviewed) | runtime 包拆分 |
|
| ECR-002 | L3 | Done (Reviewed) | runtime 包拆分 |
|
||||||
| ECR-003 | L2 | Done (Reviewed) | `081a57a` 主站威科夫 |
|
| ECR-003 | L2 | Done (Reviewed) | `081a57a` 主站威科夫 |
|
||||||
| ECR-004 | L2 | Done (Reviewed) | 威科夫硬化 / VP 减负 |
|
| 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
|
## 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/`
|
- 归档:`docs/runs/LOOP-RUN-005/`
|
||||||
- 未请求新 system tag
|
- 未请求新 system tag
|
||||||
- 分支 `feature/ECR-007-wyckoff-live-structure` 待 PR → `dev`
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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 未改。
|
||||||
@@ -53,3 +53,11 @@
|
|||||||
| ECR-007 | execution 仅 confirmed | BD-2026-007 | `execution_signal_from_wyckoff` | live-only → None | 276481e |
|
| 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 | Summary Confirmed/Live 分区 | PRODUCT | `ui.js` | 人工 + 契约键 | 276481e |
|
||||||
| ECR-007 | LOOP-RUN-005 | — | `docs/runs/LOOP-RUN-005/` | Gate + Artifact | 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` | dbb6202 |
|
||||||
|
| ECR-008 | 对外 API 不变 | ENG-008 | `initTradingView` / `disposeTradingViewCharts` | ui.js 调用点 | dbb6202 |
|
||||||
|
| ECR-008 | 无打包器 | PROFILE | `index.html` script 顺序 | 人工 | dbb6202 |
|
||||||
|
|||||||
+11
-4679
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
@@ -1386,13 +1386,19 @@
|
|||||||
<script defer src="{{ url_for('static', filename='js/app/api_client.js') }}"></script>
|
<script defer src="{{ url_for('static', filename='js/app/api_client.js') }}"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/state.js') }}"></script>
|
<script defer src="{{ url_for('static', filename='js/app/state.js') }}"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/trend.js') }}"></script>
|
<script defer src="{{ url_for('static', filename='js/app/trend.js') }}"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/macd_ui.js') }}?v=20260807e"></script>
|
<script defer src="{{ url_for('static', filename='js/app/macd_ui.js') }}?v=20260807f"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_format.js') }}?v=20260807e"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_format.js') }}?v=20260807f"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_view.js') }}?v=20260807e"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_view.js') }}?v=20260807f"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260807e"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_lifecycle.js') }}?v=20260807f"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260807e"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_shell.js') }}?v=20260807f"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_tables.js') }}?v=20260807e"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_indicators.js') }}?v=20260807f"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260807e"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_chan.js') }}?v=20260807f"></script>
|
||||||
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_overlays.js') }}?v=20260807f"></script>
|
||||||
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_finalize.js') }}?v=20260807f"></script>
|
||||||
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260807f"></script>
|
||||||
|
<script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260807f"></script>
|
||||||
|
<script defer src="{{ url_for('static', filename='js/app/chart_tables.js') }}?v=20260807f"></script>
|
||||||
|
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260807f"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/overlays.js') }}"></script>
|
<script defer src="{{ url_for('static', filename='js/app/overlays.js') }}"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/main.js') }}"></script>
|
<script defer src="{{ url_for('static', filename='js/app/main.js') }}"></script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user