Merge pull request #18 from jackyu66git/dev
web端进行优化,减少内存开销,data provider提供websocket服务
This commit is contained in:
+189
-2
@@ -17,7 +17,7 @@ from typing import Dict, Iterable, List, Optional
|
|||||||
|
|
||||||
import ccxt # type: ignore
|
import ccxt # type: ignore
|
||||||
import pandas as pd # type: ignore
|
import pandas as pd # type: ignore
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
from fastapi import FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from technical.util import resample_to_interval
|
from technical.util import resample_to_interval
|
||||||
@@ -47,6 +47,7 @@ DEFAULT_LIMIT = 500
|
|||||||
RECENT_CANDLE_LIMIT = 10
|
RECENT_CANDLE_LIMIT = 10
|
||||||
RECENT_FETCH_INTERVAL = 5 # 后台刷新循环休眠秒数
|
RECENT_FETCH_INTERVAL = 5 # 后台刷新循环休眠秒数
|
||||||
PERSIST_INTERVAL = 600 # 全量落盘周期(秒)
|
PERSIST_INTERVAL = 600 # 全量落盘周期(秒)
|
||||||
|
WS_UPDATE_CANDLE_COUNT = 2 # WebSocket 增量推送最近 K 线根数
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("data_provider")
|
logger = logging.getLogger("data_provider")
|
||||||
@@ -122,6 +123,82 @@ def timeframe_to_minutes(tf: str) -> Optional[int]:
|
|||||||
return value * multiplier
|
return value * multiplier
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketManager:
|
||||||
|
"""管理 WebSocket 连接及订阅,线程安全地广播 K 线更新。"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._subscriptions: Dict[tuple, set] = {}
|
||||||
|
self._async_lock = asyncio.Lock()
|
||||||
|
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||||
|
|
||||||
|
def set_loop(self, loop: asyncio.AbstractEventLoop) -> None:
|
||||||
|
self._loop = loop
|
||||||
|
|
||||||
|
async def connect(self, ws: WebSocket) -> None:
|
||||||
|
await ws.accept()
|
||||||
|
logger.info("WebSocket 客户端已连接")
|
||||||
|
|
||||||
|
async def disconnect(self, ws: WebSocket) -> None:
|
||||||
|
async with self._async_lock:
|
||||||
|
for key in list(self._subscriptions):
|
||||||
|
self._subscriptions[key].discard(ws)
|
||||||
|
if not self._subscriptions[key]:
|
||||||
|
del self._subscriptions[key]
|
||||||
|
logger.info("WebSocket 客户端已断开")
|
||||||
|
|
||||||
|
async def subscribe(self, ws: WebSocket, symbol: str, timeframe: str) -> None:
|
||||||
|
key = (symbol, timeframe)
|
||||||
|
async with self._async_lock:
|
||||||
|
self._subscriptions.setdefault(key, set()).add(ws)
|
||||||
|
logger.info("WebSocket 订阅: %s %s", symbol, timeframe)
|
||||||
|
|
||||||
|
async def unsubscribe(self, ws: WebSocket, symbol: str, timeframe: str) -> None:
|
||||||
|
key = (symbol, timeframe)
|
||||||
|
async with self._async_lock:
|
||||||
|
if key in self._subscriptions:
|
||||||
|
self._subscriptions[key].discard(ws)
|
||||||
|
if not self._subscriptions[key]:
|
||||||
|
del self._subscriptions[key]
|
||||||
|
|
||||||
|
def has_subscribers(self, symbol: str, timeframe: str) -> bool:
|
||||||
|
"""非异步快速检查(供同步线程调用)。"""
|
||||||
|
return bool(self._subscriptions.get((symbol, timeframe)))
|
||||||
|
|
||||||
|
async def broadcast(
|
||||||
|
self, symbol: str, timeframe: str, candles: List[Dict], msg_type: str = "kline",
|
||||||
|
) -> None:
|
||||||
|
key = (symbol, timeframe)
|
||||||
|
async with self._async_lock:
|
||||||
|
subscribers = list(self._subscriptions.get(key, set()))
|
||||||
|
if not subscribers:
|
||||||
|
return
|
||||||
|
message = json.dumps(
|
||||||
|
{"type": msg_type, "symbol": symbol, "timeframe": timeframe, "data": candles},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
dead: list = []
|
||||||
|
for ws in subscribers:
|
||||||
|
try:
|
||||||
|
await ws.send_text(message)
|
||||||
|
except Exception:
|
||||||
|
dead.append(ws)
|
||||||
|
if dead:
|
||||||
|
async with self._async_lock:
|
||||||
|
for ws in dead:
|
||||||
|
self._subscriptions.get(key, set()).discard(ws)
|
||||||
|
|
||||||
|
def broadcast_from_thread(
|
||||||
|
self, symbol: str, timeframe: str, candles: List[Dict], msg_type: str = "kline",
|
||||||
|
) -> None:
|
||||||
|
"""供同步后台线程调用,将广播提交到 asyncio 事件循环。"""
|
||||||
|
if self._loop is None or self._loop.is_closed():
|
||||||
|
return
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self.broadcast(symbol, timeframe, candles, msg_type),
|
||||||
|
self._loop,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DataProvider:
|
class DataProvider:
|
||||||
"""封装交易所连接、本地 CSV、内存缓存、断线恢复与衍生周期聚合。"""
|
"""封装交易所连接、本地 CSV、内存缓存、断线恢复与衍生周期聚合。"""
|
||||||
|
|
||||||
@@ -165,6 +242,7 @@ class DataProvider:
|
|||||||
self._resume_file: Path = self.data_dir / "resume_since.json"
|
self._resume_file: Path = self.data_dir / "resume_since.json"
|
||||||
# 尝试加载历史恢复点
|
# 尝试加载历史恢复点
|
||||||
self._load_resume_since()
|
self._load_resume_since()
|
||||||
|
self._update_callbacks: List = []
|
||||||
|
|
||||||
def _load_config(self) -> Dict[str, object]:
|
def _load_config(self) -> Dict[str, object]:
|
||||||
"""读取 JSON 配置文件。"""
|
"""读取 JSON 配置文件。"""
|
||||||
@@ -458,6 +536,18 @@ class DataProvider:
|
|||||||
# 同步写盘
|
# 同步写盘
|
||||||
self._save_resume_since()
|
self._save_resume_since()
|
||||||
|
|
||||||
|
def on_update(self, callback) -> None:
|
||||||
|
"""注册数据更新回调(签名: callback(symbol, timeframe))。"""
|
||||||
|
self._update_callbacks.append(callback)
|
||||||
|
|
||||||
|
def _notify_update(self, symbol: str, timeframe: str) -> None:
|
||||||
|
"""通知所有回调:某 symbol/timeframe 数据已更新。"""
|
||||||
|
for cb in self._update_callbacks:
|
||||||
|
try:
|
||||||
|
cb(symbol, timeframe)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("数据更新回调异常: %s", exc)
|
||||||
|
|
||||||
def start_background_workers(self) -> None:
|
def start_background_workers(self) -> None:
|
||||||
"""启动增量刷新线程与周期性落盘线程。"""
|
"""启动增量刷新线程与周期性落盘线程。"""
|
||||||
if self._fetch_thread and self._fetch_thread.is_alive():
|
if self._fetch_thread and self._fetch_thread.is_alive():
|
||||||
@@ -499,6 +589,7 @@ class DataProvider:
|
|||||||
current = self.data.setdefault(symbol, {}).get(timeframe, [])
|
current = self.data.setdefault(symbol, {}).get(timeframe, [])
|
||||||
merged = self._merge_candles(timeframe, current, history)
|
merged = self._merge_candles(timeframe, current, history)
|
||||||
self.data[symbol][timeframe] = merged
|
self.data[symbol][timeframe] = merged
|
||||||
|
self._notify_update(symbol, timeframe)
|
||||||
self._clear_resume_since(symbol, timeframe)
|
self._clear_resume_since(symbol, timeframe)
|
||||||
else:
|
else:
|
||||||
# 正常增量获取最近若干根K线
|
# 正常增量获取最近若干根K线
|
||||||
@@ -513,6 +604,7 @@ class DataProvider:
|
|||||||
current = self.data.setdefault(symbol, {}).get(timeframe, [])
|
current = self.data.setdefault(symbol, {}).get(timeframe, [])
|
||||||
merged = self._merge_candles(timeframe, current, candles)
|
merged = self._merge_candles(timeframe, current, candles)
|
||||||
self.data[symbol][timeframe] = merged
|
self.data[symbol][timeframe] = merged
|
||||||
|
self._notify_update(symbol, timeframe)
|
||||||
except ccxt.BaseError as exc:
|
except ccxt.BaseError as exc:
|
||||||
logger.error("更新最新K线失败 (%s %s): %s", symbol, timeframe, exc)
|
logger.error("更新最新K线失败 (%s %s): %s", symbol, timeframe, exc)
|
||||||
# 记录应当从何时恢复拉取,避免重连后从当前时间开始导致丢K
|
# 记录应当从何时恢复拉取,避免重连后从当前时间开始导致丢K
|
||||||
@@ -664,11 +756,38 @@ class DataProvider:
|
|||||||
|
|
||||||
|
|
||||||
def create_app(provider: DataProvider) -> FastAPI:
|
def create_app(provider: DataProvider) -> FastAPI:
|
||||||
"""构造 FastAPI 应用:lifespan 内同步 initialize 并启动后台拉数。"""
|
"""构造 FastAPI 应用:lifespan 内同步 initialize 并启动后台拉数;WebSocket 实时推送。"""
|
||||||
|
ws_manager = WebSocketManager()
|
||||||
|
|
||||||
|
def _on_data_update(symbol: str, base_tf: str) -> None:
|
||||||
|
"""后台刷新线程回调:广播基础及衍生周期更新给 WebSocket 订阅者。"""
|
||||||
|
with provider._lock:
|
||||||
|
base_data = list(provider.data.get(symbol, {}).get(base_tf, []))
|
||||||
|
recent = base_data[-WS_UPDATE_CANDLE_COUNT:] if base_data else []
|
||||||
|
if recent:
|
||||||
|
ws_manager.broadcast_from_thread(symbol, base_tf, recent)
|
||||||
|
for derived_tf, src_base in provider.derived_map.items():
|
||||||
|
if src_base != base_tf or not ws_manager.has_subscribers(symbol, derived_tf):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
target_min = timeframe_to_minutes(derived_tf)
|
||||||
|
if target_min is None:
|
||||||
|
continue
|
||||||
|
now_ms = int(time.time() * 1000)
|
||||||
|
window_ms = target_min * 60_000 * (WS_UPDATE_CANDLE_COUNT + 2)
|
||||||
|
derived = provider.get_klines(
|
||||||
|
symbol, derived_tf, start_time=now_ms - window_ms, limit=WS_UPDATE_CANDLE_COUNT,
|
||||||
|
)
|
||||||
|
if derived:
|
||||||
|
ws_manager.broadcast_from_thread(symbol, derived_tf, derived)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("衍生周期广播失败 %s %s: %s", symbol, derived_tf, exc)
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
ws_manager.set_loop(loop)
|
||||||
|
provider.on_update(_on_data_update)
|
||||||
await loop.run_in_executor(None, provider.initialize)
|
await loop.run_in_executor(None, provider.initialize)
|
||||||
provider.start_background_workers()
|
provider.start_background_workers()
|
||||||
try:
|
try:
|
||||||
@@ -734,6 +853,74 @@ def create_app(provider: DataProvider) -> FastAPI:
|
|||||||
"ready": provider.is_ready(),
|
"ready": provider.is_ready(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def websocket_endpoint(ws: WebSocket):
|
||||||
|
"""WebSocket 实时 K 线推送。
|
||||||
|
|
||||||
|
客户端发送 JSON:
|
||||||
|
{"action": "subscribe", "symbol": "BTC/USDT:USDT", "timeframe": "1m"}
|
||||||
|
{"action": "unsubscribe", "symbol": "BTC/USDT:USDT", "timeframe": "1m"}
|
||||||
|
{"action": "ping"}
|
||||||
|
服务端推送:
|
||||||
|
{"type": "subscribed", "symbol": "...", "timeframe": "..."}
|
||||||
|
{"type": "snapshot", "symbol": "...", "timeframe": "...", "data": [...]}
|
||||||
|
{"type": "kline", "symbol": "...", "timeframe": "...", "data": [...]}
|
||||||
|
{"type": "pong"}
|
||||||
|
{"type": "error", "message": "..."}
|
||||||
|
"""
|
||||||
|
await ws_manager.connect(ws)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
raw = await ws.receive_text()
|
||||||
|
try:
|
||||||
|
msg = json.loads(raw)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
await ws.send_text(json.dumps({"type": "error", "message": "invalid JSON"}))
|
||||||
|
continue
|
||||||
|
|
||||||
|
action = msg.get("action", "")
|
||||||
|
symbol = str(msg.get("symbol", "")).strip()
|
||||||
|
timeframe = str(msg.get("timeframe", "")).strip()
|
||||||
|
|
||||||
|
if action == "ping":
|
||||||
|
await ws.send_text(json.dumps({"type": "pong"}))
|
||||||
|
|
||||||
|
elif action == "subscribe":
|
||||||
|
if not symbol or not timeframe:
|
||||||
|
await ws.send_text(json.dumps(
|
||||||
|
{"type": "error", "message": "需要 symbol 和 timeframe 字段"}
|
||||||
|
))
|
||||||
|
continue
|
||||||
|
await ws_manager.subscribe(ws, symbol, timeframe)
|
||||||
|
await ws.send_text(json.dumps(
|
||||||
|
{"type": "subscribed", "symbol": symbol, "timeframe": timeframe},
|
||||||
|
ensure_ascii=False,
|
||||||
|
))
|
||||||
|
try:
|
||||||
|
snapshot = provider.get_klines(symbol, timeframe, limit=DEFAULT_LIMIT)
|
||||||
|
if snapshot:
|
||||||
|
await ws.send_text(json.dumps(
|
||||||
|
{"type": "snapshot", "symbol": symbol, "timeframe": timeframe, "data": snapshot},
|
||||||
|
ensure_ascii=False,
|
||||||
|
))
|
||||||
|
except Exception as exc:
|
||||||
|
await ws.send_text(json.dumps({"type": "error", "message": str(exc)}))
|
||||||
|
|
||||||
|
elif action == "unsubscribe":
|
||||||
|
await ws_manager.unsubscribe(ws, symbol, timeframe)
|
||||||
|
await ws.send_text(json.dumps(
|
||||||
|
{"type": "unsubscribed", "symbol": symbol, "timeframe": timeframe},
|
||||||
|
ensure_ascii=False,
|
||||||
|
))
|
||||||
|
|
||||||
|
else:
|
||||||
|
await ws.send_text(json.dumps({"type": "error", "message": f"未知 action: {action}"}))
|
||||||
|
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
await ws_manager.disconnect(ws)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+108
-253
@@ -1173,9 +1173,43 @@
|
|||||||
// 不在前端截断数据,保留完整历史,避免K线数量限制
|
// 不在前端截断数据,保留完整历史,避免K线数量限制
|
||||||
function trimDataInPlace(payload, maxLen = 2000) {
|
function trimDataInPlace(payload, maxLen = 2000) {
|
||||||
if (!payload || typeof payload !== 'object') return;
|
if (!payload || typeof payload !== 'object') return;
|
||||||
// 仅清理可能保留的旧快照引用
|
|
||||||
delete payload.original_kline_data;
|
delete payload.original_kline_data;
|
||||||
delete payload.original_macd;
|
delete payload.original_macd;
|
||||||
|
|
||||||
|
// 截断大型数组,只保留最新 maxLen 条数据
|
||||||
|
const arrayKeys = [
|
||||||
|
'kline_data', 'element_kline_data', 'sub_sub_kline_data',
|
||||||
|
'bi_list', 'element_bi_list', 'sub_sub_bi_list',
|
||||||
|
'seg_list', 'element_seg_list', 'sub_sub_seg_list',
|
||||||
|
'zs_list', 'element_zs_list', 'sub_sub_zs_list',
|
||||||
|
'uncompleted_bi_list', 'uncompleted_seg_list', 'uncompleted_zs_list',
|
||||||
|
'element_uncompleted_bi_list', 'element_uncompleted_seg_list', 'element_uncompleted_zs_list',
|
||||||
|
'bsp_list', 'element_bsp_list', 'sub_sub_bsp_list',
|
||||||
|
'trade_points', 'element_trade_points'
|
||||||
|
];
|
||||||
|
for (const key of arrayKeys) {
|
||||||
|
if (Array.isArray(payload[key]) && payload[key].length > maxLen) {
|
||||||
|
payload[key] = payload[key].slice(-maxLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 截断 MACD 子数组
|
||||||
|
const macdKeys = ['macd', 'element_macd', 'sub_sub_macd'];
|
||||||
|
for (const mk of macdKeys) {
|
||||||
|
const m = payload[mk];
|
||||||
|
if (m && typeof m === 'object') {
|
||||||
|
for (const sub of ['macd', 'signal', 'histogram']) {
|
||||||
|
if (Array.isArray(m[sub]) && m[sub].length > maxLen) {
|
||||||
|
m[sub] = m[sub].slice(-maxLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 截断 ATR 数组
|
||||||
|
for (const ak of ['atr', 'element_atr', 'sub_sub_atr']) {
|
||||||
|
if (Array.isArray(payload[ak]) && payload[ak].length > maxLen) {
|
||||||
|
payload[ak] = payload[ak].slice(-maxLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let trendDetailTable = null;
|
let trendDetailTable = null;
|
||||||
let trendChart = null;
|
let trendChart = null;
|
||||||
@@ -2120,15 +2154,6 @@
|
|||||||
}
|
}
|
||||||
currentData = data;
|
currentData = data;
|
||||||
|
|
||||||
// 检查和记录服务器返回的时区
|
|
||||||
console.log('服务器返回的时区:', data.timezone || '未指定');
|
|
||||||
// BI中枢调试输出
|
|
||||||
console.log('主周期BI中枢(完成):', Array.isArray(data.bi_zs_list) ? data.bi_zs_list.length : 0);
|
|
||||||
console.log('主周期BI中枢(未完成):', Array.isArray(data.uncompleted_bi_zs_list) ? data.uncompleted_bi_zs_list.length : 0);
|
|
||||||
console.log('次周期BI中枢(完成):', Array.isArray(data.element_bi_zs_list) ? data.element_bi_zs_list.length : 0);
|
|
||||||
console.log('次周期BI中枢(未完成):', Array.isArray(data.element_uncompleted_bi_zs_list) ? data.element_uncompleted_bi_zs_list.length : 0);
|
|
||||||
|
|
||||||
// 刷新图表
|
|
||||||
refreshChart(data);
|
refreshChart(data);
|
||||||
},
|
},
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
@@ -7010,7 +7035,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, mainChart, volumeChart, atrChart, macdChart, chanMacdChart, showMacd) {
|
function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, mainChart, volumeChart, atrChart, macdChart, chanMacdChart, showMacd) {
|
||||||
// 防止同步过程中的无限循环
|
// 清理上一轮绑定的事件监听器,防止累积
|
||||||
|
if (window._bindSyncCleanups) {
|
||||||
|
window._bindSyncCleanups.forEach(fn => { try { fn(); } catch(e) {} });
|
||||||
|
}
|
||||||
|
window._bindSyncCleanups = [];
|
||||||
|
|
||||||
let syncInProgress = false;
|
let syncInProgress = false;
|
||||||
|
|
||||||
// 用于跟踪所有图表的拖动状态 - 在函数内部定义以确保作用域正确
|
// 用于跟踪所有图表的拖动状态 - 在函数内部定义以确保作用域正确
|
||||||
@@ -7024,161 +7054,70 @@
|
|||||||
|
|
||||||
// 同步图表的时间范围
|
// 同步图表的时间范围
|
||||||
function syncCharts(sourceChart, sourceContainer) {
|
function syncCharts(sourceChart, sourceContainer) {
|
||||||
// 防止无限循环 - 使用更精确的检查
|
if (syncInProgress) return;
|
||||||
if (syncInProgress) {
|
|
||||||
console.log('🔄 同步正在进行中,跳过此次同步');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncInProgress = true;
|
syncInProgress = true;
|
||||||
console.log('🚀 开始同步图表,来源:',
|
|
||||||
sourceChart === mainChart ? '主图' :
|
|
||||||
sourceChart === volumeChart ? '成交量图' :
|
|
||||||
sourceChart === atrChart ? 'ATR图' : 'MACD图');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (sourceChart && sourceChart.timeScale) {
|
if (sourceChart && sourceChart.timeScale) {
|
||||||
const logicalRange = sourceChart.timeScale().getVisibleLogicalRange();
|
const logicalRange = sourceChart.timeScale().getVisibleLogicalRange();
|
||||||
|
|
||||||
if (logicalRange && logicalRange.from !== undefined && logicalRange.to !== undefined) {
|
if (logicalRange && logicalRange.from !== undefined && logicalRange.to !== undefined) {
|
||||||
console.log('📊 同步时间范围:', logicalRange);
|
|
||||||
|
|
||||||
// 同步主图
|
|
||||||
if (sourceChart !== mainChart && mainChart && mainChart.timeScale) {
|
if (sourceChart !== mainChart && mainChart && mainChart.timeScale) {
|
||||||
try {
|
try { mainChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
|
||||||
mainChart.timeScale().setVisibleLogicalRange(logicalRange);
|
|
||||||
console.log('✅ 主图同步完成');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('❌ 主图同步失败:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步成交量图
|
|
||||||
if (sourceChart !== volumeChart && volumeChart && volumeChart.timeScale) {
|
if (sourceChart !== volumeChart && volumeChart && volumeChart.timeScale) {
|
||||||
try {
|
try { volumeChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
|
||||||
volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
|
|
||||||
console.log('✅ 成交量图同步完成');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('❌ 成交量图同步失败:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步ATR图
|
|
||||||
if (sourceChart !== atrChart && atrChart && atrChart.timeScale) {
|
if (sourceChart !== atrChart && atrChart && atrChart.timeScale) {
|
||||||
try {
|
try { atrChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
|
||||||
atrChart.timeScale().setVisibleLogicalRange(logicalRange);
|
|
||||||
console.log('✅ ATR图同步完成');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('❌ ATR图同步失败:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步MACD图
|
|
||||||
if (showMacd && macdChart && sourceChart !== macdChart && macdChart.timeScale) {
|
if (showMacd && macdChart && sourceChart !== macdChart && macdChart.timeScale) {
|
||||||
try {
|
try { macdChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
|
||||||
macdChart.timeScale().setVisibleLogicalRange(logicalRange);
|
|
||||||
console.log('✅ MACD图同步完成');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('❌ MACD图同步失败:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步ChanMACD图
|
|
||||||
if (showMacd && chanMacdChart && sourceChart !== chanMacdChart && chanMacdChart.timeScale) {
|
if (showMacd && chanMacdChart && sourceChart !== chanMacdChart && chanMacdChart.timeScale) {
|
||||||
try {
|
try { chanMacdChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
|
||||||
chanMacdChart.timeScale().setVisibleLogicalRange(logicalRange);
|
|
||||||
console.log('✅ ChanMACD图同步完成');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('❌ ChanMACD图同步失败:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存当前的可见范围到全局状态
|
|
||||||
if (tvWidget && tvWidget.state) {
|
if (tvWidget && tvWidget.state) {
|
||||||
tvWidget.state.logicalRange = logicalRange;
|
tvWidget.state.logicalRange = logicalRange;
|
||||||
// 同时保存可见范围以确保精确对齐
|
try { tvWidget.state.visibleRange = sourceChart.timeScale().getVisibleRange(); } catch (e) {}
|
||||||
try {
|
|
||||||
const visibleRange = sourceChart.timeScale().getVisibleRange();
|
|
||||||
tvWidget.state.visibleRange = visibleRange;
|
|
||||||
console.log('💾 保存状态 - 逻辑范围:', logicalRange, '可见范围:', visibleRange);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('⚠️ 保存可见范围失败:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn('⚠️ 无效的逻辑范围:', logicalRange);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn('⚠️ 无效的源图表或时间刻度');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('💥 同步图表出错:', e);
|
console.error('同步图表出错:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 立即重置同步标志,提高响应速度
|
setTimeout(() => { syncInProgress = false; }, 1);
|
||||||
setTimeout(() => {
|
|
||||||
syncInProgress = false;
|
|
||||||
console.log('🔓 同步标志已重置');
|
|
||||||
}, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为每个图表添加事件监听
|
// 为每个图表添加事件监听
|
||||||
const addChartSyncEvents = (chartContainer, chart) => {
|
const addChartSyncEvents = (chartContainer, chart) => {
|
||||||
console.log('为图表添加同步事件监听:',
|
|
||||||
chart === mainChart ? '主图' :
|
|
||||||
chart === volumeChart ? '成交量图' :
|
|
||||||
chart === atrChart ? 'ATR图' :
|
|
||||||
chart === macdChart ? 'MACD图' :
|
|
||||||
chart === chanMacdChart ? 'ChanMACD图' : '未知图表');
|
|
||||||
|
|
||||||
// 确定当前图表类型
|
|
||||||
const chartType = chart === mainChart ? 'main' :
|
const chartType = chart === mainChart ? 'main' :
|
||||||
chart === volumeChart ? 'volume' :
|
chart === volumeChart ? 'volume' :
|
||||||
chart === atrChart ? 'atr' :
|
chart === atrChart ? 'atr' :
|
||||||
chart === macdChart ? 'macd' :
|
chart === macdChart ? 'macd' :
|
||||||
chart === chanMacdChart ? 'chanmacd' : 'unknown';
|
chart === chanMacdChart ? 'chanmacd' : 'unknown';
|
||||||
|
|
||||||
// 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法)
|
const timeRangeHandler = () => {
|
||||||
chart.timeScale().subscribeVisibleTimeRangeChange(() => {
|
|
||||||
// 使用图表特定的同步标志防止递归
|
|
||||||
if (!syncInProgress) {
|
if (!syncInProgress) {
|
||||||
console.log('✅ 检测到时间范围变化,触发同步:', chartType, '当前范围:', chart.timeScale().getVisibleLogicalRange());
|
|
||||||
syncCharts(chart, chartContainer);
|
syncCharts(chart, chartContainer);
|
||||||
} else {
|
|
||||||
console.log('⏸️ 同步进行中,跳过时间范围变化事件:', chartType);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
chart.timeScale().subscribeVisibleTimeRangeChange(timeRangeHandler);
|
||||||
|
window._bindSyncCleanups.push(() => {
|
||||||
|
try { chart.timeScale().unsubscribeVisibleTimeRangeChange(timeRangeHandler); } catch(e) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 备用的DOM事件监听(用于调试和额外保障)
|
|
||||||
let isScrolling = false;
|
let isScrolling = false;
|
||||||
|
|
||||||
// 鼠标按下事件
|
const mousedownHandler = () => { localDragStates[chartType] = true; };
|
||||||
chartContainer.addEventListener('mousedown', (e) => {
|
const mouseupHandler = () => { localDragStates[chartType] = false; };
|
||||||
localDragStates[chartType] = true;
|
const mouseleaveHandler = () => { localDragStates[chartType] = false; };
|
||||||
console.log('鼠标按下开始拖动:', chartType);
|
const wheelHandler = () => {
|
||||||
});
|
|
||||||
|
|
||||||
// 鼠标抬起事件
|
|
||||||
chartContainer.addEventListener('mouseup', (e) => {
|
|
||||||
if (localDragStates[chartType]) {
|
|
||||||
localDragStates[chartType] = false;
|
|
||||||
console.log('鼠标抬起,结束拖动:', chartType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 鼠标离开事件
|
|
||||||
chartContainer.addEventListener('mouseleave', (e) => {
|
|
||||||
if (localDragStates[chartType]) {
|
|
||||||
localDragStates[chartType] = false;
|
|
||||||
console.log('鼠标离开容器,结束拖动:', chartType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 滚轮缩放事件(保持原有逻辑)
|
|
||||||
chartContainer.addEventListener('wheel', (e) => {
|
|
||||||
if (!isScrolling) {
|
if (!isScrolling) {
|
||||||
isScrolling = true;
|
isScrolling = true;
|
||||||
console.log('滚轮缩放:', chartType);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!syncInProgress) {
|
if (!syncInProgress) {
|
||||||
syncCharts(chart, chartContainer);
|
syncCharts(chart, chartContainer);
|
||||||
@@ -7186,6 +7125,17 @@
|
|||||||
isScrolling = false;
|
isScrolling = false;
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chartContainer.addEventListener('mousedown', mousedownHandler);
|
||||||
|
chartContainer.addEventListener('mouseup', mouseupHandler);
|
||||||
|
chartContainer.addEventListener('mouseleave', mouseleaveHandler);
|
||||||
|
chartContainer.addEventListener('wheel', wheelHandler);
|
||||||
|
window._bindSyncCleanups.push(() => {
|
||||||
|
chartContainer.removeEventListener('mousedown', mousedownHandler);
|
||||||
|
chartContainer.removeEventListener('mouseup', mouseupHandler);
|
||||||
|
chartContainer.removeEventListener('mouseleave', mouseleaveHandler);
|
||||||
|
chartContainer.removeEventListener('wheel', wheelHandler);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -7206,58 +7156,35 @@
|
|||||||
addChartSyncEvents(chanMacdChartContainer, chanMacdChart);
|
addChartSyncEvents(chanMacdChartContainer, chanMacdChart);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 窗口大小变化时重绘图表
|
// 窗口大小变化时重绘图表 — 使用可清理的方式注册
|
||||||
window.addEventListener('resize', () => {
|
const resizeHandler = () => {
|
||||||
// 调整主图大小
|
|
||||||
if (mainChart && mainChartContainer) {
|
if (mainChart && mainChartContainer) {
|
||||||
mainChart.applyOptions({
|
mainChart.applyOptions({ width: mainChartContainer.clientWidth, height: mainChartContainer.clientHeight });
|
||||||
width: mainChartContainer.clientWidth,
|
|
||||||
height: mainChartContainer.clientHeight
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调整成交量图大小
|
|
||||||
if (volumeChart && volumeChartContainer) {
|
if (volumeChart && volumeChartContainer) {
|
||||||
volumeChart.applyOptions({
|
volumeChart.applyOptions({ width: volumeChartContainer.clientWidth, height: volumeChartContainer.clientHeight });
|
||||||
width: volumeChartContainer.clientWidth,
|
|
||||||
height: volumeChartContainer.clientHeight
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调整ATR图大小
|
|
||||||
if (atrChart && atrChartContainer) {
|
if (atrChart && atrChartContainer) {
|
||||||
atrChart.applyOptions({
|
atrChart.applyOptions({ width: atrChartContainer.clientWidth, height: atrChartContainer.clientHeight });
|
||||||
width: atrChartContainer.clientWidth,
|
|
||||||
height: atrChartContainer.clientHeight
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调整MACD图大小
|
|
||||||
if (showMacd && macdChart && macdChartContainer) {
|
if (showMacd && macdChart && macdChartContainer) {
|
||||||
macdChart.applyOptions({
|
macdChart.applyOptions({ width: macdChartContainer.clientWidth, height: macdChartContainer.clientHeight });
|
||||||
width: macdChartContainer.clientWidth,
|
|
||||||
height: macdChartContainer.clientHeight
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调整ChanMACD图大小
|
|
||||||
if (showMacd && chanMacdChart && chanMacdChartContainer) {
|
if (showMacd && chanMacdChart && chanMacdChartContainer) {
|
||||||
chanMacdChart.applyOptions({
|
chanMacdChart.applyOptions({ width: chanMacdChartContainer.clientWidth, height: chanMacdChartContainer.clientHeight });
|
||||||
width: chanMacdChartContainer.clientWidth,
|
|
||||||
height: chanMacdChartContainer.clientHeight
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
setTimeout(() => { if (mainChart) syncCharts(mainChart, mainChartContainer); }, 200);
|
||||||
// 重新同步 - 使用主图作为同步源
|
};
|
||||||
setTimeout(() => {
|
window.addEventListener('resize', resizeHandler);
|
||||||
if (mainChart) {
|
window._bindSyncCleanups.push(() => { window.removeEventListener('resize', resizeHandler); });
|
||||||
syncCharts(mainChart, mainChartContainer);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd) {
|
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd) {
|
||||||
// 调试变量
|
// 清理上一轮 tooltip 的事件订阅
|
||||||
|
if (window._tooltipCleanups) {
|
||||||
|
window._tooltipCleanups.forEach(fn => { try { fn(); } catch(e) {} });
|
||||||
|
}
|
||||||
|
window._tooltipCleanups = [];
|
||||||
|
|
||||||
window.debugMode = true;
|
window.debugMode = true;
|
||||||
// 初始化 U 显示状态(主/次周期分开控制)
|
// 初始化 U 显示状态(主/次周期分开控制)
|
||||||
const isShowUMain = $('#toggleUOnMain').is(':checked');
|
const isShowUMain = $('#toggleUOnMain').is(':checked');
|
||||||
@@ -7295,7 +7222,7 @@
|
|||||||
|
|
||||||
// 添加鼠标悬停事件显示提示
|
// 添加鼠标悬停事件显示提示
|
||||||
if (mainChart) {
|
if (mainChart) {
|
||||||
mainChart.subscribeCrosshairMove(param => {
|
const crosshairHandler = (param) => {
|
||||||
// 十字线同步到其他图表 - 通过DOM元素绘制垂直线实现虚线延长效果
|
// 十字线同步到其他图表 - 通过DOM元素绘制垂直线实现虚线延长效果
|
||||||
if (param.time && param.point && volumeChart) {
|
if (param.time && param.point && volumeChart) {
|
||||||
try {
|
try {
|
||||||
@@ -7378,13 +7305,6 @@
|
|||||||
const chanMacdTimeCoordinate = chanMacdChart.timeScale().timeToCoordinate(param.time);
|
const chanMacdTimeCoordinate = chanMacdChart.timeScale().timeToCoordinate(param.time);
|
||||||
if (chanMacdTimeCoordinate !== null) {
|
if (chanMacdTimeCoordinate !== null) {
|
||||||
const chanMacdChartRect = chanMacdChartContainer.getBoundingClientRect();
|
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');
|
const chanMacdLine = document.createElement('div');
|
||||||
chanMacdLine.className = 'chanmacd-crosshair-line';
|
chanMacdLine.className = 'chanmacd-crosshair-line';
|
||||||
chanMacdLine.style.position = 'fixed';
|
chanMacdLine.style.position = 'fixed';
|
||||||
@@ -7397,16 +7317,7 @@
|
|||||||
chanMacdLine.style.pointerEvents = 'none';
|
chanMacdLine.style.pointerEvents = 'none';
|
||||||
chanMacdLine.style.zIndex = '1000';
|
chanMacdLine.style.zIndex = '1000';
|
||||||
document.body.appendChild(chanMacdLine);
|
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) {
|
} catch (e) {
|
||||||
@@ -7479,9 +7390,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅记录最简短的调试信息
|
|
||||||
console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`);
|
|
||||||
|
|
||||||
// 显示自定义时区工具提示,包含价格信息
|
// 显示自定义时区工具提示,包含价格信息
|
||||||
crosshairTooltip.innerHTML = `<div style="font-weight:bold">时间: ${formattedTime}</div>` +
|
crosshairTooltip.innerHTML = `<div style="font-weight:bold">时间: ${formattedTime}</div>` +
|
||||||
(priceInfo ? `<div>${priceInfo}</div>` : '');
|
(priceInfo ? `<div>${priceInfo}</div>` : '');
|
||||||
@@ -7506,12 +7414,20 @@
|
|||||||
tooltipElement.style.display = 'none';
|
tooltipElement.style.display = 'none';
|
||||||
crosshairTooltip.style.display = 'none';
|
crosshairTooltip.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
mainChart.subscribeCrosshairMove(crosshairHandler);
|
||||||
|
window._tooltipCleanups.push(() => {
|
||||||
|
try { mainChart.unsubscribeCrosshairMove(crosshairHandler); } catch(e) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理图表缩放、平移等事件,隐藏提示
|
// 处理图表缩放、平移等事件,隐藏提示
|
||||||
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
|
const hideTooltipHandler = () => {
|
||||||
tooltipElement.style.display = 'none';
|
tooltipElement.style.display = 'none';
|
||||||
crosshairTooltip.style.display = 'none';
|
crosshairTooltip.style.display = 'none';
|
||||||
|
};
|
||||||
|
mainChart.timeScale().subscribeVisibleTimeRangeChange(hideTooltipHandler);
|
||||||
|
window._tooltipCleanups.push(() => {
|
||||||
|
try { mainChart.timeScale().unsubscribeVisibleTimeRangeChange(hideTooltipHandler); } catch(e) {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8259,60 +8175,27 @@
|
|||||||
|
|
||||||
// 只重绘分形元素(笔、线段、中枢),保留现有的K线、MACD和成交量
|
// 只重绘分形元素(笔、线段、中枢),保留现有的K线、MACD和成交量
|
||||||
function redrawFractalElements() {
|
function redrawFractalElements() {
|
||||||
// 检查图表是否已初始化
|
if (!tvWidget || !tvWidget.mainChart) return;
|
||||||
if (!tvWidget || !tvWidget.mainChart) {
|
|
||||||
console.error('图表未初始化,无法重绘分形元素');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('重绘分形元素 - 开始');
|
|
||||||
console.log('- 显示笔:', $('#showMainBi').is(':checked'));
|
|
||||||
console.log('- 显示线段:', $('#showMainSeg').is(':checked'));
|
|
||||||
console.log('- 显示中枢:', $('#showMainZs').is(':checked'));
|
|
||||||
|
|
||||||
// 保存当前的图表可见范围
|
|
||||||
const mainChart = tvWidget.mainChart;
|
const mainChart = tvWidget.mainChart;
|
||||||
const visibleRange = mainChart.timeScale().getVisibleRange();
|
|
||||||
const logicalRange = mainChart.timeScale().getVisibleLogicalRange();
|
const logicalRange = mainChart.timeScale().getVisibleLogicalRange();
|
||||||
|
const visibleRange = mainChart.timeScale().getVisibleRange();
|
||||||
|
|
||||||
// 获取当前图表设置
|
// 确保使用主周期的K线和MACD数据
|
||||||
const showMacd = $('#showMacd').is(':checked');
|
if (currentData.original_kline_data) {
|
||||||
const showOriginalKline = $('#showOriginalKline').is(':checked');
|
|
||||||
const showBi = $('#showMainBi').is(':checked');
|
|
||||||
const showSeg = $('#showMainSeg').is(':checked');
|
|
||||||
const showZs = $('#showMainZs').is(':checked');
|
|
||||||
|
|
||||||
// 保存原始K线和MACD数据,确保即使使用小级别,我们依然使用主周期的K线和MACD数据
|
|
||||||
const originalKlineData = currentData.kline_data;
|
|
||||||
const originalMacdData = currentData.macd;
|
|
||||||
|
|
||||||
// 重新初始化图表 - 这将清除所有系列并重新创建
|
|
||||||
console.log('重新初始化图表...');
|
|
||||||
|
|
||||||
// 临时存储currentData中可能被修改的字段
|
|
||||||
const tempCurrentData = {
|
|
||||||
kline_data: originalKlineData,
|
|
||||||
macd: originalMacdData
|
|
||||||
};
|
|
||||||
|
|
||||||
// 在重绘前确保K线和MACD数据不变
|
|
||||||
if (currentData.original_kline_data && currentData.original_macd) {
|
|
||||||
// 如果已经保存了原始数据,恢复它们
|
|
||||||
currentData.kline_data = currentData.original_kline_data;
|
currentData.kline_data = currentData.original_kline_data;
|
||||||
currentData.macd = currentData.original_macd;
|
|
||||||
} else {
|
|
||||||
// 首次运行 - 保存原始数据
|
|
||||||
currentData.original_kline_data = originalKlineData;
|
|
||||||
currentData.original_macd = originalMacdData;
|
|
||||||
}
|
}
|
||||||
|
if (currentData.original_macd) {
|
||||||
|
currentData.macd = currentData.original_macd;
|
||||||
|
}
|
||||||
|
// 清除冗余引用,帮助GC回收
|
||||||
|
delete currentData.original_kline_data;
|
||||||
|
delete currentData.original_macd;
|
||||||
|
|
||||||
// 调用初始化函数
|
|
||||||
initTradingView($('#symbol').val(), $('#timeframe').val());
|
initTradingView($('#symbol').val(), $('#timeframe').val());
|
||||||
|
|
||||||
// 恢复原始可见范围
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (tvWidget && tvWidget.mainChart) {
|
if (tvWidget && tvWidget.mainChart) {
|
||||||
console.log('恢复图表可见范围...');
|
|
||||||
if (logicalRange) {
|
if (logicalRange) {
|
||||||
tvWidget.mainChart.timeScale().setVisibleLogicalRange(logicalRange);
|
tvWidget.mainChart.timeScale().setVisibleLogicalRange(logicalRange);
|
||||||
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
|
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
|
||||||
@@ -8326,14 +8209,8 @@
|
|||||||
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(visibleRange);
|
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(visibleRange);
|
||||||
if (tvWidget.chanMacdChart) tvWidget.chanMacdChart.timeScale().setVisibleRange(visibleRange);
|
if (tvWidget.chanMacdChart) tvWidget.chanMacdChart.timeScale().setVisibleRange(visibleRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('图表可见范围已恢复(包括ATR图表)');
|
|
||||||
} else {
|
|
||||||
console.error('恢复图表可见范围失败 - 图表未初始化');
|
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
console.log('重绘分形元素 - 完成');
|
|
||||||
}
|
}
|
||||||
// 只更新分形元素(笔、线段、中枢)的表格数据
|
// 只更新分形元素(笔、线段、中枢)的表格数据
|
||||||
function updateFractalTables() {
|
function updateFractalTables() {
|
||||||
@@ -8424,33 +8301,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('API返回数据:', data);
|
|
||||||
console.log('K线数据点数:', data.kline_data ? data.kline_data.length : 0);
|
|
||||||
|
|
||||||
// 检查是否包含小周期数据
|
|
||||||
if (data.element_timeframe) {
|
if (data.element_timeframe) {
|
||||||
console.log('小周期笔数据点数:', data.element_bi_list ? data.element_bi_list.length : 0);
|
|
||||||
console.log('小周期线段数据点数:', data.element_seg_list ? data.element_seg_list.length : 0);
|
|
||||||
console.log('小周期中枢数据点数:', data.element_zs_list ? data.element_zs_list.length : 0);
|
|
||||||
|
|
||||||
// 更新小周期选择器
|
|
||||||
$('#elementTimeframe').val(data.element_timeframe);
|
$('#elementTimeframe').val(data.element_timeframe);
|
||||||
} else {
|
|
||||||
console.log('未提供小周期数据,使用主周期数据');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存原始K线和MACD数据,用于后续参考
|
initTradingView($('#symbol').val(), $('#timeframe').val());
|
||||||
data.original_kline_data = data.kline_data;
|
|
||||||
data.original_macd = data.macd;
|
|
||||||
|
|
||||||
// 如果图表未初始化,则创建图表,否则更新图表数据
|
|
||||||
if (!tvWidget.state.isInitialized) {
|
|
||||||
console.log('图表尚未初始化,创建新图表');
|
|
||||||
initTradingView($('#symbol').val(), $('#timeframe').val());
|
|
||||||
} else {
|
|
||||||
console.log('图表已初始化,进行增量更新');
|
|
||||||
updateTradingViewData();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新表格数据
|
// 更新表格数据
|
||||||
updateTables(data);
|
updateTables(data);
|
||||||
|
|||||||
Reference in New Issue
Block a user