diff --git a/backend/app/__pycache__/config.cpython-313.pyc b/backend/app/__pycache__/config.cpython-313.pyc index bc830f60..bc43f2b8 100644 Binary files a/backend/app/__pycache__/config.cpython-313.pyc and b/backend/app/__pycache__/config.cpython-313.pyc differ diff --git a/backend/app/analysis/__pycache__/intraday.cpython-313.pyc b/backend/app/analysis/__pycache__/intraday.cpython-313.pyc index 5459eee6..80b07a20 100644 Binary files a/backend/app/analysis/__pycache__/intraday.cpython-313.pyc and b/backend/app/analysis/__pycache__/intraday.cpython-313.pyc differ diff --git a/backend/app/analysis/intraday.py b/backend/app/analysis/intraday.py index 789b1b7f..d1aca05a 100644 --- a/backend/app/analysis/intraday.py +++ b/backend/app/analysis/intraday.py @@ -20,7 +20,12 @@ from datetime import datetime from app.data.tushare_client import tushare_client from app.data import tencent_client from app.data.models import SectorInfo, Recommendation, MarketTemperature, StockQuote -from app.data.eastmoney_client import SECTOR_LIST_URL, SECTOR_HEADERS, _parse_eastmoney_json +from app.data.eastmoney_client import ( + SECTOR_LIST_URL, + SECTOR_HEADERS, + _parse_eastmoney_json, + get_a_share_realtime_ranking, +) from app.analysis.sector_scanner import scan_hot_sectors from app.analysis.technical import add_all_indicators from app.analysis.signals import generate_signals @@ -44,9 +49,28 @@ async def intraday_market_temperature(prev_temp: MarketTemperature) -> MarketTem limit_up_count = prev_temp.limit_up_count limit_down_count = prev_temp.limit_down_count + eastmoney_quotes = await get_a_share_realtime_ranking(page_size=6000) + if eastmoney_quotes: + up_count = sum(1 for q in eastmoney_quotes if q.get("pct_chg", 0) > 0) + down_count = sum(1 for q in eastmoney_quotes if q.get("pct_chg", 0) < 0) + limit_up_count = sum(1 for q in eastmoney_quotes if q.get("pct_chg", 0) >= _limit_threshold(q.get("ts_code", ""))) + limit_down_count = sum(1 for q in eastmoney_quotes if q.get("pct_chg", 0) <= -_limit_threshold(q.get("ts_code", ""))) + logger.info( + "东方财富实时市场温度: 上涨=%s 下跌=%s 涨停=%s 跌停=%s (共%s只)", + up_count, + down_count, + limit_up_count, + limit_down_count, + len(eastmoney_quotes), + ) + else: + logger.warning("东方财富全市场实时行情为空,尝试腾讯批量行情补充涨跌家数") + try: - stock_basic = tushare_client.get_stock_basic() - if not stock_basic.empty: + if not eastmoney_quotes: + stock_basic = tushare_client.get_stock_basic() + if stock_basic.empty: + raise ValueError("股票基础列表为空") all_codes = stock_basic[~stock_basic["name"].str.contains("ST", na=False)]["ts_code"].tolist() if all_codes: @@ -61,6 +85,8 @@ async def intraday_market_temperature(prev_temp: MarketTemperature) -> MarketTem # ── 用东方财富 clist API 统计涨停跌停(比腾讯涨停价字段更可靠) ── try: + if eastmoney_quotes: + raise RuntimeError("已使用东方财富全市场实时涨跌停统计") realtime_limit_up_count = 0 realtime_limit_down_count = 0 @@ -108,7 +134,8 @@ async def intraday_market_temperature(prev_temp: MarketTemperature) -> MarketTem limit_down_count = realtime_limit_down_count logger.info(f"东方财富盘中涨跌停: 涨停={limit_up_count} 跌停={limit_down_count}") except Exception as e: - logger.warning(f"东方财富涨跌停统计失败,使用基线数据: {e}") + if not eastmoney_quotes: + logger.warning(f"东方财富涨跌停统计失败,使用基线数据: {e}") # ── 温度分数:基于实时涨跌比重新计算 ── ratio = up_count / max(down_count, 1) @@ -135,6 +162,13 @@ async def intraday_market_temperature(prev_temp: MarketTemperature) -> MarketTem ) +def _limit_threshold(ts_code: str) -> float: + code = ts_code.split(".")[0] if ts_code else "" + if code.startswith(("300", "301", "688")): + return 19.8 + return 9.8 + + async def intraday_filter_stocks( hot_sectors: list[SectorInfo], ) -> list[dict]: @@ -245,6 +279,59 @@ async def intraday_filter_stocks( return top +async def intraday_active_market_recall(limit: int = 80) -> list[dict]: + """实时全市场活跃股召回,不依赖 Tushare 板块成分映射。""" + quotes = await get_a_share_realtime_ranking(sort_by="f8", descending=True, page_size=800) + if not quotes: + return [] + + results = [] + for item in quotes: + ts_code = item.get("ts_code", "") + name = item.get("name", "") + if not ts_code or not name or "ST" in name: + continue + pct_chg = float(item.get("pct_chg", 0) or 0) + turnover_rate = float(item.get("turnover_rate", 0) or 0) + circ_mv_raw = item.get("circ_mv") + circ_mv = float(circ_mv_raw or 0) / 100000000 if circ_mv_raw else None + if pct_chg <= 0 or pct_chg >= _limit_threshold(ts_code): + continue + if turnover_rate < max(settings.min_turnover_rate * 0.5, 1.0): + continue + if circ_mv is not None and circ_mv > 0: + if circ_mv < settings.min_circ_mv or circ_mv > settings.max_circ_mv * 1.5: + continue + + recall_score = 35 + recall_score += min(max(pct_chg, 0), 8) * 4 + recall_score += min(turnover_rate, 12) * 2 + if item.get("main_net_inflow", 0) > 0: + recall_score += 8 + + results.append({ + "ts_code": ts_code, + "name": name, + "sector": "实时活跃", + "sector_stage": "intraday", + "price": item.get("price"), + "pct_chg": pct_chg, + "turnover_rate": turnover_rate, + "circ_mv": circ_mv, + "pe": item.get("pe"), + "pb": item.get("pb"), + "volume_ratio": None, + "main_net_inflow": float(item.get("main_net_inflow", 0) or 0) / 10000, + "inflow_ratio": 0, + "recall_score": round(recall_score, 1), + "recall_tags": ["realtime_active"], + "stock_role_hint": "今日全市场活跃股", + }) + + results.sort(key=lambda x: (x["recall_score"], x.get("turnover_rate", 0)), reverse=True) + return results[:limit] + + def _score_intraday(quote: StockQuote) -> float: """盘中评分逻辑(替代资金流向评分) diff --git a/backend/app/analysis/sector_realtime.py b/backend/app/analysis/sector_realtime.py index 90e07da2..c216f5b4 100644 --- a/backend/app/analysis/sector_realtime.py +++ b/backend/app/analysis/sector_realtime.py @@ -6,7 +6,7 @@ import logging -from app.config import should_prefer_realtime_today +from app.config import should_prefer_realtime_today, today_trade_date from app.data.eastmoney_client import get_sector_realtime_ranking from app.data.models import SectorInfo from app.data.tushare_client import tushare_client @@ -37,6 +37,52 @@ def _apply_empty_overlay(sector: SectorInfo) -> SectorInfo: return sector +def _sector_from_eastmoney(item: dict) -> SectorInfo: + """把东方财富板块榜转换成今日展示用 SectorInfo。""" + sector = SectorInfo( + sector_code=item.get("sector_code", ""), + sector_name=item.get("sector_name", ""), + trade_date=today_trade_date(), + pct_change=float(item.get("pct_change", 0) or 0), + capital_inflow=0, + limit_up_count=0, + days_continuous=0, + heat_score=round(max(float(item.get("pct_change", 0) or 0), 0) * 12, 1), + stage="intraday", + turnover_avg=float(item.get("turnover_rate", 0) or 0), + realtime_pct_change=float(item.get("pct_change", 0) or 0), + realtime_limit_up_count=None, + realtime_amount=round(float(item.get("amount", 0) or 0) / 10000, 2), + realtime_turnover_rate=float(item.get("turnover_rate", 0) or 0), + realtime_up_count=int(item.get("up_count", 0) or 0), + realtime_down_count=int(item.get("down_count", 0) or 0), + is_realtime=True, + data_mode="realtime_today", + ) + if item.get("leading_stock_name"): + sector.leading_stocks_realtime = [{ + "ts_code": item.get("leading_stock_code", ""), + "name": item.get("leading_stock_name", ""), + "pct_chg": float(item.get("leading_stock_pct", 0) or 0), + "amount": 0, + }] + sector.leading_stocks = sector.leading_stocks_realtime + return sector + + +async def get_today_realtime_sector_board(limit: int = 20) -> list[SectorInfo]: + """用东方财富今日板块榜生成展示列表,作为 Tushare/定时扫描滞后的兜底。""" + try: + em_sectors = await get_sector_realtime_ranking(page_size=max(limit, 20)) + except Exception: + logger.warning("东方财富今日板块榜获取失败") + return [] + + sectors = [_sector_from_eastmoney(item) for item in em_sectors[:limit]] + sectors.sort(key=lambda s: s.realtime_pct_change if s.realtime_pct_change is not None else s.pct_change, reverse=True) + return sectors + + async def enrich_sectors_with_realtime(sectors: list[SectorInfo]) -> list[SectorInfo]: """按需为板块快照追加实时字段并重排。""" if not sectors: diff --git a/backend/app/api/__pycache__/market.cpython-313.pyc b/backend/app/api/__pycache__/market.cpython-313.pyc index b8d34fb1..2eff7009 100644 Binary files a/backend/app/api/__pycache__/market.cpython-313.pyc and b/backend/app/api/__pycache__/market.cpython-313.pyc differ diff --git a/backend/app/api/__pycache__/recommendations.cpython-313.pyc b/backend/app/api/__pycache__/recommendations.cpython-313.pyc index d545b149..af1ceb67 100644 Binary files a/backend/app/api/__pycache__/recommendations.cpython-313.pyc and b/backend/app/api/__pycache__/recommendations.cpython-313.pyc differ diff --git a/backend/app/api/__pycache__/sectors.cpython-313.pyc b/backend/app/api/__pycache__/sectors.cpython-313.pyc index 8f0c39f9..2bb537af 100644 Binary files a/backend/app/api/__pycache__/sectors.cpython-313.pyc and b/backend/app/api/__pycache__/sectors.cpython-313.pyc differ diff --git a/backend/app/api/market.py b/backend/app/api/market.py index f9130aca..5ebda578 100644 --- a/backend/app/api/market.py +++ b/backend/app/api/market.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends from app.data.tushare_client import tushare_client from app.data import tencent_client from app.engine.recommender import get_latest_recommendations -from app.config import is_trading_hours, is_market_session, should_prefer_realtime_today +from app.config import is_trading_hours, is_market_session, should_prefer_realtime_today, today_trade_date from app.core.deps import get_current_admin router = APIRouter(prefix="/api/market", tags=["market"]) @@ -109,10 +109,13 @@ async def get_ops_status(): latest_market_date = str(market_row._mapping["trade_date"]) if market_row else "" latest_sector_date = str(sector_row._mapping["trade_date"]) if sector_row else "" latest_tracking_date = str(tracking_row._mapping["track_date"]) if tracking_row else "" + today = today_trade_date() + sector_lagging = bool(latest_sector_date and latest_sector_date.replace("-", "") < today) + market_lagging = bool(latest_market_date and latest_market_date.replace("-", "") < today) return { "scan_running": _scan_running, - "scan_mode": "intraday" if is_trading_hours() else "post_market", + "scan_mode": "realtime_today" if should_prefer_realtime_today(latest_market_date) else "post_market", "is_trading": is_trading_hours(), "data_freshness": { "market_trade_date": latest_market_date, @@ -122,8 +125,10 @@ async def get_ops_status(): "last_tracking_created_at": _fmt_dt(tracking_row._mapping["created_at"]) if tracking_row else "", "last_market_created_at": _fmt_dt(market_row._mapping["created_at"]) if market_row else "", "last_sector_created_at": _fmt_dt(sector_row._mapping["created_at"]) if sector_row else "", - "status": "fresh" if latest_market_date else "empty", + "status": "stale" if sector_lagging or market_lagging else "fresh" if latest_market_date else "empty", "message": ( + f"板块快照仍停留在 {latest_sector_date},展示层将优先使用今日实时板块榜。" + if sector_lagging else f"最新市场日期 {latest_market_date},最近跟踪 {latest_tracking_date or '暂无'}" if latest_market_date else "暂无市场缓存数据,请由管理员触发扫描。" diff --git a/backend/app/api/recommendations.py b/backend/app/api/recommendations.py index 35d61030..ace9c3f4 100644 --- a/backend/app/api/recommendations.py +++ b/backend/app/api/recommendations.py @@ -160,7 +160,7 @@ async def get_scan_status(): prefer_realtime = should_prefer_realtime_today(latest_trade_date) return { "is_trading": is_trading_hours(), - "scan_mode": "intraday" if prefer_realtime else "post_market", + "scan_mode": "realtime_today" if prefer_realtime else "post_market", "description": "今日实时分析优先" if prefer_realtime else "盘后分析(Tushare日级数据)", } diff --git a/backend/app/api/sectors.py b/backend/app/api/sectors.py index a6ba4fe2..f6cceb29 100644 --- a/backend/app/api/sectors.py +++ b/backend/app/api/sectors.py @@ -2,7 +2,8 @@ from fastapi import APIRouter -from app.analysis.sector_realtime import enrich_sectors_with_realtime +from app.analysis.sector_realtime import enrich_sectors_with_realtime, get_today_realtime_sector_board +from app.config import should_prefer_realtime_today, today_trade_date from app.data.tushare_client import tushare_client from app.data.cache import cache from app.engine.recommender import get_latest_sectors @@ -14,7 +15,15 @@ router = APIRouter(prefix="/api/sectors", tags=["sectors"]) async def get_hot_sectors(limit: int = 10): """获取热门板块排名(盘中自动补充实时数据)""" sectors = await get_latest_sectors() - sectors = await enrich_sectors_with_realtime(sectors) + snapshot_trade_date = sectors[0].trade_date if sectors else "" + if should_prefer_realtime_today(snapshot_trade_date) or snapshot_trade_date != today_trade_date(): + realtime_sectors = await get_today_realtime_sector_board(limit=max(limit, 20)) + if realtime_sectors: + sectors = realtime_sectors + else: + sectors = await enrich_sectors_with_realtime(sectors) + else: + sectors = await enrich_sectors_with_realtime(sectors) trade_date = sectors[0].trade_date if sectors else "" sectors_data = [ @@ -48,7 +57,7 @@ async def get_hot_sectors(limit: int = 10): ] realtime_enabled = any(s.get("is_realtime") for s in sectors_data) - mode = "realtime_overlay" if realtime_enabled else "daily_snapshot" + mode = sectors[0].data_mode if realtime_enabled and sectors else "daily_snapshot" for s in sectors_data: s["data_mode"] = mode s["structure_trade_date"] = trade_date diff --git a/backend/app/config.py b/backend/app/config.py index fea0d7ee..b0733fcf 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -129,15 +129,22 @@ def should_prefer_realtime_today(latest_trade_date: str | None = None) -> bool: """是否应该优先使用“今天”的实时数据。 规则: - 1. 非交易日直接 False - 2. 交易日内(含午休、收盘后)如果 Tushare 最新交易日还不是今天,则优先实时 - 3. 即便 Tushare 最新交易日已经是今天,只要仍处于 15:30 前的延迟窗口,也优先实时 + 1. 非工作日直接 False + 2. 交易日 09:15 后都优先实时源,包括收盘后;腾讯/东方财富会保留当日收盘快照 + 3. 盘前不使用今日实时源,避免拿到昨日收盘价却标成今日 """ - if not is_market_session() and not is_pre_close(): + from zoneinfo import ZoneInfo + + now = datetime.now(ZoneInfo("Asia/Shanghai")) + if now.weekday() >= 5: + return False + + t = now.hour * 100 + now.minute + if t < 915: return False today = today_trade_date() - if latest_trade_date and latest_trade_date != today: - return True + if latest_trade_date and latest_trade_date.replace("-", "") > today: + return False - return is_market_session() or is_pre_close() + return True diff --git a/backend/app/data/eastmoney_client.py b/backend/app/data/eastmoney_client.py index 7b8dd70c..19191b22 100644 --- a/backend/app/data/eastmoney_client.py +++ b/backend/app/data/eastmoney_client.py @@ -140,6 +140,91 @@ async def get_sector_realtime_ranking( return [] +async def get_a_share_realtime_ranking( + sort_by: str = "f3", + descending: bool = True, + page_size: int = 6000, +) -> list[dict]: + """获取 A 股实时行情列表,用于市场温度和实时候选召回。""" + cache_key = f"ashare_rt:{sort_by}:{descending}:{page_size}" + cached = cache.get(cache_key) + if cached is not None: + return cached + + params = { + "pn": "1", + "pz": str(page_size), + "po": "0" if descending else "1", + "np": "1", + "ut": "b1f8f8f8", + "fltt": "2", + "invt": "2", + "fid": sort_by, + "fs": "m:0+t:6,m:0+t:80,m:0+t:81+s:2048,m:1+t:2,m:1+t:23", + "fields": "f2,f3,f6,f8,f9,f12,f14,f20,f21,f23,f62", + } + + try: + async with httpx.AsyncClient() as client: + resp = await client.get( + SECTOR_LIST_URL, + params=params, + headers=SECTOR_HEADERS, + timeout=12, + follow_redirects=True, + ) + data = _parse_eastmoney_json(resp, "A股实时行情") + + items = data.get("data", {}).get("diff", []) + result = [] + for item in items: + pct = item.get("f3") + price = item.get("f2") + if pct == "-" or price == "-" or pct is None or price is None: + continue + result.append({ + "ts_code": _eastmoney_code_to_ts(str(item.get("f12", ""))), + "name": item.get("f14", ""), + "price": float(price or 0), + "pct_chg": float(pct or 0), + "amount": float(item.get("f6", 0) or 0), + "turnover_rate": float(item.get("f8", 0) or 0), + "pe": _safe_float(item.get("f9")), + "pb": _safe_float(item.get("f23")), + "total_mv": _safe_float(item.get("f20")), + "circ_mv": _safe_float(item.get("f21")), + "main_net_inflow": _safe_float(item.get("f62")) or 0, + }) + + ttl = 60 if _is_trading_hours() else 300 + cache.set(cache_key, result, ttl) + logger.info("东方财富A股实时行情: 获取 %s 只", len(result)) + return result + except Exception as e: + logger.error(f"东方财富A股实时行情获取失败: {e}") + await log_error( + "eastmoney", + f"东方财富A股实时行情获取失败: {e}", + detail=f"sort_by={sort_by}, page_size={page_size}", + ) + return [] + + +def _eastmoney_code_to_ts(code: str) -> str: + if code.startswith("6"): + return f"{code}.SH" + return f"{code}.SZ" + + +def _safe_float(value) -> float | None: + if value in (None, "", "-"): + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + async def get_min_kline( ts_code: str, period: str = "5", diff --git a/backend/app/engine/__pycache__/screener.cpython-313.pyc b/backend/app/engine/__pycache__/screener.cpython-313.pyc index 19e80bcf..1f093c45 100644 Binary files a/backend/app/engine/__pycache__/screener.cpython-313.pyc and b/backend/app/engine/__pycache__/screener.cpython-313.pyc differ diff --git a/backend/app/engine/screener.py b/backend/app/engine/screener.py index 9dd64a36..7dc80362 100644 --- a/backend/app/engine/screener.py +++ b/backend/app/engine/screener.py @@ -25,11 +25,17 @@ 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.sector_realtime import get_today_realtime_sector_board from app.analysis.trend_scanner import scan_trend_breakout from app.analysis.signals import generate_signals -from app.analysis.intraday import intraday_market_temperature, intraday_filter_stocks, intraday_sector_scan +from app.analysis.intraday import ( + intraday_active_market_recall, + intraday_market_temperature, + intraday_filter_stocks, + intraday_sector_scan, +) from app.data.models import MarketTemperature, SectorInfo, TechnicalSignal, Recommendation -from app.config import settings, is_trading_hours, is_market_session, should_prefer_realtime_today +from app.config import settings, should_prefer_realtime_today from app.data.tushare_client import tushare_client from app.llm.strategy_selector import select_strategy_profile @@ -48,8 +54,8 @@ async def run_screening(trade_date: str = None) -> dict: """ latest_trade_date = tushare_client.get_latest_trade_date() intraday = should_prefer_realtime_today(latest_trade_date) - scan_mode = "intraday" if intraday else "post_market" - logger.info(f"=== 筛选模式: {'盘中实时' if intraday else '盘后'} ===") + scan_mode = "realtime_today" if intraday else "post_market" + logger.info(f"=== 筛选模式: {'今日实时' if intraday else '历史收盘'} ===") # ── 市场温度 ── logger.info("=== 市场温度计 ===") @@ -65,12 +71,14 @@ async def run_screening(trade_date: str = None) -> dict: # ── Step 1: 板块定位 ── logger.info("=== Step 1: 板块定位 ===") - all_sectors = scan_hot_sectors(trade_date) + all_sectors = await get_today_realtime_sector_board(limit=30) if intraday else [] + if not all_sectors: + all_sectors = scan_hot_sectors(trade_date) # 前置过滤:只保留有资金流入 + 非末期的板块 hot_sectors = [ s for s in all_sectors - if s.capital_inflow > 0 and s.stage not in ("end",) + if (s.capital_inflow > 0 or s.is_realtime) and s.stage not in ("end",) ][:settings.top_sector_count] if not hot_sectors: @@ -81,8 +89,8 @@ async def run_screening(trade_date: str = None) -> dict: logger.info(f" 目标板块: {s.sector_name} 涨幅{s.pct_change}% 资金{s.capital_inflow:.0f}万 " f"涨停{s.limit_up_count} 阶段={s.stage}") - # 盘中用实时行情更新板块涨幅和涨停数 - if intraday: + # 如果板块来自 Tushare 快照,盘中/盘后用实时行情更新板块涨幅和广度 + if intraday and hot_sectors and not hot_sectors[0].is_realtime: hot_sectors = await intraday_sector_scan(hot_sectors) strategy_profile = await select_strategy_profile(market_temp, hot_sectors, intraday) @@ -361,6 +369,15 @@ async def _build_candidate_pool( intraday_candidates = [] _merge_candidate_batch(merged, intraday_candidates, route="intraday_active") + try: + realtime_candidates = await intraday_active_market_recall(limit=settings.candidate_pool_limit) + except Exception as e: + logger.warning(f"实时全市场召回失败: {e}") + realtime_candidates = [] + _merge_candidate_batch(merged, realtime_candidates, route="realtime_market") + else: + realtime_candidates = [] + candidates = list(merged.values()) candidates.sort(key=lambda item: ( item.get("recall_score", 0), @@ -372,7 +389,7 @@ async def _build_candidate_pool( logger.info( f"Step 2 多路召回完成: sector={len(sector_candidates)} " f"trend={len(trend_candidates)} " - f"{'intraday=' + str(len(intraday_candidates)) if intraday else ''} " + f"{'intraday=' + str(len(intraday_candidates)) + ' realtime=' + str(len(realtime_candidates)) if intraday else ''} " f"→ merged={len(top)}" ) return top diff --git a/backend/app/llm/strategy_board.py b/backend/app/llm/strategy_board.py index 532cf6e2..2349d9c6 100644 --- a/backend/app/llm/strategy_board.py +++ b/backend/app/llm/strategy_board.py @@ -7,6 +7,7 @@ import logging from app.analysis.sector_realtime import enrich_sectors_with_realtime +from app.analysis.sector_realtime import get_today_realtime_sector_board from app.config import settings, should_prefer_realtime_today, today_trade_date from app.data.models import ( MarketTemperature, @@ -61,7 +62,12 @@ async def build_strategy_board(include_llm: bool = False) -> dict: market_temp = latest.get("market_temp") recommendations = latest.get("recommendations", []) sectors = await get_latest_sectors() - sectors = await enrich_sectors_with_realtime(sectors) + snapshot_trade_date = sectors[0].trade_date if sectors else "" + if should_prefer_realtime_today(snapshot_trade_date) or snapshot_trade_date != today_trade_date(): + realtime_sectors = await get_today_realtime_sector_board(limit=20) + sectors = realtime_sectors or await enrich_sectors_with_realtime(sectors) + else: + sectors = await enrich_sectors_with_realtime(sectors) performance = await get_performance_stats() from app.llm.strategy_iteration import build_strategy_iteration_report iteration_report = await build_strategy_iteration_report(limit=50, include_llm=include_llm) diff --git a/backend/astock.db b/backend/astock.db index e72a35bc..7dcbaa3f 100644 Binary files a/backend/astock.db and b/backend/astock.db differ diff --git a/frontend/.next/server/app-paths-manifest.json b/frontend/.next/server/app-paths-manifest.json index a923b582..8bd5c2dc 100644 --- a/frontend/.next/server/app-paths-manifest.json +++ b/frontend/.next/server/app-paths-manifest.json @@ -1,7 +1,7 @@ { "/(auth)/dashboard/page": "app/(auth)/dashboard/page.js", + "/(auth)/stock/[code]/page": "app/(auth)/stock/[code]/page.js", "/(auth)/chat/page": "app/(auth)/chat/page.js", "/(public)/login/page": "app/(public)/login/page.js", - "/(auth)/stock/[code]/page": "app/(auth)/stock/[code]/page.js", "/(public)/page": "app/(public)/page.js" } \ No newline at end of file diff --git a/frontend/src/app/(auth)/dashboard/page.tsx b/frontend/src/app/(auth)/dashboard/page.tsx index 548bc231..a81b9d40 100644 --- a/frontend/src/app/(auth)/dashboard/page.tsx +++ b/frontend/src/app/(auth)/dashboard/page.tsx @@ -70,7 +70,9 @@ export default function DashboardPage() { useCallback((msg: { type: string; count?: number; scan_mode?: string; message?: string }) => { clearScanTimeout(); if (msg.type === "scan_update") { - const modeLabel = msg.scan_mode === "intraday" ? "盘中实时" : "盘后"; + const modeLabel = msg.scan_mode === "realtime_today" || msg.scan_mode === "intraday" + ? "今日实时" + : "历史收盘"; setRefreshResult(`${modeLabel}扫描完成,发现 ${msg.count ?? 0} 只股票`); setRefreshing(false); loadData(); diff --git a/frontend/src/app/(auth)/sectors/page.tsx b/frontend/src/app/(auth)/sectors/page.tsx index 15cdc91c..b5442780 100644 --- a/frontend/src/app/(auth)/sectors/page.tsx +++ b/frontend/src/app/(auth)/sectors/page.tsx @@ -423,8 +423,13 @@ export default function SectorsPage() {

板块主线

判断当前主线、板块阶段、资金持续性和领涨股强度 - {hasRealtime && · 盘中实时优先} + {hasRealtime && · 今日实时优先}

+ {hasRealtime && dataMode === "realtime_today" && ( +

+ 当前使用东方财富今日板块榜,涨幅、成交额、上涨/下跌家数与领涨股均为今日实时/收盘快照。 +

+ )} {hasRealtime && dataMode === "realtime_overlay" && (

盘中模式下,涨幅、成交额、上涨/下跌家数与领涨股为实时覆盖;阶段、资金连续性等结构字段仍基于 diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index af7fe275..27fd29d2 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -197,7 +197,7 @@ export interface SectorData { realtime_up_count?: number | null; realtime_down_count?: number | null; is_realtime?: boolean; - data_mode?: "realtime_overlay" | "daily_snapshot"; + data_mode?: "realtime_today" | "realtime_overlay" | "daily_snapshot"; structure_trade_date?: string; }