Compare commits

..

4 Commits

Author SHA1 Message Date
rob ba7b6e79ff 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>
2026-03-10 16:03:33 -03:00
rob a46d2006f3 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>
2026-03-10 15:57:38 -03:00
rob 5c37fb00b6 Upgrade Lightweight Charts from v2.1.0 to v5.1.0
- Update series creation to v5 API (addSeries with type parameter)
- Migrate watermarks to createTextWatermark plugin
- Migrate markers to createSeriesMarkers primitive
- Replace getBarSpacing() with timeScale().options().barSpacing
- Update crosshair handler: seriesPrices → seriesData
- Move scaleMargins from series options to price scale
- Fix chart sync infinite loop with time-based debounce (50ms)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-10 15:47:18 -03:00
rob 6a7a1c2b45 Fix indicator update callbacks and card display after refactor
- Bind indicators.update callback to correct this context in general.js
- Update updateDisplay to fall back to card elements when old chart elements don't exist
- Add _formatDisplayValue helper for consistent value formatting
- Remove verbose debug console.log statements
- Add FORMATIONS_PLAN.md documenting SVG overlay approach for chart formations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-10 15:01:55 -03:00
5 changed files with 1304 additions and 57 deletions

1187
FORMATIONS_PLAN.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,8 @@ class Charts {
/* A list of bound charts this is necessary for maintaining a dynamic /* A list of bound charts this is necessary for maintaining a dynamic
number of charts with their position and zoom factors bound.*/ number of charts with their position and zoom factors bound.*/
this.bound_charts=[]; this.bound_charts=[];
// Debounce timestamp to prevent infinite loop in chart synchronization
this._lastSyncTime = 0;
// Only the main chart is created by default. // Only the main chart is created by default.
this.create_main_chart(); this.create_main_chart();
} }
@ -26,8 +28,8 @@ class Charts {
// Display the trading pair as a watermark overlaying the chart. // Display the trading pair as a watermark overlaying the chart.
this.addWatermark(this.chart_1, this.trading_pair); this.addWatermark(this.chart_1, this.trading_pair);
// - Create the candle stick series for our chart // - Create the candle stick series for our chart (v5 API)
this.candleSeries = this.chart_1.addCandlestickSeries(); this.candleSeries = this.chart_1.addSeries(LightweightCharts.CandlestickSeries);
// Initialize the candlestick series if price_history is available // Initialize the candlestick series if price_history is available
if (this.price_history && this.price_history.length > 0) { if (this.price_history && this.price_history.length > 0) {
@ -78,9 +80,9 @@ class Charts {
this.addWatermark(this.chart5, 'Patterns'); this.addWatermark(this.chart5, 'Patterns');
this.bind_charts(this.chart5); this.bind_charts(this.chart5);
// Sync time scale with main chart so timestamps align // Sync time scale with main chart so timestamps align (v5 API)
if (this.chart_1) { if (this.chart_1) {
let barSpacing = this.chart_1.timeScale().getBarSpacing(); let barSpacing = this.chart_1.timeScale().options().barSpacing;
let scrollPosition = this.chart_1.timeScale().scrollPosition(); let scrollPosition = this.chart_1.timeScale().scrollPosition();
this.chart5.timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing }); this.chart5.timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing });
} }
@ -137,16 +139,20 @@ class Charts {
} }
addWatermark(chart,text){ addWatermark(chart, text){
chart.applyOptions({ // v5 API: watermarks are now created via createTextWatermark plugin
watermark: {visible: true, LightweightCharts.createTextWatermark(chart.panes()[0], {
color: '#DBC29E', horzAlign: 'center',
text: text, vertAlign: 'center',
fontSize: 30, lines: [
fontFamily: 'Roboto', {
fontStyle: 'bold', text: text,
vertAlign: 'center' color: '#DBC29E',
} fontSize: 30,
fontFamily: 'Roboto',
fontStyle: 'bold',
}
]
}); });
} }
@ -181,8 +187,11 @@ class Charts {
bind2charts(){ bind2charts(){
//On change in chart 1 change chart 2 //On change in chart 1 change chart 2
let syncHandler1 = (e) => { let syncHandler1 = (e) => {
// Get the barSpacing(zoom) and position of 1st chart. const now = Date.now();
let barSpacing1 = this.bound_charts[0].timeScale().getBarSpacing(); if (now - this._lastSyncTime < 50) return;
this._lastSyncTime = now;
// Get the barSpacing(zoom) and position of 1st chart (v5 API: options().barSpacing)
let barSpacing1 = this.bound_charts[0].timeScale().options().barSpacing;
let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition(); let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition();
// Apply barSpacing(zoom) and position to 2nd chart. // Apply barSpacing(zoom) and position to 2nd chart.
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 }); this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
@ -191,8 +200,11 @@ class Charts {
//On change in chart 2 change chart 1 //On change in chart 2 change chart 1
let syncHandler2 = (e) => { let syncHandler2 = (e) => {
// Get the barSpacing(zoom) and position of chart 2 const now = Date.now();
let barSpacing2 = this.bound_charts[1].timeScale().getBarSpacing(); if (now - this._lastSyncTime < 50) return;
this._lastSyncTime = now;
// Get the barSpacing(zoom) and position of chart 2 (v5 API: options().barSpacing)
let barSpacing2 = this.bound_charts[1].timeScale().options().barSpacing;
let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition(); let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition();
// Apply barSpacing(zoom) and position to chart 1 // Apply barSpacing(zoom) and position to chart 1
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
@ -203,8 +215,11 @@ class Charts {
//On change to chart 1 change chart 2 and 3 //On change to chart 1 change chart 2 and 3
let syncHandler = (e) => { let syncHandler = (e) => {
// Get the barSpacing(zoom) and position of chart 1 const now = Date.now();
let barSpacing1 = this.bound_charts[0].timeScale().getBarSpacing(); if (now - this._lastSyncTime < 50) return;
this._lastSyncTime = now;
// Get the barSpacing(zoom) and position of chart 1 (v5 API)
let barSpacing1 = this.bound_charts[0].timeScale().options().barSpacing;
let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition(); let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition();
// Apply barSpacing(zoom) and position to new chart // Apply barSpacing(zoom) and position to new chart
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 }); this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
@ -214,8 +229,11 @@ class Charts {
//On change to chart 2 change chart 1 and 3 //On change to chart 2 change chart 1 and 3
let syncHandler2 = (e) => { let syncHandler2 = (e) => {
// Get the barSpacing(zoom) and position of chart 2 const now = Date.now();
let barSpacing2 = this.bound_charts[1].timeScale().getBarSpacing(); if (now - this._lastSyncTime < 50) return;
this._lastSyncTime = now;
// Get the barSpacing(zoom) and position of chart 2 (v5 API)
let barSpacing2 = this.bound_charts[1].timeScale().options().barSpacing;
let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition(); let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition();
// Apply barSpacing(zoom) and position to chart 1 and 3 // Apply barSpacing(zoom) and position to chart 1 and 3
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
@ -225,8 +243,11 @@ class Charts {
//On change to chart 3 change chart 1 and 2 //On change to chart 3 change chart 1 and 2
let syncHandler3 = (e) => { let syncHandler3 = (e) => {
// Get the barSpacing(zoom) and position of new chart const now = Date.now();
let barSpacing2 = this.bound_charts[2].timeScale().getBarSpacing(); if (now - this._lastSyncTime < 50) return;
this._lastSyncTime = now;
// Get the barSpacing(zoom) and position of new chart (v5 API)
let barSpacing2 = this.bound_charts[2].timeScale().options().barSpacing;
let scrollPosition2 = this.bound_charts[2].timeScale().scrollPosition(); let scrollPosition2 = this.bound_charts[2].timeScale().scrollPosition();
// Apply barSpacing(zoom) and position to parent chart // Apply barSpacing(zoom) and position to parent chart
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
@ -236,10 +257,13 @@ class Charts {
} }
bind4charts(){ bind4charts(){
// Sync all 4 charts together // Sync all 4 charts together (v5 API: options().barSpacing)
let syncFromChart = (sourceIndex) => { let syncFromChart = (sourceIndex) => {
return (e) => { return (e) => {
let barSpacing = this.bound_charts[sourceIndex].timeScale().getBarSpacing(); const now = Date.now();
if (now - this._lastSyncTime < 50) return;
this._lastSyncTime = now;
let barSpacing = this.bound_charts[sourceIndex].timeScale().options().barSpacing;
let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition(); let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition();
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
if (i !== sourceIndex) { if (i !== sourceIndex) {
@ -255,10 +279,13 @@ class Charts {
} }
bind5charts(){ bind5charts(){
// Sync all 5 charts together (main + RSI + MACD + %B + Patterns) // Sync all 5 charts together (main + RSI + MACD + %B + Patterns) (v5 API)
let syncFromChart = (sourceIndex) => { let syncFromChart = (sourceIndex) => {
return (e) => { return (e) => {
let barSpacing = this.bound_charts[sourceIndex].timeScale().getBarSpacing(); const now = Date.now();
if (now - this._lastSyncTime < 50) return;
this._lastSyncTime = now;
let barSpacing = this.bound_charts[sourceIndex].timeScale().options().barSpacing;
let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition(); let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition();
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
if (i !== sourceIndex) { if (i !== sourceIndex) {
@ -339,14 +366,20 @@ class Charts {
console.log(`Setting ${markers.length} trade markers on chart`); console.log(`Setting ${markers.length} trade markers on chart`);
// Set markers on the candlestick series // v5 API: use createSeriesMarkers instead of series.setMarkers
this.candleSeries.setMarkers(markers); // Clear existing markers primitive if it exists
if (this.markersPrimitive) {
this.markersPrimitive.setMarkers([]);
}
// Create new markers primitive
this.markersPrimitive = LightweightCharts.createSeriesMarkers(this.candleSeries, markers);
} }
// Clear all trade markers from chart // Clear all trade markers from chart
clearTradeMarkers() { clearTradeMarkers() {
if (this.candleSeries) { // v5 API: clear markers via the markers primitive
this.candleSeries.setMarkers([]); if (this.markersPrimitive) {
this.markersPrimitive.setMarkers([]);
console.log('Trade markers cleared'); console.log('Trade markers cleared');
} }
} }

View File

@ -15,7 +15,7 @@ class User_Interface {
this.account = new Account(); this.account = new Account();
// Register a callback function for when indicator updates are received from the data object // Register a callback function for when indicator updates are received from the data object
this.data.registerCallback_i_updates(this.indicators.update); this.data.registerCallback_i_updates(this.indicators.update.bind(this.indicators));
// Initialize all components after the page has loaded and Blockly is ready // Initialize all components after the page has loaded and Blockly is ready
this.initializeAll(); this.initializeAll();

View File

@ -14,8 +14,12 @@ class Indicator_Output {
this.legend[name].style.left = 3 + 'px'; this.legend[name].style.left = 3 + 'px';
this.legend[name].style.top = 3 + 'px'; this.legend[name].style.top = 3 + 'px';
// subscribe set legend text to crosshair moves // subscribe set legend text to crosshair moves
// v5 API: seriesPrices renamed to seriesData, returns full data item
chart.subscribeCrosshairMove((param) => { chart.subscribeCrosshairMove((param) => {
this.set_legend_text(param.seriesPrices.get(lineSeries), name); const data = param.seriesData.get(lineSeries);
// Extract value from data item (could be {value} or {close} etc)
const priceValue = data ? (data.value !== undefined ? data.value : data.close) : undefined;
this.set_legend_text(priceValue, name);
}); });
} }
@ -88,21 +92,27 @@ class Indicator {
} }
addHist(name, chart, color = '#26a69a') { addHist(name, chart, color = '#26a69a') {
this.hist[name] = chart.addHistogramSeries({ // v5 API: use addSeries with HistogramSeries
this.hist[name] = chart.addSeries(LightweightCharts.HistogramSeries, {
color: color, color: color,
priceFormat: { priceFormat: {
type: 'price', type: 'price',
}, },
priceScaleId: 'volume_ps', priceScaleId: 'volume_ps',
});
// v5: scaleMargins must be set on the price scale, not series options
// Volume should only take up bottom 30% of the chart
chart.priceScale('volume_ps').applyOptions({
scaleMargins: { scaleMargins: {
top: 0, top: 0.7,
bottom: 0, bottom: 0,
}, },
}); });
} }
addLine(name, chart, color, lineWidth) { addLine(name, chart, color, lineWidth) {
this.lines[name] = chart.addLineSeries({ // v5 API: use addSeries with LineSeries
this.lines[name] = chart.addSeries(LightweightCharts.LineSeries, {
color: color, color: color,
lineWidth: lineWidth lineWidth: lineWidth
}); });
@ -111,8 +121,6 @@ class Indicator {
} }
setLine(lineName, data, value_name) { setLine(lineName, data, value_name) {
console.log('indicators[68]: setLine takes:(lineName, data, value_name)');
console.log(lineName, data, value_name);
let priceValue; let priceValue;
@ -143,6 +151,7 @@ class Indicator {
} }
updateDisplay(name, priceValue, value_name) { updateDisplay(name, priceValue, value_name) {
// Try the old element format first (legacy chart-based display)
let element = document.getElementById(this.name + '_' + value_name); let element = document.getElementById(this.name + '_' + value_name);
if (element) { if (element) {
if (typeof priceValue === 'object' && priceValue !== null) { if (typeof priceValue === 'object' && priceValue !== null) {
@ -172,7 +181,36 @@ class Indicator {
element.style.height = 'auto'; // Reset height element.style.height = 'auto'; // Reset height
element.style.height = (element.scrollHeight) + 'px'; // Adjust height based on content element.style.height = (element.scrollHeight) + 'px'; // Adjust height based on content
} else { } else {
console.warn(`Element with ID ${this.name}_${value_name} not found.`); // Try the new card-based display element
const cardElement = document.getElementById(`indicator_card_value_${this.name}`);
if (cardElement) {
let displayValue = '--';
if (typeof priceValue === 'object' && priceValue !== null) {
// For object values, get the first numeric value
const values = Object.values(priceValue).filter(v => typeof v === 'number' && !isNaN(v));
if (values.length > 0) {
displayValue = this._formatDisplayValue(values[0]);
}
} else if (typeof priceValue === 'number' && !isNaN(priceValue)) {
displayValue = this._formatDisplayValue(priceValue);
}
cardElement.textContent = displayValue;
}
// Silently ignore if neither element exists (may be during initialization)
}
}
/**
* Formats a numeric value for display in cards
*/
_formatDisplayValue(value) {
if (value === null || value === undefined || isNaN(value)) return '--';
if (Math.abs(value) >= 1000) {
return value.toFixed(0);
} else if (Math.abs(value) >= 100) {
return value.toFixed(1);
} else {
return value.toFixed(2);
} }
} }
@ -181,8 +219,6 @@ class Indicator {
} }
updateLine(name, data, value_name) { updateLine(name, data, value_name) {
console.log('indicators[68]: updateLine takes:(name, data, value_name)');
console.log(name, data, value_name);
// Check if the data is a multi-value object // Check if the data is a multi-value object
if (typeof data === 'object' && data !== null && value_name in data) { if (typeof data === 'object' && data !== null && value_name in data) {
@ -263,7 +299,6 @@ class SMA extends Indicator {
init(data) { init(data) {
this.setLine('line', data, 'value'); this.setLine('line', data, 'value');
console.log('line data', data);
} }
update(data) { update(data) {
@ -574,8 +609,8 @@ class CandlestickPattern extends Indicator {
} }
addPatternHist(name, chart) { addPatternHist(name, chart) {
// Create histogram series for pattern signals // v5 API: use addSeries with HistogramSeries
this.hist[name] = chart.addHistogramSeries({ this.hist[name] = chart.addSeries(LightweightCharts.HistogramSeries, {
priceFormat: { priceFormat: {
type: 'price', type: 'price',
}, },

File diff suppressed because one or more lines are too long