feat: 第四类买卖点(B4/S4)融入缠论引擎与 web 展示
研究侧的 fast_bsp3 一直只活在 research/lib/ 里,web 端看不到,回测与目视 两条线对不上。这次把它搬进引擎,作为独立的第四类买卖点。 之所以单独立类而不是当作 B3/S3 的低滞后版:step30/31 显示引擎原生的 B3/S3 统计上呈逆势、显著亏损(胜率 27.4%、PF 0.66、t −18.76),而同一组 过滤器把 B4 从 PF 1.59 提到 2.26 却对它无效(0.66→0.71)。两者选的是 不同的交易群体,不是同一信号的早晚两版。 - chanlun/analysis/fast_bsp.py 原样搬入 find_fast_bsp3 与 build_htf_zones, 另加 add_zone_ladder / htf_fx_timeline / attach_htf_agree - research/lib/ 两个模块改为转发,所有 step 脚本导入不变,信号逐条比对一致 - 大级别上下文用 resample 从同一份 df 构建,不额外拉数据,因此与界面上选的 周期和时间范围无关 - 前端三个复选框 + 过滤模式下拉;未过滤的原始信号用浅色,避免与主口径混淆 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+5
-1
@@ -241,7 +241,9 @@ def analyze():
|
||||
'is_sure': bool(bsp.is_sure),
|
||||
'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None,
|
||||
'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0
|
||||
} for bsp in analysis_result.get('bsp_list', [])]
|
||||
} for bsp in analysis_result.get('bsp_list', [])],
|
||||
# 添加主周期第四类买卖点(B4/S4,低滞后三类买卖点)
|
||||
'fast_bsp_list': serialize_fast_bsp_list(analysis_result.get('fast_bsp_list', []), client_tz)
|
||||
})
|
||||
|
||||
|
||||
@@ -425,6 +427,7 @@ def analyze():
|
||||
'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None,
|
||||
'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0
|
||||
} for bsp in element_analysis.get('bsp_list', [])]
|
||||
result['element_fast_bsp_list'] = serialize_fast_bsp_list(element_analysis.get('fast_bsp_list', []), client_tz)
|
||||
|
||||
# 次次周期:仅当已指定次周期且次次周期有效时获取
|
||||
if sub_sub_timeframe and is_smaller_or_equal_timeframe(sub_sub_timeframe, element_timeframe):
|
||||
@@ -526,6 +529,7 @@ def analyze():
|
||||
'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None,
|
||||
'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0
|
||||
} for bsp in sub_sub_analysis.get('bsp_list', [])]
|
||||
result['sub_sub_fast_bsp_list'] = serialize_fast_bsp_list(sub_sub_analysis.get('fast_bsp_list', []), client_tz)
|
||||
result['sub_sub_chan_macd'] = serialize_chan_macd_data(sub_sub_analysis.get('chan_macd', {}), client_tz)
|
||||
try:
|
||||
sub_sub_klc_trend = []
|
||||
|
||||
@@ -67,6 +67,7 @@ from .serialize import ( # noqa: F401
|
||||
convert_direction,
|
||||
format_time_safely,
|
||||
serialize_chan_macd_data,
|
||||
serialize_fast_bsp_list,
|
||||
clean_dataframe_for_json,
|
||||
get_uncompleted_seg_list,
|
||||
)
|
||||
|
||||
@@ -30,6 +30,15 @@ def analyze_chan(df, symbol=None, timeframe=None):
|
||||
bsp_list = []
|
||||
if len(bi_zs_list) > 0:
|
||||
bsp_list = chan.find_all_bsp(bi_list, bi_zs_list)
|
||||
# 第四类买卖点(B4/S4):复用上面刚算好的 bi_zs_list,不重复建中枢。
|
||||
# 大级别由同一份 df 重采样得到,失败时退化为空列表,不拖垮主分析。
|
||||
fast_bsp_list = []
|
||||
try:
|
||||
if len(bi_zs_list) > 0:
|
||||
fast_bsp_list = chan.cal_fast_bsp(df=df, bi_zs_list=bi_zs_list, timeframe=timeframe)
|
||||
except Exception as e:
|
||||
print(f"第四类买卖点计算出错: {e}")
|
||||
fast_bsp_list = []
|
||||
#bsp_state_list = chan.get_bsp_state(df)
|
||||
#for bsp in bsp_list:
|
||||
#print(bsp.end_time, bsp.type, bsp.dir)
|
||||
@@ -177,6 +186,7 @@ def analyze_chan(df, symbol=None, timeframe=None):
|
||||
'zs_list': zs_list,
|
||||
'bi_zs_list': bi_zs_list, # 添加BI中枢列表
|
||||
'bsp_list': bsp_list, # 添加买卖点列表
|
||||
'fast_bsp_list': fast_bsp_list, # 第四类买卖点(B4/S4)
|
||||
'klc_fx_info': klc_fx_info, # KLC分型信息
|
||||
'chan_macd': chan_macd_data, # 添加ChanMACD分析数据
|
||||
'ema52_dict': ema52_dict # 添加多时间周期EMA52数据
|
||||
|
||||
@@ -31,6 +31,36 @@ def format_time_safely(time_obj, client_tz):
|
||||
# 已经是datetime对象
|
||||
return time_obj.astimezone(client_tz).isoformat()
|
||||
|
||||
def serialize_fast_bsp_list(fast_bsp_list, client_tz):
|
||||
"""序列化第四类买卖点(ChanFastBSP)。
|
||||
|
||||
htf_agree(大级别分型同向)与 ladder_ok(中枢顺向推进)分别输出,
|
||||
由前端决定要显示全部还是只显示两者都满足的。
|
||||
"""
|
||||
out = []
|
||||
for bsp in fast_bsp_list or []:
|
||||
try:
|
||||
out.append({
|
||||
'time': format_time_safely(bsp.time, client_tz),
|
||||
'price': float(bsp.price),
|
||||
'type': str(bsp.type).split('.')[-1].split('(')[0],
|
||||
'dir': str(bsp.dir).split('.')[-1].split('(')[0],
|
||||
'is_sure': True,
|
||||
'lag': int(bsp.lag),
|
||||
'depth': float(bsp.depth),
|
||||
'zg': bsp.zg,
|
||||
'zd': bsp.zd,
|
||||
'occ': int(bsp.occ),
|
||||
'htf_agree': bsp.htf_agree,
|
||||
'ladder_ok': bsp.ladder_ok,
|
||||
'bo_time': format_time_safely(bsp.bo_time, client_tz) if bsp.bo_time else None,
|
||||
'pb_time': format_time_safely(bsp.pb_time, client_tz) if bsp.pb_time else None,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"序列化fast_bsp出错: {e}")
|
||||
continue
|
||||
return out
|
||||
|
||||
def serialize_chan_macd_data(chan_macd_data, client_tz):
|
||||
"""序列化ChanMACD数据为JSON可序列化格式"""
|
||||
serialized_data = {
|
||||
|
||||
@@ -852,6 +852,8 @@ function chartTvRenderOverlays(ctx) {
|
||||
'BSP1_SELL': { color: '#00E676', text: 'S1', position: 'aboveBar', size: 0.5 },
|
||||
'BSP2_SELL': { color: '#00B0FF', text: 'S2', position: 'aboveBar', size: 0.5 },
|
||||
'BSP3_SELL': { color: '#8B4513', text: 'S3', position: 'aboveBar', size: 0.5 },
|
||||
'BSP4_BUY': { color: '#FF6D00', text: 'B4', position: 'belowBar', size: 0.5 },
|
||||
'BSP4_SELL': { color: '#0091EA', text: 'S4', position: 'aboveBar', size: 0.5 },
|
||||
};
|
||||
|
||||
const getBspStyleKey = (bsp) => {
|
||||
@@ -987,7 +989,57 @@ function chartTvRenderOverlays(ctx) {
|
||||
// 关闭 BSP 显示时,清空全局 BSP 标记
|
||||
window.bspMarkers = [];
|
||||
}
|
||||
|
||||
|
||||
// 第四类买卖点(B4/S4):中枢突破回抽后当根入场,位置同 B3/S3 但早 7~8 根。
|
||||
// 与 BSP 分开收集,因为它数量远多于 B1/B2/B3,混在一个开关里图会糊掉。
|
||||
if ($('#showMainFastBsp').is(':checked') || $('#showElementFastBsp').is(':checked') || $('#showSubSubFastBsp').is(':checked')) {
|
||||
// 深色 = 区间套(大级别分型同向) + 中枢顺向推进都满足;浅色 = 未通过过滤
|
||||
const FAST_BSP_STYLE = {
|
||||
'BUY': { strong: '#FF6D00', weak: '#FFCC80', text: 'B4', position: 'belowBar' },
|
||||
'SELL': { strong: '#0091EA', weak: '#81D4FA', text: 'S4', position: 'aboveBar' },
|
||||
};
|
||||
const onlyFiltered = ($('#fastBspFilterMode').val() || 'all') === 'filtered';
|
||||
const allFastBspMarkers = [];
|
||||
|
||||
const collectFastBsp = function(list, prefix, label) {
|
||||
(list || []).forEach(function(bsp) {
|
||||
try {
|
||||
const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
|
||||
if (isNaN(ts)) return;
|
||||
const style = FAST_BSP_STYLE[(bsp.dir || '').toUpperCase()];
|
||||
if (!style) return;
|
||||
const passed = !!(bsp.htf_agree && bsp.ladder_ok);
|
||||
if (onlyFiltered && !passed) return;
|
||||
allFastBspMarkers.push({
|
||||
time: ts,
|
||||
position: style.position,
|
||||
color: passed ? style.strong : style.weak,
|
||||
text: prefix + (passed ? style.text : style.text.toLowerCase()),
|
||||
size: passed ? 2 : 1
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(label + '第四类买卖点处理出错:', e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if ($('#showMainFastBsp').is(':checked')) {
|
||||
collectFastBsp(currentData.fast_bsp_list, '', '主周期');
|
||||
}
|
||||
if ($('#showElementFastBsp').is(':checked')) {
|
||||
collectFastBsp(currentData.element_fast_bsp_list, 'e', '次周期');
|
||||
}
|
||||
if ($('#showSubSubFastBsp').is(':checked')) {
|
||||
collectFastBsp(currentData.sub_sub_fast_bsp_list, 's', '次次周期');
|
||||
}
|
||||
|
||||
allFastBspMarkers.sort((a, b) => a.time - b.time);
|
||||
window.fastBspMarkers = allFastBspMarkers;
|
||||
console.log(`绘制第四类买卖点,共${allFastBspMarkers.length}个标记(${onlyFiltered ? '仅过滤后' : '全部'})`);
|
||||
} else {
|
||||
window.fastBspMarkers = [];
|
||||
}
|
||||
|
||||
// 添加买卖点标记(旧版,保留兼容)
|
||||
// 这里为了与主面板上的「买卖点」开关保持一致,
|
||||
// 同时响应顶部的 `#showMainBsp` 复选框
|
||||
@@ -2113,7 +2165,8 @@ function chartTvRenderOverlays(ctx) {
|
||||
...(window.kluDivMarkersElement || []),
|
||||
...(window.kluDivMarkersSubSub || []),
|
||||
...trendMarkersToUse,
|
||||
...(window.bspMarkers || [])
|
||||
...(window.bspMarkers || []),
|
||||
...(window.fastBspMarkers || [])
|
||||
];
|
||||
if (combinedMarkers.length > 0) {
|
||||
console.log(
|
||||
@@ -2122,6 +2175,7 @@ function chartTvRenderOverlays(ctx) {
|
||||
'个,小周期分型:', allElementFxMarkers.length,
|
||||
'个,UnitTF:', (window.unittfMarkers || []).length,
|
||||
'个,BSP标记:', (window.bspMarkers || []).length,
|
||||
'个,第四类标记:', (window.fastBspMarkers || []).length,
|
||||
'个)'
|
||||
);
|
||||
|
||||
@@ -2223,14 +2277,15 @@ function chartTvRenderOverlays(ctx) {
|
||||
// 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记
|
||||
// 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记,
|
||||
// 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。
|
||||
// 修复:把 BSP 标记一并合并进来。
|
||||
// 修复:把 BSP 标记一并合并进来。第四类买卖点同理,两处都要带上。
|
||||
const onlyMainAndU = [
|
||||
...(window.mainFxMarkers || []),
|
||||
...(window.kluDivMarkersMain || []),
|
||||
...(window.kluDivMarkersElement || []),
|
||||
...(window.kluDivMarkersSubSub || []),
|
||||
...trendMarkersToUse,
|
||||
...(window.bspMarkers || [])
|
||||
...(window.bspMarkers || []),
|
||||
...(window.fastBspMarkers || [])
|
||||
];
|
||||
if (onlyMainAndU.length > 0) {
|
||||
console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, ',UnitTF:', (window.unittfMarkers || []).length, ')');
|
||||
|
||||
@@ -649,6 +649,11 @@ $('#showElementBsp').change(function() {
|
||||
updateChartDisplay();
|
||||
});
|
||||
|
||||
// 第四类买卖点(B4/S4)显示开关与过滤模式
|
||||
$('#showMainFastBsp, #showElementFastBsp, #showSubSubFastBsp, #fastBspFilterMode').change(function() {
|
||||
updateChartDisplay();
|
||||
});
|
||||
|
||||
// 在控制台输出当前显示状态
|
||||
console.log('当前显示状态:', {
|
||||
'showOriginalKline': $('#showOriginalKline').is(':checked'),
|
||||
|
||||
@@ -1017,6 +1017,17 @@
|
||||
<input class="form-check-input" type="checkbox" id="showMainBsp">
|
||||
<label class="form-check-label" for="showMainBsp">买卖点</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showMainFastBsp">
|
||||
<label class="form-check-label" for="showMainFastBsp">第四类</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<select id="fastBspFilterMode" class="form-select form-select-sm" style="width: 130px;"
|
||||
title="深色为区间套(大级别分型同向)+中枢顺向推进都满足的信号,浅色为未通过过滤">
|
||||
<option value="all" selected>第四类:全部</option>
|
||||
<option value="filtered">第四类:仅过滤后</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mt-1">
|
||||
<label class="form-label me-0 mb-0">次周期:</label>
|
||||
@@ -1059,6 +1070,10 @@
|
||||
<input class="form-check-input" type="checkbox" id="showElementBsp">
|
||||
<label class="form-check-label" for="showElementBsp">买卖点</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showElementFastBsp">
|
||||
<label class="form-check-label" for="showElementFastBsp">第四类</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mt-1">
|
||||
<label class="form-label me-0 mb-0">次次周期:</label>
|
||||
@@ -1101,6 +1116,10 @@
|
||||
<input class="form-check-input" type="checkbox" id="showSubSubBsp">
|
||||
<label class="form-check-label" for="showSubSubBsp">买卖点</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showSubSubFastBsp">
|
||||
<label class="form-check-label" for="showSubSubFastBsp">第四类</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1269,12 +1288,12 @@
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_shell.js') }}?v=20260810e"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_indicators.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_chan.js') }}?v=20260810d"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_overlays.js') }}?v=20260810e"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_overlays.js') }}?v=20260827a"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_finalize.js') }}?v=20260810a"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260809z"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tables.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260810e"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260827a"></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>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"bi_zs_list",
|
||||
"bsp_list",
|
||||
"chan_macd",
|
||||
"fast_bsp_list",
|
||||
"klc_fx_info",
|
||||
"klc_list",
|
||||
"klc_trend",
|
||||
|
||||
@@ -35,6 +35,7 @@ ANALYZE_CHAN_KEYS = {
|
||||
"zs_list",
|
||||
"bi_zs_list",
|
||||
"bsp_list",
|
||||
"fast_bsp_list",
|
||||
"klc_fx_info",
|
||||
"chan_macd",
|
||||
"ema52_dict",
|
||||
@@ -87,7 +88,7 @@ def test_klines_recent_returns_tail_only():
|
||||
|
||||
def test_contract_keys_stable():
|
||||
assert "bi_list" in CONTRACT_KEYS and "seg_list" in CONTRACT_KEYS
|
||||
for k in ("kline_data", "macd", "zs_list", "bsp_list", "chan_macd"):
|
||||
for k in ("kline_data", "macd", "zs_list", "bsp_list", "fast_bsp_list", "chan_macd"):
|
||||
assert k in CONTRACT_KEYS
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user