Compare commits
10 Commits
f9eda9f18b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 02c54cb354 | |||
| c3ca5670e3 | |||
| bde7945a1b | |||
| eccfcc4b79 | |||
| 31ac1ead5b | |||
| a3bf8624fb | |||
| df657ec621 | |||
| 5b6b134c16 | |||
| 0f1ea8976c | |||
| beda3858e9 |
198
index.html
198
index.html
@ -41,46 +41,66 @@
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Chart type button active state */
|
||||
.chart-type-btn.active {
|
||||
background-color: #2d3a4f;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Chart type button hover effect */
|
||||
.chart-type-btn:hover {
|
||||
background-color: #2d3a4f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="flex flex-col h-screen overflow-hidden bg-[#0d1421] text-white font-['Inter']">
|
||||
|
||||
<!-- Top Navigation Bar -->
|
||||
<header class="bg-[#0f131e] fixed top-0 w-full z-[60] h-16 px-6 flex items-center justify-between border-b border-[#1b1f2b]">
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Mobile Menu Button -->
|
||||
<button id="mobileMenuBtn" class="md:hidden w-10 h-10 flex items-center justify-center text-[#8fa2b3] hover:text-white hover:bg-[#2d3a4f] rounded-md transition-colors z-50">
|
||||
<span class="material-symbols-outlined text-lg">menu</span>
|
||||
</button>
|
||||
|
||||
<header class="bg-[#0f131e] fixed top-0 w-full z-[60] h-16 px-4 flex items-center justify-between border-b border-[#1b1f2b]">
|
||||
<div class="flex items-center shrink-0">
|
||||
<!-- Search/Symbol Button -->
|
||||
<div class="hidden md:flex items-center space-x-3 bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]">
|
||||
<div class="flex items-center space-x-2 bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]">
|
||||
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">search</span>
|
||||
<span class="font-bold text-sm text-[#dfe2f2]">BTC/USD</span>
|
||||
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">chevron_right</span>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Search -->
|
||||
<div class="md:hidden flex items-center bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]">
|
||||
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">search</span>
|
||||
<span class="font-bold text-sm text-[#dfe2f2] ml-2">BTC/USD</span>
|
||||
<span class="material-symbols-outlined text-sm text-[#8fa2b3] ml-2">chevron_right</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center gap-2 mr-4">
|
||||
<!-- Desktop Status (Moved inside the left-aligned group) -->
|
||||
<div class="hidden lg:flex items-center gap-2 ml-6 shrink-0 border-l border-[#2d3a4f] pl-6">
|
||||
<div class="w-2 h-2 rounded-full bg-green-500" id="statusDot"></div>
|
||||
<span class="text-xs text-[#8fa2b3]" id="statusText">Live</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 items-center overflow-x-auto no-scrollbar" id="timeframeContainer">
|
||||
<!-- Scrollable Controls Container (Right-aligned on desktop via parent justify-between) -->
|
||||
<div class="flex-1 md:flex-none flex items-center gap-2 ml-4 overflow-x-auto no-scrollbar touch-pan-x justify-end" style="-webkit-overflow-scrolling: touch;">
|
||||
<!-- Chart Type Buttons -->
|
||||
<div class="flex space-x-1 shrink-0">
|
||||
<button class="chart-type-btn w-10 h-10 flex items-center justify-center text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded transition-colors" data-chart-type="candlestick" title="Candlestick">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 5v3M7 16v3M17 3v2M17 11v4"/><rect x="5" y="8" width="4" height="8" rx="0.5"/><rect x="15" y="5" width="4" height="6" rx="0.5"/></svg>
|
||||
</button>
|
||||
<button class="chart-type-btn w-10 h-10 flex items-center justify-center text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded transition-colors" data-chart-type="line" title="Line">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 17l6-6 4 4 8-8"/></svg>
|
||||
</button>
|
||||
<button class="chart-type-btn w-10 h-10 flex items-center justify-center text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded transition-colors" data-chart-type="bar" title="Bar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 5v14M7 10H5M7 15h2M17 5v14M17 7h-2M17 12h2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Separator -->
|
||||
<div class="w-px h-8 bg-[#2d3a4f] mx-1 shrink-0"></div>
|
||||
|
||||
<!-- Timeframes Container -->
|
||||
<div class="flex space-x-1 items-center shrink-0" id="timeframeContainer">
|
||||
<!-- Timeframes injected by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-1 pt-16 overflow-hidden">
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 overflow-y-auto pb-20 md:pb-0 no-scrollbar">
|
||||
<main class="flex-1 flex flex-col overflow-y-auto pb-20 md:pb-0 no-scrollbar">
|
||||
<!-- Price Statistics -->
|
||||
<section id="priceHeader" class="grid grid-cols-2 md:grid-cols-4 gap-2 px-4 py-4 border-b border-[#1e293b] bg-[#0d1421]">
|
||||
<div>
|
||||
@ -102,33 +122,42 @@
|
||||
</section>
|
||||
|
||||
<!-- Chart Container -->
|
||||
<section class="relative w-full bg-[#0d1421] h-[60vh] md:h-[70vh]" data-purpose="chart-container" id="chartWrapper">
|
||||
<section class="relative w-full bg-[#0d1421] flex-1 min-h-[50vh]" data-purpose="chart-container" id="chartWrapper">
|
||||
<div id="chart" class="w-full h-full"></div>
|
||||
|
||||
<!-- Horizontal Drawing Toolbar (Top) -->
|
||||
<div class="absolute top-2 left-1/2 -translate-x-1/2 flex flex-row gap-1 z-30 bg-[#1a2333]/80 backdrop-blur border border-[#2d3a4f] p-1 rounded-md shadow-xl" id="drawingToolbar">
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('trend_line', event)" title="Trend Line">
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="trend_line" onclick="window.activateDrawingTool('trend_line', event)" title="Trend Line">
|
||||
<span class="material-symbols-outlined text-sm">call_split</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('horizontal_line', event)" title="Horizontal Line">
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="horizontal_line" onclick="window.activateDrawingTool('horizontal_line', event)" title="Horizontal Line">
|
||||
<span class="material-symbols-outlined text-sm">swap_horizontal_circle</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('vertical_line', event)" title="Vertical Line">
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="vertical_line" onclick="window.activateDrawingTool('vertical_line', event)" title="Vertical Line">
|
||||
<span class="material-symbols-outlined text-sm">swap_vert</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('rectangle', event)" title="Rectangle">
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="rectangle" onclick="window.activateDrawingTool('rectangle', event)" title="Rectangle">
|
||||
<span class="material-symbols-outlined text-sm">crop_square</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('text', event)" title="Text Label">
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="text" onclick="window.activateDrawingTool('text', event)" title="Text Label">
|
||||
<span class="material-symbols-outlined text-sm">text_fields</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('arrow_up', event)" title="Arrow Up">
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="arrow_pointer" onclick="window.activateDrawingTool('arrow_pointer', event)" title="Arrow Pointer">
|
||||
<span class="material-symbols-outlined text-sm">call_made</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="arrow_up" onclick="window.activateDrawingTool('arrow_up', event)" title="Arrow Up">
|
||||
<span class="material-symbols-outlined text-sm">arrow_upward</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('arrow_down', event)" title="Arrow Down">
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="arrow_down" onclick="window.activateDrawingTool('arrow_down', event)" title="Arrow Down">
|
||||
<span class="material-symbols-outlined text-sm">arrow_downward</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('measure', event)" title="Measure">
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="arrow_left" onclick="window.activateDrawingTool('arrow_left', event)" title="Arrow Left">
|
||||
<span class="material-symbols-outlined text-sm">arrow_back</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="arrow_right" onclick="window.activateDrawingTool('arrow_right', event)" title="Arrow Right">
|
||||
<span class="material-symbols-outlined text-sm">arrow_forward</span>
|
||||
</button>
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="measure" onclick="window.activateDrawingTool('measure', event)" title="Measure">
|
||||
<span class="material-symbols-outlined text-sm">straighten</span>
|
||||
</button>
|
||||
<div class="w-[1px] h-full bg-[#2d3a4f] mx-0.5"></div>
|
||||
@ -314,8 +343,13 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Draggable Divider -->
|
||||
<div id="mainChartResizer" class="h-4 bg-[#1e293b] hover:bg-blue-500 cursor-row-resize shrink-0 transition-colors z-20 flex items-center justify-center group relative">
|
||||
<div class="w-10 h-0.5 bg-gray-500 rounded opacity-50 transition-all"></div>
|
||||
</div>
|
||||
|
||||
<!-- Technical Analysis Section -->
|
||||
<section class="px-4 py-6 bg-[#0d1421] min-h-[300px]" id="taPanel">
|
||||
<section class="px-4 py-4 bg-[#0d1421] shrink-0" id="taPanel">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[#b6c4ff]">analytics</span>
|
||||
@ -324,12 +358,6 @@
|
||||
</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<span id="taLastUpdate" class="text-xs text-gray-600 mr-2 hidden sm:inline-block">--</span>
|
||||
<button class="bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-3 py-1.5 rounded text-xs font-bold hover:shadow-lg transition-all flex items-center gap-1" id="aiBtn" onclick="window.openAIAnalysis()">
|
||||
<span class="material-symbols-outlined text-sm">smart_toy</span> AI Insight
|
||||
</button>
|
||||
<button class="bg-[#1e222d] border border-[#2d3a4f] text-gray-400 px-3 py-1.5 rounded text-xs hover:text-white hover:border-gray-500 transition-colors" onclick="window.refreshTA()">
|
||||
<span class="material-symbols-outlined text-sm align-bottom">refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -414,7 +442,7 @@
|
||||
<span class="material-symbols-outlined text-sm">menu</span>
|
||||
</button>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-0 sidebar-content bg-[#0d1421]">
|
||||
<div class="flex-1 overflow-y-auto p-0 sidebar-content bg-[#0d1421] no-scrollbar">
|
||||
<div class="sidebar-tab-panel active h-full" id="tab-indicators">
|
||||
<div id="indicatorPanel" class="p-2">
|
||||
<!-- Indicators content injected here -->
|
||||
@ -488,6 +516,104 @@
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.hideSidebar = function() {
|
||||
const sidebar = document.getElementById('rightSidebar');
|
||||
if (sidebar && !sidebar.classList.contains('collapsed')) {
|
||||
sidebar.classList.add('collapsed');
|
||||
}
|
||||
};
|
||||
|
||||
window.hideSidebarAndClearDrawings = function() {
|
||||
window.hideSidebar();
|
||||
if (window.dashboard?.drawingManager) {
|
||||
window.dashboard.drawingManager.clearAll();
|
||||
}
|
||||
};
|
||||
|
||||
// Chart Resizer Logic
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const resizer = document.getElementById('mainChartResizer');
|
||||
const chartWrapper = document.getElementById('chartWrapper');
|
||||
|
||||
let isResizing = false;
|
||||
let lastDownY = 0;
|
||||
let savedHeight = localStorage.getItem('chart_height');
|
||||
|
||||
if (resizer && chartWrapper) {
|
||||
if (savedHeight) {
|
||||
chartWrapper.style.flex = 'none';
|
||||
chartWrapper.style.height = savedHeight + 'px';
|
||||
}
|
||||
|
||||
const startResize = (y) => {
|
||||
isResizing = true;
|
||||
lastDownY = y;
|
||||
document.body.style.cursor = 'row-resize';
|
||||
chartWrapper.style.pointerEvents = 'none';
|
||||
};
|
||||
|
||||
const doResize = (y) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const deltaY = y - lastDownY;
|
||||
lastDownY = y;
|
||||
|
||||
const newHeight = chartWrapper.offsetHeight + deltaY;
|
||||
|
||||
if (newHeight > 200 && newHeight < window.innerHeight - 150) {
|
||||
chartWrapper.style.flex = 'none';
|
||||
chartWrapper.style.height = newHeight + 'px';
|
||||
|
||||
if (window.dashboard && window.dashboard.chart) {
|
||||
const container = document.getElementById('chart');
|
||||
window.dashboard.chart.applyOptions({
|
||||
width: container.clientWidth,
|
||||
height: container.clientHeight
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const endResize = () => {
|
||||
if (isResizing) {
|
||||
isResizing = false;
|
||||
document.body.style.cursor = '';
|
||||
chartWrapper.style.pointerEvents = 'auto';
|
||||
const currentHeight = chartWrapper.style.height;
|
||||
localStorage.setItem('chart_height', currentHeight ? parseFloat(currentHeight) : 75);
|
||||
}
|
||||
};
|
||||
|
||||
resizer.addEventListener('mousedown', (e) => startResize(e.clientY));
|
||||
resizer.addEventListener('touchstart', (e) => startResize(e.touches[0].clientY), {passive: false});
|
||||
|
||||
document.addEventListener('mousemove', (e) => doResize(e.clientY));
|
||||
document.addEventListener('touchmove', (e) => doResize(e.touches[0].clientY), {passive: false});
|
||||
|
||||
document.addEventListener('mouseup', endResize);
|
||||
document.addEventListener('touchend', endResize);
|
||||
document.addEventListener('touchcancel', endResize);
|
||||
}
|
||||
});
|
||||
|
||||
// Hide sidebar when clicking on chart container (but not on drawing toolbar or sidebar itself)
|
||||
const chartWrapper = document.getElementById('chartWrapper');
|
||||
if (chartWrapper) {
|
||||
chartWrapper.addEventListener('click', function(e) {
|
||||
const sidebar = document.getElementById('rightSidebar');
|
||||
const drawingToolbar = document.getElementById('drawingToolbar');
|
||||
|
||||
if (!sidebar || sidebar.classList.contains('collapsed')) return;
|
||||
|
||||
const isDrawingToolbar = e.target.closest('#drawingToolbar');
|
||||
const isSidebar = e.target.closest('#rightSidebar');
|
||||
|
||||
if (!isDrawingToolbar && !isSidebar) {
|
||||
hideSidebar();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="./config.js"></script>
|
||||
|
||||
562
js/ui/chart.js
562
js/ui/chart.js
@ -3,55 +3,7 @@ import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals-
|
||||
import { calculateSignalMarkers } from './signal-markers.js';
|
||||
import { updateIndicatorCandles } from './indicators-panel-new.js';
|
||||
import { TimezoneConfig } from '../config/timezone.js';
|
||||
|
||||
export class SeriesMarkersPrimitive {
|
||||
constructor(markers) {
|
||||
this._markers = markers || [];
|
||||
this._paneViews = [new MarkersPaneView(this)];
|
||||
}
|
||||
|
||||
setMarkers(markers) {
|
||||
this._markers = markers;
|
||||
if (this._requestUpdate) {
|
||||
this._requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
attached(param) {
|
||||
this._chart = param.chart;
|
||||
this._series = param.series;
|
||||
this._requestUpdate = param.requestUpdate;
|
||||
this._requestUpdate();
|
||||
}
|
||||
|
||||
detached() {
|
||||
this._chart = undefined;
|
||||
this._series = undefined;
|
||||
this._requestUpdate = undefined;
|
||||
}
|
||||
|
||||
updateAllViews() {
|
||||
this._requestUpdate?.();
|
||||
}
|
||||
|
||||
paneViews() {
|
||||
return this._paneViews;
|
||||
}
|
||||
}
|
||||
|
||||
class MarkersPaneView {
|
||||
constructor(source) {
|
||||
this._source = source;
|
||||
}
|
||||
|
||||
renderer() {
|
||||
return new MarkersRenderer(this._source);
|
||||
}
|
||||
|
||||
zOrder() {
|
||||
return 'top';
|
||||
}
|
||||
}
|
||||
import { DrawingManager } from './drawing-tools.js';
|
||||
|
||||
class MarkersRenderer {
|
||||
constructor(source) {
|
||||
@ -67,7 +19,6 @@ class MarkersRenderer {
|
||||
const chart = this._source._chart;
|
||||
const markers = this._source._markers;
|
||||
|
||||
// Adjust coordinates to bitmap space based on pixel ratio
|
||||
const ratio = scope.horizontalPixelRatio;
|
||||
|
||||
ctx.save();
|
||||
@ -76,10 +27,8 @@ class MarkersRenderer {
|
||||
const timeCoordinate = chart.timeScale().timeToCoordinate(marker.time);
|
||||
if (timeCoordinate === null) continue;
|
||||
|
||||
// Figure out price coordinate
|
||||
let price = marker.price || marker.value;
|
||||
|
||||
// If price wasn't specified but we have the series data, grab the candle high/low
|
||||
if (!price && window.dashboard && window.dashboard.allData) {
|
||||
const data = window.dashboard.allData.get(window.dashboard.currentInterval);
|
||||
if (data) {
|
||||
@ -132,6 +81,55 @@ class MarkersRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
class MarkersPaneView {
|
||||
constructor(source) {
|
||||
this._source = source;
|
||||
}
|
||||
|
||||
renderer() {
|
||||
return new MarkersRenderer(this._source);
|
||||
}
|
||||
|
||||
zOrder() {
|
||||
return 'top';
|
||||
}
|
||||
}
|
||||
|
||||
export class SeriesMarkersPrimitive {
|
||||
constructor(markers) {
|
||||
this._markers = markers || [];
|
||||
this._paneViews = [new MarkersPaneView(this)];
|
||||
}
|
||||
|
||||
setMarkers(markers) {
|
||||
this._markers = markers;
|
||||
if (this._requestUpdate) {
|
||||
this._requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
attached(param) {
|
||||
this._chart = param.chart;
|
||||
this._series = param.series;
|
||||
this._requestUpdate = param.requestUpdate;
|
||||
this._requestUpdate();
|
||||
}
|
||||
|
||||
detached() {
|
||||
this._chart = undefined;
|
||||
this._series = undefined;
|
||||
this._requestUpdate = undefined;
|
||||
}
|
||||
|
||||
updateAllViews() {
|
||||
this._requestUpdate?.();
|
||||
}
|
||||
|
||||
paneViews() {
|
||||
return this._paneViews;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(timestamp) {
|
||||
return TimezoneConfig.formatDate(timestamp);
|
||||
}
|
||||
@ -149,13 +147,12 @@ function throttle(func, limit) {
|
||||
}
|
||||
}
|
||||
|
||||
import { DrawingManager } from './drawing-tools.js';
|
||||
|
||||
export class TradingDashboard {
|
||||
constructor() {
|
||||
this.chart = null;
|
||||
this.candleSeries = null;
|
||||
// Load settings from local storage or defaults
|
||||
this.currentChartType = localStorage.getItem('winterfail_chart_type') || 'candlestick';
|
||||
|
||||
this.symbol = localStorage.getItem('winterfail_symbol') || 'BTC';
|
||||
this.currentInterval = localStorage.getItem('winterfail_interval') || '1d';
|
||||
|
||||
@ -169,18 +166,17 @@ export class TradingDashboard {
|
||||
this.lastCandleTimestamp = null;
|
||||
this.simulationMarkers = [];
|
||||
this.avgPriceSeries = null;
|
||||
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price }
|
||||
this.dailyMAData = new Map();
|
||||
this.currentMouseTime = null;
|
||||
this.drawingManager = null;
|
||||
this.seriesMap = {};
|
||||
|
||||
// Throttled versions of heavy functions
|
||||
this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150);
|
||||
|
||||
this.init();
|
||||
}
|
||||
async loadDailyMAData() {
|
||||
try {
|
||||
// Use 1d interval for this calculation
|
||||
const interval = '1d';
|
||||
let candles = this.allData.get(interval);
|
||||
|
||||
@ -243,7 +239,6 @@ export class TradingDashboard {
|
||||
this.chart.removeSeries(this.avgPriceSeries);
|
||||
}
|
||||
|
||||
// Recreate series to apply custom colors per point via LineSeries data
|
||||
this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||
lineWidth: 2,
|
||||
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||
@ -264,6 +259,7 @@ export class TradingDashboard {
|
||||
|
||||
init() {
|
||||
this.createTimeframeButtons();
|
||||
this.createChartTypeButtons();
|
||||
this.initChart();
|
||||
this.initEventListeners();
|
||||
this.loadInitialData();
|
||||
@ -302,6 +298,43 @@ export class TradingDashboard {
|
||||
});
|
||||
}
|
||||
|
||||
createChartTypeButtons() {
|
||||
const container = document.querySelector('.flex.space-x-1:not(#timeframeContainer)');
|
||||
if (!container) return;
|
||||
|
||||
const chartTypes = [
|
||||
{
|
||||
type: 'candlestick',
|
||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 5v3M7 16v3M17 3v2M17 11v4"/><rect x="5" y="8" width="4" height="8" rx="0.5"/><rect x="15" y="5" width="4" height="6" rx="0.5"/></svg>`,
|
||||
name: 'Candlestick'
|
||||
},
|
||||
{
|
||||
type: 'line',
|
||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 17l6-6 4 4 8-8"/></svg>`,
|
||||
name: 'Line'
|
||||
},
|
||||
{
|
||||
type: 'bar',
|
||||
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 5v14M7 10H5M7 15h2M17 5v14M17 7h-2M17 12h2"/></svg>`,
|
||||
name: 'Bar'
|
||||
}
|
||||
];
|
||||
|
||||
container.innerHTML = '';
|
||||
chartTypes.forEach(chartType => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'chart-type-btn w-10 h-10 flex items-center justify-center text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded transition-colors';
|
||||
btn.dataset.chartType = chartType.type;
|
||||
btn.innerHTML = chartType.icon;
|
||||
btn.title = chartType.name;
|
||||
if (chartType.type === this.currentChartType) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
btn.addEventListener('click', () => this.switchChartType(chartType.type));
|
||||
container.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
initChart() {
|
||||
const chartContainer = document.getElementById('chart');
|
||||
|
||||
@ -324,7 +357,6 @@ export class TradingDashboard {
|
||||
borderColor: COLORS.tvBorder,
|
||||
autoScale: true,
|
||||
mode: 0,
|
||||
// Explicitly enable pinch/scale behavior on the price scale
|
||||
scaleMargins: {
|
||||
top: 0.1,
|
||||
bottom: 0.1,
|
||||
@ -349,18 +381,19 @@ export class TradingDashboard {
|
||||
mouseWheel: true,
|
||||
pressedMouseMove: true,
|
||||
horzTouchDrag: true,
|
||||
vertTouchDrag: true, // Enabled to allow chart-internal vertical scrolling
|
||||
vertTouchDrag: true,
|
||||
},
|
||||
handleScale: {
|
||||
axisPressedMouseMove: true,
|
||||
mouseWheel: true,
|
||||
pinch: true, // This enables pinch-to-zoom on touch devices
|
||||
pinch: true,
|
||||
},
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode.Normal,
|
||||
},
|
||||
});
|
||||
// Setup price format selector change handler
|
||||
const priceInput = document.getElementById("priceFormatInput");
|
||||
|
||||
// Load saved precision
|
||||
let savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision'));
|
||||
if (isNaN(savedPrecision)) savedPrecision = 2;
|
||||
|
||||
@ -383,41 +416,29 @@ export class TradingDashboard {
|
||||
});
|
||||
}
|
||||
|
||||
// Load candle colors from storage or default
|
||||
const savedUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800';
|
||||
const savedDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800';
|
||||
|
||||
const candleUpInput = document.getElementById('candleUpColor');
|
||||
const candleDownInput = document.getElementById('candleDownColor');
|
||||
|
||||
if (candleUpInput) candleUpInput.value = savedUpColor;
|
||||
if (candleDownInput) candleDownInput.value = savedDownColor;
|
||||
if (candleUpInput && this.currentChartType === 'candlestick') candleUpInput.value = savedUpColor;
|
||||
if (candleDownInput && this.currentChartType === 'candlestick') candleDownInput.value = savedDownColor;
|
||||
|
||||
// Calculate initial minMove based on saved precision
|
||||
const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(precision));
|
||||
const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(savedPrecision));
|
||||
|
||||
this.candleSeries = this.chart.addSeries(LightweightCharts.CandlestickSeries, {
|
||||
upColor: savedUpColor,
|
||||
downColor: savedDownColor,
|
||||
borderUpColor: savedUpColor,
|
||||
borderDownColor: savedDownColor,
|
||||
wickUpColor: savedUpColor,
|
||||
wickDownColor: savedDownColor,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove }
|
||||
}, 0);
|
||||
this.candleSeries = this.addSeriesByType(this.currentChartType);
|
||||
|
||||
// Color change listeners
|
||||
if (this.currentChartType === 'line') {
|
||||
this.candleSeries.setData([]);
|
||||
}
|
||||
|
||||
if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') {
|
||||
if (candleUpInput) {
|
||||
candleUpInput.addEventListener('input', (e) => {
|
||||
const color = e.target.value;
|
||||
localStorage.setItem('winterfail_candle_up', color);
|
||||
this.candleSeries.applyOptions({
|
||||
upColor: color,
|
||||
borderUpColor: color,
|
||||
wickUpColor: color
|
||||
});
|
||||
this.applyColorToChartType(color, 'up');
|
||||
});
|
||||
}
|
||||
|
||||
@ -425,25 +446,11 @@ export class TradingDashboard {
|
||||
candleDownInput.addEventListener('input', (e) => {
|
||||
const color = e.target.value;
|
||||
localStorage.setItem('winterfail_candle_down', color);
|
||||
this.candleSeries.applyOptions({
|
||||
downColor: color,
|
||||
borderDownColor: color,
|
||||
wickDownColor: color
|
||||
});
|
||||
this.applyColorToChartType(color, 'down');
|
||||
});
|
||||
}
|
||||
|
||||
this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||
color: '#00bcd4',
|
||||
lineWidth: 1,
|
||||
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||
lastValueVisible: true,
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
title: '',
|
||||
priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove }
|
||||
});
|
||||
|
||||
if (this.candleSeries) {
|
||||
this.currentPriceLine = this.candleSeries.createPriceLine({
|
||||
price: 0,
|
||||
color: '#26a69a',
|
||||
@ -452,18 +459,20 @@ export class TradingDashboard {
|
||||
axisLabelVisible: true,
|
||||
title: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.addAvgPriceSeries();
|
||||
|
||||
this.initPriceScaleControls();
|
||||
this.initNavigationControls();
|
||||
|
||||
// Initialize Drawing Manager
|
||||
this.drawingManager = new DrawingManager(this, chartContainer);
|
||||
window.activateDrawingTool = (tool, event) => {
|
||||
const e = event || window.event;
|
||||
this.drawingManager.setTool(tool, e);
|
||||
};
|
||||
|
||||
// Setup price format selector change handler
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const priceSelect = document.getElementById("priceFormatSelect");
|
||||
if (priceSelect) {
|
||||
@ -478,7 +487,6 @@ export class TradingDashboard {
|
||||
|
||||
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
|
||||
|
||||
// Subscribe to crosshair movement for Best Moving Averages updates
|
||||
this.chart.subscribeCrosshairMove(param => {
|
||||
if (param.time) {
|
||||
this.currentMouseTime = param.time;
|
||||
@ -489,6 +497,10 @@ export class TradingDashboard {
|
||||
}
|
||||
});
|
||||
|
||||
this.chart.subscribeClick(param => {
|
||||
window.hideAllPanels?.();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.chart.applyOptions({
|
||||
width: chartContainer.clientWidth,
|
||||
@ -512,7 +524,6 @@ export class TradingDashboard {
|
||||
const btnSettings = document.getElementById('btnSettings');
|
||||
const settingsPopup = document.getElementById('settingsPopup');
|
||||
|
||||
// Settings Popup Toggle and Outside Click
|
||||
if (btnSettings && settingsPopup) {
|
||||
btnSettings.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
@ -532,14 +543,12 @@ export class TradingDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize state from storage
|
||||
this.scaleState = {
|
||||
autoScale: localStorage.getItem('winterfail_scale_auto') !== 'false',
|
||||
invertScale: localStorage.getItem('winterfail_scale_invert') === 'true',
|
||||
scaleMode: parseInt(localStorage.getItem('winterfail_scale_mode')) || 0
|
||||
};
|
||||
|
||||
// UI Helpers
|
||||
const updateCheckmark = (id, active) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = active ? '✓' : '';
|
||||
@ -553,7 +562,6 @@ export class TradingDashboard {
|
||||
updateCheckmark('modePercentCheck', this.scaleState.scaleMode === 2);
|
||||
updateCheckmark('modeIndexedCheck', this.scaleState.scaleMode === 3);
|
||||
|
||||
// Apply state to chart
|
||||
this.candleSeries.priceScale().applyOptions({
|
||||
autoScale: this.scaleState.autoScale,
|
||||
invertScale: this.scaleState.invertScale,
|
||||
@ -575,14 +583,12 @@ export class TradingDashboard {
|
||||
updateUI();
|
||||
};
|
||||
|
||||
// Add keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
|
||||
|
||||
if (e.key.toLowerCase() === 'a') {
|
||||
window.toggleScaleOption('autoScale');
|
||||
} else if (e.key.toLowerCase() === 'l') {
|
||||
// Toggle between Normal (0) and Log (1)
|
||||
const newMode = this.scaleState.scaleMode === 1 ? 0 : 1;
|
||||
window.setScaleMode(newMode);
|
||||
}
|
||||
@ -641,6 +647,8 @@ export class TradingDashboard {
|
||||
}
|
||||
|
||||
initEventListeners() {
|
||||
this.initChartTypeListeners();
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
|
||||
|
||||
@ -661,18 +669,27 @@ export class TradingDashboard {
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
this.navigateToRecent();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initChartTypeListeners() {
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.chart-type-btn');
|
||||
if (!btn) return;
|
||||
|
||||
const chartType = btn.dataset.chartType;
|
||||
if (chartType) {
|
||||
this.switchChartType(chartType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearIndicatorCaches(clearSignalState = false) {
|
||||
const activeIndicators = window.getActiveIndicators?.() || [];
|
||||
activeIndicators.forEach(indicator => {
|
||||
// Always clear calculation caches
|
||||
indicator.cachedResults = null;
|
||||
indicator.cachedMeta = null;
|
||||
|
||||
// Only clear signal state if explicitly requested (e.g., timeframe change)
|
||||
// Do not clear on new candle completion - preserve signal change tracking
|
||||
if (clearSignalState) {
|
||||
indicator.lastSignalTimestamp = null;
|
||||
indicator.lastSignalType = null;
|
||||
@ -701,7 +718,7 @@ export class TradingDashboard {
|
||||
const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=BTC&interval=${this.currentInterval}&limit=${limit}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.candles && data.candles.length > 0) {
|
||||
if (data.candles && data.candles.length > 0) {
|
||||
const chartData = data.candles.reverse().map(c => ({
|
||||
time: Math.floor(new Date(c.time).getTime() / 1000),
|
||||
open: parseFloat(c.open),
|
||||
@ -715,14 +732,51 @@ if (data.candles && data.candles.length > 0) {
|
||||
const mergedData = this.mergeData(existingData, chartData);
|
||||
this.allData.set(this.currentInterval, mergedData);
|
||||
|
||||
this.candleSeries.setData(mergedData);
|
||||
if (!this.candleSeries) {
|
||||
console.error('[Chart] Candle series not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (fitToContent) {
|
||||
this.chart.timeScale().scrollToRealTime();
|
||||
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const defaultCandles = isMobile ? 125 : 300;
|
||||
|
||||
const dataLength = this.candleSeries.data().length;
|
||||
if (dataLength > defaultCandles) {
|
||||
const logicalRange = {
|
||||
from: dataLength - 1 - defaultCandles,
|
||||
to: dataLength - 1
|
||||
};
|
||||
this.chart.timeScale().setVisibleLogicalRange(logicalRange);
|
||||
}
|
||||
} else if (visibleRange) {
|
||||
this.chart.timeScale().setVisibleLogicalRange(visibleRange);
|
||||
}
|
||||
|
||||
if ((this.currentChartType === 'candlestick' || this.currentChartType === 'bar') &&
|
||||
mergedData.length > 0 &&
|
||||
mergedData[0].hasOwnProperty('open')) {
|
||||
this.candleSeries.setData(mergedData);
|
||||
} else if (this.currentChartType === 'line' &&
|
||||
mergedData.length > 0 &&
|
||||
mergedData[0].hasOwnProperty('close')) {
|
||||
const closePrices = mergedData.map(c => ({
|
||||
time: c.time,
|
||||
value: c.close
|
||||
}));
|
||||
this.candleSeries.setData(closePrices);
|
||||
} else if (mergedData.length > 0 && mergedData[0].hasOwnProperty('value')) {
|
||||
this.candleSeries.setData(mergedData);
|
||||
} else if (mergedData.length > 0) {
|
||||
const closePrices = mergedData.map(c => ({
|
||||
time: c.time,
|
||||
value: c.close || c.value
|
||||
}));
|
||||
this.candleSeries.setData(closePrices);
|
||||
}
|
||||
|
||||
const latest = mergedData[mergedData.length - 1];
|
||||
this.updateStats(latest);
|
||||
}
|
||||
@ -735,13 +789,18 @@ if (data.candles && data.candles.length > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
async loadNewData() {
|
||||
async loadNewData() {
|
||||
if (!this.hasInitialLoad || this.isLoading) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!this.candleSeries) {
|
||||
console.error('[Chart] Candle series not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.candles && data.candles.length > 0) {
|
||||
const atEdge = this.isAtRightEdge();
|
||||
|
||||
@ -761,17 +820,16 @@ async loadNewData() {
|
||||
|
||||
const latest = chartData[chartData.length - 1];
|
||||
|
||||
// Check if new candle detected
|
||||
const isNewCandle = this.lastCandleTimestamp !== null && latest.time > this.lastCandleTimestamp;
|
||||
|
||||
if (isNewCandle) {
|
||||
console.log(`[NewData Load] New candle detected: ${this.lastCandleTimestamp} -> ${latest.time}`);
|
||||
// Clear indicator caches but preserve signal state
|
||||
this.clearIndicatorCaches(false);
|
||||
}
|
||||
|
||||
this.lastCandleTimestamp = latest.time;
|
||||
|
||||
if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') {
|
||||
chartData.forEach(candle => {
|
||||
if (candle.time >= lastTimestamp &&
|
||||
!Number.isNaN(candle.time) &&
|
||||
@ -782,22 +840,33 @@ async loadNewData() {
|
||||
this.candleSeries.update(candle);
|
||||
}
|
||||
});
|
||||
} else if (this.currentChartType === 'line') {
|
||||
const closePrices = chartData.map(c => ({
|
||||
time: c.time,
|
||||
value: c.close
|
||||
}));
|
||||
|
||||
const existingData = this.candleSeries.data();
|
||||
const existingTimeSet = new Set(existingData.map(d => d.time));
|
||||
|
||||
const newDataToAppend = closePrices.filter(c => !existingTimeSet.has(c.time));
|
||||
|
||||
if (newDataToAppend.length > 0) {
|
||||
if (existingData.length === 0) {
|
||||
this.candleSeries.setData(closePrices);
|
||||
} else {
|
||||
newDataToAppend.forEach(point => {
|
||||
this.candleSeries.update(point);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const existingData = this.allData.get(this.currentInterval) || [];
|
||||
this.allData.set(this.currentInterval, this.mergeData(existingData, chartData));
|
||||
|
||||
//console.log(`[NewData Load] Added ${chartData.length} new candles, total in dataset: ${this.allData.get(this.currentInterval).length}`);
|
||||
|
||||
// Auto-scrolling disabled per user request
|
||||
/*
|
||||
if (atEdge) {
|
||||
this.chart.timeScale().scrollToRealTime();
|
||||
}
|
||||
*/
|
||||
|
||||
this.updateStats(latest);
|
||||
|
||||
//console.log('[Chart] Calling drawIndicatorsOnChart after new data');
|
||||
window.drawIndicatorsOnChart?.();
|
||||
window.updateIndicatorCandles?.();
|
||||
|
||||
@ -816,7 +885,7 @@ async loadNewData() {
|
||||
return Array.from(dataMap.values()).sort((a, b) => a.time - b.time);
|
||||
}
|
||||
|
||||
onVisibleRangeChange() {
|
||||
onVisibleRangeChange() {
|
||||
if (!this.hasInitialLoad || this.isLoading) {
|
||||
return;
|
||||
}
|
||||
@ -851,7 +920,6 @@ onVisibleRangeChange() {
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate indicators when data changes
|
||||
if (data.length !== allData?.length) {
|
||||
console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`);
|
||||
}
|
||||
@ -859,7 +927,7 @@ onVisibleRangeChange() {
|
||||
this.loadSignals().catch(e => console.error('Error loading signals:', e));
|
||||
}
|
||||
|
||||
async loadHistoricalData(beforeTime, limit = 1000) {
|
||||
async loadHistoricalData(beforeTime, limit = 1000) {
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
@ -899,9 +967,31 @@ async loadHistoricalData(beforeTime, limit = 1000) {
|
||||
console.log(`[Historical] Oldest: ${new Date(mergedData[0]?.time * 1000).toLocaleDateString()}`);
|
||||
console.log(`[Historical] Newest: ${new Date(mergedData[mergedData.length - 1]?.time * 1000).toLocaleDateString()}`);
|
||||
|
||||
if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') {
|
||||
if (mergedData.length > 0 && mergedData[0].hasOwnProperty('open')) {
|
||||
this.candleSeries.setData(mergedData);
|
||||
} else {
|
||||
const ohlcData = mergedData.map(c => ({
|
||||
time: c.time,
|
||||
open: c.value,
|
||||
high: c.value,
|
||||
low: c.value,
|
||||
close: c.value
|
||||
}));
|
||||
this.candleSeries.setData(ohlcData);
|
||||
}
|
||||
} else {
|
||||
if (mergedData.length > 0 && mergedData[0].hasOwnProperty('close')) {
|
||||
const closePrices = mergedData.map(c => ({
|
||||
time: c.time,
|
||||
value: c.close
|
||||
}));
|
||||
this.candleSeries.setData(closePrices);
|
||||
} else if (mergedData.length > 0) {
|
||||
this.candleSeries.setData(mergedData);
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate indicators and signals with the expanded dataset
|
||||
console.log(`[Historical] Recalculating indicators...`);
|
||||
window.drawIndicatorsOnChart?.();
|
||||
await this.loadSignals();
|
||||
@ -917,7 +1007,7 @@ async loadHistoricalData(beforeTime, limit = 1000) {
|
||||
}
|
||||
}
|
||||
|
||||
async loadTA() {
|
||||
async loadTA() {
|
||||
if (!this.hasInitialLoad) {
|
||||
const time = new Date().toLocaleTimeString();
|
||||
document.getElementById('taContent').innerHTML = `<div class="ta-loading">Loading technical analysis... ${time}</div>`;
|
||||
@ -942,7 +1032,7 @@ async loadTA() {
|
||||
}
|
||||
}
|
||||
|
||||
async loadSignals() {
|
||||
async loadSignals() {
|
||||
try {
|
||||
this.indicatorSignals = calculateAllIndicatorSignals();
|
||||
this.summarySignal = calculateSummarySignal(this.indicatorSignals);
|
||||
@ -960,18 +1050,14 @@ async loadSignals() {
|
||||
|
||||
let markers = calculateSignalMarkers(candles);
|
||||
|
||||
// Merge simulation markers if present
|
||||
if (this.simulationMarkers && this.simulationMarkers.length > 0) {
|
||||
markers = [...markers, ...this.simulationMarkers];
|
||||
}
|
||||
|
||||
// CRITICAL: Filter out any markers with invalid timestamps before passing to chart
|
||||
markers = markers.filter(m => m && m.time !== null && m.time !== undefined && !isNaN(m.time));
|
||||
|
||||
// Re-sort combined markers by time
|
||||
markers.sort((a, b) => a.time - b.time);
|
||||
|
||||
// Use custom primitive for markers in v5
|
||||
try {
|
||||
if (!this.markerPrimitive) {
|
||||
this.markerPrimitive = new SeriesMarkersPrimitive();
|
||||
@ -1004,7 +1090,6 @@ async loadSignals() {
|
||||
const signalColor = indSignal.signal === 'buy' ? '#26a69a' : indSignal.signal === 'sell' ? '#ef5350' : '#787b86';
|
||||
const lastSignalDate = indSignal.lastSignalDate ? formatDate(indSignal.lastSignalDate * 1000) : '-';
|
||||
|
||||
// Format params as "MA(44)" style
|
||||
let paramsStr = '';
|
||||
if (indSignal.params !== null && indSignal.params !== undefined) {
|
||||
paramsStr = `(${indSignal.params})`;
|
||||
@ -1023,16 +1108,13 @@ async loadSignals() {
|
||||
|
||||
const summaryBadge = '';
|
||||
|
||||
// Best Moving Averages Logic (1D based)
|
||||
let displayMA = { ma44: null, ma125: null, price: null, time: null };
|
||||
|
||||
if (this.currentMouseTime && this.dailyMAData.size > 0) {
|
||||
// Find the 1D candle that includes this mouse time
|
||||
const dayTimestamp = Math.floor(this.currentMouseTime / 86400) * 86400;
|
||||
if (this.dailyMAData.has(dayTimestamp)) {
|
||||
displayMA = { ...this.dailyMAData.get(dayTimestamp), time: dayTimestamp };
|
||||
} else {
|
||||
// Fallback to latest if specific day not found
|
||||
const keys = Array.from(this.dailyMAData.keys()).sort((a, b) => b - a);
|
||||
const latestKey = keys[0];
|
||||
displayMA = { ...this.dailyMAData.get(latestKey), time: latestKey };
|
||||
@ -1085,21 +1167,21 @@ async loadSignals() {
|
||||
<div class="ta-section-title">Support / Resistance</div>
|
||||
<div class="ta-level">
|
||||
<span class="ta-level-label">Resistance</span>
|
||||
<span class="ta-level-value">${data.levels.resistance.toFixed(2)}</span>
|
||||
<span class="ta-level-value">${data.levels ? data.levels.resistance.toFixed(2) : 'N/A'}</span>
|
||||
</div>
|
||||
<div class="ta-level">
|
||||
<span class="ta-level-label">Support</span>
|
||||
<span class="ta-level-value">${data.levels.support.toFixed(2)}</span>
|
||||
<span class="ta-level-value">${data.levels ? data.levels.support.toFixed(2) : 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ta-section">
|
||||
<div class="ta-section-title">Price Position</div>
|
||||
<div class="ta-position-bar">
|
||||
<div class="ta-position-marker" style="left: ${Math.min(Math.max(data.levels.position_in_range, 5), 95)}%"></div>
|
||||
<div class="ta-position-marker" style="left: ${data.levels ? Math.min(Math.max(data.levels.position_in_range, 5), 95) : 50}%"></div>
|
||||
</div>
|
||||
<div class="ta-strength" style="margin-top: 8px; font-size: 11px;">
|
||||
${data.levels.position_in_range.toFixed(0)}% in range
|
||||
${data.levels ? data.levels.position_in_range.toFixed(0) : '--'}% in range
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -1142,22 +1224,20 @@ async loadSignals() {
|
||||
}
|
||||
}
|
||||
|
||||
switchTimeframe(interval) {
|
||||
switchTimeframe(interval) {
|
||||
if (!this.intervals.includes(interval) || interval === this.currentInterval) return;
|
||||
|
||||
const oldInterval = this.currentInterval;
|
||||
this.currentInterval = interval;
|
||||
localStorage.setItem('winterfail_interval', interval); // Save setting
|
||||
localStorage.setItem('winterfail_interval', interval);
|
||||
this.hasInitialLoad = false;
|
||||
|
||||
document.querySelectorAll('.timeframe-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.interval === interval);
|
||||
});
|
||||
|
||||
// Clear indicator caches and signal state before switching timeframe
|
||||
this.clearIndicatorCaches(true);
|
||||
|
||||
// Clear old interval data, not new interval
|
||||
this.allData.delete(oldInterval);
|
||||
this.lastCandleTimestamp = null;
|
||||
|
||||
@ -1166,9 +1246,185 @@ switchTimeframe(interval) {
|
||||
window.clearSimulationResults?.();
|
||||
window.updateTimeframeDisplay?.();
|
||||
|
||||
// Notify indicators of timeframe change for recalculation
|
||||
window.onTimeframeChange?.(interval);
|
||||
}
|
||||
|
||||
switchChartType(chartType) {
|
||||
if (chartType === this.currentChartType) return;
|
||||
|
||||
this.currentChartType = chartType;
|
||||
localStorage.setItem('winterfail_chart_type', chartType);
|
||||
|
||||
const allData = this.allData.get(this.currentInterval) || [];
|
||||
const currentData = this.candleSeries ? this.candleSeries.data() : allData;
|
||||
|
||||
this.chart.removeSeries(this.candleSeries);
|
||||
delete this.seriesMap.candlestick;
|
||||
|
||||
if (this.avgPriceSeries) {
|
||||
this.chart.removeSeries(this.avgPriceSeries);
|
||||
this.avgPriceSeries = null;
|
||||
}
|
||||
|
||||
if (this.currentPriceLine) {
|
||||
this.currentPriceLine.applyOptions({
|
||||
visible: false
|
||||
});
|
||||
}
|
||||
|
||||
const newSeries = this.addSeriesByType(chartType);
|
||||
if (!newSeries) {
|
||||
console.error('[Chart] Failed to create series for type:', chartType);
|
||||
return;
|
||||
}
|
||||
this.candleSeries = newSeries;
|
||||
|
||||
this.updateChartTypeButtons();
|
||||
|
||||
if (currentData && currentData.length > 0) {
|
||||
const chartData = this.allData.get(this.currentInterval) || currentData;
|
||||
const hasOHLC = chartData.length > 0 && chartData[0].hasOwnProperty('open');
|
||||
|
||||
if (chartType === 'candlestick' || chartType === 'bar') {
|
||||
if (hasOHLC) {
|
||||
this.candleSeries.setData(chartData);
|
||||
} else {
|
||||
const ohlcData = chartData.map(c => ({
|
||||
time: c.time,
|
||||
open: c.value,
|
||||
high: c.value,
|
||||
low: c.value,
|
||||
close: c.value
|
||||
}));
|
||||
this.candleSeries.setData(ohlcData);
|
||||
}
|
||||
} else if (chartType === 'line') {
|
||||
const closePrices = chartData.length > 0 && chartData[0].hasOwnProperty('close')
|
||||
? chartData.map(c => ({
|
||||
time: c.time,
|
||||
value: c.close
|
||||
}))
|
||||
: chartData;
|
||||
this.candleSeries.setData(closePrices);
|
||||
}
|
||||
}
|
||||
|
||||
window.drawIndicatorsOnChart?.();
|
||||
}
|
||||
|
||||
addSeriesByType(chartType) {
|
||||
let series;
|
||||
const savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')) || 2;
|
||||
const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(savedPrecision));
|
||||
|
||||
switch (chartType) {
|
||||
case 'candlestick':
|
||||
const candleUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800';
|
||||
const candleDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800';
|
||||
series = this.chart.addSeries(LightweightCharts.CandlestickSeries, {
|
||||
upColor: candleUpColor,
|
||||
downColor: candleDownColor,
|
||||
borderUpColor: candleUpColor,
|
||||
borderDownColor: candleDownColor,
|
||||
wickUpColor: candleUpColor,
|
||||
wickDownColor: candleDownColor,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove }
|
||||
});
|
||||
break;
|
||||
case 'line':
|
||||
series = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||
color: '#2196f3',
|
||||
lineWidth: 2,
|
||||
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||
lastValueVisible: true,
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: true,
|
||||
priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove }
|
||||
});
|
||||
break;
|
||||
case 'bar':
|
||||
const barUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800';
|
||||
const barDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800';
|
||||
series = this.chart.addSeries(LightweightCharts.BarSeries, {
|
||||
upColor: barUpColor,
|
||||
downColor: barDownColor,
|
||||
barColors: {
|
||||
up: barUpColor,
|
||||
down: barDownColor
|
||||
},
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove }
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
this.seriesMap[chartType] = series;
|
||||
return series;
|
||||
}
|
||||
|
||||
addAvgPriceSeries() {
|
||||
const savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')) || 2;
|
||||
const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(savedPrecision));
|
||||
|
||||
this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||
color: '#00bcd4',
|
||||
lineWidth: 1,
|
||||
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||
lastValueVisible: true,
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
title: '',
|
||||
priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove }
|
||||
});
|
||||
}
|
||||
|
||||
updateChartTypeButtons() {
|
||||
const buttons = document.querySelectorAll('.chart-type-btn');
|
||||
buttons.forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.dataset.chartType === this.currentChartType) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getChartTypeData() {
|
||||
if (!this.candleSeries) return [];
|
||||
return this.candleSeries.data();
|
||||
}
|
||||
|
||||
applyColorToChartType(color, direction) {
|
||||
if (!this.candleSeries) return;
|
||||
|
||||
if (this.currentChartType === 'candlestick') {
|
||||
const options = {};
|
||||
if (direction === 'up') {
|
||||
options.upColor = color;
|
||||
options.borderUpColor = color;
|
||||
options.wickUpColor = color;
|
||||
} else {
|
||||
options.downColor = color;
|
||||
options.borderDownColor = color;
|
||||
options.wickDownColor = color;
|
||||
}
|
||||
this.candleSeries.applyOptions(options);
|
||||
} else if (this.currentChartType === 'bar') {
|
||||
const options = {
|
||||
barColors: {}
|
||||
};
|
||||
if (direction === 'up') {
|
||||
options.upColor = color;
|
||||
options.barColors.up = color;
|
||||
} else {
|
||||
options.downColor = color;
|
||||
options.barColors.down = color;
|
||||
}
|
||||
this.candleSeries.applyOptions(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshTA() {
|
||||
|
||||
@ -56,6 +56,19 @@ export class DrawingManager {
|
||||
italic: false,
|
||||
alignVert: 'top',
|
||||
alignHorz: 'left'
|
||||
},
|
||||
arrow: {
|
||||
color: '#2962ff',
|
||||
width: 2,
|
||||
style: 0,
|
||||
opacity: 100,
|
||||
text: '',
|
||||
textColor: '#2962ff',
|
||||
fontSize: 14,
|
||||
bold: false,
|
||||
italic: false,
|
||||
alignVert: 'top',
|
||||
alignHorz: 'left'
|
||||
}
|
||||
};
|
||||
|
||||
@ -81,7 +94,7 @@ export class DrawingManager {
|
||||
if (hit) {
|
||||
this.selectedDrawing = hit.drawing;
|
||||
// Only open panel for supported types
|
||||
if (['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(hit.drawing.type)) {
|
||||
if (['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(hit.drawing.type)) {
|
||||
this.toggleSettingsPanel(true);
|
||||
}
|
||||
this.update();
|
||||
@ -229,7 +242,18 @@ export class DrawingManager {
|
||||
|
||||
this.activeTool = tool;
|
||||
|
||||
if (tool === 'cursor') {
|
||||
// Update UI Button states
|
||||
document.querySelectorAll('#drawingToolbar button[data-tool]').forEach(btn => {
|
||||
if (btn.dataset.tool === tool) {
|
||||
btn.classList.add('bg-blue-600/30', 'text-blue-400');
|
||||
btn.classList.remove('text-gray-400', 'hover:bg-[#2d3a4f]');
|
||||
} else {
|
||||
btn.classList.remove('bg-blue-600/30', 'text-blue-400');
|
||||
btn.classList.add('text-gray-400', 'hover:bg-[#2d3a4f]');
|
||||
}
|
||||
});
|
||||
|
||||
if (tool === 'cursor' || !tool) {
|
||||
this.activeTool = null;
|
||||
document.body.style.cursor = 'default';
|
||||
} else if (tool === 'clear') {
|
||||
@ -237,6 +261,8 @@ export class DrawingManager {
|
||||
this.selectedDrawing = null;
|
||||
this.activeTool = null;
|
||||
this.update();
|
||||
// Instantly clear the 'clear' button state
|
||||
setTimeout(() => this.setTool(null), 100);
|
||||
} else {
|
||||
document.body.style.cursor = 'crosshair';
|
||||
this.selectedDrawing = null;
|
||||
@ -391,7 +417,14 @@ export class DrawingManager {
|
||||
color: defs.color,
|
||||
width: defs.width,
|
||||
style: defs.style,
|
||||
opacity: defs.opacity
|
||||
opacity: defs.opacity,
|
||||
text: defs.text,
|
||||
textColor: defs.textColor,
|
||||
fontSize: defs.fontSize,
|
||||
bold: defs.bold,
|
||||
italic: defs.italic,
|
||||
alignVert: defs.alignVert,
|
||||
alignHorz: defs.alignHorz
|
||||
});
|
||||
this.setTool(null);
|
||||
} else if (this.activeTool === 'vertical_line') {
|
||||
@ -402,16 +435,54 @@ export class DrawingManager {
|
||||
color: defs.color,
|
||||
width: defs.width,
|
||||
style: defs.style,
|
||||
opacity: defs.opacity
|
||||
opacity: defs.opacity,
|
||||
text: defs.text,
|
||||
textColor: defs.textColor,
|
||||
fontSize: defs.fontSize,
|
||||
bold: defs.bold,
|
||||
italic: defs.italic,
|
||||
alignVert: defs.alignVert,
|
||||
alignHorz: defs.alignHorz
|
||||
});
|
||||
this.setTool(null);
|
||||
} else if (this.activeTool === 'fib_retracement') {
|
||||
this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' };
|
||||
} else if (this.activeTool === 'arrow_up' || this.activeTool === 'arrow_down') {
|
||||
} else if (this.activeTool === 'arrow_pointer') {
|
||||
const defs = this.defaults.arrow;
|
||||
this.currentDrawing = {
|
||||
type: 'arrow_pointer', p1, p2,
|
||||
color: defs.color,
|
||||
width: defs.width,
|
||||
style: defs.style,
|
||||
opacity: defs.opacity,
|
||||
text: defs.text,
|
||||
textColor: defs.textColor,
|
||||
fontSize: defs.fontSize,
|
||||
bold: defs.bold,
|
||||
italic: defs.italic,
|
||||
alignVert: defs.alignVert,
|
||||
alignHorz: defs.alignHorz
|
||||
};
|
||||
} else if (this.activeTool === 'arrow_up' || this.activeTool === 'arrow_down' || this.activeTool === 'arrow_left' || this.activeTool === 'arrow_right') {
|
||||
let color = '#2962ff'; // Default blue for left/right
|
||||
if (this.activeTool === 'arrow_up') color = '#26a69a'; // Green
|
||||
if (this.activeTool === 'arrow_down') color = '#ef5350'; // Red
|
||||
|
||||
const defs = this.defaults.arrow;
|
||||
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'
|
||||
direction: this.activeTool.replace('arrow_', ''),
|
||||
color: color,
|
||||
width: 2, // default thickness for solid arrows
|
||||
style: defs.style,
|
||||
opacity: defs.opacity,
|
||||
text: defs.text,
|
||||
textColor: defs.textColor,
|
||||
fontSize: defs.fontSize,
|
||||
bold: defs.bold,
|
||||
italic: defs.italic,
|
||||
alignVert: defs.alignVert,
|
||||
alignHorz: defs.alignHorz
|
||||
});
|
||||
this.setTool(null);
|
||||
} else if (this.activeTool === 'text') {
|
||||
@ -759,12 +830,47 @@ export class DrawingManager {
|
||||
|
||||
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
|
||||
|
||||
if (d.type === 'trend_line' || d.type === 'ray') {
|
||||
if (d.type === 'trend_line' || d.type === 'ray' || d.type === 'arrow_pointer') {
|
||||
const x1 = this.timeToX(d.p1.time);
|
||||
const y1 = series.priceToCoordinate(d.p1.price);
|
||||
const x2 = this.timeToX(d.p2.time);
|
||||
const y2 = series.priceToCoordinate(d.p2.price);
|
||||
if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
|
||||
|
||||
if (d.type === 'arrow_pointer') {
|
||||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||
const dist = Math.hypot(x2 - x1, y2 - y1);
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x1, y1);
|
||||
ctx.rotate(angle);
|
||||
|
||||
// Fat arrow dimensions
|
||||
const headLen = Math.min(dist * 0.5, 30 + (d.width || 2) * 5);
|
||||
const headWidth = headLen * 1.2;
|
||||
const tailStartW = (d.width || 2) * 2;
|
||||
const tailEndW = headLen * 0.5;
|
||||
const indent = headLen * 0.2; // Head indentation
|
||||
|
||||
ctx.beginPath();
|
||||
// Top edge of tail
|
||||
ctx.moveTo(0, -tailStartW/2);
|
||||
ctx.lineTo(dist - headLen + indent, -tailEndW/2);
|
||||
// Top edge of head
|
||||
ctx.lineTo(dist - headLen, -headWidth/2);
|
||||
// Tip
|
||||
ctx.lineTo(dist, 0);
|
||||
// Bottom edge of head
|
||||
ctx.lineTo(dist - headLen, headWidth/2);
|
||||
// Bottom edge of tail
|
||||
ctx.lineTo(dist - headLen + indent, tailEndW/2);
|
||||
ctx.lineTo(0, tailStartW/2);
|
||||
ctx.closePath();
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
if (d.type === 'ray') {
|
||||
@ -774,6 +880,7 @@ export class DrawingManager {
|
||||
ctx.lineTo(x2, y2);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Render Text if present
|
||||
if (d.text) {
|
||||
@ -783,13 +890,27 @@ export class DrawingManager {
|
||||
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
|
||||
ctx.font = `${fontStyle}${fontSize}px Inter`;
|
||||
ctx.fillStyle = d.textColor || color;
|
||||
|
||||
let startX = x1, startY = y1, endX = x2, endY = y2;
|
||||
if (x1 > x2 || (x1 === x2 && y1 > y2)) {
|
||||
startX = x2; startY = y2; endX = x1; endY = y1;
|
||||
}
|
||||
|
||||
const angle = Math.atan2(endY - startY, endX - startX);
|
||||
const length = Math.hypot(endX - startX, endY - startY);
|
||||
|
||||
ctx.translate(startX, startY);
|
||||
ctx.rotate(angle);
|
||||
|
||||
ctx.textAlign = d.alignHorz || 'left';
|
||||
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
|
||||
|
||||
const tx = (x1 + x2) / 2;
|
||||
const ty = (y1 + y2) / 2;
|
||||
let tx = 0;
|
||||
if (d.alignHorz === 'center') tx = length / 2;
|
||||
else if (d.alignHorz === 'right') tx = length;
|
||||
|
||||
const offset = 10;
|
||||
const finalTy = d.alignVert === 'top' ? ty - offset : (d.alignVert === 'bottom' ? ty + offset : ty);
|
||||
const finalTy = d.alignVert === 'top' ? -offset : (d.alignVert === 'bottom' ? offset : 0);
|
||||
|
||||
ctx.fillText(d.text, tx, finalTy);
|
||||
ctx.restore();
|
||||
@ -797,9 +918,11 @@ export class DrawingManager {
|
||||
|
||||
if (isSelected) {
|
||||
ctx.setLineDash([]);
|
||||
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.fillStyle = d.type === 'arrow_pointer' ? 'transparent' : '#ffffff';
|
||||
ctx.strokeStyle = d.type === 'arrow_pointer' ? color : d.color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); if (d.type !== 'arrow_pointer') ctx.fill(); ctx.stroke();
|
||||
ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); if (d.type !== 'arrow_pointer') ctx.fill(); ctx.stroke();
|
||||
}
|
||||
}
|
||||
} else if (d.type === 'rectangle') {
|
||||
@ -837,41 +960,46 @@ export class DrawingManager {
|
||||
ctx.beginPath(); ctx.arc(0, y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
}
|
||||
|
||||
// Store label position for hit detection (before rendering)
|
||||
let textX = scope.mediaSize.width / 2;
|
||||
let textY = y;
|
||||
// Render Text and store label position
|
||||
let labelPos = null;
|
||||
if (d.text) {
|
||||
ctx.save();
|
||||
ctx.setLineDash([]);
|
||||
const fontSize = d.fontSize || 14;
|
||||
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
|
||||
const font = `${fontStyle}${fontSize}px Inter`;
|
||||
ctx.font = font;
|
||||
ctx.fillStyle = d.textColor || d.color;
|
||||
|
||||
const alignHorz = d.alignHorz || 'center';
|
||||
ctx.textAlign = alignHorz;
|
||||
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
|
||||
|
||||
let textX = scope.mediaSize.width / 2;
|
||||
if (alignHorz === 'left') textX = 10;
|
||||
else if (alignHorz === 'right') textX = scope.mediaSize.width - 10;
|
||||
|
||||
const offset = 10;
|
||||
let textY = d.alignVert === 'top' ? y - offset : (d.alignVert === 'bottom' ? y + offset : y);
|
||||
|
||||
ctx.fillText(d.text, textX, textY);
|
||||
|
||||
const metrics = ctx.measureText(d.text);
|
||||
const labelWidth = metrics.width + 16;
|
||||
const labelHeight = 24;
|
||||
const defaultLabelX = scope.mediaSize.width / 2 - labelWidth / 2;
|
||||
const offset = 10;
|
||||
|
||||
let defaultLabelX = scope.mediaSize.width / 2 - labelWidth / 2;
|
||||
if (alignHorz === 'left') defaultLabelX = 10;
|
||||
else if (alignHorz === 'right') defaultLabelX = scope.mediaSize.width - labelWidth - 10;
|
||||
|
||||
const defaultLabelY = d.alignVert === 'top' ? y - 40 : (d.alignVert === 'bottom' ? y + 20 : y - 12);
|
||||
|
||||
labelPos = {
|
||||
x: defaultLabelX + (d.labelOffset?.x || 0),
|
||||
y: defaultLabelY + (d.labelOffset?.y || 0),
|
||||
width: labelWidth,
|
||||
height: labelHeight
|
||||
};
|
||||
textX = scope.mediaSize.width / 2;
|
||||
textY = d.alignVert === 'top' ? y - offset : (d.alignVert === 'bottom' ? y + offset : y);
|
||||
}
|
||||
|
||||
// Render Text if present
|
||||
if (d.text) {
|
||||
ctx.save();
|
||||
ctx.setLineDash([]);
|
||||
ctx.font = font;
|
||||
ctx.fillStyle = d.textColor || d.color;
|
||||
|
||||
ctx.textAlign = d.alignHorz || 'center';
|
||||
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
|
||||
ctx.fillText(d.text, textX, textY);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
@ -902,39 +1030,48 @@ export class DrawingManager {
|
||||
ctx.beginPath(); ctx.arc(x, 0, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
}
|
||||
|
||||
// Store label position for hit detection (before rendering)
|
||||
let textX = x;
|
||||
let textY = 0;
|
||||
// Render Text and store label position
|
||||
let labelPos = null;
|
||||
if (d.text) {
|
||||
ctx.save();
|
||||
ctx.setLineDash([]);
|
||||
const fontSize = d.fontSize || 14;
|
||||
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
|
||||
const font = `${fontStyle}${fontSize}px Inter`;
|
||||
ctx.font = font;
|
||||
ctx.fillStyle = d.textColor || d.color;
|
||||
|
||||
const alignHorz = d.alignHorz || 'center';
|
||||
ctx.textAlign = alignHorz;
|
||||
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'bottom' : 'top');
|
||||
|
||||
let textX = x;
|
||||
const offset = 10;
|
||||
|
||||
let textY = scope.mediaSize.height / 2; // Middle
|
||||
if (d.alignVert === 'top') textY = offset;
|
||||
else if (d.alignVert === 'bottom') textY = scope.mediaSize.height - offset;
|
||||
|
||||
ctx.fillText(d.text, textX, textY);
|
||||
|
||||
const metrics = ctx.measureText(d.text);
|
||||
const labelWidth = metrics.width + 16;
|
||||
const labelHeight = 24;
|
||||
const offset = 10;
|
||||
const defaultLabelY = d.alignVert === 'top' ? -50 : (d.alignVert === 'bottom' ? 20 : -12);
|
||||
|
||||
let defaultLabelY = scope.mediaSize.height / 2 - labelHeight / 2; // Middle
|
||||
if (d.alignVert === 'top') defaultLabelY = offset;
|
||||
else if (d.alignVert === 'bottom') defaultLabelY = scope.mediaSize.height - offset - labelHeight;
|
||||
|
||||
let defaultLabelX = x - labelWidth / 2;
|
||||
if (alignHorz === 'left') defaultLabelX = x - labelWidth;
|
||||
else if (alignHorz === 'right') defaultLabelX = x;
|
||||
|
||||
labelPos = {
|
||||
x: x - labelWidth / 2 + (d.labelOffset?.x || 0),
|
||||
x: defaultLabelX + (d.labelOffset?.x || 0),
|
||||
y: defaultLabelY + (d.labelOffset?.y || 0),
|
||||
width: labelWidth,
|
||||
height: labelHeight
|
||||
};
|
||||
textY = d.alignVert === 'top' ? -offset : (d.alignVert === 'bottom' ? offset : 0);
|
||||
}
|
||||
|
||||
// Render Text if present
|
||||
if (d.text) {
|
||||
ctx.save();
|
||||
ctx.setLineDash([]);
|
||||
ctx.font = font;
|
||||
ctx.fillStyle = d.textColor || d.color;
|
||||
|
||||
ctx.textAlign = d.alignHorz || 'center';
|
||||
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
|
||||
ctx.fillText(d.text, textX, textY);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
@ -975,19 +1112,89 @@ export class DrawingManager {
|
||||
const x = this.timeToX(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.fillStyle = d.color;
|
||||
ctx.beginPath();
|
||||
const baseSize = isSelected ? 20 : 15;
|
||||
const w = baseSize * 1.5; // width of arrow head
|
||||
const h = baseSize * 1.5; // height of arrow head
|
||||
const tailW = baseSize * 0.8; // width of tail
|
||||
const tailH = baseSize * 1.2; // length of tail
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
|
||||
if (d.direction === 'up') ctx.rotate(0);
|
||||
else if (d.direction === 'right') ctx.rotate(Math.PI / 2);
|
||||
else if (d.direction === 'down') ctx.rotate(Math.PI);
|
||||
else if (d.direction === 'left') ctx.rotate(-Math.PI / 2);
|
||||
|
||||
// Draw a thick arrow pointing UP from the origin (x,y)
|
||||
ctx.moveTo(0, -h/2 - tailH/2); // Tip of arrow
|
||||
ctx.lineTo(w/2, h/2 - tailH/2); // Right corner of head
|
||||
ctx.lineTo(tailW/2, h/2 - tailH/2); // Inner right corner
|
||||
ctx.lineTo(tailW/2, h/2 + tailH/2); // Bottom right of tail
|
||||
ctx.lineTo(-tailW/2, h/2 + tailH/2); // Bottom left of tail
|
||||
ctx.lineTo(-tailW/2, h/2 - tailH/2); // Inner left corner
|
||||
ctx.lineTo(-w/2, h/2 - tailH/2); // Left corner of head
|
||||
ctx.closePath();
|
||||
|
||||
ctx.fill();
|
||||
if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); }
|
||||
if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2; ctx.stroke(); }
|
||||
ctx.restore();
|
||||
|
||||
// Render Text and store label position
|
||||
let labelPos = null;
|
||||
if (d.text) {
|
||||
ctx.save();
|
||||
ctx.setLineDash([]);
|
||||
const fontSize = d.fontSize || 14;
|
||||
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
|
||||
const font = `${fontStyle}${fontSize}px Inter`;
|
||||
ctx.font = font;
|
||||
ctx.fillStyle = d.textColor || d.color;
|
||||
|
||||
const alignHorz = d.alignHorz || 'center';
|
||||
ctx.textAlign = alignHorz;
|
||||
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'bottom' : 'top');
|
||||
|
||||
let textX = x;
|
||||
let textY = y;
|
||||
|
||||
// Default offset logic for arrows if align isn't strictly overriding
|
||||
const offset = 25;
|
||||
if (d.direction === 'up') textY += offset;
|
||||
else if (d.direction === 'down') textY -= offset;
|
||||
else if (d.direction === 'left') textX += offset;
|
||||
else if (d.direction === 'right') textX -= offset;
|
||||
|
||||
ctx.fillText(d.text, textX, textY);
|
||||
|
||||
const metrics = ctx.measureText(d.text);
|
||||
const labelWidth = metrics.width + 16;
|
||||
const labelHeight = 24;
|
||||
|
||||
let defaultLabelX = textX - labelWidth / 2;
|
||||
if (alignHorz === 'left') defaultLabelX = textX - labelWidth;
|
||||
else if (alignHorz === 'right') defaultLabelX = textX;
|
||||
|
||||
let defaultLabelY = textY;
|
||||
if (d.alignVert === 'top') defaultLabelY = textY;
|
||||
else if (d.alignVert === 'bottom') defaultLabelY = textY - labelHeight;
|
||||
else defaultLabelY = textY - labelHeight / 2;
|
||||
|
||||
labelPos = {
|
||||
x: defaultLabelX + (d.labelOffset?.x || 0),
|
||||
y: defaultLabelY + (d.labelOffset?.y || 0),
|
||||
width: labelWidth,
|
||||
height: labelHeight
|
||||
};
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Store label position for hit detection
|
||||
if (labelPos) {
|
||||
d.labelPos = labelPos;
|
||||
}
|
||||
}
|
||||
} else if (d.type === 'text') {
|
||||
const x = this.timeToX(d.time);
|
||||
@ -1164,6 +1371,7 @@ export class DrawingManager {
|
||||
let startX, startY, initialX, initialY;
|
||||
|
||||
header.addEventListener('mousedown', (e) => {
|
||||
if (e.target.closest('button')) return; // Allow clicking the close button
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
@ -1197,7 +1405,7 @@ export class DrawingManager {
|
||||
|
||||
window.switchTLTab = (tab) => {
|
||||
this.activeTLTab = tab;
|
||||
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type);
|
||||
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type);
|
||||
|
||||
document.getElementById('tlTabStyle').className = tab === 'style' ? 'flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium' : 'flex-1 py-2 text-center text-gray-400 hover:text-white';
|
||||
|
||||
@ -1222,11 +1430,11 @@ export class DrawingManager {
|
||||
window.setTLThickness = (width) => this.applySettings('width', width);
|
||||
window.setTLStyle = (style) => this.applySettings('style', style);
|
||||
window.toggleTLBold = () => {
|
||||
const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type) ? this.selectedDrawing.bold : this.defaults.trend_line.bold;
|
||||
const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type) ? this.selectedDrawing.bold : this.defaults.trend_line.bold;
|
||||
this.applySettings('bold', !current);
|
||||
};
|
||||
window.toggleTLItalic = () => {
|
||||
const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type) ? this.selectedDrawing.italic : this.defaults.trend_line.italic;
|
||||
const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type) ? this.selectedDrawing.italic : this.defaults.trend_line.italic;
|
||||
this.applySettings('italic', !current);
|
||||
};
|
||||
|
||||
@ -1287,6 +1495,10 @@ export class DrawingManager {
|
||||
this.applySettings('text', e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('tlTextInput').addEventListener('keydown', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
document.getElementById('tlAlignVert').addEventListener('change', (e) => {
|
||||
this.applySettings('alignVert', e.target.value);
|
||||
});
|
||||
@ -1344,7 +1556,7 @@ export class DrawingManager {
|
||||
btn.setAttribute('data-active', parseInt(btn.dataset.style) === settings.style);
|
||||
});
|
||||
|
||||
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type);
|
||||
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type);
|
||||
|
||||
const textColorBtn = document.getElementById('tlTextColorBtn');
|
||||
const textPicker = document.getElementById('tlTextColorPicker');
|
||||
@ -1358,7 +1570,6 @@ export class DrawingManager {
|
||||
if (isLineWithText) {
|
||||
if (textColorBtn && textPicker) {
|
||||
textColorBtn.style.backgroundColor = settings.textColor || settings.color;
|
||||
textPicker.classList.remove('hidden');
|
||||
}
|
||||
fontInput.value = settings.fontSize || 14;
|
||||
textInput.value = settings.text || '';
|
||||
@ -1374,7 +1585,7 @@ export class DrawingManager {
|
||||
applySettings(key, value) {
|
||||
if (this.selectedDrawing) {
|
||||
if (key === 'bold' || key === 'italic' || key === 'fontSize' || key === 'text' || key === 'textColor' || key === 'alignVert' || key === 'alignHorz') {
|
||||
const isLineWithText = ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type);
|
||||
const isLineWithText = ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type);
|
||||
if (!isLineWithText) return;
|
||||
this.selectedDrawing[key] = value;
|
||||
} else {
|
||||
@ -1394,6 +1605,8 @@ export class DrawingManager {
|
||||
this.defaults.horizontal_line[key] = value;
|
||||
} else if (drawingType === 'vertical_line') {
|
||||
this.defaults.vertical_line[key] = value;
|
||||
} else if (drawingType === 'arrow' || drawingType === 'arrow_pointer') {
|
||||
this.defaults.arrow[key] = value;
|
||||
} else {
|
||||
this.defaults.trend_line[key] = value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user