fix: xiaoguo_scanner 榜单更新+看空榜持仓预警

- 修bug:stock_rank_cxd_ths 实为'创新低',改为 stock_rank_lxsz_ths '连续上涨'
- 新增6个看多榜(险资举牌)+ 5个看空榜(创新低/持续缩量/量价齐跌/连续下跌/向下突破)
- 看空榜自动比对持仓,命中写入 xiaoguo_risk 信号
- 东财热榜静默降级(502不可修)
- 看空榜不跳过已扫描,每轮全检
This commit is contained in:
知微
2026-06-22 19:13:55 +08:00
parent 774c2e885d
commit ce687a4216
13 changed files with 8331 additions and 33663 deletions
+204 -9
View File
@@ -548,17 +548,17 @@ def reassess_strategy(code, name, price, cost, shares, current_action,
# ----- 止盈设置 -----
if is_short_term_strong_trend and not is_new_entry:
# 短炒+强趋势:不止盈让利润跑,止盈设为远位(多周期强阻或价×1.5)
# 短炒+强趋势:不止盈让利润跑
mtf_tp = mtf_adj.get("take_profit_reference", {})
if mtf_tp and mtf_tp.get("level", 0) > price * 1.2:
new_target = round(mtf_tp["level"], 2)
else:
new_target = round(price * 1.5, 2)
new_target = 0 # 无多周期阻力时不编造止盈
print(f" 短炒强趋势不止盈: 止盈设为{new_target} (+{(new_target/price-1)*100:.0f}%)")
elif sr_resist and sr_resist > 0:
new_target = round(sr_resist, 2)
else:
new_target = round(price * 1.15, 2)
new_target = 0 # 无技术面数据时不编造止盈
# ----- 风险回报比校验 -----
stop_distance = price - new_stop if price > new_stop else price * 0.02
@@ -774,7 +774,8 @@ def reassess_strategy(code, name, price, cost, shares, current_action,
action_parts.append(action_note)
if is_watchlist:
# 自选股(未入场):只有买入区+止损参考,无止盈
# 自选股(未入场):有止损参考+买入区,内部算RR需要止盈
action_parts.append(f"目标参考{new_target}")
action_parts.append(f"止损参考{new_stop}")
action_parts.append(f"买入区{entry_low}~{entry_high}")
elif is_new_entry:
@@ -841,6 +842,181 @@ def reassess_strategy(code, name, price, cost, shares, current_action,
}
def load_stock_news_sentiment(code):
"""加载小果消息面情感"""
try:
path = "/home/hmo/web-dashboard/data/xiaoguo_sentiment.json"
if not os.path.exists(path):
return {}
xg = json.load(open(path))
return xg.get("stocks", {}).get(code, {})
except Exception:
return {}
def load_fundamentals(code):
"""加载个股基本面"""
try:
path = "/home/hmo/web-dashboard/data/multi_tf_cache.json"
if not os.path.exists(path):
return {}
m = json.load(open(path))
return m.get(code, {}).get("fundamentals", {}) or {}
except Exception:
return {}
def enrich_timing_signal(base_signal, macro_desc="", sector_note="",
profit_pct=0, stock_category="", is_new_entry=False,
fundamentals=None, news_sentiment=None,
timing_signal_override=None):
"""多因子合成timing_signal——大盘+行业+基本面+技术
返回 (enriched_signal, factors_list)
- enriched_signal: 可读的多因子信号描述
- factors_list: 各因子的摘要列表(用于后续显示)
"""
# 如果已手动设定,尊重手动
if timing_signal_override and timing_signal_override != "neutral":
return timing_signal_override, [timing_signal_override]
factors = []
# 1. 大盘因子
if "偏强" in macro_desc or "大涨" in macro_desc or "bullish" in macro_desc.lower():
macro_txt = "大盘偏强"
factors.append(macro_txt)
elif "偏弱" in macro_desc or "大跌" in macro_desc or "bearish" in macro_desc.lower():
macro_txt = "大盘偏弱"
factors.append(macro_txt)
elif macro_desc and macro_desc != "宏观未加载":
factors.append("大盘中性")
# 2. 行业因子
if sector_note:
# 把"行业X大跌3%+"简化为"行业偏弱""行业X大涨3%+"简化为"行业偏强"
if "大跌" in sector_note or "下跌" in sector_note:
factors.append("行业偏弱")
elif "大涨" in sector_note:
factors.append("行业偏强")
elif "上涨" in sector_note:
factors.append("行业偏强")
else:
factors.append("行业中性")
# 3. 基本面因子
if fundamentals:
pe = fundamentals.get("pe", 0)
eps = fundamentals.get("eps", 0)
profit_growth = fundamentals.get("profit_growth", fundamentals.get("yoy_profit", ""))
revenue_growth = fundamentals.get("revenue_growth", fundamentals.get("yoy_revenue", ""))
mcap = fundamentals.get("mcap_total", 0)
pe = pe or 0
eps = eps or 0
profit_growth_str = str(profit_growth or "")
revenue_growth_str = str(revenue_growth or "")
# 净利增长
for val in [profit_growth_str, revenue_growth_str]:
try:
v = float(val.replace("%", "").replace("+", ""))
if v > 50:
factors.append("净利增50%+")
break
elif v > 20:
factors.append(f"净利增{int(v)}%")
break
elif v < -20:
factors.append("净利降20%+")
break
except (ValueError, AttributeError):
continue
# PE估值
if 0 < pe < 15:
factors.append("低估值")
elif pe > 100 or pe < 0:
factors.append("高估值")
# 市值
if mcap and mcap > 5000:
factors.append("蓝筹")
# 4. 消息面因子(小果情感)
if news_sentiment:
ns = news_sentiment.get("sentiment", "")
nc = news_sentiment.get("confidence", 0)
if ns == "positive" and nc >= 0.7:
kws = news_sentiment.get("keywords", [])
kw_str = f"{'/'.join(kws[:3])}" if kws else ""
factors.append(f"消息偏多{kw_str}")
elif ns == "negative" and nc >= 0.7:
kws = news_sentiment.get("keywords", [])
kw_str = f"{'/'.join(kws[:3])}" if kws else ""
factors.append(f"消息偏空{kw_str}")
# 5. 技术面(基础信号)
if base_signal and base_signal != "neutral":
factors.append(base_signal)
# 如果没有足够因素,返回信号不充分
if not factors:
return "信号不充分", []
return "".join(factors), factors
def reassess_with_context(code, name, price, cost, shares, current_action,
volume_signal="", sentiment="neutral", is_watchlist=False):
"""reassess_strategy + 多因子信号合成(大盘+行业+技术)
为 per_stock_reassess 等单只场景提供一站式多因子分析
"""
result = reassess_strategy(
code, name, price, cost, shares,
current_action, volume_signal, sentiment, is_watchlist
)
if not result:
return result
# 加载宏观+行业+消息+基本面上下文
try:
macro_bias, macro_desc = load_macro_context()
market_ctx = load_market_context()
stock_sector_map = load_stock_sector_map()
sector_adj = compute_sector_adjustment(code, market_ctx, stock_sector_map)
sector_note = sector_adj.get("note", "")
news_sentiment = load_stock_news_sentiment(code)
fund = load_fundamentals(code)
except Exception:
macro_desc = ""
sector_note = ""
news_sentiment = {}
fund = {}
enriched, _ = enrich_timing_signal(
base_signal=result.get("timing_signal", ""),
macro_desc=macro_desc,
sector_note=sector_note,
profit_pct=(price - cost) / cost * 100 if cost else 0,
stock_category=result.get("stock_category", ""),
is_new_entry=is_watchlist,
fundamentals=fund,
news_sentiment=news_sentiment,
)
result["timing_signal"] = enriched
# 重建 action 文本(同步多因子信号)
try:
if new_action_needs_refresh(result, {"source": "auto"}, price):
_refresh_action_text(result, price, name)
except Exception:
pass
return result
def new_action_needs_refresh(result, old_entry, price):
"""判断宏观/行业调整后是否需要刷新action文本"""
# 自选股和手动策略不做调整,不需要刷新
@@ -1100,7 +1276,25 @@ def regenerate_all(stdout=True):
if target_bias != 1.0 and old_target > 0 and not is_wl:
result["take_profit"] = round(old_target * target_bias, 2)
# 在宏观/行业调整后重建 action 文本(同步调整后的止损/止盈数字
# 加载消息面+基本面(逐个股
news_sentiment = load_stock_news_sentiment(code)
fund = load_fundamentals(code)
# 多因子合成 timing_signal:大盘+行业+消息+基本面+技术
if old_entry.get("source") != "manual":
enriched, _ = enrich_timing_signal(
base_signal=result.get("timing_signal", ""),
macro_desc=macro_desc,
sector_note=sector_note,
profit_pct=profit_pct,
stock_category=result.get("stock_category", ""),
is_new_entry=(source == "watchlist"),
fundamentals=fund,
news_sentiment=news_sentiment,
)
result["timing_signal"] = enriched
# 在宏观/行业/多因子调整后重建 action 文本(同步调整后的止损/止盈数字)
if new_action_needs_refresh(result, old_entry, price):
_refresh_action_text(result, price, name)
@@ -1150,6 +1344,8 @@ def regenerate_all(stdout=True):
sector_ctx_str = f"大盘上涨比{market_breadth}%"
new_entry = {
"code": code, "name": name, "price": price, "cost": cost,
"shares": old_entry.get("shares", 0), # 保留持仓股数
"avg_price": old_entry.get("avg_price", 0), # 保留持仓均价
"action": result["action"],
"stop_loss": result.get("stop_loss"),
"entry_low": result["entry_low"],
@@ -1174,9 +1370,8 @@ def regenerate_all(stdout=True):
new_entry["created_at"] = old_entry["created_at"]
else:
new_entry["created_at"] = result["reassessed_at"]
# 自选股写止盈,持仓股才写
if not is_wl:
new_entry["take_profit"] = result.get("take_profit")
# 自选股写止盈位(用于RR校验),但标签用"目标参考"非"止盈"
new_entry["take_profit"] = result.get("take_profit")
# --- 变更追踪 ---
old_action = old_entry.get("action", "")
@@ -1295,7 +1490,7 @@ def regenerate_all(stdout=True):
# 写回数据文件
json.dump(pf, open(PORTFOLIO_PATH, "w"), ensure_ascii=False, indent=2)
json.dump(wl, open(wl_path, "w"), ensure_ascii=False, indent=2)
json.dump(wl, open(WATCHLIST_PATH, "w"), ensure_ascii=False, indent=2)
# 写 decisions.json
decisions_path = "/home/hmo/web-dashboard/data/decisions.json"