Compare commits
4
Commits
6b72ba226b
..
chan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b1b500108 | ||
|
|
c22cd48f36 | ||
|
|
b301ad22c9 | ||
|
|
2cd50f1e01 |
@@ -49,6 +49,7 @@ research/out/run_meta_*.json
|
||||
|
||||
# Telegram 凭据。**不要提交**
|
||||
research/live/deploy/tg.env
|
||||
research/.tg.env
|
||||
|
||||
# 生产状态与信号总线。刻意放在仓库外(LIVE_HOME / BUS_DIR),这几条只防
|
||||
# 有人把它们指回仓库里:里面是日亏损累计与已处理信号键,被 git clean
|
||||
|
||||
@@ -43,6 +43,29 @@ class ChanSEG():
|
||||
self.macd_hist = macd_hist
|
||||
def set_macd_div(self, macd_div):
|
||||
self.macd_div = macd_div
|
||||
def cal_macdhist(self):
|
||||
# 线段面积 = 同向笔 MACD 柱面积之和(与笔面积口径一致)
|
||||
acc = 0.0
|
||||
seg_dir_name = getattr(self.dir, 'name', None)
|
||||
for bi in self.bi_list:
|
||||
if bi is None:
|
||||
continue
|
||||
if getattr(getattr(bi, 'dir', None), 'name', None) != seg_dir_name:
|
||||
continue
|
||||
acc += float(bi.macd_hist or 0)
|
||||
self.macd_hist = acc
|
||||
return acc
|
||||
def cal_macd_div(self):
|
||||
# 与前一个同向线段比面积:seg.pre 是反向邻段,pre.pre 才是同向
|
||||
self.macd_div = 0.0
|
||||
prev = self.pre.pre if self.pre and self.pre.pre else None
|
||||
if prev is None:
|
||||
return 0.0
|
||||
prev_hist = float(prev.macd_hist or 0)
|
||||
if prev_hist == 0:
|
||||
return 0.0
|
||||
self.macd_div = float(self.macd_hist or 0) / prev_hist
|
||||
return self.macd_div
|
||||
def set_end_bi(self, bi: ChanBI, sure_bi: ChanBI):
|
||||
self.end_bi = bi
|
||||
if bi and bi.is_sure:
|
||||
|
||||
@@ -668,13 +668,22 @@ class BiBuilderMixin:
|
||||
return bi_list
|
||||
|
||||
def check_top_fx(self, last_bottom, klc):
|
||||
if (last_bottom.high > klc.pre.low or last_bottom.high > klc.next.low) and (klc.index - last_bottom.index < 100):
|
||||
#严格笔
|
||||
#last_bottom_high = max(last_bottom.high, last_bottom.pre.high, last_bottom.next.high)
|
||||
#缠论原著笔
|
||||
last_bottom_high = last_bottom.high
|
||||
if (last_bottom_high > klc.pre.low or last_bottom_high > klc.next.low) and (klc.index - last_bottom.index < 100):
|
||||
#print(klc.end_time, "check_top_fx False")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_bottom_fx(self, last_top, klc):
|
||||
if (last_top.low < klc.pre.high or last_top.low < klc.next.high) and (klc.index - last_top.index < 100):
|
||||
#严格笔
|
||||
#last_top_low = min(last_top.low, last_top.pre.low, last_top.next.low)
|
||||
#缠论原著笔
|
||||
last_top_low = last_top.low
|
||||
if (last_top_low < klc.pre.high or last_top_low < klc.next.high) and (klc.index - last_top.index < 100):
|
||||
return False
|
||||
return True
|
||||
# 线段内的中枢
|
||||
|
||||
@@ -60,8 +60,8 @@ class IndicatorsBuilderMixin:
|
||||
30 多列的开销比全部 TA 计算本身还大(2001 行实测 TA 合计 2.5ms,
|
||||
逐列赋值 3.6ms)。增量路径每根都要走一遍,这笔开销是白付的。
|
||||
"""
|
||||
fast = 26
|
||||
slow = 52
|
||||
fast = 12
|
||||
slow = 26
|
||||
period = 9
|
||||
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
||||
bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 已 superseded
|
||||
|
||||
v0 改走免费交易所,由 **data_provider** 拉,chan 只从中转取。
|
||||
|
||||
见 [PROMOTE_deriv_relay.md](./PROMOTE_deriv_relay.md)。
|
||||
|
||||
CoinGlass 付费档等看完交易所效果再开,不要和本阶段混做。
|
||||
@@ -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 线不受影响。
|
||||
+21
-9
@@ -108,7 +108,8 @@ def analyze():
|
||||
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
|
||||
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
|
||||
'direction': convert_direction(bi.dir),
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0,
|
||||
'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0
|
||||
} for bi in analysis_result['bi_list'] if bi.is_sure],
|
||||
# 添加未完成笔列表
|
||||
'uncompleted_bi_list': [{
|
||||
@@ -118,7 +119,8 @@ def analyze():
|
||||
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
|
||||
'end_price': bi.end_klc.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, # 未完成笔没有结束价格
|
||||
'direction': convert_direction(bi.dir),
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0,
|
||||
'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0
|
||||
} for bi in analysis_result['bi_list'] if not bi.is_sure],
|
||||
'seg_list': [{
|
||||
'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(),
|
||||
@@ -126,7 +128,9 @@ def analyze():
|
||||
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
|
||||
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
|
||||
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
|
||||
'direction': convert_direction(seg.dir)
|
||||
'direction': convert_direction(seg.dir),
|
||||
'macd_div': float(getattr(seg, 'macd_div', 0) or 0),
|
||||
'macd_hist': float(getattr(seg, 'macd_hist', 0) or 0)
|
||||
} for seg in analysis_result['seg_list'] if seg.is_sure],
|
||||
# 添加未完成线段列表
|
||||
'uncompleted_seg_list': get_uncompleted_seg_list(analysis_result['seg_list'], client_tz),
|
||||
@@ -305,7 +309,8 @@ def analyze():
|
||||
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
|
||||
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
|
||||
'direction': convert_direction(bi.dir),
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0,
|
||||
'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0
|
||||
} for bi in element_analysis['bi_list'] if bi.is_sure]
|
||||
# 添加次周期未完成笔列表
|
||||
result['element_uncompleted_bi_list'] = [{
|
||||
@@ -315,7 +320,8 @@ def analyze():
|
||||
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
|
||||
'end_price': bi.end_klc.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, # 未完成笔没有结束价格
|
||||
'direction': convert_direction(bi.dir),
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0,
|
||||
'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0
|
||||
} for bi in element_analysis['bi_list'] if not bi.is_sure]
|
||||
|
||||
# 添加小周期K线数据
|
||||
@@ -341,7 +347,9 @@ def analyze():
|
||||
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
|
||||
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
|
||||
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
|
||||
'direction': convert_direction(seg.dir)
|
||||
'direction': convert_direction(seg.dir),
|
||||
'macd_div': float(getattr(seg, 'macd_div', 0) or 0),
|
||||
'macd_hist': float(getattr(seg, 'macd_hist', 0) or 0)
|
||||
} for seg in element_analysis['seg_list'] if seg.is_sure]
|
||||
# 添加次周期未完成线段列表
|
||||
result['element_uncompleted_seg_list'] = get_uncompleted_seg_list(element_analysis['seg_list'], client_tz)
|
||||
@@ -446,7 +454,8 @@ def analyze():
|
||||
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
|
||||
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
|
||||
'direction': convert_direction(bi.dir),
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0,
|
||||
'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0
|
||||
} for bi in sub_sub_analysis['bi_list'] if bi.is_sure]
|
||||
result['sub_sub_uncompleted_bi_list'] = [{
|
||||
'start_time': bi.start_klc.end_time if isinstance(bi.start_klc.end_time, str) else bi.start_klc.end_time.astimezone(client_tz).isoformat(),
|
||||
@@ -455,7 +464,8 @@ def analyze():
|
||||
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
|
||||
'end_price': bi.end_klc.low if convert_direction(bi.dir) == 1 else bi.end_klc.high,
|
||||
'direction': convert_direction(bi.dir),
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
|
||||
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0,
|
||||
'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0
|
||||
} for bi in sub_sub_analysis['bi_list'] if not bi.is_sure]
|
||||
# 次次周期 KLC 列表
|
||||
result['sub_sub_klc_list'] = [{
|
||||
@@ -477,7 +487,9 @@ def analyze():
|
||||
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
|
||||
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
|
||||
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
|
||||
'direction': convert_direction(seg.dir)
|
||||
'direction': convert_direction(seg.dir),
|
||||
'macd_div': float(getattr(seg, 'macd_div', 0) or 0),
|
||||
'macd_hist': float(getattr(seg, 'macd_hist', 0) or 0)
|
||||
} for seg in sub_sub_analysis['seg_list'] if seg.is_sure]
|
||||
result['sub_sub_uncompleted_seg_list'] = get_uncompleted_seg_list(sub_sub_analysis['seg_list'], client_tz)
|
||||
result['sub_sub_zs_list'] = [{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -48,6 +48,10 @@ def analyze_chan(df, symbol=None, timeframe=None):
|
||||
for bi in bi_list:
|
||||
bi.cal_macd_div()
|
||||
#print(bi.start_time, bi.macd_hist, bi.macd_div)
|
||||
for seg in seg_list:
|
||||
seg.cal_macdhist()
|
||||
for seg in seg_list:
|
||||
seg.cal_macd_div()
|
||||
|
||||
# 添加ChanMACD分析(复用 get_klc_list 内已算好的结果,避免同周期二次全量分析)
|
||||
chan_macd = None
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -98,8 +98,14 @@ def serialize_chan_macd_data(chan_macd_data, client_tz):
|
||||
# 序列化unittf_list(兼容新结构与枚举类型)
|
||||
for unittf in chan_macd_data.get('unittf_list', []):
|
||||
try:
|
||||
dir_value = getattr(unittf, 'uinttf_dir', None)
|
||||
dir_value = getattr(unittf, 'unittf_dir', None)
|
||||
dir_name = getattr(dir_value, 'name', dir_value if isinstance(dir_value, str) else None)
|
||||
if dir_name == 'ABOVE':
|
||||
dir_num = 1
|
||||
elif dir_name == 'UNDER':
|
||||
dir_num = -1
|
||||
else:
|
||||
dir_num = 0
|
||||
start_t = getattr(unittf, 'start_type', None)
|
||||
start_type = getattr(start_t, 'name', start_t)
|
||||
end_t = getattr(unittf, 'end_type', None)
|
||||
@@ -114,7 +120,7 @@ def serialize_chan_macd_data(chan_macd_data, client_tz):
|
||||
unittf_data = {
|
||||
'start_time': format_time_safely(getattr(unittf, 'start_time', None), client_tz),
|
||||
'end_time': format_time_safely(getattr(unittf, 'end_time', None), client_tz) if getattr(unittf, 'end_time', None) else None,
|
||||
'dir': dir_name, # 'ABOVE' | 'UNDER' | None
|
||||
'dir': dir_num,
|
||||
'start_type': start_type, # e.g. 'START' | 'CROSS0' | 'NEAR0_UP' | 'NEAR0_DOWN'
|
||||
'end_type': end_type,
|
||||
'invalid': getattr(unittf, 'invalid', False),
|
||||
@@ -307,7 +313,9 @@ def get_uncompleted_seg_list(seg_list, client_tz):
|
||||
'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(),
|
||||
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
|
||||
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
|
||||
'direction': convert_direction(seg.dir)
|
||||
'direction': convert_direction(seg.dir),
|
||||
'macd_div': float(getattr(seg, 'macd_div', 0) or 0),
|
||||
'macd_hist': float(getattr(seg, 'macd_hist', 0) or 0)
|
||||
}
|
||||
|
||||
if is_last:
|
||||
|
||||
@@ -46,6 +46,7 @@ DEFAULT_TIMEFRAME_LABELS = OrderedDict([
|
||||
("5m", "5分钟"),
|
||||
("15m", "15分钟"),
|
||||
("30m", "30分钟"),
|
||||
("45m", "45分钟"),
|
||||
("1h", "1小时"),
|
||||
("2h", "2小时"),
|
||||
("4h", "4小时"),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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(`当前显示的是<strong>${selectedPeriod} (${timeframe})</strong> 数据`);
|
||||
|
||||
@@ -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(`当前显示的是<strong>次次周期 (${subSubTimeframe})</strong> 数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>次次 (${subSubTimeframe})</strong> 数据`);
|
||||
} else if (useElementPeriod && data.element_kline_data && data.element_kline_data.length > 0) {
|
||||
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>次 (${elementTimeframe})</strong> 数据`);
|
||||
} else {
|
||||
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>主 (${mainTimeframe})</strong> 数据`);
|
||||
}
|
||||
});
|
||||
|
||||
$('#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(`当前显示的是<strong>次次周期 (${subSubTimeframe})</strong> 笔数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>次次 (${subSubTimeframe})</strong> 笔数据`);
|
||||
} else if (useElementPeriod && data.element_bi_list && data.element_bi_list.length > 0) {
|
||||
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 笔数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>次 (${elementTimeframe})</strong> 笔数据`);
|
||||
} else {
|
||||
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 笔数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>主 (${mainTimeframe})</strong> 笔数据`);
|
||||
}
|
||||
});
|
||||
|
||||
$('#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(`当前显示的是<strong>次次周期 (${subSubTimeframe})</strong> 线段数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>次次 (${subSubTimeframe})</strong> 线段数据`);
|
||||
} else if (useElementPeriod && data.element_seg_list && data.element_seg_list.length > 0) {
|
||||
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 线段数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>次 (${elementTimeframe})</strong> 线段数据`);
|
||||
} else {
|
||||
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 线段数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>主 (${mainTimeframe})</strong> 线段数据`);
|
||||
}
|
||||
});
|
||||
|
||||
$('#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(`当前显示的是<strong>次次周期 (${subSubTimeframe})</strong> 中枢数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>次次 (${subSubTimeframe})</strong> 中枢数据`);
|
||||
} else if (useElementPeriod && data.element_zs_list && data.element_zs_list.length > 0) {
|
||||
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 中枢数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>次 (${elementTimeframe})</strong> 中枢数据`);
|
||||
} else {
|
||||
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 中枢数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>主 (${mainTimeframe})</strong> 中枢数据`);
|
||||
}
|
||||
});
|
||||
|
||||
$('#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(`当前显示的是<strong>次次周期 (${subSubTimeframe})</strong> 买卖点数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>次次 (${subSubTimeframe})</strong> 买卖点数据`);
|
||||
} else if (useElementPeriod && data.element_trade_points && data.element_trade_points.length > 0) {
|
||||
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 买卖点数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>次 (${elementTimeframe})</strong> 买卖点数据`);
|
||||
} else {
|
||||
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 买卖点数据`);
|
||||
$('#dataSourceText').html(`当前显示的是<strong>主 (${mainTimeframe})</strong> 买卖点数据`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ function initTradingView(symbol, timeframe) {
|
||||
chartTvRenderChan(ctx);
|
||||
chartTvRenderOverlays(ctx);
|
||||
chartTvFinalize(ctx);
|
||||
if (window.ChanDeriv) ChanDeriv.loadOverlays();
|
||||
|
||||
console.log('图表初始化完成');
|
||||
} catch (e) {
|
||||
|
||||
@@ -178,38 +178,6 @@ function chartTvRenderChan(ctx) {
|
||||
color: bi.direction === 1 ? '#dc3545' : '#28a745',
|
||||
lineWidth: 1
|
||||
});
|
||||
|
||||
// 在笔的末端添加macd_div值标记
|
||||
if (bi.macd_div && bi.macd_div !== 0 && $('#showMainMacdDiv').is(':checked')) {
|
||||
console.log(`添加主周期macd_div标记: ${bi.macd_div.toFixed(2)}, 在时间点: ${endTime}`);
|
||||
|
||||
const macdDivLabel = mainChart.addLineSeries({
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
color: 'transparent', // 设置为透明色
|
||||
lineWidth: 0, // 线宽为0
|
||||
});
|
||||
|
||||
// 添加一个透明的数据点用于承载标记
|
||||
macdDivLabel.setData([
|
||||
{ time: endTime, value: endPrice }
|
||||
]);
|
||||
|
||||
// 主周期MACD背离标记根据笔方向显示,远离K线避免与分型重叠
|
||||
const markerPosition = bi.direction === 1 ? 'aboveBar' : 'belowBar';
|
||||
const textColor = bi.macd_div > 0 ? '#dc3545' : '#28a745';
|
||||
|
||||
// 只使用标记,不添加数据点
|
||||
macdDivLabel.setMarkers([
|
||||
{
|
||||
time: endTime,
|
||||
position: markerPosition,
|
||||
color: textColor,
|
||||
text: `${bi.macd_div.toFixed(2)}`, // 添加M前缀区分
|
||||
size: 0.6, // 更小的尺寸,远离分型标记
|
||||
}
|
||||
]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('主周期笔处理出错:', e);
|
||||
}
|
||||
@@ -260,38 +228,6 @@ function chartTvRenderChan(ctx) {
|
||||
color: bi.direction === 1 ? '#9c27b0' : '#673ab7',
|
||||
lineWidth: 1
|
||||
});
|
||||
|
||||
// 在笔的末端添加macd_div值标记
|
||||
if (bi.macd_div && bi.macd_div !== 0 && $('#showElementMacdDiv').is(':checked')) {
|
||||
console.log(`添加元素周期macd_div标记: ${bi.macd_div.toFixed(2)}, 在时间点: ${endTime}`);
|
||||
|
||||
const macdDivLabel = mainChart.addLineSeries({
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
color: 'transparent', // 设置为透明色
|
||||
lineWidth: 0, // 线宽为0
|
||||
});
|
||||
|
||||
// 添加一个透明的数据点用于承载标记
|
||||
macdDivLabel.setData([
|
||||
{ time: endTime, value: endPrice }
|
||||
]);
|
||||
|
||||
// 次周期MACD背离标记使用不同位置,进一步避免重叠
|
||||
const markerPosition = bi.direction === 1 ? 'aboveBar' : 'belowBar';
|
||||
const textColor = bi.macd_div > 0 ? '#9c27b0' : '#673ab7';
|
||||
|
||||
// 只使用标记,不添加数据点
|
||||
macdDivLabel.setMarkers([
|
||||
{
|
||||
time: endTime,
|
||||
position: markerPosition,
|
||||
color: textColor,
|
||||
text: `${bi.macd_div.toFixed(2)}`, // 添加E前缀区分次周期
|
||||
size: 0.4, // 更小的尺寸,让分型标记有更多空间
|
||||
}
|
||||
]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('次周期笔处理出错:', e);
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
/* chart_tv_indicators.js — volume / ATR / ChanMACD */
|
||||
|
||||
function formatSdMarkerText(separateDiv) {
|
||||
const n = Number(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;
|
||||
@@ -141,11 +180,11 @@ function chartTvRenderIndicators(ctx) {
|
||||
|
||||
// 使用与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数据
|
||||
const macdDataSource = useSubSubPeriod
|
||||
? (currentData.sub_sub_macd || currentData.macd)
|
||||
: (useElementPeriod ? (currentData.element_macd || currentData.macd) : currentData.macd);
|
||||
|
||||
console.log('MACD数据源选择:', useElementPeriod ? '次周期' : '主周期');
|
||||
console.log('MACD数据源选择:', useSubSubPeriod ? '次次周期' : (useElementPeriod ? '次周期' : '主周期'));
|
||||
console.log('K线数据长度:', klineDataSource.length);
|
||||
console.log('MACD数据:', macdDataSource);
|
||||
|
||||
@@ -209,7 +248,7 @@ function chartTvRenderIndicators(ctx) {
|
||||
const chanMacdLineSeries = chanMacdChart.addLineSeries({
|
||||
color: '#2962FF',
|
||||
lineWidth: 1,
|
||||
title: 'ChanMACD',
|
||||
title: 'MACD',
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
});
|
||||
@@ -370,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;
|
||||
@@ -387,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: `SD${Number(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 });
|
||||
@@ -409,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: `SD${Number(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 });
|
||||
@@ -427,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: `SD${Number(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 });
|
||||
@@ -484,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;
|
||||
@@ -494,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: `SD${Number(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 });
|
||||
@@ -519,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: `SD${Number(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 });
|
||||
@@ -543,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: `SD${Number(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 });
|
||||
@@ -555,6 +607,9 @@ function chartTvRenderIndicators(ctx) {
|
||||
});
|
||||
}
|
||||
window.kluDivMarkersSubSub = subSubMarkersAll;
|
||||
if (typeof refreshUnittfOverlayFromData === 'function') {
|
||||
refreshUnittfOverlayFromData(currentData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('独立计算 KLU 背驰标记出错:', e);
|
||||
window.kluDivMarkersMain = [];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -82,6 +82,97 @@ var SUB_SUB_KLC_TREND_STYLE = {
|
||||
UNKNOWN: { position: 'inBar', color: '#004d40', shape: 'square', size: 0.5 }
|
||||
};
|
||||
|
||||
function pushAreaTextLabel(out, item, style, text) {
|
||||
if (!item || !item.end_time || !text) return;
|
||||
var ts = Math.floor(new Date(item.end_time).getTime() / 1000);
|
||||
if (isNaN(ts)) return;
|
||||
var price = Number(item.end_price);
|
||||
if (!isFinite(price)) price = Number(item.start_price);
|
||||
if (!isFinite(price)) return;
|
||||
var up = Number(item.direction) === 1;
|
||||
out.push({
|
||||
time: ts,
|
||||
price: price,
|
||||
text: text,
|
||||
color: style.color,
|
||||
above: up
|
||||
});
|
||||
}
|
||||
|
||||
function formatMacdAreaText(v) {
|
||||
var n = Number(v);
|
||||
if (!isFinite(n) || n === 0) return '';
|
||||
var abs = Math.abs(n);
|
||||
if (abs >= 100) return n.toFixed(0);
|
||||
if (abs >= 10) return n.toFixed(1);
|
||||
return n.toFixed(2);
|
||||
}
|
||||
|
||||
function pushAreaHistMarker(out, item, style) {
|
||||
pushAreaTextLabel(out, item, style, formatMacdAreaText(item && item.macd_hist));
|
||||
}
|
||||
|
||||
function collectAreaHistMarkers(biList, uncompletedBi, segList, uncompletedSeg, biStyle, segStyle, showBi, showSeg) {
|
||||
var out = [];
|
||||
if (showBi) {
|
||||
(biList || []).forEach(function (bi) { pushAreaHistMarker(out, bi, biStyle); });
|
||||
(uncompletedBi || []).forEach(function (bi) { pushAreaHistMarker(out, bi, biStyle); });
|
||||
}
|
||||
if (showSeg) {
|
||||
(segList || []).forEach(function (seg) { pushAreaHistMarker(out, seg, segStyle); });
|
||||
(uncompletedSeg || []).forEach(function (seg) { pushAreaHistMarker(out, seg, segStyle); });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildAreaHistMarkersFromData(data) {
|
||||
var markers = [];
|
||||
if (!data) return markers;
|
||||
var showMainBi = $('#showMainBiArea').is(':checked');
|
||||
var showMainSeg = $('#showMainSegArea').is(':checked');
|
||||
if (showMainBi || showMainSeg) {
|
||||
markers = markers.concat(collectAreaHistMarkers(
|
||||
data.bi_list,
|
||||
data.uncompleted_bi_list,
|
||||
data.seg_list,
|
||||
data.uncompleted_seg_list,
|
||||
{ color: '#1565c0', size: 0.55 },
|
||||
{ color: '#00838f', size: 0.65 },
|
||||
showMainBi,
|
||||
showMainSeg
|
||||
));
|
||||
}
|
||||
var showElementBi = $('#showElementBiArea').is(':checked');
|
||||
var showElementSeg = $('#showElementSegArea').is(':checked');
|
||||
if (showElementBi || showElementSeg) {
|
||||
markers = markers.concat(collectAreaHistMarkers(
|
||||
data.element_bi_list,
|
||||
data.element_uncompleted_bi_list,
|
||||
data.element_seg_list,
|
||||
data.element_uncompleted_seg_list,
|
||||
{ color: '#3949ab', size: 0.5 },
|
||||
{ color: '#5c6bc0', size: 0.6 },
|
||||
showElementBi,
|
||||
showElementSeg
|
||||
));
|
||||
}
|
||||
var showSubSubBi = $('#showSubSubBiArea').is(':checked');
|
||||
var showSubSubSeg = $('#showSubSubSegArea').is(':checked');
|
||||
if (showSubSubBi || showSubSubSeg) {
|
||||
markers = markers.concat(collectAreaHistMarkers(
|
||||
data.sub_sub_bi_list,
|
||||
data.sub_sub_uncompleted_bi_list,
|
||||
data.sub_sub_seg_list,
|
||||
data.sub_sub_uncompleted_seg_list,
|
||||
{ color: '#2e7d32', size: 0.45 },
|
||||
{ color: '#558b2f', size: 0.55 },
|
||||
showSubSubBi,
|
||||
showSubSubSeg
|
||||
));
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
function buildKlcTrendMarker(timeAligned, trendRaw, palette) {
|
||||
var kind = normalizeKlcTrendRaw(trendRaw);
|
||||
var style = palette[kind] || palette.UNKNOWN || palette.FLAT;
|
||||
@@ -136,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') {
|
||||
@@ -163,23 +269,38 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
||||
// LWC 4 无 priceScale 订阅:采样坐标变化(含增量 setData 后自动缩放)
|
||||
var sampleSig = function () {
|
||||
var boxes = window._fxBoxVerticals || [];
|
||||
var labels = window._areaTextLabels || [];
|
||||
var series = getMainPriceSeries();
|
||||
if (!series || !boxes.length) return '0';
|
||||
if (!series || (!boxes.length && !labels.length)) return '0';
|
||||
var ts = mainChart.timeScale();
|
||||
var a = boxes[0];
|
||||
var b = boxes[boxes.length - 1];
|
||||
return [
|
||||
boxes.length,
|
||||
quant(ts.timeToCoordinate(a.time)),
|
||||
quant(series.priceToCoordinate(a.hi)),
|
||||
quant(series.priceToCoordinate(a.lo)),
|
||||
quant(ts.timeToCoordinate(b.time)),
|
||||
quant(series.priceToCoordinate(b.hi)),
|
||||
quant(series.priceToCoordinate(b.lo))
|
||||
].join('|');
|
||||
var parts = [boxes.length, labels.length, quant(plotLeftOffset(mainChart))];
|
||||
if (boxes.length) {
|
||||
var a = boxes[0];
|
||||
var b = boxes[boxes.length - 1];
|
||||
parts.push(
|
||||
quant(ts.timeToCoordinate(a.time)),
|
||||
quant(series.priceToCoordinate(a.hi)),
|
||||
quant(series.priceToCoordinate(a.lo)),
|
||||
quant(ts.timeToCoordinate(b.time)),
|
||||
quant(series.priceToCoordinate(b.hi)),
|
||||
quant(series.priceToCoordinate(b.lo))
|
||||
);
|
||||
}
|
||||
if (labels.length) {
|
||||
var la = labels[0];
|
||||
var lb = labels[labels.length - 1];
|
||||
parts.push(
|
||||
quant(ts.timeToCoordinate(la.time)),
|
||||
quant(series.priceToCoordinate(la.price)),
|
||||
quant(ts.timeToCoordinate(lb.time)),
|
||||
quant(series.priceToCoordinate(lb.price))
|
||||
);
|
||||
}
|
||||
return parts.join('|');
|
||||
};
|
||||
var redraw = function () {
|
||||
var boxes = window._fxBoxVerticals || [];
|
||||
var labels = window._areaTextLabels || [];
|
||||
var series = getMainPriceSeries();
|
||||
var rect = mainChartContainer.getBoundingClientRect();
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
@@ -191,26 +312,41 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
||||
if (!ctx2) return;
|
||||
ctx2.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx2.clearRect(0, 0, rect.width, rect.height);
|
||||
if (!series || !boxes.length) {
|
||||
if (!series || (!boxes.length && !labels.length)) {
|
||||
lastSig = sampleSig();
|
||||
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([]);
|
||||
if (labels.length) {
|
||||
ctx2.font = '11px sans-serif';
|
||||
ctx2.textAlign = 'center';
|
||||
for (var li = 0; li < labels.length; li++) {
|
||||
var lab = labels[li];
|
||||
var lx = ts.timeToCoordinate(lab.time);
|
||||
var ly = series.priceToCoordinate(lab.price);
|
||||
if (lx == null || ly == null) continue;
|
||||
ctx2.fillStyle = lab.color;
|
||||
ctx2.textBaseline = lab.above ? 'bottom' : 'top';
|
||||
ctx2.fillText(lab.text, Math.round(lx + x0), lab.above ? ly - 3 : ly + 3);
|
||||
}
|
||||
}
|
||||
lastSig = sampleSig();
|
||||
};
|
||||
var scheduleRedraw = function () {
|
||||
@@ -257,6 +393,7 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
||||
|
||||
function chartTvRenderOverlays(ctx) {
|
||||
window._fxBoxVerticals = [];
|
||||
window._areaTextLabels = [];
|
||||
var symbol = ctx.symbol;
|
||||
var timeframe = ctx.timeframe;
|
||||
var symbolConfig = ctx.symbolConfig;
|
||||
@@ -990,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` 复选框
|
||||
@@ -1615,7 +1702,7 @@ function chartTvRenderOverlays(ctx) {
|
||||
lineStyle: 2, // 虚线
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
title: '次周期布林上轨'
|
||||
title: '次布林上轨'
|
||||
});
|
||||
elementUpperBandSeries.setData(elementUpperBandData);
|
||||
|
||||
@@ -1626,7 +1713,7 @@ function chartTvRenderOverlays(ctx) {
|
||||
lineStyle: 2, // 虚线
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
title: '次周期布林下轨'
|
||||
title: '次布林下轨'
|
||||
});
|
||||
elementLowerBandSeries.setData(elementLowerBandData);
|
||||
|
||||
@@ -1636,7 +1723,7 @@ function chartTvRenderOverlays(ctx) {
|
||||
lineWidth: 1,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
title: '次周期布林中轨'
|
||||
title: '次布林中轨'
|
||||
});
|
||||
elementMiddleBandSeries.setData(elementMiddleBandData);
|
||||
|
||||
@@ -1751,7 +1838,7 @@ function chartTvRenderOverlays(ctx) {
|
||||
const fxMarker = {
|
||||
time: timestamp,
|
||||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
||||
主周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}<br>
|
||||
主${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}<br>
|
||||
强度分数: ${fx.fx_strength}分<br>
|
||||
强度等级: ${fx.fx_strength_level}<br>
|
||||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||||
@@ -1815,7 +1902,7 @@ function chartTvRenderOverlays(ctx) {
|
||||
const fxMarker = {
|
||||
time: timestamp,
|
||||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
||||
主周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}<br>
|
||||
主${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}<br>
|
||||
强度分数: ${fx.fx_strength}分<br>
|
||||
强度等级: ${fx.fx_strength_level}<br>
|
||||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||||
@@ -1852,6 +1939,14 @@ function chartTvRenderOverlays(ctx) {
|
||||
window.mainFxMarkers = [];
|
||||
window.fxMarkers = [];
|
||||
}
|
||||
window._areaTextLabels = alignMarkersToCandles(
|
||||
buildAreaHistMarkersFromData(currentData),
|
||||
candles
|
||||
);
|
||||
if (typeof window._redrawFxBoxVerticalOverlay === 'function') {
|
||||
window._redrawFxBoxVerticalOverlay();
|
||||
}
|
||||
|
||||
// 绘制小周期分型标记(含次次周期)
|
||||
if (($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) ||
|
||||
($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) ||
|
||||
@@ -1940,7 +2035,7 @@ function chartTvRenderOverlays(ctx) {
|
||||
const elementFxMarker = {
|
||||
time: timestamp,
|
||||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
||||
小周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}<br>
|
||||
次${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}<br>
|
||||
强度分数: ${fx.fx_strength}分<br>
|
||||
强度等级: ${fx.fx_strength_level}<br>
|
||||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||||
@@ -1996,7 +2091,7 @@ function chartTvRenderOverlays(ctx) {
|
||||
const elementFxMarker = {
|
||||
time: timestamp,
|
||||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
||||
小周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}<br>
|
||||
次${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}<br>
|
||||
强度分数: ${fx.fx_strength}分<br>
|
||||
强度等级: ${fx.fx_strength_level}<br>
|
||||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||||
@@ -2157,7 +2252,7 @@ function chartTvRenderOverlays(ctx) {
|
||||
trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers);
|
||||
}
|
||||
|
||||
// 合并标记并设置
|
||||
// 合并标记并设置(主图不画 U/穿零轴,只留背驰 SD/CD)
|
||||
const combinedMarkers = [
|
||||
...(window.mainFxMarkers || []),
|
||||
...allElementFxMarkers,
|
||||
@@ -2165,17 +2260,15 @@ function chartTvRenderOverlays(ctx) {
|
||||
...(window.kluDivMarkersElement || []),
|
||||
...(window.kluDivMarkersSubSub || []),
|
||||
...trendMarkersToUse,
|
||||
...(window.bspMarkers || []),
|
||||
...(window.fastBspMarkers || [])
|
||||
...(window.bspMarkers || [])
|
||||
];
|
||||
if (combinedMarkers.length > 0) {
|
||||
console.log(
|
||||
'合并设置', combinedMarkers.length, '个标记(主周期分型:',
|
||||
(window.mainFxMarkers || []).length,
|
||||
'个,小周期分型:', allElementFxMarkers.length,
|
||||
'个,UnitTF:', (window.unittfMarkers || []).length,
|
||||
'个,背驰:', (window.kluDivMarkersMain || []).length,
|
||||
'个,BSP标记:', (window.bspMarkers || []).length,
|
||||
'个,第四类标记:', (window.fastBspMarkers || []).length,
|
||||
'个)'
|
||||
);
|
||||
|
||||
@@ -2277,18 +2370,17 @@ function chartTvRenderOverlays(ctx) {
|
||||
// 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记
|
||||
// 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记,
|
||||
// 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。
|
||||
// 修复:把 BSP 标记一并合并进来。第四类买卖点同理,两处都要带上。
|
||||
// 修复:把 BSP 标记一并合并进来。两处都要带上,否则后面这次 setMarkers 会把买卖点盖掉。
|
||||
const onlyMainAndU = [
|
||||
...(window.mainFxMarkers || []),
|
||||
...(window.kluDivMarkersMain || []),
|
||||
...(window.kluDivMarkersElement || []),
|
||||
...(window.kluDivMarkersSubSub || []),
|
||||
...trendMarkersToUse,
|
||||
...(window.bspMarkers || []),
|
||||
...(window.fastBspMarkers || [])
|
||||
...(window.bspMarkers || [])
|
||||
];
|
||||
if (onlyMainAndU.length > 0) {
|
||||
console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, ',UnitTF:', (window.unittfMarkers || []).length, ')');
|
||||
console.log('仅设置', onlyMainAndU.length, '个主图标记(主周期分型:', (window.mainFxMarkers || []).length, ',背驰:', (window.kluDivMarkersMain || []).length, ')');
|
||||
|
||||
// 根据当前主系列类型设置标记
|
||||
const klineType2 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
|
||||
|
||||
@@ -77,6 +77,7 @@ function chartTvBuildShell(ctx) {
|
||||
mainChart: null,
|
||||
volumeChart: null,
|
||||
macdChart: null,
|
||||
sentimentChart: null,
|
||||
series: {
|
||||
candleSeries: null,
|
||||
lineSeries: null,
|
||||
@@ -125,6 +126,7 @@ function chartTvBuildShell(ctx) {
|
||||
// 是否显示MACD
|
||||
const showMacd = $('#showMacd').is(':checked');
|
||||
const showOriginalKline = $('#showOriginalKline').is(':checked');
|
||||
const showSentiment = ($('#dataSource').val() || 'crypto') === 'crypto' && $('#showDeriv').is(':checked');
|
||||
|
||||
// 创建主图容器
|
||||
const mainChartContainer = document.createElement('div');
|
||||
@@ -153,25 +155,20 @@ function chartTvBuildShell(ctx) {
|
||||
// 如果需要显示MACD,创建MACD容器
|
||||
let macdChartContainer = null;
|
||||
let chanMacdChartContainer = null;
|
||||
if (showMacd) {
|
||||
// 仅显示新的 ChanMACD 图:让其占用原 MACD+ChanMACD 的整体高度
|
||||
// 新布局:主图(40%) → ChanMACD(30%) → 成交量(17.5%) → ATR(12.5%)
|
||||
mainChartContainer.style.height = '40%';
|
||||
|
||||
// 隐藏旧 MACD 容器(不创建)
|
||||
// 创建 ChanMACD 容器占据原 MACD+ChanMACD 高度(30%)
|
||||
let sentimentChartContainer = null;
|
||||
if (showMacd && showSentiment) {
|
||||
mainChartContainer.style.height = '34%';
|
||||
chanMacdChartContainer = document.createElement('div');
|
||||
chanMacdChartContainer.style.width = '100%';
|
||||
chanMacdChartContainer.style.height = '30%';
|
||||
chanMacdChartContainer.style.height = 'calc(22% - 50px)';
|
||||
chanMacdChartContainer.style.position = 'absolute';
|
||||
chanMacdChartContainer.style.top = '40%';
|
||||
chanMacdChartContainer.style.top = '34%';
|
||||
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.textContent = 'MACD';
|
||||
chanMacdWatermark.style.position = 'absolute';
|
||||
chanMacdWatermark.style.top = '4px';
|
||||
chanMacdWatermark.style.left = '8px';
|
||||
@@ -179,31 +176,81 @@ function chartTvBuildShell(ctx) {
|
||||
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%';
|
||||
volumeChartContainer.style.top = 'calc(56% - 50px)';
|
||||
volumeChartContainer.style.height = 'calc(12% + 20px)';
|
||||
atrChartContainer.style.top = 'calc(68% - 30px)';
|
||||
atrChartContainer.style.height = 'calc(10% - 20px)';
|
||||
} else if (showMacd) {
|
||||
mainChartContainer.style.height = '40%';
|
||||
chanMacdChartContainer = document.createElement('div');
|
||||
chanMacdChartContainer.style.width = '100%';
|
||||
chanMacdChartContainer.style.height = 'calc(30% - 50px)';
|
||||
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';
|
||||
const chanMacdWatermark = document.createElement('div');
|
||||
chanMacdWatermark.textContent = 'MACD';
|
||||
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);
|
||||
volumeChartContainer.style.top = 'calc(70% - 50px)';
|
||||
volumeChartContainer.style.height = 'calc(17.5% + 20px)';
|
||||
atrChartContainer.style.top = 'calc(87.5% - 30px)';
|
||||
atrChartContainer.style.height = 'calc(12.5% - 20px)';
|
||||
} else if (showSentiment) {
|
||||
mainChartContainer.style.height = '48%';
|
||||
volumeChartContainer.style.top = '48%';
|
||||
volumeChartContainer.style.height = '14%';
|
||||
atrChartContainer.style.top = '62%';
|
||||
atrChartContainer.style.height = '12%';
|
||||
} else {
|
||||
// 不显示MACD时的高度 - 主图、成交量图和ATR图分配
|
||||
mainChartContainer.style.height = '55%'; // 主图占55%
|
||||
mainChartContainer.style.height = '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%
|
||||
volumeChartContainer.style.height = '22.5%';
|
||||
atrChartContainer.style.top = '77.5%';
|
||||
atrChartContainer.style.height = '22.5%';
|
||||
}
|
||||
if (showSentiment) {
|
||||
sentimentChartContainer = document.createElement('div');
|
||||
sentimentChartContainer.style.width = '100%';
|
||||
sentimentChartContainer.style.position = 'absolute';
|
||||
sentimentChartContainer.style.left = '0';
|
||||
sentimentChartContainer.style.right = '0';
|
||||
sentimentChartContainer.style.borderTop = '1px solid #e0e0e0';
|
||||
if (showMacd) {
|
||||
sentimentChartContainer.style.top = '78%';
|
||||
sentimentChartContainer.style.height = '22%';
|
||||
} else {
|
||||
sentimentChartContainer.style.top = '74%';
|
||||
sentimentChartContainer.style.height = '26%';
|
||||
}
|
||||
const sentimentWatermark = document.createElement('div');
|
||||
sentimentWatermark.textContent = '衍生品 买卖比 / 多空 / 大户 / 费率';
|
||||
sentimentWatermark.style.position = 'absolute';
|
||||
sentimentWatermark.style.top = '4px';
|
||||
sentimentWatermark.style.left = '8px';
|
||||
sentimentWatermark.style.fontSize = '11px';
|
||||
sentimentWatermark.style.color = '#888';
|
||||
sentimentWatermark.style.pointerEvents = 'none';
|
||||
sentimentChartContainer.appendChild(sentimentWatermark);
|
||||
}
|
||||
|
||||
container.appendChild(mainChartContainer);
|
||||
container.appendChild(volumeChartContainer);
|
||||
container.appendChild(atrChartContainer);
|
||||
if (showMacd) {
|
||||
// 只追加新的 ChanMACD 容器
|
||||
container.appendChild(chanMacdChartContainer);
|
||||
}
|
||||
if (showSentiment && sentimentChartContainer) {
|
||||
container.appendChild(sentimentChartContainer);
|
||||
}
|
||||
|
||||
// 防止同步过程中的无限循环(实际同步由 bindSyncEvents 负责)
|
||||
|
||||
@@ -221,6 +268,8 @@ function chartTvBuildShell(ctx) {
|
||||
chartHeight = macdChartContainer ? macdChartContainer.clientHeight : 0;
|
||||
} else if (chartType === 'chanmacd') {
|
||||
chartHeight = chanMacdChartContainer ? chanMacdChartContainer.clientHeight : 0;
|
||||
} else if (chartType === 'sentiment') {
|
||||
chartHeight = sentimentChartContainer ? sentimentChartContainer.clientHeight : 0;
|
||||
} else {
|
||||
chartHeight = mainChartContainer.clientHeight;
|
||||
}
|
||||
@@ -376,9 +425,26 @@ function chartTvBuildShell(ctx) {
|
||||
// 创建MACD图表(如果需要):仅创建新的 ChanMACD 图
|
||||
let macdChart = null;
|
||||
let chanMacdChart = null;
|
||||
let sentimentChart = null;
|
||||
if (showMacd) {
|
||||
chanMacdChart = LightweightCharts.createChart(chanMacdChartContainer, createChartOptions(false, 'chanmacd'));
|
||||
}
|
||||
if (showSentiment && sentimentChartContainer) {
|
||||
sentimentChart = LightweightCharts.createChart(sentimentChartContainer, createChartOptions(false, 'sentiment'));
|
||||
if (candles.length) {
|
||||
const axisSeries = sentimentChart.addLineSeries({
|
||||
priceScaleId: '__time',
|
||||
color: 'rgba(0,0,0,0)',
|
||||
lineWidth: 0,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: false
|
||||
});
|
||||
sentimentChart.priceScale('__time').applyOptions({ visible: false });
|
||||
axisSeries.setData(candles.map(function (c) { return { time: c.time, value: 0 }; }));
|
||||
tvWidget.series.sentimentAxisSeries = axisSeries;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建主价格系列并设置数据(支持多种图表类型)
|
||||
(function(){
|
||||
@@ -495,10 +561,13 @@ function chartTvBuildShell(ctx) {
|
||||
ctx.atrChartContainer = atrChartContainer;
|
||||
ctx.macdChartContainer = macdChartContainer;
|
||||
ctx.chanMacdChartContainer = chanMacdChartContainer;
|
||||
ctx.sentimentChartContainer = sentimentChartContainer;
|
||||
ctx.showSentiment = showSentiment;
|
||||
ctx.mainChart = mainChart;
|
||||
ctx.volumeChart = volumeChart;
|
||||
ctx.atrChart = atrChart;
|
||||
ctx.macdChart = macdChart;
|
||||
ctx.chanMacdChart = chanMacdChart;
|
||||
ctx.sentimentChart = sentimentChart;
|
||||
ctx.createChartOptions = createChartOptions;
|
||||
}
|
||||
|
||||
@@ -74,10 +74,10 @@ function readChartFormContext() {
|
||||
return {
|
||||
dataSource: dataSource,
|
||||
symbol: symbol,
|
||||
timeframe: $('#timeframe').val() || window.DEFAULT_MAIN_TIMEFRAME || '4h',
|
||||
timeframe: $('#timeframe').val() || window.DEFAULT_MAIN_TIMEFRAME || '45m',
|
||||
timezone: $('#timezone').val() || 'Asia/Shanghai',
|
||||
elementTimeframe: $('#elementTimeframe').val() || window.DEFAULT_ELEMENT_TIMEFRAME || '1m',
|
||||
subSubTimeframe: $('#subSubTimeframe').val() || '',
|
||||
elementTimeframe: $('#elementTimeframe').val() || window.DEFAULT_ELEMENT_TIMEFRAME || '15m',
|
||||
subSubTimeframe: $('#subSubTimeframe').val() || window.DEFAULT_SUB_SUB_TIMEFRAME || '5m',
|
||||
startTimeMs: $('#start_time').val() ? new Date($('#start_time').val()).getTime() : null,
|
||||
endTimeMs: $('#end_time').val() ? new Date($('#end_time').val()).getTime() : null
|
||||
};
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
/* 资金面 + 情绪面。只打本站中转。OI 叠主图左侧,其余叠情绪副图。 */
|
||||
window.ChanDeriv = (function () {
|
||||
var snapshotReq = 0;
|
||||
var latestReq = 0;
|
||||
var overlayReq = 0;
|
||||
var caches = {};
|
||||
|
||||
var METRICS = [
|
||||
{
|
||||
id: 'oi',
|
||||
checkbox: 'showOi',
|
||||
metric: 'open_interest_history',
|
||||
field: 'open_interest_amount',
|
||||
seriesKey: 'oiSeries',
|
||||
target: 'main',
|
||||
color: 'rgba(123, 31, 162, 0.85)',
|
||||
title: 'OI',
|
||||
priceFormat: { type: 'volume' }
|
||||
},
|
||||
{
|
||||
id: 'taker',
|
||||
checkbox: 'showDeriv',
|
||||
metric: 'taker_buy_sell_ratio',
|
||||
field: 'buy_sell_ratio',
|
||||
seriesKey: 'takerSeries',
|
||||
target: 'sentiment',
|
||||
color: '#0d9488',
|
||||
title: '买卖比',
|
||||
priceFormat: { type: 'price', precision: 3 }
|
||||
},
|
||||
{
|
||||
id: 'lsAccount',
|
||||
checkbox: 'showDeriv',
|
||||
metric: 'long_short_account_ratio',
|
||||
field: 'long_short_ratio',
|
||||
seriesKey: 'lsAccountSeries',
|
||||
target: 'sentiment',
|
||||
color: '#2563eb',
|
||||
title: '多空',
|
||||
priceFormat: { type: 'price', precision: 3 }
|
||||
},
|
||||
{
|
||||
id: 'lsTop',
|
||||
checkbox: 'showDeriv',
|
||||
metric: 'top_long_short_position_ratio',
|
||||
field: 'long_short_ratio',
|
||||
seriesKey: 'lsTopSeries',
|
||||
target: 'sentiment',
|
||||
color: '#ea580c',
|
||||
title: '大户',
|
||||
priceFormat: { type: 'price', precision: 3 }
|
||||
},
|
||||
{
|
||||
id: 'funding',
|
||||
checkbox: 'showDeriv',
|
||||
metric: 'funding_rate_history',
|
||||
field: 'funding_rate',
|
||||
seriesKey: 'fundingHistSeries',
|
||||
target: 'sentiment',
|
||||
color: '#7c3aed',
|
||||
title: '费率%',
|
||||
scale: 'funding',
|
||||
mul: 100,
|
||||
priceFormat: { type: 'price', precision: 4 }
|
||||
}
|
||||
];
|
||||
|
||||
function isCrypto() {
|
||||
return ($('#dataSource').val() || 'crypto') === 'crypto';
|
||||
}
|
||||
|
||||
function currentSymbol() {
|
||||
return $('#symbol').val() || 'BTC/USDT:USDT';
|
||||
}
|
||||
|
||||
function fmtOi(n) {
|
||||
if (n == null || !isFinite(Number(n))) return '—';
|
||||
return Number(n).toLocaleString('en-US', { maximumFractionDigits: 1 });
|
||||
}
|
||||
|
||||
function fmtFunding(n) {
|
||||
if (n == null || !isFinite(Number(n))) return '—';
|
||||
return (Number(n) * 100).toFixed(4) + '%';
|
||||
}
|
||||
|
||||
function fmtChg(n) {
|
||||
if (n == null || !isFinite(Number(n))) return '—';
|
||||
var v = Number(n);
|
||||
return (v > 0 ? '+' : '') + v.toFixed(2) + '%';
|
||||
}
|
||||
|
||||
function fmtBasis(n) {
|
||||
if (n == null || !isFinite(Number(n))) return '—';
|
||||
var v = Number(n);
|
||||
return (v > 0 ? '+' : '') + v.toFixed(2);
|
||||
}
|
||||
|
||||
function fmtRatio(n) {
|
||||
if (n == null || !isFinite(Number(n))) return '—';
|
||||
return Number(n).toFixed(3);
|
||||
}
|
||||
|
||||
function setChip(id, text, tone) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.textContent = text;
|
||||
el.classList.remove('up', 'down');
|
||||
if (tone) el.classList.add(tone);
|
||||
}
|
||||
|
||||
function tone(n, invert) {
|
||||
if (!isFinite(n) || n === 0) return null;
|
||||
var up = n > 0;
|
||||
if (invert) up = !up;
|
||||
return up ? 'up' : 'down';
|
||||
}
|
||||
|
||||
function renderDerivEmpty() {
|
||||
setChip('derivOi', 'OI —');
|
||||
setChip('derivOiChg', 'Δ —');
|
||||
setChip('derivFunding', '费率 —');
|
||||
setChip('derivBasis', '基差 —');
|
||||
var src = document.getElementById('derivSrc');
|
||||
if (src) src.textContent = '';
|
||||
}
|
||||
|
||||
function renderSentimentEmpty() {
|
||||
setChip('derivTaker', '买卖比 —');
|
||||
setChip('derivLs', '多空 —');
|
||||
setChip('derivTop', '大户 —');
|
||||
}
|
||||
|
||||
function setVisible(on) {
|
||||
var bar = document.getElementById('derivBar');
|
||||
var wrap = document.getElementById('showSentimentWrap');
|
||||
if (bar) bar.style.display = on ? '' : 'none';
|
||||
if (wrap) wrap.style.display = on ? '' : 'none';
|
||||
if (!on) clearAllSeries();
|
||||
}
|
||||
|
||||
function activeKlines() {
|
||||
var data = (typeof currentData !== 'undefined') ? currentData : null;
|
||||
if (!data) return [];
|
||||
if ($('#subSubPeriodKline').is(':checked') && data.sub_sub_kline_data) return data.sub_sub_kline_data;
|
||||
if ($('#elementPeriodKline').is(':checked') && data.element_kline_data) return data.element_kline_data;
|
||||
return data.kline_data || [];
|
||||
}
|
||||
|
||||
function klineRangeMs() {
|
||||
var rows = activeKlines();
|
||||
if (!rows.length) return null;
|
||||
var start = new Date(rows[0].date).getTime();
|
||||
var end = new Date(rows[rows.length - 1].date).getTime();
|
||||
if (!isFinite(start) || !isFinite(end)) return null;
|
||||
return { start: start, end: end };
|
||||
}
|
||||
|
||||
function toPoints(rows, field, mul) {
|
||||
var out = [];
|
||||
var lastT = null;
|
||||
var factor = mul || 1;
|
||||
(rows || []).forEach(function (row) {
|
||||
var ms = Number(row.timestamp);
|
||||
var val = Number(row[field]);
|
||||
if (!isFinite(ms) || !isFinite(val)) return;
|
||||
var t = Math.floor(ms / 1000);
|
||||
var v = val * factor;
|
||||
if (lastT === t) {
|
||||
out[out.length - 1].value = v;
|
||||
return;
|
||||
}
|
||||
lastT = t;
|
||||
out.push({ time: t, value: v });
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function candleTimesSec() {
|
||||
var rows = activeKlines();
|
||||
var times = [];
|
||||
(rows || []).forEach(function (k) {
|
||||
var t = Math.floor(new Date(k.date).getTime() / 1000);
|
||||
if (isFinite(t)) times.push(t);
|
||||
});
|
||||
return times;
|
||||
}
|
||||
|
||||
function alignToTimes(src, times) {
|
||||
if (!src || !src.length || !times || !times.length) return [];
|
||||
var out = [];
|
||||
var j = 0;
|
||||
var lastVal;
|
||||
for (var i = 0; i < times.length; i++) {
|
||||
var t = times[i];
|
||||
while (j < src.length && src[j].time <= t) {
|
||||
lastVal = src[j].value;
|
||||
j++;
|
||||
}
|
||||
if (lastVal !== undefined) out.push({ time: t, value: lastVal });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function syncSentimentTime() {
|
||||
if (!tvWidget || !tvWidget.mainChart || !tvWidget.sentimentChart) return;
|
||||
try {
|
||||
var vr = tvWidget.mainChart.timeScale().getVisibleRange();
|
||||
var lr = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
|
||||
var opts = tvWidget.mainChart.timeScale().options ? tvWidget.mainChart.timeScale().options() : null;
|
||||
if (opts) {
|
||||
tvWidget.sentimentChart.timeScale().applyOptions({
|
||||
barSpacing: opts.barSpacing,
|
||||
rightOffset: opts.rightOffset
|
||||
});
|
||||
}
|
||||
if (vr) tvWidget.sentimentChart.timeScale().setVisibleRange(vr);
|
||||
if (lr) tvWidget.sentimentChart.timeScale().setVisibleLogicalRange(lr);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function targetChart(spec) {
|
||||
if (!tvWidget) return null;
|
||||
if (spec.target === 'main') return tvWidget.mainChart;
|
||||
return tvWidget.sentimentChart || null;
|
||||
}
|
||||
|
||||
function safeRemove(chart, seriesKey) {
|
||||
var series = tvWidget && tvWidget.series && tvWidget.series[seriesKey];
|
||||
if (series && chart) {
|
||||
try { chart.removeSeries(series); } catch (e) {}
|
||||
}
|
||||
if (tvWidget && tvWidget.series) tvWidget.series[seriesKey] = null;
|
||||
}
|
||||
|
||||
function clearAllSeries() {
|
||||
METRICS.forEach(function (spec) {
|
||||
safeRemove(targetChart(spec), spec.seriesKey);
|
||||
});
|
||||
safeRemove(tvWidget && tvWidget.sentimentChart, 'sentimentBaseSeries');
|
||||
try {
|
||||
if (tvWidget && tvWidget.mainChart) {
|
||||
tvWidget.mainChart.applyOptions({ leftPriceScale: { visible: false } });
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function drawSeries(spec, points) {
|
||||
var chart = targetChart(spec);
|
||||
if (!chart || !points || !points.length) return;
|
||||
safeRemove(chart, spec.seriesKey);
|
||||
if (spec.target === 'main') {
|
||||
chart.applyOptions({
|
||||
leftPriceScale: {
|
||||
visible: true,
|
||||
borderVisible: false,
|
||||
scaleMargins: { top: 0.08, bottom: 0.12 }
|
||||
}
|
||||
});
|
||||
}
|
||||
var opts = {
|
||||
color: spec.color,
|
||||
lineWidth: 1,
|
||||
title: spec.title,
|
||||
lastValueVisible: true,
|
||||
priceLineVisible: false,
|
||||
priceFormat: spec.priceFormat
|
||||
};
|
||||
if (spec.target === 'main') opts.priceScaleId = 'left';
|
||||
if (spec.scale) {
|
||||
opts.priceScaleId = spec.scale;
|
||||
chart.priceScale(spec.scale).applyOptions({
|
||||
scaleMargins: { top: 0.15, bottom: 0.1 },
|
||||
borderVisible: false
|
||||
});
|
||||
}
|
||||
var series = chart.addLineSeries(opts);
|
||||
series.setData(points);
|
||||
tvWidget.series[spec.seriesKey] = series;
|
||||
if (spec.target === 'sentiment') syncSentimentTime();
|
||||
if (spec.target === 'main' && typeof window._redrawFxBoxVerticalOverlay === 'function') {
|
||||
window._redrawFxBoxVerticalOverlay();
|
||||
setTimeout(window._redrawFxBoxVerticalOverlay, 50);
|
||||
}
|
||||
}
|
||||
|
||||
function drawRatioBaseline(points) {
|
||||
var chart = tvWidget && tvWidget.sentimentChart;
|
||||
if (!chart || !points || !points.length) return;
|
||||
safeRemove(chart, 'sentimentBaseSeries');
|
||||
var baseline = points.map(function (p) { return { time: p.time, value: 1 }; });
|
||||
var series = chart.addLineSeries({
|
||||
color: 'rgba(120, 120, 120, 0.45)',
|
||||
lineWidth: 1,
|
||||
lineStyle: 2,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
title: '1.0'
|
||||
});
|
||||
series.setData(baseline);
|
||||
tvWidget.series.sentimentBaseSeries = series;
|
||||
}
|
||||
|
||||
function loadOne(spec, range, symbol, req) {
|
||||
var checked = $('#' + spec.checkbox).is(':checked');
|
||||
var chart = targetChart(spec);
|
||||
if (!checked) {
|
||||
safeRemove(chart, spec.seriesKey);
|
||||
if (spec.id === 'oi') {
|
||||
try {
|
||||
if (tvWidget && tvWidget.mainChart) {
|
||||
tvWidget.mainChart.applyOptions({ leftPriceScale: { visible: false } });
|
||||
}
|
||||
} catch (e) {}
|
||||
if (typeof window._redrawFxBoxVerticalOverlay === 'function') {
|
||||
window._redrawFxBoxVerticalOverlay();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!chart) return;
|
||||
var times = candleTimesSec();
|
||||
var key = [symbol, spec.metric, times[0] || '', times[times.length - 1] || '', times.length].join(':');
|
||||
var cached = caches[spec.id];
|
||||
function paint(raw) {
|
||||
var aligned = spec.target === 'main' || spec.target === 'sentiment' ? alignToTimes(raw, times) : raw;
|
||||
if (!aligned.length) aligned = raw;
|
||||
drawSeries(spec, aligned);
|
||||
if (spec.target === 'sentiment') maybeDrawBaseline();
|
||||
}
|
||||
if (cached && cached.raw && cached.raw.length) {
|
||||
paint(cached.raw);
|
||||
if (cached.key === key) return;
|
||||
}
|
||||
if (!range || !window.ChanApi || !ChanApi.sentimentMetrics) return;
|
||||
ChanApi.sentimentMetrics({
|
||||
metric: spec.metric,
|
||||
symbol: symbol,
|
||||
start: range.start - 8 * 60 * 60 * 1000,
|
||||
end: range.end,
|
||||
limit: 2000
|
||||
}).then(function (payload) {
|
||||
if (req !== overlayReq) return;
|
||||
var raw = toPoints(payload && payload.data, spec.field, spec.mul);
|
||||
if (!raw.length) return;
|
||||
caches[spec.id] = { key: key, raw: raw };
|
||||
if ($('#' + spec.checkbox).is(':checked') && targetChart(spec)) paint(raw);
|
||||
}).catch(function (err) {
|
||||
if (req !== overlayReq) return;
|
||||
console.warn(spec.title + ' 序列不可用', err);
|
||||
});
|
||||
}
|
||||
|
||||
function maybeDrawBaseline() {
|
||||
var times = candleTimesSec();
|
||||
if (!times.length) return;
|
||||
drawRatioBaseline(times.map(function (t) { return { time: t, value: 1 }; }));
|
||||
syncSentimentTime();
|
||||
}
|
||||
|
||||
function loadOverlays() {
|
||||
if (!isCrypto()) {
|
||||
clearAllSeries();
|
||||
return;
|
||||
}
|
||||
if (!tvWidget || !tvWidget.mainChart) return;
|
||||
var range = klineRangeMs();
|
||||
var symbol = currentSymbol();
|
||||
var req = ++overlayReq;
|
||||
METRICS.forEach(function (spec) {
|
||||
loadOne(spec, range, symbol, req);
|
||||
});
|
||||
maybeDrawBaseline();
|
||||
}
|
||||
|
||||
function loadSnapshot() {
|
||||
if (!isCrypto() || !window.ChanApi || !ChanApi.derivatives) return;
|
||||
var req = ++snapshotReq;
|
||||
ChanApi.derivatives({ symbol: currentSymbol() }).then(function (row) {
|
||||
if (req !== snapshotReq) return;
|
||||
var chg = Number(row.oi_change_pct);
|
||||
var fund = Number(row.funding_rate);
|
||||
var basis = Number(row.basis);
|
||||
setChip('derivOi', 'OI ' + fmtOi(row.open_interest));
|
||||
setChip('derivOiChg', 'Δ ' + fmtChg(row.oi_change_pct), tone(chg));
|
||||
setChip('derivFunding', '费率 ' + fmtFunding(row.funding_rate), tone(fund));
|
||||
setChip('derivBasis', '基差 ' + fmtBasis(row.basis), tone(basis));
|
||||
var src = document.getElementById('derivSrc');
|
||||
if (src) src.textContent = row.exchange || '';
|
||||
}).catch(function (err) {
|
||||
if (req !== snapshotReq) return;
|
||||
console.warn('资金面快照不可用', err);
|
||||
renderDerivEmpty();
|
||||
var src = document.getElementById('derivSrc');
|
||||
if (src) src.textContent = '不可用';
|
||||
});
|
||||
}
|
||||
|
||||
function loadLatest() {
|
||||
if (!isCrypto() || !window.ChanApi || !ChanApi.sentimentLatest) return;
|
||||
var req = ++latestReq;
|
||||
ChanApi.sentimentLatest({ symbol: currentSymbol() }).then(function (payload) {
|
||||
if (req !== latestReq) return;
|
||||
var data = (payload && payload.data) || {};
|
||||
var taker = data.taker_buy_sell_ratio || {};
|
||||
var ls = data.long_short_account_ratio || {};
|
||||
var top = data.top_long_short_position_ratio || {};
|
||||
var takerN = Number(taker.buy_sell_ratio);
|
||||
var lsN = Number(ls.long_short_ratio);
|
||||
var topN = Number(top.long_short_ratio);
|
||||
setChip('derivTaker', '买卖比 ' + fmtRatio(taker.buy_sell_ratio), isFinite(takerN) ? (takerN >= 1 ? 'up' : 'down') : null);
|
||||
setChip('derivLs', '多空 ' + fmtRatio(ls.long_short_ratio), isFinite(lsN) ? (lsN >= 1 ? 'up' : 'down') : null);
|
||||
setChip('derivTop', '大户 ' + fmtRatio(top.long_short_ratio), isFinite(topN) ? (topN >= 1 ? 'up' : 'down') : null);
|
||||
}).catch(function (err) {
|
||||
if (req !== latestReq) return;
|
||||
console.warn('情绪快照不可用', err);
|
||||
renderSentimentEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
function sync(opts) {
|
||||
opts = opts || {};
|
||||
var show = isCrypto();
|
||||
setVisible(show);
|
||||
if (!show) return;
|
||||
loadSnapshot();
|
||||
loadLatest();
|
||||
if (opts.overlay !== false) loadOverlays();
|
||||
}
|
||||
|
||||
return {
|
||||
sync: sync,
|
||||
setVisible: setVisible,
|
||||
loadOiOverlay: loadOverlays,
|
||||
loadOverlays: loadOverlays
|
||||
};
|
||||
})();
|
||||
@@ -151,7 +151,7 @@ $('#elementTimeframe').change(function() {
|
||||
|
||||
// 检查选择的元素时间周期是否小于等于主周期
|
||||
if (compareTimeframes(elementTimeframe, mainTimeframe) > 0) {
|
||||
alert('元素时间周期必须小于或等于主图表时间周期。');
|
||||
alert('次必须小于或等于主。');
|
||||
setSmallerOrEqualTimeframe(); // 重置为最大的小于等于时间周期
|
||||
return;
|
||||
}
|
||||
@@ -164,7 +164,7 @@ $('#subSubTimeframe').change(function() {
|
||||
const subSub = $(this).val();
|
||||
const elementTf = $('#elementTimeframe').val();
|
||||
if (compareTimeframes(subSub, elementTf) > 0) {
|
||||
alert('次次周期必须小于或等于次周期。');
|
||||
alert('次次必须小于或等于次。');
|
||||
ensureSubSubLteElement();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,67 @@ function clearChanMacdMarkers() {
|
||||
}
|
||||
// 清空全局UnitTF标记,避免旧数据残留影响主图合并
|
||||
window.unittfMarkers = [];
|
||||
window.unittfMarkersMain = [];
|
||||
window.unittfMarkersElement = [];
|
||||
window.unittfMarkersSubSub = [];
|
||||
}
|
||||
|
||||
function unittfDirSign(dir) {
|
||||
if (dir === 'ABOVE' || dir === 1 || dir === '1' || dir === true) return 1;
|
||||
if (dir === 'UNDER' || dir === -1 || dir === '-1') return -1;
|
||||
const n = Number(dir);
|
||||
if (n > 0) return 1;
|
||||
if (n < 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function buildUnittfOverlayMarkers(unittfList, opt) {
|
||||
const markers = [];
|
||||
if (!Array.isArray(unittfList) || !opt) return markers;
|
||||
const up = opt.up, down = opt.down, prefix = opt.prefix || 'U';
|
||||
const size = opt.size || 0.5;
|
||||
for (let i = 0; i < unittfList.length; i++) {
|
||||
const unittf = unittfList[i];
|
||||
if (!unittf || !unittf.start_time || unittf.invalid) continue;
|
||||
const startTime = Math.floor(new Date(unittf.start_time).getTime() / 1000);
|
||||
if (isNaN(startTime)) continue;
|
||||
const sign = unittfDirSign(unittf.dir);
|
||||
const color = sign > 0 ? up : down;
|
||||
const pos = sign > 0 ? 'aboveBar' : 'belowBar';
|
||||
markers.push({ time: startTime, position: pos, color: color, shape: 'circle', text: prefix + i, size: size });
|
||||
if (unittf.end_time) {
|
||||
const endTime = Math.floor(new Date(unittf.end_time).getTime() / 1000);
|
||||
if (!isNaN(endTime)) {
|
||||
markers.push({ time: endTime, position: pos, color: color, shape: 'circle', text: prefix + i + 'E', size: size });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i + 1 < unittfList.length; i++) {
|
||||
const cur = unittfList[i];
|
||||
const nxt = unittfList[i + 1];
|
||||
if (!cur || !nxt || !cur.end_time || !nxt.start_time) continue;
|
||||
const tEnd = new Date(cur.end_time).getTime();
|
||||
const tStart = new Date(nxt.start_time).getTime();
|
||||
if (!isNaN(tEnd) && tEnd === tStart) {
|
||||
const sign = unittfDirSign(nxt.dir);
|
||||
markers.push({
|
||||
time: Math.floor(tEnd / 1000),
|
||||
position: sign > 0 ? 'aboveBar' : 'belowBar',
|
||||
color: sign > 0 ? (opt.boundaryUp || up) : (opt.boundaryDown || down),
|
||||
shape: 'square',
|
||||
text: prefix + '↔',
|
||||
size: 0.6
|
||||
});
|
||||
}
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
function refreshUnittfOverlayFromData(data) {
|
||||
// U/穿零轴只画在 MACD 副图,不再铺到主图
|
||||
window.unittfMarkersMain = [];
|
||||
window.unittfMarkersElement = [];
|
||||
window.unittfMarkersSubSub = [];
|
||||
}
|
||||
// 添加所有ChanMACD标记
|
||||
function addAllChanMacdMarkers(segList, unittfList, histsetList, stateMarkers) {
|
||||
@@ -215,8 +276,7 @@ function addAllChanMacdMarkers(segList, unittfList, histsetList, stateMarkers) {
|
||||
|
||||
// 保存到全局,供主图与分型一起统一合并绘制(仅在开关开启时)
|
||||
console.log('DEBUG: U 标记数量:', signalMarkers.length);
|
||||
const allowUMerge = (window.showUOnMain && window.showUOnElement);
|
||||
window.unittfMarkers = allowUMerge ? [...signalMarkers, ...boundaryMarkers] : [];
|
||||
window.unittfMarkers = [...signalMarkers, ...boundaryMarkers];
|
||||
if (uTooltipMarkers.length > 0) {
|
||||
if (window.fxMarkers) {
|
||||
window.fxMarkers = [ ...window.fxMarkers, ...uTooltipMarkers ];
|
||||
|
||||
+17
-15
@@ -26,10 +26,10 @@ function loadSymbols() {
|
||||
});
|
||||
}
|
||||
|
||||
// 设置默认时间范围:最近 1 个月
|
||||
// 设置默认时间范围:现在倒退 1 周
|
||||
function setDefaultTimeRange() {
|
||||
const now = new Date();
|
||||
const daysBack = 30;
|
||||
const daysBack = 7;
|
||||
const start = new Date(now.getTime() - (daysBack * 24 * 60 * 60 * 1000));
|
||||
|
||||
// 格式化为datetime-local输入框所需的格式 YYYY-MM-DDThh:mm
|
||||
@@ -72,17 +72,20 @@ $(document).ready(function() {
|
||||
window.astockStatusInterval = null;
|
||||
}
|
||||
loadSymbols();
|
||||
if (window.ChanDeriv) ChanDeriv.setVisible(true);
|
||||
} else if (dataSource === 'a_stock') {
|
||||
$('#cryptoSymbolContainer').hide();
|
||||
$('#astockSymbolContainer').show();
|
||||
loadAStockSymbols();
|
||||
startAStockStatusUpdater();
|
||||
if (window.ChanDeriv) ChanDeriv.setVisible(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 检查初始数据源设置
|
||||
const initialDataSource = $('#dataSource').val();
|
||||
if (window.ChanDeriv) ChanDeriv.setVisible(initialDataSource !== 'a_stock');
|
||||
if (initialDataSource === 'a_stock') {
|
||||
$.getJSON('/api/chart_metadata', { source: 'a_stock' })
|
||||
.done(function(meta) {
|
||||
@@ -404,6 +407,7 @@ function mapTimeframeToInterval(timeframe) {
|
||||
'5m': '5',
|
||||
'15m': '15',
|
||||
'30m': '30',
|
||||
'45m': '45',
|
||||
'1h': '60',
|
||||
'2h': '120',
|
||||
'4h': '240',
|
||||
@@ -547,6 +551,7 @@ function refreshChart(data, options) {
|
||||
if (currentData && currentData.ema52_dict) {
|
||||
updateEMA52Display(currentData);
|
||||
}
|
||||
if (window.ChanDeriv) ChanDeriv.sync({ overlay: false });
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn('增量刷新失败,回退全量重建:', e);
|
||||
@@ -584,6 +589,7 @@ function refreshChart(data, options) {
|
||||
if (currentData && currentData.ema52_dict) {
|
||||
updateEMA52Display(currentData);
|
||||
}
|
||||
if (window.ChanDeriv) ChanDeriv.sync({ overlay: true });
|
||||
|
||||
}
|
||||
|
||||
@@ -597,14 +603,9 @@ function refreshChartOnly() {
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定主周期MACD背离显示开关
|
||||
$('#showMainMacdDiv').change(function() {
|
||||
refreshChartOnly();
|
||||
});
|
||||
|
||||
// 绑定次周期MACD背离显示开关
|
||||
$('#showElementMacdDiv').change(function() {
|
||||
refreshChartOnly();
|
||||
// 绑定主/次/次次周期笔面积、线段面积显示开关
|
||||
$('#showMainBiArea, #showMainSegArea, #showElementBiArea, #showElementSegArea, #showSubSubBiArea, #showSubSubSegArea').change(function() {
|
||||
updateChartDisplay();
|
||||
});
|
||||
|
||||
// 绑定分型类型显示开关(与笔一致:全量重建,避免增量路径标记未对齐)
|
||||
@@ -642,6 +643,12 @@ $('#toggleUOnElement').change(function() {
|
||||
});
|
||||
|
||||
// 买卖点显示开关
|
||||
$('#showOi').change(function() {
|
||||
if (window.ChanDeriv) ChanDeriv.loadOverlays();
|
||||
});
|
||||
$('#showDeriv').change(function() {
|
||||
updateChartDisplay();
|
||||
});
|
||||
$('#showMainBsp').change(function() {
|
||||
updateChartDisplay();
|
||||
});
|
||||
@@ -649,11 +656,6 @@ $('#showElementBsp').change(function() {
|
||||
updateChartDisplay();
|
||||
});
|
||||
|
||||
// 第四类买卖点(B4/S4)显示开关与过滤模式
|
||||
$('#showMainFastBsp, #showElementFastBsp, #showSubSubFastBsp, #fastBspFilterMode').change(function() {
|
||||
updateChartDisplay();
|
||||
});
|
||||
|
||||
// 在控制台输出当前显示状态
|
||||
console.log('当前显示状态:', {
|
||||
'showOriginalKline': $('#showOriginalKline').is(':checked'),
|
||||
|
||||
+103
-44
@@ -858,6 +858,38 @@
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.deriv-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 8px;
|
||||
padding: 6px 10px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.deriv-bar .deriv-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.deriv-bar .deriv-chip {
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #343a40;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
.deriv-bar .deriv-chip.up { color: #198754; }
|
||||
.deriv-bar .deriv-chip.down { color: #dc3545; }
|
||||
.deriv-bar .deriv-src {
|
||||
color: #868e96;
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -935,21 +967,31 @@
|
||||
<!-- 添加K线周期切换 -->
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="klinePeriod" id="mainPeriodKline" checked>
|
||||
<label class="form-check-label" for="mainPeriodKline">主周期</label>
|
||||
<label class="form-check-label" for="mainPeriodKline">主</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="klinePeriod" id="elementPeriodKline">
|
||||
<label class="form-check-label" for="elementPeriodKline">小周期</label>
|
||||
<label class="form-check-label" for="elementPeriodKline">次</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="klinePeriod" id="subSubPeriodKline">
|
||||
<label class="form-check-label" for="subSubPeriodKline">次次周期</label>
|
||||
<label class="form-check-label" for="subSubPeriodKline">次次</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showMacd" checked>
|
||||
<label class="form-check-label" for="showMacd">ChanMACD</label>
|
||||
<label class="form-check-label" for="showMacd">MACD</label>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 p-0" onclick="showMacdConfig()" style="width:22px;height:22px;line-height:1;font-size:12px;" title="MACD参数设置">⚙</button>
|
||||
</div>
|
||||
<span id="showSentimentWrap" class="d-inline-flex align-items-center">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showOi" checked>
|
||||
<label class="form-check-label" for="showOi">OI</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showDeriv" checked>
|
||||
<label class="form-check-label" for="showDeriv">衍生品</label>
|
||||
</div>
|
||||
</span>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<label for="refreshInterval" class="form-label me-2 mb-0">自动刷新:</label>
|
||||
<select id="refreshInterval" class="form-select form-select-sm me-2" style="width: 80px;">
|
||||
@@ -977,7 +1019,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mt-1">
|
||||
<label class="form-label me-0 mb-0">主周期:</label>
|
||||
<label class="form-label me-0 mb-0">主:</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<select id="timeframe" class="form-select form-select-sm me-2" style="width: 100px;">
|
||||
{% for value, label in timeframes.items() %}
|
||||
@@ -998,11 +1040,11 @@
|
||||
<label class="form-check-label" for="showMainZs">SEG中枢</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showMainBiZs">
|
||||
<input class="form-check-input" type="checkbox" id="showMainBiZs" checked>
|
||||
<label class="form-check-label" for="showMainBiZs">BI中枢</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showKlcFxType" checked>
|
||||
<input class="form-check-input" type="checkbox" id="showKlcFxType">
|
||||
<label class="form-check-label" for="showKlcFxType">KLC分型</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
@@ -1010,27 +1052,24 @@
|
||||
<label class="form-check-label" for="showMainTrend">Trend</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="toggleUOnMain">
|
||||
<input class="form-check-input" type="checkbox" id="toggleUOnMain" checked>
|
||||
<label class="form-check-label" for="toggleUOnMain">显示U</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showMainBiArea">
|
||||
<label class="form-check-label" for="showMainBiArea">笔面积</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showMainSegArea">
|
||||
<label class="form-check-label" for="showMainSegArea">线段面积</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showMainBsp">
|
||||
<label class="form-check-label" for="showMainBsp">买卖点</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showMainFastBsp">
|
||||
<label class="form-check-label" for="showMainFastBsp">第四类</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<select id="fastBspFilterMode" class="form-select form-select-sm" style="width: 130px;"
|
||||
title="深色为区间套(大级别分型同向)+中枢顺向推进都满足的信号,浅色为未通过过滤">
|
||||
<option value="all" selected>第四类:全部</option>
|
||||
<option value="filtered">第四类:仅过滤后</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mt-1">
|
||||
<label class="form-label me-0 mb-0">次周期:</label>
|
||||
<label class="form-label me-0 mb-0">次:</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<select id="elementTimeframe" class="form-select form-select-sm me-2" style="width: 100px;">
|
||||
{% for value, label in timeframes.items() %}
|
||||
@@ -1066,17 +1105,21 @@
|
||||
<input class="form-check-input" type="checkbox" id="toggleUOnElement">
|
||||
<label class="form-check-label" for="toggleUOnElement">显示U</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showElementBiArea">
|
||||
<label class="form-check-label" for="showElementBiArea">笔面积</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showElementSegArea">
|
||||
<label class="form-check-label" for="showElementSegArea">线段面积</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showElementBsp">
|
||||
<label class="form-check-label" for="showElementBsp">买卖点</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showElementFastBsp">
|
||||
<label class="form-check-label" for="showElementFastBsp">第四类</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mt-1">
|
||||
<label class="form-label me-0 mb-0">次次周期:</label>
|
||||
<label class="form-label me-0 mb-0">次次:</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<select id="subSubTimeframe" class="form-select form-select-sm me-2" style="width: 100px;">
|
||||
{% for value, label in timeframes.items() %}
|
||||
@@ -1113,12 +1156,16 @@
|
||||
<label class="form-check-label" for="toggleUOnSubSub">显示U</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showSubSubBsp">
|
||||
<label class="form-check-label" for="showSubSubBsp">买卖点</label>
|
||||
<input class="form-check-input" type="checkbox" id="showSubSubBiArea">
|
||||
<label class="form-check-label" for="showSubSubBiArea">笔面积</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showSubSubFastBsp">
|
||||
<label class="form-check-label" for="showSubSubFastBsp">第四类</label>
|
||||
<input class="form-check-input" type="checkbox" id="showSubSubSegArea">
|
||||
<label class="form-check-label" for="showSubSubSegArea">线段面积</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showSubSubBsp">
|
||||
<label class="form-check-label" for="showSubSubBsp">买卖点</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1126,6 +1173,17 @@
|
||||
</div>
|
||||
|
||||
<div id="loadingIndicator" style="display:none;"></div>
|
||||
<div id="derivBar" class="deriv-bar">
|
||||
<span class="deriv-label">资金面</span>
|
||||
<span class="deriv-chip" id="derivOi">OI —</span>
|
||||
<span class="deriv-chip" id="derivOiChg">Δ —</span>
|
||||
<span class="deriv-chip" id="derivFunding">费率 —</span>
|
||||
<span class="deriv-chip" id="derivBasis">基差 —</span>
|
||||
<span class="deriv-chip" id="derivTaker">买卖比 —</span>
|
||||
<span class="deriv-chip" id="derivLs">多空 —</span>
|
||||
<span class="deriv-chip" id="derivTop">大户 —</span>
|
||||
<span class="deriv-src" id="derivSrc"></span>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div id="tradingview_chart"></div>
|
||||
@@ -1278,24 +1336,25 @@
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script defer src="{{ url_for('static', filename='js/app/api_client.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/api_client.js') }}?v=20260910c"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/deriv_ui.js') }}?v=20260911i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/state.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/trend.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/macd_ui.js') }}?v=20260810e"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/macd_ui.js') }}?v=20260911j"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_format.js') }}?v=20260810e"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_view.js') }}?v=20260810e"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_lifecycle.js') }}?v=20260810e"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_shell.js') }}?v=20260810e"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_indicators.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_chan.js') }}?v=20260810d"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_overlays.js') }}?v=20260827a"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_finalize.js') }}?v=20260810a"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260809z"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tables.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260827a"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_view.js') }}?v=20260912a"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_lifecycle.js') }}?v=20260910c"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_shell.js') }}?v=20260911l"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_indicators.js') }}?v=20260911n"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_chan.js') }}?v=20260910a"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_overlays.js') }}?v=20260911j"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_finalize.js') }}?v=20260910c"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260910c"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260910c"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tables.js') }}?v=20260911j"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260912a"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/overlays.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/main.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/main.js') }}?v=20260901d"></script>
|
||||
|
||||
<!-- 均线配置弹窗 -->
|
||||
<div id="maConfigModal" class="ma-config-modal">
|
||||
@@ -1479,7 +1538,7 @@
|
||||
<!-- MACD参数配置弹窗 -->
|
||||
<div id="macdConfigModal" class="macd-config-modal">
|
||||
<div class="macd-config-content">
|
||||
<div class="macd-config-title">ChanMACD 参数设置</div>
|
||||
<div class="macd-config-title">MACD 参数设置</div>
|
||||
<div class="macd-config-form">
|
||||
<div class="macd-config-group">
|
||||
<label class="macd-config-label">快线周期 (Fast)</label>
|
||||
|
||||
@@ -105,6 +105,32 @@ def test_analyze_chan_keys_on_fixture():
|
||||
assert k in result["chan_macd"]
|
||||
|
||||
|
||||
def test_bi_and_seg_area_div_computed():
|
||||
from services.runtime import add_indicators, analyze_chan
|
||||
|
||||
df = add_indicators(make_ohlcv(400))
|
||||
result = analyze_chan(df, symbol="TEST/USDT:USDT", timeframe="5m")
|
||||
bis = result["bi_list"]
|
||||
segs = result["seg_list"]
|
||||
assert bis, "fixture should produce bi"
|
||||
assert all(hasattr(bi, "macd_div") for bi in bis)
|
||||
assert all(hasattr(seg, "macd_div") for seg in segs)
|
||||
assert all(hasattr(bi, "macd_hist") for bi in bis)
|
||||
assert all(hasattr(seg, "macd_hist") for seg in segs)
|
||||
same_dir_bis = [bi for bi in bis if getattr(bi, "pre", None) and getattr(bi.pre, "pre", None)]
|
||||
if same_dir_bis:
|
||||
bi = same_dir_bis[-1]
|
||||
prev = bi.pre.pre
|
||||
if prev.macd_hist:
|
||||
assert abs(bi.macd_div - (bi.macd_hist / prev.macd_hist)) < 1e-9
|
||||
same_dir_segs = [seg for seg in segs if getattr(seg, "pre", None) and getattr(seg.pre, "pre", None)]
|
||||
if same_dir_segs:
|
||||
seg = same_dir_segs[-1]
|
||||
prev = seg.pre.pre
|
||||
if prev.macd_hist:
|
||||
assert abs(seg.macd_div - (seg.macd_hist / prev.macd_hist)) < 1e-9
|
||||
|
||||
|
||||
def test_serialize_chan_macd_shape():
|
||||
from pytz import timezone
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""资金面中转:Web 只打 data_provider,字段原样回给前端。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
WEB_ROOT = Path(__file__).resolve().parents[1]
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(WEB_ROOT))
|
||||
|
||||
|
||||
SNAPSHOT = {
|
||||
"exchange": "bitget",
|
||||
"symbol": "BTC/USDT:USDT",
|
||||
"timestamp": 1789039236740,
|
||||
"datetime": "2026-09-10T11:20:36.740000Z",
|
||||
"funding_rate": 0.0001,
|
||||
"open_interest": 36207.21,
|
||||
"oi_change_pct": -0.01,
|
||||
"basis": -0.03,
|
||||
}
|
||||
|
||||
OI_HIST = {
|
||||
"metric": "open_interest_history",
|
||||
"symbol": "BTC/USDT:USDT",
|
||||
"period": "15m",
|
||||
"count": 1,
|
||||
"data": [
|
||||
{
|
||||
"timestamp": 1789038900000,
|
||||
"datetime": "2026-09-10T11:15:00Z",
|
||||
"open_interest_amount": 106162.005,
|
||||
"open_interest_value": 8269553076.678,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
app.config["TESTING"] = True
|
||||
return app.test_client()
|
||||
|
||||
|
||||
def test_derivatives_proxy_passthrough(client):
|
||||
with patch("api.provider.fetch_derivatives", return_value=SNAPSHOT) as mock_fetch:
|
||||
resp = client.get("/api/derivatives?symbol=BTC/USDT:USDT")
|
||||
assert resp.status_code == 200
|
||||
body = resp.get_json()
|
||||
assert body["funding_rate"] == 0.0001
|
||||
assert body["open_interest"] == 36207.21
|
||||
assert body["oi_change_pct"] == -0.01
|
||||
assert body["basis"] == -0.03
|
||||
mock_fetch.assert_called_once()
|
||||
assert mock_fetch.call_args[0][0] == "BTC/USDT:USDT"
|
||||
|
||||
|
||||
def test_sentiment_metrics_proxy_passthrough(client):
|
||||
with patch("api.provider.fetch_sentiment_metrics", return_value=OI_HIST) as mock_fetch:
|
||||
resp = client.get(
|
||||
"/api/sentiment/metrics?metric=open_interest_history&symbol=BTC/USDT:USDT&limit=1"
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.get_json()
|
||||
assert body["metric"] == "open_interest_history"
|
||||
assert body["data"][0]["open_interest_amount"] == 106162.005
|
||||
mock_fetch.assert_called_once()
|
||||
assert mock_fetch.call_args[0][0] == "open_interest_history"
|
||||
|
||||
|
||||
def test_sentiment_metrics_requires_metric(client):
|
||||
resp = client.get("/api/sentiment/metrics?symbol=BTC/USDT:USDT")
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_sentiment_latest_proxy_passthrough(client):
|
||||
latest = {
|
||||
"symbol": "BTC/USDT:USDT",
|
||||
"data": {
|
||||
"taker_buy_sell_ratio": {"buy_sell_ratio": 1.36},
|
||||
"long_short_account_ratio": {"long_short_ratio": 1.5},
|
||||
"top_long_short_position_ratio": {"long_short_ratio": 2.26},
|
||||
},
|
||||
}
|
||||
with patch("api.provider.fetch_sentiment_latest", return_value=latest) as mock_fetch:
|
||||
resp = client.get("/api/sentiment/latest?symbol=BTC/USDT:USDT")
|
||||
assert resp.status_code == 200
|
||||
body = resp.get_json()
|
||||
assert body["data"]["taker_buy_sell_ratio"]["buy_sell_ratio"] == 1.36
|
||||
mock_fetch.assert_called_once()
|
||||
Reference in New Issue
Block a user