Enhance converging triangle analysis with boundary utilization scoring and detailed chart mode
- Introduced a new boundary utilization score to measure price proximity to triangle boundaries, improving the accuracy of strength assessments. - Updated scoring weights to incorporate boundary utilization, adjusting the contributions of convergence, volume, and fitting scores. - Added detailed chart mode in the stock viewer, allowing users to toggle between standard and detailed views with additional metrics displayed. - Enhanced documentation to reflect new features, including usage instructions for the boundary utilization score and detailed chart mode. - Improved error handling in the stock viewer for better user experience.
@ -79,3 +79,89 @@ result = detect_converging_triangle(high, low, close, volume, params)
|
||||
- 分位数回归: `fit_boundary_quantile()`
|
||||
- 锚点拟合: `fit_boundary_anchor()`
|
||||
- 分发函数: `fit_pivot_line_dispatch()`
|
||||
|
||||
# 拟合度分数低,强度分却整体偏高
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 已实现:边界利用率分数(2026-01-27)
|
||||
|
||||
### 问题分析
|
||||
|
||||
观察图中 SZ002748 世龙实业:
|
||||
- 宽度比:0.12(非常收敛)
|
||||
- 强度分:0.177(排名第三)
|
||||
- 但肉眼观察:价格走势与三角形边界之间有**大量空白**
|
||||
|
||||
**原因**:
|
||||
- 原权重:收敛分 20%、拟合贴合度 15%
|
||||
- 当宽度比 0.12 时,收敛分 = 1 - 0.12 = 0.88
|
||||
- 收敛分贡献 = 0.20 × 0.88 = 0.176 ≈ 全部强度分
|
||||
- **收敛分只衡量"形状收窄",不衡量"价格是否贴近边界"**
|
||||
|
||||
### 解决方案
|
||||
|
||||
新增**边界利用率**分数,衡量价格走势对三角形通道空间的利用程度。
|
||||
|
||||
### 新增函数
|
||||
|
||||
```python
|
||||
def calc_boundary_utilization(
|
||||
high, low,
|
||||
upper_slope, upper_intercept,
|
||||
lower_slope, lower_intercept,
|
||||
start, end,
|
||||
) -> float:
|
||||
"""
|
||||
计算边界利用率 (0~1)
|
||||
|
||||
对窗口内每一天:
|
||||
1. 计算价格到上下边界的距离
|
||||
2. 空白比例 = (到上沿距离 + 到下沿距离) / 通道宽度
|
||||
3. 当日利用率 = 1 - 空白比例
|
||||
|
||||
返回平均利用率
|
||||
"""
|
||||
```
|
||||
|
||||
### 新权重配置
|
||||
|
||||
| 分量 | 原权重 | 新权重 | 说明 |
|
||||
|------|--------|--------|------|
|
||||
| 突破幅度 | 50% | **50%** | 不变 |
|
||||
| 收敛分 | 20% | **15%** | 降低 |
|
||||
| 成交量分 | 15% | **10%** | 降低 |
|
||||
| 拟合贴合度 | 15% | **10%** | 降低 |
|
||||
| **边界利用率** | - | **15%** | 新增 |
|
||||
|
||||
### 空白惩罚(新增)
|
||||
|
||||
为避免“通道很宽但价格很空”的误判,加入空白惩罚:
|
||||

|
||||

|
||||
```
|
||||
UTILIZATION_FLOOR = 0.20
|
||||
惩罚系数 = min(1, boundary_utilization / UTILIZATION_FLOOR)
|
||||
最终强度分 = 原强度分 × 惩罚系数
|
||||
```
|
||||
|
||||
当边界利用率明显偏低时,总分会被进一步压制。
|
||||
|
||||
### 结果字段
|
||||
|
||||
`ConvergingTriangleResult` 新增字段:
|
||||
```python
|
||||
boundary_utilization: float = 0.0 # 边界利用率分数
|
||||
```
|
||||
|
||||
### 效果
|
||||
|
||||
- 价格贴近边界(空白少)→ 利用率高 → 强度分高
|
||||
- 价格远离边界(空白多)→ 利用率低 → 强度分被惩罚
|
||||
- 当边界利用率 < 0.20 时,强度分按比例衰减(空白惩罚)
|
||||
- 解决"形状收敛但空白多"的误判问题
|
||||
|
||||
# 上/下沿线,有些点没有碰到线的边缘
|
||||

