diff --git a/server.py b/server.py index a56c909..acac27b 100644 --- a/server.py +++ b/server.py @@ -198,7 +198,59 @@ def api_market(): return jsonify(_load_json(DATA_DIR / "market.json", {})) -# ── 数据写入API(供 cron/update_data.py 调用) ────────── +# ── 信号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(): + """小果扫描统计""" + try: + from mofin_db import get_conn + conn = get_conn() + total = conn.execute("SELECT COUNT(*) FROM xiaoguo_scan_tracker").fetchone()[0] + found = conn.execute("SELECT COUNT(*) FROM xiaoguo_scan_tracker WHERE found_count>0").fetchone()[0] + recent = conn.execute(""" + SELECT code, name, last_scanned_at, found_count + FROM xiaoguo_scan_tracker + ORDER BY last_scanned_at DESC LIMIT 20 + """).fetchall() + source_count = conn.execute(""" + SELECT source, COUNT(*) as cnt FROM signal_news + WHERE datetime(created_at) > datetime('now', '-1 day') + GROUP BY source + """).fetchall() + conn.close() + return jsonify({ + "total_scanned": total, + "found_signals": found, + "recent": [dict(r) for r in recent], + "source_today": {r["source"]: r["cnt"] for r in source_count} + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +# ── 数据写入API ── @app.route("/api/update/portfolio", methods=["POST"]) def update_portfolio(): diff --git a/static/index.html b/static/index.html index 119a1ed..5e55ee0 100644 --- a/static/index.html +++ b/static/index.html @@ -50,6 +50,7 @@ body { background: #0f0f13; color: #e2e8f0; } + 📸 上传 @@ -62,6 +63,7 @@ body { background: #0f0f13; color: #e2e8f0; } + @@ -144,6 +146,7 @@ function renderTab(name) { else if (name === 'evaluation') renderEvaluation(); else if (name === 'prompts') renderPrompts(); else if (name === 'market') renderMarket(); + else if (name === 'signals') renderSignals(); } // ── Data Fetching ── @@ -1101,6 +1104,67 @@ function closePromptModal() { document.getElementById('promptModal').classList.add('hidden'); document.getElementById('promptModal').classList.remove('flex'); } + +// ── 信号页 ── +async function renderSignals() { + const el = document.getElementById('tab-signals'); + el.innerHTML = '
加载中…
'; + try { + const [signals, scan] = await Promise.all([ + fetchJSON('/api/signals'), + fetchJSON('/api/xiaoguo-scan') + ]); + const sourceToday = scan.source_today || {}; + let html = ` +
+
+
今日信号来源
+
+ 📡 趋势: ${sourceToday.trend || 0} + 🔍 小果扫描: ${sourceToday.xiaoguo || 0} +
+
+
+
小果扫描统计
+
+ 📋 累计扫描: ${scan.total_scanned || 0} 只 + 🏆 发现信号: ${scan.found_signals || 0} 只 +
+
+
`; + + if (!signals || signals.length === 0) { + html += '
暂无信号
'; + el.innerHTML = html; + return; + } + + html += '
'; + 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 === 'xiaoguo' ? '🔍' : '📡'; + html += ` +
+
+ ${sev.toUpperCase()} + ${sent} + ${sourceIcon} ${source} + ${s.created_at ? s.created_at.slice(5,16) : ''} +
+
${s.sector || '-'}
+
${(s.summary || '').slice(0,120)}
+
`; + } + html += '
'; + el.innerHTML = html; + } catch(e) { + el.innerHTML = `
加载失败: ${e.message}
`; + } +} \ No newline at end of file