This commit is contained in:
aaron 2026-04-30 10:05:41 +08:00
parent c9e0631140
commit 8ae3ee19b7
8 changed files with 247 additions and 79 deletions

View File

@ -196,14 +196,14 @@ def detect_pullback(df: pd.DataFrame) -> dict | None:
2. 5 日内有 MA5 > MA20 近期处于上升趋势 2. 5 日内有 MA5 > MA20 近期处于上升趋势
3. 价格靠近 MA10 MA20 (<3%) 3. 价格靠近 MA10 MA20 (<3%)
4. 3 日缩量相对前 5 当日开始放量反弹 4. 3 日缩量相对前 5 当日开始放量反弹
5. RSI 30-70放宽 5. RSI 只做风险提示不作为回踩成立的硬条件
""" """
last = df.iloc[-1] last = df.iloc[-1]
required = ["close", "vol", "ma5", "ma10", "ma20", "ma60", "rsi14"] required = ["close", "vol", "ma5", "ma10", "ma20", "ma60"]
if any(c not in df.columns for c in required): if any(c not in df.columns for c in required):
return None return None
if pd.isna(last["ma60"]) or pd.isna(last["rsi14"]): if pd.isna(last["ma60"]):
return None return None
# 中长期上升趋势 # 中长期上升趋势
@ -251,13 +251,10 @@ def detect_pullback(df: pd.DataFrame) -> dict | None:
if not (shrinking or bouncing_today): if not (shrinking or bouncing_today):
return None return None
# RSI放宽至 30-70
if not (30 <= last["rsi14"] <= 70):
return None
support_ma = "MA10" if near_ma10 else "MA20" support_ma = "MA10" if near_ma10 else "MA20"
support_price = last["ma10"] if near_ma10 else last["ma20"] support_price = last["ma10"] if near_ma10 else last["ma20"]
shrink_ratio = recent_3_vol / prev_5_vol if prev_5_vol > 0 else 1 shrink_ratio = recent_3_vol / prev_5_vol if prev_5_vol > 0 else 1
rsi14 = last.get("rsi14") if "rsi14" in df.columns else None
return { return {
"signal_type": EntrySignal.PULLBACK, "signal_type": EntrySignal.PULLBACK,
@ -266,6 +263,7 @@ def detect_pullback(df: pd.DataFrame) -> dict | None:
"support_price": round(support_price, 2), "support_price": round(support_price, 2),
"volume_shrink_ratio": round(shrink_ratio, 2), "volume_shrink_ratio": round(shrink_ratio, 2),
"bouncing": bouncing_today, "bouncing": bouncing_today,
"rsi14": round(float(rsi14), 1) if rsi14 is not None and not pd.isna(rsi14) else None,
}, },
} }
@ -343,16 +341,16 @@ def detect_reversal(df: pd.DataFrame) -> dict | None:
1. 5 日有 3 日以上下跌 1. 5 日有 3 日以上下跌
2. 今日放量长阳pct_chg > 3%, vol > vol_ma5 * 1.5 2. 今日放量长阳pct_chg > 3%, vol > vol_ma5 * 1.5
3. 收盘价高于近 5 日最高价强势反转 3. 收盘价高于近 5 日最高价强势反转
4. RSI 从低位回升 5 日最低 RSI < 40 4. RSI 低位只做背景信息不作为反转成立的硬条件
""" """
if len(df) < 10: if len(df) < 10:
return None return None
last = df.iloc[-1] last = df.iloc[-1]
required = ["close", "high", "low", "vol", "pct_chg", "rsi14", "vol_ma5"] required = ["close", "high", "low", "vol", "pct_chg", "vol_ma5"]
if any(c not in df.columns for c in required): if any(c not in df.columns for c in required):
return None return None
if pd.isna(last["vol_ma5"]) or last["vol_ma5"] == 0 or pd.isna(last["rsi14"]): if pd.isna(last["vol_ma5"]) or last["vol_ma5"] == 0:
return None return None
# 近 5 日有 3 日以上下跌 # 近 5 日有 3 日以上下跌
@ -372,11 +370,10 @@ def detect_reversal(df: pd.DataFrame) -> dict | None:
if last["close"] < high_5d: if last["close"] < high_5d:
return None return None
# RSI 曾在低位 min_rsi = None
rsi_5d = df["rsi14"].tail(5) if "rsi14" in df.columns:
min_rsi = rsi_5d.min() rsi_5d = df["rsi14"].tail(5)
if not pd.isna(min_rsi) and min_rsi > 40: min_rsi = rsi_5d.min()
return None
return { return {
"signal_type": EntrySignal.REVERSAL, "signal_type": EntrySignal.REVERSAL,
@ -384,7 +381,7 @@ def detect_reversal(df: pd.DataFrame) -> dict | None:
"down_days": down_count, "down_days": down_count,
"reversal_pct": round(last["pct_chg"], 2), "reversal_pct": round(last["pct_chg"], 2),
"volume_ratio": round(last["vol"] / last["vol_ma5"], 2), "volume_ratio": round(last["vol"] / last["vol_ma5"], 2),
"min_rsi": round(float(min_rsi), 1), "min_rsi": round(float(min_rsi), 1) if min_rsi is not None and not pd.isna(min_rsi) else None,
}, },
} }
@ -432,9 +429,9 @@ def _score_breakout(df: pd.DataFrame, signal: dict) -> float:
if "rsi14" in df.columns and not pd.isna(last["rsi14"]): if "rsi14" in df.columns and not pd.isna(last["rsi14"]):
if 50 <= last["rsi14"] <= 75: if 50 <= last["rsi14"] <= 75:
score += 10 score += 4
elif 45 <= last["rsi14"] <= 80: elif 45 <= last["rsi14"] <= 80:
score += 5 score += 2
return min(score, 100) return min(score, 100)
@ -474,9 +471,9 @@ def _score_breakout_confirm(df: pd.DataFrame, signal: dict) -> float:
if "rsi14" in df.columns and not pd.isna(last["rsi14"]): if "rsi14" in df.columns and not pd.isna(last["rsi14"]):
if 50 <= last["rsi14"] <= 70: if 50 <= last["rsi14"] <= 70:
score += 15 score += 4
elif 45 <= last["rsi14"] <= 75: elif 45 <= last["rsi14"] <= 75:
score += 8 score += 2
return min(score, 100) return min(score, 100)
@ -519,9 +516,9 @@ def _score_pullback(df: pd.DataFrame, signal: dict) -> float:
if "rsi14" in df.columns and not pd.isna(last["rsi14"]): if "rsi14" in df.columns and not pd.isna(last["rsi14"]):
if 40 <= last["rsi14"] <= 55: if 40 <= last["rsi14"] <= 55:
score += 15 score += 3
elif 35 <= last["rsi14"] <= 65: elif 35 <= last["rsi14"] <= 65:
score += 8 score += 1
return min(score, 100) return min(score, 100)
@ -588,14 +585,6 @@ def _score_reversal(df: pd.DataFrame, signal: dict) -> float:
elif vol_ratio > 1.5: elif vol_ratio > 1.5:
score += 12 score += 12
min_rsi = signal["details"].get("min_rsi", 50)
if min_rsi < 25:
score += 20 # 极度超卖反转
elif min_rsi < 30:
score += 15
elif min_rsi < 35:
score += 10
# 站上 MA5 加分 # 站上 MA5 加分
if "ma5" in df.columns and not pd.isna(last["ma5"]): if "ma5" in df.columns and not pd.isna(last["ma5"]):
if last["close"] > last["ma5"]: if last["close"] > last["ma5"]:

