Compare commits
4
Commits
6b72ba226b
..
chan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b1b500108 | ||
|
|
c22cd48f36 | ||
|
|
b301ad22c9 | ||
|
|
2cd50f1e01 |
@@ -49,6 +49,7 @@ research/out/run_meta_*.json
|
|||||||
|
|
||||||
# Telegram 凭据。**不要提交**
|
# Telegram 凭据。**不要提交**
|
||||||
research/live/deploy/tg.env
|
research/live/deploy/tg.env
|
||||||
|
research/.tg.env
|
||||||
|
|
||||||
# 生产状态与信号总线。刻意放在仓库外(LIVE_HOME / BUS_DIR),这几条只防
|
# 生产状态与信号总线。刻意放在仓库外(LIVE_HOME / BUS_DIR),这几条只防
|
||||||
# 有人把它们指回仓库里:里面是日亏损累计与已处理信号键,被 git clean
|
# 有人把它们指回仓库里:里面是日亏损累计与已处理信号键,被 git clean
|
||||||
|
|||||||
@@ -43,6 +43,29 @@ class ChanSEG():
|
|||||||
self.macd_hist = macd_hist
|
self.macd_hist = macd_hist
|
||||||
def set_macd_div(self, macd_div):
|
def set_macd_div(self, macd_div):
|
||||||
self.macd_div = 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):
|
def set_end_bi(self, bi: ChanBI, sure_bi: ChanBI):
|
||||||
self.end_bi = bi
|
self.end_bi = bi
|
||||||
if bi and bi.is_sure:
|
if bi and bi.is_sure:
|
||||||
|
|||||||
@@ -668,13 +668,22 @@ class BiBuilderMixin:
|
|||||||
return bi_list
|
return bi_list
|
||||||
|
|
||||||
def check_top_fx(self, last_bottom, klc):
|
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 False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def check_bottom_fx(self, last_top, klc):
|
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 False
|
||||||
return True
|
return True
|
||||||
# 线段内的中枢
|
# 线段内的中枢
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ class IndicatorsBuilderMixin:
|
|||||||
30 多列的开销比全部 TA 计算本身还大(2001 行实测 TA 合计 2.5ms,
|
30 多列的开销比全部 TA 计算本身还大(2001 行实测 TA 合计 2.5ms,
|
||||||
逐列赋值 3.6ms)。增量路径每根都要走一遍,这笔开销是白付的。
|
逐列赋值 3.6ms)。增量路径每根都要走一遍,这笔开销是白付的。
|
||||||
"""
|
"""
|
||||||
fast = 26
|
fast = 12
|
||||||
slow = 52
|
slow = 26
|
||||||
period = 9
|
period = 9
|
||||||
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
||||||
bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
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,
|
'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,
|
'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),
|
'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],
|
} for bi in analysis_result['bi_list'] if bi.is_sure],
|
||||||
# 添加未完成笔列表
|
# 添加未完成笔列表
|
||||||
'uncompleted_bi_list': [{
|
'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,
|
'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, # 未完成笔没有结束价格
|
'end_price': bi.end_klc.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, # 未完成笔没有结束价格
|
||||||
'direction': convert_direction(bi.dir),
|
'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],
|
} for bi in analysis_result['bi_list'] if not bi.is_sure],
|
||||||
'seg_list': [{
|
'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(),
|
'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,
|
'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,
|
'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,
|
'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],
|
} for seg in analysis_result['seg_list'] if seg.is_sure],
|
||||||
# 添加未完成线段列表
|
# 添加未完成线段列表
|
||||||
'uncompleted_seg_list': get_uncompleted_seg_list(analysis_result['seg_list'], client_tz),
|
'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,
|
'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,
|
'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),
|
'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]
|
} for bi in element_analysis['bi_list'] if bi.is_sure]
|
||||||
# 添加次周期未完成笔列表
|
# 添加次周期未完成笔列表
|
||||||
result['element_uncompleted_bi_list'] = [{
|
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,
|
'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, # 未完成笔没有结束价格
|
'end_price': bi.end_klc.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, # 未完成笔没有结束价格
|
||||||
'direction': convert_direction(bi.dir),
|
'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]
|
} for bi in element_analysis['bi_list'] if not bi.is_sure]
|
||||||
|
|
||||||
# 添加小周期K线数据
|
# 添加小周期K线数据
|
||||||
@@ -341,7 +347,9 @@ def analyze():
|
|||||||
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
|
'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,
|
'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,
|
'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]
|
} 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)
|
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,
|
'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,
|
'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),
|
'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]
|
} for bi in sub_sub_analysis['bi_list'] if bi.is_sure]
|
||||||
result['sub_sub_uncompleted_bi_list'] = [{
|
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(),
|
'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,
|
'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,
|
'end_price': bi.end_klc.low if convert_direction(bi.dir) == 1 else bi.end_klc.high,
|
||||||
'direction': convert_direction(bi.dir),
|
'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]
|
} for bi in sub_sub_analysis['bi_list'] if not bi.is_sure]
|
||||||
# 次次周期 KLC 列表
|
# 次次周期 KLC 列表
|
||||||
result['sub_sub_klc_list'] = [{
|
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,
|
'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,
|
'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,
|
'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]
|
} 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_uncompleted_seg_list'] = get_uncompleted_seg_list(sub_sub_analysis['seg_list'], client_tz)
|
||||||
result['sub_sub_zs_list'] = [{
|
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 config import FLASK_HOST, FLASK_PORT
|
||||||
from api.analyze import bp as analyze_bp
|
from api.analyze import bp as analyze_bp
|
||||||
from api.pages import bp as pages_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.symbols import bp as symbols_bp
|
||||||
from api.trend import bp as trend_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(analyze_bp)
|
||||||
app.register_blueprint(symbols_bp)
|
app.register_blueprint(symbols_bp)
|
||||||
app.register_blueprint(trend_bp)
|
app.register_blueprint(trend_bp)
|
||||||
|
app.register_blueprint(provider_bp)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ from .market_data import ( # noqa: F401
|
|||||||
get_crypto_kl_data,
|
get_crypto_kl_data,
|
||||||
get_a_stock_kl_data,
|
get_a_stock_kl_data,
|
||||||
load_crypto_symbols,
|
load_crypto_symbols,
|
||||||
|
fetch_derivatives,
|
||||||
|
fetch_sentiment_metrics,
|
||||||
|
fetch_sentiment_latest,
|
||||||
)
|
)
|
||||||
from .indicators import ( # noqa: F401
|
from .indicators import ( # noqa: F401
|
||||||
add_indicators,
|
add_indicators,
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ def analyze_chan(df, symbol=None, timeframe=None):
|
|||||||
for bi in bi_list:
|
for bi in bi_list:
|
||||||
bi.cal_macd_div()
|
bi.cal_macd_div()
|
||||||
#print(bi.start_time, bi.macd_hist, bi.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 内已算好的结果,避免同周期二次全量分析)
|
# 添加ChanMACD分析(复用 get_klc_list 内已算好的结果,避免同周期二次全量分析)
|
||||||
chan_macd = None
|
chan_macd = None
|
||||||
|
|||||||
@@ -319,3 +319,34 @@ def load_crypto_symbols(limit=200):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return DEFAULT_SYMBOLS[:limit]
|
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(兼容新结构与枚举类型)
|
# 序列化unittf_list(兼容新结构与枚举类型)
|
||||||
for unittf in chan_macd_data.get('unittf_list', []):
|
for unittf in chan_macd_data.get('unittf_list', []):
|
||||||
try:
|
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)
|
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_t = getattr(unittf, 'start_type', None)
|
||||||
start_type = getattr(start_t, 'name', start_t)
|
start_type = getattr(start_t, 'name', start_t)
|
||||||
end_t = getattr(unittf, 'end_type', None)
|
end_t = getattr(unittf, 'end_type', None)
|
||||||
@@ -114,7 +120,7 @@ def serialize_chan_macd_data(chan_macd_data, client_tz):
|
|||||||
unittf_data = {
|
unittf_data = {
|
||||||
'start_time': format_time_safely(getattr(unittf, 'start_time', None), client_tz),
|
'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,
|
'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'
|
'start_type': start_type, # e.g. 'START' | 'CROSS0' | 'NEAR0_UP' | 'NEAR0_DOWN'
|
||||||
'end_type': end_type,
|
'end_type': end_type,
|
||||||
'invalid': getattr(unittf, 'invalid', False),
|
'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(),
|
'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,
|
'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,
|
'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:
|
if is_last:
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ DEFAULT_TIMEFRAME_LABELS = OrderedDict([
|
|||||||
("5m", "5分钟"),
|
("5m", "5分钟"),
|
||||||
("15m", "15分钟"),
|
("15m", "15分钟"),
|
||||||
("30m", "30分钟"),
|
("30m", "30分钟"),
|
||||||
|
("45m", "45分钟"),
|
||||||
("1h", "1小时"),
|
("1h", "1小时"),
|
||||||
("2h", "2小时"),
|
("2h", "2小时"),
|
||||||
("4h", "4小时"),
|
("4h", "4小时"),
|
||||||
|
|||||||
@@ -94,19 +94,19 @@ def _prefer_smaller(candidates, labels_ordered, ceiling_tf, timeframe_keys):
|
|||||||
def compute_timeframe_defaults(labels_ordered):
|
def compute_timeframe_defaults(labels_ordered):
|
||||||
"""
|
"""
|
||||||
根据已排序的「周期 → 中文标签」映射,计算主 / 次 / 次次周期默认值。
|
根据已排序的「周期 → 中文标签」映射,计算主 / 次 / 次次周期默认值。
|
||||||
默认偏好:主 4h、次 1h、次次 15m。
|
默认偏好:主 45m、次 15m、次次 5m。
|
||||||
labels_ordered: OrderedDict 或按插入顺序排列的 dict。
|
labels_ordered: OrderedDict 或按插入顺序排列的 dict。
|
||||||
"""
|
"""
|
||||||
if not labels_ordered:
|
if not labels_ordered:
|
||||||
labels_ordered = DEFAULT_TIMEFRAME_LABELS.copy()
|
labels_ordered = DEFAULT_TIMEFRAME_LABELS.copy()
|
||||||
timeframe_keys = list(labels_ordered.keys())
|
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')
|
default_main = preferred_main or (timeframe_keys[0] if timeframe_keys else '1m')
|
||||||
if default_main not in labels_ordered and timeframe_keys:
|
if default_main not in labels_ordered and timeframe_keys:
|
||||||
default_main = timeframe_keys[0]
|
default_main = timeframe_keys[0]
|
||||||
|
|
||||||
default_element = _prefer_smaller(['1h', '15m'], labels_ordered, default_main, timeframe_keys)
|
default_element = _prefer_smaller(['15m', '5m', '30m'], labels_ordered, default_main, timeframe_keys)
|
||||||
default_sub_sub = _prefer_smaller(['15m', '5m'], labels_ordered, default_element, 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
|
return default_main, default_element, default_sub_sub, timeframe_keys
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,33 @@ window.ChanApi = {
|
|||||||
symbols: function() {
|
symbols: function() {
|
||||||
return fetch('/api/symbols').then(r => r.json());
|
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) {
|
macdConfig: function(body) {
|
||||||
if (body === undefined) return fetch('/api/macd_config').then(r => r.json());
|
if (body === undefined) return fetch('/api/macd_config').then(r => r.json());
|
||||||
return fetch('/api/macd_config', {
|
return fetch('/api/macd_config', {
|
||||||
|
|||||||
@@ -229,29 +229,29 @@ function updateTradingViewData(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新MACD数据
|
// 更新MACD数据
|
||||||
if (tvWidget.series.macdLineSeries && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data)) {
|
const macdKlineSrc = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? (currentData.element_kline_data || []) : (currentData.kline_data || []));
|
||||||
// 提取MACD数据
|
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 macdData = [];
|
||||||
const signalData = [];
|
const signalData = [];
|
||||||
const histogramData = [];
|
const histogramData = [];
|
||||||
|
|
||||||
for (let i = 0; i < currentData.kline_data.length; i++) {
|
for (let i = 0; i < macdKlineSrc.length; i++) {
|
||||||
const kline = currentData.kline_data[i];
|
const kline = macdKlineSrc[i];
|
||||||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
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({
|
macdData.push({
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
value: currentData.macd.macd[i]
|
value: macdSrc.macd[i]
|
||||||
});
|
});
|
||||||
|
|
||||||
signalData.push({
|
signalData.push({
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
value: currentData.macd.signal[i]
|
value: macdSrc.signal[i]
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置直方图颜色
|
const histValue = macdSrc.histogram[i];
|
||||||
const histValue = currentData.macd.histogram[i];
|
|
||||||
histogramData.push({
|
histogramData.push({
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
value: histValue,
|
value: histValue,
|
||||||
@@ -309,6 +309,9 @@ function updateTradingViewData(options) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (typeof refreshUnittfOverlayFromData === 'function') {
|
||||||
|
refreshUnittfOverlayFromData(currentData);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('更新ChanMACD标注失败:', e);
|
console.warn('更新ChanMACD标注失败:', e);
|
||||||
}
|
}
|
||||||
@@ -331,7 +334,8 @@ function updateTradingViewData(options) {
|
|||||||
tvWidget.volumeChart,
|
tvWidget.volumeChart,
|
||||||
tvWidget.atrChart,
|
tvWidget.atrChart,
|
||||||
tvWidget.macdChart,
|
tvWidget.macdChart,
|
||||||
tvWidget.chanMacdChart
|
tvWidget.chanMacdChart,
|
||||||
|
tvWidget.sentimentChart
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
const vr = clampedVisibleRange || savedVisibleRange;
|
const vr = clampedVisibleRange || savedVisibleRange;
|
||||||
@@ -409,8 +413,11 @@ function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContai
|
|||||||
volume: false,
|
volume: false,
|
||||||
atr: false,
|
atr: false,
|
||||||
macd: false,
|
macd: false,
|
||||||
chanmacd: false
|
chanmacd: false,
|
||||||
|
sentiment: false
|
||||||
};
|
};
|
||||||
|
const sentimentChart = tvWidget && tvWidget.sentimentChart;
|
||||||
|
const sentimentChartContainer = tvWidget && tvWidget.sentimentChartContainer;
|
||||||
|
|
||||||
// 同步图表的时间范围
|
// 同步图表的时间范围
|
||||||
function syncCharts(sourceChart, sourceContainer) {
|
function syncCharts(sourceChart, sourceContainer) {
|
||||||
@@ -438,6 +445,9 @@ function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContai
|
|||||||
if (showMacd && chanMacdChart && sourceChart !== chanMacdChart && chanMacdChart.timeScale) {
|
if (showMacd && chanMacdChart && sourceChart !== chanMacdChart && chanMacdChart.timeScale) {
|
||||||
try { chanMacdChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
|
try { chanMacdChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
|
||||||
}
|
}
|
||||||
|
if (sentimentChart && sourceChart !== sentimentChart && sentimentChart.timeScale) {
|
||||||
|
try { sentimentChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (tvWidget && tvWidget.state) {
|
if (tvWidget && tvWidget.state) {
|
||||||
tvWidget.state.logicalRange = logicalRange;
|
tvWidget.state.logicalRange = logicalRange;
|
||||||
@@ -458,7 +468,8 @@ function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContai
|
|||||||
chart === volumeChart ? 'volume' :
|
chart === volumeChart ? 'volume' :
|
||||||
chart === atrChart ? 'atr' :
|
chart === atrChart ? 'atr' :
|
||||||
chart === macdChart ? 'macd' :
|
chart === macdChart ? 'macd' :
|
||||||
chart === chanMacdChart ? 'chanmacd' : 'unknown';
|
chart === chanMacdChart ? 'chanmacd' :
|
||||||
|
chart === sentimentChart ? 'sentiment' : 'unknown';
|
||||||
|
|
||||||
const timeRangeHandler = () => {
|
const timeRangeHandler = () => {
|
||||||
if (!syncInProgress) {
|
if (!syncInProgress) {
|
||||||
@@ -515,6 +526,9 @@ function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContai
|
|||||||
if (showMacd && chanMacdChartContainer && chanMacdChart) {
|
if (showMacd && chanMacdChartContainer && chanMacdChart) {
|
||||||
addChartSyncEvents(chanMacdChartContainer, chanMacdChart);
|
addChartSyncEvents(chanMacdChartContainer, chanMacdChart);
|
||||||
}
|
}
|
||||||
|
if (sentimentChartContainer && sentimentChart) {
|
||||||
|
addChartSyncEvents(sentimentChartContainer, sentimentChart);
|
||||||
|
}
|
||||||
|
|
||||||
// 窗口大小变化时重绘图表 — 使用可清理的方式注册
|
// 窗口大小变化时重绘图表 — 使用可清理的方式注册
|
||||||
const resizeHandler = () => {
|
const resizeHandler = () => {
|
||||||
@@ -533,6 +547,9 @@ function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContai
|
|||||||
if (showMacd && chanMacdChart && chanMacdChartContainer) {
|
if (showMacd && chanMacdChart && chanMacdChartContainer) {
|
||||||
chanMacdChart.applyOptions({ width: chanMacdChartContainer.clientWidth, height: chanMacdChartContainer.clientHeight });
|
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);
|
setTimeout(() => { if (mainChart) syncCharts(mainChart, mainChartContainer); }, 200);
|
||||||
};
|
};
|
||||||
window.addEventListener('resize', resizeHandler);
|
window.addEventListener('resize', resizeHandler);
|
||||||
@@ -549,9 +566,11 @@ function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartCon
|
|||||||
// 初始化 U 显示状态(主/次周期分开控制)
|
// 初始化 U 显示状态(主/次周期分开控制)
|
||||||
const isShowUMain = $('#toggleUOnMain').is(':checked');
|
const isShowUMain = $('#toggleUOnMain').is(':checked');
|
||||||
const isShowUElement = $('#toggleUOnElement').is(':checked');
|
const isShowUElement = $('#toggleUOnElement').is(':checked');
|
||||||
|
const isShowUSubSub = $('#toggleUOnSubSub').is(':checked');
|
||||||
window.showUOnMain = isShowUMain;
|
window.showUOnMain = isShowUMain;
|
||||||
window.showUOnElement = isShowUElement;
|
window.showUOnElement = isShowUElement;
|
||||||
if (!isShowUMain && !isShowUElement) {
|
window.showUOnSubSub = isShowUSubSub;
|
||||||
|
if (!isShowUMain && !isShowUElement && !isShowUSubSub) {
|
||||||
// 隐藏时清空子图上的 U 标记
|
// 隐藏时清空子图上的 U 标记
|
||||||
if (tvWidget.series && tvWidget.series.chanMacdLineSeries) {
|
if (tvWidget.series && tvWidget.series.chanMacdLineSeries) {
|
||||||
try { tvWidget.series.chanMacdLineSeries.setMarkers([]); } catch (e) {}
|
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());
|
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> 数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>${selectedPeriod} (${timeframe})</strong> 数据`);
|
||||||
|
|
||||||
@@ -297,55 +297,55 @@ function setupDataSourceInfo(data) {
|
|||||||
$('#kline-tab, #macd-tab').off('click').on('click', function() {
|
$('#kline-tab, #macd-tab').off('click').on('click', function() {
|
||||||
$('.data-source-info').show();
|
$('.data-source-info').show();
|
||||||
if (useSubSubPeriod && data.sub_sub_kline_data && data.sub_sub_kline_data.length > 0) {
|
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) {
|
} else if (useElementPeriod && data.element_kline_data && data.element_kline_data.length > 0) {
|
||||||
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>次 (${elementTimeframe})</strong> 数据`);
|
||||||
} else {
|
} else {
|
||||||
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>主 (${mainTimeframe})</strong> 数据`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#bi-tab').off('click').on('click', function() {
|
$('#bi-tab').off('click').on('click', function() {
|
||||||
$('.data-source-info').show();
|
$('.data-source-info').show();
|
||||||
if (useSubSubPeriod && data.sub_sub_bi_list && data.sub_sub_bi_list.length > 0) {
|
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) {
|
} else if (useElementPeriod && data.element_bi_list && data.element_bi_list.length > 0) {
|
||||||
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 笔数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>次 (${elementTimeframe})</strong> 笔数据`);
|
||||||
} else {
|
} else {
|
||||||
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 笔数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>主 (${mainTimeframe})</strong> 笔数据`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#seg-tab').off('click').on('click', function() {
|
$('#seg-tab').off('click').on('click', function() {
|
||||||
$('.data-source-info').show();
|
$('.data-source-info').show();
|
||||||
if (useSubSubPeriod && data.sub_sub_seg_list && data.sub_sub_seg_list.length > 0) {
|
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) {
|
} else if (useElementPeriod && data.element_seg_list && data.element_seg_list.length > 0) {
|
||||||
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 线段数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>次 (${elementTimeframe})</strong> 线段数据`);
|
||||||
} else {
|
} else {
|
||||||
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 线段数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>主 (${mainTimeframe})</strong> 线段数据`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#zs-tab').off('click').on('click', function() {
|
$('#zs-tab').off('click').on('click', function() {
|
||||||
$('.data-source-info').show();
|
$('.data-source-info').show();
|
||||||
if (useSubSubPeriod && data.sub_sub_zs_list && data.sub_sub_zs_list.length > 0) {
|
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) {
|
} else if (useElementPeriod && data.element_zs_list && data.element_zs_list.length > 0) {
|
||||||
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 中枢数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>次 (${elementTimeframe})</strong> 中枢数据`);
|
||||||
} else {
|
} else {
|
||||||
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 中枢数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>主 (${mainTimeframe})</strong> 中枢数据`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#trade-points-tab').off('click').on('click', function() {
|
$('#trade-points-tab').off('click').on('click', function() {
|
||||||
$('.data-source-info').show();
|
$('.data-source-info').show();
|
||||||
if (useSubSubPeriod && data.sub_sub_bsp_list && data.sub_sub_bsp_list.length > 0) {
|
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) {
|
} else if (useElementPeriod && data.element_trade_points && data.element_trade_points.length > 0) {
|
||||||
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 买卖点数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>次 (${elementTimeframe})</strong> 买卖点数据`);
|
||||||
} else {
|
} else {
|
||||||
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 买卖点数据`);
|
$('#dataSourceText').html(`当前显示的是<strong>主 (${mainTimeframe})</strong> 买卖点数据`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ function initTradingView(symbol, timeframe) {
|
|||||||
chartTvRenderChan(ctx);
|
chartTvRenderChan(ctx);
|
||||||
chartTvRenderOverlays(ctx);
|
chartTvRenderOverlays(ctx);
|
||||||
chartTvFinalize(ctx);
|
chartTvFinalize(ctx);
|
||||||
|
if (window.ChanDeriv) ChanDeriv.loadOverlays();
|
||||||
|
|
||||||
console.log('图表初始化完成');
|
console.log('图表初始化完成');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -178,38 +178,6 @@ function chartTvRenderChan(ctx) {
|
|||||||
color: bi.direction === 1 ? '#dc3545' : '#28a745',
|
color: bi.direction === 1 ? '#dc3545' : '#28a745',
|
||||||
lineWidth: 1
|
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) {
|
} catch (e) {
|
||||||
console.error('主周期笔处理出错:', e);
|
console.error('主周期笔处理出错:', e);
|
||||||
}
|
}
|
||||||
@@ -260,38 +228,6 @@ function chartTvRenderChan(ctx) {
|
|||||||
color: bi.direction === 1 ? '#9c27b0' : '#673ab7',
|
color: bi.direction === 1 ? '#9c27b0' : '#673ab7',
|
||||||
lineWidth: 1
|
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) {
|
} catch (e) {
|
||||||
console.error('次周期笔处理出错:', e);
|
console.error('次周期笔处理出错:', e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ function chartTvFinalize(ctx) {
|
|||||||
var atrChart = ctx.atrChart;
|
var atrChart = ctx.atrChart;
|
||||||
var macdChart = ctx.macdChart;
|
var macdChart = ctx.macdChart;
|
||||||
var chanMacdChart = ctx.chanMacdChart;
|
var chanMacdChart = ctx.chanMacdChart;
|
||||||
|
var sentimentChart = ctx.sentimentChart;
|
||||||
|
var sentimentChartContainer = ctx.sentimentChartContainer;
|
||||||
var createChartOptions = ctx.createChartOptions;
|
var createChartOptions = ctx.createChartOptions;
|
||||||
// 同步所有图表的时间轴配置
|
// 同步所有图表的时间轴配置
|
||||||
const hasPendingRestoreView = !!window._pendingRestoreView;
|
const hasPendingRestoreView = !!window._pendingRestoreView;
|
||||||
@@ -55,6 +57,9 @@ function chartTvFinalize(ctx) {
|
|||||||
if (showMacd && macdChart) {
|
if (showMacd && macdChart) {
|
||||||
macdChart.timeScale().applyOptions(baseOptions);
|
macdChart.timeScale().applyOptions(baseOptions);
|
||||||
}
|
}
|
||||||
|
if (sentimentChart) {
|
||||||
|
sentimentChart.timeScale().applyOptions(baseOptions);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 首先同步时间轴设置
|
// 首先同步时间轴设置
|
||||||
@@ -65,7 +70,8 @@ function chartTvFinalize(ctx) {
|
|||||||
const visibleBarsCount = 200;
|
const visibleBarsCount = 200;
|
||||||
const allChartsNow = [mainChart, volumeChart, atrChart]
|
const allChartsNow = [mainChart, volumeChart, atrChart]
|
||||||
.concat(showMacd && macdChart ? [macdChart] : [])
|
.concat(showMacd && macdChart ? [macdChart] : [])
|
||||||
.concat(showMacd && chanMacdChart ? [chanMacdChart] : []);
|
.concat(showMacd && chanMacdChart ? [chanMacdChart] : [])
|
||||||
|
.concat(sentimentChart ? [sentimentChart] : []);
|
||||||
const restoreOpts = function () {
|
const restoreOpts = function () {
|
||||||
const firstT = candles && candles.length ? candles[0].time : null;
|
const firstT = candles && candles.length ? candles[0].time : null;
|
||||||
const lastT = candles && candles.length ? candles[candles.length - 1].time : null;
|
const lastT = candles && candles.length ? candles[candles.length - 1].time : null;
|
||||||
@@ -103,6 +109,9 @@ function chartTvFinalize(ctx) {
|
|||||||
if (showMacd && chanMacdChart) {
|
if (showMacd && chanMacdChart) {
|
||||||
chanMacdChart.timeScale().setVisibleLogicalRange(logRange);
|
chanMacdChart.timeScale().setVisibleLogicalRange(logRange);
|
||||||
}
|
}
|
||||||
|
if (sentimentChart) {
|
||||||
|
sentimentChart.timeScale().setVisibleLogicalRange(logRange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -117,6 +126,9 @@ function chartTvFinalize(ctx) {
|
|||||||
if (showMacd && chanMacdChart) {
|
if (showMacd && chanMacdChart) {
|
||||||
chanMacdChart.timeScale().setVisibleLogicalRange(logRange);
|
chanMacdChart.timeScale().setVisibleLogicalRange(logRange);
|
||||||
}
|
}
|
||||||
|
if (sentimentChart) {
|
||||||
|
sentimentChart.timeScale().setVisibleLogicalRange(logRange);
|
||||||
|
}
|
||||||
console.log('🔧 时间轴同步完成');
|
console.log('🔧 时间轴同步完成');
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
@@ -127,6 +139,8 @@ function chartTvFinalize(ctx) {
|
|||||||
tvWidget.atrChart = atrChart;
|
tvWidget.atrChart = atrChart;
|
||||||
tvWidget.macdChart = macdChart;
|
tvWidget.macdChart = macdChart;
|
||||||
tvWidget.chanMacdChart = chanMacdChart;
|
tvWidget.chanMacdChart = chanMacdChart;
|
||||||
|
tvWidget.sentimentChart = sentimentChart;
|
||||||
|
tvWidget.sentimentChartContainer = sentimentChartContainer;
|
||||||
tvWidget.state.isInitialized = true;
|
tvWidget.state.isInitialized = true;
|
||||||
// 注册窗口卸载时释放资源,避免GPU内存泄漏
|
// 注册窗口卸载时释放资源,避免GPU内存泄漏
|
||||||
window.onbeforeunload = function() {
|
window.onbeforeunload = function() {
|
||||||
@@ -137,6 +151,7 @@ function chartTvFinalize(ctx) {
|
|||||||
if (tvWidget.macdChart && typeof tvWidget.macdChart.remove === 'function') tvWidget.macdChart.remove();
|
if (tvWidget.macdChart && typeof tvWidget.macdChart.remove === 'function') tvWidget.macdChart.remove();
|
||||||
if (tvWidget.chanMacdChart && typeof tvWidget.chanMacdChart.remove === 'function') tvWidget.chanMacdChart.remove();
|
if (tvWidget.chanMacdChart && typeof tvWidget.chanMacdChart.remove === 'function') tvWidget.chanMacdChart.remove();
|
||||||
if (tvWidget.atrChart && typeof tvWidget.atrChart.remove === 'function') tvWidget.atrChart.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) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
@@ -220,6 +235,7 @@ function chartTvFinalize(ctx) {
|
|||||||
const allCharts = [mainChart, volumeChart, atrChart];
|
const allCharts = [mainChart, volumeChart, atrChart];
|
||||||
if (showMacd && macdChart) allCharts.push(macdChart);
|
if (showMacd && macdChart) allCharts.push(macdChart);
|
||||||
if (showMacd && chanMacdChart) allCharts.push(chanMacdChart);
|
if (showMacd && chanMacdChart) allCharts.push(chanMacdChart);
|
||||||
|
if (sentimentChart) allCharts.push(sentimentChart);
|
||||||
|
|
||||||
// 检查是否有待恢复的视图(缩放 + 位置)
|
// 检查是否有待恢复的视图(缩放 + 位置)
|
||||||
const pending = window._pendingRestoreView || pendingView;
|
const pending = window._pendingRestoreView || pendingView;
|
||||||
@@ -243,7 +259,8 @@ function chartTvFinalize(ctx) {
|
|||||||
console.log('🔧 最终同步可见范围:', visibleRange);
|
console.log('🔧 最终同步可见范围:', visibleRange);
|
||||||
[volumeChart, atrChart].concat(
|
[volumeChart, atrChart].concat(
|
||||||
showMacd && macdChart ? [macdChart] : [],
|
showMacd && macdChart ? [macdChart] : [],
|
||||||
showMacd && chanMacdChart ? [chanMacdChart] : []
|
showMacd && chanMacdChart ? [chanMacdChart] : [],
|
||||||
|
sentimentChart ? [sentimentChart] : []
|
||||||
).forEach(c => {
|
).forEach(c => {
|
||||||
try { c.timeScale().setVisibleRange(visibleRange); } catch(e) {}
|
try { c.timeScale().setVisibleRange(visibleRange); } catch(e) {}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,44 @@
|
|||||||
/* chart_tv_indicators.js — volume / ATR / ChanMACD */
|
/* 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) {
|
function chartTvRenderIndicators(ctx) {
|
||||||
var symbol = ctx.symbol;
|
var symbol = ctx.symbol;
|
||||||
var timeframe = ctx.timeframe;
|
var timeframe = ctx.timeframe;
|
||||||
@@ -141,11 +180,11 @@ function chartTvRenderIndicators(ctx) {
|
|||||||
|
|
||||||
// 使用与K线数据相同的数据源来确保时间对齐
|
// 使用与K线数据相同的数据源来确保时间对齐
|
||||||
const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
|
const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
|
||||||
const macdDataSource = useElementPeriod ?
|
const macdDataSource = useSubSubPeriod
|
||||||
(currentData.element_macd || currentData.macd) : // 如果有次周期MACD数据则使用,否则使用主周期
|
? (currentData.sub_sub_macd || currentData.macd)
|
||||||
currentData.macd; // 主周期使用主周期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('K线数据长度:', klineDataSource.length);
|
||||||
console.log('MACD数据:', macdDataSource);
|
console.log('MACD数据:', macdDataSource);
|
||||||
|
|
||||||
@@ -209,7 +248,7 @@ function chartTvRenderIndicators(ctx) {
|
|||||||
const chanMacdLineSeries = chanMacdChart.addLineSeries({
|
const chanMacdLineSeries = chanMacdChart.addLineSeries({
|
||||||
color: '#2962FF',
|
color: '#2962FF',
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
title: 'ChanMACD',
|
title: 'MACD',
|
||||||
lastValueVisible: false,
|
lastValueVisible: false,
|
||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
});
|
});
|
||||||
@@ -370,7 +409,8 @@ function chartTvRenderIndicators(ctx) {
|
|||||||
const k = klineArr[i];
|
const k = klineArr[i];
|
||||||
if (!k || !k.date) continue;
|
if (!k || !k.date) continue;
|
||||||
const t = Math.floor(new Date(k.date).getTime() / 1000);
|
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);
|
map.set(t, val);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@@ -387,15 +427,15 @@ function chartTvRenderIndicators(ctx) {
|
|||||||
if (!item || !item.time) return;
|
if (!item || !item.time) return;
|
||||||
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
||||||
if (isNaN(ts)) return;
|
if (isNaN(ts)) return;
|
||||||
if (Number(item.separate_div) > 0) {
|
if (shouldShowSdMarker(item.separate_div)) {
|
||||||
const macdVal = mainMacdMap.get(ts);
|
const macdVal = mainMacdMap.get(ts);
|
||||||
const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
|
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) {
|
if (item.continue_div === true) {
|
||||||
const macdVal = mainMacdMap.get(ts);
|
const macdVal = mainMacdMap.get(ts);
|
||||||
const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
|
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) {
|
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 });
|
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;
|
if (!item || !item.time) return;
|
||||||
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
||||||
if (isNaN(ts)) return;
|
if (isNaN(ts)) return;
|
||||||
if (Number(item.separate_div) > 0) {
|
if (shouldShowSdMarker(item.separate_div)) {
|
||||||
const macdVal = elementMacdMap.get(ts);
|
const macdVal = elementMacdMap.get(ts);
|
||||||
const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
|
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) {
|
if (item.continue_div === true) {
|
||||||
const macdVal = elementMacdMap.get(ts);
|
const macdVal = elementMacdMap.get(ts);
|
||||||
const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
|
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) {
|
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 });
|
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 subSubCm = currentData.sub_sub_chan_macd || {};
|
||||||
const subSubMarkers = [];
|
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)) {
|
if (window.showUOnSubSub && Array.isArray(subSubCm.klu_list)) {
|
||||||
subSubCm.klu_list.forEach((item) => {
|
subSubCm.klu_list.forEach((item) => {
|
||||||
if (!item || !item.time) return;
|
if (!item || !item.time) return;
|
||||||
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
||||||
if (isNaN(ts)) return;
|
if (isNaN(ts)) return;
|
||||||
if (Number(item.separate_div) > 0) {
|
if (shouldShowSdMarker(item.separate_div)) {
|
||||||
const macdVal = subSubMacdMap.get(ts);
|
const macdVal = subSubMacdMap.get(ts);
|
||||||
const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
|
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) {
|
if (item.continue_div === true) {
|
||||||
const macdVal = subSubMacdMap.get(ts);
|
const macdVal = subSubMacdMap.get(ts);
|
||||||
const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
|
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) {
|
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 });
|
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];
|
const k = klineArr[i];
|
||||||
if (!k || !k.date) continue;
|
if (!k || !k.date) continue;
|
||||||
const t = Math.floor(new Date(k.date).getTime() / 1000);
|
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);
|
map.set(t, val);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@@ -494,20 +538,24 @@ function chartTvRenderIndicators(ctx) {
|
|||||||
(currentData.element_macd || currentData.macd),
|
(currentData.element_macd || currentData.macd),
|
||||||
(currentData.element_kline_data || currentData.kline_data)
|
(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)) {
|
if ((typeof window.showUOnMain === 'undefined' ? false : window.showUOnMain) && Array.isArray(mainCmAll.klu_list)) {
|
||||||
mainCmAll.klu_list.forEach((item) => {
|
mainCmAll.klu_list.forEach((item) => {
|
||||||
if (!item || !item.time) return;
|
if (!item || !item.time) return;
|
||||||
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
||||||
if (isNaN(ts)) return;
|
if (isNaN(ts)) return;
|
||||||
if (Number(item.separate_div) > 0) {
|
if (shouldShowSdMarker(item.separate_div)) {
|
||||||
const macdVal = mainMacdMapAll.get(ts);
|
const macdVal = mainMacdMapAll.get(ts);
|
||||||
const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
|
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) {
|
if (item.continue_div === true) {
|
||||||
const macdVal = mainMacdMapAll.get(ts);
|
const macdVal = mainMacdMapAll.get(ts);
|
||||||
const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
|
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) {
|
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 });
|
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;
|
if (!item || !item.time) return;
|
||||||
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
||||||
if (isNaN(ts)) return;
|
if (isNaN(ts)) return;
|
||||||
if (Number(item.separate_div) > 0) {
|
if (shouldShowSdMarker(item.separate_div)) {
|
||||||
const macdVal = elementMacdMapAll.get(ts);
|
const macdVal = elementMacdMapAll.get(ts);
|
||||||
const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
|
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) {
|
if (item.continue_div === true) {
|
||||||
const macdVal = elementMacdMapAll.get(ts);
|
const macdVal = elementMacdMapAll.get(ts);
|
||||||
const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
|
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) {
|
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 });
|
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;
|
if (!item || !item.time) return;
|
||||||
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
const ts = Math.floor(new Date(item.time).getTime() / 1000);
|
||||||
if (isNaN(ts)) return;
|
if (isNaN(ts)) return;
|
||||||
if (Number(item.separate_div) > 0) {
|
if (shouldShowSdMarker(item.separate_div)) {
|
||||||
subSubMarkersAll.push({ time: ts, position: 'aboveBar', color: '#00897b', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
|
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) {
|
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) {
|
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 });
|
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;
|
window.kluDivMarkersSubSub = subSubMarkersAll;
|
||||||
|
if (typeof refreshUnittfOverlayFromData === 'function') {
|
||||||
|
refreshUnittfOverlayFromData(currentData);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('独立计算 KLU 背驰标记出错:', e);
|
console.warn('独立计算 KLU 背驰标记出错:', e);
|
||||||
window.kluDivMarkersMain = [];
|
window.kluDivMarkersMain = [];
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function disposeTradingViewCharts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tvWidget) {
|
if (tvWidget) {
|
||||||
['mainChart', 'volumeChart', 'macdChart', 'chanMacdChart', 'atrChart'].forEach(function (key) {
|
['mainChart', 'volumeChart', 'macdChart', 'chanMacdChart', 'atrChart', 'sentimentChart'].forEach(function (key) {
|
||||||
try {
|
try {
|
||||||
if (tvWidget[key] && typeof tvWidget[key].remove === 'function') {
|
if (tvWidget[key] && typeof tvWidget[key].remove === 'function') {
|
||||||
tvWidget[key].remove();
|
tvWidget[key].remove();
|
||||||
|
|||||||
@@ -82,6 +82,97 @@ var SUB_SUB_KLC_TREND_STYLE = {
|
|||||||
UNKNOWN: { position: 'inBar', color: '#004d40', shape: 'square', size: 0.5 }
|
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) {
|
function buildKlcTrendMarker(timeAligned, trendRaw, palette) {
|
||||||
var kind = normalizeKlcTrendRaw(trendRaw);
|
var kind = normalizeKlcTrendRaw(trendRaw);
|
||||||
var style = palette[kind] || palette.UNKNOWN || palette.FLAT;
|
var style = palette[kind] || palette.UNKNOWN || palette.FLAT;
|
||||||
@@ -136,6 +227,21 @@ function getMainPriceSeries() {
|
|||||||
s.lineSeries || s.areaSeries || s.baselineSeries || null;
|
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) {
|
function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
||||||
if (!mainChart || !mainChartContainer) return;
|
if (!mainChart || !mainChartContainer) return;
|
||||||
if (typeof window._fxBoxOverlayCleanup === 'function') {
|
if (typeof window._fxBoxOverlayCleanup === 'function') {
|
||||||
@@ -163,23 +269,38 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
|||||||
// LWC 4 无 priceScale 订阅:采样坐标变化(含增量 setData 后自动缩放)
|
// LWC 4 无 priceScale 订阅:采样坐标变化(含增量 setData 后自动缩放)
|
||||||
var sampleSig = function () {
|
var sampleSig = function () {
|
||||||
var boxes = window._fxBoxVerticals || [];
|
var boxes = window._fxBoxVerticals || [];
|
||||||
|
var labels = window._areaTextLabels || [];
|
||||||
var series = getMainPriceSeries();
|
var series = getMainPriceSeries();
|
||||||
if (!series || !boxes.length) return '0';
|
if (!series || (!boxes.length && !labels.length)) return '0';
|
||||||
var ts = mainChart.timeScale();
|
var ts = mainChart.timeScale();
|
||||||
|
var parts = [boxes.length, labels.length, quant(plotLeftOffset(mainChart))];
|
||||||
|
if (boxes.length) {
|
||||||
var a = boxes[0];
|
var a = boxes[0];
|
||||||
var b = boxes[boxes.length - 1];
|
var b = boxes[boxes.length - 1];
|
||||||
return [
|
parts.push(
|
||||||
boxes.length,
|
|
||||||
quant(ts.timeToCoordinate(a.time)),
|
quant(ts.timeToCoordinate(a.time)),
|
||||||
quant(series.priceToCoordinate(a.hi)),
|
quant(series.priceToCoordinate(a.hi)),
|
||||||
quant(series.priceToCoordinate(a.lo)),
|
quant(series.priceToCoordinate(a.lo)),
|
||||||
quant(ts.timeToCoordinate(b.time)),
|
quant(ts.timeToCoordinate(b.time)),
|
||||||
quant(series.priceToCoordinate(b.hi)),
|
quant(series.priceToCoordinate(b.hi)),
|
||||||
quant(series.priceToCoordinate(b.lo))
|
quant(series.priceToCoordinate(b.lo))
|
||||||
].join('|');
|
);
|
||||||
|
}
|
||||||
|
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 redraw = function () {
|
||||||
var boxes = window._fxBoxVerticals || [];
|
var boxes = window._fxBoxVerticals || [];
|
||||||
|
var labels = window._areaTextLabels || [];
|
||||||
var series = getMainPriceSeries();
|
var series = getMainPriceSeries();
|
||||||
var rect = mainChartContainer.getBoundingClientRect();
|
var rect = mainChartContainer.getBoundingClientRect();
|
||||||
var dpr = window.devicePixelRatio || 1;
|
var dpr = window.devicePixelRatio || 1;
|
||||||
@@ -191,26 +312,41 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
|||||||
if (!ctx2) return;
|
if (!ctx2) return;
|
||||||
ctx2.setTransform(dpr, 0, 0, dpr, 0, 0);
|
ctx2.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
ctx2.clearRect(0, 0, rect.width, rect.height);
|
ctx2.clearRect(0, 0, rect.width, rect.height);
|
||||||
if (!series || !boxes.length) {
|
if (!series || (!boxes.length && !labels.length)) {
|
||||||
lastSig = sampleSig();
|
lastSig = sampleSig();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var ts = mainChart.timeScale();
|
var ts = mainChart.timeScale();
|
||||||
|
var x0 = plotLeftOffset(mainChart);
|
||||||
for (var i = 0; i < boxes.length; i++) {
|
for (var i = 0; i < boxes.length; i++) {
|
||||||
var box = boxes[i];
|
var box = boxes[i];
|
||||||
var x = ts.timeToCoordinate(box.time);
|
var x = ts.timeToCoordinate(box.time);
|
||||||
var y1 = series.priceToCoordinate(box.hi);
|
var y1 = series.priceToCoordinate(box.hi);
|
||||||
var y2 = series.priceToCoordinate(box.lo);
|
var y2 = series.priceToCoordinate(box.lo);
|
||||||
if (x == null || y1 == null || y2 == null) continue;
|
if (x == null || y1 == null || y2 == null) continue;
|
||||||
|
var px = Math.round(x + x0) + 0.5;
|
||||||
ctx2.beginPath();
|
ctx2.beginPath();
|
||||||
ctx2.strokeStyle = box.color;
|
ctx2.strokeStyle = box.color;
|
||||||
ctx2.lineWidth = 1;
|
ctx2.lineWidth = 1;
|
||||||
ctx2.setLineDash([4, 3]);
|
ctx2.setLineDash([4, 3]);
|
||||||
ctx2.moveTo(Math.round(x) + 0.5, y1);
|
ctx2.moveTo(px, y1);
|
||||||
ctx2.lineTo(Math.round(x) + 0.5, y2);
|
ctx2.lineTo(px, y2);
|
||||||
ctx2.stroke();
|
ctx2.stroke();
|
||||||
}
|
}
|
||||||
ctx2.setLineDash([]);
|
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();
|
lastSig = sampleSig();
|
||||||
};
|
};
|
||||||
var scheduleRedraw = function () {
|
var scheduleRedraw = function () {
|
||||||
@@ -257,6 +393,7 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
|||||||
|
|
||||||
function chartTvRenderOverlays(ctx) {
|
function chartTvRenderOverlays(ctx) {
|
||||||
window._fxBoxVerticals = [];
|
window._fxBoxVerticals = [];
|
||||||
|
window._areaTextLabels = [];
|
||||||
var symbol = ctx.symbol;
|
var symbol = ctx.symbol;
|
||||||
var timeframe = ctx.timeframe;
|
var timeframe = ctx.timeframe;
|
||||||
var symbolConfig = ctx.symbolConfig;
|
var symbolConfig = ctx.symbolConfig;
|
||||||
@@ -990,56 +1127,6 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
window.bspMarkers = [];
|
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` 复选框
|
// 同时响应顶部的 `#showMainBsp` 复选框
|
||||||
@@ -1615,7 +1702,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
lineStyle: 2, // 虚线
|
lineStyle: 2, // 虚线
|
||||||
lastValueVisible: false,
|
lastValueVisible: false,
|
||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
title: '次周期布林上轨'
|
title: '次布林上轨'
|
||||||
});
|
});
|
||||||
elementUpperBandSeries.setData(elementUpperBandData);
|
elementUpperBandSeries.setData(elementUpperBandData);
|
||||||
|
|
||||||
@@ -1626,7 +1713,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
lineStyle: 2, // 虚线
|
lineStyle: 2, // 虚线
|
||||||
lastValueVisible: false,
|
lastValueVisible: false,
|
||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
title: '次周期布林下轨'
|
title: '次布林下轨'
|
||||||
});
|
});
|
||||||
elementLowerBandSeries.setData(elementLowerBandData);
|
elementLowerBandSeries.setData(elementLowerBandData);
|
||||||
|
|
||||||
@@ -1636,7 +1723,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
lastValueVisible: false,
|
lastValueVisible: false,
|
||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
title: '次周期布林中轨'
|
title: '次布林中轨'
|
||||||
});
|
});
|
||||||
elementMiddleBandSeries.setData(elementMiddleBandData);
|
elementMiddleBandSeries.setData(elementMiddleBandData);
|
||||||
|
|
||||||
@@ -1751,7 +1838,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
const fxMarker = {
|
const fxMarker = {
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
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}分<br>
|
||||||
强度等级: ${fx.fx_strength_level}<br>
|
强度等级: ${fx.fx_strength_level}<br>
|
||||||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||||||
@@ -1815,7 +1902,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
const fxMarker = {
|
const fxMarker = {
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
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}分<br>
|
||||||
强度等级: ${fx.fx_strength_level}<br>
|
强度等级: ${fx.fx_strength_level}<br>
|
||||||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||||||
@@ -1852,6 +1939,14 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
window.mainFxMarkers = [];
|
window.mainFxMarkers = [];
|
||||||
window.fxMarkers = [];
|
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) ||
|
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) ||
|
($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) ||
|
||||||
@@ -1940,7 +2035,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
const elementFxMarker = {
|
const elementFxMarker = {
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
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}分<br>
|
||||||
强度等级: ${fx.fx_strength_level}<br>
|
强度等级: ${fx.fx_strength_level}<br>
|
||||||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||||||
@@ -1996,7 +2091,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
const elementFxMarker = {
|
const elementFxMarker = {
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
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}分<br>
|
||||||
强度等级: ${fx.fx_strength_level}<br>
|
强度等级: ${fx.fx_strength_level}<br>
|
||||||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||||||
@@ -2157,7 +2252,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers);
|
trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并标记并设置
|
// 合并标记并设置(主图不画 U/穿零轴,只留背驰 SD/CD)
|
||||||
const combinedMarkers = [
|
const combinedMarkers = [
|
||||||
...(window.mainFxMarkers || []),
|
...(window.mainFxMarkers || []),
|
||||||
...allElementFxMarkers,
|
...allElementFxMarkers,
|
||||||
@@ -2165,17 +2260,15 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
...(window.kluDivMarkersElement || []),
|
...(window.kluDivMarkersElement || []),
|
||||||
...(window.kluDivMarkersSubSub || []),
|
...(window.kluDivMarkersSubSub || []),
|
||||||
...trendMarkersToUse,
|
...trendMarkersToUse,
|
||||||
...(window.bspMarkers || []),
|
...(window.bspMarkers || [])
|
||||||
...(window.fastBspMarkers || [])
|
|
||||||
];
|
];
|
||||||
if (combinedMarkers.length > 0) {
|
if (combinedMarkers.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
'合并设置', combinedMarkers.length, '个标记(主周期分型:',
|
'合并设置', combinedMarkers.length, '个标记(主周期分型:',
|
||||||
(window.mainFxMarkers || []).length,
|
(window.mainFxMarkers || []).length,
|
||||||
'个,小周期分型:', allElementFxMarkers.length,
|
'个,小周期分型:', allElementFxMarkers.length,
|
||||||
'个,UnitTF:', (window.unittfMarkers || []).length,
|
'个,背驰:', (window.kluDivMarkersMain || []).length,
|
||||||
'个,BSP标记:', (window.bspMarkers || []).length,
|
'个,BSP标记:', (window.bspMarkers || []).length,
|
||||||
'个,第四类标记:', (window.fastBspMarkers || []).length,
|
|
||||||
'个)'
|
'个)'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2277,18 +2370,17 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
// 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记
|
// 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记
|
||||||
// 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记,
|
// 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记,
|
||||||
// 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。
|
// 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。
|
||||||
// 修复:把 BSP 标记一并合并进来。第四类买卖点同理,两处都要带上。
|
// 修复:把 BSP 标记一并合并进来。两处都要带上,否则后面这次 setMarkers 会把买卖点盖掉。
|
||||||
const onlyMainAndU = [
|
const onlyMainAndU = [
|
||||||
...(window.mainFxMarkers || []),
|
...(window.mainFxMarkers || []),
|
||||||
...(window.kluDivMarkersMain || []),
|
...(window.kluDivMarkersMain || []),
|
||||||
...(window.kluDivMarkersElement || []),
|
...(window.kluDivMarkersElement || []),
|
||||||
...(window.kluDivMarkersSubSub || []),
|
...(window.kluDivMarkersSubSub || []),
|
||||||
...trendMarkersToUse,
|
...trendMarkersToUse,
|
||||||
...(window.bspMarkers || []),
|
...(window.bspMarkers || [])
|
||||||
...(window.fastBspMarkers || [])
|
|
||||||
];
|
];
|
||||||
if (onlyMainAndU.length > 0) {
|
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'));
|
const klineType2 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ function chartTvBuildShell(ctx) {
|
|||||||
mainChart: null,
|
mainChart: null,
|
||||||
volumeChart: null,
|
volumeChart: null,
|
||||||
macdChart: null,
|
macdChart: null,
|
||||||
|
sentimentChart: null,
|
||||||
series: {
|
series: {
|
||||||
candleSeries: null,
|
candleSeries: null,
|
||||||
lineSeries: null,
|
lineSeries: null,
|
||||||
@@ -125,6 +126,7 @@ function chartTvBuildShell(ctx) {
|
|||||||
// 是否显示MACD
|
// 是否显示MACD
|
||||||
const showMacd = $('#showMacd').is(':checked');
|
const showMacd = $('#showMacd').is(':checked');
|
||||||
const showOriginalKline = $('#showOriginalKline').is(':checked');
|
const showOriginalKline = $('#showOriginalKline').is(':checked');
|
||||||
|
const showSentiment = ($('#dataSource').val() || 'crypto') === 'crypto' && $('#showDeriv').is(':checked');
|
||||||
|
|
||||||
// 创建主图容器
|
// 创建主图容器
|
||||||
const mainChartContainer = document.createElement('div');
|
const mainChartContainer = document.createElement('div');
|
||||||
@@ -153,25 +155,20 @@ function chartTvBuildShell(ctx) {
|
|||||||
// 如果需要显示MACD,创建MACD容器
|
// 如果需要显示MACD,创建MACD容器
|
||||||
let macdChartContainer = null;
|
let macdChartContainer = null;
|
||||||
let chanMacdChartContainer = null;
|
let chanMacdChartContainer = null;
|
||||||
if (showMacd) {
|
let sentimentChartContainer = null;
|
||||||
// 仅显示新的 ChanMACD 图:让其占用原 MACD+ChanMACD 的整体高度
|
if (showMacd && showSentiment) {
|
||||||
// 新布局:主图(40%) → ChanMACD(30%) → 成交量(17.5%) → ATR(12.5%)
|
mainChartContainer.style.height = '34%';
|
||||||
mainChartContainer.style.height = '40%';
|
|
||||||
|
|
||||||
// 隐藏旧 MACD 容器(不创建)
|
|
||||||
// 创建 ChanMACD 容器占据原 MACD+ChanMACD 高度(30%)
|
|
||||||
chanMacdChartContainer = document.createElement('div');
|
chanMacdChartContainer = document.createElement('div');
|
||||||
chanMacdChartContainer.style.width = '100%';
|
chanMacdChartContainer.style.width = '100%';
|
||||||
chanMacdChartContainer.style.height = '30%';
|
chanMacdChartContainer.style.height = 'calc(22% - 50px)';
|
||||||
chanMacdChartContainer.style.position = 'absolute';
|
chanMacdChartContainer.style.position = 'absolute';
|
||||||
chanMacdChartContainer.style.top = '40%';
|
chanMacdChartContainer.style.top = '34%';
|
||||||
chanMacdChartContainer.style.left = '0';
|
chanMacdChartContainer.style.left = '0';
|
||||||
chanMacdChartContainer.style.right = '0';
|
chanMacdChartContainer.style.right = '0';
|
||||||
chanMacdChartContainer.style.borderTop = '1px solid #e0e0e0';
|
chanMacdChartContainer.style.borderTop = '1px solid #e0e0e0';
|
||||||
chanMacdChartContainer.style.zIndex = '10';
|
chanMacdChartContainer.style.zIndex = '10';
|
||||||
// 水印:便于区分是新的 ChanMACD 子图
|
|
||||||
const chanMacdWatermark = document.createElement('div');
|
const chanMacdWatermark = document.createElement('div');
|
||||||
chanMacdWatermark.textContent = 'ChanMACD';
|
chanMacdWatermark.textContent = 'MACD';
|
||||||
chanMacdWatermark.style.position = 'absolute';
|
chanMacdWatermark.style.position = 'absolute';
|
||||||
chanMacdWatermark.style.top = '4px';
|
chanMacdWatermark.style.top = '4px';
|
||||||
chanMacdWatermark.style.left = '8px';
|
chanMacdWatermark.style.left = '8px';
|
||||||
@@ -179,31 +176,81 @@ function chartTvBuildShell(ctx) {
|
|||||||
chanMacdWatermark.style.color = '#888';
|
chanMacdWatermark.style.color = '#888';
|
||||||
chanMacdWatermark.style.pointerEvents = 'none';
|
chanMacdWatermark.style.pointerEvents = 'none';
|
||||||
chanMacdChartContainer.appendChild(chanMacdWatermark);
|
chanMacdChartContainer.appendChild(chanMacdWatermark);
|
||||||
|
volumeChartContainer.style.top = 'calc(56% - 50px)';
|
||||||
// 成交量位于 ChanMACD 之下
|
volumeChartContainer.style.height = 'calc(12% + 20px)';
|
||||||
volumeChartContainer.style.top = '70%';
|
atrChartContainer.style.top = 'calc(68% - 30px)';
|
||||||
volumeChartContainer.style.height = '17.5%';
|
atrChartContainer.style.height = 'calc(10% - 20px)';
|
||||||
|
} else if (showMacd) {
|
||||||
// ATR 位于最底部
|
mainChartContainer.style.height = '40%';
|
||||||
atrChartContainer.style.top = '87.5%';
|
chanMacdChartContainer = document.createElement('div');
|
||||||
atrChartContainer.style.height = '12.5%';
|
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 {
|
} else {
|
||||||
// 不显示MACD时的高度 - 主图、成交量图和ATR图分配
|
mainChartContainer.style.height = '55%';
|
||||||
mainChartContainer.style.height = '55%'; // 主图占55%
|
|
||||||
volumeChartContainer.style.top = '55%';
|
volumeChartContainer.style.top = '55%';
|
||||||
volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5%
|
volumeChartContainer.style.height = '22.5%';
|
||||||
|
atrChartContainer.style.top = '77.5%';
|
||||||
atrChartContainer.style.top = '77.5%'; // ATR图从77.5%位置开始
|
atrChartContainer.style.height = '22.5%';
|
||||||
atrChartContainer.style.height = '22.5%'; // ATR图占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(mainChartContainer);
|
||||||
container.appendChild(volumeChartContainer);
|
container.appendChild(volumeChartContainer);
|
||||||
container.appendChild(atrChartContainer);
|
container.appendChild(atrChartContainer);
|
||||||
if (showMacd) {
|
if (showMacd) {
|
||||||
// 只追加新的 ChanMACD 容器
|
|
||||||
container.appendChild(chanMacdChartContainer);
|
container.appendChild(chanMacdChartContainer);
|
||||||
}
|
}
|
||||||
|
if (showSentiment && sentimentChartContainer) {
|
||||||
|
container.appendChild(sentimentChartContainer);
|
||||||
|
}
|
||||||
|
|
||||||
// 防止同步过程中的无限循环(实际同步由 bindSyncEvents 负责)
|
// 防止同步过程中的无限循环(实际同步由 bindSyncEvents 负责)
|
||||||
|
|
||||||
@@ -221,6 +268,8 @@ function chartTvBuildShell(ctx) {
|
|||||||
chartHeight = macdChartContainer ? macdChartContainer.clientHeight : 0;
|
chartHeight = macdChartContainer ? macdChartContainer.clientHeight : 0;
|
||||||
} else if (chartType === 'chanmacd') {
|
} else if (chartType === 'chanmacd') {
|
||||||
chartHeight = chanMacdChartContainer ? chanMacdChartContainer.clientHeight : 0;
|
chartHeight = chanMacdChartContainer ? chanMacdChartContainer.clientHeight : 0;
|
||||||
|
} else if (chartType === 'sentiment') {
|
||||||
|
chartHeight = sentimentChartContainer ? sentimentChartContainer.clientHeight : 0;
|
||||||
} else {
|
} else {
|
||||||
chartHeight = mainChartContainer.clientHeight;
|
chartHeight = mainChartContainer.clientHeight;
|
||||||
}
|
}
|
||||||
@@ -376,9 +425,26 @@ function chartTvBuildShell(ctx) {
|
|||||||
// 创建MACD图表(如果需要):仅创建新的 ChanMACD 图
|
// 创建MACD图表(如果需要):仅创建新的 ChanMACD 图
|
||||||
let macdChart = null;
|
let macdChart = null;
|
||||||
let chanMacdChart = null;
|
let chanMacdChart = null;
|
||||||
|
let sentimentChart = null;
|
||||||
if (showMacd) {
|
if (showMacd) {
|
||||||
chanMacdChart = LightweightCharts.createChart(chanMacdChartContainer, createChartOptions(false, 'chanmacd'));
|
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(){
|
(function(){
|
||||||
@@ -495,10 +561,13 @@ function chartTvBuildShell(ctx) {
|
|||||||
ctx.atrChartContainer = atrChartContainer;
|
ctx.atrChartContainer = atrChartContainer;
|
||||||
ctx.macdChartContainer = macdChartContainer;
|
ctx.macdChartContainer = macdChartContainer;
|
||||||
ctx.chanMacdChartContainer = chanMacdChartContainer;
|
ctx.chanMacdChartContainer = chanMacdChartContainer;
|
||||||
|
ctx.sentimentChartContainer = sentimentChartContainer;
|
||||||
|
ctx.showSentiment = showSentiment;
|
||||||
ctx.mainChart = mainChart;
|
ctx.mainChart = mainChart;
|
||||||
ctx.volumeChart = volumeChart;
|
ctx.volumeChart = volumeChart;
|
||||||
ctx.atrChart = atrChart;
|
ctx.atrChart = atrChart;
|
||||||
ctx.macdChart = macdChart;
|
ctx.macdChart = macdChart;
|
||||||
ctx.chanMacdChart = chanMacdChart;
|
ctx.chanMacdChart = chanMacdChart;
|
||||||
|
ctx.sentimentChart = sentimentChart;
|
||||||
ctx.createChartOptions = createChartOptions;
|
ctx.createChartOptions = createChartOptions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,10 +74,10 @@ function readChartFormContext() {
|
|||||||
return {
|
return {
|
||||||
dataSource: dataSource,
|
dataSource: dataSource,
|
||||||
symbol: symbol,
|
symbol: symbol,
|
||||||
timeframe: $('#timeframe').val() || window.DEFAULT_MAIN_TIMEFRAME || '4h',
|
timeframe: $('#timeframe').val() || window.DEFAULT_MAIN_TIMEFRAME || '45m',
|
||||||
timezone: $('#timezone').val() || 'Asia/Shanghai',
|
timezone: $('#timezone').val() || 'Asia/Shanghai',
|
||||||
elementTimeframe: $('#elementTimeframe').val() || window.DEFAULT_ELEMENT_TIMEFRAME || '1m',
|
elementTimeframe: $('#elementTimeframe').val() || window.DEFAULT_ELEMENT_TIMEFRAME || '15m',
|
||||||
subSubTimeframe: $('#subSubTimeframe').val() || '',
|
subSubTimeframe: $('#subSubTimeframe').val() || window.DEFAULT_SUB_SUB_TIMEFRAME || '5m',
|
||||||
startTimeMs: $('#start_time').val() ? new Date($('#start_time').val()).getTime() : null,
|
startTimeMs: $('#start_time').val() ? new Date($('#start_time').val()).getTime() : null,
|
||||||
endTimeMs: $('#end_time').val() ? new Date($('#end_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) {
|
if (compareTimeframes(elementTimeframe, mainTimeframe) > 0) {
|
||||||
alert('元素时间周期必须小于或等于主图表时间周期。');
|
alert('次必须小于或等于主。');
|
||||||
setSmallerOrEqualTimeframe(); // 重置为最大的小于等于时间周期
|
setSmallerOrEqualTimeframe(); // 重置为最大的小于等于时间周期
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ $('#subSubTimeframe').change(function() {
|
|||||||
const subSub = $(this).val();
|
const subSub = $(this).val();
|
||||||
const elementTf = $('#elementTimeframe').val();
|
const elementTf = $('#elementTimeframe').val();
|
||||||
if (compareTimeframes(subSub, elementTf) > 0) {
|
if (compareTimeframes(subSub, elementTf) > 0) {
|
||||||
alert('次次周期必须小于或等于次周期。');
|
alert('次次必须小于或等于次。');
|
||||||
ensureSubSubLteElement();
|
ensureSubSubLteElement();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,67 @@ function clearChanMacdMarkers() {
|
|||||||
}
|
}
|
||||||
// 清空全局UnitTF标记,避免旧数据残留影响主图合并
|
// 清空全局UnitTF标记,避免旧数据残留影响主图合并
|
||||||
window.unittfMarkers = [];
|
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标记
|
// 添加所有ChanMACD标记
|
||||||
function addAllChanMacdMarkers(segList, unittfList, histsetList, stateMarkers) {
|
function addAllChanMacdMarkers(segList, unittfList, histsetList, stateMarkers) {
|
||||||
@@ -215,8 +276,7 @@ function addAllChanMacdMarkers(segList, unittfList, histsetList, stateMarkers) {
|
|||||||
|
|
||||||
// 保存到全局,供主图与分型一起统一合并绘制(仅在开关开启时)
|
// 保存到全局,供主图与分型一起统一合并绘制(仅在开关开启时)
|
||||||
console.log('DEBUG: U 标记数量:', signalMarkers.length);
|
console.log('DEBUG: U 标记数量:', signalMarkers.length);
|
||||||
const allowUMerge = (window.showUOnMain && window.showUOnElement);
|
window.unittfMarkers = [...signalMarkers, ...boundaryMarkers];
|
||||||
window.unittfMarkers = allowUMerge ? [...signalMarkers, ...boundaryMarkers] : [];
|
|
||||||
if (uTooltipMarkers.length > 0) {
|
if (uTooltipMarkers.length > 0) {
|
||||||
if (window.fxMarkers) {
|
if (window.fxMarkers) {
|
||||||
window.fxMarkers = [ ...window.fxMarkers, ...uTooltipMarkers ];
|
window.fxMarkers = [ ...window.fxMarkers, ...uTooltipMarkers ];
|
||||||
|
|||||||
+17
-15
@@ -26,10 +26,10 @@ function loadSymbols() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置默认时间范围:最近 1 个月
|
// 设置默认时间范围:现在倒退 1 周
|
||||||
function setDefaultTimeRange() {
|
function setDefaultTimeRange() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const daysBack = 30;
|
const daysBack = 7;
|
||||||
const start = new Date(now.getTime() - (daysBack * 24 * 60 * 60 * 1000));
|
const start = new Date(now.getTime() - (daysBack * 24 * 60 * 60 * 1000));
|
||||||
|
|
||||||
// 格式化为datetime-local输入框所需的格式 YYYY-MM-DDThh:mm
|
// 格式化为datetime-local输入框所需的格式 YYYY-MM-DDThh:mm
|
||||||
@@ -72,17 +72,20 @@ $(document).ready(function() {
|
|||||||
window.astockStatusInterval = null;
|
window.astockStatusInterval = null;
|
||||||
}
|
}
|
||||||
loadSymbols();
|
loadSymbols();
|
||||||
|
if (window.ChanDeriv) ChanDeriv.setVisible(true);
|
||||||
} else if (dataSource === 'a_stock') {
|
} else if (dataSource === 'a_stock') {
|
||||||
$('#cryptoSymbolContainer').hide();
|
$('#cryptoSymbolContainer').hide();
|
||||||
$('#astockSymbolContainer').show();
|
$('#astockSymbolContainer').show();
|
||||||
loadAStockSymbols();
|
loadAStockSymbols();
|
||||||
startAStockStatusUpdater();
|
startAStockStatusUpdater();
|
||||||
|
if (window.ChanDeriv) ChanDeriv.setVisible(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查初始数据源设置
|
// 检查初始数据源设置
|
||||||
const initialDataSource = $('#dataSource').val();
|
const initialDataSource = $('#dataSource').val();
|
||||||
|
if (window.ChanDeriv) ChanDeriv.setVisible(initialDataSource !== 'a_stock');
|
||||||
if (initialDataSource === 'a_stock') {
|
if (initialDataSource === 'a_stock') {
|
||||||
$.getJSON('/api/chart_metadata', { source: 'a_stock' })
|
$.getJSON('/api/chart_metadata', { source: 'a_stock' })
|
||||||
.done(function(meta) {
|
.done(function(meta) {
|
||||||
@@ -404,6 +407,7 @@ function mapTimeframeToInterval(timeframe) {
|
|||||||
'5m': '5',
|
'5m': '5',
|
||||||
'15m': '15',
|
'15m': '15',
|
||||||
'30m': '30',
|
'30m': '30',
|
||||||
|
'45m': '45',
|
||||||
'1h': '60',
|
'1h': '60',
|
||||||
'2h': '120',
|
'2h': '120',
|
||||||
'4h': '240',
|
'4h': '240',
|
||||||
@@ -547,6 +551,7 @@ function refreshChart(data, options) {
|
|||||||
if (currentData && currentData.ema52_dict) {
|
if (currentData && currentData.ema52_dict) {
|
||||||
updateEMA52Display(currentData);
|
updateEMA52Display(currentData);
|
||||||
}
|
}
|
||||||
|
if (window.ChanDeriv) ChanDeriv.sync({ overlay: false });
|
||||||
return;
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('增量刷新失败,回退全量重建:', e);
|
console.warn('增量刷新失败,回退全量重建:', e);
|
||||||
@@ -584,6 +589,7 @@ function refreshChart(data, options) {
|
|||||||
if (currentData && currentData.ema52_dict) {
|
if (currentData && currentData.ema52_dict) {
|
||||||
updateEMA52Display(currentData);
|
updateEMA52Display(currentData);
|
||||||
}
|
}
|
||||||
|
if (window.ChanDeriv) ChanDeriv.sync({ overlay: true });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,14 +603,9 @@ function refreshChartOnly() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定主周期MACD背离显示开关
|
// 绑定主/次/次次周期笔面积、线段面积显示开关
|
||||||
$('#showMainMacdDiv').change(function() {
|
$('#showMainBiArea, #showMainSegArea, #showElementBiArea, #showElementSegArea, #showSubSubBiArea, #showSubSubSegArea').change(function() {
|
||||||
refreshChartOnly();
|
updateChartDisplay();
|
||||||
});
|
|
||||||
|
|
||||||
// 绑定次周期MACD背离显示开关
|
|
||||||
$('#showElementMacdDiv').change(function() {
|
|
||||||
refreshChartOnly();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绑定分型类型显示开关(与笔一致:全量重建,避免增量路径标记未对齐)
|
// 绑定分型类型显示开关(与笔一致:全量重建,避免增量路径标记未对齐)
|
||||||
@@ -642,6 +643,12 @@ $('#toggleUOnElement').change(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 买卖点显示开关
|
// 买卖点显示开关
|
||||||
|
$('#showOi').change(function() {
|
||||||
|
if (window.ChanDeriv) ChanDeriv.loadOverlays();
|
||||||
|
});
|
||||||
|
$('#showDeriv').change(function() {
|
||||||
|
updateChartDisplay();
|
||||||
|
});
|
||||||
$('#showMainBsp').change(function() {
|
$('#showMainBsp').change(function() {
|
||||||
updateChartDisplay();
|
updateChartDisplay();
|
||||||
});
|
});
|
||||||
@@ -649,11 +656,6 @@ $('#showElementBsp').change(function() {
|
|||||||
updateChartDisplay();
|
updateChartDisplay();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 第四类买卖点(B4/S4)显示开关与过滤模式
|
|
||||||
$('#showMainFastBsp, #showElementFastBsp, #showSubSubFastBsp, #fastBspFilterMode').change(function() {
|
|
||||||
updateChartDisplay();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在控制台输出当前显示状态
|
// 在控制台输出当前显示状态
|
||||||
console.log('当前显示状态:', {
|
console.log('当前显示状态:', {
|
||||||
'showOriginalKline': $('#showOriginalKline').is(':checked'),
|
'showOriginalKline': $('#showOriginalKline').is(':checked'),
|
||||||
|
|||||||
+103
-44
@@ -858,6 +858,38 @@
|
|||||||
color: #999;
|
color: #999;
|
||||||
margin-top: 2px;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -935,21 +967,31 @@
|
|||||||
<!-- 添加K线周期切换 -->
|
<!-- 添加K线周期切换 -->
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="radio" name="klinePeriod" id="mainPeriodKline" checked>
|
<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>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="radio" name="klinePeriod" id="elementPeriodKline">
|
<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>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="radio" name="klinePeriod" id="subSubPeriodKline">
|
<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>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" id="showMacd" checked>
|
<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>
|
<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>
|
</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">
|
<div class="d-flex align-items-center mb-2">
|
||||||
<label for="refreshInterval" class="form-label me-2 mb-0">自动刷新:</label>
|
<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;">
|
<select id="refreshInterval" class="form-select form-select-sm me-2" style="width: 80px;">
|
||||||
@@ -977,7 +1019,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center mt-1">
|
<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">
|
<div class="form-check form-check-inline">
|
||||||
<select id="timeframe" class="form-select form-select-sm me-2" style="width: 100px;">
|
<select id="timeframe" class="form-select form-select-sm me-2" style="width: 100px;">
|
||||||
{% for value, label in timeframes.items() %}
|
{% for value, label in timeframes.items() %}
|
||||||
@@ -998,11 +1040,11 @@
|
|||||||
<label class="form-check-label" for="showMainZs">SEG中枢</label>
|
<label class="form-check-label" for="showMainZs">SEG中枢</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
<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>
|
<label class="form-check-label" for="showMainBiZs">BI中枢</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
<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>
|
<label class="form-check-label" for="showKlcFxType">KLC分型</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
@@ -1010,27 +1052,24 @@
|
|||||||
<label class="form-check-label" for="showMainTrend">Trend</label>
|
<label class="form-check-label" for="showMainTrend">Trend</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
<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>
|
<label class="form-check-label" for="toggleUOnMain">显示U</label>
|
||||||
</div>
|
</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">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" id="showMainBsp">
|
<input class="form-check-input" type="checkbox" id="showMainBsp">
|
||||||
<label class="form-check-label" for="showMainBsp">买卖点</label>
|
<label class="form-check-label" for="showMainBsp">买卖点</label>
|
||||||
</div>
|
</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>
|
||||||
<div class="d-flex align-items-center mt-1">
|
<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">
|
<div class="form-check form-check-inline">
|
||||||
<select id="elementTimeframe" class="form-select form-select-sm me-2" style="width: 100px;">
|
<select id="elementTimeframe" class="form-select form-select-sm me-2" style="width: 100px;">
|
||||||
{% for value, label in timeframes.items() %}
|
{% for value, label in timeframes.items() %}
|
||||||
@@ -1066,17 +1105,21 @@
|
|||||||
<input class="form-check-input" type="checkbox" id="toggleUOnElement">
|
<input class="form-check-input" type="checkbox" id="toggleUOnElement">
|
||||||
<label class="form-check-label" for="toggleUOnElement">显示U</label>
|
<label class="form-check-label" for="toggleUOnElement">显示U</label>
|
||||||
</div>
|
</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">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" id="showElementBsp">
|
<input class="form-check-input" type="checkbox" id="showElementBsp">
|
||||||
<label class="form-check-label" for="showElementBsp">买卖点</label>
|
<label class="form-check-label" for="showElementBsp">买卖点</label>
|
||||||
</div>
|
</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>
|
||||||
<div class="d-flex align-items-center mt-1">
|
<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">
|
<div class="form-check form-check-inline">
|
||||||
<select id="subSubTimeframe" class="form-select form-select-sm me-2" style="width: 100px;">
|
<select id="subSubTimeframe" class="form-select form-select-sm me-2" style="width: 100px;">
|
||||||
{% for value, label in timeframes.items() %}
|
{% for value, label in timeframes.items() %}
|
||||||
@@ -1113,12 +1156,16 @@
|
|||||||
<label class="form-check-label" for="toggleUOnSubSub">显示U</label>
|
<label class="form-check-label" for="toggleUOnSubSub">显示U</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" id="showSubSubBsp">
|
<input class="form-check-input" type="checkbox" id="showSubSubBiArea">
|
||||||
<label class="form-check-label" for="showSubSubBsp">买卖点</label>
|
<label class="form-check-label" for="showSubSubBiArea">笔面积</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" id="showSubSubFastBsp">
|
<input class="form-check-input" type="checkbox" id="showSubSubSegArea">
|
||||||
<label class="form-check-label" for="showSubSubFastBsp">第四类</label>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1126,6 +1173,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loadingIndicator" style="display:none;"></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 class="chart-container">
|
||||||
<div id="tradingview_chart"></div>
|
<div id="tradingview_chart"></div>
|
||||||
@@ -1278,24 +1336,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
<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/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/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_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_view.js') }}?v=20260912a"></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_lifecycle.js') }}?v=20260910c"></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_shell.js') }}?v=20260911l"></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_indicators.js') }}?v=20260911n"></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_chan.js') }}?v=20260910a"></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_overlays.js') }}?v=20260911j"></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_finalize.js') }}?v=20260910c"></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_tv.js') }}?v=20260910c"></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_sync.js') }}?v=20260910c"></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/chart_tables.js') }}?v=20260911j"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260827a"></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/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">
|
<div id="maConfigModal" class="ma-config-modal">
|
||||||
@@ -1479,7 +1538,7 @@
|
|||||||
<!-- MACD参数配置弹窗 -->
|
<!-- MACD参数配置弹窗 -->
|
||||||
<div id="macdConfigModal" class="macd-config-modal">
|
<div id="macdConfigModal" class="macd-config-modal">
|
||||||
<div class="macd-config-content">
|
<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-form">
|
||||||
<div class="macd-config-group">
|
<div class="macd-config-group">
|
||||||
<label class="macd-config-label">快线周期 (Fast)</label>
|
<label class="macd-config-label">快线周期 (Fast)</label>
|
||||||
|
|||||||
@@ -105,6 +105,32 @@ def test_analyze_chan_keys_on_fixture():
|
|||||||
assert k in result["chan_macd"]
|
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():
|
def test_serialize_chan_macd_shape():
|
||||||
from pytz import timezone
|
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