v1.6.0: 目标跟踪(概念/主题/股票) - watch_targets表+概念智能体跟踪(关键词检索+受益个股+影响度判定+邮件通知), 自动化页加持仓管理和目标管理, 报告支持股票/概念双类型
This commit is contained in:
@@ -689,18 +689,18 @@ def api_tracking_detail(rid):
|
|||||||
|
|
||||||
@app.route("/api/tracking/run", methods=["POST"])
|
@app.route("/api/tracking/run", methods=["POST"])
|
||||||
def api_tracking_run():
|
def api_tracking_run():
|
||||||
"""立即跟踪全部自选股(后台)"""
|
"""立即跟踪全部目标(持仓+概念/主题/股票,后台)"""
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
from engine.agent import track_watchlist
|
from engine.agent import track_all
|
||||||
try:
|
try:
|
||||||
track_watchlist()
|
track_all()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("track run fail: %s", e)
|
log.error("track run fail: %s", e)
|
||||||
|
|
||||||
threading.Thread(target=run, daemon=True).start()
|
threading.Thread(target=run, daemon=True).start()
|
||||||
return jsonify({"ok": True, "msg": "跟踪任务已启动(逐只分析,约每只30-60秒)"})
|
return jsonify({"ok": True, "msg": "跟踪任务已启动(逐目标分析,约每只30-60秒)"})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/tracking/run/<code>", methods=["POST"])
|
@app.route("/api/tracking/run/<code>", methods=["POST"])
|
||||||
@@ -718,6 +718,29 @@ def api_tracking_run_one(code):
|
|||||||
return jsonify({"ok": True, "msg": "跟踪已启动"})
|
return jsonify({"ok": True, "msg": "跟踪已启动"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/targets")
|
||||||
|
def api_targets_list():
|
||||||
|
from engine.agent import list_targets
|
||||||
|
return jsonify({"items": list_targets()})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/targets", methods=["POST"])
|
||||||
|
def api_targets_add():
|
||||||
|
from engine.agent import add_target
|
||||||
|
body = request.get_json(silent=True) or {}
|
||||||
|
r = add_target(body.get("type", "concept"), body.get("name", ""),
|
||||||
|
body.get("code", ""), body.get("keywords", ""))
|
||||||
|
if r.get("error"):
|
||||||
|
return jsonify(r), 400
|
||||||
|
return jsonify(r)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/targets/<int:tid>", methods=["DELETE"])
|
||||||
|
def api_targets_delete(tid):
|
||||||
|
from engine.agent import delete_target
|
||||||
|
return jsonify(delete_target(tid))
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ 数据管理
|
# ------------------------------------------------------------------ 数据管理
|
||||||
@app.route("/api/admin/stats")
|
@app.route("/api/admin/stats")
|
||||||
def api_admin_stats():
|
def api_admin_stats():
|
||||||
|
|||||||
+18
-2
@@ -142,10 +142,11 @@ CREATE TABLE IF NOT EXISTS notification_log (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tracking_reports (
|
CREATE TABLE IF NOT EXISTS tracking_reports (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
target_type TEXT DEFAULT 'stock', -- stock(股票) / concept(概念主题)
|
||||||
code TEXT NOT NULL,
|
code TEXT NOT NULL,
|
||||||
stock_name TEXT DEFAULT '',
|
stock_name TEXT DEFAULT '',
|
||||||
industry TEXT DEFAULT '',
|
industry TEXT DEFAULT '',
|
||||||
report TEXT DEFAULT '', -- 产业链深度分析(markdown)
|
report TEXT DEFAULT '', -- 产业链/概念深度分析(markdown)
|
||||||
meta TEXT DEFAULT '{}', -- JSON:significance/impact_score/summary/新闻统计
|
meta TEXT DEFAULT '{}', -- JSON:significance/impact_score/summary/新闻统计
|
||||||
sources TEXT DEFAULT '{}', -- JSON:个股/上游/下游/同业 采集的资讯 + 指标 + 评级
|
sources TEXT DEFAULT '{}', -- JSON:个股/上游/下游/同业 采集的资讯 + 指标 + 评级
|
||||||
status TEXT DEFAULT 'done',
|
status TEXT DEFAULT 'done',
|
||||||
@@ -153,6 +154,16 @@ CREATE TABLE IF NOT EXISTS tracking_reports (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tracking_code ON tracking_reports(code);
|
CREATE INDEX IF NOT EXISTS idx_tracking_code ON tracking_reports(code);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS watch_targets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
type TEXT DEFAULT 'concept', -- concept(概念/主题) / stock(股票)
|
||||||
|
code TEXT DEFAULT '', -- stock 时的股票代码
|
||||||
|
name TEXT NOT NULL, -- 概念/主题名 或 股票名
|
||||||
|
keywords TEXT DEFAULT '', -- concept 的检索关键词(逗号分隔)
|
||||||
|
enabled INTEGER DEFAULT 1,
|
||||||
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS market_index (
|
CREATE TABLE IF NOT EXISTS market_index (
|
||||||
date TEXT PRIMARY KEY,
|
date TEXT PRIMARY KEY,
|
||||||
sh REAL DEFAULT 0, -- 上证指数(点)
|
sh REAL DEFAULT 0, -- 上证指数(点)
|
||||||
@@ -187,6 +198,10 @@ def db():
|
|||||||
def init_db():
|
def init_db():
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
conn.executescript(SCHEMA)
|
conn.executescript(SCHEMA)
|
||||||
|
# 迁移:老库补列
|
||||||
|
cols = {r[1] for r in conn.execute("PRAGMA table_info(tracking_reports)")}
|
||||||
|
if "target_type" not in cols:
|
||||||
|
conn.execute("ALTER TABLE tracking_reports ADD COLUMN target_type TEXT DEFAULT 'stock'")
|
||||||
|
|
||||||
|
|
||||||
def query(sql, args=()):
|
def query(sql, args=()):
|
||||||
@@ -219,7 +234,8 @@ def wipe_all():
|
|||||||
"""清空业务表(保留结构)+ 重置自增序列,用于重灌数据"""
|
"""清空业务表(保留结构)+ 重置自增序列,用于重灌数据"""
|
||||||
for t in ("stock_daily", "inst_ratings", "fund_holdings", "news",
|
for t in ("stock_daily", "inst_ratings", "fund_holdings", "news",
|
||||||
"institutions", "stocks", "watchlist", "analysis_cache", "analysis_history",
|
"institutions", "stocks", "watchlist", "analysis_cache", "analysis_history",
|
||||||
"market_index", "strategy_backtests", "notification_log", "tracking_reports"):
|
"market_index", "strategy_backtests", "notification_log", "tracking_reports",
|
||||||
|
"watch_targets"):
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
conn.execute(f'DELETE FROM "{t}"')
|
conn.execute(f'DELETE FROM "{t}"')
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
|
|||||||
+166
-25
@@ -261,8 +261,8 @@ def track_stock(code, focus=""):
|
|||||||
"ratings": seg["ratings"], "holdings": seg["holdings"],
|
"ratings": seg["ratings"], "holdings": seg["holdings"],
|
||||||
}
|
}
|
||||||
execute(
|
execute(
|
||||||
"INSERT INTO tracking_reports(code, stock_name, industry, report, meta, sources, status, created_at) "
|
"INSERT INTO tracking_reports(target_type, code, stock_name, industry, report, meta, sources, status, created_at) "
|
||||||
"VALUES(?,?,?,?,?,?,'done',datetime('now','localtime'))",
|
"VALUES('stock',?,?,?,?,?,?,'done',datetime('now','localtime'))",
|
||||||
(code, s["name"], s["industry"], clean_report, json.dumps(meta, ensure_ascii=False),
|
(code, s["name"], s["industry"], clean_report, json.dumps(meta, ensure_ascii=False),
|
||||||
json.dumps(sources, ensure_ascii=False)))
|
json.dumps(sources, ensure_ascii=False)))
|
||||||
rid = query_one("SELECT MAX(id) id FROM tracking_reports")["id"]
|
rid = query_one("SELECT MAX(id) id FROM tracking_reports")["id"]
|
||||||
@@ -287,60 +287,201 @@ def _first_heading(text):
|
|||||||
|
|
||||||
|
|
||||||
def _notify_if_significant(rid, stock, meta):
|
def _notify_if_significant(rid, stock, meta):
|
||||||
"""影响度达阈值且开启通知 → 邮件"""
|
"""影响度达阈值且开启通知 → 邮件(股票/概念通用)"""
|
||||||
cfg = tracking_config()
|
cfg = tracking_config()
|
||||||
try:
|
try:
|
||||||
if cfg["notify"] and int(meta["impact_score"]) >= int(cfg["impact_threshold"]):
|
if cfg["notify"] and int(meta.get("impact_score", 0)) >= int(cfg["impact_threshold"]):
|
||||||
from engine.notifier import send_email
|
from engine.notifier import send_email
|
||||||
mc = mail_config()
|
mc = mail_config()
|
||||||
|
nc = meta.get("news_counts") or {}
|
||||||
|
detail = (f"个股资讯 {nc.get('direct', 0)} 条 / 上游 {nc.get('upstream', 0)} 条 / "
|
||||||
|
f"下游 {nc.get('downstream', 0)} 条 / 同业 {nc.get('peers', 0)} 条"
|
||||||
|
if "upstream" in nc else
|
||||||
|
f"相关资讯 {nc.get('direct', 0)} 条 / 语义检索 {nc.get('rag', 0)} 条 / "
|
||||||
|
f"受益个股 {nc.get('related', 0)} 只")
|
||||||
send_email(
|
send_email(
|
||||||
f"[持仓跟踪] {stock['name']} 出现{meta.get('change_kind','')}动态(影响度{meta['impact_score']})",
|
f"[持仓跟踪] {stock['name']} 出现{meta.get('change_kind', '')}动态(影响度{meta.get('impact_score', 0)})",
|
||||||
f"""<html><body style="font-family:Microsoft YaHei;padding:20px;background:#f5f6f8;">
|
f"""<html><body style="font-family:Microsoft YaHei;padding:20px;background:#f5f6f8;">
|
||||||
<div style="max-width:640px;margin:auto;background:#fff;border-radius:8px;border:1px solid #e5e7eb;overflow:hidden;">
|
<div style="max-width:640px;margin:auto;background:#fff;border-radius:8px;border:1px solid #e5e7eb;overflow:hidden;">
|
||||||
<div style="background:#1e293b;color:#fff;padding:14px 20px;font-size:17px;font-weight:bold;">🧭 持仓跟踪 · {stock['name']}({stock['code']})</div>
|
<div style="background:#1e293b;color:#fff;padding:14px 20px;font-size:17px;font-weight:bold;">🧭 智能体跟踪 · {stock['name']}({stock['code']})</div>
|
||||||
<div style="padding:16px 20px;">
|
<div style="padding:16px 20px;">
|
||||||
<p><b>影响度:</b>{meta['impact_score']}/100({'🔴 重大' if meta['impact_score']>=65 else '🟡 关注'})<br>
|
<p><b>影响度:</b>{meta.get('impact_score', 0)}/100({'🔴 重大' if int(meta.get('impact_score', 0)) >= 65 else '🟡 关注'})<br>
|
||||||
<b>性质:</b>{meta.get('change_kind','')} | <b>显著性:</b>{meta.get('significance','')}</p>
|
<b>性质:</b>{meta.get('change_kind', '')} | <b>显著性:</b>{meta.get('significance', '')}</p>
|
||||||
<p style="font-size:15px;"><b>摘要:</b>{meta.get('summary','')}</p>
|
<p style="font-size:15px;"><b>摘要:</b>{meta.get('summary', '')}</p>
|
||||||
<p style="color:#555;"><b>产业链趋势:</b>{meta.get('chain_trend','')}</p>
|
<p style="color:#555;"><b>趋势判断:</b>{meta.get('chain_trend', '')}</p>
|
||||||
<p style="color:#888;font-size:12px;">个股资讯 {meta['news_counts']['direct']} 条 / 上游 {meta['news_counts']['upstream']} 条 / 下游 {meta['news_counts']['downstream']} 条 / 同业 {meta['news_counts']['peers']} 条</p>
|
<p style="color:#888;font-size:12px;">{detail}</p>
|
||||||
</div></div></body></html>""",
|
</div></div></body></html>""",
|
||||||
cfg=mc)
|
cfg=mc)
|
||||||
set_tracking_state(last_alert=int(meta["impact_score"]))
|
set_tracking_state(last_alert=int(meta.get("impact_score", 0)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning("track notify fail: %s", e)
|
log.warning("track notify fail: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================== 概念/主题跟踪
|
||||||
|
def track_concept(name, keywords="", focus=""):
|
||||||
|
"""跟踪一个概念/主题:采集相关新闻 → 受益个股梳理 → LLM 深度分析 → 影响度判定"""
|
||||||
|
kw = keywords.strip() or name
|
||||||
|
kw_list = [k.strip() for k in kw.replace(",", ",").split(",") if k.strip()]
|
||||||
|
kws = " ".join(kw_list)
|
||||||
|
# 1) 采集:DB 关键词检索 + RAG 向量检索
|
||||||
|
conds, args = [], []
|
||||||
|
for k in kw_list[:5]:
|
||||||
|
conds.append("(title LIKE ? OR content LIKE ?)")
|
||||||
|
args += [f"%{k}%", f"%{k}%"]
|
||||||
|
args.append(15)
|
||||||
|
db_news = query(
|
||||||
|
f"SELECT id,title,content,source,category,publish_date,sentiment,related_stocks FROM news "
|
||||||
|
f"WHERE ({' OR '.join(conds)}) AND publish_date >= date('now','-45 day') "
|
||||||
|
f"ORDER BY publish_date DESC LIMIT ?", args)
|
||||||
|
rag_hits = _rag_news_text(f"{name} 概念主题 {kws} 最新动态 政策 催化", top_k=8)
|
||||||
|
db_text = _fmt_news(db_news)
|
||||||
|
rag_text = "\n".join(rag_hits) or " (暂无)"
|
||||||
|
# 2) 受益个股:从 DB 新闻 related_stocks + RAG 命中元数据 code 汇总
|
||||||
|
codes = set()
|
||||||
|
for n in db_news:
|
||||||
|
for c in (n["related_stocks"] or "").split(","):
|
||||||
|
if c:
|
||||||
|
codes.add(c)
|
||||||
|
try:
|
||||||
|
for h in query_vectors(f"{name} {kws} 受益个股", n_results=8, name=CHROMA_NEWS_COLLECTION):
|
||||||
|
c = (h.get("metadata") or {}).get("code")
|
||||||
|
if c:
|
||||||
|
codes.add(c)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
related = query("SELECT code, name, industry FROM stocks WHERE code IN (%s)" %
|
||||||
|
",".join(["?"] * len(codes)), tuple(codes)) if codes else []
|
||||||
|
related_txt = "、".join(f"{r['name']}({r['code']},{r['industry']})" for r in related) or "暂无"
|
||||||
|
# 3) 提示词
|
||||||
|
prompt = f"""你是资深题材/概念跟踪分析师,正在深度跟踪【{name}】这一概念主题。
|
||||||
|
|
||||||
|
【概念/主题】{name}
|
||||||
|
【检索关键词】{kws}
|
||||||
|
|
||||||
|
【最新相关资讯】
|
||||||
|
{db_text}
|
||||||
|
{rag_text}
|
||||||
|
|
||||||
|
【关联受益个股】{related_txt}
|
||||||
|
|
||||||
|
【输出要求】
|
||||||
|
第一步,先输出一个 json 代码块(必须最先输出):
|
||||||
|
```json
|
||||||
|
{{"significance":"high|medium|low","impact_score":0到100的整数,"change_kind":"利好/利空/中性/震荡","summary":"一句话总结","chain_trend":"题材趋势判断"}}
|
||||||
|
```
|
||||||
|
第二步,输出 Markdown 分析报告,结构如下:
|
||||||
|
## 一、概念主题最新动态
|
||||||
|
## 二、核心驱动与催化(政策/产业/事件)
|
||||||
|
## 三、受益标的梳理(关联个股及逻辑)
|
||||||
|
## 四、市场情绪与资金动向
|
||||||
|
## 五、风险提示
|
||||||
|
## 关注要点(3-5条)
|
||||||
|
|
||||||
|
分析须严格基于资讯,避免编造。impact_score >=65 视为重大变化。"""
|
||||||
|
try:
|
||||||
|
reply = llm_chat([
|
||||||
|
{"role": "system", "content": "你是一名严谨专业的题材与概念跟踪分析师。"},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
]).strip()
|
||||||
|
if not reply:
|
||||||
|
raise RuntimeError("LLM 返回为空")
|
||||||
|
judge = parse_judge(reply)
|
||||||
|
clean_report = strip_json_block(reply)
|
||||||
|
summary = (judge or {}).get("summary") or _first_heading(clean_report)
|
||||||
|
meta = {
|
||||||
|
"significance": (judge or {}).get("significance", "medium"),
|
||||||
|
"impact_score": int((judge or {}).get("impact_score", 50)),
|
||||||
|
"change_kind": (judge or {}).get("change_kind", "中性"),
|
||||||
|
"summary": summary,
|
||||||
|
"chain_trend": (judge or {}).get("chain_trend", ""),
|
||||||
|
"news_counts": {"direct": len(db_news), "rag": len(rag_hits), "related": len(related)},
|
||||||
|
"keywords": kws,
|
||||||
|
}
|
||||||
|
sources = {"keywords": kws, "db_news": db_text, "rag_news": rag_text,
|
||||||
|
"related": related_txt}
|
||||||
|
cid = f"CONCEPT:{name}"
|
||||||
|
execute(
|
||||||
|
"INSERT INTO tracking_reports(target_type, code, stock_name, industry, report, meta, sources, status, created_at) "
|
||||||
|
"VALUES('concept',?,?,?,?,?,?,'done',datetime('now','localtime'))",
|
||||||
|
(cid, name, "概念/主题", clean_report, json.dumps(meta, ensure_ascii=False),
|
||||||
|
json.dumps(sources, ensure_ascii=False)))
|
||||||
|
rid = query_one("SELECT MAX(id) id FROM tracking_reports")["id"]
|
||||||
|
_notify_if_significant(rid, {"name": name, "code": cid}, meta)
|
||||||
|
return {"ok": True, "report_id": rid, "meta": meta}
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("track concept %s fail", name)
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================== 目标管理
|
||||||
|
def list_targets():
|
||||||
|
return query("SELECT * FROM watch_targets ORDER BY type, id")
|
||||||
|
|
||||||
|
|
||||||
|
def add_target(ttype, name, code="", keywords=""):
|
||||||
|
name = name.strip()
|
||||||
|
if not name:
|
||||||
|
return {"error": "名称不能为空"}
|
||||||
|
execute("INSERT INTO watch_targets(type, code, name, keywords) VALUES(?,?,?,?)",
|
||||||
|
(ttype, code, name, keywords))
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
def delete_target(tid):
|
||||||
|
execute("DELETE FROM watch_targets WHERE id=?", (tid,))
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
# ===================================================================== 批量与调度
|
# ===================================================================== 批量与调度
|
||||||
def track_watchlist(progress=None):
|
def track_all(progress=None):
|
||||||
"""串行跟踪自选股(持仓)。同一时刻只允许一个跟踪任务(防重复)"""
|
"""跟踪全部目标:持仓(自选股)+ 目标(概念/主题/股票)"""
|
||||||
global _cycle_running
|
global _cycle_running
|
||||||
if _cycle_running:
|
if _cycle_running:
|
||||||
return {"tracked": 0, "msg": "已有跟踪任务进行中,请稍后再试"}
|
return {"tracked": 0, "msg": "已有跟踪任务进行中,请稍后再试"}
|
||||||
_cycle_running = True
|
_cycle_running = True
|
||||||
try:
|
try:
|
||||||
stocks = query("SELECT w.code, s.name FROM watchlist w JOIN stocks s ON s.code=w.code ORDER BY w.added_at")
|
targets = []
|
||||||
if not stocks:
|
# 持仓(自选股)
|
||||||
return {"tracked": 0, "msg": "自选股为空,请先在股票池添加"}
|
for w in query("SELECT w.code, s.name FROM watchlist w JOIN stocks s ON s.code=w.code ORDER BY w.added_at"):
|
||||||
|
targets.append(("stock", w["code"], w["name"]))
|
||||||
|
# 目标(股票/概念)
|
||||||
|
for t in query("SELECT id, type, code, name, keywords FROM watch_targets WHERE enabled=1 ORDER BY id"):
|
||||||
|
if t["type"] == "stock" and t["code"]:
|
||||||
|
targets.append(("stock", t["code"], t["name"]))
|
||||||
|
else:
|
||||||
|
targets.append(("concept", t["name"], t["keywords"] or t["name"]))
|
||||||
|
# 去重
|
||||||
|
seen, uniq = set(), []
|
||||||
|
for typ, key, name in targets:
|
||||||
|
u = (typ, key)
|
||||||
|
if u in seen:
|
||||||
|
continue
|
||||||
|
seen.add(u)
|
||||||
|
uniq.append((typ, key, name))
|
||||||
|
if not uniq:
|
||||||
|
return {"tracked": 0, "msg": "暂无跟踪目标:请添加持仓/自选股或概念主题目标"}
|
||||||
results = []
|
results = []
|
||||||
for i, st in enumerate(stocks):
|
for i, (typ, key, name) in enumerate(uniq):
|
||||||
r = track_stock(st["code"])
|
if typ == "stock":
|
||||||
results.append({"code": st["code"], "name": st["name"], **r})
|
r = track_stock(key)
|
||||||
set_tracking_state(last_run=time.strftime("%Y-%m-%d %H:%M:%S"), last_stock=st["name"])
|
else:
|
||||||
|
r = track_concept(key, name)
|
||||||
|
results.append({"type": typ, "name": name, **r})
|
||||||
|
set_tracking_state(last_run=time.strftime("%Y-%m-%d %H:%M:%S"), last_stock=name)
|
||||||
if progress:
|
if progress:
|
||||||
progress(i + 1, len(stocks))
|
progress(i + 1, len(uniq))
|
||||||
return {"tracked": len(results), "results": results}
|
return {"tracked": len(results), "results": results}
|
||||||
finally:
|
finally:
|
||||||
_cycle_running = False
|
_cycle_running = False
|
||||||
|
|
||||||
|
|
||||||
def latest_reports(code, limit=5):
|
def latest_reports(code, limit=5):
|
||||||
return query("SELECT id, code, stock_name, industry, meta, status, created_at "
|
return query("SELECT id, target_type, code, stock_name, industry, meta, status, created_at "
|
||||||
"FROM tracking_reports WHERE code=? ORDER BY id DESC LIMIT ?", (code, limit))
|
"FROM tracking_reports WHERE code=? ORDER BY id DESC LIMIT ?", (code, limit))
|
||||||
|
|
||||||
|
|
||||||
def list_reports(limit=30):
|
def list_reports(limit=30):
|
||||||
return query("SELECT id, code, stock_name, industry, meta, status, created_at "
|
return query("SELECT id, target_type, code, stock_name, industry, meta, status, created_at "
|
||||||
"FROM tracking_reports ORDER BY id DESC LIMIT ?", (limit,))
|
"FROM tracking_reports ORDER BY id DESC LIMIT ?", (limit,))
|
||||||
|
|
||||||
|
|
||||||
@@ -371,7 +512,7 @@ class TrackingThread(threading.Thread):
|
|||||||
continue
|
continue
|
||||||
if cfg["enabled"]:
|
if cfg["enabled"]:
|
||||||
try:
|
try:
|
||||||
r = track_watchlist()
|
r = track_all()
|
||||||
log.info("tracking cycle: %s", r)
|
log.info("tracking cycle: %s", r)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning("tracking cycle error: %s", e)
|
log.warning("tracking cycle error: %s", e)
|
||||||
|
|||||||
+120
-15
@@ -29,6 +29,8 @@ async function loadAuto() {
|
|||||||
renderTrackingState(tr);
|
renderTrackingState(tr);
|
||||||
loadLog();
|
loadLog();
|
||||||
loadTrackReports();
|
loadTrackReports();
|
||||||
|
loadPositions();
|
||||||
|
loadTargets();
|
||||||
} catch (e) { toast('加载失败'); }
|
} catch (e) { toast('加载失败'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +104,95 @@ async function runTrackAll() {
|
|||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===================== 持仓(自选股)管理 ===================== */
|
||||||
|
async function loadPositions() {
|
||||||
|
try {
|
||||||
|
const d = await api('/api/watchlist');
|
||||||
|
const items = d.items || [];
|
||||||
|
if (!items.length) {
|
||||||
|
$('#posManage').innerHTML = '<div class="empty">暂无持仓。在下方搜索添加,或到 <a href="/stocks">股票池</a> / 个股详情页 ⭐ 添加</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$('#posManage').innerHTML = `<table>
|
||||||
|
<tr><th>股票</th><th>现价</th><th>今日</th><th>评分</th><th>操作</th></tr>
|
||||||
|
${items.map(p => `<tr>
|
||||||
|
<td><a href="/stock/${p.code}" target="_blank"><b>${p.name}</b></a><div style="color:var(--muted);font-size:12px">${p.code} · ${p.industry}</div></td>
|
||||||
|
<td class="num">${p.close}</td>
|
||||||
|
<td class="num ${pctClass(p.change_pct)}">${fmtPct(p.change_pct)}</td>
|
||||||
|
<td class="num">${p.score}</td>
|
||||||
|
<td><button class="btn btn-danger" onclick="removePos('${p.code}')">移除</button></td>
|
||||||
|
</tr>`).join('')}
|
||||||
|
</table>`;
|
||||||
|
} catch (e) { $('#posManage').innerHTML = '<div class="empty">加载失败</div>'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addPos() {
|
||||||
|
const kw = $('#posSearch').value.trim();
|
||||||
|
if (!kw) return;
|
||||||
|
try {
|
||||||
|
const d = await api('/api/stocks?keyword=' + encodeURIComponent(kw) + '&per=5');
|
||||||
|
const items = d.items || [];
|
||||||
|
if (!items.length) { toast('未找到该股票'); return; }
|
||||||
|
// 精确匹配或取第一个
|
||||||
|
const hit = items.find(x => x.code === kw || x.name === kw) || items[0];
|
||||||
|
await api('/api/watchlist/' + hit.code, { method: 'POST' });
|
||||||
|
toast(`已添加 ${hit.name} 为持仓`);
|
||||||
|
$('#posSearch').value = '';
|
||||||
|
loadPositions();
|
||||||
|
} catch (e) { toast('添加失败'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removePos(code) {
|
||||||
|
try {
|
||||||
|
await api('/api/watchlist/' + code, { method: 'DELETE' });
|
||||||
|
toast('已移除持仓');
|
||||||
|
loadPositions();
|
||||||
|
} catch (e) { toast('操作失败'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 概念/主题/股票目标管理 ===================== */
|
||||||
|
async function loadTargets() {
|
||||||
|
try {
|
||||||
|
const d = await api('/api/targets');
|
||||||
|
const items = d.items || [];
|
||||||
|
if (!items.length) {
|
||||||
|
$('#tgtManage').innerHTML = '<div class="empty">暂无跟踪目标,添加概念/主题(如 AI算力、低空经济)或个股目标</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$('#tgtManage').innerHTML = `<table>
|
||||||
|
<tr><th>类型</th><th>目标</th><th>检索关键词</th><th>状态</th><th>操作</th></tr>
|
||||||
|
${items.map(t => `<tr>
|
||||||
|
<td><span class="cat-tag ${t.type === 'concept' ? '行业' : '公司'}">${t.type === 'concept' ? '概念/主题' : '股票'}</span></td>
|
||||||
|
<td><b>${escapeHtml(t.name)}</b>${t.code ? `<div style="color:var(--muted);font-size:12px">${t.code}</div>` : ''}</td>
|
||||||
|
<td style="color:var(--text2)">${escapeHtml(t.keywords || '—')}</td>
|
||||||
|
<td>${t.enabled ? '<span class="tag tag-推荐">跟踪中</span>' : '<span class="tag tag-观望">停用</span>'}</td>
|
||||||
|
<td><button class="btn btn-danger" onclick="removeTarget(${t.id})">移除</button></td>
|
||||||
|
</tr>`).join('')}
|
||||||
|
</table>`;
|
||||||
|
} catch (e) { $('#tgtManage').innerHTML = '<div class="empty">加载失败</div>'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addTarget() {
|
||||||
|
const type = $('#tgtType').value;
|
||||||
|
const name = $('#tgtName').value.trim();
|
||||||
|
const keywords = $('#tgtKeywords').value.trim();
|
||||||
|
if (!name) { toast('请输入目标名称'); return; }
|
||||||
|
try {
|
||||||
|
await api('/api/targets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, name, keywords }) });
|
||||||
|
toast('已添加跟踪目标');
|
||||||
|
$('#tgtName').value = ''; $('#tgtKeywords').value = '';
|
||||||
|
loadTargets();
|
||||||
|
} catch (e) { toast('添加失败:' + e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTarget(id) {
|
||||||
|
try {
|
||||||
|
await api('/api/targets/' + id, { method: 'DELETE' });
|
||||||
|
toast('已移除目标');
|
||||||
|
loadTargets();
|
||||||
|
} catch (e) { toast('操作失败'); }
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== 日志与报告 ===================== */
|
/* ===================== 日志与报告 ===================== */
|
||||||
async function loadLog() {
|
async function loadLog() {
|
||||||
try {
|
try {
|
||||||
@@ -129,12 +220,14 @@ async function loadTrackReports() {
|
|||||||
let meta = {};
|
let meta = {};
|
||||||
try { meta = JSON.parse(r.meta || '{}'); } catch (e) {}
|
try { meta = JSON.parse(r.meta || '{}'); } catch (e) {}
|
||||||
const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注';
|
const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注';
|
||||||
|
const isConcept = r.target_type === 'concept';
|
||||||
return `<tr class="row-link" onclick="showTrack(${r.id})">
|
return `<tr class="row-link" onclick="showTrack(${r.id})">
|
||||||
<td style="color:var(--muted)">${r.created_at}</td>
|
<td style="color:var(--muted)">${r.created_at}</td>
|
||||||
<td><b>${r.stock_name}</b><div style="color:var(--muted);font-size:12px">${r.code}</div></td>
|
<td><span class="cat-tag ${isConcept ? '行业' : '公司'}">${isConcept ? '概念' : '股票'}</span></td>
|
||||||
|
<td><b>${r.stock_name}</b>${isConcept ? '' : `<div style="color:var(--muted);font-size:12px">${r.code}</div>`}</td>
|
||||||
<td><span class="tag ${sigCls}">${meta.impact_score || '—'}</span></td>
|
<td><span class="tag ${sigCls}">${meta.impact_score || '—'}</span></td>
|
||||||
<td><span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span></td>
|
<td><span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span></td>
|
||||||
<td style="max-width:320px;white-space:normal;color:var(--text2)">${escapeHtml(meta.summary || '')}</td>
|
<td style="max-width:300px;white-space:normal;color:var(--text2)">${escapeHtml(meta.summary || '')}</td>
|
||||||
<td><button class="btn" onclick="event.stopPropagation();showTrack(${r.id})">查看 ↗</button></td>
|
<td><button class="btn" onclick="event.stopPropagation();showTrack(${r.id})">查看 ↗</button></td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -146,16 +239,31 @@ async function showTrack(id) {
|
|||||||
const d = await api('/api/tracking/' + id);
|
const d = await api('/api/tracking/' + id);
|
||||||
const meta = d.meta || {}, src = d.sources || {};
|
const meta = d.meta || {}, src = d.sources || {};
|
||||||
const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注';
|
const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注';
|
||||||
const newsBlock = (label, icon, seg) => {
|
const isConcept = d.target_type === 'concept';
|
||||||
if (!seg) return '';
|
// 概念报告:展示关键词/资讯/受益个股;股票报告:四环节数据源
|
||||||
const txt = ((seg.db || '') + (seg.rag || '')).trim();
|
let sourceHtml = '';
|
||||||
return `<details><summary>${icon} ${label} <span class="src-count">${txt.includes('[') ? txt.split('[').length - 1 : 0}</span></summary>
|
if (isConcept) {
|
||||||
<div class="src-body"><pre class="src-pre">${escapeHtml(txt || '暂无')}</pre></div></details>`;
|
const dc = (meta.news_counts || {}).direct || 0, rc = (meta.news_counts || {}).rag || 0, nc = (meta.news_counts || {}).related || 0;
|
||||||
};
|
sourceHtml = `
|
||||||
|
<details open><summary>🔎 相关资讯(${dc} 条)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.db_news || '')}</pre></div></details>
|
||||||
|
<details><summary>🧠 语义检索命中(${rc} 条)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.rag_news || '')}</pre></div></details>
|
||||||
|
<details><summary>🏢 受益个股(${nc} 只)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.related || '')}</pre></div></details>`;
|
||||||
|
} else {
|
||||||
|
const newsBlock = (label, icon, seg) => {
|
||||||
|
if (!seg) return '';
|
||||||
|
const txt = ((seg.db || '') + (seg.rag || '')).trim();
|
||||||
|
return `<details><summary>${icon} ${label} <span class="src-count">${txt.includes('[') ? txt.split('[').length - 1 : 0}</span></summary>
|
||||||
|
<div class="src-body"><pre class="src-pre">${escapeHtml(txt || '暂无')}</pre></div></details>`;
|
||||||
|
};
|
||||||
|
sourceHtml = newsBlock('个股直接动态', '📄', src.direct) +
|
||||||
|
newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream) +
|
||||||
|
newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream) +
|
||||||
|
newsBlock('同业竞争', '🏢', src.peers);
|
||||||
|
}
|
||||||
openModal(`
|
openModal(`
|
||||||
<h3>🧭 产业链跟踪:${d.stock_name}(${d.code})</h3>
|
<h3>${isConcept ? '🎯 概念主题跟踪' : '🧭 产业链跟踪'}:${d.stock_name}${d.code && !isConcept ? '(' + d.code + ')' : ''}</h3>
|
||||||
<div class="news-meta" style="margin-bottom:12px">
|
<div class="news-meta" style="margin-bottom:12px">
|
||||||
<span class="cat-tag">${escapeHtml(d.industry)}</span>
|
<span class="cat-tag ${isConcept ? '行业' : '公司'}">${isConcept ? '概念/主题' : d.industry}</span>
|
||||||
<span class="tag ${sigCls}">影响度 ${meta.impact_score || '—'}/100</span>
|
<span class="tag ${sigCls}">影响度 ${meta.impact_score || '—'}/100</span>
|
||||||
<span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span>
|
<span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span>
|
||||||
<span style="color:var(--muted)">${d.created_at}</span>
|
<span style="color:var(--muted)">${d.created_at}</span>
|
||||||
@@ -163,14 +271,11 @@ async function showTrack(id) {
|
|||||||
<div class="markdown-body">${mdRender(d.report)}</div>
|
<div class="markdown-body">${mdRender(d.report)}</div>
|
||||||
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:10px">
|
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:10px">
|
||||||
<b style="color:var(--gold)">数据源(智能体参考内容)</b>
|
<b style="color:var(--gold)">数据源(智能体参考内容)</b>
|
||||||
${newsBlock('个股直接动态', '📄', src.direct)}
|
${sourceHtml}
|
||||||
${newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream)}
|
|
||||||
${newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream)}
|
|
||||||
${newsBlock('同业竞争', '🏢', src.peers)}
|
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:14px;text-align:right">
|
<div style="margin-top:14px;text-align:right">
|
||||||
<button class="btn" onclick="document.querySelector('.modal-mask').classList.remove('show')">关闭</button>
|
<button class="btn" onclick="document.querySelector('.modal-mask').classList.remove('show')">关闭</button>
|
||||||
<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>
|
${isConcept ? '' : `<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>`}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
} catch (e) { toast('加载失败'); }
|
} catch (e) { toast('加载失败'); }
|
||||||
|
|||||||
+22
-9
@@ -68,9 +68,11 @@ function renderRecords(items) {
|
|||||||
let meta = {};
|
let meta = {};
|
||||||
try { meta = JSON.parse(r.meta || '{}'); } catch (e) {}
|
try { meta = JSON.parse(r.meta || '{}'); } catch (e) {}
|
||||||
const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注';
|
const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注';
|
||||||
|
const isConcept = r.target_type === 'concept';
|
||||||
return `<tr class="row-link" onclick="showDetail(${r.id})">
|
return `<tr class="row-link" onclick="showDetail(${r.id})">
|
||||||
<td style="color:var(--muted)">${r.created_at}</td>
|
<td style="color:var(--muted)">${r.created_at}</td>
|
||||||
<td><b>${r.stock_name}</b><div style="color:var(--muted);font-size:12px">${r.code}</div></td>
|
<td><span class="cat-tag ${isConcept ? '行业' : '公司'}">${isConcept ? '概念/主题' : '股票'}</span></td>
|
||||||
|
<td><b>${r.stock_name}</b>${isConcept ? '' : `<div style="color:var(--muted);font-size:12px">${r.code}</div>`}</td>
|
||||||
<td style="color:var(--text2)">${escapeHtml(r.industry)}</td>
|
<td style="color:var(--text2)">${escapeHtml(r.industry)}</td>
|
||||||
<td><span class="tag ${sigCls}">${meta.impact_score || '—'}</span></td>
|
<td><span class="tag ${sigCls}">${meta.impact_score || '—'}</span></td>
|
||||||
<td><span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span></td>
|
<td><span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span></td>
|
||||||
@@ -109,10 +111,25 @@ async function showDetail(id) {
|
|||||||
return `<details><summary>${icon} ${label} <span class="src-count">${txt.includes('[') ? txt.split('[').length - 1 : 0}</span></summary>
|
return `<details><summary>${icon} ${label} <span class="src-count">${txt.includes('[') ? txt.split('[').length - 1 : 0}</span></summary>
|
||||||
<div class="src-body"><pre class="src-pre">${escapeHtml(txt || '暂无')}</pre></div></details>`;
|
<div class="src-body"><pre class="src-pre">${escapeHtml(txt || '暂无')}</pre></div></details>`;
|
||||||
};
|
};
|
||||||
|
const isConcept = d.target_type === 'concept';
|
||||||
|
let sourceHtml;
|
||||||
|
if (isConcept) {
|
||||||
|
const nc = meta.news_counts || {};
|
||||||
|
sourceHtml = `
|
||||||
|
<details open><summary>🔎 相关资讯(${nc.direct || 0} 条)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.db_news || '')}</pre></div></details>
|
||||||
|
<details><summary>🧠 语义检索命中(${nc.rag || 0} 条)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.rag_news || '')}</pre></div></details>
|
||||||
|
<details><summary>🏢 受益个股(${nc.related || 0} 只)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.related || '')}</pre></div></details>`;
|
||||||
|
} else {
|
||||||
|
sourceHtml = newsBlock('个股直接动态', '📄', src.direct) +
|
||||||
|
newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream) +
|
||||||
|
newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream) +
|
||||||
|
newsBlock('同业竞争', '🏢', src.peers) +
|
||||||
|
`<details><summary>📈 技术面 / 机构动向</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.indicators || '')}</pre></div></details>`;
|
||||||
|
}
|
||||||
openModal(`
|
openModal(`
|
||||||
<h3>🧭 产业链跟踪:${d.stock_name}(${d.code})</h3>
|
<h3>${isConcept ? '🎯 概念主题跟踪' : '🧭 产业链跟踪'}:${d.stock_name}${d.code && !isConcept ? '(' + d.code + ')' : ''}</h3>
|
||||||
<div class="news-meta" style="margin-bottom:12px">
|
<div class="news-meta" style="margin-bottom:12px">
|
||||||
<span class="cat-tag">${escapeHtml(d.industry)}</span>
|
<span class="cat-tag ${isConcept ? '行业' : '公司'}">${isConcept ? '概念/主题' : d.industry}</span>
|
||||||
<span class="tag ${sigCls}">影响度 ${meta.impact_score || '—'}/100</span>
|
<span class="tag ${sigCls}">影响度 ${meta.impact_score || '—'}/100</span>
|
||||||
<span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span>
|
<span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span>
|
||||||
<span style="color:var(--muted)">${d.created_at}</span>
|
<span style="color:var(--muted)">${d.created_at}</span>
|
||||||
@@ -120,15 +137,11 @@ async function showDetail(id) {
|
|||||||
<div class="markdown-body">${mdRender(d.report)}</div>
|
<div class="markdown-body">${mdRender(d.report)}</div>
|
||||||
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:10px">
|
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:10px">
|
||||||
<b style="color:var(--gold)">数据源(智能体参考内容)</b>
|
<b style="color:var(--gold)">数据源(智能体参考内容)</b>
|
||||||
${newsBlock('个股直接动态', '📄', src.direct)}
|
${sourceHtml}
|
||||||
${newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream)}
|
|
||||||
${newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream)}
|
|
||||||
${newsBlock('同业竞争', '🏢', src.peers)}
|
|
||||||
<details><summary>📈 技术面 / 机构动向</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.indicators || '')}</pre></div></details>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:14px;text-align:right">
|
<div style="margin-top:14px;text-align:right">
|
||||||
<button class="btn" onclick="document.querySelector('.modal-mask').classList.remove('show')">关闭</button>
|
<button class="btn" onclick="document.querySelector('.modal-mask').classList.remove('show')">关闭</button>
|
||||||
<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>
|
${isConcept ? '' : `<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>`}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
} catch (e) { toast('加载失败'); }
|
} catch (e) { toast('加载失败'); }
|
||||||
|
|||||||
@@ -60,16 +60,42 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex mt16 wrap">
|
<div class="flex mt16 wrap">
|
||||||
<button class="btn btn-primary" onclick="saveTracking()">💾 保存跟踪设置</button>
|
<button class="btn btn-primary" onclick="saveTracking()">💾 保存跟踪设置</button>
|
||||||
<button class="btn" onclick="runTrackAll()">⚡ 立即跟踪全部持仓</button>
|
<button class="btn" onclick="runTrackAll()">⚡ 立即跟踪全部目标</button>
|
||||||
<button class="btn" onclick="location.href='/tracking'">🧭 查看全部跟踪报告</button>
|
<button class="btn" onclick="location.href='/tracking'">🧭 查看全部跟踪报告</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt16 mini-stats" id="trackingState"></div>
|
<div class="mt16 mini-stats" id="trackingState"></div>
|
||||||
|
|
||||||
|
<!-- 持仓(自选股)管理 -->
|
||||||
|
<div class="mt16" style="border-top:1px solid var(--border);padding-top:6px">
|
||||||
|
<div class="card-title mt16" style="font-size:14px"><span class="bar" style="background:var(--gold)"></span>💼 持仓(自选股)管理 —— 跟踪对象 = 持仓</div>
|
||||||
|
<div class="flex mb8 wrap">
|
||||||
|
<input class="input" id="posSearch" placeholder="输入代码 / 名称搜索股票,回车添加" style="flex:1;min-width:200px">
|
||||||
|
<button class="btn btn-primary" onclick="addPos()">+ 添加持仓</button>
|
||||||
|
</div>
|
||||||
|
<div id="posManage" style="overflow:auto;max-height:260px"><div class="loading">加载中…</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 概念/主题目标 -->
|
||||||
|
<div class="mt16" style="border-top:1px solid var(--border);padding-top:6px">
|
||||||
|
<div class="card-title mt16" style="font-size:14px"><span class="bar" style="background:#f472b6"></span>🎯 概念 / 主题 / 股票 跟踪目标</div>
|
||||||
|
<div class="flex mb8 wrap">
|
||||||
|
<select class="input" id="tgtType" style="width:110px">
|
||||||
|
<option value="concept">概念/主题</option>
|
||||||
|
<option value="stock">股票代码</option>
|
||||||
|
</select>
|
||||||
|
<input class="input" id="tgtName" placeholder="目标名称,如:AI算力 / 低空经济(股票类型填代码)" style="flex:1;min-width:200px">
|
||||||
|
<input class="input" id="tgtKeywords" placeholder="检索关键词,逗号分隔(概念填)" style="flex:1;min-width:180px">
|
||||||
|
<button class="btn btn-primary" onclick="addTarget()">+ 添加目标</button>
|
||||||
|
</div>
|
||||||
|
<div id="tgtManage" style="overflow:auto;max-height:260px"><div class="loading">加载中…</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt16" style="border-top:1px solid var(--border);padding-top:6px">
|
<div class="mt16" style="border-top:1px solid var(--border);padding-top:6px">
|
||||||
<div class="card-title mt16" style="font-size:14px"><span class="bar" style="background:#8b5cf6"></span>最近跟踪报告</div>
|
<div class="card-title mt16" style="font-size:14px"><span class="bar" style="background:#8b5cf6"></span>最近跟踪报告</div>
|
||||||
<div style="overflow:auto;max-height:340px">
|
<div style="overflow:auto;max-height:340px">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>时间</th><th>股票</th><th>影响度</th><th>性质</th><th>摘要</th><th>操作</th></tr></thead>
|
<thead><tr><th>时间</th><th>类型</th><th>目标</th><th>影响度</th><th>性质</th><th>摘要</th><th>操作</th></tr></thead>
|
||||||
<tbody id="trackTb"><tr><td colspan="6" class="empty">暂无跟踪记录</td></tr></tbody>
|
<tbody id="trackTb"><tr><td colspan="7" class="empty">暂无跟踪记录</td></tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex between wrap">
|
<div class="flex between wrap">
|
||||||
<div style="color:var(--text2);font-size:13px;line-height:1.9">
|
<div style="color:var(--text2);font-size:13px;line-height:1.9">
|
||||||
🤖 <b style="color:var(--text)">大模型智能体 + 工作流</b>:定期跟踪<b>自选股(持仓)</b>,除个股公告新闻外,
|
🤖 <b style="color:var(--text)">大模型智能体 + 工作流</b>:定期跟踪<b>持仓/自选股</b>与<b>目标概念/主题/股票</b>。
|
||||||
深度分析其<b>产业链上下游与同业</b>动态(上游供给/成本、下游需求/景气、竞争格局),
|
个股跟踪除公告新闻外,深度分析其<b>产业链上下游与同业</b>动态;概念跟踪覆盖题材相关资讯与受益个股。
|
||||||
输出专业跟踪报告;发现<b>重大变化自动邮件通知</b>。
|
输出专业报告,发现<b>重大变化自动邮件通知</b>。
|
||||||
<div style="color:var(--muted);font-size:12px">跟踪对象 = 自选股(⭐ 股票池/详情页添加)· 自动跟踪间隔可在「系统设置」配置</div>
|
<div style="color:var(--muted);font-size:12px">跟踪对象管理在「⏰ 自动化 → 持仓跟踪」页 · 自动跟踪间隔可在「系统设置」配置</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex wrap">
|
<div class="flex wrap">
|
||||||
<button class="btn btn-primary" onclick="runAll()">⚡ 立即跟踪全部持仓</button>
|
<button class="btn btn-primary" onclick="runAll()">⚡ 立即跟踪全部目标</button>
|
||||||
<button class="btn" onclick="load()">🔄 刷新</button>
|
<button class="btn" onclick="load()">🔄 刷新</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,8 +27,8 @@
|
|||||||
<div class="card-title"><span class="bar" style="background:var(--gold)"></span>全部跟踪记录</div>
|
<div class="card-title"><span class="bar" style="background:var(--gold)"></span>全部跟踪记录</div>
|
||||||
<div style="overflow:auto;max-height:520px">
|
<div style="overflow:auto;max-height:520px">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>时间</th><th>股票</th><th>行业</th><th>影响度</th><th>性质</th><th>摘要</th><th>操作</th></tr></thead>
|
<thead><tr><th>时间</th><th>类型</th><th>目标</th><th>行业</th><th>影响度</th><th>性质</th><th>摘要</th><th>操作</th></tr></thead>
|
||||||
<tbody id="recTb"><tr><td colspan="7" class="empty">暂无跟踪记录,点击上方「立即跟踪全部持仓」开始</td></tr></tbody>
|
<tbody id="recTb"><tr><td colspan="8" class="empty">暂无跟踪记录,点击上方「立即跟踪全部持仓」开始</td></tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user