评分改为±100分制;库存图y轴倒置;月差序列过滤远月前12月幽灵数据

This commit is contained in:
2026-06-15 15:25:26 +08:00
parent 94a230e485
commit bc9f2682d1
4 changed files with 93 additions and 51 deletions
+30 -25
View File
@@ -198,16 +198,16 @@ def band_score(latest_value: float, history: pd.Series, day: int, inverse: bool
if not math.isfinite(low) or not math.isfinite(high):
return None
if high == low:
raw_score = 100.0 if latest_value >= high else 0.0
raw_score = 100.0 if latest_value >= high else -100.0
elif latest_value >= high:
raw_score = 100.0
elif latest_value <= low:
raw_score = 0.0
raw_score = -100.0
else:
raw_score = 100.0 * (latest_value - low) / (high - low)
score = 100.0 - raw_score if inverse else raw_score
raw_score = 200.0 * (latest_value - low) / (high - low) - 100.0
score = -raw_score if inverse else raw_score
return {
"score": max(0.0, min(100.0, score)),
"score": max(-100.0, min(100.0, score)),
"band_low": low,
"band_high": high,
"band_mean": mid,
@@ -279,7 +279,8 @@ def score_series_at(series: pd.Series, date: pd.Timestamp, inverse: bool = False
less = float((sample < value).sum())
equal = float((sample == value).sum())
raw = 100.0 * (less + 0.5 * equal) / len(sample)
return 100.0 - raw if inverse else raw
raw_mapped = raw * 2.0 - 100.0
return -raw_mapped if inverse else raw_mapped
def build_score_history(dim_series: dict[str, pd.Series]) -> list[dict[str, Any]]:
@@ -351,7 +352,8 @@ def make_records() -> dict[str, Any]:
stat.update(band)
else:
raw = stat["raw_percentile"]
score = 100.0 - raw if dim in INVERSE_DIMS else raw
raw_mapped = raw * 2.0 - 100.0
score = -raw_mapped if dim in INVERSE_DIMS else raw_mapped
stat["score"] = round(score, 1)
stat["direction"] = "低库存更优,按正常区间反向计分" if dim in INVERSE_DIMS else "高于正常区间更优"
stat["expr"] = str(row.get(dim, "")).strip()
@@ -389,7 +391,8 @@ def make_records() -> dict[str, Any]:
stat.update(band)
else:
raw = stat["raw_percentile"] if stat.get("raw_percentile") is not None else 50.0
score = 100.0 - raw if dim in INVERSE_DIMS else raw
raw_mapped = raw * 2.0 - 100.0
score = -raw_mapped if dim in INVERSE_DIMS else raw_mapped
stat["score"] = round(score, 1)
stat["direction"] = "高于正常区间更优(近月升水=强势)"
stat["expr"] = "近月(top3 OI) - 远月(最远>1万手)"
@@ -585,7 +588,7 @@ svg {{ width:100%; height:230px; display:block; }}
<div class="chips" id="weak"></div>
</div>
</section>
<div class="note">维度得分按当前阴影带范围计算(均值±K×标准差):正向指标高于上沿为 100,低于下沿为 0,区间内线性映射;库存反向。调整顶部「带宽K」可实时改变阴影带宽度和得分。</div>
<div class="note">维度得分采用 ±100 分制:高于季节性正常区间上沿为 +100,低于下沿为 -100,区间内线性映射;0 分表示处于正常中位水平。正得分偏利多,负得分偏利空;库存反向。调整顶部「带宽K」可实时改变阴影带宽度和得分。</div>
<main class="rows" id="rows"></main>
<section class="history-view" id="historyView">
<div class="history-tools">
@@ -718,9 +721,8 @@ function render() {{
document.getElementById("avgScore").textContent = scored.length ? fmt(scored.reduce((s,c)=>s+c.total,0)/scored.length) : "--";
document.getElementById("coverage").textContent = `当前筛选覆盖 ${{scored.length}} 个有可用数据的品种;${{includeDemand.checked ? "需求参与总分" : "需求不参与总分"}}${{document.getElementById("includeSpread").checked ? "月差参与总分" : "月差不参与总分"}}。`;
const sortedAll = scored.slice().sort((a,b)=>b.total-a.total);
const splitIdx = Math.ceil(sortedAll.length / 2);
const strongList = sortedAll.slice(0, splitIdx);
const weakList = sortedAll.slice(splitIdx).reverse();
const strongList = sortedAll.filter(c => c.total > 0);
const weakList = sortedAll.filter(c => c.total < 0).reverse();
document.getElementById("strong").innerHTML = strongList.length ? topChips(strongList, "red") : `<span class="hint">暂无可用数据</span>`;
document.getElementById("weak").innerHTML = weakList.length ? topChips(weakList, "good") : `<span class="hint">暂无可用数据</span>`;
rowsEl.style.display = activeView === "dashboard" ? "block" : "none";
@@ -774,7 +776,7 @@ function renderChartCell(c, dim, v) {{
<div class="chart-title">${{title}}</div>
<div class="metric-title">${{esc(v.expr)}}</div>
<div class="legend">${{Object.keys(v.years).map(y=>`<span style="--c:${{yearColor(y, v.years)}}">${{y}}</span>`).join("")}}</div>
<svg data-chart='${{esc(JSON.stringify(v.years))}}' viewBox="0 0 520 230" preserveAspectRatio="none"></svg>
<svg data-chart='${{esc(JSON.stringify(v.years))}}' data-dim='${{esc(dim)}}' viewBox="0 0 520 230" preserveAspectRatio="none"></svg>
</div>`;
}}
function renderPairChartCell(c, ps, idx) {{
@@ -917,12 +919,12 @@ function scoreFromCurrentBand(v, inverse=false) {{
if (!Number.isFinite(low) || !Number.isFinite(high)) return null;
if (low > high) {{ const tmp = low; low = high; high = tmp; }}
let rawScore;
if (high === low) rawScore = latest.value >= high ? 100 : 0;
if (high === low) rawScore = latest.value >= high ? 100 : -100;
else if (latest.value >= high) rawScore = 100;
else if (latest.value <= low) rawScore = 0;
else rawScore = 100 * (latest.value - low) / (high - low);
const score = inverse ? 100 - rawScore : rawScore;
return Math.max(0, Math.min(100, score));
else if (latest.value <= low) rawScore = -100;
else rawScore = 200 * (latest.value - low) / (high - low) - 100;
const score = inverse ? -rawScore : rawScore;
return Math.max(-100, Math.min(100, score));
}}
function smoothBand(band, radius) {{
if (!band.length || radius <= 0) return band;
@@ -1003,10 +1005,11 @@ function drawChart(svg, years) {{
let min=Math.min(...values), max=Math.max(...values);
if (min===max) {{ min-=1; max+=1; }}
const pad=(max-min)*0.12; min-=pad; max+=pad;
const y=v=> T + (max-v)/(max-min)*(H-T-B);
const invertY = svg.dataset.dim === "库存";
const y = invertY ? v => T + (v-min)/(max-min)*(H-T-B) : v => T + (max-v)/(max-min)*(H-T-B);
let out = "";
for (let i=0;i<=4;i++) {{
const yy=T+i*(H-T-B)/4, val=max-i*(max-min)/4;
const yy=T+i*(H-T-B)/4, val=invertY ? min+i*(max-min)/4 : max-i*(max-min)/4;
out += `<line class="grid" x1="${{L}}" y1="${{yy}}" x2="${{W-R}}" y2="${{yy}}"></line><text x="4" y="${{yy+4}}" font-size="12" fill="#667085">${{val.toFixed(1)}}</text>`;
}}
// x轴刻度
@@ -1095,11 +1098,13 @@ function drawHistoryChart(list) {{
const maxDate = new Date(end).getTime();
const span = Math.max(1, maxDate - minDate);
const x = d => L + (new Date(d).getTime() - minDate) / span * (W-L-R);
const y = v => T + (100 - v) / 100 * (H-T-B);
const yMin = -100, yMax = 100;
const y = v => T + (yMax - v) / (yMax - yMin) * (H-T-B);
let out = "";
for (let i=0;i<=5;i++) {{
const val = i * 20, yy = y(val);
out += `<line class="grid" x1="${{L}}" y1="${{yy}}" x2="${{W-R}}" y2="${{yy}}"></line><text x="8" y="${{yy+4}}" font-size="12" fill="#667085">${{val}}</text>`;
for (let i=0;i<=4;i++) {{
const val = yMin + i * (yMax - yMin) / 4, yy = y(val);
const isZero = val === 0;
out += `<line class="${{isZero ? 'axis' : 'grid'}}" x1="${{L}}" y1="${{yy}}" x2="${{W-R}}" y2="${{yy}}"></line><text x="8" y="${{yy+4}}" font-size="12" fill="${{isZero ? '#374151' : '#667085'}}" font-weight="${{isZero ? 'bold' : 'normal'}}">${{val}}</text>`;
}}
const ticks = 6;
for (let i=0;i<=ticks;i++) {{
@@ -1158,7 +1163,7 @@ function drawHistoryChart(list) {{
return p ? {{name:s.name, score:Number(p[1]), color:historyColor(i), date:p[0]}} : null;
}}).filter(Boolean).sort((a,b)=>b.score-a.score);
const py = (ev.clientY - rect.top) / rect.height * H;
const guideYValue = Math.max(0, Math.min(100, 100 - ((py - T) / (H - T - B)) * 100));
const guideYValue = Math.max(-100, Math.min(100, 100 - ((py - T) / (H - T - B)) * 200));
const nearest = rows.length ? rows.reduce((best, r) => Math.abs(Number(r.score) - guideYValue) < Math.abs(Number(best.score) - guideYValue) ? r : best, rows[0]) : null;
if (crosshair) {{
crosshair.style.display = "block";