添加识别中继分型

This commit is contained in:
jackyu66git
2026-03-20 18:39:02 +08:00
parent f1b0daa55f
commit 0bc36e066f
7 changed files with 402 additions and 375 deletions
+67 -5
View File
@@ -439,6 +439,11 @@ def add_indicators(df):
df['ema24'] = (ta.EMA(df, timeperiod=24)).fillna(0)
df['ema52'] = (ta.EMA(df, timeperiod=52)).fillna(0)
df['ema26'] = (ta.EMA(df, timeperiod=26)).fillna(0)
df['ema13'] = (ta.EMA(df, timeperiod=13)).fillna(0)
df['ema7'] = (ta.EMA(df, timeperiod=7)).fillna(0)
df['ema104'] = (ta.EMA(df, timeperiod=104)).fillna(0)
df['ema156'] = (ta.EMA(df, timeperiod=156)).fillna(0)
df['ema208'] = (ta.EMA(df, timeperiod=208)).fillna(0)
# 常用SMA 24/52
try:
df['sma24'] = (ta.SMA(df, timeperiod=24)).fillna(0)
@@ -624,6 +629,16 @@ def analyze_chan(df, symbol=None, timeframe=None):
# 如果分型强度小于1,设为0
if fx_strength < 1:
fx_strength = 0
# KLC 分型框(起止时间+高低价):
# 仅使用 cal_fx_box 通过 display 条件后生成的 klc.fx_box。
# 若无 fx_box,则前端不应绘制分型框。
fx_box = getattr(klc, 'fx_box', None)
box_start_time = getattr(fx_box, 'start_time', None) if fx_box else None
box_end_time = getattr(fx_box, 'end_time', None) if fx_box else None
box_high = getattr(fx_box, 'high', None) if fx_box else None
box_low = getattr(fx_box, 'low', None) if fx_box else None
if klc.bb_out:
klc_fx_info.append({
'time': klc.end_time,
@@ -632,10 +647,22 @@ def analyze_chan(df, symbol=None, timeframe=None):
'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM,
'fx_strength': fx_strength, # 分型强度分数 (0-100)
'fx_strength_level': fx_strength_level, # 分型强度等级 (极强/强/中等/弱/极弱)
'is_strong_fx': is_strong_fx # 是否为强分型
'is_strong_fx': is_strong_fx, # 是否为强分型
# 虚线分型框信息(给前端画框用)
'start_time': box_start_time,
'end_time': box_end_time,
'high': float(box_high) if box_high is not None else None,
'low': float(box_low) if box_low is not None else None,
})
except Exception as e:
# 如果出错,仍然添加基本信息,但分型强度为0
fx_box = getattr(klc, 'fx_box', None)
box_start_time = getattr(fx_box, 'start_time', None) if fx_box else None
box_end_time = getattr(fx_box, 'end_time', None) if fx_box else None
box_high = getattr(fx_box, 'high', None) if fx_box else None
box_low = getattr(fx_box, 'low', None) if fx_box else None
klc_fx_info.append({
'time': klc.end_time,
'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high,
@@ -643,7 +670,13 @@ def analyze_chan(df, symbol=None, timeframe=None):
'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM,
'fx_strength': 0,
'fx_strength_level': "",
'is_strong_fx': False
'is_strong_fx': False,
# 虚线分型框信息(给前端画框用)
'start_time': box_start_time,
'end_time': box_end_time,
'high': float(box_high) if box_high is not None else None,
'low': float(box_low) if box_low is not None else None,
})
@@ -1391,12 +1424,17 @@ def analyze():
# 添加K线分型信息
'klc_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'start_time': format_time_safely(point['start_time'], client_tz),
'end_time': format_time_safely(point['end_time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
'is_strong_fx': bool(point['is_strong_fx']), # 是否为强分型
# 分型框(虚线矩形)用到的高低价
'high': float(point['high']) if point.get('high') is not None else None,
'low': float(point['low']) if point.get('low') is not None else None
} for point in analysis_result['klc_fx_info']],
# 添加ChanMACD分析数据
'chan_macd': serialize_chan_macd_data(analysis_result.get('chan_macd', {}), client_tz),
@@ -1587,12 +1625,17 @@ def analyze():
# 添加小周期分型信息
result['element_klc_fx_info'] = [{
'time': format_time_safely(point['time'], client_tz),
'start_time': format_time_safely(point['start_time'], client_tz),
'end_time': format_time_safely(point['end_time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
'is_strong_fx': bool(point['is_strong_fx']), # 是否为强分型
# 分型框(虚线矩形)用到的高低价
'high': float(point['high']) if point.get('high') is not None else None,
'low': float(point['low']) if point.get('low') is not None else None
} for point in element_analysis['klc_fx_info']]
# 添加次周期ChanMACD分析数据
@@ -1640,6 +1683,20 @@ def analyze():
'direction': convert_direction(bi.dir),
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
} for bi in sub_sub_analysis['bi_list'] if not bi.end_klc]
# 次次周期 KLC 列表
result['sub_sub_klc_list'] = [{
'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(),
'open': float(klc.open),
'high': float(klc.high),
'low': float(klc.low),
'close': float(klc.close),
'volume': float(klc.volume) if hasattr(klc, 'volume') else 0,
'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''),
'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''),
'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''),
'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '')
} for klc in sub_sub_analysis.get('klc_list', []) if hasattr(klc, 'end_time') and klc.end_time]
result['sub_sub_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(),
'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None,
@@ -1677,12 +1734,17 @@ def analyze():
} for zs in sub_sub_analysis.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)]
result['sub_sub_klc_fx_info'] = [{
'time': format_time_safely(point['time'], client_tz),
'start_time': format_time_safely(point['start_time'], client_tz),
'end_time': format_time_safely(point['end_time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']),
'fx_strength_level': str(point['fx_strength_level']),
'is_strong_fx': bool(point['is_strong_fx'])
'is_strong_fx': bool(point['is_strong_fx']),
# 分型框(虚线矩形)用到的高低价
'high': float(point['high']) if point.get('high') is not None else None,
'low': float(point['low']) if point.get('low') is not None else None
} for point in sub_sub_analysis['klc_fx_info']]
result['sub_sub_bsp_list'] = [{
'time': format_time_safely(bsp.end_time, client_tz),
+255 -45
View File
@@ -5817,6 +5817,64 @@
allMainFxMarkers.push(markerConfig);
// 画虚线分型框(根据 start/end + high/low
if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) {
const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000);
const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000);
const high = parseFloat(fx.high);
const low = parseFloat(fx.low);
if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) {
const boxHigh = Math.max(high, low);
const boxLow = Math.min(high, low);
const boxColor = strengthColor;
const topSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
const bottomSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
const leftSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
// 左边竖线:同一 time 上下两个点(和你已有ZS绘制写法保持一致)
leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
const rightSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = [];
tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
}
}
// 创建分型标记对象,包含tooltip信息
const fxMarker = {
time: timestamp,
@@ -5966,6 +6024,63 @@
allElementFxMarkers.push(markerConfig);
// 画虚线分型框(小周期)
if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) {
const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000);
const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000);
const high = parseFloat(fx.high);
const low = parseFloat(fx.low);
if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) {
const boxHigh = Math.max(high, low);
const boxLow = Math.min(high, low);
const boxColor = strengthColor;
const topSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
const bottomSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
const leftSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
const rightSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = [];
tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
}
}
// 创建小周期分型标记对象,包含tooltip信息
const elementFxMarker = {
time: timestamp,
@@ -6061,6 +6176,63 @@
size: (fx.is_strong_fx ? 0.6 : 0.5)
};
allElementFxMarkers.push(markerConfig);
// 画虚线分型框(次次周期)
if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) {
const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000);
const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000);
const high = parseFloat(fx.high);
const low = parseFloat(fx.low);
if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) {
const boxHigh = Math.max(high, low);
const boxLow = Math.min(high, low);
const boxColor = strengthColor;
const topSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
const bottomSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
const leftSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
const rightSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = [];
tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
}
}
} catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); }
});
}
@@ -6200,7 +6372,11 @@
else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries;
else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries;
if (targetSeries) {
targetSeries.setMarkers(combinedMarkers);
try {
targetSeries.setMarkers(combinedMarkers);
} catch (e) {
console.warn('设置主系列标记失败(可能series已释放):', e);
}
} else {
console.log('未找到主数据系列,无法设置标记');
}
@@ -6324,7 +6500,11 @@
else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries;
else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries;
if (targetSeries2) {
targetSeries2.setMarkers(onlyMainAndU);
try {
targetSeries2.setMarkers(onlyMainAndU);
} catch (e) {
console.warn('设置主系列标记失败(可能series已释放):', e);
}
} else {
console.log('未找到主数据系列,无法设置标记');
}
@@ -6342,7 +6522,11 @@
else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries;
else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries;
if (targetSeries3) {
targetSeries3.setMarkers([]);
try {
targetSeries3.setMarkers([]);
} catch (e) {
console.warn('清空主系列标记失败(可能series已释放):', e);
}
}
}
}
@@ -6552,15 +6736,33 @@
// 检查是否显示原始K线
const showOriginalKline = $('#showOriginalKline').is(':checked');
// 检查是否使用小周期数据
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
// 检查是否使用次次周期 / 小周期数据
const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') &&
currentData.sub_sub_timeframe &&
currentData.sub_sub_kline_data &&
Array.isArray(currentData.sub_sub_kline_data);
const useElementPeriod = !useSubSubPeriod &&
$('#elementPeriodKline').is(':checked') &&
currentData.element_timeframe &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
// 转换K线数据
let candles = [];
if (useElementPeriod) {
if (useSubSubPeriod) {
console.log('使用次次周期K线数据');
candles = currentData.sub_sub_kline_data.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
} else if (useElementPeriod) {
console.log('使用小周期K线数据');
candles = currentData.element_kline_data.map((kline) => {
const date = new Date(kline.date);
@@ -6622,7 +6824,16 @@
// 更新成交量数据
let volumes = [];
if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) {
if (useSubSubPeriod && currentData.sub_sub_kline_data && Array.isArray(currentData.sub_sub_kline_data)) {
volumes = currentData.sub_sub_kline_data.map(kline => {
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
return {
time: timestamp,
value: parseFloat(kline.volume),
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)',
};
});
} else if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) {
volumes = currentData.element_kline_data.map(kline => {
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
return {
@@ -6649,9 +6860,9 @@
// 更新ATR数据
if (tvWidget.series.atrLineSeries) {
const atrData = [];
const atrDataSource = useElementPeriod ?
(currentData.element_atr || currentData.atr) :
currentData.atr;
const atrDataSource = useSubSubPeriod ?
(currentData.sub_sub_atr || currentData.atr) :
(useElementPeriod ? (currentData.element_atr || currentData.atr) : currentData.atr);
if (atrDataSource && Array.isArray(atrDataSource)) {
const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
@@ -9187,44 +9398,39 @@
try { updateIndicatorPanel(); } catch(e) {}
try { if ($('#maConfigModal').is(':visible')) { hideMAConfig(); } } catch(e) {}
}
// 获取当前K线数据的辅助函数
// 获取当前K线数据的辅助函数(与基础显示的主/小/次次周期保持一致)
function getCurrentCandleData() {
if (!currentData || !currentData.kline_data) {
return [];
}
// 检查是否使用小周期数据
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') &&
currentData.sub_sub_kline_data &&
Array.isArray(currentData.sub_sub_kline_data);
const useElementPeriod = !useSubSubPeriod &&
$('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
let candles = [];
if (useElementPeriod) {
candles = currentData.element_kline_data.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
} else {
candles = currentData.kline_data.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
let source = currentData.kline_data;
if (useSubSubPeriod) {
source = currentData.sub_sub_kline_data;
} else if (useElementPeriod) {
source = currentData.element_kline_data;
}
const candles = source.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
return candles;
}
// 从蜡烛数据生成 Heikin-Ashi(平均K
@@ -9253,14 +9459,20 @@
function buildKLCFromAnalysis(data) {
if (!data) return [];
// 检查是否使用小周期数据
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
data.element_klc_list &&
// 根据基础显示的K线周期选择:次次周期 / 小周期 / 主周期
const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') &&
data.sub_sub_klc_list &&
Array.isArray(data.sub_sub_klc_list);
const useElementPeriod = !useSubSubPeriod &&
$('#elementPeriodKline').is(':checked') &&
data.element_klc_list &&
Array.isArray(data.element_klc_list);
const klcList = useElementPeriod ? data.element_klc_list : data.klc_list;
const klcList = useSubSubPeriod
? data.sub_sub_klc_list
: (useElementPeriod ? data.element_klc_list : data.klc_list);
if (!klcList) return [];
if (!klcList || !Array.isArray(klcList)) return [];
const klcCandles = [];
@@ -9268,11 +9480,9 @@
klcList.forEach(klc => {
if (!klc || !klc.date) return;
// 使用KLC的date字段,转换为时间戳格式
const date = new Date(klc.date);
const timestamp = date.getTime() / 1000;
// 创建KLC蜡烛数据
const candle = {
time: timestamp,
open: klc.open || 0,