Files
group_fqcd_jr/tools/sync_market_prices.py
T
lzf_0626 87a753f7b3 feat(sync): 给 fin_market_price 补真实行情同步链路(下单前置)
## 解决的问题

全流程验收时发现:**20 只产品里只有 2 只有行情,其余下单直接 503**;而且行情一旦过期
就**没有任何机制刷新**。根因是这张表此前**只有演示种子脚本写**,而它是下单的硬前置
(TradeService 要求 close_price>0、total_fund_shares>0、source_updated_at 在 MAX_QUOTE_AGE 内)。

## 数据源(踩了两个坑才选对)

1) **东财 push2 / push2his 两个行情域名在本环境一律连不上**
   (RemoteProtocolError: Server disconnected),而它的 api.fund(净值)与
   fundf10(概况/费率)**正常** —— 即东财只有"行情类"接口不可达。
   一开始按 K 线方案写完,20 只全失败;排查时还被我自己的 except 吞掉过异常。
2) 一度怀疑被限流,等 90 秒仍失败;做源可用性对照才确认是**域名级不可达**。

最终选**腾讯行情**(qt.gtimg.cn):一次请求可带多只,给今开/最高/最低/现价/
成交量(手)/成交额(万元)/**总市值(亿元)**,正好够写一行日行情。
K 线能力仍保留在适配器里(别的网络环境可能可达),注释写明本环境不可用。

## 实现

- EastmoneyFundAdapter.fetch_tencent_quotes:解析腾讯的位置约定格式,只取语义明确的
  字段并对价格做合理性校验;成交额万元→元、成交量按"手"(与东财 K 线口径一致,实测同为 9666652)。
- MarketPriceSyncService(新):编排 + 落库。总份额按
  「已有值 → 腾讯总市值推算 → 季度规模兜底」的优先级确定,source 标明是否含推算成分;
  三者都拿不到就**跳过该产品**,不编份额。
- 	ools/sync_market_prices.py(新):CLI,演示/验收前跑一次。

## 另一个坑:非交易日的假行情行

种子脚本用 	rade_date = date.today() 写行情,而 2026-09-13 是**周六**、真实行情时间是
09-11 16:14。TradeService 取 order_by(trade_date.desc()).limit(1),于是那两行假的"今天"
永远排在真实行情前面——表现是"同步成功了、下单还说行情过期"。已删除那两行。

## 验证

- 同步:请求 20 只、落库 19 行(160129 腾讯源没有该代码)
- 下单:510300 @4.579、510500 @7.611、159948 @3.693、511810 @100.012 全部 201 已成交
  —— 用的是**真实行情价**,不再是种子编的 4.5/6.2
- 风控处置闭环:新建 ALDEMO0003 后确认接收→进入调查→到达终态,
  ack_status=已确认、handler_id=9002

## 顺带发现的疑点(未改,留给风控线确认)

ALDEMO0003 到达终态时 status='已关闭' 但 **closed_at 与 handle_result 都是 NULL**;
对照 ALDEMO0001(已排除)则 closed_at 有值。结案时间是否应该写入,需业务侧确认。

门禁:ruff 通过 / mypy 250 文件 0 错 / 单元+契约 1380 passed 2 skipped。
2026-09-13 21:14:13 +08:00

71 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""把真实场内日行情同步进 `fin_market_price`(下单的硬前置)。
## 什么时候要跑
`fin_market_price` 是下单的硬前置:`TradeService` 要求产品在这张表里有
`close_price > 0`、`total_fund_shares > 0`,且 `source_updated_at` 落在 `MAX_QUOTE_AGE` 内。
**行情过期后没有任何自动刷新机制**,表现是下单全线 `503 FUND_QUOTE_UNAVAILABLE`,
而错误信息只说"行情已过期",看不出根因。
所以:**每次演示/验收之前跑一次本脚本**。它同时解决两件事:
· 把只有 2 只产品的行情补齐到全部场内产品(`seed_sim_account_demo` 只写 2 只);
· 把已有行情的时间戳刷新到当前,让新鲜度校验通过。
## 数据源
腾讯行情(`qt.gtimg.cn`)—— 本环境唯一可用的行情源:东财的 push2 / push2his
两个行情域名实测一律连接被拒(`Server disconnected`),而它的净值/概况域名正常。
总份额按"已有值 → 腾讯总市值推算 → 季度规模兜底"的优先级确定,详见
`app/service/market_price_sync_service.py`。
## 用法
python tools/sync_market_prices.py # 全部场内产品
python tools/sync_market_prices.py --codes 510300,510500
"""
from __future__ import annotations
import argparse
import asyncio
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from app.service.market_price_sync_service import ( # noqa: E402
MarketPriceSyncService,
summarize,
)
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]
async def main() -> int:
parser = argparse.ArgumentParser(description="同步场内日行情到 fin_market_price")
parser.add_argument("--codes", default="", help="只同步这些产品代码(逗号分隔)")
args = parser.parse_args()
codes = tuple(code.strip() for code in args.codes.split(",") if code.strip())
service = MarketPriceSyncService()
result = await service.sync(product_codes=codes or None)
print(summarize(result))
skipped = result.get("skipped") or []
if skipped:
print("\n未同步的产品(以及原因):")
for item in skipped:
print(f" · {item['product_code']} {item['reason']}")
if not result.get("written"):
print("\n[警告] 一行都没写 —— 下单仍然会失败。")
return 1
print("\n完成。下单应已可用(过期校验看的是 source_updated_at)。")
return 0
if __name__ == "__main__":
sys.exit(asyncio.run(main()))