Compare commits

..

6 Commits

Author SHA1 Message Date
02c54cb354 feat: set default zoom to 300 candles desktop, 125 candles mobile 2026-03-24 08:24:14 +01:00
c3ca5670e3 style: update chart type icons and header layout for better mobile/desktop experience 2026-03-23 10:49:08 +01:00
bde7945a1b feat: add chart type selector with candlestick, line, and bar charts
- Add chart type selector with 3 chart types: candlestick (default), line, and bar

- Fix data format conversion when switching between OHLC and simple {time,value} formats

- Fix line chart refresh to use update() instead of setData() to preserve chart data

- Remove area chart type and AI Insight/refresh buttons

- Improve data handling in loadData, loadNewData, loadHistoricalData, and switchChartType methods
2026-03-23 09:47:07 +01:00
eccfcc4b79 hide indicators panel when clicking on chart 2026-03-22 21:46:31 +01:00
31ac1ead5b Disable crosshair magnet mode 2026-03-22 21:22:15 +01:00
a3bf8624fb fix: improve chart resizer for mobile with touch support, save chart height, disable sidebar scrollbar 2026-03-22 20:31:59 +01:00
2 changed files with 593 additions and 275 deletions

View File

@ -41,41 +41,61 @@
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: 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> </style>
</head> </head>
<body class="flex flex-col h-screen overflow-hidden bg-[#0d1421] text-white font-['Inter']"> <body class="flex flex-col h-screen overflow-hidden bg-[#0d1421] text-white font-['Inter']">
<!-- Top Navigation Bar --> <!-- 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]"> <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 gap-3"> <div class="flex items-center shrink-0">
<!-- 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>
<!-- Search/Symbol Button --> <!-- 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="material-symbols-outlined text-sm text-[#8fa2b3]">search</span>
<span class="font-bold text-sm text-[#dfe2f2]">BTC/USD</span> <span class="font-bold text-sm text-[#dfe2f2]">BTC/USD</span>
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">chevron_right</span> <span class="material-symbols-outlined text-sm text-[#8fa2b3]">chevron_right</span>
</div> </div>
<!-- Mobile Search --> <!-- Desktop Status (Moved inside the left-aligned group) -->
<div class="md:hidden flex items-center bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]"> <div class="hidden lg:flex items-center gap-2 ml-6 shrink-0 border-l border-[#2d3a4f] pl-6">
<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">
<div class="w-2 h-2 rounded-full bg-green-500" id="statusDot"></div> <div class="w-2 h-2 rounded-full bg-green-500" id="statusDot"></div>
<span class="text-xs text-[#8fa2b3]" id="statusText">Live</span> <span class="text-xs text-[#8fa2b3]" id="statusText">Live</span>
</div> </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 --> <!-- Timeframes injected by JS -->
</div> </div>
</div>
</header> </header>
<div class="flex flex-1 pt-16 overflow-hidden"> <div class="flex flex-1 pt-16 overflow-hidden">
@ -324,8 +344,8 @@
</section> </section>
<!-- Draggable Divider --> <!-- Draggable Divider -->
<div id="mainChartResizer" class="h-1.5 bg-[#1e293b] hover:bg-blue-500 cursor-row-resize shrink-0 transition-colors z-20 flex items-center justify-center group relative"> <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-0 group-hover:opacity-100 transition-opacity"></div> <div class="w-10 h-0.5 bg-gray-500 rounded opacity-50 transition-all"></div>
</div> </div>
<!-- Technical Analysis Section --> <!-- Technical Analysis Section -->
@ -338,12 +358,6 @@
</h2> </h2>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span id="taLastUpdate" class="text-xs text-gray-600 mr-2 hidden sm:inline-block">--</span> <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>
</div> </div>
@ -428,7 +442,7 @@
<span class="material-symbols-outlined text-sm">menu</span> <span class="material-symbols-outlined text-sm">menu</span>
</button> </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 class="sidebar-tab-panel active h-full" id="tab-indicators">
<div id="indicatorPanel" class="p-2"> <div id="indicatorPanel" class="p-2">
<!-- Indicators content injected here --> <!-- Indicators content injected here -->
@ -503,6 +517,20 @@
} }
}; };
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 // Chart Resizer Logic
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const resizer = document.getElementById('mainChartResizer'); const resizer = document.getElementById('mainChartResizer');
@ -510,29 +538,33 @@
let isResizing = false; let isResizing = false;
let lastDownY = 0; let lastDownY = 0;
let savedHeight = localStorage.getItem('chart_height');
if (resizer && chartWrapper) { if (resizer && chartWrapper) {
resizer.addEventListener('mousedown', (e) => { if (savedHeight) {
isResizing = true; chartWrapper.style.flex = 'none';
lastDownY = e.clientY; chartWrapper.style.height = savedHeight + 'px';
document.body.style.cursor = 'row-resize'; }
chartWrapper.style.pointerEvents = 'none'; // Prevent iframe/canvas capturing mouse
});
document.addEventListener('mousemove', (e) => { const startResize = (y) => {
isResizing = true;
lastDownY = y;
document.body.style.cursor = 'row-resize';
chartWrapper.style.pointerEvents = 'none';
};
const doResize = (y) => {
if (!isResizing) return; if (!isResizing) return;
const deltaY = e.clientY - lastDownY; const deltaY = y - lastDownY;
lastDownY = e.clientY; lastDownY = y;
const newHeight = chartWrapper.offsetHeight + deltaY; const newHeight = chartWrapper.offsetHeight + deltaY;
// Min 200px, Max windowHeight - 150px
if (newHeight > 200 && newHeight < window.innerHeight - 150) { if (newHeight > 200 && newHeight < window.innerHeight - 150) {
chartWrapper.style.flex = 'none'; chartWrapper.style.flex = 'none';
chartWrapper.style.height = newHeight + 'px'; chartWrapper.style.height = newHeight + 'px';
// Force chart layout recalculation if applicable
if (window.dashboard && window.dashboard.chart) { if (window.dashboard && window.dashboard.chart) {
const container = document.getElementById('chart'); const container = document.getElementById('chart');
window.dashboard.chart.applyOptions({ window.dashboard.chart.applyOptions({
@ -541,17 +573,47 @@
}); });
} }
} }
}); };
document.addEventListener('mouseup', () => { const endResize = () => {
if (isResizing) { if (isResizing) {
isResizing = false; isResizing = false;
document.body.style.cursor = ''; document.body.style.cursor = '';
chartWrapper.style.pointerEvents = 'auto'; 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>
<script src="./config.js"></script> <script src="./config.js"></script>

View File

@ -3,55 +3,7 @@ import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals-
import { calculateSignalMarkers } from './signal-markers.js'; import { calculateSignalMarkers } from './signal-markers.js';
import { updateIndicatorCandles } from './indicators-panel-new.js'; import { updateIndicatorCandles } from './indicators-panel-new.js';
import { TimezoneConfig } from '../config/timezone.js'; import { TimezoneConfig } from '../config/timezone.js';
import { DrawingManager } from './drawing-tools.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';
}
}
class MarkersRenderer { class MarkersRenderer {
constructor(source) { constructor(source) {
@ -67,7 +19,6 @@ class MarkersRenderer {
const chart = this._source._chart; const chart = this._source._chart;
const markers = this._source._markers; const markers = this._source._markers;
// Adjust coordinates to bitmap space based on pixel ratio
const ratio = scope.horizontalPixelRatio; const ratio = scope.horizontalPixelRatio;
ctx.save(); ctx.save();
@ -76,10 +27,8 @@ class MarkersRenderer {
const timeCoordinate = chart.timeScale().timeToCoordinate(marker.time); const timeCoordinate = chart.timeScale().timeToCoordinate(marker.time);
if (timeCoordinate === null) continue; if (timeCoordinate === null) continue;
// Figure out price coordinate
let price = marker.price || marker.value; 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) { if (!price && window.dashboard && window.dashboard.allData) {
const data = window.dashboard.allData.get(window.dashboard.currentInterval); const data = window.dashboard.allData.get(window.dashboard.currentInterval);
if (data) { 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) { function formatDate(timestamp) {
return TimezoneConfig.formatDate(timestamp); return TimezoneConfig.formatDate(timestamp);
} }
@ -149,13 +147,12 @@ function throttle(func, limit) {
} }
} }
import { DrawingManager } from './drawing-tools.js';
export class TradingDashboard { export class TradingDashboard {
constructor() { constructor() {
this.chart = null; this.chart = null;
this.candleSeries = 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.symbol = localStorage.getItem('winterfail_symbol') || 'BTC';
this.currentInterval = localStorage.getItem('winterfail_interval') || '1d'; this.currentInterval = localStorage.getItem('winterfail_interval') || '1d';
@ -169,18 +166,17 @@ export class TradingDashboard {
this.lastCandleTimestamp = null; this.lastCandleTimestamp = null;
this.simulationMarkers = []; this.simulationMarkers = [];
this.avgPriceSeries = null; this.avgPriceSeries = null;
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price } this.dailyMAData = new Map();
this.currentMouseTime = null; this.currentMouseTime = null;
this.drawingManager = null; this.drawingManager = null;
this.seriesMap = {};
// Throttled versions of heavy functions
this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150); this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150);
this.init(); this.init();
} }
async loadDailyMAData() { async loadDailyMAData() {
try { try {
// Use 1d interval for this calculation
const interval = '1d'; const interval = '1d';
let candles = this.allData.get(interval); let candles = this.allData.get(interval);
@ -243,7 +239,6 @@ export class TradingDashboard {
this.chart.removeSeries(this.avgPriceSeries); this.chart.removeSeries(this.avgPriceSeries);
} }
// Recreate series to apply custom colors per point via LineSeries data
this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, { this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
lineWidth: 2, lineWidth: 2,
lineStyle: LightweightCharts.LineStyle.Solid, lineStyle: LightweightCharts.LineStyle.Solid,
@ -264,6 +259,7 @@ export class TradingDashboard {
init() { init() {
this.createTimeframeButtons(); this.createTimeframeButtons();
this.createChartTypeButtons();
this.initChart(); this.initChart();
this.initEventListeners(); this.initEventListeners();
this.loadInitialData(); 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() { initChart() {
const chartContainer = document.getElementById('chart'); const chartContainer = document.getElementById('chart');
@ -324,7 +357,6 @@ export class TradingDashboard {
borderColor: COLORS.tvBorder, borderColor: COLORS.tvBorder,
autoScale: true, autoScale: true,
mode: 0, mode: 0,
// Explicitly enable pinch/scale behavior on the price scale
scaleMargins: { scaleMargins: {
top: 0.1, top: 0.1,
bottom: 0.1, bottom: 0.1,
@ -349,18 +381,19 @@ export class TradingDashboard {
mouseWheel: true, mouseWheel: true,
pressedMouseMove: true, pressedMouseMove: true,
horzTouchDrag: true, horzTouchDrag: true,
vertTouchDrag: true, // Enabled to allow chart-internal vertical scrolling vertTouchDrag: true,
}, },
handleScale: { handleScale: {
axisPressedMouseMove: true, axisPressedMouseMove: true,
mouseWheel: 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"); const priceInput = document.getElementById("priceFormatInput");
// Load saved precision
let savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')); let savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision'));
if (isNaN(savedPrecision)) savedPrecision = 2; 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 savedUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800';
const savedDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800'; const savedDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800';
const candleUpInput = document.getElementById('candleUpColor'); const candleUpInput = document.getElementById('candleUpColor');
const candleDownInput = document.getElementById('candleDownColor'); const candleDownInput = document.getElementById('candleDownColor');
if (candleUpInput) candleUpInput.value = savedUpColor; if (candleUpInput && this.currentChartType === 'candlestick') candleUpInput.value = savedUpColor;
if (candleDownInput) candleDownInput.value = savedDownColor; 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(savedPrecision));
const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(precision));
this.candleSeries = this.chart.addSeries(LightweightCharts.CandlestickSeries, { this.candleSeries = this.addSeriesByType(this.currentChartType);
upColor: savedUpColor,
downColor: savedDownColor,
borderUpColor: savedUpColor,
borderDownColor: savedDownColor,
wickUpColor: savedUpColor,
wickDownColor: savedDownColor,
lastValueVisible: false,
priceLineVisible: false,
priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove }
}, 0);
// Color change listeners if (this.currentChartType === 'line') {
this.candleSeries.setData([]);
}
if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') {
if (candleUpInput) { if (candleUpInput) {
candleUpInput.addEventListener('input', (e) => { candleUpInput.addEventListener('input', (e) => {
const color = e.target.value; const color = e.target.value;
localStorage.setItem('winterfail_candle_up', color); localStorage.setItem('winterfail_candle_up', color);
this.candleSeries.applyOptions({ this.applyColorToChartType(color, 'up');
upColor: color,
borderUpColor: color,
wickUpColor: color
});
}); });
} }
@ -425,25 +446,11 @@ export class TradingDashboard {
candleDownInput.addEventListener('input', (e) => { candleDownInput.addEventListener('input', (e) => {
const color = e.target.value; const color = e.target.value;
localStorage.setItem('winterfail_candle_down', color); localStorage.setItem('winterfail_candle_down', color);
this.candleSeries.applyOptions({ this.applyColorToChartType(color, 'down');
downColor: color,
borderDownColor: color,
wickDownColor: color
});
}); });
} }
this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, { if (this.candleSeries) {
color: '#00bcd4',
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Solid,
lastValueVisible: true,
priceLineVisible: false,
crosshairMarkerVisible: false,
title: '',
priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove }
});
this.currentPriceLine = this.candleSeries.createPriceLine({ this.currentPriceLine = this.candleSeries.createPriceLine({
price: 0, price: 0,
color: '#26a69a', color: '#26a69a',
@ -452,18 +459,20 @@ export class TradingDashboard {
axisLabelVisible: true, axisLabelVisible: true,
title: '', title: '',
}); });
}
}
this.addAvgPriceSeries();
this.initPriceScaleControls(); this.initPriceScaleControls();
this.initNavigationControls(); this.initNavigationControls();
// Initialize Drawing Manager
this.drawingManager = new DrawingManager(this, chartContainer); this.drawingManager = new DrawingManager(this, chartContainer);
window.activateDrawingTool = (tool, event) => { window.activateDrawingTool = (tool, event) => {
const e = event || window.event; const e = event || window.event;
this.drawingManager.setTool(tool, e); this.drawingManager.setTool(tool, e);
}; };
// Setup price format selector change handler
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const priceSelect = document.getElementById("priceFormatSelect"); const priceSelect = document.getElementById("priceFormatSelect");
if (priceSelect) { if (priceSelect) {
@ -478,7 +487,6 @@ export class TradingDashboard {
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this)); this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
// Subscribe to crosshair movement for Best Moving Averages updates
this.chart.subscribeCrosshairMove(param => { this.chart.subscribeCrosshairMove(param => {
if (param.time) { if (param.time) {
this.currentMouseTime = param.time; this.currentMouseTime = param.time;
@ -489,6 +497,10 @@ export class TradingDashboard {
} }
}); });
this.chart.subscribeClick(param => {
window.hideAllPanels?.();
});
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
this.chart.applyOptions({ this.chart.applyOptions({
width: chartContainer.clientWidth, width: chartContainer.clientWidth,
@ -512,7 +524,6 @@ export class TradingDashboard {
const btnSettings = document.getElementById('btnSettings'); const btnSettings = document.getElementById('btnSettings');
const settingsPopup = document.getElementById('settingsPopup'); const settingsPopup = document.getElementById('settingsPopup');
// Settings Popup Toggle and Outside Click
if (btnSettings && settingsPopup) { if (btnSettings && settingsPopup) {
btnSettings.addEventListener('click', (e) => { btnSettings.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@ -532,14 +543,12 @@ export class TradingDashboard {
} }
} }
// Initialize state from storage
this.scaleState = { this.scaleState = {
autoScale: localStorage.getItem('winterfail_scale_auto') !== 'false', autoScale: localStorage.getItem('winterfail_scale_auto') !== 'false',
invertScale: localStorage.getItem('winterfail_scale_invert') === 'true', invertScale: localStorage.getItem('winterfail_scale_invert') === 'true',
scaleMode: parseInt(localStorage.getItem('winterfail_scale_mode')) || 0 scaleMode: parseInt(localStorage.getItem('winterfail_scale_mode')) || 0
}; };
// UI Helpers
const updateCheckmark = (id, active) => { const updateCheckmark = (id, active) => {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) el.textContent = active ? '✓' : ''; if (el) el.textContent = active ? '✓' : '';
@ -553,7 +562,6 @@ export class TradingDashboard {
updateCheckmark('modePercentCheck', this.scaleState.scaleMode === 2); updateCheckmark('modePercentCheck', this.scaleState.scaleMode === 2);
updateCheckmark('modeIndexedCheck', this.scaleState.scaleMode === 3); updateCheckmark('modeIndexedCheck', this.scaleState.scaleMode === 3);
// Apply state to chart
this.candleSeries.priceScale().applyOptions({ this.candleSeries.priceScale().applyOptions({
autoScale: this.scaleState.autoScale, autoScale: this.scaleState.autoScale,
invertScale: this.scaleState.invertScale, invertScale: this.scaleState.invertScale,
@ -575,14 +583,12 @@ export class TradingDashboard {
updateUI(); updateUI();
}; };
// Add keyboard shortcuts
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
if (e.key.toLowerCase() === 'a') { if (e.key.toLowerCase() === 'a') {
window.toggleScaleOption('autoScale'); window.toggleScaleOption('autoScale');
} else if (e.key.toLowerCase() === 'l') { } else if (e.key.toLowerCase() === 'l') {
// Toggle between Normal (0) and Log (1)
const newMode = this.scaleState.scaleMode === 1 ? 0 : 1; const newMode = this.scaleState.scaleMode === 1 ? 0 : 1;
window.setScaleMode(newMode); window.setScaleMode(newMode);
} }
@ -641,6 +647,8 @@ export class TradingDashboard {
} }
initEventListeners() { initEventListeners() {
this.initChartTypeListeners();
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
@ -661,18 +669,27 @@ export class TradingDashboard {
} else if (e.key === 'ArrowUp') { } else if (e.key === 'ArrowUp') {
this.navigateToRecent(); 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) { clearIndicatorCaches(clearSignalState = false) {
const activeIndicators = window.getActiveIndicators?.() || []; const activeIndicators = window.getActiveIndicators?.() || [];
activeIndicators.forEach(indicator => { activeIndicators.forEach(indicator => {
// Always clear calculation caches
indicator.cachedResults = null; indicator.cachedResults = null;
indicator.cachedMeta = 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) { if (clearSignalState) {
indicator.lastSignalTimestamp = null; indicator.lastSignalTimestamp = null;
indicator.lastSignalType = 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 response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=BTC&interval=${this.currentInterval}&limit=${limit}`);
const data = await response.json(); 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 => ({ const chartData = data.candles.reverse().map(c => ({
time: Math.floor(new Date(c.time).getTime() / 1000), time: Math.floor(new Date(c.time).getTime() / 1000),
open: parseFloat(c.open), open: parseFloat(c.open),
@ -715,14 +732,51 @@ if (data.candles && data.candles.length > 0) {
const mergedData = this.mergeData(existingData, chartData); const mergedData = this.mergeData(existingData, chartData);
this.allData.set(this.currentInterval, mergedData); this.allData.set(this.currentInterval, mergedData);
this.candleSeries.setData(mergedData); if (!this.candleSeries) {
console.error('[Chart] Candle series not initialized');
return;
}
if (fitToContent) { if (fitToContent) {
this.chart.timeScale().scrollToRealTime(); 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) { } else if (visibleRange) {
this.chart.timeScale().setVisibleLogicalRange(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]; const latest = mergedData[mergedData.length - 1];
this.updateStats(latest); this.updateStats(latest);
} }
@ -735,13 +789,18 @@ if (data.candles && data.candles.length > 0) {
} }
} }
async loadNewData() { async loadNewData() {
if (!this.hasInitialLoad || this.isLoading) return; if (!this.hasInitialLoad || this.isLoading) return;
try { try {
const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`); const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`);
const data = await response.json(); const data = await response.json();
if (!this.candleSeries) {
console.error('[Chart] Candle series not initialized');
return;
}
if (data.candles && data.candles.length > 0) { if (data.candles && data.candles.length > 0) {
const atEdge = this.isAtRightEdge(); const atEdge = this.isAtRightEdge();
@ -761,17 +820,16 @@ async loadNewData() {
const latest = chartData[chartData.length - 1]; const latest = chartData[chartData.length - 1];
// Check if new candle detected
const isNewCandle = this.lastCandleTimestamp !== null && latest.time > this.lastCandleTimestamp; const isNewCandle = this.lastCandleTimestamp !== null && latest.time > this.lastCandleTimestamp;
if (isNewCandle) { if (isNewCandle) {
console.log(`[NewData Load] New candle detected: ${this.lastCandleTimestamp} -> ${latest.time}`); console.log(`[NewData Load] New candle detected: ${this.lastCandleTimestamp} -> ${latest.time}`);
// Clear indicator caches but preserve signal state
this.clearIndicatorCaches(false); this.clearIndicatorCaches(false);
} }
this.lastCandleTimestamp = latest.time; this.lastCandleTimestamp = latest.time;
if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') {
chartData.forEach(candle => { chartData.forEach(candle => {
if (candle.time >= lastTimestamp && if (candle.time >= lastTimestamp &&
!Number.isNaN(candle.time) && !Number.isNaN(candle.time) &&
@ -782,22 +840,33 @@ async loadNewData() {
this.candleSeries.update(candle); 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) || []; const existingData = this.allData.get(this.currentInterval) || [];
this.allData.set(this.currentInterval, this.mergeData(existingData, chartData)); 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); this.updateStats(latest);
//console.log('[Chart] Calling drawIndicatorsOnChart after new data');
window.drawIndicatorsOnChart?.(); window.drawIndicatorsOnChart?.();
window.updateIndicatorCandles?.(); window.updateIndicatorCandles?.();
@ -816,7 +885,7 @@ async loadNewData() {
return Array.from(dataMap.values()).sort((a, b) => a.time - b.time); return Array.from(dataMap.values()).sort((a, b) => a.time - b.time);
} }
onVisibleRangeChange() { onVisibleRangeChange() {
if (!this.hasInitialLoad || this.isLoading) { if (!this.hasInitialLoad || this.isLoading) {
return; return;
} }
@ -851,7 +920,6 @@ onVisibleRangeChange() {
} }
} }
// Recalculate indicators when data changes
if (data.length !== allData?.length) { if (data.length !== allData?.length) {
console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`); 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)); this.loadSignals().catch(e => console.error('Error loading signals:', e));
} }
async loadHistoricalData(beforeTime, limit = 1000) { async loadHistoricalData(beforeTime, limit = 1000) {
if (this.isLoading) { if (this.isLoading) {
return; return;
} }
@ -899,9 +967,31 @@ async loadHistoricalData(beforeTime, limit = 1000) {
console.log(`[Historical] Oldest: ${new Date(mergedData[0]?.time * 1000).toLocaleDateString()}`); console.log(`[Historical] Oldest: ${new Date(mergedData[0]?.time * 1000).toLocaleDateString()}`);
console.log(`[Historical] Newest: ${new Date(mergedData[mergedData.length - 1]?.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); 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...`); console.log(`[Historical] Recalculating indicators...`);
window.drawIndicatorsOnChart?.(); window.drawIndicatorsOnChart?.();
await this.loadSignals(); await this.loadSignals();
@ -917,7 +1007,7 @@ async loadHistoricalData(beforeTime, limit = 1000) {
} }
} }
async loadTA() { async loadTA() {
if (!this.hasInitialLoad) { if (!this.hasInitialLoad) {
const time = new Date().toLocaleTimeString(); const time = new Date().toLocaleTimeString();
document.getElementById('taContent').innerHTML = `<div class="ta-loading">Loading technical analysis... ${time}</div>`; document.getElementById('taContent').innerHTML = `<div class="ta-loading">Loading technical analysis... ${time}</div>`;
@ -942,7 +1032,7 @@ async loadTA() {
} }
} }
async loadSignals() { async loadSignals() {
try { try {
this.indicatorSignals = calculateAllIndicatorSignals(); this.indicatorSignals = calculateAllIndicatorSignals();
this.summarySignal = calculateSummarySignal(this.indicatorSignals); this.summarySignal = calculateSummarySignal(this.indicatorSignals);
@ -960,18 +1050,14 @@ async loadSignals() {
let markers = calculateSignalMarkers(candles); let markers = calculateSignalMarkers(candles);
// Merge simulation markers if present
if (this.simulationMarkers && this.simulationMarkers.length > 0) { if (this.simulationMarkers && this.simulationMarkers.length > 0) {
markers = [...markers, ...this.simulationMarkers]; 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)); 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); markers.sort((a, b) => a.time - b.time);
// Use custom primitive for markers in v5
try { try {
if (!this.markerPrimitive) { if (!this.markerPrimitive) {
this.markerPrimitive = new SeriesMarkersPrimitive(); this.markerPrimitive = new SeriesMarkersPrimitive();
@ -1004,7 +1090,6 @@ async loadSignals() {
const signalColor = indSignal.signal === 'buy' ? '#26a69a' : indSignal.signal === 'sell' ? '#ef5350' : '#787b86'; const signalColor = indSignal.signal === 'buy' ? '#26a69a' : indSignal.signal === 'sell' ? '#ef5350' : '#787b86';
const lastSignalDate = indSignal.lastSignalDate ? formatDate(indSignal.lastSignalDate * 1000) : '-'; const lastSignalDate = indSignal.lastSignalDate ? formatDate(indSignal.lastSignalDate * 1000) : '-';
// Format params as "MA(44)" style
let paramsStr = ''; let paramsStr = '';
if (indSignal.params !== null && indSignal.params !== undefined) { if (indSignal.params !== null && indSignal.params !== undefined) {
paramsStr = `(${indSignal.params})`; paramsStr = `(${indSignal.params})`;
@ -1023,16 +1108,13 @@ async loadSignals() {
const summaryBadge = ''; const summaryBadge = '';
// Best Moving Averages Logic (1D based)
let displayMA = { ma44: null, ma125: null, price: null, time: null }; let displayMA = { ma44: null, ma125: null, price: null, time: null };
if (this.currentMouseTime && this.dailyMAData.size > 0) { if (this.currentMouseTime && this.dailyMAData.size > 0) {
// Find the 1D candle that includes this mouse time
const dayTimestamp = Math.floor(this.currentMouseTime / 86400) * 86400; const dayTimestamp = Math.floor(this.currentMouseTime / 86400) * 86400;
if (this.dailyMAData.has(dayTimestamp)) { if (this.dailyMAData.has(dayTimestamp)) {
displayMA = { ...this.dailyMAData.get(dayTimestamp), time: dayTimestamp }; displayMA = { ...this.dailyMAData.get(dayTimestamp), time: dayTimestamp };
} else { } else {
// Fallback to latest if specific day not found
const keys = Array.from(this.dailyMAData.keys()).sort((a, b) => b - a); const keys = Array.from(this.dailyMAData.keys()).sort((a, b) => b - a);
const latestKey = keys[0]; const latestKey = keys[0];
displayMA = { ...this.dailyMAData.get(latestKey), time: latestKey }; displayMA = { ...this.dailyMAData.get(latestKey), time: latestKey };
@ -1085,21 +1167,21 @@ async loadSignals() {
<div class="ta-section-title">Support / Resistance</div> <div class="ta-section-title">Support / Resistance</div>
<div class="ta-level"> <div class="ta-level">
<span class="ta-level-label">Resistance</span> <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>
<div class="ta-level"> <div class="ta-level">
<span class="ta-level-label">Support</span> <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> </div>
<div class="ta-section"> <div class="ta-section">
<div class="ta-section-title">Price Position</div> <div class="ta-section-title">Price Position</div>
<div class="ta-position-bar"> <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>
<div class="ta-strength" style="margin-top: 8px; font-size: 11px;"> <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>
</div> </div>
`; `;
@ -1142,22 +1224,20 @@ async loadSignals() {
} }
} }
switchTimeframe(interval) { switchTimeframe(interval) {
if (!this.intervals.includes(interval) || interval === this.currentInterval) return; if (!this.intervals.includes(interval) || interval === this.currentInterval) return;
const oldInterval = this.currentInterval; const oldInterval = this.currentInterval;
this.currentInterval = interval; this.currentInterval = interval;
localStorage.setItem('winterfail_interval', interval); // Save setting localStorage.setItem('winterfail_interval', interval);
this.hasInitialLoad = false; this.hasInitialLoad = false;
document.querySelectorAll('.timeframe-btn').forEach(btn => { document.querySelectorAll('.timeframe-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.interval === interval); btn.classList.toggle('active', btn.dataset.interval === interval);
}); });
// Clear indicator caches and signal state before switching timeframe
this.clearIndicatorCaches(true); this.clearIndicatorCaches(true);
// Clear old interval data, not new interval
this.allData.delete(oldInterval); this.allData.delete(oldInterval);
this.lastCandleTimestamp = null; this.lastCandleTimestamp = null;
@ -1166,9 +1246,185 @@ switchTimeframe(interval) {
window.clearSimulationResults?.(); window.clearSimulationResults?.();
window.updateTimeframeDisplay?.(); window.updateTimeframeDisplay?.();
// Notify indicators of timeframe change for recalculation
window.onTimeframeChange?.(interval); 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() { export function refreshTA() {