"""Override trade helpers — auto-registration of untracked symbols. Feature: override-trade-tab Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6 """ from __future__ import annotations import logging import httpx logger = logging.getLogger("trading_engine.override") async def auto_register_symbol( ticker: str, registry_base_url: str, ) -> tuple[bool, str]: """Register an untracked symbol in the Symbol Registry. Returns ``(auto_registered, company_id)``. Calls Symbol Registry HTTP endpoints to create the company, default sources, and watchlist membership. Handles 409 conflicts gracefully. Source and watchlist failures are logged but do not block order enqueuing (best-effort). """ ticker = ticker.upper() base = registry_base_url.rstrip("/") async with httpx.AsyncClient(timeout=10.0) as client: # ------------------------------------------------------------------ # 1. Check if ticker already exists (GET /companies?active=true) # The Symbol Registry list endpoint only filters by active flag, # so we fetch all active companies and search client-side. # ------------------------------------------------------------------ try: resp = await client.get(f"{base}/companies", params={"active": "true"}) if resp.status_code == 200: companies = resp.json() for company in companies: if company.get("ticker") == ticker: logger.info( "Ticker %s already exists (company_id=%s), skipping registration", ticker, company["id"], ) return (False, str(company["id"])) else: logger.warning( "Symbol Registry GET /companies returned %d — proceeding with creation", resp.status_code, ) except httpx.HTTPError as exc: logger.warning( "Failed to query Symbol Registry for existing companies: %s — proceeding with creation", exc, ) # ------------------------------------------------------------------ # 2. Create company (POST /companies) # Requirement 4.1: legal_name = ticker, active = true (default) # ------------------------------------------------------------------ company_id: str | None = None try: resp = await client.post( f"{base}/companies", json={"ticker": ticker, "legal_name": ticker}, ) if resp.status_code == 201: data = resp.json() company_id = str(data["id"]) logger.info( "Created company for ticker %s (company_id=%s)", ticker, company_id, ) elif resp.status_code == 409: # Requirement 4.6: 409 conflict — fetch existing company logger.info( "Company %s already exists (409 conflict) — fetching existing record", ticker, ) company_id = await _fetch_company_id_by_ticker(client, base, ticker) if company_id is None: logger.error( "409 conflict for %s but could not find existing company", ticker, ) return (False, "") else: logger.error( "Failed to create company for %s: HTTP %d — %s", ticker, resp.status_code, resp.text, ) return (False, "") except httpx.HTTPError as exc: logger.error("HTTP error creating company for %s: %s", ticker, exc) return (False, "") # ------------------------------------------------------------------ # 3. Create default sources (best-effort) # Requirement 4.2: market_api + news_api # ------------------------------------------------------------------ for source_type, source_name in [ ("market_api", f"{ticker} Market Data"), ("news_api", f"{ticker} News"), ]: try: resp = await client.post( f"{base}/companies/{company_id}/sources", json={ "source_type": source_type, "source_name": source_name, }, ) if resp.status_code == 201: logger.info( "Created %s source for %s (company_id=%s)", source_type, ticker, company_id, ) else: logger.warning( "Failed to create %s source for %s: HTTP %d", source_type, ticker, resp.status_code, ) except Exception: logger.warning( "Exception creating %s source for %s — skipping", source_type, ticker, exc_info=True, ) # ------------------------------------------------------------------ # 4. Add to watchlist (best-effort) # Requirement 4.3: first active watchlist, or create # "Manual Overrides" if none exist # ------------------------------------------------------------------ try: resp = await client.get(f"{base}/watchlists") watchlists = resp.json() if resp.status_code == 200 else [] active_watchlists = [w for w in watchlists if w.get("active", False)] if active_watchlists: watchlist_id = str(active_watchlists[0]["id"]) else: # Create "Manual Overrides" watchlist create_resp = await client.post( f"{base}/watchlists", json={ "name": "Manual Overrides", "description": "Auto-created watchlist for manually traded symbols", }, ) if create_resp.status_code == 201: watchlist_id = str(create_resp.json()["id"]) logger.info("Created 'Manual Overrides' watchlist (id=%s)", watchlist_id) elif create_resp.status_code == 409: # Watchlist already exists — find it in the list resp2 = await client.get(f"{base}/watchlists") wl_list = resp2.json() if resp2.status_code == 200 else [] manual_wl = next( (w for w in wl_list if w.get("name") == "Manual Overrides"), None, ) if manual_wl: watchlist_id = str(manual_wl["id"]) else: logger.warning("Could not find 'Manual Overrides' watchlist after 409") watchlist_id = None else: logger.warning( "Failed to create 'Manual Overrides' watchlist: HTTP %d", create_resp.status_code, ) watchlist_id = None if watchlist_id: member_resp = await client.post( f"{base}/watchlists/{watchlist_id}/members/{company_id}", ) if member_resp.status_code == 201: logger.info( "Added %s to watchlist %s", ticker, watchlist_id, ) elif member_resp.status_code == 409: logger.info( "%s already a member of watchlist %s", ticker, watchlist_id, ) else: logger.warning( "Failed to add %s to watchlist %s: HTTP %d", ticker, watchlist_id, member_resp.status_code, ) except Exception: logger.warning( "Exception during watchlist operations for %s — skipping", ticker, exc_info=True, ) return (True, company_id) async def _fetch_company_id_by_ticker( client: httpx.AsyncClient, base: str, ticker: str, ) -> str | None: """Fetch the company ID for a ticker from the Symbol Registry.""" try: resp = await client.get(f"{base}/companies", params={"active": "true"}) if resp.status_code == 200: for company in resp.json(): if company.get("ticker") == ticker: return str(company["id"]) except httpx.HTTPError as exc: logger.warning("Failed to fetch company by ticker %s: %s", ticker, exc) return None