// ==================== 辅助函数 ==================== function getSubColorClass(subText) { if (subText.includes('✅')) return 'row-sub-green'; if (subText.includes('❌')) return 'row-sub-red'; return ''; } function getGroupColorClass(groupType, isGo) { if (groupType === 'macro' || groupType === 'cooldown' || groupType === 'call') { return isGo ? 'row-group-green' : 'row-group-red'; } if (groupType === 'l1' || groupType === 'l2') { return isGo ? 'row-group-green' : ''; } if (groupType === 'prelim') { return isGo ? 'row-group-green' : ''; } return ''; } function createSpacer() { const spacer = document.createElement('tr'); spacer.className = 'board-spacer-row'; return spacer; } function formatValueHtml(html) { return html .replace(/禁止:<\/strong>/g, '禁止:') .replace(/必需:<\/strong>/g, '必需:') .replace(/当前:<\/strong>/g, '当前:'); } function addStageAndGroupRow(tbody, stageHtml, groupResultHtml, subText, valueHtml, groupRowSpan, stageRowSpan, groupType = '', isGroupGo = false) { const tr = document.createElement('tr'); if (groupType) tr.classList.add('board-row-group-' + groupType); const tdStage = document.createElement('td'); tdStage.innerHTML = stageHtml; tdStage.className = 'board-stage-cell'; if (groupType) tdStage.classList.add('group-' + groupType); tdStage.rowSpan = stageRowSpan; tr.appendChild(tdStage); const tdGroup = document.createElement('td'); tdGroup.innerHTML = groupResultHtml; tdGroup.className = 'board-group-cell'; if (groupType) tdGroup.classList.add('group-' + groupType); const groupColorClass = getGroupColorClass(groupType, isGroupGo); if (groupColorClass) tdGroup.classList.add(groupColorClass); tdGroup.rowSpan = groupRowSpan; tr.appendChild(tdGroup); const tdSub = document.createElement('td'); tdSub.textContent = subText; tdSub.className = 'board-sub-cell'; const tdVal = document.createElement('td'); tdVal.innerHTML = formatValueHtml(valueHtml); tdVal.className = 'board-value-cell'; applySubColorClass(tdSub, tdVal, subText, groupType); tr.appendChild(tdSub); tr.appendChild(tdVal); tbody.appendChild(tr); } function addRowOnlySubValue(tbody, subText, valueHtml, groupType = '') { const tr = document.createElement('tr'); if (groupType) tr.classList.add('board-row-group-' + groupType); const tdSub = document.createElement('td'); tdSub.textContent = subText; tdSub.className = 'board-sub-cell'; const tdVal = document.createElement('td'); let escapedHtml = valueHtml.replace(/<(?!\/?(strong|br)\b)/gi, '<'); tdVal.innerHTML = formatValueHtml(escapedHtml); tdVal.className = 'board-value-cell'; applySubColorClass(tdSub, tdVal, subText, groupType); tr.appendChild(tdSub); tr.appendChild(tdVal); tbody.appendChild(tr); } function addSingleRow(tbody, stageHtml, groupResult, subText, valueHtml, groupType = '', isGroupGo = false) { const tr = document.createElement('tr'); if (groupType) tr.classList.add('board-row-group-' + groupType); const tdStage = document.createElement('td'); tdStage.innerHTML = stageHtml; tdStage.className = 'board-stage-cell'; if (groupType) tdStage.classList.add('group-' + groupType); tr.appendChild(tdStage); const tdGroup = document.createElement('td'); tdGroup.textContent = groupResult; tdGroup.className = 'board-group-cell'; if (groupType) tdGroup.classList.add('group-' + groupType); const groupColorClass = getGroupColorClass(groupType, isGroupGo); if (groupColorClass) tdGroup.classList.add(groupColorClass); tr.appendChild(tdGroup); const tdSub = document.createElement('td'); tdSub.textContent = subText; tdSub.className = 'board-sub-cell'; const tdVal = document.createElement('td'); tdVal.innerHTML = formatValueHtml(valueHtml); tdVal.className = 'board-value-cell'; applySubColorClass(tdSub, tdVal, subText, groupType); tr.appendChild(tdSub); tr.appendChild(tdVal); tbody.appendChild(tr); } function applySubColorClass(tdSub, tdVal, subText, groupType) { const isPass = subText.includes('✅'); const isFail = subText.includes('❌'); // 初步筛选:只有通过时添加绿色,失败时不添加红色 if (groupType === 'prelim') { if (isPass) { tdSub.classList.add('row-sub-green'); tdVal.classList.add('row-sub-green'); } return; } // 其他阶段(macro, cooldown, call, l1, l2) if (isPass) { tdSub.classList.add('row-sub-green'); tdVal.classList.add('row-sub-green'); } else if (isFail) { tdSub.classList.add('row-sub-red'); tdVal.classList.add('row-sub-red'); } }// ==================== 渲染函数 ==================== function renderFullData(apiResponse, source) { if (!apiResponse) { const div = document.createElement('div'); div.className = 'no-data'; div.textContent = '【200条数据】暂无数据,请先点击MEXC或Binance按钮获取K线数据。'; return div; } const symbols = sortSymbols(Object.keys(apiResponse)); const cardTemplate = document.getElementById('template-symbol-card'); const periodTemplate = document.getElementById('template-period-table'); if (!cardTemplate || !periodTemplate) { const errorDiv = document.createElement('div'); errorDiv.className = 'no-data'; errorDiv.textContent = '❌ 渲染模板缺失,请检查 HTML 中的 template 元素'; return errorDiv; } const fragment = document.createDocumentFragment(); for (let sym of symbols) { const intervals = apiResponse[sym]; if (!intervals) continue; const card = cardTemplate.content.cloneNode(true); card.querySelector('.symbol-name').textContent = sym; const sourceName = source === 'mexc' ? 'MEXC REST API' : 'Binance Public API'; card.querySelector('.symbol-badge').textContent = sourceName; const periodsContainer = card.querySelector('.periods-container'); for (let period of ['5m','1h','4h','15m']) { const kdata = intervals[period]; if (!kdata || kdata.error) { const errMsg = kdata?.error || '无数据'; const errorDiv = document.createElement('div'); errorDiv.className = 'error-msg'; errorDiv.textContent = `⚠️ ${period} 错误: ${errMsg}`; periodsContainer.appendChild(errorDiv); continue; } if (!Array.isArray(kdata) || kdata.length===0) { const emptyDiv = document.createElement('div'); emptyDiv.className = 'error-msg'; emptyDiv.textContent = `📭 ${period} 空数据`; periodsContainer.appendChild(emptyDiv); continue; } const sorted = [...kdata].reverse(); const periodClone = periodTemplate.content.cloneNode(true); const tbody = periodClone.querySelector('tbody'); // 在填充数据之前或之后,修改第一个 th 的内容 const firstTh = periodClone.querySelector('thead tr th:first-child'); if (firstTh) { firstTh.textContent = `开盘时间 【周期:${period}】`; } for (let k of sorted) { if (!Array.isArray(k) || k.length<8) continue; const row = document.createElement('tr'); row.innerHTML = ` ${formatTimestamp(k[0])} ${formatPrice(k[1])} ${formatPrice(k[2])} ${formatPrice(k[3])} ${formatPrice(k[4])} ${formatPrice(k[5])} ${formatTimestamp(k[6])} ${formatPrice(k[7])} `; tbody.appendChild(row); } const footer = periodClone.querySelector('.period-footer'); footer.textContent = `📍 共 ${sorted.length} 条K线 (最新在上) | 周期: ${period} | limit=200`; periodsContainer.appendChild(periodClone); } fragment.appendChild(card); } const container = document.createElement('div'); container.appendChild(fragment); return container; } function renderCurrentData(apiResponse, source) { if (!apiResponse) { const div = document.createElement('div'); div.className = 'no-data'; div.textContent = '【当前数据】暂无数据,请先点击MEXC或Binance按钮获取K线数据。'; return div; } const symbols = sortSymbols(Object.keys(apiResponse)); const rowsData = []; for (let sym of symbols) { let intervals = apiResponse[sym]; if (!intervals) continue; const period = '5m'; let kdata = intervals[period]; if (!kdata || kdata.error || !Array.isArray(kdata) || kdata.length===0) continue; const latest = kdata[kdata.length-1]; if (!latest || latest.length<8) continue; rowsData.push({ symbol: sym, openTime: latest[0], open: latest[1], high: latest[2], low: latest[3], close: latest[4], volume: latest[5], closeTime: latest[6], quoteVolume: latest[7] }); } if (rowsData.length === 0) { const div = document.createElement('div'); div.className = 'no-data'; div.textContent = '没有有效的5m最新数据'; return div; } const sourceName = source === 'mexc' ? 'MEXC REST API' : 'Binance Public API'; const template = document.getElementById('template-current-table'); if (!template) { const errorDiv = document.createElement('div'); errorDiv.className = 'no-data'; errorDiv.textContent = '❌ 当前数据模板缺失'; return errorDiv; } const clone = template.content.cloneNode(true); clone.querySelector('.table-caption').textContent = `${sourceName} · 5m (最新1条)`; const tbody = clone.querySelector('tbody'); for (let row of rowsData) { const tr = document.createElement('tr'); tr.innerHTML = ` ${formatTimestamp(row.openTime)} ${row.symbol} ${formatPrice(row.open)} ${formatPrice(row.high)} ${formatPrice(row.low)} ${formatPrice(row.close)} ${formatPrice(row.volume)} ${formatTimestamp(row.closeTime)} ${formatPrice(row.quoteVolume)} `; tbody.appendChild(tr); } const container = document.createElement('div'); container.appendChild(clone); const copyBtn = container.querySelector('.copy-table-btn'); if (copyBtn) copyBtn.id = 'copyCurrentTableBtn'; return container; }function renderParameterList(params, state) { if (!params) { const errorDiv = document.createElement('div'); errorDiv.className = 'no-data'; errorDiv.textContent = '❌ 参数数据为空,无法渲染'; return errorDiv; } const template = document.getElementById('template-param-container'); if (!template) { const errorDiv = document.createElement('div'); errorDiv.className = 'no-data'; errorDiv.textContent = '❌ 参数模板 (template-param-container) 不存在,请检查 HTML'; return errorDiv; } const clone = template.content.cloneNode(true); const b1Grid = clone.querySelector('[data-section="B1"] .param-grid'); const b2Grid = clone.querySelector('[data-section="B2"] .param-grid'); const b3Grid = clone.querySelector('[data-section="B3"] .param-grid'); if (!b1Grid || !b2Grid || !b3Grid) { const errorDiv = document.createElement('div'); errorDiv.className = 'no-data'; errorDiv.textContent = '❌ 参数模板结构错误,缺少 .param-grid 容器'; return errorDiv; } b1Grid.innerHTML = ''; b2Grid.innerHTML = ''; b3Grid.innerHTML = ''; // 移除了内部的 formatValue 函数,改用全局 formatParamValue (定义在 04.02_Utils.php) const macroB1Keys = ['MACRO_ADX_THRESHOLD', 'MACRO_ADX_LEN']; const allB1Keys = [ 'DROP_THRESHOLD_PCT', 'MACRO_ADX_THRESHOLD', 'MACRO_ADX_LEN', 'L1_ADX_THRESHOLD', 'L1_ADX_LEN', 'RSI_PRELIM_THRESHOLD', 'RSI_PRELIM_LEN', 'RSI_L2_THRESHOLD', 'RSI_L2_LEN', 'BIG_GREEN_MULTIPLIER', 'BIG_GREEN_COOLDOWN_SEC', 'FLASH_MOVE_THRESHOLD_PCT', 'MACD_FAST', 'MACD_SLOW', 'MACD_SIGNAL', 'ATR_LEN', 'ATR_SMA_LEN', 'RECENT_LOW_PERIOD', 'PIVOT_LEN', 'TURNOVER_THRESHOLDS' ]; const macroDiv = document.createElement('div'); macroDiv.innerHTML = '[MACRO] 宏观阈值
'; const currentDiv = document.createElement('div'); currentDiv.innerHTML = '[CURRENT] 交易币种阈值
'; b1Grid.appendChild(macroDiv); b1Grid.appendChild(currentDiv); for (let key of allB1Keys) { if (params[key] !== undefined) { const itemDiv = document.createElement('div'); // 使用全局 formatParamValue 进行格式化 itemDiv.innerHTML = `${key}: ${formatParamValue(key, params[key])}`; if (macroB1Keys.includes(key)) macroDiv.appendChild(itemDiv); else currentDiv.appendChild(itemDiv); } } const macroDiv2 = document.createElement('div'); macroDiv2.innerHTML = '[MACRO] 宏观共用参数
'; const currentDiv2 = document.createElement('div'); currentDiv2.innerHTML = '[CURRENT] 交易币种列表
'; b2Grid.appendChild(macroDiv2); b2Grid.appendChild(currentDiv2); if (params['MACRO_BASE'] !== undefined) { const itemDiv = document.createElement('div'); itemDiv.innerHTML = `MACRO_BASE: ${params['MACRO_BASE']}`; macroDiv2.appendChild(itemDiv); } if (params['EMA_PERIODS'] !== undefined) { const itemDiv = document.createElement('div'); // 也可以使用 formatParamValue,但为了与旧格式一致,保持原样(join) itemDiv.innerHTML = `EMA_PERIODS: ${params['EMA_PERIODS'].join(', ')}`; macroDiv2.appendChild(itemDiv); } // 根据当前激活的数据源显示对应的币种列表 let activeCoins = []; if (state && state.activeSource) { if (state.activeSource === 'mexc' && params['MEXC_COINS']) { activeCoins = params['MEXC_COINS']; currentDiv2.innerHTML = '[CURRENT] 交易币种列表 (MEXC 4币种)
'; } else if (state.activeSource === 'binance' && params['BINANCE_COINS']) { activeCoins = params['BINANCE_COINS']; currentDiv2.innerHTML = '[CURRENT] 交易币种列表 (Binance 7币种)
'; } else { // 默认显示 MEXC 列表(如果存在) if (params['MEXC_COINS']) activeCoins = params['MEXC_COINS']; } } else { // 未选择数据源时,默认显示 MEXC 列表 if (params['MEXC_COINS']) activeCoins = params['MEXC_COINS']; } if (activeCoins.length > 0) { const itemDiv = document.createElement('div'); itemDiv.innerHTML = `当前币种: ${activeCoins.join(', ')}`; currentDiv2.appendChild(itemDiv); } else { const itemDiv = document.createElement('div'); itemDiv.innerHTML = `当前币种: 未选择数据源,请先点击 MEXC 或 Binance 按钮`; currentDiv2.appendChild(itemDiv); } const scoringKeys = ['L1_PASS_SCORE', 'L2_PASS_SCORE', 'L1_MAX_SCORE', 'L2_MAX_SCORE']; const scoringDiv = document.createElement('div'); scoringDiv.innerHTML = '[CURRENT] 评分阈值
'; b3Grid.appendChild(scoringDiv); for (let key of scoringKeys) { if (params[key] !== undefined) { const itemDiv = document.createElement('div'); itemDiv.innerHTML = `${key}: ${params[key]}`; scoringDiv.appendChild(itemDiv); } } return clone; }// ==================== 动态指标卡片渲染 ==================== function renderDynamicIndicatorCard(symbol, data) { if (!data || data.error) { const errDiv = document.createElement('div'); errDiv.className = 'no-data error-margin-bottom'; errDiv.textContent = `⚠️ ${symbol} 指标加载失败: ${data?.error || '未知错误'}`; return errDiv; } // 检查字段定义是否已加载 if (!window.indicatorFields) { const errDiv = document.createElement('div'); errDiv.className = 'no-data error-margin-bottom'; errDiv.textContent = `⚠️ 指标字段定义未加载,请稍后刷新页面。`; return errDiv; } const { MACRO_ITEMS, CURRENT_ITEMS } = window.indicatorFields; const container = document.createElement('div'); container.className = 'dynamic-card'; const title = document.createElement('h3'); title.className = 'dynamic-card-title'; title.textContent = `📈 ${symbol} 实时指标 (基于最新K线)`; container.appendChild(title); const grid = document.createElement('div'); grid.className = 'dynamic-grid'; function addItem(key, label, unit, isBool) { let val = data[key]; if (val === undefined || val === null) return; let display; if (isBool) { display = val ? '是' : '否'; } else if (typeof val === 'number') { display = val.toFixed(2); if (unit) display += unit; } else { display = val; } const div = document.createElement('div'); div.innerHTML = `${label}: ${display}`; grid.appendChild(div); } for (let item of MACRO_ITEMS) addItem(item[0], item[1], item[2], false); for (let item of CURRENT_ITEMS) addItem(item[0], item[1], item[2], item[3] === 'bool'); container.appendChild(grid); return container; } function renderAllDynamicIndicators(dynamicDataMap) { if (!dynamicDataMap || Object.keys(dynamicDataMap).length === 0) { const div = document.createElement('div'); div.className = 'no-data'; div.textContent = '【实时指标】暂无数据,请先点击MEXC或Binance按钮获取K线数据。'; return div; } const container = document.createElement('div'); let hasValidData = false; for (let sym of Object.keys(dynamicDataMap)) { const data = dynamicDataMap[sym]; if (!data || data.error) continue; const card = renderDynamicIndicatorCard(sym, data); container.appendChild(card); hasValidData = true; } if (!hasValidData) { const div = document.createElement('div'); div.className = 'no-data'; div.textContent = '【实时指标】暂无数据,请先点击MEXC或Binance按钮获取K线数据。'; return div; } return container; }// ==================== 结果报告渲染 ==================== function renderResultReport(data, params = null) { if (!data || data.error) { const errDiv = document.createElement('div'); errDiv.className = 'no-data'; errDiv.textContent = '【结果数据】暂无数据,请先点击MEXC或Binance按钮获取K线数据。'; return errDiv; } // 必须提供完整参数对象 if (!params || typeof params !== 'object') { const errDiv = document.createElement('div'); errDiv.className = 'no-data error-message'; errDiv.textContent = '❌ 参数缺失,无法渲染报告。请刷新页面或重新获取数据。'; return errDiv; } // 解构所需阈值(直接使用,缺失时会导致 undefined,后续计算将显示异常) const { DROP_THRESHOLD_PCT, MACRO_ADX_THRESHOLD, L1_ADX_THRESHOLD, RSI_PRELIM_THRESHOLD, RSI_L2_THRESHOLD, L1_PASS_SCORE, L2_PASS_SCORE } = params; // 可选检查:如果任意必要参数缺失,返回错误(不影响已有逻辑) if (DROP_THRESHOLD_PCT === undefined || MACRO_ADX_THRESHOLD === undefined || L1_ADX_THRESHOLD === undefined || RSI_PRELIM_THRESHOLD === undefined || RSI_L2_THRESHOLD === undefined || L1_PASS_SCORE === undefined || L2_PASS_SCORE === undefined) { const errDiv = document.createElement('div'); errDiv.className = 'no-data error-message'; errDiv.textContent = '❌ 参数不完整,无法渲染报告。请检查参数加载是否成功。'; return errDiv; } const container = document.createElement('div'); container.className = 'result-report-container'; const headerBar = document.createElement('div'); headerBar.className = 'report-header-bar'; const copyBtn = document.createElement('button'); copyBtn.textContent = '📋 复制报告'; copyBtn.className = 'api-btn report-copy-btn'; copyBtn.onclick = function() { const textContent = container.querySelector('.result-report').innerText; navigator.clipboard.writeText(textContent).then(() => { const originalText = copyBtn.textContent; copyBtn.textContent = '✅ 已复制!'; setTimeout(() => { copyBtn.textContent = originalText; }, 1500); }).catch(err => { console.error('复制失败:', err); alert('复制失败,请手动复制'); }); }; headerBar.appendChild(copyBtn); container.appendChild(headerBar); const reportDiv = document.createElement('div'); reportDiv.className = 'result-report'; let html = ''; const symbols = sortSymbols(Object.keys(data.coins)); const rc = data.risk_control; const macroGo = data.macro_go; const macroAdx = data.macro_adx; // 全局宏观部分(使用动态阈值) function renderGlobalMacro() { let section = ''; section += "============================================================================\n"; section += "【全局宏观风控 - BTC】\n"; section += "============================================================================\n\n"; section += "阶段\n宏观风控 BTC\n2pm-2:30pm\n需满足全部条件\n\n"; section += "组结果\n" + (rc.go ? "GO ✅" : "NOTGO ❌") + "\nA1\n\n"; section += "子结果\t\t数值/条件\n"; section += `BL01 ${icon(!rc.bl01)}\t\t禁止:4H价格 > EMA200\n\t\t必需:4H价格 ≤ EMA200\n\t\t当前:4H价格 ${safeNum(rc.current?.price_4h)} | EMA200 ${safeNum(rc.current?.ema200_4h)}\n\n`; section += `BL03 ${icon(!rc.bl03)}\t\t禁止:1H MACD 0轴上死叉\n\t\t必需:MACD ≤0 且 无死叉\n\t\t当前:MACD ${safeNum(rc.current?.macd_line_1h)} | Signal ${safeNum(rc.current?.macd_signal_1h)}\n\n`; section += `BL02 ${icon(!rc.bl02)}\t\t禁止:出现 15m HL(Higher Low)\n\t\t当前:HL ${rc.bl02 ? '已形成' : '未形成'}\t\n\n`; section += "--------------------------------------------------------------------\n\n"; section += "阶段\n宏观定调 BTC\n2pm-2:30pm\n\n"; section += "组结果\n" + (macroGo ? "GO ✅" : "NOTGO ❌") + "\nB1\n\n"; section += "子结果\t\t数值/条件\n"; section += `ADX ${icon(macroGo)}\t\t必需:ADX > ${MACRO_ADX_THRESHOLD}\n\t\t当前:${safeNum(macroAdx, 1)}\n\n`; section += "--------------------------------------------------------------------\n\n"; return section; } function safeRender(fn, fallback = '') { try { const result = fn(); return (result !== undefined && result !== null) ? result : fallback; } catch (e) { console.error('渲染错误:', e); return fallback; } } function renderCooldown(symbol, coin) { if (!coin.cooldown) return ''; const cd = coin.cooldown; const remaining = cd.remaining_sec ?? 0; const bigGreenNow = cd.big_green_now ?? false; let section = ''; section += `阶段\n限时否决 ${symbol}\n(30分钟)\t\n\n`; section += "组结果\n禁空时间\t" + formatCooldown(remaining) + "\n\n"; section += "子结果\t\t数值/条件\n"; section += `BL05 ${icon(!bigGreenNow)}\t\t禁止:出现第一根大阳线\n\t\t当前:大阳线${bigGreenNow ? '已形成' : '未形成'}\n\n`; return section; } function renderPrelim(symbol, coin) { if (!coin.prelim) return ''; const p = coin.prelim; // 从 params 中获取成交额阈值(单位:USDT,需转换为 M) const turnoverThresholds = (typeof params !== 'undefined' && params.TURNOVER_THRESHOLDS) ? params.TURNOVER_THRESHOLDS : {}; let turnoverThresholdM = 8.0; // 默认 8M(与配置文件中 SOLUSDT 一致) if (turnoverThresholds[symbol] !== undefined) { turnoverThresholdM = turnoverThresholds[symbol] / 1e6; } let section = ''; section += `阶段\n初步筛选 ${symbol}\n3:30pm-4:30pm\n需满足全部条件\n\n`; section += "组结果\n" + (p.go ? "GO ✅" : "NOTGO ❌") + "\nC1\n\n"; section += "子结果\t\t数值/条件\n"; section += `跌幅 ${icon(p.drop_ok)}\t\t必需:跌 > ${DROP_THRESHOLD_PCT}%\n\t\t当前:跌 ${safeNum(p.drop_val)}%\n`; section += `成交额 ${icon(p.turnover_ok)}\t\t必需:成交额 > ${turnoverThresholdM}M USDT\n\t\t当前:${safeNum(p.turnover_val)}M\t\n`; section += `RSI ${icon(p.rsi_ok)}\t\t必需:RSI < ${RSI_PRELIM_THRESHOLD}\n\t\t当前:${safeNum(p.rsi_val)}\t\n`; return section; } function renderL1(symbol, coin) { if (!coin.l1) return ''; const l1 = coin.l1; const d = l1.details || {}; let section = ''; section += `阶段\nL1 趋势层指标 ${symbol}\n当前:${l1.total}\n合格:${L1_PASS_SCORE}\n总分:${l1.max_score}\n\n`; section += "组结果\n" + (l1.go ? "GO ✅" : "NOTGO ❌") + "\n" + l1.total + "/" + l1.max_score + "\n\n"; section += "子结果\t\t\t数值/条件\n"; section += `${icon(d.mtf_bool)} ${d.mtf}\t\t\tMTF Alignment\n\t\t\t必需:4H空头 且 1H空头\n\t\t\t当前:4H多头,1H多头\n`; section += `${icon(d.alignment_bool)} ${d.alignment}\t\t\t均线排列 \n\t\t\t必需:EMA20 < EMA50 < EMA100(空头排列)\n\t\t\t当前:${d.alignment_bool ? '空头排列' : '多头排列'} ${icon(d.alignment_bool)}\t\n`; section += `${icon(d.slope_bool)} ${d.slope}\t\t\t均线斜率\n\t\t\t必需:三条均线均向下\n\t\t\t当前:${d.slope_bool ? '三线皆下行' : '不满足'} ${icon(d.slope_bool)}\t\n`; section += `${icon(d.price_pos_bool)} ${d.price_pos}\t\t\t价格相对位置\n\t\t\t必需:收盘价 < 所有均线\n\t\t\t当前:${d.price_pos_bool ? '价格在所有EMA下方' : '价格在所有EMA上方'} ${icon(d.price_pos_bool)}\t\n`; section += `${icon(d.adx_bool)} ${d.adx}\t\t\tADX趋势强度\n\t\t\t必需:ADX > ${L1_ADX_THRESHOLD} 且 上升 且 价格下跌\n\t\t\t当前:ADX ${safeNum(d.adx_val)} > ${L1_ADX_THRESHOLD},${d.adx_rising ? '上升' : '下降'},${d.price_dropping ? '价格下跌' : '价格上涨'}\t\n`; section += `${icon(d.volume_bool)} ${d.volume}\t\t\t成交量强度 \n\t\t\t必需:放量下跌(量 > SMA20 且 阴线)\n\t\t\t当前:量 ${safeNum(d.volume_val)} | SMA20 ${safeNum(d.vol_sma_val)} | ${d.volume_bool ? '阴线' : '阳线'}\n`; return section; } function renderL2(symbol, coin) { if (!coin.l2) return ''; const l2 = coin.l2; const d2 = l2.details || {}; let section = ''; section += `阶段\nL2 执行层 ${symbol}\n当前:${l2.total}\n合格:${L2_PASS_SCORE}\n总分:${l2.max_score}\n\n`; section += "组结果\n" + (l2.go ? "GO ✅" : "NOTGO ❌") + "\n" + l2.total + "/" + l2.max_score + "\n\n"; section += "子结果\t\t数值/条件\n"; section += `${icon(d2.mf)} ${d2.mf ? 5 : 0}\t\tMF动能衰退 (LH + MF 同时出现另加5分)\n\t\t必需:RSI < ${RSI_L2_THRESHOLD} 且 MACD柱为负且扩大\n\t\t当前:RSI ${safeNum(d2.rsi_val)} | MACD柱 ${safeNum(d2.macd_hist_val)} ${d2.mf ? '满足' : '不满足'}\n`; section += `${icon(d2.a3)} ${d2.a3 ? 5 : 0}\t\tA3 跌破+弱回抽 \t\n\t\t必需:跌破近期低点 + 弱回抽(RSI 52-55,缩量)\n\t\t当前:跌破=${d2.a3 ? '满足' : '不满足'} | RSI ${safeNum(d2.rsi_val)} | 量 renderL1(symbol, coin), ''); html += "--------------------------------------------------------------------\n\n"; html += "\n============================================================================\n"; html += `${symbol}\n`; html += "============================================================================\n\n"; html += safeRender(() => renderCooldown(symbol, coin), ''); html += "--------------------------------------------------------------------\n\n"; html += safeRender(() => renderPrelim(symbol, coin), ''); html += "--------------------------------------------------------------------\n\n"; html += safeRender(() => renderL1(symbol, coin), ''); html += "--------------------------------------------------------------------\n\n"; html += safeRender(() => renderL2(symbol, coin), ''); html += "--------------------------------------------------------------------\n\n"; html += safeRender(() => renderSummary(symbol, coin), ''); html += "\n"; } reportDiv.innerHTML = html; container.appendChild(reportDiv); return container; }// ==================== 主渲染函数 ==================== function renderResultBoard(reportData, viewMode = 'B', params = {}) { console.log('[最终版] 开始渲染'); // 强制依赖 params 对象,不再提供后备默认值 if (!params || typeof params !== 'object') { var div = document.createElement('div'); div.className = 'no-data error-message'; div.textContent = '❌ 参数缺失,无法渲染看板。请刷新页面或重新获取数据。'; return div; } // 从 params 动态读取阈值(提供后备默认值,与配置文件一致) var MACRO_ADX_THRESHOLD = params.MACRO_ADX_THRESHOLD; var DROP_THRESHOLD_PCT = params.DROP_THRESHOLD_PCT; var L1_ADX_THRESHOLD = params.L1_ADX_THRESHOLD; var RSI_L2_THRESHOLD = params.RSI_L2_THRESHOLD; var L1_PASS_SCORE = params.L1_PASS_SCORE; var L2_PASS_SCORE = params.L2_PASS_SCORE; var RSI_PRELIM_THRESHOLD = params.RSI_PRELIM_THRESHOLD; // 可选检查:如果任意必要参数缺失,返回错误 if (MACRO_ADX_THRESHOLD === undefined || DROP_THRESHOLD_PCT === undefined || L1_ADX_THRESHOLD === undefined || RSI_L2_THRESHOLD === undefined || L1_PASS_SCORE === undefined || L2_PASS_SCORE === undefined || RSI_PRELIM_THRESHOLD === undefined) { var div = document.createElement('div'); div.className = 'no-data error-message'; div.textContent = '❌ 参数不完整,无法渲染看板。请检查参数加载是否成功。'; return div; } if (!reportData || reportData.error || !reportData.coins || Object.keys(reportData.coins).length === 0) { var div = document.createElement('div'); div.className = 'no-data'; div.textContent = '【结果看板】暂无有效数据,请先获取K线数据并确保后端评分计算正常。'; return div; } // 辅助函数已由 08.05a 提供(在全局作用域中) // 此处直接使用 safeNum, icon, formatCooldown, getSubColorClass, getGroupColorClass, // createSpacer, formatValueHtml, addStageAndGroupRow, addRowOnlySubValue, addSingleRow function getTotalScore(coin) { var summary = coin.summary || {}; var bl = summary.bl_score ?? 0; var r = summary.r_score ?? 0; var l1 = summary.l1_score ?? 0; var l2 = summary.l2_score ?? 0; return bl * (r + l1 + l2); } function buildCoinTable(coin, displaySymbol, globalRC, macroGo, macroAdx) { if (!coin) return null; var rc = globalRC; var rcGo = rc.go; var groupResultText = rcGo ? 'GO ✅' : 'NOTGO ❌'; var tableWrapper = document.createElement('div'); tableWrapper.className = 'board-wrapper'; var caption = document.createElement('div'); caption.className = 'board-caption'; var totalScore = getTotalScore(coin); if (totalScore !== 0) { caption.innerHTML = '📊 ' + displaySymbol + ' 评分详情 ' + totalScore + ''; } else { caption.textContent = '📊 ' + displaySymbol + ' 评分详情'; } var table = document.createElement('table'); table.className = 'result-board-table'; var thead = document.createElement('thead'); var secondHeader = (viewMode === 'A') ? '组结果' : '组'; var theadHtml = '阶段' + secondHeader + '子结果数值/条件'; thead.innerHTML = theadHtml; table.appendChild(thead); var tbody = document.createElement('tbody'); // 宏观风控 var macroStageHtml = '宏观风控 BTC
2pm-2:30pm

