# -*- coding: utf-8 -*- """ 智能荐股系统 - Flask 主应用 页面:仪表盘 / 股票池 / 荐股中心 / 财经新闻 / 机构动向 / 数据管理 / 个股详情 """ import json import logging import os import time from flask import Flask, jsonify, render_template, request from config import (IS_MOCK, SERVICE_HOST, SERVICE_NAME, SERVICE_PORT, CHROMA_NEWS_COLLECTION, CHROMA_PROFILE_COLLECTION) from database import init_db, query, query_one, execute, table_count from engine import scoring from engine.indicators import compute_indicators from rag import vector_store as vs logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s") log = logging.getLogger("app") app = Flask(__name__) # ===================================================================== 页面 @app.route("/") def page_index(): return render_template("index.html", service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/stocks") def page_stocks(): return render_template("stocks.html", service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/recommend") def page_recommend(): return render_template("recommend.html", service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/news") def page_news(): return render_template("news.html", service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/institutions") def page_institutions(): return render_template("institutions.html", service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/strategies") def page_strategies(): return render_template("strategies.html", service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/tracking") def page_tracking(): return render_template("tracking.html", service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/automation") def page_automation(): return render_template("automation.html", service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/admin") def page_admin(): return render_template("admin.html", service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/settings") def page_settings(): return render_template("settings.html", service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/stock/") def page_stock(code): return render_template("stock_detail.html", code=code, service=SERVICE_NAME, is_mock=IS_MOCK) @app.route("/analysis/") def page_analysis(aid): return render_template("analysis_detail.html", aid=aid, service=SERVICE_NAME, is_mock=IS_MOCK) # ===================================================================== 公共 def _indicators(code): rows = query("SELECT date,open,high,low,close,volume FROM stock_daily " "WHERE code=? ORDER BY date ASC", (code,)) return compute_indicators(rows) def _news_score(code): n = query_one( "SELECT AVG(sentiment) AS s FROM news WHERE (related_stocks=? OR related_stocks LIKE ? OR related_stocks LIKE ?) " "AND publish_date >= date('now','-7 day')", (code, f"%,{code}", f"{code},%")) return n["s"] if n and n["s"] is not None else 0.0 def _inst_score(code): st = query_one( "SELECT COUNT(*) AS c FROM inst_ratings WHERE stock_code=? AND rating IN ('买入','增持') " "AND rating_date >= date('now','-30 day')", (code,)) cnt = st["c"] if st else 0 return min(1.0, cnt / 4.0) def _scored_stock(code): stock = query_one("SELECT * FROM stocks WHERE code=?", (code,)) if not stock: return None ind = _indicators(code) news_s = _news_score(code) inst_s = _inst_score(code) sc = scoring.score_stock(ind, news_s, inst_s) # 机构正面评级数 / 增持数 up = query_one("SELECT COUNT(*) c FROM inst_ratings WHERE stock_code=? AND rating IN ('买入','增持') " "AND rating_date >= date('now','-30 day')", (code,)) hold_up = query_one("SELECT COUNT(*) c FROM fund_holdings WHERE stock_code=? AND change_pct>0 AND quarter=" "(SELECT MAX(quarter) FROM fund_holdings)", (code,)) reasons = scoring.build_reasons(ind, news_s, up["c"] if up else 0, hold_up["c"] if hold_up else 0) return { "stock": stock, "ind": ind, "score": sc, "reasons": reasons, "inst_up": up["c"] if up else 0, "hold_up": hold_up["c"] if hold_up else 0, "news_score": round(news_s, 2), "inst_score": round(inst_s, 2), } def _fmt(row): """转 JSON 安全(bytes 等)""" return row # ===================================================================== API @app.route("/api/health") def api_health(): return jsonify({"status": "ok", "service": SERVICE_NAME, "is_mock": IS_MOCK, "time": time.strftime("%Y-%m-%d %H:%M:%S")}) @app.route("/api/overview") def api_overview(): """仪表盘:指数 / 市场情绪 / 行业热度 / 今日荐股 / 自选 / 要闻""" idx = query("SELECT * FROM market_index ORDER BY date DESC LIMIT 2") latest = idx[0] if idx else {} prev = idx[1] if len(idx) > 1 else latest inds = [] for k, label in (("sh", "上证指数"), ("sz", "深证成指"), ("cy", "创业板指")): cur, old = latest.get(k, 0), prev.get(k, 0) or 1 inds.append({"key": k, "label": label, "value": cur, "chg": round((cur - old) / old * 100, 2)}) # 今日涨跌统计 stat = query_one( "SELECT COUNT(*) total, SUM(CASE WHEN change_pct>0 THEN 1 ELSE 0 END) up," "SUM(CASE WHEN change_pct<0 THEN 1 ELSE 0 END) down," "SUM(CASE WHEN change_pct>=9.8 THEN 1 ELSE 0 END) limit_up," "SUM(CASE WHEN change_pct<=-9.8 THEN 1 ELSE 0 END) limit_down," "ROUND(SUM(amount)/10000,2) amount_yi " "FROM stock_daily WHERE date=(SELECT MAX(date) FROM stock_daily)") # 行业热度(今日平均涨跌幅) heat = query( "SELECT s.industry, ROUND(AVG(d.change_pct),2) chg, COUNT(*) cnt " "FROM stock_daily d JOIN stocks s ON s.code=d.code " "WHERE d.date=(SELECT MAX(date) FROM stock_daily) GROUP BY s.industry " "ORDER BY chg DESC LIMIT 8") # 今日荐股 Top5(重算) rec = _top_recommend(5, "all") # 自选 watch = [] for w in query("SELECT code FROM watchlist ORDER BY added_at DESC"): d = _scored_stock(w["code"]) if d: watch.append({"code": w["code"], "name": d["stock"]["name"], "close": d["ind"].get("close"), "chg": d["ind"].get("change_pct"), "rating": d["score"]["rating"], "score": d["score"]["total"]}) news = query("SELECT id,title,source,category,sentiment,publish_date,related_stocks " "FROM news ORDER BY publish_date DESC, id DESC LIMIT 6") return jsonify({ "indexes": inds, "stat": stat, "heat": heat, "top": rec, "watchlist": watch, "news": news, }) @app.route("/api/stocks") def api_stocks(): keyword = request.args.get("keyword", "").strip() industry = request.args.get("industry", "").strip() board = request.args.get("board", "").strip() sort = request.args.get("sort", "code") order = request.args.get("order", "asc") page = max(1, int(request.args.get("page", 1))) per = min(100, int(request.args.get("per", 20))) rated = request.args.get("rating", "").strip() conds, args = [], [] if keyword: conds.append("(s.name LIKE ? OR s.code LIKE ?)") args += [f"%{keyword}%", f"%{keyword}%"] if industry: conds.append("s.industry=?") args.append(industry) if board: conds.append("s.board=?") args.append(board) where = ("WHERE " + " AND ".join(conds)) if conds else "" all_rows = query( f"SELECT s.*, d.close, d.change_pct, d.volume, d.amount " f"FROM stocks s LEFT JOIN stock_daily d ON d.code=s.code AND d.date=(SELECT MAX(date) FROM stock_daily) " f"{where}", args) out = [] for r in all_rows: sc = _scored_stock(r["code"]) if not sc: continue if rated and sc["score"]["rating"] != rated: continue out.append({ "code": r["code"], "name": r["name"], "industry": r["industry"], "board": r["board"], "close": r["close"], "change_pct": r["change_pct"], "amount": r["amount"], "market_cap": r["market_cap"], "pe": r["pe"], "score": sc["score"]["total"], "rating": sc["score"]["rating"], "reasons": sc["reasons"], "vol_ratio": sc["ind"].get("vol_ratio"), "chg_5d": sc["ind"].get("chg_5d"), }) allowed = {"code", "name", "close", "change_pct", "score", "amount", "market_cap"} if sort in allowed: out.sort(key=lambda x: (x[sort] is None, x[sort]), reverse=(order == "desc")) total = len(out) start = (page - 1) * per return jsonify({"total": total, "page": page, "per": per, "items": out[start:start + per]}) @app.route("/api/stock/") def api_stock(code): d = _scored_stock(code) if not d: return jsonify({"error": "not found"}), 404 stock = d["stock"] stock["is_watch"] = bool(query_one("SELECT 1 FROM watchlist WHERE code=?", (code,))) stock["week_change"] = d["ind"].get("chg_5d") stock["month_change"] = d["ind"].get("chg_20d") return jsonify({"stock": stock, "ind": {k: v for k, v in d["ind"].items() if k != "bars"}, "score": d["score"], "reasons": d["reasons"], "news_score": d["news_score"], "inst_score": d["inst_score"], "inst_up": d["inst_up"], "hold_up": d["hold_up"]}) @app.route("/api/stock//kline") def api_kline(code): days = min(250, int(request.args.get("days", 120))) rows = query("SELECT date,open,high,low,close,volume FROM stock_daily " "WHERE code=? ORDER BY date DESC LIMIT ?", (code, days)) rows.reverse() ind = compute_indicators(rows) bars = ind.get("bars", []) # 附 MA 序列 def ma_series(n): out = [] for i in range(len(bars)): seg = bars[max(0, i + 1 - n):i + 1] if len(seg) < n: out.append(None) else: out.append(round(sum(b["close"] for b in seg) / n, 2)) return out return jsonify({ "dates": [b["date"] for b in bars], "kline": [[b["open"], b["close"], b["low"], b["high"]] for b in bars], "volumes": [b["volume"] for b in bars], "ma5": ma_series(5), "ma10": ma_series(10), "ma20": ma_series(20), "ma60": ma_series(60), }) @app.route("/api/stock//news") def api_stock_news(code): rows = query("SELECT id,title,source,category,sentiment,publish_date,related_stocks " "FROM news WHERE (related_stocks=? OR related_stocks LIKE ? OR related_stocks LIKE ?) " "ORDER BY publish_date DESC LIMIT 30", (code, f"%,{code}", f"{code},%")) return jsonify({"items": rows}) @app.route("/api/stock//institutions") def api_stock_inst(code): ratings = query( "SELECT inst_name, rating, target_price, rating_date, prev_rating FROM inst_ratings " "WHERE stock_code=? ORDER BY rating_date DESC", (code,)) holdings = query( "SELECT inst_name, quarter, hold_shares, hold_value, change_pct FROM fund_holdings " "WHERE stock_code=? ORDER BY quarter DESC, hold_value DESC", (code,)) return jsonify({"ratings": ratings, "holdings": holdings}) @app.route("/api/news") def api_news(): keyword = request.args.get("keyword", "").strip() category = request.args.get("category", "").strip() code = request.args.get("code", "").strip() page = max(1, int(request.args.get("page", 1))) per = 15 conds, args = [], [] if keyword: conds.append("(title LIKE ? OR content LIKE ?)") args += [f"%{keyword}%", f"%{keyword}%"] if category: conds.append("category=?") args.append(category) if code: conds.append("(related_stocks=? OR related_stocks LIKE ? OR related_stocks LIKE ?)") args += [code, f"%,{code}", f"{code},%"] where = ("WHERE " + " AND ".join(conds)) if conds else "" total = query_one(f"SELECT COUNT(*) c FROM news {where}", args)["c"] items = query(f"SELECT * FROM news {where} ORDER BY publish_date DESC, id DESC LIMIT ? OFFSET ?", args + [per, (page - 1) * per]) cats = query("SELECT category, COUNT(*) c FROM news GROUP BY category ORDER BY c DESC") return jsonify({"total": total, "page": page, "per": per, "items": items, "cats": cats}) @app.route("/api/news/") def api_news_detail(nid): n = query_one("SELECT * FROM news WHERE id=?", (nid,)) if not n: return jsonify({"error": "not found"}), 404 # 关联股票信息 stocks = [] for code in (n["related_stocks"] or "").split(","): if not code: continue s = query_one("SELECT code,name,industry FROM stocks WHERE code=?", (code,)) if s: stocks.append(s) return jsonify({"news": n, "stocks": stocks}) def _top_recommend(limit=10, rating_filter="all"): """全市场评分排序""" rows = query("SELECT code FROM stocks") items = [] for r in rows: d = _scored_stock(r["code"]) if not d: continue if rating_filter != "all" and d["score"]["rating"] != rating_filter: continue items.append({ "code": r["code"], "name": d["stock"]["name"], "industry": d["stock"]["industry"], "close": d["ind"].get("close"), "change_pct": d["ind"].get("change_pct"), "score": d["score"], "reasons": d["reasons"], "chg_5d": d["ind"].get("chg_5d"), "vol_ratio": d["ind"].get("vol_ratio"), }) items.sort(key=lambda x: x["score"]["total"], reverse=True) return items[:limit] @app.route("/api/recommend") def api_recommend(): rating = request.args.get("rating", "all") limit = min(80, int(request.args.get("limit", 30))) return jsonify({"items": _top_recommend(limit, rating)}) # ------------------------------------------------------------------ AI 分析 @app.route("/api/stock//analyze", methods=["POST"]) def api_analyze(code): from engine import analyst body = request.get_json(silent=True) or {} focus = (body.get("focus") or "").strip() res = analyst.submit_report(code, focus) return jsonify(res) @app.route("/api/stock//analyze/status") def api_analyze_status(code): from engine import analyst st = analyst.report_status(code) if st: return jsonify(st) # 无运行中任务:返回最近一次历史分析 hist = analyst.list_history(code, limit=1) if hist: detail = analyst.get_history(hist[0]["id"]) return jsonify({"status": "done", "report": detail["report"], "history_id": detail["id"], "created_at": detail["created_at"], "cached": True}) return jsonify({"status": "idle"}) @app.route("/api/stock//analyses") def api_stock_analyses(code): from engine import analyst return jsonify({"items": analyst.list_history(code)}) @app.route("/api/analyses/") def api_analysis_detail(aid): from engine import analyst d = analyst.get_history(aid) if not d: return jsonify({"error": "记录不存在"}), 404 stock = query_one("SELECT code, name, industry, board FROM stocks WHERE code=?", (d["code"],)) return jsonify({"analysis": d, "stock": stock}) # ------------------------------------------------------------------ 自选 @app.route("/api/watchlist", methods=["GET"]) def api_watchlist(): items = [] for w in query("SELECT code, added_at FROM watchlist ORDER BY added_at DESC"): d = _scored_stock(w["code"]) if d: items.append({"code": w["code"], "name": d["stock"]["name"], "industry": d["stock"]["industry"], "close": d["ind"].get("close"), "change_pct": d["ind"].get("change_pct"), "rating": d["score"]["rating"], "score": d["score"]["total"], "added_at": w["added_at"]}) return jsonify({"items": items}) @app.route("/api/watchlist/", methods=["POST"]) def api_watch_add(code): execute("INSERT OR IGNORE INTO watchlist(code) VALUES(?)", (code,)) return jsonify({"ok": True}) @app.route("/api/watchlist/", methods=["DELETE"]) def api_watch_del(code): execute("DELETE FROM watchlist WHERE code=?", (code,)) return jsonify({"ok": True}) # ------------------------------------------------------------------ 机构 @app.route("/api/institutions") def api_institutions(): typ = request.args.get("type", "").strip() cond, args = "", [] if typ: cond, args = "WHERE type=?", [typ] items = query(f"SELECT * FROM institutions {cond} ORDER BY type, id", args) return jsonify({"items": items}) @app.route("/api/institutions/") def api_institution_detail(iid): inst = query_one("SELECT * FROM institutions WHERE id=?", (iid,)) if not inst: return jsonify({"error": "not found"}), 404 ratings = query( "SELECT r.stock_code, s.name, r.rating, r.target_price, r.rating_date, r.prev_rating " "FROM inst_ratings r JOIN stocks s ON s.code=r.stock_code WHERE r.inst_id=? " "ORDER BY r.rating_date DESC LIMIT 20", (iid,)) holdings = query( "SELECT h.stock_code, s.name, h.quarter, h.hold_value, h.change_pct " "FROM fund_holdings h JOIN stocks s ON s.code=h.stock_code WHERE h.inst_id=? " "ORDER BY h.quarter DESC, h.hold_value DESC LIMIT 20", (iid,)) return jsonify({"institution": inst, "ratings": ratings, "holdings": holdings}) @app.route("/api/ratings/upgrades") def api_ratings_upgrades(): """评级上调/下调榜""" rows = query( "SELECT r.stock_code, s.name, r.inst_name, r.rating, r.prev_rating, r.rating_date, r.target_price " "FROM inst_ratings r JOIN stocks s ON s.code=r.stock_code " "WHERE r.rating_date >= date('now','-45 day') " "ORDER BY r.rating_date DESC, r.id DESC LIMIT 30") return jsonify({"items": rows}) @app.route("/api/holdings/moves") def api_holdings_moves(): q = query_one("SELECT MAX(quarter) q FROM fund_holdings") latest = q["q"] if q else "2026Q2" inc = query( "SELECT h.stock_code, s.name, h.inst_name, h.hold_value, h.change_pct, h.quarter " "FROM fund_holdings h JOIN stocks s ON s.code=h.stock_code " "WHERE h.quarter=? AND h.change_pct>0 ORDER BY h.change_pct DESC LIMIT 10", (latest,)) dec = query( "SELECT h.stock_code, s.name, h.inst_name, h.hold_value, h.change_pct, h.quarter " "FROM fund_holdings h JOIN stocks s ON s.code=h.stock_code " "WHERE h.quarter=? AND h.change_pct<0 ORDER BY h.change_pct ASC LIMIT 10", (latest,)) return jsonify({"quarter": latest, "increase": inc, "decrease": dec}) # ------------------------------------------------------------------ 量化策略 @app.route("/api/strategies") def api_strategies(): from engine.strategies import STRATEGIES, strategy_summary items = [] for key, cfg in STRATEGIES.items(): items.append({"key": key, "name": cfg["name"], "icon": cfg["icon"], "params": cfg["params"], "desc": cfg["desc"], "tags": cfg["tags"], "summary": strategy_summary(key)}) return jsonify({"items": items}) @app.route("/api/backtest/market") def api_backtest_market(): from engine.strategies import market_rank key = request.args.get("strategy", "ma_cross") return jsonify({"items": market_rank(key)}) @app.route("/api/backtest") def api_backtest(): """单股×单策略回测详情(从预计算表读取)""" from engine.strategies import STRATEGIES import json as _json key = request.args.get("strategy", "ma_cross") code = request.args.get("code", "") if key not in STRATEGIES or not code: return jsonify({"error": "参数错误"}), 400 row = query_one("SELECT * FROM strategy_backtests WHERE strategy=? AND code=?", (key, code)) if not row: return jsonify({"error": "回测数据不存在,请先在数据管理页重建"}), 404 stock = query_one("SELECT name, industry, board FROM stocks WHERE code=?", (code,)) return jsonify({ "strategy": key, "config": STRATEGIES[key], "code": code, "stock_name": row["stock_name"], "stock": stock, "metrics": _json.loads(row["metrics"]), "equity": _json.loads(row["equity"]), "trades": _json.loads(row["trades"]), "run_at": row["run_at"], }) @app.route("/api/backtest/rebuild", methods=["POST"]) def api_backtest_rebuild(): import threading def run(): from engine.strategies import build_all try: build_all() except Exception as e: log.error("backtest rebuild fail: %s", e) threading.Thread(target=run, daemon=True).start() return jsonify({"ok": True, "msg": "全市场回测重建已启动"}) # ------------------------------------------------------------------ 设置与舆情监控 @app.route("/api/settings") def api_settings(): from settings import all_settings, mail_config, monitor_config, monitor_state, \ tracking_config, tracking_state, quiet_config from config import LLM_BASE_URL, LLM_API_KEY, LLM_MODEL s = all_settings() mail = mail_config() mono = monitor_config() track = tracking_config() return jsonify({ "mail": mail, "monitor": mono, "tracking": {**track, **tracking_state()}, "quiet": { # 各自动化任务的静默期配置 "monitor": quiet_config("monitor"), "tracking": quiet_config("tracking"), "report": quiet_config("report"), }, "llm": { "base_url": s.get("llm_base_url", LLM_BASE_URL), "api_key": s.get("llm_api_key", LLM_API_KEY), "model": s.get("llm_model", LLM_MODEL), }, "state": monitor_state(), }) @app.route("/api/settings", methods=["POST"]) def api_settings_save(): from settings import save_all, set_setting, _bool_str body = request.get_json(silent=True) or {} mail = body.get("mail") or {} llm = body.get("llm") or {} mono = body.get("monitor") or {} # 邮件(布尔规范化) for k in ("smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_mode", "email_to", "sender_name"): if k in mail: set_setting(k, mail[k]) if "email_enabled" in mail: set_setting("email_enabled", _bool_str(mail["email_enabled"])) # LLM for k in ("llm_base_url", "llm_api_key", "llm_model"): if k in llm: set_setting(k, str(llm[k]).strip()) # 监控 for k in ("monitor_interval", "monitor_categories", "monitor_sentiment", "monitor_importance", "monitor_keywords"): if k in mono: set_setting(k, mono[k]) if "monitor_enabled" in mono: set_setting("monitor_enabled", _bool_str(mono["monitor_enabled"])) # 持仓跟踪 track = body.get("tracking") or {} for k in ("tracking_interval", "tracking_impact_threshold"): if k in track: set_setting(k, track[k]) if "tracking_enabled" in track: set_setting("tracking_enabled", _bool_str(track["tracking_enabled"])) if "tracking_notify" in track: set_setting("tracking_notify", _bool_str(track["tracking_notify"])) # 静默期(各自动化任务独立配置) quiet = body.get("quiet") or {} for prefix in ("monitor", "tracking", "report"): q = quiet.get(prefix) or {} if "enabled" in q: set_setting(f"{prefix}_quiet_enabled", _bool_str(q["enabled"])) if "ranges" in q: set_setting(f"{prefix}_quiet_ranges", str(q["ranges"] or "").strip()) return jsonify({"ok": True, "msg": "设置已保存"}) @app.route("/api/settings/test-email", methods=["POST"]) def api_settings_test_email(): from settings import mail_config from engine.notifier import send_email body = request.get_json(silent=True) or {} try: cfg = mail_config() send_email( "[智能荐股] 邮件配置测试", "

