powinno działać
This commit is contained in:
@ -190,6 +190,7 @@
|
|||||||
background: var(--tv-panel-bg);
|
background: var(--tv-panel-bg);
|
||||||
border-top: 1px solid var(--tv-border);
|
border-top: 1px solid var(--tv-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Right Sidebar - Strategy Simulation */
|
/* Right Sidebar - Strategy Simulation */
|
||||||
@ -521,12 +522,52 @@
|
|||||||
background: rgba(0,0,0,0.7);
|
background: rgba(0,0,0,0.7);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
flex-direction: column;
|
|
||||||
min-height: 200px;
|
/* Price Scale Controls */
|
||||||
max-height: 400px;
|
.price-scale-controls {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 2px;
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ta-header {
|
.chart-wrapper:hover .price-scale-controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ps-control-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: rgba(42, 46, 57, 0.9);
|
||||||
|
border: 1px solid #363c4e;
|
||||||
|
color: #d1d4dc;
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ps-control-btn:hover {
|
||||||
|
background: #363c4e;
|
||||||
|
border-color: #4a4f5e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ps-control-btn.active {
|
||||||
|
background: #2962ff;
|
||||||
|
border-color: #2962ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ha-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -947,10 +988,14 @@
|
|||||||
<div class="chart-area">
|
<div class="chart-area">
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<div id="chart"></div>
|
<div id="chart"></div>
|
||||||
|
<div class="price-scale-controls" id="priceScaleControls">
|
||||||
|
<button class="ps-control-btn auto-scale active" id="btnAutoScale" title="Auto Scale (A)">A</button>
|
||||||
|
<button class="ps-control-btn log-scale" id="btnLogScale" title="Logarithmic Scale">L</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ta-panel" id="taPanel">
|
<div class="ta-panel" id="taPanel">
|
||||||
<div class="ta-header">
|
<div class="ha-header">
|
||||||
<div class="ta-title">
|
<div class="ta-title">
|
||||||
Technical Analysis
|
Technical Analysis
|
||||||
<span class="ta-interval" id="taInterval">1D</span>
|
<span class="ta-interval" id="taInterval">1D</span>
|
||||||
@ -1006,22 +1051,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-group">
|
<div class="config-group">
|
||||||
<label class="config-label">Timeframe</label>
|
<label class="config-label">Confirmation TF (Optional)</label>
|
||||||
<select id="simTimeframe" class="config-input">
|
|
||||||
<option value="1m">1m</option>
|
|
||||||
<option value="3m">3m</option>
|
|
||||||
<option value="5m">5m</option>
|
|
||||||
<option value="15m">15m</option>
|
|
||||||
<option value="30m">30m</option>
|
|
||||||
<option value="37m" selected>37m</option>
|
|
||||||
<option value="1h">1h</option>
|
|
||||||
<option value="4h">4h</option>
|
|
||||||
<option value="1d">1d</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-group">
|
|
||||||
<label class="config-label">Confirmation TF</label>
|
|
||||||
<select id="simSecondaryTF" class="config-input">
|
<select id="simSecondaryTF" class="config-input">
|
||||||
<option value="">None</option>
|
<option value="">None</option>
|
||||||
<option value="1h">1h</option>
|
<option value="1h">1h</option>
|
||||||
@ -1353,16 +1383,21 @@
|
|||||||
|
|
||||||
// 1. Calculate Indicators for all TFs
|
// 1. Calculate Indicators for all TFs
|
||||||
const indicatorResults = {};
|
const indicatorResults = {};
|
||||||
|
console.log('Calculating indicators for timeframes:', Object.keys(candlesMap));
|
||||||
for (const tf in candlesMap) {
|
for (const tf in candlesMap) {
|
||||||
indicatorResults[tf] = {};
|
indicatorResults[tf] = {};
|
||||||
const tfCandles = candlesMap[tf];
|
const tfCandles = candlesMap[tf];
|
||||||
const tfIndicators = (strategyConfig.indicators || []).filter(ind => (ind.timeframe || primaryTF) === tf);
|
const tfIndicators = (strategyConfig.indicators || []).filter(ind => (ind.timeframe || primaryTF) === tf);
|
||||||
|
|
||||||
|
console.log(` TF ${tf}: ${tfIndicators.length} indicators to calculate`);
|
||||||
|
|
||||||
for (const ind of tfIndicators) {
|
for (const ind of tfIndicators) {
|
||||||
const IndicatorClass = this.indicatorTypes[ind.type];
|
const IndicatorClass = this.indicatorTypes[ind.type];
|
||||||
if (IndicatorClass) {
|
if (IndicatorClass) {
|
||||||
const instance = new IndicatorClass(ind);
|
const instance = new IndicatorClass(ind);
|
||||||
indicatorResults[tf][ind.name] = instance.calculate(tfCandles);
|
indicatorResults[tf][ind.name] = instance.calculate(tfCandles);
|
||||||
|
const validValues = indicatorResults[tf][ind.name].filter(v => v !== null).length;
|
||||||
|
console.log(` Calculated ${ind.name} on ${tf}: ${validValues} valid values`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1371,32 +1406,40 @@
|
|||||||
const risk = new RiskManager(riskConfig);
|
const risk = new RiskManager(riskConfig);
|
||||||
const trades = [];
|
const trades = [];
|
||||||
let position = null;
|
let position = null;
|
||||||
const startTimeMs = new Date(simulationStart).getTime();
|
// Convert simulation start to seconds (to match candle timestamps)
|
||||||
|
const startTimeSec = Math.floor(new Date(simulationStart).getTime() / 1000);
|
||||||
|
console.log('Simulation start (seconds):', startTimeSec, 'Date:', simulationStart);
|
||||||
|
console.log('Total candles available:', candles.length);
|
||||||
|
console.log('First candle time:', candles[0].time, 'Last candle time:', candles[candles.length - 1].time);
|
||||||
|
|
||||||
// Optimized Pointer-based Timeframe Alignment
|
// Optimized Pointer-based Timeframe Alignment
|
||||||
const pointers = {};
|
const pointers = {};
|
||||||
for (const tf in candlesMap) pointers[tf] = 0;
|
for (const tf in candlesMap) pointers[tf] = 0;
|
||||||
|
|
||||||
|
let processedCandles = 0;
|
||||||
|
|
||||||
for (let i = 1; i < candles.length; i++) {
|
for (let i = 1; i < candles.length; i++) {
|
||||||
const time = new Date(candles[i].time).getTime();
|
const time = candles[i].time; // Already in seconds
|
||||||
const price = candles[i].close;
|
const price = candles[i].close;
|
||||||
|
|
||||||
// Skip candles before simulation start (used for indicator warm-up)
|
// Skip candles before simulation start (used for indicator warm-up)
|
||||||
if (time < startTimeMs) {
|
if (time < startTimeSec) {
|
||||||
// Update pointers even for skipped candles
|
// Update pointers even for skipped candles
|
||||||
for (const tf in candlesMap) {
|
for (const tf in candlesMap) {
|
||||||
while (pointers[tf] < candlesMap[tf].length - 1 &&
|
while (pointers[tf] < candlesMap[tf].length - 1 &&
|
||||||
new Date(candlesMap[tf][pointers[tf] + 1].time).getTime() <= time) {
|
candlesMap[tf][pointers[tf] + 1].time <= time) {
|
||||||
pointers[tf]++;
|
pointers[tf]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processedCandles++;
|
||||||
|
|
||||||
// Update pointers to current time
|
// Update pointers to current time
|
||||||
for (const tf in candlesMap) {
|
for (const tf in candlesMap) {
|
||||||
while (pointers[tf] < candlesMap[tf].length - 1 &&
|
while (pointers[tf] < candlesMap[tf].length - 1 &&
|
||||||
new Date(candlesMap[tf][pointers[tf] + 1].time).getTime() <= time) {
|
candlesMap[tf][pointers[tf] + 1].time <= time) {
|
||||||
pointers[tf]++;
|
pointers[tf]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1414,6 +1457,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Simulation complete: ${processedCandles} candles processed after start date, ${trades.length} trades`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total_trades: trades.length,
|
total_trades: trades.length,
|
||||||
win_rate: (trades.filter(t => t.pnl > 0).length / (trades.length || 1)) * 100,
|
win_rate: (trades.filter(t => t.pnl > 0).length / (trades.length || 1)) * 100,
|
||||||
@ -1438,6 +1483,12 @@
|
|||||||
return tfCandles[pointers[tf]].close;
|
return tfCandles[pointers[tf]].close;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Debug logging for first evaluation
|
||||||
|
if (index === 1) {
|
||||||
|
console.log('First candle time:', candles[index].time, 'Date:', new Date(candles[index].time * 1000));
|
||||||
|
console.log('MA44 value:', getVal('ma44', primaryTF));
|
||||||
|
}
|
||||||
|
|
||||||
// Simple logic for MVP strategies
|
// Simple logic for MVP strategies
|
||||||
if (config.id === 'ma44_strategy') {
|
if (config.id === 'ma44_strategy') {
|
||||||
const ma44 = getVal('ma44', primaryTF);
|
const ma44 = getVal('ma44', primaryTF);
|
||||||
@ -1445,7 +1496,15 @@
|
|||||||
|
|
||||||
// Optional: Multi-TF trend filter
|
// Optional: Multi-TF trend filter
|
||||||
const secondaryTF = config.timeframes?.secondary?.[0];
|
const secondaryTF = config.timeframes?.secondary?.[0];
|
||||||
const trendOk = !secondaryTF || (getPrice(secondaryTF) > getVal(`ma44_${secondaryTF}`, secondaryTF));
|
let trendOk = true;
|
||||||
|
if (secondaryTF) {
|
||||||
|
const secondaryPrice = getPrice(secondaryTF);
|
||||||
|
const secondaryMA = getVal(`ma44_${secondaryTF}`, secondaryTF);
|
||||||
|
trendOk = secondaryPrice > secondaryMA;
|
||||||
|
if (index === 1) {
|
||||||
|
console.log(`Trend check: ${secondaryTF} price=${secondaryPrice}, MA=${secondaryMA}, trendOk=${trendOk}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ma44) {
|
if (ma44) {
|
||||||
if (price > ma44 && trendOk) return 'BUY';
|
if (price > ma44 && trendOk) return 'BUY';
|
||||||
@ -1456,8 +1515,21 @@
|
|||||||
if (config.id === 'ma125_strategy') {
|
if (config.id === 'ma125_strategy') {
|
||||||
const ma125 = getVal('ma125', primaryTF);
|
const ma125 = getVal('ma125', primaryTF);
|
||||||
const price = candles[index].close;
|
const price = candles[index].close;
|
||||||
|
|
||||||
|
// Optional: Multi-TF trend filter
|
||||||
|
const secondaryTF = config.timeframes?.secondary?.[0];
|
||||||
|
let trendOk = true;
|
||||||
|
if (secondaryTF) {
|
||||||
|
const secondaryPrice = getPrice(secondaryTF);
|
||||||
|
const secondaryMA = getVal(`ma125_${secondaryTF}`, secondaryTF);
|
||||||
|
trendOk = secondaryPrice > secondaryMA;
|
||||||
|
if (index === 1) {
|
||||||
|
console.log(`Trend check: ${secondaryTF} price=${secondaryPrice}, MA=${secondaryMA}, trendOk=${trendOk}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ma125) {
|
if (ma125) {
|
||||||
if (price > ma125) return 'BUY';
|
if (price > ma125 && trendOk) return 'BUY';
|
||||||
if (price < ma125) return 'SELL';
|
if (price < ma125) return 'SELL';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1582,14 +1654,17 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.candleSeries = this.chart.addCandlestickSeries({
|
this.candleSeries = this.chart.addCandlestickSeries({
|
||||||
upColor: '#26a69a',
|
upColor: '#ff9800',
|
||||||
downColor: '#ef5350',
|
downColor: '#ff9800',
|
||||||
borderUpColor: '#26a69a',
|
borderUpColor: '#ff9800',
|
||||||
borderDownColor: '#ef5350',
|
borderDownColor: '#ff9800',
|
||||||
wickUpColor: '#26a69a',
|
wickUpColor: '#ff9800',
|
||||||
wickDownColor: '#ef5350',
|
wickDownColor: '#ff9800',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize price scale controls
|
||||||
|
this.initPriceScaleControls();
|
||||||
|
|
||||||
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
|
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
@ -1612,6 +1687,93 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initPriceScaleControls() {
|
||||||
|
const btnAutoScale = document.getElementById('btnAutoScale');
|
||||||
|
const btnLogScale = document.getElementById('btnLogScale');
|
||||||
|
|
||||||
|
if (!btnAutoScale || !btnLogScale) return;
|
||||||
|
|
||||||
|
// Track current state
|
||||||
|
this.priceScaleState = {
|
||||||
|
autoScale: true,
|
||||||
|
logScale: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto Scale toggle
|
||||||
|
btnAutoScale.addEventListener('click', () => {
|
||||||
|
this.priceScaleState.autoScale = !this.priceScaleState.autoScale;
|
||||||
|
btnAutoScale.classList.toggle('active', this.priceScaleState.autoScale);
|
||||||
|
|
||||||
|
this.candleSeries.priceScale().applyOptions({
|
||||||
|
autoScale: this.priceScaleState.autoScale
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Auto Scale:', this.priceScaleState.autoScale ? 'ON' : 'OFF');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log Scale toggle
|
||||||
|
btnLogScale.addEventListener('click', () => {
|
||||||
|
this.priceScaleState.logScale = !this.priceScaleState.logScale;
|
||||||
|
btnLogScale.classList.toggle('active', this.priceScaleState.logScale);
|
||||||
|
|
||||||
|
// Get current visible price range and time range before changing mode
|
||||||
|
let currentPriceRange = null;
|
||||||
|
let currentTimeRange = null;
|
||||||
|
if (!this.priceScaleState.autoScale) {
|
||||||
|
try {
|
||||||
|
currentPriceRange = this.candleSeries.priceScale().getVisiblePriceRange();
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not get price range');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
currentTimeRange = this.chart.timeScale().getVisibleLogicalRange();
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not get time range');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply log/linear mode change
|
||||||
|
this.candleSeries.priceScale().applyOptions({
|
||||||
|
mode: this.priceScaleState.logScale ? LightweightCharts.PriceScaleMode.Logarithmic : LightweightCharts.PriceScaleMode.Normal
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force chart recalculation and redraw
|
||||||
|
this.chart.applyOptions({});
|
||||||
|
|
||||||
|
// Restore ranges after mode change
|
||||||
|
setTimeout(() => {
|
||||||
|
// Restore time range (X-axis position)
|
||||||
|
if (currentTimeRange) {
|
||||||
|
try {
|
||||||
|
this.chart.timeScale().setVisibleLogicalRange(currentTimeRange);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not restore time range');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore price range if auto-scale is off
|
||||||
|
if (!this.priceScaleState.autoScale && currentPriceRange) {
|
||||||
|
try {
|
||||||
|
this.candleSeries.priceScale().setVisiblePriceRange(currentPriceRange);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not restore price range');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
console.log('Log Scale:', this.priceScaleState.logScale ? 'ON' : 'OFF');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcut 'A' for auto-scale
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'a' || e.key === 'A') {
|
||||||
|
if (e.target.tagName !== 'INPUT') {
|
||||||
|
btnAutoScale.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
initEventListeners() {
|
initEventListeners() {
|
||||||
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;
|
||||||
@ -1742,17 +1904,22 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Visible range:', visibleRange);
|
|
||||||
|
|
||||||
const data = this.candleSeries.data();
|
const data = this.candleSeries.data();
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
console.log('No data available');
|
console.log('No data available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load when user scrolls close to the left edge (earlier time)
|
// Check if we need to load more historical data
|
||||||
if (visibleRange.from < 20) {
|
// Load when user scrolls within 50 bars of the leftmost visible data
|
||||||
console.log('Near left edge, loading historical data...');
|
const barsFromLeft = visibleRange.from;
|
||||||
|
const totalBars = data.length;
|
||||||
|
|
||||||
|
console.log('Visible range:', { from: visibleRange.from, to: visibleRange.to, barsFromLeft, totalBars });
|
||||||
|
|
||||||
|
// Trigger when within 50 bars of the earliest loaded data
|
||||||
|
if (barsFromLeft < 50) {
|
||||||
|
console.log('Near left edge (within 50 bars), loading historical data...');
|
||||||
const oldestCandle = data[0];
|
const oldestCandle = data[0];
|
||||||
if (oldestCandle) {
|
if (oldestCandle) {
|
||||||
this.loadHistoricalData(oldestCandle.time);
|
this.loadHistoricalData(oldestCandle.time);
|
||||||
@ -1761,17 +1928,52 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadHistoricalData(beforeTime) {
|
async loadHistoricalData(beforeTime) {
|
||||||
if (this.isLoading) return;
|
if (this.isLoading) {
|
||||||
|
console.log('Already loading, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
console.log(`Loading historical data for ${this.currentInterval} before ${beforeTime}`);
|
||||||
|
|
||||||
|
// Get the timestamp of the leftmost visible candle to preserve view
|
||||||
|
const currentData = this.candleSeries.data();
|
||||||
|
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
|
||||||
|
let leftmostTime = null;
|
||||||
|
|
||||||
|
// Save current price range if autoScale is disabled
|
||||||
|
let savedPriceRange = null;
|
||||||
|
const isAutoScale = this.priceScaleState?.autoScale !== false;
|
||||||
|
if (!isAutoScale) {
|
||||||
|
try {
|
||||||
|
savedPriceRange = this.candleSeries.priceScale().getVisiblePriceRange();
|
||||||
|
console.log('Saving price range:', savedPriceRange);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not save price range');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibleRange && currentData.length > 0) {
|
||||||
|
const leftmostIndex = Math.floor(visibleRange.from);
|
||||||
|
if (leftmostIndex >= 0 && leftmostIndex < currentData.length) {
|
||||||
|
leftmostTime = currentData[leftmostIndex].time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch candles before the oldest visible candle
|
// Fetch candles before the oldest visible candle
|
||||||
// Use end parameter to get candles up to (but not including) the oldest candle
|
// Use end parameter to get candles up to (but not including) the oldest candle
|
||||||
const endTime = new Date((beforeTime - 1) * 1000); // 1 second before oldest
|
const endTime = new Date((beforeTime - 1) * 1000); // 1 second before oldest
|
||||||
|
|
||||||
|
console.log(`Loading historical data before ${new Date((beforeTime - 1) * 1000).toISOString()}`);
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&end=${endTime.toISOString()}&limit=500`
|
`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&end=${endTime.toISOString()}&limit=500`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.candles && data.candles.length > 0) {
|
if (data.candles && data.candles.length > 0) {
|
||||||
@ -1788,6 +1990,24 @@
|
|||||||
this.allData.set(this.currentInterval, mergedData);
|
this.allData.set(this.currentInterval, mergedData);
|
||||||
|
|
||||||
this.candleSeries.setData(mergedData);
|
this.candleSeries.setData(mergedData);
|
||||||
|
|
||||||
|
// Scroll back to the same timestamp to keep chart view static
|
||||||
|
if (leftmostTime) {
|
||||||
|
this.chart.timeScale().scrollToTime(leftmostTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore price range if autoScale was disabled
|
||||||
|
if (!isAutoScale && savedPriceRange) {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
this.candleSeries.priceScale().setVisiblePriceRange(savedPriceRange);
|
||||||
|
console.log('Restored price range:', savedPriceRange);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not restore price range');
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Loaded ${chartData.length} historical candles`);
|
console.log(`Loaded ${chartData.length} historical candles`);
|
||||||
} else {
|
} else {
|
||||||
console.log('No more historical data available');
|
console.log('No more historical data available');
|
||||||
@ -1872,6 +2092,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="ta-section">
|
||||||
|
<div class="ta-section-title">AI Insight</div>
|
||||||
|
<div style="font-size: 13px; margin-top: 8px;">
|
||||||
|
${data.ai_placeholder ? data.ai_placeholder.message : 'AI analysis available'}
|
||||||
|
</div>
|
||||||
|
<button class="ta-btn ai-btn" onclick="openAIAnalysis()" style="width: 100%; margin-top: 12px; font-size: 11px;">
|
||||||
|
Analyze
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1900,6 +2130,9 @@
|
|||||||
this.allData.delete(interval);
|
this.allData.delete(interval);
|
||||||
this.loadInitialData();
|
this.loadInitialData();
|
||||||
this.loadTA();
|
this.loadTA();
|
||||||
|
|
||||||
|
// Update simulation panel timeframe display
|
||||||
|
updateTimeframeDisplay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2277,21 +2510,45 @@
|
|||||||
try {
|
try {
|
||||||
const start = new Date(startDateInput);
|
const start = new Date(startDateInput);
|
||||||
const fetchStart = new Date(start.getTime() - 200 * 24 * 60 * 60 * 1000);
|
const fetchStart = new Date(start.getTime() - 200 * 24 * 60 * 60 * 1000);
|
||||||
const interval = document.getElementById('simTimeframe').value;
|
|
||||||
|
// Use chart's current timeframe
|
||||||
|
if (!window.dashboard) {
|
||||||
|
throw new Error('Dashboard not initialized');
|
||||||
|
}
|
||||||
|
const interval = window.dashboard.currentInterval;
|
||||||
const secondaryTF = document.getElementById('simSecondaryTF').value;
|
const secondaryTF = document.getElementById('simSecondaryTF').value;
|
||||||
const riskPercent = parseFloat(document.getElementById('simRiskPercent').value);
|
const riskPercent = parseFloat(document.getElementById('simRiskPercent').value);
|
||||||
const stopLossPercent = parseFloat(document.getElementById('simStopLoss').value);
|
const stopLossPercent = parseFloat(document.getElementById('simStopLoss').value);
|
||||||
|
|
||||||
// Fetch data
|
// Fetch data
|
||||||
const response = await fetch(`/api/v1/candles/bulk?symbol=BTC&start=${fetchStart.toISOString()}&timeframes=${interval}&timeframes=${secondaryTF}`);
|
const timeframes = [interval];
|
||||||
const data = await response.json();
|
if (secondaryTF && secondaryTF !== '') {
|
||||||
|
timeframes.push(secondaryTF);
|
||||||
|
}
|
||||||
|
|
||||||
if (!data.candles || !data.candles[interval]) {
|
const query = new URLSearchParams({ symbol: 'BTC', start: fetchStart.toISOString() });
|
||||||
throw new Error('No candle data available');
|
timeframes.forEach(tf => query.append('timeframes', tf));
|
||||||
|
|
||||||
|
console.log('Fetching candles with query:', query.toString());
|
||||||
|
|
||||||
|
const response = await fetch(`/api/v1/candles/bulk?${query.toString()}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Candle data received:', data);
|
||||||
|
console.log('Looking for interval:', interval);
|
||||||
|
console.log('Available timeframes:', Object.keys(data));
|
||||||
|
|
||||||
|
// The API returns data directly by timeframe, not wrapped in 'candles' property
|
||||||
|
if (!data[interval] || data[interval].length === 0) {
|
||||||
|
throw new Error(`No candle data available for ${interval} timeframe. Check if data exists in database.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const candlesMap = {
|
const candlesMap = {
|
||||||
[interval]: data.candles[interval].map(c => ({
|
[interval]: data[interval].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),
|
||||||
high: parseFloat(c.high),
|
high: parseFloat(c.high),
|
||||||
@ -2300,6 +2557,17 @@
|
|||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add secondary timeframe data if requested and available
|
||||||
|
if (secondaryTF && data[secondaryTF]) {
|
||||||
|
candlesMap[secondaryTF] = data[secondaryTF].map(c => ({
|
||||||
|
time: Math.floor(new Date(c.time).getTime() / 1000),
|
||||||
|
open: parseFloat(c.open),
|
||||||
|
high: parseFloat(c.high),
|
||||||
|
low: parseFloat(c.low),
|
||||||
|
close: parseFloat(c.close)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Build strategy config
|
// Build strategy config
|
||||||
const engineConfig = {
|
const engineConfig = {
|
||||||
id: strategyConfig.id,
|
id: strategyConfig.id,
|
||||||
@ -2307,22 +2575,49 @@
|
|||||||
indicators: []
|
indicators: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('Building strategy config:');
|
||||||
|
console.log(' Primary TF:', interval);
|
||||||
|
console.log(' Secondary TF:', secondaryTF);
|
||||||
|
console.log(' Available candles:', Object.keys(candlesMap));
|
||||||
|
|
||||||
// Add indicator based on strategy
|
// Add indicator based on strategy
|
||||||
if (strategyConfig.id === 'ma44_strategy') {
|
if (strategyConfig.id === 'ma44_strategy') {
|
||||||
|
// Primary timeframe indicator
|
||||||
engineConfig.indicators.push({
|
engineConfig.indicators.push({
|
||||||
name: 'ma44',
|
name: 'ma44',
|
||||||
type: 'sma',
|
type: 'sma',
|
||||||
params: { period: strategyConfig.params.maPeriod || 44 },
|
params: { period: strategyConfig.params.maPeriod || 44 },
|
||||||
timeframe: interval
|
timeframe: interval
|
||||||
});
|
});
|
||||||
|
// Confirmation timeframe indicator (for trend filter)
|
||||||
|
if (secondaryTF) {
|
||||||
|
engineConfig.indicators.push({
|
||||||
|
name: `ma44_${secondaryTF}`,
|
||||||
|
type: 'sma',
|
||||||
|
params: { period: strategyConfig.params.maPeriod || 44 },
|
||||||
|
timeframe: secondaryTF
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (strategyConfig.id === 'ma125_strategy') {
|
} else if (strategyConfig.id === 'ma125_strategy') {
|
||||||
|
// Primary timeframe indicator
|
||||||
engineConfig.indicators.push({
|
engineConfig.indicators.push({
|
||||||
name: 'ma125',
|
name: 'ma125',
|
||||||
type: 'sma',
|
type: 'sma',
|
||||||
params: { period: strategyConfig.params.maPeriod || 125 },
|
params: { period: strategyConfig.params.maPeriod || 125 },
|
||||||
timeframe: interval
|
timeframe: interval
|
||||||
});
|
});
|
||||||
|
// Confirmation timeframe indicator (for trend filter)
|
||||||
|
if (secondaryTF) {
|
||||||
|
engineConfig.indicators.push({
|
||||||
|
name: `ma125_${secondaryTF}`,
|
||||||
|
type: 'sma',
|
||||||
|
params: { period: strategyConfig.params.maPeriod || 125 },
|
||||||
|
timeframe: secondaryTF
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(' Indicators configured:', engineConfig.indicators.map(i => `${i.name} on ${i.timeframe}`));
|
||||||
|
|
||||||
// Run engine
|
// Run engine
|
||||||
const riskConfig = {
|
const riskConfig = {
|
||||||
@ -2709,9 +3004,30 @@
|
|||||||
// Clear existing trade lines
|
// Clear existing trade lines
|
||||||
clearSimulationMarkers();
|
clearSimulationMarkers();
|
||||||
|
|
||||||
trades.forEach(trade => {
|
console.log('Plotting trades:', trades.length);
|
||||||
const entryTime = Math.floor(new Date(trade.entryTime).getTime() / 1000);
|
console.log('First trade entryTime:', trades[0]?.entryTime, 'type:', typeof trades[0]?.entryTime);
|
||||||
const exitTime = Math.floor(new Date(trade.exitTime).getTime() / 1000);
|
|
||||||
|
trades.forEach((trade, i) => {
|
||||||
|
// Trade times are already in seconds (Unix timestamp)
|
||||||
|
// If they're strings (ISO dates), convert to seconds
|
||||||
|
let entryTime, exitTime;
|
||||||
|
|
||||||
|
if (typeof trade.entryTime === 'number') {
|
||||||
|
entryTime = trade.entryTime;
|
||||||
|
} else {
|
||||||
|
entryTime = Math.floor(new Date(trade.entryTime).getTime() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof trade.exitTime === 'number') {
|
||||||
|
exitTime = trade.exitTime;
|
||||||
|
} else {
|
||||||
|
exitTime = Math.floor(new Date(trade.exitTime).getTime() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
console.log('Converted entryTime:', entryTime, 'Date:', new Date(entryTime * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
const pnlSymbol = trade.pnl > 0 ? '+' : '';
|
const pnlSymbol = trade.pnl > 0 ? '+' : '';
|
||||||
|
|
||||||
// Entry Marker - Buy signal (blue)
|
// Entry Marker - Buy signal (blue)
|
||||||
@ -2776,6 +3092,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TIMEFRAME DISPLAY UPDATE
|
||||||
|
// ==========================================
|
||||||
|
function updateTimeframeDisplay() {
|
||||||
|
const display = document.getElementById('simTimeframeDisplay');
|
||||||
|
if (display && window.dashboard) {
|
||||||
|
display.value = window.dashboard.currentInterval.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// STRATEGY LOADING
|
||||||
|
// ==========================================
|
||||||
|
async function loadStrategies() {
|
||||||
|
try {
|
||||||
|
console.log('Fetching strategies from API...');
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/strategies', {
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Strategies loaded:', data);
|
||||||
|
|
||||||
|
if (!data.strategies) {
|
||||||
|
throw new Error('Invalid response format: missing strategies array');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.availableStrategies = data.strategies;
|
||||||
|
renderStrategies(data.strategies);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading strategies:', error);
|
||||||
|
|
||||||
|
let errorMessage = error.message;
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
errorMessage = 'Request timeout - API server not responding';
|
||||||
|
} else if (error.message.includes('Failed to fetch')) {
|
||||||
|
errorMessage = 'Cannot connect to API server - is it running?';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('strategyList').innerHTML =
|
||||||
|
`<div style="color: var(--tv-red); padding: 20px; text-align: center;">
|
||||||
|
${errorMessage}<br>
|
||||||
|
<small style="color: var(--tv-text-secondary);">Check console (F12) for details</small>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// INITIALIZATION
|
// INITIALIZATION
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -2783,6 +3155,7 @@
|
|||||||
window.dashboard = new TradingDashboard();
|
window.dashboard = new TradingDashboard();
|
||||||
restoreSidebarState();
|
restoreSidebarState();
|
||||||
setDefaultStartDate();
|
setDefaultStartDate();
|
||||||
|
updateTimeframeDisplay();
|
||||||
renderSavedSimulations();
|
renderSavedSimulations();
|
||||||
|
|
||||||
// Load strategies
|
// Load strategies
|
||||||
|
|||||||
Reference in New Issue
Block a user