from __future__ import annotations from collections import Counter import math from datetime import datetime, timedelta from typing import Any from sentiment_engine import apply_sentiment_to_dashboard DEMO_LIMITS = [ ("600664", "哈药股份", 4.94, 10.02, "医药", "创新药+医药流通", "09:25:00", "09:25:00", 0, 5, 11.78, 14.65, 26458), ("603580", "艾艾精工", 40.84, 9.99, "机器人", "实控人变更+机器人", "09:25:01", "09:25:01", 0, 3, 0.11, 0.53, 27190), ("600785", "新华百货", 9.32, 10.04, "零售", "新零售+股权转让", "10:32:33", "10:32:33", 2, 2, 9.57, 29.44, 4285), ("002739", "万达电影", 10.32, 10.02, "文化传媒", "影视院线+AI视频", "09:30:33", "09:30:33", 0, 2, 3.95, 217.94, 25014), ("000504", "南华生物", 9.36, 9.99, "医药", "细胞医疗+中报预增", "09:39:18", "09:39:18", 1, 2, 8.73, 30.89, 1962), ("000676", "智度股份", 6.22, 10.09, "端侧AI", "AI营销+端侧AI", "09:46:45", "09:46:45", 0, 2, 6.61, 78.36, 10368), ("600162", "香江控股", 2.78, 9.88, "房地产", "房地产+地产链", "09:30:57", "09:30:57", 1, 2, 10.56, 90.86, 4540), ("002365", "永安药业", 13.18, 10.02, "医药", "医药+宠物经济", "09:33:24", "09:33:24", 0, 2, 10.52, 38.84, 8277), ("000566", "海南海药", 5.67, 10.10, "脑机接口", "创新药+脑机接口", "11:01:12", "11:03:48", 2, 2, 22.75, 73.56, 8769), ("002632", "道明光学", 9.63, 10.06, "端侧AI", "AI手机+反光材料", "09:25:00", "09:25:00", 0, 1, 2.63, 60.15, 13417), ("000892", "欢瑞世纪", 3.87, 9.94, "文化传媒", "短剧+AI应用", "09:34:57", "09:34:57", 0, 1, 10.80, 37.96, 5635), ("603496", "恒为科技", 25.08, 10.00, "云计算", "算力+华为", "09:58:12", "10:46:30", 1, 1, 7.65, 80.31, 16611), ("603327", "福蓉科技", 8.57, 10.01, "端侧AI", "AI手机+消费电子", "09:30:02", "09:30:02", 0, 1, 7.02, 77.84, 7784), ("300968", "格林精密", 10.24, 20.00, "端侧AI", "折叠屏+AI眼镜", "09:36:33", "09:36:33", 0, 1, 20.06, 48.23, 7850), ("002045", "国光电器", 8.34, 10.03, "消费电子", "音响电声+AI眼镜", "09:37:45", "09:37:45", 0, 1, 7.11, 66.04, 4517), ("600203", "福日电子", 11.92, 9.96, "消费电子", "华为产业链+机器人", "09:45:03", "09:45:03", 0, 1, 12.04, 105.50, 10554), ("002881", "美格智能", 39.05, 10.00, "端侧AI", "物理AI+算力模组", "10:07:42", "10:07:42", 0, 1, 14.32, 128.20, 4299), ] DEMO_BROKEN = [ ("002141", "贤丰控股", 5.91, 5.35, "PCB板", "PCB板+资产重组", "09:37:03", "14:56:24", 3, 18.95, 61.05), ("002432", "九安医疗", 72.00, 7.48, "医药", "业绩增长+AI应用", "10:53:00", "14:09:45", 5, 14.13, 335.00), ("002980", "华盛昌", 107.37, 5.12, "光通信", "光通信+仪器仪表", "09:59:18", "14:38:36", 1, 17.94, 108.75), ("603725", "天安新材", 14.08, 7.40, "机器人", "机器人+新材料", "09:36:34", "14:37:19", 5, 13.58, 42.92), ("603127", "昭衍新药", 53.25, 5.20, "医药", "创新药+CRO", "10:35:49", "10:46:55", 2, 19.56, 335.66), ("002261", "拓维信息", 29.95, 6.47, "云计算", "算力+华为", "10:48:15", "10:53:54", 3, 12.04, 343.26), ("603893", "瑞芯微", 222.24, 5.58, "国产芯片", "国产芯片+端侧AI", "09:55:26", "13:31:14", 1, 7.60, 939.80), ("603103", "横店影视", 14.94, 5.21, "文化传媒", "影视院线+暑期档", "13:01:06", "13:01:51", 1, 2.79, 94.75), ] DEMO_DOWN = [ ("603683", "晶华新材", 25.56, -10.00, "新材料", "高位股风险释放", 4.41, 173.67, 1), ("603928", "兴业股份", 12.34, -9.99, "化工", "连续上涨后补跌", 11.96, 42.04, 4), ("000988", "华工科技", 130.69, -10.00, "光通信", "高位成交放大", 6.28, 1313.42, 1), ("603137", "恒尚节能", 32.05, -10.00, "建筑", "昨日涨停断板", 1.48, 58.63, 1), ("603115", "海星股份", 81.06, -10.00, "有色金属", "板块退潮", 3.02, 196.08, 1), ("605376", "博迁新材", 166.02, -10.00, "新材料", "资金兑现", 5.35, 434.31, 1), ("003020", "立方制药", 19.72, -10.00, "医药", "医药分化", 22.88, 45.00, 1), ("605255", "天普股份", 78.47, -10.00, "汽车零部件", "连板失败", 2.12, 105.21, 1), ("002123", "梦网科技", 7.68, -9.96, "通信", "板块调整", 1.39, 61.86, 2), ("603713", "密尔克卫", 64.80, -10.00, "物流", "业绩预期调整", 3.99, 103.43, 1), ] def _stock_rows() -> list[dict[str, Any]]: return [ { "code": code, "ts_code": code, "name": name, "price": price, "change": change, "sector": sector, "reason": reason, "first_time": first_time, "last_time": last_time, "open_times": open_times, "streak": streak, "turnover_rate": turnover, "amount_billion": amount, "seal_amount_million": seal, "float_mv_billion": round(amount * 3.2, 1), "status": "涨停", } for code, name, price, change, sector, reason, first_time, last_time, open_times, streak, turnover, amount, seal in DEMO_LIMITS ] def _broken_rows() -> list[dict[str, Any]]: return [ { "code": code, "ts_code": code, "name": name, "price": price, "change": change, "sector": sector, "reason": reason, "first_time": first_time, "last_time": last_time, "open_times": open_times, "streak": 1, "turnover_rate": turnover, "amount_billion": amount, "seal_amount_million": 0, "float_mv_billion": round(amount * 3.5, 1), "status": "炸板", } for code, name, price, change, sector, reason, first_time, last_time, open_times, turnover, amount in DEMO_BROKEN ] def _down_rows() -> list[dict[str, Any]]: return [ { "code": code, "ts_code": code, "name": name, "price": price, "change": change, "sector": sector, "reason": reason, "first_time": "--", "last_time": "--", "open_times": 0, "streak": streak, "turnover_rate": turnover, "amount_billion": amount, "seal_amount_million": 0, "float_mv_billion": round(amount * 4.1, 1), "status": "跌停", } for code, name, price, change, sector, reason, turnover, amount, streak in DEMO_DOWN ] def _ladders(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: result = [] for level in sorted({row["streak"] for row in rows}, reverse=True): stocks = [row for row in rows if row["streak"] == level] result.append( { "level": level, "label": "首板" if level == 1 else f"{level}板", "count": len(stocks), "stocks": stocks, } ) return result def _sectors(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: counts = Counter(row["sector"] for row in rows) result = [] for name, count in counts.most_common(): stocks = [row for row in rows if row["sector"] == name] result.append( { "name": name, "count": count, "strength": min(99, 48 + count * 9 + max(row["streak"] for row in stocks) * 4), "amount_billion": round(sum(row["amount_billion"] for row in stocks), 1), "leader": max(stocks, key=lambda row: (row["streak"], row["amount_billion"]))["name"], "change": round(sum(row["change"] for row in stocks) / count, 2), "max_streak": max(row["streak"] for row in stocks), } ) return result def _yesterday_rows(current: list[dict[str, Any]]) -> list[dict[str, Any]]: current_map = {row["code"]: row for row in current} definitions = [ ("600664", "哈药股份", 4, 10.02, "晋级"), ("603580", "艾艾精工", 2, 9.99, "晋级"), ("600785", "新华百货", 1, 10.04, "晋级"), ("002739", "万达电影", 1, 10.02, "晋级"), ("000504", "南华生物", 1, 9.99, "晋级"), ("000676", "智度股份", 1, 10.09, "晋级"), ("603127", "昭衍新药", 1, 5.20, "炸板"), ("002432", "九安医疗", 2, 7.48, "炸板"), ("001388", "信通电子", 3, -5.33, "断板"), ("605255", "天普股份", 2, -10.00, "跌停"), ("600403", "大有能源", 1, -6.75, "断板"), ("002185", "华天科技", 1, -10.00, "跌停"), ("600829", "人民同泰", 1, 2.30, "断板"), ("600844", "金煤科技", 1, 1.18, "断板"), ] rows = [] for code, name, prior_streak, current_change, outcome in definitions: current_row = current_map.get(code, {}) rows.append( { "code": code, "name": name, "prior_streak": prior_streak, "current_streak": current_row.get("streak", 0), "current_change": current_change, "current_price": current_row.get("price", 0), "sector": current_row.get("sector", "其他"), "reason": current_row.get("reason", "昨日涨停股表现跟踪"), "outcome": outcome, } ) return rows def _performance(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: result = [] for level in sorted({row["prior_streak"] for row in rows}, reverse=True): group = [row for row in rows if row["prior_streak"] == level] advanced = sum(row["outcome"] == "晋级" for row in group) positive = sum(row["current_change"] > 0 for row in group) result.append( { "level": level, "label": "昨日首板" if level == 1 else f"昨日{level}板", "count": len(group), "advanced": advanced, "advance_rate": round(advanced / len(group) * 100, 1), "positive_rate": round(positive / len(group) * 100, 1), "average_change": round(sum(row["current_change"] for row in group) / len(group), 2), } ) return result def _rotation(sectors: list[dict[str, Any]]) -> list[dict[str, Any]]: previous_counts = { "端侧AI": 7, "医药": 5, "文化传媒": 1, "消费电子": 1, "机器人": 3, "房地产": 2, "零售": 0, "云计算": 2, "脑机接口": 1, } result = [] for index, sector in enumerate(sectors, start=1): previous = previous_counts.get(sector["name"], 0) delta = sector["count"] - previous result.append( { **sector, "rank": index, "previous_count": previous, "delta": delta, "trend": "升温" if delta > 0 else "降温" if delta < 0 else "持平", } ) return result def build_demo_dashboard(trade_date: str, notice: str = "") -> dict[str, Any]: limits = _stock_rows() broken = _broken_rows() down_limits = _down_rows() ladders = _ladders(limits) sectors = _sectors(limits) yesterday = _yesterday_rows(limits) dashboard = { "meta": { "trade_date": f"{trade_date[:4]}-{trade_date[4:6]}-{trade_date[6:8]}", "previous_trade_date": "2026-07-16", "source": "demo", "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "notice": notice or "当前展示演示数据,配置 Tushare Token 后可读取真实行情。", }, "overview": { "up_count": 2344, "down_count": 2695, "flat_count": 33, "limit_up_count": 41, "limit_down_count": 3, "broken_count": 25, "amount_billion": 24035.6, "seal_rate": 62.1, }, "limits": limits, "broken": broken, "down_limits": down_limits, "yesterday_limits": yesterday, "limit_performance": _performance(yesterday), "ladders": ladders, "sectors": sectors, "sector_rotation": _rotation(sectors), } return apply_sentiment_to_dashboard(dashboard) def build_demo_dragon_tiger(trade_date: str, notice: str = "") -> dict[str, Any]: stocks = _stock_rows()[:10] seat_names = [ "机构专用", "沪股通专用", "深股通专用", "中信证券股份有限公司上海分公司", "国泰海通证券股份有限公司南京太平南路证券营业部", ] rows = [] for index, stock in enumerate(stocks): buy = round(86.5 - index * 6.3, 2) sell = round(22.8 + index * 3.1, 2) net = round(buy - sell, 2) institutions = [ { "seat_name": seat_names[index % len(seat_names)], "buy_million": buy, "sell_million": sell, "net_buy_million": net, }, { "seat_name": seat_names[(index + 2) % len(seat_names)], "buy_million": round(buy * 0.42, 2), "sell_million": round(sell * 0.65, 2), "net_buy_million": round(buy * 0.42 - sell * 0.65, 2), }, ] rows.append( { "code": stock["code"], "ts_code": stock["code"] + (".SH" if stock["code"].startswith("6") else ".SZ"), "name": stock["name"], "price": stock["price"], "change": stock["change"], "turnover_rate": stock["turnover_rate"], "amount_billion": stock["amount_billion"], "buy_million": buy, "sell_million": sell, "net_buy_million": net, "net_rate": round(net / max(buy + sell, 1) * 100, 2), "reason": "日涨幅偏离值达到7%" if index % 2 == 0 else "连续三个交易日涨幅偏离值累计达到20%", "institutions": institutions, } ) return { "meta": { "trade_date": f"{trade_date[:4]}-{trade_date[4:6]}-{trade_date[6:8]}", "source": "demo", "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "notice": notice or "龙虎榜当前展示演示数据。", }, "summary": { "stock_count": len(rows), "institution_count": sum(len(row["institutions"]) for row in rows), "net_buy_million": round(sum(row["net_buy_million"] for row in rows), 2), "positive_count": sum(row["net_buy_million"] > 0 for row in rows), }, "rows": rows, } def build_demo_stock_detail( code: str, trade_date: str, name: str = "示例股票", industry: str = "其他", notice: str = "", ) -> dict[str, Any]: end = datetime.strptime(trade_date, "%Y%m%d") seed = sum(ord(character) for character in code) base = 8 + seed % 45 prices = [] close = float(base) for index in range(90): day = end - timedelta(days=(89 - index)) drift = math.sin((index + seed) / 6) * 0.018 + 0.002 open_price = close * (1 + math.sin(index * 1.7) * 0.006) close = max(1, close * (1 + drift)) high = max(open_price, close) * (1.012 + (index % 3) * 0.002) low = min(open_price, close) * (0.988 - (index % 2) * 0.002) prices.append( { "trade_date": day.strftime("%Y-%m-%d"), "open": round(open_price, 2), "high": round(high, 2), "low": round(low, 2), "close": round(close, 2), "change": round((close / open_price - 1) * 100, 2), "volume": 180000 + (index % 11) * 26000 + seed * 10, "amount_billion": round(1.8 + (index % 9) * 0.36, 2), } ) return { "meta": { "trade_date": f"{trade_date[:4]}-{trade_date[4:6]}-{trade_date[6:8]}", "source": "demo", "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "notice": notice or "个股详情当前展示演示数据。", }, "stock": { "code": code, "ts_code": code + (".SH" if code.startswith("6") else ".SZ"), "name": name, "industry": industry, "area": "--", "market": "主板", "list_date": "--", "price": prices[-1]["close"], "change": prices[-1]["change"], }, "prices": prices, "moneyflow": { "net_million": 18.62, "large_million": 31.48, "medium_million": -4.12, "small_million": -8.74, }, }