智能荐股系统 v1.0.0:股票池/行情/新闻RAG/机构持仓/多因子评分/AI深度研报
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
AI 分析引擎:DeepSeek 深度研报 + RAG 增强
|
||||
- 检索:股票相关新闻(向量语义)+ 公司概况 + 机构动向/基金持仓(结构化)
|
||||
- 生成:结构化工研报(公司概况/基本面/技术面/消息面/机构动向/风险提示/操作建议)
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import requests
|
||||
|
||||
from config import (LLM_API_KEY, LLM_BASE_URL, LLM_MAX_TOKENS, LLM_MODEL,
|
||||
LLM_TEMPERATURE, LLM_TIMEOUT, CHROMA_NEWS_COLLECTION,
|
||||
CHROMA_PROFILE_COLLECTION)
|
||||
from database import query, query_one, execute
|
||||
from rag.vector_store import query_vectors
|
||||
|
||||
log = logging.getLogger("analyst")
|
||||
|
||||
_jobs = {} # code -> {status, report, error, ts}
|
||||
_jobs_lock = threading.Lock()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ LLM
|
||||
def llm_chat(messages, max_tokens=None, temperature=None, timeout=None):
|
||||
"""调用 DeepSeek(OpenAI 兼容)。返回最终 content(忽略推理过程)"""
|
||||
resp = requests.post(
|
||||
f"{LLM_BASE_URL}/chat/completions",
|
||||
headers={"Authorization": f"Bearer {LLM_API_KEY}"},
|
||||
json={
|
||||
"model": LLM_MODEL,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens or LLM_MAX_TOKENS,
|
||||
"temperature": LLM_TEMPERATURE if temperature is None else temperature,
|
||||
"stream": False,
|
||||
},
|
||||
timeout=timeout or LLM_TIMEOUT,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
try:
|
||||
return data["choices"][0]["message"].get("content") or ""
|
||||
except (KeyError, IndexError):
|
||||
return ""
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ RAG 检索
|
||||
def _rag_news(code, stock_name, query_text, top_k=6):
|
||||
"""检索个股相关新闻(向量语义,按 code 过滤)"""
|
||||
try:
|
||||
where = {"code": code}
|
||||
hits = query_vectors(query_text, n_results=top_k, where=where,
|
||||
name=CHROMA_NEWS_COLLECTION)
|
||||
out = []
|
||||
for h in hits:
|
||||
m = h.get("metadata", {})
|
||||
out.append({
|
||||
"title": m.get("title", ""),
|
||||
"date": m.get("date", ""),
|
||||
"sentiment": m.get("sentiment", 0),
|
||||
"text": h.get("document", "")[:400],
|
||||
})
|
||||
return out
|
||||
except Exception as e:
|
||||
log.warning("RAG news fail: %s", e)
|
||||
return []
|
||||
|
||||
|
||||
def _rag_profile(code):
|
||||
try:
|
||||
hits = query_vectors("公司主营业务与基本面", n_results=1,
|
||||
where={"code": code}, name=CHROMA_PROFILE_COLLECTION)
|
||||
if hits:
|
||||
return hits[0].get("document", "")
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _inst_summary(code):
|
||||
"""机构评级 + 基金持仓摘要(结构化)"""
|
||||
ratings = query(
|
||||
"SELECT inst_name, rating, target_price, rating_date, prev_rating "
|
||||
"FROM inst_ratings WHERE stock_code=? ORDER BY rating_date DESC LIMIT 6", (code,))
|
||||
holdings = query(
|
||||
"SELECT inst_name, quarter, hold_value, change_pct FROM fund_holdings "
|
||||
"WHERE stock_code=? ORDER BY quarter DESC, hold_value DESC LIMIT 6", (code,))
|
||||
return ratings, holdings
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 报告生成
|
||||
def _fmt_indicators(ind):
|
||||
if not ind:
|
||||
return "(暂无技术数据)"
|
||||
lines = [
|
||||
f"- 最新价 {ind.get('close')},当日 {ind.get('change_pct', 0):+.2f}%",
|
||||
f"- MA5={ind.get('ma5')} / MA10={ind.get('ma10')} / MA20={ind.get('ma20')} / MA60={ind.get('ma60')}",
|
||||
f"- RSI(14)={ind.get('rsi')},KDJ K/D/J={ind.get('kdj_k')}/{ind.get('kdj_d')}/{ind.get('kdj_j')}",
|
||||
f"- MACD DIF={ind.get('dif')} / DEA={ind.get('dea')} / 柱={ind.get('macd')}",
|
||||
f"- 量比 {ind.get('vol_ratio')},5日涨幅 {ind.get('chg_5d', 0):+.2f}%,20日涨幅 {ind.get('chg_20d', 0):+.2f}%",
|
||||
f"- 近120日区间 {ind.get('low_52w')} ~ {ind.get('high_52w')},20日波动率 {ind.get('volatility')}%",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _build_prompt(stock, ind, news_hits, profile, ratings, holdings, score, focus):
|
||||
rated = "、".join(f"{r['inst_name']}({r['rating']},目标{r['target_price']})" for r in ratings) or "暂无"
|
||||
held = ";".join(f"{h['inst_name']} {h['quarter']}持仓{h['hold_value']:.0f}万 环比{h['change_pct']:+.1f}%" for h in holdings) or "暂无"
|
||||
news_text = "\n\n".join(
|
||||
f"【{n['date']}|{n['title']}】(情感{n['sentiment']:+.2f})\n{n['text']}" for n in news_hits
|
||||
) or "(检索到相关资讯较少)"
|
||||
|
||||
return f"""你是资深A股投顾,请基于下方【资料】对股票 {stock['name']}({stock['code']}) 输出一份结构化工研报。
|
||||
|
||||
【资料】
|
||||
公司概况:
|
||||
{profile or stock.get('description', '暂无')}
|
||||
|
||||
技术面:
|
||||
{_fmt_indicators(ind)}
|
||||
|
||||
综合评分:{score.get('total')} 分(评级:{score.get('rating')}),分项:趋势{score.get('trend')}/动量{score.get('momentum')}/技术{score.get('technical')}/量能{score.get('volume')}/消息{score.get('news')}/机构{score.get('institutional')}
|
||||
|
||||
机构评级:{rated}
|
||||
基金持仓:{held}
|
||||
|
||||
相关资讯(RAG 语义检索):
|
||||
{news_text}
|
||||
|
||||
用户关注点:{focus or '整体投资价值'}
|
||||
|
||||
【输出要求】用 Markdown 输出,结构如下:
|
||||
## 一、公司概况与基本面
|
||||
## 二、技术面解读
|
||||
## 三、消息面与市场情绪
|
||||
## 四、机构动向
|
||||
## 五、风险提示
|
||||
## 六、操作建议(给出 目标区间 / 支撑位 / 压力位,说明短线与中线思路)
|
||||
注意:内容需严格基于上述资料,数据为模拟数据,结尾加一句「以上内容基于模拟数据生成,仅供系统演示,不构成投资建议」。"""
|
||||
|
||||
|
||||
def generate_report_sync(code, focus=""):
|
||||
"""同步生成报告(后台线程调用)"""
|
||||
stock = query_one("SELECT * FROM stocks WHERE code=?", (code,))
|
||||
if not stock:
|
||||
return {"error": "股票不存在"}
|
||||
ind = _indicators_for(code)
|
||||
score = _score_for(code, ind)
|
||||
hits = _rag_news(code, stock["name"], f"{stock['name']} {focus or '投资价值 业绩 利好利空'} {ind.get('close','')}")
|
||||
profile = _rag_profile(code)
|
||||
ratings, holdings = _inst_summary(code)
|
||||
prompt = _build_prompt(stock, ind, hits, profile, ratings, holdings, score, focus)
|
||||
try:
|
||||
report = llm_chat([
|
||||
{"role": "system", "content": "你是一名严谨专业的A股投资顾问,输出结构化、简洁、可执行的研报。"},
|
||||
{"role": "user", "content": prompt},
|
||||
])
|
||||
report = report.strip()
|
||||
if not report:
|
||||
raise RuntimeError("LLM 返回为空")
|
||||
execute("INSERT OR REPLACE INTO analysis_cache(code, report, created_at) VALUES(?,?,datetime('now','localtime'))",
|
||||
(code, report))
|
||||
return {"report": report, "ts": time.time()}
|
||||
except Exception as e:
|
||||
log.exception("gen report fail")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def _indicators_for(code):
|
||||
"""从 DB 读取日线并算指标(避免循环依赖 app)"""
|
||||
from engine.indicators import compute_indicators
|
||||
rows = query("SELECT date,open,high,low,close,volume FROM stock_daily WHERE code=? ORDER BY date ASC", (code,))
|
||||
return compute_indicators(rows)
|
||||
|
||||
|
||||
def _score_for(code, ind):
|
||||
from engine.scoring import score_stock
|
||||
# 新闻情感(与 app 端口径一致:精确/前缀/后缀三种匹配)
|
||||
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},%"))
|
||||
news_score = n["s"] if n and n["s"] is not None else 0.0
|
||||
# 机构热度
|
||||
st = query_one(
|
||||
"SELECT COUNT(*) AS c FROM inst_ratings WHERE stock_code=? AND rating IN ('买入','增持') "
|
||||
"AND rating_date >= date('now','-30 day')", (code,))
|
||||
inst_count = st["c"] if st else 0
|
||||
inst_score = min(1.0, inst_count / 4.0)
|
||||
return score_stock(ind, news_score, inst_score)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 异步任务
|
||||
def submit_report(code, focus=""):
|
||||
"""提交后台生成任务,立即返回"""
|
||||
with _jobs_lock:
|
||||
if _jobs.get(code, {}).get("status") == "running":
|
||||
return {"status": "running"}
|
||||
_jobs[code] = {"status": "running", "report": None, "error": None, "ts": time.time()}
|
||||
threading.Thread(target=_run_job, args=(code, focus), daemon=True).start()
|
||||
return {"status": "running"}
|
||||
|
||||
|
||||
def _run_job(code, focus):
|
||||
try:
|
||||
res = generate_report_sync(code, focus)
|
||||
with _jobs_lock:
|
||||
if res.get("error"):
|
||||
_jobs[code] = {"status": "error", "error": res["error"], "ts": time.time()}
|
||||
else:
|
||||
_jobs[code] = {"status": "done", "report": res["report"], "ts": time.time()}
|
||||
except Exception as e:
|
||||
with _jobs_lock:
|
||||
_jobs[code] = {"status": "error", "error": str(e), "ts": time.time()}
|
||||
|
||||
|
||||
def report_status(code):
|
||||
with _jobs_lock:
|
||||
return dict(_jobs.get(code, {}))
|
||||
|
||||
|
||||
def get_cached_report(code):
|
||||
return query_one("SELECT report, created_at FROM analysis_cache WHERE code=?", (code,))
|
||||
@@ -0,0 +1,164 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
技术指标计算:MA / RSI / MACD / KDJ / 量比 / 动量 / 波动率
|
||||
输入 bars:按日期升序的 [{date, open, high, low, close, volume}, ...]
|
||||
"""
|
||||
import math
|
||||
|
||||
|
||||
def _sma(vals, n):
|
||||
if len(vals) < n:
|
||||
return None
|
||||
return sum(vals[-n:]) / n
|
||||
|
||||
|
||||
def _ema(vals, n):
|
||||
if not vals:
|
||||
return None
|
||||
k = 2 / (n + 1)
|
||||
e = vals[0]
|
||||
for v in vals[1:]:
|
||||
e = v * k + e * (1 - k)
|
||||
return e
|
||||
|
||||
|
||||
def _ema_series(vals, n):
|
||||
out = []
|
||||
if not vals:
|
||||
return out
|
||||
k = 2 / (n + 1)
|
||||
e = vals[0]
|
||||
out.append(e)
|
||||
for v in vals[1:]:
|
||||
e = v * k + e * (1 - k)
|
||||
out.append(e)
|
||||
return out
|
||||
|
||||
|
||||
def rsi14(closes):
|
||||
"""Wilder RSI(14)"""
|
||||
if len(closes) < 15:
|
||||
return 50.0
|
||||
gains, losses = [], []
|
||||
for i in range(1, len(closes)):
|
||||
chg = closes[i] - closes[i - 1]
|
||||
gains.append(max(chg, 0))
|
||||
losses.append(max(-chg, 0))
|
||||
avg_g = sum(gains[:14]) / 14
|
||||
avg_l = sum(losses[:14]) / 14
|
||||
for i in range(14, len(gains)):
|
||||
avg_g = (avg_g * 13 + gains[i]) / 14
|
||||
avg_l = (avg_l * 13 + losses[i]) / 14
|
||||
if avg_l == 0:
|
||||
return 100.0
|
||||
rs = avg_g / avg_l
|
||||
return 100 - 100 / (1 + rs)
|
||||
|
||||
|
||||
def kdj(bars, n=9, k_smooth=3, d_smooth=3):
|
||||
"""返回 (K, D, J)"""
|
||||
if len(bars) < n:
|
||||
return 50.0, 50.0, 50.0
|
||||
k, d = 50.0, 50.0
|
||||
for i in range(n - 1, len(bars)):
|
||||
window = bars[i - n + 1:i + 1]
|
||||
low_n = min(b["low"] for b in window)
|
||||
high_n = max(b["high"] for b in window)
|
||||
rsv = 0 if high_n == low_n else (bars[i]["close"] - low_n) / (high_n - low_n) * 100
|
||||
k = (k * (k_smooth - 1) + rsv) / k_smooth
|
||||
d = (d * (d_smooth - 1) + k) / d_smooth
|
||||
j = 3 * k - 2 * d
|
||||
return k, d, j
|
||||
|
||||
|
||||
def compute_indicators(bars):
|
||||
"""计算全部技术指标,返回 dict(最新值 + 序列用于画图)"""
|
||||
if not bars:
|
||||
return {}
|
||||
closes = [b["close"] for b in bars]
|
||||
last = bars[-1]
|
||||
prev = bars[-2] if len(bars) > 1 else last
|
||||
|
||||
ma5 = _sma(closes, 5)
|
||||
ma10 = _sma(closes, 10)
|
||||
ma20 = _sma(closes, 20)
|
||||
ma60 = _sma(closes, 60)
|
||||
|
||||
# MACD
|
||||
ema12 = _ema_series(closes, 12)
|
||||
ema26 = _ema_series(closes, 26)
|
||||
dif_series = [e12 - e26 for e12, e26 in zip(ema12, ema26)]
|
||||
dea_series = _ema_series(dif_series, 9)
|
||||
dif = dif_series[-1] if dif_series else 0
|
||||
dea = dea_series[-1] if dea_series else 0
|
||||
macd = (dif - dea) * 2
|
||||
|
||||
rsi = rsi14(closes)
|
||||
k, d, j = kdj(bars)
|
||||
|
||||
# 涨跌幅
|
||||
chg_1d = (last["close"] - prev["close"]) / prev["close"] * 100 if prev["close"] else 0
|
||||
chg_5d = (last["close"] - closes[-6]) / closes[-6] * 100 if len(closes) > 6 else chg_1d
|
||||
chg_10d = (last["close"] - closes[-11]) / closes[-11] * 100 if len(closes) > 11 else chg_1d
|
||||
chg_20d = (last["close"] - closes[-21]) / closes[-21] * 100 if len(closes) > 21 else chg_1d
|
||||
|
||||
# 量比 = 今日量 / 前5日均量
|
||||
vol_ratio = 1.0
|
||||
if len(bars) > 6:
|
||||
avg5 = sum(b["volume"] for b in bars[-6:-1]) / 5
|
||||
if avg5 > 0:
|
||||
vol_ratio = last["volume"] / avg5
|
||||
|
||||
# 20日波动率(年化近似省略,日波动)
|
||||
returns = []
|
||||
for i in range(1, len(closes)):
|
||||
if closes[i - 1]:
|
||||
returns.append((closes[i] - closes[i - 1]) / closes[i - 1])
|
||||
vol20 = (sum(r * r for r in returns[-20:]) / max(len(returns[-20:]), 1)) ** 0.5 * 100 if returns else 0
|
||||
|
||||
# 区间高低(近120日)
|
||||
window = bars[-120:] if len(bars) > 120 else bars
|
||||
high52 = max(b["high"] for b in window)
|
||||
low52 = min(b["low"] for b in window)
|
||||
|
||||
# 均线多头排列
|
||||
if ma5 and ma10 and ma20:
|
||||
bull = ma5 > ma10 > ma20
|
||||
partial = ma5 > ma10 or ma10 > ma20
|
||||
else:
|
||||
bull, partial = False, False
|
||||
|
||||
return {
|
||||
"date": last["date"],
|
||||
"close": last["close"],
|
||||
"open": last["open"],
|
||||
"high": last["high"],
|
||||
"low": last["low"],
|
||||
"volume": last["volume"],
|
||||
"change_pct": round(chg_1d, 2),
|
||||
"chg_5d": round(chg_5d, 2),
|
||||
"chg_10d": round(chg_10d, 2),
|
||||
"chg_20d": round(chg_20d, 2),
|
||||
"ma5": round(ma5, 2) if ma5 else None,
|
||||
"ma10": round(ma10, 2) if ma10 else None,
|
||||
"ma20": round(ma20, 2) if ma20 else None,
|
||||
"ma60": round(ma60, 2) if ma60 else None,
|
||||
"rsi": round(rsi, 2),
|
||||
"kdj_k": round(k, 2),
|
||||
"kdj_d": round(d, 2),
|
||||
"kdj_j": round(j, 2),
|
||||
"dif": round(dif, 3),
|
||||
"dea": round(dea, 3),
|
||||
"macd": round(macd, 3),
|
||||
"vol_ratio": round(vol_ratio, 2),
|
||||
"volatility": round(vol20, 2),
|
||||
"high_52w": round(high52, 2),
|
||||
"low_52w": round(low52, 2),
|
||||
"trend_bull": bull,
|
||||
"trend_partial": partial,
|
||||
"bars": [
|
||||
{"date": b["date"], "open": b["open"], "high": b["high"],
|
||||
"low": b["low"], "close": b["close"], "volume": b["volume"]}
|
||||
for b in bars
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
荐股评分引擎:多因子打分模型(满分100)
|
||||
- 趋势 25分:均线多头排列 + 站上MA20
|
||||
- 动量 20分:5日涨幅区间映射
|
||||
- 技术 15分:RSI健康区间 / 超买超卖
|
||||
- 量能 10分:量比
|
||||
- 消息 15分:近7日相关新闻情感均值(RAG 信号)
|
||||
- 机构 15分:近30日评级上调 + 基金持仓环比增持
|
||||
|
||||
评级:>=82 强烈推荐 / >=68 推荐 / >=55 关注 / <55 观望
|
||||
"""
|
||||
RATINGS = [
|
||||
(82, "强烈推荐"),
|
||||
(68, "推荐"),
|
||||
(55, "关注"),
|
||||
(-1e9, "观望"),
|
||||
]
|
||||
|
||||
|
||||
def _rating(score):
|
||||
for threshold, name in RATINGS:
|
||||
if score >= threshold:
|
||||
return name
|
||||
return "观望"
|
||||
|
||||
|
||||
def _bracket(v, cuts):
|
||||
"""v 落在 [val, 分数] 区间的第一个匹配"""
|
||||
for hi, lo, score in cuts:
|
||||
if hi is None or v <= hi:
|
||||
if v >= lo:
|
||||
return score
|
||||
return 0
|
||||
|
||||
|
||||
def score_stock(ind, news_score, inst_score):
|
||||
"""ind: indicators dict;news_score: -1~1(无新闻用0);inst_score: 0~1 归一化机构热度"""
|
||||
s = {}
|
||||
|
||||
# 1. 趋势 25
|
||||
if ind.get("trend_bull"):
|
||||
s["trend"] = 25
|
||||
elif ind.get("trend_partial"):
|
||||
s["trend"] = 17
|
||||
else:
|
||||
s["trend"] = 8
|
||||
ma20 = ind.get("ma20")
|
||||
if ma20 and ind.get("close", 0) >= ma20:
|
||||
s["trend"] = min(25, s["trend"] + 4)
|
||||
|
||||
# 2. 动量 20(5日涨幅)
|
||||
chg5 = ind.get("chg_5d", 0)
|
||||
if chg5 > 12:
|
||||
s["momentum"] = 16 # 过急,扣分防追高
|
||||
elif chg5 > 6:
|
||||
s["momentum"] = 20
|
||||
elif chg5 > 2:
|
||||
s["momentum"] = 15
|
||||
elif chg5 > -2:
|
||||
s["momentum"] = 10
|
||||
elif chg5 > -6:
|
||||
s["momentum"] = 6
|
||||
else:
|
||||
s["momentum"] = 3
|
||||
|
||||
# 3. 技术 15(RSI)
|
||||
rsi = ind.get("rsi", 50)
|
||||
if 50 <= rsi <= 68:
|
||||
s["technical"] = 15
|
||||
elif 40 <= rsi < 50:
|
||||
s["technical"] = 11
|
||||
elif 68 < rsi <= 80:
|
||||
s["technical"] = 8 # 接近超买
|
||||
elif rsi < 35:
|
||||
s["technical"] = 9 # 超卖修复机会
|
||||
else:
|
||||
s["technical"] = 5
|
||||
|
||||
# 4. 量能 10(量比)
|
||||
vr = ind.get("vol_ratio", 1.0)
|
||||
if vr >= 2.0:
|
||||
s["volume"] = 10
|
||||
elif vr >= 1.3:
|
||||
s["volume"] = 8
|
||||
elif vr >= 0.8:
|
||||
s["volume"] = 6
|
||||
else:
|
||||
s["volume"] = 4
|
||||
|
||||
# 5. 消息 15
|
||||
s["news"] = round(max(0, min(15, (news_score + 1) / 2 * 15)), 1)
|
||||
|
||||
# 6. 机构 15
|
||||
s["institutional"] = round(inst_score * 15, 1)
|
||||
|
||||
total = round(sum(s.values()), 1)
|
||||
return {
|
||||
"total": total,
|
||||
"trend": s["trend"],
|
||||
"momentum": s["momentum"],
|
||||
"technical": s["technical"],
|
||||
"volume": s["volume"],
|
||||
"news": s["news"],
|
||||
"institutional": s["institutional"],
|
||||
"rating": _rating(total),
|
||||
"score_parts": s,
|
||||
}
|
||||
|
||||
|
||||
def build_reasons(ind, news_score, inst_up, hold_up):
|
||||
"""生成规则化推荐理由(供列表直接展示,无需 LLM)"""
|
||||
reasons = []
|
||||
if ind.get("trend_bull"):
|
||||
reasons.append("均线呈多头排列,中短期趋势向上")
|
||||
elif ind.get("close", 0) >= (ind.get("ma20") or 0):
|
||||
reasons.append("股价站上20日均线,趋势转强")
|
||||
else:
|
||||
reasons.append("均线空头排列,趋势偏弱,注意风险")
|
||||
|
||||
chg5 = ind.get("chg_5d", 0)
|
||||
if chg5 >= 6:
|
||||
reasons.append(f"5日涨幅{chg5:+.1f}%,动量强劲")
|
||||
elif chg5 < -6:
|
||||
reasons.append(f"5日跌幅{chg5:+.1f}%,弱势调整")
|
||||
else:
|
||||
reasons.append(f"5日涨跌{chg5:+.1f}%,动量平稳")
|
||||
|
||||
rsi = ind.get("rsi", 50)
|
||||
if rsi >= 70:
|
||||
reasons.append(f"RSI {rsi:.0f} 超买,短线回调风险增大")
|
||||
elif rsi <= 30:
|
||||
reasons.append(f"RSI {rsi:.0f} 超卖,存在修复反弹机会")
|
||||
|
||||
vr = ind.get("vol_ratio", 1.0)
|
||||
if vr >= 1.5:
|
||||
reasons.append(f"量比{vr:.2f},放量明显,资金活跃")
|
||||
elif vr < 0.7:
|
||||
reasons.append(f"量比{vr:.2f},缩量整理")
|
||||
|
||||
if news_score > 0.25:
|
||||
reasons.append("近期相关消息面偏正面,情绪回暖")
|
||||
elif news_score < -0.25:
|
||||
reasons.append("近期消息面偏空,注意利空扰动")
|
||||
|
||||
if inst_up > 0:
|
||||
reasons.append(f"近30日{inst_up}家机构给出正面评级")
|
||||
if hold_up > 0:
|
||||
reasons.append("基金最新季度环比增持")
|
||||
return reasons[:4]
|
||||
Reference in New Issue
Block a user