fix: fall back to latest SGE trading price
ci / gitleaks (push) Successful in 8s
ci / docker (push) Successful in 3m14s

This commit is contained in:
2026-09-20 08:33:55 +08:00
parent 8ecc3e265f
commit ed123b710f
2 changed files with 110 additions and 3 deletions
+55 -3
View File
@@ -19,11 +19,16 @@ from .lunar_support import solar_to_lunar_text
SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
WEATHER_URL = "https://api.open-meteo.com/v1/forecast"
GOLD_URL = "https://www.sge.com.cn/sjzx/yshqbg"
GOLD_HISTORY_URL = (
"https://www.sge.com.cn/sjzx/quotation_daily_new"
"?start_date={start_date}&end_date={end_date}"
)
WEATHER_SOURCE = "Open-Meteo"
GOLD_SOURCE = "上海黄金交易所"
WEEKDAYS = ("星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日")
SOURCE_TIMEOUT_SECONDS = 3.0
TOTAL_TIMEOUT_SECONDS = 3.5
GOLD_SOURCE_TIMEOUT_SECONDS = 6.0
TOTAL_TIMEOUT_SECONDS = 6.5
WEATHER_TTL = timedelta(minutes=15)
WEATHER_STALE_TTL = timedelta(hours=6)
GOLD_TTL = timedelta(minutes=5)
@@ -107,6 +112,36 @@ def parse_sge_au9999(html: str) -> tuple[Decimal, str]:
raise ValueError("Au99.99 quote not found")
def parse_sge_au9999_history(html: str) -> tuple[Decimal, str]:
rows = [[_text(cell) for cell in _CELL_RE.findall(row)] for row in _ROW_RE.findall(html)]
header = next(
(cells for cells in rows if "日期" in cells and "合约" in cells and "收盘价" in cells),
None,
)
if header is None:
raise ValueError("SGE history table header not found")
date_index = header.index("日期")
contract_index = header.index("合约")
price_index = header.index("收盘价")
required_length = max(date_index, contract_index, price_index) + 1
quotes = []
for cells in rows[rows.index(header) + 1 :]:
if len(cells) < required_length or cells[contract_index] != "Au99.99":
continue
try:
market_date = date.fromisoformat(cells[date_index]).isoformat()
price = Decimal(cells[price_index].replace(",", ""))
except (InvalidOperation, ValueError):
continue
if not price.is_finite() or price <= 0:
continue
quotes.append((market_date, price))
if not quotes:
raise ValueError("Au99.99 history quote not found")
market_date, price = max(quotes)
return price, market_date
async def fetch_weather(request: Callable[..., Awaitable[dict[str, Any]]]) -> dict[str, Any]:
payload = await request(
WEATHER_URL,
@@ -158,7 +193,22 @@ async def fetch_weather(request: Callable[..., Awaitable[dict[str, Any]]]) -> di
async def fetch_gold(request: Callable[..., Awaitable[str]]) -> dict[str, Any]:
price, market_date = parse_sge_au9999(await request(GOLD_URL))
html = await request(GOLD_URL)
try:
price, market_date = parse_sge_au9999(html)
except ValueError:
date_match = _DATE_RE.search(_text(html))
if date_match is None:
raise
try:
quote_date = date(*map(int, date_match.groups()))
except ValueError as exc:
raise ValueError("invalid SGE market date") from exc
start_date = quote_date - timedelta(days=31)
history_url = GOLD_HISTORY_URL.format(
start_date=start_date.isoformat(), end_date=quote_date.isoformat()
)
price, market_date = parse_sge_au9999_history(await request(history_url))
return {
"contract": "Au99.99",
"latest_price": price,
@@ -192,7 +242,9 @@ async def _refresh_source(
fetched_at: datetime,
generation: int,
) -> dict[str, Any]:
value = await asyncio.wait_for(fetcher(), timeout=SOURCE_TIMEOUT_SECONDS)
value = await asyncio.wait_for(
fetcher(), timeout=GOLD_SOURCE_TIMEOUT_SECONDS if name == "gold" else SOURCE_TIMEOUT_SECONDS
)
with _state_guard:
if generation == _generation:
newest = _cache.get(name)