Files
stonks-oracle/.kiro/specs/override-trade-tab/design.md
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

18 KiB

Design Document: Override Trade Tab

Overview

The Override Trade Tab adds a manual order entry interface to the Stonks Oracle trading engine, enabling operators to submit buy/sell orders for any ticker — including symbols not yet tracked by the platform. The feature spans three layers:

  1. Frontend — A new "Override" tab in the TradingEngine page with an order form and positions display, plus a navigation shortcut on the Trading Controls page.
  2. Backend API — A POST /api/trading/override/order endpoint in the trading engine service that validates the order, auto-registers unknown symbols via the Symbol Registry, and enqueues the job to the Redis broker queue.
  3. Broker Pipeline — The existing broker_service processes override orders identically to autonomous ones (risk evaluation → Alpaca submission → persistence), distinguished only by a source: "manual_override" marker in the job payload.

No new database tables are required. Override orders flow through the existing orders, order_events, and risk_evaluations tables. The decision_trace JSONB column stores the override source marker for audit purposes.

sequenceDiagram
    participant Op as Operator
    participant FE as Frontend (Override Tab)
    participant TE as Trading Engine API
    participant SR as Symbol Registry
    participant RQ as Redis Broker Queue
    participant BS as Broker Service
    participant AL as Alpaca

    Op->>FE: Fill order form & submit
    FE->>TE: POST /api/trading/override/order
    TE->>TE: Validate request
    alt Ticker not in companies table
        TE->>SR: POST /companies (create company)
        SR-->>TE: 201 Created / 409 Conflict (OK)
        TE->>SR: POST /companies/{id}/sources (market_api)
        TE->>SR: POST /companies/{id}/sources (news_api)
        TE->>SR: GET /watchlists
        alt Active watchlist exists
            TE->>SR: POST /watchlists/{id}/members/{company_id}
        else No active watchlist
            TE->>SR: POST /watchlists (create "Manual Overrides")
            TE->>SR: POST /watchlists/{id}/members/{company_id}
        end
    end
    TE->>RQ: RPUSH job to stonks:queue:broker
    TE-->>FE: 202 Accepted {job_id, status: "queued"}
    FE-->>Op: Success notification
    RQ->>BS: LPOP job
    BS->>BS: Risk evaluation
    BS->>AL: Submit order
    AL-->>BS: Fill/reject
    BS->>BS: Persist to orders table

Architecture

Component Boundaries

The feature touches three existing services and the frontend:

Layer Component Changes
Frontend TradingEngine.tsx Add "Override" tab to TABS array, render OverrideTradePanel
Frontend trading/OverrideTradePanel.tsx New component: order form + positions display
Frontend Trading.tsx Add "Override Trade" navigation button
Frontend api/tradingHooks.ts Add useSubmitOverrideOrder mutation hook
Frontend test/mocks/handlers.ts Add MSW handler for override endpoint
Backend services/trading/app.py Add POST /api/trading/override/order endpoint
Backend services/trading/override.py New module: validation, auto-registration, queue enqueue logic

Tab Selection via Query Parameter

The current TradingEngine.tsx uses useState for tab selection. To support direct navigation (Requirement 1.3), the tab state will be driven by a tab search parameter using TanStack Router's useSearch / useNavigate. The URL pattern becomes /trading/engine?tab=override.

Auto-Registration Flow

The auto-registration logic lives in a new services/trading/override.py module. It calls the Symbol Registry service via HTTP (internal cluster URL) rather than directly accessing the database, maintaining service boundaries. The flow:

  1. Query the Symbol Registry GET /companies?ticker={ticker} to check existence.
  2. If not found, POST /companies to create the company record.
  3. If 409 conflict (race condition), treat as success and fetch the existing company.
  4. Create two default sources: market_api and news_api.
  5. Fetch active watchlists; add the company to the first one, or create a "Manual Overrides" watchlist if none exist.

Redis Queue Integration

The override endpoint enqueues jobs to stonks:queue:broker (the same queue used by the autonomous trading engine). The job payload matches the existing schema expected by broker_service.py:

{
  "ticker": "AAPL",
  "side": "buy",
  "quantity": 10,
  "order_type": "market",
  "limit_price": null,
  "stop_price": null,
  "source": "manual_override",
  "idempotency_key": "override-<uuid>"
}

The source field is a new addition that the broker service will pass through to decision_trace. The broker service does not need modification — it already persists the full job payload in the decision_trace JSONB column.

Components and Interfaces

Backend: Override Order Endpoint

File: services/trading/app.py

# New Pydantic model
class OverrideOrderRequest(BaseModel):
    ticker: str                    # 1-10 uppercase alpha
    side: str                      # "buy" | "sell"
    quantity: float                # > 0
    order_type: str = "market"     # "market" | "limit" | "stop" | "stop_limit"
    limit_price: Optional[float] = None
    stop_price: Optional[float] = None

class OverrideOrderResponse(BaseModel):
    job_id: str
    status: str  # "queued"
    ticker: str
    side: str
    quantity: float
    auto_registered: bool  # True if symbol was newly registered

