Преглед изворни кода

Add counter-currency metal pricing

Lukas Goldschmidt пре 1 месец
родитељ
комит
3a23ec9c11
4 измењених фајлова са 76 додато и 7 уклоњено
  1. 32 4
      src/metals_mcp/mcp_tools.py
  2. 2 2
      src/metals_mcp/server_fastmcp.py
  3. 27 0
      src/metals_mcp/swissquote.py
  4. 15 1
      test_metals.py

+ 32 - 4
src/metals_mcp/mcp_tools.py

@@ -27,11 +27,39 @@ def get_capabilities() -> dict[str, Any]:
     }
 
 
-def get_price(symbol: str) -> dict[str, Any]:
-    quote = client.fetch_quote(symbol)
+def get_price(symbol: str, counter_currency: str | None = None) -> dict[str, Any]:
+    pair = client.normalize_pair(symbol, counter_currency)
+    if not client.pair_is_supported(symbol, counter_currency):
+        return {
+            "symbol": symbol.upper(),
+            "counter_currency": (counter_currency or "USD").upper(),
+            "pair": pair,
+            "price": None,
+            "timestamp": None,
+            "source": "swissquote",
+            "status": "unavailable",
+        }
+
+    quote = client.fetch_quote(pair)
     if not quote:
-        return {"symbol": symbol, "price": None, "timestamp": None, "source": "swissquote", "status": "unavailable"}
-    return {"symbol": symbol, "price": quote.mid, "timestamp": quote.timestamp, "source": "swissquote"}
+        return {
+            "symbol": symbol.upper(),
+            "counter_currency": (counter_currency or "USD").upper(),
+            "pair": pair,
+            "price": None,
+            "timestamp": None,
+            "source": "swissquote",
+            "status": "unavailable",
+        }
+    return {
+        "symbol": symbol.upper(),
+        "counter_currency": (counter_currency or "USD").upper(),
+        "pair": pair,
+        "price": quote.mid,
+        "timestamp": quote.timestamp,
+        "source": "swissquote",
+        "status": "ok",
+    }
 
 
 def get_ohlcv(symbol: str, timeframe: str = "5m", limit: int = 100) -> dict[str, Any]:

+ 2 - 2
src/metals_mcp/server_fastmcp.py

@@ -29,8 +29,8 @@ app = FastAPI(title="metals-mcp")
 
 
 @mcp.tool()
-def get_price(symbol: str):
-    return _get_price(symbol)
+def get_price(symbol: str, counter_currency: str | None = None):
+    return _get_price(symbol, counter_currency=counter_currency)
 
 
 @mcp.tool()

+ 27 - 0
src/metals_mcp/swissquote.py

@@ -2,6 +2,8 @@ from __future__ import annotations
 
 from dataclasses import dataclass
 from datetime import datetime, timezone
+import json
+from pathlib import Path
 from typing import Any
 
 import requests
@@ -11,6 +13,7 @@ HEADERS = {
     "User-Agent": "Mozilla/5.0",
     "Accept": "application/json, text/plain, */*",
 }
+PAIRS_PATH = Path(__file__).resolve().parents[3] / "swissquote_pairs.json"
 
 
 @dataclass(frozen=True)
@@ -26,6 +29,9 @@ class Quote:
 
 
 class SwissquoteClient:
+    def __init__(self) -> None:
+        self._pairs_cache: set[str] | None = None
+
     def normalize_symbol(self, symbol: str) -> str:
         cleaned = symbol.replace("/", "").upper()
         if cleaned in {"XAU", "XAG", "XPT", "XPD"}:
@@ -34,6 +40,27 @@ class SwissquoteClient:
             return symbol.upper()
         return symbol.upper()
 
+    def supported_pairs(self) -> set[str]:
+        if self._pairs_cache is not None:
+            return self._pairs_cache
+        try:
+            raw = json.loads(PAIRS_PATH.read_text())
+            pairs = raw.get("pairs", []) if isinstance(raw, dict) else []
+            self._pairs_cache = {str(item.get("symbol", "")).upper() for item in pairs if isinstance(item, dict) and item.get("symbol")}
+        except Exception:
+            self._pairs_cache = set()
+        return self._pairs_cache
+
+    def pair_is_supported(self, symbol: str, counter_currency: str | None = None) -> bool:
+        base = symbol.replace("/", "").upper() if "/" not in symbol else symbol.split("/", 1)[0].upper()
+        quote = (counter_currency or "USD").upper()
+        return f"{base}/{quote}" in self.supported_pairs()
+
+    def normalize_pair(self, symbol: str, counter_currency: str | None = None) -> str:
+        base = symbol.replace("/", "").upper() if "/" not in symbol else symbol.split("/", 1)[0].upper()
+        quote = (counter_currency or "USD").upper()
+        return f"{base}/{quote}"
+
     def fetch_quote(self, symbol: str) -> Quote | None:
         normalized = self.normalize_symbol(symbol)
         response = requests.get(BASE_URL.format(normalized), headers=HEADERS, timeout=5)

+ 15 - 1
test_metals.py

@@ -1,4 +1,4 @@
-from src.metals_mcp.mcp_tools import get_capabilities, get_candles, get_last_candle, get_price
+from src.metals_mcp.mcp_tools import client, get_capabilities, get_candles, get_last_candle, get_price
 
 
 def test_capabilities():
@@ -10,3 +10,17 @@ def test_scaffold_tools():
     assert get_price("XAU/USD")["symbol"] == "XAU/USD"
     assert get_candles("XAU/USD")["candles"] == []
     assert get_last_candle("XAU/USD")["candle"] is None
+
+
+def test_price_supports_counter_currency(monkeypatch):
+    monkeypatch.setattr(client, "pair_is_supported", lambda symbol, counter_currency=None: True)
+
+    class DummyQuote:
+        mid = 4049.0
+        timestamp = 1234567890
+
+    monkeypatch.setattr(client, "fetch_quote", lambda pair: DummyQuote())
+    quote = get_price("XAU", counter_currency="EUR")
+    assert quote["pair"] == "XAU/EUR"
+    assert quote["counter_currency"] == "EUR"
+    assert quote["price"] == 4049.0