|
||||

|
||||
BIN
discuss/images/2026-01-27-16-26-02.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
discuss/images/2026-01-27-17-56-30.png
Normal file
|
After Width: | Height: | Size: 350 KiB |
BIN
discuss/images/2026-01-27-17-56-41.png
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
discuss/images/2026-01-27-18-00-32.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
discuss/images/2026-01-27-18-01-07.png
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
discuss/images/2026-01-27-18-49-17.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
discuss/images/2026-01-27-18-49-33.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
@ -68,6 +68,7 @@ def load_stock_data(csv_path: str, target_date: int = None, all_stocks_mode: boo
|
||||
'touchesUpper': int(row.get('touches_upper', '0')),
|
||||
'touchesLower': int(row.get('touches_lower', '0')),
|
||||
'volumeConfirmed': row.get('volume_confirmed', ''),
|
||||
'boundaryUtilization': float(row.get('boundary_utilization', '0')),
|
||||
'date': date,
|
||||
'hasTriangle': True # 标记为有三角形形态
|
||||
}
|
||||
@ -77,6 +78,7 @@ def load_stock_data(csv_path: str, target_date: int = None, all_stocks_mode: boo
|
||||
# 清理文件名中的非法字符
|
||||
clean_name = stock['name'].replace('*', '').replace('?', '').replace('"', '').replace('<', '').replace('>', '').replace('|', '').replace(':', '').replace('/', '').replace('\\', '')
|
||||
stock['chartPath'] = f"charts/{date}_{stock_code}_{clean_name}.png"
|
||||
stock['chartPathDetail'] = f"charts/{date}_{stock_code}_{clean_name}_detail.png"
|
||||
|
||||
if stock_code not in stocks_map or stocks_map[stock_code]['strength'] < stock['strength']:
|
||||
stocks_map[stock_code] = stock
|
||||
@ -103,8 +105,10 @@ def load_stock_data(csv_path: str, target_date: int = None, all_stocks_mode: boo
|
||||
'touchesUpper': 0,
|
||||
'touchesLower': 0,
|
||||
'volumeConfirmed': '',
|
||||
'boundaryUtilization': 0.0,
|
||||
'date': use_date,
|
||||
'chartPath': f"charts/{use_date}_{code}_{clean_name}.png",
|
||||
'chartPathDetail': f"charts/{use_date}_{code}_{clean_name}_detail.png",
|
||||
'hasTriangle': False # 标记为无三角形形态
|
||||
}
|
||||
|
||||
@ -873,6 +877,33 @@ def generate_html(stocks: list, date: int, output_path: str):
|
||||
color: var(--accent-warm);
|
||||
}
|
||||
|
||||
/* Detail Toggle */
|
||||
.detail-toggle {
|
||||
padding: 10px 18px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 10px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-family: 'Noto Sans SC', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.detail-toggle:hover {
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.detail-toggle.active {
|
||||
background: rgba(0, 212, 170, 0.1);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* Filter Chips */
|
||||
.filter-chips {
|
||||
display: flex;
|
||||
@ -1033,6 +1064,16 @@ def generate_html(stocks: list, date: int, output_path: str):
|
||||
background: var(--accent-cool);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// 工具函数(需要在页面加载前定义,因为HTML元素的onerror会直接调用)
|
||||
function handleModalImageError(img) {
|
||||
const fallbackSrc = img.dataset.fallbackSrc;
|
||||
if (fallbackSrc && img.src !== fallbackSrc && img.dataset.fallbackTried !== 'true') {
|
||||
img.dataset.fallbackTried = 'true';
|
||||
img.src = fallbackSrc;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
@ -1062,6 +1103,9 @@ def generate_html(stocks: list, date: int, output_path: str):
|
||||
</select>
|
||||
<button class="sort-toggle active" id="sortOrder" title="切换排序顺序">↓</button>
|
||||
</div>
|
||||
<button class="detail-toggle" id="detailToggle" title="切换详细图">
|
||||
详细图: 关
|
||||
</button>
|
||||
<button class="reset-btn" id="resetBtn">
|
||||
<span>↺</span> 重置筛选
|
||||
</button>
|
||||
@ -1136,13 +1180,14 @@ def generate_html(stocks: list, date: int, output_path: str):
|
||||
<div id="imageModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<button class="close-modal" onclick="closeModal()">×</button>
|
||||
<img id="modalImage" src="" alt="股票图表">
|
||||
<img id="modalImage" src="" alt="股票图表" onerror="handleModalImageError(this)">
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// 内嵌数据
|
||||
const allStocks = STOCK_DATA;
|
||||
let currentThreshold = 0;
|
||||
let showDetailCharts = false;
|
||||
|
||||
// 筛选和排序状态
|
||||
let filters = {
|
||||
@ -1209,6 +1254,15 @@ def generate_html(stocks: list, date: int, output_path: str):
|
||||
filterAndDisplayStocks();
|
||||
});
|
||||
|
||||
// 详细图切换
|
||||
const detailToggle = document.getElementById('detailToggle');
|
||||
detailToggle.addEventListener('click', function() {
|
||||
showDetailCharts = !showDetailCharts;
|
||||
this.textContent = showDetailCharts ? '详细图: 开' : '详细图: 关';
|
||||
this.classList.toggle('active', showDetailCharts);
|
||||
filterAndDisplayStocks();
|
||||
});
|
||||
|
||||
// 方向筛选芯片
|
||||
const directionFilter = document.getElementById('directionFilter');
|
||||
directionFilter.addEventListener('click', function(e) {
|
||||
@ -1350,9 +1404,11 @@ def generate_html(stocks: list, date: int, output_path: str):
|
||||
stock.direction === 'down' ? 'direction-down' : '';
|
||||
const volumeText = stock.volumeConfirmed === 'True' ? '✓' :
|
||||
stock.volumeConfirmed === 'False' ? '✗' : '—';
|
||||
const chartPath = showDetailCharts ? stock.chartPathDetail : stock.chartPath;
|
||||
const fallbackPath = showDetailCharts ? stock.chartPath : stock.chartPathDetail;
|
||||
|
||||
return `
|
||||
<div class="stock-card" onclick="openModal('${stock.chartPath}')">
|
||||
<div class="stock-card" onclick="openModal('${stock.chartPath}', '${stock.chartPathDetail}')">
|
||||
<div class="card-header">
|
||||
<div class="stock-identity">
|
||||
<div class="stock-name">${stock.name}</div>
|
||||
@ -1381,11 +1437,16 @@ def generate_html(stocks: list, date: int, output_path: str):
|
||||
<span class="metric-label">放量确认</span>
|
||||
<span class="metric-value">${volumeText}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">边界利用率</span>
|
||||
<span class="metric-value">${(stock.boundaryUtilization || 0).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<img src="${stock.chartPath}"
|
||||
<img src="${chartPath}"
|
||||
alt="${stock.name}"
|
||||
class="stock-chart"
|
||||
data-fallback-src="${fallbackPath}"
|
||||
onerror="handleImageError(this)">
|
||||
</div>
|
||||
</div>
|
||||
@ -1394,6 +1455,12 @@ def generate_html(stocks: list, date: int, output_path: str):
|
||||
}
|
||||
|
||||
function handleImageError(img) {
|
||||
const fallbackSrc = img.dataset.fallbackSrc;
|
||||
if (fallbackSrc && img.src !== fallbackSrc && img.dataset.fallbackTried !== 'true') {
|
||||
img.dataset.fallbackTried = 'true';
|
||||
img.src = fallbackSrc;
|
||||
return;
|
||||
}
|
||||
if (img.dataset.errorHandled) return;
|
||||
img.dataset.errorHandled = 'true';
|
||||
img.style.display = 'none';
|
||||
@ -1403,9 +1470,14 @@ def generate_html(stocks: list, date: int, output_path: str):
|
||||
img.parentElement.appendChild(noChart);
|
||||
}
|
||||
|
||||
function openModal(imagePath) {
|
||||
function openModal(defaultPath, detailPath) {
|
||||
event.stopPropagation();
|
||||
document.getElementById('modalImage').src = imagePath;
|
||||
const modalImage = document.getElementById('modalImage');
|
||||
const targetPath = showDetailCharts ? detailPath : defaultPath;
|
||||
const fallbackPath = showDetailCharts ? defaultPath : detailPath;
|
||||
modalImage.dataset.fallbackTried = 'false';
|
||||
modalImage.dataset.fallbackSrc = fallbackPath;
|
||||
modalImage.src = targetPath;
|
||||
document.getElementById('imageModal').classList.add('show');
|
||||
}
|
||||
|
||||
|
||||
@ -285,6 +285,30 @@ def plot_triangle(
|
||||
# 详细模式:显示拟合点(仅在 show_details=True 且有三角形时)
|
||||
# ========================================================================
|
||||
if show_details and has_triangle and has_enough_data:
|
||||
# 标注所有枢轴点(用于查看拐点分布)
|
||||
if len(ph_display_idx) > 0:
|
||||
ax1.scatter(
|
||||
ph_display_idx,
|
||||
high_win[ph_idx],
|
||||
marker='x',
|
||||
s=60,
|
||||
color='red',
|
||||
alpha=0.6,
|
||||
zorder=4,
|
||||
label=f'上沿枢轴点({len(ph_idx)})',
|
||||
)
|
||||
if len(pl_display_idx) > 0:
|
||||
ax1.scatter(
|
||||
pl_display_idx,
|
||||
low_win[pl_idx],
|
||||
marker='x',
|
||||
s=60,
|
||||
color='green',
|
||||
alpha=0.6,
|
||||
zorder=4,
|
||||
label=f'下沿枢轴点({len(pl_idx)})',
|
||||
)
|
||||
|
||||
# 标注选中的枢轴点(用于拟合线的关键点)
|
||||
if len(selected_ph_display) >= 2:
|
||||
ax1.scatter(
|
||||
@ -324,6 +348,14 @@ def plot_triangle(
|
||||
strength = max(result.breakout_strength_up, result.breakout_strength_down)
|
||||
price_score = max(result.price_score_up, result.price_score_down)
|
||||
|
||||
# 获取边界利用率与惩罚系数(兼容旧数据)
|
||||
boundary_util = getattr(result, 'boundary_utilization', 0.0)
|
||||
utilization_floor = 0.20
|
||||
if utilization_floor > 0:
|
||||
utilization_penalty = min(1.0, boundary_util / utilization_floor)
|
||||
else:
|
||||
utilization_penalty = 1.0
|
||||
|
||||
ax1.set_title(
|
||||
f"{stock_code} {stock_name} - 收敛三角形 (检测窗口: {detect_dates[0]} ~ {detect_dates[-1]})\n"
|
||||
f"显示范围: {display_dates[0]} ~ {display_dates[-1]} ({len(display_dates)}个交易日) "
|
||||
@ -331,8 +363,9 @@ def plot_triangle(
|
||||
f"枢轴点: 高{len(ph_idx)}/低{len(pl_idx)} 触碰: 上{result.touches_upper}/下{result.touches_lower} "
|
||||
f"放量确认: {'是' if result.volume_confirmed else '否' if result.volume_confirmed is False else '-'}\n"
|
||||
f"强度分: {strength:.3f} "
|
||||
f"(价格: {price_score:.3f}×50% + 收敛: {result.convergence_score:.3f}×20% + "
|
||||
f"成交量: {result.volume_score:.3f}×15% + 拟合贴合度: {result.fitting_score:.3f}×15%)",
|
||||
f"(价格: {price_score:.3f}×50% + 收敛: {result.convergence_score:.3f}×15% + "
|
||||
f"成交量: {result.volume_score:.3f}×10% + 拟合贴合度: {result.fitting_score:.3f}×10% + "
|
||||
f"边界利用率: {boundary_util:.3f}×15%) × 利用率惩罚: {utilization_penalty:.2f}",
|
||||
fontsize=11, pad=10
|
||||
)
|
||||
else:
|
||||
@ -396,6 +429,11 @@ def main() -> None:
|
||||
action="store_true",
|
||||
help="为所有108只股票生成图表(包括不满足收敛三角形条件的)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--clear",
|
||||
action="store_true",
|
||||
help="清空当前模式的旧图片(默认不清空)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# 确定是否显示详细信息(命令行参数优先)
|
||||
@ -467,12 +505,12 @@ def main() -> None:
|
||||
print("当日无满足条件的股票")
|
||||
return
|
||||
|
||||
# 4. 创建输出目录并清空对应模式的旧图片
|
||||
# 4. 创建输出目录,并按需清空对应模式的旧图片
|
||||
os.makedirs(args.output_dir, exist_ok=True)
|
||||
|
||||
if args.clear:
|
||||
# 只清空当前模式的图片(简洁模式或详细模式)
|
||||
print(f"\n[4] 清空当前模式的旧图片...")
|
||||
suffix = "_detail.png" if show_details else ".png"
|
||||
# 找出当前模式的文件:简洁模式是不含_detail的.png,详细模式是_detail.png
|
||||
if show_details:
|
||||
old_files = [f for f in os.listdir(args.output_dir) if f.endswith('_detail.png')]
|
||||
@ -483,6 +521,8 @@ def main() -> None:
|
||||
for f in old_files:
|
||||
os.remove(os.path.join(args.output_dir, f))
|
||||
print(f" 已删除 {len(old_files)} 个旧图片 ({'详细模式' if show_details else '简洁模式'})")
|
||||
else:
|
||||
print(f"\n[4] 跳过清空旧图片(使用 --clear 可手动清空)")
|
||||
|
||||
# 5. 检测参数(从统一配置导入)
|
||||
params = DETECTION_PARAMS
|
||||
|
||||
@ -80,6 +80,7 @@ class ConvergingTriangleResult:
|
||||
convergence_score: float = 0.0 # 收敛分数
|
||||
volume_score: float = 0.0 # 成交量分数
|
||||
fitting_score: float = 0.0 # 拟合贴合度分数
|
||||
boundary_utilization: float = 0.0 # 边界利用率分数
|
||||
|
||||
# 几何属性
|
||||
upper_slope: float = 0.0
|
||||
@ -811,6 +812,72 @@ def calc_fitting_adherence(
|
||||
return min(1.0, max(0.0, adherence_score))
|
||||
|
||||
|
||||
def calc_boundary_utilization(
|
||||
high: np.ndarray,
|
||||
low: np.ndarray,
|
||||
upper_slope: float,
|
||||
upper_intercept: float,
|
||||
lower_slope: float,
|
||||
lower_intercept: float,
|
||||
start: int,
|
||||
end: int,
|
||||
) -> float:
|
||||
"""
|
||||
计算边界利用率 (0~1)
|
||||
|
||||
衡量价格走势对三角形通道空间的利用程度。
|
||||
如果价格总是远离边界线(大量空白),利用率低。
|
||||
如果价格频繁接近或触碰边界线,利用率高。
|
||||
|
||||
计算方法:
|
||||
1. 对窗口内每一天,计算价格到上下边界的相对距离
|
||||
2. 利用率 = 1 - 平均相对空白比例
|
||||
|
||||
归一化映射:
|
||||
- 空白 0% → 利用率 1.00 (价格完全贴合边界)
|
||||
- 空白 25% → 利用率 0.75 (良好)
|
||||
- 空白 50% → 利用率 0.50 (一般)
|
||||
- 空白 75% → 利用率 0.25 (较差,大量空白)
|
||||
|
||||
Args:
|
||||
high, low: 价格数据
|
||||
upper_slope, upper_intercept: 上沿线参数
|
||||
lower_slope, lower_intercept: 下沿线参数
|
||||
start, end: 窗口范围
|
||||
|
||||
Returns:
|
||||
utilization: 0~1,越大表示利用率越高
|
||||
"""
|
||||
total_utilization = 0.0
|
||||
valid_days = 0
|
||||
|
||||
for i in range(start, end + 1):
|
||||
upper_line = upper_slope * i + upper_intercept
|
||||
lower_line = lower_slope * i + lower_intercept
|
||||
channel_width = upper_line - lower_line
|
||||
|
||||
if channel_width <= 0:
|
||||
continue
|
||||
|
||||
# 计算价格到边界的距离(价格应在通道内,所以距离 >= 0)
|
||||
dist_to_upper = max(0.0, upper_line - high[i])
|
||||
dist_to_lower = max(0.0, low[i] - lower_line)
|
||||
|
||||
# 当日空白比例 = (到上沿距离 + 到下沿距离) / 通道宽度
|
||||
blank_ratio = (dist_to_upper + dist_to_lower) / channel_width
|
||||
|
||||
# 当日利用率 = 1 - 空白比例,限制在 [0, 1]
|
||||
day_utilization = max(0.0, min(1.0, 1.0 - blank_ratio))
|
||||
|
||||
total_utilization += day_utilization
|
||||
valid_days += 1
|
||||
|
||||
if valid_days == 0:
|
||||
return 0.0
|
||||
|
||||
return total_utilization / valid_days
|
||||
|
||||
|
||||
def calc_breakout_strength(
|
||||
close: float,
|
||||
upper_line: float,
|
||||
@ -818,7 +885,8 @@ def calc_breakout_strength(
|
||||
volume_ratio: float,
|
||||
width_ratio: float,
|
||||
fitting_adherence: float,
|
||||
) -> Tuple[float, float, float, float, float, float]:
|
||||
boundary_utilization: float,
|
||||
) -> Tuple[float, float, float, float, float, float, float, float]:
|
||||
"""
|
||||
计算形态强度分 (0~1)
|
||||
|
||||
@ -827,9 +895,13 @@ def calc_breakout_strength(
|
||||
|
||||
使用加权求和,各分量权重:
|
||||
- 突破幅度分 (50%): tanh 非线性归一化,3%突破≈0.42,5%突破≈0.64,10%突破≈0.91
|
||||
- 收敛分 (20%): 1 - width_ratio,收敛越强分数越高
|
||||
- 成交量分 (15%): 放量程度,2倍放量=满分
|
||||
- 拟合贴合度 (15%): 枢轴点到拟合线的贴合程度,形态纯度
|
||||
- 收敛分 (15%): 1 - width_ratio,收敛越强分数越高
|
||||
- 成交量分 (10%): 放量程度,2倍放量=满分
|
||||
- 拟合贴合度 (10%): 枢轴点到拟合线的贴合程度,形态纯度
|
||||
- 边界利用率 (15%): 价格走势对通道空间的利用程度
|
||||
|
||||
额外惩罚项:
|
||||
- 当边界利用率过低时,对总强度进行空白惩罚,避免“通道很宽但价格很空”的误判
|
||||
|
||||
突破幅度分布参考(使用 tanh(pct * 15)):
|
||||
- 1% 突破 → 0.15
|
||||
@ -846,19 +918,23 @@ def calc_breakout_strength(
|
||||
volume_ratio: 成交量相对均值的倍数
|
||||
width_ratio: 末端宽度/起始宽度
|
||||
fitting_adherence: 拟合贴合度分数 (0~1)
|
||||
boundary_utilization: 边界利用率分数 (0~1)
|
||||
|
||||
Returns:
|
||||
(strength_up, strength_down, price_score_up, price_score_down, convergence_score, vol_score, fitting_score)
|
||||
(strength_up, strength_down, price_score_up, price_score_down,
|
||||
convergence_score, vol_score, fitting_score, boundary_util_score)
|
||||
返回总强度和各分量分数,用于可视化和分析
|
||||
"""
|
||||
import math
|
||||
|
||||
# 权重配置
|
||||
# 权重配置(调整后,总和 = 100%)
|
||||
W_PRICE = 0.50 # 突破幅度权重
|
||||
W_CONVERGENCE = 0.20 # 收敛度权重
|
||||
W_VOLUME = 0.15 # 成交量权重
|
||||
W_FITTING = 0.15 # 拟合贴合度权重
|
||||
W_CONVERGENCE = 0.15 # 收敛度权重(原 20%,降低)
|
||||
W_VOLUME = 0.10 # 成交量权重(原 15%,降低)
|
||||
W_FITTING = 0.10 # 拟合贴合度权重(原 15%,降低)
|
||||
W_UTILIZATION = 0.15 # 边界利用率权重(新增)
|
||||
TANH_SCALE = 15.0 # tanh 缩放因子
|
||||
UTILIZATION_FLOOR = 0.20 # 边界利用率下限(用于空白惩罚)
|
||||
|
||||
# 1. 价格突破分数(tanh 非线性归一化)
|
||||
if upper_line > 0:
|
||||
@ -882,22 +958,36 @@ def calc_breakout_strength(
|
||||
# 4. 拟合贴合度分数(直接使用传入的分数)
|
||||
fitting_score = max(0.0, min(1.0, fitting_adherence))
|
||||
|
||||
# 5. 加权求和(计算综合强度分)
|
||||
# 5. 边界利用率分数(直接使用传入的分数)
|
||||
boundary_util_score = max(0.0, min(1.0, boundary_utilization))
|
||||
|
||||
# 6. 加权求和(计算综合强度分)
|
||||
# 不再要求必须突破,而是计算形态的综合质量分数
|
||||
strength_up = (
|
||||
W_PRICE * price_score_up +
|
||||
W_CONVERGENCE * convergence_score +
|
||||
W_VOLUME * vol_score +
|
||||
W_FITTING * fitting_score
|
||||
W_FITTING * fitting_score +
|
||||
W_UTILIZATION * boundary_util_score
|
||||
)
|
||||
|
||||
strength_down = (
|
||||
W_PRICE * price_score_down +
|
||||
W_CONVERGENCE * convergence_score +
|
||||
W_VOLUME * vol_score +
|
||||
W_FITTING * fitting_score
|
||||
W_FITTING * fitting_score +
|
||||
W_UTILIZATION * boundary_util_score
|
||||
)
|
||||
|
||||
# 7. 空白惩罚(边界利用率过低时降低总分)
|
||||
if UTILIZATION_FLOOR > 0:
|
||||
utilization_penalty = min(1.0, boundary_util_score / UTILIZATION_FLOOR)
|
||||
else:
|
||||
utilization_penalty = 1.0
|
||||
|
||||
strength_up *= utilization_penalty
|
||||
strength_down *= utilization_penalty
|
||||
|
||||
return (
|
||||
min(1.0, strength_up),
|
||||
min(1.0, strength_down),
|
||||
@ -905,7 +995,8 @@ def calc_breakout_strength(
|
||||
price_score_down,
|
||||
convergence_score,
|
||||
vol_score,
|
||||
fitting_score
|
||||
fitting_score,
|
||||
boundary_util_score
|
||||
)
|
||||
|
||||
|
||||
@ -1123,16 +1214,30 @@ def detect_converging_triangle(
|
||||
# 综合上下沿贴合度(取平均)
|
||||
fitting_adherence = (adherence_upper + adherence_lower) / 2.0
|
||||
|
||||
# 计算边界利用率(价格走势对三角形通道的利用程度)
|
||||
boundary_utilization = calc_boundary_utilization(
|
||||
high=high,
|
||||
low=low,
|
||||
upper_slope=a_u,
|
||||
upper_intercept=b_u,
|
||||
lower_slope=a_l,
|
||||
lower_intercept=b_l,
|
||||
start=start,
|
||||
end=end,
|
||||
)
|
||||
|
||||
# 计算突破强度(返回总强度和各分量分数)
|
||||
(strength_up, strength_down,
|
||||
price_score_up, price_score_down,
|
||||
convergence_score, vol_score, fitting_score) = calc_breakout_strength(
|
||||
convergence_score, vol_score, fitting_score,
|
||||
boundary_util_score) = calc_breakout_strength(
|
||||
close=close[end],
|
||||
upper_line=upper_end,
|
||||
lower_line=lower_end,
|
||||
volume_ratio=volume_ratio,
|
||||
width_ratio=width_ratio,
|
||||
fitting_adherence=fitting_adherence,
|
||||
boundary_utilization=boundary_utilization,
|
||||
)
|
||||
|
||||
return ConvergingTriangleResult(
|
||||
@ -1146,6 +1251,7 @@ def detect_converging_triangle(
|
||||
convergence_score=convergence_score,
|
||||
volume_score=vol_score,
|
||||
fitting_score=fitting_score,
|
||||
boundary_utilization=boundary_util_score,
|
||||
upper_slope=a_u,
|
||||
lower_slope=a_l,
|
||||
width_ratio=width_ratio,
|
||||
|
||||