重构: 移除信号Tab(与市场Tab重复+死数据), 市场Tab改为温区regime+板块排行, 候选池迁入选自Tab, /api/market加regimes, 删/api/signals, 修change_pct显示bug和market.json兜底

This commit is contained in:
xxm
2026-08-22 11:14:54 +08:00
parent 0c1c6cabeb
commit d82d67246f
2 changed files with 84 additions and 144 deletions
+15 -25
View File
@@ -1158,42 +1158,32 @@ def api_stock(code):
@app.route("/api/market")
def api_market():
"""市场观察"""
"""市场观察2026-08-22 重构:板块快照 + A股/港股温区 regime)"""
try:
from mofin_db import get_conn, query_latest_market
conn = get_conn()
data = query_latest_market(conn)
# 当前市场温区(每市场取最新一条 market_regime
try:
rows = conn.execute("""
SELECT mr.market, mr.date, mr.regime, mr.close,
mr.above_ma20, mr.ma20_slope, mr.roc, mr.adx
FROM market_regime mr
JOIN (SELECT market, MAX(date) md FROM market_regime GROUP BY market) t
ON mr.market = t.market AND mr.date = t.md
ORDER BY mr.market
""").fetchall()
data["regimes"] = [dict(r) for r in rows]
except Exception:
data["regimes"] = []
conn.close()
if data and data.get("sectors"):
if data and (data.get("sectors") or data.get("regimes")):
return jsonify(data)
except Exception:
pass
return jsonify(_load_json(DATA_DIR / "market.json", {}))
# ── 信号API(新增) ─────────────────────────────────────
@app.route("/api/signals")
def api_signals():
"""最近信号 + 小果分析"""
try:
from mofin_db import get_conn
conn = get_conn()
signals = conn.execute("""
SELECT sn.id, sn.sector, sn.overall_sentiment,
sn.summary, sn.source, sn.created_at,
ss.signal_type, ss.severity
FROM signal_news sn
LEFT JOIN sector_signals ss ON sn.signal_id = ss.id
ORDER BY sn.id DESC LIMIT 20
""").fetchall()
conn.close()
return jsonify([dict(r) for r in signals])
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/xiaoguo-scan")
def api_xiaoguo_scan():
"""小果扫描统计(2026-08-19 小果已废弃收尸,返回空)"""
+68 -118
View File
@@ -111,7 +111,6 @@ body { background: #0f0f13; color: #e2e8f0; }
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="prompts">📝 提示词</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="market">🌐 市场</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="signals">🔍 信号</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="health">🏥 健康</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="principles">📐 开发原则</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="research">📋 研究</button>
@@ -126,7 +125,6 @@ body { background: #0f0f13; color: #e2e8f0; }
<div id="tab-watchlist" class="tab-content hidden"></div>
<div id="tab-prompts" class="tab-content hidden"></div>
<div id="tab-market" class="tab-content hidden"></div>
<div id="tab-signals" class="tab-content hidden"></div>
<div id="tab-health" class="tab-content hidden"></div>
<div id="tab-research" class="tab-content hidden"></div>
<div id="tab-docs" class="tab-content hidden"></div>
@@ -313,7 +311,6 @@ function renderTab(name) {
else if (name === 'evaluation') renderEvaluation();
else if (name === 'prompts') renderPrompts();
else if (name === 'market') renderMarket();
else if (name === 'signals') renderSignals();
else if (name === 'health') renderHealth();
else if (name === 'principles') renderPrinciples();
else if (name === 'watch') renderWatch();
@@ -563,6 +560,7 @@ function renderWatchlist() {
window._fullAnalysisMap = {};
stocks.forEach(s => { window._fullAnalysisMap[s.code] = s.full_analysis || ''; });
el.innerHTML = `
<div id="watchlist-candidates" class="mb-4"></div>
<div class="card overflow-hidden">
<div class="p-3 border-b border-slate-800/50 flex justify-between items-center">
<span class="text-sm font-semibold">自选 · ${stocks.length} 只</span>
@@ -619,59 +617,19 @@ function renderWatchlist() {
</div>
</div>
`;
loadWatchlistCandidates();
}
// ── Market Tab ──
let marketRefreshInterval = null;
function refreshMarketData() {
const el = document.getElementById('tab-market');
if (!el || el.classList.contains('hidden')) return;
Promise.all([
fetchJSON('/api/market'),
fetchJSON('/api/signals'),
fetchJSON('/api/candidates'),
]).then(([mkt, signals, candidates]) => {
const sectors = mkt.sectors || [];
const sigs = signals || [];
const cands = candidates || [];
const trend = sigs.filter(s => s.source && s.source !== 'xiaoguo').concat(sigs.filter(s => s.source === 'market_scanner'));
el.innerHTML = `
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-semibold">🌐 市场全景 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('market','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('market','ai')">§</span><span class="spec-btn help" onclick="showModuleHelp('signals','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('signals','ai')">§</span></span></span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-4">
// ── 候选池(2026-08-22 从市场Tab迁入:候选→提拔→自选是同一管道)──
async function loadWatchlistCandidates() {
const box = document.getElementById('watchlist-candidates');
if (!box) return;
try {
const cands = await fetchJSON('/api/candidates') || [];
if (!cands.length) { box.innerHTML = ''; return; }
box.innerHTML = `
<div class="card p-4">
<h3 class="text-sm font-semibold mb-2">🏭 行业热点</h3>
${sectors.length ? sectors.slice(0,8).map(s => `
<div class="flex justify-between items-center p-1.5 ${s.change>=0?'':'bg-slate-800/20'} rounded mb-0.5">
<span class="text-xs">${s.name}</span>
<span class="text-xs font-mono ${(s.change||0)>=0?'badge-up':'badge-down'}">${(s.change||0)>=0?'+':''}${(s.change||0).toFixed(1)}%</span>
</div>
`).join('') : '<div class="text-xs text-slate-500">暂无</div>'}
</div>
<div class="card p-4">
<h3 class="text-sm font-semibold mb-2">📡 趋势信号 (${trend.length})</h3>
${trend.length ? trend.slice(0,6).map(s => `
<div class="p-1.5 bg-slate-800/30 rounded mb-0.5 text-xs">
<span class="font-semibold ${s.overall_sentiment==='利好'?'text-green-400':s.overall_sentiment==='利空'?'text-red-400':'text-slate-400'}">${s.overall_sentiment||'中性'}</span>
${s.sector} ${(s.summary||'').slice(0,60)}
</div>
`).join('') : '<div class="text-xs text-slate-500">暂无</div>'}
</div>
<div class="card p-4">
<h3 class="text-sm font-semibold mb-2">📊 信号汇总 (${sigs.length})</h3>
${sigs.length ? sigs.slice(0,6).map(s => `
<div class="p-1.5 bg-slate-800/30 rounded mb-0.5 text-xs">
<span class="font-semibold ${s.overall_sentiment==='利好'?'text-green-400':s.overall_sentiment==='利空'?'text-red-400':'text-slate-400'}">${s.overall_sentiment||'中性'}</span>
${s.sector} ${(s.summary||'').slice(0,60)}
</div>
`).join('') : '<div class="text-xs text-slate-500">暂无</div>'}
</div>
</div>
<div class="card p-4 mb-4">
<h3 class="text-sm font-semibold mb-2">🎯 主力建仓候选 (${cands.length})</h3>
${cands.length ? `
<div class="overflow-x-auto">
<table class="w-full text-xs">
<thead><tr class="text-slate-400">
@@ -704,19 +662,74 @@ function refreshMarketData() {
}).join('')}</tbody>
</table>
</div>
` : '<div class="text-xs text-slate-500">暂无候选(开盘后自动扫描)</div>'}
</div>`;
} catch(e) { box.innerHTML = ''; }
}
// ── Market Tab ──
let marketRefreshInterval = null;
function refreshMarketData() {
const el = document.getElementById('tab-market');
if (!el || el.classList.contains('hidden')) return;
Promise.all([
fetchJSON('/api/market'),
]).then(([mkt]) => {
const sectors = mkt.sectors || [];
const regimes = mkt.regimes || [];
const REG_ICON = {trend_up:'📈', choppy:'〰️', trend_down:'📉'};
const REG_NAME = {trend_up:'趋势市', choppy:'震荡市', trend_down:'下跌市'};
const regColor = s => s==='trend_up' ? 'text-emerald-400' : s==='choppy' ? 'text-amber-400' : 'text-red-400';
const MKT_NAME = {a:'A股', hk:'港股'};
const MKT_ORDER = {a:0, hk:1};
regimes.sort((x,y) => (MKT_ORDER[x.market] ?? 9) - (MKT_ORDER[y.market] ?? 9));
const moodName = {bull:'偏多', bear:'偏空', neutral:'中性'}[mkt.mood] || '';
el.innerHTML = `
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-semibold">🌐 市场全景 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('market','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('market','ai')">§</span></span></span>
<span class="text-xs text-slate-500">${mkt.timestamp||''}${moodName?' · '+moodName:''}${mkt.up_ratio!=null?' · 上涨占比 '+mkt.up_ratio+'%':''}</span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
${regimes.length ? regimes.map(r => `
<div class="card p-4">
<div class="flex items-center gap-2 mb-2">
<span class="text-lg">${REG_ICON[r.regime]||'🌐'}</span>
<span class="text-sm font-semibold">${MKT_NAME[r.market]||r.market}</span>
<span class="text-sm font-bold ${regColor(r.regime)}">${REG_NAME[r.regime]||r.regime}</span>
<span class="text-xs text-slate-500 ml-auto">${r.date||''}</span>
</div>
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
<div class="flex justify-between"><span class="text-slate-500">指数收盘</span><span class="font-mono">${r.close!=null?r.close:'—'}</span></div>
<div class="flex justify-between"><span class="text-slate-500">站上MA20</span><span class="font-mono ${r.above_ma20?'text-emerald-400':'text-red-400'}">${r.above_ma20?'是':'否'}</span></div>
<div class="flex justify-between"><span class="text-slate-500">MA20斜率</span><span class="font-mono">${r.ma20_slope!=null?r.ma20_slope.toFixed(3):'—'}</span></div>
<div class="flex justify-between"><span class="text-slate-500">ROC</span><span class="font-mono ${(r.roc||0)>=0?'text-emerald-400':'text-red-400'}">${r.roc!=null?r.roc.toFixed(2):'—'}</span></div>
<div class="flex justify-between"><span class="text-slate-500">ADX</span><span class="font-mono">${r.adx!=null?r.adx.toFixed(1):'—'}</span></div>
</div>
</div>`).join('') : '<div class="card p-4 text-xs text-slate-500 lg:col-span-2">暂无温区数据(market_thermometer_daily 每日生成)</div>'}
</div>
<div class="card p-4 mb-4">
<h3 class="text-sm font-semibold mb-2">🏭 行业热点</h3>
${sectors.length ? sectors.slice(0,8).map(s => `
<div class="flex justify-between items-center p-1.5 ${(s.change_pct||0)>=0?'':'bg-slate-800/20'} rounded mb-0.5">
<span class="text-xs">${s.name}<span class="text-slate-500 ml-1">${s.lead_stock||''}</span></span>
<span class="text-xs font-mono ${(s.change_pct||0)>=0?'badge-up':'badge-down'}">${(s.change_pct||0)>=0?'+':''}${(s.change_pct||0).toFixed(1)}%</span>
</div>
`).join('') : '<div class="text-xs text-slate-500">暂无</div>'}
</div>
<div class="card p-4">
<h3 class="text-sm font-semibold mb-2">📋 全部板块排行(${sectors.length}</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-1 max-h-48 overflow-y-auto">
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-1 max-h-96 overflow-y-auto">
${sectors.map(s => `
<div class="flex justify-between p-1 bg-slate-800/20 rounded text-xs">
<span>${s.name}</span>
<span class="font-mono ${(s.change||0)>=0?'badge-up':'badge-down'}">${(s.change||0)>=0?'+':''}${(s.change||0).toFixed(1)}%</span>
<span class="font-mono ${(s.change_pct||0)>=0?'badge-up':'badge-down'}">${(s.change_pct||0)>=0?'+':''}${(s.change_pct||0).toFixed(1)}%</span>
</div>
`).join('')}
</div>
</div>
`;
}).catch(e => {
el.innerHTML = `<div class="text-red-400 text-sm">加载失败: ${e.message}</div>`;
});
}
@@ -1136,69 +1149,6 @@ function closePromptModal() {
document.getElementById('promptModal').classList.remove('flex');
}
// ── 信号页 ──
async function renderSignals() {
const el = document.getElementById('tab-signals');
el.innerHTML = '<div class="text-slate-400 text-sm">加载中…</div>';
try {
const [signals, candidates] = await Promise.all([
fetchJSON('/api/signals'),
fetchJSON('/api/candidates')
]);
let html = `
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-semibold">🔍 信号 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('signals','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('signals','ai')">§</span></span></span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
<div class="card p-4">
<div class="text-xs text-slate-500 mb-1">今日信号</div>
<div class="flex gap-4 text-sm">
<span class="text-blue-400">📡 趋势信号: ${signals.length || 0} 条</span>
<span class="text-green-400">🎯 候选股: ${candidates.length || 0} 只</span>
</div>
</div>
<div class="card p-4">
<div class="text-xs text-slate-500 mb-1">全市场扫描</div>
<div class="flex gap-4 text-sm">
<span class="text-slate-300">📋 候选池: ${candidates.length || 0} 只</span>
<span class="text-yellow-400">🏆 已入自选: ${candidates.filter(c => c.promoted).length || 0} 只</span>
</div>
</div>
</div>`;
if (!signals || signals.length === 0) {
html += '<div class="card p-6 text-center text-slate-500 text-sm">暂无信号</div>';
el.innerHTML = html;
return;
}
html += '<div class="space-y-3">';
for (const s of signals) {
const sev = s.severity || '-';
const sevColor = sev === 'high' ? 'text-red-400' : sev === 'medium' ? 'text-yellow-400' : 'text-green-400';
const sent = s.overall_sentiment || '-';
const sentColor = sent === '利好' ? 'text-green-400' : sent === '利空' ? 'text-red-400' : 'text-slate-400';
const source = s.source || 'trend';
const sourceIcon = source === 'market_scanner' ? '🎯' : source === 'macro' ? '🌐' : '📡';
html += `
<div class="card p-4">
<div class="flex items-center gap-2 mb-1">
<span class="${sevColor} text-xs font-semibold">${sev.toUpperCase()}</span>
<span class="${sentColor} text-sm font-semibold">${sent}</span>
<span class="text-slate-500 text-xs">${sourceIcon} ${source}</span>
<span class="text-slate-600 text-xs ml-auto">${s.created_at ? s.created_at.slice(5,16) : ''}</span>
</div>
<div class="text-sm text-slate-300">${s.sector || '-'}</div>
<div class="text-xs text-slate-500 mt-1">${(s.summary || '').slice(0,120)}</div>
</div>`;
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = `<div class="text-red-400 text-sm">加载失败: ${e.message}</div>`;
}
}
// ── Spec System ──
let _specCache = {};