Files
eink-push/btc_api.py
YANG JIANKUAN 85b5113ba4 feat: E1002 新增公网图片服务(恒定 URL 现渲染)+底部改为 BTC 时K
- server.py:三条恒定 URL 并行——/e1002.png 内存现渲染 PNG(主力)、
  /e1002.html 现代网页、/e1002-web.png 网页经无头 Chrome 截图后固化六色;
  天气/橘喵/BTC 按 WEATHER_TTL/MAO_TTL/BTC_TTL 走内存缓存,过期才实拉、
  失败沿用旧值;用量不读本机文件,改由 POST /push/usage(_gpt) 推送暂存
- push_client.py:本机把 Claude 用量快照推送到服务(X-Push-Token 鉴权)
- btc_api.py:OKX/Coinbase/Huobi 免费接口逐源兜底,时K 近 72 小时,涨绿跌红
- data.py 拆出 weather_fetch/mao_fetch/btc_fetch 与 usage_from_raw,
  统一按 TZ_NAME 时区;宽版只用点阵字体整数倍字号 12/24/36/48
- probe_server.py:验证平台对同一 URL 是否周期重抓(结论:Image 控件
  不重抓,HTML 控件重抓)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-10 18:55:47 +08:00

81 lines
3.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.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
比特币 K 线行情客户端 —— 免费、无需密钥的公开接口,按顺序尝试多个源,首个成功即返回。
源(均为公开 REST无鉴权
1. OKX GET /api/v5/market/candles?instId=BTC-USDT&bar=<1H|1D> —— 主源,本地直连可达、数据最新
2. Coinbase GET /products/BTC-USD/candles?granularity=<3600|86400> —— 备源USD 计价,与 USDT 数值几乎一致)
3. Huobi GET /market/history/kline?period=<60min|1day>&symbol=btcusdt —— 备源
Binance 在本地区返回 restricted location不列入。
周期用统一代号 bar"1H"(时 K/ "1D"(日 K由 BARS 映射到各源的写法。
统一输出:按时间**升序**的 list[dict],每根 {"ts": epoch 秒, "o", "h", "l", "c"}float最后一根为当前未收盘的那根。
任何源失败抛异常并尝试下一个;全部失败抛 RuntimeError由 data 层决定沿用缓存或占位。
"""
import json
import urllib.request
TIMEOUT = 8
UA = "eink-push/1.0 (+https://github.com/)"
# 周期代号 → 各源参数:(OKX bar, Coinbase granularity 秒, Huobi period)
BARS = {"1H": ("1H", 3600, "60min"), "1D": ("1D", 86400, "1day")}
def _get_json(url):
req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"})
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
return json.loads(resp.read().decode("utf-8"))
def _okx(bar, limit):
j = _get_json(f"https://www.okx.com/api/v5/market/candles?instId=BTC-USDT&bar={BARS[bar][0]}&limit={limit}")
if str(j.get("code")) != "0":
raise RuntimeError(f"okx code={j.get('code')} msg={j.get('msg')}")
rows = j["data"] # 最新在前:[ts_ms, o, h, l, c, vol, ...]
out = [{"ts": int(r[0]) // 1000, "o": float(r[1]), "h": float(r[2]), "l": float(r[3]), "c": float(r[4])} for r in rows]
return sorted(out, key=lambda x: x["ts"])
def _coinbase(bar, limit):
rows = _get_json(f"https://api.exchange.coinbase.com/products/BTC-USD/candles?granularity={BARS[bar][1]}")
# 最新在前:[time, low, high, open, close, volume]
out = [{"ts": int(r[0]), "o": float(r[3]), "h": float(r[2]), "l": float(r[1]), "c": float(r[4])} for r in rows]
return sorted(out, key=lambda x: x["ts"])[-limit:]
def _huobi(bar, limit):
j = _get_json(f"https://api.huobi.pro/market/history/kline?period={BARS[bar][2]}&size={limit}&symbol=btcusdt")
if j.get("status") != "ok":
raise RuntimeError(f"huobi status={j.get('status')} err={j.get('err-msg')}")
out = [{"ts": int(r["id"]), "o": float(r["open"]), "h": float(r["high"]), "l": float(r["low"]), "c": float(r["close"])}
for r in j["data"]]
return sorted(out, key=lambda x: x["ts"])
SOURCES = (("okx", _okx), ("coinbase", _coinbase), ("huobi", _huobi))
def get_candles(bar="1H", limit=36):
"""返回 (源名, 升序 K 线列表)。bar 为 "1H"/"1D"。逐源尝试,全部失败抛 RuntimeError。"""
if bar not in BARS:
raise ValueError(f"不支持的周期 {bar!r},可选:{list(BARS)}")
errors = []
for name, fn in SOURCES:
try:
rows = fn(bar, limit)
if len(rows) >= 2:
return name, rows[-limit:]
errors.append(f"{name}: 数据不足({len(rows)})")
except Exception as e: # noqa: BLE001 —— 逐源兜底,任何异常都换下一个源
errors.append(f"{name}: {e}")
raise RuntimeError(f"BTC {bar} K 线全部源失败:" + "".join(errors))
if __name__ == "__main__":
src, rows = get_candles("1H", 5)
print(src)
for r in rows:
print(r)