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
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user