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:
Celes Renata
2026-04-17 07:02:30 +00:00
parent 7f67725ec8
commit 913fe8b0b3
18 changed files with 3074 additions and 17 deletions
@@ -0,0 +1 @@
{"specId": "6864b7d1-ab86-473f-b6ad-7091eaabac76", "workflowType": "requirements-first", "specType": "feature"}
+381
View File
@@ -0,0 +1,381 @@
# 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 },
);
}),
```
@@ -0,0 +1,122 @@
# Requirements Document
## Introduction
The Override Trade Tab adds a manual trading interface to the Stonks Oracle trading engine, allowing operators to buy or sell any ticker symbol — including symbols not currently tracked by the platform. When a previously untracked symbol is traded, the system automatically registers it in the companies table, creates default data sources, and adds it to the active watchlist so the full intelligence pipeline can begin tracking it. A navigation button on the Trading Controls page provides quick access to this new tab.
## Glossary
- **Trading_Engine**: The FastAPI backend service at `services/trading/app.py` that manages autonomous and manual trading operations, exposed via the `/trading/` proxy.
- **Override_Tab**: A new tab within the Trading Engine frontend page (`TradingEnginePage`) that provides a manual order entry form.
- **Symbol_Registry**: The FastAPI service at `services/symbol_registry/app.py` that manages the companies table, aliases, watchlists, and data sources.
- **Broker_Service**: The order execution worker at `services/adapters/broker_service.py` that processes order jobs from the Redis broker queue, runs risk evaluation, and submits to Alpaca.
- **Broker_Queue**: The Redis list (`stonks:queue:broker`) where order jobs are enqueued for the Broker_Service to process.
- **Order_Form**: The UI component within the Override_Tab that collects ticker, side, quantity, order type, and optional limit/stop prices from the operator.
- **Auto_Registration**: The backend process that creates a new company record, default data sources, and watchlist membership when a trade is submitted for an untracked ticker.
- **Positions_Display**: A read-only view of current broker positions shown within the Override_Tab for context.
- **Trading_Controls_Page**: The existing frontend page at `/trading` (`Trading.tsx`) that manages trading mode, risk tier, approvals, and lockouts.
## Requirements
### Requirement 1: Override Tab in Trading Engine
**User Story:** As an operator, I want a dedicated "Override" tab in the trading engine, so that I can manually submit buy and sell orders outside the autonomous trading loop.
#### Acceptance Criteria
1. THE Trading_Engine frontend SHALL display an "Override" tab in the Trading Engine page tab bar alongside existing tabs (Overview, Portfolio, Trade History, Performance, Backtest, Micro-Trading, Notifications).
2. WHEN the operator selects the Override tab, THE Override_Tab SHALL render the Order_Form and the Positions_Display.
3. THE Override_Tab SHALL be accessible via a URL query parameter or hash fragment so that external navigation links can open it directly.
### Requirement 2: Manual Order Entry Form
**User Story:** As an operator, I want to enter buy or sell orders for any ticker symbol with configurable order parameters, so that I can execute trades that the autonomous engine would not initiate on its own.
#### Acceptance Criteria
1. THE Order_Form SHALL accept the following inputs: ticker symbol (text, required), side (buy or sell, required), quantity (positive number, required), order type (market, limit, stop, or stop_limit), limit price (required when order type is limit or stop_limit), and stop price (required when order type is stop or stop_limit).
2. WHEN the operator enters a ticker symbol, THE Order_Form SHALL normalize the ticker to uppercase and validate that it contains only 1 to 10 alphabetic characters.
3. WHEN the operator selects order type "limit" or "stop_limit", THE Order_Form SHALL display the limit price input field.
4. WHEN the operator selects order type "stop" or "stop_limit", THE Order_Form SHALL display the stop price input field.
5. WHEN the operator submits the Order_Form with valid inputs, THE Override_Tab SHALL send a POST request to the Trading_Engine backend override order endpoint.
6. IF the operator submits the Order_Form with missing or invalid inputs, THEN THE Order_Form SHALL display inline validation errors and prevent submission.
### Requirement 3: Backend Override Order Endpoint
**User Story:** As the frontend, I want a backend API endpoint to accept manual override orders, so that the order can be enqueued for execution through the existing broker pipeline.
#### Acceptance Criteria
1. THE Trading_Engine SHALL expose a POST endpoint at `/api/trading/override/order` that accepts a JSON body with fields: ticker (string), side (string: "buy" or "sell"), quantity (number), order_type (string: "market", "limit", "stop", or "stop_limit"), limit_price (optional number), and stop_price (optional number).
2. WHEN the endpoint receives a valid order request, THE Trading_Engine SHALL enqueue a job on the Broker_Queue with the order parameters, a generated idempotency key, and the source marked as "manual_override".
3. WHEN the endpoint receives a valid order request for a ticker that does not exist in the companies table, THE Trading_Engine SHALL invoke the Auto_Registration process before enqueuing the order.
4. WHEN the endpoint successfully enqueues the order, THE Trading_Engine SHALL return a 202 response with the generated order job ID and a status of "queued".
5. IF the endpoint receives an invalid request (missing required fields, invalid ticker format, non-positive quantity, missing limit_price for limit orders), THEN THE Trading_Engine SHALL return a 422 response with descriptive validation errors.
6. IF the Broker_Queue is unreachable, THEN THE Trading_Engine SHALL return a 503 response with an error message indicating the broker queue is unavailable.
### Requirement 4: Auto-Registration of Untracked Symbols
**User Story:** As an operator, I want the system to automatically register a new symbol when I trade it for the first time, so that the platform begins tracking, assessing, and monitoring the new symbol without manual setup.
#### Acceptance Criteria
1. WHEN the Trading_Engine receives an override order for a ticker not present in the companies table, THE Auto_Registration process SHALL create a new company record in the companies table with the ticker, a legal name set to the ticker as a placeholder, and active status set to true.
2. WHEN the Auto_Registration process creates a new company, THE Auto_Registration process SHALL create default data sources for the company: a "market_api" source for price data and a "news_api" source for news coverage.
3. WHEN the Auto_Registration process creates a new company, THE Auto_Registration process SHALL add the company to the first active watchlist (or create a "Manual Overrides" watchlist if none exists).
4. THE Auto_Registration process SHALL use the Symbol_Registry service endpoints to create the company, sources, and watchlist membership.
5. IF the ticker already exists in the companies table, THEN THE Auto_Registration process SHALL skip registration and proceed with order enqueuing.
6. IF the Symbol_Registry returns a 409 conflict (company already exists due to a race condition), THEN THE Auto_Registration process SHALL treat the conflict as a successful registration and proceed with order enqueuing.
### Requirement 5: Navigation from Trading Controls Page
**User Story:** As an operator, I want a button on the Trading Controls page that navigates to the Override tab in the trading engine, so that I can quickly access manual trading from the controls dashboard.
#### Acceptance Criteria
1. THE Trading_Controls_Page SHALL display a navigation button labeled "Override Trade" within the Trading Mode card section.
2. WHEN the operator clicks the "Override Trade" button, THE Trading_Controls_Page SHALL navigate to the Trading Engine page with the Override tab selected.
3. THE navigation button SHALL use the TanStack Router Link component for client-side navigation.
### Requirement 6: Current Positions Display
**User Story:** As an operator, I want to see my current positions when placing an override trade, so that I have context about existing holdings before buying or selling.
#### Acceptance Criteria
1. THE Override_Tab SHALL display a table of current positions fetched from the existing positions API endpoint (`/api/positions`).
2. THE Positions_Display SHALL show for each position: ticker, quantity, average entry price, current price, and unrealized P&L.
3. WHEN the positions data is loading, THE Positions_Display SHALL show a loading indicator.
4. IF no positions exist, THEN THE Positions_Display SHALL show a message indicating no current positions.
### Requirement 7: Order Submission Feedback
**User Story:** As an operator, I want clear feedback after submitting an override order, so that I know whether the order was accepted, queued, or rejected.
#### Acceptance Criteria
1. WHEN the Trading_Engine backend returns a 202 response, THE Override_Tab SHALL display a success notification with the order job ID and "queued" status.
2. WHEN the Trading_Engine backend returns a 422 response, THE Override_Tab SHALL display the validation error messages from the response body.
3. WHEN the Trading_Engine backend returns a 503 response, THE Override_Tab SHALL display an error message indicating the broker service is unavailable.
4. IF a network error occurs during order submission, THEN THE Override_Tab SHALL display a generic connectivity error message.
5. WHEN an order is successfully submitted, THE Override_Tab SHALL reset the Order_Form fields to their default values.
6. THE Override_Tab SHALL disable the submit button and show a loading state while the order request is in flight to prevent duplicate submissions.
### Requirement 8: Error Handling for Invalid Tickers
**User Story:** As an operator, I want the system to handle invalid or non-existent tickers gracefully, so that I receive clear feedback when a trade cannot be processed.
#### Acceptance Criteria
1. WHEN the operator enters a ticker that does not match the pattern of 1 to 10 uppercase alphabetic characters, THE Order_Form SHALL display a validation error before submission.
2. IF the Broker_Service rejects the order because the broker (Alpaca) does not recognize the ticker, THEN THE Broker_Service SHALL persist the order with a "rejected" status and the rejection reason.
3. THE operator SHALL be able to view rejected override orders in the existing Trade History tab with the rejection reason visible.
### Requirement 9: Override Order Audit Trail
**User Story:** As an operator, I want override orders to be fully auditable, so that I can distinguish manual trades from autonomous ones and review the decision history.
#### Acceptance Criteria
1. WHEN the Trading_Engine enqueues an override order, THE order job SHALL include a `source` field set to "manual_override" to distinguish it from autonomous orders.
2. THE Broker_Service SHALL persist override orders in the orders table with the same schema as autonomous orders, including the decision_trace field containing the override source marker.
3. WHEN the operator views order details for an override order, THE order detail view SHALL display the "manual_override" source in the decision trace.
+153
View File
@@ -0,0 +1,153 @@
# Implementation Plan: Override Trade Tab
## Overview
Add a manual order entry interface to the Stonks Oracle trading engine. The implementation spans backend (FastAPI endpoint + auto-registration module), frontend (Override tab with order form and positions display, navigation button, API hook), MSW test handlers, property-based tests for the 3 correctness properties, and frontend component tests. Each task builds incrementally, wiring components together as they are created.
## Tasks
- [x] 1. Backend: Override order validation model and endpoint
- [x] 1.1 Add `OverrideOrderRequest` and `OverrideOrderResponse` Pydantic models to `services/trading/app.py`
- `OverrideOrderRequest` with fields: `ticker` (str), `side` (Literal["buy","sell"]), `quantity` (float > 0), `order_type` (Literal["market","limit","stop","stop_limit"], default "market"), `limit_price` (Optional[float]), `stop_price` (Optional[float])
- Add a `@field_validator("ticker")` that uppercases and validates against `^[A-Z]{1,10}$`
- Add a `@model_validator(mode="after")` that enforces `limit_price` required when order_type is "limit" or "stop_limit", and `stop_price` required when order_type is "stop" or "stop_limit"
- `OverrideOrderResponse` with fields: `job_id` (str), `status` (str), `ticker` (str), `side` (str), `quantity` (float), `auto_registered` (bool)
- _Requirements: 2.1, 2.2, 3.1, 3.5_
- [x] 1.2 Add `POST /api/trading/override/order` endpoint to `services/trading/app.py`
- Accept `OverrideOrderRequest` body
- Call `auto_register_symbol()` from `services/trading/override.py` if ticker is untracked (check via Symbol Registry `GET /companies?ticker=...`)
- Generate idempotency key as `"override-{uuid4()}"`
- Build job payload with `ticker`, `side`, `quantity`, `order_type`, `limit_price`, `stop_price`, `source: "manual_override"`, and `idempotency_key`
- Enqueue job to Redis `stonks:queue:broker` via `RPUSH`
- Return 202 with `OverrideOrderResponse`
- Return 503 if Redis is unreachable
- Use `engine.redis` for Redis access and `config` for registry base URL
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 9.1_
- [x] 2. Backend: Auto-registration module
- [x] 2.1 Create `services/trading/override.py` with `auto_register_symbol()` function
- Use `httpx.AsyncClient` to call Symbol Registry HTTP endpoints
- Check if ticker exists via `GET /companies?ticker={ticker}`
- If not found, `POST /companies` to create company (legal_name = ticker, active = true)
- Handle 409 conflict gracefully (fetch existing company and proceed)
- Create two default sources: `market_api` and `news_api` via `POST /companies/{id}/sources`
- Fetch active watchlists via `GET /watchlists`; add company to first active watchlist, or create "Manual Overrides" watchlist if none exist
- Source and watchlist failures are logged but do not block order enqueuing (best-effort)
- Return `(auto_registered: bool, company_id: str)`
- Registry base URL derived from env var or defaults to `http://symbol-registry:8000`
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6_
- [x] 2.2 Write unit tests for `auto_register_symbol()` in `tests/test_override.py`
- Mock httpx calls to Symbol Registry
- Test new symbol registration (company + sources + watchlist)
- Test existing symbol skip
- Test 409 conflict handling
- Test source/watchlist failure tolerance
- _Requirements: 4.1, 4.2, 4.3, 4.5, 4.6_
- [x] 3. Backend: Override endpoint unit tests and property-based tests
- [x] 3.1 Write unit tests for override endpoint in `tests/test_override.py`
- Test valid order returns 202 with correct response shape
- Test invalid ticker returns 422
- Test missing limit_price for limit order returns 422
- Test missing stop_price for stop order returns 422
- Test non-positive quantity returns 422
- Test enqueued job has correct structure and `source: "manual_override"`
- _Requirements: 3.1, 3.2, 3.4, 3.5, 9.1_
- [x] 3.2 Write property test for ticker validation and normalization in `tests/test_pbt_override.py`
- **Property 1: Ticker validation and normalization**
- Use Hypothesis `@settings(max_examples=100)` to generate arbitrary strings
- Assert: after uppercasing, accepted iff matches `^[A-Z]{1,10}$`; normalized output is always uppercased input
- **Validates: Requirements 2.2, 8.1**
- [x] 3.3 Write property test for override job payload completeness in `tests/test_pbt_override.py`
- **Property 2: Override job payload completeness**
- Use Hypothesis to generate valid override order requests
- Assert: enqueued payload contains all required fields, `source == "manual_override"`, `idempotency_key` starts with `"override-"`
- **Validates: Requirements 3.2, 9.1**
- [x] 3.4 Write property test for invalid override order rejection in `tests/test_pbt_override.py`
- **Property 3: Invalid override order rejection**
- Use Hypothesis to generate orders violating at least one validation rule
- Assert: endpoint returns 422 with at least one descriptive error message
- **Validates: Requirements 3.5, 2.6**
- [x] 4. Checkpoint — Backend tests
- Ensure all backend tests pass with `.venv/bin/python -m pytest tests/test_override.py tests/test_pbt_override.py -x --tb=short -q`, ask the user if questions arise.
- [x] 5. Frontend: API hook for submitting override orders
- [x] 5.1 Add `useSubmitOverrideOrder` mutation hook to `frontend/src/api/tradingHooks.ts`
- Define `OverrideOrderRequest` interface: `ticker`, `side` ("buy"|"sell"), `quantity`, `order_type` ("market"|"limit"|"stop"|"stop_limit"), optional `limit_price`, optional `stop_price`
- Define `OverrideOrderResponse` interface: `job_id`, `status`, `ticker`, `side`, `quantity`, `auto_registered`
- Use `apiPost<OverrideOrderResponse>('trading', '/api/trading/override/order', body)`
- On success, invalidate `orders`, `positions`, and `companies` query keys
- _Requirements: 2.5, 3.4, 7.1_
- [x] 6. Frontend: OverrideTradePanel component
- [x] 6.1 Create `frontend/src/pages/trading/OverrideTradePanel.tsx`
- Order form section with controlled inputs: ticker (text, auto-uppercase), side (buy/sell toggle), quantity (number), order type (select: market/limit/stop/stop_limit), conditional limit_price and stop_price fields
- Client-side validation: ticker 1-10 alpha chars, positive quantity, required price fields based on order type
- Inline validation error display on invalid inputs
- Submit via `useSubmitOverrideOrder` mutation
- Success: show green toast/banner with job_id and "queued" status, reset form
- 422 error: display validation errors inline
- 503 error: show "Broker service is unavailable" banner
- Network error: show connectivity error banner
- Submit button disabled during flight with loading state
- Positions display section using existing `usePositions()` hook from `api/hooks.ts`
- Positions table showing: ticker, quantity, avg entry price, current price, unrealized P&L
- Loading spinner while positions load, "No current positions" message when empty
- _Requirements: 1.2, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 6.1, 6.2, 6.3, 6.4, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 8.1_
- [x] 7. Frontend: Override tab integration in TradingEnginePage
- [x] 7.1 Update `frontend/src/pages/TradingEngine.tsx` to add Override tab
- Add `{ id: 'override', label: 'Override' }` to the TABS array
- Move tab state from `useState` to URL search params using TanStack Router `useSearch` / `useNavigate` for the `/trading/engine` route
- Default to `'overview'` when no `tab` param is present
- Import and render `OverrideTradePanel` when `activeTab === 'override'`
- Update the route definition in `frontend/src/routes.tsx` to accept `tab` search param via `validateSearch`
- _Requirements: 1.1, 1.2, 1.3_
- [x] 8. Frontend: Navigation button on Trading Controls page
- [x] 8.1 Add "Override Trade" button to `frontend/src/pages/Trading.tsx`
- Add a TanStack Router `Link` component inside the Trading Mode card section
- Navigate to `/trading/engine` with search param `{ tab: 'override' }`
- Style with `rounded-md bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700`
- _Requirements: 5.1, 5.2, 5.3_
- [x] 9. Frontend: MSW handlers for testing
- [x] 9.1 Add MSW handler for `POST /trading/api/trading/override/order` in `frontend/src/test/mocks/handlers.ts`
- Return 202 with mock `OverrideOrderResponse` containing `job_id`, `status: "queued"`, echoed `ticker`, `side`, `quantity`, and `auto_registered: false`
- _Requirements: 3.4, 7.1_
- [x] 10. Frontend: Component and integration tests
- [x] 10.1 Write frontend tests for Override tab in `frontend/src/test/override.test.tsx`
- Test override tab renders in tab bar
- Test override tab shows form and positions sections
- Test override tab accessible via URL param `?tab=override`
- Test order form fields are present (ticker, side, quantity, order type)
- Test conditional price fields show/hide based on order type
- Test form validation errors for invalid inputs
- Test successful order submission shows success message and resets form
- Test 422 error display
- Test submit button loading state during submission
- Test positions table renders with mock data
- Test positions loading state
- Test positions empty state
- Test "Override Trade" button on Trading page exists and links correctly
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.3, 2.4, 2.6, 5.1, 5.2, 6.1, 6.2, 6.3, 6.4, 7.1, 7.2, 7.5, 7.6_
- [x] 11. Final checkpoint
- Ensure all tests pass: backend with `.venv/bin/python -m pytest tests/test_override.py tests/test_pbt_override.py -x --tb=short -q` and frontend with `cd frontend && npx vitest --run`, ask the user if questions arise.
## Notes
- Tasks marked with `*` are optional and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Checkpoints ensure incremental validation
- Property tests validate universal correctness properties from the design document
- Unit tests validate specific examples and edge cases
- The broker_service does not need modification — it already processes the full job payload and persists `decision_trace` JSONB
- No new database migrations are required; override orders use existing `orders`, `order_events`, and `risk_evaluations` tables
+9 -8
View File
@@ -1,12 +1,13 @@
# Development Process — Test-Develop-Debug
## Local Environment
- Python 3.12 via NixOS, virtualenv at `.venv/`
- Ubuntu dev machine, Python 3.12, virtualenv at `.venv/`
- Always use `.venv/bin/python` or activate with `source .venv/bin/activate` before running Python commands
- For tools not available in `.venv/` (ruff, gh, etc.), use `nix-shell -p <pkg> --run "<cmd>"`
- Node.js 24 for frontend; `frontend/` has its own `node_modules/`
- Frontend tests: `cd frontend && npx vitest --run`
- Python tests: `nix-shell -p ruff --run "ruff check services/"` then `python -m pytest tests/ -x --tb=short -q`
- Node.js 24 via nvm — always load nvm before running Node/npm/npx commands:
`export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use 24`
- For tools not available in `.venv/` (ruff, gh, etc.), install via pip or apt as needed
- Frontend tests: load nvm first, then `cd frontend && npx vitest --run`
- Python tests: `.venv/bin/ruff check services/` then `.venv/bin/python -m pytest tests/ -x --tb=short -q`
## Workflow
1. Write or update tests for the target behavior
@@ -22,7 +23,7 @@
- Frontend: Vitest + MSW (Mock Service Worker) for deterministic API mocking, tests in `frontend/src/test/`
- Run Python tests: `python -m pytest tests/ -x --tb=short -q`
- Run frontend tests: `cd frontend && npx vitest --run`
- Lint Python: `nix-shell -p ruff --run "ruff check services/"` (or `.venv/bin/ruff check services/`)
- Lint Python: `.venv/bin/ruff check services/`
- Ruff is pinned to `ruff==0.15.10` in `requirements.txt` — CI uses the same version
- Ruff config: `ruff.toml` with `known-first-party = ["services"]` for consistent import sorting
- Pre-existing test failures (not regressions): `test_extractor_prompts.py`, `test_extractor_schemas.py`, `test_filings_adapter.py`, `test_ollama_client.py`
@@ -36,8 +37,8 @@
- `build-dashboard`: frontend/Dockerfile → GHCR (TypeScript strict mode — catches unused imports)
- `build-superset`: docker/Dockerfile.superset → GHCR
- CI handles all image builds and pushes — do NOT manually docker push
- Check CI: `nix-shell -p gh --run "gh run list -L 3"` or `gh run list -L 3`
- Re-run failed: `nix-shell -p gh --run "gh run rerun <id> --failed"`
- Check CI: `gh run list -L 3`
- Re-run failed: `gh run rerun <id> --failed`
- View failure logs: `gh run view <id> --log-failed`
## Deploy
+1
View File
@@ -19,6 +19,7 @@ fileMatchPattern: "frontend/**"
## Testing
- Vitest + MSW (Mock Service Worker) for deterministic tests
- Requires Node.js 24 via nvm — load before running: `export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use 24`
- Test setup: `src/test/setup.ts` starts MSW server
- Mock handlers: `src/test/mocks/handlers.ts`
- Test helper: `src/test/render.tsx` provides `renderRoute(path)` with QueryClient + Router
+5 -4
View File
@@ -15,10 +15,11 @@ Three-layer signal aggregation engine:
- Seed script: `python -m services.symbol_registry.seed`
## Local Dev Environment
- NixOS dev environment, Python 3.12
- Ubuntu dev machine, Python 3.12
- Virtual environment at `.venv/` — always use it for Python commands
- For tools not in `.venv/` (like `ruff`, `gh`), use `nix-shell -p <pkg> --run "<cmd>"`
- Node.js 24 for frontend (`frontend/` directory)
- Node.js 24 via nvm — always load nvm before running Node/npm commands:
`export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use 24`
- For tools not in `.venv/` (like `ruff`, `gh`), install via pip or apt as needed
- Docker available locally for image builds (but let CI handle pushes)
## Live Endpoints
@@ -44,7 +45,7 @@ Three-layer signal aggregation engine:
- Superset image: `docker/Dockerfile.superset` (apache/superset + trino + psycopg2)
- Python service images: `docker/Dockerfile` with `SERVICE_CMD` build arg
- Let CI handle image builds and pushes — do NOT manually `docker build && docker push`
- Check CI status: `nix-shell -p gh --run "gh run list -L 3"`
- Check CI status: `gh run list -L 3`
## Deployment Scripts
- `~/sources/kube/stonks-oracle/runmefirst.sh` — full deploy: DB setup, migrations, Helm install, rolling restart (runs from gremlin-1 at 192.168.42.254 where secrets are available)