fix: xiaoguo_scanner 榜单更新+看空榜持仓预警
- 修bug:stock_rank_cxd_ths 实为'创新低',改为 stock_rank_lxsz_ths '连续上涨' - 新增6个看多榜(险资举牌)+ 5个看空榜(创新低/持续缩量/量价齐跌/连续下跌/向下突破) - 看空榜自动比对持仓,命中写入 xiaoguo_risk 信号 - 东财热榜静默降级(502不可修) - 看空榜不跳过已扫描,每轮全检
This commit is contained in:
+204
-9
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user