Address second round of Codex review feedback on formations plan

Fixes:
1. Class signature consistency - reference __init__ from 1.1, don't redefine
2. Dynamic chart ID - use this.data.chart1_id instead of hardcoded 'chart1'
3. Comms wiring - use this.comms.on() pattern like signals.js
4. RAF loop guards - add pause conditions, destroy() teardown hook
5. Drag null guards - validate chartCoords before mutating formation data
6. Test organization - split MVP tests from Phase C target tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-03-10 16:03:33 -03:00
parent a46d2006f3
commit ba7b6e79ff
1 changed files with 57 additions and 20 deletions

View File

@ -146,8 +146,8 @@ class Formations:
```python ```python
class Formations: class Formations:
def __init__(self, database: Database): # Full signature - see 1.1 for __init__ implementation
self.database = database # def __init__(self, data_cache: DataCache, database: Database)
# CRUD operations # CRUD operations
def create(self, user_id: int, data: dict) -> dict def create(self, user_id: int, data: dict) -> dict
@ -231,17 +231,16 @@ elif message_type == 'delete_formation':
return {'reply': 'formation_deleted', 'data': result} return {'reply': 'formation_deleted', 'data': result}
``` ```
**Frontend handler** (in `communication.js`, follows existing pattern at line ~88): **Frontend handler** (in `formations.js`, follows signals.js pattern):
```javascript ```javascript
// Socket response handler // In Formations.registerSocketHandlers() - use comms.on() pattern
socket.on('message', (msg) => { registerSocketHandlers() {
if (msg.reply === 'formations') { this.comms.on('formations', this.handleFormationsResponse.bind(this));
UI.formations.handleFormations(msg.data); this.comms.on('formation_created', this.handleFormationCreated.bind(this));
} else if (msg.reply === 'formation_created') { this.comms.on('formation_updated', this.handleFormationUpdated.bind(this));
UI.formations.handleFormationCreated(msg.data); this.comms.on('formation_deleted', this.handleFormationDeleted.bind(this));
} this.comms.on('formation_error', this.handleFormationError.bind(this));
// ... etc }
});
``` ```
--- ---
@ -310,8 +309,8 @@ Follow the three-class pattern from `signals.js`:
**Modify:** `src/static/general.js` **Modify:** `src/static/general.js`
```javascript ```javascript
this.formations = new Formations(this); this.formations = new Formations(this);
// After charts init - pass candleSeries for v5 coordinate conversion: // After charts init - use dynamic chart ID, pass candleSeries for v5 coordinate conversion:
this.formations.initOverlay('chart1', this.charts.chart_1, this.charts.candleSeries); this.formations.initOverlay(this.data.chart1_id, this.charts.chart_1, this.charts.candleSeries);
``` ```
--- ---
@ -368,16 +367,31 @@ class FormationOverlay {
_startSyncLoop() { _startSyncLoop() {
// CRITICAL: Use requestAnimationFrame polling, NOT subscribeVisibleTimeRangeChange // CRITICAL: Use requestAnimationFrame polling, NOT subscribeVisibleTimeRangeChange
// This avoids the infinite loop problem with bound charts // This avoids the infinite loop problem with bound charts
this._loopRunning = true;
const sync = () => { const sync = () => {
this._updateAllPositions(); // Guard: only run if there's something to sync
if (!this._loopRunning) return;
if (this.formations.size > 0 || this.tempFormation) {
this._updateAllPositions();
}
this._animationFrameId = requestAnimationFrame(sync); this._animationFrameId = requestAnimationFrame(sync);
}; };
this._animationFrameId = requestAnimationFrame(sync); this._animationFrameId = requestAnimationFrame(sync);
} }
stopSyncLoop() { stopSyncLoop() {
this._loopRunning = false;
if (this._animationFrameId) { if (this._animationFrameId) {
cancelAnimationFrame(this._animationFrameId); cancelAnimationFrame(this._animationFrameId);
this._animationFrameId = null;
}
}
// Call when component is destroyed
destroy() {
this.stopSyncLoop();
if (this.svg && this.svg.parentNode) {
this.svg.parentNode.removeChild(this.svg);
} }
} }
@ -648,13 +662,20 @@ _setupDragListeners() {
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
const y = e.clientY - rect.top; const y = e.clientY - rect.top;
// Guard: skip if outside SVG bounds
if (x < 0 || y < 0 || x > rect.width || y > rect.height) return;
// Update anchor position // Update anchor position
const { anchor, lineIndex, pointIndex, formation } = this.draggingAnchor; const { anchor, lineIndex, pointIndex, formation } = this.draggingAnchor;
anchor.setAttribute('cx', x); anchor.setAttribute('cx', x);
anchor.setAttribute('cy', y); anchor.setAttribute('cy', y);
// Update formation data // Update formation data with null guard
const chartCoords = this._pixelToChart(x, y); const chartCoords = this._pixelToChart(x, y);
if (!chartCoords || chartCoords.time === null || chartCoords.price === null) {
return; // Invalid coordinates - don't mutate formation data
}
const pointKey = pointIndex === 0 ? 'point1' : 'point2'; const pointKey = pointIndex === 0 ? 'point1' : 'point2';
formation.data.lines[lineIndex][pointKey] = { formation.data.lines[lineIndex][pointKey] = {
time: chartCoords.time, time: chartCoords.time,
@ -1029,6 +1050,8 @@ Socket handling stays in `BrighterTrades.process_incoming_message()` (not app.py
## Tests Required ## Tests Required
### MVP Tests (Phase A - ship with Line + Channel)
**Backend (`tests/test_formations.py`):** **Backend (`tests/test_formations.py`):**
```python ```python
def test_create_formation_unique_constraint(): def test_create_formation_unique_constraint():
@ -1043,11 +1066,11 @@ def test_calculate_line_value_extrapolation():
def test_get_by_tbl_key_for_strategy_owner(): def test_get_by_tbl_key_for_strategy_owner():
"""Strategy uses owner's formations, not current user's""" """Strategy uses owner's formations, not current user's"""
def test_calculate_targets_head_shoulders(): def test_delete_formation():
"""Target = neckline - pattern_height""" """Formation deleted from DB and cache"""
def test_calculate_targets_double_bottom(): def test_update_formation_lines():
"""Target = neckline + pattern_height""" """Lines JSON updated correctly"""
``` ```
**Generator (`tests/test_strategy_generation.py`):** **Generator (`tests/test_strategy_generation.py`):**
@ -1056,6 +1079,20 @@ def test_formation_block_generates_process_formation():
"""Block with tbl_key generates correct function call""" """Block with tbl_key generates correct function call"""
``` ```
### Phase C Tests (add when implementing targets)
**Backend (`tests/test_formations.py`):**
```python
def test_calculate_targets_head_shoulders():
"""Target = neckline - pattern_height"""
def test_calculate_targets_double_bottom():
"""Target = neckline + pattern_height"""
def test_calculate_targets_triangle():
"""Target = apex projection"""
```
**Integration (`tests/test_strategy_execution.py`):** **Integration (`tests/test_strategy_execution.py`):**
```python ```python
def test_process_formation_in_paper_strategy(): def test_process_formation_in_paper_strategy():