This commit is contained in:
aaron 2026-04-23 17:59:43 +08:00
parent 23dd333ae4
commit d9e77a7bec
20 changed files with 302 additions and 33 deletions

View File

@ -20,7 +20,12 @@ from datetime import datetime
from app.data.tushare_client import tushare_client from app.data.tushare_client import tushare_client
from app.data import tencent_client from app.data import tencent_client
from app.data.models import SectorInfo, Recommendation, MarketTemperature, StockQuote 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.sector_scanner import scan_hot_sectors
from app.analysis.technical import add_all_indicators from app.analysis.technical import add_all_indicators
from app.analysis.signals import generate_signals 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_up_count = prev_temp.limit_up_count
limit_down_count = prev_temp.limit_down_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: try:
stock_basic = tushare_client.get_stock_basic() if not eastmoney_quotes:
if not stock_basic.empty: 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() all_codes = stock_basic[~stock_basic["name"].str.contains("ST", na=False)]["ts_code"].tolist()
if all_codes: if all_codes:
@ -61,6 +85,8 @@ async def intraday_market_temperature(prev_temp: MarketTemperature) -> MarketTem
# ── 用东方财富 clist API 统计涨停跌停(比腾讯涨停价字段更可靠) ── # ── 用东方财富 clist API 统计涨停跌停(比腾讯涨停价字段更可靠) ──
try: try:
if eastmoney_quotes:
raise RuntimeError("已使用东方财富全市场实时涨跌停统计")
realtime_limit_up_count = 0 realtime_limit_up_count = 0
realtime_limit_down_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 limit_down_count = realtime_limit_down_count
logger.info(f"东方财富盘中涨跌停: 涨停={limit_up_count} 跌停={limit_down_count}") logger.info(f"东方财富盘中涨跌停: 涨停={limit_up_count} 跌停={limit_down_count}")
except Exception as e: except Exception as e:
logger.warning(f"东方财富涨跌停统计失败,使用基线数据: {e}") if not eastmoney_quotes:
logger.warning(f"东方财富涨跌停统计失败,使用基线数据: {e}")
# ── 温度分数:基于实时涨跌比重新计算 ── # ── 温度分数:基于实时涨跌比重新计算 ──
ratio = up_count / max(down_count, 1) 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( async def intraday_filter_stocks(
hot_sectors: list[SectorInfo], hot_sectors: list[SectorInfo],
) -> list[dict]: ) -> list[dict]:
@ -245,6 +279,59 @@ async def intraday_filter_stocks(
return top 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: def _score_intraday(quote: StockQuote) -> float:
"""盘中评分逻辑(替代资金流向评分) """盘中评分逻辑(替代资金流向评分)

View File

@ -6,7 +6,7 @@
import logging 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.eastmoney_client import get_sector_realtime_ranking
from app.data.models import SectorInfo from app.data.models import SectorInfo
from app.data.tushare_client import tushare_client from app.data.tushare_client import tushare_client
@ -37,6 +37,52 @@ def _apply_empty_overlay(sector: SectorInfo) -> SectorInfo:
return sector 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]: async def enrich_sectors_with_realtime(sectors: list[SectorInfo]) -> list[SectorInfo]:
"""按需为板块快照追加实时字段并重排。""" """按需为板块快照追加实时字段并重排。"""
if not sectors: if not sectors:

View File