需满足全部条件'; var bl01Sub = 'BL01 ' + (rc.bl01 ? '❌' : '✅'); var bl01Val = '禁止:4H价格 > EMA200
必需:4H价格 ≤ EMA200
当前:4H价格 ' + safeNum(rc.current?.price_4h) + ' | EMA200 ' + safeNum(rc.current?.ema200_4h); addStageAndGroupRow(tbody, macroStageHtml, groupResultText, bl01Sub, bl01Val, 3, 3, 'macro', rcGo); var bl03Sub = 'BL03 ' + (rc.bl03 ? '❌' : '✅'); var bl03Val = '禁止:1H MACD 0轴上死叉
必需:MACD ≤0 且 无死叉
当前:MACD ' + safeNum(rc.current?.macd_line_1h) + ' | Signal ' + safeNum(rc.current?.macd_signal_1h); addRowOnlySubValue(tbody, bl03Sub, bl03Val, 'macro'); var bl02Sub = 'BL02 ' + (rc.bl02 ? '❌' : '✅'); var bl02Val = '禁止:出现 15m HL(Higher Low)
当前:HL ' + (rc.bl02 ? '已形成' : '未形成'); addRowOnlySubValue(tbody, bl02Sub, bl02Val, 'macro'); tbody.appendChild(createSpacer()); // 限时否决 var bigGreenNow = coin.cooldown?.big_green_now || false; var remainingSec = coin.cooldown?.remaining_sec || 0; var isCooldownPass = (!bigGreenNow && remainingSec <= 0); var cooldownGroupResult = isCooldownPass ? 'GO ✅' : 'NOTGO ❌'; var cooldownStageHtml = '限时否决
(30分钟)'; var bl05Sub = 'BL05 ' + (isCooldownPass ? '✅' : '❌'); var statusText = bigGreenNow ? '大阳线形成' : '大阳线未形成'; var cooldownDisplay = formatCooldown(remainingSec); var bl05Val = '禁止:出现一根大阳线
当前:' + statusText + '【禁空 ' + cooldownDisplay + '】'; addSingleRow(tbody, cooldownStageHtml, cooldownGroupResult, bl05Sub, bl05Val, 'cooldown', isCooldownPass); tbody.appendChild(createSpacer()); // 宏观定调(使用动态阈值 MACRO_ADX_THRESHOLD) var macroCallStageHtml = '宏观定调 BTC
2pm-2:30pm'; var adxSub = 'ADX ' + (macroGo ? '✅' : '❌'); var adxVal = '必需:ADX > ' + MACRO_ADX_THRESHOLD + '
当前:' + safeNum(macroAdx); addSingleRow(tbody, macroCallStageHtml, macroGo ? 'GO ✅' : 'NOTGO ❌', adxSub, adxVal, 'call', macroGo); tbody.appendChild(createSpacer()); // 初步筛选(使用动态阈值 DROP_THRESHOLD_PCT) var prelim = coin.prelim || {}; var prelimGo = prelim.go || false; var prelimStageHtml = '初步筛选
3:30pm-4:30pm