✅ 邮件配置生效

如果你收到这封邮件,说明 SMTP 设置正确,舆情通知将正常送达。

" "

发送时间:" + time.strftime("%Y-%m-%d %H:%M:%S") + "

", cfg=cfg) return jsonify({"ok": True, "msg": f"测试邮件已发送到 {cfg['email_to']}"}) except Exception as e: return jsonify({"ok": False, "error": str(e)}), 500 @app.route("/api/settings/test-llm", methods=["POST"]) def api_settings_test_llm(): from settings import llm_config from engine.analyst import llm_chat try: cfg = llm_config() rep = llm_chat([{"role": "user", "content": "回复'连接正常'四个字"}], max_tokens=20) return jsonify({"ok": True, "msg": f"连接成功({cfg['model']}):{rep[:60]}"}) except Exception as e: return jsonify({"ok": False, "error": str(e)}), 500 @app.route("/api/monitor/scan", methods=["POST"]) def api_monitor_scan(): from engine.notifier import scan_news try: r = scan_news(force=True) return jsonify({"ok": True, **r}) except Exception as e: return jsonify({"ok": False, "error": str(e)}), 500 @app.route("/api/monitor/log") def api_monitor_log(): from engine.notifier import notification_log return jsonify({"items": notification_log()}) # ------------------------------------------------------------------ 持仓跟踪 @app.route("/api/tracking") def api_tracking_list(): from engine.agent import list_reports return jsonify({"items": list_reports()}) @app.route("/api/tracking/stock/") def api_tracking_stock(code): from engine.agent import latest_reports return jsonify({"items": latest_reports(code)}) @app.route("/api/tracking/") def api_tracking_detail(rid): from engine.agent import get_report import json as _json r = get_report(rid) if not r: return jsonify({"error": "记录不存在"}), 404 r["meta"] = _json.loads(r["meta"] or "{}") r["sources"] = _json.loads(r["sources"] or "{}") return jsonify(r) @app.route("/api/tracking/run", methods=["POST"]) def api_tracking_run(): """立即跟踪全部目标(持仓+概念/主题/股票,后台)""" import threading def run(): from engine.agent import track_all try: track_all() except Exception as e: log.error("track run fail: %s", e) threading.Thread(target=run, daemon=True).start() return jsonify({"ok": True, "msg": "跟踪任务已启动(逐目标分析,约每只30-60秒)"}) @app.route("/api/tracking/run/", methods=["POST"]) def api_tracking_run_one(code): from engine.agent import track_stock import threading def run(): try: track_stock(code) except Exception as e: log.error("track %s fail: %s", code, e) threading.Thread(target=run, daemon=True).start() 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/", methods=["DELETE"]) def api_targets_delete(tid): from engine.agent import delete_target return jsonify(delete_target(tid)) # ------------------------------------------------------------------ 定时报告 @app.route("/api/report/send", methods=["POST"]) def api_report_send(): """手动触发定时报告(后台生成 + 邮件)""" from engine.report import send_daily_report import threading body = request.get_json(silent=True) or {} kind = body.get("kind", "premarket") def run(): try: r = send_daily_report(kind) log.info("report send: %s", r) except Exception as e: log.error("report send fail: %s", e) threading.Thread(target=run, daemon=True).start() return jsonify({"ok": True, "msg": f"{'盘前分析' if kind == 'premarket' else '盘后总结'}生成已启动,约需 1-2 分钟"}) @app.route("/api/report/log") def api_report_log(): from engine.report import report_log return jsonify({"items": report_log()}) # ------------------------------------------------------------------ 数据管理 @app.route("/api/admin/stats") def api_admin_stats(): tables = ("stocks", "stock_daily", "news", "institutions", "inst_ratings", "fund_holdings", "watchlist", "analysis_cache", "analysis_history", "strategy_backtests", "market_index") return jsonify({ "tables": {t: table_count(t) for t in tables}, "vector": { "news": vs.collection_count(CHROMA_NEWS_COLLECTION), "profiles": vs.collection_count(CHROMA_PROFILE_COLLECTION), }, "is_mock": IS_MOCK, "db": "stock_advisor.db", }) @app.route("/api/admin/reseed", methods=["POST"]) def api_admin_reseed(): """一键重灌数据(含向量重建),后台执行""" import subprocess import threading def run(): py = "/home/hz1/miniconda3/envs/openclaw/bin/python3" subprocess.run([py, os.path.join(os.path.dirname(os.path.abspath(__file__)), "seed_data.py")], cwd=os.path.dirname(os.path.abspath(__file__)), stdout=open(os.path.join(LOG_DIR, "reseed.log"), "w"), stderr=subprocess.STDOUT) threading.Thread(target=run, daemon=True).start() return jsonify({"ok": True, "msg": "重灌任务已启动,可在数据管理页刷新查看进度"}) @app.route("/api/admin/healthcheck") def api_admin_healthcheck(): """外部依赖连通性检查""" import requests from config import EMBEDDING_API_URL, CHROMA_HOST, CHROMA_PORT, LLM_BASE_URL out = {} try: r = requests.get(f"http://{CHROMA_HOST}:{CHROMA_PORT}/api/v2/tenants/default_tenant/databases/default_database/collections", timeout=5) out["chroma"] = "ok" if r.status_code == 200 else f"http {r.status_code}" except Exception as e: out["chroma"] = f"fail {e}" try: r = requests.post(EMBEDDING_API_URL, json={"model": "bge-large-zh-v1.5", "input": ["测试"]}, timeout=10) out["embedding"] = "ok" if r.status_code == 200 else f"http {r.status_code}" except Exception as e: out["embedding"] = f"fail {e}" out["llm"] = "配置已就绪(调用时校验)" return jsonify(out) if __name__ == "__main__": init_db() from engine.notifier import start_monitor from engine.agent import start_tracking start_monitor() start_tracking() print(f"✅ {SERVICE_NAME} 启动: http://0.0.0.0:{SERVICE_PORT}") app.run(host=SERVICE_HOST, port=SERVICE_PORT, threaded=True)