Initial commit: stock analysis backend and prototype UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-13 02:26:22 +08:00
commit 8de37d5c2d
25 changed files with 4624 additions and 0 deletions

67
backend/alerts.py Normal file
View File

@@ -0,0 +1,67 @@
"""预警引擎:拉取实时价格,评估规则,触发即生成站内事件。"""
from __future__ import annotations
import datetime as dt
from sqlalchemy import select
import akshare_service as svc
import notifier
from db import get_session
from models import AlertRule, AlertEvent
KIND_LABEL = {
"price_above": "价格突破",
"price_below": "价格跌破",
"pct_above": "涨幅达到",
"pct_below": "跌幅达到",
}
def _hit(kind, threshold, q):
price, pct = q["price"], q["pct"]
if kind == "price_above":
return price >= threshold, price
if kind == "price_below":
return price <= threshold, price
if kind == "pct_above":
return pct >= threshold, pct
if kind == "pct_below":
return pct <= -abs(threshold), pct
return False, price
def check_alerts():
with get_session() as s:
rules = s.execute(select(AlertRule).where(AlertRule.status == "active")).scalars().all()
if not rules:
return {"checked": 0, "triggered": 0}
codes = list({r.code for r in rules})
quotes = svc.realtime_quotes(codes)
triggered = 0
push_msgs = []
for r in rules:
q = quotes.get(r.code)
if not q:
continue
hit, val = _hit(r.kind, r.threshold, q)
r.last_value = q["price"]
if hit:
unit = "" if r.kind.startswith("price") else "%"
msg = (f"{q['name']} {KIND_LABEL.get(r.kind, r.kind)} {r.threshold}{unit}"
f"(现价 {q['price']}{q['pct']:+.2f}%")
s.add(AlertEvent(rule_id=r.id, code=r.code, name=q["name"], message=msg, value=val))
r.status = "triggered"
r.triggered_at = dt.datetime.now()
triggered += 1
push_msgs.append(msg)
s.commit()
# 触发后向已配置渠道推送(站外)
if push_msgs and notifier.any_enabled():
try:
notifier.notify("【智策预警】" + (push_msgs[0] if len(push_msgs) == 1 else f"{len(push_msgs)} 条预警触发"),
"\n".join(push_msgs))
except Exception:
pass
return {"checked": len(rules), "triggered": triggered}