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
|
# 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
|
## Summary
|
||||||
|
|
||||||
Add chart formations feature with:
|
Add chart formations feature with:
|
||||||
|
|
@ -305,8 +310,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:
|
// After charts init - pass candleSeries for v5 coordinate conversion:
|
||||||
this.formations.initOverlay('chart1', this.charts.chart_1);
|
this.formations.initOverlay('chart1', this.charts.chart_1, this.charts.candleSeries);
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -333,8 +338,9 @@ this.formations.initOverlay('chart1', this.charts.chart_1);
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
class FormationOverlay {
|
class FormationOverlay {
|
||||||
constructor(chartContainerId, chart) {
|
constructor(chartContainerId, chart, candleSeries) {
|
||||||
this.chart = chart;
|
this.chart = chart;
|
||||||
|
this.candleSeries = candleSeries; // Required for v5 coordinate conversion
|
||||||
this.container = document.getElementById(chartContainerId);
|
this.container = document.getElementById(chartContainerId);
|
||||||
this.svg = null;
|
this.svg = null;
|
||||||
this.formations = new Map(); // tbl_key -> {group, anchors, lines}
|
this.formations = new Map(); // tbl_key -> {group, anchors, lines}
|
||||||
|
|
@ -386,19 +392,24 @@ class FormationOverlay {
|
||||||
const lines = formation.data.lines;
|
const lines = formation.data.lines;
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[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 p1 = this._chartToPixel(line.point1.time, line.point1.price);
|
||||||
const p2 = this._chartToPixel(line.point2.time, line.point2.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];
|
const lineEl = formation.lineElements[i];
|
||||||
if (lineEl && p1 && p2) {
|
if (lineEl && extended) {
|
||||||
lineEl.setAttribute('x1', p1.x);
|
lineEl.setAttribute('x1', extended.x1);
|
||||||
lineEl.setAttribute('y1', p1.y);
|
lineEl.setAttribute('y1', extended.y1);
|
||||||
lineEl.setAttribute('x2', p2.x);
|
lineEl.setAttribute('x2', extended.x2);
|
||||||
lineEl.setAttribute('y2', p2.y);
|
lineEl.setAttribute('y2', extended.y2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update anchor elements
|
// Update anchor elements at defined points (not extended)
|
||||||
const anchor1 = formation.anchorElements[i * 2];
|
const anchor1 = formation.anchorElements[i * 2];
|
||||||
const anchor2 = formation.anchorElements[i * 2 + 1];
|
const anchor2 = formation.anchorElements[i * 2 + 1];
|
||||||
if (anchor1 && p1) {
|
if (anchor1 && p1) {
|
||||||
|
|
@ -414,11 +425,11 @@ class FormationOverlay {
|
||||||
|
|
||||||
_chartToPixel(time, price) {
|
_chartToPixel(time, price) {
|
||||||
// Convert chart coordinates to SVG pixel coordinates
|
// Convert chart coordinates to SVG pixel coordinates
|
||||||
|
// NOTE: In Lightweight Charts v5, use series for price conversion
|
||||||
const timeScale = this.chart.timeScale();
|
const timeScale = this.chart.timeScale();
|
||||||
const priceScale = this.chart.priceScale('right');
|
|
||||||
|
|
||||||
const x = timeScale.timeToCoordinate(time);
|
const x = timeScale.timeToCoordinate(time);
|
||||||
const y = priceScale.priceToCoordinate(price);
|
const y = this.candleSeries.priceToCoordinate(price);
|
||||||
|
|
||||||
if (x === null || y === null) return null;
|
if (x === null || y === null) return null;
|
||||||
return { x, y };
|
return { x, y };
|
||||||
|
|
@ -426,26 +437,104 @@ class FormationOverlay {
|
||||||
|
|
||||||
_pixelToChart(x, y) {
|
_pixelToChart(x, y) {
|
||||||
// Convert SVG pixel coordinates to chart coordinates
|
// Convert SVG pixel coordinates to chart coordinates
|
||||||
|
// NOTE: In Lightweight Charts v5, use series for price conversion
|
||||||
const timeScale = this.chart.timeScale();
|
const timeScale = this.chart.timeScale();
|
||||||
const priceScale = this.chart.priceScale('right');
|
|
||||||
|
|
||||||
const time = timeScale.coordinateToTime(x);
|
const time = timeScale.coordinateToTime(x);
|
||||||
const price = priceScale.coordinateToPrice(y);
|
const price = this.candleSeries.coordinateToPrice(y);
|
||||||
|
|
||||||
return { time, price };
|
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:**
|
**User interaction flow:**
|
||||||
1. User clicks "Line" button in HUD panel
|
1. User clicks "Line" button in HUD panel
|
||||||
2. A default line shape appears at chart center (not following mouse - simpler)
|
2. A default line shape appears at chart center with visible anchor points
|
||||||
3. User clicks chart to place the shape
|
3. User drags anchors to adjust position (no click-to-place step)
|
||||||
4. Shape becomes editable with visible anchor points
|
4. User clicks "Save" or hits Enter to persist
|
||||||
5. User drags anchors to adjust position
|
|
||||||
6. User clicks "Save" or hits Enter to persist
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// In FormationOverlay
|
// In FormationOverlay
|
||||||
|
|
@ -769,8 +858,11 @@ class StrategyInstance:
|
||||||
|
|
||||||
IMPORTANT: In backtest, returns bar timestamp (not wall clock).
|
IMPORTANT: In backtest, returns bar timestamp (not wall clock).
|
||||||
In live/paper, returns current candle's open time.
|
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)
|
**Modify:** `src/Strategies.py` (strategy instance creation, line ~268)
|
||||||
|
|
@ -849,10 +941,7 @@ initialize(targetId) {
|
||||||
|
|
||||||
loadFormationsForScope(exchange, market, timeframe) {
|
loadFormationsForScope(exchange, market, timeframe) {
|
||||||
this.currentScope = { exchange, market, timeframe };
|
this.currentScope = { exchange, market, timeframe };
|
||||||
this.comms.emit('message', {
|
this.comms.sendToApp('request_formations', { exchange, market, timeframe });
|
||||||
type: 'request_formations',
|
|
||||||
exchange, market, timeframe
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// On scope change (if we add dynamic switching later):
|
// On scope change (if we add dynamic switching later):
|
||||||
|
|
@ -878,16 +967,20 @@ onChartScopeChanged(newScope) {
|
||||||
### Modified Files
|
### Modified Files
|
||||||
| File | Changes |
|
| File | Changes |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `src/Database.py` | Add formations table |
|
| `src/BrighterTrades.py` | Initialize Formations manager + add socket handlers in `process_incoming_message()` |
|
||||||
| `src/BrighterTrades.py` | Initialize Formations manager |
|
|
||||||
| `src/app.py` | Add socket handlers |
|
|
||||||
| `src/PythonGenerator.py` | Add handle_formation, handle_current_candle_time |
|
| `src/PythonGenerator.py` | Add handle_formation, handle_current_candle_time |
|
||||||
| `src/StrategyInstance.py` | Add process_formation, get_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/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 |
|
| `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
|
## Verification Plan
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue