1
This commit is contained in:
parent
23dd333ae4
commit
d9e77a7bec
Binary file not shown.
Binary file not shown.
@ -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:
|
||||
"""盘中评分逻辑(替代资金流向评分)
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
"暂无市场缓存数据,请由管理员触发扫描。"
|
||||
|
||||
@ -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日级数据)",
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
Binary file not shown.
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Binary file not shown.
@ -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"
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -423,8 +423,13 @@ export default function SectorsPage() {
|
||||
<h1 className="text-lg font-bold tracking-tight">板块主线</h1>
|
||||
<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>
|
||||
{hasRealtime && dataMode === "realtime_today" && (
|
||||
<p className="text-[11px] text-text-muted/70 mt-2">
|
||||
当前使用东方财富今日板块榜,涨幅、成交额、上涨/下跌家数与领涨股均为今日实时/收盘快照。
|
||||
</p>
|
||||
)}
|
||||
{hasRealtime && dataMode === "realtime_overlay" && (
|
||||
<p className="text-[11px] text-text-muted/70 mt-2">
|
||||
盘中模式下,涨幅、成交额、上涨/下跌家数与领涨股为实时覆盖;阶段、资金连续性等结构字段仍基于
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user