""" Orynela Agent Lab — Python adapter (SANDBOX ONLY). This adapter ONLY talks to the Orynela Sandbox API. real_execution is permanently False. Hard-coded. No way to flip it. Usage: from orynela_adapter import OrynelaAdapter bot = OrynelaAdapter() bot.send_heartbeat() bot.get_candles("BTCUSDT", "1h", 200) # real OHLCV (t,o,h,l,c,v) for simulation bot.send_signal("BTCUSDT", "buy", 0.72, "EMA alignment") bot.simulate_order("BTCUSDT", "buy", 0.01) Requires: - Python 3.10+ - Environment variables: ORYNELA_API_BASE, ORYNELA_SANDBOX_KEY """ import os import json import urllib.request import urllib.error from typing import Optional, Any class OrynelaApiError(Exception): """Raised when the Sandbox API returns an error.""" class OrynelaAdapter: """Sandbox-only adapter for Orynela Agent Lab. Hard invariants: - environment is always "sandbox" - real_execution is always False - withdrawals are not supported """ def __init__( self, api_base: Optional[str] = None, sandbox_key: Optional[str] = None, timeout: float = 30.0, ): self.api_base = api_base or os.environ.get( "ORYNELA_API_BASE", "https://orynela.ai/api/sandbox" ) self.sandbox_key = sandbox_key or os.environ["ORYNELA_SANDBOX_KEY"] self.environment = "sandbox" self.real_execution = False # NEVER change this. Hard-coded. self.timeout = timeout def _request( self, method: str, path: str, body: Optional[dict] = None, params: Optional[dict] = None, ) -> dict: url = self.api_base + path if params: from urllib.parse import urlencode url += "?" + urlencode(params) data = json.dumps(body).encode("utf-8") if body is not None else None req = urllib.request.Request( url, data=data, headers={ "Authorization": f"Bearer {self.sandbox_key}", "Content-Type": "application/json", "Accept": "application/json", }, method=method, ) try: with urllib.request.urlopen(req, timeout=self.timeout) as r: payload = json.loads(r.read()) except urllib.error.HTTPError as e: body = e.read().decode("utf-8", errors="replace") raise OrynelaApiError(f"HTTP {e.code}: {body}") except urllib.error.URLError as e: raise OrynelaApiError(f"Network error: {e.reason}") return payload # ── Heartbeat ────────────────────────────────────────────────────────── def send_heartbeat( self, status: str = "online", latency_ms: Optional[int] = None, version: str = "0.1.0", ) -> dict: return self._request( "POST", "/heartbeat", {"status": status, "latency_ms": latency_ms, "version": version}, ) # ── Logs ─────────────────────────────────────────────────────────────── def send_log(self, level: str, message: str, context: Optional[dict] = None) -> dict: """Send a log. level ∈ {info, warning, error, risk, system}.""" assert level in ("info", "warning", "error", "risk", "system"), \ f"Invalid log level: {level}" # Defense-in-depth: scrub any credential-looking key from context client-side too. if context: forbidden = ("api_key", "secret", "password", "token", "wallet", "seed", "passphrase") context = {k: ("[REDACTED]" if any(f in k.lower() for f in forbidden) else v) for k, v in context.items()} return self._request("POST", "/logs", { "level": level, "message": message, "context": context or {}, }) # ── Signals ──────────────────────────────────────────────────────────── def send_signal( self, symbol: str, side: str, confidence: float, reasoning: str, timeframe: str = "1m", signal_type: str = "general", metadata: Optional[dict] = None, ) -> dict: """Send a signal. side ∈ {buy, sell, flat}. confidence ∈ [0, 1].""" assert side in ("buy", "sell", "flat"), f"Invalid side: {side}" assert 0 <= confidence <= 1, "confidence must be between 0 and 1" return self._request("POST", "/signals", { "symbol": symbol, "timeframe": timeframe, "side": side, "confidence": confidence, "signal_type": signal_type, "reasoning": reasoning, "metadata": metadata or {}, }) # ── Orders (simulated) ───────────────────────────────────────────────── def simulate_order( self, symbol: str, side: str, quantity: float, signal_id: Optional[int] = None, ) -> dict: """Request a SIMULATED market order. Returns dict including: - ok: bool - status: "simulated_filled" | "risk_rejected" - simulated_fill_price (if accepted) - reason (if rejected) """ assert side in ("buy", "sell"), f"Invalid side: {side}" assert quantity > 0, "quantity must be positive" # IMPORTANT — never include real_execution flag, broker key, or wallet. return self._request("POST", "/orders/simulate", { "signal_id": signal_id, "symbol": symbol, "side": side, "order_type": "market", "quantity": quantity, }) # ── Reads ────────────────────────────────────────────────────────────── def get_portfolio(self) -> dict: return self._request("GET", "/portfolio") def get_orders(self, page: int = 1) -> dict: return self._request("GET", "/orders", params={"page": page}) def get_signals(self, page: int = 1) -> dict: return self._request("GET", "/signals", params={"page": page}) def get_candles(self, symbol: str, timeframe: str = "1m", limit: int = 100) -> dict: """Fetch REAL OHLCV market data for SIMULATION (scope: market:read). Source: real public market data (Yahoo Finance, Stooq fallback, deterministic mock if a source is briefly unavailable), refreshed in near real time (~60s cache). Args: symbol: equities/ETF as-is ("SPY", "QQQ", "AAPL"…), crypto "XXXUSDT" ("BTCUSDT", "ETHUSDT", "SOLUSDT"…), or forex 6-letter ("EURUSD"…). timeframe: one of "1m", "5m", "15m", "1h", "4h", "1d". limit: number of candles, up to 500 (default 100). Returns dict: { "symbol": "BTCUSDT", "timeframe": "1h", "count": 200, "source": "yahoo" | "stooq" | "mock_deterministic", "real_execution": False, "candles": [ {"t": , "o","h","l","c": , "v": }, ... ] } A simulated order then fills at the REAL current market price (+ simulated slippage/fees), so decisions made on these candles map to realistic fills. """ return self._request("GET", "/market/candles", params={ "symbol": symbol, "timeframe": timeframe, "limit": limit, }) def candle_closes(self, symbol: str, timeframe: str = "1m", limit: int = 100) -> list: """Convenience: return just the list of close prices from get_candles().""" return [c["c"] for c in self.get_candles(symbol, timeframe, limit).get("candles", [])] def status(self) -> dict: return self._request("GET", "/status")