Commit 6f20cad

benny-dou <60535774+benny-dou@users.noreply.github.com>
2025-02-07 07:13:56
feat(price): add `/crypto` and `/stock` prefix to limit market
1 parent 2a33445
Changed files (4)
src/bridge/chartimg.py
@@ -33,7 +33,7 @@ async def send_to_chartimg_bridge(client: Client, message: Message, symbol: str,
     else:
         target_mid = to_int(reply_msg_id)
     metadata = {"target_cid": target_cid, "target_mid": target_mid, "src": f"{symbol} {interval}"}
-    cache.set(f"bridge-{symbol} {interval}", metadata, ttl=15)  # save metadata to cache
+    cache.set(f"bridge-{symbol} {interval}", metadata, ttl=60)  # save metadata to cache
     logger.warning(f"Trying chartimg bridge (@{CHART_BOT}): {symbol} {interval}")
     await client.send_message(chat_id=f"@{CHART_BOT}", text=f"/chart {symbol} {interval}")
 
src/price/coinmarketcap.py
@@ -68,10 +68,7 @@ async def cmc_supported(coin: str, fiat: str = "USD") -> dict:
 
 @cache.memoize(ttl=60)
 async def get_cmc_price(coin: str, fiat: str = "USD") -> str:
-    """Get the price of a crypto asset from CoinMarketCap.
-
-    If the market cap of the coin is less than 10M, we skip it.
-    """
+    """Get the price of a crypto asset from CoinMarketCap."""
     params = await cmc_supported(coin, fiat)
     if not params:
         return ""
@@ -81,8 +78,6 @@ async def get_cmc_price(coin: str, fiat: str = "USD") -> str:
     if not data:
         return f"CoinMarketCap price failed: {coin}"
     stats = data["quote"][fiat]
-    if float(stats["market_cap"]) < 1000_0000:  # 1000万
-        return ""
     precision = 2 if float(stats["price"]) > 1 else 6
     emoji = lambda x: "🟢" if float(x) > 0 else "🔴"
     msg = f"🪙**{data['symbol']}** ({data['name']})\n"
@@ -94,7 +89,7 @@ async def get_cmc_price(coin: str, fiat: str = "USD") -> str:
     msg += f"{emoji(stats['percent_change_90d'])}90d涨跌: {stats['percent_change_90d']:+.2f}%\n"
     date = datetime.fromisoformat(data["last_updated"]).astimezone(ZoneInfo(TZ))
     msg += f"🕒{date:%Y-%m-%d %H:%M}\n"
-    msg += "📡CoinMarketCap\n"
+    msg += f"📡[CoinMarketCap](https://coinmarketcap.com/currencies/{data['slug']})\n"
     return msg.strip()
 
 
src/price/entrypoint.py
@@ -17,22 +17,26 @@ from price.tradingview import get_tradingview_price, tradingview_supported
 
 HELP = f"""
 💵**查询价格**
-使用说明: `{PREFIX.PRICE}` + Symbol + [@Interval]
+使用说明:
+- `{PREFIX.PRICE}` + Symbol + [@Interval]
+- 或`{PREFIX.CRYPTO}` 仅查询加密货币市场
+- 或`{PREFIX.STOCK}` 仅查询股票市场
+
 其中symbol(大小写不限)支持如下类别:
 1. 加密货币, 如 `BTC`
-2. 股票&指数, 如 `AAPL` / `SPX` (A股, 港股, 美股)
+2. 股票, 如 `AAPL` / `SPX` (A股, 港股, 美股)
 3. 汇率, 如 `USD CNY` (中间有空格)
 
 K线Interval (可选):
 - 加密货币(默认30m)
 1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1D,3D,1W,1M
-- 股票&指数(默认15m)
+- 股票(默认15m)
 1m,3m,5m,15m,30m,45m,1h,2h,3h,4h,1D,1W,1M,3M,6M,1Y
 
 说明:
 - 加密货币支持币种代码(BTC), 币种名称(bitcoin), 或交易对(BTCUSDC).
 - 此外在Symbol前添加数字可以计算对应数量的价值。当前仅支持对加密货币和法币汇率进行计算。
-
+- 有些代码同时被加密货币和股票市场使用而产生冲突。例如`SPX`是标普500指数, 同时也是一个山寨币。此时可以使用`{PREFIX.CRYPTO}`或`{PREFIX.STOCK}`来限定市场。
 示例:
 1. 查询加密货币价格
 - 对于Binance和OKX支持的币种, 还会返回K线图(默认30m)
@@ -41,7 +45,7 @@ K线Interval (可选):
 - `{PREFIX.PRICE} DOGEUSDT`
 - `{PREFIX.PRICE} BTC @4h`
 
-2. 查询股票&指数价格:
+2. 查询股票价格:
 - 默认返回Interval为15m的K线图
 - `{PREFIX.PRICE}` AAPL 或 NASDAQ:AAPL
 - `{PREFIX.PRICE}` SPX 或 SP:SPX
@@ -67,13 +71,16 @@ async def get_asset_price(client: Client, message: Message, **kwargs):
         return
     info = parse_msg(message)
     # send docs if message == "/price"
-    if equal_prefix(info["text"], prefix=[PREFIX.PRICE]):
+    if equal_prefix(info["text"], prefix=[PREFIX.PRICE, PREFIX.CRYPTO, PREFIX.STOCK]):
         await send2tg(client, message, texts=HELP, **kwargs)
         return
 
-    if not startswith_prefix(info["text"], prefix=[PREFIX.PRICE]):
+    if not startswith_prefix(info["text"], prefix=[PREFIX.PRICE, PREFIX.CRYPTO, PREFIX.STOCK]):
         return
-    text = info["text"].removeprefix(PREFIX.PRICE).strip()
+    prefix = info["text"].split(" ")[0]
+    crypto_only = prefix == PREFIX.CRYPTO
+    stock_only = prefix == PREFIX.STOCK
+    text = info["text"].removeprefix(prefix).strip()
     # these patterns should use CoinMarketCap API
     # some coin has "$" in symbol, so we need to match it
     pattern_1 = r"^([\d.]+)\s+([$\dA-Za-z]+)\s+([$\dA-Za-z]+)$"  # match "1.5 BTC CNY"
@@ -92,10 +99,9 @@ async def get_asset_price(client: Client, message: Message, **kwargs):
         amount = 1
         base = matched.group(1)
         quote = matched.group(2)
-    if amount > 0 and base and quote and (msg := await cmc_convert_price(amount, base, quote)):
+    if not stock_only and amount > 0 and base and quote and (msg := await cmc_convert_price(amount, base, quote)):
         await send2tg(client, message, texts=msg, **kwargs)
         return
-
     # match interval: "BTC @1m" or "000001 @15m"
     if matched := re.search(r"^([$\dA-Za-z]+)\s+@(\d+[A-Za-z])$", text, re.IGNORECASE):
         symbol = matched.group(1)
@@ -103,7 +109,7 @@ async def get_asset_price(client: Client, message: Message, **kwargs):
     else:  # match single symbol: "BTC" / "AAPL" / "SPX" / "000001"
         symbol = text
         interval = None
-    categories = await match_symbol_category(symbol)
+    categories = await match_symbol_category(symbol, crypto_only=crypto_only, stock_only=stock_only)
     if not categories:
         await send2tg(client, message, texts=f"不支持此Symbol: {symbol.upper()}\n{HELP}", **kwargs)
         return
@@ -113,10 +119,13 @@ async def get_asset_price(client: Client, message: Message, **kwargs):
     else:
         warn_msg = None
     # Tradingview
-    if categories.get("tradingview") and await get_tradingview_price(client, message, symbol, interval, **kwargs):
-        await modify_progress(warn_msg, del_status=True, del_delay=5)
+    if not crypto_only and categories.get("tradingview") and await get_tradingview_price(client, message, symbol, interval, **kwargs):
+        await modify_progress(warn_msg, del_status=True, del_delay=10)
         return
 
+    # Belows are only for crypto market
+    if stock_only:
+        return
     # Binance & OKX will return klines chart
     if (res := await get_binance_price(symbol, interval)) or (res := await get_okx_price(symbol, interval)):
         await send2tg(client, message, **res, **kwargs)
@@ -124,27 +133,32 @@ async def get_asset_price(client: Client, message: Message, **kwargs):
     # other crypto assets supported by CoinMarketCap
     if res := await get_cmc_price(text):
         await send2tg(client, message, texts=res, **kwargs)
-    await modify_progress(warn_msg, del_status=True, del_delay=5)
+    await modify_progress(warn_msg, del_status=True, del_delay=10)
 
 
 @cache.memoize(ttl=3600)
-async def match_symbol_category(symbol: str = "") -> dict[str, str]:
+async def match_symbol_category(symbol: str = "", *, crypto_only: bool = False, stock_only: bool = False) -> dict[str, str]:
     if not ENABLE.PRICE:
         return {}
     category = {}
-    if cmc := await cmc_supported(symbol):  # {"symbol": "BTC"} or {"slug": "bitcoin"}
-        category["cmc"] = cmc.get("symbol", cmc.get("slug"))
-        category["crypto"] = cmc.get("symbol", cmc.get("slug"))
-    # okx
-    okx_symbol, _ = await okx_supported(symbol)  # (symbol, instId)
-    if okx_symbol:
-        category["okx"] = okx_symbol
-        category["crypto"] = okx_symbol
-    binance_symbol, _ = await binance_supported(symbol)  # (symbol, market)
-    if binance_symbol:
-        category["binance"] = binance_symbol
-        category["crypto"] = binance_symbol
-    if tradingview := await tradingview_supported(symbol):  # ["SSE:000001", "SZSE:000001"]
+    # Crypto market
+    if not stock_only:
+        if cmc := await cmc_supported(symbol):  # {"symbol": "BTC"} or {"slug": "bitcoin"}
+            category["cmc"] = cmc.get("symbol", cmc.get("slug"))
+            category["crypto"] = cmc.get("symbol", cmc.get("slug"))
+        # okx
+        okx_symbol, _ = await okx_supported(symbol)  # (symbol, instId)
+        if okx_symbol:
+            category["okx"] = okx_symbol
+            category["crypto"] = okx_symbol
+        binance_symbol, _ = await binance_supported(symbol)  # (symbol, market)
+        if binance_symbol:
+            category["binance"] = binance_symbol
+            category["crypto"] = binance_symbol
+
+    # Stock market
+    tradingview = []
+    if not crypto_only and (tradingview := await tradingview_supported(symbol)):  # ["SSE:000001", "SZSE:000001"]
         category["tradingview"] = ", ".join(tradingview)
     # skip some crypto ETF (e.g. Grayscale Bitcoin Mini Trust use symbol "AMEX:BTC")
     if category.get("crypto") and category.get("tradingview"):
@@ -156,7 +170,7 @@ async def match_symbol_category(symbol: str = "") -> dict[str, str]:
     # if tradingview has multiles symbols
     if len(tradingview) > 1:
         msg = f"⚠️**{symbol.upper()}**代码重复:\n"
-        msg += f"股票&指数: **{category['tradingview']}**\n"
+        msg += f"股票: **{category['tradingview']}**\n"
         msg += f"本次查询: **{tradingview[0]}**\n"
         msg += f"查询其他请使用完整代码:\n`{PREFIX.PRICE} {'/ '.join(tradingview[1:])}`"
         category["warnings"] = msg
@@ -164,10 +178,10 @@ async def match_symbol_category(symbol: str = "") -> dict[str, str]:
     # if symbol matches tradingview & crypto categories
     if category.get("crypto") and category.get("tradingview"):
         msg = f"⚠️**{symbol.upper()}**代码重复:\n"
-        msg += f"股票&指数: **{category['tradingview']}**\n"
+        msg += f"股票: **{category['tradingview']}**\n"
         msg += f"加密货币: **{category['crypto']}**\n"
-        msg += f"默认查询股票&指数: **{tradingview[0]}**\n"
-        msg += f"查询其他请使用完整代码:\n`{PREFIX.PRICE} {category['crypto']}`"
+        msg += f"默认查询股票: **{tradingview[0]}**\n"
+        msg += f"查询其他`{PREFIX.CRYPTO}` 或 `{PREFIX.STOCK}`限定市场"
         category["warnings"] = msg
 
     return category
src/config.py
@@ -59,7 +59,9 @@ class PREFIX:
     SUBTITLE = os.getenv("PREFIX_SUBTITLE", "/subtitle").lower()
     WGET = os.getenv("PREFIX_WGET", "/wget").lower()
     OCR = os.getenv("PREFIX_OCR", "/ocr").lower()
-    PRICE = os.getenv("PREFIX_PRICE", "/price").lower()
+    PRICE = os.getenv("PREFIX_PRICE", "/price").lower()  # unify crypto, stock
+    CRYPTO = os.getenv("PREFIX_CRYPTO", "/crypto").lower()  # crypto only
+    STOCK = os.getenv("PREFIX_STOCK", "/stock").lower()  # stock only
     COMBINATION = os.getenv("PREFIX_COMBINATION", "/combine").lower()