Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion gunbot_quant/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from ..core.utils import NumpyEncoder, DataValidationError
from ..strategies.strategy_library import STRATEGY_MAPPING
from ..core.backtest_engine import calculate_stats
from ..core.fxmacrodata import ENDPOINTS as FXMACRODATA_ENDPOINTS, FxMacroDataClient, FxMacroDataError

# Setup basic logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
Expand All @@ -42,6 +43,49 @@
os.makedirs(SCREENER_RESULTS_DIR, exist_ok=True)
os.makedirs(SCREENER_CONFIGS_DIR, exist_ok=True)

@router.get("/fxmacrodata/endpoints")
async def list_fxmacrodata_endpoints():
return {"endpoints": FXMACRODATA_ENDPOINTS}

@router.post("/fxmacrodata/request")
async def fxmacrodata_request(payload: Dict[str, Any]):
client = FxMacroDataClient()
try:
return client.request(
payload.get("endpoint", ""),
currency=payload.get("currency"),
indicator=payload.get("indicator"),
base=payload.get("base"),
quote_currency=payload.get("quote") or payload.get("quote_currency"),
path=payload.get("path"),
params=payload.get("params"),
body=payload.get("body"),
)
except FxMacroDataError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=f"FXMacroData request failed: {exc}") from exc

@router.get("/fxmacrodata/macro-context")
async def fxmacrodata_macro_context(
currency: str = "usd",
indicator: str = "non_farm_payrolls",
days_ahead: int = 30,
include_news: bool = True,
):
client = FxMacroDataClient()
try:
return client.macro_context(
currency,
indicator,
days_ahead=days_ahead,
include_news=include_news,
)
except FxMacroDataError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=f"FXMacroData context failed: {exc}") from exc

