main
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3from loguru import logger
4
5from config import API, PROXY, cache
6from networking import hx_req
7from price.chart import generate_chart
8from utils import number, ts_to_dt
9
10
11@cache.memoize(ttl=3600)
12async def get_okx_symbols() -> dict[str, str]:
13 """Get all symbols from OKX."""
14 logger.info("Fetching OKX symbols...")
15 res = {}
16 url = f"{API.OKX}/api/v5/public/instruments"
17 for instType in ["SWAP", "SPOT"]:
18 params = {"instType": instType}
19 response = await hx_req(url, params=params, proxy=PROXY.CRYPTO, check_keys=["data"], check_kv={"code": "0"}, silent=True)
20 if response.get("hx_error"):
21 return {}
22 data = response["data"]
23 res |= {coin["instId"].replace("-", ""): coin["instId"] for coin in data if coin["state"] == "live"}
24 return res
25
26
27async def okx_supported(coin: str) -> tuple[str, str]:
28 """Check if the coin is supported by OKX.
29
30 If supported, return the supported symbol format and the instId.
31
32 e.g. "BTC" -> ("BTCUSDT", "BTC-USDT")
33 """
34 symbols = await get_okx_symbols()
35 coin = coin.upper().replace("-", "")
36 symbol = coin
37 suffixes = ["USDT", "USDC", "USDTSWAP", "USDCSWAP", "USDSWAP"]
38 while symbol not in symbols and suffixes:
39 symbol = f"{coin}{suffixes.pop(0)}".upper()
40 return (symbol, symbols[symbol]) if symbol in symbols else ("", "")
41
42
43@cache.memoize(ttl=60)
44async def get_okx_price(coin: str, interval: str | None = None) -> dict:
45 """Get the price of a crypto asset from OKX."""
46 if interval is None:
47 interval = "30m"
48 # OKX interval unit: m, H, D, W, M
49 if interval.endswith(("h", "d", "w")):
50 interval = interval.upper()
51 if interval == "8H": # OKX does not support 8H interval
52 interval = "6H"
53 if interval not in ["1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "12H", "1D", "2D", "3D", "1W", "1M", "3M"]:
54 interval = "30m"
55
56 symbol, inst_id = await okx_supported(coin)
57 if not symbol:
58 return {}
59 url = f"{API.OKX}/api/v5/market/candles?instId={inst_id}&bar={interval}&limit=49"
60 response = await hx_req(url, proxy=PROXY.CRYPTO, check_kv={"code": "0"}, silent=True)
61 if response.get("hx_error"):
62 return {"texts": f"OKX price for {coin} failed: {response['hx_error']}"}
63 klines = response["data"] # ts, o, h, l, c
64 if not klines:
65 return {"texts": f"OKX price for {coin} failed: {response['hx_error']}"}
66 klines = sorted(klines, key=lambda x: x[0])
67 high_price = max(float(number(x[2])) for x in klines)
68 low_price = min(float(number(x[3])) for x in klines)
69 open_price = float(number(klines[0][1]))
70 close_price = float(number(klines[-1][4]))
71 change_pct = (close_price - open_price) / open_price
72 amplitude = (high_price - low_price) / low_price
73 if close_price < open_price:
74 amplitude *= -1
75 title = f"{symbol}•{interval}•OKX"
76 subtitle = f"开: {open_price} 高: {high_price} 低: {low_price} 收: {close_price} 涨: {change_pct:+.2%} 振: {abs(amplitude):.2%}"
77 text = f"{title}\n"
78 text += f"时间段: {ts_to_dt(klines[0][0]):%m-%d %H:%M} → {ts_to_dt(klines[-1][0]):%m-%d %H:%M}\n"
79 text += f"开盘价: {open_price}\n"
80 text += f"最高价: {high_price}\n"
81 text += f"最低价: {low_price}\n"
82 text += f"收盘价: {close_price} \n"
83 text += f"涨跌幅: {change_pct:+.2%}\n"
84 text += f"振幅: {amplitude:.2%}"
85 chart = await generate_chart(klines, interval, title, subtitle)
86 return {"texts": text, "media": [{"photo": chart}]}