View File

@ -1,6 +1,6 @@
"""买卖信号生成 """买卖信号生成
基于技术指标判断 7 种买入信号生成综合技术面评分 基于量价与趋势判断辅助信号RSI 等滞后指标只做风险备注不主导推荐
""" """
import logging import logging
@ -53,7 +53,7 @@ def _check_macd_golden(df: pd.DataFrame) -> bool:
def _check_rsi_healthy(df: pd.DataFrame) -> bool: def _check_rsi_healthy(df: pd.DataFrame) -> bool:
"""RSI 健康区间: RSI(14) 在 40-70""" """RSI 节奏区间: RSI(14) 在 40-70仅作为辅助节奏提示"""
if df.empty: if df.empty:
return False return False
rsi = df.iloc[-1].get("rsi14", 50) rsi = df.iloc[-1].get("rsi14", 50)
@ -258,7 +258,7 @@ def generate_signals(ts_code: str, name: str = "") -> TechnicalSignal:
(ma_bullish, 15), (ma_bullish, 15),
(volume_breakout, 20), (volume_breakout, 20),
(macd_golden, 15), (macd_golden, 15),
(rsi_healthy, 10), (rsi_healthy, 3),
(pullback_support, 15), (pullback_support, 15),
(big_yang, 15), (big_yang, 15),
(boll_support, 10), (boll_support, 10),

View File

