407 lines
17 KiB
Python
407 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
import math
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
from backend.features.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,
|
|
},
|
|
}
|