重置项目
This commit is contained in:
@@ -343,216 +343,18 @@ def _pct_band_rows(values: list[float]) -> list[dict]:
|
||||
|
||||
|
||||
def _build_overview(request: Request, as_of: date | None = None) -> dict:
|
||||
repo = request.app.state.repo
|
||||
svc = ScreenerService(repo)
|
||||
as_of = as_of or svc.latest_date()
|
||||
status = _quote_status(request)
|
||||
indices = _index_quotes(request, as_of)
|
||||
"""装配市场总览(委托给 services.market_overview_builder,保持行为一致)。
|
||||
|
||||
if not as_of:
|
||||
return {
|
||||
"as_of": None,
|
||||
"quote_status": status,
|
||||
"indices": indices,
|
||||
"breadth": {"total": 0, "up": 0, "down": 0, "flat": 0, "up_pct": 0, "down_pct": 0},
|
||||
"amount": {"total": 0, "avg": 0},
|
||||
"boards": [],
|
||||
"limit": {"limit_up": 0, "broken": 0, "failed": 0, "limit_down": 0, "max_boards": 0, "tiers": []},
|
||||
"distribution": [],
|
||||
"trend": {"above_ma5": 0, "above_ma20": 0, "above_ma60": 0, "above_ma5_pct": 0, "above_ma20_pct": 0, "above_ma60_pct": 0, "new_high": 0, "new_low": 0},
|
||||
"activity": {"avg_turnover": 0, "high_turnover": 0, "high_vol_ratio": 0, "vol_ratio": 1},
|
||||
"radar": [],
|
||||
"emotion": {"score": 50, "label": "暂无"},
|
||||
"top_gainers": [],
|
||||
"top_losers": [],
|
||||
"turnover_leaders": [],
|
||||
"active_leaders": [],
|
||||
"concept_rank": {"leading": [], "lagging": []},
|
||||
"industry_rank": {"leading": [], "lagging": []},
|
||||
}
|
||||
|
||||
df = svc._load_enriched_for_date(as_of)
|
||||
if df.is_empty():
|
||||
rows: list[dict] = []
|
||||
else:
|
||||
cols = [
|
||||
"symbol", "name", "close", "change_pct", "amount", "turnover_rate", "volume",
|
||||
"vol_ratio_5d", "consecutive_limit_ups", "signal_limit_up", "signal_broken_limit_up", "signal_limit_down",
|
||||
"ma5", "ma20", "ma60", "high_60d", "low_60d", "signal_n_day_high", "signal_n_day_low",
|
||||
]
|
||||
df = df.select([c for c in cols if c in df.columns])
|
||||
rows = df.to_dicts()
|
||||
|
||||
# 过滤真停牌(volume=0 且 change_pct=0),保留有涨跌幅的浮点误差股以对齐同花顺口径
|
||||
if rows and "volume" in rows[0]:
|
||||
rows = [r for r in rows
|
||||
if (_finite(r.get("volume")) or 0) > 0
|
||||
or (_finite(r.get("change_pct")) or 0) != 0]
|
||||
|
||||
total = len(rows)
|
||||
up = sum(1 for r in rows if (_finite(r.get("change_pct")) or 0) > 0)
|
||||
down = sum(1 for r in rows if (_finite(r.get("change_pct")) or 0) < 0)
|
||||
flat = max(0, total - up - down)
|
||||
up_pct = up / total * 100 if total else 0
|
||||
down_pct = down / total * 100 if total else 0
|
||||
|
||||
amounts = [_finite(r.get("amount")) or 0 for r in rows]
|
||||
total_amount = sum(amounts)
|
||||
avg_amount = total_amount / total if total else 0
|
||||
|
||||
pct_values = [_finite(r.get("change_pct")) for r in rows]
|
||||
pct_values = [v for v in pct_values if v is not None]
|
||||
avg_pct = sum(pct_values) / len(pct_values) if pct_values else 0
|
||||
median_pct = sorted(pct_values)[len(pct_values) // 2] if pct_values else 0
|
||||
strong_up = sum(1 for v in pct_values if v >= 0.03)
|
||||
strong_down = sum(1 for v in pct_values if v <= -0.03)
|
||||
|
||||
limit_up = sum(1 for r in rows if bool(r.get("signal_limit_up")) or (_finite(r.get("consecutive_limit_ups")) or 0) > 0)
|
||||
broken = sum(1 for r in rows if bool(r.get("signal_broken_limit_up")))
|
||||
limit_down = sum(1 for r in rows if bool(r.get("signal_limit_down")))
|
||||
max_boards = max([int(_finite(r.get("consecutive_limit_ups")) or 0) for r in rows], default=0)
|
||||
|
||||
# 五档 sealed 修正: 假涨停/假跌停不计入(需 Pro+ depth5.batch 能力)
|
||||
depth_svc = getattr(request.app.state, "depth_service", None)
|
||||
sealed_ready = False
|
||||
fake_up = 0
|
||||
fake_down = 0
|
||||
if depth_svc:
|
||||
up_map = depth_svc.get_sealed_map(as_of, is_down=False)
|
||||
down_map = depth_svc.get_sealed_map(as_of, is_down=True)
|
||||
sealed_ready = bool(up_map or down_map) and depth_svc.is_sealed_ready(as_of)
|
||||
if up_map:
|
||||
fake_up = sum(1 for v in up_map.values() if v.get("sealed") is False)
|
||||
if down_map:
|
||||
fake_down = sum(1 for v in down_map.values() if v.get("sealed") is False)
|
||||
if sealed_ready:
|
||||
limit_up = max(0, limit_up - fake_up)
|
||||
limit_down = max(0, limit_down - fake_down)
|
||||
|
||||
seal_rate = limit_up / (limit_up + broken) * 100 if (limit_up + broken) > 0 else 0
|
||||
|
||||
def above_ma_count(ma_key: str) -> int:
|
||||
return sum(1 for r in rows if (_finite(r.get("close")) is not None and _finite(r.get(ma_key)) is not None and (_finite(r.get("close")) or 0) >= (_finite(r.get(ma_key)) or 0)))
|
||||
|
||||
above_ma5 = above_ma_count("ma5")
|
||||
above_ma20 = above_ma_count("ma20")
|
||||
above_ma60 = above_ma_count("ma60")
|
||||
new_high = sum(1 for r in rows if bool(r.get("signal_n_day_high")) or (_finite(r.get("close")) is not None and _finite(r.get("high_60d")) is not None and (_finite(r.get("close")) or 0) >= (_finite(r.get("high_60d")) or 0)))
|
||||
new_low = sum(1 for r in rows if bool(r.get("signal_n_day_low")) or (_finite(r.get("close")) is not None and _finite(r.get("low_60d")) is not None and (_finite(r.get("close")) or 0) <= (_finite(r.get("low_60d")) or 0)))
|
||||
|
||||
turnovers = [_finite(r.get("turnover_rate")) for r in rows]
|
||||
turnovers = [v for v in turnovers if v is not None]
|
||||
avg_turnover = sum(turnovers) / len(turnovers) if turnovers else 0
|
||||
high_turnover = sum(1 for v in turnovers if v >= 5)
|
||||
|
||||
boards_map: dict[str, dict] = {}
|
||||
for r in rows:
|
||||
b = _board(str(r.get("symbol") or ""))
|
||||
item = boards_map.setdefault(b, {"board": b, "count": 0, "up": 0, "down": 0, "amount": 0.0})
|
||||
item["count"] += 1
|
||||
change = _finite(r.get("change_pct")) or 0
|
||||
if change > 0:
|
||||
item["up"] += 1
|
||||
elif change < 0:
|
||||
item["down"] += 1
|
||||
item["amount"] += _finite(r.get("amount")) or 0
|
||||
boards = sorted(boards_map.values(), key=lambda x: x["amount"], reverse=True)
|
||||
for b in boards:
|
||||
count = b["count"] or 1
|
||||
b["up_pct"] = b["up"] / count * 100
|
||||
|
||||
tiers_map: dict[int, int] = {}
|
||||
for r in rows:
|
||||
n = int(_finite(r.get("consecutive_limit_ups")) or 0)
|
||||
if n > 0:
|
||||
tiers_map[n] = tiers_map.get(n, 0) + 1
|
||||
tiers = [{"boards": k, "count": v} for k, v in sorted(tiers_map.items(), key=lambda item: -item[0])]
|
||||
|
||||
index_changes = [_finite(r.get("change_pct")) for r in indices]
|
||||
index_changes = [v for v in index_changes if v is not None]
|
||||
avg_index_pct = sum(index_changes) / len(index_changes) if index_changes else 0
|
||||
vol_ratios = [_finite(r.get("vol_ratio_5d")) for r in rows]
|
||||
vol_ratios = [v for v in vol_ratios if v is not None]
|
||||
avg_vol_ratio = sum(vol_ratios) / len(vol_ratios) if vol_ratios else 1
|
||||
high_vol_ratio = sum(1 for v in vol_ratios if v >= 1.5)
|
||||
|
||||
concept_rank = _dimension_rank(rows, request, "concept")
|
||||
industry_rank = _dimension_rank(rows, request, "industry", level=2)
|
||||
|
||||
strong_diff_pct = (strong_up - strong_down) / total * 100 if total else 0
|
||||
high_vol_pct = high_vol_ratio / total * 100 if total else 0
|
||||
strong_down_pct = strong_down / total * 100 if total else 0
|
||||
tier2_count = sum(t["count"] for t in tiers if t["boards"] >= 2)
|
||||
mainline_items = [*concept_rank["leading"][:3], *industry_rank["leading"][:3]]
|
||||
mainline_avg = max([_finite(item.get("avg_pct")) or 0 for item in mainline_items], default=0)
|
||||
mainline_cover_pct = max([(_finite(item.get("count")) or 0) / total * 100 for item in mainline_items], default=0) if total else 0
|
||||
mainline_score = round(_score(mainline_avg, -0.005, 0.03) * 0.65 + _score(mainline_cover_pct, 1, 12) * 0.35) if mainline_items else 50
|
||||
|
||||
radar = [
|
||||
{"key": "index", "label": "指数", "value": _score(avg_index_pct, -2.5, 2.5)},
|
||||
{"key": "profit", "label": "赚钱", "value": round(_score(up_pct, 20, 80) * 0.45 + _score(avg_pct, -0.02, 0.02) * 0.25 + _score(median_pct, -0.02, 0.02) * 0.20 + _score(strong_diff_pct, -8, 8) * 0.10)},
|
||||
{"key": "money", "label": "量能", "value": round(_score(avg_vol_ratio, 0.6, 1.8) * 0.70 + _score(high_vol_pct, 2, 12) * 0.30)},
|
||||
{"key": "speculation", "label": "投机", "value": round(_score(limit_up, 5, 90) * 0.25 + _score(seal_rate, 30, 85) * 0.35 + _score(max_boards, 1, 8) * 0.25 + _score(tier2_count, 0, 30) * 0.15)},
|
||||
{"key": "resilience", "label": "抗跌", "value": 100 - round(_score(down_pct, 20, 80) * 0.55 + _score(strong_down_pct, 1, 12) * 0.45)},
|
||||
{"key": "mainline", "label": "主线", "value": mainline_score},
|
||||
]
|
||||
emotion_score = round(sum(r["value"] for r in radar) / len(radar)) if radar else 50
|
||||
if emotion_score >= 70:
|
||||
emotion_label = "强势"
|
||||
elif emotion_score >= 55:
|
||||
emotion_label = "偏暖"
|
||||
elif emotion_score >= 45:
|
||||
emotion_label = "震荡"
|
||||
elif emotion_score >= 30:
|
||||
emotion_label = "偏冷"
|
||||
else:
|
||||
emotion_label = "冰点"
|
||||
|
||||
return _json_safe({
|
||||
"as_of": str(as_of),
|
||||
"quote_status": status,
|
||||
"indices": indices,
|
||||
"breadth": {
|
||||
"total": total,
|
||||
"up": up,
|
||||
"down": down,
|
||||
"flat": flat,
|
||||
"up_pct": up_pct,
|
||||
"down_pct": down_pct,
|
||||
"avg_pct": avg_pct,
|
||||
"median_pct": median_pct,
|
||||
"strong_up": strong_up,
|
||||
"strong_down": strong_down,
|
||||
},
|
||||
"amount": {"total": total_amount, "avg": avg_amount},
|
||||
"boards": boards,
|
||||
"limit": {"limit_up": limit_up, "broken": broken, "failed": 0, "limit_down": limit_down, "max_boards": max_boards, "seal_rate": seal_rate, "tiers": tiers, "sealed_ready": sealed_ready, "fake_up": fake_up, "fake_down": fake_down},
|
||||
"distribution": _pct_band_rows(pct_values),
|
||||
"trend": {
|
||||
"above_ma5": above_ma5,
|
||||
"above_ma20": above_ma20,
|
||||
"above_ma60": above_ma60,
|
||||
"above_ma5_pct": above_ma5 / total * 100 if total else 0,
|
||||
"above_ma20_pct": above_ma20 / total * 100 if total else 0,
|
||||
"above_ma60_pct": above_ma60 / total * 100 if total else 0,
|
||||
"new_high": new_high,
|
||||
"new_low": new_low,
|
||||
},
|
||||
"activity": {
|
||||
"avg_turnover": avg_turnover,
|
||||
"high_turnover": high_turnover,
|
||||
"high_vol_ratio": high_vol_ratio,
|
||||
"vol_ratio": avg_vol_ratio,
|
||||
},
|
||||
"radar": radar,
|
||||
"emotion": {"score": emotion_score, "label": emotion_label},
|
||||
"top_gainers": _top_rows(rows, "change_pct", True),
|
||||
"top_losers": _top_rows(rows, "change_pct", False),
|
||||
"turnover_leaders": _top_rows(rows, "amount", True),
|
||||
"active_leaders": _top_rows(rows, "turnover_rate", True),
|
||||
"concept_rank": concept_rank,
|
||||
"industry_rank": industry_rank,
|
||||
})
|
||||
逻辑已抽离至 build_market_overview,以解耦对 Request 的依赖,
|
||||
使大盘复盘等无 Request 的调用方可复用同一装配逻辑。
|
||||
"""
|
||||
from app.services.market_overview_builder import build_market_overview
|
||||
return build_market_overview(
|
||||
repo=request.app.state.repo,
|
||||
quote_service=getattr(request.app.state, "quote_service", None),
|
||||
depth_service=getattr(request.app.state, "depth_service", None),
|
||||
as_of=as_of,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/market")
|
||||
|
||||
Reference in New Issue
Block a user