需满足全部条件'; var dropSub = '跌幅 ' + (prelim.drop_ok ? '✅' : '❌'); var dropVal = '必需:跌 > ' + DROP_THRESHOLD_PCT + '%
当前:' + safeNum(prelim.drop_val) + '%'; addStageAndGroupRow(tbody, prelimStageHtml, prelimGo ? 'GO ✅' : 'NOTGO ❌', dropSub, dropVal, 3, 3, 'prelim', prelimGo); var turnoverSub = '成交额 ' + (prelim.turnover_ok ? '✅' : '❌'); var turnoverVal = '必需:成交额 > 阈值M USDT
当前:' + safeNum(prelim.turnover_val) + 'M'; addRowOnlySubValue(tbody, turnoverSub, turnoverVal, 'prelim'); var rsiSub = 'RSI ' + (prelim.rsi_ok ? '✅' : '❌'); var rsiVal = '必需:RSI < ' + RSI_PRELIM_THRESHOLD + '
当前:' + safeNum(prelim.rsi_val); addRowOnlySubValue(tbody, rsiSub, rsiVal, 'prelim'); tbody.appendChild(createSpacer()); // L1 趋势层(使用动态 L1_PASS_SCORE 和 L1_ADX_THRESHOLD) var l1 = coin.l1 || {}; var l1d = l1.details || {}; if (Object.keys(l1d).length > 0) { var l1Current = l1.total ?? 0; var l1Pass = L1_PASS_SCORE; var l1Max = l1.max_score ?? 37; var l1StageHtml = 'L1 趋势层指标

