软件简介
最近入手了积存金,由于波动较为剧烈,为了方便在办公(摸鱼)时第一时间掌握行情,闲暇时间写了这个 Gold Monitor基于自写整体框架后使用AI进行细节优化完成。它是一个常驻桌面的微型悬浮窗,设计风格参考现代化卡片 UI,主打:轻量、透明、不占地、响应快。
核心功能
实时双金行情
- 同时获取 沪金 (AU) 与 国际金 (XAUUSD) 的实时报价
- 数据源:京东金融(接口失效可替换抓取函数)
极致视觉体验
- 支持 0.2 ~ 1.0 透明度调节,融入任何桌面背景
- DPI 适配:4K 屏不模糊(兼容不同 Windows DPI API)
- 任意位置拖拽 + 位置记忆:拖动后保存窗口坐标,重启仍保持位置
- 无边框置顶悬浮显示,不占用任务栏空间
智能异动预警
价格预设(阈值提醒)
- 支持 AU / 国际金 上破 / 下破 阈值
- 触发后系统托盘气泡通知
- 内置防骚扰:穿越触发锁 + 冷却时间,避免阈值附近来回波动刷屏
波动监测(异动提醒)
- 监控指定窗口(默认 5 分钟,可自定义)内的异常涨跌
- 触发后悬浮卡片以 红 / 绿 闪烁提示(上涨红、下跌绿)
- 内置异动冷却时间,避免连续触发影响体验
离线邮件提醒(可选)
- 配置 SMTP 后可离线接收提醒邮件
- 默认适配 163 SMTP(SSL 465)
- 支持界面一键发送测试邮件确认配置可用
配置持久化
- 所有设置均保存在本地
config.json
- 包含:刷新间隔、透明度、阈值、异动窗口/阈值、冷却时间、邮件配置、窗口坐标等
关键代码(满足版块要求)
1) 两路行情抓取(沪金 / 国际金)
def _fetch_au(self):
"""沪金价格:GET pcQueryGoldProduct,取 priceValue 字段。"""
url = "https://ms.jr.jd.com/gw2/generic/CreatorSer/pc/m/pcQueryGoldProduct"
params = {"reqData": '{"goldType":"2"}'}
headers = {"Origin": "https://jdjr.jd.com", "Referer": "https://jdjr.jd.com/"}
try:
res = self.session.get(url, params=params, headers=headers, timeout=2)
res.raise_for_status()
return float(res.json()["resultData"]["data"]["priceValue"])
except Exception:
return None
def _fetch_intl(self):
"""国际金价格:POST getQuoteExtendUseUniqueCodeWithCache,取 lastPrice 字段。"""
url = "https://ms.jr.jd.com/gw2/generic/CaiFuPC/pc/m/getQuoteExtendUseUniqueCodeWithCache"
payload = {"ticket": "jd-jr-pc", "uniqueCode": "WG-XAUUSD"}
try:
res = self.session.post(url, json=payload, timeout=2)
res.raise_for_status()
return float(json.loads(res.json()["resultData"]["data"])["lastPrice"])
except Exception:
return None
2) 后台线程轮询 + 主线程刷新(避免 Tk 跨线程)
def _data_fetch_loop(self):
"""后台线程:轮询价格。禁止在此调用任何 tkinter 对象。"""
while True:
new_au = self._fetch_au()
new_intl = self._fetch_intl()
if new_au is not None:
with self._price_lock:
self.prev_au = self.au
self.au = new_au
self._check_alert(new_au, "AU")
self._track_au_extreme(new_au)
if new_intl is not None:
with self._price_lock:
self.prev_intl = self.intl
self.intl = new_intl
self._check_alert(new_intl, "INTL")
self._track_intl_extreme(new_intl)
time.sleep(self.interval + random.uniform(0.08, 0.7))
def _update_ui_cycle(self):
"""主线程:消费 action 队列 + 刷新价格显示(每 200 ms)。"""
try:
while True:
action = self._action_queue.get_nowait()
if action[0] == "flash":
self._flash_text(action[1], action[2])
except queue.Empty:
pass
with self._price_lock:
au = self.au
prev_au = self.prev_au
intl = self.intl
prev_intl = self.prev_intl
if au is not None:
self.canvas.itemconfig(self.au_value_text, text=f"{au:.2f}")
if prev_au is not None:
arrow = "↑" if au > prev_au else ("↓" if au < prev_au else "•")
clr = self.up_color if au > prev_au else (self.down_color if au < prev_au else self.muted_color)
self.canvas.itemconfig(self.au_arrow_text, text=arrow, fill=clr)
if intl is not None:
self.canvas.itemconfig(self.intl_value_text, text=f"{intl:.2f}")
if prev_intl is not None:
arrow = "↑" if intl > prev_intl else ("↓" if intl < prev_intl else "•")
clr = self.up_color if intl > prev_intl else (self.down_color if intl < prev_intl else self.muted_color)
self.canvas.itemconfig(self.intl_arrow_text, text=arrow, fill=clr)
self.root.after(200, self._update_ui_cycle)
3) 阈值穿越提醒(触发锁 + 冷却防骚扰)
def _check_alert(self, price, symbol):
"""价格阈值穿越检测:单次穿越只通知一次 + 冷却时间防止刷屏。"""
if price is None:
return
now = time.time()
if symbol == "AU":
if self.au_upper_target is not None:
if price >= self.au_upper_target and not self.au_upper_triggered:
if now - self.au_upper_last_ts >= self.alert_cooldown_sec:
msg = f"Au {self.au_upper_target},当前价格:{price:.2f}"
self._notify(msg)
self._send_alert_email("沪金提醒 — 上破", msg)
self.au_upper_last_ts = now
self.au_upper_triggered = True
if price < self.au_upper_target:
self.au_upper_triggered = False
if self.au_lower_target is not None:
if price <= self.au_lower_target and not self.au_lower_triggered:
if now - self.au_lower_last_ts >= self.alert_cooldown_sec:
msg = f"Au {self.au_lower_target},当前价格:{price:.2f}"
self._notify(msg)
self._send_alert_email("沪金提醒 — 下破", msg)
self.au_lower_last_ts = now
self.au_lower_triggered = True
if price > self.au_lower_target:
self.au_lower_triggered = False
4) 异动检测(窗口内涨跌超过阈值触发闪烁)
def _track_au_extreme(self, price):
if not self.extreme_enabled:
return
now = time.time()
self.au_history.append((now, price))
cutoff = now - self.extreme_window_sec
while self.au_history and self.au_history[0][0] < cutoff:
self.au_history.popleft()
if len(self.au_history) < 2:
return
delta = price - self.au_history[0][1]
if abs(delta) < self.extreme_threshold:
return
if now - self.extreme_last_ts < self.extreme_cooldown_sec:
return
self.extreme_last_ts = now
self._action_queue.put(("flash", self.up_color if delta > 0 else self.down_color, self.extreme_flash_times))
5) 邮件提醒(163 SMTP SSL,异步发送不阻塞)
def send_email(self, sender, password, receiver, subject, body):
"""163 SMTP SSL 发送邮件。返回 (True, '') 成功;(False, err) 失败。"""
try:
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = subject
msg["From"] = sender
msg["To"] = receiver
with smtplib.SMTP_SSL("smtp.163.com", 465, timeout=10) as smtp:
smtp.login(sender, password)
smtp.sendmail(sender, [receiver], msg.as_string())
return True, ""
except Exception as e:
return False, str(e)
def _send_alert_email(self, subject, body):
"""异步发送提醒邮件,不阻塞 fetch 循环。"""
if not self.email_enabled:
return
if not all([self.email_sender, self.email_password, self.email_receiver]):
return
threading.Thread(
target=self.send_email,
args=(self.email_sender, self.email_password, self.email_receiver, subject, body),
daemon=True
).start()
技术细节
- GUI 底座: 原生
tkinter + Canvas 绘制卡片;托盘图标使用 PIL 绘制
- 并发逻辑: 主线程 UI 负责渲染与消费动作队列;后台线程轮询请求,保证网络不阻塞界面
- 避坑指南: 处理了
SetProcessDpiAwareness 的系统兼容性,且托盘菜单回调通过 root.after(...) 切回 Tk 主线程
- 防骚扰机制: 触发锁 + 冷却时间戳,避免阈值附近反复波动导致频繁提醒
- 动态刷新频率:
interval + random.uniform(0.08, 0.7) 增加随机抖动
使用说明
- 运行环境: Python 3.8+(Windows)
- 依赖安装:
pip install requests pystray pillow
- 配置文件: 首次运行会生成
config.json,也可通过界面“设置”直接修改并保存应用
- 邮件功能: 建议使用 163 邮箱,开启 SMTP 并使用授权码(非登录密码)
- 数据来源: 京东金融公开数据;如接口失效可替换
_fetch_au / _fetch_intl
软件预览
gold.zip
(9 KB, 下载次数: 362)
免责声明
本工具仅供学习交流使用,数据实时性受网络影响,不作为任何投资建议。股市(金市)有风险,入场需谨慎!