v1.9.0: ①自动化开关即时保存(切换即生效不回落) ②持仓跟踪推送方式(聚合一起发默认/分散发) ③行情数据增量延续到最新交易日(每小时自动+手动按钮)
This commit is contained in:
+91
-11
@@ -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("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
||||
|
||||
|
||||
# ===================================================================== 批量与调度
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user