主站重建前完整 dispose、去掉重复 sync 监听,自动刷新默认增量更新;顺带消除首屏重复 analyze、复用 ChanMACD,以及全版 TV 指标/未完成中枢/布局本地缓存。 Co-authored-by: Cursor <cursoragent@cursor.com>
1395 lines
45 KiB
JavaScript
1395 lines
45 KiB
JavaScript
/**
|
||
* 缠论计算引擎 — 纯前端 JavaScript 实现
|
||
*
|
||
* 严格参照 Python 实现 (TF_DF.py, ChanBI.py, ChanSBI.py)
|
||
* 输入: OHLCV bar 数组 [{timestamp, open, high, low, close, volume}, ...]
|
||
* 输出: ChanSlice 格式 {bis, segs, zs, segzs, bsps, seg_bsps}
|
||
*/
|
||
|
||
var MIN_BI_KLC = 3 // Python: bi_klc_min = 3
|
||
|
||
// ======================== 包含处理 (KLC) ========================
|
||
|
||
/**
|
||
* 判断两根 K 线是否存在包含关系(a 完全包容 b)
|
||
* a 包含 b: a.high >= b.high && a.low <= b.low
|
||
*/
|
||
function hasInclusion(a, b) {
|
||
return a.high >= b.high && a.low <= b.low
|
||
}
|
||
|
||
/**
|
||
* 包含处理 — 参照 Python get_klc_list() (TF_DF.py:637-655)
|
||
*
|
||
* 方向决定规则 (Python line 639):
|
||
* last_klc.high < klu.high → UP, 否则 DOWN
|
||
* 第一根 KLC: open <= close → UP, 否则 DOWN
|
||
*
|
||
* 合并规则 (Python ChanKLC.check_klu_included):
|
||
* UP: 取 max(high), max(low) — 把 low 抬高
|
||
* DOWN: 取 min(high), min(low) — 把 high 压低
|
||
*/
|
||
function inclusionMerge(bars) {
|
||
if (bars.length < 2) return bars.map(function (b, i) {
|
||
return { idx: i, dir: 0, fx: null, timestamp: b.timestamp, datetime: b.datetime,
|
||
open: b.open, high: b.high, low: b.low, close: b.close, volume: b.volume }
|
||
})
|
||
|
||
var klcList = []
|
||
|
||
// 第一根 KLC: 方向基于 open vs close (Python line 650-653)
|
||
var first = bars[0]
|
||
var firstDir = first.open <= first.close ? 1 : -1
|
||
klcList.push({
|
||
idx: 0, dir: firstDir, fx: null,
|
||
timestamp: first.timestamp, datetime: first.datetime,
|
||
open: first.open, high: first.high, low: first.low, close: first.close,
|
||
volume: first.volume, _barCount: 1,
|
||
})
|
||
|
||
for (var i = 1; i < bars.length; i++) {
|
||
var lastKlc = klcList[klcList.length - 1]
|
||
var cur = bars[i]
|
||
|
||
if (hasInclusion(lastKlc, cur) || hasInclusion(cur, lastKlc)) {
|
||
// 包含关系:按上一 KLC 的方向合并
|
||
var mergeDir = lastKlc.dir
|
||
if (mergeDir === 1) {
|
||
// UP: 取 max
|
||
lastKlc.high = Math.max(lastKlc.high, cur.high)
|
||
lastKlc.low = Math.max(lastKlc.low, cur.low)
|
||
} else {
|
||
// DOWN: 取 min
|
||
lastKlc.high = Math.min(lastKlc.high, cur.high)
|
||
lastKlc.low = Math.min(lastKlc.low, cur.low)
|
||
}
|
||
lastKlc.close = cur.close
|
||
lastKlc.volume += cur.volume
|
||
lastKlc.timestamp = cur.timestamp
|
||
lastKlc.datetime = cur.datetime
|
||
lastKlc._barCount = (lastKlc._barCount || 1) + 1
|
||
} else {
|
||
// 无包含关系:创建新 KLC
|
||
var newDir = cur.high > lastKlc.high ? 1 : -1 // Python line 639
|
||
klcList.push({
|
||
idx: klcList.length, dir: newDir, fx: null,
|
||
timestamp: cur.timestamp, datetime: cur.datetime,
|
||
open: cur.open, high: cur.high, low: cur.low, close: cur.close,
|
||
volume: cur.volume, _barCount: 1,
|
||
})
|
||
}
|
||
}
|
||
|
||
return klcList
|
||
}
|
||
|
||
|
||
// ======================== 分型检测 (保留,供外部使用) ========================
|
||
|
||
/**
|
||
* 独立分型检测 — TF_DF.check_fx() 条件:
|
||
* TOP: klc.high > pre.high && klc.high > next.high
|
||
* && klc.low > pre.low && klc.low > next.low
|
||
* BOTTOM: klc.low < pre.low && klc.low < next.low
|
||
* && klc.high < pre.high && klc.high < next.high
|
||
*/
|
||
function findFractals(klcList) {
|
||
if (klcList.length < 3) return []
|
||
var fractals = []
|
||
for (var i = 1; i < klcList.length - 1; i++) {
|
||
var prev = klcList[i - 1], curr = klcList[i], next = klcList[i + 1]
|
||
if (curr.low < prev.low && curr.low < next.low &&
|
||
curr.high < prev.high && curr.high < next.high) {
|
||
fractals.push({ idx: i, klcIdx: curr.idx, type: 'BOTTOM', timestamp: curr.timestamp,
|
||
datetime: curr.datetime, price: curr.low, high: curr.high, low: curr.low })
|
||
curr.fx = 'BOTTOM'
|
||
}
|
||
if (curr.high > prev.high && curr.high > next.high &&
|
||
curr.low > prev.low && curr.low > next.low) {
|
||
fractals.push({ idx: i, klcIdx: curr.idx, type: 'TOP', timestamp: curr.timestamp,
|
||
datetime: curr.datetime, price: curr.high, high: curr.high, low: curr.low })
|
||
curr.fx = 'TOP'
|
||
}
|
||
}
|
||
return fractals
|
||
}
|
||
|
||
|
||
// ======================== 笔 (BI) 检测 ========================
|
||
|
||
/**
|
||
* 缺口测试 — Python TF_DF.check_top_fx (line 1277-1280)
|
||
* 当 last_bottom 的高点与候选顶分型两侧的低点重叠时,拒绝该顶分型
|
||
*/
|
||
function checkTopFx(lastBottom, klc, pre, next) {
|
||
if ((lastBottom.high > pre.low || lastBottom.high > next.low) &&
|
||
(klc.idx - lastBottom.idx < 100)) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 缺口测试 — Python TF_DF.check_bottom_fx (line 1282-1285)
|
||
*/
|
||
function checkBottomFx(lastTop, klc, pre, next) {
|
||
if ((lastTop.low < pre.high || lastTop.low < next.high) &&
|
||
(klc.idx - lastTop.idx < 100)) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 将内部 bi 列表转换为输出 strokes 格式
|
||
*/
|
||
function _biListToStrokes(biList) {
|
||
var strokes = []
|
||
for (var i = 0; i < biList.length; i++) {
|
||
var bi = biList[i]
|
||
if (!bi._startKlc) continue
|
||
|
||
var endKlc = bi._endKlc
|
||
if (!endKlc && bi._klcList && bi._klcList.length > 0) {
|
||
endKlc = bi._klcList[bi._klcList.length - 1]
|
||
}
|
||
// 末笔回退: 用 startKlc 作为终点 (至少显示一个点)
|
||
if (!endKlc) {
|
||
endKlc = bi._startKlc
|
||
}
|
||
|
||
var isUp = bi.dir === 'UP'
|
||
strokes.push({
|
||
idx: strokes.length,
|
||
t0: bi._startKlc.timestamp,
|
||
t1: endKlc.timestamp,
|
||
p0: isUp ? bi._startKlc.low : bi._startKlc.high,
|
||
p1: isUp ? endKlc.high : endKlc.low,
|
||
dir: isUp ? 1 : -1,
|
||
sure: bi._isSure || false,
|
||
startIdx: bi._startKlc.idx,
|
||
endIdx: endKlc.idx,
|
||
startType: isUp ? 'BOTTOM' : 'TOP',
|
||
endType: isUp ? 'TOP' : 'BOTTOM',
|
||
})
|
||
}
|
||
return strokes
|
||
}
|
||
|
||
function _addKlcToLastBi(biList, klc) {
|
||
if (biList.length > 0) {
|
||
biList[biList.length - 1]._klcList.push(klc)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 笔检测 — 严格匹配 Python cal_bi_list() (TF_DF.py:946-1255)
|
||
*
|
||
* 遍历每一根 KLC,逐根检测分型 + 缺口测试 + 状态机处理。
|
||
* 使用 lastTop/lastBottom 追踪,在满足结合律时确认笔。
|
||
*
|
||
* @param {Array} klcList — KLC 数组
|
||
* @returns {Array} strokes — 符合输出格式的笔列表
|
||
*/
|
||
function findStrokes(klcList) {
|
||
if (klcList.length < 3) return []
|
||
|
||
var biList = []
|
||
var lastTop = null // KLC 引用
|
||
var lastBottom = null // KLC 引用
|
||
|
||
for (var i = 1; i < klcList.length - 1; i++) {
|
||
var klc = klcList[i]
|
||
var pre = klcList[i - 1]
|
||
var next = klcList[i + 1]
|
||
|
||
// Step 1: 分型检测 — TF_DF.check_fx()
|
||
var fx = null
|
||
if (klc.high > pre.high && klc.high > next.high &&
|
||
klc.low > pre.low && klc.low > next.low) {
|
||
fx = 'TOP'
|
||
} else if (klc.low < pre.low && klc.low < next.low &&
|
||
klc.high < pre.high && klc.high < next.high) {
|
||
fx = 'BOTTOM'
|
||
}
|
||
|
||
// Step 2: 缺口测试 — TF_DF.check_top_fx / check_bottom_fx
|
||
if (fx === 'TOP' && lastBottom) {
|
||
if (!checkTopFx(lastBottom, klc, pre, next)) {
|
||
fx = null
|
||
}
|
||
}
|
||
if (fx === 'BOTTOM' && lastTop) {
|
||
if (!checkBottomFx(lastTop, klc, pre, next)) {
|
||
fx = null
|
||
}
|
||
}
|
||
|
||
// Step 3: 非分型 — 添加到当前笔 (Python line 967-971)
|
||
if (!fx) {
|
||
_addKlcToLastBi(biList, klc)
|
||
continue
|
||
}
|
||
|
||
// 标记 KLC
|
||
klc.fx = fx
|
||
|
||
// === 处理顶分型 ===
|
||
if (fx === 'TOP') {
|
||
if (lastTop) {
|
||
if (lastBottom) {
|
||
if (lastBottom.idx < lastTop.idx) {
|
||
// 正常交替:底在前,顶在后 (Python line 1021-1037)
|
||
if (lastTop.high > klc.high) {
|
||
// 新顶更低 → 二类卖点,保持 lastTop
|
||
_addKlcToLastBi(biList, klc)
|
||
} else {
|
||
// 新顶更高 → 替换 lastTop (一类卖点)
|
||
lastTop = klc
|
||
_addKlcToLastBi(biList, klc)
|
||
}
|
||
} else {
|
||
// 不满足结合律:顶在前,底在后 (Python line 1038-1093)
|
||
if (lastBottom.idx + MIN_BI_KLC > klc.idx) {
|
||
// KLC 间距太小 (Python line 1042-1075)
|
||
if (lastTop.high > klc.high) {
|
||
_addKlcToLastBi(biList, klc)
|
||
} else {
|
||
// 无效顶分型 (Python line 1075)
|
||
_addKlcToLastBi(biList, klc)
|
||
}
|
||
} else {
|
||
// 满足结合律:确认上一个向下笔,开启新的向上笔 (Python line 1078-1093)
|
||
var lastBi = biList[biList.length - 1]
|
||
if (!lastBi._isSure) {
|
||
lastBi._endKlc = lastBottom
|
||
lastBi._isSure = true
|
||
}
|
||
var newBi = {
|
||
dir: 'UP', _startKlc: lastBottom, _endKlc: null,
|
||
_isSure: false, _klcList: [klc], idx: biList.length,
|
||
}
|
||
biList.push(newBi)
|
||
lastTop = klc
|
||
}
|
||
}
|
||
} else {
|
||
// lastBottom 为空:初始化阶段 (Python line 1095-1109)
|
||
if (lastTop.high < klc.high) {
|
||
// 新顶更高 → 更新笔起点
|
||
var bi = biList[biList.length - 1]
|
||
bi._startKlc = klc
|
||
bi.dir = 'DOWN'
|
||
lastTop = klc
|
||
bi._klcList.push(klc)
|
||
} else {
|
||
_addKlcToLastBi(biList, klc)
|
||
}
|
||
}
|
||
} else {
|
||
// lastTop 为空 (Python line 1110-1133)
|
||
if (lastBottom) {
|
||
if (lastBottom.idx + MIN_BI_KLC > klc.idx) {
|
||
// 间距太小 (Python line 1114-1118)
|
||
_addKlcToLastBi(biList, klc)
|
||
} else {
|
||
// 第一个临时顶 (Python line 1120-1124)
|
||
lastTop = klc
|
||
_addKlcToLastBi(biList, klc)
|
||
}
|
||
} else {
|
||
// 第一个分型 → 创建第一个向下笔 (Python line 1126-1133)
|
||
lastTop = klc
|
||
var bi = {
|
||
dir: 'DOWN', _startKlc: klc, _endKlc: null,
|
||
_isSure: false, _klcList: [klc], idx: 0,
|
||
}
|
||
biList.push(bi)
|
||
}
|
||
}
|
||
}
|
||
|
||
// === 处理底分型 (镜像) ===
|
||
else {
|
||
if (lastBottom) {
|
||
if (lastTop) {
|
||
if (lastTop.idx < lastBottom.idx) {
|
||
// 正常交替:顶在前,底在后 (Python line 1139-1155)
|
||
if (lastBottom.low < klc.low) {
|
||
// 新底更高 → 二类买点,保持 lastBottom
|
||
_addKlcToLastBi(biList, klc)
|
||
} else {
|
||
// 新底更低 → 替换 lastBottom (一类买点)
|
||
lastBottom = klc
|
||
_addKlcToLastBi(biList, klc)
|
||
}
|
||
} else {
|
||
// 不满足结合律:底在前,顶在后 (Python line 1157-1208)
|
||
if (lastTop.idx + MIN_BI_KLC > klc.idx) {
|
||
// KLC 间距太小 (Python line 1160-1190)
|
||
if (lastBottom.low < klc.low) {
|
||
_addKlcToLastBi(biList, klc)
|
||
} else {
|
||
// 无效底分型 (Python line 1190)
|
||
_addKlcToLastBi(biList, klc)
|
||
}
|
||
} else {
|
||
// 满足结合律:确认上一个向上笔,开启新的向下笔 (Python line 1192-1208)
|
||
var lastBi = biList[biList.length - 1]
|
||
if (!lastBi._isSure) {
|
||
lastBi._endKlc = lastTop
|
||
lastBi._isSure = true
|
||
}
|
||
var newBi = {
|
||
dir: 'DOWN', _startKlc: lastTop, _endKlc: null,
|
||
_isSure: false, _klcList: [klc], idx: biList.length,
|
||
}
|
||
biList.push(newBi)
|
||
lastBottom = klc
|
||
}
|
||
}
|
||
} else {
|
||
// lastTop 为空:初始化阶段 (Python line 1210-1225)
|
||
if (lastBottom.low > klc.low) {
|
||
// 新底更低 → 更新笔起点
|
||
var bi = biList[biList.length - 1]
|
||
bi._startKlc = klc
|
||
bi.dir = 'UP'
|
||
lastBottom = klc
|
||
bi._klcList.push(klc)
|
||
} else {
|
||
_addKlcToLastBi(biList, klc)
|
||
}
|
||
}
|
||
} else {
|
||
// lastBottom 为空 (Python line 1227-1251)
|
||
if (lastTop) {
|
||
if (lastTop.idx + MIN_BI_KLC > klc.idx) {
|
||
// 间距太小 (Python line 1230-1234)
|
||
_addKlcToLastBi(biList, klc)
|
||
} else {
|
||
// 第一个临时底 (Python line 1236-1240)
|
||
lastBottom = klc
|
||
_addKlcToLastBi(biList, klc)
|
||
}
|
||
} else {
|
||
// 第一个分型 → 创建第一个向上笔 (Python line 1242-1251)
|
||
lastBottom = klc
|
||
var bi = {
|
||
dir: 'UP', _startKlc: klc, _endKlc: null,
|
||
_isSure: false, _klcList: [klc], idx: 0,
|
||
}
|
||
biList.push(bi)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return _biListToStrokes(biList)
|
||
}
|
||
|
||
|
||
// ======================== 线段 (SEG) 检测 ========================
|
||
|
||
/**
|
||
* 线段检测 — 特征序列 (SBI) 算法
|
||
*
|
||
* 严格参照 Python get_seg_list() (TF_DF.py:660-938) 和 ChanSBI.py
|
||
*
|
||
* 核心机制:
|
||
* 1. 特征序列: UP 段取 DOWN 笔序列, DOWN 段取 UP 笔序列
|
||
* 2. SBI 包含处理: 特征序列元素间存在包含关系时合并
|
||
* 3. SBI 分型: 3+ 特征序列元素形成顶/底分型 → 段结束
|
||
* 4. 缺口: 分型无重叠时触发 look_for 标志,确认下一段
|
||
*/
|
||
|
||
// ---- SBI (Special BI / 特征序列元素) ----
|
||
function _makeSBI(bi, idx) {
|
||
return {
|
||
startBi: bi,
|
||
endBi: null,
|
||
idx: idx,
|
||
dir: bi.dir,
|
||
high: Math.max(bi.p0, bi.p1),
|
||
low: Math.min(bi.p0, bi.p1),
|
||
pre: null,
|
||
next: null,
|
||
fx: null, // 'TOP' | 'BOTTOM' | null
|
||
hasFxGap: false,
|
||
biList: [bi],
|
||
}
|
||
}
|
||
|
||
/**
|
||
* SBI 包含检查 — 匹配 ChanSBI.check_bi_included()
|
||
*
|
||
* 条件: sbi.high > bi.high && sbi.low < bi.low (sbi 严格包容 bi)
|
||
* 且 sbi 也包容 sbi.pre (双重确认)
|
||
*
|
||
* 包含时更新:
|
||
* DOWN SBI (UP段特征序列): sbi.low = bi.low (gn>gn-1, 取更高的 low)
|
||
* UP SBI (DOWN段特征序列): sbi.high = bi.high (gn<gn-1, 取更低的 high)
|
||
*/
|
||
function _checkSBIInclusion(sbi, bi) {
|
||
var biHigh = Math.max(bi.p0, bi.p1)
|
||
var biLow = Math.min(bi.p0, bi.p1)
|
||
var included = false
|
||
|
||
if (sbi.high > biHigh && sbi.low < biLow) {
|
||
included = true
|
||
}
|
||
|
||
if (included && sbi.pre) {
|
||
// Python: pre check only reaffirms, never rejects
|
||
// if self.high > self.pre.high and self.low < self.pre.low: included = True
|
||
if (sbi.high > sbi.pre.high && sbi.low < sbi.pre.low) {
|
||
included = true
|
||
}
|
||
}
|
||
|
||
if (included) {
|
||
sbi.biList.push(bi)
|
||
if (sbi.dir === -1) {
|
||
// DOWN SBI: gn > gn-1 → low 取 bi.low (更高的 low)
|
||
sbi.low = biLow
|
||
} else {
|
||
// UP SBI: gn < gn-1 → high 取 bi.high (更低的 high)
|
||
sbi.high = biHigh
|
||
}
|
||
}
|
||
return included
|
||
}
|
||
|
||
/**
|
||
* SBI 分型检测 — 匹配 ChanSBI.check_fx()
|
||
*
|
||
* 需要 pre, self, next 三个 SBI
|
||
* TOP: self.high > pre.high && self.high > next.high
|
||
* 缺口: self.low > self.pre.high
|
||
* BOTTOM: self.low < pre.low && self.low < next.low
|
||
* 缺口: self.high < self.pre.low
|
||
*/
|
||
function _checkSBIFx(sbi) {
|
||
if (!sbi.pre || !sbi.next) return null
|
||
|
||
if (sbi.high > sbi.pre.high && sbi.high > sbi.next.high) {
|
||
sbi.fx = 'TOP'
|
||
if (sbi.low > sbi.pre.high) {
|
||
sbi.hasFxGap = true
|
||
}
|
||
return 'TOP'
|
||
}
|
||
if (sbi.low < sbi.pre.low && sbi.low < sbi.next.low) {
|
||
sbi.fx = 'BOTTOM'
|
||
if (sbi.high < sbi.pre.low) {
|
||
sbi.hasFxGap = true
|
||
}
|
||
return 'BOTTOM'
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 检查笔是否满足第一段起始条件 — 匹配 ChanBI.check_overlap()
|
||
* bi 必须有 next 和 next.next 且都已确认
|
||
*/
|
||
function _checkOverlap(bi) {
|
||
if (!bi || !bi.next || !bi.next.next) return false
|
||
// 简化: 笔已确认且有足够的后续笔即可形成第一段
|
||
// Python 原始条件要求 next.next.is_sure
|
||
if (bi.dir === 1) {
|
||
// UP bi: high > next.low && high < next.next.high
|
||
var biHigh = Math.max(bi.p0, bi.p1)
|
||
var nextLow = Math.min(bi.next.p0, bi.next.p1)
|
||
var nextNextHigh = Math.max(bi.next.next.p0, bi.next.next.p1)
|
||
return biHigh > nextLow && biHigh < nextNextHigh
|
||
} else {
|
||
// DOWN bi: high > next.high && low > next.next.low
|
||
var biHigh = Math.max(bi.p0, bi.p1)
|
||
var biLow = Math.min(bi.p0, bi.p1)
|
||
var nextHigh = Math.max(bi.next.p0, bi.next.p1)
|
||
var nextNextLow = Math.min(bi.next.next.p0, bi.next.next.p1)
|
||
return biHigh > nextHigh && biLow > nextNextLow
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从 seg 对象生成输出格式
|
||
* p0/p1 使用起始笔和结束笔的价格 (匹配 Python: seg.low=start_bi.low, seg.high=end_bi.high)
|
||
*/
|
||
function _segToOutput(seg, idx) {
|
||
var startBi = seg.startBi
|
||
var endBi = seg.endBi || seg.biList[seg.biList.length - 1]
|
||
if (!startBi || !endBi) {
|
||
startBi = seg.biList[0]
|
||
endBi = seg.biList[seg.biList.length - 1]
|
||
}
|
||
|
||
var p0, p1
|
||
if (seg.dir === 1) {
|
||
// UP 段: p0 = startBi.low (起始底), p1 = endBi.high (结束顶)
|
||
p0 = Math.min(startBi.p0, startBi.p1)
|
||
p1 = Math.max(endBi.p0, endBi.p1)
|
||
} else {
|
||
// DOWN 段: p0 = startBi.high (起始顶), p1 = endBi.low (结束底)
|
||
p0 = Math.max(startBi.p0, startBi.p1)
|
||
p1 = Math.min(endBi.p0, endBi.p1)
|
||
}
|
||
|
||
return {
|
||
idx: idx,
|
||
dir: seg.dir,
|
||
sure: seg.isSure || false,
|
||
t0: startBi.t0,
|
||
t1: endBi.t1,
|
||
p0: p0,
|
||
p1: p1,
|
||
}
|
||
}
|
||
|
||
function findSegments(strokes) {
|
||
if (strokes.length < 1) return []
|
||
|
||
// 建立双向链表 (Python bi.next / bi.pre)
|
||
for (var i = 0; i < strokes.length; i++) {
|
||
strokes[i].next = i + 1 < strokes.length ? strokes[i + 1] : null
|
||
strokes[i].pre = i > 0 ? strokes[i - 1] : null
|
||
}
|
||
|
||
// 辅助: 填充 seg.biList 从 startBiIdx 到 currentBiIdx (匹配 Python ini_seg)
|
||
function _fillSegBiList(seg, startIdx, endIdx) {
|
||
seg.biList = []
|
||
for (var j = startIdx; j <= endIdx && j < strokes.length; j++) {
|
||
seg.biList.push(strokes[j])
|
||
}
|
||
}
|
||
|
||
// 安全获取 stroke
|
||
function _safeStroke(idx) {
|
||
if (idx < 0) return strokes[0]
|
||
if (idx >= strokes.length) return strokes[strokes.length - 1]
|
||
return strokes[idx]
|
||
}
|
||
|
||
var segList = []
|
||
var upSbiList = []
|
||
var downSbiList = []
|
||
var lastUpBi = null
|
||
var lastDownBi = null
|
||
var lastUpSbi = null
|
||
var lastDownSbi = null
|
||
var lastSeg = null
|
||
var lookForBottom = false
|
||
var lookForTop = false
|
||
|
||
for (var i = 0; i < strokes.length; i++) {
|
||
var bi = strokes[i]
|
||
|
||
if (segList.length > 0) {
|
||
// ===== 已有段: 根据段方向处理 =====
|
||
if (lastSeg.dir === 1) {
|
||
// ---- 当前是 UP 段,特征序列是 DOWN 笔 ----
|
||
if (bi.dir === -1) {
|
||
// 反向笔 → 添加到特征序列
|
||
if (downSbiList.length > 1) {
|
||
var included = _checkSBIInclusion(lastDownSbi, bi)
|
||
if (!included) {
|
||
var downSbi = _makeSBI(bi, downSbiList.length)
|
||
lastDownSbi.next = downSbi
|
||
lastDownSbi.endBi = lastDownBi
|
||
downSbi.pre = lastDownSbi
|
||
downSbiList.push(downSbi)
|
||
|
||
var fx = _checkSBIFx(lastDownSbi)
|
||
if (fx === 'TOP') {
|
||
// 特征序列顶分型 → UP 段可能结束
|
||
if (lookForTop) {
|
||
// 确认上一段(第二段前)
|
||
if (segList.length >= 2) {
|
||
segList[segList.length - 2].isSure = true
|
||
}
|
||
lookForTop = false
|
||
}
|
||
|
||
if (lastDownSbi.hasFxGap) {
|
||
// 有缺口 → 段结束 + 新反向段开始
|
||
// Python pre_set_end_bi: 不设 is_sure, 等下一段确认
|
||
lookForBottom = true
|
||
lastSeg.endBi = _safeStroke(lastDownSbi.startBi.idx - 1)
|
||
// lastSeg.isSure stays false (gap segments are unsure until confirmed)
|
||
var newSeg = {
|
||
startBi: lastDownSbi.startBi,
|
||
dir: -1,
|
||
biList: [],
|
||
isSure: false,
|
||
endBi: bi,
|
||
}
|
||
_fillSegBiList(newSeg, lastDownSbi.startBi.idx, bi.idx)
|
||
segList.push(newSeg)
|
||
lastSeg.next = newSeg
|
||
newSeg.pre = lastSeg
|
||
lastSeg = newSeg
|
||
|
||
// 重置 up_sbi_list
|
||
upSbiList = []
|
||
lastUpSbi = _makeSBI(lastUpBi, 0)
|
||
upSbiList.push(lastUpSbi)
|
||
} else {
|
||
// 无缺口
|
||
if (lookForBottom) {
|
||
// Python: 不创建新段, 调整当前段起点, 关闭前一段
|
||
lookForBottom = false
|
||
lastSeg.startBi = lastDownSbi.startBi
|
||
if (segList.length >= 2) {
|
||
segList[segList.length - 2].endBi = _safeStroke(lastDownSbi.startBi.idx - 1)
|
||
// seg_list[-2].set_end_bi 会设 is_sure
|
||
// 但需要检查 endBi 是否为 sure... 这里简化, set_end_bi 总是设 sure
|
||
}
|
||
upSbiList = []
|
||
lastUpSbi = _makeSBI(lastUpBi, 0)
|
||
upSbiList.push(lastUpSbi)
|
||
lastSeg.biList.push(bi)
|
||
} else {
|
||
// 正常段结束 — Python set_end_bi: 设 is_sure = True
|
||
lastSeg.endBi = _safeStroke(lastDownSbi.startBi.idx - 1)
|
||
lastSeg.isSure = true
|
||
var newSeg = {
|
||
startBi: lastDownSbi.startBi,
|
||
dir: -1,
|
||
biList: [],
|
||
isSure: false,
|
||
endBi: bi,
|
||
}
|
||
_fillSegBiList(newSeg, lastDownSbi.startBi.idx, bi.idx)
|
||
segList.push(newSeg)
|
||
lastSeg.next = newSeg
|
||
newSeg.pre = lastSeg
|
||
lastSeg = newSeg
|
||
|
||
upSbiList = []
|
||
lastUpSbi = _makeSBI(lastUpBi, 0)
|
||
upSbiList.push(lastUpSbi)
|
||
}
|
||
}
|
||
}
|
||
lastDownSbi = downSbi
|
||
}
|
||
lastSeg.biList.push(bi)
|
||
} else if (downSbiList.length === 1) {
|
||
var included = _checkSBIInclusion(lastDownSbi, bi)
|
||
if (!included) {
|
||
var downSbi = _makeSBI(bi, downSbiList.length)
|
||
lastDownSbi.next = downSbi
|
||
lastDownSbi.endBi = lastDownBi
|
||
downSbi.pre = lastDownSbi
|
||
downSbiList.push(downSbi)
|
||
lastDownSbi = downSbi
|
||
}
|
||
lastSeg.biList.push(bi)
|
||
} else {
|
||
// downSbiList.length === 0
|
||
lastDownSbi = _makeSBI(bi, 0)
|
||
downSbiList.push(lastDownSbi)
|
||
lastSeg.biList.push(bi)
|
||
}
|
||
} else {
|
||
// bi.dir === UP (同向笔) → 添加到 up_sbi_list
|
||
if (lastUpSbi) {
|
||
var included = _checkSBIInclusion(lastUpSbi, bi)
|
||
if (!included) {
|
||
var upSbi = _makeSBI(bi, upSbiList.length)
|
||
lastUpSbi.next = upSbi
|
||
lastUpSbi.endBi = lastUpBi
|
||
upSbi.pre = lastUpSbi
|
||
upSbiList.push(upSbi)
|
||
lastUpSbi = upSbi
|
||
}
|
||
lastSeg.biList.push(bi)
|
||
}
|
||
}
|
||
} else {
|
||
// ---- 当前是 DOWN 段,特征序列是 UP 笔 ----
|
||
if (bi.dir === 1) {
|
||
// 反向笔 → 添加到特征序列
|
||
if (upSbiList.length > 1) {
|
||
var included = _checkSBIInclusion(lastUpSbi, bi)
|
||
if (!included) {
|
||
var upSbi = _makeSBI(bi, upSbiList.length)
|
||
lastUpSbi.next = upSbi
|
||
lastUpSbi.endBi = lastUpBi
|
||
upSbi.pre = lastUpSbi
|
||
upSbiList.push(upSbi)
|
||
|
||
var fx = _checkSBIFx(lastUpSbi)
|
||
if (fx === 'BOTTOM') {
|
||
// 特征序列底分型 → DOWN 段可能结束
|
||
if (lookForBottom) {
|
||
if (segList.length >= 2) {
|
||
segList[segList.length - 2].isSure = true
|
||
}
|
||
lookForBottom = false
|
||
}
|
||
|
||
if (lastUpSbi.hasFxGap) {
|
||
// 有缺口 → 段结束 + 新反向段开始
|
||
// Python pre_set_end_bi: 不设 is_sure, 等下一段确认
|
||
lookForTop = true
|
||
lastSeg.endBi = _safeStroke(lastUpSbi.startBi.idx - 1)
|
||
// lastSeg.isSure stays false (gap segments are unsure)
|
||
var newSeg = {
|
||
startBi: lastUpSbi.startBi,
|
||
dir: 1,
|
||
biList: [],
|
||
isSure: false,
|
||
endBi: bi,
|
||
}
|
||
_fillSegBiList(newSeg, lastUpSbi.startBi.idx, bi.idx)
|
||
segList.push(newSeg)
|
||
lastSeg.next = newSeg
|
||
newSeg.pre = lastSeg
|
||
lastSeg = newSeg
|
||
|
||
// 重置 down_sbi_list
|
||
downSbiList = []
|
||
lastDownSbi = _makeSBI(lastDownBi, 0)
|
||
downSbiList.push(lastDownSbi)
|
||
} else {
|
||
// 无缺口
|
||
if (lookForTop) {
|
||
// Python: 不创建新段, 调整当前段起点
|
||
lookForTop = false
|
||
lastSeg.startBi = lastUpSbi.startBi
|
||
if (segList.length >= 2) {
|
||
segList[segList.length - 2].endBi = _safeStroke(lastUpSbi.startBi.idx - 1)
|
||
}
|
||
downSbiList = []
|
||
lastDownSbi = _makeSBI(lastDownBi, 0)
|
||
downSbiList.push(lastDownSbi)
|
||
lastSeg.biList.push(bi)
|
||
} else {
|
||
// 正常段结束 — Python set_end_bi: 设 is_sure = True
|
||
lastSeg.endBi = _safeStroke(lastUpSbi.startBi.idx - 1)
|
||
lastSeg.isSure = true
|
||
var newSeg = {
|
||
startBi: lastUpSbi.startBi,
|
||
dir: 1,
|
||
biList: [],
|
||
isSure: false,
|
||
endBi: bi,
|
||
}
|
||
_fillSegBiList(newSeg, lastUpSbi.startBi.idx, bi.idx)
|
||
segList.push(newSeg)
|
||
lastSeg.next = newSeg
|
||
newSeg.pre = lastSeg
|
||
lastSeg = newSeg
|
||
|
||
downSbiList = []
|
||
lastDownSbi = _makeSBI(lastDownBi, 0)
|
||
downSbiList.push(lastDownSbi)
|
||
}
|
||
}
|
||
}
|
||
lastUpSbi = upSbi
|
||
}
|
||
lastSeg.biList.push(bi)
|
||
} else if (upSbiList.length === 1) {
|
||
var included = _checkSBIInclusion(lastUpSbi, bi)
|
||
if (!included) {
|
||
var upSbi = _makeSBI(bi, upSbiList.length)
|
||
lastUpSbi.next = upSbi
|
||
lastUpSbi.endBi = lastUpBi
|
||
upSbi.pre = lastUpSbi
|
||
upSbiList.push(upSbi)
|
||
lastUpSbi = upSbi
|
||
}
|
||
lastSeg.biList.push(bi)
|
||
} else {
|
||
// upSbiList.length === 0
|
||
lastUpSbi = _makeSBI(bi, 0)
|
||
upSbiList.push(lastUpSbi)
|
||
lastSeg.biList.push(bi)
|
||
}
|
||
} else {
|
||
// bi.dir === DOWN (同向笔) → 添加到 down_sbi_list
|
||
if (lastDownSbi) {
|
||
var included = _checkSBIInclusion(lastDownSbi, bi)
|
||
if (!included) {
|
||
var downSbi = _makeSBI(bi, downSbiList.length)
|
||
lastDownSbi.next = downSbi
|
||
lastDownSbi.endBi = lastDownBi
|
||
downSbi.pre = lastDownSbi
|
||
downSbiList.push(downSbi)
|
||
lastDownSbi = downSbi
|
||
}
|
||
lastSeg.biList.push(bi)
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// ===== 第一段: 需要 check_overlap =====
|
||
if (_checkOverlap(bi)) {
|
||
if (bi.dir === 1) {
|
||
var seg = {
|
||
startBi: bi,
|
||
dir: 1,
|
||
biList: [],
|
||
isSure: false,
|
||
endBi: bi,
|
||
}
|
||
_fillSegBiList(seg, bi.idx, bi.idx)
|
||
lastUpSbi = _makeSBI(bi, 0)
|
||
upSbiList.push(lastUpSbi)
|
||
segList.push(seg)
|
||
lastSeg = seg
|
||
} else {
|
||
var seg = {
|
||
startBi: bi,
|
||
dir: -1,
|
||
biList: [],
|
||
isSure: false,
|
||
endBi: bi,
|
||
}
|
||
_fillSegBiList(seg, bi.idx, bi.idx)
|
||
lastDownSbi = _makeSBI(bi, 0)
|
||
downSbiList.push(lastDownSbi)
|
||
segList.push(seg)
|
||
lastSeg = seg
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新 last bi 追踪 (Python line 880-885)
|
||
if (bi.dir === 1) {
|
||
lastUpBi = bi
|
||
} else {
|
||
lastDownBi = bi
|
||
}
|
||
}
|
||
|
||
// Python: 不在此处标记 sure
|
||
// - 正常结束: set_end_bi → isSure=true (已在上面设置)
|
||
// - 跳空结束: pre_set_end_bi → isSure=false, 等 look_for 确认
|
||
// - 最后一段: 永远 unsure
|
||
|
||
// 转换为输出格式
|
||
var result = []
|
||
for (var s = 0; s < segList.length; s++) {
|
||
result.push(_segToOutput(segList[s], s))
|
||
}
|
||
|
||
console.log('[SBI段] 总计:', result.length, '段 from', strokes.length, '笔')
|
||
|
||
return result
|
||
}
|
||
|
||
|
||
// ======================== 中枢 (ZS) 检测 ========================
|
||
|
||
/**
|
||
* 中枢 (Center/Pivot): 至少 3 段走势重叠的价格区间
|
||
*
|
||
* ZG = min(各段的高点), ZD = max(各段的低点)
|
||
* 条件: ZG > ZD
|
||
* 后续段若仍在区间内,扩展中枢
|
||
*/
|
||
/**
|
||
* 笔中枢检测 — 从 stroke 列表按 3 笔重叠规则计算笔级别中枢
|
||
*
|
||
* 参照 Python cal_bi_zs_list() (TF_DF.py:1295-1424):
|
||
* - 从第4根笔开始(索引3),每3根确认笔为一组
|
||
* - 上涨中枢:bi1.dir=DOWN, bi2.dir=UP, bi3.dir=DOWN → zg=min(highs), zd=max(lows)
|
||
* - 下跌中枢:bi1.dir=UP, bi2.dir=DOWN, bi3.dir=UP
|
||
* - 后中枢不重叠前中枢:UP_ZS → zg > last.zg; DOWN_ZS → zd < last.zd
|
||
* - 按两笔一组扩展到5根、7根...
|
||
* - 追踪 GG/DD 与 ZG/ZD
|
||
*/
|
||
function findBiCenters(biList) {
|
||
var zsList = []
|
||
if (biList.length < 3) return zsList
|
||
|
||
var lastZs = null
|
||
var startIdx = 3
|
||
|
||
while (startIdx + 2 < biList.length) {
|
||
var bi1 = biList[startIdx]
|
||
var bi2 = biList[startIdx + 1]
|
||
var bi3 = biList[startIdx + 2]
|
||
|
||
// 三笔必须全部确认
|
||
if (!(bi1.sure && bi2.sure && bi3.sure)) {
|
||
startIdx++
|
||
continue
|
||
}
|
||
|
||
var h1 = Math.max(bi1.p0, bi1.p1), l1 = Math.min(bi1.p0, bi1.p1)
|
||
var h2 = Math.max(bi2.p0, bi2.p1), l2 = Math.min(bi2.p0, bi2.p1)
|
||
var h3 = Math.max(bi3.p0, bi3.p1), l3 = Math.min(bi3.p0, bi3.p1)
|
||
|
||
var zg = Math.min(h1, h2, h3)
|
||
var zd = Math.max(l1, l2, l3)
|
||
|
||
if (zg <= zd) { startIdx++; continue }
|
||
|
||
// 方向判定
|
||
var valid = false
|
||
var zsDir = 0 // 1=UP, -1=DOWN
|
||
|
||
if (!lastZs) {
|
||
// 首个中枢
|
||
if (bi1.dir === -1 && bi2.dir === 1 && bi3.dir === -1) {
|
||
zsDir = 1 // UP_ZS
|
||
valid = true
|
||
} else if (bi1.dir === 1 && bi2.dir === -1 && bi3.dir === 1) {
|
||
zsDir = -1 // DOWN_ZS
|
||
valid = true
|
||
}
|
||
} else {
|
||
if (zg > lastZs.zg) {
|
||
zsDir = 1 // UP_ZS
|
||
valid = (bi1.dir === -1 && bi2.dir === 1 && bi3.dir === -1)
|
||
} else if (zd < lastZs.zd) {
|
||
zsDir = -1 // DOWN_ZS
|
||
valid = (bi1.dir === 1 && bi2.dir === -1 && bi3.dir === 1)
|
||
}
|
||
}
|
||
|
||
if (!valid) { startIdx++; continue }
|
||
|
||
var gg = Math.max(h1, h2, h3)
|
||
var dd = Math.min(l1, l2, l3)
|
||
var biList_for_zs = [bi1, bi2, bi3]
|
||
var endBiIdx = startIdx + 2
|
||
|
||
// 按两笔一组扩展
|
||
var addedAfterLeave = []
|
||
var leaveIdx = startIdx + 4
|
||
while (leaveIdx < biList.length) {
|
||
var b = biList[leaveIdx]
|
||
if (!b.sure) break
|
||
if (Math.max(b.p0, b.p1) >= zd && Math.min(b.p0, b.p1) <= zg) {
|
||
addedAfterLeave.push(biList[leaveIdx - 1])
|
||
addedAfterLeave.push(b)
|
||
} else {
|
||
break
|
||
}
|
||
leaveIdx += 2
|
||
}
|
||
|
||
if (addedAfterLeave.length > 0) {
|
||
biList_for_zs = biList_for_zs.concat(addedAfterLeave)
|
||
var highs = biList_for_zs.map(function(bi) { return Math.max(bi.p0, bi.p1) })
|
||
var lows = biList_for_zs.map(function(bi) { return Math.min(bi.p0, bi.p1) })
|
||
gg = Math.max.apply(null, highs)
|
||
dd = Math.min.apply(null, lows)
|
||
endBiIdx = startIdx + 2 + addedAfterLeave.length
|
||
}
|
||
|
||
var lastBiInCenter = biList_for_zs[biList_for_zs.length - 1]
|
||
// 是否已离开中枢:之后出现完全在 ZG 之上或 ZD 之下的确认笔 → 中枢完成
|
||
var zsSure = false
|
||
var lastInListIdx = -1
|
||
for (var li = 0; li < biList.length; li++) {
|
||
if (biList[li] === lastBiInCenter || (biList[li].t0 === lastBiInCenter.t0 && biList[li].t1 === lastBiInCenter.t1)) {
|
||
lastInListIdx = li
|
||
break
|
||
}
|
||
}
|
||
if (lastInListIdx < 0) lastInListIdx = endBiIdx
|
||
for (var j = lastInListIdx + 1; j < biList.length; j++) {
|
||
var leaveBi = biList[j]
|
||
if (!leaveBi.sure) break
|
||
var lbh = Math.max(leaveBi.p0, leaveBi.p1)
|
||
var lbl = Math.min(leaveBi.p0, leaveBi.p1)
|
||
if (lbl > zg || lbh < zd) {
|
||
zsSure = true
|
||
break
|
||
}
|
||
}
|
||
|
||
var zs = {
|
||
t0: bi1.t0,
|
||
t1: lastBiInCenter.t1,
|
||
high: zg, low: zd,
|
||
zg: zg, zd: zd,
|
||
gg: gg, dd: dd,
|
||
is_sure: zsSure,
|
||
bi_count: biList_for_zs.length,
|
||
bi_list: biList_for_zs,
|
||
start_bi_idx: startIdx,
|
||
dir: zsDir,
|
||
pre: lastZs,
|
||
next: null,
|
||
}
|
||
|
||
if (lastZs) {
|
||
lastZs.next = zs
|
||
}
|
||
|
||
zsList.push(zs)
|
||
lastZs = zs
|
||
startIdx = startIdx + 4 + (addedAfterLeave.length > 0 ? addedAfterLeave.length : 0)
|
||
}
|
||
|
||
return zsList
|
||
}
|
||
|
||
/**
|
||
* 段中枢检测 — 从 segment 列表按 3 段重叠规则计算段级别中枢
|
||
*
|
||
* 参照 Python calculate_seg_zs() (TF_DF.py:1741+):
|
||
* - 从第4段开始(索引3),每3段为一组
|
||
* - 上涨中枢:seg1.dir=DOWN, seg2.dir=UP, seg3.dir=DOWN
|
||
* - 下跌中枢:seg1.dir=UP, seg2.dir=DOWN, seg3.dir=UP
|
||
* - ZG = min(highs), ZD = max(lows),要求 ZG > ZD
|
||
* - 后中枢与前中枢不重叠
|
||
* - 扩张/扩展检测
|
||
*/
|
||
function findSegCenters(segs) {
|
||
var zsList = []
|
||
if (segs.length < 3) return zsList
|
||
|
||
var lastZs = null
|
||
var startIdx = 3
|
||
|
||
while (startIdx + 2 < segs.length) {
|
||
var s1 = segs[startIdx]
|
||
var s2 = segs[startIdx + 1]
|
||
var s3 = segs[startIdx + 2]
|
||
|
||
if (!(s1.sure && s2.sure && s3.sure)) {
|
||
startIdx++
|
||
continue
|
||
}
|
||
|
||
var h1 = Math.max(s1.p0, s1.p1), l1 = Math.min(s1.p0, s1.p1)
|
||
var h2 = Math.max(s2.p0, s2.p1), l2 = Math.min(s2.p0, s2.p1)
|
||
var h3 = Math.max(s3.p0, s3.p1), l3 = Math.min(s3.p0, s3.p1)
|
||
|
||
var zg = Math.min(h1, h2, h3)
|
||
var zd = Math.max(l1, l2, l3)
|
||
|
||
if (zg <= zd) { startIdx++; continue }
|
||
|
||
// 方向判定
|
||
var valid = false
|
||
var zsDir = 0
|
||
|
||
if (!lastZs) {
|
||
if (s1.dir === -1 && s2.dir === 1 && s3.dir === -1) {
|
||
zsDir = 1; valid = true
|
||
} else if (s1.dir === 1 && s2.dir === -1 && s3.dir === 1) {
|
||
zsDir = -1; valid = true
|
||
}
|
||
} else {
|
||
if (zg > lastZs.zg) {
|
||
zsDir = 1
|
||
valid = (s1.dir === -1 && s2.dir === 1 && s3.dir === -1)
|
||
} else if (zd < lastZs.zd) {
|
||
zsDir = -1
|
||
valid = (s1.dir === 1 && s2.dir === -1 && s3.dir === 1)
|
||
}
|
||
}
|
||
|
||
if (!valid) { startIdx++; continue }
|
||
|
||
var gg = Math.max(h1, h2, h3)
|
||
var dd = Math.min(l1, l2, l3)
|
||
var endSegIdx = startIdx + 2
|
||
|
||
// 按两段一组扩展
|
||
var addedSegs = []
|
||
var extIdx = startIdx + 4
|
||
while (extIdx < segs.length) {
|
||
var s = segs[extIdx]
|
||
if (!s.sure) break
|
||
if (Math.max(s.p0, s.p1) >= zd && Math.min(s.p0, s.p1) <= zg) {
|
||
addedSegs.push(segs[extIdx - 1])
|
||
addedSegs.push(s)
|
||
} else {
|
||
break
|
||
}
|
||
extIdx += 2
|
||
}
|
||
|
||
var segList_for_zs = [s1, s2, s3]
|
||
if (addedSegs.length > 0) {
|
||
segList_for_zs = segList_for_zs.concat(addedSegs)
|
||
gg = Math.max.apply(null, segList_for_zs.map(function(s) { return Math.max(s.p0, s.p1) }))
|
||
dd = Math.min.apply(null, segList_for_zs.map(function(s) { return Math.min(s.p0, s.p1) }))
|
||
endSegIdx = startIdx + 2 + addedSegs.length
|
||
}
|
||
|
||
var lastSegInCenter = segList_for_zs[segList_for_zs.length - 1]
|
||
var zsSure = false
|
||
var lastSegListIdx = -1
|
||
for (var lsi = 0; lsi < segs.length; lsi++) {
|
||
if (segs[lsi] === lastSegInCenter || (segs[lsi].t0 === lastSegInCenter.t0 && segs[lsi].t1 === lastSegInCenter.t1)) {
|
||
lastSegListIdx = lsi
|
||
break
|
||
}
|
||
}
|
||
if (lastSegListIdx < 0) lastSegListIdx = endSegIdx
|
||
for (var sj = lastSegListIdx + 1; sj < segs.length; sj++) {
|
||
var leaveSeg = segs[sj]
|
||
if (!leaveSeg.sure) break
|
||
var lsh = Math.max(leaveSeg.p0, leaveSeg.p1)
|
||
var lsl = Math.min(leaveSeg.p0, leaveSeg.p1)
|
||
if (lsl > zg || lsh < zd) {
|
||
zsSure = true
|
||
break
|
||
}
|
||
}
|
||
|
||
var zs = {
|
||
t0: s1.t0,
|
||
t1: lastSegInCenter.t1,
|
||
high: zg, low: zd,
|
||
zg: zg, zd: zd,
|
||
gg: gg, dd: dd,
|
||
is_sure: zsSure,
|
||
seg_count: segList_for_zs.length,
|
||
seg_list: segList_for_zs,
|
||
start_seg_idx: startIdx,
|
||
dir: zsDir,
|
||
pre: lastZs,
|
||
next: null,
|
||
}
|
||
|
||
if (lastZs) lastZs.next = zs
|
||
|
||
zsList.push(zs)
|
||
lastZs = zs
|
||
startIdx = startIdx + 4 + (addedSegs.length > 0 ? addedSegs.length : 0)
|
||
}
|
||
|
||
return zsList
|
||
}
|
||
|
||
/** @deprecated — use findBiCenters + findSegCenters */
|
||
function findCenters(segs) {
|
||
var segZss = findSegCenters(segs)
|
||
return { zs: segZss, segzs: segZss }
|
||
}
|
||
|
||
|
||
// ======================== 买卖点 (BSP) 检测 ========================
|
||
|
||
/**
|
||
* 缠论三类买卖点 —— 基于缠论原定义
|
||
*
|
||
* 一买:向下离开中枢的笔结束点(低于 ZD),与进入笔形成盘整背驰 → 位于中枢下方
|
||
* 一卖:向上离开中枢的笔结束点(高于 ZG),与进入笔形成盘整背驰 → 位于中枢上方
|
||
* 二买:一买后反弹再回调,底点不低于一买 → 位于中枢下方
|
||
* 二卖:一卖后回调再反弹,顶点不高于一卖 → 位于中枢上方
|
||
* 三买:向上离开中枢后回拉,回拉底点不破 ZG → 位于中枢上方
|
||
* 三卖:向下离开中枢后反弹,反弹顶点不破 ZD → 位于中枢下方
|
||
*/
|
||
function findBSP(strokes, biZsList, segZsList) {
|
||
var bsps = []
|
||
var segBsps = []
|
||
var bspsSeen = {}
|
||
var segBspsSeen = {}
|
||
|
||
// ==================== 笔中枢买卖点 ====================
|
||
if (biZsList && biZsList.length > 0) {
|
||
for (var zi = 0; zi < biZsList.length; zi++) {
|
||
var zs = biZsList[zi]
|
||
if (!zs.is_sure || !zs.bi_list || zs.bi_list.length < 3) continue
|
||
|
||
// 中枢最后一笔在总笔列表中的索引
|
||
var lastZsBi = zs.bi_list[zs.bi_list.length - 1]
|
||
var lastZsBiIdx = -1
|
||
for (var k = 0; k < strokes.length; k++) {
|
||
if (strokes[k].t0 === lastZsBi.t0 && strokes[k].p0 === lastZsBi.p0) {
|
||
lastZsBiIdx = k
|
||
break
|
||
}
|
||
}
|
||
if (lastZsBiIdx < 0) continue
|
||
|
||
// — 确定离开笔:中枢完成后第一根突破中枢区间的笔 —
|
||
var leaveBi = null
|
||
var leaveBiIdx = -1
|
||
for (var j = lastZsBiIdx; j < strokes.length; j++) {
|
||
var bi = strokes[j]
|
||
if (!bi.sure) continue
|
||
var biHigh = Math.max(bi.p0, bi.p1)
|
||
var biLow = Math.min(bi.p0, bi.p1)
|
||
// 向上离开:高点突破 ZG(位于中枢上方)
|
||
if (bi.dir === 1 && biHigh > zs.zg) { leaveBi = bi; leaveBiIdx = j; break }
|
||
// 向下离开:低点跌破 ZD(位于中枢下方)
|
||
if (bi.dir === -1 && biLow < zs.zd) { leaveBi = bi; leaveBiIdx = j; break }
|
||
}
|
||
if (!leaveBi) continue
|
||
|
||
var isUpLeave = leaveBi.dir === 1
|
||
|
||
// — 一类买卖点 —
|
||
// 一卖:向上离开,结束点在中枢上方
|
||
// 一买:向下离开,结束点在中枢下方
|
||
if (isUpLeave) {
|
||
var kS1 = leaveBi.t1 + '-bi-sell-T1P'
|
||
if (!bspsSeen[kS1]) {
|
||
bspsSeen[kS1] = true
|
||
bsps.push({ t: leaveBi.t1, price: leaveBi.p1, is_buy: false, types: ['T1P'],
|
||
reason: '一卖:向上离开中枢' })
|
||
}
|
||
} else {
|
||
var kB1 = leaveBi.t1 + '-bi-buy-T1'
|
||
if (!bspsSeen[kB1]) {
|
||
bspsSeen[kB1] = true
|
||
bsps.push({ t: leaveBi.t1, price: leaveBi.p1, is_buy: true, types: ['T1'],
|
||
reason: '一买:向下离开中枢' })
|
||
}
|
||
}
|
||
|
||
// — 离开后第一笔(回拉/反弹)—
|
||
var nextBi = leaveBiIdx + 1 < strokes.length ? strokes[leaveBiIdx + 1] : null
|
||
if (nextBi && nextBi.sure) {
|
||
if (isUpLeave && nextBi.dir === -1) {
|
||
// 向上离开后向下回拉 → 潜在三买(位于中枢上方)
|
||
if (Math.min(nextBi.p0, nextBi.p1) >= zs.zg) {
|
||
var kB3 = nextBi.t1 + '-bi-buy-T3A'
|
||
if (!bspsSeen[kB3]) {
|
||
bspsSeen[kB3] = true
|
||
bsps.push({ t: nextBi.t1, price: nextBi.p1, is_buy: true, types: ['T3A'],
|
||
reason: '三买:回拉不进中枢' })
|
||
}
|
||
}
|
||
} else if (!isUpLeave && nextBi.dir === 1) {
|
||
// 向下离开后向上反弹 → 潜在三卖(位于中枢下方)
|
||
if (Math.max(nextBi.p0, nextBi.p1) <= zs.zd) {
|
||
var kS3 = nextBi.t1 + '-bi-sell-T3B'
|
||
if (!bspsSeen[kS3]) {
|
||
bspsSeen[kS3] = true
|
||
bsps.push({ t: nextBi.t1, price: nextBi.p1, is_buy: false, types: ['T3B'],
|
||
reason: '三卖:反弹不进中枢' })
|
||
}
|
||
}
|
||
}
|
||
|
||
// — 离开后第二笔 → 二类买卖点 —
|
||
var secondBi = leaveBiIdx + 2 < strokes.length ? strokes[leaveBiIdx + 2] : null
|
||
if (secondBi && secondBi.sure) {
|
||
if (isUpLeave) {
|
||
// 一卖 → 回拉 → 再次向上:不创新高即二卖(位于中枢上方)
|
||
if (secondBi.dir === 1 && Math.max(secondBi.p0, secondBi.p1) < Math.max(leaveBi.p0, leaveBi.p1)) {
|
||
var kS2 = secondBi.t1 + '-bi-sell-T2S'
|
||
if (!bspsSeen[kS2]) {
|
||
bspsSeen[kS2] = true
|
||
bsps.push({ t: secondBi.t1, price: secondBi.p1, is_buy: false, types: ['T2S'],
|
||
reason: '二卖:反弹不创新高' })
|
||
}
|
||
}
|
||
} else {
|
||
// 一买 → 反弹 → 再次向下:不创新低即二买(位于中枢下方)
|
||
if (secondBi.dir === -1 && Math.min(secondBi.p0, secondBi.p1) > Math.min(leaveBi.p0, leaveBi.p1)) {
|
||
var kB2 = secondBi.t1 + '-bi-buy-T2'
|
||
if (!bspsSeen[kB2]) {
|
||
bspsSeen[kB2] = true
|
||
bsps.push({ t: secondBi.t1, price: secondBi.p1, is_buy: true, types: ['T2'],
|
||
reason: '二买:回调不创新低' })
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 段中枢买卖点 ====================
|
||
// 段中枢的买卖点同理笔中枢,使用段而非笔
|
||
if (segZsList && segZsList.length > 0) {
|
||
for (var zj = 0; zj < segZsList.length; zj++) {
|
||
var szs = segZsList[zj]
|
||
if (!szs.is_sure || !szs.seg_list || szs.seg_list.length < 3) continue
|
||
|
||
// 段中枢结束后第一笔突破中枢区间的笔
|
||
var sLastSeg = szs.seg_list[szs.seg_list.length - 1]
|
||
var sLeaveBi = null
|
||
for (var sj = 0; sj < strokes.length; sj++) {
|
||
if (strokes[sj].t0 > sLastSeg.t1) {
|
||
sLeaveBi = strokes[sj]
|
||
break
|
||
}
|
||
}
|
||
if (!sLeaveBi || !sLeaveBi.sure) continue
|
||
|
||
var slHigh = Math.max(sLeaveBi.p0, sLeaveBi.p1)
|
||
var slLow = Math.min(sLeaveBi.p0, sLeaveBi.p1)
|
||
var sLeaves = (sLeaveBi.dir === 1 && slHigh > szs.zg) || (sLeaveBi.dir === -1 && slLow < szs.zd)
|
||
if (!sLeaves) continue
|
||
|
||
if (sLeaveBi.dir === 1) {
|
||
var skS1 = sLeaveBi.t1 + '-seg-sell-T1P'
|
||
if (!segBspsSeen[skS1]) {
|
||
segBspsSeen[skS1] = true
|
||
segBsps.push({ t: sLeaveBi.t1, price: sLeaveBi.p1, is_buy: false, types: ['T1P'],
|
||
reason: '段一卖:向上离开段中枢' })
|
||
}
|
||
} else {
|
||
var skB1 = sLeaveBi.t1 + '-seg-buy-T1'
|
||
if (!segBspsSeen[skB1]) {
|
||
segBspsSeen[skB1] = true
|
||
segBsps.push({ t: sLeaveBi.t1, price: sLeaveBi.p1, is_buy: true, types: ['T1'],
|
||
reason: '段一买:向下离开段中枢' })
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return { bsps: bsps, seg_bsps: segBsps }
|
||
}
|
||
|
||
|
||
// ======================== 主入口 ========================
|
||
|
||
/**
|
||
* 计算缠论结构
|
||
* @param {Array} bars - OHLCV bar 数组
|
||
* @returns {Object} ChanSlice {bis, segs, zs, segzs, bsps, seg_bsps}
|
||
*/
|
||
function computeChan(bars) {
|
||
if (!bars || bars.length < 10) {
|
||
return { bis: [], segs: [], zs: [], segzs: [], bsps: [], seg_bsps: [] }
|
||
}
|
||
|
||
// Step 1: 包含处理 → KLC
|
||
var klcList = inclusionMerge(bars)
|
||
|
||
// Step 2+3: 笔检测(内部包含分型检测 + 缺口测试)
|
||
var strokes = findStrokes(klcList)
|
||
|
||
// Step 4: 线段检测
|
||
var segs = findSegments(strokes)
|
||
|
||
// Step 5: 中枢检测
|
||
// 笔中枢:从笔列表计算(参照 cal_bi_zs_list)
|
||
var bi_zs = findBiCenters(strokes)
|
||
// 段中枢:从段列表计算(参照 calculate_seg_zs)
|
||
var seg_zs = findSegCenters(segs)
|
||
|
||
// Step 6: 买卖点检测(基于笔中枢和段中枢)
|
||
var bspResult = findBSP(strokes, bi_zs, seg_zs)
|
||
var bsps = bspResult.bsps
|
||
var seg_bsps = bspResult.seg_bsps
|
||
|
||
console.log('[缠论引擎] KLC:', klcList.length,
|
||
'笔:', strokes.length,
|
||
'段:', segs.length,
|
||
'笔中枢:', bi_zs.length,
|
||
'段中枢:', seg_zs.length,
|
||
'BSP:', bsps.length)
|
||
|
||
return {
|
||
bis: strokes, segs: segs,
|
||
zs: bi_zs, segzs: seg_zs.length > 0 ? seg_zs : bi_zs,
|
||
bsps: bsps, seg_bsps: seg_bsps,
|
||
}
|
||
}
|
||
|
||
// Export
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = { computeChan, inclusionMerge, findFractals, findStrokes, findSegments, findCenters, findBSP }
|
||
}
|