当前:' + l1Current + '
合格:' + l1Pass + '
总分:' + l1Max + ''; var groupHtml = (l1.go ? 'GO ✅' : 'NOTGO ❌') + '
(' + l1Current + '/' + l1Max + ')'; var mtfSub = icon(l1d.mtf_bool) + ' ' + (l1d.mtf ?? 0); var mtfVal = '必需:4H空头 且 1H空头
当前:4H多头,1H多头'; addStageAndGroupRow(tbody, l1StageHtml, groupHtml, mtfSub, mtfVal, 6, 6, 'l1', l1.go); var alignSub = icon(l1d.alignment_bool) + ' ' + (l1d.alignment ?? 0); var alignVal = '必需:EMA20 < EMA50 < EMA100(空头排列)
当前:' + (l1d.alignment_bool ? '空头排列' : '多头排列'); addRowOnlySubValue(tbody, alignSub, alignVal, 'l1'); var slopeSub = icon(l1d.slope_bool) + ' ' + (l1d.slope ?? 0); var slopeVal = '必需:三条均线均向下
当前:' + (l1d.slope_bool ? '三线皆下行' : '不满足'); addRowOnlySubValue(tbody, slopeSub, slopeVal, 'l1'); var priceSub = icon(l1d.price_pos_bool) + ' ' + (l1d.price_pos ?? 0); var priceVal = '必需:收盘价 < 所有均线
当前:' + (l1d.price_pos_bool ? '价格在所有EMA下方' : '价格在所有EMA上方'); addRowOnlySubValue(tbody, priceSub, priceVal, 'l1'); var adxSubL1 = icon(l1d.adx_bool) + ' ' + (l1d.adx ?? 0); var adxValL1 = '必需:ADX > ' + L1_ADX_THRESHOLD + ' 且 上升 且 价格下跌
当前:ADX ' + safeNum(l1d.adx_val) + ',' + (l1d.adx_rising ? '上升' : '下降') + ',' + (l1d.price_dropping ? '价格下跌' : '价格上涨'); addRowOnlySubValue(tbody, adxSubL1, adxValL1, 'l1'); var volSub = icon(l1d.volume_bool) + ' ' + (l1d.volume ?? 0); var volVal = '必需:放量下跌(量 > SMA20 且 阴线)
当前:量 ' + safeNum(l1d.volume_val) + ' | SMA20 ' + safeNum(l1d.vol_sma_val) + ' | ' + (l1d.volume_bool ? '阴线' : '阳线'); addRowOnlySubValue(tbody, volSub, volVal, 'l1'); tbody.appendChild(createSpacer()); } // L2 执行层(使用动态 L2_PASS_SCORE 和 RSI_L2_THRESHOLD) var l2 = coin.l2 || {}; var l2d = l2.details || {}; if (Object.keys(l2d).length > 0) { var l2Current = l2.total ?? 0; var l2Pass = L2_PASS_SCORE; var l2Max = l2.max_score ?? 25; var l2StageHtml = 'L2 执行层

当前:' + l2Current + '
合格:' + l2Pass + '
总分:' + l2Max + ''; var groupHtml = (l2.go ? 'GO ✅' : 'NOTGO ❌') + '
(' + l2Current + '/' + l2Max + ')'; var mfSub = icon(l2d.mf) + ' ' + (l2d.mf ? 5 : 0); var mfVal = '必需:RSI < ' + RSI_L2_THRESHOLD + ' 且 MACD柱为负且扩大
当前:RSI ' + safeNum(l2d.rsi_val) + ' | MACD柱 ' + safeNum(l2d.macd_hist_val); addStageAndGroupRow(tbody, l2StageHtml, groupHtml, mfSub, mfVal, 4, 4, 'l2', l2.go); var a3Sub = icon(l2d.a3) + ' ' + (l2d.a3 ? 5 : 0); var a3Val = '必需:跌破近期低点 + 弱回抽(RSI 52-55,缩量)
当前:跌破=' + (l2d.a3 ? '满足' : '不满足') + ' | RSI ' + safeNum(l2d.rsi_val) + ' | 量