1
This commit is contained in:
parent
0902091f08
commit
1db602088d
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,6 @@ from app.data.tushare_client import tushare_client
|
||||
from app.analysis.technical import add_all_indicators
|
||||
from app.analysis.breakout_signals import (
|
||||
classify_entry_signal,
|
||||
score_trend_timing,
|
||||
score_supply_demand,
|
||||
analyze_volume_pattern,
|
||||
EntrySignal,
|
||||
@ -330,7 +329,7 @@ async def _deep_analysis(
|
||||
|
||||
results = []
|
||||
total = len(candidates)
|
||||
signal_counts = {"breakout": 0, "pullback": 0, "launch": 0, "none": 0}
|
||||
signal_counts = {"breakout": 0, "breakout_confirm": 0, "pullback": 0, "launch": 0, "reversal": 0, "none": 0}
|
||||
|
||||
for idx, row in candidates.iterrows():
|
||||
ts_code = row["ts_code"] if "ts_code" in candidates.columns else ""
|
||||
@ -358,8 +357,8 @@ async def _deep_analysis(
|
||||
continue
|
||||
signal_counts[signal_type.value] += 1
|
||||
|
||||
# 趋势&时机评分
|
||||
trend_score = score_trend_timing(df, entry_signal)
|
||||
# 趋势评分(内联简化版)
|
||||
trend_score = _simple_trend_score(df)
|
||||
|
||||
# 供需评分
|
||||
sd_score = score_supply_demand(df)
|
||||
@ -419,8 +418,9 @@ async def _deep_analysis(
|
||||
|
||||
logger.info(
|
||||
f"Phase 3 入场信号分布: "
|
||||
f"突破={signal_counts['breakout']} 回踩={signal_counts['pullback']} "
|
||||
f"启动={signal_counts['launch']} 无信号={signal_counts['none']} "
|
||||
f"突破={signal_counts['breakout']} 确认={signal_counts['breakout_confirm']} "
|
||||
f"回踩={signal_counts['pullback']} 启动={signal_counts['launch']} "
|
||||
f"反转={signal_counts['reversal']} 无信号={signal_counts['none']} "
|
||||
f"(共分析{total}只)"
|
||||
)
|
||||
|
||||
@ -448,6 +448,31 @@ def _get_multi_day_moneyflow(candidates: pd.DataFrame, trade_date: str) -> dict[
|
||||
return result
|
||||
|
||||
|
||||
def _simple_trend_score(df: pd.DataFrame) -> float:
|
||||
"""简化趋势评分 (0-100),用于回退扫描路径"""
|
||||
import numpy as np
|
||||
score = 0
|
||||
last = df.iloc[-1]
|
||||
|
||||
ma_cols = [c for c in ["ma5", "ma10", "ma20", "ma60"] if c in df.columns]
|
||||
if len(ma_cols) >= 4 and not any(pd.isna(last[c]) for c in ma_cols):
|
||||
if last["ma5"] > last["ma10"] > last["ma20"] > last["ma60"]:
|
||||
score += 50
|
||||
elif last["ma5"] > last["ma10"] > last["ma20"]:
|
||||
score += 35
|
||||
elif last["ma5"] > last["ma20"]:
|
||||
score += 20
|
||||
|
||||
if len(df) >= 20:
|
||||
recent = df.tail(20)
|
||||
if recent["high"].iloc[10:].max() > recent["high"].iloc[:10].max():
|
||||
score += 30
|
||||
if recent["low"].iloc[10:].min() > recent["low"].iloc[:10].min():
|
||||
score += 20
|
||||
|
||||
return min(score, 100)
|
||||
|
||||
|
||||
def _score_capital(stock_row: dict | pd.Series, mf_history: pd.DataFrame) -> float:
|
||||
"""资金流评分 (0-100)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -79,9 +79,9 @@ def setup_scheduler():
|
||||
args=["late_session"], id=f"late_{m}", replace_existing=True
|
||||
)
|
||||
|
||||
# 收盘总结 15:05
|
||||
# 收盘总结 16:00(Tushare 日线数据通常在 15:30 后更新完成)
|
||||
scheduler.add_job(
|
||||
_run_scan, CronTrigger(hour=15, minute=5, day_of_week="mon-fri"),
|
||||
_run_scan, CronTrigger(hour=16, minute=0, day_of_week="mon-fri"),
|
||||
args=["post_market"], id="post_market", replace_existing=True
|
||||
)
|
||||
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
|
||||
import logging
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from app.analysis.market_temp import calculate_market_temperature
|
||||
from app.analysis.sector_scanner import scan_hot_sectors
|
||||
from app.analysis.trend_scanner import scan_trend_breakout
|
||||
@ -107,7 +109,7 @@ async def run_screening(trade_date: str = None) -> dict:
|
||||
|
||||
logger.info(f"=== 筛选完成: {len(recommendations)} 只股票 ({scan_mode}) ===")
|
||||
for r in recommendations[:5]:
|
||||
signal_map = {"breakout": "突破型", "pullback": "回踩型", "launch": "启动型"}
|
||||
signal_map = {"breakout": "突破型", "breakout_confirm": "确认型", "pullback": "回踩型", "launch": "启动型", "reversal": "反转型"}
|
||||
signal_label = signal_map.get(r.entry_signal_type, r.entry_signal_type)
|
||||
logger.info(f" [{signal_label}] {r.name}({r.ts_code}) {r.level} 评分={r.score} 信号={r.signal}")
|
||||
|
||||
@ -309,7 +311,7 @@ def _build_recommendations(
|
||||
|
||||
recommendations = []
|
||||
total = len(candidates)
|
||||
signal_counts = {"breakout": 0, "pullback": 0, "launch": 0, "none": 0}
|
||||
signal_counts = {"breakout": 0, "breakout_confirm": 0, "pullback": 0, "launch": 0, "reversal": 0, "none": 0}
|
||||
|
||||
for idx, stock in enumerate(candidates):
|
||||
ts_code = stock.get("ts_code", "")
|
||||
@ -406,7 +408,7 @@ def _build_recommendations(
|
||||
|
||||
details = entry_signal.get("details", {})
|
||||
st = signal_type.value
|
||||
if st == "breakout" and details.get("resist_level"):
|
||||
if st in ("breakout", "breakout_confirm") and details.get("resist_level"):
|
||||
entry_price = details["resist_level"]
|
||||
target_price = round(entry_price * 1.05, 2)
|
||||
elif st == "pullback" and details.get("support_price"):
|
||||
@ -415,6 +417,9 @@ def _build_recommendations(
|
||||
elif st == "launch" and details.get("resist_level"):
|
||||
entry_price = round(details["resist_level"] * 1.01, 2)
|
||||
target_price = round(details["resist_level"] * 1.08, 2)
|
||||
elif st == "reversal" and tech_signal and tech_signal.support_price:
|
||||
entry_price = tech_signal.support_price
|
||||
target_price = round(entry_price * 1.08, 2)
|
||||
|
||||
# 生成推荐理由
|
||||
reasons = _generate_reasons(stock, entry_signal, tech_signal, df, intraday)
|
||||
@ -458,8 +463,9 @@ def _build_recommendations(
|
||||
|
||||
logger.info(
|
||||
f"Step 3 入场信号分布: "
|
||||
f"突破={signal_counts['breakout']} 回踩={signal_counts['pullback']} "
|
||||
f"启动={signal_counts['launch']} 无信号={signal_counts['none']} "
|
||||
f"突破={signal_counts['breakout']} 确认={signal_counts['breakout_confirm']} "
|
||||
f"回踩={signal_counts['pullback']} 启动={signal_counts['launch']} "
|
||||
f"反转={signal_counts['reversal']} 无信号={signal_counts['none']} "
|
||||
f"(共分析{total}只)"
|
||||
)
|
||||
|
||||
@ -472,21 +478,20 @@ def _build_recommendations(
|
||||
def _score_price_action(df, entry_signal: dict) -> float:
|
||||
"""价格行为学评分 (0-100)
|
||||
|
||||
纯粹关注 K 线形态和量价配合,不重复评估趋势/均线因素。
|
||||
|
||||
维度:
|
||||
- 入场信号类型质量 (40): 突破型/回踩型/启动型各自的得分
|
||||
- K线形态强度 (30): 突破日/回踩日的K线实体占比、下影线、收盘位置
|
||||
- 支撑阻力位质量 (30): 关键价格位置的测试情况
|
||||
- K线形态强度 (35): 实体占比、收盘位置、下影线
|
||||
- 量价配合 (35): 放量/缩量与价格方向的配合度
|
||||
- 入场形态质量 (30): 各信号类型的形态完成度
|
||||
"""
|
||||
import pandas as pd
|
||||
score = 0
|
||||
last = df.iloc[-1]
|
||||
details = entry_signal.get("details", {})
|
||||
signal_type = entry_signal.get("signal_type")
|
||||
|
||||
# 入场信号类型质量 (40)
|
||||
signal_score = entry_signal.get("signal_score", 0)
|
||||
score += signal_score * 0.40
|
||||
|
||||
# K线形态强度 (30)
|
||||
# K线形态强度 (35)
|
||||
day_range = last["high"] - last["low"]
|
||||
if day_range > 0:
|
||||
# 实体占比(实体/全振幅)
|
||||
@ -508,24 +513,64 @@ def _score_price_action(df, entry_signal: dict) -> float:
|
||||
elif close_position > 0.4:
|
||||
score += 3
|
||||
|
||||
# 支撑阻力位质量 (30)
|
||||
# 下影线(回踩型/启动型利好)
|
||||
lower_wick = (last["open"] - last["low"]) if last["close"] > last["open"] else (last["close"] - last["low"])
|
||||
if lower_wick > 0:
|
||||
wick_ratio = lower_wick / day_range
|
||||
if signal_type and signal_type.value in ("pullback", "reversal") and wick_ratio > 0.2:
|
||||
score += 5 # 回踩型/反转型有下影线支撑
|
||||
|
||||
# 量价配合 (35)
|
||||
vol_ma_col = "vol_ma5" if "vol_ma5" in df.columns else None
|
||||
if vol_ma_col and not pd.isna(last[vol_ma_col]) and last[vol_ma_col] > 0:
|
||||
vol_ratio = last["vol"] / last[vol_ma_col]
|
||||
price_up = last["pct_chg"] > 0 if "pct_chg" in df.columns else last["close"] > last["open"]
|
||||
|
||||
if price_up and vol_ratio > 2.0:
|
||||
score += 35 # 放量大阳
|
||||
elif price_up and vol_ratio > 1.5:
|
||||
score += 25
|
||||
elif price_up and vol_ratio > 1.2:
|
||||
score += 18
|
||||
elif not price_up and vol_ratio < 0.7:
|
||||
score += 25 # 缩量回调(良性)
|
||||
elif not price_up and vol_ratio < 0.9:
|
||||
score += 15
|
||||
elif price_up and vol_ratio > 1.0:
|
||||
score += 10
|
||||
else:
|
||||
score += 10
|
||||
|
||||
# 入场形态质量 (30) — 只评估形态完成度,不涉及均线/MACD
|
||||
if signal_type and signal_type.value == "breakout":
|
||||
# 突破型:阻力位被突破的力度
|
||||
breakout_pct = details.get("breakout_pct", 0)
|
||||
vol_ratio = details.get("volume_ratio", 1)
|
||||
if breakout_pct > 2 and vol_ratio > 2:
|
||||
score += 30 # 强力突破
|
||||
score += 30
|
||||
elif breakout_pct > 1 and vol_ratio > 1.5:
|
||||
score += 20
|
||||
elif breakout_pct > 0:
|
||||
score += 10
|
||||
score += 12
|
||||
else:
|
||||
score += 6
|
||||
|
||||
elif signal_type and signal_type.value == "breakout_confirm":
|
||||
vol_ratio = details.get("volume_ratio", 1)
|
||||
confirm_pct = details.get("confirm_pct", 0)
|
||||
if vol_ratio > 2 and confirm_pct > 2:
|
||||
score += 30
|
||||
elif vol_ratio > 1.5 and confirm_pct > 1:
|
||||
score += 22
|
||||
elif vol_ratio > 1.0:
|
||||
score += 14
|
||||
else:
|
||||
score += 8
|
||||
|
||||
elif signal_type and signal_type.value == "pullback":
|
||||
# 回踩型:支撑位的精确度
|
||||
support_ma = details.get("support_ma", "")
|
||||
shrink = details.get("volume_shrink_ratio", 1)
|
||||
if support_ma == "MA20" and shrink < 0.6:
|
||||
score += 30 # 精确回踩 MA20 且大幅缩量
|
||||
score += 30
|
||||
elif support_ma == "MA20":
|
||||
score += 22
|
||||
elif support_ma == "MA10" and shrink < 0.6:
|
||||
@ -534,15 +579,26 @@ def _score_price_action(df, entry_signal: dict) -> float:
|
||||
score += 10
|
||||
|
||||
elif signal_type and signal_type.value == "launch":
|
||||
# 启动型:整理的充分度
|
||||
range_pct = details.get("price_range_pct", 10)
|
||||
shrink = details.get("volume_shrink_ratio", 1)
|
||||
if range_pct < 3 and shrink < 0.4:
|
||||
score += 30 # 极度缩量窄幅整理
|
||||
elif range_pct < 5 and shrink < 0.6:
|
||||
if range_pct < 3:
|
||||
score += 30
|
||||
elif range_pct < 5:
|
||||
score += 20
|
||||
else:
|
||||
score += 10
|
||||
|
||||
elif signal_type and signal_type.value == "reversal":
|
||||
reversal_pct = details.get("reversal_pct", 0)
|
||||
vol_ratio = details.get("volume_ratio", 1)
|
||||
if reversal_pct > 5 and vol_ratio > 2.5:
|
||||
score += 30
|
||||
elif reversal_pct > 3 and vol_ratio > 2:
|
||||
score += 22
|
||||
elif reversal_pct > 3:
|
||||
score += 14
|
||||
else:
|
||||
score += 8
|
||||
|
||||
else:
|
||||
score += 10
|
||||
|
||||
@ -673,10 +729,13 @@ def _generate_reasons(
|
||||
) -> list[str]:
|
||||
"""生成推荐理由"""
|
||||
import pandas as pd
|
||||
from app.analysis.breakout_signals import EntrySignal
|
||||
reasons = []
|
||||
signal_type = entry_signal.get("signal_type")
|
||||
details = entry_signal.get("details", {})
|
||||
signal_map = {EntrySignal.BREAKOUT: "突破型", EntrySignal.PULLBACK: "回踩型", EntrySignal.LAUNCH: "启动型"}
|
||||
signal_map = {EntrySignal.BREAKOUT: "突破型", EntrySignal.BREAKOUT_CONFIRM: "确认型",
|
||||
EntrySignal.PULLBACK: "回踩型", EntrySignal.LAUNCH: "启动型",
|
||||
EntrySignal.REVERSAL: "反转型"}
|
||||
entry_label = signal_map.get(signal_type, "")
|
||||
|
||||
# 入场信号
|
||||
@ -686,14 +745,21 @@ def _generate_reasons(
|
||||
breakout_pct = details.get("breakout_pct", 0)
|
||||
vol_ratio = details.get("volume_ratio", 0)
|
||||
reasons.append(f"放量突破20日阻力位(涨幅{breakout_pct:.1f}%,量比{vol_ratio:.1f}倍)")
|
||||
elif st == "breakout_confirm":
|
||||
vol_ratio = details.get("volume_ratio", 0)
|
||||
confirm_pct = details.get("confirm_pct", 0)
|
||||
reasons.append(f"突破后放量确认(确认日涨{confirm_pct:.1f}%,量比{vol_ratio:.1f}倍)")
|
||||
elif st == "pullback":
|
||||
support = details.get("support_ma", "")
|
||||
shrink = details.get("volume_shrink_ratio", 0)
|
||||
reasons.append(f"缩量回踩{support}支撑(量能收缩至{shrink:.0%})")
|
||||
elif st == "launch":
|
||||
range_pct = details.get("price_range_pct", 0)
|
||||
shrink = details.get("volume_shrink_ratio", 0)
|
||||
reasons.append(f"高位缩量整理{range_pct:.1f}%后即将变盘(量缩至{shrink:.0%})")
|
||||
reasons.append(f"缩量横盘整理{range_pct:.1f}%后首日放量启动")
|
||||
elif st == "reversal":
|
||||
reversal_pct = details.get("reversal_pct", 0)
|
||||
vol_ratio = details.get("volume_ratio", 0)
|
||||
reasons.append(f"连续下跌后放量长阳反转(涨{reversal_pct:.1f}%,量比{vol_ratio:.1f}倍)")
|
||||
|
||||
# 供需分析
|
||||
if len(df) >= 10:
|
||||
@ -734,10 +800,14 @@ def _generate_risk_note(
|
||||
|
||||
if entry_type == "breakout":
|
||||
notes.append("突破型需警惕假突破,关注量能是否持续")
|
||||
elif entry_type == "breakout_confirm":
|
||||
notes.append("确认型需观察后续量能是否跟上,防止冲高回落")
|
||||
elif entry_type == "pullback":
|
||||
notes.append("回踩型可能继续下探支撑,注意止损纪律")
|
||||
elif entry_type == "launch":
|
||||
notes.append("启动型整理可能延长,注意时间成本")
|
||||
elif entry_type == "reversal":
|
||||
notes.append("反转型可能二次探底,确认底部后再加仓")
|
||||
|
||||
if market.temperature < 30:
|
||||
notes.append("市场情绪偏冷,系统性风险较高")
|
||||
|
||||
Binary file not shown.
@ -11,6 +11,11 @@
|
||||
"static/css/app/layout.css",
|
||||
"static/chunks/app/layout.js"
|
||||
],
|
||||
"/stock/[code]/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/stock/[code]/page.js"
|
||||
],
|
||||
"/recommendations/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
@ -20,11 +25,6 @@
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/sectors/page.js"
|
||||
],
|
||||
"/_not-found/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/_not-found/page.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1 +1,20 @@
|
||||
{}
|
||||
{
|
||||
"components/capital-flow.tsx -> echarts": {
|
||||
"id": "components/capital-flow.tsx -> echarts",
|
||||
"files": [
|
||||
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
|
||||
]
|
||||
},
|
||||
"components/kline-chart.tsx -> echarts": {
|
||||
"id": "components/kline-chart.tsx -> echarts",
|
||||
"files": [
|
||||
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
|
||||
]
|
||||
},
|
||||
"components/score-radar.tsx -> echarts": {
|
||||
"id": "components/score-radar.tsx -> echarts",
|
||||
"files": [
|
||||
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"/_not-found/page": "app/_not-found/page.js",
|
||||
"/page": "app/page.js",
|
||||
"/recommendations/page": "app/recommendations/page.js",
|
||||
"/stock/[code]/page": "app/stock/[code]/page.js",
|
||||
"/sectors/page": "app/sectors/page.js"
|
||||
}
|
||||
@ -1 +1 @@
|
||||
self.__REACT_LOADABLE_MANIFEST="{}"
|
||||
self.__REACT_LOADABLE_MANIFEST="{\"components/capital-flow.tsx -> echarts\":{\"id\":\"components/capital-flow.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]},\"components/kline-chart.tsx -> echarts\":{\"id\":\"components/kline-chart.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]},\"components/score-radar.tsx -> echarts\":{\"id\":\"components/score-radar.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]}}"
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"node": {},
|
||||
"edge": {},
|
||||
"encryptionKey": "t8NqvDMYNuIMnc0JzSsolcrzKZh3QqUqStILpaN7mCE="
|
||||
"encryptionKey": "xV9IIi0vV+SB9UvVKH8lRLbKebnLKLutavDhD7b36pc="
|
||||
}
|
||||
@ -125,7 +125,7 @@
|
||||
/******/
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("37ba5b2074c7bffb")
|
||||
/******/ __webpack_require__.h = () => ("c0150b0528f1b8db")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user