Add files via upload

This commit is contained in:
xiaochuan
2025-11-14 22:56:44 +00:00
committed by GitHub
parent cf1944a688
commit ef6e1e278d
96 changed files with 3300 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
"""OANDA REST adapter (mock/stub for Phase 3)."""
from __future__ import annotations
import os
from dataclasses import asdict
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional
import requests
from .adapter import CancelAck, ExecutionAdapter, OrderAck, OrderParams, PositionState
from .config_loader import load_oanda_config
from .metrics_logger import MetricsLogger, log_event
from .order_store import OrderStore
class OandaAdapter(ExecutionAdapter):
BASE_URL = "https://api-fxpractice.oanda.com/v3"
def __init__(
self,
account_id: Optional[str] = None,
token: Optional[str] = None,
order_store: Optional[OrderStore] = None,
base_url: Optional[str] = None,
timeout_ms: int = 10000,
retry_backoff: float = 1.0,
max_retries: int = 3,
metrics_path: Optional[str] = None,
error_log: str = "results/execution/errors.log",
) -> None:
self.account_id = account_id or os.environ.get("OANDA_ACCOUNT_ID", "")
self.token = token or os.environ.get("OANDA_TOKEN", "")
self.base_url = base_url or self.BASE_URL
self.timeout_ms = timeout_ms
self.retry_backoff = retry_backoff
self.max_retries = max_retries
self.metrics_logger = MetricsLogger(metrics_path)
self.error_log = Path(error_log)
self.session = requests.Session()
self.session.headers.update({"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"})
self.order_store = order_store or OrderStore()
def _endpoint(self, path: str) -> str:
return f"{self.base_url}{path}"
@classmethod
def from_config(cls, path: Optional[str] = None, order_store: Optional[OrderStore] = None) -> "OandaAdapter":
cfg = load_oanda_config(path)
return cls(order_store=order_store, **cfg)
def _log_error(self, context: str, message: str) -> None:
self.error_log.parent.mkdir(parents=True, exist_ok=True)
with self.error_log.open("a", encoding="utf-8") as fh:
fh.write(f"[{datetime.utcnow().isoformat()}] {context}: {message}\n")
def _request(self, method: str, url: str, **kwargs):
backoff = self.retry_backoff
for attempt in range(self.max_retries):
try:
resp = self.session.request(method, url, timeout=self.timeout_ms / 1000.0, **kwargs)
except requests.RequestException as exc:
self._log_error("network", str(exc))
time.sleep(backoff)
backoff *= 2
continue
if resp.status_code >= 500:
self._log_error("server_error", resp.text)
time.sleep(backoff)
backoff *= 2
continue
if resp.status_code >= 400:
self._log_error("client_error", resp.text)
resp.raise_for_status()
resp.raise_for_status()
return resp.json()
raise RuntimeError(f"Failed {method} {url} after {self.max_retries} retries")
def submit(self, order: OrderParams) -> OrderAck:
start = datetime.utcnow()
payload = {
"order": {
"instrument": order.symbol,
"units": int(order.quantity if order.side.lower() == "buy" else -order.quantity),
"type": "MARKET" if order.price is None else "LIMIT",
"timeInForce": order.tif.upper(),
}
}
if order.price is not None:
payload["order"]["price"] = f"{order.price:.5f}"
if order.client_order_id:
payload["order"]["clientExtensions"] = {"clientOrderID": order.client_order_id}
url = self._endpoint(f"/accounts/{self.account_id}/orders")
data = self._request("POST", url, json=payload)
order_id = data.get("orderCreateTransaction", {}).get("id", "")
if order_id:
self.order_store.append(order_id, order)
end = datetime.utcnow()
log_event("submit", order_id or "", order.symbol, "accepted", start, end)
ts = data.get("time")
timestamp = datetime.fromisoformat(ts.replace("Z", "+00:00")) if ts else datetime.utcnow()
return OrderAck(order_id=order_id, status="accepted", timestamp=timestamp)
def cancel(self, order_id: str) -> CancelAck:
start = datetime.utcnow()
url = self._endpoint(f"/accounts/{self.account_id}/orders/{order_id}/cancel")
data = self._request("PUT", url)
end = datetime.utcnow()
log_event("cancel", order_id, "", "cancelled", start, end)
ts = data.get("time")
timestamp = datetime.fromisoformat(ts.replace("Z", "+00:00")) if ts else datetime.utcnow()
return CancelAck(order_id=order_id, status="cancelled", timestamp=timestamp)
def sync_positions(self) -> Dict[str, PositionState]:
url = self._endpoint(f"/accounts/{self.account_id}/positions")
data = self._request("GET", url)
positions = {}
for pos in data.get("positions", []):
symbol = pos["instrument"]
net = float(pos["netUnrealizedPL"])
qty = float(pos.get("long", {}).get("units", 0)) + float(pos.get("short", {}).get("units", 0))
avg_price = float(pos.get("avgPrice", 0))
positions[symbol] = PositionState(symbol=symbol, quantity=qty, avg_price=avg_price, unrealized_pnl=net)
return positions
def heartbeat(self) -> bool:
url = self._endpoint("/accounts")
try:
self._request("GET", url)
return True
except Exception:
return False