Update formations plan for Lightweight Charts v5 compatibility
Addresses Codex review feedback:
- Use series-based coordinate conversion (series.priceToCoordinate)
- Add infinite line extension math (_getInfiniteLineEndpoints)
- Fix message pattern: use sendToApp() not emit('message')
- Remove wall clock fallback in get_current_candle_time()
- Clarify architecture: socket handlers in BrighterTrades, not app.py
- Clarify UX: shape appears at center, drag to adjust (no click-to-place)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5c37fb00b6
commit
a46d2006f3
|
|
@ -1,5 +1,10 @@
|
|||
# Chart Formations Feature Implementation Plan
|
||||
|
||||
**Lightweight Charts Version**: v5.1.0 (upgraded March 2026)
|
||||
- Uses series-based coordinate conversion: `series.priceToCoordinate()` / `series.coordinateToPrice()`
|
||||
- Markers via `createSeriesMarkers()` API
|
||||
- Series creation via `chart.addSeries(SeriesType, options)`
|
||||
|
||||
## Summary
|
||||
|
||||
Add chart formations feature with:
|
||||
|
|
@ -305,8 +310,8 @@ Follow the three-class pattern from `signals.js`:
|
|||
**Modify:** `src/static/general.js`
|
||||
```javascript
|
||||
this.formations = new Formations(this);
|
||||
// After charts init:
|
||||
this.formations.initOverlay('chart1', this.charts.chart_1);
|
||||
// After charts init - pass candleSeries for v5 coordinate conversion:
|
||||
this.formations.initOverlay('chart1', this.charts.chart_1, this.charts.candleSeries);
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -333,8 +338,9 @@ this.formations.initOverlay('chart1', this.charts.chart_1);
|
|||
|
||||
```javascript
|
||||
class FormationOverlay {
|
||||
constructor(chartContainerId, chart) {
|
||||
constructor(chartContainerId, chart, candleSeries) {
|
||||
this.chart = chart;
|
||||
this.candleSeries = candleSeries; // Required for v5 coordinate conversion
|
||||
this.container = document.getElementById(chartContainerId);
|
||||
this.svg = null;
|
||||
this.formations = new Map(); // tbl_key -> {group, anchors, lines}
|
||||
|
|
@ -386,19 +392,24 @@ class FormationOverlay {
|
|||
const lines = formation.data.lines;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Get infinite line endpoints (extends to viewport edges)
|
||||
const extended = this._getInfiniteLineEndpoints(line);
|
||||
|
||||
// Get anchor positions (at defined points, not extended)
|
||||
const p1 = this._chartToPixel(line.point1.time, line.point1.price);
|
||||
const p2 = this._chartToPixel(line.point2.time, line.point2.price);
|
||||
|
||||
// Update line element
|
||||
// Update line element with infinite extension
|
||||
const lineEl = formation.lineElements[i];
|
||||
if (lineEl && p1 && p2) {
|
||||
lineEl.setAttribute('x1', p1.x);
|
||||
lineEl.setAttribute('y1', p1.y);
|
||||
lineEl.setAttribute('x2', p2.x);
|
||||
lineEl.setAttribute('y2', p2.y);
|
||||
if (lineEl && extended) {
|
||||
lineEl.setAttribute('x1', extended.x1);
|
||||
lineEl.setAttribute('y1', extended.y1);
|
||||
lineEl.setAttribute('x2', extended.x2);
|
||||
lineEl.setAttribute('y2', extended.y2);
|
||||
}
|
||||
|
||||
// Update anchor elements
|
||||
// Update anchor elements at defined points (not extended)
|
||||
const anchor1 = formation.anchorElements[i * 2];
|
||||
const anchor2 = formation.anchorElements[i * 2 + 1];
|
||||
if (anchor1 && p1) {
|
||||
|
|
@ -414,11 +425,11 @@ class FormationOverlay {
|
|||
|
||||
_chartToPixel(time, price) {
|
||||
// Convert chart coordinates to SVG pixel coordinates
|
||||
// NOTE: In Lightweight Charts v5, use series for price conversion
|
||||
const timeScale = this.chart.timeScale();
|
||||
const priceScale = this.chart.priceScale('right');
|
||||
|
||||
const x = timeScale.timeToCoordinate(time);
|
||||
const y = priceScale.priceToCoordinate(price);
|
||||
const y = this.candleSeries.priceToCoordinate(price);
|
||||
|
||||
if (x === null || y === null) return null;
|
||||
return { x, y };
|
||||
|
|
@ -426,26 +437,104 @@ class FormationOverlay {
|
|||
|
||||
_pixelToChart(x, y) {
|
||||
// Convert SVG pixel coordinates to chart coordinates
|
||||
// NOTE: In Lightweight Charts v5, use series for price conversion
|
||||
const timeScale = this.chart.timeScale();
|
||||
const priceScale = this.chart.priceScale('right');
|
||||
|
||||
const time = timeScale.coordinateToTime(x);
|
||||
const price = priceScale.coordinateToPrice(y);
|
||||
const price = this.candleSeries.coordinateToPrice(y);
|
||||
|
||||
return { time, price };
|
||||
}
|
||||
|
||||
_getInfiniteLineEndpoints(line) {
|
||||
// Calculate extended line endpoints to viewport edges (infinite extension)
|
||||
// Returns pixel coordinates that extend beyond the defined points
|
||||
const p1 = line.point1;
|
||||
const p2 = line.point2;
|
||||
|
||||
// Get viewport bounds in pixels
|
||||
const svgRect = this.svg.getBoundingClientRect();
|
||||
const viewportWidth = svgRect.width;
|
||||
const viewportHeight = svgRect.height;
|
||||
|
||||
// Convert line points to pixels
|
||||
const px1 = this._chartToPixel(p1.time, p1.price);
|
||||
const px2 = this._chartToPixel(p2.time, p2.price);
|
||||
|
||||
if (!px1 || !px2) return null;
|
||||
|
||||
// Handle vertical line (same x)
|
||||
if (Math.abs(px2.x - px1.x) < 0.001) {
|
||||
return {
|
||||
x1: px1.x, y1: 0,
|
||||
x2: px1.x, y2: viewportHeight
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate slope and y-intercept: y = mx + b
|
||||
const m = (px2.y - px1.y) / (px2.x - px1.x);
|
||||
const b = px1.y - m * px1.x;
|
||||
|
||||
// Find intersection with viewport edges
|
||||
// Left edge (x=0): y = b
|
||||
// Right edge (x=viewportWidth): y = m * viewportWidth + b
|
||||
// Top edge (y=0): x = -b / m
|
||||
// Bottom edge (y=viewportHeight): x = (viewportHeight - b) / m
|
||||
|
||||
const intersections = [];
|
||||
|
||||
// Left edge
|
||||
const yAtLeft = b;
|
||||
if (yAtLeft >= 0 && yAtLeft <= viewportHeight) {
|
||||
intersections.push({ x: 0, y: yAtLeft });
|
||||
}
|
||||
|
||||
// Right edge
|
||||
const yAtRight = m * viewportWidth + b;
|
||||
if (yAtRight >= 0 && yAtRight <= viewportHeight) {
|
||||
intersections.push({ x: viewportWidth, y: yAtRight });
|
||||
}
|
||||
|
||||
// Top edge (if not horizontal)
|
||||
if (Math.abs(m) > 0.001) {
|
||||
const xAtTop = -b / m;
|
||||
if (xAtTop >= 0 && xAtTop <= viewportWidth) {
|
||||
intersections.push({ x: xAtTop, y: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom edge (if not horizontal)
|
||||
if (Math.abs(m) > 0.001) {
|
||||
const xAtBottom = (viewportHeight - b) / m;
|
||||
if (xAtBottom >= 0 && xAtBottom <= viewportWidth) {
|
||||
intersections.push({ x: xAtBottom, y: viewportHeight });
|
||||
}
|
||||
}
|
||||
|
||||
// Return the two furthest-apart intersections
|
||||
if (intersections.length >= 2) {
|
||||
// Sort by x coordinate and take first and last
|
||||
intersections.sort((a, b) => a.x - b.x);
|
||||
return {
|
||||
x1: intersections[0].x, y1: intersections[0].y,
|
||||
x2: intersections[intersections.length - 1].x,
|
||||
y2: intersections[intersections.length - 1].y
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to original endpoints
|
||||
return { x1: px1.x, y1: px1.y, x2: px2.x, y2: px2.y };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Click-to-Place UX
|
||||
### 3.3 Click-to-Draw UX
|
||||
|
||||
**User interaction flow:**
|
||||
1. User clicks "Line" button in HUD panel
|
||||
2. A default line shape appears at chart center (not following mouse - simpler)
|
||||
3. User clicks chart to place the shape
|
||||
4. Shape becomes editable with visible anchor points
|
||||
5. User drags anchors to adjust position
|
||||
6. User clicks "Save" or hits Enter to persist
|
||||
2. A default line shape appears at chart center with visible anchor points
|
||||
3. User drags anchors to adjust position (no click-to-place step)
|
||||
4. User clicks "Save" or hits Enter to persist
|
||||
|
||||
```javascript
|
||||
// In FormationOverlay
|
||||
|
|
@ -769,8 +858,11 @@ class StrategyInstance:
|
|||
|
||||
IMPORTANT: In backtest, returns bar timestamp (not wall clock).
|
||||
In live/paper, returns current candle's open time.
|
||||
NEVER falls back to wall clock - that would corrupt backtest results.
|
||||
"""
|
||||
return self.current_candle.get('time', int(time.time()))
|
||||
if not self.current_candle or 'time' not in self.current_candle:
|
||||
raise ValueError("current_candle.time not set - cannot use wall clock for formations")
|
||||
return self.current_candle['time']
|
||||
```
|
||||
|
||||
**Modify:** `src/Strategies.py` (strategy instance creation, line ~268)
|
||||
|
|
@ -849,10 +941,7 @@ initialize(targetId) {
|
|||
|
||||
loadFormationsForScope(exchange, market, timeframe) {
|
||||
this.currentScope = { exchange, market, timeframe };
|
||||
this.comms.emit('message', {
|
||||
type: 'request_formations',
|
||||
exchange, market, timeframe
|
||||
});
|
||||
this.comms.sendToApp('request_formations', { exchange, market, timeframe });
|
||||
}
|
||||
|
||||
// On scope change (if we add dynamic switching later):
|
||||
|
|
@ -878,16 +967,20 @@ onChartScopeChanged(newScope) {
|
|||
### Modified Files
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `src/Database.py` | Add formations table |
|
||||
| `src/BrighterTrades.py` | Initialize Formations manager |
|
||||
| `src/app.py` | Add socket handlers |
|
||||
| `src/BrighterTrades.py` | Initialize Formations manager + add socket handlers in `process_incoming_message()` |
|
||||
| `src/PythonGenerator.py` | Add handle_formation, handle_current_candle_time |
|
||||
| `src/StrategyInstance.py` | Add process_formation, get_current_candle_time |
|
||||
| `src/backtest_strategy_instance.py` | Load formations for backtest scope |
|
||||
| `src/backtest_strategy_instance.py` | Override get_current_candle_time for bar time |
|
||||
| `src/Strategies.py` | Inject formations_manager + formation_owner_id |
|
||||
| `src/backtesting.py` | Inject formations_manager |
|
||||
| `src/templates/control_panel.html` | Include formations_hud.html |
|
||||
| `src/static/general.js` | Initialize UI.formations |
|
||||
| `src/static/general.js` | Initialize UI.formations + overlay |
|
||||
| `src/static/communication.js` | Add reply handlers for formations |
|
||||
| `src/templates/index.html` | Include formations.js + formation_overlay.js |
|
||||
|
||||
**Note**: Table creation uses `_ensure_table_exists()` pattern in `Formations.py` (not Database.py).
|
||||
Socket handling stays in `BrighterTrades.process_incoming_message()` (not app.py).
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
|
|
|||
Loading…
Reference in New Issue