diff --git a/research/PROMOTE_coinglass_relay.md b/research/PROMOTE_coinglass_relay.md new file mode 100644 index 0000000..cec297a --- /dev/null +++ b/research/PROMOTE_coinglass_relay.md @@ -0,0 +1,7 @@ +# 已 superseded + +v0 改走免费交易所,由 **data_provider** 拉,chan 只从中转取。 + +见 [PROMOTE_deriv_relay.md](./PROMOTE_deriv_relay.md)。 + +CoinGlass 付费档等看完交易所效果再开,不要和本阶段混做。 diff --git a/research/PROMOTE_deriv_relay.md b/research/PROMOTE_deriv_relay.md new file mode 100644 index 0000000..3adc738 --- /dev/null +++ b/research/PROMOTE_deriv_relay.md @@ -0,0 +1,94 @@ +# 需求:资金面数据(data_provider → chan) + +**提出方:** chan +**执行方:** data_provider +**阶段:** Paper。先看效果。不进 Live。 + +--- + +## 要什么 + +chan 要在缠论图上叠资金面,和现有 K 线对得上。 + +data_provider 对外提供资金面;chan **只从 data_provider 取**,不访问交易所,不访问 CoinGlass。 + +K 线路径不动(现有 `/api/candles` 与 K 线推送)。本次只加资金面。 + +--- + +## 数据 + +先三个币:**BTC、ETH、SOL**(USDT 永续,符号与现有 K 线相同,如 `BTC/USDT:USDT`)。 + +要两样: + +1. **持仓量(OI)** + - 要历史,能覆盖缠论常用周期:`15m`、`30m`、`4h`、`1d`(有 `1h`/`2h` 更好)。 + - 要当前最新值。 + - 历史长度至少约 30 天。 + +2. **资金费率(funding)** + - 要当前值。 + - 要历史结算序列。 + - 对齐到各周期 K 线:结算点落到所在那根;非结算 bar 沿用上一次结算值,不要插值编造。 + +来源:交易所公开数据即可,本阶段不买 CoinGlass。OI 历史哪家所没有,用另一家所公开数据补,需标明来源。 + +**本阶段不要:** 清算、热力图、多空比、订单簿、CoinGlass。 + +--- + +## 给 chan 的接口 + +与 `/api/candles` 同一套约定: + +- `symbol` 与蜡烛相同 +- 时间戳毫秒 UTC +- 按周期 `tf` 取序列 +- 支持 `start` / `end` / `limit`(默认 `limit=500`) + +示例: + +```http +GET /api/deriv?symbol=BTC/USDT:USDT&tf=15m&metrics=oi,funding +``` + +每根: + +| 字段 | 要求 | +|---|---| +| `timestamp` | 与同 `tf` 的 `/api/candles` **开盘时间**对齐;对不齐的不要 | +| `oi` | 该 bar 持仓量;缺则 `null` | +| `oi_src` | 该值来自哪家所 | +| `funding` | 该 bar 资金费率;缺则 `null` | +| `funding_src` | 该值来自哪家所 | + +健康状态要能看出:资金面是否可用、各所是否通、上次成功时间。 + +--- + +## 约束 + +- 全程 HTTPS REST。本阶段不要求资金面 WebSocket。 +- chan、浏览器不得直连交易所。 +- 现有 K 线接口行为不变。 +- 一家所挂了:缺那家字段,另一家仍要能出;两边都没有且无可用数据时明确失败。 +- 上游限流或超时:不要拖垮 K 线。 + +--- + +## 不算本次 + +- CoinGlass / 付费数据 +- 清算、热力、多空 +- Live、下单 +- chan 叠图(等本接口可用再做) + +--- + +## 怎样算齐 + +1. `GET /api/deriv?symbol=BTC/USDT:USDT&tf=15m` 能拿到 `oi`、`funding`,时间能对上同参数的 `/api/candles`。 +2. chan / 浏览器零次访问交易所。 +3. 只挂一家所时,接口仍可用,只缺对应字段。 +4. K 线不受影响。 diff --git a/web/api/provider.py b/web/api/provider.py new file mode 100644 index 0000000..b815e55 --- /dev/null +++ b/web/api/provider.py @@ -0,0 +1,73 @@ +"""把 data_provider 的资金面转给 Web,浏览器不直连交易所。""" +from __future__ import annotations + +import logging + +import requests +from flask import Blueprint, jsonify, request + +from services.runtime.market_data import ( + fetch_derivatives, + fetch_sentiment_latest, + fetch_sentiment_metrics, +) + +logger = logging.getLogger(__name__) + +bp = Blueprint("provider", __name__) + + +def _http_error(exc: requests.HTTPError): + status = 502 + detail = str(exc) + if exc.response is not None: + status = exc.response.status_code or 502 + try: + body = exc.response.json() + detail = body.get("detail") or body.get("error") or detail + except Exception: + detail = exc.response.text or detail + return jsonify({"error": detail}), status + + +@bp.route("/api/derivatives") +def api_derivatives(): + symbol = (request.args.get("symbol") or "BTC/USDT:USDT").strip() + exchange = (request.args.get("exchange") or "").strip() or None + try: + return jsonify(fetch_derivatives(symbol, exchange)) + except requests.HTTPError as exc: + return _http_error(exc) + except Exception as exc: + logger.warning("derivatives 中转失败: %s", exc) + return jsonify({"error": str(exc)}), 503 + + +@bp.route("/api/sentiment/latest") +def api_sentiment_latest(): + symbol = (request.args.get("symbol") or "BTC/USDT:USDT").strip() + try: + return jsonify(fetch_sentiment_latest(symbol)) + except requests.HTTPError as exc: + return _http_error(exc) + except Exception as exc: + logger.warning("sentiment latest 中转失败: %s", exc) + return jsonify({"error": str(exc)}), 503 + + +@bp.route("/api/sentiment/metrics") +def api_sentiment_metrics(): + metric = (request.args.get("metric") or "").strip() + if not metric: + return jsonify({"error": "metric required"}), 400 + symbol = (request.args.get("symbol") or "BTC/USDT:USDT").strip() + start = request.args.get("start", type=int) + end = request.args.get("end", type=int) + limit = request.args.get("limit", type=int) + try: + return jsonify(fetch_sentiment_metrics(metric, symbol, start=start, end=end, limit=limit)) + except requests.HTTPError as exc: + return _http_error(exc) + except Exception as exc: + logger.warning("sentiment 中转失败: %s", exc) + return jsonify({"error": str(exc)}), 503 diff --git a/web/app.py b/web/app.py index 8a46e31..9cab576 100644 --- a/web/app.py +++ b/web/app.py @@ -13,6 +13,7 @@ from flask import Flask from config import FLASK_HOST, FLASK_PORT from api.analyze import bp as analyze_bp from api.pages import bp as pages_bp +from api.provider import bp as provider_bp from api.symbols import bp as symbols_bp from api.trend import bp as trend_bp @@ -23,6 +24,7 @@ def create_app() -> Flask: app.register_blueprint(analyze_bp) app.register_blueprint(symbols_bp) app.register_blueprint(trend_bp) + app.register_blueprint(provider_bp) return app diff --git a/web/services/runtime/__init__.py b/web/services/runtime/__init__.py index bcb28bb..bc32b2f 100644 --- a/web/services/runtime/__init__.py +++ b/web/services/runtime/__init__.py @@ -54,6 +54,9 @@ from .market_data import ( # noqa: F401 get_crypto_kl_data, get_a_stock_kl_data, load_crypto_symbols, + fetch_derivatives, + fetch_sentiment_metrics, + fetch_sentiment_latest, ) from .indicators import ( # noqa: F401 add_indicators, diff --git a/web/services/runtime/market_data.py b/web/services/runtime/market_data.py index 53e5fc9..9885b5a 100644 --- a/web/services/runtime/market_data.py +++ b/web/services/runtime/market_data.py @@ -319,3 +319,34 @@ def load_crypto_symbols(limit=200): except Exception: return DEFAULT_SYMBOLS[:limit] + +def _provider_get(path, params, timeout=8): + resp = requests.get(f"{DATA_SERVICE_URL}{path}", params=params, timeout=timeout) + resp.raise_for_status() + return resp.json() + + +def fetch_derivatives(symbol, exchange=None): + """当前资金面快照。只打 data_provider,不打交易所。""" + params = {"symbol": symbol} + if exchange: + params["exchange"] = exchange + return _provider_get("/api/derivatives", params) + + +def fetch_sentiment_metrics(metric, symbol, start=None, end=None, limit=None): + """情绪/资金面序列。只打 data_provider。""" + params = {"metric": metric, "symbol": symbol} + if start is not None: + params["start"] = int(start) + if end is not None: + params["end"] = int(end) + if limit is not None: + params["limit"] = int(limit) + return _provider_get("/api/sentiment/metrics", params) + + +def fetch_sentiment_latest(symbol): + """情绪面最新快照。只打 data_provider。""" + return _provider_get("/api/sentiment/latest", {"symbol": symbol}) + diff --git a/web/services/runtime/state.py b/web/services/runtime/state.py index 2142ce3..7767d25 100644 --- a/web/services/runtime/state.py +++ b/web/services/runtime/state.py @@ -46,6 +46,7 @@ DEFAULT_TIMEFRAME_LABELS = OrderedDict([ ("5m", "5分钟"), ("15m", "15分钟"), ("30m", "30分钟"), + ("45m", "45分钟"), ("1h", "1小时"), ("2h", "2小时"), ("4h", "4小时"), diff --git a/web/services/runtime/timeframes.py b/web/services/runtime/timeframes.py index deb6a1d..f851afd 100644 --- a/web/services/runtime/timeframes.py +++ b/web/services/runtime/timeframes.py @@ -94,19 +94,19 @@ def _prefer_smaller(candidates, labels_ordered, ceiling_tf, timeframe_keys): def compute_timeframe_defaults(labels_ordered): """ 根据已排序的「周期 → 中文标签」映射,计算主 / 次 / 次次周期默认值。 - 默认偏好:主 4h、次 1h、次次 15m。 + 默认偏好:主 45m、次 15m、次次 5m。 labels_ordered: OrderedDict 或按插入顺序排列的 dict。 """ if not labels_ordered: labels_ordered = DEFAULT_TIMEFRAME_LABELS.copy() timeframe_keys = list(labels_ordered.keys()) - preferred_main = next((tf for tf in ['4h', '1h', '15m'] if tf in labels_ordered), None) + preferred_main = next((tf for tf in ['45m', '30m', '1h'] if tf in labels_ordered), None) default_main = preferred_main or (timeframe_keys[0] if timeframe_keys else '1m') if default_main not in labels_ordered and timeframe_keys: default_main = timeframe_keys[0] - default_element = _prefer_smaller(['1h', '15m'], labels_ordered, default_main, timeframe_keys) - default_sub_sub = _prefer_smaller(['15m', '5m'], labels_ordered, default_element, timeframe_keys) + default_element = _prefer_smaller(['15m', '5m', '30m'], labels_ordered, default_main, timeframe_keys) + default_sub_sub = _prefer_smaller(['5m', '1m', '15m'], labels_ordered, default_element, timeframe_keys) return default_main, default_element, default_sub_sub, timeframe_keys diff --git a/web/static/js/app/api_client.js b/web/static/js/app/api_client.js index c3c3e84..828ba65 100644 --- a/web/static/js/app/api_client.js +++ b/web/static/js/app/api_client.js @@ -10,6 +10,33 @@ window.ChanApi = { symbols: function() { return fetch('/api/symbols').then(r => r.json()); }, + derivatives: function(params) { + const q = new URLSearchParams(params || {}); + return fetch('/api/derivatives?' + q.toString()).then(function(r) { + return r.json().then(function(body) { + if (!r.ok) throw new Error((body && body.error) || r.statusText); + return body; + }); + }); + }, + sentimentLatest: function(params) { + const q = new URLSearchParams(params || {}); + return fetch('/api/sentiment/latest?' + q.toString()).then(function(r) { + return r.json().then(function(body) { + if (!r.ok) throw new Error((body && body.error) || r.statusText); + return body; + }); + }); + }, + sentimentMetrics: function(params) { + const q = new URLSearchParams(params || {}); + return fetch('/api/sentiment/metrics?' + q.toString()).then(function(r) { + return r.json().then(function(body) { + if (!r.ok) throw new Error((body && body.error) || r.statusText); + return body; + }); + }); + }, macdConfig: function(body) { if (body === undefined) return fetch('/api/macd_config').then(r => r.json()); return fetch('/api/macd_config', { diff --git a/web/static/js/app/chart_sync.js b/web/static/js/app/chart_sync.js index b6d05d5..d5b85de 100644 --- a/web/static/js/app/chart_sync.js +++ b/web/static/js/app/chart_sync.js @@ -229,29 +229,29 @@ function updateTradingViewData(options) { } // 更新MACD数据 - if (tvWidget.series.macdLineSeries && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data)) { - // 提取MACD数据 + const macdKlineSrc = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? (currentData.element_kline_data || []) : (currentData.kline_data || [])); + const macdSrc = useSubSubPeriod ? (currentData.sub_sub_macd || currentData.macd) : (useElementPeriod ? (currentData.element_macd || currentData.macd) : currentData.macd); + if (tvWidget.series.macdLineSeries && macdSrc && macdKlineSrc && Array.isArray(macdKlineSrc)) { const macdData = []; const signalData = []; const histogramData = []; - for (let i = 0; i < currentData.kline_data.length; i++) { - const kline = currentData.kline_data[i]; + for (let i = 0; i < macdKlineSrc.length; i++) { + const kline = macdKlineSrc[i]; const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); - if (currentData.macd && currentData.macd.macd && currentData.macd.macd[i] !== undefined) { + if (macdSrc && macdSrc.macd && macdSrc.macd[i] !== undefined) { macdData.push({ time: timestamp, - value: currentData.macd.macd[i] + value: macdSrc.macd[i] }); signalData.push({ time: timestamp, - value: currentData.macd.signal[i] + value: macdSrc.signal[i] }); - // 设置直方图颜色 - const histValue = currentData.macd.histogram[i]; + const histValue = macdSrc.histogram[i]; histogramData.push({ time: timestamp, value: histValue, @@ -309,6 +309,9 @@ function updateTradingViewData(options) { } ); } + if (typeof refreshUnittfOverlayFromData === 'function') { + refreshUnittfOverlayFromData(currentData); + } } catch (e) { console.warn('更新ChanMACD标注失败:', e); } @@ -331,7 +334,8 @@ function updateTradingViewData(options) { tvWidget.volumeChart, tvWidget.atrChart, tvWidget.macdChart, - tvWidget.chanMacdChart + tvWidget.chanMacdChart, + tvWidget.sentimentChart ].filter(Boolean); const vr = clampedVisibleRange || savedVisibleRange; @@ -409,8 +413,11 @@ function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContai volume: false, atr: false, macd: false, - chanmacd: false + chanmacd: false, + sentiment: false }; + const sentimentChart = tvWidget && tvWidget.sentimentChart; + const sentimentChartContainer = tvWidget && tvWidget.sentimentChartContainer; // 同步图表的时间范围 function syncCharts(sourceChart, sourceContainer) { @@ -438,6 +445,9 @@ function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContai if (showMacd && chanMacdChart && sourceChart !== chanMacdChart && chanMacdChart.timeScale) { try { chanMacdChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {} } + if (sentimentChart && sourceChart !== sentimentChart && sentimentChart.timeScale) { + try { sentimentChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {} + } if (tvWidget && tvWidget.state) { tvWidget.state.logicalRange = logicalRange; @@ -458,7 +468,8 @@ function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContai chart === volumeChart ? 'volume' : chart === atrChart ? 'atr' : chart === macdChart ? 'macd' : - chart === chanMacdChart ? 'chanmacd' : 'unknown'; + chart === chanMacdChart ? 'chanmacd' : + chart === sentimentChart ? 'sentiment' : 'unknown'; const timeRangeHandler = () => { if (!syncInProgress) { @@ -515,6 +526,9 @@ function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContai if (showMacd && chanMacdChartContainer && chanMacdChart) { addChartSyncEvents(chanMacdChartContainer, chanMacdChart); } + if (sentimentChartContainer && sentimentChart) { + addChartSyncEvents(sentimentChartContainer, sentimentChart); + } // 窗口大小变化时重绘图表 — 使用可清理的方式注册 const resizeHandler = () => { @@ -533,6 +547,9 @@ function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContai if (showMacd && chanMacdChart && chanMacdChartContainer) { chanMacdChart.applyOptions({ width: chanMacdChartContainer.clientWidth, height: chanMacdChartContainer.clientHeight }); } + if (sentimentChart && sentimentChartContainer) { + sentimentChart.applyOptions({ width: sentimentChartContainer.clientWidth, height: sentimentChartContainer.clientHeight }); + } setTimeout(() => { if (mainChart) syncCharts(mainChart, mainChartContainer); }, 200); }; window.addEventListener('resize', resizeHandler); @@ -549,9 +566,11 @@ function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartCon // 初始化 U 显示状态(主/次周期分开控制) const isShowUMain = $('#toggleUOnMain').is(':checked'); const isShowUElement = $('#toggleUOnElement').is(':checked'); + const isShowUSubSub = $('#toggleUOnSubSub').is(':checked'); window.showUOnMain = isShowUMain; window.showUOnElement = isShowUElement; - if (!isShowUMain && !isShowUElement) { + window.showUOnSubSub = isShowUSubSub; + if (!isShowUMain && !isShowUElement && !isShowUSubSub) { // 隐藏时清空子图上的 U 标记 if (tvWidget.series && tvWidget.series.chanMacdLineSeries) { try { tvWidget.series.chanMacdLineSeries.setMarkers([]); } catch (e) {} diff --git a/web/static/js/app/chart_tables.js b/web/static/js/app/chart_tables.js index 3e38571..82f7fce 100644 --- a/web/static/js/app/chart_tables.js +++ b/web/static/js/app/chart_tables.js @@ -169,7 +169,7 @@ function updateTables(currentData) { }); // 更新数据源信息显示 - const selectedPeriod = useSubSubPeriod ? '次次周期' : (useElementPeriod ? '小周期' : '主周期'); + const selectedPeriod = useSubSubPeriod ? '次次' : (useElementPeriod ? '次' : '主'); const timeframe = useSubSubPeriod && data.sub_sub_timeframe ? data.sub_sub_timeframe : (useElementPeriod && data.element_timeframe ? data.element_timeframe : $('#timeframe').val()); $('#dataSourceText').html(`当前显示的是${selectedPeriod} (${timeframe}) 数据`); @@ -297,55 +297,55 @@ function setupDataSourceInfo(data) { $('#kline-tab, #macd-tab').off('click').on('click', function() { $('.data-source-info').show(); if (useSubSubPeriod && data.sub_sub_kline_data && data.sub_sub_kline_data.length > 0) { - $('#dataSourceText').html(`当前显示的是次次周期 (${subSubTimeframe}) 数据`); + $('#dataSourceText').html(`当前显示的是次次 (${subSubTimeframe}) 数据`); } else if (useElementPeriod && data.element_kline_data && data.element_kline_data.length > 0) { - $('#dataSourceText').html(`当前显示的是小周期 (${elementTimeframe}) 数据`); + $('#dataSourceText').html(`当前显示的是次 (${elementTimeframe}) 数据`); } else { - $('#dataSourceText').html(`当前显示的是主周期 (${mainTimeframe}) 数据`); + $('#dataSourceText').html(`当前显示的是主 (${mainTimeframe}) 数据`); } }); $('#bi-tab').off('click').on('click', function() { $('.data-source-info').show(); if (useSubSubPeriod && data.sub_sub_bi_list && data.sub_sub_bi_list.length > 0) { - $('#dataSourceText').html(`当前显示的是次次周期 (${subSubTimeframe}) 笔数据`); + $('#dataSourceText').html(`当前显示的是次次 (${subSubTimeframe}) 笔数据`); } else if (useElementPeriod && data.element_bi_list && data.element_bi_list.length > 0) { - $('#dataSourceText').html(`当前显示的是小周期 (${elementTimeframe}) 笔数据`); + $('#dataSourceText').html(`当前显示的是次 (${elementTimeframe}) 笔数据`); } else { - $('#dataSourceText').html(`当前显示的是主周期 (${mainTimeframe}) 笔数据`); + $('#dataSourceText').html(`当前显示的是主 (${mainTimeframe}) 笔数据`); } }); $('#seg-tab').off('click').on('click', function() { $('.data-source-info').show(); if (useSubSubPeriod && data.sub_sub_seg_list && data.sub_sub_seg_list.length > 0) { - $('#dataSourceText').html(`当前显示的是次次周期 (${subSubTimeframe}) 线段数据`); + $('#dataSourceText').html(`当前显示的是次次 (${subSubTimeframe}) 线段数据`); } else if (useElementPeriod && data.element_seg_list && data.element_seg_list.length > 0) { - $('#dataSourceText').html(`当前显示的是小周期 (${elementTimeframe}) 线段数据`); + $('#dataSourceText').html(`当前显示的是次 (${elementTimeframe}) 线段数据`); } else { - $('#dataSourceText').html(`当前显示的是主周期 (${mainTimeframe}) 线段数据`); + $('#dataSourceText').html(`当前显示的是主 (${mainTimeframe}) 线段数据`); } }); $('#zs-tab').off('click').on('click', function() { $('.data-source-info').show(); if (useSubSubPeriod && data.sub_sub_zs_list && data.sub_sub_zs_list.length > 0) { - $('#dataSourceText').html(`当前显示的是次次周期 (${subSubTimeframe}) 中枢数据`); + $('#dataSourceText').html(`当前显示的是次次 (${subSubTimeframe}) 中枢数据`); } else if (useElementPeriod && data.element_zs_list && data.element_zs_list.length > 0) { - $('#dataSourceText').html(`当前显示的是小周期 (${elementTimeframe}) 中枢数据`); + $('#dataSourceText').html(`当前显示的是次 (${elementTimeframe}) 中枢数据`); } else { - $('#dataSourceText').html(`当前显示的是主周期 (${mainTimeframe}) 中枢数据`); + $('#dataSourceText').html(`当前显示的是主 (${mainTimeframe}) 中枢数据`); } }); $('#trade-points-tab').off('click').on('click', function() { $('.data-source-info').show(); if (useSubSubPeriod && data.sub_sub_bsp_list && data.sub_sub_bsp_list.length > 0) { - $('#dataSourceText').html(`当前显示的是次次周期 (${subSubTimeframe}) 买卖点数据`); + $('#dataSourceText').html(`当前显示的是次次 (${subSubTimeframe}) 买卖点数据`); } else if (useElementPeriod && data.element_trade_points && data.element_trade_points.length > 0) { - $('#dataSourceText').html(`当前显示的是小周期 (${elementTimeframe}) 买卖点数据`); + $('#dataSourceText').html(`当前显示的是次 (${elementTimeframe}) 买卖点数据`); } else { - $('#dataSourceText').html(`当前显示的是主周期 (${mainTimeframe}) 买卖点数据`); + $('#dataSourceText').html(`当前显示的是主 (${mainTimeframe}) 买卖点数据`); } }); diff --git a/web/static/js/app/chart_tv.js b/web/static/js/app/chart_tv.js index 4990e08..82400fb 100644 --- a/web/static/js/app/chart_tv.js +++ b/web/static/js/app/chart_tv.js @@ -18,6 +18,7 @@ function initTradingView(symbol, timeframe) { chartTvRenderChan(ctx); chartTvRenderOverlays(ctx); chartTvFinalize(ctx); + if (window.ChanDeriv) ChanDeriv.loadOverlays(); console.log('图表初始化完成'); } catch (e) { diff --git a/web/static/js/app/chart_tv_finalize.js b/web/static/js/app/chart_tv_finalize.js index c9dc84f..bece7e4 100644 --- a/web/static/js/app/chart_tv_finalize.js +++ b/web/static/js/app/chart_tv_finalize.js @@ -22,6 +22,8 @@ function chartTvFinalize(ctx) { var atrChart = ctx.atrChart; var macdChart = ctx.macdChart; var chanMacdChart = ctx.chanMacdChart; + var sentimentChart = ctx.sentimentChart; + var sentimentChartContainer = ctx.sentimentChartContainer; var createChartOptions = ctx.createChartOptions; // 同步所有图表的时间轴配置 const hasPendingRestoreView = !!window._pendingRestoreView; @@ -55,6 +57,9 @@ function chartTvFinalize(ctx) { if (showMacd && macdChart) { macdChart.timeScale().applyOptions(baseOptions); } + if (sentimentChart) { + sentimentChart.timeScale().applyOptions(baseOptions); + } }; // 首先同步时间轴设置 @@ -65,7 +70,8 @@ function chartTvFinalize(ctx) { const visibleBarsCount = 200; const allChartsNow = [mainChart, volumeChart, atrChart] .concat(showMacd && macdChart ? [macdChart] : []) - .concat(showMacd && chanMacdChart ? [chanMacdChart] : []); + .concat(showMacd && chanMacdChart ? [chanMacdChart] : []) + .concat(sentimentChart ? [sentimentChart] : []); const restoreOpts = function () { const firstT = candles && candles.length ? candles[0].time : null; const lastT = candles && candles.length ? candles[candles.length - 1].time : null; @@ -103,6 +109,9 @@ function chartTvFinalize(ctx) { if (showMacd && chanMacdChart) { chanMacdChart.timeScale().setVisibleLogicalRange(logRange); } + if (sentimentChart) { + sentimentChart.timeScale().setVisibleLogicalRange(logRange); + } } return; } @@ -117,6 +126,9 @@ function chartTvFinalize(ctx) { if (showMacd && chanMacdChart) { chanMacdChart.timeScale().setVisibleLogicalRange(logRange); } + if (sentimentChart) { + sentimentChart.timeScale().setVisibleLogicalRange(logRange); + } console.log('🔧 时间轴同步完成'); } }, 50); @@ -127,6 +139,8 @@ function chartTvFinalize(ctx) { tvWidget.atrChart = atrChart; tvWidget.macdChart = macdChart; tvWidget.chanMacdChart = chanMacdChart; + tvWidget.sentimentChart = sentimentChart; + tvWidget.sentimentChartContainer = sentimentChartContainer; tvWidget.state.isInitialized = true; // 注册窗口卸载时释放资源,避免GPU内存泄漏 window.onbeforeunload = function() { @@ -137,6 +151,7 @@ function chartTvFinalize(ctx) { 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(); + if (tvWidget.sentimentChart && typeof tvWidget.sentimentChart.remove === 'function') tvWidget.sentimentChart.remove(); } } catch (e) {} }; @@ -220,6 +235,7 @@ function chartTvFinalize(ctx) { const allCharts = [mainChart, volumeChart, atrChart]; if (showMacd && macdChart) allCharts.push(macdChart); if (showMacd && chanMacdChart) allCharts.push(chanMacdChart); + if (sentimentChart) allCharts.push(sentimentChart); // 检查是否有待恢复的视图(缩放 + 位置) const pending = window._pendingRestoreView || pendingView; @@ -243,7 +259,8 @@ function chartTvFinalize(ctx) { console.log('🔧 最终同步可见范围:', visibleRange); [volumeChart, atrChart].concat( showMacd && macdChart ? [macdChart] : [], - showMacd && chanMacdChart ? [chanMacdChart] : [] + showMacd && chanMacdChart ? [chanMacdChart] : [], + sentimentChart ? [sentimentChart] : [] ).forEach(c => { try { c.timeScale().setVisibleRange(visibleRange); } catch(e) {} }); diff --git a/web/static/js/app/chart_tv_indicators.js b/web/static/js/app/chart_tv_indicators.js index d33d210..ccdc3ad 100644 --- a/web/static/js/app/chart_tv_indicators.js +++ b/web/static/js/app/chart_tv_indicators.js @@ -5,6 +5,40 @@ function formatSdMarkerText(separateDiv) { return `SD${n === 99999 ? 0 : n}`; } +function shouldShowSdMarker(separateDiv) { + const n = Number(separateDiv); + return isFinite(n) && n > 0 && n !== 99999; +} + +var SD_CD_MARKER_COLOR = '#dc3545'; +var SD_CD_MARKER_SIZE = 1.2; + +function histArrowShape(pos) { + return pos === 'belowBar' ? 'arrowDown' : 'arrowUp'; +} + +function makeSdMarker(ts, pos, separateDiv) { + return { + time: ts, + position: pos, + color: SD_CD_MARKER_COLOR, + shape: histArrowShape(pos), + text: formatSdMarkerText(separateDiv), + size: SD_CD_MARKER_SIZE + }; +} + +function makeCdMarker(ts, pos) { + return { + time: ts, + position: pos, + color: SD_CD_MARKER_COLOR, + shape: histArrowShape(pos), + text: 'CD', + size: SD_CD_MARKER_SIZE + }; +} + function chartTvRenderIndicators(ctx) { var symbol = ctx.symbol; var timeframe = ctx.timeframe; @@ -214,7 +248,7 @@ function chartTvRenderIndicators(ctx) { const chanMacdLineSeries = chanMacdChart.addLineSeries({ color: '#2962FF', lineWidth: 1, - title: 'ChanMACD', + title: 'MACD', lastValueVisible: false, priceLineVisible: false, }); @@ -375,7 +409,8 @@ function chartTvRenderIndicators(ctx) { 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; + const hist = (macdObj.histogram && macdObj.histogram[i] !== undefined && macdObj.histogram[i] !== null) ? macdObj.histogram[i] : null; + const val = (hist !== null) ? hist : ((macdObj.macd && macdObj.macd[i] !== undefined && macdObj.macd[i] !== null) ? macdObj.macd[i] : null); map.set(t, val); } return map; @@ -392,15 +427,15 @@ function chartTvRenderIndicators(ctx) { 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) { + if (shouldShowSdMarker(item.separate_div)) { 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: formatSdMarkerText(item.separate_div), size: 0.6 }); + mainMarkers.push(makeSdMarker(ts, posSd, item.separate_div)); } 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 }); + mainMarkers.push(makeCdMarker(ts, posCd)); } 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 }); @@ -414,15 +449,15 @@ function chartTvRenderIndicators(ctx) { 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) { + if (shouldShowSdMarker(item.separate_div)) { 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: formatSdMarkerText(item.separate_div), size: 0.6 }); + elementMarkers.push(makeSdMarker(ts, posSd, item.separate_div)); } 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 }); + elementMarkers.push(makeCdMarker(ts, posCd)); } 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 }); @@ -432,21 +467,24 @@ function chartTvRenderIndicators(ctx) { const subSubCm = currentData.sub_sub_chan_macd || {}; const subSubMarkers = []; - const subSubMacdMap = buildMacdTimeMap(currentData.macd, currentData.kline_data); + const subSubMacdMap = buildMacdTimeMap( + (currentData.sub_sub_macd || currentData.macd), + (currentData.sub_sub_kline_data || 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) { + if (shouldShowSdMarker(item.separate_div)) { 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: formatSdMarkerText(item.separate_div), size: 0.6 }); + subSubMarkers.push(makeSdMarker(ts, posSd, item.separate_div)); } 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 }); + subSubMarkers.push(makeCdMarker(ts, posCd)); } 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 }); @@ -489,7 +527,8 @@ function chartTvRenderIndicators(ctx) { 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; + const hist = (macdObj.histogram && macdObj.histogram[i] !== undefined && macdObj.histogram[i] !== null) ? macdObj.histogram[i] : null; + const val = (hist !== null) ? hist : ((macdObj.macd && macdObj.macd[i] !== undefined && macdObj.macd[i] !== null) ? macdObj.macd[i] : null); map.set(t, val); } return map; @@ -499,20 +538,24 @@ function chartTvRenderIndicators(ctx) { (currentData.element_macd || currentData.macd), (currentData.element_kline_data || currentData.kline_data) ); + const subSubMacdMapAll = buildMacdTimeMapAll( + (currentData.sub_sub_macd || currentData.macd), + (currentData.sub_sub_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) { + if (shouldShowSdMarker(item.separate_div)) { 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: formatSdMarkerText(item.separate_div), size: 0.6 }); + mainMarkersAll.push(makeSdMarker(ts, posSd, item.separate_div)); } 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 }); + mainMarkersAll.push(makeCdMarker(ts, posCd)); } 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 }); @@ -524,15 +567,15 @@ function chartTvRenderIndicators(ctx) { 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) { + if (shouldShowSdMarker(item.separate_div)) { 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: formatSdMarkerText(item.separate_div), size: 0.6 }); + elementMarkersAll.push(makeSdMarker(ts, posSd, item.separate_div)); } 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 }); + elementMarkersAll.push(makeCdMarker(ts, posCd)); } 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 }); @@ -548,11 +591,15 @@ function chartTvRenderIndicators(ctx) { 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: formatSdMarkerText(item.separate_div), size: 0.6 }); + if (shouldShowSdMarker(item.separate_div)) { + const macdVal = subSubMacdMapAll.get(ts); + const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; + subSubMarkersAll.push(makeSdMarker(ts, posSd, item.separate_div)); } if (item.continue_div === true) { - subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 }); + const macdVal = subSubMacdMapAll.get(ts); + const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar'; + subSubMarkersAll.push(makeCdMarker(ts, posCd)); } 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 }); diff --git a/web/static/js/app/chart_tv_lifecycle.js b/web/static/js/app/chart_tv_lifecycle.js index 3f1c19f..668330f 100644 --- a/web/static/js/app/chart_tv_lifecycle.js +++ b/web/static/js/app/chart_tv_lifecycle.js @@ -25,7 +25,7 @@ function disposeTradingViewCharts() { } if (tvWidget) { - ['mainChart', 'volumeChart', 'macdChart', 'chanMacdChart', 'atrChart'].forEach(function (key) { + ['mainChart', 'volumeChart', 'macdChart', 'chanMacdChart', 'atrChart', 'sentimentChart'].forEach(function (key) { try { if (tvWidget[key] && typeof tvWidget[key].remove === 'function') { tvWidget[key].remove(); diff --git a/web/static/js/app/chart_tv_overlays.js b/web/static/js/app/chart_tv_overlays.js index ffc4be8..3eb643d 100644 --- a/web/static/js/app/chart_tv_overlays.js +++ b/web/static/js/app/chart_tv_overlays.js @@ -82,11 +82,6 @@ var SUB_SUB_KLC_TREND_STYLE = { UNKNOWN: { position: 'inBar', color: '#004d40', shape: 'square', size: 0.5 } }; -function parseAreaDivValue(v) { - var n = Number(v); - return (isFinite(n) && n !== 0) ? n : 0; -} - function pushAreaTextLabel(out, item, style, text) { if (!item || !item.end_time || !text) return; var ts = Math.floor(new Date(item.end_time).getTime() / 1000); @@ -104,25 +99,6 @@ function pushAreaTextLabel(out, item, style, text) { }); } -function pushAreaDivMarker(out, item, style) { - var div = parseAreaDivValue(item && item.macd_div); - if (!div) return; - pushAreaTextLabel(out, item, style, div.toFixed(2)); -} - -function collectAreaDivMarkers(biList, uncompletedBi, segList, uncompletedSeg, biStyle, segStyle, showBi, showSeg) { - var out = []; - if (showBi) { - (biList || []).forEach(function (bi) { pushAreaDivMarker(out, bi, biStyle); }); - (uncompletedBi || []).forEach(function (bi) { pushAreaDivMarker(out, bi, biStyle); }); - } - if (showSeg) { - (segList || []).forEach(function (seg) { pushAreaDivMarker(out, seg, segStyle); }); - (uncompletedSeg || []).forEach(function (seg) { pushAreaDivMarker(out, seg, segStyle); }); - } - return out; -} - function formatMacdAreaText(v) { var n = Number(v); if (!isFinite(n) || n === 0) return ''; @@ -197,54 +173,6 @@ function buildAreaHistMarkersFromData(data) { return markers; } -function buildAreaDivMarkersFromData(data) { - var markers = []; - if (!data) return markers; - var showMainBi = $('#showMainMacdDiv').is(':checked'); - var showMainSeg = $('#showMainSegMacdDiv').is(':checked'); - if (showMainBi || showMainSeg) { - markers = markers.concat(collectAreaDivMarkers( - data.bi_list, - data.uncompleted_bi_list, - data.seg_list, - data.uncompleted_seg_list, - { color: '#e53935', size: 0.6 }, - { color: '#fb8c00', size: 0.7 }, - showMainBi, - showMainSeg - )); - } - var showElementBi = $('#showElementMacdDiv').is(':checked'); - var showElementSeg = $('#showElementSegMacdDiv').is(':checked'); - if (showElementBi || showElementSeg) { - markers = markers.concat(collectAreaDivMarkers( - data.element_bi_list, - data.element_uncompleted_bi_list, - data.element_seg_list, - data.element_uncompleted_seg_list, - { color: '#8e24aa', size: 0.55 }, - { color: '#5e35b1', size: 0.65 }, - showElementBi, - showElementSeg - )); - } - var showSubSubBi = $('#showSubSubMacdDiv').is(':checked'); - var showSubSubSeg = $('#showSubSubSegMacdDiv').is(':checked'); - if (showSubSubBi || showSubSubSeg) { - markers = markers.concat(collectAreaDivMarkers( - data.sub_sub_bi_list, - data.sub_sub_uncompleted_bi_list, - data.sub_sub_seg_list, - data.sub_sub_uncompleted_seg_list, - { color: '#00897b', size: 0.5 }, - { color: '#00695c', size: 0.6 }, - showSubSubBi, - showSubSubSeg - )); - } - return markers; -} - function buildKlcTrendMarker(timeAligned, trendRaw, palette) { var kind = normalizeKlcTrendRaw(trendRaw); var style = palette[kind] || palette.UNKNOWN || palette.FLAT; @@ -299,6 +227,21 @@ function getMainPriceSeries() { s.lineSeries || s.areaSeries || s.baselineSeries || null; } +function plotLeftOffset(chart) { + try { + var left = chart && chart.priceScale && chart.priceScale('left'); + if (!left) return 0; + var visible = true; + try { + var opts = left.options && left.options(); + if (opts && opts.visible === false) return 0; + } catch (e) {} + return (typeof left.width === 'function' ? left.width() : 0) || 0; + } catch (e) { + return 0; + } +} + function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) { if (!mainChart || !mainChartContainer) return; if (typeof window._fxBoxOverlayCleanup === 'function') { @@ -330,7 +273,7 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) { var series = getMainPriceSeries(); if (!series || (!boxes.length && !labels.length)) return '0'; var ts = mainChart.timeScale(); - var parts = [boxes.length, labels.length]; + var parts = [boxes.length, labels.length, quant(plotLeftOffset(mainChart))]; if (boxes.length) { var a = boxes[0]; var b = boxes[boxes.length - 1]; @@ -374,18 +317,20 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) { return; } var ts = mainChart.timeScale(); + var x0 = plotLeftOffset(mainChart); for (var i = 0; i < boxes.length; i++) { var box = boxes[i]; var x = ts.timeToCoordinate(box.time); var y1 = series.priceToCoordinate(box.hi); var y2 = series.priceToCoordinate(box.lo); if (x == null || y1 == null || y2 == null) continue; + var px = Math.round(x + x0) + 0.5; ctx2.beginPath(); ctx2.strokeStyle = box.color; ctx2.lineWidth = 1; ctx2.setLineDash([4, 3]); - ctx2.moveTo(Math.round(x) + 0.5, y1); - ctx2.lineTo(Math.round(x) + 0.5, y2); + ctx2.moveTo(px, y1); + ctx2.lineTo(px, y2); ctx2.stroke(); } ctx2.setLineDash([]); @@ -399,7 +344,7 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) { if (lx == null || ly == null) continue; ctx2.fillStyle = lab.color; ctx2.textBaseline = lab.above ? 'bottom' : 'top'; - ctx2.fillText(lab.text, Math.round(lx), lab.above ? ly - 3 : ly + 3); + ctx2.fillText(lab.text, Math.round(lx + x0), lab.above ? ly - 3 : ly + 3); } } lastSig = sampleSig(); @@ -1182,56 +1127,6 @@ function chartTvRenderOverlays(ctx) { window.bspMarkers = []; } - // 第四类买卖点(B4/S4):中枢突破回抽后当根入场,位置同 B3/S3 但早 7~8 根。 - // 与 BSP 分开收集,因为它数量远多于 B1/B2/B3,混在一个开关里图会糊掉。 - if ($('#showMainFastBsp').is(':checked') || $('#showElementFastBsp').is(':checked') || $('#showSubSubFastBsp').is(':checked')) { - // 深色 = 区间套(大级别分型同向) + 中枢顺向推进都满足;浅色 = 未通过过滤 - const FAST_BSP_STYLE = { - 'BUY': { strong: '#FF6D00', weak: '#FFCC80', text: 'B4', position: 'belowBar' }, - 'SELL': { strong: '#0091EA', weak: '#81D4FA', text: 'S4', position: 'aboveBar' }, - }; - const onlyFiltered = ($('#fastBspFilterMode').val() || 'all') === 'filtered'; - const allFastBspMarkers = []; - - const collectFastBsp = function(list, prefix, label) { - (list || []).forEach(function(bsp) { - try { - const ts = Math.floor(new Date(bsp.time).getTime() / 1000); - if (isNaN(ts)) return; - const style = FAST_BSP_STYLE[(bsp.dir || '').toUpperCase()]; - if (!style) return; - const passed = !!(bsp.htf_agree && bsp.ladder_ok); - if (onlyFiltered && !passed) return; - allFastBspMarkers.push({ - time: ts, - position: style.position, - color: passed ? style.strong : style.weak, - text: prefix + (passed ? style.text : style.text.toLowerCase()), - size: passed ? 2 : 1 - }); - } catch (e) { - console.error(label + '第四类买卖点处理出错:', e); - } - }); - }; - - if ($('#showMainFastBsp').is(':checked')) { - collectFastBsp(currentData.fast_bsp_list, '', '主周期'); - } - if ($('#showElementFastBsp').is(':checked')) { - collectFastBsp(currentData.element_fast_bsp_list, 'e', '次周期'); - } - if ($('#showSubSubFastBsp').is(':checked')) { - collectFastBsp(currentData.sub_sub_fast_bsp_list, 's', '次次周期'); - } - - allFastBspMarkers.sort((a, b) => a.time - b.time); - window.fastBspMarkers = allFastBspMarkers; - console.log(`绘制第四类买卖点,共${allFastBspMarkers.length}个标记(${onlyFiltered ? '仅过滤后' : '全部'})`); - } else { - window.fastBspMarkers = []; - } - // 添加买卖点标记(旧版,保留兼容) // 这里为了与主面板上的「买卖点」开关保持一致, // 同时响应顶部的 `#showMainBsp` 复选框 @@ -1807,7 +1702,7 @@ function chartTvRenderOverlays(ctx) { lineStyle: 2, // 虚线 lastValueVisible: false, priceLineVisible: false, - title: '次周期布林上轨' + title: '次布林上轨' }); elementUpperBandSeries.setData(elementUpperBandData); @@ -1818,7 +1713,7 @@ function chartTvRenderOverlays(ctx) { lineStyle: 2, // 虚线 lastValueVisible: false, priceLineVisible: false, - title: '次周期布林下轨' + title: '次布林下轨' }); elementLowerBandSeries.setData(elementLowerBandData); @@ -1828,7 +1723,7 @@ function chartTvRenderOverlays(ctx) { lineWidth: 1, lastValueVisible: false, priceLineVisible: false, - title: '次周期布林中轨' + title: '次布林中轨' }); elementMiddleBandSeries.setData(elementMiddleBandData); @@ -1943,7 +1838,7 @@ function chartTvRenderOverlays(ctx) { const fxMarker = { time: timestamp, tooltip: `