# --- NEW: Gunbot Period Matrix & Helper ---
GUNBOT_PERIOD_MATRIX = {
'bingx': [1, 3, 5, 15, 30, 60, 120, 240, 360, 480, 720, 1440, 4320, 10080, 43200],
Expand Down Expand Up @@ -1370,4 +1414,4 @@ async def list_discovery_results():
async def disconnect_gunbot():
"""Clears the stored Gunbot credentials."""
gunbot_client.clear_credentials()
return {"status": "success", "message": "Gunbot credentials cleared."}
return {"status": "success", "message": "Gunbot credentials cleared."}
243 changes: 243 additions & 0 deletions gunbot_quant/core/fxmacrodata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
"""FXMacroData client helpers for macro-aware Gunbot Quant workflows."""

from __future__ import annotations

import os
from dataclasses import dataclass, field
from typing import Any, Callable
from urllib.parse import quote

import requests

ENDPOINTS = (
"data_catalogue",
"announcements",
"latest_announcements",
"announcement_changes",
"calendar",
"predictions",
"forex",
"cot",
"commodity",
"commodities_latest",
"curves",
"curve_proxies",
"forward_curves",
"rate_differentials",
"forward_differentials",
"market_sessions",
"risk_sentiment",
"news",
"press_releases",
"graphql",
"custom",
)

Transport = Callable[[str, str, dict[str, Any] | None, Any | None], Any]


class FxMacroDataError(RuntimeError):
"""Raised when an FXMacroData request cannot be built or completed."""


@dataclass
class FxMacroDataClient:
api_key: str | None = None
base_url: str = "https://api.fxmacrodata.com/v1"
timeout: float = 30.0
transport: Transport | None = None
session: requests.Session = field(default_factory=requests.Session)

def __post_init__(self) -> None:
self.base_url = self.base_url.rstrip("/")
if self.api_key is None:
self.api_key = os.getenv("FXMACRODATA_API_KEY") or os.getenv("FXMD_API_KEY")

def request(
self,
endpoint: str,
*,
currency: str | None = None,
indicator: str | None = None,
base: str | None = None,
quote_currency: str | None = None,
path: str | None = None,
params: dict[str, Any] | None = None,
body: Any | None = None,
) -> Any:
method, endpoint_path = self._endpoint_path(
endpoint,
currency=currency,
indicator=indicator,
base=base,
quote_currency=quote_currency,
path=path,
body=body,
)
return self._send(method, endpoint_path, params=params, body=body)

def data_catalogue(self, currency: str, **params: Any) -> Any:
return self.request("data_catalogue", currency=currency, params=params)

def announcements(self, currency: str, indicator: str, **params: Any) -> Any:
return self.request("announcements", currency=currency, indicator=indicator, params=params)

def latest_announcements(self, currency: str, **params: Any) -> Any:
return self.request("latest_announcements", currency=currency, params=params)

def announcement_changes(self, **params: Any) -> Any:
return self.request("announcement_changes", params=params)

def calendar(self, currency: str, **params: Any) -> Any:
return self.request("calendar", currency=currency, params=params)

def predictions(self, currency: str, indicator: str, **params: Any) -> Any:
return self.request("predictions", currency=currency, indicator=indicator, params=params)

def forex(self, base: str, quote_currency: str, **params: Any) -> Any:
return self.request("forex", base=base, quote_currency=quote_currency, params=params)

def cot(self, currency: str, **params: Any) -> Any:
return self.request("cot", currency=currency, params=params)

def commodity(self, indicator: str, **params: Any) -> Any:
return self.request("commodity", indicator=indicator, params=params)

def commodities_latest(self, **params: Any) -> Any:
return self.request("commodities_latest", params=params)

def curves(self, currency: str, **params: Any) -> Any:
return self.request("curves", currency=currency, params=params)

def curve_proxies(self, currency: str, **params: Any) -> Any:
return self.request("curve_proxies", currency=currency, params=params)

def forward_curves(self, currency: str, **params: Any) -> Any:
return self.request("forward_curves", currency=currency, params=params)

def rate_differentials(self, base: str, quote_currency: str, **params: Any) -> Any:
return self.request(
"rate_differentials", base=base, quote_currency=quote_currency, params=params
)

def forward_differentials(self, base: str, quote_currency: str, **params: Any) -> Any:
return self.request(
"forward_differentials", base=base, quote_currency=quote_currency, params=params
)

def market_sessions(self, **params: Any) -> Any:
return self.request("market_sessions", params=params)

def risk_sentiment(self, **params: Any) -> Any:
return self.request("risk_sentiment", params=params)

def news(self, currency: str, **params: Any) -> Any:
return self.request("news", currency=currency, params=params)

def press_releases(self, currency: str, **params: Any) -> Any:
return self.request("press_releases", currency=currency, params=params)

def graphql(self, query: str, variables: dict[str, Any] | None = None) -> Any:
return self.request("graphql", body={"query": query, "variables": variables or {}})

def macro_context(
self,
currency: str,
indicator: str,
*,
days_ahead: int = 30,
include_news: bool = True,
) -> dict[str, Any]:
context = {
"catalogue": self.data_catalogue(currency),
"latest": self.latest_announcements(currency),
"announcement": self.announcements(currency, indicator),
"prediction": self.predictions(currency, indicator),
"calendar": self.calendar(currency, days_ahead=days_ahead),
"cot": self.cot(currency),
"curve": self.curves(currency),
}
if include_news:
context["news"] = self.news(currency)
return context

def _endpoint_path(
self,
endpoint: str,
*,
currency: str | None,
indicator: str | None,
base: str | None,
quote_currency: str | None,
path: str | None,
body: Any | None,
) -> tuple[str, str]:
if endpoint not in ENDPOINTS:
raise FxMacroDataError(f"Unsupported FXMacroData endpoint: {endpoint}")
if endpoint == "data_catalogue":
return "GET", f"/data_catalogue/{slug(currency, 'currency')}"
if endpoint == "announcements":
return "GET", f"/announcements/{slug(currency, 'currency')}/{slug(indicator, 'indicator')}"
if endpoint == "latest_announcements":
return "GET", f"/announcements/{slug(currency, 'currency')}/latest"
if endpoint == "announcement_changes":
return "GET", "/announcements/changes"
if endpoint == "calendar":
return "GET", f"/calendar/{slug(currency, 'currency')}"
if endpoint == "predictions":
return "GET", f"/predictions/{slug(currency, 'currency')}/{slug(indicator, 'indicator')}"
if endpoint == "forex":
return "GET", f"/forex/{slug(base, 'base')}/{slug(quote_currency, 'quote_currency')}"
if endpoint == "cot":
return "GET", f"/cot/{slug(currency, 'currency')}"
if endpoint == "commodity":
return "GET", f"/commodities/{slug(indicator, 'indicator')}"
if endpoint == "commodities_latest":
return "GET", "/commodities/latest"
if endpoint == "curves":
return "GET", f"/curves/{slug(currency, 'currency')}"
if endpoint == "curve_proxies":
return "GET", f"/curve_proxies/{slug(currency, 'currency')}"
if endpoint == "forward_curves":
return "GET", f"/forward_curves/{slug(currency, 'currency')}"
if endpoint == "rate_differentials":
return "GET", f"/rate_differentials/{slug(base, 'base')}/{slug(quote_currency, 'quote_currency')}"
if endpoint == "forward_differentials":
return "GET", f"/forward_differentials/{slug(base, 'base')}/{slug(quote_currency, 'quote_currency')}"
if endpoint == "market_sessions":
return "GET", "/market_sessions"
if endpoint == "risk_sentiment":
return "GET", "/risk_sentiment"
if endpoint == "news":
return "GET", f"/news/{slug(currency, 'currency')}"
if endpoint == "press_releases":
return "GET", f"/press-releases/{slug(currency, 'currency')}"
if endpoint == "graphql":
return "POST", "/graphql"
if not path:
raise FxMacroDataError("path is required for custom FXMacroData requests")
return ("POST" if body is not None else "GET"), path if path.startswith("/") else f"/{path}"

def _send(
self,
method: str,
path: str,
*,
params: dict[str, Any] | None = None,
body: Any | None = None,
) -> Any:
query = {k: v for k, v in (params or {}).items() if v is not None}
if self.api_key and "api_key" not in query:
query["api_key"] = self.api_key
url = f"{self.base_url}{path}"
if self.transport:
return self.transport(method, url, query or None, body)
response = self.session.request(method, url, params=query, json=body, timeout=self.timeout)
response.raise_for_status()
return response.json()


def slug(value: str | None, name: str) -> str:
if not value:
raise FxMacroDataError(f"{name} is required")
return quote(value.lower(), safe="")