1
This commit is contained in:
parent
c9e0631140
commit
8ae3ee19b7
@ -196,14 +196,14 @@ def detect_pullback(df: pd.DataFrame) -> dict | None:
|
||||
2. 近 5 日内有 MA5 > MA20 — 近期处于上升趋势
|
||||
3. 价格靠近 MA10 或 MA20 (<3%)
|
||||
4. 近 3 日缩量(相对前 5 日)或 当日开始放量反弹
|
||||
5. RSI 30-70(放宽)
|
||||
5. RSI 只做风险提示,不作为回踩成立的硬条件
|
||||
"""
|
||||
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):
|
||||
return None
|
||||
if pd.isna(last["ma60"]) or pd.isna(last["rsi14"]):
|
||||
if pd.isna(last["ma60"]):
|
||||
return None
|
||||
|
||||
# 中长期上升趋势
|
||||
@ -251,13 +251,10 @@ def detect_pullback(df: pd.DataFrame) -> dict | None:
|
||||
if not (shrinking or bouncing_today):
|
||||
return None
|
||||
|
||||
# RSI(放宽至 30-70)
|
||||
if not (30 <= last["rsi14"] <= 70):
|
||||
return None
|
||||
|
||||
support_ma = "MA10" if near_ma10 else "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
|
||||
rsi14 = last.get("rsi14") if "rsi14" in df.columns else None
|
||||
|
||||
return {
|
||||
"signal_type": EntrySignal.PULLBACK,
|
||||
@ -266,6 +263,7 @@ def detect_pullback(df: pd.DataFrame) -> dict | None:
|
||||
"support_price": round(support_price, 2),
|
||||
"volume_shrink_ratio": round(shrink_ratio, 2),
|
||||
"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 日以上下跌
|
||||
2. 今日放量长阳(pct_chg > 3%, vol > vol_ma5 * 1.5)
|
||||
3. 收盘价高于近 5 日最高价(强势反转)
|
||||
4. RSI 从低位回升(近 5 日最低 RSI < 40)
|
||||
4. RSI 低位只做背景信息,不作为反转成立的硬条件
|
||||
"""
|
||||
if len(df) < 10:
|
||||
return None
|
||||
|
||||
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):
|
||||
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
|
||||
|
||||
# 近 5 日有 3 日以上下跌
|
||||
@ -372,11 +370,10 @@ def detect_reversal(df: pd.DataFrame) -> dict | None:
|
||||
if last["close"] < high_5d:
|
||||
return None
|
||||
|
||||
# RSI 曾在低位
|
||||
rsi_5d = df["rsi14"].tail(5)
|
||||
min_rsi = rsi_5d.min()
|
||||
if not pd.isna(min_rsi) and min_rsi > 40:
|
||||
return None
|
||||
min_rsi = None
|
||||
if "rsi14" in df.columns:
|
||||
rsi_5d = df["rsi14"].tail(5)
|
||||
min_rsi = rsi_5d.min()
|
||||
|
||||
return {
|
||||
"signal_type": EntrySignal.REVERSAL,
|
||||
@ -384,7 +381,7 @@ def detect_reversal(df: pd.DataFrame) -> dict | None:
|
||||
"down_days": down_count,
|
||||
"reversal_pct": round(last["pct_chg"], 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 50 <= last["rsi14"] <= 75:
|
||||
score += 10
|
||||
score += 4
|
||||
elif 45 <= last["rsi14"] <= 80:
|
||||
score += 5
|
||||
score += 2
|
||||
|
||||
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 50 <= last["rsi14"] <= 70:
|
||||
score += 15
|
||||
score += 4
|
||||
elif 45 <= last["rsi14"] <= 75:
|
||||
score += 8
|
||||
score += 2
|
||||
|
||||
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 40 <= last["rsi14"] <= 55:
|
||||
score += 15
|
||||
score += 3
|
||||
elif 35 <= last["rsi14"] <= 65:
|
||||
score += 8
|
||||
score += 1
|
||||
|
||||
return min(score, 100)
|
||||
|
||||
@ -588,14 +585,6 @@ def _score_reversal(df: pd.DataFrame, signal: dict) -> float:
|
||||
elif vol_ratio > 1.5:
|
||||
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 加分
|
||||
if "ma5" in df.columns and not pd.isna(last["ma5"]):
|
||||
if last["close"] > last["ma5"]:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""买卖信号生成
|
||||
|
||||
基于技术指标判断 7 种买入信号,生成综合技术面评分。
|
||||
基于量价与趋势判断辅助信号。RSI 等滞后指标只做风险备注,不主导推荐。
|
||||
"""
|
||||
|
||||
import logging
|
||||
@ -53,7 +53,7 @@ def _check_macd_golden(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:
|
||||
return False
|
||||
rsi = df.iloc[-1].get("rsi14", 50)
|
||||
@ -258,7 +258,7 @@ def generate_signals(ts_code: str, name: str = "") -> TechnicalSignal:
|
||||
(ma_bullish, 15),
|
||||
(volume_breakout, 20),
|
||||
(macd_golden, 15),
|
||||
(rsi_healthy, 10),
|
||||
(rsi_healthy, 3),
|
||||
(pullback_support, 15),
|
||||
(big_yang, 15),
|
||||
(boll_support, 10),
|
||||
|
||||
@ -384,12 +384,12 @@ async def diagnose_stock(ts_code: str, mode: str = Query("entry")):
|
||||
)
|
||||
|
||||
signal_str = (
|
||||
f"推荐体系评分: 趋势评分={signals.trend_score}/100(均线排列+高低点结构+MA20方向,主评分10%权重), "
|
||||
f"辅助信号计数={signals.signal_count}/7(触发计分,仅供参考不参与主评分), "
|
||||
f"推荐体系评分: 趋势评分={signals.trend_score}/100(均线排列+高低点结构+MA20方向,主评分辅助项), "
|
||||
f"辅助信号计数={signals.signal_count}/7(触发计分,仅供节奏参考,不作为主评分裁判), "
|
||||
f"均线多头: {signals.ma_bullish}, "
|
||||
f"放量突破: {signals.volume_breakout}, "
|
||||
f"MACD金叉: {signals.macd_golden}, "
|
||||
f"RSI健康: {signals.rsi_healthy}, "
|
||||
f"RSI节奏区间: {signals.rsi_healthy}, "
|
||||
f"缩量回踩: {signals.pullback_support}, "
|
||||
f"放量长阳: {signals.big_yang}, "
|
||||
f"布林支撑: {signals.boll_support}, "
|
||||
@ -471,7 +471,7 @@ async def diagnose_stock(ts_code: str, mode: str = Query("entry")):
|
||||
rec_result = await db.execute(
|
||||
text(
|
||||
"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 "
|
||||
"WHERE ts_code = :code "
|
||||
"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
|
||||
rec_score_str = (
|
||||
f"\n推荐体系评分: 综合={rm['score']}, "
|
||||
f"供需={rm['supply_demand_score']}(50%权重), "
|
||||
f"形态={rm['price_action_score']}(40%权重), "
|
||||
f"趋势={rm['technical_score']}(10%权重), "
|
||||
f"资金={rm['capital_score']}(资金顺势核心), "
|
||||
f"供需={rm['supply_demand_score']}, "
|
||||
f"形态={rm['price_action_score']}, "
|
||||
f"趋势={rm['technical_score']}, "
|
||||
f"位置安全={rm['position_score']}, "
|
||||
f"板块={rm['sector']}, "
|
||||
f"信号={rm['signal']}"
|
||||
f"信号={rm['signal']}, "
|
||||
f"入场类型={rm['entry_signal_type']}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@ -556,11 +558,12 @@ async def diagnose_stock(ts_code: str, mode: str = Query("entry")):
|
||||
重要提示:
|
||||
1. 你不是在写传统研报,而是在给交易作战台输出结构化会诊意见。
|
||||
2. 如果有推荐体系评分、操作计划、跟踪信息,请优先沿用当前推荐体系,而不是另起一套标准。
|
||||
3. 趋势评分是推荐体系的技术面核心分数(均线排列40+高低点结构35+MA20方向25=满分100),辅助信号计数仅供参考不参与主评分。
|
||||
4. 位置安全评分高(>80)表示股价处于相对低位,低(<40)表示可能追高。
|
||||
5. 板块信息和推荐体系信息优先级高于单一技术指标。
|
||||
6. 先给结论和动作,再解释原因;不要先铺陈背景再拖到最后才下结论。
|
||||
7. 如果证据不足,也要明确给出“观察”或“回避”,不能写成模糊建议。
|
||||
3. 当前推荐体系以“资金顺势 + 主线板块 + 供需/量价 + 趋势”为主,技术指标只做节奏和风控确认。
|
||||
4. RSI、MACD、KDJ 等滞后指标不能单独决定买卖;RSI 超买只提示追高风险,超卖只提示弱势或反弹弹性,不等于可买或不可买。
|
||||
5. 位置安全评分高(>80)表示股价处于相对低位,低(<40)表示可能追高。
|
||||
6. 板块信息、资金面和推荐体系信息优先级高于单一技术指标。
|
||||
7. 先给结论和动作,再解释原因;不要先铺陈背景再拖到最后才下结论。
|
||||
8. 如果证据不足,也要明确给出“观察”或“回避”,不能写成模糊建议。
|
||||
{freshness_note}
|
||||
|
||||
请严格按以下 Markdown 结构输出,不要写成泛泛长文:
|
||||
|
||||
@ -119,7 +119,7 @@ class TechnicalSignal(BaseModel):
|
||||
ma_bullish: bool = False # 均线多头排列
|
||||
volume_breakout: bool = False # 放量突破
|
||||
macd_golden: bool = False # MACD金叉
|
||||
rsi_healthy: bool = False # RSI健康区间
|
||||
rsi_healthy: bool = False # RSI节奏区间(辅助参考)
|
||||
pullback_support: bool = False # 缩量回踩支撑
|
||||
big_yang: bool = False # 底部放量长阳
|
||||
boll_support: bool = False # 布林带下轨支撑
|
||||
@ -148,8 +148,8 @@ class Recommendation(BaseModel):
|
||||
sector_score: float
|
||||
capital_score: float
|
||||
technical_score: float
|
||||
supply_demand_score: float = 0 # 供需评分(主评分50%权重)
|
||||
price_action_score: float = 0 # 价格行为评分(主评分40%权重)
|
||||
supply_demand_score: float = 0 # 供需评分
|
||||
price_action_score: float = 0 # 价格行为评分
|
||||
position_score: float = 50 # 位置安全得分
|
||||
valuation_score: float = 50 # 估值安全得分
|
||||
signal: str # BUY / SELL / HOLD
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
三阶段管道:
|
||||
Step 1: 主线定位 — 把实时板块/快照板块归一成系统 MarketTheme
|
||||
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)
|
||||
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 {
|
||||
"supply_demand": 0.50,
|
||||
"price_action": 0.40,
|
||||
"trend": 0.10,
|
||||
"capital_momentum": 0.30,
|
||||
"supply_demand": 0.30,
|
||||
"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 []
|
||||
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)
|
||||
price_action_score = _score_price_action(df, entry_signal)
|
||||
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]
|
||||
trend_penalty = 1.0
|
||||
@ -660,6 +664,7 @@ async def _build_recommendations(
|
||||
trend_penalty = 0.82
|
||||
|
||||
final_score = (
|
||||
flow_momentum_score * score_weights["capital_momentum"] +
|
||||
supply_demand_score * score_weights["supply_demand"] +
|
||||
price_action_score * score_weights["price_action"] +
|
||||
trend_score * score_weights["trend"]
|
||||
@ -699,6 +704,8 @@ async def _build_recommendations(
|
||||
if entry_signal.get("signal_score", 0) >= 80:
|
||||
final_score *= 1.10
|
||||
|
||||
final_score *= _flow_confirmation_multiplier(stock, hot_theme_match, market_temp)
|
||||
|
||||
if not hot_theme_match:
|
||||
final_score *= 0.82
|
||||
elif hot_theme_match not in hot_sectors[:5]:
|
||||
@ -721,11 +728,25 @@ async def _build_recommendations(
|
||||
level = _score_to_level(final_score)
|
||||
signal = "HOLD"
|
||||
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 (
|
||||
signal_type != EntrySignal.NONE
|
||||
and entry_signal.get("signal_score", 0) >= 50
|
||||
and position_score >= 30
|
||||
and final_score >= buy_threshold
|
||||
) or (
|
||||
flow_confirmed
|
||||
and position_score >= 30
|
||||
and final_score >= buy_threshold + 2
|
||||
):
|
||||
signal = "BUY"
|
||||
|
||||
@ -796,13 +817,13 @@ async def _build_recommendations(
|
||||
if target_price < entry_price * (1 + min_target_pct):
|
||||
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)
|
||||
stock["entry_signal_type"] = signal_name
|
||||
risk_note = _generate_risk_note(market_temp, tech_signal, stock)
|
||||
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(
|
||||
signal_type=signal_name,
|
||||
signal_type=effective_signal_name,
|
||||
score=final_score,
|
||||
market_temp=market_temp,
|
||||
sector_stage=sector_stage,
|
||||
@ -820,7 +841,7 @@ async def _build_recommendations(
|
||||
score=round(final_score, 1),
|
||||
market_temp_score=round(market_temp_score, 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),
|
||||
supply_demand_score=round(supply_demand_score, 1),
|
||||
price_action_score=round(price_action_score, 1),
|
||||
@ -834,7 +855,7 @@ async def _build_recommendations(
|
||||
risk_note=risk_note,
|
||||
level=level,
|
||||
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,
|
||||
action_plan=trade_plan["action_plan"],
|
||||
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_aliases": hot_theme_match.theme_aliases if hot_theme_match else [],
|
||||
"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),
|
||||
"flow_momentum_score": round(flow_momentum_score, 1),
|
||||
"signal_matches_profile": signal_matches_profile,
|
||||
"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),
|
||||
@ -1042,6 +1064,145 @@ async def _build_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确认支撑有效可加仓",
|
||||
"launch": "早盘放量确认后即可进场,注意开盘9:30-10:00量能",
|
||||
"reversal": "午后13:30确认不回落再进场,避免早盘追高",
|
||||
"flow_momentum": "优先看资金是否延续流入和板块前排是否继续走强,分时承接确认后再分批",
|
||||
}
|
||||
return timing_map.get(signal_type, "盘中观察量价配合,确认信号后进场")
|
||||
|
||||
@ -1334,7 +1496,8 @@ def _build_trade_plan(
|
||||
"pullback": "回踩支撑",
|
||||
"launch": "缩量整理后启动",
|
||||
"reversal": "放量反转",
|
||||
}.get(signal_type, "技术信号")
|
||||
"flow_momentum": "资金顺势确认",
|
||||
}.get(signal_type, "资金与量价信号")
|
||||
|
||||
if market_temp.temperature < 40 or sector_stage in ("end",):
|
||||
action_plan = "观察"
|
||||
@ -1421,9 +1584,13 @@ def _generate_reasons(
|
||||
EntrySignal.PULLBACK: "回踩型", EntrySignal.LAUNCH: "启动型",
|
||||
EntrySignal.REVERSAL: "反转型"}
|
||||
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
|
||||
if st == "breakout":
|
||||
breakout_pct = details.get("breakout_pct", 0)
|
||||
@ -1492,6 +1659,8 @@ def _generate_risk_note(
|
||||
notes.append("启动型整理可能延长,注意时间成本")
|
||||
elif entry_type == "reversal":
|
||||
notes.append("反转型可能二次探底,确认底部后再加仓")
|
||||
elif entry_type == "flow_momentum":
|
||||
notes.append("资金顺势型需防板块分歧和资金一日游,重点看次日承接")
|
||||
|
||||
if market.temperature < 30:
|
||||
notes.append("市场情绪偏冷,系统性风险较高")
|
||||
@ -1541,6 +1710,8 @@ def _build_focus_points(
|
||||
signal_type = entry_signal.get("signal_type")
|
||||
if signal_type and getattr(signal_type, "value", "none") != "none":
|
||||
points.append(f"确认{signal_type.value}信号是否延续")
|
||||
elif stock.get("entry_signal_type") == "flow_momentum":
|
||||
points.append("确认主力流入和板块前排强度是否延续")
|
||||
if stock.get("main_net_inflow", 0) > 0:
|
||||
points.append("观察主力流入是否继续放大")
|
||||
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日缩量"
|
||||
parts.append(vol_conclusion)
|
||||
|
||||
# ── MACD 结论 ──
|
||||
# ── MACD 结论(节奏参考) ──
|
||||
dif = last.get("dif", 0) or 0
|
||||
dea = last.get("dea", 0) or 0
|
||||
macd_desc = ""
|
||||
@ -1643,19 +1814,19 @@ def _summarize_for_llm(df, entry_signal: dict, tech_signal: TechnicalSignal | No
|
||||
macd_desc += ",零轴上方(偏多)"
|
||||
else:
|
||||
macd_desc += ",零轴下方(偏空)"
|
||||
parts.append(macd_desc or "MACD数据不足")
|
||||
parts.append((macd_desc or "MACD数据不足") + ";仅作节奏参考")
|
||||
|
||||
# ── RSI 结论 ──
|
||||
# ── RSI 结论(风险提示,不做买卖裁判) ──
|
||||
rsi = last.get("rsi14", 50)
|
||||
if not pd.isna(rsi):
|
||||
if rsi > 80:
|
||||
parts.append(f"RSI14={rsi:.0f},超买区,回调风险大")
|
||||
parts.append(f"RSI14={rsi:.0f},偏热,提示追高风险但不单独否决资金顺势")
|
||||
elif rsi > 70:
|
||||
parts.append(f"RSI14={rsi:.0f},偏高,注意追高风险")
|
||||
elif rsi >= 40:
|
||||
parts.append(f"RSI14={rsi:.0f},健康区间")
|
||||
parts.append(f"RSI14={rsi:.0f},节奏中性")
|
||||
else:
|
||||
parts.append(f"RSI14={rsi:.0f},偏低,可能超卖")
|
||||
parts.append(f"RSI14={rsi:.0f},偏低,提示弱势或反弹弹性,不单独构成买点")
|
||||
|
||||
# ── 价格位置结论 ──
|
||||
if tech_signal:
|
||||
|
||||
@ -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('recall_tags', []) or ['未标注'])}
|
||||
规则参考分: {candidate.get('quant_score', 0)}/100
|
||||
资金顺势分: {candidate.get('flow_momentum_score', 0)}/100
|
||||
位置安全: {candidate.get('position_score', 50)}/100
|
||||
当前价: {candidate.get('current_price', '未知')}
|
||||
主题阶段: {candidate.get('sector_stage', '未知')}
|
||||
@ -109,6 +110,7 @@ async def analyze_single_stock(candidate: dict, market_summary: str) -> dict:
|
||||
主题: {candidate.get('sector', '未知')}
|
||||
主线主题匹配: {"是,匹配 " + candidate.get("hot_theme_name", "") if candidate.get("hot_theme_matched") else "否"}
|
||||
规则参考分: {candidate.get('quant_score', 0)}/100
|
||||
资金顺势分: {candidate.get('flow_momentum_score', 0)}/100
|
||||
位置安全: {candidate.get('position_score', 50)}/100
|
||||
当前价: {candidate.get('current_price', '未知')}"""
|
||||
|
||||
|
||||
@ -126,9 +126,11 @@ SINGLE_STOCK_ANALYSIS_PROMPT = """\
|
||||
|
||||
你的原则:
|
||||
1. 量化分数只是参考,不是最终答案
|
||||
2. 如果板块地位、量价质量、位置或时机不匹配,可以直接否决高分股
|
||||
3. 如果股票具备明确触发与失效边界,即使量化分不是最高,也可以提升优先级
|
||||
4. 输出的是交易裁决单,不是研报
|
||||
2. A股先看资金是否在主线里形成合力,再看K线形态是否给出可执行边界
|
||||
3. RSI、MACD、KDJ 这类滞后指标只能作为节奏和风险备注,不能因为超买/超卖本身否决资金顺势标的
|
||||
4. 如果板块地位、资金延续、量价质量、位置或时机不匹配,可以直接否决高分股
|
||||
5. 如果股票具备明确触发与失效边界,即使量化分不是最高,也可以提升优先级
|
||||
6. 输出的是交易裁决单,不是研报
|
||||
|
||||
请严格输出 JSON,不要输出 Markdown,不要添加多余解释。字段如下:
|
||||
{
|
||||
@ -163,9 +165,10 @@ STOCK_PREFILTER_PROMPT = """\
|
||||
|
||||
你的原则:
|
||||
1. 这一步不是最终买卖结论,只做资源分配
|
||||
2. 不能因为某一个规则分数低就直接忽略,要看题材位置、角色、量价异常、时机感
|
||||
3. 可以容忍不标准的形态,但不能容忍明显失真、明显追高、明显没有交易边界的票
|
||||
4. 输出必须是 JSON,不要输出 Markdown
|
||||
2. 优先识别资金正在进攻的主线前排或强承接标的
|
||||
3. 不能因为 RSI/MACD/KDJ 等滞后指标“不好看”就直接忽略,要看题材位置、角色、量价异常、时机感
|
||||
4. 可以容忍不标准的形态,但不能容忍明显失真、明显追高、明显没有交易边界的票
|
||||
5. 输出必须是 JSON,不要输出 Markdown
|
||||
|
||||
字段格式:
|
||||
{
|
||||
|
||||
@ -50,7 +50,7 @@ def get_strategy_profile_by_id(strategy_id: str) -> StrategyProfile:
|
||||
name="主线突破",
|
||||
description="市场偏强,优先寻找主线板块内的突破和突破确认。",
|
||||
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,
|
||||
buy_threshold=66,
|
||||
max_position_pct=30,
|
||||
@ -67,7 +67,7 @@ def get_strategy_profile_by_id(strategy_id: str) -> StrategyProfile:
|
||||
name="回踩轮动",
|
||||
description="市场震荡分化,优先做回踩支撑和板块轮动中的低吸确认。",
|
||||
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,
|
||||
buy_threshold=63,
|
||||
max_position_pct=20,
|
||||
@ -84,7 +84,7 @@ def get_strategy_profile_by_id(strategy_id: str) -> StrategyProfile:
|
||||
name="启动试错",
|
||||
description="市场偏弱,适合少量观察启动型和反转型机会,不做强追涨。",
|
||||
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,
|
||||
buy_threshold=61,
|
||||
max_position_pct=10,
|
||||
@ -101,7 +101,7 @@ def get_strategy_profile_by_id(strategy_id: str) -> StrategyProfile:
|
||||
name="防守观察",
|
||||
description="市场退潮,系统以观察池为主,不主动扩大出手。",
|
||||
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,
|
||||
buy_threshold=64,
|
||||
max_position_pct=5,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user