fix(ui): 盯盘推荐区RR三值+个股弹窗DB化

- /api/stock/<code>: 从holding_strategies读策略/三值RR/action/full_analysis
  (原先只读data/stocks/{code}.json, 与DB脱节, 弹窗打开=空白)
- openStock弹窗: 新增当前策略卡(信号/区间/损盈/三值RR/仓位/操作建议)+12维分析折叠区
- 盯盘推荐区RR列: 中值下方小字显示下沿~上沿(上次改错表了, renderWatchlist≠盯盘推荐区)
This commit is contained in:
hmo
2026-07-22 23:25:34 +08:00
parent 0f2c6555df
commit 440ce56b0b
2 changed files with 67 additions and 2 deletions
+30 -1
View File
@@ -503,8 +503,37 @@ def api_report(report_id):
@app.route("/api/stock/<code>")
def api_stock(code):
"""个股详情 + 操作建议历史"""
"""个股详情DB策略(12维分析/action/三值RR)为主,JSON历史为辅。
根治:弹窗只读 data/stocks/{code}.json,与DB脱节,打开=空白。"""
stock_data = _load_json(DATA_DIR / "stocks" / f"{code}.json", {})
try:
conn = get_conn()
conn.row_factory = sqlite3.Row
r = conn.execute("""
SELECT hs.code, hs.name, hs.timing_signal, hs.action, hs.position_advice,
hs.entry_low, hs.entry_high, hs.stop_loss, hs.take_profit,
hs.rr_ratio, hs.rr_low, hs.rr_high, hs.full_analysis, hs.reassessed_at,
hs.tag, hs.decision_type,
lp.price AS live_price, lp.change_pct
FROM holding_strategies hs
LEFT JOIN live_prices lp ON hs.code = lp.code
WHERE hs.code=? AND hs.status='active'
""", (code,)).fetchone()
conn.close()
if r:
stock_data.update({
"code": r["code"], "name": r["name"],
"timing_signal": r["timing_signal"], "action": r["action"],
"position_advice": r["position_advice"],
"entry_low": r["entry_low"], "entry_high": r["entry_high"],
"stop_loss": r["stop_loss"], "take_profit": r["take_profit"],
"rr_ratio": r["rr_ratio"], "rr_low": r["rr_low"], "rr_high": r["rr_high"],
"full_analysis": r["full_analysis"], "reassessed_at": r["reassessed_at"],
"tag": r["tag"], "decision_type": r["decision_type"],
"price": r["live_price"], "change_pct": r["change_pct"],
})
except Exception as _e:
stock_data.setdefault("db_error", str(_e))
return jsonify(stock_data)
+37 -1
View File
@@ -159,7 +159,9 @@ body { background: #0f0f13; color: #e2e8f0; }
<button onclick="closeStock()" class="text-slate-500 hover:text-white text-xl">&times;</button>
</div>
<div id="modalInfo" class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4"></div>
<div id="modalStrategy" class="mb-4"></div>
<div id="modalChart" class="h-48 mb-4 bg-slate-900 rounded-xl p-2"></div>
<div id="modalAnalysis" class="mb-4"></div>
<div>
<h3 class="text-sm font-semibold text-slate-300 mb-2">📜 操作建议历史</h3>
<div id="modalHistory" class="space-y-2"></div>
@@ -696,6 +698,40 @@ async function openStock(code) {
</div></div>
` : `<div class="col-span-full text-xs text-slate-500">暂无持仓数据</div>`;
// ── 当前策略卡(12维参数 + 三值RR + 操作建议)──
const sd = stockData || {};
const hasStrategy = sd.action || sd.entry_low || sd.timing_signal;
if (hasStrategy) {
const rrMid = sd.rr_ratio || 0, rrLo = sd.rr_low || 0, rrHi = sd.rr_high || 0;
const rrTxt = rrMid ? `${rrMid.toFixed(2)}${(rrHi>0)?` <span class="text-slate-500 text-[11px]">(下沿${rrLo.toFixed(2)} / 上沿${rrHi.toFixed(2)})</span>`:''}` : '—';
document.getElementById('modalStrategy').innerHTML = `
<h3 class="text-sm font-semibold text-slate-300 mb-2">🎯 当前策略 <span class="text-xs text-slate-500 font-normal">${(sd.reassessed_at||'').slice(0,16)}</span></h3>
<div class="bg-slate-800/40 rounded-lg p-3 space-y-1.5 text-xs">
<div class="flex flex-wrap gap-x-4 gap-y-1 font-mono">
${sd.timing_signal?`<span>信号 <b class="${sd.timing_signal.includes('买入')?'text-green-400':sd.timing_signal.includes('卖出')?'text-red-400':'text-yellow-400'}">${sd.timing_signal}</b></span>`:''}
${sd.entry_low?`<span class="text-slate-300">区间 ${sd.entry_low}~${sd.entry_high}</span>`:''}
${sd.stop_loss?`<span class="text-red-400">损 ${sd.stop_loss}</span>`:''}
${sd.take_profit?`<span class="text-green-400">盈 ${sd.take_profit}</span>`:''}
<span class="${rrMid>=1.5?'text-green-400':'text-yellow-400'}">RR ${rrTxt}</span>
${sd.position_advice?`<span class="text-blue-400">仓位 ${sd.position_advice}</span>`:''}
</div>
${sd.action?`<div class="text-slate-200 leading-relaxed border-t border-slate-700/50 pt-1.5">${sd.action}</div>`:''}
</div>`;
} else {
document.getElementById('modalStrategy').innerHTML = '';
}
// ── 12维完整分析(折叠)──
if (sd.full_analysis) {
document.getElementById('modalAnalysis').innerHTML = `
<details class="bg-slate-800/30 rounded-lg">
<summary class="text-sm font-semibold text-slate-300 p-3 cursor-pointer select-none">📋 12维完整分析(点击展开)</summary>
<pre class="text-xs text-slate-300 leading-relaxed whitespace-pre-wrap p-3 pt-0 font-sans">${sd.full_analysis}</pre>
</details>`;
} else {
document.getElementById('modalAnalysis').innerHTML = '';
}
// Render recommendation history
const history = stockData.history || [];
document.getElementById('modalHistory').innerHTML = history.length ? history.map(h => `
@@ -1542,7 +1578,7 @@ async function renderWatch() {
'<td class="p-2 text-left"><span class="text-xs px-2 py-1 rounded ' + sigCls + '">' + (sig || '—') + '</span></td>' +
'<td class="p-2 text-right font-mono text-slate-300">' + buyZone + '</td>' +
'<td class="p-2 text-right font-mono text-slate-300">' + sltp + '</td>' +
'<td class="p-2 text-right font-mono ' + (rr >= 1.5 ? 'text-green-400' : rr > 0 ? 'text-yellow-400' : 'text-slate-500') + '">' + (rr ? rr.toFixed(2) : '—') + '</td>' +
'<td class="p-2 text-right font-mono ' + (rr >= 1.5 ? 'text-green-400' : rr > 0 ? 'text-yellow-400' : 'text-slate-500') + '">' + (rr ? rr.toFixed(2) : '—') + ((s.rr_high>0)?'<br><span class="text-[10px] text-slate-500">'+(s.rr_low||0).toFixed(2)+'~'+s.rr_high.toFixed(2)+'</span>':'') + '</td>' +
'<td class="p-2 text-right font-mono text-slate-300">' + posDisplay + '</td>' +
'<td class="p-2 ' + stratCls + ' max-w-[200px] break-all line-clamp-2">' + strat + '</td>' +
'<td class="p-2 text-center"><button class="text-xs px-2 py-1 rounded bg-blue-900/50 text-blue-400 hover:bg-blue-800/50 transition" onclick="event.stopPropagation();openStrategyHistory(\'' + s.code + '\')">📋 历史</button></td>' +