fix(HEL-494): 日K默认45根并修复问天行业0/205覆盖
悬浮窗和详情页只画最近45个交易日,中枢仍保留250日历史。盘后缺sw_daily时保留成分日线内核,外显走免费申万;成分行情改为全市场快照+分页,不再截成前60只。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
3e828b346c
commit
b5d65ecb41
@@ -126,7 +126,7 @@ class EastmoneyAdapter(MarketAdapter):
|
||||
return result
|
||||
|
||||
def fetch_quotes(self, codes: list[str]) -> list[dict[str, Any]]:
|
||||
# Eastmoney clist does not accept arbitrary code lists well; use ulist.np for batches.
|
||||
# Eastmoney ulist.np accepts ~60 secids per request; page remaining codes.
|
||||
secids = []
|
||||
for code in codes:
|
||||
ts = str(code or "").upper()
|
||||
@@ -137,47 +137,59 @@ class EastmoneyAdapter(MarketAdapter):
|
||||
secids.append(f"0.{symbol}")
|
||||
if not secids:
|
||||
return []
|
||||
payload = self._get_json(
|
||||
EASTMONEY_INDEX_URL,
|
||||
{
|
||||
"secids": ",".join(secids[:60]),
|
||||
"fltt": "2",
|
||||
"invt": "2",
|
||||
"fields": "f12,f14,f2,f3,f4,f15,f16,f17,f18,f5,f6,f8,f124",
|
||||
},
|
||||
referer="https://quote.eastmoney.com/",
|
||||
)
|
||||
rows = list((payload.get("data") or {}).get("diff") or [])
|
||||
result = []
|
||||
for row in rows:
|
||||
symbol = str(row.get("f12") or "")
|
||||
if not symbol:
|
||||
continue
|
||||
ts_code = f"{symbol}.SH" if symbol.startswith(("5", "6", "9")) else f"{symbol}.SZ"
|
||||
epoch = int(finite_number(row.get("f124")) or 0)
|
||||
result.append(
|
||||
result: list[dict[str, Any]] = []
|
||||
for index in range(0, len(secids), 60):
|
||||
payload = self._get_json(
|
||||
EASTMONEY_INDEX_URL,
|
||||
{
|
||||
"ts_code": ts_code,
|
||||
"name": row.get("f14") or symbol,
|
||||
"price": round4(finite_number(row.get("f2"))),
|
||||
"pct_chg": round4(finite_number(row.get("f3"))),
|
||||
"change_amount": round4(finite_number(row.get("f4"))),
|
||||
"open": round4(finite_number(row.get("f17"))),
|
||||
"high": round4(finite_number(row.get("f15"))),
|
||||
"low": round4(finite_number(row.get("f16"))),
|
||||
"previous_close": round4(finite_number(row.get("f18"))),
|
||||
"volume": round4(finite_number(row.get("f5"))),
|
||||
"amount": round4(finite_number(row.get("f6"))),
|
||||
"turnover_rate": round4(finite_number(row.get("f8"))),
|
||||
"quote_time_epoch": epoch,
|
||||
"quote_time": (
|
||||
datetime.fromtimestamp(epoch).astimezone().isoformat(timespec="seconds")
|
||||
if epoch
|
||||
else ""
|
||||
),
|
||||
"source": "eastmoney_push2",
|
||||
}
|
||||
"secids": ",".join(secids[index:index + 60]),
|
||||
"fltt": "2",
|
||||
"invt": "2",
|
||||
"fields": "f12,f14,f2,f3,f4,f15,f16,f17,f18,f5,f6,f8,f124",
|
||||
},
|
||||
referer="https://quote.eastmoney.com/",
|
||||
)
|
||||
rows = list((payload.get("data") or {}).get("diff") or [])
|
||||
for row in rows:
|
||||
symbol = str(row.get("f12") or "")
|
||||
if not symbol:
|
||||
continue
|
||||
ts_code = f"{symbol}.SH" if symbol.startswith(("5", "6", "9")) else f"{symbol}.SZ"
|
||||
epoch = int(finite_number(row.get("f124")) or 0)
|
||||
close = round4(finite_number(row.get("f2")))
|
||||
previous = round4(finite_number(row.get("f18")))
|
||||
quote_date = (
|
||||
datetime.fromtimestamp(epoch).astimezone().strftime("%Y%m%d")
|
||||
if epoch
|
||||
else ""
|
||||
)
|
||||
result.append(
|
||||
{
|
||||
"ts_code": ts_code,
|
||||
"name": row.get("f14") or symbol,
|
||||
"price": close,
|
||||
"close": close,
|
||||
"pct_chg": round4(finite_number(row.get("f3"))),
|
||||
"change_amount": round4(finite_number(row.get("f4"))),
|
||||
"open": round4(finite_number(row.get("f17"))),
|
||||
"high": round4(finite_number(row.get("f15"))),
|
||||
"low": round4(finite_number(row.get("f16"))),
|
||||
"pre_close": previous,
|
||||
"previous_close": previous,
|
||||
"volume": round4(finite_number(row.get("f5"))),
|
||||
"vol": round4(finite_number(row.get("f5")) * 100),
|
||||
"amount": round4(finite_number(row.get("f6"))),
|
||||
"turnover_rate": round4(finite_number(row.get("f8"))),
|
||||
"quote_date": quote_date,
|
||||
"quote_time_epoch": epoch,
|
||||
"quote_time": (
|
||||
datetime.fromtimestamp(epoch).astimezone().isoformat(timespec="seconds")
|
||||
if epoch
|
||||
else ""
|
||||
),
|
||||
"source": "eastmoney_push2",
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def fetch_market_quotes(self) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -5,6 +5,7 @@ Free sources only. Never writes official eod_* tables. Uses rt_cache + LKG.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
@@ -20,6 +21,7 @@ from datahub.timeutil import isoformat, now_shanghai, yyyymmdd
|
||||
QUOTE_TTL = 60
|
||||
INDEX_TTL = 60
|
||||
INTRADAY_TTL = 20
|
||||
QUOTE_BATCH = 60
|
||||
|
||||
|
||||
class RealtimeApiError(RuntimeError):
|
||||
@@ -95,20 +97,25 @@ def fetch_quotes(db: HubDB, codes: list[str]) -> dict[str, Any]:
|
||||
if not codes:
|
||||
return fetch_market_quotes(db)
|
||||
resolved: list[str] = []
|
||||
for code in codes[:60]:
|
||||
seen: set[str] = set()
|
||||
for code in codes:
|
||||
item = resolve_code(db, code) or _guess_ts_code(code)
|
||||
if item:
|
||||
if item and item not in seen:
|
||||
seen.add(item)
|
||||
resolved.append(item)
|
||||
if not resolved:
|
||||
raise RealtimeApiError("INVALID_ARGUMENT", "no resolvable codes")
|
||||
cache_key = "quotes:" + ",".join(sorted(resolved))
|
||||
digest = hashlib.sha1(",".join(sorted(resolved)).encode("utf-8")).hexdigest()
|
||||
cache_key = f"quotes:{digest}:{len(resolved)}"
|
||||
cached = _read_cache(db, cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
adapter = EastmoneyAdapter()
|
||||
try:
|
||||
rows = adapter.fetch_quotes(resolved)
|
||||
source = "eastmoney:clist"
|
||||
rows: list[dict[str, Any]] = []
|
||||
for index in range(0, len(resolved), QUOTE_BATCH):
|
||||
rows.extend(adapter.fetch_quotes(resolved[index:index + QUOTE_BATCH]))
|
||||
source = "eastmoney:ulist"
|
||||
except Exception as exc:
|
||||
raise RealtimeApiError("SOURCE_UNAVAILABLE", f"quotes unavailable: {exc}") from exc
|
||||
payload = _envelope(
|
||||
|
||||
@@ -110,7 +110,7 @@ class Scheduler:
|
||||
("eod_b", time(15, 10)),
|
||||
("eod_c", time(16, 40)),
|
||||
("eod_d", time(16, 45)),
|
||||
("eod_e", time(18, 5)),
|
||||
("eod_e", time(15, 20)),
|
||||
("eod_f", time(22, 40)),
|
||||
("cleanup", time(0, 30)),
|
||||
("backup", time(0, 40)),
|
||||
@@ -124,6 +124,9 @@ class Scheduler:
|
||||
key = (job_id, day, at.strftime("%H%M"))
|
||||
if key in self._fired:
|
||||
continue
|
||||
if job_id not in self.jobs:
|
||||
self._fired.add(key)
|
||||
continue
|
||||
if job_id in {"eod_a", "eod_b", "eod_c", "eod_d", "eod_e", "eod_f", "stocks_refresh"} and not open_day:
|
||||
self._fired.add(key)
|
||||
continue
|
||||
@@ -203,6 +206,12 @@ class Scheduler:
|
||||
LOGGER.warning("eod retry failed for %s", day, exc_info=True)
|
||||
ran.append("eod_retry")
|
||||
self._settle_eod(day)
|
||||
if "eod_e" in self.jobs and not self.pipeline.active_batch("sector_daily", day):
|
||||
try:
|
||||
self.run_job("eod_e", day)
|
||||
ran.append("eod_e")
|
||||
except Exception:
|
||||
LOGGER.exception("sector_daily retry failed for %s", day)
|
||||
return ran
|
||||
|
||||
def _settle_eod(self, day: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user