# 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.

```mermaid
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`:

```json
{
  "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`

```python
# 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`

```python
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`

```typescript
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:

```typescript
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`:

```tsx
<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`:

```typescript
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 },
  );
}),
```
