主图增加笔/线段背驰与面积数字,并修正 SD99999 显示。

三周期分开关控制,只画数字不画图标;线段面积比沿用同向笔面积口径。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-09-10 04:33:40 +08:00
co-authored by Cursor
parent 6b72ba226b
commit 2cd50f1e01
11 changed files with 378 additions and 117 deletions
+1
View File
@@ -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
+23
View File
@@ -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:
+21 -9
View File
@@ -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'] = [{
+4
View File
@@ -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
+11 -3
View File
@@ -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:
-64
View File
@@ -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);
} }
+18 -10
View File
@@ -1,5 +1,10 @@
/* 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 chartTvRenderIndicators(ctx) { function chartTvRenderIndicators(ctx) {
var symbol = ctx.symbol; var symbol = ctx.symbol;
var timeframe = ctx.timeframe; var timeframe = ctx.timeframe;
@@ -141,11 +146,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);
@@ -390,7 +395,7 @@ function chartTvRenderIndicators(ctx) {
if (Number(item.separate_div) > 0) { if (Number(item.separate_div) > 0) {
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({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 });
} }
if (item.continue_div === true) { if (item.continue_div === true) {
const macdVal = mainMacdMap.get(ts); const macdVal = mainMacdMap.get(ts);
@@ -412,7 +417,7 @@ function chartTvRenderIndicators(ctx) {
if (Number(item.separate_div) > 0) { if (Number(item.separate_div) > 0) {
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({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 });
} }
if (item.continue_div === true) { if (item.continue_div === true) {
const macdVal = elementMacdMap.get(ts); const macdVal = elementMacdMap.get(ts);
@@ -436,7 +441,7 @@ function chartTvRenderIndicators(ctx) {
if (Number(item.separate_div) > 0) { if (Number(item.separate_div) > 0) {
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({ time: ts, position: posSd, color: '#00897b', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 });
} }
if (item.continue_div === true) { if (item.continue_div === true) {
const macdVal = subSubMacdMap.get(ts); const macdVal = subSubMacdMap.get(ts);
@@ -502,7 +507,7 @@ function chartTvRenderIndicators(ctx) {
if (Number(item.separate_div) > 0) { if (Number(item.separate_div) > 0) {
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({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 });
} }
if (item.continue_div === true) { if (item.continue_div === true) {
const macdVal = mainMacdMapAll.get(ts); const macdVal = mainMacdMapAll.get(ts);
@@ -522,7 +527,7 @@ function chartTvRenderIndicators(ctx) {
if (Number(item.separate_div) > 0) { if (Number(item.separate_div) > 0) {
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({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 });
} }
if (item.continue_div === true) { if (item.continue_div === true) {
const macdVal = elementMacdMapAll.get(ts); const macdVal = elementMacdMapAll.get(ts);
@@ -544,7 +549,7 @@ function chartTvRenderIndicators(ctx) {
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 (Number(item.separate_div) > 0) {
subSubMarkersAll.push({ time: ts, position: 'aboveBar', color: '#00897b', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); subSubMarkersAll.push({ time: ts, position: 'aboveBar', color: '#00897b', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 });
} }
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 }); subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 });
@@ -555,6 +560,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 = [];
+208 -8
View File
@@ -82,6 +82,169 @@ 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 parseAreaDivValue(v) {
var n = Number(v);
return (isFinite(n) && n !== 0) ? n : 0;
}
function pushAreaTextLabel(out, item, style, text) {
if (!item || !item.end_time || !text) return;
var ts = Math.floor(new Date(item.end_time).getTime() / 1000);
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 pushAreaDivMarker(out, item, style) {
var div = parseAreaDivValue(item && item.macd_div);
if (!div) return;
pushAreaTextLabel(out, item, style, div.toFixed(2));
}
function collectAreaDivMarkers(biList, uncompletedBi, segList, uncompletedSeg, biStyle, segStyle, showBi, showSeg) {
var out = [];
if (showBi) {
(biList || []).forEach(function (bi) { pushAreaDivMarker(out, bi, biStyle); });
(uncompletedBi || []).forEach(function (bi) { pushAreaDivMarker(out, bi, biStyle); });
}
if (showSeg) {
(segList || []).forEach(function (seg) { pushAreaDivMarker(out, seg, segStyle); });
(uncompletedSeg || []).forEach(function (seg) { pushAreaDivMarker(out, seg, segStyle); });
}
return out;
}
function formatMacdAreaText(v) {
var n = Number(v);
if (!isFinite(n) || n === 0) return '';
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 buildAreaDivMarkersFromData(data) {
var markers = [];
if (!data) return markers;
var showMainBi = $('#showMainMacdDiv').is(':checked');
var showMainSeg = $('#showMainSegMacdDiv').is(':checked');
if (showMainBi || showMainSeg) {
markers = markers.concat(collectAreaDivMarkers(
data.bi_list,
data.uncompleted_bi_list,
data.seg_list,
data.uncompleted_seg_list,
{ color: '#e53935', size: 0.6 },
{ color: '#fb8c00', size: 0.7 },
showMainBi,
showMainSeg
));
}
var showElementBi = $('#showElementMacdDiv').is(':checked');
var showElementSeg = $('#showElementSegMacdDiv').is(':checked');
if (showElementBi || showElementSeg) {
markers = markers.concat(collectAreaDivMarkers(
data.element_bi_list,
data.element_uncompleted_bi_list,
data.element_seg_list,
data.element_uncompleted_seg_list,
{ color: '#8e24aa', size: 0.55 },
{ color: '#5e35b1', size: 0.65 },
showElementBi,
showElementSeg
));
}
var showSubSubBi = $('#showSubSubMacdDiv').is(':checked');
var showSubSubSeg = $('#showSubSubSegMacdDiv').is(':checked');
if (showSubSubBi || showSubSubSeg) {
markers = markers.concat(collectAreaDivMarkers(
data.sub_sub_bi_list,
data.sub_sub_uncompleted_bi_list,
data.sub_sub_seg_list,
data.sub_sub_uncompleted_seg_list,
{ color: '#00897b', size: 0.5 },
{ color: '#00695c', size: 0.6 },
showSubSubBi,
showSubSubSeg
));
}
return markers;
}
function buildKlcTrendMarker(timeAligned, trendRaw, palette) { 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;
@@ -163,23 +326,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];
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,7 +369,7 @@ 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;
} }
@@ -211,6 +389,19 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
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), lab.above ? ly - 3 : ly + 3);
}
}
lastSig = sampleSig(); lastSig = sampleSig();
}; };
var scheduleRedraw = function () { var scheduleRedraw = function () {
@@ -257,6 +448,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;
@@ -1852,6 +2044,14 @@ function chartTvRenderOverlays(ctx) {
window.mainFxMarkers = []; window.mainFxMarkers = [];
window.fxMarkers = []; window.fxMarkers = [];
} }
window._areaTextLabels = alignMarkersToCandles(
buildAreaDivMarkersFromData(currentData).concat(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) ||
@@ -2157,7 +2357,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,
@@ -2173,7 +2373,7 @@ function chartTvRenderOverlays(ctx) {
'合并设置', 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, '个,第四类标记:', (window.fastBspMarkers || []).length,
'个)' '个)'
@@ -2288,7 +2488,7 @@ function chartTvRenderOverlays(ctx) {
...(window.fastBspMarkers || []) ...(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'));
+3 -8
View File
@@ -597,14 +597,9 @@ function refreshChartOnly() {
} }
} }
// 绑定主周期MACD背离显示开关 // 绑定主/次/次次周期笔背驰、线段背驰、笔面积、线段面积显示开关
$('#showMainMacdDiv').change(function() { $('#showMainMacdDiv, #showMainSegMacdDiv, #showElementMacdDiv, #showElementSegMacdDiv, #showSubSubMacdDiv, #showSubSubSegMacdDiv, #showMainBiArea, #showMainSegArea, #showElementBiArea, #showElementSegArea, #showSubSubBiArea, #showSubSubSegArea').change(function() {
refreshChartOnly(); updateChartDisplay();
});
// 绑定次周期MACD背离显示开关
$('#showElementMacdDiv').change(function() {
refreshChartOnly();
}); });
// 绑定分型类型显示开关(与笔一致:全量重建,避免增量路径标记未对齐) // 绑定分型类型显示开关(与笔一致:全量重建,避免增量路径标记未对齐)
+55 -7
View File
@@ -1013,6 +1013,22 @@
<input class="form-check-input" type="checkbox" id="toggleUOnMain"> <input class="form-check-input" type="checkbox" id="toggleUOnMain">
<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="showMainMacdDiv">
<label class="form-check-label" for="showMainMacdDiv">笔背驰</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainSegMacdDiv">
<label class="form-check-label" for="showMainSegMacdDiv">线段背驰</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainBiArea">
<label class="form-check-label" for="showMainBiArea">笔面积</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainSegArea">
<label class="form-check-label" for="showMainSegArea">线段面积</label>
</div>
<div class="form-check form-check-inline"> <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>
@@ -1066,6 +1082,22 @@
<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="showElementMacdDiv">
<label class="form-check-label" for="showElementMacdDiv">笔背驰</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementSegMacdDiv">
<label class="form-check-label" for="showElementSegMacdDiv">线段背驰</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementBiArea">
<label class="form-check-label" for="showElementBiArea">笔面积</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementSegArea">
<label class="form-check-label" for="showElementSegArea">线段面积</label>
</div>
<div class="form-check form-check-inline"> <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>
@@ -1112,6 +1144,22 @@
<input class="form-check-input" type="checkbox" id="toggleUOnSubSub"> <input class="form-check-input" type="checkbox" id="toggleUOnSubSub">
<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">
<input class="form-check-input" type="checkbox" id="showSubSubMacdDiv">
<label class="form-check-label" for="showSubSubMacdDiv">笔背驰</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showSubSubSegMacdDiv">
<label class="form-check-label" for="showSubSubSegMacdDiv">线段背驰</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showSubSubBiArea">
<label class="form-check-label" for="showSubSubBiArea">笔面积</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showSubSubSegArea">
<label class="form-check-label" for="showSubSubSegArea">线段面积</label>
</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="showSubSubBsp">
<label class="form-check-label" for="showSubSubBsp">买卖点</label> <label class="form-check-label" for="showSubSubBsp">买卖点</label>
@@ -1281,21 +1329,21 @@
<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=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/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=20260910a"></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=20260810e"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv_lifecycle.js') }}?v=20260810e"></script> <script defer src="{{ url_for('static', filename='js/app/chart_tv_lifecycle.js') }}?v=20260810e"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv_shell.js') }}?v=20260810e"></script> <script defer src="{{ url_for('static', filename='js/app/chart_tv_shell.js') }}?v=20260810e"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv_indicators.js') }}?v=20260808i"></script> <script defer src="{{ url_for('static', filename='js/app/chart_tv_indicators.js') }}?v=20260901e"></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=20260910d"></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=20260810a"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260808i"></script> <script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260808i"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260809z"></script> <script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260901e"></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=20260808i"></script>
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260827a"></script> <script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260910c"></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">
+26
View File
@@ -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