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:
总工
2026-09-08 17:00:01 +08:00
co-authored by Cursor multica-agent
parent 3e828b346c
commit b5d65ecb41
17 changed files with 343 additions and 127 deletions
+52 -40
View File
@@ -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]]:
+12 -5
View File
@@ -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(
+10 -1
View File
@@ -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:
@@ -219,6 +219,18 @@ class MarketQuotesTests(unittest.TestCase):
payload = self.api.handle("/v1/quotes/latest", {"codes": ["600000.SH"]})
mocked.return_value.fetch_market_quotes.assert_not_called()
self.assertEqual(payload["data"][0]["ts_code"], "600000.SH")
self.assertEqual(payload["meta"]["source"], "eastmoney:ulist")
def test_named_quotes_page_beyond_sixty_codes(self) -> None:
codes = [f"{index:06d}.SZ" for index in range(70)]
def fake_fetch(chunk):
return [{"ts_code": code, "close": 10, "pre_close": 9} for code in chunk]
with patch("datahub.realtime_serve.EastmoneyAdapter") as mocked:
mocked.return_value.fetch_quotes.side_effect = fake_fetch
payload = self.api.handle("/v1/quotes/latest", {"codes": [",".join(codes)]})
self.assertEqual(mocked.return_value.fetch_quotes.call_count, 2)
self.assertEqual(len(payload["data"]), 70)
def test_market_unavailable_stays_source_error(self) -> None:
with patch("datahub.realtime_serve.EastmoneyAdapter") as mocked: