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_quantile()`
|
||||||
- 锚点拟合: `fit_boundary_anchor()`
|
- 锚点拟合: `fit_boundary_anchor()`
|
||||||
- 分发函数: `fit_pivot_line_dispatch()`
|
- 分发函数: `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')),
|
'touchesUpper': int(row.get('touches_upper', '0')),
|
||||||
'touchesLower': int(row.get('touches_lower', '0')),
|
'touchesLower': int(row.get('touches_lower', '0')),
|
||||||
'volumeConfirmed': row.get('volume_confirmed', ''),
|
'volumeConfirmed': row.get('volume_confirmed', ''),
|
||||||
|
'boundaryUtilization': float(row.get('boundary_utilization', '0')),
|
||||||
'date': date,
|
'date': date,
|
||||||
'hasTriangle': True # 标记为有三角形形态
|
'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('\\', '')
|
clean_name = stock['name'].replace('*', '').replace('?', '').replace('"', '').replace('<', '').replace('>', '').replace('|', '').replace(':', '').replace('/', '').replace('\\', '')
|
||||||
stock['chartPath'] = f"charts/{date}_{stock_code}_{clean_name}.png"
|
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']:
|
if stock_code not in stocks_map or stocks_map[stock_code]['strength'] < stock['strength']:
|
||||||
stocks_map[stock_code] = stock
|
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,
|
'touchesUpper': 0,
|
||||||
'touchesLower': 0,
|
'touchesLower': 0,
|
||||||
'volumeConfirmed': '',
|
'volumeConfirmed': '',
|
||||||
|
'boundaryUtilization': 0.0,
|
||||||
'date': use_date,
|
'date': use_date,
|
||||||
'chartPath': f"charts/{use_date}_{code}_{clean_name}.png",
|
'chartPath': f"charts/{use_date}_{code}_{clean_name}.png",
|
||||||
|
'chartPathDetail': f"charts/{use_date}_{code}_{clean_name}_detail.png",
|
||||||
'hasTriangle': False # 标记为无三角形形态
|
'hasTriangle': False # 标记为无三角形形态
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -873,6 +877,33 @@ def generate_html(stocks: list, date: int, output_path: str):
|
|||||||
color: var(--accent-warm);
|
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 */
|
||||||
.filter-chips {
|
.filter-chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -1033,6 +1064,16 @@ def generate_html(stocks: list, date: int, output_path: str):
|
|||||||
background: var(--accent-cool);
|
background: var(--accent-cool);
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
@ -1062,6 +1103,9 @@ def generate_html(stocks: list, date: int, output_path: str):
|
|||||||
</select>
|
</select>
|
||||||
<button class="sort-toggle active" id="sortOrder" title="切换排序顺序">↓</button>
|
<button class="sort-toggle active" id="sortOrder" title="切换排序顺序">↓</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="detail-toggle" id="detailToggle" title="切换详细图">
|
||||||
|
详细图: 关
|
||||||
|
</button>
|
||||||
<button class="reset-btn" id="resetBtn">
|
<button class="reset-btn" id="resetBtn">
|
||||||
<span>↺</span> 重置筛选
|
<span>↺</span> 重置筛选
|
||||||
</button>
|
</button>
|
||||||
@ -1136,13 +1180,14 @@ def generate_html(stocks: list, date: int, output_path: str):
|
|||||||
<div id="imageModal" class="modal">
|
<div id="imageModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<button class="close-modal" onclick="closeModal()">×</button>
|
<button class="close-modal" onclick="closeModal()">×</button>
|
||||||
<img id="modalImage" src="" alt="股票图表">
|
<img id="modalImage" src="" alt="股票图表" onerror="handleModalImageError(this)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// 内嵌数据
|
// 内嵌数据
|
||||||
const allStocks = STOCK_DATA;
|
const allStocks = STOCK_DATA;
|
||||||
let currentThreshold = 0;
|
let currentThreshold = 0;
|
||||||
|
let showDetailCharts = false;
|
||||||
|
|
||||||
// 筛选和排序状态
|
// 筛选和排序状态
|
||||||
let filters = {
|
let filters = {
|
||||||
@ -1209,6 +1254,15 @@ def generate_html(stocks: list, date: int, output_path: str):
|
|||||||
filterAndDisplayStocks();
|
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');
|
const directionFilter = document.getElementById('directionFilter');
|
||||||
directionFilter.addEventListener('click', function(e) {
|
directionFilter.addEventListener('click', function(e) {
|
||||||
@ -1350,9 +1404,11 @@ def generate_html(stocks: list, date: int, output_path: str):
|
|||||||
stock.direction === 'down' ? 'direction-down' : '';
|
stock.direction === 'down' ? 'direction-down' : '';
|
||||||
const volumeText = stock.volumeConfirmed === 'True' ? '✓' :
|
const volumeText = stock.volumeConfirmed === 'True' ? '✓' :
|
||||||
stock.volumeConfirmed === 'False' ? '✗' : '—';
|
stock.volumeConfirmed === 'False' ? '✗' : '—';
|
||||||
|
const chartPath = showDetailCharts ? stock.chartPathDetail : stock.chartPath;
|
||||||
|
const fallbackPath = showDetailCharts ? stock.chartPath : stock.chartPathDetail;
|
||||||
|
|
||||||
return `
|
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="card-header">
|
||||||
<div class="stock-identity">
|
<div class="stock-identity">
|
||||||
<div class="stock-name">${stock.name}</div>
|
<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-label">放量确认</span>
|
||||||
<span class="metric-value">${volumeText}</span>
|
<span class="metric-value">${volumeText}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="metric-label">边界利用率</span>
|
||||||
|
<span class="metric-value">${(stock.boundaryUtilization || 0).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<img src="${stock.chartPath}"
|
<img src="${chartPath}"
|
||||||
alt="${stock.name}"
|
alt="${stock.name}"
|
||||||
class="stock-chart"
|
class="stock-chart"
|
||||||
|
data-fallback-src="${fallbackPath}"
|
||||||
onerror="handleImageError(this)">
|
onerror="handleImageError(this)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1394,6 +1455,12 @@ def generate_html(stocks: list, date: int, output_path: str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleImageError(img) {
|
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;
|
if (img.dataset.errorHandled) return;
|
||||||
img.dataset.errorHandled = 'true';
|
img.dataset.errorHandled = 'true';
|
||||||
img.style.display = 'none';
|
img.style.display = 'none';
|
||||||
@ -1403,9 +1470,14 @@ def generate_html(stocks: list, date: int, output_path: str):
|
|||||||
img.parentElement.appendChild(noChart);
|
img.parentElement.appendChild(noChart);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openModal(imagePath) {
|
function openModal(defaultPath, detailPath) {
|
||||||
event.stopPropagation();
|
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');
|
document.getElementById('imageModal').classList.add('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -285,6 +285,30 @@ def plot_triangle(
|
|||||||
# 详细模式:显示拟合点(仅在 show_details=True 且有三角形时)
|
# 详细模式:显示拟合点(仅在 show_details=True 且有三角形时)
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
if show_details and has_triangle and has_enough_data:
|
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:
|
if len(selected_ph_display) >= 2:
|
||||||
ax1.scatter(
|
ax1.scatter(
|
||||||
@ -324,6 +348,14 @@ def plot_triangle(
|
|||||||
strength = max(result.breakout_strength_up, result.breakout_strength_down)
|
strength = max(result.breakout_strength_up, result.breakout_strength_down)
|
||||||
price_score = max(result.price_score_up, result.price_score_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(
|
ax1.set_title(
|
||||||
f"{stock_code} {stock_name} - 收敛三角形 (检测窗口: {detect_dates[0]} ~ {detect_dates[-1]})\n"
|
f"{stock_code} {stock_name} - 收敛三角形 (检测窗口: {detect_dates[0]} ~ {detect_dates[-1]})\n"
|
||||||
f"显示范围: {display_dates[0]} ~ {display_dates[-1]} ({len(display_dates)}个交易日) "
|
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"枢轴点: 高{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"放量确认: {'是' if result.volume_confirmed else '否' if result.volume_confirmed is False else '-'}\n"
|
||||||
f"强度分: {strength:.3f} "
|
f"强度分: {strength:.3f} "
|
||||||
f"(价格: {price_score:.3f}×50% + 收敛: {result.convergence_score:.3f}×20% + "
|
f"(价格: {price_score:.3f}×50% + 收敛: {result.convergence_score:.3f}×15% + "
|
||||||
f"成交量: {result.volume_score:.3f}×15% + 拟合贴合度: {result.fitting_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
|
fontsize=11, pad=10
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -396,6 +429,11 @@ def main() -> None:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="为所有108只股票生成图表(包括不满足收敛三角形条件的)",
|
help="为所有108只股票生成图表(包括不满足收敛三角形条件的)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--clear",
|
||||||
|
action="store_true",
|
||||||
|
help="清空当前模式的旧图片(默认不清空)",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# 确定是否显示详细信息(命令行参数优先)
|
# 确定是否显示详细信息(命令行参数优先)
|
||||||
@ -467,22 +505,24 @@ def main() -> None:
|
|||||||
print("当日无满足条件的股票")
|
print("当日无满足条件的股票")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 4. 创建输出目录并清空对应模式的旧图片
|
# 4. 创建输出目录,并按需清空对应模式的旧图片
|
||||||
os.makedirs(args.output_dir, exist_ok=True)
|
os.makedirs(args.output_dir, exist_ok=True)
|
||||||
|
|
||||||
# 只清空当前模式的图片(简洁模式或详细模式)
|
if args.clear:
|
||||||
print(f"\n[4] 清空当前模式的旧图片...")
|
# 只清空当前模式的图片(简洁模式或详细模式)
|
||||||
suffix = "_detail.png" if show_details else ".png"
|
print(f"\n[4] 清空当前模式的旧图片...")
|
||||||
# 找出当前模式的文件:简洁模式是不含_detail的.png,详细模式是_detail.png
|
# 找出当前模式的文件:简洁模式是不含_detail的.png,详细模式是_detail.png
|
||||||
if show_details:
|
if show_details:
|
||||||
old_files = [f for f in os.listdir(args.output_dir) if f.endswith('_detail.png')]
|
old_files = [f for f in os.listdir(args.output_dir) if f.endswith('_detail.png')]
|
||||||
|
else:
|
||||||
|
old_files = [f for f in os.listdir(args.output_dir)
|
||||||
|
if f.endswith('.png') and not f.endswith('_detail.png')]
|
||||||
|
|
||||||
|
for f in old_files:
|
||||||
|
os.remove(os.path.join(args.output_dir, f))
|
||||||
|
print(f" 已删除 {len(old_files)} 个旧图片 ({'详细模式' if show_details else '简洁模式'})")
|
||||||
else:
|
else:
|
||||||
old_files = [f for f in os.listdir(args.output_dir)
|
print(f"\n[4] 跳过清空旧图片(使用 --clear 可手动清空)")
|
||||||
if f.endswith('.png') and not f.endswith('_detail.png')]
|
|
||||||
|
|
||||||
for f in old_files:
|
|
||||||
os.remove(os.path.join(args.output_dir, f))
|
|
||||||
print(f" 已删除 {len(old_files)} 个旧图片 ({'详细模式' if show_details else '简洁模式'})")
|
|
||||||
|
|
||||||
# 5. 检测参数(从统一配置导入)
|
# 5. 检测参数(从统一配置导入)
|
||||||
params = DETECTION_PARAMS
|
params = DETECTION_PARAMS
|
||||||
|
|||||||
@ -80,6 +80,7 @@ class ConvergingTriangleResult:
|
|||||||
convergence_score: float = 0.0 # 收敛分数
|
convergence_score: float = 0.0 # 收敛分数
|
||||||
volume_score: float = 0.0 # 成交量分数
|
volume_score: float = 0.0 # 成交量分数
|
||||||
fitting_score: float = 0.0 # 拟合贴合度分数
|
fitting_score: float = 0.0 # 拟合贴合度分数
|
||||||
|
boundary_utilization: float = 0.0 # 边界利用率分数
|
||||||
|
|
||||||
# 几何属性
|
# 几何属性
|
||||||
upper_slope: 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))
|
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(
|
def calc_breakout_strength(
|
||||||
close: float,
|
close: float,
|
||||||
upper_line: float,
|
upper_line: float,
|
||||||
@ -818,7 +885,8 @@ def calc_breakout_strength(
|
|||||||
volume_ratio: float,
|
volume_ratio: float,
|
||||||
width_ratio: float,
|
width_ratio: float,
|
||||||
fitting_adherence: 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)
|
计算形态强度分 (0~1)
|
||||||
|
|
||||||
@ -827,9 +895,13 @@ def calc_breakout_strength(
|
|||||||
|
|
||||||
使用加权求和,各分量权重:
|
使用加权求和,各分量权重:
|
||||||
- 突破幅度分 (50%): tanh 非线性归一化,3%突破≈0.42,5%突破≈0.64,10%突破≈0.91
|
- 突破幅度分 (50%): tanh 非线性归一化,3%突破≈0.42,5%突破≈0.64,10%突破≈0.91
|
||||||
- 收敛分 (20%): 1 - width_ratio,收敛越强分数越高
|
- 收敛分 (15%): 1 - width_ratio,收敛越强分数越高
|
||||||
- 成交量分 (15%): 放量程度,2倍放量=满分
|
- 成交量分 (10%): 放量程度,2倍放量=满分
|
||||||
- 拟合贴合度 (15%): 枢轴点到拟合线的贴合程度,形态纯度
|
- 拟合贴合度 (10%): 枢轴点到拟合线的贴合程度,形态纯度
|
||||||
|
- 边界利用率 (15%): 价格走势对通道空间的利用程度
|
||||||
|
|
||||||
|
额外惩罚项:
|
||||||
|
- 当边界利用率过低时,对总强度进行空白惩罚,避免“通道很宽但价格很空”的误判
|
||||||
|
|
||||||
突破幅度分布参考(使用 tanh(pct * 15)):
|
突破幅度分布参考(使用 tanh(pct * 15)):
|
||||||
- 1% 突破 → 0.15
|
- 1% 突破 → 0.15
|
||||||
@ -846,19 +918,23 @@ def calc_breakout_strength(
|
|||||||
volume_ratio: 成交量相对均值的倍数
|
volume_ratio: 成交量相对均值的倍数
|
||||||
width_ratio: 末端宽度/起始宽度
|
width_ratio: 末端宽度/起始宽度
|
||||||
fitting_adherence: 拟合贴合度分数 (0~1)
|
fitting_adherence: 拟合贴合度分数 (0~1)
|
||||||
|
boundary_utilization: 边界利用率分数 (0~1)
|
||||||
|
|
||||||
Returns:
|
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
|
import math
|
||||||
|
|
||||||
# 权重配置
|
# 权重配置(调整后,总和 = 100%)
|
||||||
W_PRICE = 0.50 # 突破幅度权重
|
W_PRICE = 0.50 # 突破幅度权重
|
||||||
W_CONVERGENCE = 0.20 # 收敛度权重
|
W_CONVERGENCE = 0.15 # 收敛度权重(原 20%,降低)
|
||||||
W_VOLUME = 0.15 # 成交量权重
|
W_VOLUME = 0.10 # 成交量权重(原 15%,降低)
|
||||||
W_FITTING = 0.15 # 拟合贴合度权重
|
W_FITTING = 0.10 # 拟合贴合度权重(原 15%,降低)
|
||||||
TANH_SCALE = 15.0 # tanh 缩放因子
|
W_UTILIZATION = 0.15 # 边界利用率权重(新增)
|
||||||
|
TANH_SCALE = 15.0 # tanh 缩放因子
|
||||||
|
UTILIZATION_FLOOR = 0.20 # 边界利用率下限(用于空白惩罚)
|
||||||
|
|
||||||
# 1. 价格突破分数(tanh 非线性归一化)
|
# 1. 价格突破分数(tanh 非线性归一化)
|
||||||
if upper_line > 0:
|
if upper_line > 0:
|
||||||
@ -882,22 +958,36 @@ def calc_breakout_strength(
|
|||||||
# 4. 拟合贴合度分数(直接使用传入的分数)
|
# 4. 拟合贴合度分数(直接使用传入的分数)
|
||||||
fitting_score = max(0.0, min(1.0, fitting_adherence))
|
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 = (
|
strength_up = (
|
||||||
W_PRICE * price_score_up +
|
W_PRICE * price_score_up +
|
||||||
W_CONVERGENCE * convergence_score +
|
W_CONVERGENCE * convergence_score +
|
||||||
W_VOLUME * vol_score +
|
W_VOLUME * vol_score +
|
||||||
W_FITTING * fitting_score
|
W_FITTING * fitting_score +
|
||||||
|
W_UTILIZATION * boundary_util_score
|
||||||
)
|
)
|
||||||
|
|
||||||
strength_down = (
|
strength_down = (
|
||||||
W_PRICE * price_score_down +
|
W_PRICE * price_score_down +
|
||||||
W_CONVERGENCE * convergence_score +
|
W_CONVERGENCE * convergence_score +
|
||||||
W_VOLUME * vol_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 (
|
return (
|
||||||
min(1.0, strength_up),
|
min(1.0, strength_up),
|
||||||
min(1.0, strength_down),
|
min(1.0, strength_down),
|
||||||
@ -905,7 +995,8 @@ def calc_breakout_strength(
|
|||||||
price_score_down,
|
price_score_down,
|
||||||
convergence_score,
|
convergence_score,
|
||||||
vol_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
|
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,
|
(strength_up, strength_down,
|
||||||
price_score_up, price_score_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],
|
close=close[end],
|
||||||
upper_line=upper_end,
|
upper_line=upper_end,
|
||||||
lower_line=lower_end,
|
lower_line=lower_end,
|
||||||
volume_ratio=volume_ratio,
|
volume_ratio=volume_ratio,
|
||||||
width_ratio=width_ratio,
|
width_ratio=width_ratio,
|
||||||
fitting_adherence=fitting_adherence,
|
fitting_adherence=fitting_adherence,
|
||||||
|
boundary_utilization=boundary_utilization,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ConvergingTriangleResult(
|
return ConvergingTriangleResult(
|
||||||
@ -1146,6 +1251,7 @@ def detect_converging_triangle(
|
|||||||
convergence_score=convergence_score,
|
convergence_score=convergence_score,
|
||||||
volume_score=vol_score,
|
volume_score=vol_score,
|
||||||
fitting_score=fitting_score,
|
fitting_score=fitting_score,
|
||||||
|
boundary_utilization=boundary_util_score,
|
||||||
upper_slope=a_u,
|
upper_slope=a_u,
|
||||||
lower_slope=a_l,
|
lower_slope=a_l,
|
||||||
width_ratio=width_ratio,
|
width_ratio=width_ratio,
|
||||||
|
|||||||