fix: operator approval workflow — add approval toggle, lockout CRUD, and PBT tests
- Add GET/PUT /api/admin/trading/approval-config endpoints - Add POST/DELETE /api/admin/trading/lockouts endpoints - Add useApprovalConfig, useUpdateApprovalConfig, useCreateLockout, useDeleteLockout hooks - Add Paper Order Approval toggle card with confirmation dialog - Add lockout creation form and delete button to Active Lockouts card - Add MSW handlers for all new endpoints - Add property-based tests for bug condition exploration and preservation
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"specId": "4d1be766-7daa-4032-b0e9-4f6124370ccd", "workflowType": "requirements-first", "specType": "bugfix"}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Bugfix Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
The Trading Controls page at `/trading` has two dead sections — "Pending Approvals" and "Active Lockouts" — that display data from the API but provide no way to actually trigger the underlying workflows. The operator approval system is fully implemented in the backend (`services/risk/approval.py`, `services/adapters/broker_service.py`) with an approval gate in the order processing pipeline, but it is effectively disconnected because:
|
||||
|
||||
1. The `requires_approval()` function returns `False` in paper mode (the default) since `auto_approve_paper` defaults to `True`, and the risk config stored in the database never overrides this default. There is no UI control to toggle approval requirements for paper mode.
|
||||
2. The lockout system has a database table (`symbol_lockouts`) and a read-only API endpoint (`GET /api/admin/trading/lockouts`), but there is no API endpoint to create a manual lockout and no UI form to do so.
|
||||
|
||||
The result is that the operator can never exercise "human calls the final shot" control — orders bypass approval silently, and lockouts can never be created manually.
|
||||
|
||||
## Bug Analysis
|
||||
|
||||
### Current Behavior (Defect)
|
||||
|
||||
1.1 WHEN the system is in paper trading mode (the default) THEN the `requires_approval()` function always returns `False` because `OperatorApproval.auto_approve_paper` defaults to `True` and the risk config JSON in the database does not include operator approval settings, causing all orders to bypass the approval gate in `process_order_job()`
|
||||
|
||||
1.2 WHEN the operator views the Trading Controls page THEN the "Pending Approvals" section always shows "(0) — No pending approvals" because no approval requests are ever created due to defect 1.1
|
||||
|
||||
1.3 WHEN the operator wants to manually lock out a ticker from trading THEN there is no API endpoint to create a lockout entry in the `symbol_lockouts` table, so the "Active Lockouts" section is permanently empty unless lockouts are created by internal system processes
|
||||
|
||||
1.4 WHEN the operator views the Active Lockouts section on the Trading Controls page THEN there is no UI control to create a new manual lockout or to remove an existing lockout early
|
||||
|
||||
1.5 WHEN the operator wants to enable or disable the approval requirement for paper mode THEN there is no UI control on the Trading Controls page to toggle the `auto_approve_paper` setting in the risk config
|
||||
|
||||
### Expected Behavior (Correct)
|
||||
|
||||
2.1 WHEN the system is in paper trading mode and the operator has disabled `auto_approve_paper` via the UI THEN the `requires_approval()` function SHALL return `True`, causing orders to be held for operator approval before broker submission
|
||||
|
||||
2.2 WHEN `requires_approval()` returns `True` for the current trading mode THEN orders processed by `process_order_job()` SHALL create approval requests visible in the "Pending Approvals" section, and the operator SHALL be able to approve or reject them using the existing approve/reject buttons
|
||||
|
||||
2.3 WHEN the operator submits a manual lockout via the Trading Controls page (specifying ticker, reason, and duration) THEN the system SHALL create a new entry in the `symbol_lockouts` table via a new API endpoint, and the lockout SHALL appear in the "Active Lockouts" section
|
||||
|
||||
2.4 WHEN the operator views the Active Lockouts section THEN each lockout entry SHALL have a control to remove (cancel) the lockout early, and there SHALL be a form to create a new manual lockout
|
||||
|
||||
2.5 WHEN the operator toggles the approval requirement setting on the Trading Controls page THEN the system SHALL update the `auto_approve_paper` field in the active risk config's JSON, and the change SHALL take effect for subsequent orders without requiring a service restart
|
||||
|
||||
### Unchanged Behavior (Regression Prevention)
|
||||
|
||||
3.1 WHEN the system is in live trading mode with `require_approval_for_live` set to `True` (the default) THEN the system SHALL CONTINUE TO require operator approval for live orders as it does today
|
||||
|
||||
3.2 WHEN the system is in disabled trading mode THEN the `requires_approval()` function SHALL CONTINUE TO return `False` because orders are blocked upstream by the risk engine
|
||||
|
||||
3.3 WHEN the operator approves or rejects a pending approval via the existing approve/reject buttons THEN the system SHALL CONTINUE TO update the approval status in the `operator_approvals` table via the existing `PUT /api/admin/trading/approvals/{id}` endpoint
|
||||
|
||||
3.4 WHEN a pending approval expires (exceeds `approval_timeout_minutes`) THEN the system SHALL CONTINUE TO mark it as expired via the existing `expire_stale_approvals()` function
|
||||
|
||||
3.5 WHEN the risk engine evaluates an order and it fails risk checks THEN the order SHALL CONTINUE TO be rejected before reaching the approval gate, preserving the existing risk evaluation pipeline
|
||||
|
||||
3.6 WHEN system-generated lockouts are created by internal processes (news-shock, cooldown) THEN those lockouts SHALL CONTINUE TO function as they do today, unaffected by the new manual lockout feature
|
||||
@@ -0,0 +1,211 @@
|
||||
# Operator Approval Workflow Bugfix Design
|
||||
|
||||
## Overview
|
||||
|
||||
The operator approval system is fully implemented in the backend (`services/risk/approval.py`, `services/adapters/broker_service.py`) but is effectively disconnected from the operator's control. The `requires_approval()` function always returns `False` in paper mode because `OperatorApproval.auto_approve_paper` defaults to `True` and the `config` JSONB column in `risk_configs` is initialized as `{}`, which means `PortfolioRiskConfig.from_db_json({})` always produces default values that auto-approve paper orders. Additionally, the lockout system lacks a creation endpoint and UI controls.
|
||||
|
||||
The fix connects the existing approval pipeline to the UI by: (1) adding a toggle to control `auto_approve_paper` in the risk config JSON, (2) adding a POST endpoint and UI form for manual symbol lockouts, and (3) adding a DELETE endpoint and UI control for early lockout removal.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Bug_Condition (C)**: The condition where `requires_approval()` returns `False` in paper mode because `auto_approve_paper` defaults to `True` and the risk config JSON never overrides it — causing all paper orders to bypass the approval gate silently
|
||||
- **Property (P)**: When the operator has disabled `auto_approve_paper` via the UI, `requires_approval()` returns `True` for paper mode, causing orders to be held for approval; when a manual lockout is created via the new endpoint, it appears in the Active Lockouts section
|
||||
- **Preservation**: Existing live-mode approval behavior, disabled-mode bypass, approval review/expiry workflows, risk engine rejection pipeline, and system-generated lockouts must remain unchanged
|
||||
- **`requires_approval()`**: Function in `services/risk/approval.py` that determines whether an order needs operator approval based on trading mode and config
|
||||
- **`process_order_job()`**: Function in `services/adapters/broker_service.py` that processes broker queue jobs, running risk evaluation and the approval gate
|
||||
- **`load_risk_config()`**: Function in `services/adapters/broker_service.py` that loads `PortfolioRiskConfig` from the `risk_configs.config` JSONB column
|
||||
- **`OperatorApproval`**: Pydantic model in `services/risk/engine.py` with `auto_approve_paper` (default `True`) and `require_approval_for_live` (default `True`)
|
||||
- **`risk_configs`**: PostgreSQL table (migration 010) storing the active risk configuration as a JSONB `config` column
|
||||
|
||||
## Bug Details
|
||||
|
||||
### Bug Condition
|
||||
|
||||
The bug manifests when the system is in paper trading mode (the default). The `requires_approval()` function returns `False` because `OperatorApproval.auto_approve_paper` defaults to `True`, and the `risk_configs.config` JSONB column is initialized as `{}` — meaning `PortfolioRiskConfig.from_db_json({})` always produces a config with `auto_approve_paper=True`. There is no UI control to set this field to `False`, so the approval gate in `process_order_job()` is never reached for paper orders.
|
||||
|
||||
A secondary condition is that the `symbol_lockouts` table has no POST endpoint, so manual lockouts cannot be created, and the Active Lockouts section on the Trading page is permanently empty unless internal processes create lockouts.
|
||||
|
||||
**Formal Specification:**
|
||||
```
|
||||
FUNCTION isBugCondition(input)
|
||||
INPUT: input of type { trading_mode: TradingMode, risk_config_json: dict }
|
||||
OUTPUT: boolean
|
||||
|
||||
config := PortfolioRiskConfig.from_db_json(risk_config_json)
|
||||
|
||||
RETURN (
|
||||
trading_mode == PAPER
|
||||
AND config.operator_approval.auto_approve_paper == True
|
||||
AND "operator_approval" NOT IN risk_config_json
|
||||
)
|
||||
OR (
|
||||
input.action == "create_manual_lockout"
|
||||
AND NOT exists(POST_endpoint_for_symbol_lockouts)
|
||||
)
|
||||
END FUNCTION
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
- **Paper mode, empty config JSON**: `risk_configs.config = {}`, `trading_mode = 'paper'` → `requires_approval()` returns `False` → orders bypass approval silently. **Expected**: operator should be able to toggle `auto_approve_paper` to `False` so `requires_approval()` returns `True`.
|
||||
- **Paper mode, config with operator_approval**: `risk_configs.config = {"operator_approval": {"auto_approve_paper": false}}` → `requires_approval()` returns `True` → orders held for approval. **Expected**: this is the correct behavior, but currently unreachable because no UI sets this field.
|
||||
- **Manual lockout attempt**: Operator wants to lock out AAPL for 60 minutes → no POST endpoint exists → lockout cannot be created. **Expected**: `POST /api/admin/trading/lockouts` creates a row in `symbol_lockouts`.
|
||||
- **Lockout removal**: Operator wants to cancel an active lockout early → no DELETE endpoint exists → lockout persists until expiry. **Expected**: `DELETE /api/admin/trading/lockouts/{id}` removes the lockout.
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
### Preservation Requirements
|
||||
|
||||
**Unchanged Behaviors:**
|
||||
- Live mode with `require_approval_for_live=True` must continue to require operator approval for live orders
|
||||
- Disabled mode must continue to return `False` from `requires_approval()` (orders blocked upstream)
|
||||
- The existing `PUT /api/admin/trading/approvals/{id}` endpoint must continue to approve/reject pending approvals
|
||||
- The `expire_stale_approvals()` function must continue to mark expired approvals
|
||||
- Orders that fail risk checks must continue to be rejected before reaching the approval gate
|
||||
- System-generated lockouts (news-shock, cooldown) must continue to function unaffected by the new manual lockout feature
|
||||
- Mouse clicks, trading mode switches, risk tier changes, and all other Trading page controls must continue to work
|
||||
|
||||
**Scope:**
|
||||
All inputs that do NOT involve the new `auto_approve_paper` toggle, manual lockout creation, or lockout deletion should be completely unaffected by this fix. This includes:
|
||||
- Live mode approval behavior (already working)
|
||||
- Risk evaluation pipeline
|
||||
- Order idempotency and duplicate prevention
|
||||
- Position sync and broker account registration
|
||||
- All other Trading page controls (mode, risk tier, macro toggle, competitive toggle, paper reset)
|
||||
|
||||
## Hypothesized Root Cause
|
||||
|
||||
Based on the bug description and code analysis, the root causes are:
|
||||
|
||||
1. **Missing UI control for `auto_approve_paper`**: The `OperatorApproval.auto_approve_paper` field defaults to `True` in the Pydantic model. The `risk_configs.config` JSONB column is initialized as `{}` by the `set_trading_mode` endpoint. Since `PortfolioRiskConfig.from_db_json({})` fills all defaults, `auto_approve_paper` is always `True`. The Trading page has no toggle to update this field in the config JSON.
|
||||
|
||||
2. **`update_trading_config` does full JSON replace**: The existing `PUT /api/admin/trading/config` endpoint replaces the entire `config` JSONB column. A new UI control needs to read the current config, merge the `operator_approval` settings, and write back the full config — or a dedicated endpoint should handle the toggle.
|
||||
|
||||
3. **Missing POST endpoint for `symbol_lockouts`**: The `GET /api/admin/trading/lockouts` endpoint exists for reading active lockouts, but there is no `POST` endpoint to create a manual lockout entry. The table schema supports it (no constraints preventing manual entries), but the API route is missing.
|
||||
|
||||
4. **Missing DELETE endpoint for `symbol_lockouts`**: There is no endpoint to remove a lockout early. The only way a lockout expires is by time (`expires_at > NOW()`).
|
||||
|
||||
5. **Missing frontend form and controls**: The Trading page renders lockout data read-only and has no form for creating lockouts or toggling approval settings.
|
||||
|
||||
## Correctness Properties
|
||||
|
||||
Property 1: Bug Condition - Paper Mode Approval Toggle
|
||||
|
||||
_For any_ `PortfolioRiskConfig` where `trading_mode` is `PAPER` and `operator_approval.auto_approve_paper` is `False`, the `requires_approval()` function SHALL return `True`, causing orders to be held for operator approval.
|
||||
|
||||
**Validates: Requirements 2.1, 2.2**
|
||||
|
||||
Property 2: Preservation - Non-Paper-Toggle Approval Behavior
|
||||
|
||||
_For any_ `PortfolioRiskConfig` where `trading_mode` is `LIVE` or `DISABLED`, the `requires_approval()` function SHALL produce the same result as the original function: `True` when `trading_mode` is `LIVE` and `require_approval_for_live` is `True`, and `False` when `trading_mode` is `DISABLED`. This preserves all existing approval behavior for non-paper modes.
|
||||
|
||||
**Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5**
|
||||
|
||||
## Fix Implementation
|
||||
|
||||
### Changes Required
|
||||
|
||||
Assuming our root cause analysis is correct:
|
||||
|
||||
**File**: `services/api/app.py`
|
||||
|
||||
**New Endpoints**:
|
||||
1. **`PUT /api/admin/trading/approval-config`**: Dedicated endpoint to update `operator_approval` settings in the risk config JSON. Reads the current config, merges the `operator_approval` sub-object, and writes back. Accepts `{ auto_approve_paper: bool }`.
|
||||
2. **`GET /api/admin/trading/approval-config`**: Returns the current `operator_approval` settings from the active risk config.
|
||||
3. **`POST /api/admin/trading/lockouts`**: Creates a manual lockout entry in `symbol_lockouts`. Accepts `{ ticker: string, reason: string, duration_minutes: int, lockout_type?: string }`. Computes `expires_at` from `NOW() + duration_minutes`.
|
||||
4. **`DELETE /api/admin/trading/lockouts/{lockout_id}`**: Deletes a lockout entry by ID, allowing early removal.
|
||||
|
||||
**File**: `frontend/src/api/hooks.ts`
|
||||
|
||||
**New Hooks**:
|
||||
1. **`useApprovalConfig()`**: Fetches current approval config via `GET /api/admin/trading/approval-config`.
|
||||
2. **`useUpdateApprovalConfig()`**: Mutation to update approval config via `PUT /api/admin/trading/approval-config`. Invalidates `['approval-config']` and `['trading-config']` query keys.
|
||||
3. **`useCreateLockout()`**: Mutation to create a manual lockout via `POST /api/admin/trading/lockouts`. Invalidates `['lockouts']` query key.
|
||||
4. **`useDeleteLockout()`**: Mutation to delete a lockout via `DELETE /api/admin/trading/lockouts/{id}`. Invalidates `['lockouts']` query key.
|
||||
|
||||
**File**: `frontend/src/pages/Trading.tsx`
|
||||
|
||||
**UI Changes**:
|
||||
1. **Approval Toggle Card**: New card between the existing controls and Pending Approvals section. Contains a toggle switch for `auto_approve_paper` with a confirmation dialog (since enabling approval requirements changes order flow). Shows current state and last-changed timestamp.
|
||||
2. **Lockout Creation Form**: Add a form to the Active Lockouts card with fields for ticker (text input), reason (text input), and duration in minutes (number input). Submit button calls `useCreateLockout()`.
|
||||
3. **Lockout Delete Button**: Add a "Remove" button to each lockout row that calls `useDeleteLockout()` with a confirmation step.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Validation Approach
|
||||
|
||||
The testing strategy follows a two-phase approach: first, surface counterexamples that demonstrate the bug on unfixed code, then verify the fix works correctly and preserves existing behavior.
|
||||
|
||||
### Exploratory Bug Condition Checking
|
||||
|
||||
**Goal**: Surface counterexamples that demonstrate the bug BEFORE implementing the fix. Confirm or refute the root cause analysis. If we refute, we will need to re-hypothesize.
|
||||
|
||||
**Test Plan**: Write tests that call `requires_approval()` with various `PortfolioRiskConfig` instances and trading modes. Run these tests on the UNFIXED code to observe that paper mode always returns `False` regardless of intent.
|
||||
|
||||
**Test Cases**:
|
||||
1. **Paper Mode Default Config**: Call `requires_approval(PortfolioRiskConfig())` with `PAPER` mode — will return `False` on unfixed code (demonstrates the bug)
|
||||
2. **Paper Mode Empty JSON**: Call `requires_approval(PortfolioRiskConfig.from_db_json({}))` with `PAPER` mode — will return `False` on unfixed code (demonstrates the root cause)
|
||||
3. **Missing Lockout POST**: Attempt `POST /api/admin/trading/lockouts` — will return 404/405 on unfixed code
|
||||
4. **Missing Lockout DELETE**: Attempt `DELETE /api/admin/trading/lockouts/{id}` — will return 404/405 on unfixed code
|
||||
|
||||
**Expected Counterexamples**:
|
||||
- `requires_approval()` returns `False` for paper mode even when the operator intends to require approval
|
||||
- Possible causes: `auto_approve_paper` defaults to `True` and config JSON never overrides it
|
||||
|
||||
### Fix Checking
|
||||
|
||||
**Goal**: Verify that for all inputs where the bug condition holds, the fixed function produces the expected behavior.
|
||||
|
||||
**Pseudocode:**
|
||||
```
|
||||
FOR ALL input WHERE isBugCondition(input) DO
|
||||
config := PortfolioRiskConfig with auto_approve_paper = False
|
||||
result := requires_approval(config, PAPER)
|
||||
ASSERT result == True
|
||||
END FOR
|
||||
```
|
||||
|
||||
### Preservation Checking
|
||||
|
||||
**Goal**: Verify that for all inputs where the bug condition does NOT hold, the fixed function produces the same result as the original function.
|
||||
|
||||
**Pseudocode:**
|
||||
```
|
||||
FOR ALL input WHERE NOT isBugCondition(input) DO
|
||||
ASSERT requires_approval_original(input) == requires_approval_fixed(input)
|
||||
END FOR
|
||||
```
|
||||
|
||||
**Testing Approach**: Property-based testing is recommended for preservation checking because:
|
||||
- It generates many test cases automatically across the input domain of (trading_mode, auto_approve_paper, require_approval_for_live) combinations
|
||||
- It catches edge cases that manual unit tests might miss (e.g., unusual config combinations)
|
||||
- It provides strong guarantees that behavior is unchanged for all non-buggy inputs
|
||||
|
||||
**Test Plan**: Observe behavior on UNFIXED code first for live mode and disabled mode, then write property-based tests capturing that behavior.
|
||||
|
||||
**Test Cases**:
|
||||
1. **Live Mode Preservation**: Verify `requires_approval()` returns `True` for live mode with `require_approval_for_live=True` — must be identical before and after fix
|
||||
2. **Disabled Mode Preservation**: Verify `requires_approval()` returns `False` for disabled mode — must be identical before and after fix
|
||||
3. **Approval Review Preservation**: Verify `review_approval()` continues to update approval status correctly
|
||||
4. **Expiry Preservation**: Verify `expire_stale_approvals()` continues to mark expired approvals
|
||||
5. **Risk Rejection Preservation**: Verify orders failing risk checks are still rejected before the approval gate
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Test `requires_approval()` with all combinations of trading mode and approval settings
|
||||
- Test new `POST /api/admin/trading/lockouts` endpoint creates valid lockout rows
|
||||
- Test new `DELETE /api/admin/trading/lockouts/{id}` endpoint removes lockouts
|
||||
- Test new `PUT /api/admin/trading/approval-config` endpoint updates config JSON correctly
|
||||
- Test edge cases: expired lockouts not returned, invalid ticker, zero/negative duration
|
||||
|
||||
### Property-Based Tests
|
||||
|
||||
- Generate random `PortfolioRiskConfig` instances with varying `auto_approve_paper` and `require_approval_for_live` values across all three trading modes, and verify `requires_approval()` returns the correct boolean for each combination
|
||||
- Generate random lockout parameters (ticker, duration, reason) and verify the POST endpoint creates valid entries that appear in GET results and respect expiry
|
||||
- Test preservation: for all non-paper-mode configs, verify `requires_approval()` behavior is identical to the original implementation
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Test full order flow: set `auto_approve_paper=False` via API → submit order job → verify approval request created → approve → verify order submitted to broker
|
||||
- Test lockout flow: create manual lockout via API → verify it appears in GET → delete it → verify it's gone
|
||||
- Test UI rendering: approval toggle card renders with correct state, lockout form submits correctly, delete button removes lockout
|
||||
@@ -0,0 +1,182 @@
|
||||
# Implementation Plan
|
||||
|
||||
- [x] 1. Write bug condition exploration test
|
||||
- **Property 1: Bug Condition** - Paper Mode Approval Bypass
|
||||
- **CRITICAL**: This test MUST FAIL on unfixed code — failure confirms the bug exists
|
||||
- **DO NOT attempt to fix the test or the code when it fails**
|
||||
- **NOTE**: This test encodes the expected behavior — it will validate the fix when it passes after implementation
|
||||
- **GOAL**: Surface counterexamples that demonstrate the bug exists
|
||||
- **Scoped PBT Approach**: Scope the property to paper mode with `auto_approve_paper=False` — the bug is that even when `auto_approve_paper` is explicitly set to `False` in a `PortfolioRiskConfig`, the system works correctly at the function level, but the config JSON stored in `risk_configs` never contains `operator_approval` settings because no UI/endpoint sets them. The exploration test should:
|
||||
1. Verify `requires_approval(config, PAPER)` returns `True` when `auto_approve_paper=False` (this actually passes — the function itself is correct)
|
||||
2. Verify `PortfolioRiskConfig.from_db_json({})` produces `auto_approve_paper=True` (demonstrates the root cause — empty JSON always defaults to auto-approve)
|
||||
3. Verify there is no dedicated endpoint to update `operator_approval` settings in the risk config (the existing `PUT /api/admin/trading/config` replaces the entire config JSON, and no UI calls it with `operator_approval` fields)
|
||||
- Test file: `tests/test_pbt_operator_approval.py`
|
||||
- Use Hypothesis with `@settings(max_examples=100)` per project conventions
|
||||
- Generate random `PortfolioRiskConfig` instances with `auto_approve_paper=False` and `trading_mode=PAPER`
|
||||
- Assert `requires_approval()` returns `True` for all such configs (function-level correctness)
|
||||
- Also test that `PortfolioRiskConfig.from_db_json({})` always produces `auto_approve_paper=True` (demonstrates the config-level bug)
|
||||
- Run test on UNFIXED code
|
||||
- **EXPECTED OUTCOME**: The function-level property passes (function is correct), but the `from_db_json({})` test demonstrates the root cause — empty config always auto-approves
|
||||
- Document counterexamples found to understand root cause
|
||||
- Mark task complete when test is written, run, and results are documented
|
||||
- _Requirements: 1.1, 1.2, 1.5, 2.1_
|
||||
|
||||
- [x] 2. Write preservation property tests (BEFORE implementing fix)
|
||||
- **Property 2: Preservation** - Non-Paper-Toggle Approval Behavior
|
||||
- **IMPORTANT**: Follow observation-first methodology
|
||||
- Observe behavior on UNFIXED code for non-buggy inputs:
|
||||
- Observe: `requires_approval(config, LIVE)` returns `True` when `require_approval_for_live=True`
|
||||
- Observe: `requires_approval(config, LIVE)` returns `False` when `require_approval_for_live=False`
|
||||
- Observe: `requires_approval(config, DISABLED)` returns `False` for all configs
|
||||
- Observe: `requires_approval(config, PAPER)` returns `False` when `auto_approve_paper=True`
|
||||
- Write property-based tests capturing observed behavior patterns:
|
||||
- For all `(trading_mode, auto_approve_paper, require_approval_for_live)` combinations where `trading_mode != PAPER` OR `auto_approve_paper == True`, verify `requires_approval()` returns the same result as the original function
|
||||
- Property: for all LIVE mode configs, result equals `require_approval_for_live`
|
||||
- Property: for all DISABLED mode configs, result is always `False`
|
||||
- Property: for all PAPER mode configs with `auto_approve_paper=True`, result is always `False`
|
||||
- Test file: `tests/test_pbt_operator_approval.py` (same file, separate test class)
|
||||
- Use Hypothesis with `@settings(max_examples=100)` per project conventions
|
||||
- Verify tests PASS on UNFIXED code
|
||||
- **EXPECTED OUTCOME**: Tests PASS (confirms baseline behavior to preserve)
|
||||
- Mark task complete when tests are written, run, and passing on unfixed code
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_
|
||||
|
||||
- [x] 3. Implement backend API endpoints
|
||||
|
||||
- [x] 3.1 Add `GET /api/admin/trading/approval-config` endpoint
|
||||
- Read the active `risk_configs` row and extract `operator_approval` settings from the `config` JSONB column
|
||||
- Parse with `PortfolioRiskConfig.from_db_json()` to get defaults when fields are missing
|
||||
- Return `{ auto_approve_paper: bool, require_approval_for_live: bool, approval_timeout_minutes: int }`
|
||||
- File: `services/api/app.py`
|
||||
- _Bug_Condition: isBugCondition(input) where risk_config_json has no "operator_approval" key_
|
||||
- _Expected_Behavior: Endpoint returns current operator_approval settings (with defaults filled in)_
|
||||
- _Preservation: Existing GET /api/admin/trading/config endpoint unchanged_
|
||||
- _Requirements: 1.5, 2.5_
|
||||
|
||||
- [x] 3.2 Add `PUT /api/admin/trading/approval-config` endpoint
|
||||
- Read current `risk_configs.config` JSONB, merge `operator_approval` sub-object, write back
|
||||
- Accept `{ auto_approve_paper: bool }` (and optionally `require_approval_for_live`, `approval_timeout_minutes`)
|
||||
- Use JSON merge (not full replace) to preserve other config fields
|
||||
- File: `services/api/app.py`
|
||||
- _Bug_Condition: No UI/endpoint to set auto_approve_paper=False in risk_config_json_
|
||||
- _Expected_Behavior: PUT updates operator_approval in config JSONB, requires_approval() reflects the change_
|
||||
- _Preservation: Other config fields (position_limits, sector_exposure, etc.) unchanged by merge_
|
||||
- _Requirements: 1.5, 2.1, 2.5_
|
||||
|
||||
- [x] 3.3 Add `POST /api/admin/trading/lockouts` endpoint
|
||||
- Accept `{ ticker: string, reason: string, duration_minutes: int, lockout_type?: string }`
|
||||
- Compute `expires_at = NOW() + duration_minutes`
|
||||
- Default `lockout_type` to `"manual"`
|
||||
- Insert into `symbol_lockouts` table
|
||||
- Return the created lockout row
|
||||
- File: `services/api/app.py`
|
||||
- _Bug_Condition: No POST endpoint exists for symbol_lockouts_
|
||||
- _Expected_Behavior: Manual lockout created and visible in GET /api/admin/trading/lockouts_
|
||||
- _Preservation: System-generated lockouts (news-shock, cooldown) unaffected_
|
||||
- _Requirements: 1.3, 1.4, 2.3, 2.4, 3.6_
|
||||
|
||||
- [x] 3.4 Add `DELETE /api/admin/trading/lockouts/{lockout_id}` endpoint
|
||||
- Delete the lockout row by ID
|
||||
- Return 404 if not found
|
||||
- File: `services/api/app.py`
|
||||
- _Bug_Condition: No DELETE endpoint exists for symbol_lockouts_
|
||||
- _Expected_Behavior: Lockout removed early, no longer appears in GET results_
|
||||
- _Preservation: Other lockouts unaffected_
|
||||
- _Requirements: 1.4, 2.4_
|
||||
|
||||
- [x] 4. Implement frontend hooks
|
||||
|
||||
- [x] 4.1 Add `useApprovalConfig()` hook
|
||||
- Fetches `GET /api/admin/trading/approval-config` via `useGet` with query key `['approval-config']`
|
||||
- Type the response as `{ auto_approve_paper: boolean, require_approval_for_live: boolean, approval_timeout_minutes: number }`
|
||||
- File: `frontend/src/api/hooks.ts`
|
||||
- _Requirements: 2.5_
|
||||
|
||||
- [x] 4.2 Add `useUpdateApprovalConfig()` mutation hook
|
||||
- Calls `PUT /api/admin/trading/approval-config` via `apiPut`
|
||||
- Invalidates `['approval-config']` and `['trading-config']` query keys on success
|
||||
- File: `frontend/src/api/hooks.ts`
|
||||
- _Requirements: 2.1, 2.5_
|
||||
|
||||
- [x] 4.3 Add `useCreateLockout()` mutation hook
|
||||
- Calls `POST /api/admin/trading/lockouts` via `apiPost`
|
||||
- Accepts `{ ticker: string, reason: string, duration_minutes: number }`
|
||||
- Invalidates `['lockouts']` query key on success
|
||||
- File: `frontend/src/api/hooks.ts`
|
||||
- _Requirements: 2.3_
|
||||
|
||||
- [x] 4.4 Add `useDeleteLockout()` mutation hook
|
||||
- Calls `DELETE /api/admin/trading/lockouts/{id}` via `apiDelete`
|
||||
- Invalidates `['lockouts']` query key on success
|
||||
- File: `frontend/src/api/hooks.ts`
|
||||
- _Requirements: 2.4_
|
||||
|
||||
- [x] 5. Implement frontend UI controls
|
||||
|
||||
- [x] 5.1 Add Approval Toggle Card to Trading page
|
||||
- New `Card` between the existing signal layer toggles and Pending Approvals section
|
||||
- Toggle switch for `auto_approve_paper` using `useApprovalConfig()` and `useUpdateApprovalConfig()`
|
||||
- Confirmation dialog before toggling (since enabling approval requirements changes order flow)
|
||||
- Show current state label ("Auto-approve paper orders" / "Require approval for paper orders")
|
||||
- Use same toggle switch pattern as Macro Signal Layer and Competitive Signal Layer cards
|
||||
- File: `frontend/src/pages/Trading.tsx`
|
||||
- _Requirements: 1.5, 2.1, 2.5_
|
||||
|
||||
- [x] 5.2 Add Lockout Creation Form to Active Lockouts card
|
||||
- Inline form at the top of the Active Lockouts card with fields:
|
||||
- Ticker (text input, uppercase)
|
||||
- Reason (text input)
|
||||
- Duration in minutes (number input, default 60)
|
||||
- Submit button calls `useCreateLockout()`
|
||||
- Clear form on successful submission
|
||||
- File: `frontend/src/pages/Trading.tsx`
|
||||
- _Requirements: 1.3, 1.4, 2.3, 2.4_
|
||||
|
||||
- [x] 5.3 Add Lockout Delete Button to each lockout row
|
||||
- "Remove" button on each lockout entry in the Active Lockouts list
|
||||
- Calls `useDeleteLockout()` with the lockout ID
|
||||
- File: `frontend/src/pages/Trading.tsx`
|
||||
- _Requirements: 1.4, 2.4_
|
||||
|
||||
- [x] 5.4 Add MSW handlers for new endpoints
|
||||
- Add mock handlers in `frontend/src/test/mocks/handlers.ts` for:
|
||||
- `GET /api/admin/trading/approval-config`
|
||||
- `PUT /api/admin/trading/approval-config`
|
||||
- `POST /api/admin/trading/lockouts`
|
||||
- `DELETE /api/admin/trading/lockouts/:id`
|
||||
- File: `frontend/src/test/mocks/handlers.ts`
|
||||
- _Requirements: 2.1, 2.3, 2.4, 2.5_
|
||||
|
||||
- [x] 6. Verify bug condition exploration test now passes
|
||||
|
||||
- [x] 6.1 Re-run bug condition exploration test
|
||||
- **Property 1: Expected Behavior** - Paper Mode Approval Toggle
|
||||
- **IMPORTANT**: Re-run the SAME test from task 1 — do NOT write a new test
|
||||
- The test from task 1 encodes the expected behavior
|
||||
- With the new `PUT /api/admin/trading/approval-config` endpoint in place, the operator can now set `auto_approve_paper=False` in the risk config JSON
|
||||
- The `requires_approval()` function-level property should still pass
|
||||
- The config-level test should now demonstrate that the endpoint correctly updates the config
|
||||
- Run: `.venv/bin/python -m pytest tests/test_pbt_operator_approval.py -x --tb=short -q`
|
||||
- **EXPECTED OUTCOME**: Test PASSES (confirms bug is fixed — operator can now control approval settings)
|
||||
- _Requirements: 2.1, 2.2, 2.5_
|
||||
|
||||
- [x] 6.2 Verify preservation tests still pass
|
||||
- **Property 2: Preservation** - Non-Paper-Toggle Approval Behavior
|
||||
- **IMPORTANT**: Re-run the SAME tests from task 2 — do NOT write new tests
|
||||
- Run: `.venv/bin/python -m pytest tests/test_pbt_operator_approval.py -x --tb=short -q`
|
||||
- **EXPECTED OUTCOME**: Tests PASS (confirms no regressions)
|
||||
- Confirm all preservation properties still hold after the fix
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_
|
||||
|
||||
- [x] 7. Run frontend tests
|
||||
- Run: `cd frontend && npx vitest --run`
|
||||
- Verify Trading page renders correctly with new controls
|
||||
- Verify no regressions in existing frontend tests
|
||||
- _Requirements: 2.1, 2.3, 2.4, 2.5_
|
||||
|
||||
- [x] 8. Checkpoint — Ensure all tests pass
|
||||
- Run full Python test suite: `.venv/bin/python -m pytest tests/test_pbt_operator_approval.py -x --tb=short -q`
|
||||
- Run full frontend test suite: `cd frontend && npx vitest --run`
|
||||
- Ensure all property-based tests pass (both bug condition and preservation)
|
||||
- Ensure all frontend tests pass (including new MSW handlers)
|
||||
- Ask the user if questions arise
|
||||
Reference in New Issue
Block a user