v1.9.0: ①自动化开关即时保存(切换即生效不回落) ②持仓跟踪推送方式(聚合一起发默认/分散发) ③行情数据增量延续到最新交易日(每小时自动+手动按钮)

This commit is contained in:
2026-09-09 15:59:35 +08:00
parent 9340577f7b
commit f20fe2f4f0
9 changed files with 317 additions and 32 deletions
+91 -11
View File
@@ -222,8 +222,8 @@ def strip_json_block(text):
# ===================================================================== 执行
def track_stock(code, focus=""):
"""执行一次跟踪,返回 {ok, report_id, meta, ...}"""
def track_stock(code, focus="", notify=True):
"""执行一次跟踪,返回 {ok, report_id, meta, ...}。notify=False 时不单独发邮件(聚合推送模式)"""
with _track_lock:
seg = collect_chain(code)
if not seg:
@@ -267,7 +267,8 @@ def track_stock(code, focus=""):
(code, s["name"], s["industry"], 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, s, meta)
if notify:
_notify_if_significant(rid, s, meta)
return {"ok": True, "report_id": rid, "meta": meta}
except Exception as e:
log.exception("track %s fail", code)
@@ -323,8 +324,9 @@ def _notify_if_significant(rid, stock, meta):
# ===================================================================== 概念/主题跟踪
def track_concept(name, keywords="", focus=""):
"""跟踪一个概念/主题:采集相关新闻 → 受益个股梳理 → LLM 深度分析 → 影响度判定"""
def track_concept(name, keywords="", focus="", notify=True):
"""跟踪一个概念/主题:采集相关新闻 → 受益个股梳理 → LLM 深度分析 → 影响度判定
notify=False 时不单独发邮件(聚合推送模式)"""
kw = keywords.strip() or name
kw_list = [k.strip() for k in kw.replace("", ",").split(",") if k.strip()]
kws = " ".join(kw_list)
@@ -411,7 +413,8 @@ def track_concept(name, keywords="", focus=""):
(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)
if notify:
_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)
@@ -437,9 +440,69 @@ def delete_target(tid):
return {"ok": True}
def _notify_aggregate(items):
"""聚合推送:一轮跟踪全部完成后,统一发一封汇总邮件(默认推送方式)"""
if not items:
return
cfg = tracking_config()
try:
if not cfg["notify"]:
return
qc = quiet_config("tracking")
if qc["enabled"] and in_quiet_period(qc["ranges"]):
log.info("tracking aggregate notify suppressed by quiet period (%d items)", len(items))
return
from engine.notifier import send_email
mc = mail_config()
rows = ""
for it in items:
meta = it.get("meta") or {}
is_concept = it.get("type") == "concept"
impact = int(meta.get("impact_score", 0))
level = "🔴 重大" if impact >= 65 else "🟡 关注"
name = escape_html(it.get("name", ""))
code = it.get("code", "")
rows += (
f"<tr>"
f"<td style='padding:8px;border:1px solid #e5e7eb'>{'🎯 概念' if is_concept else '💼 股票'}</td>"
f"<td style='padding:8px;border:1px solid #e5e7eb'><b>{name}</b>"
f"{'<div style=color:#888;font-size:12px>' + str(code) + '</div>' if not is_concept and code else ''}</td>"
f"<td style='padding:8px;border:1px solid #e5e7eb'>{meta.get('change_kind', '')}</td>"
f"<td style='padding:8px;border:1px solid #e5e7eb'>{impact}/100 <span style='color:#888;font-size:12px'>({level})</span></td>"
f"<td style='padding:8px;border:1px solid #e5e7eb;color:#555'>{escape_html(meta.get('summary', ''))}</td>"
f"</tr>"
)
send_email(
f"[持仓跟踪] 聚合报告:{len(items)} 个目标出现重大动态(影响度≥{int(cfg['impact_threshold'])}",
f"""<html><body style="font-family:Microsoft YaHei;padding:20px;background:#f5f6f8;">
<div style="max-width:760px;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;">🧭 持仓跟踪聚合报告 · {time.strftime('%Y-%m-%d %H:%M')}</div>
<div style="padding:16px 20px;">
<p style="color:#555;">本轮共跟踪 {items[0].get('_total', len(items))} 个目标,其中 <b>{len(items)}</b> 个达到重大变化判定阈值(影响度 ≥ {int(cfg['impact_threshold'])}):</p>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tr style="background:#f1f5f9"><th style="padding:8px;border:1px solid #e5e7eb;text-align:left">类型</th><th style="padding:8px;border:1px solid #e5e7eb;text-align:left">目标</th><th style="padding:8px;border:1px solid #e5e7eb;text-align:left">性质</th><th style="padding:8px;border:1px solid #e5e7eb;text-align:left">影响度</th><th style="padding:8px;border:1px solid #e5e7eb;text-align:left">摘要</th></tr>
{rows}
</table>
<p style="color:#888;font-size:12px;margin-top:14px;">详细分析报告请在系统「🧭 持仓跟踪智能体 → 最近跟踪报告」中查看。</p>
</div></div></body></html>""",
cfg=mc)
set_tracking_state(last_alert=int(items[0]["meta"].get("impact_score", 0)))
log.info("tracking aggregate notify sent: %d items", len(items))
except Exception as e:
log.warning("tracking aggregate notify fail: %s", e)
def escape_html(s):
return str(s or "").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
# ===================================================================== 批量与调度
def track_all(progress=None):
"""跟踪全部目标:持仓(自选股)+ 目标(概念/主题/股票)"""
"""跟踪全部目标:持仓(自选股)+ 目标(概念/主题/股票)
推送方式(tracking_push_mode):
aggregate(默认) —— 一轮全部分析完后,汇总重大变化统一发一封邮件;
scattered —— 每只股票分析完立即单独发邮件。
"""
global _cycle_running
if _cycle_running:
return {"tracked": 0, "msg": "已有跟踪任务进行中,请稍后再试"}
@@ -465,16 +528,33 @@ def track_all(progress=None):
uniq.append((typ, key, name))
if not uniq:
return {"tracked": 0, "msg": "暂无跟踪目标:请添加持仓/自选股或概念主题目标"}
cfg = tracking_config()
scattered = cfg.get("push_mode", "aggregate") == "scattered"
significant = [] # 聚合推送模式收集重大变化
results = []
for i, (typ, key, name) in enumerate(uniq):
if typ == "stock":
r = track_stock(key)
else:
r = track_concept(key, name)
try:
if typ == "stock":
r = track_stock(key, notify=scattered)
else:
r = track_concept(key, name, notify=scattered)
except Exception as e:
r = {"error": str(e)}
results.append({"type": typ, "name": name, **r})
if r.get("ok"):
meta = r.get("meta") or {}
if int(meta.get("impact_score", 0)) >= int(cfg["impact_threshold"]):
significant.append({"type": typ, "name": name, "code": key,
"meta": meta, "_total": len(uniq)})
set_tracking_state(last_run=time.strftime("%Y-%m-%d %H:%M:%S"), last_stock=name)
if progress:
progress(i + 1, len(uniq))
# 聚合推送:一轮分析完统一发一封汇总邮件
if not scattered and significant:
try:
_notify_aggregate(significant)
except Exception as e:
log.warning("aggregate notify error: %s", e)
return {"tracked": len(results), "results": results}
finally:
_cycle_running = False