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
This commit is contained in:
DiTus
2026-03-23 09:47:07 +01:00
parent eccfcc4b79
commit bde7945a1b
2 changed files with 501 additions and 240 deletions

View File

@ -41,6 +41,17 @@
-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']">
@ -73,9 +84,38 @@
<span class="text-xs text-[#8fa2b3]" id="statusText">Live</span>
</div>
<!-- Mobile Chart Type Selector -->
<div class="md:hidden flex items-center gap-2">
<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">
<span class="material-symbols-outlined text-sm">show_chart</span>
</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">
<span class="material-symbols-outlined text-sm">insert_chart</span>
</button>
</div>
<div class="flex items-center gap-2 overflow-x-auto no-scrollbar">
<!-- Chart Type Buttons -->
<div class="flex space-x-1">
<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">
<span class="material-symbols-outlined text-sm">show_chart</span>
</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">
<span class="material-symbols-outlined text-sm">insert_chart</span>
</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">
<span class="material-symbols-outlined text-sm">scatter_plot</span>
</button>
</div>
<!-- Timeframe Separator -->
<div class="w-px h-8 bg-[#2d3a4f] mx-0.5"></div>
<!-- Timeframes -->
<div class="flex space-x-1 items-center overflow-x-auto no-scrollbar" id="timeframeContainer">
<!-- Timeframes injected by JS -->
</div>
</div>
</header>
<div class="flex flex-1 pt-16 overflow-hidden">
@ -338,12 +378,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>

View File

@ -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,32 @@ export class TradingDashboard {
});
}
createChartTypeButtons() {
const container = document.querySelector('.flex.space-x-1:not(#timeframeContainer)');
if (!container) return;
const chartTypes = [
{ type: 'candlestick', icon: 'show_chart', name: 'Candlestick' },
{ type: 'line', icon: 'insert_chart', name: 'Line' },
{ type: 'bar', icon: 'scatter_plot', 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 = `<span class="material-symbols-outlined text-sm">${chartType.icon}</span>`;
btn.title = chartType.name;
if (chartType.type === this.currentChartType) {
btn.style.backgroundColor = '#2d3a4f';
btn.classList.add('text-blue-400');
}
btn.addEventListener('click', () => this.switchChartType(chartType.type));
container.appendChild(btn);
});
}
initChart() {
const chartContainer = document.getElementById('chart');
@ -324,7 +346,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,21 +370,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;
@ -386,41 +405,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');
});
}
@ -428,25 +435,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',
@ -455,18 +448,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) {
@ -481,7 +476,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;
@ -492,7 +486,6 @@ export class TradingDashboard {
}
});
// Hide indicators panel when clicking on chart
this.chart.subscribeClick(param => {
window.hideAllPanels?.();
});
@ -520,7 +513,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();
@ -540,14 +532,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 ? '✓' : '';
@ -561,7 +551,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,
@ -583,14 +572,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);
}
@ -649,6 +636,8 @@ export class TradingDashboard {
}
initEventListeners() {
this.initChartTypeListeners();
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
@ -672,15 +661,24 @@ export class TradingDashboard {
});
}
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;
@ -723,7 +721,10 @@ 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();
@ -731,6 +732,28 @@ if (data.candles && data.candles.length > 0) {
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);
}
@ -750,6 +773,11 @@ async loadNewData() {
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();
@ -769,17 +797,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) &&
@ -790,22 +817,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?.();
@ -859,7 +897,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...`);
}
@ -907,9 +944,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();
@ -968,18 +1027,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();
@ -1012,7 +1067,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})`;
@ -1031,16 +1085,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 };
@ -1093,21 +1144,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>
`;
@ -1155,17 +1206,15 @@ switchTimeframe(interval) {
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;
@ -1174,9 +1223,187 @@ 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('text-blue-400');
btn.style.backgroundColor = '';
if (btn.dataset.chartType === this.currentChartType) {
btn.style.backgroundColor = '#2d3a4f';
btn.classList.add('text-blue-400');
}
});
}
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() {