mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-07-28 02:47:47 +00:00
dce54845c2
- Built event-driven backtesting engine with spread/slippage modeling, 3-TP partial closes, trailing stops, and rich trade logging (20+ features) - Implemented 5 strategy signal generators (MA Breakout, VWAP Reversal, Key Level Breakout, EMA Ribbon Scalp, Momentum Exhaustion) - Full indicator library (EMA, SMA, RSI, ATR, MACD, ADX, Stochastic, Session VWAP bands, swing points, key levels, RSI divergence) - Data pipeline: Dukascopy download, validation, 70/30 train/test split - Baseline results: all 5 strategies generate 200+ trades on training data (Jan 2021 - Aug 2023), best profit factors 0.82-0.96 on select pairs - Trade logs and reports saved for Phase 3 ML feature engineering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
615 lines
18 KiB
HTML
615 lines
18 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Chart - fx-quant{% endblock %}
|
|
|
|
{% block content %}
|
|
<style>
|
|
/* Dark chart container - scoped to chart page only */
|
|
.chart-wrapper {
|
|
background: #131722;
|
|
border-radius: 8px;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
}
|
|
.chart-toolbar {
|
|
background: #1e222d;
|
|
padding: 8px 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
border-bottom: 1px solid #2a2e39;
|
|
}
|
|
.chart-toolbar label {
|
|
color: #787b86;
|
|
font-size: 0.8rem;
|
|
margin: 0;
|
|
}
|
|
.chart-toolbar select {
|
|
background: #2a2e39;
|
|
color: #d1d4dc;
|
|
border: 1px solid #363a45;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
font-size: 0.82rem;
|
|
outline: none;
|
|
}
|
|
.chart-toolbar select:focus {
|
|
border-color: #2962ff;
|
|
}
|
|
|
|
/* Indicator dropdown */
|
|
.ind-dropdown {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
.ind-btn {
|
|
background: #2a2e39;
|
|
color: #d1d4dc;
|
|
border: 1px solid #363a45;
|
|
border-radius: 4px;
|
|
padding: 4px 12px;
|
|
font-size: 0.82rem;
|
|
cursor: pointer;
|
|
}
|
|
.ind-btn:hover { border-color: #2962ff; }
|
|
.ind-panel {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
z-index: 100;
|
|
background: #1e222d;
|
|
border: 1px solid #363a45;
|
|
border-radius: 6px;
|
|
padding: 8px 0;
|
|
min-width: 220px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
|
}
|
|
.ind-panel.open { display: block; }
|
|
.ind-panel .ind-group {
|
|
padding: 4px 12px 2px;
|
|
color: #787b86;
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.ind-panel label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 4px 12px;
|
|
color: #d1d4dc;
|
|
font-size: 0.82rem;
|
|
cursor: pointer;
|
|
margin: 0;
|
|
}
|
|
.ind-panel label:hover { background: #2a2e39; }
|
|
.ind-panel input[type="checkbox"] {
|
|
accent-color: #2962ff;
|
|
}
|
|
.ind-sep {
|
|
border-top: 1px solid #2a2e39;
|
|
margin: 4px 0;
|
|
}
|
|
|
|
/* AVWAP anchor button */
|
|
.avwap-btn {
|
|
background: #2a2e39;
|
|
color: #d1d4dc;
|
|
border: 1px solid #363a45;
|
|
border-radius: 4px;
|
|
padding: 4px 10px;
|
|
font-size: 0.78rem;
|
|
cursor: pointer;
|
|
display: none;
|
|
}
|
|
.avwap-btn.active {
|
|
background: #2962ff;
|
|
border-color: #2962ff;
|
|
color: #fff;
|
|
}
|
|
.avwap-btn.visible { display: inline-block; }
|
|
|
|
/* Chart containers */
|
|
#main-chart-container { width: 100%; }
|
|
.sub-chart-container {
|
|
width: 100%;
|
|
border-top: 1px solid #2a2e39;
|
|
}
|
|
.sub-chart-label {
|
|
position: absolute;
|
|
top: 4px;
|
|
left: 8px;
|
|
color: #787b86;
|
|
font-size: 0.72rem;
|
|
z-index: 10;
|
|
pointer-events: none;
|
|
}
|
|
.sub-chart-wrapper {
|
|
position: relative;
|
|
display: none;
|
|
}
|
|
.sub-chart-wrapper.visible { display: block; }
|
|
|
|
/* No-data message */
|
|
.no-data-msg {
|
|
color: #787b86;
|
|
text-align: center;
|
|
padding: 80px 20px;
|
|
font-size: 1.1rem;
|
|
}
|
|
</style>
|
|
|
|
<div class="chart-wrapper">
|
|
<!-- Toolbar -->
|
|
<div class="chart-toolbar">
|
|
<form method="get" action="/chart" class="d-flex gap-2 align-items-center" style="margin:0;">
|
|
<label>Instrument</label>
|
|
<select name="instrument" onchange="this.form.submit()">
|
|
{% for inst in instruments %}
|
|
<option value="{{ inst }}" {% if inst == instrument %}selected{% endif %}>{{ inst.replace('_', '/') }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<label>Timeframe</label>
|
|
<select name="granularity" onchange="this.form.submit()">
|
|
{% for g in granularities %}
|
|
<option value="{{ g }}" {% if g == granularity %}selected{% endif %}>{{ g }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</form>
|
|
|
|
<!-- Indicators dropdown -->
|
|
<div class="ind-dropdown">
|
|
<button type="button" class="ind-btn" id="ind-toggle">Indicators</button>
|
|
<div class="ind-panel" id="ind-panel">
|
|
<div class="ind-group">Overlays</div>
|
|
<label><input type="checkbox" data-ind="sma_3"> SMA 3</label>
|
|
<label><input type="checkbox" data-ind="sma_20"> SMA 20</label>
|
|
<label><input type="checkbox" data-ind="sma_50"> SMA 50</label>
|
|
<label><input type="checkbox" data-ind="sma_100"> SMA 100</label>
|
|
<div class="ind-sep"></div>
|
|
<label><input type="checkbox" data-ind="ema_20"> EMA 20</label>
|
|
<label><input type="checkbox" data-ind="ema_50"> EMA 50</label>
|
|
<label><input type="checkbox" data-ind="ema_100"> EMA 100</label>
|
|
<label><input type="checkbox" data-ind="ema_200"> EMA 200</label>
|
|
<div class="ind-sep"></div>
|
|
<label><input type="checkbox" data-ind="session_vwap"> Session VWAP</label>
|
|
<label><input type="checkbox" data-ind="anchored_vwap"> Anchored VWAP</label>
|
|
<div class="ind-sep"></div>
|
|
<div class="ind-group">Chart</div>
|
|
<label><input type="checkbox" data-ind="volume" checked> Volume</label>
|
|
<label><input type="checkbox" data-ind="pivots" checked> Pivot Points</label>
|
|
<div class="ind-sep"></div>
|
|
<div class="ind-group">Separate Panes</div>
|
|
<label><input type="checkbox" data-ind="rsi_14"> RSI 14</label>
|
|
<label><input type="checkbox" data-ind="atr_14"> ATR 14</label>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="button" class="avwap-btn" id="avwap-btn" title="Click a candle to set AVWAP anchor">Set AVWAP Anchor</button>
|
|
</div>
|
|
|
|
{% if error_msg %}
|
|
<div class="no-data-msg">{{ error_msg }}</div>
|
|
{% else %}
|
|
<!-- Main chart -->
|
|
<div id="main-chart-container"></div>
|
|
|
|
<!-- Sub-chart panes -->
|
|
<div class="sub-chart-wrapper" id="rsi-wrapper">
|
|
<div class="sub-chart-label">RSI 14</div>
|
|
<div class="sub-chart-container" id="rsi-chart-container"></div>
|
|
</div>
|
|
<div class="sub-chart-wrapper" id="atr-wrapper">
|
|
<div class="sub-chart-label">ATR 14</div>
|
|
<div class="sub-chart-container" id="atr-chart-container"></div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/lightweight-charts@4/dist/lightweight-charts.standalone.production.js"></script>
|
|
<script>
|
|
(function() {
|
|
var ohlcRaw = {{ ohlc_json | safe }};
|
|
var pivots = {{ pivot_json | safe }};
|
|
var indicators = {{ indicators_json | safe }};
|
|
|
|
if (!ohlcRaw || ohlcRaw.length === 0) {
|
|
var container = document.getElementById('main-chart-container');
|
|
if (container) container.innerHTML = '<div class="no-data-msg">No candle data available for {{ instrument }} / {{ granularity }}.</div>';
|
|
return;
|
|
}
|
|
|
|
// ---- Time conversion: ISO string -> Unix timestamp ----
|
|
function toUnix(isoStr) {
|
|
return Math.floor(new Date(isoStr).getTime() / 1000);
|
|
}
|
|
|
|
// ---- Prepare OHLC data ----
|
|
var ohlcData = ohlcRaw.map(function(r) {
|
|
return { time: toUnix(r.time), open: r.open, high: r.high, low: r.low, close: r.close };
|
|
});
|
|
|
|
// ---- Create main chart ----
|
|
var mainContainer = document.getElementById('main-chart-container');
|
|
var chartHeight = Math.max(500, window.innerHeight - 260);
|
|
|
|
var chart = LightweightCharts.createChart(mainContainer, {
|
|
width: mainContainer.clientWidth,
|
|
height: chartHeight,
|
|
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
|
grid: { vertLines: { color: '#1e222d' }, horzLines: { color: '#1e222d' } },
|
|
crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
|
|
rightPriceScale: { borderColor: '#2B2B43' },
|
|
timeScale: { borderColor: '#2B2B43', timeVisible: true, secondsVisible: false },
|
|
});
|
|
|
|
// Candlestick series
|
|
var candleSeries = chart.addCandlestickSeries({
|
|
upColor: '#26a69a',
|
|
downColor: '#ef5350',
|
|
borderUpColor: '#26a69a',
|
|
borderDownColor: '#ef5350',
|
|
wickUpColor: '#26a69a',
|
|
wickDownColor: '#ef5350',
|
|
});
|
|
candleSeries.setData(ohlcData);
|
|
|
|
// Current price line
|
|
var lastCandle = ohlcData[ohlcData.length - 1];
|
|
if (lastCandle) {
|
|
candleSeries.createPriceLine({
|
|
price: lastCandle.close,
|
|
color: lastCandle.close >= lastCandle.open ? '#26a69a' : '#ef5350',
|
|
lineWidth: 1,
|
|
lineStyle: LightweightCharts.LineStyle.Dotted,
|
|
axisLabelVisible: true,
|
|
title: '',
|
|
});
|
|
}
|
|
|
|
// ---- Series management ----
|
|
var activeSeries = new Map();
|
|
|
|
// Indicator colors
|
|
var indColors = {
|
|
sma_3: '#ff9800', sma_20: '#ab47bc', sma_50: '#00bcd4', sma_100: '#e91e63',
|
|
ema_20: '#ffeb3b', ema_50: '#4caf50', ema_100: '#2196f3', ema_200: '#ff5722',
|
|
session_vwap: '#00e5ff', anchored_vwap: '#ff4081',
|
|
};
|
|
|
|
function convertIndicatorData(dataArr) {
|
|
return dataArr.map(function(d) {
|
|
var obj = { time: toUnix(d.time), value: d.value };
|
|
if (d.color) obj.color = d.color;
|
|
return obj;
|
|
});
|
|
}
|
|
|
|
// ---- Volume series ----
|
|
var volumeSeries = null;
|
|
function addVolume() {
|
|
if (volumeSeries) return;
|
|
if (!indicators.volume) return;
|
|
volumeSeries = chart.addHistogramSeries({
|
|
priceFormat: { type: 'volume' },
|
|
priceScaleId: 'volume',
|
|
});
|
|
chart.priceScale('volume').applyOptions({
|
|
scaleMargins: { top: 0.8, bottom: 0 },
|
|
});
|
|
volumeSeries.setData(convertIndicatorData(indicators.volume));
|
|
}
|
|
function removeVolume() {
|
|
if (!volumeSeries) return;
|
|
chart.removeSeries(volumeSeries);
|
|
volumeSeries = null;
|
|
}
|
|
|
|
// ---- Pivot lines ----
|
|
var pivotLines = [];
|
|
var pivotDefs = [
|
|
{ key: 'r3', color: '#b71c1c', label: 'R3' },
|
|
{ key: 'r2', color: '#e53935', label: 'R2' },
|
|
{ key: 'r1', color: '#ef9a9a', label: 'R1' },
|
|
{ key: 'pivot', color: '#42a5f5', label: 'Pivot' },
|
|
{ key: 's1', color: '#a5d6a7', label: 'S1' },
|
|
{ key: 's2', color: '#43a047', label: 'S2' },
|
|
{ key: 's3', color: '#1b5e20', label: 'S3' },
|
|
];
|
|
|
|
function addPivots() {
|
|
if (pivotLines.length > 0) return;
|
|
pivotDefs.forEach(function(lv) {
|
|
var val = pivots[lv.key];
|
|
if (val == null) return;
|
|
var line = candleSeries.createPriceLine({
|
|
price: val,
|
|
color: lv.color,
|
|
lineWidth: 1,
|
|
lineStyle: lv.key === 'pivot' ? LightweightCharts.LineStyle.Solid : LightweightCharts.LineStyle.Dashed,
|
|
axisLabelVisible: true,
|
|
title: lv.label,
|
|
});
|
|
pivotLines.push(line);
|
|
});
|
|
}
|
|
function removePivots() {
|
|
pivotLines.forEach(function(line) {
|
|
candleSeries.removePriceLine(line);
|
|
});
|
|
pivotLines = [];
|
|
}
|
|
|
|
// ---- Overlay indicator (line on main chart) ----
|
|
function addOverlay(key) {
|
|
if (activeSeries.has(key)) return;
|
|
var data = indicators[key];
|
|
if (!data || data.length === 0) return;
|
|
var series = chart.addLineSeries({
|
|
color: indColors[key] || '#999',
|
|
lineWidth: 1.5,
|
|
priceLineVisible: false,
|
|
lastValueVisible: false,
|
|
crosshairMarkerVisible: false,
|
|
});
|
|
series.setData(convertIndicatorData(data));
|
|
activeSeries.set(key, series);
|
|
}
|
|
function removeOverlay(key) {
|
|
if (!activeSeries.has(key)) return;
|
|
chart.removeSeries(activeSeries.get(key));
|
|
activeSeries.delete(key);
|
|
}
|
|
|
|
// ---- Sub-chart panes (RSI, ATR) ----
|
|
var subCharts = {};
|
|
|
|
function createSubChart(key, containerId, wrapperId, options) {
|
|
if (subCharts[key]) return;
|
|
var wrapper = document.getElementById(wrapperId);
|
|
var container = document.getElementById(containerId);
|
|
wrapper.classList.add('visible');
|
|
|
|
var subChart = LightweightCharts.createChart(container, {
|
|
width: container.clientWidth,
|
|
height: 150,
|
|
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
|
grid: { vertLines: { color: '#1e222d' }, horzLines: { color: '#1e222d' } },
|
|
crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
|
|
rightPriceScale: { borderColor: '#2B2B43' },
|
|
timeScale: { borderColor: '#2B2B43', timeVisible: true, secondsVisible: false },
|
|
});
|
|
|
|
var series = subChart.addLineSeries({
|
|
color: options.color || '#7c4dff',
|
|
lineWidth: 1.5,
|
|
priceLineVisible: false,
|
|
lastValueVisible: true,
|
|
});
|
|
|
|
var data = indicators[key];
|
|
if (data) series.setData(convertIndicatorData(data));
|
|
|
|
// Add reference lines (e.g., overbought/oversold for RSI)
|
|
if (options.refLines) {
|
|
options.refLines.forEach(function(rl) {
|
|
series.createPriceLine({
|
|
price: rl.price,
|
|
color: rl.color || '#787b86',
|
|
lineWidth: 1,
|
|
lineStyle: LightweightCharts.LineStyle.Dashed,
|
|
axisLabelVisible: true,
|
|
title: rl.label || '',
|
|
});
|
|
});
|
|
}
|
|
|
|
// Sync time scale with main chart
|
|
chart.timeScale().subscribeVisibleLogicalRangeChange(function(range) {
|
|
if (range) subChart.timeScale().setVisibleLogicalRange(range);
|
|
});
|
|
subChart.timeScale().subscribeVisibleLogicalRangeChange(function(range) {
|
|
if (range) chart.timeScale().setVisibleLogicalRange(range);
|
|
});
|
|
|
|
// Sync crosshair
|
|
chart.subscribeCrosshairMove(function(param) {
|
|
if (param.time) {
|
|
subChart.setCrosshairPosition(NaN, param.time, series);
|
|
} else {
|
|
subChart.clearCrosshairPosition();
|
|
}
|
|
});
|
|
subChart.subscribeCrosshairMove(function(param) {
|
|
if (param.time) {
|
|
chart.setCrosshairPosition(NaN, param.time, candleSeries);
|
|
} else {
|
|
chart.clearCrosshairPosition();
|
|
}
|
|
});
|
|
|
|
subCharts[key] = { chart: subChart, series: series, wrapper: wrapper };
|
|
}
|
|
|
|
function removeSubChart(key) {
|
|
if (!subCharts[key]) return;
|
|
subCharts[key].chart.remove();
|
|
subCharts[key].wrapper.classList.remove('visible');
|
|
delete subCharts[key];
|
|
}
|
|
|
|
// ---- Anchored VWAP ----
|
|
var avwapActive = false;
|
|
var avwapSeries = null;
|
|
|
|
function computeAnchoredVWAP(anchorIndex) {
|
|
var result = [];
|
|
var cumPV = 0;
|
|
var cumVol = 0;
|
|
for (var i = anchorIndex; i < ohlcRaw.length; i++) {
|
|
var r = ohlcRaw[i];
|
|
var tp = (r.high + r.low + r.close) / 3.0;
|
|
var vol = r.volume || 0;
|
|
cumPV += tp * vol;
|
|
cumVol += vol;
|
|
if (cumVol > 0) {
|
|
result.push({ time: toUnix(r.time), value: cumPV / cumVol });
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function handleAvwapClick(param) {
|
|
if (!avwapActive) return;
|
|
if (!param.time) return;
|
|
|
|
// Find the bar index
|
|
var targetTime = param.time;
|
|
var idx = -1;
|
|
for (var i = 0; i < ohlcRaw.length; i++) {
|
|
if (toUnix(ohlcRaw[i].time) === targetTime) { idx = i; break; }
|
|
}
|
|
if (idx < 0) return;
|
|
|
|
// Remove existing AVWAP
|
|
if (avwapSeries) {
|
|
chart.removeSeries(avwapSeries);
|
|
avwapSeries = null;
|
|
}
|
|
|
|
// Compute and draw
|
|
var avwapData = computeAnchoredVWAP(idx);
|
|
if (avwapData.length > 0) {
|
|
avwapSeries = chart.addLineSeries({
|
|
color: indColors.anchored_vwap,
|
|
lineWidth: 2,
|
|
priceLineVisible: false,
|
|
lastValueVisible: true,
|
|
crosshairMarkerVisible: false,
|
|
});
|
|
avwapSeries.setData(avwapData);
|
|
}
|
|
|
|
// Deactivate anchor mode
|
|
avwapActive = false;
|
|
document.getElementById('avwap-btn').classList.remove('active');
|
|
}
|
|
|
|
chart.subscribeCrosshairMove(function() {}); // ensure subscription exists
|
|
chart.subscribeClick(handleAvwapClick);
|
|
|
|
// ---- Indicator toggle logic ----
|
|
function toggleIndicator(key, enabled) {
|
|
switch(key) {
|
|
case 'volume':
|
|
enabled ? addVolume() : removeVolume();
|
|
break;
|
|
case 'pivots':
|
|
enabled ? addPivots() : removePivots();
|
|
break;
|
|
case 'rsi_14':
|
|
if (enabled) {
|
|
createSubChart('rsi_14', 'rsi-chart-container', 'rsi-wrapper', {
|
|
color: '#7c4dff',
|
|
refLines: [
|
|
{ price: 70, color: '#ef5350', label: 'OB' },
|
|
{ price: 30, color: '#26a69a', label: 'OS' },
|
|
{ price: 50, color: '#787b86', label: '' },
|
|
],
|
|
});
|
|
} else {
|
|
removeSubChart('rsi_14');
|
|
}
|
|
break;
|
|
case 'atr_14':
|
|
if (enabled) {
|
|
createSubChart('atr_14', 'atr-chart-container', 'atr-wrapper', {
|
|
color: '#ffab40',
|
|
refLines: [],
|
|
});
|
|
} else {
|
|
removeSubChart('atr_14');
|
|
}
|
|
break;
|
|
case 'anchored_vwap':
|
|
var btn = document.getElementById('avwap-btn');
|
|
if (enabled) {
|
|
btn.classList.add('visible');
|
|
} else {
|
|
btn.classList.remove('visible');
|
|
btn.classList.remove('active');
|
|
avwapActive = false;
|
|
if (avwapSeries) {
|
|
chart.removeSeries(avwapSeries);
|
|
avwapSeries = null;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
// Overlay indicators (SMA, EMA, session VWAP)
|
|
enabled ? addOverlay(key) : removeOverlay(key);
|
|
}
|
|
}
|
|
|
|
// ---- Initialize defaults (volume + pivots on) ----
|
|
addVolume();
|
|
addPivots();
|
|
|
|
// ---- Wire up checkbox events ----
|
|
document.querySelectorAll('#ind-panel input[type="checkbox"]').forEach(function(cb) {
|
|
cb.addEventListener('change', function() {
|
|
toggleIndicator(this.dataset.ind, this.checked);
|
|
});
|
|
});
|
|
|
|
// ---- Indicator dropdown toggle ----
|
|
var indToggle = document.getElementById('ind-toggle');
|
|
var indPanel = document.getElementById('ind-panel');
|
|
|
|
indToggle.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
indPanel.classList.toggle('open');
|
|
});
|
|
|
|
// Close dropdown when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
if (!indPanel.contains(e.target) && e.target !== indToggle) {
|
|
indPanel.classList.remove('open');
|
|
}
|
|
});
|
|
|
|
// Stop propagation inside panel so clicks don't close it
|
|
indPanel.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
});
|
|
|
|
// ---- AVWAP anchor button ----
|
|
document.getElementById('avwap-btn').addEventListener('click', function() {
|
|
avwapActive = !avwapActive;
|
|
this.classList.toggle('active', avwapActive);
|
|
});
|
|
|
|
// ---- Responsive resize ----
|
|
function handleResize() {
|
|
var newWidth = mainContainer.clientWidth;
|
|
var newHeight = Math.max(500, window.innerHeight - 260);
|
|
chart.applyOptions({ width: newWidth, height: newHeight });
|
|
|
|
Object.keys(subCharts).forEach(function(key) {
|
|
var sc = subCharts[key];
|
|
sc.chart.applyOptions({ width: newWidth });
|
|
});
|
|
}
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
// Fit content on load
|
|
chart.timeScale().fitContent();
|
|
|
|
})();
|
|
</script>
|
|
{% endblock %}
|