@ -384,12 +384,12 @@ async def diagnose_stock(ts_code: str, mode: str = Query("entry")):
) )
signal_str = ( signal_str = (
f"推荐体系评分: 趋势评分={signals.trend_score}/100均线排列+高低点结构+MA20方向主评分10%权重), " f"推荐体系评分: 趋势评分={signals.trend_score}/100均线排列+高低点结构+MA20方向主评分辅助项), "
f"辅助信号计数={signals.signal_count}/7触发计分仅供参考不参与主评分), " f"辅助信号计数={signals.signal_count}/7触发计分仅供节奏参考,不作为主评分裁判), "
f"均线多头: {signals.ma_bullish}, " f"均线多头: {signals.ma_bullish}, "
f"放量突破: {signals.volume_breakout}, " f"放量突破: {signals.volume_breakout}, "
f"MACD金叉: {signals.macd_golden}, " f"MACD金叉: {signals.macd_golden}, "
f"RSI健康: {signals.rsi_healthy}, " f"RSI节奏区间: {signals.rsi_healthy}, "
f"缩量回踩: {signals.pullback_support}, " f"缩量回踩: {signals.pullback_support}, "
f"放量长阳: {signals.big_yang}, " f"放量长阳: {signals.big_yang}, "
f"布林支撑: {signals.boll_support}, " f"布林支撑: {signals.boll_support}, "
@ -471,7 +471,7 @@ async def diagnose_stock(ts_code: str, mode: str = Query("entry")):
rec_result = await db.execute( rec_result = await db.execute(
text( text(
"SELECT score, supply_demand_score, price_action_score, " "SELECT score, supply_demand_score, price_action_score, "
"technical_score, position_score, sector, signal " "capital_score, technical_score, position_score, sector, signal, entry_signal_type "
"FROM recommendations " "FROM recommendations "
"WHERE ts_code = :code " "WHERE ts_code = :code "
"AND (action_plan IN ('可操作', '重点关注') OR COALESCE(llm_score, 0) >= 6 OR score >= 56) " "AND (action_plan IN ('可操作', '重点关注') OR COALESCE(llm_score, 0) >= 6 OR score >= 56) "
@ -484,12 +484,14 @@ async def diagnose_stock(ts_code: str, mode: str = Query("entry")):
rm = rec_row._mapping rm = rec_row._mapping
rec_score_str = ( rec_score_str = (
f"\n推荐体系评分: 综合={rm['score']}, " f"\n推荐体系评分: 综合={rm['score']}, "
f"供需={rm['supply_demand_score']}(50%权重), " f"资金={rm['capital_score']}(资金顺势核心), "
f"形态={rm['price_action_score']}(40%权重), " f"供需={rm['supply_demand_score']}, "
f"趋势={rm['technical_score']}(10%权重), " f"形态={rm['price_action_score']}, "
f"趋势={rm['technical_score']}, "
f"位置安全={rm['position_score']}, " f"位置安全={rm['position_score']}, "
f"板块={rm['sector']}, " f"板块={rm['sector']}, "
f"信号={rm['signal']}" f"信号={rm['signal']}, "
f"入场类型={rm['entry_signal_type']}"
) )
except Exception: except Exception:
pass pass
@ -556,11 +558,12 @@ async def diagnose_stock(ts_code: str, mode: str = Query("entry")):
重要提示 重要提示
1. 你不是在写传统研报而是在给交易作战台输出结构化会诊意见 1. 你不是在写传统研报而是在给交易作战台输出结构化会诊意见
2. 如果有推荐体系评分操作计划跟踪信息请优先沿用当前推荐体系而不是另起一套标准 2. 如果有推荐体系评分操作计划跟踪信息请优先沿用当前推荐体系而不是另起一套标准
3. 趋势评分是推荐体系的技术面核心分数均线排列40+高低点结构35+MA20方向25=满分100辅助信号计数仅供参考不参与主评分 3. 当前推荐体系以资金顺势 + 主线板块 + 供需/量价 + 趋势为主技术指标只做节奏和风控确认
4. 位置安全评分高(>80)表示股价处于相对低位(<40)表示可能追高 4. RSIMACDKDJ 等滞后指标不能单独决定买卖RSI 超买只提示追高风险超卖只提示弱势或反弹弹性不等于可买或不可买
5. 板块信息和推荐体系信息优先级高于单一技术指标 5. 位置安全评分高(>80)表示股价处于相对低位(<40)表示可能追高
6. 先给结论和动作再解释原因不要先铺陈背景再拖到最后才下结论 6. 板块信息资金面和推荐体系信息优先级高于单一技术指标
7. 如果证据不足也要明确给出观察回避不能写成模糊建议 7. 先给结论和动作再解释原因不要先铺陈背景再拖到最后才下结论
8. 如果证据不足也要明确给出观察回避不能写成模糊建议
{freshness_note} {freshness_note}
请严格按以下 Markdown 结构输出不要写成泛泛长文 请严格按以下 Markdown 结构输出不要写成泛泛长文

View File

@ -119,7 +119,7 @@ class TechnicalSignal(BaseModel):
ma_bullish: bool = False # 均线多头排列 ma_bullish: bool = False # 均线多头排列
volume_breakout: bool = False # 放量突破 volume_breakout: bool = False # 放量突破
macd_golden: bool = False # MACD金叉 macd_golden: bool = False # MACD金叉
rsi_healthy: bool = False # RSI健康区间 rsi_healthy: bool = False # RSI节奏区间(辅助参考)
pullback_support: bool = False # 缩量回踩支撑 pullback_support: bool = False # 缩量回踩支撑
big_yang: bool = False # 底部放量长阳 big_yang: bool = False # 底部放量长阳
boll_support: bool = False # 布林带下轨支撑 boll_support: bool = False # 布林带下轨支撑
@ -148,8 +148,8 @@ class Recommendation(BaseModel):
sector_score: float sector_score: float
capital_score: float capital_score: float
technical_score: float technical_score: float
supply_demand_score: float = 0 # 供需评分主评分50%权重) supply_demand_score: float = 0 # 供需评分
price_action_score: float = 0 # 价格行为评分主评分40%权重) price_action_score: float = 0 # 价格行为评分
position_score: float = 50 # 位置安全得分 position_score: float = 50 # 位置安全得分
valuation_score: float = 50 # 估值安全得分 valuation_score: float = 50 # 估值安全得分
signal: str # BUY / SELL / HOLD signal: str # BUY / SELL / HOLD

View File

@ -3,10 +3,10 @@
三阶段管道 三阶段管道
Step 1: 主线定位 把实时板块/快照板块归一成系统 MarketTheme Step 1: 主线定位 把实时板块/快照板块归一成系统 MarketTheme
Step 2: 主题内选股 从主线主题成分领涨股和实时异动中召回候选 Step 2: 主题内选股 从主线主题成分领涨股和实时异动中召回候选
Step 3: 深度分析 供需 + 价格行为 + 趋势 + LLM (10-15只推荐) Step 3: 深度分析 资金顺势 + 供需 + 价格行为 + 趋势 + LLM
评分公式供需关系 50% + 价格行为 40% + 趋势 10% 评分公式资金顺势 + 供需关系 + 价格行为 + 趋势
主题地位和资金流作为前置上下文涨停/广度只作为辅助证据 资金流和主题地位进入主评分RSI/MACD 只作为节奏与风险参考
风险乘数惩罚取最大而非叠加防过度惩罚奖励可叠加 风险乘数惩罚取最大而非叠加防过度惩罚奖励可叠加
@ -605,10 +605,12 @@ async def _build_recommendations(
total = len(candidates) total = len(candidates)
signal_counts = {"breakout": 0, "breakout_confirm": 0, "pullback": 0, "launch": 0, "reversal": 0, "none": 0} signal_counts = {"breakout": 0, "breakout_confirm": 0, "pullback": 0, "launch": 0, "reversal": 0, "none": 0}
score_weights = strategy_profile.score_weights if strategy_profile else { score_weights = strategy_profile.score_weights if strategy_profile else {
"supply_demand": 0.50, "capital_momentum": 0.30,
"price_action": 0.40, "supply_demand": 0.30,
"trend": 0.10, "price_action": 0.25,
"trend": 0.15,
} }
score_weights = _normalize_score_weights(score_weights)
signal_priority = strategy_profile.entry_signal_priority if strategy_profile else [] signal_priority = strategy_profile.entry_signal_priority if strategy_profile else []
buy_threshold = strategy_profile.buy_threshold if strategy_profile else 60 buy_threshold = strategy_profile.buy_threshold if strategy_profile else 60
@ -651,6 +653,8 @@ async def _build_recommendations(
supply_demand_score = score_supply_demand(df) supply_demand_score = score_supply_demand(df)
price_action_score = _score_price_action(df, entry_signal) price_action_score = _score_price_action(df, entry_signal)
trend_score = _score_trend(df) trend_score = _score_trend(df)
capital_score = _score_capital_simple(stock)
flow_momentum_score = _score_flow_momentum(stock, sector, hot_sectors)
last = df.iloc[-1] last = df.iloc[-1]
trend_penalty = 1.0 trend_penalty = 1.0
@ -660,6 +664,7 @@ async def _build_recommendations(
trend_penalty = 0.82 trend_penalty = 0.82
final_score = ( final_score = (
flow_momentum_score * score_weights["capital_momentum"] +
supply_demand_score * score_weights["supply_demand"] + supply_demand_score * score_weights["supply_demand"] +
price_action_score * score_weights["price_action"] + price_action_score * score_weights["price_action"] +
trend_score * score_weights["trend"] trend_score * score_weights["trend"]
@ -699,6 +704,8 @@ async def _build_recommendations(
if entry_signal.get("signal_score", 0) >= 80: if entry_signal.get("signal_score", 0) >= 80:
final_score *= 1.10 final_score *= 1.10
final_score *= _flow_confirmation_multiplier(stock, hot_theme_match, market_temp)
if not hot_theme_match: if not hot_theme_match:
final_score *= 0.82 final_score *= 0.82
elif hot_theme_match not in hot_sectors[:5]: elif hot_theme_match not in hot_sectors[:5]:
@ -721,11 +728,25 @@ async def _build_recommendations(
level = _score_to_level(final_score) level = _score_to_level(final_score)
signal = "HOLD" signal = "HOLD"
position_score = tech_signal.position_score if tech_signal else 50 position_score = tech_signal.position_score if tech_signal else 50
flow_confirmed = _is_flow_confirmed(
stock=stock,
flow_momentum_score=flow_momentum_score,
trend_score=trend_score,
price_action_score=price_action_score,
)
effective_signal_name = signal_name
if signal_name == "none" and flow_confirmed:
effective_signal_name = "flow_momentum"
if ( if (
signal_type != EntrySignal.NONE signal_type != EntrySignal.NONE
and entry_signal.get("signal_score", 0) >= 50 and entry_signal.get("signal_score", 0) >= 50
and position_score >= 30 and position_score >= 30
and final_score >= buy_threshold and final_score >= buy_threshold
) or (
flow_confirmed
and position_score >= 30
and final_score >= buy_threshold + 2
): ):
signal = "BUY" signal = "BUY"
@ -796,13 +817,13 @@ async def _build_recommendations(
if target_price < entry_price * (1 + min_target_pct): if target_price < entry_price * (1 + min_target_pct):
target_price = round(entry_price * (1 + min_target_pct), 2) target_price = round(entry_price * (1 + min_target_pct), 2)
stock["entry_signal_type"] = effective_signal_name
reasons = _generate_reasons(stock, entry_signal, tech_signal, df, intraday) reasons = _generate_reasons(stock, entry_signal, tech_signal, df, intraday)
stock["entry_signal_type"] = signal_name
risk_note = _generate_risk_note(market_temp, tech_signal, stock) risk_note = _generate_risk_note(market_temp, tech_signal, stock)
vol_pattern = analyze_volume_pattern(df) vol_pattern = analyze_volume_pattern(df)
entry_timing = _generate_entry_timing(signal_name, intraday) entry_timing = _generate_entry_timing(effective_signal_name, intraday)
trade_plan = _build_trade_plan( trade_plan = _build_trade_plan(
signal_type=signal_name, signal_type=effective_signal_name,
score=final_score, score=final_score,
market_temp=market_temp, market_temp=market_temp,
sector_stage=sector_stage, sector_stage=sector_stage,
@ -820,7 +841,7 @@ async def _build_recommendations(
score=round(final_score, 1), score=round(final_score, 1),
market_temp_score=round(market_temp_score, 1), market_temp_score=round(market_temp_score, 1),
sector_score=round(_get_sector_heat(sector, hot_sectors), 1), sector_score=round(_get_sector_heat(sector, hot_sectors), 1),
capital_score=round(_score_capital_simple(stock), 1), capital_score=round(capital_score, 1),
technical_score=round(trend_score, 1), technical_score=round(trend_score, 1),
supply_demand_score=round(supply_demand_score, 1), supply_demand_score=round(supply_demand_score, 1),
price_action_score=round(price_action_score, 1), price_action_score=round(price_action_score, 1),
@ -834,7 +855,7 @@ async def _build_recommendations(
risk_note=risk_note, risk_note=risk_note,
level=level, level=level,
strategy=strategy_profile.strategy_id if strategy_profile else "trend_breakout", strategy=strategy_profile.strategy_id if strategy_profile else "trend_breakout",
entry_signal_type=signal_name, entry_signal_type=effective_signal_name,
entry_timing=entry_timing, entry_timing=entry_timing,
action_plan=trade_plan["action_plan"], action_plan=trade_plan["action_plan"],
trigger_condition=trade_plan["trigger_condition"], trigger_condition=trade_plan["trigger_condition"],
@ -869,8 +890,9 @@ async def _build_recommendations(
"hot_theme_name": hot_theme_match.sector_name if hot_theme_match else "", "hot_theme_name": hot_theme_match.sector_name if hot_theme_match else "",
"hot_theme_aliases": hot_theme_match.theme_aliases if hot_theme_match else [], "hot_theme_aliases": hot_theme_match.theme_aliases if hot_theme_match else [],
"stock_role_hint": stock.get("stock_role_hint", "待判断"), "stock_role_hint": stock.get("stock_role_hint", "待判断"),
"entry_signal_type": signal_name, "entry_signal_type": effective_signal_name,
"entry_signal_score": round(entry_signal.get("signal_score", 0), 1), "entry_signal_score": round(entry_signal.get("signal_score", 0), 1),
"flow_momentum_score": round(flow_momentum_score, 1),
"signal_matches_profile": signal_matches_profile, "signal_matches_profile": signal_matches_profile,
"risk_tags": _build_risk_tags(market_temp, tech_signal, sector_stage, trend_penalty), "risk_tags": _build_risk_tags(market_temp, tech_signal, sector_stage, trend_penalty),
"focus_points": _build_focus_points(stock, entry_signal, tech_signal, vol_pattern, sector_stage), "focus_points": _build_focus_points(stock, entry_signal, tech_signal, vol_pattern, sector_stage),
@ -1042,6 +1064,145 @@ async def _build_recommendations(
return recommendations return recommendations
# ── 主评分辅助 ──
def _normalize_score_weights(weights: dict[str, float]) -> dict[str, float]:
"""兼容旧策略权重,并保证资金顺势进入主评分。"""
defaults = {
"capital_momentum": 0.30,
"supply_demand": 0.30,
"price_action": 0.25,
"trend": 0.15,
}
merged = {**defaults, **(weights or {})}
keys = ["capital_momentum", "supply_demand", "price_action", "trend"]
total = sum(max(float(merged.get(k, 0) or 0), 0) for k in keys)
if total <= 0:
return defaults
return {k: max(float(merged.get(k, 0) or 0), 0) / total for k in keys}
def _score_flow_momentum(stock: dict, sector_name: str, hot_sectors: list[SectorInfo]) -> float:
"""资金顺势评分:个股资金在场 + 主线板块顺风 + 活跃度确认。"""
main_net = float(stock.get("main_net_inflow", 0) or 0)
inflow_ratio = float(stock.get("inflow_ratio", 0) or 0)
turnover_rate = float(stock.get("turnover_rate", 0) or 0)
volume_ratio = stock.get("volume_ratio")
volume_ratio = float(volume_ratio) if volume_ratio not in (None, "") else 0.0
recall_score = float(stock.get("recall_score", 0) or 0)
sector_heat = _get_sector_heat(sector_name, hot_sectors)
sector_limit_up = _get_sector_limit_up(sector_name, hot_sectors)
score = 0.0
# 个股主力资金占 40 分。负流入不一票否决,但会明显降权。
if main_net > 15000:
score += 28
elif main_net > 8000:
score += 24
elif main_net > 3000:
score += 18
elif main_net > 0:
score += 10
elif main_net < -5000:
score -= 12
elif main_net < 0:
score -= 6
if inflow_ratio > 12:
score += 12
elif inflow_ratio > 8:
score += 9
elif inflow_ratio > 4:
score += 6
elif inflow_ratio > 0:
score += 3
elif inflow_ratio < -8:
score -= 8
# 主线板块和涨停广度占 25 分。
if sector_heat >= 80:
score += 16
elif sector_heat >= 65:
score += 12
elif sector_heat >= 50:
score += 8
elif sector_heat >= 35:
score += 4
if sector_limit_up >= 5:
score += 9
elif sector_limit_up >= 3:
score += 6
elif sector_limit_up >= 1:
score += 3
# 活跃度和召回强度占 35 分。
if volume_ratio >= 2.5:
score += 12
elif volume_ratio >= 1.8:
score += 9
elif volume_ratio >= 1.2:
score += 6
elif volume_ratio > 0:
score += 2
if 4 <= turnover_rate <= 12:
score += 10
elif 2 <= turnover_rate <= 20:
score += 7
elif turnover_rate > 0:
score += 3
score += min(max(recall_score, 0), 100) * 0.13
return round(max(0, min(score, 100)), 1)
def _flow_confirmation_multiplier(stock: dict, hot_theme_match: SectorInfo | None, market_temp: MarketTemperature) -> float:
"""资金与主线共振时加分,资金背离时降权。"""
main_net = float(stock.get("main_net_inflow", 0) or 0)
inflow_ratio = float(stock.get("inflow_ratio", 0) or 0)
volume_ratio = stock.get("volume_ratio")
volume_ratio = float(volume_ratio) if volume_ratio not in (None, "") else 0.0
multiplier = 1.0
if hot_theme_match and main_net > 3000 and inflow_ratio > 3:
multiplier += 0.05
if hot_theme_match and volume_ratio >= 1.5 and market_temp.temperature >= 50:
multiplier += 0.04
if main_net < -3000 and inflow_ratio < -3:
multiplier -= 0.10
elif main_net < 0 and not hot_theme_match:
multiplier -= 0.06
return max(0.82, min(multiplier, 1.12))
def _is_flow_confirmed(
stock: dict,
flow_momentum_score: float,
trend_score: float,
price_action_score: float,
) -> bool:
"""允许资金顺势票进入候选,不必等 RSI/MACD 等滞后指标确认。"""
main_net = float(stock.get("main_net_inflow", 0) or 0)
inflow_ratio = float(stock.get("inflow_ratio", 0) or 0)
volume_ratio = stock.get("volume_ratio")
volume_ratio = float(volume_ratio) if volume_ratio not in (None, "") else 0.0
tags = set(stock.get("recall_tags", []) or [])
return (
flow_momentum_score >= 72
and main_net > 2000
and inflow_ratio > 2
and volume_ratio >= 1.15
and trend_score >= 45
and price_action_score >= 42
and bool(tags & {"hot_theme_core", "theme_leader", "top_theme_member", "sector_recall"})
)
# ── 价格行为评分 ── # ── 价格行为评分 ──
@ -1309,6 +1470,7 @@ def _generate_entry_timing(signal_type: str, intraday: bool) -> str:
"pullback": "盘中靠近支撑位时分批进场尾盘14:30确认支撑有效可加仓", "pullback": "盘中靠近支撑位时分批进场尾盘14:30确认支撑有效可加仓",
"launch": "早盘放量确认后即可进场注意开盘9:30-10:00量能", "launch": "早盘放量确认后即可进场注意开盘9:30-10:00量能",
"reversal": "午后13:30确认不回落再进场避免早盘追高", "reversal": "午后13:30确认不回落再进场避免早盘追高",
"flow_momentum": "优先看资金是否延续流入和板块前排是否继续走强,分时承接确认后再分批",
} }
return timing_map.get(signal_type, "盘中观察量价配合,确认信号后进场") return timing_map.get(signal_type, "盘中观察量价配合,确认信号后进场")
@ -1334,7 +1496,8 @@ def _build_trade_plan(
"pullback": "回踩支撑", "pullback": "回踩支撑",
"launch": "缩量整理后启动", "launch": "缩量整理后启动",
"reversal": "放量反转", "reversal": "放量反转",
}.get(signal_type, "技术信号") "flow_momentum": "资金顺势确认",
}.get(signal_type, "资金与量价信号")
if market_temp.temperature < 40 or sector_stage in ("end",): if market_temp.temperature < 40 or sector_stage in ("end",):
action_plan = "观察" action_plan = "观察"
@ -1421,9 +1584,13 @@ def _generate_reasons(
EntrySignal.PULLBACK: "回踩型", EntrySignal.LAUNCH: "启动型", EntrySignal.PULLBACK: "回踩型", EntrySignal.LAUNCH: "启动型",
EntrySignal.REVERSAL: "反转型"} EntrySignal.REVERSAL: "反转型"}
entry_label = signal_map.get(signal_type, "") entry_label = signal_map.get(signal_type, "")
if stock.get("entry_signal_type") == "flow_momentum":
entry_label = "资金顺势型"
# 入场信号 # 入场信号
if entry_label and signal_type: if entry_label and stock.get("entry_signal_type") == "flow_momentum":
reasons.append("主线主题内资金顺势增强,优先跟踪承接而非等待滞后指标")
elif entry_label and signal_type:
st = signal_type.value st = signal_type.value
if st == "breakout": if st == "breakout":
breakout_pct = details.get("breakout_pct", 0) breakout_pct = details.get("breakout_pct", 0)
@ -1492,6 +1659,8 @@ def _generate_risk_note(
notes.append("启动型整理可能延长,注意时间成本") notes.append("启动型整理可能延长,注意时间成本")
elif entry_type == "reversal": elif entry_type == "reversal":
notes.append("反转型可能二次探底,确认底部后再加仓") notes.append("反转型可能二次探底,确认底部后再加仓")
elif entry_type == "flow_momentum":
notes.append("资金顺势型需防板块分歧和资金一日游,重点看次日承接")
if market.temperature < 30: if market.temperature < 30:
notes.append("市场情绪偏冷,系统性风险较高") notes.append("市场情绪偏冷,系统性风险较高")
@ -1541,6 +1710,8 @@ def _build_focus_points(
signal_type = entry_signal.get("signal_type") signal_type = entry_signal.get("signal_type")
if signal_type and getattr(signal_type, "value", "none") != "none": if signal_type and getattr(signal_type, "value", "none") != "none":
points.append(f"确认{signal_type.value}信号是否延续") points.append(f"确认{signal_type.value}信号是否延续")
elif stock.get("entry_signal_type") == "flow_momentum":
points.append("确认主力流入和板块前排强度是否延续")
if stock.get("main_net_inflow", 0) > 0: if stock.get("main_net_inflow", 0) > 0:
points.append("观察主力流入是否继续放大") points.append("观察主力流入是否继续放大")
if vol_pattern.get("volume_trend"): if vol_pattern.get("volume_trend"):
@ -1623,7 +1794,7 @@ def _summarize_for_llm(df, entry_signal: dict, tech_signal: TechnicalSignal | No
vol_conclusion += "近5日缩量" vol_conclusion += "近5日缩量"
parts.append(vol_conclusion) parts.append(vol_conclusion)
# ── MACD 结论 ── # ── MACD 结论(节奏参考) ──
dif = last.get("dif", 0) or 0 dif = last.get("dif", 0) or 0
dea = last.get("dea", 0) or 0 dea = last.get("dea", 0) or 0
macd_desc = "" macd_desc = ""
@ -1643,19 +1814,19 @@ def _summarize_for_llm(df, entry_signal: dict, tech_signal: TechnicalSignal | No
macd_desc += ",零轴上方(偏多)" macd_desc += ",零轴上方(偏多)"
else: else:
macd_desc += ",零轴下方(偏空)" macd_desc += ",零轴下方(偏空)"
parts.append(macd_desc or "MACD数据不足") parts.append((macd_desc or "MACD数据不足") + ";仅作节奏参考")
# ── RSI 结论 ── # ── RSI 结论(风险提示,不做买卖裁判) ──
rsi = last.get("rsi14", 50) rsi = last.get("rsi14", 50)
if not pd.isna(rsi): if not pd.isna(rsi):
if rsi > 80: if rsi > 80:
parts.append(f"RSI14={rsi:.0f}超买区,回调风险大") parts.append(f"RSI14={rsi:.0f}偏热,提示追高风险但不单独否决资金顺势")
elif rsi > 70: elif rsi > 70:
parts.append(f"RSI14={rsi:.0f},偏高,注意追高风险") parts.append(f"RSI14={rsi:.0f},偏高,注意追高风险")
elif rsi >= 40: elif rsi >= 40:
parts.append(f"RSI14={rsi:.0f}健康区间") parts.append(f"RSI14={rsi:.0f}节奏中性")
else: else:
parts.append(f"RSI14={rsi:.0f},偏低,可能超卖") parts.append(f"RSI14={rsi:.0f},偏低,提示弱势或反弹弹性,不单独构成买点")
# ── 价格位置结论 ── # ── 价格位置结论 ──
if tech_signal: if tech_signal:

View File

@ -27,6 +27,7 @@ async def prefilter_single_stock(candidate: dict, market_summary: str) -> dict:
主题别名: {", ".join(candidate.get("hot_theme_aliases", []) or [""])} 主题别名: {", ".join(candidate.get("hot_theme_aliases", []) or [""])}
召回来源: {', '.join(candidate.get('recall_tags', []) or ['未标注'])} 召回来源: {', '.join(candidate.get('recall_tags', []) or ['未标注'])}
规则参考分: {candidate.get('quant_score', 0)}/100 规则参考分: {candidate.get('quant_score', 0)}/100
资金顺势分: {candidate.get('flow_momentum_score', 0)}/100
位置安全: {candidate.get('position_score', 50)}/100 位置安全: {candidate.get('position_score', 50)}/100
当前价: {candidate.get('current_price', '未知')} 当前价: {candidate.get('current_price', '未知')}
主题阶段: {candidate.get('sector_stage', '未知')} 主题阶段: {candidate.get('sector_stage', '未知')}
@ -109,6 +110,7 @@ async def analyze_single_stock(candidate: dict, market_summary: str) -> dict:
主题: {candidate.get('sector', '未知')} 主题: {candidate.get('sector', '未知')}
主线主题匹配: {"是,匹配 " + candidate.get("hot_theme_name", "") if candidate.get("hot_theme_matched") else ""} 主线主题匹配: {"是,匹配 " + candidate.get("hot_theme_name", "") if candidate.get("hot_theme_matched") else ""}
规则参考分: {candidate.get('quant_score', 0)}/100 规则参考分: {candidate.get('quant_score', 0)}/100
资金顺势分: {candidate.get('flow_momentum_score', 0)}/100
位置安全: {candidate.get('position_score', 50)}/100 位置安全: {candidate.get('position_score', 50)}/100
当前价: {candidate.get('current_price', '未知')}""" 当前价: {candidate.get('current_price', '未知')}"""

View File

@ -126,9 +126,11 @@ SINGLE_STOCK_ANALYSIS_PROMPT = """\
你的原则 你的原则
1. 量化分数只是参考不是最终答案 1. 量化分数只是参考不是最终答案
2. 如果板块地位量价质量位置或时机不匹配可以直接否决高分股 2. A股先看资金是否在主线里形成合力再看K线形态是否给出可执行边界
3. 如果股票具备明确触发与失效边界即使量化分不是最高也可以提升优先级 3. RSIMACDKDJ 这类滞后指标只能作为节奏和风险备注不能因为超买/超卖本身否决资金顺势标的
4. 输出的是交易裁决单不是研报 4. 如果板块地位资金延续量价质量位置或时机不匹配可以直接否决高分股
5. 如果股票具备明确触发与失效边界即使量化分不是最高也可以提升优先级
6. 输出的是交易裁决单不是研报
请严格输出 JSON不要输出 Markdown不要添加多余解释字段如下 请严格输出 JSON不要输出 Markdown不要添加多余解释字段如下
{ {
@ -163,9 +165,10 @@ STOCK_PREFILTER_PROMPT = """\
你的原则 你的原则
1. 这一步不是最终买卖结论只做资源分配 1. 这一步不是最终买卖结论只做资源分配
2. 不能因为某一个规则分数低就直接忽略要看题材位置角色量价异常时机感 2. 优先识别资金正在进攻的主线前排或强承接标的
3. 可以容忍不标准的形态但不能容忍明显失真明显追高明显没有交易边界的票 3. 不能因为 RSI/MACD/KDJ 等滞后指标不好看就直接忽略要看题材位置角色量价异常时机感
4. 输出必须是 JSON不要输出 Markdown 4. 可以容忍不标准的形态但不能容忍明显失真明显追高明显没有交易边界的票
5. 输出必须是 JSON不要输出 Markdown
字段格式 字段格式
{ {

View File

@ -50,7 +50,7 @@ def get_strategy_profile_by_id(strategy_id: str) -> StrategyProfile:
name="主线突破", name="主线突破",
description="市场偏强,优先寻找主线板块内的突破和突破确认。", description="市场偏强,优先寻找主线板块内的突破和突破确认。",
entry_signal_priority=["breakout", "breakout_confirm", "launch", "pullback", "reversal"], entry_signal_priority=["breakout", "breakout_confirm", "launch", "pullback", "reversal"],
score_weights={"supply_demand": 0.45, "price_action": 0.35, "trend": 0.20}, score_weights={"capital_momentum": 0.30, "supply_demand": 0.30, "price_action": 0.25, "trend": 0.15},
min_score=62, min_score=62,
buy_threshold=66, buy_threshold=66,
max_position_pct=30, max_position_pct=30,
@ -67,7 +67,7 @@ def get_strategy_profile_by_id(strategy_id: str) -> StrategyProfile:
name="回踩轮动", name="回踩轮动",
description="市场震荡分化,优先做回踩支撑和板块轮动中的低吸确认。", description="市场震荡分化,优先做回踩支撑和板块轮动中的低吸确认。",
entry_signal_priority=["pullback", "breakout_confirm", "launch", "breakout", "reversal"], entry_signal_priority=["pullback", "breakout_confirm", "launch", "breakout", "reversal"],
score_weights={"supply_demand": 0.40, "price_action": 0.30, "trend": 0.30}, score_weights={"capital_momentum": 0.28, "supply_demand": 0.30, "price_action": 0.22, "trend": 0.20},
min_score=60, min_score=60,
buy_threshold=63, buy_threshold=63,
max_position_pct=20, max_position_pct=20,
@ -84,7 +84,7 @@ def get_strategy_profile_by_id(strategy_id: str) -> StrategyProfile:
name="启动试错", name="启动试错",
description="市场偏弱,适合少量观察启动型和反转型机会,不做强追涨。", description="市场偏弱,适合少量观察启动型和反转型机会,不做强追涨。",
entry_signal_priority=["launch", "reversal", "pullback", "breakout_confirm", "breakout"], entry_signal_priority=["launch", "reversal", "pullback", "breakout_confirm", "breakout"],
score_weights={"supply_demand": 0.35, "price_action": 0.35, "trend": 0.30}, score_weights={"capital_momentum": 0.32, "supply_demand": 0.28, "price_action": 0.25, "trend": 0.15},
min_score=58, min_score=58,
buy_threshold=61, buy_threshold=61,
max_position_pct=10, max_position_pct=10,
@ -101,7 +101,7 @@ def get_strategy_profile_by_id(strategy_id: str) -> StrategyProfile:
name="防守观察", name="防守观察",
description="市场退潮,系统以观察池为主,不主动扩大出手。", description="市场退潮,系统以观察池为主,不主动扩大出手。",
entry_signal_priority=["pullback", "launch", "reversal", "breakout_confirm", "breakout"], entry_signal_priority=["pullback", "launch", "reversal", "breakout_confirm", "breakout"],
score_weights={"supply_demand": 0.35, "price_action": 0.40, "trend": 0.25}, score_weights={"capital_momentum": 0.35, "supply_demand": 0.25, "price_action": 0.25, "trend": 0.15},
min_score=56, min_score=56,
buy_threshold=64, buy_threshold=64,
max_position_pct=5, max_position_pct=5,