+
refresh Reset price scale
diff --git a/js/ui/chart.js b/js/ui/chart.js
index 5dc4df5..d516fb8 100644
--- a/js/ui/chart.js
+++ b/js/ui/chart.js
@@ -149,14 +149,16 @@ function throttle(func, limit) {
}
}
+import { DrawingManager } from './drawing-tools.js';
+
export class TradingDashboard {
-constructor() {
+ constructor() {
this.chart = null;
this.candleSeries = null;
// Load settings from local storage or defaults
this.symbol = localStorage.getItem('winterfail_symbol') || 'BTC';
this.currentInterval = localStorage.getItem('winterfail_interval') || '1d';
-
+
this.intervals = INTERVALS;
this.allData = new Map();
this.isLoading = false;
@@ -169,14 +171,13 @@ constructor() {
this.avgPriceSeries = null;
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price }
this.currentMouseTime = null;
- this.drawingItems = []; // Store drawing items for management
-
+ this.drawingManager = null;
+
// Throttled versions of heavy functions
this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150);
-
+
this.init();
}
-
async loadDailyMAData() {
try {
// Use 1d interval for this calculation
@@ -454,6 +455,14 @@ constructor() {
this.initPriceScaleControls();
this.initNavigationControls();
+
+ // Initialize Drawing Manager
+ const drawingLayer = document.getElementById('drawingLayer');
+ if (drawingLayer) {
+ this.drawingManager = new DrawingManager(this.chart, this.candleSeries, drawingLayer, chartContainer);
+ window.activateDrawingTool = (tool) => this.drawingManager.setTool(tool);
+ }
+
// Setup price format selector change handler
document.addEventListener("DOMContentLoaded", () => {
const priceSelect = document.getElementById("priceFormatSelect");
@@ -523,9 +532,6 @@ constructor() {
}
}
- // Drawing Tools
- this.initDrawingTools();
-
// Initialize state from storage
this.scaleState = {
autoScale: localStorage.getItem('winterfail_scale_auto') !== 'false',
@@ -583,341 +589,6 @@ constructor() {
});
}
- initDrawingTools() {
- const btnDrawingTools = document.getElementById('btnDrawingTools');
- const drawingToolsPopup = document.getElementById('drawingToolsPopup');
-
- if (btnDrawingTools && drawingToolsPopup) {
- btnDrawingTools.addEventListener('click', (e) => {
- e.stopPropagation();
- drawingToolsPopup.classList.toggle('hidden');
- });
-
- document.addEventListener('click', closeDrawingToolsPopup);
- document.addEventListener('touchstart', closeDrawingToolsPopup, { passive: true });
-
- function closeDrawingToolsPopup(e) {
- const isInside = drawingToolsPopup.contains(e.target) || e.target === btnDrawingTools;
- const isDrawingButton = e.target.closest('#btnDrawingTools');
-
- if (!isInside && !isDrawingButton) {
- drawingToolsPopup.classList.add('hidden');
- }
- }
- }
-
- let isDrawing = false;
- let drawMode = null;
- let startPoint = null;
- let drawLine = null;
- let drawArrow = null;
- let drawText = null;
-
- window.activateDrawingTool = (tool) => {
- if (tool === 'clear') {
- this.clearDrawingTools();
- return;
- }
-
- drawMode = tool;
- isDrawing = true;
-
- const chart = this.chart;
- const container = chart.container();
-
- const onMouseDown = (e) => {
- e.preventDefault();
- const rect = container.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- if (drawMode === 'trend_line' || drawMode === 'horizontal_line' || drawMode === 'vertical_line') {
- this.createLine(x, y);
- } else if (drawMode === 'arrow_up' || drawMode === 'arrow_down') {
- this.createArrow(drawMode === 'arrow_up' ? 'arrowUp' : 'arrowDown', x, y);
- } else if (drawMode === 'text') {
- this.createText(x, y);
- }
- };
-
- const onTouchStart = (e) => {
- e.preventDefault();
- const touch = e.touches[0];
- const rect = container.getBoundingClientRect();
- const x = touch.clientX - rect.left;
- const y = touch.clientY - rect.top;
-
- if (drawMode === 'trend_line' || drawMode === 'horizontal_line' || drawMode === 'vertical_line') {
- this.createLine(x, y);
- } else if (drawMode === 'arrow_up' || drawMode === 'arrow_down') {
- this.createArrow(drawMode === 'arrow_up' ? 'arrowUp' : 'arrowDown', x, y);
- } else if (drawMode === 'text') {
- this.createText(x, y);
- }
- };
-
- container.addEventListener('mousedown', onMouseDown);
- container.addEventListener('touchstart', onTouchStart);
-
- const closeDrawing = () => {
- container.removeEventListener('mousedown', onMouseDown);
- container.removeEventListener('touchstart', onTouchStart);
- document.removeEventListener('click', closeDrawing);
- document.removeEventListener('touchstart', closeDrawing);
- };
-
- document.addEventListener('click', closeDrawing);
- document.addEventListener('touchstart', closeDrawing);
-
- drawingToolsPopup.classList.add('hidden');
- };
- }
-
- createLine(x, y) {
- const chart = this.chart;
- const candleSeries = this.candleSeries;
-
- let lineData = {
- time: [],
- value: []
- };
-
- const line = chart.addLineSeries({
- color: '#2962ff',
- lineWidth: 2,
- lineStyle: LightweightCharts.LineStyle.Solid,
- crosshairMarkerVisible: true,
- crosshairMarkerRadius: 4,
- baseIndex: 0,
- });
-
- let points = [];
-
- const container = chart.container();
- let startTime = null;
- let startPrice = null;
-
- const onMouseDown = (e) => {
- e.preventDefault();
- const rect = container.getBoundingClientRect();
- const clientX = e.clientX;
- const clientY = e.clientY;
-
- const timeCoord = chart.timeScale().coordinateToTime(x);
- const price = chart.priceScale().coordinateToPrice(y);
-
- if (startTime === null) {
- startTime = timeCoord;
- startPrice = price;
-
- line.setData([{
- time: startTime,
- value: startPrice
- }]);
- } else {
- line.setData([
- { time: startTime, value: startPrice },
- { time: timeCoord, value: price }
- ]);
- startTime = null;
- startPrice = null;
- }
- };
-
- const onTouchStart = (e) => {
- e.preventDefault();
- const touch = e.touches[0];
- const rect = container.getBoundingClientRect();
- const clientX = touch.clientX;
- const clientY = touch.clientY;
-
- const timeCoord = chart.timeScale().coordinateToTime(x);
- const price = chart.priceScale().coordinateToPrice(y);
-
- if (startTime === null) {
- startTime = timeCoord;
- startPrice = price;
-
- line.setData([{
- time: startTime,
- value: startPrice
- }]);
- } else {
- line.setData([
- { time: startTime, value: startPrice },
- { time: timeCoord, value: price }
- ]);
- startTime = null;
- startPrice = null;
- }
- };
-
- container.addEventListener('mousedown', onMouseDown);
- container.addEventListener('touchstart', onTouchStart);
-
- const cleanup = () => {
- container.removeEventListener('mousedown', onMouseDown);
- container.removeEventListener('touchstart', onTouchStart);
- };
-
- document.addEventListener('click', cleanup);
- document.addEventListener('touchstart', cleanup);
- }
-
- createArrow(arrowType, x, y) {
- const chart = this.chart;
- const container = chart.container();
-
- const markerPrimitive = new SeriesMarkersPrimitive();
- this.candleSeries.attachPrimitive(markerPrimitive);
-
- const rect = container.getBoundingClientRect();
- const time = chart.timeScale().coordinateToTime(x);
- const price = chart.priceScale().coordinateToPrice(y);
-
- const marker = {
- time: time,
- position: arrowType === 'arrowUp' ? 'belowBar' : 'aboveBar',
- color: arrowType === 'arrowUp' ? '#26a69a' : '#ef5350',
- shape: arrowType === 'arrowUp' ? 'arrowUp' : 'arrowDown',
- text: ''
- };
-
- markerPrimitive.setMarkers([marker]);
-
- let isDragging = false;
- let startX = 0;
- let startY = 0;
- let startTime = time;
- let startPrice = price;
-
- const onMouseDown = (e) => {
- e.preventDefault();
- isDragging = true;
- startX = e.clientX;
- startY = e.clientY;
- };
-
- const onMouseMove = (e) => {
- if (!isDragging) return;
-
- const dx = e.clientX - startX;
- const dy = e.clientY - startY;
- const rect = container.getBoundingClientRect();
-
- const timeCoord = chart.timeScale().coordinateToTime(x + dx);
- const newPrice = chart.priceScale().coordinateToPrice(y + dy);
-
- const newMarker = {
- time: timeCoord,
- position: arrowType === 'arrowUp' ? 'belowBar' : 'aboveBar',
- color: arrowType === 'arrowUp' ? '#26a69a' : '#ef5350',
- shape: arrowType === 'arrowUp' ? 'arrowUp' : 'arrowDown',
- text: ''
- };
-
- markerPrimitive.setMarkers([newMarker]);
- };
-
- const onMouseUp = () => {
- isDragging = false;
- };
-
- container.addEventListener('mousedown', onMouseDown);
- document.addEventListener('mousemove', onMouseMove);
- document.addEventListener('mouseup', onMouseUp);
-
- const cleanup = () => {
- container.removeEventListener('mousedown', onMouseDown);
- document.removeEventListener('mousemove', onMouseMove);
- document.removeEventListener('mouseup', onMouseUp);
- document.removeEventListener('click', cleanup);
- document.removeEventListener('touchstart', cleanup);
- };
-
- document.addEventListener('click', cleanup);
- document.addEventListener('touchstart', cleanup);
- }
-
- createText(x, y) {
- const chart = this.chart;
- const container = chart.container();
-
- const text = prompt('Enter text label:', '');
- if (!text) return;
-
- const rect = container.getBoundingClientRect();
- const time = chart.timeScale().coordinateToTime(x);
- const price = chart.priceScale().coordinateToPrice(y);
-
- const label = chart.addLabel({
- text: text,
- color: '#ffffff',
- fontSize: 11,
- fontFamily: 'Inter',
- fontWeight: 'bold',
- crosshairMarkerVisible: false,
- time: time,
- price: price
- });
-
- let isDragging = false;
- let startX = 0;
- let startY = 0;
- let startTime = time;
- let startPrice = price;
-
- const onMouseDown = (e) => {
- e.preventDefault();
- isDragging = true;
- startX = e.clientX;
- startY = e.clientY;
- };
-
- const onMouseMove = (e) => {
- if (!isDragging) return;
-
- const dx = e.clientX - startX;
- const dy = e.clientY - startY;
-
- const newTime = chart.timeScale().coordinateToTime(x + dx);
- const newPrice = chart.priceScale().coordinateToPrice(y + dy);
-
- label.applyOptions({
- time: newTime,
- price: newPrice
- });
- };
-
- const onMouseUp = () => {
- isDragging = false;
- };
-
- container.addEventListener('mousedown', onMouseDown);
- document.addEventListener('mousemove', onMouseMove);
- document.addEventListener('mouseup', onMouseUp);
-
- const cleanup = () => {
- container.removeEventListener('mousedown', onMouseDown);
- document.removeEventListener('mousemove', onMouseMove);
- document.removeEventListener('mouseup', onMouseUp);
- document.removeEventListener('click', cleanup);
- document.removeEventListener('touchstart', cleanup);
- };
-
- document.addEventListener('click', cleanup);
- document.addEventListener('touchstart', cleanup);
- }
-
- clearDrawingTools() {
- const allSeries = this.chart.series();
- allSeries.forEach(series => {
- if (series.type !== 'Candlestick' && series.type !== 'Line' && series.type !== 'Area') {
- this.chart.removeSeries(series);
- }
- });
- }
-
initNavigationControls() {
const chartWrapper = document.getElementById('chartWrapper');
const navLeft = document.getElementById('navLeft');
diff --git a/js/ui/drawing-tools.js b/js/ui/drawing-tools.js
new file mode 100644
index 0000000..46c2c23
--- /dev/null
+++ b/js/ui/drawing-tools.js
@@ -0,0 +1,483 @@
+/**
+ * Drawing Tools for Lightweight Charts
+ * Implements professional trading chart drawing primitives
+ */
+
+export class DrawingManager {
+ constructor(chart, series, canvas, container) {
+ this.chart = chart;
+ this.series = series;
+ this.canvas = canvas;
+ this.container = container;
+ this.ctx = canvas.getContext('2d');
+ this.drawings = [];
+ this.activeTool = null;
+ this.currentDrawing = null;
+ this.selectedDrawing = null;
+ this.dragMode = null;
+ this.isMouseDown = false;
+
+ // Dragging offsets
+ this.dragStartPos = null;
+ this.startP1 = null;
+ this.startP2 = null;
+ this.startPrice = null;
+ this.startTime = null;
+
+ this.init();
+ }
+
+ init() {
+ const container = this.container;
+
+ container.addEventListener('mousedown', (e) => this.handleMouseDown(e));
+ window.addEventListener('mousemove', (e) => this.handleMouseMove(e));
+ window.addEventListener('mouseup', (e) => this.handleMouseUp(e));
+
+ window.addEventListener('keydown', (e) => {
+ if ((e.key === 'Delete' || e.key === 'Backspace') && this.selectedDrawing) {
+ this.drawings = this.drawings.filter(d => d !== this.selectedDrawing);
+ this.selectedDrawing = null;
+ this.update();
+ }
+ });
+
+ this.drawingPrimitive = {
+ attached: (param) => {
+ this.requestUpdate = param.requestUpdate;
+ },
+ detached: () => {
+ this.requestUpdate = undefined;
+ },
+ updateAllViews: () => {},
+ paneViews: () => [{
+ renderer: () => ({
+ draw: (target) => this.render(target)
+ })
+ }]
+ };
+
+ this.series.attachPrimitive(this.drawingPrimitive);
+ }
+
+ updateChartInteractions() {
+ // Disable chart interactions if a tool is active, if we are currently drawing,
+ // or if we are dragging an existing object.
+ const isInteracting = this.activeTool !== null || this.currentDrawing !== null || (this.selectedDrawing !== null && this.isMouseDown);
+
+ this.chart.applyOptions({
+ handleScroll: {
+ mouseWheel: !isInteracting,
+ pressedMouseMove: !isInteracting,
+ horzTouchDrag: !isInteracting,
+ vertTouchDrag: !isInteracting,
+ },
+ handleScale: {
+ axisPressedMouseMove: !isInteracting,
+ mouseWheel: !isInteracting,
+ pinch: !isInteracting,
+ }
+ });
+ }
+
+ setTool(tool) {
+ this.activeTool = tool;
+
+ if (tool === 'cursor') {
+ this.activeTool = null;
+ document.body.style.cursor = 'default';
+ } else if (tool === 'clear') {
+ this.drawings = [];
+ this.selectedDrawing = null;
+ this.activeTool = null;
+ this.update();
+ } else {
+ document.body.style.cursor = 'crosshair';
+ this.selectedDrawing = null;
+ }
+
+ this.updateChartInteractions();
+ this.update();
+ }
+
+ getMousePos(e) {
+ const rect = this.container.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+ const time = this.chart.timeScale().coordinateToTime(x);
+ const price = this.series.coordinateToPrice(y);
+ return { x, y, time, price };
+ }
+
+ handleMouseDown(e) {
+ const pos = this.getMousePos(e);
+ this.isMouseDown = true;
+ this.dragStartPos = pos;
+
+ // Shift + Click shortcut for Measurement Tool
+ if (e.shiftKey && !this.activeTool) {
+ this.setTool('measure');
+ this.currentDrawing = {
+ type: 'measure',
+ p1: { time: pos.time, price: pos.price },
+ p2: { time: pos.time, price: pos.price },
+ color: '#2962ff'
+ };
+ this.update();
+ return;
+ }
+
+ // Click-Move-Click logic for Measurement Tool
+ if (this.activeTool === 'measure') {
+ if (this.currentDrawing) {
+ // 2nd Click: Finish
+ this.drawings.push(this.currentDrawing);
+ this.currentDrawing = null;
+ this.setTool(null);
+ this.isMouseDown = false;
+ this.update();
+ return;
+ } else {
+ // 1st Click: Start
+ this.currentDrawing = {
+ type: 'measure',
+ p1: { time: pos.time, price: pos.price },
+ p2: { time: pos.time, price: pos.price },
+ color: '#2962ff'
+ };
+ this.update();
+ return;
+ }
+ }
+
+ if (!this.activeTool) {
+ const hit = this.findHit(pos.x, pos.y);
+ if (hit) {
+ this.selectedDrawing = hit.drawing;
+ this.dragMode = hit.part;
+
+ // Store initial state for offset movement
+ const d = this.selectedDrawing;
+ if (d.p1) this.startP1 = { ...d.p1 };
+ if (d.p2) this.startP2 = { ...d.p2 };
+ if (d.price !== undefined) this.startPrice = d.price;
+ if (d.time !== undefined) this.startTime = d.time;
+
+ this.updateChartInteractions(); // Freeze chart for dragging
+ this.update();
+ return;
+ } else {
+ this.selectedDrawing = null;
+ this.dragMode = null;
+ }
+ }
+
+ if (!this.activeTool || pos.time === null || pos.price === null) {
+ this.update();
+ return;
+ }
+
+ const color = '#2962ff';
+ const p1 = { time: pos.time, price: pos.price };
+ const p2 = { time: pos.time, price: pos.price };
+
+ if (this.activeTool === 'trend_line' || this.activeTool === 'ray' || this.activeTool === 'rectangle') {
+ this.currentDrawing = { type: this.activeTool, p1, p2, color };
+ } else if (this.activeTool === 'horizontal_line') {
+ this.drawings.push({ type: 'horizontal_line', price: pos.price, color });
+ this.setTool(null);
+ } else if (this.activeTool === 'vertical_line') {
+ this.drawings.push({ type: 'vertical_line', time: pos.time, color });
+ this.setTool(null);
+ } else if (this.activeTool === 'fib_retracement') {
+ this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' };
+ } else if (this.activeTool === 'measure') {
+ this.currentDrawing = { type: 'measure', p1, p2, color };
+ } else if (this.activeTool === 'arrow_up' || this.activeTool === 'arrow_down') {
+ this.drawings.push({
+ type: 'arrow', time: pos.time, price: pos.price,
+ direction: this.activeTool === 'arrow_up' ? 'up' : 'down',
+ color: this.activeTool === 'arrow_up' ? '#26a69a' : '#ef5350'
+ });
+ this.setTool(null);
+ } else if (this.activeTool === 'text') {
+ const text = prompt('Enter text:');
+ if (text) this.drawings.push({ type: 'text', time: pos.time, price: pos.price, text, color: '#ffffff' });
+ this.setTool(null);
+ }
+ this.update();
+ }
+
+ handleMouseMove(e) {
+ const pos = this.getMousePos(e);
+ if (!this.isMouseDown && !this.activeTool) {
+ const hit = this.findHit(pos.x, pos.y);
+ this.container.style.cursor = hit ? 'pointer' : 'default';
+ }
+
+ // Track measurement even if mouse is up (Click-Move-Click)
+ if (this.currentDrawing && this.currentDrawing.type === 'measure') {
+ if (pos.time !== null) {
+ this.currentDrawing.p2 = { time: pos.time, price: pos.price };
+ this.update();
+ }
+ return;
+ }
+
+ if (!this.isMouseDown) return;
+
+ if (this.selectedDrawing && this.dragMode) {
+ if (pos.time === null || pos.price === null) return;
+ const d = this.selectedDrawing;
+ if (this.dragMode === 'p1') {
+ d.p1 = { time: pos.time, price: pos.price };
+ } else if (this.dragMode === 'p2') {
+ d.p2 = { time: pos.time, price: pos.price };
+ } else if (this.dragMode === 'all') {
+ const timeDiff = pos.time - this.dragStartPos.time;
+ const priceDiff = pos.price - this.dragStartPos.price;
+ if (d.p1 && d.p2) {
+ d.p1.time = this.startP1.time + timeDiff;
+ d.p1.price = this.startP1.price + priceDiff;
+ d.p2.time = this.startP2.time + timeDiff;
+ d.p2.price = this.startP2.price + priceDiff;
+ } else if (d.time !== undefined && d.price !== undefined) {
+ d.time = this.startTime + timeDiff;
+ d.price = this.startPrice + priceDiff;
+ } else if (d.price !== undefined) {
+ d.price = this.startPrice + priceDiff;
+ } else if (d.time !== undefined) {
+ d.time = this.startTime + timeDiff;
+ }
+ } else if (this.dragMode === 'price') {
+ d.price = pos.price;
+ } else if (this.dragMode === 'time') {
+ d.time = pos.time;
+ }
+ this.update();
+ return;
+ }
+
+ if (this.currentDrawing && pos.time !== null) {
+ this.currentDrawing.p2 = { time: pos.time, price: pos.price };
+ this.update();
+ }
+ }
+
+ handleMouseUp() {
+ // Special case for measure tool (Click-Move-Click)
+ if (this.currentDrawing && this.currentDrawing.type === 'measure') {
+ this.isMouseDown = false;
+ this.updateChartInteractions();
+ this.update();
+ return;
+ }
+
+ if (this.currentDrawing) {
+ this.drawings.push(this.currentDrawing);
+ this.currentDrawing = null;
+ this.setTool(null);
+ }
+ this.dragMode = null;
+ this.isMouseDown = false;
+ this.updateChartInteractions(); // Ensure interactions are restored (panning/zooming)
+ this.update();
+ }
+
+ findHit(x, y) {
+ const threshold = 10;
+ for (let i = this.drawings.length - 1; i >= 0; i--) {
+ const d = this.drawings[i];
+ if (d.p1 && d.p2) {
+ const x1 = this.chart.timeScale().timeToCoordinate(d.p1.time);
+ const y1 = this.series.priceToCoordinate(d.p1.price);
+ const x2 = this.chart.timeScale().timeToCoordinate(d.p2.time);
+ const y2 = this.series.priceToCoordinate(d.p2.price);
+ if (x1 === null || y1 === null || x2 === null || y2 === null) continue;
+ if (Math.hypot(x - x1, y - y1) < threshold) return { drawing: d, part: 'p1' };
+ if (Math.hypot(x - x2, y - y2) < threshold) return { drawing: d, part: 'p2' };
+
+ if (d.type === 'rectangle') {
+ if (x >= Math.min(x1, x2) && x <= Math.max(x1, x2) && y >= Math.min(y1, y2) && y <= Math.max(y1, y2)) return { drawing: d, part: 'all' };
+ } else {
+ const dist = this.distToSegment({ x, y }, { x: x1, y: y1 }, { x: x2, y: y2 });
+ if (dist < threshold) return { drawing: d, part: 'all' };
+ }
+ } else if (d.price !== undefined && d.type === 'horizontal_line') {
+ const dy = this.series.priceToCoordinate(d.price);
+ if (Math.abs(y - dy) < threshold) return { drawing: d, part: 'price' };
+ } else if (d.time !== undefined && d.type === 'vertical_line') {
+ const dx = this.chart.timeScale().timeToCoordinate(d.time);
+ if (Math.abs(x - dx) < threshold) return { drawing: d, part: 'time' };
+ } else if (d.time !== undefined && d.price !== undefined) {
+ const dx = this.chart.timeScale().timeToCoordinate(d.time);
+ const dy = this.series.priceToCoordinate(d.price);
+ if (Math.hypot(x - dx, y - dy) < threshold) return { drawing: d, part: 'all' };
+ }
+ }
+ return null;
+ }
+
+ distToSegment(p, v, w) {
+ const l2 = Math.pow(v.x - w.x, 2) + Math.pow(v.y - w.y, 2);
+ if (l2 === 0) return Math.hypot(p.x - v.x, p.y - v.y);
+ let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
+ t = Math.max(0, Math.min(1, t));
+ return Math.hypot(p.x - (v.x + t * (w.x - v.x)), p.y - (v.y + t * (w.y - v.y)));
+ }
+
+ update() {
+ if (this.requestUpdate) this.requestUpdate();
+ }
+
+ render(target) {
+ target.useMediaCoordinateSpace((scope) => {
+ const ctx = scope.context;
+ const chart = this.chart;
+ const series = this.series;
+ const allDrawings = [...this.drawings];
+ if (this.currentDrawing) allDrawings.push(this.currentDrawing);
+
+ allDrawings.forEach(d => {
+ const isSelected = d === this.selectedDrawing;
+ ctx.save();
+ ctx.strokeStyle = d.color;
+ ctx.lineWidth = isSelected ? 3 : 2;
+ if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
+
+ if (d.type === 'trend_line' || d.type === 'ray') {
+ const x1 = chart.timeScale().timeToCoordinate(d.p1.time);
+ const y1 = series.priceToCoordinate(d.p1.price);
+ const x2 = chart.timeScale().timeToCoordinate(d.p2.time);
+ const y2 = series.priceToCoordinate(d.p2.price);
+ if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
+ ctx.beginPath();
+ ctx.moveTo(x1, y1);
+ if (d.type === 'ray') {
+ const angle = Math.atan2(y2 - y1, x2 - x1);
+ ctx.lineTo(x1 + Math.cos(angle) * 10000, y1 + Math.sin(angle) * 10000);
+ } else {
+ ctx.lineTo(x2, y2);
+ }
+ ctx.stroke();
+ if (isSelected) {
+ ctx.fillStyle = '#ffffff';
+ ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
+ ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
+ }
+ }
+ } else if (d.type === 'rectangle') {
+ const x1 = chart.timeScale().timeToCoordinate(d.p1.time);
+ const y1 = series.priceToCoordinate(d.p1.price);
+ const x2 = chart.timeScale().timeToCoordinate(d.p2.time);
+ const y2 = series.priceToCoordinate(d.p2.price);
+ if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
+ ctx.fillStyle = d.color + '22';
+ ctx.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
+ ctx.strokeRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
+ if (isSelected) {
+ ctx.fillStyle = '#ffffff';
+ ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
+ ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
+ }
+ }
+ } else if (d.type === 'horizontal_line') {
+ const y = series.priceToCoordinate(d.price);
+ if (y !== null) {
+ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(scope.mediaSize.width, y); ctx.stroke();
+ }
+ } else if (d.type === 'vertical_line') {
+ const x = chart.timeScale().timeToCoordinate(d.time);
+ if (x !== null) {
+ ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, scope.mediaSize.height); ctx.stroke();
+ }
+ } else if (d.type === 'fib_retracement') {
+ const x1 = chart.timeScale().timeToCoordinate(d.p1.time);
+ const y1 = series.priceToCoordinate(d.p1.price);
+ const x2 = chart.timeScale().timeToCoordinate(d.p2.time);
+ const y2 = series.priceToCoordinate(d.p2.price);
+ if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
+ const levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
+ const range = d.p2.price - d.p1.price;
+ ctx.setLineDash([5, 5]); ctx.lineWidth = 1;
+ levels.forEach(level => {
+ const price = d.p1.price + (range * level);
+ const y = series.priceToCoordinate(price);
+ if (y !== null) {
+ ctx.beginPath(); ctx.moveTo(Math.min(x1, x2), y); ctx.lineTo(Math.max(x1, x2), y); ctx.stroke();
+ ctx.fillStyle = d.color; ctx.font = '10px Inter';
+ ctx.fillText(`${(level * 100).toFixed(1)}% (${price.toFixed(2)})`, Math.max(x1, x2) + 5, y + 3);
+ }
+ });
+ ctx.setLineDash([]); ctx.strokeStyle = d.color + '44';
+ ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
+ if (isSelected) {
+ ctx.fillStyle = '#ffffff'; ctx.strokeStyle = d.color; ctx.lineWidth = 2;
+ ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
+ ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
+ }
+ }
+ } else if (d.type === 'arrow') {
+ const x = chart.timeScale().timeToCoordinate(d.time);
+ const y = series.priceToCoordinate(d.price);
+ if (x !== null && y !== null) {
+ ctx.fillStyle = d.color; ctx.beginPath();
+ const size = isSelected ? 15 : 10;
+ if (d.direction === 'up') {
+ ctx.moveTo(x, y); ctx.lineTo(x - size/2, y + size); ctx.lineTo(x + size/2, y + size);
+ } else {
+ ctx.moveTo(x, y); ctx.lineTo(x - size/2, y - size); ctx.lineTo(x + size/2, y - size);
+ }
+ ctx.fill();
+ if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); }
+ }
+ } else if (d.type === 'text') {
+ const x = chart.timeScale().timeToCoordinate(d.time);
+ const y = series.priceToCoordinate(d.price);
+ if (x !== null && y !== null) {
+ ctx.fillStyle = d.color;
+ ctx.font = isSelected ? 'bold 14px Inter' : '12px Inter';
+ ctx.fillText(d.text, x, y);
+ if (isSelected) {
+ const metrics = ctx.measureText(d.text);
+ ctx.strokeStyle = d.color;
+ ctx.strokeRect(x - 2, y - 12, metrics.width + 4, 16);
+ }
+ }
+ } else if (d.type === 'measure') {
+ const x1 = chart.timeScale().timeToCoordinate(d.p1.time);
+ const y1 = series.priceToCoordinate(d.p1.price);
+ const x2 = chart.timeScale().timeToCoordinate(d.p2.time);
+ const y2 = series.priceToCoordinate(d.p2.price);
+ if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
+ const priceDiff = d.p2.price - d.p1.price;
+ const percentChange = (priceDiff / d.p1.price) * 100;
+ const isPositive = priceDiff >= 0;
+ const measureColor = isPositive ? '#26a69a' : '#ef5350';
+ const timeSpanSeconds = Math.abs(d.p2.time - d.p1.time);
+ const days = (timeSpanSeconds / 86400).toFixed(1);
+ ctx.fillStyle = measureColor + '22';
+ ctx.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
+ ctx.strokeStyle = measureColor;
+ ctx.lineWidth = isSelected ? 2 : 1;
+ ctx.strokeRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
+ ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
+ const text = [`${priceDiff.toFixed(2)} (${percentChange.toFixed(2)}%)`, `${days} days`];
+ const padding = 5, lineHeight = 14, boxWidth = 100, boxHeight = text.length * lineHeight + padding * 2;
+ const boxX = x2 + 10, boxY = y2 - boxHeight / 2;
+ ctx.fillStyle = '#1a2333'; ctx.fillRect(boxX, boxY, boxWidth, boxHeight);
+ ctx.strokeStyle = measureColor; ctx.strokeRect(boxX, boxY, boxWidth, boxHeight);
+ ctx.fillStyle = '#ffffff'; ctx.font = '11px Inter';
+ text.forEach((line, i) => { ctx.fillText(line, boxX + padding, boxY + padding + 10 + (i * lineHeight)); });
+ if (isSelected) {
+ ctx.fillStyle = '#ffffff';
+ ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
+ ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
+ }
+ }
+ }
+ ctx.restore();
+ });
+ });
+ }
+}