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
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:
- Frontend — A new "Override" tab in the
TradingEnginepage with an order form and positions display, plus a navigation shortcut on the Trading Controls page. - Backend API — A
POST /api/trading/override/orderendpoint 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. - Broker Pipeline — The existing
broker_serviceprocesses override orders identically to autonomous ones (risk evaluation → Alpaca submission → persistence), distinguished only by asource: "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:
- Query the Symbol Registry
GET /companies?ticker={ticker}to check existence. - If not found,
POST /companiesto create the company record. - If 409 conflict (race condition), treat as success and fetch the existing company.
- Create two default sources:
market_apiandnews_api. - 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.AsyncClientfor internal HTTP calls to the Symbol Registry. - The registry base URL is derived from
services/shared/config.py(defaults tohttp://symbol-registry:8000in-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:
-
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
useSubmitOverrideOrdermutation. -
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 whenorder_typeis"limit"or"stop_limit", must be> 0.stop_price: required whenorder_typeis"stop"or"stop_limit", must be> 0.
Existing Models (No Changes)
orderstable — Override orders are stored with the same schema. Thedecision_traceJSONB column captures{"source": "manual_override", ...}.companiestable — Auto-registered companies use the existing schema withlegal_nameset to the ticker as placeholder.sourcestable — Default sources use existingmarket_apiandnews_apitypes.
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 },
);
}),