Endpoint: POST /api/trading/override/order

  • Validates the request body (ticker format, positive quantity, conditional price fields).
  • Calls auto_register_symbol() if the ticker is untracked.
  • Enqueues the job to Redis stonks:queue:broker.
  • Returns 202 with OverrideOrderResponse.
  • Returns 422 for validation errors, 503 if Redis is unreachable.

Backend: Auto-Registration Module

File: services/trading/override.py

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: bool, company_id: str).
    Calls Symbol Registry HTTP endpoints to create company,
    default sources, and watchlist membership.
    """

Key design decisions:

  • Uses httpx.AsyncClient for internal HTTP calls to the Symbol Registry.
  • The registry base URL is derived from services/shared/config.py (defaults to http://symbol-registry:8000 in-cluster).
  • Handles 409 conflicts gracefully — fetches the existing company and proceeds.
  • Source creation failures are logged but do not block order enqueuing (best-effort).
  • Watchlist membership failures are logged but do not block order enqueuing (best-effort).

Frontend: OverrideTradePanel Component

File: frontend/src/pages/trading/OverrideTradePanel.tsx

The panel has two sections:

  1. Order Form — Controlled form with fields for ticker, side, quantity, order type, and conditional limit/stop price inputs. Client-side validation mirrors the backend rules. Submits via useSubmitOverrideOrder mutation.

  2. Positions Display — Uses the existing usePositions() hook to show current holdings in a table. Read-only context for the operator.

Frontend: API Hook

File: frontend/src/api/tradingHooks.ts

export interface OverrideOrderRequest {
  ticker: string;
  side: 'buy' | 'sell';
  quantity: number;
  order_type: 'market' | 'limit' | 'stop' | 'stop_limit';
  limit_price?: number;
  stop_price?: number;
}

export interface OverrideOrderResponse {
  job_id: string;
  status: string;
  ticker: string;
  side: string;
  quantity: number;
  auto_registered: boolean;
}

export function useSubmitOverrideOrder() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (body: OverrideOrderRequest) =>
      apiPost<OverrideOrderResponse>('trading', '/api/trading/override/order', body),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['orders'] });
      qc.invalidateQueries({ queryKey: ['positions'] });
      qc.invalidateQueries({ queryKey: ['companies'] });
    },
  });
}

Frontend: Tab Navigation Update

File: frontend/src/pages/TradingEngine.tsx

The TABS array gains an override entry. Tab state moves from useState to URL search params:

const TABS = [
  { id: 'overview', label: 'Overview' },
  { id: 'portfolio', label: 'Portfolio' },
  { id: 'history', label: 'Trade History' },
  { id: 'performance', label: 'Performance' },
  { id: 'backtest', label: 'Backtest' },
  { id: 'micro', label: 'Micro-Trading' },
  { id: 'notifications', label: 'Notifications' },
  { id: 'override', label: 'Override' },
] as const;

The active tab is read from useSearch({ from: '/trading/engine' }) and defaults to 'overview' when no tab param is present.

Frontend: Trading Controls Navigation Button

File: frontend/src/pages/Trading.tsx

A Link component inside the Trading Mode card navigates to /trading/engine?tab=override:

<Link
  to="/trading/engine"
  search={{ tab: 'override' }}
  className="rounded-md bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700"
>
  Override Trade
</Link>

Data Models

Override Order Job (Redis Queue Payload)

The job enqueued to stonks:queue:broker follows the existing broker job schema with one addition:

Field Type Required Description
ticker string yes Uppercase ticker symbol
side string yes "buy" or "sell"
quantity number yes Positive share count
order_type string yes "market", "limit", "stop", "stop_limit"
limit_price number no Required for limit/stop_limit
stop_price number no Required for stop/stop_limit
source string yes "manual_override" for override orders
idempotency_key string yes "override-{uuid}" format

Override Order Request (API)

Pydantic model OverrideOrderRequest with validators:

  • ticker: 1-10 uppercase alpha characters (regex ^[A-Z]{1,10}$), auto-uppercased.
  • side: literal "buy" or "sell".
  • quantity: float > 0.
  • order_type: literal "market", "limit", "stop", "stop_limit".
  • limit_price: required when order_type is "limit" or "stop_limit", must be > 0.
  • stop_price: required when order_type is "stop" or "stop_limit", must be > 0.

Existing Models (No Changes)

  • orders table — Override orders are stored with the same schema. The decision_trace JSONB column captures {"source": "manual_override", ...}.
  • companies table — Auto-registered companies use the existing schema with legal_name set to the ticker as placeholder.
  • sources table — Default sources use existing market_api and news_api types.

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: Ticker validation and normalization

For any string input, the ticker validation function SHALL accept it if and only if, after uppercasing, it matches the pattern ^[A-Z]{1,10}$. The normalized output SHALL always be the uppercased version of the input. All other strings (empty, longer than 10 characters, containing digits, spaces, or special characters) SHALL be rejected.

Validates: Requirements 2.2, 8.1

Property 2: Override job payload completeness

For any valid override order request (valid ticker, valid side, positive quantity, valid order type with appropriate price fields), the job payload enqueued to the broker queue SHALL contain all of: ticker (matching the request), side (matching the request), quantity (matching the request), order_type (matching the request), source equal to "manual_override", and a non-empty idempotency_key starting with "override-". If limit_price or stop_price were provided, they SHALL appear in the payload.

Validates: Requirements 3.2, 9.1

Property 3: Invalid override order rejection

For any override order request that violates at least one validation rule (invalid ticker format, side not in {"buy", "sell"}, non-positive quantity, missing limit_price when order_type is "limit" or "stop_limit", missing stop_price when order_type is "stop" or "stop_limit"), the endpoint SHALL return a 422 status code and the response body SHALL contain at least one descriptive error message.

Validates: Requirements 3.5, 2.6

Error Handling

Backend Errors

Scenario Response Behavior
Invalid request body (bad ticker, missing fields, non-positive qty) 422 Unprocessable Entity Return Pydantic validation errors as JSON
Redis broker queue unreachable 503 Service Unavailable Return {"detail": "Broker queue unavailable"}
Symbol Registry unreachable during auto-registration 202 Accepted (best-effort) Log warning, skip auto-registration, still enqueue the order. The broker will process it; the symbol just won't be tracked yet.
Symbol Registry 409 conflict on company creation Continue normally Treat as success — another request registered the symbol concurrently
Symbol Registry source/watchlist creation failure Continue normally Log warning, proceed with order enqueue. Sources and watchlist membership are best-effort.

Frontend Errors

Scenario UI Behavior
202 response Green success toast with job ID and "queued" status. Reset form.
422 response Display validation errors inline on the form fields. Do not reset form.
503 response Red error banner: "Broker service is unavailable. Please try again later."
Network error (fetch failure) Red error banner: "Unable to connect. Check your network and try again."
Positions API loading Show LoadingSpinner in positions section
Positions API empty Show "No current positions" message
Positions API error Show error message in positions section, order form remains functional

Idempotency

Override orders use a "override-{uuid}" idempotency key format. Since each submission generates a fresh UUID, duplicate prevention is handled at the broker service level (Redis + DB checks). The submit button is disabled during flight to prevent accidental double-clicks.

Testing Strategy

Unit Tests (Python — pytest)

Test Description Validates
test_override_order_validation_valid Valid order requests pass validation Req 3.1
test_override_order_validation_invalid_ticker Invalid ticker formats are rejected Req 3.5, 8.1
test_override_order_validation_missing_limit_price Limit orders without limit_price are rejected Req 3.5
test_override_order_validation_missing_stop_price Stop orders without stop_price are rejected Req 3.5
test_auto_register_new_symbol Auto-registration creates company, sources, watchlist membership Req 4.1, 4.2, 4.3
test_auto_register_existing_symbol Existing symbols skip registration Req 4.5
test_auto_register_409_conflict 409 conflict treated as success Req 4.6
test_override_enqueue_job_structure Enqueued job has correct fields and source marker Req 3.2, 9.1
test_override_endpoint_202 Valid order returns 202 with job_id Req 3.4
test_override_endpoint_422 Invalid order returns 422 Req 3.5
test_override_endpoint_503_redis_down Redis failure returns 503 Req 3.6

Property-Based Tests (Python — Hypothesis)

Property-based tests use the Hypothesis library with @settings(max_examples=100).

Test Property Tag
test_pbt_ticker_validation Property 1: Ticker validation and normalization Feature: override-trade-tab, Property 1: Ticker validation and normalization
test_pbt_override_job_payload Property 2: Override job payload completeness Feature: override-trade-tab, Property 2: Override job payload completeness
test_pbt_invalid_order_rejection Property 3: Invalid override order rejection Feature: override-trade-tab, Property 3: Invalid override order rejection

Frontend Tests (Vitest + MSW)

Test Description Validates
test override tab renders in tab bar Override tab button exists Req 1.1
test override tab shows form and positions Clicking Override tab renders both sections Req 1.2
test override tab accessible via URL param /trading/engine?tab=override opens Override tab Req 1.3
test order form fields present All form inputs render Req 2.1
test conditional price fields Limit/stop price fields show/hide based on order type Req 2.3, 2.4
test form validation errors Invalid inputs show inline errors Req 2.6
test successful order submission 202 response shows success toast, resets form Req 7.1, 7.5
test 422 error display Validation errors from backend are shown Req 7.2
test 503 error display Service unavailable message shown Req 7.3
test submit button loading state Button disabled during submission Req 7.6
test positions table renders Positions display shows data Req 6.1, 6.2
test positions loading state Loading spinner shown while fetching Req 6.3
test positions empty state Empty message when no positions Req 6.4
test override trade button on Trading page Button exists and links correctly Req 5.1, 5.2

MSW Handler

Add to frontend/src/test/mocks/handlers.ts:

http.post('/trading/api/trading/override/order', async ({ request }) => {
  const body = await request.json() as Record<string, unknown>;
  return HttpResponse.json(
    { job_id: 'job-test-123', status: 'queued', ticker: body.ticker, side: body.side, quantity: body.quantity, auto_registered: false },
    { status: 202 },
  );
}),