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:
46
index.html
46
index.html
@ -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>
|
||||
|
||||
|
||||
535
js/ui/chart.js
535
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,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;
|
||||
|
||||
@ -669,18 +658,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;
|
||||
@ -709,7 +707,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),
|
||||
@ -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);
|
||||
}
|
||||
@ -743,13 +766,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();
|
||||
|
||||
@ -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?.();
|
||||
|
||||
@ -824,7 +862,7 @@ async loadNewData() {
|
||||
return Array.from(dataMap.values()).sort((a, b) => a.time - b.time);
|
||||
}
|
||||
|
||||
onVisibleRangeChange() {
|
||||
onVisibleRangeChange() {
|
||||
if (!this.hasInitialLoad || this.isLoading) {
|
||||
return;
|
||||
}
|
||||
@ -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...`);
|
||||
}
|
||||
@ -867,7 +904,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;
|
||||
}
|
||||
@ -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();
|
||||
@ -925,7 +984,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>`;
|
||||
@ -950,7 +1009,7 @@ async loadTA() {
|
||||
}
|
||||
}
|
||||
|
||||
async loadSignals() {
|
||||
async loadSignals() {
|
||||
try {
|
||||
this.indicatorSignals = calculateAllIndicatorSignals();
|
||||
this.summarySignal = calculateSummarySignal(this.indicatorSignals);
|
||||
@ -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>
|
||||
`;
|
||||
@ -1150,22 +1201,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;
|
||||
|
||||
@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user