Files
stonks-oracle/services/trading/override.py
T
Celes Renata 913fe8b0b3 feat: override trade tab — manual order entry with auto-registration
Backend:
- OverrideOrderRequest/Response Pydantic models with ticker, quantity, price validators
- POST /api/trading/override/order endpoint (enqueue to Redis broker queue)
- auto_register_symbol() module for untracked ticker registration via Symbol Registry
- Unit tests (17) and property-based tests (3 x 100 examples)

Frontend:
- OverrideTradePanel component (order form + positions display)
- Override tab in TradingEngine page with URL search param navigation
- Override Trade button on Trading Controls page
- useSubmitOverrideOrder mutation hook
- MSW handler and 13 component/integration tests

Steering:
- Updated steering docs for Ubuntu dev machine with nvm/Node 24
2026-04-17 07:02:30 +00:00

236 lines
9.2 KiB
Python

"""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