重构: 移除信号Tab(与市场Tab重复+死数据), 市场Tab改为温区regime+板块排行, 候选池迁入选自Tab, /api/market加regimes, 删/api/signals, 修change_pct显示bug和market.json兜底
This commit is contained in:
@@ -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
@@ -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 = {};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user