@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends
from app.data.tushare_client import tushare_client from app.data.tushare_client import tushare_client
from app.data import tencent_client from app.data import tencent_client
from app.engine.recommender import get_latest_recommendations 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 from app.core.deps import get_current_admin
router = APIRouter(prefix="/api/market", tags=["market"]) 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_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_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 "" 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 { return {
"scan_running": _scan_running, "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(), "is_trading": is_trading_hours(),
"data_freshness": { "data_freshness": {
"market_trade_date": latest_market_date, "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_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_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 "", "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": ( "message": (
f"板块快照仍停留在 {latest_sector_date},展示层将优先使用今日实时板块榜。"
if sector_lagging else
f"最新市场日期 {latest_market_date},最近跟踪 {latest_tracking_date or '暂无'}" f"最新市场日期 {latest_market_date},最近跟踪 {latest_tracking_date or '暂无'}"
if latest_market_date else if latest_market_date else
"暂无市场缓存数据,请由管理员触发扫描。" "暂无市场缓存数据,请由管理员触发扫描。"

View File

@ -160,7 +160,7 @@ async def get_scan_status():
prefer_realtime = should_prefer_realtime_today(latest_trade_date) prefer_realtime = should_prefer_realtime_today(latest_trade_date)
return { return {
"is_trading": is_trading_hours(), "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日级数据", "description": "今日实时分析优先" if prefer_realtime else "盘后分析Tushare日级数据",
} }

View File

@ -2,7 +2,8 @@
from fastapi import APIRouter 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.tushare_client import tushare_client
from app.data.cache import cache from app.data.cache import cache
from app.engine.recommender import get_latest_sectors 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): async def get_hot_sectors(limit: int = 10):
"""获取热门板块排名(盘中自动补充实时数据)""" """获取热门板块排名(盘中自动补充实时数据)"""
sectors = await get_latest_sectors() 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 "" trade_date = sectors[0].trade_date if sectors else ""
sectors_data = [ 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) 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: for s in sectors_data:
s["data_mode"] = mode s["data_mode"] = mode
s["structure_trade_date"] = trade_date s["structure_trade_date"] = trade_date

View File

@ -129,15 +129,22 @@ def should_prefer_realtime_today(latest_trade_date: str | None = None) -> bool:
"""是否应该优先使用“今天”的实时数据。 """是否应该优先使用“今天”的实时数据。
规则 规则
1. 交易日直接 False 1. 工作日直接 False
2. 交易日含午休收盘后如果 Tushare 最新交易日还不是今天则优先实时 2. 交易日 09:15 后都优先实时源包括收盘后腾讯/东方财富会保留当日收盘快照
3. 即便 Tushare 最新交易日已经是今天只要仍处于 15:30 前的延迟窗口也优先实时 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 return False
today = today_trade_date() today = today_trade_date()
if latest_trade_date and latest_trade_date != today: if latest_trade_date and latest_trade_date.replace("-", "") > today:
return True return False
return is_market_session() or is_pre_close() return True

View File

@ -140,6 +140,91 @@ async def get_sector_realtime_ranking(
return [] 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( async def get_min_kline(
ts_code: str, ts_code: str,
period: str = "5", period: str = "5",

View File

@ -25,11 +25,17 @@ import pandas as pd
from app.analysis.market_temp import calculate_market_temperature from app.analysis.market_temp import calculate_market_temperature
from app.analysis.sector_scanner import scan_hot_sectors 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.trend_scanner import scan_trend_breakout
from app.analysis.signals import generate_signals 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.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.data.tushare_client import tushare_client
from app.llm.strategy_selector import select_strategy_profile 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() latest_trade_date = tushare_client.get_latest_trade_date()
intraday = should_prefer_realtime_today(latest_trade_date) intraday = should_prefer_realtime_today(latest_trade_date)
scan_mode = "intraday" if intraday else "post_market" scan_mode = "realtime_today" if intraday else "post_market"
logger.info(f"=== 筛选模式: {'盘中实时' if intraday else ''} ===") logger.info(f"=== 筛选模式: {'今日实时' if intraday else '历史收'} ===")
# ── 市场温度 ── # ── 市场温度 ──
logger.info("=== 市场温度计 ===") logger.info("=== 市场温度计 ===")
@ -65,12 +71,14 @@ async def run_screening(trade_date: str = None) -> dict:
# ── Step 1: 板块定位 ── # ── Step 1: 板块定位 ──
logger.info("=== 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 = [ hot_sectors = [
s for s in all_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] ][:settings.top_sector_count]
if not hot_sectors: 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}" logger.info(f" 目标板块: {s.sector_name} 涨幅{s.pct_change}% 资金{s.capital_inflow:.0f}"
f"涨停{s.limit_up_count} 阶段={s.stage}") f"涨停{s.limit_up_count} 阶段={s.stage}")
# 盘中用实时行情更新板块涨幅和涨停数 # 如果板块来自 Tushare 快照,盘中/盘后用实时行情更新板块涨幅和广度
if intraday: if intraday and hot_sectors and not hot_sectors[0].is_realtime:
hot_sectors = await intraday_sector_scan(hot_sectors) hot_sectors = await intraday_sector_scan(hot_sectors)
strategy_profile = await select_strategy_profile(market_temp, hot_sectors, intraday) strategy_profile = await select_strategy_profile(market_temp, hot_sectors, intraday)
@ -361,6 +369,15 @@ async def _build_candidate_pool(
intraday_candidates = [] intraday_candidates = []
_merge_candidate_batch(merged, intraday_candidates, route="intraday_active") _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 = list(merged.values())
candidates.sort(key=lambda item: ( candidates.sort(key=lambda item: (
item.get("recall_score", 0), item.get("recall_score", 0),
@ -372,7 +389,7 @@ async def _build_candidate_pool(
logger.info( logger.info(
f"Step 2 多路召回完成: sector={len(sector_candidates)} " f"Step 2 多路召回完成: sector={len(sector_candidates)} "
f"trend={len(trend_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)}" f"→ merged={len(top)}"
) )
return top return top

View File

@ -7,6 +7,7 @@
import logging import logging
from app.analysis.sector_realtime import enrich_sectors_with_realtime 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.config import settings, should_prefer_realtime_today, today_trade_date
from app.data.models import ( from app.data.models import (
MarketTemperature, MarketTemperature,
@ -61,7 +62,12 @@ async def build_strategy_board(include_llm: bool = False) -> dict:
market_temp = latest.get("market_temp") market_temp = latest.get("market_temp")
recommendations = latest.get("recommendations", []) recommendations = latest.get("recommendations", [])
sectors = await get_latest_sectors() 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() performance = await get_performance_stats()
from app.llm.strategy_iteration import build_strategy_iteration_report from app.llm.strategy_iteration import build_strategy_iteration_report
iteration_report = await build_strategy_iteration_report(limit=50, include_llm=include_llm) iteration_report = await build_strategy_iteration_report(limit=50, include_llm=include_llm)

Binary file not shown.

View File

@ -1,7 +1,7 @@
{ {
"/(auth)/dashboard/page": "app/(auth)/dashboard/page.js", "/(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", "/(auth)/chat/page": "app/(auth)/chat/page.js",
"/(public)/login/page": "app/(public)/login/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" "/(public)/page": "app/(public)/page.js"
} }

View File

@ -70,7 +70,9 @@ export default function DashboardPage() {
useCallback((msg: { type: string; count?: number; scan_mode?: string; message?: string }) => { useCallback((msg: { type: string; count?: number; scan_mode?: string; message?: string }) => {
clearScanTimeout(); clearScanTimeout();
if (msg.type === "scan_update") { 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} 只股票`); setRefreshResult(`${modeLabel}扫描完成,发现 ${msg.count ?? 0} 只股票`);
setRefreshing(false); setRefreshing(false);
loadData(); loadData();

View File

@ -423,8 +423,13 @@ export default function SectorsPage() {
<h1 className="text-lg font-bold tracking-tight">线</h1> <h1 className="text-lg font-bold tracking-tight">线</h1>
<p className="text-xs text-text-muted mt-0.5"> <p className="text-xs text-text-muted mt-0.5">
线 线
{hasRealtime && <span className="text-emerald-400/60 ml-1">· </span>} {hasRealtime && <span className="text-emerald-400/60 ml-1">· </span>}
</p> </p>
{hasRealtime && dataMode === "realtime_today" && (
<p className="text-[11px] text-text-muted/70 mt-2">
使//
</p>
)}
{hasRealtime && dataMode === "realtime_overlay" && ( {hasRealtime && dataMode === "realtime_overlay" && (
<p className="text-[11px] text-text-muted/70 mt-2"> <p className="text-[11px] text-text-muted/70 mt-2">
/ /

View File

@ -197,7 +197,7 @@ export interface SectorData {
realtime_up_count?: number | null; realtime_up_count?: number | null;
realtime_down_count?: number | null; realtime_down_count?: number | null;
is_realtime?: boolean; is_realtime?: boolean;
data_mode?: "realtime_overlay" | "daily_snapshot"; data_mode?: "realtime_today" | "realtime_overlay" | "daily_snapshot";
structure_trade_date?: string; structure_trade_date?: string;
} }