575 lines
23 KiB
Python
575 lines
23 KiB
Python
"""AkShare 数据服务层。
|
||
|
||
每个函数都做了 try/except 降级:真实数据拿不到时返回 Python 端生成的模拟数据,
|
||
并通过 `source` 字段标注来源(akshare / mock),保证前端任何情况下都有数据可渲染。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import random
|
||
import datetime as dt
|
||
from functools import wraps
|
||
|
||
from cachetools import TTLCache
|
||
|
||
import requests
|
||
from redis_cache import cache
|
||
|
||
try:
|
||
import akshare as ak
|
||
AK_OK = True
|
||
except Exception: # akshare 未安装也能跑(全部走 mock)
|
||
ak = None
|
||
AK_OK = False
|
||
|
||
# ---- 简单 TTL 缓存(按函数+参数) ----
|
||
_cache = TTLCache(maxsize=256, ttl=30)
|
||
|
||
|
||
def cached(ttl: int):
|
||
"""缓存装饰器:优先使用 Redis,降级到内存缓存"""
|
||
def deco(fn):
|
||
local = TTLCache(maxsize=64, ttl=ttl)
|
||
|
||
@wraps(fn)
|
||
def wrapper(*args, **kwargs):
|
||
# 生成缓存键
|
||
key = f"akshare:{fn.__name__}:{args}:{tuple(sorted(kwargs.items()))}"
|
||
|
||
# 优先从 Redis 读取
|
||
if cache.enabled:
|
||
cached_value = cache.get(key)
|
||
if cached_value is not None:
|
||
return cached_value
|
||
|
||
# Redis 未命中,从内存缓存读取
|
||
local_key = (fn.__name__, args, tuple(sorted(kwargs.items())))
|
||
if local_key in local:
|
||
return local[local_key]
|
||
|
||
# 执行函数
|
||
val = fn(*args, **kwargs)
|
||
|
||
# 写入 Redis
|
||
if cache.enabled:
|
||
cache.set(key, val, expire=ttl)
|
||
|
||
# 写入内存缓存(降级)
|
||
local[local_key] = val
|
||
return val
|
||
|
||
return wrapper
|
||
|
||
return deco
|
||
|
||
|
||
def _rnd(a, b):
|
||
return round(random.uniform(a, b), 2)
|
||
|
||
|
||
# ============================================================
|
||
# 指数
|
||
# ============================================================
|
||
MAJOR_INDEX = {
|
||
"sh000001": ("上证指数", 3210),
|
||
"sz399001": ("深证成指", 10180),
|
||
"sz399006": ("创业板指", 2105),
|
||
"sh000300": ("沪深300", 3760),
|
||
"bj899050": ("北证50", 1080),
|
||
}
|
||
|
||
|
||
@cached(10)
|
||
def get_indices():
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_zh_index_spot_sina()
|
||
rows = []
|
||
for code, (name, _base) in MAJOR_INDEX.items():
|
||
r = df[df["代码"] == code]
|
||
if r.empty:
|
||
continue
|
||
r = r.iloc[0]
|
||
rows.append({
|
||
"code": code, "name": name,
|
||
"price": float(r["最新价"]),
|
||
"change": float(r["涨跌额"]),
|
||
"pct": float(r["涨跌幅"]),
|
||
})
|
||
if rows:
|
||
return {"source": "akshare", "list": rows}
|
||
except Exception as e: # noqa
|
||
pass
|
||
# mock
|
||
rows = []
|
||
for code, (name, base) in MAJOR_INDEX.items():
|
||
pct = _rnd(-2.5, 2.5)
|
||
price = round(base * (1 + pct / 100), 2)
|
||
rows.append({"code": code, "name": name, "price": price,
|
||
"change": round(price - base, 2), "pct": pct})
|
||
return {"source": "mock", "list": rows}
|
||
|
||
|
||
# ============================================================
|
||
# K线
|
||
# ============================================================
|
||
# ============================================================
|
||
# 实时报价(新浪 hq,速度快且稳定,用于盯盘预警)
|
||
# ============================================================
|
||
def realtime_quotes(codes):
|
||
"""返回 {code: {name, price, prev_close, pct, open, high, low}}。失败返回 {}。"""
|
||
if not codes:
|
||
return {}
|
||
syms = ",".join(_sina_symbol(c) for c in codes)
|
||
try:
|
||
r = requests.get("https://hq.sinajs.cn/list=" + syms,
|
||
headers={"Referer": "https://finance.sina.com.cn"}, timeout=6)
|
||
out = {}
|
||
for line in r.text.split(";\n"):
|
||
if "hq_str_" not in line or '="' not in line:
|
||
continue
|
||
head, body = line.split('="', 1)
|
||
sym = head.split("hq_str_")[1].strip()
|
||
code = sym[2:]
|
||
f = body.strip('"').split(",")
|
||
if len(f) < 6 or not f[3]:
|
||
continue
|
||
price = float(f[3]); prev = float(f[2]) if f[2] else 0.0
|
||
out[code] = {"name": f[0], "open": float(f[1] or 0), "prev_close": prev,
|
||
"price": price, "high": float(f[4] or 0), "low": float(f[5] or 0),
|
||
"pct": round((price - prev) / prev * 100, 2) if prev else 0.0}
|
||
return out
|
||
except Exception:
|
||
return {}
|
||
|
||
|
||
# ============================================================
|
||
# 资讯新闻
|
||
# ============================================================
|
||
_BULL = ["涨停", "利好", "增长", "大涨", "突破", "中标", "签约", "回购", "增持", "扭亏",
|
||
"超预期", "新高", "提价", "涨价", "订单", "合作", "获批", "盈利", "分红", "重组",
|
||
"并购", "补贴", "减税", "降准", "降息", "刺激", "国产替代", "放量", "净流入"]
|
||
_BEAR = ["跌停", "利空", "下滑", "大跌", "亏损", "减持", "处罚", "退市", "违规", "下调",
|
||
"不及预期", "新低", "停牌", "质押", "爆雷", "诉讼", "解禁", "商誉", "预亏", "降价",
|
||
"裁员", "债务", "暴跌", "净流出", "风险警示"]
|
||
|
||
|
||
def judge_sentiment(text: str):
|
||
t = text or ""
|
||
pos = [w for w in _BULL if w in t]
|
||
neg = [w for w in _BEAR if w in t]
|
||
if len(pos) > len(neg):
|
||
return "利好", pos[:4]
|
||
if len(neg) > len(pos):
|
||
return "利空", neg[:4]
|
||
return "中性", (pos or neg)[:4]
|
||
|
||
|
||
@cached(120)
|
||
def get_news(limit: int = 40):
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_info_global_em()
|
||
rows = []
|
||
for _, r in df.head(limit).iterrows():
|
||
title = str(r["标题"]); summary = str(r.get("摘要", ""))
|
||
senti, kw = judge_sentiment(title + summary)
|
||
rows.append({"time": str(r["发布时间"]), "title": title, "summary": summary,
|
||
"url": str(r.get("链接", "")), "sentiment": senti, "keywords": kw})
|
||
if rows:
|
||
return {"source": "akshare", "list": rows}
|
||
except Exception:
|
||
pass
|
||
return {"source": "mock", "list": [
|
||
{"time": "—", "title": "示例资讯:市场情绪回暖,多板块走强", "summary": "(演示数据)",
|
||
"sentiment": "利好", "keywords": ["利好"], "url": ""}]}
|
||
|
||
|
||
@cached(180)
|
||
def get_stock_news(code: str, limit: int = 12):
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_news_em(symbol=code)
|
||
rows = []
|
||
for _, r in df.head(limit).iterrows():
|
||
title = str(r["新闻标题"]); content = str(r.get("新闻内容", ""))
|
||
senti, kw = judge_sentiment(title + content)
|
||
rows.append({"time": str(r["发布时间"]), "title": title,
|
||
"summary": content[:120], "source": str(r.get("文章来源", "")),
|
||
"url": str(r.get("新闻链接", "")), "sentiment": senti, "keywords": kw})
|
||
if rows:
|
||
return {"source": "akshare", "list": rows}
|
||
except Exception:
|
||
pass
|
||
return {"source": "mock", "list": []}
|
||
|
||
|
||
# 已知指数代码 → 新浪前缀映射
|
||
_INDEX_CODES = {"000001", "000300", "000016", "399001", "399006", "899050"}
|
||
|
||
def _is_index(code: str) -> bool:
|
||
return code in _INDEX_CODES or code.startswith(("sh0", "sz3990", "bj8990"))
|
||
|
||
def _sina_symbol(code: str) -> str:
|
||
if code in ("000001", "000016"): # 上证系列
|
||
return "sh" + code
|
||
if code in ("000300",): # 沪深300
|
||
return "sh" + code
|
||
if code in ("399001", "399006"): # 深证
|
||
return "sz" + code
|
||
if code.startswith("6"):
|
||
return "sh" + code
|
||
if code.startswith(("0", "3")):
|
||
return "sz" + code
|
||
if code.startswith(("8", "4")):
|
||
return "bj" + code
|
||
return "sh" + code
|
||
|
||
|
||
@cached(60)
|
||
def get_kline(symbol: str = "000001", days: int = 120):
|
||
if AK_OK:
|
||
# 指数走专用接口
|
||
if symbol in _INDEX_CODES:
|
||
try:
|
||
sym = _sina_symbol(symbol)
|
||
df = ak.stock_zh_index_daily(symbol=sym)
|
||
if df is not None and not df.empty:
|
||
df = df.tail(days)
|
||
dates = [str(d)[5:].replace("-", "/") for d in df["date"]]
|
||
ohlc = [[float(r["open"]), float(r["close"]), float(r["low"]), float(r["high"])]
|
||
for _, r in df.iterrows()]
|
||
vols = [int(r["volume"]) if "volume" in df.columns else 0 for _, r in df.iterrows()]
|
||
return {"source": "akshare", "symbol": symbol, "dates": dates, "ohlc": ohlc, "vols": vols}
|
||
except Exception:
|
||
pass
|
||
# 个股主源:新浪日线(更稳定);备源:腾讯
|
||
for src in ("sina", "tx"):
|
||
try:
|
||
sym = _sina_symbol(symbol)
|
||
if src == "sina":
|
||
df = ak.stock_zh_a_daily(symbol=sym, adjust="qfq")
|
||
else:
|
||
df = ak.stock_zh_a_hist_tx(symbol=sym)
|
||
if df is not None and not df.empty:
|
||
df = df.tail(days)
|
||
dates = [str(d)[5:].replace("-", "/") for d in df["date"]]
|
||
ohlc = [[float(o), float(c), float(l), float(h)] for o, c, l, h in
|
||
zip(df["open"], df["close"], df["low"], df["high"])]
|
||
vols = [int(v) for v in (df["volume"] if "volume" in df.columns else df["amount"])]
|
||
return {"source": "akshare", "symbol": symbol, "dates": dates, "ohlc": ohlc, "vols": vols}
|
||
except Exception:
|
||
continue
|
||
# mock
|
||
dates, ohlc, vols = [], [], []
|
||
price = 1680.0
|
||
today = dt.date.today()
|
||
for i in range(days, 0, -1):
|
||
d = today - dt.timedelta(days=i)
|
||
dates.append(f"{d.month}/{d.day}")
|
||
o = price
|
||
c = round(o + _rnd(-o * 0.03, o * 0.03), 2)
|
||
h = round(max(o, c) + _rnd(0, o * 0.02), 2)
|
||
l = round(min(o, c) - _rnd(0, o * 0.02), 2)
|
||
ohlc.append([o, c, l, h])
|
||
vols.append(int(_rnd(2, 9) * 1e6))
|
||
price = c
|
||
return {"source": "mock", "symbol": symbol, "dates": dates, "ohlc": ohlc, "vols": vols}
|
||
|
||
|
||
# ============================================================
|
||
# 行业板块(云图 / 热门板块复用)—— 新浪行业(东财push2在部分网络被封)
|
||
# ============================================================
|
||
@cached(60)
|
||
def get_industry_boards():
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_sector_spot(indicator="新浪行业")
|
||
rows = []
|
||
for _, r in df.iterrows():
|
||
rows.append({
|
||
"name": str(r["板块"]),
|
||
"pct": float(r["涨跌幅"]),
|
||
"amount": round(float(r["总成交额"]) / 1e8, 1), # 亿
|
||
"count": int(r.get("公司家数", 0) or 0),
|
||
"leader": str(r.get("股票名称", "")),
|
||
})
|
||
if rows:
|
||
rows.sort(key=lambda x: x["pct"], reverse=True)
|
||
return {"source": "akshare", "list": rows}
|
||
except Exception:
|
||
pass
|
||
sectors = ["半导体", "新能源", "医药生物", "白酒食品", "军工", "证券", "银行",
|
||
"房地产", "人工智能", "消费电子", "光伏", "汽车", "有色金属", "煤炭", "传媒"]
|
||
return {"source": "mock", "list": [
|
||
{"name": s, "pct": _rnd(-3, 6), "amount": _rnd(50, 500),
|
||
"count": int(_rnd(10, 80)), "leader": "龙头股"} for s in sectors]}
|
||
|
||
|
||
# ============================================================
|
||
# 全市场快照(情绪 / 全市场云图)
|
||
# ============================================================
|
||
@cached(60)
|
||
def _spot():
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_zh_a_spot_em()
|
||
if df is not None and not df.empty:
|
||
return df
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
@cached(30)
|
||
def get_sentiment():
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_market_activity_legu()
|
||
m = {}
|
||
for _, r in df.iterrows():
|
||
m[str(r["item"]).strip()] = r["value"]
|
||
|
||
def num(k):
|
||
try:
|
||
return int(float(m.get(k, 0)))
|
||
except Exception:
|
||
return 0
|
||
up, down, flat = num("上涨"), num("下跌"), num("平盘")
|
||
if up or down:
|
||
return {"source": "akshare", "up": up, "down": down, "flat": flat,
|
||
"limit_up": num("涨停"), "limit_down": num("跌停"),
|
||
"height": min(9, max(3, num("涨停") // 8))}
|
||
except Exception:
|
||
pass
|
||
up, down, flat = int(_rnd(1800, 3200)), int(_rnd(1200, 2600)), int(_rnd(80, 260))
|
||
return {"source": "mock", "up": up, "down": down, "flat": flat,
|
||
"limit_up": int(_rnd(20, 90)), "limit_down": int(_rnd(2, 30)), "height": int(_rnd(4, 9))}
|
||
|
||
|
||
@cached(60)
|
||
def get_treemap(mode: str = "sector"):
|
||
if mode == "all":
|
||
df = _spot()
|
||
if df is not None:
|
||
try:
|
||
top = df.sort_values("成交额", ascending=False).head(150)
|
||
items = [{"name": str(r["名称"]), "value": round(float(r["成交额"]) / 1e8, 2),
|
||
"pct": float(r["涨跌幅"])} for _, r in top.iterrows()]
|
||
return {"source": "akshare", "mode": "all", "items": items}
|
||
except Exception:
|
||
pass
|
||
# mock flat
|
||
items = [{"name": f"个股{i}", "value": _rnd(2, 50), "pct": _rnd(-9, 9)} for i in range(60)]
|
||
return {"source": "mock", "mode": "all", "items": items}
|
||
# sector
|
||
boards = get_industry_boards()
|
||
items = [{"name": b["name"], "value": b.get("amount", 1), "pct": b["pct"]} for b in boards["list"]]
|
||
return {"source": boards["source"], "mode": "sector", "items": items}
|
||
|
||
|
||
@cached(120)
|
||
def get_us_treemap():
|
||
"""美股热门板块云图(按成交额取前100只)"""
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_us_spot_em()
|
||
if df is not None and not df.empty:
|
||
top = df.sort_values("成交额", ascending=False).head(100)
|
||
items = [{"name": str(r.get("名称","")), "value": round(float(r.get("成交额",0))/1e8, 2),
|
||
"pct": round(float(r.get("涨跌幅",0)), 2)} for _, r in top.iterrows()]
|
||
items = [x for x in items if x["name"]]
|
||
return {"source": "akshare", "market": "us", "items": items}
|
||
except Exception:
|
||
pass
|
||
names = ["苹果","微软","谷歌","亚马逊","英伟达","特斯拉","Meta","台积电","巴菲特","摩根"]
|
||
return {"source": "mock", "market": "us",
|
||
"items": [{"name": n, "value": _rnd(10,200), "pct": round(_rnd(-4,4),2)} for n in names]}
|
||
|
||
|
||
@cached(120)
|
||
def get_hk_treemap():
|
||
"""港股热门板块云图(按成交额取前100只)"""
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_hk_spot_em()
|
||
if df is not None and not df.empty:
|
||
top = df.sort_values("成交额", ascending=False).head(100)
|
||
items = [{"name": str(r.get("名称","")), "value": round(float(r.get("成交额",0))/1e4, 2),
|
||
"pct": round(float(r.get("涨跌幅",0)), 2)} for _, r in top.iterrows()]
|
||
items = [x for x in items if x["name"]]
|
||
return {"source": "akshare", "market": "hk", "items": items}
|
||
except Exception:
|
||
pass
|
||
names = ["腾讯","阿里巴巴","美团","京东","小米","百度","网易","中国平安","汇丰","友邦"]
|
||
return {"source": "mock", "market": "hk",
|
||
"items": [{"name": n, "value": _rnd(5,100), "pct": round(_rnd(-4,4),2)} for n in names]}
|
||
|
||
|
||
@cached(120)
|
||
def get_all_sector_leaders(top_n: int = 5):
|
||
"""一次性获取所有板块的前N只龙头股"""
|
||
boards = get_industry_boards()
|
||
result = {}
|
||
for b in boards.get("list", []):
|
||
name = b["name"]
|
||
try:
|
||
r = get_sector_stocks(name, top_n + 1)
|
||
result[name] = r.get("stocks", [])[:top_n]
|
||
except Exception:
|
||
result[name] = []
|
||
return {"source": "akshare", "sectors": result}
|
||
|
||
|
||
@cached(300)
|
||
def get_sector_stocks(sector_name: str, limit: int = 20):
|
||
"""获取板块成分股,按成交额排序"""
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_board_industry_cons_em(symbol=sector_name)
|
||
if df is not None and not df.empty:
|
||
if "成交额" in df.columns:
|
||
df = df.sort_values("成交额", ascending=False)
|
||
stocks = []
|
||
for _, r in df.head(limit).iterrows():
|
||
try:
|
||
stocks.append({
|
||
"code": str(r.get("代码", "")),
|
||
"name": str(r.get("名称", "")),
|
||
"pct": round(float(r.get("涨跌幅", 0)), 2),
|
||
"price": round(float(r.get("最新价", 0)), 2),
|
||
"amount": round(float(r.get("成交额", 0)) / 1e8, 2),
|
||
})
|
||
except Exception:
|
||
continue
|
||
return {"source": "akshare", "name": sector_name, "stocks": stocks}
|
||
except Exception:
|
||
pass
|
||
# mock
|
||
stocks = [{"code": f"60000{i}", "name": f"{sector_name}{i+1}",
|
||
"pct": round(_rnd(-5, 5), 2), "price": round(_rnd(5, 100), 2), "amount": round(_rnd(1, 50), 2)}
|
||
for i in range(10)]
|
||
return {"source": "mock", "name": sector_name, "stocks": stocks}
|
||
|
||
|
||
# ============================================================
|
||
# 资金流向(行业)
|
||
# ============================================================
|
||
@cached(60)
|
||
def get_fund_flow():
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_fund_flow_industry(symbol="即时")
|
||
rows = []
|
||
for _, r in df.iterrows():
|
||
rows.append({"name": str(r["行业"]),
|
||
"net": round(float(r["净额"]), 2), # 同花顺已是亿元
|
||
"pct": float(r["行业-涨跌幅"])})
|
||
if rows:
|
||
rows.sort(key=lambda x: x["net"])
|
||
# 取首尾各15条,突出流入流出两端
|
||
show = rows[:15] + rows[-15:] if len(rows) > 30 else rows
|
||
show.sort(key=lambda x: x["net"])
|
||
return {"source": "akshare", "list": show}
|
||
except Exception:
|
||
pass
|
||
sectors = ["半导体", "新能源", "医药生物", "白酒食品", "军工", "证券", "银行",
|
||
"房地产", "人工智能", "消费电子", "光伏", "汽车", "有色金属", "煤炭", "传媒"]
|
||
rows = [{"name": s, "net": _rnd(-40, 60), "pct": _rnd(-3, 6)} for s in sectors]
|
||
rows.sort(key=lambda x: x["net"])
|
||
return {"source": "mock", "list": rows}
|
||
|
||
|
||
# ============================================================
|
||
# 热门股票(人气榜)
|
||
# ============================================================
|
||
@cached(60)
|
||
def get_hot_stocks():
|
||
if AK_OK:
|
||
try:
|
||
df = ak.stock_hot_rank_em()
|
||
rows = []
|
||
for _, r in df.head(20).iterrows():
|
||
rows.append({"rank": int(r["当前排名"]), "code": str(r["代码"]),
|
||
"name": str(r["股票名称"]), "price": float(r["最新价"]),
|
||
"pct": float(r["涨跌幅"])})
|
||
if rows:
|
||
return {"source": "akshare", "list": rows}
|
||
except Exception:
|
||
pass
|
||
pool = ["龙头A", "龙头B", "中军C", "黑马D", "次新E", "蓝筹F", "题材G", "妖股H"]
|
||
return {"source": "mock", "list": [
|
||
{"rank": i + 1, "code": f"60{1000+i}", "name": f"{pool[i % len(pool)]}{i}",
|
||
"price": _rnd(5, 200), "pct": _rnd(-5, 11)} for i in range(15)]}
|
||
|
||
|
||
# ============================================================
|
||
# 龙虎榜
|
||
# ============================================================
|
||
@cached(300)
|
||
def get_dragon_tiger():
|
||
if AK_OK:
|
||
try:
|
||
for back in range(0, 7):
|
||
d = (dt.date.today() - dt.timedelta(days=back)).strftime("%Y%m%d")
|
||
try:
|
||
df = ak.stock_lhb_detail_em(start_date=d, end_date=d)
|
||
except Exception:
|
||
df = None
|
||
if df is not None and not df.empty:
|
||
rows = []
|
||
for _, r in df.head(20).iterrows():
|
||
rows.append({
|
||
"code": str(r.get("代码", "")), "name": str(r.get("名称", "")),
|
||
"pct": float(r.get("涨跌幅", 0) or 0),
|
||
"net": round(float(r.get("龙虎榜净买额", 0) or 0) / 1e8, 2),
|
||
"reason": str(r.get("上榜原因", "")),
|
||
})
|
||
return {"source": "akshare", "date": d, "list": rows}
|
||
except Exception:
|
||
pass
|
||
pool = ["龙头A", "龙头B", "中军C", "黑马D"]
|
||
return {"source": "mock", "date": "", "list": [
|
||
{"code": f"60{1000+i}", "name": f"{pool[i % len(pool)]}{i}", "pct": _rnd(-3, 10),
|
||
"net": _rnd(-3, 5), "reason": ["日涨幅偏离", "换手率达20%", "连续三日涨停"][i % 3]} for i in range(12)]}
|
||
|
||
|
||
# ============================================================
|
||
# 自选股 —— 代码名称表 + 个股日线(push2 被封时的稳妥方案)
|
||
# ============================================================
|
||
@cached(3600)
|
||
def _code_name_map():
|
||
if AK_OK:
|
||
try:
|
||
cn = ak.stock_info_a_code_name()
|
||
return {str(r["code"]): str(r["name"]) for _, r in cn.iterrows()}
|
||
except Exception:
|
||
pass
|
||
return {}
|
||
|
||
|
||
def get_watchlist(symbols: list[str]):
|
||
names = {"600519": "贵州茅台", "300750": "宁德时代", "002594": "比亚迪",
|
||
"688981": "中芯国际", "300059": "东方财富", "601012": "隆基绿能"}
|
||
if AK_OK:
|
||
cmap = _code_name_map()
|
||
rows = []
|
||
for s in symbols:
|
||
try:
|
||
k = get_kline(s, 30)
|
||
if k["source"] != "akshare" or len(k["ohlc"]) < 2:
|
||
continue
|
||
last, prev = k["ohlc"][-1], k["ohlc"][-2]
|
||
price, prev_close = last[1], prev[1]
|
||
change = round(price - prev_close, 2)
|
||
pct = round(change / prev_close * 100, 2) if prev_close else 0.0
|
||
rows.append({"code": s, "name": cmap.get(s, names.get(s, s)), "price": price,
|
||
"pct": pct, "change": change,
|
||
"amount": round(k["vols"][-1] * price / 1e8, 2)})
|
||
except Exception:
|
||
continue
|
||
if rows:
|
||
return {"source": "akshare", "list": rows}
|
||
return {"source": "mock", "list": [
|
||
{"code": c, "name": names.get(c, c), "price": _rnd(20, 1800), "pct": _rnd(-4, 5),
|
||
"change": _rnd(-30, 30), "amount": _rnd(3, 60)} for c in symbols]}
|